import {createElement} from 'react';
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_CHROME, IS_EDGE, getBrowserTheme, IS_FIREFOX} from '../utils';
import {
localStorageGetItem,
localStorageSetItem,
} from 'react-devtools-shared/src/storage';
import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
import {
__DEBUG__,
LOCAL_STORAGE_SUPPORTS_PROFILING_KEY,
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
} from 'react-devtools-shared/src/constants';
import {logEvent} from 'react-devtools-shared/src/Logger';
import {
setBrowserSelectionFromReact,
setReactSelectionFromBrowser,
} from './elementSelection';
import cloneStyleTags from './cloneStyleTags';
import injectBackendManager from './injectBackendManager';
import syncSavedPreferences from './syncSavedPreferences';
import registerEventsLogger from './registerEventsLogger';
import getProfilingFlags from './getProfilingFlags';
import debounce from './debounce';
import './requestAnimationFramePolyfill';
const REACT_POLLING_TICK_COOLDOWN = 250;
const REACT_POLLING_ATTEMPTS_THRESHOLD = 20;
let reactPollingTimeoutId = null;
export function clearReactPollingTimeout() {
clearTimeout(reactPollingTimeoutId);
reactPollingTimeoutId = null;
}
export function executeIfReactHasLoaded(callback, attempt = 1) {
clearReactPollingTimeout();
if (attempt > REACT_POLLING_ATTEMPTS_THRESHOLD) {
return;
}
chrome.devtools.inspectedWindow.eval(
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
(pageHasReact, exceptionInfo) => {
if (exceptionInfo) {
const {code, description, isError, isException, value} = exceptionInfo;
if (isException) {
console.error(
`Received error while checking if react has loaded: ${value}`,
);
return;
}
if (isError) {
console.error(
`Received error with code ${code} while checking if react has loaded: ${description}`,
);
return;
}
}
if (pageHasReact) {
callback();
} else {
reactPollingTimeoutId = setTimeout(
executeIfReactHasLoaded,
REACT_POLLING_TICK_COOLDOWN,
callback,
attempt + 1,
);
}
},
);
}
let lastSubscribedBridgeListener = null;
function createBridge() {
bridge = new Bridge({
listen(fn) {
const bridgeListener = message => fn(message);
const portOnMessage = port.onMessage;
portOnMessage.addListener(bridgeListener);
lastSubscribedBridgeListener = bridgeListener;
return () => {
port?.onMessage.removeListener(bridgeListener);
lastSubscribedBridgeListener = null;
};
},
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,
);
bridge.addListener('extensionBackendInitialized', () => {
bridge.send(
'setTraceUpdatesEnabled',
localStorageGetItem(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY) === 'true',
);
});
const onBrowserElementSelectionChanged = () =>
setReactSelectionFromBrowser(bridge);
const onBridgeShutdown = () => {
chrome.devtools.panels.elements.onSelectionChanged.removeListener(
onBrowserElementSelectionChanged,
);
};
bridge.addListener('shutdown', onBridgeShutdown);
chrome.devtools.panels.elements.onSelectionChanged.addListener(
onBrowserElementSelectionChanged,
);
}
function createBridgeAndStore() {
createBridge();
const {isProfiling, supportsProfiling} = getProfilingFlags();
store = new Store(bridge, {
isProfiling,
supportsReloadAndProfile: IS_CHROME || IS_EDGE,
supportsProfiling,
supportsTimeline: IS_CHROME,
supportsTraceUpdates: true,
});
if (!isProfiling) {
store.profilerStore.profilingData = profilingData;
}
injectBackendManager(chrome.devtools.inspectedWindow.tabId);
const viewAttributeSourceFunction = (id, path) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID != null) {
bridge.send('viewAttributeSource', {id, path, rendererID});
setTimeout(() => {
chrome.devtools.inspectedWindow.eval(`
if (window.$attribute != null) {
inspect(window.$attribute);
}
`);
}, 100);
}
};
const viewElementSourceFunction = id => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID != null) {
bridge.send('viewElementSource', {id, rendererID});
setTimeout(() => {
chrome.devtools.inspectedWindow.eval(`
if (window.$type != null) {
if (
window.$type &&
window.$type.prototype &&
window.$type.prototype.isReactComponent
) {
// inspect Component.render, not constructor
inspect(window.$type.prototype.render);
} else {
// inspect Functional Component
inspect(window.$type);
}
}
`);
}, 100);
}
};
let debugIDCounter = 0;
let fetchFileWithCaching = null;
if (IS_CHROME) {
const fetchFromNetworkCache = (url, resolve, reject) => {
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,
);
}
fetchFromPage(url, resolve, reject);
}
});
return;
}
}
if (__DEBUG__) {
console.log(
`[main] fetchFromNetworkCache(${debugID}) No cached request found in getHAR()`,
);
}
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}",
},
});
`);
};
fetchFileWithCaching = url => {
return new Promise((resolve, reject) => {
fetchFromNetworkCache(url, resolve, reject);
});
};
}
const hookNamesModuleLoaderFunction = () =>
import(
'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,
viewUrlSourceFunction,
}),
);
};
}
const viewUrlSourceFunction = (url, line, col) => {
chrome.devtools.panels.openResource(url, line, col);
};
function ensureInitialHTMLIsCleared(container) {
if (container._hasInitialHTMLBeenCleared) {
return;
}
container.innerHTML = '';
container._hasInitialHTMLBeenCleared = true;
}
function createComponentsPanel() {
if (componentsPortalContainer) {
render('components');
return;
}
if (componentsPanel) {
return;
}
chrome.devtools.panels.create(
IS_CHROME || IS_EDGE ? '⚛️ Components' : 'Components',
IS_EDGE ? 'icons/production.svg' : '',
'panel.html',
createdPanel => {
componentsPanel = createdPanel;
createdPanel.onShown.addListener(portal => {
componentsPortalContainer = portal.container;
if (componentsPortalContainer != null) {
ensureInitialHTMLIsCleared(componentsPortalContainer);
render('components');
portal.injectStyles(cloneStyleTags);
logEvent({event_name: 'selected-components-tab'});
}
});
},
);
}
function createProfilerPanel() {
if (profilerPortalContainer) {
render('profiler');
return;
}
if (profilerPanel) {
return;
}
chrome.devtools.panels.create(
IS_CHROME || IS_EDGE ? '⚛️ Profiler' : 'Profiler',
IS_EDGE ? 'icons/production.svg' : '',
'panel.html',
createdPanel => {
profilerPanel = createdPanel;
createdPanel.onShown.addListener(portal => {
profilerPortalContainer = portal.container;
if (profilerPortalContainer != null) {
ensureInitialHTMLIsCleared(profilerPortalContainer);
render('profiler');
portal.injectStyles(cloneStyleTags);
logEvent({event_name: 'selected-profiler-tab'});
}
});
},
);
}
function performInTabNavigationCleanup() {
clearReactPollingTimeout();
if (store !== null) {
profilingData = store.profilerStore.profilingData;
}
if ((componentsPortalContainer || profilerPortalContainer) && root) {
flushSync(() => root.unmount());
} else {
bridge?.shutdown();
}
store = null;
bridge = null;
render = null;
root = null;
}
function performFullCleanup() {
clearReactPollingTimeout();
if ((componentsPortalContainer || profilerPortalContainer) && root) {
flushSync(() => root.unmount());
} else {
bridge?.shutdown();
}
componentsPortalContainer = null;
profilerPortalContainer = null;
root = null;
mostRecentOverrideTab = null;
store = null;
bridge = null;
render = null;
port?.disconnect();
port = null;
}
function connectExtensionPort() {
if (port) {
throw new Error('DevTools port was already connected');
}
const tabId = chrome.devtools.inspectedWindow.tabId;
port = chrome.runtime.connect({
name: String(tabId),
});
if (lastSubscribedBridgeListener) {
port.onMessage.addListener(lastSubscribedBridgeListener);
}
port.onDisconnect.addListener(() => {
port = null;
connectExtensionPort();
});
}
function mountReactDevTools() {
registerEventsLogger();
createBridgeAndStore();
setReactSelectionFromBrowser(bridge);
createComponentsPanel();
createProfilerPanel();
}
function mountReactDevToolsWhenReactHasLoaded() {
function onReactReady() {
clearReactPollingTimeout();
mountReactDevTools();
}
executeIfReactHasLoaded(onReactReady, 1);
}
let bridge = null;
let store = null;
let profilingData = null;
let componentsPanel = null;
let profilerPanel = null;
let componentsPortalContainer = null;
let profilerPortalContainer = null;
let mostRecentOverrideTab = null;
let render = null;
let root = null;
let port = null;
chrome.devtools.network.onNavigated.addListener(syncSavedPreferences);
const debouncedOnNavigatedListener = debounce(() => {
performInTabNavigationCleanup();
mountReactDevToolsWhenReactHasLoaded();
}, 500);
chrome.devtools.network.onNavigated.addListener(debouncedOnNavigatedListener);
if (IS_FIREFOX) {
window.addEventListener('unload', performFullCleanup);
} else {
window.addEventListener('beforeunload', performFullCleanup);
}
connectExtensionPort();
syncSavedPreferences();
mountReactDevToolsWhenReactHasLoaded();