@koa/router 12.0.1 → 13.0.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.
Files changed (4) hide show
  1. package/README.md +2 -1
  2. package/lib/layer.js +215 -197
  3. package/lib/router.js +669 -677
  4. package/package.json +20 -18
package/lib/router.js CHANGED
@@ -4,69 +4,661 @@
4
4
  * @author Alex Mingoia <talk@alexmingoia.com>
5
5
  * @link https://github.com/alexmingoia/koa-router
6
6
  */
7
+ const http = require('node:http');
8
+ const util = require('node:util');
9
+
10
+ const debug = util.debuglog('koa-router');
7
11
 
8
12
  const compose = require('koa-compose');
9
13
  const HttpError = require('http-errors');
10
- const methods = require('methods');
11
14
  const { pathToRegexp } = require('path-to-regexp');
15
+
12
16
  const Layer = require('./layer');
13
- const debug = require('debug')('koa-router');
17
+
18
+ const methods = http.METHODS.map((method) => method.toLowerCase());
14
19
 
15
20
  /**
16
21
  * @module koa-router
17
22
  */
23
+ class Router {
24
+ /**
25
+ * Create a new router.
26
+ *
27
+ * @example
28
+ *
29
+ * Basic usage:
30
+ *
31
+ * ```javascript
32
+ * const Koa = require('koa');
33
+ * const Router = require('@koa/router');
34
+ *
35
+ * const app = new Koa();
36
+ * const router = new Router();
37
+ *
38
+ * router.get('/', (ctx, next) => {
39
+ * // ctx.router available
40
+ * });
41
+ *
42
+ * app
43
+ * .use(router.routes())
44
+ * .use(router.allowedMethods());
45
+ * ```
46
+ *
47
+ * @alias module:koa-router
48
+ * @param {Object=} opts
49
+ * @param {Boolean=false} opts.exclusive only run last matched route's controller when there are multiple matches
50
+ * @param {String=} opts.prefix prefix router paths
51
+ * @param {String|RegExp=} opts.host host for router match
52
+ * @constructor
53
+ */
54
+ constructor(opts = {}) {
55
+ if (!(this instanceof Router)) return new Router(opts); // eslint-disable-line no-constructor-return
56
+
57
+ this.opts = opts;
58
+ this.methods = this.opts.methods || [
59
+ 'HEAD',
60
+ 'OPTIONS',
61
+ 'GET',
62
+ 'PUT',
63
+ 'PATCH',
64
+ 'POST',
65
+ 'DELETE'
66
+ ];
67
+ this.exclusive = Boolean(this.opts.exclusive);
68
+
69
+ this.params = {};
70
+ this.stack = [];
71
+ this.host = this.opts.host;
72
+ }
18
73
 
19
- module.exports = Router;
74
+ /**
75
+ * Generate URL from url pattern and given `params`.
76
+ *
77
+ * @example
78
+ *
79
+ * ```javascript
80
+ * const url = Router.url('/users/:id', {id: 1});
81
+ * // => "/users/1"
82
+ * ```
83
+ *
84
+ * @param {String} path url pattern
85
+ * @param {Object} params url parameters
86
+ * @returns {String}
87
+ */
88
+ static url(path, ...args) {
89
+ return Layer.prototype.url.apply({ path }, args);
90
+ }
20
91
 
21
- /**
22
- * Create a new router.
23
- *
24
- * @example
25
- *
26
- * Basic usage:
27
- *
28
- * ```javascript
29
- * const Koa = require('koa');
30
- * const Router = require('@koa/router');
31
- *
32
- * const app = new Koa();
33
- * const router = new Router();
34
- *
35
- * router.get('/', (ctx, next) => {
36
- * // ctx.router available
37
- * });
38
- *
39
- * app
40
- * .use(router.routes())
41
- * .use(router.allowedMethods());
42
- * ```
43
- *
44
- * @alias module:koa-router
45
- * @param {Object=} opts
46
- * @param {Boolean=false} opts.exclusive only run last matched route's controller when there are multiple matches
47
- * @param {String=} opts.prefix prefix router paths
48
- * @param {String|RegExp=} opts.host host for router match
49
- * @constructor
50
- */
92
+ /**
93
+ * Use given middleware.
94
+ *
95
+ * Middleware run in the order they are defined by `.use()`. They are invoked
96
+ * sequentially, requests start at the first middleware and work their way
97
+ * "down" the middleware stack.
98
+ *
99
+ * @example
100
+ *
101
+ * ```javascript
102
+ * // session middleware will run before authorize
103
+ * router
104
+ * .use(session())
105
+ * .use(authorize());
106
+ *
107
+ * // use middleware only with given path
108
+ * router.use('/users', userAuth());
109
+ *
110
+ * // or with an array of paths
111
+ * router.use(['/users', '/admin'], userAuth());
112
+ *
113
+ * app.use(router.routes());
114
+ * ```
115
+ *
116
+ * @param {String=} path
117
+ * @param {Function} middleware
118
+ * @param {Function=} ...
119
+ * @returns {Router}
120
+ */
121
+ use(...middleware) {
122
+ const router = this;
123
+ let path;
124
+
125
+ // support array of paths
126
+ if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
127
+ const arrPaths = middleware[0];
128
+ for (const p of arrPaths) {
129
+ router.use.apply(router, [p, ...middleware.slice(1)]);
130
+ }
131
+
132
+ return this;
133
+ }
134
+
135
+ const hasPath = typeof middleware[0] === 'string';
136
+ if (hasPath) path = middleware.shift();
137
+
138
+ for (const m of middleware) {
139
+ if (m.router) {
140
+ const cloneRouter = Object.assign(
141
+ Object.create(Router.prototype),
142
+ m.router,
143
+ {
144
+ stack: [...m.router.stack]
145
+ }
146
+ );
147
+
148
+ for (let j = 0; j < cloneRouter.stack.length; j++) {
149
+ const nestedLayer = cloneRouter.stack[j];
150
+ const cloneLayer = Object.assign(
151
+ Object.create(Layer.prototype),
152
+ nestedLayer
153
+ );
154
+
155
+ if (path) cloneLayer.setPrefix(path);
156
+ if (router.opts.prefix) cloneLayer.setPrefix(router.opts.prefix);
157
+ router.stack.push(cloneLayer);
158
+ cloneRouter.stack[j] = cloneLayer;
159
+ }
160
+
161
+ if (router.params) {
162
+ const routerParams = Object.keys(router.params);
163
+ for (const key of routerParams) {
164
+ cloneRouter.param(key, router.params[key]);
165
+ }
166
+ }
167
+ } else {
168
+ const { keys } = pathToRegexp(router.opts.prefix || '', router.opts);
169
+ const routerPrefixHasParam = Boolean(
170
+ router.opts.prefix && keys.length > 0
171
+ );
172
+ router.register(path || '([^/]*)', [], m, {
173
+ end: false,
174
+ ignoreCaptures: !hasPath && !routerPrefixHasParam,
175
+ pathIsRegexp: true
176
+ });
177
+ }
178
+ }
179
+
180
+ return this;
181
+ }
182
+
183
+ /**
184
+ * Set the path prefix for a Router instance that was already initialized.
185
+ *
186
+ * @example
187
+ *
188
+ * ```javascript
189
+ * router.prefix('/things/:thing_id')
190
+ * ```
191
+ *
192
+ * @param {String} prefix
193
+ * @returns {Router}
194
+ */
195
+ prefix(prefix) {
196
+ prefix = prefix.replace(/\/$/, '');
197
+
198
+ this.opts.prefix = prefix;
199
+
200
+ for (let i = 0; i < this.stack.length; i++) {
201
+ const route = this.stack[i];
202
+ route.setPrefix(prefix);
203
+ }
204
+
205
+ return this;
206
+ }
207
+
208
+ /**
209
+ * Returns router middleware which dispatches a route matching the request.
210
+ *
211
+ * @returns {Function}
212
+ */
213
+ middleware() {
214
+ const router = this;
215
+ const dispatch = (ctx, next) => {
216
+ debug('%s %s', ctx.method, ctx.path);
217
+
218
+ const hostMatched = router.matchHost(ctx.host);
219
+
220
+ if (!hostMatched) {
221
+ return next();
222
+ }
223
+
224
+ const path =
225
+ router.opts.routerPath ||
226
+ ctx.newRouterPath ||
227
+ ctx.path ||
228
+ ctx.routerPath;
229
+ const matched = router.match(path, ctx.method);
230
+ if (ctx.matched) {
231
+ ctx.matched.push.apply(ctx.matched, matched.path);
232
+ } else {
233
+ ctx.matched = matched.path;
234
+ }
235
+
236
+ ctx.router = router;
237
+
238
+ if (!matched.route) return next();
239
+
240
+ const matchedLayers = matched.pathAndMethod;
241
+ const mostSpecificLayer = matchedLayers[matchedLayers.length - 1];
242
+ ctx._matchedRoute = mostSpecificLayer.path;
243
+ if (mostSpecificLayer.name) {
244
+ ctx._matchedRouteName = mostSpecificLayer.name;
245
+ }
246
+
247
+ const layerChain = (
248
+ router.exclusive ? [mostSpecificLayer] : matchedLayers
249
+ ).reduce((memo, layer) => {
250
+ memo.push((ctx, next) => {
251
+ ctx.captures = layer.captures(path, ctx.captures);
252
+ ctx.request.params = layer.params(path, ctx.captures, ctx.params);
253
+ ctx.params = ctx.request.params;
254
+ ctx.routerPath = layer.path;
255
+ ctx.routerName = layer.name;
256
+ ctx._matchedRoute = layer.path;
257
+ if (layer.name) {
258
+ ctx._matchedRouteName = layer.name;
259
+ }
260
+
261
+ return next();
262
+ });
263
+ return [...memo, ...layer.stack];
264
+ }, []);
265
+
266
+ return compose(layerChain)(ctx, next);
267
+ };
268
+
269
+ dispatch.router = this;
270
+
271
+ return dispatch;
272
+ }
273
+
274
+ routes() {
275
+ return this.middleware();
276
+ }
277
+
278
+ /**
279
+ * Returns separate middleware for responding to `OPTIONS` requests with
280
+ * an `Allow` header containing the allowed methods, as well as responding
281
+ * with `405 Method Not Allowed` and `501 Not Implemented` as appropriate.
282
+ *
283
+ * @example
284
+ *
285
+ * ```javascript
286
+ * const Koa = require('koa');
287
+ * const Router = require('@koa/router');
288
+ *
289
+ * const app = new Koa();
290
+ * const router = new Router();
291
+ *
292
+ * app.use(router.routes());
293
+ * app.use(router.allowedMethods());
294
+ * ```
295
+ *
296
+ * **Example with [Boom](https://github.com/hapijs/boom)**
297
+ *
298
+ * ```javascript
299
+ * const Koa = require('koa');
300
+ * const Router = require('@koa/router');
301
+ * const Boom = require('boom');
302
+ *
303
+ * const app = new Koa();
304
+ * const router = new Router();
305
+ *
306
+ * app.use(router.routes());
307
+ * app.use(router.allowedMethods({
308
+ * throw: true,
309
+ * notImplemented: () => new Boom.notImplemented(),
310
+ * methodNotAllowed: () => new Boom.methodNotAllowed()
311
+ * }));
312
+ * ```
313
+ *
314
+ * @param {Object=} options
315
+ * @param {Boolean=} options.throw throw error instead of setting status and header
316
+ * @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error
317
+ * @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error
318
+ * @returns {Function}
319
+ */
320
+ allowedMethods(options = {}) {
321
+ const implemented = this.methods;
322
+
323
+ return (ctx, next) => {
324
+ return next().then(() => {
325
+ const allowed = {};
326
+
327
+ if (ctx.matched && (!ctx.status || ctx.status === 404)) {
328
+ for (let i = 0; i < ctx.matched.length; i++) {
329
+ const route = ctx.matched[i];
330
+ for (let j = 0; j < route.methods.length; j++) {
331
+ const method = route.methods[j];
332
+ allowed[method] = method;
333
+ }
334
+ }
335
+
336
+ const allowedArr = Object.keys(allowed);
337
+ if (!implemented.includes(ctx.method)) {
338
+ if (options.throw) {
339
+ const notImplementedThrowable =
340
+ typeof options.notImplemented === 'function'
341
+ ? options.notImplemented() // set whatever the user returns from their function
342
+ : new HttpError.NotImplemented();
343
+
344
+ throw notImplementedThrowable;
345
+ } else {
346
+ ctx.status = 501;
347
+ ctx.set('Allow', allowedArr.join(', '));
348
+ }
349
+ } else if (allowedArr.length > 0) {
350
+ if (ctx.method === 'OPTIONS') {
351
+ ctx.status = 200;
352
+ ctx.body = '';
353
+ ctx.set('Allow', allowedArr.join(', '));
354
+ } else if (!allowed[ctx.method]) {
355
+ if (options.throw) {
356
+ const notAllowedThrowable =
357
+ typeof options.methodNotAllowed === 'function'
358
+ ? options.methodNotAllowed() // set whatever the user returns from their function
359
+ : new HttpError.MethodNotAllowed();
360
+
361
+ throw notAllowedThrowable;
362
+ } else {
363
+ ctx.status = 405;
364
+ ctx.set('Allow', allowedArr.join(', '));
365
+ }
366
+ }
367
+ }
368
+ }
369
+ });
370
+ };
371
+ }
372
+
373
+ /**
374
+ * Register route with all methods.
375
+ *
376
+ * @param {String} name Optional.
377
+ * @param {String} path
378
+ * @param {Function=} middleware You may also pass multiple middleware.
379
+ * @param {Function} callback
380
+ * @returns {Router}
381
+ */
382
+ all(name, path, middleware) {
383
+ if (typeof path === 'string' || path instanceof RegExp) {
384
+ middleware = Array.prototype.slice.call(arguments, 2);
385
+ } else {
386
+ middleware = Array.prototype.slice.call(arguments, 1);
387
+ path = name;
388
+ name = null;
389
+ }
390
+
391
+ // Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array)
392
+ if (
393
+ typeof path !== 'string' &&
394
+ !(path instanceof RegExp) &&
395
+ (!Array.isArray(path) || path.length === 0)
396
+ )
397
+ throw new Error('You have to provide a path when adding an all handler');
398
+
399
+ const opts = {
400
+ name,
401
+ pathIsRegexp: path instanceof RegExp
402
+ };
403
+
404
+ this.register(path, methods, middleware, { ...this.opts, ...opts });
405
+
406
+ return this;
407
+ }
408
+
409
+ /**
410
+ * Redirect `source` to `destination` URL with optional 30x status `code`.
411
+ *
412
+ * Both `source` and `destination` can be route names.
413
+ *
414
+ * ```javascript
415
+ * router.redirect('/login', 'sign-in');
416
+ * ```
417
+ *
418
+ * This is equivalent to:
419
+ *
420
+ * ```javascript
421
+ * router.all('/login', ctx => {
422
+ * ctx.redirect('/sign-in');
423
+ * ctx.status = 301;
424
+ * });
425
+ * ```
426
+ *
427
+ * @param {String} source URL or route name.
428
+ * @param {String} destination URL or route name.
429
+ * @param {Number=} code HTTP status code (default: 301).
430
+ * @returns {Router}
431
+ */
432
+ redirect(source, destination, code) {
433
+ // lookup source route by name
434
+ if (typeof source === 'symbol' || source[0] !== '/') {
435
+ source = this.url(source);
436
+ if (source instanceof Error) throw source;
437
+ }
438
+
439
+ // lookup destination route by name
440
+ if (
441
+ typeof destination === 'symbol' ||
442
+ (destination[0] !== '/' && !destination.includes('://'))
443
+ ) {
444
+ destination = this.url(destination);
445
+ if (destination instanceof Error) throw destination;
446
+ }
447
+
448
+ return this.all(source, (ctx) => {
449
+ ctx.redirect(destination);
450
+ ctx.status = code || 301;
451
+ });
452
+ }
453
+
454
+ /**
455
+ * Create and register a route.
456
+ *
457
+ * @param {String} path Path string.
458
+ * @param {Array.<String>} methods Array of HTTP verbs.
459
+ * @param {Function} middleware Multiple middleware also accepted.
460
+ * @returns {Layer}
461
+ * @private
462
+ */
463
+ register(path, methods, middleware, newOpts = {}) {
464
+ const router = this;
465
+ const { stack } = this;
466
+ const opts = { ...this.opts, ...newOpts };
467
+ // support array of paths
468
+ if (Array.isArray(path)) {
469
+ for (const curPath of path) {
470
+ router.register.call(router, curPath, methods, middleware, opts);
471
+ }
51
472
 
52
- function Router(opts = {}) {
53
- if (!(this instanceof Router)) return new Router(opts);
54
-
55
- this.opts = opts;
56
- this.methods = this.opts.methods || [
57
- 'HEAD',
58
- 'OPTIONS',
59
- 'GET',
60
- 'PUT',
61
- 'PATCH',
62
- 'POST',
63
- 'DELETE'
64
- ];
65
- this.exclusive = Boolean(this.opts.exclusive);
66
-
67
- this.params = {};
68
- this.stack = [];
69
- this.host = this.opts.host;
473
+ return this;
474
+ }
475
+
476
+ // create route
477
+ const route = new Layer(path, methods, middleware, {
478
+ end: opts.end === false ? opts.end : true,
479
+ name: opts.name,
480
+ sensitive: opts.sensitive || false,
481
+ strict: opts.strict || false,
482
+ prefix: opts.prefix || '',
483
+ ignoreCaptures: opts.ignoreCaptures,
484
+ pathIsRegexp: opts.pathIsRegexp
485
+ });
486
+
487
+ // if parent prefix exists, add prefix to new route
488
+ if (this.opts.prefix) {
489
+ route.setPrefix(this.opts.prefix);
490
+ }
491
+
492
+ // add parameter middleware
493
+ for (let i = 0; i < Object.keys(this.params).length; i++) {
494
+ const param = Object.keys(this.params)[i];
495
+ route.param(param, this.params[param]);
496
+ }
497
+
498
+ stack.push(route);
499
+
500
+ debug('defined route %s %s', route.methods, route.path);
501
+
502
+ return route;
503
+ }
504
+
505
+ /**
506
+ * Lookup route with given `name`.
507
+ *
508
+ * @param {String} name
509
+ * @returns {Layer|false}
510
+ */
511
+ route(name) {
512
+ const routes = this.stack;
513
+
514
+ for (let len = routes.length, i = 0; i < len; i++) {
515
+ if (routes[i].name && routes[i].name === name) return routes[i];
516
+ }
517
+
518
+ return false;
519
+ }
520
+
521
+ /**
522
+ * Generate URL for route. Takes a route name and map of named `params`.
523
+ *
524
+ * @example
525
+ *
526
+ * ```javascript
527
+ * router.get('user', '/users/:id', (ctx, next) => {
528
+ * // ...
529
+ * });
530
+ *
531
+ * router.url('user', 3);
532
+ * // => "/users/3"
533
+ *
534
+ * router.url('user', { id: 3 });
535
+ * // => "/users/3"
536
+ *
537
+ * router.use((ctx, next) => {
538
+ * // redirect to named route
539
+ * ctx.redirect(ctx.router.url('sign-in'));
540
+ * })
541
+ *
542
+ * router.url('user', { id: 3 }, { query: { limit: 1 } });
543
+ * // => "/users/3?limit=1"
544
+ *
545
+ * router.url('user', { id: 3 }, { query: "limit=1" });
546
+ * // => "/users/3?limit=1"
547
+ * ```
548
+ *
549
+ * @param {String} name route name
550
+ * @param {Object} params url parameters
551
+ * @param {Object} [options] options parameter
552
+ * @param {Object|String} [options.query] query options
553
+ * @returns {String|Error}
554
+ */
555
+ url(name, ...args) {
556
+ const route = this.route(name);
557
+ if (route) return route.url.apply(route, args);
558
+
559
+ return new Error(`No route found for name: ${String(name)}`);
560
+ }
561
+
562
+ /**
563
+ * Match given `path` and return corresponding routes.
564
+ *
565
+ * @param {String} path
566
+ * @param {String} method
567
+ * @returns {Object.<path, pathAndMethod>} returns layers that matched path and
568
+ * path and method.
569
+ * @private
570
+ */
571
+ match(path, method) {
572
+ const layers = this.stack;
573
+ let layer;
574
+ const matched = {
575
+ path: [],
576
+ pathAndMethod: [],
577
+ route: false
578
+ };
579
+
580
+ for (let len = layers.length, i = 0; i < len; i++) {
581
+ layer = layers[i];
582
+
583
+ debug('test %s %s', layer.path, layer.regexp);
584
+
585
+ // eslint-disable-next-line unicorn/prefer-regexp-test
586
+ if (layer.match(path)) {
587
+ matched.path.push(layer);
588
+
589
+ if (layer.methods.length === 0 || layer.methods.includes(method)) {
590
+ matched.pathAndMethod.push(layer);
591
+ if (layer.methods.length > 0) matched.route = true;
592
+ }
593
+ }
594
+ }
595
+
596
+ return matched;
597
+ }
598
+
599
+ /**
600
+ * Match given `input` to allowed host
601
+ * @param {String} input
602
+ * @returns {boolean}
603
+ */
604
+ matchHost(input) {
605
+ const { host } = this;
606
+
607
+ if (!host) {
608
+ return true;
609
+ }
610
+
611
+ if (!input) {
612
+ return false;
613
+ }
614
+
615
+ if (typeof host === 'string') {
616
+ return input === host;
617
+ }
618
+
619
+ if (typeof host === 'object' && host instanceof RegExp) {
620
+ return host.test(input);
621
+ }
622
+ }
623
+
624
+ /**
625
+ * Run middleware for named route parameters. Useful for auto-loading or
626
+ * validation.
627
+ *
628
+ * @example
629
+ *
630
+ * ```javascript
631
+ * router
632
+ * .param('user', (id, ctx, next) => {
633
+ * ctx.user = users[id];
634
+ * if (!ctx.user) return ctx.status = 404;
635
+ * return next();
636
+ * })
637
+ * .get('/users/:user', ctx => {
638
+ * ctx.body = ctx.user;
639
+ * })
640
+ * .get('/users/:user/friends', ctx => {
641
+ * return ctx.user.getFriends().then(function(friends) {
642
+ * ctx.body = friends;
643
+ * });
644
+ * })
645
+ * // /users/3 => {"id": 3, "name": "Alex"}
646
+ * // /users/3/friends => [{"id": 4, "name": "TJ"}]
647
+ * ```
648
+ *
649
+ * @param {String} param
650
+ * @param {Function} middleware
651
+ * @returns {Router}
652
+ */
653
+ param(param, middleware) {
654
+ this.params[param] = middleware;
655
+ for (let i = 0; i < this.stack.length; i++) {
656
+ const route = this.stack[i];
657
+ route.param(param, middleware);
658
+ }
659
+
660
+ return this;
661
+ }
70
662
  }
