168 lines
5.0 KiB
TypeScript
168 lines
5.0 KiB
TypeScript
/**
|
|
* `wrnexus generate <type> <name>` — scaffold a page, component, API route, or
|
|
* schema from a template. Keeps new files consistent and gets users moving fast.
|
|
*
|
|
* wrnexus generate page about
|
|
* wrnexus generate component user-card
|
|
* wrnexus generate api users/list
|
|
* wrnexus generate schema signup
|
|
*/
|
|
|
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
|
|
export type GenerateType = "page" | "component" | "api" | "schema";
|
|
|
|
const ALIASES: Record<string, GenerateType> = {
|
|
page: "page",
|
|
p: "page",
|
|
component: "component",
|
|
c: "component",
|
|
api: "api",
|
|
a: "api",
|
|
schema: "schema",
|
|
s: "schema",
|
|
};
|
|
|
|
export interface GeneratedFile {
|
|
/** Path relative to the `app/` directory. */
|
|
path: string;
|
|
content: string;
|
|
}
|
|
|
|
export function scaffoldResource(name: string): GeneratedFile[] {
|
|
const clean = name.replace(/[^A-Za-z0-9_-]/g, "").toLowerCase();
|
|
if (!clean) throw new TypeError("resource name is required");
|
|
const singular = clean.replace(/s$/, "");
|
|
const pascal = toPascalCase(singular);
|
|
return [
|
|
{
|
|
path: `schemas/${singular}.ts`,
|
|
content: `import { v } from "@wrnexus/validation";\n\nexport default v.object({\n name: v.string().min(1, "Required"),\n});\n`,
|
|
},
|
|
{
|
|
path: `resources/${clean}.ts`,
|
|
content: `import { getDb } from "@wrnexus/db";\nimport { defineResource } from "@wrnexus/helpers";\n\nexport default defineResource({\n name: "${clean}",\n db: getDb,\n table: "${clean}",\n owner: "owner_id",\n fields: ["name"],\n permissions: { read: "${singular}:read", write: "${singular}:write" },\n});\n`,
|
|
},
|
|
{
|
|
path: `api/${clean}.ts`,
|
|
content: `import resource from "../resources/${clean}.ts";\nexport const GET = resource.list;\nexport const POST = resource.create;\n`,
|
|
},
|
|
{
|
|
path: `api/${clean}/[id].ts`,
|
|
content: `import resource from "../../resources/${clean}.ts";\nexport const GET = resource.get;\nexport const PUT = resource.update;\nexport const DELETE = resource.remove;\n`,
|
|
},
|
|
{
|
|
path: `pages/${clean}.wrn`,
|
|
content: `page ${pascal}List {\n view {\n <h1>${pascal}</h1>\n <p>Generated resource page. Connect it to /api/${clean}.</p>\n }\n}\n`,
|
|
},
|
|
];
|
|
}
|
|
|
|
export function runGenerateResource(appRoot: string, name: string | undefined): void {
|
|
if (!name) throw new TypeError("Usage: wrnexus generate resource <name>");
|
|
for (const file of scaffoldResource(name)) {
|
|
const target = join(resolve(appRoot), "app", file.path);
|
|
if (existsSync(target)) continue;
|
|
mkdirSync(dirname(target), { recursive: true });
|
|
writeFileSync(target, file.content, "utf8");
|
|
console.log(`✓ Generated app/${file.path}`);
|
|
}
|
|
}
|
|
|
|
function toPascalCase(name: string): string {
|
|
return name
|
|
.split(/[^A-Za-z0-9]+/)
|
|
.filter(Boolean)
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join("");
|
|
}
|
|
|
|
function baseName(name: string): string {
|
|
const parts = name.split("/");
|
|
return parts[parts.length - 1] ?? name;
|
|
}
|
|
|
|
/** Produce the file (relative path + content) for a generate request. */
|
|
export function scaffold(type: GenerateType, name: string): GeneratedFile {
|
|
const clean = name.replace(/\.(wrn|ts)$/, "").replace(/^\/+|\/+$/g, "");
|
|
const pascal = toPascalCase(baseName(clean));
|
|
|
|
switch (type) {
|
|
case "page":
|
|
return {
|
|
path: `pages/${clean}.wrn`,
|
|
content: `page ${pascal} {
|
|
layout = "public"
|
|
|
|
seo {
|
|
title = "${pascal}"
|
|
}
|
|
|
|
view {
|
|
<h1>${pascal}</h1>
|
|
<p>Edit app/pages/${clean}.wrn to build this page.</p>
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
case "component":
|
|
return {
|
|
path: `components/${clean}.wrn`,
|
|
content: `component ${pascal} {
|
|
props {
|
|
label = "${pascal}"
|
|
}
|
|
|
|
view {
|
|
<div class="wrn-${baseName(clean)}">{label}</div>
|
|
}
|
|
}
|
|
`,
|
|
};
|
|
case "api":
|
|
return {
|
|
path: `api/${clean}.ts`,
|
|
content: `import type { Context } from "@wrnexus/core";
|
|
|
|
export async function GET(ctx: Context): Promise<Response> {
|
|
return Response.json({ ok: true, route: ctx.url.pathname });
|
|
}
|
|
`,
|
|
};
|
|
case "schema":
|
|
return {
|
|
path: `schemas/${clean}.ts`,
|
|
content: `import { v } from "@wrnexus/validation";
|
|
|
|
export default v.object({
|
|
name: v.string().min(1, "Required"),
|
|
});
|
|
`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/** Write a scaffolded file under `<appRoot>/app`, refusing to overwrite. */
|
|
export function runGenerate(
|
|
appRoot: string,
|
|
typeArg: string | undefined,
|
|
name: string | undefined,
|
|
): void {
|
|
const type = typeArg ? ALIASES[typeArg] : undefined;
|
|
if (!type || !name) {
|
|
console.error("Usage: wrnexus generate <page|component|layout|api|schema> <name>");
|
|
process.exit(1);
|
|
}
|
|
|
|
const file = scaffold(type, name);
|
|
const target = join(resolve(appRoot), "app", file.path);
|
|
if (existsSync(target)) {
|
|
console.error(`Refusing to overwrite existing file: app/${file.path}`);
|
|
process.exit(1);
|
|
}
|
|
mkdirSync(dirname(target), { recursive: true });
|
|
writeFileSync(target, file.content, "utf8");
|
|
console.log(`✓ Created app/${file.path}`);
|
|
}
|