/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration'; import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils'; import Store from 'react-devtools-shared/src/devtools/store'; import type { InspectedElement as InspectedElementBackend, InspectedElementPayload, } from 'react-devtools-shared/src/backend/types'; import type { BackendEvents, FrontendBridge, } from 'react-devtools-shared/src/bridge'; import type { DehydratedData, InspectedElement as InspectedElementFrontend, } from 'react-devtools-shared/src/devtools/views/Components/types'; export function clearErrorsAndWarnings({ bridge, store, }: {| bridge: FrontendBridge, store: Store, |}): void { store.rootIDToRendererID.forEach(rendererID => { bridge.send('clearErrorsAndWarnings', {rendererID}); }); } export function clearErrorsForElement({ bridge, id, rendererID, }: {| bridge: FrontendBridge, id: number, rendererID: number, |}): void { bridge.send('clearErrorsForFiberID', { rendererID, id, }); } export function clearWarningsForElement({ bridge, id, rendererID, }: {| bridge: FrontendBridge, id: number, rendererID: number, |}): void { bridge.send('clearWarningsForFiberID', { rendererID, id, }); } export function copyInspectedElementPath({ bridge, id, path, rendererID, }: {| bridge: FrontendBridge, id: number, path: Array, rendererID: number, |}): void { bridge.send('copyElementPath', { id, path, rendererID, }); } export function inspectElement({ bridge, id, path, rendererID, }: {| bridge: FrontendBridge, id: number, path: Array | null, rendererID: number, |}): Promise { const requestID = requestCounter++; const promise = getPromiseForRequestID( requestID, 'inspectedElement', bridge, ); bridge.send('inspectElement', { id, path, rendererID, requestID, }); return promise; } let storeAsGlobalCount = 0; export function storeAsGlobal({ bridge, id, path, rendererID, }: {| bridge: FrontendBridge, id: number, path: Array, rendererID: number, |}): void { bridge.send('storeAsGlobal', { count: storeAsGlobalCount++, id, path, rendererID, }); } const TIMEOUT_DELAY = 5000; let requestCounter = 0; function getPromiseForRequestID( requestID: number, eventType: $Keys, bridge: FrontendBridge, ): Promise { return new Promise((resolve, reject) => { const cleanup = () => { bridge.removeListener(eventType, onInspectedElement); clearTimeout(timeoutID); }; const onInspectedElement = (data: any) => { if (data.responseID === requestID) { cleanup(); resolve((data: T)); } }; const onTimeout = () => { cleanup(); reject(); }; bridge.addListener(eventType, onInspectedElement); const timeoutID = setTimeout(onTimeout, TIMEOUT_DELAY); }); } export function cloneInspectedElementWithPath( inspectedElement: InspectedElementFrontend, path: Array, value: Object, ): InspectedElementFrontend { const hydratedValue = hydrateHelper(value, path); const clonedInspectedElement = {...inspectedElement}; fillInPath(clonedInspectedElement, value, path, hydratedValue); return clonedInspectedElement; } export function convertInspectedElementBackendToFrontend( inspectedElementBackend: InspectedElementBackend, ): InspectedElementFrontend { const { canEditFunctionProps, canEditFunctionPropsDeletePaths, canEditFunctionPropsRenamePaths, canEditHooks, canEditHooksAndDeletePaths, canEditHooksAndRenamePaths, canToggleError, isErrored, targetErrorBoundaryID, canToggleSuspense, canViewSource, hasLegacyContext, id, source, type, owners, context, hooks, props, rendererPackageName, rendererVersion, rootType, state, key, errors, warnings, } = inspectedElementBackend; const inspectedElement: InspectedElementFrontend = { canEditFunctionProps, canEditFunctionPropsDeletePaths, canEditFunctionPropsRenamePaths, canEditHooks, canEditHooksAndDeletePaths, canEditHooksAndRenamePaths, canToggleError, isErrored, targetErrorBoundaryID, canToggleSuspense, canViewSource, hasLegacyContext, id, key, rendererPackageName, rendererVersion, rootType, source, type, owners: owners === null ? null : owners.map(owner => { const [displayName, hocDisplayNames] = separateDisplayNameAndHOCs( owner.displayName, owner.type, ); return { ...owner, displayName, hocDisplayNames, }; }), context: hydrateHelper(context), hooks: hydrateHelper(hooks), props: hydrateHelper(props), state: hydrateHelper(state), errors, warnings, }; return inspectedElement; } export function hydrateHelper( dehydratedData: DehydratedData | null, path?: Array, ): Object | null { if (dehydratedData !== null) { const {cleaned, data, unserializable} = dehydratedData; if (path) { const {length} = path; if (length > 0) { // Hydration helper requires full paths, but inspection dehydrates with relative paths. // In that event it's important that we adjust the "cleaned" paths to match. return hydrate( data, cleaned.map(cleanedPath => cleanedPath.slice(length)), unserializable.map(unserializablePath => unserializablePath.slice(length), ), ); } } return hydrate(data, cleaned, unserializable); } else { return null; } }