bun-route
Version:
A fast, Express-like router for the high-performance bun.serve() HTTP server.
1,039 lines (1,035 loc) • 31.3 kB
JavaScript
// src/method.ts
var HttpMethod;
((HttpMethod2) => {
HttpMethod2[HttpMethod2["ALL"] = 1] = "ALL";
HttpMethod2[HttpMethod2["GET"] = 2] = "GET";
HttpMethod2[HttpMethod2["PUT"] = 3] = "PUT";
HttpMethod2[HttpMethod2["POST"] = 4] = "POST";
HttpMethod2[HttpMethod2["PATCH"] = 5] = "PATCH";
HttpMethod2[HttpMethod2["DELETE"] = 6] = "DELETE";
HttpMethod2[HttpMethod2["HEAD"] = 7] = "HEAD";
HttpMethod2[HttpMethod2["OPTIONS"] = 8] = "OPTIONS";
HttpMethod2[HttpMethod2["TRACE"] = 9] = "TRACE";
HttpMethod2[HttpMethod2["CONNECT"] = 10] = "CONNECT";
HttpMethod2[HttpMethod2["UNKNOWN"] = 11] = "UNKNOWN";
})(HttpMethod ||= {});
function parseHttpMethods(method) {
switch (method) {
case "*":
return 1 /* ALL */;
case "GET":
return 2 /* GET */;
case "PUT":
return 3 /* PUT */;
case "POST":
return 4 /* POST */;
case "PATCH":
return 5 /* PATCH */;
case "DELETE":
return 6 /* DELETE */;
case "HEAD":
return 7 /* HEAD */;
case "OPTIONS":
return 8 /* OPTIONS */;
case "TRACE":
return 9 /* TRACE */;
case "CONNECT":
return 10 /* CONNECT */;
default:
return 11 /* UNKNOWN */;
}
}
function stringifyHttpMethods(method) {
switch (method) {
case 1 /* ALL */:
return "ALL";
case 2 /* GET */:
return "GET";
case 3 /* PUT */:
return "PUT";
case 4 /* POST */:
return "POST";
case 5 /* PATCH */:
return "PATCH";
case 6 /* DELETE */:
return "DELETE";
case 7 /* HEAD */:
return "HEAD";
case 8 /* OPTIONS */:
return "OPTIONS";
case 9 /* TRACE */:
return "TRACE";
case 10 /* CONNECT */:
return "CONNECT";
case undefined:
return "ALL";
default:
return "UNKNOWN";
}
}
// src/middleware.ts
function unmergeRequestMiddleware(...middlewares) {
const foundMiddlewares = [];
for (const middleware of middlewares) {
if (isMergedRequestMiddleware(middleware)) {
foundMiddlewares.push(...unmergeRequestMiddleware(...middleware.base));
} else {
foundMiddlewares.push(middleware);
}
}
return foundMiddlewares;
}
function mergeRequestMiddlewares(...middlewares) {
if (middlewares.length == 0) {
throw new Error("no middlewares specified");
} else if (middlewares.length == 1) {
return middlewares[0];
}
middlewares = unmergeRequestMiddleware(...middlewares);
const mergedAsync = async (initialDefIndex, promise, req, res) => {
await promise;
if (res.submit === true || req.upgraded === true) {
return;
}
for (let i = initialDefIndex + 1;i < middlewares.length; i++) {
const middleware = middlewares[i];
const p = middleware(req, res);
if (p && p.then != null) {
await p;
}
if (res.submit === true || req.upgraded === true) {
return;
}
}
};
const baseMerged = (req, res) => {
for (let i = 0;i < middlewares.length; i++) {
const middleware = middlewares[i];
const p = middleware(req, res);
if (p && p.then != null) {
return mergedAsync(i, p, req, res);
}
if (res.submit === true || req.upgraded === true) {
return;
}
}
};
const merged = baseMerged;
merged.base = middlewares;
return merged;
}
function isMergedRequestMiddleware(middleware) {
return Array.isArray(middleware.base);
}
function isMergeableEndpointRoute(route, route2) {
if (route.method !== route2.method) {
return false;
}
if (route.splitPath == undefined && route2.splitPath == undefined) {
return true;
} else if (route.splitPath != null && route2.splitPath != null && route.splitPath.join("/") == route2.splitPath.join("/")) {
return true;
}
return false;
}
// src/responseBuilder.ts
var notFoundResponse = new Response("Not Found", {
status: 404,
statusText: "Not Found"
});
class ResponseBuilder {
submit = false;
statusCode = 200;
statusText;
bodyInit = null;
headers = [];
beforeSentHooks;
beforeSent(hook) {
if (!this.beforeSentHooks) {
this.beforeSentHooks = [];
}
this.beforeSentHooks.push(hook);
return this;
}
async startBeforeSentHookAsync(p) {
await p;
let hook = this.beforeSentHooks?.shift();
while (hook != null) {
const p2 = hook(this);
if (p2 && p2.then != null) {
await p2;
}
}
}
startBeforeSentHook() {
if (this.beforeSentHooks) {
let hook = this.beforeSentHooks.pop();
while (hook != null) {
const p = hook(this);
if (p && p.then != null) {
return this.startBeforeSentHookAsync(p);
}
hook = this.beforeSentHooks.pop();
}
}
}
build() {
return new Response(this.bodyInit, {
status: this.statusCode,
statusText: this.statusText,
headers: this.headers
});
}
reset() {
this.submit = false;
this.statusCode = 200;
this.statusText = undefined;
this.bodyInit = null;
this.headers = [];
return this;
}
status(statusCode, statusText) {
this.statusCode = statusCode;
if (statusText) {
this.statusText = statusText;
}
return this;
}
unsetHeader(name) {
this.headers = this.headers.filter((header) => header[0].toLowerCase() !== name.toLowerCase());
return this;
}
setHeader(name, value, overwrite = true) {
if (overwrite) {
this.unsetHeader(name);
}
this.headers.push([name, value]);
return this;
}
setCookie(name, value, options = {}) {
const cookieParts = [`${name}=${encodeURIComponent(value)}`];
if (options.MaxAge) {
cookieParts.push(`Max-Age=${options.MaxAge}`);
}
if (options.Path) {
cookieParts.push(`Path=${options.Path}`);
}
if (options.HttpOnly) {
cookieParts.push(`HttpOnly`);
}
if (options.Secure) {
cookieParts.push(`Secure`);
}
if (options.SameSite) {
cookieParts.push(`SameSite=${options.SameSite}`);
}
this.setHeader("Set-Cookie", cookieParts.join("; "), false);
return this;
}
unsetCookie(name) {
this.setHeader("Set-Cookie", name + "=; Expires=Thu, 01 Jan 1970 00:00:00 GMT", false);
return this;
}
body(bodyInit = null) {
this.bodyInit = bodyInit;
return this;
}
send(bodyInit = null) {
this.bodyInit = bodyInit;
this.submit = true;
}
sendRedirect(url, perma = false) {
this.reset();
this.statusCode = perma ? 308 : 307;
this.headers.push(["location", url]);
this.submit = true;
}
sendRedirectCustom(url, status) {
this.reset();
this.statusCode = status;
this.headers.push(["location", url]);
this.submit = true;
}
sendBasicAuth(bodyInit = null, realm = "User Visible Realm", charset = "UTF-8") {
this.reset();
this.statusCode = 401;
this.setHeader("WWW-Authenticate", 'Basic realm="' + realm + '", charset="' + charset + '"');
this.bodyInit = bodyInit;
this.submit = true;
}
}
// src/router.ts
var {statSync} = (() => ({}));
// node:path
function assertPath(path) {
if (typeof path !== "string")
throw new TypeError("Path must be a string. Received " + JSON.stringify(path));
}
function normalizeStringPosix(path, allowAboveRoot) {
var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
for (var i = 0;i <= path.length; ++i) {
if (i < path.length)
code = path.charCodeAt(i);
else if (code === 47)
break;
else
code = 47;
if (code === 47) {
if (lastSlash === i - 1 || dots === 1)
;
else if (lastSlash !== i - 1 && dots === 2) {
if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
if (res.length > 2) {
var lastSlashIndex = res.lastIndexOf("/");
if (lastSlashIndex !== res.length - 1) {
if (lastSlashIndex === -1)
res = "", lastSegmentLength = 0;
else
res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
lastSlash = i, dots = 0;
continue;
}
} else if (res.length === 2 || res.length === 1) {
res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
continue;
}
}
if (allowAboveRoot) {
if (res.length > 0)
res += "/..";
else
res = "..";
lastSegmentLength = 2;
}
} else {
if (res.length > 0)
res += "/" + path.slice(lastSlash + 1, i);
else
res = path.slice(lastSlash + 1, i);
lastSegmentLength = i - lastSlash - 1;
}
lastSlash = i, dots = 0;
} else if (code === 46 && dots !== -1)
++dots;
else
dots = -1;
}
return res;
}
function _format(sep, pathObject) {
var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
if (!dir)
return base;
if (dir === pathObject.root)
return dir + base;
return dir + sep + base;
}
function resolve() {
var resolvedPath = "", resolvedAbsolute = false, cwd;
for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
var path;
if (i >= 0)
path = arguments[i];
else {
if (cwd === undefined)
cwd = process.cwd();
path = cwd;
}
if (assertPath(path), path.length === 0)
continue;
resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
}
if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
if (resolvedPath.length > 0)
return "/" + resolvedPath;
else
return "/";
else if (resolvedPath.length > 0)
return resolvedPath;
else
return ".";
}
function normalize(path) {
if (assertPath(path), path.length === 0)
return ".";
var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
path = ".";
if (path.length > 0 && trailingSeparator)
path += "/";
if (isAbsolute)
return "/" + path;
return path;
}
function isAbsolute(path) {
return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
}
function join() {
if (arguments.length === 0)
return ".";
var joined;
for (var i = 0;i < arguments.length; ++i) {
var arg = arguments[i];
if (assertPath(arg), arg.length > 0)
if (joined === undefined)
joined = arg;
else
joined += "/" + arg;
}
if (joined === undefined)
return ".";
return normalize(joined);
}
function relative(from, to) {
if (assertPath(from), assertPath(to), from === to)
return "";
if (from = resolve(from), to = resolve(to), from === to)
return "";
var fromStart = 1;
for (;fromStart < from.length; ++fromStart)
if (from.charCodeAt(fromStart) !== 47)
break;
var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
for (;toStart < to.length; ++toStart)
if (to.charCodeAt(toStart) !== 47)
break;
var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
for (;i <= length; ++i) {
if (i === length) {
if (toLen > length) {
if (to.charCodeAt(toStart + i) === 47)
return to.slice(toStart + i + 1);
else if (i === 0)
return to.slice(toStart + i);
} else if (fromLen > length) {
if (from.charCodeAt(fromStart + i) === 47)
lastCommonSep = i;
else if (i === 0)
lastCommonSep = 0;
}
break;
}
var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
if (fromCode !== toCode)
break;
else if (fromCode === 47)
lastCommonSep = i;
}
var out = "";
for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
if (i === fromEnd || from.charCodeAt(i) === 47)
if (out.length === 0)
out += "..";
else
out += "/..";
if (out.length > 0)
return out + to.slice(toStart + lastCommonSep);
else {
if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
++toStart;
return to.slice(toStart);
}
}
function _makeLong(path) {
return path;
}
function dirname(path) {
if (assertPath(path), path.length === 0)
return ".";
var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
for (var i = path.length - 1;i >= 1; --i)
if (code = path.charCodeAt(i), code === 47) {
if (!matchedSlash) {
end = i;
break;
}
} else
matchedSlash = false;
if (end === -1)
return hasRoot ? "/" : ".";
if (hasRoot && end === 1)
return "//";
return path.slice(0, end);
}
function basename(path, ext) {
if (ext !== undefined && typeof ext !== "string")
throw new TypeError('"ext" argument must be a string');
assertPath(path);
var start = 0, end = -1, matchedSlash = true, i;
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
if (ext.length === path.length && ext === path)
return "";
var extIdx = ext.length - 1, firstNonSlashEnd = -1;
for (i = path.length - 1;i >= 0; --i) {
var code = path.charCodeAt(i);
if (code === 47) {
if (!matchedSlash) {
start = i + 1;
break;
}
} else {
if (firstNonSlashEnd === -1)
matchedSlash = false, firstNonSlashEnd = i + 1;
if (extIdx >= 0)
if (code === ext.charCodeAt(extIdx)) {
if (--extIdx === -1)
end = i;
} else
extIdx = -1, end = firstNonSlashEnd;
}
}
if (start === end)
end = firstNonSlashEnd;
else if (end === -1)
end = path.length;
return path.slice(start, end);
} else {
for (i = path.length - 1;i >= 0; --i)
if (path.charCodeAt(i) === 47) {
if (!matchedSlash) {
start = i + 1;
break;
}
} else if (end === -1)
matchedSlash = false, end = i + 1;
if (end === -1)
return "";
return path.slice(start, end);
}
}
function extname(path) {
assertPath(path);
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
for (var i = path.length - 1;i >= 0; --i) {
var code = path.charCodeAt(i);
if (code === 47) {
if (!matchedSlash) {
startPart = i + 1;
break;
}
continue;
}
if (end === -1)
matchedSlash = false, end = i + 1;
if (code === 46) {
if (startDot === -1)
startDot = i;
else if (preDotState !== 1)
preDotState = 1;
} else if (startDot !== -1)
preDotState = -1;
}
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
return "";
return path.slice(startDot, end);
}
function format(pathObject) {
if (pathObject === null || typeof pathObject !== "object")
throw new TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
return _format("/", pathObject);
}
function parse(path) {
assertPath(path);
var ret = { root: "", dir: "", base: "", ext: "", name: "" };
if (path.length === 0)
return ret;
var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
if (isAbsolute2)
ret.root = "/", start = 1;
else
start = 0;
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
for (;i >= start; --i) {
if (code = path.charCodeAt(i), code === 47) {
if (!matchedSlash) {
startPart = i + 1;
break;
}
continue;
}
if (end === -1)
matchedSlash = false, end = i + 1;
if (code === 46) {
if (startDot === -1)
startDot = i;
else if (preDotState !== 1)
preDotState = 1;
} else if (startDot !== -1)
preDotState = -1;
}
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
if (end !== -1)
if (startPart === 0 && isAbsolute2)
ret.base = ret.name = path.slice(1, end);
else
ret.base = ret.name = path.slice(startPart, end);
} else {
if (startPart === 0 && isAbsolute2)
ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
else
ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
ret.ext = path.slice(startDot, end);
}
if (startPart > 0)
ret.dir = path.slice(0, startPart - 1);
else if (isAbsolute2)
ret.dir = "/";
return ret;
}
var sep = "/";
var delimiter = ":";
var posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
// src/router.ts
class Router {
routes = [];
mergeHandlers = true;
static parseCookies(req, forceReload = false) {
if (!req.originCookies) {
req.cookies = {};
const cookieHeader = req.headers.get("cookie");
if (!cookieHeader) {
return;
}
const pairs = cookieHeader.split(/; */);
for (const pair of pairs) {
const splitted = pair.split("=");
const name = trimSpaces(splitted[0]);
if (name.length != 0) {
req.cookies[name] = decodeURIComponent(splitted.slice(1).join("="));
}
}
req.originCookies = {
...req.cookies
};
} else if (forceReload) {
req.cookies = {
...req.originCookies
};
}
}
static storeCookies(req, res) {
if (!req.cookies) {
res.reset().status(500).send("Request cookies store error");
return;
}
const newCookies = req.cookies;
const oldCookies = req.originCookies ?? {};
const newCookieKeys = Object.keys(newCookies);
for (const cookieKey of newCookieKeys) {
if (newCookies[cookieKey] && (!oldCookies[cookieKey] || oldCookies[cookieKey] !== newCookies[cookieKey])) {
res.setCookie(cookieKey, newCookies[cookieKey]);
}
}
for (const cookieKey of Object.keys(oldCookies)) {
if (!newCookieKeys.includes(cookieKey)) {
res.unsetCookie(cookieKey);
}
}
req.cookies = newCookies;
}
static getDefinitionString(route, handler, mergedToTop) {
let parts = ["/", "X", "/"];
if (mergedToTop) {
parts[0] = "^ (M)";
} else {
parts[0] = stringifyHttpMethods(route.method);
}
if (route.splitPath) {
parts[1] = "/" + route.splitPath.join("/");
} else {
parts[1] = "/";
}
if (isMergedRequestMiddleware(handler)) {
parts[2] = "[merged]";
} else if (handler && typeof handler.name == "string" && handler.name.length != 0) {
parts[2] = handler.name;
} else if (handler && handler.prototype && typeof handler.prototype.name == "string" && handler.prototype.name.length != 0) {
parts[2] = handler.prototype.name;
} else {
parts[2] = "[anonym]";
}
return parts;
}
dump(...servers) {
if (this.routes.length == 0) {
throw new Error("No endpoint routes defined");
}
let unmergedParts = [];
let mergedParts = [];
for (const route of this.routes) {
mergedParts.push(Router.getDefinitionString(route, route.handler, false));
unmergedParts.push(...unmergeRequestMiddleware(route.handler).map((middleware, index) => Router.getDefinitionString(route, middleware, index != 0)));
}
const both = [
...unmergedParts,
...mergedParts
];
const part1MinLen = both.sort((a, b) => b[0].length - a[0].length)[0][0].length;
const part2MinLen = both.sort((a, b) => b[1].length - a[1].length)[0][1].length;
const part3MinLen = both.sort((a, b) => b[2].length - a[2].length)[0][2].length;
const lines = [];
if (servers && servers.length != 0) {
if (servers.length == 1) {
lines.push("Server is listening on " + servers[0].url);
} else {
lines.push("Server is listening on:");
lines.push(...servers.map((server) => "- " + server.url));
}
}
lines.push("", "# Defined endpoints:", ...unmergedParts.map(([part1, part2, part3]) => "| " + part1.padEnd(part1MinLen) + " | " + part2.padEnd(part2MinLen) + " | " + part3.padEnd(part3MinLen) + " |"), "");
if (unmergedParts.length != mergedParts.length) {
lines.push("# Merged endpoints:", ...mergedParts.map(([part1, part2, part3]) => "| " + part1.padEnd(part1MinLen) + " | " + part2.padEnd(part2MinLen) + " | " + part3.padEnd(part3MinLen) + " |"), "");
}
return lines.join(`
`);
}
handle = (request, server) => this.innerHandle(request, server);
innerHandle(request, server) {
const res = new ResponseBuilder;
const req = request;
req.httpMethod = parseHttpMethods(req.method);
req.server = server;
req.cookies = {};
req.path = new URL(req.url).pathname;
req.splitPath = splitPath(req.path);
const sock = req.server.requestIP(req);
if (!sock) {
return new Response("Request closed to early", { status: 500 });
}
req.sock = sock;
const p = this.route(req, res);
if (p && p.then != null) {
return p.then(() => {
if (req.upgraded) {
return;
}
const p3 = res.startBeforeSentHook();
if (p3 && p3.then != null) {
return p3.then(() => {
return res.build();
});
}
return res.build();
});
}
if (req.upgraded) {
return;
}
const p2 = res.startBeforeSentHook();
if (p2 && p2.then != null) {
return p2.then(() => {
return res.build();
});
}
return res.build();
}
route(req, res) {
for (let i = 0;i < this.routes.length; i++) {
if (this.routes[i].method != 1 /* ALL */ && this.routes[i].method != req.httpMethod) {
continue;
}
const pathParams = requestPathMatchesRouteDefinition(req.splitPath, this.routes[i].splitPath);
if (pathParams === false) {
continue;
} else if (pathParams !== true) {
req.pathParams = pathParams;
}
const p = this.routes[i].handler(req, res);
if (p != null && p.then != null) {
return this.routeAsync(i, p, req, res);
}
if (res.submit === true || req.upgraded === true) {
return;
}
}
if (req.upgraded) {
return;
}
res.reset().status(404).body("Not found");
}
async routeAsync(initialDefIndex, promise, req, res) {
await promise;
if (res.submit === true || req.upgraded === true) {
return;
}
for (let i = initialDefIndex + 1;i < this.routes.length; i++) {
if (this.routes[i].method != null && this.routes[i].method != req.httpMethod) {
continue;
}
const pathParams = requestPathMatchesRouteDefinition(req.splitPath, this.routes[i].splitPath);
if (pathParams === false) {
continue;
} else if (pathParams !== true) {
req.pathParams = pathParams;
}
const p = this.routes[i].handler(req, res);
if (p && p.then != null) {
await p;
}
if (res.submit === true || req.upgraded === true) {
return;
}
}
if (req.upgraded) {
return;
}
res.reset().status(404).body("Not found");
}
use(method, path, handler, ...handlers) {
if (typeof handler != "function") {
throw new Error("no handler provided, type: " + typeof handler);
}
handlers = [
handler,
...handlers
];
const route = {
splitPath: splitRoutePath(path),
method: parseHttpMethods(method),
handler
};
if (this.mergeHandlers) {
const lastDef = this.routes.pop();
if (lastDef) {
if (isMergeableEndpointRoute(lastDef, route)) {
handlers.unshift(lastDef.handler);
} else {
this.routes.push(lastDef);
}
}
}
route.handler = mergeRequestMiddlewares(...unmergeRequestMiddleware(...handlers));
this.routes.push(route);
return this;
}
get(path, handler, ...handlers) {
return this.use("GET", path, handler, ...handlers);
}
post(path, handler, ...handlers) {
return this.use("POST", path, handler, ...handlers);
}
put(path, handler, ...handlers) {
return this.use("PUT", path, handler, ...handlers);
}
delete(path, handler, ...handlers) {
return this.use("DELETE", path, handler, ...handlers);
}
patch(path, handler, ...handlers) {
return this.use("PATCH", path, handler, ...handlers);
}
trace(path, handler, ...handlers) {
return this.use("TRACE", path, handler, ...handlers);
}
head(path, handler, ...handlers) {
return this.use("HEAD", path, handler, ...handlers);
}
connect(path, handler, ...handlers) {
return this.use("CONNECT", path, handler, ...handlers);
}
options(path, handler, ...handlers) {
return this.use("OPTIONS", path, handler, ...handlers);
}
ws(path) {
const wsMiddleware = (req, res) => {
if (req.server.upgrade(req)) {
req.upgraded = true;
}
};
this.use("GET", path, wsMiddleware);
return this;
}
redirect(method, path, redirectTarget, perma = false) {
const redirectMiddleware = (_, res) => res.sendRedirect(redirectTarget, perma);
this.use(method, path, redirectMiddleware);
return this;
}
static(path, targetDir, indexFile = "index.html", deepestLevel = 10) {
if (!statSync(targetDir).isDirectory()) {
throw new Error("static target is not a directory: " + targetDir);
}
const staticMiddleware = (req, res) => {
if (req.path.endsWith("/" + indexFile)) {
res.sendRedirect(req.path.slice(0, -indexFile.length), true);
return;
}
let targetPath = join(targetDir, req.splitPath == undefined ? "/" : req.path);
if (targetPath.endsWith("/")) {
targetPath += indexFile;
}
if (req.splitPath != null && req.splitPath?.length > deepestLevel) {
return;
}
try {
const file = Bun.file(targetPath);
return file.exists().then(async (exist) => {
if (exist) {
res.send(await file.arrayBuffer());
} else {
res.status(404);
}
}).catch(() => {
res.status(500, "Error while loading response content");
});
} catch (_) {
res.status(500, "Error while init response content");
}
};
this.use("GET", path, staticMiddleware);
return this;
}
basicAuth(method, path, validator, realm = "User Visible Realm", charset = "UTF-8") {
const basicAuthMiddleware = (req, res) => {
const auth = req.headers.get("authorization");
if (!auth) {
res.sendBasicAuth("Missing authorization header", realm, charset);
return;
}
let splitIndex = auth.indexOf(" ");
if (splitIndex === -1) {
res.sendBasicAuth("Unprocessable authorization header", realm, charset);
return;
}
const schema = auth.slice(0, splitIndex);
if (schema !== "Basic") {
res.sendBasicAuth("Unprocessable basic auth schema", realm, charset);
return;
}
const credentials = atob(auth.slice(splitIndex + 1));
splitIndex = credentials.indexOf(":");
if (splitIndex === -1) {
res.sendBasicAuth("Unprocessable basic auth credentials", realm, charset);
return;
}
if (!validator(credentials.slice(0, splitIndex), credentials.slice(splitIndex + 1))) {
res.sendBasicAuth("Invalid credentials", realm, charset);
return;
}
};
this.use(method, path, basicAuthMiddleware);
return this;
}
cookies(method, path, autoResponseHeaders = false) {
const cookiesMiddleware = autoResponseHeaders ? (req, res) => {
res.beforeSent((res2) => Router.storeCookies(req, res2));
Router.parseCookies(req);
} : (req) => Router.parseCookies(req);
this.use(method, path, cookiesMiddleware);
return this;
}
}
function trimSpaces(value) {
while (value.startsWith(" ") || value.startsWith("\t") || value.startsWith(`
`)) {
value = value.slice(1);
}
if (value.length == 0) {
return "";
}
while (value.endsWith(" ") || value.endsWith("\t") || value.endsWith(`
`)) {
value = value.slice(0, -1);
}
return value;
}
function splitPath(path) {
if (path == undefined) {
return;
}
while (path.startsWith("/") || path.startsWith(" ")) {
path = path.slice(1);
}
if (path.length == 0) {
return;
}
while (path.endsWith("/") || path.endsWith(" ")) {
path = path.slice(0, -1);
}
const splitPath2 = path.split("/").map((part) => {
while (part.startsWith("/") || part.startsWith(" ")) {
part = part.slice(1);
}
if (part.length == 0) {
return "";
}
while (part.endsWith("/") || part.endsWith(" ")) {
part = part.slice(0, -1);
}
return part;
}).filter((v) => v.length != 0);
if (splitPath2.length == 0) {
return;
}
return splitPath2;
}
function splitRoutePath(path) {
const splittedPath = splitPath(path);
if (splittedPath && splittedPath.length > 1 && splittedPath.slice(0, -1).includes("**")) {
throw new Error("Invalid router path, ** must be the last part");
}
return splittedPath;
}
function requestPathMatchesRouteDefinition(requestPath, routeSelector) {
if (requestPath == undefined && routeSelector == undefined) {
return [];
} else if (routeSelector == undefined) {
return false;
} else if (requestPath == undefined) {
if (routeSelector[0] == "**") {
return true;
}
return false;
} else if (requestPath.length == 0) {
throw new Error("Invalid requestPath SplitPath length, got 0, expected at least 1");
} else if (routeSelector.length == 0) {
throw new Error("Invalid routeSelector SplitPath length, got 0, expected at least 1");
} else if (routeSelector[0] == "**") {
return requestPath;
} else if (routeSelector.length < requestPath.length) {
if (routeSelector[routeSelector.length - 1] != "**") {
return false;
}
}
let pathParams = true;
for (let i = 0;i < routeSelector.length; i++) {
switch (routeSelector[i]) {
case "*":
if (requestPath.length <= i) {
return false;
}
if (pathParams === true) {
pathParams = [];
}
pathParams.push(requestPath[i]);
break;
case "**":
if (requestPath.length - i > 0) {
if (pathParams === true) {
pathParams = [];
}
pathParams.push(...requestPath.slice(i));
}
return pathParams;
case requestPath[i]:
break;
default:
return false;
}
}
return pathParams;
}
export {
unmergeRequestMiddleware,
trimSpaces,
stringifyHttpMethods,
splitRoutePath,
splitPath,
requestPathMatchesRouteDefinition,
parseHttpMethods,
notFoundResponse,
mergeRequestMiddlewares,
isMergedRequestMiddleware,
isMergeableEndpointRoute,
Router,
ResponseBuilder,
HttpMethod
};