serverus/handlers/typescript.ts
2025-06-25 16:53:45 -07:00

135 lines
4.1 KiB
TypeScript

/**
* 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<string, any>
) => undefined | Response | Promise<undefined | Response>;
/** 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<string, any>) => Promise<Response> | 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<URLPattern, ROUTE_HANDLER> = new Map<URLPattern, ROUTE_HANDLER>();
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<Response | undefined> {
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 });
if (Deno.env.get('SERVERUS_TYPESCRIPT_IMPORT_LOGGING')) {
console.log(`imported: ${import_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<string, string> = 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;
}