125 lines
5.9 KiB
JavaScript
125 lines
5.9 KiB
JavaScript
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
|