Files
Router/router.new.ts
2025-07-03 01:48:25 -06:00

282 lines
8.2 KiB
TypeScript

import type { RouterContext as oldContext } from "@bearmetal/router/types";
import { joinPaths } from "./util/join.ts";
import { InternalError, NotFound } from "@bearmetal/router/util/response";
type RouterContext = Omit<oldContext, "pattern">;
type Handler = (
req: Request,
ctx: RouterContext,
next: () => Promise<Response>,
) => Promise<Response>;
type RouteConfigurator = {
get(handler: Handler): RouteConfigurator;
post(handler: Handler): RouteConfigurator;
put(handler: Handler): RouteConfigurator;
delete(handler: Handler): RouteConfigurator;
patch(handler: Handler): RouteConfigurator;
options(handler: Handler): RouteConfigurator;
use(handler: Handler | Router): RouteConfigurator;
};
type RouteConfig = {
handlers: { [method: string]: Handler[] };
pattern: URLPattern;
};
const GET = "GET";
const POST = "POST";
const PUT = "PUT";
const PATCH = "PATCH";
const DELETE = "DELETE";
const OPTIONS = "OPTIONS";
const _use = "_use";
const methods = [GET, POST, PUT, PATCH, DELETE, OPTIONS, _use];
export class Router {
protected routes: Map<string, RouteConfig> = new Map();
/**
* @description defines a new route
* @param path the path to match, uses the same syntax as the URLPattern constructor
* @returns a RouteConfigurator object
*
* @example
* ```ts
* router.route('/users')
* .get((req, ctx) => {
* return new Response('GET /users');
* })
* .post((req, ctx) => {
* return new Response('POST /users');
* });
* ```
*/
route(path: string): RouteConfigurator {
path = fixPath(path);
const routeConfig = this.getOrCreateConfig(path);
const configurator = {
get: (handler: Handler) => {
this.getOrCreateConfigHandlers(GET, routeConfig).push(handler);
return configurator;
},
post: (handler: Handler) => {
this.getOrCreateConfigHandlers(POST, routeConfig).push(handler);
return configurator;
},
put: (handler: Handler) => {
this.getOrCreateConfigHandlers(PUT, routeConfig).push(handler);
return configurator;
},
patch: (handler: Handler) => {
this.getOrCreateConfigHandlers(PATCH, routeConfig).push(handler);
return configurator;
},
delete: (handler: Handler) => {
this.getOrCreateConfigHandlers(DELETE, routeConfig).push(handler);
return configurator;
},
options: (handler: Handler) => {
this.getOrCreateConfigHandlers(OPTIONS, routeConfig).push(handler);
return configurator;
},
use: (handler: Handler | Router) => {
if (handler instanceof Router) {
this.resolveRouterHandlerStack(path, handler);
return configurator;
}
this.getOrCreateConfigHandlers(_use, routeConfig).push(handler);
return configurator;
},
};
return configurator;
}
get(handler: Handler): void;
get(path: string, handler: Handler): void;
get(path: string | Handler, handler?: Handler): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(GET, config).push(handler!);
}
post(handler: Handler): void;
post(path: string, handler: Handler): void;
post(path: string | Handler, handler?: Handler): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(POST, config).push(handler!);
}
put(handler: Handler): void;
put(path: string, handler: Handler): void;
put(path: string | Handler, handler?: Handler): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(PUT, config).push(handler!);
}
patch(handler: Handler): void;
patch(path: string, handler: Handler): void;
patch(path: string | Handler, handler?: Handler): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(PATCH, config).push(handler!);
}
delete(handler: Handler): void;
delete(path: string, handler: Handler): void;
delete(path: string | Handler, handler?: Handler): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(DELETE, config).push(handler!);
}
options(handler: Handler): void;
options(path: string, handler: Handler): void;
options(path: string | Handler, handler?: Handler): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(OPTIONS, config).push(handler!);
}
use(handler: Handler | Router): void;
use(path: string, handler: Handler | Router): void;
use(path: string | Handler | Router, handler?: Handler | Router): void {
if (typeof path !== "string") {
handler = path;
path = "/.*";
} else {
path = fixPath(path);
}
if (handler instanceof Router) {
return this.resolveRouterHandlerStack(path, handler);
}
const config = this.getOrCreateConfig(path);
this.getOrCreateConfigHandlers(_use, config).push(handler!);
}
private getOrCreateConfig(path: string): RouteConfig {
let config = this.routes.get(path);
if (!config) {
config = {
handlers: {},
pattern: new URLPattern({ pathname: path }),
};
this.routes.set(path, config);
}
return config;
}
private getOrCreateConfigHandlers(method: string, config: RouteConfig) {
config.handlers[method] = config.handlers[method] ?? [];
return config.handlers[method];
}
resolveRouterHandlerStack(path: string, router: Router): void {
for (const route of router.rawRoutes) {
const p = joinPaths(path, route[0]);
const thisConfig = this.getOrCreateConfig(p);
const thatConfig = route[1];
for (const method of methods) {
if (!thatConfig.handlers[method]) continue;
thisConfig.handlers[method] = (thisConfig.handlers[method] ?? [])
.concat(thatConfig.handlers[method]);
}
}
}
get rawRoutes() {
return this.routes.entries();
}
get handle() {
return this.handler;
}
async handler(req: Request) {
const url = new URL(req.url);
const method = req.method;
const matchingRoutes = this.findMatchingRoutes(url);
if (!matchingRoutes.length) return NotFound();
const matchingMethods = matchingRoutes.some((r) =>
Object.hasOwn(r.config.handlers, method)
);
if (!matchingMethods) {
return new Response("Method Not Allowed", { status: 405 });
}
const middlewareStack = matchingRoutes.flatMap((r) =>
(r.config.handlers[_use] ?? []).concat(r.config.handlers[method] ?? [])
);
const ctx: RouterContext = {
url,
params: matchingRoutes.reduce((a, b) => ({ ...a, ...b.params }), {}),
state: {},
request: req,
};
let index = 0;
const executeMiddleware = async (): Promise<Response> => {
if (index < middlewareStack.length) {
const res = await middlewareStack[index++]?.(
req,
ctx,
executeMiddleware,
);
if (res instanceof Response) return res;
}
return new Response("End of stack", { status: 501 });
};
try {
return await executeMiddleware();
} catch {
return InternalError();
}
}
private findMatchingRoutes(url: URL) {
return this.routes.values().map((route) => {
const result = route.pattern.exec(url);
if (result) {
return {
config: route,
params: result.pathname.groups,
};
}
}).filter((r) => !!r).toArray();
}
}
function fixPath(path: string) {
path = path.startsWith("/") ? path : `/${path}`;
return path;
}
export default Router;