/** * 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 {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'; // Tools return plain JavaScript values with the types below. Serialization // (to TOON, JSON, etc.) is the integrator's responsibility. // Returned by any tool when the requested component/root cannot be resolved. export type ToolError = {error: string | Error}; // A single component in a tree snapshot. firstChild/nextSibling reference other // nodes by their uid, forming an adjacency list the integrator can rebuild. export type TreeNode = { uid: string, type: string, name: string, key: string | null, firstChild: string | null, nextSibling: string | null, }; // One inspected hook. value is normalized (serialization-safe); subHooks holds // the hooks called by a custom hook, recursively. export type HookNode = { id: number | null, name: string, value: mixed, subHooks: Array, }; export type NodeInfo = { uid: string, type: string, name: string, key?: string, props?: {[string]: mixed}, hooks?: Array, }; 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, }; export type TreeTools = { getComponentTree: ( depth?: number, rootUid?: string, ) => Array | 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 | ToolError, getOwnerStack: (uid: string) => Array | ToolError, // Shared with the profiler tools so component uids are consistent across all // tools. Maps a fiber to its stable uid (assigning one on first encounter). getUid: (fiber: Fiber) => string, }; /** * Map a fiber work tag number to a human-readable type string. * Every tag maps to a descriptive string; unknown tags return 'unknown'. */ 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; // Normalize a value to a plain, serialization-safe shape. Tracks seen objects // to break circular references and limits depth to avoid stack overflow on // deeply nested structures. Functions/symbols/elements become descriptive // strings so the result can be safely serialized downstream. function normalizeValue(val: mixed, seen?: Set, 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; } // Normalize props for output: skip children, normalize values. 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; } // Normalize an inspected hooks tree into a serialization-safe shape. function normalizeHooks(hooks: HooksTree): Array { return hooks.map((hook: HooksNode) => ({ id: hook.id, name: hook.name, value: normalizeValue(hook.value), subHooks: normalizeHooks(hook.subHooks), })); } export function createTreeTools( fiberRoots: Map>, rendererInternals: Map, ): 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'; } // Persistent uid state — survives across calls so the same fiber // always maps to the same uid, even after React re-renders (which // swap fiber objects via double-buffering / alternates). const fiberToUid: WeakMap = 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; } // Collect direct children of a fiber via the child/sibling linked list. function collectChildren(fiber: Fiber): Array { const result: Array = []; 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, ): 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; } // Find a fiber by uid across all mounted roots. // Returns the fiber and its renderer's internals, or an error. function findFiberByUid( uid: string, ): | {fiber: Fiber, internals: RendererInternals, error: null} | {fiber: null, internals: null, error: string} { // eslint-disable-next-line no-for-of-loops/no-for-of-loops 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, }; } // eslint-disable-next-line no-for-of-loops/no-for-of-loops 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) { // Hooks are only inspectable for function components, forwardRef, and // simple-memo components. inspectHooksOfFiberWithoutDefaultDispatcher // re-renders the component (using the renderer's injected dispatcher, // never React's shared internals), so guard by tag and tolerate failures // (e.g. a component that throws). 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; } /** * Returns a snapshot of the component tree as an array of nodes. Each node * includes: uid, type, name, key, firstChild, nextSibling (the last two * reference other nodes by uid). * * @param depth - Maximum tree depth to traverse (default 20). * @param rootUid - If provided, snapshot starts from this component. */ function getComponentTree( depth?: number = 20, rootUid?: string, ): Array | ToolError { if (rootUid != null) { const result = findFiberByUid(rootUid); if (result.error != null) { return {error: result.error}; } const nodes: Array = []; collectNodes(result.internals, result.fiber, depth, 0, nodes); return nodes; } const nodes: Array = []; // eslint-disable-next-line no-for-of-loops/no-for-of-loops 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; } /** * Returns detailed info about a single component by its uid: type, name, key, * and props (excluding children). Values are normalized to a serialization-safe * shape. * * If includeHooks is true, function components also include the inspected * hooks tree. Inspecting hooks re-renders the component's render function * (effects are not run); failures return an error payload. * * @param uid - The component uid (e.g. "r5"). * @param includeHooks - Whether to inspect hooks for function components. */ 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); } /** * Returns detailed info about the React host component for a host instance * reference. The reference is opaque: for react-dom it may be a DOM * Element/Text, but the facade only compares it by identity with host fiber * state. It does not read platform-specific fields or walk host parents. * * @param hostInstance - A renderer host instance reference. */ function getComponentByHostInstance( hostInstance: mixed, ): NodeInfo | ToolError { if (hostInstance == null) { return {error: 'Host instance is required'}; } let sawRoot = false; // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const [rendererID, roots] of fiberRoots) { const internals = rendererInternals.get(rendererID); if (internals == null) { return {error: 'Missing internals for renderer ' + rendererID}; } // eslint-disable-next-line no-for-of-loops/no-for-of-loops 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, ): 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}; /** * Searches for components by name (case-insensitive substring match). * Returns a paginated result with matching components. * * @param name - Search query to match against component display names. * @param rootUid - If provided, limits search to this component's subtree. * @param page - Page number (default 1, clamped to valid range). * @param pageSize - Results per page (default 10). */ function findComponents( name: string, rootUid?: string, page?: number = 1, pageSize?: number = 10, ): FindComponentsResult | ToolError { const query = name.toLowerCase(); const allMatches: Array = []; if (rootUid != null) { const found = findFiberByUid(rootUid); if (found.error != null) { return {error: found.error}; } const fibers: Array = []; collectMatches(found.internals, found.fiber, query, fibers); for (let i = 0; i < fibers.length; i++) { allMatches.push({fiber: fibers[i], internals: found.internals}); } } else { // eslint-disable-next-line no-for-of-loops/no-for-of-loops 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 = []; 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 = []; 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, }; } /** * Returns the definition location of a component — where the component * function or class is defined in source code. Uses the same "throwing * trick" as React DevTools to capture a stack frame from within the * component's function body. * * Returns {source: {name, fileName, line, column}} or {source: null} if the * location cannot be determined (e.g. host components, production builds). * * @param uid - The component uid (e.g. "r5"). */ 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}}; } /** * Returns the raw owner stack trace string — the chain of JSX creation * locations from this component up to the root. Each line is a stack frame * showing where was written in the owner's code. The stack can * be passed to source map tools for symbolication. * * Returns {stack: string}. DEV-only — in production, the stack will be empty. * * @param uid - The component uid (e.g. "r5"). */ 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}; } /** * Returns the structural parent branch for this fiber — the path formed by * following Fiber.return pointers from this component to the host root. * Parents describe where a node is mounted in the rendered tree, so this * branch can include host DOM components and the host root. * * This differs from owners: owners describe which components created/rendered * an element through JSX and are DEV-only metadata. Parents are structural * runtime relationships and are available whenever the fiber tree exists. * * Returns an array of {uid, name, type}, ordered from immediate parent to * root ancestor. The host root has an empty parent branch. * * @param uid - The component uid (e.g. "r5"). */ function getParentStack(uid: string): Array | ToolError { const result = findFiberByUid(uid); if (result.error != null) { return {error: result.error}; } const {internals} = result; const parents: Array = []; 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; } /** * Returns the structured list of owner components — which components rendered * or created this element through JSX, ordered from immediate owner to root * ancestor. Owners describe creation/render ownership, not where a node is * mounted in the rendered tree. Use getParentStack for structural Fiber * parent ancestry, including host DOM parents and the host root. * * Each entry includes a uid for cross-referencing with other tools (e.g. * getComponentByUid, getComponentSource, getComponentTree). * * Returns an array of {uid, name, type}, or an empty array if the component * has no owner (root component). DEV-only — in production, _debugOwner is not * available. * * @param uid - The component uid (e.g. "r5"). */ function getOwnerStack(uid: string): Array | ToolError { const result = findFiberByUid(uid); if (result.error != null) { return {error: result.error}; } const {fiber, internals} = result; const owners: Array = []; // Walk the JSX-creation owner chain from this component up to the root, // collecting only Fiber owners (client components). A Fiber's _debugOwner // points to the next owner — itself a Fiber (client) or a // ReactComponentInfo (server component); the latter continues the chain // via its .owner field. 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 { // Server component (ReactComponentInfo): continue via its .owner. owner = node.owner; } } return owners; } return { getComponentTree, getComponentByUid, getComponentByHostInstance, findComponents, getComponentSource, getOwnerStackTrace, getParentStack, getOwnerStack, getUid, }; }