0
0
mirror of https://github.com/honojs/hono.git synced 2024-12-01 10:51:01 +00:00
hono/deno_dist/context.ts

423 lines
11 KiB
TypeScript
Raw Normal View History

import { HonoRequest } from './request.ts'
import { FetchEventLike } from './types.ts'
import type { Env, NotFoundHandler, Input, TypedResponse } from './types.ts'
2022-07-10 15:17:29 +00:00
import type { CookieOptions } from './utils/cookie.ts'
import { serialize } from './utils/cookie.ts'
2022-07-02 06:09:45 +00:00
import type { StatusCode } from './utils/http-status.ts'
import type { JSONValue, InterfaceToType } from './utils/types.ts'
2022-07-02 06:09:45 +00:00
type Runtime = 'node' | 'deno' | 'bun' | 'workerd' | 'fastly' | 'edge-light' | 'lagon' | 'other'
type HeaderRecord = Record<string, string | string[]>
type Data = string | ArrayBuffer | ReadableStream
export interface ExecutionContext {
waitUntil(promise: Promise<unknown>): void
passThroughOnException(): void
}
export interface ContextVariableMap {}
2022-07-02 06:09:45 +00:00
interface Get<E extends Env> {
<Key extends keyof ContextVariableMap>(key: Key): ContextVariableMap[Key]
<Key extends keyof E['Variables']>(key: Key): E['Variables'][Key]
}
interface Set<E extends Env> {
<Key extends keyof ContextVariableMap>(key: Key, value: ContextVariableMap[Key]): void
<Key extends keyof E['Variables']>(key: Key, value: E['Variables'][Key]): void
}
interface NewResponse {
(data: Data | null, status?: StatusCode, headers?: HeaderRecord): Response
(data: Data | null, init?: ResponseInit): Response
}
interface BodyRespond extends NewResponse {}
interface TextRespond {
(text: string, status?: StatusCode, headers?: HeaderRecord): Response
(text: string, init?: ResponseInit): Response
}
interface JSONRespond {
<T = JSONValue>(object: T, status?: StatusCode, headers?: HeaderRecord): Response
<T = JSONValue>(object: T, init?: ResponseInit): Response
}
interface JSONTRespond {
<T>(
object: InterfaceToType<T> extends JSONValue ? T : JSONValue,
status?: StatusCode,
headers?: HeaderRecord
): TypedResponse<
InterfaceToType<T> extends JSONValue
? JSONValue extends InterfaceToType<T>
? never
: T
: never
>
<T>(
object: InterfaceToType<T> extends JSONValue ? T : JSONValue,
init?: ResponseInit
): TypedResponse<
InterfaceToType<T> extends JSONValue
? JSONValue extends InterfaceToType<T>
? never
: T
: never
>
}
interface HTMLRespond {
(html: string, status?: StatusCode, headers?: HeaderRecord): Response
(html: string, init?: ResponseInit): Response
}
type ContextOptions<E extends Env> = {
env: E['Bindings']
executionCtx?: FetchEventLike | ExecutionContext | undefined
notFoundHandler?: NotFoundHandler<E>
path?: string
params?: Record<string, string>
}
export class Context<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
E extends Env = any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
P extends string = any,
I extends Input = {}
> {
env: E['Bindings'] = {}
finalized: boolean = false
error: Error | undefined = undefined
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _req?: HonoRequest<any, any>
private _status: StatusCode = 200
private _exCtx: FetchEventLike | ExecutionContext | undefined // _executionCtx
2023-04-30 12:07:00 +00:00
private _pre: boolean = false // _pretty
private _preS: number = 2 // _prettySpace
private _map: Record<string, unknown> | undefined
2023-04-30 12:07:00 +00:00
private _h: Headers | undefined = undefined // _headers
private _pH: Record<string, string> | undefined = undefined // _preparedHeaders
2022-07-02 06:09:45 +00:00
private _res: Response | undefined
private _path: string = '/'
private _params?: Record<string, string> | null
private _init = true
private rawRequest?: Request | null
private notFoundHandler: NotFoundHandler<E> = () => new Response()
constructor(req: Request, options?: ContextOptions<E>) {
this.rawRequest = req
if (options) {
2023-04-30 12:07:00 +00:00
this._exCtx = options.executionCtx
this._path = options.path ?? '/'
this._params = options.params
this.env = options.env
if (options.notFoundHandler) {
this.notFoundHandler = options.notFoundHandler
}
}
}
get req(): HonoRequest<P, I['out']> {
if (this._req) {
return this._req
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this._req = new HonoRequest(this.rawRequest!, this._path, this._params!)
this.rawRequest = undefined
this._params = undefined
return this._req
}
2022-07-17 09:11:09 +00:00
}
get event(): FetchEventLike {
if (this._exCtx instanceof FetchEventLike) {
2023-04-30 12:07:00 +00:00
return this._exCtx
2022-07-02 06:09:45 +00:00
} else {
2022-07-17 09:11:09 +00:00
throw Error('This context has no FetchEvent')
2022-07-02 06:09:45 +00:00
}
2022-07-17 09:11:09 +00:00
}
2022-07-02 06:09:45 +00:00
2022-07-17 09:11:09 +00:00
get executionCtx(): ExecutionContext {
2023-04-30 12:07:00 +00:00
if (this._exCtx) {
return this._exCtx as ExecutionContext
2022-07-17 09:11:09 +00:00
} else {
throw Error('This context has no ExecutionContext')
}
2022-07-02 06:09:45 +00:00
}
get res(): Response {
this._init = false
return (this._res ||= new Response('404 Not Found', { status: 404 }))
2022-07-02 06:09:45 +00:00
}
2023-03-13 13:08:16 +00:00
set res(_res: Response | undefined) {
this._init = false
2023-03-13 13:08:16 +00:00
if (this._res && _res) {
this._res.headers.delete('content-type')
this._res.headers.forEach((v, k) => {
_res.headers.set(k, v)
})
}
2022-07-02 06:09:45 +00:00
this._res = _res
this.finalized = true
}
2023-05-05 02:46:10 +00:00
header = (name: string, value: string | undefined, options?: { append?: boolean }): void => {
// Clear the header
if (value === undefined) {
if (this._h) {
this._h.delete(name)
} else if (this._pH) {
delete this._pH[name.toLocaleLowerCase()]
}
if (this.finalized) {
this.res.headers.delete(name)
}
return
}
if (options?.append) {
2023-04-30 12:07:00 +00:00
if (!this._h) {
this._init = false
2023-04-30 12:07:00 +00:00
this._h = new Headers(this._pH)
this._pH = {}
}
2023-04-30 12:07:00 +00:00
this._h.append(name, value)
} else {
2023-04-30 12:07:00 +00:00
if (this._h) {
this._h.set(name, value)
} else {
2023-04-30 12:07:00 +00:00
this._pH ??= {}
this._pH[name.toLowerCase()] = value
}
}
2022-07-02 06:09:45 +00:00
if (this.finalized) {
if (options?.append) {
this.res.headers.append(name, value)
} else {
this.res.headers.set(name, value)
}
2022-07-02 06:09:45 +00:00
}
}
status = (status: StatusCode): void => {
2022-07-02 06:09:45 +00:00
this._status = status
}
set: Set<E> = (key: string, value: unknown) => {
2022-07-02 06:09:45 +00:00
this._map ||= {}
this._map[key as string] = value
2022-07-02 06:09:45 +00:00
}
get: Get<E> = (key: string) => {
return this._map ? this._map[key] : undefined
2022-07-02 06:09:45 +00:00
}
pretty = (prettyJSON: boolean, space: number = 2): void => {
2023-04-30 12:07:00 +00:00
this._pre = prettyJSON
this._preS = space
2022-07-02 06:09:45 +00:00
}
newResponse: NewResponse = (
data: Data | null,
arg?: StatusCode | ResponseInit,
headers?: HeaderRecord
): Response => {
// Optimized
if (this._init && !headers && !arg && this._status === 200) {
return new Response(data, {
2023-04-30 12:07:00 +00:00
headers: this._pH,
})
}
// Return Response immediately if arg is RequestInit.
if (arg && typeof arg !== 'number') {
const res = new Response(data, arg)
2023-04-30 12:07:00 +00:00
const contentType = this._pH?.['content-type']
if (contentType) {
res.headers.set('content-type', contentType)
}
return res
}
const status = arg ?? this._status
2023-04-30 12:07:00 +00:00
this._pH ??= {}
2023-04-30 12:07:00 +00:00
this._h ??= new Headers()
for (const [k, v] of Object.entries(this._pH)) {
this._h.set(k, v)
}
if (this._res) {
this._res.headers.forEach((v, k) => {
2023-04-30 12:07:00 +00:00
this._h?.set(k, v)
})
2023-04-30 12:07:00 +00:00
for (const [k, v] of Object.entries(this._pH)) {
this._h.set(k, v)
}
}
headers ??= {}
for (const [k, v] of Object.entries(headers)) {
if (typeof v === 'string') {
2023-04-30 12:07:00 +00:00
this._h.set(k, v)
} else {
2023-04-30 12:07:00 +00:00
this._h.delete(k)
for (const v2 of v) {
2023-04-30 12:07:00 +00:00
this._h.append(k, v2)
}
}
}
return new Response(data, {
status,
2023-04-30 12:07:00 +00:00
headers: this._h,
})
}
body: BodyRespond = (
data: Data | null,
arg?: StatusCode | RequestInit,
headers?: HeaderRecord
): Response => {
return typeof arg === 'number'
? this.newResponse(data, arg, headers)
: this.newResponse(data, arg)
}
text: TextRespond = (
text: string,
arg?: StatusCode | RequestInit,
headers?: HeaderRecord
): Response => {
// If the header is empty, return Response immediately.
// Content-Type will be added automatically as `text/plain`.
2023-04-30 12:07:00 +00:00
if (!this._pH) {
if (this._init && !headers && !arg) {
return new Response(text)
}
2023-04-30 12:07:00 +00:00
this._pH = {}
}
// If Content-Type is not set, we don't have to set `text/plain`.
// Fewer the header values, it will be faster.
2023-04-30 12:07:00 +00:00
if (this._pH['content-type']) {
this._pH['content-type'] = 'text/plain; charset=UTF-8'
}
return typeof arg === 'number'
? this.newResponse(text, arg, headers)
: this.newResponse(text, arg)
2022-07-02 06:09:45 +00:00
}
json: JSONRespond = <T = {}>(
2023-02-01 15:25:58 +00:00
object: T,
arg?: StatusCode | RequestInit,
2023-02-01 15:25:58 +00:00
headers?: HeaderRecord
) => {
2023-04-30 12:07:00 +00:00
const body = this._pre ? JSON.stringify(object, null, this._preS) : JSON.stringify(object)
this._pH ??= {}
this._pH['content-type'] = 'application/json; charset=UTF-8'
return typeof arg === 'number'
? this.newResponse(body, arg, headers)
: this.newResponse(body, arg)
2022-07-02 06:09:45 +00:00
}
jsonT: JSONTRespond = <T>(
object: InterfaceToType<T> extends JSONValue ? T : JSONValue,
arg?: StatusCode | RequestInit,
headers?: HeaderRecord
): TypedResponse<
InterfaceToType<T> extends JSONValue
? JSONValue extends InterfaceToType<T>
? never
: T
: never
> => {
return {
response: typeof arg === 'number' ? this.json(object, arg, headers) : this.json(object, arg),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: object as any,
format: 'json',
}
}
html: HTMLRespond = (
html: string,
arg?: StatusCode | RequestInit,
headers?: HeaderRecord
): Response => {
2023-04-30 12:07:00 +00:00
this._pH ??= {}
this._pH['content-type'] = 'text/html; charset=UTF-8'
return typeof arg === 'number'
? this.newResponse(html, arg, headers)
: this.newResponse(html, arg)
}
redirect = (location: string, status: StatusCode = 302): Response => {
2023-04-30 12:07:00 +00:00
this._h ??= new Headers()
this._h.set('Location', location)
return this.newResponse(null, status)
2022-07-02 06:09:45 +00:00
}
/** @deprecated
* Use Cookie Middleware instead of `c.cookie()`. The `c.cookie()` will be removed in v4.
*
* @example
*
* import { setCookie } from 'hono/cookie'
* // ...
* app.get('/', (c) => {
* setCookie(c, 'key', 'value')
* //...
* })
*/
cookie = (name: string, value: string, opt?: CookieOptions): void => {
2022-07-10 15:17:29 +00:00
const cookie = serialize(name, value, opt)
this.header('set-cookie', cookie, { append: true })
2022-07-10 15:17:29 +00:00
}
notFound = (): Response | Promise<Response> => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return this.notFoundHandler(this)
2022-07-02 06:09:45 +00:00
}
get runtime(): Runtime {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const global = globalThis as any
if (global?.Deno !== undefined) {
return 'deno'
}
if (global?.Bun !== undefined) {
return 'bun'
}
if (typeof global?.WebSocketPair === 'function') {
return 'workerd'
}
if (typeof global?.EdgeRuntime === 'string') {
return 'edge-light'
}
if (global?.fastly !== undefined) {
2023-01-08 00:05:49 +00:00
return 'fastly'
}
if (global?.__lagon__ !== undefined) {
return 'lagon'
}
2023-01-08 00:05:49 +00:00
if (global?.process?.release?.name === 'node') {
return 'node'
}
return 'other'
}
2022-07-02 06:09:45 +00:00
}