diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index eccd16c3171e7..4c823b2b75e68 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -43,6 +43,7 @@ import { enablePostpone, enableRefAsProp, enableFlightReadableStream, + enableOwnerStacks, } from 'shared/ReactFeatureFlags'; import { @@ -563,6 +564,7 @@ function createElement( key: mixed, props: mixed, owner: null | ReactComponentInfo, // DEV-only + stack: null | string, // DEV-only ): React$Element { let element: any; if (__DEV__ && enableRefAsProp) { @@ -623,6 +625,23 @@ function createElement( writable: true, value: null, }); + if (enableOwnerStacks) { + Object.defineProperty(element, '_debugStack', { + configurable: false, + enumerable: false, + writable: true, + value: {stack: stack}, + }); + Object.defineProperty(element, '_debugTask', { + configurable: false, + enumerable: false, + writable: true, + value: null, + }); + } + // TODO: We should be freezing the element but currently, we might write into + // _debugInfo later. We could move it into _store which remains mutable. + Object.freeze(element.props); } return element; } @@ -1003,6 +1022,7 @@ function parseModelTuple( tuple[2], tuple[3], __DEV__ ? (tuple: any)[4] : null, + __DEV__ && enableOwnerStacks ? (tuple: any)[5] : null, ); } return value; diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index e7aa52dacb4ae..b49fb79dd12b1 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -21,12 +21,24 @@ if (typeof File === 'undefined' || typeof FormData === 'undefined') { function normalizeCodeLocInfo(str) { return ( str && - str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { - return '\n in ' + name + (/\d/.test(m) ? ' (at **)' : ''); + str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) { + return ' in ' + name + (/\d/.test(m) ? ' (at **)' : ''); }) ); } +function getDebugInfo(obj) { + const debugInfo = obj._debugInfo; + if (debugInfo) { + for (let i = 0; i < debugInfo.length; i++) { + if (typeof debugInfo[i].stack === 'string') { + debugInfo[i].stack = normalizeCodeLocInfo(debugInfo[i].stack); + } + } + } + return debugInfo; +} + const heldValues = []; let finalizationCallback; function FinalizationRegistryMock(callback) { @@ -221,8 +233,19 @@ describe('ReactFlight', () => { await act(async () => { const rootModel = await ReactNoopFlightClient.read(transport); const greeting = rootModel.greeting; - expect(greeting._debugInfo).toEqual( - __DEV__ ? [{name: 'Greeting', env: 'Server', owner: null}] : undefined, + expect(getDebugInfo(greeting)).toEqual( + __DEV__ + ? [ + { + name: 'Greeting', + env: 'Server', + owner: null, + stack: gate(flag => flag.enableOwnerStacks) + ? ' in Object. (at **)' + : undefined, + }, + ] + : undefined, ); ReactNoop.render(greeting); }); @@ -248,8 +271,19 @@ describe('ReactFlight', () => { await act(async () => { const promise = ReactNoopFlightClient.read(transport); - expect(promise._debugInfo).toEqual( - __DEV__ ? [{name: 'Greeting', env: 'Server', owner: null}] : undefined, + expect(getDebugInfo(promise)).toEqual( + __DEV__ + ? [ + { + name: 'Greeting', + env: 'Server', + owner: null, + stack: gate(flag => flag.enableOwnerStacks) + ? ' in Object. (at **)' + : undefined, + }, + ] + : undefined, ); ReactNoop.render(await promise); }); @@ -2233,9 +2267,11 @@ describe('ReactFlight', () => { return !; } - const lazy = React.lazy(async () => ({ - default: , - })); + const lazy = React.lazy(async function myLazy() { + return { + default: , + }; + }); function ThirdPartyComponent() { return stranger; @@ -2269,31 +2305,61 @@ describe('ReactFlight', () => { await act(async () => { const promise = ReactNoopFlightClient.read(transport); - expect(promise._debugInfo).toEqual( + expect(getDebugInfo(promise)).toEqual( __DEV__ - ? [{name: 'ServerComponent', env: 'Server', owner: null}] + ? [ + { + name: 'ServerComponent', + env: 'Server', + owner: null, + stack: gate(flag => flag.enableOwnerStacks) + ? ' in Object. (at **)' + : undefined, + }, + ] : undefined, ); const result = await promise; const thirdPartyChildren = await result.props.children[1]; // We expect the debug info to be transferred from the inner stream to the outer. - expect(thirdPartyChildren[0]._debugInfo).toEqual( + expect(getDebugInfo(thirdPartyChildren[0])).toEqual( __DEV__ - ? [{name: 'ThirdPartyComponent', env: 'third-party', owner: null}] + ? [ + { + name: 'ThirdPartyComponent', + env: 'third-party', + owner: null, + stack: gate(flag => flag.enableOwnerStacks) + ? ' in Object. (at **)' + : undefined, + }, + ] : undefined, ); - expect(thirdPartyChildren[1]._debugInfo).toEqual( + expect(getDebugInfo(thirdPartyChildren[1])).toEqual( __DEV__ - ? [{name: 'ThirdPartyLazyComponent', env: 'third-party', owner: null}] + ? [ + { + name: 'ThirdPartyLazyComponent', + env: 'third-party', + owner: null, + stack: gate(flag => flag.enableOwnerStacks) + ? ' in myLazy (at **)\n in lazyInitializer (at **)' + : undefined, + }, + ] : undefined, ); - expect(thirdPartyChildren[2]._debugInfo).toEqual( + expect(getDebugInfo(thirdPartyChildren[2])).toEqual( __DEV__ ? [ { name: 'ThirdPartyFragmentComponent', env: 'third-party', owner: null, + stack: gate(flag => flag.enableOwnerStacks) + ? ' in Object. (at **)' + : undefined, }, ] : undefined, @@ -2357,24 +2423,47 @@ describe('ReactFlight', () => { await act(async () => { const promise = ReactNoopFlightClient.read(transport); - expect(promise._debugInfo).toEqual( + expect(getDebugInfo(promise)).toEqual( __DEV__ - ? [{name: 'ServerComponent', env: 'Server', owner: null}] + ? [ + { + name: 'ServerComponent', + env: 'Server', + owner: null, + stack: gate(flag => flag.enableOwnerStacks) + ? ' in Object. (at **)' + : undefined, + }, + ] : undefined, ); const result = await promise; const thirdPartyFragment = await result.props.children; - expect(thirdPartyFragment._debugInfo).toEqual( - __DEV__ ? [{name: 'Keyed', env: 'Server', owner: null}] : undefined, + expect(getDebugInfo(thirdPartyFragment)).toEqual( + __DEV__ + ? [ + { + name: 'Keyed', + env: 'Server', + owner: null, + stack: gate(flag => flag.enableOwnerStacks) + ? ' in ServerComponent (at **)' + : undefined, + }, + ] + : undefined, ); // We expect the debug info to be transferred from the inner stream to the outer. - expect(thirdPartyFragment.props.children._debugInfo).toEqual( + expect(getDebugInfo(thirdPartyFragment.props.children)).toEqual( __DEV__ ? [ { name: 'ThirdPartyAsyncIterableComponent', env: 'third-party', owner: null, + stack: gate(flag => flag.enableOwnerStacks) + ? ' in Object. (at **)' + : undefined, }, ] : undefined, @@ -2467,10 +2556,24 @@ describe('ReactFlight', () => { // We've rendered down to the span. expect(greeting.type).toBe('span'); if (__DEV__) { - const greetInfo = {name: 'Greeting', env: 'Server', owner: null}; - expect(greeting._debugInfo).toEqual([ + const greetInfo = { + name: 'Greeting', + env: 'Server', + owner: null, + stack: gate(flag => flag.enableOwnerStacks) + ? ' in Object. (at **)' + : undefined, + }; + expect(getDebugInfo(greeting)).toEqual([ greetInfo, - {name: 'Container', env: 'Server', owner: greetInfo}, + { + name: 'Container', + env: 'Server', + owner: greetInfo, + stack: gate(flag => flag.enableOwnerStacks) + ? ' in Greeting (at **)' + : undefined, + }, ]); // The owner that created the span was the outer server component. // We expect the debug info to be referentially equal to the owner. diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index 8ea9c1907eb6b..c325046499ebc 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -263,7 +263,7 @@ describe('ReactFlightDOMEdge', () => { const serializedContent = await readResult(stream1); - expect(serializedContent.length).toBeLessThan(400); + expect(serializedContent.length).toBeLessThan(410); expect(timesRendered).toBeLessThan(5); const model = await ReactServerDOMClient.createFromReadableStream(stream2, { @@ -296,7 +296,7 @@ describe('ReactFlightDOMEdge', () => { const [stream1, stream2] = passThrough(stream).tee(); const serializedContent = await readResult(stream1); - expect(serializedContent.length).toBeLessThan(400); + expect(serializedContent.length).toBeLessThan(__DEV__ ? 590 : 400); expect(timesRendered).toBeLessThan(5); const model = await ReactServerDOMClient.createFromReadableStream(stream2, { @@ -324,7 +324,7 @@ describe('ReactFlightDOMEdge', () => { , ); const serializedContent = await readResult(stream); - const expectedDebugInfoSize = __DEV__ ? 64 * 20 : 0; + const expectedDebugInfoSize = __DEV__ ? 300 * 20 : 0; expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize); }); @@ -742,10 +742,10 @@ describe('ReactFlightDOMEdge', () => { // We've rendered down to the span. expect(greeting.type).toBe('span'); if (__DEV__) { - const greetInfo = {name: 'Greeting', env: 'Server', owner: null}; + const greetInfo = expect.objectContaining({name: 'Greeting', env: 'Server', owner: null}); expect(lazyWrapper._debugInfo).toEqual([ greetInfo, - {name: 'Container', env: 'Server', owner: greetInfo}, + expect.objectContaining({name: 'Container', env: 'Server', owner: greetInfo}), ]); // The owner that created the span was the outer server component. // We expect the debug info to be referentially equal to the owner. diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 4b77895c98fe8..ef3d2dcfb1e1c 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -17,6 +17,7 @@ import { enableTaint, enableRefAsProp, enableServerComponentLogs, + enableOwnerStacks, } from 'shared/ReactFeatureFlags'; import {enableFlightReadableStream} from 'shared/ReactFeatureFlags'; @@ -123,6 +124,98 @@ import binaryToComparableString from 'shared/binaryToComparableString'; import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable'; +// TODO: Make this configurable on the Request. +const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\\)/; + +let callComponentFrame: null | string = null; +let callIteratorFrame: null | string = null; +let callLazyInitFrame: null | string = null; + +function isNotExternal(stackFrame: string): boolean { + return !externalRegExp.test(stackFrame); +} + +function initCallComponentFrame(): string { + // Extract the stack frame of the callComponentInDEV function. + const error = callComponentInDEV(Error, 'react-stack-top-frame', {}); + const stack = error.stack; + const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0; + const endIdx = stack.indexOf('\n', startIdx); + if (endIdx === -1) { + return stack.slice(startIdx); + } + return stack.slice(startIdx, endIdx); +} + +function initCallIteratorFrame(): string { + // Extract the stack frame of the callIteratorInDEV function. + try { + (callIteratorInDEV: any)({next: null}); + return ''; + } catch (error) { + const stack = error.stack; + const startIdx = stack.startsWith('TypeError: ') + ? stack.indexOf('\n') + 1 + : 0; + const endIdx = stack.indexOf('\n', startIdx); + if (endIdx === -1) { + return stack.slice(startIdx); + } + return stack.slice(startIdx, endIdx); + } +} + +function initCallLazyInitFrame(): string { + // Extract the stack frame of the callLazyInitInDEV function. + const error = callLazyInitInDEV({ + $$typeof: REACT_LAZY_TYPE, + _init: Error, + _payload: 'react-stack-top-frame', + }); + const stack = error.stack; + const startIdx = stack.startsWith('Error: react-stack-top-frame\n') ? 29 : 0; + const endIdx = stack.indexOf('\n', startIdx); + if (endIdx === -1) { + return stack.slice(startIdx); + } + return stack.slice(startIdx, endIdx); +} + +function filterDebugStack(error: Error): string { + // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly + // to save bandwidth even in DEV. We'll also replay these stacks on the client so by + // stripping them early we avoid that overhead. Otherwise we'd normally just rely on + // the DevTools or framework's ignore lists to filter them out. + let stack = error.stack; + if (stack.startsWith('Error: react-stack-top-frame\n')) { + // V8's default formatting prefixes with the error message which we + // don't want/need. + stack = stack.slice(29); + } + const frames = stack.split('\n').slice(1); + if (callComponentFrame === null) { + callComponentFrame = initCallComponentFrame(); + } + let lastFrameIdx = frames.indexOf(callComponentFrame); + if (lastFrameIdx === -1) { + if (callLazyInitFrame === null) { + callLazyInitFrame = initCallLazyInitFrame(); + } + lastFrameIdx = frames.indexOf(callLazyInitFrame); + if (lastFrameIdx === -1) { + if (callIteratorFrame === null) { + callIteratorFrame = initCallIteratorFrame(); + } + lastFrameIdx = frames.indexOf(callIteratorFrame); + } + } + if (lastFrameIdx !== -1) { + // Cut off everything after our "callComponent" slot since it'll be Flight internals. + frames.length = lastFrameIdx; + } + return frames.filter(isNotExternal).join('\n'); +} + initAsyncDebugInfo(); function patchConsole(consoleInst: typeof console, methodName: string) { @@ -146,10 +239,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) { // Extract the stack. Not all console logs print the full stack but they have at // least the line it was called from. We could optimize transfer by keeping just // one stack frame but keeping it simple for now and include all frames. - let stack = new Error().stack; - if (stack.startsWith('Error: \n')) { - stack = stack.slice(8); - } + let stack = filterDebugStack(new Error('react-stack-top-frame')); const firstLine = stack.indexOf('\n'); if (firstLine === -1) { stack = ''; @@ -621,6 +711,20 @@ function serializeReadableStream( return serializeByValueID(streamTask.id); } +// This indirect exists so we can exclude its stack frame in DEV (and anything below it). +/** @noinline */ +function callIteratorInDEV( + iterator: $AsyncIterator, + progress: ( + entry: + | {done: false, +value: ReactClientValue, ...} + | {done: true, +value: ReactClientValue, ...}, + ) => void, + error: (reason: mixed) => void, +) { + iterator.next().then(progress, error); +} + function serializeAsyncIterable( request: Request, task: Task, @@ -697,7 +801,11 @@ function serializeAsyncIterable( request.pendingChunks++; tryStreamTask(request, streamTask); enqueueFlush(request); - iterator.next().then(progress, error); + if (__DEV__) { + callIteratorInDEV(iterator, progress, error); + } else { + iterator.next().then(progress, error); + } } catch (x) { error(x); return; @@ -731,7 +839,11 @@ function serializeAsyncIterable( } } request.abortListeners.add(error); - iterator.next().then(progress, error); + if (__DEV__) { + callIteratorInDEV(iterator, progress, error); + } else { + iterator.next().then(progress, error); + } return serializeByValueID(streamTask.id); } @@ -809,13 +921,49 @@ function createLazyWrapperAroundWakeable(wakeable: Wakeable) { return lazyType; } +// This indirect exists so we can exclude its stack frame in DEV (and anything below it). +/** @noinline */ +function callComponentInDEV( + Component: (p: Props, arg: void) => R, + props: Props, + componentDebugInfo: ReactComponentInfo, +): R { + // The secondArg is always undefined in Server Components since refs error early. + const secondArg = undefined; + setCurrentOwner(componentDebugInfo); + try { + if (supportsComponentStorage) { + // Run the component in an Async Context that tracks the current owner. + return componentStorage.run( + componentDebugInfo, + Component, + props, + secondArg, + ); + } else { + return Component(props, secondArg); + } + } finally { + setCurrentOwner(null); + } +} + +// This indirect exists so we can exclude its stack frame in DEV (and anything below it). +/** @noinline */ +function callLazyInitInDEV(lazy: LazyComponent): any { + const payload = lazy._payload; + const init = lazy._init; + return init(payload); +} + function renderFunctionComponent( request: Request, task: Task, key: null | string, Component: (p: Props, arg: void) => any, props: Props, - owner: null | ReactComponentInfo, + owner: null | ReactComponentInfo, // DEV-only + stack: null | string, // DEV-only ): ReactJSONValue { // Reset the task's thenable state before continuing, so that if a later // component suspends we can reuse the same task object. If the same @@ -823,8 +971,6 @@ function renderFunctionComponent( const prevThenableState = task.thenableState; task.thenableState = null; - // The secondArg is always undefined in Server Components since refs error early. - const secondArg = undefined; let result; let componentDebugInfo: ReactComponentInfo; @@ -850,6 +996,9 @@ function renderFunctionComponent( env: request.environmentName, owner: owner, }; + if (enableOwnerStacks) { + (componentDebugInfo: any).stack = stack; + } // We outline this model eagerly so that we can refer to by reference as an owner. // If we had a smarter way to dedupe we might not have to do this if there ends up // being no references to this as an owner. @@ -857,24 +1006,11 @@ function renderFunctionComponent( emitDebugChunk(request, componentDebugID, componentDebugInfo); } prepareToUseHooksForComponent(prevThenableState, componentDebugInfo); - setCurrentOwner(componentDebugInfo); - try { - if (supportsComponentStorage) { - // Run the component in an Async Context that tracks the current owner. - result = componentStorage.run( - componentDebugInfo, - Component, - props, - secondArg, - ); - } else { - result = Component(props, secondArg); - } - } finally { - setCurrentOwner(null); - } + result = callComponentInDEV(Component, props, componentDebugInfo); } else { prepareToUseHooksForComponent(prevThenableState, null); + // The secondArg is always undefined in Server Components since refs error early. + const secondArg = undefined; result = Component(props, secondArg); } if (typeof result === 'object' && result !== null) { @@ -1093,6 +1229,7 @@ function renderClientElement( key: null | string, props: any, owner: null | ReactComponentInfo, // DEV-only + stack: null | string, // DEV-only ): ReactJSONValue { // We prepend the terminal client element that actually gets serialized with // the keys of any Server Components which are not serialized. @@ -1103,7 +1240,9 @@ function renderClientElement( key = keyPath + ',' + key; } const element = __DEV__ - ? [REACT_ELEMENT_TYPE, type, key, props, owner] + ? enableOwnerStacks + ? [REACT_ELEMENT_TYPE, type, key, props, owner, stack] + : [REACT_ELEMENT_TYPE, type, key, props, owner] : [REACT_ELEMENT_TYPE, type, key, props]; if (task.implicitSlot && key !== null) { // The root Server Component had no key so it was in an implicit slot. @@ -1151,6 +1290,7 @@ function renderElement( ref: mixed, props: any, owner: null | ReactComponentInfo, // DEV only + stack: null | string, // DEV only ): ReactJSONValue { if (ref !== null && ref !== undefined) { // When the ref moves to the regular props object this will implicitly @@ -1171,13 +1311,21 @@ function renderElement( if (typeof type === 'function') { if (isClientReference(type) || isTemporaryReference(type)) { // This is a reference to a Client Component. - return renderClientElement(task, type, key, props, owner); + return renderClientElement(task, type, key, props, owner, stack); } // This is a Server Component. - return renderFunctionComponent(request, task, key, type, props, owner); + return renderFunctionComponent( + request, + task, + key, + type, + props, + owner, + stack, + ); } else if (typeof type === 'string') { // This is a host element. E.g. HTML. - return renderClientElement(task, type, key, props, owner); + return renderClientElement(task, type, key, props, owner, stack); } else if (typeof type === 'symbol') { if (type === REACT_FRAGMENT_TYPE && key === null) { // For key-less fragments, we add a small optimization to avoid serializing @@ -1198,17 +1346,22 @@ function renderElement( } // This might be a built-in React component. We'll let the client decide. // Any built-in works as long as its props are serializable. - return renderClientElement(task, type, key, props, owner); + return renderClientElement(task, type, key, props, owner, stack); } else if (type != null && typeof type === 'object') { if (isClientReference(type)) { // This is a reference to a Client Component. - return renderClientElement(task, type, key, props, owner); + return renderClientElement(task, type, key, props, owner, stack); } switch (type.$$typeof) { case REACT_LAZY_TYPE: { - const payload = type._payload; - const init = type._init; - const wrappedType = init(payload); + let wrappedType; + if (__DEV__) { + wrappedType = callLazyInitInDEV(type); + } else { + const payload = type._payload; + const init = type._init; + wrappedType = init(payload); + } return renderElement( request, task, @@ -1217,6 +1370,7 @@ function renderElement( ref, props, owner, + stack, ); } case REACT_FORWARD_REF_TYPE: { @@ -1227,10 +1381,20 @@ function renderElement( type.render, props, owner, + stack, ); } case REACT_MEMO_TYPE: { - return renderElement(request, task, type.type, key, ref, props, owner); + return renderElement( + request, + task, + type.type, + key, + ref, + props, + owner, + stack, + ); } } } @@ -1822,6 +1986,9 @@ function renderModelDestructive( ref, props, __DEV__ ? element._owner : null, + __DEV__ && enableOwnerStacks + ? filterDebugStack(element._debugStack) + : null, ); } case REACT_LAZY_TYPE: { @@ -1830,9 +1997,14 @@ function renderModelDestructive( task.thenableState = null; const lazy: LazyComponent = (value: any); - const payload = lazy._payload; - const init = lazy._init; - const resolvedModel = init(payload); + let resolvedModel; + if (__DEV__) { + resolvedModel = callLazyInitInDEV(lazy); + } else { + const payload = lazy._payload; + const init = lazy._init; + resolvedModel = init(payload); + } if (__DEV__) { const debugInfo: ?ReactDebugInfo = lazy._debugInfo; if (debugInfo) { diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js index fad4d42bcc50d..f799fd7b16b49 100644 --- a/packages/react/src/jsx/ReactJSXElement.js +++ b/packages/react/src/jsx/ReactJSXElement.js @@ -13,6 +13,7 @@ import { getIteratorFn, REACT_ELEMENT_TYPE, REACT_FRAGMENT_TYPE, + REACT_LAZY_TYPE, } from 'shared/ReactSymbols'; import {checkKeyStringCoercion} from 'shared/CheckStringCoercion'; import isValidElementType from 'shared/isValidElementType'; @@ -23,6 +24,7 @@ import { disableStringRefs, disableDefaultPropsExceptForClasses, enableFastJSX, + enableOwnerStacks, } from 'shared/ReactFeatureFlags'; import {checkPropStringCoercion} from 'shared/CheckStringCoercion'; import {ClassComponent} from 'react-reconciler/src/ReactWorkTags'; @@ -30,6 +32,34 @@ import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFrom const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference'); +const createTask = + // eslint-disable-next-line react-internal/no-production-logging + __DEV__ && enableOwnerStacks && console.createTask + ? // eslint-disable-next-line react-internal/no-production-logging + console.createTask + : () => null; + +function getTaskName(type) { + if (type === REACT_FRAGMENT_TYPE) { + return '<>'; + } + if ( + typeof type === 'object' && + type !== null && + type.$$typeof === REACT_LAZY_TYPE + ) { + // We don't want to eagerly initialize the initializer in DEV mode so we can't + // call it to extract the type so we don't know the type of this component. + return '<...>'; + } + try { + const name = getComponentNameFromType(type); + return name ? '<' + name + '>' : '<...>'; + } catch (x) { + return '<...>'; + } +} + function getOwner() { if (__DEV__ || !disableStringRefs) { const dispatcher = ReactSharedInternals.A; @@ -194,7 +224,17 @@ function elementRefGetterWithDeprecationWarning() { * indicating filename, line number, and/or other information. * @internal */ -function ReactElement(type, key, _ref, self, source, owner, props) { +function ReactElement( + type, + key, + _ref, + self, + source, + owner, + props, + debugStack, + debugTask, +) { let ref; if (enableRefAsProp) { // When enableRefAsProp is on, ignore whatever was passed as the ref @@ -311,6 +351,20 @@ function ReactElement(type, key, _ref, self, source, owner, props) { writable: true, value: null, }); + if (enableOwnerStacks) { + Object.defineProperty(element, '_debugStack', { + configurable: false, + enumerable: false, + writable: true, + value: debugStack, + }); + Object.defineProperty(element, '_debugTask', { + configurable: false, + enumerable: false, + writable: true, + value: debugTask, + }); + } if (Object.freeze) { Object.freeze(element.props); Object.freeze(element); @@ -404,7 +458,17 @@ export function jsxProd(type, config, maybeKey) { } } - return ReactElement(type, key, ref, undefined, undefined, getOwner(), props); + return ReactElement( + type, + key, + ref, + undefined, + undefined, + getOwner(), + props, + undefined, + undefined, + ); } // While `jsxDEV` should never be called when running in production, we do @@ -652,6 +716,8 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) { source, getOwner(), props, + __DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined, + __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined, ); if (type === REACT_FRAGMENT_TYPE) { @@ -842,6 +908,8 @@ export function createElement(type, config, children) { undefined, getOwner(), props, + __DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined, + __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined, ); if (type === REACT_FRAGMENT_TYPE) { @@ -862,6 +930,8 @@ export function cloneAndReplaceKey(oldElement, newKey) { undefined, !__DEV__ && disableStringRefs ? undefined : oldElement._owner, oldElement.props, + __DEV__ && enableOwnerStacks ? oldElement._debugStack : undefined, + __DEV__ && enableOwnerStacks ? oldElement._debugTask : undefined, ); } @@ -973,6 +1043,8 @@ export function cloneElement(element, config, children) { undefined, owner, props, + __DEV__ && enableOwnerStacks ? element._debugStack : undefined, + __DEV__ && enableOwnerStacks ? element._debugTask : undefined, ); for (let i = 2; i < arguments.length; i++) { diff --git a/packages/shared/ReactElementType.js b/packages/shared/ReactElementType.js index 86b74aa00c1bd..1ae3ead9cb426 100644 --- a/packages/shared/ReactElementType.js +++ b/packages/shared/ReactElementType.js @@ -7,6 +7,12 @@ * @flow */ +import type {ReactDebugInfo} from './ReactTypes'; + +interface ConsoleTask { + run(f: () => T): T; +} + export type ReactElement = { $$typeof: any, type: any, @@ -18,4 +24,7 @@ export type ReactElement = { // __DEV__ _store: {validated: boolean, ...}, + _debugInfo: null | ReactDebugInfo, + _debugStack: Error, + _debugTask: null | ConsoleTask, }; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 1c2ea36054676..e0140ac53533a 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -182,6 +182,7 @@ export type ReactComponentInfo = { +name?: string, +env?: string, +owner?: null | ReactComponentInfo, + +stack?: null | string, }; export type ReactAsyncInfo = { diff --git a/scripts/error-codes/transform-error-messages.js b/scripts/error-codes/transform-error-messages.js index 234224b89b1c1..56c831f9ae7dd 100644 --- a/scripts/error-codes/transform-error-messages.js +++ b/scripts/error-codes/transform-error-messages.js @@ -49,6 +49,11 @@ module.exports = function (babel) { errorMsgExpressions ); + if (errorMsgLiteral === 'react-stack-top-frame') { + // This is a special case for generating stack traces. + return; + } + let prodErrorId = errorMap[errorMsgLiteral]; if (prodErrorId === undefined) { // There is no error code for this message. Add an inline comment diff --git a/scripts/eslint-rules/prod-error-codes.js b/scripts/eslint-rules/prod-error-codes.js index 3d99b8c1feada..9177cae67e465 100644 --- a/scripts/eslint-rules/prod-error-codes.js +++ b/scripts/eslint-rules/prod-error-codes.js @@ -50,6 +50,10 @@ module.exports = { return; } const errorMessage = nodeToErrorTemplate(errorMessageNode); + if (errorMessage === 'react-stack-top-frame') { + // This is a special case for generating stack traces. + return; + } if (errorMessages.has(errorMessage)) { return; } diff --git a/scripts/jest/setupTests.js b/scripts/jest/setupTests.js index 5aa238804b680..aa1f051ac4922 100644 --- a/scripts/jest/setupTests.js +++ b/scripts/jest/setupTests.js @@ -292,9 +292,14 @@ function lazyRequireFunctionExports(moduleName) { // If this export is a function, return a wrapper function that lazily // requires the implementation from the current module cache. if (typeof originalModule[prop] === 'function') { - return function () { + const wrapper = function () { return jest.requireActual(moduleName)[prop].apply(this, arguments); }; + // We use this to trick the filtering of Flight to exclude this frame. + Object.defineProperty(wrapper, 'name', { + value: '()', + }); + return wrapper; } else { return originalModule[prop]; }