@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 CHANGED
@@ -1,55 +1,46 @@
1
1
  # @b9g/router
2
2
 
3
- **Universal request router for ServiceWorker applications. Built on web standards with generator-based middleware.**
3
+ **Universal request router built on web standards with generator-based middleware.**
4
4
 
5
5
  ## Features
6
6
 
7
- - **ServiceWorker Compatible**: Designed for ServiceWorker `fetch` event handling
7
+ - **Web Standards**: Built on URLPattern-like syntax, Request, and Response APIs
8
8
  - **Generator Middleware**: Uses `yield` for flow control (no Express-style `next()`)
9
- - **Web Standards**: Built on URLPattern, Request, and Response APIs
10
9
  - **Universal**: Same code runs in browsers, Node.js, Bun, and edge platforms
11
10
  - **Simple Context**: Route parameters and middleware-extensible context
11
+ - **Router Composition**: Mount subrouters with path prefixes
12
12
 
13
13
  ## Installation
14
14
 
15
15
  ```bash
16
- npm install @b9g/router @b9g/match-pattern
16
+ npm install @b9g/router
17
17
  ```
18
18
 
19
19
  ## Quick Start
20
20
 
21
21
  ```javascript
22
- import { Router } from '@b9g/router';
22
+ import {Router} from '@b9g/router';
23
23
 
24
24
  const router = new Router();
25
25
 
26
26
  // Simple route
27
- router.get('/hello', () => new Response('Hello World!'));
27
+ router.route('/hello').get(() => new Response('Hello World!'));
28
28
 
29
29
  // Route with parameters
30
- router.get('/posts/:id', (request, context) => {
31
- const { id } = context.params;
32
- return Response.json({ id, title: `Post ${id}` });
30
+ router.route('/posts/:id').get((request, context) => {
31
+ const {id} = context.params;
32
+ return Response.json({id, title: `Post ${id}`});
33
33
  });
34
34
 
35
35
  // Handle request
36
- const response = await router.handler(request);
36
+ const response = await router.handle(request);
37
37
  ```
38
38
 
39
39
  ## Middleware
40
40
 
41
- The router supports generator-based middleware with `yield` for clean flow control:
42
-
41
+ The router supports function and generator-based middleware with `yield` for clean flow control:
43
42
  ```javascript
44
- // Global middleware using generator pattern
45
- router.use(async function* (request, context) {
46
- console.log(`${request.method} ${request.url}`);
47
- const response = yield request;
48
- console.log(`${response.status}`);
49
- return response;
50
- });
51
-
52
- // Function middleware (can short-circuit)
43
+ // Function middleware
53
44
  router.use(async (request, context) => {
54
45
  if (!request.headers.get('Authorization')) {
55
46
  return new Response('Unauthorized', { status: 401 });
@@ -57,69 +48,16 @@ router.use(async (request, context) => {
57
48
  // Return null/undefined to continue to next middleware
58
49
  return null;
59
50
  });
60
- ```
61
-
62
- ## Caching
63
-
64
- The router doesn't provide built-in cache integration. For caching, use the global `caches` API directly in your handlers:
65
-
66
- ```javascript
67
- router.get('/api/posts/:id', async (request, context) => {
68
- // Use global caches API
69
- const cache = await caches.open('api-v1');
70
-
71
- const cached = await cache.match(request);
72
- if (cached) return cached;
73
-
74
- const post = await db.posts.get(context.params.id);
75
- const response = Response.json(post);
76
-
77
- await cache.put(request, response.clone());
78
- return response;
79
- });
80
- ```
81
-
82
- Or implement caching as middleware:
83
-
84
- ```javascript
85
- // Cache middleware
86
- async function* cacheMiddleware(request, context) {
87
- if (request.method !== 'GET') {
88
- return yield request;
89
- }
90
-
91
- const cache = await caches.open('pages-v1');
92
- const cached = await cache.match(request);
93
- if (cached) return cached;
94
51
 
52
+ // Generator middleware
53
+ router.use(async function* (request, context) {
54
+ console.log(`${request.method} ${request.url}`);
95
55
  const response = yield request;
96
-
97
- if (response.ok) {
98
- await cache.put(request, response.clone());
99
- }
100
-
56
+ console.log(`${response.status}`);
101
57
  return response;
102
- }
103
-
104
- router.use(cacheMiddleware);
58
+ });
105
59
  ```
