UNPKG

@koa/router

Version:

Router middleware for koa. Maintained by Forward Email and Lad.

1,404 lines (1,394 loc) 40.4 kB
// src/router.ts import compose from "koa-compose"; import HttpError from "http-errors"; // src/layer.ts import { parse as parseUrl, format as formatUrl } from "url"; // src/utils/path-to-regexp-wrapper.ts import { pathToRegexp, compile, parse } from "path-to-regexp"; function compilePathToRegexp(path, options = {}) { const normalizedOptions = { ...options }; if ("strict" in normalizedOptions && !("trailing" in normalizedOptions)) { normalizedOptions.trailing = normalizedOptions.strict !== true; delete normalizedOptions.strict; } delete normalizedOptions.pathAsRegExp; delete normalizedOptions.ignoreCaptures; delete normalizedOptions.prefix; const { regexp, keys } = pathToRegexp(path, normalizedOptions); return { regexp, keys }; } function compilePath(path, options = {}) { return compile(path, options); } function parsePath(path, options) { return parse(path, options); } function normalizeLayerOptionsToPathToRegexp(options = {}) { const normalized = { sensitive: options.sensitive, end: options.end, strict: options.strict, trailing: options.trailing }; if ("strict" in normalized && !("trailing" in normalized)) { normalized.trailing = normalized.strict !== true; delete normalized.strict; } for (const key of Object.keys(normalized)) { if (normalized[key] === void 0) { delete normalized[key]; } } return normalized; } // src/utils/safe-decode-uri-components.ts function safeDecodeURIComponent(text) { try { return decodeURIComponent(text); } catch { return text; } } // src/layer.ts var Layer = class { opts; name; methods; paramNames; stack; path; regexp; /** * Initialize a new routing Layer with given `method`, `path`, and `middleware`. * * @param path - Path string or regular expression * @param methods - Array of HTTP verbs * @param middleware - Layer callback/middleware or series of * @param opts - Layer options * @private */ constructor(path, methods, middleware, options = {}) { this.opts = options; this.name = this.opts.name || void 0; this.methods = this._normalizeHttpMethods(methods); this.stack = this._normalizeAndValidateMiddleware( middleware, methods, path ); this.path = path; this.paramNames = []; this._configurePathMatching(); } /** * Normalize HTTP methods and add automatic HEAD support for GET * @private */ _normalizeHttpMethods(methods) { const normalizedMethods = []; for (const method of methods) { const upperMethod = method.toUpperCase(); normalizedMethods.push(upperMethod); if (upperMethod === "GET") { normalizedMethods.unshift("HEAD"); } } return normalizedMethods; } /** * Normalize middleware to array and validate all are functions * @private */ _normalizeAndValidateMiddleware(middleware, methods, path) { const middlewareArray = Array.isArray(middleware) ? middleware : [middleware]; for (const middlewareFunction of middlewareArray) { const middlewareType = typeof middlewareFunction; if (middlewareType !== "function") { const routeIdentifier = this.opts.name || path; throw new Error( `${methods.toString()} \`${routeIdentifier}\`: \`middleware\` must be a function, not \`${middlewareType}\`` ); } } return middlewareArray; } /** * Configure path matching regexp and parameters * @private */ _configurePathMatching() { if (this.opts.pathAsRegExp === true) { this.regexp = this.path instanceof RegExp ? this.path : new RegExp(this.path); } else if (this.path) { this._configurePathToRegexp(); } } /** * Configure path-to-regexp for string paths * @private */ _configurePathToRegexp() { const options = normalizeLayerOptionsToPathToRegexp(this.opts); const { regexp, keys } = compilePathToRegexp(this.path, options); this.regexp = regexp; this.paramNames = keys; } /** * Returns whether request `path` matches route. * * @param path - Request path * @returns Whether path matches * @private */ match(path) { return this.regexp.test(path); } /** * Returns map of URL parameters for given `path` and `paramNames`. * * @param _path - Request path (not used, kept for API compatibility) * @param captures - Captured values from regexp * @param existingParams - Existing params to merge with * @returns Parameter map * @private */ params(_path, captures, existingParameters = {}) { const parameterValues = { ...existingParameters }; for (const [captureIndex, capturedValue] of captures.entries()) { const parameterDefinition = this.paramNames[captureIndex]; if (parameterDefinition && capturedValue && capturedValue.length > 0) { const parameterName = parameterDefinition.name; parameterValues[parameterName] = safeDecodeURIComponent(capturedValue); } } return parameterValues; } /** * Returns array of regexp url path captures. * * @param path - Request path * @returns Array of captured values * @private */ captures(path) { if (this.opts.ignoreCaptures) { return []; } const match = path.match(this.regexp); return match ? match.slice(1) : []; } /** * Generate URL for route using given `params`. * * @example * * ```javascript * const route = new Layer('/users/:id', ['GET'], fn); * * route.url({ id: 123 }); // => "/users/123" * ``` * * @param args - URL parameters (various formats supported) * @returns Generated URL * @throws Error if route path is a RegExp (cannot generate URL from RegExp) * @private */ url(...arguments_) { if (this.path instanceof RegExp) { throw new TypeError( "Cannot generate URL for routes defined with RegExp paths. Use string paths with named parameters instead." ); } const { params, options } = this._parseUrlArguments(arguments_); const cleanPath = this.path.replaceAll("(.*)", ""); const pathCompiler = compilePath(cleanPath, { encode: encodeURIComponent, ...options }); const parameterReplacements = this._buildParamReplacements( params, cleanPath ); const generatedUrl = pathCompiler(parameterReplacements); if (options && options.query) { return this._addQueryString(generatedUrl, options.query); } return generatedUrl; } /** * Parse url() arguments into params and options * Supports multiple call signatures: * - url({ id: 1 }) * - url(1, 2, 3) * - url({ query: {...} }) * - url({ id: 1 }, { query: {...} }) * @private */ _parseUrlArguments(allArguments) { let parameters = allArguments[0] ?? {}; let options = allArguments[1]; if (typeof parameters !== "object" || parameters === null) { const argumentsList = [...allArguments]; const lastArgument = argumentsList.at(-1); if (typeof lastArgument === "object" && lastArgument !== null) { options = lastArgument; parameters = argumentsList.slice(0, -1); } else { parameters = argumentsList; } } else if (parameters && !options) { const parameterKeys = Object.keys(parameters); const isOnlyOptions = parameterKeys.length === 1 && parameterKeys[0] === "query"; if (isOnlyOptions) { options = parameters; parameters = {}; } else if ("query" in parameters && parameters.query) { const { query, ...restParameters } = parameters; options = { query }; parameters = restParameters; } } return { params: parameters, options }; } /** * Build parameter replacements for URL generation * @private */ _buildParamReplacements(parameters, cleanPath) { const { tokens } = parsePath(cleanPath); const hasNamedParameters = tokens.some( (token) => "name" in token && token.name ); const parameterReplacements = {}; if (Array.isArray(parameters)) { let parameterIndex = 0; for (const token of tokens) { if ("name" in token && token.name) { parameterReplacements[token.name] = String( parameters[parameterIndex++] ); } } } else if (hasNamedParameters && typeof parameters === "object" && !("query" in parameters)) { for (const [parameterName, parameterValue] of Object.entries( parameters )) { parameterReplacements[parameterName] = String(parameterValue); } } return parameterReplacements; } /** * Add query string to URL * @private */ _addQueryString(baseUrl, query) { const parsed = parseUrl(baseUrl); const urlObject = { ...parsed, query: parsed.query ?? void 0 }; if (typeof query === "string") { urlObject.search = query; urlObject.query = void 0; } else { urlObject.search = void 0; urlObject.query = query; } return formatUrl(urlObject); } /** * 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 paramName - Parameter name * @param paramHandler - Middleware function * @returns This layer instance * @private */ param(parameterName, parameterHandler) { const middlewareStack = this.stack; const routeParameterNames = this.paramNames; const parameterMiddleware = this._createParamMiddleware( parameterName, parameterHandler ); const parameterNamesList = routeParameterNames.map( (parameterDefinition) => parameterDefinition.name ); const parameterPosition = parameterNamesList.indexOf(parameterName); if (parameterPosition !== -1) { this._insertParamMiddleware( middlewareStack, parameterMiddleware, parameterNamesList, parameterPosition ); } return this; } /** * Create param middleware with deduplication tracking * @private */ _createParamMiddleware(parameterName, parameterHandler) { const middleware = ((context, next) => { if (!context._matchedParams) { context._matchedParams = /* @__PURE__ */ new WeakMap(); } if (context._matchedParams.has(parameterHandler)) { return next(); } context._matchedParams.set(parameterHandler, true); return parameterHandler(context.params[parameterName], context, next); }); middleware.param = parameterName; middleware._originalFn = parameterHandler; return middleware; } /** * Insert param middleware at the correct position in the stack * @private */ _insertParamMiddleware(middlewareStack, parameterMiddleware, parameterNamesList, currentParameterPosition) { let inserted = false; for (let stackIndex = 0; stackIndex < middlewareStack.length; stackIndex++) { const existingMiddleware = middlewareStack[stackIndex]; if (!existingMiddleware.param) { middlewareStack.splice(stackIndex, 0, parameterMiddleware); inserted = true; break; } const existingParameterPosition = parameterNamesList.indexOf( existingMiddleware.param ); if (existingParameterPosition > currentParameterPosition) { middlewareStack.splice(stackIndex, 0, parameterMiddleware); inserted = true; break; } } if (!inserted) { middlewareStack.push(parameterMiddleware); } } /** * Prefix route path. * * @param prefixPath - Prefix to prepend * @returns This layer instance * @private */ setPrefix(prefixPath) { if (!this.path) { return this; } if (this.path instanceof RegExp) { return this; } this.path = this._applyPrefix(prefixPath); this._reconfigurePathMatching(prefixPath); return this; } /** * Apply prefix to the current path * @private */ _applyPrefix(prefixPath) { const isRootPath = this.path === "/"; const isStrictMode = this.opts.strict === true; const prefixHasParameters = prefixPath.includes(":"); const pathIsRawRegex = this.opts.pathAsRegExp === true && typeof this.path === "string"; if (prefixHasParameters && pathIsRawRegex) { const currentPath = this.path; if (currentPath === String.raw`(?:\/|$)` || currentPath === String.raw`(?:\\\/|$)`) { this.path = "{/*rest}"; this.opts.pathAsRegExp = false; } } if (isRootPath && !isStrictMode) { return prefixPath; } return `${prefixPath}${this.path}`; } /** * Reconfigure path matching after prefix is applied * @private */ _reconfigurePathMatching(prefixPath) { const treatAsRegExp = this.opts.pathAsRegExp === true; const prefixHasParameters = prefixPath && prefixPath.includes(":"); if (prefixHasParameters && treatAsRegExp) { const options = normalizeLayerOptionsToPathToRegexp(this.opts); const { regexp, keys } = compilePathToRegexp( this.path, options ); this.regexp = regexp; this.paramNames = keys; this.opts.pathAsRegExp = false; } else if (treatAsRegExp) { const pathString = this.path; const anchoredPattern = pathString.startsWith("^") ? pathString : `^${pathString}`; this.regexp = this.path instanceof RegExp ? this.path : new RegExp(anchoredPattern); } else { const options = normalizeLayerOptionsToPathToRegexp(this.opts); const { regexp, keys } = compilePathToRegexp( this.path, options ); this.regexp = regexp; this.paramNames = keys; } } }; // src/utils/http-methods.ts import http from "http"; function getAllHttpMethods() { return http.METHODS.map((method) => method.toLowerCase()); } var COMMON_HTTP_METHODS = [ "get", "post", "put", "patch", "delete", "del", "head", "options" ]; // src/utils/parameter-helpers.ts function normalizeParameterMiddleware(parameterMiddleware) { if (!parameterMiddleware) { return []; } if (Array.isArray(parameterMiddleware)) { return parameterMiddleware; } return [parameterMiddleware]; } function applyParameterMiddlewareToRoute(route, parameterName, parameterMiddleware) { const middlewareList = normalizeParameterMiddleware( parameterMiddleware ); for (const middleware of middlewareList) { route.param(parameterName, middleware); } } function applyAllParameterMiddleware(route, parametersObject) { const parameterNames = Object.keys(parametersObject); for (const parameterName of parameterNames) { const parameterMiddleware = parametersObject[parameterName]; applyParameterMiddlewareToRoute( route, parameterName, parameterMiddleware ); } } // src/utils/path-helpers.ts function hasPathParameters(path, options = {}) { if (!path) { return false; } const { keys } = compilePathToRegexp(path, options); return keys.length > 0; } function determineMiddlewarePath(explicitPath, hasPrefixParameters) { if (explicitPath !== void 0) { if (typeof explicitPath === "string") { if (explicitPath === "") { return { path: "{/*rest}", pathAsRegExp: false }; } if (explicitPath === "/") { return { path: "/", pathAsRegExp: false }; } return { path: explicitPath, pathAsRegExp: false }; } return { path: explicitPath, pathAsRegExp: true }; } if (hasPrefixParameters) { return { path: "{/*rest}", pathAsRegExp: false }; } return { path: String.raw`(?:\/|$)`, pathAsRegExp: true }; } // src/utils/debug.ts import debugModule from "debug"; var debug = debugModule("koa-router"); // src/router.ts var httpMethods = getAllHttpMethods(); var Router = class { opts; methods; exclusive; params; stack; host; /** * 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 opts - Router options * @constructor */ constructor(options = {}) { this.opts = options; this.methods = this.opts.methods || [ "HEAD", "OPTIONS", "GET", "PUT", "PATCH", "POST", "DELETE" ]; this.exclusive = Boolean(this.opts.exclusive); this.params = {}; this.stack = []; this.host = this.opts.host; } /** * Generate URL from url pattern and given `params`. * * @example * * ```javascript * const url = Router.url('/users/:id', {id: 1}); * // => "/users/1" * ``` * * @param path - URL pattern * @param args - URL parameters * @returns Generated URL */ static url(path, ...arguments_) { const temporaryLayer = new Layer(path, [], () => { }); return temporaryLayer.url(...arguments_); } use(...middleware) { let explicitPath; if (this._isPathArray(middleware[0])) { return this._useWithPathArray(middleware); } const hasExplicitPath = this._hasExplicitPath(middleware[0]); if (hasExplicitPath) { explicitPath = middleware.shift(); } if (middleware.length === 0) { throw new Error( "You must provide at least one middleware function to router.use()" ); } for (const currentMiddleware of middleware) { if (this._isNestedRouter(currentMiddleware)) { this._mountNestedRouter( currentMiddleware, explicitPath ); } else { this._registerMiddleware( currentMiddleware, explicitPath, hasExplicitPath ); } } return this; } /** * Check if first argument is an array of paths (all elements must be strings) * @private */ _isPathArray(firstArgument) { return Array.isArray(firstArgument) && firstArgument.length > 0 && firstArgument.every((item) => typeof item === "string"); } /** * Check if first argument is an explicit path (string or RegExp) * Empty string counts as explicit path to enable param capture * @private */ _hasExplicitPath(firstArgument) { return typeof firstArgument === "string" || firstArgument instanceof RegExp; } /** * Check if middleware contains a nested router * @private */ _isNestedRouter(middleware) { return typeof middleware === "function" && "router" in middleware && middleware.router !== void 0; } /** * Apply middleware to multiple paths * @private */ _useWithPathArray(middleware) { const pathArray = middleware[0]; const remainingMiddleware = middleware.slice(1); for (const singlePath of pathArray) { Reflect.apply(this.use, this, [singlePath, ...remainingMiddleware]); } return this; } /** * Mount a nested router * @private */ _mountNestedRouter(middlewareWithRouter, mountPath) { const nestedRouter = middlewareWithRouter.router; const clonedRouter = this._cloneRouter(nestedRouter); const mountPathHasParameters = mountPath && typeof mountPath === "string" && hasPathParameters(mountPath, this.opts); for (let routeIndex = 0; routeIndex < clonedRouter.stack.length; routeIndex++) { const nestedLayer = clonedRouter.stack[routeIndex]; const clonedLayer = this._cloneLayer(nestedLayer); if (mountPath && typeof mountPath === "string") { clonedLayer.setPrefix(mountPath); } if (this.opts.prefix) { clonedLayer.setPrefix(this.opts.prefix); } if (clonedLayer.methods.length === 0 && mountPathHasParameters) { clonedLayer.opts.ignoreCaptures = false; } this.stack.push(clonedLayer); clonedRouter.stack[routeIndex] = clonedLayer; } if (this.params) { this._applyParamMiddlewareToRouter(clonedRouter); } } /** * Clone a router instance * @private */ _cloneRouter(sourceRouter) { return Object.assign( Object.create(Object.getPrototypeOf(sourceRouter)), sourceRouter, { stack: [...sourceRouter.stack] } ); } /** * Clone a layer instance (deep clone to avoid shared references) * @private */ _cloneLayer(sourceLayer) { const cloned = Object.assign( Object.create(Object.getPrototypeOf(sourceLayer)), sourceLayer, { // Deep clone arrays and objects to avoid shared references stack: [...sourceLayer.stack], methods: [...sourceLayer.methods], paramNames: [...sourceLayer.paramNames], opts: { ...sourceLayer.opts } } ); return cloned; } /** * Apply this router's param middleware to a nested router * @private */ _applyParamMiddlewareToRouter(targetRouter) { const parameterNames = Object.keys(this.params); for (const parameterName of parameterNames) { const parameterMiddleware = this.params[parameterName]; applyParameterMiddlewareToRoute( targetRouter, parameterName, parameterMiddleware ); } } /** * Register regular middleware (not nested router) * @private */ _registerMiddleware(middleware, explicitPath, hasExplicitPath) { const prefixHasParameters = hasPathParameters( this.opts.prefix || "", this.opts ); const effectiveExplicitPath = (() => { if (explicitPath !== void 0) return explicitPath; if (prefixHasParameters) return ""; return; })(); const effectiveHasExplicitPath = hasExplicitPath || explicitPath === void 0 && prefixHasParameters; const { path: middlewarePath, pathAsRegExp } = determineMiddlewarePath( effectiveExplicitPath, prefixHasParameters ); let finalPath = middlewarePath; let usePathToRegexp = pathAsRegExp; const isRootPath = effectiveHasExplicitPath && middlewarePath === "/"; if (effectiveHasExplicitPath && typeof middlewarePath === "string") { finalPath = middlewarePath; usePathToRegexp = false; } this.register(finalPath, [], middleware, { end: isRootPath, ignoreCaptures: !effectiveHasExplicitPath && !prefixHasParameters, pathAsRegExp: usePathToRegexp }); } /** * Set the path prefix for a Router instance that was already initialized. * Note: Calling this method multiple times will replace the prefix, not stack them. * * @example * * ```javascript * router.prefix('/things/:thing_id') * ``` * * @param prefixPath - Prefix string * @returns This router instance */ prefix(prefixPath) { const normalizedPrefix = prefixPath.replace(/\/$/, ""); const previousPrefix = this.opts.prefix || ""; this.opts.prefix = normalizedPrefix; for (const route of this.stack) { if (previousPrefix && typeof route.path === "string" && route.path.startsWith(previousPrefix)) { route.path = route.path.slice(previousPrefix.length) || "/"; } route.setPrefix(normalizedPrefix); } return this; } /** * Returns router middleware which dispatches a route matching the request. * * @returns Router middleware */ middleware() { const dispatchMiddleware = function(context, next) { debug("%s %s", context.method, context.path); if (!this.matchHost(context.host)) { return next(); } const requestPath = this._getRequestPath(context); const matchResult = this.match(requestPath, context.method); this._storeMatchedRoutes(context, matchResult); context.router = this; if (!matchResult.route) { return next(); } const matchedLayers = matchResult.pathAndMethod; this._setMatchedRouteInfo(context, matchedLayers); const middlewareChain = this._buildMiddlewareChain( matchedLayers, requestPath ); return compose(middlewareChain)( context, next ); }.bind(this); dispatchMiddleware.router = this; return dispatchMiddleware; } /** * Get the request path to use for routing * @private */ _getRequestPath(context) { const context_ = context; return this.opts.routerPath || context_.newRouterPath || context_.path || context_.routerPath || ""; } /** * Store matched routes on context * @private */ _storeMatchedRoutes(context, matchResult) { const context_ = context; if (context_.matched) { context_.matched.push(...matchResult.path); } else { context_.matched = matchResult.path; } } /** * Set matched route information on context * @private */ _setMatchedRouteInfo(context, matchedLayers) { const context_ = context; const routeLayer = matchedLayers.toReversed().find((layer) => layer.methods.length > 0); if (routeLayer) { context_._matchedRoute = routeLayer.path; if (routeLayer.name) { context_._matchedRouteName = routeLayer.name; } } } /** * Build middleware chain from matched layers * @private */ _buildMiddlewareChain(matchedLayers, requestPath) { const layersToExecute = this.opts.exclusive ? [matchedLayers.at(-1)].filter( (layer) => layer !== void 0 ) : matchedLayers; const middlewareChain = []; for (const layer of layersToExecute) { middlewareChain.push( (context, next) => { const routerContext = context; routerContext.captures = layer.captures(requestPath); routerContext.request.params = layer.params( requestPath, routerContext.captures || [], routerContext.params ); routerContext.params = routerContext.request.params; routerContext.routerPath = layer.path; routerContext.routerName = layer.name || void 0; routerContext._matchedRoute = layer.path; if (layer.name) { routerContext._matchedRouteName = layer.name; } return next(); }, ...layer.stack ); } return middlewareChain; } 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 options - Options object * @returns Middleware function */ allowedMethods(options = {}) { const implementedMethods = this.methods; return (context, next) => { const routerContext = context; return next().then(() => { if (!this._shouldProcessAllowedMethods(routerContext)) { return; } const matchedRoutes = routerContext.matched || []; const allowedMethods = this._collectAllowedMethods(matchedRoutes); const allowedMethodsList = Object.keys(allowedMethods); const requestMethod = context.method.toUpperCase(); if (!implementedMethods.includes(requestMethod)) { this._handleNotImplemented( routerContext, allowedMethodsList, options ); return; } if (requestMethod === "OPTIONS" && allowedMethodsList.length > 0) { this._handleOptionsRequest(routerContext, allowedMethodsList); return; } if (allowedMethodsList.length > 0 && !allowedMethods[requestMethod]) { this._handleMethodNotAllowed( routerContext, allowedMethodsList, options ); } }); }; } /** * Check if we should process allowed methods * @private */ _shouldProcessAllowedMethods(context) { return !!(context.matched && (!context.status || context.status === 404)); } /** * Collect all allowed methods from matched routes * @private */ _collectAllowedMethods(matchedRoutes) { const allowedMethods = {}; for (const route of matchedRoutes) { for (const method of route.methods) { allowedMethods[method] = method; } } return allowedMethods; } /** * Handle 501 Not Implemented response * @private */ _handleNotImplemented(context, allowedMethodsList, options) { if (options.throw) { const error = typeof options.notImplemented === "function" ? options.notImplemented() : new HttpError.NotImplemented(); throw error; } context.status = 501; context.set("Allow", allowedMethodsList.join(", ")); } /** * Handle OPTIONS request * @private */ _handleOptionsRequest(context, allowedMethodsList) { context.status = 200; context.body = ""; context.set("Allow", allowedMethodsList.join(", ")); } /** * Handle 405 Method Not Allowed response * @private */ _handleMethodNotAllowed(context, allowedMethodsList, options) { if (options.throw) { const error = typeof options.methodNotAllowed === "function" ? options.methodNotAllowed() : new HttpError.MethodNotAllowed(); throw error; } context.status = 405; context.set("Allow", allowedMethodsList.join(", ")); } all(...arguments_) { let name; let path; let middleware; if (arguments_.length >= 2 && (typeof arguments_[1] === "string" || arguments_[1] instanceof RegExp)) { name = arguments_[0]; path = arguments_[1]; middleware = arguments_.slice(2); } else { name = void 0; path = arguments_[0]; middleware = arguments_.slice(1); } 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 routeOptions = { name, pathAsRegExp: path instanceof RegExp }; this.register(path, httpMethods, middleware, { ...this.opts, ...routeOptions }); 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 source - URL or route name * @param destination - URL or route name * @param code - HTTP status code (default: 301) * @returns This router instance */ redirect(source, destination, code) { let resolvedSource = source; let resolvedDestination = destination; if (typeof source === "symbol" || typeof source === "string" && source[0] !== "/") { const sourceUrl = this.url(source); if (sourceUrl instanceof Error) throw sourceUrl; resolvedSource = sourceUrl; } if (typeof destination === "symbol" || typeof destination === "string" && destination[0] !== "/" && !destination.includes("://")) { const destinationUrl = this.url(destination); if (destinationUrl instanceof Error) throw destinationUrl; resolvedDestination = destinationUrl; } const result = this.all( resolvedSource, (context) => { context.redirect(resolvedDestination); context.status = code || 301; } ); return result; } /** * Create and register a route. * * @param path - Path string * @param methods - Array of HTTP verbs * @param middleware - Middleware functions * @param additionalOptions - Additional options * @returns Created layer * @private */ register(path, methods, middleware, additionalOptions = {}) { const mergedOptions = { ...this.opts, ...additionalOptions }; if (Array.isArray(path)) { return this._registerMultiplePaths( path, methods, middleware, mergedOptions ); } const routeLayer = this._createRouteLayer( path, methods, middleware, mergedOptions ); if (this.opts.prefix) { routeLayer.setPrefix(this.opts.prefix); } applyAllParameterMiddleware(routeLayer, this.params); this.stack.push(routeLayer); debug("defined route %s %s", routeLayer.methods, routeLayer.path); return routeLayer; } /** * Register multiple paths with the same configuration * @private */ _registerMultiplePaths(pathArray, methods, middleware, options) { for (const singlePath of pathArray) { this.register.call(this, singlePath, methods, middleware, options); } return this; } /** * Create a route layer with given configuration * @private */ _createRouteLayer(path, methods, middleware, options) { return new Layer(path, methods, middleware, { end: options.end === false ? options.end : true, name: options.name, sensitive: options.sensitive || false, strict: options.strict || false, prefix: options.prefix || "", ignoreCaptures: options.ignoreCaptures, pathAsRegExp: options.pathAsRegExp }); } /** * Lookup route with given `name`. * * @param name - Route name * @returns Matched layer or false */ route(name) { const matchingRoute = this.stack.find((route) => route.name === name); return matchingRoute || 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 name - Route name * @param args - URL parameters * @returns Generated URL or Error */ url(name, ...arguments_) { const route = this.route(name); if (route) return route.url(...arguments_); return new Error(`No route found for name: ${String(name)}`); } /** * Match given `path` and return corresponding routes. * * @param path - Request path * @param method - HTTP method * @returns Match result with matched layers * @private */ match(path, method) { const matchResult = { path: [], pathAndMethod: [], route: false }; const normalizedMethod = method.toUpperCase(); for (const layer of this.stack) { debug("test %s %s", layer.path, layer.regexp); if (layer.match(path)) { matchResult.path.push(layer); const isMiddleware = layer.methods.length === 0; const matchesMethod = layer.methods.includes(normalizedMethod); if (isMiddleware || matchesMethod) { matchResult.pathAndMethod.push(layer); if (layer.methods.length > 0) { matchResult.route = true; } } } } return matchResult; } /** * Match given `input` to allowed host * @param input - Host to check * @returns Whether host matches */ matchHost(input) { const { host } = this; if (!host) { return true; } if (!input) { return false; } if (typeof host === "string") { return input === host; } if (Array.isArray(host)) { return host.includes(input); } if (host instanceof RegExp) { return host.test(input); } return false; } /** * 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 param - Parameter name * @param middleware - Parameter middleware * @returns This router instance */ param(parameter, middleware) { if (!this.params[parameter]) { this.params[parameter] = []; } if (!Array.isArray(this.params[parameter])) { this.params[parameter] = [ this.params[parameter] ]; } this.params[parameter].push(middleware); for (const route of this.stack) { route.param(parameter, middleware); } return this; } /** * Helper method for registering HTTP verb routes * @internal - Used by dynamically added HTTP methods */ _registerMethod(method, ...arguments_) { let name; let path; let middleware; if (arguments_.length >= 2 && (typeof arguments_[1] === "string" || arguments_[1] instanceof RegExp)) { name = arguments_[0]; path = arguments_[1]; middleware = arguments_.slice(2); } else { name = void 0; path = arguments_[0]; middleware = arguments_.slice(1); } 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` ); const options = { name, pathAsRegExp: path instanceof RegExp }; this.register(path, [method], middleware, { ...this.opts, ...options }); return this; } get(...arguments_) { return this._registerMethod("get", ...arguments_); } post(...arguments_) { return this._registerMethod("post", ...arguments_); } put(...arguments_) { return this._registerMethod("put", ...arguments_); } patch(...arguments_) { return this._registerMethod("patch", ...arguments_); } delete(...arguments_) { return this._registerMethod("delete", ...arguments_); } del(...arguments_) { return this.delete.apply( this, arguments_ ); } head(...arguments_) { return this._registerMethod("head", ...arguments_); } options(...arguments_) { return this._registerMethod("options", ...arguments_); } }; var RouterExport = Router; var router_default = RouterExport; for (const httpMethod of httpMethods) { const isAlreadyDefined = COMMON_HTTP_METHODS.includes(httpMethod) || httpMethod in Router.prototype; if (!isAlreadyDefined) { Object.defineProperty(Router.prototype, httpMethod, { value: function(...arguments_) { return this._registerMethod(httpMethod, ...arguments_); }, writable: true, configurable: true, enumerable: false }); } } export { RouterExport as Router, router_default as default };