Compare commits
4 Commits
bug
...
authorize-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77b0730aaf | ||
|
|
5184897a99 | ||
|
|
43644d7b64 | ||
|
|
5e1c2bf1fb |
@@ -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,6 +565,15 @@
|
||||
"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",
|
||||
@@ -887,8 +896,7 @@
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/8602086?v=4",
|
||||
"profile": "http://ricardotrejos.tech",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -982,192 +990,6 @@
|
||||
"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,
|
||||
|
||||
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -32,9 +32,9 @@ jobs:
|
||||
**/node_modules
|
||||
/home/runner/.cache/Cypress
|
||||
C:\Users\runneradmin\AppData\Local\Cypress\Cache
|
||||
key: ${{ runner.os }}-yarn-v3-${{ hashFiles('yarn.lock') }}
|
||||
key: ${{ runner.os }}-yarn-v2-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-v3-
|
||||
${{ runner.os }}-yarn-v2-
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --silent
|
||||
env:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -19,4 +19,3 @@ dist
|
||||
**/.env.*.local
|
||||
**/.envrc
|
||||
.blitz-*
|
||||
.blitz-cli-cache
|
||||
|
||||
105
MAINTAINERS.md
105
MAINTAINERS.md
@@ -1 +1,104 @@
|
||||
This document has moved here: https://blitzjs.com/docs/maintainers
|
||||
# ❤️ 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 can’t do a full review right away, leave a comment thanking them and saying we’ll 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
|
||||
|
||||
67
README.md
67
README.md
@@ -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-122-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-103-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">
|
||||
@@ -116,35 +116,23 @@ 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="200px">
|
||||
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/Fauna_Logo_Blue.png" width="175px">
|
||||
</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>
|
||||
|
||||
@@ -200,7 +188,6 @@ _Issue triage, pull request triage, community encouragement and moderation, etc_
|
||||
</tr>
|
||||
<tr>
|
||||
<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 -->
|
||||
@@ -263,7 +250,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>
|
||||
@@ -291,94 +278,70 @@ 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://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>
|
||||
<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></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://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>
|
||||
<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>
|
||||
|
||||
<!-- markdownlint-enable -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
||||
|
||||
@@ -2,7 +2,6 @@ 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)
|
||||
@@ -17,13 +16,8 @@ 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(
|
||||
{
|
||||
@@ -91,41 +85,5 @@ 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})
|
||||
},
|
||||
),
|
||||
],
|
||||
})
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
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} from "app/auth/validations"
|
||||
import {LoginInput, LoginInputType} from "app/auth/validations"
|
||||
|
||||
type LoginFormProps = {
|
||||
onSuccess?: () => void
|
||||
@@ -13,7 +12,8 @@ export const LoginForm = (props: LoginFormProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Login</h1>
|
||||
<Form
|
||||
|
||||
<Form<LoginInputType>
|
||||
submitText="Log In"
|
||||
schema={LoginInput}
|
||||
initialValues={{email: undefined, password: undefined}}
|
||||
@@ -36,9 +36,6 @@ 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} from "app/auth/validations"
|
||||
import {SignupInput, SignupInputType} from "app/auth/validations"
|
||||
|
||||
const SignupPage: BlitzPage = () => {
|
||||
const router = useRouter()
|
||||
@@ -18,7 +18,7 @@ const SignupPage: BlitzPage = () => {
|
||||
<div>
|
||||
<h1>Create an Account</h1>
|
||||
|
||||
<Form
|
||||
<Form<SignupInputType>
|
||||
submitText="Create Account"
|
||||
schema={SignupInput}
|
||||
onSubmit={async (values) => {
|
||||
|
||||
@@ -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<S extends z.ZodType<any, any>> = {
|
||||
type FormProps<FormValues> = {
|
||||
/** All your form fields */
|
||||
children: ReactNode
|
||||
/** Text to display in the submit button */
|
||||
submitText: string
|
||||
onSubmit: FinalFormProps<z.infer<S>>["onSubmit"]
|
||||
initialValues?: FinalFormProps<z.infer<S>>["initialValues"]
|
||||
schema?: S
|
||||
onSubmit: FinalFormProps<FormValues>["onSubmit"]
|
||||
initialValues?: FinalFormProps<FormValues>["initialValues"]
|
||||
schema?: z.ZodType<any, any>
|
||||
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
|
||||
|
||||
export function Form<S extends z.ZodType<any, any>>({
|
||||
export function Form<FormValues extends Record<string, unknown>>({
|
||||
children,
|
||||
submitText,
|
||||
schema,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
...props
|
||||
}: FormProps<S>) {
|
||||
}: FormProps<FormValues>) {
|
||||
return (
|
||||
<FinalForm
|
||||
<FinalForm<FormValues>
|
||||
initialValues={initialValues}
|
||||
validate={(values) => {
|
||||
if (!schema) return
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import {AppProps, ErrorComponent, useRouter} from "blitz"
|
||||
import {AppProps, ErrorComponent} 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
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
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
|
||||
@@ -1,16 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
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
|
||||
@@ -1,68 +0,0 @@
|
||||
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
|
||||
@@ -1,21 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ export default async function getCurrentUser(_ = null, ctx: {session?: SessionCo
|
||||
if (!ctx.session?.userId) return null
|
||||
|
||||
const user = await db.user.findOne({
|
||||
where: {id: ctx.session!.userId},
|
||||
where: {id: ctx.session.userId},
|
||||
select: {id: true, name: true, email: true, role: true},
|
||||
})
|
||||
|
||||
|
||||
@@ -10,11 +10,12 @@ type GetUserInput = {
|
||||
|
||||
export default async function getUser(
|
||||
{where, select}: GetUserInput,
|
||||
ctx: {session?: SessionContext} = {},
|
||||
ctx: {session: SessionContext},
|
||||
) {
|
||||
ctx.session?.authorize(["admin", "user"])
|
||||
ctx.session.authorize(["admin", "user"])
|
||||
|
||||
const user = await db.user.findOne({where, select})
|
||||
const user = await db.user.findOne({where: {id: ctx.session.userId}})
|
||||
// const user = await db.user.findOne({where, select})
|
||||
|
||||
if (!user) throw new NotFoundError(`User with id ${where.id} does not exist`)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@examples/auth",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"scripts": {
|
||||
"start": "blitz start",
|
||||
"studio": "blitz db studio",
|
||||
@@ -35,9 +35,8 @@
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.4.1",
|
||||
"@prisma/client": "2.4.1",
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"blitz": "0.21.2-canary.1",
|
||||
"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-7f28234f8",
|
||||
@@ -50,7 +49,6 @@
|
||||
"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",
|
||||
|
||||
8
examples/auth/types.ts
Normal file
8
examples/auth/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {DefaultAuthTypes} from "blitz"
|
||||
import {User} from "db"
|
||||
|
||||
declare module "blitz" {
|
||||
export interface AuthTypes extends DefaultAuthTypes {
|
||||
userId: User["id"]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "no-prisma",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"scripts": {
|
||||
"start": "blitz start",
|
||||
"build": "blitz build",
|
||||
@@ -26,7 +26,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"blitz": "0.21.2-canary.1",
|
||||
"knex": "0.21.2",
|
||||
"react": "0.0.0-experimental-7f28234f8",
|
||||
"react-dom": "0.0.0-experimental-7f28234f8",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@examples/plain-js",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"scripts": {
|
||||
"start": "blitz start",
|
||||
"build": "blitz db migrate && blitz build",
|
||||
@@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.4.1",
|
||||
"@prisma/client": "2.4.1",
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"blitz": "0.21.2-canary.1",
|
||||
"react": "0.0.0-experimental-7f28234f8",
|
||||
"react-dom": "0.0.0-experimental-7f28234f8"
|
||||
},
|
||||
|
||||
@@ -2,18 +2,17 @@ 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] = useQuery(getProduct, {where: {id}})
|
||||
const [product, {mutate}] = useQuery(getProduct, {where: {id}})
|
||||
|
||||
return (
|
||||
<ProductForm
|
||||
product={product}
|
||||
onSuccess={() => {
|
||||
queryCache.invalidateQueries("/api/products/queries/getProducts")
|
||||
onSuccess={(updatedProduct) => {
|
||||
mutate(updatedProduct)
|
||||
router.push("/admin/products")
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -43,8 +43,7 @@ describe("admin/products/[handle] page", () => {
|
||||
cy.get("button").click()
|
||||
|
||||
cy.location("pathname").should("equal", "/admin/products")
|
||||
// Todo - make test work for this
|
||||
// cy.get("ul > li:last-child").contains(data[0] + random)
|
||||
cy.get("ul > li:last-child").contains(data[0] + random)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@examples/store",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "blitz db migrate && blitz build",
|
||||
@@ -18,12 +18,13 @@
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.4.1",
|
||||
"@prisma/client": "2.4.1",
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"blitz": "0.21.2-canary.1",
|
||||
"final-form": "4.19.1",
|
||||
"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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"scripts": {
|
||||
"build": "blitz db migrate && blitz build",
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||
@@ -30,7 +30,7 @@
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.4.1",
|
||||
"@prisma/client": "2.4.1",
|
||||
"blitz": "0.23.1-canary.0",
|
||||
"blitz": "0.21.2-canary.1",
|
||||
"react": "0.0.0-experimental-7f28234f8",
|
||||
"react-dom": "0.0.0-experimental-7f28234f8",
|
||||
"typescript": "3.8.3"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"packages": ["packages/*"],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
"@wessberg/rollup-plugin-ts": "1.3.3",
|
||||
"babel-eslint": "10.x",
|
||||
"babel-jest": "26.3.0",
|
||||
"concurrently": "5.3.0",
|
||||
"cpy-cli": "3.1.1",
|
||||
"cross-env": "7.0.2",
|
||||
"debug": "4.1.1",
|
||||
@@ -95,8 +94,6 @@
|
||||
"directory-tree": "2.2.4",
|
||||
"eslint": "7.7.0",
|
||||
"eslint-config-react-app": "5.2.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",
|
||||
|
||||
@@ -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.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
@@ -39,11 +39,11 @@
|
||||
"url": "https://github.com/blitz-js/blitz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@blitzjs/cli": "0.21.2-canary.1",
|
||||
"@blitzjs/core": "0.21.2-canary.1",
|
||||
"@blitzjs/generator": "0.21.2-canary.1",
|
||||
"@blitzjs/installer": "0.21.2-canary.1",
|
||||
"@blitzjs/server": "0.21.2-canary.1",
|
||||
"envinfo": "7.7.2",
|
||||
"os-name": "3.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
|
||||
@@ -5,16 +5,12 @@ import chalk from "chalk"
|
||||
import {parseSemver} from "../utils/parse-semver"
|
||||
|
||||
async function main() {
|
||||
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`,
|
||||
),
|
||||
)
|
||||
}
|
||||
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(
|
||||
@@ -41,6 +37,7 @@ 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)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@blitzjs/cli",
|
||||
"description": "Blitz.js CLI",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"b": "./bin/run",
|
||||
@@ -30,15 +30,14 @@
|
||||
"/lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@blitzjs/display": "0.23.1-canary.0",
|
||||
"@blitzjs/repl": "0.23.1-canary.0",
|
||||
"@blitzjs/display": "0.21.2-canary.1",
|
||||
"@blitzjs/repl": "0.21.2-canary.1",
|
||||
"@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.3",
|
||||
@@ -55,13 +54,12 @@
|
||||
"rimraf": "3.0.2",
|
||||
"tar": "6.0.2",
|
||||
"ts-node": "8.9.0",
|
||||
"tsconfig-paths": "3.9.0",
|
||||
"v8-compile-cache": "2.1.1"
|
||||
"tsconfig-paths": "3.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blitzjs/generator": "0.23.1-canary.0",
|
||||
"@blitzjs/installer": "0.23.1-canary.0",
|
||||
"@blitzjs/server": "0.23.1-canary.0",
|
||||
"@blitzjs/generator": "0.21.2-canary.1",
|
||||
"@blitzjs/installer": "0.21.2-canary.1",
|
||||
"@blitzjs/server": "0.21.2-canary.1",
|
||||
"@oclif/dev-cli": "1.22.2",
|
||||
"@oclif/test": "1.2.5",
|
||||
"@prisma/cli": "2.4.1",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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"
|
||||
@@ -10,7 +12,7 @@ export class Build extends Command {
|
||||
}
|
||||
|
||||
try {
|
||||
await require("@blitzjs/server").build(config)
|
||||
await build(config, runPrismaGeneration({silent: true, failSilently: true}))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
process.exit(1) // clean up?
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
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"
|
||||
|
||||
const projectRoot = require("pkg-dir").sync() || process.cwd()
|
||||
const isTypescript = require("fs").existsSync(require("path").join(projectRoot, "tsconfig.json"))
|
||||
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"))
|
||||
|
||||
export class Console extends Command {
|
||||
static description = "Run the Blitz console REPL"
|
||||
@@ -13,17 +22,19 @@ 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) {
|
||||
require("../utils/setup-ts-node").setupTsnode()
|
||||
setupTsnode()
|
||||
}
|
||||
|
||||
require("@blitzjs/repl").runRepl(Console.replOptions)
|
||||
await runPrismaGeneration({silent: true, failSilently: true})
|
||||
spinner.succeed()
|
||||
|
||||
runRepl(Console.replOptions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import {Command, flags} from "@oclif/command"
|
||||
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 pEvent from "p-event"
|
||||
import {getConfig, getSchema} from "@prisma/sdk"
|
||||
|
||||
const getPrismaBin = () => require("@blitzjs/server").resolveBinAsync("@prisma/cli", "prisma")
|
||||
const getPrismaBin = () => resolveBinAsync("@prisma/cli", "prisma")
|
||||
let prismaBin: string
|
||||
let schemaArg: string
|
||||
|
||||
@@ -16,11 +26,11 @@ const runPrisma = async (args: string[], silent = false) => {
|
||||
}
|
||||
}
|
||||
|
||||
const cp = require("cross-spawn").spawn(prismaBin, args, {
|
||||
const cp = spawn(prismaBin, args, {
|
||||
stdio: silent ? "ignore" : "inherit",
|
||||
env: process.env,
|
||||
})
|
||||
const code = await require("p-event")(cp, "exit", {rejectionEvents: []})
|
||||
const code = await pEvent(cp, "exit", {rejectionEvents: []})
|
||||
|
||||
return code === 0
|
||||
}
|
||||
@@ -43,8 +53,8 @@ export const runPrismaGeneration = async ({silent = false, failSilently = false}
|
||||
}
|
||||
}
|
||||
|
||||
const runMigrateUp = async ({silent = false} = {}, schemaArgLocal = schemaArg) => {
|
||||
const args = ["migrate", "up", schemaArgLocal, "--create-db", "--experimental"]
|
||||
const runMigrateUp = async ({silent = false} = {}) => {
|
||||
const args = ["migrate", "up", schemaArg, "--create-db", "--experimental"]
|
||||
|
||||
if (process.env.NODE_ENV === "production" || silent) {
|
||||
args.push("--auto-approve")
|
||||
@@ -59,17 +69,16 @@ const runMigrateUp = async ({silent = false} = {}, schemaArgLocal = schemaArg) =
|
||||
return runPrismaGeneration({silent})
|
||||
}
|
||||
|
||||
export const runMigrate = async (flags: object = {}, schemaArgLocal = schemaArg) => {
|
||||
export const runMigrate = async (name?: string) => {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return runMigrateUp({}, schemaArgLocal)
|
||||
return runMigrateUp()
|
||||
}
|
||||
// @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 silent = Boolean(name)
|
||||
const args = ["migrate", "save", schemaArg, "--create-db", "--experimental"]
|
||||
if (name) {
|
||||
args.push("--name", name)
|
||||
}
|
||||
|
||||
const success = await runPrisma(args, silent)
|
||||
|
||||
@@ -77,7 +86,7 @@ export const runMigrate = async (flags: object = {}, schemaArgLocal = schemaArg)
|
||||
throw new Error("Migration failed")
|
||||
}
|
||||
|
||||
return runMigrateUp({silent}, schemaArgLocal)
|
||||
return runMigrateUp({silent})
|
||||
}
|
||||
|
||||
export async function resetPostgres(connectionString: string, db: any): Promise<void> {
|
||||
@@ -122,12 +131,8 @@ export async function resetMysql(connectionString: string, db: any): Promise<voi
|
||||
|
||||
export async function resetSqlite(connectionString: string): Promise<void> {
|
||||
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)
|
||||
const dbPath = path.join(projectRoot, "db", relativePath)
|
||||
const unlink = promisify(fs.unlink)
|
||||
try {
|
||||
// delete database from folder
|
||||
await unlink(dbPath)
|
||||
@@ -151,21 +156,17 @@ export function getDbName(connectionString: string): string {
|
||||
export class Db extends Command {
|
||||
static description = `Run database commands
|
||||
|
||||
${require("chalk").bold(
|
||||
"migrate",
|
||||
)} Run any needed migrations via Prisma 2 and generate Prisma Client.
|
||||
${chalk.bold("migrate")} Run any needed migrations via Prisma 2 and generate Prisma Client.
|
||||
|
||||
${require("chalk").bold(
|
||||
${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.
|
||||
|
||||
${require("chalk").bold(
|
||||
${chalk.bold(
|
||||
"studio",
|
||||
)} Open the Prisma Studio UI at http://localhost:5555 so you can easily see and change data in your database.
|
||||
|
||||
${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.
|
||||
${chalk.bold("reset")} Reset the database and run a fresh migration via Prisma 2.
|
||||
`
|
||||
|
||||
static args = [
|
||||
@@ -181,25 +182,21 @@ ${require("chalk").bold(
|
||||
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, flags} = this.parse(Db)
|
||||
const command = args["command"]
|
||||
|
||||
// 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")
|
||||
const schemaPath = path.join(process.cwd(), "db", "schema.prisma")
|
||||
schemaArg = `--schema=${schemaPath}`
|
||||
|
||||
if (command === "migrate" || command === "m") {
|
||||
try {
|
||||
return await runMigrate(flags)
|
||||
return await runMigrate(flags.name)
|
||||
} catch (error) {
|
||||
if (Object.keys(flags).length > 0) {
|
||||
if (flags.name) {
|
||||
throw error
|
||||
} else {
|
||||
process.exit(1)
|
||||
@@ -217,45 +214,41 @@ ${require("chalk").bold(
|
||||
}
|
||||
|
||||
if (command === "reset") {
|
||||
const forceSkipConfirmation = flags.force
|
||||
const spinner = log.spinner("Loading your database").start()
|
||||
await runPrismaGeneration({silent: true, failSilently: true})
|
||||
spinner.succeed()
|
||||
|
||||
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?",
|
||||
})
|
||||
const {confirm} = await prompt<{confirm: string}>({
|
||||
type: "confirm",
|
||||
name: "confirm",
|
||||
message: "Are you sure you want to reset your database and erase ALL data?",
|
||||
})
|
||||
|
||||
if (!confirm) {
|
||||
return
|
||||
}
|
||||
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 schemaPath = path.join(projectRoot, "db/schema.prisma")
|
||||
const datamodel = await getSchema(schemaPath)
|
||||
const config = await getConfig({datamodel})
|
||||
const dataSource = config.datasources[0]
|
||||
const providerType = dataSource.activeProvider
|
||||
const connectionString = dataSource.url.value
|
||||
|
||||
if (providerType === "postgresql") {
|
||||
await resetPostgres(connectionString, db)
|
||||
return
|
||||
resetPostgres(connectionString, db)
|
||||
} else if (providerType === "mysql") {
|
||||
await resetMysql(connectionString, db)
|
||||
return
|
||||
resetMysql(connectionString, db)
|
||||
} else if (providerType === "sqlite") {
|
||||
await resetSqlite(connectionString)
|
||||
return
|
||||
resetSqlite(connectionString)
|
||||
} else {
|
||||
log.error("Could not find a valid database configuration")
|
||||
return
|
||||
this.log("Could not find a valid database configuration")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (command === "help") {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import {Command} from "../command"
|
||||
import {flags} from "@oclif/command"
|
||||
import {log} from "@blitzjs/display"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import enquirer from "enquirer"
|
||||
import _pluralize from "pluralize"
|
||||
import {
|
||||
PageGenerator,
|
||||
MutationGenerator,
|
||||
@@ -10,13 +13,15 @@ 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) => require("camelcase")(str, {pascalCase: true})
|
||||
const getIsTypescript = () =>
|
||||
require("fs").existsSync(
|
||||
require("path").join(require("../utils/get-project-root").projectRoot, "tsconfig.json"),
|
||||
)
|
||||
|
||||
const pascalCase = (str: string) => camelCase(str, {pascalCase: true})
|
||||
|
||||
const projectRoot = pkgDir.sync() || process.cwd()
|
||||
const isTypescript = fs.existsSync(path.join(projectRoot, "tsconfig.json"))
|
||||
|
||||
enum ResourceType {
|
||||
All = "all",
|
||||
@@ -41,18 +46,18 @@ interface Args {
|
||||
}
|
||||
|
||||
function pluralize(input: string): string {
|
||||
return require("pluralize").isPlural(input) ? input : require("pluralize").plural(input)
|
||||
return _pluralize.isPlural(input) ? input : _pluralize.plural(input)
|
||||
}
|
||||
|
||||
function singular(input: string): string {
|
||||
return require("pluralize").isSingular(input) ? input : require("pluralize").singular(input)
|
||||
return _pluralize.isSingular(input) ? input : _pluralize.singular(input)
|
||||
}
|
||||
|
||||
function modelName(input: string = "") {
|
||||
return require("camelcase")(singular(input))
|
||||
return camelCase(singular(input))
|
||||
}
|
||||
function modelNames(input: string = "") {
|
||||
return require("camelcase")(pluralize(input))
|
||||
return camelCase(pluralize(input))
|
||||
}
|
||||
function ModelName(input: string = "") {
|
||||
return pascalCase(singular(input))
|
||||
@@ -152,33 +157,31 @@ export class Generate extends Command {
|
||||
]
|
||||
|
||||
async promptForTargetDirectory(paths: string[]): Promise<string> {
|
||||
return require("enquirer")
|
||||
.prompt({
|
||||
return enquirer
|
||||
.prompt<{directory: string}>({
|
||||
name: "directory",
|
||||
type: "select",
|
||||
message: "Please select a target directory:",
|
||||
choices: paths,
|
||||
})
|
||||
.then((resp: any) => resp.directory)
|
||||
.then((resp) => resp.directory)
|
||||
}
|
||||
|
||||
async genericConfirmPrompt(message: string): Promise<boolean> {
|
||||
return require("enquirer")
|
||||
.prompt({
|
||||
return enquirer
|
||||
.prompt<{continue: string}>({
|
||||
name: "continue",
|
||||
type: "select",
|
||||
message: message,
|
||||
choices: ["Yes", "No"],
|
||||
})
|
||||
.then((resp: any) => resp.continue === "Yes")
|
||||
.then((resp) => resp.continue === "Yes")
|
||||
}
|
||||
|
||||
async handleNoContext(message: string): Promise<void> {
|
||||
const shouldCreateNewRoot = await this.genericConfirmPrompt(message)
|
||||
if (!shouldCreateNewRoot) {
|
||||
require("@blitzjs/display").log.error(
|
||||
"Could not determine proper location for files. Aborting.",
|
||||
)
|
||||
log.error("Could not determine proper location for files. Aborting.")
|
||||
this.exit(0)
|
||||
}
|
||||
}
|
||||
@@ -189,7 +192,7 @@ export class Generate extends Command {
|
||||
if (modelSegments.length > 1) {
|
||||
return {
|
||||
model: modelSegments[modelSegments.length - 1],
|
||||
context: require("path").join(...modelSegments.slice(0, modelSegments.length - 1)),
|
||||
context: path.join(...modelSegments.slice(0, modelSegments.length - 1)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +201,7 @@ export class Generate extends Command {
|
||||
|
||||
return {
|
||||
model: modelName,
|
||||
context: require("path").join(...contextSegments),
|
||||
context: path.join(...contextSegments),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +222,7 @@ export class Generate extends Command {
|
||||
const generators = generatorMap[args.type]
|
||||
for (const GeneratorClass of generators) {
|
||||
const generator = new GeneratorClass({
|
||||
destinationRoot: require("path").resolve(),
|
||||
destinationRoot: path.resolve(),
|
||||
extraArgs: argv.slice(2).filter((arg) => !arg.startsWith("-")),
|
||||
modelName: singularRootContext,
|
||||
modelNames: modelNames(singularRootContext),
|
||||
@@ -229,10 +232,9 @@ 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: getIsTypescript(),
|
||||
useTs: isTypescript,
|
||||
})
|
||||
await generator.run()
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import {Command} from "../command"
|
||||
import type {RecipeExecutor} from "@blitzjs/installer"
|
||||
import * as path from "path"
|
||||
import {RecipeExecutor} from "@blitzjs/installer"
|
||||
import _got from "got"
|
||||
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 require("got")(url).catch((e: any) => Boolean(console.error(e)) || e)
|
||||
return _got(url).catch((e) => Boolean(console.error(e)) || e)
|
||||
}
|
||||
|
||||
async function gotJSON(url: string) {
|
||||
@@ -20,7 +28,7 @@ async function isUrlValid(url: string) {
|
||||
}
|
||||
|
||||
function requireJSON(file: string) {
|
||||
return JSON.parse(require("fs-extra").readFileSync(file).toString("utf-8"))
|
||||
return JSON.parse(readFileSync(file).toString("utf-8"))
|
||||
}
|
||||
|
||||
const GH_ROOT = "https://github.com/"
|
||||
@@ -107,13 +115,10 @@ export class Install extends Command {
|
||||
defaultBranch: string,
|
||||
subdirectory?: string,
|
||||
): Promise<string> {
|
||||
const recipeDir = require("path").join(
|
||||
require("os").tmpdir(),
|
||||
`blitz-recipe-${repoFullName.replace("/", "-")}`,
|
||||
)
|
||||
const recipeDir = path.join(os.tmpdir(), `blitz-recipe-${repoFullName.replace("/", "-")}`)
|
||||
// clean up from previous run in case of error
|
||||
require("rimraf").sync(recipeDir)
|
||||
require("fs-extra").mkdirSync(recipeDir)
|
||||
rimraf.sync(recipeDir)
|
||||
mkdirSync(recipeDir)
|
||||
process.chdir(recipeDir)
|
||||
|
||||
const repoName = repoFullName.split("/")[1]
|
||||
@@ -122,8 +127,8 @@ export class Install extends Command {
|
||||
const extractPath = subdirectory ? [`${repoName}-${defaultBranch}/${subdirectory}`] : undefined
|
||||
const depth = subdirectory ? subdirectory.split("/").length + 1 : 1
|
||||
await pipeline(
|
||||
require("got").stream(`${CODE_ROOT}${repoFullName}/tar.gz/${defaultBranch}`),
|
||||
require("tar").extract({strip: depth}, extractPath),
|
||||
_got.stream(`${CODE_ROOT}${repoFullName}/tar.gz/${defaultBranch}`),
|
||||
tar.extract({strip: depth}, extractPath),
|
||||
)
|
||||
|
||||
return recipeDir
|
||||
@@ -144,11 +149,9 @@ export class Install extends Command {
|
||||
}
|
||||
|
||||
async run() {
|
||||
require("../utils/setup-ts-node").setupTsnode()
|
||||
setupTsnode()
|
||||
const {args} = this.parse(Install)
|
||||
const pkgManager = require("fs-extra").existsSync(require("path").resolve("yarn.lock"))
|
||||
? "yarn"
|
||||
: "npm"
|
||||
const pkgManager = existsSync(path.resolve("yarn.lock")) ? "yarn" : "npm"
|
||||
const originalCwd = process.cwd()
|
||||
const recipeInfo = this.normalizeRecipePath(args.recipe)
|
||||
|
||||
@@ -175,21 +178,21 @@ export class Install extends Command {
|
||||
|
||||
spinner = log.spinner("Installing package.json dependencies").start()
|
||||
await new Promise((resolve) => {
|
||||
const installProcess = require("cross-spawn")(pkgManager, ["install"])
|
||||
const installProcess = spawn(pkgManager, ["install"])
|
||||
installProcess.on("exit", resolve)
|
||||
})
|
||||
spinner.stop()
|
||||
|
||||
const recipePackageMain = requireJSON("./package.json").main
|
||||
const recipeEntry = require("path").resolve(recipePackageMain)
|
||||
const recipeEntry = path.resolve(recipePackageMain)
|
||||
process.chdir(originalCwd)
|
||||
|
||||
await this.installRecipeAtPath(recipeEntry)
|
||||
|
||||
require("rimraf").sync(recipeRepoPath)
|
||||
rimraf.sync(recipeRepoPath)
|
||||
}
|
||||
} else {
|
||||
await this.installRecipeAtPath(require("path").resolve(args.recipe))
|
||||
await this.installRecipeAtPath(path.resolve(args.recipe))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import * as path from "path"
|
||||
import {flags} from "@oclif/command"
|
||||
import {Command} from "../command"
|
||||
import type {AppGeneratorOptions} from "@blitzjs/generator"
|
||||
import {AppGenerator, 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"
|
||||
import {Db} from "./db"
|
||||
|
||||
export interface Flags {
|
||||
ts: boolean
|
||||
@@ -57,8 +60,8 @@ export class New extends Command {
|
||||
debug("flags: ", flags)
|
||||
|
||||
try {
|
||||
const destinationRoot = require("path").resolve(args.name)
|
||||
const appName = require("path").basename(destinationRoot)
|
||||
const destinationRoot = path.resolve(args.name)
|
||||
const appName = path.basename(destinationRoot)
|
||||
|
||||
const formChoices: Array<{name: AppGeneratorOptions["form"]; message?: string}> = [
|
||||
{name: "React Final Form", message: "React Final Form (recommended)"},
|
||||
@@ -75,7 +78,7 @@ export class New extends Command {
|
||||
|
||||
const {"dry-run": dryRun, "skip-install": skipInstall, npm} = flags
|
||||
|
||||
const generator = new (require("@blitzjs/generator").AppGenerator)({
|
||||
const generator = new AppGenerator({
|
||||
destinationRoot,
|
||||
appName,
|
||||
dryRun,
|
||||
@@ -102,7 +105,7 @@ export class New extends Command {
|
||||
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"])
|
||||
await Db.run(["migrate", "--name", "Initial Migration"])
|
||||
spinner.succeed()
|
||||
} catch {
|
||||
spinner.fail()
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import {dev, prod} from "@blitzjs/server"
|
||||
import {Command, flags} from "@oclif/command"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import pkgDir from "pkg-dir"
|
||||
import {runPrismaGeneration} from "./db"
|
||||
|
||||
const projectRoot = pkgDir.sync() || process.cwd()
|
||||
const isTypescript = fs.existsSync(path.join(projectRoot, "tsconfig.json"))
|
||||
|
||||
export class Start extends Command {
|
||||
static description = "Start a development server"
|
||||
@@ -17,31 +24,24 @@ 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() {
|
||||
|
||||
const {flags} = this.parse(Start)
|
||||
|
||||
const config = {
|
||||
rootFolder: process.cwd(),
|
||||
port: flags.port,
|
||||
hostname: flags.hostname,
|
||||
inspect: flags.inspect,
|
||||
clean: flags["no-incremental-build"],
|
||||
isTypescript,
|
||||
}
|
||||
|
||||
try {
|
||||
if (flags.production) {
|
||||
await prod(config)
|
||||
await prod(config, runPrismaGeneration({silent: true, failSilently: true}))
|
||||
} else {
|
||||
await dev(config)
|
||||
await dev(config, runPrismaGeneration({silent: true, failSilently: true}))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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"
|
||||
@@ -19,7 +20,7 @@ export class Test extends Command {
|
||||
if (watch) {
|
||||
watchMode = watch === "watch" || watch === "w"
|
||||
}
|
||||
const packageManager = require("has-yarn")() ? "yarn" : "npm"
|
||||
const packageManager = hasYarn() ? "yarn" : "npm"
|
||||
|
||||
if (watchMode) spawn(packageManager, ["test:watch"], {stdio: "inherit"})
|
||||
else spawn(packageManager, ["test"], {stdio: "inherit"})
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
disconnect: () => Promise.resolve(),
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default async () => {
|
||||
await Promise.resolve(10)
|
||||
}
|
||||
@@ -37,11 +37,30 @@ 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, failSilently: true})
|
||||
})
|
||||
|
||||
it("runs repl", async () => {
|
||||
await Console.prototype.run()
|
||||
expect(repl.runRepl).toHaveBeenCalled()
|
||||
|
||||
@@ -17,7 +17,6 @@ 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")
|
||||
@@ -42,11 +41,6 @@ beforeAll(async () => {
|
||||
["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},
|
||||
]
|
||||
})
|
||||
|
||||
describe("Db command", () => {
|
||||
@@ -82,13 +76,6 @@ describe("Db command", () => {
|
||||
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
|
||||
@@ -150,18 +137,6 @@ 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"])
|
||||
|
||||
@@ -189,4 +164,10 @@ describe("Db command", () => {
|
||||
|
||||
expect(spawn.mock.calls.length).toBe(0)
|
||||
})
|
||||
|
||||
it("runs db migrate silently with the right args when name flag is used", async () => {
|
||||
await Db.run(["migrate", "--name", "name"])
|
||||
|
||||
expectDbMigrateWithNameOutcome()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -10,7 +10,6 @@
|
||||
"sourceMap": false,
|
||||
"esModuleInterop": true,
|
||||
"types": [],
|
||||
"noEmit": false,
|
||||
"lib": ["dom", "dom.iterable", "ES2018"]
|
||||
},
|
||||
"include": ["src/**/*", "types"],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"config"
|
||||
],
|
||||
"author": "Fran Zekan <zekan.fran369@gmail.com>",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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"],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@blitzjs/core",
|
||||
"description": "Blitz.js core functionality",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
@@ -40,19 +40,18 @@
|
||||
"url": "https://github.com/blitz-js/blitz"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blitzjs/config": "0.23.1-canary.0",
|
||||
"@blitzjs/display": "0.23.1-canary.0",
|
||||
"@blitzjs/config": "0.21.2-canary.1",
|
||||
"@blitzjs/display": "0.21.2-canary.1",
|
||||
"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.23.0",
|
||||
"react-query": "2.5.11",
|
||||
"serialize-error": "6.0.0",
|
||||
"superjson": "1.2.2",
|
||||
"url": "0.11.0"
|
||||
"superjson": "1.2.1",
|
||||
"url": "0.11.0",
|
||||
"utility-types": "3.10.0"
|
||||
},
|
||||
"gitHead": "d3b9fce0bdd251c2b1890793b0aa1cd77c1c0922"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {NextPage, NextComponentType, NextPageContext} from "next"
|
||||
import {NextPage, NextComponentType} from "next"
|
||||
import {AppProps as NextAppProps} from "next/app"
|
||||
|
||||
export * from "./use-query"
|
||||
@@ -49,10 +49,10 @@ 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 type BlitzComponentType = NextComponentType
|
||||
|
||||
export interface AppProps<P = {}> extends NextAppProps<P> {
|
||||
Component: BlitzComponentType<NextPageContext, any, P> & {
|
||||
export interface AppProps extends NextAppProps {
|
||||
Component: BlitzComponentType & {
|
||||
getLayout?: (component: JSX.Element) => JSX.Element
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
/* 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 {
|
||||
protocol?: string
|
||||
}
|
||||
export interface MiddlewareRequest extends BlitzApiRequest {}
|
||||
export interface MiddlewareResponse extends BlitzApiResponse {
|
||||
/**
|
||||
* This will be passed as the second argument to Blitz queries/mutations.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* 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 {BlitzApiRequest, BlitzApiResponse} from "."
|
||||
import {
|
||||
getAllMiddlewareForModule,
|
||||
handleRequestWithMiddleware,
|
||||
@@ -9,17 +7,14 @@ import {
|
||||
} from "./middleware"
|
||||
import {SessionContext, PublicData} from "./supertokens"
|
||||
import {log} from "@blitzjs/display"
|
||||
import passport, {AuthenticateOptions, Strategy} from "passport"
|
||||
import passport, {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 = {
|
||||
@@ -39,23 +34,19 @@ 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[] = [
|
||||
connectMiddleware(cookieSessionMiddleware as ConnectMiddleware),
|
||||
connectMiddleware(passportMiddleware as ConnectMiddleware),
|
||||
// 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(passport.session()),
|
||||
]
|
||||
|
||||
if (config.secureProxy) {
|
||||
middleware.push(secureProxyMiddleware)
|
||||
}
|
||||
|
||||
if (!req.query.auth.length) {
|
||||
return res.status(404).end()
|
||||
}
|
||||
@@ -80,9 +71,7 @@ export function passportAuth(config: BlitzPassportConfig) {
|
||||
return next()
|
||||
})
|
||||
}
|
||||
middleware.push(
|
||||
connectMiddleware(passport.authenticate(strategy.name, {...config.authenticateOptions})),
|
||||
)
|
||||
middleware.push(connectMiddleware(passport.authenticate(strategy.name)))
|
||||
} else if (req.query.auth[1] === "callback") {
|
||||
log.info(`Processing callback for ${strategy.name}...`)
|
||||
middleware.push(
|
||||
|
||||
@@ -19,7 +19,7 @@ type Options = {
|
||||
resultOfGetFetchMore?: any
|
||||
}
|
||||
|
||||
export function executeRpcCall(url: string, params: any, opts: Options = {}) {
|
||||
export async function executeRpcCall(url: string, params: any, opts: Options = {}) {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
const headers: Record<string, any> = {
|
||||
@@ -46,68 +46,56 @@ export function executeRpcCall(url: string, params: any, opts: Options = {}) {
|
||||
serialized = serialize(params)
|
||||
}
|
||||
|
||||
// Create a new AbortController instance for this request
|
||||
const controller = new AbortController()
|
||||
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,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
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()
|
||||
}
|
||||
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 payload
|
||||
try {
|
||||
payload = await result.json()
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse json from request to ${url}`)
|
||||
}
|
||||
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 (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
|
||||
if (!opts.fromQueryHook) {
|
||||
const queryKey = getQueryKey(url, params)
|
||||
queryCache.setQueryData(queryKey, data)
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
executeRpcCall.warm = (url: string) => {
|
||||
@@ -125,18 +113,13 @@ interface ResolverEnhancement {
|
||||
apiUrl: string
|
||||
}
|
||||
}
|
||||
|
||||
interface CancellablePromise<T> extends Promise<T> {
|
||||
cancel?: Function
|
||||
}
|
||||
|
||||
export interface RpcFunction {
|
||||
(params: any, opts: any): CancellablePromise<any>
|
||||
(params: any, opts: any): Promise<any>
|
||||
}
|
||||
export interface EnhancedRpcFunction extends RpcFunction, ResolverEnhancement {}
|
||||
|
||||
export interface EnhancedResolverModule extends ResolverEnhancement {
|
||||
(input: any, ctx: Record<string, any>): CancellablePromise<unknown>
|
||||
(input: any, ctx: Record<string, any>): Promise<unknown>
|
||||
middleware?: Middleware[]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
// @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")
|
||||
})
|
||||
})
|
||||
@@ -1,23 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import {useState} from "react"
|
||||
import BadBehavior from "bad-behavior"
|
||||
import {useIsomorphicLayoutEffect} from "./utils/hooks"
|
||||
import {queryCache} from "react-query"
|
||||
import {DeepNonNullable} from "utility-types"
|
||||
|
||||
export const TOKEN_SEPARATOR = ";"
|
||||
export const HANDLE_SEPARATOR = ":"
|
||||
@@ -27,14 +28,19 @@ function assert(condition: any, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message)
|
||||
}
|
||||
|
||||
export interface PublicData extends Record<any, any> {
|
||||
export interface DefaultAuthTypes {
|
||||
userId: any
|
||||
}
|
||||
export interface AuthTypes extends DefaultAuthTypes {}
|
||||
|
||||
export interface PublicData extends Record<any, any> {
|
||||
userId: AuthTypes["userId"] | null
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
export interface SessionModel extends Record<any, any> {
|
||||
handle: string
|
||||
userId?: any
|
||||
userId?: AuthTypes["userId"]
|
||||
expiresAt?: Date
|
||||
hashedSessionToken?: string
|
||||
antiCSRFToken?: string
|
||||
@@ -47,31 +53,40 @@ export type SessionConfig = {
|
||||
method?: "essential" | "advanced"
|
||||
sameSite?: "none" | "lax" | "strict"
|
||||
getSession: (handle: string) => Promise<SessionModel | null>
|
||||
getSessions: (userId: any) => Promise<SessionModel[]>
|
||||
getSessions: (userId: AuthTypes["userId"]) => Promise<SessionModel[]>
|
||||
createSession: (session: SessionModel) => Promise<SessionModel>
|
||||
updateSession: (handle: string, session: Partial<SessionModel>) => Promise<SessionModel>
|
||||
deleteSession: (handle: string) => Promise<SessionModel>
|
||||
unstable_isAuthorized: (userRoles: string[], input?: any) => boolean
|
||||
}
|
||||
|
||||
export interface SessionContext {
|
||||
export interface SessionContextBase {
|
||||
/**
|
||||
* null if anonymous
|
||||
*/
|
||||
userId: any
|
||||
userId: unknown
|
||||
roles: string[]
|
||||
handle: string | null
|
||||
publicData: PublicData
|
||||
authorize: (input?: any) => void
|
||||
isAuthorized: (input?: any) => boolean
|
||||
authorize(input?: any): asserts this is AuthenticatedSessionContext
|
||||
isAuthorized(input?: any): boolean
|
||||
// authorize: (roleOrRoles?: string | string[]) => void
|
||||
// isAuthorized: (roleOrRoles?: string | string[]) => boolean
|
||||
create: (publicData: PublicData, privateData?: Record<any, any>) => Promise<void>
|
||||
revoke: () => Promise<void>
|
||||
revokeAll: () => Promise<void>
|
||||
getPrivateData: () => Promise<Record<any, any>>
|
||||
setPrivateData: (data: Record<any, any>) => Promise<void>
|
||||
setPublicData: (data: Record<any, any>) => Promise<void>
|
||||
create(publicData: DeepNonNullable<PublicData>, privateData?: Record<any, any>): Promise<void>
|
||||
revoke(): Promise<void>
|
||||
revokeAll(): Promise<void>
|
||||
getPrivateData(): Promise<Record<any, any>>
|
||||
setPrivateData(data: Record<any, any>): Promise<void>
|
||||
setPublicData(data: Record<any, any>): Promise<void>
|
||||
}
|
||||
|
||||
export interface AuthenticatedSessionContext extends SessionContextBase {
|
||||
userId: AuthTypes["userId"]
|
||||
}
|
||||
|
||||
// Anonymous session context
|
||||
export interface SessionContext extends SessionContextBase {
|
||||
userId: AuthTypes["userId"] | null
|
||||
}
|
||||
|
||||
// Taken from https://github.com/HenrikJoreteg/cookie-getter
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
useInfiniteQuery as useInfiniteReactQuery,
|
||||
InfiniteQueryResult,
|
||||
InfiniteQueryConfig,
|
||||
InfiniteQueryOptions,
|
||||
} from "react-query"
|
||||
import {emptyQueryFn, retryFunction} from "./use-query"
|
||||
import {useIsDevPrerender, emptyQueryFn, retryFunction} from "./use-query"
|
||||
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
|
||||
import {getQueryCacheFunctions, QueryCacheFunctions, getInfiniteQueryKey} from "./utils/query-cache"
|
||||
import {EnhancedRpcFunction} from "./rpc"
|
||||
@@ -14,12 +14,10 @@ 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: InfiniteQueryConfig<PromiseReturnType<T>, any>,
|
||||
options: InfiniteQueryOptions<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")
|
||||
@@ -31,13 +29,15 @@ export function useInfiniteQuery<T extends QueryFn>(
|
||||
)
|
||||
}
|
||||
|
||||
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
const queryRpcFn = useIsDevPrerender()
|
||||
? emptyQueryFn
|
||||
: ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
|
||||
const queryKey = getInfiniteQueryKey(queryFn, params)
|
||||
|
||||
const {data, ...queryRest} = useInfiniteReactQuery({
|
||||
queryKey,
|
||||
queryFn: (_infinite: boolean, _apiUrl: string, params: any, resultOfGetFetchMore?: any) =>
|
||||
queryFn: (_infinite, _apiUrl, params, resultOfGetFetchMore?) =>
|
||||
queryRpcFn(params, {fromQueryHook: true, resultOfGetFetchMore}),
|
||||
config: {
|
||||
suspense: true,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
usePaginatedQuery as usePaginatedReactQuery,
|
||||
PaginatedQueryResult,
|
||||
PaginatedQueryConfig,
|
||||
QueryOptions,
|
||||
} from "react-query"
|
||||
import {emptyQueryFn, retryFunction} from "./use-query"
|
||||
import {useIsDevPrerender, emptyQueryFn, retryFunction} from "./use-query"
|
||||
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
|
||||
import {QueryCacheFunctions, getQueryCacheFunctions, getQueryKey} from "./utils/query-cache"
|
||||
import {EnhancedRpcFunction} from "./rpc"
|
||||
@@ -14,12 +14,10 @@ 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?: PaginatedQueryConfig<PromiseReturnType<T>>,
|
||||
options?: QueryOptions<PaginatedQueryResult<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")
|
||||
@@ -31,13 +29,15 @@ export function usePaginatedQuery<T extends QueryFn>(
|
||||
)
|
||||
}
|
||||
|
||||
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
const queryRpcFn = useIsDevPrerender()
|
||||
? emptyQueryFn
|
||||
: ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
|
||||
const queryKey = getQueryKey(queryFn, params)
|
||||
|
||||
const {resolvedData, ...queryRest} = usePaginatedReactQuery({
|
||||
queryKey,
|
||||
queryFn: (_apiUrl: string, params: any) => queryRpcFn(params, {fromQueryHook: true}),
|
||||
queryFn: (_apiUrl, params) => queryRpcFn(params, {fromQueryHook: true}),
|
||||
config: {
|
||||
suspense: true,
|
||||
retry: retryFunction,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {useRouter} from "next/router"
|
||||
import {useRouterQuery} from "./use-router-query"
|
||||
import {fromPairs} from "lodash"
|
||||
|
||||
type ParsedUrlQueryValue = string | string[] | undefined
|
||||
|
||||
@@ -32,7 +31,7 @@ function areQueryValuesEqual(value1: ParsedUrlQueryValue, value2: ParsedUrlQuery
|
||||
}
|
||||
|
||||
export function extractRouterParams(routerQuery: ParsedUrlQuery, query: ParsedUrlQuery) {
|
||||
return fromPairs(
|
||||
return Object.fromEntries(
|
||||
Object.entries(routerQuery).filter(
|
||||
([key, value]) =>
|
||||
typeof query[key] === "undefined" || !areQueryValuesEqual(value, query[key]),
|
||||
@@ -52,9 +51,9 @@ export function useParams(returnType?: "string" | "number" | "array") {
|
||||
|
||||
if (returnType === "string") {
|
||||
const params: Record<string, string> = {}
|
||||
for (const key in rawParams) {
|
||||
if (typeof rawParams[key] === "string") {
|
||||
params[key] = rawParams[key] as string
|
||||
for (const [key, value] of Object.entries(rawParams)) {
|
||||
if (typeof value === "string") {
|
||||
params[key] = value
|
||||
}
|
||||
}
|
||||
return params
|
||||
@@ -62,9 +61,9 @@ export function useParams(returnType?: "string" | "number" | "array") {
|
||||
|
||||
if (returnType === "number") {
|
||||
const params: Record<string, number> = {}
|
||||
for (const key in rawParams) {
|
||||
if (rawParams[key]) {
|
||||
params[key] = Number(rawParams[key])
|
||||
for (const [key, value] of Object.entries(rawParams)) {
|
||||
if (value) {
|
||||
params[key] = Number(value)
|
||||
}
|
||||
}
|
||||
return params
|
||||
@@ -72,9 +71,9 @@ export function useParams(returnType?: "string" | "number" | "array") {
|
||||
|
||||
if (returnType === "array") {
|
||||
const params: Record<string, Array<string | undefined>> = {}
|
||||
for (const key in rawParams) {
|
||||
if (Array.isArray(rawParams[key])) {
|
||||
params[key] = rawParams[key] as Array<string | undefined>
|
||||
for (const [key, value] of Object.entries(rawParams)) {
|
||||
if (Array.isArray(value)) {
|
||||
params[key] = value
|
||||
}
|
||||
}
|
||||
return params
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useQuery as useReactQuery, QueryResult, QueryConfig} from "react-query"
|
||||
import {useQuery as useReactQuery, QueryResult, QueryOptions} from "react-query"
|
||||
import {PromiseReturnType, InferUnaryParam, QueryFn} from "./types"
|
||||
import {QueryCacheFunctions, getQueryCacheFunctions, getQueryKey} from "./utils/query-cache"
|
||||
import {EnhancedRpcFunction} from "./rpc"
|
||||
@@ -19,6 +19,17 @@ 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
|
||||
|
||||
@@ -31,7 +42,7 @@ export const retryFunction = (failureCount: number, error: any) => {
|
||||
export function useQuery<T extends QueryFn>(
|
||||
queryFn: T,
|
||||
params: InferUnaryParam<T> | (() => InferUnaryParam<T>),
|
||||
options?: QueryConfig<PromiseReturnType<T>>,
|
||||
options?: QueryOptions<QueryResult<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")
|
||||
@@ -43,13 +54,15 @@ export function useQuery<T extends QueryFn>(
|
||||
)
|
||||
}
|
||||
|
||||
const queryRpcFn = isServer ? emptyQueryFn : ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
const queryRpcFn = useIsDevPrerender()
|
||||
? emptyQueryFn
|
||||
: ((queryFn as unknown) as EnhancedRpcFunction)
|
||||
|
||||
const queryKey = getQueryKey(queryFn, params)
|
||||
|
||||
const {data, ...queryRest} = useReactQuery({
|
||||
queryKey,
|
||||
queryFn: (_apiUrl: string, params: any) => queryRpcFn(params, {fromQueryHook: true}),
|
||||
queryFn: (_apiUrl, params) => queryRpcFn(params, {fromQueryHook: true}),
|
||||
config: {
|
||||
suspense: true,
|
||||
retry: retryFunction,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {QueryKey} from "react-query"
|
||||
import {QueryKeyPart} 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, ...QueryKey[]] {
|
||||
export function getQueryKey(cacheKey: string, params: any): readonly [string, ...QueryKeyPart[]] {
|
||||
return [cacheKey, typeof params === "function" ? (params as Function)() : params]
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface QueryCacheFunctions<T> {
|
||||
mutate: (newData: T | ((oldData: T | undefined) => T), opts?: MutateOptions) => void
|
||||
}
|
||||
|
||||
export const getQueryCacheFunctions = <T>(queryKey: QueryKey): QueryCacheFunctions<T> => ({
|
||||
export const getQueryCacheFunctions = <T>(queryKey: QueryKey<any>): QueryCacheFunctions<T> => ({
|
||||
mutate: (newData, opts = {refetch: true}) => {
|
||||
queryCache.setQueryData(queryKey, newData)
|
||||
if (opts.refetch) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@blitzjs/display",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"description": "Display package for the Blitz CLI",
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@blitzjs/file-pipeline",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"description": "Display package for the Blitz CLI",
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -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,7 +19,12 @@ export function createEnrichFiles() {
|
||||
}
|
||||
|
||||
if (!file.hash) {
|
||||
file.hash = hash(file.path + file.stat?.mtime.toString())
|
||||
const hash = crypto
|
||||
.createHash("md5")
|
||||
.update(JSON.stringify({path: file.path, s: file.stat?.mtime}))
|
||||
.digest("hex")
|
||||
|
||||
file.hash = hash
|
||||
}
|
||||
|
||||
return file
|
||||
|
||||
92
packages/file-pipeline/src/helpers/work-optimizer/README.md
Normal file
92
packages/file-pipeline/src/helpers/work-optimizer/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# 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
|
||||
}
|
||||
```
|
||||
@@ -1,79 +1,37 @@
|
||||
// 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.
|
||||
*/
|
||||
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) : {}
|
||||
// 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> = []
|
||||
|
||||
const stats = {todo, done}
|
||||
|
||||
const reportComplete = transform.file(async (file) => {
|
||||
const pathHash = getOriginalPathHash(file)
|
||||
|
||||
delete todo[pathHash]
|
||||
|
||||
if (file.event === "add") {
|
||||
done[pathHash] = file.hash
|
||||
const reportComplete = transform.file((file) => {
|
||||
if (file.hash) {
|
||||
done.push(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[pathHash] === file.hash || todo[pathHash] === file.hash) {
|
||||
if (done.includes(file.hash) || todo.includes(file.hash)) {
|
||||
log.debug("Rejecting because this job has been done before: " + file.path)
|
||||
return next()
|
||||
}
|
||||
|
||||
todo[pathHash] = file.hash
|
||||
todo.push(file.hash)
|
||||
|
||||
push(file)
|
||||
|
||||
|
||||
@@ -1,31 +1,24 @@
|
||||
import {createWorkOptimizer} from "."
|
||||
import {take} from "../../test-utils"
|
||||
import {hash} from "../../utils"
|
||||
import {testStreamItems} from "../../test-utils"
|
||||
import {pipeline} from "../../streams"
|
||||
import {normalize, resolve} from "path"
|
||||
import {normalize} from "path"
|
||||
import File from "vinyl"
|
||||
|
||||
const pathToOneHash = hash(normalize("to/one"))
|
||||
const pathToTwoHash = hash(normalize("to/two"))
|
||||
const pathToThreeHash = hash(normalize("to/three"))
|
||||
function logItem(fileOrString: {path: string} | string) {
|
||||
if (typeof fileOrString === "string") {
|
||||
return fileOrString
|
||||
}
|
||||
return fileOrString.path
|
||||
}
|
||||
|
||||
describe("agnosticSource", () => {
|
||||
test("basic throughput", async () => {
|
||||
const saveCache = jest.fn()
|
||||
const readCache = jest.fn()
|
||||
const {triage, reportComplete} = createWorkOptimizer(
|
||||
normalize("/path"),
|
||||
normalize("/dest"),
|
||||
saveCache,
|
||||
readCache,
|
||||
)
|
||||
|
||||
const {triage, reportComplete} = createWorkOptimizer()
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "one",
|
||||
path: normalize("/path/to/one"),
|
||||
content: Buffer.from("one"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -34,7 +27,6 @@ describe("agnosticSource", () => {
|
||||
hash: "two",
|
||||
path: normalize("/path/to/two"),
|
||||
content: Buffer.from("two"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -43,31 +35,21 @@ 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)
|
||||
const items = await take<File>(stream, 3)
|
||||
expect(items.map(({path}) => path)).toEqual(expected)
|
||||
await testStreamItems(stream, expected, logItem)
|
||||
})
|
||||
|
||||
test("same file is rejected", async () => {
|
||||
const saveCache = jest.fn()
|
||||
const readCache = jest.fn()
|
||||
const {triage, reportComplete} = createWorkOptimizer(
|
||||
normalize("/path"),
|
||||
normalize("/dest"),
|
||||
saveCache,
|
||||
readCache,
|
||||
)
|
||||
const {triage, reportComplete} = createWorkOptimizer()
|
||||
triage.write(
|
||||
new File({
|
||||
hash: "one",
|
||||
path: normalize("/path/to/one"),
|
||||
content: Buffer.from("one"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -76,7 +58,6 @@ describe("agnosticSource", () => {
|
||||
hash: "one",
|
||||
path: normalize("/path/to/one"),
|
||||
content: Buffer.from("one"),
|
||||
event: "add",
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -85,156 +66,11 @@ 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)
|
||||
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,
|
||||
)
|
||||
await testStreamItems(stream, expected, logItem)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,9 +8,7 @@ import {Writable} from "stream"
|
||||
import {isFile} from "../../utils"
|
||||
import {transform} from "../../transform"
|
||||
|
||||
const isUnlinkFile = (file: File) => {
|
||||
return file.event === "unlink" || file.event === "unlinkDir"
|
||||
}
|
||||
const isUnlinkFile = (file: File) => file.event === "unlink" || file.event === "unlinkDir"
|
||||
|
||||
/**
|
||||
* Returns a Stage that writes files to the destination path
|
||||
|
||||
@@ -1,61 +1,16 @@
|
||||
import {Writable} from "stream"
|
||||
import File from "vinyl"
|
||||
import {pipeline, through} from "./streams"
|
||||
import {Stage, StageArgs, StageConfig, EventedFile} from "./types"
|
||||
import {Stage, StageArgs, StageConfig} from "./types"
|
||||
import {agnosticSource} from "./helpers/agnostic-source"
|
||||
import {createEnrichFiles} from "./helpers/enrich-files"
|
||||
import {createFileCache, FileCache} from "./helpers/file-cache"
|
||||
import {createFileCache} 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
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
return file.hash.indexOf(":") === -1
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,13 +30,18 @@ export function createPipeline(
|
||||
) {
|
||||
// Helper streams don't account for business stages
|
||||
const input = through.obj()
|
||||
const optimizer = createWorkOptimizer(config.src, config.dest)
|
||||
const optimizer = createWorkOptimizer()
|
||||
const enrichFiles = createEnrichFiles()
|
||||
const srcCache = createFileCache(isSourceFile)
|
||||
const idleHandler = createIdleHandler(bus)
|
||||
|
||||
// Send this object to every stage
|
||||
const api = createStageArgs(config, input, bus, srcCache.cache)
|
||||
const api: StageArgs = {
|
||||
config,
|
||||
input,
|
||||
bus,
|
||||
getInputCache: () => srcCache.cache,
|
||||
}
|
||||
|
||||
// Initialize each stage
|
||||
const initializedStages = stages.map((stage) => stage(api))
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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[],
|
||||
@@ -27,22 +25,3 @@ 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)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ describe("transformFiles", () => {
|
||||
.concat(["ready"]),
|
||||
logFile,
|
||||
),
|
||||
transformFiles(normalize("/foo"), [], normalize("/bar"), {source, writer}),
|
||||
transformFiles(normalize("/foo"), [], normalize("/bar"), {source, writer, noclean: true}),
|
||||
])
|
||||
}, 10000)
|
||||
})
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {ensureDir, pathExists, remove} from "fs-extra"
|
||||
import {Transform} from "stream"
|
||||
import {createDisplay} from "../display"
|
||||
import {ERROR_THROWN, READY} from "../events"
|
||||
import {pipe} from "../streams"
|
||||
import {createPipeline} from "../pipeline"
|
||||
import {pipe, through} from "../streams"
|
||||
import {pathExists, ensureDir, remove} from "fs-extra"
|
||||
import {through} from "../streams"
|
||||
import {createDisplay} from "../display"
|
||||
import {READY, ERROR_THROWN} from "../events"
|
||||
import {Stage} from "../types"
|
||||
import {Transform} from "stream"
|
||||
|
||||
type FSStreamer = {stream: NodeJS.ReadWriteStream}
|
||||
|
||||
@@ -15,7 +16,8 @@ type SynchronizeFilesOptions = {
|
||||
bus?: Transform
|
||||
source?: FSStreamer
|
||||
writer?: FSStreamer
|
||||
clean?: boolean
|
||||
noclean?: boolean
|
||||
isTypescript?: boolean
|
||||
}
|
||||
|
||||
const defaultBus = through.obj()
|
||||
@@ -39,13 +41,17 @@ export async function transformFiles(
|
||||
bus = defaultBus,
|
||||
source,
|
||||
writer,
|
||||
clean: requestClean,
|
||||
noclean = false,
|
||||
isTypescript = true,
|
||||
} = options
|
||||
|
||||
if (requestClean) await clean(dest)
|
||||
// HACK: cleaning the dev folder on every restart means we do more work than necessary
|
||||
// TODO: remove this clean and devise a way to resolve differences in stream
|
||||
if (!noclean) await clean(dest)
|
||||
|
||||
// const errors = createErrorsStream(reporter.stream)
|
||||
const display = createDisplay()
|
||||
return await new Promise((resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const config = {
|
||||
cwd: src,
|
||||
src,
|
||||
@@ -53,6 +59,7 @@ export async function transformFiles(
|
||||
include,
|
||||
ignore,
|
||||
watch,
|
||||
isTypescript,
|
||||
}
|
||||
const fileTransformPipeline = createPipeline(config, stages, bus, source, writer)
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("transformFiles", () => {
|
||||
["/foo/one", "/foo/two", "/foo/three"].map(normalize).concat(["ready"]),
|
||||
logFile,
|
||||
),
|
||||
transformFiles(normalize("/foo"), [], normalize("/bar"), {source, writer}),
|
||||
transformFiles(normalize("/foo"), [], normalize("/bar"), {source, writer, noclean: true}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Writable} from "stream"
|
||||
import File from "vinyl"
|
||||
import {FileCache} from "./helpers/file-cache"
|
||||
import File from "vinyl"
|
||||
|
||||
export type EventedFile = {
|
||||
event: "add" | "change" | "unlink" | "unlinkDir"
|
||||
@@ -21,6 +21,7 @@ export type StageConfig = {
|
||||
include: string[]
|
||||
ignore: string[]
|
||||
watch: boolean
|
||||
isTypescript: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,13 +32,6 @@ export type StageArgs = {
|
||||
input: Writable
|
||||
bus: Writable
|
||||
getInputCache: () => FileCache
|
||||
processNewFile: (file: File) => void
|
||||
processNewChildFile: (a: {
|
||||
parent: EventedFile
|
||||
child: File
|
||||
stageId: string
|
||||
subfileId: string
|
||||
}) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import File from "vinyl"
|
||||
import {PipelineEvent, EventedFile} from "./types"
|
||||
import crypto from "crypto"
|
||||
|
||||
export function isFile(file: any): file is EventedFile {
|
||||
return File.isVinyl(file)
|
||||
@@ -9,7 +8,3 @@ export function isFile(file: any): file is EventedFile {
|
||||
export function isEvent(file: any): file is PipelineEvent {
|
||||
return typeof file === "string"
|
||||
}
|
||||
|
||||
export function hash(input: string) {
|
||||
return crypto.createHash("md5").update(input).digest("hex")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@blitzjs/generator",
|
||||
"version": "0.23.1-canary.0",
|
||||
"version": "0.21.2-canary.1",
|
||||
"description": "File generation for the Blitz CLI",
|
||||
"homepage": "https://github.com/blitz-js/blitz#readme",
|
||||
"license": "MIT",
|
||||
@@ -36,7 +36,7 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "7.9.0",
|
||||
"@babel/plugin-transform-typescript": "7.9.4",
|
||||
"@blitzjs/display": "0.23.1-canary.0",
|
||||
"@blitzjs/display": "0.21.2-canary.1",
|
||||
"@types/jscodeshift": "0.7.1",
|
||||
"chalk": "4.0.0",
|
||||
"cross-spawn": "7.0.3",
|
||||
@@ -48,7 +48,7 @@
|
||||
"jscodeshift": "0.10.0",
|
||||
"mem-fs": "1.1.3",
|
||||
"mem-fs-editor": "6.0.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-fetch": "2.6.0",
|
||||
"pluralize": "8.0.0",
|
||||
"recast": "0.19.1",
|
||||
"username": "5.1.0",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {Generator, GeneratorOptions} from "../generator"
|
||||
import {join} from "path"
|
||||
import {camelCaseToKebabCase} from "../utils/kebab-case"
|
||||
|
||||
export interface PageGeneratorOptions extends GeneratorOptions {
|
||||
ModelName: string
|
||||
@@ -52,10 +51,9 @@ export class PageGenerator extends Generator<PageGeneratorOptions> {
|
||||
}
|
||||
|
||||
getTargetDirectory() {
|
||||
const kebabCaseModelName = camelCaseToKebabCase(this.options.modelNames)
|
||||
const parent = this.options.parentModels
|
||||
? `${this.options.parentModels}/__parentModelParam__/`
|
||||
: ""
|
||||
return `app/${this.getModelNamesPath()}/pages/${parent}${kebabCaseModelName}`
|
||||
return `app/${this.getModelNamesPath()}/pages/${parent}${this.options.modelNames}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {Generator, GeneratorOptions} from "../generator"
|
||||
import {join} from "path"
|
||||
|
||||
export interface QueryGeneratorOptions extends GeneratorOptions {
|
||||
rawInput: string
|
||||
modelName: string
|
||||
}
|
||||
|
||||
export class QueryGenerator extends Generator<QueryGeneratorOptions> {
|
||||
@@ -12,7 +12,7 @@ export class QueryGenerator extends Generator<QueryGeneratorOptions> {
|
||||
// eslint-disable-next-line require-await
|
||||
async getTemplateValues() {
|
||||
return {
|
||||
rawInput: this.options.rawInput,
|
||||
modelName: this.options.modelName,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export function camelCaseToKebabCase(transformString: string) {
|
||||
return transformString.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, "$1-$2").toLowerCase()
|
||||
}
|
||||
@@ -6,7 +6,6 @@ type NpmDepResponse = {versions: Record<string, PackageInformation>}
|
||||
export const fetchAllVersions = async (dependency: string) => {
|
||||
const res = await got(`https://registry.npmjs.org/${dependency}`, {
|
||||
retry: {limit: 3},
|
||||
timeout: 3000,
|
||||
responseType: "json",
|
||||
}).json<NpmDepResponse>()
|
||||
return Object.keys(res.versions)
|
||||
@@ -17,7 +16,6 @@ type NpmDistTagsResponse = {latest: string; canary: string}
|
||||
export const fetchDistTags = async (dependency: string) => {
|
||||
const res = await got(`https://registry.npmjs.org/-/package/${dependency}/dist-tags`, {
|
||||
retry: {limit: 3},
|
||||
timeout: 3000,
|
||||
responseType: "json",
|
||||
}).json<NpmDistTagsResponse>()
|
||||
return res
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# This env file should NOT be checked into source control
|
||||
# This is the place for values that changed for every developer
|
||||
|
||||
# SQLite is ready to go out of the box, but you can switch to Postgres
|
||||
# by first changing the provider from "sqlite" to "postgres" in the Prisma
|
||||
# schema file and by second swapping the DATABASE_URL below.
|
||||
# SQLite is ready to go out of the box, but you can switch to Postgres easily
|
||||
# by swapping the DATABASE_URL.
|
||||
DATABASE_URL="file:./db.sqlite"
|
||||
# DATABASE_URL=postgresql://__username__@localhost:5432/__name__
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# THIS FILE SHOULD NOT BE CHECKED INTO YOUR VERSION CONTROL SYSTEM
|
||||
|
||||
# SQLite is ready to go out of the box, but you can switch to Postgres
|
||||
# by first changing the provider from "sqlite" to "postgres" in the Prisma
|
||||
# schema file and by second swapping the DATABASE_URL below.
|
||||
# SQLite is ready to go out of the box, but you can switch to Postgres easily
|
||||
# by swapping the DATABASE_URL.
|
||||
DATABASE_URL="file:./db_test.sqlite"
|
||||
# DATABASE_URL=postgresql://__username__@localhost:5432/__name___test
|
||||
|
||||
@@ -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<S extends z.ZodType<any, any>> = {
|
||||
type FormProps<FormValues> = {
|
||||
/** All your form fields */
|
||||
children: ReactNode
|
||||
/** Text to display in the submit button */
|
||||
submitText: string
|
||||
schema?: S
|
||||
onSubmit: FinalFormProps<z.infer<S>>["onSubmit"]
|
||||
initialValues?: FinalFormProps<z.infer<S>>["initialValues"]
|
||||
onSubmit: FinalFormProps<FormValues>["onSubmit"]
|
||||
initialValues?: FinalFormProps<FormValues>["initialValues"]
|
||||
schema?: z.ZodType<any, any>
|
||||
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
|
||||
|
||||
export function Form<S extends z.ZodType<any, any>>({
|
||||
export function Form<FormValues extends Record<string, unknown>>({
|
||||
children,
|
||||
submitText,
|
||||
schema,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
...props
|
||||
}: FormProps<S>) {
|
||||
}: FormProps<FormValues>) {
|
||||
return (
|
||||
<FinalForm
|
||||
<FinalForm<FormValues>
|
||||
initialValues={initialValues}
|
||||
validate={(values) => {
|
||||
if (!schema) return
|
||||
|
||||
@@ -18,8 +18,6 @@ export const LabeledTextField = React.forwardRef<HTMLInputElement, LabeledTextFi
|
||||
meta: {touched, error, submitError, submitting},
|
||||
} = useField(name)
|
||||
|
||||
const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError
|
||||
|
||||
return (
|
||||
<div {...outerProps}>
|
||||
<label>
|
||||
@@ -27,9 +25,9 @@ export const LabeledTextField = React.forwardRef<HTMLInputElement, LabeledTextFi
|
||||
<input {...input} disabled={submitting} {...props} ref={ref} />
|
||||
</label>
|
||||
|
||||
{touched && normalizedError && (
|
||||
{touched && (error || submitError) && (
|
||||
<div role="alert" style={{color: "red"}}>
|
||||
{normalizedError}
|
||||
{error || submitError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useState, ReactNode, PropsWithoutRef } from "react"
|
||||
import { Formik, FormikProps } from "formik"
|
||||
import * as z from "zod"
|
||||
import React, { useState, ReactNode, PropsWithoutRef } from "react";
|
||||
import { Formik, FormikProps, FormikErrors } from "formik";
|
||||
import * as z from "zod";
|
||||
|
||||
type FormProps<S extends z.ZodType<any, any>> = {
|
||||
type FormProps<FormValues> = {
|
||||
/** All your form fields */
|
||||
children: ReactNode
|
||||
children: ReactNode;
|
||||
/** Text to display in the submit button */
|
||||
submitText: string
|
||||
schema?: S
|
||||
onSubmit: (values: z.infer<S>) => Promise<void | OnSubmitResult>
|
||||
initialValues?: FormikProps<z.infer<S>>["initialValues"]
|
||||
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
|
||||
submitText: string;
|
||||
onSubmit: (values: FormValues) => Promise<void | OnSubmitResult>;
|
||||
initialValues?: FormikProps<FormValues>["initialValues"];
|
||||
schema?: z.ZodType<any, any>;
|
||||
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">;
|
||||
|
||||
type OnSubmitResult = {
|
||||
FORM_ERROR?: string
|
||||
@@ -19,40 +19,44 @@ type OnSubmitResult = {
|
||||
|
||||
export const FORM_ERROR = "FORM_ERROR"
|
||||
|
||||
export function Form<S extends z.ZodType<any, any>>({
|
||||
export function Form<FormValues extends Record<string, unknown>>({
|
||||
children,
|
||||
submitText,
|
||||
schema,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
...props
|
||||
}: FormProps<S>) {
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
}: FormProps<FormValues>) {
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues || {}}
|
||||
<Formik<FormValues>
|
||||
initialValues={initialValues || {} as FormValues}
|
||||
validate={(values) => {
|
||||
if (!schema) return
|
||||
if (!schema) return;
|
||||
try {
|
||||
schema.parse(values)
|
||||
schema.parse(values);
|
||||
} catch (error) {
|
||||
return error.formErrors.fieldErrors
|
||||
return error.formErrors.fieldErrors;
|
||||
}
|
||||
}}
|
||||
onSubmit={async (values, { setErrors }) => {
|
||||
const { FORM_ERROR, ...otherErrors } = (await onSubmit(values)) || {}
|
||||
onSubmit={async (values, {setErrors}) => {
|
||||
const {FORM_ERROR, ...otherErrors} = (await onSubmit(values as FormValues)) || {}
|
||||
|
||||
if (FORM_ERROR) {
|
||||
setFormError(FORM_ERROR)
|
||||
}
|
||||
if(FORM_ERROR) {
|
||||
setFormError(FORM_ERROR);
|
||||
}
|
||||
|
||||
if (Object.keys(otherErrors).length > 0) {
|
||||
setErrors(otherErrors)
|
||||
}
|
||||
if(Object.keys(otherErrors).length > 0) {
|
||||
setErrors(otherErrors as FormikErrors<FormValues>)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ handleSubmit, isSubmitting }) => (
|
||||
<form onSubmit={handleSubmit} className="form" {...props}>
|
||||
{({ handleSubmit, isSubmitting, }) => (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="form"
|
||||
{...props}
|
||||
>
|
||||
{/* Form fields supplied as children are rendered here */}
|
||||
{children}
|
||||
|
||||
@@ -74,7 +78,7 @@ export function Form<S extends z.ZodType<any, any>>({
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Form
|
||||
export default Form;
|
||||
|
||||
@@ -2,14 +2,14 @@ import React, { useState, ReactNode, PropsWithoutRef } from "react"
|
||||
import { FormProvider, useForm, UseFormOptions } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
type FormProps<S extends z.ZodType<any, any>> = {
|
||||
type FormProps<FormValues> = {
|
||||
/** All your form fields */
|
||||
children: ReactNode
|
||||
/** Text to display in the submit button */
|
||||
submitText: string
|
||||
schema?: S
|
||||
onSubmit: (values: z.infer<S>) => Promise<void | OnSubmitResult>
|
||||
initialValues?: UseFormOptions<z.infer<S>>["defaultValues"]
|
||||
onSubmit: (values: FormValues) => Promise<void | OnSubmitResult>
|
||||
initialValues?: UseFormOptions<FormValues>["defaultValues"]
|
||||
schema?: z.ZodType<any, any>
|
||||
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
|
||||
|
||||
type OnSubmitResult = {
|
||||
@@ -19,15 +19,15 @@ type OnSubmitResult = {
|
||||
|
||||
export const FORM_ERROR = "FORM_ERROR"
|
||||
|
||||
export function Form<S extends z.ZodType<any, any>>({
|
||||
export function Form<FormValues extends Record<string, unknown>>({
|
||||
children,
|
||||
submitText,
|
||||
schema,
|
||||
initialValues,
|
||||
onSubmit,
|
||||
...props
|
||||
}: FormProps<S>) {
|
||||
const ctx = useForm<z.infer<S>>({
|
||||
}: FormProps<FormValues>) {
|
||||
const ctx = useForm<FormValues>({
|
||||
mode: "onBlur",
|
||||
resolver: async (values) => {
|
||||
try {
|
||||
@@ -36,7 +36,7 @@ export function Form<S extends z.ZodType<any, any>>({
|
||||
}
|
||||
return { values, errors: {} }
|
||||
} catch (error) {
|
||||
return { values: {}, errors: error.formErrors?.fieldErrors }
|
||||
return { values: {}, errors: error.formErrors?.fieldErrors } as any
|
||||
}
|
||||
},
|
||||
defaultValues: initialValues,
|
||||
@@ -47,7 +47,7 @@ export function Form<S extends z.ZodType<any, any>>({
|
||||
<FormProvider {...ctx}>
|
||||
<form
|
||||
onSubmit={ctx.handleSubmit(async (values) => {
|
||||
const result = (await onSubmit(values)) || {}
|
||||
const result = (await onSubmit(values as FormValues)) || {}
|
||||
for (const [key, value] of Object.entries(result)) {
|
||||
if (key === FORM_ERROR) {
|
||||
setFormError(value)
|
||||
|
||||
@@ -18,7 +18,7 @@ export const verifyPassword = async (hashedPassword: string, password: string) =
|
||||
}
|
||||
|
||||
export const authenticateUser = async (email: string, password: string) => {
|
||||
const user = await db.user.findOne({ where: { email: email.toLowerCase() } })
|
||||
const user = await db.user.findOne({ where: { email } })
|
||||
|
||||
if (!user || !user.hashedPassword) throw new AuthenticationError()
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
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 } from "app/auth/validations"
|
||||
import { LoginInput, LoginInputType } from "app/auth/validations"
|
||||
|
||||
type LoginFormProps = {
|
||||
onSuccess?: () => void
|
||||
@@ -14,14 +13,14 @@ export const LoginForm = (props: LoginFormProps) => {
|
||||
<div>
|
||||
<h1>Login</h1>
|
||||
|
||||
<Form
|
||||
<Form<LoginInputType>
|
||||
submitText="Log In"
|
||||
schema={LoginInput}
|
||||
initialValues={{ email: "", password: "" }}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await login({ email: values.email, password: values.password })
|
||||
props.onSuccess?.()
|
||||
props.onSuccess && props.onSuccess()
|
||||
} catch (error) {
|
||||
if (error.name === "AuthenticationError") {
|
||||
return { [FORM_ERROR]: "Sorry, those credentials are invalid" }
|
||||
@@ -37,10 +36,6 @@ 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react"
|
||||
import { LabeledTextField } from "app/components/LabeledTextField"
|
||||
import { Form, FORM_ERROR } from "app/components/Form"
|
||||
import signup from "app/auth/mutations/signup"
|
||||
import { SignupInput } from "app/auth/validations"
|
||||
import { SignupInput, SignupInputType } from "app/auth/validations"
|
||||
|
||||
type SignupFormProps = {
|
||||
onSuccess?: () => void
|
||||
@@ -13,14 +13,14 @@ export const SignupForm = (props: SignupFormProps) => {
|
||||
<div>
|
||||
<h1>Create an Account</h1>
|
||||
|
||||
<Form
|
||||
<Form<SignupInputType>
|
||||
submitText="Create Account"
|
||||
schema={SignupInput}
|
||||
initialValues={{ email: "", password: "" }}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await signup({ email: values.email, password: values.password })
|
||||
props.onSuccess?.()
|
||||
props.onSuccess && props.onSuccess()
|
||||
} catch (error) {
|
||||
if (error.code === "P2002" && error.meta?.target?.includes("email")) {
|
||||
// This error comes from Prisma
|
||||
|
||||
@@ -12,7 +12,7 @@ export default async function signup(
|
||||
|
||||
const hashedPassword = await hashPassword(password)
|
||||
const user = await db.user.create({
|
||||
data: { email: email.toLowerCase(), hashedPassword, role: "user" },
|
||||
data: { email, hashedPassword, role: "user" },
|
||||
select: { id: true, name: true, email: true, role: true },
|
||||
})
|
||||
|
||||
|
||||
@@ -6,17 +6,15 @@ type LayoutProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const Layout = ({ title, children }: LayoutProps) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title || "__name__"}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
const Layout = ({ title, children }: LayoutProps) => (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title || "__name__"}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
|
||||
export default Layout
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { AppProps, ErrorComponent, useRouter } from "blitz"
|
||||
import { AppProps, ErrorComponent } from "blitz"
|
||||
import { ErrorBoundary, FallbackProps } from "react-error-boundary"
|
||||
import { queryCache } from "react-query"
|
||||
import LoginForm from "app/auth/components/LoginForm"
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const getLayout = Component.getLayout || ((page) => page)
|
||||
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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
provider = ["sqlite", "postgres"]
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// import db from "./index"
|
||||
|
||||
/*
|
||||
* This seed function is executed when you run `blitz db seed`.
|
||||
*
|
||||
* Probably you want to use a library like https://chancejs.com
|
||||
* or https://github.com/Marak/Faker.js to easily generate
|
||||
* realistic data.
|
||||
*/
|
||||
const seed = async () => {
|
||||
// for (let i = 0; i < 5; i++) {
|
||||
// await db.project.create({ data: { name: "Project " + i } })
|
||||
// }
|
||||
}
|
||||
|
||||
export default seed
|
||||
@@ -12,17 +12,14 @@
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
"prisma": {
|
||||
"schema": "db/schema.prisma"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"printWidth": 100
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "tsc && lint-staged && pretty-quick --staged",
|
||||
"pre-push": "yarn lint && yarn test"
|
||||
"pre-commit": "lint-staged && pretty-quick --staged",
|
||||
"pre-push": "blitz test"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
@@ -42,13 +39,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "5.x",
|
||||
"@testing-library/react": "11.x",
|
||||
"@testing-library/react": "10.x",
|
||||
"@testing-library/react-hooks": "3.x",
|
||||
"@types/jest": "26.x",
|
||||
"@types/react": "16.x",
|
||||
"@types/secure-password": "3.x",
|
||||
"@typescript-eslint/eslint-plugin": "4.x",
|
||||
"@typescript-eslint/parser": "4.x",
|
||||
"@typescript-eslint/eslint-plugin": "3.x",
|
||||
"@typescript-eslint/parser": "3.x",
|
||||
"babel-eslint": "10.x",
|
||||
"eslint": "7.x",
|
||||
"eslint-config-react-app": "5.x",
|
||||
@@ -64,8 +61,8 @@
|
||||
"react-test-renderer": "16.x",
|
||||
"lint-staged": "10.x",
|
||||
"prettier": "2.x",
|
||||
"pretty-quick": "3.x",
|
||||
"typescript": "4.x",
|
||||
"pretty-quick": "2.x",
|
||||
"typescript": "3.x",
|
||||
"ts-jest": "26.x"
|
||||
},
|
||||
"private": true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user