diff --git a/packages/react-devtools-extensions/build.js b/packages/react-devtools-extensions/build.js index 4701bbdcba9b1..eb39056eaa606 100644 --- a/packages/react-devtools-extensions/build.js +++ b/packages/react-devtools-extensions/build.js @@ -102,17 +102,6 @@ const build = async (tempPath, manifestPath) => { } manifest.description += `\n\nCreated from revision ${commit} on ${dateString}.`; - if (process.env.NODE_ENV === 'development') { - // When building the local development version of the - // extension we want to be able to have a stable extension ID - // for the local build (in order to be able to reliably detect - // duplicate installations of DevTools). - // By specifying a key in the built manifest.json file, - // we can make it so the generated extension ID is stable. - // For more details see the docs here: https://developer.chrome.com/docs/extensions/mv2/manifest/key/ - manifest.key = 'reactdevtoolslocalbuilduniquekey'; - } - writeFileSync(copiedManifestPath, JSON.stringify(manifest, null, 2)); // Pack the extension diff --git a/packages/react-devtools-extensions/package.json b/packages/react-devtools-extensions/package.json index 3a6bdfa7c044c..7d1f018a6bb2b 100644 --- a/packages/react-devtools-extensions/package.json +++ b/packages/react-devtools-extensions/package.json @@ -4,15 +4,15 @@ "private": true, "scripts": { "build": "cross-env NODE_ENV=production yarn run build:chrome && yarn run build:firefox && yarn run build:edge", - "build:local": "cross-env NODE_ENV=development yarn run build:chrome:local && yarn run build:firefox:local && yarn run build:edge:local", + "build:dev": "cross-env NODE_ENV=development yarn run build:chrome:dev && yarn run build:firefox:dev && yarn run build:edge:dev", "build:chrome": "cross-env NODE_ENV=production node ./chrome/build", "build:chrome:fb": "cross-env NODE_ENV=production FEATURE_FLAG_TARGET=extension-fb node ./chrome/build --crx", - "build:chrome:local": "cross-env NODE_ENV=development node ./chrome/build", + "build:chrome:dev": "cross-env NODE_ENV=development node ./chrome/build", "build:firefox": "cross-env NODE_ENV=production node ./firefox/build", - "build:firefox:local": "cross-env NODE_ENV=development node ./firefox/build", + "build:firefox:dev": "cross-env NODE_ENV=development node ./firefox/build", "build:edge": "cross-env NODE_ENV=production node ./edge/build", "build:edge:fb": "cross-env NODE_ENV=production FEATURE_FLAG_TARGET=extension-fb node ./edge/build --crx", - "build:edge:local": "cross-env NODE_ENV=development node ./edge/build", + "build:edge:dev": "cross-env NODE_ENV=development node ./edge/build", "test:chrome": "node ./chrome/test", "test:firefox": "node ./firefox/test", "test:edge": "node ./edge/test", diff --git a/packages/react-devtools-extensions/src/background.js b/packages/react-devtools-extensions/src/background.js index 85595d26010bf..9e09513b78fb4 100644 --- a/packages/react-devtools-extensions/src/background.js +++ b/packages/react-devtools-extensions/src/background.js @@ -6,8 +6,6 @@ const ports = {}; const IS_FIREFOX = navigator.userAgent.indexOf('Firefox') >= 0; -import {EXTENSION_INSTALL_CHECK_MESSAGE} from './constants'; - chrome.runtime.onConnect.addListener(function(port) { let tab = null; let name = null; @@ -118,14 +116,6 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { } }); -chrome.runtime.onMessageExternal.addListener( - (request, sender, sendResponse) => { - if (request === EXTENSION_INSTALL_CHECK_MESSAGE) { - sendResponse(true); - } - }, -); - chrome.runtime.onMessage.addListener((request, sender) => { const tab = sender.tab; if (tab) { diff --git a/packages/react-devtools-extensions/src/checkForDuplicateInstallations.js b/packages/react-devtools-extensions/src/checkForDuplicateInstallations.js deleted file mode 100644 index e54476283cc61..0000000000000 --- a/packages/react-devtools-extensions/src/checkForDuplicateInstallations.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict-local - */ - -declare var chrome: any; - -import {__DEBUG__} from 'react-devtools-shared/src/constants'; -import { - EXTENSION_INSTALL_CHECK_MESSAGE, - EXTENSION_INSTALLATION_TYPE, - INTERNAL_EXTENSION_ID, - LOCAL_EXTENSION_ID, -} from './constants'; - -const UNRECOGNIZED_EXTENSION_WARNING = - 'React Developer Tools: You are running an unrecognized installation of the React Developer Tools extension, which might conflict with other versions of the extension installed in your browser. ' + - 'Please make sure you only have a single version of the extension installed or enabled. ' + - 'If you are developing this extension locally, make sure to build the extension using the `yarn build::local` command.'; - -export function checkForDuplicateInstallations(callback: boolean => void) { - switch (EXTENSION_INSTALLATION_TYPE) { - case 'public': { - // If this is the public extension (e.g. from Chrome Web Store), check if an internal - // or local build of the extension is also installed, and if so, disable this extension. - // TODO show warning if other installations are present. - checkForInstalledExtensions([ - INTERNAL_EXTENSION_ID, - LOCAL_EXTENSION_ID, - ]).then(areExtensionsInstalled => { - if (areExtensionsInstalled.some(isInstalled => isInstalled)) { - callback(true); - } else { - callback(false); - } - }); - break; - } - case 'internal': { - // If this is the internal extension, check if a local build of the extension - // is also installed, and if so, disable this extension. - // If the public version of the extension is also installed, that extension - // will disable itself. - // TODO show warning if other installations are present. - checkForInstalledExtension(LOCAL_EXTENSION_ID).then(isInstalled => { - if (isInstalled) { - callback(true); - } else { - callback(false); - } - }); - break; - } - case 'local': { - if (__DEV__) { - // If this is the local extension (i.e. built locally during development), - // always keep this one enabled. Other installations disable themselves if - // they detect the local build is installed. - callback(false); - break; - } - - // If this extension wasn't built locally during development, we can't reliably - // detect if there are other installations of DevTools present. - // In this case, assume there are no duplicate exensions and show a warning about - // potential conflicts. - console.error(UNRECOGNIZED_EXTENSION_WARNING); - chrome.devtools.inspectedWindow.eval( - `console.error("${UNRECOGNIZED_EXTENSION_WARNING}")`, - ); - callback(false); - break; - } - case 'unknown': { - // If we don't know how this extension was built, we can't reliably detect if there - // are other installations of DevTools present. - // In this case, assume there are no duplicate exensions and show a warning about - // potential conflicts. - console.error(UNRECOGNIZED_EXTENSION_WARNING); - chrome.devtools.inspectedWindow.eval( - `console.error("${UNRECOGNIZED_EXTENSION_WARNING}")`, - ); - callback(false); - break; - } - default: { - (EXTENSION_INSTALLATION_TYPE: empty); - } - } -} - -function checkForInstalledExtensions( - extensionIds: string[], -): Promise { - return Promise.all( - extensionIds.map(extensionId => checkForInstalledExtension(extensionId)), - ); -} - -function checkForInstalledExtension(extensionId: string): Promise { - return new Promise(resolve => { - chrome.runtime.sendMessage( - extensionId, - EXTENSION_INSTALL_CHECK_MESSAGE, - response => { - if (__DEBUG__) { - console.log( - 'checkForDuplicateInstallations: Duplicate installation check responded with', - { - response, - error: chrome.runtime.lastError?.message, - currentExtension: EXTENSION_INSTALLATION_TYPE, - checkingExtension: extensionId, - }, - ); - } - if (chrome.runtime.lastError != null) { - resolve(false); - } else { - resolve(true); - } - }, - ); - }); -} diff --git a/packages/react-devtools-extensions/src/constants.js b/packages/react-devtools-extensions/src/constants.js deleted file mode 100644 index 5e85e4aec40e7..0000000000000 --- a/packages/react-devtools-extensions/src/constants.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow strict-local - */ - -declare var chrome: any; - -export const CURRENT_EXTENSION_ID = chrome.runtime.id; - -export const EXTENSION_INSTALL_CHECK_MESSAGE = 'extension-install-check'; - -export const CHROME_WEBSTORE_EXTENSION_ID = 'fmkadmapgofadopljbjfkapdkoienihi'; -export const INTERNAL_EXTENSION_ID = 'dnjnjgbfilfphmojnmhliehogmojhclc'; -export const LOCAL_EXTENSION_ID = 'ikiahnapldjmdmpkmfhjdjilojjhgcbf'; - -export const EXTENSION_INSTALLATION_TYPE: - | 'public' - | 'internal' - | 'local' - | 'unknown' = - CURRENT_EXTENSION_ID === CHROME_WEBSTORE_EXTENSION_ID - ? 'public' - : CURRENT_EXTENSION_ID === INTERNAL_EXTENSION_ID - ? 'internal' - : CURRENT_EXTENSION_ID === LOCAL_EXTENSION_ID - ? 'local' - : 'unknown'; diff --git a/packages/react-devtools-extensions/src/injectGlobalHook.js b/packages/react-devtools-extensions/src/injectGlobalHook.js index 02d5109e291eb..701d4927487d9 100644 --- a/packages/react-devtools-extensions/src/injectGlobalHook.js +++ b/packages/react-devtools-extensions/src/injectGlobalHook.js @@ -2,11 +2,7 @@ import nullthrows from 'nullthrows'; import {installHook} from 'react-devtools-shared/src/hook'; -import { - __DEBUG__, - SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, -} from 'react-devtools-shared/src/constants'; -import {CURRENT_EXTENSION_ID, EXTENSION_INSTALLATION_TYPE} from './constants'; +import {SESSION_STORAGE_RELOAD_AND_PROFILE_KEY} from 'react-devtools-shared/src/constants'; import {sessionStorageGetItem} from 'react-devtools-shared/src/storage'; function injectCode(code) { @@ -31,17 +27,7 @@ window.addEventListener('message', function onMessage({data, source}) { if (source !== window || !data) { return; } - if (data.extensionId !== CURRENT_EXTENSION_ID) { - if (__DEBUG__) { - console.log( - `[injectGlobalHook] Received message '${data.source}' from different extension instance. Skipping message.`, - { - currentExtension: EXTENSION_INSTALLATION_TYPE, - }, - ); - } - return; - } + switch (data.source) { case 'react-devtools-detector': lastDetectionResult = { @@ -116,7 +102,6 @@ window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on('renderer', function({reactBuildType}) window.postMessage({ source: 'react-devtools-detector', reactBuildType, - extensionId: "${CURRENT_EXTENSION_ID}", }, '*'); }); `; diff --git a/packages/react-devtools-extensions/src/main.js b/packages/react-devtools-extensions/src/main.js index 8280158e1db70..6a3836839a4ea 100644 --- a/packages/react-devtools-extensions/src/main.js +++ b/packages/react-devtools-extensions/src/main.js @@ -22,8 +22,6 @@ import { import DevTools from 'react-devtools-shared/src/devtools/views/DevTools'; import {__DEBUG__} from 'react-devtools-shared/src/constants'; import {logEvent} from 'react-devtools-shared/src/Logger'; -import {CURRENT_EXTENSION_ID, EXTENSION_INSTALLATION_TYPE} from './constants'; -import {checkForDuplicateInstallations} from './checkForDuplicateInstallations'; const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY = 'React::DevTools::supportsProfiling'; @@ -72,158 +70,135 @@ function createPanelIfReactLoaded() { return; } - checkForDuplicateInstallations(hasDuplicateInstallation => { - if (hasDuplicateInstallation) { - if (__DEBUG__) { - console.log( - '[main] createPanelIfReactLoaded: Duplicate installation detected, skipping initialization of extension.', - {currentExtension: EXTENSION_INSTALLATION_TYPE}, - ); - } - panelCreated = true; - clearInterval(loadCheckInterval); - return; - } - - if (__DEBUG__) { - console.log( - '[main] createPanelIfReactLoaded: No duplicate installations detected, continuing with initialization.', - {currentExtension: EXTENSION_INSTALLATION_TYPE}, - ); - } + panelCreated = true; - panelCreated = true; + clearInterval(loadCheckInterval); - clearInterval(loadCheckInterval); + let bridge = null; + let store = null; - let bridge = null; - let store = null; + let profilingData = null; - let profilingData = null; + let componentsPortalContainer = null; + let profilerPortalContainer = null; - let componentsPortalContainer = null; - let profilerPortalContainer = null; + let cloneStyleTags = null; + let mostRecentOverrideTab = null; + let render = null; + let root = null; - let cloneStyleTags = null; - let mostRecentOverrideTab = null; - let render = null; - let root = null; + const tabId = chrome.devtools.inspectedWindow.tabId; - const tabId = chrome.devtools.inspectedWindow.tabId; + registerDevToolsEventLogger('extension'); - registerDevToolsEventLogger('extension'); + function initBridgeAndStore() { + const port = chrome.runtime.connect({ + name: String(tabId), + }); + // Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation, + // so it makes no sense to handle it here. + + bridge = new Bridge({ + listen(fn) { + const listener = message => fn(message); + // Store the reference so that we unsubscribe from the same object. + const portOnMessage = port.onMessage; + portOnMessage.addListener(listener); + return () => { + portOnMessage.removeListener(listener); + }; + }, + send(event: string, payload: any, transferable?: Array) { + port.postMessage({event, payload}, transferable); + }, + }); + bridge.addListener('reloadAppForProfiling', () => { + localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true'); + chrome.devtools.inspectedWindow.eval('window.location.reload();'); + }); + bridge.addListener('syncSelectionToNativeElementsPanel', () => { + setBrowserSelectionFromReact(); + }); - function initBridgeAndStore() { - const port = chrome.runtime.connect({ - name: String(tabId), - }); - // Looks like `port.onDisconnect` does not trigger on in-tab navigation like new URL or back/forward navigation, - // so it makes no sense to handle it here. - - bridge = new Bridge({ - listen(fn) { - const listener = message => fn(message); - // Store the reference so that we unsubscribe from the same object. - const portOnMessage = port.onMessage; - portOnMessage.addListener(listener); - return () => { - portOnMessage.removeListener(listener); - }; - }, - send(event: string, payload: any, transferable?: Array) { - port.postMessage({event, payload}, transferable); - }, - }); - bridge.addListener('reloadAppForProfiling', () => { - localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true'); - chrome.devtools.inspectedWindow.eval('window.location.reload();'); - }); - bridge.addListener('syncSelectionToNativeElementsPanel', () => { - setBrowserSelectionFromReact(); - }); + // This flag lets us tip the Store off early that we expect to be profiling. + // This avoids flashing a temporary "Profiling not supported" message in the Profiler tab, + // after a user has clicked the "reload and profile" button. + let isProfiling = false; + let supportsProfiling = false; + if ( + localStorageGetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY) === 'true' + ) { + supportsProfiling = true; + isProfiling = true; + localStorageRemoveItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY); + } - // This flag lets us tip the Store off early that we expect to be profiling. - // This avoids flashing a temporary "Profiling not supported" message in the Profiler tab, - // after a user has clicked the "reload and profile" button. - let isProfiling = false; - let supportsProfiling = false; - if ( - localStorageGetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY) === 'true' - ) { - supportsProfiling = true; - isProfiling = true; - localStorageRemoveItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY); - } + if (store !== null) { + profilingData = store.profilerStore.profilingData; + } - if (store !== null) { - profilingData = store.profilerStore.profilingData; - } + bridge.addListener('extensionBackendInitialized', () => { + // Initialize the renderer's trace-updates setting. + // This handles the case of navigating to a new page after the DevTools have already been shown. + bridge.send( + 'setTraceUpdatesEnabled', + localStorageGetItem(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY) === + 'true', + ); + }); - bridge.addListener('extensionBackendInitialized', () => { - // Initialize the renderer's trace-updates setting. - // This handles the case of navigating to a new page after the DevTools have already been shown. - bridge.send( - 'setTraceUpdatesEnabled', - localStorageGetItem(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY) === - 'true', - ); - }); + store = new Store(bridge, { + isProfiling, + supportsReloadAndProfile: isChrome, + supportsProfiling, + // At this time, the scheduling profiler can only parse Chrome performance profiles. + supportsSchedulingProfiler: isChrome, + supportsTraceUpdates: true, + }); + store.profilerStore.profilingData = profilingData; + + // 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); + } + }, + ); - store = new Store(bridge, { - isProfiling, - supportsReloadAndProfile: isChrome, - supportsProfiling, - // At this time, the scheduling profiler can only parse Chrome performance profiles. - supportsSchedulingProfiler: isChrome, - supportsTraceUpdates: true, - }); - store.profilerStore.profilingData = profilingData; - - // 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', - extensionId: "${CURRENT_EXTENSION_ID}" - }, '*');`, - function(response, evalError) { - if (evalError) { - console.error(evalError); - } - }, - ); + const viewAttributeSourceFunction = (id, path) => { + const rendererID = store.getRendererIDForElement(id); + if (rendererID != null) { + // Ask the renderer interface to find the specified attribute, + // and store it as a global variable on the window. + bridge.send('viewAttributeSource', {id, path, rendererID}); - const viewAttributeSourceFunction = (id, path) => { - const rendererID = store.getRendererIDForElement(id); - if (rendererID != null) { - // Ask the renderer interface to find the specified attribute, - // and store it as a global variable on the window. - bridge.send('viewAttributeSource', {id, path, rendererID}); - - setTimeout(() => { - // Ask Chrome to display the location of the attribute, - // assuming the renderer found a match. - chrome.devtools.inspectedWindow.eval(` + setTimeout(() => { + // Ask Chrome to display the location of the attribute, + // assuming the renderer found a match. + chrome.devtools.inspectedWindow.eval(` if (window.$attribute != null) { inspect(window.$attribute); } `); - }, 100); - } - }; + }, 100); + } + }; - const viewElementSourceFunction = id => { - const rendererID = store.getRendererIDForElement(id); - if (rendererID != null) { - // Ask the renderer interface to determine the component function, - // and store it as a global variable on the window - bridge.send('viewElementSource', {id, rendererID}); - - setTimeout(() => { - // Ask Chrome to display the location of the component function, - // or a render method if it is a Class (ideally Class instance, not type) - // assuming the renderer found one. - chrome.devtools.inspectedWindow.eval(` + const viewElementSourceFunction = id => { + const rendererID = store.getRendererIDForElement(id); + if (rendererID != null) { + // Ask the renderer interface to determine the component function, + // and store it as a global variable on the window + bridge.send('viewElementSource', {id, rendererID}); + + setTimeout(() => { + // Ask Chrome to display the location of the component function, + // or a render method if it is a Class (ideally Class instance, not type) + // assuming the renderer found one. + chrome.devtools.inspectedWindow.eval(` if (window.$type != null) { if ( window.$type && @@ -238,293 +213,288 @@ function createPanelIfReactLoaded() { } } `); - }, 100); - } - }; + }, 100); + } + }; - let debugIDCounter = 0; + let debugIDCounter = 0; - // 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) { - 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. - let debugID = null; + // 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) { + 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. + let debugID = null; - if (__DEBUG__) { - debugID = debugIDCounter++; - console.log(`[main] fetchFromNetworkCache(${debugID})`, url); - } + if (__DEBUG__) { + debugID = debugIDCounter++; + console.log(`[main] fetchFromNetworkCache(${debugID})`, url); + } - chrome.devtools.network.getHAR(harLog => { - for (let i = 0; i < harLog.entries.length; i++) { - const entry = harLog.entries[i]; - if (url === entry.request.url) { - if (__DEBUG__) { - console.log( - `[main] fetchFromNetworkCache(${debugID}) Found matching URL in HAR`, - url, - ); - } + chrome.devtools.network.getHAR(harLog => { + for (let i = 0; i < harLog.entries.length; i++) { + const entry = harLog.entries[i]; + if (url === entry.request.url) { + if (__DEBUG__) { + console.log( + `[main] fetchFromNetworkCache(${debugID}) Found matching URL in HAR`, + url, + ); + } - entry.getContent(content => { - if (content) { - if (__DEBUG__) { - console.log( - `[main] fetchFromNetworkCache(${debugID}) Content retrieved`, - ); - } - - resolve(content); - } else { - if (__DEBUG__) { - console.log( - `[main] fetchFromNetworkCache(${debugID}) Invalid content returned by getContent()`, - content, - ); - } - - // Edge case where getContent() returned null; fall back to fetch. - fetchFromPage(url, resolve, reject); + entry.getContent(content => { + if (content) { + if (__DEBUG__) { + console.log( + `[main] fetchFromNetworkCache(${debugID}) Content retrieved`, + ); } - }); - return; - } - } + resolve(content); + } else { + if (__DEBUG__) { + console.log( + `[main] fetchFromNetworkCache(${debugID}) Invalid content returned by getContent()`, + content, + ); + } - if (__DEBUG__) { - console.log( - `[main] fetchFromNetworkCache(${debugID}) No cached request found in getHAR()`, - ); - } + // Edge case where getContent() returned null; fall back to fetch. + fetchFromPage(url, resolve, reject); + } + }); - // No matching URL found; fall back to fetch. - fetchFromPage(url, resolve, reject); - }); - }; + return; + } + } - const fetchFromPage = (url, resolve, reject) => { if (__DEBUG__) { - console.log('[main] fetchFromPage()', url); + console.log( + `[main] fetchFromNetworkCache(${debugID}) No cached request found in getHAR()`, + ); } - function onPortMessage({payload, source}) { - if (source === 'react-devtools-content-script') { - switch (payload?.type) { - case 'fetch-file-with-cache-complete': - chrome.runtime.onMessage.removeListener(onPortMessage); - resolve(payload.value); - break; - case 'fetch-file-with-cache-error': - chrome.runtime.onMessage.removeListener(onPortMessage); - reject(payload.value); - break; - } + // No matching URL found; fall back to fetch. + fetchFromPage(url, resolve, reject); + }); + }; + + const fetchFromPage = (url, resolve, reject) => { + if (__DEBUG__) { + console.log('[main] fetchFromPage()', url); + } + + function onPortMessage({payload, source}) { + if (source === 'react-devtools-content-script') { + switch (payload?.type) { + case 'fetch-file-with-cache-complete': + chrome.runtime.onMessage.removeListener(onPortMessage); + resolve(payload.value); + break; + case 'fetch-file-with-cache-error': + chrome.runtime.onMessage.removeListener(onPortMessage); + reject(payload.value); + break; } } + } - chrome.runtime.onMessage.addListener(onPortMessage); + chrome.runtime.onMessage.addListener(onPortMessage); - chrome.devtools.inspectedWindow.eval(` + chrome.devtools.inspectedWindow.eval(` window.postMessage({ source: 'react-devtools-extension', - extensionId: "${CURRENT_EXTENSION_ID}" payload: { type: 'fetch-file-with-cache', url: "${url}", }, }); `); - }; - - // Fetching files from the extension won't make use of the network cache - // for resources that have already been loaded by the page. - // This helper function allows the extension to request files to be fetched - // by the content script (running in the page) to increase the likelihood of a cache hit. - fetchFileWithCaching = url => { - return new Promise((resolve, reject) => { - // Try fetching from the Network cache first. - // If DevTools was opened after the page started loading, we may have missed some requests. - // So fall back to a fetch() from the page and hope we get a cached response that way. - fetchFromNetworkCache(url, resolve, reject); - }); - }; - } - - // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. - const hookNamesModuleLoaderFunction = () => - import( - /* webpackChunkName: 'parseHookNames' */ 'react-devtools-shared/src/hooks/parseHookNames' - ); - - root = createRoot(document.createElement('div')); - - render = (overrideTab = mostRecentOverrideTab) => { - mostRecentOverrideTab = overrideTab; - root.render( - createElement(DevTools, { - bridge, - browserTheme: getBrowserTheme(), - componentsPortalContainer, - enabledInspectedElementContextMenu: true, - fetchFileWithCaching, - hookNamesModuleLoaderFunction, - overrideTab, - profilerPortalContainer, - showTabBar: false, - store, - warnIfUnsupportedVersionDetected: true, - viewAttributeSourceFunction, - viewElementSourceFunction, - }), - ); }; - render(); + // Fetching files from the extension won't make use of the network cache + // for resources that have already been loaded by the page. + // This helper function allows the extension to request files to be fetched + // by the content script (running in the page) to increase the likelihood of a cache hit. + fetchFileWithCaching = url => { + return new Promise((resolve, reject) => { + // Try fetching from the Network cache first. + // If DevTools was opened after the page started loading, we may have missed some requests. + // So fall back to a fetch() from the page and hope we get a cached response that way. + fetchFromNetworkCache(url, resolve, reject); + }); + }; } - cloneStyleTags = () => { - const linkTags = []; - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const linkTag of document.getElementsByTagName('link')) { - if (linkTag.rel === 'stylesheet') { - const newLinkTag = document.createElement('link'); - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const attribute of linkTag.attributes) { - newLinkTag.setAttribute( - attribute.nodeName, - attribute.nodeValue, - ); - } - linkTags.push(newLinkTag); - } - } - return linkTags; + // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration. + const hookNamesModuleLoaderFunction = () => + import( + /* webpackChunkName: 'parseHookNames' */ 'react-devtools-shared/src/hooks/parseHookNames' + ); + + root = createRoot(document.createElement('div')); + + render = (overrideTab = mostRecentOverrideTab) => { + mostRecentOverrideTab = overrideTab; + root.render( + createElement(DevTools, { + bridge, + browserTheme: getBrowserTheme(), + componentsPortalContainer, + enabledInspectedElementContextMenu: true, + fetchFileWithCaching, + hookNamesModuleLoaderFunction, + overrideTab, + profilerPortalContainer, + showTabBar: false, + store, + warnIfUnsupportedVersionDetected: true, + viewAttributeSourceFunction, + viewElementSourceFunction, + }), + ); }; - initBridgeAndStore(); + render(); + } - function ensureInitialHTMLIsCleared(container) { - if (container._hasInitialHTMLBeenCleared) { - return; + cloneStyleTags = () => { + const linkTags = []; + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const linkTag of document.getElementsByTagName('link')) { + if (linkTag.rel === 'stylesheet') { + const newLinkTag = document.createElement('link'); + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const attribute of linkTag.attributes) { + newLinkTag.setAttribute(attribute.nodeName, attribute.nodeValue); + } + linkTags.push(newLinkTag); } - container.innerHTML = ''; - container._hasInitialHTMLBeenCleared = true; } + return linkTags; + }; - function setBrowserSelectionFromReact() { - // This is currently only called on demand when you press "view DOM". - // In the future, if Chrome adds an inspect() that doesn't switch tabs, - // we could make this happen automatically when you select another component. - chrome.devtools.inspectedWindow.eval( - '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + - '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' + - 'false', - (didSelectionChange, evalError) => { - if (evalError) { - console.error(evalError); - } - }, - ); - } + initBridgeAndStore(); - function setReactSelectionFromBrowser() { - // When the user chooses a different node in the browser Elements tab, - // copy it over to the hook object so that we can sync the selection. - chrome.devtools.inspectedWindow.eval( - '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + - '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' + - 'false', - (didSelectionChange, evalError) => { - if (evalError) { - console.error(evalError); - } else if (didSelectionChange) { - // Remember to sync the selection next time we show Components tab. - needsToSyncElementSelection = true; - } - }, - ); + function ensureInitialHTMLIsCleared(container) { + if (container._hasInitialHTMLBeenCleared) { + return; } + container.innerHTML = ''; + container._hasInitialHTMLBeenCleared = true; + } - setReactSelectionFromBrowser(); - chrome.devtools.panels.elements.onSelectionChanged.addListener(() => { - setReactSelectionFromBrowser(); - }); + function setBrowserSelectionFromReact() { + // This is currently only called on demand when you press "view DOM". + // In the future, if Chrome adds an inspect() that doesn't switch tabs, + // we could make this happen automatically when you select another component. + chrome.devtools.inspectedWindow.eval( + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + + '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' + + 'false', + (didSelectionChange, evalError) => { + if (evalError) { + console.error(evalError); + } + }, + ); + } - let currentPanel = null; - let needsToSyncElementSelection = false; - - chrome.devtools.panels.create( - isChrome ? '⚛️ Components' : 'Components', - '', - 'panel.html', - extensionPanel => { - extensionPanel.onShown.addListener(panel => { - if (needsToSyncElementSelection) { - needsToSyncElementSelection = false; - bridge.send('syncSelectionFromNativeElementsPanel'); - } + function setReactSelectionFromBrowser() { + // When the user chooses a different node in the browser Elements tab, + // copy it over to the hook object so that we can sync the selection. + chrome.devtools.inspectedWindow.eval( + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' + + 'false', + (didSelectionChange, evalError) => { + if (evalError) { + console.error(evalError); + } else if (didSelectionChange) { + // Remember to sync the selection next time we show Components tab. + needsToSyncElementSelection = true; + } + }, + ); + } - if (currentPanel === panel) { - return; - } + setReactSelectionFromBrowser(); + chrome.devtools.panels.elements.onSelectionChanged.addListener(() => { + setReactSelectionFromBrowser(); + }); - currentPanel = panel; - componentsPortalContainer = panel.container; + let currentPanel = null; + let needsToSyncElementSelection = false; + + chrome.devtools.panels.create( + isChrome ? '⚛️ Components' : 'Components', + '', + 'panel.html', + extensionPanel => { + extensionPanel.onShown.addListener(panel => { + if (needsToSyncElementSelection) { + needsToSyncElementSelection = false; + bridge.send('syncSelectionFromNativeElementsPanel'); + } - if (componentsPortalContainer != null) { - ensureInitialHTMLIsCleared(componentsPortalContainer); - render('components'); - panel.injectStyles(cloneStyleTags); - logEvent({event_name: 'selected-components-tab'}); - } - }); - extensionPanel.onHidden.addListener(panel => { - // TODO: Stop highlighting and stuff. - }); - }, - ); + if (currentPanel === panel) { + return; + } - chrome.devtools.panels.create( - isChrome ? '⚛️ Profiler' : 'Profiler', - '', - 'panel.html', - extensionPanel => { - extensionPanel.onShown.addListener(panel => { - if (currentPanel === panel) { - return; - } + currentPanel = panel; + componentsPortalContainer = panel.container; - currentPanel = panel; - profilerPortalContainer = panel.container; + if (componentsPortalContainer != null) { + ensureInitialHTMLIsCleared(componentsPortalContainer); + render('components'); + panel.injectStyles(cloneStyleTags); + logEvent({event_name: 'selected-components-tab'}); + } + }); + extensionPanel.onHidden.addListener(panel => { + // TODO: Stop highlighting and stuff. + }); + }, + ); + + chrome.devtools.panels.create( + isChrome ? '⚛️ Profiler' : 'Profiler', + '', + 'panel.html', + extensionPanel => { + extensionPanel.onShown.addListener(panel => { + if (currentPanel === panel) { + return; + } - if (profilerPortalContainer != null) { - ensureInitialHTMLIsCleared(profilerPortalContainer); - render('profiler'); - panel.injectStyles(cloneStyleTags); - logEvent({event_name: 'selected-profiler-tab'}); - } - }); - }, - ); + currentPanel = panel; + profilerPortalContainer = panel.container; - chrome.devtools.network.onNavigated.removeListener(checkPageForReact); + if (profilerPortalContainer != null) { + ensureInitialHTMLIsCleared(profilerPortalContainer); + render('profiler'); + panel.injectStyles(cloneStyleTags); + logEvent({event_name: 'selected-profiler-tab'}); + } + }); + }, + ); - // Re-initialize DevTools panel when a new page is loaded. - chrome.devtools.network.onNavigated.addListener(function onNavigated() { - // Re-initialize saved filters on navigation, - // since global values stored on window get reset in this case. - syncSavedPreferences(); + chrome.devtools.network.onNavigated.removeListener(checkPageForReact); - // It's easiest to recreate the DevTools panel (to clean up potential stale state). - // We can revisit this in the future as a small optimization. - flushSync(() => root.unmount()); + // Re-initialize DevTools panel when a new page is loaded. + chrome.devtools.network.onNavigated.addListener(function onNavigated() { + // Re-initialize saved filters on navigation, + // since global values stored on window get reset in this case. + syncSavedPreferences(); - initBridgeAndStore(); - }); + // It's easiest to recreate the DevTools panel (to clean up potential stale state). + // We can revisit this in the future as a small optimization. + flushSync(() => root.unmount()); + + initBridgeAndStore(); }); }, ); diff --git a/packages/react-devtools/CONTRIBUTING.md b/packages/react-devtools/CONTRIBUTING.md index 06816532838fd..3b93022009617 100644 --- a/packages/react-devtools/CONTRIBUTING.md +++ b/packages/react-devtools/CONTRIBUTING.md @@ -57,7 +57,7 @@ Some changes requiring testing in the browser extension (e.g. like "named hooks" ```sh cd cd packages/react-devtools-extensions -yarn build:chrome:local && yarn test:chrome +yarn build:chrome && yarn test:chrome ``` This will launch a standalone version of Chrome with the locally built React DevTools pre-installed. If you are testing a specific URL, you can make your testing even faster by passing the `--url` argument to the test script: ```sh