1
0
mirror of synced 2026-02-08 06:00:13 -05:00

Compare commits

...

165 Commits
TAERGP ... bug

Author SHA1 Message Date
Brandon Bayer
c8700453a4 add pages 2020-09-24 12:16:46 -04:00
Brandon Bayer
d623d8cc33 v0.23.1-canary.0 2020-09-23 22:26:52 -04:00
Brandon Bayer
5ca38ca6d8 Fix ES5 compatibility: upgrade superjson to 1.2.2 (#1147)
(patch)
2020-09-23 22:22:08 -04:00
Brandon Bayer
db0128c971 Upgrade react-query to 2.23.0 (#945)
(patch)
2020-09-23 22:14:29 -04:00
Mohamed Shaban
856aa53843 Fix blitz console not closing on first Ctrl+D (#1142)
Co-authored-by: Fran Zekan <zekan.fran369@gmail.com>
Co-authored-by: Brandon Bayer <b@bayer.ws> (patch)
2020-09-23 21:57:13 -04:00
Fran Zekan
f9c3f82b27 Add blitz db seed for seeding data in your database (#678)
Co-authored-by: Brandon Bayer <b@bayer.ws> (minor)
2020-09-23 21:42:32 -04:00
allcontributors[bot]
97d5f396de docs: add drmas as a contributor (#1146)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-23 21:38:33 -04:00
Rudi Yardley
760fba77ab Speed up blitz start by 40% by adding incremental dev cache (#1137)
(patch)
2020-09-23 21:35:18 -04:00
Brandon Bayer
f83ce0bd72 Redirect MAINTAINERS.md to the new page in the docs 2020-09-23 21:07:15 -04:00
Brandon Bayer
7425952616 (newapp) Improve Form typing: remove need to pass schema type (#1144) 2020-09-23 19:55:27 -04:00
Brandon Bayer
7fac0134b3 Add @kitze as a contributor 2020-09-23 10:24:38 -04:00
Brandon Bayer
df46939fe3 back out no-for-of eslint rule
(meta)
2020-09-22 19:45:11 -04:00
Brandon Bayer
9e88b11496 v0.23.0 2020-09-22 19:28:46 -04:00
Ricardo Trejos
c427ae23d4 Add a timeout to the "retrieving freshest dependencies" step for blitz new(#1132)
(patch)
2020-09-21 20:58:32 -04:00
Enrico Schaaf
cb849c5ba9 Fix blitz autocomplete adding alpha warning message to the shell config (#1122)
(patch)
2020-09-21 19:35:34 -04:00
अभिनाश (Avinash)
6e777dd72a add lint rule to prevent Object.fromEntries in core package (#1129)
(meta)
2020-09-21 19:34:16 -04:00
Brandon Bayer
80ab4876f2 Improve error message for missing SESSION_SECRET_KEY (#1136)
(patch)
2020-09-21 16:00:59 -04:00
Brandon Bayer
cd0bf1f970 (newapp) Change signup & login to lowerCase emails before saving and verifying (#1135) 2020-09-21 14:56:38 -04:00
allcontributors[bot]
5f5a5c8ef7 docs: add enricoschaaf as a contributor (#1128)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-19 22:27:51 -04:00
Enrico Schaaf
9a1e0d0de7 Update Taiwind recipe to opt in to future Tailwind CSS changes (#1118)
(recipe)
2020-09-19 22:27:34 -04:00
Brandon Bayer
fb44203510 v0.22.3-canary.0 2020-09-19 22:01:38 -04:00
Brandon Bayer
010057b34c Fix ES5 compatibility (#1124)
(patch)
2020-09-19 21:10:50 -04:00
Brandon Bayer
575e862ae3 Remove prisma client generation from blitz in favor of new package.json schema config (Requires Prisma 2.7.0+ & new field in pkg.json)) (#1121)
(major)
2020-09-19 19:30:03 -04:00
Satoshi Nitawaki
aed6b8875a Fix slack channel name in MAINTAINERS.md (#1117)
(meta)
2020-09-19 10:59:40 -04:00
Satoshi Nitawaki
10b6f859fd Fix blitz new spewing out all the database migration logs (#1116)
(patch)
2020-09-19 10:54:26 -04:00
Brandon Bayer
9ac856c0ee Update @cardotrejos as a contributor 2020-09-19 10:50:09 -04:00
अभिनाश (Avinash)
1ff7f36482 Enable automatic network request cancelation for queries/mutations (#1092)
Co-authored-by: Brandon Bayer <b@bayer.ws> (minor)
2020-09-19 10:47:58 -04:00
Brandon Bayer
df150da37e Fix cli cache file to be in temp directory instead of project directory (#1087)
(patch)
2020-09-19 10:45:37 -04:00
Bhanu Teja Pachipulusu
eb7409c0b3 Add --force flag for blitz db reset command (#1107)
(minor)
2020-09-19 10:44:22 -04:00
Brandon Bayer
1411a1d366 🎉 Add Satoshi Nitawaki as L1 Maintainer (#1119)
(meta)
2020-09-19 10:35:59 -04:00
allcontributors[bot]
ec95cb40de docs: add hardfire as a contributor (#1113)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-18 18:15:07 -04:00
Brandon Bayer
b502d7ea1e Add dotenv-flow to jest config for new apps (#1112)
(newapp)
2020-09-18 18:08:03 -04:00
allcontributors[bot]
74bf2a9e4c docs: add doeixd as a contributor (#1109)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-18 15:36:03 -04:00
Brandon Bayer
1ee637c367 Revert "add dotenv-flow to jest setup file for new apps"
This reverts commit 23a33f1c3d.
2020-09-18 15:15:41 -04:00
Brandon Bayer
c08771b57e Revert "move it to jest config"
This reverts commit 8a468e4f79.
2020-09-18 15:15:01 -04:00
Brandon Bayer
8a468e4f79 move it to jest config 2020-09-18 15:14:31 -04:00
Brandon Bayer
23a33f1c3d add dotenv-flow to jest setup file for new apps 2020-09-18 15:12:37 -04:00
Brandon Bayer
b3767861a2 Fix inability to have a /test page (#1111)
(patch)
2020-09-18 13:46:56 -04:00
Patrick G
65acfff0ff Add test, tests, spec, specs to build hash ignore list (#1102)
Co-authored-by: Brandon Bayer <b@bayer.ws> (patch)
2020-09-18 11:23:45 -04:00
Brandon Bayer
ab3fc26409 Add @sergiodxa as a contributor 2020-09-18 11:11:05 -04:00
Brandon Bayer
7b7039e0e3 bump CI cache key
(ignore)
2020-09-18 11:05:04 -04:00
engelkes-finstreet
e93f24452c Fix blitz generate pages to use kebab-case for all url paths (#1089)
(patch)
2020-09-18 10:21:00 -04:00
Brandon Bayer
63c9375331 add "recipe" and "newapp" type to release patch
(meta)
2020-09-18 10:01:57 -04:00
Brandon Bayer
adfb486004 Tweaks to new app template pkg scripts: add tsc to pre-commit and lint to pre-push (#1104)
(minor)
2020-09-18 09:58:04 -04:00
Brandon Bayer
5828736369 Improve Recipe for Render.com (#1108)
(recipe)
2020-09-18 09:53:25 -04:00
Brandon Bayer
40a93ee62d Add @tsriram as a contributor 2020-09-18 09:52:42 -04:00
Brandon Bayer
8fa82c7661 Revert "Tweaks to new app template pkg scripts: add tsc to pre-commit and lint to pre-push"
This reverts commit 141003df89.
2020-09-17 16:46:01 -04:00
Brandon Bayer
141003df89 Tweaks to new app template pkg scripts: add tsc to pre-commit and lint to pre-push 2020-09-17 16:44:10 -04:00
Brandon Bayer
6ef7b8a2de Improve rendering of errors in generated Final Form <LabeledTextField> (#1095)
(patch)
2020-09-16 17:09:06 -04:00
allcontributors[bot]
6ade33b849 docs: add taylorcjohnson as a contributor (#1093)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-16 09:23:56 -04:00
Taylor Johnson
02d3aa8259 Change the new app schema provider to be just sqlite instead of an array (#1091)
(patch)
2020-09-16 09:23:11 -04:00
Brandon Bayer
1ae2bb3ee3 v0.22.2-canary.0 2020-09-14 22:07:30 -04:00
Brandon Bayer
d84c73d2bb Add @ditorojuan as a contributor 2020-09-14 22:00:56 -04:00
Brandon Bayer
89b55971f1 Add @johnletey as a contributor 2020-09-14 22:00:37 -04:00
Brandon Bayer
fd1856bc7b Add @cktang88 as a contributor 2020-09-14 21:59:28 -04:00
Brandon Bayer
e6dbbababb Add @johncantrell97 as a contributor 2020-09-14 21:59:19 -04:00
Brandon Bayer
3e2b5ddc8e Add @JoseRFelix as a contributor 2020-09-14 21:59:03 -04:00
Brandon Bayer
1b974a0371 Add @obii-bit as a contributor 2020-09-14 21:58:20 -04:00
engelkes-finstreet
8bf9667a15 Fix blitz generate query does not preserve plurality (#1083)
(patch)
2020-09-14 21:50:03 -04:00
yuta0801
94bd4c166c Fix an incorrect error message in blitz start (#1064) 2020-09-14 20:26:55 -04:00
allcontributors[bot]
4ad9f9bdc9 docs: add yuta0801 as a contributor (#1086)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-14 20:25:47 -04:00
yuta0801
13e1526ef5 Add --inspect flag to enable Node.js inspector (#1063)
(minor)
2020-09-14 20:25:36 -04:00
Brandon Bayer
ab3021a371 Significantly improve CLI performance: reduce execute time from 1700ms -> 900ms (#1057)
(patch)
2020-09-14 20:15:50 -04:00
allcontributors[bot]
3e506c1dce docs: add xiaoyu-tamu as a contributor (#1085)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (patch)
2020-09-14 20:13:27 -04:00
Michael Li
85356ca8e8 Fix type error for useQuery/usePaginatedQuery options object (#1059)
(patch)
2020-09-14 20:13:00 -04:00
Brandon Bayer
530ce851e4 Add React Bricks as Seedling Sponsor!
(meta)
2020-09-14 12:13:07 -04:00
Brandon Bayer
d953ef795a v0.22.1 2020-09-12 14:03:09 -04:00
Brandon Bayer
7fcb0945a2 Fix bug in default _app.tsx where Links inside ErrorBoundary don't work (#1054)
(patch)
2020-09-12 13:54:49 -04:00
Brandon Bayer
ab4670c21b v0.22.0 2020-09-11 17:48:30 -04:00
Brandon Bayer
1ae7bf77b2 Tiny tweak to Layout component in new apps (#1051)
(minor)
2020-09-11 17:46:35 -04:00
Brandon Bayer
63e3fe1ccb Add link to signup page to login form for new apps (#1050)
(minor)
2020-09-11 17:03:14 -04:00
Jirka Svoboda
280a2b5c4f refactor(server): Added isTypescript explicit flag, config refactored (#1014)
Co-authored-by: Rudi Yardley <contact@rudiyardley.com> (meta)
2020-09-11 11:16:45 -04:00
Brandon Bayer
bf2734d907 v0.21.2-canary.2 2020-09-11 11:10:32 -04:00
Sigurd Moland Wahl
07341c14d3 Fix bug where test files would cause blitz build to fail (#1045)
(patch)
2020-09-11 11:07:46 -04:00
allcontributors[bot]
e150b67cf4 docs: add PixelsCommander as a contributor (#1048)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-11 11:04:47 -04:00
Denis Radin
1a1722168c Add secureProxy option the the Passport.js adapter (#1033)
(minor)
2020-09-11 11:04:33 -04:00
dependabot[bot]
7f266b0c98 Bump node-fetch from 2.6.0 to 2.6.1 in /packages/generator (#1042)
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (meta)
2020-09-11 09:35:50 -04:00
engelkes-finstreet
6e92a2dfde Update dependencies in new app template (#1036)
(patch)
2020-09-10 22:17:43 -04:00
Ante Primorac
66cd1ec650 Fix: export AppProps as a generic type (#1034)
(patch)
2020-09-10 22:16:51 -04:00
allcontributors[bot]
7c4916324e docs: add engelkes-finstreet as a contributor (#1041)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-10 22:12:32 -04:00
engelkes-finstreet
d747e34853 Add ability to set custom authenticateOptions for the Passport.js adapter (#1024)
(minor)
2020-09-10 22:10:37 -04:00
Brandon Bayer
3afab440c8 Add @sirmyron as a contributor 2020-09-10 22:05:04 -04:00
allcontributors[bot]
e576e6332c docs: add nitaking as a contributor (#1040)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-10 22:03:49 -04:00
Satoshi Nitawaki
60d0c9d0bf Enable passing extra prisma args to blitz db migrate(#1012)
(minor)
2020-09-10 22:03:34 -04:00
Brandon Bayer
8f800d388b Fix UnhandledPromiseRejectionWarning from useQuery during static pre-rendering (#1038)
(patch)
2020-09-10 09:59:38 -04:00
Brandon Bayer
0fb3163ed5 Revert "Fix UnhandledPromiseRejectionWarning from useQuery during blitz build"
This reverts commit a64cf7d62a.
2020-09-10 09:39:01 -04:00
Brandon Bayer
a64cf7d62a Fix UnhandledPromiseRejectionWarning from useQuery during blitz build 2020-09-10 09:37:56 -04:00
Brandon Bayer
ac78d9a6e1 Fix ctx.session.authorize() throwing AuthorizationError instead of AuthenticationError if user logged out. (#1030)
(patch)
2020-09-09 15:13:53 -04:00
Brandon Bayer
46035af2b3 v0.21.2-canary.1 2020-09-09 12:30:14 -04:00
Ante Primorac
3ee531f221 Fix heroku rebuilding on start (#1029)
(patch)
2020-09-09 12:22:39 -04:00
Brandon Bayer
294be124f2 v0.21.2-canary.0 2020-09-09 10:05:41 -04:00
Ante Primorac
b1b8b5f15e Fix Heroku issue where blitz start --production always rebuilds even after running blitz build(#1023)
(patch)
2020-09-08 21:43:49 -04:00
Brandon Bayer
4f1d80f970 Add @madflow as a contributor 2020-09-08 21:35:17 -04:00
Alan Alickovic
622016f4b7 Fix react warnings from Formik in new apps (#1019)
(patch)
2020-09-08 21:16:09 -04:00
Brandon Bayer
ceb0262540 Minor improvements to default auth-utils.ts file (#1021)
(minor)
2020-09-08 14:40:20 -04:00
allcontributors[bot]
8e9f2d097e docs: add clgeoio as a contributor (#999)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-09-03 09:58:20 -04:00
Cody G
155dfaa4da Adds no-use-before-define lint rule (#998)
(meta)
2020-09-03 09:58:00 -04:00
Brandon Bayer
ec0273f09d v0.21.1 2020-09-02 13:23:20 -04:00
Lori Karikari
7a80f2be2f Upgrade Next.js from 9.5.2 to 9.5.3 (#991)
Co-authored-by: Brandon Bayer <b@bayer.ws> (patch)
2020-09-02 13:16:17 -04:00
Brandon Bayer
0821c019bd Fix some cache key issues with useQuery (#995)
(patch)
2020-09-02 12:06:21 -04:00
Brandon Bayer
f6cc8d152e Fix CI by removing browser setting for cypress (#996)
(meta)
2020-09-02 11:37:25 -04:00
Rafael Nunes
2921cb5a85 Moves auth and recipes to features section (#990) 2020-09-02 10:05:21 -04:00
Brandon Bayer
017c1ff813 v0.21.0 2020-09-01 16:22:14 -04:00
Brandon Bayer
eabe54c8ec Fix blitz db reset (#986)
(patch)
2020-09-01 16:20:28 -04:00
Brandon Bayer
ba14973ad3 Improve the useCurrentUser() hook in the new app template (#985)
(minor)
2020-09-01 15:36:00 -04:00
Brandon Bayer
e29786ba24 Fix Undefined function deleteCookie error after session expires (#982)
(patch)
2020-09-01 15:06:29 -04:00
Brady Pascoe
71e3e48f69 Fix husky git hooks not set up properly in new apps (#980)
Co-authored-by: Brandon Bayer <b@bayer.ws> (patch)
2020-09-01 11:09:18 -04:00
Brandon Bayer
c0ef559eae Revert default react version to 0.0.0-experimental-7f28234f8 because of error boundary bug (#981)
(minor)
2020-09-01 10:30:24 -04:00
Brandon Bayer
60a338b730 Upgrade environment variable support to match Next.js (use dotenv-flow) (#979)
(minor)
2020-08-31 21:55:13 -04:00
Weilbyte
b4039ff9af Improve git init for new apps so it can't get stuck #882 (#888)
Co-authored-by: Brandon Bayer <b@bayer.ws> (patch)
2020-08-31 17:36:01 -04:00
Dillon Raphael
f51a0de5dc Fix session.isAuthorized to not throw an error if user not logged in (#977)
(patch)
2020-08-31 17:22:51 -04:00
allcontributors[bot]
5a006fa89f docs: add dillonraphael as a contributor (#978)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-08-31 17:13:36 -04:00
Brandon Bayer
1ecaa5ea76 v0.20.0 2020-08-31 16:23:11 -04:00
allcontributors[bot]
5d0eb4e44e docs: add jschepmans as a contributor (#974)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-08-31 16:20:58 -04:00
Johan Schepmans
1756d57a39 Update "Whats included?" section in the Readme for new apps (#972)
Co-authored-by: Adam Markon <amarkon895@gmail.com>
Co-authored-by: Brandon Bayer <b@bayer.ws> (patch)
2020-08-31 16:20:43 -04:00
Brandon Bayer
2cf1b63895 Fix issues preventing usage with Typescript strict mode (#970)
(patch)
2020-08-31 16:15:13 -04:00
Brandon Bayer
b0c2a58a83 Add pagination to blitz generate by default (#964)
(minor)
2020-08-31 15:59:33 -04:00
Brandon Bayer
188b8d8ae8 Add @brunocrosier as a contributor 2020-08-31 09:22:59 -04:00
Brandon Bayer
fbd5f2815f Add @yhoiseth as a contributor 2020-08-31 09:22:38 -04:00
Steffan
898f39bf8e Change store example to use single query for both pagination and infinite loading (#881)
Co-authored-by: Brandon Bayer <b@bayer.ws> (example)
2020-08-30 18:18:39 -04:00
Brandon Bayer
91f5056b9d Fix bug when using same query for usePaginatedQuery and useInfiniteQuery (#963)
(patch)
2020-08-30 17:11:48 -04:00
Brandon Bayer
a00a529387 v0.19.0 2020-08-30 16:03:14 -04:00
Brandon Bayer
b5d564fab8 v0.18.1-canary.1 2020-08-30 15:42:39 -04:00
Brandon Bayer
be19dbccf8 add missing dependency
(ignore)
2020-08-30 15:38:58 -04:00
Brandon Bayer
b21df113a8 v0.18.1-canary.0 2020-08-30 15:32:11 -04:00
Brandon Bayer
4e0abb6ca6 Fix useInfiniteQuery() regression in 0.18.0 (#962)
* fix useInfiniteQuery

* tweak (patch)
2020-08-30 15:28:26 -04:00
Brandon Bayer
9cbec551f8 Upgrade tsdx and some other dependencies (#960)
* upgrade tsdx and some other dependencies

* add missing dep (meta)
2020-08-29 17:26:51 -04:00
Brandon Bayer
7531cf66f7 Upgrade default react version to 0.0.0-experimental-94c0244ba (#961)
(minor)
2020-08-29 17:11:07 -04:00
allcontributors[bot]
cf0d77e010 docs: add alan2207 as a contributor (#959)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-08-29 15:40:09 -04:00
Alan Alickovic
713f20d494 Add isLoading property to useSession() hook result (#936)
(minor)
2020-08-29 15:39:53 -04:00
Brandon Bayer
19293c1efb Add .blitz to modulePathIgnorePatterns in Jest config for new apps (#946)
(patch)
2020-08-25 18:50:11 -04:00
George Karagkiaouris
2466a6b98c Automatically run initial db migrations during new app setup (#927)
Co-authored-by: Brandon Bayer <b@bayer.ws> (minor)
2020-08-25 17:32:30 -04:00
allcontributors[bot]
de8e0a6808 docs: add svobik7 as a contributor (#944)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-08-25 17:29:54 -04:00
Jirka Svoboda
8b9252c697 Fix queries and mutations for plain JS projects (#878)
(patch)
2020-08-25 17:29:23 -04:00
allcontributors[bot]
12c0f72243 docs: add karaggeorge as a contributor (#943)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-08-25 17:19:47 -04:00
George Karagkiaouris
5e43599338 Add autocomplete to the Blitz CLI (#928)
* Add autocomplete

* Pin plugin version (minor)
2020-08-25 17:19:18 -04:00
George Karagkiaouris
1bf5bf0492 Fix jest config for new apps and fix example test (#924)
(patch)
2020-08-25 17:17:55 -04:00
Dylan Brookes
ff9f70daa8 Fix mdx files not working (#935)
Co-authored-by: merelinguist <merelinguist@users.noreply.github.com> (patch)
2020-08-25 17:09:43 -04:00
Brandon Bayer
89676e4ba8 Elias retired from L1 maintainer 2020-08-25 15:08:43 -04:00
allcontributors[bot]
679efef951 docs: add bpas247 as a contributor (#934)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-08-22 23:30:17 -04:00
Brady Pascoe
bcd9e8dbc9 Add Formik as form option when creating a new app (#926)
(minor)
2020-08-22 23:29:56 -04:00
allcontributors[bot]
8b0fa91233 docs: add karaggeorge as a contributor (#933)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-08-22 20:58:36 -04:00
George Karagkiaouris
e55e90c345 Fix url encoding for authError in the Passport.js adapter (#925)
(patch)
2020-08-22 20:57:18 -04:00
George Karagkiaouris
4e81c97b2b Fix regex to include dynamic imports (#931) 2020-08-22 18:00:17 +10:00
Dwight Watson
16b414bd3d Remove favicon from generators (#921)
Co-authored-by: Dylan Brookes <24858006+merelinguist@users.noreply.github.com>
2020-08-21 18:36:45 +01:00
Dylan Brookes
8798c9fb3d Fix Tailwind Recipe purging in production (#930)
(ignore)
2020-08-21 08:58:46 -04:00
Brandon Bayer
9f91767246 fix couple dependency version bugs in recipes
(ignore)
2020-08-19 20:45:15 -04:00
Brandon Bayer
a6878b35d0 v0.18.0 2020-08-19 20:40:50 -04:00
Adam Markon
39c3b27038 Migrate Recipes to jscodeshift and Support conditional JSX expressions (#898)
Co-authored-by: Brandon Bayer <b@bayer.ws> (minor)
2020-08-19 20:38:49 -04:00
Brandon Bayer
5cc54ab981 v0.17.1-canary.7 2020-08-19 17:35:52 -04:00
Brandon Bayer
91034216a5 Add prompt for form library when running blitz new (#917)
(minor)
2020-08-19 16:51:21 -04:00
Brandon Bayer
2c78ead4cd Finish adding new Layout component to auth pages in new app template (#919)
(patch)
2020-08-19 15:52:34 -04:00
Brandon Bayer
a0cf98bc47 Some improvements to auth in new app template (#916)
(minor)
2020-08-19 15:07:09 -04:00
Brandon Bayer
6898ead0a5 [RFC] Blitz Auth Session Management (#475)
(ignore)
2020-08-19 14:10:30 -04:00
Weilbyte
c7843e8077 Fix to allow usage without db/index.(ts|js) file (#906)
(patch)
2020-08-19 14:08:15 -04:00
Brandon Bayer
370486291a v0.17.1-canary.6 2020-08-18 21:08:57 -04:00
Brandon Bayer
e26e04c603 add ability to disable CSRF protection (#915) 2020-08-18 20:33:04 -04:00
Brandon Bayer
1fa7d17587 Fix: Refetch all queries if session changes (#914)
(patch)
2020-08-18 20:32:44 -04:00
Brandon Bayer
27bc2e49c4 Change ctx.session.userId type to be any (#912)
(patch)
2020-08-18 18:17:00 -04:00
Dwight Watson
18beb6fb1d Add Persistent layout to new app template with <Layout> and getLayout() (#897)
Co-authored-by: Brandon Bayer <b@bayer.ws> (minor)
2020-08-18 17:58:40 -04:00
allcontributors[bot]
5f68bda686 docs: add cardotrejos as a contributor (#911)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-08-18 17:51:29 -04:00
Ricardo Trejos
81ec5c8528 Bump Prisma Version to Latest in Examples (#866) (#893)
Co-authored-by: Brandon Bayer <b@bayer.ws> (example)
2020-08-18 17:51:10 -04:00
Simon Edelmann
0d0c808cc3 Change useQuery to only retry on network errors by default (#908)
(patch)
2020-08-18 16:47:24 -04:00
allcontributors[bot]
097a2196f3 docs: add Weilbyte as a contributor (#907)
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> (meta)
2020-08-18 12:23:48 -04:00
Weilbyte
c592b04c18 Add --no-git flag to blitz new #889 (WIP) (#905)
(minor)
2020-08-18 12:23:25 -04:00
194 changed files with 7386 additions and 3360 deletions

View File

@@ -357,7 +357,7 @@
]
},
{
"login": "skn0tt",
"login": "Skn0tt",
"name": "Simon Knott",
"avatar_url": "https://avatars1.githubusercontent.com/u/14912729?v=4",
"profile": "http://simonknott.de",
@@ -565,15 +565,6 @@
"code"
]
},
{
"login": "jletey",
"name": "John Letey",
"avatar_url": "https://avatars1.githubusercontent.com/u/62398724?v=4",
"profile": "https://github.com/jletey",
"contributions": [
"code"
]
},
{
"login": "pixelmord",
"name": "Andreas Adam",
@@ -879,6 +870,304 @@
"contributions": [
"code"
]
},
{
"login": "Weilbyte",
"name": "Weilbyte",
"avatar_url": "https://avatars1.githubusercontent.com/u/43392677?v=4",
"profile": "https://github.com/Weilbyte",
"contributions": [
"code",
"doc"
]
},
{
"login": "cardotrejos",
"name": "Ricardo Trejos",
"avatar_url": "https://avatars1.githubusercontent.com/u/8602086?v=4",
"profile": "http://ricardotrejos.tech",
"contributions": [
"code",
"doc"
]
},
{
"login": "karaggeorge",
"name": "George Karagkiaouris",
"avatar_url": "https://avatars0.githubusercontent.com/u/8822835?v=4",
"profile": "https://gkaragkiaouris.tech/",
"contributions": [
"code",
"doc"
]
},
{
"login": "bpas247",
"name": "Brady Pascoe",
"avatar_url": "https://avatars0.githubusercontent.com/u/18705892?v=4",
"profile": "https://www.linkedin.com/in/brady-pascoe-3bba6b13a/",
"contributions": [
"code"
]
},
{
"login": "svobik7",
"name": "Jirka Svoboda",
"avatar_url": "https://avatars1.githubusercontent.com/u/761766?v=4",
"profile": "https://www.yeahcoach.com",
"contributions": [
"code"
]
},
{
"login": "alan2207",
"name": "Alan Alickovic",
"avatar_url": "https://avatars3.githubusercontent.com/u/12713315?v=4",
"profile": "https://github.com/alan2207",
"contributions": [
"code",
"doc"
]
},
{
"login": "yhoiseth",
"name": "Yngve Høiseth",
"avatar_url": "https://avatars0.githubusercontent.com/u/8469540?v=4",
"profile": "https://yngve.hoiseth.net",
"contributions": [
"doc"
]
},
{
"login": "brunocrosier",
"name": "Bruno Crosier",
"avatar_url": "https://avatars1.githubusercontent.com/u/18399089?v=4",
"profile": "https://twitter.com/bruno_crosier",
"contributions": [
"doc"
]
},
{
"login": "jschepmans",
"name": "Johan Schepmans",
"avatar_url": "https://avatars2.githubusercontent.com/u/5782977?v=4",
"profile": "https://github.com/jschepmans",
"contributions": [
"code"
]
},
{
"login": "dillonraphael",
"name": "Dillon Raphael",
"avatar_url": "https://avatars0.githubusercontent.com/u/3496193?v=4",
"profile": "https://twitter.com/dillonraphael",
"contributions": [
"code"
]
},
{
"login": "clgeoio",
"name": "Cody G",
"avatar_url": "https://avatars2.githubusercontent.com/u/37571416?v=4",
"profile": "https://github.com/clgeoio",
"contributions": [
"code"
]
},
{
"login": "madflow",
"name": "madflow",
"avatar_url": "https://avatars0.githubusercontent.com/u/183248?v=4",
"profile": "https://github.com/madflow",
"contributions": [
"doc"
]
},
{
"login": "nitaking",
"name": "Satoshi Nitawaki",
"avatar_url": "https://avatars2.githubusercontent.com/u/10850034?v=4",
"profile": "https://twitter.com/nitaking_",
"contributions": [
"code",
"maintenance",
"question"
]
},
{
"login": "sirmyron",
"name": "sirmyron",
"avatar_url": "https://avatars2.githubusercontent.com/u/1430136?v=4",
"profile": "https://github.com/sirmyron",
"contributions": [
"doc"
]
},
{
"login": "engelkes-finstreet",
"name": "engelkes-finstreet",
"avatar_url": "https://avatars1.githubusercontent.com/u/36962022?v=4",
"profile": "https://github.com/engelkes-finstreet",
"contributions": [
"code",
"doc"
]
},
{
"login": "PixelsCommander",
"name": "Denis Radin",
"avatar_url": "https://avatars2.githubusercontent.com/u/810671?v=4",
"profile": "http://twitter.com/pixelscommander",
"contributions": [
"review",
"code",
"doc"
]
},
{
"login": "xiaoyu-tamu",
"name": "Michael Li",
"avatar_url": "https://avatars3.githubusercontent.com/u/33362998?v=4",
"profile": "https://github.com/xiaoyu-tamu",
"contributions": [
"code"
]
},
{
"login": "yuta0801",
"name": "yuta0801",
"avatar_url": "https://avatars2.githubusercontent.com/u/21266306?v=4",
"profile": "https://github.com/yuta0801",
"contributions": [
"code"
]
},
{
"login": "Obii-bit",
"name": "Obadja Ris",
"avatar_url": "https://avatars2.githubusercontent.com/u/67339820?v=4",
"profile": "https://github.com/Obii-bit",
"contributions": [
"doc"
]
},
{
"login": "JoseRFelix",
"name": "Jose Felix ",
"avatar_url": "https://avatars2.githubusercontent.com/u/21092519?v=4",
"profile": "http://jfelix.info",
"contributions": [
"code"
]
},
{
"login": "johncantrell97",
"name": "John Cantrell",
"avatar_url": "https://avatars3.githubusercontent.com/u/41305919?v=4",
"profile": "https://github.com/johncantrell97",
"contributions": [
"code"
]
},
{
"login": "cktang88",
"name": "Kwuang Tang",
"avatar_url": "https://avatars1.githubusercontent.com/u/10319942?v=4",
"profile": "http://kwuang.me",
"contributions": [
"code"
]
},
{
"login": "johnletey",
"name": "John Letey",
"avatar_url": "https://avatars1.githubusercontent.com/u/62398724?v=4",
"profile": "https://github.com/johnletey",
"contributions": [
"code"
]
},
{
"login": "ditorojuan",
"name": "Juan Di Toro",
"avatar_url": "https://avatars0.githubusercontent.com/u/22530892?v=4",
"profile": "https://github.com/ditorojuan",
"contributions": [
"code"
]
},
{
"login": "taylorcjohnson",
"name": "Taylor Johnson",
"avatar_url": "https://avatars0.githubusercontent.com/u/10552296?v=4",
"profile": "https://github.com/taylorcjohnson",
"contributions": [
"code",
"doc"
]
},
{
"login": "tsriram",
"name": "Sriram Thiagarajan",
"avatar_url": "https://avatars3.githubusercontent.com/u/450559?v=4",
"profile": "https://twitter.com/tsriram",
"contributions": [
"doc"
]
},
{
"login": "sergiodxa",
"name": "Sergio Xalambrí",
"avatar_url": "https://avatars2.githubusercontent.com/u/1312018?v=4",
"profile": "https://sergiodxa.com",
"contributions": [
"doc"
]
},
{
"login": "doeixd",
"name": "Patrick G",
"avatar_url": "https://avatars3.githubusercontent.com/u/13461122?v=4",
"profile": "https://github.com/doeixd",
"contributions": [
"code"
]
},
{
"login": "hardfire",
"name": "अभिनाश (Avinash)",
"avatar_url": "https://avatars3.githubusercontent.com/u/513457?v=4",
"profile": "http://avinash.com.np",
"contributions": [
"code"
]
},
{
"login": "enricoschaaf",
"name": "Enrico Schaaf",
"avatar_url": "https://avatars1.githubusercontent.com/u/54645197?v=4",
"profile": "http://enricoschaaf.com",
"contributions": [
"code"
]
},
{
"login": "kitze",
"name": "Kitze",
"avatar_url": "https://avatars0.githubusercontent.com/u/1160594?v=4",
"profile": "http://kitze.io",
"contributions": [
"ideas"
]
},
{
"login": "drmas",
"name": "Mohamed Shaban",
"avatar_url": "https://avatars3.githubusercontent.com/u/644440?v=4",
"profile": "https://github.com/drmas",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -24,6 +24,7 @@ module.exports = {
},
],
"@typescript-eslint/no-floating-promises": "error",
"no-use-before-define": ["error", {functions: false, classes: false}],
},
ignorePatterns: ["packages/cli/", "packages/generator/templates", ".eslintrc.js"],
overrides: [

View File

@@ -32,9 +32,9 @@ jobs:
**/node_modules
/home/runner/.cache/Cypress
C:\Users\runneradmin\AppData\Local\Cypress\Cache
key: ${{ runner.os }}-yarn-v2-${{ hashFiles('yarn.lock') }}
key: ${{ runner.os }}-yarn-v3-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-v2-
${{ runner.os }}-yarn-v3-
- name: Install dependencies
run: yarn install --frozen-lockfile --silent
env:

8
.gitignore vendored
View File

@@ -15,10 +15,8 @@ tsconfig.tsbuildinfo
dist
.now
# local env files
**/.envrc
**/.env
**/.env.local
**/.env.development.local
**/.env.test.local
**/.env.production.local
**/.env.*.local
**/.envrc
.blitz-*
.blitz-cli-cache

View File

@@ -1,104 +1 @@
# ❤️ Blitz Maintainers
Aside from the core team, there are two levels of maintainers, described below.
## Becoming a Maintainer
We always need more level 1 maintainers! The main requirement is that you can show empathy when communicating online. We'll train you as needed on the other specifics. **This is a great role if you have limited time, because you can spend just as much time as you have without any ongoing responsibilities (unlike level 2)**
Level 2 maintainers have a much higher responsibility, so usually you will spend time as a level 1 maintainer before moving to level 2.
Please DM a core team member (Brandon Bayer, Rudi Yardley, or Dylan Brookes) in Slack if you're interested in becoming an official maintainer!
## Level 1 Maintainers
Level 1 maintainers are critical for a healthy Blitz community and project. They take a lot of burden off the core team and level 2 maintainers so they can focus on higher level things with longer term impact.
The primary responsibilities of level 1 maintainers are:
- Being a friendly, welcoming voice for the Blitz community
- Issue triage
- Pull request triage
- Monitor and answer the `#-help` slack channel
- Community encouragement
- Community moderation
- Tracking and ensuring progress of key issues
## Level 2 Maintainers
Level 2 maintainers are the backbone of the project. They are watchdogs over the code, ensuring code quality, correctness, and security. They also facilitate a rapid pace of progress.
The primary responsibilities of level 2 maintainers are:
- Code ownership over specific parts of the project
- Maintaining and improving the architecture of what they own
- Final pull request reviews
- Merging pull requests
- Tracking and ensuring progress of open pull requests
## ⚠️ Fundamentals
Maintainers are the face of the project and the front-line touch point for the community. Therefore maintainers have the very important responsibility of making people feel welcome, valued, understood, and appreciated.
**Please take time to read and understand everything outlined in this [guide on building welcoming communities](https://opensource.guide/building-community)**
Some especially important points:
- **Gratitude:** immediately express gratitude when someone opens an issue or PR. This takes effort/time and we appreciate it
- **Responsiveness:** during issue/PR triage, even if we cant do a full review right away, leave a comment thanking them and saying well review it soon
- **Understanding:** it's critical to ensure you understand exactly what someone is saying before you respond. Ask plenty of questions if needed. It's very bad if someone has to reply to your response and say "actually I was asking about X"
- In fact, at least one question is almost always required before you can respond appropriately  — whether in Github or in Slack
## Slack
- All `#-*` channels are for Blitz users
- All `#dev-*` channels are for Blitz internal development
If someone that's not a maintainer post in the wrong area, that's fine. Don't tell them they posted in the wrong place. But as a maintainer, you should for sure post in the right channel :)
## Issue Triage
#### If a bug report:
- Does it have enough information? Versions? Logs? Some way to reproduce?
- Has this already been fixed in a previous release?
- Is there already an existing issue for this?
### If a feature/change request:
- Is it clear what the request is and what the benefit will be?
- Is this an obvious win for Blitz? Then accept it
- If not obvious, then pull in a core team member or level 2 maintainer for more review
### Actions
1. Add tags:
- Add a `kind/*` tag
- Add a `scope/*` tag
- Add a `status/*` tag
- Add a good first/second issue tag if appropriate
## Pull Request Triage
- Are the changes covered by tests?
- Do the changes look ok? Make sure there's no obvious issues
### Actions
1. Kindly request any changes if needed
2. Else add a Github approval so that level 2 maintainers know it's already had an initial review
## Final PR Review & Merging (Level 2 maintainers)
As a level 2 maintainer, it is your responsibility to make sure broken code and regressions never reach the canary branch.
1. Ensure the PR'ed code fully works as intended and that there are no regressions in related code
1. If not fully covered by automated tests, you need to pull down the code locally and manually verify everything (the Github CLI helps with this!)
2. During squash & merge:
1. Change the commit title to be public friendly - this exact text will go in the release notes
2. Add the commit type in the description, in parenthesis like `(patch)`. Commit types:
- `major` - major breaking change
- `minor` - minor feature addition
- `patch` - patches, bug fixes, perf improvements, etc
- `example` - change to an example app
- `meta` - internal meta change related to the Blitz repo/project
This document has moved here: https://blitzjs.com/docs/maintainers

View File

@@ -1,21 +1,19 @@
# 2020-08-17 Blitz Contributor Call
- Attending: Brandon Bayer, Adam Markon, Kellen Mace, Myron Davis, Dwight Watson
- Brandon:
- Auth out, set up by default in canary release
- Need to work on a potential CSRF bug
- Next major release will include auth by default and allow you to choose your form library
- Next auth features are email confirmation
- After that, logging and plugins are next
- Auth out, set up by default in canary release
- Need to work on a potential CSRF bug
- Next major release will include auth by default and allow you to choose your form library
- Next auth features are email confirmation
- After that, logging and plugins are next
- Adam:
- Overhauled the recipe infrastructure. Now using jscodeshift instead of recast
- Added support for conditional JSX in templates
- Going to work on custom templates next
- Overhauled the recipe infrastructure. Now using jscodeshift instead of recast
- Added support for conditional JSX in templates
- Going to work on custom templates next
- Dwight
- Has been opening issues for problems
- Made a few PRs for some issues
- Has been opening issues for problems
- Made a few PRs for some issues
# 2020-07-07 Blitz Contributor Call

View File

@@ -6,7 +6,7 @@
<img alt="" src="https://img.shields.io/badge/Join%20our%20community-6700EB.svg?style=for-the-badge&labelColor=000000&logoWidth=20&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQ9SURBVHgB7d3dVdtAEIbhcSpICUoH0IEogQqSVBBSAU4FSSpIOoAORAfQgSghHXzZ1U/YcMD4R9rZmf2ec3y448LyiNf27iLiGIAmPLrweC9Un3DhrzG6EarLNP09nlwJ1SOZ/lQr5N80/S/p2QMVCBf5N17XCfm1Y/rBHqjAG9PPHvBsz+mf9WAP+HLA9M/YA14cOP2payH7jpj+VCtk1wnTP+vj7xCy6cTpn7EHLMLp059iD1iD8eveJbVCNsSLheX1YA/YgOWnf8YeKB3Wmf7Ud6Fy4f/FHmtpxbl3YlC4MJ/Cj0bWdwPnPbARg+L0S54XQHS32WwuxClzd4CM0z9rPfeAuTtA5ulPXYQ7wZ04Y+oOoDD9KZc9YOoOoDj9s4dwFzgXR6w1wIPoOvPWA9buAHEJ173o3gWiy3AnuBUHLEbgmYwvAk1/wuM8vAgexThzbwPDkx7/DHwVXfFOxP2GmsKd4Ab6zPeAyU8CI7AHFmH2BRCBPXAyk18GzUrqAXCTiR4ssyj0VFw/oCU8+e+RZ33AWz6KMaYbIIWxB+JSLs1bsbkeMN0AqakHvoku9oA2sAfqBvbAQdw0QArsgb25aYBUQT3QgT2gB+yBuqGcHij2UCqXDZACe2Anlw2QYg/QAOyBuoE98CL3DZDCuK4/rh/Q7oGL6U+TOvcNkJoijN8X1C48+T+g75eQDrAH/qmqAVJgDwyqaoAUe4AGYA/UDZX3QLUNkEIZPRCd5+6BahsgVUgPROwBTSijB7jpVAvGHriHvmw9wAZ4BpX1ABvgmakHtPcbRuwBTWAPULgAV9D/jKDY9YRvwvgEaurD44uQHvAol7qBW7WKluVtIHiUS7GyvA0s6CiXDnxrpQfsgbqBS7GKk/2jYHCrVlGyfxTMrVo0ALdq1Q3sgSKofh0M9oA61a+D2QM0AHugbmAPqClmSRjK2apVVQ8UsySsoK1aHdgDesCtWnUDeyCrIpeFg1u3sylyWTi3btMA7IG6gT2wuuK3hoE9sKrit4YVslWLPaAN7IG6ocKt2zmY2h4O9sDiTG0PZw/QANy6XTewBxZj9ogYVHy025LMHhEz9cBn0We6B0yfERReBLfhx0/R1YQHPx/QBPbA0VwcEwf2wNFcHBPHHjiem3MC2QPHcXdSaJjA+KfgTPQ8hhfjBzHC40mhlzJ+Xq9lK4a4PCs43AVaGTed5mZq+iOXZwWHi3AnOj2wFWNcnxYe7gTxLtBKHuamP/J+Wnh8a5irB7ZC5Yk9gPX1QuXC+usHWqGyhYvUYR0a7zboUOFCNVhnk0krZAOW7wFOvzXhom2xnEbIHizTA1wEYhWW6YFGyC6c1gOcfg9wfA80Qj7g8B7g9HuCww+haIR8wf49wOn3Cvv9k8tGyC/s7gFOv3fY3QONkH+v9MBWqB7PeqDn9FcIT//kcitUn6kHOu/T/xfWzlQy3dEHhwAAAABJRU5ErkJggg==">
</a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-91-17BB8A.svg?style=for-the-badge&labelColor=000000"></a>
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-122-17BB8A.svg?style=for-the-badge&labelColor=000000"></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
<a aria-label="License" href="https://github.com/blitz-js/blitz/blob/canary/LICENSE">
<img alt="" src="https://img.shields.io/npm/l/blitz.svg?style=for-the-badge&labelColor=000000&color=blue">
@@ -65,12 +65,12 @@ Run `npm install -g blitz`
⚡️ CLI with code scaffolding, Rails-style console REPL, etc<br>
⚡️ GraphQL Ready<br>
⚡️ Deploy serverless or serverful<br>
**Other key features coming:**<br>
⚡️ Highly secure authentication <br>
⚡️ Authorization you can use on both server and client<br>
⚡️ Recipes for easily adding libraries like Tailwind, CSS-in-JS, etc.<br>
**Other key features coming:**<br>
⚡️ Model validation you can use on both server and client<br>
⚡️ Plugins for easily adding libraries like Tailwind, CSS-in-JS, etc.<br>
⚡️ React native support<br>
⚡️ GUI so you don't have to use the CLI<br>
@@ -116,23 +116,35 @@ Your financial contributions help ensure Blitz continues to be developed and mai
👉 View options and contribute at [GitHub Sponsors](https://github.com/sponsors/blitz-js), [PayPal](https://paypal.me/thebayers), or [Open Collective](https://opencollective.com/blitzjs)
### 🌱 Seedling Sponsors
<a aria-label="React Bricks" href="https://reactbricks.com/?utm_source=blitzjs&utm_medium=sponsorship&utm_campaign=blitzjs_sponsorship">
<img alt="" src="https://reactbricks.com/reactbricks_icon.svg" width="30px">
</a>
### 🥉 Bronze Sponsors
<a aria-label="Your Company" href="#">
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="100px">
</a>
### 🥈 Silver Sponsors
<a aria-label="Fauna" href="https://dashboard.fauna.com/accounts/register?utm_source=BlitzJS&utm_medium=sponsorship&utm_campaign=BlitzJS_Sponsorship_2020">
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/Fauna_Logo_Blue.png" width="175px">
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/Fauna_Logo_Blue.png" width="200px">
</a>
### 🏆 Gold Sponsors
<a aria-label="Your Company" href="#">
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="300px">
</a>
### 💎 Diamond Sponsors
<a aria-label="Your Company" href="#">
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="400px">
</a>
<br>
@@ -173,8 +185,6 @@ _Code ownership, pull request approvals and merging, etc_ (see [MAINTAINERS.md](
_Issue triage, pull request triage, community encouragement and moderation, etc_ (see [MAINTAINERS.md](./MAINTAINERS.md))
We need more woman & nonbinary level 1 maintainers. See [MAINTAINERS.md](./MAINTAINERS.md) for what this entails
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
@@ -183,14 +193,14 @@ We need more woman & nonbinary level 1 maintainers. See [MAINTAINERS.md](./MAINT
<td align="center"><a href="https://github.com/LoriKarikari"><img src="https://avatars1.githubusercontent.com/u/7902980?v=4" width="100px;" alt=""/><br /><sub><b>Lori Karikari</b></sub></a></td>
<td align="center"><a href="https://corey-brown.com"><img src="https://avatars1.githubusercontent.com/u/12791148?v=4" width="100px;" alt=""/><br /><sub><b>Corey Brown</b></sub></a></td>
<td align="center"><a href="http://simonknott.de"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4" width="100px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a></td>
<td align="center"><a href="https://twitter.com/GeggsElias"><img src="https://avatars3.githubusercontent.com/u/22719177?v=4" width="100px;" alt=""/><br /><sub><b>Elias Johansson</b></sub></a></td>
<td align="center"><a href="http://jeremyliberman.com/"><img src="https://avatars3.githubusercontent.com/u/2754163?v=4" width="100px;" alt=""/><br /><sub><b>Jeremy Liberman</b></td>
<td align="center"><a href="http://jagascript.com"><img src="https://avatars0.githubusercontent.com/u/4562878?v=4" width="100px;" alt=""/><br /><sub><b>Jaga Santagostino</b></sub></a></td>
<td align="center"><a href="https://simonpeterdebbarma.com"><img src="https://avatars3.githubusercontent.com/u/31207418?v=4" width="100px;" alt=""/><br /><sub><b>Simon Debbarma</b></sub></a></td>
<td align="center"><a href="https://github.com/jclancy93"><img src="https://avatars2.githubusercontent.com/u/7850202?v=4" width="100px;" alt=""/><br /><sub><b>Jack Clancy</b></sub></a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/jclancy93"><img src="https://avatars2.githubusercontent.com/u/7850202?v=4" width="100px;" alt=""/><br /><sub><b>Jack Clancy</b></sub></a></td>
<td align="center"><a href="https://twitter.com/ivandevp"><img src="https://avatars3.githubusercontent.com/u/9284690?v=4" width="100px;" alt=""/><br /><sub><b>Ivan Medina</b></sub></a></td>
<td align="center"><a href="https://twitter.com/nitaking_"><img src="https://avatars2.githubusercontent.com/u/10850034?v=4" width="100px;" alt=""/><br /><sub><b>Satoshi Nitawaki</b></sub></a></td>
</tr>
</table>
<!-- markdownlint-enable -->
@@ -253,7 +263,7 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
<td align="center"><a href="https://github.com/ntgussoni"><img src="https://avatars0.githubusercontent.com/u/10161067?v=4" width="100px;" alt=""/><br /><sub><b>Nicolas Torres</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ntgussoni" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=ntgussoni" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://simonknott.de"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4" width="100px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=skn0tt" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=skn0tt" title="Tests">⚠️</a> <a href="#maintenance-skn0tt" title="Maintenance">🚧</a></td>
<td align="center"><a href="http://simonknott.de"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4" width="100px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Skn0tt" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=Skn0tt" title="Tests">⚠️</a> <a href="#maintenance-Skn0tt" title="Maintenance">🚧</a></td>
<td align="center"><a href="http://jagascript.com"><img src="https://avatars0.githubusercontent.com/u/4562878?v=4" width="100px;" alt=""/><br /><sub><b>Jaga Santagostino</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=kandros" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=kandros" title="Documentation">📖</a> <a href="#maintenance-kandros" title="Maintenance">🚧</a></td>
<td align="center"><a href="http://www.joaoportela.com"><img src="https://avatars0.githubusercontent.com/u/1010018?v=4" width="100px;" alt=""/><br /><sub><b>João Portela</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jportela" title="Code">💻</a></td>
<td align="center"><a href="http://dajin.dev"><img src="https://avatars0.githubusercontent.com/u/7122182?v=4" width="100px;" alt=""/><br /><sub><b>Da-Jin Chu</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=dajinchu" title="Code">💻</a></td>
@@ -281,48 +291,89 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
</tr>
<tr>
<td align="center"><a href="https://github.com/pgrimaud"><img src="https://avatars1.githubusercontent.com/u/1866496?v=4" width="100px;" alt=""/><br /><sub><b>Pierre Grimaud</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=pgrimaud" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/jletey"><img src="https://avatars1.githubusercontent.com/u/62398724?v=4" width="100px;" alt=""/><br /><sub><b>John Letey</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jletey" title="Code">💻</a></td>
<td align="center"><a href="https://pixelmord.github.io"><img src="https://avatars2.githubusercontent.com/u/224168?v=4" width="100px;" alt=""/><br /><sub><b>Andreas Adam</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=pixelmord" title="Code">💻</a></td>
<td align="center"><a href="https://kevo.dev"><img src="https://avatars3.githubusercontent.com/u/15717067?v=4" width="100px;" alt=""/><br /><sub><b>Kevin Tovar</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=kevotovar" title="Code">💻</a></td>
<td align="center"><a href="http://anteprimorac.com.hr"><img src="https://avatars0.githubusercontent.com/u/972083?v=4" width="100px;" alt=""/><br /><sub><b>Ante Primorac</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=anteprimorac" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=anteprimorac" title="Documentation">📖</a></td>
<td align="center"><a href="http://mykalmachon.dev"><img src="https://avatars1.githubusercontent.com/u/7844994?v=4" width="100px;" alt=""/><br /><sub><b>Mykal Machon</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=MykalMachon" title="Code">💻</a></td>
<td align="center"><a href="https://jamiedavenport.dev"><img src="https://avatars2.githubusercontent.com/u/1329874?v=4" width="100px;" alt=""/><br /><sub><b>Jamie Davenport</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jamiedavenport" title="Code">💻</a></td>
<td align="center"><a href="https://cloudnweb.dev/"><img src="https://avatars0.githubusercontent.com/u/17050715?v=4" width="100px;" alt=""/><br /><sub><b>GaneshMani</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ganeshmani" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://cloudnweb.dev/"><img src="https://avatars0.githubusercontent.com/u/17050715?v=4" width="100px;" alt=""/><br /><sub><b>GaneshMani</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ganeshmani" title="Code">💻</a></td>
<td align="center"><a href="http://ramonmorcillo.com"><img src="https://avatars3.githubusercontent.com/u/31936665?v=4" width="100px;" alt=""/><br /><sub><b>reymon359</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=reymon359" title="Code">💻</a></td>
<td align="center"><a href="https://www.linkedin.com/in/gregory-vasquez-96413b184/"><img src="https://avatars1.githubusercontent.com/u/36422346?v=4" width="100px;" alt=""/><br /><sub><b>gvasquez11</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=gvasquez11" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/josemiguelo"><img src="https://avatars1.githubusercontent.com/u/15330034?v=4" width="100px;" alt=""/><br /><sub><b> José Miguel Ochoa</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=josemiguelo" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/osirvent"><img src="https://avatars2.githubusercontent.com/u/5927133?v=4" width="100px;" alt=""/><br /><sub><b>Oscar Sirvent</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=osirvent" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=osirvent" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/donni106"><img src="https://avatars0.githubusercontent.com/u/1942953?v=4" width="100px;" alt=""/><br /><sub><b>Daniel Molnar</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=donni106" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=donni106" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/exclipy"><img src="https://avatars1.githubusercontent.com/u/508799?v=4" width="100px;" alt=""/><br /><sub><b>Kevin Wu Won</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=exclipy" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/tehnuge"><img src="https://avatars1.githubusercontent.com/u/1928236?v=4" width="100px;" alt=""/><br /><sub><b>John Duong</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=tehnuge" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/tehnuge"><img src="https://avatars1.githubusercontent.com/u/1928236?v=4" width="100px;" alt=""/><br /><sub><b>John Duong</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=tehnuge" title="Code">💻</a></td>
<td align="center"><a href="https://noahfleischmann.com"><img src="https://avatars0.githubusercontent.com/u/23707137?v=4" width="100px;" alt=""/><br /><sub><b>Noah Fleischmann</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=fnoah" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/toshi1127"><img src="https://avatars3.githubusercontent.com/u/32378535?v=4" width="100px;" alt=""/><br /><sub><b>Matsumoto Toshi</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=toshi1127" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=toshi1127" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/simonedelmann"><img src="https://avatars2.githubusercontent.com/u/2821076?v=4" width="100px;" alt=""/><br /><sub><b>Simon Edelmann</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=simonedelmann" title="Code">💻</a></td>
<td align="center"><a href="https://shaun.church"><img src="https://avatars3.githubusercontent.com/u/571764?v=4" width="100px;" alt=""/><br /><sub><b>Shaun Church</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=shaunchurch" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=shaunchurch" title="Code">💻</a></td>
<td align="center"><a href="https://styfle.dev"><img src="https://avatars1.githubusercontent.com/u/229881?v=4" width="100px;" alt=""/><br /><sub><b>Steven</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=styfle" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/SigurdMW"><img src="https://avatars3.githubusercontent.com/u/6359003?v=4" width="100px;" alt=""/><br /><sub><b>Sigurd Moland Wahl</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=SigurdMW" title="Code">💻</a></td>
<td align="center"><a href="https://brianandrews.dev/"><img src="https://avatars1.githubusercontent.com/u/6384100?v=4" width="100px;" alt=""/><br /><sub><b>Brian Andrews</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sbardian" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://brianandrews.dev/"><img src="https://avatars1.githubusercontent.com/u/6384100?v=4" width="100px;" alt=""/><br /><sub><b>Brian Andrews</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sbardian" title="Documentation">📖</a></td>
<td align="center"><a href="http://garrisonsnelling.com"><img src="https://avatars0.githubusercontent.com/u/5100597?v=4" width="100px;" alt=""/><br /><sub><b>Garrison Snelling</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=garrisons" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/tylangesmith"><img src="https://avatars1.githubusercontent.com/u/22609577?v=4" width="100px;" alt=""/><br /><sub><b>Ty Lange-Smith</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=tylangesmith" title="Code">💻</a></td>
<td align="center"><a href="https://rubenmoya.dev"><img src="https://avatars3.githubusercontent.com/u/905225?v=4" width="100px;" alt=""/><br /><sub><b>Rubén Moya</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=rubenmoya" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=rubenmoya" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/robertgrzonka"><img src="https://avatars0.githubusercontent.com/u/35585466?v=4" width="100px;" alt=""/><br /><sub><b>robertgrzonka</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=robertgrzonka" title="Code">💻</a> <a href="#infra-robertgrzonka" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://github.com/agoxlea"><img src="https://avatars3.githubusercontent.com/u/1240841?v=4" width="100px;" alt=""/><br /><sub><b>Alex Orr</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=agoxlea" title="Code">💻</a></td>
<td align="center"><a href="https://christse.io"><img src="https://avatars1.githubusercontent.com/u/250450?v=4" width="100px;" alt=""/><br /><sub><b>Chris Tse</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=chris-tse" title="Code">💻</a></td>
<td align="center"><a href="http://twitter.com/nettofarah"><img src="https://avatars1.githubusercontent.com/u/270688?v=4" width="100px;" alt=""/><br /><sub><b>Netto Farah</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=nettofarah" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://twitter.com/nettofarah"><img src="https://avatars1.githubusercontent.com/u/270688?v=4" width="100px;" alt=""/><br /><sub><b>Netto Farah</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=nettofarah" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/rohanjulka19"><img src="https://avatars0.githubusercontent.com/u/19673968?v=4" width="100px;" alt=""/><br /><sub><b>Rohan Julka</b></sub></a><br /><a href="#infra-rohanjulka19" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center"><a href="https://www.ivansantos.me"><img src="https://avatars3.githubusercontent.com/u/301291?v=4" width="100px;" alt=""/><br /><sub><b>Ivan Santos</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=pragmaticivan" title="Code">💻</a></td>
<td align="center"><a href="https://able.bio"><img src="https://avatars0.githubusercontent.com/u/12991390?v=4" width="100px;" alt=""/><br /><sub><b>Soumyajit Pathak</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=drenther" title="Code">💻</a></td>
<td align="center"><a href="http://www.sebastiankurpiel.com"><img src="https://avatars2.githubusercontent.com/u/16307737?v=4" width="100px;" alt=""/><br /><sub><b>Sebastian Kurpiel</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=SebastianKurp" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/scisteffan"><img src="https://avatars2.githubusercontent.com/u/2676185?v=4" width="100px;" alt=""/><br /><sub><b>Steffan</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=scisteffan" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=scisteffan" title="Documentation">📖</a> <a href="#financial-scisteffan" title="Financial">💵</a></td>
<td align="center"><a href="https://github.com/kripod"><img src="https://avatars3.githubusercontent.com/u/14854048?v=4" width="100px;" alt=""/><br /><sub><b>Kristóf Poduszló</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=kripod" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Weilbyte"><img src="https://avatars1.githubusercontent.com/u/43392677?v=4" width="100px;" alt=""/><br /><sub><b>Weilbyte</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Weilbyte" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=Weilbyte" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="http://ricardotrejos.tech"><img src="https://avatars1.githubusercontent.com/u/8602086?v=4" width="100px;" alt=""/><br /><sub><b>Ricardo Trejos</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=cardotrejos" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=cardotrejos" title="Documentation">📖</a></td>
<td align="center"><a href="https://gkaragkiaouris.tech/"><img src="https://avatars0.githubusercontent.com/u/8822835?v=4" width="100px;" alt=""/><br /><sub><b>George Karagkiaouris</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=karaggeorge" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=karaggeorge" title="Documentation">📖</a></td>
<td align="center"><a href="https://www.linkedin.com/in/brady-pascoe-3bba6b13a/"><img src="https://avatars0.githubusercontent.com/u/18705892?v=4" width="100px;" alt=""/><br /><sub><b>Brady Pascoe</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=bpas247" title="Code">💻</a></td>
<td align="center"><a href="https://www.yeahcoach.com"><img src="https://avatars1.githubusercontent.com/u/761766?v=4" width="100px;" alt=""/><br /><sub><b>Jirka Svoboda</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=svobik7" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/alan2207"><img src="https://avatars3.githubusercontent.com/u/12713315?v=4" width="100px;" alt=""/><br /><sub><b>Alan Alickovic</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=alan2207" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=alan2207" title="Documentation">📖</a></td>
<td align="center"><a href="https://yngve.hoiseth.net"><img src="https://avatars0.githubusercontent.com/u/8469540?v=4" width="100px;" alt=""/><br /><sub><b>Yngve Høiseth</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=yhoiseth" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/bruno_crosier"><img src="https://avatars1.githubusercontent.com/u/18399089?v=4" width="100px;" alt=""/><br /><sub><b>Bruno Crosier</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=brunocrosier" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/jschepmans"><img src="https://avatars2.githubusercontent.com/u/5782977?v=4" width="100px;" alt=""/><br /><sub><b>Johan Schepmans</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jschepmans" title="Code">💻</a></td>
<td align="center"><a href="https://twitter.com/dillonraphael"><img src="https://avatars0.githubusercontent.com/u/3496193?v=4" width="100px;" alt=""/><br /><sub><b>Dillon Raphael</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=dillonraphael" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/clgeoio"><img src="https://avatars2.githubusercontent.com/u/37571416?v=4" width="100px;" alt=""/><br /><sub><b>Cody G</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=clgeoio" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/madflow"><img src="https://avatars0.githubusercontent.com/u/183248?v=4" width="100px;" alt=""/><br /><sub><b>madflow</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=madflow" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/nitaking_"><img src="https://avatars2.githubusercontent.com/u/10850034?v=4" width="100px;" alt=""/><br /><sub><b>Satoshi Nitawaki</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=nitaking" title="Code">💻</a> <a href="#maintenance-nitaking" title="Maintenance">🚧</a> <a href="#question-nitaking" title="Answering Questions">💬</a></td>
<td align="center"><a href="https://github.com/sirmyron"><img src="https://avatars2.githubusercontent.com/u/1430136?v=4" width="100px;" alt=""/><br /><sub><b>sirmyron</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sirmyron" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/engelkes-finstreet"><img src="https://avatars1.githubusercontent.com/u/36962022?v=4" width="100px;" alt=""/><br /><sub><b>engelkes-finstreet</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="http://twitter.com/pixelscommander"><img src="https://avatars2.githubusercontent.com/u/810671?v=4" width="100px;" alt=""/><br /><sub><b>Denis Radin</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3APixelsCommander" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/blitz-js/blitz/commits?author=PixelsCommander" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=PixelsCommander" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/xiaoyu-tamu"><img src="https://avatars3.githubusercontent.com/u/33362998?v=4" width="100px;" alt=""/><br /><sub><b>Michael Li</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=xiaoyu-tamu" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/yuta0801"><img src="https://avatars2.githubusercontent.com/u/21266306?v=4" width="100px;" alt=""/><br /><sub><b>yuta0801</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=yuta0801" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Obii-bit"><img src="https://avatars2.githubusercontent.com/u/67339820?v=4" width="100px;" alt=""/><br /><sub><b>Obadja Ris</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Obii-bit" title="Documentation">📖</a></td>
<td align="center"><a href="http://jfelix.info"><img src="https://avatars2.githubusercontent.com/u/21092519?v=4" width="100px;" alt=""/><br /><sub><b>Jose Felix </b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=JoseRFelix" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/johncantrell97"><img src="https://avatars3.githubusercontent.com/u/41305919?v=4" width="100px;" alt=""/><br /><sub><b>John Cantrell</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=johncantrell97" title="Code">💻</a></td>
<td align="center"><a href="http://kwuang.me"><img src="https://avatars1.githubusercontent.com/u/10319942?v=4" width="100px;" alt=""/><br /><sub><b>Kwuang Tang</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=cktang88" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/johnletey"><img src="https://avatars1.githubusercontent.com/u/62398724?v=4" width="100px;" alt=""/><br /><sub><b>John Letey</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=johnletey" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ditorojuan"><img src="https://avatars0.githubusercontent.com/u/22530892?v=4" width="100px;" alt=""/><br /><sub><b>Juan Di Toro</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ditorojuan" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/taylorcjohnson"><img src="https://avatars0.githubusercontent.com/u/10552296?v=4" width="100px;" alt=""/><br /><sub><b>Taylor Johnson</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=taylorcjohnson" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=taylorcjohnson" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/tsriram"><img src="https://avatars3.githubusercontent.com/u/450559?v=4" width="100px;" alt=""/><br /><sub><b>Sriram Thiagarajan</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=tsriram" title="Documentation">📖</a></td>
<td align="center"><a href="https://sergiodxa.com"><img src="https://avatars2.githubusercontent.com/u/1312018?v=4" width="100px;" alt=""/><br /><sub><b>Sergio Xalambrí</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sergiodxa" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/doeixd"><img src="https://avatars3.githubusercontent.com/u/13461122?v=4" width="100px;" alt=""/><br /><sub><b>Patrick G</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=doeixd" title="Code">💻</a></td>
<td align="center"><a href="http://avinash.com.np"><img src="https://avatars3.githubusercontent.com/u/513457?v=4" width="100px;" alt=""/><br /><sub><b>अभिनाश (Avinash)</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=hardfire" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://enricoschaaf.com"><img src="https://avatars1.githubusercontent.com/u/54645197?v=4" width="100px;" alt=""/><br /><sub><b>Enrico Schaaf</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=enricoschaaf" title="Code">💻</a></td>
<td align="center"><a href="http://kitze.io"><img src="https://avatars0.githubusercontent.com/u/1160594?v=4" width="100px;" alt=""/><br /><sub><b>Kitze</b></sub></a><br /><a href="#ideas-kitze" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center"><a href="https://github.com/drmas"><img src="https://avatars3.githubusercontent.com/u/644440?v=4" width="100px;" alt=""/><br /><sub><b>Mohamed Shaban</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=drmas" title="Code">💻</a></td>
</tr>
</table>

View File

@@ -2,6 +2,7 @@ import {passportAuth} from "blitz"
import db from "db"
import {Strategy as TwitterStrategy} from "passport-twitter"
import {Strategy as GitHubStrategy} from "passport-github2"
import {Strategy as Auth0Strategy} from "passport-auth0"
function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message)
@@ -16,8 +17,13 @@ assert(
assert(process.env.GITHUB_CLIENT_ID, "You must provide the GITHUB_CLIENT_ID env variable")
assert(process.env.GITHUB_CLIENT_SECRET, "You must provide the GITHUB_CLIENT_SECRET env variable")
assert(process.env.AUTH0_DOMAIN, "You must provide the AUTH0_DOMAIN env variable")
assert(process.env.AUTH0_CLIENT_ID, "You must provide the AUTH0_CLIENT_ID env variable")
assert(process.env.AUTH0_CLIENT_SECRET, "You must provide the AUTH0_CLIENT_SECRET env variable")
export default passportAuth({
successRedirectUrl: "/",
authenticateOptions: {scope: "openid email profile"}, //used for Auth0Strategy - without an empty profile is returned
strategies: [
new TwitterStrategy(
{
@@ -85,5 +91,41 @@ export default passportAuth({
done(null, {publicData})
},
),
new Auth0Strategy(
{
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
callbackURL:
process.env.NODE_ENV === "production"
? "https://auth-example-flybayer.blitzjs.vercel.app/api/auth/auth0/callback"
: "http://localhost:3000/api/auth/auth0/callback",
},
async function (_token, _tokenSecret, extraParams, profile, done) {
const email = profile.emails && profile.emails[0]?.value
if (!email) {
// This can happen if you haven't enabled email access in your twitter app permissions
return done(new Error("GitHub OAuth response doesn't have email."))
}
const user = await db.user.upsert({
where: {email},
create: {
email,
name: profile.displayName,
},
update: {email},
})
const publicData = {
userId: user.id,
roles: [user.role],
source: "auth0",
githubUsername: profile.username,
}
done(undefined, {publicData})
},
),
],
})

View File

@@ -1,6 +1,6 @@
import {AuthenticationError} from "blitz"
import SecurePassword from "secure-password"
import db, {User} from "db"
import db from "db"
const SP = new SecurePassword()
@@ -9,7 +9,12 @@ export const hashPassword = async (password: string) => {
return hashedBuffer.toString("base64")
}
export const verifyPassword = async (hashedPassword: string, password: string) => {
return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
try {
return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
} catch (error) {
console.error(error)
return false
}
}
export const authenticateUser = async (email: string, password: string) => {
@@ -29,6 +34,6 @@ export const authenticateUser = async (email: string, password: string) => {
throw new AuthenticationError()
}
delete user.hashedPassword
return user as Omit<User, "hashedPassword">
const {hashedPassword, ...rest} = user
return rest
}

View File

@@ -1,8 +1,9 @@
import React from "react"
import {Link} from "blitz"
import {LabeledTextField} from "app/components/LabeledTextField"
import {Form, FORM_ERROR} from "app/components/Form"
import login from "app/auth/mutations/login"
import {LoginInput, LoginInputType} from "app/auth/validations"
import {LoginInput} from "app/auth/validations"
type LoginFormProps = {
onSuccess?: () => void
@@ -12,8 +13,7 @@ export const LoginForm = (props: LoginFormProps) => {
return (
<div>
<h1>Login</h1>
<Form<LoginInputType>
<Form
submitText="Log In"
schema={LoginInput}
initialValues={{email: undefined, password: undefined}}
@@ -36,6 +36,9 @@ export const LoginForm = (props: LoginFormProps) => {
<LabeledTextField name="email" label="Email" placeholder="Email" />
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
</Form>
<div style={{marginTop: "1rem"}}>
Or <Link href="/signup">Sign Up</Link>
</div>
</div>
)
}

View File

@@ -1,5 +1,5 @@
import {SessionContext} from "blitz"
import {authenticateUser} from "app/auth"
import {authenticateUser} from "app/auth/auth-utils"
import {LoginInput, LoginInputType} from "../validations"
export default async function login(input: LoginInputType, ctx: {session?: SessionContext} = {}) {

View File

@@ -1,6 +1,6 @@
import db from "db"
import {SessionContext} from "blitz"
import {hashPassword} from "app/auth"
import {hashPassword} from "app/auth/auth-utils"
import {SignupInput, SignupInputType} from "app/auth/validations"
export default async function signup(input: SignupInputType, ctx: {session?: SessionContext} = {}) {

View File

@@ -3,7 +3,7 @@ import {Head, useRouter, BlitzPage} from "blitz"
import {Form, FORM_ERROR} from "app/components/Form"
import {LabeledTextField} from "app/components/LabeledTextField"
import signup from "app/auth/mutations/signup"
import {SignupInput, SignupInputType} from "app/auth/validations"
import {SignupInput} from "app/auth/validations"
const SignupPage: BlitzPage = () => {
const router = useRouter()
@@ -18,7 +18,7 @@ const SignupPage: BlitzPage = () => {
<div>
<h1>Create an Account</h1>
<Form<SignupInputType>
<Form
submitText="Create Account"
schema={SignupInput}
onSubmit={async (values) => {
@@ -30,7 +30,10 @@ const SignupPage: BlitzPage = () => {
// This error comes from Prisma
return {email: "This email is already being used"}
} else {
return {[FORM_ERROR]: error.toString()}
return {
[FORM_ERROR]:
"Sorry, we had an unexpected error. Please try again. - " + error.toString(),
}
}
}
}}

View File

@@ -3,26 +3,26 @@ import {Form as FinalForm, FormProps as FinalFormProps} from "react-final-form"
import * as z from "zod"
export {FORM_ERROR} from "final-form"
type FormProps<FormValues> = {
type FormProps<S extends z.ZodType<any, any>> = {
/** All your form fields */
children: ReactNode
/** Text to display in the submit button */
submitText: string
onSubmit: FinalFormProps<FormValues>["onSubmit"]
initialValues?: FinalFormProps<FormValues>["initialValues"]
schema?: z.ZodType<any, any>
onSubmit: FinalFormProps<z.infer<S>>["onSubmit"]
initialValues?: FinalFormProps<z.infer<S>>["initialValues"]
schema?: S
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
export function Form<FormValues extends Record<string, unknown>>({
export function Form<S extends z.ZodType<any, any>>({
children,
submitText,
schema,
initialValues,
onSubmit,
...props
}: FormProps<FormValues>) {
}: FormProps<S>) {
return (
<FinalForm<FormValues>
<FinalForm
initialValues={initialValues}
validate={(values) => {
if (!schema) return

View File

@@ -0,0 +1,10 @@
import {useQuery, useSession} from "blitz"
import getCurrentUser from "app/users/queries/getCurrentUser"
export const useCurrentUser = () => {
// We wouldn't have to useSession() here, but doing so improves perf on initial
// load since we can skip the getCurrentUser() request.
const session = useSession()
const [user] = useQuery(getCurrentUser, null, {enabled: !!session.userId})
return session.userId ? user : null
}

View File

@@ -1,12 +1,14 @@
import {AppProps, ErrorComponent} from "blitz"
import {AppProps, ErrorComponent, useRouter} from "blitz"
import {ErrorBoundary} from "react-error-boundary"
import {queryCache} from "react-query"
import LoginForm from "app/auth/components/LoginForm"
export default function App({Component, pageProps}: AppProps) {
const router = useRouter()
return (
<ErrorBoundary
FallbackComponent={RootErrorFallback}
resetKeys={[router.asPath]}
onReset={() => {
// This ensures the Blitz useQuery hooks will automatically refetch
// data any time you reset the error boundary

View File

@@ -1,11 +1,22 @@
import {Suspense} from "react"
import {Head, Link, useSession, useRouterQuery} from "blitz"
import getUser from "app/users/queries/getUser"
import trackView from "app/users/mutations/trackView"
import Layout from "app/layouts/Layout"
import {useCurrentUser} from "app/hooks/useCurrentUser"
const CurrentUserInfo = () => {
const currentUser = useCurrentUser()
return <pre>{JSON.stringify(currentUser, null, 2)}</pre>
}
const UserStuff = () => {
const session = useSession()
const query = useRouterQuery()
if (session.isLoading) return <div>Loading...</div>
return (
<div>
{!session.userId && (
@@ -26,6 +37,9 @@ const UserStuff = () => {
</>
)}
<pre>{JSON.stringify(session, null, 2)}</pre>
<Suspense fallback="Loading...">
<CurrentUserInfo />
</Suspense>
<button
onClick={async () => {
try {

View File

@@ -0,0 +1,23 @@
import React from "react"
type SessionFormProps = {
initialValues: any
onSubmit: React.FormEventHandler<HTMLFormElement>
}
const SessionForm = ({initialValues, onSubmit}: SessionFormProps) => {
return (
<form
onSubmit={(event) => {
event.preventDefault()
onSubmit(event)
}}
>
<div>Put your form fields here. But for now, just click submit</div>
<div>{JSON.stringify(initialValues)}</div>
<button>Submit</button>
</form>
)
}
export default SessionForm

View File

@@ -0,0 +1,16 @@
import {SessionContext} from "blitz"
import db, {SessionCreateArgs} from "db"
type CreateSessionInput = {
data: SessionCreateArgs["data"]
}
export default async function createSession(
{data}: CreateSessionInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session!.authorize()
const session = await db.session.create({data})
return session
}

View File

@@ -0,0 +1,17 @@
import {SessionContext} from "blitz"
import db, {SessionDeleteArgs} from "db"
type DeleteSessionInput = {
where: SessionDeleteArgs["where"]
}
export default async function deleteSession(
{where}: DeleteSessionInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session!.authorize()
const session = await db.session.delete({where})
return session
}

View File

@@ -0,0 +1,18 @@
import {SessionContext} from "blitz"
import db, {SessionUpdateArgs} from "db"
type UpdateSessionInput = {
where: SessionUpdateArgs["where"]
data: SessionUpdateArgs["data"]
}
export default async function updateSession(
{where, data}: UpdateSessionInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session!.authorize()
const session = await db.session.update({where, data})
return session
}

View File

@@ -0,0 +1,60 @@
import React, {Suspense} from "react"
import Layout from "app/layouts/Layout"
import {Head, Link, useRouter, useQuery, useParam, BlitzPage} from "blitz"
import getSession from "app/sessions/queries/getSession"
import deleteSession from "app/sessions/mutations/deleteSession"
export const Session = () => {
const router = useRouter()
const sessionId = useParam("sessionId", "number")
const [session] = useQuery(getSession, {where: {id: sessionId}})
return (
<div>
<h1>Session {session.id}</h1>
<pre>{JSON.stringify(session, null, 2)}</pre>
<Link href="/sessions/[sessionId]/edit" as={`/sessions/${session.id}/edit`}>
<a>Edit</a>
</Link>
<button
type="button"
onClick={async () => {
if (window.confirm("This will be deleted")) {
await deleteSession({where: {id: session.id}})
router.push("/sessions")
}
}}
>
Delete
</button>
</div>
)
}
const ShowSessionPage: BlitzPage = () => {
return (
<div>
<Head>
<title>Session</title>
</Head>
<main>
<p>
<Link href="/sessions">
<a>Sessions</a>
</Link>
</p>
<Suspense fallback={<div>Loading...</div>}>
<Session />
</Suspense>
</main>
</div>
)
}
ShowSessionPage.getLayout = (page) => <Layout>{page}</Layout>
export default ShowSessionPage

View File

@@ -0,0 +1,68 @@
import React, {Suspense} from "react"
import Layout from "app/layouts/Layout"
import {Head, Link, usePaginatedQuery, useRouter, BlitzPage} from "blitz"
import getSessions from "app/sessions/queries/getSessions"
const ITEMS_PER_PAGE = 100
export const SessionsList = () => {
const router = useRouter()
const page = Number(router.query.page) || 0
const [{sessions, hasMore}] = usePaginatedQuery(getSessions, {
orderBy: {id: "asc"},
skip: ITEMS_PER_PAGE * page,
take: ITEMS_PER_PAGE,
})
const goToPreviousPage = () => router.push({query: {page: page - 1}})
const goToNextPage = () => router.push({query: {page: page + 1}})
return (
<div>
<ul>
{sessions.map((session) => (
<li key={session.id}>
<Link href="/sessions/[sessionId]" as={`/sessions/${session.id}`}>
<a>{session.name}</a>
</Link>
</li>
))}
</ul>
<button disabled={page === 0} onClick={goToPreviousPage}>
Previous
</button>
<button disabled={!hasMore} onClick={goToNextPage}>
Next
</button>
</div>
)
}
const SessionsPage: BlitzPage = () => {
return (
<div>
<Head>
<title>Sessions</title>
</Head>
<main>
<h1>Sessions</h1>
<p>
<Link href="/sessions/new">
<a>Create Session</a>
</Link>
</p>
<Suspense fallback={<div>Loading...</div>}>
<SessionsList />
</Suspense>
</main>
</div>
)
}
SessionsPage.getLayout = (page) => <Layout>{page}</Layout>
export default SessionsPage

View File

@@ -0,0 +1,21 @@
import {NotFoundError, SessionContext} from "blitz"
import db, {FindOneSessionArgs} from "db"
type GetSessionInput = {
where: FindOneSessionArgs["where"]
// Only available if a model relationship exists
// include?: FindOneSessionArgs['include']
}
export default async function getSession(
{where /* include */}: GetSessionInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session!.authorize()
const session = await db.session.findOne({where})
if (!session) throw new NotFoundError()
return session
}

View File

@@ -0,0 +1,35 @@
import {SessionContext} from "blitz"
import db, {FindManySessionArgs} from "db"
type GetSessionsInput = {
where?: FindManySessionArgs["where"]
orderBy?: FindManySessionArgs["orderBy"]
skip?: FindManySessionArgs["skip"]
take?: FindManySessionArgs["take"]
// Only available if a model relationship exists
// include?: FindManySessionArgs['include']
}
export default async function getSessions(
{where, orderBy, skip = 0, take}: GetSessionsInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session!.authorize()
const sessions = await db.session.findMany({
where,
orderBy,
take,
skip,
})
const count = await db.session.count()
const hasMore = typeof take === "number" ? skip + take < count : false
const nextPage = hasMore ? {take, skip: skip + take!} : null
return {
sessions,
nextPage,
hasMore,
}
}

View File

@@ -9,9 +9,6 @@ export default async function updateUser(
{where, data}: UpdateUserInput,
ctx: Record<any, any> = {},
) {
// Don't allow updating
delete data.id
const user = await db.user.update({where, data})
return user

View File

@@ -0,0 +1,13 @@
import db from "db"
import {SessionContext} from "blitz"
export default async function getCurrentUser(_ = null, ctx: {session?: SessionContext} = {}) {
if (!ctx.session?.userId) return null
const user = await db.user.findOne({
where: {id: ctx.session!.userId},
select: {id: true, name: true, email: true, role: true},
})
return user
}

View File

@@ -15,7 +15,7 @@ export default async function getUsers(
{where, orderBy, cursor, take, skip}: GetUsersInput,
ctx: {session?: SessionContext} = {},
) {
ctx.session?.authorize(["admin"])
ctx.session!.authorize(["admin"])
const users = await db.user.findMany({
where,

View File

@@ -7,6 +7,7 @@ module.exports = withBundleAnalyzer({
middleware: [
sessionMiddleware({
unstable_isAuthorized: unstable_simpleRolesIsAuthorized,
// sessionExpiryMinutes: 1,
}),
],
/*

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/auth",
"version": "0.17.1-canary.5",
"version": "0.23.1-canary.0",
"scripts": {
"start": "blitz start",
"studio": "blitz db studio",
@@ -8,7 +8,7 @@
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
"analyze": "cross-env ANALYZE=true blitz build",
"cy:open": "cypress open",
"cy:run": "cypress run --browser chrome",
"cy:run": "cypress run",
"test:start": "blitz db migrate && blitz start --production -p 3099",
"test": "cross-env NODE_ENV=test start-server-and-test test:start http://localhost:3099 cy:run"
},
@@ -33,14 +33,15 @@
]
},
"dependencies": {
"@prisma/cli": "2.1.0",
"@prisma/client": "2.1.0",
"blitz": "0.17.1-canary.5",
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.23.1-canary.0",
"final-form": "4.20.1",
"passport-auth0": "1.3.3",
"passport-github2": "0.1.11",
"passport-twitter": "1.0.4",
"react": "0.0.0-experimental-33c3af284",
"react-dom": "0.0.0-experimental-33c3af284",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8",
"react-error-boundary": "2.3.1",
"react-final-form": "6.5.1",
"secure-password": "4.0.0",
@@ -49,6 +50,7 @@
"devDependencies": {
"@cypress/skip-test": "2.5.0",
"@next/bundle-analyzer": "latest",
"@types/passport-auth0": "1.0.4",
"@types/passport-github2": "1.2.4",
"@types/passport-twitter": "1.0.36",
"@types/react": "16.9.38",
@@ -67,7 +69,7 @@
"eslint-plugin-react": "7.20.5",
"eslint-plugin-react-hooks": "4.0.8",
"husky": "4.2.5",
"lint-staged": "10.2.11",
"lint-staged": "10.2.13",
"prettier": "2.0.5",
"pretty-quick": "2.0.1",
"start-server-and-test": "1.11.2",

View File

@@ -1,6 +1,6 @@
{
"name": "no-prisma",
"version": "0.17.1-canary.5",
"version": "0.23.1-canary.0",
"scripts": {
"start": "blitz start",
"build": "blitz build",
@@ -26,10 +26,10 @@
]
},
"dependencies": {
"blitz": "0.17.1-canary.5",
"blitz": "0.23.1-canary.0",
"knex": "0.21.2",
"react": "0.0.0-experimental-33c3af284",
"react-dom": "0.0.0-experimental-33c3af284",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8",
"sqlite3": "5.0.0"
},
"devDependencies": {
@@ -45,7 +45,7 @@
"eslint-plugin-react": "7.20.5",
"eslint-plugin-react-hooks": "4.0.8",
"husky": "4.2.5",
"lint-staged": "10.2.11",
"lint-staged": "10.2.13",
"prettier": "2.0.5",
"pretty-quick": "2.0.1",
"typescript": "3.9.6"

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/plain-js",
"version": "0.17.1-canary.5",
"version": "0.23.1-canary.0",
"scripts": {
"start": "blitz start",
"build": "blitz db migrate && blitz build",
@@ -29,11 +29,11 @@
]
},
"dependencies": {
"@prisma/cli": "2.0.0",
"@prisma/client": "2.0.0",
"blitz": "0.17.1-canary.5",
"react": "0.0.0-experimental-33c3af284",
"react-dom": "0.0.0-experimental-33c3af284"
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.23.1-canary.0",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8"
},
"devDependencies": {
"@types/react": "16.9.35",

View File

@@ -2,17 +2,18 @@ import {Suspense} from "react"
import {Link, useRouter, useQuery, useParam} from "blitz"
import getProduct from "app/products/queries/getProduct"
import ProductForm from "app/products/components/ProductForm"
import {queryCache} from "react-query"
function Product() {
const router = useRouter()
const id = useParam("id", "number")
const [product, {mutate}] = useQuery(getProduct, {where: {id}})
const [product] = useQuery(getProduct, {where: {id}})
return (
<ProductForm
product={product}
onSuccess={(updatedProduct) => {
mutate(updatedProduct)
onSuccess={() => {
queryCache.invalidateQueries("/api/products/queries/getProducts")
router.push("/admin/products")
}}
/>

View File

@@ -6,7 +6,7 @@ import getProduct from "app/products/queries/getProduct"
function ProductsList() {
const {orderby = "id", order = "desc"} = useRouterQuery()
const [products] = useQuery(getProducts, {
const [{products}] = useQuery(getProducts, {
orderBy: {
[Array.isArray(orderby) ? orderby[0] : orderby]: order,
},

View File

@@ -3,10 +3,10 @@ import {Product, ProductCreateInput, ProductUpdateInput} from "db"
import createProduct from "../mutations/createProduct"
import updateProduct from "../mutations/updateProduct"
type ProductInput = ProductCreateInput | ProductUpdateInput
type ProductInput = ProductCreateInput | Product
function isNew(product: ProductInput): product is ProductCreateInput {
return (product as ProductUpdateInput).id === undefined
return (product as any).id === undefined
}
type ProductFormProps = {
@@ -19,7 +19,7 @@ function ProductForm({product, style, onSuccess, ...props}: ProductFormProps) {
return (
<Form
initialValues={product || {name: null, handle: null, description: null, price: null}}
onSubmit={async (data: ProductInput) => {
onSubmit={async (data: any) => {
if (isNew(data)) {
try {
const product = await createProduct({data})
@@ -29,7 +29,10 @@ function ProductForm({product, style, onSuccess, ...props}: ProductFormProps) {
}
} else {
try {
const product = await updateProduct({where: {id: data.id}, data})
// Can't update id
const id = data.id
delete data.id
const product = await updateProduct({where: {id}, data})
onSuccess(product)
} catch (error) {
alert("Error updating product " + JSON.stringify(error, null, 2))

View File

@@ -6,9 +6,6 @@ type UpdateProductInput = {
}
export default async function updateProduct({where, data}: UpdateProductInput) {
// Don't allow updating
delete data.id
const product = await db.product.update({where, data})
return product

View File

@@ -10,7 +10,7 @@ type StaticProps = {
}
export const getStaticProps: GetStaticProps<StaticProps> = async (ctx) => {
const product = await getProduct({where: {handle: ctx.params.handle as string}})
const product = await getProduct({where: {handle: ctx.params!.handle as string}})
const dataString = superjson.stringify(product)
return {
props: {dataString},
@@ -18,7 +18,7 @@ export const getStaticProps: GetStaticProps<StaticProps> = async (ctx) => {
}
}
export const getStaticPaths: GetStaticPaths = async () => {
const paths = (await getProducts({orderBy: {id: "desc"}})).map(({handle}) => ({
const paths = (await getProducts({orderBy: {id: "desc"}})).products.map(({handle}) => ({
params: {handle},
}))
return {

View File

@@ -9,7 +9,7 @@ type StaticProps = {
}
export const getStaticProps: GetStaticProps<StaticProps> = async () => {
const products = await getProducts({orderBy: {id: "desc"}})
const {products} = await getProducts({orderBy: {id: "desc"}})
const dataString = superjson.stringify(products)
return {
props: {dataString},

View File

@@ -1,10 +1,10 @@
import {Suspense, Fragment} from "react"
import {BlitzPage, useInfiniteQuery} from "blitz"
import getProductsInfinite from "app/products/queries/getProductsInfinite"
import {BlitzPage, useInfiniteQuery, Link} from "blitz"
import getProducts from "app/products/queries/getProducts"
const Products = () => {
const [groupedProducts, {isFetching, isFetchingMore, fetchMore, canFetchMore}] = useInfiniteQuery(
getProductsInfinite,
getProducts,
(page = {take: 3, skip: 0}) => page,
{
getFetchMore: (lastGroup) => lastGroup.nextPage,
@@ -36,6 +36,9 @@ const Page: BlitzPage = function () {
return (
<div>
<h1>Products - Infinite</h1>
<Link href="/products/paginated">
<a>Go to Paginated Product List</a>
</Link>
<Suspense fallback={<div>Loading...</div>}>
<Products />
</Suspense>

View File

@@ -1,16 +1,20 @@
import {Suspense, useState} from "react"
import {Link, BlitzPage, usePaginatedQuery} from "blitz"
import {Suspense} from "react"
import {Link, BlitzPage, usePaginatedQuery, useRouter} from "blitz"
import getProducts from "app/products/queries/getProducts"
const ITEMS_PER_PAGE = 3
const Products = () => {
const [page, setPage] = useState(0)
const [products] = usePaginatedQuery(getProducts, {
const router = useRouter()
const page = Number(router.query.page) || 0
const [{products, hasMore}] = usePaginatedQuery(getProducts, {
skip: ITEMS_PER_PAGE * page,
take: ITEMS_PER_PAGE,
})
const goToPreviousPage = () => router.push({query: {page: page - 1}})
const goToNextPage = () => router.push({query: {page: page + 1}})
return (
<div>
{products.map((product) => (
@@ -20,10 +24,10 @@ const Products = () => {
</Link>
</p>
))}
<button disabled={page === 0} onClick={() => setPage(page - 1)}>
<button disabled={page === 0} onClick={goToPreviousPage}>
Previous
</button>
<button disabled={products.length !== ITEMS_PER_PAGE} onClick={() => setPage(page + 1)}>
<button disabled={!hasMore} onClick={goToNextPage}>
Next
</button>
</div>
@@ -34,6 +38,9 @@ const Page: BlitzPage = function () {
return (
<div>
<h1>Products - Paginated</h1>
<Link href="/products/infinite">
<a>Go to Infinite Product List</a>
</Link>
<Suspense fallback={<div>Loading...</div>}>
<Products />
</Suspense>

View File

@@ -20,7 +20,7 @@ export const getServerSideProps: GetServerSideProps = async ({req, res}) => {
}
const Page: BlitzPage<PageProps> = function ({dataString}) {
const products = useMemo(() => superjson.parse(dataString), [dataString]) as Products
const {products} = useMemo(() => superjson.parse(dataString), [dataString]) as Products
return (
<div>

View File

@@ -12,7 +12,7 @@ type GetProductsInput = {
}
export default async function getProducts(
{where, orderBy, skip, cursor, take}: GetProductsInput,
{where, orderBy, skip = 0, cursor, take}: GetProductsInput,
ctx: Record<any, unknown> = {},
) {
if (ctx.referer) {
@@ -27,7 +27,15 @@ export default async function getProducts(
take,
})
return products
const count = await db.product.count()
const hasMore = typeof take === "number" ? skip + take < count : false
const nextPage = hasMore ? {take, skip: skip + take!} : null
return {
products,
nextPage,
hasMore,
}
}
export const middleware: Middleware[] = [

View File

@@ -1,29 +0,0 @@
import db, {FindManyProductArgs} from "db"
type GetProductsInput = {
where?: FindManyProductArgs["where"]
orderBy?: FindManyProductArgs["orderBy"]
skip?: FindManyProductArgs["skip"]
cursor?: FindManyProductArgs["cursor"]
take?: FindManyProductArgs["take"]
// Only available if a model relationship exists
// include?: FindManyProductArgs['include']
}
export default async function getProducts({where, orderBy, take, skip}: GetProductsInput) {
const products = await db.product.findMany({
where,
orderBy,
take,
skip,
})
const count = await db.product.count()
const hasMore = skip + take < count
const nextPage = hasMore ? {take, skip: skip + take} : null
return {
products,
nextPage,
}
}

View File

@@ -43,7 +43,8 @@ describe("admin/products/[handle] page", () => {
cy.get("button").click()
cy.location("pathname").should("equal", "/admin/products")
cy.get("ul > li:last-child").contains(data[0] + random)
// Todo - make test work for this
// cy.get("ul > li:last-child").contains(data[0] + random)
})
})

View File

@@ -0,0 +1,30 @@
import db from "./index"
const randomString = (len: number, offset = 3) => {
let output = ""
for (let i = 0; i < len + Math.ceil((Math.random() - 0.5) * offset); i++) {
const ascii = Math.floor(Math.random() * 26) + (i % 2 === 0 ? 97 : 65)
output += String.fromCharCode(ascii)
}
return output
}
const randomProduct = () => {
return {
name: randomString(10),
handle: randomString(6, 0),
description: Array.from(new Array(10), () => randomString(10)).join(" "),
price: Math.floor(Math.random() * 10000),
}
}
const seed = async () => {
for (let i = 0; i < 5; i++) {
await db.product.create({ data: randomProduct() })
}
await db.user.create({ data: { email: "foo@bar.com", name: "Foobar" } })
}
export default seed

View File

@@ -1,6 +1,6 @@
{
"name": "@examples/store",
"version": "0.17.1-canary.5",
"version": "0.23.1-canary.0",
"private": true,
"scripts": {
"build": "blitz db migrate && blitz build",
@@ -16,14 +16,14 @@
"trailingComma": "all"
},
"dependencies": {
"@prisma/cli": "2.0.0",
"@prisma/client": "2.0.0",
"blitz": "0.17.1-canary.5",
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.23.1-canary.0",
"final-form": "4.19.1",
"react": "0.0.0-experimental-33c3af284",
"react-dom": "0.0.0-experimental-33c3af284",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8",
"react-error-boundary": "2.3.1",
"react-final-form": "6.4.0",
"superjson": "1.2.1",
"typescript": "3.8.3"
},
"devDependencies": {

View File

@@ -6,6 +6,7 @@
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,

View File

@@ -1,6 +1,6 @@
{
"name": "tailwind",
"version": "0.17.1-canary.5",
"version": "0.23.1-canary.0",
"scripts": {
"build": "blitz db migrate && blitz build",
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
@@ -28,11 +28,11 @@
]
},
"dependencies": {
"@prisma/cli": "2.0.0",
"@prisma/client": "2.0.0",
"blitz": "0.17.1-canary.5",
"react": "0.0.0-experimental-33c3af284",
"react-dom": "0.0.0-experimental-33c3af284",
"@prisma/cli": "2.4.1",
"@prisma/client": "2.4.1",
"blitz": "0.23.1-canary.0",
"react": "0.0.0-experimental-7f28234f8",
"react-dom": "0.0.0-experimental-7f28234f8",
"typescript": "3.8.3"
},
"devDependencies": {
@@ -48,7 +48,7 @@
"eslint-plugin-react": "7.20.5",
"eslint-plugin-react-hooks": "4.0.8",
"husky": "4.2.5",
"lint-staged": "10.1.7",
"lint-staged": "10.2.13",
"postcss-preset-env": "6.7.0",
"prettier": "2.0.5",
"pretty-quick": "2.0.1",

View File

@@ -1,5 +1,5 @@
{
"version": "0.17.1-canary.5",
"version": "0.23.1-canary.0",
"packages": ["packages/*"],
"npmClient": "yarn",
"useWorkspaces": true,

View File

@@ -45,10 +45,12 @@
"@testing-library/react-hooks": "3.3.0",
"@types/b64-lite": "1.3.0",
"@types/cookie": "0.4.0",
"@types/cross-spawn": "6.0.1",
"@types/cookie-session": "2.0.41",
"@types/cross-spawn": "6.0.2",
"@types/debug": "4.1.5",
"@types/detect-port": "1.3.0",
"@types/diff": "4.0.2",
"@types/dotenv-flow": "3.0.1",
"@types/flush-write-stream": "1.0.0",
"@types/from2": "2.3.0",
"@types/fs-extra": "8.1.0",
@@ -65,6 +67,7 @@
"@types/node": "13.13.2",
"@types/node-fetch": "2.5.7",
"@types/parallel-transform": "1.1.0",
"@types/passport": "1.0.4",
"@types/pluralize": "0.0.29",
"@types/prettier": "2.0.0",
"@types/pump": "1.1.0",
@@ -78,54 +81,57 @@
"@types/vinyl": "2.0.4",
"@types/vinyl-fs": "2.4.11",
"@types/webpack": "4.41.13",
"@types/passport": "1.0.4",
"@types/cookie-session": "2.0.41",
"@typescript-eslint/eslint-plugin": "2.x",
"@typescript-eslint/parser": "2.x",
"@wessberg/cjs-to-esm-transformer": "0.0.19",
"@wessberg/rollup-plugin-ts": "1.2.24",
"react-test-renderer": "16.13.1",
"@wessberg/cjs-to-esm-transformer": "0.0.22",
"@wessberg/rollup-plugin-ts": "1.3.3",
"babel-eslint": "10.x",
"cpy-cli": "3.1.0",
"cross-env": "7.0.0",
"babel-jest": "26.3.0",
"concurrently": "5.3.0",
"cpy-cli": "3.1.1",
"cross-env": "7.0.2",
"debug": "4.1.1",
"delay": "4.3.0",
"directory-tree": "2.2.4",
"eslint": "7.x",
"eslint": "7.7.0",
"eslint-config-react-app": "5.2.1",
"eslint-plugin-flowtype": "4.x",
"eslint-plugin-import": "2.x",
"eslint-plugin-jsx-a11y": "6.x",
"eslint-plugin-prettier": "3.1.3",
"eslint-plugin-react": "7.x",
"eslint-plugin-react-hooks": "2.x",
"eslint-plugin-unicorn": "19.0.1",
"eslint-plugin-es": "mysticatea/eslint-plugin-es",
"eslint-plugin-es5": "1.5.0",
"eslint-plugin-flowtype": "5.2.0",
"eslint-plugin-import": "2.22.0",
"eslint-plugin-jsx-a11y": "6.3.1",
"eslint-plugin-prettier": "3.1.4",
"eslint-plugin-react": "7.20.6",
"eslint-plugin-react-hooks": "4.1.0",
"eslint-plugin-unicorn": "21.0.0",
"husky": "4.2.5",
"isomorphic-unfetch": "3.0.0",
"jest": "24.9.0",
"jest-environment-jsdom-sixteen": "1.0.3",
"lerna": "3.20.2",
"lint-staged": "10.0.8",
"mock-fs": "4.12.0",
"lerna": "3.22.1",
"lint-staged": "10.2.13",
"mock-fs": "4.13.0",
"nock": "12.0.3",
"npm-run-all": "4.1.5",
"patch-package": "6.2.2",
"postinstall-postinstall": "2.1.0",
"prettier": "2.0.4",
"prettier": "2.1.1",
"pretty-quick": "2.0.1",
"prompt": "1.0.0",
"react-test-renderer": "16.13.1",
"release": "6.1.0",
"rimraf": "3.0.2",
"rollup": "2.7.2",
"rollup": "2.26.8",
"rollup-plugin-commonjs": "10.1.0",
"rollup-plugin-json": "4.0.0",
"rollup-plugin-node-polyfills": "0.2.1",
"rollup-plugin-node-resolve": "5.2.0",
"rollup-plugin-peer-deps-external": "2.2.2",
"rollup-plugin-peer-deps-external": "2.2.3",
"semver": "7.3.2",
"stdout-stderr": "0.1.13",
"test-listen": "1.1.0",
"ts-jest": "24.3.0",
"tsdx": "0.13.1",
"tsdx": "0.13.3",
"tslib": "1.11.1",
"typescript": "3.8.3",
"wait-on": "4.0.2"
@@ -147,5 +153,6 @@
"@types/react": "16.9.34",
"@types/react-dom": "16.9.8",
"ts-jest": "24.3.0"
}
},
"version": "0.0.0"
}

View File

@@ -1,7 +1,7 @@
{
"name": "blitz",
"description": "Blitz is a Rails-like framework for monolithic, full-stack React apps — built on Next.js",
"version": "0.17.1-canary.5",
"version": "0.23.1-canary.0",
"license": "MIT",
"scripts": {
"clean": "rimraf dist",
@@ -39,11 +39,11 @@
"url": "https://github.com/blitz-js/blitz"
},
"dependencies": {
"@blitzjs/cli": "0.17.1-canary.5",
"@blitzjs/core": "0.17.1-canary.5",
"@blitzjs/generator": "0.17.1-canary.5",
"@blitzjs/installer": "0.17.1-canary.5",
"@blitzjs/server": "0.17.1-canary.5",
"@blitzjs/cli": "0.23.1-canary.0",
"@blitzjs/core": "0.23.1-canary.0",
"@blitzjs/generator": "0.23.1-canary.0",
"@blitzjs/installer": "0.23.1-canary.0",
"@blitzjs/server": "0.23.1-canary.0",
"envinfo": "7.7.2",
"os-name": "3.1.0",
"pkg-dir": "4.2.0",

View File

@@ -5,12 +5,16 @@ import chalk from "chalk"
import {parseSemver} from "../utils/parse-semver"
async function main() {
console.log(
chalk.yellow(
`You are using alpha software - if you have any problems, please open an issue here:
https://github.com/blitz-js/blitz/issues/new/choose\n`,
),
)
const options = require("minimist")(process.argv.slice(2))
if (options._[0] !== "autocomplete:script" || Object.keys(options).length > 1) {
console.log(
chalk.yellow(
`You are using alpha software - if you have any problems, please open an issue here:
https://github.com/blitz-js/blitz/issues/new/choose\n`,
),
)
}
if (parseSemver(process.version).major < 12) {
console.log(
@@ -37,7 +41,6 @@ async function main() {
const cli = require(cliPkgPath)
const options = require("minimist")(process.argv.slice(2))
const hasVersionFlag = options._.length === 0 && (options.v || options.version)
const hasVerboseFlag = options._.length === 0 && (options.V || options.verbose)

View File

@@ -1,7 +1,7 @@
{
"name": "@blitzjs/cli",
"description": "Blitz.js CLI",
"version": "0.17.1-canary.5",
"version": "0.23.1-canary.0",
"license": "MIT",
"scripts": {
"b": "./bin/run",
@@ -30,43 +30,51 @@
"/lib"
],
"dependencies": {
"@blitzjs/display": "0.17.1-canary.5",
"@blitzjs/repl": "0.17.1-canary.5",
"@blitzjs/display": "0.23.1-canary.0",
"@blitzjs/repl": "0.23.1-canary.0",
"@oclif/command": "1.5.20",
"@oclif/config": "1.15.1",
"@oclif/plugin-autocomplete": "0.2.0",
"@oclif/plugin-help": "2.2.3",
"@oclif/plugin-not-found": "1.2.3",
"@prisma/sdk": "2.6.0",
"@salesforce/lazy-require": "0.3.2",
"camelcase": "6.0.0",
"chalk": "4.0.0",
"cross-spawn": "7.0.2",
"dotenv": "8.2.0",
"cross-spawn": "7.0.3",
"dotenv-expand": "5.1.0",
"dotenv-flow": "3.2.0",
"enquirer": "2.3.4",
"got": "11.1.3",
"has-yarn": "2.1.0",
"hasbin": "1.2.3",
"minimist": "1.2.5",
"p-event": "4.2.0",
"pkg-dir": "4.2.0",
"pluralize": "8.0.0",
"rimraf": "3.0.2",
"tar": "6.0.2",
"ts-node": "8.9.0",
"tsconfig-paths": "3.9.0"
"tsconfig-paths": "3.9.0",
"v8-compile-cache": "2.1.1"
},
"devDependencies": {
"@blitzjs/generator": "0.17.1-canary.5",
"@blitzjs/installer": "0.17.1-canary.5",
"@blitzjs/server": "0.17.1-canary.5",
"@blitzjs/generator": "0.23.1-canary.0",
"@blitzjs/installer": "0.23.1-canary.0",
"@blitzjs/server": "0.23.1-canary.0",
"@oclif/dev-cli": "1.22.2",
"@oclif/test": "1.2.5",
"@prisma/cli": "2.0.0-beta.3",
"nock": "13.0.0-beta.3"
"@prisma/cli": "2.4.1",
"nock": "13.0.0-beta.3",
"stdout-stderr": "0.1.13"
},
"oclif": {
"commands": "./lib/src/commands",
"bin": "blitz",
"plugins": [
"@oclif/plugin-help",
"@oclif/plugin-not-found"
"@oclif/plugin-not-found",
"@oclif/plugin-autocomplete"
],
"hooks": {
"init": [

View File

@@ -3,7 +3,7 @@ import chalk from "chalk"
import {isBlitzRoot, IsBlitzRootError} from "./utils/is-blitz-root"
const whitelistGlobal = ["-h", "--help", "help", "new"]
const whitelistGlobal = ["-h", "--help", "help", "new", "autocomplete", "autocomplete:script"]
export const hook: Hook<"init"> = async function (options) {
const {id} = options

View File

@@ -1,6 +1,4 @@
import {Command} from "@oclif/command"
import {build} from "@blitzjs/server"
import {runPrismaGeneration} from "./db"
export class Build extends Command {
static description = "Create a production build"
@@ -12,7 +10,7 @@ export class Build extends Command {
}
try {
await build(config, runPrismaGeneration({silent: true}))
await require("@blitzjs/server").build(config)
} catch (err) {
console.error(err)
process.exit(1) // clean up?

View File

@@ -1,16 +1,7 @@
import {runRepl} from "@blitzjs/repl"
import {Command} from "@oclif/command"
import path from "path"
import fs from "fs"
import pkgDir from "pkg-dir"
import {log} from "@blitzjs/display"
import chalk from "chalk"
import {setupTsnode} from "../utils/setup-ts-node"
import {runPrismaGeneration} from "./db"
const projectRoot = pkgDir.sync() || process.cwd()
const isTypescript = fs.existsSync(path.join(projectRoot, "tsconfig.json"))
const projectRoot = require("pkg-dir").sync() || process.cwd()
const isTypescript = require("fs").existsSync(require("path").join(projectRoot, "tsconfig.json"))
export class Console extends Command {
static description = "Run the Blitz console REPL"
@@ -22,19 +13,17 @@ export class Console extends Command {
}
async run() {
const {log} = require("@blitzjs/display")
const chalk = require("chalk")
log.branded("You have entered the Blitz console")
console.log(chalk.yellow("Tips: - Exit by typing .exit or pressing Ctrl-D"))
console.log(chalk.yellow(" - Use your db like this: await db.project.findMany()"))
console.log(chalk.yellow(" - Use your queries/mutations like this: await getProjects({})"))
const spinner = log.spinner("Loading your code").start()
if (isTypescript) {
setupTsnode()
require("../utils/setup-ts-node").setupTsnode()
}
await runPrismaGeneration({silent: true})
spinner.succeed()
runRepl(Console.replOptions)
require("@blitzjs/repl").runRepl(Console.replOptions)
}
}

View File

@@ -1,95 +1,98 @@
import {resolveBinAsync} from "@blitzjs/server"
import {log} from "@blitzjs/display"
import {Command, flags} from "@oclif/command"
import chalk from "chalk"
import {spawn} from "cross-spawn"
import {prompt} from "enquirer"
import * as fs from "fs"
import * as path from "path"
import {promisify} from "util"
import {projectRoot} from "../utils/get-project-root"
import {log} from "@blitzjs/display"
const schemaPath = path.join(process.cwd(), "db", "schema.prisma")
const schemaArg = `--schema=${schemaPath}`
const getPrismaBin = () => resolveBinAsync("@prisma/cli", "prisma")
const getPrismaBin = () => require("@blitzjs/server").resolveBinAsync("@prisma/cli", "prisma")
let prismaBin: string
let schemaArg: string
const runPrisma = async (args: string[], silent = false) => {
if (!prismaBin) {
try {
prismaBin = await getPrismaBin()
} catch {
throw new Error(
"Oops, we can't find Prisma Client. Please make sure it's installed in your project",
)
}
}
const cp = require("cross-spawn").spawn(prismaBin, args, {
stdio: silent ? "ignore" : "inherit",
env: process.env,
})
const code = await require("p-event")(cp, "exit", {rejectionEvents: []})
return code === 0
}
const runPrismaExitOnError = async (...args: Parameters<typeof runPrisma>) => {
const success = await runPrisma(...args)
if (!success) {
process.exit(1)
}
}
// Prisma client generation will fail if no model is defined in the schema.
// So the silent option is here to ignore that failure
export const runPrismaGeneration = async ({silent = false} = {}) => {
try {
const prismaBin = await getPrismaBin()
export const runPrismaGeneration = async ({silent = false, failSilently = false} = {}) => {
const success = await runPrisma(["generate", schemaArg], silent)
return new Promise((resolve) => {
spawn(prismaBin, ["generate", schemaArg], {stdio: silent ? "ignore" : "inherit"}).on(
"exit",
(code) => {
if (code === 0) {
resolve()
} else if (silent) {
resolve()
} else {
process.exit(1)
}
},
)
})
} catch (error) {
if (silent) return
throw new Error(
"Oops, we can't find Prisma Client. Please make sure it's installed in your project",
)
if (!success && !failSilently) {
throw new Error("Prisma Client generation failed")
}
}
const runMigrateUp = (prismaBin: string, resolve: (value?: unknown) => void) => {
const args = ["migrate", "up", schemaArg, "--create-db", "--experimental"]
if (process.env.NODE_ENV === "production") {
const runMigrateUp = async ({silent = false} = {}, schemaArgLocal = schemaArg) => {
const args = ["migrate", "up", schemaArgLocal, "--create-db", "--experimental"]
if (process.env.NODE_ENV === "production" || silent) {
args.push("--auto-approve")
}
const cp = spawn(prismaBin, args, {stdio: "inherit"})
cp.on("exit", async (code) => {
if (code === 0) {
await runPrismaGeneration()
resolve()
} else {
process.exit(1)
}
})
const success = await runPrisma(args, silent)
if (!success) {
throw new Error("Migration failed")
}
return runPrismaGeneration({silent})
}
export const runMigrate = async () => {
const prismaBin = await getPrismaBin()
return new Promise((resolve) => {
if (process.env.NODE_ENV === "production") {
runMigrateUp(prismaBin, resolve)
} else {
const cp = spawn(prismaBin, ["migrate", "save", schemaArg, "--create-db", "--experimental"], {
stdio: "inherit",
})
cp.on("exit", (code) => {
if (code === 0) {
runMigrateUp(prismaBin, resolve)
} else {
process.exit(1)
}
})
}
})
export const runMigrate = async (flags: object = {}, schemaArgLocal = schemaArg) => {
if (process.env.NODE_ENV === "production") {
return runMigrateUp({}, schemaArgLocal)
}
// @ts-ignore escape:TS7053
const nestedFlags = Object.keys(flags).map((key) => [`--${key}`, flags[key]])
const options = ([] as string[]).concat(...nestedFlags)
const silent = options.includes("--name")
const args = ["migrate", "save", schemaArgLocal, "--create-db", "--experimental", ...options]
const success = await runPrisma(args, silent)
if (!success) {
throw new Error("Migration failed")
}
return runMigrateUp({silent}, schemaArgLocal)
}
export async function resetPostgres(connectionString: string, db: any): Promise<void> {
const dbName: string = getDbName(connectionString)
try {
// close all other connections
await db.queryRaw(
await db.$queryRaw(
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND datname='${dbName};'`,
)
// currently assuming the public schema is being used
// delete schema and recreate with the appropriate privileges
await db.executeRaw("DROP SCHEMA public cascade;")
await db.executeRaw("CREATE SCHEMA public;")
await db.executeRaw("GRANT ALL ON schema public TO postgres;")
await db.executeRaw("GRANT ALL ON schema public TO public;")
await db.$executeRaw("DROP SCHEMA public cascade;")
await db.$executeRaw("CREATE SCHEMA public;")
await db.$executeRaw("GRANT ALL ON schema public TO postgres;")
await db.$executeRaw("GRANT ALL ON schema public TO public;")
// run migration
await runMigrate()
log.success("Your database has been reset.")
@@ -105,7 +108,7 @@ export async function resetMysql(connectionString: string, db: any): Promise<voi
const dbName: string = getDbName(connectionString)
try {
// delete database
await db.executeRaw(`DROP DATABASE \`${dbName}\``)
await db.$executeRaw(`DROP DATABASE \`${dbName}\``)
// run migration
await runMigrate()
log.success("Your database has been reset.")
@@ -118,8 +121,13 @@ export async function resetMysql(connectionString: string, db: any): Promise<voi
}
export async function resetSqlite(connectionString: string): Promise<void> {
const dbPath: string = connectionString.replace(/^file:/, "").replace(/^(?:\.\.[\\/])+/, "")
const unlink = promisify(fs.unlink)
const relativePath = connectionString.replace(/^file:/, "").replace(/^(?:\.\.[\\/])+/, "")
const dbPath = require("path").join(
require("../utils/get-project-root").projectRoot,
"db",
relativePath,
)
const unlink = require("util").promisify(require("fs").unlink)
try {
// delete database from folder
await unlink(dbPath)
@@ -143,17 +151,21 @@ export function getDbName(connectionString: string): string {
export class Db extends Command {
static description = `Run database commands
${chalk.bold("migrate")} Run any needed migrations via Prisma 2 and generate Prisma Client.
${require("chalk").bold(
"migrate",
)} Run any needed migrations via Prisma 2 and generate Prisma Client.
${chalk.bold(
${require("chalk").bold(
"introspect",
)} Will introspect the database defined in db/schema.prisma and automatically generate a complete schema.prisma file for you. Lastly, it'll generate Prisma Client.
${chalk.bold(
${require("chalk").bold(
"studio",
)} Open the Prisma Studio UI at http://localhost:5555 so you can easily see and change data in your database.
${chalk.bold("reset")} Reset the database and run a fresh migration via Prisma 2.
${require("chalk").bold(
"reset",
)} Reset the database and run a fresh migration via Prisma 2. You can also pass --force to skip all the user prompts.
`
static args = [
@@ -167,79 +179,93 @@ ${chalk.bold("reset")} Reset the database and run a fresh migration via Prisma
static flags = {
help: flags.help({char: "h"}),
// Used by `new` command to perform the initial migration
name: flags.string({hidden: true}),
// Used by `reset` command to skip the confirmation prompt
force: flags.boolean({char: "f", hidden: true}),
}
static strict = false
async run() {
const {args} = this.parse(Db)
const {args, flags} = this.parse(Db)
const command = args["command"]
const prismaBin = await getPrismaBin()
// Needs to happen at run-time since the `new` command needs to change the cwd before running
const schemaPath = require("path").join(process.cwd(), "db", "schema.prisma")
schemaArg = `--schema=${schemaPath}`
if (command === "migrate" || command === "m") {
await runMigrate()
} else if (command === "introspect") {
const cp = spawn(prismaBin, ["introspect", schemaArg], {
stdio: "inherit",
})
cp.on("exit", (code) => {
if (code === 0) {
spawn(prismaBin, ["generate", schemaArg], {stdio: "inherit"}).on(
"exit",
(code: number) => {
if (code !== 0) {
process.exit(1)
}
},
)
try {
return await runMigrate(flags)
} catch (error) {
if (Object.keys(flags).length > 0) {
throw error
} else {
process.exit(1)
}
})
} else if (command === "studio") {
const cp = spawn(prismaBin, ["studio", schemaArg, "--experimental"], {
stdio: "inherit",
})
cp.on("exit", (code) => {
if (code === 0) {
} else {
process.exit(1)
}
})
} else if (command === "reset") {
const spinner = log.spinner("Loading your database").start()
await runPrismaGeneration({silent: true})
spinner.succeed()
await prompt<{confirm: string}>({
type: "confirm",
name: "confirm",
message: "Are you sure you want to reset your database and erase ALL data?",
}).then((res) => {
if (res.confirm) {
const prismaClientPath = require.resolve("@prisma/client", {paths: [projectRoot]})
const {PrismaClient} = require(prismaClientPath)
const db = new PrismaClient()
const dataSource: any = db.engine.datasources[0]
const providerType: string = dataSource.name
const connectionString: string = dataSource.url
if (providerType === "postgresql") {
resetPostgres(connectionString, db)
} else if (providerType === "mysql") {
resetMysql(connectionString, db)
} else if (providerType === "sqlite") {
resetSqlite(connectionString)
} else {
this.log("Could not find a valid database configuration")
}
}
})
} else if (command === "help") {
await Db.run(["--help"])
} else {
this.log("\nUh oh, Blitz does not support that command.")
this.log("You can try running a prisma command directly with:")
this.log("\n `npm run prisma COMMAND` or `yarn prisma COMMAND`\n")
this.log("Or you can list available db commands with with:")
this.log("\n `npm run blitz db --help` or `yarn blitz db --help`\n")
}
}
if (command === "introspect") {
await runPrismaExitOnError(["introspect", schemaArg])
return runPrismaExitOnError(["generate", schemaArg])
}
if (command === "studio") {
return runPrismaExitOnError(["studio", schemaArg, "--experimental"])
}
if (command === "reset") {
const forceSkipConfirmation = flags.force
if (!forceSkipConfirmation) {
const {confirm} = await require("enquirer").prompt({
type: "confirm",
name: "confirm",
message: "Are you sure you want to reset your database and erase ALL data?",
})
if (!confirm) {
return
}
}
log.progress("Resetting your database...")
const {projectRoot} = require("../utils/get-project-root")
const prismaClientPath = require.resolve("@prisma/client", {paths: [projectRoot]})
const {PrismaClient} = require(prismaClientPath)
const db = new PrismaClient()
const schemaPath = require("path").join(projectRoot, "db/schema.prisma")
const datamodel = await require("@prisma/sdk").getSchema(schemaPath)
const config = await require("@prisma/sdk").getConfig({datamodel})
const dataSource = config.datasources[0]
const providerType = dataSource.activeProvider
const connectionString = dataSource.url.value
if (providerType === "postgresql") {
await resetPostgres(connectionString, db)
return
} else if (providerType === "mysql") {
await resetMysql(connectionString, db)
return
} else if (providerType === "sqlite") {
await resetSqlite(connectionString)
return
} else {
log.error("Could not find a valid database configuration")
return
}
}
if (command === "help") {
return Db.run(["--help"])
}
this.log("\nUh oh, Blitz does not support that command.")
this.log("You can try running a prisma command directly with:")
this.log("\n `npm run prisma COMMAND` or `yarn prisma COMMAND`\n")
this.log("Or you can list available db commands with with:")
this.log("\n `npm run blitz db --help` or `yarn blitz db --help`\n")
}
}

View File

@@ -1,9 +1,6 @@
import {Command} from "../command"
import {flags} from "@oclif/command"
import * as fs from "fs"
import * as path from "path"
import enquirer from "enquirer"
import _pluralize from "pluralize"
import {log} from "@blitzjs/display"
import {
PageGenerator,
MutationGenerator,
@@ -13,15 +10,13 @@ import {
QueryGenerator,
} from "@blitzjs/generator"
import {PromptAbortedError} from "../errors/prompt-aborted"
import {log} from "@blitzjs/display"
import camelCase from "camelcase"
import pkgDir from "pkg-dir"
const debug = require("debug")("blitz:generate")
const pascalCase = (str: string) => camelCase(str, {pascalCase: true})
const projectRoot = pkgDir.sync() || process.cwd()
const isTypescript = fs.existsSync(path.join(projectRoot, "tsconfig.json"))
const pascalCase = (str: string) => require("camelcase")(str, {pascalCase: true})
const getIsTypescript = () =>
require("fs").existsSync(
require("path").join(require("../utils/get-project-root").projectRoot, "tsconfig.json"),
)
enum ResourceType {
All = "all",
@@ -46,18 +41,18 @@ interface Args {
}
function pluralize(input: string): string {
return _pluralize.isPlural(input) ? input : _pluralize.plural(input)
return require("pluralize").isPlural(input) ? input : require("pluralize").plural(input)
}
function singular(input: string): string {
return _pluralize.isSingular(input) ? input : _pluralize.singular(input)
return require("pluralize").isSingular(input) ? input : require("pluralize").singular(input)
}
function modelName(input: string = "") {
return camelCase(singular(input))
return require("camelcase")(singular(input))
}
function modelNames(input: string = "") {
return camelCase(pluralize(input))
return require("camelcase")(pluralize(input))
}
function ModelName(input: string = "") {
return pascalCase(singular(input))
@@ -157,31 +152,33 @@ export class Generate extends Command {
]
async promptForTargetDirectory(paths: string[]): Promise<string> {
return enquirer
.prompt<{directory: string}>({
return require("enquirer")
.prompt({
name: "directory",
type: "select",
message: "Please select a target directory:",
choices: paths,
})
.then((resp) => resp.directory)
.then((resp: any) => resp.directory)
}
async genericConfirmPrompt(message: string): Promise<boolean> {
return enquirer
.prompt<{continue: string}>({
return require("enquirer")
.prompt({
name: "continue",
type: "select",
message: message,
choices: ["Yes", "No"],
})
.then((resp) => resp.continue === "Yes")
.then((resp: any) => resp.continue === "Yes")
}
async handleNoContext(message: string): Promise<void> {
const shouldCreateNewRoot = await this.genericConfirmPrompt(message)
if (!shouldCreateNewRoot) {
log.error("Could not determine proper location for files. Aborting.")
require("@blitzjs/display").log.error(
"Could not determine proper location for files. Aborting.",
)
this.exit(0)
}
}
@@ -192,7 +189,7 @@ export class Generate extends Command {
if (modelSegments.length > 1) {
return {
model: modelSegments[modelSegments.length - 1],
context: path.join(...modelSegments.slice(0, modelSegments.length - 1)),
context: require("path").join(...modelSegments.slice(0, modelSegments.length - 1)),
}
}
@@ -201,7 +198,7 @@ export class Generate extends Command {
return {
model: modelName,
context: path.join(...contextSegments),
context: require("path").join(...contextSegments),
}
}
@@ -222,7 +219,7 @@ export class Generate extends Command {
const generators = generatorMap[args.type]
for (const GeneratorClass of generators) {
const generator = new GeneratorClass({
destinationRoot: path.resolve(),
destinationRoot: require("path").resolve(),
extraArgs: argv.slice(2).filter((arg) => !arg.startsWith("-")),
modelName: singularRootContext,
modelNames: modelNames(singularRootContext),
@@ -232,9 +229,10 @@ export class Generate extends Command {
parentModels: modelNames(flags.parent),
ParentModel: ModelName(flags.parent),
ParentModels: ModelNames(flags.parent),
rawInput: model,
dryRun: flags["dry-run"],
context: context,
useTs: isTypescript,
useTs: getIsTypescript(),
})
await generator.run()
}

View File

@@ -1,22 +1,14 @@
import {Command} from "../command"
import * as path from "path"
import {RecipeExecutor} from "@blitzjs/installer"
import _got from "got"
import type {RecipeExecutor} from "@blitzjs/installer"
import {log} from "@blitzjs/display"
import {dedent} from "../utils/dedent"
import {Stream} from "stream"
import {promisify} from "util"
import tar from "tar"
import {mkdirSync, readFileSync, existsSync} from "fs-extra"
import rimraf from "rimraf"
import spawn from "cross-spawn"
import * as os from "os"
import {setupTsnode} from "../utils/setup-ts-node"
const pipeline = promisify(Stream.pipeline)
async function got(url: string) {
return _got(url).catch((e) => Boolean(console.error(e)) || e)
return require("got")(url).catch((e: any) => Boolean(console.error(e)) || e)
}
async function gotJSON(url: string) {
@@ -28,7 +20,7 @@ async function isUrlValid(url: string) {
}
function requireJSON(file: string) {
return JSON.parse(readFileSync(file).toString("utf-8"))
return JSON.parse(require("fs-extra").readFileSync(file).toString("utf-8"))
}
const GH_ROOT = "https://github.com/"
@@ -115,10 +107,13 @@ export class Install extends Command {
defaultBranch: string,
subdirectory?: string,
): Promise<string> {
const recipeDir = path.join(os.tmpdir(), `blitz-recipe-${repoFullName.replace("/", "-")}`)
const recipeDir = require("path").join(
require("os").tmpdir(),
`blitz-recipe-${repoFullName.replace("/", "-")}`,
)
// clean up from previous run in case of error
rimraf.sync(recipeDir)
mkdirSync(recipeDir)
require("rimraf").sync(recipeDir)
require("fs-extra").mkdirSync(recipeDir)
process.chdir(recipeDir)
const repoName = repoFullName.split("/")[1]
@@ -127,8 +122,8 @@ export class Install extends Command {
const extractPath = subdirectory ? [`${repoName}-${defaultBranch}/${subdirectory}`] : undefined
const depth = subdirectory ? subdirectory.split("/").length + 1 : 1
await pipeline(
_got.stream(`${CODE_ROOT}${repoFullName}/tar.gz/${defaultBranch}`),
tar.extract({strip: depth}, extractPath),
require("got").stream(`${CODE_ROOT}${repoFullName}/tar.gz/${defaultBranch}`),
require("tar").extract({strip: depth}, extractPath),
)
return recipeDir
@@ -149,9 +144,11 @@ export class Install extends Command {
}
async run() {
setupTsnode()
require("../utils/setup-ts-node").setupTsnode()
const {args} = this.parse(Install)
const pkgManager = existsSync(path.resolve("yarn.lock")) ? "yarn" : "npm"
const pkgManager = require("fs-extra").existsSync(require("path").resolve("yarn.lock"))
? "yarn"
: "npm"
const originalCwd = process.cwd()
const recipeInfo = this.normalizeRecipePath(args.recipe)
@@ -178,21 +175,21 @@ export class Install extends Command {
spinner = log.spinner("Installing package.json dependencies").start()
await new Promise((resolve) => {
const installProcess = spawn(pkgManager, ["install"])
const installProcess = require("cross-spawn")(pkgManager, ["install"])
installProcess.on("exit", resolve)
})
spinner.stop()
const recipePackageMain = requireJSON("./package.json").main
const recipeEntry = path.resolve(recipePackageMain)
const recipeEntry = require("path").resolve(recipePackageMain)
process.chdir(originalCwd)
await this.installRecipeAtPath(recipeEntry)
rimraf.sync(recipeRepoPath)
require("rimraf").sync(recipeRepoPath)
}
} else {
await this.installRecipeAtPath(path.resolve(args.recipe))
await this.installRecipeAtPath(require("path").resolve(args.recipe))
}
}
}

View File

@@ -1,12 +1,10 @@
import * as path from "path"
import {flags} from "@oclif/command"
import {Command} from "../command"
import {AppGenerator} from "@blitzjs/generator"
import type {AppGeneratorOptions} from "@blitzjs/generator"
import chalk from "chalk"
import hasbin from "hasbin"
import {log} from "@blitzjs/display"
const debug = require("debug")("blitz:new")
import {PromptAbortedError} from "../errors/prompt-aborted"
export interface Flags {
@@ -47,6 +45,10 @@ export class New extends Command {
"dry-run": flags.boolean({
description: "show what files will be created without writing them to disk",
}),
"no-git": flags.boolean({
description: "Skip git repository creation",
default: false,
}),
}
async run() {
@@ -54,26 +56,70 @@ export class New extends Command {
debug("args: ", args)
debug("flags: ", flags)
const destinationRoot = path.resolve(args.name)
const appName = path.basename(destinationRoot)
const generator = new AppGenerator({
destinationRoot,
appName,
dryRun: flags["dry-run"],
useTs: !flags.js,
yarn: !flags.npm,
version: this.config.version,
skipInstall: flags["skip-install"],
})
try {
const destinationRoot = require("path").resolve(args.name)
const appName = require("path").basename(destinationRoot)
const formChoices: Array<{name: AppGeneratorOptions["form"]; message?: string}> = [
{name: "React Final Form", message: "React Final Form (recommended)"},
{name: "React Hook Form"},
{name: "Formik"},
]
const promptResult: any = await this.enquirer.prompt({
type: "select",
name: "form",
message: "Pick a form library (you can switch to something else later if you want)",
choices: formChoices,
})
const {"dry-run": dryRun, "skip-install": skipInstall, npm} = flags
const generator = new (require("@blitzjs/generator").AppGenerator)({
destinationRoot,
appName,
dryRun,
useTs: !flags.js,
yarn: !npm,
form: promptResult.form,
version: this.config.version,
skipInstall,
skipGit: flags["no-git"],
})
this.log("\n" + log.withBrand("Hang tight while we set up your new Blitz app!") + "\n")
await generator.run()
const postInstallSteps = [`cd ${args.name}`]
const needsInstall = dryRun || skipInstall
if (needsInstall) {
postInstallSteps.push(npm ? "npm install" : "yarn")
postInstallSteps.push("blitz db migrate (when asked, you can name the migration anything)")
} else {
const spinner = log.spinner(log.withBrand("Initializing SQLite database")).start()
try {
// Required in order for DATABASE_URL to be available
require("dotenv-expand")(require("dotenv-flow").config({silent: true}))
await require("./db").Db.run(["migrate", "--name", "Initial Migration"])
spinner.succeed()
} catch {
spinner.fail()
postInstallSteps.push(
"blitz db migrate (when asked, you can name the migration anything)",
)
}
}
postInstallSteps.push("blitz start")
this.log("\n" + log.withBrand("Your new Blitz app is ready! Next steps:") + "\n")
this.log(chalk.yellow(` 1. cd ${args.name}`))
this.log(chalk.yellow(` 2. blitz db migrate`))
this.log(chalk.yellow(` 3. blitz start`))
postInstallSteps.forEach((step, index) => {
this.log(chalk.yellow(` ${index + 1}. ${step}`))
})
this.log("") // new line
} catch (err) {
if (err instanceof PromptAbortedError) this.exit(0)

View File

@@ -0,0 +1,46 @@
import {Command} from "@oclif/command"
import {join} from "path"
import pkgDir from "pkg-dir"
import {log} from "@blitzjs/display"
import {runMigrate} from "./db"
const projectRoot = pkgDir.sync() || process.cwd()
export class Seed extends Command {
static description = `Fill database with seed data`
async run() {
log.branded("Seeding database")
let spinner = log.spinner("Loading seeds").start()
let seeds: Function
try {
seeds = require(join(projectRoot, "db/seeds")).default
if (seeds === undefined) {
throw new Error(`Cant find default export from db/seeds`)
}
} catch (err) {
log.error(err)
this.error(`Couldn't import default from db/seeds.ts or db/seeds/index.ts file`)
}
spinner.succeed()
spinner = log.spinner("Checking for database migrations").start()
await runMigrate({}, `--schema=${join(process.cwd(), "db", "schema.prisma")}`)
spinner.succeed()
try {
console.log(log.withCaret("Seeding..."))
await seeds()
} catch (err) {
log.error(err)
this.error(`Couldn't run imported function, are you sure it's a function?`)
}
const db = require(join(projectRoot, "db/index")).default
await db.disconnect()
log.success("Done seeding")
}
}

View File

@@ -1,7 +1,5 @@
import {Command, flags} from "@oclif/command"
import {dev, prod} from "@blitzjs/server"
import {runPrismaGeneration} from "./db"
import {Command, flags} from "@oclif/command"
export class Start extends Command {
static description = "Start a development server"
@@ -19,6 +17,13 @@ export class Start extends Command {
char: "H",
description: "Set server hostname",
}),
inspect: flags.boolean({
description: "Enable the Node.js inspector",
}),
["no-incremental-build"]: flags.boolean({
description:
"Disable incremental build and start from a fresh cache. Incremental build is automatically enabled for development mode and disabled during `blitz build` or when the `--production` flag is supplied.",
}),
}
async run() {
@@ -28,13 +33,15 @@ export class Start extends Command {
rootFolder: process.cwd(),
port: flags.port,
hostname: flags.hostname,
inspect: flags.inspect,
clean: flags["no-incremental-build"],
}
try {
if (flags.production) {
await prod(config, runPrismaGeneration({silent: true}))
await prod(config)
} else {
await dev(config, runPrismaGeneration({silent: true}))
await dev(config)
}
} catch (err) {
console.error(err)

View File

@@ -1,6 +1,5 @@
import {spawn} from "cross-spawn"
import {Command} from "@oclif/command"
import hasYarn from "has-yarn"
export class Test extends Command {
static description = "Run project tests"
@@ -20,7 +19,7 @@ export class Test extends Command {
if (watch) {
watchMode = watch === "watch" || watch === "w"
}
const packageManager = hasYarn() ? "yarn" : "npm"
const packageManager = require("has-yarn")() ? "yarn" : "npm"
if (watchMode) spawn(packageManager, ["test:watch"], {stdio: "inherit"})
else spawn(packageManager, ["test"], {stdio: "inherit"})

View File

@@ -1,7 +1,11 @@
require("v8-compile-cache")
const cacheFile = require("path").join(__dirname, ".blitzjs-cli-cache")
const lazyLoad = require("@salesforce/lazy-require").default.create(cacheFile)
lazyLoad.start()
import {run as oclifRun} from "@oclif/command"
// Load the .env environment variable so it's available for all commands
require("dotenv").config()
require("dotenv-expand")(require("dotenv-flow").config({silent: true}))
export function run() {
oclifRun()

View File

@@ -0,0 +1,3 @@
export default {
disconnect: () => Promise.resolve(),
}

View File

@@ -0,0 +1,3 @@
export default async () => {
await Promise.resolve(10)
}

View File

@@ -6,7 +6,7 @@ const spawn = jest.fn(() => {
onSpy = jest.fn(function on(_: string, callback: (_: number) => {}) {
callback(0)
})
return {on: onSpy}
return {on: onSpy, off: jest.fn()}
})
jest.doMock("cross-spawn", () => ({spawn}))

View File

@@ -37,30 +37,11 @@ jest.mock(
}),
)
jest.mock(
"../../src/commands/db",
jest.fn(() => {
return {
runPrismaGeneration: jest.fn(),
}
}),
)
describe("Console command", () => {
beforeEach(() => {
jest.resetAllMocks()
})
it("runs PrismaGeneration", async () => {
await Console.prototype.run()
expect(db.runPrismaGeneration).toHaveBeenCalled()
})
it("runs PrismaGeneration with silent allowed", async () => {
await Console.prototype.run()
expect(db.runPrismaGeneration).toHaveBeenCalledWith({silent: true})
})
it("runs repl", async () => {
await Console.prototype.run()
expect(repl.runRepl).toHaveBeenCalled()

View File

@@ -1,14 +1,12 @@
import * as path from "path"
import {resolveBinAsync} from "@blitzjs/server"
let onSpy: jest.Mock
const spawn = jest.fn(() => {
onSpy = jest.fn(function on(_: string, callback: (_: number) => {}) {
callback(0)
})
return {on: onSpy}
let onSpy = jest.fn(function on(_: string, callback: (_: number) => {}) {
callback(0)
})
const spawn = jest.fn(() => ({on: onSpy, off: jest.fn()}))
jest.doMock("cross-spawn", () => ({spawn}))
import {Db} from "../../src/commands/db"
@@ -18,6 +16,8 @@ let prismaBin: string
let migrateSaveParams: any[]
let migrateUpDevParams: any[]
let migrateUpProdParams: any[]
let migrateSaveWithNameParams: any[]
let migrateSaveWithUnknownParams: any[]
beforeAll(async () => {
schemaArg = `--schema=${path.join(process.cwd(), "db", "schema.prisma")}`
prismaBin = await resolveBinAsync("@prisma/cli", "prisma")
@@ -25,17 +25,27 @@ beforeAll(async () => {
migrateSaveParams = [
prismaBin,
["migrate", "save", schemaArg, "--create-db", "--experimental"],
{stdio: "inherit"},
{stdio: "inherit", env: process.env},
]
migrateUpDevParams = [
prismaBin,
["migrate", "up", schemaArg, "--create-db", "--experimental"],
{stdio: "inherit"},
{stdio: "inherit", env: process.env},
]
migrateUpProdParams = [
prismaBin,
["migrate", "up", schemaArg, "--create-db", "--experimental", "--auto-approve"],
{stdio: "inherit"},
{stdio: "inherit", env: process.env},
]
migrateSaveWithNameParams = [
prismaBin,
["migrate", "save", schemaArg, "--create-db", "--experimental", "--name", "name"],
{stdio: "ignore", env: process.env},
]
migrateSaveWithUnknownParams = [
prismaBin,
["migrate", "save", schemaArg, "--create-db", "--experimental"],
{stdio: "inherit", env: process.env},
]
})
@@ -50,23 +60,35 @@ describe("Db command", () => {
function expectDbMigrateOutcome() {
expect(spawn).toBeCalledWith(...migrateSaveParams)
expect(spawn.mock.calls.length).toBe(3)
expect(spawn).toHaveBeenCalledTimes(3)
// following expection is not working
//expect(onSpy).toHaveBeenCalledWith(0);
expect(onSpy).toHaveBeenCalledTimes(3)
expect(spawn).toBeCalledWith(...migrateUpDevParams)
}
function expectProductionDbMigrateOutcome() {
expect(spawn.mock.calls.length).toBe(2)
expect(spawn).toHaveBeenCalledTimes(2)
// following expection is not working
//expect(onSpy).toHaveBeenCalledWith(0);
expect(onSpy).toHaveBeenCalledTimes(2)
expect(spawn).toBeCalledWith(...migrateUpProdParams)
}
function expectDbMigrateWithNameOutcome() {
expect(spawn).toBeCalledWith(...migrateSaveWithNameParams)
expect(spawn).toHaveBeenCalledTimes(3)
expect(onSpy).toHaveBeenCalledTimes(3)
}
function expectDbMigrateWithUnknownFlag() {
expect(spawn).toBeCalledWith(...migrateSaveWithUnknownParams)
expect(spawn).toHaveBeenCalledTimes(3)
expect(onSpy).toHaveBeenCalledTimes(3)
}
it("runs db help when no command given", async () => {
// When running the help command oclif exits with code 0
// Unfortantely it treats this as an exception and throws accordingly
@@ -128,16 +150,38 @@ describe("Db command", () => {
expectProductionDbMigrateOutcome()
})
it("runs db migrate silently with the right args when name flag is used", async () => {
await Db.run(["migrate", "--name", "name"])
expectDbMigrateWithNameOutcome()
})
it("runs db migrate. (with unknown flags)", async () => {
await Db.run(["migrate", "--hoge", "aaa"])
expectDbMigrateWithUnknownFlag()
})
it("runs db introspect", async () => {
await Db.run(["introspect"])
expect(spawn).toHaveBeenCalled()
expect(spawn).toHaveBeenCalledWith(prismaBin, ["introspect", schemaArg], {
stdio: "inherit",
env: process.env,
})
expect(spawn).toHaveBeenCalledWith(prismaBin, ["generate", schemaArg], {
stdio: "inherit",
env: process.env,
})
})
it("runs db studio", async () => {
await Db.run(["studio"])
expect(spawn).toHaveBeenCalled()
expect(spawn).toHaveBeenCalledWith(prismaBin, ["studio", schemaArg, "--experimental"], {
stdio: "inherit",
env: process.env,
})
})
it("does not run db in case of invalid command", async () => {

View File

@@ -6,6 +6,7 @@ import * as os from "os"
import fetch from "node-fetch"
import nock from "nock"
import rimraf from "rimraf"
import {stdout} from "stdout-stderr"
jest.setTimeout(120 * 1000)
const blitzCliPackageJson = require("../../package.json")
@@ -25,23 +26,52 @@ async function getBlitzDistTags() {
*/
const testIfNotWindows = process.platform === "win32" ? test.skip : test
jest.mock("enquirer", () => {
return jest.fn().mockImplementation(() => {
return {
prompt: jest.fn().mockImplementation(() => ({form: "React Final Form"})),
}
})
})
describe("`new` command", () => {
describe("when scaffolding new project", () => {
beforeEach(() => {
stdout.stripColor = true
stdout.start()
})
afterEach(() => {
stdout.stop()
})
jest.setTimeout(120 * 1000)
function makeTempDir() {
const tmpDirPath = path.join(os.tmpdir(), "blitzjs-test-")
return fs.mkdtempSync(tmpDirPath)
}
async function whileStayingInCWD(task: () => PromiseLike<void>) {
const oldCWD = process.cwd()
await task()
process.chdir(oldCWD)
}
async function withNewApp(test: (dirName: string, packageJson: any) => Promise<void> | void) {
function makeTempDir() {
const tmpDirPath = path.join(os.tmpdir(), "blitzjs-test-")
function getStepsFromOutput() {
const output = stdout.output
const exp = /^ \d. (.*)$/gm
const matches = []
let match
return fs.mkdtempSync(tmpDirPath)
while ((match = exp.exec(output)) != null) {
matches.push(match[1].trim())
}
return matches
}
async function withNewApp(test: (dirName: string, packageJson: any) => Promise<void> | void) {
const tempDir = makeTempDir()
await whileStayingInCWD(() => New.run([tempDir, "--skip-install"]))
@@ -60,7 +90,7 @@ describe("`new` command", () => {
testIfNotWindows(
"pins Blitz to the current version",
async () =>
await withNewApp(async (_, packageJson) => {
await withNewApp(async (dirName, packageJson) => {
const {
dependencies: {blitz: blitzVersion},
} = packageJson
@@ -71,9 +101,24 @@ describe("`new` command", () => {
} else {
expect(blitzVersion).toEqual(latest)
}
expect(getStepsFromOutput()).toStrictEqual([
`cd ${dirName}`,
"yarn",
"blitz db migrate (when asked, you can name the migration anything)",
"blitz start",
])
}),
)
testIfNotWindows("performs all steps on a full install", async () => {
const tempDir = makeTempDir()
await whileStayingInCWD(() => New.run([tempDir]))
rimraf.sync(tempDir)
expect(getStepsFromOutput()).toStrictEqual([`cd ${tempDir}`, "blitz start"])
})
it("fetches latest version from template", async () => {
const expectedVersion = "3.0.0"
const templatePackage = {name: "eslint-plugin-react-hooks", version: "3.x"}

View File

@@ -0,0 +1,81 @@
import {join} from "path"
import pkgDir from "pkg-dir"
import {resolveBinAsync} from "@blitzjs/server"
let onSpy = jest.fn(function on(_: string, callback: (_: number) => {}) {
callback(0)
})
const spawn = jest.fn(() => ({on: onSpy, off: jest.fn()}))
jest.doMock("cross-spawn", () => ({spawn}))
pkgDir.sync = jest.fn(() => join(__dirname, "../__fixtures__/"))
let seedsFn: jest.Mock
jest.doMock("../__fixtures__/db/seeds", () => {
seedsFn = jest.fn()
return {default: seedsFn}
})
let disconnect: jest.Mock
jest.doMock("../__fixtures__/db", () => {
disconnect = jest.fn()
return {default: {disconnect}}
})
import {Seed} from "../../src/commands/seed"
let schemaArg: string
let prismaBin: string
let migrateUpDevParams: any[]
let migrateSaveParams: any[]
beforeAll(async () => {
schemaArg = `--schema=${join(process.cwd(), "db", "schema.prisma")}`
prismaBin = await resolveBinAsync("@prisma/cli", "prisma")
migrateSaveParams = [
prismaBin,
["migrate", "save", schemaArg, "--create-db", "--experimental"],
{stdio: "inherit", env: process.env},
]
migrateUpDevParams = [
prismaBin,
["migrate", "up", schemaArg, "--create-db", "--experimental"],
{stdio: "inherit", env: process.env},
]
jest.spyOn(global.console, "log").mockImplementation(jest.fn((output: string) => {}))
})
describe("Start command", () => {
beforeEach(() => {
jest.clearAllMocks()
})
afterEach(() => {
process.env.NODE_ENV = "test"
})
function expectDbMigrateOutcome() {
expect(spawn).toBeCalledWith(...migrateSaveParams)
expect(spawn.mock.calls.length).toBe(3)
expect(onSpy).toHaveBeenCalledTimes(3)
expect(spawn).toBeCalledWith(...migrateUpDevParams)
}
it("runs migrations and closes db at the end", async () => {
await Seed.run()
expectDbMigrateOutcome()
})
it("seeds the db", async () => {
await Seed.run()
expect(seedsFn).toBeCalled()
})
it("closes connection at the end", async () => {
await Seed.run()
expect(disconnect).toBeCalled()
})
})

View File

@@ -8,7 +8,7 @@ const spawn = jest.fn(() => {
onSpy = jest.fn(function on(_: string, callback: (_: number) => {}) {
callback(0)
})
return {on: onSpy}
return {on: onSpy, off: jest.fn()}
})
jest.doMock("cross-spawn", () => ({spawn}))

View File

@@ -10,6 +10,7 @@
"sourceMap": false,
"esModuleInterop": true,
"types": [],
"noEmit": false,
"lib": ["dom", "dom.iterable", "ES2018"]
},
"include": ["src/**/*", "types"],

View File

@@ -7,7 +7,7 @@
"config"
],
"author": "Fran Zekan <zekan.fran369@gmail.com>",
"version": "0.17.1-canary.5",
"version": "0.23.1-canary.0",
"license": "MIT",
"scripts": {
"clean": "rimraf dist",

View File

@@ -0,0 +1,16 @@
module.exports = {
extends: ["../../.eslintrc.js"],
plugins: ["es5", "es"],
rules: {
"es/no-object-fromentries": "error",
"es5/no-generators": "error",
"es5/no-typeof-symbol": "error",
"es5/no-es6-methods": "error",
"es5/no-es6-static-methods": [
"error",
{
exceptMethods: ["Object.assign"],
},
],
},
}

View File

@@ -1,7 +1,7 @@
{
"name": "@blitzjs/core",
"description": "Blitz.js core functionality",
"version": "0.17.1-canary.5",
"version": "0.23.1-canary.0",
"license": "MIT",
"scripts": {
"clean": "rimraf dist",
@@ -40,15 +40,18 @@
"url": "https://github.com/blitz-js/blitz"
},
"dependencies": {
"@blitzjs/config": "0.17.1-canary.5",
"@blitzjs/display": "0.17.1-canary.5",
"@blitzjs/config": "0.23.1-canary.0",
"@blitzjs/display": "0.23.1-canary.0",
"bad-behavior": "1.0.1",
"cookie-session": "1.4.0",
"deepmerge": "4.2.2",
"lodash": "^4.17.19",
"lodash-es": "^4.17.15",
"passport": "0.4.1",
"pretty-ms": "6.0.1",
"react-query": "2.5.11",
"react-query": "2.23.0",
"serialize-error": "6.0.0",
"superjson": "1.2.1",
"superjson": "1.2.2",
"url": "0.11.0"
},
"gitHead": "d3b9fce0bdd251c2b1890793b0aa1cd77c1c0922"

View File

@@ -1,3 +1,6 @@
import {NextPage, NextComponentType, NextPageContext} from "next"
import {AppProps as NextAppProps} from "next/app"
export * from "./use-query"
export * from "./use-paginated-query"
export * from "./use-params"
@@ -22,13 +25,10 @@ export {
GetServerSideProps,
InferGetStaticPropsType,
InferGetServerSidePropsType,
NextPage as BlitzPage,
NextApiRequest as BlitzApiRequest,
NextApiResponse as BlitzApiResponse,
} from "next"
export {AppProps} from "next/app"
export {default as Head} from "next/head"
export {default as Link} from "next/link"
@@ -49,4 +49,14 @@ export {default as dynamic} from "next/dynamic"
export {default as ErrorComponent} from "next/error"
export type BlitzComponentType<C = NextPageContext, IP = {}, P = {}> = NextComponentType<C, IP, P>
export interface AppProps<P = {}> extends NextAppProps<P> {
Component: BlitzComponentType<NextPageContext, any, P> & {
getLayout?: (component: JSX.Element) => JSX.Element
}
}
export type BlitzPage<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (component: JSX.Element) => JSX.Element
}
export {isLocalhost} from "./utils/index"

View File

@@ -1,10 +1,14 @@
/* eslint-disable es5/no-for-of -- file only used on the server */
/* eslint-disable es5/no-es6-methods -- file only used on the server */
import {BlitzApiRequest, BlitzApiResponse} from "."
import {IncomingMessage, ServerResponse} from "http"
import {EnhancedResolverModule} from "./rpc"
import {getConfig} from "@blitzjs/config"
import {log} from "@blitzjs/display"
export interface MiddlewareRequest extends BlitzApiRequest {}
export interface MiddlewareRequest extends BlitzApiRequest {
protocol?: string
}
export interface MiddlewareResponse extends BlitzApiResponse {
/**
* This will be passed as the second argument to Blitz queries/mutations.

View File

@@ -1,4 +1,6 @@
import {BlitzApiRequest, BlitzApiResponse} from "."
/* eslint-disable es5/no-for-of -- file only used on the server */
/* eslint-disable es5/no-es6-methods -- file only used on the server */
import {BlitzApiRequest, BlitzApiResponse, ConnectMiddleware} from "."
import {
getAllMiddlewareForModule,
handleRequestWithMiddleware,
@@ -7,14 +9,17 @@ import {
} from "./middleware"
import {SessionContext, PublicData} from "./supertokens"
import {log} from "@blitzjs/display"
import passport, {Strategy} from "passport"
import passport, {AuthenticateOptions, Strategy} from "passport"
import cookieSession from "cookie-session"
import {isLocalhost} from "./utils/index"
import {secureProxyMiddleware} from "./secure-proxy-middleware"
export type BlitzPassportConfig = {
successRedirectUrl?: string
errorRedirectUrl?: string
authenticateOptions?: AuthenticateOptions
strategies: Required<Strategy>[]
secureProxy?: boolean
}
export type VerifyCallbackResult = {
@@ -34,19 +39,23 @@ const INTERNAL_REDIRECT_URL_KEY = "_redirectUrl"
export function passportAuth(config: BlitzPassportConfig) {
return async function authHandler(req: BlitzApiRequest, res: BlitzApiResponse) {
const cookieSessionMiddleware = cookieSession({
secret: process.env.SESSION_SECRET_KEY || "default-dev-secret",
secure: process.env.NODE_ENV === "production" && !isLocalhost(req),
})
const passportMiddleware = passport.initialize()
const middleware: Middleware[] = [
// TODO - fix TS type - shouldn't need `any` here
connectMiddleware(
cookieSession({
secret: process.env.SESSION_SECRET_KEY || "default-dev-secret",
secure: process.env.NODE_ENV === "production" && !isLocalhost(req),
}) as any,
),
// TODO - fix TS type - shouldn't need `any` here
connectMiddleware(passport.initialize() as any),
connectMiddleware(cookieSessionMiddleware as ConnectMiddleware),
connectMiddleware(passportMiddleware as ConnectMiddleware),
connectMiddleware(passport.session()),
]
if (config.secureProxy) {
middleware.push(secureProxyMiddleware)
}
if (!req.query.auth.length) {
return res.status(404).end()
}
@@ -71,7 +80,9 @@ export function passportAuth(config: BlitzPassportConfig) {
return next()
})
}
middleware.push(connectMiddleware(passport.authenticate(strategy.name)))
middleware.push(
connectMiddleware(passport.authenticate(strategy.name, {...config.authenticateOptions})),
)
} else if (req.query.auth[1] === "callback") {
log.info(`Processing callback for ${strategy.name}...`)
middleware.push(
@@ -109,7 +120,7 @@ export function passportAuth(config: BlitzPassportConfig) {
"/"
if (error) {
redirectUrl += "?authError=" + error.toString()
redirectUrl += "?authError=" + encodeURIComponent(error.toString())
res.setHeader("Location", redirectUrl)
res.statusCode = 302
res.end()

View File

@@ -12,12 +12,14 @@ import {
} from "./supertokens"
import {CSRFTokenMismatchError} from "./errors"
import {serialize, deserialize} from "superjson"
import merge from "deepmerge"
type Options = {
fromQueryHook?: boolean
resultOfGetFetchMore?: any
}
export async function executeRpcCall(url: string, params: any, opts: Options = {}) {
export function executeRpcCall(url: string, params: any, opts: Options = {}) {
if (typeof window === "undefined") return
const headers: Record<string, any> = {
@@ -29,59 +31,83 @@ export async function executeRpcCall(url: string, params: any, opts: Options = {
headers[HEADER_CSRF] = antiCSRFToken
}
// query hook already serializes the params because otherwise react-query will mess it up
const serialized = opts.fromQueryHook ? params : serialize(params)
const result = await window.fetch(url, {
method: "POST",
headers,
credentials: "include",
redirect: "follow",
body: JSON.stringify({
// TODO remove `|| null` once superjson allows `undefined`
params: serialized.json || null,
meta: {
params: serialized.meta,
},
}),
})
if (result.headers) {
for (const [name] of result.headers.entries()) {
if (name.toLowerCase() === HEADER_PUBLIC_DATA_TOKEN) publicDataStore.updateState()
if (name.toLowerCase() === HEADER_SESSION_REVOKED) publicDataStore.clear()
if (name.toLowerCase() === HEADER_CSRF_ERROR) {
throw new CSRFTokenMismatchError()
}
let serialized
if (opts.fromQueryHook) {
// We have to serialize query arguments inside the hooks, otherwise react-query will use
// JSON.parse(JSON.stringify) so by the time the arguments come here the real JS objects are lost
serialized = params
if (opts.resultOfGetFetchMore) {
// useInfiniteQuery usually passes in extra pageParams here that come from getFetchMore()
// This isn't serialized inside useInfiniteQuery because this data is provided separately
// by react-query
serialized = merge(params, serialize(opts.resultOfGetFetchMore))
}
}
let payload
try {
payload = await result.json()
} catch (error) {
throw new Error(`Failed to parse json from request to ${url}`)
}
if (payload.error) {
const error = deserializeError(payload.error)
// We don't clear the publicDataStore for anonymous users
if (error.name === "AuthenticationError" && publicDataStore.getData().userId) {
publicDataStore.clear()
}
throw error
} else {
const data =
payload.result === undefined
? undefined
: deserialize({json: payload.result, meta: payload.meta?.result})
if (!opts.fromQueryHook) {
const queryKey = getQueryKey(url, params)
queryCache.setQueryData(queryKey, data)
}
return data
serialized = serialize(params)
}
// Create a new AbortController instance for this request
const controller = new AbortController()
const promise: CancellablePromise<any> = window
.fetch(url, {
method: "POST",
headers,
credentials: "include",
redirect: "follow",
body: JSON.stringify({
// TODO remove `|| null` once superjson allows `undefined`
params: serialized.json || null,
meta: {
params: serialized.meta,
},
}),
signal: controller.signal,
})
.then(async (result) => {
if (result.headers) {
if (result.headers.get(HEADER_PUBLIC_DATA_TOKEN)) {
publicDataStore.updateState()
}
if (result.headers.get(HEADER_SESSION_REVOKED)) {
publicDataStore.clear()
}
if (result.headers.get(HEADER_CSRF_ERROR)) {
throw new CSRFTokenMismatchError()
}
}
let payload
try {
payload = await result.json()
} catch (error) {
throw new Error(`Failed to parse json from request to ${url}`)
}
if (payload.error) {
const error = deserializeError(payload.error)
// We don't clear the publicDataStore for anonymous users
if (error.name === "AuthenticationError" && publicDataStore.getData().userId) {
publicDataStore.clear()
}
throw error
} else {
const data =
payload.result === undefined
? undefined
: deserialize({json: payload.result, meta: payload.meta?.result})
if (!opts.fromQueryHook) {
const queryKey = getQueryKey(url, params)
queryCache.setQueryData(queryKey, data)
}
return data
}
})
promise.cancel = () => controller.abort()
return promise
}
executeRpcCall.warm = (url: string) => {
@@ -99,13 +125,18 @@ interface ResolverEnhancement {
apiUrl: string
}
}
interface CancellablePromise<T> extends Promise<T> {
cancel?: Function
}
export interface RpcFunction {
(params: any, opts: any): Promise<any>
(params: any, opts: any): CancellablePromise<any>
}
export interface EnhancedRpcFunction extends RpcFunction, ResolverEnhancement {}
export interface EnhancedResolverModule extends ResolverEnhancement {
(input: any, ctx: Record<string, any>): Promise<unknown>
(input: any, ctx: Record<string, any>): CancellablePromise<unknown>
middleware?: Middleware[]
}

View File

@@ -0,0 +1,54 @@
// @ts-ignore
import {Request} from "express"
import {secureProxyMiddleware} from "./secure-proxy-middleware"
import {Socket} from "net"
// @ts-ignore
let reqSecure: Request = {
connection: new Socket(),
method: "GET",
url: "/stuff?q=thing",
headers: {
"x-forwarded-proto": "https",
},
}
// @ts-ignore
let reqHttp: Request = {
connection: new Socket(),
method: "GET",
url: "/stuff?q=thing",
headers: {
"x-forwarded-proto": "http",
},
}
// @ts-ignore
let reqNoHeader: Request = {
connection: new Socket(),
method: "GET",
url: "/stuff?q=thing",
}
const res = {}
describe("secure proxy middleware", () => {
it("should set https protocol if X-Forwarded-Proto is https", () => {
// @ts-ignore
secureProxyMiddleware(reqSecure, res, () => null)
expect(reqSecure.protocol).toEqual("https")
})
it("should set http protocol if X-Forwarded-Proto is absent", () => {
// @ts-ignore
secureProxyMiddleware(reqNoHeader, res, () => null)
expect(reqNoHeader.protocol).toEqual("http")
})
it("should set http protocol if X-Forwarded-Proto is http", () => {
// @ts-ignore
secureProxyMiddleware(reqHttp, res, () => null)
expect(reqHttp.protocol).toEqual("http")
})
})

View File

@@ -0,0 +1,23 @@
import {Middleware, MiddlewareRequest, MiddlewareResponse} from "middleware"
export const secureProxyMiddleware: Middleware = function (
req: MiddlewareRequest,
_res: MiddlewareResponse,
next: (error?: Error) => void,
) {
req.protocol = getProtocol(req)
next()
}
function getProtocol(req: MiddlewareRequest) {
// @ts-ignore
// For some reason there is no encrypted on socket while it is expected
if (req.connection.encrypted) {
return "https"
}
const forwardedProto = req.headers && (req.headers["x-forwarded-proto"] as string)
if (forwardedProto) {
return forwardedProto.split(/\s*,\s*/)[0]
}
return "http"
}

View File

@@ -1,5 +1,7 @@
import {useState, useEffect} from "react"
import {useState} from "react"
import BadBehavior from "bad-behavior"
import {useIsomorphicLayoutEffect} from "./utils/hooks"
import {queryCache} from "react-query"
export const TOKEN_SEPARATOR = ";"
export const HANDLE_SEPARATOR = ":"
@@ -26,13 +28,13 @@ function assert(condition: any, message: string): asserts condition {
}
export interface PublicData extends Record<any, any> {
userId: string | number | null
userId: any
roles: string[]
}
export interface SessionModel extends Record<any, any> {
handle: string
userId?: string | number
userId?: any
expiresAt?: Date
hashedSessionToken?: string
antiCSRFToken?: string
@@ -45,7 +47,7 @@ export type SessionConfig = {
method?: "essential" | "advanced"
sameSite?: "none" | "lax" | "strict"
getSession: (handle: string) => Promise<SessionModel | null>
getSessions: (userId: string | number) => Promise<SessionModel[]>
getSessions: (userId: any) => Promise<SessionModel[]>
createSession: (session: SessionModel) => Promise<SessionModel>
updateSession: (handle: string, session: Partial<SessionModel>) => Promise<SessionModel>
deleteSession: (handle: string) => Promise<SessionModel>
@@ -56,7 +58,7 @@ export interface SessionContext {
/**
* null if anonymous
*/
userId: string | number | null
userId: any
roles: string[]
handle: string | null
publicData: PublicData
@@ -72,6 +74,25 @@ export interface SessionContext {
setPublicData: (data: Record<any, any>) => Promise<void>
}
// Taken from https://github.com/HenrikJoreteg/cookie-getter
// simple commonJS cookie reader, best perf according to http://jsperf.com/cookie-parsing
export function readCookie(name: string) {
if (typeof document === "undefined") return null
const cookie = document.cookie
const setPos = cookie.search(new RegExp("\\b" + name + "="))
const stopPos = cookie.indexOf(";", setPos)
let res
if (!~setPos) return null
res = decodeURIComponent(cookie.substring(setPos, ~stopPos ? stopPos : undefined).split("=")[1])
return res.charAt(0) === "{" ? JSON.parse(res) : res
}
export const setCookie = (name: string, value: string, expires: string) => {
const result = `${name}=${value};path=/;expires=${expires}`
document.cookie = result
}
export const deleteCookie = (name: string) => setCookie(name, "", "Thu, 01 Jan 1970 00:00:01 GMT")
export const getAntiCSRFToken = () => readCookie(COOKIE_CSRF_TOKEN)
export const getPublicDataToken = () => readCookie(COOKIE_PUBLIC_DATA_TOKEN)
@@ -133,6 +154,7 @@ export const publicDataStore = {
},
clear() {
deleteCookie(COOKIE_PUBLIC_DATA_TOKEN)
queryCache.clear()
this.updateState()
},
}
@@ -140,35 +162,17 @@ publicDataStore.initialize()
export const useSession = () => {
const [publicData, setPublicData] = useState(emptyPublicData)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
useIsomorphicLayoutEffect(() => {
// Initialize on mount
setPublicData(publicDataStore.getData())
setIsLoading(false)
const subscription = publicDataStore.observable.subscribe(setPublicData)
return subscription.unsubscribe
}, [])
return publicData
}
// Taken from https://github.com/HenrikJoreteg/cookie-getter
// simple commonJS cookie reader, best perf according to http://jsperf.com/cookie-parsing
export function readCookie(name: string) {
if (typeof document === "undefined") return null
const cookie = document.cookie
const setPos = cookie.search(new RegExp("\\b" + name + "="))
const stopPos = cookie.indexOf(";", setPos)
let res
if (!~setPos) return null
res = decodeURIComponent(cookie.substring(setPos, ~stopPos ? stopPos : undefined).split("=")[1])
return res.charAt(0) === "{" ? JSON.parse(res) : res
}
export const deleteCookie = (name: string) => setCookie(name, "", "Thu, 01 Jan 1970 00:00:01 GMT")
export const setCookie = (name: string, value: string, expires: string) => {
const result = `${name}=${value};path=/;expires=${expires}`
document.cookie = result
return {...publicData, isLoading}
}
/*

View File

@@ -1,13 +1,12 @@
import {
useInfiniteQuery as useInfiniteReactQuery,
InfiniteQueryResult,
InfiniteQueryOptions,
InfiniteQueryConfig,
} from "react-query"
import {useIsDevPrerender, emptyQueryFn, retryFunction} from "./use-query"
import {emptyQueryFn, retryFunction} from "./use-query"
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
import {getQueryCacheFunctions, QueryCacheFunctions} from "./utils/query-cache"
import {getQueryCacheFunctions, QueryCacheFunctions, getInfiniteQueryKey} from "./utils/query-cache"
import {EnhancedRpcFunction} from "./rpc"
import {serialize} from "superjson"
type RestQueryResult<T extends QueryFn> = Omit<
InfiniteQueryResult<PromiseReturnType<T>, any>,
@@ -15,10 +14,12 @@ type RestQueryResult<T extends QueryFn> = Omit<
> &
QueryCacheFunctions<PromiseReturnType<T>[]>
const isServer = typeof window === "undefined"
export function useInfiniteQuery<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
options: InfiniteQueryOptions<PromiseReturnType<T>, any>,
options: InfiniteQueryConfig<PromiseReturnType<T>, any>,
): [PromiseReturnType<T>[], RestQueryResult<T>] {
if (typeof queryFn === "undefined") {
throw new Error("useInfiniteQuery is missing the first argument - it must be a query function")
@@ -30,16 +31,14 @@ export function useInfiniteQuery<T extends QueryFn>(
)
}
const queryRpcFn = useIsDevPrerender()
? emptyQueryFn
: ((queryFn as unknown) as EnhancedRpcFunction)
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
const queryKey = getInfiniteQueryKey(queryFn, params)
const {data, ...queryRest} = useInfiniteReactQuery({
queryKey: [
queryRpcFn._meta.apiUrl,
serialize(typeof params === "function" ? (params as Function)() : params),
],
queryFn: (_: string, params, more?) => queryRpcFn({...params, ...more}, {fromQueryHook: true}),
queryKey,
queryFn: (_infinite: boolean, _apiUrl: string, params: any, resultOfGetFetchMore?: any) =>
queryRpcFn(params, {fromQueryHook: true, resultOfGetFetchMore}),
config: {
suspense: true,
retry: retryFunction,
@@ -49,7 +48,7 @@ export function useInfiniteQuery<T extends QueryFn>(
const rest = {
...queryRest,
...getQueryCacheFunctions<PromiseReturnType<T>>(queryRpcFn._meta.apiUrl),
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
}
return [data as PromiseReturnType<T>[], rest as RestQueryResult<T>]

View File

@@ -1,13 +1,12 @@
import {
usePaginatedQuery as usePaginatedReactQuery,
PaginatedQueryResult,
QueryOptions,
PaginatedQueryConfig,
} from "react-query"
import {useIsDevPrerender, emptyQueryFn, retryFunction} from "./use-query"
import {emptyQueryFn, retryFunction} from "./use-query"
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
import {QueryCacheFunctions, getQueryCacheFunctions} from "./utils/query-cache"
import {QueryCacheFunctions, getQueryCacheFunctions, getQueryKey} from "./utils/query-cache"
import {EnhancedRpcFunction} from "./rpc"
import {serialize} from "superjson"
type RestQueryResult<T extends QueryFn> = Omit<
PaginatedQueryResult<PromiseReturnType<T>>,
@@ -15,10 +14,12 @@ type RestQueryResult<T extends QueryFn> = Omit<
> &
QueryCacheFunctions<PromiseReturnType<T>>
const isServer = typeof window === "undefined"
export function usePaginatedQuery<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
options?: QueryOptions<PaginatedQueryResult<PromiseReturnType<T>>>,
options?: PaginatedQueryConfig<PromiseReturnType<T>>,
): [PromiseReturnType<T>, RestQueryResult<T>] {
if (typeof queryFn === "undefined") {
throw new Error("usePaginatedQuery is missing the first argument - it must be a query function")
@@ -30,16 +31,13 @@ export function usePaginatedQuery<T extends QueryFn>(
)
}
const queryRpcFn = useIsDevPrerender()
? emptyQueryFn
: ((queryFn as unknown) as EnhancedRpcFunction)
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
const queryKey = getQueryKey(queryFn, params)
const {resolvedData, ...queryRest} = usePaginatedReactQuery({
queryKey: [
queryRpcFn._meta.apiUrl,
serialize(typeof params === "function" ? (params as Function)() : params),
],
queryFn: (_: string, params) => queryRpcFn(params, {fromQueryHook: true}),
queryKey,
queryFn: (_apiUrl: string, params: any) => queryRpcFn(params, {fromQueryHook: true}),
config: {
suspense: true,
retry: retryFunction,
@@ -49,7 +47,7 @@ export function usePaginatedQuery<T extends QueryFn>(
const rest = {
...queryRest,
...getQueryCacheFunctions<PromiseReturnType<T>>(queryRpcFn._meta.apiUrl),
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
}
return [resolvedData as PromiseReturnType<T>, rest as RestQueryResult<T>]

View File

@@ -1,5 +1,6 @@
import {useRouter} from "next/router"
import {useRouterQuery} from "./use-router-query"
import {fromPairs} from "lodash"
type ParsedUrlQueryValue = string | string[] | undefined
@@ -31,7 +32,7 @@ function areQueryValuesEqual(value1: ParsedUrlQueryValue, value2: ParsedUrlQuery
}
export function extractRouterParams(routerQuery: ParsedUrlQuery, query: ParsedUrlQuery) {
return Object.fromEntries(
return fromPairs(
Object.entries(routerQuery).filter(
([key, value]) =>
typeof query[key] === "undefined" || !areQueryValuesEqual(value, query[key]),
@@ -51,9 +52,9 @@ export function useParams(returnType?: "string" | "number" | "array") {
if (returnType === "string") {
const params: Record<string, string> = {}
for (const [key, value] of Object.entries(rawParams)) {
if (typeof value === "string") {
params[key] = value
for (const key in rawParams) {
if (typeof rawParams[key] === "string") {
params[key] = rawParams[key] as string
}
}
return params
@@ -61,9 +62,9 @@ export function useParams(returnType?: "string" | "number" | "array") {
if (returnType === "number") {
const params: Record<string, number> = {}
for (const [key, value] of Object.entries(rawParams)) {
if (value) {
params[key] = Number(value)
for (const key in rawParams) {
if (rawParams[key]) {
params[key] = Number(rawParams[key])
}
}
return params
@@ -71,9 +72,9 @@ export function useParams(returnType?: "string" | "number" | "array") {
if (returnType === "array") {
const params: Record<string, Array<string | undefined>> = {}
for (const [key, value] of Object.entries(rawParams)) {
if (Array.isArray(value)) {
params[key] = value
for (const key in rawParams) {
if (Array.isArray(rawParams[key])) {
params[key] = rawParams[key] as Array<string | undefined>
}
}
return params

View File

@@ -1,8 +1,7 @@
import {useQuery as useReactQuery, QueryResult, QueryOptions} from "react-query"
import {useQuery as useReactQuery, QueryResult, QueryConfig} from "react-query"
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
import {QueryCacheFunctions, getQueryCacheFunctions} from "./utils/query-cache"
import {QueryCacheFunctions, getQueryCacheFunctions, getQueryKey} from "./utils/query-cache"
import {EnhancedRpcFunction} from "./rpc"
import {serialize} from "superjson"
type RestQueryResult<T extends QueryFn> = Omit<QueryResult<PromiseReturnType<T>>, "data"> &
QueryCacheFunctions<PromiseReturnType<T>>
@@ -20,35 +19,19 @@ export const emptyQueryFn: EnhancedRpcFunction = (() => {
const isServer = typeof window === "undefined"
// NOTE - this is only for use inside useQuery
export const useIsDevPrerender = () => {
if (process.env.NODE_ENV === "production") {
return false
} else {
// useQuery is only for client-side data fetching, so if it's running on the
// server, it's for pre-render
return isServer
}
}
export const retryFunction = (failureCount: number, error: any) => {
if (process.env.NODE_ENV !== "production") return false
if (error.name === "AuthenticationError") return false
if (error.name === "AuthorizationError") return false
if (error.name === "CSRFTokenMismatchError") return false
if (error.name === "NotFoundError") return false
if (error.name === "ZodError") return false
// Prisma errors
if (typeof error.code === "string" && error.code.startsWith("P")) return false
if (failureCount > 2) return false
return true
// Retry (max. 3 times) only if network error detected
if (error.message === "Network request failed" && failureCount <= 3) return true
return false
}
export function useQuery<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
options?: QueryOptions<QueryResult<PromiseReturnType<T>>>,
options?: QueryConfig<PromiseReturnType<T>>,
): [PromiseReturnType<T>, RestQueryResult<T>] {
if (typeof queryFn === "undefined") {
throw new Error("useQuery is missing the first argument - it must be a query function")
@@ -60,16 +43,13 @@ export function useQuery<T extends QueryFn>(
)
}
const queryRpcFn = useIsDevPrerender()
? emptyQueryFn
: ((queryFn as unknown) as EnhancedRpcFunction)
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
const queryKey = getQueryKey(queryFn, params)
const {data, ...queryRest} = useReactQuery({
queryKey: [
queryRpcFn._meta.apiUrl,
serialize(typeof params === "function" ? (params as Function)() : params),
],
queryFn: (_: string, params) => queryRpcFn(params, {fromQueryHook: true}),
queryKey,
queryFn: (_apiUrl: string, params: any) => queryRpcFn(params, {fromQueryHook: true}),
config: {
suspense: true,
retry: retryFunction,
@@ -79,7 +59,7 @@ export function useQuery<T extends QueryFn>(
const rest = {
...queryRest,
...getQueryCacheFunctions<PromiseReturnType<T>>(queryRpcFn._meta.apiUrl),
...getQueryCacheFunctions<PromiseReturnType<T>>(queryKey),
}
return [data as PromiseReturnType<T>, rest as RestQueryResult<T>]

View File

@@ -0,0 +1,7 @@
import {useEffect, useLayoutEffect} from "react"
const isServer = typeof window === "undefined"
// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser.
export const useIsomorphicLayoutEffect = isServer ? useEffect : useLayoutEffect

View File

@@ -1,10 +1,10 @@
import {QueryKeyPart} from "react-query"
import {QueryKey} from "react-query"
import {BlitzApiRequest} from "../"
import {IncomingMessage} from "http"
export const isServer = typeof window === "undefined"
export function getQueryKey(cacheKey: string, params: any): readonly [string, ...QueryKeyPart[]] {
export function getQueryKey(cacheKey: string, params: any): readonly [string, ...QueryKey[]] {
return [cacheKey, typeof params === "function" ? (params as Function)() : params]
}

View File

@@ -1,4 +1,7 @@
import {queryCache} from "react-query"
import {queryCache, QueryKey} from "react-query"
import {serialize} from "superjson"
import {InferUnaryParam, QueryFn} from "../types"
import {EnhancedRpcFunction} from "rpc"
type MutateOptions = {
refetch?: boolean
@@ -8,7 +11,7 @@ export interface QueryCacheFunctions<T> {
mutate: (newData: T | ((oldData: T | undefined) => T), opts?: MutateOptions) => void
}
export const getQueryCacheFunctions = <T>(queryKey: string): QueryCacheFunctions<T> => ({
export const getQueryCacheFunctions = <T>(queryKey: QueryKey): QueryCacheFunctions<T> => ({
mutate: (newData, opts = {refetch: true}) => {
queryCache.setQueryData(queryKey, newData)
if (opts.refetch) {
@@ -17,3 +20,46 @@ export const getQueryCacheFunctions = <T>(queryKey: string): QueryCacheFunctions
return null
},
})
export function getQueryKey<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
) {
if (typeof queryFn === "undefined") {
throw new Error("getQueryKey is missing the first argument - it must be a query function")
}
if (typeof params === "undefined") {
throw new Error(
"getQueryKey is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
)
}
const queryKey: [string, Record<string, any>] = [
((queryFn as unknown) as EnhancedRpcFunction)._meta.apiUrl,
serialize(typeof params === "function" ? (params as Function)() : params),
]
return queryKey
}
export function getInfiniteQueryKey<T extends QueryFn>(
queryFn: T,
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
) {
if (typeof queryFn === "undefined") {
throw new Error("getQueryKey is missing the first argument - it must be a query function")
}
if (typeof params === "undefined") {
throw new Error(
"getQueryKey is missing the second argument. This will be the input to your query function on the server. Pass `null` if the query function doesn't take any arguments",
)
}
const queryKey: ["infinite", string, Record<string, any>] = [
// we need an extra cache key for infinite loading so that the cache for
// for this query is stored separately since the hook result is an array of results. Without this cache for usePaginatedQuery and this will conflict and break.
"infinite",
((queryFn as unknown) as EnhancedRpcFunction)._meta.apiUrl,
serialize(typeof params === "function" ? (params as Function)() : params),
]
return queryKey
}

View File

@@ -19,6 +19,26 @@ type DefaultParams = Parameters<typeof defaultRender>
type RenderUI = DefaultParams[0]
type RenderOptions = DefaultParams[1] & {router?: Partial<NextRouter>}
const mockRouter: NextRouter = {
basePath: "",
pathname: "/",
route: "/",
asPath: "/",
query: {},
push: jest.fn(),
replace: jest.fn(),
reload: jest.fn(),
back: jest.fn(),
prefetch: jest.fn(),
beforePopState: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
},
isFallback: false,
}
export function render(ui: RenderUI, {wrapper, router, ...options}: RenderOptions = {}) {
if (!wrapper) {
wrapper = ({children}) => (
@@ -54,23 +74,3 @@ export function renderHook(
return defaultRenderHook(hook, {wrapper, ...options})
}
const mockRouter: NextRouter = {
basePath: "",
pathname: "/",
route: "/",
asPath: "/",
query: {},
push: jest.fn(),
replace: jest.fn(),
reload: jest.fn(),
back: jest.fn(),
prefetch: jest.fn(),
beforePopState: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
},
isFallback: false,
}

View File

@@ -1,6 +1,6 @@
{
"name": "@blitzjs/display",
"version": "0.17.1-canary.5",
"version": "0.23.1-canary.0",
"description": "Display package for the Blitz CLI",
"homepage": "https://github.com/blitz-js/blitz#readme",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "@blitzjs/file-pipeline",
"version": "0.17.1-canary.5",
"version": "0.23.1-canary.0",
"description": "Display package for the Blitz CLI",
"homepage": "https://github.com/blitz-js/blitz#readme",
"license": "MIT",
@@ -31,7 +31,7 @@
},
"dependencies": {
"chalk": "4.0.0",
"chokidar": "3.4.0",
"chokidar": "3.4.2",
"flush-write-stream": "2.0.0",
"from2": "2.3.0",
"fs-extra": "9.0.0",

View File

@@ -1,5 +1,5 @@
import crypto from "crypto"
import {transform} from "../transform"
import {hash} from "../utils"
/**
* Returns a stage that prepares files coming into the stream
* with correct event information as well as hash information
@@ -19,12 +19,7 @@ export function createEnrichFiles() {
}
if (!file.hash) {
const hash = crypto
.createHash("md5")
.update(JSON.stringify({path: file.path, s: file.stat?.mtime}))
.digest("hex")
file.hash = hash
file.hash = hash(file.path + file.stat?.mtime.toString())
}
return file

View File

@@ -1,92 +0,0 @@
# Future thinking - work-optimizer
So one future issue we have been trying to account for here is how to solve the dirty sync problem with streams. Basically, we want Blitz to do as little work as possible. At this point, we are blowing away Blitz folders when we start but it would be smarter to analyze the source and destination folders and only manipulate the files that are actually required to be changed. This is not required as of now but will be a consideration as we try and get this thing faster and faster to live up to its name. To prepare for this we have setup a work optimizer that checks the hash of the input file and guards against new work being done
The following is a rough plan for how to do this. (Likely to change/improve at a later point)
- Encode vinyl files + stats
```ts
const hash = crypto
.createHash("md5")
.update(file.path + file.stats.mtime)
.digest("hex")
file.hash = hash
```
- Use those hashes to index file details in the following structures:
Following
```ts
// reduced to as the first step during input
const input = {abc123def456: "/foo/bar/baz", def456abc123: "/foo/bar/bop"}
// reduced to as the last step just before file write
const complete = {
abc123def456: {
input: "/foo/bar/baz",
output: ["/bas/boop/blop", "/bas/boop/ding", "/bas/boop/bar"],
},
def456abc123: {
input: "/foo/bar/bing",
output: ["/bas/boop/ping", "/bas/boop/foo", "/bas/boop/fawn"],
},
cbd123aef456: {
input: "/foo/bar/bop",
output: ["/bas/boop/thing"],
},
}
```
Has this file hash been processed?
```ts
const hash => !!output[hash];
```
Which files do I need to delete based on input?
```ts
const deleteHashes = Object.keys(output).filter((hash) => input[hash])
```
- Output can also be indexed by filetype to keep going with our hacky error mapping (eventually this should probably be a sourcemap)
```json
{
"/bas/boop/bar": "/foo/bar/baz",
"/bas/boop/blop": "/foo/bar/baz",
"/bas/boop/ding": "/foo/bar/baz",
"/bas/boop/fawn": "/foo/bar/bing",
"/bas/boop/foo": "/foo/bar/bing",
"/bas/boop/ping": "/foo/bar/bing",
"/bas/boop/thing": "/foo/bar/bop"
}
```
Does my output match my input ie. am I in a stable state? or in our case can we return the promise.
```ts
function isStable(input, output) {
if (!input || !output) {
return // We are not stable if we don't have both an input or output
}
const inputKeys = Object.keys(input)
const outputKeys = Object.keys(output)
if (inputKeys.length !== outputKeys.length) {
return false
}
match = true
for (let i = 0; i < inputKeys.length; i++) {
match = match && outputKey[i] === inputKeys[i]
if (!match) {
return false
}
}
return true
}
```

View File

@@ -1,37 +1,79 @@
// Mostly concerned with solving the Dirty Sync problem
import {log} from "@blitzjs/display"
import {transform} from "../../transform"
import {hash} from "../../utils"
import debounce from "lodash/debounce"
import {writeFile, existsSync, readFileSync} from "fs-extra"
import {resolve, relative} from "path"
import File from "vinyl"
const defaultSaveCache = debounce((filePath: string, data: object) => {
return writeFile(filePath, Buffer.from(JSON.stringify(data, null, 2)))
.then(() => {})
.catch(() => {})
}, 500)
const defaultReadCache = (filePath: string) => {
// We need to do sync file reading here as this cache
// must be loaded before the stream is added to the pipeline
// or we end up with more complexity having to cache files as they come in
return existsSync(filePath) ? readFileSync(filePath).toString() : ""
}
/**
* Returns streams that help handling work optimisation in the file transform stream.
*/
// TODO: This needs quite a bit of work before we can manage a dirty start
// Currently this does not do much aside from guard against repeated work
export function createWorkOptimizer() {
const todo: Array<string> = []
const done: Array<string> = []
export function createWorkOptimizer(
src: string,
dest: string,
saveCache: (filePath: string, data: object) => Promise<void> = defaultSaveCache,
readCache: (filePath: string) => string = defaultReadCache,
) {
const getOriginalPathHash = (file: File) => {
return hash(relative(src, file.history[0]))
}
const doneCacheLocation = resolve(dest, ".blitz.incache.json")
const doneStr = readCache(doneCacheLocation)
const todo: Record<string, string> = {}
const done: Record<string, string> = doneStr ? JSON.parse(doneStr) : {}
const stats = {todo, done}
const reportComplete = transform.file((file) => {
if (file.hash) {
done.push(file.hash)
const reportComplete = transform.file(async (file) => {
const pathHash = getOriginalPathHash(file)
delete todo[pathHash]
if (file.event === "add") {
done[pathHash] = file.hash
}
if (file.event === "unlink") {
delete done[pathHash]
}
await saveCache(resolve(dest, ".blitz.incache.json"), done)
return file
})
const triage = transform.file((file, {push, next}) => {
const pathHash = getOriginalPathHash(file)
if (!file.hash) {
log.debug("File does not have hash! " + file.path)
return next()
}
// Dont send files that have already been done or have already been added
if (done.includes(file.hash) || todo.includes(file.hash)) {
if (done[pathHash] === file.hash || todo[pathHash] === file.hash) {
log.debug("Rejecting because this job has been done before: " + file.path)
return next()
}
todo.push(file.hash)
todo[pathHash] = file.hash
push(file)

View File

@@ -1,24 +1,31 @@
import {createWorkOptimizer} from "."
import {testStreamItems} from "../../test-utils"
import {take} from "../../test-utils"
import {hash} from "../../utils"
import {pipeline} from "../../streams"
import {normalize} from "path"
import {normalize, resolve} from "path"
import File from "vinyl"
function logItem(fileOrString: {path: string} | string) {
if (typeof fileOrString === "string") {
return fileOrString
}
return fileOrString.path
}
const pathToOneHash = hash(normalize("to/one"))
const pathToTwoHash = hash(normalize("to/two"))
const pathToThreeHash = hash(normalize("to/three"))
describe("agnosticSource", () => {
test("basic throughput", async () => {
const {triage, reportComplete} = createWorkOptimizer()
const saveCache = jest.fn()
const readCache = jest.fn()
const {triage, reportComplete} = createWorkOptimizer(
normalize("/path"),
normalize("/dest"),
saveCache,
readCache,
)
triage.write(
new File({
hash: "one",
path: normalize("/path/to/one"),
content: Buffer.from("one"),
event: "add",
}),
)
@@ -27,6 +34,7 @@ describe("agnosticSource", () => {
hash: "two",
path: normalize("/path/to/two"),
content: Buffer.from("two"),
event: "add",
}),
)
@@ -35,21 +43,31 @@ describe("agnosticSource", () => {
hash: "three",
path: normalize("/path/to/three"),
content: Buffer.from("three"),
event: "add",
}),
)
const expected = ["/path/to/one", "/path/to/two", "/path/to/three"].map(normalize)
const stream = pipeline(triage, reportComplete)
await testStreamItems(stream, expected, logItem)
const items = await take<File>(stream, 3)
expect(items.map(({path}) => path)).toEqual(expected)
})
test("same file is rejected", async () => {
const {triage, reportComplete} = createWorkOptimizer()
const saveCache = jest.fn()
const readCache = jest.fn()
const {triage, reportComplete} = createWorkOptimizer(
normalize("/path"),
normalize("/dest"),
saveCache,
readCache,
)
triage.write(
new File({
hash: "one",
path: normalize("/path/to/one"),
content: Buffer.from("one"),
event: "add",
}),
)
@@ -58,6 +76,7 @@ describe("agnosticSource", () => {
hash: "one",
path: normalize("/path/to/one"),
content: Buffer.from("one"),
event: "add",
}),
)
@@ -66,11 +85,156 @@ describe("agnosticSource", () => {
hash: "two",
path: normalize("/path/to/two"),
content: Buffer.from("two"),
event: "add",
}),
)
const expected = ["/path/to/one", "/path/to/two"].map(normalize)
const stream = pipeline(triage, reportComplete)
await testStreamItems(stream, expected, logItem)
const items = await take<File>(stream, 2)
expect(items.map(({path}) => path)).toEqual(expected)
})
test("read cache from disk and skips cached files with the same hash and path", async () => {
const saveCache = jest.fn()
const readCache = jest.fn(() => {
return `{"${pathToOneHash}": "one","${pathToTwoHash}": "two"}`
})
const {triage, reportComplete} = createWorkOptimizer(
normalize("/path"),
normalize("/dest"),
saveCache,
readCache,
)
triage.write(
new File({
hash: "one",
path: normalize("/path/to/one"),
content: Buffer.from("one"),
event: "add",
}),
)
triage.write(
new File({
hash: "two",
path: normalize("/path/to/two"),
content: Buffer.from("two"),
event: "add",
}),
)
triage.write(
new File({
hash: "three",
path: normalize("/path/to/three"),
content: Buffer.from("three"),
event: "add",
}),
)
const stream = pipeline(triage, reportComplete)
const [item] = await take<File>(stream, 1)
expect(item.path).toEqual(normalize("/path/to/three"))
})
test("save cache should be saved correctly", async () => {
const saveCache = jest.fn()
const readCache = jest.fn()
const {triage, reportComplete} = createWorkOptimizer(
normalize("/path"),
normalize("/dest"),
saveCache,
readCache,
)
triage.write(
new File({
hash: "one",
path: normalize("/path/to/one"),
content: Buffer.from("one"),
event: "add",
}),
)
triage.write(
new File({
hash: "two",
path: normalize("/path/to/two"),
content: Buffer.from("two"),
event: "add",
}),
)
triage.write(
new File({
hash: "three",
path: normalize("/path/to/three"),
content: Buffer.from("three"),
event: "add",
}),
)
const stream = pipeline(triage, reportComplete)
await take(stream, 3)
const doneObj = {
[`${pathToOneHash}`]: "one",
[`${pathToTwoHash}`]: "two",
[`${pathToThreeHash}`]: "three",
}
expect(saveCache).toHaveBeenCalledWith(resolve(normalize("/dest/.blitz.incache.json")), doneObj)
})
test("should keep track of deleted files", async () => {
const saveCache = jest.fn()
const readCache = jest.fn()
const {triage, reportComplete} = createWorkOptimizer(
normalize("/path"),
normalize("/dest"),
saveCache,
readCache,
)
triage.write(
new File({
hash: "one",
path: normalize("/path/to/one"),
content: Buffer.from("one"),
event: "add",
}),
)
triage.write(
new File({
hash: "two",
path: normalize("/path/to/two"),
content: Buffer.from("two"),
event: "add",
}),
)
triage.write(
new File({
hash: "three",
path: normalize("/path/to/three"),
content: Buffer.from("three"),
event: "add",
}),
)
triage.write(
new File({
hash: "something else",
path: normalize("/path/to/two"),
event: "unlink",
}),
)
const stream = pipeline(triage, reportComplete)
await take(stream, 4)
const expectedDone = {
[pathToOneHash]: "one",
[pathToThreeHash]: "three",
}
expect(saveCache).toHaveBeenCalledWith(
resolve(normalize("/dest/.blitz.incache.json")),
expectedDone,
)
})
})

View File

@@ -7,6 +7,11 @@ import {FILE_WRITTEN, FILE_DELETED} from "../../events"
import {Writable} from "stream"
import {isFile} from "../../utils"
import {transform} from "../../transform"
const isUnlinkFile = (file: File) => {
return file.event === "unlink" || file.event === "unlinkDir"
}
/**
* Returns a Stage that writes files to the destination path
*/
@@ -29,5 +34,3 @@ export const createWrite = (
return {stream}
}
const isUnlinkFile = (file: File) => file.event === "unlink" || file.event === "unlinkDir"

View File

@@ -1,16 +1,61 @@
import {Writable} from "stream"
import File from "vinyl"
import {pipeline, through} from "./streams"
import {Stage, StageArgs, StageConfig} from "./types"
import {Stage, StageArgs, StageConfig, EventedFile} from "./types"
import {agnosticSource} from "./helpers/agnostic-source"
import {createEnrichFiles} from "./helpers/enrich-files"
import {createFileCache} from "./helpers/file-cache"
import {createFileCache, FileCache} from "./helpers/file-cache"
import {createIdleHandler} from "./helpers/idle-handler"
import {createWorkOptimizer} from "./helpers/work-optimizer"
import {createWrite} from "./helpers/writer"
import {Stats} from "fs"
export function isSourceFile(file: File) {
return file.hash.indexOf(":") === -1
return file.hash?.indexOf(":") === -1
}
function createStageArgs(
config: StageConfig,
input: Writable,
bus: Writable,
cache: FileCache,
): StageArgs {
const getInputCache = () => cache
function processNewFile(file: File) {
if (!file.stat) {
// Add a stats here so we can then generate a new ID
// during enrichment
const stat = new Stats()
file.stat = stat
file.event = "add"
}
input.write(file)
}
function processNewChildFile({
parent,
child,
stageId,
subfileId,
}: {
parent: EventedFile
child: File
stageId: string
subfileId: string
}) {
child.hash = [parent.hash, stageId, subfileId].join("|")
processNewFile(child)
}
return {
config,
input,
bus,
getInputCache,
processNewFile,
processNewChildFile,
}
}
/**
@@ -30,18 +75,13 @@ export function createPipeline(
) {
// Helper streams don't account for business stages
const input = through.obj()
const optimizer = createWorkOptimizer()
const optimizer = createWorkOptimizer(config.src, config.dest)
const enrichFiles = createEnrichFiles()
const srcCache = createFileCache(isSourceFile)
const idleHandler = createIdleHandler(bus)
// Send this object to every stage
const api: StageArgs = {
config,
input,
bus,
getInputCache: () => srcCache.cache,
}
const api = createStageArgs(config, input, bus, srcCache.cache)
// Initialize each stage
const initializedStages = stages.map((stage) => stage(api))

View File

@@ -1,6 +1,8 @@
import {through, pipeline} from "./streams"
const defaultLogger = (file: any) => (typeof file === "string" ? file : file.path)
// Test expected log items
export function testStreamItems(
stream: NodeJS.ReadWriteStream,
expected: any[],
@@ -25,3 +27,22 @@ export function testStreamItems(
)
})
}
export function take<T>(stream: NodeJS.ReadWriteStream, num: number): Promise<T[]> {
return new Promise((done) => {
let items: T[] = []
const st = pipeline(
stream,
through.obj((item, _, next) => {
items.push(item)
if (items.length === num) {
st.end()
setImmediate(() => {
done(items)
})
}
next(null, item)
}),
)
})
}

Some files were not shown because too many files have changed in this diff Show More