Skip to content

Commit 0dbacf2

Browse files
authored
[DevTools] Improve Layering Between Console and Renderer (#30925)
The console instrumentation should not know about things like Fibers. Only the renderer bindings should know about that stuff. We can improve the layering by just moving all that stuff behind a `getComponentStack` helper that gets injected by the renderer. This sets us up for the Flight renderer #30906 to have its own implementation of this function.
1 parent fa3cf50 commit 0dbacf2

5 files changed

Lines changed: 145 additions & 137 deletions

File tree

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

Lines changed: 20 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,17 @@ 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+
() => {
1097+
throw Error('foo');
1098+
},
1099+
() => {
1100+
return {
1101+
enableOwnerStacks: true,
1102+
componentStack: '\n at FakeStack (fake-file)',
1103+
};
1104+
},
1105+
);
11061106
};
11071107

11081108
React = require('react');
@@ -1142,11 +1142,18 @@ describe('console error', () => {
11421142
expect(mockLog.mock.calls[0][0]).toBe('log');
11431143

11441144
expect(mockWarn).toHaveBeenCalledTimes(1);
1145-
expect(mockWarn.mock.calls[0]).toHaveLength(1);
1145+
expect(mockWarn.mock.calls[0]).toHaveLength(2);
11461146
expect(mockWarn.mock.calls[0][0]).toBe('warn');
1147+
// An error in showInlineWarningsAndErrors doesn't need to break component stacks.
1148+
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
1149+
'\n in FakeStack (at **)',
1150+
);
11471151

11481152
expect(mockError).toHaveBeenCalledTimes(1);
1149-
expect(mockError.mock.calls[0]).toHaveLength(1);
1153+
expect(mockError.mock.calls[0]).toHaveLength(2);
11501154
expect(mockError.mock.calls[0][0]).toBe('error');
1155+
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
1156+
'\n in FakeStack (at **)',
1157+
);
11511158
});
11521159
});

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

Lines changed: 56 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,7 @@
77
* @flow
88
*/
99

10-
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
11-
import type {
12-
LegacyDispatcherRef,
13-
CurrentDispatcherRef,
14-
ReactRenderer,
15-
WorkTagMap,
16-
ConsolePatchSettings,
17-
} from './types';
10+
import type {ConsolePatchSettings} from './types';
1811

1912
import {
2013
formatConsoleArguments,
@@ -25,14 +18,6 @@ import {
2518
ANSI_STYLE_DIMMING_TEMPLATE,
2619
ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK,
2720
} 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';
3621
import {castBool, castBrowserTheme} from '../utils';
3722

3823
const OVERRIDE_CONSOLE_METHODS = ['error', 'trace', 'warn'];
@@ -90,21 +75,15 @@ function restorePotentiallyModifiedArgs(args: Array<any>): Array<any> {
9075
}
9176
}
9277

93-
type OnErrorOrWarning = (
94-
fiber: Fiber,
95-
type: 'error' | 'warn',
96-
args: Array<any>,
97-
) => void;
98-
99-
const injectedRenderers: Map<
100-
ReactRenderer,
101-
{
102-
currentDispatcherRef: LegacyDispatcherRef | CurrentDispatcherRef,
103-
getCurrentFiber: () => Fiber | null,
104-
onErrorOrWarning: ?OnErrorOrWarning,
105-
workTagMap: WorkTagMap,
106-
},
107-
> = new Map();
78+
type OnErrorOrWarning = (type: 'error' | 'warn', args: Array<any>) => void;
79+
type GetComponentStack = (
80+
topFrame: Error,
81+
) => null | {enableOwnerStacks: boolean, componentStack: string};
82+
83+
const injectedRenderers: Array<{
84+
onErrorOrWarning: ?OnErrorOrWarning,
85+
getComponentStack: ?GetComponentStack,
86+
}> = [];
10887

