Skip to main content
This release is 6 versions behind 17.1.3 — the latest version of @oak/oak. Jump to latest

@oak/oak@16.0.0
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
This package works with Cloudflare Workers
This package works with Node.js
This package works with Deno
This package works with Bun
JSR Score
100%
Published
7 months ago (16.0.0)
Package root>router.ts
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, ) }`; } }