weifuwu 0.25.2 → 0.27.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.
Files changed (208) hide show
  1. package/README.md +291 -2489
  2. package/ai/provider.ts +129 -0
  3. package/ai/stream.ts +63 -0
  4. package/cli.ts +55 -257
  5. package/core/cookie.ts +114 -0
  6. package/core/env.ts +142 -0
  7. package/core/logger.ts +72 -0
  8. package/core/router.ts +795 -0
  9. package/core/serve.ts +294 -0
  10. package/core/sse.ts +85 -0
  11. package/core/trace.ts +146 -0
  12. package/graphql.ts +267 -0
  13. package/hub.ts +133 -0
  14. package/index.ts +71 -0
  15. package/mailer.ts +81 -0
  16. package/middleware/compress.ts +103 -0
  17. package/middleware/cors.ts +81 -0
  18. package/middleware/csrf.ts +112 -0
  19. package/middleware/flash.ts +144 -0
  20. package/middleware/health.ts +44 -0
  21. package/middleware/helmet.ts +98 -0
  22. package/middleware/i18n.ts +175 -0
  23. package/middleware/rate-limit.ts +167 -0
  24. package/middleware/request-id.ts +60 -0
  25. package/middleware/static.ts +149 -0
  26. package/middleware/theme.ts +84 -0
  27. package/middleware/upload.ts +168 -0
  28. package/middleware/validate.ts +186 -0
  29. package/package.json +14 -36
  30. package/postgres/client.ts +132 -0
  31. package/postgres/index.ts +4 -0
  32. package/postgres/module.ts +37 -0
  33. package/postgres/schema/columns.ts +186 -0
  34. package/postgres/schema/index.ts +36 -0
  35. package/postgres/schema/sql.ts +39 -0
  36. package/postgres/schema/table.ts +548 -0
  37. package/postgres/schema/where.ts +99 -0
  38. package/postgres/types.ts +48 -0
  39. package/queue/cron.ts +90 -0
  40. package/queue/index.ts +654 -0
  41. package/queue/types.ts +60 -0
  42. package/redis/client.ts +24 -0
  43. package/{dist/redis/index.d.ts → redis/index.ts} +2 -2
  44. package/redis/types.ts +28 -0
  45. package/types.ts +78 -0
  46. package/cli/template/app.ts +0 -22
  47. package/cli/template/index.ts +0 -10
  48. package/cli/template/locales/en.json +0 -13
  49. package/cli/template/locales/zh-CN.json +0 -13
  50. package/cli/template/locales/zh-TW.json +0 -13
  51. package/cli/template/locales/zh.json +0 -13
  52. package/cli/template/ui/app/globals.css +0 -2
  53. package/cli/template/ui/app/layout.tsx +0 -15
  54. package/cli/template/ui/app/page.tsx +0 -124
  55. package/cli/template/ui/components/Greeting.tsx +0 -3
  56. package/dist/agent/client.d.ts +0 -2
  57. package/dist/agent/index.d.ts +0 -2
  58. package/dist/agent/rest.d.ts +0 -14
  59. package/dist/agent/run.d.ts +0 -19
  60. package/dist/agent/types.d.ts +0 -55
  61. package/dist/ai/provider.d.ts +0 -45
  62. package/dist/ai/utils.d.ts +0 -5
  63. package/dist/ai/workflow.d.ts +0 -17
  64. package/dist/ai-sdk.d.ts +0 -2
  65. package/dist/ai.d.ts +0 -13
  66. package/dist/analytics.d.ts +0 -45
  67. package/dist/auth.d.ts +0 -22
  68. package/dist/cache.d.ts +0 -74
  69. package/dist/cli.d.ts +0 -2
  70. package/dist/cli.js +0 -302
  71. package/dist/client-locale.d.ts +0 -25
  72. package/dist/client-pref.d.ts +0 -3
  73. package/dist/client-router.d.ts +0 -300
  74. package/dist/client-state.d.ts +0 -22
  75. package/dist/client-theme.d.ts +0 -36
  76. package/dist/compile.d.ts +0 -15
  77. package/dist/compress.d.ts +0 -20
  78. package/dist/cookie.d.ts +0 -36
  79. package/dist/cors.d.ts +0 -25
  80. package/dist/cron-utils.d.ts +0 -73
  81. package/dist/csrf.d.ts +0 -47
  82. package/dist/deploy/config.d.ts +0 -2
  83. package/dist/deploy/gateway.d.ts +0 -2
  84. package/dist/deploy/index.d.ts +0 -4
  85. package/dist/deploy/manager.d.ts +0 -16
  86. package/dist/deploy/process.d.ts +0 -14
  87. package/dist/deploy/types.d.ts +0 -53
  88. package/dist/env.d.ts +0 -69
  89. package/dist/error-boundary.d.ts +0 -2
  90. package/dist/flash.d.ts +0 -90
  91. package/dist/fts.d.ts +0 -36
  92. package/dist/graphql.d.ts +0 -16
  93. package/dist/head.d.ts +0 -6
  94. package/dist/health.d.ts +0 -24
  95. package/dist/helmet.d.ts +0 -33
  96. package/dist/html-shell.d.ts +0 -1
  97. package/dist/hub.d.ts +0 -37
  98. package/dist/i18n.d.ts +0 -39
  99. package/dist/iii/client.d.ts +0 -2
  100. package/dist/iii/index.d.ts +0 -4
  101. package/dist/iii/register-worker.d.ts +0 -9
  102. package/dist/iii/rest.d.ts +0 -3
  103. package/dist/iii/stream.d.ts +0 -82
  104. package/dist/iii/types.d.ts +0 -121
  105. package/dist/iii/worker.d.ts +0 -2
  106. package/dist/iii/ws.d.ts +0 -22
  107. package/dist/index.d.ts +0 -101
  108. package/dist/index.js +0 -12752
  109. package/dist/kb/index.d.ts +0 -3
  110. package/dist/kb/types.d.ts +0 -72
  111. package/dist/layout.d.ts +0 -2
  112. package/dist/live.d.ts +0 -7
  113. package/dist/logdb/client.d.ts +0 -2
  114. package/dist/logdb/index.d.ts +0 -2
  115. package/dist/logdb/rest.d.ts +0 -5
  116. package/dist/logdb/types.d.ts +0 -27
  117. package/dist/logger.d.ts +0 -16
  118. package/dist/mailer.d.ts +0 -51
  119. package/dist/mcp.d.ts +0 -34
  120. package/dist/messager/agent.d.ts +0 -11
  121. package/dist/messager/client.d.ts +0 -2
  122. package/dist/messager/index.d.ts +0 -2
  123. package/dist/messager/rest.d.ts +0 -15
  124. package/dist/messager/types.d.ts +0 -57
  125. package/dist/messager/ws.d.ts +0 -14
  126. package/dist/module-server.d.ts +0 -9
  127. package/dist/not-found.d.ts +0 -2
  128. package/dist/notifier/client.d.ts +0 -2
  129. package/dist/notifier/index.d.ts +0 -2
  130. package/dist/notifier/types.d.ts +0 -105
  131. package/dist/opencode/client.d.ts +0 -2
  132. package/dist/opencode/index.d.ts +0 -2
  133. package/dist/opencode/permissions.d.ts +0 -5
  134. package/dist/opencode/prompt.d.ts +0 -8
  135. package/dist/opencode/rest.d.ts +0 -16
  136. package/dist/opencode/run.d.ts +0 -13
  137. package/dist/opencode/session.d.ts +0 -26
  138. package/dist/opencode/skills.d.ts +0 -4
  139. package/dist/opencode/tools/bash.d.ts +0 -6
  140. package/dist/opencode/tools/edit.d.ts +0 -19
  141. package/dist/opencode/tools/glob.d.ts +0 -9
  142. package/dist/opencode/tools/grep.d.ts +0 -17
  143. package/dist/opencode/tools/index.d.ts +0 -12
  144. package/dist/opencode/tools/question.d.ts +0 -5
  145. package/dist/opencode/tools/read.d.ts +0 -16
  146. package/dist/opencode/tools/skill.d.ts +0 -18
  147. package/dist/opencode/tools/web.d.ts +0 -18
  148. package/dist/opencode/tools/write.d.ts +0 -13
  149. package/dist/opencode/types.d.ts +0 -90
  150. package/dist/opencode/ws.d.ts +0 -21
  151. package/dist/permissions.d.ts +0 -51
  152. package/dist/postgres/client.d.ts +0 -4
  153. package/dist/postgres/index.d.ts +0 -4
  154. package/dist/postgres/module.d.ts +0 -17
  155. package/dist/postgres/schema/columns.d.ts +0 -99
  156. package/dist/postgres/schema/index.d.ts +0 -6
  157. package/dist/postgres/schema/sql.d.ts +0 -22
  158. package/dist/postgres/schema/table.d.ts +0 -141
  159. package/dist/postgres/schema/where.d.ts +0 -29
  160. package/dist/postgres/types.d.ts +0 -50
  161. package/dist/queue/index.d.ts +0 -2
  162. package/dist/queue/types.d.ts +0 -62
  163. package/dist/rate-limit.d.ts +0 -45
  164. package/dist/react.d.ts +0 -14
  165. package/dist/react.js +0 -751
  166. package/dist/redis/client.d.ts +0 -2
  167. package/dist/redis/types.d.ts +0 -18
  168. package/dist/request-id.d.ts +0 -40
  169. package/dist/router.d.ts +0 -73
  170. package/dist/s3.d.ts +0 -68
  171. package/dist/seo.d.ts +0 -104
  172. package/dist/serve.d.ts +0 -38
  173. package/dist/server-registry.d.ts +0 -10
  174. package/dist/session.d.ts +0 -117
  175. package/dist/sse.d.ts +0 -47
  176. package/dist/ssr-entries.d.ts +0 -4
  177. package/dist/ssr.d.ts +0 -11
  178. package/dist/static.d.ts +0 -23
  179. package/dist/stream.d.ts +0 -24
  180. package/dist/tailwind.d.ts +0 -15
  181. package/dist/tenant/client.d.ts +0 -2
  182. package/dist/tenant/graphql.d.ts +0 -3
  183. package/dist/tenant/index.d.ts +0 -2
  184. package/dist/tenant/rest.d.ts +0 -3
  185. package/dist/tenant/schema.d.ts +0 -5
  186. package/dist/tenant/types.d.ts +0 -48
  187. package/dist/tenant/utils.d.ts +0 -9
  188. package/dist/test-utils.d.ts +0 -194
  189. package/dist/theme.d.ts +0 -31
  190. package/dist/trace.d.ts +0 -95
  191. package/dist/tsx-context.d.ts +0 -32
  192. package/dist/types.d.ts +0 -47
  193. package/dist/upload.d.ts +0 -55
  194. package/dist/use-action.d.ts +0 -42
  195. package/dist/use-agent-stream.d.ts +0 -49
  196. package/dist/use-flash-message.d.ts +0 -17
  197. package/dist/use-websocket.d.ts +0 -42
  198. package/dist/user/client.d.ts +0 -30
  199. package/dist/user/index.d.ts +0 -2
  200. package/dist/user/oauth-login.d.ts +0 -21
  201. package/dist/user/oauth2.d.ts +0 -31
  202. package/dist/user/types.d.ts +0 -178
  203. package/dist/validate.d.ts +0 -32
  204. package/dist/vendor.d.ts +0 -7
  205. package/dist/webhook.d.ts +0 -79
  206. package/opencode/ui/app/globals.css +0 -1
  207. package/opencode/ui/app/layout.tsx +0 -13
  208. package/opencode/ui/app/page.tsx +0 -523
