mirror of
https://github.com/facebook/react.git
synced 2026-02-24 12:43:00 +00:00
We currently have a failing test for React DevTools against React 17. This started failing in https://github.com/facebook/react/pull/30899, where we changed logic for error tracking and started relying on `onPostCommitFiberRoot` hook. Looking at https://github.com/facebook/react/pull/21183, `onPostCommitFiberRoot` was shipped in 18, which means that any console errors / warnings emitted in passive effects won't be recorded by React DevTools for React < 18.
703 lines
25 KiB
JavaScript
703 lines
25 KiB
JavaScript
/**
|
|
* Install the hook on window, which is an event emitter.
|
|
* Note: this global hook __REACT_DEVTOOLS_GLOBAL_HOOK__ is a de facto public API.
|
|
* It's especially important to avoid creating direct dependency on the DevTools Backend.
|
|
* That's why we still inline the whole event emitter implementation,
|
|
* the string format implementation, and part of the console implementation here.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {
|
|
DevToolsHook,
|
|
Handler,
|
|
ReactRenderer,
|
|
RendererID,
|
|
RendererInterface,
|
|
DevToolsBackend,
|
|
DevToolsHookSettings,
|
|
ProfilingSettings,
|
|
} from './backend/types';
|
|
|
|
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';
|
|
import formatConsoleArguments from 'react-devtools-shared/src/backend/utils/formatConsoleArguments';
|
|
import formatWithStyles from 'react-devtools-shared/src/backend/utils/formatWithStyles';
|
|
|
|
// 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;
|
|
|
|
const defaultProfilingSettings: ProfilingSettings = {
|
|
recordChangeDescriptions: false,
|
|
recordTimeline: false,
|
|
};
|
|
|
|
export function installHook(
|
|
target: any,
|
|
maybeSettingsOrSettingsPromise?:
|
|
| DevToolsHookSettings
|
|
| Promise<DevToolsHookSettings>,
|
|
shouldStartProfilingNow: boolean = false,
|
|
profilingSettings: ProfilingSettings = defaultProfilingSettings,
|
|
): DevToolsHook | null {
|
|
if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
|
|
return null;
|
|
}
|
|
|
|
function detectReactBuildType(renderer: ReactRenderer) {
|
|
try {
|
|
if (typeof renderer.version === 'string') {
|
|
// React DOM Fiber (16+)
|
|
if (renderer.bundleType > 0) {
|
|
// This is not a production build.
|
|
// We are currently only using 0 (PROD) and 1 (DEV)
|
|
// but might add 2 (PROFILE) in the future.
|
|
return 'development';
|
|
}
|
|
|
|
// React 16 uses flat bundles. If we report the bundle as production
|
|
// version, it means we also minified and envified it ourselves.
|
|
return 'production';
|
|
// Note: There is still a risk that the CommonJS entry point has not
|
|
// been envified or uglified. In this case the user would have *both*
|
|
// development and production bundle, but only the prod one would run.
|
|
// This would be really bad. We have a separate check for this because
|
|
// it happens *outside* of the renderer injection. See `checkDCE` below.
|
|
}
|
|
|
|
// $FlowFixMe[method-unbinding]
|
|
const toString = Function.prototype.toString;
|
|
if (renderer.Mount && renderer.Mount._renderNewRootComponent) {
|
|
// React DOM Stack
|
|
const renderRootCode = toString.call(
|
|
renderer.Mount._renderNewRootComponent,
|
|
);
|
|
// Filter out bad results (if that is even possible):
|
|
if (renderRootCode.indexOf('function') !== 0) {
|
|
// Hope for the best if we're not sure.
|
|
return 'production';
|
|
}
|
|
// Check for React DOM Stack < 15.1.0 in development.
|
|
// If it contains "storedMeasure" call, it's wrapped in ReactPerf (DEV only).
|
|
// This would be true even if it's minified, as method name still matches.
|
|
if (renderRootCode.indexOf('storedMeasure') !== -1) {
|
|
return 'development';
|
|
}
|
|
// For other versions (and configurations) it's not so easy.
|
|
// Let's quickly exclude proper production builds.
|
|
// If it contains a warning message, it's either a DEV build,
|
|
// or an PROD build without proper dead code elimination.
|
|
if (renderRootCode.indexOf('should be a pure function') !== -1) {
|
|
// Now how do we tell a DEV build from a bad PROD build?
|
|
// If we see NODE_ENV, we're going to assume this is a dev build
|
|
// because most likely it is referring to an empty shim.
|
|
if (renderRootCode.indexOf('NODE_ENV') !== -1) {
|
|
return 'development';
|
|
}
|
|
// If we see "development", we're dealing with an envified DEV build
|
|
// (such as the official React DEV UMD).
|
|
if (renderRootCode.indexOf('development') !== -1) {
|
|
return 'development';
|
|
}
|
|
// I've seen process.env.NODE_ENV !== 'production' being smartly
|
|
// replaced by `true` in DEV by Webpack. I don't know how that
|
|
// works but we can safely guard against it because `true` was
|
|
// never used in the function source since it was written.
|
|
if (renderRootCode.indexOf('true') !== -1) {
|
|
return 'development';
|
|
}
|
|
// By now either it is a production build that has not been minified,
|
|
// or (worse) this is a minified development build using non-standard
|
|
// environment (e.g. "staging"). We're going to look at whether
|
|
// the function argument name is mangled:
|
|
if (
|
|
// 0.13 to 15
|
|
renderRootCode.indexOf('nextElement') !== -1 ||
|
|
// 0.12
|
|
renderRootCode.indexOf('nextComponent') !== -1
|
|
) {
|
|
// We can't be certain whether this is a development build or not,
|
|
// but it is definitely unminified.
|
|
return 'unminified';
|
|
} else {
|
|
// This is likely a minified development build.
|
|
return 'development';
|
|
}
|
|
}
|
|
// By now we know that it's envified and dead code elimination worked,
|
|
// but what if it's still not minified? (Is this even possible?)
|
|
// Let's check matches for the first argument name.
|
|
if (
|
|
// 0.13 to 15
|
|
renderRootCode.indexOf('nextElement') !== -1 ||
|
|
// 0.12
|
|
renderRootCode.indexOf('nextComponent') !== -1
|
|
) {
|
|
return 'unminified';
|
|
}
|
|
// Seems like we're using the production version.
|
|
// However, the branch above is Stack-only so this is 15 or earlier.
|
|
return 'outdated';
|
|
}
|
|
} catch (err) {
|
|
// Weird environments may exist.
|
|
// This code needs a higher fault tolerance
|
|
// because it runs even with closed DevTools.
|
|
// TODO: should we catch errors in all injected code, and not just this part?
|
|
}
|
|
return 'production';
|
|
}
|
|
|
|
function checkDCE(fn: Function) {
|
|
// This runs for production versions of React.
|
|
// Needs to be super safe.
|
|
try {
|
|
// $FlowFixMe[method-unbinding]
|
|
const toString = Function.prototype.toString;
|
|
const code = toString.call(fn);
|
|
|
|
// This is a string embedded in the passed function under DEV-only
|
|
// condition. However the function executes only in PROD. Therefore,
|
|
// if we see it, dead code elimination did not work.
|
|
if (code.indexOf('^_^') > -1) {
|
|
// Remember to report during next injection.
|
|
hasDetectedBadDCE = true;
|
|
|
|
// Bonus: throw an exception hoping that it gets picked up by a reporting system.
|
|
// Not synchronously so that it doesn't break the calling code.
|
|
setTimeout(function () {
|
|
throw new Error(
|
|
'React is running in production mode, but dead code ' +
|
|
'elimination has not been applied. Read how to correctly ' +
|
|
'configure React for production: ' +
|
|
'https://react.dev/link/perf-use-production-build',
|
|
);
|
|
});
|
|
}
|
|
} catch (err) {}
|
|
}
|
|
|
|
// TODO: isProfiling should be stateful, and we should update it once profiling is finished
|
|
const isProfiling = shouldStartProfilingNow;
|
|
let uidCounter = 0;
|
|
function inject(renderer: ReactRenderer): number {
|
|
const id = ++uidCounter;
|
|
renderers.set(id, renderer);
|
|
|
|
const reactBuildType = hasDetectedBadDCE
|
|
? 'deadcode'
|
|
: detectReactBuildType(renderer);
|
|
|
|
hook.emit('renderer', {
|
|
id,
|
|
renderer,
|
|
reactBuildType,
|
|
});
|
|
|
|
const rendererInterface = attachRenderer(
|
|
hook,
|
|
id,
|
|
renderer,
|
|
target,
|
|
isProfiling,
|
|
profilingSettings,
|
|
);
|
|
if (rendererInterface != null) {
|
|
hook.rendererInterfaces.set(id, rendererInterface);
|
|
hook.emit('renderer-attached', {id, rendererInterface});
|
|
} else {
|
|
hook.hasUnsupportedRendererAttached = true;
|
|
hook.emit('unsupported-renderer-version');
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
let hasDetectedBadDCE = false;
|
|
|
|
function sub(event: string, fn: Handler) {
|
|
hook.on(event, fn);
|
|
return () => hook.off(event, fn);
|
|
}
|
|
|
|
function on(event: string, fn: Handler) {
|
|
if (!listeners[event]) {
|
|
listeners[event] = [];
|
|
}
|
|
listeners[event].push(fn);
|
|
}
|
|
|
|
function off(event: string, fn: Handler) {
|
|
if (!listeners[event]) {
|
|
return;
|
|
}
|
|
const index = listeners[event].indexOf(fn);
|
|
if (index !== -1) {
|
|
listeners[event].splice(index, 1);
|
|
}
|
|
if (!listeners[event].length) {
|
|
delete listeners[event];
|
|
}
|
|
}
|
|
|
|
function emit(event: string, data: any) {
|
|
if (listeners[event]) {
|
|
listeners[event].map(fn => fn(data));
|
|
}
|
|
}
|
|
|
|
function getFiberRoots(rendererID: RendererID) {
|
|
const roots = fiberRoots;
|
|
if (!roots[rendererID]) {
|
|
roots[rendererID] = new Set();
|
|
}
|
|
return roots[rendererID];
|
|
}
|
|
|
|
function onCommitFiberUnmount(rendererID: RendererID, fiber: any) {
|
|
const rendererInterface = rendererInterfaces.get(rendererID);
|
|
if (rendererInterface != null) {
|
|
rendererInterface.handleCommitFiberUnmount(fiber);
|
|
}
|
|
}
|
|
|
|
function onCommitFiberRoot(
|
|
rendererID: RendererID,
|
|
root: any,
|
|
priorityLevel: void | number,
|
|
) {
|
|
const mountedRoots = hook.getFiberRoots(rendererID);
|
|
const current = root.current;
|
|
const isKnownRoot = mountedRoots.has(root);
|
|
const isUnmounting =
|
|
current.memoizedState == null || current.memoizedState.element == null;
|
|
|
|
// Keep track of mounted roots so we can hydrate when DevTools connect.
|
|
if (!isKnownRoot && !isUnmounting) {
|
|
mountedRoots.add(root);
|
|
} else if (isKnownRoot && isUnmounting) {
|
|
mountedRoots.delete(root);
|
|
}
|
|
const rendererInterface = rendererInterfaces.get(rendererID);
|
|
if (rendererInterface != null) {
|
|
rendererInterface.handleCommitFiberRoot(root, priorityLevel);
|
|
}
|
|
}
|
|
|
|
function onPostCommitFiberRoot(rendererID: RendererID, root: any) {
|
|
const rendererInterface = rendererInterfaces.get(rendererID);
|
|
if (rendererInterface != null) {
|
|
rendererInterface.handlePostCommitFiberRoot(root);
|
|
}
|
|
}
|
|
|
|
let isRunningDuringStrictModeInvocation = false;
|
|
function setStrictMode(rendererID: RendererID, isStrictMode: boolean) {
|
|
isRunningDuringStrictModeInvocation = isStrictMode;
|
|
|
|
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> = [];
|
|
const moduleRanges: Array<[StackFrameString, StackFrameString]> = [];
|
|
|
|
function getTopStackFrameString(error: Error): StackFrameString | null {
|
|
const frames = error.stack.split('\n');
|
|
const frame = frames.length > 1 ? frames[1] : null;
|
|
return frame;
|
|
}
|
|
|
|
function getInternalModuleRanges(): Array<
|
|
[StackFrameString, StackFrameString],
|
|
> {
|
|
return moduleRanges;
|
|
}
|
|
|
|
function registerInternalModuleStart(error: Error) {
|
|
const startStackFrame = getTopStackFrameString(error);
|
|
if (startStackFrame !== null) {
|
|
openModuleRangesStack.push(startStackFrame);
|
|
}
|
|
}
|
|
|
|
function registerInternalModuleStop(error: Error) {
|
|
if (openModuleRangesStack.length > 0) {
|
|
const startStackFrame = openModuleRangesStack.pop();
|
|
const stopStackFrame = getTopStackFrameString(error);
|
|
if (stopStackFrame !== null) {
|
|
// $FlowFixMe[incompatible-call]
|
|
moduleRanges.push([startStackFrame, stopStackFrame]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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__) {
|
|
let argsWithCSSStyles = formatWithStyles(
|
|
args,
|
|
FIREFOX_CONSOLE_DIMMING_COLOR,
|
|
);
|
|
|
|
if (injectedComponentStackAsFakeError) {
|
|
argsWithCSSStyles = [
|
|
`${argsWithCSSStyles[0]} %o`,
|
|
...argsWithCSSStyles.slice(1),
|
|
];
|
|
}
|
|
|
|
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>();
|
|
const listeners: {[string]: Array<Handler>} = {};
|
|
const renderers = new Map<RendererID, ReactRenderer>();
|
|
const backends = new Map<string, DevToolsBackend>();
|
|
|
|
const hook: DevToolsHook = {
|
|
rendererInterfaces,
|
|
listeners,
|
|
|
|
backends,
|
|
|
|
// Fast Refresh for web relies on this.
|
|
renderers,
|
|
hasUnsupportedRendererAttached: false,
|
|
|
|
emit,
|
|
getFiberRoots,
|
|
inject,
|
|
on,
|
|
off,
|
|
sub,
|
|
|
|
// This is a legacy flag.
|
|
// React v16 checks the hook for this to ensure DevTools is new enough.
|
|
supportsFiber: true,
|
|
|
|
// React Flight Client checks the hook for this to ensure DevTools is new enough.
|
|
supportsFlight: true,
|
|
|
|
// React calls these methods.
|
|
checkDCE,
|
|
onCommitFiberUnmount,
|
|
onCommitFiberRoot,
|
|
// React v18.0+
|
|
onPostCommitFiberRoot,
|
|
setStrictMode,
|
|
|
|
// Schedule Profiler runtime helpers.
|
|
// These internal React modules to report their own boundaries
|
|
// which in turn enables the profiler to dim or filter internal frames.
|
|
getInternalModuleRanges,
|
|
registerInternalModuleStart,
|
|
registerInternalModuleStop,
|
|
};
|
|
|
|
if (maybeSettingsOrSettingsPromise == null) {
|
|
// Set default settings
|
|
hook.settings = {
|
|
appendComponentStack: true,
|
|
breakOnConsoleErrors: false,
|
|
showInlineWarningsAndErrors: true,
|
|
hideConsoleLogsInStrictMode: false,
|
|
};
|
|
patchConsoleForErrorsAndWarnings();
|
|
} else {
|
|
Promise.resolve(maybeSettingsOrSettingsPromise)
|
|
.then(settings => {
|
|
hook.settings = settings;
|
|
hook.emit('settingsInitialized', settings);
|
|
|
|
patchConsoleForErrorsAndWarnings();
|
|
})
|
|
.catch(() => {
|
|
targetConsole.error(
|
|
"React DevTools failed to get Console Patching settings. Console won't be patched and some console features will not work.",
|
|
);
|
|
});
|
|
}
|
|
|
|
Object.defineProperty(
|
|
target,
|
|
'__REACT_DEVTOOLS_GLOBAL_HOOK__',
|
|
({
|
|
// This property needs to be configurable for the test environment,
|
|
// else we won't be able to delete and recreate it between tests.
|
|
configurable: __DEV__,
|
|
enumerable: false,
|
|
get() {
|
|
return hook;
|
|
},
|
|
}: Object),
|
|
);
|
|
|
|
return hook;
|
|
}
|