main
js 837 lines 25.8 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import {extractLocationFromComponentStack} from 'react-devtools-shared/src/backend/utils/parseStackTrace';
11 import {
12 getOwnerStackByFiberInDev,
13 getSourceLocationByFiber,
14 } from 'react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack';
15 import {getDispatcherRef} from 'react-devtools-shared/src/backend/shared/DevToolsReactDispatcher';
16 import {inspectHooksOfFiberWithoutDefaultDispatcher} from 'react-debug-tools';
17
18 import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
19 import type {WorkTagMap} from 'react-devtools-shared/src/backend/types';
20 import type {HooksTree, HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
21 import type {RendererInternals} from './DevToolsFacade';
22
23 // Tools return plain JavaScript values with the types below. Serialization
24 // (to TOON, JSON, etc.) is the integrator's responsibility.
25
26 // Returned by any tool when the requested component/root cannot be resolved.
27 export type ToolError = {error: string | Error};
28
29 // A single component in a tree snapshot. firstChild/nextSibling reference other
30 // nodes by their uid, forming an adjacency list the integrator can rebuild.
31 export type TreeNode = {
32 uid: string,
33 type: string,
34 name: string,
35 key: string | null,
36 firstChild: string | null,
37 nextSibling: string | null,
38 };
39
40 // One inspected hook. value is normalized (serialization-safe); subHooks holds
41 // the hooks called by a custom hook, recursively.
42 export type HookNode = {
43 id: number | null,
44 name: string,
45 value: mixed,
46 subHooks: Array<HookNode>,
47 };
48
49 export type NodeInfo = {
50 uid: string,
51 type: string,
52 name: string,
53 key?: string,
54 props?: {[string]: mixed},
55 hooks?: Array<HookNode>,
56 };
57
58 export type SourceLocation = {
59 name: string,
60 fileName: string,
61 line: number,
62 column: number,
63 };
64
65 export type ComponentSource = {source: SourceLocation | null};
66
67 export type OwnersStack = {stack: string};
68
69 export type ComponentBranchEntry = {uid: string, name: string, type: string};
70
71 export type ParentEntry = ComponentBranchEntry;
72
73 export type OwnerEntry = ComponentBranchEntry;
74
75 export type FindComponentsResult = {
76 page: number,
77 pageSize: number,
78 totalCount: number,
79 totalPages: number,
80 results: Array<TreeNode>,
81 };
82
83 export type TreeTools = {
84 getComponentTree: (
85 depth?: number,
86 rootUid?: string,
87 ) => Array<TreeNode> | ToolError,
88 getComponentByUid: (
89 uid: string,
90 includeHooks?: boolean,
91 ) => NodeInfo | ToolError,
92 getComponentByHostInstance: (hostInstance: mixed) => NodeInfo | ToolError,
93 findComponents: (
94 name: string,
95 rootUid?: string,
96 page?: number,
97 pageSize?: number,
98 ) => FindComponentsResult | ToolError,
99 getComponentSource: (uid: string) => ComponentSource | ToolError,
100 getOwnerStackTrace: (uid: string) => OwnersStack | ToolError,
101 getParentStack: (uid: string) => Array<ParentEntry> | ToolError,
102 getOwnerStack: (uid: string) => Array<OwnerEntry> | ToolError,
103 // Shared with the profiler tools so component uids are consistent across all
104 // tools. Maps a fiber to its stable uid (assigning one on first encounter).
105 getUid: (fiber: Fiber) => string,
106 };
107
108 /**
109 * Map a fiber work tag number to a human-readable type string.
110 * Every tag maps to a descriptive string; unknown tags return 'unknown'.
111 */
112 export function getTypeTag(workTagMap: WorkTagMap, tag: number): string {
113 const {
114 FunctionComponent,
115 IncompleteFunctionComponent,
116 ClassComponent,
117 IncompleteClassComponent,
118 HostComponent,
119 HostHoistable,
120 HostSingleton,
121 HostRoot,
122 ForwardRef,
123 MemoComponent,
124 SimpleMemoComponent,
125 ContextConsumer,
126 ContextProvider,
127 SuspenseComponent,
128 SuspenseListComponent,
129 LazyComponent,
130 Profiler,
131 HostPortal,
132 ActivityComponent,
133 ViewTransitionComponent,
134 CacheComponent,
135 ScopeComponent,
136 OffscreenComponent,
137 LegacyHiddenComponent,
138 Throw,
139 HostText,
140 Fragment,
141 DehydratedSuspenseComponent,
142 Mode,
143 } = workTagMap;
144
145 switch (tag) {
146 case FunctionComponent:
147 case IncompleteFunctionComponent:
148 return 'function';
149 case ClassComponent:
150 case IncompleteClassComponent:
151 return 'class';
152 case HostComponent:
153 case HostHoistable:
154 case HostSingleton:
155 return 'host';
156 case HostRoot:
157 return 'root';
158 case ForwardRef:
159 return 'forwardRef';
160 case MemoComponent:
161 case SimpleMemoComponent:
162 return 'memo';
163 case ContextConsumer:
164 case ContextProvider:
165 return 'context';
166 case SuspenseComponent:
167 return 'suspense';
168 case SuspenseListComponent:
169 return 'suspenseList';
170 case LazyComponent:
171 return 'lazy';
172 case Profiler:
173 return 'profiler';
174 case HostPortal:
175 return 'portal';
176 case ActivityComponent:
177 return 'activity';
178 case ViewTransitionComponent:
179 return 'viewTransition';
180 case CacheComponent:
181 return 'cache';
182 case ScopeComponent:
183 return 'scope';
184 case OffscreenComponent:
185 case LegacyHiddenComponent:
186 return 'offscreen';
187 case Throw:
188 return 'throw';
189 case HostText:
190 return 'text';
191 case Fragment:
192 return 'fragment';
193 case Mode:
194 return 'mode';
195 case DehydratedSuspenseComponent:
196 return 'dehydrated';
197 default:
198 return 'unknown';
199 }
200 }
201
202 const MAX_NORMALIZE_DEPTH = 3;
203
204 // Normalize a value to a plain, serialization-safe shape. Tracks seen objects
205 // to break circular references and limits depth to avoid stack overflow on
206 // deeply nested structures. Functions/symbols/elements become descriptive
207 // strings so the result can be safely serialized downstream.
208 function normalizeValue(val: mixed, seen?: Set<mixed>, depth?: number): mixed {
209 if (val === undefined) return null;
210 if (typeof val === 'function')
211 return val.name ? '[fn ' + val.name + ']' : '[fn]';
212 if (typeof val === 'symbol') return '[symbol]';
213 if (typeof val === 'object' && val !== null) {
214 if ((val as any).$$typeof != null) return '[React element]';
215 const currentDepth = depth || 0;
216 if (currentDepth >= MAX_NORMALIZE_DEPTH) return '[max depth]';
217 const currentSeen = seen || new Set();
218 if (currentSeen.has(val)) return '[circular]';
219 currentSeen.add(val);
220 if (Array.isArray(val)) {
221 const mapped = val.map((v: mixed) =>
222 normalizeValue(v, currentSeen, currentDepth + 1),
223 );
224 currentSeen.delete(val);
225 return mapped;
226 }
227 const result: {[string]: mixed} = {};
228 const keys = Object.keys(val);
229 for (let i = 0; i < keys.length; i++) {
230 result[keys[i]] = normalizeValue(
231 (val as any)[keys[i]],
232 currentSeen,
233 currentDepth + 1,
234 );
235 }
236 currentSeen.delete(val);
237 return result;
238 }
239 return val;
240 }
241
242 // Normalize props for output: skip children, normalize values.
243 function normalizeProps(props: mixed): {[string]: mixed} | null {
244 if (props == null || typeof props !== 'object') return null;
245 const result: {[string]: mixed} = {};
246 const keys = Object.keys(props);
247 let hasProps = false;
248 for (let i = 0; i < keys.length; i++) {
249 const key = keys[i];
250 if (key === 'children') continue;
251 result[key] = normalizeValue((props as any)[key]);
252 hasProps = true;
253 }
254 return hasProps ? result : null;
255 }
256
257 // Normalize an inspected hooks tree into a serialization-safe shape.
258 function normalizeHooks(hooks: HooksTree): Array<HookNode> {
259 return hooks.map((hook: HooksNode) => ({
260 id: hook.id,
261 name: hook.name,
262 value: normalizeValue(hook.value),
263 subHooks: normalizeHooks(hook.subHooks),
264 }));
265 }
266
267 export function createTreeTools(
268 fiberRoots: Map<number, Set<FiberRoot>>,
269 rendererInternals: Map<number, RendererInternals>,
270 ): TreeTools {
271 function getTypeTagForFiber(
272 internals: RendererInternals,
273 fiber: Fiber,
274 ): string {
275 return getTypeTag(internals.ReactTypeOfWork, fiber.tag);
276 }
277
278 function getDisplayName(internals: RendererInternals, fiber: Fiber): string {
279 return internals.getDisplayNameForFiber(fiber) || 'Unknown';
280 }
281
282 // Persistent uid state — survives across calls so the same fiber
283 // always maps to the same uid, even after React re-renders (which
284 // swap fiber objects via double-buffering / alternates).
285 const fiberToUid: WeakMap<Fiber, string> = new WeakMap();
286 let nextId: number = 0;
287
288 function getUid(fiber: Fiber): string {
289 let uid = fiberToUid.get(fiber);
290 if (uid != null) return uid;
291 const alt = fiber.alternate;
292 if (alt != null) {
293 uid = fiberToUid.get(alt);
294 if (uid != null) {
295 fiberToUid.set(fiber, uid);
296 return uid;
297 }
298 }
299 uid = 'r' + nextId++;
300 fiberToUid.set(fiber, uid);
301 return uid;
302 }
303
304 // Collect direct children of a fiber via the child/sibling linked list.
305 function collectChildren(fiber: Fiber): Array<Fiber> {
306 const result: Array<Fiber> = [];
307 let child = fiber.child;
308 while (child !== null) {
309 result.push(child);
310 child = child.sibling;
311 }
312 return result;
313 }
314
315 function collectNodes(
316 internals: RendererInternals,
317 fiber: Fiber,
318 maxDepth: number,
319 currentDepth: number,
320 nodes: Array<TreeNode>,
321 ): void {
322 const children = currentDepth < maxDepth ? collectChildren(fiber) : [];
323 const firstChild = children.length > 0 ? getUid(children[0]) : null;
324 nodes.push({
325 uid: getUid(fiber),
326 type: getTypeTagForFiber(internals, fiber),
327 name: getDisplayName(internals, fiber),
328 key: fiber.key != null ? String(fiber.key) : null,
329 firstChild,
330 nextSibling: null,
331 });
332 for (let i = 0; i < children.length; i++) {
333 collectNodes(internals, children[i], maxDepth, currentDepth + 1, nodes);
334 if (i < children.length - 1) {
335 const childUid = getUid(children[i]);
336 for (let j = nodes.length - 1; j >= 0; j--) {
337 if (nodes[j].uid === childUid) {
338 nodes[j].nextSibling = getUid(children[i + 1]);
339 break;
340 }
341 }
342 }
343 }
344 }
345
346 function findByUid(fiber: Fiber, targetUid: string): Fiber | null {
347 if (getUid(fiber) === targetUid) return fiber;
348 const children = collectChildren(fiber);
349 for (let i = 0; i < children.length; i++) {
350 const found = findByUid(children[i], targetUid);
351 if (found != null) return found;
352 }
353 return null;
354 }
355
356 // Find a fiber by uid across all mounted roots.
357 // Returns the fiber and its renderer's internals, or an error.
358 function findFiberByUid(
359 uid: string,
360 ):
361 | {fiber: Fiber, internals: RendererInternals, error: null}
362 | {fiber: null, internals: null, error: string} {
363 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
364 for (const [rendererID, roots] of fiberRoots) {
365 const internals = rendererInternals.get(rendererID);
366 if (internals == null) {
367 return {
368 fiber: null,
369 internals: null,
370 error: 'Missing internals for renderer ' + rendererID,
371 };
372 }
373 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
374 for (const root of roots) {
375 const fiber = findByUid(root.current, uid);
376 if (fiber != null) return {fiber, internals, error: null};
377 }
378 }
379 return {
380 fiber: null,
381 internals: null,
382 error: 'Component not found: "' + uid + '"',
383 };
384 }
385
386 function getHostInstanceForFiber(
387 internals: RendererInternals,
388 fiber: Fiber,
389 ): mixed {
390 const {HostComponent, HostText, HostSingleton, HostHoistable} =
391 internals.ReactTypeOfWork;
392
393 if (
394 fiber.tag === HostComponent ||
395 fiber.tag === HostText ||
396 fiber.tag === HostSingleton
397 ) {
398 return fiber.stateNode;
399 }
400
401 if (fiber.tag === HostHoistable) {
402 const resource = fiber.memoizedState;
403 if (
404 resource != null &&
405 typeof resource === 'object' &&
406 (resource as any).instance != null
407 ) {
408 return (resource as any).instance;
409 }
410 }
411
412 return null;
413 }
414
415 function findByHostInstance(
416 internals: RendererInternals,
417 root: Fiber,
418 hostInstance: mixed,
419 ): Fiber | null {
420 let current: Fiber | null = root;
421 while (current !== null) {
422 if (getHostInstanceForFiber(internals, current) === hostInstance) {
423 return current;
424 }
425
426 if (current.child !== null) {
427 current = current.child;
428 continue;
429 }
430
431 while (current !== null && current !== root && current.sibling === null) {
432 current = current.return;
433 }
434 if (current === null || current === root) {
435 return null;
436 }
437 current = current.sibling;
438 }
439 return null;
440 }
441
442 function buildNodeInfo(
443 fiber: Fiber,
444 internals: RendererInternals,
445 includeHooks?: boolean = false,
446 ): NodeInfo | ToolError {
447 const info: NodeInfo = {
448 uid: getUid(fiber),
449 type: getTypeTagForFiber(internals, fiber),
450 name: getDisplayName(internals, fiber),
451 };
452 if (fiber.key != null) {
453 info.key = String(fiber.key);
454 }
455 const props = normalizeProps(fiber.memoizedProps);
456 if (props != null) {
457 info.props = props;
458 }
459 if (includeHooks) {
460 // Hooks are only inspectable for function components, forwardRef, and
461 // simple-memo components. inspectHooksOfFiberWithoutDefaultDispatcher
462 // re-renders the component (using the renderer's injected dispatcher,
463 // never React's shared internals), so guard by tag and tolerate failures
464 // (e.g. a component that throws).
465 const {FunctionComponent, SimpleMemoComponent, ForwardRef} =
466 internals.ReactTypeOfWork;
467 if (
468 fiber.tag === FunctionComponent ||
469 fiber.tag === SimpleMemoComponent ||
470 fiber.tag === ForwardRef
471 ) {
472 try {
473 const hooksTree = inspectHooksOfFiberWithoutDefaultDispatcher(
474 fiber,
475 getDispatcherRef(internals),
476 );
477 info.hooks = normalizeHooks(hooksTree);
478 } catch (error) {
479 return {
480 error: new Error('Failed to inspect hooks.', {cause: error}),
481 };
482 }
483 }
484 }
485 return info;
486 }
487
488 /**
489 * Returns a snapshot of the component tree as an array of nodes. Each node
490 * includes: uid, type, name, key, firstChild, nextSibling (the last two
491 * reference other nodes by uid).
492 *
493 * @param depth - Maximum tree depth to traverse (default 20).
494 * @param rootUid - If provided, snapshot starts from this component.
495 */
496 function getComponentTree(
497 depth?: number = 20,
498 rootUid?: string,
499 ): Array<TreeNode> | ToolError {
500 if (rootUid != null) {
501 const result = findFiberByUid(rootUid);
502 if (result.error != null) {
503 return {error: result.error};
504 }
505 const nodes: Array<TreeNode> = [];
506 collectNodes(result.internals, result.fiber, depth, 0, nodes);
507 return nodes;
508 }
509
510 const nodes: Array<TreeNode> = [];
511 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
512 for (const [rendererID, roots] of fiberRoots) {
513 const internals = rendererInternals.get(rendererID);
514 if (internals == null) {
515 return {error: 'Missing internals for renderer ' + rendererID};
516 }
517 roots.forEach(root => {
518 collectNodes(internals, root.current, depth, 0, nodes);
519 });
520 }
521 if (nodes.length === 0) {
522 return {error: 'No mounted React roots found'};
523 }
524 return nodes;
525 }
526
527 /**
528 * Returns detailed info about a single component by its uid: type, name, key,
529 * and props (excluding children). Values are normalized to a serialization-safe
530 * shape.
531 *
532 * If includeHooks is true, function components also include the inspected
533 * hooks tree. Inspecting hooks re-renders the component's render function
534 * (effects are not run); failures return an error payload.
535 *
536 * @param uid - The component uid (e.g. "r5").
537 * @param includeHooks - Whether to inspect hooks for function components.
538 */
539 function getComponentByUid(
540 uid: string,
541 includeHooks?: boolean = false,
542 ): NodeInfo | ToolError {
543 const result = findFiberByUid(uid);
544 if (result.error != null) {
545 return {error: result.error};
546 }
547 return buildNodeInfo(result.fiber, result.internals, includeHooks);
548 }
549
550 /**
551 * Returns detailed info about the React host component for a host instance
552 * reference. The reference is opaque: for react-dom it may be a DOM
553 * Element/Text, but the facade only compares it by identity with host fiber
554 * state. It does not read platform-specific fields or walk host parents.
555 *
556 * @param hostInstance - A renderer host instance reference.
557 */
558 function getComponentByHostInstance(
559 hostInstance: mixed,
560 ): NodeInfo | ToolError {
561 if (hostInstance == null) {
562 return {error: 'Host instance is required'};
563 }
564
565 let sawRoot = false;
566 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
567 for (const [rendererID, roots] of fiberRoots) {
568 const internals = rendererInternals.get(rendererID);
569 if (internals == null) {
570 return {error: 'Missing internals for renderer ' + rendererID};
571 }
572 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
573 for (const root of roots) {
574 sawRoot = true;
575 const hostFiber = findByHostInstance(
576 internals,
577 root.current,
578 hostInstance,
579 );
580 if (hostFiber !== null) {
581 return buildNodeInfo(hostFiber, internals);
582 }
583 }
584 }
585
586 if (!sawRoot) {
587 return {error: 'No mounted React roots found'};
588 }
589 return {error: 'Host instance is not managed by React'};
590 }
591
592 function collectMatches(
593 internals: RendererInternals,
594 fiber: Fiber,
595 query: string,
596 matches: Array<Fiber>,
597 ): void {
598 const displayName = internals.getDisplayNameForFiber(fiber);
599 if (
600 displayName != null &&
601 displayName.toLowerCase().indexOf(query) !== -1
602 ) {
603 matches.push(fiber);
604 }
605 let child = fiber.child;
606 while (child !== null) {
607 collectMatches(internals, child, query, matches);
608 child = child.sibling;
609 }
610 }
611
612 type FiberMatch = {fiber: Fiber, internals: RendererInternals};
613
614 /**
615 * Searches for components by name (case-insensitive substring match).
616 * Returns a paginated result with matching components.
617 *
618 * @param name - Search query to match against component display names.
619 * @param rootUid - If provided, limits search to this component's subtree.
620 * @param page - Page number (default 1, clamped to valid range).
621 * @param pageSize - Results per page (default 10).
622 */
623 function findComponents(
624 name: string,
625 rootUid?: string,
626 page?: number = 1,
627 pageSize?: number = 10,
628 ): FindComponentsResult | ToolError {
629 const query = name.toLowerCase();
630 const allMatches: Array<FiberMatch> = [];
631
632 if (rootUid != null) {
633 const found = findFiberByUid(rootUid);
634 if (found.error != null) {
635 return {error: found.error};
636 }
637 const fibers: Array<Fiber> = [];
638 collectMatches(found.internals, found.fiber, query, fibers);
639 for (let i = 0; i < fibers.length; i++) {
640 allMatches.push({fiber: fibers[i], internals: found.internals});
641 }
642 } else {
643 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
644 for (const [rendererID, roots] of fiberRoots) {
645 const internals = rendererInternals.get(rendererID);
646 if (internals == null) {
647 return {error: 'Missing internals for renderer ' + rendererID};
648 }
649 roots.forEach(root => {
650 const fibers: Array<Fiber> = [];
651 collectMatches(internals, root.current, query, fibers);
652 for (let i = 0; i < fibers.length; i++) {
653 allMatches.push({fiber: fibers[i], internals});
654 }
655 });
656 }
657 }
658
659 const totalCount = allMatches.length;
660 const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
661 const clampedPage = Math.max(1, Math.min(page, totalPages));
662 const startIdx = (clampedPage - 1) * pageSize;
663 const pageMatches = allMatches.slice(startIdx, startIdx + pageSize);
664
665 const rows: Array<TreeNode> = [];
666 for (let i = 0; i < pageMatches.length; i++) {
667 const {fiber, internals} = pageMatches[i];
668 const children = collectChildren(fiber);
669 rows.push({
670 uid: getUid(fiber),
671 type: getTypeTagForFiber(internals, fiber),
672 name: getDisplayName(internals, fiber),
673 key: fiber.key != null ? String(fiber.key) : null,
674 firstChild: children.length > 0 ? getUid(children[0]) : null,
675 nextSibling: null,
676 });
677 }
678
679 return {
680 page: clampedPage,
681 pageSize,
682 totalCount,
683 totalPages,
684 results: rows,
685 };
686 }
687
688 /**
689 * Returns the definition location of a component — where the component
690 * function or class is defined in source code. Uses the same "throwing
691 * trick" as React DevTools to capture a stack frame from within the
692 * component's function body.
693 *
694 * Returns {source: {name, fileName, line, column}} or {source: null} if the
695 * location cannot be determined (e.g. host components, production builds).
696 *
697 * @param uid - The component uid (e.g. "r5").
698 */
699 function getComponentSource(uid: string): ComponentSource | ToolError {
700 const result = findFiberByUid(uid);
701 if (result.error != null) {
702 return {error: result.error};
703 }
704 const {fiber, internals} = result;
705 const stackFrame = getSourceLocationByFiber(
706 internals.ReactTypeOfWork,
707 fiber,
708 internals.currentDispatcherRef,
709 );
710 if (stackFrame == null) {
711 return {source: null};
712 }
713 const location = extractLocationFromComponentStack(stackFrame);
714 if (location == null) {
715 return {source: null};
716 }
717 const [name, fileName, line, column] = location;
718 return {source: {name, fileName, line, column}};
719 }
720
721 /**
722 * Returns the raw owner stack trace string — the chain of JSX creation
723 * locations from this component up to the root. Each line is a stack frame
724 * showing where <Component /> was written in the owner's code. The stack can
725 * be passed to source map tools for symbolication.
726 *
727 * Returns {stack: string}. DEV-only — in production, the stack will be empty.
728 *
729 * @param uid - The component uid (e.g. "r5").
730 */
731 function getOwnerStackTrace(uid: string): OwnersStack | ToolError {
732 const result = findFiberByUid(uid);
733 if (result.error != null) {
734 return {error: result.error};
735 }
736 const {fiber, internals} = result;
737 const stackString = getOwnerStackByFiberInDev(
738 internals.ReactTypeOfWork,
739 fiber,
740 internals.currentDispatcherRef,
741 );
742 return {stack: stackString};
743 }
744
745 /**
746 * Returns the structural parent branch for this fiber — the path formed by
747 * following Fiber.return pointers from this component to the host root.
748 * Parents describe where a node is mounted in the rendered tree, so this
749 * branch can include host DOM components and the host root.
750 *
751 * This differs from owners: owners describe which components created/rendered
752 * an element through JSX and are DEV-only metadata. Parents are structural
753 * runtime relationships and are available whenever the fiber tree exists.
754 *
755 * Returns an array of {uid, name, type}, ordered from immediate parent to
756 * root ancestor. The host root has an empty parent branch.
757 *
758 * @param uid - The component uid (e.g. "r5").
759 */
760 function getParentStack(uid: string): Array<ParentEntry> | ToolError {
761 const result = findFiberByUid(uid);
762 if (result.error != null) {
763 return {error: result.error};
764 }
765 const {internals} = result;
766 const parents: Array<ParentEntry> = [];
767 let parent = result.fiber.return;
768 while (parent !== null) {
769 parents.push({
770 uid: getUid(parent),
771 name: getDisplayName(internals, parent),
772 type: getTypeTagForFiber(internals, parent),
773 });
774 parent = parent.return;
775 }
776 return parents;
777 }
778
779 /**
780 * Returns the structured list of owner components — which components rendered
781 * or created this element through JSX, ordered from immediate owner to root
782 * ancestor. Owners describe creation/render ownership, not where a node is
783 * mounted in the rendered tree. Use getParentStack for structural Fiber
784 * parent ancestry, including host DOM parents and the host root.
785 *
786 * Each entry includes a uid for cross-referencing with other tools (e.g.
787 * getComponentByUid, getComponentSource, getComponentTree).
788 *
789 * Returns an array of {uid, name, type}, or an empty array if the component
790 * has no owner (root component). DEV-only — in production, _debugOwner is not
791 * available.
792 *
793 * @param uid - The component uid (e.g. "r5").
794 */
795 function getOwnerStack(uid: string): Array<OwnerEntry> | ToolError {
796 const result = findFiberByUid(uid);
797 if (result.error != null) {
798 return {error: result.error};
799 }
800 const {fiber, internals} = result;
801
802 const owners: Array<OwnerEntry> = [];
803 // Walk the JSX-creation owner chain from this component up to the root,
804 // collecting only Fiber owners (client components). A Fiber's _debugOwner
805 // points to the next owner — itself a Fiber (client) or a
806 // ReactComponentInfo (server component); the latter continues the chain
807 // via its .owner field.
808 let owner: mixed = fiber._debugOwner;
809 while (owner != null) {
810 const node: any = owner;
811 if (typeof node.tag === 'number') {
812 owners.push({
813 uid: getUid(node),
814 name: getDisplayName(internals, node),
815 type: getTypeTagForFiber(internals, node),
816 });
817 owner = node._debugOwner;
818 } else {
819 // Server component (ReactComponentInfo): continue via its .owner.
820 owner = node.owner;
821 }
822 }
823 return owners;
824 }
825
826 return {
827 getComponentTree,
828 getComponentByUid,
829 getComponentByHostInstance,
830 findComponents,
831 getComponentSource,
832 getOwnerStackTrace,
833 getParentStack,
834 getOwnerStack,
835 getUid,
836 };
837 }