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

166 lines
6.6 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { z } from 'zod';
import { config } from '../../config.js';
import { detectProject } from '../../lib/frameworks/detect.js';
import { camelCase, kebabCase, pascalCase } from '../../lib/strings.js';
import { errorResult, jsonResult, targetSchema, toErrorMessage } from '../shared.js';
const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
export function registerGenerateApiRouteTool(server) {
server.registerTool('generate_api_route', {
title: 'Generate API Route',
description: 'Scaffold a backend route + handler for the detected framework: controller + routes/api.php entry for Laravel, controller + Routes.php entry for CodeIgniter, an app/pages API route file for Next.js, or a standalone Express router file otherwise.',
inputSchema: {
name: z.string().min(1).describe('Resource/route name, e.g. "posts" or "UserProfile".'),
method: z.enum(METHODS).optional().default('GET'),
target: targetSchema,
},
}, async ({ name, method, target }) => {
try {
const detected = detectProject(config.workspaceRoot);
const adapter = target === 'frontend'
? detected.frontend
: target === 'backend'
? detected.backend
: (detected.backend ?? detected.frontend ?? detected.primary);
if (!adapter)
return errorResult(`target "${target}" was requested but no such project was detected.`);
switch (adapter.kind) {
case 'laravel':
return jsonResult(generateLaravelRoute(adapter.root, name, method));
case 'codeigniter':
return jsonResult(generateCodeIgniterRoute(adapter.root, name, method));
case 'react':
return jsonResult(generateNextApiRoute(adapter.root, name, method));
default:
return jsonResult(generateExpressRoute(adapter.root, name, method));
}
}
catch (error) {
return errorResult(toErrorMessage(error));
}
});
}
function appendUnlessPresent(file, header, line) {
fs.mkdirSync(path.dirname(file), { recursive: true });
if (!fs.existsSync(file)) {
fs.writeFileSync(file, header);
}
const existing = fs.readFileSync(file, 'utf8');
if (!existing.includes(line.trim())) {
fs.appendFileSync(file, `${line}\n`);
}
}
function writeIfMissing(file, contents) {
fs.mkdirSync(path.dirname(file), { recursive: true });
if (fs.existsSync(file))
return false;
fs.writeFileSync(file, contents);
return true;
}
function generateLaravelRoute(root, name, method) {
const controllerName = `${pascalCase(name)}Controller`;
const controllerFile = path.join(root, 'app', 'Http', 'Controllers', `${controllerName}.php`);
const controllerCreated = writeIfMissing(controllerFile, `<?php
namespace App\\Http\\Controllers;
use Illuminate\\Http\\Request;
class ${controllerName}
{
public function __invoke(Request $request)
{
return response()->json([]);
}
}
`);
const routesFile = path.join(root, 'routes', 'api.php');
appendUnlessPresent(routesFile, `<?php
use Illuminate\\Support\\Facades\\Route;
`, `Route::${method.toLowerCase()}('/${kebabCase(name)}', \\App\\Http\\Controllers\\${controllerName}::class);`);
return { kind: 'laravel', controllerFile, controllerCreated, routesFile, route: `${method} /api/${kebabCase(name)}` };
}
function generateCodeIgniterRoute(root, name, method) {
const controllerName = pascalCase(name);
const controllerFile = path.join(root, 'app', 'Controllers', `${controllerName}.php`);
const controllerCreated = writeIfMissing(controllerFile, `<?php
namespace App\\Controllers;
class ${controllerName} extends BaseController
{
public function index()
{
return $this->response->setJSON([]);
}
}
`);
const routesFile = path.join(root, 'app', 'Config', 'Routes.php');
appendUnlessPresent(routesFile, `<?php
use CodeIgniter\\Router\\RouteCollection;
/** @var RouteCollection $routes */
`, `$routes->${method.toLowerCase()}('${kebabCase(name)}', '${controllerName}::index');`);
return { kind: 'codeigniter', controllerFile, controllerCreated, routesFile, route: `${method} /${kebabCase(name)}` };
}
function generateNextApiRoute(root, name, method) {
const appDir = path.join(root, 'app');
const srcAppDir = path.join(root, 'src', 'app');
const pagesDir = path.join(root, 'pages');
const srcPagesDir = path.join(root, 'src', 'pages');
const appRoot = fs.existsSync(appDir) ? appDir : fs.existsSync(srcAppDir) ? srcAppDir : null;
const pagesRoot = fs.existsSync(pagesDir) ? pagesDir : fs.existsSync(srcPagesDir) ? srcPagesDir : null;
if (appRoot) {
const file = path.join(appRoot, 'api', kebabCase(name), 'route.ts');
const created = writeIfMissing(file, `import { NextResponse } from 'next/server';
export async function ${method}() {
return NextResponse.json({});
}
`);
return { kind: 'next-app-router', file, created, route: `${method} /api/${kebabCase(name)}` };
}
if (pagesRoot) {
const file = path.join(pagesRoot, 'api', `${kebabCase(name)}.ts`);
const created = writeIfMissing(file, `import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== '${method}') {
res.status(405).end();
return;
}
res.status(200).json({});
}
`);
return { kind: 'next-pages-router', file, created, route: `${method} /api/${kebabCase(name)}` };
}
throw new Error('The detected React project has no "app" or "pages" directory, so it has no built-in API routes (this looks like a plain Vite React app, not Next.js). ' +
'Scaffold a separate backend (Laravel/CodeIgniter/express-api) instead, or use target: "backend" if one already exists.');
}
function generateExpressRoute(root, name, method) {
const file = path.join(root, 'routes', `${kebabCase(name)}.js`);
const created = writeIfMissing(file, `import { Router } from 'express';
const router = Router();
router.${method.toLowerCase()}('/', (req, res) => {
res.json({});
});
export default router;
`);
const varName = `${camelCase(name)}Router`;
return {
kind: 'express',
file,
created,
route: `${method} /api/${kebabCase(name)}`,
wiring: `Not auto-mounted. Add to your app entry point: import ${varName} from './routes/${kebabCase(name)}.js'; app.use('/api/${kebabCase(name)}', ${varName});`,
};
}
//# sourceMappingURL=generateApiRoute.js.map