weifuwu 0.12.0 → 0.13.1

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 CHANGED
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: weifuwu
3
+ description: Web-standard HTTP framework for Node.js — (req, ctx) => Response
4
+ ---
5
+
1
6
  # weifuwu
2
7
 
3
8
  **Web-standard HTTP framework for Node.js.** `(req, ctx) => Response` — no framework-specific objects, just the Web API your browser already speaks.
@@ -11,26 +16,22 @@ Everything follows the same `(req, ctx) => Response` contract. The Router handle
11
16
  ## Features
12
17
 
13
18
  - **Web Standard** — `Request` / `Response` / `ReadableStream`, zero abstractions
14
- - **Trie router** — static > param > wildcard, sub-router mounting, path params
15
- - **Middleware** — global, path-scoped, route-level — onion model, short-circuit
16
- - **Middleware modules** — `auth()`, `cors()`, `logger()`, `rateLimit()`, `compress()`, `validate()`, `upload()`
17
- - **React SSR + Hydration** — `tsx({ dir })` page.tsx / load.ts / layout.tsx / route.ts / not-found.tsx
18
- - **WebSocket** — `router.ws()` with upgrade middleware (auth before connect)
19
- - **GraphQL** — `graphql(handler)` sub-Router with GraphiQL IDE
20
- - **AI streaming** — `aiStream(handler)` sub-Router via Vercel AI SDK
21
- - **DAG workflow tool** — `runWorkflow()` multi-step execution engine as a single AI SDK `Tool`
22
- - **AI Agent** — `agent()` server-side AI agents with chat/tool-use/knowledge types, OpenAI-compatible, Ollama-ready
23
- - **Messaging** — `messager()` real-time chat with channels, WebSocket, agent routing, webhook support
24
- - **Tenant BaaS** — `tenant()` — multi-tenant dynamic tables, auto REST + GraphQL, row-level isolation, pgvector/HNSW
25
- - **Redis** — `redis()` ioredis client, `ctx.redis`, middleware
26
- - **Queue** — `queue()` Redis-backed job queue with immediate, delayed, and cron scheduling
27
- - **Auth** — `user()` register/login/JWT + OAuth2 Server (authorization code + PKCE + client_credentials)
28
- - **Static files** — `serveStatic()` with ETag, 304, MIME, directory index
29
- - **Cookie** — `getCookies()`, `setCookie()`, `deleteCookie()` immutable
30
- - **Error handling** — global `onError()`
31
- - **Deploy** — `deploy()` — self-hosted PaaS: multi-app reverse proxy, subdomain routing, zero-downtime updates, auto SSL, Git-based deployment
32
- - **Zero build** — native TypeScript in Node.js v24+
33
- - **Zero deps** (core) — only `node:http` and `node:stream`
19
+ - **Zero build** — native TypeScript in Node.js v24+, zero deps (core)
20
+ - **Trie router** — static > param > wildcard, sub-router mounting, WebSocket
21
+ - **Middleware** — global/path-scoped/route-level onion model with short-circuit
22
+ - **Modules** — auth, validation, upload, compression, rate-limit, cookies, static files, CORS, logging
23
+ - **React SSR** — `tsx()` pages, layouts, loaders, route handlers, Tailwind CSS, HMR
24
+ - **PostgreSQL** — schema builder with type-safe DDL + CRUD, transactions, vector search
25
+ - **Auth** — password + JWT + OAuth2 Server (authorization code / PKCE / client_credentials)
26
+ - **Real-time** — WebSocket, messaging channels with agent routing
27
+ - **AI** — streaming endpoint, DAG workflow tool, AI agents with RAG and tool-use
28
+ - **Data** — Redis client, job queue with cron scheduling
29
+ - **Multi-tenant BaaS** — dynamic tables, auto REST + GraphQL, row-level isolation
30
+ - **Deploy** — self-hosted PaaS: multi-app proxy, zero-downtime updates, auto SSL
31
+ - **i18n** — locale detection, JSON translations, `ctx.t()`
32
+ - **Email** — SMTP or custom transport
33
+ - **Health check** — configurable `/health` endpoint
34
+ - **Test utilities** — `createTestServer()` — one-line test server setup
34
35
 
35
36
  ## Quick start
36
37
 
@@ -41,1444 +42,111 @@ import { serve } from 'weifuwu'
41
42
  serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
