1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. // Reach styles need to come before any component styles.
    
  11. // This makes overriding the styles simpler.
    
  12. import '@reach/menu-button/styles.css';
    
  13. import '@reach/tooltip/styles.css';
    
  14. 
    
  15. import * as React from 'react';
    
  16. import {useCallback, useEffect, useLayoutEffect, useMemo, useRef} from 'react';
    
  17. import Store from '../store';
    
  18. import {
    
  19.   BridgeContext,
    
  20.   ContextMenuContext,
    
  21.   StoreContext,
    
  22.   OptionsContext,
    
  23. } from './context';
    
  24. import Components from './Components/Components';
    
  25. import Profiler from './Profiler/Profiler';
    
  26. import TabBar from './TabBar';
    
  27. import {SettingsContextController} from './Settings/SettingsContext';
    
  28. import {TreeContextController} from './Components/TreeContext';
    
  29. import ViewElementSourceContext from './Components/ViewElementSourceContext';
    
  30. import ViewSourceContext from './Components/ViewSourceContext';
    
  31. import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext';
    
  32. import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
    
  33. import {ProfilerContextController} from './Profiler/ProfilerContext';
    
  34. import {TimelineContextController} from 'react-devtools-timeline/src/TimelineContext';
    
  35. import {ModalDialogContextController} from './ModalDialog';
    
  36. import ReactLogo from './ReactLogo';
    
  37. import UnsupportedBridgeProtocolDialog from './UnsupportedBridgeProtocolDialog';
    
  38. import UnsupportedVersionDialog from './UnsupportedVersionDialog';
    
  39. import WarnIfLegacyBackendDetected from './WarnIfLegacyBackendDetected';
    
  40. import {useLocalStorage} from './hooks';
    
  41. import ThemeProvider from './ThemeProvider';
    
  42. import {LOCAL_STORAGE_DEFAULT_TAB_KEY} from '../../constants';
    
  43. import {logEvent} from '../../Logger';
    
  44. 
    
  45. import styles from './DevTools.css';
    
  46. 
    
  47. import './root.css';
    
  48. 
    
  49. import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
    
  50. import type {FetchFileWithCaching} from './Components/FetchFileWithCachingContext';
    
  51. import type {HookNamesModuleLoaderFunction} from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
    
  52. import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
    
  53. import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types';
    
  54. 
    
  55. export type TabID = 'components' | 'profiler';
    
  56. 
    
  57. export type ViewElementSource = (
    
  58.   id: number,
    
  59.   inspectedElement: InspectedElement,
    
  60. ) => void;
    
  61. export type ViewUrlSource = (url: string, row: number, column: number) => void;
    
  62. export type ViewAttributeSource = (
    
  63.   id: number,
    
  64.   path: Array<string | number>,
    
  65. ) => void;
    
  66. export type CanViewElementSource = (
    
  67.   inspectedElement: InspectedElement,
    
  68. ) => boolean;
    
  69. 
    
  70. export type Props = {
    
  71.   bridge: FrontendBridge,
    
  72.   browserTheme?: BrowserTheme,
    
  73.   canViewElementSourceFunction?: ?CanViewElementSource,
    
  74.   defaultTab?: TabID,
    
  75.   enabledInspectedElementContextMenu?: boolean,
    
  76.   showTabBar?: boolean,
    
  77.   store: Store,
    
  78.   warnIfLegacyBackendDetected?: boolean,
    
  79.   warnIfUnsupportedVersionDetected?: boolean,
    
  80.   viewAttributeSourceFunction?: ?ViewAttributeSource,
    
  81.   viewElementSourceFunction?: ?ViewElementSource,
    
  82.   viewUrlSourceFunction?: ?ViewUrlSource,
    
  83.   readOnly?: boolean,
    
  84.   hideSettings?: boolean,
    
  85.   hideToggleErrorAction?: boolean,
    
  86.   hideToggleSuspenseAction?: boolean,
    
  87.   hideLogAction?: boolean,
    
  88.   hideViewSourceAction?: boolean,
    
  89. 
    
  90.   // This property is used only by the web extension target.
    
  91.   // The built-in tab UI is hidden in that case, in favor of the browser's own panel tabs.
    
  92.   // This is done to save space within the app.
    
  93.   // Because of this, the extension needs to be able to change which tab is active/rendered.
    
  94.   overrideTab?: TabID,
    
  95. 
    
  96.   // To avoid potential multi-root trickiness, the web extension uses portals to render tabs.
    
  97.   // The root <DevTools> app is rendered in the top-level extension window,
    
  98.   // but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels.
    
  99.   componentsPortalContainer?: Element,
    
  100.   profilerPortalContainer?: Element,
    
  101. 
    
  102.   // Loads and parses source maps for function components
    
  103.   // and extracts hook "names" based on the variables the hook return values get assigned to.
    
  104.   // Not every DevTools build can load source maps, so this property is optional.
    
  105.   fetchFileWithCaching?: ?FetchFileWithCaching,
    
  106.   // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
    
  107.   hookNamesModuleLoaderFunction?: ?HookNamesModuleLoaderFunction,
    
  108. };
    
  109. 
    
  110. const componentsTab = {
    
  111.   id: ('components': TabID),
    
  112.   icon: 'components',
    
  113.   label: 'Components',
    
  114.   title: 'React Components',
    
  115. };
    
  116. const profilerTab = {
    
  117.   id: ('profiler': TabID),
    
  118.   icon: 'profiler',
    
  119.   label: 'Profiler',
    
  120.   title: 'React Profiler',
    
  121. };
    
  122. 
    
  123. const tabs = [componentsTab, profilerTab];
    
  124. 
    
  125. export default function DevTools({
    
  126.   bridge,
    
  127.   browserTheme = 'light',
    
  128.   canViewElementSourceFunction,
    
  129.   componentsPortalContainer,
    
  130.   defaultTab = 'components',
    
  131.   enabledInspectedElementContextMenu = false,
    
  132.   fetchFileWithCaching,
    
  133.   hookNamesModuleLoaderFunction,
    
  134.   overrideTab,
    
  135.   profilerPortalContainer,
    
  136.   showTabBar = false,
    
  137.   store,
    
  138.   warnIfLegacyBackendDetected = false,
    
  139.   warnIfUnsupportedVersionDetected = false,
    
  140.   viewAttributeSourceFunction,
    
  141.   viewElementSourceFunction,
    
  142.   viewUrlSourceFunction,
    
  143.   readOnly,
    
  144.   hideSettings,
    
  145.   hideToggleErrorAction,
    
  146.   hideToggleSuspenseAction,
    
  147.   hideLogAction,
    
  148.   hideViewSourceAction,
    
  149. }: Props): React.Node {
    
  150.   const [currentTab, setTab] = useLocalStorage<TabID>(
    
  151.     LOCAL_STORAGE_DEFAULT_TAB_KEY,
    
  152.     defaultTab,
    
  153.   );
    
  154. 
    
  155.   let tab = currentTab;
    
  156. 
    
  157.   if (overrideTab != null) {
    
  158.     tab = overrideTab;
    
  159.   }
    
  160. 
    
  161.   const selectTab = useCallback(
    
  162.     (tabId: TabID) => {
    
  163.       // We show the TabBar when DevTools is NOT rendered as a browser extension.
    
  164.       // In this case, we want to capture when people select tabs with the TabBar.
    
  165.       // When DevTools is rendered as an extension, we capture this event when
    
  166.       // the browser devtools panel changes.
    
  167.       if (showTabBar === true) {
    
  168.         if (tabId === 'components') {
    
  169.           logEvent({event_name: 'selected-components-tab'});
    
  170.         } else {
    
  171.           logEvent({event_name: 'selected-profiler-tab'});
    
  172.         }
    
  173.       }
    
  174.       setTab(tabId);
    
  175.     },
    
  176.     [setTab, showTabBar],
    
  177.   );
    
  178. 
    
  179.   const options = useMemo(
    
  180.     () => ({
    
  181.       readOnly: readOnly || false,
    
  182.       hideSettings: hideSettings || false,
    
  183.       hideToggleErrorAction: hideToggleErrorAction || false,
    
  184.       hideToggleSuspenseAction: hideToggleSuspenseAction || false,
    
  185.       hideLogAction: hideLogAction || false,
    
  186.       hideViewSourceAction: hideViewSourceAction || false,
    
  187.     }),
    
  188.     [
    
  189.       readOnly,
    
  190.       hideSettings,
    
  191.       hideToggleErrorAction,
    
  192.       hideToggleSuspenseAction,
    
  193.       hideLogAction,
    
  194.       hideViewSourceAction,
    
  195.     ],
    
  196.   );
    
  197. 
    
  198.   const viewElementSource = useMemo(
    
  199.     () => ({
    
  200.       canViewElementSourceFunction: canViewElementSourceFunction || null,
    
  201.       viewElementSourceFunction: viewElementSourceFunction || null,
    
  202.     }),
    
  203.     [canViewElementSourceFunction, viewElementSourceFunction],
    
  204.   );
    
  205. 
    
  206.   const viewSource = useMemo(
    
  207.     () => ({
    
  208.       viewUrlSourceFunction: viewUrlSourceFunction || null,
    
  209.       // todo(blakef): Add inspect(...) method here and remove viewElementSource
    
  210.       // to consolidate source code inspection.
    
  211.     }),
    
  212.     [viewUrlSourceFunction],
    
  213.   );
    
  214. 
    
  215.   const contextMenu = useMemo(
    
  216.     () => ({
    
  217.       isEnabledForInspectedElement: enabledInspectedElementContextMenu,
    
  218.       viewAttributeSourceFunction: viewAttributeSourceFunction || null,
    
  219.     }),
    
  220.     [enabledInspectedElementContextMenu, viewAttributeSourceFunction],
    
  221.   );
    
  222. 
    
  223.   const devToolsRef = useRef<HTMLElement | null>(null);
    
  224. 
    
  225.   useEffect(() => {
    
  226.     if (!showTabBar) {
    
  227.       return;
    
  228.     }
    
  229. 
    
  230.     const div = devToolsRef.current;
    
  231.     if (div === null) {
    
  232.       return;
    
  233.     }
    
  234. 
    
  235.     const ownerWindow = div.ownerDocument.defaultView;
    
  236.     const handleKeyDown = (event: KeyboardEvent) => {
    
  237.       if (event.ctrlKey || event.metaKey) {
    
  238.         switch (event.key) {
    
  239.           case '1':
    
  240.             selectTab(tabs[0].id);
    
  241.             event.preventDefault();
    
  242.             event.stopPropagation();
    
  243.             break;
    
  244.           case '2':
    
  245.             selectTab(tabs[1].id);
    
  246.             event.preventDefault();
    
  247.             event.stopPropagation();
    
  248.             break;
    
  249.         }
    
  250.       }
    
  251.     };
    
  252.     ownerWindow.addEventListener('keydown', handleKeyDown);
    
  253.     return () => {
    
  254.       ownerWindow.removeEventListener('keydown', handleKeyDown);
    
  255.     };
    
  256.   }, [showTabBar]);
    
  257. 
    
  258.   useLayoutEffect(() => {
    
  259.     return () => {
    
  260.       try {
    
  261.         // Shut the Bridge down synchronously (during unmount).
    
  262.         bridge.shutdown();
    
  263.       } catch (error) {
    
  264.         // Attempting to use a disconnected port.
    
  265.       }
    
  266.     };
    
  267.   }, [bridge]);
    
  268. 
    
  269.   useEffect(() => {
    
  270.     logEvent({event_name: 'loaded-dev-tools'});
    
  271.   }, []);
    
  272. 
    
  273.   return (
    
  274.     <BridgeContext.Provider value={bridge}>
    
  275.       <StoreContext.Provider value={store}>
    
  276.         <OptionsContext.Provider value={options}>
    
  277.           <ContextMenuContext.Provider value={contextMenu}>
    
  278.             <ModalDialogContextController>
    
  279.               <SettingsContextController
    
  280.                 browserTheme={browserTheme}
    
  281.                 componentsPortalContainer={componentsPortalContainer}
    
  282.                 profilerPortalContainer={profilerPortalContainer}>
    
  283.                 <ViewElementSourceContext.Provider value={viewElementSource}>
    
  284.                   <ViewSourceContext.Provider value={viewSource}>
    
  285.                     <HookNamesModuleLoaderContext.Provider
    
  286.                       value={hookNamesModuleLoaderFunction || null}>
    
  287.                       <FetchFileWithCachingContext.Provider
    
  288.                         value={fetchFileWithCaching || null}>
    
  289.                         <TreeContextController>
    
  290.                           <ProfilerContextController>
    
  291.                             <TimelineContextController>
    
  292.                               <ThemeProvider>
    
  293.                                 <div
    
  294.                                   className={styles.DevTools}
    
  295.                                   ref={devToolsRef}
    
  296.                                   data-react-devtools-portal-root={true}>
    
  297.                                   {showTabBar && (
    
  298.                                     <div className={styles.TabBar}>
    
  299.                                       <ReactLogo />
    
  300.                                       <span className={styles.DevToolsVersion}>
    
  301.                                         {process.env.DEVTOOLS_VERSION}
    
  302.                                       </span>
    
  303.                                       <div className={styles.Spacer} />
    
  304.                                       <TabBar
    
  305.                                         currentTab={tab}
    
  306.                                         id="DevTools"
    
  307.                                         selectTab={selectTab}
    
  308.                                         tabs={tabs}
    
  309.                                         type="navigation"
    
  310.                                       />
    
  311.                                     </div>
    
  312.                                   )}
    
  313.                                   <div
    
  314.                                     className={styles.TabContent}
    
  315.                                     hidden={tab !== 'components'}>
    
  316.                                     <Components
    
  317.                                       portalContainer={
    
  318.                                         componentsPortalContainer
    
  319.                                       }
    
  320.                                     />
    
  321.                                   </div>
    
  322.                                   <div
    
  323.                                     className={styles.TabContent}
    
  324.                                     hidden={tab !== 'profiler'}>
    
  325.                                     <Profiler
    
  326.                                       portalContainer={profilerPortalContainer}
    
  327.                                     />
    
  328.                                   </div>
    
  329.                                 </div>
    
  330.                               </ThemeProvider>
    
  331.                             </TimelineContextController>
    
  332.                           </ProfilerContextController>
    
  333.                         </TreeContextController>
    
  334.                       </FetchFileWithCachingContext.Provider>
    
  335.                     </HookNamesModuleLoaderContext.Provider>
    
  336.                   </ViewSourceContext.Provider>
    
  337.                 </ViewElementSourceContext.Provider>
    
  338.               </SettingsContextController>
    
  339.               <UnsupportedBridgeProtocolDialog />
    
  340.               {warnIfLegacyBackendDetected && <WarnIfLegacyBackendDetected />}
    
  341.               {warnIfUnsupportedVersionDetected && <UnsupportedVersionDialog />}
    
  342.             </ModalDialogContextController>
    
  343.           </ContextMenuContext.Provider>
    
  344.         </OptionsContext.Provider>
    
  345.       </StoreContext.Provider>
    
  346.     </BridgeContext.Provider>
    
  347.   );
    
  348. }