135 lines
4.1 KiB
TypeScript
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;
|
|
}
|