42
43
  ```
43
44
 
44
- ### React + Tailwind
45
-
46
- ```bash
47
- npm install weifuwu
48
- mkdir -p ui/pages
49
- ```
50
-
51
- ```ts
52
- // app.ts
53
- import { serve, Router } from 'weifuwu'
54
-
55
- const app = new Router()
56
- app.use('/', await tsx({ dir: './ui/' }))
57
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
58
- ```
59
-
60
- ```tsx
61
- // ui/pages/page.tsx
62
- export default function Home() {
63
- return <h1 className="text-3xl font-bold text-blue-600">Hello</h1>
64
- }
65
- ```
66
-
67
- ```bash
68
- node app.ts
69
- ```
70
-
71
- Open http://localhost:3000 — Tailwind CSS is compiled automatically, pages hot-reload on save.
72
-
73
- ## Router
74
-
75
- ```ts
76
- import { serve, Router } from 'weifuwu'
77
-
78
- const app = new Router()
79
- .use((req, ctx, next) => {
80
- console.log(`${req.method} ${new URL(req.url).pathname}`)
81
- return next(req, ctx)
82
- })
83
- .get('/hello/:name', (req, ctx) =>
84
- Response.json({ message: `Hello, ${ctx.params.name}!` }),
85
- )
86
- .post('/data', async (req, ctx) => {
87
- const body = await req.json()
88
- return Response.json(body, { status: 201 })
89
- })
90
-
91
- serve(app.handler(), { port: 3000 })
92
- ```
93
-
94
- ## Built-in middleware
95
-
96
- ### Auth
97
-
98
- ```ts
99
- import { auth } from 'weifuwu'
100
-
101
- // Static bearer token
102
- app.use(auth({ token: 'sk-123' }))
103
-
104
- // Custom verify (JWT, DB, etc.) — return object to set ctx.user
105
- app.use(auth({
106
- verify: async (token) => {
107
- const user = await db.findUserByToken(token)
108
- return user ? { sub: user.id, role: user.role } : null
109
- },
110
- }))
111
-
112
- // Proxy validation to external auth service
113
- app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
114
-
115
- // Custom header
116
- app.use(auth({ header: 'X-API-Key', token: 'my-key' }))
117
- ```
118
-
119
- ### CORS
120
-
121
- ```ts
122
- import { cors } from 'weifuwu'
123
-
124
- app.use(cors()) // allow all
125
- app.use(cors({ origin: ['https://example.com'] })) // whitelist
126
- app.use(cors({ origin: (o) => o.endsWith('.trusted.com') ? o : false }))
127
- app.use(cors({ credentials: true, maxAge: 3600 }))
128
- ```
129
-
130
- ### Logger
131
-
132
- ```ts
133
- import { logger } from 'weifuwu'
134
-
135
- app.use(logger()) // GET /hello 200 5ms
136
- app.use(logger({ format: 'combined' })) // with query params
137
- ```
138
-
139
- ### Rate limit
140
-
141
- ```ts
142
- import { rateLimit } from 'weifuwu'
143
-
144
- app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min
145
- app.get('/api', rateLimit({ max: 10 }), handler) // per-route
146
-
147
- // Custom key (by API key, user ID, etc.)
148
- app.use(rateLimit({
149
- max: 1000,
150
- key: (req) => req.headers.get('x-api-key') ?? 'anonymous',
151
- }))
152
- ```
153
-
154
- ### Compression
155
-
156
- ```ts
157
- import { compress } from 'weifuwu'
158
-
159
- app.use(compress()) // brotli > gzip > deflate
160
- app.use(compress({ threshold: 2048 })) // only compress > 2KB
161
- ```
162
-
163
- ## Validation
164
-
165
- ```ts
166
- import { z } from 'zod'
167
- import { validate } from 'weifuwu'
168
-
169
- const CreateUser = z.object({
170
- name: z.string().min(1),
171
- email: z.string().email(),
172
- })
173
-
174
- router.post('/users',
175
- validate({ body: CreateUser }),
176
- (req, ctx) => {
177
- // ctx.parsed.body — typed & validated
178
- },
179
- )
180
- ```
181
-
182
- ## File upload
183
-
184
- ```ts
185
- import { upload } from 'weifuwu'
186
-
187
- router.post('/upload',
188
- upload({ dir: './uploads', maxFileSize: 10_485_760 }),
189
- (req, ctx) => {
190
- // ctx.parsed.files.avatar → { name, type, size, path }
191
- // ctx.parsed.fields.title → 'hello'
192
- },
193
- )
194
- ```
195
-
196
- ## Cookie
197
-
198
- ```ts
199
- import { getCookies, setCookie, deleteCookie } from 'weifuwu'
200
-
201
- // Read
202
- const cookies = getCookies(req) // { session: 'abc' }
203
-
204
- // Set (immutable — returns new Response)
205
- let res = new Response('ok')
206
- res = setCookie(res, 'session', 'token', { httpOnly: true, secure: true, maxAge: 3600 })
207
-
208
- // Delete
209
- res = deleteCookie(res, 'session')
210
- ```
211
-
212
- ## Static files
213
-
214
- ```ts
215
- import { serveStatic } from 'weifuwu'
216
-
217
- router.get('/static/*', serveStatic('./public'))
218
- ```
219
-
220
- Features: MIME type detection (20+ types), ETag + If-None-Match (304), directory index (index.html), path traversal protection, Cache-Control.
221
-
222
- ## PostgreSQL
223
-
224
- Built-in PostgreSQL client — connection management, type-safe DDL, transactions, and module lifecycle.
225
-
226
- ```ts
227
- import { serve, Router, postgres } from 'weifuwu'
228
-
229
- const app = new Router()
230
- const pg = postgres() // reads DATABASE_URL
231
- app.use(pg) // injects ctx.sql into handlers
232
- ```
233
-
234
- ### Type-safe DDL with schema builder
235
-
236
- Define tables declaratively with type inference — no raw SQL for common operations, no Zod needed:
237
-
238
- ```ts
239
- import { pgTable, serial, uuid, text, integer, boolean, timestamptz, jsonb, sql } from 'weifuwu'
240
-
241
- const users = pgTable('_users', {
242
- id: serial('id').primaryKey(),
243
- name: text('name').notNull(),
244
- email: text('email').unique().notNull(),
245
- age: integer('age'),
246
- active: boolean('active').default(true),
247
- createdAt: timestamptz('created_at').default(sql`NOW()`),
248
- metadata: jsonb<{ role: string }>('metadata'),
249
- })
250
- ```
251
-
252
- Supports 10 column types:
253
- | Builder | DDL | TS Type |
254
- |---------|-----|---------|
255
- | `serial()` | `SERIAL` | `number` |
256
- | `uuid()` | `UUID` | `string` |
257
- | `text()` | `TEXT` | `string` |
258
- | `integer()` | `INTEGER` | `number` |
259
- | `boolean()` | `BOOLEAN` | `boolean` |
260
- | `timestamptz()` | `TIMESTAMPTZ` | `string` |
261
- | `jsonb<T>()` | `JSONB` | `T` |
262
- | `textArray()` | `TEXT[]` | `string[]` |
263
- | `vector(name, dims)` | `vector(N)` | `number[]` |
264
-
265
- Column constraints chainable: `.primaryKey()`, `.notNull()`, `.nullable()`, `.default(value | sql\`...\`)`, `.unique()`, `.references(table, column?, onDelete?)`.
266
-
267
- ### DDL execution
268
-
269
- ```ts
270
- await users.create() // CREATE TABLE IF NOT EXISTS
271
- await users.createIndex('email') // CREATE INDEX
272
- await users.createUniqueIndex('slug') // CREATE UNIQUE INDEX
273
- await users.createIndex('created_at', { desc: true })
274
- await users.createIndex(['a', 'b']) // multi-column
275
- await users.createIndex('embedding', { // pgvector HNSW
276
- type: 'hnsw', operator: 'vector_cosine_ops',
277
- })
278
- await users.drop({ cascade: true })
279
- ```
280
-
281
- ### Type-safe CRUD with BoundTable
282
-
283
- Two usage paths — use `pg.table()` when you have a `pg` handle, or `pgTable()` with explicit `sql`:
284
-
285
- ```ts
286
- // pg.table() — auto-binds sql, no need to pass it
287
- const users = pg.table('_users', {
288
- id: serial('id').primaryKey(),
289
- name: text('name').notNull(),
290
- email: text('email').unique(),
291
- active: boolean('active').default(true),
292
- createdAt: timestamptz('created_at').default(sql`NOW()`),
293
- })
294
-
295
- // INSERT ... RETURNING * — auto-strips serial id
296
- const user = await users.insert({ name: 'Alice', email: 'alice@test.com' })
297
- // → { id: 1, name: 'Alice', email: 'alice@test.com', active: true, ... }
298
-
299
- // SELECT ... WHERE id = ? LIMIT 1
300
- const found = await users.findById(1)
301
-
302
- // SELECT ... WHERE ... [ORDER BY ...] [LIMIT ...] [OFFSET ...]
303
- const admins = await users.find({ role: 'admin' })
304
- const sorted = await users.find({ active: true }, { orderBy: { name: 'asc' } })
305
- const page = await users.find(undefined, { limit: 10, offset: 0 })
306
- const filtered = await users.find({ role: 'admin' }, { orderBy: { name: 'desc' }, limit: 5 })
307
-
308
- // UPDATE ... SET ... WHERE ... RETURNING *
309
- const updated = await users.update({ id: 1 }, { name: 'Bob' })
310
- // With SQL expressions:
311
- await users.update({ id: 1 }, { name: 'Bob', updated_at: sql`NOW()` })
312
-
313
- // DELETE ... WHERE ... RETURNING 1
314
- const ok = await users.delete({ id: 1 })
315
- ```
316
-
317
- When using `pgTable()` directly (without `pg`), pass `sql` as the first argument:
318
-
319
- ```ts
320
- const t = pgTable('_users', { ... })
321
- await t.insert(ctx.sql, { name: 'Alice' })
322
- await t.find(ctx.sql, { role: 'admin' }, { orderBy: { name: 'asc' } })
323
- ```
324
-
325
- ### Complex queries use raw SQL
326
-
327
- ```ts
328
- app.get('/users/stats', async (req, ctx) => {
329
- const rows = await ctx.sql`
330
- SELECT u.*, count(p.id) as posts
331
- FROM ${users} u LEFT JOIN posts p ON p.user_id = u.id
332
- GROUP BY u.id
333
- `
334
- return Response.json(rows)
335
- })
336
- ```
337
-
338
- ### Transactions
339
-
340
- ```ts
341
- const result = await pg.transaction(async (tx) => {
342
- const [user] = await tx`INSERT INTO "_users" (...) VALUES (...) RETURNING *`
343
- const [wallet] = await tx`INSERT INTO "_wallets" ("user_id") VALUES (${user.id}) RETURNING *`
344
- return { user, wallet }
345
- })
346
- ```
347
-
348
- ### Connection lifecycle
349
-
350
- ```ts
351
- const pg = postgres() // reads DATABASE_URL
352
- const pg = postgres('postgres://...') // explicit connection
353
- const pg = postgres({
354
- connection: 'postgres://...',
355
- max: 10, // pool size
356
- ssl: { rejectUnauthorized: false }, // SSL options
357
- idle_timeout: 30, // idle timeout (s)
358
- connect_timeout: 10, // connection timeout (s)
359
- closeTimeout: 5, // close grace period (s)
360
- signal: ac.signal, // abort → sql.end()
361
- })
362
- await pg.close()
363
- ```
364
-
365
- ### Module base class
366
-
367
- Every database module (`opencode`, `messager`, `tenant`, `agent`, `user`) extends `PgModule`:
368
-
369
- ```ts
370
- import { PgModule } from 'weifuwu'
371
-
372
- class MyModule extends PgModule {
373
- constructor(pg: PostgresClient) {
374
- super(pg) // sets this.sql = pg.sql
375
- }
376
- async migrate() { /* override */ }
377
- // close() inherited — calls pg.close() automatically
378
- }
379
- ```
380
-
381
- ## Opencode
382
-
383
- AI programming assistant — chat with LLM agents that have access to filesystem tools, skills, and isolated session workspaces.
384
-
385
- ```ts
386
- import { serve, Router, postgres, opencode } from 'weifuwu'
387
-
388
- const app = new Router()
389
- const pg = postgres()
390
- const oc = await opencode({ pg, permissions: { ... } })
391
-
392
- await oc.migrate()
393
- app.use('/opencode', await oc.router())
394
- app.ws('/opencode', oc.wsHandler())
395
-
396
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
397
- ```
398
-
399
- ### Session-isolated workspaces
400
-
401
- Each session gets its own sandbox directory — tools operate within it, files cannot escape:
402
-
403
- ```
404
- cwd/.sessions/opencode/1/ ← session 1's workspace
405
- cwd/.sessions/opencode/2/ ← session 2's workspace
406
- cwd/.sessions/chat/3/ ← different mount point
407
- ```
408
-
409
- Workspaces are computed from `cwd { ctx.mountPath } { sessionId }`. The system prompt shows the session's workspace so the LLM knows where it is.
410
-
411
- ### Tools
412
-
413
- | Tool | Description |
414
- |------|-------------|
415
- | `bash` | Execute shell commands in the workspace |
416
- | `read` | Read files with offset/limit |
417
- | `write` | Create or overwrite files |
418
- | `edit` | Exact string replacements |
419
- | `grep` | Regex content search |
420
- | `glob` | Glob pattern file search |
421
- | `web` | Fetch URL content |
422
- | `question` | Ask the user for input |
423
- | `skill` | Load a skill on demand |
424
-
425
- ### Skills
426
-
427
- Skills are discovered from filesystem and loaded on demand via the `skill` tool — no system prompt bloat:
428
-
429
- - Project: `.opencode/skills/{name}/SKILL.md`
430
- - Global: `~/.config/opencode/skills/{name}/SKILL.md`
431
- - Also reads: `.claude/skills/`, `.agents/skills/` (project + global)
432
-
433
- ```ts
434
- const oc = await opencode({
435
- pg,
436
- skills: [{ name: 'git', description: 'Git workflow', content: '...' }],
437
- })
438
- ```
439
-
440
- ### Permissions
441
-
442
- Control tool access per conversation:
45
+ ### Full app
443
46
 
444
47
  ```ts
445
- const oc = await opencode({
446
- pg,
447
- permissions: {
448
- bash: { allow: true },
449
- read: { allow: true },
450
- write: { allow: false },
451
- edit: { allow: false },
452
- skill: { '*': { allow: true }, 'internal-*': { allow: false } },
453
- },
454
- })
455
- ```
456
-
457
- ### Workspace isolation
458
-
459
- ```ts
460
- const oc = await opencode({ pg, permissions })
461
- // All sessions inherit the instance's workspace (default: process.cwd())
462
- // Sessions cannot override their workspace
463
- // Different mount points = different opencode() instances = isolated workspaces
464
- ```
465
-
466
- ```ts
467
- import { serve, Router, postgres, user } from 'weifuwu'
48
+ import { serve, Router, postgres, user, aiStream, graphql } from 'weifuwu'
49
+ import { openai } from '@ai-sdk/openai'
468
50
 
469
51
  const app = new Router()
470
52
  const pg = postgres()
471
- await pg.migrate()
472
53
 
54
+ // Auth
473
55
  const auth = user({ pg, jwtSecret: process.env.JWT_SECRET! })
474
-
475
- // POST /auth/register { email, password, name }
476
- // POST /auth/login { email, password }
477
- // GET /auth/oauth/authorize?client_id=...&redirect_uri=...&response_type=code
478
- // POST /auth/oauth/consent
479
- // POST /auth/oauth/token (grant_type=authorization_code|client_credentials)
56
+ await auth.migrate()
480
57
  app.use('/auth', auth.router())
481
58
 
482
- // Protected routes — verifies JWT, sets ctx.user
483
- app.get('/me', auth.middleware(), async (req, ctx) => {
484
- return Response.json(ctx.user)
485
- // { id, email, name, role }
486
- })
487
- ```
488
-
489
- Password hashing uses `crypto.scryptSync` + `timingSafeEqual` (Node.js built-in, zero deps). JWT tokens use the `jsonwebtoken` package. The users table (`_users` by default) is auto-created on first `migrate()`.
490
-
491
- ### OAuth2 Server
492
-
493
- Enable OAuth2 Server to let third-party apps (SPA, mobile, microservices) authenticate users through your app.
494
-
495
- ```ts
496
- const auth = user({
497
- pg,
498
- jwtSecret: process.env.JWT_SECRET!,
499
- oauth2: { server: true },
500
- })
501
-
502
- await auth.migrate() // creates _users + _oauth2_clients + _oauth2_codes + _oauth2_tokens
503
-
504
- // Register a client app (programmatic — CLI, admin UI, seed script)
505
- const client = await auth.registerClient({
506
- name: 'My SPA',
507
- redirectUris: ['https://myapp.com/callback'],
508
- })
509
- // → { clientId, clientSecret, name, redirectUris }
510
-
511
- // Use auth middleware to protect routes — OAuth2 JWT tokens work seamlessly
512
- app.get('/api/data', auth.middleware(), handler)
513
- ```
514
-
515
- #### Supported Grant Types
516
-
517
- | Grant | Use Case | PKCE |
518
- |-------|----------|------|
519
- | `authorization_code` (with client_secret) | Server-side apps | Optional |
520
- | `authorization_code` (with `code_challenge`/`code_verifier`) | SPA / Mobile apps | Required |
521
- | `client_credentials` | Machine-to-machine | — |
522
-
523
- #### Flow (Authorization Code + PKCE)
524
-
525
- ```
526
- 1. 第三方 App 引导用户:
527
- GET /oauth/authorize?client_id=xxx&redirect_uri=https://app.com/cb
528
- &response_type=code&code_challenge=S256&state=yyy
529
-
530
- 2. 用户未登录 → 302 到 /login?redirect=... → 登录后自动回到授权页
531
-
532
- 3. 用户确认授权 → POST /oauth/consent { approve: true, client_id, ... }
533
- 302 redirect_uri?code=xxx&state=yyy
534
-
535
- 4. 第三方 App POST /oauth/token
536
- { grant_type: authorization_code, code, client_id, client_secret,
537
- redirect_uri, code_verifier }
538
- → { access_token, token_type: "Bearer", expires_in, refresh_token }
539
-
540
- 5. access_token 是标准 JWT,auth.middleware() 和 auth.verify() 直接可用
541
- ```
542
-
543
- #### Client Management
544
-
545
- ```ts
546
- const client = await auth.registerClient({ name, redirectUris })
547
- const found = await auth.getClient(client.clientId)
548
- await auth.revokeClient(client.clientId)
549
- ```
550
-
551
- #### Using OAuth2 Tokens with the Built-in Auth Middleware
552
-
553
- OAuth2 Server 签发的 `access_token` 与密码登录的 JWT 使用同一 `jwtSecret`,payload 向下兼容(`sub`、`email`、`role`),所以 `auth()` 无需任何修改即可验证 OAuth2 签发的 token:
554
-
555
- ```ts
556
- import { auth } from 'weifuwu'
557
-
558
- // 同一个 auth() 中间件同时支持密码登录 JWT 和 OAuth2 JWT
559
- app.get('/api', auth({ verify: (token) => auth.verify(token) }), handler)
560
- ```
561
-
562
- For `client_credentials` tokens (machine-to-machine), `verify()` returns `null` since no user is associated.
563
-
564
- ## Tenant BaaS
565
-
566
- Built-in multi-tenant backend-as-a-service — define tables at runtime via API, get RESTful CRUD + GraphQL automatically, with row-level tenant isolation.
567
-
568
- ```ts
569
- import { serve, Router, postgres, user, tenant } from 'weifuwu'
570
-
571
- const pg = postgres()
572
- const u = user({ pg, jwtSecret: process.env.JWT_SECRET! })
573
- const t = tenant({ pg, usersTable: '_users' })
574
-
575
- await pg.migrate()
576
- await u.migrate()
577
- await t.migrate() // creates _tenants, _tenant_members, _user_tables
578
-
579
- const app = new Router()
580
- app.use('/auth', u.router())
581
- app.use('/api', u.middleware()) // → ctx.user
582
- app.use('/api', t.middleware()) // → ctx.tenant
583
- app.use('/api', t.router()) // → management + data CRUD
584
- app.use('/graphql', t.graphql()) // → dynamic GraphQL
585
- ```
586
-
587
- ### System tables
588
-
589
- | Table | Purpose |
590
- |-------|---------|
591
- | `_tenants` | Tenant records (`id TEXT PK DEFAULT gen_random_uuid()`, `name`, `created_at`) |
592
- | `_tenant_members` | User-tenant membership (`tenant_id`, `user_id`, `role`) |
593
- | `_user_tables` | Dynamic table definitions (`tenant_id`, `slug`, `fields JSONB`) |
594
-
595
- ### Dynamic table API
596
-
597
- Create a table at runtime:
598
-
599
- ```json
600
- POST /api/tables
601
- {
602
- "slug": "articles",
603
- "fields": [
604
- { "name": "title", "type": "string", "required": true },
605
- { "name": "content", "type": "text" },
606
- { "name": "status", "type": "enum", "options": ["draft", "published"], "default": "draft" },
607
- { "name": "views", "type": "integer", "default": 0 },
608
- { "name": "embedding", "type": "vector", "dimensions": 1536, "index": "hnsw" }
609
- ]
610
- }
611
- ```
612
-
613
- → Creates a PostgreSQL table with `id SERIAL PK`, `tenant_id TEXT NOT NULL`, and the specified columns, plus indexes. The table name is internally scoped to the tenant.
614
-
615
- ### Field types
616
-
617
- | type | PostgreSQL | Index support |
618
- |------|-----------|---------------|
619
- | `string` | `TEXT` | `true`, `unique` |
620
- | `integer` | `INTEGER` | `true`, `desc`, `unique` |
621
- | `float` | `DOUBLE PRECISION` | `true`, `desc` |
622
- | `boolean` | `BOOLEAN` | `true` |
623
- | `text` | `TEXT` | `true` |
624
- | `datetime` | `TIMESTAMPTZ` | `true`, `desc` |
625
- | `date` | `DATE` | `true`, `desc` |
626
- | `enum` | `TEXT` (with validation) | `true` |
627
- | `json` | `JSONB` | `gin` |
628
- | `vector` | `vector(n)` (pgvector) | `hnsw` (HNSW, vector_cosine_ops) |
629
-
630
- ### Relationships
631
-
632
- Declare a foreign key via the `relation` field:
633
-
634
- ```json
635
- { "name": "article_id", "type": "integer", "relation": { "table": "articles", "onDelete": "cascade" } }
636
- ```
637
-
638
- Supported relationship patterns:
639
-
640
- | Pattern | Detection | REST | GraphQL |
641
- |---------|-----------|------|---------|
642
- | **belongs_to** | Field with `relation` | — | `comment.article` resolver |
643
- | **has_many** | Another table has a relation pointing here | `GET /api/articles/:id/comments` | `article.comments` resolver |
644
- | **M2M** | Junction table with exactly two relation fields | `GET /api/articles/:id/tags` (bypasses junction) | `article.tags` / `tag.articles` resolver |
645
- | **Self-ref** | Relation field pointing to same table | — | With depth control |
646
-
647
- ### RESTful API
648
-
649
- All routes require `ctx.tenant` (set by `t.middleware()`). All queries automatically filter by `tenant_id`.
650
-
651
- | Route | Method | Description |
652
- |-------|--------|-------------|
653
- | `/sys/tenants` | POST | Create tenant, caller becomes admin |
654
- | `/sys/tenants` | GET | List user's tenants |
655
- | `/sys/tenants/invite` | POST | Invite user by email (admin) |
656
- | `/sys/tenants/members/:userId` | DELETE | Remove member (admin) |
657
- | `/sys/tables` | POST/GET | Create / list dynamic tables |
658
- | `/sys/tables/:slug` | GET/PATCH/DELETE | Get schema / add fields / drop table |
659
- | `/:slug` | GET | List rows (limit, offset, sort) |
660
- | `/:slug` | POST | Create row |
661
- | `/:slug/:id` | GET/PATCH/DELETE | Get / update / delete row |
662
- | `/:slug/:id/:_nested` | GET | List related rows (has_many / M2M) |
663
- | `/:slug/:id/:_nested` | POST | Create related row (auto-fills relation field) |
664
-
665
- ### Vector search
666
-
667
- ```http
668
- GET /api/articles?search_vector=[0.1,0.2,...]&search_field=embedding&search_limit=10
669
- ```
670
-
671
- Returns rows ordered by cosine distance (`<=>`), includes `_distance` field. Supports `l2` (`<->`) and `ip` (`<#>`):
672
-
673
- ```http
674
- GET /api/articles?search_vector=[...]&search_field=embedding&search_distance=l2
675
- ```
676
-
677
- ### GraphQL
678
-
679
- Dynamic GraphQL schema generated per-request based on the authenticated tenant's tables:
680
-
681
- ```graphql
682
- type Article {
683
- id: ID!
684
- title: String!
685
- content: String
686
- status: String
687
- comments(limit: Int, offset: Int): [Comment!]!
688
- }
689
-
690
- type Query {
691
- articles(limit: Int, offset: Int): [Article!]!
692
- getArticle(id: ID!): Article
693
- }
694
-
695
- type Mutation {
696
- createArticle(data: CreateArticleInput!): Article!
697
- updateArticle(id: ID!, data: PatchArticleInput!): Article!
698
- deleteArticle(id: ID!): Boolean!
699
- }
700
- ```
701
-
702
- Built with `graphql-js` native constructors (`GraphQLObjectType`), no SDL generation, no `makeExecutableSchema`.
703
-
704
- ### Middleware
705
-
706
- `t.middleware()` extracts the tenant context:
707
-
708
- 1. Requires `ctx.user` (from `u.middleware()`)
709
- 2. Looks up user's tenant memberships
710
- 3. Single tenant → automatically set `ctx.tenant`
711
- 4. Multiple tenants → require `X-Tenant-ID` header, return 300 with tenant list if missing
712
- 5. No tenants → 403
713
-
714
- ### Tenant lifecycle
715
-
716
- ```ts
717
- const t = tenant({ pg, usersTable: '_users' })
718
-
719
- // Create a tenant — the caller becomes admin
720
- const tenant = await (await fetch('http://localhost/api/sys/tenants', {
721
- method: 'POST',
722
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer <jwt>' },
723
- body: JSON.stringify({ name: 'Acme Corp' }),
724
- })).json()
725
- // → { id: "uuid", name: "Acme Corp", created_at: "..." }
726
-
727
- // Invite a member
728
- await fetch('http://localhost/api/sys/tenants/invite', {
729
- method: 'POST',
730
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer <jwt>' },
731
- body: JSON.stringify({ email: 'colleague@acme.com', role: 'member' }),
732
- })
733
- ```
734
-
735
- ## AI Agent
736
-
737
- Server-side AI agents with OpenAI-compatible API. Built-in chat, tool-use (tool-calling), and knowledge (RAG) types. Works out of the box with Ollama or any OpenAI-compatible provider.
738
-
739
- ```ts
740
- import { agent } from 'weifuwu'
741
-
742
- const agents = agent({ pg })
743
-
744
- await agents.migrate()
745
- app.use('/api', agents.router())
746
- ```
747
-
748
- | Type | Description | Execution |
749
- |------|-------------|-----------|
750
- | `chat` | Pure conversation | `streamText()` / `generateText()` |
751
- | `tool-use` | Tool-calling agent | `streamText({ tools })` |
752
-
753
- ### Knowledge (RAG)
754
-
755
- Add documents to any agent — `searchKnowledge` tool auto-injected:
756
-
757
- ```ts
758
- await agents.addKnowledge(agentId, 'Title', 'Document content...')
759
- // The agent automatically calls searchKnowledge when answering
760
- ```
761
-
762
- ### Streaming
763
-
764
- ```http
765
- POST /agents/:id/run { input: "hello", stream: true }
766
- → event-stream (fullStream SSE: text-delta, tool-call, tool-result, finish)
767
- ```
768
-
769
- ### Programmatic API
770
-
771
- ```ts
772
- const result = await agents.run(agentId, { input: 'hello', stream: false })
773
- // { output: "Hello!", elapsed: 1234 }
774
- ```
775
-
776
- ## Messager
777
-
778
- Real-time chat with channels, WebSocket, and agent routing.
779
-
780
- ```ts
781
- import { messager, agent } from 'weifuwu'
782
-
783
- const agents = agent({ pg })
784
- const msg = messager({ pg, agents })
785
-
786
- await msg.migrate()
787
- app.use('/api', msg.router())
788
- app.ws('/ws', u.middleware(), msg.wsHandler())
789
- ```
790
-
791
- ### Channels
792
-
793
- ```http
794
- POST /channels name, type (channel|dm), members
795
- GET /channels
796
- GET /channels/:id
797
- ```
798
-
799
- ### Messages
800
-
801
- ```http
802
- GET /channels/:id/messages ?limit=50&before={id}
803
- POST /channels/:id/messages content, sender_type, type
804
- POST /channels/:id/read last_message_id
805
- ```
806
-
807
- ### WebSocket
808
-
809
- ```json
810
- { "type": "message", "channel_id": 1, "content": "Hi" }
811
- { "type": "typing", "channel_id": 1, "is_typing": true }
812
- { "type": "read", "channel_id": 1, "last_message_id": 42 }
813
- ```
814
-
815
- ### Programmatic send
816
-
817
- ```ts
818
- await msg.send(channelId, 'System message', { sender_type: 'system' })
819
- ```
820
-
821
- ## WebSocket
822
- message(ws, ctx, data) {
823
- ws.send(`echo: ${data}`)
824
- },
825
- close(ws, ctx) {
826
- console.log('disconnected')
827
- },
828
- })
829
-
830
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
831
- ```
832
-
833
- Middleware runs **before** WebSocket upgrade — you can reject connections with HTTP status codes:
834
-
835
- ```ts
836
- app.ws('/secure',
837
- (req, _ctx, next) => {
838
- const auth = req.headers.get('Authorization')
839
- if (!auth) return Response.json({ error: 'Unauthorized' }, { status: 401 })
840
- return next(req, _ctx)
841
- },
842
- { open(ws) { ws.send('authorized') } },
843
- )
844
- ```
845
-
846
- ## GraphQL
847
-
848
- GraphQL endpoint with GraphiQL IDE. Mount as a sub-Router:
849
-
850
- ```ts
851
- import { serve, Router, graphql } from 'weifuwu'
59
+ // AI streaming
60
+ const chat = await aiStream(async (req) => ({
61
+ model: openai('gpt-4o'),
62
+ messages: (await req.json()).messages,
63
+ }))
64
+ app.use('/chat', chat.router())
852
65
 
