Revert logic for checking for duplicate installations of DevTools (#22638)

* Revert "Only show DevTools warning about unrecognized build in Chrome (#22571)"

This reverts commit b72dc8e930.

* Revert "Show warning in UI when duplicate installations of DevTools extension are detected (#22563)"

This reverts commit 930c9e7eeb.

* Revert "Prevent errors/crashing when multiple installs of DevTools are present (#22517)"

This reverts commit 545d4c2de7.

* Remove all references to passing extensionId in postMessage

* Keep build changes

* lint
This commit is contained in:
Juan
2021-10-27 16:34:44 -04:00
committed by GitHub
parent 6c3dcc7a47
commit 26bc8ff9bf
8 changed files with 377 additions and 713 deletions

View File

@@ -1,20 +1,11 @@
// @flow strict-local
/* global chrome */
'use strict';
declare var chrome: any;
const ports: {
[tab: string]: {|devtools: any, 'content-script': any|},
} = {};
const ports = {};
const IS_FIREFOX = navigator.userAgent.indexOf('Firefox') >= 0;
import {
EXTENSION_INSTALL_CHECK,
SHOW_DUPLICATE_EXTENSION_WARNING,
} from './constants';
chrome.runtime.onConnect.addListener(function(port) {
let tab = null;
let name = null;
@@ -125,15 +116,6 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
}
});
chrome.runtime.onMessageExternal.addListener(
(request, sender, sendResponse) => {
if (request === EXTENSION_INSTALL_CHECK) {
sendResponse(true);
chrome.runtime.sendMessage(SHOW_DUPLICATE_EXTENSION_WARNING);
}
},
);
chrome.runtime.onMessage.addListener((request, sender) => {
const tab = sender.tab;
if (tab) {

View File

@@ -1,137 +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 {
INTERNAL_EXTENSION_ID,
LOCAL_EXTENSION_ID,
__DEBUG__,
} from 'react-devtools-shared/src/constants';
import {getBrowserName} from './utils';
import {
EXTENSION_INSTALL_CHECK,
EXTENSION_INSTALLATION_TYPE,
} from './constants';
const IS_CHROME = getBrowserName() === 'Chrome';
const UNRECOGNIZED_EXTENSION_ERROR =
'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:<browser>: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_ERROR);
chrome.devtools.inspectedWindow.eval(
`console.error("${UNRECOGNIZED_EXTENSION_ERROR}")`,
);
callback(false);
break;
}
case 'unknown': {
// TODO: Support duplicate extension detection in other browsers
if (IS_CHROME) {
// 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_ERROR);
chrome.devtools.inspectedWindow.eval(
`console.error("${UNRECOGNIZED_EXTENSION_ERROR}")`,
);
}
callback(false);
break;
}
default: {
(EXTENSION_INSTALLATION_TYPE: empty);
}
}
}
function checkForInstalledExtensions(
extensionIds: string[],
): Promise<boolean[]> {
return Promise.all(
extensionIds.map(extensionId => checkForInstalledExtension(extensionId)),
);
}
function checkForInstalledExtension(extensionId: string): Promise<boolean> {
return new Promise(resolve => {
chrome.runtime.sendMessage(
extensionId,
EXTENSION_INSTALL_CHECK,
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);
}
},
);
});
}

View File

@@ -1,35 +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
*/
import {
CHROME_WEBSTORE_EXTENSION_ID,
INTERNAL_EXTENSION_ID,
LOCAL_EXTENSION_ID,
} from 'react-devtools-shared/src/constants';
declare var chrome: any;
export const CURRENT_EXTENSION_ID = chrome.runtime.id;
export const EXTENSION_INSTALL_CHECK = 'extension-install-check';
export const SHOW_DUPLICATE_EXTENSION_WARNING =
'show-duplicate-extension-warning';
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';

View File

@@ -2,8 +2,6 @@
'use strict';
import {CURRENT_EXTENSION_ID} from './constants';
let backendDisconnected: boolean = false;
let backendInitialized: boolean = false;
@@ -12,7 +10,6 @@ function sayHelloToBackend() {
{
source: 'react-devtools-content-script',
hello: true,
extensionId: CURRENT_EXTENSION_ID,
},
'*',
);
@@ -23,7 +20,6 @@ function handleMessageFromDevtools(message) {
{
source: 'react-devtools-content-script',
payload: message,
extensionId: CURRENT_EXTENSION_ID,
},
'*',
);
@@ -53,7 +49,6 @@ function handleDisconnect() {
type: 'event',
event: 'shutdown',
},
extensionId: CURRENT_EXTENSION_ID,
},
'*',
);

View File

