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 Agent from 'react-devtools-shared/src/backend/agent';
    
  11. import Bridge from 'react-devtools-shared/src/bridge';
    
  12. import {installHook} from 'react-devtools-shared/src/hook';
    
  13. import {initBackend} from 'react-devtools-shared/src/backend';
    
  14. import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console';
    
  15. import {__DEBUG__} from 'react-devtools-shared/src/constants';
    
  16. import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
    
  17. import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils';
    
  18. import {
    
  19.   initializeUsingCachedSettings,
    
  20.   cacheConsolePatchSettings,
    
  21.   type DevToolsSettingsManager,
    
  22. } from './cachedSettings';
    
  23. 
    
  24. import type {BackendBridge} from 'react-devtools-shared/src/bridge';
    
  25. import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
    
  26. import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
    
  27. import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
    
  28. 
    
  29. type ConnectOptions = {
    
  30.   host?: string,
    
  31.   nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
    
  32.   port?: number,
    
  33.   useHttps?: boolean,
    
  34.   resolveRNStyle?: ResolveNativeStyle,
    
  35.   retryConnectionDelay?: number,
    
  36.   isAppActive?: () => boolean,
    
  37.   websocket?: ?WebSocket,
    
  38.   devToolsSettingsManager: ?DevToolsSettingsManager,
    
  39.   ...
    
  40. };
    
  41. 
    
  42. // Install a global variable to allow patching console early (during injection).
    
  43. // This provides React Native developers with components stacks even if they don't run DevTools.
    
  44. installConsoleFunctionsToWindow();
    
  45. installHook(window);
    
  46. 
    
  47. const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
    
  48. 
    
  49. let savedComponentFilters: Array<ComponentFilter> =
    
  50.   getDefaultComponentFilters();
    
  51. 
    
  52. function debug(methodName: string, ...args: Array<mixed>) {
    
  53.   if (__DEBUG__) {
    
  54.     console.log(
    
  55.       `%c[core/backend] %c${methodName}`,
    
  56.       'color: teal; font-weight: bold;',
    
  57.       'font-weight: bold;',
    
  58.       ...args,
    
  59.     );
    
  60.   }
    
  61. }
    
  62. 
    
  63. export function connectToDevTools(options: ?ConnectOptions) {
    
  64.   if (hook == null) {
    
  65.     // DevTools didn't get injected into this page (maybe b'c of the contentType).
    
  66.     return;
    
  67.   }
    
  68.   const {
    
  69.     host = 'localhost',
    
  70.     nativeStyleEditorValidAttributes,
    
  71.     useHttps = false,
    
  72.     port = 8097,
    
  73.     websocket,
    
  74.     resolveRNStyle = (null: $FlowFixMe),
    
  75.     retryConnectionDelay = 2000,
    
  76.     isAppActive = () => true,
    
  77.     devToolsSettingsManager,
    
  78.   } = options || {};
    
  79. 
    
  80.   const protocol = useHttps ? 'wss' : 'ws';
    
  81.   let retryTimeoutID: TimeoutID | null = null;
    
  82. 
    
  83.   function scheduleRetry() {
    
  84.     if (retryTimeoutID === null) {
    
  85.       // Two seconds because RN had issues with quick retries.
    
  86.       retryTimeoutID = setTimeout(
    
  87.         () => connectToDevTools(options),
    
  88.         retryConnectionDelay,
    
  89.       );
    
  90.     }
    
  91.   }
    
  92. 
    
  93.   if (devToolsSettingsManager != null) {
    
  94.     try {
    
  95.       initializeUsingCachedSettings(devToolsSettingsManager);
    
  96.     } catch (e) {
    
  97.       // If we call a method on devToolsSettingsManager that throws, or if
    
  98.       // is invalid data read out, don't throw and don't interrupt initialization
    
  99.       console.error(e);
    
  100.     }
    
  101.   }
    
  102. 
    
  103.   if (!isAppActive()) {
    
  104.     // If the app is in background, maybe retry later.
    
  105.     // Don't actually attempt to connect until we're in foreground.
    
  106.     scheduleRetry();
    
  107.     return;
    
  108.   }
    
  109. 
    
  110.   let bridge: BackendBridge | null = null;
    
  111. 
    
  112.   const messageListeners = [];
    
  113.   const uri = protocol + '://' + host + ':' + port;
    
  114. 
    
  115.   // If existing websocket is passed, use it.
    
  116.   // This is necessary to support our custom integrations.
    
  117.   // See D6251744.
    
  118.   const ws = websocket ? websocket : new window.WebSocket(uri);
    
  119.   ws.onclose = handleClose;
    
  120.   ws.onerror = handleFailed;
    
  121.   ws.onmessage = handleMessage;
    
  122.   ws.onopen = function () {
    
  123.     bridge = new Bridge({
    
  124.       listen(fn) {
    
  125.         messageListeners.push(fn);
    
  126.         return () => {
    
  127.           const index = messageListeners.indexOf(fn);
    
  128.           if (index >= 0) {
    
  129.             messageListeners.splice(index, 1);
    
  130.           }
    
  131.         };
    
  132.       },
    
  133.       send(event: string, payload: any, transferable?: Array<any>) {
    
  134.         if (ws.readyState === ws.OPEN) {
    
  135.           if (__DEBUG__) {
    
  136.             debug('wall.send()', event, payload);
    
  137.           }
    
  138. 
    
  139.           ws.send(JSON.stringify({event, payload}));
    
  140.         } else {
    
  141.           if (__DEBUG__) {
    
  142.             debug(
    
  143.               'wall.send()',
    
  144.               'Shutting down bridge because of closed WebSocket connection',
    
  145.             );
    
  146.           }
    
  147. 
    
  148.           if (bridge !== null) {
    
  149.             bridge.shutdown();
    
  150.           }
    
  151. 
    
  152.           scheduleRetry();
    
  153.         }
    
  154.       },
    
  155.     });
    
  156.     bridge.addListener(
    
  157.       'updateComponentFilters',
    
  158.       (componentFilters: Array<ComponentFilter>) => {
    
  159.         // Save filter changes in memory, in case DevTools is reloaded.
    
  160.         // In that case, the renderer will already be using the updated values.
    
  161.         // We'll lose these in between backend reloads but that can't be helped.
    
  162.         savedComponentFilters = componentFilters;
    
  163.       },
    
  164.     );
    
  165. 
    
  166.     if (devToolsSettingsManager != null && bridge != null) {
    
  167.       bridge.addListener('updateConsolePatchSettings', consolePatchSettings =>
    
  168.         cacheConsolePatchSettings(
    
  169.           devToolsSettingsManager,
    
  170.           consolePatchSettings,
    
  171.         ),
    
  172.       );
    
  173.     }
    
  174. 
    
  175.     // The renderer interface doesn't read saved component filters directly,
    
  176.     // because they are generally stored in localStorage within the context of the extension.
    
  177.     // Because of this it relies on the extension to pass filters.
    
  178.     // In the case of the standalone DevTools being used with a website,
    
  179.     // saved filters are injected along with the backend script tag so we shouldn't override them here.
    
  180.     // This injection strategy doesn't work for React Native though.
    
  181.     // Ideally the backend would save the filters itself, but RN doesn't provide a sync storage solution.
    
  182.     // So for now we just fall back to using the default filters...
    
  183.     if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) {
    
  184.       // $FlowFixMe[incompatible-use] found when upgrading Flow
    
  185.       bridge.send('overrideComponentFilters', savedComponentFilters);
    
  186.     }
    
  187. 
    
  188.     // TODO (npm-packages) Warn if "isBackendStorageAPISupported"
    
  189.     // $FlowFixMe[incompatible-call] found when upgrading Flow
    
  190.     const agent = new Agent(bridge);
    
  191.     agent.addListener('shutdown', () => {
    
  192.       // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
    
  193.       // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
    
  194.       hook.emit('shutdown');
    
  195.     });
    
  196. 
    
  197.     initBackend(hook, agent, window);
    
  198. 
    
  199.     // Setup React Native style editor if the environment supports it.
    
  200.     if (resolveRNStyle != null || hook.resolveRNStyle != null) {
    
  201.       setupNativeStyleEditor(
    
  202.         // $FlowFixMe[incompatible-call] found when upgrading Flow
    
  203.         bridge,
    
  204.         agent,
    
  205.         ((resolveRNStyle || hook.resolveRNStyle: any): ResolveNativeStyle),
    
  206.         nativeStyleEditorValidAttributes ||
    
  207.           hook.nativeStyleEditorValidAttributes ||
    
  208.           null,
    
  209.       );
    
  210.     } else {
    
  211.       // Otherwise listen to detect if the environment later supports it.
    
  212.       // For example, Flipper does not eagerly inject these values.
    
  213.       // Instead it relies on the React Native Inspector to lazily inject them.
    
  214.       let lazyResolveRNStyle;
    
  215.       let lazyNativeStyleEditorValidAttributes;
    
  216. 
    
  217.       const initAfterTick = () => {
    
  218.         if (bridge !== null) {
    
  219.           setupNativeStyleEditor(
    
  220.             bridge,
    
  221.             agent,
    
  222.             lazyResolveRNStyle,
    
  223.             lazyNativeStyleEditorValidAttributes,
    
  224.           );
    
  225.         }
    
  226.       };
    
  227. 
    
  228.       if (!hook.hasOwnProperty('resolveRNStyle')) {
    
  229.         Object.defineProperty(
    
  230.           hook,
    
  231.           'resolveRNStyle',
    
  232.           ({
    
  233.             enumerable: false,
    
  234.             get() {
    
  235.               return lazyResolveRNStyle;
    
  236.             },
    
  237.             set(value: $FlowFixMe) {
    
  238.               lazyResolveRNStyle = value;
    
  239.               initAfterTick();
    
  240.             },
    
  241.           }: Object),
    
  242.         );
    
  243.       }
    
  244.       if (!hook.hasOwnProperty('nativeStyleEditorValidAttributes')) {
    
  245.         Object.defineProperty(
    
  246.           hook,
    
  247.           'nativeStyleEditorValidAttributes',
    
  248.           ({
    
  249.             enumerable: false,
    
  250.             get() {
    
  251.               return lazyNativeStyleEditorValidAttributes;
    
  252.             },
    
  253.             set(value: $FlowFixMe) {
    
  254.               lazyNativeStyleEditorValidAttributes = value;
    
  255.               initAfterTick();
    
  256.             },
    
  257.           }: Object),
    
  258.         );
    
  259.       }
    
  260.     }
    
  261.   };
    
  262. 
    
  263.   function handleClose() {
    
  264.     if (__DEBUG__) {
    
  265.       debug('WebSocket.onclose');
    
  266.     }
    
  267. 
    
  268.     if (bridge !== null) {
    
  269.       bridge.emit('shutdown');
    
  270.     }
    
  271. 
    
  272.     scheduleRetry();
    
  273.   }
    
  274. 
    
  275.   function handleFailed() {
    
  276.     if (__DEBUG__) {
    
  277.       debug('WebSocket.onerror');
    
  278.     }
    
  279. 
    
  280.     scheduleRetry();
    
  281.   }
    
  282. 
    
  283.   function handleMessage(event: MessageEvent) {
    
  284.     let data;
    
  285.     try {
    
  286.       if (typeof event.data === 'string') {
    
  287.         data = JSON.parse(event.data);
    
  288.         if (__DEBUG__) {
    
  289.           debug('WebSocket.onmessage', data);
    
  290.         }
    
  291.       } else {
    
  292.         throw Error();
    
  293.       }
    
  294.     } catch (e) {
    
  295.       console.error(
    
  296.         '[React DevTools] Failed to parse JSON: ' + (event.data: any),
    
  297.       );
    
  298.       return;
    
  299.     }
    
  300.     messageListeners.forEach(fn => {
    
  301.       try {
    
  302.         fn(data);
    
  303.       } catch (error) {
    
  304.         // jsc doesn't play so well with tracebacks that go into eval'd code,
    
  305.         // so the stack trace here will stop at the `eval()` call. Getting the
    
  306.         // message that caused the error is the best we can do for now.
    
  307.         console.log('[React DevTools] Error calling listener', data);
    
  308.         console.log('error:', error);
    
  309.         throw error;
    
  310.       }
    
  311.     });
    
  312.   }
    
  313. }