diff --git a/packages/next/lib/is-serializable-props.ts b/packages/next/lib/is-serializable-props.ts index 6053b064be6d8ce..0a45620bcbff50b 100644 --- a/packages/next/lib/is-serializable-props.ts +++ b/packages/next/lib/is-serializable-props.ts @@ -23,22 +23,26 @@ export function isSerializableProps( ) } - const visited = new WeakSet() - - function visit(value: any, path: string) { + function visit(visited: Map, value: any, path: string) { if (visited.has(value)) { throw new SerializableError( page, method, path, - 'Circular references cannot be expressed in JSON.' + `Circular references cannot be expressed in JSON (references: \`${visited.get( + value + ) || '(self)'}\`).` ) } - visited.add(value) + visited.set(value, path) } - function isSerializable(value: any, path: string): true { + function isSerializable( + refs: Map, + value: any, + path: string + ): true { const type = typeof value if ( // `null` can be serialized, but not `undefined`. @@ -65,7 +69,7 @@ export function isSerializableProps( } if (isPlainObject(value)) { - visit(value, path) + visit(refs, value, path) if ( Object.entries(value).every(([key, value]) => { @@ -73,8 +77,10 @@ export function isSerializableProps( ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]` + const newRefs = new Map(refs) return ( - isSerializable(key, nextPath) && isSerializable(value, nextPath) + isSerializable(newRefs, key, nextPath) && + isSerializable(newRefs, value, nextPath) ) }) ) { @@ -90,11 +96,12 @@ export function isSerializableProps( } if (Array.isArray(value)) { - visit(value, path) + visit(refs, value, path) + const newRefs = new Map(refs) if ( value.every((value, index) => - isSerializable(value, `${path}[${index}]`) + isSerializable(newRefs, value, `${path}[${index}]`) ) ) { return true @@ -124,7 +131,7 @@ export function isSerializableProps( ) } - return isSerializable(input, '') + return isSerializable(new Map(), input, '') } export class SerializableError extends Error { diff --git a/test/unit/is-serializable-props.test.js b/test/unit/is-serializable-props.test.js index 55c1a325126cf17..451090000b52ea9 100644 --- a/test/unit/is-serializable-props.test.js +++ b/test/unit/is-serializable-props.test.js @@ -161,13 +161,13 @@ Reason: \`function\` cannot be serialized as JSON. Please only return JSON seria expect(() => isSerializableProps('/', 'test', obj)) .toThrowErrorMatchingInlineSnapshot(` "Error serializing \`.child\` returned from \`test\` in \\"/\\". -Reason: Circular references cannot be expressed in JSON." +Reason: Circular references cannot be expressed in JSON (references: \`(self)\`)." `) expect(() => isSerializableProps('/', 'test', { k: [obj] })) .toThrowErrorMatchingInlineSnapshot(` "Error serializing \`.k[0].child\` returned from \`test\` in \\"/\\". -Reason: Circular references cannot be expressed in JSON." +Reason: Circular references cannot be expressed in JSON (references: \`.k[0]\`)." `) }) @@ -178,13 +178,101 @@ Reason: Circular references cannot be expressed in JSON." expect(() => isSerializableProps('/', 'test', { arr })) .toThrowErrorMatchingInlineSnapshot(` "Error serializing \`.arr[2]\` returned from \`test\` in \\"/\\". -Reason: Circular references cannot be expressed in JSON." +Reason: Circular references cannot be expressed in JSON (references: \`.arr\`)." `) expect(() => isSerializableProps('/', 'test', { k: [{ arr }] })) .toThrowErrorMatchingInlineSnapshot(` "Error serializing \`.k[0].arr[2]\` returned from \`test\` in \\"/\\". -Reason: Circular references cannot be expressed in JSON." +Reason: Circular references cannot be expressed in JSON (references: \`.k[0].arr\`)." `) }) + + it('can handle deep obj circular refs', () => { + const obj = { foo: 'bar', test: true, leve1: { level2: {} } } + obj.leve1.level2.child = obj + + expect(() => isSerializableProps('/', 'test', obj)) + .toThrowErrorMatchingInlineSnapshot(` +"Error serializing \`.leve1.level2.child\` returned from \`test\` in \\"/\\". +Reason: Circular references cannot be expressed in JSON (references: \`(self)\`)." +`) + }) + + it('can handle deep obj circular refs (with arrays)', () => { + const obj = { foo: 'bar', test: true, leve1: { level2: {} } } + obj.leve1.level2.child = [{ another: [obj] }] + + expect(() => isSerializableProps('/', 'test', obj)) + .toThrowErrorMatchingInlineSnapshot(` +"Error serializing \`.leve1.level2.child[0].another[0]\` returned from \`test\` in \\"/\\". +Reason: Circular references cannot be expressed in JSON (references: \`(self)\`)." +`) + }) + + it('can handle deep arr circular refs', () => { + const arr = [1, 2, []] + arr[3] = [false, [null, 0, arr]] + + expect(() => isSerializableProps('/', 'test', { k: arr })) + .toThrowErrorMatchingInlineSnapshot(` +"Error serializing \`.k[3][1][2]\` returned from \`test\` in \\"/\\". +Reason: Circular references cannot be expressed in JSON (references: \`.k\`)." +`) + }) + + it('can handle deep arr circular refs (with objects)', () => { + const arr = [1, 2, []] + arr[3] = [false, { nested: [null, 0, arr] }] + + expect(() => isSerializableProps('/', 'test', { k: arr })) + .toThrowErrorMatchingInlineSnapshot(` +"Error serializing \`.k[3][1].nested[2]\` returned from \`test\` in \\"/\\". +Reason: Circular references cannot be expressed in JSON (references: \`.k\`)." +`) + }) + + it('allows multi object refs', () => { + const obj = { foo: 'bar', test: true } + expect( + isSerializableProps('/', 'test', { + obj1: obj, + obj2: obj, + }) + ).toBe(true) + }) + + it('allows multi object refs nested', () => { + const obj = { foo: 'bar', test: true } + expect( + isSerializableProps('/', 'test', { + obj1: obj, + obj2: obj, + anArray: [obj], + aKey: { obj }, + }) + ).toBe(true) + }) + + it('allows multi array refs', () => { + const arr = [{ foo: 'bar' }, true] + expect( + isSerializableProps('/', 'test', { + arr1: arr, + arr2: arr, + }) + ).toBe(true) + }) + + it('allows multi array refs nested', () => { + const arr = [{ foo: 'bar' }, true] + expect( + isSerializableProps('/', 'test', { + arr1: arr, + arr2: arr, + arr3: [arr], + arr4: [1, [2, 3, arr]], + }) + ).toBe(true) + }) }) diff --git a/yarn.lock b/yarn.lock index af1559a64f740a0..123d36c5498cc91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4145,7 +4145,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@4.8.3, browserslist@^4.0.0, browserslist@^4.3.6, browserslist@^4.6.0, browserslist@^4.6.4, browserslist@^4.8.0, browserslist@^4.8.2, browserslist@^4.8.3: +browserslist@4.8.3, browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6, browserslist@^4.0.0, browserslist@^4.3.6, browserslist@^4.6.0, browserslist@^4.6.4, browserslist@^4.8.0, browserslist@^4.8.2, browserslist@^4.8.3: version "4.8.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.3.tgz#65802fcd77177c878e015f0e3189f2c4f627ba44" integrity sha512-iU43cMMknxG1ClEZ2MDKeonKE1CCrFVkQK2AqO2YWFmvIrx4JWrvQ4w4hQez6EpVI8rHTtqh/ruHHDHSOKxvUg== @@ -4154,14 +4154,6 @@ browserslist@4.8.3, browserslist@^4.0.0, browserslist@^4.3.6, browserslist@^4.6. electron-to-chromium "^1.3.322" node-releases "^1.1.44" -browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: - version "1.7.7" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" - integrity sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk= - dependencies: - caniuse-db "^1.0.30000639" - electron-to-chromium "^1.2.7" - browserstack-local@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/browserstack-local/-/browserstack-local-1.4.0.tgz#d979cac056f57b9af159b3bcd7fdc09b4354537c" @@ -4465,21 +4457,11 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634: resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001023.tgz#f856f71af16a5a44e81f1fcefc1673912a43da72" integrity sha512-EnlshvE6oAum+wWwKmJNVaoqJMjIc0bLUy4Dj77VVnz1o6bzSPr1Ze9iPy6g5ycg1xD6jGU6vBmo7pLEz2MbCQ== -caniuse-db@^1.0.30000639: - version "1.0.30001033" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001033.tgz#383288df833c85d83c2bfc3469245ec1fa1f881e" - integrity sha512-2ZReq+OHqHhsIQSiv8OVNhQ6Ht9eYJpwblZydHV8nI44Od6J5YUl3J9Wxvjry/v969jCHH5fR9+C6FwJ41XbOQ== - -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001012, caniuse-lite@^1.0.30001017, caniuse-lite@^1.0.30001019: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001012, caniuse-lite@^1.0.30001017, caniuse-lite@^1.0.30001019, caniuse-lite@^1.0.30001020: version "1.0.30001019" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001019.tgz#857e3fccaad2b2feb3f1f6d8a8f62d747ea648e1" integrity sha512-6ljkLtF1KM5fQ+5ZN0wuyVvvebJxgJPTmScOMaFuQN2QuOzvRJnWSKfzQskQU5IOU4Gap3zasYPIinzwUjoj/g== -caniuse-lite@^1.0.30001020: - version "1.0.30001033" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001033.tgz#60c328fb56860de60f9a2cb419c31fb80587cba0" - integrity sha512-8Ibzxee6ibc5q88cM1usPsMpJOG5CTq0s/dKOmlekPbDGKt+UrnOOTPSjQz3kVo6yL7N4SB5xd+FGLHQmbzh6A== - capitalize@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/capitalize/-/capitalize-1.0.0.tgz#dc802c580aee101929020d2ca14b4ca8a0ae44be" @@ -6271,11 +6253,6 @@ ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== -electron-to-chromium@^1.2.7: - version "1.3.372" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.372.tgz#fb61b6dfe06f3278a384d084ebef75d463ec7580" - integrity sha512-77a4jYC52OdisHM+Tne7dgWEvQT1FoNu/jYl279pP88ZtG4ZRIPyhQwAKxj6C2rzsyC1OwsOds9JlZtNncSz6g== - electron-to-chromium@^1.3.322: version "1.3.327" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.327.tgz#516f28b4271727004362b4ac814494ae64d9dde7"