import { http } from "./deps.ts";import { requestContext } from "./http.ts";
import type { RouterRequest, RouterShape, Handler } from "./client.ts";
export interface Stack<S extends RouterShape = RouterShape> { (req: RouterRequest<S>, connInfo: http.ConnInfo): Promise<Response>; readonly routes: S;}
const nextPathGroupName = "__nextPath";
export function stack<S extends RouterShape>(routes: S): Stack<S> { for (const [k, _] of Object.entries(routes)) { const split = k.split("/").filter(v => !!v);
if (split[split.length-1] === "*") { split.pop(); }
for (const s of split) { if ( !s.match(/^:[a-zA-Z_$][a-zA-Z_$0-9]$/) && !s.match(/^[^:*?(){}]*$/) ) { throw new SyntaxError( `"${k}" isn't a valid stack route. Stack routes only support basic path segments and named path segments (groups). Non-trailing wildcards, RegExp, optionals, and other advanced URLPattern syntax is not supported`, ); } } }
const handlers: Record<string, Handler | Handler[]> = {}; for (const k of Object.keys(routes)) { const v = routes[k]; if (v && typeof v === "object" && !Array.isArray(v)) { handlers[k] = stack(v); } else if (v) { handlers[k] = v; } }
const paths = Object.keys(handlers); const sortedPaths: string[] = paths.sort((a, b) => { if (a === b) { return 0; } if (a === "*" || a === "/*") { return 1; } if (b === "*" || b === "/*") { return -1; }
if (a.endsWith("/*")) { a = a.slice(0, a.length - 2); } if (b.endsWith("/*")) { b = b.slice(0, b.length - 2); } const la = a.split("/").filter(v => !!v).length; const lb = b.split("/").filter(v => !!v).length; if (la !== lb) { return lb - la; }
return paths.indexOf(a) - paths.indexOf(b); });
const patterns = new Map<URLPattern, Handler | Handler[]>(); for (const op of sortedPaths) { let p = "/" + op.split("/").filter(v => !!v).join("/");
if (p.endsWith("/*")) { p = p.slice(0, p.length - 2); } p = `${p}/:${nextPathGroupName}*/*?`; patterns.set(new URLPattern(p, "http://_._"), handlers[op]); }
const handler = async ( req: Request, conn: http.ConnInfo, ): Promise<Response> => { const reqCtx = requestContext(req); if (reqCtx.redirect) { return reqCtx.redirect; }
let last404 = new Response("404 not found", { status: 404 });
for (const [pattern, handler] of patterns.entries()) { const match = pattern.exec(reqCtx.path, "http://_._"); if (!match) { continue; }
const groups = { ...reqCtx.groups, ...match.pathname.groups }; const path = `/${groups[nextPathGroupName] || ""}`; delete groups[nextPathGroupName];
const oPath = reqCtx.path; const oGroups = reqCtx.groups;
Object.assign(reqCtx, { path, groups });
try { if (Array.isArray(handler)) { for (const h of handler) { const response = await h(req, conn); if (didMatch(response)) { return response; } else { last404 = response; } } } else { const response = await handler(req, conn); if (didMatch(response)) { return response; } else { last404 = response; } } } finally { Object.assign(req, { path: oPath, groups: oGroups }); } }
return last404; };
return Object.assign(handler, { routes });}
function didMatch(response: Response): boolean { if (response.status === 404) { const mime = response.headers.get("content-type"); if (mime === "text/plain") { return false; } } return true;}