Skip to content

Commit 1c3825f

Browse files
committed
Move getComponentStack computation into renderer
The console patching shouldn't need to know about Fibers.
1 parent f97f19f commit 1c3825f

File tree

3 files changed

+102
-65
lines changed

3 files changed

+102
-65
lines changed

packages/react-devtools-shared/src/__tests__/console-test.js

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,6 @@ describe('console', () => {
5454
fakeConsole,
5555
);
5656

57-
const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
58-
global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => {
59-
rendererID = inject(internals);
60-
61-
Console.registerRenderer(internals);
62-
return rendererID;
63-
};
64-
6557
React = require('react');
6658
if (
6759
React.version.startsWith('19') &&
@@ -1100,9 +1092,18 @@ describe('console error', () => {
11001092
global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => {
11011093
inject(internals);
11021094

1103-
Console.registerRenderer(internals, () => {
1104-
throw Error('foo');
1105-
});
1095+
Console.registerRenderer(
1096+
internals,
1097+
() => {
1098+
throw Error('foo');
1099+
},
1100+
() => {
1101+
return {
1102+
enableOwnerStacks: true,
1103+
componentStack: '\n at FakeStack (fake-file)',
1104+
};
1105+
},
1106+
);
11061107
};
11071108

11081109
React = require('react');
@@ -1146,14 +1147,14 @@ describe('console error', () => {
11461147
expect(mockWarn.mock.calls[0][0]).toBe('warn');
11471148
// An error in showInlineWarningsAndErrors doesn't need to break component stacks.
11481149
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
1149-
'\n in App (at **)',
1150+
'\n in FakeStack (at **)',
11501151
);
11511152

11521153
expect(mockError).toHaveBeenCalledTimes(1);
11531154
expect(mockError.mock.calls[0]).toHaveLength(2);
11541155
expect(mockError.mock.calls[0][0]).toBe('error');
11551156
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
1156-
'\n in App (at **)',
1157+
'\n in FakeStack (at **)',
11571158
);
11581159
});
11591160
});

packages/react-devtools-shared/src/backend/console.js

Lines changed: 30 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,7 @@ import {
2525
ANSI_STYLE_DIMMING_TEMPLATE,
2626
ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK,
2727
} from 'react-devtools-shared/src/constants';
28-
import {getInternalReactConstants, getDispatcherRef} from './fiber/renderer';
29-
import {
30-
getStackByFiberInDevAndProd,
31-
getOwnerStackByFiberInDev,
32-
supportsOwnerStacks,
33-
supportsConsoleTasks,
34-
} from './fiber/DevToolsFiberComponentStack';
35-
import {formatOwnerStack} from './shared/DevToolsOwnerStack';
28+
import {getInternalReactConstants} from './fiber/renderer';
3629
import {castBool, castBrowserTheme} from '../utils';
3730

3831
const OVERRIDE_CONSOLE_METHODS = ['error', 'trace', 'warn'];
@@ -91,6 +84,9 @@ function restorePotentiallyModifiedArgs(args: Array<any>): Array<any> {
9184
}
9285

9386
type OnErrorOrWarning = (type: 'error' | 'warn', args: Array<any>) => void;
87+
type GetComponentStack = (
88+
topFrame: Error,
89+
) => null | {enableOwnerStacks: boolean, componentStack: string};
9490

9591
const injectedRenderers: Map<
9692
ReactRenderer,
@@ -99,6 +95,7 @@ const injectedRenderers: Map<
9995
getCurrentFiber: () => Fiber | null,
10096
onErrorOrWarning: ?OnErrorOrWarning,
10197
workTagMap: WorkTagMap,
98+
getComponentStack: ?GetComponentStack,
10299
},
103100
> = new Map();
104101

