diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index 25001502f1c2b..d588d4f91e9f0 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -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__; diff --git a/packages/react-devtools-inline/src/backend.js b/packages/react-devtools-inline/src/backend.js index e7d0485b37569..fca1535c4e5ba 100644 --- a/packages/react-devtools-inline/src/backend.js +++ b/packages/react-devtools-inline/src/backend.js @@ -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); } diff --git a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js index b99db5c540097..54af62db44e92 100644 --- a/packages/react-devtools-shared/src/__tests__/componentStacks-test.js +++ b/packages/react-devtools-shared/src/__tests__/componentStacks-test.js @@ -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()); - 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()); - 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 **)', + ]); }); }); diff --git a/packages/react-devtools-shared/src/__tests__/console-test.js b/packages/react-devtools-shared/src/__tests__/console-test.js index 516762132e884..00d6d9712679a 100644 --- a/packages/react-devtools-shared/src/__tests__/console-test.js +++ b/packages/react-devtools-shared/src/__tests__/console-test.js @@ -7,52 +7,25 @@ * @flow */ -import {getVersionedRenderImplementation, normalizeCodeLocInfo} from './utils'; +import { + getVersionedRenderImplementation, + normalizeCodeLocInfo, +} from 'react-devtools-shared/src/__tests__/utils'; let React; let ReactDOMClient; let act; -let fakeConsole; -let mockError; -let mockInfo; -let mockGroup; -let mockGroupCollapsed; -let mockLog; -let mockWarn; -let patchConsole; -let unpatchConsole; let rendererID; let supportsOwnerStacks = false; describe('console', () => { beforeEach(() => { - const Console = require('react-devtools-shared/src/backend/console'); - - patchConsole = Console.patch; - unpatchConsole = Console.unpatch; - - // Patch a fake console so we can verify with tests below. - // Patching the real console is too complicated, - // because Jest itself has hooks into it as does our test env setup. - mockError = jest.fn(); - mockInfo = jest.fn(); - mockGroup = jest.fn(); - mockGroupCollapsed = jest.fn(); - mockLog = jest.fn(); - mockWarn = jest.fn(); - fakeConsole = { - error: mockError, - info: mockInfo, - log: mockLog, - warn: mockWarn, - group: mockGroup, - groupCollapsed: mockGroupCollapsed, - }; + const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => { + rendererID = inject(internals); - Console.dangerous_setTargetConsoleForTesting(fakeConsole); - global.__REACT_DEVTOOLS_GLOBAL_HOOK__.dangerous_setTargetConsoleForTesting( - fakeConsole, - ); + return rendererID; + }; React = require('react'); if ( @@ -69,137 +42,44 @@ describe('console', () => { const {render} = getVersionedRenderImplementation(); - // @reactVersion >=18.0 - it('should not patch console methods that are not explicitly overridden', () => { - expect(fakeConsole.error).not.toBe(mockError); - expect(fakeConsole.info).toBe(mockInfo); - expect(fakeConsole.log).toBe(mockLog); - expect(fakeConsole.warn).not.toBe(mockWarn); - expect(fakeConsole.group).toBe(mockGroup); - expect(fakeConsole.groupCollapsed).toBe(mockGroupCollapsed); - }); - - // @reactVersion >=18.0 - it('should patch the console when appendComponentStack is enabled', () => { - unpatchConsole(); - - expect(fakeConsole.error).toBe(mockError); - expect(fakeConsole.warn).toBe(mockWarn); - - patchConsole({ - appendComponentStack: true, - breakOnConsoleErrors: false, - showInlineWarningsAndErrors: false, - }); - - expect(fakeConsole.error).not.toBe(mockError); - expect(fakeConsole.warn).not.toBe(mockWarn); - }); - - // @reactVersion >=18.0 - it('should patch the console when breakOnConsoleErrors is enabled', () => { - unpatchConsole(); - - expect(fakeConsole.error).toBe(mockError); - expect(fakeConsole.warn).toBe(mockWarn); - - patchConsole({ - appendComponentStack: false, - breakOnConsoleErrors: true, - showInlineWarningsAndErrors: false, - }); - - expect(fakeConsole.error).not.toBe(mockError); - expect(fakeConsole.warn).not.toBe(mockWarn); - }); - - // @reactVersion >=18.0 - it('should patch the console when showInlineWarningsAndErrors is enabled', () => { - unpatchConsole(); - - expect(fakeConsole.error).toBe(mockError); - expect(fakeConsole.warn).toBe(mockWarn); - - patchConsole({ - appendComponentStack: false, - breakOnConsoleErrors: false, - showInlineWarningsAndErrors: true, - }); - - expect(fakeConsole.error).not.toBe(mockError); - expect(fakeConsole.warn).not.toBe(mockWarn); - }); - - // @reactVersion >=18.0 - it('should only patch the console once', () => { - const {error, warn} = fakeConsole; - - patchConsole({ - appendComponentStack: true, - breakOnConsoleErrors: false, - showInlineWarningsAndErrors: false, - }); - - expect(fakeConsole.error).toBe(error); - expect(fakeConsole.warn).toBe(warn); - }); - - // @reactVersion >=18.0 - it('should un-patch when requested', () => { - expect(fakeConsole.error).not.toBe(mockError); - expect(fakeConsole.warn).not.toBe(mockWarn); + // @reactVersion >= 18.0 + it('should pass through logs when there is no current fiber', () => { + expect(global.consoleLogMock).toHaveBeenCalledTimes(0); + expect(global.consoleWarnMock).toHaveBeenCalledTimes(0); + expect(global.consoleErrorMock).toHaveBeenCalledTimes(0); - unpatchConsole(); + console.log('log'); + console.warn('warn'); + console.error('error'); - expect(fakeConsole.error).toBe(mockError); - expect(fakeConsole.warn).toBe(mockWarn); + expect(global.consoleLogMock.mock.calls).toEqual([['log']]); + expect(global.consoleWarnMock.mock.calls).toEqual([['warn']]); + expect(global.consoleErrorMock.mock.calls).toEqual([['error']]); }); - // @reactVersion >=18.0 - it('should pass through logs when there is no current fiber', () => { - expect(mockLog).toHaveBeenCalledTimes(0); - expect(mockWarn).toHaveBeenCalledTimes(0); - expect(mockError).toHaveBeenCalledTimes(0); - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - }); - - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should not append multiple stacks', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true; const Child = ({children}) => { - fakeConsole.warn('warn\n in Child (at fake.js:123)'); - fakeConsole.error('error', '\n in Child (at fake.js:123)'); + console.warn('warn', '\n in Child (at fake.js:123)'); + console.error('error', '\n in Child (at fake.js:123)'); return null; }; act(() => render()); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe( - 'warn\n in Child (at fake.js:123)', - ); - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(mockError.mock.calls[0][1]).toBe('\n in Child (at fake.js:123)'); + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual(['warn', '\n in Child (at **)']); + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual(['error', '\n in Child (at **)']); }); - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should append component stacks to errors and warnings logged during render', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true; const Intermediate = ({children}) => children; const Parent = ({children}) => ( @@ -208,36 +88,34 @@ describe('console', () => { ); const Child = ({children}) => { - fakeConsole.error('error'); - fakeConsole.log('log'); - fakeConsole.warn('warn'); + console.error('error'); + console.log('log'); + console.warn('warn'); return null; }; act(() => render()); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + expect(global.consoleLogMock.mock.calls).toEqual([['log']]); + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'warn', supportsOwnerStacks ? '\n in Child (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( + ]); + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'error', supportsOwnerStacks ? '\n in Child (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); + ]); }); - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should append component stacks to errors and warnings logged from effects', () => { const Intermediate = ({children}) => children; const Parent = ({children}) => ( @@ -247,60 +125,63 @@ describe('console', () => { ); const Child = ({children}) => { React.useLayoutEffect(function Child_useLayoutEffect() { - fakeConsole.error('active error'); - fakeConsole.log('active log'); - fakeConsole.warn('active warn'); + console.error('active error'); + console.log('active log'); + console.warn('active warn'); }); React.useEffect(function Child_useEffect() { - fakeConsole.error('passive error'); - fakeConsole.log('passive log'); - fakeConsole.warn('passive warn'); + console.error('passive error'); + console.log('passive log'); + console.warn('passive warn'); }); return null; }; act(() => render()); - expect(mockLog).toHaveBeenCalledTimes(2); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('active log'); - expect(mockLog.mock.calls[1]).toHaveLength(1); - expect(mockLog.mock.calls[1][0]).toBe('passive log'); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(mockWarn.mock.calls[0][0]).toBe('active warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + expect(global.consoleLogMock.mock.calls).toEqual([ + ['active log'], + ['passive log'], + ]); + + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'active warn', supportsOwnerStacks ? '\n in Child_useLayoutEffect (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1][0]).toBe('passive warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual( + ]); + expect( + global.consoleWarnMock.mock.calls[1].map(normalizeCodeLocInfo), + ).toEqual([ + 'passive warn', supportsOwnerStacks ? '\n in Child_useEffect (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('active error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( + ]); + + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'active error', supportsOwnerStacks ? '\n in Child_useLayoutEffect (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1][0]).toBe('passive error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe( + ]); + expect( + global.consoleErrorMock.mock.calls[1].map(normalizeCodeLocInfo), + ).toEqual([ + 'passive error', supportsOwnerStacks ? '\n in Child_useEffect (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); + ]); }); - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should append component stacks to errors and warnings logged from commit hooks', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true; const Intermediate = ({children}) => children; const Parent = ({children}) => ( @@ -310,14 +191,14 @@ describe('console', () => { ); class Child extends React.Component { componentDidMount() { - fakeConsole.error('didMount error'); - fakeConsole.log('didMount log'); - fakeConsole.warn('didMount warn'); + console.error('didMount error'); + console.log('didMount log'); + console.warn('didMount warn'); } componentDidUpdate() { - fakeConsole.error('didUpdate error'); - fakeConsole.log('didUpdate log'); - fakeConsole.warn('didUpdate warn'); + console.error('didUpdate error'); + console.log('didUpdate log'); + console.warn('didUpdate warn'); } render() { return null; @@ -327,44 +208,47 @@ describe('console', () => { act(() => render()); act(() => render()); - expect(mockLog).toHaveBeenCalledTimes(2); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('didMount log'); - expect(mockLog.mock.calls[1]).toHaveLength(1); - expect(mockLog.mock.calls[1][0]).toBe('didUpdate log'); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(mockWarn.mock.calls[0][0]).toBe('didMount warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + expect(global.consoleLogMock.mock.calls).toEqual([ + ['didMount log'], + ['didUpdate log'], + ]); + + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'didMount warn', supportsOwnerStacks ? '\n in Child.componentDidMount (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1][0]).toBe('didUpdate warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual( + ]); + expect( + global.consoleWarnMock.mock.calls[1].map(normalizeCodeLocInfo), + ).toEqual([ + 'didUpdate warn', supportsOwnerStacks ? '\n in Child.componentDidUpdate (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('didMount error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( + ]); + + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'didMount error', supportsOwnerStacks ? '\n in Child.componentDidMount (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1][0]).toBe('didUpdate error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe( + ]); + expect( + global.consoleErrorMock.mock.calls[1].map(normalizeCodeLocInfo), + ).toEqual([ + 'didUpdate error', supportsOwnerStacks ? '\n in Child.componentDidUpdate (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); + ]); }); - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should append component stacks to errors and warnings logged from gDSFP', () => { const Intermediate = ({children}) => children; const Parent = ({children}) => ( @@ -375,9 +259,9 @@ describe('console', () => { class Child extends React.Component { state = {}; static getDerivedStateFromProps() { - fakeConsole.error('error'); - fakeConsole.log('log'); - fakeConsole.warn('warn'); + console.error('error'); + console.log('log'); + console.warn('warn'); return null; } render() { @@ -387,71 +271,27 @@ describe('console', () => { act(() => render()); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + expect(global.consoleLogMock.mock.calls).toEqual([['log']]); + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'warn', supportsOwnerStacks ? '\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( + ]); + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'error', supportsOwnerStacks ? '\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - }); - - // @reactVersion >=18.0 - it('should append stacks after being uninstalled and reinstalled', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - - const Child = ({children}) => { - fakeConsole.warn('warn'); - fakeConsole.error('error'); - return null; - }; - - act(() => render()); - - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - - patchConsole({ - appendComponentStack: true, - breakOnConsoleErrors: false, - showInlineWarningsAndErrors: false, - }); - act(() => render()); - - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1][0]).toBe('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual( - '\n in Child (at **)', - ); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe( - '\n in Child (at **)', - ); + ]); }); - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should be resilient to prepareStackTrace', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true; - Error.prepareStackTrace = function (error, callsites) { const stack = ['An error occurred:', error.message]; for (let i = 0; i < callsites.length; i++) { @@ -473,62 +313,66 @@ describe('console', () => { ); const Child = ({children}) => { - fakeConsole.error('error'); - fakeConsole.log('log'); - fakeConsole.warn('warn'); + console.error('error'); + console.log('log'); + console.warn('warn'); return null; }; act(() => render()); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + expect(global.consoleLogMock.mock.calls).toEqual([['log']]); + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'warn', supportsOwnerStacks ? '\n in Child (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( + ]); + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'error', supportsOwnerStacks ? '\n in Child (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); + ]); }); - // @reactVersion >=18.0 + // @reactVersion >= 18.0 it('should correctly log Symbols', () => { + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + const Component = ({children}) => { - fakeConsole.warn('Symbol:', Symbol('')); + console.warn('Symbol:', Symbol('')); return null; }; act(() => render()); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0][0]).toBe('Symbol:'); + expect(global.consoleWarnMock.mock.calls).toMatchInlineSnapshot(` + [ + [ + "Symbol:", + Symbol(), + ], + ] + `); }); it('should double log if hideConsoleLogsInStrictMode is disabled in Strict mode', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + false; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); - fakeConsole.info('info'); - fakeConsole.group('group'); - fakeConsole.groupCollapsed('groupCollapsed'); + console.log('log'); + console.warn('warn'); + console.error('error'); return
; } @@ -539,77 +383,38 @@ describe('console', () => { , ), ); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockLog.mock.calls[1]).toEqual([ + + expect(global.consoleLogMock).toHaveBeenCalledTimes(2); + expect(global.consoleLogMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log', ]); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1]).toEqual([ + expect(global.consoleWarnMock).toHaveBeenCalledTimes(2); + expect(global.consoleWarnMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn', ]); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1]).toEqual([ + expect(global.consoleErrorMock).toHaveBeenCalledTimes(2); + expect(global.consoleErrorMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error', ]); - - expect(mockInfo).toHaveBeenCalledTimes(2); - expect(mockInfo.mock.calls[0]).toHaveLength(1); - expect(mockInfo.mock.calls[0][0]).toBe('info'); - expect(mockInfo.mock.calls[1]).toHaveLength(2); - expect(mockInfo.mock.calls[1]).toEqual([ - '\x1b[2;38;2;124;124;124m%s\x1b[0m', - 'info', - ]); - - expect(mockGroup).toHaveBeenCalledTimes(2); - expect(mockGroup.mock.calls[0]).toHaveLength(1); - expect(mockGroup.mock.calls[0][0]).toBe('group'); - expect(mockGroup.mock.calls[1]).toHaveLength(2); - expect(mockGroup.mock.calls[1]).toEqual([ - '\x1b[2;38;2;124;124;124m%s\x1b[0m', - 'group', - ]); - - expect(mockGroupCollapsed).toHaveBeenCalledTimes(2); - expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1); - expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed'); - expect(mockGroupCollapsed.mock.calls[1]).toHaveLength(2); - expect(mockGroupCollapsed.mock.calls[1]).toEqual([ - '\x1b[2;38;2;124;124;124m%s\x1b[0m', - 'groupCollapsed', - ]); }); it('should not double log if hideConsoleLogsInStrictMode is enabled in Strict mode', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + true; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { - console.log( - 'CALL', - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__, - ); - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); - fakeConsole.info('info'); - fakeConsole.group('group'); - fakeConsole.groupCollapsed('groupCollapsed'); + console.log('log'); + console.warn('warn'); + console.error('error'); return
; } @@ -621,54 +426,29 @@ describe('console', () => { ), ); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - - expect(mockInfo).toHaveBeenCalledTimes(1); - expect(mockInfo.mock.calls[0]).toHaveLength(1); - expect(mockInfo.mock.calls[0][0]).toBe('info'); - - expect(mockGroup).toHaveBeenCalledTimes(1); - expect(mockGroup.mock.calls[0]).toHaveLength(1); - expect(mockGroup.mock.calls[0][0]).toBe('group'); - - expect(mockGroupCollapsed).toHaveBeenCalledTimes(1); - expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1); - expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed'); + expect(global.consoleLogMock).toHaveBeenCalledTimes(1); + expect(global.consoleWarnMock).toHaveBeenCalledTimes(1); + expect(global.consoleErrorMock).toHaveBeenCalledTimes(1); }); it('should double log from Effects if hideConsoleLogsInStrictMode is disabled in Strict mode', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + false; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { React.useEffect(() => { - fakeConsole.log('log effect create'); - fakeConsole.warn('warn effect create'); - fakeConsole.error('error effect create'); - fakeConsole.info('info effect create'); - fakeConsole.group('group effect create'); - fakeConsole.groupCollapsed('groupCollapsed effect create'); + console.log('log effect create'); + console.warn('warn effect create'); + console.error('error effect create'); return () => { - fakeConsole.log('log effect cleanup'); - fakeConsole.warn('warn effect cleanup'); - fakeConsole.error('error effect cleanup'); - fakeConsole.info('info effect cleanup'); - fakeConsole.group('group effect cleanup'); - fakeConsole.groupCollapsed('groupCollapsed effect cleanup'); + console.log('log effect cleanup'); + console.warn('warn effect cleanup'); + console.error('error effect cleanup'); }; }); @@ -682,61 +462,41 @@ describe('console', () => { , ), ); - expect(mockLog.mock.calls).toEqual([ + expect(global.consoleLogMock.mock.calls).toEqual([ ['log effect create'], ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log effect cleanup'], ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log effect create'], ]); - expect(mockWarn.mock.calls).toEqual([ + expect(global.consoleWarnMock.mock.calls).toEqual([ ['warn effect create'], ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn effect cleanup'], ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn effect create'], ]); - expect(mockError.mock.calls).toEqual([ + expect(global.consoleErrorMock.mock.calls).toEqual([ ['error effect create'], ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error effect cleanup'], ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error effect create'], ]); - expect(mockInfo.mock.calls).toEqual([ - ['info effect create'], - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'info effect cleanup'], - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'info effect create'], - ]); - expect(mockGroup.mock.calls).toEqual([ - ['group effect create'], - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'group effect cleanup'], - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'group effect create'], - ]); - expect(mockGroupCollapsed.mock.calls).toEqual([ - ['groupCollapsed effect create'], - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'groupCollapsed effect cleanup'], - ['\x1b[2;38;2;124;124;124m%s\x1b[0m', 'groupCollapsed effect create'], - ]); }); it('should not double log from Effects if hideConsoleLogsInStrictMode is enabled in Strict mode', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + true; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { React.useEffect(() => { - fakeConsole.log('log effect create'); - fakeConsole.warn('warn effect create'); - fakeConsole.error('error effect create'); - fakeConsole.info('info effect create'); - fakeConsole.group('group effect create'); - fakeConsole.groupCollapsed('groupCollapsed effect create'); + console.log('log effect create'); + console.warn('warn effect create'); + console.error('error effect create'); return () => { - fakeConsole.log('log effect cleanup'); - fakeConsole.warn('warn effect cleanup'); - fakeConsole.error('error effect cleanup'); - fakeConsole.info('info effect cleanup'); - fakeConsole.group('group effect cleanup'); - fakeConsole.groupCollapsed('groupCollapsed effect cleanup'); + console.log('log effect cleanup'); + console.warn('warn effect cleanup'); + console.error('error effect cleanup'); }; }); @@ -750,31 +510,25 @@ describe('console', () => { , ), ); - expect(mockLog.mock.calls).toEqual([['log effect create']]); - expect(mockWarn.mock.calls).toEqual([['warn effect create']]); - expect(mockError.mock.calls).toEqual([['error effect create']]); - expect(mockInfo.mock.calls).toEqual([['info effect create']]); - expect(mockGroup.mock.calls).toEqual([['group effect create']]); - expect(mockGroupCollapsed.mock.calls).toEqual([ - ['groupCollapsed effect create'], - ]); + + expect(global.consoleLogMock).toHaveBeenCalledTimes(1); + expect(global.consoleWarnMock).toHaveBeenCalledTimes(1); + expect(global.consoleErrorMock).toHaveBeenCalledTimes(1); }); it('should double log from useMemo if hideConsoleLogsInStrictMode is disabled in Strict mode', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + false; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { React.useMemo(() => { - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); - fakeConsole.info('info'); - fakeConsole.group('group'); - fakeConsole.groupCollapsed('groupCollapsed'); + console.log('log'); + console.warn('warn'); + console.error('error'); }, []); return
; } @@ -786,78 +540,39 @@ describe('console', () => { , ), ); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockLog.mock.calls[1]).toEqual([ + + expect(global.consoleLogMock).toHaveBeenCalledTimes(2); + expect(global.consoleLogMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log', ]); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1]).toEqual([ + expect(global.consoleWarnMock).toHaveBeenCalledTimes(2); + expect(global.consoleWarnMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn', ]); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1]).toEqual([ + expect(global.consoleErrorMock).toHaveBeenCalledTimes(2); + expect(global.consoleErrorMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error', ]); - - expect(mockInfo).toHaveBeenCalledTimes(2); - expect(mockInfo.mock.calls[0]).toHaveLength(1); - expect(mockInfo.mock.calls[0][0]).toBe('info'); - expect(mockInfo.mock.calls[1]).toHaveLength(2); - expect(mockInfo.mock.calls[1]).toEqual([ - '\x1b[2;38;2;124;124;124m%s\x1b[0m', - 'info', - ]); - - expect(mockGroup).toHaveBeenCalledTimes(2); - expect(mockGroup.mock.calls[0]).toHaveLength(1); - expect(mockGroup.mock.calls[0][0]).toBe('group'); - expect(mockGroup.mock.calls[1]).toHaveLength(2); - expect(mockGroup.mock.calls[1]).toEqual([ - '\x1b[2;38;2;124;124;124m%s\x1b[0m', - 'group', - ]); - - expect(mockGroupCollapsed).toHaveBeenCalledTimes(2); - expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1); - expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed'); - expect(mockGroupCollapsed.mock.calls[1]).toHaveLength(2); - expect(mockGroupCollapsed.mock.calls[1]).toEqual([ - '\x1b[2;38;2;124;124;124m%s\x1b[0m', - 'groupCollapsed', - ]); }); it('should not double log from useMemo fns if hideConsoleLogsInStrictMode is enabled in Strict mode', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + true; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { React.useMemo(() => { - console.log( - 'CALL', - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__, - ); - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); - fakeConsole.info('info'); - fakeConsole.group('group'); - fakeConsole.groupCollapsed('groupCollapsed'); + console.log('log'); + console.warn('warn'); + console.error('error'); }, []); return
; } @@ -870,49 +585,27 @@ describe('console', () => { ), ); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - - expect(mockInfo).toHaveBeenCalledTimes(1); - expect(mockInfo.mock.calls[0]).toHaveLength(1); - expect(mockInfo.mock.calls[0][0]).toBe('info'); - - expect(mockGroup).toHaveBeenCalledTimes(1); - expect(mockGroup.mock.calls[0]).toHaveLength(1); - expect(mockGroup.mock.calls[0][0]).toBe('group'); - - expect(mockGroupCollapsed).toHaveBeenCalledTimes(1); - expect(mockGroupCollapsed.mock.calls[0]).toHaveLength(1); - expect(mockGroupCollapsed.mock.calls[0][0]).toBe('groupCollapsed'); + expect(global.consoleLogMock).toHaveBeenCalledTimes(1); + expect(global.consoleWarnMock).toHaveBeenCalledTimes(1); + expect(global.consoleErrorMock).toHaveBeenCalledTimes(1); }); it('should double log in Strict mode initial render for extension', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + false; // This simulates a render that happens before React DevTools have finished // their handshake to attach the React DOM renderer functions to DevTools // In this case, we should still be able to mock the console in Strict mode - global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.set( - rendererID, - null, - ); + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.delete(rendererID); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); + console.log('log'); + console.warn('warn'); + console.error('error'); return
; } @@ -924,52 +617,41 @@ describe('console', () => { ), ); - expect(mockLog).toHaveBeenCalledTimes(2); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockLog.mock.calls[1]).toHaveLength(2); - expect(mockLog.mock.calls[1]).toEqual([ + expect(global.consoleLogMock).toHaveBeenCalledTimes(2); + expect(global.consoleLogMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'log', ]); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - expect(mockWarn.mock.calls[1]).toHaveLength(2); - expect(mockWarn.mock.calls[1]).toEqual([ + expect(global.consoleWarnMock).toHaveBeenCalledTimes(2); + expect(global.consoleWarnMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'warn', ]); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(mockError.mock.calls[1]).toHaveLength(2); - expect(mockError.mock.calls[1]).toEqual([ + expect(global.consoleErrorMock).toHaveBeenCalledTimes(2); + expect(global.consoleErrorMock.mock.calls[1]).toEqual([ '\x1b[2;38;2;124;124;124m%s\x1b[0m', 'error', ]); }); it('should not double log in Strict mode initial render for extension', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + true; // This simulates a render that happens before React DevTools have finished // their handshake to attach the React DOM renderer functions to DevTools // In this case, we should still be able to mock the console in Strict mode - global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.set( - rendererID, - null, - ); + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.delete(rendererID); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); function App() { - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); + console.log('log'); + console.warn('warn'); + console.error('error'); return
; } @@ -980,22 +662,16 @@ describe('console', () => { , ), ); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(1); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(1); - expect(mockError.mock.calls[0][0]).toBe('error'); + expect(global.consoleLogMock).toHaveBeenCalledTimes(1); + expect(global.consoleWarnMock).toHaveBeenCalledTimes(1); + expect(global.consoleErrorMock).toHaveBeenCalledTimes(1); }); it('should properly dim component stacks during strict mode double log', () => { - global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true; - global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.appendComponentStack = true; + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.settings.hideConsoleLogsInStrictMode = + false; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); @@ -1007,8 +683,8 @@ describe('console', () => { ); const Child = ({children}) => { - fakeConsole.error('error'); - fakeConsole.warn('warn'); + console.error('error'); + console.warn('warn'); return null; }; @@ -1020,140 +696,41 @@ describe('console', () => { ), ); - expect(mockWarn).toHaveBeenCalledTimes(2); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual( + expect( + global.consoleWarnMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'warn', supportsOwnerStacks ? '\n in Child (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockWarn.mock.calls[1]).toHaveLength(3); - expect(mockWarn.mock.calls[1][0]).toEqual( + ]); + + expect( + global.consoleWarnMock.mock.calls[1].map(normalizeCodeLocInfo), + ).toEqual([ '\x1b[2;38;2;124;124;124m%s %o\x1b[0m', - ); - expect(mockWarn.mock.calls[1][1]).toMatch('warn'); - expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][2]).trim()).toEqual( + 'warn', supportsOwnerStacks - ? 'in Object.overrideMethod (at **)' + // TODO: This leading frame is due to our extra wrapper that shouldn't exist. - '\n in Child (at **)\n in Parent (at **)' + ? '\n in Child (at **)\n in Parent (at **)' : 'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); + ]); - expect(mockError).toHaveBeenCalledTimes(2); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toEqual( + expect( + global.consoleErrorMock.mock.calls[0].map(normalizeCodeLocInfo), + ).toEqual([ + 'error', supportsOwnerStacks ? '\n in Child (at **)\n in Parent (at **)' : '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - expect(mockError.mock.calls[1]).toHaveLength(3); - expect(mockError.mock.calls[1][0]).toEqual( + ]); + expect( + global.consoleErrorMock.mock.calls[1].map(normalizeCodeLocInfo), + ).toEqual([ '\x1b[2;38;2;124;124;124m%s %o\x1b[0m', - ); - expect(mockError.mock.calls[1][1]).toEqual('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[1][2]).trim()).toEqual( + 'error', supportsOwnerStacks - ? 'in Object.overrideMethod (at **)' + // TODO: This leading frame is due to our extra wrapper that shouldn't exist. - '\n in Child (at **)\n in Parent (at **)' + ? '\n in Child (at **)\n in Parent (at **)' : 'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)', - ); - }); -}); - -describe('console error', () => { - beforeEach(() => { - jest.resetModules(); - - const Console = require('react-devtools-shared/src/backend/console'); - patchConsole = Console.patch; - unpatchConsole = Console.unpatch; - - // Patch a fake console so we can verify with tests below. - // Patching the real console is too complicated, - // because Jest itself has hooks into it as does our test env setup. - mockError = jest.fn(); - mockInfo = jest.fn(); - mockGroup = jest.fn(); - mockGroupCollapsed = jest.fn(); - mockLog = jest.fn(); - mockWarn = jest.fn(); - fakeConsole = { - error: mockError, - info: mockInfo, - log: mockLog, - warn: mockWarn, - group: mockGroup, - groupCollapsed: mockGroupCollapsed, - }; - - Console.dangerous_setTargetConsoleForTesting(fakeConsole); - - const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject; - global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => { - inject(internals); - - Console.registerRenderer( - () => { - throw Error('foo'); - }, - () => { - return { - enableOwnerStacks: true, - componentStack: '\n at FakeStack (fake-file)', - }; - }, - ); - }; - - React = require('react'); - ReactDOMClient = require('react-dom/client'); - - const utils = require('./utils'); - act = utils.act; - }); - - // @reactVersion >=18.0 - it('error in console log throws without interfering with logging', () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - - function App() { - fakeConsole.log('log'); - fakeConsole.warn('warn'); - fakeConsole.error('error'); - return
; - } - - patchConsole({ - appendComponentStack: true, - breakOnConsoleErrors: false, - showInlineWarningsAndErrors: true, - hideConsoleLogsInStrictMode: false, - }); - - expect(() => { - act(() => { - root.render(); - }); - }).toThrowError('foo'); - - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog.mock.calls[0]).toHaveLength(1); - expect(mockLog.mock.calls[0][0]).toBe('log'); - - expect(mockWarn).toHaveBeenCalledTimes(1); - expect(mockWarn.mock.calls[0]).toHaveLength(2); - expect(mockWarn.mock.calls[0][0]).toBe('warn'); - // An error in showInlineWarningsAndErrors doesn't need to break component stacks. - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( - '\n in FakeStack (at **)', - ); - - expect(mockError).toHaveBeenCalledTimes(1); - expect(mockError.mock.calls[0]).toHaveLength(2); - expect(mockError.mock.calls[0][0]).toBe('error'); - expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe( - '\n in FakeStack (at **)', - ); + ]); }); }); diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index 79cda67f99b18..d4afd05899a56 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -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,61 +104,36 @@ global.gate = fn => { return fn(flags); }; -beforeEach(() => { - global.mockClipboardCopy = jest.fn(); - - // Test environment doesn't support document methods like execCommand() - // Also once the backend components below have been required, - // it's too late for a test to mock the clipboard-js modules. - jest.mock('clipboard-js', () => ({copy: global.mockClipboardCopy})); - - // These files should be required (and re-required) before each test, - // rather than imported at the head of the module. - // That's because we reset modules between tests, - // which disconnects the DevTool's cache from the current dispatcher ref. - const Agent = require('react-devtools-shared/src/backend/agent').default; - const {initBackend} = require('react-devtools-shared/src/backend'); - const Bridge = require('react-devtools-shared/src/bridge').default; - const Store = require('react-devtools-shared/src/devtools/store').default; - const {installHook} = require('react-devtools-shared/src/hook'); - const { - getDefaultComponentFilters, - setSavedComponentFilters, - } = require('react-devtools-shared/src/utils'); +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; + } - // Fake timers let us flush Bridge operations between setup and assertions. - jest.useFakeTimers(); + return global._ignoredErrorOrWarningMessages.some(errorOrWarningMessage => { + return firstArg.indexOf(errorOrWarningMessage) !== -1; + }); +} - // We use fake timers heavily in tests but the bridge batching now uses microtasks. - global.devtoolsJestTestScheduler = callback => { - setTimeout(callback, 0); - }; +function patchConsoleForTestingBeforeHookInstallation() { + const originalConsoleError = console.error; + const originalConsoleWarn = console.warn; + const originalConsoleLog = console.log; - // Use utils.js#withErrorsOrWarningsIgnored instead of directly mutating this array. - 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; - }, - ); + const consoleErrorMock = jest.fn(); + const consoleWarnMock = jest.fn(); + const consoleLogMock = jest.fn(); - return shouldFilter; - } + global.consoleErrorMock = consoleErrorMock; + global.consoleWarnMock = consoleWarnMock; + global.consoleLogMock = consoleLogMock; - const originalConsoleError = console.error; console.error = (...args) => { let firstArg = args[0]; if (typeof firstArg === 'string' && firstArg.startsWith('Warning: ')) { @@ -184,17 +160,68 @@ beforeEach(() => { // Errors can be ignored by running in a special context provided by utils.js#withErrorsOrWarningsIgnored return; } + + consoleErrorMock(...args); 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; } + + 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() + // Also once the backend components below have been required, + // it's too late for a test to mock the clipboard-js modules. + jest.mock('clipboard-js', () => ({copy: global.mockClipboardCopy})); + + // These files should be required (and re-required) before each test, + // rather than imported at the head of the module. + // That's because we reset modules between tests, + // which disconnects the DevTool's cache from the current dispatcher ref. + const Agent = require('react-devtools-shared/src/backend/agent').default; + const {initBackend} = require('react-devtools-shared/src/backend'); + const Bridge = require('react-devtools-shared/src/bridge').default; + const Store = require('react-devtools-shared/src/devtools/store').default; + const {installHook} = require('react-devtools-shared/src/hook'); + const { + getDefaultComponentFilters, + setSavedComponentFilters, + } = require('react-devtools-shared/src/utils'); + + // Fake timers let us flush Bridge operations between setup and assertions. + jest.useFakeTimers(); + + // We use fake timers heavily in tests but the bridge batching now uses microtasks. + global.devtoolsJestTestScheduler = callback => { + setTimeout(callback, 0); + }; + + // Use utils.js#withErrorsOrWarningsIgnored instead of directly mutating this array. + global._ignoredErrorOrWarningMessages = [ + 'react-test-renderer is deprecated.', + ]; // 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 store = new Store(((bridge: any): FrontendBridge)); + 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)); - 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. diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 8112fdcd2584b..814c6c1c40e67 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -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, + settings: $ReadOnly, ) => 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) => void = diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index 0b6b19626a9bb..9e61285b50fe4 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -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): 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 = / \(\\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm; -function areStackTracesEqual(a: string, b: string): boolean { - return a.replace(frameDiffs, '') === b.replace(frameDiffs, ''); -} - -function restorePotentiallyModifiedArgs(args: Array): Array { - // 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): 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 : - // 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 : - // 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, - }; -} diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 2fd768b5d01c3..b8636c557ea25 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -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, }; diff --git a/packages/react-devtools-shared/src/backend/flight/renderer.js b/packages/react-devtools-shared/src/backend/flight/renderer.js index 065dc81a071a7..3d8befd4215e7 100644 --- a/packages/react-devtools-shared/src/backend/flight/renderer.js +++ b/packages/react-devtools-shared/src/backend/flight/renderer.js @@ -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 []; diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 828d05fc27f81..8e26dcae445ee 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -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, }; diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index fa9949e3ddc5d..bdc93a19baa8b 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -404,7 +404,6 @@ export type RendererInterface = { path: Array, value: any, ) => void, - patchConsoleForStrictMode: () => void, getElementAttributeByPath: ( id: number, path: Array, @@ -427,7 +426,6 @@ export type RendererInterface = { path: Array, count: number, ) => void, - unpatchConsoleForStrictMode: () => void, updateComponentFilters: (componentFilters: Array) => void, getEnvironmentNames: () => Array, diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index 65a52b571a680..156319b366d71 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -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], getEnvironmentNames: [], - updateConsolePatchSettings: [ConsolePatchSettings], + updateConsolePatchSettings: [DevToolsHookSettings], viewAttributeSource: [ViewAttributeSourceParams], viewElementSource: [ElementAndRendererID], diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index 1dd1fcbd4c455..ac56d13d6ab4e 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -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 = / \(\\)$|\@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, - style?: string, - ): $ReadOnlyArray { + function formatWithStyles(inputArgs: Array, style?: string): Array { 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) => { - // 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(); - } + let isRunningDuringStrictModeInvocation = false; + function setStrictMode(rendererID: RendererID, isStrictMode: boolean) { + isRunningDuringStrictModeInvocation = isStrictMode; + + if (isStrictMode) { + patchConsoleForStrictMode(); } 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; - - patchConsoleForInitialCommitInStrictMode(hideConsoleLogsInStrictMode); - } else { - unpatchConsoleForInitialCommitInStrictMode(); - } + 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) => 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 = []; @@ -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) => 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 : + // 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 : + // 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} = {}; const rendererInterfaces = new Map(); @@ -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__',