@b9g/router 0.1.5 → 0.1.7
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 +130 -73
- package/package.json +13 -10
- package/src/index.d.ts +185 -5
- package/src/index.js +667 -1
- package/src/router.d.ts +0 -167
- package/src/router.js +0 -504
package/src/index.js
CHANGED
|
@@ -1,6 +1,672 @@
|
|
|
1
1
|
/// <reference types="./index.d.ts" />
|
|
2
2
|
// src/index.ts
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
MatchPattern,
|
|
5
|
+
isSimplePattern,
|
|
6
|
+
compilePathname
|
|
7
|
+
} from "@b9g/match-pattern";
|
|
8
|
+
import { InternalServerError, isHTTPError } from "@b9g/http-errors";
|
|
9
|
+
var RadixNode = class {
|
|
10
|
+
children;
|
|
11
|
+
// char -> RadixNode
|
|
12
|
+
handlers;
|
|
13
|
+
// method -> handler
|
|
14
|
+
paramName;
|
|
15
|
+
// param name if this is a :param segment
|
|
16
|
+
paramChild;
|
|
17
|
+
// child node for :param
|
|
18
|
+
wildcardChild;
|
|
19
|
+
// child node for * wildcard
|
|
20
|
+
constructor() {
|
|
21
|
+
this.children = /* @__PURE__ */ new Map();
|
|
22
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
23
|
+
this.paramName = null;
|
|
24
|
+
this.paramChild = null;
|
|
25
|
+
this.wildcardChild = null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var RadixTreeExecutor = class {
|
|
29
|
+
#root;
|
|
30
|
+
#complexRoutes;
|
|
31
|
+
constructor(routes) {
|
|
32
|
+
this.#root = new RadixNode();
|
|
33
|
+
this.#complexRoutes = [];
|
|
34
|
+
for (const route of routes) {
|
|
35
|
+
const pathname = route.pattern.pathname;
|
|
36
|
+
if (isSimplePattern(pathname)) {
|
|
37
|
+
this.#addToTree(pathname, route.method, route.handler);
|
|
38
|
+
} else {
|
|
39
|
+
const compiled = compilePathname(pathname);
|
|
40
|
+
this.#complexRoutes.push({
|
|
41
|
+
compiled,
|
|
42
|
+
method: route.method,
|
|
43
|
+
handler: route.handler,
|
|
44
|
+
pattern: route.pattern
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Add a simple pattern to the radix tree
|
|
51
|
+
*/
|
|
52
|
+
#addToTree(pathname, method, handler) {
|
|
53
|
+
let node = this.#root;
|
|
54
|
+
let i = 0;
|
|
55
|
+
while (i < pathname.length) {
|
|
56
|
+
const char = pathname[i];
|
|
57
|
+
if (char === ":") {
|
|
58
|
+
const match = pathname.slice(i).match(/^:(\w+)/);
|
|
59
|
+
if (match) {
|
|
60
|
+
const paramName = match[1];
|
|
61
|
+
if (!node.paramChild) {
|
|
62
|
+
node.paramChild = new RadixNode();
|
|
63
|
+
node.paramChild.paramName = paramName;
|
|
64
|
+
}
|
|
65
|
+
node = node.paramChild;
|
|
66
|
+
i += match[0].length;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (char === "*") {
|
|
71
|
+
if (!node.wildcardChild) {
|
|
72
|
+
node.wildcardChild = new RadixNode();
|
|
73
|
+
}
|
|
74
|
+
node = node.wildcardChild;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
if (!node.children.has(char)) {
|
|
78
|
+
node.children.set(char, new RadixNode());
|
|
79
|
+
}
|
|
80
|
+
node = node.children.get(char);
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
node.handlers.set(method, handler);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Match a pathname against the radix tree
|
|
87
|
+
*/
|
|
88
|
+
#matchTree(pathname, method) {
|
|
89
|
+
const params = {};
|
|
90
|
+
let node = this.#root;
|
|
91
|
+
let i = 0;
|
|
92
|
+
while (i < pathname.length) {
|
|
93
|
+
const char = pathname[i];
|
|
94
|
+
if (node.children.has(char)) {
|
|
95
|
+
node = node.children.get(char);
|
|
96
|
+
i++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (node.paramChild) {
|
|
100
|
+
let j = i;
|
|
101
|
+
while (j < pathname.length && pathname[j] !== "/") {
|
|
102
|
+
j++;
|
|
103
|
+
}
|
|
104
|
+
const value = pathname.slice(i, j);
|
|
105
|
+
if (value) {
|
|
106
|
+
params[node.paramChild.paramName] = value;
|
|
107
|
+
node = node.paramChild;
|
|
108
|
+
i = j;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (node.wildcardChild) {
|
|
113
|
+
const rest = pathname.slice(i);
|
|
114
|
+
params["0"] = rest;
|
|
115
|
+
node = node.wildcardChild;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
let handler = node.handlers.get(method);
|
|
121
|
+
if (!handler && method === "HEAD") {
|
|
122
|
+
handler = node.handlers.get("GET");
|
|
123
|
+
}
|
|
124
|
+
if (handler) {
|
|
125
|
+
return { handler, params };
|
|
126
|
+
}
|
|
127
|
+
if (node.wildcardChild) {
|
|
128
|
+
let wildcardHandler = node.wildcardChild.handlers.get(method);
|
|
129
|
+
if (!wildcardHandler && method === "HEAD") {
|
|
130
|
+
wildcardHandler = node.wildcardChild.handlers.get("GET");
|
|
131
|
+
}
|
|
132
|
+
if (wildcardHandler) {
|
|
133
|
+
params["0"] = "";
|
|
134
|
+
return { handler: wildcardHandler, params };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Find the first route that matches the request
|
|
141
|
+
*/
|
|
142
|
+
match(request) {
|
|
143
|
+
const url = new URL(request.url);
|
|
144
|
+
const method = request.method.toUpperCase();
|
|
145
|
+
const pathname = url.pathname;
|
|
146
|
+
const treeResult = this.#matchTree(pathname, method);
|
|
147
|
+
if (treeResult) {
|
|
148
|
+
return {
|
|
149
|
+
handler: treeResult.handler,
|
|
150
|
+
context: { params: treeResult.params }
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
for (const route of this.#complexRoutes) {
|
|
154
|
+
const methodMatches = route.method === method || method === "HEAD" && route.method === "GET";
|
|
155
|
+
if (!methodMatches) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const match = pathname.match(route.compiled.regex);
|
|
159
|
+
if (match) {
|
|
160
|
+
const params = {};
|
|
161
|
+
for (let i = 0; i < route.compiled.paramNames.length; i++) {
|
|
162
|
+
if (match[i + 1] !== void 0) {
|
|
163
|
+
params[route.compiled.paramNames[i]] = match[i + 1];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
handler: route.handler,
|
|
168
|
+
context: { params }
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var RouteBuilder = class {
|
|
176
|
+
#router;
|
|
177
|
+
#pattern;
|
|
178
|
+
constructor(router, pattern) {
|
|
179
|
+
this.#router = router;
|
|
180
|
+
this.#pattern = pattern;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Register a GET handler for this route pattern
|
|
184
|
+
*/
|
|
185
|
+
get(handler) {
|
|
186
|
+
this.#router.addRoute("GET", this.#pattern, handler);
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Register a POST handler for this route pattern
|
|
191
|
+
*/
|
|
192
|
+
post(handler) {
|
|
193
|
+
this.#router.addRoute("POST", this.#pattern, handler);
|
|
194
|
+
return this;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Register a PUT handler for this route pattern
|
|
198
|
+
*/
|
|
199
|
+
put(handler) {
|
|
200
|
+
this.#router.addRoute("PUT", this.#pattern, handler);
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Register a DELETE handler for this route pattern
|
|
205
|
+
*/
|
|
206
|
+
delete(handler) {
|
|
207
|
+
this.#router.addRoute("DELETE", this.#pattern, handler);
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Register a PATCH handler for this route pattern
|
|
212
|
+
*/
|
|
213
|
+
patch(handler) {
|
|
214
|
+
this.#router.addRoute("PATCH", this.#pattern, handler);
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Register a HEAD handler for this route pattern
|
|
219
|
+
*/
|
|
220
|
+
head(handler) {
|
|
221
|
+
this.#router.addRoute("HEAD", this.#pattern, handler);
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Register an OPTIONS handler for this route pattern
|
|
226
|
+
*/
|
|
227
|
+
options(handler) {
|
|
228
|
+
this.#router.addRoute("OPTIONS", this.#pattern, handler);
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Register a handler for all HTTP methods on this route pattern
|
|
233
|
+
*/
|
|
234
|
+
all(handler) {
|
|
235
|
+
const methods = [
|
|
236
|
+
"GET",
|
|
237
|
+
"POST",
|
|
238
|
+
"PUT",
|
|
239
|
+
"DELETE",
|
|
240
|
+
"PATCH",
|
|
241
|
+
"HEAD",
|
|
242
|
+
"OPTIONS"
|
|
243
|
+
];
|
|
244
|
+
methods.forEach((method) => {
|
|
245
|
+
this.#router.addRoute(method, this.#pattern, handler);
|
|
246
|
+
});
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
var Router = class {
|
|
251
|
+
#routes;
|
|
252
|
+
#middlewares;
|
|
253
|
+
#executor;
|
|
254
|
+
#dirty;
|
|
255
|
+
constructor() {
|
|
256
|
+
this.#routes = [];
|
|
257
|
+
this.#middlewares = [];
|
|
258
|
+
this.#executor = null;
|
|
259
|
+
this.#dirty = false;
|
|
260
|
+
this.#handlerImpl = async (request) => {
|
|
261
|
+
try {
|
|
262
|
+
if (this.#dirty || !this.#executor) {
|
|
263
|
+
this.#executor = new RadixTreeExecutor(this.#routes);
|
|
264
|
+
this.#dirty = false;
|
|
265
|
+
}
|
|
266
|
+
const matchResult = this.#executor.match(request);
|
|
267
|
+
if (matchResult) {
|
|
268
|
+
const mutableRequest = this.#createMutableRequest(request);
|
|
269
|
+
return await this.#executeMiddlewareStack(
|
|
270
|
+
this.#middlewares,
|
|
271
|
+
mutableRequest,
|
|
272
|
+
matchResult.context,
|
|
273
|
+
matchResult.handler,
|
|
274
|
+
request.url,
|
|
275
|
+
this.#executor
|
|
276
|
+
);
|
|
277
|
+
} else {
|
|
278
|
+
const notFoundHandler = async () => {
|
|
279
|
+
return new Response("Not Found", { status: 404 });
|
|
280
|
+
};
|
|
281
|
+
const mutableRequest = this.#createMutableRequest(request);
|
|
282
|
+
return await this.#executeMiddlewareStack(
|
|
283
|
+
this.#middlewares,
|
|
284
|
+
mutableRequest,
|
|
285
|
+
{ params: {} },
|
|
286
|
+
notFoundHandler,
|
|
287
|
+
request.url,
|
|
288
|
+
this.#executor
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
return this.#createErrorResponse(error);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
this.handler = this.#handlerImpl;
|
|
296
|
+
}
|
|
297
|
+
use(pathPrefixOrMiddleware, maybeMiddleware) {
|
|
298
|
+
if (typeof pathPrefixOrMiddleware === "string") {
|
|
299
|
+
const middleware = maybeMiddleware;
|
|
300
|
+
if (!this.#isValidMiddleware(middleware)) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
"Invalid middleware type. Must be function or async generator function."
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
this.#middlewares.push({
|
|
306
|
+
middleware,
|
|
307
|
+
pathPrefix: pathPrefixOrMiddleware
|
|
308
|
+
});
|
|
309
|
+
} else {
|
|
310
|
+
if (!this.#isValidMiddleware(pathPrefixOrMiddleware)) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
"Invalid middleware type. Must be function or async generator function."
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
this.#middlewares.push({ middleware: pathPrefixOrMiddleware });
|
|
316
|
+
}
|
|
317
|
+
this.#dirty = true;
|
|
318
|
+
}
|
|
319
|
+
route(patternOrConfig) {
|
|
320
|
+
if (typeof patternOrConfig === "string") {
|
|
321
|
+
return new RouteBuilder(this, patternOrConfig);
|
|
322
|
+
} else {
|
|
323
|
+
return new RouteBuilder(this, patternOrConfig.pattern);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Internal method called by RouteBuilder to register routes
|
|
328
|
+
* Public for RouteBuilder access, but not intended for direct use
|
|
329
|
+
*/
|
|
330
|
+
addRoute(method, pattern, handler) {
|
|
331
|
+
const matchPattern = new MatchPattern(pattern);
|
|
332
|
+
this.#routes.push({
|
|
333
|
+
pattern: matchPattern,
|
|
334
|
+
method: method.toUpperCase(),
|
|
335
|
+
handler
|
|
336
|
+
});
|
|
337
|
+
this.#dirty = true;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Handle a request - main entrypoint for ServiceWorker usage
|
|
341
|
+
* Returns a response or throws if no route matches
|
|
342
|
+
*/
|
|
343
|
+
handler;
|
|
344
|
+
#handlerImpl;
|
|
345
|
+
/**
|
|
346
|
+
* Match a request against registered routes and execute the handler chain
|
|
347
|
+
* Returns the response from the matched handler, or null if no route matches
|
|
348
|
+
* Note: Global middleware executes even if no route matches
|
|
349
|
+
*/
|
|
350
|
+
async match(request) {
|
|
351
|
+
if (this.#dirty || !this.#executor) {
|
|
352
|
+
this.#executor = new RadixTreeExecutor(this.#routes);
|
|
353
|
+
this.#dirty = false;
|
|
354
|
+
}
|
|
355
|
+
const mutableRequest = this.#createMutableRequest(request);
|
|
356
|
+
const originalURL = mutableRequest.url;
|
|
357
|
+
let matchResult = this.#executor.match(request);
|
|
358
|
+
let handler;
|
|
359
|
+
let context;
|
|
360
|
+
if (matchResult) {
|
|
361
|
+
handler = matchResult.handler;
|
|
362
|
+
context = matchResult.context;
|
|
363
|
+
} else {
|
|
364
|
+
handler = async () => new Response("Not Found", { status: 404 });
|
|
365
|
+
context = { params: {} };
|
|
366
|
+
}
|
|
367
|
+
let response = await this.#executeMiddlewareStack(
|
|
368
|
+
this.#middlewares,
|
|
369
|
+
mutableRequest,
|
|
370
|
+
context,
|
|
371
|
+
handler,
|
|
372
|
+
originalURL,
|
|
373
|
+
this.#executor
|
|
374
|
+
// Pass executor for re-routing
|
|
375
|
+
);
|
|
376
|
+
if (!matchResult && response?.status === 404) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
if (response && request.method.toUpperCase() === "HEAD") {
|
|
380
|
+
response = new Response(null, {
|
|
381
|
+
status: response.status,
|
|
382
|
+
statusText: response.statusText,
|
|
383
|
+
headers: response.headers
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return response;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Get registered routes for debugging/introspection
|
|
390
|
+
*/
|
|
391
|
+
getRoutes() {
|
|
392
|
+
return [...this.#routes];
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Get registered middleware for debugging/introspection
|
|
396
|
+
*/
|
|
397
|
+
getMiddlewares() {
|
|
398
|
+
return [...this.#middlewares];
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Mount a subrouter at a specific path prefix
|
|
402
|
+
* All routes from the subrouter will be prefixed with the mount path
|
|
403
|
+
*
|
|
404
|
+
* Example:
|
|
405
|
+
* const apiRouter = new Router();
|
|
406
|
+
* apiRouter.route('/users').get(getUsersHandler);
|
|
407
|
+
* apiRouter.route('/users/:id').get(getUserHandler);
|
|
408
|
+
*
|
|
409
|
+
* const mainRouter = new Router();
|
|
410
|
+
* mainRouter.mount('/api/v1', apiRouter);
|
|
411
|
+
* // Routes become: /api/v1/users, /api/v1/users/:id
|
|
412
|
+
*/
|
|
413
|
+
mount(mountPath, subrouter) {
|
|
414
|
+
const normalizedMountPath = this.#normalizeMountPath(mountPath);
|
|
415
|
+
const subroutes = subrouter.getRoutes();
|
|
416
|
+
for (const subroute of subroutes) {
|
|
417
|
+
const mountedPattern = this.#combinePaths(
|
|
418
|
+
normalizedMountPath,
|
|
419
|
+
subroute.pattern.pathname
|
|
420
|
+
);
|
|
421
|
+
this.#routes.push({
|
|
422
|
+
pattern: new MatchPattern(mountedPattern),
|
|
423
|
+
method: subroute.method,
|
|
424
|
+
handler: subroute.handler
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
const submiddlewares = subrouter.getMiddlewares();
|
|
428
|
+
for (const submiddleware of submiddlewares) {
|
|
429
|
+
let composedPrefix;
|
|
430
|
+
if (submiddleware.pathPrefix) {
|
|
431
|
+
composedPrefix = this.#combinePaths(
|
|
432
|
+
normalizedMountPath,
|
|
433
|
+
submiddleware.pathPrefix
|
|
434
|
+
);
|
|
435
|
+
} else {
|
|
436
|
+
composedPrefix = normalizedMountPath;
|
|
437
|
+
}
|
|
438
|
+
this.#middlewares.push({
|
|
439
|
+
middleware: submiddleware.middleware,
|
|
440
|
+
pathPrefix: composedPrefix
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
this.#dirty = true;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Normalize mount path: ensure it starts with / and doesn't end with /
|
|
447
|
+
*/
|
|
448
|
+
#normalizeMountPath(mountPath) {
|
|
449
|
+
if (!mountPath.startsWith("/")) {
|
|
450
|
+
mountPath = "/" + mountPath;
|
|
451
|
+
}
|
|
452
|
+
if (mountPath.endsWith("/") && mountPath.length > 1) {
|
|
453
|
+
mountPath = mountPath.slice(0, -1);
|
|
454
|
+
}
|
|
455
|
+
return mountPath;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Combine mount path with route pattern
|
|
459
|
+
*/
|
|
460
|
+
#combinePaths(mountPath, routePattern) {
|
|
461
|
+
if (routePattern === "/") {
|
|
462
|
+
return mountPath;
|
|
463
|
+
}
|
|
464
|
+
if (!routePattern.startsWith("/")) {
|
|
465
|
+
routePattern = "/" + routePattern;
|
|
466
|
+
}
|
|
467
|
+
return mountPath + routePattern;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Validate that a function is valid middleware
|
|
471
|
+
*/
|
|
472
|
+
#isValidMiddleware(middleware) {
|
|
473
|
+
const constructorName = middleware.constructor.name;
|
|
474
|
+
return constructorName === "AsyncGeneratorFunction" || constructorName === "GeneratorFunction" || constructorName === "AsyncFunction" || constructorName === "Function";
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Detect if a function is a generator middleware
|
|
478
|
+
*/
|
|
479
|
+
#isGeneratorMiddleware(middleware) {
|
|
480
|
+
const name = middleware.constructor.name;
|
|
481
|
+
return name === "GeneratorFunction" || name === "AsyncGeneratorFunction";
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Check if a request pathname matches a middleware's path prefix
|
|
485
|
+
* Matches on segment boundaries: /admin matches /admin, /admin/, /admin/users
|
|
486
|
+
* but NOT /administrator
|
|
487
|
+
*/
|
|
488
|
+
#matchesPathPrefix(pathname, pathPrefix) {
|
|
489
|
+
if (pathname === pathPrefix) {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
if (pathname.startsWith(pathPrefix)) {
|
|
493
|
+
const nextChar = pathname[pathPrefix.length];
|
|
494
|
+
return nextChar === "/" || nextChar === void 0;
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Execute middleware stack with guaranteed execution using Rack-style LIFO order
|
|
500
|
+
*/
|
|
501
|
+
async #executeMiddlewareStack(middlewares, request, context, handler, originalURL, executor) {
|
|
502
|
+
const runningGenerators = [];
|
|
503
|
+
let currentResponse = null;
|
|
504
|
+
const requestPathname = new URL(request.url).pathname;
|
|
505
|
+
for (let i = 0; i < middlewares.length; i++) {
|
|
506
|
+
const entry = middlewares[i];
|
|
507
|
+
const middleware = entry.middleware;
|
|
508
|
+
if (entry.pathPrefix && !this.#matchesPathPrefix(requestPathname, entry.pathPrefix)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (this.#isGeneratorMiddleware(middleware)) {
|
|
512
|
+
const generator = middleware(request, context);
|
|
513
|
+
const result = await generator.next();
|
|
514
|
+
if (result.done) {
|
|
515
|
+
if (result.value) {
|
|
516
|
+
currentResponse = result.value;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
runningGenerators.push({ generator, index: i });
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
const result = await middleware(
|
|
524
|
+
request,
|
|
525
|
+
context
|
|
526
|
+
);
|
|
527
|
+
if (result) {
|
|
528
|
+
currentResponse = result;
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (!currentResponse) {
|
|
534
|
+
let finalHandler = handler;
|
|
535
|
+
let finalContext = context;
|
|
536
|
+
if (request.url !== originalURL && executor) {
|
|
537
|
+
const newMatchResult = executor.match(
|
|
538
|
+
new Request(request.url, {
|
|
539
|
+
method: request.method,
|
|
540
|
+
headers: request.headers,
|
|
541
|
+
body: request.body
|
|
542
|
+
})
|
|
543
|
+
);
|
|
544
|
+
if (newMatchResult) {
|
|
545
|
+
finalHandler = newMatchResult.handler;
|
|
546
|
+
finalContext = newMatchResult.context;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
let handlerError = null;
|
|
550
|
+
try {
|
|
551
|
+
currentResponse = await finalHandler(request, finalContext);
|
|
552
|
+
} catch (error) {
|
|
553
|
+
handlerError = error;
|
|
554
|
+
}
|
|
555
|
+
if (handlerError) {
|
|
556
|
+
currentResponse = await this.#handleErrorThroughGenerators(
|
|
557
|
+
handlerError,
|
|
558
|
+
runningGenerators
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (request.url !== originalURL && currentResponse) {
|
|
563
|
+
currentResponse = this.#handleAutomaticRedirect(
|
|
564
|
+
originalURL,
|
|
565
|
+
request.url,
|
|
566
|
+
request.method
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
for (let i = runningGenerators.length - 1; i >= 0; i--) {
|
|
570
|
+
const { generator } = runningGenerators[i];
|
|
571
|
+
const result = await generator.next(currentResponse);
|
|
572
|
+
if (result.value && result.done) {
|
|
573
|
+
currentResponse = result.value;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return currentResponse;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Handle errors by trying generators in reverse order
|
|
580
|
+
*/
|
|
581
|
+
async #handleErrorThroughGenerators(error, runningGenerators) {
|
|
582
|
+
for (let i = runningGenerators.length - 1; i >= 0; i--) {
|
|
583
|
+
const { generator } = runningGenerators[i];
|
|
584
|
+
try {
|
|
585
|
+
const result = await generator.throw(error);
|
|
586
|
+
if (result.done && result.value) {
|
|
587
|
+
runningGenerators.splice(i, 1);
|
|
588
|
+
return result.value;
|
|
589
|
+
}
|
|
590
|
+
} catch (generatorError) {
|
|
591
|
+
runningGenerators.splice(i, 1);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Create a mutable request wrapper that allows URL modification
|
|
599
|
+
*/
|
|
600
|
+
#createMutableRequest(request) {
|
|
601
|
+
return {
|
|
602
|
+
url: request.url,
|
|
603
|
+
method: request.method,
|
|
604
|
+
headers: new Headers(request.headers),
|
|
605
|
+
body: request.body,
|
|
606
|
+
bodyUsed: request.bodyUsed,
|
|
607
|
+
cache: request.cache,
|
|
608
|
+
credentials: request.credentials,
|
|
609
|
+
destination: request.destination,
|
|
610
|
+
integrity: request.integrity,
|
|
611
|
+
keepalive: request.keepalive,
|
|
612
|
+
mode: request.mode,
|
|
613
|
+
redirect: request.redirect,
|
|
614
|
+
referrer: request.referrer,
|
|
615
|
+
referrerPolicy: request.referrerPolicy,
|
|
616
|
+
signal: request.signal,
|
|
617
|
+
// Add all other Request methods
|
|
618
|
+
arrayBuffer: () => request.arrayBuffer(),
|
|
619
|
+
blob: () => request.blob(),
|
|
620
|
+
clone: () => request.clone(),
|
|
621
|
+
formData: () => request.formData(),
|
|
622
|
+
json: () => request.json(),
|
|
623
|
+
text: () => request.text()
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Handle automatic redirects when URL is modified
|
|
628
|
+
*/
|
|
629
|
+
#handleAutomaticRedirect(originalURL, newURL, method) {
|
|
630
|
+
const originalURLObj = new URL(originalURL);
|
|
631
|
+
const newURLObj = new URL(newURL);
|
|
632
|
+
if (originalURLObj.hostname !== newURLObj.hostname || originalURLObj.port !== newURLObj.port && originalURLObj.port !== "" && newURLObj.port !== "") {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`Cross-origin redirect not allowed: ${originalURL} -> ${newURL}`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
let status = 302;
|
|
638
|
+
if (originalURLObj.protocol !== newURLObj.protocol) {
|
|
639
|
+
status = 301;
|
|
640
|
+
} else if (method.toUpperCase() !== "GET") {
|
|
641
|
+
status = 307;
|
|
642
|
+
}
|
|
643
|
+
return new Response(null, {
|
|
644
|
+
status,
|
|
645
|
+
headers: {
|
|
646
|
+
Location: newURL
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Get route statistics
|
|
652
|
+
*/
|
|
653
|
+
getStats() {
|
|
654
|
+
return {
|
|
655
|
+
routeCount: this.#routes.length,
|
|
656
|
+
middlewareCount: this.#middlewares.length,
|
|
657
|
+
compiled: !this.#dirty && this.#executor !== null
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Create an error response for unhandled errors
|
|
662
|
+
* Uses HTTPError.toResponse() for consistent error formatting
|
|
663
|
+
*/
|
|
664
|
+
#createErrorResponse(error) {
|
|
665
|
+
const httpError = isHTTPError(error) ? error : new InternalServerError(error.message, { cause: error });
|
|
666
|
+
const isDev = import.meta.env?.MODE !== "production";
|
|
667
|
+
return httpError.toResponse(isDev);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
4
670
|
export {
|
|
5
671
|
Router
|
|
6
672
|
};
|