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