Skip to content

Commit b12be26

Browse files
committed
Print component stacks as error objects to get source mapping
1 parent f38c22b commit b12be26

File tree

9 files changed

+91
-26
lines changed

9 files changed

+91
-26
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ module.exports = {
490490
'packages/react-devtools-extensions/**/*.js',
491491
'packages/react-devtools-shared/src/hook.js',
492492
'packages/react-devtools-shared/src/backend/console.js',
493+
'packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js',
493494
],
494495
globals: {
495496
__IS_CHROME__: 'readonly',

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,7 +1000,7 @@ describe('console', () => {
10001000
);
10011001
expect(mockWarn.mock.calls[1]).toHaveLength(3);
10021002
expect(mockWarn.mock.calls[1][0]).toEqual(
1003-
'\x1b[2;38;2;124;124;124m%s %s\x1b[0m',
1003+
'\x1b[2;38;2;124;124;124m%s %o\x1b[0m',
10041004
);
10051005
expect(mockWarn.mock.calls[1][1]).toMatch('warn');
10061006
expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][2]).trim()).toEqual(
@@ -1014,7 +1014,7 @@ describe('console', () => {
10141014
);
10151015
expect(mockError.mock.calls[1]).toHaveLength(3);
10161016
expect(mockError.mock.calls[1][0]).toEqual(
1017-
'\x1b[2;38;2;124;124;124m%s %s\x1b[0m',
1017+
'\x1b[2;38;2;124;124;124m%s %o\x1b[0m',
10181018
);
10191019
expect(mockError.mock.calls[1][1]).toEqual('error');
10201020
expect(normalizeCodeLocInfo(mockError.mock.calls[1][2]).trim()).toEqual(

packages/react-devtools-shared/src/__tests__/utils.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,9 @@ export function overrideFeatureFlags(overrideFlags) {
463463
}
464464

465465
export function normalizeCodeLocInfo(str) {
466+
if (typeof str === 'object' && str !== null) {
467+
str = str.stack;
468+
}
466469
if (typeof str !== 'string') {
467470
return str;
468471
}

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,19 @@ export function describeBuiltInComponentFrame(name: string): string {
2929
prefix = (match && match[1]) || '';
3030
}
3131
}
32+
let suffix = '';
33+
if (__IS_CHROME__ || __IS_EDGE__) {
34+
suffix = ' (<anonymous>)';
35+
} else if (__IS_FIREFOX__) {
36+
suffix = '@unknown:0:0';
37+
}
3238
// We use the prefix to ensure our stacks line up with native stack frames.
33-
return '\n' + prefix + name;
39+
// We use a suffix to ensure it gets parsed natively.
40+
return '\n' + prefix + name + suffix;
3441
}
3542

3643
export function describeDebugInfoFrame(name: string, env: ?string): string {
37-
return describeBuiltInComponentFrame(name + (env ? ' (' + env + ')' : ''));
44+
return describeBuiltInComponentFrame(name + (env ? ' [' + env + ']' : ''));
3845
}
3946

4047
let reentry = false;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export function describeFiber(
2828
currentDispatcherRef: CurrentDispatcherRef,
2929
): string {
3030
const {
31+
HostHoistable,
32+
HostSingleton,
3133
HostComponent,
3234
LazyComponent,
3335
SuspenseComponent,
@@ -40,6 +42,8 @@ export function describeFiber(
4042
} = workTagMap;
4143

4244
switch (workInProgress.tag) {
45+
case HostHoistable:
46+
case HostSingleton:
4347
case HostComponent:
4448
return describeBuiltInComponentFrame(workInProgress.type);
4549
case LazyComponent:

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

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ function isStrictModeOverride(args: Array<any>): boolean {
6363
}
6464
}
6565

66+
// We add a suffix to some frames that older versions of React didn't do.
67+
// To compare if it's equivalent we strip out the suffix to see if they're
68+
// still equivalent. Similarly, we sometimes use [] and sometimes () so we
69+
// strip them to for the comparison.
70+
const frameDiffs = / \(\<anonymous\>\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm;
71+
function areStackTracesEqual(a: string, b: string): boolean {
72+
return a.replace(frameDiffs, '') === b.replace(frameDiffs, '');
73+
}
74+
6675
function restorePotentiallyModifiedArgs(args: Array<any>): Array<any> {
6776
// If the arguments don't have any styles applied, then just copy
6877
if (!isStrictModeOverride(args)) {
@@ -202,17 +211,11 @@ export function patch({
202211

203212
// $FlowFixMe[missing-local-annot]
204213
const overrideMethod = (...args) => {
205-
let shouldAppendWarningStack = false;
206-
if (method !== 'log') {
207-
if (consoleSettingsRef.appendComponentStack) {
208-
const lastArg = args.length > 0 ? args[args.length - 1] : null;
209-
const alreadyHasComponentStack =
210-
typeof lastArg === 'string' && isStringComponentStack(lastArg);
211-
212-
// If we are ever called with a string that already has a component stack,
213-
// e.g. a React error/warning, don't append a second stack.
214-
shouldAppendWarningStack = !alreadyHasComponentStack;
215-
}
214+
let alreadyHasComponentStack = false;
215+
if (method !== 'log' && consoleSettingsRef.appendComponentStack) {
216+
const lastArg = args.length > 0 ? args[args.length - 1] : null;
217+
alreadyHasComponentStack =
218+
typeof lastArg === 'string' && isStringComponentStack(lastArg); // The last argument should be a component stack.
216219
}
217220

218221
const shouldShowInlineWarningsAndErrors =
@@ -242,7 +245,7 @@ export function patch({
242245
}
243246

244247
if (
245-
shouldAppendWarningStack &&
248+
consoleSettingsRef.appendComponentStack &&
246249
!supportsNativeConsoleTasks(current)
247250
) {
248251
const componentStack = getStackByFiberInDevAndProd(
@@ -251,17 +254,60 @@ export function patch({
251254
(currentDispatcherRef: any),
252255
);
253256
if (componentStack !== '') {
254-
if (isStrictModeOverride(args)) {
255-
if (__IS_FIREFOX__) {
256-
args[0] = `${args[0]} %s`;
257-
args.push(componentStack);
258-
} else {
259-
args[0] =
260-
ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK;
261-
args.push(componentStack);
257+
// Create a fake Error so that when we print it we get native source maps. Every
258+
// browser will print the .stack property of the error and then parse it back for source
259+
// mapping. Rather than print the internal slot. So it doesn't matter that the internal
260+
// slot doesn't line up.
261+
const fakeError = new Error('');
262+
// In Chromium, only the stack property is printed but in Firefox the <name>:<message>
263+
// gets printed so to make the colon make sense, we name it so we print Component Stack:
264+
// and similarly Safari leave an expandable slot.
265+
fakeError.name = 'Component Stack'; // This gets printed
266+
// In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack
267+
// formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it
268+
// to our own stack.
269+
fakeError.stack =
270+
__IS_CHROME__ || __IS_EDGE__
271+
? 'Error Component Stack:' + componentStack
272+
: componentStack;
273+
if (alreadyHasComponentStack) {
274+
// Only modify the component stack if it matches what we would've added anyway.
275+
// Otherwise we assume it was a non-React stack.
276+
if (
277+
areStackTracesEqual(
278+
args[args.length - 1],
279+
componentStack,
280+
)
281+
) {
282+
args[args.length - 1] = fakeError;
283+
if (isStrictModeOverride(args)) {
284+
if (__IS_FIREFOX__) {
285+
args[0] = `${args[0]} %o`;
286+
} else {
287+
args[0] =
288+
ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK;
289+
}
290+
} else {
291+
const firstArg = args[0];
292+
if (
293+
args.length > 1 &&
294+
typeof firstArg === 'string' &&
295+
firstArg.endsWith('%s')
296+
) {
297+
args[0] = firstArg.slice(0, firstArg.length - 2); // Strip the %s param
298+
}
299+
}
262300
}
263301
} else {
264-
args.push(componentStack);
302+
args.push(fakeError);
303+
if (isStrictModeOverride(args)) {
304+
if (__IS_FIREFOX__) {
305+
args[0] = `${args[0]} %o`;
306+
} else {
307+
args[0] =
308+
ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK;
309+
}
310+
}
265311
}
266312
}
267313
}

packages/react-devtools-shared/src/constants.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,4 @@ export const PROFILER_EXPORT_VERSION = 5;
6262
export const FIREFOX_CONSOLE_DIMMING_COLOR = 'color: rgba(124, 124, 124, 0.75)';
6363
export const ANSI_STYLE_DIMMING_TEMPLATE = '\x1b[2;38;2;124;124;124m%s\x1b[0m';
6464
export const ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK =
65-
'\x1b[2;38;2;124;124;124m%s %s\x1b[0m';
65+
'\x1b[2;38;2;124;124;124m%s %o\x1b[0m';

scripts/flow/react-devtools.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ declare const __EXTENSION__: boolean;
1313
declare const __TEST__: boolean;
1414

1515
declare const __IS_FIREFOX__: boolean;
16+
declare const __IS_CHROME__: boolean;
17+
declare const __IS_EDGE__: boolean;

scripts/jest/devtools/setupEnv.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ if (!global.hasOwnProperty('localStorage')) {
1212
global.__DEV__ = process.env.NODE_ENV !== 'production';
1313
global.__TEST__ = true;
1414
global.__IS_FIREFOX__ = false;
15+
global.__IS_CHROME__ = false;
16+
global.__IS_EDGE__ = false;
1517

1618
const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion;
1719

0 commit comments

Comments
 (0)