koa-router
Version:
Router middleware for koa. Maintained by Forward Email and Lad.
263 lines (233 loc) • 7.06 kB
JavaScript
const { parse: parseUrl, format: formatUrl } = require('node:url');
const { pathToRegexp, compile, parse, stringify } = require('path-to-regexp');
module.exports = class Layer {
/**
* Initialize a new routing Layer with given `method`, `path`, and `middleware`.
*
* @param {String|RegExp} path Path string or regular expression.
* @param {Array} methods Array of HTTP verbs.
* @param {Array} middleware Layer callback/middleware or series of.
* @param {Object=} opts
* @param {String=} opts.name route name
* @param {String=} opts.sensitive case sensitive (default: false)
* @param {String=} opts.strict require the trailing slash (default: false)
* @param {Boolean=} opts.pathIsRegexp if true, treat `path` as a regular expression
* @returns {Layer}
* @private
*/
constructor(path, methods, middleware, opts = {}) {
this.opts = opts;
this.name = this.opts.name || null;
this.methods = [];
this.paramNames = [];
this.stack = Array.isArray(middleware) ? middleware : [middleware];
for (const method of methods) {
const l = this.methods.push(method.toUpperCase());
if (this.methods[l - 1] === 'GET') this.methods.unshift('HEAD');
}
// ensure middleware is a function
for (let i = 0; i < this.stack.length; i++) {
const fn = this.stack[i];
const type = typeof fn;
if (type !== 'function')
throw new Error(
`${methods.toString()} \`${
this.opts.name || path
}\`: \`middleware\` must be a function, not \`${type}\``
);
}
this.path = path;
if (this.opts.pathIsRegexp === true) {
this.regexp = new RegExp(path);
} else if (this.path) {
if (this.opts.strict === true) {
// path-to-regexp renamed strict to trailing in v8.1.0
this.opts.trailing = false;
}
const { regexp: regex, keys } = pathToRegexp(this.path, this.opts);
this.regexp = regex;
this.paramNames = keys;
}
}
/**
* Returns whether request `path` matches route.
*
* @param {String} path
* @returns {Boolean}
* @private
*/
match(path) {
return this.regexp.test(path);
}
/**
* Returns map of URL parameters for given `path` and `paramNames`.
*
* @param {String} path
* @param {Array.<String>} captures
* @param {Object=} params
* @returns {Object}
* @private
*/
params(path, captures, params = {}) {
for (let len = captures.length, i = 0; i < len; i++) {
if (this.paramNames[i]) {
const c = captures[i];
if (c && c.length > 0)
params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c;
}
}
return params;
}
/**
* Returns array of regexp url path captures.
*
* @param {String} path
* @returns {Array.<String>}
* @private
*/
captures(path) {
return this.opts.ignoreCaptures ? [] : path.match(this.regexp).slice(1);
}
/**
* Generate URL for route using given `params`.
*
* @example
*
* ```javascript
* const route = new Layer('/users/:id', ['GET'], fn);
*
* route.url({ id: 123 }); // => "/users/123"
* ```
*
* @param {Object} params url parameters
* @returns {String}
* @private
*/
url(params, options) {
let args = params;
const url = this.path.replace(/\(\.\*\)/g, '');
if (typeof params !== 'object') {
args = Array.prototype.slice.call(arguments);
if (typeof args[args.length - 1] === 'object') {
options = args[args.length - 1];
args = args.slice(0, -1);
}
}
const toPath = compile(url, { encode: encodeURIComponent, ...options });
let replaced;
const { tokens } = parse(url);
let replace = {};
if (Array.isArray(args)) {
for (let len = tokens.length, i = 0, j = 0; i < len; i++) {
if (tokens[i].name) {
replace[tokens[i].name] = args[j++];
}
}
} else if (tokens.some((token) => token.name)) {
replace = params;
} else if (!options) {
options = params;
}
for (const [key, value] of Object.entries(replace)) {
replace[key] = String(value);
}
replaced = toPath(replace);
if (options && options.query) {
replaced = parseUrl(replaced);
if (typeof options.query === 'string') {
replaced.search = options.query;
} else {
replaced.search = undefined;
replaced.query = options.query;
}
return formatUrl(replaced);
}
return replaced;
}
/**
* Run validations on route named parameters.
*
* @example
*
* ```javascript
* router
* .param('user', function (id, ctx, next) {
* ctx.user = users[id];
* if (!ctx.user) return ctx.status = 404;
* next();
* })
* .get('/users/:user', function (ctx, next) {
* ctx.body = ctx.user;
* });
* ```
*
* @param {String} param
* @param {Function} middleware
* @returns {Layer}
* @private
*/
param(param, fn) {
const { stack } = this;
const params = this.paramNames;
const middleware = function (ctx, next) {
return fn.call(this, ctx.params[param], ctx, next);
};
middleware.param = param;
const names = params.map(function (p) {
return p.name;
});
const x = names.indexOf(param);
if (x > -1) {
// iterate through the stack, to figure out where to place the handler fn
stack.some((fn, i) => {
// param handlers are always first, so when we find an fn w/o a param property, stop here
// if the param handler at this part of the stack comes after the one we are adding, stop here
if (!fn.param || names.indexOf(fn.param) > x) {
// inject this param handler right before the current item
stack.splice(i, 0, middleware);
return true; // then break the loop
}
});
}
return this;
}
/**
* Prefix route path.
*
* @param {String} prefix
* @returns {Layer}
* @private
*/
setPrefix(prefix) {
if (this.path) {
this.path =
this.path !== '/' || this.opts.strict === true
? `${prefix}${this.path}`
: prefix;
if (this.opts.pathIsRegexp === true || prefix instanceof RegExp) {
this.regexp = new RegExp(this.path);
} else if (this.path) {
const { regexp: regex, keys } = pathToRegexp(this.path, this.opts);
this.regexp = regex;
this.paramNames = keys;
}
}
return this;
}
};
/**
* Safe decodeURIComponent, won't throw any error.
* If `decodeURIComponent` error happen, just return the original value.
*
* @param {String} text
* @returns {String} URL decode original string.
* @private
*/
function safeDecodeURIComponent(text) {
try {
// @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url
return decodeURIComponent(text.replace(/\+/g, ' '));
} catch {
return text;
}
}