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 type {
    
  11.   DevToolsHook,
    
  12.   ReactRenderer,
    
  13. } from 'react-devtools-shared/src/backend/types';
    
  14. import {hasAssignedBackend} from 'react-devtools-shared/src/backend/utils';
    
  15. import {COMPACT_VERSION_NAME} from 'react-devtools-extensions/src/utils';
    
  16. 
    
  17. let welcomeHasInitialized = false;
    
  18. 
    
  19. function welcome(event: $FlowFixMe) {
    
  20.   if (
    
  21.     event.source !== window ||
    
  22.     event.data.source !== 'react-devtools-content-script'
    
  23.   ) {
    
  24.     return;
    
  25.   }
    
  26. 
    
  27.   // In some circumstances, this method is called more than once for a single welcome message.
    
  28.   // The exact circumstances of this are unclear, though it seems related to 3rd party event batching code.
    
  29.   //
    
  30.   // Regardless, call this method multiple times can cause DevTools to add duplicate elements to the Store
    
  31.   // (and throw an error) or worse yet, choke up entirely and freeze the browser.
    
  32.   //
    
  33.   // The simplest solution is to ignore the duplicate events.
    
  34.   // To be clear, this SHOULD NOT BE NECESSARY, since we remove the event handler below.
    
  35.   //
    
  36.   // See https://github.com/facebook/react/issues/24162
    
  37.   if (welcomeHasInitialized) {
    
  38.     console.warn(
    
  39.       'React DevTools detected duplicate welcome "message" events from the content script.',
    
  40.     );
    
  41.     return;
    
  42.   }
    
  43. 
    
  44.   welcomeHasInitialized = true;
    
  45. 
    
  46.   window.removeEventListener('message', welcome);
    
  47. 
    
  48.   setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
    
  49. }
    
  50. 
    
  51. window.addEventListener('message', welcome);
    
  52. 
    
  53. function setup(hook: ?DevToolsHook) {
    
  54.   // this should not happen, but Chrome can be weird sometimes
    
  55.   if (hook == null) {
    
  56.     return;
    
  57.   }
    
  58. 
    
  59.   // register renderers that have already injected themselves.
    
  60.   hook.renderers.forEach(renderer => {
    
  61.     registerRenderer(renderer, hook);
    
  62.   });
    
  63. 
    
  64.   // Activate and remove from required all present backends, registered within the hook
    
  65.   hook.backends.forEach((_, backendVersion) => {
    
  66.     requiredBackends.delete(backendVersion);
    
  67.     activateBackend(backendVersion, hook);
    
  68.   });
    
  69. 
    
  70.   updateRequiredBackends();
    
  71. 
    
  72.   // register renderers that inject themselves later.
    
  73.   hook.sub('renderer', ({renderer}) => {
    
  74.     registerRenderer(renderer, hook);
    
  75.     updateRequiredBackends();
    
  76.   });
    
  77. 
    
  78.   // listen for backend installations.
    
  79.   hook.sub('devtools-backend-installed', version => {
    
  80.     activateBackend(version, hook);
    
  81.     updateRequiredBackends();
    
  82.   });
    
  83. }
    
  84. 
    
  85. const requiredBackends = new Set<string>();
    
  86. 
    
  87. function registerRenderer(renderer: ReactRenderer, hook: DevToolsHook) {
    
  88.   let version = renderer.reconcilerVersion || renderer.version;
    
  89.   if (!hasAssignedBackend(version)) {
    
  90.     version = COMPACT_VERSION_NAME;
    
  91.   }
    
  92. 
    
  93.   // Check if required backend is already activated, no need to require again
    
  94.   if (!hook.backends.has(version)) {
    
  95.     requiredBackends.add(version);
    
  96.   }
    
  97. }
    
  98. 
    
  99. function activateBackend(version: string, hook: DevToolsHook) {
    
  100.   const backend = hook.backends.get(version);
    
  101.   if (!backend) {
    
  102.     throw new Error(`Could not find backend for version "${version}"`);
    
  103.   }
    
  104. 
    
  105.   const {Agent, Bridge, initBackend, setupNativeStyleEditor} = backend;
    
  106.   const bridge = new Bridge({
    
  107.     listen(fn) {
    
  108.       const listener = (event: $FlowFixMe) => {
    
  109.         if (
    
  110.           event.source !== window ||
    
  111.           !event.data ||
    
  112.           event.data.source !== 'react-devtools-content-script' ||
    
  113.           !event.data.payload
    
  114.         ) {
    
  115.           return;
    
  116.         }
    
  117.         fn(event.data.payload);
    
  118.       };
    
  119.       window.addEventListener('message', listener);
    
  120.       return () => {
    
  121.         window.removeEventListener('message', listener);
    
  122.       };
    
  123.     },
    
  124.     send(event: string, payload: any, transferable?: Array<any>) {
    
  125.       window.postMessage(
    
  126.         {
    
  127.           source: 'react-devtools-bridge',
    
  128.           payload: {event, payload},
    
  129.         },
    
  130.         '*',
    
  131.         transferable,
    
  132.       );
    
  133.     },
    
  134.   });
    
  135. 
    
  136.   const agent = new Agent(bridge);
    
  137.   agent.addListener('shutdown', () => {
    
  138.     // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
    
  139.     // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
    
  140.     hook.emit('shutdown');
    
  141.   });
    
  142. 
    
  143.   initBackend(hook, agent, window);
    
  144. 
    
  145.   // Setup React Native style editor if a renderer like react-native-web has injected it.
    
  146.   if (typeof setupNativeStyleEditor === 'function' && hook.resolveRNStyle) {
    
  147.     setupNativeStyleEditor(
    
  148.       bridge,
    
  149.       agent,
    
  150.       hook.resolveRNStyle,
    
  151.       hook.nativeStyleEditorValidAttributes,
    
  152.     );
    
  153.   }
    
  154. 
    
  155.   // Let the frontend know that the backend has attached listeners and is ready for messages.
    
  156.   // This covers the case of syncing saved values after reloading/navigating while DevTools remain open.
    
  157.   bridge.send('extensionBackendInitialized');
    
  158. 
    
  159.   // this backend is activated
    
  160.   requiredBackends.delete(version);
    
  161. }
    
  162. 
    
  163. // tell the service worker which versions of backends are needed for the current page
    
  164. function updateRequiredBackends() {
    
  165.   if (requiredBackends.size === 0) {
    
  166.     return;
    
  167.   }
    
  168. 
    
  169.   window.postMessage(
    
  170.     {
    
  171.       source: 'react-devtools-backend-manager',
    
  172.       payload: {
    
  173.         type: 'require-backends',
    
  174.         versions: Array.from(requiredBackends),
    
  175.       },
    
  176.     },
    
  177.     '*',
    
  178.   );
    
  179. }