exstack
Version:
A utility library designed to simplify and enhance Express.js applications.
274 lines (272 loc) • 8.24 kB
JavaScript
const require_http_error = require('../helps/http-error.cjs');
const require_utils = require('./utils.cjs');
const require_param = require('./param.cjs');
const require_handler = require('../handler.cjs');
const require_layer = require('./layer.cjs');
const require_smart = require('./smart.cjs');
const require_types = require('../types.cjs');
const require_index = require('./trie-tree/index.cjs');
const require_index$1 = require('./reg-exp/index.cjs');
//#region src/router/index.ts
/**
* Lightweight, Express-compatible router built on top of a RegExp/Trie based matcher.
* Supports single or multiple route matchers (Trie, RegExp, or both).
*
* Inspired by Hono (https://github.com/honojs/hono) for SmartRouter ideas
* like multi-router delegation and single-router optimization.
*
*/
var Router = class {
routes = [];
#basePath = "/";
#path = "/";
router;
/** Register a GET route. */
get;
/** Register a POST route. */
post;
/** Register a PUT route. */
put;
/** Register a DELETE route. */
delete;
/** Register a PATCH route. */
patch;
/** Register a HEAD route. */
head;
/** Register an OPTIONS route. */
options;
/** Register a route matching any HTTP method. */
all;
/**
* Creates a new Router instance.
*
* @param router - Determines internal matcher:
* 'trie' → uses TrieRouter
* 'regexp' → uses RegExpRouter
* 'both' → uses SmartRouter (RegExp + Trie)
* IRouter → use provided router directly
*
* @example
* // Multi-router (default)
* import express from 'express';
* import {Router} from 'exstack';
*
* const app = express();
* const api = new Router(); // uses both Trie + RegExp internally
*
* api.get('/ping', (req, res) => res.send('pong'));
* api.post('/login', (req, res) => res.send({ token: 'abc123' }));
*
* app.use(api.dispatch);
*/
constructor(router = "both") {
[...require_types.METHODS, require_types.METHOD_NAME_ALL_LOWERCASE].forEach((method) => {
this[method] = (arg1, ...args) => {
const path = typeof arg1 === "string" ? arg1 : this.#path;
if (typeof arg1 !== "string") this.#addRoute(method, path, arg1);
args.forEach((handler) => this.#addRoute(method, path, handler));
return this;
};
});
switch (router) {
case "trie":
this.router = new require_index.TrieRouter();
break;
case "regexp":
this.router = new require_index$1.RegExpRouter();
break;
case "both":
this.router = new require_smart.SmartRouter({ routers: [new require_index$1.RegExpRouter(), new require_index.TrieRouter()] });
break;
default: throw new Error(`Router constructor expects 'trie', 'regexp', 'both'. Received: ${router}`);
}
}
/**
* Register a route for one or more HTTP methods and paths.
*
* @param method - A single method or an array of methods (e.g. `'get'`, `'post'`).
* @param path - A single path or an array of paths.
* @param handlers - One or more handlers to attach.
* @returns This router instance (for chaining).
*
* @example
* ```ts
* router.on(['get', 'post'], ['/user', '/account'], handler);
* ```
*/
on = (method, path, ...handlers) => {
for (const p of [path].flat()) {
this.#path = p;
for (const m of [method].flat()) handlers.forEach((handler) => this.#addRoute(m.toUpperCase(), this.#path, handler));
}
return this;
};
/**
* Registers middleware handlers.
* Works similarly to `app.use()` in Express.
*
* - If called with a path: attaches handlers only for that path.
* - If called without a path: applies globally to all requests.
*
* @param arg1 - Path string or the first handler function.
* @param handlers - Additional handler functions.
* @returns This router instance.
*
* @example
* ```ts
* router.use(authMiddleware);
* router.use('/api', apiMiddleware);
* ```
*/
use = (arg1, ...handlers) => {
if (typeof arg1 === "string") this.#path = arg1;
else {
this.#path = "*";
handlers.unshift(arg1);
}
handlers.forEach((handler) => this.#addRoute(require_types.METHOD_NAME_ALL, this.#path, handler));
return this;
};
/**
* Mounts another `Router` instance at a given path prefix.
*
* @param path - Path prefix at which to mount the sub-router.
* @param router - Another Router instance to mount.
* @returns This router instance.
*
* @example
* // Example 1
* const r1 = new Router();
* const r2 = new Router();
*
* api.get('/user', (req, res) => res.send('user'));
* app.route('/api', api); // Mounts as /api/user
*
* // Example 2
* const r1 = new Router('trie');
* const r2 = new Router('regexp');
*
* app.use('/r1', r1.dispatch); // Mounts as /r1
* app.use('/r2', r2.dispatch) // Mounts as /r2
*
* // Example 3
* const r1 = new Router('trie'); // regex
* const r2 = new Router('trie'); // regex
*
* r1.route('/r2', r2); // Mounts as /r2
*
* // Example 4
* const r1 = new Router();
* const r2 = new Router('trie');
* const r3 = new Router('regex');
*
* r1.route('/r2', r2); // Mounts as /r2
* r1.route('/r3', r3); // Mounts as /r3
*
*/
route(path, router) {
if (router === this) throw new Error("Cannot mount router onto itself");
const base = require_utils.mergePath(this.#basePath, path);
if (this.router.name === router.router.name || this.router.name === "SmartRouter") router.routes.forEach((r) => {
this.#addRoute(r.method, require_utils.mergePath(base, r.path), r.handler);
});
else throw new Error(`Cannot mount sub-router with different type (${router.router.name}) on root router (${this.router.name})!`);
return this;
}
/**
* Internal method that registers a route into the internal matcher.
*/
#addRoute(method, path, handler) {
method = method.toUpperCase();
const fullPath = require_utils.mergePath(this.#basePath, path);
const route = {
basePath: this.#basePath,
path: fullPath,
method,
handler
};
this.router.add(method, path, [handler, route]);
this.routes.push(route);
return this;
}
/**
* Lazily attaches `req.params` and `req.param()` helpers to a request object.
*/
#attachParams(req, result) {
if (req._param) return;
const instance = new require_param.Param(req, result);
req._param = instance;
const parentParams = req.params ? { ...req.params } : {};
Object.defineProperty(req, "params", {
configurable: true,
enumerable: true,
get() {
const local = instance.params();
return Object.keys(parentParams).length ? {
...parentParams,
...local
} : local;
},
set(value) {
Object.defineProperty(req, "params", {
value,
writable: true,
configurable: true,
enumerable: true
});
}
});
req.param = (key) => instance.param(key) ?? parentParams[key];
}
/**
* Express-compatible middleware that dispatches incoming requests.
*
* @remarks
* Matches the incoming request against registered routes,
* attaches `req.params` dynamically, and invokes matched handlers.
* Falls back to `next()` if no route matches.
*
* @example
* ```ts
* const app = express();
* const api = new Router();
*
* api.get('/ping', (req, res) => res.send('pong'));
*
* app.use(api.dispatch);
* ```
*/
dispatch = (req, res, next) => {
try {
const path = req.path || req.url;
const method = req.method === "HEAD" ? "GET" : req.method;
const result = this.router.match(method, path);
if (typeof req.valid !== "function") req.valid = (t) => {
throw new require_http_error.HttpError(501, {
code: "INTERNAL_SERVER_ERROR",
message: `Request validation for '${t}' was not run. Use validator.${t === "all" ? "all()" : t + "()"} middleware first.`
});
};
if (!result || !result[0]?.length) return next();
this.#attachParams(req, result);
const handlers = result[0];
if (handlers.length === 1) {
const handler = handlers[0][0][0];
req.routeIndex = 0;
try {
const maybePromise = handler(req, res, next);
if (maybePromise instanceof Promise) maybePromise.then((v) => require_handler.handleResult(v, res)).catch(next);
else require_handler.handleResult(maybePromise, res);
} catch (err) {
next(err);
}
return;
}
require_layer.compose(handlers)(req, res, next);
} catch (error) {
next(error);
}
};
};
//#endregion
exports.Router = Router;