Socket
Socket
Sign inDemoInstall

koa-router

Package Overview
Dependencies
Maintainers
2
Versions
91
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

koa-router - npm Package Compare versions

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;

@@ -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
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc