diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 857ce99868d9b..5e40d0d054225 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -3423,4 +3423,96 @@ describe('ReactFlight', () => { ); expect(caughtError.digest).toBe('digest("my-error")'); }); + + // @gate __DEV__ + it('can render deep but cut off JSX in debug info', async () => { + function createDeepJSX(n) { + if (n <= 0) { + return null; + } + return
{createDeepJSX(n - 1)}
; + } + + function ServerComponent(props) { + return
not using props
; + } + + const transport = ReactNoopFlightServer.render({ + root: ( + + {createDeepJSX(100) /* deper than objectLimit */} + + ), + }); + + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); + const root = rootModel.root; + const children = root._debugInfo[0].props.children; + expect(children.type).toBe('div'); + expect(children.props.children.type).toBe('div'); + ReactNoop.render(root); + }); + + expect(ReactNoop).toMatchRenderedOutput(
not using props
); + }); + + // @gate __DEV__ + it('can render deep but cut off Map/Set in debug info', async () => { + function createDeepMap(n) { + if (n <= 0) { + return null; + } + const map = new Map(); + map.set('key', createDeepMap(n - 1)); + return map; + } + + function createDeepSet(n) { + if (n <= 0) { + return null; + } + const set = new Set(); + set.add(createDeepSet(n - 1)); + return set; + } + + function ServerComponent(props) { + return
not using props
; + } + + const transport = ReactNoopFlightServer.render({ + set: ( + + ), + map: ( + + ), + }); + + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); + const set = rootModel.set._debugInfo[0].props.set; + const map = rootModel.map._debugInfo[0].props.map; + expect(set instanceof Set).toBe(true); + expect(set.size).toBe(1); + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const entry of set) { + expect(entry instanceof Set).toBe(true); + break; + } + + expect(map instanceof Map).toBe(true); + expect(map.size).toBe(1); + expect(map.get('key') instanceof Map).toBe(true); + + ReactNoop.render(rootModel.set); + }); + + expect(ReactNoop).toMatchRenderedOutput(
not using props
); + }); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 5db03d628146f..de2920d1343ba 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -137,6 +137,9 @@ import binaryToComparableString from 'shared/binaryToComparableString'; import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable'; +// DEV-only set containing internal objects that should not be limited and turned into getters. +const doNotLimit: WeakSet = __DEV__ ? new WeakSet() : (null: any); + function defaultFilterStackFrame( filename: string, functionName: string, @@ -2147,6 +2150,22 @@ function serializeConsoleMap( ): string { // Like serializeMap but for renderConsoleValue. const entries = Array.from(map); + // The Map itself doesn't take up any space but the outlined object does. + counter.objectLimit++; + for (let i = 0; i < entries.length; i++) { + // Outline every object entry in case we run out of space to serialize them. + // Because we can't mark these values as limited. + const entry = entries[i]; + doNotLimit.add(entry); + const key = entry[0]; + const value = entry[1]; + if (typeof key === 'object' && key !== null) { + doNotLimit.add(key); + } + if (typeof value === 'object' && value !== null) { + doNotLimit.add(value); + } + } const id = outlineConsoleValue(request, counter, entries); return '$Q' + id.toString(16); } @@ -2158,6 +2177,16 @@ function serializeConsoleSet( ): string { // Like serializeMap but for renderConsoleValue. const entries = Array.from(set); + // The Set itself doesn't take up any space but the outlined object does. + counter.objectLimit++; + for (let i = 0; i < entries.length; i++) { + // Outline every object entry in case we run out of space to serialize them. + // Because we can't mark these values as limited. + const entry = entries[i]; + if (typeof entry === 'object' && entry !== null) { + doNotLimit.add(entry); + } + } const id = outlineConsoleValue(request, counter, entries); return '$W' + id.toString(16); } @@ -3362,20 +3391,15 @@ function renderConsoleValue( parentPropertyName: string, value: ReactClientValue, ): ReactJSONValue { - // Make sure that `parent[parentPropertyName]` wasn't JSONified before `value` was passed to us - // $FlowFixMe[incompatible-use] - const originalValue = parent[parentPropertyName]; - if ( - typeof originalValue === 'object' && - originalValue !== value && - !(originalValue instanceof Date) - ) { - } - if (value === null) { return null; } + // Special Symbol, that's very common. + if (value === REACT_ELEMENT_TYPE) { + return '$'; + } + if (typeof value === 'object') { if (isClientReference(value)) { // We actually have this value on the client so we could import it. @@ -3407,7 +3431,7 @@ function renderConsoleValue( return existingReference; } - if (counter.objectLimit <= 0) { + if (counter.objectLimit <= 0 && !doNotLimit.has(value)) { // We've reached our max number of objects to serialize across the wire so we serialize this // as a marker so that the client can error when this is accessed by the console. return serializeLimitedObject(); @@ -3427,13 +3451,12 @@ function renderConsoleValue( if (element._debugStack != null) { // Outline the debug stack so that it doesn't get cut off. debugStack = filterStackTrace(request, element._debugStack, 1); - const stackId = outlineConsoleValue( - request, - {objectLimit: debugStack.length + 2}, - debugStack, - ); - request.writtenObjects.set(debugStack, serializeByValueID(stackId)); + doNotLimit.add(debugStack); + for (let i = 0; i < debugStack.length; i++) { + doNotLimit.add(debugStack[i]); + } } + doNotLimit.add(element.props); return [ REACT_ELEMENT_TYPE, element.type, @@ -3578,6 +3601,9 @@ function renderConsoleValue( if (typeof value === 'string') { if (value[value.length - 1] === 'Z') { // Possibly a Date, whose toJSON automatically calls toISOString + // Make sure that `parent[parentPropertyName]` wasn't JSONified before `value` was passed to us + // $FlowFixMe[incompatible-use] + const originalValue = parent[parentPropertyName]; if (originalValue instanceof Date) { return serializeDateFromDateJSON(value); } @@ -3662,6 +3688,11 @@ function outlineConsoleValue( ); } + if (typeof model === 'object' && model !== null) { + // We can't limit outlined values. + doNotLimit.add(model); + } + function replacer( this: | {+[key: string | number]: ReactClientValue}