mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Reformat code to modularize tools
This commit is contained in:
@@ -8,20 +8,11 @@
|
||||
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {z} from 'zod';
|
||||
import {compile, type PrintedCompilerPipelineValue} from './compiler';
|
||||
import {
|
||||
CompilerPipelineValue,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
PluginOptions,
|
||||
SourceLocation,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import * as cheerio from 'cheerio';
|
||||
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';
|
||||
import compileTool from './tools/compileTool';
|
||||
import devDocsTool from './tools/devDocsTool';
|
||||
|
||||
function calculateMean(values: number[]): string {
|
||||
return values.length > 0
|
||||
@@ -41,40 +32,29 @@ server.tool(
|
||||
query: z.string(),
|
||||
},
|
||||
async ({query}) => {
|
||||
try {
|
||||
const pages = await queryAlgolia(query);
|
||||
if (pages.length === 0) {
|
||||
const result = await devDocsTool(query);
|
||||
|
||||
switch (result.kind) {
|
||||
case 'success': {
|
||||
return {
|
||||
content: [{type: 'text' as const, text: `No results`}],
|
||||
isError: false,
|
||||
content: result.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text: text,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
const content = pages.map(html => {
|
||||
const $ = cheerio.load(html);
|
||||
// react.dev should always have at least one <article> with the main content
|
||||
const article = $('article').html();
|
||||
if (article != null) {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text: convert(article),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
// Fallback to converting the whole page to text.
|
||||
text: convert($.html()),
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
content,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: result.text}],
|
||||
};
|
||||
default:
|
||||
assertExhaustive(result, `Unhandled result ${JSON.stringify(result)}`);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -93,199 +73,47 @@ server.tool(
|
||||
passName: z.enum(['HIR', 'ReactiveFunction', 'All', '@DEBUG']).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: {
|
||||
assertExhaustive(result, `Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
|
||||
const compilerOptions: Partial<PluginOptions> = {
|
||||
panicThreshold: 'none',
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename, event): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
const detail = event.detail;
|
||||
const loc =
|
||||
detail.loc == null || typeof detail.loc == 'symbol'
|
||||
? event.fnLoc
|
||||
: detail.loc;
|
||||
errors.push({
|
||||
message: detail.reason,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const result = await compile({
|
||||
text,
|
||||
file: 'anonymous.tsx',
|
||||
options: compilerOptions,
|
||||
});
|
||||
if (result.code == null) {
|
||||
const results = await compileTool(text, passName);
|
||||
|
||||
switch (results.kind) {
|
||||
case 'success': {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: 'Error: Could not compile'}],
|
||||
};
|
||||
}
|
||||
const requestedPasses: Array<{type: 'text'; text: string}> = [];
|
||||
if (passName != null) {
|
||||
switch (passName) {
|
||||
case 'All': {
|
||||
const hir = pipelinePasses.get('PropagateScopeDependenciesHIR');
|
||||
if (hir !== undefined) {
|
||||
for (const pipelineValue of hir) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
const reactiveFunc = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (reactiveFunc !== undefined) {
|
||||
for (const pipelineValue of reactiveFunc) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'HIR': {
|
||||
// Last pass before HIR -> ReactiveFunction
|
||||
const requestedPass = pipelinePasses.get(
|
||||
'PropagateScopeDependenciesHIR',
|
||||
);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ReactiveFunction': {
|
||||
// Last pass
|
||||
const requestedPass = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '@DEBUG': {
|
||||
for (const [, pipelinePass] of pipelinePasses) {
|
||||
for (const pass of pipelinePass) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: `${pass.name}\n\n${pass.value}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(
|
||||
passName,
|
||||
`Unhandled passName option: ${passName}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const requestedPass = pipelinePasses.get(passName);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
if (pipelineValue.name === passName) {
|
||||
requestedPasses.push({
|
||||
type: 'text' as const,
|
||||
text: pipelineValue.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
return {
|
||||
content: errors.map(err => {
|
||||
isError: false,
|
||||
content: results.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text:
|
||||
err.loc === null || typeof err.loc === 'symbol'
|
||||
? `React Compiler bailed out:\n\n${err.message}`
|
||||
: `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`,
|
||||
text,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{type: 'text' as const, text: result.code},
|
||||
...requestedPasses,
|
||||
],
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
|
||||
};
|
||||
case 'bailout': {
|
||||
return {
|
||||
isError: true,
|
||||
content: results.content.map(text => {
|
||||
return {
|
||||
type: 'text' as const,
|
||||
text,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'error':
|
||||
case 'compile-error':
|
||||
return {
|
||||
isError: true,
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: results.text,
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
assertExhaustive(
|
||||
results,
|
||||
`Unhandled result ${JSON.stringify(results)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import {compile, type PrintedCompilerPipelineValue} from '../compiler';
|
||||
import {
|
||||
CompilerPipelineValue,
|
||||
printReactiveFunctionWithOutlined,
|
||||
printFunctionWithOutlined,
|
||||
PluginOptions,
|
||||
SourceLocation,
|
||||
} from 'babel-plugin-react-compiler/src';
|
||||
import assertExhaustive from '../utils/assertExhaustive';
|
||||
|
||||
type PassNameType = 'HIR' | 'ReactiveFunction' | 'All' | '@DEBUG' | undefined;
|
||||
|
||||
type CompilerToolOutput =
|
||||
| {
|
||||
kind: 'success';
|
||||
content: Array<string>;
|
||||
}
|
||||
| {
|
||||
kind: 'bailout';
|
||||
content: Array<string>;
|
||||
}
|
||||
| {
|
||||
kind: 'compile-error';
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
kind: 'error';
|
||||
text: string;
|
||||
};
|
||||
|
||||
export default async function compileTool(
|
||||
text: string,
|
||||
passName: PassNameType,
|
||||
): Promise<CompilerToolOutput> {
|
||||
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: {
|
||||
assertExhaustive(result, `Unhandled result ${result}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
|
||||
const compilerOptions: Partial<PluginOptions> = {
|
||||
panicThreshold: 'none',
|
||||
logger: {
|
||||
debugLogIRs: logIR,
|
||||
logEvent: (_filename, event): void => {
|
||||
if (event.kind === 'CompileError') {
|
||||
const detail = event.detail;
|
||||
const loc =
|
||||
detail.loc == null || typeof detail.loc == 'symbol'
|
||||
? event.fnLoc
|
||||
: detail.loc;
|
||||
errors.push({
|
||||
message: detail.reason,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const result = await compile({
|
||||
text,
|
||||
file: 'anonymous.tsx',
|
||||
options: compilerOptions,
|
||||
});
|
||||
if (result.code == null) {
|
||||
return {
|
||||
kind: 'compile-error',
|
||||
text: 'Error: Could not compile',
|
||||
};
|
||||
}
|
||||
const requestedPasses: Array<string> = [];
|
||||
if (passName != null) {
|
||||
switch (passName) {
|
||||
case 'All': {
|
||||
const hir = pipelinePasses.get('PropagateScopeDependenciesHIR');
|
||||
if (hir !== undefined) {
|
||||
for (const pipelineValue of hir) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
}
|
||||
const reactiveFunc = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (reactiveFunc !== undefined) {
|
||||
for (const pipelineValue of reactiveFunc) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'HIR': {
|
||||
// Last pass before HIR -> ReactiveFunction
|
||||
const requestedPass = pipelinePasses.get(
|
||||
'PropagateScopeDependenciesHIR',
|
||||
);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ReactiveFunction': {
|
||||
// Last pass
|
||||
const requestedPass = pipelinePasses.get('PruneHoistedContexts');
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
} else {
|
||||
console.error(`Could not find requested pass ${passName}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '@DEBUG': {
|
||||
for (const [, pipelinePass] of pipelinePasses) {
|
||||
for (const pass of pipelinePass) {
|
||||
requestedPasses.push(`${pass.name}\n\n${pass.value}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertExhaustive(passName, `Unhandled passName option: ${passName}`);
|
||||
}
|
||||
}
|
||||
const requestedPass = pipelinePasses.get(passName);
|
||||
if (requestedPass !== undefined) {
|
||||
for (const pipelineValue of requestedPass) {
|
||||
if (pipelineValue.name === passName) {
|
||||
requestedPasses.push(pipelineValue.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
return {
|
||||
kind: 'bailout',
|
||||
content: errors.map(err => {
|
||||
return err.loc === null || typeof err.loc === 'symbol'
|
||||
? `React Compiler bailed out:\n\n${err.message}`
|
||||
: `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`;
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: 'success',
|
||||
content: [result.code, ...requestedPasses],
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
kind: 'error',
|
||||
text: `Error: ${err.stack}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as cheerio from 'cheerio';
|
||||
import {convert} from 'html-to-text';
|
||||
import {queryAlgolia} from '../utils/algolia';
|
||||
|
||||
type DevDocsToolOutput = {
|
||||
kind: 'success';
|
||||
content: Array<string>;
|
||||
} | {
|
||||
kind: 'error';
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool for querying React dev docs from react.dev
|
||||
* @param query The search query to look up in the React documentation
|
||||
* @returns A promise that resolves to the search results
|
||||
*/
|
||||
export default async function devDocsTool(query: string): Promise<DevDocsToolOutput> {
|
||||
try {
|
||||
const pages = await queryAlgolia(query);
|
||||
if (pages.length === 0) {
|
||||
return {
|
||||
kind: 'error',
|
||||
text: `No results`,
|
||||
};
|
||||
}
|
||||
const content = pages.map(html => {
|
||||
const $ = cheerio.load(html);
|
||||
// react.dev should always have at least one <article> with the main content
|
||||
const article = $('article').html();
|
||||
if (article != null) {
|
||||
return convert(article)
|
||||
} else {
|
||||
return convert($.html())
|
||||
}
|
||||
});
|
||||
return {
|
||||
kind: 'success',
|
||||
content,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
kind: 'error',
|
||||
text: `Error: ${err.stack}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -125,14 +125,16 @@ export async function measurePerformance(
|
||||
|
||||
// ui chaos monkey
|
||||
const selectors = await page.evaluate(() => {
|
||||
window.__INTERACTABLE_SELECTORS__ = [];
|
||||
(window as any).__INTERACTABLE_SELECTORS__ = [];
|
||||
const elements = Array.from(document.querySelectorAll('a')).concat(
|
||||
Array.from(document.querySelectorAll('button')),
|
||||
Array.from(document.querySelectorAll('button')) as any,
|
||||
);
|
||||
for (const el of elements) {
|
||||
window.__INTERACTABLE_SELECTORS__.push(el.tagName.toLowerCase());
|
||||
(window as any).__INTERACTABLE_SELECTORS__.push(
|
||||
el.tagName.toLowerCase(),
|
||||
);
|
||||
}
|
||||
return window.__INTERACTABLE_SELECTORS__;
|
||||
return (window as any).__INTERACTABLE_SELECTORS__;
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
|
||||
Reference in New Issue
Block a user