853
- const app = new Router()
66
+ // GraphQL
854
67
  const gql = graphql(() => ({
855
- schema: `
856
- type Query { hello: String }
857
- type Mutation { setMessage(msg: String!): String }
858
- `,
859
- resolvers: {
860
- Query: { hello: () => 'world' },
861
- Mutation: { setMessage: (_, { msg }) => msg },
862
- },
863
- graphiql: true,
68
+ schema: `type Query { hello: String }`,
69
+ resolvers: { Query: { hello: () => 'world' } },
864
70
  }))
865
71
  app.use('/graphql', gql.router())
866
72
 
867
- serve(app.handler(), { port: 3000 })
868
- ```
869
-
870
- The handler receives `(req, ctx)` so you can customize the schema based on the request.
871
-
872
- ## AI streaming
873
-
874
- Server-sent event streaming via the Vercel AI SDK:
875
-
876
- ```ts
877
- import { serve, Router, aiStream } from 'weifuwu'
878
- import { openai } from '@ai-sdk/openai'
879
-
880
- const app = new Router()
881
- const chat = await aiStream(async (req, ctx) => {
882
- const { messages } = await req.json()
883
- return { model: openai('gpt-4o'), messages }
884
- })
885
- app.use('/chat', chat.router())
73
+ // Static files
74
+ app.get('/static/*', serveStatic('./public'))
886
75
 