@@ -130,6 +127,7 @@ export function dangerous_setTargetConsoleForTesting(
130127
export function registerRenderer(
131128
renderer: ReactRenderer,
132129
onErrorOrWarning?: OnErrorOrWarning,
130+
getComponentStack?: GetComponentStack,
133131
): void {
134132
const {currentDispatcherRef, getCurrentFiber, version} = renderer;
135133

@@ -143,6 +141,7 @@ export function registerRenderer(
143141
getCurrentFiber,
144142
workTagMap: ReactTypeOfWork,
145143
onErrorOrWarning,
144+
getComponentStack,
146145
});
147146
}
148147
}
@@ -217,13 +216,12 @@ export function patch({
217216
// We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
218217
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
219218
for (const renderer of injectedRenderers.values()) {
220-
const currentDispatcherRef = getDispatcherRef(renderer);
221-
const {getCurrentFiber, onErrorOrWarning, workTagMap} = renderer;
219+
const {getComponentStack, onErrorOrWarning} = renderer;
222220
try {
223221
if (shouldShowInlineWarningsAndErrors) {
224222
// patch() is called by two places: (1) the hook and (2) the renderer backend.
225223
// The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
226-
if (typeof onErrorOrWarning === 'function') {
224+
if (onErrorOrWarning != null) {
227225
onErrorOrWarning(
228226
((method: any): 'error' | 'warn'),
229227
// Restore and copy args before we mutate them (e.g. adding the component stack)
@@ -237,46 +235,26 @@ export function patch({
237235
throw error;
238236
}, 0);
239237
}
240-
const current: ?Fiber = getCurrentFiber();
241-
if (current != null) {
242-
try {
243-
if (
244-
consoleSettingsRef.appendComponentStack &&
245-
!supportsConsoleTasks(current)
246-
) {
247-
const enableOwnerStacks = supportsOwnerStacks(current);
248-
let componentStack = '';
249-
if (enableOwnerStacks) {
250-
// Prefix the owner stack with the current stack. I.e. what called
251-
// console.error. While this will also be part of the native stack,
252-
// it is hidden and not presented alongside this argument so we print
253-
// them all together.
254-
const topStackFrames = formatOwnerStack(
255-
new Error('react-stack-top-frame'),
256-
);
257-
if (topStackFrames) {
258-
componentStack += '\n' + topStackFrames;
259-
}
260-
componentStack += getOwnerStackByFiberInDev(
261-
workTagMap,
262-
current,
263-
(currentDispatcherRef: any),
264-
);
265-
} else {
266-
componentStack = getStackByFiberInDevAndProd(
267-
workTagMap,
268-
current,
269-
(currentDispatcherRef: any),
270-
);
271-
}
238+
try {
239+
if (
240+
consoleSettingsRef.appendComponentStack &&
241+
getComponentStack != null
242+
) {
243+
// This needs to be directly in the wrapper so we can pop exactly one frame.
244+
const topFrame = Error('react-stack-top-frame');
245+
const match = getComponentStack(topFrame);
246+
if (match !== null) {
247+
const {enableOwnerStacks, componentStack} = match;
248+
// Empty string means we have a match but no component stack.
249+
// We don't need to look in other renderers but we also don't add anything.
272250
if (componentStack !== '') {
273251
// Create a fake Error so that when we print it we get native source maps. Every
274252
// browser will print the .stack property of the error and then parse it back for source
275253
// mapping. Rather than print the internal slot. So it doesn't matter that the internal
276254
// slot doesn't line up.
277255
const fakeError = new Error('');
278256
// In Chromium, only the stack property is printed but in Firefox the <name>:<message>
279-
// gets printed so to make the colon make sense, we name it so we print Component Stack:
257+
// gets printed so to make the colon make sense, we name it so we print Stack:
280258
// and similarly Safari leave an expandable slot.
281259
fakeError.name = enableOwnerStacks
282260
? 'Stack'
@@ -290,6 +268,7 @@ export function patch({
290268
? 'Error Stack:'
291269
: 'Error Component Stack:') + componentStack
292270
: componentStack;
271+
293272
if (alreadyHasComponentStack) {
294273
// Only modify the component stack if it matches what we would've added anyway.
295274
// Otherwise we assume it was a non-React stack.
@@ -325,15 +304,15 @@ export function patch({
325304
}
326305
}
327306
}
307+
// Don't add stacks from other renderers.
308+
break;
328309
}
329-
} catch (error) {
330-
// Don't let a DevTools or React internal error interfere with logging.
331-
setTimeout(() => {
332-
throw error;
333-
}, 0);
334-
} finally {
335-
break;
336310
}
311+
} catch (error) {
312+
// Don't let a DevTools or React internal error interfere with logging.
313+
setTimeout(() => {
314+
throw error;
315+
}, 0);
337316
}
338317
}
339318

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ import {enableStyleXFeatures} from 'react-devtools-feature-flags';
103103
import is from 'shared/objectIs';
104104
import hasOwnProperty from 'shared/hasOwnProperty';
105105

106+
import {
107+
getStackByFiberInDevAndProd,
108+
getOwnerStackByFiberInDev,
109+
supportsOwnerStacks,
110+
supportsConsoleTasks,
111+
} from './DevToolsFiberComponentStack';
112+
106113
// $FlowFixMe[method-unbinding]
107114
const toString = Object.prototype.toString;
108115

@@ -1063,6 +1070,56 @@ export function attach(
10631070
}
10641071
}
10651072

1073+
function getComponentStack(
1074+
topFrame: Error,
1075+
): null | {enableOwnerStacks: boolean, componentStack: string} {
1076+
if (getCurrentFiber === undefined) {
1077+
// Expected this to be part of the renderer. Ignore.
1078+
return null;
1079+
}
1080+
const current = getCurrentFiber();
1081+
if (current === null) {
1082+
// Outside of our render scope.
1083+
return null;
1084+
}
1085+
1086+
if (supportsConsoleTasks(current)) {
1087+
// This will be handled natively by console.createTask. No need for
1088+
// DevTools to add it.
1089+
return null;
1090+
}
1091+
1092+
const dispatcherRef = getDispatcherRef(renderer);
1093+
if (dispatcherRef === undefined) {
1094+
return null;
1095+
}
1096+
1097+
const enableOwnerStacks = supportsOwnerStacks(current);
1098+
let componentStack = '';
1099+
if (enableOwnerStacks) {
1100+
// Prefix the owner stack with the current stack. I.e. what called
1101+
// console.error. While this will also be part of the native stack,
1102+
// it is hidden and not presented alongside this argument so we print
1103+
// them all together.
1104+
const topStackFrames = formatOwnerStack(topFrame);
1105+
if (topStackFrames) {
1106+
componentStack += '\n' + topStackFrames;
1107+
}
1108+
componentStack += getOwnerStackByFiberInDev(
1109+
ReactTypeOfWork,
1110+
current,
1111+
dispatcherRef,
1112+
);
1113+
} else {
1114+
componentStack = getStackByFiberInDevAndProd(
1115+
ReactTypeOfWork,
1116+
current,
1117+
dispatcherRef,
1118+
);
1119+
}
1120+
return {enableOwnerStacks, componentStack};
1121+
}
1122+
10661123
// Called when an error or warning is logged during render, commit, or passive (including unmount functions).
10671124
function onErrorOrWarning(
10681125
type: 'error' | 'warn',
@@ -1128,7 +1185,7 @@ export function attach(
11281185
// Patching the console enables DevTools to do a few useful things:
11291186
// * Append component stacks to warnings and error messages
11301187
// * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
1131-
registerRendererWithConsole(renderer, onErrorOrWarning);
1188+
registerRendererWithConsole(renderer, onErrorOrWarning, getComponentStack);
11321189

11331190
// The renderer interface can't read these preferences directly,
11341191
// because it is stored in localStorage within the context of the extension.

0 commit comments

Comments
 (0)