Skip to content

Commit

Permalink
replace zod with superstruct
Browse files Browse the repository at this point in the history
  • Loading branch information
feedthejim committed Sep 27, 2023
1 parent 9f8b4d8 commit fcf912b
Show file tree
Hide file tree
Showing 12 changed files with 131 additions and 80 deletions.
4 changes: 2 additions & 2 deletions packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -297,6 +296,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",
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/compiled/superstruct/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(()=>{var e={318:function(e,t){(function(e,n){true?n(t):0})(this,(function(e){"use strict";class StructError extends TypeError{constructor(e,t){let n;const{message:r,explanation:i,...c}=e;const{path:o}=e;const a=o.length===0?r:`At path: ${o.join(".")} -- ${r}`;super(i??a);if(i!=null)this.cause=a;Object.assign(this,c);this.name=this.constructor.name;this.failures=()=>n??(n=[e,...t()])}}function isIterable(e){return isObject(e)&&typeof e[Symbol.iterator]==="function"}function isObject(e){return typeof e==="object"&&e!=null}function isPlainObject(e){if(Object.prototype.toString.call(e)!=="[object Object]"){return false}const t=Object.getPrototypeOf(e);return t===null||t===Object.prototype}function print(e){if(typeof e==="symbol"){return e.toString()}return typeof e==="string"?JSON.stringify(e):`${e}`}function shiftIterator(e){const{done:t,value:n}=e.next();return t?undefined:n}function toFailure(e,t,n,r){if(e===true){return}else if(e===false){e={}}else if(typeof e==="string"){e={message:e}}const{path:i,branch:c}=t;const{type:o}=n;const{refinement:a,message:s=`Expected a value of type \`${o}\`${a?` with refinement \`${a}\``:""}, but received: \`${print(r)}\``}=e;return{value:r,type:o,refinement:a,key:i[i.length-1],path:i,branch:c,...e,message:s}}function*toFailures(e,t,n,r){if(!isIterable(e)){e=[e]}for(const i of e){const e=toFailure(i,t,n,r);if(e){yield e}}}function*run(e,t,n={}){const{path:r=[],branch:i=[e],coerce:c=false,mask:o=false}=n;const a={path:r,branch:i};if(c){e=t.coercer(e,a);if(o&&t.type!=="type"&&isObject(t.schema)&&isObject(e)&&!Array.isArray(e)){for(const n in e){if(t.schema[n]===undefined){delete e[n]}}}}let s="valid";for(const r of t.validator(e,a)){r.explanation=n.message;s="not_valid";yield[r,undefined]}for(let[u,f,l]of t.entries(e,a)){const t=run(f,l,{path:u===undefined?r:[...r,u],branch:u===undefined?i:[...i,f],coerce:c,mask:o,message:n.message});for(const n of t){if(n[0]){s=n[0].refinement!=null?"not_refined":"not_valid";yield[n[0],undefined]}else if(c){f=n[1];if(u===undefined){e=f}else if(e instanceof Map){e.set(u,f)}else if(e instanceof Set){e.add(f)}else if(isObject(e)){if(f!==undefined||u in e)e[u]=f}}}}if(s!=="not_valid"){for(const r of t.refiner(e,a)){r.explanation=n.message;s="not_refined";yield[r,undefined]}}if(s==="valid"){yield[undefined,e]}}class Struct{constructor(e){const{type:t,schema:n,validator:r,refiner:i,coercer:c=(e=>e),entries:o=function*(){}}=e;this.type=t;this.schema=n;this.entries=o;this.coercer=c;if(r){this.validator=(e,t)=>{const n=r(e,t);return toFailures(n,t,this,e)}}else{this.validator=()=>[]}if(i){this.refiner=(e,t)=>{const n=i(e,t);return toFailures(n,t,this,e)}}else{this.refiner=()=>[]}}assert(e,t){return assert(e,this,t)}create(e,t){return create(e,this,t)}is(e){return is(e,this)}mask(e,t){return mask(e,this,t)}validate(e,t={}){return validate(e,this,t)}}function assert(e,t,n){const r=validate(e,t,{message:n});if(r[0]){throw r[0]}}function create(e,t,n){const r=validate(e,t,{coerce:true,message:n});if(r[0]){throw r[0]}else{return r[1]}}function mask(e,t,n){const r=validate(e,t,{coerce:true,mask:true,message:n});if(r[0]){throw r[0]}else{return r[1]}}function is(e,t){const n=validate(e,t);return!n[0]}function validate(e,t,n={}){const r=run(e,t,n);const i=shiftIterator(r);if(i[0]){const e=new StructError(i[0],(function*(){for(const e of r){if(e[0]){yield e[0]}}}));return[e,undefined]}else{const e=i[1];return[undefined,e]}}function assign(...e){const t=e[0].type==="type";const n=e.map((e=>e.schema));const r=Object.assign({},...n);return t?type(r):object(r)}function define(e,t){return new Struct({type:e,schema:null,validator:t})}function deprecated(e,t){return new Struct({...e,refiner:(t,n)=>t===undefined||e.refiner(t,n),validator(n,r){if(n===undefined){return true}else{t(n,r);return e.validator(n,r)}}})}function dynamic(e){return new Struct({type:"dynamic",schema:null,*entries(t,n){const r=e(t,n);yield*r.entries(t,n)},validator(t,n){const r=e(t,n);return r.validator(t,n)},coercer(t,n){const r=e(t,n);return r.coercer(t,n)},refiner(t,n){const r=e(t,n);return r.refiner(t,n)}})}function lazy(e){let t;return new Struct({type:"lazy",schema:null,*entries(n,r){t??(t=e());yield*t.entries(n,r)},validator(n,r){t??(t=e());return t.validator(n,r)},coercer(n,r){t??(t=e());return t.coercer(n,r)},refiner(n,r){t??(t=e());return t.refiner(n,r)}})}function omit(e,t){const{schema:n}=e;const r={...n};for(const e of t){delete r[e]}switch(e.type){case"type":return type(r);default:return object(r)}}function partial(e){const t=e instanceof Struct?{...e.schema}:{...e};for(const e in t){t[e]=optional(t[e])}return object(t)}function pick(e,t){const{schema:n}=e;const r={};for(const e of t){r[e]=n[e]}return object(r)}function struct(e,t){console.warn("superstruct@0.11 - The `struct` helper has been renamed to `define`.");return define(e,t)}function any(){return define("any",(()=>true))}function array(e){return new Struct({type:"array",schema:e,*entries(t){if(e&&Array.isArray(t)){for(const[n,r]of t.entries()){yield[n,r,e]}}},coercer(e){return Array.isArray(e)?e.slice():e},validator(e){return Array.isArray(e)||`Expected an array value, but received: ${print(e)}`}})}function bigint(){return define("bigint",(e=>typeof e==="bigint"))}function boolean(){return define("boolean",(e=>typeof e==="boolean"))}function date(){return define("date",(e=>e instanceof Date&&!isNaN(e.getTime())||`Expected a valid \`Date\` object, but received: ${print(e)}`))}function enums(e){const t={};const n=e.map((e=>print(e))).join();for(const n of e){t[n]=n}return new Struct({type:"enums",schema:t,validator(t){return e.includes(t)||`Expected one of \`${n}\`, but received: ${print(t)}`}})}function func(){return define("func",(e=>typeof e==="function"||`Expected a function, but received: ${print(e)}`))}function instance(e){return define("instance",(t=>t instanceof e||`Expected a \`${e.name}\` instance, but received: ${print(t)}`))}function integer(){return define("integer",(e=>typeof e==="number"&&!isNaN(e)&&Number.isInteger(e)||`Expected an integer, but received: ${print(e)}`))}function intersection(e){return new Struct({type:"intersection",schema:null,*entries(t,n){for(const r of e){yield*r.entries(t,n)}},*validator(t,n){for(const r of e){yield*r.validator(t,n)}},*refiner(t,n){for(const r of e){yield*r.refiner(t,n)}}})}function literal(e){const t=print(e);const n=typeof e;return new Struct({type:"literal",schema:n==="string"||n==="number"||n==="boolean"?e:null,validator(n){return n===e||`Expected the literal \`${t}\`, but received: ${print(n)}`}})}function map(e,t){return new Struct({type:"map",schema:null,*entries(n){if(e&&t&&n instanceof Map){for(const[r,i]of n.entries()){yield[r,r,e];yield[r,i,t]}}},coercer(e){return e instanceof Map?new Map(e):e},validator(e){return e instanceof Map||`Expected a \`Map\` object, but received: ${print(e)}`}})}function never(){return define("never",(()=>false))}function nullable(e){return new Struct({...e,validator:(t,n)=>t===null||e.validator(t,n),refiner:(t,n)=>t===null||e.refiner(t,n)})}function number(){return define("number",(e=>typeof e==="number"&&!isNaN(e)||`Expected a number, but received: ${print(e)}`))}function object(e){const t=e?Object.keys(e):[];const n=never();return new Struct({type:"object",schema:e?e:null,*entries(r){if(e&&isObject(r)){const i=new Set(Object.keys(r));for(const n of t){i.delete(n);yield[n,r[n],e[n]]}for(const e of i){yield[e,r[e],n]}}},validator(e){return isObject(e)||`Expected an object, but received: ${print(e)}`},coercer(e){return isObject(e)?{...e}:e}})}function optional(e){return new Struct({...e,validator:(t,n)=>t===undefined||e.validator(t,n),refiner:(t,n)=>t===undefined||e.refiner(t,n)})}function record(e,t){return new Struct({type:"record",schema:null,*entries(n){if(isObject(n)){for(const r in n){const i=n[r];yield[r,r,e];yield[r,i,t]}}},validator(e){return isObject(e)||`Expected an object, but received: ${print(e)}`}})}function regexp(){return define("regexp",(e=>e instanceof RegExp))}function set(e){return new Struct({type:"set",schema:null,*entries(t){if(e&&t instanceof Set){for(const n of t){yield[n,n,e]}}},coercer(e){return e instanceof Set?new Set(e):e},validator(e){return e instanceof Set||`Expected a \`Set\` object, but received: ${print(e)}`}})}function string(){return define("string",(e=>typeof e==="string"||`Expected a string, but received: ${print(e)}`))}function tuple(e){const t=never();return new Struct({type:"tuple",schema:null,*entries(n){if(Array.isArray(n)){const r=Math.max(e.length,n.length);for(let i=0;i<r;i++){yield[i,n[i],e[i]||t]}}},validator(e){return Array.isArray(e)||`Expected an array, but received: ${print(e)}`}})}function type(e){const t=Object.keys(e);return new Struct({type:"type",schema:e,*entries(n){if(isObject(n)){for(const r of t){yield[r,n[r],e[r]]}}},validator(e){return isObject(e)||`Expected an object, but received: ${print(e)}`},coercer(e){return isObject(e)?{...e}:e}})}function union(e){const t=e.map((e=>e.type)).join(" | ");return new Struct({type:"union",schema:null,coercer(t){for(const n of e){const[e,r]=n.validate(t,{coerce:true});if(!e){return r}}return t},validator(n,r){const i=[];for(const t of e){const[...e]=run(n,t,r);const[c]=e;if(!c[0]){return[]}else{for(const[t]of e){if(t){i.push(t)}}}}return[`Expected the value to satisfy a union of \`${t}\`, but received: ${print(n)}`,...i]}})}function unknown(){return define("unknown",(()=>true))}function coerce(e,t,n){return new Struct({...e,coercer:(r,i)=>is(r,t)?e.coercer(n(r,i),i):e.coercer(r,i)})}function defaulted(e,t,n={}){return coerce(e,unknown(),(e=>{const r=typeof t==="function"?t():t;if(e===undefined){return r}if(!n.strict&&isPlainObject(e)&&isPlainObject(r)){const t={...e};let n=false;for(const e in r){if(t[e]===undefined){t[e]=r[e];n=true}}if(n){return t}}return e}))}function trimmed(e){return coerce(e,string(),(e=>e.trim()))}function empty(e){return refine(e,"empty",(t=>{const n=getSize(t);return n===0||`Expected an empty ${e.type} but received one with a size of \`${n}\``}))}function getSize(e){if(e instanceof Map||e instanceof Set){return e.size}else{return e.length}}function max(e,t,n={}){const{exclusive:r}=n;return refine(e,"max",(n=>r?n<t:n<=t||`Expected a ${e.type} less than ${r?"":"or equal to "}${t} but received \`${n}\``))}function min(e,t,n={}){const{exclusive:r}=n;return refine(e,"min",(n=>r?n>t:n>=t||`Expected a ${e.type} greater than ${r?"":"or equal to "}${t} but received \`${n}\``))}function nonempty(e){return refine(e,"nonempty",(t=>{const n=getSize(t);return n>0||`Expected a nonempty ${e.type} but received an empty one`}))}function pattern(e,t){return refine(e,"pattern",(n=>t.test(n)||`Expected a ${e.type} matching \`/${t.source}/\` but received "${n}"`))}function size(e,t,n=t){const r=`Expected a ${e.type}`;const i=t===n?`of \`${t}\``:`between \`${t}\` and \`${n}\``;return refine(e,"size",(e=>{if(typeof e==="number"||e instanceof Date){return t<=e&&e<=n||`${r} ${i} but received \`${e}\``}else if(e instanceof Map||e instanceof Set){const{size:c}=e;return t<=c&&c<=n||`${r} with a size ${i} but received one with a size of \`${c}\``}else{const{length:c}=e;return t<=c&&c<=n||`${r} with a length ${i} but received one with a length of \`${c}\``}}))}function refine(e,t,n){return new Struct({...e,*refiner(r,i){yield*e.refiner(r,i);const c=n(r,i);const o=toFailures(c,i,e,r);for(const e of o){yield{...e,refinement:t}}}})}e.Struct=Struct;e.StructError=StructError;e.any=any;e.array=array;e.assert=assert;e.assign=assign;e.bigint=bigint;e.boolean=boolean;e.coerce=coerce;e.create=create;e.date=date;e.defaulted=defaulted;e.define=define;e.deprecated=deprecated;e.dynamic=dynamic;e.empty=empty;e.enums=enums;e.func=func;e.instance=instance;e.integer=integer;e.intersection=intersection;e.is=is;e.lazy=lazy;e.literal=literal;e.map=map;e.mask=mask;e.max=max;e.min=min;e.never=never;e.nonempty=nonempty;e.nullable=nullable;e.number=number;e.object=object;e.omit=omit;e.optional=optional;e.partial=partial;e.pattern=pattern;e.pick=pick;e.record=record;e.refine=refine;e.regexp=regexp;e.set=set;e.size=size;e.string=string;e.struct=struct;e.trimmed=trimmed;e.tuple=tuple;e.type=type;e.union=union;e.unknown=unknown;e.validate=validate}))}};if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";var t={};e[318](0,t);module.exports=t})();
1 change: 1 addition & 0 deletions packages/next/src/compiled/superstruct/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"superstruct","main":"index.cjs","license":"MIT"}
21 changes: 0 additions & 21 deletions packages/next/src/compiled/zod/LICENSE