@@ -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,19 +27,6 @@ window.addEventListener('message', function onMessage({data, source}) {
if (source !== window || !data) {
return;
}
if (data.extensionId != null && data.extensionId !== CURRENT_EXTENSION_ID) {
if (__DEBUG__) {
console.log(
`[injectGlobalHook] Received message '${data.source}' from different extension instance. Skipping message.`,
{
currentExtension: EXTENSION_INSTALLATION_TYPE,
currentExtensionId: CURRENT_EXTENSION_ID,
providedExtensionId: data.extensionId,
},
);
}
return;
}
switch (data.source) {
case 'react-devtools-detector':
lastDetectionResult = {
@@ -118,7 +101,6 @@ window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on('renderer', function({reactBuildType})
window.postMessage({
source: 'react-devtools-detector',
reactBuildType,
extensionId: "${CURRENT_EXTENSION_ID}",
}, '*');
});
`;

View File

@@ -22,18 +22,11 @@ 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,
SHOW_DUPLICATE_EXTENSION_WARNING,
} from './constants';
import {checkForDuplicateInstallations} from './checkForDuplicateInstallations';
const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
'React::DevTools::supportsProfiling';
const isChrome = getBrowserName() === 'Chrome';
const isEdge = getBrowserName() === 'Edge';
let panelCreated = false;
@@ -77,186 +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;
panelCreated = true;
clearInterval(loadCheckInterval);
let bridge = null;
let store = null;
let profilingData = null;
let componentsPortalContainer = null;
let profilerPortalContainer = null;
let cloneStyleTags = null;
let mostRecentOverrideTab = null;
let render = null;
let root = null;
const tabId = chrome.devtools.inspectedWindow.tabId;
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<any>) {
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);
}
if (__DEBUG__) {
console.log(
'[main] createPanelIfReactLoaded: No duplicate installations detected, continuing with initialization.',
{currentExtension: EXTENSION_INSTALLATION_TYPE},
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',
);
}
});
panelCreated = 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;
clearInterval(loadCheckInterval);
let bridge = null;
let store = null;
let profilingData = null;
let componentsPortalContainer = null;
let profilerPortalContainer = null;
let cloneStyleTags = null;
let mostRecentOverrideTab = null;
let render = null;
let root = null;
let warnIfDuplicateInstallation = false;
const tabId = chrome.devtools.inspectedWindow.tabId;
registerDevToolsEventLogger('extension');
function onDuplicateExtensionMessage(message) {
if (message === SHOW_DUPLICATE_EXTENSION_WARNING) {
chrome.runtime.onMessage.removeListener(
onDuplicateExtensionMessage,
);
if (warnIfDuplicateInstallation === true) {
return;
// 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);
}
warnIfDuplicateInstallation = true;
const errorMessage =
'React Developer Tools: We detected that there are multiple versions of React Developer Tools ' +
'installed and enabled in your browser at the same time, which will cause ' +
'issues while using the extension. ' +
'Please ensure that you have installed and enabled only a single ' +
'version of React Developer Tools before proceeding.';
console.error(errorMessage);
chrome.devtools.inspectedWindow.eval(
`console.error("${errorMessage}")`,
);
if (render != null) {
render();
}
}
}
chrome.runtime.onMessage.addListener(onDuplicateExtensionMessage);
},
);
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.
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});
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<any>) {
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);
}
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',
);
});
store = new Store(bridge, {
isProfiling,
supportsReloadAndProfile: isChrome || isEdge,
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});
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});
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(`
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 &&
@@ -271,294 +213,288 @@ function createPanelIfReactLoaded() {
}
}
`);
}, 100);
}
};
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;
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,
);
}
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);
}
});
return;
}
}
if (__DEBUG__) {
console.log(
`[main] fetchFromNetworkCache(${debugID}) No cached request found in getHAR()`,
);
}
// 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.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);
});
};
}, 100);
}
// 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,
warnIfDuplicateInstallation,
showTabBar: false,
store,
warnIfUnsupportedVersionDetected: true,
viewAttributeSourceFunction,
viewElementSourceFunction,
}),
);
};
render();
}
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;
};
initBridgeAndStore();
let debugIDCounter = 0;
function ensureInitialHTMLIsCleared(container) {
if (container._hasInitialHTMLBeenCleared) {
return;
// 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);
}
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);
}
});
return;
}
}
if (__DEBUG__) {
console.log(
`[main] fetchFromNetworkCache(${debugID}) No cached request found in getHAR()`,
);
}
// 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.devtools.inspectedWindow.eval(`
window.postMessage({
source: 'react-devtools-extension',
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();
}
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 ensureInitialHTMLIsCleared(container) {
if (container._hasInitialHTMLBeenCleared) {
return;
}
container.innerHTML = '';
container._hasInitialHTMLBeenCleared = true;
}
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 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);
}
},
);
}
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;
}
},
);
}
setReactSelectionFromBrowser();
chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
setReactSelectionFromBrowser();
chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
setReactSelectionFromBrowser();
});
});
let currentPanel = null;
let needsToSyncElementSelection = false;
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');
}
chrome.devtools.panels.create(
isChrome ? '⚛️ Components' : 'Components',
'',
'panel.html',
extensionPanel => {
extensionPanel.onShown.addListener(panel => {
if (needsToSyncElementSelection) {
needsToSyncElementSelection = false;
bridge.send('syncSelectionFromNativeElementsPanel');
}
if (currentPanel === panel) {
return;
}
if (currentPanel === panel) {
return;
}
currentPanel = panel;
componentsPortalContainer = panel.container;
currentPanel = panel;
componentsPortalContainer = 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.
});
},
);
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;
}
chrome.devtools.panels.create(
isChrome ? '⚛️ Profiler' : 'Profiler',
'',
'panel.html',
extensionPanel => {
extensionPanel.onShown.addListener(panel => {
if (currentPanel === panel) {
return;
}
currentPanel = panel;
profilerPortalContainer = panel.container;
currentPanel = panel;
profilerPortalContainer = panel.container;
if (profilerPortalContainer != null) {
ensureInitialHTMLIsCleared(profilerPortalContainer);
render('profiler');
panel.injectStyles(cloneStyleTags);
logEvent({event_name: 'selected-profiler-tab'});
}
});
},
);
if (profilerPortalContainer != null) {
ensureInitialHTMLIsCleared(profilerPortalContainer);
render('profiler');
panel.injectStyles(cloneStyleTags);
logEvent({event_name: 'selected-profiler-tab'});
}
});
},
);
chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
// 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();
// 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();
// 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());
// 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();
});
initBridgeAndStore();
});
},
);

