bunway
Version:
Express-style routing toolkit built natively for Bun.
178 lines • 6.41 kB
JavaScript
import { createAutoBodyParser } from "../middlewares/bodyParser";
import { DEFAULT_BODY_PARSER_OPTIONS, resolveBodyParserOptions, } from "../config";
import { WayContext } from "./context";
import { buildHttpErrorResponse, HttpError, isHttpError } from "./errors";
/**
* BunWay router implementation.
*
* Usage mirrors Express:
* ```ts
* const router = new Router();
* router.use(cors());
* router.get("/health", (ctx) => ctx.res.text("OK"));
* router.post("/users", async (ctx) => ctx.res.created(await ctx.req.parseBody()));
* ```
*
* Internally the router still works with {@link Request}/{@link Response}, which
* means handlers can choose to return a native Response or use `ctx.res` helpers.
* Middleware-supplied extras (CORS headers, etc.) are merged in the finalizer so
* behaviour stays consistent regardless of handler style.
*/
export class Router {
routes = [];
children = [];
middlewares = []; // global middleware
bodyParserConfig;
autoBodyParser;
constructor(options) {
this.bodyParserConfig = resolveBodyParserOptions(options?.bodyParser, DEFAULT_BODY_PARSER_OPTIONS);
this.autoBodyParser = createAutoBodyParser((ctx) => ctx.req.getBodyParserConfig());
}
// Route registration
get(path, ...handlers) {
this.add("GET", path, handlers);
}
post(path, ...handlers) {
this.add("POST", path, handlers);
}
put(path, ...handlers) {
this.add("PUT", path, handlers);
}
delete(path, ...handlers) {
this.add("DELETE", path, handlers);
}
patch(path, ...handlers) {
this.add("PATCH", path, handlers);
}
options(path, ...handlers) {
this.add("OPTIONS", path, handlers);
}
use(a, b) {
if (typeof a === "string" && b) {
this.children.push({ prefix: a, router: b });
return;
}
if (typeof a === "function") {
this.middlewares.push(a);
return;
}
throw new Error("Invalid use() signature");
}
setBodyParser(options) {
this.bodyParserConfig = resolveBodyParserOptions(options, DEFAULT_BODY_PARSER_OPTIONS);
}
configureBodyParser(options) {
this.bodyParserConfig = resolveBodyParserOptions(options, this.bodyParserConfig);
}
/** Snapshot of the current resolved parser configuration. */
getBodyParserConfig() {
return this.bodyParserConfig;
}
async handle(req) {
const url = new URL(req.url);
const method = req.method.toUpperCase();
const pathname = url.pathname;
// 1) Sub-routers
for (const { prefix, router } of this.children) {
if (pathname.startsWith(prefix)) {
const newUrl = new URL(req.url);
newUrl.pathname = pathname.slice(prefix.length) || "/";
const newReq = new Request(newUrl.toString(), req); // TS-safe
return await router.handle(newReq);
}
}
// 2) Match routes
for (const route of this.routes) {
if (route.method !== method)
continue;
const match = route.regex.exec(pathname);
if (!match)
continue;
// Params
const params = {};
route.keys.forEach((key, i) => {
params[key] = match[i + 1];
});
// Context
const ctx = new WayContext(req, { bodyParser: this.bodyParserConfig });
ctx.req.params = params;
// Pipeline: global middleware first, then route handlers
// Compose middleware in the same order express does.
const pipeline = [...this.middlewares, this.autoBodyParser, ...route.handlers];
let idx = 0;
let finalResponse = null;
const next = async () => {
const handler = pipeline[idx++];
if (!handler)
return;
const result = await handler(ctx, next);
if (result instanceof Response) {
finalResponse = result;
}
};
try {
await next();
}
catch (err) {
const errorResponse = resolveRouterError(err, ctx);
return finalizeResponse(ctx, errorResponse);
}
return finalizeResponse(ctx, finalResponse);
}
// 3) 404
return new Response(JSON.stringify({ error: "Not Found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
// Internals
add(method, path, handlers) {
const { regex, keys } = this.pathToRegex(path);
this.routes.push({ method, path, regex, keys, handlers });
}
pathToRegex(path) {
const keys = [];
const regexStr = path
.replace(/\/:([^/]+)/g, (_, key) => {
keys.push(key);
return "/([^/]+)";
})
.replace(/\//g, "\\/");
return { regex: new RegExp(`^${regexStr}$`), keys };
}
}
/**
* Applies final adjustments (like middleware supplied headers) to the
* response that will ultimately be returned to Bun.
*/
function finalizeResponse(ctx, explicit) {
let response = explicit ?? ctx.res.last ?? new Response(null, { status: 200 });
const extraHeaders = ctx.req.locals.__corsHeaders;
if (extraHeaders && Object.keys(extraHeaders).length > 0) {
const merged = new Headers(response.headers);
for (const [key, value] of Object.entries(extraHeaders)) {
merged.set(key, value);
}
response = new Response(response.body, {
status: response.status,
headers: merged,
});
}
return response;
}
/**
* Converts thrown values into user-friendly HTTP responses.
* Prefers HttpError instances created by middleware/handlers but will fall
* back to a generic 500 response for unexpected errors.
*/
function resolveRouterError(err, ctx) {
if (isHttpError(err)) {
return buildHttpErrorResponse(ctx, err);
}
const error = err instanceof Error ? err : undefined;
const httpError = new HttpError(500, "Internal Server Error", {
cause: error,
});
return buildHttpErrorResponse(ctx, httpError);
}
//# sourceMappingURL=router.js.map