perf: replace zod with superstruct (#56083)
This PR replaces the usage of Zod in the App Router in favour of a smaller validation library. We don't use much of zod's more advanced capabilities so it doesn't make sense to import all of it on the server at the moment. Also added some unit tests. This results in a 44kb win, which will impact cold boot Before: <img width="594" alt="CleanShot 2023-09-27 at 13 34 09@2x" src="https://github.com/vercel/next.js/assets/11064311/43cc7687-947d-40a1-9e1e-c2a60caf53c0"> After: <img width="564" alt="CleanShot 2023-09-27 at 13 34 48@2x" src="https://github.com/vercel/next.js/assets/11064311/c3d3f5d6-e1f6-4969-bd11-dcd191d34ce6"> <!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide ### Adding or Updating Examples - The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md - Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # -->
This commit is contained in:
parent
3b0ee4e5f8
commit
c2fd08063c
16 changed files with 166 additions and 87 deletions
1
packages/next/index.d.ts
vendored
1
packages/next/index.d.ts
vendored
|
@ -1,4 +1,5 @@
|
|||
/// <reference types="./types/global" />
|
||||
/// <reference types="./types/compiled" />
|
||||
/// <reference path="./dist/styled-jsx/types/global.d.ts" />
|
||||
/// <reference path="./amp.d.ts" />
|
||||
/// <reference path="./app.d.ts" />
|
||||
|
|
|
@ -96,8 +96,7 @@
|
|||
"caniuse-lite": "^1.0.30001406",
|
||||
"postcss": "8.4.14",
|
||||
"styled-jsx": "5.1.1",
|
||||
"watchpack": "2.4.0",
|
||||
"zod": "3.21.4"
|
||||
"watchpack": "2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
|
@ -296,6 +295,7 @@
|
|||
"string-hash": "1.1.3",
|
||||
"string_decoder": "1.3.0",
|
||||
"strip-ansi": "6.0.0",
|
||||
"superstruct": "1.0.3",
|
||||
"tar": "6.1.15",
|
||||
"taskr": "1.1.0",
|
||||
"terser": "5.14.1",
|
||||
|
|
File diff suppressed because one or more lines are too long
1
packages/next/src/compiled/superstruct/index.cjs
Normal file
1
packages/next/src/compiled/superstruct/index.cjs
Normal file
File diff suppressed because one or more lines are too long
1
packages/next/src/compiled/superstruct/package.json
Normal file
1
packages/next/src/compiled/superstruct/package.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"name":"superstruct","main":"index.cjs","license":"MIT"}
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
{"name":"zod","main":"index.js","author":"Colin McDonnell <colin@colinhacks.com>","license":"MIT"}
|
|
@ -1,5 +1,6 @@
|
|||
import { FlightRouterState } from './types'
|
||||
import { flightRouterStateSchema } from './types'
|
||||
import { assert } from 'next/dist/compiled/superstruct'
|
||||
|
||||
export function parseAndValidateFlightRouterState(
|
||||
stateHeader: string | string[] | undefined
|
||||
|
@ -23,9 +24,9 @@ export function parseAndValidateFlightRouterState(
|
|||
}
|
||||
|
||||
try {
|
||||
return flightRouterStateSchema.parse(
|
||||
JSON.parse(decodeURIComponent(stateHeader))
|
||||
)
|
||||
const state = JSON.parse(decodeURIComponent(stateHeader))
|
||||
assert(state, flightRouterStateSchema)
|
||||
return state
|
||||
} catch {
|
||||
throw new Error('The router state header was sent but could not be parsed.')
|
||||
}
|
||||
|
|
72
packages/next/src/server/app-render/types.test.ts
Normal file
72
packages/next/src/server/app-render/types.test.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { flightRouterStateSchema } from './types'
|
||||
import { assert } from 'next/dist/compiled/superstruct'
|
||||
|
||||
const validFixtures = [
|
||||
[
|
||||
['a', 'b', 'c'],
|
||||
{
|
||||
a: [['a', 'b', 'c'], {}],
|
||||
b: [['a', 'b', 'c'], {}],
|
||||
},
|
||||
],
|
||||
[
|
||||
['a', 'b', 'c'],
|
||||
{
|
||||
a: [['a', 'b', 'c'], {}],
|
||||
b: [['a', 'b', 'c'], {}],
|
||||
},
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
],
|
||||
[
|
||||
['a', 'b', 'c'],
|
||||
{
|
||||
a: [['a', 'b', 'c'], {}],
|
||||
b: [['a', 'b', 'c'], {}],
|
||||
},
|
||||
null,
|
||||
'refetch',
|
||||
],
|
||||
]
|
||||
|
||||
const invalidFixtures = [
|
||||
// plain wrong
|
||||
['1', 'b', 'c'],
|
||||
// invalid enum
|
||||
[['a', 'b', 'foo'], {}],
|
||||
// invalid url
|
||||
[
|
||||
['a', 'b', 'c'],
|
||||
{
|
||||
a: [['a', 'b', 'c'], {}],
|
||||
b: [['a', 'b', 'c'], {}],
|
||||
},
|
||||
{
|
||||
invalid: 'invalid',
|
||||
},
|
||||
],
|
||||
// invalid isRootLayout
|
||||
[
|
||||
['a', 'b', 'c'],
|
||||
{
|
||||
a: [['a', 'b', 'c'], {}],
|
||||
b: [['a', 'b', 'c'], {}],
|
||||
},
|
||||
null,
|
||||
1,
|
||||
],
|
||||
]
|
||||
|
||||
describe('flightRouterStateSchema', () => {
|
||||
it('should validate a correct flight router state', () => {
|
||||
for (const state of validFixtures) {
|
||||
expect(() => assert(state, flightRouterStateSchema)).not.toThrow()
|
||||
}
|
||||
})
|
||||
it('should not validate an incorrect flight router state', () => {
|
||||
for (const state of invalidFixtures) {
|
||||
expect(() => assert(state, flightRouterStateSchema)).toThrow()
|
||||
}
|
||||
})
|
||||
})
|
|
@ -5,55 +5,35 @@ import type { ClientReferenceManifest } from '../../build/webpack/plugins/flight
|
|||
import type { NextFontManifest } from '../../build/webpack/plugins/next-font-manifest-plugin'
|
||||
import type { ParsedUrlQuery } from 'querystring'
|
||||
|
||||
import zod from 'zod'
|
||||
import s from 'next/dist/compiled/superstruct'
|
||||
|
||||
export type DynamicParamTypes = 'catchall' | 'optional-catchall' | 'dynamic'
|
||||
|
||||
const dynamicParamTypesSchema = zod.enum(['c', 'oc', 'd'])
|
||||
/**
|
||||
* c = catchall
|
||||
* oc = optional catchall
|
||||
* d = dynamic
|
||||
*/
|
||||
export type DynamicParamTypesShort = zod.infer<typeof dynamicParamTypesSchema>
|
||||
const dynamicParamTypesSchema = s.enums(['c', 'oc', 'd'])
|
||||
|
||||
const segmentSchema = zod.union([
|
||||
zod.string(),
|
||||
zod.tuple([zod.string(), zod.string(), dynamicParamTypesSchema]),
|
||||
export type DynamicParamTypesShort = s.Infer<typeof dynamicParamTypesSchema>
|
||||
|
||||
const segmentSchema = s.union([
|
||||
s.string(),
|
||||
s.tuple([s.string(), s.string(), dynamicParamTypesSchema]),
|
||||
])
|
||||
/**
|
||||
* Segment in the router state.
|
||||
*/
|
||||
export type Segment = zod.infer<typeof segmentSchema>
|
||||
|
||||
export const flightRouterStateSchema: zod.ZodType<FlightRouterState> = zod.lazy(
|
||||
() => {
|
||||
const parallelRoutesSchema = zod.record(flightRouterStateSchema)
|
||||
const urlSchema = zod.string().nullable().optional()
|
||||
const refreshSchema = zod.literal('refetch').nullable().optional()
|
||||
const isRootLayoutSchema = zod.boolean().optional()
|
||||
export type Segment = s.Infer<typeof segmentSchema>
|
||||
|
||||
// unfortunately the tuple is not understood well by Describe so we have to
|
||||
// use any here. This does not have any impact on the runtime type since the validation
|
||||
// does work correctly.
|
||||
export const flightRouterStateSchema: s.Describe<any> = s.tuple([
|
||||
segmentSchema,
|
||||
s.record(
|
||||
s.string(),
|
||||
s.lazy(() => flightRouterStateSchema)
|
||||
),
|
||||
s.optional(s.nullable(s.string())),
|
||||
s.optional(s.nullable(s.literal('refetch'))),
|
||||
s.optional(s.boolean()),
|
||||
])
|
||||
|
||||
// Due to the lack of optional tuple types in Zod, we need to use union here.
|
||||
// https://github.com/colinhacks/zod/issues/1465
|
||||
return zod.union([
|
||||
zod.tuple([
|
||||
segmentSchema,
|
||||
parallelRoutesSchema,
|
||||
urlSchema,
|
||||
refreshSchema,
|
||||
isRootLayoutSchema,
|
||||
]),
|
||||
zod.tuple([
|
||||
segmentSchema,
|
||||
parallelRoutesSchema,
|
||||
urlSchema,
|
||||
refreshSchema,
|
||||
]),
|
||||
zod.tuple([segmentSchema, parallelRoutesSchema, urlSchema]),
|
||||
zod.tuple([segmentSchema, parallelRoutesSchema]),
|
||||
])
|
||||
}
|
||||
)
|
||||
/**
|
||||
* Router state
|
||||
*/
|
||||
|
|
|
@ -1982,6 +1982,16 @@ export async function ncc_unistore(task, opts) {
|
|||
.ncc({ packageName: 'unistore', externals })
|
||||
.target('src/compiled/unistore')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
externals['unistore'] = 'next/dist/compiled/superstruct'
|
||||
export async function ncc_superstruct(task, opts) {
|
||||
await task
|
||||
.source(relative(__dirname, require.resolve('superstruct')))
|
||||
.ncc({ packageName: 'superstruct', externals })
|
||||
.target('src/compiled/superstruct')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
externals['web-vitals'] = 'next/dist/compiled/web-vitals'
|
||||
export async function ncc_web_vitals(task, opts) {
|
||||
|
@ -2304,6 +2314,7 @@ export async function ncc(task, opts) {
|
|||
'ncc_source_map',
|
||||
'ncc_string_hash',
|
||||
'ncc_strip_ansi',
|
||||
'ncc_superstruct',
|
||||
'ncc_nft',
|
||||
'ncc_tar',
|
||||
'ncc_terser',
|
||||
|
|
44
packages/next/types/compiled.d.ts
vendored
44
packages/next/types/compiled.d.ts
vendored
|
@ -8,14 +8,38 @@ declare module 'next/dist/compiled/webpack/webpack' {
|
|||
export let GraphHelpers: any
|
||||
export let sources: any
|
||||
export let StringXor: any
|
||||
export const webpack: any
|
||||
export const Compiler: any
|
||||
export const Compilation: any
|
||||
export const Module: any
|
||||
export const Stats: any
|
||||
export const Template: any
|
||||
export const RuntimeModule: any
|
||||
export const RuntimeGlobals: any
|
||||
export const NormalModule: any
|
||||
export const ResolvePluginInstance: any
|
||||
namespace webpack {
|
||||
export type Compiler = any
|
||||
export type WebpackPluginInstance = any
|
||||
export type Compilation = any
|
||||
export type Module = any
|
||||
export type Stats = any
|
||||
export type Template = any
|
||||
export type RuntimeModule = any
|
||||
export type RuntimeGlobals = any
|
||||
export type NormalModule = any
|
||||
export type ResolvePluginInstance = any
|
||||
export type Configuration = any
|
||||
export type ResolveOptions = any
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export type LoaderContext<T> = any
|
||||
export type RuleSetUseItem = any
|
||||
export type EntryObject = any
|
||||
export type Chunk = any
|
||||
export type ChunkGroup = any
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
namespace sources {
|
||||
export type RawSource = any
|
||||
}
|
||||
}
|
||||
export var webpack: any
|
||||
}
|
||||
|
||||
declare module 'next/dist/compiled/superstruct' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export type Struct<T, S> = any
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export type Infer<T = any> = any
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export type Describe<T> = any
|
||||
}
|
||||
|
|
5
packages/next/types/misc.d.ts
vendored
5
packages/next/types/misc.d.ts
vendored
|
@ -105,6 +105,11 @@ declare module 'next/dist/compiled/amphtml-validator' {
|
|||
import m from 'amphtml-validator'
|
||||
export = m
|
||||
}
|
||||
|
||||
declare module 'next/dist/compiled/superstruct' {
|
||||
import m from 'superstruct'
|
||||
export = m
|
||||
}
|
||||
declare module 'next/dist/compiled/async-retry'
|
||||
declare module 'next/dist/compiled/async-sema' {
|
||||
import m from 'async-sema'
|
||||
|
|
|
@ -816,9 +816,6 @@ importers:
|
|||
watchpack:
|
||||
specifier: 2.4.0
|
||||
version: 2.4.0
|
||||
zod:
|
||||
specifier: 3.21.4
|
||||
version: 3.21.4
|
||||
devDependencies:
|
||||
'@ampproject/toolbox-optimizer':
|
||||
specifier: 2.8.3
|
||||
|
@ -1320,7 +1317,7 @@ importers:
|
|||
version: 0.13.4
|
||||
sass-loader:
|
||||
specifier: 12.4.0
|
||||
version: 12.4.0(webpack@5.86.0)
|
||||
version: 12.4.0(sass@1.54.0)(webpack@5.86.0)
|
||||
schema-utils2:
|
||||
specifier: npm:schema-utils@2.7.1
|
||||
version: /schema-utils@2.7.1
|
||||
|
@ -1366,6 +1363,9 @@ importers:
|
|||
strip-ansi:
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0
|
||||
superstruct:
|
||||
specifier: 1.0.3
|
||||
version: 1.0.3
|
||||
tar:
|
||||
specifier: 6.1.15
|
||||
version: 6.1.15
|
||||
|
@ -23711,7 +23711,7 @@ packages:
|
|||
/safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
/sass-loader@12.4.0(webpack@5.86.0):
|
||||
/sass-loader@12.4.0(sass@1.54.0)(webpack@5.86.0):
|
||||
resolution: {integrity: sha512-7xN+8khDIzym1oL9XyS6zP6Ges+Bo2B2xbPrjdMHEYyV3AQYhd/wXeru++3ODHF0zMjYmVadblSKrPrjEkL8mg==}
|
||||
engines: {node: '>= 12.13.0'}
|
||||
peerDependencies:
|
||||
|
@ -23729,6 +23729,7 @@ packages:
|
|||
dependencies:
|
||||
klona: 2.0.4
|
||||
neo-async: 2.6.2
|
||||
sass: 1.54.0
|
||||
webpack: 5.86.0(@swc/core@1.3.85)
|
||||
dev: true
|
||||
|
||||
|
@ -24860,6 +24861,11 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/superstruct@1.0.3:
|
||||
resolution: {integrity: sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dev: true
|
||||
|
||||
/supports-color@2.0.0:
|
||||
resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
"test/**/*.test.ts",
|
||||
"test/**/*.test.tsx",
|
||||
"test/**/test/*",
|
||||
"packages/next/types/webpack.d.ts",
|
||||
"packages/next/types/compiled.d.ts",
|
||||
"test/e2e/pages-dir/client-navigation/index.test.js",
|
||||
"test/e2e/pages-dir/client-navigation/rendering.js"
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue