@samitouri / QOS-React-2 / commits / cb3fb42129

Patch console to append component stacks (#348)

* Patch console.warn and console.error to auto-append owners-only component stacks. This setting is enabled by default and will work for React Native even if no front-end DevTools shell is being used. The setting can be disabled via a new, persisted user preference though.

Brian Vaughn committed Jul 17, 2019 at 11:12 UTC cb3fb4212972ad514ff53bff96530a0dffcaabcf
24 files changed +1083 -255
flow-typed/npm/react-test-renderer_v16.x.x.js
+1 -1
@@ -66,7 +66,7 @@ declare module 'react-test-renderer' {
66 options?: TestRendererOptions
67 ): ReactTestRenderer;
68
69 - declare function act(callback: () => void): Thenable;
69 + declare function act(callback: () => ?Thenable): Thenable;
70 }
71
72 declare module 'react-test-renderer/shallow' {
package.json
+5 -5
@@ -138,18 +138,18 @@
138 "opener": "^1.5.1",
139 "prettier": "^1.16.4",
140 "prop-types": "^15.6.2",
141 - "react": "^0.0.0-50b50c26f",
141 + "react": "^0.0.0-424099da6",
142 "react-15": "npm:react@^15",
143 "react-color": "^2.11.7",
144 - "react-dom": "^0.0.0-50b50c26f",
144 + "react-dom": "^0.0.0-424099da6",
145 "react-dom-15": "npm:react-dom@^15",
146 - "react-is": "^0.0.0-50b50c26f",
147 - "react-test-renderer": "^0.0.0-50b50c26f",
146 + "react-is": "0.0.0-424099da6",
147 + "react-test-renderer": "^0.0.0-424099da6",
148 "react-virtualized-auto-sizer": "^1.0.2",
149 "react-window": "./vendor/react-window",
150 "request-promise": "^4.2.4",
151 "rimraf": "^2.6.3",
152 - "scheduler": "^0.0.0-50b50c26f",
152 + "scheduler": "^0.0.0-424099da6",
153 "semver": "^5.5.1",
154 "serve-static": "^1.14.1",
155 "style-loader": "^0.23.1",
packages/react-devtools-core/src/standalone.js
+9 -5
@@ -9,7 +9,7 @@ import {
9 } from 'react-dom';
10 import Bridge from 'src/bridge';
11 import Store from 'src/devtools/store';
12 -import { getSavedComponentFilters } from 'src/utils';
12 +import { getSavedComponentFilters, getAppendComponentStack } from 'src/utils';
13 import { Server } from 'ws';
14 import { existsSync, readFileSync } from 'fs';
15 import { installHook } from 'src/hook';
@@ -241,12 +241,16 @@ function startServer(port?: number = 8097) {
241 // because they are generally stored in localStorage within the context of the extension.
242 // Because of this it relies on the extension to pass filters, so include them wth the response here.
243 // This will ensure that saved filters are shared across different web pages.
244 - const savedFiltersString = `window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
245 - getSavedComponentFilters()
246 - )};`;
244 + const savedPreferencesString = `
245 + window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
246 + getSavedComponentFilters()
247 + )};
248 + window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
249 + getAppendComponentStack()
250 + )};`;
251
252 response.end(
249 - savedFiltersString +
253 + savedPreferencesString +
254 '\n;' +
255 backendFile.toString() +
256 '\n;' +
shells/browser/shared/src/main.js
+12 -5
@@ -6,7 +6,7 @@ import Bridge from 'src/bridge';
6 import Store from 'src/devtools/store';
7 import inject from './inject';
8 import { createViewElementSource, getBrowserTheme } from './utils';
9 -import { getSavedComponentFilters } from 'src/utils';
9 +import { getSavedComponentFilters, getAppendComponentStack } from 'src/utils';
10 import {
11 localStorageGetItem,
12 localStorageRemoveItem,
@@ -22,16 +22,23 @@ let panelCreated = false;
22 // The renderer interface can't read saved component filters directly,
23 // because they are stored in localStorage within the context of the extension.
24 // Instead it relies on the extension to pass filters through.
25 -function initializeSavedComponentFilters() {
25 +function syncSavedPreferences() {
26 const componentFilters = getSavedComponentFilters();
27 chrome.devtools.inspectedWindow.eval(
28 `window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
29 componentFilters
30 )};`
31 );
32 +
33 + const appendComponentStack = getAppendComponentStack();
34 + chrome.devtools.inspectedWindow.eval(
35 + `window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
36 + appendComponentStack
37 + )};`
38 + );
39 }
40
34 -initializeSavedComponentFilters();
41 +syncSavedPreferences();
42
43 function createPanelIfReactLoaded() {
44 if (panelCreated) {
@@ -286,7 +293,7 @@ function createPanelIfReactLoaded() {
293 chrome.devtools.network.onNavigated.addListener(function onNavigated() {
294 // Re-initialize saved filters on navigation,
295 // since global values stored on window get reset in this case.
289 - initializeSavedComponentFilters();
296 + syncSavedPreferences();
297
298 // It's easiest to recreate the DevTools panel (to clean up potential stale state).
299 // We can revisit this in the future as a small optimization.
@@ -302,7 +309,7 @@ function createPanelIfReactLoaded() {
309
310 // Load (or reload) the DevTools extension when the user navigates to a new page.
311 function checkPageForReact() {
305 - initializeSavedComponentFilters();
312 + syncSavedPreferences();
313 createPanelIfReactLoaded();
314 }
315
shells/dev/src/devtools.js
+2 -1
@@ -8,7 +8,7 @@ import { installHook } from 'src/hook';
8 import { initDevTools } from 'src/devtools';
9 import Store from 'src/devtools/store';
10 import DevTools from 'src/devtools/views/DevTools';
11 -import { getSavedComponentFilters } from 'src/utils';
11 +import { getSavedComponentFilters, getAppendComponentStack } from 'src/utils';
12
13 const iframe = ((document.getElementById('target'): any): HTMLIFrameElement);
14
@@ -18,6 +18,7 @@ const { contentDocument, contentWindow } = iframe;
18 // because they are stored in localStorage within the context of the extension.
19 // Instead it relies on the extension to pass filters through.
20 contentWindow.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = getSavedComponentFilters();
21 +contentWindow.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = getAppendComponentStack();
22
23 installHook(contentWindow);
24
src/__tests__/console-test.js new
+349
@@ -0,0 +1,349 @@
1 +// @flow
2 +
3 +describe('console', () => {
4 + let React;
5 + let ReactDOM;
6 + let act;
7 + let enableConsole;
8 + let disableConsole;
9 + let fakeConsole;
10 + let mockError;
11 + let mockLog;
12 + let mockWarn;
13 + let patchConsole;
14 + let unpatchConsole;
15 +
16 + beforeEach(() => {
17 + const Console = require('../backend/console');
18 + enableConsole = Console.enable;
19 + disableConsole = Console.disable;
20 + patchConsole = Console.patch;
21 + unpatchConsole = Console.unpatch;
22 +
23 + const inject = global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject;
24 + global.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = internals => {
25 + inject(internals);
26 +
27 + Console.registerRenderer(internals);
28 + };
29 +
30 + React = require('react');
31 + ReactDOM = require('react-dom');
32 +
33 + const utils = require('./utils');
34 + act = utils.act;
35 +
36 + // Patch a fake console so we can verify with tests below.
37 + // Patching the real console is too complicated,
38 + // because Jest itself has hooks into it as does our test env setup.
39 + mockError = jest.fn();
40 + mockLog = jest.fn();
41 + mockWarn = jest.fn();
42 + fakeConsole = {
43 + error: mockError,
44 + log: mockLog,
45 + warn: mockWarn,
46 + };
47 +
48 + patchConsole(fakeConsole);
49 + });
50 +
51 + function normalizeCodeLocInfo(str) {
52 + return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
53 + }
54 +
55 + it('should only patch the console once', () => {
56 + const { error, warn } = fakeConsole;
57 +
58 + patchConsole(fakeConsole);
59 +
60 + expect(fakeConsole.error).toBe(error);
61 + expect(fakeConsole.warn).toBe(warn);
62 + });
63 +
64 + it('should un-patch when requested', () => {
65 + expect(fakeConsole.error).not.toBe(mockError);
66 + expect(fakeConsole.warn).not.toBe(mockWarn);
67 +
68 + unpatchConsole();
69 +
70 + expect(fakeConsole.error).toBe(mockError);
71 + expect(fakeConsole.warn).toBe(mockWarn);
72 + });
73 +
74 + it('should pass through logs when there is no current fiber', () => {
75 + expect(mockLog).toHaveBeenCalledTimes(0);
76 + expect(mockWarn).toHaveBeenCalledTimes(0);
77 + expect(mockError).toHaveBeenCalledTimes(0);
78 + fakeConsole.log('log');
79 + fakeConsole.warn('warn');
80 + fakeConsole.error('error');
81 + expect(mockLog).toHaveBeenCalledTimes(1);
82 + expect(mockLog.mock.calls[0]).toHaveLength(1);
83 + expect(mockLog.mock.calls[0][0]).toBe('log');
84 + expect(mockWarn).toHaveBeenCalledTimes(1);
85 + expect(mockWarn.mock.calls[0]).toHaveLength(1);
86 + expect(mockWarn.mock.calls[0][0]).toBe('warn');
87 + expect(mockError).toHaveBeenCalledTimes(1);
88 + expect(mockError.mock.calls[0]).toHaveLength(1);
89 + expect(mockError.mock.calls[0][0]).toBe('error');
90 + });
91 +
92 + it('should suppress console logging when disabled', () => {
93 + disableConsole();
94 + fakeConsole.log('log');
95 + fakeConsole.warn('warn');
96 + fakeConsole.error('error');
97 + expect(mockLog).toHaveBeenCalledTimes(0);
98 + expect(mockWarn).toHaveBeenCalledTimes(0);
99 + expect(mockError).toHaveBeenCalledTimes(0);
100 +
101 + enableConsole();
102 + fakeConsole.log('log');
103 + fakeConsole.warn('warn');
104 + fakeConsole.error('error');
105 + expect(mockLog).toHaveBeenCalledTimes(1);
106 + expect(mockLog.mock.calls[0]).toHaveLength(1);
107 + expect(mockLog.mock.calls[0][0]).toBe('log');
108 + expect(mockWarn).toHaveBeenCalledTimes(1);
109 + expect(mockWarn.mock.calls[0]).toHaveLength(1);
110 + expect(mockWarn.mock.calls[0][0]).toBe('warn');
111 + expect(mockError).toHaveBeenCalledTimes(1);
112 + expect(mockError.mock.calls[0]).toHaveLength(1);
113 + expect(mockError.mock.calls[0][0]).toBe('error');
114 + });
115 +
116 + it('should not append multiple stacks', () => {
117 + const Child = () => {
118 + fakeConsole.warn('warn\n in Child (at fake.js:123)');
119 + fakeConsole.error('error', '\n in Child (at fake.js:123)');
120 + return null;
121 + };
122 +
123 + act(() => ReactDOM.render(<Child />, document.createElement('div')));
124 +
125 + expect(mockWarn).toHaveBeenCalledTimes(1);
126 + expect(mockWarn.mock.calls[0]).toHaveLength(1);
127 + expect(mockWarn.mock.calls[0][0]).toBe(
128 + 'warn\n in Child (at fake.js:123)'
129 + );
130 + expect(mockError).toHaveBeenCalledTimes(1);
131 + expect(mockError.mock.calls[0]).toHaveLength(2);
132 + expect(mockError.mock.calls[0][0]).toBe('error');
133 + expect(mockError.mock.calls[0][1]).toBe('\n in Child (at fake.js:123)');
134 + });
135 +
136 + it('should append component stacks to errors and warnings logged during render', () => {
137 + const Intermediate = ({ children }) => children;
138 + const Parent = () => (
139 + <Intermediate>
140 + <Child />
141 + </Intermediate>
142 + );
143 + const Child = () => {
144 + fakeConsole.error('error');
145 + fakeConsole.log('log');
146 + fakeConsole.warn('warn');
147 + return null;
148 + };
149 +
150 + act(() => ReactDOM.render(<Parent />, document.createElement('div')));
151 +
152 + expect(mockLog).toHaveBeenCalledTimes(1);
153 + expect(mockLog.mock.calls[0]).toHaveLength(1);
154 + expect(mockLog.mock.calls[0][0]).toBe('log');
155 + expect(mockWarn).toHaveBeenCalledTimes(1);
156 + expect(mockWarn.mock.calls[0]).toHaveLength(2);
157 + expect(mockWarn.mock.calls[0][0]).toBe('warn');
158 + expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
159 + '\n in Child (at **)\n in Parent (at **)'
160 + );
161 + expect(mockError).toHaveBeenCalledTimes(1);
162 + expect(mockError.mock.calls[0]).toHaveLength(2);
163 + expect(mockError.mock.calls[0][0]).toBe('error');
164 + expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
165 + '\n in Child (at **)\n in Parent (at **)'
166 + );
167 + });
168 +
169 + it('should append component stacks to errors and warnings logged from effects', () => {
170 + const Intermediate = ({ children }) => children;
171 + const Parent = () => (
172 + <Intermediate>
173 + <Child />
174 + </Intermediate>
175 + );
176 + const Child = () => {
177 + React.useLayoutEffect(() => {
178 + fakeConsole.error('active error');
179 + fakeConsole.log('active log');
180 + fakeConsole.warn('active warn');
181 + });
182 + React.useEffect(() => {
183 + fakeConsole.error('passive error');
184 + fakeConsole.log('passive log');
185 + fakeConsole.warn('passive warn');
186 + });
187 + return null;
188 + };
189 +
190 + act(() => ReactDOM.render(<Parent />, document.createElement('div')));
191 +
192 + expect(mockLog).toHaveBeenCalledTimes(2);
193 + expect(mockLog.mock.calls[0]).toHaveLength(1);
194 + expect(mockLog.mock.calls[0][0]).toBe('active log');
195 + expect(mockLog.mock.calls[1]).toHaveLength(1);
196 + expect(mockLog.mock.calls[1][0]).toBe('passive log');
197 + expect(mockWarn).toHaveBeenCalledTimes(2);
198 + expect(mockWarn.mock.calls[0]).toHaveLength(2);
199 + expect(mockWarn.mock.calls[0][0]).toBe('active warn');
200 + expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
201 + '\n in Child (at **)\n in Parent (at **)'
202 + );
203 + expect(mockWarn.mock.calls[1]).toHaveLength(2);
204 + expect(mockWarn.mock.calls[1][0]).toBe('passive warn');
205 + expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
206 + '\n in Child (at **)\n in Parent (at **)'
207 + );
208 + expect(mockError).toHaveBeenCalledTimes(2);
209 + expect(mockError.mock.calls[0]).toHaveLength(2);
210 + expect(mockError.mock.calls[0][0]).toBe('active error');
211 + expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
212 + '\n in Child (at **)\n in Parent (at **)'
213 + );
214 + expect(mockError.mock.calls[1]).toHaveLength(2);
215 + expect(mockError.mock.calls[1][0]).toBe('passive error');
216 + expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
217 + '\n in Child (at **)\n in Parent (at **)'
218 + );
219 + });
220 +
221 + it('should append component stacks to errors and warnings logged from commit hooks', () => {
222 + const Intermediate = ({ children }) => children;
223 + const Parent = () => (
224 + <Intermediate>
225 + <Child />
226 + </Intermediate>
227 + );
228 + class Child extends React.Component<any> {
229 + componentDidMount() {
230 + fakeConsole.error('didMount error');
231 + fakeConsole.log('didMount log');
232 + fakeConsole.warn('didMount warn');
233 + }
234 + componentDidUpdate() {
235 + fakeConsole.error('didUpdate error');
236 + fakeConsole.log('didUpdate log');
237 + fakeConsole.warn('didUpdate warn');
238 + }
239 + render() {
240 + return null;
241 + }
242 + }
243 +
244 + const container = document.createElement('div');
245 + act(() => ReactDOM.render(<Parent />, container));
246 + act(() => ReactDOM.render(<Parent />, container));
247 +
248 + expect(mockLog).toHaveBeenCalledTimes(2);
249 + expect(mockLog.mock.calls[0]).toHaveLength(1);
250 + expect(mockLog.mock.calls[0][0]).toBe('didMount log');
251 + expect(mockLog.mock.calls[1]).toHaveLength(1);
252 + expect(mockLog.mock.calls[1][0]).toBe('didUpdate log');
253 + expect(mockWarn).toHaveBeenCalledTimes(2);
254 + expect(mockWarn.mock.calls[0]).toHaveLength(2);
255 + expect(mockWarn.mock.calls[0][0]).toBe('didMount warn');
256 + expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
257 + '\n in Child (at **)\n in Parent (at **)'
258 + );
259 + expect(mockWarn.mock.calls[1]).toHaveLength(2);
260 + expect(mockWarn.mock.calls[1][0]).toBe('didUpdate warn');
261 + expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
262 + '\n in Child (at **)\n in Parent (at **)'
263 + );
264 + expect(mockError).toHaveBeenCalledTimes(2);
265 + expect(mockError.mock.calls[0]).toHaveLength(2);
266 + expect(mockError.mock.calls[0][0]).toBe('didMount error');
267 + expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
268 + '\n in Child (at **)\n in Parent (at **)'
269 + );
270 + expect(mockError.mock.calls[1]).toHaveLength(2);
271 + expect(mockError.mock.calls[1][0]).toBe('didUpdate error');
272 + expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
273 + '\n in Child (at **)\n in Parent (at **)'
274 + );
275 + });
276 +
277 + it('should append component stacks to errors and warnings logged from gDSFP', () => {
278 + const Intermediate = ({ children }) => children;
279 + const Parent = () => (
280 + <Intermediate>
281 + <Child />
282 + </Intermediate>
283 + );
284 + class Child extends React.Component<any, any> {
285 + state = {};
286 + static getDerivedStateFromProps() {
287 + fakeConsole.error('error');
288 + fakeConsole.log('log');
289 + fakeConsole.warn('warn');
290 + return null;
291 + }
292 + render() {
293 + return null;
294 + }
295 + }
296 +
297 + act(() => ReactDOM.render(<Parent />, document.createElement('div')));
298 +
299 + expect(mockLog).toHaveBeenCalledTimes(1);
300 + expect(mockLog.mock.calls[0]).toHaveLength(1);
301 + expect(mockLog.mock.calls[0][0]).toBe('log');
302 + expect(mockWarn).toHaveBeenCalledTimes(1);
303 + expect(mockWarn.mock.calls[0]).toHaveLength(2);
304 + expect(mockWarn.mock.calls[0][0]).toBe('warn');
305 + expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
306 + '\n in Child (at **)\n in Parent (at **)'
307 + );
308 + expect(mockError).toHaveBeenCalledTimes(1);
309 + expect(mockError.mock.calls[0]).toHaveLength(2);
310 + expect(mockError.mock.calls[0][0]).toBe('error');
311 + expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toBe(
312 + '\n in Child (at **)\n in Parent (at **)'
313 + );
314 + });
315 +
316 + it('should append stacks after being uninstalled and reinstalled', () => {
317 + const Child = () => {
318 + fakeConsole.warn('warn');
319 + fakeConsole.error('error');
320 + return null;
321 + };
322 +
323 + unpatchConsole();
324 + act(() => ReactDOM.render(<Child />, document.createElement('div')));
325 +
326 + expect(mockWarn).toHaveBeenCalledTimes(1);
327 + expect(mockWarn.mock.calls[0]).toHaveLength(1);
328 + expect(mockWarn.mock.calls[0][0]).toBe('warn');
329 + expect(mockError).toHaveBeenCalledTimes(1);
330 + expect(mockError.mock.calls[0]).toHaveLength(1);
331 + expect(mockError.mock.calls[0][0]).toBe('error');
332 +
333 + patchConsole(fakeConsole);
334 + act(() => ReactDOM.render(<Child />, document.createElement('div')));
335 +
336 + expect(mockWarn).toHaveBeenCalledTimes(2);
337 + expect(mockWarn.mock.calls[1]).toHaveLength(2);
338 + expect(mockWarn.mock.calls[1][0]).toBe('warn');
339 + expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][1])).toEqual(
340 + '\n in Child (at **)'
341 + );
342 + expect(mockError).toHaveBeenCalledTimes(2);
343 + expect(mockError.mock.calls[1]).toHaveLength(2);
344 + expect(mockError.mock.calls[1][0]).toBe('error');
345 + expect(normalizeCodeLocInfo(mockError.mock.calls[1][1])).toBe(
346 + '\n in Child (at **)'
347 + );
348 + });
349 +});
src/__tests__/inspectedElementContext-test.js
+58 -43
@@ -530,16 +530,20 @@ describe('InspectedElementContext', () => {
530
531 inspectedElement = null;
532 TestUtils.act(() => {
533 - getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
534 - jest.runOnlyPendingTimers();
533 + TestRenderer.act(() => {
534 + getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
535 + jest.runOnlyPendingTimers();
536 + });
537 });
538 expect(inspectedElement).not.toBeNull();
539 expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
540
541 inspectedElement = null;
542 TestUtils.act(() => {
541 - getInspectedElementPath(id, ['props', 'nestedObject', 'a', 'b', 'c']);
542 - jest.runOnlyPendingTimers();
543 + TestRenderer.act(() => {
544 + getInspectedElementPath(id, ['props', 'nestedObject', 'a', 'b', 'c']);
545 + jest.runOnlyPendingTimers();
546 + });
547 });
548 expect(inspectedElement).not.toBeNull();
549 expect(inspectedElement).toMatchSnapshot(
@@ -548,16 +552,18 @@ describe('InspectedElementContext', () => {
552
553 inspectedElement = null;
554 TestUtils.act(() => {
551 - getInspectedElementPath(id, [
552 - 'props',
553 - 'nestedObject',
554 - 'a',
555 - 'b',
556 - 'c',
557 - 0,
558 - 'd',
559 - ]);
560 - jest.runOnlyPendingTimers();
555 + TestRenderer.act(() => {
556 + getInspectedElementPath(id, [
557 + 'props',
558 + 'nestedObject',
559 + 'a',
560 + 'b',
561 + 'c',
562 + 0,
563 + 'd',
564 + ]);
565 + jest.runOnlyPendingTimers();
566 + });
567 });
568 expect(inspectedElement).not.toBeNull();
569 expect(inspectedElement).toMatchSnapshot(
@@ -566,16 +572,20 @@ describe('InspectedElementContext', () => {
572
573 inspectedElement = null;
574 TestUtils.act(() => {
569 - getInspectedElementPath(id, ['hooks', 0, 'value']);
570 - jest.runOnlyPendingTimers();
575 + TestRenderer.act(() => {
576 + getInspectedElementPath(id, ['hooks', 0, 'value']);
577 + jest.runOnlyPendingTimers();
578 + });
579 });
580 expect(inspectedElement).not.toBeNull();
581 expect(inspectedElement).toMatchSnapshot('5: Inspect hooks.0.value');
582
583 inspectedElement = null;
584 TestUtils.act(() => {
577 - getInspectedElementPath(id, ['hooks', 0, 'value', 'foo', 'bar']);
578 - jest.runOnlyPendingTimers();
585 + TestRenderer.act(() => {
586 + getInspectedElementPath(id, ['hooks', 0, 'value', 'foo', 'bar']);
587 + jest.runOnlyPendingTimers();
588 + });
589 });
590 expect(inspectedElement).not.toBeNull();
591 expect(inspectedElement).toMatchSnapshot(
@@ -645,7 +655,7 @@ describe('InspectedElementContext', () => {
655 expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
656
657 inspectedElement = null;
648 - TestUtils.act(() => {
658 + TestRenderer.act(() => {
659 getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
660 jest.runOnlyPendingTimers();
661 });
@@ -653,44 +663,46 @@ describe('InspectedElementContext', () => {
663 expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
664
665 inspectedElement = null;
656 - TestUtils.act(() => {
666 + TestRenderer.act(() => {
667 getInspectedElementPath(id, ['props', 'nestedObject', 'c']);
668 jest.runOnlyPendingTimers();
669 });
670 expect(inspectedElement).not.toBeNull();
671 expect(inspectedElement).toMatchSnapshot('3: Inspect props.nestedObject.c');
672
663 - TestUtils.act(() => {
664 - ReactDOM.render(
665 - <Example
666 - nestedObject={{
667 - a: {
668 - value: 2,
669 - b: {
673 + TestRenderer.act(() => {
674 + TestUtils.act(() => {
675 + ReactDOM.render(
676 + <Example
677 + nestedObject={{
678 + a: {
679 value: 2,
680 + b: {
681 + value: 2,
682 + },
683 },
672 - },
673 - c: {
674 - value: 2,
675 - d: {
684 + c: {
685 value: 2,
677 - e: {
686 + d: {
687 value: 2,
688 + e: {
689 + value: 2,
690 + },
691 },
692 },
681 - },
682 - }}
683 - />,
684 - container
685 - );
693 + }}
694 + />,
695 + container
696 + );
697 + });
698 });
699
688 - TestUtils.act(() => {
700 + TestRenderer.act(() => {
701 inspectedElement = null;
702 jest.advanceTimersByTime(1000);
691 - expect(inspectedElement).not.toBeNull();
692 - expect(inspectedElement).toMatchSnapshot('4: update inspected element');
703 });
704 + expect(inspectedElement).not.toBeNull();
705 + expect(inspectedElement).toMatchSnapshot('4: update inspected element');
706
707 done();
708 });
@@ -764,9 +776,12 @@ describe('InspectedElementContext', () => {
776 });
777
778 inspectedElement = null;
767 - TestUtils.act(() => {
768 - getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
769 - jest.runOnlyPendingTimers();
779 +
780 + TestRenderer.act(() => {
781 + TestUtils.act(() => {
782 + getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
783 + jest.runOnlyPendingTimers();
784 + });
785 });
786 expect(inspectedElement).not.toBeNull();
787 expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
src/__tests__/profilingCache-test.js
+14 -14
@@ -34,7 +34,7 @@ describe('ProfilingCache', () => {
34
35 it('should collect data for each root (including ones added or mounted after profiling started)', () => {
36 const Parent = ({ count }) => {
37 - Scheduler.advanceTime(10);
37 + Scheduler.unstable_advanceTime(10);
38 const children = new Array(count)
39 .fill(true)
40 .map((_, index) => <Child key={index} duration={index} />);
@@ -46,7 +46,7 @@ describe('ProfilingCache', () => {
46 );
47 };
48 const Child = ({ duration }) => {
49 - Scheduler.advanceTime(duration);
49 + Scheduler.unstable_advanceTime(duration);
50 return null;
51 };
52 const MemoizedChild = React.memo(Child);
@@ -118,7 +118,7 @@ describe('ProfilingCache', () => {
118
119 it('should collect data for each commit', () => {
120 const Parent = ({ count }) => {
121 - Scheduler.advanceTime(10);
121 + Scheduler.unstable_advanceTime(10);
122 const children = new Array(count)
123 .fill(true)
124 .map((_, index) => <Child key={index} duration={index} />);
@@ -130,7 +130,7 @@ describe('ProfilingCache', () => {
130 );
131 };
132 const Child = ({ duration }) => {
133 - Scheduler.advanceTime(duration);
133 + Scheduler.unstable_advanceTime(duration);
134 return null;
135 };
136 const MemoizedChild = React.memo(Child);
@@ -305,7 +305,7 @@ describe('ProfilingCache', () => {
305 store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];
306
307 const Grandparent = () => {
308 - Scheduler.advanceTime(10);
308 + Scheduler.unstable_advanceTime(10);
309 return (
310 <React.Fragment>
311 <Parent key="one" />
@@ -314,11 +314,11 @@ describe('ProfilingCache', () => {
314 );
315 };
316 const Parent = () => {
317 - Scheduler.advanceTime(2);
317 + Scheduler.unstable_advanceTime(2);
318 return <Child />;
319 };
320 const Child = () => {
321 - Scheduler.advanceTime(1);
321 + Scheduler.unstable_advanceTime(1);
322 return null;
323 };
324
@@ -361,7 +361,7 @@ describe('ProfilingCache', () => {
361 };
362
363 const Parent = () => {
364 - Scheduler.advanceTime(10);
364 + Scheduler.unstable_advanceTime(10);
365 return (
366 <React.Suspense fallback={<Fallback />}>
367 <Async />
@@ -369,11 +369,11 @@ describe('ProfilingCache', () => {
369 );
370 };
371 const Fallback = () => {
372 - Scheduler.advanceTime(2);
372 + Scheduler.unstable_advanceTime(2);
373 return 'Fallback...';
374 };
375 const Async = () => {
376 - Scheduler.advanceTime(3);
376 + Scheduler.unstable_advanceTime(3);
377 const data = getData();
378 return data;
379 };
@@ -412,7 +412,7 @@ describe('ProfilingCache', () => {
412
413 it('should collect data for each rendered fiber', () => {
414 const Parent = ({ count }) => {
415 - Scheduler.advanceTime(10);
415 + Scheduler.unstable_advanceTime(10);
416 const children = new Array(count)
417 .fill(true)
418 .map((_, index) => <Child key={index} duration={index} />);
@@ -424,7 +424,7 @@ describe('ProfilingCache', () => {
424 );
425 };
426 const Child = ({ duration }) => {
427 - Scheduler.advanceTime(duration);
427 + Scheduler.unstable_advanceTime(duration);
428 return null;
429 };
430 const MemoizedChild = React.memo(Child);
@@ -496,7 +496,7 @@ describe('ProfilingCache', () => {
496
497 it('should report every traced interaction', () => {
498 const Parent = ({ count }) => {
499 - Scheduler.advanceTime(10);
499 + Scheduler.unstable_advanceTime(10);
500 const children = new Array(count)
501 .fill(true)
502 .map((_, index) => <Child key={index} duration={index} />);
@@ -508,7 +508,7 @@ describe('ProfilingCache', () => {
508 );
509 };
510 const Child = ({ duration }) => {
511 - Scheduler.advanceTime(duration);
511 + Scheduler.unstable_advanceTime(duration);
512 return null;
513 };
514 const MemoizedChild = React.memo(Child);
src/__tests__/profilingCharts-test.js
+6 -6
@@ -30,7 +30,7 @@ describe('profiling charts', () => {
30 describe('flamegraph chart', () => {
31 it('should contain valid data', () => {
32 const Parent = ({ count }) => {
33 - Scheduler.advanceTime(10);
33 + Scheduler.unstable_advanceTime(10);
34 return (
35 <React.Fragment>
36 <Child key="first" duration={3} />
@@ -42,7 +42,7 @@ describe('profiling charts', () => {
42
43 // Memoize children to verify that chart doesn't include in the update.
44 const Child = React.memo(function Child({ duration }) {
45 - Scheduler.advanceTime(duration);
45 + Scheduler.unstable_advanceTime(duration);
46 return null;
47 });
48
@@ -106,7 +106,7 @@ describe('profiling charts', () => {
106 describe('ranked chart', () => {
107 it('should contain valid data', () => {
108 const Parent = ({ count }) => {
109 - Scheduler.advanceTime(10);
109 + Scheduler.unstable_advanceTime(10);
110 return (
111 <React.Fragment>
112 <Child key="first" duration={3} />
@@ -118,7 +118,7 @@ describe('profiling charts', () => {
118
119 // Memoize children to verify that chart doesn't include in the update.
120 const Child = React.memo(function Child({ duration }) {
121 - Scheduler.advanceTime(duration);
121 + Scheduler.unstable_advanceTime(duration);
122 return null;
123 });
124
@@ -178,7 +178,7 @@ describe('profiling charts', () => {
178 describe('interactions', () => {
179 it('should contain valid data', () => {
180 const Parent = ({ count }) => {
181 - Scheduler.advanceTime(10);
181 + Scheduler.unstable_advanceTime(10);
182 return (
183 <React.Fragment>
184 <Child key="first" duration={3} />
@@ -190,7 +190,7 @@ describe('profiling charts', () => {
190
191 // Memoize children to verify that chart doesn't include in the update.
192 const Child = React.memo(function Child({ duration }) {
193 - Scheduler.advanceTime(duration);
193 + Scheduler.unstable_advanceTime(duration);
194 return null;
195 });
196
src/__tests__/profilingCommitTreeBuilder-test.js
+2 -2
@@ -27,13 +27,13 @@ describe('commit tree', () => {
27
28 it('should be able to rebuild the store tree for each commit', () => {
29 const Parent = ({ count }) => {
30 - Scheduler.advanceTime(10);
30 + Scheduler.unstable_advanceTime(10);
31 return new Array(count)
32 .fill(true)
33 .map((_, index) => <Child key={index} />);
34 };
35 const Child = React.memo(function Child() {
36 - Scheduler.advanceTime(2);
36 + Scheduler.unstable_advanceTime(2);
37 return null;
38 });
39
src/__tests__/utils.js
+35 -15
@@ -8,14 +8,20 @@ import type { ProfilingDataFrontend } from 'src/devtools/views/Profiler/types';
8 import type { ElementType } from 'src/types';
9
10 export function act(callback: Function): void {
11 - const TestUtils = require('react-dom/test-utils');
12 - TestUtils.act(() => {
13 - callback();
11 + const { act: actTestRenderer } = require('react-test-renderer');
12 + const { act: actDOM } = require('react-dom/test-utils');
13 +
14 + actDOM(() => {
15 + actTestRenderer(() => {
16 + callback();
17 + });
18 });
19
20 // Flush Bridge operations
17 - TestUtils.act(() => {
18 - jest.runAllTimers();
21 + actDOM(() => {
22 + actTestRenderer(() => {
23 + jest.runAllTimers();
24 + });
25 });
26 }
27
@@ -23,24 +29,31 @@ export async function actAsync(
29 cb: () => *,
30 recursivelyFlush: boolean = true
31 ): Promise<void> {
26 - const TestUtils = require('react-dom/test-utils');
32 + const { act: actTestRenderer } = require('react-test-renderer');
33 + const { act: actDOM } = require('react-dom/test-utils');
34
35 // $FlowFixMe Flow doens't know about "await act()" yet
29 - await TestUtils.act(async () => {
30 - await cb();
36 + await actDOM(async () => {
37 + await actTestRenderer(async () => {
38 + await cb();
39 + });
40 });
41
42 if (recursivelyFlush) {
43 while (jest.getTimerCount() > 0) {
44 // $FlowFixMe Flow doens't know about "await act()" yet
36 - await TestUtils.act(async () => {
37 - jest.runAllTimers();
45 + await actDOM(async () => {
46 + await actTestRenderer(async () => {
47 + jest.runAllTimers();
48 + });
49 });
50 }
51 } else {
52 // $FlowFixMe Flow doesn't know about "await act()" yet
42 - await TestUtils.act(async () => {
43 - jest.runOnlyPendingTimers();
53 + await actDOM(async () => {
54 + await actTestRenderer(async () => {
55 + jest.runOnlyPendingTimers();
56 + });
57 });
58 }
59 }
@@ -122,10 +135,17 @@ export function getRendererID(): number {
135 throw Error('Agent unavailable.');
136 }
137 const ids = Object.keys(global.agent._rendererInterfaces);
125 - if (ids.length !== 1) {
126 - throw Error('Multiple renderers attached.');
138 +
139 + const id = ids.find(id => {
140 + const rendererInterface = global.agent._rendererInterfaces[id];
141 + return rendererInterface.renderer.rendererPackageName === 'react-dom';
142 + });
143 +
144 + if (ids == null) {
145 + throw Error('Could not find renderer.');
146 }
128 - return parseInt(ids[0], 10);
147 +
148 + return parseInt(id, 10);
149 }
150
151 export function requireTestRenderer(): ReactTestRenderer {
src/backend/agent.js
+17
@@ -15,6 +15,7 @@ import {
15 sessionStorageSetItem,
16 } from 'src/storage';
17 import setupHighlighter from './views/Highlighter';
18 +import { patch as patchConsole, unpatch as unpatchConsole } from './console';
19
20 import type {
21 InstanceAndStyle,
@@ -133,6 +134,10 @@ export default class Agent extends EventEmitter<{|
134 this.syncSelectionFromNativeElementsPanel
135 );
136 bridge.addListener('shutdown', this.shutdown);
137 + bridge.addListener(
138 + 'updateAppendComponentStack',
139 + this.updateAppendComponentStack
140 + );
141 bridge.addListener('updateComponentFilters', this.updateComponentFilters);
142 bridge.addListener('viewElementSource', this.viewElementSource);
143
@@ -402,6 +407,18 @@ export default class Agent extends EventEmitter<{|
407 this._bridge.send('profilingStatus', this._isProfiling);
408 };
409
410 + updateAppendComponentStack = (appendComponentStack: boolean) => {
411 + // If the frontend preference has change,
412 + // or in the case of React Native- if the backend is just finding out the preference-
413 + // then install or uninstall the console overrides.
414 + // It's safe to call these methods multiple times, so we don't need to worry about that.
415 + if (appendComponentStack) {
416 + patchConsole();
417 + } else {
418 + unpatchConsole();
419 + }
420 + };
421 +
422 updateComponentFilters = (componentFilters: Array<ComponentFilter>) => {
423 for (let rendererID in this._rendererInterfaces) {
424 const renderer = ((this._rendererInterfaces[
src/backend/console.js new
+125
@@ -0,0 +1,125 @@
1 +// @flow
2 +
3 +import { getInternalReactConstants } from './renderer';
4 +import describeComponentFrame from './describeComponentFrame';
5 +
6 +import type { Fiber, ReactRenderer } from './types';
7 +
8 +const FRAME_REGEX = /\n {4}in /;
9 +
10 +const injectedRenderers: Map<
11 + ReactRenderer,
12 + {|
13 + getCurrentFiber: () => Fiber | null,
14 + getDisplayNameForFiber: (fiber: Fiber) => string | null,
15 + |}
16 +> = new Map();
17 +
18 +let isDisabled: boolean = false;
19 +let unpatchFn: null | (() => void) = null;
20 +
21 +export function disable(): void {
22 + isDisabled = true;
23 +}
24 +
25 +export function enable(): void {
26 + isDisabled = false;
27 +}
28 +
29 +export function registerRenderer(renderer: ReactRenderer): void {
30 + const { getCurrentFiber, findFiberByHostInstance, version } = renderer;
31 +
32 + // Ignore React v15 and older because they don't expose a component stack anyway.
33 + if (typeof findFiberByHostInstance !== 'function') {
34 + return;
35 + }
36 +
37 + if (typeof getCurrentFiber === 'function') {
38 + const { getDisplayNameForFiber } = getInternalReactConstants(version);
39 +
40 + injectedRenderers.set(renderer, {
41 + getCurrentFiber,
42 + getDisplayNameForFiber,
43 + });
44 + }
45 +}
46 +
47 +export function patch(targetConsole?: Object = console): void {
48 + if (unpatchFn !== null) {
49 + // Don't patch twice.
50 + return;
51 + }
52 +
53 + const originalConsoleMethods = { ...targetConsole };
54 +
55 + unpatchFn = () => {
56 + for (let method in targetConsole) {
57 + try {
58 + // $FlowFixMe property error|warn is not writable.
59 + targetConsole[method] = originalConsoleMethods[method];
60 + } catch (error) {}
61 + }
62 + };
63 +
64 + for (let method in targetConsole) {
65 + const appendComponentStack =
66 + method === 'error' || method === 'warn' || method === 'trace';
67 +
68 + const originalMethod = targetConsole[method];
69 + const overrideMethod = (...args) => {
70 + if (isDisabled) return;
71 +
72 + if (appendComponentStack) {
73 + // If we are ever called with a string that already has a component stack, e.g. a React error/warning,
74 + // don't append a second stack.
75 + const alreadyHasComponentStack =
76 + args.length > 0 && FRAME_REGEX.exec(args[args.length - 1]);
77 +
78 + if (!alreadyHasComponentStack) {
79 + // If there's a component stack for at least one of the injected renderers, append it.
80 + // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?)
81 + for (let {
82 + getCurrentFiber,
83 + getDisplayNameForFiber,
84 + } of injectedRenderers.values()) {
85 + let current: ?Fiber = getCurrentFiber();
86 + let ownerStack: string = '';
87 + while (current != null) {
88 + const name = getDisplayNameForFiber(current);
89 + const owner = current._debugOwner;
90 + const ownerName =
91 + owner != null ? getDisplayNameForFiber(owner) : null;
92 +
93 + ownerStack += describeComponentFrame(
94 + name,
95 + current._debugSource,
96 + ownerName
97 + );
98 +
99 + current = owner;
100 + }
101 +
102 + if (ownerStack !== '') {
103 + args.push(ownerStack);
104 + break;
105 + }
106 + }
107 + }
108 + }
109 +
110 + originalMethod(...args);
111 + };
112 +
113 + try {
114 + // $FlowFixMe property error|warn is not writable.
115 + targetConsole[method] = overrideMethod;
116 + } catch (error) {}
117 + }
118 +}
119 +
120 +export function unpatch(): void {
121 + if (unpatchFn !== null) {
122 + unpatchFn();
123 + unpatchFn = null;
124 + }
125 +}
src/backend/describeComponentFrame.js new
+41
@@ -0,0 +1,41 @@
1 +// @flow
2 +
3 +// This file was forked from the React GitHub repo:
4 +// https://raw.githubusercontent.com/facebook/react/master/packages/shared/describeComponentFrame.js
5 +//
6 +// It has been modified sligthly to add a zero width space as commented below.
7 +
8 +const BEFORE_SLASH_RE = /^(.*)[\\/]/;
9 +
10 +export default function describeComponentFrame(
11 + name: null | string,
12 + source: any,
13 + ownerName: null | string
14 +) {
15 + let sourceInfo = '';
16 + if (source) {
17 + let path = source.fileName;
18 + let fileName = path.replace(BEFORE_SLASH_RE, '');
19 + if (__DEV__) {
20 + // In DEV, include code for a common special case:
21 + // prefer "folder/index.js" instead of just "index.js".
22 + if (/^index\./.test(fileName)) {
23 + const match = path.match(BEFORE_SLASH_RE);
24 + if (match) {
25 + const pathBeforeSlash = match[1];
26 + if (pathBeforeSlash) {
27 + const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');
28 + // Note the below string contains a zero width space after the "/" character.
29 + // This is to prevent browsers like Chrome from formatting the file name as a link.
30 + // (Since this is a source link, it would not work to open the source file anyway.)
31 + fileName = folderName + '/​' + fileName;
32 + }
33 + }
34 + }
35 + }
36 + sourceInfo = ' (at ' + fileName + ':' + source.lineNumber + ')';
37 + } else if (ownerName) {
38 + sourceInfo = ' (created by ' + ownerName + ')';
39 + }
40 + return '\n in ' + (name || 'Unknown') + sourceInfo;
41 +}
src/backend/index.js
+2 -1
@@ -1,11 +1,12 @@
1 // @flow
2
3 -import type { DevToolsHook, ReactRenderer, RendererInterface } from './types';
3 import Agent from './agent';
4
5 import { attach } from './renderer';
6 import { attach as attachLegacy } from './legacy/renderer';
7
8 +import type { DevToolsHook, ReactRenderer, RendererInterface } from './types';
9 +
10 export function initBackend(
11 hook: DevToolsHook,
12 agent: Agent,
src/backend/renderer.js
+271 -116
@@ -39,6 +39,12 @@ import {
39 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
40 } from '../constants';
41 import { inspectHooksOfFiber } from './ReactDebugHooks';
42 +import {
43 + disable as disableConsole,
44 + enable as enableConsole,
45 + patch as patchConsole,
46 + registerRenderer as registerRendererWithConsole,
47 +} from './console';
48
49 import type {
50 ChangeDescription,
@@ -59,8 +65,89 @@ import type {
65 import type { Interaction } from 'src/devtools/views/Profiler/types';
66 import type { ComponentFilter, ElementType } from 'src/types';
67
62 -function getInternalReactConstants(version) {
63 - const ReactSymbols = {
68 +type getDisplayNameForFiberType = (fiber: Fiber) => string | null;
69 +type getTypeSymbolType = (type: any) => Symbol | number;
70 +
71 +type ReactSymbolsType = {
72 + CONCURRENT_MODE_NUMBER: number,
73 + CONCURRENT_MODE_SYMBOL_STRING: string,
74 + DEPRECATED_ASYNC_MODE_SYMBOL_STRING: string,
75 + CONTEXT_CONSUMER_NUMBER: number,
76 + CONTEXT_CONSUMER_SYMBOL_STRING: string,
77 + CONTEXT_PROVIDER_NUMBER: number,
78 + CONTEXT_PROVIDER_SYMBOL_STRING: string,
79 + EVENT_COMPONENT_NUMBER: number,
80 + EVENT_COMPONENT_STRING: string,
81 + EVENT_TARGET_NUMBER: number,
82 + EVENT_TARGET_STRING: string,
83 + EVENT_TARGET_TOUCH_HIT_NUMBER: number,
84 + EVENT_TARGET_TOUCH_HIT_STRING: string,
85 + FORWARD_REF_NUMBER: number,
86 + FORWARD_REF_SYMBOL_STRING: string,
87 + MEMO_NUMBER: number,
88 + MEMO_SYMBOL_STRING: string,
89 + PROFILER_NUMBER: number,
90 + PROFILER_SYMBOL_STRING: string,
91 + STRICT_MODE_NUMBER: number,
92 + STRICT_MODE_SYMBOL_STRING: string,
93 + SUSPENSE_NUMBER: number,
94 + SUSPENSE_SYMBOL_STRING: string,
95 + DEPRECATED_PLACEHOLDER_SYMBOL_STRING: string,
96 +};
97 +
98 +type ReactPriorityLevelsType = {|
99 + ImmediatePriority: number,
100 + UserBlockingPriority: number,
101 + NormalPriority: number,
102 + LowPriority: number,
103 + IdlePriority: number,
104 + NoPriority: number,
105 +|};
106 +
107 +type ReactTypeOfWorkType = {|
108 + ClassComponent: number,
109 + ContextConsumer: number,
110 + ContextProvider: number,
111 + CoroutineComponent: number,
112 + CoroutineHandlerPhase: number,
113 + DehydratedSuspenseComponent: number,
114 + EventComponent: number,
115 + EventTarget: number,
116 + ForwardRef: number,
117 + Fragment: number,
118 + FunctionComponent: number,
119 + HostComponent: number,
120 + HostPortal: number,
121 + HostRoot: number,
122 + HostText: number,
123 + IncompleteClassComponent: number,
124 + IndeterminateComponent: number,
125 + LazyComponent: number,
126 + MemoComponent: number,
127 + Mode: number,
128 + Profiler: number,
129 + SimpleMemoComponent: number,
130 + SuspenseComponent: number,
131 + YieldComponent: number,
132 +|};
133 +
134 +type ReactTypeOfSideEffectType = {|
135 + NoEffect: number,
136 + PerformedWork: number,
137 + Placement: number,
138 +|};
139 +
140 +export function getInternalReactConstants(
141 + version: string
142 +): {|
143 + getDisplayNameForFiber: getDisplayNameForFiberType,
144 + getTypeSymbol: getTypeSymbolType,
145 + ReactPriorityLevels: ReactPriorityLevelsType,
146 + ReactSymbols: ReactSymbolsType,
147 + ReactTypeOfSideEffect: ReactTypeOfSideEffectType,
148 + ReactTypeOfWork: ReactTypeOfWorkType,
149 +|} {
150 + const ReactSymbols: ReactSymbolsType = {
151 CONCURRENT_MODE_NUMBER: 0xeacf,
152 CONCURRENT_MODE_SYMBOL_STRING: 'Symbol(react.concurrent_mode)',
153 DEPRECATED_ASYNC_MODE_SYMBOL_STRING: 'Symbol(react.async_mode)',
@@ -87,7 +174,7 @@ function getInternalReactConstants(version) {
174 DEPRECATED_PLACEHOLDER_SYMBOL_STRING: 'Symbol(react.placeholder)',
175 };
176
90 - const ReactTypeOfSideEffect = {
177 + const ReactTypeOfSideEffect: ReactTypeOfSideEffectType = {
178 NoEffect: 0b00,
179 PerformedWork: 0b01,
180 Placement: 0b10,
@@ -100,7 +187,7 @@ function getInternalReactConstants(version) {
187 // Technically these priority levels are invalid for versions before 16.9,
188 // but 16.9 is the first version to report priority level to DevTools,
189 // so we can avoid checking for earlier versions and support pre-16.9 canary releases in the process.
103 - const ReactPriorityLevels = {
190 + const ReactPriorityLevels: ReactPriorityLevelsType = {
191 ImmediatePriority: 99,
192 UserBlockingPriority: 98,
193 NormalPriority: 97,
@@ -109,7 +196,7 @@ function getInternalReactConstants(version) {
196 NoPriority: 90,
197 };
198
112 - let ReactTypeOfWork;
199 + let ReactTypeOfWork: ReactTypeOfWorkType = ((null: any): ReactTypeOfWorkType);
200
201 // **********************************************************
202 // The section below is copied from files in React repo.
@@ -200,7 +287,149 @@ function getInternalReactConstants(version) {
287 // End of copied code.
288 // **********************************************************
289
290 + function getTypeSymbol(type: any): Symbol | number {
291 + const symbolOrNumber =
292 + typeof type === 'object' && type !== null ? type.$$typeof : type;
293 +
294 + return typeof symbolOrNumber === 'symbol'
295 + ? symbolOrNumber.toString()
296 + : symbolOrNumber;
297 + }
298 +
299 + const {
300 + ClassComponent,
301 + IncompleteClassComponent,
302 + FunctionComponent,
303 + IndeterminateComponent,
304 + EventComponent,
305 + EventTarget,
306 + ForwardRef,
307 + HostRoot,
308 + HostComponent,
309 + HostPortal,
310 + HostText,
311 + Fragment,
312 + MemoComponent,
313 + SimpleMemoComponent,
314 + } = ReactTypeOfWork;
315 +
316 + const {
317 + EVENT_TARGET_TOUCH_HIT_NUMBER,
318 + EVENT_TARGET_TOUCH_HIT_STRING,
319 + CONCURRENT_MODE_NUMBER,
320 + CONCURRENT_MODE_SYMBOL_STRING,
321 + DEPRECATED_ASYNC_MODE_SYMBOL_STRING,
322 + CONTEXT_PROVIDER_NUMBER,
323 + CONTEXT_PROVIDER_SYMBOL_STRING,
324 + CONTEXT_CONSUMER_NUMBER,
325 + CONTEXT_CONSUMER_SYMBOL_STRING,
326 + STRICT_MODE_NUMBER,
327 + STRICT_MODE_SYMBOL_STRING,
328 + SUSPENSE_NUMBER,
329 + SUSPENSE_SYMBOL_STRING,
330 + DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
331 + PROFILER_NUMBER,
332 + PROFILER_SYMBOL_STRING,
333 + } = ReactSymbols;
334 +
335 + // NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
336 + function getDisplayNameForFiber(fiber: Fiber): string | null {
337 + const { elementType, type, tag } = fiber;
338 +
339 + // This is to support lazy components with a Promise as the type.
340 + // see https://github.com/facebook/react/pull/13397
341 + let resolvedType = type;
342 + if (typeof type === 'object' && type !== null) {
343 + if (typeof type.then === 'function') {
344 + resolvedType = type._reactResult;
345 + }
346 + }
347 +
348 + let resolvedContext: any = null;
349 +
350 + switch (tag) {
351 + case ClassComponent:
352 + case IncompleteClassComponent:
353 + return getDisplayName(resolvedType);
354 + case FunctionComponent:
355 + case IndeterminateComponent:
356 + return getDisplayName(resolvedType);
357 + case EventComponent:
358 + return type.responder.displayName || 'EventComponent';
359 + case EventTarget:
360 + switch (getTypeSymbol(elementType.type)) {
361 + case EVENT_TARGET_TOUCH_HIT_NUMBER:
362 + case EVENT_TARGET_TOUCH_HIT_STRING:
363 + return 'TouchHitTarget';
364 + default:
365 + return elementType.displayName || 'EventTarget';
366 + }
367 + case ForwardRef:
368 + return (
369 + resolvedType.displayName ||
370 + getDisplayName(resolvedType.render, 'Anonymous')
371 + );
372 + case HostRoot:
373 + return null;
374 + case HostComponent:
375 + return type;
376 + case HostPortal:
377 + case HostText:
378 + case Fragment:
379 + return null;
380 + case MemoComponent:
381 + case SimpleMemoComponent:
382 + if (elementType.displayName) {
383 + return elementType.displayName;
384 + } else {
385 + return getDisplayName(type, 'Anonymous');
386 + }
387 + default:
388 + const typeSymbol = getTypeSymbol(type);
389 +
390 + switch (typeSymbol) {
391 + case CONCURRENT_MODE_NUMBER:
392 + case CONCURRENT_MODE_SYMBOL_STRING:
393 + case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
394 + return null;
395 + case CONTEXT_PROVIDER_NUMBER:
396 + case CONTEXT_PROVIDER_SYMBOL_STRING:
397 + // 16.3.0 exposed the context object as "context"
398 + // PR #12501 changed it to "_context" for 16.3.1+
399 + // NOTE Keep in sync with inspectElementRaw()
400 + resolvedContext = fiber.type._context || fiber.type.context;
401 + return `${resolvedContext.displayName || 'Context'}.Provider`;
402 + case CONTEXT_CONSUMER_NUMBER:
403 + case CONTEXT_CONSUMER_SYMBOL_STRING:
404 + // 16.3-16.5 read from "type" because the Consumer is the actual context object.
405 + // 16.6+ should read from "type._context" because Consumer can be different (in DEV).
406 + // NOTE Keep in sync with inspectElementRaw()
407 + resolvedContext = fiber.type._context || fiber.type;
408 +
409 + // NOTE: TraceUpdatesBackendManager depends on the name ending in '.Consumer'
410 + // If you change the name, figure out a more resilient way to detect it.
411 + return `${resolvedContext.displayName || 'Context'}.Consumer`;
412 + case STRICT_MODE_NUMBER:
413 + case STRICT_MODE_SYMBOL_STRING:
414 + return null;
415 + case SUSPENSE_NUMBER:
416 + case SUSPENSE_SYMBOL_STRING:
417 + case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
418 + return 'Suspense';
419 + case PROFILER_NUMBER:
420 + case PROFILER_SYMBOL_STRING:
421 + return `Profiler(${fiber.memoizedProps.id})`;
422 + default:
423 + // Unknown element type.
424 + // This may mean a new element type that has not yet been added to DevTools.
425 + return null;
426 + }
427 + }
428 + }
429 +
430 return {
431 + getDisplayNameForFiber,
432 + getTypeSymbol,
433 ReactPriorityLevels,
434 ReactTypeOfWork,
435 ReactSymbols,
@@ -215,6 +444,8 @@ export function attach(
444 global: Object
445 ): RendererInterface {
446 const {
447 + getDisplayNameForFiber,
448 + getTypeSymbol,
449 ReactPriorityLevels,
450 ReactTypeOfWork,
451 ReactSymbols,
@@ -256,8 +487,6 @@ export function attach(
487 CONTEXT_CONSUMER_SYMBOL_STRING,
488 CONTEXT_PROVIDER_NUMBER,
489 CONTEXT_PROVIDER_SYMBOL_STRING,
259 - EVENT_TARGET_TOUCH_HIT_NUMBER,
260 - EVENT_TARGET_TOUCH_HIT_STRING,
490 PROFILER_NUMBER,
491 PROFILER_SYMBOL_STRING,
492 STRICT_MODE_NUMBER,
@@ -277,6 +506,22 @@ export function attach(
506 typeof setSuspenseHandler === 'function' &&
507 typeof scheduleUpdate === 'function';
508
509 + // Patching the console enables DevTools to do a few useful things:
510 + // * Append component stacks to warnings and error messages
511 + // * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
512 + //
513 + // Don't patch in test environments because we don't want to interfere with Jest's own console overrides.
514 + if (process.env.NODE_ENV !== 'test') {
515 + registerRendererWithConsole(renderer);
516 +
517 + // The renderer interface can't read this preference directly,
518 + // because it is stored in localStorage within the context of the extension.
519 + // It relies on the extension to pass the preference through via the global.
520 + if (window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false) {
521 + patchConsole();
522 + }
523 + }
524 +
525 const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => {
526 if (__DEBUG__) {
527 const displayName = getDisplayNameForFiber(fiber) || 'null';
@@ -345,7 +590,10 @@ export function attach(
590 if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ != null) {
591 applyComponentFilters(window.__REACT_DEVTOOLS_COMPONENT_FILTERS__);
592 } else {
348 - console.warn('⚛️ DevTools: Could not locate saved component filters');
593 + // Unfortunately this feature is not expected to work for React Native for now.
594 + // It would be annoying for us to spam YellowBox warnings with unactionable stuff,
595 + // so for now just skip this message...
596 + //console.warn('⚛️ DevTools: Could not locate saved component filters');
597
598 // Fallback to assuming the default filters in this case.
599 applyComponentFilters(getDefaultComponentFilters());
@@ -447,111 +695,6 @@ export function attach(
695
696 return false;
697 }
450 -
451 - function getTypeSymbol(type: any): Symbol | number {
452 - const symbolOrNumber =
453 - typeof type === 'object' && type !== null ? type.$$typeof : type;
454 -
455 - return typeof symbolOrNumber === 'symbol'
456 - ? symbolOrNumber.toString()
457 - : symbolOrNumber;
458 - }
459 -
460 - // NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
461 - function getDisplayNameForFiber(fiber: Fiber): string | null {
462 - const { elementType, type, tag } = fiber;
463 -
464 - // This is to support lazy components with a Promise as the type.
465 - // see https://github.com/facebook/react/pull/13397
466 - let resolvedType = type;
467 - if (typeof type === 'object' && type !== null) {
468 - if (typeof type.then === 'function') {
469 - resolvedType = type._reactResult;
470 - }
471 - }
472 -
473 - let resolvedContext: any = null;
474 -
475 - switch (tag) {
476 - case ClassComponent:
477 - case IncompleteClassComponent:
478 - return getDisplayName(resolvedType);
479 - case FunctionComponent:
480 - case IndeterminateComponent:
481 - return getDisplayName(resolvedType);
482 - case EventComponent:
483 - return type.responder.displayName || 'EventComponent';
484 - case EventTarget:
485 - switch (getTypeSymbol(elementType.type)) {
486 - case EVENT_TARGET_TOUCH_HIT_NUMBER:
487 - case EVENT_TARGET_TOUCH_HIT_STRING:
488 - return 'TouchHitTarget';
489 - default:
490 - return elementType.displayName || 'EventTarget';
491 - }
492 - case ForwardRef:
493 - return (
494 - resolvedType.displayName ||
495 - getDisplayName(resolvedType.render, 'Anonymous')
496 - );
497 - case HostRoot:
498 - return null;
499 - case HostComponent:
500 - return type;
501 - case HostPortal:
502 - case HostText:
503 - case Fragment:
504 - return null;
505 - case MemoComponent:
506 - case SimpleMemoComponent:
507 - if (elementType.displayName) {
508 - return elementType.displayName;
509 - } else {
510 - return getDisplayName(type, 'Anonymous');
511 - }
512 - default:
513 - const typeSymbol = getTypeSymbol(type);
514 -
515 - switch (typeSymbol) {
516 - case CONCURRENT_MODE_NUMBER:
517 - case CONCURRENT_MODE_SYMBOL_STRING:
518 - case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
519 - return null;
520 - case CONTEXT_PROVIDER_NUMBER:
521 - case CONTEXT_PROVIDER_SYMBOL_STRING:
522 - // 16.3.0 exposed the context object as "context"
523 - // PR #12501 changed it to "_context" for 16.3.1+
524 - // NOTE Keep in sync with inspectElementRaw()
525 - resolvedContext = fiber.type._context || fiber.type.context;
526 - return `${resolvedContext.displayName || 'Context'}.Provider`;
527 - case CONTEXT_CONSUMER_NUMBER:
528 - case CONTEXT_CONSUMER_SYMBOL_STRING:
529 - // 16.3-16.5 read from "type" because the Consumer is the actual context object.
530 - // 16.6+ should read from "type._context" because Consumer can be different (in DEV).
531 - // NOTE Keep in sync with inspectElementRaw()
532 - resolvedContext = fiber.type._context || fiber.type;
533 -
534 - // NOTE: TraceUpdatesBackendManager depends on the name ending in '.Consumer'
535 - // If you change the name, figure out a more resilient way to detect it.
536 - return `${resolvedContext.displayName || 'Context'}.Consumer`;
537 - case STRICT_MODE_NUMBER:
538 - case STRICT_MODE_SYMBOL_STRING:
539 - return null;
540 - case SUSPENSE_NUMBER:
541 - case SUSPENSE_SYMBOL_STRING:
542 - case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
543 - return 'Suspense';
544 - case PROFILER_NUMBER:
545 - case PROFILER_SYMBOL_STRING:
546 - return `Profiler(${fiber.memoizedProps.id})`;
547 - default:
548 - // Unknown element type.
549 - // This may mean a new element type that has not yet been added to DevTools.
550 - return null;
551 - }
552 - }
553 - }
554 -
698 // NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods
699 function getElementTypeForFiber(fiber: Fiber): ElementType {
700 const { type, tag } = fiber;
@@ -2109,6 +2252,20 @@ export function attach(
2252 node = node.return;
2253 }
2254
2255 + let hooks = null;
2256 + if (usesHooks) {
2257 + // Suppress console logging while re-rendering
2258 + try {
2259 + disableConsole();
2260 + hooks = inspectHooksOfFiber(
2261 + fiber,
2262 + (renderer.currentDispatcherRef: any)
2263 + );
2264 + } finally {
2265 + enableConsole();
2266 + }
2267 + }
2268 +
2269 return {
2270 id,
2271
@@ -2136,9 +2293,7 @@ export function attach(
2293 // TODO Review sanitization approach for the below inspectable values.
2294 context,
2295 events,
2139 - hooks: usesHooks
2140 - ? inspectHooksOfFiber(fiber, (renderer.currentDispatcherRef: any))
2141 - : null,
2296 + hooks,
2297 props: memoizedProps,
2298 state: usesHooks ? null : memoizedState,
2299
src/backend/types.js
+4
@@ -118,6 +118,10 @@ export type ReactRenderer = {
118 // Only injected by React v16.8+ in order to support hooks inspection.
119 currentDispatcherRef?: {| current: null | Dispatcher |},
120
121 + // Only injected by React v16.9+ in DEV mode.
122 + // Enables DevTools to append owners-only component stack to error messages.
123 + getCurrentFiber?: () => Fiber | null,
124 +
125 // <= 15
126 Mount?: any,
127 };
src/bridge.js
+1
@@ -98,6 +98,7 @@ export default class Bridge extends EventEmitter<{|
98 stopProfiling: [],
99 syncSelectionFromNativeElementsPanel: [],
100 syncSelectionToNativeElementsPanel: [],
101 + updateAppendComponentStack: [boolean],
102 updateComponentFilters: [Array<ComponentFilter>],
103 viewElementSource: [ElementAndRendererID],
104
src/constants.js
+3
@@ -20,4 +20,7 @@ export const SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
20 export const SESSION_STORAGE_RELOAD_AND_PROFILE_KEY =
21 'React::DevTools::reloadAndProfile';
22
23 +export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY =
24 + 'React::DevTools::appendComponentStack';
25 +
26 export const PROFILER_EXPORT_VERSION = 4;
src/devtools/views/Settings/GeneralSettings.js
+25 -16
@@ -1,27 +1,25 @@
1 // @flow
2
3 -import React, { useCallback, useContext } from 'react';
3 +import React, { useContext } from 'react';
4 import { SettingsContext } from './SettingsContext';
5
6 import styles from './SettingsShared.css';
7
8 export default function GeneralSettings(_: {||}) {
9 - const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
10 - SettingsContext
11 - );
9 + const {
10 + displayDensity,
11 + setDisplayDensity,
12 + theme,
13 + setTheme,
14 + appendComponentStack,
15 + setAppendComponentStack,
16 + } = useContext(SettingsContext);
17
13 - const updateDisplayDensity = useCallback(
14 - ({ currentTarget }) => {
15 - setDisplayDensity(currentTarget.value);
16 - },
17 - [setDisplayDensity]
18 - );
19 - const updateTheme = useCallback(
20 - ({ currentTarget }) => {
21 - setTheme(currentTarget.value);
22 - },
23 - [setTheme]
24 - );
18 + const updateDisplayDensity = ({ currentTarget }) =>
19 + setDisplayDensity(currentTarget.value);
20 + const updateTheme = ({ currentTarget }) => setTheme(currentTarget.value);
21 + const updateappendComponentStack = ({ currentTarget }) =>
22 + setAppendComponentStack(currentTarget.checked);
23
24 return (
25 <div className={styles.Settings}>
@@ -45,6 +43,17 @@ export default function GeneralSettings(_: {||}) {
43 <option value="comfortable">Comfortable</option>
44 </select>
45 </div>
46 +
47 + <div className={styles.Setting}>
48 + <label>
49 + <input
50 + type="checkbox"
51 + checked={appendComponentStack}
52 + onChange={updateappendComponentStack}
53 + />{' '}
54 + Append component stacks to console warnings and errors.
55 + </label>
56 + </div>
57 </div>
58 );
59 }
src/devtools/views/Settings/SettingsContext.js
+26 -1
@@ -1,7 +1,15 @@
1 // @flow
2
3 -import React, { createContext, useLayoutEffect, useMemo } from 'react';
3 +import React, {
4 + createContext,
5 + useContext,
6 + useEffect,
7 + useLayoutEffect,
8 + useMemo,
9 +} from 'react';
10 +import { LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY } from 'src/constants';
11 import { useLocalStorage } from '../hooks';
12 +import { BridgeContext } from '../context';
13
14 import type { BrowserTheme } from '../DevTools';
15
@@ -16,6 +24,9 @@ type Context = {|
24 // Specified as a separate prop so it can trigger a re-render of FixedSizeList.
25 lineHeight: number,
26
27 + appendComponentStack: boolean,
28 + setAppendComponentStack: (value: boolean) => void,
29 +
30 theme: Theme,
31 setTheme(value: Theme): void,
32 |};
@@ -40,6 +51,8 @@ function SettingsContextController({
51 profilerPortalContainer,
52 settingsPortalContainer,
53 }: Props) {
54 + const bridge = useContext(BridgeContext);
55 +
56 const [displayDensity, setDisplayDensity] = useLocalStorage<DisplayDensity>(
57 'React::DevTools::displayDensity',
58 'compact'
@@ -48,6 +61,10 @@ function SettingsContextController({
61 'React::DevTools::theme',
62 'auto'
63 );
64 + const [
65 + appendComponentStack,
66 + setAppendComponentStack,
67 + ] = useLocalStorage<boolean>(LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY, true);
68
69 const documentElements = useMemo<DocumentElements>(() => {
70 const array: Array<HTMLElement> = [
@@ -117,12 +134,18 @@ function SettingsContextController({
134 }
135 }, [browserTheme, theme, documentElements]);
136
137 + useEffect(() => {
138 + bridge.send('updateAppendComponentStack', appendComponentStack);
139 + }, [bridge, appendComponentStack]);
140 +
141 const value = useMemo(
142 () => ({
143 displayDensity,
144 setDisplayDensity,
145 theme,
146 setTheme,
147 + appendComponentStack,
148 + setAppendComponentStack,
149 lineHeight:
150 displayDensity === 'compact'
151 ? compactLineHeight
@@ -134,6 +157,8 @@ function SettingsContextController({
157 displayDensity,
158 setDisplayDensity,
159 setTheme,
160 + appendComponentStack,
161 + setAppendComponentStack,
162 theme,
163 ]
164 );
src/hook.js
+31
@@ -7,6 +7,11 @@
7 * @flow
8 */
9
10 +import {
11 + patch as patchConsole,
12 + registerRenderer as registerRendererWithConsole,
13 +} from './backend/console';
14 +
15 import type { DevToolsHook } from 'src/backend/types';
16
17 declare var window: any;
@@ -155,6 +160,32 @@ export function installHook(target: any): DevToolsHook | null {
160 ? 'deadcode'
161 : detectReactBuildType(renderer);
162
163 + // Patching the console enables DevTools to do a few useful things:
164 + // * Append component stacks to warnings and error messages
165 + // * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
166 + //
167 + // For React Native, we intentionally patch early (during injection).
168 + // This provides React Native developers with components stacks even if they don't run DevTools.
169 + // This won't work for DOM though, since this entire file is eval'ed and inserted as a script tag.
170 + // In that case, we'll patch later (when the frontend attaches).
171 + //
172 + // Don't patch in test environments because we don't want to interfere with Jest's own console overrides.
173 + if (process.env.NODE_ENV !== 'test') {
174 + try {
175 + // The installHook() function is injected by being stringified in the browser,
176 + // so imports outside of this function do not get included.
177 + //
178 + // Normally we could check "typeof patchConsole === 'function'",
179 + // but Webpack wraps imports with an object (e.g. _backend_console__WEBPACK_IMPORTED_MODULE_0__)
180 + // and the object itself will be undefined as well for the reasons mentioned above,
181 + // so we use try/catch instead.
182 + if (window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false) {
183 + registerRendererWithConsole(renderer);
184 + patchConsole();
185 + }
186 + } catch (error) {}
187 + }
188 +
189 // If we have just reloaded to profile, we need to inject the renderer interface before the app loads.
190 // Otherwise the renderer won't yet exist and we can skip this step.
191 const attach = target.__REACT_DEVTOOLS_ATTACH__;
src/utils.js
+21 -1
@@ -8,7 +8,10 @@ import {
8 TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
9 } from './constants';
10 import { ElementTypeRoot } from 'src/types';
11 -import { LOCAL_STORAGE_FILTER_PREFERENCES_KEY } from './constants';
11 +import {
12 + LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
13 + LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
14 +} from './constants';
15 import { ComponentFilterElementType, ElementTypeHostComponent } from './types';
16 import {
17 ElementTypeClass,
@@ -198,6 +201,23 @@ export function saveComponentFilters(
201 );
202 }
203
204 +export function getAppendComponentStack(): boolean {
205 + try {
206 + const raw = localStorageGetItem(LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY);
207 + if (raw != null) {
208 + return JSON.parse(raw);
209 + }
210 + } catch (error) {}
211 + return true;
212 +}
213 +
214 +export function setAppendComponentStack(value: boolean): void {
215 + localStorageSetItem(
216 + LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
217 + JSON.stringify(value)
218 + );
219 +}
220 +
221 export function separateDisplayNameAndHOCs(
222 displayName: string | null,
223 type: ElementType
yarn.lock
+23 -23
@@ -9944,20 +9944,20 @@ react-color@^2.11.7:
9944 object-assign "^4.1.0"
9945 prop-types "^15.5.10"
9946
9947 -react-dom@^0.0.0-50b50c26f:
9948 - version "0.0.0-50b50c26f"
9949 - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-50b50c26f.tgz#3cd8da0f2276ed4b7a926e1807d2675b2eb40227"
9950 - integrity sha512-da9qleWDdBdAguEIDvvpFE0iuS8hfcCSGgZTYKRQMlSh5A94Ktr1otL4rgDTFH+bNsOwz3XrvEBYRA6WaE9xzQ==
9947 +react-dom@^0.0.0-424099da6:
9948 + version "0.0.0-424099da6"
9949 + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-424099da6.tgz#2a6392d5730fd7688d7ec5a56b2f9f8e3627d271"
9950 + integrity sha512-B6x2YWaw06xJ0br9wsny6a9frqmaNAw+vGrm08W7JZp7WfiBYKhM6Nt9seijaAVzajp49fe18orNMgL12Lafsg==
9951 dependencies:
9952 loose-envify "^1.1.0"
9953 object-assign "^4.1.1"
9954 prop-types "^15.6.2"
9955 - scheduler "0.0.0-50b50c26f"
9955 + scheduler "0.0.0-424099da6"
9956
9957 -react-is@0.0.0-50b50c26f, react-is@^0.0.0-50b50c26f:
9958 - version "0.0.0-50b50c26f"
9959 - resolved "https://registry.yarnpkg.com/react-is/-/react-is-0.0.0-50b50c26f.tgz#c4003ffffef9bd2b287979f9041a23d12a607bf2"
9960 - integrity sha512-9Y6ZvdOVmOxXs9mGuFy6eXHBww8RJCtJAh94b1hkbjhnW8Mb5ADScDoxJBVxcNuX9hvDkhENspC96ZQK1NIv3g==
9957 +react-is@0.0.0-424099da6:
9958 + version "0.0.0-424099da6"
9959 + resolved "https://registry.yarnpkg.com/react-is/-/react-is-0.0.0-424099da6.tgz#a8b1322bbb1ef014b33ee3bff30d3be9a4729baa"
9960 + integrity sha512-VMFvIdqNV0eB8YxmE9katf3XM4qbdKGhLYANfohwktTryrWWOUOoVRX6IHm4iN06LgHwWLBOBP/YARc1qzuF2w==
9961
9962 react-is@^16.8.1:
9963 version "16.8.3"
@@ -9969,15 +9969,15 @@ react-is@^16.8.4:
9969 resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2"
9970 integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA==
9971
9972 -react-test-renderer@^0.0.0-50b50c26f:
9973 - version "0.0.0-50b50c26f"
9974 - resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-0.0.0-50b50c26f.tgz#1a85cf9073ef5a932d03bee36fcfd9bf15aeae2c"
9975 - integrity sha512-gWc4L+mFIUCjvBpafR88n4/i/oaKHD6rzVyZY+XBou9MNtr2rRkjePOhBVsiYlCwkj+zZi6klV9b05TMzftosA==
9972 +react-test-renderer@^0.0.0-424099da6:
9973 + version "0.0.0-424099da6"
9974 + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-0.0.0-424099da6.tgz#75272c39e0b45e99dbd674977a4afc32a2d37f81"
9975 + integrity sha512-tF9NutO52Js52L390poDUvnN43j77SekXyIt0KzQa+HdgY7uvvzKH9ouqCz3M5f52eDZDff4OG4cZuZ+lNLIfQ==
9976 dependencies:
9977 object-assign "^4.1.1"
9978 prop-types "^15.6.2"
9979 - react-is "0.0.0-50b50c26f"
9980 - scheduler "0.0.0-50b50c26f"
9979 + react-is "0.0.0-424099da6"
9980 + scheduler "0.0.0-424099da6"
9981
9982 react-virtualized-auto-sizer@^1.0.2:
9983 version "1.0.2"
@@ -9990,10 +9990,10 @@ react-window@./vendor/react-window:
9990 "@babel/runtime" "^7.0.0"
9991 memoize-one ">=3.1.1 <6"
9992
9993 -react@^0.0.0-50b50c26f:
9994 - version "0.0.0-50b50c26f"
9995 - resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-50b50c26f.tgz#b782b579ce1f5d8bd696c5e45c744714ebecb111"
9996 - integrity sha512-jUAzS4DeWTdUZ/3kqm2T6C9OIpiAf2qdwVamCts0qzwYVni1/gUTOWK1ui0J+eaRzKxrIEzVvmCMxFd35lP/pA==
9993 +react@^0.0.0-424099da6:
9994 + version "0.0.0-424099da6"
9995 + resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-424099da6.tgz#bf9155a10bb09783cfdc9e79438062af4b249861"
9996 + integrity sha512-z/brDYS4RaX3+zknH8nIV7i9B7We3hFBdD0QWhDKKgEHInFLF1Y/+2GsdedDI69GWw8Hv6mw1iilycHjHRaCZA==
9997 dependencies:
9998 loose-envify "^1.1.0"
9999 object-assign "^4.1.1"
@@ -10658,10 +10658,10 @@ sax@>=0.6.0, sax@^1.2.4:
10658 resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
10659 integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
10660
10661 -scheduler@0.0.0-50b50c26f, scheduler@^0.0.0-50b50c26f:
10662 - version "0.0.0-50b50c26f"
10663 - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-50b50c26f.tgz#09bedde1c64d7a042b557bee2dbf5faf5fd58a50"
10664 - integrity sha512-LBN3zrP8iBdILOoYxybFtkU7j+ldZTHORKyYyVLwXuIwGQ8/Xhs5VZjNQ5R2Xru2zv3GGVpJSbd47EpDuD2EHw==
10661 +scheduler@0.0.0-424099da6, scheduler@^0.0.0-424099da6:
10662 + version "0.0.0-424099da6"
10663 + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-424099da6.tgz#5311edfc2716479475517fdcbc24909948026bdb"
10664 + integrity sha512-eDsz8sdikcel1lKRDJhUZ17K22rOdmJAV07coMgIvdX0MoHh9cVNQ+iryU7O9Gi/c7ySE3DaX1M9xBqAqyXA3g==
10665 dependencies:
10666 loose-envify "^1.1.0"
10667 object-assign "^4.1.1"