weifuwu 0.27.2 → 0.27.4

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 (90) hide show
  1. package/README.md +0 -19
  2. package/dist/ai/provider.d.ts +45 -0
  3. package/dist/ai/stream.d.ts +13 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +131 -0
  6. package/dist/core/cookie.d.ts +36 -0
  7. package/dist/core/env.d.ts +69 -0
  8. package/dist/core/logger.d.ts +16 -0
  9. package/dist/core/router.d.ts +72 -0
  10. package/dist/core/serve.d.ts +38 -0
  11. package/dist/core/sse.d.ts +47 -0
  12. package/dist/core/trace.d.ts +95 -0
  13. package/dist/graphql.d.ts +16 -0
  14. package/dist/hub.d.ts +36 -0
  15. package/dist/index.d.ts +59 -0
  16. package/dist/index.js +3937 -0
  17. package/dist/mailer.d.ts +51 -0
  18. package/dist/middleware/compress.d.ts +20 -0
  19. package/dist/middleware/cors.d.ts +25 -0
  20. package/dist/middleware/csrf.d.ts +47 -0
  21. package/dist/middleware/flash.d.ts +90 -0
  22. package/dist/middleware/health.d.ts +24 -0
  23. package/dist/middleware/helmet.d.ts +33 -0
  24. package/dist/middleware/i18n.d.ts +39 -0
  25. package/dist/middleware/rate-limit.d.ts +44 -0
  26. package/dist/middleware/request-id.d.ts +40 -0
  27. package/dist/middleware/static.d.ts +23 -0
  28. package/dist/middleware/theme.d.ts +31 -0
  29. package/dist/middleware/upload.d.ts +55 -0
  30. package/dist/middleware/validate.d.ts +32 -0
  31. package/dist/postgres/client.d.ts +4 -0
  32. package/dist/postgres/index.d.ts +4 -0
  33. package/dist/postgres/module.d.ts +16 -0
  34. package/dist/postgres/schema/columns.d.ts +99 -0
  35. package/dist/postgres/schema/index.d.ts +6 -0
  36. package/dist/postgres/schema/sql.d.ts +22 -0
  37. package/dist/postgres/schema/table.d.ts +141 -0
  38. package/dist/postgres/schema/where.d.ts +29 -0
  39. package/dist/postgres/types.d.ts +49 -0
  40. package/dist/queue/cron.d.ts +9 -0
  41. package/dist/queue/index.d.ts +2 -0
  42. package/dist/queue/types.d.ts +61 -0
  43. package/dist/redis/client.d.ts +2 -0
  44. package/{redis/index.ts → dist/redis/index.d.ts} +2 -2
  45. package/dist/redis/types.d.ts +17 -0
  46. package/dist/test/test-utils.d.ts +193 -0
  47. package/dist/types.d.ts +50 -0
  48. package/package.json +10 -12
  49. package/ai/provider.ts +0 -129
  50. package/ai/stream.ts +0 -63
  51. package/cli.ts +0 -147
  52. package/core/cookie.ts +0 -114
  53. package/core/env.ts +0 -142
  54. package/core/logger.ts +0 -72
  55. package/core/router.ts +0 -795
  56. package/core/serve.ts +0 -294
  57. package/core/sse.ts +0 -85
  58. package/core/trace.ts +0 -146
  59. package/graphql.ts +0 -267
  60. package/hub.ts +0 -133
  61. package/index.ts +0 -71
  62. package/mailer.ts +0 -81
  63. package/middleware/compress.ts +0 -103
  64. package/middleware/cors.ts +0 -81
  65. package/middleware/csrf.ts +0 -112
  66. package/middleware/flash.ts +0 -144
  67. package/middleware/health.ts +0 -44
  68. package/middleware/helmet.ts +0 -98
  69. package/middleware/i18n.ts +0 -175
  70. package/middleware/rate-limit.ts +0 -167
  71. package/middleware/request-id.ts +0 -60
  72. package/middleware/static.ts +0 -149
  73. package/middleware/theme.ts +0 -84
  74. package/middleware/upload.ts +0 -168
  75. package/middleware/validate.ts +0 -186
  76. package/postgres/client.ts +0 -132
  77. package/postgres/index.ts +0 -4
  78. package/postgres/module.ts +0 -37
  79. package/postgres/schema/columns.ts +0 -186
  80. package/postgres/schema/index.ts +0 -36
  81. package/postgres/schema/sql.ts +0 -39
  82. package/postgres/schema/table.ts +0 -548
  83. package/postgres/schema/where.ts +0 -99
  84. package/postgres/types.ts +0 -48
  85. package/queue/cron.ts +0 -90
  86. package/queue/index.ts +0 -654
  87. package/queue/types.ts +0 -60
  88. package/redis/client.ts +0 -24
  89. package/redis/types.ts +0 -28
  90. package/types.ts +0 -78
