Skip to content

Commit 2efe63d

Browse files
author
Brian Vaughn
authored
DevTools: Add break-on-warn feature (#19048)
This commit adds a new tab to the Settings modal: Debugging This new tab has the append component stacks feature and a new one: break on warn This new feature adds a debugger statement into the console override
1 parent 89edb0e commit 2efe63d

File tree

19 files changed

+281
-79
lines changed

19 files changed

+281
-79
lines changed

packages/react-devtools-core/src/standalone.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import {
1717
import Bridge from 'react-devtools-shared/src/bridge';
1818
import Store from 'react-devtools-shared/src/devtools/store';
1919
import {
20-
getSavedComponentFilters,
2120
getAppendComponentStack,
21+
getBreakOnConsoleErrors,
22+
getSavedComponentFilters,
2223
} from 'react-devtools-shared/src/utils';
2324
import {Server} from 'ws';
2425
import {join} from 'path';
@@ -282,11 +283,14 @@ function startServer(port?: number = 8097) {
282283
// Because of this it relies on the extension to pass filters, so include them wth the response here.
283284
// This will ensure that saved filters are shared across different web pages.
284285
const savedPreferencesString = `
285-
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
286-
getSavedComponentFilters(),
287-
)};
288286
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
289287
getAppendComponentStack(),
288+
)};
289+
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify(
290+
getBreakOnConsoleErrors(),
291+
)};
292+
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
293+
getSavedComponentFilters(),
290294
)};`;
291295

292296
response.end(

packages/react-devtools-extensions/src/main.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import Store from 'react-devtools-shared/src/devtools/store';
77
import {getBrowserName, getBrowserTheme} from './utils';
88
import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants';
99
import {
10-
getSavedComponentFilters,
1110
getAppendComponentStack,
11+
getBreakOnConsoleErrors,
12+
getSavedComponentFilters,
1213
} from 'react-devtools-shared/src/utils';
1314
import {
1415
localStorageGetItem,
@@ -28,17 +29,18 @@ let panelCreated = false;
2829
// because they are stored in localStorage within the context of the extension.
2930
// Instead it relies on the extension to pass filters through.
3031
function syncSavedPreferences() {
31-
const componentFilters = getSavedComponentFilters();
32-
chrome.devtools.inspectedWindow.eval(
33-
`window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
34-
componentFilters,
35-
)};`,
36-
);
37-
3832
const appendComponentStack = getAppendComponentStack();
33+
const breakOnConsoleErrors = getBreakOnConsoleErrors();
34+
const componentFilters = getSavedComponentFilters();
3935
chrome.devtools.inspectedWindow.eval(
4036
`window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
4137
appendComponentStack,
38+
)};
39+
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify(
40+
breakOnConsoleErrors,
41+
)};
42+
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
43+
componentFilters,
4244
)};`,
4345
);
4446
}

packages/react-devtools-extensions/webpack.backend.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const {resolve} = require('path');
44
const {DefinePlugin} = require('webpack');
5+
const TerserPlugin = require('terser-webpack-plugin');
56
const {GITHUB_URL, getVersionString} = require('./utils');
67

78
const NODE_ENV = process.env.NODE_ENV;
@@ -39,6 +40,16 @@ module.exports = {
3940
scheduler: resolve(builtModulesDir, 'scheduler'),
4041
},
4142
},
43+
optimization: {
44+
minimizer: [
45+
new TerserPlugin({
46+
terserOptions: {
47+
compress: {drop_debugger: false},
48+
output: {comments: true},
49+
},
50+
}),
51+
],
52+
},
4253
plugins: [
4354
new DefinePlugin({
4455
__DEV__: true,

packages/react-devtools-inline/src/backend.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ function startActivation(contentWindow: window) {
2020
// so it's safe to cleanup after we've received it.
2121
contentWindow.removeEventListener('message', onMessage);
2222

23-
const {appendComponentStack, componentFilters} = data;
23+
const {
24+
appendComponentStack,
25+
breakOnConsoleErrors,
26+
componentFilters,
27+
} = data;
2428

2529
contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
30+
contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
2631
contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
2732

2833
// TRICKY
@@ -33,6 +38,7 @@ function startActivation(contentWindow: window) {
3338
// but it doesn't really hurt anything to store them there too.
3439
if (contentWindow !== window) {
3540
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
41+
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
3642
window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
3743
}
3844

packages/react-devtools-inline/src/frontend.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import Bridge from 'react-devtools-shared/src/bridge';
66
import Store from 'react-devtools-shared/src/devtools/store';
77
import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
88
import {
9-
getSavedComponentFilters,
109
getAppendComponentStack,
10+
getBreakOnConsoleErrors,
11+
getSavedComponentFilters,
1112
} from 'react-devtools-shared/src/utils';
1213
import {
1314
MESSAGE_TYPE_GET_SAVED_PREFERENCES,
@@ -38,6 +39,7 @@ export function initialize(
3839
{
3940
type: MESSAGE_TYPE_SAVED_PREFERENCES,
4041
appendComponentStack: getAppendComponentStack(),
42+
breakOnConsoleErrors: getBreakOnConsoleErrors(),
4143
componentFilters: getSavedComponentFilters(),
4244
},
4345
'*',

packages/react-devtools-inline/webpack.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const {resolve} = require('path');
22
const {DefinePlugin} = require('webpack');
3+
const TerserPlugin = require('terser-webpack-plugin');
34
const {
45
GITHUB_URL,
56
getVersionString,
@@ -36,6 +37,16 @@ module.exports = {
3637
'react-is': 'react-is',
3738
scheduler: 'scheduler',
3839
},
40+
optimization: {
41+
minimizer: [
42+
new TerserPlugin({
43+
terserOptions: {
44+
compress: {drop_debugger: false},
45+
output: {comments: true},
46+
},
47+
}),
48+
],
49+
},
3950
plugins: [
4051
new DefinePlugin({
4152
__DEV__,

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ describe('console', () => {
4444

4545
// Note the Console module only patches once,
4646
// so it's important to patch the test console before injection.
47-
patchConsole();
47+
patchConsole({
48+
appendComponentStack: true,
49+
breakOnWarn: false,
50+
});
4851

4952
const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
5053
global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => {
@@ -79,7 +82,10 @@ describe('console', () => {
7982
it('should only patch the console once', () => {
8083
const {error, warn} = fakeConsole;
8184

82-
patchConsole();
85+
patchConsole({
86+
appendComponentStack: true,
87+
breakOnWarn: false,
88+
});
8389

8490
expect(fakeConsole.error).toBe(error);
8591
expect(fakeConsole.warn).toBe(warn);
@@ -330,7 +336,10 @@ describe('console', () => {
330336
expect(mockError.mock.calls[0]).toHaveLength(1);
331337
expect(mockError.mock.calls[0][0]).toBe('error');
332338

333-
patchConsole();
339+
patchConsole({
340+
appendComponentStack: true,
341+
breakOnWarn: false,
342+
});
334343
act(() => ReactDOM.render(<Child />, document.createElement('div')));
335344

336345
expect(mockWarn).toHaveBeenCalledTimes(2);

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ export default class Agent extends EventEmitter<{|
161161
);
162162
bridge.addListener('shutdown', this.shutdown);
163163
bridge.addListener(
164-
'updateAppendComponentStack',
165-
this.updateAppendComponentStack,
164+
'updateConsolePatchSettings',
165+
this.updateConsolePatchSettings,
166166
);
167167
bridge.addListener('updateComponentFilters', this.updateComponentFilters);
168168
bridge.addListener('viewAttributeSource', this.viewAttributeSource);
@@ -443,13 +443,19 @@ export default class Agent extends EventEmitter<{|
443443
}
444444
};
445445

446-
updateAppendComponentStack = (appendComponentStack: boolean) => {
446+
updateConsolePatchSettings = ({
447+
appendComponentStack,
448+
breakOnConsoleErrors,
449+
}: {|
450+
appendComponentStack: boolean,
451+
breakOnConsoleErrors: boolean,
452+
|}) => {
447453
// If the frontend preference has change,
448454
// or in the case of React Native- if the backend is just finding out the preference-
449455
// then install or uninstall the console overrides.
450456
// It's safe to call these methods multiple times, so we don't need to worry about that.
451-
if (appendComponentStack) {
452-
patchConsole();
457+
if (appendComponentStack || breakOnConsoleErrors) {
458+
patchConsole({appendComponentStack, breakOnConsoleErrors});
453459
} else {
454460
unpatchConsole();
455461
}

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

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,25 @@ export function registerRenderer(renderer: ReactRenderer): void {
8080
}
8181
}
8282

83+
const consoleSettingsRef = {
84+
appendComponentStack: false,
85+
breakOnConsoleErrors: false,
86+
};
87+
8388
// Patches whitelisted console methods to append component stack for the current fiber.
8489
// Call unpatch() to remove the injected behavior.
85-
export function patch(): void {
90+
export function patch({
91+
appendComponentStack,
92+
breakOnConsoleErrors,
93+
}: {
94+
appendComponentStack: boolean,
95+
breakOnConsoleErrors: boolean,
96+
}): void {
97+
// Settings may change after we've patched the console.
98+
// Using a shared ref allows the patch function to read the latest values.
99+
consoleSettingsRef.appendComponentStack = appendComponentStack;
100+
consoleSettingsRef.breakOnConsoleErrors = breakOnConsoleErrors;
101+
86102
if (unpatchFn !== null) {
87103
// Don't patch twice.
88104
return;
@@ -105,40 +121,56 @@ export function patch(): void {
105121
targetConsole[method]);
106122

107123
const overrideMethod = (...args) => {
108-
try {
109-
// If we are ever called with a string that already has a component stack, e.g. a React error/warning,
110-
// don't append a second stack.
111-
const lastArg = args.length > 0 ? args[args.length - 1] : null;
112-
const alreadyHasComponentStack =
113-
lastArg !== null &&
114-
(PREFIX_REGEX.test(lastArg) ||
115-
ROW_COLUMN_NUMBER_REGEX.test(lastArg));
116-
117-
if (!alreadyHasComponentStack) {
118-
// If there's a component stack for at least one of the injected renderers, append it.
119-
// We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
120-
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
121-
for (const {
122-
currentDispatcherRef,
123-
getCurrentFiber,
124-
workTagMap,
125-
} of injectedRenderers.values()) {
126-
const current: ?Fiber = getCurrentFiber();
127-
if (current != null) {
128-
const componentStack = getStackByFiberInDevAndProd(
129-
workTagMap,
130-
current,
131-
currentDispatcherRef,
132-
);
133-
if (componentStack !== '') {
134-
args.push(componentStack);
124+
const latestAppendComponentStack =
125+
consoleSettingsRef.appendComponentStack;
126+
const latestBreakOnConsoleErrors =
127+
consoleSettingsRef.breakOnConsoleErrors;
128+
129+
if (latestAppendComponentStack) {
130+
try {
131+
// If we are ever called with a string that already has a component stack, e.g. a React error/warning,
132+
// don't append a second stack.
133+
const lastArg = args.length > 0 ? args[args.length - 1] : null;
134+
const alreadyHasComponentStack =
135+
lastArg !== null &&
136+
(PREFIX_REGEX.test(lastArg) ||
137+
ROW_COLUMN_NUMBER_REGEX.test(lastArg));
138+
139+
if (!alreadyHasComponentStack) {
140+
// If there's a component stack for at least one of the injected renderers, append it.
141+
// We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
142+
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
143+
for (const {
144+
currentDispatcherRef,
145+
getCurrentFiber,
146+
workTagMap,
147+
} of injectedRenderers.values()) {
148+
const current: ?Fiber = getCurrentFiber();
149+
if (current != null) {
150+
const componentStack = getStackByFiberInDevAndProd(
151+
workTagMap,
152+
current,
153+
currentDispatcherRef,
154+
);
155+
if (componentStack !== '') {
156+
args.push(componentStack);
157+
}
158+
break;
135159
}
136-
break;
137160
}
138161
}
162+
} catch (error) {
163+
// Don't let a DevTools or React internal error interfere with logging.
139164
}
140-
} catch (error) {
141-
// Don't let a DevTools or React internal error interfere with logging.
165+
}
166+
167+
if (latestBreakOnConsoleErrors) {
168+
// --- Welcome to debugging with React DevTools ---
169+
// This debugger statement means that you've enabled the "break on warnings" feature.
170+
// Use the browser's Call Stack panel to step out of this override function-
171+
// to where the original warning or error was logged.
172+
// eslint-disable-next-line no-debugger
173+
debugger;
142174
}
143175

144176
originalMethod(...args);

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -430,11 +430,18 @@ export function attach(
430430
if (process.env.NODE_ENV !== 'test') {
431431
registerRendererWithConsole(renderer);
432432

433-
// The renderer interface can't read this preference directly,
433+
// The renderer interface can't read these preferences directly,
434434
// because it is stored in localStorage within the context of the extension.
435435
// It relies on the extension to pass the preference through via the global.
436-
if (window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false) {
437-
patchConsole();
436+
const appendComponentStack =
437+
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false;
438+
const breakOnConsoleErrors =
439+
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ === true;
440+
if (appendComponentStack || breakOnConsoleErrors) {
441+
patchConsole({
442+
appendComponentStack,
443+
breakOnConsoleErrors,
444+
});
438445
}
439446
}
440447

0 commit comments

Comments
 (0)