-
-
Notifications
You must be signed in to change notification settings - Fork 535
/
hono.ts
210 lines (178 loc) · 6.31 KB
/
hono.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import { compose } from './compose.ts'
import type { Context } from './context.ts'
import { HonoContext } from './context.ts'
import { extendRequestPrototype } from './request.ts'
import type { Router } from './router.ts'
import { METHOD_NAME_ALL, METHOD_NAME_ALL_LOWERCASE } from './router.ts'
import { TrieRouter } from './router/trie-router/index.ts' // Default Router
import { getPathFromURL, mergePath } from './utils/url.ts'
type Env = Record<string, any>
export type Handler<RequestParamKeyType extends string = string, E = Env> = (
c: Context<RequestParamKeyType, E>,
next: Next
) => Response | Promise<Response> | Promise<void> | Promise<Response | undefined>
export type NotFoundHandler<E = Env> = (c: Context<string, E>) => Response | Promise<Response>
export type ErrorHandler<E = Env> = (err: Error, c: Context<string, E>) => Response
export type Next = () => Promise<void>
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ParamKeyName<NameWithPattern> = NameWithPattern extends `${infer Name}{${infer _Pattern}`
? Name
: NameWithPattern
type ParamKey<Component> = Component extends `:${infer NameWithPattern}`
? ParamKeyName<NameWithPattern>
: never
type ParamKeys<Path> = Path extends `${infer Component}/${infer Rest}`
? ParamKey<Component> | ParamKeys<Rest>
: ParamKey<Path>
interface HandlerInterface<T extends string, E extends Env = Env, U = Hono<E, T>> {
// app.get('/', handler, handler...)
<Path extends string>(
path: Path,
...handlers: Handler<ParamKeys<Path> extends never ? string : ParamKeys<Path>, E>[]
): U
(path: string, ...handlers: Handler<string, E>[]): U
// app.get(handler...)
<Path extends string>(
...handlers: Handler<ParamKeys<Path> extends never ? string : ParamKeys<Path>, E>[]
): U
(...handlers: Handler<string, E>[]): U
}
const methods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'] as const
type Methods = typeof methods[number] | typeof METHOD_NAME_ALL_LOWERCASE
function defineDynamicClass(): {
new <E extends Env, T extends string, U>(): {
[K in Methods]: HandlerInterface<T, E, U>
}
} {
return class {} as any
}
interface Route<E extends Env> {
path: string
method: string
handler: Handler<string, E>
}
export class Hono<E extends Env = Env, P extends string = '/'> extends defineDynamicClass()<
E,
P,
Hono<E, P>
> {
readonly router: Router<Handler<string, E>> = new TrieRouter()
readonly strict: boolean = true // strict routing - default is true
private _tempPath: string = ''
private path: string = '/'
routes: Route<E>[] = []
constructor(init: Partial<Pick<Hono, 'router' | 'strict'>> = {}) {
super()
extendRequestPrototype()
const allMethods = [...methods, METHOD_NAME_ALL_LOWERCASE]
allMethods.map((method) => {
this[method] = <Path extends string = ''>(
args1: Path | Handler<ParamKeys<Path>, E>,
...args: [Handler<ParamKeys<Path>, E>]
) => {
if (typeof args1 === 'string') {
this.path = args1
} else {
this.addRoute(method, this.path, args1)
}
args.map((handler) => {
if (typeof handler !== 'string') {
this.addRoute(method, this.path, handler)
}
})
return this
}
})
Object.assign(this, init)
}
private notFoundHandler: NotFoundHandler = (c: Context) => {
const message = '404 Not Found'
return c.text(message, 404)
}
private errorHandler: ErrorHandler = (err: Error, c: Context) => {
console.error(`${err.stack || err.message}`)
const message = 'Internal Server Error'
return c.text(message, 500)
}
route(path: string, app?: Hono<any>): Hono<E, P> {
this._tempPath = path
if (app) {
app.routes.map((r) => {
this.addRoute(r.method, r.path, r.handler)
})
this._tempPath = ''
}
return this
}
use(path: string, ...middleware: Handler<string, E>[]): Hono<E, P>
use(...middleware: Handler<string, E>[]): Hono<E, P>
use(arg1: string | Handler<string, E>, ...handlers: Handler<string, E>[]): Hono<E, P> {
if (typeof arg1 === 'string') {
this.path = arg1
} else {
handlers.unshift(arg1)
}
handlers.map((handler) => {
this.addRoute(METHOD_NAME_ALL, this.path, handler)
})
return this
}
onError(handler: ErrorHandler<E>): Hono<E, P> {
this.errorHandler = handler as ErrorHandler
return this
}
notFound(handler: NotFoundHandler<E>): Hono<E, P> {
this.notFoundHandler = handler as NotFoundHandler
return this
}
private addRoute(method: string, path: string, handler: Handler<string, E>): void {
method = method.toUpperCase()
if (this._tempPath) {
path = mergePath(this._tempPath, path)
}
this.router.add(method, path, handler)
const r: Route<E> = { path: path, method: method, handler: handler }
this.routes.push(r)
}
private matchRoute(method: string, path: string) {
return this.router.match(method, path)
}
private async dispatch(
request: Request,
eventOrExecutionCtx?: ExecutionContext | FetchEvent,
env?: E
): Promise<Response> {
const path = getPathFromURL(request.url, this.strict)
const method = request.method
const result = this.matchRoute(method, path)
request.paramData = result?.params
const handlers = result ? result.handlers : [this.notFoundHandler]
const c = new HonoContext<string, E>(request, env, eventOrExecutionCtx, this.notFoundHandler)
const composed = compose<HonoContext>(handlers, this.errorHandler, this.notFoundHandler)
let context: HonoContext
try {
context = await composed(c)
if (!context.finalized) {
throw new Error(
'Context is not finalized. You may forget returning Response object or `await next()`'
)
}
} catch (err) {
if (err instanceof Error) {
return this.errorHandler(err, c)
}
throw err
}
return context.res
}
handleEvent(event: FetchEvent): Promise<Response> {
return this.dispatch(event.request, event)
}
fetch = (request: Request, env?: E, executionCtx?: ExecutionContext) => {
return this.dispatch(request, executionCtx, env)
}
request(input: RequestInfo, requestInit?: RequestInit): Promise<Response> {
const req = input instanceof Request ? input : new Request(input, requestInit)
return this.dispatch(req)
}
}