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 {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
    
  11. import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
    
  12. import Store from 'react-devtools-shared/src/devtools/store';
    
  13. import TimeoutError from 'react-devtools-shared/src/errors/TimeoutError';
    
  14. import ElementPollingCancellationError from 'react-devtools-shared/src/errors/ElementPollingCancellationError';
    
  15. 
    
  16. import type {
    
  17.   InspectedElement as InspectedElementBackend,
    
  18.   InspectedElementPayload,
    
  19. } from 'react-devtools-shared/src/backend/types';
    
  20. import type {
    
  21.   BackendEvents,
    
  22.   FrontendBridge,
    
  23. } from 'react-devtools-shared/src/bridge';
    
  24. import type {
    
  25.   DehydratedData,
    
  26.   InspectedElement as InspectedElementFrontend,
    
  27. } from 'react-devtools-shared/src/frontend/types';
    
  28. import type {InspectedElementPath} from 'react-devtools-shared/src/frontend/types';
    
  29. 
    
  30. export function clearErrorsAndWarnings({
    
  31.   bridge,
    
  32.   store,
    
  33. }: {
    
  34.   bridge: FrontendBridge,
    
  35.   store: Store,
    
  36. }): void {
    
  37.   store.rootIDToRendererID.forEach(rendererID => {
    
  38.     bridge.send('clearErrorsAndWarnings', {rendererID});
    
  39.   });
    
  40. }
    
  41. 
    
  42. export function clearErrorsForElement({
    
  43.   bridge,
    
  44.   id,
    
  45.   rendererID,
    
  46. }: {
    
  47.   bridge: FrontendBridge,
    
  48.   id: number,
    
  49.   rendererID: number,
    
  50. }): void {
    
  51.   bridge.send('clearErrorsForFiberID', {
    
  52.     rendererID,
    
  53.     id,
    
  54.   });
    
  55. }
    
  56. 
    
  57. export function clearWarningsForElement({
    
  58.   bridge,
    
  59.   id,
    
  60.   rendererID,
    
  61. }: {
    
  62.   bridge: FrontendBridge,
    
  63.   id: number,
    
  64.   rendererID: number,
    
  65. }): void {
    
  66.   bridge.send('clearWarningsForFiberID', {
    
  67.     rendererID,
    
  68.     id,
    
  69.   });
    
  70. }
    
  71. 
    
  72. export function copyInspectedElementPath({
    
  73.   bridge,
    
  74.   id,
    
  75.   path,
    
  76.   rendererID,
    
  77. }: {
    
  78.   bridge: FrontendBridge,
    
  79.   id: number,
    
  80.   path: Array<string | number>,
    
  81.   rendererID: number,
    
  82. }): void {
    
  83.   bridge.send('copyElementPath', {
    
  84.     id,
    
  85.     path,
    
  86.     rendererID,
    
  87.   });
    
  88. }
    
  89. 
    
  90. export function inspectElement(
    
  91.   bridge: FrontendBridge,
    
  92.   forceFullData: boolean,
    
  93.   id: number,
    
  94.   path: InspectedElementPath | null,
    
  95.   rendererID: number,
    
  96.   shouldListenToPauseEvents: boolean = false,
    
  97. ): Promise<InspectedElementPayload> {
    
  98.   const requestID = requestCounter++;
    
  99.   const promise = getPromiseForRequestID<InspectedElementPayload>(
    
  100.     requestID,
    
  101.     'inspectedElement',
    
  102.     bridge,
    
  103.     `Timed out while inspecting element ${id}.`,
    
  104.     shouldListenToPauseEvents,
    
  105.   );
    
  106. 
    
  107.   bridge.send('inspectElement', {
    
  108.     forceFullData,
    
  109.     id,
    
  110.     path,
    
  111.     rendererID,
    
  112.     requestID,
    
  113.   });
    
  114. 
    
  115.   return promise;
    
  116. }
    
  117. 
    
  118. let storeAsGlobalCount = 0;
    
  119. 
    
  120. export function storeAsGlobal({
    
  121.   bridge,
    
  122.   id,
    
  123.   path,
    
  124.   rendererID,
    
  125. }: {
    
  126.   bridge: FrontendBridge,
    
  127.   id: number,
    
  128.   path: Array<string | number>,
    
  129.   rendererID: number,
    
  130. }): void {
    
  131.   bridge.send('storeAsGlobal', {
    
  132.     count: storeAsGlobalCount++,
    
  133.     id,
    
  134.     path,
    
  135.     rendererID,
    
  136.   });
    
  137. }
    
  138. 
    
  139. const TIMEOUT_DELAY = 10_000;
    
  140. 
    
  141. let requestCounter = 0;
    
  142. 
    
  143. function getPromiseForRequestID<T>(
    
  144.   requestID: number,
    
  145.   eventType: $Keys<BackendEvents>,
    
  146.   bridge: FrontendBridge,
    
  147.   timeoutMessage: string,
    
  148.   shouldListenToPauseEvents: boolean = false,
    
  149. ): Promise<T> {
    
  150.   return new Promise((resolve, reject) => {
    
  151.     const cleanup = () => {
    
  152.       bridge.removeListener(eventType, onInspectedElement);
    
  153.       bridge.removeListener('shutdown', onShutdown);
    
  154. 
    
  155.       if (shouldListenToPauseEvents) {
    
  156.         bridge.removeListener('pauseElementPolling', onDisconnect);
    
  157.       }
    
  158. 
    
  159.       clearTimeout(timeoutID);
    
  160.     };
    
  161. 
    
  162.     const onShutdown = () => {
    
  163.       cleanup();
    
  164.       reject(
    
  165.         new Error(
    
  166.           'Failed to inspect element. Try again or restart React DevTools.',
    
  167.         ),
    
  168.       );
    
  169.     };
    
  170. 
    
  171.     const onDisconnect = () => {
    
  172.       cleanup();
    
  173.       reject(new ElementPollingCancellationError());
    
  174.     };
    
  175. 
    
  176.     const onInspectedElement = (data: any) => {
    
  177.       if (data.responseID === requestID) {
    
  178.         cleanup();
    
  179.         resolve((data: T));
    
  180.       }
    
  181.     };
    
  182. 
    
  183.     const onTimeout = () => {
    
  184.       cleanup();
    
  185.       reject(new TimeoutError(timeoutMessage));
    
  186.     };
    
  187. 
    
  188.     bridge.addListener(eventType, onInspectedElement);
    
  189.     bridge.addListener('shutdown', onShutdown);
    
  190. 
    
  191.     if (shouldListenToPauseEvents) {
    
  192.       bridge.addListener('pauseElementPolling', onDisconnect);
    
  193.     }
    
  194. 
    
  195.     const timeoutID = setTimeout(onTimeout, TIMEOUT_DELAY);
    
  196.   });
    
  197. }
    
  198. 
    
  199. export function cloneInspectedElementWithPath(
    
  200.   inspectedElement: InspectedElementFrontend,
    
  201.   path: Array<string | number>,
    
  202.   value: Object,
    
  203. ): InspectedElementFrontend {
    
  204.   const hydratedValue = hydrateHelper(value, path);
    
  205.   const clonedInspectedElement = {...inspectedElement};
    
  206. 
    
  207.   fillInPath(clonedInspectedElement, value, path, hydratedValue);
    
  208. 
    
  209.   return clonedInspectedElement;
    
  210. }
    
  211. 
    
  212. export function convertInspectedElementBackendToFrontend(
    
  213.   inspectedElementBackend: InspectedElementBackend,
    
  214. ): InspectedElementFrontend {
    
  215.   const {
    
  216.     canEditFunctionProps,
    
  217.     canEditFunctionPropsDeletePaths,
    
  218.     canEditFunctionPropsRenamePaths,
    
  219.     canEditHooks,
    
  220.     canEditHooksAndDeletePaths,
    
  221.     canEditHooksAndRenamePaths,
    
  222.     canToggleError,
    
  223.     isErrored,
    
  224.     targetErrorBoundaryID,
    
  225.     canToggleSuspense,
    
  226.     canViewSource,
    
  227.     hasLegacyContext,
    
  228.     id,
    
  229.     source,
    
  230.     type,
    
  231.     owners,
    
  232.     context,
    
  233.     hooks,
    
  234.     plugins,
    
  235.     props,
    
  236.     rendererPackageName,
    
  237.     rendererVersion,
    
  238.     rootType,
    
  239.     state,
    
  240.     key,
    
  241.     errors,
    
  242.     warnings,
    
  243.   } = inspectedElementBackend;
    
  244. 
    
  245.   const inspectedElement: InspectedElementFrontend = {
    
  246.     canEditFunctionProps,
    
  247.     canEditFunctionPropsDeletePaths,
    
  248.     canEditFunctionPropsRenamePaths,
    
  249.     canEditHooks,
    
  250.     canEditHooksAndDeletePaths,
    
  251.     canEditHooksAndRenamePaths,
    
  252.     canToggleError,
    
  253.     isErrored,
    
  254.     targetErrorBoundaryID,
    
  255.     canToggleSuspense,
    
  256.     canViewSource,
    
  257.     hasLegacyContext,
    
  258.     id,
    
  259.     key,
    
  260.     plugins,
    
  261.     rendererPackageName,
    
  262.     rendererVersion,
    
  263.     rootType,
    
  264.     source,
    
  265.     type,
    
  266.     owners:
    
  267.       owners === null
    
  268.         ? null
    
  269.         : owners.map(owner => {
    
  270.             const [displayName, hocDisplayNames] = separateDisplayNameAndHOCs(
    
  271.               owner.displayName,
    
  272.               owner.type,
    
  273.             );
    
  274.             return {
    
  275.               ...owner,
    
  276.               displayName,
    
  277.               hocDisplayNames,
    
  278.             };
    
  279.           }),
    
  280.     context: hydrateHelper(context),
    
  281.     hooks: hydrateHelper(hooks),
    
  282.     props: hydrateHelper(props),
    
  283.     state: hydrateHelper(state),
    
  284.     errors,
    
  285.     warnings,
    
  286.   };
    
  287. 
    
  288.   return inspectedElement;
    
  289. }
    
  290. 
    
  291. export function hydrateHelper(
    
  292.   dehydratedData: DehydratedData | null,
    
  293.   path: ?InspectedElementPath,
    
  294. ): Object | null {
    
  295.   if (dehydratedData !== null) {
    
  296.     const {cleaned, data, unserializable} = dehydratedData;
    
  297. 
    
  298.     if (path) {
    
  299.       const {length} = path;
    
  300.       if (length > 0) {
    
  301.         // Hydration helper requires full paths, but inspection dehydrates with relative paths.
    
  302.         // In that event it's important that we adjust the "cleaned" paths to match.
    
  303.         return hydrate(
    
  304.           data,
    
  305.           cleaned.map(cleanedPath => cleanedPath.slice(length)),
    
  306.           unserializable.map(unserializablePath =>
    
  307.             unserializablePath.slice(length),
    
  308.           ),
    
  309.         );
    
  310.       }
    
  311.     }
    
  312. 
    
  313.     return hydrate(data, cleaned, unserializable);
    
  314.   } else {
    
  315.     return null;
    
  316.   }
    
  317. }