@bepalo/router 1.11.32 → 1.12.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/cjs/framework.d.ts +2 -4
  2. package/dist/cjs/framework.d.ts.map +1 -1
  3. package/dist/cjs/framework.js +4 -6
  4. package/dist/cjs/framework.js.map +1 -1
  5. package/dist/cjs/helpers.d.ts +2 -2
  6. package/dist/cjs/helpers.d.ts.map +1 -1
  7. package/dist/cjs/helpers.js +1 -1
  8. package/dist/cjs/helpers.js.map +1 -1
  9. package/dist/cjs/index.d.ts +5 -5
  10. package/dist/cjs/index.d.ts.map +1 -1
  11. package/dist/cjs/index.js +5 -5
  12. package/dist/cjs/index.js.map +1 -1
  13. package/dist/cjs/middlewares.d.ts +2 -2
  14. package/dist/cjs/middlewares.d.ts.map +1 -1
  15. package/dist/cjs/middlewares.js +24 -24
  16. package/dist/cjs/middlewares.js.map +1 -1
  17. package/dist/cjs/router.d.ts +2 -2
  18. package/dist/cjs/router.d.ts.map +1 -1
  19. package/dist/cjs/router.js +8 -8
  20. package/dist/cjs/router.js.map +1 -1
  21. package/dist/cjs/types.d.ts +1 -1
  22. package/dist/cjs/types.d.ts.map +1 -1
  23. package/dist/cjs/upload-stream.d.ts +1 -1
  24. package/dist/cjs/upload-stream.d.ts.map +1 -1
  25. package/dist/cjs/upload-stream.js +7 -7
  26. package/dist/cjs/upload-stream.js.map +1 -1
  27. package/dist/framework.d.ts +2 -4
  28. package/dist/framework.d.ts.map +1 -1
  29. package/dist/framework.js +4 -6
  30. package/dist/framework.js.map +1 -1
  31. package/dist/helpers.d.ts +2 -2
  32. package/dist/helpers.d.ts.map +1 -1
  33. package/dist/helpers.js +1 -1
  34. package/dist/helpers.js.map +1 -1
  35. package/dist/index.d.ts +5 -5
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +5 -5
  38. package/dist/index.js.map +1 -1
  39. package/dist/middlewares.d.ts +2 -2
  40. package/dist/middlewares.d.ts.map +1 -1
  41. package/dist/middlewares.js +24 -24
  42. package/dist/middlewares.js.map +1 -1
  43. package/dist/router.d.ts +2 -2
  44. package/dist/router.d.ts.map +1 -1
  45. package/dist/router.js +8 -8
  46. package/dist/router.js.map +1 -1
  47. package/dist/types.d.ts +1 -1
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/upload-stream.d.ts +1 -1
  50. package/dist/upload-stream.d.ts.map +1 -1
  51. package/dist/upload-stream.js +7 -7
  52. package/dist/upload-stream.js.map +1 -1
  53. package/package.json +8 -1
  54. package/src/framework.deno.ts +194 -0
  55. package/src/framework.ts +197 -0
  56. package/src/helpers.ts +829 -0
  57. package/src/index.ts +5 -0
  58. package/src/list.ts +462 -0
  59. package/src/middlewares.deno.ts +855 -0
  60. package/src/middlewares.ts +851 -0
  61. package/src/router.ts +993 -0
  62. package/src/tree.ts +139 -0
  63. package/src/types.ts +197 -0
  64. package/src/upload-stream.ts +661 -0
  65. package/dist/cjs/framework.deno.d.ts +0 -31
  66. package/dist/cjs/framework.deno.d.ts.map +0 -1
  67. package/dist/cjs/framework.deno.js +0 -245
  68. package/dist/cjs/framework.deno.js.map +0 -1
  69. package/dist/framework.deno.d.ts +0 -31
  70. package/dist/framework.deno.d.ts.map +0 -1
  71. package/dist/framework.deno.js +0 -245
  72. package/dist/framework.deno.js.map +0 -1
