Skip to content

Commit 0a65580

Browse files
authored
Merge pull request #64 from joaovjo/feature/sprint-10
feat(routing): typed route registry + routes:compile (Sprint 10)
2 parents 321ca80 + b0c092b commit 0a65580

40 files changed

Lines changed: 713 additions & 36 deletions

dist/index.d.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export type {
6565
MigrateRefreshCommandOptions,
6666
MigrateRollbackCommandOptions,
6767
ParsedArguments,
68+
RoutesCompileCommandOptions,
6869
StubWriteResult,
6970
} from "@ninots/console";
7071
export {
@@ -81,6 +82,7 @@ export {
8182
MigrateRollbackCommand,
8283
OutputStyle,
8384
PathResolver,
85+
RoutesCompileCommand,
8486
StubExistsError,
8587
applyStubReplacements,
8688
migrationTimestamp,
@@ -167,8 +169,17 @@ export type {
167169
RouteHandler,
168170
RouteMatch,
169171
RouteParams,
172+
RouteRegistry,
173+
RouteResolver,
174+
} from "@ninots/routing";
175+
export {
176+
Route,
177+
Router,
178+
emitRouteRegistry,
179+
loadRoutes,
180+
route,
181+
setRouteResolver,
170182
} from "@ninots/routing";
171-
export { Route, Router } from "@ninots/routing";
172183
export * from "@ninots/session";
173184
export * from "@ninots/support";
174185
export * from "@ninots/validation";

index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export type {
6565
MigrateRefreshCommandOptions,
6666
MigrateRollbackCommandOptions,
6767
ParsedArguments,
68+
RoutesCompileCommandOptions,
6869
StubWriteResult,
6970
} from "@ninots/console";
7071
export {
@@ -81,6 +82,7 @@ export {
8182
MigrateRollbackCommand,
8283
OutputStyle,
8384
PathResolver,
85+
RoutesCompileCommand,
8486
StubExistsError,
8587
applyStubReplacements,
8688
migrationTimestamp,
@@ -167,8 +169,17 @@ export type {
167169
RouteHandler,
168170
RouteMatch,
169171
RouteParams,
172+
RouteRegistry,
173+
RouteResolver,
174+
} from "@ninots/routing";
175+
export {
176+
Route,
177+
Router,
178+
emitRouteRegistry,
179+
loadRoutes,
180+
route,
181+
setRouteResolver,
170182
} from "@ninots/routing";
171-
export { Route, Router } from "@ninots/routing";
172183
export * from "@ninots/session";
173184
export * from "@ninots/support";
174185
export * from "@ninots/validation";

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"verify:all": "bun run verify:no-any && bun run type-check && bun run test && bun run lint",
3838
"verify:explicit-types": "tsc --noImplicitAny --strict --noEmit",
3939
"verify:no-any": "bun run scripts/verify.ts no-any",
40+
"verify:route-types": "bun run scripts/verify-route-types.ts",
4041
"verify:tests": "bun test"
4142
},
4243
"bin": {

packages/console/dist/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export { MakeViewCommand, type MakeViewCommandOptions } from "./src/commands/mak
1212
export { MigrateCommand, type MigrateCommandOptions } from "./src/commands/migrate-command";
1313
export { MigrateRefreshCommand, type MigrateRefreshCommandOptions, } from "./src/commands/migrate-refresh-command";
1414
export { MigrateRollbackCommand, type MigrateRollbackCommandOptions, } from "./src/commands/migrate-rollback-command";
15+
export { RoutesCompileCommand, type RoutesCompileCommandOptions, } from "./src/commands/routes-compile-command";
1516
export { Command } from "./src/command";
1617
export { PathResolver, StubExistsError, applyStubReplacements, migrationTimestamp, normalizeControllerName, normalizeMigrationName, normalizeModelName, normalizeModuleName, normalizeViewName, type GeneratorPathsConfig, type StubWriteResult, } from "./src/generator";
1718
export type { CommandInterface } from "./src/contracts/command-interface";

packages/console/dist/src/commands/make-controller-command.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export declare class MakeControllerCommand extends Command {
1212
private readonly resolver;
1313
constructor(options?: MakeControllerCommandOptions);
1414
handle(): Promise<number>;
15+
private ensureResourceView;
1516
private ensureWebBinding;
1617
private writeWebResourceRoutes;
1718
private writeApiResourceRoutes;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Router } from "@ninots/routing";
2+
import { Command } from "../command";
3+
export interface RoutesCompileCommandOptions {
4+
readonly resolveRouter: () => Router | Promise<Router>;
5+
}
6+
/**
7+
* Compile a typed route registry (`.d.ts`) from the application's Router.
8+
*
9+
* Boot path: inject `resolveRouter` (same pattern as MigrateCommand) —
10+
* typically `bootstrap()` → `ROUTER_KEY` → `getRoutes()`. Writes via `Bun.write`.
11+
*/
12+
export declare class RoutesCompileCommand extends Command {
13+
private readonly options;
14+
signature: string;
15+
description: string;
16+
constructor(options: RoutesCompileCommandOptions);
17+
handle(): Promise<number>;
18+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
export declare const STUB_TEMPLATES: {
2-
readonly "api-resource-routes": " router.get(\"/{{ routePrefix }}\", () => {{ variableName }}.index());\n router.get(\"/{{ routePrefix }}/:id\", (request, params) => {{ variableName }}.show(request, params));\n router.put(\"/{{ routePrefix }}/:id\", (request, params) => {{ variableName }}.update(request, params));\n router.delete(\"/{{ routePrefix }}/:id\", (request, params) => {{ variableName }}.destroy(request, params));\n";
2+
readonly "api-resource-routes": " router.get(\"/{{ routePrefix }}\", () => {{ variableName }}.index()).name(\"{{ routePrefix }}.index\");\n router.get(\"/{{ routePrefix }}/:id\", (request, params) => {{ variableName }}.show(request, params)).name(\"{{ routePrefix }}.show\");\n router.put(\"/{{ routePrefix }}/:id\", (request, params) => {{ variableName }}.update(request, params)).name(\"{{ routePrefix }}.update\");\n router.delete(\"/{{ routePrefix }}/:id\", (request, params) => {{ variableName }}.destroy(request, params)).name(\"{{ routePrefix }}.destroy\");\n";
33
readonly controller: "import { Controller } from \"@/app/Http/Controllers/Controller\";\n\n/**\n * {{ className }} HTTP controller.\n */\nexport class {{ className }} extends Controller {\n}\n";
44
readonly "controller-api": "import type { RouteParams } from \"@ninots/framework\";\nimport { Controller } from \"@/app/Http/Controllers/Controller\";\n\n/**\n * {{ className }} API resource controller (no POST routes).\n */\nexport class {{ className }} extends Controller {\n public async index(): Promise<Response> {\n return this.json([]);\n }\n\n public async show(_request: Request, params?: RouteParams): Promise<Response> {\n return this.json({ id: params?.id });\n }\n\n public async update(request: Request, params?: RouteParams): Promise<Response> {\n const data = (await request.json()) as Record<string, unknown>;\n return this.json({ id: params?.id, ...data });\n }\n\n public async destroy(_request: Request, params?: RouteParams): Promise<Response> {\n return this.noContent();\n }\n}\n";
55
readonly "controller-resource": "import type { RouteParams } from \"@ninots/framework\";\nimport { render } from \"@ninots/view\";\nimport { Controller } from \"@/app/Http/Controllers/Controller\";\nimport { {{ viewExportName }} } from \"{{ viewImportPath }}\";\n\n/**\n * {{ className }} resource controller (web + CSRF group).\n */\nexport class {{ className }} extends Controller {\n public async index(): Promise<Response> {\n return render({{ viewExportName }}, {});\n }\n\n public async create(): Promise<Response> {\n return render({{ viewExportName }}, { title: \"Create\" });\n }\n\n public async store(_request: Request): Promise<Response> {\n return this.redirect(\"/{{ routePrefix }}\");\n }\n\n public async show(_request: Request, params?: RouteParams): Promise<Response> {\n return render({{ viewExportName }}, { id: params?.id });\n }\n\n public async edit(_request: Request, params?: RouteParams): Promise<Response> {\n return render({{ viewExportName }}, { id: params?.id, title: \"Edit\" });\n }\n\n public async update(_request: Request, params?: RouteParams): Promise<Response> {\n return this.json({ id: params?.id, updated: true });\n }\n\n public async destroy(_request: Request, params?: RouteParams): Promise<Response> {\n return this.noContent();\n }\n}\n";
66
readonly migration: "import type { Connection, Migration } from \"@ninots/framework\";\n\n/**\n * {{ className }} migration.\n */\nexport default class {{ className }} implements Migration {\n public async up(connection: Connection): Promise<void> {\n await connection.run(`\n CREATE TABLE IF NOT EXISTS {{ tableName }} (\n id INTEGER PRIMARY KEY AUTOINCREMENT\n )\n `);\n }\n\n public async down(connection: Connection): Promise<void> {\n await connection.run(\"DROP TABLE IF EXISTS {{ tableName }}\");\n }\n}\n";
77
readonly model: "import { Model, Table } from \"@ninots/framework\";\n\n/**\n * {{ className }} model.\n */\n@Table(\"{{ tableName }}\")\nexport class {{ className }} extends Model {\n protected static override fillable: string[] = [];\n}\n";
88
readonly view: "import { withLayout } from \"@ninots/view\";\nimport { AppLayout } from \"@/resources/views/layouts/app\";\n\nexport interface {{ exportName }}Props {\n title?: string;\n id?: string;\n}\n\nfunction {{ exportName }}Page({ title = \"{{ exportName }}\", id }: {{ exportName }}Props) {\n return (\n <section className=\"welcome\">\n <h1>{title}</h1>\n {id ? <p>Resource ID: {id}</p> : null}\n </section>\n );\n}\n\nexport const {{ exportName }} = withLayout(AppLayout, {{ exportName }}Page, { title: \"{{ exportName }} — Ninots\" });\n";
9-
readonly "web-resource-routes": " router.get(\"/{{ routePrefix }}\", () => {{ variableName }}.index());\n router.get(\"/{{ routePrefix }}/create\", () => {{ variableName }}.create());\n router.post(\"/{{ routePrefix }}\", (request) => {{ variableName }}.store(request));\n router.get(\"/{{ routePrefix }}/:id\", (request, params) => {{ variableName }}.show(request, params));\n router.get(\"/{{ routePrefix }}/:id/edit\", (request, params) => {{ variableName }}.edit(request, params));\n router.put(\"/{{ routePrefix }}/:id\", (request, params) => {{ variableName }}.update(request, params));\n router.delete(\"/{{ routePrefix }}/:id\", (request, params) => {{ variableName }}.destroy(request, params));\n";
9+
readonly "web-resource-routes": " router.get(\"/{{ routePrefix }}\", () => {{ variableName }}.index()).name(\"{{ routePrefix }}.index\");\n router.get(\"/{{ routePrefix }}/create\", () => {{ variableName }}.create()).name(\"{{ routePrefix }}.create\");\n router.post(\"/{{ routePrefix }}\", (request) => {{ variableName }}.store(request)).name(\"{{ routePrefix }}.store\");\n router.get(\"/{{ routePrefix }}/:id\", (request, params) => {{ variableName }}.show(request, params)).name(\"{{ routePrefix }}.show\");\n router.get(\"/{{ routePrefix }}/:id/edit\", (request, params) => {{ variableName }}.edit(request, params)).name(\"{{ routePrefix }}.edit\");\n router.put(\"/{{ routePrefix }}/:id\", (request, params) => {{ variableName }}.update(request, params)).name(\"{{ routePrefix }}.update\");\n router.delete(\"/{{ routePrefix }}/:id\", (request, params) => {{ variableName }}.destroy(request, params)).name(\"{{ routePrefix }}.destroy\");\n";
1010
readonly "module-provider": "import type { Application } from \"@ninots/framework\";\nimport { ROUTER_KEY, ServiceProvider } from \"@ninots/framework\";\nimport type { Router } from \"@ninots/framework\";\nimport { {{ routesRegisterName }} } from \"{{ routesImportPath }}\";\n\n/**\n * {{ className }} module service provider.\n *\n * Loads module routes via {{ routesRegisterName }} — do not append to routes/web.ts.\n */\nexport class {{ providerClassName }} extends ServiceProvider {\n constructor(app: Application) {\n super(app.container);\n }\n\n public override register(): void {\n // Bind module services here.\n }\n\n public override boot(): void {\n const router = this.app.make<Router>(ROUTER_KEY);\n {{ routesRegisterName }}(router);\n }\n}\n";
11-
readonly "module-routes": "import type { Router } from \"@ninots/framework\";\n\n/**\n * Register HTTP routes for the {{ className }} module.\n */\nexport function {{ routesRegisterName }}(router: Router): void {\n router.group({ prefix: \"/{{ routePrefix }}\", middleware: [\"web\"] }, () => {\n // Module routes go here.\n });\n}\n";
11+
readonly "module-routes": "import type { Router } from \"@ninots/framework\";\n\n/**\n * Register HTTP routes for the {{ className }} module.\n */\nexport function {{ routesRegisterName }}(router: Router): void {\n router.group({ prefix: \"/{{ routePrefix }}\", middleware: [\"web\"] }, () => {\n router.get(\"/\", () => new Response(\"{{ className }} module\")).name(\"{{ routePrefix }}.index\");\n });\n}\n";
1212
};
1313
export type StubTemplateName = keyof typeof STUB_TEMPLATES;
1414
export declare function getStubTemplate(name: StubTemplateName): string;

packages/console/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export {
1919
MigrateRollbackCommand,
2020
type MigrateRollbackCommandOptions,
2121
} from "./src/commands/migrate-rollback-command";
22+
export {
23+
RoutesCompileCommand,
24+
type RoutesCompileCommandOptions,
25+
} from "./src/commands/routes-compile-command";
2226

2327
export { Command } from "./src/command";
2428
export {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { join } from "node:path";
2+
import type { Router } from "@ninots/routing";
3+
import { emitRouteRegistry } from "@ninots/routing";
4+
import { Command } from "../command";
5+
6+
export interface RoutesCompileCommandOptions {
7+
readonly resolveRouter: () => Router | Promise<Router>;
8+
}
9+
10+
/**
11+
* Compile a typed route registry (`.d.ts`) from the application's Router.
12+
*
13+
* Boot path: inject `resolveRouter` (same pattern as MigrateCommand) —
14+
* typically `bootstrap()` → `ROUTER_KEY` → `getRoutes()`. Writes via `Bun.write`.
15+
*/
16+
export class RoutesCompileCommand extends Command {
17+
signature = "routes:compile {--out=types/routes.d.ts}";
18+
description = "Compile typed route registry from registered routes";
19+
20+
constructor(private readonly options: RoutesCompileCommandOptions) {
21+
super();
22+
}
23+
24+
async handle(): Promise<number> {
25+
try {
26+
const router = await this.options.resolveRouter();
27+
const outOption = this.option("out");
28+
const outRel = typeof outOption === "string" && outOption.length > 0 ? outOption : "types/routes.d.ts";
29+
const outPath = join(process.cwd(), outRel);
30+
31+
const content = emitRouteRegistry(router.getRoutes());
32+
await Bun.write(outPath, content);
33+
this.success(`Wrote ${outRel}`);
34+
return 0;
35+
} catch (error: unknown) {
36+
const message = error instanceof Error ? error.message : String(error);
37+
this.error(`routes:compile failed: ${message}`);
38+
return 1;
39+
}
40+
}
41+
}

packages/console/src/generator/stub-templates.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
export const STUB_TEMPLATES = {
2-
"api-resource-routes": ` router.get("/{{ routePrefix }}", () => {{ variableName }}.index());
3-
router.get("/{{ routePrefix }}/:id", (request, params) => {{ variableName }}.show(request, params));
4-
router.put("/{{ routePrefix }}/:id", (request, params) => {{ variableName }}.update(request, params));
5-
router.delete("/{{ routePrefix }}/:id", (request, params) => {{ variableName }}.destroy(request, params));
2+
"api-resource-routes": ` router.get("/{{ routePrefix }}", () => {{ variableName }}.index()).name("{{ routePrefix }}.index");
3+
router.get("/{{ routePrefix }}/:id", (request, params) => {{ variableName }}.show(request, params)).name("{{ routePrefix }}.show");
4+
router.put("/{{ routePrefix }}/:id", (request, params) => {{ variableName }}.update(request, params)).name("{{ routePrefix }}.update");
5+
router.delete("/{{ routePrefix }}/:id", (request, params) => {{ variableName }}.destroy(request, params)).name("{{ routePrefix }}.destroy");
66
`,
77
controller: `import { Controller } from "@/app/Http/Controllers/Controller";
88
@@ -123,13 +123,13 @@ function {{ exportName }}Page({ title = "{{ exportName }}", id }: {{ exportName
123123
124124
export const {{ exportName }} = withLayout(AppLayout, {{ exportName }}Page, { title: "{{ exportName }} — Ninots" });
125125
`,
126-
"web-resource-routes": ` router.get("/{{ routePrefix }}", () => {{ variableName }}.index());
127-
router.get("/{{ routePrefix }}/create", () => {{ variableName }}.create());
128-
router.post("/{{ routePrefix }}", (request) => {{ variableName }}.store(request));
129-
router.get("/{{ routePrefix }}/:id", (request, params) => {{ variableName }}.show(request, params));
130-
router.get("/{{ routePrefix }}/:id/edit", (request, params) => {{ variableName }}.edit(request, params));
131-
router.put("/{{ routePrefix }}/:id", (request, params) => {{ variableName }}.update(request, params));
132-
router.delete("/{{ routePrefix }}/:id", (request, params) => {{ variableName }}.destroy(request, params));
126+
"web-resource-routes": ` router.get("/{{ routePrefix }}", () => {{ variableName }}.index()).name("{{ routePrefix }}.index");
127+
router.get("/{{ routePrefix }}/create", () => {{ variableName }}.create()).name("{{ routePrefix }}.create");
128+
router.post("/{{ routePrefix }}", (request) => {{ variableName }}.store(request)).name("{{ routePrefix }}.store");
129+
router.get("/{{ routePrefix }}/:id", (request, params) => {{ variableName }}.show(request, params)).name("{{ routePrefix }}.show");
130+
router.get("/{{ routePrefix }}/:id/edit", (request, params) => {{ variableName }}.edit(request, params)).name("{{ routePrefix }}.edit");
131+
router.put("/{{ routePrefix }}/:id", (request, params) => {{ variableName }}.update(request, params)).name("{{ routePrefix }}.update");
132+
router.delete("/{{ routePrefix }}/:id", (request, params) => {{ variableName }}.destroy(request, params)).name("{{ routePrefix }}.destroy");
133133
`,
134134
"module-provider": `import type { Application } from "@ninots/framework";
135135
import { ROUTER_KEY, ServiceProvider } from "@ninots/framework";
@@ -163,7 +163,7 @@ export class {{ providerClassName }} extends ServiceProvider {
163163
*/
164164
export function {{ routesRegisterName }}(router: Router): void {
165165
router.group({ prefix: "/{{ routePrefix }}", middleware: ["web"] }, () => {
166-
// Module routes go here.
166+
router.get("/", () => new Response("{{ className }} module")).name("{{ routePrefix }}.index");
167167
});
168168
}
169169
`,

0 commit comments

Comments
 (0)