/** * Typed-routes codegen. From the scanned page routes, emit `app/routes.gen.ts` * with a `Routes` map (path -> param types) and an `href()` builder. */ import { getRouteParams, type Route } from "./match.ts"; import { nameRoutes } from "./named.ts"; export function generateRoutesFile(pages: Route[]): string { const seen = new Set(); const entries: string[] = []; const namedEntries: string[] = []; for (const page of [...pages].sort((a, b) => a.raw.localeCompare(b.raw))) { if (seen.has(page.raw)) continue; seen.add(page.raw); const params = page.paramMeta ?? getRouteParams(page.raw); const type = params.length ? `{ ${params .map((param) => { const key = `${JSON.stringify(param.name)}${param.optional ? "?" : ""}`; const value = param.catchAll ? "string | readonly string[]" : "string"; return `${key}: ${value}`; }) .join("; ")} }` : "Record"; entries.push(` ${JSON.stringify(page.raw)}: ${type};`); } for (const page of nameRoutes([...pages].sort((a, b) => a.raw.localeCompare(b.raw)))) namedEntries.push(` ${JSON.stringify(page.name)}: ${JSON.stringify(page.raw)};`); return `// AUTO-GENERATED by \`wrnexus dev\` - do not edit. // Typed routes support required, optional, and catch-all parameters. export interface Routes { ${entries.join("\n") || " [path: string]: Record;"} } export interface RouteNames { ${namedEntries.join("\n") || " [name: string]: string;"} } export interface RouteQueries { [path: string]: Record; } export type RoutePath = keyof Routes; export type RouteName = keyof RouteNames; export type RouteQuery

= P extends keyof RouteQueries ? RouteQueries[P] : Record; type RouteValue = string | readonly string[] | undefined; function encodeRouteValue(value: RouteValue, catchAll: boolean): string { if (value === undefined) return ""; const values = Array.isArray(value) ? value : catchAll ? String(value).split("/") : [String(value)]; return values.map((part) => encodeURIComponent(part)).join("/"); } function buildHref(path: string, params: Record = {}): string { const output: string[] = []; for (const segment of path.split("/").filter(Boolean)) { let name: string | undefined; let optional = false; let catchAll = false; if (segment.startsWith("[[") && segment.endsWith("]]")) { optional = true; name = segment.slice(2, -2); } else if (segment.startsWith("[") && segment.endsWith("]")) { name = segment.slice(1, -1); if (name.endsWith("?")) { optional = true; name = name.slice(0, -1); } } if (!name) { output.push(segment); continue; } if (name.startsWith("...")) { catchAll = true; name = name.slice(3); } const value = params[name]; if (value === undefined && optional) continue; if (value === undefined) throw new Error(\`WRN-ROUTE-MISSING-PARAM: Missing route parameter '\${name}'.\`); output.push(encodeRouteValue(value, catchAll)); } return "/" + output.filter(Boolean).join("/"); } export function href

( path: P, ...args: keyof Routes[P] extends never ? [] : Record extends Routes[P] ? [params?: Routes[P]] : [params: Routes[P]] ): string { const params = (args[0] ?? {}) as Record; return buildHref(String(path), params); } export function route( name: N, ...args: keyof Routes[RouteNames[N]] extends never ? [params?: Routes[RouteNames[N]], query?: RouteQuery] : Record extends Routes[RouteNames[N]] ? [params?: Routes[RouteNames[N]], query?: RouteQuery] : [params: Routes[RouteNames[N]], query?: RouteQuery] ): string { const paths: Record = ${JSON.stringify(Object.fromEntries(nameRoutes([...pages].sort((a, b) => a.raw.localeCompare(b.raw))).map((page) => [page.name, page.raw])), null, 2)} as Record; const output = buildHref(paths[name], (args[0] ?? {}) as Record); const query = args[1]; if (!query) return output; const search = new URLSearchParams(); for (const [key, value] of Object.entries(query)) if (value !== undefined && value !== null) search.set(key, String(value)); const text = search.toString(); return text ? \`\${output}?\${text}\` : output; } `; }