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"