@koa/router 15.4.0 → 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 CHANGED
@@ -266,14 +266,14 @@ Create a new router instance.
266
266
 
267
267
  **Options:**
268
268
 
269
- | Option | Type | Description |
270
- | ----------- | ------------------------------ | ----------------------------------------- |
271
- | `prefix` | `string` | Prefix all routes with this path |
272
- | `exclusive` | `boolean` | Only run the most specific matching route |
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 |
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 |
277
277
 
278
278
  **Example:**
279
279
 
package/dist/index.d.mts CHANGED
@@ -748,9 +748,18 @@ declare const Router: RouterConstructor;
748
748
 
749
749
  type RouterOptions = {
750
750
  /**
751
- * Only run last matched route's controller when there are multiple matches
752
- */
753
- exclusive?: boolean;
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
761
+ */
762
+ exclusive?: boolean | 'specificity';
754
763
  /**
755
764
  * Prefix for all routes
756
765
  */
package/dist/index.d.ts CHANGED
@@ -748,9 +748,18 @@ declare const Router: RouterConstructor;
748
748
 
749
749
  type RouterOptions = {
750
750
  /**
751
- * Only run last matched route's controller when there are multiple matches
752
- */
753
- exclusive?: boolean;
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
761
+ */
762
+ exclusive?: boolean | 'specificity';
754
763
  /**
755
764
  * Prefix for all routes
756
765
  */
package/dist/index.js CHANGED
@@ -966,8 +966,11 @@ var RouterImplementation = class {
966
966
  const previousPrefix = this.opts.prefix || "";
967
967
  this.opts.prefix = normalizedPrefix;
968
968
  for (const route of this.stack) {
969
- if (previousPrefix && typeof route.path === "string" && route.path.startsWith(previousPrefix)) {
970
- route.path = route.path.slice(previousPrefix.length) || "/";
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
+ }
971
974
  }
972
975
  route.setPrefix(normalizedPrefix);
973
976
  }
@@ -1049,9 +1052,22 @@ var RouterImplementation = class {
1049
1052
  * @private
1050
1053
  */
1051
1054
  _buildMiddlewareChain(matchedLayers, requestPath) {
1052
- const layersToExecute = this.opts.exclusive ? [matchedLayers.at(-1)].filter(
1053
- (layer) => layer !== void 0
1054
- ) : matchedLayers;
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
+ }
1055
1071
  const middlewareChain = [];
1056
1072
  for (const layer of layersToExecute) {
1057
1073
  middlewareChain.push(
package/dist/index.mjs CHANGED
@@ -927,8 +927,11 @@ var RouterImplementation = class {
927
927
  const previousPrefix = this.opts.prefix || "";
928
928
  this.opts.prefix = normalizedPrefix;
929
929
  for (const route of this.stack) {
930
- if (previousPrefix && typeof route.path === "string" && route.path.startsWith(previousPrefix)) {
931
- route.path = route.path.slice(previousPrefix.length) || "/";
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
+ }
932
935
  }
933
936
  route.setPrefix(normalizedPrefix);
934
937
  }
@@ -1010,9 +1013,22 @@ var RouterImplementation = class {
1010
1013
  * @private
1011
1014
  */
1012
1015
  _buildMiddlewareChain(matchedLayers, requestPath) {
1013
- const layersToExecute = this.opts.exclusive ? [matchedLayers.at(-1)].filter(
1014
- (layer) => layer !== void 0
1015
- ) : matchedLayers;
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
+ }
1016
1032
  const middlewareChain = [];
1017
1033
  for (const layer of layersToExecute) {
1018
1034
  middlewareChain.push(
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.0",
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.3.0"
31
+ "path-to-regexp": "^8.4.2"
32
32
  },
33
33
  "devDependencies": {
34
- "@commitlint/cli": "^20.4.4",
35
- "@commitlint/config-conventional": "^20.4.4",
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.12",
38
+ "@types/debug": "^4.1.13",
39
39
  "@types/jsonwebtoken": "^9.0.10",
40
- "@types/koa": "^3.0.1",
41
- "@types/node": "^25.5.0",
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.57.0",
44
- "@typescript-eslint/parser": "^8.57.0",
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.3",
48
- "eslint-plugin-unicorn": "^63.0.0",
47
+ "eslint": "^10.3.0",
48
+ "eslint-plugin-unicorn": "^64.0.0",
49
49
  "husky": "^9.1.7",
50
- "joi": "^18.0.2",
50
+ "joi": "^18.1.2",
51
51
  "jsonwebtoken": "^9.0.3",
52
- "koa": "^3.1.2",
52
+ "koa": "^3.2.0",
53
53
  "lint-staged": "^16.4.0",
54
- "np": "^11.0.2",
55
- "prettier": "^3.8.1",
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",