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
JavaScript
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);
}
}
}