@b9g/router 0.1.6 → 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 +1 -1
- package/package.json +1 -1
- package/src/index.d.ts +8 -6
- package/src/index.js +77 -56
package/README.md
CHANGED
|
@@ -117,7 +117,7 @@ router.use(cacheMiddleware);
|
|
|
117
117
|
- `GeneratorMiddleware` - Generator-based middleware type using `yield`
|
|
118
118
|
- `FunctionMiddleware` - Simple function middleware type
|
|
119
119
|
- `Middleware` - Union of GeneratorMiddleware | FunctionMiddleware
|
|
120
|
-
- `
|
|
120
|
+
- `HTTPMethod` - HTTP method string literal type
|
|
121
121
|
- `RouteConfig` - Route configuration object
|
|
122
122
|
|
|
123
123
|
## API Reference
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -30,14 +30,14 @@ export type Handler = (request: Request, context: RouteContext) => Response | Pr
|
|
|
30
30
|
* Generator middleware signature - uses yield for continuation
|
|
31
31
|
* Provides clean syntax and eliminates control flow bugs
|
|
32
32
|
*/
|
|
33
|
-
export type GeneratorMiddleware = (request: Request, context: RouteContext) => AsyncGenerator<Request, Response | null | undefined, Response>;
|
|
33
|
+
export type GeneratorMiddleware = (request: Request, context: RouteContext) => Generator<Request, Response | null | undefined, Response> | AsyncGenerator<Request, Response | null | undefined, Response>;
|
|
34
34
|
/**
|
|
35
35
|
* Function middleware signature - supports short-circuiting
|
|
36
36
|
* Can modify request and context, and can return a Response to short-circuit
|
|
37
37
|
* - Return Response: short-circuits, skipping remaining middleware and handler
|
|
38
38
|
* - Return null/undefined: continues to next middleware (fallthrough)
|
|
39
39
|
*/
|
|
40
|
-
export type FunctionMiddleware = (request: Request, context: RouteContext) => null | undefined | Response |
|
|
40
|
+
export type FunctionMiddleware = (request: Request, context: RouteContext) => Response | null | undefined | Promise<Response | null | undefined>;
|
|
41
41
|
/**
|
|
42
42
|
* Union type for all supported middleware types
|
|
43
43
|
* Framework automatically detects type and executes appropriately
|
|
@@ -46,7 +46,7 @@ export type Middleware = GeneratorMiddleware | FunctionMiddleware;
|
|
|
46
46
|
/**
|
|
47
47
|
* HTTP methods supported by the router
|
|
48
48
|
*/
|
|
49
|
-
export type
|
|
49
|
+
export type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
|
|
50
50
|
/**
|
|
51
51
|
* Route configuration options
|
|
52
52
|
*/
|
|
@@ -67,6 +67,8 @@ interface RouteEntry {
|
|
|
67
67
|
*/
|
|
68
68
|
interface MiddlewareEntry {
|
|
69
69
|
middleware: Middleware;
|
|
70
|
+
/** If set, middleware only runs for paths matching this prefix */
|
|
71
|
+
pathPrefix?: string;
|
|
70
72
|
}
|
|
71
73
|
/**
|
|
72
74
|
* RouteBuilder provides a chainable API for defining routes with multiple HTTP methods
|
|
@@ -126,9 +128,9 @@ export declare class Router {
|
|
|
126
128
|
*/
|
|
127
129
|
use(middleware: Middleware): void;
|
|
128
130
|
/**
|
|
129
|
-
* Register
|
|
131
|
+
* Register middleware that only applies to routes matching the path prefix
|
|
130
132
|
*/
|
|
131
|
-
use(
|
|
133
|
+
use(pathPrefix: string, middleware: Middleware): void;
|
|
132
134
|
/**
|
|
133
135
|
* Create a route builder for the given pattern
|
|
134
136
|
* Returns a chainable interface for registering HTTP method handlers
|
|
@@ -144,7 +146,7 @@ export declare class Router {
|
|
|
144
146
|
* Internal method called by RouteBuilder to register routes
|
|
145
147
|
* Public for RouteBuilder access, but not intended for direct use
|
|
146
148
|
*/
|
|
147
|
-
addRoute(method:
|
|
149
|
+
addRoute(method: HTTPMethod, pattern: string, handler: Handler): void;
|
|
148
150
|
/**
|
|
149
151
|
* Handle a request - main entrypoint for ServiceWorker usage
|
|
150
152
|
* Returns a response or throws if no route matches
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
isSimplePattern,
|
|
6
6
|
compilePathname
|
|
7
7
|
} from "@b9g/match-pattern";
|
|
8
|
+
import { InternalServerError, isHTTPError } from "@b9g/http-errors";
|
|
8
9
|
var RadixNode = class {
|
|
9
10
|
children;
|
|
10
11
|
// char -> RadixNode
|
|
@@ -116,12 +117,18 @@ var RadixTreeExecutor = class {
|
|
|
116
117
|
}
|
|
117
118
|
return null;
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
+
let handler = node.handlers.get(method);
|
|
121
|
+
if (!handler && method === "HEAD") {
|
|
122
|
+
handler = node.handlers.get("GET");
|
|
123
|
+
}
|
|
120
124
|
if (handler) {
|
|
121
125
|
return { handler, params };
|
|
122
126
|
}
|
|
123
127
|
if (node.wildcardChild) {
|
|
124
|
-
|
|
128
|
+
let wildcardHandler = node.wildcardChild.handlers.get(method);
|
|
129
|
+
if (!wildcardHandler && method === "HEAD") {
|
|
130
|
+
wildcardHandler = node.wildcardChild.handlers.get("GET");
|
|
131
|
+
}
|
|
125
132
|
if (wildcardHandler) {
|
|
126
133
|
params["0"] = "";
|
|
127
134
|
return { handler: wildcardHandler, params };
|
|
@@ -144,7 +151,8 @@ var RadixTreeExecutor = class {
|
|
|
144
151
|
};
|
|
145
152
|
}
|
|
146
153
|
for (const route of this.#complexRoutes) {
|
|
147
|
-
|
|
154
|
+
const methodMatches = route.method === method || method === "HEAD" && route.method === "GET";
|
|
155
|
+
if (!methodMatches) {
|
|
148
156
|
continue;
|
|
149
157
|
}
|
|
150
158
|
const match = pathname.match(route.compiled.regex);
|
|
@@ -286,28 +294,27 @@ var Router = class {
|
|
|
286
294
|
};
|
|
287
295
|
this.handler = this.#handlerImpl;
|
|
288
296
|
}
|
|
289
|
-
use(
|
|
290
|
-
if (typeof
|
|
291
|
-
|
|
292
|
-
this
|
|
293
|
-
this.addRoute("PUT", patternOrMiddleware, handler);
|
|
294
|
-
this.addRoute("DELETE", patternOrMiddleware, handler);
|
|
295
|
-
this.addRoute("PATCH", patternOrMiddleware, handler);
|
|
296
|
-
this.addRoute("HEAD", patternOrMiddleware, handler);
|
|
297
|
-
this.addRoute("OPTIONS", patternOrMiddleware, handler);
|
|
298
|
-
} else if (typeof patternOrMiddleware === "function") {
|
|
299
|
-
if (!this.#isValidMiddleware(patternOrMiddleware)) {
|
|
297
|
+
use(pathPrefixOrMiddleware, maybeMiddleware) {
|
|
298
|
+
if (typeof pathPrefixOrMiddleware === "string") {
|
|
299
|
+
const middleware = maybeMiddleware;
|
|
300
|
+
if (!this.#isValidMiddleware(middleware)) {
|
|
300
301
|
throw new Error(
|
|
301
302
|
"Invalid middleware type. Must be function or async generator function."
|
|
302
303
|
);
|
|
303
304
|
}
|
|
304
|
-
this.#middlewares.push({
|
|
305
|
-
|
|
305
|
+
this.#middlewares.push({
|
|
306
|
+
middleware,
|
|
307
|
+
pathPrefix: pathPrefixOrMiddleware
|
|
308
|
+
});
|
|
306
309
|
} else {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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 });
|
|
310
316
|
}
|
|
317
|
+
this.#dirty = true;
|
|
311
318
|
}
|
|
312
319
|
route(patternOrConfig) {
|
|
313
320
|
if (typeof patternOrConfig === "string") {
|
|
@@ -357,7 +364,7 @@ var Router = class {
|
|
|
357
364
|
handler = async () => new Response("Not Found", { status: 404 });
|
|
358
365
|
context = { params: {} };
|
|
359
366
|
}
|
|
360
|
-
|
|
367
|
+
let response = await this.#executeMiddlewareStack(
|
|
361
368
|
this.#middlewares,
|
|
362
369
|
mutableRequest,
|
|
363
370
|
context,
|
|
@@ -369,6 +376,13 @@ var Router = class {
|
|
|
369
376
|
if (!matchResult && response?.status === 404) {
|
|
370
377
|
return null;
|
|
371
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
|
+
}
|
|
372
386
|
return response;
|
|
373
387
|
}
|
|
374
388
|
/**
|
|
@@ -412,7 +426,19 @@ var Router = class {
|
|
|
412
426
|
}
|
|
413
427
|
const submiddlewares = subrouter.getMiddlewares();
|
|
414
428
|
for (const submiddleware of submiddlewares) {
|
|
415
|
-
|
|
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
|
+
});
|
|
416
442
|
}
|
|
417
443
|
this.#dirty = true;
|
|
418
444
|
}
|
|
@@ -445,13 +471,29 @@ var Router = class {
|
|
|
445
471
|
*/
|
|
446
472
|
#isValidMiddleware(middleware) {
|
|
447
473
|
const constructorName = middleware.constructor.name;
|
|
448
|
-
return constructorName === "AsyncGeneratorFunction" || constructorName === "AsyncFunction" || constructorName === "Function";
|
|
474
|
+
return constructorName === "AsyncGeneratorFunction" || constructorName === "GeneratorFunction" || constructorName === "AsyncFunction" || constructorName === "Function";
|
|
449
475
|
}
|
|
450
476
|
/**
|
|
451
477
|
* Detect if a function is a generator middleware
|
|
452
478
|
*/
|
|
453
479
|
#isGeneratorMiddleware(middleware) {
|
|
454
|
-
|
|
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;
|
|
455
497
|
}
|
|
456
498
|
/**
|
|
457
499
|
* Execute middleware stack with guaranteed execution using Rack-style LIFO order
|
|
@@ -459,8 +501,13 @@ var Router = class {
|
|
|
459
501
|
async #executeMiddlewareStack(middlewares, request, context, handler, originalURL, executor) {
|
|
460
502
|
const runningGenerators = [];
|
|
461
503
|
let currentResponse = null;
|
|
504
|
+
const requestPathname = new URL(request.url).pathname;
|
|
462
505
|
for (let i = 0; i < middlewares.length; i++) {
|
|
463
|
-
const
|
|
506
|
+
const entry = middlewares[i];
|
|
507
|
+
const middleware = entry.middleware;
|
|
508
|
+
if (entry.pathPrefix && !this.#matchesPathPrefix(requestPathname, entry.pathPrefix)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
464
511
|
if (this.#isGeneratorMiddleware(middleware)) {
|
|
465
512
|
const generator = middleware(request, context);
|
|
466
513
|
const result = await generator.next();
|
|
@@ -522,7 +569,7 @@ var Router = class {
|
|
|
522
569
|
for (let i = runningGenerators.length - 1; i >= 0; i--) {
|
|
523
570
|
const { generator } = runningGenerators[i];
|
|
524
571
|
const result = await generator.next(currentResponse);
|
|
525
|
-
if (result.value) {
|
|
572
|
+
if (result.value && result.done) {
|
|
526
573
|
currentResponse = result.value;
|
|
527
574
|
}
|
|
528
575
|
}
|
|
@@ -536,7 +583,7 @@ var Router = class {
|
|
|
536
583
|
const { generator } = runningGenerators[i];
|
|
537
584
|
try {
|
|
538
585
|
const result = await generator.throw(error);
|
|
539
|
-
if (result.value) {
|
|
586
|
+
if (result.done && result.value) {
|
|
540
587
|
runningGenerators.splice(i, 1);
|
|
541
588
|
return result.value;
|
|
542
589
|
}
|
|
@@ -612,40 +659,14 @@ var Router = class {
|
|
|
612
659
|
}
|
|
613
660
|
/**
|
|
614
661
|
* Create an error response for unhandled errors
|
|
615
|
-
*
|
|
662
|
+
* Uses HTTPError.toResponse() for consistent error formatting
|
|
616
663
|
*/
|
|
617
664
|
#createErrorResponse(error) {
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
<html>
|
|
622
|
-
<head>
|
|
623
|
-
<title>500 Internal Server Error</title>
|
|
624
|
-
<style>
|
|
625
|
-
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
|
|
626
|
-
h1 { color: #dc2626; }
|
|
627
|
-
pre { background: #1e1e1e; color: #d4d4d4; padding: 1rem; overflow-x: auto; border-radius: 4px; }
|
|
628
|
-
.message { font-size: 1.25rem; color: #374151; }
|
|
629
|
-
</style>
|
|
630
|
-
</head>
|
|
631
|
-
<body>
|
|
632
|
-
<h1>500 Internal Server Error</h1>
|
|
633
|
-
<p class="message">${escapeHtml(error.message)}</p>
|
|
634
|
-
<pre>${escapeHtml(error.stack || "No stack trace available")}</pre>
|
|
635
|
-
</body>
|
|
636
|
-
</html>`;
|
|
637
|
-
return new Response(html, {
|
|
638
|
-
status: 500,
|
|
639
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
640
|
-
});
|
|
641
|
-
} else {
|
|
642
|
-
return new Response("Internal Server Error", { status: 500 });
|
|
643
|
-
}
|
|
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);
|
|
644
668
|
}
|
|
645
669
|
};
|
|
646
|
-
function escapeHtml(str) {
|
|
647
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
648
|
-
}
|
|
649
670
|
export {
|
|
650
671
|
Router
|
|
651
672
|
};
|