@b9g/router 0.1.7 → 0.1.9
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/package.json +3 -2
- package/src/index.d.ts +24 -0
- package/src/index.js +52 -102
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/router",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Universal request router for ServiceWorker applications. Built on web standards with cache-aware routing and generator-based middleware.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"router",
|
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
"shovel"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"@b9g/http-errors": "^0.1.5",
|
|
16
17
|
"@b9g/match-pattern": "^0.1.7"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
|
-
"@b9g/libuild": "^0.1.
|
|
20
|
+
"@b9g/libuild": "^0.1.18",
|
|
20
21
|
"bun-types": "latest"
|
|
21
22
|
},
|
|
22
23
|
"type": "module",
|
package/src/index.d.ts
CHANGED
|
@@ -189,4 +189,28 @@ export declare class Router {
|
|
|
189
189
|
compiled: boolean;
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Mode for trailing slash normalization
|
|
194
|
+
* - "strip": Redirect /path/ → /path (removes trailing slash)
|
|
195
|
+
* - "add": Redirect /path → /path/ (adds trailing slash)
|
|
196
|
+
*/
|
|
197
|
+
export type TrailingSlashMode = "strip" | "add";
|
|
198
|
+
/**
|
|
199
|
+
* Middleware that normalizes trailing slashes via 301 redirect
|
|
200
|
+
*
|
|
201
|
+
* @param mode - "strip" removes trailing slash, "add" adds trailing slash
|
|
202
|
+
* @returns Function middleware that redirects non-canonical URLs
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```typescript
|
|
206
|
+
* import { Router, trailingSlash } from "@b9g/router";
|
|
207
|
+
*
|
|
208
|
+
* const router = new Router();
|
|
209
|
+
* router.use(trailingSlash("strip")); // Redirect /path/ → /path
|
|
210
|
+
*
|
|
211
|
+
* // Can also be scoped to specific paths
|
|
212
|
+
* router.use("/api", trailingSlash("strip"));
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
export declare function trailingSlash(mode: TrailingSlashMode): FunctionMiddleware;
|
|
192
216
|
export {};
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,11 @@ import {
|
|
|
5
5
|
isSimplePattern,
|
|
6
6
|
compilePathname
|
|
7
7
|
} from "@b9g/match-pattern";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
InternalServerError,
|
|
10
|
+
NotFound,
|
|
11
|
+
isHTTPError
|
|
12
|
+
} from "@b9g/http-errors";
|
|
9
13
|
var RadixNode = class {
|
|
10
14
|
children;
|
|
11
15
|
// char -> RadixNode
|
|
@@ -265,27 +269,21 @@ var Router = class {
|
|
|
265
269
|
}
|
|
266
270
|
const matchResult = this.#executor.match(request);
|
|
267
271
|
if (matchResult) {
|
|
268
|
-
const mutableRequest = this.#createMutableRequest(request);
|
|
269
272
|
return await this.#executeMiddlewareStack(
|
|
270
273
|
this.#middlewares,
|
|
271
|
-
|
|
274
|
+
request,
|
|
272
275
|
matchResult.context,
|
|
273
|
-
matchResult.handler
|
|
274
|
-
request.url,
|
|
275
|
-
this.#executor
|
|
276
|
+
matchResult.handler
|
|
276
277
|
);
|
|
277
278
|
} else {
|
|
278
279
|
const notFoundHandler = async () => {
|
|
279
|
-
|
|
280
|
+
throw new NotFound();
|
|
280
281
|
};
|
|
281
|
-
const mutableRequest = this.#createMutableRequest(request);
|
|
282
282
|
return await this.#executeMiddlewareStack(
|
|
283
283
|
this.#middlewares,
|
|
284
|
-
|
|
284
|
+
request,
|
|
285
285
|
{ params: {} },
|
|
286
|
-
notFoundHandler
|
|
287
|
-
request.url,
|
|
288
|
-
this.#executor
|
|
286
|
+
notFoundHandler
|
|
289
287
|
);
|
|
290
288
|
}
|
|
291
289
|
} catch (error) {
|
|
@@ -352,8 +350,6 @@ var Router = class {
|
|
|
352
350
|
this.#executor = new RadixTreeExecutor(this.#routes);
|
|
353
351
|
this.#dirty = false;
|
|
354
352
|
}
|
|
355
|
-
const mutableRequest = this.#createMutableRequest(request);
|
|
356
|
-
const originalURL = mutableRequest.url;
|
|
357
353
|
let matchResult = this.#executor.match(request);
|
|
358
354
|
let handler;
|
|
359
355
|
let context;
|
|
@@ -361,18 +357,25 @@ var Router = class {
|
|
|
361
357
|
handler = matchResult.handler;
|
|
362
358
|
context = matchResult.context;
|
|
363
359
|
} else {
|
|
364
|
-
handler = async () =>
|
|
360
|
+
handler = async () => {
|
|
361
|
+
throw new NotFound();
|
|
362
|
+
};
|
|
365
363
|
context = { params: {} };
|
|
366
364
|
}
|
|
367
|
-
let response
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
)
|
|
365
|
+
let response;
|
|
366
|
+
try {
|
|
367
|
+
response = await this.#executeMiddlewareStack(
|
|
368
|
+
this.#middlewares,
|
|
369
|
+
request,
|
|
370
|
+
context,
|
|
371
|
+
handler
|
|
372
|
+
);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (!matchResult && isHTTPError(error) && error.status === 404) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
376
379
|
if (!matchResult && response?.status === 404) {
|
|
377
380
|
return null;
|
|
378
381
|
}
|
|
@@ -498,7 +501,7 @@ var Router = class {
|
|
|
498
501
|
/**
|
|
499
502
|
* Execute middleware stack with guaranteed execution using Rack-style LIFO order
|
|
500
503
|
*/
|
|
501
|
-
async #executeMiddlewareStack(middlewares, request, context, handler
|
|
504
|
+
async #executeMiddlewareStack(middlewares, request, context, handler) {
|
|
502
505
|
const runningGenerators = [];
|
|
503
506
|
let currentResponse = null;
|
|
504
507
|
const requestPathname = new URL(request.url).pathname;
|
|
@@ -531,24 +534,9 @@ var Router = class {
|
|
|
531
534
|
}
|
|
532
535
|
}
|
|
533
536
|
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
537
|
let handlerError = null;
|
|
550
538
|
try {
|
|
551
|
-
currentResponse = await
|
|
539
|
+
currentResponse = await handler(request, context);
|
|
552
540
|
} catch (error) {
|
|
553
541
|
handlerError = error;
|
|
554
542
|
}
|
|
@@ -559,13 +547,6 @@ var Router = class {
|
|
|
559
547
|
);
|
|
560
548
|
}
|
|
561
549
|
}
|
|
562
|
-
if (request.url !== originalURL && currentResponse) {
|
|
563
|
-
currentResponse = this.#handleAutomaticRedirect(
|
|
564
|
-
originalURL,
|
|
565
|
-
request.url,
|
|
566
|
-
request.method
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
550
|
for (let i = runningGenerators.length - 1; i >= 0; i--) {
|
|
570
551
|
const { generator } = runningGenerators[i];
|
|
571
552
|
const result = await generator.next(currentResponse);
|
|
@@ -594,59 +575,6 @@ var Router = class {
|
|
|
594
575
|
}
|
|
595
576
|
throw error;
|
|
596
577
|
}
|
|
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
578
|
/**
|
|
651
579
|
* Get route statistics
|
|
652
580
|
*/
|
|
@@ -667,6 +595,28 @@ var Router = class {
|
|
|
667
595
|
return httpError.toResponse(isDev);
|
|
668
596
|
}
|
|
669
597
|
};
|
|
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
|
+
}
|
|
670
619
|
export {
|
|
671
|
-
Router
|
|
620
|
+
Router,
|
|
621
|
+
trailingSlash
|
|
672
622
|
};
|