This release is 6 versions behind 17.1.3 — the latest version of @oak/oak. Jump to latest
Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
A middleware framework for handling HTTP with Deno, Node.js, Bun and Cloudflare Workers 🐿️🦕🥟⚙️
This package works with Cloudflare Workers, Node.js, Deno, Bun
JSR Score
100%
Published
7 months ago (16.0.0)
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472/** * Contains the router of oak. Typical usage is the creation of an application * instance, the creation of a router instance, registration of route * middleware, registration of router with the application, and then starting to * listen for requests. * * # Example * * ```ts * import { Application } from "jsr:@oak/oak@14/application"; * import { Router } from "jsr:@oak/oak@14/router"; * * const app = new Application(); * const router = new Router(); * router.get("/", (ctx) => { * ctx.response.body = "hello world!"; * }); * app.use(router.routes()); * app.use(router.allowedMethods()); * * app.listen(); * ``` * * @module */ /** * Adapted directly from @koa/router at * https://github.com/koajs/router/ which is licensed as: * * The MIT License (MIT) * * Copyright (c) 2015 Alexander C. Mingoia * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import type { State } from "./application.ts"; import type { Context } from "./context.ts"; import { assert, compile, errors, type HTTPMethods, type Key, type ParseOptions, pathParse, pathToRegexp, type RedirectStatus, Status, type TokensToRegexpOptions, } from "./deps.ts"; import { compose, type Middleware } from "./middleware.ts"; import { decodeComponent } from "./utils/decode_component.ts"; interface Matches<R extends string> { path: Layer<R>[]; pathAndMethod: Layer<R>[]; route: boolean; name?: string; } /** Options which can be specified when calling the `.allowedMethods()` method * on a {@linkcode Router} instance. */ export interface RouterAllowedMethodsOptions { /** Use the value returned from this function instead of an HTTP error * `MethodNotAllowed`. */ // deno-lint-ignore no-explicit-any methodNotAllowed?(): any; /** Use the value returned from this function instead of an HTTP error * `NotImplemented`. */ // deno-lint-ignore no-explicit-any notImplemented?(): any; /** When dealing with a non-implemented method or a method not allowed, throw * an error instead of setting the status and header for the response. */ throw?: boolean; } /** The internal abstraction of a route used by the oak {@linkcode Router}. */ export interface Route< R extends string, P extends RouteParams<R> = RouteParams<R>, // deno-lint-ignore no-explicit-any S extends State = Record<string, any>, > { /** The HTTP methods that this route handles. */ methods: HTTPMethods[]; /** The middleware that will be applied to this route. */ middleware: RouterMiddleware<R, P, S>[]; /** An optional name for the route. */ name?: string; /** Options that were used to create the route. */ options: LayerOptions; /** The parameters that are identified in the route that will be parsed out * on matched requests. */ paramNames: (keyof P)[]; /** The path that this route manages. */ path: string; /** The regular expression used for matching and parsing parameters for the * route. */ regexp: RegExp; } /** The context passed router middleware. */ export interface RouterContext< R extends string, P extends RouteParams<R> = RouteParams<R>, // deno-lint-ignore no-explicit-any S extends State = Record<string, any>, > extends Context<S> { /** When matching the route, an array of the capturing groups from the regular * expression. */ captures: string[]; /** The routes that were matched for this request. */ matched?: Layer<R, P, S>[]; /** Any parameters parsed from the route when matched. */ params: P; /** A reference to the router instance. */ router: Router; /** If the matched route has a `name`, the matched route name is provided * here. */ routeName?: string; /** Overrides the matched path for future route middleware, when a * `routerPath` option is not defined on the `Router` options. */ routerPath?: string; } /** The interface that {@linkcode Router} middleware should adhere to. */ export interface RouterMiddleware< R extends string, P extends RouteParams<R> = RouteParams<R>, // deno-lint-ignore no-explicit-any S extends State = Record<string, any>, > { (context: RouterContext<R, P, S>, next: () => Promise<unknown>): | Promise<unknown> | unknown; /** For route parameter middleware, the `param` key for this parameter will * be set. */ param?: keyof P; // deno-lint-ignore no-explicit-any router?: Router<any>; } /** Options which can be specified when creating a new instance of a * {@linkcode Router}. */ export interface RouterOptions { /** Override the default set of methods supported by the router. */ methods?: HTTPMethods[]; /** Only handle routes where the requested path starts with the prefix. */ prefix?: string; /** Override the `request.url.pathname` when matching middleware to run. */ routerPath?: string; /** Determines if routes are matched in a case sensitive way. Defaults to * `false`. */ sensitive?: boolean; /** Determines if routes are matched strictly, where the trailing `/` is not * optional. Defaults to `false`. */ strict?: boolean; } /** Middleware that will be called by the router when handling a specific * parameter, which the middleware will be called when a request matches the * route parameter. */ export interface RouterParamMiddleware< R extends string, P extends RouteParams<R> = RouteParams<R>, // deno-lint-ignore no-explicit-any S extends State = Record<string, any>, > { ( param: string, context: RouterContext<R, P, S>, next: () => Promise<unknown>, ): Promise<unknown> | unknown; // deno-lint-ignore no-explicit-any router?: Router<any>; } interface ParamsDictionary { [key: string]: string; } type RemoveTail<S extends string, Tail extends string> = S extends `${infer P}${Tail}` ? P : S; type GetRouteParams<S extends string> = RemoveTail< RemoveTail<RemoveTail<S, `/${string}`>, `-${string}`>, `.${string}` >; /** A dynamic type which attempts to determine the route params based on * matching the route string. */ export type RouteParams<Route extends string> = string extends Route ? ParamsDictionary : Route extends `${string}(${string}` ? ParamsDictionary : Route extends `${string}:${infer Rest}` ? & ( GetRouteParams<Rest> extends never ? ParamsDictionary : GetRouteParams<Rest> extends `${infer ParamName}?` ? { [P in ParamName]?: string } : { [P in GetRouteParams<Rest>]: string } ) & (Rest extends `${GetRouteParams<Rest>}${infer Next}` ? RouteParams<Next> : unknown) : Record<string | number, string | undefined>; type LayerOptions = TokensToRegexpOptions & ParseOptions & { ignoreCaptures?: boolean; name?: string; }; type RegisterOptions = LayerOptions & { ignorePrefix?: boolean; }; type UrlOptions = TokensToRegexpOptions & ParseOptions & { /** When generating a URL from a route, add the query to the URL. If an * object */ query?: URLSearchParams | Record<string, string> | string; }; /** Generate a URL from a string, potentially replace route params with * values. */ function toUrl<R extends string>( url: string, params = {} as RouteParams<R>, options?: UrlOptions, ) { const tokens = pathParse(url); let replace = {} as RouteParams<R>; if (tokens.some((token) => typeof token === "object")) { replace = params; } else { options = params; } const toPath = compile(url, options); const replaced = toPath(replace); if (options && options.query) { const url = new URL(replaced, "http://oak"); if (typeof options.query === "string") { url.search = options.query; } else { url.search = String( options.query instanceof URLSearchParams ? options.query : new URLSearchParams(options.query), ); } return `${url.pathname}${url.search}${url.hash}`; } return replaced; } /** An internal class used to group together middleware when using multiple * middlewares with a router. */ export class Layer< R extends string, P extends RouteParams<R> = RouteParams<R>, // deno-lint-ignore no-explicit-any S extends State = Record<string, any>, > { #opts: LayerOptions; #paramNames: Key[] = []; #regexp: RegExp; methods: HTTPMethods[]; name?: string; path: string; stack: RouterMiddleware<R, P, S>[]; constructor( path: string, methods: HTTPMethods[], middleware: RouterMiddleware<R, P, S> | RouterMiddleware<R, P, S>[], { name, ...opts }: LayerOptions = {}, ) { this.#opts = opts; this.name = name; this.methods = [...methods]; if (this.methods.includes("GET")) { this.methods.unshift("HEAD"); } this.stack = Array.isArray(middleware) ? middleware.slice() : [middleware]; this.path = path; this.#regexp = pathToRegexp(path, this.#paramNames, this.#opts); } clone(): Layer<R, P, S> { return new Layer( this.path, this.methods, this.stack, { name: this.name, ...this.#opts }, ); } match(path: string): boolean { return this.#regexp.test(path); } params( captures: string[], existingParams: RouteParams<R> = {} as RouteParams<R>, ): RouteParams<R> { const params = existingParams; for (let i = 0; i < captures.length; i++) { if (this.#paramNames[i]) { const c = captures[i]; params[this.#paramNames[i].name] = c ? decodeComponent(c) : c; } } return params; } captures(path: string): string[] { if (this.#opts.ignoreCaptures) { return []; } return path.match(this.#regexp)?.slice(1) ?? []; } url( params: RouteParams<R> = {} as RouteParams<R>, options?: UrlOptions, ): string { const url = this.path.replace(/\(\.\*\)/g, ""); return toUrl(url, params, options); } param( param: string, // deno-lint-ignore no-explicit-any fn: RouterParamMiddleware<any, any, any>, ): this { const stack = this.stack; const params = this.#paramNames; const middleware: RouterMiddleware<R> = function ( this: Router, ctx, next, ): Promise<unknown> | unknown { const p = ctx.params[param]; assert(p); return fn.call(this, p, ctx, next); }; middleware.param = param; const names = params.map((p) => p.name); const x = names.indexOf(param); if (x >= 0) { for (let i = 0; i < stack.length; i++) { const fn = stack[i]; if (!fn.param || names.indexOf(fn.param as (string | number)) > x) { stack.splice(i, 0, middleware); break; } } } return this; } setPrefix(prefix: string): this { 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; } // deno-lint-ignore no-explicit-any toJSON(): Route<any, any, any> { return { methods: [...this.methods], middleware: [...this.stack], paramNames: this.#paramNames.map((key) => key.name), path: this.path, regexp: this.#regexp, options: { ...this.#opts }, }; } [Symbol.for("Deno.customInspect")]( inspect: (value: unknown) => string, ): string { return `${this.constructor.name} ${ inspect({ methods: this.methods, middleware: this.stack, options: this.#opts, paramNames: this.#paramNames.map((key) => key.name), path: this.path, regexp: this.#regexp, }) }`; } [Symbol.for("nodejs.util.inspect.custom")]( depth: number, // deno-lint-ignore no-explicit-any options: any, inspect: (value: unknown, options?: unknown) => string, // deno-lint-ignore no-explicit-any ): any { if (depth < 0) { return options.stylize(`[${this.constructor.name}]`, "special"); } const newOptions = Object.assign({}, options, { depth: options.depth === null ? null : options.depth - 1, }); return `${options.stylize(this.constructor.name, "special")} ${ inspect( { methods: this.methods, middleware: this.stack, options: this.#opts, paramNames: this.#paramNames.map((key) => key.name), path: this.path, regexp: this.#regexp, }, newOptions, ) }`; } } /** An interface for registering middleware that will run when certain HTTP * methods and paths are requested, as well as provides a way to parameterize * parts of the requested path. * * ### Basic example * * ```ts * import { Application, Router } from "jsr:@oak/oak/"; * * const router = new Router(); * router.get("/", (ctx, next) => { * // handle the GET endpoint here * }); * router.all("/item/:item", (ctx, next) => { * // called for all HTTP verbs/requests * ctx.params.item; // contains the value of `:item` from the parsed URL * }); * * const app = new Application(); * app.use(router.routes()); * app.use(router.allowedMethods()); * * app.listen({ port: 8080 }); * ``` */ export class Router< // deno-lint-ignore no-explicit-any RS extends State = Record<string, any>, > { #opts: RouterOptions; #methods: HTTPMethods[]; // deno-lint-ignore no-explicit-any #params: Record<string, RouterParamMiddleware<any, any, any>> = {}; #stack: Layer<string>[] = []; #match(path: string, method: HTTPMethods): Matches<string> { const matches: Matches<string> = { path: [], pathAndMethod: [], route: false, }; for (const route of this.#stack) { if (route.match(path)) { matches.path.push(route); if (route.methods.length === 0 || route.methods.includes(method)) { matches.pathAndMethod.push(route); if (route.methods.length) { matches.route = true; matches.name = route.name; } } } } return matches; } #register( path: string | string[], middlewares: RouterMiddleware<string>[], methods: HTTPMethods[], options: RegisterOptions = {}, ): void { if (Array.isArray(path)) { for (const p of path) { this.#register(p, middlewares, methods, options); } return; } let layerMiddlewares: RouterMiddleware<string>[] = []; for (const middleware of middlewares) { if (!middleware.router) { layerMiddlewares.push(middleware); continue; } if (layerMiddlewares.length) { this.#addLayer(path, layerMiddlewares, methods, options); layerMiddlewares = []; } const router = middleware.router.#clone(); for (const layer of router.#stack) { if (!options.ignorePrefix) { layer.setPrefix(path); } if (this.#opts.prefix) { layer.setPrefix(this.#opts.prefix); } this.#stack.push(layer); } for (const [param, mw] of Object.entries(this.#params)) { router.param(param, mw); } } if (layerMiddlewares.length) { this.#addLayer(path, layerMiddlewares, methods, options); } } #addLayer( path: string, middlewares: RouterMiddleware<string>[], methods: HTTPMethods[], options: LayerOptions = {}, ) { const { end, name, sensitive = this.#opts.sensitive, strict = this.#opts.strict, ignoreCaptures, } = options; const route = new Layer(path, methods, middlewares, { end, name, sensitive, strict, ignoreCaptures, }); if (this.#opts.prefix) { route.setPrefix(this.#opts.prefix); } for (const [param, mw] of Object.entries(this.#params)) { route.param(param, mw); } this.#stack.push(route); } #route(name: string): Layer<string> | undefined { for (const route of this.#stack) { if (route.name === name) { return route; } } } #useVerb( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string>, middleware: RouterMiddleware<string>[], methods: HTTPMethods[], ): void { let name: string | undefined = undefined; let path: string; if (typeof pathOrMiddleware === "string") { name = nameOrPath; path = pathOrMiddleware; } else { path = nameOrPath; middleware.unshift(pathOrMiddleware); } this.#register(path, middleware, methods, { name }); } #clone(): Router<RS> { const router = new Router<RS>(this.#opts); router.#methods = router.#methods.slice(); router.#params = { ...this.#params }; router.#stack = this.#stack.map((layer) => layer.clone()); return router; } constructor(opts: RouterOptions = {}) { this.#opts = opts; this.#methods = opts.methods ?? [ "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", ]; } /** Register named middleware for the specified routes when specified methods * are requested. */ add< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( methods: HTTPMethods[] | HTTPMethods, name: string, path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the specified methods is * requested. */ add< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( methods: HTTPMethods[] | HTTPMethods, path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the specified methods * are requested with explicit path parameters. */ add< P extends RouteParams<string>, S extends State = RS, >( methods: HTTPMethods[] | HTTPMethods, nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)>; add< P extends RouteParams<string> = RouteParams<string>, S extends State = RS, >( methods: HTTPMethods[] | HTTPMethods, nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)> { this.#useVerb( nameOrPath, pathOrMiddleware as (string | RouterMiddleware<string>), middleware as RouterMiddleware<string>[], typeof methods === "string" ? [methods] : methods, ); return this; } /** Register named middleware for the specified routes when the `DELETE`, * `GET`, `POST`, or `PUT` method is requested. */ all< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( name: string, path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `DELETE`, * `GET`, `POST`, or `PUT` method is requested. */ all< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `DELETE`, * `GET`, `POST`, or `PUT` method is requested with explicit path parameters. */ all< P extends RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)>; all< P extends RouteParams<string> = RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)> { this.#useVerb( nameOrPath, pathOrMiddleware as (string | RouterMiddleware<string>), middleware as RouterMiddleware<string>[], this.#methods.filter((method) => method !== "OPTIONS"), ); return this; } /** Middleware that handles requests for HTTP methods registered with the * router. If none of the routes handle a method, then "not allowed" logic * will be used. If a method is supported by some routes, but not the * particular matched router, then "not implemented" will be returned. * * The middleware will also automatically handle the `OPTIONS` method, * responding with a `200 OK` when the `Allowed` header sent to the allowed * methods for a given route. * * By default, a "not allowed" request will respond with a `405 Not Allowed` * and a "not implemented" will respond with a `501 Not Implemented`. Setting * the option `.throw` to `true` will cause the middleware to throw an * `HTTPError` instead of setting the response status. The error can be * overridden by providing a `.notImplemented` or `.notAllowed` method in the * options, of which the value will be returned will be thrown instead of the * HTTP error. */ allowedMethods( options: RouterAllowedMethodsOptions = {}, ): Middleware { const implemented = this.#methods; const allowedMethods: Middleware = async (context, next) => { const ctx = context as RouterContext<string>; await next(); if (!ctx.response.status || ctx.response.status === Status.NotFound) { assert(ctx.matched); const allowed = new Set<HTTPMethods>(); for (const route of ctx.matched) { for (const method of route.methods) { allowed.add(method); } } const allowedStr = [...allowed].join(", "); if (!implemented.includes(ctx.request.method)) { if (options.throw) { throw options.notImplemented ? options.notImplemented() : new errors.NotImplemented(); } else { ctx.response.status = Status.NotImplemented; ctx.response.headers.set("Allow", allowedStr); } } else if (allowed.size) { if (ctx.request.method === "OPTIONS") { ctx.response.status = Status.OK; ctx.response.headers.set("Allow", allowedStr); } else if (!allowed.has(ctx.request.method)) { if (options.throw) { throw options.methodNotAllowed ? options.methodNotAllowed() : new errors.MethodNotAllowed(); } else { ctx.response.status = Status.MethodNotAllowed; ctx.response.headers.set("Allow", allowedStr); } } } } }; return allowedMethods; } /** Register named middleware for the specified routes when the `DELETE`, * method is requested. */ delete< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( name: string, path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `DELETE`, * method is requested. */ delete< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `DELETE`, * method is requested with explicit path parameters. */ delete< P extends RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)>; delete< P extends RouteParams<string> = RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)> { this.#useVerb( nameOrPath, pathOrMiddleware as (string | RouterMiddleware<string>), middleware as RouterMiddleware<string>[], ["DELETE"], ); return this; } /** Iterate over the routes currently added to the router. To be compatible * with the iterable interfaces, both the key and value are set to the value * of the route. */ *entries(): IterableIterator<[Route<string>, Route<string>]> { for (const route of this.#stack) { const value = route.toJSON(); yield [value, value]; } } /** Iterate over the routes currently added to the router, calling the * `callback` function for each value. */ forEach( callback: ( value1: Route<string>, value2: Route<string>, router: this, ) => void, // deno-lint-ignore no-explicit-any thisArg: any = null, ): void { for (const route of this.#stack) { const value = route.toJSON(); callback.call(thisArg, value, value, this); } } /** Register named middleware for the specified routes when the `GET`, * method is requested. */ get< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( name: string, path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `GET`, * method is requested. */ get< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `GET`, * method is requested with explicit path parameters. */ get< P extends RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)>; get< P extends RouteParams<string> = RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)> { this.#useVerb( nameOrPath, pathOrMiddleware as (string | RouterMiddleware<string>), middleware as RouterMiddleware<string>[], ["GET"], ); return this; } /** Register named middleware for the specified routes when the `HEAD`, * method is requested. */ head< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( name: string, path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `HEAD`, * method is requested. */ head< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `HEAD`, * method is requested with explicit path parameters. */ head< P extends RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)>; head< P extends RouteParams<string> = RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)> { this.#useVerb( nameOrPath, pathOrMiddleware as (string | RouterMiddleware<string>), middleware as RouterMiddleware<string>[], ["HEAD"], ); return this; } /** Iterate over the routes currently added to the router. To be compatible * with the iterable interfaces, the key is set to the value of the route. */ *keys(): IterableIterator<Route<string>> { for (const route of this.#stack) { yield route.toJSON(); } } /** Register named middleware for the specified routes when the `OPTIONS`, * method is requested. */ options< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( name: string, path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `OPTIONS`, * method is requested. */ options< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `OPTIONS`, * method is requested with explicit path parameters. */ options< P extends RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)>; options< P extends RouteParams<string> = RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)> { this.#useVerb( nameOrPath, pathOrMiddleware as (string | RouterMiddleware<string>), middleware as RouterMiddleware<string>[], ["OPTIONS"], ); return this; } /** Register param middleware, which will be called when the particular param * is parsed from the route. */ param<R extends string, S extends State = RS>( param: keyof RouteParams<R>, middleware: RouterParamMiddleware<R, RouteParams<R>, S>, ): Router<S> { this.#params[param as string] = middleware; for (const route of this.#stack) { route.param(param as string, middleware); } return this; } /** Register named middleware for the specified routes when the `PATCH`, * method is requested. */ patch< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( name: string, path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `PATCH`, * method is requested. */ patch< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `PATCH`, * method is requested with explicit path parameters. */ patch< P extends RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)>; patch< P extends RouteParams<string> = RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)> { this.#useVerb( nameOrPath, pathOrMiddleware as (string | RouterMiddleware<string>), middleware as RouterMiddleware<string>[], ["PATCH"], ); return this; } /** Register named middleware for the specified routes when the `POST`, * method is requested. */ post< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( name: string, path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `POST`, * method is requested. */ post< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `POST`, * method is requested with explicit path parameters. */ post< P extends RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)>; post< P extends RouteParams<string> = RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)> { this.#useVerb( nameOrPath, pathOrMiddleware as (string | RouterMiddleware<string>), middleware as RouterMiddleware<string>[], ["POST"], ); return this; } /** Set the router prefix for this router. */ prefix(prefix: string): this { prefix = prefix.replace(/\/$/, ""); this.#opts.prefix = prefix; for (const route of this.#stack) { route.setPrefix(prefix); } return this; } /** Register named middleware for the specified routes when the `PUT` * method is requested. */ put< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( name: string, path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `PUT` * method is requested. */ put< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware for the specified routes when the `PUT` * method is requested with explicit path parameters. */ put< P extends RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)>; put< P extends RouteParams<string> = RouteParams<string>, S extends State = RS, >( nameOrPath: string, pathOrMiddleware: string | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)> { this.#useVerb( nameOrPath, pathOrMiddleware as (string | RouterMiddleware<string>), middleware as RouterMiddleware<string>[], ["PUT"], ); return this; } /** Register a direction middleware, where when the `source` path is matched * the router will redirect the request to the `destination` path. A `status` * of `302 Found` will be set by default. * * The `source` and `destination` can be named routes. */ redirect( source: string, destination: string | URL, status: RedirectStatus = Status.Found, ): this { if (source[0] !== "/") { const s = this.url(source); if (!s) { throw new RangeError(`Could not resolve named route: "${source}"`); } source = s; } if (typeof destination === "string") { if (destination[0] !== "/") { const d = this.url(destination); if (!d) { try { const url = new URL(destination); destination = url; } catch { throw new RangeError(`Could not resolve named route: "${source}"`); } } else { destination = d; } } } this.all(source, async (ctx, next) => { await next(); ctx.response.redirect(destination); ctx.response.status = status; }); return this; } /** Return middleware that will do all the route processing that the router * has been configured to handle. Typical usage would be something like this: * * ```ts * import { Application, Router } from "jsr:@oak/oak/"; * * const app = new Application(); * const router = new Router(); * * // register routes * * app.use(router.routes()); * app.use(router.allowedMethods()); * await app.listen({ port: 80 }); * ``` */ routes(): Middleware { const dispatch = ( context: Context, next: () => Promise<unknown>, ): Promise<unknown> => { const ctx = context as RouterContext<string>; let pathname: string; let method: HTTPMethods; try { const { url: { pathname: p }, method: m } = ctx.request; pathname = p; method = m; } catch (e) { return Promise.reject(e); } const path = this.#opts.routerPath ?? ctx.routerPath ?? decodeURI(pathname); const matches = this.#match(path, method); if (ctx.matched) { ctx.matched.push(...matches.path); } else { ctx.matched = [...matches.path]; } // deno-lint-ignore no-explicit-any ctx.router = this as Router<any>; if (!matches.route) return next(); ctx.routeName = matches.name; const { pathAndMethod: matchedRoutes } = matches; const chain = matchedRoutes.reduce( (prev, route) => [ ...prev, (ctx, next) => { ctx.captures = route.captures(path); ctx.params = route.params(ctx.captures, ctx.params); return next(); }, ...route.stack, ], [] as RouterMiddleware<string>[], ); return compose(chain)(ctx, next); }; dispatch.router = this; return dispatch; } /** Generate a URL pathname for a named route, interpolating the optional * params provided. Also accepts an optional set of options. */ url<P extends RouteParams<string> = RouteParams<string>>( name: string, params?: P, options?: UrlOptions, ): string | undefined { const route = this.#route(name); if (route) { return route.url(params, options); } } /** Register middleware to be used on every matched route. */ use< P extends RouteParams<string> = RouteParams<string>, S extends State = RS, >( middleware: RouterMiddleware<string, P, S>, ...middlewares: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware to be used on every route that matches the supplied * `path`. */ use< R extends string, P extends RouteParams<R> = RouteParams<R>, S extends State = RS, >( path: R, middleware: RouterMiddleware<R, P, S>, ...middlewares: RouterMiddleware<R, P, S>[] ): Router<S extends RS ? S : (S & RS)>; /** Register middleware to be used on every route that matches the supplied * `path` with explicit path parameters. */ use< P extends RouteParams<string>, S extends State = RS, >( path: string, middleware: RouterMiddleware<string, P, S>, ...middlewares: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)>; use< P extends RouteParams<string> = RouteParams<string>, S extends State = RS, >( path: string[], middleware: RouterMiddleware<string, P, S>, ...middlewares: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)>; use< P extends RouteParams<string> = RouteParams<string>, S extends State = RS, >( pathOrMiddleware: string | string[] | RouterMiddleware<string, P, S>, ...middleware: RouterMiddleware<string, P, S>[] ): Router<S extends RS ? S : (S & RS)> { let path: string | string[] | undefined; if ( typeof pathOrMiddleware === "string" || Array.isArray(pathOrMiddleware) ) { path = pathOrMiddleware; } else { middleware.unshift(pathOrMiddleware); } this.#register( path ?? "(.*)", middleware as RouterMiddleware<string>[], [], { end: false, ignoreCaptures: !path, ignorePrefix: !path }, ); return this; } /** Iterate over the routes currently added to the router. */ *values(): IterableIterator<Route<string, RouteParams<string>, RS>> { for (const route of this.#stack) { yield route.toJSON(); } } /** Provide an iterator interface that iterates over the routes registered * with the router. */ *[Symbol.iterator](): IterableIterator< Route<string, RouteParams<string>, RS> > { for (const route of this.#stack) { yield route.toJSON(); } } /** Generate a URL pathname based on the provided path, interpolating the * optional params provided. Also accepts an optional set of options. */ static url<R extends string>( path: R, params?: RouteParams<R>, options?: UrlOptions, ): string { return toUrl(path, params, options); } [Symbol.for("Deno.customInspect")]( inspect: (value: unknown) => string, ): string { return `${this.constructor.name} ${ inspect({ "#params": this.#params, "#stack": this.#stack }) }`; } [Symbol.for("nodejs.util.inspect.custom")]( depth: number, // deno-lint-ignore no-explicit-any options: any, inspect: (value: unknown, options?: unknown) => string, // deno-lint-ignore no-explicit-any ): any { if (depth < 0) { return options.stylize(`[${this.constructor.name}]`, "special"); } const newOptions = Object.assign({}, options, { depth: options.depth === null ? null : options.depth - 1, }); return `${options.stylize(this.constructor.name, "special")} ${ inspect( { "#params": this.#params, "#stack": this.#stack }, newOptions, ) }`; } }