@koa/router 15.5.0 → 15.7.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
@@ -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
 
@@ -457,7 +458,26 @@ router.get('/users', (ctx) => {
457
458
  // Responds to /api/v1/users, /api/v2/users, etc.
458
459
  ```
459
460
 
460
- **Note:** Middleware now correctly executes when the prefix contains parameters.
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
@@ -821,6 +821,12 @@ type LayerOptions = {
821
821
  * Ignore captures in route matching
822
822
  */
823
823
  ignoreCaptures?: boolean;
824
+ /**
825
+ * Ignore a generated wildcard route parameter when populating ctx.params
826
+ *
827
+ * @internal
828
+ */
829
+ ignoreWildcardParameter?: string | number;
824
830
  /**
825
831
  * Treat path as a regular expression
826
832
  */
package/dist/index.d.ts CHANGED
@@ -821,6 +821,12 @@ type LayerOptions = {
821
821
  * Ignore captures in route matching
822
822
  */
823
823
  ignoreCaptures?: boolean;
824
+ /**
825
+ * Ignore a generated wildcard route parameter when populating ctx.params
826
+ *
827
+ * @internal
828
+ */
829
+ ignoreWildcardParameter?: string | number;
824
830
  /**
825
831
  * Treat path as a regular expression
826
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
  }
@@ -516,12 +519,12 @@ var Layer = class {
516
519
  * @private
517
520
  */
518
521
  _insertParamMiddleware(middlewareStack, parameterMiddleware, parameterNamesList, currentParameterPosition) {
519
- let inserted = false;
522
+ let isInserted = false;
520
523
  for (let stackIndex = 0; stackIndex < middlewareStack.length; stackIndex++) {
521
524
  const existingMiddleware = middlewareStack[stackIndex];
522
525
  if (!existingMiddleware.param) {
523
526
  middlewareStack.splice(stackIndex, 0, parameterMiddleware);
524
- inserted = true;
527
+ isInserted = true;
525
528
  break;
526
529
  }
527
530
  const existingParameterPosition = parameterNamesList.indexOf(
@@ -529,11 +532,11 @@ var Layer = class {
529
532
  );
530
533
  if (existingParameterPosition > currentParameterPosition) {
531
534
  middlewareStack.splice(stackIndex, 0, parameterMiddleware);
532
- inserted = true;
535
+ isInserted = true;
533
536
  break;
534
537
  }
535
538
  }
536
- if (!inserted) {
539
+ if (!isInserted) {
537
540
  middlewareStack.push(parameterMiddleware);
538
541
  }
539
542
  }
@@ -562,9 +565,9 @@ var Layer = class {
562
565
  _applyPrefix(prefixPath) {
563
566
  const isRootPath = this.path === "/";
564
567
  const isStrictMode = this.opts.strict === true;
565
- const prefixHasParameters = prefixPath.includes(":");
566
- const pathIsRawRegex = this.opts.pathAsRegExp === true && typeof this.path === "string";
567
- if (prefixHasParameters && pathIsRawRegex) {
568
+ const isPrefixHasParameters = prefixPath.includes(":");
569
+ const isPathIsRawRegex = this.opts.pathAsRegExp === true && typeof this.path === "string";
570
+ if (isPrefixHasParameters && isPathIsRawRegex) {
568
571
  const currentPath = this.path;
569
572
  if (currentPath === String.raw`(?:\/|$)` || currentPath === String.raw`(?:\\\/|$)`) {
570
573
  this.path = "{/*rest}";
@@ -581,9 +584,9 @@ var Layer = class {
581
584
  * @private
582
585
  */
583
586
  _reconfigurePathMatching(prefixPath) {
584
- const treatAsRegExp = this.opts.pathAsRegExp === true;
587
+ const isTreatAsRegExp = this.opts.pathAsRegExp === true;
585
588
  const prefixHasParameters = prefixPath && prefixPath.includes(":");
586
- if (prefixHasParameters && treatAsRegExp) {
589
+ if (prefixHasParameters && isTreatAsRegExp) {
587
590
  const options = normalizeLayerOptionsToPathToRegexp(this.opts);
588
591
  const { regexp, keys } = compilePathToRegexp(
589
592
  this.path,
@@ -592,7 +595,7 @@ var Layer = class {
592
595
  this.regexp = regexp;
593
596
  this.paramNames = keys;
594
597
  this.opts.pathAsRegExp = false;
595
- } else if (treatAsRegExp) {
598
+ } else if (isTreatAsRegExp) {
596
599
  const pathString = this.path;
597
600
  const anchoredPattern = pathString.startsWith("^") ? pathString : `^${pathString}`;
598
601
  this.regexp = this.path instanceof RegExp ? this.path : new RegExp(anchoredPattern);
@@ -780,10 +783,10 @@ var RouterImplementation = class {
780
783
  return temporaryLayer.url(...arguments_);
781
784
  }
782
785
  use(...middleware) {
783
- let explicitPath;
784
786
  if (this._isPathArray(middleware[0])) {
785
787
  return this._useWithPathArray(middleware);
786
788
  }
789
+ let explicitPath;
787
790
  const hasExplicitPath = this._hasExplicitPath(middleware[0]);
788
791
  if (hasExplicitPath) {
789
792
  explicitPath = middleware.shift();
@@ -921,31 +924,33 @@ var RouterImplementation = class {
921
924
  * @private
922
925
  */
923
926
  _registerMiddleware(middleware, explicitPath, hasExplicitPath) {
924
- const prefixHasParameters = hasPathParameters(
927
+ const isPrefixHasParameters = hasPathParameters(
925
928
  this.opts.prefix || "",
926
929
  this.opts
927
930
  );
928
931
  const effectiveExplicitPath = (() => {
929
932
  if (explicitPath !== void 0) return explicitPath;
930
- if (prefixHasParameters) return "";
933
+ if (isPrefixHasParameters) return "";
931
934
  return;
932
935
  })();
933
- const effectiveHasExplicitPath = hasExplicitPath || explicitPath === void 0 && prefixHasParameters;
936
+ const isEffectiveHasExplicitPath = hasExplicitPath || explicitPath === void 0 && isPrefixHasParameters;
934
937
  const { path: middlewarePath, pathAsRegExp } = determineMiddlewarePath(
935
938
  effectiveExplicitPath,
936
- prefixHasParameters
939
+ isPrefixHasParameters
937
940
  );
938
941
  let finalPath = middlewarePath;
939
- let usePathToRegexp = pathAsRegExp;
940
- const isRootPath = effectiveHasExplicitPath && middlewarePath === "/";
941
- if (effectiveHasExplicitPath && typeof middlewarePath === "string") {
942
+ let isUsePathToRegexp = pathAsRegExp;
943
+ const isRootPath = isEffectiveHasExplicitPath && middlewarePath === "/";
944
+ if (isEffectiveHasExplicitPath && typeof middlewarePath === "string") {
942
945
  finalPath = middlewarePath;
943
- usePathToRegexp = false;
946
+ isUsePathToRegexp = false;
944
947
  }
948
+ const ignoreWildcardParameter = effectiveExplicitPath === "" && middlewarePath === "{/*rest}" ? "rest" : void 0;
945
949
  this.register(finalPath, [], middleware, {
946
950
  end: isRootPath,
947
- ignoreCaptures: !effectiveHasExplicitPath && !prefixHasParameters,
948
- pathAsRegExp: usePathToRegexp
951
+ ignoreCaptures: !isEffectiveHasExplicitPath && !isPrefixHasParameters,
952
+ ignoreWildcardParameter,
953
+ pathAsRegExp: isUsePathToRegexp
949
954
  });
950
955
  }
951
956
  /**
@@ -1397,6 +1402,7 @@ var RouterImplementation = class {
1397
1402
  strict: options.strict || false,
1398
1403
  prefix: options.prefix || "",
1399
1404
  ignoreCaptures: options.ignoreCaptures,
1405
+ ignoreWildcardParameter: options.ignoreWildcardParameter,
1400
1406
  pathAsRegExp: options.pathAsRegExp
1401
1407
  });
1402
1408
  }
@@ -1467,8 +1473,8 @@ var RouterImplementation = class {
1467
1473
  if (layer.match(path)) {
1468
1474
  matchResult.path.push(layer);
1469
1475
  const isMiddleware = layer.methods.length === 0;
1470
- const matchesMethod = layer.methods.includes(normalizedMethod);
1471
- if (isMiddleware || matchesMethod) {
1476
+ const isMatchesMethod = layer.methods.includes(normalizedMethod);
1477
+ if (isMiddleware || isMatchesMethod) {
1472
1478
  matchResult.pathAndMethod.push(layer);
1473
1479
  if (layer.methods.length > 0) {
1474
1480
  matchResult.route = true;
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
  }
@@ -477,12 +480,12 @@ var Layer = class {
477
480
  * @private
478
481
  */
479
482
  _insertParamMiddleware(middlewareStack, parameterMiddleware, parameterNamesList, currentParameterPosition) {
480
- let inserted = false;
483
+ let isInserted = false;
481
484
  for (let stackIndex = 0; stackIndex < middlewareStack.length; stackIndex++) {
482
485
  const existingMiddleware = middlewareStack[stackIndex];
483
486
  if (!existingMiddleware.param) {
484
487
  middlewareStack.splice(stackIndex, 0, parameterMiddleware);
485
- inserted = true;
488
+ isInserted = true;
486
489
  break;
487
490
  }
488
491
  const existingParameterPosition = parameterNamesList.indexOf(
@@ -490,11 +493,11 @@ var Layer = class {
490
493
  );
491
494
  if (existingParameterPosition > currentParameterPosition) {
492
495
  middlewareStack.splice(stackIndex, 0, parameterMiddleware);
493
- inserted = true;
496
+ isInserted = true;
494
497
  break;
495
498
  }
496
499
  }
497
- if (!inserted) {
500
+ if (!isInserted) {
498
501
  middlewareStack.push(parameterMiddleware);
499
502
  }
500
503
  }
@@ -523,9 +526,9 @@ var Layer = class {
523
526
  _applyPrefix(prefixPath) {
524
527
  const isRootPath = this.path === "/";
525
528
  const isStrictMode = this.opts.strict === true;
526
- const prefixHasParameters = prefixPath.includes(":");
527
- const pathIsRawRegex = this.opts.pathAsRegExp === true && typeof this.path === "string";
528
- if (prefixHasParameters && pathIsRawRegex) {
529
+ const isPrefixHasParameters = prefixPath.includes(":");
530
+ const isPathIsRawRegex = this.opts.pathAsRegExp === true && typeof this.path === "string";
531
+ if (isPrefixHasParameters && isPathIsRawRegex) {
529
532
  const currentPath = this.path;
530
533
  if (currentPath === String.raw`(?:\/|$)` || currentPath === String.raw`(?:\\\/|$)`) {
531
534
  this.path = "{/*rest}";
@@ -542,9 +545,9 @@ var Layer = class {
542
545
  * @private
543
546
  */
544
547
  _reconfigurePathMatching(prefixPath) {
545
- const treatAsRegExp = this.opts.pathAsRegExp === true;
548
+ const isTreatAsRegExp = this.opts.pathAsRegExp === true;
546
549
  const prefixHasParameters = prefixPath && prefixPath.includes(":");
547
- if (prefixHasParameters && treatAsRegExp) {
550
+ if (prefixHasParameters && isTreatAsRegExp) {
548
551
  const options = normalizeLayerOptionsToPathToRegexp(this.opts);
549
552
  const { regexp, keys } = compilePathToRegexp(
550
553
  this.path,
@@ -553,7 +556,7 @@ var Layer = class {
553
556
  this.regexp = regexp;
554
557
  this.paramNames = keys;
555
558
  this.opts.pathAsRegExp = false;
556
- } else if (treatAsRegExp) {
559
+ } else if (isTreatAsRegExp) {
557
560
  const pathString = this.path;
558
561
  const anchoredPattern = pathString.startsWith("^") ? pathString : `^${pathString}`;
559
562
  this.regexp = this.path instanceof RegExp ? this.path : new RegExp(anchoredPattern);
@@ -741,10 +744,10 @@ var RouterImplementation = class {
741
744
  return temporaryLayer.url(...arguments_);
742
745
  }
743
746
  use(...middleware) {
744
- let explicitPath;
745
747
  if (this._isPathArray(middleware[0])) {
746
748
  return this._useWithPathArray(middleware);
747
749
  }
750
+ let explicitPath;
748
751
  const hasExplicitPath = this._hasExplicitPath(middleware[0]);
749
752
  if (hasExplicitPath) {
750
753
  explicitPath = middleware.shift();
@@ -882,31 +885,33 @@ var RouterImplementation = class {
882
885
  * @private
883
886
  */
884
887
  _registerMiddleware(middleware, explicitPath, hasExplicitPath) {
885
- const prefixHasParameters = hasPathParameters(
888
+ const isPrefixHasParameters = hasPathParameters(
886
889
  this.opts.prefix || "",
887
890
  this.opts
888
891
  );
889
892
  const effectiveExplicitPath = (() => {
890
893
  if (explicitPath !== void 0) return explicitPath;
891
- if (prefixHasParameters) return "";
894
+ if (isPrefixHasParameters) return "";
892
895
  return;
893
896
  })();
894
- const effectiveHasExplicitPath = hasExplicitPath || explicitPath === void 0 && prefixHasParameters;
897
+ const isEffectiveHasExplicitPath = hasExplicitPath || explicitPath === void 0 && isPrefixHasParameters;
895
898
  const { path: middlewarePath, pathAsRegExp } = determineMiddlewarePath(
896
899
  effectiveExplicitPath,
897
- prefixHasParameters
900
+ isPrefixHasParameters
898
901
  );
899
902
  let finalPath = middlewarePath;
900
- let usePathToRegexp = pathAsRegExp;
901
- const isRootPath = effectiveHasExplicitPath && middlewarePath === "/";
902
- if (effectiveHasExplicitPath && typeof middlewarePath === "string") {
903
+ let isUsePathToRegexp = pathAsRegExp;
904
+ const isRootPath = isEffectiveHasExplicitPath && middlewarePath === "/";
905
+ if (isEffectiveHasExplicitPath && typeof middlewarePath === "string") {
903
906
  finalPath = middlewarePath;
904
- usePathToRegexp = false;
907
+ isUsePathToRegexp = false;
905
908
  }
909
+ const ignoreWildcardParameter = effectiveExplicitPath === "" && middlewarePath === "{/*rest}" ? "rest" : void 0;
906
910
  this.register(finalPath, [], middleware, {
907
911
  end: isRootPath,
908
- ignoreCaptures: !effectiveHasExplicitPath && !prefixHasParameters,
909
- pathAsRegExp: usePathToRegexp
912
+ ignoreCaptures: !isEffectiveHasExplicitPath && !isPrefixHasParameters,
913
+ ignoreWildcardParameter,
914
+ pathAsRegExp: isUsePathToRegexp
910
915
  });
911
916
  }
912
917
  /**
@@ -1358,6 +1363,7 @@ var RouterImplementation = class {
1358
1363
  strict: options.strict || false,
1359
1364
  prefix: options.prefix || "",
1360
1365
  ignoreCaptures: options.ignoreCaptures,
1366
+ ignoreWildcardParameter: options.ignoreWildcardParameter,
1361
1367
  pathAsRegExp: options.pathAsRegExp
1362
1368
  });
1363
1369
  }
@@ -1428,8 +1434,8 @@ var RouterImplementation = class {
1428
1434
  if (layer.match(path)) {
1429
1435
  matchResult.path.push(layer);
1430
1436
  const isMiddleware = layer.methods.length === 0;
1431
- const matchesMethod = layer.methods.includes(normalizedMethod);
1432
- if (isMiddleware || matchesMethod) {
1437
+ const isMatchesMethod = layer.methods.includes(normalizedMethod);
1438
+ if (isMiddleware || isMatchesMethod) {
1433
1439
  matchResult.pathAndMethod.push(layer);
1434
1440
  if (layer.methods.length > 0) {
1435
1441
  matchResult.route = true;
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.5.0",
4
+ "version": "15.7.0",
5
5
  "author": "Koa.js",
6
6
  "bugs": {
7
7
  "url": "https://github.com/koajs/router/issues",
@@ -31,28 +31,28 @@
31
31
  "path-to-regexp": "^8.4.2"
32
32
  },
33
33
  "devDependencies": {
34
- "@commitlint/cli": "^20.5.3",
35
- "@commitlint/config-conventional": "^20.5.3",
34
+ "@commitlint/cli": "^21.2.0",
35
+ "@commitlint/config-conventional": "^21.2.0",
36
36
  "@eslint/js": "^10.0.1",
37
37
  "@koa/bodyparser": "^6.1.0",
38
38
  "@types/debug": "^4.1.13",
39
39
  "@types/jsonwebtoken": "^9.0.10",
40
- "@types/koa": "^3.0.2",
41
- "@types/node": "^25.6.0",
40
+ "@types/koa": "^3.0.3",
41
+ "@types/node": "^26.1.0",
42
42
  "@types/supertest": "^7.2.0",
43
- "@typescript-eslint/eslint-plugin": "^8.59.1",
44
- "@typescript-eslint/parser": "^8.59.1",
43
+ "@typescript-eslint/eslint-plugin": "^8.62.1",
44
+ "@typescript-eslint/parser": "^8.62.1",
45
45
  "c8": "^11.0.0",
46
46
  "chalk": "^5.6.2",
47
- "eslint": "^10.3.0",
48
- "eslint-plugin-unicorn": "^64.0.0",
47
+ "eslint": "^10.6.0",
48
+ "eslint-plugin-unicorn": "^70.0.0",
49
49
  "husky": "^9.1.7",
50
- "joi": "^18.1.2",
50
+ "joi": "^18.2.3",
51
51
  "jsonwebtoken": "^9.0.3",
52
- "koa": "^3.2.0",
53
- "lint-staged": "^16.4.0",
54
- "np": "^11.2.0",
55
- "prettier": "^3.8.3",
52
+ "koa": "^3.2.1",
53
+ "lint-staged": "^17.0.8",
54
+ "np": "^11.2.1",
55
+ "prettier": "^3.9.4",
56
56
  "rimraf": "^6.1.3",
57
57
  "supertest": "^7.2.2",
58
58
  "ts-node": "^10.9.2",