@samitouri / QOS-React-2 / commits / f718199313

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

Mengdi Chen committed Mar 28, 2023 at 12:45 UTC f7181993133a89b54f93d79d199be3392e5af6fc
5 files changed +60 -46
packages/react-devtools-extensions/chrome/manifest.json
+1 -1
@@ -4,7 +4,7 @@
4 "description": "Adds React debugging tools to the Chrome Developer Tools.",
5 "version": "4.27.4",
6 "version_name": "4.27.4",
7 - "minimum_chrome_version": "88",
7 + "minimum_chrome_version": "102",
8 "icons": {
9 "16": "icons/16-production.png",
10 "32": "icons/32-production.png",
packages/react-devtools-extensions/edge/manifest.json
+1 -1
@@ -4,7 +4,7 @@
4 "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
5 "version": "4.27.4",
6 "version_name": "4.27.4",
7 - "minimum_chrome_version": "88",
7 + "minimum_chrome_version": "102",
8 "icons": {
9 "16": "icons/16-production.png",
10 "32": "icons/32-production.png",
packages/react-devtools-extensions/src/background.js
+14
@@ -7,6 +7,7 @@ import {IS_FIREFOX} from './utils';
7 const ports = {};
8
9 if (!IS_FIREFOX) {
10 + // equivalent logic for Firefox is in prepareInjection.js
11 // Manifest V3 method of injecting content scripts (not yet supported in Firefox)
12 // Note: the "world" option in registerContentScripts is only available in Chrome v102+
13 // 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) => {
183 break;
184 }
185 }
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 + }
199 }
200 });
packages/react-devtools-extensions/src/contentScripts/prepareInjection.js
+18 -27
@@ -3,6 +3,12 @@
3 import nullthrows from 'nullthrows';
4 import {IS_FIREFOX} from '../utils';
5
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).
12 function injectScriptSync(src) {
13 let code = '';
14 const request = new XMLHttpRequest();
@@ -21,13 +27,6 @@ function injectScriptSync(src) {
27 nullthrows(script.parentNode).removeChild(script);
28 }
29
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 -
30 let lastDetectionResult;
31
32 // We want to detect when a renderer attaches, and notify the "background page"
@@ -90,9 +89,11 @@ window.addEventListener('message', function onMessage({data, source}) {
89 }
90 break;
91 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 + }
97 break;
98 }
99 });
@@ -108,25 +109,15 @@ window.addEventListener('pageshow', function ({target}) {
109 chrome.runtime.sendMessage(lastDetectionResult);
110 });
111
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 -
112 // Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with.
113 // 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 + }
121 }
122 }
123
packages/react-devtools-extensions/src/main.js
+26 -17
@@ -5,7 +5,7 @@ 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 {getBrowserName, getBrowserTheme} from './utils';
8 +import {IS_CHROME, IS_EDGE, getBrowserTheme} from './utils';
9 import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants';
10 import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDevToolsEventLogger';
11 import {
@@ -27,9 +27,6 @@ import {logEvent} from 'react-devtools-shared/src/Logger';
27 const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
28 'React::DevTools::supportsProfiling';
29
30 -const isChrome = getBrowserName() === 'Chrome';
31 -const isEdge = getBrowserName() === 'Edge';
32 -
30 // rAF never fires on devtools_page (because it's in the background)
31 // https://bugs.chromium.org/p/chromium/issues/detail?id=1241986#c31
32 // Since we render React elements here, we need to polyfill it with setTimeout
@@ -176,10 +173,10 @@ function createPanelIfReactLoaded() {
173
174 store = new Store(bridge, {
175 isProfiling,
179 - supportsReloadAndProfile: isChrome || isEdge,
176 + supportsReloadAndProfile: IS_CHROME || IS_EDGE,
177 supportsProfiling,
178 // At this time, the timeline can only parse Chrome performance profiles.
182 - supportsTimeline: isChrome,
179 + supportsTimeline: IS_CHROME,
180 supportsTraceUpdates: true,
181 });
182 if (!isProfiling) {
@@ -188,14 +185,26 @@ function createPanelIfReactLoaded() {
185
186 // Initialize the backend only once the Store has been initialized.
187 // 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 + }
208
209 const viewAttributeSourceFunction = (id, path) => {
210 const rendererID = store.getRendererIDForElement(id);
@@ -255,7 +264,7 @@ function createPanelIfReactLoaded() {
264 // For some reason in Firefox, chrome.runtime.sendMessage() from a content script
265 // never reaches the chrome.runtime.onMessage event listener.
266 let fetchFileWithCaching = null;
258 - if (isChrome) {
267 + if (IS_CHROME) {
268 const fetchFromNetworkCache = (url, resolve, reject) => {
269 // Debug ID allows us to avoid re-logging (potentially long) URL strings below,
270 // while also still associating (potentially) interleaved logs with the original request.
@@ -463,7 +472,7 @@ function createPanelIfReactLoaded() {
472 let needsToSyncElementSelection = false;
473
474 chrome.devtools.panels.create(
466 - isChrome || isEdge ? '⚛️ Components' : 'Components',
475 + IS_CHROME || IS_EDGE ? '⚛️ Components' : 'Components',
476 '',
477 'panel.html',
478 extensionPanel => {
@@ -494,7 +503,7 @@ function createPanelIfReactLoaded() {
503 );
504
505 chrome.devtools.panels.create(
497 - isChrome || isEdge ? '⚛️ Profiler' : 'Profiler',
506 + IS_CHROME || IS_EDGE ? '⚛️ Profiler' : 'Profiler',
507 '',
508 'panel.html',
509 extensionPanel => {