@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 +256 -0
- package/package.json +15 -11
- package/src/index.d.ts +183 -5
- package/src/index.js +646 -1
- package/src/router.d.ts +0 -167
- package/src/router.js +0 -504
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
|
+
"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.
|
|
16
|
+
"@b9g/match-pattern": "^0.1.7"
|
|
6
17
|
},
|
|
7
18
|
"devDependencies": {
|
|
8
|
-
"@b9g/libuild": "^0.1.
|
|
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
|
-
* -
|
|
9
|
-
* -
|
|
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
|
-
|
|
12
|
-
|
|
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 {};
|