1. /* global chrome */
    
  2. 
    
  3. import {createElement} from 'react';
    
  4. import {flushSync} from 'react-dom';
    
  5. import {createRoot} from 'react-dom/client';
    
  6. import Bridge from 'react-devtools-shared/src/bridge';
    
  7. import Store from 'react-devtools-shared/src/devtools/store';
    
  8. import {getBrowserTheme} from '../utils';
    
  9. import {
    
  10.   localStorageGetItem,
    
  11.   localStorageSetItem,
    
  12. } from 'react-devtools-shared/src/storage';
    
  13. import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
    
  14. import {
    
  15.   LOCAL_STORAGE_SUPPORTS_PROFILING_KEY,
    
  16.   LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
    
  17. } from 'react-devtools-shared/src/constants';
    
  18. import {logEvent} from 'react-devtools-shared/src/Logger';
    
  19. 
    
  20. import {
    
  21.   setBrowserSelectionFromReact,
    
  22.   setReactSelectionFromBrowser,
    
  23. } from './elementSelection';
    
  24. import {startReactPolling} from './reactPolling';
    
  25. import cloneStyleTags from './cloneStyleTags';
    
  26. import fetchFileWithCaching from './fetchFileWithCaching';
    
  27. import injectBackendManager from './injectBackendManager';
    
  28. import syncSavedPreferences from './syncSavedPreferences';
    
  29. import registerEventsLogger from './registerEventsLogger';
    
  30. import getProfilingFlags from './getProfilingFlags';
    
  31. import debounce from './debounce';
    
  32. import './requestAnimationFramePolyfill';
    
  33. 
    
  34. function createBridge() {
    
  35.   bridge = new Bridge({
    
  36.     listen(fn) {
    
  37.       const bridgeListener = message => fn(message);
    
  38.       // Store the reference so that we unsubscribe from the same object.
    
  39.       const portOnMessage = port.onMessage;
    
  40.       portOnMessage.addListener(bridgeListener);
    
  41. 
    
  42.       lastSubscribedBridgeListener = bridgeListener;
    
  43. 
    
  44.       return () => {
    
  45.         port?.onMessage.removeListener(bridgeListener);
    
  46.         lastSubscribedBridgeListener = null;
    
  47.       };
    
  48.     },
    
  49. 
    
  50.     send(event: string, payload: any, transferable?: Array<any>) {
    
  51.       port?.postMessage({event, payload}, transferable);
    
  52.     },
    
  53.   });
    
  54. 
    
  55.   bridge.addListener('reloadAppForProfiling', () => {
    
  56.     localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
    
  57.     chrome.devtools.inspectedWindow.eval('window.location.reload();');
    
  58.   });
    
  59. 
    
  60.   bridge.addListener(
    
  61.     'syncSelectionToNativeElementsPanel',
    
  62.     setBrowserSelectionFromReact,
    
  63.   );
    
  64. 
    
  65.   bridge.addListener('extensionBackendInitialized', () => {
    
  66.     // Initialize the renderer's trace-updates setting.
    
  67.     // This handles the case of navigating to a new page after the DevTools have already been shown.
    
  68.     bridge.send(
    
  69.       'setTraceUpdatesEnabled',
    
  70.       localStorageGetItem(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY) === 'true',
    
  71.     );
    
  72.   });
    
  73. 
    
  74.   const onBrowserElementSelectionChanged = () =>
    
  75.     setReactSelectionFromBrowser(bridge);
    
  76.   const onBridgeShutdown = () => {
    
  77.     chrome.devtools.panels.elements.onSelectionChanged.removeListener(
    
  78.       onBrowserElementSelectionChanged,
    
  79.     );
    
  80.   };
    
  81. 
    
  82.   bridge.addListener('shutdown', onBridgeShutdown);
    
  83. 
    
  84.   chrome.devtools.panels.elements.onSelectionChanged.addListener(
    
  85.     onBrowserElementSelectionChanged,
    
  86.   );
    
  87. }
    
  88. 
    
  89. function createBridgeAndStore() {
    
  90.   createBridge();
    
  91. 
    
  92.   const {isProfiling, supportsProfiling} = getProfilingFlags();
    
  93. 
    
  94.   store = new Store(bridge, {
    
  95.     isProfiling,
    
  96.     supportsReloadAndProfile: __IS_CHROME__ || __IS_EDGE__,
    
  97.     supportsProfiling,
    
  98.     // At this time, the timeline can only parse Chrome performance profiles.
    
  99.     supportsTimeline: __IS_CHROME__,
    
  100.     supportsTraceUpdates: true,
    
  101.   });
    
  102. 
    
  103.   if (!isProfiling) {
    
  104.     // We previously stored this in performCleanup function
    
  105.     store.profilerStore.profilingData = profilingData;
    
  106.   }
    
  107. 
    
  108.   // Initialize the backend only once the Store has been initialized.
    
  109.   // Otherwise, the Store may miss important initial tree op codes.
    
  110.   injectBackendManager(chrome.devtools.inspectedWindow.tabId);
    
  111. 
    
  112.   const viewAttributeSourceFunction = (id, path) => {
    
  113.     const rendererID = store.getRendererIDForElement(id);
    
  114.     if (rendererID != null) {
    
  115.       // Ask the renderer interface to find the specified attribute,
    
  116.       // and store it as a global variable on the window.
    
  117.       bridge.send('viewAttributeSource', {id, path, rendererID});
    
  118. 
    
  119.       setTimeout(() => {
    
  120.         // Ask Chrome to display the location of the attribute,
    
  121.         // assuming the renderer found a match.
    
  122.         chrome.devtools.inspectedWindow.eval(`
    
  123.                 if (window.$attribute != null) {
    
  124.                   inspect(window.$attribute);
    
  125.                 }
    
  126.               `);
    
  127.       }, 100);
    
  128.     }
    
  129.   };
    
  130. 
    
  131.   const viewElementSourceFunction = id => {
    
  132.     const rendererID = store.getRendererIDForElement(id);
    
  133.     if (rendererID != null) {
    
  134.       // Ask the renderer interface to determine the component function,
    
  135.       // and store it as a global variable on the window
    
  136.       bridge.send('viewElementSource', {id, rendererID});
    
  137. 
    
  138.       setTimeout(() => {
    
  139.         // Ask Chrome to display the location of the component function,
    
  140.         // or a render method if it is a Class (ideally Class instance, not type)
    
  141.         // assuming the renderer found one.
    
  142.         chrome.devtools.inspectedWindow.eval(`
    
  143.                 if (window.$type != null) {
    
  144.                   if (
    
  145.                     window.$type &&
    
  146.                     window.$type.prototype &&
    
  147.                     window.$type.prototype.isReactComponent
    
  148.                   ) {
    
  149.                     // inspect Component.render, not constructor
    
  150.                     inspect(window.$type.prototype.render);
    
  151.                   } else {
    
  152.                     // inspect Functional Component
    
  153.                     inspect(window.$type);
    
  154.                   }
    
  155.                 }
    
  156.               `);
    
  157.       }, 100);
    
  158.     }
    
  159.   };
    
  160. 
    
  161.   // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
    
  162.   const hookNamesModuleLoaderFunction = () =>
    
  163.     import(
    
  164.       /* webpackChunkName: 'parseHookNames' */ 'react-devtools-shared/src/hooks/parseHookNames'
    
  165.     );
    
  166. 
    
  167.   root = createRoot(document.createElement('div'));
    
  168. 
    
  169.   render = (overrideTab = mostRecentOverrideTab) => {
    
  170.     mostRecentOverrideTab = overrideTab;
    
  171. 
    
  172.     root.render(
    
  173.       createElement(DevTools, {
    
  174.         bridge,
    
  175.         browserTheme: getBrowserTheme(),
    
  176.         componentsPortalContainer,
    
  177.         enabledInspectedElementContextMenu: true,
    
  178.         fetchFileWithCaching,
    
  179.         hookNamesModuleLoaderFunction,
    
  180.         overrideTab,
    
  181.         profilerPortalContainer,
    
  182.         showTabBar: false,
    
  183.         store,
    
  184.         warnIfUnsupportedVersionDetected: true,
    
  185.         viewAttributeSourceFunction,
    
  186.         viewElementSourceFunction,
    
  187.         viewUrlSourceFunction,
    
  188.       }),
    
  189.     );
    
  190.   };
    
  191. }
    
  192. 
    
  193. const viewUrlSourceFunction = (url, line, col) => {
    
  194.   chrome.devtools.panels.openResource(url, line, col);
    
  195. };
    
  196. 
    
  197. function ensureInitialHTMLIsCleared(container) {
    
  198.   if (container._hasInitialHTMLBeenCleared) {
    
  199.     return;
    
  200.   }
    
  201. 
    
  202.   container.innerHTML = '';
    
  203.   container._hasInitialHTMLBeenCleared = true;
    
  204. }
    
  205. 
    
  206. function createComponentsPanel() {
    
  207.   if (componentsPortalContainer) {
    
  208.     // Panel is created and user opened it at least once
    
  209.     ensureInitialHTMLIsCleared(componentsPortalContainer);
    
  210.     render('components');
    
  211. 
    
  212.     return;
    
  213.   }
    
  214. 
    
  215.   if (componentsPanel) {
    
  216.     // Panel is created, but wasn't opened yet, so no document is present for it
    
  217.     return;
    
  218.   }
    
  219. 
    
  220.   chrome.devtools.panels.create(
    
  221.     __IS_CHROME__ || __IS_EDGE__ ? '⚛️ Components' : 'Components',
    
  222.     __IS_EDGE__ ? 'icons/production.svg' : '',
    
  223.     'panel.html',
    
  224.     createdPanel => {
    
  225.       componentsPanel = createdPanel;
    
  226. 
    
  227.       createdPanel.onShown.addListener(portal => {
    
  228.         componentsPortalContainer = portal.container;
    
  229.         if (componentsPortalContainer != null && render) {
    
  230.           ensureInitialHTMLIsCleared(componentsPortalContainer);
    
  231. 
    
  232.           render('components');
    
  233.           portal.injectStyles(cloneStyleTags);
    
  234. 
    
  235.           logEvent({event_name: 'selected-components-tab'});
    
  236.         }
    
  237.       });
    
  238. 
    
  239.       // TODO: we should listen to createdPanel.onHidden to unmount some listeners
    
  240.       // and potentially stop highlighting
    
  241.     },
    
  242.   );
    
  243. }
    
  244. 
    
  245. function createProfilerPanel() {
    
  246.   if (profilerPortalContainer) {
    
  247.     // Panel is created and user opened it at least once
    
  248.     ensureInitialHTMLIsCleared(profilerPortalContainer);
    
  249.     render('profiler');
    
  250. 
    
  251.     return;
    
  252.   }
    
  253. 
    
  254.   if (profilerPanel) {
    
  255.     // Panel is created, but wasn't opened yet, so no document is present for it
    
  256.     return;
    
  257.   }
    
  258. 
    
  259.   chrome.devtools.panels.create(
    
  260.     __IS_CHROME__ || __IS_EDGE__ ? '⚛️ Profiler' : 'Profiler',
    
  261.     __IS_EDGE__ ? 'icons/production.svg' : '',
    
  262.     'panel.html',
    
  263.     createdPanel => {
    
  264.       profilerPanel = createdPanel;
    
  265. 
    
  266.       createdPanel.onShown.addListener(portal => {
    
  267.         profilerPortalContainer = portal.container;
    
  268.         if (profilerPortalContainer != null && render) {
    
  269.           ensureInitialHTMLIsCleared(profilerPortalContainer);
    
  270. 
    
  271.           render('profiler');
    
  272.           portal.injectStyles(cloneStyleTags);
    
  273. 
    
  274.           logEvent({event_name: 'selected-profiler-tab'});
    
  275.         }
    
  276.       });
    
  277.     },
    
  278.   );
    
  279. }
    
  280. 
    
  281. function performInTabNavigationCleanup() {
    
  282.   // Potentially, if react hasn't loaded yet and user performs in-tab navigation
    
  283.   clearReactPollingInstance();
    
  284. 
    
  285.   if (store !== null) {
    
  286.     // Store profiling data, so it can be used later
    
  287.     profilingData = store.profilerStore.profilingData;
    
  288.   }
    
  289. 
    
  290.   // If panels were already created, and we have already mounted React root to display
    
  291.   // tabs (Components or Profiler), we should unmount root first and render them again
    
  292.   if ((componentsPortalContainer || profilerPortalContainer) && root) {
    
  293.     // It's easiest to recreate the DevTools panel (to clean up potential stale state).
    
  294.     // We can revisit this in the future as a small optimization.
    
  295.     // This should also emit bridge.shutdown, but only if this root was mounted
    
  296.     flushSync(() => root.unmount());
    
  297.   } else {
    
  298.     // In case Browser DevTools were opened, but user never pressed on extension panels
    
  299.     // They were never mounted and there is nothing to unmount, but we need to emit shutdown event
    
  300.     // because bridge was already created
    
  301.     bridge?.shutdown();
    
  302.   }
    
  303. 
    
  304.   // Do not nullify componentsPanelPortal and profilerPanelPortal on purpose,
    
  305.   // They are not recreated when user does in-tab navigation, and they can only be accessed via
    
  306.   // callback in onShown listener, which is called only when panel has been shown
    
  307.   // This event won't be emitted again after in-tab navigation, if DevTools panel keeps being opened
    
  308. 
    
  309.   // Do not clean mostRecentOverrideTab on purpose, so we remember last opened
    
  310.   // React DevTools tab, when user does in-tab navigation
    
  311. 
    
  312.   store = null;
    
  313.   bridge = null;
    
  314.   render = null;
    
  315.   root = null;
    
  316. }
    
  317. 
    
  318. function performFullCleanup() {
    
  319.   // Potentially, if react hasn't loaded yet and user closed the browser DevTools
    
  320.   clearReactPollingInstance();
    
  321. 
    
  322.   if ((componentsPortalContainer || profilerPortalContainer) && root) {
    
  323.     // This should also emit bridge.shutdown, but only if this root was mounted
    
  324.     flushSync(() => root.unmount());
    
  325.   } else {
    
  326.     bridge?.shutdown();
    
  327.   }
    
  328. 
    
  329.   componentsPortalContainer = null;
    
  330.   profilerPortalContainer = null;
    
  331.   root = null;
    
  332. 
    
  333.   mostRecentOverrideTab = null;
    
  334.   store = null;
    
  335.   bridge = null;
    
  336.   render = null;
    
  337. 
    
  338.   port?.disconnect();
    
  339.   port = null;
    
  340. }
    
  341. 
    
  342. function connectExtensionPort() {
    
  343.   if (port) {
    
  344.     throw new Error('DevTools port was already connected');
    
  345.   }
    
  346. 
    
  347.   const tabId = chrome.devtools.inspectedWindow.tabId;
    
  348.   port = chrome.runtime.connect({
    
  349.     name: String(tabId),
    
  350.   });
    
  351. 
    
  352.   // If DevTools port was reconnected and Bridge was already created
    
  353.   // We should subscribe bridge to this port events
    
  354.   // This could happen if service worker dies and all ports are disconnected,
    
  355.   // but later user continues the session and Chrome reconnects all ports
    
  356.   // Bridge object is still in-memory, though
    
  357.   if (lastSubscribedBridgeListener) {
    
  358.     port.onMessage.addListener(lastSubscribedBridgeListener);
    
  359.   }
    
  360. 
    
  361.   // This port may be disconnected by Chrome at some point, this callback
    
  362.   // will be executed only if this port was disconnected from the other end
    
  363.   // so, when we call `port.disconnect()` from this script,
    
  364.   // this should not trigger this callback and port reconnection
    
  365.   port.onDisconnect.addListener(() => {
    
  366.     port = null;
    
  367.     connectExtensionPort();
    
  368.   });
    
  369. }
    
  370. 
    
  371. function mountReactDevTools() {
    
  372.   reactPollingInstance = null;
    
  373. 
    
  374.   registerEventsLogger();
    
  375. 
    
  376.   createBridgeAndStore();
    
  377. 
    
  378.   setReactSelectionFromBrowser(bridge);
    
  379. 
    
  380.   createComponentsPanel();
    
  381.   createProfilerPanel();
    
  382. }
    
  383. 
    
  384. let reactPollingInstance = null;
    
  385. function clearReactPollingInstance() {
    
  386.   reactPollingInstance?.abort();
    
  387.   reactPollingInstance = null;
    
  388. }
    
  389. 
    
  390. function showNoReactDisclaimer() {
    
  391.   if (componentsPortalContainer) {
    
  392.     componentsPortalContainer.innerHTML =
    
  393.       '<h1 class="no-react-disclaimer">Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.</h1>';
    
  394.     delete componentsPortalContainer._hasInitialHTMLBeenCleared;
    
  395.   }
    
  396. 
    
  397.   if (profilerPortalContainer) {
    
  398.     profilerPortalContainer.innerHTML =
    
  399.       '<h1 class="no-react-disclaimer">Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.</h1>';
    
  400.     delete profilerPortalContainer._hasInitialHTMLBeenCleared;
    
  401.   }
    
  402. }
    
  403. 
    
  404. function mountReactDevToolsWhenReactHasLoaded() {
    
  405.   reactPollingInstance = startReactPolling(
    
  406.     mountReactDevTools,
    
  407.     5, // ~5 seconds
    
  408.     showNoReactDisclaimer,
    
  409.   );
    
  410. }
    
  411. 
    
  412. let bridge = null;
    
  413. let lastSubscribedBridgeListener = null;
    
  414. let store = null;
    
  415. 
    
  416. let profilingData = null;
    
  417. 
    
  418. let componentsPanel = null;
    
  419. let profilerPanel = null;
    
  420. let componentsPortalContainer = null;
    
  421. let profilerPortalContainer = null;
    
  422. 
    
  423. let mostRecentOverrideTab = null;
    
  424. let render = null;
    
  425. let root = null;
    
  426. 
    
  427. let port = null;
    
  428. 
    
  429. // Re-initialize saved filters on navigation,
    
  430. // since global values stored on window get reset in this case.
    
  431. chrome.devtools.network.onNavigated.addListener(syncSavedPreferences);
    
  432. 
    
  433. // In case when multiple navigation events emitted in a short period of time
    
  434. // This debounced callback primarily used to avoid mounting React DevTools multiple times, which results
    
  435. // into subscribing to the same events from Bridge and window multiple times
    
  436. // In this case, we will handle `operations` event twice or more and user will see
    
  437. // `Cannot add node "1" because a node with that id is already in the Store.`
    
  438. const debouncedOnNavigatedListener = debounce(() => {
    
  439.   performInTabNavigationCleanup();
    
  440.   mountReactDevToolsWhenReactHasLoaded();
    
  441. }, 500);
    
  442. 
    
  443. // Cleanup previous page state and remount everything
    
  444. chrome.devtools.network.onNavigated.addListener(debouncedOnNavigatedListener);
    
  445. 
    
  446. // Should be emitted when browser DevTools are closed
    
  447. if (__IS_FIREFOX__) {
    
  448.   // For some reason Firefox doesn't emit onBeforeUnload event
    
  449.   window.addEventListener('unload', performFullCleanup);
    
  450. } else {
    
  451.   window.addEventListener('beforeunload', performFullCleanup);
    
  452. }
    
  453. 
    
  454. connectExtensionPort();
    
  455. 
    
  456. syncSavedPreferences();
    
  457. mountReactDevToolsWhenReactHasLoaded();