init project
This commit is contained in:
Vendored
+119
@@ -0,0 +1,119 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireReact } from './requireReact.js';
|
||||
export function registerAddReactRouteTool(server) {
|
||||
server.registerTool('add_react_route', {
|
||||
title: 'Add React Route',
|
||||
description: 'Add a route entry for the detected React router: Next.js App Router (creates a page file), Next.js Pages Router (creates a page file), react-router (appends a route to a routes file), or TanStack Router (appends a file-based route under src/routes).',
|
||||
inputSchema: {
|
||||
path: z.string().min(1).describe('URL path, e.g. "/users" or "/users/:id".'),
|
||||
component: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Component name to import, e.g. "UserList" or a relative path like "./pages/UserList".'),
|
||||
file: z.string().optional().describe('Override the file to write/edit. Auto-detected by default.'),
|
||||
},
|
||||
}, async ({ path: routePath, component, file: fileOverride }) => {
|
||||
try {
|
||||
const react = requireReact();
|
||||
const router = detectRouterType(react.root);
|
||||
switch (router) {
|
||||
case 'next-app':
|
||||
case 'next-pages': {
|
||||
const file = fileOverride ??
|
||||
path.join(react.root, router === 'next-app' ? 'app' : 'pages', ...routePath.split('/').filter(Boolean), 'page.tsx');
|
||||
const created = writeNextPage(file, component, router === 'next-app');
|
||||
return jsonResult({ router, file, created });
|
||||
}
|
||||
case 'react-router':
|
||||
case 'tanstack-router': {
|
||||
const file = fileOverride ??
|
||||
(router === 'react-router'
|
||||
? path.join(react.root, 'src', 'routes.tsx')
|
||||
: path.join(react.root, 'src', 'routes', `${routePath.replace(/[^a-z0-9]/gi, '-')}.tsx`));
|
||||
const wired = appendRoute(file, routePath, component, router);
|
||||
return jsonResult({ router, file, wired });
|
||||
}
|
||||
default:
|
||||
return jsonResult({
|
||||
router: 'unknown',
|
||||
message: 'No known React router detected (Next.js app/pages, react-router, or TanStack Router). ' +
|
||||
'Provide the file argument explicitly to add a route entry.',
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
function detectRouterType(root) {
|
||||
if (fs.existsSync(path.join(root, 'app', 'layout.tsx')) || fs.existsSync(path.join(root, 'app', 'layout.jsx'))) {
|
||||
return 'next-app';
|
||||
}
|
||||
if (fs.existsSync(path.join(root, 'pages', 'index.tsx')) || fs.existsSync(path.join(root, 'pages', 'index.jsx'))) {
|
||||
return 'next-pages';
|
||||
}
|
||||
if (fs.existsSync(path.join(root, 'src', 'app', 'layout.tsx')) || fs.existsSync(path.join(root, 'src', 'app', 'layout.jsx'))) {
|
||||
return 'next-app';
|
||||
}
|
||||
if (fs.existsSync(path.join(root, 'src', 'pages', 'index.tsx')) ||
|
||||
fs.existsSync(path.join(root, 'src', 'pages', 'index.jsx'))) {
|
||||
return 'next-pages';
|
||||
}
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
||||
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
||||
if (deps['@tanstack/react-router'])
|
||||
return 'tanstack-router';
|
||||
if (deps['react-router-dom'] || deps['react-router'])
|
||||
return 'react-router';
|
||||
return 'unknown';
|
||||
}
|
||||
function writeNextPage(file, component, appRouter) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
if (fs.existsSync(file))
|
||||
return false;
|
||||
const importPath = component.startsWith('.') ? component : `./${component}`;
|
||||
const contents = appRouter
|
||||
? `import { ${component} } from '${importPath}';
|
||||
|
||||
export default function Page() {
|
||||
return <${component} />;
|
||||
}
|
||||
`
|
||||
: `import { ${component} } from '${importPath}';
|
||||
|
||||
export default function ${component}Page() {
|
||||
return <${component} />;
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(file, contents);
|
||||
return true;
|
||||
}
|
||||
function appendRoute(file, routePath, component, router) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
const importPath = component.startsWith('.') ? component : `./${component}`;
|
||||
const routeLine = router === 'tanstack-router'
|
||||
? `// TODO: route ${routePath} -> <${component} /> (TanStack Router uses file-based routing; this file was created).`
|
||||
: `{ path: '${routePath}', element: <${component} /> },`;
|
||||
if (!fs.existsSync(file)) {
|
||||
fs.writeFileSync(file, `import { ${component} } from '${importPath}';
|
||||
|
||||
// Add this route to your router configuration:
|
||||
${routeLine}
|
||||
`);
|
||||
return true;
|
||||
}
|
||||
const existing = fs.readFileSync(file, 'utf8');
|
||||
if (existing.includes(routeLine.trim()))
|
||||
return false;
|
||||
let next = existing;
|
||||
if (!new RegExp(`import\\s+\\{[^}]*\\b${component}\\b[^}]*\\}\\s+from\\s+['"]${importPath.replace(/\./g, '\\.')}['"];`).test(next)) {
|
||||
next = `import { ${component} } from '${importPath}';\n${next}`;
|
||||
}
|
||||
fs.writeFileSync(file, `${next.trimEnd()}\n${routeLine}\n`);
|
||||
return true;
|
||||
}
|
||||
//# sourceMappingURL=addReactRoute.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"addReactRoute.js","sourceRoot":"","sources":["../../../src/tools/react/addReactRoute.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+107
@@ -0,0 +1,107 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireReact } from './requireReact.js';
|
||||
export function registerAnalyzeReactComponentTool(server) {
|
||||
server.registerTool('analyze_react_component', {
|
||||
title: 'Analyze React Component',
|
||||
description: 'Static scan of a React component file for common issues: missing `key` in lists, hook-rule violations, unused props, and Next.js App Router heuristics (hooks/events without "use client").',
|
||||
inputSchema: {
|
||||
file: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Path to the component file, relative to the React project root.'),
|
||||
},
|
||||
}, async ({ file }) => {
|
||||
try {
|
||||
const react = requireReact();
|
||||
const absolutePath = path.resolve(react.root, file);
|
||||
if (!absolutePath.startsWith(react.root + path.sep)) {
|
||||
return errorResult('Refusing to analyze a file outside the React project root.');
|
||||
}
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
return errorResult(`File not found: ${file}`);
|
||||
}
|
||||
const source = fs.readFileSync(absolutePath, 'utf8');
|
||||
const issues = [];
|
||||
const lines = source.split('\n');
|
||||
const hasUseClient = /^\s*['"]use client['"]\s*;?/m.test(source);
|
||||
const usesClientOnlyApis = /\bon[A-Z][A-Za-z]+\s*=/.test(source) ||
|
||||
/\buse(State|Effect|Reducer|Ref|LayoutEffect|Callback|Memo|Context)\s*\(/.test(source);
|
||||
if (!hasUseClient && usesClientOnlyApis) {
|
||||
issues.push({
|
||||
type: 'missing-use-client',
|
||||
line: 1,
|
||||
message: 'File uses client-only APIs (hooks or event handlers) but is missing a "use client" directive. ' +
|
||||
'Required for Next.js App Router Client Components.',
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i] ?? '';
|
||||
const lineNumber = i + 1;
|
||||
// Missing key in array maps (heuristic).
|
||||
if (/(\w+\s*\.\s*map|React\.Children\.map)\s*\(/.test(line) && !/key\s*=/.test(line)) {
|
||||
issues.push({
|
||||
type: 'missing-key',
|
||||
line: lineNumber,
|
||||
message: 'Array.map without a key prop on the returned element.',
|
||||
});
|
||||
}
|
||||
// Conditional or loop hook calls (simple heuristics).
|
||||
if (/\buse[A-Z][A-Za-z0-9]*\s*\(/.test(line)) {
|
||||
if (/\b(if|while|for|switch)\s*\(/.test(line)) {
|
||||
issues.push({
|
||||
type: 'rules-of-hooks',
|
||||
line: lineNumber,
|
||||
message: 'Hook call inside a conditional or loop block.',
|
||||
});
|
||||
}
|
||||
}
|
||||
// Functions / non-serializable values passed into imported components from a Server Component.
|
||||
if (!hasUseClient && /<(?:[A-Z][\w.]*)\b[^>]*\{[^}]*=>/.test(line)) {
|
||||
issues.push({
|
||||
type: 'server-to-client-props',
|
||||
line: lineNumber,
|
||||
message: 'Looks like a function/arrow is passed as a prop from a Server Component. ' +
|
||||
'Move interactive children into a Client Component ("use client").',
|
||||
});
|
||||
}
|
||||
}
|
||||
// Unused props: find destructured props and see if they are referenced.
|
||||
const propDestructuring = source.match(/function\s+\w+\s*\(\s*\{\s*([^}]+)\s*\}\s*\)/);
|
||||
if (propDestructuring && propDestructuring[1]) {
|
||||
const rawProps = propDestructuring[1];
|
||||
const props = rawProps
|
||||
.split(',')
|
||||
.map((p) => p.trim().split(/\s*:\s*/)[0]?.replace(/\?|:.*$/, '').trim() ?? '')
|
||||
.filter(Boolean);
|
||||
const destructuringIndex = propDestructuring.index ?? 0;
|
||||
for (const prop of props) {
|
||||
const propRegex = new RegExp(`\\b${prop}\\b`, 'g');
|
||||
const matches = [...source.matchAll(propRegex)].length;
|
||||
// The declaration itself counts as one match, plus the original destructuring pattern may add another.
|
||||
if (matches <= 2) {
|
||||
issues.push({
|
||||
type: 'unused-prop',
|
||||
line: source.substring(0, destructuringIndex).split('\n').length,
|
||||
message: `Prop "${prop}" appears unused in the component body.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return jsonResult({
|
||||
file,
|
||||
projectRoot: react.root,
|
||||
hasUseClient,
|
||||
issues,
|
||||
issueCount: issues.length,
|
||||
note: 'This is a heuristic static scan, not a full ESLint run. Use run_lint for comprehensive checks.',
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=analyzeComponent.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"analyzeComponent.js","sourceRoot":"","sources":["../../../src/tools/react/analyzeComponent.ts"],"names":[],"mappings":"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"}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
||||
import { pascalCase } from '../../lib/strings.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireReact } from './requireReact.js';
|
||||
function resolveAppDir(root) {
|
||||
if (fs.existsSync(path.join(root, 'src', 'app')))
|
||||
return path.join(root, 'src', 'app');
|
||||
return path.join(root, 'app');
|
||||
}
|
||||
function routeSegments(routePath) {
|
||||
return routePath.split('/').filter(Boolean);
|
||||
}
|
||||
function assertNextAppRouter(root) {
|
||||
const appDir = resolveAppDir(root);
|
||||
const hasLayout = fs.existsSync(path.join(appDir, 'layout.tsx')) ||
|
||||
fs.existsSync(path.join(appDir, 'layout.jsx')) ||
|
||||
fs.existsSync(path.join(root, 'next.config.js')) ||
|
||||
fs.existsSync(path.join(root, 'next.config.mjs')) ||
|
||||
fs.existsSync(path.join(root, 'next.config.ts'));
|
||||
if (!hasLayout && !fs.existsSync(appDir)) {
|
||||
throw new Error('Next.js App Router not detected. Expected an app/ (or src/app/) directory or next.config.*.');
|
||||
}
|
||||
}
|
||||
export function registerGenerateNextPageTool(server) {
|
||||
server.registerTool('generate_next_page', {
|
||||
title: 'Generate Next.js Page',
|
||||
description: 'Scaffold a Next.js App Router page.tsx under app/{path}/ (or src/app/). Optionally also creates loading.tsx and error.tsx.',
|
||||
inputSchema: {
|
||||
path: z.string().min(1).describe('URL path, e.g. "/users" or "/blog/[slug]".'),
|
||||
title: z.string().optional().describe('Optional heading text; defaults to a PascalCase name from the path.'),
|
||||
withLoading: z.boolean().optional().default(false).describe('Also create loading.tsx.'),
|
||||
withError: z.boolean().optional().default(false).describe('Also create error.tsx.'),
|
||||
overwrite: z.boolean().optional().default(false),
|
||||
},
|
||||
}, async ({ path: routePath, title, withLoading, withError, overwrite }) => {
|
||||
try {
|
||||
const react = requireReact();
|
||||
assertNextAppRouter(react.root);
|
||||
const typescript = fs.existsSync(path.join(react.root, 'tsconfig.json'));
|
||||
const ext = typescript ? 'tsx' : 'jsx';
|
||||
const appDir = resolveAppDir(react.root);
|
||||
const dir = resolveWorkspacePath(appDir, path.join(...routeSegments(routePath)));
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const heading = title ?? pascalCase(routeSegments(routePath).join(' ') || 'Page');
|
||||
const files = [];
|
||||
const pageFile = path.join(dir, `page.${ext}`);
|
||||
if (!overwrite && fs.existsSync(pageFile)) {
|
||||
return errorResult(`"${pageFile}" already exists. Pass overwrite: true to replace it.`);
|
||||
}
|
||||
fs.writeFileSync(pageFile, `export default function ${heading.replace(/[^A-Za-z0-9]/g, '') || 'Page'}Page() {
|
||||
return (
|
||||
<main>
|
||||
<h1>${heading}</h1>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
`);
|
||||
files.push(pageFile);
|
||||
if (withLoading) {
|
||||
const loadingFile = path.join(dir, `loading.${ext}`);
|
||||
if (overwrite || !fs.existsSync(loadingFile)) {
|
||||
fs.writeFileSync(loadingFile, `export default function Loading() {\n return <p>Loading…</p>;\n}\n`);
|
||||
files.push(loadingFile);
|
||||
}
|
||||
}
|
||||
if (withError) {
|
||||
const errorFile = path.join(dir, `error.${ext}`);
|
||||
if (overwrite || !fs.existsSync(errorFile)) {
|
||||
fs.writeFileSync(errorFile, `'use client';\n\nexport default function Error({ error, reset }: { error: Error; reset: () => void }) {\n return (\n <div>\n <p>{error.message}</p>\n <button type="button" onClick={() => reset()}>\n Try again\n </button>\n </div>\n );\n}\n`);
|
||||
files.push(errorFile);
|
||||
}
|
||||
}
|
||||
return jsonResult({ router: 'app', path: routePath, files });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGenerateNextLayoutTool(server) {
|
||||
server.registerTool('generate_next_layout', {
|
||||
title: 'Generate Next.js Layout',
|
||||
description: 'Scaffold a Next.js App Router layout.tsx under app/{path}/ (or src/app/).',
|
||||
inputSchema: {
|
||||
path: z
|
||||
.string()
|
||||
.optional()
|
||||
.default('')
|
||||
.describe('URL path segment for a nested layout, e.g. "/dashboard". Empty for the root layout.'),
|
||||
overwrite: z.boolean().optional().default(false),
|
||||
},
|
||||
}, async ({ path: routePath, overwrite }) => {
|
||||
try {
|
||||
const react = requireReact();
|
||||
assertNextAppRouter(react.root);
|
||||
const typescript = fs.existsSync(path.join(react.root, 'tsconfig.json'));
|
||||
const ext = typescript ? 'tsx' : 'jsx';
|
||||
const appDir = resolveAppDir(react.root);
|
||||
const segments = routeSegments(routePath ?? '');
|
||||
const dir = segments.length === 0 ? appDir : resolveWorkspacePath(appDir, path.join(...segments));
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const layoutFile = path.join(dir, `layout.${ext}`);
|
||||
if (!overwrite && fs.existsSync(layoutFile)) {
|
||||
return errorResult(`"${layoutFile}" already exists. Pass overwrite: true to replace it.`);
|
||||
}
|
||||
const childrenType = typescript ? '{ children }: { children: React.ReactNode }' : '{ children }';
|
||||
fs.writeFileSync(layoutFile, `export default function Layout(${childrenType}) {
|
||||
return (
|
||||
<section>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
`);
|
||||
return jsonResult({ router: 'app', path: routePath || '/', file: layoutFile });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=generateNextFiles.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+71
@@ -0,0 +1,71 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { camelCase, pascalCase } from '../../lib/strings.js';
|
||||
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireReact } from './requireReact.js';
|
||||
export function registerGenerateReactHookTool(server) {
|
||||
server.registerTool('generate_react_hook', {
|
||||
title: 'Generate React Hook',
|
||||
description: 'Scaffold a custom React hook (useXyz.ts) and a matching test file. Detects TypeScript from tsconfig.json.',
|
||||
inputSchema: {
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Hook name without the "use" prefix, e.g. "Counter" or "fetchUser" (camelCase is fine).'),
|
||||
directory: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Directory relative to the React project root. Defaults to "src/hooks".'),
|
||||
withTest: z.boolean().optional().default(true),
|
||||
overwrite: z.boolean().optional().default(false),
|
||||
},
|
||||
}, async ({ name, directory, withTest, overwrite }) => {
|
||||
try {
|
||||
const react = requireReact();
|
||||
const typescript = fs.existsSync(path.join(react.root, 'tsconfig.json'));
|
||||
const ext = typescript ? 'ts' : 'js';
|
||||
const hookName = name.startsWith('use') ? camelCase(name) : `use${pascalCase(name)}`;
|
||||
const dir = resolveWorkspacePath(react.root, directory ?? 'src/hooks');
|
||||
const hookFile = path.join(dir, `${hookName}.${ext}`);
|
||||
assertNotExists(hookFile, overwrite);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(hookFile, `import { useState } from 'react';
|
||||
|
||||
export function ${hookName}() {
|
||||
const [count, setCount] = useState(0);
|
||||
return { count, increment: () => setCount((c) => c + 1) };
|
||||
}
|
||||
`);
|
||||
const files = [hookFile];
|
||||
if (withTest) {
|
||||
const testFile = path.join(dir, `${hookName}.test.${ext}`);
|
||||
assertNotExists(testFile, overwrite);
|
||||
fs.writeFileSync(testFile, `import { describe, expect, it } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { ${hookName} } from './${hookName}';
|
||||
|
||||
describe('${hookName}', () => {
|
||||
it('increments count', () => {
|
||||
const { result } = renderHook(() => ${hookName}());
|
||||
act(() => result.current.increment());
|
||||
expect(result.current.count).toBe(1);
|
||||
});
|
||||
});
|
||||
`);
|
||||
files.push(testFile);
|
||||
}
|
||||
return jsonResult({ name: hookName, files });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
function assertNotExists(file, overwrite) {
|
||||
if (!overwrite && fs.existsSync(file)) {
|
||||
throw new Error(`"${file}" already exists. Pass overwrite: true to replace it.`);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=generateReactHook.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"generateReactHook.js","sourceRoot":"","sources":["../../../src/tools/react/generateReactHook.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import { registerGenerateReactHookTool } from './generateReactHook.js';
|
||||
import { registerAddReactRouteTool } from './addReactRoute.js';
|
||||
import { registerRunReactTestsTool } from './runReactTests.js';
|
||||
import { registerAnalyzeReactComponentTool } from './analyzeComponent.js';
|
||||
import { registerGenerateNextLayoutTool, registerGenerateNextPageTool } from './generateNextFiles.js';
|
||||
export function registerReactTools(server) {
|
||||
registerGenerateReactHookTool(server);
|
||||
registerAddReactRouteTool(server);
|
||||
registerRunReactTestsTool(server);
|
||||
registerAnalyzeReactComponentTool(server);
|
||||
registerGenerateNextPageTool(server);
|
||||
registerGenerateNextLayoutTool(server);
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/react/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAE,iCAAiC,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAE,8BAA8B,EAAE,4BAA4B,EAAE,MAAM,wBAAwB,CAAC;AAEtG,MAAM,UAAU,kBAAkB,CAAC,MAAiB;IAClD,6BAA6B,CAAC,MAAM,CAAC,CAAC;IACtC,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAClC,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAClC,iCAAiC,CAAC,MAAM,CAAC,CAAC;IAC1C,4BAA4B,CAAC,MAAM,CAAC,CAAC;IACrC,8BAA8B,CAAC,MAAM,CAAC,CAAC;AACzC,CAAC"}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
import { config } from '../../config.js';
|
||||
import { detectProject } from '../../lib/frameworks/detect.js';
|
||||
import { ReactAdapter } from '../../lib/frameworks/react.js';
|
||||
export function requireReact() {
|
||||
const detected = detectProject(config.workspaceRoot);
|
||||
if (detected.frontend instanceof ReactAdapter) {
|
||||
return detected.frontend;
|
||||
}
|
||||
if (detected.primary instanceof ReactAdapter) {
|
||||
return detected.primary;
|
||||
}
|
||||
throw new Error('No React project detected in WORKSPACE_ROOT. ' +
|
||||
'Expected a package.json with a react dependency (or react-scripts/vite/next config) at the workspace root.');
|
||||
}
|
||||
//# sourceMappingURL=requireReact.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"requireReact.js","sourceRoot":"","sources":["../../../src/tools/react/requireReact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D,MAAM,UAAU,YAAY;IAC1B,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACrD,IAAI,QAAQ,CAAC,QAAQ,YAAY,YAAY,EAAE,CAAC;QAC9C,OAAO,QAAQ,CAAC,QAAQ,CAAC;IAC3B,CAAC;IACD,IAAI,QAAQ,CAAC,OAAO,YAAY,YAAY,EAAE,CAAC;QAC7C,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC1B,CAAC;IACD,MAAM,IAAI,KAAK,CACb,+CAA+C;QAC7C,4GAA4G,CAC/G,CAAC;AACJ,CAAC"}
|
||||
Vendored
+67
@@ -0,0 +1,67 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { detectJsPackageManager, runScriptCommandFor } from '../../lib/frameworks/packageManager.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireReact } from './requireReact.js';
|
||||
function parseReactTestingLibraryOutput(output) {
|
||||
// Extract failing test names and common RTL errors for a quick summary.
|
||||
const failingTests = output
|
||||
.split(/\n(?=\s*FAIL|●)/)
|
||||
.filter((chunk) => chunk.includes('●'))
|
||||
.map((chunk) => {
|
||||
const match = chunk.match(/●\s*(.+?)(?:\n|$)/);
|
||||
return match?.[1]?.trim() ?? null;
|
||||
})
|
||||
.filter((item) => Boolean(item));
|
||||
const rtlErrors = [];
|
||||
if (/Unable to find an element/i.test(output))
|
||||
rtlErrors.push('Unable to find an element (getBy/queryBy failed)');
|
||||
if (/Found multiple elements/i.test(output))
|
||||
rtlErrors.push('Found multiple elements with the same query');
|
||||
if (/found accessibility element/i.test(output))
|
||||
rtlErrors.push('Role/name based query mismatch');
|
||||
if (/Timed out in waitFor/i.test(output))
|
||||
rtlErrors.push('waitFor timeout (async assertion not satisfied)');
|
||||
if (/Warning: An update to .* inside a test was not wrapped in act/i.test(output)) {
|
||||
rtlErrors.push('Missing act() wrapper around state update');
|
||||
}
|
||||
const totalMatch = output.match(/Tests\s+(\d+)\s+passed/i) || output.match(/Test Suites?:\s*(\d+)\s+passed/i);
|
||||
return {
|
||||
failingTestCount: failingTests.length,
|
||||
failingTests: failingTests.slice(0, 10),
|
||||
rtlErrors: rtlErrors.slice(0, 10),
|
||||
testsPassed: totalMatch ? Number(totalMatch[1]) : null,
|
||||
};
|
||||
}
|
||||
export function registerRunReactTestsTool(server) {
|
||||
server.registerTool('run_react_tests', {
|
||||
title: 'Run React Tests',
|
||||
description: 'Run the React test runner (Jest/Vitest + React Testing Library) via package.json "test" script and surface RTL-specific failures.',
|
||||
inputSchema: {
|
||||
extraArgs: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Extra CLI args, e.g. ["--filter", "UserCard"].'),
|
||||
},
|
||||
}, async ({ extraArgs }) => {
|
||||
try {
|
||||
const react = requireReact();
|
||||
const pm = detectJsPackageManager(react.root);
|
||||
const { command, args } = runScriptCommandFor(pm, 'test', extraArgs ?? []);
|
||||
const result = await runCommand(command, args, {
|
||||
cwd: react.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({
|
||||
runner: 'npm-test-script',
|
||||
summary: parseReactTestingLibraryOutput(result.output),
|
||||
...result,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=runReactTests.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"runReactTests.js","sourceRoot":"","sources":["../../../src/tools/react/runReactTests.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AACrG,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,SAAS,8BAA8B,CAAC,MAAc;IACpD,wEAAwE;IACxE,MAAM,YAAY,GAAG,MAAM;SACxB,KAAK,CAAC,iBAAiB,CAAC;SACxB,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACb,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAC/C,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC;IACpC,CAAC,CAAC;SACD,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAEnD,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,IAAI,4BAA4B,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,SAAS,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAC;IAClH,IAAI,0BAA0B,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,SAAS,CAAC,IAAI,CAAC,6CAA6C,CAAC,CAAC;IAC3G,IAAI,8BAA8B,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,SAAS,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;IAClG,IAAI,uBAAuB,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,SAAS,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAC;IAC5G,IAAI,gEAAgE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAClF,SAAS,CAAC,IAAI,CAAC,2CAA2C,CAAC,CAAC;IAC9D,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,yBAAyB,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;IAC9G,OAAO;QACL,gBAAgB,EAAE,YAAY,CAAC,MAAM;QACrC,YAAY,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;QACvC,SAAS,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;QACjC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;KACvD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,MAAiB;IACzD,MAAM,CAAC,YAAY,CACjB,iBAAiB,EACjB;QACE,KAAK,EAAE,iBAAiB;QACxB,WAAW,EACT,mIAAmI;QACrI,WAAW,EAAE;YACX,SAAS,EAAE,CAAC;iBACT,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;iBACjB,QAAQ,EAAE;iBACV,QAAQ,CAAC,gDAAgD,CAAC;SAC9D;KACF,EACD,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE;QACtB,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,YAAY,EAAE,CAAC;YAC7B,MAAM,EAAE,GAAG,sBAAsB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC9C,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC;YAC3E,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE;gBAC7C,GAAG,EAAE,KAAK,CAAC,IAAI;gBACf,SAAS,EAAE,MAAM,CAAC,wBAAwB;aAC3C,CAAC,CAAC;YACH,OAAO,UAAU,CAAC;gBAChB,MAAM,EAAE,iBAAiB;gBACzB,OAAO,EAAE,8BAA8B,CAAC,MAAM,CAAC,MAAM,CAAC;gBACtD,GAAG,MAAM;aACV,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Reference in New Issue
Block a user