UNPKG

dewy

Version:

Dewy(dǝw-y) is a minimalist HTTP server framework with a small codebase, utilizing built-in URLPattern for efficient routing.

184 lines (183 loc) 6.38 kB
import * as dntShim from "./_dnt.shims.js"; import { RouteNotFoundError } from "./error/route_not_found_error.js"; import { ServerError } from "./error/server_error.js"; /** @internal */ function normalizePath(path) { return "/" + path.replace(/^\/+|\/+$/g, ""); } /** @internal */ function joinPath(path, otherPath) { return normalizePath(path.replace(/\/+$/g, "") + "/" + otherPath.replace(/^\/+/g, "")); } /** @internal */ function normalizePatterns(group, pattern) { const prefix = group?.prefix ?? null; const domains = group?.domains ?? []; if (domains.length === 0) { domains.push(null); } return domains.map((domain) => { if (prefix) { if (typeof pattern === "string") { return new dntShim.URLPattern({ ...domain ? { hostname: domain } : {}, pathname: joinPath(prefix, pattern), }); } return new dntShim.URLPattern({ ...pattern, ...domain ? { hostname: domain } : {}, pathname: joinPath(prefix, pattern.pathname ?? ""), }); } if (typeof pattern === "string") { return new dntShim.URLPattern({ ...domain ? { hostname: domain } : {}, pathname: normalizePath(pattern), }); } if (domain) { return new dntShim.URLPattern({ ...pattern, hostname: domain, }); } if (pattern instanceof dntShim.URLPattern) { return pattern; } return new dntShim.URLPattern(pattern); }); } const ALL_METHOD = ["GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]; const defaultErrorHandler = (error) => { if (error instanceof ServerError) { return new Response(error.message, error.init); } if (error instanceof RouteNotFoundError) { return new Response("Not Found", { status: 404 }); } return new Response("Internal Server Error", { status: 500, }); }; export class Router { constructor(options = {}) { Object.defineProperty(this, "_groups", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "_definedMiddlewares", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "_routes", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "_errorHandler", { enumerable: true, configurable: true, writable: true, value: void 0 }); this._errorHandler = options.errorHandler ?? defaultErrorHandler; } use(...middlewares) { this._definedMiddlewares.push(...middlewares); } group({ domain, prefix, middleware }, handler) { const currentDomains = domain ? (Array.isArray(domain) ? domain : [domain]) : []; const currentMiddlewares = middleware ? (Array.isArray(middleware) ? middleware : [middleware]) : []; const lastGroup = this._groups.at(-1) ?? null; const group = { domains: (lastGroup?.domains ?? []).concat(currentDomains), prefix: joinPath(lastGroup?.prefix ?? "", prefix ?? ""), middlewares: (lastGroup?.middlewares ?? []).concat(currentMiddlewares), }; this._groups.push(group); handler(); this._groups.pop(); } get(pattern, fn) { return this.addRoute({ method: ["GET", "HEAD"], pattern }, fn); } head(pattern, fn) { return this.addRoute({ method: ["HEAD"], pattern }, fn); } post(pattern, fn) { return this.addRoute({ method: ["POST"], pattern }, fn); } put(pattern, fn) { return this.addRoute({ method: ["PUT"], pattern }, fn); } del(pattern, fn) { return this.addRoute({ method: ["DELETE"], pattern }, fn); } options(pattern, fn) { return this.addRoute({ method: ["OPTIONS"], pattern }, fn); } patch(pattern, fn) { return this.addRoute({ method: ["PATCH"], pattern }, fn); } all(pattern, fn) { return this.addRoute({ method: ALL_METHOD, pattern }, fn); } addRoute(route, fn) { const methods = Array.isArray(route.method) ? route.method : [route.method]; const middlewares = route.middleware ? (Array.isArray(route.middleware) ? route.middleware : [route.middleware]) : []; for (let method of methods) { method = method.trim().toUpperCase(); let routeGroup = this._routes.get(method); if (!routeGroup) { routeGroup = []; this._routes.set(method, routeGroup); } const group = this._groups.at(-1) ?? null; for (const pattern of normalizePatterns(group, route.pattern)) { routeGroup.push([pattern, fn, [ ...this._definedMiddlewares, ...group?.middlewares ?? [], ...middlewares, ]]); } } } async dispatch(request) { async function execute(middlewares, fn, ctx) { if (middlewares.length === 0) { return await fn(ctx); } const [middleware, ...nextMiddlewares] = middlewares; return await middleware(ctx, async (nextCtx) => { return await execute(nextMiddlewares, fn, nextCtx ?? ctx); }); } try { const routes = this._routes.get(request.method.toUpperCase()) ?? []; for (const [pattern, fn, middlewares] of routes) { const match = pattern.exec(request.url); if (match) { return await execute(middlewares, fn, { match, request }); } } throw new RouteNotFoundError("Not Found"); } catch (e) { return await this._errorHandler(e); } } }