From 53442d3024e1516a9c73f10b0d30fba39179a6a4 Mon Sep 17 00:00:00 2001 From: Mengdi Chen Date: Mon, 27 Mar 2023 17:24:33 -0400 Subject: [PATCH 1/5] [DevTools] improve script injection logic --- .../chrome/manifest.json | 2 +- .../edge/manifest.json | 2 +- .../src/background.js | 14 +++++- .../src/contentScripts/prepareInjection.js | 45 ++++++++----------- .../react-devtools-extensions/src/main.js | 43 +++++++++++------- 5 files changed, 59 insertions(+), 47 deletions(-) diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index 9a193e81a694d..32abe1ffaa3c0 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -4,7 +4,7 @@ "description": "Adds React debugging tools to the Chrome Developer Tools.", "version": "4.27.4", "version_name": "4.27.4", - "minimum_chrome_version": "88", + "minimum_chrome_version": "102", "icons": { "16": "icons/16-production.png", "32": "icons/32-production.png", diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index 3d09228ed7140..23b535099943f 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -4,7 +4,7 @@ "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.", "version": "4.27.4", "version_name": "4.27.4", - "minimum_chrome_version": "88", + "minimum_chrome_version": "102", "icons": { "16": "icons/16-production.png", "32": "icons/32-production.png", diff --git a/packages/react-devtools-extensions/src/background.js b/packages/react-devtools-extensions/src/background.js index 5b24734be6371..e577f40093e56 100644 --- a/packages/react-devtools-extensions/src/background.js +++ b/packages/react-devtools-extensions/src/background.js @@ -6,7 +6,7 @@ import {IS_FIREFOX} from './utils'; const ports = {}; -if (!IS_FIREFOX) { +if (!IS_FIREFOX) { // equivalent logic for Firefox is in prepareInjection.js // Manifest V3 method of injecting content scripts (not yet supported in Firefox) // Note: the "world" option in registerContentScripts is only available in Chrome v102+ // It's critical since it allows us to directly run scripts on the "main" world on the page @@ -182,5 +182,17 @@ chrome.runtime.onMessage.addListener((request, sender) => { break; } } + } else if (request.payload?.tabId) { + const tabId = request.payload?.tabId; + // This is sent from the devtools page when it is ready for injecting the backend + if (request.payload.type === 'react-devtools-inject-backend') { + if (!IS_FIREFOX) { // equivalent logic for Firefox is in prepareInjection.js + chrome.scripting.executeScript({ + target: {tabId}, + files: ['/build/react_devtools_backend.js'], + world: chrome.scripting.ExecutionWorld.MAIN + }); + } + } } }); diff --git a/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js b/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js index dfa9ef64f4d83..7c136aee58b67 100644 --- a/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js +++ b/packages/react-devtools-extensions/src/contentScripts/prepareInjection.js @@ -3,6 +3,12 @@ import nullthrows from 'nullthrows'; import {IS_FIREFOX} from '../utils'; +// We run scripts on the page via the service worker (backgroud.js) for +// Manifest V3 extensions (Chrome & Edge). +// We need to inject this code for Firefox only because it does not support ExecutionWorld.MAIN +// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld +// In this content script we have access to DOM, but don't have access to the webpage's window, +// so we inject this inline script tag into the webpage (allowed in Manifest V2). function injectScriptSync(src) { let code = ''; const request = new XMLHttpRequest(); @@ -21,13 +27,6 @@ function injectScriptSync(src) { nullthrows(script.parentNode).removeChild(script); } -function injectScriptAsync(src) { - const script = document.createElement('script'); - script.src = src; - nullthrows(document.documentElement).appendChild(script); - nullthrows(script.parentNode).removeChild(script); -} - let lastDetectionResult; // We want to detect when a renderer attaches, and notify the "background page" @@ -90,9 +89,11 @@ window.addEventListener('message', function onMessage({data, source}) { } break; case 'react-devtools-inject-backend': - injectScriptAsync( - chrome.runtime.getURL('build/react_devtools_backend.js'), - ); + if (IS_FIREFOX) { + injectScriptSync( + chrome.runtime.getURL('build/react_devtools_backend.js'), + ); + } break; } }); @@ -108,25 +109,15 @@ window.addEventListener('pageshow', function ({target}) { chrome.runtime.sendMessage(lastDetectionResult); }); -// We create a "sync" script tag to page to inject the global hook on Manifest V2 extensions. -// To comply with the new security policy in V3, we use chrome.scripting.registerContentScripts instead (see background.js). -// However, the new API only works for Chrome v102+. -// We insert a "async" script tag as a fallback for older versions. -// It has known issues if JS on the page is faster than the extension. -// Users will see a notice in components tab when that happens (see ). -// For Firefox, V3 is not ready, so sync injection is still the best approach. -const injectScript = IS_FIREFOX ? injectScriptSync : injectScriptAsync; - // Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with. // Only do this for HTML documents though, to avoid e.g. breaking syntax highlighting for XML docs. -// We need to inject this code because content scripts (ie injectGlobalHook.js) don't have access -// to the webpage's window, so in order to access front end settings -// and communicate with React, we must inject this code into the webpage -switch (document.contentType) { - case 'text/html': - case 'application/xhtml+xml': { - injectScript(chrome.runtime.getURL('build/installHook.js')); - break; +if (IS_FIREFOX) { + switch (document.contentType) { + case 'text/html': + case 'application/xhtml+xml': { + injectScriptSync(chrome.runtime.getURL('build/installHook.js')); + break; + } } } diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index eccea0177b26e..8fed09adc8064 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -5,7 +5,7 @@ import {flushSync} from 'react-dom'; import {createRoot} from 'react-dom/client'; import Bridge from 'react-devtools-shared/src/bridge'; import Store from 'react-devtools-shared/src/devtools/store'; -import {getBrowserName, getBrowserTheme} from './utils'; +import {IS_FIREFOX, IS_CHROME, IS_EDGE, getBrowserTheme} from './utils'; import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants'; import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDevToolsEventLogger'; import { @@ -27,9 +27,6 @@ import {logEvent} from 'react-devtools-shared/src/Logger'; const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY = 'React::DevTools::supportsProfiling'; -const isChrome = getBrowserName() === 'Chrome'; -const isEdge = getBrowserName() === 'Edge'; - // rAF never fires on devtools_page (because it's in the background) // https://bugs.chromium.org/p/chromium/issues/detail?id=1241986#c31 // Since we render React elements here, we need to polyfill it with setTimeout @@ -176,10 +173,10 @@ function createPanelIfReactLoaded() { store = new Store(bridge, { isProfiling, - supportsReloadAndProfile: isChrome || isEdge, + supportsReloadAndProfile: IS_CHROME || IS_EDGE, supportsProfiling, // At this time, the timeline can only parse Chrome performance profiles. - supportsTimeline: isChrome, + supportsTimeline: IS_CHROME, supportsTraceUpdates: true, }); if (!isProfiling) { @@ -188,14 +185,26 @@ function createPanelIfReactLoaded() { // Initialize the backend only once the Store has been initialized. // Otherwise the Store may miss important initial tree op codes. - chrome.devtools.inspectedWindow.eval( - `window.postMessage({ source: 'react-devtools-inject-backend' }, '*');`, - function (response, evalError) { - if (evalError) { - console.error(evalError); - } - }, - ); + if (IS_CHROME | IS_EDGE) { + chrome.runtime.sendMessage({ + source: 'react-devtools-main', + payload: { + type: 'react-devtools-inject-backend', + tabId, + }, + }); + } else { + // Firefox does not support executing script in MAIN wolrd from content script. + // see prepareInjection.js + chrome.devtools.inspectedWindow.eval( + `window.postMessage({ source: 'react-devtools-inject-backend' }, '*');`, + function (response, evalError) { + if (evalError) { + console.error(evalError); + } + }, + ); + } const viewAttributeSourceFunction = (id, path) => { const rendererID = store.getRendererIDForElement(id); @@ -255,7 +264,7 @@ function createPanelIfReactLoaded() { // For some reason in Firefox, chrome.runtime.sendMessage() from a content script // never reaches the chrome.runtime.onMessage event listener. let fetchFileWithCaching = null; - if (isChrome) { + if (IS_CHROME) { const fetchFromNetworkCache = (url, resolve, reject) => { // Debug ID allows us to avoid re-logging (potentially long) URL strings below, // while also still associating (potentially) interleaved logs with the original request. @@ -463,7 +472,7 @@ function createPanelIfReactLoaded() { let needsToSyncElementSelection = false; chrome.devtools.panels.create( - isChrome || isEdge ? '⚛️ Components' : 'Components', + IS_CHROME || IS_EDGE ? '⚛️ Components' : 'Components', '', 'panel.html', extensionPanel => { @@ -494,7 +503,7 @@ function createPanelIfReactLoaded() { ); chrome.devtools.panels.create( - isChrome || isEdge ? '⚛️ Profiler' : 'Profiler', + IS_CHROME || IS_EDGE ? '⚛️ Profiler' : 'Profiler', '', 'panel.html', extensionPanel => { From 4b609805985bcb45355dfefaef3ba88048396f69 Mon Sep 17 00:00:00 2001 From: Mengdi Chen Date: Tue, 28 Mar 2023 10:33:52 -0400 Subject: [PATCH 2/5] fix lint --- packages/react-devtools-extensions/src/background.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/react-devtools-extensions/src/background.js b/packages/react-devtools-extensions/src/background.js index e577f40093e56..bab24ddf5fe5c 100644 --- a/packages/react-devtools-extensions/src/background.js +++ b/packages/react-devtools-extensions/src/background.js @@ -6,7 +6,8 @@ import {IS_FIREFOX} from './utils'; const ports = {}; -if (!IS_FIREFOX) { // equivalent logic for Firefox is in prepareInjection.js +if (!IS_FIREFOX) { + // equivalent logic for Firefox is in prepareInjection.js // Manifest V3 method of injecting content scripts (not yet supported in Firefox) // Note: the "world" option in registerContentScripts is only available in Chrome v102+ // It's critical since it allows us to directly run scripts on the "main" world on the page @@ -186,11 +187,12 @@ chrome.runtime.onMessage.addListener((request, sender) => { const tabId = request.payload?.tabId; // This is sent from the devtools page when it is ready for injecting the backend if (request.payload.type === 'react-devtools-inject-backend') { - if (!IS_FIREFOX) { // equivalent logic for Firefox is in prepareInjection.js + if (!IS_FIREFOX) { + // equivalent logic for Firefox is in prepareInjection.js chrome.scripting.executeScript({ target: {tabId}, files: ['/build/react_devtools_backend.js'], - world: chrome.scripting.ExecutionWorld.MAIN + world: chrome.scripting.ExecutionWorld.MAIN, }); } } From 0904f8d38580a30d989d03c6ca611b78c3c12b72 Mon Sep 17 00:00:00 2001 From: Mengdi Chen Date: Tue, 28 Mar 2023 10:49:26 -0400 Subject: [PATCH 3/5] fix lint --- packages/react-devtools-extensions/src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index 8fed09adc8064..eb7afa4b2cc22 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -5,7 +5,7 @@ import {flushSync} from 'react-dom'; import {createRoot} from 'react-dom/client'; import Bridge from 'react-devtools-shared/src/bridge'; import Store from 'react-devtools-shared/src/devtools/store'; -import {IS_FIREFOX, IS_CHROME, IS_EDGE, getBrowserTheme} from './utils'; +import {IS_CHROME, IS_EDGE, getBrowserTheme} from './utils'; import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants'; import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDevToolsEventLogger'; import { From 096811da6e22dccc08fad9b9abdf22d1c33ce529 Mon Sep 17 00:00:00 2001 From: Mengdi Chen Date: Tue, 28 Mar 2023 11:48:43 -0400 Subject: [PATCH 4/5] fix per comment --- packages/react-devtools-extensions/src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index eb7afa4b2cc22..a1938b21ec861 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -185,7 +185,7 @@ function createPanelIfReactLoaded() { // Initialize the backend only once the Store has been initialized. // Otherwise the Store may miss important initial tree op codes. - if (IS_CHROME | IS_EDGE) { + if (IS_CHROME || IS_EDGE) { chrome.runtime.sendMessage({ source: 'react-devtools-main', payload: { From 002d8554640e409a2595ba2267b6a8a4323a95ac Mon Sep 17 00:00:00 2001 From: Mengdi Chen Date: Tue, 28 Mar 2023 12:02:06 -0400 Subject: [PATCH 5/5] comment nit Co-authored-by: Ruslan Lesiutin --- packages/react-devtools-extensions/src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index a1938b21ec861..95d36f1915e53 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -194,7 +194,7 @@ function createPanelIfReactLoaded() { }, }); } else { - // Firefox does not support executing script in MAIN wolrd from content script. + // Firefox does not support executing script in ExecutionWorld.MAIN from content script. // see prepareInjection.js chrome.devtools.inspectedWindow.eval( `window.postMessage({ source: 'react-devtools-inject-backend' }, '*');`,