@koa/router 14.0.0 → 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 -43
- 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 +55 -28
- package/HISTORY.md +0 -187
- package/lib/API_tpl.hbs +0 -7
- package/lib/layer.js +0 -262
- package/lib/router.js +0 -835
package/dist/layer.mjs
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// src/layer.ts
|
|
2
|
+
import { parse as parseUrl, format as formatUrl } from "url";
|
|
3
|
+
|
|
4
|
+
// src/utils/path-to-regexp-wrapper.ts
|
|
5
|
+
import { pathToRegexp, compile, parse } from "path-to-regexp";
|
|
6
|
+
function compilePathToRegexp(path, options = {}) {
|
|
7
|
+
const normalizedOptions = { ...options };
|
|
8
|
+
if ("strict" in normalizedOptions && !("trailing" in normalizedOptions)) {
|
|
9
|
+
normalizedOptions.trailing = normalizedOptions.strict !== true;
|
|
10
|
+
delete normalizedOptions.strict;
|
|
11
|
+
}
|
|
12
|
+
delete normalizedOptions.pathAsRegExp;
|
|
13
|
+
delete normalizedOptions.ignoreCaptures;
|
|
14
|
+
delete normalizedOptions.prefix;
|
|
15
|
+
const { regexp, keys } = pathToRegexp(path, normalizedOptions);
|
|
16
|
+
return { regexp, keys };
|
|
17
|
+
}
|
|
18
|
+
function compilePath(path, options = {}) {
|
|
19
|
+
return compile(path, options);
|
|
20
|
+
}
|
|
21
|
+
function parsePath(path, options) {
|
|
22
|
+
return parse(path, options);
|
|
23
|
+
}
|
|
24
|
+
function normalizeLayerOptionsToPathToRegexp(options = {}) {
|
|
25
|
+
const normalized = {
|
|
26
|
+
sensitive: options.sensitive,
|
|
27
|
+
end: options.end,
|
|
28
|
+
strict: options.strict,
|
|
29
|
+
trailing: options.trailing
|
|
30
|
+
};
|
|
31
|
+
if ("strict" in normalized && !("trailing" in normalized)) {
|
|
32
|
+
normalized.trailing = normalized.strict !== true;
|
|
33
|
+
delete normalized.strict;
|
|
34
|
+
}
|
|
35
|
+
for (const key of Object.keys(normalized)) {
|
|
36
|
+
if (normalized[key] === void 0) {
|
|
37
|
+
delete normalized[key];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/layer.ts
|
|
44
|
+
function safeDecodeURIComponent(text) {
|
|
45
|
+
try {
|
|
46
|
+
return decodeURIComponent(text);
|
|
47
|
+
} catch {
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
var Layer = class {
|
|
52
|
+
opts;
|
|
53
|
+
name;
|
|
54
|
+
methods;
|
|
55
|
+
paramNames;
|
|
56
|
+
stack;
|
|
57
|
+
path;
|
|
58
|
+
regexp;
|
|
59
|
+
/**
|
|
60
|
+
* Initialize a new routing Layer with given `method`, `path`, and `middleware`.
|
|
61
|
+
*
|
|
62
|
+
* @param path - Path string or regular expression
|
|
63
|
+
* @param methods - Array of HTTP verbs
|
|
64
|
+
* @param middleware - Layer callback/middleware or series of
|
|
65
|
+
* @param opts - Layer options
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
constructor(path, methods, middleware, options = {}) {
|
|
69
|
+
this.opts = options;
|
|
70
|
+
this.name = this.opts.name || void 0;
|
|
71
|
+
this.methods = this._normalizeHttpMethods(methods);
|
|
72
|
+
this.stack = this._normalizeAndValidateMiddleware(
|
|
73
|
+
middleware,
|
|
74
|
+
methods,
|
|
75
|
+
path
|
|
76
|
+
);
|
|
77
|
+
this.path = path;
|
|
78
|
+
this.paramNames = [];
|
|
79
|
+
this._configurePathMatching();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Normalize HTTP methods and add automatic HEAD support for GET
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
_normalizeHttpMethods(methods) {
|
|
86
|
+
const normalizedMethods = [];
|
|
87
|
+
for (const method of methods) {
|
|
88
|
+
const upperMethod = method.toUpperCase();
|
|
89
|
+
normalizedMethods.push(upperMethod);
|
|
90
|
+
if (upperMethod === "GET") {
|
|
91
|
+
normalizedMethods.unshift("HEAD");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return normalizedMethods;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Normalize middleware to array and validate all are functions
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
_normalizeAndValidateMiddleware(middleware, methods, path) {
|
|
101
|
+
const middlewareArray = Array.isArray(middleware) ? middleware : [middleware];
|
|
102
|
+
for (const middlewareFunction of middlewareArray) {
|
|
103
|
+
const middlewareType = typeof middlewareFunction;
|
|
104
|
+
if (middlewareType !== "function") {
|
|
105
|
+
const routeIdentifier = this.opts.name || path;
|
|
106
|
+
throw new Error(
|
|
107
|
+
`${methods.toString()} \`${routeIdentifier}\`: \`middleware\` must be a function, not \`${middlewareType}\``
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return middlewareArray;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Configure path matching regexp and parameters
|
|
115
|
+
* @private
|
|
116
|
+
*/
|
|
117
|
+
_configurePathMatching() {
|
|
118
|
+
if (this.opts.pathAsRegExp === true) {
|
|
119
|
+
this.regexp = this.path instanceof RegExp ? this.path : new RegExp(this.path);
|
|
120
|
+
} else if (this.path) {
|
|
121
|
+
this._configurePathToRegexp();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Configure path-to-regexp for string paths
|
|
126
|
+
* @private
|
|
127
|
+
*/
|
|
128
|
+
_configurePathToRegexp() {
|
|
129
|
+
const options = normalizeLayerOptionsToPathToRegexp(this.opts);
|
|
130
|
+
const { regexp, keys } = compilePathToRegexp(this.path, options);
|
|
131
|
+
this.regexp = regexp;
|
|
132
|
+
this.paramNames = keys;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Returns whether request `path` matches route.
|
|
136
|
+
*
|
|
137
|
+
* @param path - Request path
|
|
138
|
+
* @returns Whether path matches
|
|
139
|
+
* @private
|
|
140
|
+
*/
|
|
141
|
+
match(path) {
|
|
142
|
+
return this.regexp.test(path);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Returns map of URL parameters for given `path` and `paramNames`.
|
|
146
|
+
*
|
|
147
|
+
* @param _path - Request path (not used, kept for API compatibility)
|
|
148
|
+
* @param captures - Captured values from regexp
|
|
149
|
+
* @param existingParams - Existing params to merge with
|
|
150
|
+
* @returns Parameter map
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
params(_path, captures, existingParameters = {}) {
|
|
154
|
+
const parameterValues = { ...existingParameters };
|
|
155
|
+
for (const [captureIndex, capturedValue] of captures.entries()) {
|
|
156
|
+
const parameterDefinition = this.paramNames[captureIndex];
|
|
157
|
+
if (parameterDefinition && capturedValue && capturedValue.length > 0) {
|
|
158
|
+
const parameterName = parameterDefinition.name;
|
|
159
|
+
parameterValues[parameterName] = safeDecodeURIComponent(capturedValue);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return parameterValues;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Returns array of regexp url path captures.
|
|
166
|
+
*
|
|
167
|
+
* @param path - Request path
|
|
168
|
+
* @returns Array of captured values
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
captures(path) {
|
|
172
|
+
if (this.opts.ignoreCaptures) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
const match = path.match(this.regexp);
|
|
176
|
+
return match ? match.slice(1) : [];
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Generate URL for route using given `params`.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
*
|
|
183
|
+
* ```javascript
|
|
184
|
+
* const route = new Layer('/users/:id', ['GET'], fn);
|
|
185
|
+
*
|
|
186
|
+
* route.url({ id: 123 }); // => "/users/123"
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* @param args - URL parameters (various formats supported)
|
|
190
|
+
* @returns Generated URL
|
|
191
|
+
* @private
|
|
192
|
+
*/
|
|
193
|
+
url(...arguments_) {
|
|
194
|
+
const { params, options } = this._parseUrlArguments(arguments_);
|
|
195
|
+
const cleanPath = this.path.replaceAll("(.*)", "");
|
|
196
|
+
const pathCompiler = compilePath(cleanPath, {
|
|
197
|
+
encode: encodeURIComponent,
|
|
198
|
+
...options
|
|
199
|
+
});
|
|
200
|
+
const parameterReplacements = this._buildParamReplacements(
|
|
201
|
+
params,
|
|
202
|
+
cleanPath
|
|
203
|
+
);
|
|
204
|
+
const generatedUrl = pathCompiler(parameterReplacements);
|
|
205
|
+
if (options && options.query) {
|
|
206
|
+
return this._addQueryString(generatedUrl, options.query);
|
|
207
|
+
}
|
|
208
|
+
return generatedUrl;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Parse url() arguments into params and options
|
|
212
|
+
* Supports multiple call signatures:
|
|
213
|
+
* - url({ id: 1 })
|
|
214
|
+
* - url(1, 2, 3)
|
|
215
|
+
* - url({ query: {...} })
|
|
216
|
+
* - url({ id: 1 }, { query: {...} })
|
|
217
|
+
* @private
|
|
218
|
+
*/
|
|
219
|
+
_parseUrlArguments(allArguments) {
|
|
220
|
+
let parameters = allArguments[0];
|
|
221
|
+
let options = allArguments[1];
|
|
222
|
+
if (typeof parameters !== "object") {
|
|
223
|
+
const argumentsList = [...allArguments];
|
|
224
|
+
const lastArgument = argumentsList.at(-1);
|
|
225
|
+
if (typeof lastArgument === "object") {
|
|
226
|
+
options = lastArgument;
|
|
227
|
+
parameters = argumentsList.slice(0, -1);
|
|
228
|
+
} else {
|
|
229
|
+
parameters = argumentsList;
|
|
230
|
+
}
|
|
231
|
+
} else if (parameters && parameters.query && !options) {
|
|
232
|
+
options = parameters;
|
|
233
|
+
parameters = {};
|
|
234
|
+
}
|
|
235
|
+
return { params: parameters, options };
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Build parameter replacements for URL generation
|
|
239
|
+
* @private
|
|
240
|
+
*/
|
|
241
|
+
_buildParamReplacements(parameters, cleanPath) {
|
|
242
|
+
const { tokens } = parsePath(cleanPath);
|
|
243
|
+
const hasNamedParameters = tokens.some(
|
|
244
|
+
(token) => "name" in token && token.name
|
|
245
|
+
);
|
|
246
|
+
const parameterReplacements = {};
|
|
247
|
+
if (Array.isArray(parameters)) {
|
|
248
|
+
let parameterIndex = 0;
|
|
249
|
+
for (const token of tokens) {
|
|
250
|
+
if ("name" in token && token.name) {
|
|
251
|
+
parameterReplacements[token.name] = String(
|
|
252
|
+
parameters[parameterIndex++]
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} else if (hasNamedParameters && typeof parameters === "object" && !parameters.query) {
|
|
257
|
+
for (const [parameterName, parameterValue] of Object.entries(
|
|
258
|
+
parameters
|
|
259
|
+
)) {
|
|
260
|
+
parameterReplacements[parameterName] = String(parameterValue);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return parameterReplacements;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Add query string to URL
|
|
267
|
+
* @private
|
|
268
|
+
*/
|
|
269
|
+
_addQueryString(baseUrl, query) {
|
|
270
|
+
const parsedUrl = parseUrl(baseUrl);
|
|
271
|
+
if (typeof query === "string") {
|
|
272
|
+
parsedUrl.search = query;
|
|
273
|
+
} else {
|
|
274
|
+
parsedUrl.search = void 0;
|
|
275
|
+
parsedUrl.query = query;
|
|
276
|
+
}
|
|
277
|
+
return formatUrl(parsedUrl);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Run validations on route named parameters.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
*
|
|
284
|
+
* ```javascript
|
|
285
|
+
* router
|
|
286
|
+
* .param('user', function (id, ctx, next) {
|
|
287
|
+
* ctx.user = users[id];
|
|
288
|
+
* if (!ctx.user) return ctx.status = 404;
|
|
289
|
+
* next();
|
|
290
|
+
* })
|
|
291
|
+
* .get('/users/:user', function (ctx, next) {
|
|
292
|
+
* ctx.body = ctx.user;
|
|
293
|
+
* });
|
|
294
|
+
* ```
|
|
295
|
+
*
|
|
296
|
+
* @param paramName - Parameter name
|
|
297
|
+
* @param paramHandler - Middleware function
|
|
298
|
+
* @returns This layer instance
|
|
299
|
+
* @private
|
|
300
|
+
*/
|
|
301
|
+
param(parameterName, parameterHandler) {
|
|
302
|
+
const middlewareStack = this.stack;
|
|
303
|
+
const routeParameterNames = this.paramNames;
|
|
304
|
+
const parameterMiddleware = this._createParamMiddleware(
|
|
305
|
+
parameterName,
|
|
306
|
+
parameterHandler
|
|
307
|
+
);
|
|
308
|
+
const parameterNamesList = routeParameterNames.map(
|
|
309
|
+
(parameterDefinition) => parameterDefinition.name
|
|
310
|
+
);
|
|
311
|
+
const parameterPosition = parameterNamesList.indexOf(parameterName);
|
|
312
|
+
if (parameterPosition !== -1) {
|
|
313
|
+
this._insertParamMiddleware(
|
|
314
|
+
middlewareStack,
|
|
315
|
+
parameterMiddleware,
|
|
316
|
+
parameterNamesList,
|
|
317
|
+
parameterPosition
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return this;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Create param middleware with deduplication tracking
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
_createParamMiddleware(parameterName, parameterHandler) {
|
|
327
|
+
const middleware = function(context, next) {
|
|
328
|
+
if (!context._matchedParams) {
|
|
329
|
+
context._matchedParams = /* @__PURE__ */ new WeakMap();
|
|
330
|
+
}
|
|
331
|
+
if (context._matchedParams.has(parameterHandler)) {
|
|
332
|
+
return next();
|
|
333
|
+
}
|
|
334
|
+
context._matchedParams.set(parameterHandler, true);
|
|
335
|
+
return parameterHandler.call(
|
|
336
|
+
this,
|
|
337
|
+
context.params[parameterName],
|
|
338
|
+
context,
|
|
339
|
+
next
|
|
340
|
+
);
|
|
341
|
+
};
|
|
342
|
+
middleware.param = parameterName;
|
|
343
|
+
middleware._originalFn = parameterHandler;
|
|
344
|
+
return middleware;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Insert param middleware at the correct position in the stack
|
|
348
|
+
* @private
|
|
349
|
+
*/
|
|
350
|
+
_insertParamMiddleware(middlewareStack, parameterMiddleware, parameterNamesList, currentParameterPosition) {
|
|
351
|
+
middlewareStack.some((existingMiddleware, stackIndex) => {
|
|
352
|
+
if (!existingMiddleware.param) {
|
|
353
|
+
middlewareStack.splice(stackIndex, 0, parameterMiddleware);
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
const existingParameterPosition = parameterNamesList.indexOf(
|
|
357
|
+
existingMiddleware.param
|
|
358
|
+
);
|
|
359
|
+
if (existingParameterPosition > currentParameterPosition) {
|
|
360
|
+
middlewareStack.splice(stackIndex, 0, parameterMiddleware);
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Prefix route path.
|
|
368
|
+
*
|
|
369
|
+
* @param prefixPath - Prefix to prepend
|
|
370
|
+
* @returns This layer instance
|
|
371
|
+
* @private
|
|
372
|
+
*/
|
|
373
|
+
setPrefix(prefixPath) {
|
|
374
|
+
if (!this.path) {
|
|
375
|
+
return this;
|
|
376
|
+
}
|
|
377
|
+
if (this.path instanceof RegExp) {
|
|
378
|
+
return this;
|
|
379
|
+
}
|
|
380
|
+
this.path = this._applyPrefix(prefixPath);
|
|
381
|
+
this._reconfigurePathMatching(prefixPath);
|
|
382
|
+
return this;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Apply prefix to the current path
|
|
386
|
+
* @private
|
|
387
|
+
*/
|
|
388
|
+
_applyPrefix(prefixPath) {
|
|
389
|
+
const isRootPath = this.path === "/";
|
|
390
|
+
const isStrictMode = this.opts.strict === true;
|
|
391
|
+
const prefixHasParameters = prefixPath.includes(":");
|
|
392
|
+
const pathIsRawRegex = this.opts.pathAsRegExp === true && typeof this.path === "string";
|
|
393
|
+
if (prefixHasParameters && pathIsRawRegex) {
|
|
394
|
+
const currentPath = this.path;
|
|
395
|
+
if (currentPath === String.raw`(?:\/|$)` || currentPath === String.raw`(?:\/|$)`) {
|
|
396
|
+
this.path = "{/*rest}";
|
|
397
|
+
this.opts.pathAsRegExp = false;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (isRootPath && !isStrictMode) {
|
|
401
|
+
return prefixPath;
|
|
402
|
+
}
|
|
403
|
+
return `${prefixPath}${this.path}`;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Reconfigure path matching after prefix is applied
|
|
407
|
+
* @private
|
|
408
|
+
*/
|
|
409
|
+
_reconfigurePathMatching(prefixPath) {
|
|
410
|
+
const treatAsRegExp = this.opts.pathAsRegExp === true;
|
|
411
|
+
const prefixHasParameters = prefixPath && prefixPath.includes(":");
|
|
412
|
+
if (prefixHasParameters && treatAsRegExp) {
|
|
413
|
+
const options = normalizeLayerOptionsToPathToRegexp(this.opts);
|
|
414
|
+
const { regexp, keys } = compilePathToRegexp(
|
|
415
|
+
this.path,
|
|
416
|
+
options
|
|
417
|
+
);
|
|
418
|
+
this.regexp = regexp;
|
|
419
|
+
this.paramNames = keys;
|
|
420
|
+
this.opts.pathAsRegExp = false;
|
|
421
|
+
} else if (treatAsRegExp) {
|
|
422
|
+
this.regexp = this.path instanceof RegExp ? this.path : new RegExp(this.path);
|
|
423
|
+
} else {
|
|
424
|
+
const options = normalizeLayerOptionsToPathToRegexp(this.opts);
|
|
425
|
+
const { regexp, keys } = compilePathToRegexp(
|
|
426
|
+
this.path,
|
|
427
|
+
options
|
|
428
|
+
);
|
|
429
|
+
this.regexp = regexp;
|
|
430
|
+
this.paramNames = keys;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
export {
|
|
435
|
+
Layer as default
|
|
436
|
+
};
|
package/dist/router.d.ts
ADDED