import {extractLocationFromComponentStack} from 'react-devtools-shared/src/backend/utils/parseStackTrace';
import {
getOwnerStackByFiberInDev,
getSourceLocationByFiber,
} from 'react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack';
import {getDispatcherRef} from 'react-devtools-shared/src/backend/shared/DevToolsReactDispatcher';
import {inspectHooksOfFiberWithoutDefaultDispatcher} from 'react-debug-tools';
import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
import type {WorkTagMap} from 'react-devtools-shared/src/backend/types';
import type {HooksTree, HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
import type {RendererInternals} from './DevToolsFacade';
export type ToolError = {error: string | Error};
export type TreeNode = {
uid: string,
type: string,
name: string,
key: string | null,
firstChild: string | null,
nextSibling: string | null,
};
export type HookNode = {
id: number | null,
name: string,
value: mixed,
subHooks: Array<HookNode>,
};
export type NodeInfo = {
uid: string,
type: string,
name: string,
key?: string,
props?: {[string]: mixed},
hooks?: Array<HookNode>,
};
export type SourceLocation = {
name: string,
fileName: string,
line: number,
column: number,
};
export type ComponentSource = {source: SourceLocation | null};
export type OwnersStack = {stack: string};
export type ComponentBranchEntry = {uid: string, name: string, type: string};
export type ParentEntry = ComponentBranchEntry;
export type OwnerEntry = ComponentBranchEntry;
export type FindComponentsResult = {
page: number,
pageSize: number,
totalCount: number,
totalPages: number,
results: Array<TreeNode>,
};
export type TreeTools = {
getComponentTree: (
depth?: number,
rootUid?: string,
) => Array<TreeNode> | ToolError,
getComponentByUid: (
uid: string,
includeHooks?: boolean,
) => NodeInfo | ToolError,
getComponentByHostInstance: (hostInstance: mixed) => NodeInfo | ToolError,
findComponents: (
name: string,
rootUid?: string,
page?: number,
pageSize?: number,
) => FindComponentsResult | ToolError,
getComponentSource: (uid: string) => ComponentSource | ToolError,
getOwnerStackTrace: (uid: string) => OwnersStack | ToolError,
getParentStack: (uid: string) => Array<ParentEntry> | ToolError,
getOwnerStack: (uid: string) => Array<OwnerEntry> | ToolError,
getUid: (fiber: Fiber) => string,
};
export function getTypeTag(workTagMap: WorkTagMap, tag: number): string {
const {
FunctionComponent,
IncompleteFunctionComponent,
ClassComponent,
IncompleteClassComponent,
HostComponent,
HostHoistable,
HostSingleton,
HostRoot,
ForwardRef,
MemoComponent,
SimpleMemoComponent,
ContextConsumer,
ContextProvider,
SuspenseComponent,
SuspenseListComponent,
LazyComponent,
Profiler,
HostPortal,
ActivityComponent,
ViewTransitionComponent,
CacheComponent,
ScopeComponent,
OffscreenComponent,
LegacyHiddenComponent,
Throw,
HostText,
Fragment,
DehydratedSuspenseComponent,
Mode,
} = workTagMap;
switch (tag) {
case FunctionComponent:
case IncompleteFunctionComponent:
return 'function';
case ClassComponent:
case IncompleteClassComponent:
return 'class';
case HostComponent:
case HostHoistable:
case HostSingleton:
return 'host';
case HostRoot:
return 'root';
case ForwardRef:
return 'forwardRef';
case MemoComponent:
case SimpleMemoComponent:
return 'memo';
case ContextConsumer:
case ContextProvider:
return 'context';
case SuspenseComponent:
return 'suspense';
case SuspenseListComponent:
return 'suspenseList';
case LazyComponent:
return 'lazy';
case Profiler:
return 'profiler';
case HostPortal:
return 'portal';
case ActivityComponent:
return 'activity';
case ViewTransitionComponent:
return 'viewTransition';
case CacheComponent:
return 'cache';
case ScopeComponent:
return 'scope';
case OffscreenComponent:
case LegacyHiddenComponent:
return 'offscreen';
case Throw:
return 'throw';
case HostText:
return 'text';
case Fragment:
return 'fragment';
case Mode:
return 'mode';
case DehydratedSuspenseComponent:
return 'dehydrated';
default:
return 'unknown';
}
}
const MAX_NORMALIZE_DEPTH = 3;
function normalizeValue(val: mixed, seen?: Set<mixed>, depth?: number): mixed {
if (val === undefined) return null;
if (typeof val === 'function')
return val.name ? '[fn ' + val.name + ']' : '[fn]';
if (typeof val === 'symbol') return '[symbol]';
if (typeof val === 'object' && val !== null) {
if ((val as any).$$typeof != null) return '[React element]';
const currentDepth = depth || 0;
if (currentDepth >= MAX_NORMALIZE_DEPTH) return '[max depth]';
const currentSeen = seen || new Set();
if (currentSeen.has(val)) return '[circular]';
currentSeen.add(val);
if (Array.isArray(val)) {
const mapped = val.map((v: mixed) =>
normalizeValue(v, currentSeen, currentDepth + 1),
);
currentSeen.delete(val);
return mapped;
}
const result: {[string]: mixed} = {};
const keys = Object.keys(val);
for (let i = 0; i < keys.length; i++) {
result[keys[i]] = normalizeValue(
(val as any)[keys[i]],
currentSeen,
currentDepth + 1,
);
}
currentSeen.delete(val);
return result;
}
return val;
}
function normalizeProps(props: mixed): {[string]: mixed} | null {
if (props == null || typeof props !== 'object') return null;
const result: {[string]: mixed} = {};
const keys = Object.keys(props);
let hasProps = false;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key === 'children') continue;
result[key] = normalizeValue((props as any)[key]);
hasProps = true;
}
return hasProps ? result : null;
}
function normalizeHooks(hooks: HooksTree): Array<HookNode> {
return hooks.map((hook: HooksNode) => ({
id: hook.id,
name: hook.name,
value: normalizeValue(hook.value),
subHooks: normalizeHooks(hook.subHooks),
}));
}
export function createTreeTools(
fiberRoots: Map<number, Set<FiberRoot>>,
rendererInternals: Map<number, RendererInternals>,
): TreeTools {
function getTypeTagForFiber(
internals: RendererInternals,
fiber: Fiber,
): string {
return getTypeTag(internals.ReactTypeOfWork, fiber.tag);
}
function getDisplayName(internals: RendererInternals, fiber: Fiber): string {
return internals.getDisplayNameForFiber(fiber) || 'Unknown';
}
const fiberToUid: WeakMap<Fiber, string> = new WeakMap();
let nextId: number = 0;
function getUid(fiber: Fiber): string {
let uid = fiberToUid.get(fiber);
if (uid != null) return uid;
const alt = fiber.alternate;
if (alt != null) {
uid = fiberToUid.get(alt);
if (uid != null) {
fiberToUid.set(fiber, uid);
return uid;
}
}
uid = 'r' + nextId++;
fiberToUid.set(fiber, uid);
return uid;
}
function collectChildren(fiber: Fiber): Array<Fiber> {
const result: Array<Fiber> = [];
let child = fiber.child;
while (child !== null) {
result.push(child);
child = child.sibling;
}
return result;
}
function collectNodes(
internals: RendererInternals,
fiber: Fiber,
maxDepth: number,
currentDepth: number,
nodes: Array<TreeNode>,
): void {
const children = currentDepth < maxDepth ? collectChildren(fiber) : [];
const firstChild = children.length > 0 ? getUid(children[0]) : null;
nodes.push({
uid: getUid(fiber),
type: getTypeTagForFiber(internals, fiber),
name: getDisplayName(internals, fiber),
key: fiber.key != null ? String(fiber.key) : null,
firstChild,
nextSibling: null,
});
for (let i = 0; i < children.length; i++) {
collectNodes(internals, children[i], maxDepth, currentDepth + 1, nodes);
if (i < children.length - 1) {
const childUid = getUid(children[i]);
for (let j = nodes.length - 1; j >= 0; j--) {
if (nodes[j].uid === childUid) {
nodes[j].nextSibling = getUid(children[i + 1]);
break;
}
}
}
}
}
function findByUid(fiber: Fiber, targetUid: string): Fiber | null {
if (getUid(fiber) === targetUid) return fiber;
const children = collectChildren(fiber);
for (let i = 0; i < children.length; i++) {
const found = findByUid(children[i], targetUid);
if (found != null) return found;
}
return null;
}
function findFiberByUid(
uid: string,
):
| {fiber: Fiber, internals: RendererInternals, error: null}
| {fiber: null, internals: null, error: string} {
for (const [rendererID, roots] of fiberRoots) {
const internals = rendererInternals.get(rendererID);
if (internals == null) {
return {
fiber: null,
internals: null,
error: 'Missing internals for renderer ' + rendererID,
};
}
for (const root of roots) {
const fiber = findByUid(root.current, uid);
if (fiber != null) return {fiber, internals, error: null};
}
}
return {
fiber: null,
internals: null,
error: 'Component not found: "' + uid + '"',
};
}
function getHostInstanceForFiber(
internals: RendererInternals,
fiber: Fiber,
): mixed {
const {HostComponent, HostText, HostSingleton, HostHoistable} =
internals.ReactTypeOfWork;
if (
fiber.tag === HostComponent ||
fiber.tag === HostText ||
fiber.tag === HostSingleton
) {
return fiber.stateNode;
}
if (fiber.tag === HostHoistable) {
const resource = fiber.memoizedState;
if (
resource != null &&
typeof resource === 'object' &&
(resource as any).instance != null
) {
return (resource as any).instance;
}
}
return null;
}
function findByHostInstance(
internals: RendererInternals,
root: Fiber,
hostInstance: mixed,
): Fiber | null {
let current: Fiber | null = root;
while (current !== null) {
if (getHostInstanceForFiber(internals, current) === hostInstance) {
return current;
}
if (current.child !== null) {
current = current.child;
continue;
}
while (current !== null && current !== root && current.sibling === null) {
current = current.return;
}
if (current === null || current === root) {
return null;
}
current = current.sibling;
}
return null;
}
function buildNodeInfo(
fiber: Fiber,
internals: RendererInternals,
includeHooks?: boolean = false,
): NodeInfo | ToolError {
const info: NodeInfo = {
uid: getUid(fiber),
type: getTypeTagForFiber(internals, fiber),
name: getDisplayName(internals, fiber),
};
if (fiber.key != null) {
info.key = String(fiber.key);
}
const props = normalizeProps(fiber.memoizedProps);
if (props != null) {
info.props = props;
}
if (includeHooks) {
const {FunctionComponent, SimpleMemoComponent, ForwardRef} =
internals.ReactTypeOfWork;
if (
fiber.tag === FunctionComponent ||
fiber.tag === SimpleMemoComponent ||
fiber.tag === ForwardRef
) {
try {
const hooksTree = inspectHooksOfFiberWithoutDefaultDispatcher(
fiber,
getDispatcherRef(internals),
);
info.hooks = normalizeHooks(hooksTree);
} catch (error) {
return {
error: new Error('Failed to inspect hooks.', {cause: error}),
};
}
}
}
return info;
}
function getComponentTree(
depth?: number = 20,
rootUid?: string,
): Array<TreeNode> | ToolError {
if (rootUid != null) {
const result = findFiberByUid(rootUid);
if (result.error != null) {
return {error: result.error};
}
const nodes: Array<TreeNode> = [];
collectNodes(result.internals, result.fiber, depth, 0, nodes);
return nodes;
}
const nodes: Array<TreeNode> = [];
for (const [rendererID, roots] of fiberRoots) {
const internals = rendererInternals.get(rendererID);
if (internals == null) {
return {error: 'Missing internals for renderer ' + rendererID};
}
roots.forEach(root => {
collectNodes(internals, root.current, depth, 0, nodes);
});
}
if (nodes.length === 0) {
return {error: 'No mounted React roots found'};
}
return nodes;
}
function getComponentByUid(
uid: string,
includeHooks?: boolean = false,
): NodeInfo | ToolError {
const result = findFiberByUid(uid);
if (result.error != null) {
return {error: result.error};
}
return buildNodeInfo(result.fiber, result.internals, includeHooks);
}
function getComponentByHostInstance(
hostInstance: mixed,
): NodeInfo | ToolError {
if (hostInstance == null) {
return {error: 'Host instance is required'};
}
let sawRoot = false;
for (const [rendererID, roots] of fiberRoots) {
const internals = rendererInternals.get(rendererID);
if (internals == null) {
return {error: 'Missing internals for renderer ' + rendererID};
}
for (const root of roots) {
sawRoot = true;
const hostFiber = findByHostInstance(
internals,
root.current,
hostInstance,
);
if (hostFiber !== null) {
return buildNodeInfo(hostFiber, internals);
}
}
}
if (!sawRoot) {
return {error: 'No mounted React roots found'};
}
return {error: 'Host instance is not managed by React'};
}
function collectMatches(
internals: RendererInternals,
fiber: Fiber,
query: string,
matches: Array<Fiber>,
): void {
const displayName = internals.getDisplayNameForFiber(fiber);
if (
displayName != null &&
displayName.toLowerCase().indexOf(query) !== -1
) {
matches.push(fiber);
}
let child = fiber.child;
while (child !== null) {
collectMatches(internals, child, query, matches);
child = child.sibling;
}
}
type FiberMatch = {fiber: Fiber, internals: RendererInternals};
function findComponents(
name: string,
rootUid?: string,
page?: number = 1,
pageSize?: number = 10,
): FindComponentsResult | ToolError {
const query = name.toLowerCase();
const allMatches: Array<FiberMatch> = [];
if (rootUid != null) {
const found = findFiberByUid(rootUid);
if (found.error != null) {
return {error: found.error};
}
const fibers: Array<Fiber> = [];
collectMatches(found.internals, found.fiber, query, fibers);
for (let i = 0; i < fibers.length; i++) {
allMatches.push({fiber: fibers[i], internals: found.internals});
}
} else {
for (const [rendererID, roots] of fiberRoots) {
const internals = rendererInternals.get(rendererID);
if (internals == null) {
return {error: 'Missing internals for renderer ' + rendererID};
}
roots.forEach(root => {
const fibers: Array<Fiber> = [];
collectMatches(internals, root.current, query, fibers);
for (let i = 0; i < fibers.length; i++) {
allMatches.push({fiber: fibers[i], internals});
}
});
}
}
const totalCount = allMatches.length;
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
const clampedPage = Math.max(1, Math.min(page, totalPages));
const startIdx = (clampedPage - 1) * pageSize;
const pageMatches = allMatches.slice(startIdx, startIdx + pageSize);
const rows: Array<TreeNode> = [];
for (let i = 0; i < pageMatches.length; i++) {
const {fiber, internals} = pageMatches[i];
const children = collectChildren(fiber);
rows.push({
uid: getUid(fiber),
type: getTypeTagForFiber(internals, fiber),
name: getDisplayName(internals, fiber),
key: fiber.key != null ? String(fiber.key) : null,
firstChild: children.length > 0 ? getUid(children[0]) : null,
nextSibling: null,
});
}
return {
page: clampedPage,
pageSize,
totalCount,
totalPages,
results: rows,
};
}
function getComponentSource(uid: string): ComponentSource | ToolError {
const result = findFiberByUid(uid);
if (result.error != null) {
return {error: result.error};
}
const {fiber, internals} = result;
const stackFrame = getSourceLocationByFiber(
internals.ReactTypeOfWork,
fiber,
internals.currentDispatcherRef,
);
if (stackFrame == null) {
return {source: null};
}
const location = extractLocationFromComponentStack(stackFrame);
if (location == null) {
return {source: null};
}
const [name, fileName, line, column] = location;
return {source: {name, fileName, line, column}};
}
function getOwnerStackTrace(uid: string): OwnersStack | ToolError {
const result = findFiberByUid(uid);
if (result.error != null) {
return {error: result.error};
}
const {fiber, internals} = result;
const stackString = getOwnerStackByFiberInDev(
internals.ReactTypeOfWork,
fiber,
internals.currentDispatcherRef,
);
return {stack: stackString};
}
function getParentStack(uid: string): Array<ParentEntry> | ToolError {
const result = findFiberByUid(uid);
if (result.error != null) {
return {error: result.error};
}
const {internals} = result;
const parents: Array<ParentEntry> = [];
let parent = result.fiber.return;
while (parent !== null) {
parents.push({
uid: getUid(parent),
name: getDisplayName(internals, parent),
type: getTypeTagForFiber(internals, parent),
});
parent = parent.return;
}
return parents;
}
function getOwnerStack(uid: string): Array<OwnerEntry> | ToolError {
const result = findFiberByUid(uid);
if (result.error != null) {
return {error: result.error};
}
const {fiber, internals} = result;
const owners: Array<OwnerEntry> = [];
let owner: mixed = fiber._debugOwner;
while (owner != null) {
const node: any = owner;
if (typeof node.tag === 'number') {
owners.push({
uid: getUid(node),
name: getDisplayName(internals, node),
type: getTypeTagForFiber(internals, node),
});
owner = node._debugOwner;
} else {
owner = node.owner;
}
}
return owners;
}
return {
getComponentTree,
getComponentByUid,
getComponentByHostInstance,
findComponents,
getComponentSource,
getOwnerStackTrace,
getParentStack,
getOwnerStack,
getUid,
};
}