UNPKG

exstack

Version:

A utility library designed to simplify and enhance Express.js applications.

274 lines (272 loc) 8.24 kB
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;