package/core/router.ts ADDED
@@ -0,0 +1,795 @@
1
+ /* eslint-disable no-console */
2
+ import { WebSocketServer } from 'ws'
3
+ import type {
4
+ WebSocket,
5
+ Context,
6
+ Handler,
7
+ Middleware,
8
+ MiddlewareMeta,
9
+ ErrorHandler,
10
+ } from '../types.ts'
11
+ import http, { type IncomingMessage } from 'node:http'
12
+ import type { Duplex } from 'node:stream'
13
+ import { createHub, type Hub } from '../hub.ts'
14
+
15
+ import { isProd } from './env.ts'
16
+
17
+ export type WebSocketHandler = {
18
+ open?: (ws: WebSocket, ctx: Context) => void | Promise<void>
19
+ message?: (ws: WebSocket, ctx: Context, data: string | Buffer) => void | Promise<void>
20
+ close?: (ws: WebSocket, ctx: Context) => void | Promise<void>
21
+ error?: (ws: WebSocket, ctx: Context, error: Error) => void | Promise<void>
22
+ }
23
+
24
+ type TrieNode = {
25
+ children: Map<string, TrieNode>
26
+ handlers: Map<string, Handler>
27
+ middlewares: Map<string, Middleware[]>
28
+ param?: string
29
+ wildcard?: boolean
30
+ pathMws: Middleware[]
31
+ }
32
+
33
+ type WsTrieNode = {
34
+ children: Map<string, WsTrieNode>
35
+ handler?: WebSocketHandler
36
+ middlewares: Middleware[]
37
+ param?: string
38
+ wildcard?: boolean
39
+ }
40
+
41
+ const createTrieNode = (): TrieNode => ({
42
+ children: new Map(),
43
+ handlers: new Map(),
44
+ middlewares: new Map(),
45
+ pathMws: [],
46
+ })
47
+
48
+ const createWsNode = (): WsTrieNode => ({
49
+ children: new Map(),
50
+ middlewares: [],
51
+ })
52
+
53
+ interface TrieNodeBase<T> {
54
+ children: Map<string, T>
55
+ param?: string
56
+ wildcard?: boolean
57
+ }
58
+
59
+ function createParamChild<T extends TrieNodeBase<T>>(
60
+ node: T,
61
+ segment: string,
62
+ createNode: () => T,
63
+ ): T {
64
+ const paramName = segment.slice(1)
65
+ if (!node.children.has(':')) {
66
+ const child = createNode()
67
+ child.param = paramName
68
+ node.children.set(':', child)
69
+ }
70
+ const child = node.children.get(':')!
71
+ if (child.param !== paramName) {
72
+ throw new Error(
73
+ `Param name conflict: ":${child.param}" already registered, cannot register ":"${paramName}"`,
74
+ )
75
+ }
76
+ return child
77
+ }
78
+
79
+ function getOrCreateChild<T extends TrieNodeBase<T>>(
80
+ node: T,
81
+ segment: string,
82
+ createNode: () => T,
83
+ allowWildcard: boolean,
84
+ ): T {
85
+ if (allowWildcard && segment === '*') {
86
+ node.wildcard = true
87
+ return node
88
+ }
89
+ if (segment.startsWith(':')) return createParamChild(node, segment, createNode)
90
+ if (!node.children.has(segment)) node.children.set(segment, createNode())
91
+ return node.children.get(segment)!
92
+ }
93
+
94
+ function matchChild<T extends TrieNodeBase<T>>(
95
+ node: T,
96
+ segment: string,
97
+ params: Record<string, string>,
98
+ allowWildcard = false,
99
+ ): T | null {
100
+ if (node.children.has(segment)) return node.children.get(segment)!
101
+ if (node.children.has(':')) {
102
+ const child = node.children.get(':')!
103
+ if (child.param) params[child.param] = segment
104
+ return child
105
+ }
106
+ if (allowWildcard && node.wildcard) return node
107
+ return null
108
+ }
109
+
110
+ type WsMatchResult = {
111
+ handler: WebSocketHandler
112
+ middlewares: Middleware[]
113
+ params: Record<string, string>
114
+ } | null
115
+
116
+ type WsUpgradeHandler = (req: IncomingMessage, socket: Duplex, head: Buffer) => void
117
+
118
+ // Router<T> — T accumulates types from global middleware calls via use(mw).
119
+ // Route-level middleware does not change the Router's type parameter.
120
+ export class Router<T extends Context = Context> {
121
+ private root: TrieNode = createTrieNode()
122
+ private wsRoot: WsTrieNode = createWsNode()
123
+ private globalMws: Middleware[] = []
124
+ private errorHandler?: ErrorHandler<T>
125
+ private _hasWildcard = false
126
+ private _hub?: Hub
127
+ private _wss?: WebSocketServer
128
+ /** Track which ctx fields have been injected so far (for dependency checking). */
129
+ private _ctxFields = new Set<string>()
130
+ private get wss(): WebSocketServer {
131
+ if (!this._wss) this._wss = new WebSocketServer({ noServer: true })
132
+ return this._wss
133
+ }
134
+
135
+ private get hub(): Hub {
136
+ if (!this._hub) this._hub = createHub()
137
+ return this._hub
138
+ }
139
+
140
+ /** Inject a custom hub (e.g. with Redis for cross-process broadcast). */
141
+ wsHub(hub: Hub): this {
142
+ this._hub = hub
143
+ return this
144
+ }
145
+
146
+ // Global middleware — accumulates types into Router<T>.
147
+ // The middleware's In type is Context (base); Out is what it injects.
148
+ // Router accumulates via intersection: Router<T & Out>
149
+ use<Out extends Context>(mw: Middleware<Context, Out>): Router<T & Out>
150
+ // Path-scoped middleware — does not accumulate
151
+ use(path: string, mw: Middleware<T, T>): Router<T>
152
+ // Mount sub-router — flattens into parent, does not accumulate
153
+ use(path: string, router: Router<Context>): Router<T>
154
+ // Module with .middleware() — auto-register middleware + mount at /
155
+ use(mod: Router & { middleware: () => Middleware }): Router<T>
156
+ use(
157
+ arg1: string | Middleware<Context, Context> | (Router & { middleware: () => Middleware }),
158
+ arg2?: Router<Context> | Middleware<T, T>,
159
+ ): Router<T> {
160
+ if (typeof arg1 === 'string') {
161
+ if (arg2 instanceof Router) {
162
+ this._mountRouter(arg1, arg2)
163
+ } else if (typeof arg2 === 'function') {
164
+ let node = this.root
165
+ for (const segment of this.splitPath(arg1)) {
166
+ node = getOrCreateChild(node, segment, createTrieNode, false)
167
+ }
168
+ node.pathMws.push(arg2 as unknown as Middleware)
169
+ this._checkMiddlewareMeta(arg2, `${arg1}`)
170
+ }
171
+ } else if (typeof arg1 === 'function') {
172
+ this.globalMws.push(arg1 as unknown as Middleware)
173
+ this._checkMiddlewareMeta(arg1, 'global')
174
+ } else if (
175
+ typeof arg1 === 'object' &&
176
+ arg1 !== null &&
177
+ 'middleware' in arg1 &&
178
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
179
+ typeof (arg1 as any).middleware === 'function' &&
180
+ arg1 instanceof Router
181
+ ) {
182
+ // Auto-register modules with .middleware() — e.g. theme(), i18n(), analytics()
183
+ // Registers both the middleware and mounts routes at /
184
+ const mod = arg1 as Router & { middleware: () => Middleware }
185
+ const mw = mod.middleware()
186
+ this.globalMws.push(mw as unknown as Middleware)
187
+ this._checkMiddlewareMeta(mw, 'global (auto-registered)')
188
+ this._mountRouter('/', mod as Router)
189
+ }
190
+ return this
191
+ }
192
+
193
+ /**
194
+ * Check a middleware's dependency metadata and emit warnings if
195
+ * required fields haven't been injected yet.
196
+ * Attach __meta to a middleware function:
197
+ *
198
+ * ```ts
199
+ * mw.__meta = { injects: ['sql'], depends: ['session'] }
200
+ * ```
201
+ */
202
+ private _checkMiddlewareMeta(mw: unknown, location: string): void {
203
+ const meta: MiddlewareMeta | undefined =
204
+ (mw as Middleware).__meta ??
205
+ (typeof mw === 'object' && mw && 'middleware' in mw
206
+ ? (mw as { middleware(): Middleware }).middleware().__meta
207
+ : undefined)
208
+ if (!meta) return
209
+
210
+ for (const dep of meta.depends) {
211
+ if (!this._ctxFields.has(dep)) {
212
+ console.warn(
213
+ `[weifuwu] Middleware at "${location}" depends on ctx.${dep} but it hasn't been registered yet.` +
214
+ `\n Register the provider before this middleware:` +
215
+ `\n app.use(${dep}()) // add before this middleware` +
216
+ `\n Current ctx fields: [${[...this._ctxFields].join(', ')}]`,
217
+ )
218
+ }
219
+ }
220
+
221
+ for (const field of meta.injects) {
222
+ this._ctxFields.add(field)
223
+ }
224
+ }
225
+
226
+ // Route registration — returns Router<T> unchanged.
227
+ // Route-level middleware and handlers get Context<T>.
228
+ get(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
229
+ return this._route('GET', path, ...args)
230
+ }
231
+
232
+ post(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
233
+ return this._route('POST', path, ...args)
234
+ }
235
+
236
+ put(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
237
+ return this._route('PUT', path, ...args)
238
+ }
239
+
240
+ delete(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
241
+ return this._route('DELETE', path, ...args)
242
+ }
243
+
244
+ patch(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
245
+ return this._route('PATCH', path, ...args)
246
+ }
247
+
248
+ head(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
249
+ return this._route('HEAD', path, ...args)
250
+ }
251
+
252
+ options(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
253
+ return this._route('OPTIONS', path, ...args)
254
+ }
255
+
256
+ all(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T> {
257
+ return this._route('*', path, ...args)
258
+ }
259
+
260
+ onError(handler: ErrorHandler<T>): Router<T> {
261
+ this.errorHandler = handler
262
+ return this
263
+ }
264
+
265
+ private _route(
266
+ method: string,
267
+ path: string,
268
+ ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]
269
+ ): Router<T> {
270
+ return this._routeImpl(method, path, args)
271
+ }
272
+
273
+ /** Internal route registration — no type constraints (used by _mountRouter). */
274
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
275
+ private _routeImpl(method: string, path: string, args: any[]): Router<T> {
276
+ const last = args[args.length - 1]
277
+ if (last instanceof Router) {
278
+ this._mountRouter(path, last, args.slice(0, -1))
279
+ return this
280
+ }
281
+ const handler = args.pop()
282
+ const middlewares: Middleware[] = args
283
+ const segments = this.splitPath(path)
284
+ let node = this.root
285
+
286
+ for (const segment of segments) {
287
+ if (segment === '*') {
288
+ this._hasWildcard = true
289
+ const remaining = segments.indexOf('*') < segments.length - 1
290
+ if (remaining) {
291
+ console.warn(`Route "${path}": segments after "*" are ignored`)
292
+ }
293
+ node.wildcard = true
294
+ node.handlers.set(method, handler)
295
+ if (middlewares.length > 0) node.middlewares.set(method, middlewares)
296
+ return this
297
+ }
298
+ node = getOrCreateChild(node, segment, createTrieNode, false)
299
+ }
300
+
301
+ if (!isProd() && node.handlers.has(method)) {
302
+ console.warn(`[router] route conflict: ${method} ${path} overwrites existing handler`)
303
+ }
304
+ node.handlers.set(method, handler)
305
+ if (middlewares.length > 0) node.middlewares.set(method, middlewares)
306
+ return this
307
+ }
308
+
309
+ ws(path: string, ...args: [...Middleware<T, T>[], WebSocketHandler]): Router<T> {
310
+ const handler = args.pop()! as WebSocketHandler
311
+ const middlewares = args as unknown as Middleware[]
312
+ const segments = this.splitPath(path)
313
+ let node = this.wsRoot
314
+
315
+ for (const segment of segments) {
316
+ node = getOrCreateChild(node, segment, createWsNode, true)
317
+ }
318
+
319
+ node.handler = handler
320
+ node.middlewares = middlewares
321
+ return this
322
+ }
323
+
324
+ handler(): Handler<T> {
325
+ return (req, ctx) => {
326
+ const url = new URL(req.url)
327
+ return this.handle(req, ctx as Context, this.splitPath(url.pathname))
328
+ }
329
+ }
330
+
331
+ /** Returns a human-readable list of all registered routes. Useful for debugging. */
332
+ routes(): string[] {
333
+ const result: string[] = []
334
+ if (this.globalMws.length > 0) {
335
+ result.push(`MIDDLEWARE [${this.globalMws.length} global]`)
336
+ }
337
+ this._collectRoutes(this.root, '', result)
338
+ this._collectWsRoutes(this.wsRoot, '', result)
339
+ return result
340
+ }
341
+
342
+ private _collectRoutes(node: TrieNode, prefix: string, result: string[]): void {
343
+ for (const [method] of node.handlers) {
344
+ const m = method === '*' ? 'ANY' : method
345
+ const path = (prefix || '/') + (node.wildcard ? '/*' : '')
346
+ const middlewares = node.middlewares.get(method)
347
+ const mwCount = middlewares ? ` (+${middlewares.length} mw)` : ''
348
+ result.push(`${m.padEnd(7)} ${path}${mwCount}`)
349
+ }
350
+ for (const [seg, child] of node.children) {
351
+ const segment = seg === ':' ? `:${child.param}` : seg
352
+ this._collectRoutes(child, prefix + '/' + segment, result)
353
+ }
354
+ }
355
+
356
+ private _collectWsRoutes(node: WsTrieNode, prefix: string, result: string[]): void {
357
+ if (node.handler) {
358
+ const path = prefix || '/'
359
+ const mwCount = node.middlewares.length ? ` (+${node.middlewares.length} mw)` : ''
360
+ result.push(`WS ${path}${mwCount}`)
361
+ }
362
+ for (const [seg, child] of node.children) {
363
+ const segment = seg === ':' ? `:${child.param}` : seg
364
+ this._collectWsRoutes(child, prefix + '/' + segment, result)
365
+ }
366
+ }
367
+
368
+ websocketHandler(): WsUpgradeHandler {
369
+ const wsRoot = this.wsRoot
370
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
371
+ const router = this
372
+
373
+ return (req, socket, head) => {
374
+ const url = new URL(req.url ?? '/', 'http://localhost')
375
+ const segments = url.pathname.split('/').filter(Boolean)
376
+
377
+ const match = router.matchWsTrie(wsRoot, segments)
378
+ if (!match) {
379
+ socket.destroy()
380
+ return
381
+ }
382
+
383
+ const query = Object.fromEntries(url.searchParams)
384
+ const ctx = { params: match.params, query } as Context
385
+
386
+ const allMws =
387
+ router.globalMws.length === 0 && match.middlewares.length === 0
388
+ ? ([] as Middleware[])
389
+ : [...router.globalMws, ...match.middlewares]
390
+
391
+ if (allMws.length === 0) {
392
+ upgradeSocket(router.wss, req, socket, head, match.handler, ctx, router.hub)
393
+ return
394
+ }
395
+
396
+ const finalHandler: Handler = () => {
397
+ try {
398
+ upgradeSocket(router.wss, req, socket, head, match.handler, ctx, router.hub)
399
+ } catch {
400
+ socket.destroy()
401
+ return new Response('WebSocket upgrade failed', { status: 500 })
402
+ }
403
+ return new Response(null, { status: 200 })
404
+ }
405
+
406
+ const webReq = new Request(url.href, {
407
+ method: req.method ?? 'GET',
408
+ headers: nodeReqHeadersToRecord(req.headers),
409
+ })
410
+
411
+ void router
412
+ .runChain(allMws, finalHandler, webReq, ctx)
413
+ .then((result) => {
414
+ if (result.status >= 400) {
415
+ sendHttpResponseOnSocket(socket, result)
416
+ }
417
+ })
418
+ .catch(() => {
419
+ socket.destroy()
420
+ })
421
+ }
422
+ }
423
+
424
+ private _mountRouter(prefix: string, sub: Router<Context>, extraMws: Middleware[] = []): void {
425
+ const base = prefix === '/' ? '' : prefix.replace(/\/$/, '')
426
+
427
+ const mountMw: Middleware = (req, ctx, next) => {
428
+ ctx.mountPath = (ctx.mountPath || '') + base
429
+ return next(req, ctx)
430
+ }
431
+
432
+ const allExtra =
433
+ extraMws.length === 0 && sub.globalMws.length === 0
434
+ ? [mountMw]
435
+ : [mountMw, ...extraMws, ...sub.globalMws]
436
+
437
+ const routes: Array<{
438
+ method: string
439
+ path: string
440
+ handler: Handler
441
+ middlewares: Middleware[]
442
+ }> = []
443
+ this._collect(sub.root, '', routes, [])
444
+ for (const { method, path, handler, middlewares } of routes) {
445
+ this._routeImpl(method, base + path, [...allExtra, ...middlewares, handler])
446
+ }
447
+
448
+ const wsRoutes: Array<{ path: string; handler: WebSocketHandler; middlewares: Middleware[] }> =
449
+ []
450
+ this._collectWs(sub.wsRoot, '', wsRoutes)
451
+ for (const { path, handler, middlewares } of wsRoutes) {
452
+ this.ws(
453
+ base + path,
454
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
455
+ ...(allExtra as Middleware<any, any>[]),
456
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
457
+ ...(middlewares as Middleware<any, any>[]),
458
+ handler,
459
+ )
460
+ }
461
+ }
462
+
463
+ private mergeMws(base: Middleware[], extra: Middleware[]): Middleware[] {
464
+ if (base.length === 0) return extra.length === 0 ? base : extra
465
+ if (extra.length === 0) return base
466
+ return [...base, ...extra]
467
+ }
468
+
469
+ private _collect(
470
+ node: TrieNode,
471
+ prefix: string,
472
+ result: Array<{ method: string; path: string; handler: Handler; middlewares: Middleware[] }>,
473
+ pathMwsAcc: Middleware[],
474
+ ): void {
475
+ const mws = this.mergeMws(pathMwsAcc, node.pathMws)
476
+ for (const [method, handler] of node.handlers) {
477
+ const rmws = node.middlewares.get(method) || []
478
+ const suffix = node.wildcard ? '/*' : ''
479
+ result.push({
480
+ method,
481
+ path: (prefix || '/') + suffix,
482
+ handler,
483
+ middlewares: this.mergeMws(mws, rmws),
484
+ })
485
+ }
486
+ for (const [seg, child] of node.children) {
487
+ const next = seg === ':' ? `/:${child.param}` : `/${seg}`
488
+ this._collect(child, prefix + next, result, mws)
489
+ }
490
+ }
491
+
492
+ private _collectWs(
493
+ node: WsTrieNode,
494
+ prefix: string,
495
+ result: Array<{ path: string; handler: WebSocketHandler; middlewares: Middleware[] }>,
496
+ pathMwsAcc: Middleware[] = [],
497
+ ): void {
498
+ const mws = this.mergeMws(pathMwsAcc, node.middlewares)
499
+ if (node.handler) result.push({ path: prefix || '/', handler: node.handler, middlewares: mws })
500
+ for (const [seg, child] of node.children) {
501
+ const next = seg === ':' ? `/:${child.param}` : `/${seg}`
502
+ this._collectWs(child, prefix + next, result, mws)
503
+ }
504
+ }
505
+
506
+ private splitPath(path: string): string[] {
507
+ return path.split('/').filter(Boolean)
508
+ }
509
+
510
+ private matchTrie(
511
+ method: string,
512
+ segments: string[],
513
+ ): {
514
+ handler?: Handler
515
+ middlewares: Middleware[]
516
+ pathMws: Middleware[]
517
+ params: Record<string, string>
518
+ allowedMethods?: string[]
519
+ } | null {
520
+ let node = this.root
521
+ const params: Record<string, string> = {}
522
+ const pathMws: Middleware[] = []
523
+ let wildcardHandler: Handler | null = null
524
+ let wildcardMws: Middleware[] = []
525
+ let wildcardIdx = -1
526
+
527
+ for (let i = 0; i < segments.length; i++) {
528
+ pathMws.push(...node.pathMws)
529
+
530
+ if (this._hasWildcard && node.wildcard) {
531
+ const h = node.handlers.get('*') || node.handlers.get(method)
532
+ if (h) {
533
+ wildcardHandler = h
534
+ wildcardMws = node.middlewares.get(method) || node.middlewares.get('*') || []
535
+ wildcardIdx = i
536
+ }
537
+ }
538
+
539
+ const segment = segments[i]
540
+
541
+ const next = matchChild(node, segment, params, false)
542
+ if (!next) {
543
+ if (wildcardHandler) {
544
+ params['*'] = segments.slice(wildcardIdx).join('/')
545
+ return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params }
546
+ }
547
+ return null
548
+ }
549
+ node = next
550
+ }
551
+
552
+ pathMws.push(...node.pathMws)
553
+
554
+ const handler = node.handlers.get(method) || node.handlers.get('*')
555
+ if (handler) {
556
+ if (node.wildcard) params['*'] = segments.slice(segments.length).join('/')
557
+ return {
558
+ handler,
559
+ middlewares: node.middlewares.get(method) || node.middlewares.get('*') || [],
560
+ pathMws,
561
+ params,
562
+ }
563
+ }
564
+
565
+ if (wildcardHandler) {
566
+ params['*'] = segments.slice(wildcardIdx).join('/')
567
+ return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params }
568
+ }
569
+
570
+ if (node.handlers.size > 0) {
571
+ return {
572
+ middlewares: [],
573
+ pathMws,
574
+ params,
575
+ allowedMethods: [...node.handlers.keys()].filter((k) => k !== '*'),
576
+ }
577
+ }
578
+
579
+ return null
580
+ }
581
+
582
+ private matchWsTrie(root: WsTrieNode, segments: string[]): WsMatchResult {
583
+ let node = root
584
+ const params: Record<string, string> = {}
585
+
586
+ for (const segment of segments) {
587
+ const next = matchChild(node, segment, params, true)
588
+ if (!next) return null
589
+ node = next
590
+ }
591
+
592
+ return node.handler ? { handler: node.handler, middlewares: node.middlewares, params } : null
593
+ }
594
+
595
+ private async handleError(e: unknown, req: Request, ctx: Context): Promise<Response> {
596
+ const err = e instanceof Error ? e : new Error(String(e))
597
+ console.error(err)
598
+ return this.errorHandler
599
+ ? await this.errorHandler(err, req, ctx as T)
600
+ : new Response('Internal Server Error', { status: 500 })
601
+ }
602
+
603
+ private async handle(req: Request, ctx: Context, segments: string[]): Promise<Response> {
604
+ const match = this.matchTrie(req.method, segments)
605
+
606
+ if (match) {
607
+ Object.assign(ctx.params, match.params)
608
+
609
+ if (match.handler) {
610
+ const { handler, middlewares: routeMws, pathMws } = match
611
+ const mws = this.mergeMws(this.mergeMws(this.globalMws, pathMws), routeMws)
612
+ try {
613
+ return await this.runChain(mws, handler, req, ctx)
614
+ } catch (e) {
615
+ return this.handleError(e, req, ctx)
616
+ }
617
+ }
618
+
619
+ if (match.allowedMethods && match.allowedMethods.length > 0) {
620
+ if (this.globalMws.length > 0) {
621
+ try {
622
+ return await this.runChain(
623
+ this.globalMws,
624
+ () =>
625
+ new Response('Method Not Allowed', {
626
+ status: 405,
627
+ headers: { Allow: match.allowedMethods!.join(', ') },
628
+ }),
629
+ req,
630
+ ctx,
631
+ )
632
+ } catch (e) {
633
+ return this.handleError(e, req, ctx)
634
+ }
635
+ }
636
+ return new Response('Method Not Allowed', {
637
+ status: 405,
638
+ headers: { Allow: match.allowedMethods.join(', ') },
639
+ })
640
+ }
641
+ }
642
+
643
+ if (this.globalMws.length > 0) {
644
+ try {
645
+ return await this.runChain(
646
+ this.globalMws,
647
+ () => {
648
+ if (!isProd()) {
649
+ return Response.json(
650
+ { error: 'Not Found', path: '/' + segments.join('/'), method: req.method },
651
+ { status: 404 },
652
+ )
653
+ }
654
+ return new Response('Not Found', { status: 404 })
655
+ },
656
+ req,
657
+ ctx,
658
+ )
659
+ } catch (e) {
660
+ return this.handleError(e, req, ctx)
661
+ }
662
+ }
663
+
664
+ if (!isProd()) {
665
+ return Response.json(
666
+ {
667
+ error: 'Not Found',
668
+ path: '/' + segments.join('/'),
669
+ method: req.method,
670
+ },
671
+ { status: 404 },
672
+ )
673
+ }
674
+ return new Response('Not Found', { status: 404 })
675
+ }
676
+
677
+ private async runChain(
678
+ middlewares: Middleware[],
679
+ finalHandler: Handler,
680
+ req: Request,
681
+ ctx: Context,
682
+ ): Promise<Response> {
683
+ if (middlewares.length === 0) return await finalHandler(req, ctx)
684
+ return await runChainLoop(middlewares, 0, finalHandler, req, ctx)
685
+ }
686
+ }
687
+
688
+ function runChainLoop(
689
+ middlewares: Middleware[],
690
+ index: number,
691
+ finalHandler: Handler,
692
+ req: Request,
693
+ ctx: Context,
694
+ ): Promise<Response> {
695
+ if (index < middlewares.length) {
696
+ const mw = middlewares[index]
697
+ let called = false
698
+ const dispatch: Handler = (r, c) => {
699
+ if (called) {
700
+ console.warn(
701
+ '[router] next() called more than once in middleware — ignoring duplicate call',
702
+ )
703
+ return Promise.resolve(new Response('Internal Server Error', { status: 500 }))
704
+ }
705
+ called = true
706
+ return runChainLoop(middlewares, index + 1, finalHandler, r, c)
707
+ }
708
+ return Promise.resolve(mw(req, ctx, dispatch as unknown as Parameters<typeof mw>[2]))
709
+ }
710
+ return Promise.resolve(finalHandler(req, ctx))
711
+ }
712
+
713
+ function upgradeSocket(
714
+ wss: WebSocketServer,
715
+ req: IncomingMessage,
716
+ socket: Duplex,
717
+ head: Buffer,
718
+ handler: WebSocketHandler,
719
+ ctx: Context,
720
+ hub: Hub,
721
+ ): void {
722
+ wss.handleUpgrade(req, socket, head, (ws) => {
723
+ // ── Per-connection ctx — cloned from upgrade ctx ─────────
724
+ // Each connection gets its own ctx, inheriting params/query/user/etc.
725
+ const connCtx: Context = { ...ctx, params: { ...ctx.params }, query: { ...ctx.query } }
726
+
727
+ // ── ctx.ws — per-connection WS helpers ───────────────────
728
+ const wsState: Record<string, unknown> = {}
729
+ connCtx.ws = {
730
+ get state() {
731
+ return wsState
732
+ },
733
+ json(data: unknown) {
734
+ ws.send(JSON.stringify(data))
735
+ },
736
+ join(room: string) {
737
+ hub.join(room, ws)
738
+ },
739
+ leave(_room: string) {
740
+ hub.leave(ws)
741
+ },
742
+ sendRoom(room: string, data: unknown) {
743
+ hub.broadcast(room, JSON.stringify(data))
744
+ },
745
+ }
746
+
747
+ if (handler.open) {
748
+ handler.open(ws, connCtx)
749
+ }
750
+
751
+ ws.on('message', (data) => {
752
+ handler.message?.(ws, connCtx, data as string | Buffer)
753
+ })
754
+
755
+ ws.on('close', () => {
756
+ hub.leave(ws)
757
+ handler.close?.(ws, connCtx)
758
+ })
759
+
760
+ ws.on('error', (err) => {
761
+ handler.error?.(ws, connCtx, err)
762
+ })
763
+ })
764
+ }
765
+
766
+ function nodeReqHeadersToRecord(headers: http.IncomingHttpHeaders): Record<string, string> {
767
+ const result: Record<string, string> = {}
768
+ for (const [k, v] of Object.entries(headers)) {
769
+ if (v !== undefined) result[k] = Array.isArray(v) ? v.join(', ') : v
770
+ }
771
+ return result
772
+ }
773
+
774
+ function sendHttpResponseOnSocket(socket: Duplex, response: Response): void {
775
+ const statusLine = `HTTP/1.1 ${response.status} ${response.statusText}`
776
+ const headerLines: string[] = [statusLine]
777
+ response.headers.forEach((value, key) => {
778
+ headerLines.push(`${key}: ${value}`)
779
+ })
780
+ headerLines.push('Connection: close')
781
+ headerLines.push('')
782
+ const headerStr = headerLines.join('\r\n')
783
+
784
+ response
785
+ .arrayBuffer()
786
+ .then((buf) => {
787
+ socket.write(headerStr + '\r\n')
788
+ if (buf.byteLength > 0) socket.write(Buffer.from(buf))
789
+ socket.end()
790
+ })
791
+ .catch(() => {
792
+ socket.write(headerStr + '\r\n')
793
+ socket.end()
794
+ })
795
+ }