weifuwu 0.28.0 → 0.30.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/README.md +394 -0
- package/dist/ai/provider.d.ts +45 -0
- package/dist/ai/stream.d.ts +13 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +178 -0
- package/dist/core/cookie.d.ts +36 -0
- package/dist/core/env.d.ts +69 -0
- package/dist/core/html.d.ts +34 -0
- package/dist/core/logger.d.ts +16 -0
- package/dist/core/router.d.ts +95 -0
- package/dist/core/serve.d.ts +36 -0
- package/dist/core/sse.d.ts +47 -0
- package/dist/core/trace.d.ts +95 -0
- package/dist/graphql.d.ts +16 -0
- package/dist/hub.d.ts +36 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +3387 -0
- package/dist/middleware/compress.d.ts +20 -0
- package/dist/middleware/cors.d.ts +25 -0
- package/dist/middleware/csrf.d.ts +31 -0
- package/dist/middleware/flash.d.ts +44 -0
- package/dist/middleware/health.d.ts +24 -0
- package/dist/middleware/helmet.d.ts +33 -0
- package/dist/middleware/i18n.d.ts +39 -0
- package/dist/middleware/rate-limit.d.ts +44 -0
- package/dist/middleware/request-id.d.ts +40 -0
- package/dist/middleware/static.d.ts +23 -0
- package/dist/middleware/theme.d.ts +31 -0
- package/dist/middleware/upload.d.ts +55 -0
- package/dist/middleware/validate.d.ts +32 -0
- package/dist/postgres/client.d.ts +4 -0
- package/dist/postgres/index.d.ts +3 -0
- package/dist/postgres/module.d.ts +12 -0
- package/dist/postgres/types.d.ts +42 -0
- package/dist/queue/cron.d.ts +9 -0
- package/dist/queue/index.d.ts +2 -0
- package/dist/queue/types.d.ts +61 -0
- package/dist/react/index.js +2573 -0
- package/dist/redis/client.d.ts +2 -0
- package/dist/redis/index.d.ts +2 -0
- package/dist/redis/types.d.ts +17 -0
- package/dist/test/test-utils.d.ts +193 -0
- package/dist/types.d.ts +82 -0
- package/package.json +31 -9
- package/index.d.ts +0 -8
- package/index.js +0 -26
package/README.md
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# weifuwu
|
|
2
|
+
|
|
3
|
+
Web-standard HTTP microframework for Node.js — `(req, ctx) => Response`.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install weifuwu
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { serve, Router } from 'weifuwu'
|
|
13
|
+
|
|
14
|
+
const app = new Router()
|
|
15
|
+
app.get('/', () => new Response('Hello'))
|
|
16
|
+
serve(app, { port: 3000 })
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Exports
|
|
20
|
+
|
|
21
|
+
Everything is exported from `'weifuwu'`:
|
|
22
|
+
|
|
23
|
+
### Core
|
|
24
|
+
|
|
25
|
+
| Export | Kind | Description |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `serve(app, opts?)` | function | Start HTTP server. Takes a `Router`. Returns `Server`. |
|
|
28
|
+
| `Router` | class | Trie-based HTTP router with WebSocket support. |
|
|
29
|
+
| `loadEnv(path?)` | function | Load `.env` file into `process.env`. |
|
|
30
|
+
| `isDev()` | function | `true` unless `NODE_ENV` is `production` or `test`. |
|
|
31
|
+
| `isProd()` | function | `true` if `NODE_ENV` is `production`. |
|
|
32
|
+
| `isBundled()` | function | `true` when running the esbuild bundle (checks `__WFW_BUNDLED__`). |
|
|
33
|
+
| `getPublicEnv()` | function | Returns all `process.env` keys prefixed with `WEIFUWU_PUBLIC_`. |
|
|
34
|
+
| `DEFAULT_MAX_BODY` | const | `10 * 1024 * 1024` (10MB). |
|
|
35
|
+
|
|
36
|
+
### Router
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const app = new Router()
|
|
40
|
+
|
|
41
|
+
// HTTP methods
|
|
42
|
+
app.get(path, ...handlers)
|
|
43
|
+
app.post(path, ...handlers)
|
|
44
|
+
app.put(path, ...handlers)
|
|
45
|
+
app.delete(path, ...handlers)
|
|
46
|
+
app.patch(path, ...handlers)
|
|
47
|
+
app.head(path, ...handlers)
|
|
48
|
+
app.options(path, ...handlers)
|
|
49
|
+
app.all(path, ...handlers) // match any method
|
|
50
|
+
|
|
51
|
+
// WebSocket
|
|
52
|
+
app.ws(path, ...middlewares, handler)
|
|
53
|
+
// handler: { open?, message?, close?, error? }
|
|
54
|
+
|
|
55
|
+
// Middleware & mounting
|
|
56
|
+
app.use(middleware) // global middleware
|
|
57
|
+
app.mount(prefix, router) // mount sub-router at prefix
|
|
58
|
+
app.onError(handler) // global error handler
|
|
59
|
+
app.routes() // debug: string[] of all registered routes
|
|
60
|
+
|
|
61
|
+
// Path params
|
|
62
|
+
app.get('/users/:id', (req, ctx) => {
|
|
63
|
+
const id = ctx.params.id // string
|
|
64
|
+
const q = ctx.query.search // from ?search=...
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Middleware
|
|
69
|
+
|
|
70
|
+
| Export | Kind | Description |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `cors(opts?)` | middleware | CORS headers. `origin`, `methods`, `headers`, `credentials`, `maxAge`. |
|
|
73
|
+
| `helmet(opts?)` | middleware | Security headers (CSP, HSTS, X-Frame-Options, etc). |
|
|
74
|
+
| `compress(opts?)` | middleware | gzip / brotli / deflate response compression. |
|
|
75
|
+
| `rateLimit(opts?)` | middleware | Sliding-window rate limiter. `windowMs`, `max`, `keyGenerator`. |
|
|
76
|
+
| `requestId(opts?)` | middleware | Generates `X-Request-ID` header. `header`, `generator`. |
|
|
77
|
+
| `logger(opts?)` | middleware | Structured request logging. `format`, `level`, `stream`. |
|
|
78
|
+
| `validate(schemas?)` | middleware | Zod-based request validation. `{ body?, query?, params?, headers? }`. |
|
|
79
|
+
| `upload(opts?)` | middleware | Multipart file upload. `maxSize`, `dest`, `preservePath`. |
|
|
80
|
+
| `health(opts?)` | function→Router | Health check router. Mount at `/_health` or custom path. |
|
|
81
|
+
| `env()` | middleware | Injects `WEIFUWU_PUBLIC_*` env vars into `ctx.env`. |
|
|
82
|
+
| `trace(opts?)` | middleware | Injects trace context into `ctx.trace` (requestId, traceId, startTime, elapsed). |
|
|
83
|
+
|
|
84
|
+
### Route-level tools
|
|
85
|
+
|
|
86
|
+
| Export | Kind | Description |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `serveStatic(root, opts?)` | handler | Serve static files. `cacheControl`, `index`, `dotfiles`. |
|
|
89
|
+
| `graphql(options)` | function→Router | GraphQL endpoint. Pass a `GraphQLHandler` function `({ query, variables, ctx })`. |
|
|
90
|
+
|
|
91
|
+
### HTML rendering
|
|
92
|
+
|
|
93
|
+
| Export | Kind | Description |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `html(template, ...values)` | tagged template | Auto-escaped HTML builder. Signal/Computed-aware. |
|
|
96
|
+
| `raw(content)` | function | Raw unescaped HTML string (use with caution). |
|
|
97
|
+
|
|
98
|
+
### SSE (Server-Sent Events)
|
|
99
|
+
|
|
100
|
+
| Export | Kind | Description |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `createSSEStream()` | function | Create an SSE stream controller for `text/event-stream` responses. |
|
|
103
|
+
| `formatSSE(event, data)` | function | Format a single SSE event string. |
|
|
104
|
+
| `formatSSEData(data)` | function | Format SSE data string. |
|
|
105
|
+
|
|
106
|
+
### Cookies
|
|
107
|
+
|
|
108
|
+
| Export | Kind | Description |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `getCookies(req)` | function | Parse cookies from `Request`. Returns `Record<string, string>`. |
|
|
111
|
+
| `setCookie(headers, name, value, opts?)` | function | Set `Set-Cookie` header. `path`, `domain`, `maxAge`, `httpOnly`, `secure`, `sameSite`, `partitioned`. |
|
|
112
|
+
| `deleteCookie(headers, name, opts?)` | function | Delete a cookie (Max-Age=0). |
|
|
113
|
+
|
|
114
|
+
### Postgres
|
|
115
|
+
|
|
116
|
+
| Export | Kind | Description |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `postgres(opts?)` | middleware+client | Create a Postgres client. `url`, `host`, `port`, `database`, `user`, `password`, `max`. |
|
|
119
|
+
| `MIGRATIONS_TABLE` | const | Migration table name: `_weifuwu_migrations`. |
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const sql = postgres({ url: process.env.DATABASE_URL })
|
|
123
|
+
|
|
124
|
+
// As middleware — injects ctx.sql
|
|
125
|
+
app.use(sql)
|
|
126
|
+
// As query client
|
|
127
|
+
const rows = await sql.client`SELECT * FROM users WHERE id = ${id}`
|
|
128
|
+
|
|
129
|
+
// Migrations
|
|
130
|
+
await sql.migrate({
|
|
131
|
+
'001_init': async (s) => {
|
|
132
|
+
await s`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Redis
|
|
138
|
+
|
|
139
|
+
| Export | Kind | Description |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `redis(opts?)` | middleware+client | Create a Redis client (ioredis). `url`, `host`, `port`, `password`, `db`. |
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const r = redis({ url: process.env.REDIS_URL })
|
|
145
|
+
app.use(r) // injects ctx.redis
|
|
146
|
+
await r.client.set('key', 'value')
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Queue & Cron
|
|
150
|
+
|
|
151
|
+
| Export | Kind | Description |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `queue(opts?)` | middleware+instance | In-process job queue with cron support. `concurrency`. |
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const q = queue({ concurrency: 3 })
|
|
157
|
+
app.use(q) // injects ctx.queue
|
|
158
|
+
|
|
159
|
+
q.process('send-email', async (job) => {
|
|
160
|
+
await sendEmail(job.data.to)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
q.cron('cleanup', '0 3 * * *', async () => {
|
|
164
|
+
await cleanupOldSessions()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
await q.add('send-email', { to: 'user@example.com' })
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### WebSocket Hub
|
|
171
|
+
|
|
172
|
+
| Export | Kind | Description |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| `createHub(opts?)` | function | Pub/Sub hub for cross-connection broadcast. Supports Redis adapter for multi-process. |
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const hub = createHub()
|
|
178
|
+
app.ws('/chat', {
|
|
179
|
+
open(ws, ctx) {
|
|
180
|
+
ctx.ws.join('lobby')
|
|
181
|
+
ctx.ws.json({ type: 'join', room: 'lobby' })
|
|
182
|
+
},
|
|
183
|
+
message(ws, ctx, data) {
|
|
184
|
+
ctx.ws.sendRoom('lobby', { type: 'chat', text: data.toString() })
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Theme & I18n
|
|
190
|
+
|
|
191
|
+
| Export | Kind | Description |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `theme(opts?)` | middleware+Router | Theme middleware. Injects `ctx.theme` with `get()`, `set(theme)`, `toggle()`. Routes: `/__theme/:theme`. |
|
|
194
|
+
| `i18n(opts?)` | middleware+Router | I18n middleware. Injects `ctx.i18n` with `locale`, `t(key, params?)`, `set(locale)`. `dir` for locale JSON files. Routes: `/__lang/:locale`. |
|
|
195
|
+
| `flash()` | middleware | Flash messages. Injects `ctx.flash` with `set(msg)`, `get()`. One-time messages across redirects. |
|
|
196
|
+
| `csrf()` | middleware | CSRF protection. Injects `ctx.csrf` with `token`. Auto-validates on `POST`/`PUT`/`PATCH`/`DELETE`. |
|
|
197
|
+
|
|
198
|
+
### Tracing
|
|
199
|
+
|
|
200
|
+
| Export | Kind | Description |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `currentTraceId()` | function | Get current request's trace ID (from AsyncLocalStorage). |
|
|
203
|
+
| `currentTrace()` | function | Get current trace context `{ traceId, startTime }`. |
|
|
204
|
+
| `runWithTrace(id, fn)` | function | Run `fn` with a trace context. Auto-generates ID if null. |
|
|
205
|
+
| `traceElapsed()` | function | Milliseconds since trace start. |
|
|
206
|
+
|
|
207
|
+
### Error handling
|
|
208
|
+
|
|
209
|
+
| Export | Kind | Description |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `HttpError` | class | `new HttpError(message, status)`. Throw to return that status. |
|
|
212
|
+
| `app.onError(handler)` | Router method | Global error handler: `(error, req, ctx) => Response`. |
|
|
213
|
+
|
|
214
|
+
### Testing
|
|
215
|
+
|
|
216
|
+
| Export | Kind | Description |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `testApp(app)` | function | Create a test client for a Router. Returns `{ get, post, put, delete, patch }` for `(path, opts?)`. |
|
|
219
|
+
| `createTestServer(app, opts?)` | function | Start a test server on a random port. Returns `{ server, url }`. |
|
|
220
|
+
| `createTestDb()` | function | Create an ephemeral Postgres test database. |
|
|
221
|
+
| `withTestDb(fn)` | function | Run `fn(sql)` with a temp database, auto-cleanup. |
|
|
222
|
+
|
|
223
|
+
### Types
|
|
224
|
+
|
|
225
|
+
| Export | Description |
|
|
226
|
+
|---|---|
|
|
227
|
+
| `Context` | `{ params, query, mountPath?, user?, loaderData?, env?, [key: string] }` |
|
|
228
|
+
| `Handler<T>` | `(req: Request, ctx: T) => Response \| Promise<Response>` |
|
|
229
|
+
| `Middleware<In, Out>` | `(req, ctx, next) => Response \| Promise<Response>` |
|
|
230
|
+
| `WebSocketHandler` | `{ open?, message?, close?, error? }` |
|
|
231
|
+
| `HttpError` | `Error` subclass with `.status: number` |
|
|
232
|
+
| `Closeable` | `{ close(): Promise<void> }` |
|
|
233
|
+
| `Server` | `{ stop, close, port, hostname, ready }` |
|
|
234
|
+
| `ServeOptions` | `{ port, hostname, signal, maxBodySize, timeout, keepAliveTimeout, headersTimeout, shutdown }` |
|
|
235
|
+
|
|
236
|
+
## Handler / Middleware patterns
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// Handler — standard Web API
|
|
240
|
+
app.get('/api/data', (req, ctx) => {
|
|
241
|
+
const { q } = ctx.query
|
|
242
|
+
const id = ctx.params.id
|
|
243
|
+
return Response.json({ q, id })
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Async
|
|
247
|
+
app.get('/db/users', async (req, ctx) => {
|
|
248
|
+
const rows = await sql`SELECT * FROM users`
|
|
249
|
+
return Response.json(rows)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
// Throwing HttpError
|
|
253
|
+
app.get('/item/:id', (req, ctx) => {
|
|
254
|
+
const item = db.get(ctx.params.id)
|
|
255
|
+
if (!item) throw new HttpError('Not found', 404)
|
|
256
|
+
return Response.json(item)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// Middleware
|
|
260
|
+
app.use(async (req, ctx, next) => {
|
|
261
|
+
const start = Date.now()
|
|
262
|
+
const res = await next(req, ctx)
|
|
263
|
+
console.log(`${req.method} ${req.url} ${Date.now() - start}ms`)
|
|
264
|
+
return res
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// Route-level middleware (before handler)
|
|
268
|
+
app.get('/admin', validate(bodySchema), authCheck, adminHandler)
|
|
269
|
+
|
|
270
|
+
// Error handler
|
|
271
|
+
app.onError((err, req, ctx) => {
|
|
272
|
+
if (err instanceof HttpError) {
|
|
273
|
+
return Response.json({ error: err.message }, { status: err.status })
|
|
274
|
+
}
|
|
275
|
+
console.error(err)
|
|
276
|
+
return new Response('Internal error', { status: 500 })
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Complete example
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import { serve, Router, loadEnv, cors, helmet, compress, logger, requestId, rateLimit, postgres, redis, queue, HttpError, html } from 'weifuwu'
|
|
284
|
+
|
|
285
|
+
loadEnv()
|
|
286
|
+
|
|
287
|
+
const app = new Router()
|
|
288
|
+
|
|
289
|
+
// Global middleware
|
|
290
|
+
app.use(requestId())
|
|
291
|
+
app.use(logger())
|
|
292
|
+
app.use(cors())
|
|
293
|
+
app.use(helmet())
|
|
294
|
+
app.use(compress())
|
|
295
|
+
app.use(rateLimit({ windowMs: 60_000, max: 100 }))
|
|
296
|
+
|
|
297
|
+
// Database & services
|
|
298
|
+
const sql = postgres({ url: process.env.DATABASE_URL })
|
|
299
|
+
const cache = redis({ url: process.env.REDIS_URL })
|
|
300
|
+
const jobs = queue()
|
|
301
|
+
|
|
302
|
+
app.use(sql)
|
|
303
|
+
app.use(cache)
|
|
304
|
+
app.use(jobs)
|
|
305
|
+
|
|
306
|
+
// Routes
|
|
307
|
+
app.get('/', () => html`<h1>weifuwu</h1><p>Running on ${process.version}</p>`)
|
|
308
|
+
|
|
309
|
+
app.get('/api/users', async (req, ctx) => {
|
|
310
|
+
const users = await ctx.sql`SELECT id, name, email FROM users ORDER BY id`
|
|
311
|
+
return Response.json(users)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
app.get('/api/users/:id', async (req, ctx) => {
|
|
315
|
+
const [user] = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
|
|
316
|
+
if (!user) throw new HttpError('User not found', 404)
|
|
317
|
+
return Response.json(user)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
app.post('/api/users', async (req, ctx) => {
|
|
321
|
+
const { name, email } = await req.json()
|
|
322
|
+
const [user] = await ctx.sql`INSERT INTO users (name, email) VALUES (${name}, ${email}) RETURNING *`
|
|
323
|
+
return Response.json(user, { status: 201 })
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// Background job
|
|
327
|
+
jobs.process('welcome-email', async (job) => {
|
|
328
|
+
const { userId } = job.data
|
|
329
|
+
const [user] = await sql.client`SELECT * FROM users WHERE id = ${userId}`
|
|
330
|
+
await sendWelcomeEmail(user.email)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
app.post('/api/register', async (req, ctx) => {
|
|
334
|
+
const { name, email } = await req.json()
|
|
335
|
+
const [user] = await ctx.sql`INSERT INTO users (name, email) VALUES (${name}, ${email}) RETURNING *`
|
|
336
|
+
await ctx.queue.add('welcome-email', { userId: user.id })
|
|
337
|
+
return Response.json(user, { status: 201 })
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// WebSocket chat
|
|
341
|
+
app.ws('/chat', {
|
|
342
|
+
open(ws, ctx) {
|
|
343
|
+
ctx.ws.join('lobby')
|
|
344
|
+
},
|
|
345
|
+
message(ws, ctx, data) {
|
|
346
|
+
ctx.ws.sendRoom('lobby', { text: data.toString() })
|
|
347
|
+
},
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// Error handling
|
|
351
|
+
app.onError((err, req, ctx) => {
|
|
352
|
+
if (err instanceof HttpError) {
|
|
353
|
+
return Response.json({ error: err.message }, { status: err.status })
|
|
354
|
+
}
|
|
355
|
+
return Response.json({ error: 'Internal error' }, { status: 500 })
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
// Health check
|
|
359
|
+
import { health } from 'weifuwu'
|
|
360
|
+
app.mount('/_health', health())
|
|
361
|
+
|
|
362
|
+
serve(app, { port: 3000 })
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Project Structure
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
weifuwu/
|
|
369
|
+
├── package.json
|
|
370
|
+
├── tsconfig.json
|
|
371
|
+
├── scripts/
|
|
372
|
+
│ ├── build.mjs
|
|
373
|
+
│ └── release.mjs
|
|
374
|
+
├── src/
|
|
375
|
+
│ ├── index.ts ← all exports
|
|
376
|
+
│ ├── types.ts ← Context, Handler, Middleware, HttpError
|
|
377
|
+
├── core/ ← serve, router, trace, env, logger, cookie, sse, html
|
|
378
|
+
│ ├── middleware/ ← cors, compress, helmet, rate-limit, static, validate, upload, health, theme, i18n, flash, csrf, request-id
|
|
379
|
+
├── postgres/ ← postgres client, migrations, module injection
|
|
380
|
+
│ ├── redis/ ← redis client
|
|
381
|
+
│ ├── queue/ ← job queue, cron
|
|
382
|
+
│ ├── graphql.ts ← schema-first GraphQL handler
|
|
383
|
+
│ ├── hub.ts ← pub/sub hub (WebSocket rooms)
|
|
384
|
+
│ └── test/ ← 324 tests
|
|
385
|
+
└── dist/
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Development
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
npm run build # esbuild bundle → dist/index.js
|
|
392
|
+
npm run typecheck # tsc --noEmit
|
|
393
|
+
npm test # node --test (324 tests, 0 failures)
|
|
394
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Context, Middleware } from '../types.ts';
|
|
2
|
+
import { generateText as aiGenerateText, streamText as aiStreamText, type LanguageModel, type EmbeddingModel } from 'ai';
|
|
3
|
+
declare module '../types.ts' {
|
|
4
|
+
interface Context {
|
|
5
|
+
ai: AIProvider;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface AIProviderInjected {
|
|
9
|
+
ai: AIProvider;
|
|
10
|
+
}
|
|
11
|
+
export interface AIProviderOptions {
|
|
12
|
+
/** API base URL (default: OPENAI_BASE_URL env or http://localhost:11434/v1). */
|
|
13
|
+
baseURL?: string;
|
|
14
|
+
/** API key (default: OPENAI_API_KEY env or 'ollama'). */
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
/** Chat model name (default: OPENAI_MODEL env or 'qwen3:0.6b'). */
|
|
17
|
+
model?: string;
|
|
18
|
+
/** Embedding model name (default: OPENAI_EMBEDDING_MODEL env or 'qwen3-embedding:0.6b'). */
|
|
19
|
+
embeddingModel?: string;
|
|
20
|
+
/** Vector dimension (default: EMBEDDING_DIMENSION env or 1024). */
|
|
21
|
+
embeddingDimension?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface AIProvider {
|
|
24
|
+
/** Get the language model. Caches by default; pass a name to override. */
|
|
25
|
+
model(name?: string): LanguageModel;
|
|
26
|
+
/** Get the embedding model. Caches by default; pass a name to override. */
|
|
27
|
+
embeddingModel(name?: string): EmbeddingModel;
|
|
28
|
+
/** Embed a single text string into a vector. */
|
|
29
|
+
embed(text: string): Promise<number[]>;
|
|
30
|
+
/** Embed multiple text strings in batch. */
|
|
31
|
+
embedMany(texts: string[]): Promise<number[][]>;
|
|
32
|
+
/** The configured vector dimension. */
|
|
33
|
+
readonly dimension: number;
|
|
34
|
+
/**
|
|
35
|
+
* Generate text using the configured model.
|
|
36
|
+
* All options are passed through to the AI SDK's `generateText`, with `model` auto-injected.
|
|
37
|
+
*/
|
|
38
|
+
generateText(params: Omit<Parameters<typeof aiGenerateText>[0], 'model'>): ReturnType<typeof aiGenerateText>;
|
|
39
|
+
/**
|
|
40
|
+
* Stream text using the configured model.
|
|
41
|
+
* All options are passed through to the AI SDK's `streamText`, with `model` auto-injected.
|
|
42
|
+
*/
|
|
43
|
+
streamText(params: Omit<Parameters<typeof aiStreamText>[0], 'model'>): ReturnType<typeof aiStreamText>;
|
|
44
|
+
}
|
|
45
|
+
export declare function aiProvider(options?: AIProviderOptions): Middleware<Context, Context & AIProviderInjected> & AIProvider;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Context } from '../types.ts';
|
|
2
|
+
import { Router } from '../core/router.ts';
|
|
3
|
+
import type { AIProvider } from './provider.ts';
|
|
4
|
+
export type AIHandler = (req: Request, ctx: Context) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
5
|
+
export declare const _ai: Record<string, any>;
|
|
6
|
+
/**
|
|
7
|
+
* Create a streaming AI endpoint.
|
|
8
|
+
*
|
|
9
|
+
* @param handler - Returns options for `streamText` or `streamObject` (if `schema` is present).
|
|
10
|
+
* @param provider - Optional AI provider. If provided and the handler does not return a `model`,
|
|
11
|
+
* `provider.model()` is used as the default.
|
|
12
|
+
*/
|
|
13
|
+
export declare function aiStream(handler: AIHandler, provider?: AIProvider): Promise<Router>;
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// cli.ts
|
|
4
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import { join, dirname, resolve } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { parseArgs } from "node:util";
|
|
10
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
var __dirname = dirname(__filename);
|
|
12
|
+
var pkgRoot = existsSync(join(__dirname, "package.json")) ? __dirname : resolve(__dirname, "..");
|
|
13
|
+
async function readPkg() {
|
|
14
|
+
return JSON.parse(
|
|
15
|
+
await import("node:fs/promises").then(
|
|
16
|
+
(fs) => fs.readFile(join(pkgRoot, "package.json"), "utf-8")
|
|
17
|
+
)
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
async function cmdVersion() {
|
|
21
|
+
const pkg = await readPkg();
|
|
22
|
+
console.log(pkg.version);
|
|
23
|
+
}
|
|
24
|
+
async function cmdInit(name, opts) {
|
|
25
|
+
const targetDir = resolve(process.cwd(), name);
|
|
26
|
+
if (existsSync(targetDir)) {
|
|
27
|
+
console.error(`Directory ${name} already exists.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const pkg = await readPkg();
|
|
31
|
+
const typesNodeVersion = pkg.devDependencies?.["@types/node"] || "^22";
|
|
32
|
+
if (opts.ssr) {
|
|
33
|
+
await generateReactSsr(targetDir, name, pkg.version, typesNodeVersion, opts.skipInstall);
|
|
34
|
+
} else {
|
|
35
|
+
await generateMinimal(targetDir, name, pkg.version, typesNodeVersion, opts.skipInstall);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function generateMinimal(targetDir, name, version, typesNodeVersion, skipInstall) {
|
|
39
|
+
await mkdir(targetDir, { recursive: true });
|
|
40
|
+
await writeFile(
|
|
41
|
+
join(targetDir, "app.ts"),
|
|
42
|
+
[
|
|
43
|
+
`import { Router } from 'weifuwu'`,
|
|
44
|
+
``,
|
|
45
|
+
`export const app = new Router()`,
|
|
46
|
+
``,
|
|
47
|
+
`app.get('/', () => new Response('Hello from ${name}!'))`,
|
|
48
|
+
`app.get('/api/ping', () => Response.json({ pong: true, time: new Date().toISOString() }))`,
|
|
49
|
+
``
|
|
50
|
+
].join("\n")
|
|
51
|
+
);
|
|
52
|
+
await writeFile(
|
|
53
|
+
join(targetDir, "index.ts"),
|
|
54
|
+
[
|
|
55
|
+
`import { loadEnv, serve } from 'weifuwu'`,
|
|
56
|
+
`import { app } from './app.ts'`,
|
|
57
|
+
``,
|
|
58
|
+
`loadEnv()`,
|
|
59
|
+
`const port = Number(process.env.PORT) || 3000`,
|
|
60
|
+
`serve(app.handler(), { port })`,
|
|
61
|
+
``
|
|
62
|
+
].join("\n")
|
|
63
|
+
);
|
|
64
|
+
await writePackageJson(targetDir, name, version, typesNodeVersion, {});
|
|
65
|
+
await writeCommonFiles(targetDir);
|
|
66
|
+
await finishInit(targetDir, skipInstall);
|
|
67
|
+
}
|
|
68
|
+
async function generateReactSsr(targetDir, name, version, typesNodeVersion, skipInstall) {
|
|
69
|
+
const templateDir = join(pkgRoot, "cli", "template", "react");
|
|
70
|
+
await copyRecursive(templateDir, targetDir);
|
|
71
|
+
await writePackageJson(targetDir, name, version, typesNodeVersion, {
|
|
72
|
+
dependencies: {
|
|
73
|
+
react: "^19",
|
|
74
|
+
"react-dom": "^19",
|
|
75
|
+
"@tailwindcss/postcss": "^4",
|
|
76
|
+
tailwindcss: "^4",
|
|
77
|
+
postcss: "^8"
|
|
78
|
+
},
|
|
79
|
+
devDependencies: {
|
|
80
|
+
"@types/react": "^19",
|
|
81
|
+
"@types/react-dom": "^19",
|
|
82
|
+
esbuild: "^0.28"
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
await writeFile(join(targetDir, ".gitignore"), "node_modules\n.env\n.weifuwu\n");
|
|
86
|
+
await finishInit(targetDir, skipInstall);
|
|
87
|
+
}
|
|
88
|
+
async function copyRecursive(src, dest) {
|
|
89
|
+
const { readdir, stat, copyFile } = await import("node:fs/promises");
|
|
90
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
const srcPath = join(src, entry.name);
|
|
93
|
+
const destPath = join(dest, entry.name);
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
await mkdir(destPath, { recursive: true });
|
|
96
|
+
await copyRecursive(srcPath, destPath);
|
|
97
|
+
} else if (entry.isFile()) {
|
|
98
|
+
await copyFile(srcPath, destPath);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function writePackageJson(targetDir, name, version, typesNodeVersion, extra) {
|
|
103
|
+
const pkg = {
|
|
104
|
+
name,
|
|
105
|
+
type: "module",
|
|
106
|
+
scripts: {
|
|
107
|
+
dev: "node --watch index.ts",
|
|
108
|
+
start: "node index.ts"
|
|
109
|
+
},
|
|
110
|
+
dependencies: {
|
|
111
|
+
weifuwu: "^" + version
|
|
112
|
+
},
|
|
113
|
+
devDependencies: {
|
|
114
|
+
"@types/node": typesNodeVersion
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
if (extra) {
|
|
118
|
+
if (extra.dependencies) {
|
|
119
|
+
Object.assign(pkg.dependencies, extra.dependencies);
|
|
120
|
+
}
|
|
121
|
+
if (extra.devDependencies) {
|
|
122
|
+
Object.assign(pkg.devDependencies, extra.devDependencies);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await writeFile(join(targetDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
126
|
+
}
|
|
127
|
+
async function writeCommonFiles(targetDir) {
|
|
128
|
+
await writeFile(join(targetDir, ".gitignore"), "node_modules\n.env\n.weifuwu\n");
|
|
129
|
+
await writeFile(join(targetDir, ".env"), "PORT=3000\n");
|
|
130
|
+
}
|
|
131
|
+
async function finishInit(targetDir, skipInstall) {
|
|
132
|
+
if (!skipInstall) {
|
|
133
|
+
console.log("\nInstalling dependencies...");
|
|
134
|
+
execSync("npm install", { cwd: targetDir, stdio: "inherit" });
|
|
135
|
+
}
|
|
136
|
+
console.log(`
|
|
137
|
+
\u2705 Created ${targetDir.split("/").pop()}/`);
|
|
138
|
+
console.log(` cd ${targetDir.split("/").pop()}`);
|
|
139
|
+
if (skipInstall) console.log(` npm install`);
|
|
140
|
+
console.log(` npm run dev`);
|
|
141
|
+
}
|
|
142
|
+
var cmd = process.argv[2];
|
|
143
|
+
var HELP = `
|
|
144
|
+
weifuwu \u2014 Web-standard HTTP microframework for Node.js
|
|
145
|
+
|
|
146
|
+
Usage:
|
|
147
|
+
npx weifuwu init <name> Create a new API project
|
|
148
|
+
npx weifuwu init <name> --ssr Create a React SSR project
|
|
149
|
+
npx weifuwu init <name> --skip-install Skip npm install
|
|
150
|
+
npx weifuwu version Print version
|
|
151
|
+
`;
|
|
152
|
+
if (cmd === "version" || cmd === "-v" || cmd === "--version") {
|
|
153
|
+
cmdVersion().catch(console.error);
|
|
154
|
+
} else if (cmd === "init") {
|
|
155
|
+
const { values, positionals } = parseArgs({
|
|
156
|
+
args: process.argv.slice(3),
|
|
157
|
+
options: {
|
|
158
|
+
"skip-install": { type: "boolean" },
|
|
159
|
+
"ssr": { type: "boolean" },
|
|
160
|
+
"react": { type: "boolean" }
|
|
161
|
+
},
|
|
162
|
+
strict: false,
|
|
163
|
+
allowPositionals: true
|
|
164
|
+
});
|
|
165
|
+
const name = positionals[0];
|
|
166
|
+
if (!name) {
|
|
167
|
+
console.error("Usage: npx weifuwu init <name> [--ssr] [--skip-install]");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
cmdInit(name, {
|
|
171
|
+
skipInstall: !!values["skip-install"],
|
|
172
|
+
ssr: !!(values["ssr"] || values["react"])
|
|
173
|
+
}).catch(
|
|
174
|
+
console.error
|
|
175
|
+
);
|
|
176
|
+
} else {
|
|
177
|
+
console.log(HELP);
|
|
178
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Options for setting a cookie. All fields map to standard Set-Cookie attributes. */
|
|
2
|
+
export interface CookieOptions {
|
|
3
|
+
domain?: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
maxAge?: number;
|
|
6
|
+
expires?: Date;
|
|
7
|
+
httpOnly?: boolean;
|
|
8
|
+
secure?: boolean;
|
|
9
|
+
sameSite?: 'strict' | 'lax' | 'none';
|
|
10
|
+
}
|
|
11
|
+
/** Parse cookies from a Request's `Cookie` header.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const cookies = getCookies(req)
|
|
16
|
+
* console.log(cookies.session_id) // value or undefined
|
|
17
|
+
* ``` */
|
|
18
|
+
export declare function getCookies(req: Request): Record<string, string>;
|
|
19
|
+
/** Set a cookie on a Response.
|
|
20
|
+
*
|
|
21
|
+
* Appends a `Set-Cookie` header to the existing response headers.
|
|
22
|
+
* Returns a new Response with the added header.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const res = new Response('ok')
|
|
27
|
+
* return setCookie(res, 'session', token, { httpOnly: true, path: '/' })
|
|
28
|
+
* ``` */
|
|
29
|
+
export declare function setCookie(res: Response, name: string, value: string, options?: CookieOptions): Response;
|
|
30
|
+
/** Delete a cookie by setting `Max-Age=0`.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* return deleteCookie(res, 'session')
|
|
35
|
+
* ``` */
|
|
36
|
+
export declare function deleteCookie(res: Response, name: string, options?: Omit<CookieOptions, 'maxAge'>): Response;
|