@koa/router 12.0.1 → 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 +2 -1
  2. package/lib/layer.js +192 -197
  3. package/lib/router.js +658 -678
  4. package/package.json +19 -17
package/lib/router.js CHANGED
@@ -4,69 +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');
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
+ );
51
147
 
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;
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
+ }
603
+
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
+ }
70
655
  }
71
656
 
72
657
  /**
@@ -207,639 +792,34 @@ function Router(opts = {}) {
207
792
  * @param {Function} callback route callback
208
793
  * @returns {Router}
209
794
  */
210
-
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 });
233
-
234
- return this;
235
- };
236
- }
237
-
238
- setMethodVerb(method_);
239
- }
240
-
241
- // Alias for `router.delete()` because delete is a reserved word
242
- // eslint-disable-next-line dot-notation
243
- Router.prototype.del = Router.prototype['delete'];
244
-
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);
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);
391
799
  } else {
392
- ctx.matched = matched.path;
800
+ middleware = Array.prototype.slice.call(arguments, 1);
801
+ path = name;
802
+ name = null;
393
803
  }
394
804
 
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;
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
+ );
624
814
 
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
- }
815
+ this.register(path, [method], middleware, { name });
630
816
 
631
817
  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
818
  };
819
+ }
742
820
 
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
- };
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'];
827
824
 
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
- };
825
+ module.exports = Router;