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..bab24ddf5fe5c 100644 --- a/packages/react-devtools-extensions/src/background.js +++ b/packages/react-devtools-extensions/src/background.js @@ -7,6 +7,7 @@ import {IS_FIREFOX} from './utils'; const ports = {}; 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 +183,18 @@ 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..95d36f1915e53 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_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 ExecutionWorld.MAIN 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 => {