weifuwu 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +47 -0
- package/README.md +316 -0
- package/compress.ts +69 -0
- package/cookie.ts +58 -0
- package/index.ts +19 -0
- package/middleware.ts +178 -0
- package/package.json +21 -0
- package/rate-limit.ts +68 -0
- package/router.ts +702 -0
- package/serve.ts +126 -0
- package/static.ts +113 -0
- package/test/compress.test.ts +106 -0
- package/test/cookie.test.ts +79 -0
- package/test/middleware.test.ts +407 -0
- package/test/rate-limit.test.ts +94 -0
- package/test/static.test.ts +93 -0
- package/test/unode.test.ts +401 -0
- package/test/upload.test.ts +130 -0
- package/test/validate.test.ts +133 -0
- package/types.ts +23 -0
- package/upload.ts +101 -0
- package/validate.ts +88 -0
package/router.ts
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { WebSocketServer, type WebSocket } from 'ws'
|
|
2
|
+
import type { IncomingMessage } from 'node:http'
|
|
3
|
+
import type { Duplex } from 'node:stream'
|
|
4
|
+
import { buildSchema, graphql, type GraphQLSchema } from 'graphql'
|
|
5
|
+
import { makeExecutableSchema } from '@graphql-tools/schema'
|
|
6
|
+
import { streamText } from 'ai'
|
|
7
|
+
import type { Context, Handler, Middleware, ErrorHandler } from './types.ts'
|
|
8
|
+
|
|
9
|
+
type StreamTextParams = Parameters<typeof streamText>[0]
|
|
10
|
+
|
|
11
|
+
export type WebSocketHandler = {
|
|
12
|
+
open?: (ws: WebSocket, ctx: Context) => void | Promise<void>
|
|
13
|
+
message?: (ws: WebSocket, ctx: Context, data: string | Buffer) => void | Promise<void>
|
|
14
|
+
close?: (ws: WebSocket, ctx: Context) => void | Promise<void>
|
|
15
|
+
error?: (ws: WebSocket, ctx: Context, error: Error) => void | Promise<void>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type AIOptions = Omit<StreamTextParams, 'model'> & {
|
|
19
|
+
model: string | (() => any)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type AIHandler = (
|
|
23
|
+
req: Request,
|
|
24
|
+
ctx: Context,
|
|
25
|
+
) => AIOptions | Promise<AIOptions>
|
|
26
|
+
|
|
27
|
+
export type GraphQLOptions = {
|
|
28
|
+
schema: string | GraphQLSchema
|
|
29
|
+
rootValue?: any
|
|
30
|
+
resolvers?: any
|
|
31
|
+
context?: (req: Request, ctx: Context) => Record<string, any> | Promise<Record<string, any>>
|
|
32
|
+
graphiql?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type TrieNode = {
|
|
36
|
+
children: Map<string, TrieNode>
|
|
37
|
+
handlers: Map<string, Handler>
|
|
38
|
+
middlewares: Map<string, Middleware[]>
|
|
39
|
+
param?: string
|
|
40
|
+
wildcard?: boolean
|
|
41
|
+
pathMws: Middleware[]
|
|
42
|
+
subRouter?: Router
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type WsTrieNode = {
|
|
46
|
+
children: Map<string, WsTrieNode>
|
|
47
|
+
handler?: WebSocketHandler
|
|
48
|
+
middlewares: Middleware[]
|
|
49
|
+
param?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const createTrieNode = (): TrieNode => ({
|
|
53
|
+
children: new Map(),
|
|
54
|
+
handlers: new Map(),
|
|
55
|
+
middlewares: new Map(),
|
|
56
|
+
pathMws: [],
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const createWsNode = (): WsTrieNode => ({
|
|
60
|
+
children: new Map(),
|
|
61
|
+
middlewares: [],
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const getTrieNode = (node: TrieNode, segment: string): TrieNode => {
|
|
65
|
+
if (segment.startsWith(':')) {
|
|
66
|
+
if (!node.children.has(':')) {
|
|
67
|
+
const child = createTrieNode()
|
|
68
|
+
child.param = segment.slice(1)
|
|
69
|
+
node.children.set(':', child)
|
|
70
|
+
}
|
|
71
|
+
const child = node.children.get(':')!
|
|
72
|
+
if (child.param !== segment.slice(1)) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Param name conflict: ":${child.param}" already registered at this path position, cannot register ":"${segment.slice(1)}"`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
return child
|
|
78
|
+
}
|
|
79
|
+
if (!node.children.has(segment)) {
|
|
80
|
+
node.children.set(segment, createTrieNode())
|
|
81
|
+
}
|
|
82
|
+
return node.children.get(segment)!
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const matchTrieNode = (
|
|
86
|
+
node: TrieNode,
|
|
87
|
+
segment: string,
|
|
88
|
+
params: Record<string, string>,
|
|
89
|
+
): TrieNode | null => {
|
|
90
|
+
if (node.children.has(segment)) return node.children.get(segment)!
|
|
91
|
+
if (node.children.has(':')) {
|
|
92
|
+
const child = node.children.get(':')!
|
|
93
|
+
if (child.param) params[child.param] = segment
|
|
94
|
+
return child
|
|
95
|
+
}
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const getWsNode = (node: WsTrieNode, segment: string): WsTrieNode => {
|
|
100
|
+
if (segment.startsWith(':')) {
|
|
101
|
+
if (!node.children.has(':')) {
|
|
102
|
+
const child = createWsNode()
|
|
103
|
+
child.param = segment.slice(1)
|
|
104
|
+
node.children.set(':', child)
|
|
105
|
+
}
|
|
106
|
+
const child = node.children.get(':')!
|
|
107
|
+
if (child.param !== segment.slice(1)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Param name conflict: ":${child.param}" already registered at this path position`,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
return child
|
|
113
|
+
}
|
|
114
|
+
if (!node.children.has(segment)) {
|
|
115
|
+
node.children.set(segment, createWsNode())
|
|
116
|
+
}
|
|
117
|
+
return node.children.get(segment)!
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const matchWsNode = (
|
|
121
|
+
node: WsTrieNode,
|
|
122
|
+
segment: string,
|
|
123
|
+
params: Record<string, string>,
|
|
124
|
+
): WsTrieNode | null => {
|
|
125
|
+
if (node.children.has(segment)) return node.children.get(segment)!
|
|
126
|
+
if (node.children.has(':')) {
|
|
127
|
+
const child = node.children.get(':')!
|
|
128
|
+
if (child.param) params[child.param] = segment
|
|
129
|
+
return child
|
|
130
|
+
}
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type WsMatchResult = {
|
|
135
|
+
handler: WebSocketHandler
|
|
136
|
+
middlewares: Middleware[]
|
|
137
|
+
params: Record<string, string>
|
|
138
|
+
} | null
|
|
139
|
+
|
|
140
|
+
type WsUpgradeHandler = (req: IncomingMessage, socket: Duplex, head: Buffer) => void
|
|
141
|
+
|
|
142
|
+
export class Router {
|
|
143
|
+
private root: TrieNode = createTrieNode()
|
|
144
|
+
private wsRoot: WsTrieNode = createWsNode()
|
|
145
|
+
private globalMws: Middleware[] = []
|
|
146
|
+
private errorHandler?: ErrorHandler
|
|
147
|
+
|
|
148
|
+
use(mw: Middleware): this
|
|
149
|
+
use(path: string, router: Router): this
|
|
150
|
+
use(path: string, mw: Middleware): this
|
|
151
|
+
use(arg1: string | Middleware, arg2?: Router | Middleware): this {
|
|
152
|
+
if (typeof arg1 === 'string') {
|
|
153
|
+
if (arg2 instanceof Router) {
|
|
154
|
+
let node = this.root
|
|
155
|
+
for (const segment of this.splitPath(arg1)) {
|
|
156
|
+
node = getTrieNode(node, segment)
|
|
157
|
+
}
|
|
158
|
+
node.subRouter = arg2
|
|
159
|
+
} else if (typeof arg2 === 'function') {
|
|
160
|
+
let node = this.root
|
|
161
|
+
for (const segment of this.splitPath(arg1)) {
|
|
162
|
+
node = getTrieNode(node, segment)
|
|
163
|
+
}
|
|
164
|
+
node.pathMws.push(arg2)
|
|
165
|
+
}
|
|
166
|
+
} else if (typeof arg1 === 'function') {
|
|
167
|
+
this.globalMws.push(arg1)
|
|
168
|
+
}
|
|
169
|
+
return this
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
get(path: string, ...args: [...Middleware[], Handler]): this {
|
|
173
|
+
return this.route('GET', path, ...args)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
post(path: string, ...args: [...Middleware[], Handler]): this {
|
|
177
|
+
return this.route('POST', path, ...args)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
put(path: string, ...args: [...Middleware[], Handler]): this {
|
|
181
|
+
return this.route('PUT', path, ...args)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
delete(path: string, ...args: [...Middleware[], Handler]): this {
|
|
185
|
+
return this.route('DELETE', path, ...args)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
patch(path: string, ...args: [...Middleware[], Handler]): this {
|
|
189
|
+
return this.route('PATCH', path, ...args)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
head(path: string, ...args: [...Middleware[], Handler]): this {
|
|
193
|
+
return this.route('HEAD', path, ...args)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
options(path: string, ...args: [...Middleware[], Handler]): this {
|
|
197
|
+
return this.route('OPTIONS', path, ...args)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
all(path: string, ...args: [...Middleware[], Handler]): this {
|
|
201
|
+
return this.route('*', path, ...args)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
onError(handler: ErrorHandler): this {
|
|
205
|
+
this.errorHandler = handler
|
|
206
|
+
return this
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
route(method: string, path: string, ...args: [...Middleware[], Handler]): this {
|
|
210
|
+
const handler = args.pop()! as Handler
|
|
211
|
+
const middlewares = args as Middleware[]
|
|
212
|
+
const segments = this.splitPath(path)
|
|
213
|
+
let node = this.root
|
|
214
|
+
|
|
215
|
+
for (const segment of segments) {
|
|
216
|
+
if (segment === '*') {
|
|
217
|
+
node.wildcard = true
|
|
218
|
+
node.handlers.set(method, handler)
|
|
219
|
+
if (middlewares.length > 0) node.middlewares.set(method, middlewares)
|
|
220
|
+
return this
|
|
221
|
+
}
|
|
222
|
+
node = getTrieNode(node, segment)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
node.handlers.set(method, handler)
|
|
226
|
+
if (middlewares.length > 0) node.middlewares.set(method, middlewares)
|
|
227
|
+
return this
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
ws(path: string, ...args: [...Middleware[], WebSocketHandler]): this {
|
|
231
|
+
const handler = args.pop()! as WebSocketHandler
|
|
232
|
+
const middlewares = args as Middleware[]
|
|
233
|
+
const segments = this.splitPath(path)
|
|
234
|
+
let node = this.wsRoot
|
|
235
|
+
|
|
236
|
+
for (const segment of segments) {
|
|
237
|
+
node = getWsNode(node, segment)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
node.handler = handler
|
|
241
|
+
if (middlewares.length > 0) node.middlewares = middlewares
|
|
242
|
+
return this
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
graphql(path: string, ...args: [...Middleware[], GraphQLOptions]): this {
|
|
246
|
+
const options = args.pop()! as GraphQLOptions
|
|
247
|
+
const middlewares = args as Middleware[]
|
|
248
|
+
const schema: GraphQLSchema =
|
|
249
|
+
typeof options.schema === 'string'
|
|
250
|
+
? options.resolvers
|
|
251
|
+
? makeExecutableSchema({
|
|
252
|
+
typeDefs: options.schema,
|
|
253
|
+
resolvers: options.resolvers,
|
|
254
|
+
})
|
|
255
|
+
: buildSchema(options.schema)
|
|
256
|
+
: options.schema
|
|
257
|
+
|
|
258
|
+
const handler: Handler = (req, ctx) => {
|
|
259
|
+
const url = new URL(req.url)
|
|
260
|
+
|
|
261
|
+
if (
|
|
262
|
+
options.graphiql &&
|
|
263
|
+
req.method === 'GET' &&
|
|
264
|
+
!url.searchParams.has('query')
|
|
265
|
+
) {
|
|
266
|
+
return new Response(getGraphiQLHtml(url.pathname), {
|
|
267
|
+
status: 200,
|
|
268
|
+
headers: { 'Content-Type': 'text/html' },
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (req.method !== 'GET' && req.method !== 'POST') {
|
|
273
|
+
return new Response('Not Found', { status: 404 })
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const paramsPromise =
|
|
277
|
+
req.method === 'GET'
|
|
278
|
+
? Promise.resolve(parseGraphQLParamsFromGet(url))
|
|
279
|
+
: parseGraphQLParamsFromPost(req)
|
|
280
|
+
|
|
281
|
+
return paramsPromise.then((params) => {
|
|
282
|
+
if (!params) {
|
|
283
|
+
return Response.json(
|
|
284
|
+
{ errors: [{ message: 'Missing query' }] },
|
|
285
|
+
{ status: 400 },
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
return executeGraphQLQuery(schema, params, options, req, ctx)
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return this.all(path, ...middlewares, handler)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
ai(path: string, ...args: [...Middleware[], AIHandler]): this {
|
|
296
|
+
const handler = args.pop()! as AIHandler
|
|
297
|
+
const middlewares = args as Middleware[]
|
|
298
|
+
|
|
299
|
+
const routeHandler: Handler = async (req, ctx) => {
|
|
300
|
+
const options = await handler(req, ctx)
|
|
301
|
+
const result = streamText(options as StreamTextParams)
|
|
302
|
+
return result.toTextStreamResponse()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return this.post(path, ...middlewares, routeHandler)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
handler(): Handler {
|
|
309
|
+
return (req, ctx) => {
|
|
310
|
+
const url = new URL(req.url)
|
|
311
|
+
return this.handle(req, ctx, this.splitPath(url.pathname), Object.fromEntries(url.searchParams))
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
websocketHandler(): WsUpgradeHandler {
|
|
316
|
+
const wss = new WebSocketServer({ noServer: true })
|
|
317
|
+
const wsRoot = this.wsRoot
|
|
318
|
+
const router = this
|
|
319
|
+
|
|
320
|
+
return (req, socket, head) => {
|
|
321
|
+
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
322
|
+
const segments = url.pathname.split('/').filter(Boolean)
|
|
323
|
+
const query = Object.fromEntries(url.searchParams)
|
|
324
|
+
|
|
325
|
+
const match = router.matchWsTrie(wsRoot, segments)
|
|
326
|
+
if (!match) {
|
|
327
|
+
socket.destroy()
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const webReq = new Request(url.href, {
|
|
332
|
+
method: req.method ?? 'GET',
|
|
333
|
+
headers: Object.fromEntries(
|
|
334
|
+
Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : v ?? '']),
|
|
335
|
+
),
|
|
336
|
+
})
|
|
337
|
+
const ctx: Context = { params: match.params, query }
|
|
338
|
+
|
|
339
|
+
if (match.middlewares.length === 0) {
|
|
340
|
+
upgradeSocket(wss, req, socket, head, match.handler, ctx)
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let index = 0
|
|
345
|
+
const dispatch: Handler = async (innerReq, ctx) => {
|
|
346
|
+
if (index < match.middlewares.length) {
|
|
347
|
+
const mw = match.middlewares[index++]
|
|
348
|
+
return mw!(innerReq, ctx, dispatch)
|
|
349
|
+
}
|
|
350
|
+
return await new Promise<Response>((resolve) => {
|
|
351
|
+
upgradeSocket(wss, req, socket, head, match.handler, ctx)
|
|
352
|
+
resolve(new Response(null, { status: 101 }))
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
Promise.resolve(dispatch(webReq, ctx)).then((result) => {
|
|
357
|
+
if (result.status !== 101) {
|
|
358
|
+
sendHttpResponseOnSocket(socket, result)
|
|
359
|
+
}
|
|
360
|
+
}).catch(() => {
|
|
361
|
+
socket.destroy()
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private splitPath(path: string): string[] {
|
|
367
|
+
return path.split('/').filter(Boolean)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private matchTrie(
|
|
371
|
+
method: string,
|
|
372
|
+
segments: string[],
|
|
373
|
+
): {
|
|
374
|
+
handler?: Handler
|
|
375
|
+
middlewares: Middleware[]
|
|
376
|
+
pathMws: Middleware[]
|
|
377
|
+
params: Record<string, string>
|
|
378
|
+
subRouter?: { router: Router; remainingIdx: number }
|
|
379
|
+
} | null {
|
|
380
|
+
let node = this.root
|
|
381
|
+
const params: Record<string, string> = {}
|
|
382
|
+
const pathMws: Middleware[] = [...this.root.pathMws]
|
|
383
|
+
let wildcardHandler: Handler | null = null
|
|
384
|
+
let wildcardMws: Middleware[] = []
|
|
385
|
+
let wildcardIdx = -1
|
|
386
|
+
|
|
387
|
+
for (let i = 0; i < segments.length; i++) {
|
|
388
|
+
if (node.subRouter) {
|
|
389
|
+
return {
|
|
390
|
+
pathMws,
|
|
391
|
+
params,
|
|
392
|
+
middlewares: [],
|
|
393
|
+
subRouter: { router: node.subRouter, remainingIdx: i },
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
pathMws.push(...node.pathMws)
|
|
398
|
+
|
|
399
|
+
if (node.wildcard) {
|
|
400
|
+
const h = node.handlers.get(method) || node.handlers.get('*')
|
|
401
|
+
if (h) {
|
|
402
|
+
wildcardHandler = h
|
|
403
|
+
wildcardMws = node.middlewares.get(method) || node.middlewares.get('*') || []
|
|
404
|
+
wildcardIdx = i
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const segment = segments[i]
|
|
409
|
+
if (!segment) break
|
|
410
|
+
|
|
411
|
+
const next = matchTrieNode(node, segment, params)
|
|
412
|
+
if (!next) {
|
|
413
|
+
if (wildcardHandler) {
|
|
414
|
+
params['*'] = segments.slice(wildcardIdx).join('/')
|
|
415
|
+
return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params }
|
|
416
|
+
}
|
|
417
|
+
return null
|
|
418
|
+
}
|
|
419
|
+
node = next
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (node.subRouter) {
|
|
423
|
+
return {
|
|
424
|
+
pathMws,
|
|
425
|
+
params,
|
|
426
|
+
middlewares: [],
|
|
427
|
+
subRouter: { router: node.subRouter, remainingIdx: segments.length },
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
pathMws.push(...node.pathMws)
|
|
432
|
+
|
|
433
|
+
const handler = node.handlers.get(method) || node.handlers.get('*')
|
|
434
|
+
if (handler) {
|
|
435
|
+
if (node.wildcard) params['*'] = segments.slice(segments.length).join('/')
|
|
436
|
+
return {
|
|
437
|
+
handler,
|
|
438
|
+
middlewares: node.middlewares.get(method) || node.middlewares.get('*') || [],
|
|
439
|
+
pathMws,
|
|
440
|
+
params,
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (wildcardHandler) {
|
|
445
|
+
params['*'] = segments.slice(wildcardIdx).join('/')
|
|
446
|
+
return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params }
|
|
447
|
+
}
|
|
448
|
+
return null
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private matchWsTrie(root: WsTrieNode, segments: string[]): WsMatchResult {
|
|
452
|
+
let node = root
|
|
453
|
+
const params: Record<string, string> = {}
|
|
454
|
+
|
|
455
|
+
for (const segment of segments) {
|
|
456
|
+
const next = matchWsNode(node, segment, params)
|
|
457
|
+
if (!next) return null
|
|
458
|
+
node = next
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return node.handler
|
|
462
|
+
? { handler: node.handler, middlewares: node.middlewares, params }
|
|
463
|
+
: null
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private async handle(
|
|
467
|
+
req: Request,
|
|
468
|
+
ctx: Context,
|
|
469
|
+
segments: string[],
|
|
470
|
+
query: Record<string, string>,
|
|
471
|
+
): Promise<Response> {
|
|
472
|
+
const match = this.matchTrie(req.method, segments)
|
|
473
|
+
|
|
474
|
+
if (match?.subRouter) {
|
|
475
|
+
const { router: sub, remainingIdx } = match.subRouter
|
|
476
|
+
const remainingSegments = segments.slice(remainingIdx)
|
|
477
|
+
const delegate: Handler = (req, ctx) =>
|
|
478
|
+
sub.handle(req, ctx, remainingSegments, query)
|
|
479
|
+
|
|
480
|
+
const allMws = this.globalMws.length + match.pathMws.length === 0
|
|
481
|
+
? [] as Middleware[]
|
|
482
|
+
: [...this.globalMws, ...match.pathMws]
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
return await this.runChain(allMws, delegate, req, { ...ctx, params: { ...ctx.params, ...match.params } })
|
|
486
|
+
} catch (e) {
|
|
487
|
+
return this.errorHandler
|
|
488
|
+
? this.errorHandler(e as Error, req, ctx)
|
|
489
|
+
: new Response('Internal Server Error', { status: 500 })
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (match?.handler) {
|
|
494
|
+
const { handler, middlewares: routeMws, pathMws, params } = match
|
|
495
|
+
const allMws = this.globalMws.length + pathMws.length + routeMws.length === 0
|
|
496
|
+
? [] as Middleware[]
|
|
497
|
+
: [...this.globalMws, ...pathMws, ...routeMws]
|
|
498
|
+
const ctxWithMatch = { ...ctx, params: { ...ctx.params, ...params } }
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
return await this.runChain(allMws, handler, req, ctxWithMatch)
|
|
502
|
+
} catch (e) {
|
|
503
|
+
return this.errorHandler
|
|
504
|
+
? this.errorHandler(e as Error, req, ctxWithMatch)
|
|
505
|
+
: new Response('Internal Server Error', { status: 500 })
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (this.globalMws.length > 0) {
|
|
510
|
+
try {
|
|
511
|
+
const delegate: Handler = () => new Response('Not Found', { status: 404 })
|
|
512
|
+
return await this.runChain(this.globalMws, delegate, req, ctx)
|
|
513
|
+
} catch (e) {
|
|
514
|
+
return this.errorHandler
|
|
515
|
+
? this.errorHandler(e as Error, req, ctx)
|
|
516
|
+
: new Response('Internal Server Error', { status: 500 })
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return new Response('Not Found', { status: 404 })
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private async runChain(
|
|
524
|
+
middlewares: Middleware[],
|
|
525
|
+
finalHandler: Handler,
|
|
526
|
+
req: Request,
|
|
527
|
+
ctx: Context,
|
|
528
|
+
): Promise<Response> {
|
|
529
|
+
let index = 0
|
|
530
|
+
const dispatch: Handler = async (req, ctx) => {
|
|
531
|
+
if (index < middlewares.length) {
|
|
532
|
+
const mw = middlewares[index++]
|
|
533
|
+
return mw
|
|
534
|
+
? await mw(req, ctx, dispatch)
|
|
535
|
+
: new Response('Middleware error', { status: 500 })
|
|
536
|
+
}
|
|
537
|
+
return await finalHandler(req, ctx)
|
|
538
|
+
}
|
|
539
|
+
return dispatch(req, ctx)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function upgradeSocket(
|
|
544
|
+
wss: WebSocketServer,
|
|
545
|
+
req: IncomingMessage,
|
|
546
|
+
socket: Duplex,
|
|
547
|
+
head: Buffer,
|
|
548
|
+
handler: WebSocketHandler,
|
|
549
|
+
ctx: Context,
|
|
550
|
+
): void {
|
|
551
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
552
|
+
if (handler.open) {
|
|
553
|
+
handler.open(ws, ctx)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
ws.on('message', (data) => {
|
|
557
|
+
handler.message?.(ws, ctx, data as string | Buffer)
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
ws.on('close', () => {
|
|
561
|
+
handler.close?.(ws, ctx)
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
ws.on('error', (err) => {
|
|
565
|
+
handler.error?.(ws, ctx, err)
|
|
566
|
+
})
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function sendHttpResponseOnSocket(socket: Duplex, response: Response): void {
|
|
571
|
+
const statusLine = `HTTP/1.1 ${response.status} ${response.statusText}`
|
|
572
|
+
const headerLines: string[] = [statusLine]
|
|
573
|
+
response.headers.forEach((value, key) => {
|
|
574
|
+
headerLines.push(`${key}: ${value}`)
|
|
575
|
+
})
|
|
576
|
+
headerLines.push('Connection: close')
|
|
577
|
+
headerLines.push('')
|
|
578
|
+
const headerStr = headerLines.join('\r\n')
|
|
579
|
+
|
|
580
|
+
response.arrayBuffer().then((buf) => {
|
|
581
|
+
const body = Buffer.from(buf)
|
|
582
|
+
socket.write(headerStr + '\r\n' + body.toString())
|
|
583
|
+
socket.end()
|
|
584
|
+
}).catch(() => {
|
|
585
|
+
socket.write(headerStr + '\r\n')
|
|
586
|
+
socket.end()
|
|
587
|
+
})
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
type GraphQLParams = {
|
|
591
|
+
query: string
|
|
592
|
+
variables: Record<string, any>
|
|
593
|
+
operationName?: string
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function parseGraphQLParamsFromGet(url: URL): GraphQLParams | null {
|
|
597
|
+
const query = url.searchParams.get('query')
|
|
598
|
+
if (!query) return null
|
|
599
|
+
const variablesStr = url.searchParams.get('variables')
|
|
600
|
+
let variables = {}
|
|
601
|
+
if (variablesStr) {
|
|
602
|
+
try {
|
|
603
|
+
variables = JSON.parse(variablesStr)
|
|
604
|
+
} catch {
|
|
605
|
+
return null
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
query,
|
|
610
|
+
variables,
|
|
611
|
+
operationName: url.searchParams.get('operationName') || undefined,
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function parseGraphQLParamsFromPost(req: Request): Promise<GraphQLParams | null> {
|
|
616
|
+
try {
|
|
617
|
+
const body = (await req.json()) as {
|
|
618
|
+
query?: string
|
|
619
|
+
variables?: Record<string, any>
|
|
620
|
+
operationName?: string
|
|
621
|
+
}
|
|
622
|
+
if (!body.query) return null
|
|
623
|
+
return {
|
|
624
|
+
query: body.query,
|
|
625
|
+
variables: body.variables || {},
|
|
626
|
+
operationName: body.operationName,
|
|
627
|
+
}
|
|
628
|
+
} catch {
|
|
629
|
+
return null
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function executeGraphQLQuery(
|
|
634
|
+
schema: GraphQLSchema,
|
|
635
|
+
params: GraphQLParams,
|
|
636
|
+
options: GraphQLOptions,
|
|
637
|
+
req: Request,
|
|
638
|
+
ctx: Context,
|
|
639
|
+
): Promise<Response> {
|
|
640
|
+
const contextValue = options.context ? await options.context(req, ctx) : ctx
|
|
641
|
+
const result = await graphql({
|
|
642
|
+
schema,
|
|
643
|
+
source: params.query,
|
|
644
|
+
rootValue: options.rootValue,
|
|
645
|
+
contextValue,
|
|
646
|
+
variableValues: params.variables,
|
|
647
|
+
operationName: params.operationName,
|
|
648
|
+
})
|
|
649
|
+
return Response.json(result, { status: result.errors ? 400 : 200 })
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function getGraphiQLHtml(endpoint: string): string {
|
|
653
|
+
return `<!doctype html>
|
|
654
|
+
<html lang="en">
|
|
655
|
+
<head>
|
|
656
|
+
<meta charset="UTF-8" />
|
|
657
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
658
|
+
<title>GraphiQL</title>
|
|
659
|
+
<style>
|
|
660
|
+
body { margin: 0; }
|
|
661
|
+
#graphiql { height: 100dvh; }
|
|
662
|
+
</style>
|
|
663
|
+
<link rel="stylesheet" href="https://esm.sh/graphiql@5.2.2/dist/style.css" />
|
|
664
|
+
<script type="importmap">
|
|
665
|
+
{
|
|
666
|
+
"imports": {
|
|
667
|
+
"react": "https://esm.sh/react@19.2.5",
|
|
668
|
+
"react/": "https://esm.sh/react@19.2.5/",
|
|
669
|
+
"react-dom": "https://esm.sh/react-dom@19.2.5",
|
|
670
|
+
"react-dom/": "https://esm.sh/react-dom@19.2.5/",
|
|
671
|
+
"graphiql": "https://esm.sh/graphiql@5.2.2?standalone&external=react,react-dom,@graphiql/react,graphql",
|
|
672
|
+
"graphiql/": "https://esm.sh/graphiql@5.2.2/",
|
|
673
|
+
"@graphiql/react": "https://esm.sh/@graphiql/react@0.37.3?standalone&external=react,react-dom,graphql,@graphiql/toolkit,@emotion/is-prop-valid",
|
|
674
|
+
"@graphiql/toolkit": "https://esm.sh/@graphiql/toolkit@0.11.3?standalone&external=graphql",
|
|
675
|
+
"graphql": "https://esm.sh/graphql@16.13.2",
|
|
676
|
+
"@emotion/is-prop-valid": "data:text/javascript,"
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
</script>
|
|
680
|
+
<script type="module">
|
|
681
|
+
import React from 'react';
|
|
682
|
+
import ReactDOM from 'react-dom/client';
|
|
683
|
+
import { GraphiQL } from 'graphiql';
|
|
684
|
+
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
|
685
|
+
import 'graphiql/setup-workers/esm.sh';
|
|
686
|
+
|
|
687
|
+
const fetcher = createGraphiQLFetcher({ url: "${endpoint}" });
|
|
688
|
+
|
|
689
|
+
function App() {
|
|
690
|
+
return React.createElement(GraphiQL, { fetcher });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const container = document.getElementById('graphiql');
|
|
694
|
+
const root = ReactDOM.createRoot(container);
|
|
695
|
+
root.render(React.createElement(App));
|
|
696
|
+
</script>
|
|
697
|
+
</head>
|
|
698
|
+
<body>
|
|
699
|
+
<div id="graphiql">Loading\u2026</div>
|
|
700
|
+
</body>
|
|
701
|
+
</html>`
|
|
702
|
+
}
|