This file was deleted.

1 change: 0 additions & 1 deletion packages/next/src/compiled/zod/index.js

This file was deleted.

1 change: 0 additions & 1 deletion packages/next/src/compiled/zod/package.json

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.')
}
Expand Down
72 changes: 72 additions & 0 deletions packages/next/src/server/app-render/types.test.ts
Original file line number Diff line number Diff line change
@@ -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()
}
})
})
68 changes: 24 additions & 44 deletions packages/next/src/server/app-render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'])

export type DynamicParamTypesShort = s.Infer<typeof dynamicParamTypesSchema>

const segmentSchema = zod.union([
zod.string(),
zod.tuple([zod.string(), zod.string(), 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()

// 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]),
])
}
)

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()),
])

/**
* Router state
*/
Expand Down
11 changes: 11 additions & 0 deletions packages/next/taskfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -1991,6 +1991,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) {
Expand Down Expand Up @@ -2314,6 +2324,7 @@ export async function ncc(task, opts) {
'ncc_source_map',
'ncc_string_hash',
'ncc_strip_ansi',
'ncc_superstruct',
'ncc_nft',
'ncc_tar',
'ncc_terser',
Expand Down
8 changes: 8 additions & 0 deletions packages/next/types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,11 @@ interface NextFetchRequestConfig {
interface RequestInit {
next?: NextFetchRequestConfig | undefined
}

// TOOD: remove this. Somehow putting this in compiler.d.ts doesn't work when skipLibCheck: false
// so we put it here instead
declare module 'next/dist/compiled/superstruct' {
// eslint-disable-next-line
import m from 'superstruct'
export = m
}
Loading

0 comments on commit fcf912b

Please sign in to comment.