/** * Copyright (c) Meta Platforms, Inc. and 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 type { Thenable, ReactComponentInfo, ReactDebugInfo, ReactAsyncInfo, ReactIOInfo, ReactStackTrace, ReactCallSite, Wakeable, } from 'shared/ReactTypes'; import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; import { ComponentFilterDisplayName, ComponentFilterElementType, ComponentFilterHOC, ComponentFilterLocation, ComponentFilterEnvironmentName, ElementTypeClass, ElementTypeContext, ElementTypeFunction, ElementTypeForwardRef, ElementTypeHostComponent, ElementTypeMemo, ElementTypeOtherOrUnknown, ElementTypeProfiler, ElementTypeRoot, ElementTypeSuspense, ElementTypeSuspenseList, ElementTypeTracingMarker, ElementTypeViewTransition, ElementTypeActivity, ElementTypeVirtual, StrictMode, } from 'react-devtools-shared/src/frontend/types'; import { deletePathInObject, getDisplayName, getWrappedDisplayName, getDefaultComponentFilters, getInObject, getUID, renamePathInObject, setInObject, utfEncodeString, filterOutLocationComponentFilters, } from 'react-devtools-shared/src/utils'; import { formatConsoleArgumentsToSingleString, formatDurationToMicrosecondsGranularity, gt, gte, serializeToString, } from 'react-devtools-shared/src/backend/utils'; import { extractLocationFromComponentStack, extractLocationFromOwnerStack, parseStackTrace, } from 'react-devtools-shared/src/backend/utils/parseStackTrace'; import { cleanForBridge, copyWithDelete, copyWithRename, copyWithSet, getEffectDurations, } from '../utils'; import { __DEBUG__, PROFILING_FLAG_BASIC_SUPPORT, PROFILING_FLAG_TIMELINE_SUPPORT, TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, TREE_OPERATION_REORDER_CHILDREN, TREE_OPERATION_SET_SUBTREE_MODE, TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, SUSPENSE_TREE_OPERATION_ADD, SUSPENSE_TREE_OPERATION_REMOVE, SUSPENSE_TREE_OPERATION_REORDER_CHILDREN, SUSPENSE_TREE_OPERATION_RESIZE, UNKNOWN_SUSPENDERS_NONE, UNKNOWN_SUSPENDERS_REASON_PRODUCTION, UNKNOWN_SUSPENDERS_REASON_OLD_VERSION, UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE, } from '../../constants'; import {inspectHooksOfFiber} from 'react-debug-tools'; import { CONCURRENT_MODE_NUMBER, CONCURRENT_MODE_SYMBOL_STRING, DEPRECATED_ASYNC_MODE_SYMBOL_STRING, PROVIDER_NUMBER, PROVIDER_SYMBOL_STRING, CONTEXT_NUMBER, CONTEXT_SYMBOL_STRING, CONSUMER_SYMBOL_STRING, STRICT_MODE_NUMBER, STRICT_MODE_SYMBOL_STRING, PROFILER_NUMBER, PROFILER_SYMBOL_STRING, REACT_MEMO_CACHE_SENTINEL, SCOPE_NUMBER, SCOPE_SYMBOL_STRING, FORWARD_REF_NUMBER, FORWARD_REF_SYMBOL_STRING, MEMO_NUMBER, MEMO_SYMBOL_STRING, SERVER_CONTEXT_SYMBOL_STRING, LAZY_SYMBOL_STRING, } from '../shared/ReactSymbols'; import {enableStyleXFeatures} from 'react-devtools-feature-flags'; import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponentLogs'; import is from 'shared/objectIs'; import hasOwnProperty from 'shared/hasOwnProperty'; import {getIODescription} from 'shared/ReactIODescription'; import { getStackByFiberInDevAndProd, getOwnerStackByFiberInDev, supportsOwnerStacks, supportsConsoleTasks, } from './DevToolsFiberComponentStack'; // $FlowFixMe[method-unbinding] const toString = Object.prototype.toString; function isError(object: mixed) { return toString.call(object) === '[object Error]'; } import {getStyleXData} from '../StyleX/utils'; import {createProfilingHooks} from '../profilingHooks'; import type {GetTimelineData, ToggleProfilingStatus} from '../profilingHooks'; import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type { ChangeDescription, CommitDataBackend, DevToolsHook, InspectedElement, InspectedElementPayload, InstanceAndStyle, HostInstance, PathFrame, PathMatch, ProfilingDataBackend, ProfilingDataForRootBackend, ReactRenderer, RendererInterface, SerializedElement, SerializedAsyncInfo, WorkTagMap, CurrentDispatcherRef, LegacyDispatcherRef, ProfilingSettings, } from '../types'; import type { ComponentFilter, ElementType, Plugins, } from 'react-devtools-shared/src/frontend/types'; import type {ReactFunctionLocation} from 'shared/ReactTypes'; import {getSourceLocationByFiber} from './DevToolsFiberComponentStack'; import {formatOwnerStack} from '../shared/DevToolsOwnerStack'; // Kinds const FIBER_INSTANCE = 0; const VIRTUAL_INSTANCE = 1; const FILTERED_FIBER_INSTANCE = 2; // This type represents a stateful instance of a Client Component i.e. a Fiber pair. // These instances also let us track stateful DevTools meta data like id and warnings. type FiberInstance = { kind: 0, id: number, parent: null | DevToolsInstance, firstChild: null | DevToolsInstance, nextSibling: null | DevToolsInstance, source: null | string | Error | ReactFunctionLocation, // source location of this component function, or owned child stack logCount: number, // total number of errors/warnings last seen treeBaseDuration: number, // the profiled time of the last render of this subtree suspendedBy: null | Array, // things that suspended in the children position of this component suspenseNode: null | SuspenseNode, data: Fiber, // one of a Fiber pair }; function createFiberInstance(fiber: Fiber): FiberInstance { return { kind: FIBER_INSTANCE, id: getUID(), parent: null, firstChild: null, nextSibling: null, source: null, logCount: 0, treeBaseDuration: 0, suspendedBy: null, suspenseNode: null, data: fiber, }; } type FilteredFiberInstance = { kind: 2, // We exclude id from the type to get errors if we try to access it. // However it is still in the object to preserve hidden class. // id: number, parent: null | DevToolsInstance, firstChild: null | DevToolsInstance, nextSibling: null | DevToolsInstance, source: null | string | Error | ReactFunctionLocation, // always null here. logCount: number, // total number of errors/warnings last seen treeBaseDuration: number, // the profiled time of the last render of this subtree suspendedBy: null | Array, // only used at the root suspenseNode: null | SuspenseNode, data: Fiber, // one of a Fiber pair }; // This is used to represent a filtered Fiber but still lets us find its host instance. function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance { return ({ kind: FILTERED_FIBER_INSTANCE, id: 0, parent: null, firstChild: null, nextSibling: null, source: null, logCount: 0, treeBaseDuration: 0, suspendedBy: null, suspenseNode: null, data: fiber, }: any); } // This type represents a stateful instance of a Server Component or a Component // that gets optimized away - e.g. call-through without creating a Fiber. // It's basically a virtual Fiber. This is not a semantic concept in React. // It only exists as a virtual concept to let the same Element in the DevTools // persist. To be selectable separately from all ReactComponentInfo and overtime. type VirtualInstance = { kind: 1, id: number, parent: null | DevToolsInstance, firstChild: null | DevToolsInstance, nextSibling: null | DevToolsInstance, source: null | string | Error | ReactFunctionLocation, // source location of this server component, or owned child stack logCount: number, // total number of errors/warnings last seen treeBaseDuration: number, // the profiled time of the last render of this subtree suspendedBy: null | Array, // things that blocked the server component's child from rendering suspenseNode: null, // The latest info for this instance. This can be updated over time and the // same info can appear in more than once ServerComponentInstance. data: ReactComponentInfo, }; function createVirtualInstance( debugEntry: ReactComponentInfo, ): VirtualInstance { return { kind: VIRTUAL_INSTANCE, id: getUID(), parent: null, firstChild: null, nextSibling: null, source: null, logCount: 0, treeBaseDuration: 0, suspendedBy: null, suspenseNode: null, data: debugEntry, }; } type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance; // A Generic Rect super type which can include DOMRect and other objects with similar shape like in React Native. type Rect = {x: number, y: number, width: number, height: number, ...}; type SuspenseNode = { // The Instance can be a Suspense boundary, a SuspenseList Row, or HostRoot. // It can also be disconnected from the main tree if it's a Filtered Instance. instance: FiberInstance | FilteredFiberInstance, parent: null | SuspenseNode, firstChild: null | SuspenseNode, nextSibling: null | SuspenseNode, rects: null | Array, // The bounding rects of content children. suspendedBy: Map>, // Tracks which data we're suspended by and the children that suspend it. // Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all // also in the parent sets. This determine whether this could contribute in the loading sequence. hasUniqueSuspenders: boolean, // Track whether anything suspended in this boundary that we can't track either because it was using throw // a promise, an older version of React or because we're inspecting prod. hasUnknownSuspenders: boolean, }; function createSuspenseNode( instance: FiberInstance | FilteredFiberInstance, ): SuspenseNode { return (instance.suspenseNode = { instance: instance, parent: null, firstChild: null, nextSibling: null, rects: null, suspendedBy: new Map(), hasUniqueSuspenders: false, hasUnknownSuspenders: false, }); } type getDisplayNameForFiberType = (fiber: Fiber) => string | null; type getTypeSymbolType = (type: any) => symbol | string | number; type ReactPriorityLevelsType = { ImmediatePriority: number, UserBlockingPriority: number, NormalPriority: number, LowPriority: number, IdlePriority: number, NoPriority: number, }; export function getDispatcherRef(renderer: { +currentDispatcherRef?: LegacyDispatcherRef | CurrentDispatcherRef, ... }): void | CurrentDispatcherRef { if (renderer.currentDispatcherRef === undefined) { return undefined; } const injectedRef = renderer.currentDispatcherRef; if ( typeof injectedRef.H === 'undefined' && typeof injectedRef.current !== 'undefined' ) { // We got a legacy dispatcher injected, let's create a wrapper proxy to translate. return { get H() { return (injectedRef: any).current; }, set H(value) { (injectedRef: any).current = value; }, }; } return (injectedRef: any); } function getFiberFlags(fiber: Fiber): number { // The name of this field changed from "effectTag" to "flags" return fiber.flags !== undefined ? fiber.flags : (fiber: any).effectTag; } // Some environments (e.g. React Native / Hermes) don't support the performance API yet. const getCurrentTime = // $FlowFixMe[method-unbinding] typeof performance === 'object' && typeof performance.now === 'function' ? () => performance.now() : () => Date.now(); export function getInternalReactConstants(version: string): { getDisplayNameForFiber: getDisplayNameForFiberType, getTypeSymbol: getTypeSymbolType, ReactPriorityLevels: ReactPriorityLevelsType, ReactTypeOfWork: WorkTagMap, StrictModeBits: number, SuspenseyImagesMode: number, } { // ********************************************************** // The section below is copied from files in React repo. // Keep it in sync, and add version guards if it changes. // // Technically these priority levels are invalid for versions before 16.9, // but 16.9 is the first version to report priority level to DevTools, // so we can avoid checking for earlier versions and support pre-16.9 canary releases in the process. let ReactPriorityLevels: ReactPriorityLevelsType = { ImmediatePriority: 99, UserBlockingPriority: 98, NormalPriority: 97, LowPriority: 96, IdlePriority: 95, NoPriority: 90, }; if (gt(version, '17.0.2')) { ReactPriorityLevels = { ImmediatePriority: 1, UserBlockingPriority: 2, NormalPriority: 3, LowPriority: 4, IdlePriority: 5, NoPriority: 0, }; } let StrictModeBits = 0; if (gte(version, '18.0.0-alpha')) { // 18+ StrictModeBits = 0b011000; } else if (gte(version, '16.9.0')) { // 16.9 - 17 StrictModeBits = 0b1; } else if (gte(version, '16.3.0')) { // 16.3 - 16.8 StrictModeBits = 0b10; } const SuspenseyImagesMode = 0b0100000; let ReactTypeOfWork: WorkTagMap = ((null: any): WorkTagMap); // ********************************************************** // The section below is copied from files in React repo. // Keep it in sync, and add version guards if it changes. // // TODO Update the gt() check below to be gte() whichever the next version number is. // Currently the version in Git is 17.0.2 (but that version has not been/may not end up being released). if (gt(version, '17.0.1')) { ReactTypeOfWork = { CacheComponent: 24, // Experimental ClassComponent: 1, ContextConsumer: 9, ContextProvider: 10, CoroutineComponent: -1, // Removed CoroutineHandlerPhase: -1, // Removed DehydratedSuspenseComponent: 18, // Behind a flag ForwardRef: 11, Fragment: 7, FunctionComponent: 0, HostComponent: 5, HostPortal: 4, HostRoot: 3, HostHoistable: 26, // In reality, 18.2+. But doesn't hurt to include it here HostSingleton: 27, // Same as above HostText: 6, IncompleteClassComponent: 17, IncompleteFunctionComponent: 28, IndeterminateComponent: 2, // removed in 19.0.0 LazyComponent: 16, LegacyHiddenComponent: 23, MemoComponent: 14, Mode: 8, OffscreenComponent: 22, // Experimental Profiler: 12, ScopeComponent: 21, // Experimental SimpleMemoComponent: 15, SuspenseComponent: 13, SuspenseListComponent: 19, // Experimental TracingMarkerComponent: 25, // Experimental - This is technically in 18 but we don't // want to fork again so we're adding it here instead YieldComponent: -1, // Removed Throw: 29, ViewTransitionComponent: 30, // Experimental ActivityComponent: 31, }; } else if (gte(version, '17.0.0-alpha')) { ReactTypeOfWork = { CacheComponent: -1, // Doesn't exist yet ClassComponent: 1, ContextConsumer: 9, ContextProvider: 10, CoroutineComponent: -1, // Removed CoroutineHandlerPhase: -1, // Removed DehydratedSuspenseComponent: 18, // Behind a flag ForwardRef: 11, Fragment: 7, FunctionComponent: 0, HostComponent: 5, HostPortal: 4, HostRoot: 3, HostHoistable: -1, // Doesn't exist yet HostSingleton: -1, // Doesn't exist yet HostText: 6, IncompleteClassComponent: 17, IncompleteFunctionComponent: -1, // Doesn't exist yet IndeterminateComponent: 2, LazyComponent: 16, LegacyHiddenComponent: 24, MemoComponent: 14, Mode: 8, OffscreenComponent: 23, // Experimental Profiler: 12, ScopeComponent: 21, // Experimental SimpleMemoComponent: 15, SuspenseComponent: 13, SuspenseListComponent: 19, // Experimental TracingMarkerComponent: -1, // Doesn't exist yet YieldComponent: -1, // Removed Throw: -1, // Doesn't exist yet ViewTransitionComponent: -1, // Doesn't exist yet ActivityComponent: -1, // Doesn't exist yet }; } else if (gte(version, '16.6.0-beta.0')) { ReactTypeOfWork = { CacheComponent: -1, // Doesn't exist yet ClassComponent: 1, ContextConsumer: 9, ContextProvider: 10, CoroutineComponent: -1, // Removed CoroutineHandlerPhase: -1, // Removed DehydratedSuspenseComponent: 18, // Behind a flag ForwardRef: 11, Fragment: 7, FunctionComponent: 0, HostComponent: 5, HostPortal: 4, HostRoot: 3, HostHoistable: -1, // Doesn't exist yet HostSingleton: -1, // Doesn't exist yet HostText: 6, IncompleteClassComponent: 17, IncompleteFunctionComponent: -1, // Doesn't exist yet IndeterminateComponent: 2, LazyComponent: 16, LegacyHiddenComponent: -1, MemoComponent: 14, Mode: 8, OffscreenComponent: -1, // Experimental Profiler: 12, ScopeComponent: -1, // Experimental SimpleMemoComponent: 15, SuspenseComponent: 13, SuspenseListComponent: 19, // Experimental TracingMarkerComponent: -1, // Doesn't exist yet YieldComponent: -1, // Removed Throw: -1, // Doesn't exist yet ViewTransitionComponent: -1, // Doesn't exist yet ActivityComponent: -1, // Doesn't exist yet }; } else if (gte(version, '16.4.3-alpha')) { ReactTypeOfWork = { CacheComponent: -1, // Doesn't exist yet ClassComponent: 2, ContextConsumer: 11, ContextProvider: 12, CoroutineComponent: -1, // Removed CoroutineHandlerPhase: -1, // Removed DehydratedSuspenseComponent: -1, // Doesn't exist yet ForwardRef: 13, Fragment: 9, FunctionComponent: 0, HostComponent: 7, HostPortal: 6, HostRoot: 5, HostHoistable: -1, // Doesn't exist yet HostSingleton: -1, // Doesn't exist yet HostText: 8, IncompleteClassComponent: -1, // Doesn't exist yet IncompleteFunctionComponent: -1, // Doesn't exist yet IndeterminateComponent: 4, LazyComponent: -1, // Doesn't exist yet LegacyHiddenComponent: -1, MemoComponent: -1, // Doesn't exist yet Mode: 10, OffscreenComponent: -1, // Experimental Profiler: 15, ScopeComponent: -1, // Experimental SimpleMemoComponent: -1, // Doesn't exist yet SuspenseComponent: 16, SuspenseListComponent: -1, // Doesn't exist yet TracingMarkerComponent: -1, // Doesn't exist yet YieldComponent: -1, // Removed Throw: -1, // Doesn't exist yet ViewTransitionComponent: -1, // Doesn't exist yet ActivityComponent: -1, // Doesn't exist yet }; } else { ReactTypeOfWork = { CacheComponent: -1, // Doesn't exist yet ClassComponent: 2, ContextConsumer: 12, ContextProvider: 13, CoroutineComponent: 7, CoroutineHandlerPhase: 8, DehydratedSuspenseComponent: -1, // Doesn't exist yet ForwardRef: 14, Fragment: 10, FunctionComponent: 1, HostComponent: 5, HostPortal: 4, HostRoot: 3, HostHoistable: -1, // Doesn't exist yet HostSingleton: -1, // Doesn't exist yet HostText: 6, IncompleteClassComponent: -1, // Doesn't exist yet IncompleteFunctionComponent: -1, // Doesn't exist yet IndeterminateComponent: 0, LazyComponent: -1, // Doesn't exist yet LegacyHiddenComponent: -1, MemoComponent: -1, // Doesn't exist yet Mode: 11, OffscreenComponent: -1, // Experimental Profiler: 15, ScopeComponent: -1, // Experimental SimpleMemoComponent: -1, // Doesn't exist yet SuspenseComponent: 16, SuspenseListComponent: -1, // Doesn't exist yet TracingMarkerComponent: -1, // Doesn't exist yet YieldComponent: 9, Throw: -1, // Doesn't exist yet ViewTransitionComponent: -1, // Doesn't exist yet ActivityComponent: -1, // Doesn't exist yet }; } // ********************************************************** // End of copied code. // ********************************************************** function getTypeSymbol(type: any): symbol | string | number { const symbolOrNumber = typeof type === 'object' && type !== null ? type.$$typeof : type; return typeof symbolOrNumber === 'symbol' ? symbolOrNumber.toString() : symbolOrNumber; } const { CacheComponent, ClassComponent, IncompleteClassComponent, IncompleteFunctionComponent, FunctionComponent, IndeterminateComponent, ForwardRef, HostRoot, HostHoistable, HostSingleton, HostComponent, HostPortal, HostText, Fragment, LazyComponent, LegacyHiddenComponent, MemoComponent, OffscreenComponent, Profiler, ScopeComponent, SimpleMemoComponent, SuspenseComponent, SuspenseListComponent, TracingMarkerComponent, Throw, ViewTransitionComponent, ActivityComponent, } = ReactTypeOfWork; function resolveFiberType(type: any): $FlowFixMe { const typeSymbol = getTypeSymbol(type); switch (typeSymbol) { case MEMO_NUMBER: case MEMO_SYMBOL_STRING: // recursively resolving memo type in case of memo(forwardRef(Component)) return resolveFiberType(type.type); case FORWARD_REF_NUMBER: case FORWARD_REF_SYMBOL_STRING: return type.render; default: return type; } } // NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods function getDisplayNameForFiber( fiber: Fiber, shouldSkipForgetCheck: boolean = false, ): string | null { const {elementType, type, tag} = fiber; let resolvedType = type; if (typeof type === 'object' && type !== null) { resolvedType = resolveFiberType(type); } let resolvedContext: any = null; if ( !shouldSkipForgetCheck && // $FlowFixMe[incompatible-type] fiber.updateQueue is mixed (fiber.updateQueue?.memoCache != null || (Array.isArray(fiber.memoizedState?.memoizedState) && fiber.memoizedState.memoizedState[0]?.[REACT_MEMO_CACHE_SENTINEL]) || fiber.memoizedState?.memoizedState?.[REACT_MEMO_CACHE_SENTINEL]) ) { const displayNameWithoutForgetWrapper = getDisplayNameForFiber( fiber, true, ); if (displayNameWithoutForgetWrapper == null) { return null; } return `Forget(${displayNameWithoutForgetWrapper})`; } switch (tag) { case ActivityComponent: return 'Activity'; case CacheComponent: return 'Cache'; case ClassComponent: case IncompleteClassComponent: case IncompleteFunctionComponent: case FunctionComponent: case IndeterminateComponent: return getDisplayName(resolvedType); case ForwardRef: return getWrappedDisplayName( elementType, resolvedType, 'ForwardRef', 'Anonymous', ); case HostRoot: const fiberRoot = fiber.stateNode; if (fiberRoot != null && fiberRoot._debugRootType !== null) { return fiberRoot._debugRootType; } return null; case HostComponent: case HostSingleton: case HostHoistable: return type; case HostPortal: case HostText: return null; case Fragment: return 'Fragment'; case LazyComponent: // This display name will not be user visible. // Once a Lazy component loads its inner component, React replaces the tag and type. // This display name will only show up in console logs when DevTools DEBUG mode is on. return 'Lazy'; case MemoComponent: case SimpleMemoComponent: // Display name in React does not use `Memo` as a wrapper but fallback name. return getWrappedDisplayName( elementType, resolvedType, 'Memo', 'Anonymous', ); case SuspenseComponent: return 'Suspense'; case LegacyHiddenComponent: return 'LegacyHidden'; case OffscreenComponent: return 'Offscreen'; case ScopeComponent: return 'Scope'; case SuspenseListComponent: return 'SuspenseList'; case Profiler: return 'Profiler'; case TracingMarkerComponent: return 'TracingMarker'; case ViewTransitionComponent: return 'ViewTransition'; case Throw: // This should really never be visible. return 'Error'; default: const typeSymbol = getTypeSymbol(type); switch (typeSymbol) { case CONCURRENT_MODE_NUMBER: case CONCURRENT_MODE_SYMBOL_STRING: case DEPRECATED_ASYNC_MODE_SYMBOL_STRING: return null; case PROVIDER_NUMBER: case PROVIDER_SYMBOL_STRING: // 16.3.0 exposed the context object as "context" // PR #12501 changed it to "_context" for 16.3.1+ // NOTE Keep in sync with inspectElementRaw() resolvedContext = fiber.type._context || fiber.type.context; return `${resolvedContext.displayName || 'Context'}.Provider`; case CONTEXT_NUMBER: case CONTEXT_SYMBOL_STRING: case SERVER_CONTEXT_SYMBOL_STRING: if ( fiber.type._context === undefined && fiber.type.Provider === fiber.type ) { // In 19+, Context.Provider === Context, so this is a provider. resolvedContext = fiber.type; return `${resolvedContext.displayName || 'Context'}.Provider`; } // 16.3-16.5 read from "type" because the Consumer is the actual context object. // 16.6+ should read from "type._context" because Consumer can be different (in DEV). // NOTE Keep in sync with inspectElementRaw() resolvedContext = fiber.type._context || fiber.type; // NOTE: TraceUpdatesBackendManager depends on the name ending in '.Consumer' // If you change the name, figure out a more resilient way to detect it. return `${resolvedContext.displayName || 'Context'}.Consumer`; case CONSUMER_SYMBOL_STRING: // 19+ resolvedContext = fiber.type._context; return `${resolvedContext.displayName || 'Context'}.Consumer`; case STRICT_MODE_NUMBER: case STRICT_MODE_SYMBOL_STRING: return null; case PROFILER_NUMBER: case PROFILER_SYMBOL_STRING: return `Profiler(${fiber.memoizedProps.id})`; case SCOPE_NUMBER: case SCOPE_SYMBOL_STRING: return 'Scope'; default: // Unknown element type. // This may mean a new element type that has not yet been added to DevTools. return null; } } } return { getDisplayNameForFiber, getTypeSymbol, ReactPriorityLevels, ReactTypeOfWork, StrictModeBits, SuspenseyImagesMode, }; } // All environment names we've seen so far. This lets us create a list of filters to apply. // This should ideally include env of filtered Components too so that you can add those as // filters at the same time as removing some other filter. const knownEnvironmentNames: Set = new Set(); // Map of FiberRoot to their root FiberInstance. const rootToFiberInstanceMap: Map = new Map(); // Map of id to FiberInstance or VirtualInstance. // This Map is used to e.g. get the display name for a Fiber or schedule an update, // operations that should be the same whether the current and work-in-progress Fiber is used. const idToDevToolsInstanceMap: Map< FiberInstance['id'] | VirtualInstance['id'], FiberInstance | VirtualInstance, > = new Map(); const idToSuspenseNodeMap: Map = new Map(); // Map of canonical HostInstances to the nearest parent DevToolsInstance. const publicInstanceToDevToolsInstanceMap: Map = new Map(); // Map of resource DOM nodes to all the nearest DevToolsInstances that depend on it. const hostResourceToDevToolsInstanceMap: Map< HostInstance, Set, > = new Map(); // Ideally, this should be injected from Reconciler config function getPublicInstance(instance: HostInstance): HostInstance { // Typically the PublicInstance and HostInstance is the same thing but not in Fabric. // So we need to detect this and use that as the public instance. // React Native. Modern. Fabric. if (typeof instance === 'object' && instance !== null) { if (typeof instance.canonical === 'object' && instance.canonical !== null) { if ( typeof instance.canonical.publicInstance === 'object' && instance.canonical.publicInstance !== null ) { return instance.canonical.publicInstance; } } // React Native. Legacy. Paper. if (typeof instance._nativeTag === 'number') { return instance._nativeTag; } } // React Web. Usually a DOM element. return instance; } function getNativeTag(instance: HostInstance): number | null { if (typeof instance !== 'object' || instance === null) { return null; } // Modern. Fabric. if ( instance.canonical != null && typeof instance.canonical.nativeTag === 'number' ) { return instance.canonical.nativeTag; } // Legacy. Paper. if (typeof instance._nativeTag === 'number') { return instance._nativeTag; } return null; } function aquireHostInstance( nearestInstance: DevToolsInstance, hostInstance: HostInstance, ): void { const publicInstance = getPublicInstance(hostInstance); publicInstanceToDevToolsInstanceMap.set(publicInstance, nearestInstance); } function releaseHostInstance( nearestInstance: DevToolsInstance, hostInstance: HostInstance, ): void { const publicInstance = getPublicInstance(hostInstance); if ( publicInstanceToDevToolsInstanceMap.get(publicInstance) === nearestInstance ) { publicInstanceToDevToolsInstanceMap.delete(publicInstance); } } function aquireHostResource( nearestInstance: DevToolsInstance, resource: ?{instance?: HostInstance}, ): void { const hostInstance = resource && resource.instance; if (hostInstance) { const publicInstance = getPublicInstance(hostInstance); let resourceInstances = hostResourceToDevToolsInstanceMap.get(publicInstance); if (resourceInstances === undefined) { resourceInstances = new Set(); hostResourceToDevToolsInstanceMap.set(publicInstance, resourceInstances); // Store the first match in the main map for quick access when selecting DOM node. publicInstanceToDevToolsInstanceMap.set(publicInstance, nearestInstance); } resourceInstances.add(nearestInstance); } } function releaseHostResource( nearestInstance: DevToolsInstance, resource: ?{instance?: HostInstance}, ): void { const hostInstance = resource && resource.instance; if (hostInstance) { const publicInstance = getPublicInstance(hostInstance); const resourceInstances = hostResourceToDevToolsInstanceMap.get(publicInstance); if (resourceInstances !== undefined) { resourceInstances.delete(nearestInstance); if (resourceInstances.size === 0) { hostResourceToDevToolsInstanceMap.delete(publicInstance); publicInstanceToDevToolsInstanceMap.delete(publicInstance); } else if ( publicInstanceToDevToolsInstanceMap.get(publicInstance) === nearestInstance ) { // This was the first one. Store the next first one in the main map for easy access. // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const firstInstance of resourceInstances) { publicInstanceToDevToolsInstanceMap.set( firstInstance, nearestInstance, ); break; } } } } } export function attach( hook: DevToolsHook, rendererID: number, renderer: ReactRenderer, global: Object, shouldStartProfilingNow: boolean, profilingSettings: ProfilingSettings, ): RendererInterface { // Newer versions of the reconciler package also specific reconciler version. // If that version number is present, use it. // Third party renderer versions may not match the reconciler version, // and the latter is what's important in terms of tags and symbols. const version = renderer.reconcilerVersion || renderer.version; const { getDisplayNameForFiber, getTypeSymbol, ReactPriorityLevels, ReactTypeOfWork, StrictModeBits, SuspenseyImagesMode, } = getInternalReactConstants(version); const { ActivityComponent, ClassComponent, ContextConsumer, DehydratedSuspenseComponent, ForwardRef, Fragment, FunctionComponent, HostRoot, HostHoistable, HostSingleton, HostPortal, HostComponent, HostText, IncompleteClassComponent, IncompleteFunctionComponent, IndeterminateComponent, LegacyHiddenComponent, MemoComponent, OffscreenComponent, SimpleMemoComponent, SuspenseComponent, SuspenseListComponent, TracingMarkerComponent, Throw, ViewTransitionComponent, } = ReactTypeOfWork; const { ImmediatePriority, UserBlockingPriority, NormalPriority, LowPriority, IdlePriority, NoPriority, } = ReactPriorityLevels; const { getLaneLabelMap, injectProfilingHooks, overrideHookState, overrideHookStateDeletePath, overrideHookStateRenamePath, overrideProps, overridePropsDeletePath, overridePropsRenamePath, scheduleRefresh, setErrorHandler, setSuspenseHandler, scheduleUpdate, getCurrentFiber, } = renderer; const supportsTogglingError = typeof setErrorHandler === 'function' && typeof scheduleUpdate === 'function'; const supportsTogglingSuspense = typeof setSuspenseHandler === 'function' && typeof scheduleUpdate === 'function'; if (typeof scheduleRefresh === 'function') { // When Fast Refresh updates a component, the frontend may need to purge cached information. // For example, ASTs cached for the component (for named hooks) may no longer be valid. // Send a signal to the frontend to purge this cached information. // The "fastRefreshScheduled" dispatched is global (not Fiber or even Renderer specific). // This is less effecient since it means the front-end will need to purge the entire cache, // but this is probably an okay trade off in order to reduce coupling between the DevTools and Fast Refresh. renderer.scheduleRefresh = (...args) => { try { hook.emit('fastRefreshScheduled'); } finally { return scheduleRefresh(...args); } }; } let getTimelineData: null | GetTimelineData = null; let toggleProfilingStatus: null | ToggleProfilingStatus = null; if (typeof injectProfilingHooks === 'function') { const response = createProfilingHooks({ getDisplayNameForFiber, getIsProfiling: () => isProfiling, getLaneLabelMap, currentDispatcherRef: getDispatcherRef(renderer), workTagMap: ReactTypeOfWork, reactVersion: version, }); // Pass the Profiling hooks to the reconciler for it to call during render. injectProfilingHooks(response.profilingHooks); // Hang onto this toggle so we can notify the external methods of profiling status changes. getTimelineData = response.getTimelineData; toggleProfilingStatus = response.toggleProfilingStatus; } type ComponentLogs = { errors: Map, errorsCount: number, warnings: Map, warningsCount: number, }; // Tracks Errors/Warnings logs added to a Fiber. They are added before the commit and get // picked up a FiberInstance. This keeps it around as long as the Fiber is alive which // lets the Fiber get reparented/remounted and still observe the previous errors/warnings. // Unless we explicitly clear the logs from a Fiber. const fiberToComponentLogsMap: WeakMap = new WeakMap(); // Tracks whether we've performed a commit since the last log. This is used to know // whether we received any new logs between the commit and post commit phases. I.e. // if any passive effects called console.warn / console.error. let needsToFlushComponentLogs = false; function bruteForceFlushErrorsAndWarnings() { // Refresh error/warning count for all mounted unfiltered Fibers. let hasChanges = false; // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const devtoolsInstance of idToDevToolsInstanceMap.values()) { if (devtoolsInstance.kind === FIBER_INSTANCE) { const fiber = devtoolsInstance.data; const componentLogsEntry = fiberToComponentLogsMap.get(fiber); const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry); if (changed) { hasChanges = true; updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id); } } else { // Virtual Instances cannot log in passive effects and so never appear here. } } if (hasChanges) { flushPendingEvents(); } } function clearErrorsAndWarnings() { // Note, this only clears logs for Fibers that have instances. If they're filtered // and then mount, the logs are there. Ensuring we only clear what you've seen. // If we wanted to clear the whole set, we'd replace fiberToComponentLogsMap with a // new WeakMap. It's unclear whether we should clear componentInfoToComponentLogsMap // since it's shared by other renderers but presumably it would. // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const devtoolsInstance of idToDevToolsInstanceMap.values()) { if (devtoolsInstance.kind === FIBER_INSTANCE) { const fiber = devtoolsInstance.data; fiberToComponentLogsMap.delete(fiber); if (fiber.alternate) { fiberToComponentLogsMap.delete(fiber.alternate); } } else { componentInfoToComponentLogsMap.delete(devtoolsInstance.data); } const changed = recordConsoleLogs(devtoolsInstance, undefined); if (changed) { updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id); } } flushPendingEvents(); } function clearConsoleLogsHelper(instanceID: number, type: 'error' | 'warn') { const devtoolsInstance = idToDevToolsInstanceMap.get(instanceID); if (devtoolsInstance !== undefined) { let componentLogsEntry; if (devtoolsInstance.kind === FIBER_INSTANCE) { const fiber = devtoolsInstance.data; componentLogsEntry = fiberToComponentLogsMap.get(fiber); if (componentLogsEntry === undefined && fiber.alternate !== null) { componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate); } } else { const componentInfo = devtoolsInstance.data; componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo); } if (componentLogsEntry !== undefined) { if (type === 'error') { componentLogsEntry.errors.clear(); componentLogsEntry.errorsCount = 0; } else { componentLogsEntry.warnings.clear(); componentLogsEntry.warningsCount = 0; } const changed = recordConsoleLogs(devtoolsInstance, componentLogsEntry); if (changed) { flushPendingEvents(); updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id); } } } } function clearErrorsForElementID(instanceID: number) { clearConsoleLogsHelper(instanceID, 'error'); } function clearWarningsForElementID(instanceID: number) { clearConsoleLogsHelper(instanceID, 'warn'); } function updateMostRecentlyInspectedElementIfNecessary( fiberID: number, ): void { if ( mostRecentlyInspectedElement !== null && mostRecentlyInspectedElement.id === fiberID ) { hasElementUpdatedSinceLastInspected = true; } } function getComponentStack( topFrame: Error, ): null | {enableOwnerStacks: boolean, componentStack: string} { if (getCurrentFiber == null) { // Expected this to be part of the renderer. Ignore. return null; } const current = getCurrentFiber(); if (current === null) { // Outside of our render scope. return null; } if (supportsConsoleTasks(current)) { // This will be handled natively by console.createTask. No need for // DevTools to add it. return null; } const dispatcherRef = getDispatcherRef(renderer); if (dispatcherRef === undefined) { return null; } const enableOwnerStacks = supportsOwnerStacks(current); let componentStack = ''; if (enableOwnerStacks) { // Prefix the owner stack with the current stack. I.e. what called // console.error. While this will also be part of the native stack, // it is hidden and not presented alongside this argument so we print // them all together. const topStackFrames = formatOwnerStack(topFrame); if (topStackFrames) { componentStack += '\n' + topStackFrames; } componentStack += getOwnerStackByFiberInDev( ReactTypeOfWork, current, dispatcherRef, ); } else { componentStack = getStackByFiberInDevAndProd( ReactTypeOfWork, current, dispatcherRef, ); } return {enableOwnerStacks, componentStack}; } // Called when an error or warning is logged during render, commit, or passive (including unmount functions). function onErrorOrWarning( type: 'error' | 'warn', args: $ReadOnlyArray, ): void { if (getCurrentFiber == null) { // Expected this to be part of the renderer. Ignore. return; } const fiber = getCurrentFiber(); if (fiber === null) { // Outside of our render scope. return; } if (type === 'error') { // if this is an error simulated by us to trigger error boundary, ignore if ( forceErrorForFibers.get(fiber) === true || (fiber.alternate !== null && forceErrorForFibers.get(fiber.alternate) === true) ) { return; } } // We can't really use this message as a unique key, since we can't distinguish // different objects in this implementation. We have to delegate displaying of the objects // to the environment, the browser console, for example, so this is why this should be kept // as an array of arguments, instead of the plain string. // [Warning: %o, {...}] and [Warning: %o, {...}] will be considered as the same message, // even if objects are different const message = formatConsoleArgumentsToSingleString(...args); // Track the warning/error for later. let componentLogsEntry = fiberToComponentLogsMap.get(fiber); if (componentLogsEntry === undefined && fiber.alternate !== null) { componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate); if (componentLogsEntry !== undefined) { // Use the same set for both Fibers. fiberToComponentLogsMap.set(fiber, componentLogsEntry); } } if (componentLogsEntry === undefined) { componentLogsEntry = { errors: new Map(), errorsCount: 0, warnings: new Map(), warningsCount: 0, }; fiberToComponentLogsMap.set(fiber, componentLogsEntry); } const messageMap = type === 'error' ? componentLogsEntry.errors : componentLogsEntry.warnings; const count = messageMap.get(message) || 0; messageMap.set(message, count + 1); if (type === 'error') { componentLogsEntry.errorsCount++; } else { componentLogsEntry.warningsCount++; } // The changes will be flushed later when we commit. // If the log happened in a passive effect, then this happens after we've // already committed the new tree so the change won't show up until we rerender // that component again. We need to visit a Component with passive effects in // handlePostCommitFiberRoot again to ensure that we flush the changes after passive. needsToFlushComponentLogs = true; } function debug( name: string, instance: DevToolsInstance, parentInstance: null | DevToolsInstance, extraString: string = '', ): void { if (__DEBUG__) { const displayName = instance.kind === VIRTUAL_INSTANCE ? instance.data.name || 'null' : instance.data.tag + ':' + (getDisplayNameForFiber(instance.data) || 'null'); const maybeID = instance.kind === FILTERED_FIBER_INSTANCE ? '' : instance.id; const parentDisplayName = parentInstance === null ? '' : parentInstance.kind === VIRTUAL_INSTANCE ? parentInstance.data.name || 'null' : parentInstance.data.tag + ':' + (getDisplayNameForFiber(parentInstance.data) || 'null'); const maybeParentID = parentInstance === null || parentInstance.kind === FILTERED_FIBER_INSTANCE ? '' : parentInstance.id; console.groupCollapsed( `[renderer] %c${name} %c${displayName} (${maybeID}) %c${ parentInstance ? `${parentDisplayName} (${maybeParentID})` : '' } %c${extraString}`, 'color: red; font-weight: bold;', 'color: blue;', 'color: purple;', 'color: black;', ); console.log(new Error().stack.split('\n').slice(1).join('\n')); console.groupEnd(); } } // eslint-disable-next-line no-unused-vars function debugTree(instance: DevToolsInstance, indent: number = 0) { if (__DEBUG__) { const name = (instance.kind !== VIRTUAL_INSTANCE ? getDisplayNameForFiber(instance.data) : instance.data.name) || ''; console.log( ' '.repeat(indent) + '- ' + (instance.kind === FILTERED_FIBER_INSTANCE ? 0 : instance.id) + ' (' + name + ')', 'parent', instance.parent === null ? ' ' : instance.parent.kind === FILTERED_FIBER_INSTANCE ? 0 : instance.parent.id, 'next', instance.nextSibling === null ? ' ' : instance.nextSibling.id, ); let child = instance.firstChild; while (child !== null) { debugTree(child, indent + 1); child = child.nextSibling; } } } // Configurable Components tree filters. const hideElementsWithDisplayNames: Set = new Set(); const hideElementsWithPaths: Set = new Set(); const hideElementsWithTypes: Set = new Set(); const hideElementsWithEnvs: Set = new Set(); // Highlight updates let traceUpdatesEnabled: boolean = false; const traceUpdatesForNodes: Set = new Set(); function applyComponentFilters(componentFilters: Array) { hideElementsWithTypes.clear(); hideElementsWithDisplayNames.clear(); hideElementsWithPaths.clear(); hideElementsWithEnvs.clear(); componentFilters.forEach(componentFilter => { if (!componentFilter.isEnabled) { return; } switch (componentFilter.type) { case ComponentFilterDisplayName: if (componentFilter.isValid && componentFilter.value !== '') { hideElementsWithDisplayNames.add( new RegExp(componentFilter.value, 'i'), ); } break; case ComponentFilterElementType: hideElementsWithTypes.add(componentFilter.value); break; case ComponentFilterLocation: if (componentFilter.isValid && componentFilter.value !== '') { hideElementsWithPaths.add(new RegExp(componentFilter.value, 'i')); } break; case ComponentFilterHOC: hideElementsWithDisplayNames.add(new RegExp('\\(')); break; case ComponentFilterEnvironmentName: hideElementsWithEnvs.add(componentFilter.value); break; default: console.warn( `Invalid component filter type "${componentFilter.type}"`, ); break; } }); } // The renderer interface can't read saved component filters directly, // because they are stored in localStorage within the context of the extension. // Instead it relies on the extension to pass filters through. if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ != null) { const componentFiltersWithoutLocationBasedOnes = filterOutLocationComponentFilters( window.__REACT_DEVTOOLS_COMPONENT_FILTERS__, ); applyComponentFilters(componentFiltersWithoutLocationBasedOnes); } else { // Unfortunately this feature is not expected to work for React Native for now. // It would be annoying for us to spam YellowBox warnings with unactionable stuff, // so for now just skip this message... //console.warn('⚛ DevTools: Could not locate saved component filters'); // Fallback to assuming the default filters in this case. applyComponentFilters(getDefaultComponentFilters()); } // If necessary, we can revisit optimizing this operation. // For example, we could add a new recursive unmount tree operation. // The unmount operations are already significantly smaller than mount operations though. // This is something to keep in mind for later. function updateComponentFilters(componentFilters: Array) { if (isProfiling) { // Re-mounting a tree while profiling is in progress might break a lot of assumptions. // If necessary, we could support this- but it doesn't seem like a necessary use case. throw Error('Cannot modify filter preferences while profiling'); } // Recursively unmount all roots. hook.getFiberRoots(rendererID).forEach(root => { const rootInstance = rootToFiberInstanceMap.get(root); if (rootInstance === undefined) { throw new Error( 'Expected the root instance to already exist when applying filters', ); } currentRoot = rootInstance; unmountInstanceRecursively(rootInstance); rootToFiberInstanceMap.delete(root); flushPendingEvents(root); currentRoot = (null: any); }); applyComponentFilters(componentFilters); // Reset pseudo counters so that new path selections will be persisted. rootDisplayNameCounter.clear(); // Recursively re-mount all roots with new filter criteria applied. hook.getFiberRoots(rendererID).forEach(root => { const current = root.current; const newRoot = createFiberInstance(current); rootToFiberInstanceMap.set(root, newRoot); idToDevToolsInstanceMap.set(newRoot.id, newRoot); // Before the traversals, remember to start tracking // our path in case we have selection to restore. if (trackedPath !== null) { mightBeOnTrackedPath = true; } currentRoot = newRoot; setRootPseudoKey(currentRoot.id, root.current); mountFiberRecursively(root.current, false); flushPendingEvents(root); currentRoot = (null: any); }); flushPendingEvents(); needsToFlushComponentLogs = false; } function getEnvironmentNames(): Array { return Array.from(knownEnvironmentNames); } function isFiberHydrated(fiber: Fiber): boolean { if (OffscreenComponent === -1) { throw new Error('not implemented for legacy suspense'); } switch (fiber.tag) { case HostRoot: const rootState = fiber.memoizedState; return !rootState.isDehydrated; case SuspenseComponent: const suspenseState = fiber.memoizedState; return suspenseState === null || suspenseState.dehydrated === null; default: throw new Error('not implemented for work tag ' + fiber.tag); } } function shouldFilterVirtual( data: ReactComponentInfo, secondaryEnv: null | string, ): boolean { // For purposes of filtering Server Components are always Function Components. // Environment will be used to filter Server vs Client. // Technically they can be forwardRef and memo too but those filters will go away // as those become just plain user space function components like any HoC. if (hideElementsWithTypes.has(ElementTypeFunction)) { return true; } if (hideElementsWithDisplayNames.size > 0) { const displayName = data.name; if (displayName != null) { // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const displayNameRegExp of hideElementsWithDisplayNames) { if (displayNameRegExp.test(displayName)) { return true; } } } } if ( (data.env == null || hideElementsWithEnvs.has(data.env)) && (secondaryEnv === null || hideElementsWithEnvs.has(secondaryEnv)) ) { // If a Component has two environments, you have to filter both for it not to appear. return true; } return false; } // NOTICE Keep in sync with get*ForFiber methods function shouldFilterFiber(fiber: Fiber): boolean { const {tag, type, key} = fiber; switch (tag) { case DehydratedSuspenseComponent: // TODO: ideally we would show dehydrated Suspense immediately. // However, it has some special behavior (like disconnecting // an alternate and turning into real Suspense) which breaks DevTools. // For now, ignore it, and only show it once it gets hydrated. // https://github.com/bvaughn/react-devtools-experimental/issues/197 return true; case HostPortal: case HostText: case LegacyHiddenComponent: case OffscreenComponent: case Throw: return true; case HostRoot: // It is never valid to filter the root element. return false; case Fragment: return key === null; default: const typeSymbol = getTypeSymbol(type); switch (typeSymbol) { case CONCURRENT_MODE_NUMBER: case CONCURRENT_MODE_SYMBOL_STRING: case DEPRECATED_ASYNC_MODE_SYMBOL_STRING: case STRICT_MODE_NUMBER: case STRICT_MODE_SYMBOL_STRING: return true; default: break; } } const elementType = getElementTypeForFiber(fiber); if (hideElementsWithTypes.has(elementType)) { return true; } if (hideElementsWithDisplayNames.size > 0) { const displayName = getDisplayNameForFiber(fiber); if (displayName != null) { // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const displayNameRegExp of hideElementsWithDisplayNames) { if (displayNameRegExp.test(displayName)) { return true; } } } } if (hideElementsWithEnvs.has('Client')) { // If we're filtering out the Client environment we should filter out all // "Client Components". Technically that also includes the built-ins but // since that doesn't actually include any additional code loading it's // useful to not filter out the built-ins. Those can be filtered separately. // There's no other way to filter out just Function components on the Client. // Therefore, this only filters Class and Function components. switch (tag) { case ClassComponent: case IncompleteClassComponent: case IncompleteFunctionComponent: case FunctionComponent: case IndeterminateComponent: case ForwardRef: case MemoComponent: case SimpleMemoComponent: return true; } } /* DISABLED: https://github.com/facebook/react/pull/28417 if (hideElementsWithPaths.size > 0) { const source = getSourceForFiber(fiber); if (source != null) { const {fileName} = source; // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const pathRegExp of hideElementsWithPaths) { if (pathRegExp.test(fileName)) { return true; } } } } */ return false; } // NOTICE Keep in sync with shouldFilterFiber() and other get*ForFiber methods function getElementTypeForFiber(fiber: Fiber): ElementType { const {type, tag} = fiber; switch (tag) { case ActivityComponent: return ElementTypeActivity; case ClassComponent: case IncompleteClassComponent: return ElementTypeClass; case IncompleteFunctionComponent: case FunctionComponent: case IndeterminateComponent: return ElementTypeFunction; case ForwardRef: return ElementTypeForwardRef; case HostRoot: return ElementTypeRoot; case HostComponent: case HostHoistable: case HostSingleton: return ElementTypeHostComponent; case HostPortal: case HostText: case Fragment: return ElementTypeOtherOrUnknown; case MemoComponent: case SimpleMemoComponent: return ElementTypeMemo; case SuspenseComponent: return ElementTypeSuspense; case SuspenseListComponent: return ElementTypeSuspenseList; case TracingMarkerComponent: return ElementTypeTracingMarker; case ViewTransitionComponent: return ElementTypeViewTransition; default: const typeSymbol = getTypeSymbol(type); switch (typeSymbol) { case CONCURRENT_MODE_NUMBER: case CONCURRENT_MODE_SYMBOL_STRING: case DEPRECATED_ASYNC_MODE_SYMBOL_STRING: return ElementTypeOtherOrUnknown; case PROVIDER_NUMBER: case PROVIDER_SYMBOL_STRING: return ElementTypeContext; case CONTEXT_NUMBER: case CONTEXT_SYMBOL_STRING: return ElementTypeContext; case STRICT_MODE_NUMBER: case STRICT_MODE_SYMBOL_STRING: return ElementTypeOtherOrUnknown; case PROFILER_NUMBER: case PROFILER_SYMBOL_STRING: return ElementTypeProfiler; default: return ElementTypeOtherOrUnknown; } } } // When a mount or update is in progress, this value tracks the root that is being operated on. let currentRoot: FiberInstance = (null: any); // Removes a Fiber (and its alternate) from the Maps used to track their id. // This method should always be called when a Fiber is unmounting. function untrackFiber(nearestInstance: DevToolsInstance, fiber: Fiber) { if (forceErrorForFibers.size > 0) { forceErrorForFibers.delete(fiber); if (fiber.alternate) { forceErrorForFibers.delete(fiber.alternate); } if (forceErrorForFibers.size === 0 && setErrorHandler != null) { setErrorHandler(shouldErrorFiberAlwaysNull); } } if (forceFallbackForFibers.size > 0) { forceFallbackForFibers.delete(fiber); if (fiber.alternate) { forceFallbackForFibers.delete(fiber.alternate); } if (forceFallbackForFibers.size === 0 && setSuspenseHandler != null) { setSuspenseHandler(shouldSuspendFiberAlwaysFalse); } } // TODO: Consider using a WeakMap instead. The only thing where that doesn't work // is React Native Paper which tracks tags but that support is eventually going away // and can use the old findFiberByHostInstance strategy. if (fiber.tag === HostHoistable) { releaseHostResource(nearestInstance, fiber.memoizedState); } else if ( fiber.tag === HostComponent || fiber.tag === HostText || fiber.tag === HostSingleton ) { releaseHostInstance(nearestInstance, fiber.stateNode); } // Recursively clean up any filtered Fibers below this one as well since // we won't recordUnmount on those. for (let child = fiber.child; child !== null; child = child.sibling) { if (shouldFilterFiber(child)) { untrackFiber(nearestInstance, child); } } } function getChangeDescription( prevFiber: Fiber | null, nextFiber: Fiber, ): ChangeDescription | null { switch (nextFiber.tag) { case ClassComponent: if (prevFiber === null) { return { context: null, didHooksChange: false, isFirstMount: true, props: null, state: null, }; } else { const data: ChangeDescription = { context: getContextChanged(prevFiber, nextFiber), didHooksChange: false, isFirstMount: false, props: getChangedKeys( prevFiber.memoizedProps, nextFiber.memoizedProps, ), state: getChangedKeys( prevFiber.memoizedState, nextFiber.memoizedState, ), }; return data; } case IncompleteFunctionComponent: case FunctionComponent: case IndeterminateComponent: case ForwardRef: case MemoComponent: case SimpleMemoComponent: if (prevFiber === null) { return { context: null, didHooksChange: false, isFirstMount: true, props: null, state: null, }; } else { const indices = getChangedHooksIndices( prevFiber.memoizedState, nextFiber.memoizedState, ); const data: ChangeDescription = { context: getContextChanged(prevFiber, nextFiber), didHooksChange: indices !== null && indices.length > 0, isFirstMount: false, props: getChangedKeys( prevFiber.memoizedProps, nextFiber.memoizedProps, ), state: null, hooks: indices, }; // Only traverse the hooks list once, depending on what info we're returning. return data; } default: return null; } } function getContextChanged(prevFiber: Fiber, nextFiber: Fiber): boolean { let prevContext = prevFiber.dependencies && prevFiber.dependencies.firstContext; let nextContext = nextFiber.dependencies && nextFiber.dependencies.firstContext; while (prevContext && nextContext) { // Note this only works for versions of React that support this key (e.v. 18+) // For older versions, there's no good way to read the current context value after render has completed. // This is because React maintains a stack of context values during render, // but by the time DevTools is called, render has finished and the stack is empty. if (prevContext.context !== nextContext.context) { // If the order of context has changed, then the later context values might have // changed too but the main reason it rerendered was earlier. Either an earlier // context changed value but then we would have exited already. If we end up here // it's because a state or props change caused the order of contexts used to change. // So the main cause is not the contexts themselves. return false; } if (!is(prevContext.memoizedValue, nextContext.memoizedValue)) { return true; } prevContext = prevContext.next; nextContext = nextContext.next; } return false; } function isHookThatCanScheduleUpdate(hookObject: any) { const queue = hookObject.queue; if (!queue) { return false; } const boundHasOwnProperty = hasOwnProperty.bind(queue); // Detect the shape of useState() / useReducer() / useTransition() // using the attributes that are unique to these hooks // but also stable (e.g. not tied to current Lanes implementation) // We don't check for dispatch property, because useTransition doesn't have it if (boundHasOwnProperty('pending')) { return true; } // Detect useSyncExternalStore() return ( boundHasOwnProperty('value') && boundHasOwnProperty('getSnapshot') && typeof queue.getSnapshot === 'function' ); } function didStatefulHookChange(prev: any, next: any): boolean { const prevMemoizedState = prev.memoizedState; const nextMemoizedState = next.memoizedState; if (isHookThatCanScheduleUpdate(prev)) { return prevMemoizedState !== nextMemoizedState; } return false; } function getChangedHooksIndices(prev: any, next: any): null | Array { if (prev == null || next == null) { return null; } const indices = []; let index = 0; while (next !== null) { if (didStatefulHookChange(prev, next)) { indices.push(index); } next = next.next; prev = prev.next; index++; } return indices; } function getChangedKeys(prev: any, next: any): null | Array { if (prev == null || next == null) { return null; } const keys = new Set([...Object.keys(prev), ...Object.keys(next)]); const changedKeys = []; // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const key of keys) { if (prev[key] !== next[key]) { changedKeys.push(key); } } return changedKeys; } function didFiberRender(prevFiber: Fiber, nextFiber: Fiber): boolean { switch (nextFiber.tag) { case ClassComponent: case FunctionComponent: case ContextConsumer: case MemoComponent: case SimpleMemoComponent: case ForwardRef: // For types that execute user code, we check PerformedWork effect. // We don't reflect bailouts (either referential or sCU) in DevTools. // TODO: This flag is a leaked implementation detail. Once we start // releasing DevTools in lockstep with React, we should import a // function from the reconciler instead. const PerformedWork = 0b000000000000000000000000001; return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork; // Note: ContextConsumer only gets PerformedWork effect in 16.3.3+ // so it won't get highlighted with React 16.3.0 to 16.3.2. default: // For host components and other types, we compare inputs // to determine whether something is an update. return ( prevFiber.memoizedProps !== nextFiber.memoizedProps || prevFiber.memoizedState !== nextFiber.memoizedState || prevFiber.ref !== nextFiber.ref ); } } type OperationsArray = Array; type StringTableEntry = { encodedString: Array, id: number, }; const pendingOperations: OperationsArray = []; const pendingRealUnmountedIDs: Array = []; const pendingRealUnmountedSuspenseIDs: Array = []; let pendingOperationsQueue: Array | null = []; const pendingStringTable: Map = new Map(); let pendingStringTableLength: number = 0; let pendingUnmountedRootID: FiberInstance['id'] | null = null; function pushOperation(op: number): void { if (__DEV__) { if (!Number.isInteger(op)) { console.error( 'pushOperation() was called but the value is not an integer.', op, ); } } pendingOperations.push(op); } function shouldBailoutWithPendingOperations() { if (isProfiling) { if ( currentCommitProfilingMetadata != null && currentCommitProfilingMetadata.durations.length > 0 ) { return false; } } return ( pendingOperations.length === 0 && pendingRealUnmountedIDs.length === 0 && pendingRealUnmountedSuspenseIDs.length === 0 && pendingUnmountedRootID === null ); } function flushOrQueueOperations(operations: OperationsArray): void { if (shouldBailoutWithPendingOperations()) { return; } if (pendingOperationsQueue !== null) { pendingOperationsQueue.push(operations); } else { hook.emit('operations', operations); } } function recordConsoleLogs( instance: FiberInstance | VirtualInstance, componentLogsEntry: void | ComponentLogs, ): boolean { if (componentLogsEntry === undefined) { if (instance.logCount === 0) { // Nothing has changed. return false; } // Reset to zero. instance.logCount = 0; pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS); pushOperation(instance.id); pushOperation(0); pushOperation(0); return true; } else { const totalCount = componentLogsEntry.errorsCount + componentLogsEntry.warningsCount; if (instance.logCount === totalCount) { // Nothing has changed. return false; } // Update counts. instance.logCount = totalCount; pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS); pushOperation(instance.id); pushOperation(componentLogsEntry.errorsCount); pushOperation(componentLogsEntry.warningsCount); return true; } } function flushPendingEvents(root: Object): void { if (shouldBailoutWithPendingOperations()) { // If we aren't profiling, we can just bail out here. // No use sending an empty update over the bridge. // // The Profiler stores metadata for each commit and reconstructs the app tree per commit using: // (1) an initial tree snapshot and // (2) the operations array for each commit // Because of this, it's important that the operations and metadata arrays align, // So it's important not to omit even empty operations while profiling is active. return; } const numUnmountIDs = pendingRealUnmountedIDs.length + (pendingUnmountedRootID === null ? 0 : 1); const numUnmountSuspenseIDs = pendingRealUnmountedSuspenseIDs.length; const operations = new Array( // Identify which renderer this update is coming from. 2 + // [rendererID, rootFiberID] // How big is the string table? 1 + // [stringTableLength] // Then goes the actual string table. pendingStringTableLength + // All unmounts of Suspense boundaries are batched in a single message. // [TREE_OPERATION_REMOVE_SUSPENSE, removedSuspenseIDLength, ...ids] (numUnmountSuspenseIDs > 0 ? 2 + numUnmountSuspenseIDs : 0) + // All unmounts are batched in a single message. // [TREE_OPERATION_REMOVE, removedIDLength, ...ids] (numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) + // Regular operations pendingOperations.length, ); // Identify which renderer this update is coming from. // This enables roots to be mapped to renderers, // Which in turn enables fiber props, states, and hooks to be inspected. let i = 0; operations[i++] = rendererID; if (currentRoot === null) { // TODO: This is not always safe so this field is probably not needed. operations[i++] = -1; } else { operations[i++] = currentRoot.id; } // Now fill in the string table. // [stringTableLength, str1Length, ...str1, str2Length, ...str2, ...] operations[i++] = pendingStringTableLength; pendingStringTable.forEach((entry, stringKey) => { const encodedString = entry.encodedString; // Don't use the string length. // It won't work for multibyte characters (like emoji). const length = encodedString.length; operations[i++] = length; for (let j = 0; j < length; j++) { operations[i + j] = encodedString[j]; } i += length; }); if (numUnmountSuspenseIDs > 0) { // All unmounts of Suspense boundaries are batched in a single message. operations[i++] = SUSPENSE_TREE_OPERATION_REMOVE; // The first number is how many unmounted IDs we're gonna send. operations[i++] = numUnmountSuspenseIDs; // Fill in the real unmounts in the reverse order. // They were inserted parents-first by React, but we want children-first. // So we traverse our array backwards. for (let j = 0; j < pendingRealUnmountedSuspenseIDs.length; j++) { operations[i++] = pendingRealUnmountedSuspenseIDs[j]; } } if (numUnmountIDs > 0) { // All unmounts except roots are batched in a single message. operations[i++] = TREE_OPERATION_REMOVE; // The first number is how many unmounted IDs we're gonna send. operations[i++] = numUnmountIDs; // Fill in the real unmounts in the reverse order. // They were inserted parents-first by React, but we want children-first. // So we traverse our array backwards. for (let j = 0; j < pendingRealUnmountedIDs.length; j++) { operations[i++] = pendingRealUnmountedIDs[j]; } // The root ID should always be unmounted last. if (pendingUnmountedRootID !== null) { operations[i] = pendingUnmountedRootID; i++; } } // Fill in the rest of the operations. for (let j = 0; j < pendingOperations.length; j++) { operations[i + j] = pendingOperations[j]; } i += pendingOperations.length; // Let the frontend know about tree operations. flushOrQueueOperations(operations); // Reset all of the pending state now that we've told the frontend about it. pendingOperations.length = 0; pendingRealUnmountedIDs.length = 0; pendingRealUnmountedSuspenseIDs.length = 0; pendingUnmountedRootID = null; pendingStringTable.clear(); pendingStringTableLength = 0; } function measureHostInstance(instance: HostInstance): null | Array { // Feature detect measurement capabilities of this environment. // TODO: Consider making this capability injected by the ReactRenderer. if (typeof instance !== 'object' || instance === null) { return null; } if (typeof instance.getClientRects === 'function') { // DOM const result = []; const doc = instance.ownerDocument; const win = doc && doc.defaultView; const scrollX = win ? win.scrollX : 0; const scrollY = win ? win.scrollY : 0; const rects = instance.getClientRects(); for (let i = 0; i < rects.length; i++) { const rect = rects[i]; result.push({ x: rect.x + scrollX, y: rect.y + scrollY, width: rect.width, height: rect.height, }); } return result; } if (instance.canonical) { // Native const publicInstance = instance.canonical.publicInstance; if (!publicInstance) { // The publicInstance may not have been initialized yet if there was no ref on this node. // We can't initialize it from any existing Hook but we could fallback to this async form: // renderer.extraDevToolsConfig.getInspectorDataForInstance(instance).hierarchy[last].getInspectorData().measure(callback) return null; } if (typeof publicInstance.getBoundingClientRect === 'function') { // enableAccessToHostTreeInFabric / ReadOnlyElement return [publicInstance.getBoundingClientRect()]; } if (typeof publicInstance.unstable_getBoundingClientRect === 'function') { // ReactFabricHostComponent return [publicInstance.unstable_getBoundingClientRect()]; } } return null; } function measureInstance(instance: DevToolsInstance): null | Array { // Synchronously return the client rects of the Host instances directly inside this Instance. const hostInstances = findAllCurrentHostInstances(instance); let result: null | Array = null; for (let i = 0; i < hostInstances.length; i++) { const childResult = measureHostInstance(hostInstances[i]); if (childResult !== null) { if (result === null) { result = childResult; } else { result = result.concat(childResult); } } } return result; } function getStringID(string: string | null): number { if (string === null) { return 0; } const existingEntry = pendingStringTable.get(string); if (existingEntry !== undefined) { return existingEntry.id; } const id = pendingStringTable.size + 1; const encodedString = utfEncodeString(string); pendingStringTable.set(string, { encodedString, id, }); // The string table total length needs to account both for the string length, // and for the array item that contains the length itself. // // Don't use string length for this table. // It won't work for multibyte characters (like emoji). pendingStringTableLength += encodedString.length + 1; return id; } let isInDisconnectedSubtree = false; function recordMount( fiber: Fiber, parentInstance: DevToolsInstance | null, ): FiberInstance { const isRoot = fiber.tag === HostRoot; let fiberInstance; if (isRoot) { const entry = rootToFiberInstanceMap.get(fiber.stateNode); if (entry === undefined) { throw new Error('The root should have been registered at this point'); } fiberInstance = entry; } else { fiberInstance = createFiberInstance(fiber); } idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance); if (__DEBUG__) { debug('recordMount()', fiberInstance, parentInstance); } recordReconnect(fiberInstance, parentInstance); return fiberInstance; } function recordReconnect( fiberInstance: FiberInstance, parentInstance: DevToolsInstance | null, ): void { if (isInDisconnectedSubtree) { // We're disconnected. We'll reconnect a hidden mount after the parent reappears. return; } const id = fiberInstance.id; const fiber = fiberInstance.data; const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration'); const isRoot = fiber.tag === HostRoot; if (isRoot) { const hasOwnerMetadata = fiber.hasOwnProperty('_debugOwner'); // Adding a new field here would require a bridge protocol version bump (a backwads breaking change). // Instead let's re-purpose a pre-existing field to carry more information. let profilingFlags = 0; if (isProfilingSupported) { profilingFlags = PROFILING_FLAG_BASIC_SUPPORT; if (typeof injectProfilingHooks === 'function') { profilingFlags |= PROFILING_FLAG_TIMELINE_SUPPORT; } } // Set supportsStrictMode to false for production renderer builds const isProductionBuildOfRenderer = renderer.bundleType === 0; pushOperation(TREE_OPERATION_ADD); pushOperation(id); pushOperation(ElementTypeRoot); pushOperation((fiber.mode & StrictModeBits) !== 0 ? 1 : 0); pushOperation(profilingFlags); pushOperation( !isProductionBuildOfRenderer && StrictModeBits !== 0 ? 1 : 0, ); pushOperation(hasOwnerMetadata ? 1 : 0); if (isProfiling) { if (displayNamesByRootID !== null) { displayNamesByRootID.set(id, getDisplayNameForRoot(fiber)); } } } else { const {key} = fiber; const displayName = getDisplayNameForFiber(fiber); const elementType = getElementTypeForFiber(fiber); // Finding the owner instance might require traversing the whole parent path which // doesn't have great big O notation. Ideally we'd lazily fetch the owner when we // need it but we have some synchronous operations in the front end like Alt+Left // which selects the owner immediately. Typically most owners are only a few parents // away so maybe it's not so bad. const debugOwner = getUnfilteredOwner(fiber); const ownerInstance = findNearestOwnerInstance( parentInstance, debugOwner, ); if ( ownerInstance !== null && debugOwner === fiber._debugOwner && fiber._debugStack != null && ownerInstance.source === null ) { // The new Fiber is directly owned by the ownerInstance. Therefore somewhere on // the debugStack will be a stack frame inside the ownerInstance's source. ownerInstance.source = fiber._debugStack; } let unfilteredParent = parentInstance; while ( unfilteredParent !== null && unfilteredParent.kind === FILTERED_FIBER_INSTANCE ) { unfilteredParent = unfilteredParent.parent; } const ownerID = ownerInstance === null ? 0 : ownerInstance.id; const parentID = unfilteredParent === null ? 0 : unfilteredParent.id; const displayNameStringID = getStringID(displayName); // This check is a guard to handle a React element that has been modified // in such a way as to bypass the default stringification of the "key" property. const keyString = key === null ? null : String(key); const keyStringID = getStringID(keyString); const nameProp = fiber.tag === SuspenseComponent ? fiber.memoizedProps.name : fiber.tag === ActivityComponent ? fiber.memoizedProps.name : null; const namePropString = nameProp == null ? null : String(nameProp); const namePropStringID = getStringID(namePropString); pushOperation(TREE_OPERATION_ADD); pushOperation(id); pushOperation(elementType); pushOperation(parentID); pushOperation(ownerID); pushOperation(displayNameStringID); pushOperation(keyStringID); pushOperation(namePropStringID); // If this subtree has a new mode, let the frontend know. if ((fiber.mode & StrictModeBits) !== 0) { let parentFiber = null; let parentFiberInstance = parentInstance; while (parentFiberInstance !== null) { if (parentFiberInstance.kind === FIBER_INSTANCE) { parentFiber = parentFiberInstance.data; break; } parentFiberInstance = parentFiberInstance.parent; } if (parentFiber === null || (parentFiber.mode & StrictModeBits) === 0) { pushOperation(TREE_OPERATION_SET_SUBTREE_MODE); pushOperation(id); pushOperation(StrictMode); } } } let componentLogsEntry = fiberToComponentLogsMap.get(fiber); if (componentLogsEntry === undefined && fiber.alternate !== null) { componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate); } recordConsoleLogs(fiberInstance, componentLogsEntry); if (isProfilingSupported) { recordProfilingDurations(fiberInstance, null); } } function recordVirtualMount( instance: VirtualInstance, parentInstance: DevToolsInstance | null, secondaryEnv: null | string, ): void { const id = instance.id; idToDevToolsInstanceMap.set(id, instance); recordVirtualReconnect(instance, parentInstance, secondaryEnv); } function recordVirtualReconnect( instance: VirtualInstance, parentInstance: DevToolsInstance | null, secondaryEnv: null | string, ): void { if (isInDisconnectedSubtree) { // We're disconnected. We'll reconnect a hidden mount after the parent reappears. return; } const componentInfo = instance.data; const key = typeof componentInfo.key === 'string' ? componentInfo.key : null; const env = componentInfo.env; let displayName = componentInfo.name || ''; if (typeof env === 'string') { // We model environment as an HoC name for now. if (secondaryEnv !== null) { displayName = secondaryEnv + '(' + displayName + ')'; } displayName = env + '(' + displayName + ')'; } const elementType = ElementTypeVirtual; // Finding the owner instance might require traversing the whole parent path which // doesn't have great big O notation. Ideally we'd lazily fetch the owner when we // need it but we have some synchronous operations in the front end like Alt+Left // which selects the owner immediately. Typically most owners are only a few parents // away so maybe it's not so bad. const debugOwner = getUnfilteredOwner(componentInfo); const ownerInstance = findNearestOwnerInstance(parentInstance, debugOwner); if ( ownerInstance !== null && debugOwner === componentInfo.owner && componentInfo.debugStack != null && ownerInstance.source === null ) { // The new Fiber is directly owned by the ownerInstance. Therefore somewhere on // the debugStack will be a stack frame inside the ownerInstance's source. ownerInstance.source = componentInfo.debugStack; } let unfilteredParent = parentInstance; while ( unfilteredParent !== null && unfilteredParent.kind === FILTERED_FIBER_INSTANCE ) { unfilteredParent = unfilteredParent.parent; } const ownerID = ownerInstance === null ? 0 : ownerInstance.id; const parentID = unfilteredParent === null ? 0 : unfilteredParent.id; const displayNameStringID = getStringID(displayName); // This check is a guard to handle a React element that has been modified // in such a way as to bypass the default stringification of the "key" property. const keyString = key === null ? null : String(key); const keyStringID = getStringID(keyString); const namePropStringID = getStringID(null); const id = instance.id; pushOperation(TREE_OPERATION_ADD); pushOperation(id); pushOperation(elementType); pushOperation(parentID); pushOperation(ownerID); pushOperation(displayNameStringID); pushOperation(keyStringID); pushOperation(namePropStringID); const componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo); recordConsoleLogs(instance, componentLogsEntry); } function recordSuspenseMount( suspenseInstance: SuspenseNode, parentSuspenseInstance: SuspenseNode | null, ): void { const fiberInstance = suspenseInstance.instance; if (fiberInstance.kind === FILTERED_FIBER_INSTANCE) { throw new Error('Cannot record a mount for a filtered Fiber instance.'); } const fiberID = fiberInstance.id; let unfilteredParent = parentSuspenseInstance; while ( unfilteredParent !== null && unfilteredParent.instance.kind === FILTERED_FIBER_INSTANCE ) { unfilteredParent = unfilteredParent.parent; } const unfilteredParentInstance = unfilteredParent !== null ? unfilteredParent.instance : null; if ( unfilteredParentInstance !== null && unfilteredParentInstance.kind === FILTERED_FIBER_INSTANCE ) { throw new Error( 'Should not have a filtered instance at this point. This is a bug.', ); } const parentID = unfilteredParentInstance === null ? 0 : unfilteredParentInstance.id; const fiber = fiberInstance.data; const props = fiber.memoizedProps; // TODO: Compute a fallback name based on Owner, key etc. const name = props === null ? null : props.name || null; const nameStringID = getStringID(name); if (__DEBUG__) { console.log('recordSuspenseMount()', suspenseInstance); } idToSuspenseNodeMap.set(fiberID, suspenseInstance); pushOperation(SUSPENSE_TREE_OPERATION_ADD); pushOperation(fiberID); pushOperation(parentID); pushOperation(nameStringID); const rects = suspenseInstance.rects; if (rects === null) { pushOperation(-1); } else { pushOperation(rects.length); for (let i = 0; i < rects.length; ++i) { const rect = rects[i]; pushOperation(Math.round(rect.x)); pushOperation(Math.round(rect.y)); pushOperation(Math.round(rect.width)); pushOperation(Math.round(rect.height)); } } } function recordUnmount(fiberInstance: FiberInstance): void { if (__DEBUG__) { debug('recordUnmount()', fiberInstance, reconcilingParent); } recordDisconnect(fiberInstance); const suspenseNode = fiberInstance.suspenseNode; if (suspenseNode !== null) { recordSuspenseUnmount(suspenseNode); } idToDevToolsInstanceMap.delete(fiberInstance.id); untrackFiber(fiberInstance, fiberInstance.data); } function recordDisconnect(fiberInstance: FiberInstance): void { if (isInDisconnectedSubtree) { // Already disconnected. return; } const fiber = fiberInstance.data; if (trackedPathMatchInstance === fiberInstance) { // We're in the process of trying to restore previous selection. // If this fiber matched but is being hidden, there's no use trying. // Reset the state so we don't keep holding onto it. setTrackedPath(null); } const id = fiberInstance.id; const isRoot = fiber.tag === HostRoot; if (isRoot) { // Roots must be removed only after all children have been removed. // So we track it separately. pendingUnmountedRootID = id; } else { // To maintain child-first ordering, // we'll push it into one of these queues, // and later arrange them in the correct order. pendingRealUnmountedIDs.push(id); } } function recordSuspenseResize(suspenseNode: SuspenseNode): void { if (__DEBUG__) { console.log('recordSuspenseResize()', suspenseNode); } const fiberInstance = suspenseNode.instance; if (fiberInstance.kind !== FIBER_INSTANCE) { // TODO: Resizes of filtered Suspense nodes are currently dropped. return; } pushOperation(SUSPENSE_TREE_OPERATION_RESIZE); pushOperation(fiberInstance.id); const rects = suspenseNode.rects; if (rects === null) { pushOperation(-1); } else { pushOperation(rects.length); for (let i = 0; i < rects.length; ++i) { const rect = rects[i]; pushOperation(Math.round(rect.x)); pushOperation(Math.round(rect.y)); pushOperation(Math.round(rect.width)); pushOperation(Math.round(rect.height)); } } } function recordSuspenseUnmount(suspenseInstance: SuspenseNode): void { if (__DEBUG__) { console.log( 'recordSuspenseUnmount()', suspenseInstance, reconcilingParentSuspenseNode, ); } const devtoolsInstance = suspenseInstance.instance; if (devtoolsInstance.kind !== FIBER_INSTANCE) { throw new Error("Can't unmount a filtered SuspenseNode. This is a bug."); } const fiberInstance = devtoolsInstance; const id = fiberInstance.id; // To maintain child-first ordering, // we'll push it into one of these queues, // and later arrange them in the correct order. pendingRealUnmountedSuspenseIDs.push(id); idToSuspenseNodeMap.delete(id); } // Running state of the remaining children from the previous version of this parent that // we haven't yet added back. This should be reset anytime we change parent. // Any remaining ones at the end will be deleted. let remainingReconcilingChildren: null | DevToolsInstance = null; // The previously placed child. let previouslyReconciledSibling: null | DevToolsInstance = null; // To save on stack allocation and ensure that they are updated as a pair, we also store // the current parent here as well. let reconcilingParent: null | DevToolsInstance = null; let remainingReconcilingChildrenSuspenseNodes: null | SuspenseNode = null; // The previously placed child. let previouslyReconciledSiblingSuspenseNode: null | SuspenseNode = null; // To save on stack allocation and ensure that they are updated as a pair, we also store // the current parent here as well. let reconcilingParentSuspenseNode: null | SuspenseNode = null; function ioExistsInSuspenseAncestor( suspenseNode: SuspenseNode, ioInfo: ReactIOInfo, ): boolean { let ancestor = suspenseNode.parent; while (ancestor !== null) { if (ancestor.suspendedBy.has(ioInfo)) { return true; } ancestor = ancestor.parent; } return false; } function insertSuspendedBy(asyncInfo: ReactAsyncInfo): void { if (reconcilingParent === null || reconcilingParentSuspenseNode === null) { throw new Error( 'It should not be possible to have suspended data outside the root. ' + 'Even suspending at the first position is still a child of the root.', ); } const parentSuspenseNode = reconcilingParentSuspenseNode; // Use the nearest unfiltered parent so that there's always some component that has // the entry on it even if you filter, or the root if all are filtered. let parentInstance = reconcilingParent; while ( parentInstance.kind === FILTERED_FIBER_INSTANCE && parentInstance.parent !== null ) { parentInstance = parentInstance.parent; } const suspenseNodeSuspendedBy = parentSuspenseNode.suspendedBy; const ioInfo = asyncInfo.awaited; let suspendedBySet = suspenseNodeSuspendedBy.get(ioInfo); if (suspendedBySet === undefined) { suspendedBySet = new Set(); suspenseNodeSuspendedBy.set(asyncInfo.awaited, suspendedBySet); } // The child of the Suspense boundary that was suspended on this, or null if suspended at the root. // This is used to keep track of how many dependents are still alive and also to get information // like owner instances to link down into the tree. if (!suspendedBySet.has(parentInstance)) { suspendedBySet.add(parentInstance); if ( !parentSuspenseNode.hasUniqueSuspenders && !ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo) ) { // This didn't exist in the parent before, so let's mark this boundary as having a unique suspender. parentSuspenseNode.hasUniqueSuspenders = true; } } // We have observed at least one known reason this might have been suspended. parentSuspenseNode.hasUnknownSuspenders = false; // Suspending right below the root is not attributed to any particular component in UI // other than the SuspenseNode and the HostRoot's FiberInstance. const suspendedBy = parentInstance.suspendedBy; if (suspendedBy === null) { parentInstance.suspendedBy = [asyncInfo]; } else if (suspendedBy.indexOf(asyncInfo) === -1) { suspendedBy.push(asyncInfo); } } function getAwaitInSuspendedByFromIO( suspensedBy: Array, ioInfo: ReactIOInfo, ): null | ReactAsyncInfo { for (let i = 0; i < suspensedBy.length; i++) { const asyncInfo = suspensedBy[i]; if (asyncInfo.awaited === ioInfo) { return asyncInfo; } } return null; } function unblockSuspendedBy( parentSuspenseNode: SuspenseNode, ioInfo: ReactIOInfo, ): void { const firstChild = parentSuspenseNode.firstChild; if (firstChild === null) { return; } let node: SuspenseNode = firstChild; while (node !== null) { if (node.suspendedBy.has(ioInfo)) { // We have found a child boundary that depended on the unblocked I/O. // It can now be marked as having unique suspenders. We can skip its children // since they'll still be blocked by this one. node.hasUniqueSuspenders = true; node.hasUnknownSuspenders = false; } else if (node.firstChild !== null) { node = node.firstChild; continue; } while (node.nextSibling === null) { if (node.parent === null || node.parent === parentSuspenseNode) { return; } node = node.parent; } node = node.nextSibling; } } function removePreviousSuspendedBy( instance: DevToolsInstance, previousSuspendedBy: null | Array, ): void { // Remove any async info from the parent, if they were in the previous set but // is no longer in the new set. const parentSuspenseNode = reconcilingParentSuspenseNode; if (previousSuspendedBy !== null && parentSuspenseNode !== null) { const nextSuspendedBy = instance.suspendedBy; for (let i = 0; i < previousSuspendedBy.length; i++) { const asyncInfo = previousSuspendedBy[i]; if ( nextSuspendedBy === null || (nextSuspendedBy.indexOf(asyncInfo) === -1 && getAwaitInSuspendedByFromIO(nextSuspendedBy, asyncInfo.awaited) === null) ) { // This IO entry is no longer blocking the current tree. // Let's remove it from the parent SuspenseNode. const ioInfo = asyncInfo.awaited; const suspendedBySet = parentSuspenseNode.suspendedBy.get(ioInfo); if ( suspendedBySet === undefined || !suspendedBySet.delete(instance) ) { throw new Error( 'We are cleaning up async info that was not on the parent Suspense boundary. ' + 'This is a bug in React.', ); } if (suspendedBySet.size === 0) { parentSuspenseNode.suspendedBy.delete(asyncInfo.awaited); } if ( parentSuspenseNode.hasUniqueSuspenders && !ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo) ) { // This entry wasn't in any ancestor and is no longer in this suspense boundary. // This means that a child might now be the unique suspender for this IO. // Search the child boundaries to see if we can reveal any of them. unblockSuspendedBy(parentSuspenseNode, ioInfo); } } } } } function insertChild(instance: DevToolsInstance): void { const parentInstance = reconcilingParent; if (parentInstance === null) { // This instance is at the root. return; } // Place it in the parent. instance.parent = parentInstance; if (previouslyReconciledSibling === null) { previouslyReconciledSibling = instance; parentInstance.firstChild = instance; } else { previouslyReconciledSibling.nextSibling = instance; previouslyReconciledSibling = instance; } instance.nextSibling = null; // Insert any SuspenseNode into its parent Node. const suspenseNode = instance.suspenseNode; if (suspenseNode !== null) { const parentNode = reconcilingParentSuspenseNode; if (parentNode !== null) { suspenseNode.parent = parentNode; if (previouslyReconciledSiblingSuspenseNode === null) { previouslyReconciledSiblingSuspenseNode = suspenseNode; parentNode.firstChild = suspenseNode; } else { previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode; previouslyReconciledSiblingSuspenseNode = suspenseNode; } suspenseNode.nextSibling = null; } } } function moveChild( instance: DevToolsInstance, previousSibling: null | DevToolsInstance, ): void { removeChild(instance, previousSibling); insertChild(instance); } function removeChild( instance: DevToolsInstance, previousSibling: null | DevToolsInstance, ): void { if (instance.parent === null) { if (remainingReconcilingChildren === instance) { throw new Error( 'Remaining children should not have items with no parent', ); } else if (instance.nextSibling !== null) { throw new Error('A deleted instance should not have next siblings'); } // Already deleted. return; } const parentInstance = reconcilingParent; if (parentInstance === null) { throw new Error('Should not have a parent if we are at the root'); } if (instance.parent !== parentInstance) { throw new Error( 'Cannot remove a node from a different parent than is being reconciled.', ); } // Remove an existing child from its current position, which we assume is in the // remainingReconcilingChildren set. if (previousSibling === null) { // We're first in the remaining set. Remove us. if (remainingReconcilingChildren !== instance) { throw new Error( 'Expected a placed child to be moved from the remaining set.', ); } remainingReconcilingChildren = instance.nextSibling; } else { previousSibling.nextSibling = instance.nextSibling; } instance.nextSibling = null; instance.parent = null; // Remove any SuspenseNode from its parent. const suspenseNode = instance.suspenseNode; if (suspenseNode !== null && suspenseNode.parent !== null) { const parentNode = reconcilingParentSuspenseNode; if (parentNode === null) { throw new Error('Should not have a parent if we are at the root'); } if (suspenseNode.parent !== parentNode) { throw new Error( 'Cannot remove a Suspense node from a different parent than is being reconciled.', ); } let previousSuspenseSibling = remainingReconcilingChildrenSuspenseNodes; if (previousSuspenseSibling === suspenseNode) { // We're first in the remaining set. Remove us. remainingReconcilingChildrenSuspenseNodes = suspenseNode.nextSibling; } else { // Search for our previous sibling and remove us. while (previousSuspenseSibling !== null) { if (previousSuspenseSibling.nextSibling === suspenseNode) { previousSuspenseSibling.nextSibling = suspenseNode.nextSibling; break; } previousSuspenseSibling = previousSuspenseSibling.nextSibling; } } suspenseNode.nextSibling = null; suspenseNode.parent = null; } } function unmountRemainingChildren() { if ( reconcilingParent !== null && (reconcilingParent.kind === FIBER_INSTANCE || reconcilingParent.kind === FILTERED_FIBER_INSTANCE) && reconcilingParent.data.tag === OffscreenComponent && reconcilingParent.data.memoizedState !== null && !isInDisconnectedSubtree ) { // This is a hidden offscreen, we need to execute this in the context of a disconnected subtree. isInDisconnectedSubtree = true; try { let child = remainingReconcilingChildren; while (child !== null) { unmountInstanceRecursively(child); child = remainingReconcilingChildren; } } finally { isInDisconnectedSubtree = false; } } else { let child = remainingReconcilingChildren; while (child !== null) { unmountInstanceRecursively(child); child = remainingReconcilingChildren; } } } function isChildOf( parentInstance: DevToolsInstance, childInstance: DevToolsInstance, grandParent: DevToolsInstance, ): boolean { let instance = childInstance.parent; while (instance !== null) { if (parentInstance === instance) { return true; } if (instance === parentInstance.parent || instance === grandParent) { // This was a sibling but not inside the FiberInstance. We can bail out. break; } instance = instance.parent; } return false; } function areEqualRects( a: null | Array, b: null | Array, ): boolean { if (a === null) { return b === null; } if (b === null) { return false; } if (a.length !== b.length) { return false; } for (let i = 0; i < a.length; i++) { const aRect = a[i]; const bRect = b[i]; if ( aRect.x !== bRect.x || aRect.y !== bRect.y || aRect.width !== bRect.width || aRect.height !== bRect.height ) { return false; } } return true; } function measureUnchangedSuspenseNodesRecursively( suspenseNode: SuspenseNode, ): void { if (isInDisconnectedSubtree) { // We don't update rects inside disconnected subtrees. return; } const nextRects = measureInstance(suspenseNode.instance); const prevRects = suspenseNode.rects; if (areEqualRects(prevRects, nextRects)) { return; // Unchanged } // The rect has changed. While the bailed out root wasn't in a disconnected subtree, // it's possible that this node was in one. So we need to check if we're offscreen. let parent = suspenseNode.instance.parent; while (parent !== null) { if ( (parent.kind === FIBER_INSTANCE || parent.kind === FILTERED_FIBER_INSTANCE) && parent.data.tag === OffscreenComponent && parent.data.memoizedState !== null ) { // We're inside a hidden offscreen Fiber. We're in a disconnected tree. return; } if (parent.suspenseNode !== null) { // Found our parent SuspenseNode. We can bail out now. break; } parent = parent.parent; } // We changed inside a visible tree. // Since this boundary changed, it's possible it also affected its children so lets // measure them as well. for ( let child = suspenseNode.firstChild; child !== null; child = child.nextSibling ) { measureUnchangedSuspenseNodesRecursively(child); } suspenseNode.rects = nextRects; recordSuspenseResize(suspenseNode); } function consumeSuspenseNodesOfExistingInstance( instance: DevToolsInstance, ): void { // We need to also consume any unchanged Suspense boundaries. let suspenseNode = remainingReconcilingChildrenSuspenseNodes; if (suspenseNode === null) { return; } const parentSuspenseNode = reconcilingParentSuspenseNode; if (parentSuspenseNode === null) { throw new Error( 'The should not be any remaining suspense node children if there is no parent.', ); } let foundOne = false; let previousSkippedSibling = null; while (suspenseNode !== null) { // Check if this SuspenseNode was a child of the bailed out FiberInstance. if ( isChildOf(instance, suspenseNode.instance, parentSuspenseNode.instance) ) { foundOne = true; // The suspenseNode was child of the bailed out Fiber. // First, remove it from the remaining children set. const nextRemainingSibling = suspenseNode.nextSibling; if (previousSkippedSibling === null) { remainingReconcilingChildrenSuspenseNodes = nextRemainingSibling; } else { previousSkippedSibling.nextSibling = nextRemainingSibling; } suspenseNode.nextSibling = null; // Then, re-insert it into the newly reconciled set. if (previouslyReconciledSiblingSuspenseNode === null) { parentSuspenseNode.firstChild = suspenseNode; } else { previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode; } previouslyReconciledSiblingSuspenseNode = suspenseNode; // While React didn't rerender this node, it's possible that it was affected by // layout due to mutation of a parent or sibling. Check if it changed size. measureUnchangedSuspenseNodesRecursively(suspenseNode); // Continue suspenseNode = nextRemainingSibling; } else if (foundOne) { // If we found one and then hit a miss, we assume that we're passed the sequence because // they should've all been consecutive. break; } else { previousSkippedSibling = suspenseNode; suspenseNode = suspenseNode.nextSibling; } } } function mountVirtualInstanceRecursively( virtualInstance: VirtualInstance, firstChild: Fiber, lastChild: null | Fiber, // non-inclusive traceNearestHostComponentUpdate: boolean, virtualLevel: number, // the nth level of virtual instances ): void { // If we have the tree selection from previous reload, try to match this Instance. // Also remember whether to do the same for siblings. const mightSiblingsBeOnTrackedPath = updateVirtualTrackedPathStateBeforeMount( virtualInstance, reconcilingParent, ); const stashedParent = reconcilingParent; const stashedPrevious = previouslyReconciledSibling; const stashedRemaining = remainingReconcilingChildren; // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = virtualInstance; previouslyReconciledSibling = null; remainingReconcilingChildren = null; try { mountVirtualChildrenRecursively( firstChild, lastChild, traceNearestHostComponentUpdate, virtualLevel + 1, ); // Must be called after all children have been appended. recordVirtualProfilingDurations(virtualInstance); } finally { reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; remainingReconcilingChildren = stashedRemaining; updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath); } } function recordVirtualUnmount(instance: VirtualInstance) { recordVirtualDisconnect(instance); idToDevToolsInstanceMap.delete(instance.id); } function recordVirtualDisconnect(instance: VirtualInstance) { if (isInDisconnectedSubtree) { return; } if (trackedPathMatchInstance === instance) { // We're in the process of trying to restore previous selection. // If this fiber matched but is being unmounted, there's no use trying. // Reset the state so we don't keep holding onto it. setTrackedPath(null); } const id = instance.id; pendingRealUnmountedIDs.push(id); } function getSecondaryEnvironmentName( debugInfo: ?ReactDebugInfo, index: number, ): null | string { if (debugInfo != null) { const componentInfo: ReactComponentInfo = (debugInfo[index]: any); for (let i = index + 1; i < debugInfo.length; i++) { const debugEntry = debugInfo[i]; if (typeof debugEntry.env === 'string') { // If the next environment is different then this component was the boundary // and it changed before entering the next component. So we assign this // component a secondary environment. return componentInfo.env !== debugEntry.env ? debugEntry.env : null; } } } return null; } function trackDebugInfoFromLazyType(fiber: Fiber): void { // The debugInfo from a Lazy isn't propagated onto _debugInfo of the parent Fiber the way // it is when used in child position. So we need to pick it up explicitly. const type = fiber.elementType; const typeSymbol = getTypeSymbol(type); // The elementType might be have been a LazyComponent. if (typeSymbol === LAZY_SYMBOL_STRING) { const debugInfo: ?ReactDebugInfo = type._debugInfo; if (debugInfo) { for (let i = 0; i < debugInfo.length; i++) { const debugEntry = debugInfo[i]; if (debugEntry.awaited) { const asyncInfo: ReactAsyncInfo = (debugEntry: any); insertSuspendedBy(asyncInfo); } } } } } function trackDebugInfoFromUsedThenables(fiber: Fiber): void { // If a Fiber called use() in DEV mode then we may have collected _debugThenableState on // the dependencies. If so, then this will contain the thenables passed to use(). // These won't have their debug info picked up by fiber._debugInfo since that just // contains things suspending the children. We have to collect use() separately. const dependencies = fiber.dependencies; if (dependencies == null) { return; } const thenableState = dependencies._debugThenableState; if (thenableState == null) { return; } // In DEV the thenableState is an inner object. const usedThenables: any = thenableState.thenables || thenableState; if (!Array.isArray(usedThenables)) { return; } for (let i = 0; i < usedThenables.length; i++) { const thenable: Thenable = usedThenables[i]; const debugInfo = thenable._debugInfo; if (debugInfo) { for (let j = 0; j < debugInfo.length; j++) { const debugEntry = debugInfo[j]; if (debugEntry.awaited) { const asyncInfo: ReactAsyncInfo = (debugEntry: any); insertSuspendedBy(asyncInfo); } } } } } const hostAsyncInfoCache: WeakMap<{...}, ReactAsyncInfo> = new WeakMap(); function trackDebugInfoFromHostResource( devtoolsInstance: DevToolsInstance, fiber: Fiber, ): void { const resource: ?{ type: 'stylesheet' | 'style' | 'script' | 'void', instance?: null | HostInstance, ... } = fiber.memoizedState; if (resource == null) { return; } // Use a cached entry based on the resource. This ensures that if we use the same // resource in multiple places, it gets deduped and inner boundaries don't consider it // as contributing to those boundaries. const existingEntry = hostAsyncInfoCache.get(resource); if (existingEntry !== undefined) { insertSuspendedBy(existingEntry); return; } const props: { href?: string, media?: string, ... } = fiber.memoizedProps; // Stylesheet resources may suspend. We need to track that. const mayResourceSuspendCommit = resource.type === 'stylesheet' && // If it doesn't match the currently debugged media, then it doesn't count. (typeof props.media !== 'string' || typeof matchMedia !== 'function' || matchMedia(props.media)); if (!mayResourceSuspendCommit) { return; } const instance = resource.instance; if (instance == null) { return; } // Unlike props.href, this href will be fully qualified which we need for comparison below. const href = instance.href; if (typeof href !== 'string') { return; } let start = -1; let end = -1; // $FlowFixMe[method-unbinding] if (typeof performance.getEntriesByType === 'function') { // We may be able to collect the start and end time of this resource from Performance Observer. const resourceEntries = performance.getEntriesByType('resource'); for (let i = 0; i < resourceEntries.length; i++) { const resourceEntry = resourceEntries[i]; if (resourceEntry.name === href) { start = resourceEntry.startTime; end = start + resourceEntry.duration; } } } const value = instance.sheet; const promise = Promise.resolve(value); (promise: any).status = 'fulfilled'; (promise: any).value = value; const ioInfo: ReactIOInfo = { name: 'stylesheet', start, end, value: promise, // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file. owner: fiber, // Allow linking to the if it's not filtered. }; const asyncInfo: ReactAsyncInfo = { awaited: ioInfo, // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file. owner: fiber._debugOwner == null ? null : fiber._debugOwner, debugStack: fiber._debugStack == null ? null : fiber._debugStack, debugTask: fiber._debugTask == null ? null : fiber._debugTask, }; hostAsyncInfoCache.set(resource, asyncInfo); insertSuspendedBy(asyncInfo); } function trackDebugInfoFromHostComponent( devtoolsInstance: DevToolsInstance, fiber: Fiber, ): void { if (fiber.tag !== HostComponent) { return; } if ((fiber.mode & SuspenseyImagesMode) === 0) { // In any released version, Suspensey Images are only enabled inside a ViewTransition // subtree, which is enabled by the SuspenseyImagesMode. // TODO: If we ever enable the enableSuspenseyImages flag then it would be enabled for // all images and we'd need some other check for if the version of React has that enabled. return; } const type = fiber.type; const props: { src?: string, onLoad?: (event: any) => void, loading?: 'eager' | 'lazy', ... } = fiber.memoizedProps; const maySuspendCommit = type === 'img' && props.src != null && props.src !== '' && props.onLoad == null && props.loading !== 'lazy'; // Note: We don't track "maySuspendCommitOnUpdate" separately because it doesn't matter if // it didn't suspend this particular update if it would've suspended if it mounted in this // state, since we're tracking the dependencies inside the current state. if (!maySuspendCommit) { return; } const instance = fiber.stateNode; if (instance == null) { // Should never happen. return; } // Unlike props.src, currentSrc will be fully qualified which we need for comparison below. // Unlike instance.src it will be resolved into the media queries currently matching which is // the state we're inspecting. const src = instance.currentSrc; if (typeof src !== 'string' || src === '') { return; } let start = -1; let end = -1; let fileSize = 0; // $FlowFixMe[method-unbinding] if (typeof performance.getEntriesByType === 'function') { // We may be able to collect the start and end time of this resource from Performance Observer. const resourceEntries = performance.getEntriesByType('resource'); for (let i = 0; i < resourceEntries.length; i++) { const resourceEntry = resourceEntries[i]; if (resourceEntry.name === src) { start = resourceEntry.startTime; end = start + resourceEntry.duration; // $FlowFixMe[prop-missing] fileSize = (resourceEntry.encodedBodySize: any) || 0; } } } // A representation of the image data itself. // TODO: We could render a little preview in the front end from the resource API. const value: { currentSrc: string, naturalWidth?: number, naturalHeight?: number, fileSize?: number, } = { currentSrc: src, }; if (instance.naturalWidth > 0 && instance.naturalHeight > 0) { // The intrinsic size of the file value itself, if it's loaded value.naturalWidth = instance.naturalWidth; value.naturalHeight = instance.naturalHeight; } if (fileSize > 0) { // Cross-origin images won't have a file size that we can access. value.fileSize = fileSize; } const promise = Promise.resolve(value); (promise: any).status = 'fulfilled'; (promise: any).value = value; const ioInfo: ReactIOInfo = { name: 'img', start, end, value: promise, // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file. owner: fiber, // Allow linking to the if it's not filtered. }; const asyncInfo: ReactAsyncInfo = { awaited: ioInfo, // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file. owner: fiber._debugOwner == null ? null : fiber._debugOwner, debugStack: fiber._debugStack == null ? null : fiber._debugStack, debugTask: fiber._debugTask == null ? null : fiber._debugTask, }; insertSuspendedBy(asyncInfo); } function trackThrownPromisesFromRetryCache( suspenseNode: SuspenseNode, retryCache: ?WeakSet, ): void { if (retryCache != null) { // If a Suspense boundary ever committed in fallback state with a retryCache, that // suggests that something unique to that boundary was suspensey since otherwise // it wouldn't have thrown and so never created the retryCache. // Unfortunately if we don't have any DEV time debug info or debug thenables then // we have no meta data to show. However, we still mark this Suspense boundary as // participating in the loading sequence since apparently it can suspend. suspenseNode.hasUniqueSuspenders = true; // We have not seen any reason yet for why this suspense node might have been // suspended but it clearly has been at some point. If we later discover a reason // we'll clear this flag again. suspenseNode.hasUnknownSuspenders = true; } } function mountVirtualChildrenRecursively( firstChild: Fiber, lastChild: null | Fiber, // non-inclusive traceNearestHostComponentUpdate: boolean, virtualLevel: number, // the nth level of virtual instances ): void { // Iterate over siblings rather than recursing. // This reduces the chance of stack overflow for wide trees (e.g. lists with many items). let fiber: Fiber | null = firstChild; let previousVirtualInstance: null | VirtualInstance = null; let previousVirtualInstanceFirstFiber: Fiber = firstChild; while (fiber !== null && fiber !== lastChild) { let level = 0; if (fiber._debugInfo) { for (let i = 0; i < fiber._debugInfo.length; i++) { const debugEntry = fiber._debugInfo[i]; if (debugEntry.awaited) { // Async Info const asyncInfo: ReactAsyncInfo = (debugEntry: any); if (level === virtualLevel) { // Track any async info between the previous virtual instance up until to this // instance and add it to the parent. This can add the same set multiple times // so we assume insertSuspendedBy dedupes. insertSuspendedBy(asyncInfo); } continue; } if (typeof debugEntry.name !== 'string') { // Not a Component. Some other Debug Info. continue; } // Scan up until the next Component to see if this component changed environment. const componentInfo: ReactComponentInfo = (debugEntry: any); const secondaryEnv = getSecondaryEnvironmentName(fiber._debugInfo, i); if (componentInfo.env != null) { knownEnvironmentNames.add(componentInfo.env); } if (secondaryEnv !== null) { knownEnvironmentNames.add(secondaryEnv); } if (shouldFilterVirtual(componentInfo, secondaryEnv)) { // Skip. continue; } if (level === virtualLevel) { if ( previousVirtualInstance === null || // Consecutive children with the same debug entry as a parent gets // treated as if they share the same virtual instance. previousVirtualInstance.data !== debugEntry ) { if (previousVirtualInstance !== null) { // Mount any previous children that should go into the previous parent. mountVirtualInstanceRecursively( previousVirtualInstance, previousVirtualInstanceFirstFiber, fiber, traceNearestHostComponentUpdate, virtualLevel, ); } previousVirtualInstance = createVirtualInstance(componentInfo); recordVirtualMount( previousVirtualInstance, reconcilingParent, secondaryEnv, ); insertChild(previousVirtualInstance); previousVirtualInstanceFirstFiber = fiber; } level++; break; } else { level++; } } } if (level === virtualLevel) { if (previousVirtualInstance !== null) { // If we were working on a virtual instance and this is not a virtual // instance, then we end the sequence and mount any previous children // that should go into the previous virtual instance. mountVirtualInstanceRecursively( previousVirtualInstance, previousVirtualInstanceFirstFiber, fiber, traceNearestHostComponentUpdate, virtualLevel, ); previousVirtualInstance = null; } // We've reached the end of the virtual levels, but not beyond, // and now continue with the regular fiber. mountFiberRecursively(fiber, traceNearestHostComponentUpdate); } fiber = fiber.sibling; } if (previousVirtualInstance !== null) { // Mount any previous children that should go into the previous parent. mountVirtualInstanceRecursively( previousVirtualInstance, previousVirtualInstanceFirstFiber, null, traceNearestHostComponentUpdate, virtualLevel, ); } } function mountChildrenRecursively( firstChild: Fiber, traceNearestHostComponentUpdate: boolean, ): void { mountVirtualChildrenRecursively( firstChild, null, traceNearestHostComponentUpdate, 0, // first level ); } function mountSuspenseChildrenRecursively( contentFiber: Fiber, traceNearestHostComponentUpdate: boolean, stashedSuspenseParent: SuspenseNode | null, stashedSuspensePrevious: SuspenseNode | null, stashedSuspenseRemaining: SuspenseNode | null, ) { const fallbackFiber = contentFiber.sibling; // First update only the Offscreen boundary. I.e. the main content. mountVirtualChildrenRecursively( contentFiber, fallbackFiber, traceNearestHostComponentUpdate, 0, // first level ); if (fallbackFiber !== null) { const fallbackStashedSuspenseParent = stashedSuspenseParent; const fallbackStashedSuspensePrevious = stashedSuspensePrevious; const fallbackStashedSuspenseRemaining = stashedSuspenseRemaining; // Next, we'll pop back out of the SuspenseNode that we added above and now we'll // reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode. // Since the fallback conceptually blocks the parent. reconcilingParentSuspenseNode = stashedSuspenseParent; previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; try { mountVirtualChildrenRecursively( fallbackFiber, null, traceNearestHostComponentUpdate, 0, // first level ); } finally { reconcilingParentSuspenseNode = fallbackStashedSuspenseParent; previouslyReconciledSiblingSuspenseNode = fallbackStashedSuspensePrevious; remainingReconcilingChildrenSuspenseNodes = fallbackStashedSuspenseRemaining; } } } function mountFiberRecursively( fiber: Fiber, traceNearestHostComponentUpdate: boolean, ): void { const shouldIncludeInTree = !shouldFilterFiber(fiber); let newInstance = null; let newSuspenseNode = null; if (shouldIncludeInTree) { newInstance = recordMount(fiber, reconcilingParent); if (fiber.tag === SuspenseComponent || fiber.tag === HostRoot) { newSuspenseNode = createSuspenseNode(newInstance); // Measure this Suspense node. In general we shouldn't do this until we have // inserted the new children but since we know this is a FiberInstance we'll // just use the Fiber anyway. // Fallbacks get attributed to the parent so we only measure if we're // showing primary content. if (OffscreenComponent === -1) { const isTimedOut = fiber.memoizedState !== null; if (!isTimedOut) { newSuspenseNode.rects = measureInstance(newInstance); } } else { const hydrated = isFiberHydrated(fiber); if (hydrated) { const contentFiber = fiber.child; if (contentFiber === null) { throw new Error( 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', ); } } else { // This Suspense Fiber is still dehydrated. It won't have any children // until hydration. } const isTimedOut = fiber.memoizedState !== null; if (!isTimedOut) { newSuspenseNode.rects = measureInstance(newInstance); } } recordSuspenseMount(newSuspenseNode, reconcilingParentSuspenseNode); } insertChild(newInstance); if (__DEBUG__) { debug('mountFiberRecursively()', newInstance, reconcilingParent); } } else if ( (reconcilingParent !== null && reconcilingParent.kind === VIRTUAL_INSTANCE) || fiber.tag === SuspenseComponent || fiber.tag === OffscreenComponent // Use to keep resuspended instances alive inside a SuspenseComponent. ) { // If the parent is a Virtual Instance and we filtered this Fiber we include a // hidden node. We also include this if it's a Suspense boundary so we can track those // in the Suspense tree. if ( reconcilingParent !== null && reconcilingParent.kind === VIRTUAL_INSTANCE && reconcilingParent.data === fiber._debugOwner && fiber._debugStack != null && reconcilingParent.source === null ) { // The new Fiber is directly owned by the parent. Therefore somewhere on the // debugStack will be a stack frame inside parent that we can use as its soruce. reconcilingParent.source = fiber._debugStack; } newInstance = createFilteredFiberInstance(fiber); if (fiber.tag === SuspenseComponent) { newSuspenseNode = createSuspenseNode(newInstance); // Measure this Suspense node. In general we shouldn't do this until we have // inserted the new children but since we know this is a FiberInstance we'll // just use the Fiber anyway. // Fallbacks get attributed to the parent so we only measure if we're // showing primary content. if (OffscreenComponent === -1) { const isTimedOut = fiber.memoizedState !== null; if (!isTimedOut) { newSuspenseNode.rects = measureInstance(newInstance); } } else { const hydrated = isFiberHydrated(fiber); if (hydrated) { const contentFiber = fiber.child; if (contentFiber === null) { throw new Error( 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', ); } } else { // This Suspense Fiber is still dehydrated. It won't have any children // until hydration. } const suspenseState = fiber.memoizedState; const isTimedOut = suspenseState !== null; if (!isTimedOut) { newSuspenseNode.rects = measureInstance(newInstance); } } } insertChild(newInstance); if (__DEBUG__) { debug('mountFiberRecursively()', newInstance, reconcilingParent); } } // If we have the tree selection from previous reload, try to match this Fiber. // Also remember whether to do the same for siblings. const mightSiblingsBeOnTrackedPath = updateTrackedPathStateBeforeMount( fiber, newInstance, ); const stashedParent = reconcilingParent; const stashedPrevious = previouslyReconciledSibling; const stashedRemaining = remainingReconcilingChildren; const stashedSuspenseParent = reconcilingParentSuspenseNode; const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; if (newInstance !== null) { // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = newInstance; previouslyReconciledSibling = null; remainingReconcilingChildren = null; } let shouldPopSuspenseNode = false; if (newSuspenseNode !== null) { reconcilingParentSuspenseNode = newSuspenseNode; previouslyReconciledSiblingSuspenseNode = null; remainingReconcilingChildrenSuspenseNodes = null; shouldPopSuspenseNode = true; } try { if (traceUpdatesEnabled) { if (traceNearestHostComponentUpdate) { const elementType = getElementTypeForFiber(fiber); // If an ancestor updated, we should mark the nearest host nodes for highlighting. if (elementType === ElementTypeHostComponent) { traceUpdatesForNodes.add(fiber.stateNode); traceNearestHostComponentUpdate = false; } } // We intentionally do not re-enable the traceNearestHostComponentUpdate flag in this branch, // because we don't want to highlight every host node inside of a newly mounted subtree. } trackDebugInfoFromLazyType(fiber); trackDebugInfoFromUsedThenables(fiber); if (fiber.tag === HostHoistable) { const nearestInstance = reconcilingParent; if (nearestInstance === null) { throw new Error('Did not expect a host hoistable to be the root'); } aquireHostResource(nearestInstance, fiber.memoizedState); trackDebugInfoFromHostResource(nearestInstance, fiber); } else if ( fiber.tag === HostComponent || fiber.tag === HostText || fiber.tag === HostSingleton ) { const nearestInstance = reconcilingParent; if (nearestInstance === null) { throw new Error('Did not expect a host hoistable to be the root'); } aquireHostInstance(nearestInstance, fiber.stateNode); trackDebugInfoFromHostComponent(nearestInstance, fiber); } if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) { // If an Offscreen component is hidden, mount its children as disconnected. const stashedDisconnected = isInDisconnectedSubtree; isInDisconnectedSubtree = true; try { if (fiber.child !== null) { mountChildrenRecursively(fiber.child, false); } } finally { isInDisconnectedSubtree = stashedDisconnected; } } else if (fiber.tag === SuspenseComponent && OffscreenComponent === -1) { // Legacy Suspense without the Offscreen wrapper. For the modern Suspense we just handle the // Offscreen wrapper itself specially. if (newSuspenseNode !== null) { trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode); } const isTimedOut = fiber.memoizedState !== null; if (isTimedOut) { // Special case: if Suspense mounts in a timed-out state, // get the fallback child from the inner fragment and mount // it as if it was our own child. Updates handle this too. const primaryChildFragment = fiber.child; const fallbackChildFragment = primaryChildFragment ? primaryChildFragment.sibling : null; if (fallbackChildFragment) { const fallbackChild = fallbackChildFragment.child; if (fallbackChild !== null) { updateTrackedPathStateBeforeMount(fallbackChildFragment, null); mountChildrenRecursively( fallbackChild, traceNearestHostComponentUpdate, ); } } // TODO: Track SuspenseNode in resuspended trees. } else { const primaryChild: Fiber | null = fiber.child; if (primaryChild !== null) { mountChildrenRecursively( primaryChild, traceNearestHostComponentUpdate, ); } } } else if ( fiber.tag === SuspenseComponent && OffscreenComponent !== -1 && newInstance !== null && newSuspenseNode !== null ) { // Modern Suspense path const contentFiber = fiber.child; const hydrated = isFiberHydrated(fiber); if (hydrated) { if (contentFiber === null) { throw new Error( 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', ); } trackThrownPromisesFromRetryCache(newSuspenseNode, fiber.stateNode); mountSuspenseChildrenRecursively( contentFiber, traceNearestHostComponentUpdate, stashedSuspenseParent, stashedSuspensePrevious, stashedSuspenseRemaining, ); } else { // This Suspense Fiber is still dehydrated. It won't have any children // until hydration. } } else { if (fiber.child !== null) { mountChildrenRecursively( fiber.child, traceNearestHostComponentUpdate, ); } } } finally { if (newInstance !== null) { reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; remainingReconcilingChildren = stashedRemaining; } if (shouldPopSuspenseNode) { reconcilingParentSuspenseNode = stashedSuspenseParent; previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; } } // We're exiting this Fiber now, and entering its siblings. // If we have selection to restore, we might need to re-activate tracking. updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath); } // We use this to simulate unmounting for Suspense trees // when we switch from primary to fallback, or deleting a subtree. function unmountInstanceRecursively(instance: DevToolsInstance) { if (__DEBUG__) { debug('unmountInstanceRecursively()', instance, reconcilingParent); } const stashedParent = reconcilingParent; const stashedPrevious = previouslyReconciledSibling; const stashedRemaining = remainingReconcilingChildren; const stashedSuspenseParent = reconcilingParentSuspenseNode; const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; const previousSuspendedBy = instance.suspendedBy; // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = instance; previouslyReconciledSibling = null; // Move all the children of this instance to the remaining set. remainingReconcilingChildren = instance.firstChild; instance.firstChild = null; instance.suspendedBy = null; if (instance.suspenseNode !== null) { reconcilingParentSuspenseNode = instance.suspenseNode; previouslyReconciledSiblingSuspenseNode = null; remainingReconcilingChildrenSuspenseNodes = null; } try { // Unmount the remaining set. unmountRemainingChildren(); removePreviousSuspendedBy(instance, previousSuspendedBy); } finally { reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; remainingReconcilingChildren = stashedRemaining; if (instance.suspenseNode !== null) { reconcilingParentSuspenseNode = stashedSuspenseParent; previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; } } if (instance.kind === FIBER_INSTANCE) { recordUnmount(instance); } else if (instance.kind === VIRTUAL_INSTANCE) { recordVirtualUnmount(instance); } else { untrackFiber(instance, instance.data); } removeChild(instance, null); } function recordProfilingDurations( fiberInstance: FiberInstance, prevFiber: null | Fiber, ) { const id = fiberInstance.id; const fiber = fiberInstance.data; const {actualDuration, treeBaseDuration} = fiber; fiberInstance.treeBaseDuration = treeBaseDuration || 0; if (isProfiling) { // It's important to update treeBaseDuration even if the current Fiber did not render, // because it's possible that one of its descendants did. if ( prevFiber == null || treeBaseDuration !== prevFiber.treeBaseDuration ) { // Tree base duration updates are included in the operations typed array. // So we have to convert them from milliseconds to microseconds so we can send them as ints. const convertedTreeBaseDuration = Math.floor( (treeBaseDuration || 0) * 1000, ); pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION); pushOperation(id); pushOperation(convertedTreeBaseDuration); } if (prevFiber == null || didFiberRender(prevFiber, fiber)) { if (actualDuration != null) { // The actual duration reported by React includes time spent working on children. // This is useful information, but it's also useful to be able to exclude child durations. // The frontend can't compute this, since the immediate children may have been filtered out. // So we need to do this on the backend. // Note that this calculated self duration is not the same thing as the base duration. // The two are calculated differently (tree duration does not accumulate). let selfDuration = actualDuration; let child = fiber.child; while (child !== null) { selfDuration -= child.actualDuration || 0; child = child.sibling; } // If profiling is active, store durations for elements that were rendered during the commit. // Note that we should do this for any fiber we performed work on, regardless of its actualDuration value. // In some cases actualDuration might be 0 for fibers we worked on (particularly if we're using Date.now) // In other cases (e.g. Memo) actualDuration might be greater than 0 even if we "bailed out". const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData); metadata.durations.push(id, actualDuration, selfDuration); metadata.maxActualDuration = Math.max( metadata.maxActualDuration, actualDuration, ); if (recordChangeDescriptions) { const changeDescription = getChangeDescription(prevFiber, fiber); if (changeDescription !== null) { if (metadata.changeDescriptions !== null) { metadata.changeDescriptions.set(id, changeDescription); } } } } } // If this Fiber was in the set of memoizedUpdaters we need to record // it to be included in the description of the commit. const fiberRoot: FiberRoot = currentRoot.data.stateNode; const updaters = fiberRoot.memoizedUpdaters; if ( updaters != null && (updaters.has(fiber) || // We check the alternate here because we're matching identity and // prevFiber might be same as fiber. (fiber.alternate !== null && updaters.has(fiber.alternate))) ) { const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData); if (metadata.updaters === null) { metadata.updaters = []; } metadata.updaters.push(instanceToSerializedElement(fiberInstance)); } } } function recordVirtualProfilingDurations(virtualInstance: VirtualInstance) { const id = virtualInstance.id; let treeBaseDuration = 0; // Add up the base duration of the child instances. The virtual base duration // will be the same as children's duration since we don't take up any render // time in the virtual instance. for ( let child = virtualInstance.firstChild; child !== null; child = child.nextSibling ) { treeBaseDuration += child.treeBaseDuration; } if (isProfiling) { const previousTreeBaseDuration = virtualInstance.treeBaseDuration; if (treeBaseDuration !== previousTreeBaseDuration) { // Tree base duration updates are included in the operations typed array. // So we have to convert them from milliseconds to microseconds so we can send them as ints. const convertedTreeBaseDuration = Math.floor( (treeBaseDuration || 0) * 1000, ); pushOperation(TREE_OPERATION_UPDATE_TREE_BASE_DURATION); pushOperation(id); pushOperation(convertedTreeBaseDuration); } } virtualInstance.treeBaseDuration = treeBaseDuration; } function addUnfilteredChildrenIDs( parentInstance: DevToolsInstance, nextChildren: Array, ): void { let child: null | DevToolsInstance = parentInstance.firstChild; while (child !== null) { if (child.kind === FILTERED_FIBER_INSTANCE) { const fiber = child.data; if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) { // The children of this Offscreen are hidden so they don't get added. } else { addUnfilteredChildrenIDs(child, nextChildren); } } else { nextChildren.push(child.id); } child = child.nextSibling; } } function recordResetChildren( parentInstance: FiberInstance | VirtualInstance, ) { if (__DEBUG__) { if (parentInstance.firstChild !== null) { debug( 'recordResetChildren()', parentInstance.firstChild, parentInstance, ); } } // The frontend only really cares about the displayName, key, and children. // The first two don't really change, so we are only concerned with the order of children here. // This is trickier than a simple comparison though, since certain types of fibers are filtered. const nextChildren: Array = []; addUnfilteredChildrenIDs(parentInstance, nextChildren); const numChildren = nextChildren.length; if (numChildren < 2) { // No need to reorder. return; } pushOperation(TREE_OPERATION_REORDER_CHILDREN); pushOperation(parentInstance.id); pushOperation(numChildren); for (let i = 0; i < nextChildren.length; i++) { pushOperation(nextChildren[i]); } } function addUnfilteredSuspenseChildrenIDs( parentInstance: SuspenseNode, nextChildren: Array, ): void { let child: null | SuspenseNode = parentInstance.firstChild; while (child !== null) { if (child.instance.kind === FILTERED_FIBER_INSTANCE) { addUnfilteredSuspenseChildrenIDs(child, nextChildren); } else { nextChildren.push(child.instance.id); } child = child.nextSibling; } } function recordResetSuspenseChildren(parentInstance: SuspenseNode) { if (__DEBUG__) { if (parentInstance.firstChild !== null) { console.log( 'recordResetSuspenseChildren()', parentInstance.firstChild, parentInstance, ); } } // The frontend only really cares about the name, and children. // The first two don't really change, so we are only concerned with the order of children here. // This is trickier than a simple comparison though, since certain types of fibers are filtered. const nextChildren: Array = []; addUnfilteredSuspenseChildrenIDs(parentInstance, nextChildren); const numChildren = nextChildren.length; if (numChildren < 2) { // No need to reorder. return; } pushOperation(SUSPENSE_TREE_OPERATION_REORDER_CHILDREN); // $FlowFixMe[incompatible-call] TODO: Allow filtering SuspenseNode pushOperation(parentInstance.instance.id); pushOperation(numChildren); for (let i = 0; i < nextChildren.length; i++) { pushOperation(nextChildren[i]); } } const NoUpdate = /* */ 0b00; const ShouldResetChildren = /* */ 0b01; const ShouldResetSuspenseChildren = /* */ 0b10; function updateVirtualInstanceRecursively( virtualInstance: VirtualInstance, nextFirstChild: Fiber, nextLastChild: null | Fiber, // non-inclusive prevFirstChild: null | Fiber, traceNearestHostComponentUpdate: boolean, virtualLevel: number, // the nth level of virtual instances ): number { const stashedParent = reconcilingParent; const stashedPrevious = previouslyReconciledSibling; const stashedRemaining = remainingReconcilingChildren; const previousSuspendedBy = virtualInstance.suspendedBy; // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = virtualInstance; previouslyReconciledSibling = null; // Move all the children of this instance to the remaining set. // We'll move them back one by one, and anything that remains is deleted. remainingReconcilingChildren = virtualInstance.firstChild; virtualInstance.firstChild = null; virtualInstance.suspendedBy = null; try { let updateFlags = updateVirtualChildrenRecursively( nextFirstChild, nextLastChild, prevFirstChild, traceNearestHostComponentUpdate, virtualLevel + 1, ); if ((updateFlags & ShouldResetChildren) !== NoUpdate) { recordResetChildren(virtualInstance); updateFlags &= ~ShouldResetChildren; } removePreviousSuspendedBy(virtualInstance, previousSuspendedBy); // Update the errors/warnings count. If this Instance has switched to a different // ReactComponentInfo instance, such as when refreshing Server Components, then // we replace all the previous logs with the ones associated with the new ones rather // than merging. Because deduping is expected to happen at the request level. const componentLogsEntry = componentInfoToComponentLogsMap.get( virtualInstance.data, ); recordConsoleLogs(virtualInstance, componentLogsEntry); // Must be called after all children have been appended. recordVirtualProfilingDurations(virtualInstance); return updateFlags; } finally { unmountRemainingChildren(); reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; remainingReconcilingChildren = stashedRemaining; } } function updateVirtualChildrenRecursively( nextFirstChild: Fiber, nextLastChild: null | Fiber, // non-inclusive prevFirstChild: null | Fiber, traceNearestHostComponentUpdate: boolean, virtualLevel: number, // the nth level of virtual instances ): number { let updateFlags = NoUpdate; // If the first child is different, we need to traverse them. // Each next child will be either a new child (mount) or an alternate (update). let nextChild: null | Fiber = nextFirstChild; let prevChildAtSameIndex = prevFirstChild; let previousVirtualInstance: null | VirtualInstance = null; let previousVirtualInstanceWasMount: boolean = false; let previousVirtualInstanceNextFirstFiber: Fiber = nextFirstChild; let previousVirtualInstancePrevFirstFiber: null | Fiber = prevFirstChild; while (nextChild !== null && nextChild !== nextLastChild) { let level = 0; if (nextChild._debugInfo) { for (let i = 0; i < nextChild._debugInfo.length; i++) { const debugEntry = nextChild._debugInfo[i]; if (debugEntry.awaited) { // Async Info const asyncInfo: ReactAsyncInfo = (debugEntry: any); if (level === virtualLevel) { // Track any async info between the previous virtual instance up until to this // instance and add it to the parent. This can add the same set multiple times // so we assume insertSuspendedBy dedupes. insertSuspendedBy(asyncInfo); } continue; } if (typeof debugEntry.name !== 'string') { // Not a Component. Some other Debug Info. continue; } const componentInfo: ReactComponentInfo = (debugEntry: any); const secondaryEnv = getSecondaryEnvironmentName( nextChild._debugInfo, i, ); if (componentInfo.env != null) { knownEnvironmentNames.add(componentInfo.env); } if (secondaryEnv !== null) { knownEnvironmentNames.add(secondaryEnv); } if (shouldFilterVirtual(componentInfo, secondaryEnv)) { continue; } if (level === virtualLevel) { if ( previousVirtualInstance === null || // Consecutive children with the same debug entry as a parent gets // treated as if they share the same virtual instance. previousVirtualInstance.data !== componentInfo ) { if (previousVirtualInstance !== null) { // Mount any previous children that should go into the previous parent. if (previousVirtualInstanceWasMount) { mountVirtualInstanceRecursively( previousVirtualInstance, previousVirtualInstanceNextFirstFiber, nextChild, traceNearestHostComponentUpdate, virtualLevel, ); updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } else { updateFlags |= updateVirtualInstanceRecursively( previousVirtualInstance, previousVirtualInstanceNextFirstFiber, nextChild, previousVirtualInstancePrevFirstFiber, traceNearestHostComponentUpdate, virtualLevel, ); } } let previousSiblingOfBestMatch = null; let bestMatch = remainingReconcilingChildren; if (componentInfo.key != null) { // If there is a key try to find a matching key in the set. bestMatch = remainingReconcilingChildren; while (bestMatch !== null) { if ( bestMatch.kind === VIRTUAL_INSTANCE && bestMatch.data.key === componentInfo.key ) { break; } previousSiblingOfBestMatch = bestMatch; bestMatch = bestMatch.nextSibling; } } if ( bestMatch !== null && bestMatch.kind === VIRTUAL_INSTANCE && bestMatch.data.name === componentInfo.name && bestMatch.data.env === componentInfo.env && bestMatch.data.key === componentInfo.key ) { // If the previous children had a virtual instance in the same slot // with the same name, then we claim it and reuse it for this update. // Update it with the latest entry. bestMatch.data = componentInfo; moveChild(bestMatch, previousSiblingOfBestMatch); previousVirtualInstance = bestMatch; previousVirtualInstanceWasMount = false; } else { // Otherwise we create a new instance. const newVirtualInstance = createVirtualInstance(componentInfo); recordVirtualMount( newVirtualInstance, reconcilingParent, secondaryEnv, ); insertChild(newVirtualInstance); previousVirtualInstance = newVirtualInstance; previousVirtualInstanceWasMount = true; updateFlags |= ShouldResetChildren; } // Existing children might be reparented into this new virtual instance. // TODO: This will cause the front end to error which needs to be fixed. previousVirtualInstanceNextFirstFiber = nextChild; previousVirtualInstancePrevFirstFiber = prevChildAtSameIndex; } level++; break; } else { level++; } } } if (level === virtualLevel) { if (previousVirtualInstance !== null) { // If we were working on a virtual instance and this is not a virtual // instance, then we end the sequence and update any previous children // that should go into the previous virtual instance. if (previousVirtualInstanceWasMount) { mountVirtualInstanceRecursively( previousVirtualInstance, previousVirtualInstanceNextFirstFiber, nextChild, traceNearestHostComponentUpdate, virtualLevel, ); updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } else { updateFlags |= updateVirtualInstanceRecursively( previousVirtualInstance, previousVirtualInstanceNextFirstFiber, nextChild, previousVirtualInstancePrevFirstFiber, traceNearestHostComponentUpdate, virtualLevel, ); } previousVirtualInstance = null; } // We've reached the end of the virtual levels, but not beyond, // and now continue with the regular fiber. // Do a fast pass over the remaining children to find the previous instance. // TODO: This doesn't have the best O(n) for a large set of children that are // reordered. Consider using a temporary map if it's not the very next one. let prevChild; if (prevChildAtSameIndex === nextChild) { // This set is unchanged. We're just going through it to place all the // children again. prevChild = nextChild; } else { // We don't actually need to rely on the alternate here. We could also // reconcile against stateNode, key or whatever. Doesn't have to be same // Fiber pair. prevChild = nextChild.alternate; } let previousSiblingOfExistingInstance = null; let existingInstance = null; if (prevChild !== null) { existingInstance = remainingReconcilingChildren; while (existingInstance !== null) { if (existingInstance.data === prevChild) { break; } previousSiblingOfExistingInstance = existingInstance; existingInstance = existingInstance.nextSibling; } } if (existingInstance !== null) { // Common case. Match in the same parent. const fiberInstance: FiberInstance | FilteredFiberInstance = (existingInstance: any); // Only matches if it's a Fiber. // We keep track if the order of the children matches the previous order. // They are always different referentially, but if the instances line up // conceptually we'll want to know that. if (prevChild !== prevChildAtSameIndex) { updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } moveChild(fiberInstance, previousSiblingOfExistingInstance); // If a nested tree child order changed but it can't handle its own // child order invalidation (e.g. because it's filtered out like host nodes), // propagate the need to reset child order upwards to this Fiber. updateFlags |= updateFiberRecursively( fiberInstance, nextChild, (prevChild: any), traceNearestHostComponentUpdate, ); } else if (prevChild !== null && shouldFilterFiber(nextChild)) { // The filtered instance could've reordered. if (prevChild !== prevChildAtSameIndex) { updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } // If this Fiber should be filtered, we need to still update its children. // This relies on an alternate since we don't have an Instance with the previous // child on it. Ideally, the reconciliation wouldn't need previous Fibers that // are filtered from the tree. updateFlags |= updateFiberRecursively( null, nextChild, prevChild, traceNearestHostComponentUpdate, ); } else { // It's possible for a FiberInstance to be reparented when virtual parents // get their sequence split or change structure with the same render result. // In this case we unmount the and remount the FiberInstances. // This might cause us to lose the selection but it's an edge case. // We let the previous instance remain in the "remaining queue" it is // in to be deleted at the end since it'll have no match. mountFiberRecursively(nextChild, traceNearestHostComponentUpdate); // Need to mark the parent set to remount the new instance. updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } } // Try the next child. nextChild = nextChild.sibling; // Advance the pointer in the previous list so that we can // keep comparing if they line up. if ( (updateFlags & ShouldResetChildren) === NoUpdate && prevChildAtSameIndex !== null ) { prevChildAtSameIndex = prevChildAtSameIndex.sibling; } } if (previousVirtualInstance !== null) { if (previousVirtualInstanceWasMount) { mountVirtualInstanceRecursively( previousVirtualInstance, previousVirtualInstanceNextFirstFiber, null, traceNearestHostComponentUpdate, virtualLevel, ); updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } else { updateFlags |= updateVirtualInstanceRecursively( previousVirtualInstance, previousVirtualInstanceNextFirstFiber, null, previousVirtualInstancePrevFirstFiber, traceNearestHostComponentUpdate, virtualLevel, ); } } // If we have no more children, but used to, they don't line up. if (prevChildAtSameIndex !== null) { updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } return updateFlags; } // Returns whether closest unfiltered fiber parent needs to reset its child list. function updateChildrenRecursively( nextFirstChild: null | Fiber, prevFirstChild: null | Fiber, traceNearestHostComponentUpdate: boolean, ): number { if (nextFirstChild === null) { return prevFirstChild !== null ? ShouldResetChildren : NoUpdate; } return updateVirtualChildrenRecursively( nextFirstChild, null, prevFirstChild, traceNearestHostComponentUpdate, 0, ); } function updateSuspenseChildrenRecursively( nextContentFiber: Fiber, prevContentFiber: Fiber, traceNearestHostComponentUpdate: boolean, stashedSuspenseParent: null | SuspenseNode, stashedSuspensePrevious: null | SuspenseNode, stashedSuspenseRemaining: null | SuspenseNode, ): number { let updateFlags = NoUpdate; const prevFallbackFiber = prevContentFiber.sibling; const nextFallbackFiber = nextContentFiber.sibling; // First update only the Offscreen boundary. I.e. the main content. updateFlags |= updateVirtualChildrenRecursively( nextContentFiber, nextFallbackFiber, prevContentFiber, traceNearestHostComponentUpdate, 0, ); if (prevFallbackFiber !== null || nextFallbackFiber !== null) { const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode; const fallbackStashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; const fallbackStashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; // Next, we'll pop back out of the SuspenseNode that we added above and now we'll // reconcile the fallback, reconciling anything in the context of the parent SuspenseNode. // Since the fallback conceptually blocks the parent. reconcilingParentSuspenseNode = stashedSuspenseParent; previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; try { if (nextFallbackFiber === null) { unmountRemainingChildren(); } else { updateFlags |= updateVirtualChildrenRecursively( nextFallbackFiber, null, prevFallbackFiber, traceNearestHostComponentUpdate, 0, ); } } finally { reconcilingParentSuspenseNode = fallbackStashedSuspenseParent; previouslyReconciledSiblingSuspenseNode = fallbackStashedSuspensePrevious; remainingReconcilingChildrenSuspenseNodes = fallbackStashedSuspenseRemaining; } } return updateFlags; } // Returns whether closest unfiltered fiber parent needs to reset its child list. function updateFiberRecursively( fiberInstance: null | FiberInstance | FilteredFiberInstance, // null if this should be filtered nextFiber: Fiber, prevFiber: Fiber, traceNearestHostComponentUpdate: boolean, ): number { if (__DEBUG__) { if (fiberInstance !== null) { debug('updateFiberRecursively()', fiberInstance, reconcilingParent); } } if (traceUpdatesEnabled) { const elementType = getElementTypeForFiber(nextFiber); if (traceNearestHostComponentUpdate) { // If an ancestor updated, we should mark the nearest host nodes for highlighting. if (elementType === ElementTypeHostComponent) { traceUpdatesForNodes.add(nextFiber.stateNode); traceNearestHostComponentUpdate = false; } } else { if ( elementType === ElementTypeFunction || elementType === ElementTypeClass || elementType === ElementTypeContext || elementType === ElementTypeMemo || elementType === ElementTypeForwardRef ) { // Otherwise if this is a traced ancestor, flag for the nearest host descendant(s). traceNearestHostComponentUpdate = didFiberRender( prevFiber, nextFiber, ); } } } const stashedParent = reconcilingParent; const stashedPrevious = previouslyReconciledSibling; const stashedRemaining = remainingReconcilingChildren; const stashedSuspenseParent = reconcilingParentSuspenseNode; const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode; const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes; let shouldMeasureSuspenseNode = false; let previousSuspendedBy = null; if (fiberInstance !== null) { previousSuspendedBy = fiberInstance.suspendedBy; // Update the Fiber so we that we always keep the current Fiber on the data. fiberInstance.data = nextFiber; if ( mostRecentlyInspectedElement !== null && mostRecentlyInspectedElement.id === fiberInstance.id && didFiberRender(prevFiber, nextFiber) ) { // If this Fiber has updated, clear cached inspected data. // If it is inspected again, it may need to be re-run to obtain updated hooks values. hasElementUpdatedSinceLastInspected = true; } // Push a new DevTools instance parent while reconciling this subtree. reconcilingParent = fiberInstance; previouslyReconciledSibling = null; // Move all the children of this instance to the remaining set. // We'll move them back one by one, and anything that remains is deleted. remainingReconcilingChildren = fiberInstance.firstChild; fiberInstance.firstChild = null; fiberInstance.suspendedBy = null; const suspenseNode = fiberInstance.suspenseNode; if (suspenseNode !== null) { reconcilingParentSuspenseNode = suspenseNode; previouslyReconciledSiblingSuspenseNode = null; remainingReconcilingChildrenSuspenseNodes = suspenseNode.firstChild; suspenseNode.firstChild = null; shouldMeasureSuspenseNode = true; } } try { trackDebugInfoFromLazyType(nextFiber); trackDebugInfoFromUsedThenables(nextFiber); if ( nextFiber.tag === HostHoistable && prevFiber.memoizedState !== nextFiber.memoizedState ) { const nearestInstance = reconcilingParent; if (nearestInstance === null) { throw new Error('Did not expect a host hoistable to be the root'); } releaseHostResource(nearestInstance, prevFiber.memoizedState); aquireHostResource(nearestInstance, nextFiber.memoizedState); trackDebugInfoFromHostResource(nearestInstance, nextFiber); } else if ( nextFiber.tag === HostComponent || nextFiber.tag === HostText || nextFiber.tag === HostSingleton ) { const nearestInstance = reconcilingParent; if (nearestInstance === null) { throw new Error('Did not expect a host hoistable to be the root'); } if (prevFiber.stateNode !== nextFiber.stateNode) { // In persistent mode, it's possible for the stateNode to update with // a new clone. In that case we need to release the old one and aquire // new one instead. releaseHostInstance(nearestInstance, prevFiber.stateNode); aquireHostInstance(nearestInstance, nextFiber.stateNode); } trackDebugInfoFromHostComponent(nearestInstance, nextFiber); } let updateFlags = NoUpdate; // The behavior of timed-out legacy Suspense trees is unique. Without the Offscreen wrapper. // Rather than unmount the timed out content (and possibly lose important state), // React re-parents this content within a hidden Fragment while the fallback is showing. // This behavior doesn't need to be observable in the DevTools though. // It might even result in a bad user experience for e.g. node selection in the Elements panel. // The easiest fix is to strip out the intermediate Fragment fibers, // so the Elements panel and Profiler don't need to special case them. // Suspense components only have a non-null memoizedState if they're timed-out. const isLegacySuspense = nextFiber.tag === SuspenseComponent && OffscreenComponent === -1; const prevDidTimeout = isLegacySuspense && prevFiber.memoizedState !== null; const nextDidTimeOut = isLegacySuspense && nextFiber.memoizedState !== null; const isOffscreen = nextFiber.tag === OffscreenComponent; const prevWasHidden = isOffscreen && prevFiber.memoizedState !== null; const nextIsHidden = isOffscreen && nextFiber.memoizedState !== null; if (isLegacySuspense) { if ( fiberInstance !== null && fiberInstance.suspenseNode !== null && (prevFiber.stateNode === null) !== (nextFiber.stateNode === null) ) { trackThrownPromisesFromRetryCache( fiberInstance.suspenseNode, nextFiber.stateNode, ); } } // The logic below is inspired by the code paths in updateSuspenseComponent() // inside ReactFiberBeginWork in the React source code. if (prevDidTimeout && nextDidTimeOut) { // Fallback -> Fallback: // 1. Reconcile fallback set. const nextFiberChild = nextFiber.child; const nextFallbackChildSet = nextFiberChild ? nextFiberChild.sibling : null; // Note: We can't use nextFiber.child.sibling.alternate // because the set is special and alternate may not exist. const prevFiberChild = prevFiber.child; const prevFallbackChildSet = prevFiberChild ? prevFiberChild.sibling : null; if (prevFallbackChildSet == null && nextFallbackChildSet != null) { mountChildrenRecursively( nextFallbackChildSet, traceNearestHostComponentUpdate, ); updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } const childrenUpdateFlags = nextFallbackChildSet != null && prevFallbackChildSet != null ? updateChildrenRecursively( nextFallbackChildSet, prevFallbackChildSet, traceNearestHostComponentUpdate, ) : NoUpdate; updateFlags |= childrenUpdateFlags; } else if (prevDidTimeout && !nextDidTimeOut) { // Fallback -> Primary: // 1. Unmount fallback set // Note: don't emulate fallback unmount because React actually did it. // 2. Mount primary set const nextPrimaryChildSet = nextFiber.child; if (nextPrimaryChildSet !== null) { mountChildrenRecursively( nextPrimaryChildSet, traceNearestHostComponentUpdate, ); updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } } else if (!prevDidTimeout && nextDidTimeOut) { // Primary -> Fallback: // 1. Hide primary set // We simply don't re-add the fallback children and let // unmountRemainingChildren() handle it. // 2. Mount fallback set const nextFiberChild = nextFiber.child; const nextFallbackChildSet = nextFiberChild ? nextFiberChild.sibling : null; if (nextFallbackChildSet != null) { mountChildrenRecursively( nextFallbackChildSet, traceNearestHostComponentUpdate, ); updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } } else if (nextIsHidden) { if (!prevWasHidden) { // We're hiding the children. Disconnect them from the front end but keep state. if (fiberInstance !== null && !isInDisconnectedSubtree) { disconnectChildrenRecursively(remainingReconcilingChildren); } } // Update children inside the hidden tree if they committed with a new updates. const stashedDisconnected = isInDisconnectedSubtree; isInDisconnectedSubtree = true; try { updateFlags |= updateChildrenRecursively( nextFiber.child, prevFiber.child, false, ); } finally { isInDisconnectedSubtree = stashedDisconnected; } } else if (prevWasHidden && !nextIsHidden) { // We're revealing the hidden children. We now need to update them to the latest state. // We do this while still in the disconnected state and then we reconnect the new ones. // This avoids reconnecting things that are about to be removed anyway. const stashedDisconnected = isInDisconnectedSubtree; isInDisconnectedSubtree = true; try { if (nextFiber.child !== null) { updateFlags |= updateChildrenRecursively( nextFiber.child, prevFiber.child, false, ); } // Ensure we unmount any remaining children inside the isInDisconnectedSubtree flag // since they should not trigger real deletions. unmountRemainingChildren(); remainingReconcilingChildren = null; } finally { isInDisconnectedSubtree = stashedDisconnected; } if (fiberInstance !== null && !isInDisconnectedSubtree) { reconnectChildrenRecursively(fiberInstance); // Children may have reordered while they were hidden. updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren; } } else if ( nextFiber.tag === SuspenseComponent && OffscreenComponent !== -1 && fiberInstance !== null && fiberInstance.suspenseNode !== null ) { // Modern Suspense path const suspenseNode = fiberInstance.suspenseNode; const prevContentFiber = prevFiber.child; const nextContentFiber = nextFiber.child; const previousHydrated = isFiberHydrated(prevFiber); const nextHydrated = isFiberHydrated(nextFiber); if (previousHydrated && nextHydrated) { if (nextContentFiber === null || prevContentFiber === null) { throw new Error( 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', ); } if ( (prevFiber.stateNode === null) !== (nextFiber.stateNode === null) ) { trackThrownPromisesFromRetryCache( suspenseNode, nextFiber.stateNode, ); } shouldMeasureSuspenseNode = false; updateFlags |= updateSuspenseChildrenRecursively( nextContentFiber, prevContentFiber, traceNearestHostComponentUpdate, stashedSuspenseParent, stashedSuspensePrevious, stashedSuspenseRemaining, ); if (nextFiber.memoizedState === null) { // Measure this Suspense node in case it changed. We don't update the rect while // we're inside a disconnected subtree nor if we are the Suspense boundary that // is suspended. This lets us keep the rectangle of the displayed content while // we're suspended to visualize the resulting state. shouldMeasureSuspenseNode = !isInDisconnectedSubtree; } } else if (!previousHydrated && nextHydrated) { if (nextContentFiber === null) { throw new Error( 'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.', ); } trackThrownPromisesFromRetryCache(suspenseNode, nextFiber.stateNode); mountSuspenseChildrenRecursively( nextContentFiber, traceNearestHostComponentUpdate, stashedSuspenseParent, stashedSuspensePrevious, stashedSuspenseRemaining, ); } else if (previousHydrated && !nextHydrated) { throw new Error( 'Encountered a dehydrated Suspense boundary that was previously hydrated.', ); } else { // This Suspense Fiber is still dehydrated. It won't have any children // until hydration. } } else { // Common case: Primary -> Primary. // This is the same code path as for non-Suspense fibers. if (nextFiber.child !== prevFiber.child) { updateFlags |= updateChildrenRecursively( nextFiber.child, prevFiber.child, traceNearestHostComponentUpdate, ); } else { // Children are unchanged. if (fiberInstance !== null) { // All the remaining children will be children of this same fiber so we can just reuse them. // I.e. we just restore them by undoing what we did above. fiberInstance.firstChild = remainingReconcilingChildren; remainingReconcilingChildren = null; consumeSuspenseNodesOfExistingInstance(fiberInstance); if (traceUpdatesEnabled) { // If we're tracing updates and we've bailed out before reaching a host node, // we should fall back to recursively marking the nearest host descendants for highlight. if (traceNearestHostComponentUpdate) { const hostInstances = findAllCurrentHostInstances(fiberInstance); hostInstances.forEach(hostInstance => { traceUpdatesForNodes.add(hostInstance); }); } } } else { const childrenUpdateFlags = updateChildrenRecursively( nextFiber.child, prevFiber.child, false, ); // If this fiber is filtered there might be changes to this set elsewhere so we have // to visit each child to place it back in the set. We let the child bail out instead. if ((childrenUpdateFlags & ShouldResetChildren) !== NoUpdate) { throw new Error( 'The children should not have changed if we pass in the same set.', ); } updateFlags |= childrenUpdateFlags; } } } if (fiberInstance !== null) { removePreviousSuspendedBy(fiberInstance, previousSuspendedBy); if (fiberInstance.kind === FIBER_INSTANCE) { let componentLogsEntry = fiberToComponentLogsMap.get( fiberInstance.data, ); if ( componentLogsEntry === undefined && fiberInstance.data.alternate ) { componentLogsEntry = fiberToComponentLogsMap.get( fiberInstance.data.alternate, ); } recordConsoleLogs(fiberInstance, componentLogsEntry); const isProfilingSupported = nextFiber.hasOwnProperty('treeBaseDuration'); if (isProfilingSupported) { recordProfilingDurations(fiberInstance, prevFiber); } } } if ((updateFlags & ShouldResetChildren) !== NoUpdate) { // We need to crawl the subtree for closest non-filtered Fibers // so that we can display them in a flat children set. if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) { recordResetChildren(fiberInstance); // We've handled the child order change for this Fiber. // Since it's included, there's no need to invalidate parent child order. updateFlags &= ~ShouldResetChildren; } else { // Let the closest unfiltered parent Fiber reset its child order instead. } } else { } if ((updateFlags & ShouldResetSuspenseChildren) !== NoUpdate) { if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) { const suspenseNode = fiberInstance.suspenseNode; if (suspenseNode !== null) { recordResetSuspenseChildren(suspenseNode); updateFlags &= ~ShouldResetSuspenseChildren; } } else { // Let the closest unfiltered parent Fiber reset its child order instead. } } return updateFlags; } finally { if (fiberInstance !== null) { unmountRemainingChildren(); reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; remainingReconcilingChildren = stashedRemaining; if (shouldMeasureSuspenseNode) { if ( !isInDisconnectedSubtree && reconcilingParentSuspenseNode !== null ) { // Measure this Suspense node in case it changed. We don't update the rect // while we're inside a disconnected subtree so that we keep the outline // as it was before we hid the parent. const suspenseNode = reconcilingParentSuspenseNode; const prevRects = suspenseNode.rects; const nextRects = measureInstance(fiberInstance); if (!areEqualRects(prevRects, nextRects)) { suspenseNode.rects = nextRects; recordSuspenseResize(suspenseNode); } } } if (fiberInstance.suspenseNode !== null) { reconcilingParentSuspenseNode = stashedSuspenseParent; previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; } } } } function disconnectChildrenRecursively(firstChild: null | DevToolsInstance) { for (let child = firstChild; child !== null; child = child.nextSibling) { if ( (child.kind === FIBER_INSTANCE || child.kind === FILTERED_FIBER_INSTANCE) && child.data.tag === OffscreenComponent && child.data.memoizedState !== null ) { // This instance's children are already disconnected. } else { disconnectChildrenRecursively(child.firstChild); } if (child.kind === FIBER_INSTANCE) { recordDisconnect(child); } else if (child.kind === VIRTUAL_INSTANCE) { recordVirtualDisconnect(child); } } } function reconnectChildrenRecursively(parentInstance: DevToolsInstance) { for ( let child = parentInstance.firstChild; child !== null; child = child.nextSibling ) { if (child.kind === FIBER_INSTANCE) { recordReconnect(child, parentInstance); } else if (child.kind === VIRTUAL_INSTANCE) { const secondaryEnv = null; // TODO: We don't have this data anywhere. We could just stash it somewhere. recordVirtualReconnect(child, parentInstance, secondaryEnv); } if ( (child.kind === FIBER_INSTANCE || child.kind === FILTERED_FIBER_INSTANCE) && child.data.tag === OffscreenComponent && child.data.memoizedState !== null ) { // This instance's children should remain disconnected. } else { reconnectChildrenRecursively(child); } } } function cleanup() { isProfiling = false; } function rootSupportsProfiling(root: any) { if (root.memoizedInteractions != null) { // v16 builds include this field for the scheduler/tracing API. return true; } else if ( root.current != null && root.current.hasOwnProperty('treeBaseDuration') ) { // The scheduler/tracing API was removed in v17 though // so we need to check a non-root Fiber. return true; } else { return false; } } function flushInitialOperations() { const localPendingOperationsQueue = pendingOperationsQueue; pendingOperationsQueue = null; if ( localPendingOperationsQueue !== null && localPendingOperationsQueue.length > 0 ) { // We may have already queued up some operations before the frontend connected // If so, let the frontend know about them. localPendingOperationsQueue.forEach(operations => { hook.emit('operations', operations); }); } else { // Before the traversals, remember to start tracking // our path in case we have selection to restore. if (trackedPath !== null) { mightBeOnTrackedPath = true; } // If we have not been profiling, then we can just walk the tree and build up its current state as-is. hook.getFiberRoots(rendererID).forEach(root => { const current = root.current; const newRoot = createFiberInstance(current); rootToFiberInstanceMap.set(root, newRoot); idToDevToolsInstanceMap.set(newRoot.id, newRoot); currentRoot = newRoot; setRootPseudoKey(currentRoot.id, root.current); // Handle multi-renderer edge-case where only some v16 renderers support profiling. if (isProfiling && rootSupportsProfiling(root)) { // If profiling is active, store commit time and duration. // The frontend may request this information after profiling has stopped. currentCommitProfilingMetadata = { changeDescriptions: recordChangeDescriptions ? new Map() : null, durations: [], commitTime: getCurrentTime() - profilingStartTime, maxActualDuration: 0, priorityLevel: null, updaters: null, effectDuration: null, passiveEffectDuration: null, }; } mountFiberRecursively(root.current, false); flushPendingEvents(root); needsToFlushComponentLogs = false; currentRoot = (null: any); }); } } function handleCommitFiberUnmount(fiber: any) { // This Hook is no longer used. After having shipped DevTools everywhere it is // safe to stop calling it from Fiber. } function handlePostCommitFiberRoot(root: any) { if (isProfiling && rootSupportsProfiling(root)) { if (currentCommitProfilingMetadata !== null) { const {effectDuration, passiveEffectDuration} = getEffectDurations(root); // $FlowFixMe[incompatible-use] found when upgrading Flow currentCommitProfilingMetadata.effectDuration = effectDuration; // $FlowFixMe[incompatible-use] found when upgrading Flow currentCommitProfilingMetadata.passiveEffectDuration = passiveEffectDuration; } } if (needsToFlushComponentLogs) { // We received new logs after commit. I.e. in a passive effect. We need to // traverse the tree to find the affected ones. If we just moved the whole // tree traversal from handleCommitFiberRoot to handlePostCommitFiberRoot // this wouldn't be needed. For now we just brute force check all instances. // This is not that common of a case. bruteForceFlushErrorsAndWarnings(); } } function handleCommitFiberRoot( root: FiberRoot, priorityLevel: void | number, ) { const current = root.current; let prevFiber: null | Fiber = null; let rootInstance = rootToFiberInstanceMap.get(root); if (!rootInstance) { rootInstance = createFiberInstance(current); rootToFiberInstanceMap.set(root, rootInstance); idToDevToolsInstanceMap.set(rootInstance.id, rootInstance); } else { prevFiber = rootInstance.data; } currentRoot = rootInstance; // Before the traversals, remember to start tracking // our path in case we have selection to restore. if (trackedPath !== null) { mightBeOnTrackedPath = true; } if (traceUpdatesEnabled) { traceUpdatesForNodes.clear(); } // Handle multi-renderer edge-case where only some v16 renderers support profiling. const isProfilingSupported = rootSupportsProfiling(root); if (isProfiling && isProfilingSupported) { // If profiling is active, store commit time and duration. // The frontend may request this information after profiling has stopped. currentCommitProfilingMetadata = { changeDescriptions: recordChangeDescriptions ? new Map() : null, durations: [], commitTime: getCurrentTime() - profilingStartTime, maxActualDuration: 0, priorityLevel: priorityLevel == null ? null : formatPriorityLevel(priorityLevel), updaters: null, // Initialize to null; if new enough React version is running, // these values will be read during separate handlePostCommitFiberRoot() call. effectDuration: null, passiveEffectDuration: null, }; } if (prevFiber !== null) { // TODO: relying on this seems a bit fishy. const wasMounted = prevFiber.memoizedState != null && prevFiber.memoizedState.element != null && // A dehydrated root is not considered mounted prevFiber.memoizedState.isDehydrated !== true; const isMounted = current.memoizedState != null && current.memoizedState.element != null && // A dehydrated root is not considered mounted current.memoizedState.isDehydrated !== true; if (!wasMounted && isMounted) { // Mount a new root. setRootPseudoKey(currentRoot.id, current); mountFiberRecursively(current, false); } else if (wasMounted && isMounted) { // Update an existing root. updateFiberRecursively(rootInstance, current, prevFiber, false); } else if (wasMounted && !isMounted) { // Unmount an existing root. unmountInstanceRecursively(rootInstance); removeRootPseudoKey(currentRoot.id); rootToFiberInstanceMap.delete(root); } } else { // Mount a new root. setRootPseudoKey(currentRoot.id, current); mountFiberRecursively(current, false); } if (isProfiling && isProfilingSupported) { if (!shouldBailoutWithPendingOperations()) { const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get( currentRoot.id, ); if (commitProfilingMetadata != null) { commitProfilingMetadata.push( ((currentCommitProfilingMetadata: any): CommitProfilingData), ); } else { ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).set( currentRoot.id, [((currentCommitProfilingMetadata: any): CommitProfilingData)], ); } } } // We're done here. flushPendingEvents(root); needsToFlushComponentLogs = false; if (traceUpdatesEnabled) { hook.emit('traceUpdates', traceUpdatesForNodes); } currentRoot = (null: any); } function getResourceInstance(fiber: Fiber): HostInstance | null { if (fiber.tag === HostHoistable) { const resource = fiber.memoizedState; // Feature Detect a DOM Specific Instance of a Resource if ( typeof resource === 'object' && resource !== null && resource.instance != null ) { return resource.instance; } } return null; } function appendHostInstancesByDevToolsInstance( devtoolsInstance: DevToolsInstance, hostInstances: Array, ) { if (devtoolsInstance.kind !== VIRTUAL_INSTANCE) { const fiber = devtoolsInstance.data; appendHostInstancesByFiber(fiber, hostInstances); return; } // Search the tree for the nearest child Fiber and add all its host instances. // TODO: If the true nearest Fiber is filtered, we might skip it and instead include all // the children below it. In the extreme case, searching the whole tree. for ( let child = devtoolsInstance.firstChild; child !== null; child = child.nextSibling ) { appendHostInstancesByDevToolsInstance(child, hostInstances); } } function appendHostInstancesByFiber( fiber: Fiber, hostInstances: Array, ): void { // Next we'll drill down this component to find all HostComponent/Text. let node: Fiber = fiber; while (true) { if ( node.tag === HostComponent || node.tag === HostText || node.tag === HostSingleton || node.tag === HostHoistable ) { const hostInstance = node.stateNode || getResourceInstance(node); if (hostInstance) { hostInstances.push(hostInstance); } } else if (node.child) { node.child.return = node; node = node.child; continue; } if (node === fiber) { return; } while (!node.sibling) { if (!node.return || node.return === fiber) { return; } node = node.return; } node.sibling.return = node.return; node = node.sibling; } } function findAllCurrentHostInstances( devtoolsInstance: DevToolsInstance, ): $ReadOnlyArray { const hostInstances: Array = []; appendHostInstancesByDevToolsInstance(devtoolsInstance, hostInstances); return hostInstances; } function findHostInstancesForElementID(id: number) { try { const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { console.warn(`Could not find DevToolsInstance with id "${id}"`); return null; } return findAllCurrentHostInstances(devtoolsInstance); } catch (err) { // The fiber might have unmounted by now. return null; } } function getDisplayNameForElementID(id: number): null | string { const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { return null; } if (devtoolsInstance.kind === FIBER_INSTANCE) { return getDisplayNameForFiber(devtoolsInstance.data); } else { return devtoolsInstance.data.name || ''; } } function getNearestSuspenseNode(instance: DevToolsInstance): SuspenseNode { while (instance.suspenseNode === null) { if (instance.parent === null) { throw new Error( 'There should always be a SuspenseNode parent on a mounted instance.', ); } instance = instance.parent; } return instance.suspenseNode; } function getNearestMountedDOMNode(publicInstance: Element): null | Element { let domNode: null | Element = publicInstance; while (domNode && !publicInstanceToDevToolsInstanceMap.has(domNode)) { // $FlowFixMe: In practice this is either null or Element. domNode = domNode.parentNode; } return domNode; } function getElementIDForHostInstance( publicInstance: HostInstance, ): number | null { const instance = publicInstanceToDevToolsInstanceMap.get(publicInstance); if (instance !== undefined) { if (instance.kind === FILTERED_FIBER_INSTANCE) { // A Filtered Fiber Instance will always have a Virtual Instance as a parent. return ((instance.parent: any): VirtualInstance).id; } return instance.id; } return null; } function getElementAttributeByPath( id: number, path: Array, ): mixed { if (isMostRecentlyInspectedElement(id)) { return getInObject( ((mostRecentlyInspectedElement: any): InspectedElement), path, ); } return undefined; } function getElementSourceFunctionById(id: number): null | Function { const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { console.warn(`Could not find DevToolsInstance with id "${id}"`); return null; } if (devtoolsInstance.kind !== FIBER_INSTANCE) { // TODO: Handle VirtualInstance. return null; } const fiber = devtoolsInstance.data; const {elementType, tag, type} = fiber; switch (tag) { case ClassComponent: case IncompleteClassComponent: case IncompleteFunctionComponent: case IndeterminateComponent: case FunctionComponent: return type; case ForwardRef: return type.render; case MemoComponent: case SimpleMemoComponent: return elementType != null && elementType.type != null ? elementType.type : type; default: return null; } } function instanceToSerializedElement( instance: FiberInstance | VirtualInstance, ): SerializedElement { if (instance.kind === FIBER_INSTANCE) { const fiber = instance.data; return { displayName: getDisplayNameForFiber(fiber) || 'Anonymous', id: instance.id, key: fiber.key, env: null, stack: fiber._debugOwner == null || fiber._debugStack == null ? null : parseStackTrace(fiber._debugStack, 1), type: getElementTypeForFiber(fiber), }; } else { const componentInfo = instance.data; return { displayName: componentInfo.name || 'Anonymous', id: instance.id, key: componentInfo.key == null ? null : componentInfo.key, env: componentInfo.env == null ? null : componentInfo.env, stack: componentInfo.owner == null || componentInfo.debugStack == null ? null : parseStackTrace(componentInfo.debugStack, 1), type: ElementTypeVirtual, }; } } function getOwnersList(id: number): Array | null { const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { console.warn(`Could not find DevToolsInstance with id "${id}"`); return null; } const self = instanceToSerializedElement(devtoolsInstance); const owners = getOwnersListFromInstance(devtoolsInstance); // This is particular API is prefixed with the current instance too for some reason. if (owners === null) { return [self]; } owners.unshift(self); owners.reverse(); return owners; } function getOwnersListFromInstance( instance: DevToolsInstance, ): Array | null { let owner = getUnfilteredOwner(instance.data); if (owner === null) { return null; } const owners: Array = []; let parentInstance: null | DevToolsInstance = instance.parent; while (parentInstance !== null && owner !== null) { const ownerInstance = findNearestOwnerInstance(parentInstance, owner); if (ownerInstance !== null) { owners.push(instanceToSerializedElement(ownerInstance)); // Get the next owner and keep searching from the previous match. owner = getUnfilteredOwner(owner); parentInstance = ownerInstance.parent; } else { break; } } return owners; } function getUnfilteredOwner( owner: ReactComponentInfo | Fiber | null | void, ): ReactComponentInfo | Fiber | null { if (owner == null) { return null; } if (typeof owner.tag === 'number') { const ownerFiber: Fiber = (owner: any); // Refined owner = ownerFiber._debugOwner; } else { const ownerInfo: ReactComponentInfo = (owner: any); // Refined owner = ownerInfo.owner; } while (owner) { if (typeof owner.tag === 'number') { const ownerFiber: Fiber = (owner: any); // Refined if (!shouldFilterFiber(ownerFiber)) { return ownerFiber; } owner = ownerFiber._debugOwner; } else { const ownerInfo: ReactComponentInfo = (owner: any); // Refined if (!shouldFilterVirtual(ownerInfo, null)) { return ownerInfo; } owner = ownerInfo.owner; } } return null; } function findNearestOwnerInstance( parentInstance: null | DevToolsInstance, owner: void | null | ReactComponentInfo | Fiber, ): null | FiberInstance | VirtualInstance { if (owner == null) { return null; } // Search the parent path for any instance that matches this kind of owner. while (parentInstance !== null) { if ( parentInstance.data === owner || // Typically both owner and instance.data would refer to the current version of a Fiber // but it is possible for memoization to ignore the owner on the JSX. Then the new Fiber // isn't propagated down as the new owner. In that case we might match the alternate // instead. This is a bit hacky but the fastest check since type casting owner to a Fiber // needs a duck type check anyway. parentInstance.data === (owner: any).alternate ) { if (parentInstance.kind === FILTERED_FIBER_INSTANCE) { return null; } return parentInstance; } parentInstance = parentInstance.parent; } // It is technically possible to create an element and render it in a different parent // but this is a weird edge case and it is worth not having to scan the tree or keep // a register for every fiber/component info. return null; } function inspectHooks(fiber: Fiber): HooksTree { const originalConsoleMethods: {[string]: $FlowFixMe} = {}; // Temporarily disable all console logging before re-running the hook. for (const method in console) { try { // $FlowFixMe[invalid-computed-prop] originalConsoleMethods[method] = console[method]; // $FlowFixMe[prop-missing] console[method] = () => {}; } catch (error) {} } try { return inspectHooksOfFiber(fiber, getDispatcherRef(renderer)); } finally { // Restore original console functionality. for (const method in originalConsoleMethods) { try { // $FlowFixMe[prop-missing] console[method] = originalConsoleMethods[method]; } catch (error) {} } } } function getSuspendedByOfSuspenseNode( suspenseNode: SuspenseNode, ): Array { // Collect all ReactAsyncInfo that was suspending this SuspenseNode but // isn't also in any parent set. const result: Array = []; if (!suspenseNode.hasUniqueSuspenders) { return result; } // Cache the inspection of Hooks in case we need it for multiple entries. // We don't need a full map here since it's likely that every ioInfo that's unique // to a specific instance will have those appear in order of when that instance was discovered. let hooksCacheKey: null | DevToolsInstance = null; let hooksCache: null | HooksTree = null; suspenseNode.suspendedBy.forEach((set, ioInfo) => { let parentNode = suspenseNode.parent; while (parentNode !== null) { if (parentNode.suspendedBy.has(ioInfo)) { return; } parentNode = parentNode.parent; } // We have the ioInfo but we need to find at least one corresponding await // to go along with it. We don't really need to show every child that awaits the same // thing so we just pick the first one that is still alive. if (set.size === 0) { return; } const firstInstance: DevToolsInstance = (set.values().next().value: any); if (firstInstance.suspendedBy !== null) { const asyncInfo = getAwaitInSuspendedByFromIO( firstInstance.suspendedBy, ioInfo, ); if (asyncInfo !== null) { let hooks: null | HooksTree = null; if (asyncInfo.stack == null && asyncInfo.owner == null) { if (hooksCacheKey === firstInstance) { hooks = hooksCache; } else if (firstInstance.kind !== VIRTUAL_INSTANCE) { const fiber = firstInstance.data; if ( fiber.dependencies && fiber.dependencies._debugThenableState ) { // This entry had no stack nor owner but this Fiber used Hooks so we might // be able to get the stack from the Hook. hooksCacheKey = firstInstance; hooksCache = hooks = inspectHooks(fiber); } } } result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks)); } } }); return result; } const FALLBACK_THROTTLE_MS: number = 300; function getSuspendedByRange( suspenseNode: SuspenseNode, ): null | [number, number] { let min = Infinity; let max = -Infinity; suspenseNode.suspendedBy.forEach((_, ioInfo) => { if (ioInfo.end > max) { max = ioInfo.end; } if (ioInfo.start < min) { min = ioInfo.start; } }); const parentSuspenseNode = suspenseNode.parent; if (parentSuspenseNode !== null) { let parentMax = -Infinity; parentSuspenseNode.suspendedBy.forEach((_, ioInfo) => { if (ioInfo.end > parentMax) { parentMax = ioInfo.end; } }); // The parent max is theoretically the earlier the parent could've committed. // Therefore, the theoretical max that the child could be throttled is that plus 300ms. const throttleTime = parentMax + FALLBACK_THROTTLE_MS; if (throttleTime > max) { // If the theoretical throttle time is later than the earliest reveal then we extend // the max time to show that this is timespan could possibly get throttled. max = throttleTime; } // We use the end of the previous boundary as the start time for this boundary unless, // that's earlier than we'd need to expand to the full fallback throttle range. It // suggests that the parent was loaded earlier than this one. let startTime = max - FALLBACK_THROTTLE_MS; if (parentMax > startTime) { startTime = parentMax; } // If the first fetch of this boundary starts before that, then we use that as the start. if (startTime < min) { min = startTime; } } if (min < Infinity && max > -Infinity) { return [min, max]; } return null; } function getAwaitStackFromHooks( hooks: HooksTree, asyncInfo: ReactAsyncInfo, ): null | ReactStackTrace { // TODO: We search through the hooks tree generated by inspectHooksOfFiber so that we can // use the information already extracted but ideally this search would be faster since we // could know which index to extract from the debug state. for (let i = 0; i < hooks.length; i++) { const node = hooks[i]; const debugInfo = node.debugInfo; if (debugInfo != null && debugInfo.indexOf(asyncInfo) !== -1) { // Found a matching Hook. We'll now use its source location to construct a stack. const source = node.hookSource; if ( source != null && source.functionName !== null && source.fileName !== null && source.lineNumber !== null && source.columnNumber !== null ) { // Unfortunately this is in a slightly different format. TODO: Unify HookNode with ReactCallSite. const callSite: ReactCallSite = [ source.functionName, source.fileName, source.lineNumber, source.columnNumber, 0, 0, false, ]; // As we return we'll add any custom hooks parent stacks to the array. return [callSite]; } else { return []; } } // Otherwise, search the sub hooks of any custom hook. const matchedStack = getAwaitStackFromHooks(node.subHooks, asyncInfo); if (matchedStack !== null) { // Append this custom hook to the stack trace since it must have been called inside of it. const source = node.hookSource; if ( source != null && source.functionName !== null && source.fileName !== null && source.lineNumber !== null && source.columnNumber !== null ) { // Unfortunately this is in a slightly different format. TODO: Unify HookNode with ReactCallSite. const callSite: ReactCallSite = [ source.functionName, source.fileName, source.lineNumber, source.columnNumber, 0, 0, false, ]; matchedStack.push(callSite); } return matchedStack; } } return null; } function serializeAsyncInfo( asyncInfo: ReactAsyncInfo, parentInstance: DevToolsInstance, hooks: null | HooksTree, ): SerializedAsyncInfo { const ioInfo = asyncInfo.awaited; const ioOwnerInstance = findNearestOwnerInstance( parentInstance, ioInfo.owner, ); let awaitStack = asyncInfo.debugStack == null ? null : // While we have a ReactStackTrace on ioInfo.stack, that will point to the location on // the server. We need a location that points to the virtual source on the client which // we can then use to source map to the original location. parseStackTrace(asyncInfo.debugStack, 1); let awaitOwnerInstance: null | FiberInstance | VirtualInstance; if ( asyncInfo.owner == null && (awaitStack === null || awaitStack.length === 0) ) { // We had no owner nor stack for the await. This can happen if you render it as a child // or throw a Promise. Replace it with the parent as the await. awaitStack = null; awaitOwnerInstance = parentInstance.kind === FILTERED_FIBER_INSTANCE ? null : parentInstance; if ( parentInstance.kind === FIBER_INSTANCE || parentInstance.kind === FILTERED_FIBER_INSTANCE ) { const fiber = parentInstance.data; switch (fiber.tag) { case ClassComponent: case FunctionComponent: case IncompleteClassComponent: case IncompleteFunctionComponent: case IndeterminateComponent: case MemoComponent: case SimpleMemoComponent: // If we awaited in the child position of a component, then the best stack would be the // return callsite but we don't have that available so instead we skip. The callsite of // the JSX would be misleading in this case. The same thing happens with throw-a-Promise. if (hooks !== null) { // If this component used Hooks we might be able to instead infer the stack from the // use() callsite if this async info came from a hook. Let's search the tree to find it. awaitStack = getAwaitStackFromHooks(hooks, asyncInfo); } break; default: // If we awaited by passing a Promise to a built-in element, then the JSX callsite is a // good stack trace to use for the await. if ( fiber._debugOwner != null && fiber._debugStack != null && typeof fiber._debugStack !== 'string' ) { awaitStack = parseStackTrace(fiber._debugStack, 1); awaitOwnerInstance = findNearestOwnerInstance( parentInstance, fiber._debugOwner, ); } } } } else { awaitOwnerInstance = findNearestOwnerInstance( parentInstance, asyncInfo.owner, ); } const value: any = ioInfo.value; let resolvedValue = undefined; if ( typeof value === 'object' && value !== null && typeof value.then === 'function' ) { switch (value.status) { case 'fulfilled': resolvedValue = value.value; break; case 'rejected': resolvedValue = value.reason; break; } } return { awaited: { name: ioInfo.name, description: getIODescription(resolvedValue), start: ioInfo.start, end: ioInfo.end, value: ioInfo.value == null ? null : ioInfo.value, env: ioInfo.env == null ? null : ioInfo.env, owner: ioOwnerInstance === null ? null : instanceToSerializedElement(ioOwnerInstance), stack: ioInfo.debugStack == null ? null : // While we have a ReactStackTrace on ioInfo.stack, that will point to the location on // the server. We need a location that points to the virtual source on the client which // we can then use to source map to the original location. parseStackTrace(ioInfo.debugStack, 1), }, env: asyncInfo.env == null ? null : asyncInfo.env, owner: awaitOwnerInstance === null ? null : instanceToSerializedElement(awaitOwnerInstance), stack: awaitStack, }; } // Fast path props lookup for React Native style editor. // Could use inspectElementRaw() but that would require shallow rendering hooks components, // and could also mess with memoization. function getInstanceAndStyle(id: number): InstanceAndStyle { let instance = null; let style = null; const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { console.warn(`Could not find DevToolsInstance with id "${id}"`); return {instance, style}; } if (devtoolsInstance.kind !== FIBER_INSTANCE) { // TODO: Handle VirtualInstance. return {instance, style}; } const fiber = devtoolsInstance.data; if (fiber !== null) { instance = fiber.stateNode; if (fiber.memoizedProps !== null) { style = fiber.memoizedProps.style; } } return {instance, style}; } function isErrorBoundary(fiber: Fiber): boolean { const {tag, type} = fiber; switch (tag) { case ClassComponent: case IncompleteClassComponent: const instance = fiber.stateNode; return ( typeof type.getDerivedStateFromError === 'function' || (instance !== null && typeof instance.componentDidCatch === 'function') ); default: return false; } } function inspectElementRaw(id: number): InspectedElement | null { const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { console.warn(`Could not find DevToolsInstance with id "${id}"`); return null; } if (devtoolsInstance.kind === VIRTUAL_INSTANCE) { return inspectVirtualInstanceRaw(devtoolsInstance); } if (devtoolsInstance.kind === FIBER_INSTANCE) { return inspectFiberInstanceRaw(devtoolsInstance); } (devtoolsInstance: FilteredFiberInstance); // assert exhaustive throw new Error('Unsupported instance kind'); } function inspectFiberInstanceRaw( fiberInstance: FiberInstance, ): InspectedElement | null { const fiber = fiberInstance.data; if (fiber == null) { return null; } const { stateNode, key, memoizedProps, memoizedState, dependencies, tag, type, } = fiber; const elementType = getElementTypeForFiber(fiber); const usesHooks = (tag === FunctionComponent || tag === SimpleMemoComponent || tag === ForwardRef) && (!!memoizedState || !!dependencies); // TODO Show custom UI for Cache like we do for Suspense // For now, just hide state data entirely since it's not meant to be inspected. const showState = tag === ClassComponent || tag === IncompleteClassComponent; const typeSymbol = getTypeSymbol(type); let canViewSource = false; let context = null; if ( tag === ClassComponent || tag === FunctionComponent || tag === IncompleteClassComponent || tag === IncompleteFunctionComponent || tag === IndeterminateComponent || tag === MemoComponent || tag === ForwardRef || tag === SimpleMemoComponent ) { canViewSource = true; if (stateNode && stateNode.context != null) { // Don't show an empty context object for class components that don't use the context API. const shouldHideContext = elementType === ElementTypeClass && !(type.contextTypes || type.contextType); if (!shouldHideContext) { context = stateNode.context; } } } else if ( // Detect pre-19 Context Consumers (typeSymbol === CONTEXT_NUMBER || typeSymbol === CONTEXT_SYMBOL_STRING) && !( // In 19+, CONTEXT_SYMBOL_STRING means a Provider instead. // It will be handled in a different branch below. // Eventually, this entire branch can be removed. (type._context === undefined && type.Provider === type) ) ) { // 16.3-16.5 read from "type" because the Consumer is the actual context object. // 16.6+ should read from "type._context" because Consumer can be different (in DEV). // NOTE Keep in sync with getDisplayNameForFiber() const consumerResolvedContext = type._context || type; // Global context value. context = consumerResolvedContext._currentValue || null; // Look for overridden value. let current = ((fiber: any): Fiber).return; while (current !== null) { const currentType = current.type; const currentTypeSymbol = getTypeSymbol(currentType); if ( currentTypeSymbol === PROVIDER_NUMBER || currentTypeSymbol === PROVIDER_SYMBOL_STRING ) { // 16.3.0 exposed the context object as "context" // PR #12501 changed it to "_context" for 16.3.1+ // NOTE Keep in sync with getDisplayNameForFiber() const providerResolvedContext = currentType._context || currentType.context; if (providerResolvedContext === consumerResolvedContext) { context = current.memoizedProps.value; break; } } current = current.return; } } else if ( // Detect 19+ Context Consumers typeSymbol === CONSUMER_SYMBOL_STRING ) { // This branch is 19+ only, where Context.Provider === Context. // NOTE Keep in sync with getDisplayNameForFiber() const consumerResolvedContext = type._context; // Global context value. context = consumerResolvedContext._currentValue || null; // Look for overridden value. let current = ((fiber: any): Fiber).return; while (current !== null) { const currentType = current.type; const currentTypeSymbol = getTypeSymbol(currentType); if ( // In 19+, these are Context Providers currentTypeSymbol === CONTEXT_SYMBOL_STRING ) { const providerResolvedContext = currentType; if (providerResolvedContext === consumerResolvedContext) { context = current.memoizedProps.value; break; } } current = current.return; } } let hasLegacyContext = false; if (context !== null) { hasLegacyContext = !!type.contextTypes; // To simplify hydration and display logic for context, wrap in a value object. // Otherwise simple values (e.g. strings, booleans) become harder to handle. context = {value: context}; } const owners: null | Array = getOwnersListFromInstance(fiberInstance); let hooks: null | HooksTree = null; if (usesHooks) { hooks = inspectHooks(fiber); } let rootType = null; let current = fiber; let hasErrorBoundary = false; let hasSuspenseBoundary = false; while (current.return !== null) { const temp = current; current = current.return; if (temp.tag === SuspenseComponent) { hasSuspenseBoundary = true; } else if (isErrorBoundary(temp)) { hasErrorBoundary = true; } } const fiberRoot = current.stateNode; if (fiberRoot != null && fiberRoot._debugRootType !== null) { rootType = fiberRoot._debugRootType; } const isTimedOutSuspense = tag === SuspenseComponent && memoizedState !== null; let isErrored = false; if (isErrorBoundary(fiber)) { // if the current inspected element is an error boundary, // either that we want to use it to toggle off error state // or that we allow to force error state on it if it's within another // error boundary // // TODO: This flag is a leaked implementation detail. Once we start // releasing DevTools in lockstep with React, we should import a function // from the reconciler instead. const DidCapture = 0b000000000000000000010000000; isErrored = (fiber.flags & DidCapture) !== 0 || forceErrorForFibers.get(fiber) === true || (fiber.alternate !== null && forceErrorForFibers.get(fiber.alternate) === true); } const plugins: Plugins = { stylex: null, }; if (enableStyleXFeatures) { if (memoizedProps != null && memoizedProps.hasOwnProperty('xstyle')) { plugins.stylex = getStyleXData(memoizedProps.xstyle); } } let source = null; if (canViewSource) { source = getSourceForFiberInstance(fiberInstance); } let componentLogsEntry = fiberToComponentLogsMap.get(fiber); if (componentLogsEntry === undefined && fiber.alternate !== null) { componentLogsEntry = fiberToComponentLogsMap.get(fiber.alternate); } let nativeTag = null; if (elementType === ElementTypeHostComponent) { nativeTag = getNativeTag(fiber.stateNode); } let isSuspended: boolean | null = null; if (tag === SuspenseComponent) { isSuspended = memoizedState !== null; } const suspendedBy = fiberInstance.suspenseNode !== null ? // If this is a Suspense boundary, then we include everything in the subtree that might suspend // this boundary down to the next Suspense boundary. getSuspendedByOfSuspenseNode(fiberInstance.suspenseNode) : // This set is an edge case where if you pass a promise to a Client Component into a children // position without a Server Component as the direct parent. E.g.
{promise}
// In this case, this becomes associated with the Client/Host Component where as normally // you'd expect these to be associated with the Server Component that awaited the data. // TODO: Prepend other suspense sources like css, images and use(). fiberInstance.suspendedBy === null ? [] : fiberInstance.suspendedBy.map(info => serializeAsyncInfo(info, fiberInstance, hooks), ); const suspendedByRange = getSuspendedByRange( getNearestSuspenseNode(fiberInstance), ); let unknownSuspenders = UNKNOWN_SUSPENDERS_NONE; if ( fiberInstance.suspenseNode !== null && fiberInstance.suspenseNode.hasUnknownSuspenders && !isTimedOutSuspense ) { // Something unknown threw to suspended this boundary. Let's figure out why that might be. if (renderer.bundleType === 0) { unknownSuspenders = UNKNOWN_SUSPENDERS_REASON_PRODUCTION; } else if (!('_debugInfo' in fiber)) { // TODO: We really should detect _debugThenable and the auto-instrumentation for lazy/thenables too. unknownSuspenders = UNKNOWN_SUSPENDERS_REASON_OLD_VERSION; } else { unknownSuspenders = UNKNOWN_SUSPENDERS_REASON_THROWN_PROMISE; } } return { id: fiberInstance.id, // Does the current renderer support editable hooks and function props? canEditHooks: typeof overrideHookState === 'function', canEditFunctionProps: typeof overrideProps === 'function', // Does the current renderer support advanced editing interface? canEditHooksAndDeletePaths: typeof overrideHookStateDeletePath === 'function', canEditHooksAndRenamePaths: typeof overrideHookStateRenamePath === 'function', canEditFunctionPropsDeletePaths: typeof overridePropsDeletePath === 'function', canEditFunctionPropsRenamePaths: typeof overridePropsRenamePath === 'function', canToggleError: supportsTogglingError && hasErrorBoundary, // Is this error boundary in error state. isErrored, canToggleSuspense: supportsTogglingSuspense && hasSuspenseBoundary && // If it's showing the real content, we can always flip fallback. (!isTimedOutSuspense || // If it's showing fallback because we previously forced it to, // allow toggling it back to remove the fallback override. forceFallbackForFibers.has(fiber) || (fiber.alternate !== null && forceFallbackForFibers.has(fiber.alternate))), isSuspended: isSuspended, source, stack: fiber._debugOwner == null || fiber._debugStack == null ? null : parseStackTrace(fiber._debugStack, 1), // Does the component have legacy context attached to it. hasLegacyContext, key: key != null ? key : null, type: elementType, // Inspectable properties. // TODO Review sanitization approach for the below inspectable values. context, hooks, props: memoizedProps, state: showState ? memoizedState : null, errors: componentLogsEntry === undefined ? [] : Array.from(componentLogsEntry.errors.entries()), warnings: componentLogsEntry === undefined ? [] : Array.from(componentLogsEntry.warnings.entries()), suspendedBy: suspendedBy, suspendedByRange: suspendedByRange, unknownSuspenders: unknownSuspenders, // List of owners owners, env: null, rootType, rendererPackageName: renderer.rendererPackageName, rendererVersion: renderer.version, plugins, nativeTag, }; } function inspectVirtualInstanceRaw( virtualInstance: VirtualInstance, ): InspectedElement | null { const source = getSourceForInstance(virtualInstance); const componentInfo = virtualInstance.data; const key = typeof componentInfo.key === 'string' ? componentInfo.key : null; const props = componentInfo.props == null ? null : componentInfo.props; const owners: null | Array = getOwnersListFromInstance(virtualInstance); let rootType = null; let hasErrorBoundary = false; let hasSuspenseBoundary = false; const nearestFiber = getNearestFiber(virtualInstance); if (nearestFiber !== null) { let current = nearestFiber; while (current.return !== null) { const temp = current; current = current.return; if (temp.tag === SuspenseComponent) { hasSuspenseBoundary = true; } else if (isErrorBoundary(temp)) { hasErrorBoundary = true; } } const fiberRoot = current.stateNode; if (fiberRoot != null && fiberRoot._debugRootType !== null) { rootType = fiberRoot._debugRootType; } } const plugins: Plugins = { stylex: null, }; const componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo); const isSuspended = null; // Things that Suspended this Server Component (use(), awaits and direct child promises) const suspendedBy = virtualInstance.suspendedBy; const suspendedByRange = getSuspendedByRange( getNearestSuspenseNode(virtualInstance), ); return { id: virtualInstance.id, canEditHooks: false, canEditFunctionProps: false, canEditHooksAndDeletePaths: false, canEditHooksAndRenamePaths: false, canEditFunctionPropsDeletePaths: false, canEditFunctionPropsRenamePaths: false, canToggleError: supportsTogglingError && hasErrorBoundary, isErrored: false, canToggleSuspense: supportsTogglingSuspense && hasSuspenseBoundary, isSuspended: isSuspended, source, stack: componentInfo.owner == null || componentInfo.debugStack == null ? null : parseStackTrace(componentInfo.debugStack, 1), // Does the component have legacy context attached to it. hasLegacyContext: false, key: key, type: ElementTypeVirtual, // Inspectable properties. // TODO Review sanitization approach for the below inspectable values. context: null, hooks: null, props: props, state: null, errors: componentLogsEntry === undefined ? [] : Array.from(componentLogsEntry.errors.entries()), warnings: componentLogsEntry === undefined ? [] : Array.from(componentLogsEntry.warnings.entries()), suspendedBy: suspendedBy === null ? [] : suspendedBy.map(info => serializeAsyncInfo(info, virtualInstance, null), ), suspendedByRange: suspendedByRange, unknownSuspenders: UNKNOWN_SUSPENDERS_NONE, // List of owners owners, env: componentInfo.env == null ? null : componentInfo.env, rootType, rendererPackageName: renderer.rendererPackageName, rendererVersion: renderer.version, plugins, nativeTag: null, }; } let mostRecentlyInspectedElement: InspectedElement | null = null; let hasElementUpdatedSinceLastInspected: boolean = false; let currentlyInspectedPaths: Object = {}; function isMostRecentlyInspectedElement(id: number): boolean { return ( mostRecentlyInspectedElement !== null && mostRecentlyInspectedElement.id === id ); } function isMostRecentlyInspectedElementCurrent(id: number): boolean { return ( isMostRecentlyInspectedElement(id) && !hasElementUpdatedSinceLastInspected ); } // Track the intersection of currently inspected paths, // so that we can send their data along if the element is re-rendered. function mergeInspectedPaths(path: Array) { let current = currentlyInspectedPaths; path.forEach(key => { if (!current[key]) { current[key] = {}; } current = current[key]; }); } function createIsPathAllowed( key: string | null, secondaryCategory: 'suspendedBy' | 'hooks' | null, ) { // This function helps prevent previously-inspected paths from being dehydrated in updates. // This is important to avoid a bad user experience where expanded toggles collapse on update. return function isPathAllowed(path: Array): boolean { switch (secondaryCategory) { case 'hooks': if (path.length === 1) { // Never dehydrate the "hooks" object at the top levels. return true; } if ( path[path.length - 2] === 'hookSource' && path[path.length - 1] === 'fileName' ) { // It's important to preserve the full file name (URL) for hook sources // in case the user has enabled the named hooks feature. // Otherwise the frontend may end up with a partial URL which it can't load. return true; } if ( path[path.length - 1] === 'subHooks' || path[path.length - 2] === 'subHooks' ) { // Dehydrating the 'subHooks' property makes the HooksTree UI a lot more complicated, // so it's easiest for now if we just don't break on this boundary. // We can always dehydrate a level deeper (in the value object). return true; } break; case 'suspendedBy': if (path.length < 5) { // Never dehydrate anything above suspendedBy[index].awaited.value // Those are part of the internal meta data. We only dehydrate inside the Promise. return true; } break; default: break; } let current = key === null ? currentlyInspectedPaths : currentlyInspectedPaths[key]; if (!current) { return false; } for (let i = 0; i < path.length; i++) { current = current[path[i]]; if (!current) { return false; } } return true; }; } function updateSelectedElement(inspectedElement: InspectedElement): void { const {hooks, id, props} = inspectedElement; const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { console.warn(`Could not find DevToolsInstance with id "${id}"`); return; } if (devtoolsInstance.kind !== FIBER_INSTANCE) { // TODO: Handle VirtualInstance. return; } const fiber = devtoolsInstance.data; const {elementType, stateNode, tag, type} = fiber; switch (tag) { case ClassComponent: case IncompleteClassComponent: case IndeterminateComponent: global.$r = stateNode; break; case IncompleteFunctionComponent: case FunctionComponent: global.$r = { hooks, props, type, }; break; case ForwardRef: global.$r = { hooks, props, type: type.render, }; break; case MemoComponent: case SimpleMemoComponent: global.$r = { hooks, props, type: elementType != null && elementType.type != null ? elementType.type : type, }; break; default: global.$r = null; break; } } function storeAsGlobal( id: number, path: Array, count: number, ): void { if (isMostRecentlyInspectedElement(id)) { const value = getInObject( ((mostRecentlyInspectedElement: any): InspectedElement), path, ); const key = `$reactTemp${count}`; window[key] = value; console.log(key); console.log(value); } } function getSerializedElementValueByPath( id: number, path: Array, ): ?string { if (isMostRecentlyInspectedElement(id)) { const valueToCopy = getInObject( ((mostRecentlyInspectedElement: any): InspectedElement), path, ); return serializeToString(valueToCopy); } } function inspectElement( requestID: number, id: number, path: Array | null, forceFullData: boolean, ): InspectedElementPayload { if (path !== null) { mergeInspectedPaths(path); } if (isMostRecentlyInspectedElement(id) && !forceFullData) { if (!hasElementUpdatedSinceLastInspected) { if (path !== null) { let secondaryCategory = null; if (path[0] === 'hooks') { secondaryCategory = 'hooks'; } // If this element has not been updated since it was last inspected, // we can just return the subset of data in the newly-inspected path. return { id, responseID: requestID, type: 'hydrated-path', path, value: cleanForBridge( getInObject( ((mostRecentlyInspectedElement: any): InspectedElement), path, ), createIsPathAllowed(null, secondaryCategory), path, ), }; } else { // If this element has not been updated since it was last inspected, we don't need to return it. // Instead we can just return the ID to indicate that it has not changed. return { id, responseID: requestID, type: 'no-change', }; } } } else { currentlyInspectedPaths = {}; } hasElementUpdatedSinceLastInspected = false; try { mostRecentlyInspectedElement = inspectElementRaw(id); } catch (error) { // the error name is synced with ReactDebugHooks if (error.name === 'ReactDebugToolsRenderError') { let message = 'Error rendering inspected element.'; let stack; // Log error & cause for user to debug console.error(message + '\n\n', error); if (error.cause != null) { const componentName = getDisplayNameForElementID(id); console.error( 'React DevTools encountered an error while trying to inspect hooks. ' + 'This is most likely caused by an error in current inspected component' + (componentName != null ? `: "${componentName}".` : '.') + '\nThe error thrown in the component is: \n\n', error.cause, ); if (error.cause instanceof Error) { message = error.cause.message || message; stack = error.cause.stack; } } return { type: 'error', errorType: 'user', id, responseID: requestID, message, stack, }; } // the error name is synced with ReactDebugHooks if (error.name === 'ReactDebugToolsUnsupportedHookError') { return { type: 'error', errorType: 'unknown-hook', id, responseID: requestID, message: 'Unsupported hook in the react-debug-tools package: ' + error.message, }; } // Log Uncaught Error console.error('Error inspecting element.\n\n', error); return { type: 'error', errorType: 'uncaught', id, responseID: requestID, message: error.message, stack: error.stack, }; } if (mostRecentlyInspectedElement === null) { return { id, responseID: requestID, type: 'not-found', }; } const inspectedElement = mostRecentlyInspectedElement; // Any time an inspected element has an update, // we should update the selected $r value as wel. // Do this before dehydration (cleanForBridge). updateSelectedElement(inspectedElement); // Clone before cleaning so that we preserve the full data. // This will enable us to send patches without re-inspecting if hydrated paths are requested. // (Reducing how often we shallow-render is a better DX for function components that use hooks.) const cleanedInspectedElement = {...inspectedElement}; // $FlowFixMe[prop-missing] found when upgrading Flow cleanedInspectedElement.context = cleanForBridge( inspectedElement.context, createIsPathAllowed('context', null), ); // $FlowFixMe[prop-missing] found when upgrading Flow cleanedInspectedElement.hooks = cleanForBridge( inspectedElement.hooks, createIsPathAllowed('hooks', 'hooks'), ); // $FlowFixMe[prop-missing] found when upgrading Flow cleanedInspectedElement.props = cleanForBridge( inspectedElement.props, createIsPathAllowed('props', null), ); // $FlowFixMe[prop-missing] found when upgrading Flow cleanedInspectedElement.state = cleanForBridge( inspectedElement.state, createIsPathAllowed('state', null), ); // $FlowFixMe[prop-missing] found when upgrading Flow cleanedInspectedElement.suspendedBy = cleanForBridge( inspectedElement.suspendedBy, createIsPathAllowed('suspendedBy', 'suspendedBy'), ); return { id, responseID: requestID, type: 'full-data', // $FlowFixMe[prop-missing] found when upgrading Flow value: cleanedInspectedElement, }; } function logElementToConsole(id: number) { const result = isMostRecentlyInspectedElementCurrent(id) ? mostRecentlyInspectedElement : inspectElementRaw(id); if (result === null) { console.warn(`Could not find DevToolsInstance with id "${id}"`); return; } const displayName = getDisplayNameForElementID(id); const supportsGroup = typeof console.groupCollapsed === 'function'; if (supportsGroup) { console.groupCollapsed( `[Click to expand] %c<${displayName || 'Component'} />`, // --dom-tag-name-color is the CSS variable Chrome styles HTML elements with in the console. 'color: var(--dom-tag-name-color); font-weight: normal;', ); } if (result.props !== null) { console.log('Props:', result.props); } if (result.state !== null) { console.log('State:', result.state); } if (result.hooks !== null) { console.log('Hooks:', result.hooks); } const hostInstances = findHostInstancesForElementID(id); if (hostInstances !== null) { console.log('Nodes:', hostInstances); } if (window.chrome || /firefox/i.test(navigator.userAgent)) { console.log( 'Right-click any value to save it as a global variable for further inspection.', ); } if (supportsGroup) { console.groupEnd(); } } function deletePath( type: 'context' | 'hooks' | 'props' | 'state', id: number, hookID: ?number, path: Array, ): void { const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { console.warn(`Could not find DevToolsInstance with id "${id}"`); return; } if (devtoolsInstance.kind !== FIBER_INSTANCE) { // TODO: Handle VirtualInstance. return; } const fiber = devtoolsInstance.data; if (fiber !== null) { const instance = fiber.stateNode; switch (type) { case 'context': // To simplify hydration and display of primitive context values (e.g. number, string) // the inspectElement() method wraps context in a {value: ...} object. // We need to remove the first part of the path (the "value") before continuing. path = path.slice(1); switch (fiber.tag) { case ClassComponent: if (path.length === 0) { // Simple context value (noop) } else { deletePathInObject(instance.context, path); } instance.forceUpdate(); break; case FunctionComponent: // Function components using legacy context are not editable // because there's no instance on which to create a cloned, mutated context. break; } break; case 'hooks': if (typeof overrideHookStateDeletePath === 'function') { overrideHookStateDeletePath(fiber, ((hookID: any): number), path); } break; case 'props': if (instance === null) { if (typeof overridePropsDeletePath === 'function') { overridePropsDeletePath(fiber, path); } } else { fiber.pendingProps = copyWithDelete(instance.props, path); instance.forceUpdate(); } break; case 'state': deletePathInObject(instance.state, path); instance.forceUpdate(); break; } } } function renamePath( type: 'context' | 'hooks' | 'props' | 'state', id: number, hookID: ?number, oldPath: Array, newPath: Array, ): void { const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { console.warn(`Could not find DevToolsInstance with id "${id}"`); return; } if (devtoolsInstance.kind !== FIBER_INSTANCE) { // TODO: Handle VirtualInstance. return; } const fiber = devtoolsInstance.data; if (fiber !== null) { const instance = fiber.stateNode; switch (type) { case 'context': // To simplify hydration and display of primitive context values (e.g. number, string) // the inspectElement() method wraps context in a {value: ...} object. // We need to remove the first part of the path (the "value") before continuing. oldPath = oldPath.slice(1); newPath = newPath.slice(1); switch (fiber.tag) { case ClassComponent: if (oldPath.length === 0) { // Simple context value (noop) } else { renamePathInObject(instance.context, oldPath, newPath); } instance.forceUpdate(); break; case FunctionComponent: // Function components using legacy context are not editable // because there's no instance on which to create a cloned, mutated context. break; } break; case 'hooks': if (typeof overrideHookStateRenamePath === 'function') { overrideHookStateRenamePath( fiber, ((hookID: any): number), oldPath, newPath, ); } break; case 'props': if (instance === null) { if (typeof overridePropsRenamePath === 'function') { overridePropsRenamePath(fiber, oldPath, newPath); } } else { fiber.pendingProps = copyWithRename( instance.props, oldPath, newPath, ); instance.forceUpdate(); } break; case 'state': renamePathInObject(instance.state, oldPath, newPath); instance.forceUpdate(); break; } } } function overrideValueAtPath( type: 'context' | 'hooks' | 'props' | 'state', id: number, hookID: ?number, path: Array, value: any, ): void { const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { console.warn(`Could not find DevToolsInstance with id "${id}"`); return; } if (devtoolsInstance.kind !== FIBER_INSTANCE) { // TODO: Handle VirtualInstance. return; } const fiber = devtoolsInstance.data; if (fiber !== null) { const instance = fiber.stateNode; switch (type) { case 'context': // To simplify hydration and display of primitive context values (e.g. number, string) // the inspectElement() method wraps context in a {value: ...} object. // We need to remove the first part of the path (the "value") before continuing. path = path.slice(1); switch (fiber.tag) { case ClassComponent: if (path.length === 0) { // Simple context value instance.context = value; } else { setInObject(instance.context, path, value); } instance.forceUpdate(); break; case FunctionComponent: // Function components using legacy context are not editable // because there's no instance on which to create a cloned, mutated context. break; } break; case 'hooks': if (typeof overrideHookState === 'function') { overrideHookState(fiber, ((hookID: any): number), path, value); } break; case 'props': switch (fiber.tag) { case ClassComponent: fiber.pendingProps = copyWithSet(instance.props, path, value); instance.forceUpdate(); break; default: if (typeof overrideProps === 'function') { overrideProps(fiber, path, value); } break; } break; case 'state': switch (fiber.tag) { case ClassComponent: setInObject(instance.state, path, value); instance.forceUpdate(); break; } break; } } } type CommitProfilingData = { changeDescriptions: Map | null, commitTime: number, durations: Array, effectDuration: number | null, maxActualDuration: number, passiveEffectDuration: number | null, priorityLevel: string | null, updaters: Array | null, }; type CommitProfilingMetadataMap = Map>; type DisplayNamesByRootID = Map; let currentCommitProfilingMetadata: CommitProfilingData | null = null; let displayNamesByRootID: DisplayNamesByRootID | null = null; let initialTreeBaseDurationsMap: Map> | null = null; let isProfiling: boolean = false; let profilingStartTime: number = 0; let recordChangeDescriptions: boolean = false; let recordTimeline: boolean = false; let rootToCommitProfilingMetadataMap: CommitProfilingMetadataMap | null = null; function getProfilingData(): ProfilingDataBackend { const dataForRoots: Array = []; if (rootToCommitProfilingMetadataMap === null) { throw Error( 'getProfilingData() called before any profiling data was recorded', ); } rootToCommitProfilingMetadataMap.forEach( (commitProfilingMetadata, rootID) => { const commitData: Array = []; const displayName = (displayNamesByRootID !== null && displayNamesByRootID.get(rootID)) || 'Unknown'; const initialTreeBaseDurations: Array<[number, number]> = (initialTreeBaseDurationsMap !== null && initialTreeBaseDurationsMap.get(rootID)) || []; commitProfilingMetadata.forEach((commitProfilingData, commitIndex) => { const { changeDescriptions, durations, effectDuration, maxActualDuration, passiveEffectDuration, priorityLevel, commitTime, updaters, } = commitProfilingData; const fiberActualDurations: Array<[number, number]> = []; const fiberSelfDurations: Array<[number, number]> = []; for (let i = 0; i < durations.length; i += 3) { const fiberID = durations[i]; fiberActualDurations.push([ fiberID, formatDurationToMicrosecondsGranularity(durations[i + 1]), ]); fiberSelfDurations.push([ fiberID, formatDurationToMicrosecondsGranularity(durations[i + 2]), ]); } commitData.push({ changeDescriptions: changeDescriptions !== null ? Array.from(changeDescriptions.entries()) : null, duration: formatDurationToMicrosecondsGranularity(maxActualDuration), effectDuration: effectDuration !== null ? formatDurationToMicrosecondsGranularity(effectDuration) : null, fiberActualDurations, fiberSelfDurations, passiveEffectDuration: passiveEffectDuration !== null ? formatDurationToMicrosecondsGranularity(passiveEffectDuration) : null, priorityLevel, timestamp: commitTime, updaters, }); }); dataForRoots.push({ commitData, displayName, initialTreeBaseDurations, rootID, }); }, ); let timelineData = null; if (typeof getTimelineData === 'function') { const currentTimelineData = getTimelineData(); if (currentTimelineData) { const { batchUIDToMeasuresMap, internalModuleSourceToRanges, laneToLabelMap, laneToReactMeasureMap, ...rest } = currentTimelineData; timelineData = { ...rest, // Most of the data is safe to parse as-is, // but we need to convert the nested Arrays back to Maps. // Most of the data is safe to serialize as-is, // but we need to convert the Maps to nested Arrays. batchUIDToMeasuresKeyValueArray: Array.from( batchUIDToMeasuresMap.entries(), ), internalModuleSourceToRanges: Array.from( internalModuleSourceToRanges.entries(), ), laneToLabelKeyValueArray: Array.from(laneToLabelMap.entries()), laneToReactMeasureKeyValueArray: Array.from( laneToReactMeasureMap.entries(), ), }; } } return { dataForRoots, rendererID, timelineData, }; } function snapshotTreeBaseDurations( instance: DevToolsInstance, target: Array<[number, number]>, ) { // We don't need to convert milliseconds to microseconds in this case, // because the profiling summary is JSON serialized. if (instance.kind !== FILTERED_FIBER_INSTANCE) { target.push([instance.id, instance.treeBaseDuration]); } for ( let child = instance.firstChild; child !== null; child = child.nextSibling ) { snapshotTreeBaseDurations(child, target); } } function startProfiling( shouldRecordChangeDescriptions: boolean, shouldRecordTimeline: boolean, ) { if (isProfiling) { return; } recordChangeDescriptions = shouldRecordChangeDescriptions; recordTimeline = shouldRecordTimeline; // Capture initial values as of the time profiling starts. // It's important we snapshot both the durations and the id-to-root map, // since either of these may change during the profiling session // (e.g. when a fiber is re-rendered or when a fiber gets removed). displayNamesByRootID = new Map(); initialTreeBaseDurationsMap = new Map(); hook.getFiberRoots(rendererID).forEach(root => { const rootInstance = rootToFiberInstanceMap.get(root); if (rootInstance === undefined) { throw new Error( 'Expected the root instance to already exist when starting profiling', ); } const rootID = rootInstance.id; ((displayNamesByRootID: any): DisplayNamesByRootID).set( rootID, getDisplayNameForRoot(root.current), ); const initialTreeBaseDurations: Array<[number, number]> = []; snapshotTreeBaseDurations(rootInstance, initialTreeBaseDurations); (initialTreeBaseDurationsMap: any).set(rootID, initialTreeBaseDurations); }); isProfiling = true; profilingStartTime = getCurrentTime(); rootToCommitProfilingMetadataMap = new Map(); if (toggleProfilingStatus !== null) { toggleProfilingStatus(true, recordTimeline); } } function stopProfiling() { isProfiling = false; recordChangeDescriptions = false; if (toggleProfilingStatus !== null) { toggleProfilingStatus(false, recordTimeline); } recordTimeline = false; } // Automatically start profiling so that we don't miss timing info from initial "mount". if (shouldStartProfilingNow) { startProfiling( profilingSettings.recordChangeDescriptions, profilingSettings.recordTimeline, ); } function getNearestFiber(devtoolsInstance: DevToolsInstance): null | Fiber { if (devtoolsInstance.kind === VIRTUAL_INSTANCE) { let inst: DevToolsInstance = devtoolsInstance; while (inst.kind === VIRTUAL_INSTANCE) { // For virtual instances, we search deeper until we find a Fiber instance. // Then we search upwards from that Fiber. That's because Virtual Instances // will always have an Fiber child filtered or not. If we searched its parents // we might skip through a filtered Error Boundary before we hit a FiberInstance. if (inst.firstChild === null) { return null; } inst = inst.firstChild; } return inst.data.return; } else { return devtoolsInstance.data; } } // React will switch between these implementations depending on whether // we have any manually suspended/errored-out Fibers or not. function shouldErrorFiberAlwaysNull() { return null; } // Map of Fiber and its force error status: true (error), false (toggled off) const forceErrorForFibers = new Map(); function shouldErrorFiberAccordingToMap(fiber: any): boolean { if (typeof setErrorHandler !== 'function') { throw new Error( 'Expected overrideError() to not get called for earlier React versions.', ); } let status = forceErrorForFibers.get(fiber); if (status === false) { // TRICKY overrideError adds entries to this Map, // so ideally it would be the method that clears them too, // but that would break the functionality of the feature, // since DevTools needs to tell React to act differently than it normally would // (don't just re-render the failed boundary, but reset its errored state too). // So we can only clear it after telling React to reset the state. // Technically this is premature and we should schedule it for later, // since the render could always fail without committing the updated error boundary, // but since this is a DEV-only feature, the simplicity is worth the trade off. forceErrorForFibers.delete(fiber); if (forceErrorForFibers.size === 0) { // Last override is gone. Switch React back to fast path. setErrorHandler(shouldErrorFiberAlwaysNull); } return false; } if (status === undefined && fiber.alternate !== null) { status = forceErrorForFibers.get(fiber.alternate); if (status === false) { forceErrorForFibers.delete(fiber.alternate); if (forceErrorForFibers.size === 0) { // Last override is gone. Switch React back to fast path. setErrorHandler(shouldErrorFiberAlwaysNull); } } } if (status === undefined) { return false; } return status; } function overrideError(id: number, forceError: boolean) { if ( typeof setErrorHandler !== 'function' || typeof scheduleUpdate !== 'function' ) { throw new Error( 'Expected overrideError() to not get called for earlier React versions.', ); } const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { return; } const nearestFiber = getNearestFiber(devtoolsInstance); if (nearestFiber === null) { return; } let fiber = nearestFiber; while (!isErrorBoundary(fiber)) { if (fiber.return === null) { return; } fiber = fiber.return; } forceErrorForFibers.set(fiber, forceError); if (fiber.alternate !== null) { // We only need one of the Fibers in the set. forceErrorForFibers.delete(fiber.alternate); } if (forceErrorForFibers.size === 1) { // First override is added. Switch React to slower path. setErrorHandler(shouldErrorFiberAccordingToMap); } scheduleUpdate(fiber); } function shouldSuspendFiberAlwaysFalse() { return false; } const forceFallbackForFibers = new Set(); function shouldSuspendFiberAccordingToSet(fiber: Fiber): boolean { return ( forceFallbackForFibers.has(fiber) || (fiber.alternate !== null && forceFallbackForFibers.has(fiber.alternate)) ); } function overrideSuspense(id: number, forceFallback: boolean) { if ( typeof setSuspenseHandler !== 'function' || typeof scheduleUpdate !== 'function' ) { throw new Error( 'Expected overrideSuspense() to not get called for earlier React versions.', ); } const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { return; } const nearestFiber = getNearestFiber(devtoolsInstance); if (nearestFiber === null) { return; } let fiber = nearestFiber; while (fiber.tag !== SuspenseComponent) { if (fiber.return === null) { return; } fiber = fiber.return; } if (fiber.alternate !== null) { // We only need one of the Fibers in the set. forceFallbackForFibers.delete(fiber.alternate); } if (forceFallback) { forceFallbackForFibers.add(fiber); if (forceFallbackForFibers.size === 1) { // First override is added. Switch React to slower path. setSuspenseHandler(shouldSuspendFiberAccordingToSet); } } else { forceFallbackForFibers.delete(fiber); if (forceFallbackForFibers.size === 0) { // Last override is gone. Switch React back to fast path. setSuspenseHandler(shouldSuspendFiberAlwaysFalse); } } scheduleUpdate(fiber); } // Remember if we're trying to restore the selection after reload. // In that case, we'll do some extra checks for matching mounts. let trackedPath: Array | null = null; let trackedPathMatchFiber: Fiber | null = null; // This is the deepest unfiltered match of a Fiber. let trackedPathMatchInstance: FiberInstance | VirtualInstance | null = null; // This is the deepest matched filtered Instance. let trackedPathMatchDepth = -1; let mightBeOnTrackedPath = false; function setTrackedPath(path: Array | null) { if (path === null) { trackedPathMatchFiber = null; trackedPathMatchInstance = null; trackedPathMatchDepth = -1; mightBeOnTrackedPath = false; } trackedPath = path; } // We call this before traversing a new mount. // It remembers whether this Fiber is the next best match for tracked path. // The return value signals whether we should keep matching siblings or not. function updateTrackedPathStateBeforeMount( fiber: Fiber, fiberInstance: null | FiberInstance | FilteredFiberInstance, ): boolean { if (trackedPath === null || !mightBeOnTrackedPath) { // Fast path: there's nothing to track so do nothing and ignore siblings. return false; } const returnFiber = fiber.return; const returnAlternate = returnFiber !== null ? returnFiber.alternate : null; // By now we know there's some selection to restore, and this is a new Fiber. // Is this newly mounted Fiber a direct child of the current best match? // (This will also be true for new roots if we haven't matched anything yet.) if ( trackedPathMatchFiber === returnFiber || (trackedPathMatchFiber === returnAlternate && returnAlternate !== null) ) { // Is this the next Fiber we should select? Let's compare the frames. const actualFrame = getPathFrame(fiber); // $FlowFixMe[incompatible-use] found when upgrading Flow const expectedFrame = trackedPath[trackedPathMatchDepth + 1]; if (expectedFrame === undefined) { throw new Error('Expected to see a frame at the next depth.'); } if ( actualFrame.index === expectedFrame.index && actualFrame.key === expectedFrame.key && actualFrame.displayName === expectedFrame.displayName ) { // We have our next match. trackedPathMatchFiber = fiber; if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) { trackedPathMatchInstance = fiberInstance; } trackedPathMatchDepth++; // Are we out of frames to match? // $FlowFixMe[incompatible-use] found when upgrading Flow if (trackedPathMatchDepth === trackedPath.length - 1) { // There's nothing that can possibly match afterwards. // Don't check the children. mightBeOnTrackedPath = false; } else { // Check the children, as they might reveal the next match. mightBeOnTrackedPath = true; } // In either case, since we have a match, we don't need // to check the siblings. They'll never match. return false; } } if (trackedPathMatchFiber === null && fiberInstance === null) { // We're now looking for a Virtual Instance. It might be inside filtered Fibers // so we keep looking below. return true; } // This Fiber's parent is on the path, but this Fiber itself isn't. // There's no need to check its children--they won't be on the path either. mightBeOnTrackedPath = false; // However, one of its siblings may be on the path so keep searching. return true; } function updateVirtualTrackedPathStateBeforeMount( virtualInstance: VirtualInstance, parentInstance: null | DevToolsInstance, ): boolean { if (trackedPath === null || !mightBeOnTrackedPath) { // Fast path: there's nothing to track so do nothing and ignore siblings. return false; } // Check if we've matched our nearest unfiltered parent so far. if (trackedPathMatchInstance === parentInstance) { const actualFrame = getVirtualPathFrame(virtualInstance); // $FlowFixMe[incompatible-use] found when upgrading Flow const expectedFrame = trackedPath[trackedPathMatchDepth + 1]; if (expectedFrame === undefined) { throw new Error('Expected to see a frame at the next depth.'); } if ( actualFrame.index === expectedFrame.index && actualFrame.key === expectedFrame.key && actualFrame.displayName === expectedFrame.displayName ) { // We have our next match. trackedPathMatchFiber = null; // Don't bother looking in Fibers anymore. We're deeper now. trackedPathMatchInstance = virtualInstance; trackedPathMatchDepth++; // Are we out of frames to match? // $FlowFixMe[incompatible-use] found when upgrading Flow if (trackedPathMatchDepth === trackedPath.length - 1) { // There's nothing that can possibly match afterwards. // Don't check the children. mightBeOnTrackedPath = false; } else { // Check the children, as they might reveal the next match. mightBeOnTrackedPath = true; } // In either case, since we have a match, we don't need // to check the siblings. They'll never match. return false; } } if (trackedPathMatchFiber !== null) { // We're still looking for a Fiber which might be underneath this instance. return true; } // This Instance's parent is on the path, but this Instance itself isn't. // There's no need to check its children--they won't be on the path either. mightBeOnTrackedPath = false; // However, one of its siblings may be on the path so keep searching. return true; } function updateTrackedPathStateAfterMount( mightSiblingsBeOnTrackedPath: boolean, ) { // updateTrackedPathStateBeforeMount() told us whether to match siblings. // Now that we're entering siblings, let's use that information. mightBeOnTrackedPath = mightSiblingsBeOnTrackedPath; } // Roots don't have a real persistent identity. // A root's "pseudo key" is "childDisplayName:indexWithThatName". // For example, "App:0" or, in case of similar roots, "Story:0", "Story:1", etc. // We will use this to try to disambiguate roots when restoring selection between reloads. const rootPseudoKeys: Map = new Map(); const rootDisplayNameCounter: Map = new Map(); function setRootPseudoKey(id: number, fiber: Fiber) { const name = getDisplayNameForRoot(fiber); const counter = rootDisplayNameCounter.get(name) || 0; rootDisplayNameCounter.set(name, counter + 1); const pseudoKey = `${name}:${counter}`; rootPseudoKeys.set(id, pseudoKey); } function removeRootPseudoKey(id: number) { const pseudoKey = rootPseudoKeys.get(id); if (pseudoKey === undefined) { throw new Error('Expected root pseudo key to be known.'); } const name = pseudoKey.slice(0, pseudoKey.lastIndexOf(':')); const counter = rootDisplayNameCounter.get(name); if (counter === undefined) { throw new Error('Expected counter to be known.'); } if (counter > 1) { rootDisplayNameCounter.set(name, counter - 1); } else { rootDisplayNameCounter.delete(name); } rootPseudoKeys.delete(id); } function getDisplayNameForRoot(fiber: Fiber): string { let preferredDisplayName = null; let fallbackDisplayName = null; let child = fiber.child; // Go at most three levels deep into direct children // while searching for a child that has a displayName. for (let i = 0; i < 3; i++) { if (child === null) { break; } const displayName = getDisplayNameForFiber(child); if (displayName !== null) { // Prefer display names that we get from user-defined components. // We want to avoid using e.g. 'Suspense' unless we find nothing else. if (typeof child.type === 'function') { // There's a few user-defined tags, but we'll prefer the ones // that are usually explicitly named (function or class components). preferredDisplayName = displayName; } else if (fallbackDisplayName === null) { fallbackDisplayName = displayName; } } if (preferredDisplayName !== null) { break; } child = child.child; } return preferredDisplayName || fallbackDisplayName || 'Anonymous'; } function getPathFrame(fiber: Fiber): PathFrame { const {key} = fiber; let displayName = getDisplayNameForFiber(fiber); const index = fiber.index; switch (fiber.tag) { case HostRoot: // Roots don't have a real displayName, index, or key. // Instead, we'll use the pseudo key (childDisplayName:indexWithThatName). const rootInstance = rootToFiberInstanceMap.get(fiber.stateNode); if (rootInstance === undefined) { throw new Error( 'Expected the root instance to exist when computing a path', ); } const pseudoKey = rootPseudoKeys.get(rootInstance.id); if (pseudoKey === undefined) { throw new Error('Expected mounted root to have known pseudo key.'); } displayName = pseudoKey; break; case HostComponent: displayName = fiber.type; break; default: break; } return { displayName, key, index, }; } function getVirtualPathFrame(virtualInstance: VirtualInstance): PathFrame { return { displayName: virtualInstance.data.name || '', key: virtualInstance.data.key == null ? null : virtualInstance.data.key, index: -1, // We use -1 to indicate that this is a virtual path frame. }; } // Produces a serializable representation that does a best effort // of identifying a particular Fiber between page reloads. // The return path will contain Fibers that are "invisible" to the store // because their keys and indexes are important to restoring the selection. function getPathForElement(id: number): Array | null { const devtoolsInstance = idToDevToolsInstanceMap.get(id); if (devtoolsInstance === undefined) { return null; } const keyPath = []; let inst: DevToolsInstance = devtoolsInstance; while (inst.kind === VIRTUAL_INSTANCE) { keyPath.push(getVirtualPathFrame(inst)); if (inst.parent === null) { // This is a bug but non-essential. We should've found a root instance. return null; } inst = inst.parent; } let fiber: null | Fiber = inst.data; while (fiber !== null) { // $FlowFixMe[incompatible-call] found when upgrading Flow keyPath.push(getPathFrame(fiber)); // $FlowFixMe[incompatible-use] found when upgrading Flow fiber = fiber.return; } keyPath.reverse(); return keyPath; } function getBestMatchForTrackedPath(): PathMatch | null { if (trackedPath === null) { // Nothing to match. return null; } if (trackedPathMatchInstance === null) { // We didn't find anything. return null; } return { id: trackedPathMatchInstance.id, // $FlowFixMe[incompatible-use] found when upgrading Flow isFullMatch: trackedPathMatchDepth === trackedPath.length - 1, }; } const formatPriorityLevel = (priorityLevel: ?number) => { if (priorityLevel == null) { return 'Unknown'; } switch (priorityLevel) { case ImmediatePriority: return 'Immediate'; case UserBlockingPriority: return 'User-Blocking'; case NormalPriority: return 'Normal'; case LowPriority: return 'Low'; case IdlePriority: return 'Idle'; case NoPriority: default: return 'Unknown'; } }; function setTraceUpdatesEnabled(isEnabled: boolean): void { traceUpdatesEnabled = isEnabled; } function hasElementWithId(id: number): boolean { return idToDevToolsInstanceMap.has(id); } function getSourceForFiberInstance( fiberInstance: FiberInstance, ): ReactFunctionLocation | null { // Favor the owner source if we have one. const ownerSource = getSourceForInstance(fiberInstance); if (ownerSource !== null) { return ownerSource; } // Otherwise fallback to the throwing trick. const dispatcherRef = getDispatcherRef(renderer); const stackFrame = dispatcherRef == null ? null : getSourceLocationByFiber( ReactTypeOfWork, fiberInstance.data, dispatcherRef, ); if (stackFrame === null) { return null; } const source = extractLocationFromComponentStack(stackFrame); fiberInstance.source = source; return source; } function getSourceForInstance( instance: DevToolsInstance, ): ReactFunctionLocation | null { let unresolvedSource = instance.source; if (unresolvedSource === null) { // We don't have any source yet. We can try again later in case an owned child mounts later. // TODO: We won't have any information here if the child is filtered. return null; } if (instance.kind === VIRTUAL_INSTANCE) { // We might have found one on the virtual instance. const debugLocation = instance.data.debugLocation; if (debugLocation != null) { unresolvedSource = debugLocation; } } // If we have the debug stack (the creation stack of the JSX) for any owned child of this // component, then at the bottom of that stack will be a stack frame that is somewhere within // the component's function body. Typically it would be the callsite of the JSX unless there's // any intermediate utility functions. This won't point to the top of the component function // but it's at least somewhere within it. if (isError(unresolvedSource)) { return (instance.source = extractLocationFromOwnerStack( (unresolvedSource: any), )); } if (typeof unresolvedSource === 'string') { const idx = unresolvedSource.lastIndexOf('\n'); const lastLine = idx === -1 ? unresolvedSource : unresolvedSource.slice(idx + 1); return (instance.source = extractLocationFromComponentStack(lastLine)); } // $FlowFixMe: refined. return unresolvedSource; } type InternalMcpFunctions = { __internal_only_getComponentTree?: Function, }; const internalMcpFunctions: InternalMcpFunctions = {}; if (__IS_INTERNAL_MCP_BUILD__) { // eslint-disable-next-line no-inner-declarations function __internal_only_getComponentTree(): string { let treeString = ''; function buildTreeString( instance: DevToolsInstance, prefix: string = '', isLastChild: boolean = true, ): void { if (!instance) return; const name = (instance.kind !== VIRTUAL_INSTANCE ? getDisplayNameForFiber(instance.data) : instance.data.name) || 'Unknown'; const id = instance.id !== undefined ? instance.id : 'unknown'; if (name !== 'createRoot()') { treeString += prefix + (isLastChild ? '└── ' : '├── ') + name + ' (id: ' + id + ')\n'; } const childPrefix = prefix + (isLastChild ? ' ' : '│ '); let childCount = 0; let tempChild = instance.firstChild; while (tempChild !== null) { childCount++; tempChild = tempChild.nextSibling; } let child = instance.firstChild; let currentChildIndex = 0; while (child !== null) { currentChildIndex++; const isLastSibling = currentChildIndex === childCount; buildTreeString(child, childPrefix, isLastSibling); child = child.nextSibling; } } const rootInstances: Array = []; idToDevToolsInstanceMap.forEach(instance => { if (instance.parent === null || instance.parent.parent === null) { rootInstances.push(instance); } }); if (rootInstances.length > 0) { for (let i = 0; i < rootInstances.length; i++) { const isLast = i === rootInstances.length - 1; buildTreeString(rootInstances[i], '', isLast); if (!isLast) { treeString += '\n'; } } } else { treeString = 'No component tree found.'; } return treeString; } internalMcpFunctions.__internal_only_getComponentTree = __internal_only_getComponentTree; } return { cleanup, clearErrorsAndWarnings, clearErrorsForElementID, clearWarningsForElementID, getSerializedElementValueByPath, deletePath, findHostInstancesForElementID, flushInitialOperations, getBestMatchForTrackedPath, getDisplayNameForElementID, getNearestMountedDOMNode, getElementIDForHostInstance, getInstanceAndStyle, getOwnersList, getPathForElement, getProfilingData, handleCommitFiberRoot, handleCommitFiberUnmount, handlePostCommitFiberRoot, hasElementWithId, inspectElement, logElementToConsole, getComponentStack, getElementAttributeByPath, getElementSourceFunctionById, onErrorOrWarning, overrideError, overrideSuspense, overrideValueAtPath, renamePath, renderer, setTraceUpdatesEnabled, setTrackedPath, startProfiling, stopProfiling, storeAsGlobal, updateComponentFilters, getEnvironmentNames, ...internalMcpFunctions, }; }