Skip to content

Commit f718199

Browse files
mondaychenhoxyq
andauthored
[DevTools] browser extension: improve script injection logic (#26492)
## Summary - Drop extension support for Chrome / Edge <v102 since they have less than 0.1% usage ([see data](https://caniuse.com/usage-table)) - Improve script injection logic when possible so that the scripts injected by the extension are no longer shown in Network (which caused a lot of confusion in the past) ## How did you test this change? Built and tested locally, works as usual on Firefox. For Chrome/Edge **Before:** Scripts shown in Network tab <img width="1279" alt="Untitled 2" src="https://user-images.githubusercontent.com/1001890/228074363-1d00d503-d4b5-4339-8dd6-fd0467e36e3e.png"> **After:** No scripts shown <img width="1329" alt="image" src="https://user-images.githubusercontent.com/1001890/228074596-2084722b-bf3c-495e-a852-15f122233155.png"> --------- Co-authored-by: Ruslan Lesiutin <rdlesyutin@gmail.com>
1 parent 41b4714 commit f718199

File tree

5 files changed

+60
-46
lines changed

5 files changed

+60
-46
lines changed

packages/react-devtools-extensions/chrome/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Adds React debugging tools to the Chrome Developer Tools.",
55
"version": "4.27.4",
66
"version_name": "4.27.4",
7-
"minimum_chrome_version": "88",
7+
"minimum_chrome_version": "102",
88
"icons": {
99
"16": "icons/16-production.png",
1010
"32": "icons/32-production.png",

packages/react-devtools-extensions/edge/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
55
"version": "4.27.4",
66
"version_name": "4.27.4",
7-
"minimum_chrome_version": "88",
7+
"minimum_chrome_version": "102",
88
"icons": {
99
"16": "icons/16-production.png",
1010
"32": "icons/32-production.png",

packages/react-devtools-extensions/src/background.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {IS_FIREFOX} from './utils';
77
const ports = {};
88

99
if (!IS_FIREFOX) {
10+
// equivalent logic for Firefox is in prepareInjection.js
1011
// Manifest V3 method of injecting content scripts (not yet supported in Firefox)
1112
// Note: the "world" option in registerContentScripts is only available in Chrome v102+
1213
// It's critical since it allows us to directly run scripts on the "main" world on the page
@@ -182,5 +183,18 @@ chrome.runtime.onMessage.addListener((request, sender) => {
182183
break;
183184
}
184185
}
186+
} else if (request.payload?.tabId) {
187+
const tabId = request.payload?.tabId;
188+
// This is sent from the devtools page when it is ready for injecting the backend
189+
if (request.payload.type === 'react-devtools-inject-backend') {
190+
if (!IS_FIREFOX) {
191+
// equivalent logic for Firefox is in prepareInjection.js
192+
chrome.scripting.executeScript({
193+
target: {tabId},
194+
files: ['/build/react_devtools_backend.js'],
195+
world: chrome.scripting.ExecutionWorld.MAIN,
196+
});
197+
}
198+
}
185199
}
186200
});

packages/react-devtools-extensions/src/contentScripts/prepareInjection.js

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
import nullthrows from 'nullthrows';
44
import {IS_FIREFOX} from '../utils';
55

6+
// We run scripts on the page via the service worker (backgroud.js) for
7+
// Manifest V3 extensions (Chrome & Edge).
8+
// We need to inject this code for Firefox only because it does not support ExecutionWorld.MAIN
9+
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld
10+
// In this content script we have access to DOM, but don't have access to the webpage's window,
11+
// so we inject this inline script tag into the webpage (allowed in Manifest V2).
612
function injectScriptSync(src) {
713
let code = '';
814
const request = new XMLHttpRequest();
@@ -21,13 +27,6 @@ function injectScriptSync(src) {
2127
nullthrows(script.parentNode).removeChild(script);
2228
}
2329

24-
function injectScriptAsync(src) {
25-
const script = document.createElement('script');
26-
script.src = src;
27-
nullthrows(document.documentElement).appendChild(script);
28-
nullthrows(script.parentNode).removeChild(script);
29-
}
30-
3130
let lastDetectionResult;
3231

3332
// We want to detect when a renderer attaches, and notify the "background page"
@@ -90,9 +89,11 @@ window.addEventListener('message', function onMessage({data, source}) {
9089
}
9190
break;
9291
case 'react-devtools-inject-backend':
93-
injectScriptAsync(
94-
chrome.runtime.getURL('build/react_devtools_backend.js'),
95-
);
92+
if (IS_FIREFOX) {
93+
injectScriptSync(
94+
chrome.runtime.getURL('build/react_devtools_backend.js'),
95+
);
96+
}
9697
break;
9798
}
9899
});
@@ -108,25 +109,15 @@ window.addEventListener('pageshow', function ({target}) {
108109
chrome.runtime.sendMessage(lastDetectionResult);
109110
});
110111