10988
let targetConsole: Object = console;
11089
let targetConsoleMethods: {[string]: $FlowFixMe} = {};
@@ -132,23 +111,13 @@ export function dangerous_setTargetConsoleForTesting(
132111
// These internals will be used if the console is patched.
133112
// Injecting them separately allows the console to easily be patched or un-patched later (at runtime).
134113
export function registerRenderer(
135-
renderer: ReactRenderer,
136114
onErrorOrWarning?: OnErrorOrWarning,
115+
getComponentStack?: GetComponentStack,
137116
): void {
138-
const {currentDispatcherRef, getCurrentFiber, version} = renderer;
139-
140-
// currentDispatcherRef gets injected for v16.8+ to support hooks inspection.
141-
// getCurrentFiber gets injected for v16.9+.
142-
if (currentDispatcherRef != null && typeof getCurrentFiber === 'function') {
143-
const {ReactTypeOfWork} = getInternalReactConstants(version);
144-
145-
injectedRenderers.set(renderer, {
146-
currentDispatcherRef,
147-
getCurrentFiber,
148-
workTagMap: ReactTypeOfWork,
149-
onErrorOrWarning,
150-
});
151-
}
117+
injectedRenderers.push({
118+
onErrorOrWarning,
119+
getComponentStack,
120+
});
152121
}
153122

154123
const consoleSettingsRef: ConsolePatchSettings = {
@@ -219,63 +188,47 @@ export function patch({
219188

220189
// Search for the first renderer that has a current Fiber.
221190
// We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
222-
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
223-
for (const renderer of injectedRenderers.values()) {
224-
const currentDispatcherRef = getDispatcherRef(renderer);
225-
const {getCurrentFiber, onErrorOrWarning, workTagMap} = renderer;
226-
const current: ?Fiber = getCurrentFiber();
227-
if (current != null) {
228-
try {
229-
if (shouldShowInlineWarningsAndErrors) {
230-
// patch() is called by two places: (1) the hook and (2) the renderer backend.
231-
// The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
232-
if (typeof onErrorOrWarning === 'function') {
233-
onErrorOrWarning(
234-
current,
235-
((method: any): 'error' | 'warn'),
236-
// Restore and copy args before we mutate them (e.g. adding the component stack)
237-
restorePotentiallyModifiedArgs(args),
238-
);
239-
}
191+
for (let i = 0; i < injectedRenderers.length; i++) {
192+
const renderer = injectedRenderers[i];
193+
const {getComponentStack, onErrorOrWarning} = renderer;
194+
try {
195+
if (shouldShowInlineWarningsAndErrors) {
196+
// patch() is called by two places: (1) the hook and (2) the renderer backend.
197+
// The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
198+
if (onErrorOrWarning != null) {
199+
onErrorOrWarning(
200+
((method: any): 'error' | 'warn'),
201+
// Restore and copy args before we mutate them (e.g. adding the component stack)
202+
restorePotentiallyModifiedArgs(args),
203+
);
240204
}
241-
242-
if (
243-
consoleSettingsRef.appendComponentStack &&
244-
!supportsConsoleTasks(current)
245-
) {
246-
const enableOwnerStacks = supportsOwnerStacks(current);
247-
let componentStack = '';
248-
if (enableOwnerStacks) {
249-
// Prefix the owner stack with the current stack. I.e. what called
250-
// console.error. While this will also be part of the native stack,
251-
// it is hidden and not presented alongside this argument so we print
252-
// them all together.
253-
const topStackFrames = formatOwnerStack(
254-
new Error('react-stack-top-frame'),
255-
);
256-
if (topStackFrames) {
257-
componentStack += '\n' + topStackFrames;
258-
}
259-
componentStack += getOwnerStackByFiberInDev(
260-
workTagMap,
261-
current,
262-
(currentDispatcherRef: any),
263-
);
264-
} else {
265-
componentStack = getStackByFiberInDevAndProd(
266-
workTagMap,
267-
current,
268-
(currentDispatcherRef: any),
269-
);
270-
}
205+
}
206+
} catch (error) {
207+
// Don't let a DevTools or React internal error interfere with logging.
208+
setTimeout(() => {
209+
throw error;
210+
}, 0);
211+
}
212+
try {
213+
if (
214+
consoleSettingsRef.appendComponentStack &&
215+
getComponentStack != null
216+
) {
217+
// This needs to be directly in the wrapper so we can pop exactly one frame.
218+
const topFrame = Error('react-stack-top-frame');
219+
const match = getComponentStack(topFrame);
220+
if (match !== null) {
221+
const {enableOwnerStacks, componentStack} = match;
222+
// Empty string means we have a match but no component stack.
223+
// We don't need to look in other renderers but we also don't add anything.
271224
if (componentStack !== '') {
272225
// Create a fake Error so that when we print it we get native source maps. Every
273226
// browser will print the .stack property of the error and then parse it back for source
274227
// mapping. Rather than print the internal slot. So it doesn't matter that the internal
275228
// slot doesn't line up.
276229
const fakeError = new Error('');
277230
// In Chromium, only the stack property is printed but in Firefox the <name>:<message>
278-
// gets printed so to make the colon make sense, we name it so we print Component Stack:
231+
// gets printed so to make the colon make sense, we name it so we print Stack:
279232
// and similarly Safari leave an expandable slot.
280233
fakeError.name = enableOwnerStacks
281234
? 'Stack'
@@ -289,6 +242,7 @@ export function patch({
289242
? 'Error Stack:'
290243
: 'Error Component Stack:') + componentStack
291244
: componentStack;
245+
292246
if (alreadyHasComponentStack) {
293247
// Only modify the component stack if it matches what we would've added anyway.
294248
// Otherwise we assume it was a non-React stack.
@@ -324,15 +278,15 @@ export function patch({
324278
}
325279
}
326280
}
281+
// Don't add stacks from other renderers.
282+
break;
327283
}
328-
} catch (error) {
329-
// Don't let a DevTools or React internal error interfere with logging.
330-
setTimeout(() => {
331-
throw error;
332-
}, 0);
333-
} finally {
334-
break;
335284
}
285+
} catch (error) {
286+
// Don't let a DevTools or React internal error interfere with logging.
287+
setTimeout(() => {
288+
throw error;
289+
}, 0);
336290
}
337291
}
338292

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

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent
106106
import is from 'shared/objectIs';
107107
import hasOwnProperty from 'shared/hasOwnProperty';
108108

109+
import {
110+
getStackByFiberInDevAndProd,
111+
getOwnerStackByFiberInDev,
112+
supportsOwnerStacks,
113+
supportsConsoleTasks,
114+
} from './DevToolsFiberComponentStack';
115+
109116
// $FlowFixMe[method-unbinding]
110117
const toString = Object.prototype.toString;
111118

@@ -912,6 +919,7 @@ export function attach(
912919
setErrorHandler,
913920
setSuspenseHandler,
914921
scheduleUpdate,
922+
getCurrentFiber,
915923
} = renderer;
916924
const supportsTogglingError =
917925
typeof setErrorHandler === 'function' &&
@@ -1067,12 +1075,70 @@ export function attach(
10671075
}
10681076
}
10691077

1078+
function getComponentStack(
1079+
topFrame: Error,
1080+
): null | {enableOwnerStacks: boolean, componentStack: string} {
1081+
if (getCurrentFiber === undefined) {
1082+
// Expected this to be part of the renderer. Ignore.
1083+
return null;
1084+
}
1085+
const current = getCurrentFiber();
1086+
if (current === null) {
1087+
// Outside of our render scope.
1088+
return null;
1089+
}
1090+
1091+
if (supportsConsoleTasks(current)) {
1092+
// This will be handled natively by console.createTask. No need for
1093+
// DevTools to add it.
1094+
return null;
1095+
}
1096+
1097+
const dispatcherRef = getDispatcherRef(renderer);
1098+
if (dispatcherRef === undefined) {
1099+
return null;
1100+
}
1101+
1102+
const enableOwnerStacks = supportsOwnerStacks(current);
1103+
let componentStack = '';
1104+
if (enableOwnerStacks) {
1105+
// Prefix the owner stack with the current stack. I.e. what called
1106+
// console.error. While this will also be part of the native stack,
1107+
// it is hidden and not presented alongside this argument so we print
1108+
// them all together.
1109+
const topStackFrames = formatOwnerStack(topFrame);
1110+
if (topStackFrames) {
1111+
componentStack += '\n' + topStackFrames;
1112+
}
1113+
componentStack += getOwnerStackByFiberInDev(
1114+
ReactTypeOfWork,
1115+
current,
1116+
dispatcherRef,
1117+
);
1118+
} else {
1119+
componentStack = getStackByFiberInDevAndProd(
1120+
ReactTypeOfWork,
1121+
current,
1122+
dispatcherRef,
1123+
);
1124+
}
1125+
return {enableOwnerStacks, componentStack};
1126+
}
1127+
10701128
// Called when an error or warning is logged during render, commit, or passive (including unmount functions).
10711129
function onErrorOrWarning(
1072-
fiber: Fiber,
10731130
type: 'error' | 'warn',
10741131
args: $ReadOnlyArray<any>,
10751132
): void {
1133+
if (getCurrentFiber === undefined) {
1134+
// Expected this to be part of the renderer. Ignore.
1135+
return;
1136+
}
1137+
const fiber = getCurrentFiber();
1138+
if (fiber === null) {
1139+
// Outside of our render scope.
1140+
return;
1141+
}
10761142
if (type === 'error') {
10771143
// if this is an error simulated by us to trigger error boundary, ignore
10781144
if (
@@ -1135,7 +1201,7 @@ export function attach(
11351201
// Patching the console enables DevTools to do a few useful things:
11361202
// * Append component stacks to warnings and error messages
11371203
// * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
1138-
registerRendererWithConsole(renderer, onErrorOrWarning);
1204+
registerRendererWithConsole(onErrorOrWarning, getComponentStack);
11391205

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function attach(
2121
global: Object,
2222
): RendererInterface {
2323
patchConsoleUsingWindowValues();
24-
registerRendererWithConsole(renderer);
24+
registerRendererWithConsole(); // TODO: Fill in the impl
2525

2626
return {
2727
cleanup() {},

0 commit comments

Comments
 (0)