@samitouri / QOS-React / commits / 5a31758ed6

[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 }