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