@b9g/router 0.1.7 → 0.1.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/router",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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,6 +13,7 @@
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": {
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 { InternalServerError, isHTTPError } from "@b9g/http-errors";
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
@@ -276,7 +280,7 @@ var Router = class {
276
280
  );
277
281
  } else {
278
282
  const notFoundHandler = async () => {
279
- return new Response("Not Found", { status: 404 });
283
+ throw new NotFound();
280
284
  };
281
285
  const mutableRequest = this.#createMutableRequest(request);
282
286
  return await this.#executeMiddlewareStack(
@@ -361,18 +365,28 @@ var Router = class {
361
365
  handler = matchResult.handler;
362
366
  context = matchResult.context;
363
367
  } else {
364
- handler = async () => new Response("Not Found", { status: 404 });
368
+ handler = async () => {
369
+ throw new NotFound();
370
+ };
365
371
  context = { params: {} };
366
372
  }
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
- );
373
+ let response;
374
+ try {
375
+ response = await this.#executeMiddlewareStack(
376
+ this.#middlewares,
377
+ mutableRequest,
378
+ context,
379
+ handler,
380
+ originalURL,
381
+ this.#executor
382
+ // Pass executor for re-routing
383
+ );
384
+ } catch (error) {
385
+ if (!matchResult && isHTTPError(error) && error.status === 404) {
386
+ return null;
387
+ }
388
+ throw error;
389
+ }
376
390
  if (!matchResult && response?.status === 404) {
377
391
  return null;
378
392
  }
@@ -553,10 +567,18 @@ var Router = class {
553
567
  handlerError = error;
554
568
  }
555
569
  if (handlerError) {
556
- currentResponse = await this.#handleErrorThroughGenerators(
557
- handlerError,
558
- runningGenerators
559
- );
570
+ if (request.url !== originalURL) {
571
+ currentResponse = this.#handleAutomaticRedirect(
572
+ originalURL,
573
+ request.url,
574
+ request.method
575
+ );
576
+ } else {
577
+ currentResponse = await this.#handleErrorThroughGenerators(
578
+ handlerError,
579
+ runningGenerators
580
+ );
581
+ }
560
582
  }
561
583
  }
562
584
  if (request.url !== originalURL && currentResponse) {
@@ -667,6 +689,28 @@ var Router = class {
667
689
  return httpError.toResponse(isDev);
668
690
  }
669
691
  };
692
+ function trailingSlash(mode) {
693
+ return (request, _context) => {
694
+ const url = new URL(request.url);
695
+ const pathname = url.pathname;
696
+ if (pathname === "/")
697
+ return;
698
+ let newPathname = null;
699
+ if (mode === "strip" && pathname.endsWith("/")) {
700
+ newPathname = pathname.slice(0, -1);
701
+ } else if (mode === "add" && !pathname.endsWith("/")) {
702
+ newPathname = pathname + "/";
703
+ }
704
+ if (newPathname) {
705
+ url.pathname = newPathname;
706
+ return new Response(null, {
707
+ status: 301,
708
+ headers: { Location: url.toString() }
709
+ });
710
+ }
711
+ };
712
+ }
670
713
  export {
671
- Router
714
+ Router,
715
+ trailingSlash
672
716
  };