/** * Handles requests for which there are Typescript files that match * and adhere to the `ROUTE_HANDLER` interface. * @module */ import { walk } from '@std/fs'; import { delay } from '@std/async/delay'; import * as path from '@std/path'; import { getCookies } from '@std/http/cookie'; /** A `PRECHECK` must take a `Request` and `meta` data and return a `Response` IF THERE IS A PROBLEM. */ export type PRECHECK = ( request: Request, meta: Record ) => undefined | Response | Promise; /** A `PRECHECK_TABLE` maps from HTTP methods to an array of `PRECHECK`s to be run. */ export type PRECHECKS_TABLE = Record<'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', PRECHECK[]>; /** A `ROUTE_HANDLER_METHOD` must take a `Request` and `meta` data and return a `Response`. */ export type ROUTE_HANDLER_METHOD = (request: Request, meta: Record) => Promise | Response; /** A `ROUTE_HANDLER` can export methods for handling various HTTP requests. */ export interface ROUTE_HANDLER { PRECHECKS?: PRECHECKS_TABLE; GET?: ROUTE_HANDLER_METHOD; POST?: ROUTE_HANDLER_METHOD; PUT?: ROUTE_HANDLER_METHOD; DELETE?: ROUTE_HANDLER_METHOD; PATCH?: ROUTE_HANDLER_METHOD; default?: ROUTE_HANDLER_METHOD; } const routes: Map = new Map(); let loading: boolean = false; let all_routes_loaded: boolean = false; /** * Handles requests for which there are typescript files in the root tree. * * NOTE: On initial request the tree will be scanned and handlers loaded, * concurrent requests should wait for the load, but until the tree has been * scanned, requests may take longer while the load completes. * * @param request The incoming HTTP request * @returns Either a response (a handler for the request path and method was found) or undefined if unhandled. */ export default async function handle_typescript(request: Request): Promise { if (!all_routes_loaded) { if (!loading) { loading = true; const root_directory = path.resolve(Deno.cwd()); for await ( const entry of walk(root_directory, { exts: ['.ts'], skip: [/\.test\.ts$/] }) ) { if (entry.isFile) { const relative_path = entry.path.substring(root_directory.length); const route_path = relative_path .replace(/\.ts$/, '') //.replace(/\[(\w+)\]/g, ':$1') .replace(/\/index$/, '') .replace(/___/g, ':') || // required for windows, uncivilized OS that it is '/'; const import_path = new URL('file://' + entry.path, import.meta.url).toString(); try { const module: ROUTE_HANDLER = await import(import_path) as ROUTE_HANDLER; const pattern = new URLPattern({ pathname: route_path }); routes.set(pattern, module); } catch (error) { console.error(`Error mounting module ${import_path} at ${route_path}\n\n${error}\n`); } } } all_routes_loaded = true; loading = false; } do { await delay(10); } while (!all_routes_loaded); } for (const [pattern, handler_module] of routes) { const match = pattern.exec(request.url); if (match) { const method = request.method as keyof ROUTE_HANDLER; const method_handler: ROUTE_HANDLER_METHOD = (handler_module[method] ?? handler_module.default) as ROUTE_HANDLER_METHOD; if (!method_handler) { return; } const cookies: Record = getCookies(request.headers); const query = Object.fromEntries(new URL(request.url).searchParams.entries()); const metadata = { cookies, params: match.pathname.groups, query }; const prechecks: PRECHECK[] | undefined = handler_module.PRECHECKS?.[request.method as keyof PRECHECKS_TABLE]; if (Array.isArray(prechecks)) { for (const precheck of prechecks) { const result = await precheck(request, metadata); if (result) { return result; } } } return await method_handler(request, metadata); } } } export function unload(): void { loading = false; routes.clear(); all_routes_loaded = false; }