@rabbit-company/web
Version:
High-performance web framework
1,088 lines (1,084 loc) • 34.1 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __moduleCache = /* @__PURE__ */ new WeakMap;
var __toCommonJS = (from) => {
var entry = __moduleCache.get(from), desc;
if (entry)
return entry;
entry = __defProp({}, "__esModule", { value: true });
if (from && typeof from === "object" || typeof from === "function")
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
get: () => from[key],
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
}));
__moduleCache.set(from, entry);
return entry;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
});
};
// packages/core/src/index.ts
var exports_src = {};
__export(exports_src, {
Web: () => Web
});
module.exports = __toCommonJS(exports_src);
var Runtime = {
isBun: typeof globalThis !== "undefined" && typeof globalThis.Bun !== "undefined",
isDeno: typeof globalThis !== "undefined" && typeof globalThis.Deno !== "undefined",
isNode: typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" && typeof globalThis.Bun === "undefined" && typeof globalThis.Deno === "undefined",
isCloudflareWorkers: typeof globalThis.navigator !== "undefined" && globalThis.navigator.userAgent === "Cloudflare-Workers"
};
class TrieNode {
segment;
children = new Map;
paramChild;
wildcardChild;
handlers;
method;
routeId;
constructor(segment) {
this.segment = segment;
}
}
var EMPTY_PARAMS = Object.freeze({});
var EMPTY_SEARCH_PARAMS = new URLSearchParams;
class Web {
routes = [];
middlewares = [];
methodMiddlewareCache = new Map;
urlCache = new Map;
segmentCache = new Map;
matcherCache = new Map;
routeMatchCache = new Map;
idCounter = 0;
roots = {
GET: new TrieNode,
POST: new TrieNode,
PUT: new TrieNode,
DELETE: new TrieNode,
PATCH: new TrieNode,
OPTIONS: new TrieNode,
HEAD: new TrieNode
};
constructor() {
this.handle = this.handle.bind(this);
this.handleBun = this.handleBun.bind(this);
this.handleCloudflare = this.handleCloudflare.bind(this);
}
generateId() {
return `${Date.now()}-${++this.idCounter}`;
}
clearCaches() {
this.methodMiddlewareCache.clear();
this.urlCache.clear();
this.segmentCache.clear();
this.routeMatchCache.clear();
}
rebuildTrie() {
this.roots = {
GET: new TrieNode,
POST: new TrieNode,
PUT: new TrieNode,
DELETE: new TrieNode,
PATCH: new TrieNode,
OPTIONS: new TrieNode,
HEAD: new TrieNode
};
for (const route of this.routes) {
this.addRouteToTrie(route.method, route.path, route.handlers, route.id);
}
}
addRouteToTrie(method, path, handlers, routeId) {
const segments = this.getPathSegments(path);
let node = this.roots[method];
for (const segment of segments) {
if (segment === "*") {
if (!node.wildcardChild) {
node.wildcardChild = new TrieNode("*");
}
node = node.wildcardChild;
break;
} else if (segment.startsWith(":")) {
const paramName = segment.slice(1);
if (!node.paramChild) {
node.paramChild = {
node: new TrieNode(segment),
name: paramName
};
}
node = node.paramChild.node;
} else {
if (!node.children.has(segment)) {
node.children.set(segment, new TrieNode(segment));
}
node = node.children.get(segment);
}
}
node.handlers = handlers;
node.method = method;
node.routeId = routeId;
}
errorHandler;
notFoundHandler;
onError(handler) {
this.errorHandler = handler;
return this;
}
onNotFound(handler) {
this.notFoundHandler = handler;
return this;
}
getPathSegments(path) {
let segments = this.segmentCache.get(path);
if (segments)
return segments;
segments = path.split("/").filter(Boolean);
if (this.segmentCache.size < 1000) {
this.segmentCache.set(path, segments);
}
return segments;
}
parseUrl(url) {
const cached = this.urlCache.get(url);
if (cached)
return cached;
if (url[0] === "/" && !url.includes("?") && !url.includes("#")) {
const result2 = { pathname: url, searchParams: undefined };
if (this.urlCache.size < 2000) {
this.urlCache.set(url, result2);
}
return result2;
}
const queryStart = url.indexOf("?");
const hashStart = url.indexOf("#");
let pathnameEnd = url.length;
if (queryStart !== -1)
pathnameEnd = Math.min(pathnameEnd, queryStart);
if (hashStart !== -1)
pathnameEnd = Math.min(pathnameEnd, hashStart);
let pathname;
const protocolEnd = url.indexOf("://");
if (protocolEnd !== -1) {
const hostStart = protocolEnd + 3;
const pathStart = url.indexOf("/", hostStart);
pathname = pathStart !== -1 ? url.slice(pathStart, pathnameEnd) : "/";
} else {
pathname = url.slice(0, pathnameEnd) || "/";
}
let searchParams;
if (queryStart !== -1) {
const queryEnd = hashStart !== -1 ? hashStart : url.length;
const queryString = url.slice(queryStart + 1, queryEnd);
if (queryString.length > 0) {
searchParams = new URLSearchParams(queryString);
}
}
const result = { pathname, searchParams };
if (this.urlCache.size >= 2000) {
const firstKey = this.urlCache.keys().next().value;
if (firstKey !== undefined)
this.urlCache.delete(firstKey);
}
this.urlCache.set(url, result);
return result;
}
use(...args) {
this.addMiddleware(...args);
return this;
}
removeMiddleware(id) {
const initialLength = this.middlewares.length;
this.middlewares = this.middlewares.filter((mw) => mw.id !== id);
if (this.middlewares.length !== initialLength) {
this.clearCaches();
return true;
}
return false;
}
removeMiddlewareBy(criteria) {
const initialLength = this.middlewares.length;
if (!criteria.method && !criteria.path)
return 0;
this.middlewares = this.middlewares.filter((mw) => {
if (criteria.method && mw.method !== criteria.method)
return true;
if (criteria.path && mw.path !== criteria.path)
return true;
return false;
});
const removedCount = initialLength - this.middlewares.length;
if (removedCount > 0) {
this.clearCaches();
}
return removedCount;
}
addMiddleware(...args) {
this.clearCaches();
const id = this.generateId();
if (args.length === 1) {
const [handler] = args;
this.middlewares.push({
id,
match: () => ({ matched: true, params: {} }),
handler
});
} else if (args.length === 2) {
const [path, handler] = args;
const segments = this.getPathSegments(path);
const match = this.getCachedMatcher(path, segments);
this.middlewares.push({
id,
path,
pathPrefix: getStaticPrefix(path),
match: (url) => match(this.getPathSegments(url)),
handler
});
} else {
const [method, path, handler] = args;
const segments = this.getPathSegments(path);
const match = this.getCachedMatcher(path, segments);
this.middlewares.push({
id,
method,
path,
pathPrefix: getStaticPrefix(path),
match: (url) => match(this.getPathSegments(url)),
handler
});
}
return id;
}
getMiddlewares() {
return this.middlewares.map((mw) => ({
id: mw.id,
method: mw.method,
path: mw.path
}));
}
getCachedMatcher(path, segments) {
let matcher = this.matcherCache.get(path);
if (!matcher) {
matcher = createPathMatcherSegments(segments);
this.matcherCache.set(path, matcher);
}
return matcher;
}
addRoute(method, path, ...handlers) {
this.clearCaches();
const id = this.generateId();
this.addRouteToTrie(method, path, handlers, id);
const hasOptionsRoute = this.routes.some((route) => route.method === "OPTIONS" && route.path === path);
if (method !== "OPTIONS" && !hasOptionsRoute) {
const optionsId = this.generateId();
this.addRouteToTrie("OPTIONS", path, [
async (ctx) => {
const response = new Response(null, { status: 204 });
return response;
}
], optionsId);
}
const matcher = this.getCachedMatcher(path, this.getPathSegments(path));
this.routes.push({
id,
method,
path,
handlers,
match: (url) => matcher(this.getPathSegments(url))
});
return id;
}
removeRoute(id) {
const initialLength = this.routes.length;
this.routes = this.routes.filter((route) => route.id !== id);
if (this.routes.length !== initialLength) {
this.clearCaches();
this.rebuildTrie();
return true;
}
return false;
}
removeRoutesBy(criteria) {
const initialLength = this.routes.length;
if (!criteria.method && !criteria.path)
return 0;
this.routes = this.routes.filter((route) => {
if (criteria.method && route.method !== criteria.method)
return true;
if (criteria.path && route.path !== criteria.path)
return true;
return false;
});
const removedCount = initialLength - this.routes.length;
if (removedCount > 0) {
this.clearCaches();
this.rebuildTrie();
}
return removedCount;
}
getRoutes() {
return this.routes.map((route) => ({
id: route.id,
method: route.method,
path: route.path
}));
}
clear() {
this.routes = [];
this.middlewares = [];
this.clearCaches();
this.rebuildTrie();
}
match(method, path) {
const cacheKey = `${method}:${path}`;
const cached = this.routeMatchCache.get(cacheKey);
if (cached !== undefined)
return cached;
const root = this.roots[method];
if (!root) {
this.routeMatchCache.set(cacheKey, null);
return null;
}
const segments = this.getPathSegments(path);
if (segments.length === 0) {
const result = root.handlers ? { handlers: root.handlers, params: EMPTY_PARAMS } : null;
if (this.routeMatchCache.size < 500) {
this.routeMatchCache.set(cacheKey, result);
}
return result;
}
const params = {};
let node = root;
let i = 0;
while (node && i < segments.length) {
const segment = segments[i];
const staticChild = node.children.get(segment);
if (staticChild) {
node = staticChild;
i++;
continue;
}
if (node.paramChild) {
params[node.paramChild.name] = decodeURIComponent(segment);
node = node.paramChild.node;
i++;
continue;
}
if (node.wildcardChild) {
params["*"] = segments.slice(i).join("/");
node = node.wildcardChild;
break;
}
if (this.routeMatchCache.size < 500) {
this.routeMatchCache.set(cacheKey, null);
}
return null;
}
if (i === segments.length || node.segment === "*") {
if (node.handlers) {
const result = Object.keys(params).length === 0 ? { handlers: node.handlers, params: EMPTY_PARAMS } : { handlers: node.handlers, params };
if (this.routeMatchCache.size < 500) {
this.routeMatchCache.set(cacheKey, result);
}
return result;
}
}
if (this.routeMatchCache.size < 500) {
this.routeMatchCache.set(cacheKey, null);
}
return null;
}
getMethodMiddlewares(method) {
if (this.methodMiddlewareCache.has(method)) {
return this.methodMiddlewareCache.get(method);
}
const result = this.middlewares.filter((mw) => {
if (mw.method && mw.method !== method)
return false;
return true;
});
this.methodMiddlewareCache.set(method, result);
return result;
}
scope(path, callback) {
const scopedApp = new this.constructor;
callback(scopedApp);
this.route(path, scopedApp);
return this;
}
route(prefix, subApp) {
const baseSegments = this.getPathSegments(prefix);
for (const mw of subApp.middlewares) {
const originalMatch = mw.match;
const prefixedMatch = (url) => {
const urlSegments = this.getPathSegments(url);
if (urlSegments.length < baseSegments.length) {
return { matched: false, params: {} };
}
for (let i = 0;i < baseSegments.length; i++) {
if (baseSegments[i] !== urlSegments[i]) {
return { matched: false, params: {} };
}
}
const subSegments = urlSegments.slice(baseSegments.length);
const subPath = "/" + subSegments.join("/");
return originalMatch(subPath);
};
this.middlewares.push({
...mw,
id: this.generateId(),
match: prefixedMatch,
path: prefix + (mw.path ?? ""),
pathPrefix: getStaticPrefix(prefix + (mw.path ?? ""))
});
}
for (const route of subApp.routes) {
const newPath = joinPaths(prefix, route.path);
this.addRoute(route.method, newPath, ...route.handlers);
}
return this;
}
get(path, ...handlers) {
this.addRoute("GET", path, ...handlers);
return this;
}
post(path, ...handlers) {
this.addRoute("POST", path, ...handlers);
return this;
}
put(path, ...handlers) {
this.addRoute("PUT", path, ...handlers);
return this;
}
delete(path, ...handlers) {
this.addRoute("DELETE", path, ...handlers);
return this;
}
patch(path, ...handlers) {
this.addRoute("PATCH", path, ...handlers);
return this;
}
options(path, ...handlers) {
this.addRoute("OPTIONS", path, ...handlers);
return this;
}
head(path, ...handlers) {
this.addRoute("HEAD", path, ...handlers.map((handler) => async (ctx, next) => {
const res = await handler(ctx, next);
if (res instanceof Response) {
return new Response(null, {
status: res.status,
headers: res.headers
});
}
return res;
}));
return this;
}
createContext(req, params, parsedUrl, clientIp, env) {
const responseHeaders = new Headers;
const state = {};
const ctx = {
req,
params,
state,
env: {},
clientIp,
header: (name, value) => {
responseHeaders.set(name, value);
},
set: (key, value) => {
state[key] = value;
},
get: (key) => state[key],
redirect: (url, status = 302) => {
responseHeaders.set("Location", url);
return new Response(null, {
status,
headers: responseHeaders
});
},
body: async () => {
if (!req.body)
return {};
const type = req.headers.get("content-type") ?? "";
if (type.includes("application/x-www-form-urlencoded")) {
const formData = await req.formData();
return Object.fromEntries(formData.entries());
}
return type.includes("application/json") ? req.json() : {};
},
json: (data, status = 200, headers) => {
const allHeaders = headers ? new Headers(responseHeaders) : responseHeaders;
allHeaders.set("Content-Type", "application/json");
if (headers) {
Object.entries(headers).forEach(([name, value]) => {
allHeaders.set(name, value);
});
}
return new Response(JSON.stringify(data), {
status,
headers: allHeaders
});
},
text: (data, status = 200, headers) => {
const allHeaders = headers ? new Headers(responseHeaders) : responseHeaders;
allHeaders.set("Content-Type", "text/plain");
if (headers) {
Object.entries(headers).forEach(([name, value]) => {
allHeaders.set(name, value);
});
}
return new Response(data, {
status,
headers: allHeaders
});
},
html: (html, status = 200, headers) => {
const allHeaders = headers ? new Headers(responseHeaders) : responseHeaders;
allHeaders.set("Content-Type", "text/html; charset=utf-8");
if (headers) {
Object.entries(headers).forEach(([name, value]) => {
allHeaders.set(name, value);
});
}
return new Response(html, {
status,
headers: allHeaders
});
},
query: () => parsedUrl.searchParams || EMPTY_SEARCH_PARAMS
};
return ctx;
}
async createNotFoundResponse(req, parsedUrl, clientIp) {
if (this.notFoundHandler) {
const ctx = this.createContext(req, EMPTY_PARAMS, parsedUrl, clientIp);
return this.notFoundHandler(ctx);
}
return new Response("Not Found", { status: 404 });
}
async handle(req) {
const method = req.method;
const parsedUrl = this.parseUrl(req.url);
const path = parsedUrl.pathname;
try {
const matched = this.match(method, path);
if (!matched) {
return this.createNotFoundResponse(req, parsedUrl);
}
if (this.middlewares.length === 0 && matched.params === EMPTY_PARAMS && matched.handlers?.length === 1) {
const ctx2 = this.createContext(req, EMPTY_PARAMS, parsedUrl);
const result = await matched.handlers[0](ctx2, async () => {});
return result instanceof Response ? result : new Response("No response returned by handler", { status: 500 });
}
if (this.middlewares.length === 0) {
const ctx2 = this.createContext(req, matched.params, parsedUrl);
const handlers2 = matched.handlers;
if (handlers2) {
for (let i = 0;i < handlers2.length; i++) {
const result = await handlers2[i](ctx2, async () => {});
if (result instanceof Response) {
return result;
}
}
}
return new Response("No response returned by handler", { status: 500 });
}
const methodMiddlewares = this.getMethodMiddlewares(method);
let finalParams = matched.params;
const middlewares = new Array(methodMiddlewares.length);
let middlewareCount = 0;
if (methodMiddlewares.length > 0) {
for (let i = 0;i < methodMiddlewares.length; i++) {
const mw = methodMiddlewares[i];
if (mw.pathPrefix && !path.startsWith(mw.pathPrefix)) {
continue;
}
const matchResult = mw.match(path);
if (matchResult.matched) {
if (Object.keys(matchResult.params).length > 0) {
if (finalParams === EMPTY_PARAMS) {
finalParams = { ...matchResult.params };
} else {
finalParams = { ...finalParams, ...matchResult.params };
}
}
middlewares[middlewareCount++] = mw.handler;
}
}
}
const ctx = this.createContext(req, finalParams, parsedUrl);
const handlers = matched.handlers;
const allMiddleware = [];
for (let i = 0;i < middlewareCount; i++) {
allMiddleware.push(middlewares[i]);
}
if (handlers) {
allMiddleware.push(...handlers);
}
if (allMiddleware.length === 0) {
return this.createNotFoundResponse(req, parsedUrl);
}
let currentIndex = 0;
let response = undefined;
const dispatch = async () => {
if (currentIndex >= allMiddleware.length) {
return;
}
const middleware = allMiddleware[currentIndex++];
const result = await middleware(ctx, dispatch);
if (result instanceof Response) {
response = result;
}
return result;
};
await dispatch();
if (response instanceof Response) {
return response;
}
if (!handlers || handlers.length === 0) {
return this.createNotFoundResponse(req, parsedUrl);
}
return new Response("No response returned by handler", { status: 500 });
} catch (err) {
if (this.errorHandler) {
const errorCtx = {
req,
params: EMPTY_PARAMS,
state: {},
env: {},
text: (data, status = 500) => new Response(data, { status }),
json: (data, status = 500) => new Response(JSON.stringify(data), {
status,
headers: new Headers({ "Content-Type": "application/json" })
}),
html: (html, status = 500) => new Response(html, {
status,
headers: new Headers({ "Content-Type": "text/html; charset=utf-8" })
}),
query: () => parsedUrl.searchParams || EMPTY_SEARCH_PARAMS,
body: async () => ({}),
header: () => {},
set: () => {},
get: () => {
return;
},
redirect: () => new Response(null, { status: 302 })
};
return this.errorHandler(err, errorCtx);
}
return new Response("Internal Server Error", { status: 500 });
}
}
async handleWithIp(req, clientIp, env) {
const method = req.method;
const parsedUrl = this.parseUrl(req.url);
const path = parsedUrl.pathname;
try {
const matched = this.match(method, path);
if (!matched) {
return this.createNotFoundResponse(req, parsedUrl, clientIp);
}
if (this.middlewares.length === 0 && matched.params === EMPTY_PARAMS && matched.handlers?.length === 1) {
const ctx2 = this.createContext(req, EMPTY_PARAMS, parsedUrl, clientIp, env);
const result = await matched.handlers[0](ctx2, async () => {});
return result instanceof Response ? result : new Response("No response returned by handler", { status: 500 });
}
if (this.middlewares.length === 0) {
const ctx2 = this.createContext(req, matched.params, parsedUrl, clientIp, env);
const handlers2 = matched.handlers;
if (handlers2) {
for (let i = 0;i < handlers2.length; i++) {
const result = await handlers2[i](ctx2, async () => {});
if (result instanceof Response) {
return result;
}
}
}
return new Response("No response returned by handler", { status: 500 });
}
const methodMiddlewares = this.getMethodMiddlewares(method);
let finalParams = matched.params;
const middlewares = new Array(methodMiddlewares.length);
let middlewareCount = 0;
if (methodMiddlewares.length > 0) {
for (let i = 0;i < methodMiddlewares.length; i++) {
const mw = methodMiddlewares[i];
if (mw.pathPrefix && !path.startsWith(mw.pathPrefix)) {
continue;
}
const matchResult = mw.match(path);
if (matchResult.matched) {
if (Object.keys(matchResult.params).length > 0) {
if (finalParams === EMPTY_PARAMS) {
finalParams = { ...matchResult.params };
} else {
finalParams = { ...finalParams, ...matchResult.params };
}
}
middlewares[middlewareCount++] = mw.handler;
}
}
}
const ctx = this.createContext(req, finalParams, parsedUrl, clientIp, env);
const handlers = matched.handlers;
const allMiddleware = [];
for (let i = 0;i < middlewareCount; i++) {
allMiddleware.push(middlewares[i]);
}
if (handlers) {
allMiddleware.push(...handlers);
}
if (allMiddleware.length === 0) {
return this.createNotFoundResponse(req, parsedUrl, clientIp);
}
let currentIndex = 0;
let response = undefined;
const dispatch = async () => {
if (currentIndex >= allMiddleware.length) {
return;
}
const middleware = allMiddleware[currentIndex++];
const result = await middleware(ctx, dispatch);
if (result instanceof Response) {
response = result;
}
return result;
};
await dispatch();
if (response instanceof Response) {
return response;
}
if (!handlers || handlers.length === 0) {
return this.createNotFoundResponse(req, parsedUrl, clientIp);
}
return new Response("No response returned by handler", { status: 500 });
} catch (err) {
if (this.errorHandler) {
const errorCtx = {
req,
params: EMPTY_PARAMS,
state: {},
env: env || {},
clientIp,
text: (data, status = 500) => new Response(data, { status }),
json: (data, status = 500) => new Response(JSON.stringify(data), {
status,
headers: new Headers({ "Content-Type": "application/json" })
}),
html: (html, status = 500) => new Response(html, {
status,
headers: new Headers({ "Content-Type": "text/html; charset=utf-8" })
}),
query: () => parsedUrl.searchParams || EMPTY_SEARCH_PARAMS,
body: async () => ({}),
header: () => {},
set: () => {},
get: () => {
return;
},
redirect: () => new Response(null, { status: 302 })
};
return this.errorHandler(err, errorCtx);
}
return new Response("Internal Server Error", { status: 500 });
}
}
async handleBun(req, server) {
const clientIp = server?.requestIP?.(req)?.address;
return this.handleWithIp(req, clientIp);
}
async handleDeno(req, info) {
const clientIp = info?.remoteAddr?.hostname;
return this.handleWithIp(req, clientIp);
}
async handleCloudflare(req, env, ctx) {
const clientIp = req.headers.get("CF-Connecting-IP") || undefined;
return this.handleWithIp(req, clientIp, env);
}
async handleNode(nodeReq, nodeRes) {
const req = nodeReq;
const res = nodeRes;
try {
const clientIp = req.socket?.remoteAddress;
const host = req.headers.host || "localhost";
const protocol = req.socket?.encrypted ? "https" : "http";
const url = `${protocol}://${host}${req.url}`;
const headers = new Headers;
for (const [key, value] of Object.entries(req.headers)) {
if (value) {
if (Array.isArray(value)) {
value.forEach((v) => headers.append(key, v));
} else {
headers.set(key, value);
}
}
}
let body = null;
if (req.method !== "GET" && req.method !== "HEAD") {
const chunks = [];
await new Promise((resolve, reject) => {
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => resolve());
req.on("error", reject);
});
if (chunks.length > 0) {
body = Buffer.concat(chunks);
}
}
const webRequest = new Request(url, {
method: req.method,
headers,
body,
duplex: "half"
});
const response = await this.handleWithIp(webRequest, clientIp);
const responseHeaders = {};
response.headers.forEach((value, key) => {
responseHeaders[key] = value;
});
res.writeHead(response.status, responseHeaders);
if (response.body) {
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done)
break;
res.write(value);
}
} finally {
reader.releaseLock();
}
}
res.end();
} catch (error) {
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "text/plain" });
}
res.end("Internal Server Error");
}
}
async listen(options = {}) {
const { port = 3000, hostname = "localhost", onListen, node: nodeOptions = {}, deno: denoOptions = {}, bun: bunOptions = {} } = options;
if (Runtime.isBun) {
const bunGlobal = globalThis;
const serverConfig = {
port,
hostname,
fetch: this.handleBun.bind(this),
...bunOptions
};
const bunServer = bunGlobal.Bun.serve(serverConfig);
const server = {
port: bunServer.port,
hostname: bunServer.hostname || hostname,
runtime: "bun",
instance: bunServer,
stop: async () => {
bunServer.stop();
}
};
if (onListen) {
onListen({
port: server.port,
hostname: server.hostname,
runtime: server.runtime
});
}
return server;
} else if (Runtime.isDeno) {
const denoGlobal = globalThis;
const serverConfig = {
port,
hostname,
...denoOptions
};
const handler = (req, info) => {
return this.handleDeno(req, info);
};
const denoServer = denoGlobal.Deno.serve(serverConfig, handler);
const server = {
port,
hostname,
runtime: "deno",
instance: denoServer,
stop: async () => {
await denoServer.shutdown();
}
};
if (onListen) {
onListen({
port: server.port,
hostname: server.hostname,
runtime: server.runtime
});
}
return server;
} else if (Runtime.isCloudflareWorkers) {
const server = {
port,
hostname,
runtime: "cloudflare-workers",
instance: null,
stop: async () => {}
};
if (onListen) {
onListen({
port: server.port,
hostname: server.hostname,
runtime: server.runtime
});
}
return server;
} else if (Runtime.isNode) {
const module2 = nodeOptions.https ? "https" : "http";
const { createServer } = await import(module2);
let nodeServer;
const requestHandler = (req, res) => {
this.handleNode(req, res).catch((err) => {
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("Internal Server Error");
}
});
};
if (nodeOptions.https) {
const httpsOptions = {
key: nodeOptions.key,
cert: nodeOptions.cert
};
nodeServer = createServer(httpsOptions, requestHandler);
} else {
nodeServer = createServer(requestHandler);
}
await new Promise((resolve, reject) => {
const errorHandler = (err) => {
reject(err);
};
nodeServer.on("error", errorHandler);
nodeServer.listen(port, hostname, () => {
nodeServer.off("error", errorHandler);
resolve();
});
});
const server = {
port,
hostname,
runtime: "node",
instance: nodeServer,
stop: async () => {
return new Promise((resolve, reject) => {
nodeServer.close((err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
};
if (onListen) {
onListen({
port: server.port,
hostname: server.hostname,
runtime: server.runtime
});
}
return server;
} else {
throw new Error(`Unsupported runtime. This framework supports Bun, Deno, Node.js and Cloudflare Workers.`);
}
}
}
function getStaticPrefix(path) {
if (!path || path === "/")
return "/";
const segments = path.split("/").filter(Boolean);
const staticSegments = [];
for (const segment of segments) {
if (segment.startsWith(":") || segment === "*") {
break;
}
staticSegments.push(segment);
}
return staticSegments.length > 0 ? "/" + staticSegments.join("/") : "/";
}
function createPathMatcherSegments(segments) {
const segmentCount = segments.length;
const hasWildcard = segments[segmentCount - 1] === "*";
const paramPositions = [];
for (let i = 0;i < segmentCount; i++) {
if (segments[i].startsWith(":")) {
paramPositions.push({ index: i, name: segments[i].slice(1) });
}
}
const hasParams = paramPositions.length > 0;
return (urlSegments) => {
if (!hasWildcard && urlSegments.length !== segmentCount) {
return { matched: false, params: {} };
}
if (hasWildcard && urlSegments.length < segmentCount - 1) {
return { matched: false, params: {} };
}
if (!hasParams && !hasWildcard) {
for (let i = 0;i < segmentCount; i++) {
if (segments[i] !== urlSegments[i]) {
return { matched: false, params: {} };
}
}
return { matched: true, params: {} };
}
const params = {};
for (let i = 0;i < segmentCount; i++) {
const seg = segments[i];
const part = urlSegments[i];
if (seg === "*") {
params["*"] = urlSegments.slice(i).join("/");
return { matched: true, params };
}
if (seg.startsWith(":")) {
if (!part)
return { matched: false, params: {} };
params[seg.slice(1)] = decodeURIComponent(part);
} else if (seg !== part) {
return { matched: false, params: {} };
}
}
const matched = urlSegments.length === segmentCount;
return {
matched,
params: matched ? params : {}
};
};
}
function joinPaths(...paths) {
let result = "/";
for (let i = 0;i < paths.length; i++) {
const p = paths[i];
if (p && p !== "/") {
let start = 0;
let end = p.length;
if (p[0] === "/")
start++;
if (p[end - 1] === "/")
end--;
if (end > start) {
if (result !== "/")
result += "/";
result += p.slice(start, end);
}
}
}
return result;
}