@b9g/router 0.1.3 → 0.1.6

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 ADDED
@@ -0,0 +1,256 @@
1
+ # @b9g/router
2
+
3
+ **Universal request router for ServiceWorker applications. Built on web standards with generator-based middleware.**
4
+
5
+ ## Features
6
+
7
+ - **ServiceWorker Compatible**: Designed for ServiceWorker `fetch` event handling
8
+ - **Generator Middleware**: Uses `yield` for flow control (no Express-style `next()`)
9
+ - **Web Standards**: Built on URLPattern, Request, and Response APIs
10
+ - **Universal**: Same code runs in browsers, Node.js, Bun, and edge platforms
11
+ - **Simple Context**: Route parameters and middleware-extensible context
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @b9g/router @b9g/match-pattern
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```javascript
22
+ import { Router } from '@b9g/router';
23
+
24
+ const router = new Router();
25
+
26
+ // Simple route
27
+ router.get('/hello', () => new Response('Hello World!'));
28
+
29
+ // Route with parameters
30
+ router.get('/posts/:id', (request, context) => {
31
+ const { id } = context.params;
32
+ return Response.json({ id, title: `Post ${id}` });
33
+ });
34
+
35
+ // Handle request
36
+ const response = await router.handler(request);
37
+ ```
38
+
39
+ ## Middleware
40
+
41
+ The router supports generator-based middleware with `yield` for clean flow control:
42
+
43
+ ```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)
53
+ router.use(async (request, context) => {
54
+ if (!request.headers.get('Authorization')) {
55
+ return new Response('Unauthorized', { status: 401 });
56
+ }
57
+ // Return null/undefined to continue to next middleware
58
+ return null;
59
+ });
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
+
95
+ const response = yield request;
96
+
97
+ if (response.ok) {
98
+ await cache.put(request, response.clone());
99
+ }
100
+
101
+ return response;
102
+ }
103
+
104
+ router.use(cacheMiddleware);
105
+ ```
106
+
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
+ ## API Reference
124
+
125
+ ### Router
126
+
127
+ #### Constructor
128
+
129
+ ```javascript
130
+ new Router()
131
+ ```
132
+
133
+ #### Methods
134
+
135
+ ##### `route(pattern)`
136
+
137
+ Create a route builder for the given pattern.
138
+
139
+ ```javascript
140
+ router.route('/api/posts/:id')
141
+ .get(handler)
142
+ .post(handler)
143
+ .delete(handler);
144
+ ```
145
+
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
+
158
+ ##### `use(middleware)`
159
+
160
+ Add global middleware.
161
+
162
+ ```javascript
163
+ router.use(loggingMiddleware);
164
+ ```
165
+
166
+ ##### `handler(request): Promise<Response>`
167
+
168
+ Bound handler function for processing requests.
169
+
170
+ ```javascript
171
+ const response = await router.handler(request);
172
+ ```
173
+
174
+ ### Context Object
175
+
176
+ Handler and middleware functions receive a context object:
177
+
178
+ ```javascript
179
+ {
180
+ params: Record<string, string>, // URL parameters
181
+ // Middleware can add arbitrary properties
182
+ }
183
+ ```
184
+
185
+ Middleware can extend context with custom properties:
186
+
187
+ ```javascript
188
+ router.use(async function* (request, context) {
189
+ context.user = await authenticate(request);
190
+ return yield request;
191
+ });
192
+ ```
193
+
194
+ ## Examples
195
+
196
+ ### Basic API Router
197
+
198
+ ```javascript
199
+ const router = new Router();
200
+
201
+ router.get('/api/health', () =>
202
+ Response.json({ status: 'ok' })
203
+ );
204
+
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
+ });
215
+ ```
216
+
217
+ ### Authentication Middleware
218
+
219
+ ```javascript
220
+ const router = new Router();
221
+
222
+ // Add user to context
223
+ router.use(async function* (request, context) {
224
+ const token = request.headers.get('Authorization')?.replace('Bearer ', '');
225
+ if (token) {
226
+ context.user = await verifyToken(token);
227
+ }
228
+ return yield request;
229
+ });
230
+
231
+ // Protected route
232
+ router.get('/api/profile', async (request, context) => {
233
+ if (!context.user) {
234
+ return new Response('Unauthorized', { status: 401 });
235
+ }
236
+ return Response.json(context.user);
237
+ });
238
+ ```
239
+
240
+ ### Subrouter Mounting
241
+
242
+ ```javascript
243
+ // API subrouter
244
+ const apiRouter = new Router();
245
+ apiRouter.get('/users', getUsersHandler);
246
+ apiRouter.get('/posts', getPostsHandler);
247
+
248
+ // Main router
249
+ const mainRouter = new Router();
250
+ mainRouter.mount('/api/v1', apiRouter);
251
+ // Routes become: /api/v1/users, /api/v1/posts
252
+ ```
253
+
254
+ ## License
255
+
256
+ MIT
package/package.json CHANGED
@@ -1,11 +1,23 @@
1
1
  {
2
2
  "name": "@b9g/router",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
+ "description": "Universal request router for ServiceWorker applications. Built on web standards with cache-aware routing and generator-based middleware.",
5
+ "keywords": [
6
+ "router",
7
+ "serviceworker",
8
+ "middleware",
9
+ "cache",
10
+ "universal",
11
+ "web-standards",
12
+ "urlpattern",
13
+ "shovel"
14
+ ],
4
15
  "dependencies": {
5
- "@b9g/match-pattern": "^0.1.3"
16
+ "@b9g/match-pattern": "^0.1.7"
6
17
  },
7
18
  "devDependencies": {
8
- "@b9g/libuild": "^0.1.10"
19
+ "@b9g/libuild": "^0.1.11",
20
+ "bun-types": "latest"
9
21
  },
10
22
  "type": "module",
11
23
  "types": "src/index.d.ts",
@@ -23,14 +35,6 @@
23
35
  "types": "./src/index.d.ts",
24
36
  "import": "./src/index.js"
25
37
  },
