1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
    
  11. import type {
    
  12.   CurrentDispatcherRef,
    
  13.   ReactRenderer,
    
  14.   WorkTagMap,
    
  15.   ConsolePatchSettings,
    
  16. } from './types';
    
  17. import {format, formatWithStyles} from './utils';
    
  18. 
    
  19. import {getInternalReactConstants} from './renderer';
    
  20. import {getStackByFiberInDevAndProd} from './DevToolsFiberComponentStack';
    
  21. import {consoleManagedByDevToolsDuringStrictMode} from 'react-devtools-feature-flags';
    
  22. import {castBool, castBrowserTheme} from '../utils';
    
  23. 
    
  24. const OVERRIDE_CONSOLE_METHODS = ['error', 'trace', 'warn'];
    
  25. const DIMMED_NODE_CONSOLE_COLOR = '\x1b[2m%s\x1b[0m';
    
  26. 
    
  27. // React's custom built component stack strings match "\s{4}in"
    
  28. // Chrome's prefix matches "\s{4}at"
    
  29. const PREFIX_REGEX = /\s{4}(in|at)\s{1}/;
    
  30. // Firefox and Safari have no prefix ("")
    
  31. // but we can fallback to looking for location info (e.g. "foo.js:12:345")
    
  32. const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/;
    
  33. 
    
  34. export function isStringComponentStack(text: string): boolean {
    
  35.   return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text);
    
  36. }
    
  37. 
    
  38. const STYLE_DIRECTIVE_REGEX = /^%c/;
    
  39. 
    
  40. // This function tells whether or not the arguments for a console
    
  41. // method has been overridden by the patchForStrictMode function.
    
  42. // If it has we'll need to do some special formatting of the arguments
    
  43. // so the console color stays consistent
    
  44. function isStrictModeOverride(args: Array<string>, method: string): boolean {
    
  45.   return (
    
  46.     args.length >= 2 &&
    
  47.     STYLE_DIRECTIVE_REGEX.test(args[0]) &&
    
  48.     args[1] === `color: ${getConsoleColor(method) || ''}`
    
  49.   );
    
  50. }
    
  51. 
    
  52. function getConsoleColor(method: string): ?string {
    
  53.   switch (method) {
    
  54.     case 'warn':
    
  55.       return consoleSettingsRef.browserTheme === 'light'
    
  56.         ? process.env.LIGHT_MODE_DIMMED_WARNING_COLOR
    
  57.         : process.env.DARK_MODE_DIMMED_WARNING_COLOR;
    
  58.     case 'error':
    
  59.       return consoleSettingsRef.browserTheme === 'light'
    
  60.         ? process.env.LIGHT_MODE_DIMMED_ERROR_COLOR
    
  61.         : process.env.DARK_MODE_DIMMED_ERROR_COLOR;
    
  62.     case 'log':
    
  63.     default:
    
  64.       return consoleSettingsRef.browserTheme === 'light'
    
  65.         ? process.env.LIGHT_MODE_DIMMED_LOG_COLOR
    
  66.         : process.env.DARK_MODE_DIMMED_LOG_COLOR;
    
  67.   }
    
  68. }
    
  69. type OnErrorOrWarning = (
    
  70.   fiber: Fiber,
    
  71.   type: 'error' | 'warn',
    
  72.   args: Array<any>,
    
  73. ) => void;
    
  74. 
    
  75. const injectedRenderers: Map<
    
  76.   ReactRenderer,
    
  77.   {
    
  78.     currentDispatcherRef: CurrentDispatcherRef,
    
  79.     getCurrentFiber: () => Fiber | null,
    
  80.     onErrorOrWarning: ?OnErrorOrWarning,
    
  81.     workTagMap: WorkTagMap,
    
  82.   },
    
  83. > = new Map();
    
  84. 
    
  85. let targetConsole: Object = console;
    
  86. let targetConsoleMethods: {[string]: $FlowFixMe} = {};
    
  87. for (const method in console) {
    
  88.   targetConsoleMethods[method] = console[method];
    
  89. }
    
  90. 
    
  91. let unpatchFn: null | (() => void) = null;
    
  92. 
    
  93. let isNode = false;
    
  94. try {
    
  95.   isNode = this === global;
    
  96. } catch (error) {}
    
  97. 
    
  98. // Enables e.g. Jest tests to inject a mock console object.
    
  99. export function dangerous_setTargetConsoleForTesting(
    
  100.   targetConsoleForTesting: Object,
    
  101. ): void {
    
  102.   targetConsole = targetConsoleForTesting;
    
  103. 
    
  104.   targetConsoleMethods = ({}: {[string]: $FlowFixMe});
    
  105.   for (const method in targetConsole) {
    
  106.     targetConsoleMethods[method] = console[method];
    
  107.   }
    
  108. }
    
  109. 
    
  110. // v16 renderers should use this method to inject internals necessary to generate a component stack.
    
  111. // These internals will be used if the console is patched.
    
  112. // Injecting them separately allows the console to easily be patched or un-patched later (at runtime).
    
  113. export function registerRenderer(
    
  114.   renderer: ReactRenderer,
    
  115.   onErrorOrWarning?: OnErrorOrWarning,
    
  116. ): void {
    
  117.   const {
    
  118.     currentDispatcherRef,
    
  119.     getCurrentFiber,
    
  120.     findFiberByHostInstance,
    
  121.     version,
    
  122.   } = renderer;
    
  123. 
    
  124.   // Ignore React v15 and older because they don't expose a component stack anyway.
    
  125.   if (typeof findFiberByHostInstance !== 'function') {
    
  126.     return;
    
  127.   }
    
  128. 
    
  129.   // currentDispatcherRef gets injected for v16.8+ to support hooks inspection.
    
  130.   // getCurrentFiber gets injected for v16.9+.
    
  131.   if (currentDispatcherRef != null && typeof getCurrentFiber === 'function') {
    
  132.     const {ReactTypeOfWork} = getInternalReactConstants(version);
    
  133. 
    
  134.     injectedRenderers.set(renderer, {
    
  135.       currentDispatcherRef,
    
  136.       getCurrentFiber,
    
  137.       workTagMap: ReactTypeOfWork,
    
  138.       onErrorOrWarning,
    
  139.     });
    
  140.   }
    
  141. }
    
  142. 
    
  143. const consoleSettingsRef: ConsolePatchSettings = {
    
  144.   appendComponentStack: false,
    
  145.   breakOnConsoleErrors: false,
    
  146.   showInlineWarningsAndErrors: false,
    
  147.   hideConsoleLogsInStrictMode: false,
    
  148.   browserTheme: 'dark',
    
  149. };
    
  150. 
    
  151. // Patches console methods to append component stack for the current fiber.
    
  152. // Call unpatch() to remove the injected behavior.
    
  153. export function patch({
    
  154.   appendComponentStack,
    
  155.   breakOnConsoleErrors,
    
  156.   showInlineWarningsAndErrors,
    
  157.   hideConsoleLogsInStrictMode,
    
  158.   browserTheme,
    
  159. }: ConsolePatchSettings): void {
    
  160.   // Settings may change after we've patched the console.
    
  161.   // Using a shared ref allows the patch function to read the latest values.
    
  162.   consoleSettingsRef.appendComponentStack = appendComponentStack;
    
  163.   consoleSettingsRef.breakOnConsoleErrors = breakOnConsoleErrors;
    
  164.   consoleSettingsRef.showInlineWarningsAndErrors = showInlineWarningsAndErrors;
    
  165.   consoleSettingsRef.hideConsoleLogsInStrictMode = hideConsoleLogsInStrictMode;
    
  166.   consoleSettingsRef.browserTheme = browserTheme;
    
  167. 
    
  168.   if (
    
  169.     appendComponentStack ||
    
  170.     breakOnConsoleErrors ||
    
  171.     showInlineWarningsAndErrors
    
  172.   ) {
    
  173.     if (unpatchFn !== null) {
    
  174.       // Don't patch twice.
    
  175.       return;
    
  176.     }
    
  177. 
    
  178.     const originalConsoleMethods: {[string]: $FlowFixMe} = {};
    
  179. 
    
  180.     unpatchFn = () => {
    
  181.       for (const method in originalConsoleMethods) {
    
  182.         try {
    
  183.           targetConsole[method] = originalConsoleMethods[method];
    
  184.         } catch (error) {}
    
  185.       }
    
  186.     };
    
  187. 
    
  188.     OVERRIDE_CONSOLE_METHODS.forEach(method => {
    
  189.       try {
    
  190.         const originalMethod = (originalConsoleMethods[method] = targetConsole[
    
  191.           method
    
  192.         ].__REACT_DEVTOOLS_ORIGINAL_METHOD__
    
  193.           ? targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
    
  194.           : targetConsole[method]);
    
  195. 
    
  196.         // $FlowFixMe[missing-local-annot]
    
  197.         const overrideMethod = (...args) => {
    
  198.           let shouldAppendWarningStack = false;
    
  199.           if (method !== 'log') {
    
  200.             if (consoleSettingsRef.appendComponentStack) {
    
  201.               const lastArg = args.length > 0 ? args[args.length - 1] : null;
    
  202.               const alreadyHasComponentStack =
    
  203.                 typeof lastArg === 'string' && isStringComponentStack(lastArg);
    
  204. 
    
  205.               // If we are ever called with a string that already has a component stack,
    
  206.               // e.g. a React error/warning, don't append a second stack.
    
  207.               shouldAppendWarningStack = !alreadyHasComponentStack;
    
  208.             }
    
  209.           }
    
  210. 
    
  211.           const shouldShowInlineWarningsAndErrors =
    
  212.             consoleSettingsRef.showInlineWarningsAndErrors &&
    
  213.             (method === 'error' || method === 'warn');
    
  214. 
    
  215.           // Search for the first renderer that has a current Fiber.
    
  216.           // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
    
  217.           // eslint-disable-next-line no-for-of-loops/no-for-of-loops
    
  218.           for (const {
    
  219.             currentDispatcherRef,
    
  220.             getCurrentFiber,
    
  221.             onErrorOrWarning,
    
  222.             workTagMap,
    
  223.           } of injectedRenderers.values()) {
    
  224.             const current: ?Fiber = getCurrentFiber();
    
  225.             if (current != null) {
    
  226.               try {
    
  227.                 if (shouldShowInlineWarningsAndErrors) {
    
  228.                   // patch() is called by two places: (1) the hook and (2) the renderer backend.
    
  229.                   // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
    
  230.                   if (typeof onErrorOrWarning === 'function') {
    
  231.                     onErrorOrWarning(
    
  232.                       current,
    
  233.                       ((method: any): 'error' | 'warn'),
    
  234.                       // Copy args before we mutate them (e.g. adding the component stack)
    
  235.                       args.slice(),
    
  236.                     );
    
  237.                   }
    
  238.                 }
    
  239. 
    
  240.                 if (shouldAppendWarningStack) {
    
  241.                   const componentStack = getStackByFiberInDevAndProd(
    
  242.                     workTagMap,
    
  243.                     current,
    
  244.                     currentDispatcherRef,
    
  245.                   );
    
  246.                   if (componentStack !== '') {
    
  247.                     if (isStrictModeOverride(args, method)) {
    
  248.                       args[0] = `${args[0]} %s`;
    
  249.                       args.push(componentStack);
    
  250.                     } else {
    
  251.                       args.push(componentStack);
    
  252.                     }
    
  253.                   }
    
  254.                 }
    
  255.               } catch (error) {
    
  256.                 // Don't let a DevTools or React internal error interfere with logging.
    
  257.                 setTimeout(() => {
    
  258.                   throw error;
    
  259.                 }, 0);
    
  260.               } finally {
    
  261.                 break;
    
  262.               }
    
  263.             }
    
  264.           }
    
  265. 
    
  266.           if (consoleSettingsRef.breakOnConsoleErrors) {
    
  267.             // --- Welcome to debugging with React DevTools ---
    
  268.             // This debugger statement means that you've enabled the "break on warnings" feature.
    
  269.             // Use the browser's Call Stack panel to step out of this override function-
    
  270.             // to where the original warning or error was logged.
    
  271.             // eslint-disable-next-line no-debugger
    
  272.             debugger;
    
  273.           }
    
  274. 
    
  275.           originalMethod(...args);
    
  276.         };
    
  277. 
    
  278.         overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
    
  279.         originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod;
    
  280. 
    
  281.         targetConsole[method] = overrideMethod;
    
  282.       } catch (error) {}
    
  283.     });
    
  284.   } else {
    
  285.     unpatch();
    
  286.   }
    
  287. }
    
  288. 
    
  289. // Removed component stack patch from console methods.
    
  290. export function unpatch(): void {
    
  291.   if (unpatchFn !== null) {
    
  292.     unpatchFn();
    
  293.     unpatchFn = null;
    
  294.   }
    
  295. }
    
  296. 
    
  297. let unpatchForStrictModeFn: null | (() => void) = null;
    
  298. 
    
  299. // NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialRenderInStrictMode
    
  300. export function patchForStrictMode() {
    
  301.   if (consoleManagedByDevToolsDuringStrictMode) {
    
  302.     const overrideConsoleMethods = [
    
  303.       'error',
    
  304.       'group',
    
  305.       'groupCollapsed',
    
  306.       'info',
    
  307.       'log',
    
  308.       'trace',
    
  309.       'warn',
    
  310.     ];
    
  311. 
    
  312.     if (unpatchForStrictModeFn !== null) {
    
  313.       // Don't patch twice.
    
  314.       return;
    
  315.     }
    
  316. 
    
  317.     const originalConsoleMethods: {[string]: $FlowFixMe} = {};
    
  318. 
    
  319.     unpatchForStrictModeFn = () => {
    
  320.       for (const method in originalConsoleMethods) {
    
  321.         try {
    
  322.           targetConsole[method] = originalConsoleMethods[method];
    
  323.         } catch (error) {}
    
  324.       }
    
  325.     };
    
  326. 
    
  327.     overrideConsoleMethods.forEach(method => {
    
  328.       try {
    
  329.         const originalMethod = (originalConsoleMethods[method] = targetConsole[
    
  330.           method
    
  331.         ].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
    
  332.           ? targetConsole[method].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
    
  333.           : targetConsole[method]);
    
  334. 
    
  335.         // $FlowFixMe[missing-local-annot]
    
  336.         const overrideMethod = (...args) => {
    
  337.           if (!consoleSettingsRef.hideConsoleLogsInStrictMode) {
    
  338.             // Dim the text color of the double logs if we're not
    
  339.             // hiding them.
    
  340.             if (isNode) {
    
  341.               originalMethod(DIMMED_NODE_CONSOLE_COLOR, format(...args));
    
  342.             } else {
    
  343.               const color = getConsoleColor(method);
    
  344.               if (color) {
    
  345.                 originalMethod(...formatWithStyles(args, `color: ${color}`));
    
  346.               } else {
    
  347.                 throw Error('Console color is not defined');
    
  348.               }
    
  349.             }
    
  350.           }
    
  351.         };
    
  352. 
    
  353.         overrideMethod.__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ =
    
  354.           originalMethod;
    
  355.         originalMethod.__REACT_DEVTOOLS_STRICT_MODE_OVERRIDE_METHOD__ =
    
  356.           overrideMethod;
    
  357. 
    
  358.         targetConsole[method] = overrideMethod;
    
  359.       } catch (error) {}
    
  360.     });
    
  361.   }
    
  362. }
    
  363. 
    
  364. // NOTE: KEEP IN SYNC with src/hook.js:unpatchConsoleForInitialRenderInStrictMode
    
  365. export function unpatchForStrictMode(): void {
    
  366.   if (consoleManagedByDevToolsDuringStrictMode) {
    
  367.     if (unpatchForStrictModeFn !== null) {
    
  368.       unpatchForStrictModeFn();
    
  369.       unpatchForStrictModeFn = null;
    
  370.     }
    
  371.   }
    
  372. }
    
  373. 
    
  374. export function patchConsoleUsingWindowValues() {
    
  375.   const appendComponentStack =
    
  376.     castBool(window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__) ?? true;
    
  377.   const breakOnConsoleErrors =
    
  378.     castBool(window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__) ?? false;
    
  379.   const showInlineWarningsAndErrors =
    
  380.     castBool(window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__) ?? true;
    
  381.   const hideConsoleLogsInStrictMode =
    
  382.     castBool(window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__) ??
    
  383.     false;
    
  384.   const browserTheme =
    
  385.     castBrowserTheme(window.__REACT_DEVTOOLS_BROWSER_THEME__) ?? 'dark';
    
  386. 
    
  387.   patch({
    
  388.     appendComponentStack,
    
  389.     breakOnConsoleErrors,
    
  390.     showInlineWarningsAndErrors,
    
  391.     hideConsoleLogsInStrictMode,
    
  392.     browserTheme,
    
  393.   });
    
  394. }
    
  395. 
    
  396. // After receiving cached console patch settings from React Native, we set them on window.
    
  397. // When the console is initially patched (in renderer.js and hook.js), these values are read.
    
  398. // The browser extension (etc.) sets these values on window, but through another method.
    
  399. export function writeConsolePatchSettingsToWindow(
    
  400.   settings: ConsolePatchSettings,
    
  401. ): void {
    
  402.   window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ =
    
  403.     settings.appendComponentStack;
    
  404.   window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ =
    
  405.     settings.breakOnConsoleErrors;
    
  406.   window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ =
    
  407.     settings.showInlineWarningsAndErrors;
    
  408.   window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ =
    
  409.     settings.hideConsoleLogsInStrictMode;
    
  410.   window.__REACT_DEVTOOLS_BROWSER_THEME__ = settings.browserTheme;
    
  411. }
    
  412. 
    
  413. export function installConsoleFunctionsToWindow(): void {
    
  414.   window.__REACT_DEVTOOLS_CONSOLE_FUNCTIONS__ = {
    
  415.     patchConsoleUsingWindowValues,
    
  416.     registerRendererWithConsole: registerRenderer,
    
  417.   };
    
  418. }