@samitouri / QOS-React-2 / commits / 5b57bc6e31

[Draft] don't patch console during first render (#22308)

Previously, DevTools always overrode the native console to dim or supress StrictMode double logging. It also overrode console.log (in addition to console.error and console.warn). However, this changes the location shown by the browser console, which causes a bad developer experience. There is currently a TC39 proposal that would allow us to extend console without breaking developer experience, but in the meantime this PR changes the StrictMode console override behavior so that we only patch the console during the StrictMode double render so that, during the first render, the location points to developer code rather than our DevTools console code.

Luna Ruan committed Sep 21, 2021 at 15:00 UTC 5b57bc6e31689ad715161cdcfa1b1d31eac2ba3b
18 files changed +676 -454
packages/react-devtools-shared/src/__tests__/console-test.js
+172 -50
@@ -17,11 +17,10 @@ let mockLog;
17 let mockWarn;
18 let patchConsole;
19 let unpatchConsole;
20 +let rendererID;
21
22 describe('console', () => {
23 beforeEach(() => {
23 - jest.resetModules();
24 -
24 const Console = require('react-devtools-shared/src/backend/console');
25 patchConsole = Console.patch;
26 unpatchConsole = Console.unpatch;
@@ -41,21 +40,16 @@ describe('console', () => {
40 };
41
42 Console.dangerous_setTargetConsoleForTesting(fakeConsole);
44 -
45 - // Note the Console module only patches once,
46 - // so it's important to patch the test console before injection.
47 - patchConsole({
48 - appendComponentStack: true,
49 - breakOnWarn: false,
50 - showInlineWarningsAndErrors: false,
51 - hideDoubleLogsInStrictLegacy: false,
52 - });
43 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.dangerous_setTargetConsoleForTesting(
44 + fakeConsole,
45 + );
46
47 const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
48 global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => {
56 - inject(internals);
49 + rendererID = inject(internals);
50
51 Console.registerRenderer(internals);
52 + return rendererID;
53 };
54
55 React = require('react');
@@ -78,7 +72,7 @@ describe('console', () => {
72 it('should not patch console methods that are not explicitly overriden', () => {
73 expect(fakeConsole.error).not.toBe(mockError);
74 expect(fakeConsole.info).toBe(mockInfo);
81 - expect(fakeConsole.log).not.toBe(mockLog);
75 + expect(fakeConsole.log).toBe(mockLog);
76 expect(fakeConsole.warn).not.toBe(mockWarn);
77 });
78
@@ -90,7 +84,7 @@ describe('console', () => {
84
85 patchConsole({
86 appendComponentStack: true,
93 - breakOnWarn: false,
87 + breakOnConsoleErrors: false,
88 showInlineWarningsAndErrors: false,
89 });
90
@@ -98,7 +92,7 @@ describe('console', () => {
92 expect(fakeConsole.warn).not.toBe(mockWarn);
93 });
94
101 - it('should patch the console when breakOnWarn is enabled', () => {
95 + it('should patch the console when breakOnConsoleErrors is enabled', () => {
96 unpatchConsole();
97
98 expect(fakeConsole.error).toBe(mockError);
@@ -106,7 +100,7 @@ describe('console', () => {
100
101 patchConsole({
102 appendComponentStack: false,
109 - breakOnWarn: true,
103 + breakOnConsoleErrors: true,
104 showInlineWarningsAndErrors: false,
105 });
106
@@ -122,7 +116,7 @@ describe('console', () => {
116
117 patchConsole({
118 appendComponentStack: false,
125 - breakOnWarn: false,
119 + breakOnConsoleErrors: false,
120 showInlineWarningsAndErrors: true,
121 });
122
@@ -135,7 +129,7 @@ describe('console', () => {
129
130 patchConsole({
131 appendComponentStack: true,
138 - breakOnWarn: false,
132 + breakOnConsoleErrors: false,
133 showInlineWarningsAndErrors: false,
134 });
135
@@ -172,6 +166,8 @@ describe('console', () => {
166 });
167
168 it('should not append multiple stacks', () => {
169 + global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
170 +
171 const Child = ({children}) => {
172 fakeConsole.warn('warn\n in Child (at fake.js:123)');
173 fakeConsole.error('error', '\n in Child (at fake.js:123)');
@@ -192,6 +188,8 @@ describe('console', () => {
188 });
189
190 it('should append component stacks to errors and warnings logged during render', () => {
191 + global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
192 +
193 const Intermediate = ({children}) => children;
194 const Parent = ({children}) => (
195 <Intermediate>
@@ -277,6 +275,8 @@ describe('console', () => {
275 });
276
277 it('should append component stacks to errors and warnings logged from commit hooks', () => {
278 + global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
279 +
280 const Intermediate = ({children}) => children;
281 const Parent = ({children}) => (
282 <Intermediate>
@@ -372,13 +372,14 @@ describe('console', () => {
372 });
373
374 it('should append stacks after being uninstalled and reinstalled', () => {
375 + global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
376 +
377 const Child = ({children}) => {
378 fakeConsole.warn('warn');
379 fakeConsole.error('error');
380 return null;
381 };
382
381 - unpatchConsole();
383 act(() => legacyRender(<Child />, document.createElement('div')));
384
385 expect(mockWarn).toHaveBeenCalledTimes(1);
@@ -390,7 +391,7 @@ describe('console', () => {
391
392 patchConsole({
393 appendComponentStack: true,
393 - breakOnWarn: false,
394 + breakOnConsoleErrors: false,
395 showInlineWarningsAndErrors: false,
396 });
397 act(() => legacyRender(<Child />, document.createElement('div')));
@@ -410,6 +411,8 @@ describe('console', () => {
411 });
412
413 it('should be resilient to prepareStackTrace', () => {
414 + global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
415 +
416 Error.prepareStackTrace = function(error, callsites) {
417 const stack = ['An error occurred:', error.message];
418 for (let i = 0; i < callsites.length; i++) {
@@ -469,6 +472,9 @@ describe('console', () => {
472 });
473
474 it('should double log if hideConsoleLogsInStrictMode is disabled in Strict mode', () => {
475 + global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
476 + global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
477 +
478 const container = document.createElement('div');
479 const root = ReactDOM.createRoot(container);
480
@@ -479,13 +485,6 @@ describe('console', () => {
485 return <div />;
486 }
487
482 - patchConsole({
483 - appendComponentStack: false,
484 - breakOnWarn: false,
485 - showInlineWarningsAndErrors: false,
486 - hideConsoleLogsInStrictMode: false,
487 - });
488 -
488 act(() =>
489 root.render(
490 <React.StrictMode>
@@ -493,8 +492,6 @@ describe('console', () => {
492 </React.StrictMode>,
493 ),
494 );
496 -
497 - expect(mockLog).toHaveBeenCalledTimes(2);
495 expect(mockLog.mock.calls[0]).toHaveLength(1);
496 expect(mockLog.mock.calls[0][0]).toBe('log');
497 expect(mockLog.mock.calls[1]).toHaveLength(2);
@@ -514,6 +511,9 @@ describe('console', () => {
511 });
512
513 it('should not double log if hideConsoleLogsInStrictMode is enabled in Strict mode', () => {
514 + global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
515 + global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true;
516 +
517 const container = document.createElement('div');
518 const root = ReactDOM.createRoot(container);
519
@@ -524,12 +524,47 @@ describe('console', () => {
524 return <div />;
525 }
526
527 - patchConsole({
528 - appendComponentStack: false,
529 - breakOnWarn: false,
530 - showInlineWarningsAndErrors: false,
531 - hideConsoleLogsInStrictMode: true,
532 - });
527 + act(() =>
528 + root.render(
529 + <React.StrictMode>
530 + <App />
531 + </React.StrictMode>,
532 + ),
533 + );
534 +
535 + expect(mockLog).toHaveBeenCalledTimes(1);
536 + expect(mockLog.mock.calls[0]).toHaveLength(1);
537 + expect(mockLog.mock.calls[0][0]).toBe('log');
538 +
539 + expect(mockWarn).toHaveBeenCalledTimes(1);
540 + expect(mockWarn.mock.calls[0]).toHaveLength(1);
541 + expect(mockWarn.mock.calls[0][0]).toBe('warn');
542 +
543 + expect(mockError).toHaveBeenCalledTimes(1);
544 + expect(mockError.mock.calls[0]).toHaveLength(1);
545 + expect(mockError.mock.calls[0][0]).toBe('error');
546 + });
547 +
548 + it('should double log in Strict mode initial render for extension', () => {
549 + global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
550 + global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
551 +
552 + // This simulates a render that happens before React DevTools have finished
553 + // their handshake to attach the React DOM renderer functions to DevTools
554 + // In this case, we should still be able to mock the console in Strict mode
555 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.set(
556 + rendererID,
557 + null,
558 + );
559 + const container = document.createElement('div');
560 + const root = ReactDOM.createRoot(container);
561 +
562 + function App() {
563 + fakeConsole.log('log');
564 + fakeConsole.warn('warn');
565 + fakeConsole.error('error');
566 + return <div />;
567 + }
568
569 act(() =>
570 root.render(
@@ -539,6 +574,53 @@ describe('console', () => {
574 ),
575 );
576
577 + expect(mockLog).toHaveBeenCalledTimes(2);
578 + expect(mockLog.mock.calls[0]).toHaveLength(1);
579 + expect(mockLog.mock.calls[0][0]).toBe('log');
580 + expect(mockLog.mock.calls[1]).toHaveLength(2);
581 + expect(mockLog.mock.calls[1][0]).toBe('%clog');
582 +
583 + expect(mockWarn).toHaveBeenCalledTimes(2);
584 + expect(mockWarn.mock.calls[0]).toHaveLength(1);
585 + expect(mockWarn.mock.calls[0][0]).toBe('warn');
586 + expect(mockWarn.mock.calls[1]).toHaveLength(2);
587 + expect(mockWarn.mock.calls[1][0]).toBe('%cwarn');
588 +
589 + expect(mockError).toHaveBeenCalledTimes(2);
590 + expect(mockError.mock.calls[0]).toHaveLength(1);
591 + expect(mockError.mock.calls[0][0]).toBe('error');
592 + expect(mockError.mock.calls[1]).toHaveLength(2);
593 + expect(mockError.mock.calls[1][0]).toBe('%cerror');
594 + });
595 +
596 + it('should not double log in Strict mode initial render for extension', () => {
597 + global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = false;
598 + global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = true;
599 +
600 + // This simulates a render that happens before React DevTools have finished
601 + // their handshake to attach the React DOM renderer functions to DevTools
602 + // In this case, we should still be able to mock the console in Strict mode
603 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.set(
604 + rendererID,
605 + null,
606 + );
607 + const container = document.createElement('div');
608 + const root = ReactDOM.createRoot(container);
609 +
610 + function App() {
611 + fakeConsole.log('log');
612 + fakeConsole.warn('warn');
613 + fakeConsole.error('error');
614 + return <div />;
615 + }
616 +
617 + act(() =>
618 + root.render(
619 + <React.StrictMode>
620 + <App />
621 + </React.StrictMode>,
622 + ),
623 + );
624 expect(mockLog).toHaveBeenCalledTimes(1);
625 expect(mockLog.mock.calls[0]).toHaveLength(1);
626 expect(mockLog.mock.calls[0][0]).toBe('log');
@@ -551,6 +633,56 @@ describe('console', () => {
633 expect(mockError.mock.calls[0]).toHaveLength(1);
634 expect(mockError.mock.calls[0][0]).toBe('error');
635 });
636 +
637 + it('should properly dim component stacks during strict mode double log', () => {
638 + global.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = true;
639 + global.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = false;
640 +
641 + const container = document.createElement('div');
642 + const root = ReactDOM.createRoot(container);
643 +
644 + const Intermediate = ({children}) => children;
645 + const Parent = ({children}) => (
646 + <Intermediate>
647 + <Child />
648 + </Intermediate>
649 + );
650 + const Child = ({children}) => {
651 + fakeConsole.error('error');
652 + fakeConsole.warn('warn');
653 + return null;
654 + };
655 +
656 + act(() =>
657 + root.render(
658 + <React.StrictMode>
659 + <Parent />
660 + </React.StrictMode>,
661 + ),
662 + );
663 +
664 + expect(mockWarn).toHaveBeenCalledTimes(2);
665 + expect(mockWarn.mock.calls[0]).toHaveLength(2);
666 + expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
667 + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
668 + );
669 + expect(mockWarn.mock.calls[1]).toHaveLength(2);
670 + expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][0])).toEqual(
671 + '%cwarn \n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
672 + );
673 + expect(mockWarn.mock.calls[1][1]).toMatch('color: rgba(');
674 +
675 + expect(mockError).toHaveBeenCalledTimes(2);
676 + expect(mockError.mock.calls[0]).toHaveLength(2);
677 + expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toEqual(
678 + '\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
679 + );
680 + expect(mockError.mock.calls[1]).toHaveLength(2);
681 + expect(normalizeCodeLocInfo(mockError.mock.calls[1][0])).toEqual(
682 + '%cerror \n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
683 + );
684 + expect(mockError.mock.calls[1][1]).toMatch('color: rgba(');
685 + });
686 });
687
688 describe('console error', () => {
@@ -577,23 +709,13 @@ describe('console error', () => {
709
710 Console.dangerous_setTargetConsoleForTesting(fakeConsole);
711
580 - // Note the Console module only patches once,
581 - // so it's important to patch the test console before injection.
582 - patchConsole({
583 - appendComponentStack: true,
584 - breakOnWarn: false,
585 - showInlineWarningsAndErrors: false,
586 - hideDoubleLogsInStrictLegacy: false,
587 - });
588 -
712 const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
713 global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => {
591 - internals.getIsStrictMode = () => {
592 - throw Error('foo');
593 - };
714 inject(internals);
715
596 - Console.registerRenderer(internals);
716 + Console.registerRenderer(internals, () => {
717 + throw Error('foo');
718 + });
719 };
720
721 React = require('react');
@@ -617,8 +739,8 @@ describe('console error', () => {
739
740 patchConsole({
741 appendComponentStack: true,
620 - breakOnWarn: false,
621 - showInlineWarningsAndErrors: false,
742 + breakOnConsoleErrors: false,
743 + showInlineWarningsAndErrors: true,
744 hideConsoleLogsInStrictMode: false,
745 });
746
packages/react-devtools-shared/src/backend/console.js
+200 -137
@@ -16,7 +16,7 @@ import {getInternalReactConstants} from './renderer';
16 import {getStackByFiberInDevAndProd} from './DevToolsFiberComponentStack';
17 import {consoleManagedByDevToolsDuringStrictMode} from 'react-devtools-feature-flags';
18
19 -const OVERRIDE_CONSOLE_METHODS = ['error', 'trace', 'warn', 'log'];
19 +const OVERRIDE_CONSOLE_METHODS = ['error', 'trace', 'warn'];
20 const DIMMED_NODE_CONSOLE_COLOR = '\x1b[2m%s\x1b[0m';
21
22 // React's custom built component stack strings match "\s{4}in"
@@ -30,6 +30,37 @@ export function isStringComponentStack(text: string): boolean {
30 return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text);
31 }
32
33 +const STYLE_DIRECTIVE_REGEX = /^%c/;
34 +
35 +// This function tells whether or not the arguments for a console
36 +// method has been overridden by the patchForStrictMode function.
37 +// If it has we'll need to do some special formatting of the arguments
38 +// so the console color stays consistent
39 +function isStrictModeOverride(args: Array<string>, method: string): boolean {
40 + return (
41 + args.length === 2 &&
42 + STYLE_DIRECTIVE_REGEX.test(args[0]) &&
43 + args[1] === `color: ${getConsoleColor(method) || ''}`
44 + );
45 +}
46 +
47 +function getConsoleColor(method: string): ?string {
48 + switch (method) {
49 + case 'warn':
50 + return consoleSettingsRef.browserTheme === 'light'
51 + ? process.env.LIGHT_MODE_DIMMED_WARNING_COLOR
52 + : process.env.DARK_MODE_DIMMED_WARNING_COLOR;
53 + case 'error':
54 + return consoleSettingsRef.browserTheme === 'light'
55 + ? process.env.LIGHT_MODE_DIMMED_ERROR_COLOR
56 + : process.env.DARK_MODE_DIMMED_ERROR_COLOR;
57 + case 'log':
58 + default:
59 + return consoleSettingsRef.browserTheme === 'light'
60 + ? process.env.LIGHT_MODE_DIMMED_LOG_COLOR
61 + : process.env.DARK_MODE_DIMMED_LOG_COLOR;
62 + }
63 +}
64 type OnErrorOrWarning = (
65 fiber: Fiber,
66 type: 'error' | 'warn',
@@ -43,7 +74,6 @@ const injectedRenderers: Map<
74 getCurrentFiber: () => Fiber | null,
75 onErrorOrWarning: ?OnErrorOrWarning,
76 workTagMap: WorkTagMap,
46 - getIsStrictMode: ?() => boolean,
77 |},
78 > = new Map();
79
@@ -82,7 +112,6 @@ export function registerRenderer(
112 const {
113 currentDispatcherRef,
114 getCurrentFiber,
85 - getIsStrictMode,
115 findFiberByHostInstance,
116 version,
117 } = renderer;
@@ -100,7 +129,6 @@ export function registerRenderer(
129 injectedRenderers.set(renderer, {
130 currentDispatcherRef,
131 getCurrentFiber,
103 - getIsStrictMode,
132 workTagMap: ReactTypeOfWork,
133 onErrorOrWarning,
134 });
@@ -112,11 +140,11 @@ const consoleSettingsRef = {
140 breakOnConsoleErrors: false,
141 showInlineWarningsAndErrors: false,
142 hideConsoleLogsInStrictMode: false,
143 + browserTheme: 'dark',
144 };
145
146 // Patches console methods to append component stack for the current fiber.
147 // Call unpatch() to remove the injected behavior.
119 -// NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialRenderInExtension
148 export function patch({
149 appendComponentStack,
150 breakOnConsoleErrors,
@@ -136,141 +164,176 @@ export function patch({
164 consoleSettingsRef.breakOnConsoleErrors = breakOnConsoleErrors;
165 consoleSettingsRef.showInlineWarningsAndErrors = showInlineWarningsAndErrors;
166 consoleSettingsRef.hideConsoleLogsInStrictMode = hideConsoleLogsInStrictMode;
167 + consoleSettingsRef.browserTheme = browserTheme;
168 +
169 + if (
170 + appendComponentStack ||
171 + breakOnConsoleErrors ||
172 + showInlineWarningsAndErrors
173 + ) {
174 + if (unpatchFn !== null) {
175 + // Don't patch twice.
176 + return;
177 + }
178
140 - if (unpatchFn !== null) {
141 - // Don't patch twice.
142 - return;
143 - }
179 + const originalConsoleMethods = {};
180
145 - const originalConsoleMethods = {};
181 + unpatchFn = () => {
182 + for (const method in originalConsoleMethods) {
183 + try {
184 + // $FlowFixMe property error|warn is not writable.
185 + targetConsole[method] = originalConsoleMethods[method];
186 + } catch (error) {}
187 + }
188 + };
189
147 - unpatchFn = () => {
148 - for (const method in originalConsoleMethods) {
190 + OVERRIDE_CONSOLE_METHODS.forEach(method => {
191 try {
150 - // $FlowFixMe property error|warn is not writable.
151 - targetConsole[method] = originalConsoleMethods[method];
152 - } catch (error) {}
153 - }
154 - };
155 -
156 - OVERRIDE_CONSOLE_METHODS.forEach(method => {
157 - try {
158 - const originalMethod = (originalConsoleMethods[method] = targetConsole[
159 - method
160 - ].__REACT_DEVTOOLS_ORIGINAL_METHOD__
161 - ? targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
162 - : targetConsole[method]);
163 -
164 - const overrideMethod = (...args) => {
165 - let shouldAppendWarningStack = false;
166 - if (method !== 'log') {
167 - if (consoleSettingsRef.appendComponentStack) {
168 - const lastArg = args.length > 0 ? args[args.length - 1] : null;
169 - const alreadyHasComponentStack =
170 - typeof lastArg === 'string' && isStringComponentStack(lastArg);
171 -
172 - // If we are ever called with a string that already has a component stack,
173 - // e.g. a React error/warning, don't append a second stack.
174 - shouldAppendWarningStack = !alreadyHasComponentStack;
192 + const originalMethod = (originalConsoleMethods[method] = targetConsole[
193 + method
194 + ].__REACT_DEVTOOLS_ORIGINAL_METHOD__
195 + ? targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
196 + : targetConsole[method]);
197 +
198 + const overrideMethod = (...args) => {
199 + let shouldAppendWarningStack = false;
200 + if (method !== 'log') {
201 + if (consoleSettingsRef.appendComponentStack) {
202 + const lastArg = args.length > 0 ? args[args.length - 1] : null;
203 + const alreadyHasComponentStack =
204 + typeof lastArg === 'string' && isStringComponentStack(lastArg);
205 +
206 + // If we are ever called with a string that already has a component stack,
207 + // e.g. a React error/warning, don't append a second stack.
208 + shouldAppendWarningStack = !alreadyHasComponentStack;
209 + }
210 }
176 - }
177 -
178 - const shouldShowInlineWarningsAndErrors =
179 - consoleSettingsRef.showInlineWarningsAndErrors &&
180 - (method === 'error' || method === 'warn');
181 -
182 - let isInStrictMode = false;
183 -
184 - // Search for the first renderer that has a current Fiber.
185 - // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
186 - // eslint-disable-next-line no-for-of-loops/no-for-of-loops
187 - for (const {
188 - currentDispatcherRef,
189 - getCurrentFiber,
190 - onErrorOrWarning,
191 - workTagMap,
192 - getIsStrictMode,
193 - } of injectedRenderers.values()) {
194 - const current: ?Fiber = getCurrentFiber();
195 - if (current != null) {
196 - try {
197 - if (typeof getIsStrictMode === 'function' && getIsStrictMode()) {
198 - isInStrictMode = true;
199 - }
211
201 - if (shouldShowInlineWarningsAndErrors) {
202 - // patch() is called by two places: (1) the hook and (2) the renderer backend.
203 - // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
204 - if (typeof onErrorOrWarning === 'function') {
205 - onErrorOrWarning(
206 - current,
207 - ((method: any): 'error' | 'warn'),
208 - // Copy args before we mutate them (e.g. adding the component stack)
209 - args.slice(),
210 - );
212 + const shouldShowInlineWarningsAndErrors =
213 + consoleSettingsRef.showInlineWarningsAndErrors &&
214 + (method === 'error' || method === 'warn');
215 +
216 + // Search for the first renderer that has a current Fiber.
217 + // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
218 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
219 + for (const {
220 + currentDispatcherRef,
221 + getCurrentFiber,
222 + onErrorOrWarning,
223 + workTagMap,
224 + } of injectedRenderers.values()) {
225 + const current: ?Fiber = getCurrentFiber();
226 + if (current != null) {
227 + try {
228 + if (shouldShowInlineWarningsAndErrors) {
229 + // patch() is called by two places: (1) the hook and (2) the renderer backend.
230 + // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning.
231 + if (typeof onErrorOrWarning === 'function') {
232 + onErrorOrWarning(
233 + current,
234 + ((method: any): 'error' | 'warn'),
235 + // Copy args before we mutate them (e.g. adding the component stack)
236 + args.slice(),
237 + );
238 + }
239 }
212 - }
240
214 - if (shouldAppendWarningStack) {
215 - const componentStack = getStackByFiberInDevAndProd(
216 - workTagMap,
217 - current,
218 - currentDispatcherRef,
219 - );
220 - if (componentStack !== '') {
221 - args.push(componentStack);
241 + if (shouldAppendWarningStack) {
242 + const componentStack = getStackByFiberInDevAndProd(
243 + workTagMap,
244 + current,
245 + currentDispatcherRef,
246 + );
247 + if (componentStack !== '') {
248 + if (isStrictModeOverride(args, method)) {
249 + args[0] = format(args[0], componentStack);
250 + } else {
251 + args.push(componentStack);
252 + }
253 + }
254 }
255 + } catch (error) {
256 + // Don't let a DevTools or React internal error interfere with logging.
257 + setTimeout(() => {
258 + throw error;
259 + }, 0);
260 + } finally {
261 + break;
262 }
224 - } catch (error) {
225 - // Don't let a DevTools or React internal error interfere with logging.
226 - setTimeout(() => {
227 - throw error;
228 - }, 0);
229 - } finally {
230 - break;
263 }
264 }
233 - }
234 -
235 - if (consoleSettingsRef.breakOnConsoleErrors) {
236 - // --- Welcome to debugging with React DevTools ---
237 - // This debugger statement means that you've enabled the "break on warnings" feature.
238 - // Use the browser's Call Stack panel to step out of this override function-
239 - // to where the original warning or error was logged.
240 - // eslint-disable-next-line no-debugger
241 - debugger;
242 - }
243 -
244 - if (consoleManagedByDevToolsDuringStrictMode && isInStrictMode) {
265 +
266 + if (consoleSettingsRef.breakOnConsoleErrors) {
267 + // --- Welcome to debugging with React DevTools ---
268 + // This debugger statement means that you've enabled the "break on warnings" feature.
269 + // Use the browser's Call Stack panel to step out of this override function-
270 + // to where the original warning or error was logged.
271 + // eslint-disable-next-line no-debugger
272 + debugger;
273 + }
274 +
275 + originalMethod(...args);
276 + };
277 +
278 + overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
279 + originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod;
280 +
281 + // $FlowFixMe property error|warn is not writable.
282 + targetConsole[method] = overrideMethod;
283 + } catch (error) {}
284 + });
285 + } else {
286 + unpatch();
287 + }
288 +}
289 +
290 +// Removed component stack patch from console methods.
291 +export function unpatch(): void {
292 + if (unpatchFn !== null) {
293 + unpatchFn();
294 + unpatchFn = null;
295 + }
296 +}
297 +
298 +let unpatchForStrictModeFn: null | (() => void) = null;
299 +
300 +// NOTE: KEEP IN SYNC with src/hook.js:patchConsoleForInitialRenderInStrictMode
301 +export function patchForStrictMode() {
302 + if (consoleManagedByDevToolsDuringStrictMode) {
303 + const overrideConsoleMethods = ['error', 'trace', 'warn', 'log'];
304 +
305 + if (unpatchForStrictModeFn !== null) {
306 + // Don't patch twice.
307 + return;
308 + }
309 +
310 + const originalConsoleMethods = {};
311 +
312 + unpatchForStrictModeFn = () => {
313 + for (const method in originalConsoleMethods) {
314 + try {
315 + // $FlowFixMe property error|warn is not writable.
316 + targetConsole[method] = originalConsoleMethods[method];
317 + } catch (error) {}
318 + }
319 + };
320 +
321 + overrideConsoleMethods.forEach(method => {
322 + try {
323 + const originalMethod = (originalConsoleMethods[method] = targetConsole[
324 + method
325 + ].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
326 + ? targetConsole[method].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
327 + : targetConsole[method]);
328 +
329 + const overrideMethod = (...args) => {
330 if (!consoleSettingsRef.hideConsoleLogsInStrictMode) {
331 // Dim the text color of the double logs if we're not
332 // hiding them.
333 if (isNode) {
334 originalMethod(DIMMED_NODE_CONSOLE_COLOR, format(...args));
335 } else {
251 - let color;
252 - switch (method) {
253 - case 'warn':
254 - color =
255 - browserTheme === 'light'
256 - ? process.env.LIGHT_MODE_DIMMED_WARNING_COLOR
257 - : process.env.DARK_MODE_DIMMED_WARNING_COLOR;
258 - break;
259 - case 'error':
260 - color =
261 - browserTheme === 'light'
262 - ? process.env.LIGHT_MODE_DIMMED_ERROR_COLOR
263 - : process.env.DARK_MODE_DIMMED_ERROR_COLOR;
264 - break;
265 - case 'log':
266 - default:
267 - color =
268 - browserTheme === 'light'
269 - ? process.env.LIGHT_MODE_DIMMED_LOG_COLOR
270 - : process.env.DARK_MODE_DIMMED_LOG_COLOR;
271 - break;
272 - }
273 -
336 + const color = getConsoleColor(method);
337 if (color) {
338 originalMethod(`%c${format(...args)}`, `color: ${color}`);
339 } else {
@@ -278,24 +341,24 @@ export function patch({
341 }
342 }
343 }
281 - } else {
282 - originalMethod(...args);
283 - }
284 - };
344 + };
345
286 - overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
287 - originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod;
346 + overrideMethod.__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ = originalMethod;
347 + originalMethod.__REACT_DEVTOOLS_STRICT_MODE_OVERRIDE_METHOD__ = overrideMethod;
348
289 - // $FlowFixMe property error|warn is not writable.
290 - targetConsole[method] = overrideMethod;
291 - } catch (error) {}
292 - });
349 + // $FlowFixMe property error|warn is not writable.
350 + targetConsole[method] = overrideMethod;
351 + } catch (error) {}
352 + });
353 + }
354 }
355
295 -// Removed component stack patch from console methods.
296 -export function unpatch(): void {
297 - if (unpatchFn !== null) {
298 - unpatchFn();
299 - unpatchFn = null;
356 +// NOTE: KEEP IN SYNC with src/hook.js:unpatchConsoleForInitialRenderInStrictMode
357 +export function unpatchForStrictMode(): void {
358 + if (consoleManagedByDevToolsDuringStrictMode) {
359 + if (unpatchForStrictModeFn !== null) {
360 + unpatchForStrictModeFn();
361 + unpatchForStrictModeFn = null;
362 + }
363 }
364 }
packages/react-devtools-shared/src/backend/legacy/renderer.js
+6
@@ -1073,6 +1073,10 @@ export function attach(
1073 // Not implemented
1074 }
1075
1076 + function patchConsoleForStrictMode() {}
1077 +
1078 + function unpatchConsoleForStrictMode() {}
1079 +
1080 return {
1081 clearErrorsAndWarnings,
1082 clearErrorsForFiberID,
@@ -1101,6 +1105,7 @@ export function attach(
1105 overrideSuspense,
1106 overrideValueAtPath,
1107 renamePath,
1108 + patchConsoleForStrictMode,
1109 prepareViewAttributeSource,
1110 prepareViewElementSource,
1111 renderer,
@@ -1109,6 +1114,7 @@ export function attach(
1114 startProfiling,
1115 stopProfiling,
1116 storeAsGlobal,
1117 + unpatchConsoleForStrictMode,
1118 updateComponentFilters,
1119 };
1120 }
packages/react-devtools-shared/src/backend/renderer.js
+4
@@ -59,6 +59,8 @@ import {inspectHooksOfFiber} from 'react-debug-tools';
59 import {
60 patch as patchConsole,
61 registerRenderer as registerRendererWithConsole,
62 + patchForStrictMode as patchConsoleForStrictMode,
63 + unpatchForStrictMode as unpatchConsoleForStrictMode,
64 } from './console';
65 import {
66 CONCURRENT_MODE_NUMBER,
@@ -4249,6 +4251,7 @@ export function attach(
4251 handlePostCommitFiberRoot,
4252 inspectElement,
4253 logElementToConsole,
4254 + patchConsoleForStrictMode,
4255 prepareViewAttributeSource,
4256 prepareViewElementSource,
4257 overrideError,
@@ -4261,6 +4264,7 @@ export function attach(
4264 startProfiling,
4265 stopProfiling,
4266 storeAsGlobal,
4267 + unpatchConsoleForStrictMode,
4268 updateComponentFilters,
4269 };
4270 }
packages/react-devtools-shared/src/backend/types.js
+5 -2
@@ -136,8 +136,6 @@ 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,
139 // 17.0.2+
140 reconcilerVersion?: string,
141 // Uniquely identifies React DOM v15.
@@ -352,6 +350,7 @@ export type RendererInterface = {
350 path: Array<string | number>,
351 value: any,
352 ) => void,
353 + patchConsoleForStrictMode: () => void,
354 prepareViewAttributeSource: (
355 id: number,
356 path: Array<string | number>,
@@ -374,6 +373,7 @@ export type RendererInterface = {
373 path: Array<string | number>,
374 count: number,
375 ) => void,
376 + unpatchConsoleForStrictMode: () => void,
377 updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void,
378 ...
379 };
@@ -408,5 +408,8 @@ export type DevToolsHook = {
408 // Added in v16.9 to support Fast Refresh
409 didError?: boolean,
410 ) => void,
411 +
412 + // Testing
413 + dangerous_setTargetConsoleForTesting?: (fakeConsole: Object) => void,
414 ...
415 };
packages/react-devtools-shared/src/hook.js
+189 -161
@@ -8,15 +8,12 @@
8 * @flow
9 */
10
11 -import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
12 -import type {ReactRenderer} from './backend/types';
11 import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
12
13 import {
14 patch as patchConsole,
15 registerRenderer as registerRendererWithConsole,
16 } from './backend/console';
19 -import {consoleManagedByDevToolsDuringStrictMode} from 'react-devtools-feature-flags';
17
18 import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
19
@@ -27,6 +24,23 @@ export function installHook(target: any): DevToolsHook | null {
24 return null;
25 }
26
27 + let targetConsole: Object = console;
28 + let targetConsoleMethods = {};
29 + for (const method in console) {
30 + targetConsoleMethods[method] = console[method];
31 + }
32 +
33 + function dangerous_setTargetConsoleForTesting(
34 + targetConsoleForTesting: Object,
35 + ): void {
36 + targetConsole = targetConsoleForTesting;
37 +
38 + targetConsoleMethods = {};
39 + for (const method in targetConsole) {
40 + targetConsoleMethods[method] = console[method];
41 + }
42 + }
43 +
44 function detectReactBuildType(renderer) {
45 try {
46 if (typeof renderer.version === 'string') {
@@ -163,156 +177,148 @@ export function installHook(target: any): DevToolsHook | null {
177 maybeMessage: any,
178 ...inputArgs: $ReadOnlyArray<any>
179 ): string {
166 - if (consoleManagedByDevToolsDuringStrictMode) {
167 - const args = inputArgs.slice();
168 -
169 - // Symbols cannot be concatenated with Strings.
170 - let formatted: string =
171 - typeof maybeMessage === 'symbol'
172 - ? maybeMessage.toString()
173 - : '' + maybeMessage;
174 -
175 - // If the first argument is a string, check for substitutions.
176 - if (typeof maybeMessage === 'string') {
177 - if (args.length) {
178 - const REGEXP = /(%?)(%([jds]))/g;
179 -
180 - formatted = formatted.replace(REGEXP, (match, escaped, ptn, flag) => {
181 - let arg = args.shift();
182 - switch (flag) {
183 - case 's':
184 - arg += '';
185 - break;
186 - case 'd':
187 - case 'i':
188 - arg = parseInt(arg, 10).toString();
189 - break;
190 - case 'f':
191 - arg = parseFloat(arg).toString();
192 - break;
193 - }
194 - if (!escaped) {
195 - return arg;
196 - }
197 - args.unshift(arg);
198 - return match;
199 - });
200 - }
201 - }
180 + const args = inputArgs.slice();
181
203 - // Arguments that remain after formatting.
204 - if (args.length) {
205 - for (let i = 0; i < args.length; i++) {
206 - const arg = args[i];
182 + // Symbols cannot be concatenated with Strings.
183 + let formatted: string =
184 + typeof maybeMessage === 'symbol'
185 + ? maybeMessage.toString()
186 + : '' + maybeMessage;
187
208 - // Symbols cannot be concatenated with Strings.
209 - formatted += ' ' + (typeof arg === 'symbol' ? arg.toString() : arg);
210 - }
188 + // If the first argument is a string, check for substitutions.
189 + if (typeof maybeMessage === 'string') {
190 + if (args.length) {
191 + const REGEXP = /(%?)(%([jds]))/g;
192 +
193 + formatted = formatted.replace(REGEXP, (match, escaped, ptn, flag) => {
194 + let arg = args.shift();
195 + switch (flag) {
196 + case 's':
197 + arg += '';
198 + break;
199 + case 'd':
200 + case 'i':
201 + arg = parseInt(arg, 10).toString();
202 + break;
203 + case 'f':
204 + arg = parseFloat(arg).toString();
205 + break;
206 + }
207 + if (!escaped) {
208 + return arg;
209 + }
210 + args.unshift(arg);
211 + return match;
212 + });
213 }
214 + }
215
213 - // Update escaped %% values.
214 - formatted = formatted.replace(/%{2,2}/g, '%');
216 + // Arguments that remain after formatting.
217 + if (args.length) {
218 + for (let i = 0; i < args.length; i++) {
219 + const arg = args[i];
220
216 - return '' + formatted;
221 + // Symbols cannot be concatenated with Strings.
222 + formatted += ' ' + (typeof arg === 'symbol' ? arg.toString() : arg);
223 + }
224 }
225
219 - return '';
226 + // Update escaped %% values.
227 + formatted = formatted.replace(/%{2,2}/g, '%');
228 +
229 + return '' + formatted;
230 }
231
222 - // NOTE: KEEP IN SYNC with src/backend/console.js:patch
223 - function patchConsoleForInitialRenderInExtension(
224 - renderer: ReactRenderer,
225 - {
226 - hideConsoleLogsInStrictMode,
227 - browserTheme,
228 - }: {hideConsoleLogsInStrictMode: boolean, browserTheme: BrowserTheme},
229 - ): void {
230 - if (consoleManagedByDevToolsDuringStrictMode) {
231 - const overrideConsoleMethods = ['error', 'trace', 'warn', 'log'];
232 -
233 - if (__EXTENSION__) {
234 - const targetConsole = console;
235 -
236 - const originalConsoleMethods = {};
237 -
238 - overrideConsoleMethods.forEach(method => {
239 - try {
240 - const originalMethod = (originalConsoleMethods[
241 - method
242 - ] = targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
243 - ? targetConsole[method].__REACT_DEVTOOLS_ORIGINAL_METHOD__
244 - : targetConsole[method]);
245 -
246 - const overrideMethod = (...args) => {
247 - let isInStrictMode = false;
248 -
249 - // Search for the first renderer that has a current Fiber.
250 - // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
251 - const {getCurrentFiber, getIsStrictMode} = renderer;
252 - if (typeof getCurrentFiber !== 'function') {
253 - return;
254 - }
255 -
256 - const current: ?Fiber = getCurrentFiber();
257 - if (current != null) {
258 - try {
259 - if (
260 - typeof getIsStrictMode === 'function' &&
261 - getIsStrictMode()
262 - ) {
263 - isInStrictMode = true;
264 - }
265 - } catch (error) {
266 - // Don't let a DevTools or React internal error interfere with logging.
267 - }
268 - }
269 -
270 - if (isInStrictMode) {
271 - if (!hideConsoleLogsInStrictMode) {
272 - // Dim the text color of the double logs if we're not
273 - // hiding them.
274 - let color;
275 - switch (method) {
276 - case 'warn':
277 - color =
278 - browserTheme === 'light'
279 - ? process.env.LIGHT_MODE_DIMMED_WARNING_COLOR
280 - : process.env.DARK_MODE_DIMMED_WARNING_COLOR;
281 - break;
282 - case 'error':
283 - color =
284 - browserTheme === 'light'
285 - ? process.env.LIGHT_MODE_DIMMED_ERROR_COLOR
286 - : process.env.DARK_MODE_DIMMED_ERROR_COLOR;
287 - break;
288 - case 'log':
289 - default:
290 - color =
291 - browserTheme === 'light'
292 - ? process.env.LIGHT_MODE_DIMMED_LOG_COLOR
293 - : process.env.DARK_MODE_DIMMED_LOG_COLOR;
294 - break;
295 - }
296 -
297 - if (color) {
298 - originalMethod(`%c${format(...args)}`, `color: ${color}`);
299 - } else {
300 - throw Error('Console color is not defined');
301 - }
302 - }
303 - } else {
304 - originalMethod(...args);
305 - }
306 - };
307 -
308 - overrideMethod.__REACT_DEVTOOLS_ORIGINAL_METHOD__ = originalMethod;
309 - originalMethod.__REACT_DEVTOOLS_OVERRIDE_METHOD__ = overrideMethod;
310 -
311 - // $FlowFixMe property error|warn is not writable.
312 - targetConsole[method] = overrideMethod;
313 - } catch (error) {}
314 - });
232 + let unpatchFn = null;
233 +
234 + // NOTE: KEEP IN SYNC with src/backend/console.js:patchForStrictMode
235 + // This function hides or dims console logs during the initial double renderer
236 + // in Strict Mode. We need this function because during initial render,
237 + // React and DevTools are connecting and the renderer interface isn't avaiable
238 + // and we want to be able to have consistent logging behavior for double logs
239 + // during the initial renderer.
240 + function patchConsoleForInitialRenderInStrictMode({
241 + hideConsoleLogsInStrictMode,
242 + browserTheme,
243 + }: {
244 + hideConsoleLogsInStrictMode: boolean,
245 + browserTheme: BrowserTheme,
246 + }) {
247 + const overrideConsoleMethods = ['error', 'trace', 'warn', 'log'];
248 +
249 + if (unpatchFn !== null) {
250 + // Don't patch twice.
251 + return;
252 + }
253 +
254 + const originalConsoleMethods = {};
255 +
256 + unpatchFn = () => {
257 + for (const method in originalConsoleMethods) {
258 + try {
259 + // $FlowFixMe property error|warn is not writable.
260 + targetConsole[method] = originalConsoleMethods[method];
261 + } catch (error) {}
262 }
263 + };
264 +
265 + overrideConsoleMethods.forEach(method => {
266 + try {
267 + const originalMethod = (originalConsoleMethods[method] = targetConsole[
268 + method
269 + ].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
270 + ? targetConsole[method].__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__
271 + : targetConsole[method]);
272 +
273 + const overrideMethod = (...args) => {
274 + if (!hideConsoleLogsInStrictMode) {
275 + // Dim the text color of the double logs if we're not
276 + // hiding them.
277 + let color;
278 + switch (method) {
279 + case 'warn':
280 + color =
281 + browserTheme === 'light'
282 + ? process.env.LIGHT_MODE_DIMMED_WARNING_COLOR
283 + : process.env.DARK_MODE_DIMMED_WARNING_COLOR;
284 + break;
285 + case 'error':
286 + color =
287 + browserTheme === 'light'
288 + ? process.env.LIGHT_MODE_DIMMED_ERROR_COLOR
289 + : process.env.DARK_MODE_DIMMED_ERROR_COLOR;
290 + break;
291 + case 'log':
292 + default:
293 + color =
294 + browserTheme === 'light'
295 + ? process.env.LIGHT_MODE_DIMMED_LOG_COLOR
296 + : process.env.DARK_MODE_DIMMED_LOG_COLOR;
297 + break;
298 + }
299 +
300 + if (color) {
301 + originalMethod(`%c${format(...args)}`, `color: ${color}`);
302 + } else {
303 + throw Error('Console color is not defined');
304 + }
305 + }
306 + };
307 +
308 + overrideMethod.__REACT_DEVTOOLS_STRICT_MODE_ORIGINAL_METHOD__ = originalMethod;
309 + originalMethod.__REACT_DEVTOOLS_STRICT_MODE_OVERRIDE_METHOD__ = overrideMethod;
310 +
311 + // $FlowFixMe property error|warn is not writable.
312 + targetConsole[method] = overrideMethod;
313 + } catch (error) {}
314 + });
315 + }
316 +
317 + // NOTE: KEEP IN SYNC with src/backend/console.js:unpatchForStrictMode
318 + function unpatchConsoleForInitialRenderInStrictMode() {
319 + if (unpatchFn !== null) {
320 + unpatchFn();
321 + unpatchFn = null;
322 }
323 }
324
@@ -343,7 +349,7 @@ export function installHook(target: any): DevToolsHook | null {
349 // Note that because this function is inlined, this conditional check must only use static booleans.
350 // Otherwise the extension will throw with an undefined error.
351 // (See comments in the try/catch below for more context on inlining.)
346 - if (!__TEST__) {
352 + if (!__TEST__ && !__EXTENSION__) {
353 try {
354 const appendComponentStack =
355 window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false;
@@ -362,23 +368,14 @@ export function installHook(target: any): DevToolsHook | null {
368 // but Webpack wraps imports with an object (e.g. _backend_console__WEBPACK_IMPORTED_MODULE_0__)
369 // and the object itself will be undefined as well for the reasons mentioned above,
370 // so we use try/catch instead.
365 - if (!__EXTENSION__) {
366 - registerRendererWithConsole(renderer);
367 - patchConsole({
368 - appendComponentStack,
369 - breakOnConsoleErrors,
370 - showInlineWarningsAndErrors,
371 - hideConsoleLogsInStrictMode,
372 - browserTheme,
373 - });
374 - } else {
375 - if (consoleManagedByDevToolsDuringStrictMode) {
376 - patchConsoleForInitialRenderInExtension(renderer, {
377 - hideConsoleLogsInStrictMode,
378 - browserTheme,
379 - });
380 - }
381 - }
371 + registerRendererWithConsole(renderer);
372 + patchConsole({
373 + appendComponentStack,
374 + breakOnConsoleErrors,
375 + showInlineWarningsAndErrors,
376 + hideConsoleLogsInStrictMode,
377 + browserTheme,
378 + });
379 } catch (error) {}
380 }
381
@@ -473,6 +470,32 @@ export function installHook(target: any): DevToolsHook | null {
470 }
471 }
472
473 + function setStrictMode(rendererID, isStrictMode) {
474 + const rendererInterface = rendererInterfaces.get(rendererID);
475 + if (rendererInterface != null) {
476 + if (isStrictMode) {
477 + rendererInterface.patchConsoleForStrictMode();
478 + } else {
479 + rendererInterface.unpatchConsoleForStrictMode();
480 + }
481 + } else {
482 + // This should only happen during initial render in the extension before DevTools
483 + // finishes its handshake with the injected renderer
484 + if (isStrictMode) {
485 + const hideConsoleLogsInStrictMode =
486 + window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ === true;
487 + const browserTheme = window.__REACT_DEVTOOLS_BROWSER_THEME__;
488 +
489 + patchConsoleForInitialRenderInStrictMode({
490 + hideConsoleLogsInStrictMode,
491 + browserTheme,
492 + });
493 + } else {
494 + unpatchConsoleForInitialRenderInStrictMode();
495 + }
496 + }
497 + }
498 +
499 // TODO: More meaningful names for "rendererInterfaces" and "renderers".
500 const fiberRoots = {};
501 const rendererInterfaces = new Map();
@@ -502,8 +525,13 @@ export function installHook(target: any): DevToolsHook | null {
525 onCommitFiberUnmount,
526 onCommitFiberRoot,
527 onPostCommitFiberRoot,
528 + setStrictMode,
529 };
530
531 + if (__TEST__) {
532 + hook.dangerous_setTargetConsoleForTesting = dangerous_setTargetConsoleForTesting;
533 + }
534 +
535 Object.defineProperty(
536 target,
537 '__REACT_DEVTOOLS_GLOBAL_HOOK__',
packages/react-devtools-shell/src/app/console.js
+6 -7
@@ -11,12 +11,6 @@ function ignoreStrings(
11 methodName: string,
12 stringsToIgnore: Array<string>,
13 ): void {
14 - // HACKY In the test harness, DevTools overrides the parent window's console.
15 - // Our test app code uses the iframe's console though.
16 - // To simulate a more accurate end-to-end environment,
17 - // the shell's console patching should pass through to the parent override methods.
18 - const originalMethod = window.parent.console[methodName];
19 -
14 console[methodName] = (...args) => {
15 const maybeString = args[0];
16 if (typeof maybeString === 'string') {
@@ -26,7 +20,12 @@ function ignoreStrings(
20 }
21 }
22 }
29 - originalMethod(...args);
23 +
24 + // HACKY In the test harness, DevTools overrides the parent window's console.
25 + // Our test app code uses the iframe's console though.
26 + // To simulate a more accurate end-to-end environment,
27 + // the shell's console patching should pass through to the parent override methods.
28 + window.parent.console[methodName](...args);
29 };
30 }
31
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+2 -5
@@ -149,11 +149,7 @@ import {
149 getOffscreenContainerProps,
150 } from './ReactFiberHostConfig';
151 import type {SuspenseInstance} from './ReactFiberHostConfig';
152 -import {
153 - shouldError,
154 - shouldSuspend,
155 - setIsStrictModeForDevtools,
156 -} from './ReactFiberReconciler';
152 +import {shouldError, shouldSuspend} from './ReactFiberReconciler';
153 import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.new';
154 import {
155 suspenseStackCursor,
@@ -235,6 +231,7 @@ import {createCapturedValue} from './ReactCapturedValue';
231 import {createClassErrorUpdate} from './ReactFiberThrow.new';
232 import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.new';
233 import is from 'shared/objectIs';
234 +import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.new';
235
236 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
237
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+2 -5
@@ -149,11 +149,7 @@ import {
149 getOffscreenContainerProps,
150 } from './ReactFiberHostConfig';
151 import type {SuspenseInstance} from './ReactFiberHostConfig';
152 -import {
153 - shouldError,
154 - shouldSuspend,
155 - setIsStrictModeForDevtools,
156 -} from './ReactFiberReconciler';
152 +import {shouldError, shouldSuspend} from './ReactFiberReconciler';
153 import {pushHostContext, pushHostContainer} from './ReactFiberHostContext.old';
154 import {
155 suspenseStackCursor,
@@ -235,6 +231,7 @@ import {createCapturedValue} from './ReactCapturedValue';
231 import {createClassErrorUpdate} from './ReactFiberThrow.old';
232 import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.old';
233 import is from 'shared/objectIs';
234 +import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.old';
235
236 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
237
packages/react-reconciler/src/ReactFiberClassComponent.new.js
+1 -1
@@ -38,7 +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';
41 +import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.new';
42
43 import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
44 import {
packages/react-reconciler/src/ReactFiberClassComponent.old.js
+1 -1
@@ -38,7 +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';
41 +import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.old';
42
43 import {resolveDefaultProps} from './ReactFiberLazyComponent.old';
44 import {
packages/react-reconciler/src/ReactFiberDevToolsHook.new.js
+42 -1
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
10 +import {
11 + consoleManagedByDevToolsDuringStrictMode,
12 + enableProfilerTimer,
13 +} from 'shared/ReactFeatureFlags';
14
15 import type {Fiber, FiberRoot} from './ReactInternalTypes';
16 import type {ReactNodeList} from 'shared/ReactTypes';
@@ -25,7 +28,11 @@ import {
28 UserBlockingPriority as UserBlockingSchedulerPriority,
29 NormalPriority as NormalSchedulerPriority,
30 IdlePriority as IdleSchedulerPriority,
31 + unstable_yieldValue,
32 + unstable_setDisableYieldValue,
33 } from './Scheduler';
34 +import {setSuppressWarning} from 'shared/consoleWithStackDev';
35 +import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
36
37 declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: Object | void;
38
@@ -171,3 +178,37 @@ export function onCommitUnmount(fiber: Fiber) {
178 }
179 }
180 }
181 +
182 +export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
183 + if (consoleManagedByDevToolsDuringStrictMode) {
184 + if (typeof unstable_yieldValue === 'function') {
185 + // We're in a test because Scheduler.unstable_yieldValue only exists
186 + // in SchedulerMock. To reduce the noise in strict mode tests,
187 + // suppress warnings and disable scheduler yielding during the double render
188 + unstable_setDisableYieldValue(newIsStrictMode);
189 + setSuppressWarning(newIsStrictMode);
190 + }
191 +
192 + if (injectedHook && typeof injectedHook.setStrictMode === 'function') {
193 + try {
194 + injectedHook.setStrictMode(rendererID, newIsStrictMode);
195 + } catch (err) {
196 + if (__DEV__) {
197 + if (!hasLoggedError) {
198 + hasLoggedError = true;
199 + console.error(
200 + 'React instrumentation encountered an error: %s',
201 + err,
202 + );
203 + }
204 + }
205 + }
206 + }
207 + } else {
208 + if (newIsStrictMode) {
209 + disableLogs();
210 + } else {
211 + reenableLogs();
212 + }
213 + }
214 +}
packages/react-reconciler/src/ReactFiberDevToolsHook.old.js
+42 -1
@@ -7,7 +7,10 @@
7 * @flow
8 */
9
10 -import {enableProfilerTimer} from 'shared/ReactFeatureFlags';
10 +import {
11 + consoleManagedByDevToolsDuringStrictMode,
12 + enableProfilerTimer,
13 +} from 'shared/ReactFeatureFlags';
14
15 import type {Fiber, FiberRoot} from './ReactInternalTypes';
16 import type {ReactNodeList} from 'shared/ReactTypes';
@@ -25,7 +28,11 @@ import {
28 UserBlockingPriority as UserBlockingSchedulerPriority,
29 NormalPriority as NormalSchedulerPriority,
30 IdlePriority as IdleSchedulerPriority,
31 + unstable_yieldValue,
32 + unstable_setDisableYieldValue,
33 } from './Scheduler';
34 +import {setSuppressWarning} from 'shared/consoleWithStackDev';
35 +import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
36
37 declare var __REACT_DEVTOOLS_GLOBAL_HOOK__: Object | void;
38
@@ -171,3 +178,37 @@ export function onCommitUnmount(fiber: Fiber) {
178 }
179 }
180 }
181 +
182 +export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
183 + if (consoleManagedByDevToolsDuringStrictMode) {
184 + if (typeof unstable_yieldValue === 'function') {
185 + // We're in a test because Scheduler.unstable_yieldValue only exists
186 + // in SchedulerMock. To reduce the noise in strict mode tests,
187 + // suppress warnings and disable scheduler yielding during the double render
188 + unstable_setDisableYieldValue(newIsStrictMode);
189 + setSuppressWarning(newIsStrictMode);
190 + }
191 +
192 + if (injectedHook && typeof injectedHook.setStrictMode === 'function') {
193 + try {
194 + injectedHook.setStrictMode(rendererID, newIsStrictMode);
195 + } catch (err) {
196 + if (__DEV__) {
197 + if (!hasLoggedError) {
198 + hasLoggedError = true;
199 + console.error(
200 + 'React instrumentation encountered an error: %s',
201 + err,
202 + );
203 + }
204 + }
205 + }
206 + }
207 + } else {
208 + if (newIsStrictMode) {
209 + disableLogs();
210 + } else {
211 + reenableLogs();
212 + }
213 + }
214 +}
packages/react-reconciler/src/ReactFiberReconciler.js
-11
@@ -48,8 +48,6 @@ import {
48 observeVisibleRects as observeVisibleRects_old,
49 runWithPriority as runWithPriority_old,
50 getCurrentUpdatePriority as getCurrentUpdatePriority_old,
51 - getIsStrictModeForDevtools as getIsStrictModeForDevtools_old,
52 - setIsStrictModeForDevtools as setIsStrictModeForDevtools_old,
51 } from './ReactFiberReconciler.old';
52
53 import {
@@ -86,8 +84,6 @@ import {
84 observeVisibleRects as observeVisibleRects_new,
85 runWithPriority as runWithPriority_new,
86 getCurrentUpdatePriority as getCurrentUpdatePriority_new,
89 - getIsStrictModeForDevtools as getIsStrictModeForDevtools_new,
90 - setIsStrictModeForDevtools as setIsStrictModeForDevtools_new,
87 } from './ReactFiberReconciler.new';
88
89 export const createContainer = enableNewReconciler
@@ -189,10 +185,3 @@ export const observeVisibleRects = enableNewReconciler
185 export const runWithPriority = enableNewReconciler
186 ? runWithPriority_new
187 : runWithPriority_old;
192 -
193 -export const getIsStrictModeForDevtools = enableNewReconciler
194 - ? getIsStrictModeForDevtools_new
195 - : getIsStrictModeForDevtools_old;
196 -export const setIsStrictModeForDevtools = enableNewReconciler
197 - ? setIsStrictModeForDevtools_new
198 - : setIsStrictModeForDevtools_old;
packages/react-reconciler/src/ReactFiberReconciler.new.js
+1 -35
@@ -35,10 +35,7 @@ import {
35 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
36 import invariant from 'shared/invariant';
37 import isArray from 'shared/isArray';
38 -import {
39 - enableSchedulingProfiler,
40 - consoleManagedByDevToolsDuringStrictMode,
41 -} from 'shared/ReactFeatureFlags';
38 +import {enableSchedulingProfiler} from 'shared/ReactFeatureFlags';
39 import ReactSharedInternals from 'shared/ReactSharedInternals';
40 import {getPublicInstance} from './ReactFiberHostConfig';
41 import {
@@ -107,10 +104,6 @@ export {
104 observeVisibleRects,
105 } from './ReactTestSelectors';
106
110 -import * as Scheduler from './Scheduler';
111 -import {setSuppressWarning} from 'shared/consoleWithStackDev';
112 -import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
113 -
107 type OpaqueRoot = FiberRoot;
108
109 // 0 is PROD, 1 is DEV.
@@ -464,8 +457,6 @@ export function shouldSuspend(fiber: Fiber): boolean {
457 return shouldSuspendImpl(fiber);
458 }
459
467 -let isStrictMode = false;
468 -
460 let overrideHookState = null;
461 let overrideHookStateDeletePath = null;
462 let overrideHookStateRenamePath = null;
@@ -715,30 +706,6 @@ function getCurrentFiberForDevTools() {
706 return ReactCurrentFiberCurrent;
707 }
708
718 -export function getIsStrictModeForDevtools() {
719 - return isStrictMode;
720 -}
721 -
722 -export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
723 - isStrictMode = newIsStrictMode;
724 -
725 - if (consoleManagedByDevToolsDuringStrictMode) {
726 - // We're in a test because Scheduler.unstable_yieldValue only exists
727 - // in SchedulerMock. To reduce the noise in strict mode tests,
728 - // suppress warnings and disable scheduler yielding during the double render
729 - if (typeof Scheduler.unstable_yieldValue === 'function') {
730 - Scheduler.unstable_setDisableYieldValue(newIsStrictMode);
731 - setSuppressWarning(newIsStrictMode);
732 - }
733 - } else {
734 - if (newIsStrictMode) {
735 - disableLogs();
736 - } else {
737 - reenableLogs();
738 - }
739 - }
740 -}
741 -
709 export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
710 const {findFiberByHostInstance} = devToolsConfig;
711 const {ReactCurrentDispatcher} = ReactSharedInternals;
@@ -768,7 +735,6 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
735 setRefreshHandler: __DEV__ ? setRefreshHandler : null,
736 // Enables DevTools to append owner stacks to error messages in DEV mode.
737 getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null,
771 - getIsStrictMode: __DEV__ ? getIsStrictModeForDevtools : null,
738 // Enables DevTools to detect reconciler version rather than renderer version
739 // which may not match for third party renderers.
740 reconcilerVersion: ReactVersion,
packages/react-reconciler/src/ReactFiberReconciler.old.js
+1 -35
@@ -35,10 +35,7 @@ import {
35 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
36 import invariant from 'shared/invariant';
37 import isArray from 'shared/isArray';
38 -import {
39 - enableSchedulingProfiler,
40 - consoleManagedByDevToolsDuringStrictMode,
41 -} from 'shared/ReactFeatureFlags';
38 +import {enableSchedulingProfiler} from 'shared/ReactFeatureFlags';
39 import ReactSharedInternals from 'shared/ReactSharedInternals';
40 import {getPublicInstance} from './ReactFiberHostConfig';
41 import {
@@ -107,10 +104,6 @@ export {
104 observeVisibleRects,
105 } from './ReactTestSelectors';
106
110 -import * as Scheduler from './Scheduler';
111 -import {setSuppressWarning} from 'shared/consoleWithStackDev';
112 -import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
113 -
107 type OpaqueRoot = FiberRoot;
108
109 // 0 is PROD, 1 is DEV.
@@ -464,8 +457,6 @@ export function shouldSuspend(fiber: Fiber): boolean {
457 return shouldSuspendImpl(fiber);
458 }
459
467 -let isStrictMode = false;
468 -
460 let overrideHookState = null;
461 let overrideHookStateDeletePath = null;
462 let overrideHookStateRenamePath = null;
@@ -715,30 +706,6 @@ function getCurrentFiberForDevTools() {
706 return ReactCurrentFiberCurrent;
707 }
708
718 -export function getIsStrictModeForDevtools() {
719 - return isStrictMode;
720 -}
721 -
722 -export function setIsStrictModeForDevtools(newIsStrictMode: boolean) {
723 - isStrictMode = newIsStrictMode;
724 -
725 - if (consoleManagedByDevToolsDuringStrictMode) {
726 - // We're in a test because Scheduler.unstable_yieldValue only exists
727 - // in SchedulerMock. To reduce the noise in strict mode tests,
728 - // suppress warnings and disable scheduler yielding during the double render
729 - if (typeof Scheduler.unstable_yieldValue === 'function') {
730 - Scheduler.unstable_setDisableYieldValue(newIsStrictMode);
731 - setSuppressWarning(newIsStrictMode);
732 - }
733 - } else {
734 - if (newIsStrictMode) {
735 - disableLogs();
736 - } else {
737 - reenableLogs();
738 - }
739 - }
740 -}
741 -
709 export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
710 const {findFiberByHostInstance} = devToolsConfig;
711 const {ReactCurrentDispatcher} = ReactSharedInternals;
@@ -768,7 +735,6 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
735 setRefreshHandler: __DEV__ ? setRefreshHandler : null,
736 // Enables DevTools to append owner stacks to error messages in DEV mode.
737 getCurrentFiber: __DEV__ ? getCurrentFiberForDevTools : null,
771 - getIsStrictMode: __DEV__ ? getIsStrictModeForDevtools : null,
738 // Enables DevTools to detect reconciler version rather than renderer version
739 // which may not match for third party renderers.
740 reconcilerVersion: ReactVersion,
packages/react-reconciler/src/ReactUpdateQueue.new.js
+1 -1
@@ -110,7 +110,7 @@ import {
110 isInterleavedUpdate,
111 } from './ReactFiberWorkLoop.new';
112 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
113 -import {setIsStrictModeForDevtools} from './ReactFiberReconciler';
113 +import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.new';
114
115 import invariant from 'shared/invariant';
116
packages/react-reconciler/src/ReactUpdateQueue.old.js
+1 -1
@@ -110,7 +110,7 @@ import {
110 isInterleavedUpdate,
111 } from './ReactFiberWorkLoop.old';
112 import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
113 -import {setIsStrictModeForDevtools} from './ReactFiberReconciler';
113 +import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.old';
114
115 import invariant from 'shared/invariant';
116