106
60
 
107
- ## Exports
108
-
109
- ### Classes
110
-
111
- - `Router` - Request router with pattern matching and middleware support
112
-
113
- ### Types
114
-
115
- - `RouteContext` - Context object passed to handlers with params and middleware-added properties
116
- - `Handler` - Route handler function type `(request, context) => Response | Promise<Response>`
117
- - `GeneratorMiddleware` - Generator-based middleware type using `yield`
118
- - `FunctionMiddleware` - Simple function middleware type
119
- - `Middleware` - Union of GeneratorMiddleware | FunctionMiddleware
120
- - `HTTPMethod` - HTTP method string literal type
121
- - `RouteConfig` - Route configuration object
122
-
123
61
  ## API Reference
124
62
 
125
63
  ### Router
@@ -143,17 +81,6 @@ router.route('/api/posts/:id')
143
81
  .delete(handler);
144
82
  ```
145
83
 
146
- ##### HTTP Method Shortcuts
147
-
148
- ```javascript
149
- router.get(pattern, handler)
150
- router.post(pattern, handler)
151
- router.put(pattern, handler)
152
- router.delete(pattern, handler)
153
- router.patch(pattern, handler)
154
- router.head(pattern, handler)
155
- router.options(pattern, handler)
156
- ```
157
84
 
158
85
  ##### `use(middleware)`
159
86
 
@@ -163,12 +90,58 @@ Add global middleware.
163
90
  router.use(loggingMiddleware);
164
91
  ```
165
92
 
166
- ##### `handler(request): Promise<Response>`
93
+ ##### `handle(request): Promise<Response>`
94
+
95
+ Handle an incoming request and return a response.
96
+
97
+ ```javascript
98
+ const response = await router.handle(request);
99
+ ```
100
+
101
+ ##### `mount(path, subrouter)`
102
+
103
+ Mount a subrouter at a specific path prefix.
104
+
105
+ ```javascript
106
+ const apiRouter = new Router();
107
+ apiRouter.route('/users').get(handler);
108
+
109
+ const mainRouter = new Router();
110
+ mainRouter.mount('/api/v1', apiRouter);
111
+ // Routes become: /api/v1/users
112
+ ```
113
+
114
+ ##### `match(url): RouteMatch | null`
115
+
116
+ Match a URL against registered routes without executing handlers.
117
+
118
+ ```javascript
119
+ const match = router.match(new URL('https://example.com/api/users'));
120
+ if (match) {
121
+ console.log(match.params, match.methods);
122
+ }
123
+ ```
124
+
125
+ #### Properties
126
+
127
+ ##### `routes: RouteEntry[]`
128
+
129
+ Read-only array of registered routes for introspection.
130
+
131
+ ```javascript
132
+ router.routes.forEach(route => {
133
+ console.log(route.pattern, route.method);
134
+ });
135
+ ```
136
+
137
+ ##### `middlewares: MiddlewareEntry[]`
167
138
 
168
- Bound handler function for processing requests.
139
+ Read-only array of registered middleware for introspection.
169
140
 
170
141
  ```javascript
171
- const response = await router.handler(request);
142
+ router.middlewares.forEach(mw => {
143
+ console.log(mw.pathPrefix);
144
+ });
172
145
  ```
173
146
 
174
147
  ### Context Object
