koa-router
Advanced tools
Comparing version 12.0.1 to 13.0.1
404
lib/layer.js
@@ -1,227 +0,244 @@ | ||
const { parse: parseUrl, format: formatUrl } = require('url'); | ||
const { pathToRegexp, compile, parse } = require('path-to-regexp'); | ||
const { parse: parseUrl, format: formatUrl } = require('node:url'); | ||
module.exports = Layer; | ||
const { pathToRegexp, compile, parse, stringify } = require('path-to-regexp'); | ||
/** | ||
* 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) | ||
* @returns {Layer} | ||
* @private | ||
*/ | ||
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]; | ||
function Layer(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'); | ||
} | ||
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}\`` | ||
); | ||
} | ||
// 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; | ||
this.path = path; | ||
this.regexp = pathToRegexp(path, this.paramNames, this.opts); | ||
} | ||
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; | ||
} | ||
/** | ||
* Returns whether request `path` matches route. | ||
* | ||
* @param {String} path | ||
* @returns {Boolean} | ||
* @private | ||
*/ | ||
Layer.prototype.match = function (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 | ||
*/ | ||
Layer.prototype.params = function (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; | ||
const { regexp: regex, keys } = pathToRegexp(this.path, this.opts); | ||
this.regexp = regex; | ||
this.paramNames = keys; | ||
} | ||
} | ||
return params; | ||
}; | ||
/** | ||
* Returns whether request `path` matches route. | ||
* | ||
* @param {String} path | ||
* @returns {Boolean} | ||
* @private | ||
*/ | ||
match(path) { | ||
return this.regexp.test(path); | ||
} | ||
/** | ||
* Returns array of regexp url path captures. | ||
* | ||
* @param {String} path | ||
* @returns {Array.<String>} | ||
* @private | ||
*/ | ||
/** | ||
* 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; | ||
} | ||
} | ||
Layer.prototype.captures = function (path) { | ||
return this.opts.ignoreCaptures ? [] : path.match(this.regexp).slice(1); | ||
}; | ||
return params; | ||
} | ||
/** | ||
* 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 | ||
*/ | ||
/** | ||
* 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); | ||
} | ||
Layer.prototype.url = function (params, options) { | ||
let args = params; | ||
const url = this.path.replace(/\(\.\*\)/g, ''); | ||
/** | ||
* 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); | ||
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 toPath = compile(url, { encode: encodeURIComponent, ...options }); | ||
let replaced; | ||
const { tokens } = parse(url); | ||
let replace = {}; | ||
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; | ||
} | ||
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++]; | ||
for (const [key, value] of Object.entries(replace)) { | ||
replace[key] = String(value); | ||
} | ||
} else if (tokens.some((token) => token.name)) { | ||
replace = params; | ||
} else if (!options) { | ||
options = params; | ||
} | ||
replaced = toPath(replace); | ||
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; | ||
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 formatUrl(replaced); | ||
return 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); | ||
}; | ||
/** | ||
* 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 | ||
*/ | ||
middleware.param = param; | ||
Layer.prototype.param = function (param, fn) { | ||
const { stack } = this; | ||
const params = this.paramNames; | ||
const middleware = function (ctx, next) { | ||
return fn.call(this, ctx.params[param], ctx, next); | ||
}; | ||
const names = params.map(function (p) { | ||
return p.name; | ||
}); | ||
middleware.param = param; | ||
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 | ||
} | ||
}); | ||
} | ||
const names = params.map(function (p) { | ||
return p.name; | ||
}); | ||
return this; | ||
} | ||
const x = names.indexOf(param); | ||
if (x > -1) { | ||
// iterate through the stack, to figure out where to place the handler fn | ||
stack.some(function (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 | ||
/** | ||
* 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; | ||
}; | ||
/** | ||
* Prefix route path. | ||
* | ||
* @param {String} prefix | ||
* @returns {Layer} | ||
* @private | ||
*/ | ||
Layer.prototype.setPrefix = function (prefix) { | ||
if (this.path) { | ||
this.path = | ||
this.path !== '/' || this.opts.strict === true | ||
? `${prefix}${this.path}` | ||
: prefix; | ||
this.paramNames = []; | ||
this.regexp = pathToRegexp(this.path, this.paramNames, this.opts); | ||
return this; | ||
} | ||
return this; | ||
}; | ||
@@ -240,3 +257,4 @@ | ||
try { | ||
return decodeURIComponent(text); | ||
// @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 { | ||
@@ -243,0 +261,0 @@ return text; |
1342
lib/router.js
@@ -7,65 +7,657 @@ /** | ||
*/ | ||
const http = require('node:http'); | ||
const util = require('node:util'); | ||
const debug = util.debuglog('koa-router'); | ||
const compose = require('koa-compose'); | ||
const HttpError = require('http-errors'); | ||
const methods = require('methods'); | ||
const { pathToRegexp } = require('path-to-regexp'); | ||
const Layer = require('./layer'); | ||
const debug = require('debug')('koa-router'); | ||
const methods = http.METHODS.map((method) => method.toLowerCase()); | ||
/** | ||
* @module koa-router | ||
*/ | ||
class Router { | ||
/** | ||
* Create a new router. | ||
* | ||
* @example | ||
* | ||
* Basic usage: | ||
* | ||
* ```javascript | ||
* const Koa = require('koa'); | ||
* const Router = require('@koa/router'); | ||
* | ||
* const app = new Koa(); | ||
* const router = new Router(); | ||
* | ||
* router.get('/', (ctx, next) => { | ||
* // ctx.router available | ||
* }); | ||
* | ||
* app | ||
* .use(router.routes()) | ||
* .use(router.allowedMethods()); | ||
* ``` | ||
* | ||
* @alias module:koa-router | ||
* @param {Object=} opts | ||
* @param {Boolean=false} opts.exclusive only run last matched route's controller when there are multiple matches | ||
* @param {String=} opts.prefix prefix router paths | ||
* @param {String|RegExp=} opts.host host for router match | ||
* @constructor | ||
*/ | ||
constructor(opts = {}) { | ||
if (!(this instanceof Router)) return new Router(opts); // eslint-disable-line no-constructor-return | ||
module.exports = Router; | ||
this.opts = opts; | ||
this.methods = this.opts.methods || [ | ||
'HEAD', | ||
'OPTIONS', | ||
'GET', | ||
'PUT', | ||
'PATCH', | ||
'POST', | ||
'DELETE' | ||
]; | ||
this.exclusive = Boolean(this.opts.exclusive); | ||
/** | ||
* Create a new router. | ||
* | ||
* @example | ||
* | ||
* Basic usage: | ||
* | ||
* ```javascript | ||
* const Koa = require('koa'); | ||
* const Router = require('@koa/router'); | ||
* | ||
* const app = new Koa(); | ||
* const router = new Router(); | ||
* | ||
* router.get('/', (ctx, next) => { | ||
* // ctx.router available | ||
* }); | ||
* | ||
* app | ||
* .use(router.routes()) | ||
* .use(router.allowedMethods()); | ||
* ``` | ||
* | ||
* @alias module:koa-router | ||
* @param {Object=} opts | ||
* @param {Boolean=false} opts.exclusive only run last matched route's controller when there are multiple matches | ||
* @param {String=} opts.prefix prefix router paths | ||
* @param {String|RegExp=} opts.host host for router match | ||
* @constructor | ||
*/ | ||
this.params = {}; | ||
this.stack = []; | ||
this.host = this.opts.host; | ||
} | ||
function Router(opts = {}) { | ||
if (!(this instanceof Router)) return new Router(opts); | ||
/** | ||
* Generate URL from url pattern and given `params`. | ||
* | ||
* @example | ||
* | ||
* ```javascript | ||
* const url = Router.url('/users/:id', {id: 1}); | ||
* // => "/users/1" | ||
* ``` | ||
* | ||
* @param {String} path url pattern | ||
* @param {Object} params url parameters | ||
* @returns {String} | ||
*/ | ||
static url(path, ...args) { | ||
return Layer.prototype.url.apply({ path }, args); | ||
} | ||
this.opts = opts; | ||
this.methods = this.opts.methods || [ | ||
'HEAD', | ||
'OPTIONS', | ||
'GET', | ||
'PUT', | ||
'PATCH', | ||
'POST', | ||
'DELETE' | ||
]; | ||
this.exclusive = Boolean(this.opts.exclusive); | ||
/** | ||
* Use given middleware. | ||
* | ||
* Middleware run in the order they are defined by `.use()`. They are invoked | ||
* sequentially, requests start at the first middleware and work their way | ||
* "down" the middleware stack. | ||
* | ||
* @example | ||
* | ||
* ```javascript | ||
* // session middleware will run before authorize | ||
* router | ||
* .use(session()) | ||
* .use(authorize()); | ||
* | ||
* // use middleware only with given path | ||
* router.use('/users', userAuth()); | ||
* | ||
* // or with an array of paths | ||
* router.use(['/users', '/admin'], userAuth()); | ||
* | ||
* app.use(router.routes()); | ||
* ``` | ||
* | ||
* @param {String=} path | ||
* @param {Function} middleware | ||
* @param {Function=} ... | ||
* @returns {Router} | ||
*/ | ||
use(...middleware) { | ||
const router = this; | ||
let path; | ||
this.params = {}; | ||
this.stack = []; | ||
this.host = this.opts.host; | ||
// support array of paths | ||
if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') { | ||
const arrPaths = middleware[0]; | ||
for (const p of arrPaths) { | ||
router.use.apply(router, [p, ...middleware.slice(1)]); | ||
} | ||
return this; | ||
} | ||
const hasPath = typeof middleware[0] === 'string'; | ||
if (hasPath) path = middleware.shift(); | ||
for (const m of middleware) { | ||
if (m.router) { | ||
const cloneRouter = Object.assign( | ||
Object.create(Router.prototype), | ||
m.router, | ||
{ | ||
stack: [...m.router.stack] | ||
} | ||
); | ||
for (let j = 0; j < cloneRouter.stack.length; j++) { | ||
const nestedLayer = cloneRouter.stack[j]; | ||
const cloneLayer = Object.assign( | ||
Object.create(Layer.prototype), | ||
nestedLayer | ||
); | ||
if (path) cloneLayer.setPrefix(path); | ||
if (router.opts.prefix) cloneLayer.setPrefix(router.opts.prefix); | ||
router.stack.push(cloneLayer); | ||
cloneRouter.stack[j] = cloneLayer; | ||
} | ||
if (router.params) { | ||
const routerParams = Object.keys(router.params); | ||
for (const key of routerParams) { | ||
cloneRouter.param(key, router.params[key]); | ||
} | ||
} | ||
} else { | ||
const { keys } = pathToRegexp(router.opts.prefix || '', router.opts); | ||
const routerPrefixHasParam = Boolean( | ||
router.opts.prefix && keys.length > 0 | ||
); | ||
router.register(path || '([^/]*)', [], m, { | ||
end: false, | ||
ignoreCaptures: !hasPath && !routerPrefixHasParam, | ||
pathIsRegexp: true | ||
}); | ||
} | ||
} | ||
return this; | ||
} | ||
/** | ||
* Set the path prefix for a Router instance that was already initialized. | ||
* | ||
* @example | ||
* | ||
* ```javascript | ||
* router.prefix('/things/:thing_id') | ||
* ``` | ||
* | ||
* @param {String} prefix | ||
* @returns {Router} | ||
*/ | ||
prefix(prefix) { | ||
prefix = prefix.replace(/\/$/, ''); | ||
this.opts.prefix = prefix; | ||
for (let i = 0; i < this.stack.length; i++) { | ||
const route = this.stack[i]; | ||
route.setPrefix(prefix); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Returns router middleware which dispatches a route matching the request. | ||
* | ||
* @returns {Function} | ||
*/ | ||
middleware() { | ||
const router = this; | ||
const dispatch = (ctx, next) => { | ||
debug('%s %s', ctx.method, ctx.path); | ||
const hostMatched = router.matchHost(ctx.host); | ||
if (!hostMatched) { | ||
return next(); | ||
} | ||
const path = | ||
router.opts.routerPath || | ||
ctx.newRouterPath || | ||
ctx.path || | ||
ctx.routerPath; | ||
const matched = router.match(path, ctx.method); | ||
if (ctx.matched) { | ||
ctx.matched.push.apply(ctx.matched, matched.path); | ||
} else { | ||
ctx.matched = matched.path; | ||
} | ||
ctx.router = router; | ||
if (!matched.route) return next(); | ||
const matchedLayers = matched.pathAndMethod; | ||
const mostSpecificLayer = matchedLayers[matchedLayers.length - 1]; | ||
ctx._matchedRoute = mostSpecificLayer.path; | ||
if (mostSpecificLayer.name) { | ||
ctx._matchedRouteName = mostSpecificLayer.name; | ||
} | ||
const layerChain = ( | ||
router.exclusive ? [mostSpecificLayer] : matchedLayers | ||
).reduce((memo, layer) => { | ||
memo.push((ctx, next) => { | ||
ctx.captures = layer.captures(path, ctx.captures); | ||
ctx.request.params = layer.params(path, ctx.captures, ctx.params); | ||
ctx.params = ctx.request.params; | ||
ctx.routerPath = layer.path; | ||
ctx.routerName = layer.name; | ||
ctx._matchedRoute = layer.path; | ||
if (layer.name) { | ||
ctx._matchedRouteName = layer.name; | ||
} | ||
return next(); | ||
}); | ||
return [...memo, ...layer.stack]; | ||
}, []); | ||
return compose(layerChain)(ctx, next); | ||
}; | ||
dispatch.router = this; | ||
return dispatch; | ||
} | ||
routes() { | ||
return this.middleware(); | ||
} | ||
/** | ||
* Returns separate middleware for responding to `OPTIONS` requests with | ||
* an `Allow` header containing the allowed methods, as well as responding | ||
* with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. | ||
* | ||
* @example | ||
* | ||
* ```javascript | ||
* const Koa = require('koa'); | ||
* const Router = require('@koa/router'); | ||
* | ||
* const app = new Koa(); | ||
* const router = new Router(); | ||
* | ||
* app.use(router.routes()); | ||
* app.use(router.allowedMethods()); | ||
* ``` | ||
* | ||
* **Example with [Boom](https://github.com/hapijs/boom)** | ||
* | ||
* ```javascript | ||
* const Koa = require('koa'); | ||
* const Router = require('@koa/router'); | ||
* const Boom = require('boom'); | ||
* | ||
* const app = new Koa(); | ||
* const router = new Router(); | ||
* | ||
* app.use(router.routes()); | ||
* app.use(router.allowedMethods({ | ||
* throw: true, | ||
* notImplemented: () => new Boom.notImplemented(), | ||
* methodNotAllowed: () => new Boom.methodNotAllowed() | ||
* })); | ||
* ``` | ||
* | ||
* @param {Object=} options | ||
* @param {Boolean=} options.throw throw error instead of setting status and header | ||
* @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error | ||
* @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error | ||
* @returns {Function} | ||
*/ | ||
allowedMethods(options = {}) { | ||
const implemented = this.methods; | ||
return (ctx, next) => { | ||
return next().then(() => { | ||
const allowed = {}; | ||
if (ctx.matched && (!ctx.status || ctx.status === 404)) { | ||
for (let i = 0; i < ctx.matched.length; i++) { | ||
const route = ctx.matched[i]; | ||
for (let j = 0; j < route.methods.length; j++) { | ||
const method = route.methods[j]; | ||
allowed[method] = method; | ||
} | ||
} | ||
const allowedArr = Object.keys(allowed); | ||
if (!implemented.includes(ctx.method)) { | ||
if (options.throw) { | ||
const notImplementedThrowable = | ||
typeof options.notImplemented === 'function' | ||
? options.notImplemented() // set whatever the user returns from their function | ||
: new HttpError.NotImplemented(); | ||
throw notImplementedThrowable; | ||
} else { | ||
ctx.status = 501; | ||
ctx.set('Allow', allowedArr.join(', ')); | ||
} | ||
} else if (allowedArr.length > 0) { | ||
if (ctx.method === 'OPTIONS') { | ||
ctx.status = 200; | ||
ctx.body = ''; | ||
ctx.set('Allow', allowedArr.join(', ')); | ||
} else if (!allowed[ctx.method]) { | ||
if (options.throw) { | ||
const notAllowedThrowable = | ||
typeof options.methodNotAllowed === 'function' | ||
? options.methodNotAllowed() // set whatever the user returns from their function | ||
: new HttpError.MethodNotAllowed(); | ||
throw notAllowedThrowable; | ||
} else { | ||
ctx.status = 405; | ||
ctx.set('Allow', allowedArr.join(', ')); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
}; | ||
} | ||
/** | ||
* Register route with all methods. | ||
* | ||
* @param {String} name Optional. | ||
* @param {String} path | ||
* @param {Function=} middleware You may also pass multiple middleware. | ||
* @param {Function} callback | ||
* @returns {Router} | ||
*/ | ||
all(name, path, middleware) { | ||
if (typeof path === 'string' || path instanceof RegExp) { | ||
middleware = Array.prototype.slice.call(arguments, 2); | ||
} else { | ||
middleware = Array.prototype.slice.call(arguments, 1); | ||
path = name; | ||
name = null; | ||
} | ||
// Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array) | ||
if ( | ||
typeof path !== 'string' && | ||
!(path instanceof RegExp) && | ||
(!Array.isArray(path) || path.length === 0) | ||
) | ||
throw new Error('You have to provide a path when adding an all handler'); | ||
const opts = { | ||
name, | ||
pathIsRegexp: path instanceof RegExp | ||
}; | ||
this.register(path, methods, middleware, { ...this.opts, ...opts }); | ||
return this; | ||
} | ||
/** | ||
* Redirect `source` to `destination` URL with optional 30x status `code`. | ||
* | ||
* Both `source` and `destination` can be route names. | ||
* | ||
* ```javascript | ||
* router.redirect('/login', 'sign-in'); | ||
* ``` | ||
* | ||
* This is equivalent to: | ||
* | ||
* ```javascript | ||
* router.all('/login', ctx => { | ||
* ctx.redirect('/sign-in'); | ||
* ctx.status = 301; | ||
* }); | ||
* ``` | ||
* | ||
* @param {String} source URL or route name. | ||
* @param {String} destination URL or route name. | ||
* @param {Number=} code HTTP status code (default: 301). | ||
* @returns {Router} | ||
*/ | ||
redirect(source, destination, code) { | ||
// lookup source route by name | ||
if (typeof source === 'symbol' || source[0] !== '/') { | ||
source = this.url(source); | ||
if (source instanceof Error) throw source; | ||
} | ||
// lookup destination route by name | ||
if ( | ||
typeof destination === 'symbol' || | ||
(destination[0] !== '/' && !destination.includes('://')) | ||
) { | ||
destination = this.url(destination); | ||
if (destination instanceof Error) throw destination; | ||
} | ||
return this.all(source, (ctx) => { | ||
ctx.redirect(destination); | ||
ctx.status = code || 301; | ||
}); | ||
} | ||
/** | ||
* Create and register a route. | ||
* | ||
* @param {String} path Path string. | ||
* @param {Array.<String>} methods Array of HTTP verbs. | ||
* @param {Function} middleware Multiple middleware also accepted. | ||
* @returns {Layer} | ||
* @private | ||
*/ | ||
register(path, methods, middleware, newOpts = {}) { | ||
const router = this; | ||
const { stack } = this; | ||
const opts = { ...this.opts, ...newOpts }; | ||
// support array of paths | ||
if (Array.isArray(path)) { | ||
for (const curPath of path) { | ||
router.register.call(router, curPath, methods, middleware, opts); | ||
} | ||
return this; | ||
} | ||
// create route | ||
const route = new Layer(path, methods, middleware, { | ||
end: opts.end === false ? opts.end : true, | ||
name: opts.name, | ||
sensitive: opts.sensitive || false, | ||
strict: opts.strict || false, | ||
prefix: opts.prefix || '', | ||
ignoreCaptures: opts.ignoreCaptures, | ||
pathIsRegexp: opts.pathIsRegexp | ||
}); | ||
// if parent prefix exists, add prefix to new route | ||
if (this.opts.prefix) { | ||
route.setPrefix(this.opts.prefix); | ||
} | ||
// add parameter middleware | ||
for (let i = 0; i < Object.keys(this.params).length; i++) { | ||
const param = Object.keys(this.params)[i]; | ||
route.param(param, this.params[param]); | ||
} | ||
stack.push(route); | ||
debug('defined route %s %s', route.methods, route.path); | ||
return route; | ||
} | ||
/** | ||
* Lookup route with given `name`. | ||
* | ||
* @param {String} name | ||
* @returns {Layer|false} | ||
*/ | ||
route(name) { | ||
const routes = this.stack; | ||
for (let len = routes.length, i = 0; i < len; i++) { | ||
if (routes[i].name && routes[i].name === name) return routes[i]; | ||
} | ||
return false; | ||
} | ||
/** | ||
* Generate URL for route. Takes a route name and map of named `params`. | ||
* | ||
* @example | ||
* | ||
* ```javascript | ||
* router.get('user', '/users/:id', (ctx, next) => { | ||
* // ... | ||
* }); | ||
* | ||
* router.url('user', 3); | ||
* // => "/users/3" | ||
* | ||
* router.url('user', { id: 3 }); | ||
* // => "/users/3" | ||
* | ||
* router.use((ctx, next) => { | ||
* // redirect to named route | ||
* ctx.redirect(ctx.router.url('sign-in')); | ||
* }) | ||
* | ||
* router.url('user', { id: 3 }, { query: { limit: 1 } }); | ||
* // => "/users/3?limit=1" | ||
* | ||
* router.url('user', { id: 3 }, { query: "limit=1" }); | ||
* // => "/users/3?limit=1" | ||
* ``` | ||
* | ||
* @param {String} name route name | ||
* @param {Object} params url parameters | ||
* @param {Object} [options] options parameter | ||
* @param {Object|String} [options.query] query options | ||
* @returns {String|Error} | ||
*/ | ||
url(name, ...args) { | ||
const route = this.route(name); | ||
if (route) return route.url.apply(route, args); | ||
return new Error(`No route found for name: ${String(name)}`); | ||
} | ||
/** | ||
* Match given `path` and return corresponding routes. | ||
* | ||
* @param {String} path | ||
* @param {String} method | ||
* @returns {Object.<path, pathAndMethod>} returns layers that matched path and | ||
* path and method. | ||
* @private | ||
*/ | ||
match(path, method) { | ||
const layers = this.stack; | ||
let layer; | ||
const matched = { | ||
path: [], | ||
pathAndMethod: [], | ||
route: false | ||
}; | ||
for (let len = layers.length, i = 0; i < len; i++) { | ||
layer = layers[i]; | ||
debug('test %s %s', layer.path, layer.regexp); | ||
// eslint-disable-next-line unicorn/prefer-regexp-test | ||
if (layer.match(path)) { | ||
matched.path.push(layer); | ||
if (layer.methods.length === 0 || layer.methods.includes(method)) { | ||
matched.pathAndMethod.push(layer); | ||
if (layer.methods.length > 0) matched.route = true; | ||
} | ||
} | ||
} | ||
return matched; | ||
} | ||
/** | ||
* Match given `input` to allowed host | ||
* @param {String} input | ||
* @returns {boolean} | ||
*/ | ||
matchHost(input) { | ||
const { host } = this; | ||
if (!host) { | ||
return true; | ||
} | ||
if (!input) { | ||
return false; | ||
} | ||
if (typeof host === 'string') { | ||
return input === host; | ||
} | ||
if (typeof host === 'object' && host instanceof RegExp) { | ||
return host.test(input); | ||
} | ||
} | ||
/** | ||
* Run middleware for named route parameters. Useful for auto-loading or | ||
* validation. | ||
* | ||
* @example | ||
* | ||
* ```javascript | ||
* router | ||
* .param('user', (id, ctx, next) => { | ||
* ctx.user = users[id]; | ||
* if (!ctx.user) return ctx.status = 404; | ||
* return next(); | ||
* }) | ||
* .get('/users/:user', ctx => { | ||
* ctx.body = ctx.user; | ||
* }) | ||
* .get('/users/:user/friends', ctx => { | ||
* return ctx.user.getFriends().then(function(friends) { | ||
* ctx.body = friends; | ||
* }); | ||
* }) | ||
* // /users/3 => {"id": 3, "name": "Alex"} | ||
* // /users/3/friends => [{"id": 4, "name": "TJ"}] | ||
* ``` | ||
* | ||
* @param {String} param | ||
* @param {Function} middleware | ||
* @returns {Router} | ||
*/ | ||
param(param, middleware) { | ||
this.params[param] = middleware; | ||
for (let i = 0; i < this.stack.length; i++) { | ||
const route = this.stack[i]; | ||
route.param(param, middleware); | ||
} | ||
return this; | ||
} | ||
} | ||
@@ -211,31 +803,31 @@ | ||
*/ | ||
for (const method of methods) { | ||
Router.prototype[method] = function (name, path, middleware) { | ||
if (typeof path === 'string' || path instanceof RegExp) { | ||
middleware = Array.prototype.slice.call(arguments, 2); | ||
} else { | ||
middleware = Array.prototype.slice.call(arguments, 1); | ||
path = name; | ||
name = null; | ||
} | ||
for (const method_ of methods) { | ||
function setMethodVerb(method) { | ||
Router.prototype[method] = function (name, path, middleware) { | ||
if (typeof path === 'string' || path instanceof RegExp) { | ||
middleware = Array.prototype.slice.call(arguments, 2); | ||
} else { | ||
middleware = Array.prototype.slice.call(arguments, 1); | ||
path = name; | ||
name = null; | ||
} | ||
// Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array) | ||
if ( | ||
typeof path !== 'string' && | ||
!(path instanceof RegExp) && | ||
(!Array.isArray(path) || path.length === 0) | ||
) | ||
throw new Error( | ||
`You have to provide a path when adding a ${method} handler` | ||
); | ||
// Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array) | ||
if ( | ||
typeof path !== 'string' && | ||
!(path instanceof RegExp) && | ||
(!Array.isArray(path) || path.length === 0) | ||
) | ||
throw new Error( | ||
`You have to provide a path when adding a ${method} handler` | ||
); | ||
this.register(path, [method], middleware, { name }); | ||
return this; | ||
const opts = { | ||
name, | ||
pathIsRegexp: path instanceof RegExp | ||
}; | ||
} | ||
setMethodVerb(method_); | ||
// pass opts to register call on verb methods | ||
this.register(path, [method], middleware, { ...this.opts, ...opts }); | ||
return this; | ||
}; | ||
} | ||
@@ -247,602 +839,2 @@ | ||
/** | ||
* Use given middleware. | ||
* | ||
* Middleware run in the order they are defined by `.use()`. They are invoked | ||
* sequentially, requests start at the first middleware and work their way | ||
* "down" the middleware stack. | ||
* | ||
* @example | ||
* | ||
* ```javascript | ||
* // session middleware will run before authorize | ||
* router | ||
* .use(session()) | ||
* .use(authorize()); | ||
* | ||
* // use middleware only with given path | ||
* router.use('/users', userAuth()); | ||
* | ||
* // or with an array of paths | ||
* router.use(['/users', '/admin'], userAuth()); | ||
* | ||
* app.use(router.routes()); | ||
* ``` | ||
* | ||
* @param {String=} path | ||
* @param {Function} middleware | ||
* @param {Function=} ... | ||
* @returns {Router} | ||
*/ | ||
Router.prototype.use = function () { | ||
const router = this; | ||
const middleware = Array.prototype.slice.call(arguments); | ||
let path; | ||
// support array of paths | ||
if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') { | ||
const arrPaths = middleware[0]; | ||
for (const p of arrPaths) { | ||
router.use.apply(router, [p].concat(middleware.slice(1))); | ||
} | ||
return this; | ||
} | ||
const hasPath = typeof middleware[0] === 'string'; | ||
if (hasPath) path = middleware.shift(); | ||
for (const m of middleware) { | ||
if (m.router) { | ||
const cloneRouter = Object.assign( | ||
Object.create(Router.prototype), | ||
m.router, | ||
{ | ||
stack: [...m.router.stack] | ||
} | ||
); | ||
for (let j = 0; j < cloneRouter.stack.length; j++) { | ||
const nestedLayer = cloneRouter.stack[j]; | ||
const cloneLayer = Object.assign( | ||
Object.create(Layer.prototype), | ||
nestedLayer | ||
); | ||
if (path) cloneLayer.setPrefix(path); | ||
if (router.opts.prefix) cloneLayer.setPrefix(router.opts.prefix); | ||
router.stack.push(cloneLayer); | ||
cloneRouter.stack[j] = cloneLayer; | ||
} | ||
if (router.params) { | ||
function setRouterParams(paramArr) { | ||
const routerParams = paramArr; | ||
for (const key of routerParams) { | ||
cloneRouter.param(key, router.params[key]); | ||
} | ||
} | ||
setRouterParams(Object.keys(router.params)); | ||
} | ||
} else { | ||
const keys = []; | ||
pathToRegexp(router.opts.prefix || '', keys); | ||
const routerPrefixHasParam = router.opts.prefix && keys.length; | ||
router.register(path || '([^/]*)', [], m, { | ||
end: false, | ||
ignoreCaptures: !hasPath && !routerPrefixHasParam | ||
}); | ||
} | ||
} | ||
return this; | ||
}; | ||
/** | ||
* Set the path prefix for a Router instance that was already initialized. | ||
* | ||
* @example | ||
* | ||
* ```javascript | ||
* router.prefix('/things/:thing_id') | ||
* ``` | ||
* | ||
* @param {String} prefix | ||
* @returns {Router} | ||
*/ | ||
Router.prototype.prefix = function (prefix) { | ||
prefix = prefix.replace(/\/$/, ''); | ||
this.opts.prefix = prefix; | ||
for (let i = 0; i < this.stack.length; i++) { | ||
const route = this.stack[i]; | ||
route.setPrefix(prefix); | ||
} | ||
return this; | ||
}; | ||
/** | ||
* Returns router middleware which dispatches a route matching the request. | ||
* | ||
* @returns {Function} | ||
*/ | ||
Router.prototype.routes = Router.prototype.middleware = function () { | ||
const router = this; | ||
const dispatch = function dispatch(ctx, next) { | ||
debug('%s %s', ctx.method, ctx.path); | ||
const hostMatched = router.matchHost(ctx.host); | ||
if (!hostMatched) { | ||
return next(); | ||
} | ||
const path = | ||
router.opts.routerPath || ctx.newRouterPath || ctx.path || ctx.routerPath; | ||
const matched = router.match(path, ctx.method); | ||
let layerChain; | ||
if (ctx.matched) { | ||
ctx.matched.push.apply(ctx.matched, matched.path); | ||
} else { | ||
ctx.matched = matched.path; | ||
} | ||
ctx.router = router; | ||
if (!matched.route) return next(); | ||
const matchedLayers = matched.pathAndMethod; | ||
const mostSpecificLayer = matchedLayers[matchedLayers.length - 1]; | ||
ctx._matchedRoute = mostSpecificLayer.path; | ||
if (mostSpecificLayer.name) { | ||
ctx._matchedRouteName = mostSpecificLayer.name; | ||
} | ||
layerChain = ( | ||
router.exclusive ? [mostSpecificLayer] : matchedLayers | ||
).reduce(function (memo, layer) { | ||
memo.push(function (ctx, next) { | ||
ctx.captures = layer.captures(path, ctx.captures); | ||
ctx.params = ctx.request.params = layer.params( | ||
path, | ||
ctx.captures, | ||
ctx.params | ||
); | ||
ctx.routerPath = layer.path; | ||
ctx.routerName = layer.name; | ||
ctx._matchedRoute = layer.path; | ||
if (layer.name) { | ||
ctx._matchedRouteName = layer.name; | ||
} | ||
return next(); | ||
}); | ||
return memo.concat(layer.stack); | ||
}, []); | ||
return compose(layerChain)(ctx, next); | ||
}; | ||
dispatch.router = this; | ||
return dispatch; | ||
}; | ||
/** | ||
* Returns separate middleware for responding to `OPTIONS` requests with | ||
* an `Allow` header containing the allowed methods, as well as responding | ||
* with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. | ||
* | ||
* @example | ||
* | ||
* ```javascript | ||
* const Koa = require('koa'); | ||
* const Router = require('@koa/router'); | ||
* | ||
* const app = new Koa(); | ||
* const router = new Router(); | ||
* | ||
* app.use(router.routes()); | ||
* app.use(router.allowedMethods()); | ||
* ``` | ||
* | ||
* **Example with [Boom](https://github.com/hapijs/boom)** | ||
* | ||
* ```javascript | ||
* const Koa = require('koa'); | ||
* const Router = require('@koa/router'); | ||
* const Boom = require('boom'); | ||
* | ||
* const app = new Koa(); | ||
* const router = new Router(); | ||
* | ||
* app.use(router.routes()); | ||
* app.use(router.allowedMethods({ | ||
* throw: true, | ||
* notImplemented: () => new Boom.notImplemented(), | ||
* methodNotAllowed: () => new Boom.methodNotAllowed() | ||
* })); | ||
* ``` | ||
* | ||
* @param {Object=} options | ||
* @param {Boolean=} options.throw throw error instead of setting status and header | ||
* @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error | ||
* @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error | ||
* @returns {Function} | ||
*/ | ||
Router.prototype.allowedMethods = function (options = {}) { | ||
const implemented = this.methods; | ||
return function allowedMethods(ctx, next) { | ||
return next().then(function () { | ||
const allowed = {}; | ||
if (!ctx.status || ctx.status === 404) { | ||
for (let i = 0; i < ctx.matched.length; i++) { | ||
const route = ctx.matched[i]; | ||
for (let j = 0; j < route.methods.length; j++) { | ||
const method = route.methods[j]; | ||
allowed[method] = method; | ||
} | ||
} | ||
const allowedArr = Object.keys(allowed); | ||
if (!~implemented.indexOf(ctx.method)) { | ||
if (options.throw) { | ||
const notImplementedThrowable = | ||
typeof options.notImplemented === 'function' | ||
? options.notImplemented() // set whatever the user returns from their function | ||
: new HttpError.NotImplemented(); | ||
throw notImplementedThrowable; | ||
} else { | ||
ctx.status = 501; | ||
ctx.set('Allow', allowedArr.join(', ')); | ||
} | ||
} else if (allowedArr.length > 0) { | ||
if (ctx.method === 'OPTIONS') { | ||
ctx.status = 200; | ||
ctx.body = ''; | ||
ctx.set('Allow', allowedArr.join(', ')); | ||
} else if (!allowed[ctx.method]) { | ||
if (options.throw) { | ||
const notAllowedThrowable = | ||
typeof options.methodNotAllowed === 'function' | ||
? options.methodNotAllowed() // set whatever the user returns from their function | ||
: new HttpError.MethodNotAllowed(); | ||
throw notAllowedThrowable; | ||
} else { | ||
ctx.status = 405; | ||
ctx.set('Allow', allowedArr.join(', ')); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
}; | ||
}; | ||
/** | ||
* Register route with all methods. | ||
* | ||
* @param {String} name Optional. | ||
* @param {String} path | ||
* @param {Function=} middleware You may also pass multiple middleware. | ||
* @param {Function} callback | ||
* @returns {Router} | ||
*/ | ||
Router.prototype.all = function (name, path, middleware) { | ||
if (typeof path === 'string') { | ||
middleware = Array.prototype.slice.call(arguments, 2); | ||
} else { | ||
middleware = Array.prototype.slice.call(arguments, 1); | ||
path = name; | ||
name = null; | ||
} | ||
// Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array) | ||
if ( | ||
typeof path !== 'string' && | ||
!(path instanceof RegExp) && | ||
(!Array.isArray(path) || path.length === 0) | ||
) | ||
throw new Error('You have to provide a path when adding an all handler'); | ||
this.register(path, methods, middleware, { name }); | ||
return this; | ||
}; | ||
/** | ||
* Redirect `source` to `destination` URL with optional 30x status `code`. | ||
* | ||
* Both `source` and `destination` can be route names. | ||
* | ||
* ```javascript | ||
* router.redirect('/login', 'sign-in'); | ||
* ``` | ||
* | ||
* This is equivalent to: | ||
* | ||
* ```javascript | ||
* router.all('/login', ctx => { | ||
* ctx.redirect('/sign-in'); | ||
* ctx.status = 301; | ||
* }); | ||
* ``` | ||
* | ||
* @param {String} source URL or route name. | ||
* @param {String} destination URL or route name. | ||
* @param {Number=} code HTTP status code (default: 301). | ||
* @returns {Router} | ||
*/ | ||
Router.prototype.redirect = function (source, destination, code) { | ||
// lookup source route by name | ||
if (typeof source === 'symbol' || source[0] !== '/') { | ||
source = this.url(source); | ||
if (source instanceof Error) throw source; | ||
} | ||
// lookup destination route by name | ||
if ( | ||
typeof destination === 'symbol' || | ||
(destination[0] !== '/' && !destination.includes('://')) | ||
) { | ||
destination = this.url(destination); | ||
if (destination instanceof Error) throw destination; | ||
} | ||
return this.all(source, (ctx) => { | ||
ctx.redirect(destination); | ||
ctx.status = code || 301; | ||
}); | ||
}; | ||
/** | ||
* Create and register a route. | ||
* | ||
* @param {String} path Path string. | ||
* @param {Array.<String>} methods Array of HTTP verbs. | ||
* @param {Function} middleware Multiple middleware also accepted. | ||
* @returns {Layer} | ||
* @private | ||
*/ | ||
Router.prototype.register = function (path, methods, middleware, opts = {}) { | ||
const router = this; | ||
const { stack } = this; | ||
// support array of paths | ||
if (Array.isArray(path)) { | ||
for (const curPath of path) { | ||
router.register.call(router, curPath, methods, middleware, opts); | ||
} | ||
return this; | ||
} | ||
// create route | ||
const route = new Layer(path, methods, middleware, { | ||
end: opts.end === false ? opts.end : true, | ||
name: opts.name, | ||
sensitive: opts.sensitive || this.opts.sensitive || false, | ||
strict: opts.strict || this.opts.strict || false, | ||
prefix: opts.prefix || this.opts.prefix || '', | ||
ignoreCaptures: opts.ignoreCaptures | ||
}); | ||
if (this.opts.prefix) { | ||
route.setPrefix(this.opts.prefix); | ||
} | ||
// add parameter middleware | ||
for (let i = 0; i < Object.keys(this.params).length; i++) { | ||
const param = Object.keys(this.params)[i]; | ||
route.param(param, this.params[param]); | ||
} | ||
stack.push(route); | ||
debug('defined route %s %s', route.methods, route.path); | ||
return route; | ||
}; | ||
/** | ||
* Lookup route with given `name`. | ||
* | ||
* @param {String} name | ||
* @returns {Layer|false} | ||
*/ | ||
Router.prototype.route = function (name) { | ||
const routes = this.stack; | ||
for (let len = routes.length, i = 0; i < len; i++) { | ||
if (routes[i].name && routes[i].name === name) return routes[i]; | ||
} | ||
return false; | ||
}; | ||
/** | ||
* Generate URL for route. Takes a route name and map of named `params`. | ||
* | ||
* @example | ||
* | ||
* ```javascript | ||
* router.get('user', '/users/:id', (ctx, next) => { | ||
* // ... | ||
* }); | ||
* | ||
* router.url('user', 3); | ||
* // => "/users/3" | ||
* | ||
* router.url('user', { id: 3 }); | ||
* // => "/users/3" | ||
* | ||
* router.use((ctx, next) => { | ||
* // redirect to named route | ||
* ctx.redirect(ctx.router.url('sign-in')); | ||
* }) | ||
* | ||
* router.url('user', { id: 3 }, { query: { limit: 1 } }); | ||
* // => "/users/3?limit=1" | ||
* | ||
* router.url('user', { id: 3 }, { query: "limit=1" }); | ||
* // => "/users/3?limit=1" | ||
* ``` | ||
* | ||
* @param {String} name route name | ||
* @param {Object} params url parameters | ||
* @param {Object} [options] options parameter | ||
* @param {Object|String} [options.query] query options | ||
* @returns {String|Error} | ||
*/ | ||
Router.prototype.url = function (name, params) { | ||
const route = this.route(name); | ||
if (route) { | ||
const args = Array.prototype.slice.call(arguments, 1); | ||
return route.url.apply(route, args); | ||
} | ||
return new Error(`No route found for name: ${String(name)}`); | ||
}; | ||
/** | ||
* Match given `path` and return corresponding routes. | ||
* | ||
* @param {String} path | ||
* @param {String} method | ||
* @returns {Object.<path, pathAndMethod>} returns layers that matched path and | ||
* path and method. | ||
* @private | ||
*/ | ||
Router.prototype.match = function (path, method) { | ||
const layers = this.stack; | ||
let layer; | ||
const matched = { | ||
path: [], | ||
pathAndMethod: [], | ||
route: false | ||
}; | ||
for (let len = layers.length, i = 0; i < len; i++) { | ||
layer = layers[i]; | ||
debug('test %s %s', layer.path, layer.regexp); | ||
// eslint-disable-next-line unicorn/prefer-regexp-test | ||
if (layer.match(path)) { | ||
matched.path.push(layer); | ||
if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) { | ||
matched.pathAndMethod.push(layer); | ||
if (layer.methods.length > 0) matched.route = true; | ||
} | ||
} | ||
} | ||
return matched; | ||
}; | ||
/** | ||
* Match given `input` to allowed host | ||
* @param {String} input | ||
* @returns {boolean} | ||
*/ | ||
Router.prototype.matchHost = function (input) { | ||
const { host } = this; | ||
if (!host) { | ||
return true; | ||
} | ||
if (!input) { | ||
return false; | ||
} | ||
if (typeof host === 'string') { | ||
return input === host; | ||
} | ||
if (typeof host === 'object' && host instanceof RegExp) { | ||
return host.test(input); | ||
} | ||
}; | ||
/** | ||
* Run middleware for named route parameters. Useful for auto-loading or | ||
* validation. | ||
* | ||
* @example | ||
* | ||
* ```javascript | ||
* router | ||
* .param('user', (id, ctx, next) => { | ||
* ctx.user = users[id]; | ||
* if (!ctx.user) return ctx.status = 404; | ||
* return next(); | ||
* }) | ||
* .get('/users/:user', ctx => { | ||
* ctx.body = ctx.user; | ||
* }) | ||
* .get('/users/:user/friends', ctx => { | ||
* return ctx.user.getFriends().then(function(friends) { | ||
* ctx.body = friends; | ||
* }); | ||
* }) | ||
* // /users/3 => {"id": 3, "name": "Alex"} | ||
* // /users/3/friends => [{"id": 4, "name": "TJ"}] | ||
* ``` | ||
* | ||
* @param {String} param | ||
* @param {Function} middleware | ||
* @returns {Router} | ||
*/ | ||
Router.prototype.param = function (param, middleware) { | ||
this.params[param] = middleware; | ||
for (let i = 0; i < this.stack.length; i++) { | ||
const route = this.stack[i]; | ||
route.param(param, middleware); | ||
} | ||
return this; | ||
}; | ||
/** | ||
* Generate URL from url pattern and given `params`. | ||
* | ||
* @example | ||
* | ||
* ```javascript | ||
* const url = Router.url('/users/:id', {id: 1}); | ||
* // => "/users/1" | ||
* ``` | ||
* | ||
* @param {String} path url pattern | ||
* @param {Object} params url parameters | ||
* @returns {String} | ||
*/ | ||
Router.url = function (path) { | ||
const args = Array.prototype.slice.call(arguments, 1); | ||
return Layer.prototype.url.apply({ path }, args); | ||
}; | ||
module.exports = Router; |
{ | ||
"name": "koa-router", | ||
"description": "Router middleware for koa. Maintained by Forward Email and Lad.", | ||
"version": "12.0.1", | ||
"version": "13.0.1", | ||
"author": "Alex Mingoia <talk@alexmingoia.com>", | ||
@@ -11,11 +11,18 @@ "bugs": { | ||
"contributors": [ | ||
"Alex Mingoia <talk@alexmingoia.com>", | ||
"@koajs" | ||
{ | ||
"name": "Alex Mingoia", | ||
"email": "talk@alexmingoia.com" | ||
}, | ||
{ | ||
"name": "@koajs" | ||
}, | ||
{ | ||
"name": "Imed Jaberi", | ||
"email": "imed-jaberi@outlook.com" | ||
} | ||
], | ||
"dependencies": { | ||
"debug": "^4.3.4", | ||
"http-errors": "^2.0.0", | ||
"koa-compose": "^4.1.0", | ||
"methods": "^1.1.2", | ||
"path-to-regexp": "^6.2.1" | ||
"path-to-regexp": "^8.1.0" | ||
}, | ||
@@ -26,23 +33,18 @@ "devDependencies": { | ||
"@ladjs/env": "^4.0.0", | ||
"ava": "^5.3.1", | ||
"cross-env": "^7.0.3", | ||
"eslint": "8.39.0", | ||
"eslint": "^8.39.0", | ||
"eslint-config-xo-lass": "^2.0.1", | ||
"expect.js": "^0.3.1", | ||
"fixpack": "^4.0.0", | ||
"husky": "^8.0.3", | ||
"jsdoc-to-markdown": "^8.0.0", | ||
"koa": "^2.14.2", | ||
"koa": "^2.15.3", | ||
"lint-staged": "^14.0.1", | ||
"mocha": "^10.2.0", | ||
"nyc": "^15.1.0", | ||
"mocha": "^10.7.3", | ||
"nyc": "^17.0.0", | ||
"remark-cli": "11", | ||
"remark-preset-github": "^4.0.4", | ||
"should": "^13.2.3", | ||
"supertest": "^6.3.3", | ||
"wrk": "^1.2.1", | ||
"supertest": "^7.0.0", | ||
"xo": "0.53.1" | ||
}, | ||
"engines": { | ||
"node": ">= 12" | ||
"node": ">= 18" | ||
}, | ||
@@ -63,3 +65,3 @@ "files": [ | ||
"type": "git", | ||
"url": "https://github.com/koajs/router.git" | ||
"url": "git+https://github.com/koajs/router.git" | ||
}, | ||
@@ -66,0 +68,0 @@ "scripts": { |
@@ -72,2 +72,3 @@ # [@koa/router](https://github.com/koajs/router) | ||
| **@koajs** | | ||
| **Imed Jaberi** | | ||
@@ -86,2 +87,2 @@ | ||
[npm]: https//www.npmjs.com | ||
[npm]: https://www.npmjs.com |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
34245
3
16
990
87
+ Addedpath-to-regexp@8.2.0(transitive)
- Removeddebug@^4.3.4
- Removedmethods@^1.1.2
- Removeddebug@4.3.7(transitive)
- Removedmethods@1.1.2(transitive)
- Removedms@2.1.3(transitive)
- Removedpath-to-regexp@6.3.0(transitive)
Updatedpath-to-regexp@^8.1.0