@koa/router 14.0.0 → 15.0.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 ADDED
@@ -0,0 +1,1341 @@
1
+ // src/router.ts
2
+ import debugModule from "debug";
3
+ import compose from "koa-compose";
4
+ import HttpError from "http-errors";
5
+
6
+ // src/layer.ts
7
+ import { parse as parseUrl, format as formatUrl } from "url";
8
+
9
+ // src/utils/path-to-regexp-wrapper.ts
10
+ import { pathToRegexp, compile, parse } from "path-to-regexp";
11
+ function compilePathToRegexp(path, options = {}) {
12
+ const normalizedOptions = { ...options };
13
+ if ("strict" in normalizedOptions && !("trailing" in normalizedOptions)) {
14
+ normalizedOptions.trailing = normalizedOptions.strict !== true;
15
+ delete normalizedOptions.strict;
16
+ }
17
+ delete normalizedOptions.pathAsRegExp;
18
+ delete normalizedOptions.ignoreCaptures;
19
+ delete normalizedOptions.prefix;
20
+ const { regexp, keys } = pathToRegexp(path, normalizedOptions);
21
+ return { regexp, keys };
22
+ }
23
+ function compilePath(path, options = {}) {
24
+ return compile(path, options);
25
+ }
26
+ function parsePath(path, options) {
27
+ return parse(path, options);
28
+ }
29
+ function normalizeLayerOptionsToPathToRegexp(options = {}) {
30
+ const normalized = {
31
+ sensitive: options.sensitive,
32
+ end: options.end,
33
+ strict: options.strict,
34
+ trailing: options.trailing
35
+ };
36
+ if ("strict" in normalized && !("trailing" in normalized)) {
37
+ normalized.trailing = normalized.strict !== true;
38
+ delete normalized.strict;
39
+ }
40
+ for (const key of Object.keys(normalized)) {
41
+ if (normalized[key] === void 0) {
42
+ delete normalized[key];
43
+ }
44
+ }
45
+ return normalized;
46
+ }
47
+
48
+ // src/layer.ts
49
+ function safeDecodeURIComponent(text) {
50
+ try {
51
+ return decodeURIComponent(text);
52
+ } catch {
53
+ return text;
54
+ }
55
+ }
56
+ var Layer = class {
57
+ opts;
58
+ name;
59
+ methods;
60
+ paramNames;
61
+ stack;
62
+ path;
63
+ regexp;
64
+ /**
65
+ * Initialize a new routing Layer with given `method`, `path`, and `middleware`.
66
+ *
67
+ * @param path - Path string or regular expression
68
+ * @param methods - Array of HTTP verbs
69
+ * @param middleware - Layer callback/middleware or series of
70
+ * @param opts - Layer options
71
+ * @private
72
+ */
73
+ constructor(path, methods, middleware, options = {}) {
74
+ this.opts = options;
75
+ this.name = this.opts.name || void 0;
76
+ this.methods = this._normalizeHttpMethods(methods);
77
+ this.stack = this._normalizeAndValidateMiddleware(
78
+ middleware,
79
+ methods,
80
+ path
81
+ );
82
+ this.path = path;
83
+ this.paramNames = [];
84
+ this._configurePathMatching();
85
+ }
86
+ /**
87
+ * Normalize HTTP methods and add automatic HEAD support for GET
88
+ * @private
89
+ */
90
+ _normalizeHttpMethods(methods) {
91
+ const normalizedMethods = [];
92
+ for (const method of methods) {
93
+ const upperMethod = method.toUpperCase();
94
+ normalizedMethods.push(upperMethod);
95
+ if (upperMethod === "GET") {
96
+ normalizedMethods.unshift("HEAD");
97
+ }
98
+ }
99
+ return normalizedMethods;
100
+ }
101
+ /**
102
+ * Normalize middleware to array and validate all are functions
103
+ * @private
104
+ */
105
+ _normalizeAndValidateMiddleware(middleware, methods, path) {
106
+ const middlewareArray = Array.isArray(middleware) ? middleware : [middleware];
107
+ for (const middlewareFunction of middlewareArray) {
108
+ const middlewareType = typeof middlewareFunction;
109
+ if (middlewareType !== "function") {
110
+ const routeIdentifier = this.opts.name || path;
111
+ throw new Error(
112
+ `${methods.toString()} \`${routeIdentifier}\`: \`middleware\` must be a function, not \`${middlewareType}\``
113
+ );
114
+ }
115
+ }
116
+ return middlewareArray;
117
+ }
118
+ /**
119
+ * Configure path matching regexp and parameters
120
+ * @private
121
+ */
122
+ _configurePathMatching() {
123
+ if (this.opts.pathAsRegExp === true) {
124
+ this.regexp = this.path instanceof RegExp ? this.path : new RegExp(this.path);
125
+ } else if (this.path) {
126
+ this._configurePathToRegexp();
127
+ }
128
+ }
129
+ /**
130
+ * Configure path-to-regexp for string paths
131
+ * @private
132
+ */
133
+ _configurePathToRegexp() {
134
+ const options = normalizeLayerOptionsToPathToRegexp(this.opts);
135
+ const { regexp, keys } = compilePathToRegexp(this.path, options);
136
+ this.regexp = regexp;
137
+ this.paramNames = keys;
138
+ }
139
+ /**
140
+ * Returns whether request `path` matches route.
141
+ *
142
+ * @param path - Request path
143
+ * @returns Whether path matches
144
+ * @private
145
+ */
146
+ match(path) {
147
+ return this.regexp.test(path);
148
+ }
149
+ /**
150
+ * Returns map of URL parameters for given `path` and `paramNames`.
151
+ *
152
+ * @param _path - Request path (not used, kept for API compatibility)
153
+ * @param captures - Captured values from regexp
154
+ * @param existingParams - Existing params to merge with
155
+ * @returns Parameter map
156
+ * @private
157
+ */
158
+ params(_path, captures, existingParameters = {}) {
159
+ const parameterValues = { ...existingParameters };
160
+ for (const [captureIndex, capturedValue] of captures.entries()) {
161
+ const parameterDefinition = this.paramNames[captureIndex];
162
+ if (parameterDefinition && capturedValue && capturedValue.length > 0) {
163
+ const parameterName = parameterDefinition.name;
164
+ parameterValues[parameterName] = safeDecodeURIComponent(capturedValue);
165
+ }
166
+ }
167
+ return parameterValues;
168
+ }
169
+ /**
170
+ * Returns array of regexp url path captures.
171
+ *
172
+ * @param path - Request path
173
+ * @returns Array of captured values
174
+ * @private
175
+ */
176
+ captures(path) {
177
+ if (this.opts.ignoreCaptures) {
178
+ return [];
179
+ }
180
+ const match = path.match(this.regexp);
181
+ return match ? match.slice(1) : [];
182
+ }
183
+ /**
184
+ * Generate URL for route using given `params`.
185
+ *
186
+ * @example
187
+ *
188
+ * ```javascript
189
+ * const route = new Layer('/users/:id', ['GET'], fn);
190
+ *
191
+ * route.url({ id: 123 }); // => "/users/123"
192
+ * ```
193
+ *
194
+ * @param args - URL parameters (various formats supported)
195
+ * @returns Generated URL
196
+ * @private
197
+ */
198
+ url(...arguments_) {
199
+ const { params, options } = this._parseUrlArguments(arguments_);
200
+ const cleanPath = this.path.replaceAll("(.*)", "");
201
+ const pathCompiler = compilePath(cleanPath, {
202
+ encode: encodeURIComponent,
203
+ ...options
204
+ });
205
+ const parameterReplacements = this._buildParamReplacements(
206
+ params,
207
+ cleanPath
208
+ );
209
+ const generatedUrl = pathCompiler(parameterReplacements);
210
+ if (options && options.query) {
211
+ return this._addQueryString(generatedUrl, options.query);
212
+ }
213
+ return generatedUrl;
214
+ }
215
+ /**
216
+ * Parse url() arguments into params and options
217
+ * Supports multiple call signatures:
218
+ * - url({ id: 1 })
219
+ * - url(1, 2, 3)
220
+ * - url({ query: {...} })
221
+ * - url({ id: 1 }, { query: {...} })
222
+ * @private
223
+ */
224
+ _parseUrlArguments(allArguments) {
225
+ let parameters = allArguments[0];
226
+ let options = allArguments[1];
227
+ if (typeof parameters !== "object") {
228
+ const argumentsList = [...allArguments];
229
+ const lastArgument = argumentsList.at(-1);
230
+ if (typeof lastArgument === "object") {
231
+ options = lastArgument;
232
+ parameters = argumentsList.slice(0, -1);
233
+ } else {
234
+ parameters = argumentsList;
235
+ }
236
+ } else if (parameters && parameters.query && !options) {
237
+ options = parameters;
238
+ parameters = {};
239
+ }
240
+ return { params: parameters, options };
241
+ }
242
+ /**
243
+ * Build parameter replacements for URL generation
244
+ * @private
245
+ */
246
+ _buildParamReplacements(parameters, cleanPath) {
247
+ const { tokens } = parsePath(cleanPath);
248
+ const hasNamedParameters = tokens.some(
249
+ (token) => "name" in token && token.name
250
+ );
251
+ const parameterReplacements = {};
252
+ if (Array.isArray(parameters)) {
253
+ let parameterIndex = 0;
254
+ for (const token of tokens) {
255
+ if ("name" in token && token.name) {
256
+ parameterReplacements[token.name] = String(
257
+ parameters[parameterIndex++]
258
+ );
259
+ }
260
+ }
261
+ } else if (hasNamedParameters && typeof parameters === "object" && !parameters.query) {
262
+ for (const [parameterName, parameterValue] of Object.entries(
263
+ parameters
264
+ )) {
265
+ parameterReplacements[parameterName] = String(parameterValue);
266
+ }
267
+ }
268
+ return parameterReplacements;
269
+ }
270
+ /**
271
+ * Add query string to URL
272
+ * @private
273
+ */
274
+ _addQueryString(baseUrl, query) {
275
+ const parsedUrl = parseUrl(baseUrl);
276
+ if (typeof query === "string") {
277
+ parsedUrl.search = query;
278
+ } else {
279
+ parsedUrl.search = void 0;
280
+ parsedUrl.query = query;
281
+ }
282
+ return formatUrl(parsedUrl);
283
+ }
284
+ /**
285
+ * Run validations on route named parameters.
286
+ *
287
+ * @example
288
+ *
289
+ * ```javascript
290
+ * router
291
+ * .param('user', function (id, ctx, next) {
292
+ * ctx.user = users[id];
293
+ * if (!ctx.user) return ctx.status = 404;
294
+ * next();
295
+ * })
296
+ * .get('/users/:user', function (ctx, next) {
297
+ * ctx.body = ctx.user;
298
+ * });
299
+ * ```
300
+ *
301
+ * @param paramName - Parameter name
302
+ * @param paramHandler - Middleware function
303
+ * @returns This layer instance
304
+ * @private
305
+ */
306
+ param(parameterName, parameterHandler) {
307
+ const middlewareStack = this.stack;
308
+ const routeParameterNames = this.paramNames;
309
+ const parameterMiddleware = this._createParamMiddleware(
310
+ parameterName,
311
+ parameterHandler
312
+ );
313
+ const parameterNamesList = routeParameterNames.map(
314
+ (parameterDefinition) => parameterDefinition.name
315
+ );
316
+ const parameterPosition = parameterNamesList.indexOf(parameterName);
317
+ if (parameterPosition !== -1) {
318
+ this._insertParamMiddleware(
319
+ middlewareStack,
320
+ parameterMiddleware,
321
+ parameterNamesList,
322
+ parameterPosition
323
+ );
324
+ }
325
+ return this;
326
+ }
327
+ /**
328
+ * Create param middleware with deduplication tracking
329
+ * @private
330
+ */
331
+ _createParamMiddleware(parameterName, parameterHandler) {
332
+ const middleware = function(context, next) {
333
+ if (!context._matchedParams) {
334
+ context._matchedParams = /* @__PURE__ */ new WeakMap();
335
+ }
336
+ if (context._matchedParams.has(parameterHandler)) {
337
+ return next();
338
+ }
339
+ context._matchedParams.set(parameterHandler, true);
340
+ return parameterHandler.call(
341
+ this,
342
+ context.params[parameterName],
343
+ context,
344
+ next
345
+ );
346
+ };
347
+ middleware.param = parameterName;
348
+ middleware._originalFn = parameterHandler;
349
+ return middleware;
350
+ }
351
+ /**
352
+ * Insert param middleware at the correct position in the stack
353
+ * @private
354
+ */
355
+ _insertParamMiddleware(middlewareStack, parameterMiddleware, parameterNamesList, currentParameterPosition) {
356
+ middlewareStack.some((existingMiddleware, stackIndex) => {
357
+ if (!existingMiddleware.param) {
358
+ middlewareStack.splice(stackIndex, 0, parameterMiddleware);
359
+ return true;
360
+ }
361
+ const existingParameterPosition = parameterNamesList.indexOf(
362
+ existingMiddleware.param
363
+ );
364
+ if (existingParameterPosition > currentParameterPosition) {
365
+ middlewareStack.splice(stackIndex, 0, parameterMiddleware);
366
+ return true;
367
+ }
368
+ return false;
369
+ });
370
+ }
371
+ /**
372
+ * Prefix route path.
373
+ *
374
+ * @param prefixPath - Prefix to prepend
375
+ * @returns This layer instance
376
+ * @private
377
+ */
378
+ setPrefix(prefixPath) {
379
+ if (!this.path) {
380
+ return this;
381
+ }
382
+ if (this.path instanceof RegExp) {
383
+ return this;
384
+ }
385
+ this.path = this._applyPrefix(prefixPath);
386
+ this._reconfigurePathMatching(prefixPath);
387
+ return this;
388
+ }
389
+ /**
390
+ * Apply prefix to the current path
391
+ * @private
392
+ */
393
+ _applyPrefix(prefixPath) {
394
+ const isRootPath = this.path === "/";
395
+ const isStrictMode = this.opts.strict === true;
396
+ const prefixHasParameters = prefixPath.includes(":");
397
+ const pathIsRawRegex = this.opts.pathAsRegExp === true && typeof this.path === "string";
398
+ if (prefixHasParameters && pathIsRawRegex) {
399
+ const currentPath = this.path;
400
+ if (currentPath === String.raw`(?:\/|$)` || currentPath === String.raw`(?:\/|$)`) {
401
+ this.path = "{/*rest}";
402
+ this.opts.pathAsRegExp = false;
403
+ }
404
+ }
405
+ if (isRootPath && !isStrictMode) {
406
+ return prefixPath;
407
+ }
408
+ return `${prefixPath}${this.path}`;
409
+ }
410
+ /**
411
+ * Reconfigure path matching after prefix is applied
412
+ * @private
413
+ */
414
+ _reconfigurePathMatching(prefixPath) {
415
+ const treatAsRegExp = this.opts.pathAsRegExp === true;
416
+ const prefixHasParameters = prefixPath && prefixPath.includes(":");
417
+ if (prefixHasParameters && treatAsRegExp) {
418
+ const options = normalizeLayerOptionsToPathToRegexp(this.opts);
419
+ const { regexp, keys } = compilePathToRegexp(
420
+ this.path,
421
+ options
422
+ );
423
+ this.regexp = regexp;
424
+ this.paramNames = keys;
425
+ this.opts.pathAsRegExp = false;
426
+ } else if (treatAsRegExp) {
427
+ this.regexp = this.path instanceof RegExp ? this.path : new RegExp(this.path);
428
+ } else {
429
+ const options = normalizeLayerOptionsToPathToRegexp(this.opts);
430
+ const { regexp, keys } = compilePathToRegexp(
431
+ this.path,
432
+ options
433
+ );
434
+ this.regexp = regexp;
435
+ this.paramNames = keys;
436
+ }
437
+ }
438
+ };
439
+
440
+ // src/utils/http-methods.ts
441
+ import http from "http";
442
+ function getAllHttpMethods() {
443
+ return http.METHODS.map((method) => method.toLowerCase());
444
+ }
445
+ var COMMON_HTTP_METHODS = [
446
+ "get",
447
+ "post",
448
+ "put",
449
+ "patch",
450
+ "delete",
451
+ "del",
452
+ "head",
453
+ "options"
454
+ ];
455
+
456
+ // src/utils/parameter-helpers.ts
457
+ function normalizeParameterMiddleware(parameterMiddleware) {
458
+ if (!parameterMiddleware) {
459
+ return [];
460
+ }
461
+ if (Array.isArray(parameterMiddleware)) {
462
+ return parameterMiddleware;
463
+ }
464
+ return [parameterMiddleware];
465
+ }
466
+ function applyParameterMiddlewareToRoute(route, parameterName, parameterMiddleware) {
467
+ const middlewareList = normalizeParameterMiddleware(parameterMiddleware);
468
+ for (const middleware of middlewareList) {
469
+ route.param(parameterName, middleware);
470
+ }
471
+ }
472
+ function applyAllParameterMiddleware(route, parametersObject) {
473
+ const parameterNames = Object.keys(parametersObject);
474
+ for (const parameterName of parameterNames) {
475
+ const parameterMiddleware = parametersObject[parameterName];
476
+ applyParameterMiddlewareToRoute(route, parameterName, parameterMiddleware);
477
+ }
478
+ }
479
+
480
+ // src/utils/path-helpers.ts
481
+ function hasPathParameters(path, options = {}) {
482
+ if (!path) {
483
+ return false;
484
+ }
485
+ const { keys } = compilePathToRegexp(path, options);
486
+ return keys.length > 0;
487
+ }
488
+ function determineMiddlewarePath(explicitPath, hasPrefixParameters) {
489
+ if (explicitPath !== void 0) {
490
+ if (typeof explicitPath === "string") {
491
+ if (explicitPath === "") {
492
+ return {
493
+ path: "{/*rest}",
494
+ pathAsRegExp: false
495
+ };
496
+ }
497
+ if (explicitPath === "/") {
498
+ return {
499
+ path: "/",
500
+ pathAsRegExp: false
501
+ };
502
+ }
503
+ return {
504
+ path: explicitPath,
505
+ pathAsRegExp: false
506
+ };
507
+ }
508
+ return {
509
+ path: explicitPath,
510
+ pathAsRegExp: true
511
+ };
512
+ }
513
+ if (hasPrefixParameters) {
514
+ return {
515
+ path: "{/*rest}",
516
+ pathAsRegExp: false
517
+ };
518
+ }
519
+ return {
520
+ path: String.raw`(?:\/|$)`,
521
+ pathAsRegExp: true
522
+ };
523
+ }
524
+
525
+ // src/router.ts
526
+ var debug = debugModule("koa-router");
527
+ var httpMethods = getAllHttpMethods();
528
+ var Router = class {
529
+ opts;
530
+ methods;
531
+ exclusive;
532
+ params;
533
+ stack;
534
+ host;
535
+ /**
536
+ * Create a new router.
537
+ *
538
+ * @example
539
+ *
540
+ * Basic usage:
541
+ *
542
+ * ```javascript
543
+ * const Koa = require('koa');
544
+ * const Router = require('@koa/router');
545
+ *
546
+ * const app = new Koa();
547
+ * const router = new Router();
548
+ *
549
+ * router.get('/', (ctx, next) => {
550
+ * // ctx.router available
551
+ * });
552
+ *
553
+ * app
554
+ * .use(router.routes())
555
+ * .use(router.allowedMethods());
556
+ * ```
557
+ *
558
+ * @alias module:koa-router
559
+ * @param opts - Router options
560
+ * @constructor
561
+ */
562
+ constructor(options = {}) {
563
+ this.opts = options;
564
+ this.methods = this.opts.methods || [
565
+ "HEAD",
566
+ "OPTIONS",
567
+ "GET",
568
+ "PUT",
569
+ "PATCH",
570
+ "POST",
571
+ "DELETE"
572
+ ];
573
+ this.exclusive = Boolean(this.opts.exclusive);
574
+ this.params = {};
575
+ this.stack = [];
576
+ this.host = this.opts.host;
577
+ }
578
+ /**
579
+ * Generate URL from url pattern and given `params`.
580
+ *
581
+ * @example
582
+ *
583
+ * ```javascript
584
+ * const url = Router.url('/users/:id', {id: 1});
585
+ * // => "/users/1"
586
+ * ```
587
+ *
588
+ * @param path - URL pattern
589
+ * @param args - URL parameters
590
+ * @returns Generated URL
591
+ */
592
+ static url(path, ...arguments_) {
593
+ const temporaryLayer = new Layer(path, [], () => {
594
+ });
595
+ return temporaryLayer.url(...arguments_);
596
+ }
597
+ use(...middleware) {
598
+ let explicitPath;
599
+ if (this._isPathArray(middleware[0])) {
600
+ return this._useWithPathArray(middleware);
601
+ }
602
+ const hasExplicitPath = this._hasExplicitPath(middleware[0]);
603
+ if (hasExplicitPath) {
604
+ explicitPath = middleware.shift();
605
+ }
606
+ for (const currentMiddleware of middleware) {
607
+ if (this._isNestedRouter(currentMiddleware)) {
608
+ this._mountNestedRouter(
609
+ currentMiddleware,
610
+ explicitPath
611
+ );
612
+ } else {
613
+ this._registerMiddleware(
614
+ currentMiddleware,
615
+ explicitPath,
616
+ hasExplicitPath
617
+ );
618
+ }
619
+ }
620
+ return this;
621
+ }
622
+ /**
623
+ * Check if first argument is an array of paths
624
+ * @private
625
+ */
626
+ _isPathArray(firstArgument) {
627
+ return Array.isArray(firstArgument) && typeof firstArgument[0] === "string";
628
+ }
629
+ /**
630
+ * Check if first argument is an explicit path (string or RegExp)
631
+ * Empty string counts as explicit path to enable param capture
632
+ * @private
633
+ */
634
+ _hasExplicitPath(firstArgument) {
635
+ return typeof firstArgument === "string" || firstArgument instanceof RegExp;
636
+ }
637
+ /**
638
+ * Check if middleware contains a nested router
639
+ * @private
640
+ */
641
+ _isNestedRouter(middleware) {
642
+ return middleware.router !== void 0;
643
+ }
644
+ /**
645
+ * Apply middleware to multiple paths
646
+ * @private
647
+ */
648
+ _useWithPathArray(middleware) {
649
+ const pathArray = middleware[0];
650
+ const remainingMiddleware = middleware.slice(1);
651
+ for (const singlePath of pathArray) {
652
+ Reflect.apply(this.use, this, [singlePath, ...remainingMiddleware]);
653
+ }
654
+ return this;
655
+ }
656
+ /**
657
+ * Mount a nested router
658
+ * @private
659
+ */
660
+ _mountNestedRouter(middlewareWithRouter, mountPath) {
661
+ const nestedRouter = middlewareWithRouter.router;
662
+ const clonedRouter = this._cloneRouter(nestedRouter);
663
+ const mountPathHasParameters = mountPath && typeof mountPath === "string" && hasPathParameters(mountPath, this.opts);
664
+ for (let routeIndex = 0; routeIndex < clonedRouter.stack.length; routeIndex++) {
665
+ const nestedLayer = clonedRouter.stack[routeIndex];
666
+ const clonedLayer = this._cloneLayer(nestedLayer);
667
+ if (mountPath && typeof mountPath === "string") {
668
+ clonedLayer.setPrefix(mountPath);
669
+ }
670
+ if (this.opts.prefix) {
671
+ clonedLayer.setPrefix(this.opts.prefix);
672
+ }
673
+ if (clonedLayer.methods.length === 0 && mountPathHasParameters) {
674
+ clonedLayer.opts.ignoreCaptures = false;
675
+ }
676
+ this.stack.push(clonedLayer);
677
+ clonedRouter.stack[routeIndex] = clonedLayer;
678
+ }
679
+ if (this.params) {
680
+ this._applyParamMiddlewareToRouter(clonedRouter);
681
+ }
682
+ }
683
+ /**
684
+ * Clone a router instance
685
+ * @private
686
+ */
687
+ _cloneRouter(sourceRouter) {
688
+ return Object.assign(
689
+ Object.create(Object.getPrototypeOf(sourceRouter)),
690
+ sourceRouter,
691
+ {
692
+ stack: [...sourceRouter.stack]
693
+ }
694
+ );
695
+ }
696
+ /**
697
+ * Clone a layer instance
698
+ * @private
699
+ */
700
+ _cloneLayer(sourceLayer) {
701
+ return Object.assign(
702
+ Object.create(Object.getPrototypeOf(sourceLayer)),
703
+ sourceLayer
704
+ );
705
+ }
706
+ /**
707
+ * Apply this router's param middleware to a nested router
708
+ * @private
709
+ */
710
+ _applyParamMiddlewareToRouter(targetRouter) {
711
+ const parameterNames = Object.keys(this.params);
712
+ for (const parameterName of parameterNames) {
713
+ const parameterMiddleware = this.params[parameterName];
714
+ applyParameterMiddlewareToRoute(
715
+ targetRouter,
716
+ parameterName,
717
+ parameterMiddleware
718
+ );
719
+ }
720
+ }
721
+ /**
722
+ * Register regular middleware (not nested router)
723
+ * @private
724
+ */
725
+ _registerMiddleware(middleware, explicitPath, hasExplicitPath) {
726
+ const prefixHasParameters = hasPathParameters(
727
+ this.opts.prefix || "",
728
+ this.opts
729
+ );
730
+ const effectiveExplicitPath = (() => {
731
+ if (explicitPath !== void 0) return explicitPath;
732
+ if (prefixHasParameters) return "";
733
+ return;
734
+ })();
735
+ const effectiveHasExplicitPath = hasExplicitPath || explicitPath === void 0 && prefixHasParameters;
736
+ const { path: middlewarePath, pathAsRegExp } = determineMiddlewarePath(
737
+ effectiveExplicitPath,
738
+ prefixHasParameters
739
+ );
740
+ let finalPath = middlewarePath;
741
+ let usePathToRegexp = pathAsRegExp;
742
+ const isRootPath = effectiveHasExplicitPath && middlewarePath === "/";
743
+ if (effectiveHasExplicitPath && typeof middlewarePath === "string") {
744
+ finalPath = middlewarePath;
745
+ usePathToRegexp = false;
746
+ }
747
+ this.register(finalPath, [], middleware, {
748
+ end: isRootPath,
749
+ ignoreCaptures: !effectiveHasExplicitPath && !prefixHasParameters,
750
+ pathAsRegExp: usePathToRegexp
751
+ });
752
+ }
753
+ /**
754
+ * Set the path prefix for a Router instance that was already initialized.
755
+ *
756
+ * @example
757
+ *
758
+ * ```javascript
759
+ * router.prefix('/things/:thing_id')
760
+ * ```
761
+ *
762
+ * @param prefixPath - Prefix string
763
+ * @returns This router instance
764
+ */
765
+ prefix(prefixPath) {
766
+ const normalizedPrefix = prefixPath.replace(/\/$/, "");
767
+ this.opts.prefix = normalizedPrefix;
768
+ for (const route of this.stack) {
769
+ route.setPrefix(normalizedPrefix);
770
+ }
771
+ return this;
772
+ }
773
+ /**
774
+ * Returns router middleware which dispatches a route matching the request.
775
+ *
776
+ * @returns Router middleware
777
+ */
778
+ middleware() {
779
+ const dispatchMiddleware = function(context, next) {
780
+ debug("%s %s", context.method, context.path);
781
+ if (!this.matchHost(context.host)) {
782
+ return next();
783
+ }
784
+ const requestPath = this._getRequestPath(context);
785
+ const matchResult = this.match(requestPath, context.method);
786
+ this._storeMatchedRoutes(context, matchResult);
787
+ context.router = this;
788
+ if (!matchResult.route) {
789
+ return next();
790
+ }
791
+ const matchedLayers = matchResult.pathAndMethod;
792
+ this._setMatchedRouteInfo(context, matchedLayers);
793
+ const middlewareChain = this._buildMiddlewareChain(
794
+ matchedLayers,
795
+ requestPath
796
+ );
797
+ return compose(middlewareChain)(context, next);
798
+ }.bind(this);
799
+ dispatchMiddleware.router = this;
800
+ return dispatchMiddleware;
801
+ }
802
+ /**
803
+ * Get the request path to use for routing
804
+ * @private
805
+ */
806
+ _getRequestPath(context) {
807
+ return this.opts.routerPath || context.newRouterPath || context.path || context.routerPath || "";
808
+ }
809
+ /**
810
+ * Store matched routes on context
811
+ * @private
812
+ */
813
+ _storeMatchedRoutes(context, matchResult) {
814
+ if (context.matched) {
815
+ context.matched.push(...matchResult.path);
816
+ } else {
817
+ context.matched = matchResult.path;
818
+ }
819
+ }
820
+ /**
821
+ * Set matched route information on context
822
+ * @private
823
+ */
824
+ _setMatchedRouteInfo(context, matchedLayers) {
825
+ const routeLayer = matchedLayers.toReversed().find((layer) => layer.methods.length > 0);
826
+ if (routeLayer) {
827
+ context._matchedRoute = routeLayer.path;
828
+ if (routeLayer.name) {
829
+ context._matchedRouteName = routeLayer.name;
830
+ }
831
+ }
832
+ }
833
+ /**
834
+ * Build middleware chain from matched layers
835
+ * @private
836
+ */
837
+ _buildMiddlewareChain(matchedLayers, requestPath) {
838
+ const layersToExecute = this.opts.exclusive ? [matchedLayers.at(-1)].filter(
839
+ (layer) => layer !== void 0
840
+ ) : matchedLayers;
841
+ const middlewareChain = [];
842
+ for (const layer of layersToExecute) {
843
+ middlewareChain.push(
844
+ (context, next) => {
845
+ const routerContext = context;
846
+ routerContext.captures = layer.captures(requestPath);
847
+ routerContext.request.params = layer.params(
848
+ requestPath,
849
+ routerContext.captures || [],
850
+ routerContext.params
851
+ );
852
+ routerContext.params = routerContext.request.params;
853
+ routerContext.routerPath = layer.path;
854
+ routerContext.routerName = layer.name || void 0;
855
+ routerContext._matchedRoute = layer.path;
856
+ if (layer.name) {
857
+ routerContext._matchedRouteName = layer.name;
858
+ }
859
+ return next();
860
+ },
861
+ ...layer.stack
862
+ );
863
+ }
864
+ return middlewareChain;
865
+ }
866
+ routes() {
867
+ return this.middleware();
868
+ }
869
+ /**
870
+ * Returns separate middleware for responding to `OPTIONS` requests with
871
+ * an `Allow` header containing the allowed methods, as well as responding
872
+ * with `405 Method Not Allowed` and `501 Not Implemented` as appropriate.
873
+ *
874
+ * @example
875
+ *
876
+ * ```javascript
877
+ * const Koa = require('koa');
878
+ * const Router = require('@koa/router');
879
+ *
880
+ * const app = new Koa();
881
+ * const router = new Router();
882
+ *
883
+ * app.use(router.routes());
884
+ * app.use(router.allowedMethods());
885
+ * ```
886
+ *
887
+ * **Example with [Boom](https://github.com/hapijs/boom)**
888
+ *
889
+ * ```javascript
890
+ * const Koa = require('koa');
891
+ * const Router = require('@koa/router');
892
+ * const Boom = require('boom');
893
+ *
894
+ * const app = new Koa();
895
+ * const router = new Router();
896
+ *
897
+ * app.use(router.routes());
898
+ * app.use(router.allowedMethods({
899
+ * throw: true,
900
+ * notImplemented: () => new Boom.notImplemented(),
901
+ * methodNotAllowed: () => new Boom.methodNotAllowed()
902
+ * }));
903
+ * ```
904
+ *
905
+ * @param options - Options object
906
+ * @returns Middleware function
907
+ */
908
+ allowedMethods(options = {}) {
909
+ const implementedMethods = this.methods;
910
+ return (context, next) => {
911
+ const routerContext = context;
912
+ return next().then(() => {
913
+ if (!this._shouldProcessAllowedMethods(routerContext)) {
914
+ return;
915
+ }
916
+ const allowedMethods = this._collectAllowedMethods(
917
+ routerContext.matched
918
+ );
919
+ const allowedMethodsList = Object.keys(allowedMethods);
920
+ if (!implementedMethods.includes(context.method)) {
921
+ this._handleNotImplemented(
922
+ routerContext,
923
+ allowedMethodsList,
924
+ options
925
+ );
926
+ return;
927
+ }
928
+ if (context.method === "OPTIONS" && allowedMethodsList.length > 0) {
929
+ this._handleOptionsRequest(routerContext, allowedMethodsList);
930
+ return;
931
+ }
932
+ if (allowedMethodsList.length > 0 && !allowedMethods[context.method]) {
933
+ this._handleMethodNotAllowed(
934
+ routerContext,
935
+ allowedMethodsList,
936
+ options
937
+ );
938
+ }
939
+ });
940
+ };
941
+ }
942
+ /**
943
+ * Check if we should process allowed methods
944
+ * @private
945
+ */
946
+ _shouldProcessAllowedMethods(context) {
947
+ return !!(context.matched && (!context.status || context.status === 404));
948
+ }
949
+ /**
950
+ * Collect all allowed methods from matched routes
951
+ * @private
952
+ */
953
+ _collectAllowedMethods(matchedRoutes) {
954
+ const allowedMethods = {};
955
+ for (const route of matchedRoutes) {
956
+ for (const method of route.methods) {
957
+ allowedMethods[method] = method;
958
+ }
959
+ }
960
+ return allowedMethods;
961
+ }
962
+ /**
963
+ * Handle 501 Not Implemented response
964
+ * @private
965
+ */
966
+ _handleNotImplemented(context, allowedMethodsList, options) {
967
+ if (options.throw) {
968
+ const error = typeof options.notImplemented === "function" ? options.notImplemented() : new HttpError.NotImplemented();
969
+ throw error;
970
+ }
971
+ context.status = 501;
972
+ context.set("Allow", allowedMethodsList.join(", "));
973
+ }
974
+ /**
975
+ * Handle OPTIONS request
976
+ * @private
977
+ */
978
+ _handleOptionsRequest(context, allowedMethodsList) {
979
+ context.status = 200;
980
+ context.body = "";
981
+ context.set("Allow", allowedMethodsList.join(", "));
982
+ }
983
+ /**
984
+ * Handle 405 Method Not Allowed response
985
+ * @private
986
+ */
987
+ _handleMethodNotAllowed(context, allowedMethodsList, options) {
988
+ if (options.throw) {
989
+ const error = typeof options.methodNotAllowed === "function" ? options.methodNotAllowed() : new HttpError.MethodNotAllowed();
990
+ throw error;
991
+ }
992
+ context.status = 405;
993
+ context.set("Allow", allowedMethodsList.join(", "));
994
+ }
995
+ all(...arguments_) {
996
+ let name;
997
+ let path;
998
+ let middleware;
999
+ if (arguments_.length >= 2 && (typeof arguments_[1] === "string" || arguments_[1] instanceof RegExp)) {
1000
+ name = arguments_[0];
1001
+ path = arguments_[1];
1002
+ middleware = arguments_.slice(2);
1003
+ } else {
1004
+ name = void 0;
1005
+ path = arguments_[0];
1006
+ middleware = arguments_.slice(1);
1007
+ }
1008
+ if (typeof path !== "string" && !(path instanceof RegExp) && (!Array.isArray(path) || path.length === 0))
1009
+ throw new Error("You have to provide a path when adding an all handler");
1010
+ const routeOptions = {
1011
+ name,
1012
+ pathAsRegExp: path instanceof RegExp
1013
+ };
1014
+ this.register(path, httpMethods, middleware, {
1015
+ ...this.opts,
1016
+ ...routeOptions
1017
+ });
1018
+ return this;
1019
+ }
1020
+ /**
1021
+ * Redirect `source` to `destination` URL with optional 30x status `code`.
1022
+ *
1023
+ * Both `source` and `destination` can be route names.
1024
+ *
1025
+ * ```javascript
1026
+ * router.redirect('/login', 'sign-in');
1027
+ * ```
1028
+ *
1029
+ * This is equivalent to:
1030
+ *
1031
+ * ```javascript
1032
+ * router.all('/login', ctx => {
1033
+ * ctx.redirect('/sign-in');
1034
+ * ctx.status = 301;
1035
+ * });
1036
+ * ```
1037
+ *
1038
+ * @param source - URL or route name
1039
+ * @param destination - URL or route name
1040
+ * @param code - HTTP status code (default: 301)
1041
+ * @returns This router instance
1042
+ */
1043
+ redirect(source, destination, code) {
1044
+ let resolvedSource = source;
1045
+ let resolvedDestination = destination;
1046
+ if (typeof source === "symbol" || source[0] !== "/") {
1047
+ const sourceUrl = this.url(source);
1048
+ if (sourceUrl instanceof Error) throw sourceUrl;
1049
+ resolvedSource = sourceUrl;
1050
+ }
1051
+ if (typeof destination === "symbol" || destination[0] !== "/" && !destination.includes("://")) {
1052
+ const destinationUrl = this.url(destination);
1053
+ if (destinationUrl instanceof Error) throw destinationUrl;
1054
+ resolvedDestination = destinationUrl;
1055
+ }
1056
+ const result = this.all(
1057
+ resolvedSource,
1058
+ (context) => {
1059
+ context.redirect(resolvedDestination);
1060
+ context.status = code || 301;
1061
+ }
1062
+ );
1063
+ return result;
1064
+ }
1065
+ /**
1066
+ * Create and register a route.
1067
+ *
1068
+ * @param path - Path string
1069
+ * @param methods - Array of HTTP verbs
1070
+ * @param middleware - Middleware functions
1071
+ * @param additionalOptions - Additional options
1072
+ * @returns Created layer
1073
+ * @private
1074
+ */
1075
+ register(path, methods, middleware, additionalOptions = {}) {
1076
+ const mergedOptions = { ...this.opts, ...additionalOptions };
1077
+ if (Array.isArray(path)) {
1078
+ return this._registerMultiplePaths(
1079
+ path,
1080
+ methods,
1081
+ middleware,
1082
+ mergedOptions
1083
+ );
1084
+ }
1085
+ const routeLayer = this._createRouteLayer(
1086
+ path,
1087
+ methods,
1088
+ middleware,
1089
+ mergedOptions
1090
+ );
1091
+ if (this.opts.prefix) {
1092
+ routeLayer.setPrefix(this.opts.prefix);
1093
+ }
1094
+ applyAllParameterMiddleware(routeLayer, this.params);
1095
+ this.stack.push(routeLayer);
1096
+ debug("defined route %s %s", routeLayer.methods, routeLayer.path);
1097
+ return routeLayer;
1098
+ }
1099
+ /**
1100
+ * Register multiple paths with the same configuration
1101
+ * @private
1102
+ */
1103
+ _registerMultiplePaths(pathArray, methods, middleware, options) {
1104
+ for (const singlePath of pathArray) {
1105
+ this.register.call(this, singlePath, methods, middleware, options);
1106
+ }
1107
+ return this;
1108
+ }
1109
+ /**
1110
+ * Create a route layer with given configuration
1111
+ * @private
1112
+ */
1113
+ _createRouteLayer(path, methods, middleware, options) {
1114
+ return new Layer(path, methods, middleware, {
1115
+ end: options.end === false ? options.end : true,
1116
+ name: options.name,
1117
+ sensitive: options.sensitive || false,
1118
+ strict: options.strict || false,
1119
+ prefix: options.prefix || "",
1120
+ ignoreCaptures: options.ignoreCaptures,
1121
+ pathAsRegExp: options.pathAsRegExp
1122
+ });
1123
+ }
1124
+ /**
1125
+ * Lookup route with given `name`.
1126
+ *
1127
+ * @param name - Route name
1128
+ * @returns Matched layer or false
1129
+ */
1130
+ route(name) {
1131
+ const matchingRoute = this.stack.find((route) => route.name === name);
1132
+ return matchingRoute || false;
1133
+ }
1134
+ /**
1135
+ * Generate URL for route. Takes a route name and map of named `params`.
1136
+ *
1137
+ * @example
1138
+ *
1139
+ * ```javascript
1140
+ * router.get('user', '/users/:id', (ctx, next) => {
1141
+ * // ...
1142
+ * });
1143
+ *
1144
+ * router.url('user', 3);
1145
+ * // => "/users/3"
1146
+ *
1147
+ * router.url('user', { id: 3 });
1148
+ * // => "/users/3"
1149
+ *
1150
+ * router.use((ctx, next) => {
1151
+ * // redirect to named route
1152
+ * ctx.redirect(ctx.router.url('sign-in'));
1153
+ * })
1154
+ *
1155
+ * router.url('user', { id: 3 }, { query: { limit: 1 } });
1156
+ * // => "/users/3?limit=1"
1157
+ *
1158
+ * router.url('user', { id: 3 }, { query: "limit=1" });
1159
+ * // => "/users/3?limit=1"
1160
+ * ```
1161
+ *
1162
+ * @param name - Route name
1163
+ * @param args - URL parameters
1164
+ * @returns Generated URL or Error
1165
+ */
1166
+ url(name, ...arguments_) {
1167
+ const route = this.route(name);
1168
+ if (route) return route.url.apply(route, arguments_);
1169
+ return new Error(`No route found for name: ${String(name)}`);
1170
+ }
1171
+ /**
1172
+ * Match given `path` and return corresponding routes.
1173
+ *
1174
+ * @param path - Request path
1175
+ * @param method - HTTP method
1176
+ * @returns Match result with matched layers
1177
+ * @private
1178
+ */
1179
+ match(path, method) {
1180
+ const matchResult = {
1181
+ path: [],
1182
+ pathAndMethod: [],
1183
+ route: false
1184
+ };
1185
+ for (const layer of this.stack) {
1186
+ debug("test %s %s", layer.path, layer.regexp);
1187
+ if (layer.match(path)) {
1188
+ matchResult.path.push(layer);
1189
+ const isMiddleware = layer.methods.length === 0;
1190
+ const matchesMethod = layer.methods.includes(method);
1191
+ if (isMiddleware || matchesMethod) {
1192
+ matchResult.pathAndMethod.push(layer);
1193
+ if (layer.methods.length > 0) {
1194
+ matchResult.route = true;
1195
+ }
1196
+ }
1197
+ }
1198
+ }
1199
+ return matchResult;
1200
+ }
1201
+ /**
1202
+ * Match given `input` to allowed host
1203
+ * @param input - Host to check
1204
+ * @returns Whether host matches
1205
+ */
1206
+ matchHost(input) {
1207
+ const { host } = this;
1208
+ if (!host) {
1209
+ return true;
1210
+ }
1211
+ if (!input) {
1212
+ return false;
1213
+ }
1214
+ if (typeof host === "string") {
1215
+ return input === host;
1216
+ }
1217
+ if (Array.isArray(host)) {
1218
+ return host.includes(input);
1219
+ }
1220
+ if (host instanceof RegExp) {
1221
+ return host.test(input);
1222
+ }
1223
+ return false;
1224
+ }
1225
+ /**
1226
+ * Run middleware for named route parameters. Useful for auto-loading or
1227
+ * validation.
1228
+ *
1229
+ * @example
1230
+ *
1231
+ * ```javascript
1232
+ * router
1233
+ * .param('user', (id, ctx, next) => {
1234
+ * ctx.user = users[id];
1235
+ * if (!ctx.user) return ctx.status = 404;
1236
+ * return next();
1237
+ * })
1238
+ * .get('/users/:user', ctx => {
1239
+ * ctx.body = ctx.user;
1240
+ * })
1241
+ * .get('/users/:user/friends', ctx => {
1242
+ * return ctx.user.getFriends().then(function(friends) {
1243
+ * ctx.body = friends;
1244
+ * });
1245
+ * })
1246
+ * // /users/3 => {"id": 3, "name": "Alex"}
1247
+ * // /users/3/friends => [{"id": 4, "name": "TJ"}]
1248
+ * ```
1249
+ *
1250
+ * @param param - Parameter name
1251
+ * @param middleware - Parameter middleware
1252
+ * @returns This router instance
1253
+ */
1254
+ param(parameter, middleware) {
1255
+ if (!this.params[parameter]) {
1256
+ this.params[parameter] = [];
1257
+ }
1258
+ if (!Array.isArray(this.params[parameter])) {
1259
+ this.params[parameter] = [
1260
+ this.params[parameter]
1261
+ ];
1262
+ }
1263
+ this.params[parameter].push(middleware);
1264
+ for (const route of this.stack) {
1265
+ route.param(parameter, middleware);
1266
+ }
1267
+ return this;
1268
+ }
1269
+ /**
1270
+ * Helper method for registering HTTP verb routes
1271
+ * @internal - Used by dynamically added HTTP methods
1272
+ */
1273
+ _registerMethod(method, ...arguments_) {
1274
+ let name;
1275
+ let path;
1276
+ let middleware;
1277
+ if (arguments_.length >= 2 && (typeof arguments_[1] === "string" || arguments_[1] instanceof RegExp)) {
1278
+ name = arguments_[0];
1279
+ path = arguments_[1];
1280
+ middleware = arguments_.slice(2);
1281
+ } else {
1282
+ name = void 0;
1283
+ path = arguments_[0];
1284
+ middleware = arguments_.slice(1);
1285
+ }
1286
+ if (typeof path !== "string" && !(path instanceof RegExp) && (!Array.isArray(path) || path.length === 0))
1287
+ throw new Error(
1288
+ `You have to provide a path when adding a ${method} handler`
1289
+ );
1290
+ const options = {
1291
+ name,
1292
+ pathAsRegExp: path instanceof RegExp
1293
+ };
1294
+ this.register(path, [method], middleware, {
1295
+ ...this.opts,
1296
+ ...options
1297
+ });
1298
+ return this;
1299
+ }
1300
+ get(...arguments_) {
1301
+ return this._registerMethod("get", ...arguments_);
1302
+ }
1303
+ post(...arguments_) {
1304
+ return this._registerMethod("post", ...arguments_);
1305
+ }
1306
+ put(...arguments_) {
1307
+ return this._registerMethod("put", ...arguments_);
1308
+ }
1309
+ patch(...arguments_) {
1310
+ return this._registerMethod("patch", ...arguments_);
1311
+ }
1312
+ delete(...arguments_) {
1313
+ return this._registerMethod("delete", ...arguments_);
1314
+ }
1315
+ del(...arguments_) {
1316
+ return this.delete.apply(this, arguments_);
1317
+ }
1318
+ head(...arguments_) {
1319
+ return this._registerMethod("head", ...arguments_);
1320
+ }
1321
+ options(...arguments_) {
1322
+ return this._registerMethod("options", ...arguments_);
1323
+ }
1324
+ };
1325
+ for (const httpMethod of httpMethods) {
1326
+ const isAlreadyDefined = COMMON_HTTP_METHODS.includes(httpMethod) || Router.prototype[httpMethod];
1327
+ if (!isAlreadyDefined) {
1328
+ Object.defineProperty(Router.prototype, httpMethod, {
1329
+ value: function(...arguments_) {
1330
+ return this._registerMethod(httpMethod, ...arguments_);
1331
+ },
1332
+ writable: true,
1333
+ configurable: true,
1334
+ enumerable: false
1335
+ });
1336
+ }
1337
+ }
1338
+ export {
1339
+ Router,
1340
+ Router as default
1341
+ };