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"