Skip to content
Closed
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: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v18.20.1
v20.19.0
2 changes: 1 addition & 1 deletion compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"prettier": "^3.3.3",
"prettier-plugin-hermes-parser": "^0.26.0",
"prompt-promise": "^1.0.3",
"rimraf": "^5.0.10",
"rimraf": "^6.0.1",
"to-fast-properties": "^2.0.0",
"tsup": "^8.4.0",
"typescript": "^5.4.3",
Expand Down
29 changes: 29 additions & 0 deletions compiler/packages/react-compiler-mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "react-compiler-mcp-server",
"version": "0.0.0",
"description": "React Compiler MCP Server (experimental)",
"bin": {
"react-compiler-mcp-server": "./dist/index.js"
},
"scripts": {
"build": "rimraf dist && tsup",
"test": "echo 'no tests'",
"watch": "yarn build --watch"
},
"dependencies": {
"@babel/core": "^7.26.0",
"@babel/parser": "^7.26",
"@babel/plugin-syntax-typescript": "^7.25.9",
"@babel/types": "^7.26.0",
"@modelcontextprotocol/sdk": "^1.9.0",
"prettier": "^3.3.3",
"zod": "^3.23.8"
},
"devDependencies": {},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/react.git",
"directory": "compiler/packages/react-compiler-mcp-server"
}
}
66 changes: 66 additions & 0 deletions compiler/packages/react-compiler-mcp-server/src/compiler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import type * as BabelCore from '@babel/core';
import {parseAsync, transformFromAstAsync} from '@babel/core';
import BabelPluginReactCompiler, {
type PluginOptions,
} from 'babel-plugin-react-compiler/src';
import * as babelParser from 'prettier/plugins/babel.js';
import estreeParser from 'prettier/plugins/estree';
import * as typescriptParser from 'prettier/plugins/typescript';
import * as prettier from 'prettier/standalone';

export let lastResult: BabelCore.BabelFileResult | null = null;

type CompileOptions = {
text: string;
file: string;
options: Partial<PluginOptions> | null;
};
export async function compile({
text,
file,
options,
}: CompileOptions): Promise<BabelCore.BabelFileResult> {
const ast = await parseAsync(text, {
sourceFileName: file,
parserOpts: {
plugins: ['typescript', 'jsx'],
},
sourceType: 'module',
});
if (ast == null) {
throw new Error('Could not parse');
}
const plugins =
options != null
? [[BabelPluginReactCompiler, options]]
: [[BabelPluginReactCompiler]];
const result = await transformFromAstAsync(ast, text, {
filename: file,
highlightCode: false,
retainLines: true,
plugins,
sourceType: 'module',
sourceFileName: file,
});
if (result?.code == null) {
throw new Error(
`Expected BabelPluginReactCompiler to compile successfully, got ${result}`,
);
}
result.code = await prettier.format(result.code, {
semi: false,
parser: 'babel-ts',
plugins: [babelParser, estreeParser, typescriptParser],
});
if (result.code != null) {
lastResult = result;
}
return result;
}
151 changes: 151 additions & 0 deletions compiler/packages/react-compiler-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
import {z} from 'zod';
import {compile} from './compiler';
import {
CompilerPipelineValue,
printReactiveFunctionWithOutlined,
printFunctionWithOutlined,
} from 'babel-plugin-react-compiler/src';
import {type CallToolResult} from '@modelcontextprotocol/sdk/types.js';

export type PrintedCompilerPipelineValue =
| {
kind: 'hir';
name: string;
fnName: string | null;
value: string;
}
| {kind: 'reactive'; name: string; fnName: string | null; value: string}
| {kind: 'debug'; name: string; fnName: string | null; value: string};

const server = new McpServer({
name: 'React Compiler',
version: '0.0.0',
capabilities: {
resources: {},
tools: {},
},
});

server.tool(
'analyze',
'Use React Compiler to analyze React code',
{
text: z.string(),
passName: z.string().optional(),
},
async ({text, passName}) => {
const pipelinePasses = new Map<
string,
Array<PrintedCompilerPipelineValue>
>();
const recordPass: (
result: PrintedCompilerPipelineValue,
) => void = result => {
const entry = pipelinePasses.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
pipelinePasses.set(result.name, [result]);
}
};
const logIR = (result: CompilerPipelineValue): void => {
switch (result.kind) {
case 'ast': {
break;
}
case 'hir': {
recordPass({
kind: 'hir',
fnName: result.value.id,
name: result.name,
value: printFunctionWithOutlined(result.value),
});
break;
}
case 'reactive': {
recordPass({
kind: 'reactive',
fnName: result.value.id,
name: result.name,
value: printReactiveFunctionWithOutlined(result.value),
});
break;
}
case 'debug': {
recordPass({
kind: 'debug',
fnName: null,
name: result.name,
value: result.value,
});
break;
}
default: {
const _: never = result;
throw new Error(`Unhandled result ${result}`);
}
}
};
const compilerOptions = {
logger: {
debugLogIRs: logIR,
logEvent: () => {},
},
};
try {
const result = await compile({
text,
file: 'anonymous.tsx',
options: compilerOptions,
});
if (result.code == null) {
return {
isError: true,
content: [{type: 'text', text: 'Error: Could not compile'}],
};
}
const requestedPasses: Array<{type: 'text'; text: string}> = [];
if (passName != null) {
const requestedPass = pipelinePasses.get(passName);
if (requestedPass !== undefined) {
for (const pipelineValue of requestedPass) {
if (pipelineValue.name === passName) {
requestedPasses.push({
type: 'text',
text: pipelineValue.value,
});
}
}
}
}
return {
content: [{type: 'text', text: result.code}, ...requestedPasses],
};
} catch (err) {
return {
isError: true,
content: [{type: 'text', text: `Error: ${err}`}],
};
}
},
);

async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('React Compiler MCP Server running on stdio');
}

main().catch(error => {
console.error('Fatal error in main():', error);
process.exit(1);
});
22 changes: 22 additions & 0 deletions compiler/packages/react-compiler-mcp-server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
"module": "Node16",
"moduleResolution": "Node16",
"rootDir": "../",
"noEmit": true,
"jsx": "react-jsxdev",
"lib": ["ES2022"],

// weaken strictness from preset
"importsNotUsedAsValues": "remove",
"noUncheckedIndexedAccess": false,
"noUnusedParameters": false,
"useUnknownInCatchVariables": false,
"target": "ES2022",
// ideally turn off only during dev, or on a per-file basis
"noUnusedLocals": false,
},
"exclude": ["node_modules"],
"include": ["src/**/*.ts"],
}
28 changes: 28 additions & 0 deletions compiler/packages/react-compiler-mcp-server/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {defineConfig} from 'tsup';

export default defineConfig({
entry: ['./src/index.ts'],
outDir: './dist',
external: [],
splitting: false,
sourcemap: false,
dts: false,
bundle: true,
format: 'cjs',
platform: 'node',
target: 'es2022',
banner: {
js: `/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @lightSyntaxTransform
* @noflow
* @nolint
* @preventMunge
* @preserve-invariant-messages
*/`,
},
});
Loading
Loading