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 (
${heading}
Loading…
;\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{error.message}
\n \n