mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
refactor[react-devtools]: move console patching to global hook (#30596)
Stacked on https://github.com/facebook/react/pull/30566 and whats under it. See [this commit](https://github.com/facebook/react/pull/30596/commits/374fd737e4b0b7028afb765838db7c0e22def865). It is mostly copying code from one place to another and updating tests. With these changes, for every console method that we patch, there is going to be a single applied patch: - For `error`, `warn`, and `trace` we are patching when hook is installed. This guarantees that component stacks are going to be appended even if browser DevTools are not opened. We pay some price for it, though: if user has browser DevTools closed and if at this point some warning or error is emitted (logged), the next time user opens browser DevTools, they are going to see `hook.js` as the source frame. Unfortunately, ignore listing from source maps is not applied retroactively, and I don't know if its a bug or just a design limitations. Once browser DevTools are opened, source maps will be loaded and ignore listing will be applied for all emitted logs in the future. - For `log`, `info`, `group`, `groupCollapsed` we are only patching when React notifies React DevTools about running in StrictMode. We unpatch the methods right after it.
This commit is contained in:
-4
@@ -11,7 +11,6 @@ import Agent from 'react-devtools-shared/src/backend/agent';
|
||||
import Bridge from 'react-devtools-shared/src/bridge';
|
||||
import {installHook} from 'react-devtools-shared/src/hook';
|
||||
import {initBackend} from 'react-devtools-shared/src/backend';
|
||||
import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console';
|
||||
import {__DEBUG__} from 'react-devtools-shared/src/constants';
|
||||
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
|
||||
import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils';
|
||||
@@ -41,9 +40,6 @@ type ConnectOptions = {
|
||||
devToolsSettingsManager: ?DevToolsSettingsManager,
|
||||
};
|
||||
|
||||
// Install a global variable to allow patching console early (during injection).
|
||||
// This provides React Native developers with components stacks even if they don't run DevTools.
|
||||
installConsoleFunctionsToWindow();
|
||||
installHook(window);
|
||||
|
||||
const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import Agent from 'react-devtools-shared/src/backend/agent';
|
||||
import Bridge from 'react-devtools-shared/src/bridge';
|
||||
import {initBackend} from 'react-devtools-shared/src/backend';
|
||||
import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console';
|
||||
import {installHook} from 'react-devtools-shared/src/hook';
|
||||
import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
|
||||
|
||||
@@ -120,8 +119,5 @@ export function createBridge(contentWindow: any, wall?: Wall): BackendBridge {
|
||||
}
|
||||
|
||||
export function initialize(contentWindow: any): void {
|
||||
// Install a global variable to allow patching console early (during injection).
|
||||
// This provides React Native developers with components stacks even if they don't run DevTools.
|
||||
installConsoleFunctionsToWindow();
|
||||
installHook(contentWindow);
|
||||
}
|
||||
|
||||
@@ -7,27 +7,17 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {getVersionedRenderImplementation, normalizeCodeLocInfo} from './utils';
|
||||
import {
|
||||
getVersionedRenderImplementation,
|
||||
normalizeCodeLocInfo,
|
||||
} from 'react-devtools-shared/src/__tests__/utils';
|
||||
|
||||
describe('component stack', () => {
|
||||
let React;
|
||||
let act;
|
||||
let mockError;
|
||||
let mockWarn;
|
||||
let supportsOwnerStacks;
|
||||
|
||||
beforeEach(() => {
|
||||
// Intercept native console methods before DevTools bootstraps.
|
||||
// Normalize component stack locations.
|
||||
mockError = jest.fn();
|
||||
mockWarn = jest.fn();
|
||||
console.error = (...args) => {
|
||||
mockError(...args.map(normalizeCodeLocInfo));
|
||||
};
|
||||
console.warn = (...args) => {
|
||||
mockWarn(...args.map(normalizeCodeLocInfo));
|
||||
};
|
||||
|
||||
const utils = require('./utils');
|
||||
act = utils.act;
|
||||
|
||||
@@ -54,18 +44,22 @@ describe('component stack', () => {
|
||||
|
||||
act(() => render(<Grandparent />));
|
||||
|
||||
expect(mockError).toHaveBeenCalledWith(
|
||||
expect(
|
||||
global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
|
||||
).toEqual([
|
||||
'Test error.',
|
||||
'\n in Child (at **)' +
|
||||
'\n in Parent (at **)' +
|
||||
'\n in Grandparent (at **)',
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
]);
|
||||
expect(
|
||||
global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
|
||||
).toEqual([
|
||||
'Test warning.',
|
||||
'\n in Child (at **)' +
|
||||
'\n in Parent (at **)' +
|
||||
'\n in Grandparent (at **)',
|
||||
);
|
||||
]);
|
||||
});
|
||||
|
||||
// This test should have caught #19911
|
||||
@@ -89,13 +83,15 @@ describe('component stack', () => {
|
||||
|
||||
expect(useEffectCount).toBe(1);
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect(
|
||||
global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
|
||||
).toEqual([
|
||||
'Warning to trigger appended component stacks.',
|
||||
'\n in Example (at **)',
|
||||
);
|
||||
]);
|
||||
});
|
||||
|
||||
// @reactVersion >=18.3
|
||||
// @reactVersion >= 18.3
|
||||
it('should log the current component stack with debug info from promises', () => {
|
||||
const Child = () => {
|
||||
console.error('Test error.');
|
||||
@@ -117,23 +113,27 @@ describe('component stack', () => {
|
||||
|
||||
act(() => render(<Grandparent />));
|
||||
|
||||
expect(mockError).toHaveBeenCalledWith(
|
||||
expect(
|
||||
global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo),
|
||||
).toEqual([
|
||||
'Test error.',
|
||||
supportsOwnerStacks
|
||||
? '\n in Child (at **)'
|
||||
: '\n in Child (at **)' +
|
||||
'\n in ServerComponent (at **)' +
|
||||
'\n in Parent (at **)' +
|
||||
'\n in Grandparent (at **)',
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
'\n in ServerComponent (at **)' +
|
||||
'\n in Parent (at **)' +
|
||||
'\n in Grandparent (at **)',
|
||||
]);
|
||||
expect(
|
||||
global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo),
|
||||
).toEqual([
|
||||
'Test warning.',
|
||||
supportsOwnerStacks
|
||||
? '\n in Child (at **)'
|
||||
: '\n in Child (at **)' +
|
||||
'\n in ServerComponent (at **)' +
|
||||
'\n in Parent (at **)' +
|
||||
'\n in Grandparent (at **)',
|
||||
);
|
||||
'\n in ServerComponent (at **)' +
|
||||
'\n in Parent (at **)' +
|
||||
'\n in Grandparent (at **)',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+278
-701
File diff suppressed because it is too large
Load Diff
+97
-65
@@ -13,6 +13,7 @@ import type {
|
||||
BackendBridge,
|
||||
FrontendBridge,
|
||||
} from 'react-devtools-shared/src/bridge';
|
||||
|
||||
const {getTestFlags} = require('../../../../scripts/jest/TestFlags');
|
||||
|
||||
// Argument is serialized when passed from jest-cli script through to setupTests.
|
||||
@@ -103,7 +104,91 @@ global.gate = fn => {
|
||||
return fn(flags);
|
||||
};
|
||||
|
||||
function shouldIgnoreConsoleErrorOrWarn(args) {
|
||||
let firstArg = args[0];
|
||||
if (
|
||||
firstArg !== null &&
|
||||
typeof firstArg === 'object' &&
|
||||
String(firstArg).indexOf('Error: Uncaught [') === 0
|
||||
) {
|
||||
firstArg = String(firstArg);
|
||||
} else if (typeof firstArg !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return global._ignoredErrorOrWarningMessages.some(errorOrWarningMessage => {
|
||||
return firstArg.indexOf(errorOrWarningMessage) !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
function patchConsoleForTestingBeforeHookInstallation() {
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
const originalConsoleLog = console.log;
|
||||
|
||||
const consoleErrorMock = jest.fn();
|
||||
const consoleWarnMock = jest.fn();
|
||||
const consoleLogMock = jest.fn();
|
||||
|
||||
global.consoleErrorMock = consoleErrorMock;
|
||||
global.consoleWarnMock = consoleWarnMock;
|
||||
global.consoleLogMock = consoleLogMock;
|
||||
|
||||
console.error = (...args) => {
|
||||
let firstArg = args[0];
|
||||
if (typeof firstArg === 'string' && firstArg.startsWith('Warning: ')) {
|
||||
// Older React versions might use the Warning: prefix. I'm not sure
|
||||
// if they use this code path.
|
||||
firstArg = firstArg.slice(9);
|
||||
}
|
||||
if (firstArg === 'React instrumentation encountered an error: %s') {
|
||||
// Rethrow errors from React.
|
||||
throw args[1];
|
||||
} else if (
|
||||
typeof firstArg === 'string' &&
|
||||
(firstArg.startsWith("It looks like you're using the wrong act()") ||
|
||||
firstArg.startsWith(
|
||||
'The current testing environment is not configured to support act',
|
||||
) ||
|
||||
firstArg.startsWith('You seem to have overlapping act() calls'))
|
||||
) {
|
||||
// DevTools intentionally wraps updates with acts from both DOM and test-renderer,
|
||||
// since test updates are expected to impact both renderers.
|
||||
return;
|
||||
} else if (shouldIgnoreConsoleErrorOrWarn(args)) {
|
||||
// Allows testing how DevTools behaves when it encounters console.error without cluttering the test output.
|
||||
// Errors can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
|
||||
return;
|
||||
}
|
||||
|
||||
consoleErrorMock(...args);
|
||||
originalConsoleError.apply(console, args);
|
||||
};
|
||||
console.warn = (...args) => {
|
||||
if (shouldIgnoreConsoleErrorOrWarn(args)) {
|
||||
// Allows testing how DevTools behaves when it encounters console.warn without cluttering the test output.
|
||||
// Warnings can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
|
||||
return;
|
||||
}
|
||||
|
||||
consoleWarnMock(...args);
|
||||
originalConsoleWarn.apply(console, args);
|
||||
};
|
||||
console.log = (...args) => {
|
||||
consoleLogMock(...args);
|
||||
originalConsoleLog.apply(console, args);
|
||||
};
|
||||
}
|
||||
|
||||
function unpatchConsoleAfterTesting() {
|
||||
delete global.consoleErrorMock;
|
||||
delete global.consoleWarnMock;
|
||||
delete global.consoleLogMock;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
patchConsoleForTestingBeforeHookInstallation();
|
||||
|
||||
global.mockClipboardCopy = jest.fn();
|
||||
|
||||
// Test environment doesn't support document methods like execCommand()
|
||||
@@ -137,64 +222,6 @@ beforeEach(() => {
|
||||
global._ignoredErrorOrWarningMessages = [
|
||||
'react-test-renderer is deprecated.',
|
||||
];
|
||||
function shouldIgnoreConsoleErrorOrWarn(args) {
|
||||
let firstArg = args[0];
|
||||
if (
|
||||
firstArg !== null &&
|
||||
typeof firstArg === 'object' &&
|
||||
String(firstArg).indexOf('Error: Uncaught [') === 0
|
||||
) {
|
||||
firstArg = String(firstArg);
|
||||
} else if (typeof firstArg !== 'string') {
|
||||
return false;
|
||||
}
|
||||
const shouldFilter = global._ignoredErrorOrWarningMessages.some(
|
||||
errorOrWarningMessage => {
|
||||
return firstArg.indexOf(errorOrWarningMessage) !== -1;
|
||||
},
|
||||
);
|
||||
|
||||
return shouldFilter;
|
||||
}
|
||||
|
||||
const originalConsoleError = console.error;
|
||||
console.error = (...args) => {
|
||||
let firstArg = args[0];
|
||||
if (typeof firstArg === 'string' && firstArg.startsWith('Warning: ')) {
|
||||
// Older React versions might use the Warning: prefix. I'm not sure
|
||||
// if they use this code path.
|
||||
firstArg = firstArg.slice(9);
|
||||
}
|
||||
if (firstArg === 'React instrumentation encountered an error: %s') {
|
||||
// Rethrow errors from React.
|
||||
throw args[1];
|
||||
} else if (
|
||||
typeof firstArg === 'string' &&
|
||||
(firstArg.startsWith("It looks like you're using the wrong act()") ||
|
||||
firstArg.startsWith(
|
||||
'The current testing environment is not configured to support act',
|
||||
) ||
|
||||
firstArg.startsWith('You seem to have overlapping act() calls'))
|
||||
) {
|
||||
// DevTools intentionally wraps updates with acts from both DOM and test-renderer,
|
||||
// since test updates are expected to impact both renderers.
|
||||
return;
|
||||
} else if (shouldIgnoreConsoleErrorOrWarn(args)) {
|
||||
// Allows testing how DevTools behaves when it encounters console.error without cluttering the test output.
|
||||
// Errors can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
|
||||
return;
|
||||
}
|
||||
originalConsoleError.apply(console, args);
|
||||
};
|
||||
const originalConsoleWarn = console.warn;
|
||||
console.warn = (...args) => {
|
||||
if (shouldIgnoreConsoleErrorOrWarn(args)) {
|
||||
// Allows testing how DevTools behaves when it encounters console.warn without cluttering the test output.
|
||||
// Warnings can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored
|
||||
return;
|
||||
}
|
||||
originalConsoleWarn.apply(console, args);
|
||||
};
|
||||
|
||||
// Initialize filters to a known good state.
|
||||
setSavedComponentFilters(getDefaultComponentFilters());
|
||||
@@ -203,7 +230,12 @@ beforeEach(() => {
|
||||
// Also initialize inline warnings so that we can test them.
|
||||
global.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = true;
|
||||
|
||||
installHook(global);
|
||||
installHook(global, {
|
||||
appendComponentStack: true,
|
||||
breakOnConsoleErrors: false,
|
||||
showInlineWarningsAndErrors: true,
|
||||
hideConsoleLogsInStrictMode: false,
|
||||
});
|
||||
|
||||
const bridgeListeners = [];
|
||||
const bridge = new Bridge({
|
||||
@@ -221,14 +253,12 @@ beforeEach(() => {
|
||||
},
|
||||
});
|
||||
|
||||
const agent = new Agent(((bridge: any): BackendBridge));
|
||||
|
||||
const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
|
||||
initBackend(hook, agent, global);
|
||||
|
||||
const store = new Store(((bridge: any): FrontendBridge));
|
||||
|
||||
const agent = new Agent(((bridge: any): BackendBridge));
|
||||
const hook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
initBackend(hook, agent, global);
|
||||
|
||||
global.agent = agent;
|
||||
global.bridge = bridge;
|
||||
global.store = store;
|
||||
@@ -243,8 +273,10 @@ beforeEach(() => {
|
||||
}
|
||||
global.fetch = mockFetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
unpatchConsoleAfterTesting();
|
||||
|
||||
// It's important to reset modules between test runs;
|
||||
// Without this, ReactDOM won't re-inject itself into the new hook.
|
||||
|
||||
+1
-9
@@ -24,7 +24,6 @@ import {
|
||||
initialize as setupTraceUpdates,
|
||||
toggleEnabled as setTraceUpdatesEnabled,
|
||||
} from './views/TraceUpdates';
|
||||
import {patch as patchConsole} from './console';
|
||||
import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
|
||||
|
||||
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
||||
@@ -36,7 +35,6 @@ import type {
|
||||
PathMatch,
|
||||
RendererID,
|
||||
RendererInterface,
|
||||
ConsolePatchSettings,
|
||||
DevToolsHookSettings,
|
||||
} from './types';
|
||||
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
|
||||
@@ -805,7 +803,7 @@ export default class Agent extends EventEmitter<{
|
||||
};
|
||||
|
||||
updateConsolePatchSettings: (
|
||||
settings: $ReadOnly<ConsolePatchSettings>,
|
||||
settings: $ReadOnly<DevToolsHookSettings>,
|
||||
) => void = settings => {
|
||||
// Propagate the settings, so Backend can subscribe to it and modify hook
|
||||
this.emit('updateHookSettings', {
|
||||
@@ -814,12 +812,6 @@ export default class Agent extends EventEmitter<{
|
||||
showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors,
|
||||
hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode,
|
||||
});
|
||||
|
||||
// If the frontend preferences have changed,
|
||||
// or in the case of React Native- if the backend is just finding out the preferences-
|
||||
// then reinstall the console overrides.
|
||||
// It's safe to call `patchConsole` multiple times.
|
||||
patchConsole(settings);
|
||||
};
|
||||
|
||||
updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void =
|
||||
|
||||
+1
-417
@@ -7,416 +7,7 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {
|
||||
ConsolePatchSettings,
|
||||
OnErrorOrWarning,
|
||||
GetComponentStack,
|
||||
} from './types';
|
||||
|
||||
import {
|
||||
formatConsoleArguments,
|
||||
formatWithStyles,
|
||||
} from 'react-devtools-shared/src/backend/utils';
|
||||
import {
|
||||
FIREFOX_CONSOLE_DIMMING_COLOR,
|
||||
ANSI_STYLE_DIMMING_TEMPLATE,
|
||||
ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK,
|
||||
} from 'react-devtools-shared/src/constants';
|
||||
import {castBool} from '../utils';
|
||||
|
||||
const OVERRIDE_CONSOLE_METHODS = ['error', 'trace', 'warn'];
|
||||
|
||||
// React's custom built component stack strings match "\s{4}in"
|
||||
// Chrome's prefix matches "\s{4}at"
|
||||
const PREFIX_REGEX = /\s{4}(in|at)\s{1}/;
|
||||
// Firefox and Safari have no prefix ("")
|
||||
// but we can fallback to looking for location info (e.g. "foo.js:12:345")
|
||||
const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/;
|
||||
|
||||
export function isStringComponentStack(text: string): boolean {
|
||||
return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text);
|
||||
}
|
||||
|
||||
const STYLE_DIRECTIVE_REGEX = /^%c/;
|
||||
|
||||
// This function tells whether or not the arguments for a console
|
||||
// method has been overridden by the patchForStrictMode function.
|
||||
// If it has we'll need to do some special formatting of the arguments
|
||||
// so the console color stays consistent
|
||||
function isStrictModeOverride(args: Array<any>): boolean {
|
||||
if (__IS_FIREFOX__) {
|
||||
return (
|
||||
args.length >= 2 &&
|
||||
STYLE_DIRECTIVE_REGEX.test(args[0]) &&
|
||||
args[1] === FIREFOX_CONSOLE_DIMMING_COLOR
|
||||
);
|
||||
} else {
|
||||
return args.length >= 2 && args[0] === ANSI_STYLE_DIMMING_TEMPLATE;
|
||||
}
|
||||
}
|
||||
|
||||
// We add a suffix to some frames that older versions of React didn't do.
|
||||
// To compare if it's equivalent we strip out the suffix to see if they're
|
||||
// still equivalent. Similarly, we sometimes use [] and sometimes () so we
|
||||
// strip them to for the comparison.
|
||||
const frameDiffs = / \(\<anonymous\>\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm;
|
||||
function areStackTracesEqual(a: string, b: string): boolean {
|
||||
return a.replace(frameDiffs, '') === b.replace(frameDiffs, '');
|
||||
}
|
||||
|
||||
function restorePotentiallyModifiedArgs(args: Array<any>): Array<any> {
|
||||
// If the arguments don't have any styles applied, then just copy
|
||||
if (!isStrictModeOverride(args)) {
|
||||
return args.slice();
|
||||
}
|
||||
|
||||
if (__IS_FIREFOX__) {
|
||||
// Filter out %c from the start of the first argument and color as a second argument
|
||||
return [args[0].slice(2)].concat(args.slice(2));
|
||||
} else {
|
||||
// Filter out the `\x1b...%s\x1b` template
|
||||
return args.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
const injectedRenderers: Array<{
|
||||
onErrorOrWarning: ?OnErrorOrWarning,
|
||||
getComponentStack: ?GetComponentStack,
|
||||
}> = [];
|
||||
|
||||
let targetConsole: Object = console;
|
||||
let targetConsoleMethods: {[string]: $FlowFixMe} = {};
|
||||
for (const method in console) {
|
||||
// $FlowFixMe[invalid-computed-prop]
|
||||
targetConsoleMethods[method] = console[method];
|
||||
}
|
||||
|
||||
let unpatchFn: null | (() => void) = null;
|
||||
|
||||
// Enables e.g. Jest tests to inject a mock console object.
|
||||
export function dangerous_setTargetConsoleForTesting(
|
||||
targetConsoleForTesting: Object,
|
||||
): void {
|
||||
targetConsole = targetConsoleForTesting;
|
||||
|
||||
targetConsoleMethods = ({}: {[string]: $FlowFixMe});
|
||||
for (const method in targetConsole) {
|
||||
// $FlowFixMe[invalid-computed-prop]
|
||||
targetConsoleMethods[method] = console[method];
|
||||
}
|
||||
}
|
||||
|
||||
// v16 renderers should use this method to inject internals necessary to generate a component stack.
|
||||
// These internals will be used if the console is patched.
|
||||
// Injecting them separately allows the console to easily be patched or un-patched later (at runtime).
|
||||
export function registerRenderer(
|
||||
onErrorOrWarning?: OnErrorOrWarning,
|
||||
getComponentStack?: GetComponentStack,
|
||||
): void {
|
||||
injectedRenderers.push({
|
||||
onErrorOrWarning,
|
||||
getComponentStack,
|
||||
});
|
||||
}
|
||||
|
||||
const consoleSettingsRef: ConsolePatchSettings = {
|
||||
appendComponentStack: false,
|
||||
breakOnConsoleErrors: false,
|
||||
showInlineWarningsAndErrors: false,
|
||||
hideConsoleLogsInStrictMode: false,
|
||||
};
|
||||
|
||||
// Patches console methods to append component stack for the current fiber.
|
||||
// Call unpatch() to remove the injected behavior.
|
||||
export function patch({
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
showInlineWarningsAndErrors,
|
||||
hideConsoleLogsInStrictMode,
|
||||
}: $ReadOnly<ConsolePatchSettings>): void {
|
||||
// Settings may change after we've patched the console.
|
||||
// Using a shared ref allows the patch function to read the latest values.
|
||||
consoleSettingsRef.appendComponentStack = appendComponentStack;
|
||||
consoleSettingsRef.breakOnConsoleErrors = breakOnConsoleErrors;
|
||||
consoleSettingsRef.showInlineWarningsAndErrors = showInlineWarningsAndErrors;
|
||||
consoleSettingsRef.hideConsoleLogsInStrictMode = hideConsoleLogsInStrictMode;
|
||||
|
||||
if (
|
||||
appendComponentStack ||
|
||||
breakOnConsoleErrors ||
|
||||
showInlineWarningsAndErrors
|
||||
) {
|
||||
if (unpatchFn !== null) {
|
||||
// Don't patch twice.
|
||||
return;
|
||||
}
|
||||
|
||||
const originalConsoleMethods: {[string]: $FlowFixMe} = {};
|
||||
|
||||
unpatchFn = () => {
|
||||
for (const method in originalConsoleMethods) {
|
||||
try {
|
||||
targetConsole[method] = originalConsoleMethods[method];
|
||||
} catch (error) {}
|
||||
}
|
||||
};
|
||||
|
||||
OVERRIDE_CONSOLE_METHODS.forEach(method => {
|
||||
try {
|
||||
const originalMethod = (originalConsoleMethods[method] = targetConsole[
|
||||
method
|
||||
].__REACT_DEVTOOLS_ORIGINAL_METHOD__
|
||||
? targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
|
||||
: targetConsole[method]);
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
const overrideMethod = (...args) => {
|
||||
let alreadyHasComponentStack = false;
|
||||
if (method !== 'log' && consoleSettingsRef.appendComponentStack) {
|
||||
const lastArg = args.length > 0 ? args[args.length - 1] : null;
|
||||
alreadyHasComponentStack =
|
||||
typeof lastArg === 'string' && isStringComponentStack(lastArg); // The last argument should be a component stack.
|
||||
}
|
||||
|
||||
const shouldShowInlineWarningsAndErrors =
|
||||
consoleSettingsRef.showInlineWarningsAndErrors &&
|
||||
(method === 'error' || method === 'warn');
|
||||
|
||||
// Search for the first renderer that has a current Fiber.
|
||||
// We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
|
||||
for (let i = 0; i < injectedRenderers.length; i++) {
|
||||
const renderer = injectedRenderers[i];
|
||||
const {getComponentStack, onErrorOrWarning} = renderer;
|
||||
try {
|
||||
if (shouldShowInlineWarningsAndErrors) {
|
||||
// patch() is called by two places: (1) the hook and (2) the renderer backend.
|
||||
// The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
|
||||
if (onErrorOrWarning != null) {
|
||||
onErrorOrWarning(
|
||||
((method: any): 'error' | 'warn'),
|
||||
// Restore and copy args before we mutate them (e.g. adding the component stack)
|
||||
restorePotentiallyModifiedArgs(args),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't let a DevTools or React internal error interfere with logging.
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
}, 0);
|
||||
}
|
||||
try {
|
||||
if (
|
||||
consoleSettingsRef.appendComponentStack &&
|
||||
getComponentStack != null
|
||||
) {
|
||||
// This needs to be directly in the wrapper so we can pop exactly one frame.
|
||||
const topFrame = Error('react-stack-top-frame');
|
||||
const match = getComponentStack(topFrame);
|
||||
if (match !== null) {
|
||||
const {enableOwnerStacks, componentStack} = match;
|
||||
// Empty string means we have a match but no component stack.
|
||||
// We don't need to look in other renderers but we also don't add anything.
|
||||
if (componentStack !== '') {
|
||||
// Create a fake Error so that when we print it we get native source maps. Every
|
||||
// browser will print the .stack property of the error and then parse it back for source
|
||||
// mapping. Rather than print the internal slot. So it doesn't matter that the internal
|
||||
// slot doesn't line up.
|
||||
const fakeError = new Error('');
|
||||
// In Chromium, only the stack property is printed but in Firefox the <name>:<message>
|
||||
// gets printed so to make the colon make sense, we name it so we print Stack:
|
||||
// and similarly Safari leave an expandable slot.
|
||||
if (__IS_CHROME__ || __IS_EDGE__) {
|
||||
// Before sending the stack to Chrome DevTools for formatting,
|
||||
// V8 will reconstruct this according to the template <name>: <message><stack-frames>
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/value-mirror.cc;l=252-311;drc=bdc48d1b1312cc40c00282efb1c9c5f41dcdca9a
|
||||
// It has to start with ^[\w.]*Error\b to trigger stack formatting.
|
||||
fakeError.name = enableOwnerStacks
|
||||
? 'Error Stack'
|
||||
: 'Error Component Stack'; // This gets printed
|
||||
} else {
|
||||
fakeError.name = enableOwnerStacks
|
||||
? 'Stack'
|
||||
: 'Component Stack'; // This gets printed
|
||||
}
|
||||
// In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack
|
||||
// formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it
|
||||
// to our own stack.
|
||||
fakeError.stack =
|
||||
__IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__
|
||||
? (enableOwnerStacks
|
||||
? 'Error Stack:'
|
||||
: 'Error Component Stack:') + componentStack
|
||||
: componentStack;
|
||||
|
||||
if (alreadyHasComponentStack) {
|
||||
// Only modify the component stack if it matches what we would've added anyway.
|
||||
// Otherwise we assume it was a non-React stack.
|
||||
if (isStrictModeOverride(args)) {
|
||||
// We do nothing to Strict Mode overrides that already has a stack
|
||||
// because we have already lost some context for how to format it
|
||||
// since we've already merged the stack into the log at this point.
|
||||
} else if (
|
||||
areStackTracesEqual(
|
||||
args[args.length - 1],
|
||||
componentStack,
|
||||
)
|
||||
) {
|
||||
const firstArg = args[0];
|
||||
if (
|
||||
args.length > 1 &&
|
||||
typeof firstArg === 'string' &&
|
||||
firstArg.endsWith('%s')
|
||||
) {
|
||||
args[0] = firstArg.slice(0, firstArg.length - 2); // Strip the %s param
|
||||
}
|
||||
args[args.length - 1] = fakeError;
|
||||
}
|
||||
} else {
|
||||
args.push(fakeError);
|
||||
if (isStrictModeOverride(args)) {
|
||||
if (__IS_FIREFOX__) {
|
||||
args[0] = `${args[0]} %o`;
|
||||
} else {
|
||||
args[0] =
|
||||
ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Don't add stacks from other renderers.
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't let a DevTools or React internal error interfere with logging.
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (consoleSettingsRef.breakOnConsoleErrors) {
|
||||
// --- Welcome to debugging with React DevTools ---
|
||||
// This debugger statement means that you've enabled the "break on warnings" feature.
|
||||
// Use the browser's Call Stack panel to step out of this override function-
|
||||
// to where the original warning or error was logged.
|
||||
// eslint-disable-next-line no-debugger
|
||||
debugger;
|
||||
}
|
||||
|
||||
originalMethod(...args);
|
||||
};
|
||||
|
||||
overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
|
||||
originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod;
|
||||
|
||||
targetConsole[method] = overrideMethod;
|
||||
} catch (error) {}
|
||||
});
|
||||
} else {
|
||||
unpatch();
|
||||
}
|
||||
}
|
||||
|
||||
// Removed component stack patch from console methods.
|
||||
export function unpatch(): void {
|
||||
if (unpatchFn !== null) {
|
||||
unpatchFn();
|
||||
unpatchFn = null;
|
||||
}
|
||||
}
|
||||
|
||||
let unpatchForStrictModeFn: null | (() => void) = null;
|
||||
|
||||
// NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialCommitInStrictMode
|
||||
export function patchForStrictMode() {
|
||||
const overrideConsoleMethods = [
|
||||
'error',
|
||||
'group',
|
||||
'groupCollapsed',
|
||||
'info',
|
||||
'log',
|
||||
'trace',
|
||||
'warn',
|
||||
];
|
||||
|
||||
if (unpatchForStrictModeFn !== null) {
|
||||
// Don't patch twice.
|
||||
return;
|
||||
}
|
||||
|
||||
const originalConsoleMethods: {[string]: $FlowFixMe} = {};
|
||||
|
||||
unpatchForStrictModeFn = () => {
|
||||
for (const method in originalConsoleMethods) {
|
||||
try {
|
||||
targetConsole[method] = originalConsoleMethods[method];
|
||||
} catch (error) {}
|
||||
}
|
||||
};
|
||||
|
||||
overrideConsoleMethods.forEach(method => {
|
||||
try {
|
||||
const originalMethod = (originalConsoleMethods[method] = targetConsole[
|
||||
method
|
||||
].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
|
||||
? targetConsole[method].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
|
||||
: targetConsole[method]);
|
||||
|
||||
// $FlowFixMe[missing-local-annot]
|
||||
const overrideMethod = (...args) => {
|
||||
if (!consoleSettingsRef.hideConsoleLogsInStrictMode) {
|
||||
// Dim the text color of the double logs if we're not hiding them.
|
||||
if (__IS_FIREFOX__) {
|
||||
originalMethod(
|
||||
...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR),
|
||||
);
|
||||
} else {
|
||||
originalMethod(
|
||||
ANSI_STYLE_DIMMING_TEMPLATE,
|
||||
...formatConsoleArguments(...args),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
overrideMethod.__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ =
|
||||
originalMethod;
|
||||
originalMethod.__REACT_DEVTOOLS_STRICT_MODE_OVERRIDE_METHOD__ =
|
||||
overrideMethod;
|
||||
|
||||
targetConsole[method] = overrideMethod;
|
||||
} catch (error) {}
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: KEEP IN SYNC with src/hook.js:unpatchConsoleForInitialCommitInStrictMode
|
||||
export function unpatchForStrictMode(): void {
|
||||
if (unpatchForStrictModeFn !== null) {
|
||||
unpatchForStrictModeFn();
|
||||
unpatchForStrictModeFn = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function patchConsoleUsingWindowValues() {
|
||||
const appendComponentStack =
|
||||
castBool(window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__) ?? true;
|
||||
const breakOnConsoleErrors =
|
||||
castBool(window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__) ?? false;
|
||||
const showInlineWarningsAndErrors =
|
||||
castBool(window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__) ?? true;
|
||||
const hideConsoleLogsInStrictMode =
|
||||
castBool(window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__) ??
|
||||
false;
|
||||
|
||||
patch({
|
||||
appendComponentStack,
|
||||
breakOnConsoleErrors,
|
||||
showInlineWarningsAndErrors,
|
||||
hideConsoleLogsInStrictMode,
|
||||
});
|
||||
}
|
||||
import type {ConsolePatchSettings} from './types';
|
||||
|
||||
// After receiving cached console patch settings from React Native, we set them on window.
|
||||
// When the console is initially patched (in renderer.js and hook.js), these values are read.
|
||||
@@ -433,10 +24,3 @@ export function writeConsolePatchSettingsToWindow(
|
||||
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ =
|
||||
settings.hideConsoleLogsInStrictMode;
|
||||
}
|
||||
|
||||
export function installConsoleFunctionsToWindow(): void {
|
||||
window.__REACT_DEVTOOLS_CONSOLE_FUNCTIONS__ = {
|
||||
patchConsoleUsingWindowValues,
|
||||
registerRendererWithConsole: registerRenderer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,12 +71,6 @@ import {
|
||||
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
|
||||
} from '../../constants';
|
||||
import {inspectHooksOfFiber} from 'react-debug-tools';
|
||||
import {
|
||||
patchConsoleUsingWindowValues,
|
||||
registerRenderer as registerRendererWithConsole,
|
||||
patchForStrictMode as patchConsoleForStrictMode,
|
||||
unpatchForStrictMode as unpatchConsoleForStrictMode,
|
||||
} from '../console';
|
||||
import {
|
||||
CONCURRENT_MODE_NUMBER,
|
||||
CONCURRENT_MODE_SYMBOL_STRING,
|
||||
@@ -1198,16 +1192,6 @@ export function attach(
|
||||
needsToFlushComponentLogs = true;
|
||||
}
|
||||
|
||||
// Patching the console enables DevTools to do a few useful things:
|
||||
// * Append component stacks to warnings and error messages
|
||||
// * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
|
||||
registerRendererWithConsole(onErrorOrWarning, getComponentStack);
|
||||
|
||||
// The renderer interface can't read these preferences directly,
|
||||
// because it is stored in localStorage within the context of the extension.
|
||||
// It relies on the extension to pass the preference through via the global.
|
||||
patchConsoleUsingWindowValues();
|
||||
|
||||
function debug(
|
||||
name: string,
|
||||
instance: DevToolsInstance,
|
||||
@@ -5788,7 +5772,6 @@ export function attach(
|
||||
hasElementWithId,
|
||||
inspectElement,
|
||||
logElementToConsole,
|
||||
patchConsoleForStrictMode,
|
||||
getComponentStack,
|
||||
getElementAttributeByPath,
|
||||
getElementSourceFunctionById,
|
||||
@@ -5803,7 +5786,6 @@ export function attach(
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
storeAsGlobal,
|
||||
unpatchConsoleForStrictMode,
|
||||
updateComponentFilters,
|
||||
getEnvironmentNames,
|
||||
};
|
||||
|
||||
@@ -19,11 +19,6 @@ import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponent
|
||||
|
||||
import {formatConsoleArgumentsToSingleString} from 'react-devtools-shared/src/backend/utils';
|
||||
|
||||
import {
|
||||
patchConsoleUsingWindowValues,
|
||||
registerRenderer as registerRendererWithConsole,
|
||||
} from '../console';
|
||||
|
||||
function supportsConsoleTasks(componentInfo: ReactComponentInfo): boolean {
|
||||
// If this ReactComponentInfo supports native console.createTask then we are already running
|
||||
// inside a native async stack trace if it's active - meaning the DevTools is open.
|
||||
@@ -145,9 +140,6 @@ export function attach(
|
||||
// The changes will be flushed later when we commit this tree to Fiber.
|
||||
}
|
||||
|
||||
patchConsoleUsingWindowValues();
|
||||
registerRendererWithConsole(onErrorOrWarning, getComponentStack);
|
||||
|
||||
return {
|
||||
cleanup() {},
|
||||
clearErrorsAndWarnings() {},
|
||||
@@ -205,7 +197,6 @@ export function attach(
|
||||
};
|
||||
},
|
||||
logElementToConsole() {},
|
||||
patchConsoleForStrictMode() {},
|
||||
getElementAttributeByPath() {},
|
||||
getElementSourceFunctionById() {},
|
||||
onErrorOrWarning,
|
||||
@@ -219,7 +210,6 @@ export function attach(
|
||||
startProfiling() {},
|
||||
stopProfiling() {},
|
||||
storeAsGlobal() {},
|
||||
unpatchConsoleForStrictMode() {},
|
||||
updateComponentFilters() {},
|
||||
getEnvironmentNames() {
|
||||
return [];
|
||||
|
||||
@@ -1103,10 +1103,6 @@ export function attach(
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
function patchConsoleForStrictMode() {}
|
||||
|
||||
function unpatchConsoleForStrictMode() {}
|
||||
|
||||
function hasElementWithId(id: number): boolean {
|
||||
return idToInternalInstanceMap.has(id);
|
||||
}
|
||||
@@ -1141,7 +1137,6 @@ export function attach(
|
||||
overrideSuspense,
|
||||
overrideValueAtPath,
|
||||
renamePath,
|
||||
patchConsoleForStrictMode,
|
||||
getElementAttributeByPath,
|
||||
getElementSourceFunctionById,
|
||||
renderer,
|
||||
@@ -1150,7 +1145,6 @@ export function attach(
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
storeAsGlobal,
|
||||
unpatchConsoleForStrictMode,
|
||||
updateComponentFilters,
|
||||
getEnvironmentNames,
|
||||
};
|
||||
|
||||
@@ -404,7 +404,6 @@ export type RendererInterface = {
|
||||
path: Array<string | number>,
|
||||
value: any,
|
||||
) => void,
|
||||
patchConsoleForStrictMode: () => void,
|
||||
getElementAttributeByPath: (
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
@@ -427,7 +426,6 @@ export type RendererInterface = {
|
||||
path: Array<string | number>,
|
||||
count: number,
|
||||
) => void,
|
||||
unpatchConsoleForStrictMode: () => void,
|
||||
updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void,
|
||||
getEnvironmentNames: () => Array<string>,
|
||||
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ import type {
|
||||
OwnersList,
|
||||
ProfilingDataBackend,
|
||||
RendererID,
|
||||
ConsolePatchSettings,
|
||||
DevToolsHookSettings,
|
||||
} from 'react-devtools-shared/src/backend/types';
|
||||
import type {StyleAndLayout as StyleAndLayoutPayload} from 'react-devtools-shared/src/backend/NativeStyleEditor/types';
|
||||
|
||||
@@ -241,7 +241,7 @@ type FrontendEvents = {
|
||||
storeAsGlobal: [StoreAsGlobalParams],
|
||||
updateComponentFilters: [Array<ComponentFilter>],
|
||||
getEnvironmentNames: [],
|
||||
updateConsolePatchSettings: [ConsolePatchSettings],
|
||||
updateConsolePatchSettings: [DevToolsHookSettings],
|
||||
viewAttributeSource: [ViewAttributeSourceParams],
|
||||
viewElementSource: [ElementAndRendererID],
|
||||
|
||||
|
||||
+282
-126
@@ -21,10 +21,31 @@ import type {
|
||||
import {
|
||||
FIREFOX_CONSOLE_DIMMING_COLOR,
|
||||
ANSI_STYLE_DIMMING_TEMPLATE,
|
||||
ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK,
|
||||
} from 'react-devtools-shared/src/constants';
|
||||
import attachRenderer from './attachRenderer';
|
||||
|
||||
declare var window: any;
|
||||
// React's custom built component stack strings match "\s{4}in"
|
||||
// Chrome's prefix matches "\s{4}at"
|
||||
const PREFIX_REGEX = /\s{4}(in|at)\s{1}/;
|
||||
// Firefox and Safari have no prefix ("")
|
||||
// but we can fallback to looking for location info (e.g. "foo.js:12:345")
|
||||
const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/;
|
||||
|
||||
function isStringComponentStack(text: string): boolean {
|
||||
return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text);
|
||||
}
|
||||
|
||||
// We add a suffix to some frames that older versions of React didn't do.
|
||||
// To compare if it's equivalent we strip out the suffix to see if they're
|
||||
// still equivalent. Similarly, we sometimes use [] and sometimes () so we
|
||||
// strip them to for the comparison.
|
||||
const frameDiffs = / \(\<anonymous\>\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm;
|
||||
function areStackTracesEqual(a: string, b: string): boolean {
|
||||
return a.replace(frameDiffs, '') === b.replace(frameDiffs, '');
|
||||
}
|
||||
|
||||
const targetConsole: Object = console;
|
||||
|
||||
export function installHook(
|
||||
target: any,
|
||||
@@ -36,25 +57,6 @@ export function installHook(
|
||||
return null;
|
||||
}
|
||||
|
||||
let targetConsole: Object = console;
|
||||
let targetConsoleMethods: {[string]: $FlowFixMe} = {};
|
||||
for (const method in console) {
|
||||
// $FlowFixMe[invalid-computed-prop]
|
||||
targetConsoleMethods[method] = console[method];
|
||||
}
|
||||
|
||||
function dangerous_setTargetConsoleForTesting(
|
||||
targetConsoleForTesting: Object,
|
||||
): void {
|
||||
targetConsole = targetConsoleForTesting;
|
||||
|
||||
targetConsoleMethods = ({}: {[string]: $FlowFixMe});
|
||||
for (const method in targetConsole) {
|
||||
// $FlowFixMe[invalid-computed-prop]
|
||||
targetConsoleMethods[method] = console[method];
|
||||
}
|
||||
}
|
||||
|
||||
function detectReactBuildType(renderer: ReactRenderer) {
|
||||
try {
|
||||
if (typeof renderer.version === 'string') {
|
||||
@@ -189,10 +191,7 @@ export function installHook(
|
||||
}
|
||||
|
||||
// NOTE: KEEP IN SYNC with src/backend/utils.js
|
||||
function formatWithStyles(
|
||||
inputArgs: $ReadOnlyArray<any>,
|
||||
style?: string,
|
||||
): $ReadOnlyArray<any> {
|
||||
function formatWithStyles(inputArgs: Array<any>, style?: string): Array<any> {
|
||||
if (
|
||||
inputArgs === undefined ||
|
||||
inputArgs === null ||
|
||||
@@ -285,85 +284,6 @@ export function installHook(
|
||||
return [template, ...args];
|
||||
}
|
||||
|
||||
let unpatchFn = null;
|
||||
|
||||
// NOTE: KEEP IN SYNC with src/backend/console.js:patchForStrictMode
|
||||
// This function hides or dims console logs during the initial double renderer
|
||||
// in Strict Mode. We need this function because during initial render,
|
||||
// React and DevTools are connecting and the renderer interface isn't avaiable
|
||||
// and we want to be able to have consistent logging behavior for double logs
|
||||
// during the initial renderer.
|
||||
function patchConsoleForInitialCommitInStrictMode(
|
||||
hideConsoleLogsInStrictMode: boolean,
|
||||
) {
|
||||
const overrideConsoleMethods = [
|
||||
'error',
|
||||
'group',
|
||||
'groupCollapsed',
|
||||
'info',
|
||||
'log',
|
||||
'trace',
|
||||
'warn',
|
||||
];
|
||||
|
||||
if (unpatchFn !== null) {
|
||||
// Don't patch twice.
|
||||
return;
|
||||
}
|
||||
|
||||
const originalConsoleMethods: {[string]: $FlowFixMe} = {};
|
||||
|
||||
unpatchFn = () => {
|
||||
for (const method in originalConsoleMethods) {
|
||||
try {
|
||||
targetConsole[method] = originalConsoleMethods[method];
|
||||
} catch (error) {}
|
||||
}
|
||||
};
|
||||
|
||||
overrideConsoleMethods.forEach(method => {
|
||||
try {
|
||||
const originalMethod = (originalConsoleMethods[method] = targetConsole[
|
||||
method
|
||||
].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
|
||||
? targetConsole[method].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
|
||||
: targetConsole[method]);
|
||||
|
||||
const overrideMethod = (...args: $ReadOnlyArray<any>) => {
|
||||
// Dim the text color of the double logs if we're not hiding them.
|
||||
if (!hideConsoleLogsInStrictMode) {
|
||||
// Firefox doesn't support ANSI escape sequences
|
||||
if (__IS_FIREFOX__) {
|
||||
originalMethod(
|
||||
...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR),
|
||||
);
|
||||
} else {
|
||||
originalMethod(
|
||||
ANSI_STYLE_DIMMING_TEMPLATE,
|
||||
...formatConsoleArguments(...args),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
overrideMethod.__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ =
|
||||
originalMethod;
|
||||
originalMethod.__REACT_DEVTOOLS_STRICT_MODE_OVERRIDE_METHOD__ =
|
||||
overrideMethod;
|
||||
|
||||
targetConsole[method] = overrideMethod;
|
||||
} catch (error) {}
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: KEEP IN SYNC with src/backend/console.js:unpatchForStrictMode
|
||||
function unpatchConsoleForInitialCommitInStrictMode() {
|
||||
if (unpatchFn !== null) {
|
||||
unpatchFn();
|
||||
unpatchFn = null;
|
||||
}
|
||||
}
|
||||
|
||||
let uidCounter = 0;
|
||||
function inject(renderer: ReactRenderer): number {
|
||||
const id = ++uidCounter;
|
||||
@@ -469,28 +389,85 @@ export function installHook(
|
||||
}
|
||||
}
|
||||
|
||||
function setStrictMode(rendererID: RendererID, isStrictMode: any) {
|
||||
const rendererInterface = rendererInterfaces.get(rendererID);
|
||||
if (rendererInterface != null) {
|
||||
if (isStrictMode) {
|
||||
rendererInterface.patchConsoleForStrictMode();
|
||||
} else {
|
||||
rendererInterface.unpatchConsoleForStrictMode();
|
||||
}
|
||||
} else {
|
||||
// This should only happen during initial commit in the extension before DevTools
|
||||
// finishes its handshake with the injected renderer
|
||||
if (isStrictMode) {
|
||||
const hideConsoleLogsInStrictMode =
|
||||
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ === true;
|
||||
let isRunningDuringStrictModeInvocation = false;
|
||||
function setStrictMode(rendererID: RendererID, isStrictMode: boolean) {
|
||||
isRunningDuringStrictModeInvocation = isStrictMode;
|
||||
|
||||
patchConsoleForInitialCommitInStrictMode(hideConsoleLogsInStrictMode);
|
||||
} else {
|
||||
unpatchConsoleForInitialCommitInStrictMode();
|
||||
}
|
||||
if (isStrictMode) {
|
||||
patchConsoleForStrictMode();
|
||||
} else {
|
||||
unpatchConsoleForStrictMode();
|
||||
}
|
||||
}
|
||||
|
||||
const unpatchConsoleCallbacks = [];
|
||||
// For StrictMode we patch console once we are running in StrictMode and unpatch right after it
|
||||
// So patching could happen multiple times during the runtime
|
||||
// Notice how we don't patch error or warn methods, because they are already patched in patchConsoleForErrorsAndWarnings
|
||||
// This will only happen once, when hook is installed
|
||||
function patchConsoleForStrictMode() {
|
||||
// Don't patch console in case settings were not injected
|
||||
if (!hook.settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't patch twice
|
||||
if (unpatchConsoleCallbacks.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point 'error', 'warn', and 'trace' methods are already patched
|
||||
// by React DevTools hook to append component stacks and other possible features.
|
||||
const consoleMethodsToOverrideForStrictMode = [
|
||||
'group',
|
||||
'groupCollapsed',
|
||||
'info',
|
||||
'log',
|
||||
];
|
||||
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const method of consoleMethodsToOverrideForStrictMode) {
|
||||
const originalMethod = targetConsole[method];
|
||||
const overrideMethod: (...args: Array<any>) => void = (
|
||||
...args: any[]
|
||||
) => {
|
||||
const settings = hook.settings;
|
||||
// Something unexpected happened, fallback to just printing the console message.
|
||||
if (settings == null) {
|
||||
originalMethod(...args);
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.hideConsoleLogsInStrictMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dim the text color of the double logs if we're not hiding them.
|
||||
// Firefox doesn't support ANSI escape sequences
|
||||
if (__IS_FIREFOX__) {
|
||||
originalMethod(
|
||||
...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR),
|
||||
);
|
||||
} else {
|
||||
originalMethod(
|
||||
ANSI_STYLE_DIMMING_TEMPLATE,
|
||||
...formatConsoleArguments(...args),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
targetConsole[method] = overrideMethod;
|
||||
unpatchConsoleCallbacks.push(() => {
|
||||
targetConsole[method] = originalMethod;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function unpatchConsoleForStrictMode() {
|
||||
unpatchConsoleCallbacks.forEach(callback => callback());
|
||||
unpatchConsoleCallbacks.length = 0;
|
||||
}
|
||||
|
||||
type StackFrameString = string;
|
||||
|
||||
const openModuleRangesStack: Array<StackFrameString> = [];
|
||||
@@ -526,6 +503,188 @@ export function installHook(
|
||||
}
|
||||
}
|
||||
|
||||
// For Errors and Warnings we only patch console once
|
||||
function patchConsoleForErrorsAndWarnings() {
|
||||
// Don't patch console in case settings were not injected
|
||||
if (!hook.settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const consoleMethodsToOverrideForErrorsAndWarnings = [
|
||||
'error',
|
||||
'trace',
|
||||
'warn',
|
||||
];
|
||||
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const method of consoleMethodsToOverrideForErrorsAndWarnings) {
|
||||
const originalMethod = targetConsole[method];
|
||||
const overrideMethod: (...args: Array<any>) => void = (...args) => {
|
||||
const settings = hook.settings;
|
||||
// Something unexpected happened, fallback to just printing the console message.
|
||||
if (settings == null) {
|
||||
originalMethod(...args);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isRunningDuringStrictModeInvocation &&
|
||||
settings.hideConsoleLogsInStrictMode
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let injectedComponentStackAsFakeError = false;
|
||||
let alreadyHasComponentStack = false;
|
||||
if (settings.appendComponentStack) {
|
||||
const lastArg = args.length > 0 ? args[args.length - 1] : null;
|
||||
alreadyHasComponentStack =
|
||||
typeof lastArg === 'string' && isStringComponentStack(lastArg); // The last argument should be a component stack.
|
||||
}
|
||||
|
||||
const shouldShowInlineWarningsAndErrors =
|
||||
settings.showInlineWarningsAndErrors &&
|
||||
(method === 'error' || method === 'warn');
|
||||
|
||||
// Search for the first renderer that has a current Fiber.
|
||||
// We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
|
||||
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
|
||||
for (const rendererInterface of hook.rendererInterfaces.values()) {
|
||||
const {onErrorOrWarning, getComponentStack} = rendererInterface;
|
||||
try {
|
||||
if (shouldShowInlineWarningsAndErrors) {
|
||||
// patch() is called by two places: (1) the hook and (2) the renderer backend.
|
||||
// The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
|
||||
if (onErrorOrWarning != null) {
|
||||
onErrorOrWarning(
|
||||
((method: any): 'error' | 'warn'),
|
||||
args.slice(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't let a DevTools or React internal error interfere with logging.
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
try {
|
||||
if (settings.appendComponentStack && getComponentStack != null) {
|
||||
// This needs to be directly in the wrapper so we can pop exactly one frame.
|
||||
const topFrame = Error('react-stack-top-frame');
|
||||
const match = getComponentStack(topFrame);
|
||||
if (match !== null) {
|
||||
const {enableOwnerStacks, componentStack} = match;
|
||||
// Empty string means we have a match but no component stack.
|
||||
// We don't need to look in other renderers but we also don't add anything.
|
||||
if (componentStack !== '') {
|
||||
// Create a fake Error so that when we print it we get native source maps. Every
|
||||
// browser will print the .stack property of the error and then parse it back for source
|
||||
// mapping. Rather than print the internal slot. So it doesn't matter that the internal
|
||||
// slot doesn't line up.
|
||||
const fakeError = new Error('');
|
||||
// In Chromium, only the stack property is printed but in Firefox the <name>:<message>
|
||||
// gets printed so to make the colon make sense, we name it so we print Stack:
|
||||
// and similarly Safari leave an expandable slot.
|
||||
if (__IS_CHROME__ || __IS_EDGE__) {
|
||||
// Before sending the stack to Chrome DevTools for formatting,
|
||||
// V8 will reconstruct this according to the template <name>: <message><stack-frames>
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/value-mirror.cc;l=252-311;drc=bdc48d1b1312cc40c00282efb1c9c5f41dcdca9a
|
||||
// It has to start with ^[\w.]*Error\b to trigger stack formatting.
|
||||
fakeError.name = enableOwnerStacks
|
||||
? 'Error Stack'
|
||||
: 'Error Component Stack'; // This gets printed
|
||||
} else {
|
||||
fakeError.name = enableOwnerStacks
|
||||
? 'Stack'
|
||||
: 'Component Stack'; // This gets printed
|
||||
}
|
||||
// In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack
|
||||
// formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it
|
||||
// to our own stack.
|
||||
fakeError.stack =
|
||||
__IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__
|
||||
? (enableOwnerStacks
|
||||
? 'Error Stack:'
|
||||
: 'Error Component Stack:') + componentStack
|
||||
: componentStack;
|
||||
|
||||
if (alreadyHasComponentStack) {
|
||||
// Only modify the component stack if it matches what we would've added anyway.
|
||||
// Otherwise we assume it was a non-React stack.
|
||||
if (
|
||||
areStackTracesEqual(args[args.length - 1], componentStack)
|
||||
) {
|
||||
const firstArg = args[0];
|
||||
if (
|
||||
args.length > 1 &&
|
||||
typeof firstArg === 'string' &&
|
||||
firstArg.endsWith('%s')
|
||||
) {
|
||||
args[0] = firstArg.slice(0, firstArg.length - 2); // Strip the %s param
|
||||
}
|
||||
args[args.length - 1] = fakeError;
|
||||
injectedComponentStackAsFakeError = true;
|
||||
}
|
||||
} else {
|
||||
args.push(fakeError);
|
||||
injectedComponentStackAsFakeError = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't add stacks from other renderers.
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't let a DevTools or React internal error interfere with logging.
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.breakOnConsoleErrors) {
|
||||
// --- Welcome to debugging with React DevTools ---
|
||||
// This debugger statement means that you've enabled the "break on warnings" feature.
|
||||
// Use the browser's Call Stack panel to step out of this override function
|
||||
// to where the original warning or error was logged.
|
||||
// eslint-disable-next-line no-debugger
|
||||
debugger;
|
||||
}
|
||||
|
||||
if (isRunningDuringStrictModeInvocation) {
|
||||
// Dim the text color of the double logs if we're not hiding them.
|
||||
// Firefox doesn't support ANSI escape sequences
|
||||
if (__IS_FIREFOX__) {
|
||||
const argsWithCSSStyles = formatWithStyles(
|
||||
args,
|
||||
FIREFOX_CONSOLE_DIMMING_COLOR,
|
||||
);
|
||||
|
||||
if (injectedComponentStackAsFakeError) {
|
||||
argsWithCSSStyles[0] = `${argsWithCSSStyles[0]} %o`;
|
||||
}
|
||||
|
||||
originalMethod(...argsWithCSSStyles);
|
||||
} else {
|
||||
originalMethod(
|
||||
injectedComponentStackAsFakeError
|
||||
? ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK
|
||||
: ANSI_STYLE_DIMMING_TEMPLATE,
|
||||
...formatConsoleArguments(...args),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
originalMethod(...args);
|
||||
}
|
||||
};
|
||||
|
||||
targetConsole[method] = overrideMethod;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: More meaningful names for "rendererInterfaces" and "renderers".
|
||||
const fiberRoots: {[RendererID]: Set<mixed>} = {};
|
||||
const rendererInterfaces = new Map<RendererID, RendererInterface>();
|
||||
@@ -580,10 +739,12 @@ export function installHook(
|
||||
showInlineWarningsAndErrors: true,
|
||||
hideConsoleLogsInStrictMode: false,
|
||||
};
|
||||
patchConsoleForErrorsAndWarnings();
|
||||
} else {
|
||||
Promise.resolve(maybeSettingsOrSettingsPromise)
|
||||
.then(settings => {
|
||||
hook.settings = settings;
|
||||
patchConsoleForErrorsAndWarnings();
|
||||
})
|
||||
.catch(() => {
|
||||
targetConsole.error(
|
||||
@@ -592,11 +753,6 @@ export function installHook(
|
||||
});
|
||||
}
|
||||
|
||||
if (__TEST__) {
|
||||
hook.dangerous_setTargetConsoleForTesting =
|
||||
dangerous_setTargetConsoleForTesting;
|
||||
}
|
||||
|
||||
Object.defineProperty(
|
||||
target,
|
||||
'__REACT_DEVTOOLS_GLOBAL_HOOK__',
|
||||
|
||||
Reference in New Issue
Block a user