Skip to content

Commit 3508aee

Browse files
authored
[react-devtools] add parent stack tool (react#36825)
This PR adds a new `getParentStack` tool to the Facade and an integration of it in `react-devtools-cdt-mcp`. Similarly to the owner stack tool, it receives a `uid` of a specific component and returns structural parent stack. For every node, it includes `uid`, `name`, `type`.
1 parent 7ce677d commit 3508aee

7 files changed

Lines changed: 323 additions & 20 deletions

File tree

packages/react-devtools-cdt-mcp/README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,24 @@ Raw owner stack trace — the chain of JSX creation locations up to the root.
8989
- **Input:** `uid` (string, required).
9090
- **Output:** `{stack: string}` (DEV-only; empty in production).
9191

92+
### `react_get_parent_stack`
93+
94+
Rendered parent list — where a component is mounted in the rendered component
95+
tree.
96+
97+
- **Input:** `uid` (string, required).
98+
- **Output:** an array of `{uid, name, type}`, ordered from immediate parent to
99+
root (empty for the root). This can include host DOM components and the root.
100+
92101
### `react_get_owner_stack`
93102

94-
Structured owner list — which components rendered this one.
103+
Structured owner list — which components created/rendered this element through
104+
JSX.
95105

96106
- **Input:** `uid` (string, required).
97107
- **Output:** an array of `{uid, name, type}`, ordered from immediate owner to
98-
root ancestor (empty for a root component). DEV-only.
108+
root owner (empty for a root component). DEV-only. Owners are not structural
109+
parents; use `react_get_parent_stack` for mounted tree ancestry.
99110

100111
### `react_start_profiling`
101112

packages/react-devtools-cdt-mcp/e2e/run.flow.js

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@ type OwnersStackResult = {
110110
stack: string,
111111
...
112112
};
113-
type Owner = {
113+
type ComponentBranchEntry = {
114+
uid: string,
114115
name: string,
116+
type: string,
115117
...
116118
};
117119
type ErrorPayload = {
@@ -146,6 +148,7 @@ const TOOL_NAMES = [
146148
'react_find_components',
147149
'react_get_component_source',
148150
'react_get_owner_stack_trace',
151+
'react_get_parent_stack',
149152
'react_get_owner_stack',
150153
'react_start_profiling',
151154
'react_stop_profiling',
@@ -665,17 +668,29 @@ function parseNamedObject(value: mixed, message: string): {name: string, ...} {
665668
};
666669
}
667670

671+
function parseComponentBranchEntry(
672+
value: mixed,
673+
message: string
674+
): ComponentBranchEntry {
675+
const object = expectObject(value, message);
676+
return {
677+
uid: expectString(object.uid, `${message} uid`),
678+
name: expectString(object.name, `${message} name`),
679+
type: expectString(object.type, `${message} type`),
680+
};
681+
}
682+
668683
function parseComponentDetails(value: mixed): ComponentDetails {
669684
const object = expectObject(value, 'Expected component details object');
670-
return {
671-
...object,
685+
const details: ComponentDetails = {
672686
name: expectString(object.name, 'Expected component details name'),
673687
type: expectString(object.type, 'Expected component details type'),
674688
hooks: expectArray(object.hooks, 'Expected component details hooks').map(
675689
(hook, index) =>
676690
parseNamedObject(hook, `Expected component hook ${index}`)
677691
),
678692
};
693+
return details;
679694
}
680695

681696
function parseComponentType(value: mixed): string {
@@ -775,9 +790,13 @@ function parseOwnersStack(value: mixed): OwnersStackResult {
775790
};
776791
}
777792

778-
function parseOwnersBranch(value: mixed): Array<Owner> {
779-
return expectArray(value, 'Expected owners branch array').map(
780-
(owner, index) => parseNamedObject(owner, `Expected owner ${index}`)
793+
function parseComponentBranch(
794+
value: mixed,
795+
label: string
796+
): Array<ComponentBranchEntry> {
797+
return expectArray(value, `Expected ${label} branch array`).map(
798+
(entry, index) =>
799+
parseComponentBranchEntry(entry, `Expected ${label} ${index}`)
781800
);
782801
}
783802

@@ -989,6 +1008,31 @@ async function runE2E(chrome: Chrome, appUrl: string): Promise<void> {
9891008
node => node.name === 'Todo' && node.type === 'function',
9901009
'Expected function component Todo'
9911010
);
1011+
const todoList = findNode(
1012+
tree,
1013+
node => node.name === 'TodoList' && node.type === 'function',
1014+
'Expected function component TodoList'
1015+
);
1016+
const todoListHost = findNode(
1017+
tree,
1018+
node => node.name === 'ul' && node.type === 'host',
1019+
'Expected host ul for TodoList'
1020+
);
1021+
const mainNode = findNode(
1022+
tree,
1023+
node => node.name === 'main' && node.type === 'host',
1024+
'Expected host main'
1025+
);
1026+
const app = findNode(
1027+
tree,
1028+
node => node.name === 'App' && node.type === 'function',
1029+
'Expected function component App'
1030+
);
1031+
const root = findNode(
1032+
tree,
1033+
node => node.type === 'root',
1034+
'Expected root node'
1035+
);
9921036
const memoBox = findNode(
9931037
tree,
9941038
node => node.name.includes('MemoBox') && node.type === 'memo',
@@ -1069,7 +1113,7 @@ async function runE2E(chrome: Chrome, appUrl: string): Promise<void> {
10691113
assert.strictEqual(domLookup.type, 'host');
10701114
assert.strictEqual(domLookup.name, 'button');
10711115

1072-
log('Checking source, owners, and error payloads...');
1116+
log('Checking source, parents, owners, and error payloads...');
10731117
const source = parseSourceResult(
10741118
await callTool('react_get_component_source', {
10751119
uid: counter.uid,
@@ -1088,10 +1132,45 @@ async function runE2E(chrome: Chrome, appUrl: string): Promise<void> {
10881132
);
10891133
}
10901134

1091-
const ownersBranch = parseOwnersBranch(
1135+
const parentsBranch = parseComponentBranch(
1136+
await callTool('react_get_parent_stack', {
1137+
uid: todo.uid,
1138+
}),
1139+
'parents'
1140+
);
1141+
assert.deepStrictEqual(parentsBranch, [
1142+
{
1143+
uid: todoListHost.uid,
1144+
name: todoListHost.name,
1145+
type: todoListHost.type,
1146+
},
1147+
{
1148+
uid: todoList.uid,
1149+
name: todoList.name,
1150+
type: todoList.type,
1151+
},
1152+
{
1153+
uid: mainNode.uid,
1154+
name: mainNode.name,
1155+
type: mainNode.type,
1156+
},
1157+
{
1158+
uid: app.uid,
1159+
name: app.name,
1160+
type: app.type,
1161+
},
1162+
{
1163+
uid: root.uid,
1164+
name: root.name,
1165+
type: root.type,
1166+
},
1167+
]);
1168+
1169+
const ownersBranch = parseComponentBranch(
10921170
await callTool('react_get_owner_stack', {
10931171
uid: todo.uid,
1094-
})
1172+
}),
1173+
'owners'
10951174
);
10961175
assert(
10971176
ownersBranch.some(owner => owner.name === 'TodoList'),

packages/react-devtools-cdt-mcp/src/DevToolsCdtMcp.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,12 +191,31 @@ const TOOL_DEFINITIONS: Array<ToolDefinition> = [
191191
},
192192
call: (tools, args) => tools.getOwnerStackTrace(args.uid),
193193
},
194+
{
195+
name: 'react_get_parent_stack',
196+
description:
197+
'Rendered parent chain for a component, from immediate parent to root: ' +
198+
'an array of {uid, name, type}. Parents describe where the node is ' +
199+
'mounted in the rendered component tree and may include host DOM ' +
200+
'components and the root. This differs from owners, which describe JSX ' +
201+
'creation/render ownership.',
202+
inputSchema: {
203+
type: 'object',
204+
properties: {
205+
uid: {type: 'string', description: 'Component uid, e.g. "r5".'},
206+
},
207+
required: ['uid'],
208+
},
209+
call: (tools, args) => tools.getParentStack(args.uid),
210+
},
194211
{
195212
name: 'react_get_owner_stack',
196213
description:
197-
'Structured owner chain for a component, from immediate owner to root ' +
198-
'ancestor: an array of {uid, name, type} (empty for a root ' +
199-
'component). DEV-only.',
214+
'JSX owner chain for a component, from immediate owner to root owner: ' +
215+
'an array of {uid, name, type} (empty for a root component). Owners ' +
216+
'describe which components created/rendered this element through JSX, ' +
217+
'not where it is mounted in the rendered component tree. This DEV-only metadata ' +
218+
'differs from structural parents.',
200219
inputSchema: {
201220
type: 'object',
202221
properties: {

packages/react-devtools-cdt-mcp/src/__tests__/DevToolsCdtMcp-test.js

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const TOOL_NAMES = [
1414
'react_find_components',
1515
'react_get_component_source',
1616
'react_get_owner_stack_trace',
17+
'react_get_parent_stack',
1718
'react_get_owner_stack',
1819
'react_start_profiling',
1920
'react_stop_profiling',
@@ -105,6 +106,12 @@ describe('react-devtools-cdt-mcp', () => {
105106
expect(tool.inputSchema.type).toBe('object');
106107
expect(typeof tool.execute).toBe('function');
107108
});
109+
expect(getTool('react_get_parent_stack').description).toEqual(
110+
expect.stringContaining('Rendered parent chain'),
111+
);
112+
expect(getTool('react_get_owner_stack').description).toEqual(
113+
expect.stringContaining('Owners describe'),
114+
);
108115
});
109116

110117
it('declares JSON-Schema input with required params', () => {
@@ -144,6 +151,16 @@ describe('react-devtools-cdt-mcp', () => {
144151
},
145152
required: ['name'],
146153
});
154+
expect(getTool('react_get_parent_stack').inputSchema).toEqual({
155+
type: 'object',
156+
properties: {uid: {type: 'string', description: expect.any(String)}},
157+
required: ['uid'],
158+
});
159+
expect(getTool('react_get_owner_stack').inputSchema).toEqual({
160+
type: 'object',
161+
properties: {uid: {type: 'string', description: expect.any(String)}},
162+
required: ['uid'],
163+
});
147164
expect(getTool('react_start_profiling').inputSchema).toEqual({
148165
type: 'object',
149166
properties: {
@@ -174,8 +191,8 @@ describe('react-devtools-cdt-mcp', () => {
174191
});
175192

176193
const result = getTool('react_get_component_tree').execute({});
177-
// Uids are assigned deterministically as fibers are first encountered. A
178-
// fiber's first child is assigned before the fiber itself, so App (the host
194+
// Uids are assigned deterministically as nodes are first encountered. A
195+
// node's first child is assigned before the node itself, so App (the host
179196
// root's first child) is r0, the host root is r1, and the div is r2.
180197
expect(result).toEqual({
181198
nodes: [
@@ -320,6 +337,34 @@ describe('react-devtools-cdt-mcp', () => {
320337
});
321338
});
322339

340+
it('react_get_parent_stack returns structural ancestors', () => {
341+
function Child() {
342+
return <span>leaf</span>;
343+
}
344+
function Owner() {
345+
return (
346+
<section>
347+
<Child />
348+
</section>
349+
);
350+
}
351+
352+
act(() => {
353+
ReactDOMClient.createRoot(container).render(<Owner />);
354+
});
355+
356+
const tree = getTool('react_get_component_tree').execute({}).nodes;
357+
const child = tree.find(n => n.name === 'Child');
358+
359+
expect(getTool('react_get_parent_stack').execute({uid: child.uid})).toEqual(
360+
[
361+
{uid: 'r2', name: 'section', type: 'host'},
362+
{uid: 'r0', name: 'Owner', type: 'function'},
363+
{uid: 'r1', name: expect.any(String), type: 'root'},
364+
],
365+
);
366+
});
367+
323368
it('react_get_component_by_dom_element returns the DOM element component', () => {
324369
function Wrapper({children}) {
325370
return <section className="wrap">{children}</section>;
@@ -428,7 +473,7 @@ describe('react-devtools-cdt-mcp', () => {
428473
expect(overview).toHaveLength(1);
429474
expect(overview[0].commit).toBe(0);
430475

431-
// The commit report lists the fibers that rendered, sorted by actualDuration
476+
// The commit report lists the components that rendered, sorted by actualDuration
432477
// descending, with uids assigned in commit-walk order: the host root r0
433478
// (widest duration), then Counter r1, then the div r2. Durations are
434479
// timing-dependent, so assert identity (uid/name/type) only.

packages/react-devtools-facade/src/DevToolsFacadeTools.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
NodeInfo,
1414
ComponentSource,
1515
OwnersStack,
16+
ParentEntry,
1617
OwnerEntry,
1718
FindComponentsResult,
1819
ToolError,
@@ -34,6 +35,8 @@ export type {
3435
ComponentSource,
3536
SourceLocation,
3637
OwnersStack,
38+
ComponentBranchEntry,
39+
ParentEntry,
3740
OwnerEntry,
3841
FindComponentsResult,
3942
ToolError,
@@ -68,6 +71,7 @@ export type Tools = {
6871
) => FindComponentsResult | ToolError,
6972
getComponentSource: (uid: string) => ComponentSource | ToolError,
7073
getOwnerStackTrace: (uid: string) => OwnersStack | ToolError,
74+
getParentStack: (uid: string) => Array<ParentEntry> | ToolError,
7175
getOwnerStack: (uid: string) => Array<OwnerEntry> | ToolError,
7276
startProfiling: (traceName?: string) => StartProfilingResult | ToolError,
7377
stopProfiling: () => StopProfilingResult | ToolError,
@@ -102,6 +106,7 @@ export function createTools(facade: Facade): Tools {
102106
findComponents: tree.findComponents,
103107
getComponentSource: tree.getComponentSource,
104108
getOwnerStackTrace: tree.getOwnerStackTrace,
109+
getParentStack: tree.getParentStack,
105110
getOwnerStack: tree.getOwnerStack,
106111
startProfiling: profiler.startProfiling,
107112
stopProfiling: profiler.stopProfiling,

0 commit comments

Comments
 (0)