From 81ce4c040ac91a4c45af5a0acd0e73c6f28d54aa Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 4 Apr 2024 14:50:58 -0400 Subject: [PATCH 1/7] Test --- .../src/__tests__/ReactFlight-test.js | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 55956ddc93ecd..8d87832c87b96 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -2145,4 +2145,49 @@ describe('ReactFlight', () => { expect(loggedFn).not.toBe(foo); expect(loggedFn.toString()).toBe(foo.toString()); }); + + it('uses the server component debug info as the element owner in DEV', async () => { + function Container({children}) { + return children; + } + + function Greeting({firstName}) { + // We can't use JSX here because it'll use the Client React. + return ReactServer.createElement( + Container, + null, + ReactServer.createElement('span', null, 'Hello, ', firstName), + ); + } + + const model = { + greeting: ReactServer.createElement(Greeting, {firstName: 'Seb'}), + }; + + const transport = ReactNoopFlightServer.render(model); + + await act(async () => { + const rootModel = await ReactNoopFlightClient.read(transport); + const greeting = rootModel.greeting; + // We've rendered down to the span. + expect(greeting.type).toBe('span'); + if (__DEV__) { + expect(greeting._debugInfo).toEqual([ + {name: 'Greeting', env: 'Server'}, + {name: 'Container', env: 'Server'}, + ]); + // The owner that created the span was the outer server component. + // We expect the debug info to be referentially equal to the owner. + expect(greeting._owner).toBe(greeting._debugInfo[0]); + } else { + expect(greeting._debugInfo).toBe(undefined); + expect(greeting._owner).toBe( + gate(flags => flags.disableStringRefs) ? undefined : null, + ); + } + ReactNoop.render(greeting); + }); + + expect(ReactNoop).toMatchRenderedOutput(Hello, Seb); + }); }); From 6bfed882d48c10439c3bf9827bb06bdfa45ec0e6 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 4 Apr 2024 17:20:29 -0400 Subject: [PATCH 2/7] Encode debug info with the console serialization instead of plain JSON That way we can reuse objects by reference from elsewhere in the playload. We can't use the regular serialization because it requires a task and emits results into that ID. We could maybe create a new variant that can be shared by imports too but since we only need this from DEV and we already have a sufficient one for this particular use case we can use it. --- .../react-client/src/ReactFlightClient.js | 5 ++++- .../react-server/src/ReactFlightServer.js | 21 ++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 082f94261bfd7..8a8888b9a9ed9 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1286,7 +1286,10 @@ function processFullRow( } case 68 /* "D" */: { if (__DEV__) { - const debugInfo = JSON.parse(row); + const debugInfo: ReactComponentInfo | ReactAsyncInfo = parseModel( + response, + row, + ); resolveDebugInfo(response, id, debugInfo); return; } diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index ad0a28f37175c..b4594b924131b 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1904,8 +1904,27 @@ function emitDebugChunk( ); } + // We use the console encoding so that we can dedupe objects but don't necessarily + // use the full serialization that requires a task. + const counter = {objectCount: 0}; + function replacer( + this: + | {+[key: string | number]: ReactClientValue} + | $ReadOnlyArray, + parentPropertyName: string, + value: ReactClientValue, + ): ReactJSONValue { + return renderConsoleValue( + request, + counter, + this, + parentPropertyName, + value, + ); + } + // $FlowFixMe[incompatible-type] stringify can return null - const json: string = stringify(debugInfo); + const json: string = stringify(debugInfo, replacer); const row = serializeRowHeader('D', id) + json + '\n'; const processedChunk = stringToChunk(row); request.completedRegularChunks.push(processedChunk); From a1570caccf9d603a4595b9515241efa4c6bbe609 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 4 Apr 2024 17:26:40 -0400 Subject: [PATCH 3/7] Outline Server Component debug info so that we can refer to it by reference later --- packages/react-server/src/ReactFlightServer.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index b4594b924131b..20afab4b9f0cb 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -614,10 +614,17 @@ function renderFunctionComponent( const componentName = (Component: any).displayName || Component.name || ''; request.pendingChunks++; - emitDebugChunk(request, debugID, { + + const componentDebugID = debugID; + const serverComponentMetaData: ReactComponentInfo = { name: componentName, env: request.environmentName, - }); + }; + // 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. + outlineModel(request, serverComponentMetaData); + emitDebugChunk(request, componentDebugID, serverComponentMetaData); } } From d290097e877785466ffd8b712334d7570975540d Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 4 Apr 2024 18:06:00 -0400 Subject: [PATCH 4/7] Initialize current owner in DEV before rendering a server component If we suspend, we stash it on prevThenableState as a hack. Then we serialize the owner as the last slot in the Element tuple so we can restore it on the client. --- .../react-client/src/ReactFlightClient.js | 12 +++- packages/react-server/src/ReactFlightHooks.js | 13 ++++- .../react-server/src/ReactFlightServer.js | 58 ++++++++++++++----- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 8a8888b9a9ed9..915a387835265 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -484,6 +484,7 @@ function createElement( type: mixed, key: mixed, props: mixed, + owner: null | ReactComponentInfo, // DEV-only ): React$Element { let element: any; if (__DEV__ && enableRefAsProp) { @@ -493,7 +494,7 @@ function createElement( type, key, props, - _owner: null, + _owner: owner, }: any); Object.defineProperty(element, 'ref', { enumerable: false, @@ -520,7 +521,7 @@ function createElement( props, // Record the component responsible for creating this element. - _owner: null, + _owner: owner, }: any); } @@ -854,7 +855,12 @@ function parseModelTuple( if (tuple[0] === REACT_ELEMENT_TYPE) { // TODO: Consider having React just directly accept these arrays as elements. // Or even change the ReactElement type to be an array. - return createElement(tuple[1], tuple[2], tuple[3]); + return createElement( + tuple[1], + tuple[2], + tuple[3], + __DEV__ ? (tuple: any)[4] : null, + ); } return value; } diff --git a/packages/react-server/src/ReactFlightHooks.js b/packages/react-server/src/ReactFlightHooks.js index 75a99dc558ea5..e85e0e9ce6a60 100644 --- a/packages/react-server/src/ReactFlightHooks.js +++ b/packages/react-server/src/ReactFlightHooks.js @@ -9,7 +9,7 @@ import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes'; import type {Request} from './ReactFlightServer'; -import type {Thenable, Usable} from 'shared/ReactTypes'; +import type {Thenable, Usable, ReactComponentInfo} from 'shared/ReactTypes'; import type {ThenableState} from './ReactFlightThenable'; import { REACT_MEMO_CACHE_SENTINEL, @@ -21,6 +21,7 @@ import {isClientReference} from './ReactFlightServerConfig'; let currentRequest = null; let thenableIndexCounter = 0; let thenableState = null; +let currentComponentDebugInfo = null; export function prepareToUseHooksForRequest(request: Request) { currentRequest = request; @@ -32,9 +33,13 @@ export function resetHooksForRequest() { export function prepareToUseHooksForComponent( prevThenableState: ThenableState | null, + componentDebugInfo: null | ReactComponentInfo, ) { thenableIndexCounter = 0; thenableState = prevThenableState; + if (__DEV__) { + currentComponentDebugInfo = componentDebugInfo; + } } export function getThenableStateAfterSuspending(): ThenableState { @@ -42,6 +47,12 @@ export function getThenableStateAfterSuspending(): ThenableState { // which is not really supported anymore, it will be empty. We use the empty set as a // marker to know if this was a replay of the same component or first attempt. const state = thenableState || createThenableState(); + if (__DEV__) { + // This is a hack but we stash the debug info here so that we don't need a completely + // different data structure just for this in DEV. Not too happy about it. + (state: any)._componentDebugInfo = currentComponentDebugInfo; + currentComponentDebugInfo = null; + } thenableState = null; return state; } diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 20afab4b9f0cb..66d7cc8c04ff1 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -58,6 +58,7 @@ import type { ReactComponentInfo, ReactAsyncInfo, } from 'shared/ReactTypes'; +import type {ReactElement} from 'shared/ReactElementType'; import type {LazyComponent} from 'react/src/ReactLazy'; import type {TemporaryReference} from './ReactFlightServerTemporaryReferences'; @@ -303,6 +304,7 @@ const { ReactCurrentCache, } = ReactServerSharedInternals; const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; function throwTaintViolation(message: string) { // eslint-disable-next-line react-internal/prod-error-codes @@ -601,6 +603,7 @@ function renderFunctionComponent( const prevThenableState = task.thenableState; task.thenableState = null; + let componentDebugInfo: null | ReactComponentInfo = null; if (__DEV__) { if (debugID === null) { // We don't have a chunk to assign debug info. We need to outline this @@ -609,6 +612,8 @@ function renderFunctionComponent( } else if (prevThenableState !== null) { // This is a replay and we've already emitted the debug info of this component // in the first pass. We skip emitting a duplicate line. + // As a hack we stashed the previous component debug info on this object in DEV. + componentDebugInfo = (prevThenableState: any)._componentDebugInfo; } else { // This is a new component in the same task so we can emit more debug info. const componentName = @@ -616,22 +621,32 @@ function renderFunctionComponent( request.pendingChunks++; const componentDebugID = debugID; - const serverComponentMetaData: ReactComponentInfo = { + componentDebugInfo = { name: componentName, env: request.environmentName, }; // 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. - outlineModel(request, serverComponentMetaData); - emitDebugChunk(request, componentDebugID, serverComponentMetaData); + outlineModel(request, componentDebugInfo); + emitDebugChunk(request, componentDebugID, componentDebugInfo); } } - prepareToUseHooksForComponent(prevThenableState); + prepareToUseHooksForComponent(prevThenableState, componentDebugInfo); // The secondArg is always undefined in Server Components since refs error early. const secondArg = undefined; - let result = Component(props, secondArg); + let result; + if (__DEV__) { + ReactCurrentOwner.current = componentDebugInfo; + try { + result = Component(props, secondArg); + } finally { + ReactCurrentOwner.current = null; + } + } else { + result = Component(props, secondArg); + } if ( typeof result === 'object' && result !== null && @@ -730,9 +745,12 @@ function renderClientElement( type: any, key: null | string, props: any, + owner: null | ReactComponentInfo, // DEV-only ): ReactJSONValue { if (!enableServerComponentKeys) { - return [REACT_ELEMENT_TYPE, type, key, props]; + return __DEV__ + ? [REACT_ELEMENT_TYPE, type, key, props, owner] + : [REACT_ELEMENT_TYPE, type, key, props]; } // We prepend the terminal client element that actually gets serialized with // the keys of any Server Components which are not serialized. @@ -742,7 +760,9 @@ function renderClientElement( } else if (keyPath !== null) { key = keyPath + ',' + key; } - const element = [REACT_ELEMENT_TYPE, type, key, props]; + const element = __DEV__ + ? [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. // If we had a key lower, it would end up in that slot with an explicit key. @@ -788,6 +808,7 @@ function renderElement( key: null | string, ref: mixed, props: any, + owner: null | ReactComponentInfo, // DEV only ): ReactJSONValue { if (ref !== null && ref !== undefined) { // When the ref moves to the regular props object this will implicitly @@ -808,13 +829,13 @@ 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); + return renderClientElement(task, type, key, props, owner); } // This is a Server Component. return renderFunctionComponent(request, task, key, type, props); } else if (typeof type === 'string') { // This is a host element. E.g. HTML. - return renderClientElement(task, type, key, props); + return renderClientElement(task, type, key, props, owner); } else if (typeof type === 'symbol') { if (type === REACT_FRAGMENT_TYPE && key === null) { // For key-less fragments, we add a small optimization to avoid serializing @@ -835,24 +856,32 @@ 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); + return renderClientElement(task, type, key, props, owner); } else if (type != null && typeof type === 'object') { if (isClientReference(type)) { // This is a reference to a Client Component. - return renderClientElement(task, type, key, props); + return renderClientElement(task, type, key, props, owner); } switch (type.$$typeof) { case REACT_LAZY_TYPE: { const payload = type._payload; const init = type._init; const wrappedType = init(payload); - return renderElement(request, task, wrappedType, key, ref, props); + return renderElement( + request, + task, + wrappedType, + key, + ref, + props, + owner, + ); } case REACT_FORWARD_REF_TYPE: { return renderFunctionComponent(request, task, key, type.render, props); } case REACT_MEMO_TYPE: { - return renderElement(request, task, type.type, key, ref, props); + return renderElement(request, task, type.type, key, ref, props, owner); } } } @@ -1363,7 +1392,7 @@ function renderModelDestructive( writtenObjects.set((value: any).props, NEVER_OUTLINED); } - const element: React$Element = (value: any); + const element: ReactElement = (value: any); if (__DEV__) { const debugInfo: ?ReactDebugInfo = (value: any)._debugInfo; @@ -1401,6 +1430,7 @@ function renderModelDestructive( element.key, ref, props, + __DEV__ ? element._owner : null, ); } case REACT_LAZY_TYPE: { From b44d3020de3e0d1903d7e3f0bb89dea30e43bfbd Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 4 Apr 2024 18:28:13 -0400 Subject: [PATCH 5/7] Include the owner of a Server Component which must also be another Server Component --- .../src/__tests__/ReactFlight-test.js | 17 ++++++++++------- .../src/__tests__/componentStacks-test.js | 1 + .../src/__tests__/ReactFlightDOMEdge-test.js | 2 +- packages/react-server/src/ReactFlightServer.js | 13 +++++++++++-- packages/react/src/__tests__/ReactFetch-test.js | 2 +- packages/shared/ReactTypes.js | 1 + 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 8d87832c87b96..6fbd2360c82ef 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -214,7 +214,7 @@ describe('ReactFlight', () => { const rootModel = await ReactNoopFlightClient.read(transport); const greeting = rootModel.greeting; expect(greeting._debugInfo).toEqual( - __DEV__ ? [{name: 'Greeting', env: 'Server'}] : undefined, + __DEV__ ? [{name: 'Greeting', env: 'Server', owner: null}] : undefined, ); ReactNoop.render(greeting); }); @@ -241,7 +241,7 @@ describe('ReactFlight', () => { await act(async () => { const promise = ReactNoopFlightClient.read(transport); expect(promise._debugInfo).toEqual( - __DEV__ ? [{name: 'Greeting', env: 'Server'}] : undefined, + __DEV__ ? [{name: 'Greeting', env: 'Server', owner: null}] : undefined, ); ReactNoop.render(await promise); }); @@ -2072,19 +2072,21 @@ describe('ReactFlight', () => { await act(async () => { const promise = ReactNoopFlightClient.read(transport); expect(promise._debugInfo).toEqual( - __DEV__ ? [{name: 'ServerComponent', env: 'Server'}] : undefined, + __DEV__ + ? [{name: 'ServerComponent', env: 'Server', owner: null}] + : 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( __DEV__ - ? [{name: 'ThirdPartyComponent', env: 'third-party'}] + ? [{name: 'ThirdPartyComponent', env: 'third-party', owner: null}] : undefined, ); expect(thirdPartyChildren[1]._debugInfo).toEqual( __DEV__ - ? [{name: 'ThirdPartyLazyComponent', env: 'third-party'}] + ? [{name: 'ThirdPartyLazyComponent', env: 'third-party', owner: null}] : undefined, ); ReactNoop.render(result); @@ -2172,9 +2174,10 @@ 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([ - {name: 'Greeting', env: 'Server'}, - {name: 'Container', env: 'Server'}, + greetInfo, + {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-devtools-shared/src/__tests__/componentStacks-test.js b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js index 3d17546e39d19..02e2ad6802f61 100644 --- a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js +++ b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js @@ -101,6 +101,7 @@ describe('component stack', () => { { name: 'ServerComponent', env: 'Server', + owner: null, }, ]; const Parent = () => ChildPromise; 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 cce874d8148db..8304d9927d372 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -290,7 +290,7 @@ describe('ReactFlightDOMEdge', () => { , ); const serializedContent = await readResult(stream); - const expectedDebugInfoSize = __DEV__ ? 42 * 20 : 0; + const expectedDebugInfoSize = __DEV__ ? 64 * 20 : 0; expect(serializedContent.length).toBeLessThan(150 + expectedDebugInfoSize); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 66d7cc8c04ff1..f001036093980 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -596,6 +596,7 @@ function renderFunctionComponent( key: null | string, Component: (p: Props, arg: void) => any, props: Props, + owner: null | ReactComponentInfo, ): 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 @@ -624,6 +625,7 @@ function renderFunctionComponent( componentDebugInfo = { name: componentName, env: request.environmentName, + owner: owner, }; // 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 @@ -832,7 +834,7 @@ function renderElement( return renderClientElement(task, type, key, props, owner); } // This is a Server Component. - return renderFunctionComponent(request, task, key, type, props); + return renderFunctionComponent(request, task, key, type, props, owner); } else if (typeof type === 'string') { // This is a host element. E.g. HTML. return renderClientElement(task, type, key, props, owner); @@ -878,7 +880,14 @@ function renderElement( ); } case REACT_FORWARD_REF_TYPE: { - return renderFunctionComponent(request, task, key, type.render, props); + return renderFunctionComponent( + request, + task, + key, + type.render, + props, + owner, + ); } case REACT_MEMO_TYPE: { return renderElement(request, task, type.type, key, ref, props, owner); diff --git a/packages/react/src/__tests__/ReactFetch-test.js b/packages/react/src/__tests__/ReactFetch-test.js index cd658066c8ca6..e612bdf6a9780 100644 --- a/packages/react/src/__tests__/ReactFetch-test.js +++ b/packages/react/src/__tests__/ReactFetch-test.js @@ -86,7 +86,7 @@ describe('ReactFetch', () => { const promise = render(Component); expect(await promise).toMatchInlineSnapshot(`"GET world []"`); expect(promise._debugInfo).toEqual( - __DEV__ ? [{name: 'Component', env: 'Server'}] : undefined, + __DEV__ ? [{name: 'Component', env: 'Server', owner: null}] : undefined, ); expect(fetchCount).toBe(1); }); diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 59f3362e0cf8c..1c2ea36054676 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -181,6 +181,7 @@ export type Awaited = T extends null | void export type ReactComponentInfo = { +name?: string, +env?: string, + +owner?: null | ReactComponentInfo, }; export type ReactAsyncInfo = { From d8e167a555ff9ba2004974f949843b0da29a2f6b Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 4 Apr 2024 18:19:15 -0400 Subject: [PATCH 6/7] Since debugOwner can now be a non-Fiber, fix up places that assumes it is I left DevTools and ReactNativeFiberInspector to ignore them for now. --- .../backend/DevToolsComponentStackFrame.js | 22 ++----- .../backend/DevToolsFiberComponentStack.js | 16 ++--- .../src/backend/renderer.js | 53 ++++++++++------ .../src/ReactNativeFiberInspector.js | 26 +++++--- .../react-reconciler/src/ReactCurrentFiber.js | 6 +- packages/react-reconciler/src/ReactFiber.js | 7 ++- .../src/ReactFiberComponentStack.js | 19 +++--- .../src/ReactInternalTypes.js | 3 +- .../src/getComponentNameFromFiber.js | 13 ++++ .../src/ReactFizzComponentStack.js | 6 +- packages/react/src/jsx/ReactJSXElement.js | 12 ++-- packages/shared/ReactComponentStackFrame.js | 61 ++++++------------- 12 files changed, 119 insertions(+), 125 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js b/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js index 0e412a43f74c3..316245492c64f 100644 --- a/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js +++ b/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js @@ -33,10 +33,7 @@ import { import {disableLogs, reenableLogs} from './DevToolsConsolePatching'; let prefix; -export function describeBuiltInComponentFrame( - name: string, - ownerFn: void | null | Function, -): string { +export function describeBuiltInComponentFrame(name: string): string { if (prefix === undefined) { // Extract the VM specific prefix used by each line. try { @@ -51,10 +48,7 @@ export function describeBuiltInComponentFrame( } export function describeDebugInfoFrame(name: string, env: ?string): string { - return describeBuiltInComponentFrame( - name + (env ? ' (' + env + ')' : ''), - null, - ); + return describeBuiltInComponentFrame(name + (env ? ' (' + env + ')' : '')); } let reentry = false; @@ -292,7 +286,6 @@ export function describeNativeComponentFrame( export function describeClassComponentFrame( ctor: Function, - ownerFn: void | null | Function, currentDispatcherRef: CurrentDispatcherRef, ): string { return describeNativeComponentFrame(ctor, true, currentDispatcherRef); @@ -300,7 +293,6 @@ export function describeClassComponentFrame( export function describeFunctionComponentFrame( fn: Function, - ownerFn: void | null | Function, currentDispatcherRef: CurrentDispatcherRef, ): string { return describeNativeComponentFrame(fn, false, currentDispatcherRef); @@ -313,7 +305,6 @@ function shouldConstruct(Component: Function) { export function describeUnknownElementTypeFrameInDEV( type: any, - ownerFn: void | null | Function, currentDispatcherRef: CurrentDispatcherRef, ): string { if (!__DEV__) { @@ -330,15 +321,15 @@ export function describeUnknownElementTypeFrameInDEV( ); } if (typeof type === 'string') { - return describeBuiltInComponentFrame(type, ownerFn); + return describeBuiltInComponentFrame(type); } switch (type) { case SUSPENSE_NUMBER: case SUSPENSE_SYMBOL_STRING: - return describeBuiltInComponentFrame('Suspense', ownerFn); + return describeBuiltInComponentFrame('Suspense'); case SUSPENSE_LIST_NUMBER: case SUSPENSE_LIST_SYMBOL_STRING: - return describeBuiltInComponentFrame('SuspenseList', ownerFn); + return describeBuiltInComponentFrame('SuspenseList'); } if (typeof type === 'object') { switch (type.$$typeof) { @@ -346,7 +337,6 @@ export function describeUnknownElementTypeFrameInDEV( case FORWARD_REF_SYMBOL_STRING: return describeFunctionComponentFrame( type.render, - ownerFn, currentDispatcherRef, ); case MEMO_NUMBER: @@ -354,7 +344,6 @@ export function describeUnknownElementTypeFrameInDEV( // Memo may contain any component type so we recursively resolve it. return describeUnknownElementTypeFrameInDEV( type.type, - ownerFn, currentDispatcherRef, ); case LAZY_NUMBER: @@ -366,7 +355,6 @@ export function describeUnknownElementTypeFrameInDEV( // Lazy may contain any component type so we recursively resolve it. return describeUnknownElementTypeFrameInDEV( init(payload), - ownerFn, currentDispatcherRef, ); } catch (x) {} diff --git a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js index 77f0adf37ea4e..6d99cf4f21e52 100644 --- a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js +++ b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js @@ -39,38 +39,30 @@ export function describeFiber( ClassComponent, } = workTagMap; - const owner: null | Function = __DEV__ - ? workInProgress._debugOwner - ? workInProgress._debugOwner.type - : null - : null; switch (workInProgress.tag) { case HostComponent: - return describeBuiltInComponentFrame(workInProgress.type, owner); + return describeBuiltInComponentFrame(workInProgress.type); case LazyComponent: - return describeBuiltInComponentFrame('Lazy', owner); + return describeBuiltInComponentFrame('Lazy'); case SuspenseComponent: - return describeBuiltInComponentFrame('Suspense', owner); + return describeBuiltInComponentFrame('Suspense'); case SuspenseListComponent: - return describeBuiltInComponentFrame('SuspenseList', owner); + return describeBuiltInComponentFrame('SuspenseList'); case FunctionComponent: case IndeterminateComponent: case SimpleMemoComponent: return describeFunctionComponentFrame( workInProgress.type, - owner, currentDispatcherRef, ); case ForwardRef: return describeFunctionComponentFrame( workInProgress.type.render, - owner, currentDispatcherRef, ); case ClassComponent: return describeClassComponentFrame( workInProgress.type, - owner, currentDispatcherRef, ); default: diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 803ef4b76b549..1f333d1271d67 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -1952,15 +1952,24 @@ export function attach( const {key} = fiber; const displayName = getDisplayNameForFiber(fiber); const elementType = getElementTypeForFiber(fiber); - const {_debugOwner} = fiber; + const debugOwner = fiber._debugOwner; // Ideally we should call getFiberIDThrows() for _debugOwner, // since owners are almost always higher in the tree (and so have already been processed), // but in some (rare) instances reported in open source, a descendant mounts before an owner. // Since this is a DEV only field it's probably okay to also just lazily generate and ID here if needed. // See https://github.com/facebook/react/issues/21445 - const ownerID = - _debugOwner != null ? getOrGenerateFiberID(_debugOwner) : 0; + let ownerID: number; + if (debugOwner != null) { + if (typeof debugOwner.tag === 'number') { + ownerID = getOrGenerateFiberID((debugOwner: any)); + } else { + // TODO: Track Server Component Owners. + ownerID = 0; + } + } else { + ownerID = 0; + } const parentID = parentFiber ? getFiberIDThrows(parentFiber) : 0; const displayNameStringID = getStringID(displayName); @@ -3104,15 +3113,17 @@ export function attach( return null; } - const {_debugOwner} = fiber; - const owners: Array = [fiberToSerializedElement(fiber)]; - if (_debugOwner) { - let owner: null | Fiber = _debugOwner; - while (owner !== null) { - owners.unshift(fiberToSerializedElement(owner)); - owner = owner._debugOwner || null; + let owner = fiber._debugOwner; + while (owner != null) { + if (typeof owner.tag === 'number') { + const ownerFiber: Fiber = (owner: any); // Refined + owners.unshift(fiberToSerializedElement(ownerFiber)); + owner = ownerFiber._debugOwner; + } else { + // TODO: Track Server Component Owners. + break; } } @@ -3173,7 +3184,7 @@ export function attach( } const { - _debugOwner, + _debugOwner: debugOwner, stateNode, key, memoizedProps, @@ -3300,13 +3311,19 @@ export function attach( context = {value: context}; } - let owners = null; - if (_debugOwner) { - owners = ([]: Array); - let owner: null | Fiber = _debugOwner; - while (owner !== null) { - owners.push(fiberToSerializedElement(owner)); - owner = owner._debugOwner || null; + let owners: null | Array = null; + let owner = debugOwner; + while (owner != null) { + if (typeof owner.tag === 'number') { + const ownerFiber: Fiber = (owner: any); // Refined + if (owners === null) { + owners = []; + } + owners.push(fiberToSerializedElement(ownerFiber)); + owner = ownerFiber._debugOwner; + } else { + // TODO: Track Server Component Owners. + break; } } diff --git a/packages/react-native-renderer/src/ReactNativeFiberInspector.js b/packages/react-native-renderer/src/ReactNativeFiberInspector.js index f30012b2cf917..14d87f7a5502b 100644 --- a/packages/react-native-renderer/src/ReactNativeFiberInspector.js +++ b/packages/react-native-renderer/src/ReactNativeFiberInspector.js @@ -103,13 +103,21 @@ function getInspectorDataForInstance( } const fiber = findCurrentFiberUsingSlowPath(closestInstance); + if (fiber === null) { + // Might not be currently mounted. + return { + hierarchy: [], + props: emptyObject, + selectedIndex: null, + componentStack: '', + }; + } const fiberHierarchy = getOwnerHierarchy(fiber); const instance = lastNonHostInstance(fiberHierarchy); const hierarchy = createHierarchy(fiberHierarchy); const props = getHostProps(instance); const selectedIndex = fiberHierarchy.indexOf(instance); - const componentStack = - fiber !== null ? getStackByFiberInDevAndProd(fiber) : ''; + const componentStack = getStackByFiberInDevAndProd(fiber); return { closestInstance: instance, @@ -125,7 +133,7 @@ function getInspectorDataForInstance( ); } -function getOwnerHierarchy(instance: any) { +function getOwnerHierarchy(instance: Fiber) { const hierarchy: Array<$FlowFixMe> = []; traverseOwnerTreeUp(hierarchy, instance); return hierarchy; @@ -143,15 +151,17 @@ function lastNonHostInstance(hierarchy) { return hierarchy[0]; } -// $FlowFixMe[missing-local-annot] function traverseOwnerTreeUp( hierarchy: Array<$FlowFixMe>, - instance: any, + instance: Fiber, ): void { if (__DEV__ || enableGetInspectorDataForInstanceInProduction) { - if (instance) { - hierarchy.unshift(instance); - traverseOwnerTreeUp(hierarchy, instance._debugOwner); + hierarchy.unshift(instance); + const owner = instance._debugOwner; + if (owner != null && typeof owner.tag === 'number') { + traverseOwnerTreeUp(hierarchy, (owner: any)); + } else { + // TODO: Traverse Server Components owners. } } } diff --git a/packages/react-reconciler/src/ReactCurrentFiber.js b/packages/react-reconciler/src/ReactCurrentFiber.js index 6c283ed0d35f4..c52d55c0cce73 100644 --- a/packages/react-reconciler/src/ReactCurrentFiber.js +++ b/packages/react-reconciler/src/ReactCurrentFiber.js @@ -11,7 +11,7 @@ import type {Fiber} from './ReactInternalTypes'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack'; -import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; +import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber'; const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; @@ -24,8 +24,8 @@ export function getCurrentFiberOwnerNameInDevOrNull(): string | null { return null; } const owner = current._debugOwner; - if (owner !== null && typeof owner !== 'undefined') { - return getComponentNameFromFiber(owner); + if (owner != null) { + return getComponentNameFromOwner(owner); } } return null; diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 7b1a6514ebd5f..1de7d42cfe7d0 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -68,7 +68,7 @@ import { TracingMarkerComponent, } from './ReactWorkTags'; import {OffscreenVisible} from './ReactFiberActivityComponent'; -import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber'; +import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber'; import {isDevToolsPresent} from './ReactFiberDevToolsHook'; import { resolveClassForHotReloading, @@ -110,6 +110,7 @@ import { attachOffscreenInstance, } from './ReactFiberCommitWork'; import {getHostContext} from './ReactFiberHostContext'; +import type {ReactComponentInfo} from '../../shared/ReactTypes'; export type {Fiber}; @@ -475,7 +476,7 @@ export function createFiberFromTypeAndProps( type: any, // React$ElementType key: null | string, pendingProps: any, - owner: null | Fiber, + owner: null | ReactComponentInfo | Fiber, mode: TypeOfMode, lanes: Lanes, ): Fiber { @@ -610,7 +611,7 @@ export function createFiberFromTypeAndProps( "it's defined in, or you might have mixed up default and " + 'named imports.'; } - const ownerName = owner ? getComponentNameFromFiber(owner) : null; + const ownerName = owner ? getComponentNameFromOwner(owner) : null; if (ownerName) { info += '\n\nCheck the render method of `' + ownerName + '`.'; } diff --git a/packages/react-reconciler/src/ReactFiberComponentStack.js b/packages/react-reconciler/src/ReactFiberComponentStack.js index f292cb51d10b4..193c27aef1e1a 100644 --- a/packages/react-reconciler/src/ReactFiberComponentStack.js +++ b/packages/react-reconciler/src/ReactFiberComponentStack.js @@ -29,29 +29,24 @@ import { } from 'shared/ReactComponentStackFrame'; function describeFiber(fiber: Fiber): string { - const owner: null | Function = __DEV__ - ? fiber._debugOwner - ? fiber._debugOwner.type - : null - : null; switch (fiber.tag) { case HostHoistable: case HostSingleton: case HostComponent: - return describeBuiltInComponentFrame(fiber.type, owner); + return describeBuiltInComponentFrame(fiber.type); case LazyComponent: - return describeBuiltInComponentFrame('Lazy', owner); + return describeBuiltInComponentFrame('Lazy'); case SuspenseComponent: - return describeBuiltInComponentFrame('Suspense', owner); + return describeBuiltInComponentFrame('Suspense'); case SuspenseListComponent: - return describeBuiltInComponentFrame('SuspenseList', owner); + return describeBuiltInComponentFrame('SuspenseList'); case FunctionComponent: case SimpleMemoComponent: - return describeFunctionComponentFrame(fiber.type, owner); + return describeFunctionComponentFrame(fiber.type); case ForwardRef: - return describeFunctionComponentFrame(fiber.type.render, owner); + return describeFunctionComponentFrame(fiber.type.render); case ClassComponent: - return describeClassComponentFrame(fiber.type, owner); + return describeClassComponentFrame(fiber.type); default: return ''; } diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index 3b82dc40c828b..f12b9a16c570b 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -15,6 +15,7 @@ import type { Usable, ReactFormState, Awaited, + ReactComponentInfo, ReactDebugInfo, } from 'shared/ReactTypes'; import type {WorkTag} from './ReactWorkTags'; @@ -193,7 +194,7 @@ export type Fiber = { // __DEV__ only _debugInfo?: ReactDebugInfo | null, - _debugOwner?: Fiber | null, + _debugOwner?: ReactComponentInfo | Fiber | null, _debugIsCurrentlyTiming?: boolean, _debugNeedsRemount?: boolean, diff --git a/packages/react-reconciler/src/getComponentNameFromFiber.js b/packages/react-reconciler/src/getComponentNameFromFiber.js index 6659b14b7f0d0..332324900fa88 100644 --- a/packages/react-reconciler/src/getComponentNameFromFiber.js +++ b/packages/react-reconciler/src/getComponentNameFromFiber.js @@ -47,6 +47,7 @@ import { } from 'react-reconciler/src/ReactWorkTags'; import getComponentNameFromType from 'shared/getComponentNameFromType'; import {REACT_STRICT_MODE_TYPE} from 'shared/ReactSymbols'; +import type {ReactComponentInfo} from '../../shared/ReactTypes'; // Keep in sync with shared/getComponentNameFromType function getWrappedName( @@ -66,6 +67,18 @@ function getContextName(type: ReactContext) { return type.displayName || 'Context'; } +export function getComponentNameFromOwner( + owner: Fiber | ReactComponentInfo, +): string | null { + if (typeof owner.tag === 'number') { + return getComponentNameFromFiber((owner: any)); + } + if (typeof owner.name === 'string') { + return owner.name; + } + return null; +} + export default function getComponentNameFromFiber(fiber: Fiber): string | null { const {tag, type} = fiber; switch (tag) { diff --git a/packages/react-server/src/ReactFizzComponentStack.js b/packages/react-server/src/ReactFizzComponentStack.js index 7aeb6b0802c92..84b4d82d8d45f 100644 --- a/packages/react-server/src/ReactFizzComponentStack.js +++ b/packages/react-server/src/ReactFizzComponentStack.js @@ -43,13 +43,13 @@ export function getStackByComponentStackNode( do { switch (node.tag) { case 0: - info += describeBuiltInComponentFrame(node.type, null); + info += describeBuiltInComponentFrame(node.type); break; case 1: - info += describeFunctionComponentFrame(node.type, null); + info += describeFunctionComponentFrame(node.type); break; case 2: - info += describeClassComponentFrame(node.type, null); + info += describeClassComponentFrame(node.type); break; } // $FlowFixMe[incompatible-type] we bail out when we get a null diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js index d20c1d4a25b56..0b8fb2f149c1e 100644 --- a/packages/react/src/jsx/ReactJSXElement.js +++ b/packages/react/src/jsx/ReactJSXElement.js @@ -1008,13 +1008,17 @@ function validateExplicitKey(element, parentType) { let childOwner = ''; if ( element && - element._owner && + element._owner != null && element._owner !== ReactCurrentOwner.current ) { + let ownerName = null; + if (typeof element._owner.tag === 'number') { + ownerName = getComponentNameFromType(element._owner.type); + } else if (typeof element._owner.name === 'string') { + ownerName = element._owner.name; + } // Give the component that originally created this child. - childOwner = ` It was passed a child from ${getComponentNameFromType( - element._owner.type, - )}.`; + childOwner = ` It was passed a child from ${ownerName}.`; } setCurrentlyValidatingElement(element); diff --git a/packages/shared/ReactComponentStackFrame.js b/packages/shared/ReactComponentStackFrame.js index c928f191d7b79..b103b760e9954 100644 --- a/packages/shared/ReactComponentStackFrame.js +++ b/packages/shared/ReactComponentStackFrame.js @@ -26,10 +26,7 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; const {ReactCurrentDispatcher} = ReactSharedInternals; let prefix; -export function describeBuiltInComponentFrame( - name: string, - ownerFn: void | null | Function, -): string { +export function describeBuiltInComponentFrame(name: string): string { if (enableComponentStackLocations) { if (prefix === undefined) { // Extract the VM specific prefix used by each line. @@ -43,19 +40,12 @@ export function describeBuiltInComponentFrame( // We use the prefix to ensure our stacks line up with native stack frames. return '\n' + prefix + name; } else { - let ownerName = null; - if (__DEV__ && ownerFn) { - ownerName = ownerFn.displayName || ownerFn.name || null; - } - return describeComponentFrame(name, ownerName); + return describeComponentFrame(name); } } export function describeDebugInfoFrame(name: string, env: ?string): string { - return describeBuiltInComponentFrame( - name + (env ? ' (' + env + ')' : ''), - null, - ); + return describeBuiltInComponentFrame(name + (env ? ' (' + env + ')' : '')); } let reentry = false; @@ -298,29 +288,19 @@ export function describeNativeComponentFrame( return syntheticFrame; } -function describeComponentFrame(name: null | string, ownerName: null | string) { - let sourceInfo = ''; - if (ownerName) { - sourceInfo = ' (created by ' + ownerName + ')'; - } - return '\n in ' + (name || 'Unknown') + sourceInfo; +function describeComponentFrame(name: null | string) { + return '\n in ' + (name || 'Unknown'); } -export function describeClassComponentFrame( - ctor: Function, - ownerFn: void | null | Function, -): string { +export function describeClassComponentFrame(ctor: Function): string { if (enableComponentStackLocations) { return describeNativeComponentFrame(ctor, true); } else { - return describeFunctionComponentFrame(ctor, ownerFn); + return describeFunctionComponentFrame(ctor); } } -export function describeFunctionComponentFrame( - fn: Function, - ownerFn: void | null | Function, -): string { +export function describeFunctionComponentFrame(fn: Function): string { if (enableComponentStackLocations) { return describeNativeComponentFrame(fn, false); } else { @@ -328,11 +308,7 @@ export function describeFunctionComponentFrame( return ''; } const name = fn.displayName || fn.name || null; - let ownerName = null; - if (__DEV__ && ownerFn) { - ownerName = ownerFn.displayName || ownerFn.name || null; - } - return describeComponentFrame(name, ownerName); + return describeComponentFrame(name); } } @@ -341,10 +317,7 @@ function shouldConstruct(Component: Function) { return !!(prototype && prototype.isReactComponent); } -export function describeUnknownElementTypeFrameInDEV( - type: any, - ownerFn: void | null | Function, -): string { +export function describeUnknownElementTypeFrameInDEV(type: any): string { if (!__DEV__) { return ''; } @@ -355,32 +328,32 @@ export function describeUnknownElementTypeFrameInDEV( if (enableComponentStackLocations) { return describeNativeComponentFrame(type, shouldConstruct(type)); } else { - return describeFunctionComponentFrame(type, ownerFn); + return describeFunctionComponentFrame(type); } } if (typeof type === 'string') { - return describeBuiltInComponentFrame(type, ownerFn); + return describeBuiltInComponentFrame(type); } switch (type) { case REACT_SUSPENSE_TYPE: - return describeBuiltInComponentFrame('Suspense', ownerFn); + return describeBuiltInComponentFrame('Suspense'); case REACT_SUSPENSE_LIST_TYPE: - return describeBuiltInComponentFrame('SuspenseList', ownerFn); + return describeBuiltInComponentFrame('SuspenseList'); } if (typeof type === 'object') { switch (type.$$typeof) { case REACT_FORWARD_REF_TYPE: - return describeFunctionComponentFrame(type.render, ownerFn); + return describeFunctionComponentFrame(type.render); case REACT_MEMO_TYPE: // Memo may contain any component type so we recursively resolve it. - return describeUnknownElementTypeFrameInDEV(type.type, ownerFn); + return describeUnknownElementTypeFrameInDEV(type.type); case REACT_LAZY_TYPE: { const lazyComponent: LazyComponent = (type: any); const payload = lazyComponent._payload; const init = lazyComponent._init; try { // Lazy may contain any component type so we recursively resolve it. - return describeUnknownElementTypeFrameInDEV(init(payload), ownerFn); + return describeUnknownElementTypeFrameInDEV(init(payload)); } catch (x) {} } } From 79901a41af2ba82e2a1628a989ac0cd8f2360cb4 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 4 Apr 2024 21:52:53 -0400 Subject: [PATCH 7/7] Track owner of console logs That way we can also restore owner stack traces for those logs and associate the log with a particular component in DevTools. --- packages/react-client/src/ReactFlightClient.js | 8 +++++--- packages/react-server/src/ReactFlightServer.js | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 915a387835265..c0cec0db366cf 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1138,12 +1138,14 @@ function resolveConsoleEntry( ); } - const payload: [string, string, string, mixed] = parseModel(response, value); + const payload: [string, string, null | ReactComponentInfo, string, mixed] = + parseModel(response, value); const methodName = payload[0]; // TODO: Restore the fake stack before logging. // const stackTrace = payload[1]; - const env = payload[2]; - const args = payload.slice(3); + // const owner = payload[2]; + const env = payload[3]; + const args = payload.slice(4); printToConsole(methodName, args, env); } diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index f001036093980..a8c648881fe2c 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -154,7 +154,8 @@ function patchConsole(consoleInst: typeof console, methodName: string) { // We don't currently use this id for anything but we emit it so that we can later // refer to previous logs in debug info to associate them with a component. const id = request.nextChunkId++; - emitConsoleChunk(request, id, methodName, stack, arguments); + const owner: null | ReactComponentInfo = ReactCurrentOwner.current; + emitConsoleChunk(request, id, methodName, owner, stack, arguments); } // $FlowFixMe[prop-missing] return originalMethod.apply(this, arguments); @@ -2272,6 +2273,7 @@ function emitConsoleChunk( request: Request, id: number, methodName: string, + owner: null | ReactComponentInfo, stackTrace: string, args: Array, ): void { @@ -2306,7 +2308,7 @@ function emitConsoleChunk( // TODO: Don't double badge if this log came from another Flight Client. const env = request.environmentName; - const payload = [methodName, stackTrace, env]; + const payload = [methodName, stackTrace, owner, env]; // $FlowFixMe[method-unbinding] payload.push.apply(payload, args); // $FlowFixMe[incompatible-type] stringify can return null