View File

@@ -34,7 +34,6 @@ import {SchedulingProfilerContextController} from 'react-devtools-scheduling-pro
import {ModalDialogContextController} from './ModalDialog';
import ReactLogo from './ReactLogo';
import UnsupportedBridgeProtocolDialog from './UnsupportedBridgeProtocolDialog';
import DuplicateInstallationDialog from './DuplicateInstallationDialog';
import UnsupportedVersionDialog from './UnsupportedVersionDialog';
import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected';
import {useLocalStorage} from './hooks';
@@ -74,7 +73,6 @@ export type Props = {|
enabledInspectedElementContextMenu?: boolean,
showTabBar?: boolean,
store: Store,
warnIfDuplicateInstallation?: boolean,
warnIfLegacyBackendDetected?: boolean,
warnIfUnsupportedVersionDetected?: boolean,
viewAttributeSourceFunction?: ?ViewAttributeSource,
@@ -134,7 +132,6 @@ export default function DevTools({
profilerPortalContainer,
showTabBar = false,
store,
warnIfDuplicateInstallation = false,
warnIfLegacyBackendDetected = false,
warnIfUnsupportedVersionDetected = false,
viewAttributeSourceFunction,
@@ -322,7 +319,6 @@ export default function DevTools({
</ViewElementSourceContext.Provider>
</SettingsContextController>
<UnsupportedBridgeProtocolDialog />
{warnIfDuplicateInstallation && <DuplicateInstallationDialog />}
{warnIfLegacyBackendDetected && <WarnIfLegacyBackendDetected />}
{warnIfUnsupportedVersionDetected && <UnsupportedVersionDialog />}
</ModalDialogContextController>

View File

@@ -1,55 +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
*/
import * as React from 'react';
import {Fragment, useContext, useEffect} from 'react';
import {isInternalFacebookBuild} from 'react-devtools-feature-flags';
import {ModalDialogContext} from './ModalDialog';
export default function DuplicateInstallationDialog(_: {||}) {
const {dispatch} = useContext(ModalDialogContext);
useEffect(() => {
dispatch({
canBeDismissed: false,
id: 'DuplicateInstallationDialog',
type: 'SHOW',
title: 'Duplicate Installations of DevTools Detected',
content: <DialogContent />,
});
}, []);
return null;
}
function DialogContent(_: {||}) {
return (
<Fragment>
<p>
We detected that there are multiple versions of React Developer Tools
installed and enabled in your browser at the same time, which will cause
issues while using the extension.
</p>
{isInternalFacebookBuild ? (
<p>
Before proceeding, please ensure that the only enabled version of
React Developer Tools is the internal (Chef-installed) version. To
manage your extensions, visit the <code>about://extensions</code> page
in your browser.
</p>
) : (
<p>
Please ensure that you have installed and enabled only a single
version of React Developer Tools before proceeding. To manage your
extensions, visit the <code>about://extensions</code> page in your
browser.
</p>
)}
</Fragment>
);
}