Skip to content

Commit 0ecc21c

Browse files
author
Brian Vaughn
committed
DevTools: Add break-on-warn feature
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 cb14168 commit 0ecc21c

File tree

16 files changed

+242
-76
lines changed

16 files changed

+242
-76
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-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-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__ !== false;
440+
if (appendComponentStack || breakOnConsoleErrors) {
441+
patchConsole({
442+
appendComponentStack,
443+
breakOnConsoleErrors,
444+
});
438445
}
439446
}
440447

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ type NativeStyleEditor_SetValueParams = {|
8585
value: string,
8686
|};
8787

88+
type UpdateConsolePatchSettingsParams = {|
89+
appendComponentStack: boolean,
90+
breakOnConsoleErrors: boolean,
91+
|};
92+
8893
type BackendEvents = {|
8994
extensionBackendInitialized: [],
9095
inspectedElement: [InspectedElementPayload],
@@ -133,8 +138,8 @@ type FrontendEvents = {|
133138
stopInspectingNative: [boolean],
134139
stopProfiling: [],
135140
storeAsGlobal: [StoreAsGlobalParams],
136-
updateAppendComponentStack: [boolean],
137141
updateComponentFilters: [Array<ComponentFilter>],
142+
updateConsolePatchSettings: [UpdateConsolePatchSettingsParams],
138143
viewAttributeSource: [ViewAttributeSourceParams],
139144
viewElementSource: [ElementAndRendererID],
140145

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export const SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
2727
export const SESSION_STORAGE_RELOAD_AND_PROFILE_KEY =
2828
'React::DevTools::reloadAndProfile';
2929

30+
export const LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_WARNINGS =
31+
'React::DevTools::breakOnConsoleErrors';
32+
3033
export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY =
3134
'React::DevTools::appendComponentStack';
3235

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import * as React from 'react';
11+
import {useContext} from 'react';
12+
import {SettingsContext} from './SettingsContext';
13+
14+
import styles from './SettingsShared.css';
15+
16+
export default function DebuggingSettings(_: {||}) {
17+
const {
18+
appendComponentStack,
19+
breakOnConsoleErrors,
20+
setAppendComponentStack,
21+
setBreakOnConsoleErrors,
22+
} = useContext(SettingsContext);
23+
24+
return (
25+
<div className={styles.Settings}>
26+
<div className={styles.Setting}>
27+
<label>
28+
<input
29+
type="checkbox"
30+
checked={appendComponentStack}
31+
onChange={({currentTarget}) =>
32+
setAppendComponentStack(currentTarget.checked)
33+
}
34+
/>{' '}
35+
Append component stacks to console warnings and errors.
36+
</label>
37+
</div>
38+
39+
<div className={styles.Setting}>
40+
<label>
41+
<input
42+
type="checkbox"
43+
checked={breakOnConsoleErrors}
44+
onChange={({currentTarget}) =>
45+
setBreakOnConsoleErrors(currentTarget.checked)
46+
}
47+
/>{' '}
48+
Break on warnings
49+
</label>
50+
</div>
51+
</div>
52+
);
53+
}

0 commit comments

Comments
 (0)