26
- "./router": {
27
- "types": "./src/router.d.ts",
28
- "import": "./src/router.js"
29
- },
30
- "./router.js": {
31
- "types": "./src/router.d.ts",
32
- "import": "./src/router.js"
33
- },
34
38
  "./package.json": "./package.json"
35
39
  }
36
40
  }
package/src/index.d.ts CHANGED
@@ -3,10 +3,188 @@
3
3
  *
4
4
  * Features:
5
5
  * - Pure Request/Response routing (works anywhere)
6
- * - Middleware chain with next() continuation
7
6
  * - Chainable route builder API
8
- * - Integration with @b9g/match-pattern for enhanced URL matching
9
- * - Prepared for future cache-first architecture
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
10
13
  */
11
- export { Router } from "./router.js";
12
- export type { Handler, Middleware, RouteContext, HttpMethod, RouterOptions, RouteConfig, RouteCacheConfig, } from "./_types.js";
14
+ /**
15
+ * Context object passed to handlers and middleware
16
+ * Contains route parameters extracted from URL pattern matching
17
+ */
18
+ export interface RouteContext {
19
+ /** Route parameters extracted from URL pattern matching */
20
+ params: Record<string, string>;
21
+ /** Middleware can add arbitrary properties for context sharing */
22
+ [key: string]: any;
23
+ }
24
+ /**
25
+ * Handler function signature - terminal response producer
26
+ * Handlers are terminal - must return a Response
27
+ */
28
+ export type Handler = (request: Request, context: RouteContext) => Response | Promise<Response>;
29
+ /**
30
+ * Generator middleware signature - uses yield for continuation
31
+ * Provides clean syntax and eliminates control flow bugs
32
+ */
33
+ export type GeneratorMiddleware = (request: Request, context: RouteContext) => AsyncGenerator<Request, Response | null | undefined, Response>;
34
+ /**
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)
39
+ */
40
+ export type FunctionMiddleware = (request: Request, context: RouteContext) => null | undefined | Response | Promise<null | undefined | Response>;
41
+ /**
42
+ * Union type for all supported middleware types
43
+ * Framework automatically detects type and executes appropriately
44
+ */
45
+ export type Middleware = GeneratorMiddleware | FunctionMiddleware;
46
+ /**
47
+ * HTTP methods supported by the router
48
+ */
49
+ export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS";
50
+ /**
51
+ * Route configuration options
52
+ */
53
+ export interface RouteConfig {
54
+ /** URL pattern for the route */
55
+ pattern: string;
56
+ }
57
+ /**
58
+ * Internal route entry stored by the router
59
+ */
60
+ interface RouteEntry {
61
+ pattern: import("@b9g/match-pattern").MatchPattern;
62
+ method: string;
63
+ handler: Handler;
64
+ }
65
+ /**
66
+ * Internal middleware entry stored by the router
67
+ */
68
+ interface MiddlewareEntry {
69
+ middleware: Middleware;
70
+ }
71
+ /**
72
+ * RouteBuilder provides a chainable API for defining routes with multiple HTTP methods
73
+ *
74
+ * Example:
75
+ * router.route('/api/users/:id')
76
+ * .get(getUserHandler)
77
+ * .put(updateUserHandler)
78
+ * .delete(deleteUserHandler);
79
+ */
80
+ declare class RouteBuilder {
81
+ #private;
82
+ constructor(router: Router, pattern: string);
83
+ /**
84
+ * Register a GET handler for this route pattern
85
+ */
86
+ get(handler: Handler): RouteBuilder;
87
+ /**
88
+ * Register a POST handler for this route pattern
89
+ */
90
+ post(handler: Handler): RouteBuilder;
91
+ /**
92
+ * Register a PUT handler for this route pattern
93
+ */
94
+ put(handler: Handler): RouteBuilder;
95
+ /**
96
+ * Register a DELETE handler for this route pattern
97
+ */
98
+ delete(handler: Handler): RouteBuilder;
99
+ /**
100
+ * Register a PATCH handler for this route pattern
101
+ */
102
+ patch(handler: Handler): RouteBuilder;
103
+ /**
104
+ * Register a HEAD handler for this route pattern
105
+ */
106
+ head(handler: Handler): RouteBuilder;
107
+ /**
108
+ * Register an OPTIONS handler for this route pattern
109
+ */
110
+ options(handler: Handler): RouteBuilder;
111
+ /**
112
+ * Register a handler for all HTTP methods on this route pattern
113
+ */
114
+ all(handler: Handler): RouteBuilder;
115
+ }
116
+ /**
117
+ * Router provides Request/Response routing with middleware support
118
+ * Designed to work universally across all JavaScript runtimes
119
+ */
120
+ export declare class Router {
121
+ #private;
122
+ constructor();
123
+ /**
124
+ * Register middleware that applies to all routes
125
+ * Middleware executes in the order it was registered
126
+ */
127
+ use(middleware: Middleware): void;
128
+ /**
129
+ * Register a handler for a specific pattern
130
+ */
131
+ use(pattern: string, handler: Handler): void;
132
+ /**
133
+ * Create a route builder for the given pattern
134
+ * Returns a chainable interface for registering HTTP method handlers
135
+ *
136
+ * Example:
137
+ * router.route('/api/users/:id')
138
+ * .get(getUserHandler)
139
+ * .put(updateUserHandler);
140
+ */
141
+ route(pattern: string): RouteBuilder;
142
+ route(config: RouteConfig): RouteBuilder;
143
+ /**
144
+ * Internal method called by RouteBuilder to register routes
145
+ * Public for RouteBuilder access, but not intended for direct use
146
+ */
147
+ addRoute(method: HttpMethod, pattern: string, handler: Handler): void;
148
+ /**
149
+ * Handle a request - main entrypoint for ServiceWorker usage
150
+ * Returns a response or throws if no route matches
151
+ */
152
+ handler: (request: Request) => Promise<Response>;
153
+ /**
154
+ * Match a request against registered routes and execute the handler chain
155
+ * Returns the response from the matched handler, or null if no route matches
156
+ * Note: Global middleware executes even if no route matches
157
+ */
158
+ match(request: Request): Promise<Response | null>;
159
+ /**
160
+ * Get registered routes for debugging/introspection
161
+ */
162
+ getRoutes(): RouteEntry[];
163
+ /**
164
+ * Get registered middleware for debugging/introspection
165
+ */
166
+ getMiddlewares(): MiddlewareEntry[];
167
+ /**
168
+ * Mount a subrouter at a specific path prefix
169
+ * All routes from the subrouter will be prefixed with the mount path
170
+ *
171
+ * Example:
172
+ * const apiRouter = new Router();
173
+ * apiRouter.route('/users').get(getUsersHandler);
174
+ * apiRouter.route('/users/:id').get(getUserHandler);
175
+ *
176
+ * const mainRouter = new Router();
177
+ * mainRouter.mount('/api/v1', apiRouter);
178
+ * // Routes become: /api/v1/users, /api/v1/users/:id
179
+ */
180
+ mount(mountPath: string, subrouter: Router): void;
181
+ /**
182
+ * Get route statistics
183
+ */
184
+ getStats(): {
185
+ routeCount: number;
186
+ middlewareCount: number;
187
+ compiled: boolean;
188
+ };
189
+ }
190
+ export {};