[DevTools] Allow inspection before streaming has finished in Chrome (#34360)
Sebastian "Sebbie" Silbermann committed
Sep 4, 2025 at 12:21 UTC
5a31758ed626d1c8aa68b0d2491256020094993a
5 files changed
+61
-9
.eslintrc.js
+1
@@ -577,6 +577,7 @@ module.exports = {
577
$AsyncIterator: 'readonly',
578
Iterator: 'readonly',
579
AsyncIterator: 'readonly',
580
+ IntervalID: 'readonly',
581
IteratorResult: 'readonly',
582
JSONValue: 'readonly',
583
JSResourceReference: 'readonly',
packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js
+1
-1
@@ -6,7 +6,7 @@ const contentScriptsToInject = [
6
js: ['build/proxy.js'],
7
matches: ['<all_urls>'],
8
persistAcrossSessions: true,
9
- runAt: 'document_end',
9
+ runAt: 'document_start',
10
world: chrome.scripting.ExecutionWorld.ISOLATED,
11
},
12
{
packages/react-devtools-extensions/src/background/executeScript.js
+26
-2
@@ -1,6 +1,20 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
/* global chrome */
10
3
-export function executeScriptInIsolatedWorld({target, files}) {
11
+export function executeScriptInIsolatedWorld({
12
+ target,
13
+ files,
14
+}: {
15
+ files: any,
16
+ target: any,
17
+}): Promise<void> {
18
return chrome.scripting.executeScript({
19
target,
20
files,
@@ -8,10 +22,20 @@ export function executeScriptInIsolatedWorld({target, files}) {
22
});
23
}
24
11
-export function executeScriptInMainWorld({target, files}) {
25
+export function executeScriptInMainWorld({
26
+ target,
27
+ files,
28
+ injectImmediately,
29
+}: {
30
+ files: any,
31
+ target: any,
32
+ // It's nice to have this required to make active choices.
33
+ injectImmediately: boolean,
34
+}): Promise<void> {
35
return chrome.scripting.executeScript({
36
target,
37
files,
38
+ injectImmediately,
39
world: chrome.scripting.ExecutionWorld.MAIN,
40
});
41
}
packages/react-devtools-extensions/src/background/messageHandlers.js
+13
-1
@@ -1,5 +1,6 @@
1
/* global chrome */
2
3
+import {__DEBUG__} from 'react-devtools-shared/src/constants';
4
import setExtensionIconAndPopup from './setExtensionIconAndPopup';
5
import {executeScriptInMainWorld} from './executeScript';
6
@@ -25,6 +26,7 @@ export function handleBackendManagerMessage(message, sender) {
26
payload.versions.forEach(version => {
27
if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
28
executeScriptInMainWorld({
29
+ injectImmediately: true,
30
target: {tabId: sender.tab.id},
31
files: [`/build/react_devtools_backend_${version}.js`],
32
});
@@ -79,9 +81,19 @@ export function handleDevToolsPageMessage(message) {
81
}
82
83
executeScriptInMainWorld({
84
+ injectImmediately: true,
85
target: {tabId},
86
files: ['/build/backendManager.js'],
84
- });
87
+ }).then(
88
+ () => {
89
+ if (__DEBUG__) {
90
+ console.log('Successfully injected backend manager');
91
+ }
92
+ },
93
+ reason => {
94
+ console.error('Failed to inject backend manager:', reason);
95
+ },
96
+ );
97
98
break;
99
}
packages/react-devtools-extensions/src/contentScripts/proxy.js
+20
-5
@@ -1,8 +1,16 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
/* global chrome */
10
11
'use strict';
12
5
-window.addEventListener('pageshow', function ({target}) {
13
+function injectProxy({target}: {target: any}) {
14
// Firefox's behaviour for injecting this content script can be unpredictable
15
// While navigating the history, some content scripts might not be re-injected and still be alive
16
if (!window.__REACT_DEVTOOLS_PROXY_INJECTED__) {
@@ -14,7 +22,7 @@ window.addEventListener('pageshow', function ({target}) {
22
// The backend waits to install the global hook until notified by the content script.
23
// In the event of a page reload, the content script might be loaded before the backend manager is injected.
24
// Because of this we need to poll the backend manager until it has been initialized.
17
- const intervalID = setInterval(() => {
25
+ const intervalID: IntervalID = setInterval(() => {
26
if (backendInitialized) {
27
clearInterval(intervalID);
28
} else {
@@ -22,7 +30,11 @@ window.addEventListener('pageshow', function ({target}) {
30
}
31
}, 500);
32
}
25
-});
33
+}
34
+
35
+window.addEventListener('pagereveal', injectProxy);
36
+// For backwards compat with browsers not implementing `pagereveal` which is a fairly new event.
37
+window.addEventListener('pageshow', injectProxy);
38
39
window.addEventListener('pagehide', function ({target}) {
40
if (target !== window.document) {
@@ -45,7 +57,7 @@ function sayHelloToBackendManager() {
57
);
58
}
59
48
-function handleMessageFromDevtools(message) {
60
+function handleMessageFromDevtools(message: any) {
61
window.postMessage(
62
{
63
source: 'react-devtools-content-script',
@@ -55,7 +67,7 @@ function handleMessageFromDevtools(message) {
67
);
68
}
69
58
-function handleMessageFromPage(event) {
70
+function handleMessageFromPage(event: any) {
71
if (event.source !== window || !event.data) {
72
return;
73
}
@@ -65,6 +77,7 @@ function handleMessageFromPage(event) {
77
case 'react-devtools-bridge': {
78
backendInitialized = true;
79
80
+ // $FlowFixMe[incompatible-use]
81
port.postMessage(event.data.payload);
82
break;
83
}
@@ -99,6 +112,8 @@ function connectPort() {
112
113
window.addEventListener('message', handleMessageFromPage);
114
115
+ // $FlowFixMe[incompatible-use]
116
port.onMessage.addListener(handleMessageFromDevtools);
117
+ // $FlowFixMe[incompatible-use]
118
port.onDisconnect.addListener(handleDisconnect);
119
}