@b9g/router 0.1.9 → 0.2.0-beta.1
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/README.md +173 -108
- package/package.json +14 -7
- package/src/index.d.ts +63 -93
- package/src/index.js +281 -212
- package/src/middleware.d.ts +101 -0
- package/src/middleware.js +107 -0
package/src/index.js
CHANGED
|
@@ -6,15 +6,17 @@ import {
|
|
|
6
6
|
compilePathname
|
|
7
7
|
} from "@b9g/match-pattern";
|
|
8
8
|
import {
|
|
9
|
+
isHTTPError,
|
|
9
10
|
InternalServerError,
|
|
10
|
-
NotFound
|
|
11
|
-
isHTTPError
|
|
11
|
+
NotFound
|
|
12
12
|
} from "@b9g/http-errors";
|
|
13
|
+
import { getLogger } from "@logtape/logtape";
|
|
14
|
+
var logger = getLogger(["shovel", "router"]);
|
|
13
15
|
var RadixNode = class {
|
|
14
16
|
children;
|
|
15
17
|
// char -> RadixNode
|
|
16
|
-
|
|
17
|
-
// method ->
|
|
18
|
+
routes;
|
|
19
|
+
// method -> RouteEntry
|
|
18
20
|
paramName;
|
|
19
21
|
// param name if this is a :param segment
|
|
20
22
|
paramChild;
|
|
@@ -23,7 +25,7 @@ var RadixNode = class {
|
|
|
23
25
|
// child node for * wildcard
|
|
24
26
|
constructor() {
|
|
25
27
|
this.children = /* @__PURE__ */ new Map();
|
|
26
|
-
this.
|
|
28
|
+
this.routes = /* @__PURE__ */ new Map();
|
|
27
29
|
this.paramName = null;
|
|
28
30
|
this.paramChild = null;
|
|
29
31
|
this.wildcardChild = null;
|
|
@@ -38,22 +40,17 @@ var RadixTreeExecutor = class {
|
|
|
38
40
|
for (const route of routes) {
|
|
39
41
|
const pathname = route.pattern.pathname;
|
|
40
42
|
if (isSimplePattern(pathname)) {
|
|
41
|
-
this.#addToTree(pathname, route
|
|
43
|
+
this.#addToTree(pathname, route);
|
|
42
44
|
} else {
|
|
43
45
|
const compiled = compilePathname(pathname);
|
|
44
|
-
this.#complexRoutes.push({
|
|
45
|
-
compiled,
|
|
46
|
-
method: route.method,
|
|
47
|
-
handler: route.handler,
|
|
48
|
-
pattern: route.pattern
|
|
49
|
-
});
|
|
46
|
+
this.#complexRoutes.push({ compiled, route });
|
|
50
47
|
}
|
|
51
48
|
}
|
|
52
49
|
}
|
|
53
50
|
/**
|
|
54
51
|
* Add a simple pattern to the radix tree
|
|
55
52
|
*/
|
|
56
|
-
#addToTree(pathname,
|
|
53
|
+
#addToTree(pathname, route) {
|
|
57
54
|
let node = this.#root;
|
|
58
55
|
let i = 0;
|
|
59
56
|
while (i < pathname.length) {
|
|
@@ -84,15 +81,18 @@ var RadixTreeExecutor = class {
|
|
|
84
81
|
node = node.children.get(char);
|
|
85
82
|
i++;
|
|
86
83
|
}
|
|
87
|
-
node.
|
|
84
|
+
node.routes.set(route.method, route);
|
|
88
85
|
}
|
|
89
86
|
/**
|
|
90
|
-
* Match a pathname against the radix tree
|
|
87
|
+
* Match a pathname against the radix tree (for URL matching)
|
|
91
88
|
*/
|
|
92
|
-
#
|
|
89
|
+
#matchTreeByPath(pathname) {
|
|
93
90
|
const params = {};
|
|
94
91
|
let node = this.#root;
|
|
95
92
|
let i = 0;
|
|
93
|
+
if (!pathname) {
|
|
94
|
+
return node.routes.size > 0 ? { node, params } : null;
|
|
95
|
+
}
|
|
96
96
|
while (i < pathname.length) {
|
|
97
97
|
const char = pathname[i];
|
|
98
98
|
if (node.children.has(char)) {
|
|
@@ -121,55 +121,101 @@ var RadixTreeExecutor = class {
|
|
|
121
121
|
}
|
|
122
122
|
return null;
|
|
123
123
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
handler = node.handlers.get("GET");
|
|
124
|
+
if (node.routes.size > 0) {
|
|
125
|
+
return { node, params };
|
|
127
126
|
}
|
|
128
|
-
if (
|
|
129
|
-
|
|
127
|
+
if (node.wildcardChild && node.wildcardChild.routes.size > 0) {
|
|
128
|
+
params["0"] = "";
|
|
129
|
+
return { node: node.wildcardChild, params };
|
|
130
130
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Match a pathname against the radix tree (for request handling with method)
|
|
135
|
+
*/
|
|
136
|
+
#matchTree(pathname, method) {
|
|
137
|
+
const result = this.#matchTreeByPath(pathname);
|
|
138
|
+
if (!result) return null;
|
|
139
|
+
const { node, params } = result;
|
|
140
|
+
let entry = node.routes.get(method);
|
|
141
|
+
if (!entry && method === "HEAD") {
|
|
142
|
+
entry = node.routes.get("GET");
|
|
143
|
+
}
|
|
144
|
+
if (entry) {
|
|
145
|
+
return { entry, params };
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Match a URL against registered routes (returns RouteMatch info)
|
|
151
|
+
*/
|
|
152
|
+
matchURL(url) {
|
|
153
|
+
const urlObj = typeof url === "string" ? new URL(url, "http://localhost") : url;
|
|
154
|
+
const pathname = urlObj.pathname;
|
|
155
|
+
const treeResult = this.#matchTreeByPath(pathname);
|
|
156
|
+
if (treeResult) {
|
|
157
|
+
const { node, params } = treeResult;
|
|
158
|
+
const methods = Array.from(node.routes.keys());
|
|
159
|
+
const firstEntry = node.routes.values().next().value;
|
|
160
|
+
return {
|
|
161
|
+
params,
|
|
162
|
+
methods,
|
|
163
|
+
name: firstEntry?.name,
|
|
164
|
+
pattern: firstEntry?.pattern.pathname ?? ""
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
for (const { compiled, route } of this.#complexRoutes) {
|
|
168
|
+
const match = pathname.match(compiled.regex);
|
|
169
|
+
if (match) {
|
|
170
|
+
const params = {};
|
|
171
|
+
for (let i = 0; i < compiled.paramNames.length; i++) {
|
|
172
|
+
if (match[i + 1] !== void 0) {
|
|
173
|
+
params[compiled.paramNames[i]] = match[i + 1];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const methods = this.#complexRoutes.filter((r) => r.route.pattern.pathname === route.pattern.pathname).map((r) => r.route.method);
|
|
177
|
+
return {
|
|
178
|
+
params,
|
|
179
|
+
methods,
|
|
180
|
+
name: route.name,
|
|
181
|
+
pattern: route.pattern.pathname
|
|
182
|
+
};
|
|
139
183
|
}
|
|
140
184
|
}
|
|
141
185
|
return null;
|
|
142
186
|
}
|
|
143
187
|
/**
|
|
144
|
-
* Find the first route that matches the request
|
|
188
|
+
* Find the first route that matches the request (for handling)
|
|
145
189
|
*/
|
|
146
|
-
|
|
190
|
+
matchRequest(request) {
|
|
147
191
|
const url = new URL(request.url);
|
|
148
192
|
const method = request.method.toUpperCase();
|
|
149
193
|
const pathname = url.pathname;
|
|
150
194
|
const treeResult = this.#matchTree(pathname, method);
|
|
151
195
|
if (treeResult) {
|
|
152
196
|
return {
|
|
153
|
-
handler: treeResult.handler,
|
|
154
|
-
context: { params: treeResult.params }
|
|
197
|
+
handler: treeResult.entry.handler,
|
|
198
|
+
context: { params: treeResult.params },
|
|
199
|
+
entry: treeResult.entry
|
|
155
200
|
};
|
|
156
201
|
}
|
|
157
|
-
for (const route of this.#complexRoutes) {
|
|
202
|
+
for (const { compiled, route } of this.#complexRoutes) {
|
|
158
203
|
const methodMatches = route.method === method || method === "HEAD" && route.method === "GET";
|
|
159
204
|
if (!methodMatches) {
|
|
160
205
|
continue;
|
|
161
206
|
}
|
|
162
|
-
const match = pathname.match(
|
|
207
|
+
const match = pathname.match(compiled.regex);
|
|
163
208
|
if (match) {
|
|
164
209
|
const params = {};
|
|
165
|
-
for (let i = 0; i <
|
|
210
|
+
for (let i = 0; i < compiled.paramNames.length; i++) {
|
|
166
211
|
if (match[i + 1] !== void 0) {
|
|
167
|
-
params[
|
|
212
|
+
params[compiled.paramNames[i]] = match[i + 1];
|
|
168
213
|
}
|
|
169
214
|
}
|
|
170
215
|
return {
|
|
171
216
|
handler: route.handler,
|
|
172
|
-
context: { params }
|
|
217
|
+
context: { params },
|
|
218
|
+
entry: route
|
|
173
219
|
};
|
|
174
220
|
}
|
|
175
221
|
}
|
|
@@ -179,57 +225,110 @@ var RadixTreeExecutor = class {
|
|
|
179
225
|
var RouteBuilder = class {
|
|
180
226
|
#router;
|
|
181
227
|
#pattern;
|
|
182
|
-
|
|
228
|
+
#name;
|
|
229
|
+
#middlewares;
|
|
230
|
+
constructor(router, pattern, options) {
|
|
183
231
|
this.#router = router;
|
|
184
232
|
this.#pattern = pattern;
|
|
233
|
+
this.#name = options?.name;
|
|
234
|
+
this.#middlewares = [];
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Add route-scoped middleware that only runs when this pattern matches
|
|
238
|
+
*/
|
|
239
|
+
use(middleware) {
|
|
240
|
+
this.#middlewares.push(middleware);
|
|
241
|
+
return this;
|
|
185
242
|
}
|
|
186
243
|
/**
|
|
187
244
|
* Register a GET handler for this route pattern
|
|
188
245
|
*/
|
|
189
246
|
get(handler) {
|
|
190
|
-
this.#router.addRoute(
|
|
247
|
+
this.#router.addRoute(
|
|
248
|
+
"GET",
|
|
249
|
+
this.#pattern,
|
|
250
|
+
handler,
|
|
251
|
+
this.#name,
|
|
252
|
+
this.#middlewares
|
|
253
|
+
);
|
|
191
254
|
return this;
|
|
192
255
|
}
|
|
193
256
|
/**
|
|
194
257
|
* Register a POST handler for this route pattern
|
|
195
258
|
*/
|
|
196
259
|
post(handler) {
|
|
197
|
-
this.#router.addRoute(
|
|
260
|
+
this.#router.addRoute(
|
|
261
|
+
"POST",
|
|
262
|
+
this.#pattern,
|
|
263
|
+
handler,
|
|
264
|
+
this.#name,
|
|
265
|
+
this.#middlewares
|
|
266
|
+
);
|
|
198
267
|
return this;
|
|
199
268
|
}
|
|
200
269
|
/**
|
|
201
270
|
* Register a PUT handler for this route pattern
|
|
202
271
|
*/
|
|
203
272
|
put(handler) {
|
|
204
|
-
this.#router.addRoute(
|
|
273
|
+
this.#router.addRoute(
|
|
274
|
+
"PUT",
|
|
275
|
+
this.#pattern,
|
|
276
|
+
handler,
|
|
277
|
+
this.#name,
|
|
278
|
+
this.#middlewares
|
|
279
|
+
);
|
|
205
280
|
return this;
|
|
206
281
|
}
|
|
207
282
|
/**
|
|
208
283
|
* Register a DELETE handler for this route pattern
|
|
209
284
|
*/
|
|
210
285
|
delete(handler) {
|
|
211
|
-
this.#router.addRoute(
|
|
286
|
+
this.#router.addRoute(
|
|
287
|
+
"DELETE",
|
|
288
|
+
this.#pattern,
|
|
289
|
+
handler,
|
|
290
|
+
this.#name,
|
|
291
|
+
this.#middlewares
|
|
292
|
+
);
|
|
212
293
|
return this;
|
|
213
294
|
}
|
|
214
295
|
/**
|
|
215
296
|
* Register a PATCH handler for this route pattern
|
|
216
297
|
*/
|
|
217
298
|
patch(handler) {
|
|
218
|
-
this.#router.addRoute(
|
|
299
|
+
this.#router.addRoute(
|
|
300
|
+
"PATCH",
|
|
301
|
+
this.#pattern,
|
|
302
|
+
handler,
|
|
303
|
+
this.#name,
|
|
304
|
+
this.#middlewares
|
|
305
|
+
);
|
|
219
306
|
return this;
|
|
220
307
|
}
|
|
221
308
|
/**
|
|
222
309
|
* Register a HEAD handler for this route pattern
|
|
223
310
|
*/
|
|
224
311
|
head(handler) {
|
|
225
|
-
this.#router.addRoute(
|
|
312
|
+
this.#router.addRoute(
|
|
313
|
+
"HEAD",
|
|
314
|
+
this.#pattern,
|
|
315
|
+
handler,
|
|
316
|
+
this.#name,
|
|
317
|
+
this.#middlewares
|
|
318
|
+
);
|
|
226
319
|
return this;
|
|
227
320
|
}
|
|
228
321
|
/**
|
|
229
322
|
* Register an OPTIONS handler for this route pattern
|
|
230
323
|
*/
|
|
231
324
|
options(handler) {
|
|
232
|
-
this.#router.addRoute(
|
|
325
|
+
this.#router.addRoute(
|
|
326
|
+
"OPTIONS",
|
|
327
|
+
this.#pattern,
|
|
328
|
+
handler,
|
|
329
|
+
this.#name,
|
|
330
|
+
this.#middlewares
|
|
331
|
+
);
|
|
233
332
|
return this;
|
|
234
333
|
}
|
|
235
334
|
/**
|
|
@@ -246,51 +345,34 @@ var RouteBuilder = class {
|
|
|
246
345
|
"OPTIONS"
|
|
247
346
|
];
|
|
248
347
|
methods.forEach((method) => {
|
|
249
|
-
this.#router.addRoute(
|
|
348
|
+
this.#router.addRoute(
|
|
349
|
+
method,
|
|
350
|
+
this.#pattern,
|
|
351
|
+
handler,
|
|
352
|
+
this.#name,
|
|
353
|
+
this.#middlewares
|
|
354
|
+
);
|
|
250
355
|
});
|
|
251
356
|
return this;
|
|
252
357
|
}
|
|
253
358
|
};
|
|
254
359
|
var Router = class {
|
|
255
|
-
|
|
256
|
-
|
|
360
|
+
routes;
|
|
361
|
+
middlewares;
|
|
257
362
|
#executor;
|
|
258
|
-
#dirty;
|
|
259
363
|
constructor() {
|
|
260
|
-
this
|
|
261
|
-
this
|
|
364
|
+
this.routes = [];
|
|
365
|
+
this.middlewares = [];
|
|
262
366
|
this.#executor = null;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return await this.#executeMiddlewareStack(
|
|
273
|
-
this.#middlewares,
|
|
274
|
-
request,
|
|
275
|
-
matchResult.context,
|
|
276
|
-
matchResult.handler
|
|
277
|
-
);
|
|
278
|
-
} else {
|
|
279
|
-
const notFoundHandler = async () => {
|
|
280
|
-
throw new NotFound();
|
|
281
|
-
};
|
|
282
|
-
return await this.#executeMiddlewareStack(
|
|
283
|
-
this.#middlewares,
|
|
284
|
-
request,
|
|
285
|
-
{ params: {} },
|
|
286
|
-
notFoundHandler
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
} catch (error) {
|
|
290
|
-
return this.#createErrorResponse(error);
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
this.handler = this.#handlerImpl;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Ensure the executor is compiled and up to date
|
|
370
|
+
*/
|
|
371
|
+
#ensureCompiled() {
|
|
372
|
+
if (!this.#executor) {
|
|
373
|
+
this.#executor = new RadixTreeExecutor(this.routes);
|
|
374
|
+
}
|
|
375
|
+
return this.#executor;
|
|
294
376
|
}
|
|
295
377
|
use(pathPrefixOrMiddleware, maybeMiddleware) {
|
|
296
378
|
if (typeof pathPrefixOrMiddleware === "string") {
|
|
@@ -300,7 +382,7 @@ var Router = class {
|
|
|
300
382
|
"Invalid middleware type. Must be function or async generator function."
|
|
301
383
|
);
|
|
302
384
|
}
|
|
303
|
-
this
|
|
385
|
+
this.middlewares.push({
|
|
304
386
|
middleware,
|
|
305
387
|
pathPrefix: pathPrefixOrMiddleware
|
|
306
388
|
});
|
|
@@ -310,95 +392,89 @@ var Router = class {
|
|
|
310
392
|
"Invalid middleware type. Must be function or async generator function."
|
|
311
393
|
);
|
|
312
394
|
}
|
|
313
|
-
this
|
|
395
|
+
this.middlewares.push({ middleware: pathPrefixOrMiddleware });
|
|
314
396
|
}
|
|
315
|
-
this.#
|
|
397
|
+
this.#executor = null;
|
|
316
398
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
399
|
+
/**
|
|
400
|
+
* Create a route builder for the given pattern
|
|
401
|
+
* Returns a chainable interface for registering HTTP method handlers
|
|
402
|
+
*
|
|
403
|
+
* Example:
|
|
404
|
+
* router.route('/api/users/:id', { name: 'user' })
|
|
405
|
+
* .use(authMiddleware)
|
|
406
|
+
* .get(getUserHandler)
|
|
407
|
+
* .put(updateUserHandler);
|
|
408
|
+
*/
|
|
409
|
+
route(pattern, options) {
|
|
410
|
+
return new RouteBuilder(this, pattern, options);
|
|
323
411
|
}
|
|
324
412
|
/**
|
|
325
413
|
* Internal method called by RouteBuilder to register routes
|
|
326
414
|
* Public for RouteBuilder access, but not intended for direct use
|
|
327
415
|
*/
|
|
328
|
-
addRoute(method, pattern, handler) {
|
|
416
|
+
addRoute(method, pattern, handler, name, middlewares = []) {
|
|
329
417
|
const matchPattern = new MatchPattern(pattern);
|
|
330
|
-
this
|
|
418
|
+
this.routes.push({
|
|
331
419
|
pattern: matchPattern,
|
|
332
420
|
method: method.toUpperCase(),
|
|
333
|
-
handler
|
|
421
|
+
handler,
|
|
422
|
+
name,
|
|
423
|
+
middlewares
|
|
334
424
|
});
|
|
335
|
-
this.#
|
|
425
|
+
this.#executor = null;
|
|
336
426
|
}
|
|
337
427
|
/**
|
|
338
|
-
*
|
|
339
|
-
* Returns
|
|
428
|
+
* Match a URL against registered routes
|
|
429
|
+
* Returns route info (params, methods, name, pattern) or null if no match
|
|
430
|
+
* Does not execute handlers - use handle() for that
|
|
340
431
|
*/
|
|
341
|
-
|
|
342
|
-
|
|
432
|
+
match(url) {
|
|
433
|
+
const executor = this.#ensureCompiled();
|
|
434
|
+
return executor.matchURL(url);
|
|
435
|
+
}
|
|
343
436
|
/**
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* Note: Global middleware executes even if no route matches
|
|
437
|
+
* Handle a request - main entrypoint for ServiceWorker usage
|
|
438
|
+
* Executes the matched handler with middleware chain
|
|
347
439
|
*/
|
|
348
|
-
async
|
|
349
|
-
|
|
350
|
-
this.#executor = new RadixTreeExecutor(this.#routes);
|
|
351
|
-
this.#dirty = false;
|
|
352
|
-
}
|
|
353
|
-
let matchResult = this.#executor.match(request);
|
|
354
|
-
let handler;
|
|
355
|
-
let context;
|
|
356
|
-
if (matchResult) {
|
|
357
|
-
handler = matchResult.handler;
|
|
358
|
-
context = matchResult.context;
|
|
359
|
-
} else {
|
|
360
|
-
handler = async () => {
|
|
361
|
-
throw new NotFound();
|
|
362
|
-
};
|
|
363
|
-
context = { params: {} };
|
|
364
|
-
}
|
|
365
|
-
let response;
|
|
440
|
+
async handle(request) {
|
|
441
|
+
const executor = this.#ensureCompiled();
|
|
366
442
|
try {
|
|
367
|
-
|
|
368
|
-
|
|
443
|
+
const matchResult = executor.matchRequest(request);
|
|
444
|
+
let handler;
|
|
445
|
+
let context;
|
|
446
|
+
let routeMiddleware = [];
|
|
447
|
+
if (matchResult) {
|
|
448
|
+
if (!matchResult.handler) {
|
|
449
|
+
throw new NotFound("Route has no handler");
|
|
450
|
+
}
|
|
451
|
+
handler = matchResult.handler;
|
|
452
|
+
context = matchResult.context;
|
|
453
|
+
routeMiddleware = matchResult.entry.middlewares;
|
|
454
|
+
} else {
|
|
455
|
+
handler = async () => {
|
|
456
|
+
throw new NotFound();
|
|
457
|
+
};
|
|
458
|
+
context = { params: {} };
|
|
459
|
+
}
|
|
460
|
+
let response = await this.#executeMiddlewareStack(
|
|
461
|
+
this.middlewares,
|
|
462
|
+
routeMiddleware,
|
|
369
463
|
request,
|
|
370
464
|
context,
|
|
371
465
|
handler
|
|
372
466
|
);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
467
|
+
if (request.method.toUpperCase() === "HEAD") {
|
|
468
|
+
response = new Response(null, {
|
|
469
|
+
status: response.status,
|
|
470
|
+
statusText: response.statusText,
|
|
471
|
+
headers: response.headers
|
|
472
|
+
});
|
|
376
473
|
}
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
if (response && request.method.toUpperCase() === "HEAD") {
|
|
383
|
-
response = new Response(null, {
|
|
384
|
-
status: response.status,
|
|
385
|
-
statusText: response.statusText,
|
|
386
|
-
headers: response.headers
|
|
387
|
-
});
|
|
474
|
+
return response;
|
|
475
|
+
} catch (error) {
|
|
476
|
+
return this.#createErrorResponse(error);
|
|
388
477
|
}
|
|
389
|
-
return response;
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Get registered routes for debugging/introspection
|
|
393
|
-
*/
|
|
394
|
-
getRoutes() {
|
|
395
|
-
return [...this.#routes];
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Get registered middleware for debugging/introspection
|
|
399
|
-
*/
|
|
400
|
-
getMiddlewares() {
|
|
401
|
-
return [...this.#middlewares];
|
|
402
478
|
}
|
|
403
479
|
/**
|
|
404
480
|
* Mount a subrouter at a specific path prefix
|
|
@@ -415,19 +491,21 @@ var Router = class {
|
|
|
415
491
|
*/
|
|
416
492
|
mount(mountPath, subrouter) {
|
|
417
493
|
const normalizedMountPath = this.#normalizeMountPath(mountPath);
|
|
418
|
-
const subroutes = subrouter.
|
|
494
|
+
const subroutes = subrouter.routes;
|
|
419
495
|
for (const subroute of subroutes) {
|
|
420
496
|
const mountedPattern = this.#combinePaths(
|
|
421
497
|
normalizedMountPath,
|
|
422
498
|
subroute.pattern.pathname
|
|
423
499
|
);
|
|
424
|
-
this
|
|
500
|
+
this.routes.push({
|
|
425
501
|
pattern: new MatchPattern(mountedPattern),
|
|
426
502
|
method: subroute.method,
|
|
427
|
-
handler: subroute.handler
|
|
503
|
+
handler: subroute.handler,
|
|
504
|
+
name: subroute.name,
|
|
505
|
+
middlewares: subroute.middlewares
|
|
428
506
|
});
|
|
429
507
|
}
|
|
430
|
-
const submiddlewares = subrouter.
|
|
508
|
+
const submiddlewares = subrouter.middlewares;
|
|
431
509
|
for (const submiddleware of submiddlewares) {
|
|
432
510
|
let composedPrefix;
|
|
433
511
|
if (submiddleware.pathPrefix) {
|
|
@@ -438,12 +516,12 @@ var Router = class {
|
|
|
438
516
|
} else {
|
|
439
517
|
composedPrefix = normalizedMountPath;
|
|
440
518
|
}
|
|
441
|
-
this
|
|
519
|
+
this.middlewares.push({
|
|
442
520
|
middleware: submiddleware.middleware,
|
|
443
521
|
pathPrefix: composedPrefix
|
|
444
522
|
});
|
|
445
523
|
}
|
|
446
|
-
this.#
|
|
524
|
+
this.#executor = null;
|
|
447
525
|
}
|
|
448
526
|
/**
|
|
449
527
|
* Normalize mount path: ensure it starts with / and doesn't end with /
|
|
@@ -498,39 +576,58 @@ var Router = class {
|
|
|
498
576
|
}
|
|
499
577
|
return false;
|
|
500
578
|
}
|
|
579
|
+
/**
|
|
580
|
+
* Execute a single middleware and track generator state
|
|
581
|
+
* Returns true if middleware short-circuited (returned Response early)
|
|
582
|
+
*/
|
|
583
|
+
async #executeMiddleware(middleware, request, context, runningGenerators) {
|
|
584
|
+
if (this.#isGeneratorMiddleware(middleware)) {
|
|
585
|
+
const generator = middleware(request, context);
|
|
586
|
+
const result = await generator.next();
|
|
587
|
+
if (result.done) {
|
|
588
|
+
if (result.value) {
|
|
589
|
+
return result.value;
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
runningGenerators.push({ generator });
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
const result = await middleware(request, context);
|
|
596
|
+
if (result) {
|
|
597
|
+
return result;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
501
602
|
/**
|
|
502
603
|
* Execute middleware stack with guaranteed execution using Rack-style LIFO order
|
|
604
|
+
* Global/path middleware runs first, then route-scoped middleware, then handler
|
|
503
605
|
*/
|
|
504
|
-
async #executeMiddlewareStack(
|
|
606
|
+
async #executeMiddlewareStack(globalMiddlewares, routeMiddlewares, request, context, handler) {
|
|
505
607
|
const runningGenerators = [];
|
|
506
608
|
let currentResponse = null;
|
|
507
609
|
const requestPathname = new URL(request.url).pathname;
|
|
508
|
-
for (
|
|
509
|
-
const entry = middlewares[i];
|
|
510
|
-
const middleware = entry.middleware;
|
|
610
|
+
for (const entry of globalMiddlewares) {
|
|
511
611
|
if (entry.pathPrefix && !this.#matchesPathPrefix(requestPathname, entry.pathPrefix)) {
|
|
512
612
|
continue;
|
|
513
613
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const result = await middleware(
|
|
614
|
+
currentResponse = await this.#executeMiddleware(
|
|
615
|
+
entry.middleware,
|
|
616
|
+
request,
|
|
617
|
+
context,
|
|
618
|
+
runningGenerators
|
|
619
|
+
);
|
|
620
|
+
if (currentResponse) break;
|
|
621
|
+
}
|
|
622
|
+
if (!currentResponse) {
|
|
623
|
+
for (const middleware of routeMiddlewares) {
|
|
624
|
+
currentResponse = await this.#executeMiddleware(
|
|
625
|
+
middleware,
|
|
527
626
|
request,
|
|
528
|
-
context
|
|
627
|
+
context,
|
|
628
|
+
runningGenerators
|
|
529
629
|
);
|
|
530
|
-
if (
|
|
531
|
-
currentResponse = result;
|
|
532
|
-
break;
|
|
533
|
-
}
|
|
630
|
+
if (currentResponse) break;
|
|
534
631
|
}
|
|
535
632
|
}
|
|
536
633
|
if (!currentResponse) {
|
|
@@ -575,48 +672,20 @@ var Router = class {
|
|
|
575
672
|
}
|
|
576
673
|
throw error;
|
|
577
674
|
}
|
|
578
|
-
/**
|
|
579
|
-
* Get route statistics
|
|
580
|
-
*/
|
|
581
|
-
getStats() {
|
|
582
|
-
return {
|
|
583
|
-
routeCount: this.#routes.length,
|
|
584
|
-
middlewareCount: this.#middlewares.length,
|
|
585
|
-
compiled: !this.#dirty && this.#executor !== null
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
675
|
/**
|
|
589
676
|
* Create an error response for unhandled errors
|
|
590
677
|
* Uses HTTPError.toResponse() for consistent error formatting
|
|
591
678
|
*/
|
|
592
679
|
#createErrorResponse(error) {
|
|
593
|
-
const httpError = isHTTPError(error) ? error : new InternalServerError(error.message, { cause: error });
|
|
594
680
|
const isDev = import.meta.env?.MODE !== "production";
|
|
681
|
+
if (isDev && !isHTTPError(error)) {
|
|
682
|
+
logger.error`Unhandled error: ${error}`;
|
|
683
|
+
}
|
|
684
|
+
const httpError = isHTTPError(error) ? error : new InternalServerError(error.message, { cause: error });
|
|
595
685
|
return httpError.toResponse(isDev);
|
|
596
686
|
}
|
|
597
687
|
};
|
|
598
|
-
function trailingSlash(mode) {
|
|
599
|
-
return (request, _context) => {
|
|
600
|
-
const url = new URL(request.url);
|
|
601
|
-
const pathname = url.pathname;
|
|
602
|
-
if (pathname === "/")
|
|
603
|
-
return;
|
|
604
|
-
let newPathname = null;
|
|
605
|
-
if (mode === "strip" && pathname.endsWith("/")) {
|
|
606
|
-
newPathname = pathname.slice(0, -1);
|
|
607
|
-
} else if (mode === "add" && !pathname.endsWith("/")) {
|
|
608
|
-
newPathname = pathname + "/";
|
|
609
|
-
}
|
|
610
|
-
if (newPathname) {
|
|
611
|
-
url.pathname = newPathname;
|
|
612
|
-
return new Response(null, {
|
|
613
|
-
status: 301,
|
|
614
|
-
headers: { Location: url.toString() }
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
688
|
export {
|
|
620
|
-
|
|
621
|
-
|
|
689
|
+
RouteBuilder,
|
|
690
|
+
Router
|
|
622
691
|
};
|