/** * Route compilation + matching. * * Supported segments: * [id] required parameter * [id?] optional parameter * [[id]] optional parameter (directory-friendly form) * [...slug] required catch-all * [[...slug]] optional catch-all */ export interface RouteParam { name: string; optional: boolean; catchAll: boolean; } export interface Route { /** The human-readable route pattern, e.g. `/users/[id]`. */ raw: string; /** Absolute path to the module that handles this route. */ file: string; /** Compiled matcher. */ regex: RegExp; /** Ordered names of dynamic params captured by `regex`. */ paramNames: string[]; /** Rich parameter metadata. Optional for compatibility with old manifests. */ paramMeta?: RouteParam[]; } export interface RouteMatch { route: Route; params: Record; } const ESCAPE_RE = /[.*+?^${}()|[\]\\]/g; function parseParamSegment(segment: string): RouteParam | null { let inner: string | null = null; let optional = false; if (segment.startsWith("[[") && segment.endsWith("]]")) { inner = segment.slice(2, -2); optional = true; } else if (segment.startsWith("[") && segment.endsWith("]")) { inner = segment.slice(1, -1); if (inner.endsWith("?")) { optional = true; inner = inner.slice(0, -1); } } if (inner === null) return null; const catchAll = inner.startsWith("..."); const name = catchAll ? inner.slice(3) : inner; if (!/^[A-Za-z_$][\w$-]*$/.test(name)) { throw new Error(`WRN-ROUTE-PARAM: Invalid route parameter '${segment}'.`); } return { name, optional, catchAll }; } /** Return parameter metadata without requiring callers to inspect the regex. */ export function getRouteParams(raw: string): RouteParam[] { return raw .split("/") .filter(Boolean) .map(parseParamSegment) .filter((value): value is RouteParam => value !== null); } /** Compile a WRNexus route pattern into a RegExp + parameter metadata. */ export function compileRoutePattern( raw: string, ): Pick { if (raw === "/") { return { regex: /^\/$/, paramNames: [], paramMeta: [] }; } const paramMeta: RouteParam[] = []; let source = "^"; const segments = raw.split("/").filter(Boolean); for (const segment of segments) { const param = parseParamSegment(segment); if (!param) { source += `/${segment.replace(ESCAPE_RE, "\\$&")}`; continue; } if (paramMeta.some((existing) => existing.name === param.name)) { throw new Error( `WRN-ROUTE-DUPLICATE-PARAM: Parameter '${param.name}' appears more than once in '${raw}'.`, ); } paramMeta.push(param); const capture = param.catchAll ? "(.+?)" : "([^/]+)"; source += param.optional ? `(?:/${capture})?` : `/${capture}`; } source += "/?$"; return { regex: new RegExp(source), paramNames: paramMeta.map((param) => param.name), paramMeta, }; } function routeSpecificity(route: Route): number[] { const segments = route.raw.split("/").filter(Boolean); let staticCount = 0; let requiredCount = 0; let optionalCount = 0; let catchAllCount = 0; for (const segment of segments) { const param = parseParamSegment(segment); if (!param) staticCount++; else if (param.catchAll) catchAllCount++; else if (param.optional) optionalCount++; else requiredCount++; } return [ staticCount, requiredCount, -optionalCount, -catchAllCount, segments.length, route.raw.length, ]; } /** * Order routes so static and constrained routes win over optional/catch-all * routes. The ordering remains deterministic for identical specificity. */ export function sortRoutes(routes: Route[]): Route[] { return [...routes].sort((a, b) => { const as = routeSpecificity(a); const bs = routeSpecificity(b); for (let i = 0; i < as.length; i++) { if (as[i] !== bs[i]) return bs[i]! - as[i]!; } return a.raw.localeCompare(b.raw) || a.file.localeCompare(b.file); }); } /** Find duplicate URL patterns before request handling starts. */ export function findRouteConflicts(routes: Route[]): Array<{ raw: string; files: string[] }> { const grouped = new Map(); for (const route of routes) { const files = grouped.get(route.raw) ?? []; files.push(route.file); grouped.set(route.raw, files); } return [...grouped] .filter(([, files]) => files.length > 1) .map(([raw, files]) => ({ raw, files })); } /** Find the first route whose pattern matches `pathname`. */ export function matchRoute(routes: Route[], pathname: string): RouteMatch | null { for (const route of routes) { const match = route.regex.exec(pathname); if (!match) continue; const params: Record = {}; try { route.paramNames.forEach((name, index) => { const value = match[index + 1]; if (value !== undefined) params[name] = decodeURIComponent(value); }); } catch { // A malformed percent-encoded path is not a valid route match. Treat it // as a 404 instead of allowing decodeURIComponent to become a 500. return null; } return { route, params }; } return null; }