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