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 EventEmitter from './events';
    
  11. 
    
  12. import type {ComponentFilter, Wall} from './frontend/types';
    
  13. import type {
    
  14.   InspectedElementPayload,
    
  15.   OwnersList,
    
  16.   ProfilingDataBackend,
    
  17.   RendererID,
    
  18.   ConsolePatchSettings,
    
  19. } from 'react-devtools-shared/src/backend/types';
    
  20. import type {StyleAndLayout as StyleAndLayoutPayload} from 'react-devtools-shared/src/backend/NativeStyleEditor/types';
    
  21. 
    
  22. const BATCH_DURATION = 100;
    
  23. 
    
  24. // This message specifies the version of the DevTools protocol currently supported by the backend,
    
  25. // as well as the earliest NPM version (e.g. "4.13.0") that protocol is supported by on the frontend.
    
  26. // This enables an older frontend to display an upgrade message to users for a newer, unsupported backend.
    
  27. export type BridgeProtocol = {
    
  28.   // Version supported by the current frontend/backend.
    
  29.   version: number,
    
  30. 
    
  31.   // NPM version range that also supports this version.
    
  32.   // Note that 'maxNpmVersion' is only set when the version is bumped.
    
  33.   minNpmVersion: string,
    
  34.   maxNpmVersion: string | null,
    
  35. };
    
  36. 
    
  37. // Bump protocol version whenever a backwards breaking change is made
    
  38. // in the messages sent between BackendBridge and FrontendBridge.
    
  39. // This mapping is embedded in both frontend and backend builds.
    
  40. //
    
  41. // The backend protocol will always be the latest entry in the BRIDGE_PROTOCOL array.
    
  42. //
    
  43. // When an older frontend connects to a newer backend,
    
  44. // the backend can send the minNpmVersion and the frontend can display an NPM upgrade prompt.
    
  45. //
    
  46. // When a newer frontend connects with an older protocol version,
    
  47. // the frontend can use the embedded minNpmVersion/maxNpmVersion values to display a downgrade prompt.
    
  48. export const BRIDGE_PROTOCOL: Array<BridgeProtocol> = [
    
  49.   // This version technically never existed,
    
  50.   // but a backwards breaking change was added in 4.11,
    
  51.   // so the safest guess to downgrade the frontend would be to version 4.10.
    
  52.   {
    
  53.     version: 0,
    
  54.     minNpmVersion: '"<4.11.0"',
    
  55.     maxNpmVersion: '"<4.11.0"',
    
  56.   },
    
  57.   // Versions 4.11.x – 4.12.x contained the backwards breaking change,
    
  58.   // but we didn't add the "fix" of checking the protocol version until 4.13,
    
  59.   // so we don't recommend downgrading to 4.11 or 4.12.
    
  60.   {
    
  61.     version: 1,
    
  62.     minNpmVersion: '4.13.0',
    
  63.     maxNpmVersion: '4.21.0',
    
  64.   },
    
  65.   // Version 2 adds a StrictMode-enabled and supports-StrictMode bits to add-root operation.
    
  66.   {
    
  67.     version: 2,
    
  68.     minNpmVersion: '4.22.0',
    
  69.     maxNpmVersion: null,
    
  70.   },
    
  71. ];
    
  72. 
    
  73. export const currentBridgeProtocol: BridgeProtocol =
    
  74.   BRIDGE_PROTOCOL[BRIDGE_PROTOCOL.length - 1];
    
  75. 
    
  76. type ElementAndRendererID = {id: number, rendererID: RendererID};
    
  77. 
    
  78. type Message = {
    
  79.   event: string,
    
  80.   payload: any,
    
  81. };
    
  82. 
    
  83. type HighlightElementInDOM = {
    
  84.   ...ElementAndRendererID,
    
  85.   displayName: string | null,
    
  86.   hideAfterTimeout: boolean,
    
  87.   openNativeElementsPanel: boolean,
    
  88.   scrollIntoView: boolean,
    
  89. };
    
  90. 
    
  91. type OverrideValue = {
    
  92.   ...ElementAndRendererID,
    
  93.   path: Array<string | number>,
    
  94.   wasForwarded?: boolean,
    
  95.   value: any,
    
  96. };
    
  97. 
    
  98. type OverrideHookState = {
    
  99.   ...OverrideValue,
    
  100.   hookID: number,
    
  101. };
    
  102. 
    
  103. type PathType = 'props' | 'hooks' | 'state' | 'context';
    
  104. 
    
  105. type DeletePath = {
    
  106.   ...ElementAndRendererID,
    
  107.   type: PathType,
    
  108.   hookID?: ?number,
    
  109.   path: Array<string | number>,
    
  110. };
    
  111. 
    
  112. type RenamePath = {
    
  113.   ...ElementAndRendererID,
    
  114.   type: PathType,
    
  115.   hookID?: ?number,
    
  116.   oldPath: Array<string | number>,
    
  117.   newPath: Array<string | number>,
    
  118. };
    
  119. 
    
  120. type OverrideValueAtPath = {
    
  121.   ...ElementAndRendererID,
    
  122.   type: PathType,
    
  123.   hookID?: ?number,
    
  124.   path: Array<string | number>,
    
  125.   value: any,
    
  126. };
    
  127. 
    
  128. type OverrideError = {
    
  129.   ...ElementAndRendererID,
    
  130.   forceError: boolean,
    
  131. };
    
  132. 
    
  133. type OverrideSuspense = {
    
  134.   ...ElementAndRendererID,
    
  135.   forceFallback: boolean,
    
  136. };
    
  137. 
    
  138. type CopyElementPathParams = {
    
  139.   ...ElementAndRendererID,
    
  140.   path: Array<string | number>,
    
  141. };
    
  142. 
    
  143. type ViewAttributeSourceParams = {
    
  144.   ...ElementAndRendererID,
    
  145.   path: Array<string | number>,
    
  146. };
    
  147. 
    
  148. type InspectElementParams = {
    
  149.   ...ElementAndRendererID,
    
  150.   forceFullData: boolean,
    
  151.   path: Array<number | string> | null,
    
  152.   requestID: number,
    
  153. };
    
  154. 
    
  155. type StoreAsGlobalParams = {
    
  156.   ...ElementAndRendererID,
    
  157.   count: number,
    
  158.   path: Array<string | number>,
    
  159. };
    
  160. 
    
  161. type NativeStyleEditor_RenameAttributeParams = {
    
  162.   ...ElementAndRendererID,
    
  163.   oldName: string,
    
  164.   newName: string,
    
  165.   value: string,
    
  166. };
    
  167. 
    
  168. type NativeStyleEditor_SetValueParams = {
    
  169.   ...ElementAndRendererID,
    
  170.   name: string,
    
  171.   value: string,
    
  172. };
    
  173. 
    
  174. type SavedPreferencesParams = {
    
  175.   appendComponentStack: boolean,
    
  176.   breakOnConsoleErrors: boolean,
    
  177.   componentFilters: Array<ComponentFilter>,
    
  178.   showInlineWarningsAndErrors: boolean,
    
  179.   hideConsoleLogsInStrictMode: boolean,
    
  180. };
    
  181. 
    
  182. export type BackendEvents = {
    
  183.   backendVersion: [string],
    
  184.   bridgeProtocol: [BridgeProtocol],
    
  185.   extensionBackendInitialized: [],
    
  186.   fastRefreshScheduled: [],
    
  187.   getSavedPreferences: [],
    
  188.   inspectedElement: [InspectedElementPayload],
    
  189.   isBackendStorageAPISupported: [boolean],
    
  190.   isSynchronousXHRSupported: [boolean],
    
  191.   operations: [Array<number>],
    
  192.   ownersList: [OwnersList],
    
  193.   overrideComponentFilters: [Array<ComponentFilter>],
    
  194.   profilingData: [ProfilingDataBackend],
    
  195.   profilingStatus: [boolean],
    
  196.   reloadAppForProfiling: [],
    
  197.   saveToClipboard: [string],
    
  198.   selectFiber: [number],
    
  199.   shutdown: [],
    
  200.   stopInspectingNative: [boolean],
    
  201.   syncSelectionFromNativeElementsPanel: [],
    
  202.   syncSelectionToNativeElementsPanel: [],
    
  203.   unsupportedRendererVersion: [RendererID],
    
  204. 
    
  205.   // React Native style editor plug-in.
    
  206.   isNativeStyleEditorSupported: [
    
  207.     {isSupported: boolean, validAttributes: ?$ReadOnlyArray<string>},
    
  208.   ],
    
  209.   NativeStyleEditor_styleAndLayout: [StyleAndLayoutPayload],
    
  210. };
    
  211. 
    
  212. type FrontendEvents = {
    
  213.   clearErrorsAndWarnings: [{rendererID: RendererID}],
    
  214.   clearErrorsForFiberID: [ElementAndRendererID],
    
  215.   clearNativeElementHighlight: [],
    
  216.   clearWarningsForFiberID: [ElementAndRendererID],
    
  217.   copyElementPath: [CopyElementPathParams],
    
  218.   deletePath: [DeletePath],
    
  219.   getBackendVersion: [],
    
  220.   getBridgeProtocol: [],
    
  221.   getOwnersList: [ElementAndRendererID],
    
  222.   getProfilingData: [{rendererID: RendererID}],
    
  223.   getProfilingStatus: [],
    
  224.   highlightNativeElement: [HighlightElementInDOM],
    
  225.   inspectElement: [InspectElementParams],
    
  226.   logElementToConsole: [ElementAndRendererID],
    
  227.   overrideError: [OverrideError],
    
  228.   overrideSuspense: [OverrideSuspense],
    
  229.   overrideValueAtPath: [OverrideValueAtPath],
    
  230.   profilingData: [ProfilingDataBackend],
    
  231.   reloadAndProfile: [boolean],
    
  232.   renamePath: [RenamePath],
    
  233.   savedPreferences: [SavedPreferencesParams],
    
  234.   selectFiber: [number],
    
  235.   setTraceUpdatesEnabled: [boolean],
    
  236.   shutdown: [],
    
  237.   startInspectingNative: [],
    
  238.   startProfiling: [boolean],
    
  239.   stopInspectingNative: [boolean],
    
  240.   stopProfiling: [],
    
  241.   storeAsGlobal: [StoreAsGlobalParams],
    
  242.   updateComponentFilters: [Array<ComponentFilter>],
    
  243.   updateConsolePatchSettings: [ConsolePatchSettings],
    
  244.   viewAttributeSource: [ViewAttributeSourceParams],
    
  245.   viewElementSource: [ElementAndRendererID],
    
  246. 
    
  247.   // React Native style editor plug-in.
    
  248.   NativeStyleEditor_measure: [ElementAndRendererID],
    
  249.   NativeStyleEditor_renameAttribute: [NativeStyleEditor_RenameAttributeParams],
    
  250.   NativeStyleEditor_setValue: [NativeStyleEditor_SetValueParams],
    
  251. 
    
  252.   // Temporarily support newer standalone front-ends sending commands to older embedded backends.
    
  253.   // We do this because React Native embeds the React DevTools backend,
    
  254.   // but cannot control which version of the frontend users use.
    
  255.   //
    
  256.   // Note that nothing in the newer backend actually listens to these events,
    
  257.   // but the new frontend still dispatches them (in case older backends are listening to them instead).
    
  258.   //
    
  259.   // Note that this approach does no support the combination of a newer backend with an older frontend.
    
  260.   // It would be more work to support both approaches (and not run handlers twice)
    
  261.   // so I chose to support the more likely/common scenario (and the one more difficult for an end user to "fix").
    
  262.   overrideContext: [OverrideValue],
    
  263.   overrideHookState: [OverrideHookState],
    
  264.   overrideProps: [OverrideValue],
    
  265.   overrideState: [OverrideValue],
    
  266. 
    
  267.   resumeElementPolling: [],
    
  268.   pauseElementPolling: [],
    
  269. };
    
  270. 
    
  271. class Bridge<
    
  272.   OutgoingEvents: Object,
    
  273.   IncomingEvents: Object,
    
  274. > extends EventEmitter<{
    
  275.   ...IncomingEvents,
    
  276.   ...OutgoingEvents,
    
  277. }> {
    
  278.   _isShutdown: boolean = false;
    
  279.   _messageQueue: Array<any> = [];
    
  280.   _timeoutID: TimeoutID | null = null;
    
  281.   _wall: Wall;
    
  282.   _wallUnlisten: Function | null = null;
    
  283. 
    
  284.   constructor(wall: Wall) {
    
  285.     super();
    
  286. 
    
  287.     this._wall = wall;
    
  288. 
    
  289.     this._wallUnlisten =
    
  290.       wall.listen((message: Message) => {
    
  291.         if (message && message.event) {
    
  292.           (this: any).emit(message.event, message.payload);
    
  293.         }
    
  294.       }) || null;
    
  295. 
    
  296.     // Temporarily support older standalone front-ends sending commands to newer embedded backends.
    
  297.     // We do this because React Native embeds the React DevTools backend,
    
  298.     // but cannot control which version of the frontend users use.
    
  299.     this.addListener('overrideValueAtPath', this.overrideValueAtPath);
    
  300.   }
    
  301. 
    
  302.   // Listening directly to the wall isn't advised.
    
  303.   // It can be used to listen for legacy (v3) messages (since they use a different format).
    
  304.   get wall(): Wall {
    
  305.     return this._wall;
    
  306.   }
    
  307. 
    
  308.   send<EventName: $Keys<OutgoingEvents>>(
    
  309.     event: EventName,
    
  310.     ...payload: $ElementType<OutgoingEvents, EventName>
    
  311.   ) {
    
  312.     if (this._isShutdown) {
    
  313.       console.warn(
    
  314.         `Cannot send message "${event}" through a Bridge that has been shutdown.`,
    
  315.       );
    
  316.       return;
    
  317.     }
    
  318. 
    
  319.     // When we receive a message:
    
  320.     // - we add it to our queue of messages to be sent
    
  321.     // - if there hasn't been a message recently, we set a timer for 0 ms in
    
  322.     //   the future, allowing all messages created in the same tick to be sent
    
  323.     //   together
    
  324.     // - if there *has* been a message flushed in the last BATCH_DURATION ms
    
  325.     //   (or we're waiting for our setTimeout-0 to fire), then _timeoutID will
    
  326.     //   be set, and we'll simply add to the queue and wait for that
    
  327.     this._messageQueue.push(event, payload);
    
  328.     if (!this._timeoutID) {
    
  329.       this._timeoutID = setTimeout(this._flush, 0);
    
  330.     }
    
  331.   }
    
  332. 
    
  333.   shutdown() {
    
  334.     if (this._isShutdown) {
    
  335.       console.warn('Bridge was already shutdown.');
    
  336.       return;
    
  337.     }
    
  338. 
    
  339.     // Queue the shutdown outgoing message for subscribers.
    
  340.     this.emit('shutdown');
    
  341.     this.send('shutdown');
    
  342. 
    
  343.     // Mark this bridge as destroyed, i.e. disable its public API.
    
  344.     this._isShutdown = true;
    
  345. 
    
  346.     // Disable the API inherited from EventEmitter that can add more listeners and send more messages.
    
  347.     // $FlowFixMe[cannot-write] This property is not writable.
    
  348.     this.addListener = function () {};
    
  349.     // $FlowFixMe[cannot-write] This property is not writable.
    
  350.     this.emit = function () {};
    
  351.     // NOTE: There's also EventEmitter API like `on` and `prependListener` that we didn't add to our Flow type of EventEmitter.
    
  352. 
    
  353.     // Unsubscribe this bridge incoming message listeners to be sure, and so they don't have to do that.
    
  354.     this.removeAllListeners();
    
  355. 
    
  356.     // Stop accepting and emitting incoming messages from the wall.
    
  357.     const wallUnlisten = this._wallUnlisten;
    
  358.     if (wallUnlisten) {
    
  359.       wallUnlisten();
    
  360.     }
    
  361. 
    
  362.     // Synchronously flush all queued outgoing messages.
    
  363.     // At this step the subscribers' code may run in this call stack.
    
  364.     do {
    
  365.       this._flush();
    
  366.     } while (this._messageQueue.length);
    
  367. 
    
  368.     // Make sure once again that there is no dangling timer.
    
  369.     if (this._timeoutID !== null) {
    
  370.       clearTimeout(this._timeoutID);
    
  371.       this._timeoutID = null;
    
  372.     }
    
  373.   }
    
  374. 
    
  375.   _flush: () => void = () => {
    
  376.     // This method is used after the bridge is marked as destroyed in shutdown sequence,
    
  377.     // so we do not bail out if the bridge marked as destroyed.
    
  378.     // It is a private method that the bridge ensures is only called at the right times.
    
  379. 
    
  380.     if (this._timeoutID !== null) {
    
  381.       clearTimeout(this._timeoutID);
    
  382.       this._timeoutID = null;
    
  383.     }
    
  384. 
    
  385.     if (this._messageQueue.length) {
    
  386.       for (let i = 0; i < this._messageQueue.length; i += 2) {
    
  387.         this._wall.send(this._messageQueue[i], ...this._messageQueue[i + 1]);
    
  388.       }
    
  389.       this._messageQueue.length = 0;
    
  390. 
    
  391.       // Check again for queued messages in BATCH_DURATION ms. This will keep
    
  392.       // flushing in a loop as long as messages continue to be added. Once no
    
  393.       // more are, the timer expires.
    
  394.       this._timeoutID = setTimeout(this._flush, BATCH_DURATION);
    
  395.     }
    
  396.   };
    
  397. 
    
  398.   // Temporarily support older standalone backends by forwarding "overrideValueAtPath" commands
    
  399.   // to the older message types they may be listening to.
    
  400.   overrideValueAtPath: OverrideValueAtPath => void = ({
    
  401.     id,
    
  402.     path,
    
  403.     rendererID,
    
  404.     type,
    
  405.     value,
    
  406.   }: OverrideValueAtPath) => {
    
  407.     switch (type) {
    
  408.       case 'context':
    
  409.         this.send('overrideContext', {
    
  410.           id,
    
  411.           path,
    
  412.           rendererID,
    
  413.           wasForwarded: true,
    
  414.           value,
    
  415.         });
    
  416.         break;
    
  417.       case 'hooks':
    
  418.         this.send('overrideHookState', {
    
  419.           id,
    
  420.           path,
    
  421.           rendererID,
    
  422.           wasForwarded: true,
    
  423.           value,
    
  424.         });
    
  425.         break;
    
  426.       case 'props':
    
  427.         this.send('overrideProps', {
    
  428.           id,
    
  429.           path,
    
  430.           rendererID,
    
  431.           wasForwarded: true,
    
  432.           value,
    
  433.         });
    
  434.         break;
    
  435.       case 'state':
    
  436.         this.send('overrideState', {
    
  437.           id,
    
  438.           path,
    
  439.           rendererID,
    
  440.           wasForwarded: true,
    
  441.           value,
    
  442.         });
    
  443.         break;
    
  444.     }
    
  445.   };
    
  446. }
    
  447. 
    
  448. export type BackendBridge = Bridge<BackendEvents, FrontendEvents>;
    
  449. export type FrontendBridge = Bridge<FrontendEvents, BackendEvents>;
    
  450. 
    
  451. export default Bridge;