887
76
  serve(app.handler(), { port: 3000 })
888
77
  ```
889
78
 
890
- ## runWorkflow
891
-
892
- Multi-step DAG execution engine — packaged as a single AI SDK `Tool`. Use it with `streamText()` or `generateText()` when the LLM needs conditional logic, loops, or multi-step tool orchestration.
893
-
894
- ```ts
895
- import { tool, streamText } from 'ai'
896
- import { runWorkflow } from 'weifuwu'
897
- import { z } from 'zod'
898
-
899
- const tools = {
900
- queryUser: tool({
901
- description: 'Query user info',
902
- inputSchema: z.object({ userId: z.string() }),
903
- execute: async ({ userId }) => ({ id: userId, email: 'user@test.com', name: 'Test' }),
904
- }),
905
- sendEmail: tool({
906
- description: 'Send an email',
907
- inputSchema: z.object({ to: z.string(), subject: z.string() }),
908
- execute: async ({ to, subject }) => ({ sent: true }),
909
- }),
910
- runWF: runWorkflow({ tools: { queryUser, sendEmail } }),
911
- }
912
-
913
- // Use in any streamText call — the LLM can decide when to trigger a workflow
914
- const result = await streamText({
915
- model,
916
- tools,
917
- messages: [{ role: 'user', content: '查询用户123,如果存在则发送欢迎邮件' }],
918
- })
919
- ```
920
-
921
- ### Node types
922
-
923
- 7 built-in node types for defining the execution graph:
924
-
925
- | Node | Purpose | Input |
926
- |------|---------|-------|
927
- | `call` | Call a registered AI SDK Tool | `{ tool: "name", args: {...} }` |
928
- | `set` | Assign a variable | `{ name: "x", value: 42 }` |
929
- | `get` | Read a variable | `{ name: "x" }` |
930
- | `eval` | Evaluate an expression | `{ expression: "$var.x + 1" }` |
931
- | `if` | Conditional branch | `{ conditions: [{ test: ..., body: [nodes] }] }` |
932
- | `while` | Loop | `{ condition: "$var.i < 5" }, body: [nodes]` |
933
- | `http` | HTTP request | `{ url: "https://...", method: "GET" }` |
934
-
935
- ### Reference syntax
936
-
937
- | Pattern | Meaning | Example |
938
- |---------|---------|---------|
939
- | `$var.x` | Variable `x` | `$var.counter` |
940
- | `$nodes.u.output` | Full output of node `u` | `$nodes.u.output` |
941
- | `$nodes.u.output.field` | Specific field | `$nodes.u.output.email` |
942
- | `$input.userId` | Input param | `$input.userId` |
943
-
944
- ### LLM generation
945
-
946
- Pass a `model` to `runWorkflow` — the LLM generates the workflow JSON from a goal:
947
-
948
- ```ts
949
- const runWF = runWorkflow({
950
- tools: { queryUser, sendEmail },
951
- model: openai('gpt-4o'),
952
- })
953
-
954
- const result = await streamText({
955
- model,
956
- tools: { runWF },
957
- })
958
- ```
959
-
960
- The LLM calls `runWF` with a goal, and `runWorkflow` internally calls `generateText` to produce the workflow nodes, then executes them.
961
-
962
- ## React pages with tsx()
963
-
964
- ```ts
965
- import { serve, Router } from 'weifuwu'
966
- import { tsx } from 'weifuwu/tsx'
967
-
968
- const app = new Router()
969
- app.use('/', await tsx({ dir: './ui/' }))
970
-
971
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
972
- ```
973
-
974
- ### Directory structure
975
-
976
- ```
977
- ui/
978
- ├── pages/ ← 页面文件
979
- │ ├── page.tsx → GET / (React component, default export)
980
- │ ├── layout.tsx → root layout (HTML shell, receives req/ctx, NOT hydrated)
981
- │ ├── not-found.tsx → 404 error page (rendered for unmatched routes, wrapped in layout)
982
- │ ├── about/page.tsx → GET /about
983
- │ ├── blog/[slug]/
984
- │ │ ├── page.tsx → GET /blog/:slug
985
- │ │ ├── load.ts → data fetching (server-only, default export)
986
- │ │ └── route.ts → POST /blog/:slug (API, named exports POST/PUT/DELETE/...)
987
- │ ├── blog/layout.tsx → /blog/* layout (UI structure, receives children, hydrated)
988
- │ └── api/search/
989
- │ └── route.ts → GET /api/search (standalone API, no page.tsx needed)
990
- └── components/ ← 组件文件(会被热更自动感知)
991
- └── button.tsx
992
- ```
993
-
994
- ### Development mode
995
-
996
- tsx() runs in development mode automatically when `NODE_ENV !== 'production'`:
997
-
998
- - **File watching** — chokidar watches the `dir` directory for `.tsx`/`.ts` changes
999
- - Page files in `pages/` → single-file recompilation + registry update
1000
- - Component files in `components/` → full rebuild of all pages
1001
- - New files are detected automatically
1002
- - **Live reload** — Compiled via esbuild `write: false` + `vm.Script.runInContext` (no disk writes, no `node --watch` conflict)
1003
- - **WebSocket auto-refresh** — `/__weifuwu/livereload` endpoint pushes reload signals; browser refreshes automatically
1004
- - **`node --watch` compatible** — External files (`app.ts`, `middleware/`) handled by `--watch` restart; `ui/` changes handled by tsx() without conflict
1005
-
1006
- ```bash
1007
- node app.ts # development (auto-reload + live refresh)
1008
- NODE_ENV=production node app.ts # production
1009
- ```
1010
-
1011
- ### Tailwind CSS
1012
-
1013
- tsx() includes built-in Tailwind CSS v4 support. If an `app.css` file exists in the `dir` directory, it is compiled automatically through PostCSS + `@tailwindcss/postcss`. If no `app.css` is found, one is created automatically:
1014
-
1015
- ```css
1016
- @import "tailwindcss";
1017
- ```
1018
-
1019
- Write `className` directly in your components — no CLI, no configuration:
1020
-
1021
- ```tsx
1022
- export default function Home() {
1023
- return <h1 className="text-3xl font-bold text-blue-600">Hello</h1>
1024
- }
1025
- ```
1026
-
1027
- In development mode, Tailwind is reprocessed whenever a `.tsx` file changes (new class names are picked up automatically).
1028
-
1029
- ### `@` alias
1030
-
1031
- If your project has a `tsconfig.json` or `jsconfig.json` with `compilerOptions.paths`, tsx() reads it automatically and passes aliases to all esbuild builds (SSR compilation, hydration bundles, and hot reload):
1032
-
1033
- ```json
1034
- {
1035
- "compilerOptions": {
1036
- "paths": {
1037
- "@/*": ["./ui/*"]
1038
- }
1039
- }
1040
- }
1041
- ```
1042
-
1043
- This enables imports like `@/components/button` or `@/lib/utils` in both server-rendered and client-hydrated code.
1044
-
1045
- ### shadcn/ui
1046
-
1047
- tsx() works with [shadcn/ui](https://ui.shadcn.com) out of the box. The `@` alias and Tailwind CSS are handled automatically.
1048
-
1049
- ```bash
1050
- # 1. Install shadcn CLI and init (select "other" framework)
1051
- npx shadcn@latest init
1052
-
1053
- # 2. When prompted, configure:
1054
- # - Style: your preference
1055
- # - Base color: your preference
1056
- # - CSS file path: ui/app.css
1057
- # - Import alias: @/ → ./ui/
1058
- # - React hooks: yes
1059
- ```
1060
-
1061
- ```json
1062
- // tsconfig.json (generated by shadcn init)
1063
- {
1064
- "compilerOptions": {
1065
- "paths": {
1066
- "@/*": ["./ui/*"]
1067
- }
1068
- }
1069
- }
1070
- ```
1071
-
1072
- Add components:
1073
-
1074
- ```bash
1075
- npx shadcn@latest add button card dialog
1076
- ```
1077
-
1078
- Use them in your pages:
1079
-
1080
- ```tsx
1081
- // ui/pages/page.tsx
1082
- import { Button } from '@/components/ui/button'
1083
-
1084
- export default function Home() {
1085
- return <Button variant="outline">Click me</Button>
1086
- }
1087
79
  ```
1088
-
1089
- ```bash
1090
80
  node app.ts
1091
81
  ```
1092
82
 
1093
- ### Backward compatibility
1094
-
1095
- `tsx({ dir: './pages/' })` still works. When there is no `pages/` subdirectory under `dir`, the `dir` itself is used as the pages directory.
1096
-
1097
- ### page.tsx page component
1098
-
1099
- ```tsx
1100
- export default function Page({ params, query }: {
1101
- params: { slug: string }
1102
- query: Record<string, string>
1103
- }) {
1104
- return <article><h1>{params.slug}</h1></article>
1105
- }
1106
- ```
1107
-
1108
- ### load.ts data fetching (server-only)
1109
-
1110
- ```ts
1111
- export default async function load({ params, query }: {
1112
- params: Record<string, string>
1113
- query: Record<string, string>
1114
- }) {
1115
- const data = await db.query(params.slug)
1116
- return { data } // merged into props passed to page.tsx
1117
- }
1118
- ```
1119
-
1120
- ### layout.tsx
1121
-
1122
- **Root layout** (`pages/layout.tsx`) — receives `{ children, req, ctx }`:
1123
-
1124
- ```tsx
1125
- export default function RootLayout({ children, req, ctx }: {
1126
- children: React.ReactNode
1127
- req: Request
1128
- ctx: Context
1129
- }) {
1130
- return (
1131
- <html>
1132
- <head><title>App</title></head>
1133
- <body><div id="__weifuwu_root">{children}</div></body>
1134
- </html>
1135
- )
1136
- }
1137
- ```
1138
-
1139
- **Nested layouts** (`pages/blog/layout.tsx`) — receives only `{ children }`.
1140
-
1141
- ### route.ts — API (co-located with page)
1142
-
1143
- ```ts
1144
- export const POST: Handler = async (req, ctx) => {
1145
- const body = await req.json()
1146
- return Response.json({ ...body, slug: ctx.params.slug })
1147
- }
1148
- ```
1149
-
1150
- ### not-found.tsx — 404 page
1151
-
1152
- ```tsx
1153
- export default function NotFound() {
1154
- return <h1 class="text-4xl">404 – Not Found</h1>
1155
- }
1156
- ```
1157
-
1158
- ## Health check
1159
-
1160
- ```ts
1161
- import { serve, Router, health } from 'weifuwu'
1162
-
1163
- const app = new Router()
1164
- app.use(health()) // GET /health → 200
1165
- app.use(health({ path: '/healthz' })) // custom path
1166
- app.use(health({
1167
- check: async () => { await db.sql`SELECT 1` }, // fail → 503
1168
- }))
1169
- serve(app.handler(), { port: 3000 })
1170
- ```
1171
-
1172
- Returns a `Router` — mount with `app.use()`.
1173
-
1174
- ## Internationalization
1175
-
1176
- ```ts
1177
- import { i18n } from 'weifuwu'
1178
-
1179
- app.use(i18n({ dir: './locales', defaultLocale: 'en' }))
1180
-
1181
- // In any handler after i18n middleware:
1182
- app.get('/hello', (req, ctx) => {
1183
- const msg = ctx.t('greeting', { name: 'World' })
1184
- return Response.json({ message: msg, locale: ctx.locale })
1185
- })
1186
- ```
1187
-
1188
- Locale detection: `Cookie: locale=zh` → `Accept-Language: zh-CN` → `defaultLocale`.
1189
-
1190
- ## Email
1191
-
1192
- ```ts
1193
- import { mailer } from 'weifuwu'
1194
-
1195
- // SMTP transport
1196
- const mail = mailer({
1197
- transport: 'smtp://user:pass@smtp.example.com',
1198
- from: 'noreply@example.com',
1199
- })
1200
- await mail.send({ to: 'user@example.com', subject: 'Welcome', html: '<h1>Hi!</h1>' })
1201
- await mail.close()
1202
-
1203
- // Custom transport (Resend, SES, SendGrid, etc.)
1204
- const mail2 = mailer({
1205
- send: async (msg) => { await resend.emails.send(msg) },
1206
- })
1207
- await mail2.send({ to: 'user@example.com', subject: 'Hi', text: 'Hello' })
1208
- await mail2.close()
1209
- ```
1210
-
1211
- ## Test utilities
1212
-
1213
- ```ts
1214
- import { createTestServer } from 'weifuwu'
1215
-
1216
- const { server, url } = await createTestServer(app.handler())
1217
- const res = await fetch(`${url}/api/users`)
1218
- assert.equal(res.status, 200)
1219
- server.stop()
1220
- ```
1221
-
1222
- ## Usage within a full app
1223
-
1224
- ```ts
1225
- import { serve, Router, aiStream, graphql } from 'weifuwu'
1226
-
1227
- const app = new Router()
1228
- app.use('/', await tsx({ dir: './pages/' }))
1229
- const chat = await aiStream(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages }))
1230
- app.use('/chat', chat.router())
1231
- const gql = graphql(() => ({ schema: `type Query { hello: String }`, resolvers: { Query: { hello: () => 'world' } } }))
1232
- app.use('/graphql', gql.router())
1233
- app.ws('/chat', { message(ws, _, data) { ws.send(data) } })
1234
-
1235
- serve(app.handler(), { websocket: app.websocketHandler() })
1236
- ```
1237
-
1238
- ```bash
1239
- node app.ts # development (auto-reload + live refresh)
1240
- NODE_ENV=production node app.ts # production
1241
- ```
1242
-
1243
- No build step, no configuration file — just Node.js.
1244
-
1245
- ## Graceful shutdown
1246
-
1247
- ```ts
1248
- import { serve } from 'weifuwu'
1249
- import type { Server } from 'weifuwu'
1250
-
1251
- const ac = new AbortController()
1252
- let server: Server
1253
-
1254
- process.on('SIGTERM', () => {
1255
- ac.abort()
1256
- server.stop()
1257
- })
1258
-
1259
- server = serve((req, ctx) => new Response('Hello'), {
1260
- port: 3000,
1261
- signal: ac.signal,
1262
- })
1263
- await server.ready
1264
- ```
1265
-
1266
- ### Using with WebSocket
1267
-
1268
- ```ts
1269
- const app = new Router().ws('/chat', { … })
1270
- const server = serve(app.handler(), {
1271
- port: 3000,
1272
- signal: ac.signal,
1273
- websocket: app.websocketHandler(),
1274
- })
1275
- ```
1276
-
1277
- ## Error handling
1278
-
1279
- ```ts
1280
- const app = new Router()
1281
- .onError((err, req, ctx) =>
1282
- Response.json({ error: err.message }, { status: 500 }),
1283
- )
1284
- .get('/crash', () => { throw new Error('boom') })
1285
- ```
1286
-
1287
- ## Deploy
1288
-
1289
- See [deploy.md](./deploy.md) for complete documentation — VPS setup, subdomain routing, blue-green zero-downtime, WebSocket bridge, Git webhook, auto SSL, and management API.
1290
-
1291
- Quick start on a fresh VPS:
1292
-
1293
- ```bash
1294
- # 1. Install Node.js
1295
- curl -fsSL https://deb.nodesource.com/setup_24.x | bash -
1296
- apt-get install -y nodejs git
1297
-
1298
- # 2. Create deploy project
1299
- mkdir -p /opt/deploy && cd /opt/deploy
1300
- npm init -y && npm install weifuwu
1301
-
1302
- # 3. Write deploy.ts
1303
- cat > deploy.ts << 'EOF'
1304
- import { deploy, defineConfig } from 'weifuwu'
1305
- await deploy(defineConfig({
1306
- domain: 'example.com',
1307
- deployToken: process.env.DEPLOY_TOKEN,
1308
- apps: {
1309
- blog: {
1310
- repo: 'https://github.com/me/my-blog.git',
1311
- subdomain: 'blog',
1312
- entry: 'app.ts',
1313
- port: 3001,
1314
- },
1315
- },
1316
- }))
1317
- EOF
1318
-
1319
- # 4. Run
1320
- DEPLOY_TOKEN='my-secret' node deploy.ts
1321
- ```
1322
-
1323
- ## API
1324
-
1325
- ### `serve(handler, options?)`
1326
-
1327
- | Option | Default | Description |
1328
- |--------|---------|-------------|
1329
- | `port` | `0` | Listen port (`0` = random) |
1330
- | `hostname` | `'0.0.0.0'` | Bind address |
1331
- | `signal` | — | `AbortSignal` for graceful shutdown |
1332
- | `websocket` | — | Upgrade handler from `router.websocketHandler()` |
1333
-
1334
- Returns `{ stop, port, hostname, ready }`.
1335
-
1336
- ### `user(options)`
1337
-
1338
- | Option | Default | Description |
1339
- |--------|---------|-------------|
1340
- | `pg` | — | PostgreSQL client from `postgres()` |
1341
- | `jwtSecret` | — | Secret key for JWT signing |
1342
- | `table` | `'_users'` | Users table name |
1343
- | `expiresIn` | `'24h'` | JWT expiration |
1344
- | `oauth2.server` | `false` | Enable OAuth2 Server |
1345
-
1346
- Returns `UserModule` — `{ router, middleware, migrate, register, login, verify, registerClient, getClient, revokeClient, close }`.
1347
-
1348
- ### `tenant(options)`
1349
-
1350
- | Option | Default | Description |
1351
- |--------|---------|-------------|
1352
- | `pg` | — | PostgreSQL client from `postgres()` |
1353
- | `usersTable` | — | Users table name (matching the `table` option passed to `user()`) |
1354
-
1355
- Returns `TenantModule` — `{ migrate, middleware, router, graphql, close }`.
1356
-
1357
- ### `agent(options)`
1358
-
1359
- | Option | Default | Description |
1360
- |--------|---------|-------------|
1361
- | `pg` | — | PostgreSQL client from `postgres()` |
1362
- | `model` | env `OPENAI_MODEL` → Ollama | `LanguageModel` from ai SDK |
1363
- | `embeddingModel` | env `OPENAI_EMBEDDING_MODEL` → Ollama | `EmbeddingModel` for knowledge RAG |
1364
- | `embeddingDimension` | `1024` | Vector dimension for pgvector |
1365
- | `tools` | — | Tools for tool-use agents (ai SDK `Tool` objects) |
1366
-
1367
- Returns `AgentModule` — `{ migrate, router, run, addKnowledge, close }`.
1368
-
1369
- ### `opencode(options)`
1370
-
1371
- | Option | Default | Description |
1372
- |--------|---------|-------------|
1373
- | `pg` | — | PostgreSQL client from `postgres()` |
1374
- | `workspace` | `process.cwd()` | Base directory for `.sessions` |
1375
- | `model` | `'deepseek-v4-flash'` | LLM model name |
1376
- | `baseURL` | env `DEEPSEEK_BASE_URL` | API base URL |
1377
- | `apiKey` | env `DEEPSEEK_API_KEY` | API key |
1378
- | `systemPrompt` | — | Custom system prompt |
1379
- | `skills` | `[]` | Static skill definitions |
1380
- | `permissions` | — | Tool permission config |
1381
-
1382
- Returns `OpencodeModule` — `{ migrate, router, wsHandler, close }`.
1383
-
1384
- ### `messager(options)`
1385
-
1386
- | Option | Default | Description |
1387
- |--------|---------|-------------|
1388
- | `pg` | — | PostgreSQL client from `postgres()` |
1389
- | `agents` | — | `AgentModule` instance (enables agent message routing) |
1390
-
1391
- Returns `MessagerModule` — `{ migrate, router, wsHandler, send, close }`.
1392
-
1393
- ### `tsx(options)`
1394
-
1395
- | Option | Default | Description |
1396
- |--------|---------|-------------|
1397
- | `dir` | — | UI directory path (containing `pages/` and optionally `components/`) |
1398
-
1399
- Returns `Promise<Router>`.
1400
-
1401
- Auto-detected features (no configuration needed):
1402
-
1403
- | Feature | Behavior |
1404
- |---------|----------|
1405
- | **File watching** | Enabled in dev mode. Watches `dir` for changes, recompiles on the fly, sends reload via WebSocket |
1406
- | **WebSocket live reload** | Endpoint at `/__weifuwu/livereload`. Browser auto-refreshes on file changes or server restart |
1407
- | **Tailwind CSS** | Auto-detected when `app.css` exists. Compiled through PostCSS + `@tailwindcss/postcss`. Served at `/__wfw/style.css`, auto-injected into HTML `<head>` |
1408
- | **`@` alias** | Read from `tsconfig.json` / `jsconfig.json` `compilerOptions.paths`. Passed to all esbuild builds |
1409
- | **Process state** | Dev mode keeps the process alive on file changes. DB connections, WebSockets, in-memory caches persist |
1410
-
1411
- To use WebSocket features, pass `router.websocketHandler()` to `serve()`:
1412
-
1413
- ```ts
1414
- serve(app.handler(), { websocket: app.websocketHandler() })
1415
- ```
1416
-
1417
- ### `Router`
1418
-
1419
- | Method | Description |
1420
- |--------|-------------|
1421
- | `get/post/put/delete/patch/head/options/all(path, ...mws, handler)` | Route registration |
1422
- | `use(mw)` / `use(path, mw)` / `use(path, subRouter)` | Middleware / sub-router |
1423
- | `ws(path, ...mws, handler)` | WebSocket route |
1424
- | `onError(handler)` | Global error handler |
1425
- | `handler()` | Returns `(req, ctx) => Response` for `serve()` |
1426
- | `websocketHandler()` | Returns upgrade handler for `serve({ websocket })` |
1427
-
1428
- ### Middleware modules
1429
-
1430
- | Import | Description |
1431
- |--------|-------------|
83
+ ## Documentation
84
+
85
+ | Module | Doc | Description |
86
+ |--------|-----|-------------|
87
+ | **Router** | [docs/router.md](./docs/router.md) | Routes, middleware, WebSocket, error handling |
88
+ | **Middleware** | [docs/middleware.md](./docs/middleware.md) | auth, cors, logger, rateLimit, compress, validate, upload, cookie, static |
89
+ | **PostgreSQL** | [docs/postgres.md](./docs/postgres.md) | Schema builder, CRUD, DDL, transactions, PgModule |
90
+ | **Auth & User** | [docs/user.md](./docs/user.md) | Password, JWT, OAuth2 Server, Social Login cookbook |
91
+ | **React SSR** | [docs/tsx.md](./docs/tsx.md) | pages, layouts, loaders, Tailwind, shadcn/ui |
92
+ | **AI** | [docs/ai.md](./docs/ai.md) | `aiStream()`, `runWorkflow()` |
93
+ | **AI Agent** | [docs/agent.md](./docs/agent.md) | Chat, tool-use, RAG knowledge |
94
+ | **Opencode** | [docs/opencode.md](./docs/opencode.md) | Programming assistant, skills, sessions, permissions |
95
+ | **Messager** | [docs/messager.md](./docs/messager.md) | Real-time chat, channels, WebSocket, agent routing |
96
+ | **GraphQL** | [docs/graphql.md](./docs/graphql.md) | GraphQL endpoint with GraphiQL |
97
+ | **Tenant BaaS** | [docs/tenant.md](./docs/tenant.md) | Dynamic tables, auto REST + GraphQL, row isolation |
98
+ | **Extra** | [docs/extra.md](./docs/extra.md) | Health check, i18n, email, test utilities |
99
+
100
+ ### Infrastructure
101
+
102
+ | Module | Import | What it gives you |
103
+ |--------|--------|-------------------|
104
+ | PostgreSQL | `postgres(options?)` | Connection pool + schema builder + CRUD + transactions |
105
+ | Redis | `redis(options?)` | ioredis client injected as `ctx.redis` |
106
+ | Queue | `queue(options?)` | Redis-backed job queue with cron scheduling |
107
+ | Deploy | `deploy(config)` | Self-hosted PaaS, see [deploy.md](./deploy.md) |
108
+
109
+ ### Mountable modules
110
+
111
+ All use the same pattern — `const m = module(options)` → `app.use('/path', m.router())`:
112
+
113
+ | Module | Purpose | Also provides |
114
+ |--------|---------|---------------|
115
+ | `user(options)` | Auth (password + JWT + OAuth2) | `migrate()`, `middleware()`, `register()`, `login()`, `verify()`, `close()` |
116
+ | `tenant(options)` | Multi-tenant BaaS | `migrate()`, `middleware()`, `graphql()`, `close()` |
117
+ | `agent(options)` | AI agents | `migrate()`, `run()`, `addKnowledge()`, `close()` |
118
+ | `opencode(options)` | Programming assistant | `migrate()`, `wsHandler()`, `close()` |
119
+ | `messager(options)` | Real-time messaging | `migrate()`, `wsHandler()`, `send()`, `close()` |
120
+ | `aiStream(handler)` | AI streaming endpoint | — |
121
+ | `graphql(handler)` | GraphQL endpoint | — |
122
+ | `health(options?)` | Health check | — |
123
+
124
+ ### Middleware (all `(req, ctx, next) => Response`)
125
+
126
+ | Middleware | Description |
127
+ |-----------|-------------|
1432
128
  | `auth(options)` | Bearer token / custom header / verify / proxy |
1433
129
  | `cors(options?)` | CORS with preflight, origin whitelist, credentials |
1434
130
  | `logger(options?)` | Request logging with duration |
1435
131
  | `rateLimit(options?)` | In-memory rate limiting with headers |
1436
132
  | `compress(options?)` | Brotli / Gzip / Deflate compression |
1437
- | `validate(schemas)` | Zod validation middleware |
1438
- | `upload(options?)` | Multipart file upload middleware |
1439
-
1440
- ### Sub-Router modules (mount via `app.use()`)
1441
-
1442
- | Import | Description |
1443
- |--------|-------------|
1444
- | `postgres(options?)` | PostgreSQL connection + DDL schema builder + transactions + module lifecycle |
1445
- | `redis(options?)` | Redis client (ioredis) — injects `ctx.redis` |
1446
- | `queue(options?)` | Redis-backed job queue — immediate, delayed, cron scheduling |
1447
- | `user(options)` | Built-in authentication (password + OAuth2 Server + JWT, middleware) |
1448
- | `tenant(options)` | Multi-tenant BaaS — dynamic tables, REST + GraphQL auto-generation, row-level isolation |
1449
- | `agent(options)` | AI Agent — chat/tool-use/knowledge, Ollama-ready, programmatic API |
1450
- | `messager(options)` | Real-time messaging — channels, WebSocket, agent routing, webhooks |
1451
- | `opencode(options)` | AI programming assistant — chat agents with tools, skills, permissions, isolated workspaces |
1452
- | `graphql(handler)` | GraphQL endpoint (GET/POST + GraphiQL) |
1453
- | `aiStream(handler)` | AI streaming endpoint (POST) |
1454
- | `runWorkflow(options)` | DAG execution engine as an AI SDK `Tool` — use with `streamText()` |
133
+ | `validate(schemas)` | Zod validation (body, query, params) |
134
+ | `upload(options?)` | Multipart file upload |
135
+ | `i18n(options)` | Internationalization — `ctx.t()`, locale detection |
1455
136
 
1456
- ### Deploy
1457
-
1458
- | Import | Description |
1459
- |--------|-------------|
1460
- | `deploy(config)` | Start the deployment platform — see [deploy.md](./deploy.md) |
1461
- | `defineConfig(config)` | Type-safe config helper with validation — see [deploy.md](./deploy.md) |
1462
-
1463
- ### Utilities
137
+ ### Utility functions
1464
138
 
1465
139
  | Function | Description |
1466
140
  |----------|-------------|
1467
- | `serveStatic(root, options?)` | Static file serving handler |
1468
- | `getCookies(req)` | Parse Cookie header object |
1469
- | `setCookie(res, name, value, options?)` | Set cookie (returns new Response) |
1470
- | `deleteCookie(res, name)` | Delete cookie (returns new Response) |
1471
- | `useTsx()` | Hook returning `{ params, query, user, parsed }` from `TsxContext` |
1472
- | `runWorkflow(options)` | Create a DAG execution AI SDK `Tool` — `{ tools?, model?, maxSteps? }` |
1473
- | `pgTable(name, columns)` | Type-safe table schema definition with DDL + CRUD |
1474
- | `pg.table(name, columns)` | Pre-bound table (no `sql` parameter needed for CRUD) |
1475
- | `serial()`, `uuid()`, `text()`, `integer()`, `boolean()`, `timestamptz()`, `jsonb()`, `textArray()`, `vector()` | Column type builders |
1476
- | `sql(strings, ...)` | SQL expression literal for defaults and SET values (e.g. `sql\`NOW()\``) |
1477
- | `PgModule` | Base class for database-backed modules (provides `sql`, `close()`) |
1478
- | `BoundTable` | Table with pre-bound `sql` — returned by `pg.table()` |
1479
- | `FindOptions` | Query options: `{ orderBy?, limit?, offset? }` for `find()` |
1480
-
1481
- Import `useTsx` and `TsxContext` from `'weifuwu'`.
141
+ | `serveStatic(root, options?)` | Static file serving |
142
+ | `getCookies(req)` / `setCookie(res, ...)` / `deleteCookie(res, ...)` | Cookie helpers |
143
+ | `mailer(options)` | Email sender (SMTP or custom) |
144
+ | `createTestServer(handler)` | Start test server `{ server, url }` |
145
+ | `runWorkflow(options)` | DAG execution engine as AI SDK `Tool` |
146
+ | `pgTable(name, columns)` | Type-safe table schema builder |
147
+ | `pg.table(name, columns)` | Pre-bound table (no `sql` param needed) |
148
+ | `serial()`, `uuid()`, `text()`, ... | Column type builders |
149
+ | `PgModule` | Base class for DB-backed modules |
1482
150
 
1483
151
  ## License
1484
152