@samitouri / QOS-React-2 / commits / 60a30cf32e

Console Logging for StrictMode Double Rendering (#22030)

React currently suppress console logs in StrictMode during double rendering. However, this causes a lot of confusion. This PR moves the console suppression logic from React into React Devtools. Now by default, we no longer suppress console logs. Instead, we gray out the logs in console during double render. We also add a setting in React Devtools to allow developers to hide console logs during double render if they choose.

Luna Ruan committed Aug 25, 2021 at 15:35 UTC 60a30cf32e1f974b82f0c7da9d8a03822e5b5208
49 files changed +799 -180
packages/react-devtools-core/src/standalone.js
+4
@@ -21,6 +21,7 @@ import {
21 getBreakOnConsoleErrors,
22 getSavedComponentFilters,
23 getShowInlineWarningsAndErrors,
24 + getHideConsoleLogsInStrictMode,
25 } from 'react-devtools-shared/src/utils';
26 import {Server} from 'ws';
27 import {join} from 'path';
@@ -310,6 +311,9 @@ function startServer(
311 )};
312 window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
313 getShowInlineWarningsAndErrors(),
314 + )};
315 + window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = ${JSON.stringify(
316 + getHideConsoleLogsInStrictMode(),
317 )};`;
318
319 response.end(
packages/react-devtools-core/webpack.backend.js
+12
@@ -1,6 +1,12 @@
1 const {resolve} = require('path');
2 const {DefinePlugin} = require('webpack');
3 const {
4 + DARK_MODE_DIMMED_WARNING_COLOR,
5 + DARK_MODE_DIMMED_ERROR_COLOR,
6 + DARK_MODE_DIMMED_LOG_COLOR,
7 + LIGHT_MODE_DIMMED_WARNING_COLOR,
8 + LIGHT_MODE_DIMMED_ERROR_COLOR,
9 + LIGHT_MODE_DIMMED_LOG_COLOR,
10 GITHUB_URL,
11 getVersionString,
12 } = require('react-devtools-extensions/utils');
@@ -60,6 +66,12 @@ module.exports = {
66 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
67 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
68 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
69 + 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
70 + 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
71 + 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
72 + 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
73 + 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
74 + 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
75 }),
76 ],
77 optimization: {
packages/react-devtools-core/webpack.standalone.js
+12
@@ -1,6 +1,12 @@
1 const {resolve} = require('path');
2 const {DefinePlugin} = require('webpack');
3 const {
4 + DARK_MODE_DIMMED_WARNING_COLOR,
5 + DARK_MODE_DIMMED_ERROR_COLOR,
6 + DARK_MODE_DIMMED_LOG_COLOR,
7 + LIGHT_MODE_DIMMED_WARNING_COLOR,
8 + LIGHT_MODE_DIMMED_ERROR_COLOR,
9 + LIGHT_MODE_DIMMED_LOG_COLOR,
10 GITHUB_URL,
11 getVersionString,
12 } = require('react-devtools-extensions/utils');
@@ -67,6 +73,12 @@ module.exports = {
73 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
74 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
75 'process.env.NODE_ENV': `"${NODE_ENV}"`,
76 + 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
77 + 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
78 + 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
79 + 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
80 + 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
81 + 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
82 }),
83 ],
84 module: {
packages/react-devtools-extensions/src/injectGlobalHook.js
+3
@@ -88,6 +88,9 @@ if (sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true') {
88
89 // Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with.
90 // Only do this for HTML documents though, to avoid e.g. breaking syntax highlighting for XML docs.
91 +// We need to inject this code because content scripts (ie injectGlobalHook.js) don't have access
92 +// to the webpage's window, so in order to access front end settings
93 +// and communicate with React, we must inject this code into the webpage
94 if ('text/html' === document.contentType) {
95 injectCode(
96 ';(' +
packages/react-devtools-extensions/src/main.js
+7
@@ -11,6 +11,7 @@ import {
11 getBreakOnConsoleErrors,
12 getSavedComponentFilters,
13 getShowInlineWarningsAndErrors,
14 + getHideConsoleLogsInStrictMode,
15 } from 'react-devtools-shared/src/utils';
16 import {
17 localStorageGetItem,
@@ -42,6 +43,12 @@ function syncSavedPreferences() {
43 )};
44 window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
45 getShowInlineWarningsAndErrors(),
46 + )};
47 + window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = ${JSON.stringify(
48 + getHideConsoleLogsInStrictMode(),
49 + )};
50 + window.__REACT_DEVTOOLS_BROWSER_THEME__ = ${JSON.stringify(
51 + getBrowserTheme(),
52 )};`,
53 );
54 }
packages/react-devtools-extensions/src/utils.js
+2 -2
@@ -1,5 +1,7 @@
1 /* global chrome */
2
3 +import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
4 +
5 const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;
6
7 export type BrowserName = 'Chrome' | 'Firefox';
@@ -8,8 +10,6 @@ export function getBrowserName(): BrowserName {
10 return IS_CHROME ? 'Chrome' : 'Firefox';
11 }
12
11 -export type BrowserTheme = 'dark' | 'light';
12 -
13 export function getBrowserTheme(): BrowserTheme {
14 if (IS_CHROME) {
15 // chrome.devtools.panels added in Chrome 18.
packages/react-devtools-extensions/utils.js
+13
@@ -9,6 +9,13 @@ const {execSync} = require('child_process');
9 const {readFileSync} = require('fs');
10 const {resolve} = require('path');
11
12 +const DARK_MODE_DIMMED_WARNING_COLOR = 'rgba(250, 180, 50, 0.5)';
13 +const DARK_MODE_DIMMED_ERROR_COLOR = 'rgba(250, 123, 130, 0.5)';
14 +const DARK_MODE_DIMMED_LOG_COLOR = 'rgba(125, 125, 125, 0.5)';
15 +const LIGHT_MODE_DIMMED_WARNING_COLOR = 'rgba(250, 180, 50, 0.75)';
16 +const LIGHT_MODE_DIMMED_ERROR_COLOR = 'rgba(250, 123, 130, 0.75)';
17 +const LIGHT_MODE_DIMMED_LOG_COLOR = 'rgba(125, 125, 125, 0.75)';
18 +
19 const GITHUB_URL = 'https://github.com/facebook/react';
20
21 function getGitCommit() {
@@ -36,6 +43,12 @@ function getVersionString() {
43 }
44
45 module.exports = {
46 + DARK_MODE_DIMMED_WARNING_COLOR,
47 + DARK_MODE_DIMMED_ERROR_COLOR,
48 + DARK_MODE_DIMMED_LOG_COLOR,
49 + LIGHT_MODE_DIMMED_WARNING_COLOR,
50 + LIGHT_MODE_DIMMED_ERROR_COLOR,
51 + LIGHT_MODE_DIMMED_LOG_COLOR,
52 GITHUB_URL,
53 getGitCommit,
54 getVersionString,
packages/react-devtools-extensions/webpack.backend.js
+17 -2
@@ -2,7 +2,16 @@
2
3 const {resolve} = require('path');
4 const {DefinePlugin} = require('webpack');
5 -const {GITHUB_URL, getVersionString} = require('./utils');
5 +const {
6 + DARK_MODE_DIMMED_WARNING_COLOR,
7 + DARK_MODE_DIMMED_ERROR_COLOR,
8 + DARK_MODE_DIMMED_LOG_COLOR,
9 + LIGHT_MODE_DIMMED_WARNING_COLOR,
10 + LIGHT_MODE_DIMMED_ERROR_COLOR,
11 + LIGHT_MODE_DIMMED_LOG_COLOR,
12 + GITHUB_URL,
13 + getVersionString,
14 +} = require('./utils');
15 const {resolveFeatureFlags} = require('react-devtools-shared/buildUtils');
16
17 const NODE_ENV = process.env.NODE_ENV;
@@ -54,10 +63,16 @@ module.exports = {
63 new DefinePlugin({
64 __DEV__: true,
65 __PROFILE__: false,
57 - __EXPERIMENTAL__: true,
66 + __DEV____DEV__: true,
67 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
68 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
69 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
70 + 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
71 + 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
72 + 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
73 + 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
74 + 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
75 + 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
76 }),
77 ],
78 module: {
packages/react-devtools-extensions/webpack.config.js
+16 -1
@@ -2,7 +2,16 @@
2
3 const {resolve} = require('path');
4 const {DefinePlugin} = require('webpack');
5 -const {GITHUB_URL, getVersionString} = require('./utils');
5 +const {
6 + DARK_MODE_DIMMED_WARNING_COLOR,
7 + DARK_MODE_DIMMED_ERROR_COLOR,
8 + DARK_MODE_DIMMED_LOG_COLOR,
9 + LIGHT_MODE_DIMMED_WARNING_COLOR,
10 + LIGHT_MODE_DIMMED_ERROR_COLOR,
11 + LIGHT_MODE_DIMMED_LOG_COLOR,
12 + GITHUB_URL,
13 + getVersionString,
14 +} = require('./utils');
15 const {resolveFeatureFlags} = require('react-devtools-shared/buildUtils');
16
17 const NODE_ENV = process.env.NODE_ENV;
@@ -76,6 +85,12 @@ module.exports = {
85 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
86 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
87 'process.env.NODE_ENV': `"${NODE_ENV}"`,
88 + 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
89 + 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
90 + 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
91 + 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
92 + 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
93 + 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
94 }),
95 ],
96 module: {
packages/react-devtools-inline/README.md
+5 -4
@@ -83,14 +83,15 @@ const { contentWindow } = iframe;
83 // This must be called before React is loaded into that frame.
84 initializeBackend(contentWindow);
85
86 -// React application can be injected into <iframe> at any time now...
87 -// Note that this would need to be done via <script> tag injection,
88 -// as setting the src of the <iframe> would load a new page (without the injected backend).
89 -
86 // Initialize DevTools UI to listen to the hook we just installed.
87 // This returns a React component we can render anywhere in the parent window.
88 +// This also must be called before React is loaded into the iframe
89 const DevTools = initializeFrontend(contentWindow);
90
91 +// React application can be injected into <iframe> at any time now...
92 +// Note that this would need to be done via <script> tag injection,
93 +// as setting the src of the <iframe> would load a new page (without the injected backend).
94 +
95 // <DevTools /> interface can be rendered in the parent window at any time now...
96 // Be sure to use either ReactDOM.createRoot()
97 // or ReactDOM.createSyncRoot() to render this component.
packages/react-devtools-inline/src/backend.js
+3
@@ -25,12 +25,14 @@ function startActivation(contentWindow: window) {
25 breakOnConsoleErrors,
26 componentFilters,
27 showInlineWarningsAndErrors,
28 + hideConsoleLogsInStrictMode,
29 } = data;
30
31 contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = appendComponentStack;
32 contentWindow.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
33 contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
34 contentWindow.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
35 + contentWindow.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
36
37 // TRICKY
38 // The backend entry point may be required in the context of an iframe or the parent window.
@@ -43,6 +45,7 @@ function startActivation(contentWindow: window) {
45 window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = breakOnConsoleErrors;
46 window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = componentFilters;
47 window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = showInlineWarningsAndErrors;
48 + window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = hideConsoleLogsInStrictMode;
49 }
50
51 finishActivation(contentWindow);
packages/react-devtools-inline/src/frontend.js
+2
@@ -10,6 +10,7 @@ import {
10 getBreakOnConsoleErrors,
11 getSavedComponentFilters,
12 getShowInlineWarningsAndErrors,
13 + getHideConsoleLogsInStrictMode,
14 } from 'react-devtools-shared/src/utils';
15 import {
16 MESSAGE_TYPE_GET_SAVED_PREFERENCES,
@@ -88,6 +89,7 @@ export function initialize(
89 breakOnConsoleErrors: getBreakOnConsoleErrors(),
90 componentFilters: getSavedComponentFilters(),
91 showInlineWarningsAndErrors: getShowInlineWarningsAndErrors(),
92 + hideConsoleLogsInStrictMode: getHideConsoleLogsInStrictMode(),
93 },
94 '*',
95 );
packages/react-devtools-inline/webpack.config.js
+12
@@ -1,6 +1,12 @@
1 const {resolve} = require('path');
2 const {DefinePlugin} = require('webpack');
3 const {
4 + DARK_MODE_DIMMED_WARNING_COLOR,
5 + DARK_MODE_DIMMED_ERROR_COLOR,
6 + DARK_MODE_DIMMED_LOG_COLOR,
7 + LIGHT_MODE_DIMMED_WARNING_COLOR,
8 + LIGHT_MODE_DIMMED_ERROR_COLOR,
9 + LIGHT_MODE_DIMMED_LOG_COLOR,
10 GITHUB_URL,
11 getVersionString,
12 } = require('react-devtools-extensions/utils');
@@ -70,6 +76,12 @@ module.exports = {
76 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
77 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
78 'process.env.NODE_ENV': `"${NODE_ENV}"`,
79 + 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
80 + 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
81 + 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
82 + 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
83 + 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
84 + 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
85 }),
86 ],
87 module: {
packages/react-devtools-shared/src/__tests__/console-test.js
+89 -2
@@ -9,6 +9,7 @@
9
10 describe('console', () => {
11 let React;
12 + let ReactDOM;
13 let act;
14 let fakeConsole;
15 let legacyRender;
@@ -48,6 +49,7 @@ describe('console', () => {
49 appendComponentStack: true,
50 breakOnWarn: false,
51 showInlineWarningsAndErrors: false,
52 + hideDoubleLogsInStrictLegacy: false,
53 });
54
55 const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
@@ -58,6 +60,7 @@ describe('console', () => {
60 };
61
62 React = require('react');
63 + ReactDOM = require('react-dom');
64
65 const utils = require('./utils');
66 act = utils.act;
@@ -73,10 +76,10 @@ describe('console', () => {
76 );
77 }
78
76 - it('should not patch console methods that do not receive component stacks', () => {
79 + it('should not patch console methods that are not explicitly overriden', () => {
80 expect(fakeConsole.error).not.toBe(mockError);
81 expect(fakeConsole.info).toBe(mockInfo);
79 - expect(fakeConsole.log).toBe(mockLog);
82 + expect(fakeConsole.log).not.toBe(mockLog);
83 expect(fakeConsole.warn).not.toBe(mockWarn);
84 });
85
@@ -465,4 +468,88 @@ describe('console', () => {
468 expect(mockWarn).toHaveBeenCalledTimes(1);
469 expect(mockWarn.mock.calls[0][0]).toBe('Symbol:');
470 });
471 +
472 + it('should double log if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
473 + const container = document.createElement('div');
474 + const root = ReactDOM.createRoot(container);
475 +
476 + function App() {
477 + fakeConsole.log('log');
478 + fakeConsole.warn('warn');
479 + fakeConsole.error('error');
480 + return <div />;
481 + }
482 +
483 + patchConsole({
484 + appendComponentStack: false,
485 + breakOnWarn: false,
486 + showInlineWarningsAndErrors: false,
487 + hideConsoleLogsInStrictMode: false,
488 + });
489 +
490 + act(() =>
491 + root.render(
492 + <React.StrictMode>
493 + <App />
494 + </React.StrictMode>,
495 + ),
496 + );
497 +
498 + expect(mockLog).toHaveBeenCalledTimes(2);
499 + expect(mockLog.mock.calls[0]).toHaveLength(1);
500 + expect(mockLog.mock.calls[0][0]).toBe('log');
501 + expect(mockLog.mock.calls[1]).toHaveLength(2);
502 + expect(mockLog.mock.calls[1][0]).toBe('%clog');
503 +
504 + expect(mockWarn).toHaveBeenCalledTimes(2);
505 + expect(mockWarn.mock.calls[0]).toHaveLength(1);
506 + expect(mockWarn.mock.calls[0][0]).toBe('warn');
507 + expect(mockWarn.mock.calls[1]).toHaveLength(2);
508 + expect(mockWarn.mock.calls[1][0]).toBe('%cwarn');
509 +
510 + expect(mockError).toHaveBeenCalledTimes(2);
511 + expect(mockError.mock.calls[0]).toHaveLength(1);
512 + expect(mockError.mock.calls[0][0]).toBe('error');
513 + expect(mockError.mock.calls[1]).toHaveLength(2);
514 + expect(mockError.mock.calls[1][0]).toBe('%cerror');
515 + });
516 +
517 + it('should not double log if hideConsoleLogsInStrictMode is enabled in Strict mode', () => {
518 + const container = document.createElement('div');
519 + const root = ReactDOM.createRoot(container);
520 +
521 + function App() {
522 + fakeConsole.log('log');
523 + fakeConsole.warn('warn');
524 + fakeConsole.error('error');
525 + return <div />;
526 + }
527 +
528 + patchConsole({
529 + appendComponentStack: false,
530 + breakOnWarn: false,
531 + showInlineWarningsAndErrors: false,
532 + hideConsoleLogsInStrictMode: true,
533 + });
534 +
535 + act(() =>
536 + root.render(
537 + <React.StrictMode>
538 + <App />
539 + </React.StrictMode>,
540 + ),
541 + );
542 +
543 + expect(mockLog).toHaveBeenCalledTimes(1);
544 + expect(mockLog.mock.calls[0]).toHaveLength(1);
545 + expect(mockLog.mock.calls[0][0]).toBe('log');
546 +
547 + expect(mockWarn).toHaveBeenCalledTimes(1);
548 + expect(mockWarn.mock.calls[0]).toHaveLength(1);
549 + expect(mockWarn.mock.calls[0][0]).toBe('warn');
550 +
551 + expect(mockError).toHaveBeenCalledTimes(1);
552 + expect(mockError.mock.calls[0]).toHaveLength(1);
553 + expect(mockError.mock.calls[0][0]).toBe('error');
554 + });
555 });
packages/react-devtools-shared/src/__tests__/setupEnv.js
+16
@@ -1,5 +1,14 @@
1 'use strict';
2
3 +const {
4 + DARK_MODE_DIMMED_WARNING_COLOR,
5 + DARK_MODE_DIMMED_ERROR_COLOR,
6 + DARK_MODE_DIMMED_LOG_COLOR,
7 + LIGHT_MODE_DIMMED_WARNING_COLOR,
8 + LIGHT_MODE_DIMMED_ERROR_COLOR,
9 + LIGHT_MODE_DIMMED_LOG_COLOR,
10 +} = require('react-devtools-extensions/utils');
11 +
12 // DevTools stores preferences between sessions in localStorage
13 if (!global.hasOwnProperty('localStorage')) {
14 global.localStorage = require('local-storage-fallback').default;
@@ -8,3 +17,10 @@ if (!global.hasOwnProperty('localStorage')) {
17 // Mimic the global we set with Webpack's DefinePlugin
18 global.__DEV__ = process.env.NODE_ENV !== 'production';
19 global.__TEST__ = true;
20 +
21 +global.process.env.DARK_MODE_DIMMED_WARNING_COLOR = DARK_MODE_DIMMED_WARNING_COLOR;
22 +global.process.env.DARK_MODE_DIMMED_ERROR_COLOR = DARK_MODE_DIMMED_ERROR_COLOR;
23 +global.process.env.DARK_MODE_DIMMED_LOG_COLOR = DARK_MODE_DIMMED_LOG_COLOR;
24 +global.process.env.LIGHT_MODE_DIMMED_WARNING_COLOR = LIGHT_MODE_DIMMED_WARNING_COLOR;
25 +global.process.env.LIGHT_MODE_DIMMED_ERROR_COLOR = LIGHT_MODE_DIMMED_ERROR_COLOR;
26 +global.process.env.LIGHT_MODE_DIMMED_LOG_COLOR = LIGHT_MODE_DIMMED_LOG_COLOR;
packages/react-devtools-shared/src/backend/agent.js
+14 -15
@@ -25,7 +25,7 @@ import {
25 initialize as setupTraceUpdates,
26 toggleEnabled as setTraceUpdatesEnabled,
27 } from './views/TraceUpdates';
28 -import {patch as patchConsole, unpatch as unpatchConsole} from './console';
28 +import {patch as patchConsole} from './console';
29 import {currentBridgeProtocol} from 'react-devtools-shared/src/bridge';
30
31 import type {BackendBridge} from 'react-devtools-shared/src/bridge';
@@ -40,6 +40,7 @@ import type {
40 } from './types';
41 import type {ComponentFilter} from '../types';
42 import {isSynchronousXHRSupported} from './utils';
43 +import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
44
45 const debug = (methodName, ...args) => {
46 if (__DEBUG__) {
@@ -635,28 +636,26 @@ export default class Agent extends EventEmitter<{|
636 appendComponentStack,
637 breakOnConsoleErrors,
638 showInlineWarningsAndErrors,
639 + hideConsoleLogsInStrictMode,
640 + browserTheme,
641 }: {|
642 appendComponentStack: boolean,
643 breakOnConsoleErrors: boolean,
644 showInlineWarningsAndErrors: boolean,
645 + hideConsoleLogsInStrictMode: boolean,
646 + browserTheme: BrowserTheme,
647 |}) => {
648 // If the frontend preference has change,
649 // or in the case of React Native- if the backend is just finding out the preference-
645 - // then install or uninstall the console overrides.
650 + // then reinstall the console overrides.
651 // It's safe to call these methods multiple times, so we don't need to worry about that.
647 - if (
648 - appendComponentStack ||
649 - breakOnConsoleErrors ||
650 - showInlineWarningsAndErrors
651 - ) {
652 - patchConsole({
653 - appendComponentStack,
654 - breakOnConsoleErrors,
655 - showInlineWarningsAndErrors,
656 - });
657 - } else {
658 - unpatchConsole();
659 - }
652 + patchConsole({
653 + appendComponentStack,
654 + breakOnConsoleErrors,
655 + showInlineWarningsAndErrors,
656 + hideConsoleLogsInStrictMode,
657 + browserTheme,
658 + });
659 };
660
661 updateComponentFilters = (componentFilters: Array<ComponentFilter>) => {
packages/react-devtools-shared/src/backend/console.js
+116 -49
@@ -9,11 +9,14 @@
9
10 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
11 import type {CurrentDispatcherRef, ReactRenderer, WorkTagMap} from './types';
12 +import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
13 +import {format} from './utils';
14
15 import {getInternalReactConstants} from './renderer';
16 import {getStackByFiberInDevAndProd} from './DevToolsFiberComponentStack';
17
16 -const APPEND_STACK_TO_METHODS = ['error', 'trace', 'warn'];
18 +const OVERRIDE_CONSOLE_METHODS = ['error', 'trace', 'warn', 'log'];
19 +const DIMMED_NODE_CONSOLE_COLOR = '\x1b[2m%s\x1b[0m';
20
21 // React's custom built component stack strings match "\s{4}in"
22 // Chrome's prefix matches "\s{4}at"
@@ -39,6 +42,7 @@ const injectedRenderers: Map<
42 getCurrentFiber: () => Fiber | null,
43 onErrorOrWarning: ?OnErrorOrWarning,
44 workTagMap: WorkTagMap,
45 + getIsStrictMode: ?() => boolean,
46 |},
47 > = new Map();
48
@@ -50,6 +54,11 @@ for (const method in console) {
54
55 let unpatchFn: null | (() => void) = null;
56
57 +let isNode = false;
58 +try {
59 + isNode = this === global;
60 +} catch (error) {}
61 +
62 // Enables e.g. Jest tests to inject a mock console object.
63 export function dangerous_setTargetConsoleForTesting(
64 targetConsoleForTesting: Object,
@@ -72,6 +81,7 @@ export function registerRenderer(
81 const {
82 currentDispatcherRef,
83 getCurrentFiber,
84 + getIsStrictMode,
85 findFiberByHostInstance,
86 version,
87 } = renderer;
@@ -89,6 +99,7 @@ export function registerRenderer(
99 injectedRenderers.set(renderer, {
100 currentDispatcherRef,
101 getCurrentFiber,
102 + getIsStrictMode,
103 workTagMap: ReactTypeOfWork,
104 onErrorOrWarning,
105 });
@@ -99,24 +110,31 @@ const consoleSettingsRef = {
110 appendComponentStack: false,
111 breakOnConsoleErrors: false,
112 showInlineWarningsAndErrors: false,
113 + hideConsoleLogsInStrictMode: false,
114 };
115
116 // Patches console methods to append component stack for the current fiber.
117 // Call unpatch() to remove the injected behavior.
118 +// NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialRenderInExtension
119 export function patch({
120 appendComponentStack,
121 breakOnConsoleErrors,
122 showInlineWarningsAndErrors,
123 + hideConsoleLogsInStrictMode,
124 + browserTheme,
125 }: {
126 appendComponentStack: boolean,
127 breakOnConsoleErrors: boolean,
128 showInlineWarningsAndErrors: boolean,
129 + hideConsoleLogsInStrictMode: boolean,
130 + browserTheme: BrowserTheme,
131 }): void {
132 // Settings may change after we've patched the console.
133 // Using a shared ref allows the patch function to read the latest values.
134 consoleSettingsRef.appendComponentStack = appendComponentStack;
135 consoleSettingsRef.breakOnConsoleErrors = breakOnConsoleErrors;
136 consoleSettingsRef.showInlineWarningsAndErrors = showInlineWarningsAndErrors;
137 + consoleSettingsRef.hideConsoleLogsInStrictMode = hideConsoleLogsInStrictMode;
138
139 if (unpatchFn !== null) {
140 // Don't patch twice.
@@ -134,68 +152,78 @@ export function patch({
152 }
153 };
154
137 - APPEND_STACK_TO_METHODS.forEach(method => {
155 + OVERRIDE_CONSOLE_METHODS.forEach(method => {
156 try {
139 - const originalMethod = (originalConsoleMethods[method] =
140 - targetConsole[method]);
157 + const originalMethod = (originalConsoleMethods[method] = targetConsole[
158 + method
159 + ].__REACT_DEVTOOLS_ORIGINAL_METHOD__
160 + ? targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
161 + : targetConsole[method]);
162
163 const overrideMethod = (...args) => {
164 let shouldAppendWarningStack = false;
144 - if (consoleSettingsRef.appendComponentStack) {
145 - const lastArg = args.length > 0 ? args[args.length - 1] : null;
146 - const alreadyHasComponentStack =
147 - typeof lastArg === 'string' && isStringComponentStack(lastArg);
148 -
149 - // If we are ever called with a string that already has a component stack,
150 - // e.g. a React error/warning, don't append a second stack.
151 - shouldAppendWarningStack = !alreadyHasComponentStack;
165 + if (method !== 'log') {
166 + if (consoleSettingsRef.appendComponentStack) {
167 + const lastArg = args.length > 0 ? args[args.length - 1] : null;
168 + const alreadyHasComponentStack =
169 + typeof lastArg === 'string' && isStringComponentStack(lastArg);
170 +
171 + // If we are ever called with a string that already has a component stack,
172 + // e.g. a React error/warning, don't append a second stack.
173 + shouldAppendWarningStack = !alreadyHasComponentStack;
174 + }
175 }
176
177 const shouldShowInlineWarningsAndErrors =
178 consoleSettingsRef.showInlineWarningsAndErrors &&
179 (method === 'error' || method === 'warn');
180
158 - if (shouldAppendWarningStack || shouldShowInlineWarningsAndErrors) {
159 - // Search for the first renderer that has a current Fiber.
160 - // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
161 - // eslint-disable-next-line no-for-of-loops/no-for-of-loops
162 - for (const {
163 - currentDispatcherRef,
164 - getCurrentFiber,
165 - onErrorOrWarning,
166 - workTagMap,
167 - } of injectedRenderers.values()) {
168 - const current: ?Fiber = getCurrentFiber();
169 - if (current != null) {
170 - try {
171 - if (shouldShowInlineWarningsAndErrors) {
172 - // patch() is called by two places: (1) the hook and (2) the renderer backend.
173 - // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
174 - if (typeof onErrorOrWarning === 'function') {
175 - onErrorOrWarning(
176 - current,
177 - ((method: any): 'error' | 'warn'),
178 - // Copy args before we mutate them (e.g. adding the component stack)
179 - args.slice(),
180 - );
181 - }
182 - }
181 + let isInStrictMode = false;
182
184 - if (shouldAppendWarningStack) {
185 - const componentStack = getStackByFiberInDevAndProd(
186 - workTagMap,
183 + // Search for the first renderer that has a current Fiber.
184 + // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
185 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
186 + for (const {
187 + currentDispatcherRef,
188 + getCurrentFiber,
189 + onErrorOrWarning,
190 + workTagMap,
191 + getIsStrictMode,
192 + } of injectedRenderers.values()) {
193 + const current: ?Fiber = getCurrentFiber();
194 + if (current != null) {
195 + try {
196 + if (typeof getIsStrictMode === 'function' && getIsStrictMode()) {
197 + isInStrictMode = true;
198 + }
199 +
200 + if (shouldShowInlineWarningsAndErrors) {
201 + // patch() is called by two places: (1) the hook and (2) the renderer backend.
202 + // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
203 + if (typeof onErrorOrWarning === 'function') {
204 + onErrorOrWarning(
205 current,
188 - currentDispatcherRef,
206 + ((method: any): 'error' | 'warn'),
207 + // Copy args before we mutate them (e.g. adding the component stack)
208 + args.slice(),
209 );
190 - if (componentStack !== '') {
191 - args.push(componentStack);
192 - }
210 }
194 - } catch (error) {
195 - // Don't let a DevTools or React internal error interfere with logging.
196 - } finally {
197 - break;
211 }
212 +
213 + if (shouldAppendWarningStack) {
214 + const componentStack = getStackByFiberInDevAndProd(
215 + workTagMap,
216 + current,
217 + currentDispatcherRef,
218 + );
219 + if (componentStack !== '') {
220 + args.push(componentStack);
221 + }
222 + }
223 + } catch (error) {
224 + // Don't let a DevTools or React internal error interfere with logging.
225 + } finally {
226 + break;
227 }
228 }
229 }
@@ -209,7 +237,46 @@ export function patch({
237 debugger;
238 }
239
212 - originalMethod(...args);
240 + if (isInStrictMode) {
241 + if (!consoleSettingsRef.hideConsoleLogsInStrictMode) {
242 + // Dim the text color of the double logs if we're not
243 + // hiding them.
244 + if (isNode) {
245 + originalMethod(DIMMED_NODE_CONSOLE_COLOR, format(...args));
246 + } else {
247 + let color;
248 + switch (method) {
249 + case 'warn':
250 + color =
251 + browserTheme === 'light'
252 + ? process.env.LIGHT_MODE_DIMMED_WARNING_COLOR
253 + : process.env.DARK_MODE_DIMMED_WARNING_COLOR;
254 + break;
255 + case 'error':
256 + color =
257 + browserTheme === 'light'
258 + ? process.env.LIGHT_MODE_DIMMED_ERROR_COLOR
259 + : process.env.DARK_MODE_DIMMED_ERROR_COLOR;
260 + break;
261 + case 'log':
262 + default:
263 + color =
264 + browserTheme === 'light'
265 + ? process.env.LIGHT_MODE_DIMMED_LOG_COLOR
266 + : process.env.DARK_MODE_DIMMED_LOG_COLOR;
267 + break;
268 + }
269 +
270 + if (color) {
271 + originalMethod(`%c${format(...args)}`, `color: ${color}`);
272 + } else {
273 + throw Error('Console color is not defined');
274 + }
275 + }
276 + }
277 + } else {
278 + originalMethod(...args);
279 + }
280 };
281
282 overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
packages/react-devtools-shared/src/backend/renderer.js
+11 -11
@@ -740,17 +740,17 @@ export function attach(
740 window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ === true;
741 const showInlineWarningsAndErrors =
742 window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ !== false;
743 - if (
744 - appendComponentStack ||
745 - breakOnConsoleErrors ||
746 - showInlineWarningsAndErrors
747 - ) {
748 - patchConsole({
749 - appendComponentStack,
750 - breakOnConsoleErrors,
751 - showInlineWarningsAndErrors,
752 - });
753 - }
743 + const hideConsoleLogsInStrictMode =
744 + window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ === true;
745 + const browserTheme = window.__REACT_DEVTOOLS_BROWSER_THEME__;
746 +
747 + patchConsole({
748 + appendComponentStack,
749 + breakOnConsoleErrors,
750 + showInlineWarningsAndErrors,
751 + hideConsoleLogsInStrictMode,
752 + browserTheme,
753 + });
754 }
755
756 const debug = (
packages/react-devtools-shared/src/backend/types.js
+2
@@ -136,6 +136,8 @@ export type ReactRenderer = {
136 // Only injected by React v16.9+ in DEV mode.
137 // Enables DevTools to append owners-only component stack to error messages.
138 getCurrentFiber?: () => Fiber | null,
139 +
140 + getIsStrictMode?: () => boolean,
141 // 17.0.2+
142 reconcilerVersion?: string,
143 // Uniquely identifies React DOM v15.
packages/react-devtools-shared/src/backend/utils.js
+1
@@ -159,6 +159,7 @@ export function serializeToString(data: any): string {
159 // based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1
160 // based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions
161 // Implements s, d, i and f placeholders
162 +// NOTE: KEEP IN SYNC with src/hook.js
163 export function format(
164 maybeMessage: any,
165 ...inputArgs: $ReadOnlyArray<any>
packages/react-devtools-shared/src/bridge.js
+3
@@ -17,6 +17,7 @@ import type {
17 RendererID,
18 } from 'react-devtools-shared/src/backend/types';
19 import type {StyleAndLayout as StyleAndLayoutPayload} from 'react-devtools-shared/src/backend/NativeStyleEditor/types';
20 +import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
21
22 const BATCH_DURATION = 100;
23
@@ -164,6 +165,8 @@ type UpdateConsolePatchSettingsParams = {|
165 appendComponentStack: boolean,
166 breakOnConsoleErrors: boolean,
167 showInlineWarningsAndErrors: boolean,
168 + hideConsoleLogsInStrictMode: boolean,
169 + browserTheme: BrowserTheme,
170 |};
171
172 export type BackendEvents = {|
packages/react-devtools-shared/src/constants.js
+3
@@ -47,6 +47,9 @@ export const LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY =
47 export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY =
48 'React::DevTools::traceUpdatesEnabled';
49
50 +export const LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE =
51 + 'React::DevTools::hideConsoleLogsInStrictMode';
52 +
53 export const PROFILER_EXPORT_VERSION = 5;
54
55 export const CHANGE_LOG_URL =
packages/react-devtools-shared/src/devtools/views/Settings/DebuggingSettings.js
+13 -2
@@ -17,10 +17,12 @@ export default function DebuggingSettings(_: {||}) {
17 const {
18 appendComponentStack,
19 breakOnConsoleErrors,
20 + hideConsoleLogsInStrictMode,
21 setAppendComponentStack,
22 setBreakOnConsoleErrors,
23 setShowInlineWarningsAndErrors,
24 showInlineWarningsAndErrors,
25 + sethideConsoleLogsInStrictMode,
26 } = useContext(SettingsContext);
27
28 return (
@@ -64,8 +66,17 @@ export default function DebuggingSettings(_: {||}) {
66 </label>
67 </div>
68
67 - <div className={styles.ConsoleAPIWarning}>
68 - These settings require DevTools to override native console APIs.
69 + <div className={styles.Setting}>
70 + <label>
71 + <input
72 + type="checkbox"
73 + checked={hideConsoleLogsInStrictMode}
74 + onChange={({currentTarget}) =>
75 + sethideConsoleLogsInStrictMode(currentTarget.checked)
76 + }
77 + />{' '}
78 + Hide logs during second render in Strict Mode
79 + </label>
80 </div>
81 </div>
82 );
packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js
+19
@@ -23,6 +23,7 @@ import {
23 LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
24 LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
25 LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
26 + LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
27 } from 'react-devtools-shared/src/constants';
28 import {useLocalStorage} from '../hooks';
29 import {BridgeContext} from '../context';
@@ -49,6 +50,9 @@ type Context = {|
50 parseHookNames: boolean,
51 setParseHookNames: (value: boolean) => void,
52
53 + hideConsoleLogsInStrictMode: boolean,
54 + sethideConsoleLogsInStrictMode: (value: boolean) => void,
55 +
56 showInlineWarningsAndErrors: boolean,
57 setShowInlineWarningsAndErrors: (value: boolean) => void,
58
@@ -104,6 +108,13 @@ function SettingsContextController({
108 LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
109 false,
110 );
111 + const [
112 + hideConsoleLogsInStrictMode,
113 + sethideConsoleLogsInStrictMode,
114 + ] = useLocalStorage<boolean>(
115 + LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
116 + false,
117 + );
118 const [
119 showInlineWarningsAndErrors,
120 setShowInlineWarningsAndErrors,
@@ -169,12 +180,16 @@ function SettingsContextController({
180 appendComponentStack,
181 breakOnConsoleErrors,
182 showInlineWarningsAndErrors,
183 + hideConsoleLogsInStrictMode,
184 + browserTheme,
185 });
186 }, [
187 bridge,
188 appendComponentStack,
189 breakOnConsoleErrors,
190 showInlineWarningsAndErrors,
191 + hideConsoleLogsInStrictMode,
192 + browserTheme,
193 ]);
194
195 useEffect(() => {
@@ -199,6 +214,8 @@ function SettingsContextController({
214 setTraceUpdatesEnabled,
215 setShowInlineWarningsAndErrors,
216 showInlineWarningsAndErrors,
217 + sethideConsoleLogsInStrictMode,
218 + hideConsoleLogsInStrictMode,
219 theme,
220 browserTheme,
221 traceUpdatesEnabled,
@@ -216,6 +233,8 @@ function SettingsContextController({
233 setTraceUpdatesEnabled,
234 setShowInlineWarningsAndErrors,
235 showInlineWarningsAndErrors,
236 + sethideConsoleLogsInStrictMode,
237 + hideConsoleLogsInStrictMode,
238 theme,
239 browserTheme,
240 traceUpdatesEnabled,
packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css
+1 -2
@@ -128,8 +128,7 @@
128 background-color: var(--color-toggle-text);
129 }
130
131 -.ReleaseNotes,
132 -.ConsoleAPIWarning {
131 +.ReleaseNotes {
132 width: 100%;
133 background-color: var(--color-background-hover);
134 padding: 0.25rem 0.5rem;
packages/react-devtools-shared/src/hook.js
+179 -9
@@ -2,11 +2,16 @@
2 * Install the hook on window, which is an event emitter.
3 * Note because Chrome content scripts cannot directly modify the window object,
4 * we are evaling this function by inserting a script tag.
5 - * That's why we have to inline the whole event emitter implementation here.
5 + * That's why we have to inline the whole event emitter implementation,
6 + * the string format implementation, and part of the console implementation here.
7 *
8 * @flow
9 */
10
11 +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
12 +import type {ReactRenderer} from './backend/types';
13 +import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
14 +
15 import {
16 patch as patchConsole,
17 registerRenderer as registerRendererWithConsole,
@@ -152,6 +157,158 @@ export function installHook(target: any): DevToolsHook | null {
157 } catch (err) {}
158 }
159
160 + // NOTE: KEEP IN SYNC with src/backend/utils.js
161 + function format(
162 + maybeMessage: any,
163 + ...inputArgs: $ReadOnlyArray<any>
164 + ): string {
165 + const args = inputArgs.slice();
166 +
167 + // Symbols cannot be concatenated with Strings.
168 + let formatted: string =
169 + typeof maybeMessage === 'symbol'
170 + ? maybeMessage.toString()
171 + : '' + maybeMessage;
172 +
173 + // If the first argument is a string, check for substitutions.
174 + if (typeof maybeMessage === 'string') {
175 + if (args.length) {
176 + const REGEXP = /(%?)(%([jds]))/g;
177 +
178 + formatted = formatted.replace(REGEXP, (match, escaped, ptn, flag) => {
179 + let arg = args.shift();
180 + switch (flag) {
181 + case 's':
182 + arg += '';
183 + break;
184 + case 'd':
185 + case 'i':
186 + arg = parseInt(arg, 10).toString();
187 + break;
188 + case 'f':
189 + arg = parseFloat(arg).toString();
190 + break;
191 + }
192 + if (!escaped) {
193 + return arg;
194 + }
195 + args.unshift(arg);
196 + return match;
197 + });
198 + }
199 + }
200 +
201 + // Arguments that remain after formatting.
202 + if (args.length) {
203 + for (let i = 0; i < args.length; i++) {
204 + const arg = args[i];
205 +
206 + // Symbols cannot be concatenated with Strings.
207 + formatted += ' ' + (typeof arg === 'symbol' ? arg.toString() : arg);
208 + }
209 + }
210 +
211 + // Update escaped %% values.
212 + formatted = formatted.replace(/%{2,2}/g, '%');
213 +
214 + return '' + formatted;
215 + }
216 +
217 + // NOTE: KEEP IN SYNC with src/backend/console.js:patch
218 + function patchConsoleForInitialRenderInExtension(
219 + renderer: ReactRenderer,
220 + {
221 + hideConsoleLogsInStrictMode,
222 + browserTheme,
223 + }: {hideConsoleLogsInStrictMode: boolean, browserTheme: BrowserTheme},
224 + ): void {
225 + const overrideConsoleMethods = ['error', 'trace', 'warn', 'log'];
226 +
227 + if (__EXTENSION__) {
228 + const targetConsole = console;
229 +
230 + const originalConsoleMethods = {};
231 +
232 + overrideConsoleMethods.forEach(method => {
233 + try {
234 + const originalMethod = (originalConsoleMethods[
235 + method
236 + ] = targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
237 + ? targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
238 + : targetConsole[method]);
239 +
240 + const overrideMethod = (...args) => {
241 + let isInStrictMode = false;
242 +
243 + // Search for the first renderer that has a current Fiber.
244 + // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
245 + const {getCurrentFiber, getIsStrictMode} = renderer;
246 + if (typeof getCurrentFiber !== 'function') {
247 + return;
248 + }
249 +
250 + const current: ?Fiber = getCurrentFiber();
251 + if (current != null) {
252 + try {
253 + if (
254 + typeof getIsStrictMode === 'function' &&
255 + getIsStrictMode()
256 + ) {
257 + isInStrictMode = true;
258 + }
259 + } catch (error) {
260 + // Don't let a DevTools or React internal error interfere with logging.
261 + }
262 + }
263 +
264 + if (isInStrictMode) {
265 + if (!hideConsoleLogsInStrictMode) {
266 + // Dim the text color of the double logs if we're not
267 + // hiding them.
268 + let color;
269 + switch (method) {
270 + case 'warn':
271 + color =
272 + browserTheme === 'light'
273 + ? process.env.LIGHT_MODE_DIMMED_WARNING_COLOR
274 + : process.env.DARK_MODE_DIMMED_WARNING_COLOR;
275 + break;
276 + case 'error':
277 + color =
278 + browserTheme === 'light'
279 + ? process.env.LIGHT_MODE_DIMMED_ERROR_COLOR
280 + : process.env.DARK_MODE_DIMMED_ERROR_COLOR;
281 + break;
282 + case 'log':
283 + default:
284 + color =
285 + browserTheme === 'light'
286 + ? process.env.LIGHT_MODE_DIMMED_LOG_COLOR
287 + : process.env.DARK_MODE_DIMMED_LOG_COLOR;
288 + break;
289 + }
290 +
291 + if (color) {
292 + originalMethod(`%c${format(...args)}`, `color: ${color}`);
293 + } else {
294 + throw Error('Console color is not defined');
295 + }
296 + }
297 + } else {
298 + originalMethod(...args);
299 + }
300 + };
301 +
302 + overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
303 + originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod;
304 +
305 + // $FlowFixMe property error|warn is not writable.
306 + targetConsole[method] = overrideMethod;
307 + } catch (error) {}
308 + });
309 + }
310 + }
311 +
312 let uidCounter = 0;
313
314 function inject(renderer) {
@@ -164,19 +321,22 @@ export function installHook(target: any): DevToolsHook | null {
321
322 // Patching the console enables DevTools to do a few useful things:
323 // * Append component stacks to warnings and error messages
324 + // * Disabling or marking logs during a double render in Strict Mode
325 // * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
326 //
327 // For React Native, we intentionally patch early (during injection).
328 // This provides React Native developers with components stacks even if they don't run DevTools.
329 + //
330 // This won't work for DOM though, since this entire file is eval'ed and inserted as a script tag.
172 - // In that case, we'll patch later (when the frontend attaches).
331 + // In that case, we'll only patch parts of the console that are needed during the first render
332 + // and patch everything else later (when the frontend attaches).
333 //
334 // Don't patch in test environments because we don't want to interfere with Jest's own console overrides.
335 //
336 // Note that because this function is inlined, this conditional check must only use static booleans.
337 // Otherwise the extension will throw with an undefined error.
338 // (See comments in the try/catch below for more context on inlining.)
179 - if (!__EXTENSION__ && !__TEST__) {
339 + if (!__TEST__) {
340 try {
341 const appendComponentStack =
342 window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false;
@@ -184,6 +344,9 @@ export function installHook(target: any): DevToolsHook | null {
344 window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ === true;
345 const showInlineWarningsAndErrors =
346 window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ !== false;
347 + const hideConsoleLogsInStrictMode =
348 + window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ === true;
349 + const browserTheme = window.__REACT_DEVTOOLS_BROWSER_THEME__;
350
351 // The installHook() function is injected by being stringified in the browser,
352 // so imports outside of this function do not get included.
@@ -192,16 +355,19 @@ export function installHook(target: any): DevToolsHook | null {
355 // but Webpack wraps imports with an object (e.g. _backend_console__WEBPACK_IMPORTED_MODULE_0__)
356 // and the object itself will be undefined as well for the reasons mentioned above,
357 // so we use try/catch instead.
195 - if (
196 - appendComponentStack ||
197 - breakOnConsoleErrors ||
198 - showInlineWarningsAndErrors
199 - ) {
358 + if (!__EXTENSION__) {
359 registerRendererWithConsole(renderer);
360 patchConsole({
361 appendComponentStack,
362 breakOnConsoleErrors,
363 showInlineWarningsAndErrors,
364 + hideConsoleLogsInStrictMode,
365 + browserTheme,
366 + });
367 + } else {
368 + patchConsoleForInitialRenderInExtension(renderer, {
369 + hideConsoleLogsInStrictMode,
370 + browserTheme,
371 });
372 }
373 } catch (error) {}
@@ -215,7 +381,11 @@ export function installHook(target: any): DevToolsHook | null {
381 hook.rendererInterfaces.set(id, rendererInterface);
382 }
383
218 - hook.emit('renderer', {id, renderer, reactBuildType});
384 + hook.emit('renderer', {
385 + id,
386 + renderer,
387 + reactBuildType,
388 + });
389
390 return id;
391 }
packages/react-devtools-shared/src/utils.js
+20
@@ -37,6 +37,7 @@ import {
37 LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
38 LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
39 LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
40 + LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
41 } from './constants';
42 import {ComponentFilterElementType, ElementTypeHostComponent} from './types';
43 import {
@@ -317,6 +318,25 @@ export function setBreakOnConsoleErrors(value: boolean): void {
318 );
319 }
320
321 +export function getHideConsoleLogsInStrictMode(): boolean {
322 + try {
323 + const raw = localStorageGetItem(
324 + LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
325 + );
326 + if (raw != null) {
327 + return JSON.parse(raw);
328 + }
329 + } catch (error) {}
330 + return false;
331 +}
332 +
333 +export function sethideConsoleLogsInStrictMode(value: boolean): void {
334 + localStorageSetItem(
335 + LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
336 + JSON.stringify(value),
337 + );
338 +}
339 +
340 export function getShowInlineWarningsAndErrors(): boolean {
341 try {
342 const raw = localStorageGetItem(
packages/react-devtools-shell/src/app/console.js
+4
@@ -37,3 +37,7 @@ export function ignoreErrors(errorsToIgnore: Array<string>): void {
37 export function ignoreWarnings(warningsToIgnore: Array<string>): void {
38 ignoreStrings('warn', warningsToIgnore);
39 }
40 +
41 +export function ignoreLogs(logsToIgnore: Array<string>): void {
42 + ignoreStrings('log', logsToIgnore);
43 +}
packages/react-devtools-shell/src/app/index.js
+2 -1
@@ -19,7 +19,7 @@ import ToDoList from './ToDoList';
19 import Toggle from './Toggle';
20 import ErrorBoundaries from './ErrorBoundaries';
21 import SuspenseTree from './SuspenseTree';
22 -import {ignoreErrors, ignoreWarnings} from './console';
22 +import {ignoreErrors, ignoreLogs, ignoreWarnings} from './console';
23
24 import './styles.css';
25
@@ -32,6 +32,7 @@ ignoreErrors([
32 'Warning: %s is deprecated in StrictMode.', // findDOMNode
33 ]);
34 ignoreWarnings(['Warning: componentWillReceiveProps has been renamed']);
35 +ignoreLogs([]);
36
37 const roots = [];
38
packages/react-devtools-shell/src/devtools.js
+8 -6
@@ -19,6 +19,14 @@ contentWindow.__REACT_DEVTOOLS_TARGET_WINDOW__ = window;
19
20 initializeBackend(contentWindow);
21
22 +// Initialize the front end and activate the backend early so that we are able
23 +// to pass console settings in local storage to the backend before initial render
24 +const DevTools = initializeFrontend(contentWindow);
25 +
26 +// Activate the backend only once the DevTools frontend Store has been initialized.
27 +// Otherwise the Store may miss important initial tree op codes.
28 +activateBackend(contentWindow);
29 +
30 const container = ((document.getElementById('devtools'): any): HTMLElement);
31
32 let isTestAppMounted = true;
@@ -45,12 +53,6 @@ mountButton.addEventListener('click', function() {
53 inject('dist/app.js', () => {
54 initDevTools({
55 connect(cb) {
48 - const DevTools = initializeFrontend(contentWindow);
49 -
50 - // Activate the backend only once the DevTools frontend Store has been initialized.
51 - // Otherwise the Store may miss important initial tree op codes.
52 - activateBackend(contentWindow);
53 -
56 const root = createRoot(container);
57 root.render(
58 createElement(DevTools, {
packages/react-devtools-shell/webpack.config.js
+12
@@ -1,6 +1,12 @@
1 const {resolve} = require('path');
2 const {DefinePlugin} = require('webpack');
3 const {
4 + DARK_MODE_DIMMED_WARNING_COLOR,
5 + DARK_MODE_DIMMED_ERROR_COLOR,
6 + DARK_MODE_DIMMED_LOG_COLOR,
7 + LIGHT_MODE_DIMMED_WARNING_COLOR,
8 + LIGHT_MODE_DIMMED_ERROR_COLOR,
9 + LIGHT_MODE_DIMMED_LOG_COLOR,
10 GITHUB_URL,
11 getVersionString,
12 } = require('react-devtools-extensions/utils');
@@ -59,6 +65,12 @@ const config = {
65 'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
66 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-shell"`,
67 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
68 + 'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
69 + 'process.env.DARK_MODE_DIMMED_ERROR_COLOR': `"${DARK_MODE_DIMMED_ERROR_COLOR}"`,
70 + 'process.env.DARK_MODE_DIMMED_LOG_COLOR': `"${DARK_MODE_DIMMED_LOG_COLOR}"`,
71 + 'process.env.LIGHT_MODE_DIMMED_WARNING_COLOR': `"${LIGHT_MODE_DIMMED_WARNING_COLOR}"`,
72 + 'process.env.LIGHT_MODE_DIMMED_ERROR_COLOR': `"${LIGHT_MODE_DIMMED_ERROR_COLOR}"`,
73 + 'process.env.LIGHT_MODE_DIMMED_LOG_COLOR': `"${LIGHT_MODE_DIMMED_LOG_COLOR}"`,
74 }),
75 ],
76 module: {
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+13 -11
@@ -151,7 +151,11 @@ import {
151 getOffscreenContainerProps,
152 } from './ReactFiberHostConfig';
153 import type {SuspenseInstance} from './ReactFiberHostConfig';
154 -import {shouldError, shouldSuspend} from './ReactFiberReconciler';
154 +import {
155 + shouldError,
156 + shouldSuspend,
157 + setIsStrictModeForDevtools,
158 +} from './ReactFiberReconciler';
159 import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.new';
160 import {
161 suspenseStackCursor,
@@ -235,8 +239,6 @@ import {createClassErrorUpdate} from './ReactFiberThrow.new';
239 import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.new';
240 import is from 'shared/objectIs';
241
238 -import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
239 -
242 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
243
244 let didReceiveUpdate: boolean = false;
@@ -379,7 +381,7 @@ function updateForwardRef(
381 debugRenderPhaseSideEffectsForStrictMode &&
382 workInProgress.mode & StrictLegacyMode
383 ) {
382 - disableLogs();
384 + setIsStrictModeForDevtools(true);
385 try {
386 nextChildren = renderWithHooks(
387 current,
@@ -390,7 +392,7 @@ function updateForwardRef(
392 renderLanes,
393 );
394 } finally {
393 - reenableLogs();
395 + setIsStrictModeForDevtools(false);
396 }
397 }
398 setIsRendering(false);
@@ -984,7 +986,7 @@ function updateFunctionComponent(
986 debugRenderPhaseSideEffectsForStrictMode &&
987 workInProgress.mode & StrictLegacyMode
988 ) {
987 - disableLogs();
989 + setIsStrictModeForDevtools(true);
990 try {
991 nextChildren = renderWithHooks(
992 current,
@@ -995,7 +997,7 @@ function updateFunctionComponent(
997 renderLanes,
998 );
999 } finally {
998 - reenableLogs();
1000 + setIsStrictModeForDevtools(false);
1001 }
1002 }
1003 setIsRendering(false);
@@ -1201,11 +1203,11 @@ function finishClassComponent(
1203 debugRenderPhaseSideEffectsForStrictMode &&
1204 workInProgress.mode & StrictLegacyMode
1205 ) {
1204 - disableLogs();
1206 + setIsStrictModeForDevtools(true);
1207 try {
1208 instance.render();
1209 } finally {
1208 - reenableLogs();
1210 + setIsStrictModeForDevtools(false);
1211 }
1212 }
1213 setIsRendering(false);
@@ -1741,7 +1743,7 @@ function mountIndeterminateComponent(
1743 debugRenderPhaseSideEffectsForStrictMode &&
1744 workInProgress.mode & StrictLegacyMode
1745 ) {
1744 - disableLogs();
1746 + setIsStrictModeForDevtools(true);
1747 try {
1748 value = renderWithHooks(
1749 null,
@@ -1752,7 +1754,7 @@ function mountIndeterminateComponent(
1754 renderLanes,
1755 );
1756 } finally {
1755 - reenableLogs();
1757 + setIsStrictModeForDevtools(false);
1758 }
1759 }
1760 }
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+13 -11
@@ -151,7 +151,11 @@ import {
151 getOffscreenContainerProps,
152 } from './ReactFiberHostConfig';
153 import type {SuspenseInstance} from './ReactFiberHostConfig';
154 -import {shouldError, shouldSuspend} from './ReactFiberReconciler';
154 +import {
155 + shouldError,
156 + shouldSuspend,
157 + setIsStrictModeForDevtools,
158 +} from './ReactFiberReconciler';
159 import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.old';
160 import {
161 suspenseStackCursor,
@@ -235,8 +239,6 @@ import {createClassErrorUpdate} from './ReactFiberThrow.old';
239 import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.old';
240 import is from 'shared/objectIs';
241
238 -import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
239 -
242 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
243
244 let didReceiveUpdate: boolean = false;
@@ -379,7 +381,7 @@ function updateForwardRef(
381 debugRenderPhaseSideEffectsForStrictMode &&
382 workInProgress.mode & StrictLegacyMode
383 ) {
382 - disableLogs();
384 + setIsStrictModeForDevtools(true);
385 try {
386 nextChildren = renderWithHooks(
387 current,
@@ -390,7 +392,7 @@ function updateForwardRef(
392 renderLanes,
393 );
394 } finally {
393 - reenableLogs();
395 + setIsStrictModeForDevtools(false);
396 }
397 }
398 setIsRendering(false);
@@ -984,7 +986,7 @@ function updateFunctionComponent(
986 debugRenderPhaseSideEffectsForStrictMode &&
987 workInProgress.mode & StrictLegacyMode
988 ) {
987 - disableLogs();
989 + setIsStrictModeForDevtools(true);
990 try {
991 nextChildren = renderWithHooks(
992 current,
@@ -995,7 +997,7 @@ function updateFunctionComponent(
997 renderLanes,
998 );
999 } finally {
998 - reenableLogs();
1000 + setIsStrictModeForDevtools(false);
1001 }
1002 }
1003 setIsRendering(false);
@@ -1201,11 +1203,11 @@ function finishClassComponent(
1203 debugRenderPhaseSideEffectsForStrictMode &&
1204 workInProgress.mode & StrictLegacyMode
1205 ) {
1204 - disableLogs();
1206 + setIsStrictModeForDevtools(true);
1207 try {
1208 instance.render();
1209 } finally {
1208 - reenableLogs();
1210 + setIsStrictModeForDevtools(false);
1211 }
1212 }
1213 setIsRendering(false);
@@ -1741,7 +1743,7 @@ function mountIndeterminateComponent(
1743 debugRenderPhaseSideEffectsForStrictMode &&
1744 workInProgress.mode & StrictLegacyMode
1745 ) {
1744 - disableLogs();
1746 + setIsStrictModeForDevtools(true);
1747 try {
1748 value = renderWithHooks(
1749 null,
@@ -1752,7 +1754,7 @@ function mountIndeterminateComponent(
1754 renderLanes,
1755 );
1756 } finally {
1755 - reenableLogs();
1757 + setIsStrictModeForDevtools(false);
1758 }
1759 }
1760 }
packages/react-reconciler/src/ReactFiberClassComponent.new.js
+7 -7
@@ -38,6 +38,7 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
38 import invariant from 'shared/invariant';
39 import isArray from 'shared/isArray';
40 import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
41 +import {setIsStrictModeForDevtools} from './ReactFiberReconciler';
42
43 import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
44 import {
@@ -75,7 +76,6 @@ import {
76 } from './ReactFiberWorkLoop.new';
77 import {logForceUpdateScheduled, logStateUpdateScheduled} from './DebugTracing';
78
78 -import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
79 import {
80 markForceUpdateScheduled,
81 markStateUpdateScheduled,
@@ -175,12 +175,12 @@ function applyDerivedStateFromProps(
175 debugRenderPhaseSideEffectsForStrictMode &&
176 workInProgress.mode & StrictLegacyMode
177 ) {
178 - disableLogs();
178 + setIsStrictModeForDevtools(true);
179 try {
180 // Invoke the function an extra time to help detect side-effects.
181 partialState = getDerivedStateFromProps(nextProps, prevState);
182 } finally {
183 - reenableLogs();
183 + setIsStrictModeForDevtools(false);
184 }
185 }
186 warnOnUndefinedDerivedState(ctor, partialState);
@@ -328,7 +328,7 @@ function checkShouldComponentUpdate(
328 debugRenderPhaseSideEffectsForStrictMode &&
329 workInProgress.mode & StrictLegacyMode
330 ) {
331 - disableLogs();
331 + setIsStrictModeForDevtools(true);
332 try {
333 // Invoke the function an extra time to help detect side-effects.
334 shouldUpdate = instance.shouldComponentUpdate(
@@ -337,7 +337,7 @@ function checkShouldComponentUpdate(
337 nextContext,
338 );
339 } finally {
340 - reenableLogs();
340 + setIsStrictModeForDevtools(false);
341 }
342 }
343 if (shouldUpdate === undefined) {
@@ -662,11 +662,11 @@ function constructClassInstance(
662 debugRenderPhaseSideEffectsForStrictMode &&
663 workInProgress.mode & StrictLegacyMode
664 ) {
665 - disableLogs();
665 + setIsStrictModeForDevtools(true);
666 try {
667 instance = new ctor(props, context); // eslint-disable-line no-new
668 } finally {
669 - reenableLogs();
669 + setIsStrictModeForDevtools(false);
670 }
671 }
672 }
packages/react-reconciler/src/ReactFiberClassComponent.old.js
+7 -7
@@ -38,6 +38,7 @@ import getComponentNameFromType from 'shared/getComponentNameFromType';
38 import invariant from 'shared/invariant';
39 import isArray from 'shared/isArray';
40 import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols';
41 +import {setIsStrictModeForDevtools} from './ReactFiberReconciler';
42
43 import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
44 import {
@@ -75,7 +76,6 @@ import {
76 } from './ReactFiberWorkLoop.old';
77 import {logForceUpdateScheduled, logStateUpdateScheduled} from './DebugTracing';
78
78 -import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
79 import {
80 markForceUpdateScheduled,
81 markStateUpdateScheduled,
@@ -175,12 +175,12 @@ function applyDerivedStateFromProps(
175 debugRenderPhaseSideEffectsForStrictMode &&
176 workInProgress.mode & StrictLegacyMode
177 ) {
178 - disableLogs();
178 + setIsStrictModeForDevtools(true);
179 try {
180 // Invoke the function an extra time to help detect side-effects.
181 partialState = getDerivedStateFromProps(nextProps, prevState);
182 } finally {
183 - reenableLogs();
183 + setIsStrictModeForDevtools(false);
184 }
185 }
186 warnOnUndefinedDerivedState(ctor, partialState);
@@ -328,7 +328,7 @@ function checkShouldComponentUpdate(
328 debugRenderPhaseSideEffectsForStrictMode &&
329 workInProgress.mode & StrictLegacyMode
330 ) {
331 - disableLogs();
331 + setIsStrictModeForDevtools(true);
332 try {
333 // Invoke the function an extra time to help detect side-effects.
334 shouldUpdate = instance.shouldComponentUpdate(
@@ -337,7 +337,7 @@ function checkShouldComponentUpdate(
337 nextContext,
338 );
339 } finally {
340 - reenableLogs();
340 + setIsStrictModeForDevtools(false);
341 }
342 }
343 if (shouldUpdate === undefined) {
@@ -662,11 +662,11 @@ function constructClassInstance(
662 debugRenderPhaseSideEffectsForStrictMode &&
663 workInProgress.mode & StrictLegacyMode
664 ) {
665 - disableLogs();
665 + setIsStrictModeForDevtools(true);
666 try {
667 instance = new ctor(props, context); // eslint-disable-line no-new
668 } finally {
669 - reenableLogs();
669 + setIsStrictModeForDevtools(false);
670 }
671 }
672 }
packages/react-reconciler/src/ReactFiberHooks.new.js
+4 -3
@@ -113,6 +113,7 @@ import {
113 entangleTransitions,
114 } from './ReactUpdateQueue.new';
115 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
116 +import {getIsStrictModeForDevtools} from './ReactFiberReconciler.new';
117
118 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
119
@@ -1021,9 +1022,9 @@ function readFromUnsubscribedMutableSource<Source, Snapshot>(
1022 // dev, we can present a more accurate error message.
1023 if (__DEV__) {
1024 // eslint-disable-next-line react-internal/no-production-logging
1024 - if (console.log.__reactDisabledLog) {
1025 - // If the logs are disabled, this is the dev-only double render. This is
1026 - // only reachable if there was a mutation during render. Show a helpful
1025 + if (getIsStrictModeForDevtools()) {
1026 + // If getIsStrictModeForDevtools is true, this is the dev-only double render
1027 + // This is only reachable if there was a mutation during render. Show a helpful
1028 // error message.
1029 //
1030 // Something interesting to note: because we only double render in
packages/react-reconciler/src/ReactFiberHooks.old.js
+4 -3
@@ -113,6 +113,7 @@ import {
113 entangleTransitions,
114 } from './ReactUpdateQueue.old';
115 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
116 +import {getIsStrictModeForDevtools} from './ReactFiberReconciler.old';
117
118 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
119
@@ -1021,9 +1022,9 @@ function readFromUnsubscribedMutableSource<Source, Snapshot>(
1022 // dev, we can present a more accurate error message.
1023 if (__DEV__) {
1024 // eslint-disable-next-line react-internal/no-production-logging
1024 - if (console.log.__reactDisabledLog) {
1025 - // If the logs are disabled, this is the dev-only double render. This is
1026 - // only reachable if there was a mutation during render. Show a helpful
1025 + if (getIsStrictModeForDevtools()) {
1026 + // If getIsStrictModeForDevtools is true, this is the dev-only double render
1027 + // This is only reachable if there was a mutation during render. Show a helpful
1028 // error message.
1029 //
1030 // Something interesting to note: because we only double render in
packages/react-reconciler/src/ReactFiberReconciler.js
+11
@@ -49,6 +49,8 @@ import {
49 registerMutableSourceForHydration as registerMutableSourceForHydration_old,
50 runWithPriority as runWithPriority_old,
51 getCurrentUpdatePriority as getCurrentUpdatePriority_old,
52 + getIsStrictModeForDevtools as getIsStrictModeForDevtools_old,
53 + setIsStrictModeForDevtools as setIsStrictModeForDevtools_old,
54 } from './ReactFiberReconciler.old';
55
56 import {
@@ -86,6 +88,8 @@ import {
88 registerMutableSourceForHydration as registerMutableSourceForHydration_new,
89 runWithPriority as runWithPriority_new,
90 getCurrentUpdatePriority as getCurrentUpdatePriority_new,
91 + getIsStrictModeForDevtools as getIsStrictModeForDevtools_new,
92 + setIsStrictModeForDevtools as setIsStrictModeForDevtools_new,
93 } from './ReactFiberReconciler.new';
94
95 export const createContainer = enableNewReconciler
@@ -190,3 +194,10 @@ export const registerMutableSourceForHydration = enableNewReconciler
194 export const runWithPriority = enableNewReconciler
195 ? runWithPriority_new
196 : runWithPriority_old;
197 +
198 +export const getIsStrictModeForDevtools = enableNewReconciler
199 + ? getIsStrictModeForDevtools_new
200 + : getIsStrictModeForDevtools_old;
201 +export const setIsStrictModeForDevtools = enableNewReconciler
202 + ? setIsStrictModeForDevtools_new
203 + : setIsStrictModeForDevtools_old;
packages/react-reconciler/src/ReactFiberReconciler.new.js
+21
@@ -105,6 +105,9 @@ export {
105 observeVisibleRects,
106 } from './ReactTestSelectors';
107
108 +import * as Scheduler from './Scheduler';
109 +import {setSuppressWarning} from 'shared/consoleWithStackDev';
110 +
111 type OpaqueRoot = FiberRoot;
112
113 // 0 is PROD, 1 is DEV.
@@ -458,6 +461,8 @@ export function shouldSuspend(fiber: Fiber): boolean {
461 return shouldSuspendImpl(fiber);
462 }
463
464 +let isStrictMode = false;
465 +
466 let overrideHookState = null;
467 let overrideHookStateDeletePath = null;
468 let overrideHookStateRenamePath = null;
@@ -707,6 +712,21 @@ function getCurrentFiberForDevTools() {
712 return ReactCurrentFiberCurrent;
713 }
714
715 +export function getIsStrictModeForDevtools() {
716 + return isStrictMode;
717 +}
718 +
719 +export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
720 + // We're in a test because Scheduler.unstable_yieldValue only exists
721 + // in SchedulerMock. To reduce the noise in strict mode tests,
722 + // suppress warnings and disable scheduler yielding during the double render
723 + if (typeof Scheduler.unstable_yieldValue === 'function') {
724 + Scheduler.unstable_setDisableYieldValue(newIsStrictMode);
725 + setSuppressWarning(newIsStrictMode);
726 + }
727 + isStrictMode = newIsStrictMode;
728 +}
729 +
730 export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
731 const {findFiberByHostInstance} = devToolsConfig;
732 const {ReactCurrentDispatcher} = ReactSharedInternals;
@@ -736,6 +756,7 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
756 setRefreshHandler: __DEV__ ? setRefreshHandler : null,
757 // Enables DevTools to append owner stacks to error messages in DEV mode.
758 getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null,
759 + getIsStrictMode: __DEV__ ? getIsStrictModeForDevtools : null,
760 // Enables DevTools to detect reconciler version rather than renderer version
761 // which may not match for third party renderers.
762 reconcilerVersion: ReactVersion,
packages/react-reconciler/src/ReactFiberReconciler.old.js
+21
@@ -105,6 +105,9 @@ export {
105 observeVisibleRects,
106 } from './ReactTestSelectors';
107
108 +import * as Scheduler from './Scheduler';
109 +import {setSuppressWarning} from 'shared/consoleWithStackDev';
110 +
111 type OpaqueRoot = FiberRoot;
112
113 // 0 is PROD, 1 is DEV.
@@ -458,6 +461,8 @@ export function shouldSuspend(fiber: Fiber): boolean {
461 return shouldSuspendImpl(fiber);
462 }
463
464 +let isStrictMode = false;
465 +
466 let overrideHookState = null;
467 let overrideHookStateDeletePath = null;
468 let overrideHookStateRenamePath = null;
@@ -707,6 +712,21 @@ function getCurrentFiberForDevTools() {
712 return ReactCurrentFiberCurrent;
713 }
714
715 +export function getIsStrictModeForDevtools() {
716 + return isStrictMode;
717 +}
718 +
719 +export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
720 + // We're in a test because Scheduler.unstable_yieldValue only exists
721 + // in SchedulerMock. To reduce the noise in strict mode tests,
722 + // suppress warnings and disable scheduler yielding during the double render
723 + if (typeof Scheduler.unstable_yieldValue === 'function') {
724 + Scheduler.unstable_setDisableYieldValue(newIsStrictMode);
725 + setSuppressWarning(newIsStrictMode);
726 + }
727 + isStrictMode = newIsStrictMode;
728 +}
729 +
730 export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
731 const {findFiberByHostInstance} = devToolsConfig;
732 const {ReactCurrentDispatcher} = ReactSharedInternals;
@@ -736,6 +756,7 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
756 setRefreshHandler: __DEV__ ? setRefreshHandler : null,
757 // Enables DevTools to append owner stacks to error messages in DEV mode.
758 getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null,
759 + getIsStrictMode: __DEV__ ? getIsStrictModeForDevtools : null,
760 // Enables DevTools to detect reconciler version rather than renderer version
761 // which may not match for third party renderers.
762 reconcilerVersion: ReactVersion,
packages/react-reconciler/src/ReactUpdateQueue.new.js
+5 -6
@@ -110,11 +110,10 @@ import {
110 isInterleavedUpdate,
111 } from './ReactFiberWorkLoop.new';
112 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
113 +import {setIsStrictModeForDevtools} from './ReactFiberReconciler';
114
115 import invariant from 'shared/invariant';
116
116 -import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
117 -
117 export type Update<State> = {|
118 // TODO: Temporary field. Will remove this by storing a map of
119 // transition -> event time on the root.
@@ -394,11 +393,11 @@ function getStateFromUpdate<State>(
393 debugRenderPhaseSideEffectsForStrictMode &&
394 workInProgress.mode & StrictLegacyMode
395 ) {
397 - disableLogs();
396 + setIsStrictModeForDevtools(true);
397 try {
398 payload.call(instance, prevState, nextProps);
399 } finally {
401 - reenableLogs();
400 + setIsStrictModeForDevtools(false);
401 }
402 }
403 exitDisallowedContextReadInDEV();
@@ -427,11 +426,11 @@ function getStateFromUpdate<State>(
426 debugRenderPhaseSideEffectsForStrictMode &&
427 workInProgress.mode & StrictLegacyMode
428 ) {
430 - disableLogs();
429 + setIsStrictModeForDevtools(true);
430 try {
431 payload.call(instance, prevState, nextProps);
432 } finally {
434 - reenableLogs();
433 + setIsStrictModeForDevtools(false);
434 }
435 }
436 exitDisallowedContextReadInDEV();
packages/react-reconciler/src/ReactUpdateQueue.old.js
+5 -6
@@ -110,11 +110,10 @@ import {
110 isInterleavedUpdate,
111 } from './ReactFiberWorkLoop.old';
112 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
113 +import {setIsStrictModeForDevtools} from './ReactFiberReconciler';
114
115 import invariant from 'shared/invariant';
116
116 -import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
117 -
117 export type Update<State> = {|
118 // TODO: Temporary field. Will remove this by storing a map of
119 // transition -> event time on the root.
@@ -394,11 +393,11 @@ function getStateFromUpdate<State>(
393 debugRenderPhaseSideEffectsForStrictMode &&
394 workInProgress.mode & StrictLegacyMode
395 ) {
397 - disableLogs();
396 + setIsStrictModeForDevtools(true);
397 try {
398 payload.call(instance, prevState, nextProps);
399 } finally {
401 - reenableLogs();
400 + setIsStrictModeForDevtools(false);
401 }
402 }
403 exitDisallowedContextReadInDEV();
@@ -427,11 +426,11 @@ function getStateFromUpdate<State>(
426 debugRenderPhaseSideEffectsForStrictMode &&
427 workInProgress.mode & StrictLegacyMode
428 ) {
430 - disableLogs();
429 + setIsStrictModeForDevtools(true);
430 try {
431 payload.call(instance, prevState, nextProps);
432 } finally {
434 - reenableLogs();
433 + setIsStrictModeForDevtools(false);
434 }
435 }
436 exitDisallowedContextReadInDEV();
packages/react-reconciler/src/Scheduler.js
+6
@@ -26,3 +26,9 @@ export const NormalPriority = Scheduler.unstable_NormalPriority;
26 export const LowPriority = Scheduler.unstable_LowPriority;
27 export const IdlePriority = Scheduler.unstable_IdlePriority;
28 export type SchedulerCallback = (isSync: boolean) => SchedulerCallback | null;
29 +
30 +// this doesn't actually exist on the scheduler, but it *does*
31 +// on scheduler/unstable_mock, which we'll need for internal testing
32 +export const unstable_yieldValue = Scheduler.unstable_yieldValue;
33 +export const unstable_setDisableYieldValue =
34 + Scheduler.unstable_setDisableYieldValue;
packages/react/src/__tests__/ReactStrictMode-test.js
+13 -13
@@ -893,8 +893,8 @@ describe('context legacy', () => {
893 ReactDOM.render(<Root />, container);
894 });
895
896 - describe('disableLogs', () => {
897 - it('disables logs once for class double render', () => {
896 + describe('logging', () => {
897 + it('does not disable logs for class double render', () => {
898 spyOnDevAndProd(console, 'log');
899
900 let count = 0;
@@ -915,14 +915,14 @@ describe('context legacy', () => {
915 );
916
917 expect(count).toBe(__DEV__ ? 2 : 1);
918 - expect(console.log).toBeCalledTimes(1);
918 + expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1);
919 // Note: we should display the first log because otherwise
920 // there is a risk of suppressing warnings when they happen,
921 // and on the next render they'd get deduplicated and ignored.
922 expect(console.log).toBeCalledWith('foo 1');
923 });
924
925 - it('disables logs once for class double ctor', () => {
925 + it('does not disable logs for class double ctor', () => {
926 spyOnDevAndProd(console, 'log');
927
928 let count = 0;
@@ -946,14 +946,14 @@ describe('context legacy', () => {
946 );
947
948 expect(count).toBe(__DEV__ ? 2 : 1);
949 - expect(console.log).toBeCalledTimes(1);
949 + expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1);
950 // Note: we should display the first log because otherwise
951 // there is a risk of suppressing warnings when they happen,
952 // and on the next render they'd get deduplicated and ignored.
953 expect(console.log).toBeCalledWith('foo 1');
954 });
955
956 - it('disables logs once for class double getDerivedStateFromProps', () => {
956 + it('does not disable logs for class double getDerivedStateFromProps', () => {
957 spyOnDevAndProd(console, 'log');
958
959 let count = 0;
@@ -978,14 +978,14 @@ describe('context legacy', () => {
978 );
979
980 expect(count).toBe(__DEV__ ? 2 : 1);
981 - expect(console.log).toBeCalledTimes(1);
981 + expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1);
982 // Note: we should display the first log because otherwise
983 // there is a risk of suppressing warnings when they happen,
984 // and on the next render they'd get deduplicated and ignored.
985 expect(console.log).toBeCalledWith('foo 1');
986 });
987
988 - it('disables logs once for class double shouldComponentUpdate', () => {
988 + it('does not disable logs for class double shouldComponentUpdate', () => {
989 spyOnDevAndProd(console, 'log');
990
991 let count = 0;
@@ -1017,14 +1017,14 @@ describe('context legacy', () => {
1017 );
1018
1019 expect(count).toBe(__DEV__ ? 2 : 1);
1020 - expect(console.log).toBeCalledTimes(1);
1020 + expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1);
1021 // Note: we should display the first log because otherwise
1022 // there is a risk of suppressing warnings when they happen,
1023 // and on the next render they'd get deduplicated and ignored.
1024 expect(console.log).toBeCalledWith('foo 1');
1025 });
1026
1027 - it('disables logs once for class state updaters', () => {
1027 + it('does not disable logs for class state updaters', () => {
1028 spyOnDevAndProd(console, 'log');
1029
1030 let inst;
@@ -1051,14 +1051,14 @@ describe('context legacy', () => {
1051 });
1052
1053 expect(count).toBe(__DEV__ ? 2 : 1);
1054 - expect(console.log).toBeCalledTimes(1);
1054 + expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1);
1055 // Note: we should display the first log because otherwise
1056 // there is a risk of suppressing warnings when they happen,
1057 // and on the next render they'd get deduplicated and ignored.
1058 expect(console.log).toBeCalledWith('foo 1');
1059 });
1060
1061 - it('disables logs once for function double render', () => {
1061 + it('does not disable logs for function double render', () => {
1062 spyOnDevAndProd(console, 'log');
1063
1064 let count = 0;
@@ -1077,7 +1077,7 @@ describe('context legacy', () => {
1077 );
1078
1079 expect(count).toBe(__DEV__ ? 2 : 1);
1080 - expect(console.log).toBeCalledTimes(1);
1080 + expect(console.log).toBeCalledTimes(__DEV__ ? 2 : 1);
1081 // Note: we should display the first log because otherwise
1082 // there is a risk of suppressing warnings when they happen,
1083 // and on the next render they'd get deduplicated and ignored.
packages/scheduler/src/forks/SchedulerMock.js
+9 -2
@@ -79,6 +79,12 @@ let isFlushing: boolean = false;
79 let needsPaint: boolean = false;
80 let shouldYieldForPaint: boolean = false;
81
82 +var disableYieldValue = false;
83 +
84 +function setDisableYieldValue(newValue) {
85 + disableYieldValue = newValue;
86 +}
87 +
88 function advanceTimers(currentTime) {
89 // Check for tasks that are no longer delayed and add them to the queue.
90 let timer = peek(timerQueue);
@@ -570,7 +576,7 @@ function unstable_flushAll(): void {
576
577 function unstable_yieldValue(value: mixed): void {
578 // eslint-disable-next-line react-internal/no-production-logging
573 - if (console.log.name === 'disabledLog') {
579 + if (console.log.name === 'disabledLog' || disableYieldValue) {
580 // If console.log has been patched, we assume we're in render
581 // replaying and we ignore any values yielding in the second pass.
582 return;
@@ -584,7 +590,7 @@ function unstable_yieldValue(value: mixed): void {
590
591 function unstable_advanceTime(ms: number) {
592 // eslint-disable-next-line react-internal/no-production-logging
587 - if (console.log.name === 'disabledLog') {
593 + if (console.log.name === 'disabledLog' || disableYieldValue) {
594 // If console.log has been patched, we assume we're in render
595 // replaying and we ignore any time advancing in the second pass.
596 return;
@@ -629,6 +635,7 @@ export {
635 unstable_yieldValue,
636 unstable_advanceTime,
637 reset,
638 + setDisableYieldValue as unstable_setDisableYieldValue,
639 };
640
641 export const unstable_Profiling = enableProfiling
packages/shared/consoleWithStackDev.js
+13 -2
@@ -7,6 +7,13 @@
7
8 import ReactSharedInternals from 'shared/ReactSharedInternals';
9
10 +let suppressWarning = false;
11 +export function setSuppressWarning(newSuppressWarning) {
12 + if (__DEV__) {
13 + suppressWarning = newSuppressWarning;
14 + }
15 +}
16 +
17 // In DEV, calls to console.warn and console.error get replaced
18 // by calls to these methods by a Babel plugin.
19 //
@@ -15,13 +22,17 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
22
23 export function warn(format, ...args) {
24 if (__DEV__) {
18 - printWarning('warn', format, args);
25 + if (!suppressWarning) {
26 + printWarning('warn', format, args);
27 + }
28 }
29 }
30
31 export function error(format, ...args) {
32 if (__DEV__) {
24 - printWarning('error', format, args);
33 + if (!suppressWarning) {
34 + printWarning('error', format, args);
35 + }
36 }
37 }
38
packages/shared/forks/Scheduler.umd.js
+6 -1
@@ -31,8 +31,11 @@ const {
31 unstable_forceFrameRate,
32
33 // this doesn't actually exist on the scheduler, but it *does*
34 - // on scheduler/unstable_mock, which we'll need inside act().
34 + // on scheduler/unstable_mock, which we'll need inside act()
35 + // and for internal testing
36 unstable_flushAllWithoutAsserting,
37 + unstable_yieldValue,
38 + unstable_setDisableYieldValue,
39 } = ReactInternals.Scheduler;
40
41 export {
@@ -54,4 +57,6 @@ export {
57 unstable_IdlePriority,
58 unstable_forceFrameRate,
59 unstable_flushAllWithoutAsserting,
60 + unstable_yieldValue,
61 + unstable_setDisableYieldValue,
62 };
packages/shared/forks/consoleWithStackDev.www.js
+13 -2
@@ -8,15 +8,26 @@
8 // This refers to a WWW module.
9 const warningWWW = require('warning');
10
11 +let suppressWarning = false;
12 +export function setSuppressWarning(newSuppressWarning) {
13 + if (__DEV__) {
14 + suppressWarning = newSuppressWarning;
15 + }
16 +}
17 +
18 export function warn(format, ...args) {
19 if (__DEV__) {
13 - printWarning('warn', format, args);
20 + if (!suppressWarning) {
21 + printWarning('warn', format, args);
22 + }
23 }
24 }
25
26 export function error(format, ...args) {
27 if (__DEV__) {
19 - printWarning('error', format, args);
28 + if (!suppressWarning) {
29 + printWarning('error', format, args);
30 + }
31 }
32 }
33
scripts/flow/react-devtools.js
+7
@@ -9,3 +9,10 @@
9
10 declare var __EXTENSION__: boolean;
11 declare var __TEST__: boolean;
12 +
13 +declare var DARK_MODE_DIMMED_WARNING_COLOR: string;
14 +declare var DARK_MODE_DIMMED_ERROR_COLOR: string;
15 +declare var DARK_MODE_DIMMED_LOG_COLOR: string;
16 +declare var LIGHT_MODE_DIMMED_WARNING_COLOR: string;
17 +declare var LIGHT_MODE_DIMMED_ERROR_COLOR: string;
18 +declare var LIGHT_MODE_DIMMED_LOG_COLOR: string;