113 lines
5.2 KiB
JavaScript
113 lines
5.2 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { z } from 'zod';
|
|
import { config } from '../../config.js';
|
|
import { runCommand } from '../../lib/runCommand.js';
|
|
import { kebabCase, pascalCase } from '../../lib/strings.js';
|
|
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
|
import { requireLaravel } from './requireLaravel.js';
|
|
function pluralize(word) {
|
|
const lower = word.toLowerCase();
|
|
if (lower.endsWith('ies'))
|
|
return word;
|
|
if (lower.endsWith('y') && !/[aeiou]y$/i.test(word))
|
|
return `${word.slice(0, -1)}ies`;
|
|
if (/(s|x|z|ch|sh)$/i.test(word))
|
|
return `${word}es`;
|
|
if (lower.endsWith('s'))
|
|
return word;
|
|
return `${word}s`;
|
|
}
|
|
export function registerGenerateLaravelResourceTool(server) {
|
|
server.registerTool('generate_laravel_resource', {
|
|
title: 'Generate Laravel Resource',
|
|
description: 'One-shot scaffold of a full Laravel resource: model + migration + factory + seeder + controller, ' +
|
|
'then wire an apiResource/resource route. Wraps the relevant `php artisan make:*` calls.',
|
|
inputSchema: {
|
|
name: z
|
|
.string()
|
|
.min(1)
|
|
.describe('Resource name, e.g. "Post" or "blog_post". Converted to PascalCase for the model/controller.'),
|
|
api: z
|
|
.boolean()
|
|
.optional()
|
|
.default(true)
|
|
.describe('If true (default), generate an API controller (--api) and register Route::apiResource in routes/api.php. If false, generate a web resource controller and register Route::resource in routes/web.php.'),
|
|
migrate: z
|
|
.boolean()
|
|
.optional()
|
|
.default(false)
|
|
.describe('If true, run `php artisan migrate` after scaffolding. Requires a configured database.'),
|
|
},
|
|
}, async ({ name, api, migrate }) => {
|
|
try {
|
|
const laravel = requireLaravel();
|
|
const model = pascalCase(name);
|
|
const controller = `${model}Controller`;
|
|
const routeName = kebabCase(pluralize(model));
|
|
const steps = [];
|
|
// Model + migration + factory + seeder in one go.
|
|
const modelResult = await runCommand('php', ['artisan', 'make:model', model, '-mfs', '--no-interaction'], { cwd: laravel.root, timeoutMs: config.scaffoldCommandTimeoutMs });
|
|
steps.push({ step: 'make:model -mfs', ...modelResult });
|
|
const controllerArgs = api
|
|
? ['artisan', 'make:controller', controller, '--api', '--model=' + model, '--no-interaction']
|
|
: ['artisan', 'make:controller', controller, '--resource', '--model=' + model, '--no-interaction'];
|
|
const controllerResult = await runCommand('php', controllerArgs, {
|
|
cwd: laravel.root,
|
|
timeoutMs: config.scaffoldCommandTimeoutMs,
|
|
});
|
|
steps.push({ step: 'make:controller', ...controllerResult });
|
|
const routesFile = path.join(laravel.root, 'routes', api ? 'api.php' : 'web.php');
|
|
const routeLine = api
|
|
? `Route::apiResource('${routeName}', \\App\\Http\\Controllers\\${controller}::class);`
|
|
: `Route::resource('${routeName}', \\App\\Http\\Controllers\\${controller}::class);`;
|
|
const routeWired = wireRoute(routesFile, routeLine, api);
|
|
steps.push({ step: 'wire-route', routesFile, routeLine, wired: routeWired });
|
|
let migrateResult = null;
|
|
if (migrate) {
|
|
migrateResult = await runCommand('php', ['artisan', 'migrate', '--no-interaction'], {
|
|
cwd: laravel.root,
|
|
timeoutMs: config.scaffoldCommandTimeoutMs,
|
|
});
|
|
steps.push({ step: 'migrate', ...migrateResult });
|
|
}
|
|
return jsonResult({
|
|
model,
|
|
controller,
|
|
route: `${api ? 'apiResource' : 'resource'} /${routeName}`,
|
|
routesFile,
|
|
routeWired,
|
|
steps,
|
|
});
|
|
}
|
|
catch (error) {
|
|
return errorResult(toErrorMessage(error));
|
|
}
|
|
});
|
|
}
|
|
function wireRoute(routesFile, routeLine, api) {
|
|
fs.mkdirSync(path.dirname(routesFile), { recursive: true });
|
|
if (!fs.existsSync(routesFile)) {
|
|
const header = api
|
|
? `<?php\n\nuse Illuminate\\Support\\Facades\\Route;\n\n`
|
|
: `<?php\n\nuse Illuminate\\Support\\Facades\\Route;\n\n`;
|
|
fs.writeFileSync(routesFile, header + routeLine + '\n');
|
|
return true;
|
|
}
|
|
const existing = fs.readFileSync(routesFile, 'utf8');
|
|
if (existing.includes(routeLine.trim()))
|
|
return false;
|
|
// Ensure the Route facade is imported if the file already exists without it.
|
|
let next = existing;
|
|
if (!/use\s+Illuminate\\Support\\Facades\\Route\s*;/.test(next)) {
|
|
if (next.startsWith('<?php')) {
|
|
next = next.replace('<?php', "<?php\n\nuse Illuminate\\Support\\Facades\\Route;");
|
|
}
|
|
else {
|
|
next = `<?php\n\nuse Illuminate\\Support\\Facades\\Route;\n\n${next}`;
|
|
}
|
|
}
|
|
fs.writeFileSync(routesFile, `${next.trimEnd()}\n${routeLine}\n`);
|
|
return true;
|
|
}
|
|
//# sourceMappingURL=generateLaravelResource.js.map
|