111-
// We create a "sync" script tag to page to inject the global hook on Manifest V2 extensions.
112-
// To comply with the new security policy in V3, we use chrome.scripting.registerContentScripts instead (see background.js).
113-
// However, the new API only works for Chrome v102+.
114-
// We insert a "async" script tag as a fallback for older versions.
115-
// It has known issues if JS on the page is faster than the extension.
116-
// Users will see a notice in components tab when that happens (see <Tree>).
117-
// For Firefox, V3 is not ready, so sync injection is still the best approach.
118-
const injectScript = IS_FIREFOX ? injectScriptSync : injectScriptAsync;
119-
120112
// Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with.
121113
// Only do this for HTML documents though, to avoid e.g. breaking syntax highlighting for XML docs.
122-
// We need to inject this code because content scripts (ie injectGlobalHook.js) don't have access
123-
// to the webpage's window, so in order to access front end settings
124-
// and communicate with React, we must inject this code into the webpage
125-
switch (document.contentType) {
126-
case 'text/html':
127-
case 'application/xhtml+xml': {
128-
injectScript(chrome.runtime.getURL('build/installHook.js'));
129-
break;
114+
if (IS_FIREFOX) {
115+
switch (document.contentType) {
116+
case 'text/html':
117+
case 'application/xhtml+xml': {
118+
injectScriptSync(chrome.runtime.getURL('build/installHook.js'));
119+
break;
120+
}
130121
}
131122
}
132123

packages/react-devtools-extensions/src/main.js

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {flushSync} from 'react-dom';
55
import {createRoot} from 'react-dom/client';
66
import Bridge from 'react-devtools-shared/src/bridge';
77
import Store from 'react-devtools-shared/src/devtools/store';
8-
import {getBrowserName, getBrowserTheme} from './utils';
8+
import {IS_CHROME, IS_EDGE, getBrowserTheme} from './utils';
99
import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants';
1010
import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDevToolsEventLogger';
1111
import {
@@ -27,9 +27,6 @@ import {logEvent} from 'react-devtools-shared/src/Logger';
2727
const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
2828
'React::DevTools::supportsProfiling';
2929

30-
const isChrome = getBrowserName() === 'Chrome';
31-
const isEdge = getBrowserName() === 'Edge';
32-
3330
// rAF never fires on devtools_page (because it's in the background)
3431
// https://bugs.chromium.org/p/chromium/issues/detail?id=1241986#c31
3532
// Since we render React elements here, we need to polyfill it with setTimeout
@@ -176,10 +173,10 @@ function createPanelIfReactLoaded() {
176173

177174
store = new Store(bridge, {
178175
isProfiling,
179-
supportsReloadAndProfile: isChrome || isEdge,
176+
supportsReloadAndProfile: IS_CHROME || IS_EDGE,
180177
supportsProfiling,
181178
// At this time, the timeline can only parse Chrome performance profiles.
182-
supportsTimeline: isChrome,
179+
supportsTimeline: IS_CHROME,
183180
supportsTraceUpdates: true,
184181
});
185182
if (!isProfiling) {
@@ -188,14 +185,26 @@ function createPanelIfReactLoaded() {
188185

189186
// Initialize the backend only once the Store has been initialized.
190187
// Otherwise the Store may miss important initial tree op codes.
191-
chrome.devtools.inspectedWindow.eval(
192-
`window.postMessage({ source: 'react-devtools-inject-backend' }, '*');`,
193-
function (response, evalError) {
194-
if (evalError) {
195-
console.error(evalError);
196-
}
197-
},
198-
);
188+
if (IS_CHROME || IS_EDGE) {
189+
chrome.runtime.sendMessage({
190+
source: 'react-devtools-main',
191+
payload: {
192+
type: 'react-devtools-inject-backend',
193+
tabId,
194+
},
195+
});
196+
} else {
197+
// Firefox does not support executing script in ExecutionWorld.MAIN from content script.
198+
// see prepareInjection.js
199+
chrome.devtools.inspectedWindow.eval(
200+
`window.postMessage({ source: 'react-devtools-inject-backend' }, '*');`,
201+
function (response, evalError) {
202+
if (evalError) {
203+
console.error(evalError);
204+
}
205+
},
206+
);
207+
}
199208

200209
const viewAttributeSourceFunction = (id, path) => {
201210
const rendererID = store.getRendererIDForElement(id);
@@ -255,7 +264,7 @@ function createPanelIfReactLoaded() {
255264
// For some reason in Firefox, chrome.runtime.sendMessage() from a content script
256265
// never reaches the chrome.runtime.onMessage event listener.
257266
let fetchFileWithCaching = null;
258-
if (isChrome) {
267+
if (IS_CHROME) {
259268
const fetchFromNetworkCache = (url, resolve, reject) => {
260269
// Debug ID allows us to avoid re-logging (potentially long) URL strings below,
261270
// while also still associating (potentially) interleaved logs with the original request.
@@ -463,7 +472,7 @@ function createPanelIfReactLoaded() {
463472
let needsToSyncElementSelection = false;
464473

465474
chrome.devtools.panels.create(
466-
isChrome || isEdge ? '⚛️ Components' : 'Components',
475+
IS_CHROME || IS_EDGE ? '⚛️ Components' : 'Components',
467476
'',
468477
'panel.html',
469478
extensionPanel => {
@@ -494,7 +503,7 @@ function createPanelIfReactLoaded() {
494503
);
495504

496505
chrome.devtools.panels.create(
497-
isChrome || isEdge ? '⚛️ Profiler' : 'Profiler',
506+
IS_CHROME || IS_EDGE ? '⚛️ Profiler' : 'Profiler',
498507
'',
499508
'panel.html',
500509
extensionPanel => {

0 commit comments

Comments
 (0)