@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.
- package/README.md +2 -1
- package/lib/layer.js +192 -197
- package/lib/router.js +658 -678
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
800
|
+
middleware = Array.prototype.slice.call(arguments, 1);
|
|
801
|
+
path = name;
|
|
802
|
+
name = null;
|
|
393
803
|
}
|
|
394
804
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
744
|
-
|
|
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;
|