rsnext/packages/next/lib/is-serializable-props.ts
Pieter Bogaerts 662fa6362a
fix: fixes #33314 move is-plain-object for es5 compilation (#33690)
## Bug

- [ ] Related issues linked using `fixes #33314` #33314
- [ ] Moved the `is-plain-object` file to the shared directory since it's emitted to the client and thus needs to be transpiled.

This is just my 2nd PR so if I'm missing something please let me know.
2022-01-27 17:59:42 +00:00

143 lines
3.5 KiB
TypeScript

import {
isPlainObject,
getObjectClassLabel,
} from '../shared/lib/is-plain-object'
const regexpPlainIdentifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/
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: { ... } }\` (received: \`${getObjectClassLabel(
input
)}\`).`
)
}
function visit(visited: Map<any, string>, value: any, path: string) {
if (visited.has(value)) {
throw new SerializableError(
page,
method,
path,
`Circular references cannot be expressed in JSON (references: \`${
visited.get(value) || '(self)'
}\`).`
)
}
visited.set(value, path)
}
function isSerializable(
refs: Map<any, string>,
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.'
)
}
if (isPlainObject(value)) {
visit(refs, value, path)
if (
Object.entries(value).every(([key, nestedValue]) => {
const nextPath = regexpPlainIdentifier.test(key)
? `${path}.${key}`
: `${path}[${JSON.stringify(key)}]`
const newRefs = new Map(refs)
return (
isSerializable(newRefs, key, nextPath) &&
isSerializable(newRefs, nestedValue, nextPath)
)
})
) {
return true
}
throw new SerializableError(
page,
method,
path,
`invariant: Unknown error encountered in Object.`
)
}
if (Array.isArray(value)) {
visit(refs, value, path)
if (
value.every((nestedValue, index) => {
const newRefs = new Map(refs)
return isSerializable(newRefs, nestedValue, `${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(new Map(), 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}`
)
}
}