-
Notifications
You must be signed in to change notification settings - Fork 50.3k
[mcp] Add MCP tool to print out the component tree of the currently open React App #33305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 20 commits
0e5c79c
8fa3dfc
a75932b
76dddd1
94718f1
2852c9d
26315d6
d6d929e
789e5f0
a85b0b0
049bfbb
c5ab27a
1e4614b
ab86a5e
81c3a53
183bd4f
9cae1ce
9275c83
6c71a77
df0a663
a586117
ecb1861
ca1d5e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import puppeteer from 'puppeteer'; | ||
|
|
||
| export async function parseReactComponentTree(url: string): Promise<string> { | ||
| try { | ||
| const browser = await puppeteer.connect({ | ||
| browserURL: 'http://127.0.0.1:9222', | ||
| defaultViewport: null, | ||
| }); | ||
|
|
||
| const pages = await browser.pages(); | ||
|
|
||
| let localhostPage = null; | ||
| for (const page of pages) { | ||
| const pageUrl = await page.url(); | ||
|
|
||
| if (pageUrl.startsWith(url)) { | ||
| localhostPage = page; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (localhostPage) { | ||
| const componentTree = await localhostPage.evaluate(() => { | ||
| return (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces | ||
| .get(1) | ||
| .__internal_only_getComponentTree(); | ||
| }); | ||
|
|
||
| return componentTree; | ||
| } else { | ||
| throw new Error( | ||
| `Could not open the page at ${url}. Is your server running?`, | ||
| ); | ||
| } | ||
| } catch (error) { | ||
| throw new Error('Failed extract component tree' + error); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5859,6 +5859,75 @@ export function attach( | |
| return unresolvedSource; | ||
| } | ||
|
|
||
| function __internal_only_getComponentTree(): string { | ||
| let treeString = ''; | ||
|
|
||
| function buildTreeString( | ||
| instance: DevToolsInstance, | ||
| prefix: string = '', | ||
| isLastChild: boolean = true, | ||
| ): void { | ||
| if (!instance) return; | ||
|
|
||
| const name = | ||
| (instance.kind !== VIRTUAL_INSTANCE | ||
| ? getDisplayNameForFiber(instance.data) | ||
|
||
| : instance.data.name) || 'Unknown'; | ||
|
|
||
| const id = instance.id !== undefined ? instance.id : 'unknown'; | ||
|
|
||
| if (name !== 'createRoot()') { | ||
| treeString += | ||
| prefix + | ||
| (isLastChild ? '└── ' : '├── ') + | ||
| name + | ||
| ' (id: ' + | ||
| id + | ||
| ')\n'; | ||
| } | ||
|
|
||
| const childPrefix = prefix + (isLastChild ? ' ' : '│ '); | ||
|
|
||
| let childCount = 0; | ||
| let tempChild = instance.firstChild; | ||
| while (tempChild !== null) { | ||
| childCount++; | ||
| tempChild = tempChild.nextSibling; | ||
| } | ||
|
|
||
| let child = instance.firstChild; | ||
| let currentChildIndex = 0; | ||
|
|
||
| while (child !== null) { | ||
| currentChildIndex++; | ||
| const isLastSibling = currentChildIndex === childCount; | ||
| buildTreeString(child, childPrefix, isLastSibling); | ||
| child = child.nextSibling; | ||
| } | ||
| } | ||
|
|
||
| const rootInstances: Array<DevToolsInstance> = []; | ||
| idToDevToolsInstanceMap.forEach(instance => { | ||
| if (instance.parent === null || instance.parent.parent === null) { | ||
| rootInstances.push(instance); | ||
| } | ||
| }); | ||
|
|
||
| if (rootInstances.length > 0) { | ||
| for (let i = 0; i < rootInstances.length; i++) { | ||
| const isLast = i === rootInstances.length - 1; | ||
| buildTreeString(rootInstances[i], '', isLast); | ||
| if (!isLast) { | ||
| treeString += '\n'; | ||
| } | ||
| } | ||
| } else { | ||
| treeString = 'No component tree found.'; | ||
| } | ||
|
|
||
| return treeString; | ||
| } | ||
|
|
||
| return { | ||
| cleanup, | ||
| clearErrorsAndWarnings, | ||
|
|
@@ -5873,6 +5942,7 @@ export function attach( | |
| getNearestMountedDOMNode, | ||
| getElementIDForHostInstance, | ||
| getInstanceAndStyle, | ||
| ...(__IS_INTERNAL_MCP_BUILD__ && {__internal_only_getComponentTree}), | ||
| getOwnersList, | ||
| getPathForElement, | ||
| getProfilingData, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get(1)will usually return you the Fiber renderer, basically the client-side renderer of React.In case of RSC, there could also be another renderer. I am not sure about the order of registration, but it would probably be registered after the Fiber one.
For component tree, we probably care only about Fiber renderer, but worth keeping in mind that there could be rare cases where there are multiple renderers.