@koa/router 15.0.0 → 15.1.1

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/dist/index.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  // src/router.ts
2
- import debugModule from "debug";
3
2
  import compose from "koa-compose";
4
3
  import HttpError from "http-errors";
5
4
 
@@ -45,7 +44,7 @@ function normalizeLayerOptionsToPathToRegexp(options = {}) {
45
44
  return normalized;
46
45
  }
47
46
 
48
- // src/layer.ts
47
+ // src/utils/safe-decode-uri-components.ts
49
48
  function safeDecodeURIComponent(text) {
50
49
  try {
51
50
  return decodeURIComponent(text);
@@ -53,6 +52,8 @@ function safeDecodeURIComponent(text) {
53
52
  return text;
54
53
  }
55
54
  }
55
+
56
+ // src/layer.ts
56
57
  var Layer = class {
57
58
  opts;
58
59
  name;
@@ -193,9 +194,15 @@ var Layer = class {
193
194
  *
194
195
  * @param args - URL parameters (various formats supported)
195
196
  * @returns Generated URL
197
+ * @throws Error if route path is a RegExp (cannot generate URL from RegExp)
196
198
  * @private
197
199
  */
198
200
  url(...arguments_) {
201
+ if (this.path instanceof RegExp) {
202
+ throw new TypeError(
203
+ "Cannot generate URL for routes defined with RegExp paths. Use string paths with named parameters instead."
204
+ );
205
+ }
199
206
  const { params, options } = this._parseUrlArguments(arguments_);
200
207
  const cleanPath = this.path.replaceAll("(.*)", "");
201
208
  const pathCompiler = compilePath(cleanPath, {
@@ -222,20 +229,28 @@ var Layer = class {
222
229
  * @private
223
230
  */
224
231
  _parseUrlArguments(allArguments) {
225
- let parameters = allArguments[0];
232
+ let parameters = allArguments[0] ?? {};
226
233
  let options = allArguments[1];
227
- if (typeof parameters !== "object") {
234
+ if (typeof parameters !== "object" || parameters === null) {
228
235
  const argumentsList = [...allArguments];
229
236
  const lastArgument = argumentsList.at(-1);
230
- if (typeof lastArgument === "object") {
237
+ if (typeof lastArgument === "object" && lastArgument !== null) {
231
238
  options = lastArgument;
232
239
  parameters = argumentsList.slice(0, -1);
233
240
  } else {
234
241
  parameters = argumentsList;
235
242
  }
236
- } else if (parameters && parameters.query && !options) {
237
- options = parameters;
238
- parameters = {};
243
+ } else if (parameters && !options) {
244
+ const parameterKeys = Object.keys(parameters);
245
+ const isOnlyOptions = parameterKeys.length === 1 && parameterKeys[0] === "query";
246
+ if (isOnlyOptions) {
247
+ options = parameters;
248
+ parameters = {};
249
+ } else if ("query" in parameters && parameters.query) {
250
+ const { query, ...restParameters } = parameters;
251
+ options = { query };
252
+ parameters = restParameters;
253
+ }
239
254
  }
240
255
  return { params: parameters, options };
241
256
  }
@@ -258,7 +273,7 @@ var Layer = class {
258
273
  );
259
274
  }
260
275
  }
261
- } else if (hasNamedParameters && typeof parameters === "object" && !parameters.query) {
276
+ } else if (hasNamedParameters && typeof parameters === "object" && !("query" in parameters)) {
262
277
  for (const [parameterName, parameterValue] of Object.entries(
263
278
  parameters
264
279
  )) {
@@ -272,14 +287,19 @@ var Layer = class {
272
287
  * @private
273
288
  */
274
289
  _addQueryString(baseUrl, query) {
275
- const parsedUrl = parseUrl(baseUrl);
290
+ const parsed = parseUrl(baseUrl);
291
+ const urlObject = {
292
+ ...parsed,
293
+ query: parsed.query ?? void 0
294
+ };
276
295
  if (typeof query === "string") {
277
- parsedUrl.search = query;
296
+ urlObject.search = query;
297
+ urlObject.query = void 0;
278
298
  } else {
279
- parsedUrl.search = void 0;
280
- parsedUrl.query = query;
299
+ urlObject.search = void 0;
300
+ urlObject.query = query;
281
301
  }
282
- return formatUrl(parsedUrl);
302
+ return formatUrl(urlObject);
283
303
  }
284
304
  /**
285
305
  * Run validations on route named parameters.
@@ -329,7 +349,7 @@ var Layer = class {
329
349
  * @private
330
350
  */
331
351
  _createParamMiddleware(parameterName, parameterHandler) {
332
- const middleware = function(context, next) {
352
+ const middleware = ((context, next) => {
333
353
  if (!context._matchedParams) {
334
354
  context._matchedParams = /* @__PURE__ */ new WeakMap();
335
355
  }
@@ -337,13 +357,8 @@ var Layer = class {
337
357
  return next();
338
358
  }
339
359
  context._matchedParams.set(parameterHandler, true);
340
- return parameterHandler.call(
341
- this,
342
- context.params[parameterName],
343
- context,
344
- next
345
- );
346
- };
360
+ return parameterHandler(context.params[parameterName], context, next);
361
+ });
347
362
  middleware.param = parameterName;
348
363
  middleware._originalFn = parameterHandler;
349
364
  return middleware;
@@ -353,20 +368,26 @@ var Layer = class {
353
368
  * @private
354
369
  */
355
370
  _insertParamMiddleware(middlewareStack, parameterMiddleware, parameterNamesList, currentParameterPosition) {
356
- middlewareStack.some((existingMiddleware, stackIndex) => {
371
+ let inserted = false;
372
+ for (let stackIndex = 0; stackIndex < middlewareStack.length; stackIndex++) {
373
+ const existingMiddleware = middlewareStack[stackIndex];
357
374
  if (!existingMiddleware.param) {
358
375
  middlewareStack.splice(stackIndex, 0, parameterMiddleware);
359
- return true;
376
+ inserted = true;
377
+ break;
360
378
  }
361
379
  const existingParameterPosition = parameterNamesList.indexOf(
362
380
  existingMiddleware.param
363
381
  );
364
382
  if (existingParameterPosition > currentParameterPosition) {
365
383
  middlewareStack.splice(stackIndex, 0, parameterMiddleware);
366
- return true;
384
+ inserted = true;
385
+ break;
367
386
  }
368
- return false;
369
- });
387
+ }
388
+ if (!inserted) {
389
+ middlewareStack.push(parameterMiddleware);
390
+ }
370
391
  }
371
392
  /**
372
393
  * Prefix route path.
@@ -397,7 +418,7 @@ var Layer = class {
397
418
  const pathIsRawRegex = this.opts.pathAsRegExp === true && typeof this.path === "string";
398
419
  if (prefixHasParameters && pathIsRawRegex) {
399
420
  const currentPath = this.path;
400
- if (currentPath === String.raw`(?:\/|$)` || currentPath === String.raw`(?:\/|$)`) {
421
+ if (currentPath === String.raw`(?:\/|$)` || currentPath === String.raw`(?:\\\/|$)`) {
401
422
  this.path = "{/*rest}";
402
423
  this.opts.pathAsRegExp = false;
403
424
  }
@@ -424,7 +445,9 @@ var Layer = class {
424
445
  this.paramNames = keys;
425
446
  this.opts.pathAsRegExp = false;
426
447
  } else if (treatAsRegExp) {
427
- this.regexp = this.path instanceof RegExp ? this.path : new RegExp(this.path);
448
+ const pathString = this.path;
449
+ const anchoredPattern = pathString.startsWith("^") ? pathString : `^${pathString}`;
450
+ this.regexp = this.path instanceof RegExp ? this.path : new RegExp(anchoredPattern);
428
451
  } else {
429
452
  const options = normalizeLayerOptionsToPathToRegexp(this.opts);
430
453
  const { regexp, keys } = compilePathToRegexp(
@@ -464,7 +487,9 @@ function normalizeParameterMiddleware(parameterMiddleware) {
464
487
  return [parameterMiddleware];
465
488
  }
466
489
  function applyParameterMiddlewareToRoute(route, parameterName, parameterMiddleware) {
467
- const middlewareList = normalizeParameterMiddleware(parameterMiddleware);
490
+ const middlewareList = normalizeParameterMiddleware(
491
+ parameterMiddleware
492
+ );
468
493
  for (const middleware of middlewareList) {
469
494
  route.param(parameterName, middleware);
470
495
  }
@@ -473,7 +498,11 @@ function applyAllParameterMiddleware(route, parametersObject) {
473
498
  const parameterNames = Object.keys(parametersObject);
474
499
  for (const parameterName of parameterNames) {
475
500
  const parameterMiddleware = parametersObject[parameterName];
476
- applyParameterMiddlewareToRoute(route, parameterName, parameterMiddleware);
501
+ applyParameterMiddlewareToRoute(
502
+ route,
503
+ parameterName,
504
+ parameterMiddleware
505
+ );
477
506
  }
478
507
  }
479
508
 
@@ -522,8 +551,11 @@ function determineMiddlewarePath(explicitPath, hasPrefixParameters) {
522
551
  };
523
552
  }
524
553
 
525
- // src/router.ts
554
+ // src/utils/debug.ts
555
+ import debugModule from "debug";
526
556
  var debug = debugModule("koa-router");
557
+
558
+ // src/router.ts
527
559
  var httpMethods = getAllHttpMethods();
528
560
  var Router = class {
529
561
  opts;
@@ -603,6 +635,11 @@ var Router = class {
603
635
  if (hasExplicitPath) {
604
636
  explicitPath = middleware.shift();
605
637
  }
638
+ if (middleware.length === 0) {
639
+ throw new Error(
640
+ "You must provide at least one middleware function to router.use()"
641
+ );
642
+ }
606
643
  for (const currentMiddleware of middleware) {
607
644
  if (this._isNestedRouter(currentMiddleware)) {
608
645
  this._mountNestedRouter(
@@ -620,11 +657,11 @@ var Router = class {
620
657
  return this;
621
658
  }
622
659
  /**
623
- * Check if first argument is an array of paths
660
+ * Check if first argument is an array of paths (all elements must be strings)
624
661
  * @private
625
662
  */
626
663
  _isPathArray(firstArgument) {
627
- return Array.isArray(firstArgument) && typeof firstArgument[0] === "string";
664
+ return Array.isArray(firstArgument) && firstArgument.length > 0 && firstArgument.every((item) => typeof item === "string");
628
665
  }
629
666
  /**
630
667
  * Check if first argument is an explicit path (string or RegExp)
@@ -639,7 +676,7 @@ var Router = class {
639
676
  * @private
640
677
  */
641
678
  _isNestedRouter(middleware) {
642
- return middleware.router !== void 0;
679
+ return typeof middleware === "function" && "router" in middleware && middleware.router !== void 0;
643
680
  }
644
681
  /**
645
682
  * Apply middleware to multiple paths
@@ -694,14 +731,22 @@ var Router = class {
694
731
  );
695
732
  }
696
733
  /**
697
- * Clone a layer instance
734
+ * Clone a layer instance (deep clone to avoid shared references)
698
735
  * @private
699
736
  */
700
737
  _cloneLayer(sourceLayer) {
701
- return Object.assign(
738
+ const cloned = Object.assign(
702
739
  Object.create(Object.getPrototypeOf(sourceLayer)),
703
- sourceLayer
740
+ sourceLayer,
741
+ {
742
+ // Deep clone arrays and objects to avoid shared references
743
+ stack: [...sourceLayer.stack],
744
+ methods: [...sourceLayer.methods],
745
+ paramNames: [...sourceLayer.paramNames],
746
+ opts: { ...sourceLayer.opts }
747
+ }
704
748
  );
749
+ return cloned;
705
750
  }
706
751
  /**
707
752
  * Apply this router's param middleware to a nested router
@@ -752,6 +797,7 @@ var Router = class {
752
797
  }
753
798
  /**
754
799
  * Set the path prefix for a Router instance that was already initialized.
800
+ * Note: Calling this method multiple times will replace the prefix, not stack them.
755
801
  *
756
802
  * @example
757
803
  *
@@ -764,8 +810,12 @@ var Router = class {
764
810
  */
765
811
  prefix(prefixPath) {
766
812
  const normalizedPrefix = prefixPath.replace(/\/$/, "");
813
+ const previousPrefix = this.opts.prefix || "";
767
814
  this.opts.prefix = normalizedPrefix;
768
815
  for (const route of this.stack) {
816
+ if (previousPrefix && typeof route.path === "string" && route.path.startsWith(previousPrefix)) {
817
+ route.path = route.path.slice(previousPrefix.length) || "/";
818
+ }
769
819
  route.setPrefix(normalizedPrefix);
770
820
  }
771
821
  return this;
@@ -794,7 +844,10 @@ var Router = class {
794
844
  matchedLayers,
795
845
  requestPath
796
846
  );
797
- return compose(middlewareChain)(context, next);
847
+ return compose(middlewareChain)(
848
+ context,
849
+ next
850
+ );
798
851
  }.bind(this);
799
852
  dispatchMiddleware.router = this;
800
853
  return dispatchMiddleware;
@@ -804,17 +857,19 @@ var Router = class {
804
857
  * @private
805
858
  */
806
859
  _getRequestPath(context) {
807
- return this.opts.routerPath || context.newRouterPath || context.path || context.routerPath || "";
860
+ const context_ = context;
861
+ return this.opts.routerPath || context_.newRouterPath || context_.path || context_.routerPath || "";
808
862
  }
809
863
  /**
810
864
  * Store matched routes on context
811
865
  * @private
812
866
  */
813
867
  _storeMatchedRoutes(context, matchResult) {
814
- if (context.matched) {
815
- context.matched.push(...matchResult.path);
868
+ const context_ = context;
869
+ if (context_.matched) {
870
+ context_.matched.push(...matchResult.path);
816
871
  } else {
817
- context.matched = matchResult.path;
872
+ context_.matched = matchResult.path;
818
873
  }
819
874
  }
820
875
  /**
@@ -822,11 +877,12 @@ var Router = class {
822
877
  * @private
823
878
  */
824
879
  _setMatchedRouteInfo(context, matchedLayers) {
880
+ const context_ = context;
825
881
  const routeLayer = matchedLayers.toReversed().find((layer) => layer.methods.length > 0);
826
882
  if (routeLayer) {
827
- context._matchedRoute = routeLayer.path;
883
+ context_._matchedRoute = routeLayer.path;
828
884
  if (routeLayer.name) {
829
- context._matchedRouteName = routeLayer.name;
885
+ context_._matchedRouteName = routeLayer.name;
830
886
  }
831
887
  }
832
888
  }
@@ -913,11 +969,11 @@ var Router = class {
913
969
  if (!this._shouldProcessAllowedMethods(routerContext)) {
914
970
  return;
915
971
  }
916
- const allowedMethods = this._collectAllowedMethods(
917
- routerContext.matched
918
- );
972
+ const matchedRoutes = routerContext.matched || [];
973
+ const allowedMethods = this._collectAllowedMethods(matchedRoutes);
919
974
  const allowedMethodsList = Object.keys(allowedMethods);
920
- if (!implementedMethods.includes(context.method)) {
975
+ const requestMethod = context.method.toUpperCase();
976
+ if (!implementedMethods.includes(requestMethod)) {
921
977
  this._handleNotImplemented(
922
978
  routerContext,
923
979
  allowedMethodsList,
@@ -925,11 +981,11 @@ var Router = class {
925
981
  );
926
982
  return;
927
983
  }
928
- if (context.method === "OPTIONS" && allowedMethodsList.length > 0) {
984
+ if (requestMethod === "OPTIONS" && allowedMethodsList.length > 0) {
929
985
  this._handleOptionsRequest(routerContext, allowedMethodsList);
930
986
  return;
931
987
  }
932
- if (allowedMethodsList.length > 0 && !allowedMethods[context.method]) {
988
+ if (allowedMethodsList.length > 0 && !allowedMethods[requestMethod]) {
933
989
  this._handleMethodNotAllowed(
934
990
  routerContext,
935
991
  allowedMethodsList,
@@ -1043,12 +1099,12 @@ var Router = class {
1043
1099
  redirect(source, destination, code) {
1044
1100
  let resolvedSource = source;
1045
1101
  let resolvedDestination = destination;
1046
- if (typeof source === "symbol" || source[0] !== "/") {
1102
+ if (typeof source === "symbol" || typeof source === "string" && source[0] !== "/") {
1047
1103
  const sourceUrl = this.url(source);
1048
1104
  if (sourceUrl instanceof Error) throw sourceUrl;
1049
1105
  resolvedSource = sourceUrl;
1050
1106
  }
1051
- if (typeof destination === "symbol" || destination[0] !== "/" && !destination.includes("://")) {
1107
+ if (typeof destination === "symbol" || typeof destination === "string" && destination[0] !== "/" && !destination.includes("://")) {
1052
1108
  const destinationUrl = this.url(destination);
1053
1109
  if (destinationUrl instanceof Error) throw destinationUrl;
1054
1110
  resolvedDestination = destinationUrl;
@@ -1165,7 +1221,7 @@ var Router = class {
1165
1221
  */
1166
1222
  url(name, ...arguments_) {
1167
1223
  const route = this.route(name);
1168
- if (route) return route.url.apply(route, arguments_);
1224
+ if (route) return route.url(...arguments_);
1169
1225
  return new Error(`No route found for name: ${String(name)}`);
1170
1226
  }
1171
1227
  /**
@@ -1182,12 +1238,13 @@ var Router = class {
1182
1238
  pathAndMethod: [],
1183
1239
  route: false
1184
1240
  };
1241
+ const normalizedMethod = method.toUpperCase();
1185
1242
  for (const layer of this.stack) {
1186
1243
  debug("test %s %s", layer.path, layer.regexp);
1187
1244
  if (layer.match(path)) {
1188
1245
  matchResult.path.push(layer);
1189
1246
  const isMiddleware = layer.methods.length === 0;
1190
- const matchesMethod = layer.methods.includes(method);
1247
+ const matchesMethod = layer.methods.includes(normalizedMethod);
1191
1248
  if (isMiddleware || matchesMethod) {
1192
1249
  matchResult.pathAndMethod.push(layer);
1193
1250
  if (layer.methods.length > 0) {
@@ -1313,7 +1370,10 @@ var Router = class {
1313
1370
  return this._registerMethod("delete", ...arguments_);
1314
1371
  }
1315
1372
  del(...arguments_) {
1316
- return this.delete.apply(this, arguments_);
1373
+ return this.delete.apply(
1374
+ this,
1375
+ arguments_
1376
+ );
1317
1377
  }
1318
1378
  head(...arguments_) {
1319
1379
  return this._registerMethod("head", ...arguments_);
@@ -1322,8 +1382,10 @@ var Router = class {
1322
1382
  return this._registerMethod("options", ...arguments_);
1323
1383
  }
1324
1384
  };
1385
+ var RouterExport = Router;
1386
+ var router_default = RouterExport;
1325
1387
  for (const httpMethod of httpMethods) {
1326
- const isAlreadyDefined = COMMON_HTTP_METHODS.includes(httpMethod) || Router.prototype[httpMethod];
1388
+ const isAlreadyDefined = COMMON_HTTP_METHODS.includes(httpMethod) || httpMethod in Router.prototype;
1327
1389
  if (!isAlreadyDefined) {
1328
1390
  Object.defineProperty(Router.prototype, httpMethod, {
1329
1391
  value: function(...arguments_) {
@@ -1336,6 +1398,6 @@ for (const httpMethod of httpMethods) {
1336
1398
  }
1337
1399
  }
1338
1400
  export {
1339
- Router,
1340
- Router as default
1401
+ RouterExport as Router,
1402
+ router_default as default
1341
1403
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@koa/router",
3
3
  "description": "Router middleware for koa. Maintained by Forward Email and Lad.",
4
- "version": "15.0.0",
4
+ "version": "15.1.1",
5
5
  "author": "Alex Mingoia <talk@alexmingoia.com>",
6
6
  "bugs": {
7
7
  "url": "https://github.com/koajs/router/issues",
@@ -20,6 +20,14 @@
20
20
  "email": "imed-jaberi@outlook.com"
21
21
  }
22
22
  ],
23
+ "peerDependencies": {
24
+ "koa": "^2.0.0 || ^3.0.0"
25
+ },
26
+ "peerDependenciesMeta": {
27
+ "koa": {
28
+ "optional": false
29
+ }
30
+ },
23
31
  "dependencies": {
24
32
  "debug": "^4.4.3",
25
33
  "http-errors": "^2.0.1",
@@ -27,25 +35,26 @@
27
35
  "path-to-regexp": "^8.3.0"
28
36
  },
29
37
  "devDependencies": {
30
- "@commitlint/cli": "^20.1.0",
31
- "@commitlint/config-conventional": "^20.0.0",
38
+ "@commitlint/cli": "^20.2.0",
39
+ "@commitlint/config-conventional": "^20.2.0",
40
+ "@koa/bodyparser": "^6.0.0",
32
41
  "@types/debug": "^4.1.12",
33
42
  "@types/jsonwebtoken": "^9.0.7",
34
43
  "@types/koa": "^3.0.1",
35
- "@types/node": "^24.10.1",
44
+ "@types/node": "^25.0.3",
36
45
  "@types/supertest": "^6.0.3",
37
- "@typescript-eslint/eslint-plugin": "^8.48.0",
38
- "@typescript-eslint/parser": "^8.48.0",
46
+ "@typescript-eslint/eslint-plugin": "^8.50.0",
47
+ "@typescript-eslint/parser": "^8.50.0",
39
48
  "c8": "^10.1.3",
40
49
  "chalk": "^5.4.1",
41
- "eslint": "^9.39.1",
50
+ "eslint": "^9.39.2",
42
51
  "eslint-plugin-unicorn": "^62.0.0",
43
52
  "husky": "^9.1.7",
44
53
  "joi": "^18.0.2",
45
- "jsonwebtoken": "^9.0.2",
54
+ "jsonwebtoken": "^9.0.3",
46
55
  "koa": "^3.1.1",
47
56
  "lint-staged": "^16.2.7",
48
- "prettier": "^3.7.1",
57
+ "prettier": "^3.7.4",
49
58
  "rimraf": "^6.1.2",
50
59
  "supertest": "^7.1.4",
51
60
  "ts-node": "^10.9.2",
@@ -60,8 +69,14 @@
60
69
  "types": "./dist/index.d.ts",
61
70
  "exports": {
62
71
  ".": {
63
- "require": "./dist/index.js",
64
- "import": "./dist/index.mjs"
72
+ "import": {
73
+ "types": "./dist/index.d.mts",
74
+ "import": "./dist/index.mjs"
75
+ },
76
+ "require": {
77
+ "types": "./dist/index.d.ts",
78
+ "require": "./dist/index.js"
79
+ }
65
80
  }
66
81
  },
67
82
  "files": [
@@ -97,6 +112,7 @@
97
112
  "test:all": "TS_NODE_PROJECT=tsconfig.ts-node.json node --require ts-node/register --test test/*.test.ts test/**/*.test.ts recipes/**/*.test.ts",
98
113
  "test:coverage": "c8 npm run test:all",
99
114
  "ts:check": "tsc --noEmit --project tsconfig.typecheck.json",
115
+ "ts:check:test": "tsc --noEmit --project tsconfig.test.json",
100
116
  "prebuild": "rimraf dist",
101
117
  "build": "tsup",
102
118
  "prepublishOnly": "npm run build"