Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Verify GS(S)P Serializability (#10857)
* Verify GS(S)P Serializability * Add support for cyclic refs * Add unit tests * Test for error in development mode * Fix prerender preview tests * Fix gssp preview tests * fix 2x test cases * Add desired test * fix some more tests * Fix route manifest expect * Fix test expects * Test that `getServerSideProps` does not error in production * Test that getStaticProps is not checked in production * Test serialization check during build * Fix export detection for serverless * Update test/unit/is-serializable-props.test.js Co-Authored-By: JJ Kasper <jj@jjsweb.site> Co-authored-by: JJ Kasper <jj@jjsweb.site>
- Loading branch information
Showing
16 changed files
with
549 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
const regexpPlainIdentifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/ | ||
|
||
function isPlainObject(value: any): boolean { | ||
if (Object.prototype.toString.call(value) !== '[object Object]') { | ||
return false | ||
} | ||
|
||
const prototype = Object.getPrototypeOf(value) | ||
return prototype === null || prototype === Object.prototype | ||
} | ||
|
||
export function isSerializableProps( | ||
page: string, | ||
method: string, | ||
input: any | ||
): true { | ||
if (!isPlainObject(input)) { | ||
throw new SerializableError( | ||
page, | ||
method, | ||
'', | ||
`Props must be returned as a plain object from ${method}: \`{ props: { ... } }\`.` | ||
) | ||
} | ||
|
||
const visited = new WeakSet() | ||
|
||
function visit(value: any, path: string) { | ||
if (visited.has(value)) { | ||
throw new SerializableError( | ||
page, | ||
method, | ||
path, | ||
'Circular references cannot be expressed in JSON.' | ||
) | ||
} | ||
|
||
visited.add(value) | ||
} | ||
|
||
function isSerializable(value: any, path: string): true { | ||
const type = typeof value | ||
if ( | ||
// `null` can be serialized, but not `undefined`. | ||
value === null || | ||
// n.b. `bigint`, `function`, `symbol`, and `undefined` cannot be | ||
// serialized. | ||
// | ||
// `object` is special-cased below, as it may represent `null`, an Array, | ||
// a plain object, a class, et al. | ||
type === 'boolean' || | ||
type === 'number' || | ||
type === 'string' | ||
) { | ||
return true | ||
} | ||
|
||
if (type === 'undefined') { | ||
throw new SerializableError( | ||
page, | ||
method, | ||
path, | ||
'`undefined` cannot be serialized as JSON. Please use `null` or omit this value all together.' | ||
) | ||
} | ||
|
||
if (isPlainObject(value)) { | ||
visit(value, path) | ||
|
||
if ( | ||
Object.entries(value).every(([key, value]) => { | ||
const nextPath = regexpPlainIdentifier.test(key) | ||
? `${path}.${key}` | ||
: `${path}[${JSON.stringify(key)}]` | ||
|
||
return ( | ||
isSerializable(key, nextPath) && isSerializable(value, nextPath) | ||
) | ||
}) | ||
) { | ||
return true | ||
} | ||
|
||
throw new SerializableError( | ||
page, | ||
method, | ||
path, | ||
`invariant: Unknown error encountered in Object.` | ||
) | ||
} | ||
|
||
if (Array.isArray(value)) { | ||
visit(value, path) | ||
|
||
if ( | ||
value.every((value, index) => | ||
isSerializable(value, `${path}[${index}]`) | ||
) | ||
) { | ||
return true | ||
} | ||
|
||
throw new SerializableError( | ||
page, | ||
method, | ||
path, | ||
`invariant: Unknown error encountered in Array.` | ||
) | ||
} | ||
|
||
// None of these can be expressed as JSON: | ||
// const type: "bigint" | "symbol" | "object" | "function" | ||
throw new SerializableError( | ||
page, | ||
method, | ||
path, | ||
'`' + | ||
type + | ||
'`' + | ||
(type === 'object' | ||
? ` ("${Object.prototype.toString.call(value)}")` | ||
: '') + | ||
' cannot be serialized as JSON. Please only return JSON serializable data types.' | ||
) | ||
} | ||
|
||
return isSerializable(input, '') | ||
} | ||
|
||
export class SerializableError extends Error { | ||
constructor(page: string, method: string, path: string, message: string) { | ||
super( | ||
path | ||
? `Error serializing \`${path}\` returned from \`${method}\` in "${page}".\nReason: ${message}` | ||
: `Error serializing props returned from \`${method}\` in "${page}".\nReason: ${message}` | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export async function getServerSideProps() { | ||
return { | ||
props: { time: new Date() }, | ||
} | ||
} | ||
|
||
const Page = ({ time }) => { | ||
return <p>hello {time.toString()}</p> | ||
} | ||
|
||
export default Page |
Oops, something went wrong.