mirror of
https://github.com/facebook/react.git
synced 2026-02-26 18:58:05 +00:00
Just adding the name so it shows up. (Note that no experimental ones are added to the list of filters atm. Including SuspenseList etc.)
5846 lines
197 KiB
JavaScript
5846 lines
197 KiB
JavaScript
/**
|
|
* 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 {ReactComponentInfo, ReactDebugInfo} from 'shared/ReactTypes';
|
|
|
|
import {
|
|
ComponentFilterDisplayName,
|
|
ComponentFilterElementType,
|
|
ComponentFilterHOC,
|
|
ComponentFilterLocation,
|
|
ComponentFilterEnvironmentName,
|
|
ElementTypeClass,
|
|
ElementTypeContext,
|
|
ElementTypeFunction,
|
|
ElementTypeForwardRef,
|
|
ElementTypeHostComponent,
|
|
ElementTypeMemo,
|
|
ElementTypeOtherOrUnknown,
|
|
ElementTypeProfiler,
|
|
ElementTypeRoot,
|
|
ElementTypeSuspense,
|
|
ElementTypeSuspenseList,
|
|
ElementTypeTracingMarker,
|
|
ElementTypeViewTransition,
|
|
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,
|
|
parseSourceFromComponentStack,
|
|
serializeToString,
|
|
} from 'react-devtools-shared/src/backend/utils';
|
|
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,
|
|
} 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,
|
|
} 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 {
|
|
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,
|
|
WorkTagMap,
|
|
CurrentDispatcherRef,
|
|
LegacyDispatcherRef,
|
|
ProfilingSettings,
|
|
} from '../types';
|
|
import type {
|
|
ComponentFilter,
|
|
ElementType,
|
|
Plugins,
|
|
} from 'react-devtools-shared/src/frontend/types';
|
|
import type {Source} from 'react-devtools-shared/src/shared/types';
|
|
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 | Source, // 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
|
|
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,
|
|
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 | Source, // always null here.
|
|
logCount: number, // total number of errors/warnings last seen
|
|
treeBaseDuration: number, // the profiled time of the last render of this subtree
|
|
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,
|
|
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 | Source, // 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
|
|
// 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,
|
|
data: debugEntry,
|
|
};
|
|
}
|
|
|
|
type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance;
|
|
|
|
type getDisplayNameForFiberType = (fiber: Fiber) => string | null;
|
|
type getTypeSymbolType = (type: any) => symbol | 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,
|
|
} {
|
|
// **********************************************************
|
|
// 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;
|
|
}
|
|
|
|
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
|
|
};
|
|
} 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
|
|
};
|
|
} 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
|
|
};
|
|
} 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
|
|
};
|
|
} 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
|
|
};
|
|
}
|
|
// **********************************************************
|
|
// End of copied code.
|
|
// **********************************************************
|
|
|
|
function getTypeSymbol(type: any): symbol | number {
|
|
const symbolOrNumber =
|
|
typeof type === 'object' && type !== null ? type.$$typeof : type;
|
|
|
|
return typeof symbolOrNumber === 'symbol'
|
|
? // $FlowFixMe[incompatible-return] `toString()` doesn't match the type signature?
|
|
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,
|
|
} = 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 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,
|
|
};
|
|
}
|
|
|
|
// 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<string> = new Set();
|
|
|
|
// Map of FiberRoot to their root FiberInstance.
|
|
const rootToFiberInstanceMap: Map<FiberRoot, FiberInstance> = 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<number, FiberInstance | VirtualInstance> =
|
|
new Map();
|
|
|
|
// Map of canonical HostInstances to the nearest parent DevToolsInstance.
|
|
const publicInstanceToDevToolsInstanceMap: Map<HostInstance, DevToolsInstance> =
|
|
new Map();
|
|
// Map of resource DOM nodes to all the nearest DevToolsInstances that depend on it.
|
|
const hostResourceToDevToolsInstanceMap: Map<
|
|
HostInstance,
|
|
Set<DevToolsInstance>,
|
|
> = 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 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,
|
|
} = getInternalReactConstants(version);
|
|
const {
|
|
CacheComponent,
|
|
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<string, number>,
|
|
errorsCount: number,
|
|
warnings: Map<string, number>,
|
|
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<Fiber, ComponentLogs> = 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<any>,
|
|
): 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 ? '<no id>' : 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
|
|
? '<no id>'
|
|
: 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<RegExp> = new Set();
|
|
const hideElementsWithPaths: Set<RegExp> = new Set();
|
|
const hideElementsWithTypes: Set<ElementType> = new Set();
|
|
const hideElementsWithEnvs: Set<string> = new Set();
|
|
|
|
// Highlight updates
|
|
let traceUpdatesEnabled: boolean = false;
|
|
const traceUpdatesForNodes: Set<HostInstance> = new Set();
|
|
|
|
function applyComponentFilters(componentFilters: Array<ComponentFilter>) {
|
|
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<ComponentFilter>) {
|
|
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<string> {
|
|
return Array.from(knownEnvironmentNames);
|
|
}
|
|
|
|
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 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<number> {
|
|
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<string> {
|
|
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<number>;
|
|
|
|
type StringTableEntry = {
|
|
encodedString: Array<number>,
|
|
id: number,
|
|
};
|
|
|
|
const pendingOperations: OperationsArray = [];
|
|
const pendingRealUnmountedIDs: Array<number> = [];
|
|
let pendingOperationsQueue: Array<OperationsArray> | null = [];
|
|
const pendingStringTable: Map<string, StringTableEntry> = new Map();
|
|
let pendingStringTableLength: number = 0;
|
|
let pendingUnmountedRootID: number | 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 &&
|
|
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 operations = new Array<number>(
|
|
// 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 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 (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;
|
|
pendingUnmountedRootID = null;
|
|
pendingStringTable.clear();
|
|
pendingStringTableLength = 0;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
|
|
const id = fiberInstance.id;
|
|
|
|
if (__DEBUG__) {
|
|
debug('recordMount()', fiberInstance, parentInstance);
|
|
}
|
|
|
|
const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
|
|
|
|
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;
|
|
}
|
|
const ownerID = ownerInstance === null ? 0 : ownerInstance.id;
|
|
const parentID = parentInstance
|
|
? parentInstance.kind === FILTERED_FIBER_INSTANCE
|
|
? // A Filtered Fiber Instance will always have a Virtual Instance as a parent.
|
|
((parentInstance.parent: any): VirtualInstance).id
|
|
: parentInstance.id
|
|
: 0;
|
|
|
|
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);
|
|
|
|
pushOperation(TREE_OPERATION_ADD);
|
|
pushOperation(id);
|
|
pushOperation(elementType);
|
|
pushOperation(parentID);
|
|
pushOperation(ownerID);
|
|
pushOperation(displayNameStringID);
|
|
pushOperation(keyStringID);
|
|
|
|
// 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);
|
|
}
|
|
return fiberInstance;
|
|
}
|
|
|
|
function recordVirtualMount(
|
|
instance: VirtualInstance,
|
|
parentInstance: DevToolsInstance | null,
|
|
secondaryEnv: null | string,
|
|
): void {
|
|
const id = instance.id;
|
|
|
|
idToDevToolsInstanceMap.set(id, instance);
|
|
|
|
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;
|
|
}
|
|
const ownerID = ownerInstance === null ? 0 : ownerInstance.id;
|
|
const parentID = parentInstance
|
|
? parentInstance.kind === FILTERED_FIBER_INSTANCE
|
|
? // A Filtered Fiber Instance will always have a Virtual Instance as a parent.
|
|
((parentInstance.parent: any): VirtualInstance).id
|
|
: parentInstance.id
|
|
: 0;
|
|
|
|
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);
|
|
|
|
pushOperation(TREE_OPERATION_ADD);
|
|
pushOperation(id);
|
|
pushOperation(elementType);
|
|
pushOperation(parentID);
|
|
pushOperation(ownerID);
|
|
pushOperation(displayNameStringID);
|
|
pushOperation(keyStringID);
|
|
|
|
const componentLogsEntry =
|
|
componentInfoToComponentLogsMap.get(componentInfo);
|
|
recordConsoleLogs(instance, componentLogsEntry);
|
|
}
|
|
|
|
function recordUnmount(fiberInstance: FiberInstance): void {
|
|
const fiber = fiberInstance.data;
|
|
if (__DEBUG__) {
|
|
debug('recordUnmount()', fiberInstance, reconcilingParent);
|
|
}
|
|
|
|
if (trackedPathMatchInstance === fiberInstance) {
|
|
// 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 = 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);
|
|
}
|
|
|
|
idToDevToolsInstanceMap.delete(fiberInstance.id);
|
|
|
|
untrackFiber(fiberInstance, fiber);
|
|
}
|
|
|
|
// 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;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
function unmountRemainingChildren() {
|
|
let child = remainingReconcilingChildren;
|
|
while (child !== null) {
|
|
unmountInstanceRecursively(child);
|
|
child = remainingReconcilingChildren;
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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 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 (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 mountFiberRecursively(
|
|
fiber: Fiber,
|
|
traceNearestHostComponentUpdate: boolean,
|
|
): void {
|
|
const shouldIncludeInTree = !shouldFilterFiber(fiber);
|
|
let newInstance = null;
|
|
if (shouldIncludeInTree) {
|
|
newInstance = recordMount(fiber, reconcilingParent);
|
|
insertChild(newInstance);
|
|
if (__DEBUG__) {
|
|
debug('mountFiberRecursively()', newInstance, reconcilingParent);
|
|
}
|
|
} else if (
|
|
reconcilingParent !== null &&
|
|
reconcilingParent.kind === VIRTUAL_INSTANCE
|
|
) {
|
|
// If the parent is a Virtual Instance and we filtered this Fiber we include a
|
|
// hidden node.
|
|
|
|
if (
|
|
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);
|
|
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;
|
|
if (newInstance !== null) {
|
|
// Push a new DevTools instance parent while reconciling this subtree.
|
|
reconcilingParent = newInstance;
|
|
previouslyReconciledSibling = null;
|
|
remainingReconcilingChildren = null;
|
|
}
|
|
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.
|
|
}
|
|
|
|
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);
|
|
} 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);
|
|
}
|
|
|
|
if (fiber.tag === SuspenseComponent) {
|
|
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,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
let primaryChild: Fiber | null = null;
|
|
const areSuspenseChildrenConditionallyWrapped =
|
|
OffscreenComponent === -1;
|
|
if (areSuspenseChildrenConditionallyWrapped) {
|
|
primaryChild = fiber.child;
|
|
} else if (fiber.child !== null) {
|
|
primaryChild = fiber.child.child;
|
|
updateTrackedPathStateBeforeMount(fiber.child, null);
|
|
}
|
|
if (primaryChild !== null) {
|
|
mountChildrenRecursively(
|
|
primaryChild,
|
|
traceNearestHostComponentUpdate,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
if (fiber.child !== null) {
|
|
mountChildrenRecursively(
|
|
fiber.child,
|
|
traceNearestHostComponentUpdate,
|
|
);
|
|
}
|
|
}
|
|
} finally {
|
|
if (newInstance !== null) {
|
|
reconcilingParent = stashedParent;
|
|
previouslyReconciledSibling = stashedPrevious;
|
|
remainingReconcilingChildren = stashedRemaining;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
// 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;
|
|
try {
|
|
// Unmount the remaining set.
|
|
unmountRemainingChildren();
|
|
} finally {
|
|
reconcilingParent = stashedParent;
|
|
previouslyReconciledSibling = stashedPrevious;
|
|
remainingReconcilingChildren = stashedRemaining;
|
|
}
|
|
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 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<number> = [];
|
|
|
|
let child: null | DevToolsInstance = parentInstance.firstChild;
|
|
while (child !== null) {
|
|
if (child.kind === FILTERED_FIBER_INSTANCE) {
|
|
for (
|
|
let innerChild: null | DevToolsInstance = parentInstance.firstChild;
|
|
innerChild !== null;
|
|
innerChild = innerChild.nextSibling
|
|
) {
|
|
nextChildren.push((innerChild: any).id);
|
|
}
|
|
} else {
|
|
nextChildren.push(child.id);
|
|
}
|
|
child = child.nextSibling;
|
|
}
|
|
|
|
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 updateVirtualInstanceRecursively(
|
|
virtualInstance: VirtualInstance,
|
|
nextFirstChild: Fiber,
|
|
nextLastChild: null | Fiber, // non-inclusive
|
|
prevFirstChild: null | Fiber,
|
|
traceNearestHostComponentUpdate: boolean,
|
|
virtualLevel: number, // the nth level of virtual instances
|
|
): void {
|
|
const stashedParent = reconcilingParent;
|
|
const stashedPrevious = previouslyReconciledSibling;
|
|
const stashedRemaining = remainingReconcilingChildren;
|
|
// 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;
|
|
try {
|
|
if (
|
|
updateVirtualChildrenRecursively(
|
|
nextFirstChild,
|
|
nextLastChild,
|
|
prevFirstChild,
|
|
traceNearestHostComponentUpdate,
|
|
virtualLevel + 1,
|
|
)
|
|
) {
|
|
recordResetChildren(virtualInstance);
|
|
}
|
|
// 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);
|
|
} 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
|
|
): boolean {
|
|
let shouldResetChildren = false;
|
|
// 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 (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,
|
|
);
|
|
} else {
|
|
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;
|
|
shouldResetChildren = true;
|
|
}
|
|
// 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,
|
|
);
|
|
} else {
|
|
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 = (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) {
|
|
shouldResetChildren = true;
|
|
}
|
|
|
|
moveChild(fiberInstance, previousSiblingOfExistingInstance);
|
|
|
|
if (
|
|
updateFiberRecursively(
|
|
fiberInstance,
|
|
nextChild,
|
|
(prevChild: any),
|
|
traceNearestHostComponentUpdate,
|
|
)
|
|
) {
|
|
// 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.
|
|
shouldResetChildren = true;
|
|
}
|
|
} else if (prevChild !== null && shouldFilterFiber(nextChild)) {
|
|
// 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.
|
|
if (
|
|
updateFiberRecursively(
|
|
null,
|
|
nextChild,
|
|
prevChild,
|
|
traceNearestHostComponentUpdate,
|
|
)
|
|
) {
|
|
shouldResetChildren = true;
|
|
}
|
|
} 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.
|
|
shouldResetChildren = true;
|
|
}
|
|
}
|
|
// 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 (!shouldResetChildren && prevChildAtSameIndex !== null) {
|
|
prevChildAtSameIndex = prevChildAtSameIndex.sibling;
|
|
}
|
|
}
|
|
if (previousVirtualInstance !== null) {
|
|
if (previousVirtualInstanceWasMount) {
|
|
mountVirtualInstanceRecursively(
|
|
previousVirtualInstance,
|
|
previousVirtualInstanceNextFirstFiber,
|
|
null,
|
|
traceNearestHostComponentUpdate,
|
|
virtualLevel,
|
|
);
|
|
} else {
|
|
updateVirtualInstanceRecursively(
|
|
previousVirtualInstance,
|
|
previousVirtualInstanceNextFirstFiber,
|
|
null,
|
|
previousVirtualInstancePrevFirstFiber,
|
|
traceNearestHostComponentUpdate,
|
|
virtualLevel,
|
|
);
|
|
}
|
|
}
|
|
// If we have no more children, but used to, they don't line up.
|
|
if (prevChildAtSameIndex !== null) {
|
|
shouldResetChildren = true;
|
|
}
|
|
return shouldResetChildren;
|
|
}
|
|
|
|
// Returns whether closest unfiltered fiber parent needs to reset its child list.
|
|
function updateChildrenRecursively(
|
|
nextFirstChild: null | Fiber,
|
|
prevFirstChild: null | Fiber,
|
|
traceNearestHostComponentUpdate: boolean,
|
|
): boolean {
|
|
if (nextFirstChild === null) {
|
|
return prevFirstChild !== null;
|
|
}
|
|
return updateVirtualChildrenRecursively(
|
|
nextFirstChild,
|
|
null,
|
|
prevFirstChild,
|
|
traceNearestHostComponentUpdate,
|
|
0,
|
|
);
|
|
}
|
|
|
|
// Returns whether closest unfiltered fiber parent needs to reset its child list.
|
|
function updateFiberRecursively(
|
|
fiberInstance: null | FiberInstance, // null if this should be filtered
|
|
nextFiber: Fiber,
|
|
prevFiber: Fiber,
|
|
traceNearestHostComponentUpdate: boolean,
|
|
): boolean {
|
|
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;
|
|
if (fiberInstance !== null) {
|
|
// 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;
|
|
}
|
|
try {
|
|
if (nextFiber.tag === HostHoistable) {
|
|
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);
|
|
}
|
|
|
|
const isSuspense = nextFiber.tag === SuspenseComponent;
|
|
let shouldResetChildren = false;
|
|
// The behavior of timed-out Suspense trees is unique.
|
|
// 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 prevDidTimeout = isSuspense && prevFiber.memoizedState !== null;
|
|
const nextDidTimeOut = isSuspense && nextFiber.memoizedState !== null;
|
|
// 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,
|
|
);
|
|
|
|
shouldResetChildren = true;
|
|
}
|
|
|
|
if (
|
|
nextFallbackChildSet != null &&
|
|
prevFallbackChildSet != null &&
|
|
updateChildrenRecursively(
|
|
nextFallbackChildSet,
|
|
prevFallbackChildSet,
|
|
traceNearestHostComponentUpdate,
|
|
)
|
|
) {
|
|
shouldResetChildren = true;
|
|
}
|
|
} 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,
|
|
);
|
|
}
|
|
shouldResetChildren = true;
|
|
} 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,
|
|
);
|
|
shouldResetChildren = true;
|
|
}
|
|
} else {
|
|
// Common case: Primary -> Primary.
|
|
// This is the same code path as for non-Suspense fibers.
|
|
if (nextFiber.child !== prevFiber.child) {
|
|
if (
|
|
updateChildrenRecursively(
|
|
nextFiber.child,
|
|
prevFiber.child,
|
|
traceNearestHostComponentUpdate,
|
|
)
|
|
) {
|
|
shouldResetChildren = true;
|
|
}
|
|
} 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;
|
|
|
|
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 {
|
|
// 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 (
|
|
updateChildrenRecursively(nextFiber.child, prevFiber.child, false)
|
|
) {
|
|
throw new Error(
|
|
'The children should not have changed if we pass in the same set.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fiberInstance !== null) {
|
|
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 (shouldResetChildren) {
|
|
// 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) {
|
|
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.
|
|
return false;
|
|
} else {
|
|
// Let the closest unfiltered parent Fiber reset its child order instead.
|
|
return true;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
} finally {
|
|
if (fiberInstance !== null) {
|
|
unmountRemainingChildren();
|
|
reconcilingParent = stashedParent;
|
|
previouslyReconciledSibling = stashedPrevious;
|
|
remainingReconcilingChildren = stashedRemaining;
|
|
}
|
|
}
|
|
}
|
|
|
|
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<HostInstance>,
|
|
) {
|
|
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<HostInstance>,
|
|
): 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<HostInstance> {
|
|
const hostInstances: Array<HostInstance> = [];
|
|
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 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<string | number>,
|
|
): 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,
|
|
type: getElementTypeForFiber(fiber),
|
|
};
|
|
} else {
|
|
const componentInfo = instance.data;
|
|
return {
|
|
displayName: componentInfo.name || 'Anonymous',
|
|
id: instance.id,
|
|
key: componentInfo.key == null ? null : componentInfo.key,
|
|
type: ElementTypeVirtual,
|
|
};
|
|
}
|
|
}
|
|
|
|
function getOwnersList(id: number): Array<SerializedElement> | 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<SerializedElement> | null {
|
|
let owner = getUnfilteredOwner(instance.data);
|
|
if (owner === null) {
|
|
return null;
|
|
}
|
|
const owners: Array<SerializedElement> = [];
|
|
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;
|
|
}
|
|
|
|
// 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 = !usesHooks && tag !== CacheComponent;
|
|
|
|
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<SerializedElement> =
|
|
getOwnersListFromInstance(fiberInstance);
|
|
|
|
let hooks = null;
|
|
if (usesHooks) {
|
|
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 {
|
|
hooks = inspectHooksOfFiber(fiber, getDispatcherRef(renderer));
|
|
} finally {
|
|
// Restore original console functionality.
|
|
for (const method in originalConsoleMethods) {
|
|
try {
|
|
// $FlowFixMe[prop-missing]
|
|
console[method] = originalConsoleMethods[method];
|
|
} catch (error) {}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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))),
|
|
|
|
// Can view component source location.
|
|
canViewSource,
|
|
source,
|
|
|
|
// 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()),
|
|
|
|
// List of owners
|
|
owners,
|
|
|
|
rootType,
|
|
rendererPackageName: renderer.rendererPackageName,
|
|
rendererVersion: renderer.version,
|
|
|
|
plugins,
|
|
};
|
|
}
|
|
|
|
function inspectVirtualInstanceRaw(
|
|
virtualInstance: VirtualInstance,
|
|
): InspectedElement | null {
|
|
const canViewSource = true;
|
|
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<SerializedElement> =
|
|
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);
|
|
|
|
return {
|
|
id: virtualInstance.id,
|
|
|
|
canEditHooks: false,
|
|
canEditFunctionProps: false,
|
|
|
|
canEditHooksAndDeletePaths: false,
|
|
canEditHooksAndRenamePaths: false,
|
|
canEditFunctionPropsDeletePaths: false,
|
|
canEditFunctionPropsRenamePaths: false,
|
|
|
|
canToggleError: supportsTogglingError && hasErrorBoundary,
|
|
isErrored: false,
|
|
|
|
canToggleSuspense: supportsTogglingSuspense && hasSuspenseBoundary,
|
|
|
|
// Can view component source location.
|
|
canViewSource,
|
|
source,
|
|
|
|
// 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()),
|
|
// List of owners
|
|
owners,
|
|
|
|
rootType,
|
|
rendererPackageName: renderer.rendererPackageName,
|
|
rendererVersion: renderer.version,
|
|
|
|
plugins,
|
|
};
|
|
}
|
|
|
|
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<string | number>) {
|
|
let current = currentlyInspectedPaths;
|
|
path.forEach(key => {
|
|
if (!current[key]) {
|
|
current[key] = {};
|
|
}
|
|
current = current[key];
|
|
});
|
|
}
|
|
|
|
function createIsPathAllowed(
|
|
key: string | null,
|
|
secondaryCategory: '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<string | number>): 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;
|
|
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<string | number>,
|
|
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 | number>,
|
|
): ?string {
|
|
if (isMostRecentlyInspectedElement(id)) {
|
|
const valueToCopy = getInObject(
|
|
((mostRecentlyInspectedElement: any): InspectedElement),
|
|
path,
|
|
);
|
|
|
|
return serializeToString(valueToCopy);
|
|
}
|
|
}
|
|
|
|
function inspectElement(
|
|
requestID: number,
|
|
id: number,
|
|
path: Array<string | number> | 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',
|
|
};
|
|
}
|
|
|
|
// Any time an inspected element has an update,
|
|
// we should update the selected $r value as wel.
|
|
// Do this before dehydration (cleanForBridge).
|
|
updateSelectedElement(mostRecentlyInspectedElement);
|
|
|
|
// 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 = {...mostRecentlyInspectedElement};
|
|
// $FlowFixMe[prop-missing] found when upgrading Flow
|
|
cleanedInspectedElement.context = cleanForBridge(
|
|
cleanedInspectedElement.context,
|
|
createIsPathAllowed('context', null),
|
|
);
|
|
// $FlowFixMe[prop-missing] found when upgrading Flow
|
|
cleanedInspectedElement.hooks = cleanForBridge(
|
|
cleanedInspectedElement.hooks,
|
|
createIsPathAllowed('hooks', 'hooks'),
|
|
);
|
|
// $FlowFixMe[prop-missing] found when upgrading Flow
|
|
cleanedInspectedElement.props = cleanForBridge(
|
|
cleanedInspectedElement.props,
|
|
createIsPathAllowed('props', null),
|
|
);
|
|
// $FlowFixMe[prop-missing] found when upgrading Flow
|
|
cleanedInspectedElement.state = cleanForBridge(
|
|
cleanedInspectedElement.state,
|
|
createIsPathAllowed('state', null),
|
|
);
|
|
|
|
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<string | number>,
|
|
): 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<string | number>,
|
|
newPath: Array<string | number>,
|
|
): 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<string | number>,
|
|
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<number, ChangeDescription> | null,
|
|
commitTime: number,
|
|
durations: Array<number>,
|
|
effectDuration: number | null,
|
|
maxActualDuration: number,
|
|
passiveEffectDuration: number | null,
|
|
priorityLevel: string | null,
|
|
updaters: Array<SerializedElement> | null,
|
|
};
|
|
|
|
type CommitProfilingMetadataMap = Map<number, Array<CommitProfilingData>>;
|
|
type DisplayNamesByRootID = Map<number, string>;
|
|
|
|
let currentCommitProfilingMetadata: CommitProfilingData | null = null;
|
|
let displayNamesByRootID: DisplayNamesByRootID | null = null;
|
|
let initialTreeBaseDurationsMap: Map<number, Array<[number, number]>> | 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<ProfilingDataForRootBackend> = [];
|
|
|
|
if (rootToCommitProfilingMetadataMap === null) {
|
|
throw Error(
|
|
'getProfilingData() called before any profiling data was recorded',
|
|
);
|
|
}
|
|
|
|
rootToCommitProfilingMetadataMap.forEach(
|
|
(commitProfilingMetadata, rootID) => {
|
|
const commitData: Array<CommitDataBackend> = [];
|
|
|
|
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<Fiber, boolean>();
|
|
|
|
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<Fiber>();
|
|
|
|
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<PathFrame> | 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<PathFrame> | 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<number, string> = new Map();
|
|
const rootDisplayNameCounter: Map<string, number> = 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<PathFrame> | 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,
|
|
): Source | null {
|
|
const unresolvedSource = fiberInstance.source;
|
|
if (
|
|
unresolvedSource !== null &&
|
|
typeof unresolvedSource === 'object' &&
|
|
!isError(unresolvedSource)
|
|
) {
|
|
// $FlowFixMe: isError should have refined it.
|
|
return unresolvedSource;
|
|
}
|
|
const dispatcherRef = getDispatcherRef(renderer);
|
|
const stackFrame =
|
|
dispatcherRef == null
|
|
? null
|
|
: getSourceLocationByFiber(
|
|
ReactTypeOfWork,
|
|
fiberInstance.data,
|
|
dispatcherRef,
|
|
);
|
|
if (stackFrame === null) {
|
|
// If we don't find a source location by throwing, try to get one
|
|
// from an owned child if possible. This is the same branch as
|
|
// for virtual instances.
|
|
return getSourceForInstance(fiberInstance);
|
|
}
|
|
const source = parseSourceFromComponentStack(stackFrame);
|
|
fiberInstance.source = source;
|
|
return source;
|
|
}
|
|
|
|
function getSourceForInstance(instance: DevToolsInstance): Source | 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 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)) {
|
|
unresolvedSource = formatOwnerStack((unresolvedSource: any));
|
|
}
|
|
if (typeof unresolvedSource === 'string') {
|
|
const idx = unresolvedSource.lastIndexOf('\n');
|
|
const lastLine =
|
|
idx === -1 ? unresolvedSource : unresolvedSource.slice(idx + 1);
|
|
return (instance.source = parseSourceFromComponentStack(lastLine));
|
|
}
|
|
|
|
// $FlowFixMe: refined.
|
|
return unresolvedSource;
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|