@b9g/router 0.1.5 → 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/src/router.d.ts DELETED
@@ -1,167 +0,0 @@
1
- import type { Handler, Middleware, RouteEntry, MiddlewareEntry, HttpMethod, RouterOptions, RouteConfig, RouteCacheConfig } from "./_types.js";
2
- /**
3
- * RouteBuilder provides a chainable API for defining routes with multiple HTTP methods
4
- *
5
- * Example:
6
- * router.route('/api/users/:id')
7
- * .get(getUserHandler)
8
- * .put(updateUserHandler)
9
- * .delete(deleteUserHandler);
10
- */
11
- declare class RouteBuilder {
12
- private router;
13
- private pattern;
14
- private cacheConfig?;
15
- constructor(router: Router, pattern: string, cacheConfig?: RouteCacheConfig);
16
- /**
17
- * Register a GET handler for this route pattern
18
- */
19
- get(handler: Handler): RouteBuilder;
20
- /**
21
- * Register a POST handler for this route pattern
22
- */
23
- post(handler: Handler): RouteBuilder;
24
- /**
25
- * Register a PUT handler for this route pattern
26
- */
27
- put(handler: Handler): RouteBuilder;
28
- /**
29
- * Register a DELETE handler for this route pattern
30
- */
31
- delete(handler: Handler): RouteBuilder;
32
- /**
33
- * Register a PATCH handler for this route pattern
34
- */
35
- patch(handler: Handler): RouteBuilder;
36
- /**
37
- * Register a HEAD handler for this route pattern
38
- */
39
- head(handler: Handler): RouteBuilder;
40
- /**
41
- * Register an OPTIONS handler for this route pattern
42
- */
43
- options(handler: Handler): RouteBuilder;
44
- /**
45
- * Register a handler for all HTTP methods on this route pattern
46
- */
47
- all(handler: Handler): RouteBuilder;
48
- }
49
- /**
50
- * Router provides Request/Response routing with middleware support
51
- * Designed to work universally across all JavaScript runtimes
52
- */
53
- export declare class Router {
54
- private routes;
55
- private middlewares;
56
- private executor;
57
- private dirty;
58
- private caches?;
59
- constructor(options?: RouterOptions);
60
- /**
61
- * Register middleware that applies to all routes
62
- * Middleware executes in the order it was registered
63
- */
64
- use(middleware: Middleware): void;
65
- /**
66
- * Register a handler for a specific pattern
67
- */
68
- use(pattern: string, handler: Handler): void;
69
- /**
70
- * Create a route builder for the given pattern
71
- * Returns a chainable interface for registering HTTP method handlers
72
- *
73
- * Example:
74
- * router.route('/api/users/:id')
75
- * .get(getUserHandler)
76
- * .put(updateUserHandler);
77
- *
78
- * With cache configuration:
79
- * router.route({ pattern: '/api/users/:id', cache: { name: 'users' } })
80
- * .get(getUserHandler);
81
- */
82
- route(pattern: string): RouteBuilder;
83
- route(config: RouteConfig): RouteBuilder;
84
- /**
85
- * Internal method called by RouteBuilder to register routes
86
- * Public for RouteBuilder access, but not intended for direct use
87
- */
88
- addRoute(method: HttpMethod, pattern: string, handler: Handler, cache?: RouteCacheConfig): void;
89
- /**
90
- * Handle a request - main entrypoint for ServiceWorker usage
91
- * Returns a response or throws if no route matches
92
- */
93
- handler: (request: Request) => Promise<Response>;
94
- /**
95
- * Match a request against registered routes and execute the handler chain
96
- * Returns the response from the matched handler, or null if no route matches
97
- * Note: Global middleware executes even if no route matches
98
- */
99
- match(request: Request): Promise<Response | null>;
100
- /**
101
- * Build the complete route context including cache access
102
- */
103
- private buildContext;
104
- /**
105
- * Get registered routes for debugging/introspection
106
- */
107
- getRoutes(): RouteEntry[];
108
- /**
109
- * Get registered middleware for debugging/introspection
110
- */
111
- getMiddlewares(): MiddlewareEntry[];
112
- /**
113
- * Mount a subrouter at a specific path prefix
114
- * All routes from the subrouter will be prefixed with the mount path
115
- *
116
- * Example:
117
- * const apiRouter = new Router();
118
- * apiRouter.route('/users').get(getUsersHandler);
119
- * apiRouter.route('/users/:id').get(getUserHandler);
120
- *
121
- * const mainRouter = new Router();
122
- * mainRouter.mount('/api/v1', apiRouter);
123
- * // Routes become: /api/v1/users, /api/v1/users/:id
124
- */
125
- mount(mountPath: string, subrouter: Router): void;
126
- /**
127
- * Normalize mount path: ensure it starts with / and doesn't end with /
128
- */
129
- private normalizeMountPath;
130
- /**
131
- * Combine mount path with route pattern
132
- */
133
- private combinePaths;
134
- /**
135
- * Validate that a function is valid middleware
136
- */
137
- private isValidMiddleware;
138
- /**
139
- * Detect if a function is a generator middleware
140
- */
141
- private isGeneratorMiddleware;
142
- /**
143
- * Execute middleware stack with guaranteed execution using Rack-style LIFO order
144
- */
145
- private executeMiddlewareStack;
146
- /**
147
- * Handle errors by trying generators in reverse order
148
- */
149
- private handleErrorThroughGenerators;
150
- /**
151
- * Create a mutable request wrapper that allows URL modification
152
- */
153
- private createMutableRequest;
154
- /**
155
- * Handle automatic redirects when URL is modified
156
- */
157
- private handleAutomaticRedirect;
158
- /**
159
- * Get route statistics
160
- */
161
- getStats(): {
162
- routeCount: number;
163
- middlewareCount: number;
164
- compiled: boolean;
165
- };
166
- }
167
- export {};
package/src/router.js DELETED
@@ -1,504 +0,0 @@
1
- /// <reference types="./router.d.ts" />
2
- // src/router.ts
3
- import { MatchPattern } from "@b9g/match-pattern";
4
- var LinearExecutor = class {
5
- constructor(routes) {
6
- this.routes = routes;
7
- }
8
- /**
9
- * Find the first route that matches the request
10
- * Returns null if no route matches
11
- */
12
- match(request) {
13
- const url = new URL(request.url);
14
- const method = request.method.toUpperCase();
15
- for (const route of this.routes) {
16
- if (route.method !== method) {
17
- continue;
18
- }
19
- if (route.pattern.test(url)) {
20
- const result = route.pattern.exec(url);
21
- if (result) {
22
- return {
23
- handler: route.handler,
24
- context: {
25
- params: result.params
26
- },
27
- cacheConfig: route.cache
28
- };
29
- }
30
- }
31
- }
32
- return null;
33
- }
34
- };
35
- var RouteBuilder = class {
36
- constructor(router, pattern, cacheConfig) {
37
- this.router = router;
38
- this.pattern = pattern;
39
- this.cacheConfig = cacheConfig;
40
- }
41
- /**
42
- * Register a GET handler for this route pattern
43
- */
44
- get(handler) {
45
- this.router.addRoute("GET", this.pattern, handler, this.cacheConfig);
46
- return this;
47
- }
48
- /**
49
- * Register a POST handler for this route pattern
50
- */
51
- post(handler) {
52
- this.router.addRoute("POST", this.pattern, handler, this.cacheConfig);
53
- return this;
54
- }
55
- /**
56
- * Register a PUT handler for this route pattern
57
- */
58
- put(handler) {
59
- this.router.addRoute("PUT", this.pattern, handler, this.cacheConfig);
60
- return this;
61
- }
62
- /**
63
- * Register a DELETE handler for this route pattern
64
- */
65
- delete(handler) {
66
- this.router.addRoute("DELETE", this.pattern, handler, this.cacheConfig);
67
- return this;
68
- }
69
- /**
70
- * Register a PATCH handler for this route pattern
71
- */
72
- patch(handler) {
73
- this.router.addRoute("PATCH", this.pattern, handler, this.cacheConfig);
74
- return this;
75
- }
76
- /**
77
- * Register a HEAD handler for this route pattern
78
- */
79
- head(handler) {
80
- this.router.addRoute("HEAD", this.pattern, handler, this.cacheConfig);
81
- return this;
82
- }
83
- /**
84
- * Register an OPTIONS handler for this route pattern
85
- */
86
- options(handler) {
87
- this.router.addRoute("OPTIONS", this.pattern, handler, this.cacheConfig);
88
- return this;
89
- }
90
- /**
91
- * Register a handler for all HTTP methods on this route pattern
92
- */
93
- all(handler) {
94
- const methods = [
95
- "GET",
96
- "POST",
97
- "PUT",
98
- "DELETE",
99
- "PATCH",
100
- "HEAD",
101
- "OPTIONS"
102
- ];
103
- methods.forEach((method) => {
104
- this.router.addRoute(method, this.pattern, handler, this.cacheConfig);
105
- });
106
- return this;
107
- }
108
- };
109
- var Router = class {
110
- routes = [];
111
- middlewares = [];
112
- executor = null;
113
- dirty = false;
114
- caches;
115
- constructor(options) {
116
- this.caches = options?.caches;
117
- }
118
- use(patternOrMiddleware, handler) {
119
- if (typeof patternOrMiddleware === "string" && handler) {
120
- this.addRoute("GET", patternOrMiddleware, handler);
121
- this.addRoute("POST", patternOrMiddleware, handler);
122
- this.addRoute("PUT", patternOrMiddleware, handler);
123
- this.addRoute("DELETE", patternOrMiddleware, handler);
124
- this.addRoute("PATCH", patternOrMiddleware, handler);
125
- this.addRoute("HEAD", patternOrMiddleware, handler);
126
- this.addRoute("OPTIONS", patternOrMiddleware, handler);
127
- } else if (typeof patternOrMiddleware === "function") {
128
- if (!this.isValidMiddleware(patternOrMiddleware)) {
129
- throw new Error(
130
- "Invalid middleware type. Must be function or async generator function."
131
- );
132
- }
133
- this.middlewares.push({ middleware: patternOrMiddleware });
134
- this.dirty = true;
135
- } else {
136
- throw new Error(
137
- "Invalid middleware type. Must be function or async generator function."
138
- );
139
- }
140
- }
141
- route(patternOrConfig) {
142
- if (typeof patternOrConfig === "string") {
143
- return new RouteBuilder(this, patternOrConfig);
144
- } else {
145
- return new RouteBuilder(
146
- this,
147
- patternOrConfig.pattern,
148
- patternOrConfig.cache
149
- );
150
- }
151
- }
152
- /**
153
- * Internal method called by RouteBuilder to register routes
154
- * Public for RouteBuilder access, but not intended for direct use
155
- */
156
- addRoute(method, pattern, handler, cache) {
157
- const matchPattern = new MatchPattern(pattern);
158
- this.routes.push({
159
- pattern: matchPattern,
160
- method: method.toUpperCase(),
161
- handler,
162
- cache
163
- });
164
- this.dirty = true;
165
- }
166
- /**
167
- * Handle a request - main entrypoint for ServiceWorker usage
168
- * Returns a response or throws if no route matches
169
- */
170
- handler = async (request) => {
171
- if (this.dirty || !this.executor) {
172
- this.executor = new LinearExecutor(this.routes);
173
- this.dirty = false;
174
- }
175
- const matchResult = this.executor.match(request);
176
- if (matchResult) {
177
- const context = await this.buildContext(
178
- matchResult.context,
179
- matchResult.cacheConfig
180
- );
181
- const mutableRequest = this.createMutableRequest(request);
182
- return this.executeMiddlewareStack(
183
- this.middlewares,
184
- mutableRequest,
185
- context,
186
- matchResult.handler,
187
- request.url,
188
- this.executor
189
- );
190
- } else {
191
- const notFoundHandler = async () => {
192
- return new Response("Not Found", { status: 404 });
193
- };
194
- const mutableRequest = this.createMutableRequest(request);
195
- return this.executeMiddlewareStack(
196
- this.middlewares,
197
- mutableRequest,
198
- { params: {} },
199
- notFoundHandler,
200
- request.url,
201
- this.executor
202
- );
203
- }
204
- };
205
- /**
206
- * Match a request against registered routes and execute the handler chain
207
- * Returns the response from the matched handler, or null if no route matches
208
- * Note: Global middleware executes even if no route matches
209
- */
210
- async match(request) {
211
- if (this.dirty || !this.executor) {
212
- this.executor = new LinearExecutor(this.routes);
213
- this.dirty = false;
214
- }
215
- const mutableRequest = this.createMutableRequest(request);
216
- const originalUrl = mutableRequest.url;
217
- let matchResult = this.executor.match(request);
218
- let handler;
219
- let context;
220
- if (matchResult) {
221
- handler = matchResult.handler;
222
- context = await this.buildContext(
223
- matchResult.context,
224
- matchResult.cacheConfig
225
- );
226
- } else {
227
- handler = async () => new Response("Not Found", { status: 404 });
228
- context = { params: {} };
229
- }
230
- const response = await this.executeMiddlewareStack(
231
- this.middlewares,
232
- mutableRequest,
233
- context,
234
- handler,
235
- originalUrl,
236
- this.executor
237
- // Pass executor for re-routing
238
- );
239
- if (!matchResult && response?.status === 404) {
240
- return null;
241
- }
242
- return response;
243
- }
244
- /**
245
- * Build the complete route context including cache access
246
- */
247
- async buildContext(baseContext, cacheConfig) {
248
- const context = { ...baseContext };
249
- if (this.caches) {
250
- context.caches = this.caches;
251
- if (cacheConfig?.name) {
252
- try {
253
- context.cache = await this.caches.open(cacheConfig.name);
254
- } catch (error) {
255
- console.warn(`Failed to open cache '${cacheConfig.name}':`, error);
256
- }
257
- }
258
- }
259
- return context;
260
- }
261
- /**
262
- * Get registered routes for debugging/introspection
263
- */
264
- getRoutes() {
265
- return [...this.routes];
266
- }
267
- /**
268
- * Get registered middleware for debugging/introspection
269
- */
270
- getMiddlewares() {
271
- return [...this.middlewares];
272
- }
273
- /**
274
- * Mount a subrouter at a specific path prefix
275
- * All routes from the subrouter will be prefixed with the mount path
276
- *
277
- * Example:
278
- * const apiRouter = new Router();
279
- * apiRouter.route('/users').get(getUsersHandler);
280
- * apiRouter.route('/users/:id').get(getUserHandler);
281
- *
282
- * const mainRouter = new Router();
283
- * mainRouter.mount('/api/v1', apiRouter);
284
- * // Routes become: /api/v1/users, /api/v1/users/:id
285
- */
286
- mount(mountPath, subrouter) {
287
- const normalizedMountPath = this.normalizeMountPath(mountPath);
288
- const subroutes = subrouter.getRoutes();
289
- for (const subroute of subroutes) {
290
- const mountedPattern = this.combinePaths(
291
- normalizedMountPath,
292
- subroute.pattern.pathname
293
- );
294
- this.routes.push({
295
- pattern: new MatchPattern(mountedPattern),
296
- method: subroute.method,
297
- handler: subroute.handler,
298
- cache: subroute.cache
299
- });
300
- }
301
- const submiddlewares = subrouter.getMiddlewares();
302
- for (const submiddleware of submiddlewares) {
303
- this.middlewares.push(submiddleware);
304
- }
305
- this.dirty = true;
306
- }
307
- /**
308
- * Normalize mount path: ensure it starts with / and doesn't end with /
309
- */
310
- normalizeMountPath(mountPath) {
311
- if (!mountPath.startsWith("/")) {
312
- mountPath = "/" + mountPath;
313
- }
314
- if (mountPath.endsWith("/") && mountPath.length > 1) {
315
- mountPath = mountPath.slice(0, -1);
316
- }
317
- return mountPath;
318
- }
319
- /**
320
- * Combine mount path with route pattern
321
- */
322
- combinePaths(mountPath, routePattern) {
323
- if (routePattern === "/") {
324
- return mountPath;
325
- }
326
- if (!routePattern.startsWith("/")) {
327
- routePattern = "/" + routePattern;
328
- }
329
- return mountPath + routePattern;
330
- }
331
- /**
332
- * Validate that a function is valid middleware
333
- */
334
- isValidMiddleware(middleware) {
335
- const constructorName = middleware.constructor.name;
336
- return constructorName === "AsyncGeneratorFunction" || constructorName === "AsyncFunction" || constructorName === "Function";
337
- }
338
- /**
339
- * Detect if a function is a generator middleware
340
- */
341
- isGeneratorMiddleware(middleware) {
342
- return middleware.constructor.name === "AsyncGeneratorFunction";
343
- }
344
- /**
345
- * Execute middleware stack with guaranteed execution using Rack-style LIFO order
346
- */
347
- async executeMiddlewareStack(middlewares, request, context, handler, originalUrl, executor) {
348
- const runningGenerators = [];
349
- let currentResponse = null;
350
- for (let i = 0; i < middlewares.length; i++) {
351
- const middleware = middlewares[i].middleware;
352
- if (this.isGeneratorMiddleware(middleware)) {
353
- const generator = middleware(request, context);
354
- const result = await generator.next();
355
- if (result.done) {
356
- if (result.value) {
357
- currentResponse = result.value;
358
- break;
359
- }
360
- } else {
361
- runningGenerators.push({ generator, index: i });
362
- }
363
- } else {
364
- const result = await middleware(request, context);
365
- if (result) {
366
- currentResponse = result;
367
- break;
368
- }
369
- }
370
- }
371
- if (!currentResponse) {
372
- let finalHandler = handler;
373
- let finalContext = context;
374
- if (request.url !== originalUrl && executor) {
375
- const newMatchResult = executor.match(
376
- new Request(request.url, {
377
- method: request.method,
378
- headers: request.headers,
379
- body: request.body
380
- })
381
- );
382
- if (newMatchResult) {
383
- finalHandler = newMatchResult.handler;
384
- finalContext = await this.buildContext(
385
- newMatchResult.context,
386
- newMatchResult.cacheConfig || void 0
387
- );
388
- }
389
- }
390
- let handlerError = null;
391
- try {
392
- currentResponse = await finalHandler(request, finalContext);
393
- } catch (error) {
394
- handlerError = error;
395
- }
396
- if (handlerError) {
397
- currentResponse = await this.handleErrorThroughGenerators(
398
- handlerError,
399
- runningGenerators
400
- );
401
- }
402
- }
403
- if (request.url !== originalUrl && currentResponse) {
404
- currentResponse = this.handleAutomaticRedirect(
405
- originalUrl,
406
- request.url,
407
- request.method
408
- );
409
- }
410
- for (let i = runningGenerators.length - 1; i >= 0; i--) {
411
- const { generator } = runningGenerators[i];
412
- const result = await generator.next(currentResponse);
413
- if (result.value) {
414
- currentResponse = result.value;
415
- }
416
- }
417
- return currentResponse;
418
- }
419
- /**
420
- * Handle errors by trying generators in reverse order
421
- */
422
- async handleErrorThroughGenerators(error, runningGenerators) {
423
- for (let i = runningGenerators.length - 1; i >= 0; i--) {
424
- const { generator } = runningGenerators[i];
425
- try {
426
- const result = await generator.throw(error);
427
- if (result.value) {
428
- runningGenerators.splice(i, 1);
429
- return result.value;
430
- }
431
- } catch (generatorError) {
432
- runningGenerators.splice(i, 1);
433
- continue;
434
- }
435
- }
436
- throw error;
437
- }
438
- /**
439
- * Create a mutable request wrapper that allows URL modification
440
- */
441
- createMutableRequest(request) {
442
- return {
443
- url: request.url,
444
- method: request.method,
445
- headers: new Headers(request.headers),
446
- body: request.body,
447
- bodyUsed: request.bodyUsed,
448
- cache: request.cache,
449
- credentials: request.credentials,
450
- destination: request.destination,
451
- integrity: request.integrity,
452
- keepalive: request.keepalive,
453
- mode: request.mode,
454
- redirect: request.redirect,
455
- referrer: request.referrer,
456
- referrerPolicy: request.referrerPolicy,
457
- signal: request.signal,
458
- // Add all other Request methods
459
- arrayBuffer: () => request.arrayBuffer(),
460
- blob: () => request.blob(),
461
- clone: () => request.clone(),
462
- formData: () => request.formData(),
463
- json: () => request.json(),
464
- text: () => request.text()
465
- };
466
- }
467
- /**
468
- * Handle automatic redirects when URL is modified
469
- */
470
- handleAutomaticRedirect(originalUrl, newUrl, method) {
471
- const originalURL = new URL(originalUrl);
472
- const newURL = new URL(newUrl);
473
- if (originalURL.hostname !== newURL.hostname || originalURL.port !== newURL.port && originalURL.port !== "" && newURL.port !== "") {
474
- throw new Error(
475
- `Cross-origin redirect not allowed: ${originalUrl} -> ${newUrl}`
476
- );
477
- }
478
- let status = 302;
479
- if (originalURL.protocol !== newURL.protocol) {
480
- status = 301;
481
- } else if (method.toUpperCase() !== "GET") {
482
- status = 307;
483
- }
484
- return new Response(null, {
485
- status,
486
- headers: {
487
- Location: newUrl
488
- }
489
- });
490
- }
491
- /**
492
- * Get route statistics
493
- */
494
- getStats() {
495
- return {
496
- routeCount: this.routes.length,
497
- middlewareCount: this.middlewares.length,
498
- compiled: !this.dirty && this.executor !== null
499
- };
500
- }
501
- };
502
- export {
503
- Router
504
- };