Files
2026-07-31 13:12:54 -04:00

119 lines
5.3 KiB
JavaScript

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