package/core/serve.ts DELETED
@@ -1,294 +0,0 @@
1
- /* eslint-disable no-console */
2
- import http, { type IncomingMessage, type ServerResponse } from 'node:http'
3
- import type { Duplex } from 'node:stream'
4
- import { HttpError, type Context, type Handler } from '../types.ts'
5
- import { runWithTrace, currentTraceId } from './trace.ts'
6
-
7
- export interface ServeOptions {
8
- port?: number
9
- hostname?: string
10
- signal?: AbortSignal
11
- websocket?: (req: IncomingMessage, socket: Duplex, head: Buffer) => void
12
- /** Max request body size in bytes. Default: 10MB. Set to 0 for unlimited. */
13
- maxBodySize?: number
14
- /** Socket timeout in ms (inactivity). Default: 30_000. */
15
- timeout?: number
16
- /** Keep-Alive idle timeout in ms. Default: 5_000. */
17
- keepAliveTimeout?: number
18
- /** Headers timeout in ms (must be > keepAliveTimeout). Default: 6_000. */
19
- headersTimeout?: number
20
- shutdown?: boolean
21
- }
22
-
23
- export interface Server {
24
- stop: (timeoutMs?: number) => Promise<void>
25
- /** Alias for `stop()`. Prefer this for consistency with other modules. */
26
- close: (timeoutMs?: number) => Promise<void>
27
- readonly port: number
28
- readonly hostname: string
29
- ready: Promise<void>
30
- }
31
-
32
- /** Default max body size: 10MB. Set maxBodySize: 0 for unlimited. */
33
- export const DEFAULT_MAX_BODY = 10 * 1024 * 1024
34
-
35
- export async function readBody(req: IncomingMessage, maxSize?: number): Promise<Buffer> {
36
- const limit = maxSize ?? DEFAULT_MAX_BODY
37
-
38
- if (limit > 0) {
39
- const cl = parseInt(req.headers['content-length'] ?? '0', 10)
40
- if (cl > limit) throw new HttpError('Request body too large', 413)
41
- }
42
-
43
- const chunks: Buffer[] = []
44
- let total = 0
45
- for await (const chunk of req) {
46
- total += (chunk as Buffer).byteLength
47
- if (limit > 0 && total > limit) throw new HttpError('Request body too large', 413)
48
- chunks.push(chunk as Buffer)
49
- }
50
- return Buffer.concat(chunks)
51
- }
52
-
53
- export function createRequest(
54
- req: IncomingMessage,
55
- body: Buffer,
56
- ): [Request, Record<string, string>] {
57
- const url = new URL(req.url ?? '/', 'http://localhost')
58
- const query = Object.fromEntries(url.searchParams)
59
-
60
- const headers: Record<string, string> = {}
61
- for (const [key, value] of Object.entries(req.headers)) {
62
- if (value !== undefined) {
63
- headers[key] = Array.isArray(value) ? value.join(', ') : value
64
- }
65
- }
66
-
67
- const request = new Request(url.href, {
68
- method: req.method?.toUpperCase() ?? 'GET',
69
- headers,
70
- body:
71
- req.method !== 'GET' && req.method !== 'HEAD' && body.length > 0 ? (body as BodyInit) : null,
72
- })
73
-
74
- return [request, query]
75
- }
76
-
77
- export async function sendResponse(
78
- res: ServerResponse,
79
- response: Response,
80
- opts?: { traceId?: string | null },
81
- ): Promise<void> {
82
- const headers: Record<string, string | string[]> = {}
83
- response.headers.forEach((value, key) => {
84
- if (key.toLowerCase() === 'set-cookie') {
85
- const existing = headers[key]
86
- headers[key] = existing
87
- ? Array.isArray(existing)
88
- ? [...existing, value]
89
- : [existing, value]
90
- : value
91
- } else {
92
- headers[key] = value
93
- }
94
- })
95
-
96
- // Inject trace header — zero allocation, no Response re-wrapping
97
- if (opts?.traceId && !headers['x-trace-id']) {
98
- headers['x-trace-id'] = opts.traceId
99
- }
100
-
101
- res.writeHead(response.status, response.statusText, headers)
102
-
103
- if (response.body) {
104
- const reader = response.body.getReader()
105
- try {
106
- while (true) {
107
- const { done, value } = await reader.read()
108
- if (done) break
109
- res.write(value)
110
- }
111
- res.end()
112
- } catch (err) {
113
- // Client disconnected or write failed — destroy socket cleanly
114
- if (!res.destroyed) {
115
- res.destroy(err instanceof Error ? err : undefined)
116
- }
117
- } finally {
118
- reader.releaseLock()
119
- }
120
- return
121
- }
122
-
123
- res.end()
124
- }
125
-
126
- export async function createTestServer(
127
- handler: Handler,
128
- options?: ServeOptions,
129
- ): Promise<{ server: Server; url: string }> {
130
- const server = serve(handler, { ...options, port: options?.port ?? 0, shutdown: false })
131
- await server.ready
132
- return { server, url: `http://localhost:${server.port}` }
133
- }
134
-
135
- export function serve(handler: Handler, options?: ServeOptions): Server {
136
- const port = options?.port ?? 0
137
- const hostname = options?.hostname ?? '0.0.0.0'
138
-
139
- const server = http.createServer(async (req, res) => {
140
- const incomingTrace =
141
- (req.headers['x-trace-id'] as string) ||
142
- (req.headers['traceparent'] as string)?.split('-')[1] ||
143
- null
144
-
145
- await runWithTrace(incomingTrace, async () => {
146
- try {
147
- const body = await readBody(req, options?.maxBodySize)
148
- const [request, query] = createRequest(req, body)
149
- const response = await handler(request, { params: {}, query } as Context)
150
- await sendResponse(res, response, { traceId: currentTraceId() })
151
- } catch (err) {
152
- if (err instanceof HttpError && err.status === 413) {
153
- res.writeHead(413, { 'Content-Type': 'text/plain' })
154
- res.end('Request Body Too Large')
155
- return
156
- }
157
- const msg = err instanceof Error ? err.message : String(err)
158
- console.error(`[${currentTraceId()}] unhandled error: ${msg}`)
159
- if (err instanceof Error && err.stack) console.error(err.stack)
160
- res.writeHead(500, { 'Content-Type': 'text/plain' })
161
- res.end('Internal Server Error')
162
- }
163
- })
164
- })
165
-
166
- // Connection timeouts — prevent slowloris and idle connection leaks
167
- server.timeout = options?.timeout ?? 30_000
168
- server.keepAliveTimeout = options?.keepAliveTimeout ?? 5_000
169
- server.headersTimeout = options?.headersTimeout ?? 6_000
170
-
171
- if (options?.websocket) {
172
- server.on('upgrade', options.websocket)
173
- }
174
-
175
- let resolveReady!: () => void
176
- const ready = new Promise<void>((r) => {
177
- resolveReady = r
178
- })
179
-
180
- let shutdownHandler: (() => void) | null = null
181
-
182
- if (options?.shutdown !== false) {
183
- let shuttingDown = false
184
- const shutdown = () => {
185
- if (shuttingDown) return
186
- shuttingDown = true
187
- server.close()
188
- // Give in-flight requests a chance to complete
189
- const timer = setTimeout(() => {
190
- server.closeAllConnections()
191
- process.exit(0)
192
- }, 10_000)
193
- server.on('close', () => {
194
- clearTimeout(timer)
195
- process.exit(0)
196
- })
197
- }
198
- shutdownHandler = shutdown
199
- process.on('SIGTERM', shutdown)
200
- process.on('SIGINT', shutdown)
201
- }
202
-
203
- let _cachedPort = 0
204
- let _cachedHostname = ''
205
-
206
- if (options?.signal) {
207
- if (options.signal.aborted) {
208
- _cachedPort = 0
209
- _cachedHostname = ''
210
- server.close()
211
- resolveReady()
212
- return {
213
- stop: () => Promise.resolve(),
214
- close: () => Promise.resolve(),
215
- ready,
216
- get port() {
217
- return 0
218
- },
219
- get hostname() {
220
- return hostname
221
- },
222
- }
223
- }
224
- options.signal.addEventListener(
225
- 'abort',
226
- () => {
227
- server.close()
228
- },
229
- { once: true },
230
- )
231
- }
232
-
233
- server.on('error', (err) => {
234
- console.error('Failed to start server:', err.message)
235
- server.close()
236
- _cachedPort = 0
237
- resolveReady()
238
- })
239
-
240
- server.listen(port, hostname, () => {
241
- const addr = server.address()
242
- if (addr && typeof addr !== 'string') {
243
- _cachedPort = addr.port
244
- _cachedHostname = addr.address
245
- }
246
- resolveReady()
247
-
248
- // Startup message — automatic in all environments
249
- const displayHost = _cachedHostname === '0.0.0.0' ? 'localhost' : _cachedHostname || 'localhost'
250
- console.log(`weifuwu listening on http://${displayHost}:${_cachedPort}`)
251
- })
252
-
253
- async function stop(timeoutMs = 10_000): Promise<void> {
254
- if (shutdownHandler) {
255
- process.off('SIGTERM', shutdownHandler)
256
- process.off('SIGINT', shutdownHandler)
257
- shutdownHandler = null
258
- }
259
- if (!server.listening) return
260
-
261
- // 1. Stop accepting new connections
262
- server.close()
263
-
264
- // 2. Close idle keep-alive connections
265
- server.closeIdleConnections()
266
-
267
- // 3. Wait for in-flight requests to finish, or force-close after timeout
268
- return new Promise<void>((resolve) => {
269
- const timer = setTimeout(() => {
270
- server.closeAllConnections()
271
- resolve()
272
- }, timeoutMs)
273
-
274
- server.on('close', () => {
275
- clearTimeout(timer)
276
- resolve()
277
- })
278
- })
279
- }
280
-
281
- return {
282
- close: stop,
283
- stop,
284
- ready,
285
- get port() {
286
- if (!server.listening) return 0
287
- return _cachedPort
288
- },
289
- get hostname() {
290
- if (!server.listening) return hostname
291
- return _cachedHostname || hostname
292
- },
293
- }
294
- }
package/core/sse.ts DELETED
@@ -1,85 +0,0 @@
1
- const encoder = new TextEncoder()
2
-
3
- /**
4
- * Format an SSE message string with a named event type.
5
- *
6
- * ```ts
7
- * formatSSE('ping', { ts: Date.now() })
8
- * // "event: ping\ndata: {"ts":...}\n\n"
9
- * ```
10
- */
11
- export function formatSSE(event: string, data: unknown): string {
12
- return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
13
- }
14
-
15
- /**
16
- * Format an SSE message string with only a data line (no event type).
17
- *
18
- * ```ts
19
- * formatSSEData({ message: 'hello' })
20
- * // "data: {"message":"hello"}\n\n"
21
- * ```
22
- */
23
- export function formatSSEData(data: unknown): string {
24
- return `data: ${JSON.stringify(data)}\n\n`
25
- }
26
-
27
- /** An SSE event to be sent via {@link createSSEStream}. */
28
- export interface SSEEvent {
29
- /** Event type (maps to `event:` field). */
30
- event: string
31
- /** Event payload (serialized as JSON in `data:` field). */
32
- data: unknown
33
- }
34
-
35
- /**
36
- * Create a Server-Sent Events (SSE) `Response` from an async iterable.
37
- *
38
- * Each item in the iterable is serialized as an SSE message:
39
- * - If the item has a `.type` property → `event: {type}` + `data: {item}`
40
- * - Otherwise → `data: {item}`
41
- *
42
- * Errors are sent as `event: error` messages. `AbortError` is silently ignored.
43
- *
44
- * ```ts
45
- * app.get('/events', () => {
46
- * async function* generate() {
47
- * yield { type: 'ping', data: { ts: Date.now() } }
48
- * }
49
- * return createSSEStream(generate())
50
- * })
51
- * ```
52
- */
53
- export function createSSEStream(
54
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
- iterable: AsyncIterable<any>,
56
- opts?: { headers?: Record<string, string>; status?: number },
57
- ): Response {
58
- return new Response(
59
- new ReadableStream<Uint8Array>({
60
- async start(controller) {
61
- try {
62
- for await (const event of iterable) {
63
- const text = event.type ? formatSSE(event.type, event) : formatSSEData(event)
64
- controller.enqueue(encoder.encode(text))
65
- }
66
- } catch (e: unknown) {
67
- if (e instanceof Error && e.name !== 'AbortError') {
68
- controller.enqueue(encoder.encode(formatSSE('error', { error: e.message })))
69
- }
70
- } finally {
71
- controller.close()
72
- }
73
- },
74
- }),
75
- {
76
- status: opts?.status ?? 200,
77
- headers: {
78
- 'Content-Type': 'text/event-stream',
79
- 'Cache-Control': 'no-cache',
80
- Connection: 'keep-alive',
81
- ...opts?.headers,
82
- },
83
- },
84
- )
85
- }
package/core/trace.ts DELETED
@@ -1,146 +0,0 @@
1
- import crypto from 'node:crypto'
2
- import { AsyncLocalStorage } from 'node:async_hooks'
3
- import type { Context, Middleware } from '../types.ts'
4
-
5
- // Augment Context with trace property
6
- declare module '../types.ts' {
7
- interface Context {
8
- trace: TraceInjected
9
- }
10
- }
11
-
12
- export interface TraceInjected {
13
- /** Unique request identifier (from X-Request-ID header or auto-generated). */
14
- requestId: string
15
- /** Unique trace identifier for the request. */
16
- traceId: string
17
- /** Milliseconds elapsed since the trace started. */
18
- elapsed: () => number
19
- /** Timestamp (ms) when the trace started. */
20
- startTime: number
21
- }
22
-
23
- export interface TraceContext {
24
- /** Unique identifier for the current request trace. */
25
- traceId: string
26
- /** Timestamp (ms since epoch) when the trace started. */
27
- startTime: number
28
- }
29
-
30
- const als = new AsyncLocalStorage<TraceContext>()
31
-
32
- /**
33
- * Get the current request's trace ID.
34
- * Returns `undefined` when called outside a request context (e.g. at startup).
35
- *
36
- * ```ts
37
- * const traceId = currentTraceId()
38
- * log.info({ traceId }, 'request started')
39
- * ```
40
- */
41
- export function currentTraceId(): string | undefined {
42
- return als.getStore()?.traceId
43
- }
44
-
45
- /**
46
- * Get the full current trace context ({ traceId, startTime }).
47
- * Returns `undefined` outside a request.
48
- */
49
- export function currentTrace(): TraceContext | undefined {
50
- return als.getStore()
51
- }
52
-
53
- /**
54
- * Run a function inside a trace context.
55
- * Used internally by `serve()` for every incoming request.
56
- * If `incomingTraceId` is provided (e.g. from an `X-Trace-Id` header) it is reused;
57
- * otherwise a new UUID is generated.
58
- *
59
- * ```ts
60
- * const result = runWithTrace(req.headers.get('x-trace-id'), () => {
61
- * return handleRequest(req)
62
- * })
63
- * ```
64
- *
65
- * @param incomingTraceId - Optional trace ID from upstream. Pass `null` to auto-generate.
66
- * @param fn - Function to execute within the trace scope.
67
- * @returns The return value of `fn`.
68
- */
69
- export function runWithTrace<T>(incomingTraceId: string | null, fn: () => T): T {
70
- const traceId = incomingTraceId || crypto.randomUUID()
71
- const startTime = Date.now()
72
- return als.run({ traceId, startTime }, fn)
73
- }
74
-
75
- /**
76
- * Milliseconds elapsed since the current trace started.
77
- * Returns `0` if called outside a request context.
78
- *
79
- * ```ts
80
- * app.use(async (req, ctx, next) => {
81
- * const res = await next(req, ctx)
82
- * console.log('handled in', traceElapsed(), 'ms')
83
- * return res
84
- * })
85
- * ```
86
- */
87
- export function traceElapsed(): number {
88
- const ctx = als.getStore()
89
- if (!ctx) return 0
90
- return Date.now() - ctx.startTime
91
- }
92
-
93
- /** Options for {@link trace}. */
94
- export interface TraceOptions {
95
- /** Header name for request ID (default: `'X-Request-ID'`). */
96
- header?: string
97
- /** Custom ID generator (default: `crypto.randomUUID`). */
98
- generator?: () => string
99
- }
100
-
101
- /**
102
- * Request tracing middleware.
103
- *
104
- * Injects `ctx.trace = { requestId, traceId, elapsed, startTime }`.
105
- * Reads/writes `X-Request-ID` header. Combines the functionality of `requestId()`
106
- * with the per-request tracing from `AsyncLocalStorage`.
107
- *
108
- * ```ts
109
- * import { trace } from 'weifuwu'
110
- * app.use(trace())
111
- *
112
- * app.get('/', (req, ctx) => {
113
- * console.log(ctx.trace.requestId) // 550e8400-e29b-...
114
- * console.log(ctx.trace.traceId) // same as currentTraceId()
115
- * console.log(ctx.trace.elapsed()) // ms since request start
116
- * })
117
- * ```
118
- */
119
- export function trace(
120
- options?: TraceOptions,
121
- ): Middleware<Context, Context & { trace: TraceInjected }> {
122
- const header = options?.header ?? 'X-Request-ID'
123
- const gen = options?.generator ?? (() => crypto.randomUUID())
124
-
125
- return async (req, ctx, next) => {
126
- const existing = req.headers.get(header)
127
- const requestId = existing ?? gen()
128
- const tc = als.getStore()
129
-
130
- ;(ctx as Context & { trace: TraceInjected }).trace = {
131
- requestId,
132
- traceId: tc?.traceId ?? requestId,
133
- startTime: tc?.startTime ?? Date.now(),
134
- elapsed: () => {
135
- const t = als.getStore()
136
- return t ? Date.now() - t.startTime : 0
137
- },
138
- }
139
-
140
- const res = await next(req, ctx as Context & { trace: TraceInjected })
141
- if (res.headers.has(header)) return res
142
- const h = new Headers(res.headers)
143
- h.set(header, requestId)
144
- return new Response(res.body, { status: res.status, statusText: res.statusText, headers: h })
145
- }
146
- }