diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index 817b34386c205..b3bea12e14a1f 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -18,6 +18,7 @@ import Bridge from 'react-devtools-shared/src/bridge'; import Store from 'react-devtools-shared/src/devtools/store'; import { getAppendComponentStack, + getsuppressDoubleLogging, getBreakOnConsoleErrors, getSavedComponentFilters, } from 'react-devtools-shared/src/utils'; @@ -298,6 +299,9 @@ function startServer( window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify( getAppendComponentStack(), )}; + window.__REACT_DEVTOOLS_SUPPRESS_DOUBLE_LOGGING__ = ${JSON.stringify( + getsuppressDoubleLogging(), + )}; window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify( getBreakOnConsoleErrors(), )}; diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index 00e3735c72c11..a0a6df239ccba 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -7,6 +7,7 @@ import Store from 'react-devtools-shared/src/devtools/store'; import {getBrowserName, getBrowserTheme} from './utils'; import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants'; import { + getsuppressDoubleLogging, getAppendComponentStack, getBreakOnConsoleErrors, getSavedComponentFilters, @@ -30,12 +31,16 @@ let panelCreated = false; // Instead it relies on the extension to pass filters through. function syncSavedPreferences() { const appendComponentStack = getAppendComponentStack(); + const suppressDoubleLogging = getsuppressDoubleLogging(); const breakOnConsoleErrors = getBreakOnConsoleErrors(); const componentFilters = getSavedComponentFilters(); chrome.devtools.inspectedWindow.eval( `window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify( appendComponentStack, )}; + window.__REACT_DEVTOOLS_SUPPRESS_DOUBLE_LOGGING__ = ${JSON.stringify( + suppressDoubleLogging, + )}; window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify( breakOnConsoleErrors, )}; diff --git a/packages/react-devtools-inline/src/backend.js b/packages/react-devtools-inline/src/backend.js index fb46276924652..dc870984e0166 100644 --- a/packages/react-devtools-inline/src/backend.js +++ b/packages/react-devtools-inline/src/backend.js @@ -22,12 +22,14 @@ function startActivation(contentWindow: window) { const { appendComponentStack, + suppressDoubleLogging, breakOnConsoleErrors, componentFilters, } = data; contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack; contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors; + contentWindow.__REACT_DEVTOOLS_SUPPRESS_DOUBLE_LOGGING__ = suppressDoubleLogging; contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters; // TRICKY @@ -39,6 +41,7 @@ function startActivation(contentWindow: window) { if (contentWindow !== window) { window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack; window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors; + window.__REACT_DEVTOOLS_SUPPRESS_DOUBLE_LOGGING__ = suppressDoubleLogging; window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters; } diff --git a/packages/react-devtools-inline/src/frontend.js b/packages/react-devtools-inline/src/frontend.js index 9a5c145471139..2e816034db65d 100644 --- a/packages/react-devtools-inline/src/frontend.js +++ b/packages/react-devtools-inline/src/frontend.js @@ -9,6 +9,7 @@ import { getAppendComponentStack, getBreakOnConsoleErrors, getSavedComponentFilters, + getsuppressDoubleLogging, } from 'react-devtools-shared/src/utils'; import { MESSAGE_TYPE_GET_SAVED_PREFERENCES, @@ -39,6 +40,7 @@ export function initialize( { type: MESSAGE_TYPE_SAVED_PREFERENCES, appendComponentStack: getAppendComponentStack(), + suppressDoubleLogging: getsuppressDoubleLogging(), breakOnConsoleErrors: getBreakOnConsoleErrors(), componentFilters: getSavedComponentFilters(), }, diff --git a/packages/react-devtools-shared/src/__tests__/console-test.js b/packages/react-devtools-shared/src/__tests__/console-test.js index f5e2ff65eaea8..b77b4a7749908 100644 --- a/packages/react-devtools-shared/src/__tests__/console-test.js +++ b/packages/react-devtools-shared/src/__tests__/console-test.js @@ -86,7 +86,6 @@ describe('console', () => { appendComponentStack: true, breakOnWarn: false, }); - expect(fakeConsole.error).toBe(error); expect(fakeConsole.warn).toBe(warn); }); diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 14592952f434f..a6e9f31267b4b 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -446,9 +446,11 @@ export default class Agent extends EventEmitter<{| updateConsolePatchSettings = ({ appendComponentStack, breakOnConsoleErrors, + suppressDoubleLogging, }: {| appendComponentStack: boolean, breakOnConsoleErrors: boolean, + suppressDoubleLogging: boolean, |}) => { // If the frontend preference has change, // or in the case of React Native- if the backend is just finding out the preference- diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index f30d3034db94c..c47f6529f6366 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -88,6 +88,7 @@ type NativeStyleEditor_SetValueParams = {| type UpdateConsolePatchSettingsParams = {| appendComponentStack: boolean, breakOnConsoleErrors: boolean, + suppressDoubleLogging: boolean, |}; type BackendEvents = {| diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index c5223f7cb43d9..4438c6a7f4cdc 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -30,6 +30,9 @@ export const SESSION_STORAGE_RELOAD_AND_PROFILE_KEY = export const LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS = 'React::DevTools::breakOnConsoleErrors'; +export const LOCAL_STORAGE_SHOULD_SUPPRESS_DOUBLE_LOGGING = + 'React::DevTools::suppressDoubleLogging'; + export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY = 'React::DevTools::appendComponentStack'; diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js index bc26ead4fc150..d859b51816843 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js @@ -17,6 +17,8 @@ export default function DebuggingSettings(_: {||}) { const { appendComponentStack, breakOnConsoleErrors, + suppressDoubleLogging, + setsuppressDoubleLogging, setAppendComponentStack, setBreakOnConsoleErrors, } = useContext(SettingsContext); @@ -35,7 +37,18 @@ export default function DebuggingSettings(_: {||}) { Append component stacks to console warnings and errors. </label> </div> - + <div className={styles.Setting}> + <label> + <input + type="checkbox" + checked={!suppressDoubleLogging} + onChange={({currentTarget}) => + setsuppressDoubleLogging(!currentTarget.checked) + } + />{' '} + Suppress console during development-only second render pass + </label> + </div> <div className={styles.Setting}> <label> <input diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index 9196a4b333cff..539197b9874f5 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -21,6 +21,7 @@ import { LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS, LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY, LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY, + LOCAL_STORAGE_SHOULD_SUPPRESS_DOUBLE_LOGGING, } from 'react-devtools-shared/src/constants'; import {useLocalStorage} from '../hooks'; import {BridgeContext} from '../context'; @@ -38,6 +39,9 @@ type Context = {| // Specified as a separate prop so it can trigger a re-render of FixedSizeList. lineHeight: number, + suppressDoubleLogging: boolean, + setsuppressDoubleLogging: (value: boolean) => void, + appendComponentStack: boolean, setAppendComponentStack: (value: boolean) => void, @@ -79,6 +83,15 @@ function SettingsContextController({ 'React::DevTools::theme', 'auto', ); + + const [ + suppressDoubleLogging, + setsuppressDoubleLogging, + ] = useLocalStorage<boolean>( + LOCAL_STORAGE_SHOULD_SUPPRESS_DOUBLE_LOGGING, + false, + ); + const [ appendComponentStack, setAppendComponentStack, @@ -147,8 +160,14 @@ function SettingsContextController({ bridge.send('updateConsolePatchSettings', { appendComponentStack, breakOnConsoleErrors, + suppressDoubleLogging, }); - }, [bridge, appendComponentStack, breakOnConsoleErrors]); + }, [ + bridge, + appendComponentStack, + breakOnConsoleErrors, + suppressDoubleLogging, + ]); useEffect(() => { bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled); @@ -158,6 +177,7 @@ function SettingsContextController({ () => ({ appendComponentStack, breakOnConsoleErrors, + suppressDoubleLogging, displayDensity, lineHeight: displayDensity === 'compact' @@ -165,6 +185,7 @@ function SettingsContextController({ : COMFORTABLE_LINE_HEIGHT, setAppendComponentStack, setBreakOnConsoleErrors, + setsuppressDoubleLogging, setDisplayDensity, setTheme, setTraceUpdatesEnabled, @@ -175,8 +196,10 @@ function SettingsContextController({ appendComponentStack, breakOnConsoleErrors, displayDensity, + suppressDoubleLogging, setAppendComponentStack, setBreakOnConsoleErrors, + setsuppressDoubleLogging, setDisplayDensity, setTheme, setTraceUpdatesEnabled, diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index b0b873cca163d..f992266f87456 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -34,6 +34,7 @@ import { LOCAL_STORAGE_FILTER_PREFERENCES_KEY, LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS, LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY, + LOCAL_STORAGE_SHOULD_SUPPRESS_DOUBLE_LOGGING, } from './constants'; import {ComponentFilterElementType, ElementTypeHostComponent} from './types'; import { @@ -261,6 +262,25 @@ export function getBreakOnConsoleErrors(): boolean { return false; } +export function setsuppressDoubleLogging(value: boolean): void { + localStorageSetItem( + LOCAL_STORAGE_SHOULD_SUPPRESS_DOUBLE_LOGGING, + JSON.stringify(value), + ); +} + +export function getsuppressDoubleLogging(): boolean { + try { + const raw = localStorageGetItem( + LOCAL_STORAGE_SHOULD_SUPPRESS_DOUBLE_LOGGING, + ); + if (raw != null) { + return JSON.parse(raw); + } + } catch (error) {} + return false; +} + export function setBreakOnConsoleErrors(value: boolean): void { localStorageSetItem( LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS, diff --git a/packages/shared/ConsolePatchingDev.js b/packages/shared/ConsolePatchingDev.js index ed7f7f7d188aa..8a7e34e5b739f 100644 --- a/packages/shared/ConsolePatchingDev.js +++ b/packages/shared/ConsolePatchingDev.js @@ -11,6 +11,7 @@ // replaying on render function. This currently only patches the object // lazily which won't cover if the log function was extracted eagerly. // We could also eagerly patch the method. +import {canUseDOM} from './ExecutionEnvironment'; let disabledDepth = 0; let prevLog; @@ -20,65 +21,85 @@ let prevError; let prevGroup; let prevGroupCollapsed; let prevGroupEnd; +const {__REACT_DEVTOOLS_SUPPRESS_DOUBLE_LOGGING__: suppressLog} = canUseDOM + ? window + : global; function disabledLog() {} disabledLog.__reactDisabledLog = true; +function patchConsole() { + /* eslint-disable react-internal/no-production-logging */ + prevLog = console.log; + prevInfo = console.info; + prevWarn = console.warn; + prevError = console.error; + prevGroup = console.group; + prevGroupCollapsed = console.groupCollapsed; + prevGroupEnd = console.groupEnd; + // https://github.com/facebook/react/issues/19099 + const props = { + configurable: true, + enumerable: true, + value: disabledLog, + writable: true, + }; + // $FlowFixMe Flow thinks console is immutable. + Object.defineProperties(console, { + info: props, + log: props, + warn: props, + error: props, + group: props, + groupCollapsed: props, + groupEnd: props, + }); + /* eslint-disable react-internal/no-production-logging */ +} + +function unPatchConsole() { + /* eslint-disable react-internal/no-production-logging */ + const props = { + configurable: true, + enumerable: true, + writable: true, + }; + // $FlowFixMe Flow thinks console is immutable. + Object.defineProperties(console, { + log: {...props, value: prevLog}, + info: {...props, value: prevInfo}, + warn: {...props, value: prevWarn}, + error: {...props, value: prevError}, + group: {...props, value: prevGroup}, + groupCollapsed: {...props, value: prevGroupCollapsed}, + groupEnd: {...props, value: prevGroupEnd}, + }); + /* eslint-enable react-internal/no-production-logging */ +} + +let suppressDoubleLogging = suppressLog; export function disableLogs(): void { if (__DEV__) { if (disabledDepth === 0) { - /* eslint-disable react-internal/no-production-logging */ - prevLog = console.log; - prevInfo = console.info; - prevWarn = console.warn; - prevError = console.error; - prevGroup = console.group; - prevGroupCollapsed = console.groupCollapsed; - prevGroupEnd = console.groupEnd; - // https://github.com/facebook/react/issues/19099 - const props = { - configurable: true, - enumerable: true, - value: disabledLog, - writable: true, - }; - // $FlowFixMe Flow thinks console is immutable. - Object.defineProperties(console, { - info: props, - log: props, - warn: props, - error: props, - group: props, - groupCollapsed: props, - groupEnd: props, - }); - /* eslint-enable react-internal/no-production-logging */ + if (!suppressDoubleLogging) { + patchConsole(); + } } - disabledDepth++; } + disabledDepth++; } export function reenableLogs(): void { if (__DEV__) { disabledDepth--; if (disabledDepth === 0) { - /* eslint-disable react-internal/no-production-logging */ - const props = { - configurable: true, - enumerable: true, - writable: true, - }; - // $FlowFixMe Flow thinks console is immutable. - Object.defineProperties(console, { - log: {...props, value: prevLog}, - info: {...props, value: prevInfo}, - warn: {...props, value: prevWarn}, - error: {...props, value: prevError}, - group: {...props, value: prevGroup}, - groupCollapsed: {...props, value: prevGroupCollapsed}, - groupEnd: {...props, value: prevGroupEnd}, - }); - /* eslint-enable react-internal/no-production-logging */ + try { + if (!suppressDoubleLogging) { + unPatchConsole(); + } + } finally { + suppressDoubleLogging = suppressLog; + } } if (disabledDepth < 0) { console.error(