@bepalo/router 1.11.32 → 1.12.33
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.
- package/dist/cjs/framework.d.ts +2 -4
- package/dist/cjs/framework.d.ts.map +1 -1
- package/dist/cjs/framework.js +4 -6
- package/dist/cjs/framework.js.map +1 -1
- package/dist/cjs/helpers.d.ts +2 -2
- package/dist/cjs/helpers.d.ts.map +1 -1
- package/dist/cjs/helpers.js +1 -1
- package/dist/cjs/helpers.js.map +1 -1
- package/dist/cjs/index.d.ts +5 -5
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +5 -5
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/middlewares.d.ts +2 -2
- package/dist/cjs/middlewares.d.ts.map +1 -1
- package/dist/cjs/middlewares.js +24 -24
- package/dist/cjs/middlewares.js.map +1 -1
- package/dist/cjs/router.d.ts +2 -2
- package/dist/cjs/router.d.ts.map +1 -1
- package/dist/cjs/router.js +8 -8
- package/dist/cjs/router.js.map +1 -1
- package/dist/cjs/types.d.ts +1 -1
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/upload-stream.d.ts +1 -1
- package/dist/cjs/upload-stream.d.ts.map +1 -1
- package/dist/cjs/upload-stream.js +7 -7
- package/dist/cjs/upload-stream.js.map +1 -1
- package/dist/framework.d.ts +2 -4
- package/dist/framework.d.ts.map +1 -1
- package/dist/framework.js +4 -6
- package/dist/framework.js.map +1 -1
- package/dist/helpers.d.ts +2 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +1 -1
- package/dist/helpers.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/middlewares.d.ts +2 -2
- package/dist/middlewares.d.ts.map +1 -1
- package/dist/middlewares.js +24 -24
- package/dist/middlewares.js.map +1 -1
- package/dist/router.d.ts +2 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +8 -8
- package/dist/router.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/upload-stream.d.ts +1 -1
- package/dist/upload-stream.d.ts.map +1 -1
- package/dist/upload-stream.js +7 -7
- package/dist/upload-stream.js.map +1 -1
- package/package.json +8 -1
- package/src/framework.deno.ts +194 -0
- package/src/framework.ts +197 -0
- package/src/helpers.ts +829 -0
- package/src/index.ts +5 -0
- package/src/list.ts +462 -0
- package/src/middlewares.deno.ts +851 -0
- package/src/middlewares.ts +851 -0
- package/src/router.ts +993 -0
- package/src/tree.ts +139 -0
- package/src/types.ts +197 -0
- package/src/upload-stream.ts +661 -0
- package/dist/cjs/framework.deno.d.ts +0 -31
- package/dist/cjs/framework.deno.d.ts.map +0 -1
- package/dist/cjs/framework.deno.js +0 -245
- package/dist/cjs/framework.deno.js.map +0 -1
- package/dist/framework.deno.d.ts +0 -31
- package/dist/framework.deno.d.ts.map +0 -1
- package/dist/framework.deno.js +0 -245
- 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;
|