diff --git a/.eslintrc.js b/.eslintrc.js
index c1eb5b34ebe82..1bb4e868d0694 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -496,6 +496,7 @@ module.exports = {
'packages/react-devtools-shared/src/devtools/views/**/*.js',
'packages/react-devtools-shared/src/hook.js',
'packages/react-devtools-shared/src/backend/console.js',
+ 'packages/react-devtools-shared/src/backend/fiber/renderer.js',
'packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js',
'packages/react-devtools-shared/src/frontend/utils/withPermissionsCheck.js',
],
@@ -504,6 +505,7 @@ module.exports = {
__IS_FIREFOX__: 'readonly',
__IS_EDGE__: 'readonly',
__IS_NATIVE__: 'readonly',
+ __IS_INTERNAL_MCP_BUILD__: 'readonly',
__IS_INTERNAL_VERSION__: 'readonly',
chrome: 'readonly',
},
diff --git a/.prettierrc.js b/.prettierrc.js
index 37cf9c9d3a89b..aa54cbae1f9f8 100644
--- a/.prettierrc.js
+++ b/.prettierrc.js
@@ -3,13 +3,12 @@
const {esNextPaths} = require('./scripts/shared/pathsByLanguageVersion');
module.exports = {
- plugins: ['prettier-plugin-hermes-parser'],
bracketSpacing: false,
singleQuote: true,
bracketSameLine: true,
trailingComma: 'es5',
printWidth: 80,
- parser: 'hermes',
+ parser: 'flow',
arrowParens: 'avoid',
overrides: [
{
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.expect.md
index 171a57f3ea5ed..6e99be2e6f4cc 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.expect.md
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.expect.md
@@ -3,7 +3,7 @@
```javascript
function ternary(props) {
- const a = props.a && props.b ? props.c || props.d : props.e ?? props.f;
+ const a = props.a && props.b ? props.c || props.d : (props.e ?? props.f);
const b = props.a ? (props.b && props.c ? props.d : props.e) : props.f;
return a ? b : null;
}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.js
index 8a741ccb12f25..2a39d90bbcd6d 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.js
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ternary-expression.js
@@ -1,5 +1,5 @@
function ternary(props) {
- const a = props.a && props.b ? props.c || props.d : props.e ?? props.f;
+ const a = props.a && props.b ? props.c || props.d : (props.e ?? props.f);
const b = props.a ? (props.b && props.c ? props.d : props.e) : props.f;
return a ? b : null;
}
diff --git a/compiler/packages/react-mcp-server/src/index.ts b/compiler/packages/react-mcp-server/src/index.ts
index 2ec747eac4dfd..2871027d64ce3 100644
--- a/compiler/packages/react-mcp-server/src/index.ts
+++ b/compiler/packages/react-mcp-server/src/index.ts
@@ -21,6 +21,7 @@ import {queryAlgolia} from './utils/algolia';
import assertExhaustive from './utils/assertExhaustive';
import {convert} from 'html-to-text';
import {measurePerformance} from './tools/runtimePerf';
+import {parseReactComponentTree} from './tools/componentTree';
function calculateMean(values: number[]): string {
return values.length > 0
@@ -366,6 +367,45 @@ ${calculateMean(results.renderTime)}
},
);
+server.tool(
+ 'parse-react-component-tree',
+ `
+ This tool gets the component tree of a React App.
+ passing in a url will attempt to connect to the browser and get the current state of the component tree. If no url is passed in,
+ the default url will be used (http://localhost:3000).
+
+
+ - The url should be a full url with the protocol (http:// or https://) and the domain name (e.g. localhost:3000).
+ - Also the user should be running a Chrome browser running on debug mode on port 9222. If you receive an error message, advise the user to run
+ the following comand in the terminal:
+ MacOS: "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome"
+ Windows: "chrome.exe --remote-debugging-port=9222 --user-data-dir=C:\temp\chrome"
+
+ `,
+ {
+ url: z.string().optional().default('http://localhost:3000'),
+ },
+ async ({url}) => {
+ try {
+ const componentTree = await parseReactComponentTree(url);
+
+ return {
+ content: [
+ {
+ type: 'text' as const,
+ text: componentTree,
+ },
+ ],
+ };
+ } catch (err) {
+ return {
+ isError: true,
+ content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
+ };
+ }
+ },
+);
+
server.prompt('review-react-code', () => ({
messages: [
{
diff --git a/compiler/packages/react-mcp-server/src/tools/componentTree.ts b/compiler/packages/react-mcp-server/src/tools/componentTree.ts
new file mode 100644
index 0000000000000..a124066a9424e
--- /dev/null
+++ b/compiler/packages/react-mcp-server/src/tools/componentTree.ts
@@ -0,0 +1,38 @@
+import puppeteer from 'puppeteer';
+
+export async function parseReactComponentTree(url: string): Promise {
+ 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);
+ }
+}
diff --git a/package.json b/package.json
index 875986ebdaf6a..6ad9211568541 100644
--- a/package.json
+++ b/package.json
@@ -91,7 +91,6 @@
"ncp": "^2.0.0",
"prettier": "^3.3.3",
"prettier-2": "npm:prettier@^2",
- "prettier-plugin-hermes-parser": "^0.23.0",
"pretty-format": "^29.4.1",
"prop-types": "^15.6.2",
"random-seed": "^0.3.0",
diff --git a/packages/react-devtools-core/webpack.backend.js b/packages/react-devtools-core/webpack.backend.js
index 32d4fadcb5884..c1312fc6d8ec8 100644
--- a/packages/react-devtools-core/webpack.backend.js
+++ b/packages/react-devtools-core/webpack.backend.js
@@ -72,6 +72,7 @@ module.exports = {
__IS_CHROME__: false,
__IS_EDGE__: false,
__IS_NATIVE__: true,
+ __IS_INTERNAL_MCP_BUILD__: false,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
diff --git a/packages/react-devtools-core/webpack.standalone.js b/packages/react-devtools-core/webpack.standalone.js
index 8caadec10b070..6a9636c6911b1 100644
--- a/packages/react-devtools-core/webpack.standalone.js
+++ b/packages/react-devtools-core/webpack.standalone.js
@@ -91,6 +91,7 @@ module.exports = {
__IS_FIREFOX__: false,
__IS_CHROME__: false,
__IS_EDGE__: false,
+ __IS_INTERNAL_MCP_BUILD__: false,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
diff --git a/packages/react-devtools-extensions/webpack.backend.js b/packages/react-devtools-extensions/webpack.backend.js
index effa6cc330bb0..4bfa05183067e 100644
--- a/packages/react-devtools-extensions/webpack.backend.js
+++ b/packages/react-devtools-extensions/webpack.backend.js
@@ -78,6 +78,7 @@ module.exports = {
__IS_FIREFOX__: IS_FIREFOX,
__IS_EDGE__: IS_EDGE,
__IS_NATIVE__: false,
+ __IS_INTERNAL_MCP_BUILD__: false,
}),
new Webpack.SourceMapDevToolPlugin({
filename: '[file].map',
diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js
index 51b8f4e2105e3..4a3052517c851 100644
--- a/packages/react-devtools-extensions/webpack.config.js
+++ b/packages/react-devtools-extensions/webpack.config.js
@@ -33,6 +33,8 @@ const IS_FIREFOX = process.env.IS_FIREFOX === 'true';
const IS_EDGE = process.env.IS_EDGE === 'true';
const IS_INTERNAL_VERSION = process.env.FEATURE_FLAG_TARGET === 'extension-fb';
+const IS_INTERNAL_MCP_BUILD = process.env.IS_INTERNAL_MCP_BUILD === 'true';
+
const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
const babelOptions = {
@@ -113,6 +115,7 @@ module.exports = {
__IS_FIREFOX__: IS_FIREFOX,
__IS_EDGE__: IS_EDGE,
__IS_NATIVE__: false,
+ __IS_INTERNAL_MCP_BUILD__: IS_INTERNAL_MCP_BUILD,
__IS_INTERNAL_VERSION__: IS_INTERNAL_VERSION,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
diff --git a/packages/react-devtools-fusebox/webpack.config.frontend.js b/packages/react-devtools-fusebox/webpack.config.frontend.js
index ab7906ca84d63..ea04f4dad2d0d 100644
--- a/packages/react-devtools-fusebox/webpack.config.frontend.js
+++ b/packages/react-devtools-fusebox/webpack.config.frontend.js
@@ -86,6 +86,7 @@ module.exports = {
__IS_CHROME__: false,
__IS_FIREFOX__: false,
__IS_EDGE__: false,
+ __IS_INTERNAL_MCP_BUILD__: false,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-fusebox"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js
index 3a92dff1f2195..9fa900dfa65f2 100644
--- a/packages/react-devtools-inline/webpack.config.js
+++ b/packages/react-devtools-inline/webpack.config.js
@@ -78,6 +78,7 @@ module.exports = {
__IS_FIREFOX__: false,
__IS_EDGE__: false,
__IS_NATIVE__: false,
+ __IS_INTERNAL_MCP_BUILD__: false,
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-inline"`,
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js
index 4fc59a24d9272..94246df6485e4 100644
--- a/packages/react-devtools-shared/src/backend/fiber/renderer.js
+++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js
@@ -5859,6 +5859,86 @@ export function attach(
return unresolvedSource;
}
+ type InternalMcpFunctions = {
+ __internal_only_getComponentTree?: Function,
+ };
+
+ const internalMcpFunctions: InternalMcpFunctions = {};
+ if (__IS_INTERNAL_MCP_BUILD__) {
+ // eslint-disable-next-line no-inner-declarations
+ 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 = [];
+ 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;
+ }
+
+ internalMcpFunctions.__internal_only_getComponentTree =
+ __internal_only_getComponentTree;
+ }
+
return {
cleanup,
clearErrorsAndWarnings,
@@ -5898,5 +5978,6 @@ export function attach(
storeAsGlobal,
updateComponentFilters,
getEnvironmentNames,
+ ...internalMcpFunctions,
};
}
diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/types.js b/packages/react-devtools-shared/src/devtools/ContextMenu/types.js
index c2f296db10fe5..9fda9be199941 100644
--- a/packages/react-devtools-shared/src/devtools/ContextMenu/types.js
+++ b/packages/react-devtools-shared/src/devtools/ContextMenu/types.js
@@ -25,7 +25,7 @@ export type ContextMenuHandle = {
hide(): void,
};
-/*::
-export type ContextMenuComponent = component(ref: React$RefSetter);
-*/
+export type ContextMenuComponent = component(
+ ref: React$RefSetter,
+);
export type ContextMenuRef = {current: ContextMenuHandle | null};
diff --git a/packages/react-devtools-shared/src/hooks/astUtils.js b/packages/react-devtools-shared/src/hooks/astUtils.js
index d2d2088349e0c..13adcc0e36639 100644
--- a/packages/react-devtools-shared/src/hooks/astUtils.js
+++ b/packages/react-devtools-shared/src/hooks/astUtils.js
@@ -289,7 +289,7 @@ function getHookVariableName(
const nodeType = hook.node.id.type;
switch (nodeType) {
case AST_NODE_TYPES.ARRAY_PATTERN:
- return !isCustomHook ? hook.node.id.elements[0]?.name ?? null : null;
+ return !isCustomHook ? (hook.node.id.elements[0]?.name ?? null) : null;
case AST_NODE_TYPES.IDENTIFIER:
return hook.node.id.name;
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index 6fbad9adac195..37d3e5fd12a11 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -253,7 +253,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
id: instance.id,
type: type,
parent: instance.parent,
- children: keepChildren ? instance.children : children ?? [],
+ children: keepChildren ? instance.children : (children ?? []),
text: shouldSetTextContent(type, newProps)
? computeText((newProps.children: any) + '', instance.context)
: null,
diff --git a/scripts/flow/config/flowconfig b/scripts/flow/config/flowconfig
index bd5092f1580a8..5fa574b50f74c 100644
--- a/scripts/flow/config/flowconfig
+++ b/scripts/flow/config/flowconfig
@@ -14,8 +14,6 @@
.*/__mocks__/.*
.*/__tests__/.*
-# contains modern flow syntax that requires a Flow upgrade
-.*/node_modules/prettier-plugin-hermes-parser/.*
# TODO: noop should get its own inlinedHostConfig entry
.*/packages/react-noop-renderer/.*
diff --git a/scripts/flow/react-devtools.js b/scripts/flow/react-devtools.js
index 4e0f2a915ede6..09a251bbe2f5f 100644
--- a/scripts/flow/react-devtools.js
+++ b/scripts/flow/react-devtools.js
@@ -16,5 +16,6 @@ declare const __IS_FIREFOX__: boolean;
declare const __IS_CHROME__: boolean;
declare const __IS_EDGE__: boolean;
declare const __IS_NATIVE__: boolean;
+declare const __IS_INTERNAL_MCP_BUILD__: boolean;
declare const chrome: any;
diff --git a/scripts/jest/devtools/setupEnv.js b/scripts/jest/devtools/setupEnv.js
index a797c0951435f..32bf13e686c77 100644
--- a/scripts/jest/devtools/setupEnv.js
+++ b/scripts/jest/devtools/setupEnv.js
@@ -15,6 +15,7 @@ global.__IS_FIREFOX__ = false;
global.__IS_CHROME__ = false;
global.__IS_EDGE__ = false;
global.__IS_NATIVE__ = false;
+global.__IS_INTERNAL_MCP_BUILD__ = false;
const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion;
diff --git a/yarn.lock b/yarn.lock
index 0496eb7844560..c2780d78f0ac5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9975,11 +9975,6 @@ hermes-eslint@^0.25.1:
hermes-estree "0.25.1"
hermes-parser "0.25.1"
-hermes-estree@0.23.0:
- version "0.23.0"
- resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.23.0.tgz#89c5419877b9d6bce4bb616821f496f5c5daddbc"
- integrity sha512-Rkp0PNLGpORw4ktsttkVbpYJbrYKS3hAnkxu8D9nvQi6LvSbuPa+tYw/t2u3Gjc35lYd/k95YkjqyTcN4zspag==
-
hermes-estree@0.23.1:
version "0.23.1"
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.23.1.tgz#d0bac369a030188120ee7024926aabe5a9f84fdb"
@@ -9990,13 +9985,6 @@ hermes-estree@0.25.1:
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
-hermes-parser@0.23.0:
- version "0.23.0"
- resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.23.0.tgz#3541907b77ca9e94fd093e8ef0ff97ca5340dee8"
- integrity sha512-xLwM4ylfHGwrm+2qXfO1JT/fnqEDGSnpS/9hQ4VLtqTexSviu2ZpBgz07U8jVtndq67qdb/ps0qvaWDZ3fkTyg==
- dependencies:
- hermes-estree "0.23.0"
-
hermes-parser@0.23.1:
version "0.23.1"
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.23.1.tgz#e5de648e664f3b3d84d01b48fc7ab164f4b68205"
@@ -14088,15 +14076,6 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
-prettier-plugin-hermes-parser@0.23.0, prettier-plugin-hermes-parser@^0.23.0:
- version "0.23.0"
- resolved "https://registry.yarnpkg.com/prettier-plugin-hermes-parser/-/prettier-plugin-hermes-parser-0.23.0.tgz#67fa061e503600087169283e150bc3f3239bf39c"
- integrity sha512-EMwgZFcKDyVfUCvIy/kxVc4siYEOYPt7lLqtaELVadKYNbOLUFjYW3QKGZ8jzidUy4DonfFbi/hJOXJ5vfRzxA==
- dependencies:
- hermes-estree "0.23.0"
- hermes-parser "0.23.0"
- prettier-plugin-hermes-parser "0.23.0"
-
prettier@*, prettier@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"