Skip to content

Commit

Permalink
refactor: rewrite config schema in zod (#56383)
Browse files Browse the repository at this point in the history
The PR supersedes the #53150, which is way too outdated, has way too many conflicts, and also heavily relies on GitHub Copilot (which makes the progress slow and tedious).

The PR uses [`json-schema-to-zod`](https://github.com/StefanTerdell/json-schema-to-zod) (instead of the GitHub Copilot) to generate the zod schema, and manually replaces all generated `z.customRefine` with my hand-written zod schema.

TODO:

- [x] Convert schema
- [x] Reduce `z.any()` usage
- [x] Create human-readable errors from the `ZodError`
- [x] Update test cases to reflect the latest error message

-----

The benefit of using zod over ajv:

- Easier maintenance: zod schema is straightforward to compose.
- Better typescript support: config schema now strictly reflects the `NextConfig` type.
- Smaller installation size: by replacing `ajv` and `@segment/ajv-human-errors` w/ `zod`, I am able to reduce installation size by 114 KiB.
- Better Extension: the zod error message is easy to customize.

-----

In the previous PR #56083, @feedthejim replaces `zod` w/ `superstruct`. `superstruct` is lightweight and fast, which makes it perfect for creating simple schemas for RSC payload. But, this also means `superstruct` has its limitations compared to `zod`:

- `superstruct`'s syntax is different, and some utilities's usage is counter-intuitive:
  - `z.array(z.string()).gt(1)` vs `s.size(s.array(s.string()), 1)`
  - `z.numer().gt(1)` vs `s.size(s.number(), 1)`, `s.min(s.number(), 1)`
  - `z.boolean().optional().nullable()` vs `s.nullable(s.optional(z.boolean()))`
- `superstruct` has weaker TypeScript support and worse DX compared to `zod` when composing huge schema:
  - `zod.ZodType + z.object()` can provide a more detailed type mismatch message on which specific property is the culprit, while `Describe + s.object()` provides almost no information at all.
- `zod`'s schema is more powerful
  - `z.function()` supports `z.args()` and `z.returns()`, while `superstruct` only has `s.func()`
  - zod also has Promise type `z.promise()` and intersection type `z.and()`
- `superstruct`'s error is harder to parse compared to `zod`'s `ZodError`

So in the PR, I re-introduced `zod` for `next.config.js` validation.
  • Loading branch information
SukkaW authored and huozhi committed Oct 5, 2023
1 parent 7e1f311 commit 10b4ef9
Show file tree
Hide file tree
Showing 25 changed files with 836 additions and 1,034 deletions.
19 changes: 19 additions & 0 deletions packages/next-swc/crates/core/src/react_server_components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@ impl<C: Comments> ReactServerComponents<C> {
}

fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
// If the
if self.is_from_node_modules(&self.filepath) {
return;
}
for import in imports {
let source = import.source.0.clone();
if self.invalid_server_imports.contains(&source) {
Expand Down Expand Up @@ -391,6 +395,9 @@ impl<C: Comments> ReactServerComponents<C> {
}

fn assert_server_filename(&self, module: &Module) {
if self.is_from_node_modules(&self.filepath) {
return;
}
let is_error_file = Regex::new(r"[\\/]error\.(ts|js)x?$")
.unwrap()
.is_match(&self.filepath);
Expand All @@ -416,6 +423,9 @@ impl<C: Comments> ReactServerComponents<C> {
}

fn assert_client_graph(&self, imports: &[ModuleImports]) {
if self.is_from_node_modules(&self.filepath) {
return;
}
for import in imports {
let source = import.source.0.clone();
if self.invalid_client_imports.contains(&source) {
Expand All @@ -432,6 +442,9 @@ impl<C: Comments> ReactServerComponents<C> {
}

fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
if self.is_from_node_modules(&self.filepath) {
return;
}
let is_layout_or_page = Regex::new(r"[\\/](page|layout)\.(ts|js)x?$")
.unwrap()
.is_match(&self.filepath);
Expand Down Expand Up @@ -562,6 +575,12 @@ impl<C: Comments> ReactServerComponents<C> {
},
);
}

fn is_from_node_modules(&self, filepath: &str) -> bool {
Regex::new(r"[\\/]node_modules[\\/]")
.unwrap()
.is_match(filepath)
}
}

pub fn server_components<C: Comments>(
Expand Down
5 changes: 2 additions & 3 deletions packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@
"@next/swc": "13.5.5-canary.2",
"@opentelemetry/api": "1.4.1",
"@playwright/test": "^1.35.1",
"@segment/ajv-human-errors": "2.1.2",
"@taskr/clear": "1.1.0",
"@taskr/esnext": "1.1.0",
"@types/amphtml-validator": "1.0.0",
Expand Down Expand Up @@ -194,7 +193,6 @@
"@vercel/nft": "0.22.6",
"@vercel/turbopack-ecmascript-runtime": "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-231002.1",
"acorn": "8.5.0",
"ajv": "8.11.0",
"amphtml-validator": "1.0.35",
"anser": "1.4.9",
"arg": "4.1.0",
Expand Down Expand Up @@ -314,7 +312,8 @@
"webpack": "5.86.0",
"webpack-sources1": "npm:webpack-sources@1.4.3",
"webpack-sources3": "npm:webpack-sources@3.2.3",
"ws": "8.2.3"
"ws": "8.2.3",
"zod": "3.22.3"
},
"engines": {
"node": ">=16.14.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/compiled/sass-loader/cjs.js

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions packages/next/src/compiled/zod/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Colin McDonnell

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
1 change: 1 addition & 0 deletions packages/next/src/compiled/zod/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/next/src/compiled/zod/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"zod","main":"index.js","author":"Colin McDonnell <colin@colinhacks.com>","license":"MIT"}
Loading

0 comments on commit 10b4ef9

Please sign in to comment.