1
-/* global chrome */
2
-
3
-import {createElement} from 'react';
4
-import {flushSync} from 'react-dom';
5
-import {createRoot} from 'react-dom/client';
6
-import Bridge from 'react-devtools-shared/src/bridge';
7
-import Store from 'react-devtools-shared/src/devtools/store';
8
-import {IS_CHROME, IS_EDGE, getBrowserTheme} from './utils';
9
-import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDevToolsEventLogger';
10
-import {
11
- getAppendComponentStack,
12
- getBreakOnConsoleErrors,
13
- getSavedComponentFilters,
14
- getShowInlineWarningsAndErrors,
15
- getHideConsoleLogsInStrictMode,
16
-} from 'react-devtools-shared/src/utils';
17
-import {
18
- localStorageGetItem,
19
- localStorageRemoveItem,
20
- localStorageSetItem,
21
-} from 'react-devtools-shared/src/storage';
22
-import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
23
-import {
24
- __DEBUG__,
25
- LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
26
-} from 'react-devtools-shared/src/constants';
27
-import {logEvent} from 'react-devtools-shared/src/Logger';
28
-
29
-const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
30
- 'React::DevTools::supportsProfiling';
31
-
32
-// rAF never fires on devtools_page (because it's in the background)
33
-// https://bugs.chromium.org/p/chromium/issues/detail?id=1241986#c31
34
-// Since we render React elements here, we need to polyfill it with setTimeout
35
-// The polyfill is based on https://gist.github.com/jalbam/5fe05443270fa6d8136238ec72accbc0
36
-const FRAME_TIME = 16;
37
-let lastTime = 0;
38
-window.requestAnimationFrame = function (callback, element) {
39
- const now = window.performance.now();
40
- const nextTime = Math.max(lastTime + FRAME_TIME, now);
41
- return setTimeout(function () {
42
- callback((lastTime = nextTime));
43
- }, nextTime - now);
44
-};
45
-window.cancelAnimationFrame = clearTimeout;
46
-
47
-let panelCreated = false;
48
-
49
-// The renderer interface can't read saved component filters directly,
50
-// because they are stored in localStorage within the context of the extension.
51
-// Instead it relies on the extension to pass filters through.
52
-function syncSavedPreferences() {
53
- chrome.devtools.inspectedWindow.eval(
54
- `window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
55
- getAppendComponentStack(),
56
- )};
57
- window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify(
58
- getBreakOnConsoleErrors(),
59
- )};
60
- window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
61
- getSavedComponentFilters(),
62
- )};
63
- window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
64
- getShowInlineWarningsAndErrors(),
65
- )};
66
- window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = ${JSON.stringify(
67
- getHideConsoleLogsInStrictMode(),
68
- )};
69
- window.__REACT_DEVTOOLS_BROWSER_THEME__ = ${JSON.stringify(
70
- getBrowserTheme(),
71
- )};`,
72
- );
73
-}
74
-
75
-syncSavedPreferences();
76
-
77
-function createPanelIfReactLoaded() {
78
- if (panelCreated) {
79
- return;
80
- }
81
-
82
- chrome.devtools.inspectedWindow.eval(
83
- 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
84
- function (pageHasReact, error) {
85
- if (!pageHasReact || panelCreated) {
86
- return;
87
- }
88
-
89
- panelCreated = true;
90
-
91
- clearInterval(loadCheckInterval);
92
-
93
- let bridge = null;
94
- let store = null;
95
-
96
- let profilingData = null;
97
-
98
- let componentsPortalContainer = null;
99
- let profilerPortalContainer = null;
100
-
101
- let cloneStyleTags = null;
102
- let mostRecentOverrideTab = null;
103
- let render = null;
104
- let root = null;
105
-
106
- const tabId = chrome.devtools.inspectedWindow.tabId;
107
-
108
- registerDevToolsEventLogger('extension', async () => {
109
- // TODO: after we upgrade to Manifest V3, chrome.tabs.query returns a Promise
110
- // without the callback.
111
- return new Promise(resolve => {
112
- chrome.tabs.query({active: true, currentWindow: true}, tabs => {
113
- resolve({
114
- page_url: tabs[0]?.url,
115
- });
116
- });
117
- });
118
- });
119
-
120
- function initBridgeAndStore() {
121
- const port = chrome.runtime.connect({
122
- name: String(tabId),
123
- });
124
- // Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation,
125
- // so it makes no sense to handle it here.
126
-
127
- bridge = new Bridge({
128
- listen(fn) {
129
- const listener = message => fn(message);
130
- // Store the reference so that we unsubscribe from the same object.
131
- const portOnMessage = port.onMessage;
132
- portOnMessage.addListener(listener);
133
- return () => {
134
- portOnMessage.removeListener(listener);
135
- };
136
- },
137
- send(event: string, payload: any, transferable?: Array<any>) {
138
- port.postMessage({event, payload}, transferable);
139
- },
140
- });
141
- bridge.addListener('reloadAppForProfiling', () => {
142
- localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
143
- chrome.devtools.inspectedWindow.eval('window.location.reload();');
144
- });
145
- bridge.addListener('syncSelectionToNativeElementsPanel', () => {
146
- setBrowserSelectionFromReact();
147
- });
148
-
149
- // This flag lets us tip the Store off early that we expect to be profiling.
150
- // This avoids flashing a temporary "Profiling not supported" message in the Profiler tab,
151
- // after a user has clicked the "reload and profile" button.
152
- let isProfiling = false;
153
- let supportsProfiling = false;
154
- if (
155
- localStorageGetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY) === 'true'
156
- ) {
157
- supportsProfiling = true;
158
- isProfiling = true;
159
- localStorageRemoveItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY);
160
- }
161
-
162
- if (store !== null) {
163
- profilingData = store.profilerStore.profilingData;
164
- }
165
-
166
- bridge.addListener('extensionBackendInitialized', () => {
167
- // Initialize the renderer's trace-updates setting.
168
- // This handles the case of navigating to a new page after the DevTools have already been shown.
169
- bridge.send(
170
- 'setTraceUpdatesEnabled',
171
- localStorageGetItem(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY) ===
172
- 'true',
173
- );
174
- });
175
-
176
- store = new Store(bridge, {
177
- isProfiling,
178
- supportsReloadAndProfile: IS_CHROME || IS_EDGE,
179
- supportsProfiling,
180
- // At this time, the timeline can only parse Chrome performance profiles.
181
- supportsTimeline: IS_CHROME,
182
- supportsTraceUpdates: true,
183
- });
184
- if (!isProfiling) {
185
- store.profilerStore.profilingData = profilingData;
186
- }
187
-
188
- // Initialize the backend only once the Store has been initialized.
189
- // Otherwise the Store may miss important initial tree op codes.
190
- if (IS_CHROME || IS_EDGE) {
191
- chrome.runtime.sendMessage({
192
- source: 'react-devtools-main',
193
- payload: {
194
- type: 'react-devtools-inject-backend-manager',
195
- tabId,
196
- },
197
- });
198
- } else {
199
- // Firefox does not support executing script in ExecutionWorld.MAIN from content script.
200
- // see prepareInjection.js
201
- chrome.devtools.inspectedWindow.eval(
202
- `window.postMessage({ source: 'react-devtools-inject-backend-manager' }, '*');`,
203
- function (response, evalError) {
204
- if (evalError) {
205
- console.error(evalError);
206
- }
207
- },
208
- );
209
- }
210
-
211
- const viewAttributeSourceFunction = (id, path) => {
212
- const rendererID = store.getRendererIDForElement(id);
213
- if (rendererID != null) {
214
- // Ask the renderer interface to find the specified attribute,
215
- // and store it as a global variable on the window.
216
- bridge.send('viewAttributeSource', {id, path, rendererID});
217
-
218
- setTimeout(() => {
219
- // Ask Chrome to display the location of the attribute,
220
- // assuming the renderer found a match.
221
- chrome.devtools.inspectedWindow.eval(`
222
- if (window.$attribute != null) {
223
- inspect(window.$attribute);
224
- }
225
- `);
226
- }, 100);
227
- }
228
- };
229
-
230
- const viewElementSourceFunction = id => {
231
- const rendererID = store.getRendererIDForElement(id);
232
- if (rendererID != null) {
233
- // Ask the renderer interface to determine the component function,
234
- // and store it as a global variable on the window
235
- bridge.send('viewElementSource', {id, rendererID});
236
-
237
- setTimeout(() => {
238
- // Ask Chrome to display the location of the component function,
239
- // or a render method if it is a Class (ideally Class instance, not type)
240
- // assuming the renderer found one.
241
- chrome.devtools.inspectedWindow.eval(`
242
- if (window.$type != null) {
243
- if (
244
- window.$type &&
245
- window.$type.prototype &&
246
- window.$type.prototype.isReactComponent
247
- ) {
248
- // inspect Component.render, not constructor
249
- inspect(window.$type.prototype.render);
250
- } else {
251
- // inspect Functional Component
252
- inspect(window.$type);
253
- }
254
- }
255
- `);
256
- }, 100);
257
- }
258
- };
259
-
260
- const viewUrlSourceFunction = (url, line, col) => {
261
- chrome.devtools.panels.openResource(url, line, col);
262
- };
263
-
264
- let debugIDCounter = 0;
265
-
266
- // For some reason in Firefox, chrome.runtime.sendMessage() from a content script
267
- // never reaches the chrome.runtime.onMessage event listener.
268
- let fetchFileWithCaching = null;
269
- if (IS_CHROME) {
270
- const fetchFromNetworkCache = (url, resolve, reject) => {
271
- // Debug ID allows us to avoid re-logging (potentially long) URL strings below,
272
- // while also still associating (potentially) interleaved logs with the original request.
273
- let debugID = null;
274
-
275
- if (__DEBUG__) {
276
- debugID = debugIDCounter++;
277
- console.log(`[main] fetchFromNetworkCache(${debugID})`, url);
278
- }
279
-
280
- chrome.devtools.network.getHAR(harLog => {
281
- for (let i = 0; i < harLog.entries.length; i++) {
282
- const entry = harLog.entries[i];
283
- if (url === entry.request.url) {
284
- if (__DEBUG__) {
285
- console.log(
286
- `[main] fetchFromNetworkCache(${debugID}) Found matching URL in HAR`,
287
- url,
288
- );
289
- }
290
-
291
- entry.getContent(content => {
292
- if (content) {
293
- if (__DEBUG__) {
294
- console.log(
295
- `[main] fetchFromNetworkCache(${debugID}) Content retrieved`,
296
- );
297
- }
298
-
299
- resolve(content);
300
- } else {
301
- if (__DEBUG__) {
302
- console.log(
303
- `[main] fetchFromNetworkCache(${debugID}) Invalid content returned by getContent()`,
304
- content,
305
- );
306
- }
307
-
308
- // Edge case where getContent() returned null; fall back to fetch.
309
- fetchFromPage(url, resolve, reject);
310
- }
311
- });
312
-
313
- return;
314
- }
315
- }
316
-
317
- if (__DEBUG__) {
318
- console.log(
319
- `[main] fetchFromNetworkCache(${debugID}) No cached request found in getHAR()`,
320
- );
321
- }
322
-
323
- // No matching URL found; fall back to fetch.
324
- fetchFromPage(url, resolve, reject);
325
- });
326
- };
327
-
328
- const fetchFromPage = (url, resolve, reject) => {
329
- if (__DEBUG__) {
330
- console.log('[main] fetchFromPage()', url);
331
- }
332
-
333
- function onPortMessage({payload, source}) {
334
- if (source === 'react-devtools-content-script') {
335
- switch (payload?.type) {
336
- case 'fetch-file-with-cache-complete':
337
- chrome.runtime.onMessage.removeListener(onPortMessage);
338
- resolve(payload.value);
339
- break;
340
- case 'fetch-file-with-cache-error':
341
- chrome.runtime.onMessage.removeListener(onPortMessage);
342
- reject(payload.value);
343
- break;
344
- }
345
- }
346
- }
347
-
348
- chrome.runtime.onMessage.addListener(onPortMessage);
349
-
350
- chrome.devtools.inspectedWindow.eval(`
351
- window.postMessage({
352
- source: 'react-devtools-extension',
353
- payload: {
354
- type: 'fetch-file-with-cache',
355
- url: "${url}",
356
- },
357
- });
358
- `);
359
- };
360
-
361
- // Fetching files from the extension won't make use of the network cache
362
- // for resources that have already been loaded by the page.
363
- // This helper function allows the extension to request files to be fetched
364
- // by the content script (running in the page) to increase the likelihood of a cache hit.
365
- fetchFileWithCaching = url => {
366
- return new Promise((resolve, reject) => {
367
- // Try fetching from the Network cache first.
368
- // If DevTools was opened after the page started loading, we may have missed some requests.
369
- // So fall back to a fetch() from the page and hope we get a cached response that way.
370
- fetchFromNetworkCache(url, resolve, reject);
371
- });
372
- };
373
- }
374
-
375
- // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
376
- const hookNamesModuleLoaderFunction = () =>
377
- import(
378
- /* webpackChunkName: 'parseHookNames' */ 'react-devtools-shared/src/hooks/parseHookNames'
379
- );
380
-
381
- root = createRoot(document.createElement('div'));
382
-
383
- render = (overrideTab = mostRecentOverrideTab) => {
384
- mostRecentOverrideTab = overrideTab;
385
- root.render(
386
- createElement(DevTools, {
387
- bridge,
388
- browserTheme: getBrowserTheme(),
389
- componentsPortalContainer,
390
- enabledInspectedElementContextMenu: true,
391
- fetchFileWithCaching,
392
- hookNamesModuleLoaderFunction,
393
- overrideTab,
394
- profilerPortalContainer,
395
- showTabBar: false,
396
- store,
397
- warnIfUnsupportedVersionDetected: true,
398
- viewAttributeSourceFunction,
399
- viewElementSourceFunction,
400
- viewUrlSourceFunction,
401
- }),
402
- );
403
- };
404
-
405
- render();
406
- }
407
-
408
- cloneStyleTags = () => {
409
- const linkTags = [];
410
- // eslint-disable-next-line no-for-of-loops/no-for-of-loops
411
- for (const linkTag of document.getElementsByTagName('link')) {
412
- if (linkTag.rel === 'stylesheet') {
413
- const newLinkTag = document.createElement('link');
414
- // eslint-disable-next-line no-for-of-loops/no-for-of-loops
415
- for (const attribute of linkTag.attributes) {
416
- newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue);
417
- }
418
- linkTags.push(newLinkTag);
419
- }
420
- }
421
- return linkTags;
422
- };
423
-
424
- initBridgeAndStore();
425
-
426
- function ensureInitialHTMLIsCleared(container) {
427
- if (container._hasInitialHTMLBeenCleared) {
428
- return;
429
- }
430
- container.innerHTML = '';
431
- container._hasInitialHTMLBeenCleared = true;
432
- }
433
-
434
- function setBrowserSelectionFromReact() {
435
- // This is currently only called on demand when you press "view DOM".
436
- // In the future, if Chrome adds an inspect() that doesn't switch tabs,
437
- // we could make this happen automatically when you select another component.
438
- chrome.devtools.inspectedWindow.eval(
439
- '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
440
- '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
441
- 'false',
442
- (didSelectionChange, evalError) => {
443
- if (evalError) {
444
- console.error(evalError);
445
- }
446
- },
447
- );
448
- }
449
-
450
- function setReactSelectionFromBrowser() {
451
- // When the user chooses a different node in the browser Elements tab,
452
- // copy it over to the hook object so that we can sync the selection.
453
- chrome.devtools.inspectedWindow.eval(
454
- '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
455
- '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
456
- 'false',
457
- (didSelectionChange, evalError) => {
458
- if (evalError) {
459
- console.error(evalError);
460
- } else if (didSelectionChange) {
461
- // Remember to sync the selection next time we show Components tab.
462
- needsToSyncElementSelection = true;
463
- }
464
- },
465
- );
466
- }
467
-
468
- setReactSelectionFromBrowser();
469
- chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
470
- setReactSelectionFromBrowser();
471
- });
472
-
473
- let currentPanel = null;
474
- let needsToSyncElementSelection = false;
475
-
476
- chrome.devtools.panels.create(
477
- IS_CHROME || IS_EDGE ? '⚛️ Components' : 'Components',
478
- IS_EDGE ? 'icons/production.svg' : '',
479
- 'panel.html',
480
- extensionPanel => {
481
- extensionPanel.onShown.addListener(panel => {
482
- if (needsToSyncElementSelection) {
483
- needsToSyncElementSelection = false;
484
- bridge.send('syncSelectionFromNativeElementsPanel');
485
- }
486
-
487
- if (currentPanel === panel) {
488
- return;
489
- }
490
-
491
- currentPanel = panel;
492
- componentsPortalContainer = panel.container;
493
-
494
- if (componentsPortalContainer != null) {
495
- ensureInitialHTMLIsCleared(componentsPortalContainer);
496
- render('components');
497
- panel.injectStyles(cloneStyleTags);
498
- logEvent({event_name: 'selected-components-tab'});
499
- }
500
- });
501
- extensionPanel.onHidden.addListener(panel => {
502
- // TODO: Stop highlighting and stuff.
503
- });
504
- },
505
- );
506
-
507
- chrome.devtools.panels.create(
508
- IS_CHROME || IS_EDGE ? '⚛️ Profiler' : 'Profiler',
509
- IS_EDGE ? 'icons/production.svg' : '',
510
- 'panel.html',
511
- extensionPanel => {
512
- extensionPanel.onShown.addListener(panel => {
513
- if (currentPanel === panel) {
514
- return;
515
- }
516
-
517
- currentPanel = panel;
518
- profilerPortalContainer = panel.container;
519
-
520
- if (profilerPortalContainer != null) {
521
- ensureInitialHTMLIsCleared(profilerPortalContainer);
522
- render('profiler');
523
- panel.injectStyles(cloneStyleTags);
524
- logEvent({event_name: 'selected-profiler-tab'});
525
- }
526
- });
527
- },
528
- );
529
-
530
- chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
531
-
532
- // Re-initialize DevTools panel when a new page is loaded.
533
- chrome.devtools.network.onNavigated.addListener(function onNavigated() {
534
- // Re-initialize saved filters on navigation,
535
- // since global values stored on window get reset in this case.
536
- syncSavedPreferences();
537
-
538
- // It's easiest to recreate the DevTools panel (to clean up potential stale state).
539
- // We can revisit this in the future as a small optimization.
540
- flushSync(() => root.unmount());
541
-
542
- initBridgeAndStore();
543
- });
544
- },
545
- );
546
-}
547
-
548
-// Load (or reload) the DevTools extension when the user navigates to a new page.
549
-function checkPageForReact() {
550
- syncSavedPreferences();
551
- createPanelIfReactLoaded();
552
-}
553
-
554
-chrome.devtools.network.onNavigated.addListener(checkPageForReact);
555
-
556
-// Check to see if React has loaded once per second in case React is added
557
-// after page load
558
-const loadCheckInterval = setInterval(function () {
559
- createPanelIfReactLoaded();
560
-}, 1000);
561
-
562
-createPanelIfReactLoaded();