package/src/router.ts ADDED
@@ -0,0 +1,993 @@
1
+ /**
2
+ * @file A fast radix-trie based router for JavaScript runtimes.
3
+ * @module @bepalo/router
4
+ * @author Natnael Eshetu
5
+ * @exports Router
6
+ */
7
+
8
+ import { Tree } from "./tree.ts";
9
+ import type {
10
+ HttpMethod,
11
+ MethodPath,
12
+ Pipeline,
13
+ HandlerType,
14
+ HttpPath,
15
+ Handler,
16
+ HeaderTuple,
17
+ BoundHandler,
18
+ CTXError,
19
+ } from "./types.ts";
20
+
21
+ /**
22
+ * Checks if a string is a valid HTTP method.
23
+ * @param {string} method - The method string to validate
24
+ * @returns {boolean} True if the method is valid, false otherwise
25
+ */
26
+ export const isValidHttpMethod = (method: string): boolean => {
27
+ switch (method) {
28
+ case "HEAD":
29
+ case "OPTIONS":
30
+ case "GET":
31
+ case "POST":
32
+ case "PUT":
33
+ case "PATCH":
34
+ case "DELETE":
35
+ return true;
36
+ default:
37
+ return false;
38
+ }
39
+ };
40
+
41
+ /**
42
+ * Represents a parameter extracted from a route path.
43
+ * @typedef {Object} NodeParam
44
+ * @property {string} name - The parameter name (without the colon)
45
+ * @property {number} index - The position of the parameter in the path
46
+ */
47
+ type NodeParam = {
48
+ name: string;
49
+ index: number;
50
+ };
51
+
52
+ /**
53
+ * Represents a node in the routing tree.
54
+ * @typedef {Object} RouteNode
55
+ * @property {HttpMethod} method - HTTP method for this route
56
+ * @property {string} pathname - The original path pattern
57
+ * @property {Array<string>} nodes - Split path segments for the trie
58
+ * @property {Pipeline<Context>} pipeline - Handlers to execute for this route
59
+ * @property {Map<number, NodeParam>} [params] - Parameters extracted from the path
60
+ * @template Context
61
+ */
62
+ type RouteNode<Context = {}> = {
63
+ method: HttpMethod;
64
+ pathname: string;
65
+ nodes: Array<string>;
66
+ pipeline: Pipeline<Context>;
67
+ params?: Map<number, NodeParam>;
68
+ };
69
+
70
+ /**
71
+ * Base context object for router handlers.
72
+ * @typedef {Object} RouterContext
73
+ * @property {Record<string, string>} params - Route parameters extracted from the URL
74
+ * @property {Headers} headers - Response headers (can be modified by handlers)
75
+ * @property {Response} [response] - The final response object (set by handlers)
76
+ * @property {Error} [error] - Error object (set when an exception occurs)
77
+ * @property {Object} found - Information about which route types were matched
78
+ * @property {boolean} found.hooks - Whether any hooks were found
79
+ * @property {boolean} found.afters - Whether any after handlers were found
80
+ * @property {boolean} found.filters - Whether any filters were found
81
+ * @property {boolean} found.handlers - Whether any handlers were found
82
+ * @property {boolean} found.fallbacks - Whether any fallbacks were found
83
+ * @property {boolean} found.catchers - Whether any catchers were found
84
+ */
85
+ export type RouterContext<XContext = {}> = {
86
+ url: URL;
87
+ params: Record<string, string>;
88
+ headers: Headers;
89
+ response?: Response;
90
+ error?: Error;
91
+ found: {
92
+ hooks: boolean;
93
+ afters: boolean;
94
+ filters: boolean;
95
+ handlers: boolean;
96
+ fallbacks: boolean;
97
+ catchers: boolean;
98
+ };
99
+ } & XContext;
100
+
101
+ /**
102
+ * Initializes method trees for all HTTP methods.
103
+ * @returns {Record<HttpMethod, Tree<RouteNode<Context>>>} Trees for each HTTP method
104
+ * @template Context
105
+ */
106
+ function initMethodTrees<Context = {}>(): Record<
107
+ HttpMethod,
108
+ Tree<RouteNode<Context>>
109
+ > {
110
+ return {
111
+ HEAD: new Tree<RouteNode<Context>>(),
112
+ OPTIONS: new Tree<RouteNode<Context>>(),
113
+ GET: new Tree<RouteNode<Context>>(),
114
+ POST: new Tree<RouteNode<Context>>(),
115
+ PUT: new Tree<RouteNode<Context>>(),
116
+ PATCH: new Tree<RouteNode<Context>>(),
117
+ DELETE: new Tree<RouteNode<Context>>(),
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Configuration options for enabling/disabling handler types.
123
+ * @typedef {Object} HandlerEnable
124
+ * @property {boolean} hooks - Enable hook handlers
125
+ * @property {boolean} afters - Enable after handlers
126
+ * @property {boolean} filters - Enable filter handlers
127
+ * @property {boolean} fallbacks - Enable fallback handlers
128
+ * @property {boolean} catchers - Enable catcher handlers
129
+ */
130
+ interface HandlerEnable {
131
+ hooks: boolean;
132
+ afters: boolean;
133
+ filters: boolean;
134
+ fallbacks: boolean;
135
+ catchers: boolean;
136
+ }
137
+
138
+ /**
139
+ * Configuration options for the Router.
140
+ * @typedef {Object} RouterConfig
141
+ * @property {Array<HeaderTuple>|{(): Array<HeaderTuple>}} [defaultHeaders] - Default headers to add to all responses
142
+ * @property {Handler<Context>} [defaultCatcher] - Default error handler for uncaught exceptions
143
+ * @property {Handler<Context>} [defaultFallback] - Default handler for unmatched routes
144
+ * @property {HandlerEnable} [enable] - Configuration for enabling/disabling handler types
145
+ * @template Context
146
+ */
147
+ export interface RouterConfig<Context extends RouterContext = RouterContext> {
148
+ defaultHeaders?: Array<HeaderTuple> | { (): Array<HeaderTuple> };
149
+ defaultCatcher?: Handler<Context & CTXError>;
150
+ defaultFallback?: Handler<Context>;
151
+ enable?: Partial<HandlerEnable>;
152
+ normalizeTrailingSlash?: boolean;
153
+ }
154
+
155
+ /**
156
+ * Options for route registration.
157
+ * @typedef {Object} HandlerOptions
158
+ * @property {boolean} [overwrite] - Allow overwriting existing routes
159
+ */
160
+ export interface HandlerOptions {
161
+ overwrite?: boolean;
162
+ }
163
+
164
+ /**
165
+ * Handler settings infromation for use with append and auditing
166
+ */
167
+ interface HandlerSetter<Context extends RouterContext = RouterContext> {
168
+ handlerType: HandlerType;
169
+ urls: "*" | MethodPath | Array<MethodPath>;
170
+ pipeline: Handler<Context> | Pipeline<Context>;
171
+ options?: HandlerOptions;
172
+ }
173
+
174
+ /** @constant {Array} emptyArray - Empty array constant for optimization */
175
+ const emptyArray: unknown[] = [];
176
+
177
+ /**
178
+ * A fast radix-trie based router for JavaScript runtimes.
179
+ * Supports hooks, filters, handlers, fallbacks, catchers, and after handlers.
180
+ * @class
181
+ * @template EXContext
182
+ * @template Context extends RouterContext<EXContext>
183
+ * @example
184
+ * const router = new Router();
185
+ *
186
+ * // Register a simple GET handler
187
+ * router.handle("GET /users/:id", async (req, ctx) => {
188
+ * const userId = ctx.params.id;
189
+ * return json({ userId });
190
+ * });
191
+ *
192
+ * // Register a hook that runs before all /api routes
193
+ * router.hook("* /api/**", (req, ctx) => {
194
+ * console.log(`API request: ${req.method} ${req.url}`);
195
+ * });
196
+ *
197
+ * // Register an error handler
198
+ * router.catch("* /**", (req, ctx) => {
199
+ * console.error(ctx.error);
200
+ * return json({ error: "Something went wrong" }, { status: 500 });
201
+ * });
202
+ *
203
+ * // Handle a request and get a response
204
+ * const response = await router.respond(new Request("http://localhost/"));
205
+ *
206
+ */
207
+ export class Router<
208
+ EXTContext = {},
209
+ Context extends RouterContext<EXTContext> = RouterContext<EXTContext>,
210
+ > {
211
+ #trees: Record<HandlerType, Record<HttpMethod, Tree<RouteNode<Context>>>> = {
212
+ filter: initMethodTrees<Context>(),
213
+ hook: initMethodTrees<Readonly<Context>>(),
214
+ handler: initMethodTrees<Context>(),
215
+ fallback: initMethodTrees<Context>(),
216
+ catcher: initMethodTrees<Context>(),
217
+ after: initMethodTrees<Context>(),
218
+ };
219
+ #enable: HandlerEnable = {
220
+ hooks: true,
221
+ afters: true,
222
+ filters: true,
223
+ fallbacks: true,
224
+ catchers: true,
225
+ };
226
+ #defaultHeaders: Array<HeaderTuple> | { (): Array<HeaderTuple> } = [];
227
+ #defaultCatcher?: Handler<Context & CTXError>;
228
+ #defaultFallback?: Handler<Context>;
229
+ #normalizeTrailingSlash: boolean = false;
230
+ #setters: Set<HandlerSetter<Context>> = new Set();
231
+
232
+ static #ALL_METHOD_PATHS: MethodPath[] = [
233
+ "HEAD /.**",
234
+ "OPTIONS /.**",
235
+ "GET /.**",
236
+ "POST /.**",
237
+ "PUT /.**",
238
+ "PATCH /.**",
239
+ "DELETE /.**",
240
+ ];
241
+
242
+ /**
243
+ * Gets the routing trees for all handler types.
244
+ * @returns {Record<HandlerType, Record<HttpMethod, Tree<RouteNode<Context>>>>}
245
+ */
246
+ get trees(): Record<
247
+ HandlerType,
248
+ Record<HttpMethod, Tree<RouteNode<Context>>>
249
+ > {
250
+ return this.#trees;
251
+ }
252
+
253
+ /**
254
+ * Gets the enabled handler types configuration.
255
+ * @returns {HandlerEnable}
256
+ */
257
+ get enabled(): HandlerEnable {
258
+ return this.#enable;
259
+ }
260
+
261
+ /**
262
+ * Gets the default headers configuration.
263
+ * @returns {Array<HeaderTuple>|{():Array<HeaderTuple>}}
264
+ */
265
+ get defaultHeaders(): Array<HeaderTuple> {
266
+ return typeof this.#defaultHeaders === "function"
267
+ ? this.#defaultHeaders()
268
+ : this.#defaultHeaders;
269
+ }
270
+
271
+ /**
272
+ * Gets the default catcher handler.
273
+ * @returns {Handler<Context>|undefined}
274
+ */
275
+ get defaultCatcher(): Handler<Context & CTXError> | undefined {
276
+ return this.#defaultCatcher;
277
+ }
278
+
279
+ /**
280
+ * Gets the default fallback handler.
281
+ * @returns {Handler<Context>|undefined}
282
+ */
283
+ get defaultFallback(): Handler<Context> | undefined {
284
+ return this.#defaultFallback;
285
+ }
286
+
287
+ /**
288
+ * Gets the current configuration to normalize trailing slash.
289
+ * @returns {boolean}
290
+ */
291
+ get normalizeTrailingSlash(): boolean {
292
+ return this.#normalizeTrailingSlash;
293
+ }
294
+
295
+ /**
296
+ * Gets the route registration history.
297
+ * @returns {Set<HandlerSetter<Context>>}
298
+ */
299
+ get setters(): Set<HandlerSetter<Context>> {
300
+ return this.#setters;
301
+ }
302
+
303
+ /**
304
+ * Creates a new Router instance.
305
+ * @param {RouterConfig<Context>} [config] - Configuration options
306
+ */
307
+ constructor(config?: RouterConfig<Context>) {
308
+ this.respond = this.respond.bind(this);
309
+ if (config?.defaultHeaders) {
310
+ this.#defaultHeaders = config.defaultHeaders;
311
+ }
312
+ if (config?.enable) {
313
+ this.#enable = {
314
+ hooks: false,
315
+ afters: false,
316
+ filters: false,
317
+ fallbacks: false,
318
+ catchers: false,
319
+ ...config.enable,
320
+ };
321
+ }
322
+ if (config?.defaultCatcher) {
323
+ this.#defaultCatcher = config.defaultCatcher;
324
+ }
325
+ if (config?.defaultFallback) {
326
+ this.#defaultFallback = config.defaultFallback;
327
+ }
328
+ if (config?.normalizeTrailingSlash) {
329
+ this.#normalizeTrailingSlash = config.normalizeTrailingSlash;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Registers a hook handler that runs before other handlers.
335
+ * Hooks cannot modify the response directly but can modify context.
336
+ * Their responses are ignored.
337
+ * @param {"*"|MethodPath|Array<MethodPath>} urls - URL patterns to match
338
+ * @param {Handler<Context & XContext>|Pipeline<Context & XContext>} pipeline - Handler(s) to execute
339
+ * @param {HandlerOptions} [options] - Registration options
340
+ * @returns {Router<Context & XContext>} The router instance for chaining
341
+ * @template XContext
342
+ * @example
343
+ * router.hook("GET /api/**", (req, ctx) => {
344
+ * ctx.startTime = Date.now();
345
+ * });
346
+ */
347
+ hook<XContext = {}>(
348
+ urls: "*" | MethodPath | Array<MethodPath>,
349
+ pipeline: Handler<Context & XContext> | Pipeline<Context & XContext>,
350
+ options?: HandlerOptions,
351
+ ): Router<Context & XContext> {
352
+ return this.setRoutes(
353
+ "hook",
354
+ urls,
355
+ pipeline as unknown as Handler<Context> | Pipeline<Context>,
356
+ options,
357
+ );
358
+ }
359
+
360
+ /**
361
+ * Registers an after handler that runs after the response is created.
362
+ * After handlers can inspect and modify the response from the context.
363
+ * Their responses are ignored.
364
+ * @param {"*"|MethodPath|Array<MethodPath>} urls - URL patterns to match
365
+ * @param {Handler<Context & XContext>|Pipeline<Context & XContext>} pipeline - Handler(s) to execute
366
+ * @param {HandlerOptions} [options] - Registration options
367
+ * @returns {Router<Context & XContext>} The router instance for chaining
368
+ * @template XContext
369
+ * @example
370
+ * router.after("GET /**", (req, ctx) => {
371
+ * console.log(`Request completed: ${req.method} ${req.url}`);
372
+ * });
373
+ */
374
+ after<XContext = {}>(
375
+ urls: "*" | MethodPath | Array<MethodPath>,
376
+ pipeline: Handler<Context & XContext> | Pipeline<Context & XContext>,
377
+ options?: HandlerOptions,
378
+ ): Router<Context & XContext> {
379
+ return this.setRoutes(
380
+ "after",
381
+ urls,
382
+ pipeline as unknown as Handler<Context> | Pipeline<Context>,
383
+ options,
384
+ );
385
+ }
386
+
387
+ /**
388
+ * Registers a filter handler that can intercept and modify requests.
389
+ * Filters run after hooks but before handlers and can return a response.
390
+ * @param {"*"|MethodPath|Array<MethodPath>} urls - URL patterns to match
391
+ * @param {Handler<Context & XContext>|Pipeline<Context & XContext>} pipeline - Handler(s) to execute
392
+ * @param {HandlerOptions} [options] - Registration options
393
+ * @returns {Router<Context & XContext>} The router instance for chaining
394
+ * @template XContext
395
+ * @example
396
+ * router.filter("GET /admin/**", (req, ctx) => {
397
+ * if (!req.headers.get("x-admin-token")) {
398
+ * return json({ error: "Unauthorized" }, { status: 401 });
399
+ * }
400
+ * });
401
+ */
402
+ filter<XContext = {}>(
403
+ urls: "*" | MethodPath | Array<MethodPath>,
404
+ pipeline: Handler<Context & XContext> | Pipeline<Context & XContext>,
405
+ options?: HandlerOptions,
406
+ ): Router<Context & XContext> {
407
+ return this.setRoutes(
408
+ "filter",
409
+ urls,
410
+ pipeline as unknown as Handler<Context> | Pipeline<Context>,
411
+ options,
412
+ );
413
+ }
414
+
415
+ /**
416
+ * Registers a main request handler.
417
+ * Handlers are the primary way to respond to requests.
418
+ * @param {"*"|MethodPath|Array<MethodPath>} urls - URL patterns to match
419
+ * @param {Handler<Context & XContext>|Pipeline<Context & XContext>} pipeline - Handler(s) to execute
420
+ * @param {HandlerOptions} [options] - Registration options
421
+ * @returns {Router<Context & XContext>} The router instance for chaining
422
+ * @template XContext
423
+ * @example
424
+ * router.handle("GET /users", async (req, ctx) => {
425
+ * const users = await getUsers();
426
+ * return json({ users });
427
+ * });
428
+ */
429
+ handle<XContext = {}>(
430
+ urls: "*" | MethodPath | Array<MethodPath>,
431
+ pipeline: Handler<Context & XContext> | Pipeline<Context & XContext>,
432
+ options?: HandlerOptions,
433
+ ): Router<Context & XContext> {
434
+ return this.setRoutes(
435
+ "handler",
436
+ urls,
437
+ pipeline as unknown as Handler<Context> | Pipeline<Context>,
438
+ options,
439
+ );
440
+ }
441
+
442
+ /**
443
+ * Registers a fallback handler that runs when no main handler matches.
444
+ * Fallbacks are useful for custom 404 pages or default behaviors.
445
+ * @param {"*"|MethodPath|Array<MethodPath>} urls - URL patterns to match
446
+ * @param {Handler<Context & XContext>|Pipeline<Context & XContext>} pipeline - Handler(s) to execute
447
+ * @param {HandlerOptions} [options] - Registration options
448
+ * @returns {Router<Context & XContext>} The router instance for chaining
449
+ * @template XContext
450
+ * @example
451
+ * router.fallback("GET /**", (req, ctx) => {
452
+ * return json({ error: "Not found" }, { status: 404 });
453
+ * });
454
+ */
455
+ fallback<XContext = {}>(
456
+ urls: "*" | MethodPath | Array<MethodPath>,
457
+ pipeline: Handler<Context & XContext> | Pipeline<Context & XContext>,
458
+ options?: HandlerOptions,
459
+ ): Router<Context & XContext> {
460
+ return this.setRoutes(
461
+ "fallback",
462
+ urls,
463
+ pipeline as unknown as Handler<Context> | Pipeline<Context>,
464
+ options,
465
+ );
466
+ }
467
+
468
+ /**
469
+ * Registers an error handler for catching exceptions.
470
+ * Catchers receive the error in the context and can return a response.
471
+ * @param {"*"|MethodPath|Array<MethodPath>} urls - URL patterns to match
472
+ * @param {Handler<Context & XContext & CTXError>|Pipeline<Context & XContext & CTXError>} pipeline - Handler(s) to execute
473
+ * @param {HandlerOptions} [options] - Registration options
474
+ * @returns {Router<Context & XContext & CTXError>} The router instance for chaining
475
+ * @template XContext
476
+ * @example
477
+ * router.catch("GET /**", (req, ctx) => {
478
+ * console.error(ctx.error);
479
+ * return json({ error: "Internal server error" }, { status: 500 });
480
+ * });
481
+ */
482
+ catch<XContext = {}>(
483
+ urls: "*" | MethodPath | Array<MethodPath>,
484
+ pipeline:
485
+ | Handler<Context & XContext & CTXError>
486
+ | Pipeline<Context & XContext & CTXError>,
487
+ options?: HandlerOptions,
488
+ ): Router<Context & XContext & CTXError> {
489
+ return this.setRoutes(
490
+ "catcher",
491
+ urls,
492
+ pipeline as unknown as Handler<Context> | Pipeline<Context>,
493
+ options,
494
+ );
495
+ }
496
+
497
+ /**
498
+ * Appends routes from another router under a base URL.
499
+ * But does not merge router configurations.
500
+ * Useful for organizing routes by prefix.
501
+ * @param {`/${string}`} baseUrl - The base URL to mount the router under
502
+ * @param {Router<Context>} router - The router to append
503
+ * @param {HandlerOptions} [options] - Registration options
504
+ * @returns {Router<Context>} The router instance for chaining
505
+ * @example
506
+ * const apiRouter = new Router();
507
+ * apiRouter.handle("GET /users", getUsersHandler);
508
+ *
509
+ * const mainRouter = new Router();
510
+ * mainRouter.append("/api", apiRouter);
511
+ * // Now GET /api/users routes to getUsersHandler
512
+ */
513
+ append(
514
+ baseUrl: `/${string}`,
515
+ router: Router<Context>,
516
+ options?: HandlerOptions,
517
+ ): Router<Context> {
518
+ baseUrl =
519
+ baseUrl.charAt(baseUrl.length - 1) === "/"
520
+ ? (baseUrl.slice(0, baseUrl.length - 1) as `/${string}`)
521
+ : baseUrl;
522
+ for (const elem of router.#setters) {
523
+ let urls: "*" | MethodPath | Array<MethodPath>;
524
+ if (typeof elem.urls === "string") {
525
+ const [method, path] = elem.urls.split(" ", 2);
526
+ urls = `${method} ${baseUrl}${path}` as MethodPath;
527
+ } else {
528
+ urls = elem.urls.map((url: MethodPath) => {
529
+ const [method, path] = url.split(" ", 2);
530
+ return `${method} ${baseUrl}${path}` as MethodPath;
531
+ });
532
+ }
533
+ this.setRoutes(elem.handlerType, urls, elem.pipeline, {
534
+ ...elem.options,
535
+ ...options,
536
+ });
537
+ }
538
+ return this;
539
+ }
540
+
541
+ /**
542
+ * Low-level method to register routes of any handler type.
543
+ * @param {HandlerType} handlerType - The type of handler to register
544
+ * @param {"*"|MethodPath|Array<MethodPath>} urls - URL patterns to match
545
+ * @param {Handler<Context>|Pipeline<Context>|Handler<Context&CTXError>|Pipeline<Context&CTXError>} pipeline_ - Handler(s) to execute
546
+ * @param {HandlerOptions} [options] - Registration options
547
+ * @returns {Router<Context>} The router instance for chaining
548
+ * @private
549
+ */
550
+ setRoutes(
551
+ handlerType: HandlerType,
552
+ urls: "*" | MethodPath | Array<MethodPath>,
553
+ pipeline_:
554
+ | Handler<Context>
555
+ | Pipeline<Context>
556
+ | Handler<Context & CTXError>
557
+ | Pipeline<Context & CTXError>,
558
+ options?: HandlerOptions,
559
+ ): Router<Context> {
560
+ const pipeline: Pipeline<Context> | Pipeline<Context & CTXError> =
561
+ Array.isArray(pipeline_)
562
+ ? pipeline_
563
+ : ([pipeline_] as Pipeline<Context> | Pipeline<Context & CTXError>);
564
+ const splitUrls = this.#splitUrl(
565
+ urls === "*" ? Router.#ALL_METHOD_PATHS : urls,
566
+ );
567
+ for (const { method, nodes, params, pathname } of splitUrls) {
568
+ const treeNode = this.#trees[handlerType][method];
569
+ const splitPaths = this.#normalizePathname(pathname);
570
+ const splitPathsLength_1 = splitPaths.length - 1;
571
+ for (let i = 0; i < splitPathsLength_1; i++) {
572
+ switch (splitPaths[i]) {
573
+ case "**":
574
+ throw new Error(
575
+ `Super-glob '**' in the middle of pathname '${pathname}'. Should only be at the end.`,
576
+ );
577
+ case ".**":
578
+ throw new Error(
579
+ `Super-glob '.**' in the middle of pathname '${pathname}'. Should only be at the end.`,
580
+ );
581
+ case ".*":
582
+ throw new Error(
583
+ `glob '.*' in the middle of pathname '${pathname}'. Should only be at the end.`,
584
+ );
585
+ }
586
+ }
587
+ if (!options?.overwrite) {
588
+ const node = treeNode.get(splitPaths);
589
+ if (node) {
590
+ const maxLen = Math.max(node.nodes.length, splitPaths.length);
591
+ let colliding: number | null = null;
592
+ for (let i = 0; i < maxLen; i++) {
593
+ if (node.nodes[i] === "*") {
594
+ if (splitPaths[i].startsWith(":") || splitPaths[i] === "*") {
595
+ colliding = i;
596
+ } else {
597
+ break;
598
+ }
599
+ } else if (node.nodes[i] === splitPaths[i]) {
600
+ colliding = i;
601
+ }
602
+ }
603
+ if (colliding != null && colliding === maxLen - 1) {
604
+ throw new Error(
605
+ `Overriding route ${method} '${node.pathname}' with '${pathname}'`,
606
+ );
607
+ }
608
+ }
609
+ }
610
+ treeNode.set(nodes, {
611
+ method,
612
+ nodes,
613
+ pipeline: pipeline as Pipeline<Context>,
614
+ pathname,
615
+ params,
616
+ });
617
+ }
618
+ // add to setters for later use
619
+ this.#setters.add({
620
+ handlerType,
621
+ urls,
622
+ pipeline: pipeline_ as Pipeline<Context>,
623
+ options,
624
+ });
625
+ return this;
626
+ }
627
+
628
+ /**
629
+ * Handles an incoming HTTP request and returns a response.
630
+ * This is the main entry point for request processing.
631
+ * Handlers are only called if they are not disabled.
632
+ * @param {Request} req - The incoming HTTP request
633
+ * @param {Partial<Context>} [context] - Initial context object
634
+ * @returns {Promise<Response>} The HTTP response
635
+ */
636
+ async respond(
637
+ req: Request,
638
+ context?: Partial<Omit<Context, "url" | "error" | "found" | "response">>,
639
+ ): Promise<Response> {
640
+ const method = req.method as HttpMethod;
641
+ if (!isValidHttpMethod(method)) {
642
+ return new Response("Method Not Allowed", {
643
+ status: 405,
644
+ statusText: "Method Not Allowed",
645
+ headers: context?.headers ?? new Headers(this.defaultHeaders),
646
+ });
647
+ }
648
+ let response: void | boolean | Response = undefined;
649
+ const url = new URL(req.url);
650
+ const key = this.#normalizePathname(url.pathname);
651
+ const hookNodes = this.enabled.hooks
652
+ ? this.#trees.hook[method].getAll(key)
653
+ : (emptyArray as RouteNode<Context>[]);
654
+ const afterNodes = this.enabled.afters
655
+ ? this.#trees.after[method].getAll(key)
656
+ : (emptyArray as RouteNode<Context>[]);
657
+ const filterNodes = this.enabled.filters
658
+ ? this.#trees.filter[method].getAll(key)
659
+ : (emptyArray as RouteNode<Context>[]);
660
+ const handlerNodes = this.#trees.handler[method].getAll(key);
661
+ const fallbackNodes = this.enabled.fallbacks
662
+ ? this.#trees.fallback[method].getAll(key)
663
+ : (emptyArray as RouteNode<Context>[]);
664
+ const catcherNodes = (
665
+ this.enabled.catchers
666
+ ? this.#trees.catcher[method].getAll(key)
667
+ : emptyArray
668
+ ) as RouteNode<Context & CTXError>[];
669
+ const found = {
670
+ hooks: hookNodes.length > 0,
671
+ afters: afterNodes.length > 0,
672
+ filters: filterNodes.length > 0,
673
+ handlers: handlerNodes.length > 0,
674
+ fallbacks: fallbackNodes.length > 0,
675
+ catchers: catcherNodes.length > 0,
676
+ };
677
+ const ctx = {
678
+ url,
679
+ params: context?.params ?? {},
680
+ headers: context?.headers ?? new Headers(this.defaultHeaders),
681
+ found,
682
+ ...context,
683
+ } as Context;
684
+ const reqCtx: [req: Request, ctx: RouterContext<Context>] = [req, ctx];
685
+ try {
686
+ // hooks
687
+ if (found.hooks) {
688
+ const params = hookNodes[0].params;
689
+ if (params) {
690
+ for (const [index, param] of params) {
691
+ ctx.params[param.name] = key[index];
692
+ }
693
+ }
694
+ let hookResponse: void | boolean | Response = undefined;
695
+ for (const hookNode of hookNodes) {
696
+ for (const hook of hookNode.pipeline) {
697
+ hookResponse = await (hook as BoundHandler<Context>).apply(
698
+ this,
699
+ reqCtx,
700
+ );
701
+ if (hookResponse) break;
702
+ }
703
+ if (hookResponse) break;
704
+ }
705
+ }
706
+ // filters
707
+ if (found.filters) {
708
+ const params = filterNodes[0].params;
709
+ if (params) {
710
+ for (const [index, param] of params) {
711
+ ctx.params[param.name] = key[index];
712
+ }
713
+ }
714
+ for (const filterNode of filterNodes) {
715
+ for (const filter of filterNode.pipeline) {
716
+ response = await (filter as BoundHandler<Context>).apply(
717
+ this,
718
+ reqCtx,
719
+ );
720
+ if (response) break;
721
+ }
722
+ if (response) break;
723
+ }
724
+ }
725
+ // handlers
726
+ if (found.handlers) {
727
+ const params = handlerNodes[0].params;
728
+ if (params) {
729
+ for (const [index, param] of params) {
730
+ ctx.params[param.name] = key[index];
731
+ }
732
+ }
733
+ if (!(response instanceof Response)) {
734
+ for (const handlerNode of handlerNodes) {
735
+ for (const handler of handlerNode.pipeline) {
736
+ response = await (handler as BoundHandler<Context>).apply(
737
+ this,
738
+ reqCtx,
739
+ );
740
+ if (response) break;
741
+ }
742
+ if (response) break;
743
+ }
744
+ }
745
+ }
746
+ // fallbacks
747
+ if (!(response instanceof Response)) {
748
+ if (found.fallbacks) {
749
+ const params = fallbackNodes[0].params;
750
+ if (params) {
751
+ for (const [index, param] of params) {
752
+ ctx.params[param.name] = key[index];
753
+ }
754
+ }
755
+ for (const fallbackNode of fallbackNodes) {
756
+ for (const fallback of fallbackNode.pipeline) {
757
+ response = await (fallback as BoundHandler<Context>).apply(
758
+ this,
759
+ reqCtx,
760
+ );
761
+ if (response) break;
762
+ }
763
+ if (response) break;
764
+ }
765
+ }
766
+ }
767
+ if (!(response instanceof Response) && this.#defaultFallback) {
768
+ response = await this.#defaultFallback(req, ctx);
769
+ }
770
+ // append context headers to response
771
+ if (response instanceof Response) {
772
+ if (ctx.headers) {
773
+ for (const [key, value] of ctx.headers) {
774
+ response.headers.set(key, value);
775
+ }
776
+ }
777
+ }
778
+ response =
779
+ (typeof response === "boolean" ? null : response) ??
780
+ (found.handlers || found.fallbacks
781
+ ? new Response(null, {
782
+ status: 204,
783
+ statusText: "No Content",
784
+ headers: ctx.headers,
785
+ })
786
+ : new Response("Not Found", {
787
+ status: 404,
788
+ statusText: "Not Found",
789
+ headers: ctx.headers,
790
+ }));
791
+ if (response instanceof Response) {
792
+ ctx.response = response;
793
+ }
794
+ // after response handlers
795
+ if (found.afters) {
796
+ const params = afterNodes[0].params;
797
+ if (params) {
798
+ for (const [index, param] of params) {
799
+ ctx.params[param.name] = key[index];
800
+ }
801
+ }
802
+ let afterResponse: void | boolean | Response = undefined;
803
+ for (const afterNode of afterNodes) {
804
+ for (const after of afterNode.pipeline) {
805
+ afterResponse = await (after as BoundHandler<Context>).apply(
806
+ this,
807
+ reqCtx,
808
+ );
809
+ if (afterResponse) break;
810
+ }
811
+ if (afterResponse) break;
812
+ }
813
+ }
814
+ } catch (error) {
815
+ // error handlers
816
+ ctx.error = error instanceof Error ? error : new Error(String(error));
817
+ if (found.catchers) {
818
+ try {
819
+ const params = catcherNodes[0].params;
820
+ if (params) {
821
+ for (const [index, param] of params) {
822
+ ctx.params[param.name] = key[index];
823
+ }
824
+ }
825
+ for (const catcherNode of catcherNodes) {
826
+ for (const catcher of catcherNode.pipeline) {
827
+ response = await (catcher as BoundHandler<Context>).apply(
828
+ this,
829
+ reqCtx,
830
+ );
831
+ if (response) break;
832
+ }
833
+ if (response) break;
834
+ }
835
+ } catch (error) {
836
+ ctx.error = error instanceof Error ? error : new Error(String(error));
837
+ if (this.#defaultCatcher) {
838
+ response = await this.#defaultCatcher(
839
+ req,
840
+ ctx as Context & CTXError,
841
+ );
842
+ if (response instanceof Response) {
843
+ ctx.response = response;
844
+ }
845
+ }
846
+ }
847
+ }
848
+ if (this.#defaultCatcher && !(response instanceof Response)) {
849
+ response = await this.#defaultCatcher(req, ctx as Context & CTXError);
850
+ if (response instanceof Response) {
851
+ ctx.response = response;
852
+ }
853
+ }
854
+ if (!(response instanceof Response)) {
855
+ response = new Response("Internal Server Error", {
856
+ status: 500,
857
+ statusText: "Internal Server Error",
858
+ headers: ctx.headers,
859
+ });
860
+ ctx.response = response;
861
+ }
862
+ }
863
+ return response;
864
+ }
865
+
866
+ #normalizePathname(pathname: string): string[] {
867
+ return pathname === "/"
868
+ ? [""]
869
+ : (this.#normalizeTrailingSlash
870
+ ? pathname.substring(
871
+ 1,
872
+ pathname.endsWith("/") ? pathname.length - 1 : pathname.length,
873
+ )
874
+ : pathname.substring(1)
875
+ ).split("/");
876
+ }
877
+
878
+ /**
879
+ * Splits URL patterns into their components for routing.
880
+ * Supports wildcards (*), super-globs (**), and parameters (:param).
881
+ * @param {MethodPath|Array<MethodPath>} urls - URL patterns to split
882
+ * @returns {Array<SplitURL>} Array of split URL components
883
+ * @private
884
+ * @example
885
+ * // Returns: [{ method: 'GET', pathname: '/users/:id', nodes: ['users', '*'], params: Map({1: {name: 'id', index: 1}}) }]
886
+ * splitUrl(["GET /users/:id"]);
887
+ */
888
+ #splitUrl(urls: MethodPath | Array<MethodPath>): Array<SplitURL> {
889
+ urls = (Array.isArray(urls) ? urls : [urls]) as Array<MethodPath>;
890
+ let splitUrls: Array<SplitURL> = [];
891
+ for (const mp of urls) {
892
+ const [_method, pathname] = mp
893
+ .split(" ", 2)
894
+ .map((mu: string) => mu.trim()) as [
895
+ HttpMethod | "ALL" | "CRUD",
896
+ HttpPath,
897
+ ];
898
+ const methods =
899
+ _method === "ALL"
900
+ ? ALL_METHODS
901
+ : _method === "CRUD"
902
+ ? CRUD_METHODS
903
+ : [_method];
904
+ const pathNodes = this.#normalizePathname(pathname);
905
+ for (const method of methods) {
906
+ const params: Map<number, { name: string; index: number }> = new Map();
907
+ const nodes: Array<string> = [];
908
+ const lastPathNode =
909
+ pathNodes.length > 0 && pathNodes[pathNodes.length - 1];
910
+ // check the last path node to match globs '.**'
911
+ if (lastPathNode === ".**") {
912
+ const curNodes = pathNodes.slice(0, pathNodes.length - 1);
913
+ splitUrls.push({
914
+ method,
915
+ pathname,
916
+ nodes: curNodes.length === 0 ? [""] : [...curNodes],
917
+ params,
918
+ });
919
+ splitUrls.push({
920
+ method,
921
+ pathname,
922
+ nodes: [...curNodes, "**"],
923
+ params,
924
+ });
925
+ } else if (lastPathNode === ".*") {
926
+ const curNodes = pathNodes.splice(0, pathNodes.length - 1);
927
+ splitUrls.push({
928
+ method,
929
+ pathname,
930
+ nodes: curNodes.length === 0 ? [""] : [...curNodes],
931
+ params,
932
+ });
933
+ splitUrls.push({
934
+ method,
935
+ pathname,
936
+ nodes: [...curNodes, "*"],
937
+ params,
938
+ });
939
+ } else {
940
+ // process the path nodes
941
+ for (let index = 0; index < pathNodes.length; index++) {
942
+ const pathNode = pathNodes[index];
943
+ if (pathNode === ".**" && index < pathNodes.length - 1) {
944
+ throw new Error("Super-Glob not at the end of pathname");
945
+ }
946
+ if (pathNode.startsWith(":")) {
947
+ const name = pathNode.substring(1);
948
+ params.set(index, { name, index });
949
+ nodes.push("*");
950
+ } else {
951
+ nodes.push(pathNode);
952
+ }
953
+ }
954
+ splitUrls.push({ method, pathname, nodes, params });
955
+ }
956
+ }
957
+ }
958
+ return splitUrls;
959
+ }
960
+ }
961
+
962
+ export interface SplitURL {
963
+ method: HttpMethod;
964
+ pathname: HttpPath;
965
+ nodes: string[];
966
+ params: Map<
967
+ number,
968
+ {
969
+ name: string;
970
+ index: number;
971
+ }
972
+ >;
973
+ }
974
+
975
+ export const ALL_METHODS: HttpMethod[] = [
976
+ "HEAD",
977
+ "OPTIONS",
978
+ "GET",
979
+ "POST",
980
+ "PUT",
981
+ "PATCH",
982
+ "DELETE",
983
+ ];
984
+
985
+ export const CRUD_METHODS: HttpMethod[] = [
986
+ "GET",
987
+ "POST",
988
+ "PUT",
989
+ "PATCH",
990
+ "DELETE",
991
+ ];
992
+
993
+ export default Router;