import type {RootType} from 'react-dom/src/client/ReactDOMRoot';
import type {FrontendBridge, Message} from 'react-devtools-shared/src/bridge';
import type {
TabID,
ViewElementSource,
} from 'react-devtools-shared/src/devtools/views/DevTools';
import type {SourceSelection} from 'react-devtools-shared/src/devtools/views/Editor/EditorPane';
import type {Element} from 'react-devtools-shared/src/frontend/types';
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 {getBrowserTheme} from '../utils';
import {
localStorageGetItem,
localStorageSetItem,
} from 'react-devtools-shared/src/storage';
import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
import {
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 {
getAlwaysOpenInEditor,
getOpenInEditorURL,
normalizeUrlIfValid,
} from 'react-devtools-shared/src/utils';
import {checkConditions} from 'react-devtools-shared/src/devtools/views/Editor/utils';
import * as parseHookNames from 'react-devtools-shared/src/hooks/parseHookNames';
import {
setBrowserSelectionFromReact,
setReactSelectionFromBrowser,
} from './elementSelection';
import {viewAttributeSource} from './sourceSelection';
import {evalInInspectedWindow} from './evalInInspectedWindow';
import {startReactPolling} from './reactPolling';
import {cloneStyleTags} from './cloneStyleTags';
import fetchFileWithCaching from './fetchFileWithCaching';
import injectBackendManager from './injectBackendManager';
import registerEventsLogger from './registerEventsLogger';
import getProfilingFlags from './getProfilingFlags';
import debounce from './debounce';
import './requestAnimationFramePolyfill';
const resolvedParseHookNames = Promise.resolve(parseHookNames);
const hookNamesModuleLoaderFunction = () => resolvedParseHookNames;
function createBridge() {
bridge = new Bridge({
listen(fn) {
const bridgeListener = (message: Message) => fn(message);
const portOnMessage = ((port: any): ExtensionPort).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');
evalInInspectedWindow('reload', [], () => {});
});
bridge.addListener(
'syncSelectionToBuiltinElementsPanel',
setBrowserSelectionFromReact,
);
bridge.addListener('extensionBackendInitialized', () => {
bridge.send(
'setTraceUpdatesEnabled',
localStorageGetItem(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY) === 'true',
);
});
const sourcesPanel = chrome.devtools.panels.sources;
const onBrowserElementSelectionChanged = () =>
setReactSelectionFromBrowser(bridge);
const onBrowserSourceSelectionChanged = (location: {
url: string,
startLine: number,
startColumn: number,
endLine: number,
endColumn: number,
}) => {
if (
currentSelectedSource === null ||
currentSelectedSource.url !== location.url
) {
currentSelectedSource = {
url: location.url,
selectionRef: {
line: location.startLine + 1,
column: location.startColumn + 1,
},
};
render();
} else {
const selectionRef = currentSelectedSource.selectionRef;
selectionRef.line = location.startLine + 1;
selectionRef.column = location.startColumn + 1;
}
};
const onBridgeShutdown = () => {
chrome.devtools.panels.elements.onSelectionChanged.removeListener(
onBrowserElementSelectionChanged,
);
if (sourcesPanel && sourcesPanel.onSelectionChanged) {
currentSelectedSource = null;
sourcesPanel.onSelectionChanged.removeListener(
onBrowserSourceSelectionChanged,
);
}
};
bridge.addListener('shutdown', onBridgeShutdown);
chrome.devtools.panels.elements.onSelectionChanged.addListener(
onBrowserElementSelectionChanged,
);
if (sourcesPanel && sourcesPanel.onSelectionChanged) {
sourcesPanel.onSelectionChanged.addListener(
onBrowserSourceSelectionChanged,
);
}
}
function createBridgeAndStore() {
createBridge();
const {isProfiling} = getProfilingFlags();
store = new Store(bridge, {
isProfiling,
supportsReloadAndProfile: __IS_CHROME__ || __IS_EDGE__,
supportsTimeline: __IS_CHROME__,
supportsTraceUpdates: true,
supportsInspectMatchingDOMElement: true,
supportsClickToInspect: true,
});
store.addListener('enableSuspenseTab', () => {
createSuspensePanel();
});
store.addListener('settingsUpdated', (hookSettings, componentFilters) => {
chrome.storage.local.set({...hookSettings, componentFilters});
});
if (!isProfiling) {
store.profilerStore.profilingData = profilingData;
}
injectBackendManager(chrome.devtools.inspectedWindow.tabId);
const viewAttributeSourceFunction = (
id: Element['id'],
path: Array<string | number>,
) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID != null) {
viewAttributeSource(rendererID, id, path);
}
};
const viewElementSourceFunction: ViewElementSource = (
source,
symbolicatedSource,
) => {
const [, sourceURL, line, column] = symbolicatedSource
? symbolicatedSource
: source;
chrome.devtools.panels.openResource(
normalizeUrlIfValid(sourceURL),
line - 1,
column - 1,
);
};
root = createRoot(document.createElement('div'));
render = (overrideTab: TabID | null = mostRecentOverrideTab) => {
mostRecentOverrideTab = overrideTab;
root.render(
createElement(DevTools, {
bridge,
browserTheme: getBrowserTheme(),
componentsPortalContainer,
inspectedElementPortalContainer,
profilerPortalContainer,
editorPortalContainer,
currentSelectedSource,
enabledInspectedElementContextMenu: true,
fetchFileWithCaching,
hookNamesModuleLoaderFunction,
overrideTab,
showTabBar: false,
store,
suspensePortalContainer,
warnIfUnsupportedVersionDetected: true,
viewAttributeSourceFunction,
canViewElementSourceFunction: () => __IS_CHROME__ || __IS_EDGE__,
viewElementSourceFunction,
}),
);
};
}
function ensureInitialHTMLIsCleared(
container: HTMLElement & {_hasInitialHTMLBeenCleared?: boolean},
) {
if (container._hasInitialHTMLBeenCleared) {
return;
}
container.innerHTML = '';
container._hasInitialHTMLBeenCleared = true;
}
function createComponentsPanel() {
if (componentsPortalContainer) {
ensureInitialHTMLIsCleared(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 && render) {
ensureInitialHTMLIsCleared(componentsPortalContainer);
render('components');
portal.injectStyles(cloneStyleTags);
logEvent({event_name: 'selected-components-tab'});
}
});
createdPanel.onShown.addListener(() => {
bridge.emit('extensionComponentsPanelShown');
});
createdPanel.onHidden.addListener(() => {
bridge.emit('extensionComponentsPanelHidden');
});
},
);
}
function createElementsInspectPanel() {
if (inspectedElementPortalContainer) {
ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
render();
return;
}
if (inspectedElementPane) {
return;
}
const elementsPanel = chrome.devtools.panels.elements;
if (__IS_FIREFOX__ || !elementsPanel || !elementsPanel.createSidebarPane) {
return;
}
elementsPanel.createSidebarPane('React Element ⚛', createdPane => {
inspectedElementPane = createdPane;
createdPane.setPage('panel.html');
createdPane.setHeight('75px');
createdPane.onShown.addListener(portal => {
inspectedElementPortalContainer = portal.container;
if (inspectedElementPortalContainer != null && render) {
ensureInitialHTMLIsCleared(inspectedElementPortalContainer);
bridge.send('syncSelectionFromBuiltinElementsPanel');
render();
portal.injectStyles(cloneStyleTags);
logEvent({event_name: 'selected-inspected-element-pane'});
}
});
});
}
function createProfilerPanel() {
if (profilerPortalContainer) {
ensureInitialHTMLIsCleared(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 && render) {
ensureInitialHTMLIsCleared(profilerPortalContainer);
render('profiler');
portal.injectStyles(cloneStyleTags);
logEvent({event_name: 'selected-profiler-tab'});
}
});
},
);
}
function createSourcesEditorPanel() {
if (editorPortalContainer) {
ensureInitialHTMLIsCleared(editorPortalContainer);
render();
return;
}
if (editorPane) {
return;
}
const sourcesPanel = chrome.devtools.panels.sources;
if (!sourcesPanel || !sourcesPanel.createSidebarPane) {
return;
}
sourcesPanel.createSidebarPane('Code Editor ⚛', createdPane => {
editorPane = createdPane;
createdPane.setPage('panel.html');
createdPane.setHeight('75px');
createdPane.onShown.addListener(portal => {
editorPortalContainer = portal.container;
if (editorPortalContainer != null && render) {
ensureInitialHTMLIsCleared(editorPortalContainer);
render();
portal.injectStyles(cloneStyleTags);
logEvent({event_name: 'selected-editor-pane'});
}
});
});
}
function createSuspensePanel() {
if (suspensePortalContainer) {
ensureInitialHTMLIsCleared(suspensePortalContainer);
render('suspense');
return;
}
if (suspensePanel) {
return;
}
chrome.devtools.panels.create(
__IS_CHROME__ || __IS_EDGE__ ? 'Suspense ⚛' : 'Suspense',
__IS_EDGE__ ? 'icons/production.svg' : '',
'panel.html',
createdPanel => {
suspensePanel = createdPanel;
createdPanel.onShown.addListener(portal => {
suspensePortalContainer = portal.container;
if (suspensePortalContainer != null && render) {
ensureInitialHTMLIsCleared(suspensePortalContainer);
render('suspense');
portal.injectStyles(cloneStyleTags);
logEvent({event_name: 'selected-suspense-tab'});
}
});
},
);
}
function performInTabNavigationCleanup() {
clearReactPollingInstance();
if (store !== null) {
profilingData = store.profilerStore.profilingData;
}
if (
(componentsPortalContainer ||
profilerPortalContainer ||
suspensePortalContainer) &&
root
) {
flushSync(() => root.unmount());
} else {
bridge?.shutdown();
}
store = (null: $FlowFixMe);
bridge = (null: $FlowFixMe);
render = (null: $FlowFixMe);
root = (null: $FlowFixMe);
}
function performFullCleanup() {
clearReactPollingInstance();
if (
(componentsPortalContainer ||
profilerPortalContainer ||
suspensePortalContainer) &&
root
) {
flushSync(() => root.unmount());
} else {
bridge?.shutdown();
}
componentsPortalContainer = null;
profilerPortalContainer = null;
suspensePortalContainer = null;
root = (null: $FlowFixMe);
mostRecentOverrideTab = null;
store = (null: $FlowFixMe);
bridge = (null: $FlowFixMe);
render = (null: $FlowFixMe);
port?.disconnect();
port = (null: $FlowFixMe);
}
function connectExtensionPort(): void {
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: $FlowFixMe);
connectExtensionPort();
});
}
function mountReactDevTools() {
reactPollingInstance = null;
registerEventsLogger();
createBridgeAndStore();
createComponentsPanel();
createProfilerPanel();
createSourcesEditorPanel();
createElementsInspectPanel();
}
let reactPollingInstance = null;
function clearReactPollingInstance() {
reactPollingInstance?.abort();
reactPollingInstance = null;
}
function showNoReactDisclaimer() {
if (componentsPortalContainer) {
componentsPortalContainer.innerHTML =
'<h1 class="no-react-disclaimer">Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.</h1>';
delete componentsPortalContainer._hasInitialHTMLBeenCleared;
}
if (profilerPortalContainer) {
profilerPortalContainer.innerHTML =
'<h1 class="no-react-disclaimer">Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.</h1>';
delete profilerPortalContainer._hasInitialHTMLBeenCleared;
}
if (suspensePortalContainer) {
suspensePortalContainer.innerHTML =
'<h1 class="no-react-disclaimer">Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.</h1>';
delete suspensePortalContainer._hasInitialHTMLBeenCleared;
}
}
function mountReactDevToolsWhenReactHasLoaded() {
reactPollingInstance = startReactPolling(
mountReactDevTools,
5,
showNoReactDisclaimer,
);
}
let bridge: FrontendBridge = (null: $FlowFixMe);
let lastSubscribedBridgeListener = null;
let store: Store = (null: $FlowFixMe);
let profilingData = null;
let componentsPanel = null;
let profilerPanel = null;
let suspensePanel = null;
let editorPane = null;
let inspectedElementPane = null;
let componentsPortalContainer = null;
let profilerPortalContainer = null;
let suspensePortalContainer = null;
let editorPortalContainer = null;
let inspectedElementPortalContainer = null;
let mostRecentOverrideTab: null | TabID = null;
let render: (overrideTab?: TabID) => void = (null: $FlowFixMe);
let root: RootType = (null: $FlowFixMe);
let currentSelectedSource: null | SourceSelection = null;
type ExtensionEvent = {
addListener(callback: (message: Message, port: ExtensionPort) => void): void,
removeListener(
callback: (message: Message, port: ExtensionPort) => void,
): void,
};
type ExtensionPort = {
onDisconnect: ExtensionEvent,
onMessage: ExtensionEvent,
postMessage(message: mixed, transferable?: Array<mixed>): void,
disconnect(): void,
};
let port: ExtensionPort = (null: $FlowFixMe);
const debouncedMountReactDevToolsCallback = debounce(
mountReactDevToolsWhenReactHasLoaded,
500,
);
function onNavigatedToOtherPage() {
performInTabNavigationCleanup();
debouncedMountReactDevToolsCallback();
}
chrome.devtools.network.onNavigated.addListener(onNavigatedToOtherPage);
if (__IS_FIREFOX__) {
window.addEventListener('unload', performFullCleanup);
} else {
window.addEventListener('beforeunload', performFullCleanup);
}
connectExtensionPort();
mountReactDevToolsWhenReactHasLoaded();
function onThemeChanged() {
render();
}
if (chrome.devtools.panels.setThemeChangeHandler) {
chrome.devtools.panels.setThemeChangeHandler(onThemeChanged);
} else if (chrome.devtools.panels.onThemeChanged) {
chrome.devtools.panels.onThemeChanged.addListener(onThemeChanged);
}
if (chrome.devtools.panels.setOpenResourceHandler) {
chrome.devtools.panels.setOpenResourceHandler(
(
resource,
lineNumber = 1,
columnNumber = 1,
) => {
const alwaysOpenInEditor = getAlwaysOpenInEditor();
const editorURL = getOpenInEditorURL();
if (alwaysOpenInEditor && editorURL) {
const location = ['', resource.url, lineNumber, columnNumber];
const {url, shouldDisableButton} = checkConditions(editorURL, location);
if (!shouldDisableButton) {
window.open(url);
return;
}
}
chrome.devtools.panels.openResource(
resource.url,
lineNumber - 1,
columnNumber - 1,
maybeError => {
if (maybeError && maybeError.isError) {
window.open(resource.url);
}
},
);
},
);
}