166 lines
6.6 KiB
JavaScript
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
|