71
663
 
72
664
  /**
@@ -207,639 +799,39 @@ function Router(opts = {}) {
207
799
  * @param {Function} callback route callback
208
800
  * @returns {Router}
209
801
  */
802
+ for (const method of methods) {
803
+ Router.prototype[method] = function (name, path, middleware) {
804
+ if (typeof path === 'string' || path instanceof RegExp) {
805
+ middleware = Array.prototype.slice.call(arguments, 2);
806
+ } else {
807
+ middleware = Array.prototype.slice.call(arguments, 1);
808
+ path = name;
809
+ name = null;
810
+ }
210
811
 
211
- for (const method_ of methods) {
212
- function setMethodVerb(method) {
213
- Router.prototype[method] = function (name, path, middleware) {
214
- if (typeof path === 'string' || path instanceof RegExp) {
215
- middleware = Array.prototype.slice.call(arguments, 2);
216
- } else {
217
- middleware = Array.prototype.slice.call(arguments, 1);
218
- path = name;
219
- name = null;
220
- }
221
-
222
- // Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array)
223
- if (
224
- typeof path !== 'string' &&
225
- !(path instanceof RegExp) &&
226
- (!Array.isArray(path) || path.length === 0)
227
- )
228
- throw new Error(
229
- `You have to provide a path when adding a ${method} handler`
230
- );
231
-
232
- this.register(path, [method], middleware, { name });
812
+ // Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array)
813
+ if (
814
+ typeof path !== 'string' &&
815
+ !(path instanceof RegExp) &&
816
+ (!Array.isArray(path) || path.length === 0)
817
+ )
818
+ throw new Error(
819
+ `You have to provide a path when adding a ${method} handler`
820
+ );
233
821
 
234
- return this;
822
+ const opts = {
823
+ name,
824
+ pathIsRegexp: path instanceof RegExp
235
825
  };
236
- }
237
826
 
238
- setMethodVerb(method_);
827
+ // pass opts to register call on verb methods
828
+ this.register(path, [method], middleware, { ...this.opts, ...opts });
829
+ return this;
830
+ };
239
831
  }
240
832
 
241
833
  // Alias for `router.delete()` because delete is a reserved word
242
834
  // eslint-disable-next-line dot-notation
243
835
  Router.prototype.del = Router.prototype['delete'];
244
836
 
245
- /**
246
- * Use given middleware.
247
- *
248
- * Middleware run in the order they are defined by `.use()`. They are invoked
249
- * sequentially, requests start at the first middleware and work their way
250
- * "down" the middleware stack.
251
- *
252
- * @example
253
- *
254
- * ```javascript
255
- * // session middleware will run before authorize
256
- * router
257
- * .use(session())
258
- * .use(authorize());
259
- *
260
- * // use middleware only with given path
261
- * router.use('/users', userAuth());
262
- *
263
- * // or with an array of paths
264
- * router.use(['/users', '/admin'], userAuth());
265
- *
266
- * app.use(router.routes());
267
- * ```
268
- *
269
- * @param {String=} path
270
- * @param {Function} middleware
271
- * @param {Function=} ...
272
- * @returns {Router}
273
- */
274
-
275
- Router.prototype.use = function () {
276
- const router = this;
277
- const middleware = Array.prototype.slice.call(arguments);
278
- let path;
279
-
280
- // support array of paths
281
- if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
282
- const arrPaths = middleware[0];
283
- for (const p of arrPaths) {
284
- router.use.apply(router, [p].concat(middleware.slice(1)));
285
- }
286
-
287
- return this;
288
- }
289
-
290
- const hasPath = typeof middleware[0] === 'string';
291
- if (hasPath) path = middleware.shift();
292
-
293
- for (const m of middleware) {
294
- if (m.router) {
295
- const cloneRouter = Object.assign(
296
- Object.create(Router.prototype),
297
- m.router,
298
- {
299
- stack: [...m.router.stack]
300
- }
301
- );
302
-
303
- for (let j = 0; j < cloneRouter.stack.length; j++) {
304
- const nestedLayer = cloneRouter.stack[j];
305
- const cloneLayer = Object.assign(
306
- Object.create(Layer.prototype),
307
- nestedLayer
308
- );
309
-
310
- if (path) cloneLayer.setPrefix(path);
311
- if (router.opts.prefix) cloneLayer.setPrefix(router.opts.prefix);
312
- router.stack.push(cloneLayer);
313
- cloneRouter.stack[j] = cloneLayer;
314
- }
315
-
316
- if (router.params) {
317
- function setRouterParams(paramArr) {
318
- const routerParams = paramArr;
319
- for (const key of routerParams) {
320
- cloneRouter.param(key, router.params[key]);
321
- }
322
- }
323
-
324
- setRouterParams(Object.keys(router.params));
325
- }
326
- } else {
327
- const keys = [];
328
- pathToRegexp(router.opts.prefix || '', keys);
329
- const routerPrefixHasParam = router.opts.prefix && keys.length;
330
- router.register(path || '([^/]*)', [], m, {
331
- end: false,
332
- ignoreCaptures: !hasPath && !routerPrefixHasParam
333
- });
334
- }
335
- }
336
-
337
- return this;
338
- };
339
-
340
- /**
341
- * Set the path prefix for a Router instance that was already initialized.
342
- *
343
- * @example
344
- *
345
- * ```javascript
346
- * router.prefix('/things/:thing_id')
347
- * ```
348
- *
349
- * @param {String} prefix
350
- * @returns {Router}
351
- */
352
-
353
- Router.prototype.prefix = function (prefix) {
354
- prefix = prefix.replace(/\/$/, '');
355
-
356
- this.opts.prefix = prefix;
357
-
358
- for (let i = 0; i < this.stack.length; i++) {
359
- const route = this.stack[i];
360
- route.setPrefix(prefix);
361
- }
362
-
363
- return this;
364
- };
365
-
366
- /**
367
- * Returns router middleware which dispatches a route matching the request.
368
- *
369
- * @returns {Function}
370
- */
371
-
372
- Router.prototype.routes = Router.prototype.middleware = function () {
373
- const router = this;
374
-
375
- const dispatch = function dispatch(ctx, next) {
376
- debug('%s %s', ctx.method, ctx.path);
377
-
378
- const hostMatched = router.matchHost(ctx.host);
379
-
380
- if (!hostMatched) {
381
- return next();
382
- }
383
-
384
- const path =
385
- router.opts.routerPath || ctx.newRouterPath || ctx.path || ctx.routerPath;
386
- const matched = router.match(path, ctx.method);
387
- let layerChain;
388
-
389
- if (ctx.matched) {
390
- ctx.matched.push.apply(ctx.matched, matched.path);
391
- } else {
392
- ctx.matched = matched.path;
393
- }
394
-
395
- ctx.router = router;
396
-
397
- if (!matched.route) return next();
398
-
399
- const matchedLayers = matched.pathAndMethod;
400
- const mostSpecificLayer = matchedLayers[matchedLayers.length - 1];
401
- ctx._matchedRoute = mostSpecificLayer.path;
402
- if (mostSpecificLayer.name) {
403
- ctx._matchedRouteName = mostSpecificLayer.name;
404
- }
405
-
406
- layerChain = (
407
- router.exclusive ? [mostSpecificLayer] : matchedLayers
408
- ).reduce(function (memo, layer) {
409
- memo.push(function (ctx, next) {
410
- ctx.captures = layer.captures(path, ctx.captures);
411
- ctx.params = ctx.request.params = layer.params(
412
- path,
413
- ctx.captures,
414
- ctx.params
415
- );
416
- ctx.routerPath = layer.path;
417
- ctx.routerName = layer.name;
418
- ctx._matchedRoute = layer.path;
419
- if (layer.name) {
420
- ctx._matchedRouteName = layer.name;
421
- }
422
-
423
- return next();
424
- });
425
- return memo.concat(layer.stack);
426
- }, []);
427
-
428
- return compose(layerChain)(ctx, next);
429
- };
430
-
431
- dispatch.router = this;
432
-
433
- return dispatch;
434
- };
435
-
436
- /**
437
- * Returns separate middleware for responding to `OPTIONS` requests with
438
- * an `Allow` header containing the allowed methods, as well as responding
439
- * with `405 Method Not Allowed` and `501 Not Implemented` as appropriate.
440
- *
441
- * @example
442
- *
443
- * ```javascript
444
- * const Koa = require('koa');
445
- * const Router = require('@koa/router');
446
- *
447
- * const app = new Koa();
448
- * const router = new Router();
449
- *
450
- * app.use(router.routes());
451
- * app.use(router.allowedMethods());
452
- * ```
453
- *
454
- * **Example with [Boom](https://github.com/hapijs/boom)**
455
- *
456
- * ```javascript
457
- * const Koa = require('koa');
458
- * const Router = require('@koa/router');
459
- * const Boom = require('boom');
460
- *
461
- * const app = new Koa();
462
- * const router = new Router();
463
- *
464
- * app.use(router.routes());
465
- * app.use(router.allowedMethods({
466
- * throw: true,
467
- * notImplemented: () => new Boom.notImplemented(),
468
- * methodNotAllowed: () => new Boom.methodNotAllowed()
469
- * }));
470
- * ```
471
- *
472
- * @param {Object=} options
473
- * @param {Boolean=} options.throw throw error instead of setting status and header
474
- * @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error
475
- * @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error
476
- * @returns {Function}
477
- */
478
-
479
- Router.prototype.allowedMethods = function (options = {}) {
480
- const implemented = this.methods;
481
-
482
- return function allowedMethods(ctx, next) {
483
- return next().then(function () {
484
- const allowed = {};
485
-
486
- if (!ctx.status || ctx.status === 404) {
487
- for (let i = 0; i < ctx.matched.length; i++) {
488
- const route = ctx.matched[i];
489
- for (let j = 0; j < route.methods.length; j++) {
490
- const method = route.methods[j];
491
- allowed[method] = method;
492
- }
493
- }
494
-
495
- const allowedArr = Object.keys(allowed);
496
-
497
- if (!~implemented.indexOf(ctx.method)) {
498
- if (options.throw) {
499
- const notImplementedThrowable =
500
- typeof options.notImplemented === 'function'
501
- ? options.notImplemented() // set whatever the user returns from their function
502
- : new HttpError.NotImplemented();
503
-
504
- throw notImplementedThrowable;
505
- } else {
506
- ctx.status = 501;
507
- ctx.set('Allow', allowedArr.join(', '));
508
- }
509
- } else if (allowedArr.length > 0) {
510
- if (ctx.method === 'OPTIONS') {
511
- ctx.status = 200;
512
- ctx.body = '';
513
- ctx.set('Allow', allowedArr.join(', '));
514
- } else if (!allowed[ctx.method]) {
515
- if (options.throw) {
516
- const notAllowedThrowable =
517
- typeof options.methodNotAllowed === 'function'
518
- ? options.methodNotAllowed() // set whatever the user returns from their function
519
- : new HttpError.MethodNotAllowed();
520
-
521
- throw notAllowedThrowable;
522
- } else {
523
- ctx.status = 405;
524
- ctx.set('Allow', allowedArr.join(', '));
525
- }
526
- }
527
- }
528
- }
529
- });
530
- };
531
- };
532
-
533
- /**
534
- * Register route with all methods.
535
- *
536
- * @param {String} name Optional.
537
- * @param {String} path
538
- * @param {Function=} middleware You may also pass multiple middleware.
539
- * @param {Function} callback
540
- * @returns {Router}
541
- */
542
-
543
- Router.prototype.all = function (name, path, middleware) {
544
- if (typeof path === 'string') {
545
- middleware = Array.prototype.slice.call(arguments, 2);
546
- } else {
547
- middleware = Array.prototype.slice.call(arguments, 1);
548
- path = name;
549
- name = null;
550
- }
551
-
552
- // Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array)
553
- if (
554
- typeof path !== 'string' &&
555
- !(path instanceof RegExp) &&
556
- (!Array.isArray(path) || path.length === 0)
557
- )
558
- throw new Error('You have to provide a path when adding an all handler');
559
-
560
- this.register(path, methods, middleware, { name });
561
-
562
- return this;
563
- };
564
-
565
- /**
566
- * Redirect `source` to `destination` URL with optional 30x status `code`.
567
- *
568
- * Both `source` and `destination` can be route names.
569
- *
570
- * ```javascript
571
- * router.redirect('/login', 'sign-in');
572
- * ```
573
- *
574
- * This is equivalent to:
575
- *
576
- * ```javascript
577
- * router.all('/login', ctx => {
578
- * ctx.redirect('/sign-in');
579
- * ctx.status = 301;
580
- * });
581
- * ```
582
- *
583
- * @param {String} source URL or route name.
584
- * @param {String} destination URL or route name.
585
- * @param {Number=} code HTTP status code (default: 301).
586
- * @returns {Router}
587
- */
588
-
589
- Router.prototype.redirect = function (source, destination, code) {
590
- // lookup source route by name
591
- if (typeof source === 'symbol' || source[0] !== '/') {
592
- source = this.url(source);
593
- if (source instanceof Error) throw source;
594
- }
595
-
596
- // lookup destination route by name
597
- if (
598
- typeof destination === 'symbol' ||
599
- (destination[0] !== '/' && !destination.includes('://'))
600
- ) {
601
- destination = this.url(destination);
602
- if (destination instanceof Error) throw destination;
603
- }
604
-
605
- return this.all(source, (ctx) => {
606
- ctx.redirect(destination);
607
- ctx.status = code || 301;
608
- });
609
- };
610
-
611
- /**
612
- * Create and register a route.
613
- *
614
- * @param {String} path Path string.
615
- * @param {Array.<String>} methods Array of HTTP verbs.
616
- * @param {Function} middleware Multiple middleware also accepted.
617
- * @returns {Layer}
618
- * @private
619
- */
620
-
621
- Router.prototype.register = function (path, methods, middleware, opts = {}) {
622
- const router = this;
623
- const { stack } = this;
624
-
625
- // support array of paths
626
- if (Array.isArray(path)) {
627
- for (const curPath of path) {
628
- router.register.call(router, curPath, methods, middleware, opts);
629
- }
630
-
631
- return this;
632
- }
633
-
634
- // create route
635
- const route = new Layer(path, methods, middleware, {
636
- end: opts.end === false ? opts.end : true,
637
- name: opts.name,
638
- sensitive: opts.sensitive || this.opts.sensitive || false,
639
- strict: opts.strict || this.opts.strict || false,
640
- prefix: opts.prefix || this.opts.prefix || '',
641
- ignoreCaptures: opts.ignoreCaptures
642
- });
643
-
644
- if (this.opts.prefix) {
645
- route.setPrefix(this.opts.prefix);
646
- }
647
-
648
- // add parameter middleware
649
- for (let i = 0; i < Object.keys(this.params).length; i++) {
650
- const param = Object.keys(this.params)[i];
651
- route.param(param, this.params[param]);
652
- }
653
-
654
- stack.push(route);
655
-
656
- debug('defined route %s %s', route.methods, route.path);
657
-
658
- return route;
659
- };
660
-
661
- /**
662
- * Lookup route with given `name`.
663
- *
664
- * @param {String} name
665
- * @returns {Layer|false}
666
- */
667
-
668
- Router.prototype.route = function (name) {
669
- const routes = this.stack;
670
-
671
- for (let len = routes.length, i = 0; i < len; i++) {
672
- if (routes[i].name && routes[i].name === name) return routes[i];
673
- }
674
-
675
- return false;
676
- };
677
-
678
- /**
679
- * Generate URL for route. Takes a route name and map of named `params`.
680
- *
681
- * @example
682
- *
683
- * ```javascript
684
- * router.get('user', '/users/:id', (ctx, next) => {
685
- * // ...
686
- * });
687
- *
688
- * router.url('user', 3);
689
- * // => "/users/3"
690
- *
691
- * router.url('user', { id: 3 });
692
- * // => "/users/3"
693
- *
694
- * router.use((ctx, next) => {
695
- * // redirect to named route
696
- * ctx.redirect(ctx.router.url('sign-in'));
697
- * })
698
- *
699
- * router.url('user', { id: 3 }, { query: { limit: 1 } });
700
- * // => "/users/3?limit=1"
701
- *
702
- * router.url('user', { id: 3 }, { query: "limit=1" });
703
- * // => "/users/3?limit=1"
704
- * ```
705
- *
706
- * @param {String} name route name
707
- * @param {Object} params url parameters
708
- * @param {Object} [options] options parameter
709
- * @param {Object|String} [options.query] query options
710
- * @returns {String|Error}
711
- */
712
-
713
- Router.prototype.url = function (name, params) {
714
- const route = this.route(name);
715
-
716
- if (route) {
717
- const args = Array.prototype.slice.call(arguments, 1);
718
- return route.url.apply(route, args);
719
- }
720
-
721
- return new Error(`No route found for name: ${String(name)}`);
722
- };
723
-
724
- /**
725
- * Match given `path` and return corresponding routes.
726
- *
727
- * @param {String} path
728
- * @param {String} method
729
- * @returns {Object.<path, pathAndMethod>} returns layers that matched path and
730
- * path and method.
731
- * @private
732
- */
733
-
734
- Router.prototype.match = function (path, method) {
735
- const layers = this.stack;
736
- let layer;
737
- const matched = {
738
- path: [],
739
- pathAndMethod: [],
740
- route: false
741
- };
742
-
743
- for (let len = layers.length, i = 0; i < len; i++) {
744
- layer = layers[i];
745
-
746
- debug('test %s %s', layer.path, layer.regexp);
747
-
748
- // eslint-disable-next-line unicorn/prefer-regexp-test
749
- if (layer.match(path)) {
750
- matched.path.push(layer);
751
-
752
- if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
753
- matched.pathAndMethod.push(layer);
754
- if (layer.methods.length > 0) matched.route = true;
755
- }
756
- }
757
- }
758
-
759
- return matched;
760
- };
761
-
762
- /**
763
- * Match given `input` to allowed host
764
- * @param {String} input
765
- * @returns {boolean}
766
- */
767
-
768
- Router.prototype.matchHost = function (input) {
769
- const { host } = this;
770
-
771
- if (!host) {
772
- return true;
773
- }
774
-
775
- if (!input) {
776
- return false;
777
- }
778
-
779
- if (typeof host === 'string') {
780
- return input === host;
781
- }
782
-
783
- if (typeof host === 'object' && host instanceof RegExp) {
784
- return host.test(input);
785
- }
786
- };
787
-
788
- /**
789
- * Run middleware for named route parameters. Useful for auto-loading or
790
- * validation.
791
- *
792
- * @example
793
- *
794
- * ```javascript
795
- * router
796
- * .param('user', (id, ctx, next) => {
797
- * ctx.user = users[id];
798
- * if (!ctx.user) return ctx.status = 404;
799
- * return next();
800
- * })
801
- * .get('/users/:user', ctx => {
802
- * ctx.body = ctx.user;
803
- * })
804
- * .get('/users/:user/friends', ctx => {
805
- * return ctx.user.getFriends().then(function(friends) {
806
- * ctx.body = friends;
807
- * });
808
- * })
809
- * // /users/3 => {"id": 3, "name": "Alex"}
810
- * // /users/3/friends => [{"id": 4, "name": "TJ"}]
811
- * ```
812
- *
813
- * @param {String} param
814
- * @param {Function} middleware
815
- * @returns {Router}
816
- */
817
-
818
- Router.prototype.param = function (param, middleware) {
819
- this.params[param] = middleware;
820
- for (let i = 0; i < this.stack.length; i++) {
821
- const route = this.stack[i];
822
- route.param(param, middleware);
823
- }
824
-
825
- return this;
826
- };
827
-
828
- /**
829
- * Generate URL from url pattern and given `params`.
830
- *
831
- * @example
832
- *
833
- * ```javascript
834
- * const url = Router.url('/users/:id', {id: 1});
835
- * // => "/users/1"
836
- * ```
837
- *
838
- * @param {String} path url pattern
839
- * @param {Object} params url parameters
840
- * @returns {String}
841
- */
842
- Router.url = function (path) {
843
- const args = Array.prototype.slice.call(arguments, 1);
844
- return Layer.prototype.url.apply({ path }, args);
845
- };
837
+ module.exports = Router;