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. import {createElement} from 'react';
    
  11. import {flushSync} from 'react-dom';
    
  12. import {createRoot} from 'react-dom/client';
    
  13. import Bridge from 'react-devtools-shared/src/bridge';
    
  14. import Store from 'react-devtools-shared/src/devtools/store';
    
  15. import {
    
  16.   getAppendComponentStack,
    
  17.   getBreakOnConsoleErrors,
    
  18.   getSavedComponentFilters,
    
  19.   getShowInlineWarningsAndErrors,
    
  20.   getHideConsoleLogsInStrictMode,
    
  21. } from 'react-devtools-shared/src/utils';
    
  22. import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDevToolsEventLogger';
    
  23. import {Server} from 'ws';
    
  24. import {join} from 'path';
    
  25. import {readFileSync} from 'fs';
    
  26. import {installHook} from 'react-devtools-shared/src/hook';
    
  27. import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
    
  28. import {doesFilePathExist, launchEditor} from './editor';
    
  29. import {
    
  30.   __DEBUG__,
    
  31.   LOCAL_STORAGE_DEFAULT_TAB_KEY,
    
  32. } from 'react-devtools-shared/src/constants';
    
  33. import {localStorageSetItem} from 'react-devtools-shared/src/storage';
    
  34. 
    
  35. import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
    
  36. import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
    
  37. 
    
  38. installHook(window);
    
  39. 
    
  40. export type StatusTypes = 'server-connected' | 'devtools-connected' | 'error';
    
  41. export type StatusListener = (message: string, status: StatusTypes) => void;
    
  42. export type OnDisconnectedCallback = () => void;
    
  43. 
    
  44. let node: HTMLElement = ((null: any): HTMLElement);
    
  45. let nodeWaitingToConnectHTML: string = '';
    
  46. let projectRoots: Array<string> = [];
    
  47. let statusListener: StatusListener = (
    
  48.   message: string,
    
  49.   status?: StatusTypes,
    
  50. ) => {};
    
  51. let disconnectedCallback: OnDisconnectedCallback = () => {};
    
  52. 
    
  53. // TODO (Webpack 5) Hopefully we can remove this prop after the Webpack 5 migration.
    
  54. function hookNamesModuleLoaderFunction() {
    
  55.   return import(
    
  56.     /* webpackChunkName: 'parseHookNames' */ 'react-devtools-shared/src/hooks/parseHookNames'
    
  57.   );
    
  58. }
    
  59. 
    
  60. function setContentDOMNode(value: HTMLElement): typeof DevtoolsUI {
    
  61.   node = value;
    
  62. 
    
  63.   // Save so we can restore the exact waiting message between sessions.
    
  64.   nodeWaitingToConnectHTML = node.innerHTML;
    
  65. 
    
  66.   return DevtoolsUI;
    
  67. }
    
  68. 
    
  69. function setProjectRoots(value: Array<string>) {
    
  70.   projectRoots = value;
    
  71. }
    
  72. 
    
  73. function setStatusListener(value: StatusListener): typeof DevtoolsUI {
    
  74.   statusListener = value;
    
  75.   return DevtoolsUI;
    
  76. }
    
  77. 
    
  78. function setDisconnectedCallback(
    
  79.   value: OnDisconnectedCallback,
    
  80. ): typeof DevtoolsUI {
    
  81.   disconnectedCallback = value;
    
  82.   return DevtoolsUI;
    
  83. }
    
  84. 
    
  85. let bridge: FrontendBridge | null = null;
    
  86. let store: Store | null = null;
    
  87. let root = null;
    
  88. 
    
  89. const log = (...args: Array<mixed>) => console.log('[React DevTools]', ...args);
    
  90. log.warn = (...args: Array<mixed>) => console.warn('[React DevTools]', ...args);
    
  91. log.error = (...args: Array<mixed>) =>
    
  92.   console.error('[React DevTools]', ...args);
    
  93. 
    
  94. function debug(methodName: string, ...args: Array<mixed>) {
    
  95.   if (__DEBUG__) {
    
  96.     console.log(
    
  97.       `%c[core/standalone] %c${methodName}`,
    
  98.       'color: teal; font-weight: bold;',
    
  99.       'font-weight: bold;',
    
  100.       ...args,
    
  101.     );
    
  102.   }
    
  103. }
    
  104. 
    
  105. function safeUnmount() {
    
  106.   flushSync(() => {
    
  107.     if (root !== null) {
    
  108.       root.unmount();
    
  109.       root = null;
    
  110.     }
    
  111.   });
    
  112. }
    
  113. 
    
  114. function reload() {
    
  115.   safeUnmount();
    
  116. 
    
  117.   node.innerHTML = '';
    
  118. 
    
  119.   setTimeout(() => {
    
  120.     root = createRoot(node);
    
  121.     root.render(
    
  122.       createElement(DevTools, {
    
  123.         bridge: ((bridge: any): FrontendBridge),
    
  124.         canViewElementSourceFunction,
    
  125.         hookNamesModuleLoaderFunction,
    
  126.         showTabBar: true,
    
  127.         store: ((store: any): Store),
    
  128.         warnIfLegacyBackendDetected: true,
    
  129.         viewElementSourceFunction,
    
  130.       }),
    
  131.     );
    
  132.   }, 100);
    
  133. }
    
  134. 
    
  135. function canViewElementSourceFunction(
    
  136.   inspectedElement: InspectedElement,
    
  137. ): boolean {
    
  138.   if (
    
  139.     inspectedElement.canViewSource === false ||
    
  140.     inspectedElement.source === null
    
  141.   ) {
    
  142.     return false;
    
  143.   }
    
  144. 
    
  145.   const {source} = inspectedElement;
    
  146. 
    
  147.   return doesFilePathExist(source.fileName, projectRoots);
    
  148. }
    
  149. 
    
  150. function viewElementSourceFunction(
    
  151.   id: number,
    
  152.   inspectedElement: InspectedElement,
    
  153. ): void {
    
  154.   const {source} = inspectedElement;
    
  155.   if (source !== null) {
    
  156.     launchEditor(source.fileName, source.lineNumber, projectRoots);
    
  157.   } else {
    
  158.     log.error('Cannot inspect element', id);
    
  159.   }
    
  160. }
    
  161. 
    
  162. function onDisconnected() {
    
  163.   safeUnmount();
    
  164. 
    
  165.   node.innerHTML = nodeWaitingToConnectHTML;
    
  166. 
    
  167.   disconnectedCallback();
    
  168. }
    
  169. 
    
  170. function onError({code, message}: $FlowFixMe) {
    
  171.   safeUnmount();
    
  172. 
    
  173.   if (code === 'EADDRINUSE') {
    
  174.     node.innerHTML = `
    
  175.       <div class="box">
    
  176.         <div class="box-header">
    
  177.           Another instance of DevTools is running.
    
  178.         </div>
    
  179.         <div class="box-content">
    
  180.           Only one copy of DevTools can be used at a time.
    
  181.         </div>
    
  182.       </div>
    
  183.     `;
    
  184.   } else {
    
  185.     node.innerHTML = `
    
  186.       <div class="box">
    
  187.         <div class="box-header">
    
  188.           Unknown error
    
  189.         </div>
    
  190.         <div class="box-content">
    
  191.           ${message}
    
  192.         </div>
    
  193.       </div>
    
  194.     `;
    
  195.   }
    
  196. }
    
  197. 
    
  198. function openProfiler() {
    
  199.   // Mocked up bridge and store to allow the DevTools to be rendered
    
  200.   bridge = new Bridge({listen: () => {}, send: () => {}});
    
  201.   store = new Store(bridge, {});
    
  202. 
    
  203.   // Ensure the Profiler tab is shown initially.
    
  204.   localStorageSetItem(
    
  205.     LOCAL_STORAGE_DEFAULT_TAB_KEY,
    
  206.     JSON.stringify('profiler'),
    
  207.   );
    
  208. 
    
  209.   reload();
    
  210. }
    
  211. 
    
  212. function initialize(socket: WebSocket) {
    
  213.   const listeners = [];
    
  214.   socket.onmessage = event => {
    
  215.     let data;
    
  216.     try {
    
  217.       if (typeof event.data === 'string') {
    
  218.         data = JSON.parse(event.data);
    
  219. 
    
  220.         if (__DEBUG__) {
    
  221.           debug('WebSocket.onmessage', data);
    
  222.         }
    
  223.       } else {
    
  224.         throw Error();
    
  225.       }
    
  226.     } catch (e) {
    
  227.       log.error('Failed to parse JSON', event.data);
    
  228.       return;
    
  229.     }
    
  230.     listeners.forEach(fn => {
    
  231.       try {
    
  232.         fn(data);
    
  233.       } catch (error) {
    
  234.         log.error('Error calling listener', data);
    
  235.         throw error;
    
  236.       }
    
  237.     });
    
  238.   };
    
  239. 
    
  240.   bridge = new Bridge({
    
  241.     listen(fn) {
    
  242.       listeners.push(fn);
    
  243.       return () => {
    
  244.         const index = listeners.indexOf(fn);
    
  245.         if (index >= 0) {
    
  246.           listeners.splice(index, 1);
    
  247.         }
    
  248.       };
    
  249.     },
    
  250.     send(event: string, payload: any, transferable?: Array<any>) {
    
  251.       if (socket.readyState === socket.OPEN) {
    
  252.         socket.send(JSON.stringify({event, payload}));
    
  253.       }
    
  254.     },
    
  255.   });
    
  256.   ((bridge: any): FrontendBridge).addListener('shutdown', () => {
    
  257.     socket.close();
    
  258.   });
    
  259. 
    
  260.   // $FlowFixMe[incompatible-call] found when upgrading Flow
    
  261.   store = new Store(bridge, {
    
  262.     checkBridgeProtocolCompatibility: true,
    
  263.     supportsNativeInspection: true,
    
  264.     supportsTraceUpdates: true,
    
  265.   });
    
  266. 
    
  267.   log('Connected');
    
  268.   statusListener('DevTools initialized.', 'devtools-connected');
    
  269.   reload();
    
  270. }
    
  271. 
    
  272. let startServerTimeoutID: TimeoutID | null = null;
    
  273. 
    
  274. function connectToSocket(socket: WebSocket): {close(): void} {
    
  275.   socket.onerror = err => {
    
  276.     onDisconnected();
    
  277.     log.error('Error with websocket connection', err);
    
  278.   };
    
  279.   socket.onclose = () => {
    
  280.     onDisconnected();
    
  281.     log('Connection to RN closed');
    
  282.   };
    
  283.   initialize(socket);
    
  284. 
    
  285.   return {
    
  286.     close: function () {
    
  287.       onDisconnected();
    
  288.     },
    
  289.   };
    
  290. }
    
  291. 
    
  292. type ServerOptions = {
    
  293.   key?: string,
    
  294.   cert?: string,
    
  295. };
    
  296. 
    
  297. type LoggerOptions = {
    
  298.   surface?: ?string,
    
  299. };
    
  300. 
    
  301. function startServer(
    
  302.   port: number = 8097,
    
  303.   host: string = 'localhost',
    
  304.   httpsOptions?: ServerOptions,
    
  305.   loggerOptions?: LoggerOptions,
    
  306. ): {close(): void} {
    
  307.   registerDevToolsEventLogger(loggerOptions?.surface ?? 'standalone');
    
  308. 
    
  309.   const useHttps = !!httpsOptions;
    
  310.   const httpServer = useHttps
    
  311.     ? require('https').createServer(httpsOptions)
    
  312.     : require('http').createServer();
    
  313.   const server = new Server({server: httpServer});
    
  314.   let connected: WebSocket | null = null;
    
  315.   server.on('connection', (socket: WebSocket) => {
    
  316.     if (connected !== null) {
    
  317.       connected.close();
    
  318.       log.warn(
    
  319.         'Only one connection allowed at a time.',
    
  320.         'Closing the previous connection',
    
  321.       );
    
  322.     }
    
  323.     connected = socket;
    
  324.     socket.onerror = error => {
    
  325.       connected = null;
    
  326.       onDisconnected();
    
  327.       log.error('Error with websocket connection', error);
    
  328.     };
    
  329.     socket.onclose = () => {
    
  330.       connected = null;
    
  331.       onDisconnected();
    
  332.       log('Connection to RN closed');
    
  333.     };
    
  334.     initialize(socket);
    
  335.   });
    
  336. 
    
  337.   server.on('error', (event: $FlowFixMe) => {
    
  338.     onError(event);
    
  339.     log.error('Failed to start the DevTools server', event);
    
  340.     startServerTimeoutID = setTimeout(() => startServer(port), 1000);
    
  341.   });
    
  342. 
    
  343.   httpServer.on('request', (request: $FlowFixMe, response: $FlowFixMe) => {
    
  344.     // Serve a file that immediately sets up the connection.
    
  345.     const backendFile = readFileSync(join(__dirname, 'backend.js'));
    
  346. 
    
  347.     // The renderer interface doesn't read saved component filters directly,
    
  348.     // because they are generally stored in localStorage within the context of the extension.
    
  349.     // Because of this it relies on the extension to pass filters, so include them wth the response here.
    
  350.     // This will ensure that saved filters are shared across different web pages.
    
  351.     const savedPreferencesString = `
    
  352.       window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
    
  353.         getAppendComponentStack(),
    
  354.       )};
    
  355.       window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify(
    
  356.         getBreakOnConsoleErrors(),
    
  357.       )};
    
  358.       window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
    
  359.         getSavedComponentFilters(),
    
  360.       )};
    
  361.       window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
    
  362.         getShowInlineWarningsAndErrors(),
    
  363.       )};
    
  364.       window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = ${JSON.stringify(
    
  365.         getHideConsoleLogsInStrictMode(),
    
  366.       )};`;
    
  367. 
    
  368.     response.end(
    
  369.       savedPreferencesString +
    
  370.         '\n;' +
    
  371.         backendFile.toString() +
    
  372.         '\n;' +
    
  373.         `ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${
    
  374.           useHttps ? 'true' : 'false'
    
  375.         }});`,
    
  376.     );
    
  377.   });
    
  378. 
    
  379.   httpServer.on('error', (event: $FlowFixMe) => {
    
  380.     onError(event);
    
  381.     statusListener('Failed to start the server.', 'error');
    
  382.     startServerTimeoutID = setTimeout(() => startServer(port), 1000);
    
  383.   });
    
  384. 
    
  385.   httpServer.listen(port, () => {
    
  386.     statusListener(
    
  387.       'The server is listening on the port ' + port + '.',
    
  388.       'server-connected',
    
  389.     );
    
  390.   });
    
  391. 
    
  392.   return {
    
  393.     close: function () {
    
  394.       connected = null;
    
  395.       onDisconnected();
    
  396.       if (startServerTimeoutID !== null) {
    
  397.         clearTimeout(startServerTimeoutID);
    
  398.       }
    
  399.       server.close();
    
  400.       httpServer.close();
    
  401.     },
    
  402.   };
    
  403. }
    
  404. 
    
  405. const DevtoolsUI = {
    
  406.   connectToSocket,
    
  407.   setContentDOMNode,
    
  408.   setProjectRoots,
    
  409.   setStatusListener,
    
  410.   setDisconnectedCallback,
    
  411.   startServer,
    
  412.   openProfiler,
    
  413. };
    
  414. 
    
  415. export default DevtoolsUI;