mirror of
https://github.com/facebook/react.git
synced 2026-02-25 13:13:03 +00:00
Right now we are patching console 2 times: when hook is installed (before page is loaded) and when backend is connected. Because of this, even if user had `appendComponentStack` setting enabled, all emitted error and warning logs are not going to have component stacks appended. They also won't have component stacks appended retroactively when user opens browser DevTools (this is when frontend is initialized and connects to backend). This behavior adds potential race conditions with LogBox in React Native, and also unpredictable to the user, because in order to get component stacks logged you have to open browser DevTools, but by the time you do it, error or warning log was already emitted. To solve this, we are going to only patch console in the hook object, because it is guaranteed to load even before React. Settings are going to be synchronized with the hook via Bridge, and React DevTools Backend Host (React Native or browser extension shell) will be responsible for persisting these settings across the session, this is going to be implemented in a separate PR.
450 lines
17 KiB
JavaScript
450 lines
17 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @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, castBrowserTheme} 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,
|
|
browserTheme: 'dark',
|
|
};
|
|
|
|
// 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,
|
|
browserTheme,
|
|
}: $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;
|
|
consoleSettingsRef.browserTheme = browserTheme;
|
|
|
|
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;
|
|
const browserTheme =
|
|
castBrowserTheme(window.__REACT_DEVTOOLS_BROWSER_THEME__) ?? 'dark';
|
|
|
|
patch({
|
|
appendComponentStack,
|
|
breakOnConsoleErrors,
|
|
showInlineWarningsAndErrors,
|
|
hideConsoleLogsInStrictMode,
|
|
browserTheme,
|
|
});
|
|
}
|
|
|
|
// 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.
|
|
// The browser extension (etc.) sets these values on window, but through another method.
|
|
export function writeConsolePatchSettingsToWindow(
|
|
settings: ConsolePatchSettings,
|
|
): void {
|
|
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ =
|
|
settings.appendComponentStack;
|
|
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ =
|
|
settings.breakOnConsoleErrors;
|
|
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ =
|
|
settings.showInlineWarningsAndErrors;
|
|
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ =
|
|
settings.hideConsoleLogsInStrictMode;
|
|
window.__REACT_DEVTOOLS_BROWSER_THEME__ = settings.browserTheme;
|
|
}
|
|
|
|
export function installConsoleFunctionsToWindow(): void {
|
|
window.__REACT_DEVTOOLS_CONSOLE_FUNCTIONS__ = {
|
|
patchConsoleUsingWindowValues,
|
|
registerRendererWithConsole: registerRenderer,
|
|
};
|
|
}
|