@koa/router 15.3.2 → 15.5.0
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 +181 -14
- package/dist/index.d.mts +123 -3
- package/dist/index.d.ts +123 -3
- package/dist/index.js +145 -8
- package/dist/index.mjs +144 -8
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -192,10 +192,15 @@ import {
|
|
|
192
192
|
RouterOptions,
|
|
193
193
|
RouterMiddleware,
|
|
194
194
|
RouterParameterMiddleware,
|
|
195
|
-
RouterParamContext,
|
|
196
195
|
AllowedMethodsOptions,
|
|
197
196
|
UrlOptions,
|
|
198
|
-
HttpMethod
|
|
197
|
+
HttpMethod,
|
|
198
|
+
MatchResult,
|
|
199
|
+
LayerOptions,
|
|
200
|
+
Layer,
|
|
201
|
+
RouterEvent,
|
|
202
|
+
RouterEventSelector,
|
|
203
|
+
RouterEvents
|
|
199
204
|
} from '@koa/router';
|
|
200
205
|
import type { Next } from 'koa';
|
|
201
206
|
|
|
@@ -261,14 +266,14 @@ Create a new router instance.
|
|
|
261
266
|
|
|
262
267
|
**Options:**
|
|
263
268
|
|
|
264
|
-
| Option | Type | Description
|
|
265
|
-
| ----------- | ------------------------------ |
|
|
266
|
-
| `prefix` | `string` | Prefix all routes with this path
|
|
267
|
-
| `exclusive` | `boolean`
|
|
268
|
-
| `host` | `string \| string[] \| RegExp` | Match routes only for this hostname(s)
|
|
269
|
-
| `methods` | `string[]` | Custom HTTP methods to support
|
|
270
|
-
| `sensitive` | `boolean` | Enable case-sensitive routing
|
|
271
|
-
| `strict` | `boolean` | Require trailing slashes
|
|
269
|
+
| Option | Type | Description |
|
|
270
|
+
| ----------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
271
|
+
| `prefix` | `string` | Prefix all routes with this path |
|
|
272
|
+
| `exclusive` | `boolean \| 'specificity'` | `true`: only run the last-registered matching route. `'specificity'`: only run the route with the fewest path parameters (OpenAPI-compliant). Omit to run all matching routes. |
|
|
273
|
+
| `host` | `string \| string[] \| RegExp` | Match routes only for this hostname(s) |
|
|
274
|
+
| `methods` | `string[]` | Custom HTTP methods to support |
|
|
275
|
+
| `sensitive` | `boolean` | Enable case-sensitive routing |
|
|
276
|
+
| `strict` | `boolean` | Require trailing slashes |
|
|
272
277
|
|
|
273
278
|
**Example:**
|
|
274
279
|
|
|
@@ -740,6 +745,56 @@ const url = Router.url('/users/:id', { id: 1, name: 'John' });
|
|
|
740
745
|
// => "/users/1"
|
|
741
746
|
```
|
|
742
747
|
|
|
748
|
+
### router.on() (experimental)
|
|
749
|
+
|
|
750
|
+
Register an event handler on the router for lifecycle events.
|
|
751
|
+
|
|
752
|
+
**Signature:**
|
|
753
|
+
|
|
754
|
+
```typescript
|
|
755
|
+
router.on(event: RouterEventSelector, handler: RouterMiddleware): Router
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
**Parameters:**
|
|
759
|
+
|
|
760
|
+
| Parameter | Type | Description |
|
|
761
|
+
| --------- | --------------------- | ---------------------------------------------------------------- |
|
|
762
|
+
| `event` | `RouterEventSelector` | Event name string, `RouterEvents` constant, or selector function |
|
|
763
|
+
| `handler` | `RouterMiddleware` | Middleware function `(ctx, next) => {}` |
|
|
764
|
+
|
|
765
|
+
**Returns:** Router instance for chaining.
|
|
766
|
+
|
|
767
|
+
**Active events:**
|
|
768
|
+
|
|
769
|
+
| Event | Constant | Description |
|
|
770
|
+
| ------------- | ----------------------- | ----------------------------------------- |
|
|
771
|
+
| `'not-found'` | `RouterEvents.NotFound` | Fires when no route matched path + method |
|
|
772
|
+
|
|
773
|
+
**Example:**
|
|
774
|
+
|
|
775
|
+
```javascript
|
|
776
|
+
import { RouterEvents } from '@koa/router';
|
|
777
|
+
|
|
778
|
+
// All three forms are equivalent:
|
|
779
|
+
router.on(RouterEvents.NotFound, handler); // constant (recommended)
|
|
780
|
+
router.on((events) => events.NotFound, handler); // selector function
|
|
781
|
+
router.on('not-found', handler); // raw string
|
|
782
|
+
|
|
783
|
+
// Handlers can be chained:
|
|
784
|
+
router
|
|
785
|
+
.on(RouterEvents.NotFound, async (ctx, next) => {
|
|
786
|
+
console.log('no route matched:', ctx.path);
|
|
787
|
+
await next();
|
|
788
|
+
})
|
|
789
|
+
.on(RouterEvents.NotFound, (ctx) => {
|
|
790
|
+
ctx.status = 404;
|
|
791
|
+
ctx.body = { error: 'Not Found' };
|
|
792
|
+
});
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
> **Note:** This API is experimental and may change in future versions. See the
|
|
796
|
+
> [Not Found Event](#not-found-event-experimental) section in Advanced Features for details.
|
|
797
|
+
|
|
743
798
|
## Advanced Features
|
|
744
799
|
|
|
745
800
|
### Host Matching
|
|
@@ -842,7 +897,8 @@ router.get('/post/:id', handler);
|
|
|
842
897
|
|
|
843
898
|
### Catch-All Routes
|
|
844
899
|
|
|
845
|
-
Create a catch-all route that
|
|
900
|
+
Create a catch-all route that runs when no specific route handler has set a response.
|
|
901
|
+
Check `!ctx.body` to determine whether a previous handler already responded:
|
|
846
902
|
|
|
847
903
|
```javascript
|
|
848
904
|
router.get('/users', handler1);
|
|
@@ -850,13 +906,38 @@ router.get('/posts', handler2);
|
|
|
850
906
|
|
|
851
907
|
// Catch-all for unmatched routes
|
|
852
908
|
router.all('{/*rest}', (ctx) => {
|
|
853
|
-
if (!ctx.
|
|
909
|
+
if (!ctx.body) {
|
|
854
910
|
ctx.status = 404;
|
|
855
911
|
ctx.body = { error: 'Not Found' };
|
|
856
912
|
}
|
|
857
913
|
});
|
|
858
914
|
```
|
|
859
915
|
|
|
916
|
+
> **Note:** `ctx.matched.length === 0` will never be true inside a catch-all handler.
|
|
917
|
+
> The router populates `ctx.matched` **before** the handler body runs, so the catch-all
|
|
918
|
+
> layer (`{/*rest}`) is already included in the array by the time your code executes.
|
|
919
|
+
>
|
|
920
|
+
> **Recommended:** Use `!ctx.body` as shown above — it is the simplest and most reliable
|
|
921
|
+
> condition inside a catch-all route.
|
|
922
|
+
>
|
|
923
|
+
> **Workaround:** If you specifically need `ctx.matched`-based logic inside a catch-all,
|
|
924
|
+
> filter out the catch-all layer first and check the remaining array:
|
|
925
|
+
>
|
|
926
|
+
> ```javascript
|
|
927
|
+
> router.all('{/*rest}', (ctx) => {
|
|
928
|
+
> const realMatches = ctx.matched.filter(
|
|
929
|
+
> (layer) => layer.path !== '{/*rest}'
|
|
930
|
+
> );
|
|
931
|
+
> if (realMatches.length === 0) {
|
|
932
|
+
> ctx.status = 404;
|
|
933
|
+
> ctx.body = { error: 'Not Found' };
|
|
934
|
+
> }
|
|
935
|
+
> });
|
|
936
|
+
> ```
|
|
937
|
+
>
|
|
938
|
+
> For cleaner and more precise 404 detection, prefer app-level middleware with
|
|
939
|
+
> `ctx.routeMatched` — see the [404 Handling](#404-handling) section.
|
|
940
|
+
|
|
860
941
|
### Array of Paths
|
|
861
942
|
|
|
862
943
|
Register multiple paths with the same middleware:
|
|
@@ -868,12 +949,30 @@ router.get(['/users', '/people'], handler);
|
|
|
868
949
|
|
|
869
950
|
### 404 Handling
|
|
870
951
|
|
|
871
|
-
Implement custom 404 handling
|
|
952
|
+
Implement custom 404 handling using app-level middleware after the router.
|
|
953
|
+
|
|
954
|
+
**Using `ctx.routeMatched` (recommended):**
|
|
955
|
+
|
|
956
|
+
```javascript
|
|
957
|
+
app.use(router.routes());
|
|
958
|
+
|
|
959
|
+
// ctx.routeMatched is set by the router before handlers run
|
|
960
|
+
app.use((ctx) => {
|
|
961
|
+
if (!ctx.routeMatched) {
|
|
962
|
+
ctx.status = 404;
|
|
963
|
+
ctx.body = {
|
|
964
|
+
error: 'Not Found',
|
|
965
|
+
path: ctx.path
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
**Using `ctx.matched`:**
|
|
872
972
|
|
|
873
973
|
```javascript
|
|
874
974
|
app.use(router.routes());
|
|
875
975
|
|
|
876
|
-
// 404 middleware - runs after router
|
|
877
976
|
app.use((ctx) => {
|
|
878
977
|
if (!ctx.matched || ctx.matched.length === 0) {
|
|
879
978
|
ctx.status = 404;
|
|
@@ -885,6 +984,69 @@ app.use((ctx) => {
|
|
|
885
984
|
});
|
|
886
985
|
```
|
|
887
986
|
|
|
987
|
+
> For a router-scoped alternative that doesn't require app-level middleware,
|
|
988
|
+
> see the [Not Found Event](#not-found-event-experimental) section below.
|
|
989
|
+
|
|
990
|
+
### Not Found Event (Experimental)
|
|
991
|
+
|
|
992
|
+
> **Note:** This API is experimental and may change in future versions.
|
|
993
|
+
|
|
994
|
+
Use `router.on()` for a clean, dedicated 404 handler that runs only when no route
|
|
995
|
+
matched — without needing a catch-all route. Three equivalent forms are supported:
|
|
996
|
+
|
|
997
|
+
```javascript
|
|
998
|
+
import { RouterEvents } from '@koa/router';
|
|
999
|
+
|
|
1000
|
+
// Named constant (recommended — autocomplete + refactor safe)
|
|
1001
|
+
router.on(RouterEvents.NotFound, handler);
|
|
1002
|
+
|
|
1003
|
+
// Selector function (fluent style)
|
|
1004
|
+
router.on((events) => events.NotFound, handler);
|
|
1005
|
+
|
|
1006
|
+
// Raw string (still accepted)
|
|
1007
|
+
router.on('not-found', handler);
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
Full example:
|
|
1011
|
+
|
|
1012
|
+
```javascript
|
|
1013
|
+
import { RouterEvents } from '@koa/router';
|
|
1014
|
+
|
|
1015
|
+
router.get('/users', handler1);
|
|
1016
|
+
router.get('/posts', handler2);
|
|
1017
|
+
|
|
1018
|
+
router.on(RouterEvents.NotFound, (ctx) => {
|
|
1019
|
+
ctx.status = 404;
|
|
1020
|
+
ctx.body = {
|
|
1021
|
+
error: 'Not Found',
|
|
1022
|
+
path: ctx.path,
|
|
1023
|
+
method: ctx.method
|
|
1024
|
+
};
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
app.use(router.routes());
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
Multiple handlers are supported and compose in registration order:
|
|
1031
|
+
|
|
1032
|
+
```javascript
|
|
1033
|
+
router.on(RouterEvents.NotFound, async (ctx, next) => {
|
|
1034
|
+
console.log('no route matched:', ctx.path);
|
|
1035
|
+
await next();
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
router.on(RouterEvents.NotFound, (ctx) => {
|
|
1039
|
+
ctx.status = 404;
|
|
1040
|
+
ctx.body = { error: 'Not Found' };
|
|
1041
|
+
});
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
Benefits over catch-all routes:
|
|
1045
|
+
|
|
1046
|
+
- Does not appear in `ctx.matched`
|
|
1047
|
+
- Only fires when no route matched — no need to check `ctx.routeMatched` or `ctx.body`
|
|
1048
|
+
- Handlers compose naturally with `next()`
|
|
1049
|
+
|
|
888
1050
|
## Best Practices
|
|
889
1051
|
|
|
890
1052
|
### 1. Use Middleware Composition
|
|
@@ -983,6 +1145,9 @@ router.get('/users/:id', (ctx: RouterContext) => {
|
|
|
983
1145
|
// All matched layers
|
|
984
1146
|
const matched = ctx.matched; // Array of Layer objects
|
|
985
1147
|
|
|
1148
|
+
// Whether any route (with HTTP methods) was matched
|
|
1149
|
+
const wasMatched = ctx.routeMatched; // boolean | undefined
|
|
1150
|
+
|
|
986
1151
|
// Captured values from RegExp routes
|
|
987
1152
|
const captures = ctx.captures; // string[] | undefined
|
|
988
1153
|
|
|
@@ -1043,11 +1208,13 @@ See the [recipes directory](./recipes/) for complete TypeScript examples:
|
|
|
1043
1208
|
- **[Authentication & Authorization](./recipes/authentication-authorization/)** - JWT-based authentication with middleware
|
|
1044
1209
|
- **[Request Validation](./recipes/request-validation/)** - Validate request data with middleware
|
|
1045
1210
|
- **[Parameter Validation](./recipes/parameter-validation/)** - Validate and transform parameters using router.param()
|
|
1211
|
+
- **[Regex Parameter Validation](./recipes/regex-parameter-validation/)** - Validate URL parameters with regex (replacement for `:param(regex)` in v14+)
|
|
1046
1212
|
- **[API Versioning](./recipes/api-versioning/)** - Implement API versioning with multiple routers
|
|
1047
1213
|
- **[Error Handling](./recipes/error-handling/)** - Centralized error handling with custom error classes
|
|
1048
1214
|
- **[Pagination](./recipes/pagination/)** - Implement pagination for list endpoints
|
|
1049
1215
|
- **[Health Checks](./recipes/health-checks/)** - Add health check endpoints for monitoring
|
|
1050
1216
|
- **[TypeScript Recipe](./recipes/typescript-recipe/)** - Full TypeScript example with types and type safety
|
|
1217
|
+
- **[Not Found Handling](./recipes/not-found-handling/)** - All approaches to handling unmatched routes: `ctx.routeMatched`, events, catch-all
|
|
1051
1218
|
|
|
1052
1219
|
Each recipe file contains complete, runnable TypeScript code that you can copy and adapt to your needs.
|
|
1053
1220
|
|
package/dist/index.d.mts
CHANGED
|
@@ -172,6 +172,47 @@ declare class Layer<StateT = DefaultState, ContextT = DefaultContext, BodyT = un
|
|
|
172
172
|
private _reconfigurePathMatching;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Named constants for active router lifecycle events.
|
|
177
|
+
*
|
|
178
|
+
* Only events that are fully implemented appear here. Planned events will be
|
|
179
|
+
* added as each one is implemented so the public API always reflects what
|
|
180
|
+
* actually works.
|
|
181
|
+
*
|
|
182
|
+
* @experimental
|
|
183
|
+
*/
|
|
184
|
+
declare const RouterEvents: {
|
|
185
|
+
/**
|
|
186
|
+
* Fires when no route matched the request path + HTTP method.
|
|
187
|
+
*/
|
|
188
|
+
readonly NotFound: "not-found";
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Union of all valid router event name strings.
|
|
192
|
+
* Derived from `RouterEvents` so the two are always in sync.
|
|
193
|
+
*
|
|
194
|
+
* @experimental
|
|
195
|
+
*/
|
|
196
|
+
type RouterEvent = (typeof RouterEvents)[keyof typeof RouterEvents];
|
|
197
|
+
/**
|
|
198
|
+
* Accepts either a raw event name string or a selector function that receives
|
|
199
|
+
* the `RouterEvents` object and returns an event name.
|
|
200
|
+
*
|
|
201
|
+
* Both forms are equivalent — choose whichever reads better in context.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* // string
|
|
206
|
+
* 'not-found'
|
|
207
|
+
*
|
|
208
|
+
* // selector function
|
|
209
|
+
* (events) => events.NotFound
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* @experimental
|
|
213
|
+
*/
|
|
214
|
+
type RouterEventSelector = RouterEvent | ((events: typeof RouterEvents) => RouterEvent);
|
|
215
|
+
|
|
175
216
|
type RouterInstance<StateT = koa.DefaultState, ContextT = koa.DefaultContext> = RouterImplementation<StateT, ContextT>;
|
|
176
217
|
/**
|
|
177
218
|
* Middleware with router property
|
|
@@ -189,6 +230,11 @@ declare class RouterImplementation<StateT = koa.DefaultState, ContextT = koa.Def
|
|
|
189
230
|
params: Record<string, RouterParameterMiddleware<StateT, ContextT> | RouterParameterMiddleware<StateT, ContextT>[]>;
|
|
190
231
|
stack: Layer<StateT, ContextT>[];
|
|
191
232
|
host?: string | string[] | RegExp;
|
|
233
|
+
/**
|
|
234
|
+
* Event emitter for router lifecycle events (experimental)
|
|
235
|
+
* @internal
|
|
236
|
+
*/
|
|
237
|
+
private _events;
|
|
192
238
|
/**
|
|
193
239
|
* Create a new router.
|
|
194
240
|
*
|
|
@@ -356,6 +402,52 @@ declare class RouterImplementation<StateT = koa.DefaultState, ContextT = koa.Def
|
|
|
356
402
|
*/
|
|
357
403
|
private _buildMiddlewareChain;
|
|
358
404
|
routes(): RouterComposedMiddleware<StateT, ContextT>;
|
|
405
|
+
/**
|
|
406
|
+
* Register an event handler on the router.
|
|
407
|
+
*
|
|
408
|
+
* @experimental This API is experimental and may change in future versions.
|
|
409
|
+
*
|
|
410
|
+
* **Active events:**
|
|
411
|
+
* - `'not-found'` — fires when no route matched path + method. Handlers are
|
|
412
|
+
* composed and called instead of `next()`, so they are responsible for
|
|
413
|
+
* calling `next()` themselves if they want to pass control downstream.
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
*
|
|
417
|
+
* All three forms are equivalent:
|
|
418
|
+
*
|
|
419
|
+
* ```javascript
|
|
420
|
+
* import { RouterEvents } from '@koa/router';
|
|
421
|
+
*
|
|
422
|
+
* // Named constant (recommended — autocomplete + refactor safe)
|
|
423
|
+
* router.on(RouterEvents.NotFound, handler);
|
|
424
|
+
*
|
|
425
|
+
* // Selector function (fluent style)
|
|
426
|
+
* router.on((events) => events.NotFound, handler);
|
|
427
|
+
*
|
|
428
|
+
* // Raw string (still accepted)
|
|
429
|
+
* router.on('not-found', handler);
|
|
430
|
+
* ```
|
|
431
|
+
*
|
|
432
|
+
* Multiple handlers compose in registration order:
|
|
433
|
+
*
|
|
434
|
+
* ```javascript
|
|
435
|
+
* router.on(RouterEvents.NotFound, async (ctx, next) => {
|
|
436
|
+
* console.log('no route matched', ctx.path);
|
|
437
|
+
* await next();
|
|
438
|
+
* });
|
|
439
|
+
* router.on(RouterEvents.NotFound, (ctx) => {
|
|
440
|
+
* ctx.status = 404;
|
|
441
|
+
* ctx.body = { error: 'Not Found' };
|
|
442
|
+
* });
|
|
443
|
+
* ```
|
|
444
|
+
*
|
|
445
|
+
* @param event - Event name string, `RouterEvents` constant, or selector
|
|
446
|
+
* function `(events) => events.NotFound` (see `RouterEventSelector`)
|
|
447
|
+
* @param handler - Middleware to run when the event fires
|
|
448
|
+
* @returns This router instance for chaining
|
|
449
|
+
*/
|
|
450
|
+
on(event: RouterEventSelector, handler: RouterMiddleware<StateT, ContextT>): Router<StateT, ContextT>;
|
|
359
451
|
/**
|
|
360
452
|
* Returns separate middleware for responding to `OPTIONS` requests with
|
|
361
453
|
* an `Allow` header containing the allowed methods, as well as responding
|
|
@@ -656,9 +748,18 @@ declare const Router: RouterConstructor;
|
|
|
656
748
|
|
|
657
749
|
type RouterOptions = {
|
|
658
750
|
/**
|
|
659
|
-
*
|
|
751
|
+
* Control which route is executed when multiple routes match a request.
|
|
752
|
+
*
|
|
753
|
+
* - `true` — run only the last registered matching route (legacy behaviour)
|
|
754
|
+
* - `'specificity'` — run only the most specific matching route, defined as
|
|
755
|
+
* the one with the fewest path parameters. This is OpenAPI-compliant
|
|
756
|
+
* (see https://spec.openapis.org/oas/v3.0.3#path-templating-matching) and
|
|
757
|
+
* is the recommended mode for code generated from OpenAPI specifications.
|
|
758
|
+
* When two routes have an equal number of parameters the last registered
|
|
759
|
+
* one wins, preserving deterministic behaviour.
|
|
760
|
+
* - `false` / omitted — all matching routes run in registration order
|
|
660
761
|
*/
|
|
661
|
-
exclusive?: boolean;
|
|
762
|
+
exclusive?: boolean | 'specificity';
|
|
662
763
|
/**
|
|
663
764
|
* Prefix for all routes
|
|
664
765
|
*/
|
|
@@ -806,6 +907,25 @@ type RouterContext<StateT = DefaultState, ContextT = DefaultContext, BodyT = unk
|
|
|
806
907
|
* Array of matched layers
|
|
807
908
|
*/
|
|
808
909
|
matched?: Layer<StateT, ContextT, BodyT>[];
|
|
910
|
+
/**
|
|
911
|
+
* Whether a route (with HTTP methods) was matched for this request.
|
|
912
|
+
* Set by the router before any handlers run.
|
|
913
|
+
*
|
|
914
|
+
* Use this in app-level middleware after `router.routes()` to detect
|
|
915
|
+
* requests that the router did not handle.
|
|
916
|
+
*
|
|
917
|
+
* @example
|
|
918
|
+
* ```javascript
|
|
919
|
+
* app.use(router.routes());
|
|
920
|
+
* app.use((ctx) => {
|
|
921
|
+
* if (!ctx.routeMatched) {
|
|
922
|
+
* ctx.status = 404;
|
|
923
|
+
* ctx.body = { error: 'Not Found' };
|
|
924
|
+
* }
|
|
925
|
+
* });
|
|
926
|
+
* ```
|
|
927
|
+
*/
|
|
928
|
+
routeMatched?: boolean;
|
|
809
929
|
/**
|
|
810
930
|
* Captured values from path
|
|
811
931
|
*/
|
|
@@ -889,4 +1009,4 @@ type ParameterValidationOptions = {
|
|
|
889
1009
|
*/
|
|
890
1010
|
declare function createParameterValidationMiddleware(parameterName: string, pattern: RegExp, options?: ParameterValidationOptions): RouterMiddleware<any, any, any> & RouterParameterMiddleware<any, any, any>;
|
|
891
1011
|
|
|
892
|
-
export { type AllowedMethodsOptions, type HttpMethod, Layer, type LayerOptions, type MatchResult, type ParameterValidationOptions, Router, type RouterContext, type RouterInstance, type RouterMethodFunction, type RouterMiddleware, type RouterOptions, type RouterOptionsWithMethods, type RouterParameterMiddleware, type RouterWithMethods, type UrlOptions, createParameterValidationMiddleware, Router as default };
|
|
1012
|
+
export { type AllowedMethodsOptions, type HttpMethod, Layer, type LayerOptions, type MatchResult, type ParameterValidationOptions, Router, type RouterContext, type RouterEvent, type RouterEventSelector, RouterEvents, type RouterInstance, type RouterMethodFunction, type RouterMiddleware, type RouterOptions, type RouterOptionsWithMethods, type RouterParameterMiddleware, type RouterWithMethods, type UrlOptions, createParameterValidationMiddleware, Router as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -172,6 +172,47 @@ declare class Layer<StateT = DefaultState, ContextT = DefaultContext, BodyT = un
|
|
|
172
172
|
private _reconfigurePathMatching;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Named constants for active router lifecycle events.
|
|
177
|
+
*
|
|
178
|
+
* Only events that are fully implemented appear here. Planned events will be
|
|
179
|
+
* added as each one is implemented so the public API always reflects what
|
|
180
|
+
* actually works.
|
|
181
|
+
*
|
|
182
|
+
* @experimental
|
|
183
|
+
*/
|
|
184
|
+
declare const RouterEvents: {
|
|
185
|
+
/**
|
|
186
|
+
* Fires when no route matched the request path + HTTP method.
|
|
187
|
+
*/
|
|
188
|
+
readonly NotFound: "not-found";
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Union of all valid router event name strings.
|
|
192
|
+
* Derived from `RouterEvents` so the two are always in sync.
|
|
193
|
+
*
|
|
194
|
+
* @experimental
|
|
195
|
+
*/
|
|
196
|
+
type RouterEvent = (typeof RouterEvents)[keyof typeof RouterEvents];
|
|
197
|
+
/**
|
|
198
|
+
* Accepts either a raw event name string or a selector function that receives
|
|
199
|
+
* the `RouterEvents` object and returns an event name.
|
|
200
|
+
*
|
|
201
|
+
* Both forms are equivalent — choose whichever reads better in context.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```typescript
|
|
205
|
+
* // string
|
|
206
|
+
* 'not-found'
|
|
207
|
+
*
|
|
208
|
+
* // selector function
|
|
209
|
+
* (events) => events.NotFound
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* @experimental
|
|
213
|
+
*/
|
|
214
|
+
type RouterEventSelector = RouterEvent | ((events: typeof RouterEvents) => RouterEvent);
|
|
215
|
+
|
|
175
216
|
type RouterInstance<StateT = koa.DefaultState, ContextT = koa.DefaultContext> = RouterImplementation<StateT, ContextT>;
|
|
176
217
|
/**
|
|
177
218
|
* Middleware with router property
|
|
@@ -189,6 +230,11 @@ declare class RouterImplementation<StateT = koa.DefaultState, ContextT = koa.Def
|
|
|
189
230
|
params: Record<string, RouterParameterMiddleware<StateT, ContextT> | RouterParameterMiddleware<StateT, ContextT>[]>;
|
|
190
231
|
stack: Layer<StateT, ContextT>[];
|
|
191
232
|
host?: string | string[] | RegExp;
|
|
233
|
+
/**
|
|
234
|
+
* Event emitter for router lifecycle events (experimental)
|
|
235
|
+
* @internal
|
|
236
|
+
*/
|
|
237
|
+
private _events;
|
|
192
238
|
/**
|
|
193
239
|
* Create a new router.
|
|
194
240
|
*
|
|
@@ -356,6 +402,52 @@ declare class RouterImplementation<StateT = koa.DefaultState, ContextT = koa.Def
|
|
|
356
402
|
*/
|
|
357
403
|
private _buildMiddlewareChain;
|
|
358
404
|
routes(): RouterComposedMiddleware<StateT, ContextT>;
|
|
405
|
+
/**
|
|
406
|
+
* Register an event handler on the router.
|
|
407
|
+
*
|
|
408
|
+
* @experimental This API is experimental and may change in future versions.
|
|
409
|
+
*
|
|
410
|
+
* **Active events:**
|
|
411
|
+
* - `'not-found'` — fires when no route matched path + method. Handlers are
|
|
412
|
+
* composed and called instead of `next()`, so they are responsible for
|
|
413
|
+
* calling `next()` themselves if they want to pass control downstream.
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
*
|
|
417
|
+
* All three forms are equivalent:
|
|
418
|
+
*
|
|
419
|
+
* ```javascript
|
|
420
|
+
* import { RouterEvents } from '@koa/router';
|
|
421
|
+
*
|
|
422
|
+
* // Named constant (recommended — autocomplete + refactor safe)
|
|
423
|
+
* router.on(RouterEvents.NotFound, handler);
|
|
424
|
+
*
|
|
425
|
+
* // Selector function (fluent style)
|
|
426
|
+
* router.on((events) => events.NotFound, handler);
|
|
427
|
+
*
|
|
428
|
+
* // Raw string (still accepted)
|
|
429
|
+
* router.on('not-found', handler);
|
|
430
|
+
* ```
|
|
431
|
+
*
|
|
432
|
+
* Multiple handlers compose in registration order:
|
|
433
|
+
*
|
|
434
|
+
* ```javascript
|
|
435
|
+
* router.on(RouterEvents.NotFound, async (ctx, next) => {
|
|
436
|
+
* console.log('no route matched', ctx.path);
|
|
437
|
+
* await next();
|
|
438
|
+
* });
|
|
439
|
+
* router.on(RouterEvents.NotFound, (ctx) => {
|
|
440
|
+
* ctx.status = 404;
|
|
441
|
+
* ctx.body = { error: 'Not Found' };
|
|
442
|
+
* });
|
|
443
|
+
* ```
|
|
444
|
+
*
|
|
445
|
+
* @param event - Event name string, `RouterEvents` constant, or selector
|
|
446
|
+
* function `(events) => events.NotFound` (see `RouterEventSelector`)
|
|
447
|
+
* @param handler - Middleware to run when the event fires
|
|
448
|
+
* @returns This router instance for chaining
|
|
449
|
+
*/
|
|
450
|
+
on(event: RouterEventSelector, handler: RouterMiddleware<StateT, ContextT>): Router<StateT, ContextT>;
|
|
359
451
|
/**
|
|
360
452
|
* Returns separate middleware for responding to `OPTIONS` requests with
|
|
361
453
|
* an `Allow` header containing the allowed methods, as well as responding
|
|
@@ -656,9 +748,18 @@ declare const Router: RouterConstructor;
|
|
|
656
748
|
|
|
657
749
|
type RouterOptions = {
|
|
658
750
|
/**
|
|
659
|
-
*
|
|
751
|
+
* Control which route is executed when multiple routes match a request.
|
|
752
|
+
*
|
|
753
|
+
* - `true` — run only the last registered matching route (legacy behaviour)
|
|
754
|
+
* - `'specificity'` — run only the most specific matching route, defined as
|
|
755
|
+
* the one with the fewest path parameters. This is OpenAPI-compliant
|
|
756
|
+
* (see https://spec.openapis.org/oas/v3.0.3#path-templating-matching) and
|
|
757
|
+
* is the recommended mode for code generated from OpenAPI specifications.
|
|
758
|
+
* When two routes have an equal number of parameters the last registered
|
|
759
|
+
* one wins, preserving deterministic behaviour.
|
|
760
|
+
* - `false` / omitted — all matching routes run in registration order
|
|
660
761
|
*/
|
|
661
|
-
exclusive?: boolean;
|
|
762
|
+
exclusive?: boolean | 'specificity';
|
|
662
763
|
/**
|
|
663
764
|
* Prefix for all routes
|
|
664
765
|
*/
|
|
@@ -806,6 +907,25 @@ type RouterContext<StateT = DefaultState, ContextT = DefaultContext, BodyT = unk
|
|
|
806
907
|
* Array of matched layers
|
|
807
908
|
*/
|
|
808
909
|
matched?: Layer<StateT, ContextT, BodyT>[];
|
|
910
|
+
/**
|
|
911
|
+
* Whether a route (with HTTP methods) was matched for this request.
|
|
912
|
+
* Set by the router before any handlers run.
|
|
913
|
+
*
|
|
914
|
+
* Use this in app-level middleware after `router.routes()` to detect
|
|
915
|
+
* requests that the router did not handle.
|
|
916
|
+
*
|
|
917
|
+
* @example
|
|
918
|
+
* ```javascript
|
|
919
|
+
* app.use(router.routes());
|
|
920
|
+
* app.use((ctx) => {
|
|
921
|
+
* if (!ctx.routeMatched) {
|
|
922
|
+
* ctx.status = 404;
|
|
923
|
+
* ctx.body = { error: 'Not Found' };
|
|
924
|
+
* }
|
|
925
|
+
* });
|
|
926
|
+
* ```
|
|
927
|
+
*/
|
|
928
|
+
routeMatched?: boolean;
|
|
809
929
|
/**
|
|
810
930
|
* Captured values from path
|
|
811
931
|
*/
|
|
@@ -889,4 +1009,4 @@ type ParameterValidationOptions = {
|
|
|
889
1009
|
*/
|
|
890
1010
|
declare function createParameterValidationMiddleware(parameterName: string, pattern: RegExp, options?: ParameterValidationOptions): RouterMiddleware<any, any, any> & RouterParameterMiddleware<any, any, any>;
|
|
891
1011
|
|
|
892
|
-
export { type AllowedMethodsOptions, type HttpMethod, Layer, type LayerOptions, type MatchResult, type ParameterValidationOptions, Router, type RouterContext, type RouterInstance, type RouterMethodFunction, type RouterMiddleware, type RouterOptions, type RouterOptionsWithMethods, type RouterParameterMiddleware, type RouterWithMethods, type UrlOptions, createParameterValidationMiddleware, Router as default };
|
|
1012
|
+
export { type AllowedMethodsOptions, type HttpMethod, Layer, type LayerOptions, type MatchResult, type ParameterValidationOptions, Router, type RouterContext, type RouterEvent, type RouterEventSelector, RouterEvents, type RouterInstance, type RouterMethodFunction, type RouterMiddleware, type RouterOptions, type RouterOptionsWithMethods, type RouterParameterMiddleware, type RouterWithMethods, type UrlOptions, createParameterValidationMiddleware, Router as default };
|
package/dist/index.js
CHANGED
|
@@ -31,11 +31,72 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
Router: () => Router,
|
|
34
|
+
RouterEvents: () => RouterEvents,
|
|
34
35
|
createParameterValidationMiddleware: () => createParameterValidationMiddleware,
|
|
35
36
|
default: () => router_default
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(index_exports);
|
|
38
39
|
|
|
40
|
+
// src/utils/router-events.ts
|
|
41
|
+
var import_koa_compose = __toESM(require("koa-compose"));
|
|
42
|
+
var RouterEvents = {
|
|
43
|
+
/**
|
|
44
|
+
* Fires when no route matched the request path + HTTP method.
|
|
45
|
+
*/
|
|
46
|
+
NotFound: "not-found"
|
|
47
|
+
// /**
|
|
48
|
+
// * Fires when the request path matched a registered route but the HTTP
|
|
49
|
+
// * method did not match any of its allowed methods.
|
|
50
|
+
// *
|
|
51
|
+
// * @planned Not yet active — handlers are stored but not called.
|
|
52
|
+
// */
|
|
53
|
+
// MethodNotAllowed: 'method-not-allowed',
|
|
54
|
+
// /**
|
|
55
|
+
// * Fires after a route is matched and before its handlers run.
|
|
56
|
+
// * Useful for tracing and logging the matched route.
|
|
57
|
+
// *
|
|
58
|
+
// * @planned Not yet active — handlers are stored but not called.
|
|
59
|
+
// */
|
|
60
|
+
// Match: 'match',
|
|
61
|
+
// /**
|
|
62
|
+
// * Fires on every request that enters the router, before route matching.
|
|
63
|
+
// * Useful for per-router metrics and request logging.
|
|
64
|
+
// *
|
|
65
|
+
// * @planned Not yet active — handlers are stored but not called.
|
|
66
|
+
// */
|
|
67
|
+
// Dispatch: 'dispatch'
|
|
68
|
+
};
|
|
69
|
+
function resolveEvent(selector) {
|
|
70
|
+
return typeof selector === "function" ? selector(RouterEvents) : selector;
|
|
71
|
+
}
|
|
72
|
+
var RouterEventEmitter = class {
|
|
73
|
+
_handlers = /* @__PURE__ */ new Map();
|
|
74
|
+
/**
|
|
75
|
+
* Register a handler for the given event.
|
|
76
|
+
* Multiple handlers for the same event are composed in registration order.
|
|
77
|
+
*/
|
|
78
|
+
register(event, handler) {
|
|
79
|
+
const existing = this._handlers.get(event) ?? [];
|
|
80
|
+
existing.push(handler);
|
|
81
|
+
this._handlers.set(event, existing);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Emit an event by composing and running all registered handlers.
|
|
85
|
+
* If no handlers are registered for the event, calls `next()` directly.
|
|
86
|
+
*
|
|
87
|
+
* @param event - The event to emit
|
|
88
|
+
* @param context - The router context
|
|
89
|
+
* @param next - The downstream next function
|
|
90
|
+
*/
|
|
91
|
+
emit(event, context, next) {
|
|
92
|
+
const handlers = this._handlers.get(event);
|
|
93
|
+
if (handlers && handlers.length > 0) {
|
|
94
|
+
return (0, import_koa_compose.default)(handlers)(context, next);
|
|
95
|
+
}
|
|
96
|
+
return next();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
39
100
|
// src/utils/parameter-match.ts
|
|
40
101
|
var import_http_errors = __toESM(require("http-errors"));
|
|
41
102
|
function createParameterValidationMiddleware(parameterName, pattern, options = {}) {
|
|
@@ -86,7 +147,7 @@ function createParameterValidationMiddleware(parameterName, pattern, options = {
|
|
|
86
147
|
}
|
|
87
148
|
|
|
88
149
|
// src/router.ts
|
|
89
|
-
var
|
|
150
|
+
var import_koa_compose2 = __toESM(require("koa-compose"));
|
|
90
151
|
var import_http_errors2 = __toESM(require("http-errors"));
|
|
91
152
|
|
|
92
153
|
// src/layer.ts
|
|
@@ -651,6 +712,11 @@ var RouterImplementation = class {
|
|
|
651
712
|
params;
|
|
652
713
|
stack;
|
|
653
714
|
host;
|
|
715
|
+
/**
|
|
716
|
+
* Event emitter for router lifecycle events (experimental)
|
|
717
|
+
* @internal
|
|
718
|
+
*/
|
|
719
|
+
_events = new RouterEventEmitter();
|
|
654
720
|
/**
|
|
655
721
|
* Create a new router.
|
|
656
722
|
*
|
|
@@ -900,8 +966,11 @@ var RouterImplementation = class {
|
|
|
900
966
|
const previousPrefix = this.opts.prefix || "";
|
|
901
967
|
this.opts.prefix = normalizedPrefix;
|
|
902
968
|
for (const route of this.stack) {
|
|
903
|
-
if (
|
|
904
|
-
|
|
969
|
+
if (typeof route.path === "string" && previousPrefix) {
|
|
970
|
+
const stripBoundary = previousPrefix + "/";
|
|
971
|
+
if (route.path === previousPrefix || route.path.startsWith(stripBoundary)) {
|
|
972
|
+
route.path = route.path.slice(previousPrefix.length) || "/";
|
|
973
|
+
}
|
|
905
974
|
}
|
|
906
975
|
route.setPrefix(normalizedPrefix);
|
|
907
976
|
}
|
|
@@ -922,8 +991,13 @@ var RouterImplementation = class {
|
|
|
922
991
|
const matchResult = this.match(requestPath, context.method);
|
|
923
992
|
this._storeMatchedRoutes(context, matchResult);
|
|
924
993
|
context.router = this;
|
|
994
|
+
context.routeMatched = matchResult.route;
|
|
925
995
|
if (!matchResult.route) {
|
|
926
|
-
return
|
|
996
|
+
return this._events.emit(
|
|
997
|
+
RouterEvents.NotFound,
|
|
998
|
+
context,
|
|
999
|
+
next
|
|
1000
|
+
);
|
|
927
1001
|
}
|
|
928
1002
|
const matchedLayers = matchResult.pathAndMethod;
|
|
929
1003
|
this._setMatchedRouteInfo(context, matchedLayers);
|
|
@@ -931,7 +1005,7 @@ var RouterImplementation = class {
|
|
|
931
1005
|
matchedLayers,
|
|
932
1006
|
requestPath
|
|
933
1007
|
);
|
|
934
|
-
return (0,
|
|
1008
|
+
return (0, import_koa_compose2.default)(middlewareChain)(
|
|
935
1009
|
context,
|
|
936
1010
|
next
|
|
937
1011
|
);
|
|
@@ -978,9 +1052,22 @@ var RouterImplementation = class {
|
|
|
978
1052
|
* @private
|
|
979
1053
|
*/
|
|
980
1054
|
_buildMiddlewareChain(matchedLayers, requestPath) {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1055
|
+
let layersToExecute;
|
|
1056
|
+
if (this.exclusive) {
|
|
1057
|
+
let chosenLayer;
|
|
1058
|
+
if (this.opts.exclusive === "specificity") {
|
|
1059
|
+
for (const layer of matchedLayers) {
|
|
1060
|
+
if (!chosenLayer || layer.paramNames.length < chosenLayer.paramNames.length) {
|
|
1061
|
+
chosenLayer = layer;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
} else {
|
|
1065
|
+
chosenLayer = matchedLayers.at(-1);
|
|
1066
|
+
}
|
|
1067
|
+
layersToExecute = chosenLayer ? [chosenLayer] : [];
|
|
1068
|
+
} else {
|
|
1069
|
+
layersToExecute = matchedLayers;
|
|
1070
|
+
}
|
|
984
1071
|
const middlewareChain = [];
|
|
985
1072
|
for (const layer of layersToExecute) {
|
|
986
1073
|
middlewareChain.push(
|
|
@@ -1009,6 +1096,55 @@ var RouterImplementation = class {
|
|
|
1009
1096
|
routes() {
|
|
1010
1097
|
return this.middleware();
|
|
1011
1098
|
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Register an event handler on the router.
|
|
1101
|
+
*
|
|
1102
|
+
* @experimental This API is experimental and may change in future versions.
|
|
1103
|
+
*
|
|
1104
|
+
* **Active events:**
|
|
1105
|
+
* - `'not-found'` — fires when no route matched path + method. Handlers are
|
|
1106
|
+
* composed and called instead of `next()`, so they are responsible for
|
|
1107
|
+
* calling `next()` themselves if they want to pass control downstream.
|
|
1108
|
+
*
|
|
1109
|
+
* @example
|
|
1110
|
+
*
|
|
1111
|
+
* All three forms are equivalent:
|
|
1112
|
+
*
|
|
1113
|
+
* ```javascript
|
|
1114
|
+
* import { RouterEvents } from '@koa/router';
|
|
1115
|
+
*
|
|
1116
|
+
* // Named constant (recommended — autocomplete + refactor safe)
|
|
1117
|
+
* router.on(RouterEvents.NotFound, handler);
|
|
1118
|
+
*
|
|
1119
|
+
* // Selector function (fluent style)
|
|
1120
|
+
* router.on((events) => events.NotFound, handler);
|
|
1121
|
+
*
|
|
1122
|
+
* // Raw string (still accepted)
|
|
1123
|
+
* router.on('not-found', handler);
|
|
1124
|
+
* ```
|
|
1125
|
+
*
|
|
1126
|
+
* Multiple handlers compose in registration order:
|
|
1127
|
+
*
|
|
1128
|
+
* ```javascript
|
|
1129
|
+
* router.on(RouterEvents.NotFound, async (ctx, next) => {
|
|
1130
|
+
* console.log('no route matched', ctx.path);
|
|
1131
|
+
* await next();
|
|
1132
|
+
* });
|
|
1133
|
+
* router.on(RouterEvents.NotFound, (ctx) => {
|
|
1134
|
+
* ctx.status = 404;
|
|
1135
|
+
* ctx.body = { error: 'Not Found' };
|
|
1136
|
+
* });
|
|
1137
|
+
* ```
|
|
1138
|
+
*
|
|
1139
|
+
* @param event - Event name string, `RouterEvents` constant, or selector
|
|
1140
|
+
* function `(events) => events.NotFound` (see `RouterEventSelector`)
|
|
1141
|
+
* @param handler - Middleware to run when the event fires
|
|
1142
|
+
* @returns This router instance for chaining
|
|
1143
|
+
*/
|
|
1144
|
+
on(event, handler) {
|
|
1145
|
+
this._events.register(resolveEvent(event), handler);
|
|
1146
|
+
return this;
|
|
1147
|
+
}
|
|
1012
1148
|
/**
|
|
1013
1149
|
* Returns separate middleware for responding to `OPTIONS` requests with
|
|
1014
1150
|
* an `Allow` header containing the allowed methods, as well as responding
|
|
@@ -1487,6 +1623,7 @@ for (const httpMethod of httpMethods) {
|
|
|
1487
1623
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1488
1624
|
0 && (module.exports = {
|
|
1489
1625
|
Router,
|
|
1626
|
+
RouterEvents,
|
|
1490
1627
|
createParameterValidationMiddleware
|
|
1491
1628
|
});
|
|
1492
1629
|
if (module.exports.default) {
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
+
// src/utils/router-events.ts
|
|
2
|
+
import compose from "koa-compose";
|
|
3
|
+
var RouterEvents = {
|
|
4
|
+
/**
|
|
5
|
+
* Fires when no route matched the request path + HTTP method.
|
|
6
|
+
*/
|
|
7
|
+
NotFound: "not-found"
|
|
8
|
+
// /**
|
|
9
|
+
// * Fires when the request path matched a registered route but the HTTP
|
|
10
|
+
// * method did not match any of its allowed methods.
|
|
11
|
+
// *
|
|
12
|
+
// * @planned Not yet active — handlers are stored but not called.
|
|
13
|
+
// */
|
|
14
|
+
// MethodNotAllowed: 'method-not-allowed',
|
|
15
|
+
// /**
|
|
16
|
+
// * Fires after a route is matched and before its handlers run.
|
|
17
|
+
// * Useful for tracing and logging the matched route.
|
|
18
|
+
// *
|
|
19
|
+
// * @planned Not yet active — handlers are stored but not called.
|
|
20
|
+
// */
|
|
21
|
+
// Match: 'match',
|
|
22
|
+
// /**
|
|
23
|
+
// * Fires on every request that enters the router, before route matching.
|
|
24
|
+
// * Useful for per-router metrics and request logging.
|
|
25
|
+
// *
|
|
26
|
+
// * @planned Not yet active — handlers are stored but not called.
|
|
27
|
+
// */
|
|
28
|
+
// Dispatch: 'dispatch'
|
|
29
|
+
};
|
|
30
|
+
function resolveEvent(selector) {
|
|
31
|
+
return typeof selector === "function" ? selector(RouterEvents) : selector;
|
|
32
|
+
}
|
|
33
|
+
var RouterEventEmitter = class {
|
|
34
|
+
_handlers = /* @__PURE__ */ new Map();
|
|
35
|
+
/**
|
|
36
|
+
* Register a handler for the given event.
|
|
37
|
+
* Multiple handlers for the same event are composed in registration order.
|
|
38
|
+
*/
|
|
39
|
+
register(event, handler) {
|
|
40
|
+
const existing = this._handlers.get(event) ?? [];
|
|
41
|
+
existing.push(handler);
|
|
42
|
+
this._handlers.set(event, existing);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Emit an event by composing and running all registered handlers.
|
|
46
|
+
* If no handlers are registered for the event, calls `next()` directly.
|
|
47
|
+
*
|
|
48
|
+
* @param event - The event to emit
|
|
49
|
+
* @param context - The router context
|
|
50
|
+
* @param next - The downstream next function
|
|
51
|
+
*/
|
|
52
|
+
emit(event, context, next) {
|
|
53
|
+
const handlers = this._handlers.get(event);
|
|
54
|
+
if (handlers && handlers.length > 0) {
|
|
55
|
+
return compose(handlers)(context, next);
|
|
56
|
+
}
|
|
57
|
+
return next();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
1
61
|
// src/utils/parameter-match.ts
|
|
2
62
|
import createHttpError from "http-errors";
|
|
3
63
|
function createParameterValidationMiddleware(parameterName, pattern, options = {}) {
|
|
@@ -48,7 +108,7 @@ function createParameterValidationMiddleware(parameterName, pattern, options = {
|
|
|
48
108
|
}
|
|
49
109
|
|
|
50
110
|
// src/router.ts
|
|
51
|
-
import
|
|
111
|
+
import compose2 from "koa-compose";
|
|
52
112
|
import HttpError from "http-errors";
|
|
53
113
|
|
|
54
114
|
// src/layer.ts
|
|
@@ -613,6 +673,11 @@ var RouterImplementation = class {
|
|
|
613
673
|
params;
|
|
614
674
|
stack;
|
|
615
675
|
host;
|
|
676
|
+
/**
|
|
677
|
+
* Event emitter for router lifecycle events (experimental)
|
|
678
|
+
* @internal
|
|
679
|
+
*/
|
|
680
|
+
_events = new RouterEventEmitter();
|
|
616
681
|
/**
|
|
617
682
|
* Create a new router.
|
|
618
683
|
*
|
|
@@ -862,8 +927,11 @@ var RouterImplementation = class {
|
|
|
862
927
|
const previousPrefix = this.opts.prefix || "";
|
|
863
928
|
this.opts.prefix = normalizedPrefix;
|
|
864
929
|
for (const route of this.stack) {
|
|
865
|
-
if (
|
|
866
|
-
|
|
930
|
+
if (typeof route.path === "string" && previousPrefix) {
|
|
931
|
+
const stripBoundary = previousPrefix + "/";
|
|
932
|
+
if (route.path === previousPrefix || route.path.startsWith(stripBoundary)) {
|
|
933
|
+
route.path = route.path.slice(previousPrefix.length) || "/";
|
|
934
|
+
}
|
|
867
935
|
}
|
|
868
936
|
route.setPrefix(normalizedPrefix);
|
|
869
937
|
}
|
|
@@ -884,8 +952,13 @@ var RouterImplementation = class {
|
|
|
884
952
|
const matchResult = this.match(requestPath, context.method);
|
|
885
953
|
this._storeMatchedRoutes(context, matchResult);
|
|
886
954
|
context.router = this;
|
|
955
|
+
context.routeMatched = matchResult.route;
|
|
887
956
|
if (!matchResult.route) {
|
|
888
|
-
return
|
|
957
|
+
return this._events.emit(
|
|
958
|
+
RouterEvents.NotFound,
|
|
959
|
+
context,
|
|
960
|
+
next
|
|
961
|
+
);
|
|
889
962
|
}
|
|
890
963
|
const matchedLayers = matchResult.pathAndMethod;
|
|
891
964
|
this._setMatchedRouteInfo(context, matchedLayers);
|
|
@@ -893,7 +966,7 @@ var RouterImplementation = class {
|
|
|
893
966
|
matchedLayers,
|
|
894
967
|
requestPath
|
|
895
968
|
);
|
|
896
|
-
return
|
|
969
|
+
return compose2(middlewareChain)(
|
|
897
970
|
context,
|
|
898
971
|
next
|
|
899
972
|
);
|
|
@@ -940,9 +1013,22 @@ var RouterImplementation = class {
|
|
|
940
1013
|
* @private
|
|
941
1014
|
*/
|
|
942
1015
|
_buildMiddlewareChain(matchedLayers, requestPath) {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1016
|
+
let layersToExecute;
|
|
1017
|
+
if (this.exclusive) {
|
|
1018
|
+
let chosenLayer;
|
|
1019
|
+
if (this.opts.exclusive === "specificity") {
|
|
1020
|
+
for (const layer of matchedLayers) {
|
|
1021
|
+
if (!chosenLayer || layer.paramNames.length < chosenLayer.paramNames.length) {
|
|
1022
|
+
chosenLayer = layer;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
} else {
|
|
1026
|
+
chosenLayer = matchedLayers.at(-1);
|
|
1027
|
+
}
|
|
1028
|
+
layersToExecute = chosenLayer ? [chosenLayer] : [];
|
|
1029
|
+
} else {
|
|
1030
|
+
layersToExecute = matchedLayers;
|
|
1031
|
+
}
|
|
946
1032
|
const middlewareChain = [];
|
|
947
1033
|
for (const layer of layersToExecute) {
|
|
948
1034
|
middlewareChain.push(
|
|
@@ -971,6 +1057,55 @@ var RouterImplementation = class {
|
|
|
971
1057
|
routes() {
|
|
972
1058
|
return this.middleware();
|
|
973
1059
|
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Register an event handler on the router.
|
|
1062
|
+
*
|
|
1063
|
+
* @experimental This API is experimental and may change in future versions.
|
|
1064
|
+
*
|
|
1065
|
+
* **Active events:**
|
|
1066
|
+
* - `'not-found'` — fires when no route matched path + method. Handlers are
|
|
1067
|
+
* composed and called instead of `next()`, so they are responsible for
|
|
1068
|
+
* calling `next()` themselves if they want to pass control downstream.
|
|
1069
|
+
*
|
|
1070
|
+
* @example
|
|
1071
|
+
*
|
|
1072
|
+
* All three forms are equivalent:
|
|
1073
|
+
*
|
|
1074
|
+
* ```javascript
|
|
1075
|
+
* import { RouterEvents } from '@koa/router';
|
|
1076
|
+
*
|
|
1077
|
+
* // Named constant (recommended — autocomplete + refactor safe)
|
|
1078
|
+
* router.on(RouterEvents.NotFound, handler);
|
|
1079
|
+
*
|
|
1080
|
+
* // Selector function (fluent style)
|
|
1081
|
+
* router.on((events) => events.NotFound, handler);
|
|
1082
|
+
*
|
|
1083
|
+
* // Raw string (still accepted)
|
|
1084
|
+
* router.on('not-found', handler);
|
|
1085
|
+
* ```
|
|
1086
|
+
*
|
|
1087
|
+
* Multiple handlers compose in registration order:
|
|
1088
|
+
*
|
|
1089
|
+
* ```javascript
|
|
1090
|
+
* router.on(RouterEvents.NotFound, async (ctx, next) => {
|
|
1091
|
+
* console.log('no route matched', ctx.path);
|
|
1092
|
+
* await next();
|
|
1093
|
+
* });
|
|
1094
|
+
* router.on(RouterEvents.NotFound, (ctx) => {
|
|
1095
|
+
* ctx.status = 404;
|
|
1096
|
+
* ctx.body = { error: 'Not Found' };
|
|
1097
|
+
* });
|
|
1098
|
+
* ```
|
|
1099
|
+
*
|
|
1100
|
+
* @param event - Event name string, `RouterEvents` constant, or selector
|
|
1101
|
+
* function `(events) => events.NotFound` (see `RouterEventSelector`)
|
|
1102
|
+
* @param handler - Middleware to run when the event fires
|
|
1103
|
+
* @returns This router instance for chaining
|
|
1104
|
+
*/
|
|
1105
|
+
on(event, handler) {
|
|
1106
|
+
this._events.register(resolveEvent(event), handler);
|
|
1107
|
+
return this;
|
|
1108
|
+
}
|
|
974
1109
|
/**
|
|
975
1110
|
* Returns separate middleware for responding to `OPTIONS` requests with
|
|
976
1111
|
* an `Allow` header containing the allowed methods, as well as responding
|
|
@@ -1448,6 +1583,7 @@ for (const httpMethod of httpMethods) {
|
|
|
1448
1583
|
}
|
|
1449
1584
|
export {
|
|
1450
1585
|
Router,
|
|
1586
|
+
RouterEvents,
|
|
1451
1587
|
createParameterValidationMiddleware,
|
|
1452
1588
|
router_default as default
|
|
1453
1589
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koa/router",
|
|
3
3
|
"description": "Router middleware for Koa.",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.5.0",
|
|
5
5
|
"author": "Koa.js",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/koajs/router/issues",
|
|
@@ -28,31 +28,31 @@
|
|
|
28
28
|
"debug": "^4.4.3",
|
|
29
29
|
"http-errors": "^2.0.1",
|
|
30
30
|
"koa-compose": "^4.1.0",
|
|
31
|
-
"path-to-regexp": "^8.
|
|
31
|
+
"path-to-regexp": "^8.4.2"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@commitlint/cli": "^20.
|
|
35
|
-
"@commitlint/config-conventional": "^20.
|
|
34
|
+
"@commitlint/cli": "^20.5.3",
|
|
35
|
+
"@commitlint/config-conventional": "^20.5.3",
|
|
36
36
|
"@eslint/js": "^10.0.1",
|
|
37
37
|
"@koa/bodyparser": "^6.1.0",
|
|
38
|
-
"@types/debug": "^4.1.
|
|
38
|
+
"@types/debug": "^4.1.13",
|
|
39
39
|
"@types/jsonwebtoken": "^9.0.10",
|
|
40
|
-
"@types/koa": "^3.0.
|
|
41
|
-
"@types/node": "^25.
|
|
40
|
+
"@types/koa": "^3.0.2",
|
|
41
|
+
"@types/node": "^25.6.0",
|
|
42
42
|
"@types/supertest": "^7.2.0",
|
|
43
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
44
|
-
"@typescript-eslint/parser": "^8.
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^8.59.1",
|
|
44
|
+
"@typescript-eslint/parser": "^8.59.1",
|
|
45
45
|
"c8": "^11.0.0",
|
|
46
46
|
"chalk": "^5.6.2",
|
|
47
|
-
"eslint": "^10.0
|
|
48
|
-
"eslint-plugin-unicorn": "^
|
|
47
|
+
"eslint": "^10.3.0",
|
|
48
|
+
"eslint-plugin-unicorn": "^64.0.0",
|
|
49
49
|
"husky": "^9.1.7",
|
|
50
|
-
"joi": "^18.
|
|
50
|
+
"joi": "^18.1.2",
|
|
51
51
|
"jsonwebtoken": "^9.0.3",
|
|
52
|
-
"koa": "^3.
|
|
52
|
+
"koa": "^3.2.0",
|
|
53
53
|
"lint-staged": "^16.4.0",
|
|
54
|
-
"np": "^11.0
|
|
55
|
-
"prettier": "^3.8.
|
|
54
|
+
"np": "^11.2.0",
|
|
55
|
+
"prettier": "^3.8.3",
|
|
56
56
|
"rimraf": "^6.1.3",
|
|
57
57
|
"supertest": "^7.2.2",
|
|
58
58
|
"ts-node": "^10.9.2",
|