@@ -198,20 +171,20 @@ router.use(async function* (request, context) {
198
171
  ```javascript
199
172
  const router = new Router();
200
173
 
201
- router.get('/api/health', () =>
202
- Response.json({ status: 'ok' })
174
+ router.route('/api/health').get(() =>
175
+ Response.json({status: 'ok'})
203
176
  );
204
177
 
205
- router.get('/api/posts', async () => {
206
- const posts = await db.posts.findAll();
207
- return Response.json(posts);
208
- });
209
-
210
- router.post('/api/posts', async (request) => {
211
- const data = await request.json();
212
- const post = await db.posts.create(data);
213
- return Response.json(post, { status: 201 });
214
- });
178
+ router.route('/api/posts')
179
+ .get(async () => {
180
+ const posts = await db.posts.findAll();
181
+ return Response.json(posts);
182
+ })
183
+ .post(async (request) => {
184
+ const data = await request.json();
185
+ const post = await db.posts.create(data);
186
+ return Response.json(post, {status: 201});
187
+ });
215
188
  ```
216
189
 
217
190
  ### Authentication Middleware
@@ -229,7 +202,7 @@ router.use(async function* (request, context) {
229
202
  });
230
203
 
231
204
  // Protected route
232
- router.get('/api/profile', async (request, context) => {
205
+ router.route('/api/profile').get(async (request, context) => {
233
206
  if (!context.user) {
234
207
  return new Response('Unauthorized', { status: 401 });
235
208
  }
@@ -242,8 +215,8 @@ router.get('/api/profile', async (request, context) => {
242
215
  ```javascript
243
216
  // API subrouter
244
217
  const apiRouter = new Router();
245
- apiRouter.get('/users', getUsersHandler);
246
- apiRouter.get('/posts', getPostsHandler);
218
+ apiRouter.route('/users').get(getUsersHandler);
219
+ apiRouter.route('/posts').get(getPostsHandler);
247
220
 
248
221
  // Main router
249
222
  const mainRouter = new Router();
@@ -251,6 +224,98 @@ mainRouter.mount('/api/v1', apiRouter);
251
224
  // Routes become: /api/v1/users, /api/v1/posts
252
225
  ```
253
226
 
227
+ ## Exports
228
+
229
+ ### Classes
230
+
231
+ - `Router` - Main router class
232
+ - `RouteBuilder` - Fluent API for defining routes (returned by `router.route()`)
233
+
234
+ ### Types
235
+
236
+ ```typescript
237
+ // Handler and middleware types
238
+ type Handler = (request: Request, context: RouteContext) => Response | Promise<Response>
239
+ type FunctionMiddleware = (request: Request, context: RouteContext) => Response | null | undefined | Promise<Response | null | undefined>
240
+ type GeneratorMiddleware = (request: Request, context: RouteContext) => Generator<Request, Response | null | undefined, Response> | AsyncGenerator<Request, Response | null | undefined, Response>
241
+ type Middleware = GeneratorMiddleware | FunctionMiddleware
242
+
243
+ // Context and route types
244
+ interface RouteContext {
245
+ params: Record<string, string>
246
+ }
247
+
248
+ interface RouteOptions {
249
+ name?: string
250
+ }
251
+
252
+ interface RouteMatch {
253
+ params: Record<string, string>
254
+ methods: string[]
255
+ }
256
+
257
+ interface RouteEntry {
258
+ pattern: MatchPattern
259
+ method: string
260
+ handler: Handler
261
+ name?: string
262
+ middleware: Middleware[]
263
+ }
264
+
265
+ interface MiddlewareEntry {
266
+ middleware: Middleware
267
+ pathPrefix?: string
268
+ }
269
+
270
+ // HTTP methods
271
+ type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS"
272
+
273
+ // Utility types
274
+ type TrailingSlashMode = "strip" | "add"
275
+ ```
276
+
277
+ ## Middleware Utilities
278
+
279
+ Standard middleware is available from `@b9g/router/middleware`:
280
+
281
+ ```typescript
282
+ import {cors, trailingSlash} from '@b9g/router/middleware';
283
+
284
+ // CORS middleware
285
+ router.use(cors({
286
+ origin: "https://example.com",
287
+ credentials: true
288
+ }));
289
+
290
+ // Trailing slash normalization
291
+ router.use(trailingSlash("strip")); // /path/ → /path
292
+ ```
293
+
294
+ ### Available Middleware
295
+
296
+ #### `cors(options?: CORSOptions)`
297
+
298
+ Handles Cross-Origin Resource Sharing headers and preflight requests.
299
+
300
+ ```typescript
301
+ interface CORSOptions {
302
+ origin?: string | string[] | ((origin: string) => boolean); // Default: "*"
303
+ methods?: string[]; // Default: ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"]
304
+ allowedHeaders?: string[]; // Default: ["Content-Type", "Authorization"]
305
+ exposedHeaders?: string[];
306
+ credentials?: boolean; // Default: false
307
+ maxAge?: number; // Default: 86400 (24 hours)
308
+ }
309
+ ```
310
+
311
+ #### `trailingSlash(mode: TrailingSlashMode)`
312
+
313
+ Normalizes URL trailing slashes via 301 redirect.
314
+
315
+ ```typescript
316
+ type TrailingSlashMode = "strip" | "add";
317
+ ```
318
+
254
319
  ## License
255
320
 
256
321
  MIT
package/package.json CHANGED
@@ -1,24 +1,23 @@
1
1
  {
2
2
  "name": "@b9g/router",
3
- "version": "0.1.9",
4
- "description": "Universal request router for ServiceWorker applications. Built on web standards with cache-aware routing and generator-based middleware.",
3
+ "version": "0.2.0-beta.1",
4
+ "description": "Universal request router built on web standards with generator-based middleware.",
5
5
  "keywords": [
6
6
  "router",
7
7
  "serviceworker",
8
8
  "middleware",
9
- "cache",
10
9
  "universal",
11
10
  "web-standards",
12
11
  "urlpattern",
12
+ "generator",
13
13
  "shovel"
14
14
  ],
15
15
  "dependencies": {
16
- "@b9g/http-errors": "^0.1.5",
17
- "@b9g/match-pattern": "^0.1.7"
16
+ "@b9g/http-errors": "^0.2.0-beta.0",
17
+ "@b9g/match-pattern": "^0.2.0-beta.0"
18
18
  },
19
19
  "devDependencies": {
20
- "@b9g/libuild": "^0.1.18",
21
- "bun-types": "latest"
20
+ "@b9g/libuild": "^0.1.18"
22
21
  },
23
22
  "type": "module",
24
23
  "types": "src/index.d.ts",
@@ -36,6 +35,14 @@
36
35
  "types": "./src/index.d.ts",
37
36
  "import": "./src/index.js"
38
37
  },
38
+ "./middleware": {
39
+ "types": "./src/middleware.d.ts",
40
+ "import": "./src/middleware.js"
41
+ },
42
+ "./middleware.js": {
43
+ "types": "./src/middleware.d.ts",
44
+ "import": "./src/middleware.js"
45
+ },
39
46
  "./package.json": "./package.json"
40
47
  }
41
48
  }
package/src/index.d.ts CHANGED
@@ -1,25 +1,14 @@
1
- /**
2
- * @b9g/router - Universal request router built on web standards
3
- *
4
- * Features:
5
- * - Pure Request/Response routing (works anywhere)
6
- * - Chainable route builder API
7
- * - Generator-based middleware with yield continuation
8
- * - Integration with URLPattern and MatchPattern for enhanced URL matching
9
- * - Cache-aware routing
10
- * TODO:
11
- * - Portable param matching
12
- * - Typechecking
13
- */
1
+ /** @b9g/router - Universal request router built on web standards */
14
2
  /**
15
3
  * Context object passed to handlers and middleware
16
4
  * Contains route parameters extracted from URL pattern matching
5
+ * Augmentable via module declaration for middleware-specific properties
17
6
  */
18
7
  export interface RouteContext {
19
8
  /** Route parameters extracted from URL pattern matching */
20
9
  params: Record<string, string>;
21
- /** Middleware can add arbitrary properties for context sharing */
22
- [key: string]: any;
10
+ /** Allow middleware to add arbitrary properties to context */
11
+ [key: string]: unknown;
23
12
  }
24
13
  /**
25
14
  * Handler function signature - terminal response producer
@@ -27,17 +16,16 @@ export interface RouteContext {
27
16
  */
28
17
  export type Handler = (request: Request, context: RouteContext) => Response | Promise<Response>;
29
18
  /**
30
- * Generator middleware signature - uses yield for continuation
31
- * Provides clean syntax and eliminates control flow bugs
19
+ * Function middleware signature
20
+ * Can modify request and context, and can return a Response to short-circuit
32
21
  */
33
- export type GeneratorMiddleware = (request: Request, context: RouteContext) => Generator<Request, Response | null | undefined, Response> | AsyncGenerator<Request, Response | null | undefined, Response>;
22
+ export type FunctionMiddleware = (request: Request, context: RouteContext) => Response | null | undefined | void | Promise<Response | null | undefined | void>;
34
23
  /**
35
- * Function middleware signature - supports short-circuiting
36
- * Can modify request and context, and can return a Response to short-circuit
37
- * - Return Response: short-circuits, skipping remaining middleware and handler
38
- * - Return null/undefined: continues to next middleware (fallthrough)
24
+ * Generator middleware signature - uses yield for continuation.
25
+ * Yield to pass control to the next middleware/handler, receive Response back.
26
+ * Optionally yield a modified Request (or yield without value to use original).
39
27
  */
40
- export type FunctionMiddleware = (request: Request, context: RouteContext) => Response | null | undefined | Promise<Response | null | undefined>;
28
+ export type GeneratorMiddleware = (request: Request, context: RouteContext) => Generator<Request | undefined, Response | null | undefined | void, Response> | AsyncGenerator<Request | undefined, Response | null | undefined | void, Response>;
41
29
  /**
42
30
  * Union type for all supported middleware types
43
31
  * Framework automatically detects type and executes appropriately
@@ -48,24 +36,40 @@ export type Middleware = GeneratorMiddleware | FunctionMiddleware;
48
36
  */
49
37
  export type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
50
38
  /**
51
- * Route configuration options
39
+ * Route options for configuring route metadata
40
+ * Augmentable via module declaration for custom metadata
41
+ */
42
+ export interface RouteOptions {
43
+ /** Optional name for the route, useful for matching/identification */
44
+ name?: string;
45
+ }
46
+ /**
47
+ * Result of matching a URL against registered routes
52
48
  */
53
- export interface RouteConfig {
54
- /** URL pattern for the route */
49
+ export interface RouteMatch {
50
+ /** Route parameters extracted from URL pattern matching */
51
+ params: Record<string, string>;
52
+ /** HTTP methods registered for this pattern */
53
+ methods: string[];
54
+ /** Route name if provided */
55
+ name?: string;
56
+ /** Original pattern string */
55
57
  pattern: string;
56
58
  }
57
59
  /**
58
- * Internal route entry stored by the router
60
+ * Route entry stored by the router
59
61
  */
60
- interface RouteEntry {
62
+ export interface RouteEntry {
61
63
  pattern: import("@b9g/match-pattern").MatchPattern;
62
64
  method: string;
63
- handler: Handler;
65
+ handler?: Handler;
66
+ name?: string;
67
+ middlewares: Middleware[];
64
68
  }
65
69
  /**
66
70
  * Internal middleware entry stored by the router
67
71
  */
68
- interface MiddlewareEntry {
72
+ export interface MiddlewareEntry {
69
73
  middleware: Middleware;
70
74
  /** If set, middleware only runs for paths matching this prefix */
71
75
  pathPrefix?: string;
@@ -74,46 +78,51 @@ interface MiddlewareEntry {
74
78
  * RouteBuilder provides a chainable API for defining routes with multiple HTTP methods
75
79
  *
76
80
  * Example:
77
- * router.route('/api/users/:id')
81
+ * router.route('/api/users/:id', { name: 'user' })
82
+ * .use(authMiddleware)
78
83
  * .get(getUserHandler)
79
84
  * .put(updateUserHandler)
80
85
  * .delete(deleteUserHandler);
81
86
  */
82
- declare class RouteBuilder {
87
+ export declare class RouteBuilder {
83
88
  #private;
84
- constructor(router: Router, pattern: string);
89
+ constructor(router: Router, pattern: string, options?: RouteOptions);
90
+ /**
91
+ * Add route-scoped middleware that only runs when this pattern matches
92
+ */
93
+ use(middleware: Middleware): RouteBuilder;
85
94
  /**
86
95
  * Register a GET handler for this route pattern
87
96
  */
88
- get(handler: Handler): RouteBuilder;
97
+ get(handler?: Handler): RouteBuilder;
89
98
  /**
90
99
  * Register a POST handler for this route pattern
91
100
  */
92
- post(handler: Handler): RouteBuilder;
101
+ post(handler?: Handler): RouteBuilder;
93
102
  /**
94
103
  * Register a PUT handler for this route pattern
95
104
  */
96
- put(handler: Handler): RouteBuilder;
105
+ put(handler?: Handler): RouteBuilder;
97
106
  /**
98
107
  * Register a DELETE handler for this route pattern
99
108
  */
100
- delete(handler: Handler): RouteBuilder;
109
+ delete(handler?: Handler): RouteBuilder;
101
110
  /**
102
111
  * Register a PATCH handler for this route pattern
103
112
  */
104
- patch(handler: Handler): RouteBuilder;
113
+ patch(handler?: Handler): RouteBuilder;
105
114
  /**
106
115
  * Register a HEAD handler for this route pattern
107
116
  */
108
- head(handler: Handler): RouteBuilder;
117
+ head(handler?: Handler): RouteBuilder;
109
118
  /**
110
119
  * Register an OPTIONS handler for this route pattern
111
120
  */
112
- options(handler: Handler): RouteBuilder;
121
+ options(handler?: Handler): RouteBuilder;
113
122
  /**
114
123
  * Register a handler for all HTTP methods on this route pattern
115
124
  */
116
- all(handler: Handler): RouteBuilder;
125
+ all(handler?: Handler): RouteBuilder;
117
126
  }
118
127
  /**
119
128
  * Router provides Request/Response routing with middleware support
@@ -121,6 +130,8 @@ declare class RouteBuilder {
121
130
  */
122
131
  export declare class Router {
123
132
  #private;
133
+ readonly routes: RouteEntry[];
134
+ readonly middlewares: MiddlewareEntry[];
124
135
  constructor();
125
136
  /**
126
137
  * Register middleware that applies to all routes
@@ -136,36 +147,28 @@ export declare class Router {
136
147
  * Returns a chainable interface for registering HTTP method handlers
137
148
  *
138
149
  * Example:
139
- * router.route('/api/users/:id')
150
+ * router.route('/api/users/:id', { name: 'user' })
151
+ * .use(authMiddleware)
140
152
  * .get(getUserHandler)
141
153
  * .put(updateUserHandler);
142
154
  */
143
- route(pattern: string): RouteBuilder;
144
- route(config: RouteConfig): RouteBuilder;
155
+ route(pattern: string, options?: RouteOptions): RouteBuilder;
145
156
  /**
146
157
  * Internal method called by RouteBuilder to register routes
147
158
  * Public for RouteBuilder access, but not intended for direct use
148
159
  */
149
- addRoute(method: HTTPMethod, pattern: string, handler: Handler): void;
150
- /**
151
- * Handle a request - main entrypoint for ServiceWorker usage
152
- * Returns a response or throws if no route matches
153
- */
154
- handler: (request: Request) => Promise<Response>;
155
- /**
156
- * Match a request against registered routes and execute the handler chain
157
- * Returns the response from the matched handler, or null if no route matches
158
- * Note: Global middleware executes even if no route matches
159
- */
160
- match(request: Request): Promise<Response | null>;
160
+ addRoute(method: HTTPMethod, pattern: string, handler?: Handler, name?: string, middlewares?: Middleware[]): void;
161
161
  /**
162
- * Get registered routes for debugging/introspection
162
+ * Match a URL against registered routes
163
+ * Returns route info (params, methods, name, pattern) or null if no match
164
+ * Does not execute handlers - use handle() for that
163
165
  */
164
- getRoutes(): RouteEntry[];
166
+ match(url: string | URL): RouteMatch | null;
165
167
  /**
166
- * Get registered middleware for debugging/introspection
168
+ * Handle a request - main entrypoint for ServiceWorker usage
169
+ * Executes the matched handler with middleware chain
167
170
  */
168
- getMiddlewares(): MiddlewareEntry[];
171
+ handle(request: Request): Promise<Response>;
169
172
  /**
170
173
  * Mount a subrouter at a specific path prefix
171
174
  * All routes from the subrouter will be prefixed with the mount path
@@ -180,37 +183,4 @@ export declare class Router {
180
183
  * // Routes become: /api/v1/users, /api/v1/users/:id
181
184
  */
182
185
  mount(mountPath: string, subrouter: Router): void;
183
- /**
184
- * Get route statistics
185
- */
186
- getStats(): {
187
- routeCount: number;
188
- middlewareCount: number;
189
- compiled: boolean;
190
- };
191
186
  }
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;
216
- export {};