@koa/router 15.4.0 → 15.6.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 +31 -9
- package/dist/index.d.mts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +27 -5
- package/dist/index.mjs +27 -5
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
- ✅ **405 Method Not Allowed** - Automatic method validation
|
|
41
41
|
- ✅ **501 Not Implemented** - Proper HTTP status codes
|
|
42
42
|
- ✅ **Async/Await** - Full promise-based middleware support
|
|
43
|
+
- ✅ **Clean ctx.params** - Only URL parameters you define are present in `ctx.params`; no internal routing keys leak out
|
|
43
44
|
|
|
44
45
|
## Installation
|
|
45
46
|
|
|
@@ -266,14 +267,14 @@ Create a new router instance.
|
|
|
266
267
|
|
|
267
268
|
**Options:**
|
|
268
269
|
|
|
269
|
-
| Option | Type | Description
|
|
270
|
-
| ----------- | ------------------------------ |
|
|
271
|
-
| `prefix` | `string` | Prefix all routes with this path
|
|
272
|
-
| `exclusive` | `boolean`
|
|
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
|
|
270
|
+
| Option | Type | Description |
|
|
271
|
+
| ----------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
272
|
+
| `prefix` | `string` | Prefix all routes with this path |
|
|
273
|
+
| `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. |
|
|
274
|
+
| `host` | `string \| string[] \| RegExp` | Match routes only for this hostname(s) |
|
|
275
|
+
| `methods` | `string[]` | Custom HTTP methods to support |
|
|
276
|
+
| `sensitive` | `boolean` | Enable case-sensitive routing |
|
|
277
|
+
| `strict` | `boolean` | Require trailing slashes |
|
|
277
278
|
|
|
278
279
|
**Example:**
|
|
279
280
|
|
|
@@ -457,7 +458,26 @@ router.get('/users', (ctx) => {
|
|
|
457
458
|
// Responds to /api/v1/users, /api/v2/users, etc.
|
|
458
459
|
```
|
|
459
460
|
|
|
460
|
-
**
|
|
461
|
+
**Middleware on a parameterized prefix:**
|
|
462
|
+
|
|
463
|
+
When `router.use()` is attached to a router whose prefix contains URL parameters, `ctx.params` inside the middleware will contain **only** the parameters defined by the prefix — no internal routing keys are ever added.
|
|
464
|
+
|
|
465
|
+
```javascript
|
|
466
|
+
const router = new Router({ prefix: '/:tenantId' });
|
|
467
|
+
|
|
468
|
+
// Auth / authz middleware — receives only the defined prefix param
|
|
469
|
+
router.use(async (ctx, next) => {
|
|
470
|
+
console.log(ctx.params); // => { tenantId: 'acme' } (no extraneous keys)
|
|
471
|
+
await next();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
router.get('/users', (ctx) => {
|
|
475
|
+
console.log(ctx.params); // => { tenantId: 'acme' }
|
|
476
|
+
ctx.body = ctx.params;
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
This is particularly useful when running **strict parameter validation** inside a `router.use()` middleware — the shape of `ctx.params` is predictable and contains only what you defined.
|
|
461
481
|
|
|
462
482
|
### URL Parameters
|
|
463
483
|
|
|
@@ -565,6 +585,8 @@ router.use('/nested', nestedRouter.routes());
|
|
|
565
585
|
|
|
566
586
|
**Note:** Middleware path boundaries are correctly enforced. Middleware scoped to `/api` will only run for routes matching `/api/*`, not for unrelated routes.
|
|
567
587
|
|
|
588
|
+
**Note:** When `router.use()` is used on a router with a parameterized prefix (e.g. `prefix: '/:id'`), `ctx.params` inside that middleware will contain **only** the parameters defined by the prefix and route path — no internal wildcard keys are exposed. See [Router Prefixes](#router-prefixes) for a full example.
|
|
589
|
+
|
|
568
590
|
### router.prefix()
|
|
569
591
|
|
|
570
592
|
Set the path prefix for a Router instance after initialization.
|
package/dist/index.d.mts
CHANGED
|
@@ -748,9 +748,18 @@ declare const Router: RouterConstructor;
|
|
|
748
748
|
|
|
749
749
|
type RouterOptions = {
|
|
750
750
|
/**
|
|
751
|
-
*
|
|
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
|
|
752
761
|
*/
|
|
753
|
-
exclusive?: boolean;
|
|
762
|
+
exclusive?: boolean | 'specificity';
|
|
754
763
|
/**
|
|
755
764
|
* Prefix for all routes
|
|
756
765
|
*/
|
|
@@ -812,6 +821,12 @@ type LayerOptions = {
|
|
|
812
821
|
* Ignore captures in route matching
|
|
813
822
|
*/
|
|
814
823
|
ignoreCaptures?: boolean;
|
|
824
|
+
/**
|
|
825
|
+
* Ignore a generated wildcard route parameter when populating ctx.params
|
|
826
|
+
*
|
|
827
|
+
* @internal
|
|
828
|
+
*/
|
|
829
|
+
ignoreWildcardParameter?: string | number;
|
|
815
830
|
/**
|
|
816
831
|
* Treat path as a regular expression
|
|
817
832
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -748,9 +748,18 @@ declare const Router: RouterConstructor;
|
|
|
748
748
|
|
|
749
749
|
type RouterOptions = {
|
|
750
750
|
/**
|
|
751
|
-
*
|
|
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
|
|
752
761
|
*/
|
|
753
|
-
exclusive?: boolean;
|
|
762
|
+
exclusive?: boolean | 'specificity';
|
|
754
763
|
/**
|
|
755
764
|
* Prefix for all routes
|
|
756
765
|
*/
|
|
@@ -812,6 +821,12 @@ type LayerOptions = {
|
|
|
812
821
|
* Ignore captures in route matching
|
|
813
822
|
*/
|
|
814
823
|
ignoreCaptures?: boolean;
|
|
824
|
+
/**
|
|
825
|
+
* Ignore a generated wildcard route parameter when populating ctx.params
|
|
826
|
+
*
|
|
827
|
+
* @internal
|
|
828
|
+
*/
|
|
829
|
+
ignoreWildcardParameter?: string | number;
|
|
815
830
|
/**
|
|
816
831
|
* Treat path as a regular expression
|
|
817
832
|
*/
|
package/dist/index.js
CHANGED
|
@@ -310,6 +310,9 @@ var Layer = class {
|
|
|
310
310
|
const parameterDefinition = this.paramNames[captureIndex];
|
|
311
311
|
if (parameterDefinition && capturedValue && capturedValue.length > 0) {
|
|
312
312
|
const parameterName = parameterDefinition.name;
|
|
313
|
+
if (this.opts.ignoreWildcardParameter === parameterName && parameterDefinition.type === "wildcard") {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
313
316
|
parameterValues[parameterName] = safeDecodeURIComponent(capturedValue);
|
|
314
317
|
}
|
|
315
318
|
}
|
|
@@ -942,9 +945,11 @@ var RouterImplementation = class {
|
|
|
942
945
|
finalPath = middlewarePath;
|
|
943
946
|
usePathToRegexp = false;
|
|
944
947
|
}
|
|
948
|
+
const ignoreWildcardParameter = effectiveExplicitPath === "" && middlewarePath === "{/*rest}" ? "rest" : void 0;
|
|
945
949
|
this.register(finalPath, [], middleware, {
|
|
946
950
|
end: isRootPath,
|
|
947
951
|
ignoreCaptures: !effectiveHasExplicitPath && !prefixHasParameters,
|
|
952
|
+
ignoreWildcardParameter,
|
|
948
953
|
pathAsRegExp: usePathToRegexp
|
|
949
954
|
});
|
|
950
955
|
}
|
|
@@ -966,8 +971,11 @@ var RouterImplementation = class {
|
|
|
966
971
|
const previousPrefix = this.opts.prefix || "";
|
|
967
972
|
this.opts.prefix = normalizedPrefix;
|
|
968
973
|
for (const route of this.stack) {
|
|
969
|
-
if (
|
|
970
|
-
|
|
974
|
+
if (typeof route.path === "string" && previousPrefix) {
|
|
975
|
+
const stripBoundary = previousPrefix + "/";
|
|
976
|
+
if (route.path === previousPrefix || route.path.startsWith(stripBoundary)) {
|
|
977
|
+
route.path = route.path.slice(previousPrefix.length) || "/";
|
|
978
|
+
}
|
|
971
979
|
}
|
|
972
980
|
route.setPrefix(normalizedPrefix);
|
|
973
981
|
}
|
|
@@ -1049,9 +1057,22 @@ var RouterImplementation = class {
|
|
|
1049
1057
|
* @private
|
|
1050
1058
|
*/
|
|
1051
1059
|
_buildMiddlewareChain(matchedLayers, requestPath) {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1060
|
+
let layersToExecute;
|
|
1061
|
+
if (this.exclusive) {
|
|
1062
|
+
let chosenLayer;
|
|
1063
|
+
if (this.opts.exclusive === "specificity") {
|
|
1064
|
+
for (const layer of matchedLayers) {
|
|
1065
|
+
if (!chosenLayer || layer.paramNames.length < chosenLayer.paramNames.length) {
|
|
1066
|
+
chosenLayer = layer;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
} else {
|
|
1070
|
+
chosenLayer = matchedLayers.at(-1);
|
|
1071
|
+
}
|
|
1072
|
+
layersToExecute = chosenLayer ? [chosenLayer] : [];
|
|
1073
|
+
} else {
|
|
1074
|
+
layersToExecute = matchedLayers;
|
|
1075
|
+
}
|
|
1055
1076
|
const middlewareChain = [];
|
|
1056
1077
|
for (const layer of layersToExecute) {
|
|
1057
1078
|
middlewareChain.push(
|
|
@@ -1381,6 +1402,7 @@ var RouterImplementation = class {
|
|
|
1381
1402
|
strict: options.strict || false,
|
|
1382
1403
|
prefix: options.prefix || "",
|
|
1383
1404
|
ignoreCaptures: options.ignoreCaptures,
|
|
1405
|
+
ignoreWildcardParameter: options.ignoreWildcardParameter,
|
|
1384
1406
|
pathAsRegExp: options.pathAsRegExp
|
|
1385
1407
|
});
|
|
1386
1408
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -271,6 +271,9 @@ var Layer = class {
|
|
|
271
271
|
const parameterDefinition = this.paramNames[captureIndex];
|
|
272
272
|
if (parameterDefinition && capturedValue && capturedValue.length > 0) {
|
|
273
273
|
const parameterName = parameterDefinition.name;
|
|
274
|
+
if (this.opts.ignoreWildcardParameter === parameterName && parameterDefinition.type === "wildcard") {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
274
277
|
parameterValues[parameterName] = safeDecodeURIComponent(capturedValue);
|
|
275
278
|
}
|
|
276
279
|
}
|
|
@@ -903,9 +906,11 @@ var RouterImplementation = class {
|
|
|
903
906
|
finalPath = middlewarePath;
|
|
904
907
|
usePathToRegexp = false;
|
|
905
908
|
}
|
|
909
|
+
const ignoreWildcardParameter = effectiveExplicitPath === "" && middlewarePath === "{/*rest}" ? "rest" : void 0;
|
|
906
910
|
this.register(finalPath, [], middleware, {
|
|
907
911
|
end: isRootPath,
|
|
908
912
|
ignoreCaptures: !effectiveHasExplicitPath && !prefixHasParameters,
|
|
913
|
+
ignoreWildcardParameter,
|
|
909
914
|
pathAsRegExp: usePathToRegexp
|
|
910
915
|
});
|
|
911
916
|
}
|
|
@@ -927,8 +932,11 @@ var RouterImplementation = class {
|
|
|
927
932
|
const previousPrefix = this.opts.prefix || "";
|
|
928
933
|
this.opts.prefix = normalizedPrefix;
|
|
929
934
|
for (const route of this.stack) {
|
|
930
|
-
if (
|
|
931
|
-
|
|
935
|
+
if (typeof route.path === "string" && previousPrefix) {
|
|
936
|
+
const stripBoundary = previousPrefix + "/";
|
|
937
|
+
if (route.path === previousPrefix || route.path.startsWith(stripBoundary)) {
|
|
938
|
+
route.path = route.path.slice(previousPrefix.length) || "/";
|
|
939
|
+
}
|
|
932
940
|
}
|
|
933
941
|
route.setPrefix(normalizedPrefix);
|
|
934
942
|
}
|
|
@@ -1010,9 +1018,22 @@ var RouterImplementation = class {
|
|
|
1010
1018
|
* @private
|
|
1011
1019
|
*/
|
|
1012
1020
|
_buildMiddlewareChain(matchedLayers, requestPath) {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1021
|
+
let layersToExecute;
|
|
1022
|
+
if (this.exclusive) {
|
|
1023
|
+
let chosenLayer;
|
|
1024
|
+
if (this.opts.exclusive === "specificity") {
|
|
1025
|
+
for (const layer of matchedLayers) {
|
|
1026
|
+
if (!chosenLayer || layer.paramNames.length < chosenLayer.paramNames.length) {
|
|
1027
|
+
chosenLayer = layer;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
} else {
|
|
1031
|
+
chosenLayer = matchedLayers.at(-1);
|
|
1032
|
+
}
|
|
1033
|
+
layersToExecute = chosenLayer ? [chosenLayer] : [];
|
|
1034
|
+
} else {
|
|
1035
|
+
layersToExecute = matchedLayers;
|
|
1036
|
+
}
|
|
1016
1037
|
const middlewareChain = [];
|
|
1017
1038
|
for (const layer of layersToExecute) {
|
|
1018
1039
|
middlewareChain.push(
|
|
@@ -1342,6 +1363,7 @@ var RouterImplementation = class {
|
|
|
1342
1363
|
strict: options.strict || false,
|
|
1343
1364
|
prefix: options.prefix || "",
|
|
1344
1365
|
ignoreCaptures: options.ignoreCaptures,
|
|
1366
|
+
ignoreWildcardParameter: options.ignoreWildcardParameter,
|
|
1345
1367
|
pathAsRegExp: options.pathAsRegExp
|
|
1346
1368
|
});
|
|
1347
1369
|
}
|
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.6.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": "^
|
|
35
|
-
"@commitlint/config-conventional": "^
|
|
34
|
+
"@commitlint/cli": "^21.0.2",
|
|
35
|
+
"@commitlint/config-conventional": "^21.0.2",
|
|
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.3",
|
|
41
|
+
"@types/node": "^25.9.1",
|
|
42
42
|
"@types/supertest": "^7.2.0",
|
|
43
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
44
|
-
"@typescript-eslint/parser": "^8.
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^8.60.0",
|
|
44
|
+
"@typescript-eslint/parser": "^8.60.0",
|
|
45
45
|
"c8": "^11.0.0",
|
|
46
46
|
"chalk": "^5.6.2",
|
|
47
|
-
"eslint": "^10.
|
|
48
|
-
"eslint-plugin-unicorn": "^
|
|
47
|
+
"eslint": "^10.4.1",
|
|
48
|
+
"eslint-plugin-unicorn": "^64.0.0",
|
|
49
49
|
"husky": "^9.1.7",
|
|
50
|
-
"joi": "^18.
|
|
50
|
+
"joi": "^18.2.1",
|
|
51
51
|
"jsonwebtoken": "^9.0.3",
|
|
52
|
-
"koa": "^3.1
|
|
53
|
-
"lint-staged": "^
|
|
54
|
-
"np": "^11.
|
|
55
|
-
"prettier": "^3.8.
|
|
52
|
+
"koa": "^3.2.1",
|
|
53
|
+
"lint-staged": "^17.0.6",
|
|
54
|
+
"np": "^11.2.1",
|
|
55
|
+
"prettier": "^3.8.3",
|
|
56
56
|
"rimraf": "^6.1.3",
|
|
57
57
|
"supertest": "^7.2.2",
|
|
58
58
|
"ts-node": "^10.9.2",
|