@koa/router 15.0.0 → 15.1.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/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,9 +810,19 @@ 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) {
769
- route.setPrefix(normalizedPrefix);
816
+ if (previousPrefix && typeof route.path === "string") {
817
+ if (route.path.startsWith(previousPrefix)) {
818
+ route.path = route.path.slice(previousPrefix.length) || "/";
819
+ route.setPrefix(normalizedPrefix);
820
+ } else {
821
+ route.setPrefix(normalizedPrefix);
822
+ }
823
+ } else {
824
+ route.setPrefix(normalizedPrefix);
825
+ }
770
826
  }
771
827
  return this;
772
828
  }
@@ -794,7 +850,10 @@ var Router = class {
794
850
  matchedLayers,
795
851
  requestPath
796
852
  );
797
- return compose(middlewareChain)(context, next);
853
+ return compose(middlewareChain)(
854
+ context,
855
+ next
856
+ );
798
857
  }.bind(this);
799
858
  dispatchMiddleware.router = this;
800
859
  return dispatchMiddleware;
@@ -804,17 +863,19 @@ var Router = class {
804
863
  * @private
805
864
  */
806
865
  _getRequestPath(context) {
807
- return this.opts.routerPath || context.newRouterPath || context.path || context.routerPath || "";
866
+ const context_ = context;
867
+ return this.opts.routerPath || context_.newRouterPath || context_.path || context_.routerPath || "";
808
868
  }
809
869
  /**
810
870
  * Store matched routes on context
811
871
  * @private
812
872
  */
813
873
  _storeMatchedRoutes(context, matchResult) {
814
- if (context.matched) {
815
- context.matched.push(...matchResult.path);
874
+ const context_ = context;
875
+ if (context_.matched) {
876
+ context_.matched.push(...matchResult.path);
816
877
  } else {
817
- context.matched = matchResult.path;
878
+ context_.matched = matchResult.path;
818
879
  }
819
880
  }
820
881
  /**
@@ -822,11 +883,12 @@ var Router = class {
822
883
  * @private
823
884
  */
824
885
  _setMatchedRouteInfo(context, matchedLayers) {
886
+ const context_ = context;
825
887
  const routeLayer = matchedLayers.toReversed().find((layer) => layer.methods.length > 0);
826
888
  if (routeLayer) {
827
- context._matchedRoute = routeLayer.path;
889
+ context_._matchedRoute = routeLayer.path;
828
890
  if (routeLayer.name) {
829
- context._matchedRouteName = routeLayer.name;
891
+ context_._matchedRouteName = routeLayer.name;
830
892
  }
831
893
  }
832
894
  }
@@ -913,11 +975,11 @@ var Router = class {
913
975
  if (!this._shouldProcessAllowedMethods(routerContext)) {
914
976
  return;
915
977
  }
916
- const allowedMethods = this._collectAllowedMethods(
917
- routerContext.matched
918
- );
978
+ const matchedRoutes = routerContext.matched || [];
979
+ const allowedMethods = this._collectAllowedMethods(matchedRoutes);
919
980
  const allowedMethodsList = Object.keys(allowedMethods);
920
- if (!implementedMethods.includes(context.method)) {
981
+ const requestMethod = context.method.toUpperCase();
982
+ if (!implementedMethods.includes(requestMethod)) {
921
983
  this._handleNotImplemented(
922
984
  routerContext,
923
985
  allowedMethodsList,
@@ -925,11 +987,11 @@ var Router = class {
925
987
  );
926
988
  return;
927
989
  }
928
- if (context.method === "OPTIONS" && allowedMethodsList.length > 0) {
990
+ if (requestMethod === "OPTIONS" && allowedMethodsList.length > 0) {
929
991
  this._handleOptionsRequest(routerContext, allowedMethodsList);
930
992
  return;
931
993
  }
932
- if (allowedMethodsList.length > 0 && !allowedMethods[context.method]) {
994
+ if (allowedMethodsList.length > 0 && !allowedMethods[requestMethod]) {
933
995
  this._handleMethodNotAllowed(
934
996
  routerContext,
935
997
  allowedMethodsList,
@@ -1043,12 +1105,12 @@ var Router = class {
1043
1105
  redirect(source, destination, code) {
1044
1106
  let resolvedSource = source;
1045
1107
  let resolvedDestination = destination;
1046
- if (typeof source === "symbol" || source[0] !== "/") {
1108
+ if (typeof source === "symbol" || typeof source === "string" && source[0] !== "/") {
1047
1109
  const sourceUrl = this.url(source);
1048
1110
  if (sourceUrl instanceof Error) throw sourceUrl;
1049
1111
  resolvedSource = sourceUrl;
1050
1112
  }
1051
- if (typeof destination === "symbol" || destination[0] !== "/" && !destination.includes("://")) {
1113
+ if (typeof destination === "symbol" || typeof destination === "string" && destination[0] !== "/" && !destination.includes("://")) {
1052
1114
  const destinationUrl = this.url(destination);
1053
1115
  if (destinationUrl instanceof Error) throw destinationUrl;
1054
1116
  resolvedDestination = destinationUrl;
@@ -1165,7 +1227,7 @@ var Router = class {
1165
1227
  */
1166
1228
  url(name, ...arguments_) {
1167
1229
  const route = this.route(name);
1168
- if (route) return route.url.apply(route, arguments_);
1230
+ if (route) return route.url(...arguments_);
1169
1231
  return new Error(`No route found for name: ${String(name)}`);
1170
1232
  }
1171
1233
  /**
@@ -1182,12 +1244,13 @@ var Router = class {
1182
1244
  pathAndMethod: [],
1183
1245
  route: false
1184
1246
  };
1247
+ const normalizedMethod = method.toUpperCase();
1185
1248
  for (const layer of this.stack) {
1186
1249
  debug("test %s %s", layer.path, layer.regexp);
1187
1250
  if (layer.match(path)) {
1188
1251
  matchResult.path.push(layer);
1189
1252
  const isMiddleware = layer.methods.length === 0;
1190
- const matchesMethod = layer.methods.includes(method);
1253
+ const matchesMethod = layer.methods.includes(normalizedMethod);
1191
1254
  if (isMiddleware || matchesMethod) {
1192
1255
  matchResult.pathAndMethod.push(layer);
1193
1256
  if (layer.methods.length > 0) {
@@ -1313,7 +1376,10 @@ var Router = class {
1313
1376
  return this._registerMethod("delete", ...arguments_);
1314
1377
  }
1315
1378
  del(...arguments_) {
1316
- return this.delete.apply(this, arguments_);
1379
+ return this.delete.apply(
1380
+ this,
1381
+ arguments_
1382
+ );
1317
1383
  }
1318
1384
  head(...arguments_) {
1319
1385
  return this._registerMethod("head", ...arguments_);
@@ -1322,8 +1388,10 @@ var Router = class {
1322
1388
  return this._registerMethod("options", ...arguments_);
1323
1389
  }
1324
1390
  };
1391
+ var RouterExport = Router;
1392
+ var router_default = RouterExport;
1325
1393
  for (const httpMethod of httpMethods) {
1326
- const isAlreadyDefined = COMMON_HTTP_METHODS.includes(httpMethod) || Router.prototype[httpMethod];
1394
+ const isAlreadyDefined = COMMON_HTTP_METHODS.includes(httpMethod) || httpMethod in Router.prototype;
1327
1395
  if (!isAlreadyDefined) {
1328
1396
  Object.defineProperty(Router.prototype, httpMethod, {
1329
1397
  value: function(...arguments_) {
@@ -1336,6 +1404,6 @@ for (const httpMethod of httpMethods) {
1336
1404
  }
1337
1405
  }
1338
1406
  export {
1339
- Router,
1340
- Router as default
1407
+ RouterExport as Router,
1408
+ router_default as default
1341
1409
  };
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.0",
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.1",
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.49.0",
47
+ "@typescript-eslint/parser": "^8.49.0",
39
48
  "c8": "^10.1.3",
40
49
  "chalk": "^5.4.1",
41
50
  "eslint": "^9.39.1",
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"