Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
Expand All @@ -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',
},
Expand Down
3 changes: 1 addition & 2 deletions .prettierrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
40 changes: 40 additions & 0 deletions compiler/packages/react-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).

<requirements>
- 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"
</requirements>
`,
{
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: [
{
Expand Down
38 changes: 38 additions & 0 deletions compiler/packages/react-mcp-server/src/tools/componentTree.ts
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);
}
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/react-devtools-core/webpack.backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}"`,
Expand Down
1 change: 1 addition & 0 deletions packages/react-devtools-core/webpack.standalone.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/react-devtools-extensions/webpack.backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions packages/react-devtools-extensions/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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}"`,
Expand Down
1 change: 1 addition & 0 deletions packages/react-devtools-fusebox/webpack.config.frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/react-devtools-inline/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
81 changes: 81 additions & 0 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<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;
}

internalMcpFunctions.__internal_only_getComponentTree =
__internal_only_getComponentTree;
}

return {
cleanup,
clearErrorsAndWarnings,
Expand Down Expand Up @@ -5898,5 +5978,6 @@ export function attach(
storeAsGlobal,
updateComponentFilters,
getEnvironmentNames,
...internalMcpFunctions,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export type ContextMenuHandle = {
hide(): void,
};

/*::
export type ContextMenuComponent = component(ref: React$RefSetter<ContextMenuHandle>);
*/
export type ContextMenuComponent = component(
ref: React$RefSetter<ContextMenuHandle>,
);
export type ContextMenuRef = {current: ContextMenuHandle | null};
2 changes: 1 addition & 1 deletion packages/react-devtools-shared/src/hooks/astUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 0 additions & 2 deletions scripts/flow/config/flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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/.*
Expand Down
1 change: 1 addition & 0 deletions scripts/flow/react-devtools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
1 change: 1 addition & 0 deletions scripts/jest/devtools/setupEnv.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
21 changes: 0 additions & 21 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9975,11 +9975,6 @@ hermes-eslint@^0.25.1:
hermes-estree "0.25.1"
hermes-parser "0.25.1"

[email protected]:
version "0.23.0"
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.23.0.tgz#89c5419877b9d6bce4bb616821f496f5c5daddbc"
integrity sha512-Rkp0PNLGpORw4ktsttkVbpYJbrYKS3hAnkxu8D9nvQi6LvSbuPa+tYw/t2u3Gjc35lYd/k95YkjqyTcN4zspag==

[email protected]:
version "0.23.1"
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.23.1.tgz#d0bac369a030188120ee7024926aabe5a9f84fdb"
Expand All @@ -9990,13 +9985,6 @@ [email protected]:
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==

[email protected]:
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"

[email protected]:
version "0.23.1"
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.23.1.tgz#e5de648e664f3b3d84d01b48fc7ab164f4b68205"
Expand Down Expand Up @@ -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==

[email protected], 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"
Expand Down
Loading