weifuwu 0.17.9 → 0.17.10

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
@@ -5,1304 +5,544 @@ description: Web-standard HTTP framework for Node.js — (req, ctx) => Response
5
5
 
6
6
  # weifuwu
7
7
 
8
- **Web-standard HTTP framework for Node.js.** `(req, ctx) => Response` — no framework-specific objects, just the Web API your browser already speaks.
9
-
10
- - [Quick start](#quick-start)
11
- - [serve() — HTTP server](#serve--http-server)
12
- - [Router](#router)
13
- - [Middleware](#middleware)
14
- - [React SSR (tsx)](#react-ssr-tsx)
15
- - [PostgreSQL](#postgresql)
16
- - [Auth](#auth)
17
- - [WebSocket & Real-time](#websocket--real-time)
18
- - [AI](#ai)
19
- - [Data Layer](#data-layer)
20
- - [iii — Worker / Function / Trigger](#iii--worker--function--trigger)
21
- - [Multi-tenant BaaS](#multi-tenant-baas)
22
- - [Messager](#messager)
23
- - [LogDB](#logdb)
24
- - [SEO](#seo)
25
- - [Opencode](#opencode)
26
- - [Deploy](#deploy)
27
- - [Health check](#health-check)
28
- - [Preferences](#preferences)
29
- - [Email](#email)
30
- - [Server-Sent Events](#server-sent-events)
31
- - [Utility functions](#utility-functions)
32
- - [Testing](#testing)
33
- - [License](#license)
8
+ **Web-standard HTTP framework for Node.js.** `(req, ctx) => Response` — no framework-specific objects.
34
9
 
35
- ---
36
-
37
- ## Quick start
38
-
39
- ### Hello World
10
+ ## Quick Start
40
11
 
41
12
  ```ts
42
13
  import { serve } from 'weifuwu'
43
14
  serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
44
15
  ```
45
16
 
46
- ### Full-stack SSR in one file
47
-
48
17
  ```ts
49
18
  import { serve, Router, tsx, preferences } from 'weifuwu'
50
-
51
19
  const app = new Router()
52
- app.use(preferences({
53
- dir: './locales', // i18n (0 extra deps)
54
- theme: {}, // dark mode (0 extra deps)
55
- }))
20
+ app.use(preferences({ dir: './locales' }))
56
21
  app.use('/', await tsx({ dir: './ui' }))
57
-
58
22
  serve(app.handler(), { port: 3000 })
59
23
  ```
60
24
 
61
- Your tsx pages can use it directly, and locale/theme switching routes (`/__lang/zh`, `/__theme/dark`) work automatically without extra routes:
62
-
63
- ```tsx
64
- import { Head, useCtx, useData, createStore } from 'weifuwu/react'
65
-
66
- export default function Page() {
67
- const { t, theme } = useCtx() // i18n + theme
68
- const { data } = useData('/api/list') // data fetching
69
- return <h1>{t('hello')} / {theme}</h1>
70
- }
71
- ```
72
-
73
- **Zero extra dependencies** — no zustand, react-query, next-intl, next-themes, react-hot-toast needed.
74
-
75
- ### weifuwu init
76
-
77
25
  ```bash
78
- npx weifuwu init my-app
79
- cd my-app && npm install && npm run dev
26
+ npx weifuwu init my-app && cd my-app && npm run dev
80
27
  ```
81
28
 
82
29
  ---
83
30
 
84
- ## serve() — HTTP server
31
+ ## Core Concepts
85
32
 
86
- ```ts
87
- import { serve } from 'weifuwu'
88
- import type { Server } from 'weifuwu'
33
+ ### serve()
89
34
 
35
+ ```ts
90
36
  const server = serve(handler, { port: 3000 })
91
37
  await server.ready
92
- console.log(`Listening on http://localhost:${server.port}`)
93
38
  ```
94
39
 
95
- ### Options
96
-
97
40
  | Option | Type | Default | Description |
98
41
  |--------|------|---------|-------------|
99
- | `port` | `number` | `0` (random) | Listen port |
42
+ | `port` | `number` | `0` | Listen port |
100
43
  | `hostname` | `string` | `'0.0.0.0'` | Listen address |
101
44
  | `signal` | `AbortSignal` | — | Shutdown on abort |
102
45
  | `websocket` | `WsUpgradeHandler` | — | WebSocket upgrade handler |
103
- | `maxBodySize` | `number` | — | Max request body bytes |
104
- | `shutdown` | `boolean` | `true` | Auto-register SIGTERM/SIGINT |
105
-
106
- Graceful shutdown is **enabled by default** — `serve()` registers `SIGTERM` and `SIGINT` handlers that call `server.close()`. Set `shutdown: false` to disable.
107
-
108
- ### Server
109
-
110
- ```ts
111
- interface Server {
112
- stop: () => void
113
- readonly port: number
114
- readonly hostname: string
115
- ready: Promise<void>
116
- }
117
- ```
118
-
119
- ### createTestServer
46
+ | `maxBodySize` | `number` | — | Max body bytes |
47
+ | `shutdown` | `boolean` | `true` | Auto SIGTERM/SIGINT |
120
48
 
121
49
  ```ts
50
+ interface Server { stop: () => void; readonly port: number; readonly hostname: string; ready: Promise<void> }
122
51
  const { server, url } = await createTestServer(handler)
123
- // url = 'http://localhost:PORT'
124
52
  ```
125
53
 
126
- ---
127
-
128
- ## Router
54
+ ### Router
129
55
 
130
56
  ```ts
131
- import { Router } from 'weifuwu'
132
-
133
57
  const app = new Router()
134
- .get('/hello/:name', (req, ctx) =>
135
- Response.json({ message: `Hello, ${ctx.params.name}!` }),
136
- )
137
- .post('/data', async (req, ctx) => {
138
- const body = await req.json()
139
- return Response.json(body, { status: 201 })
140
- })
141
- ```
58
+ app.get('/hello/:name', (req, ctx) => Response.json({ message: `Hello, ${ctx.params.name}!` }))
59
+ app.post('/data', async (req, ctx) => { const body = await req.json(); return Response.json(body, { status: 201 }) })
60
+ app.use('/admin', authMW) // path-scoped middleware
61
+ app.use('/admin', adminRouter) // sub-router (flattened into parent trie)
62
+ app.ws('/echo', { open(ws) { ws.send('connected') }, message(ws, _ctx, data) { ws.send(`echo: ${data}`) } })
63
+ app.onError((err, req, ctx) => Response.json({ error: err.message }, { status: 500 }))
142
64
 
143
- ### Route patterns
65
+ const handler = app.handler()
66
+ const wsHandler = app.websocketHandler()
67
+ serve(handler, { port: 3000, websocket: wsHandler })
68
+ ```
144
69
 
145
70
  | Pattern | Example | Match |
146
71
  |---------|---------|-------|
147
- | Static | `/about` | Exact path |
148
- | Param | `/users/:id` | `/users/42` → `ctx.params.id = '42'` |
72
+ | Static | `/about` | exact |
73
+ | Param | `/users/:id` | `/users/42` → `ctx.params.id` |
149
74
  | Wildcard | `/static/*` | `/static/js/app.js` |
150
75
 
151
- Query params are auto-parsed into `ctx.query`.
76
+ Query params `ctx.query`.
152
77
 
153
- ### Sub-router mounting
78
+ ### Middleware
154
79
 
155
80
  ```ts
156
- const admin = new Router()
157
- admin.use(auth({ token: 'secret' }))
158
- admin.get('/dashboard', handler)
159
-
160
- app.use('/admin', admin)
161
- // Mounts admin routes at /admin/dashboard, etc.
81
+ type Middleware = (req: Request, ctx: Context, next: Handler) => Response | Promise<Response>
82
+ app.use(mw) // global
83
+ app.use('/admin', mw) // path-scoped
84
+ app.get('/admin', mw, handler) // route-level
162
85
  ```
163
86
 
164
- ### WebSocket
87
+ ---
165
88
 
166
- ```ts
167
- app.ws('/echo', {
168
- open(ws, ctx) { ws.send('connected') },
169
- message(ws, ctx, data) { ws.send(`echo: ${data}`) },
170
- close(ws, ctx) { /* cleanup */ },
171
- error(ws, ctx, err) { /* log */ },
172
- })
89
+ ## Module Patterns
173
90
 
174
- serve(app.handler(), {
175
- port: 3000,
176
- websocket: app.websocketHandler(),
177
- })
178
- ```
91
+ All modules follow one of 5 patterns. The pattern letter is marked in each module's heading.
92
+
93
+ | Pattern | How to mount | Example |
94
+ |---------|-------------|---------|
95
+ | `[A]` | `app.use(mod())` | `compress()`, `preferences()` |
96
+ | `[B]` | `app.use(mod())` + call `.stop()` / `.close()` etc. | `rateLimit({...})` |
97
+ | `[C]` | `app.use(mod.middleware())` + `app.use('/', mod.router())` | `analytics()`, `user()` |
98
+ | `[D]` | `app.use(mod().handler())` | `health()`, `seo()` |
99
+ | `[E]` | `app.use('/', g.router().handler())` | `graphql(handler)` |
100
+
101
+ ---
179
102
 
180
- ### Error handling
103
+ ## Module Reference
104
+
105
+ ### agent [C]
181
106
 
182
107
  ```ts
183
- app.onError((err, req, ctx) =>
184
- Response.json({ error: err.message }, { status: 500 }),
185
- )
108
+ const a = agent({ pg })
109
+ await a.migrate()
110
+ app.use('/api', a.router())
111
+ await a.addKnowledge(agentId, 'Title', 'docs')
112
+ a.run(agentId, { task: 'summarize' })
186
113
  ```
187
114
 
188
- ---
115
+ ### aiStream [E]
116
+
117
+ ```ts
118
+ const chat = await aiStream(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages }))
119
+ app.use('/chat', chat.router().handler())
120
+ ```
189
121
 
190
- ## Middleware
122
+ ### analytics [C]
191
123
 
192
- All middleware follows `(req, ctx, next) => Response | Promise<Response>`.
124
+ In-memory or PostgreSQL page view tracking with built-in dashboard.
193
125
 
194
126
  ```ts
195
- app.use(middleware) // global
196
- app.use('/admin', middleware) // path-scoped
197
- app.get('/admin', middleware, handler) // route-level
127
+ const a = analytics()
128
+ app.use(a.middleware())
129
+ app.use('/', a.router()) // GET /__analytics (dashboard), GET /__analytics/data?days=7 (JSON)
198
130
  ```
199
131
 
200
- | Middleware | Description |
201
- |-----------|-------------|
202
- | `auth(options)` | Bearer token / custom header / verify / proxy |
203
- | `cors(options?)` | CORS with preflight, origin whitelist, credentials |
204
- | `csrf(options?)` | Double-submit cookie CSRF protection |
205
- | `logger(options?)` | Request logging with duration |
206
- | `rateLimit(options?)` | In-memory rate limiting with headers |
207
- | `compress(options?)` | Brotli / Gzip / Deflate compression |
208
- | `validate(schemas)` | Zod validation (body, query, params) |
209
- | `upload(options?)` | Multipart file upload |
210
- | `preferences(options?)` | Locale + theme detection, `ctx.t()`, `ctx.prefs`, `ctx.setPref()` |
211
- | `seoMiddleware(options?)` | `X-Robots-Tag` header — string or path-based function |
212
- | `helmet(options?)` | Security headers — CSP, HSTS, X-Frame-Options, etc. |
213
- | `requestId(options?)` | `X-Request-ID` header + `ctx.requestId` |
214
- | `health(options?)` | `GET /health` endpoint with custom checks |
215
-
216
- ### auth
132
+ | Option | Type | Default | Description |
133
+ |--------|------|---------|-------------|
134
+ | `pg` | `object` | | PostgreSQL client for persistence |
135
+ | `excluded` | `string[]` | `['/__analytics', '/__wfw', '/static']` | Paths to skip |
217
136
 
218
137
  ```ts
219
- import { auth } from 'weifuwu'
138
+ // With PostgreSQL
139
+ const a = analytics({ pg })
140
+ await a.migrate()
141
+ app.use(a.middleware())
142
+ app.use('/', a.router())
143
+ ```
144
+
145
+ ### auth [A]
220
146
 
147
+ ```ts
221
148
  app.use(auth({ token: 'sk-123' })) // static token
222
149
  app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
223
150
  app.use(auth({ verify: async (token) => ({ sub: 'abc' }) })) // custom verify
224
151
  app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
225
152
  ```
226
153
 
227
- ### cors
154
+ ### compress [A]
228
155
 
229
156
  ```ts
230
- import { cors } from 'weifuwu'
157
+ app.use(compress()) // brotli > gzip > deflate
158
+ app.use(compress({ threshold: 2048 })) // only > 2KB
159
+ ```
231
160
 
232
- app.use(cors()) // allow all
233
- app.use(cors({ origin: ['https://example.com'] })) // whitelist
161
+ ### cors [A]
162
+
163
+ ```ts
164
+ app.use(cors()) // allow all
165
+ app.use(cors({ origin: ['https://example.com'] })) // whitelist
234
166
  app.use(cors({ origin: (o) => o.endsWith('.trusted.com') && o }))
235
167
  app.use(cors({ credentials: true, maxAge: 3600 }))
236
168
  ```
237
169
 
238
- ### csrf
239
-
240
- Double-submit cookie pattern. Sets `_csrf` cookie on GET, validates `X-CSRF-Token` header (or `_csrf` body field) on POST/PUT/DELETE/PATCH.
170
+ ### csrf [A]
241
171
 
242
172
  ```ts
243
- import { csrf } from 'weifuwu'
244
-
245
173
  app.use(csrf())
246
-
247
174
  // ctx.csrfToken available in handlers
248
- app.get('/form', (req, ctx) => {
249
- return new Response(`<input name="_csrf" value="${ctx.csrfToken}" hidden />`, {
250
- headers: { 'content-type': 'text/html' },
251
- })
252
- })
175
+ // Auto-validates X-CSRF-Token header on POST/PUT/DELETE/PATCH
253
176
  ```
254
177
 
255
178
  | Option | Default | Description |
256
179
  |--------|---------|-------------|
257
180
  | `cookie` | `'_csrf'` | Cookie name |
258
181
  | `header` | `'x-csrf-token'` | Header name |
259
- | `key` | `'_csrf'` | Body field name (fallback) |
260
- | `excludeMethods` | `['GET', 'HEAD', 'OPTIONS']` | Skip validation |
182
+ | `key` | `'_csrf'` | Body field fallback |
183
+ | `excludeMethods` | `['GET','HEAD','OPTIONS']` | Skip validation |
261
184
 
262
- For fetch-based forms, `useAction()` reads the `_csrf` cookie automatically.
263
-
264
- ### logger
265
-
266
- ```ts
267
- import { logger } from 'weifuwu'
268
-
269
- app.use(logger()) // GET /hello 200 5ms
270
- app.use(logger({ format: 'combined' })) // with query params
271
- ```
272
-
273
- ### rateLimit
185
+ ### deploy
274
186
 
275
187
  ```ts
276
- import { rateLimit } from 'weifuwu'
277
-
278
- app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min
279
- app.get('/api', rateLimit({ max: 10 }), handler) // per-route
280
- app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
188
+ import { deploy, defineConfig } from 'weifuwu'
189
+ const config = defineConfig({ apps: [{ name: 'api', dir: './api', domain: 'api.example.com', port: 3001 }] })
190
+ await deploy(config)
281
191
  ```
282
192
 
283
- ### compress
193
+ ### health [D]
284
194
 
285
195
  ```ts
286
- import { compress } from 'weifuwu'
287
-
288
- app.use(compress()) // brotli > gzip > deflate
289
- app.use(compress({ threshold: 2048 })) // only > 2KB
196
+ app.use(health()) // GET /health 200
197
+ app.use(health({ checks: { db: async () => { await pg.sql`SELECT 1`; return { ok: true } } } }))
290
198
  ```
291
199
 
292
- ### validate
293
-
294
- ```ts
295
- import { z } from 'zod'
296
- import { validate } from 'weifuwu'
297
-
298
- const CreateUser = z.object({ name: z.string().min(1), email: z.string().email() })
299
- router.post('/users', validate({ body: CreateUser }), (req, ctx) => {
300
- // ctx.parsed.body — typed & validated
301
- })
302
- ```
200
+ ### helmet [A]
303
201
 
304
- ### upload
202
+ 13 security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
305
203
 
306
204
  ```ts
307
- import { upload } from 'weifuwu'
308
-
309
- router.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760 }), (req, ctx) => {
310
- // ctx.parsed.files.avatar → { name, type, size, path }
311
- // ctx.parsed.fields.title → 'hello'
312
- })
205
+ app.use(helmet())
206
+ app.use(helmet({ contentSecurityPolicy: "default-src 'self'", xFrameOptions: 'DENY' }))
313
207
  ```
314
208
 
315
- ### cookie
209
+ ### iii [C] — Worker / Function / Trigger
316
210
 
317
211
  ```ts
318
- import { getCookies, setCookie, deleteCookie } from 'weifuwu'
212
+ const engine = iii({ pg, redis })
213
+ app.use('/iii', engine.router())
319
214
 
320
- const cookies = getCookies(req) // { session: 'abc' }
321
- let res = new Response('ok')
322
- res = setCookie(res, 'session', 'token', { httpOnly: true, secure: true, maxAge: 3600 })
323
- res = deleteCookie(res, 'session')
215
+ const w = createWorker('orders')
216
+ w.registerFunction('orders::create', async (payload) => db.query('INSERT INTO orders ...', [payload.items]))
217
+ engine.addWorker(w)
218
+ await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })
324
219
  ```
325
220
 
326
- | Option | Type | Description |
327
- |--------|------|-------------|
328
- | `domain` | `string` | Cookie domain |
329
- | `path` | `string` | Cookie path |
330
- | `maxAge` | `number` | Seconds |
331
- | `expires` | `Date` | Expiration |
332
- | `httpOnly` | `boolean` | Not accessible to JS |
333
- | `secure` | `boolean` | HTTPS only |
334
- | `sameSite` | `'strict' \| 'lax' \| 'none'` | SameSite policy |
335
-
336
- ### serveStatic
221
+ | Method | Description |
222
+ |--------|-------------|
223
+ | `.addWorker(w)` | Register a worker |
224
+ | `.trigger({ function_id, payload, action? })` | Invoke a function (sync or void) |
225
+ | `.router()` | REST + WS API |
337
226
 
338
- ```ts
339
- import { serveStatic } from 'weifuwu'
340
-
341
- router.get('/static/*', serveStatic('./public'))
342
- ```
343
-
344
- 20+ MIME types, ETag + 304, directory index, path traversal protection, Cache-Control.
227
+ ### logdb [C]
345
228
 
346
- ### helmet
229
+ PostgreSQL structured event logging with monthly partitioning.
347
230
 
348
231
  ```ts
349
- import { helmet } from 'weifuwu'
350
-
351
- app.use(helmet()) // CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
352
-
353
- app.use(helmet({
354
- contentSecurityPolicy: "default-src 'self'",
355
- xFrameOptions: 'DENY',
356
- strictTransportSecurity: 'max-age=63072000; includeSubDomains; preload',
357
- }))
232
+ const logger = logdb({ pg })
233
+ await logger.migrate()
234
+ app.use('/logs', logger.router())
235
+ await logger.clean(12) // drop partitions older than 12 months
236
+ await logger.log({ level: 'info', source: 'app', message: 'hello' })
358
237
  ```
359
238
 
360
- 13 security headers by default: `X-Content-Type-Options`, `X-Frame-Options`, `X-XSS-Protection`, `Strict-Transport-Security`, `Content-Security-Policy`, `Referrer-Policy`, `Permissions-Policy`, `Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy`, `Cross-Origin-Embedder-Policy`, `X-DNS-Prefetch-Control`, `X-Download-Options`, `X-Permitted-Cross-Domain-Policies`.
239
+ | Method | Path | Description |
240
+ |--------|------|-------------|
241
+ | POST | `/` | Create log entry |
242
+ | GET | `/` | Query (`?level=`, `?source=`, `?after=`, `?before=`, `?meta.*=`) |
243
+ | GET | `/:id` | Get single entry |
361
244
 
362
- ### requestId
245
+ ### logger [A]
363
246
 
364
247
  ```ts
365
- import { requestId } from 'weifuwu'
366
-
367
- app.use(requestId())
368
- // Sets X-Request-ID header on responses, available as ctx.requestId
248
+ app.use(logger()) // GET /hello 200 5ms
249
+ app.use(logger({ format: 'combined' })) // with query params
369
250
  ```
370
251
 
371
- ---
372
-
373
- ## React SSR (tsx)
252
+ ### mailer
374
253
 
375
254
  ```ts
376
- import { serve, Router, tsx } from 'weifuwu'
377
-
378
- const app = new Router()
379
- app.use('/', await tsx({ dir: './ui/' }))
380
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
381
- ```
382
-
383
- ### Directory structure
384
-
385
- ```
386
- ui/
387
- ├── pages/
388
- │ ├── page.tsx → GET / (React component)
389
- │ ├── layout.tsx → root layout (HTML shell, receives req/ctx)
390
- │ ├── not-found.tsx → 404 page
391
- │ ├── about/page.tsx → GET /about
392
- │ ├── blog/[slug]/
393
- │ │ ├── page.tsx → GET /blog/:slug
394
- │ │ ├── load.ts → data fetching (server-only)
395
- │ │ └── route.ts → POST /blog/:slug (API, named exports)
396
- │ ├── blog/layout.tsx → /blog/* layout (UI structure, hydrated)
397
- │ └── api/search/
398
- │ └── route.ts → GET /api/search
399
- └── components/
400
- └── button.tsx
401
- ```
402
-
403
- ### page.tsx — page component
404
-
405
- Components receive `{ params, query }` from routing and can use hooks for
406
- context, data fetching, state, URL sync, and meta tags (see [exports table](#react-exports-weifuwureact)):
407
-
408
- ```tsx
409
- import { Head, useCtx, useData, createStore, useQueryState } from 'weifuwu/react'
410
-
411
- const useFilters = createStore({ category: '' })
412
-
413
- export default function Page({ params, query }: { params: { slug: string }; query: Record<string, string> }) {
414
- const { t, locale, theme } = useCtx() // i18n + theme + prefs
415
- const [page, setPage] = useQueryState('page', '1') // URL sync
416
- const { data, loading, mutate } = useData(`/api/posts?page=${page}`) // data fetching
417
-
418
- return (
419
- <>
420
- <Head><title>{t('page.title')}</title></Head>
421
- {loading ? <Skeleton /> : data.posts.map(p => <Card key={p.id} />)}
422
- </>
423
- )
424
- }
425
- ```
426
-
427
- ### load.ts — data fetching (server-only)
428
-
429
- ```ts
430
- export default async function load({ params, query }: {
431
- params: Record<string, string>
432
- query: Record<string, string>
433
- }) {
434
- const data = await db.query(params.slug)
435
- return { data } // merged into page component props
436
- }
437
- ```
438
-
439
- ### layout.tsx
440
-
441
- **Root layout** (`pages/layout.tsx`) — receives `{ children, req, ctx }`:
442
-
443
- > The `<div id="__weifuwu_root">` hydration target is **auto-injected** by the framework — do not add it manually. Just render `{children}` where you want page content.
444
-
445
- ```tsx
446
- export default function RootLayout({ children, req, ctx }: {
447
- children: React.ReactNode
448
- req: Request
449
- ctx: Context
450
- }) {
451
- return (
452
- <html>
453
- <head><title>App</title></head>
454
- <body><main>{children}</main></body>
455
- </html>
456
- )
457
- }
255
+ const mail = mailer({ host: 'smtp.example.com', port: 587, auth: { user, pass } })
256
+ await mail.send({ to: 'user@test.com', subject: 'Hello', text: 'Body', html: '<p>Body</p>' })
458
257
  ```
459
258
 
460
- **Nested layouts** (`pages/blog/layout.tsx`) — receives only `{ children }`.
461
-
462
- Page components access preferences, i18n, and theme via `useCtx()` — see [Preferences](#preferences).
259
+ ### messager [C]
463
260
 
464
- ### route.ts API (co-located with page)
261
+ Real-time chat with channels, WebSocket, agent routing.
465
262
 
466
263
  ```ts
467
- export const POST: Handler = async (req, ctx) => {
468
- const body = await req.json()
469
- return Response.json({ ...body, slug: ctx.params.slug })
470
- }
471
- ```
472
-
473
- ### Public environment variables
474
-
475
- Prefix with `WEIFUWU_PUBLIC_` for automatic inlining into the client hydration bundle:
476
-
477
- ```bash
478
- WEIFUWU_PUBLIC_API_URL=https://api.example.com
479
- ```
480
-
481
- ```tsx
482
- // page.tsx — works on both server and client
483
- const apiUrl = process.env.WEIFUWU_PUBLIC_API_URL
484
- ```
485
-
486
- The hydration bundle also injects `self.process = { env: {} }` as a safety net so any `process.env.*` reference in bundled dependencies won't throw.
487
-
488
- ### Streaming SSR
489
-
490
- HTML is streamed via `TransformStream` — the browser starts rendering before the full page is ready. `<head>` content (theme blocking script, locale data, CSS) is injected at the `</head>` boundary and sent immediately.
491
-
492
- ### Persistent layout
493
-
494
- The client hydration bundle creates a persistent `App` root that wraps all pages. Client-side navigation via `<Link>` (or `navigate()`) replaces the page component in-place instead of unmounting and re-hydrating. This means:
495
- - `TsxContext.Provider` stays alive across navigations
496
- - `createStore()` state persists (no cache-busting on bundle imports)
497
- - Faster navigation — React only re-renders the page component
498
-
499
- ### Client-side hooks
500
-
501
- #### useWebsocket — auto-reconnecting WebSocket
502
-
503
- ```tsx
504
- import { useWebsocket } from 'weifuwu/react'
505
-
506
- function Chat() {
507
- const { send, lastMessage, readyState, close, reconnect } = useWebsocket('/ws/chat', {
508
- onMessage: (data) => console.log('received', data),
509
- reconnect: { maxRetries: 10, delay: 3000 },
510
- })
511
-
512
- return <div>
513
- <p>Status: {readyState === 1 ? 'Connected' : readyState === 0 ? 'Connecting...' : 'Disconnected'}</p>
514
- <button onClick={() => send('Hello')}>Send</button>
515
- {lastMessage && <p>Last: {lastMessage}</p>}
516
- </div>
517
- }
518
- ```
519
-
520
- `url` accepts `string`, `URL`, or `() => string | URL | null` (function form avoids reconnecting on every render). `close()` disables auto-reconnect; `reconnect()` resets retry count.
521
-
522
- #### useAction — async form submission
523
-
524
- ```tsx
525
- import { useAction } from 'weifuwu/react'
526
-
527
- function FeedbackForm() {
528
- const { submit, data, error, pending, reset } = useAction('/api/feedback', { method: 'POST' })
529
-
530
- return <form onSubmit={() => submit({ name, email })}>
531
- <button disabled={pending}>{pending ? 'Saving...' : 'Submit'}</button>
532
- {error && <p className="text-red-500">{error.message}</p>}
533
- {data && <p>Saved: {data.id}</p>}
534
- </form>
535
- }
536
- ```
537
-
538
- Auto-serializes JSON, auto-reads `_csrf` cookie and sends as `X-CSRF-Token`. Returns `{ submit, data, error, pending, reset }`. `submit(body?)` returns `Promise<T>`.
539
-
540
- ### Client-side navigation
541
-
542
- ```tsx
543
- import { Link, useNavigate, useNavigating } from 'weifuwu/react'
544
-
545
- function Nav() {
546
- const navigate = useNavigate()
547
- const loading = useNavigating()
548
- return (
549
- <nav className={loading ? 'opacity-50' : ''}>
550
- <Link href="/about" prefetch>About</Link>
551
- <button onClick={() => navigate('/contact')}>Contact</button>
552
- </nav>
553
- )
554
- }
555
- ```
556
-
557
- `navigate(href)` fetches the target via SSR, extracts `__weifuwu_root` content and `__WEIFUWU_PROPS`, replaces in-place, then imports the new hydration bundle. `load.ts` runs on the server for every navigation. Initial load is full SSR; subsequent navigations are client-side.
558
-
559
- Language and theme switching URLs (`/__lang/:locale`, `/__theme/:theme`) are handled by `navigate()` without page reload — the preference cookie is set via a JSON request and the context is updated via `setCtx()` (see [Preferences](#preferences)).
560
-
561
- - `<Link prefetch>` — pre-fetches page data on hover / when entering viewport (200px margin)
562
- - `useNavigating()` — reactive boolean, `true` while navigation is in-flight
563
- - `isNavigating()` / `onNavigate(fn)` — non-hook alternatives
564
- - Scroll position is saved before navigation and restored after the new page renders
565
-
566
- ### Head — per-page meta tags
567
-
568
- ```tsx
569
- import { Head } from 'weifuwu/react'
570
-
571
- export default function Page() {
572
- return (
573
- <>
574
- <Head>
575
- <title>My Page - App</title>
576
- <meta name="description" content="Page description" />
577
- <meta property="og:title" content="My Page" />
578
- </Head>
579
- <h1>Content</h1>
580
- </>
581
- )
582
- }
264
+ const msg = messager({ pg, agents, redis: redis() })
265
+ await msg.migrate()
266
+ app.ws('/ws', u.middleware(), msg.wsHandler())
267
+ await msg.send(channelId, 'System message', { sender_type: 'system' })
583
268
  ```
584
269
 
585
- During SSR, the `<Head>` content is extracted from the body and merged into `<head>`. On client-side navigation via `<Link>`, title and meta tags are updated automatically.
270
+ ### opencode [C]
586
271
 
587
- ### Flash messages
272
+ AI programming assistant.
588
273
 
589
274
  ```ts
590
- // Server: set a flash message before redirect
591
- router.post('/post', (req, ctx) => {
592
- return ctx.setPref('flash', JSON.stringify({
593
- type: 'success', message: 'Published!'
594
- })) // → 302 with Set-Cookie: flash=...
595
- })
596
- ```
597
-
598
- ```tsx
599
- // Client: display flash from preferences
600
- function Toast() {
601
- const { prefs } = useCtx()
602
- const flash = prefs?.flash ? JSON.parse(prefs.flash) : null
603
- if (!flash) return null
604
- return <div className={`toast ${flash.type}`}>{flash.message}</div>
605
- }
606
- ```
607
-
608
- Flash is read once from the cookie, then automatically cleared on the response. After page refresh the flash is gone.
609
-
610
- ### Client-side state management
611
-
612
- #### createStore — shared state (replaces Zustand)
613
-
614
- ```tsx
615
- import { createStore } from 'weifuwu/react'
616
-
617
- const useStore = createStore({ count: 0, items: [] as string[] })
618
-
619
- function Counter() {
620
- const count = useStore(s => s.count) // selector
621
- const { setState, getState } = useStore() // full state + API
622
- return <button onClick={() => setState({ count: count + 1 })}>{count}</button>
623
- }
624
-
625
- function List() {
626
- const items = useStore(s => s.items)
627
- return items.map(i => <div>{i}</div>)
628
- }
629
-
630
- // Outside components:
631
- useStore.getState()
632
- useStore.setState({ count: 1 })
633
- useStore.subscribe(() => {})
634
- ```
635
-
636
- Uses `useSyncExternalStore` internally. No context provider needed. State persists across client-side navigations (no cache-busting on bundle imports).
637
-
638
- #### useData — data fetching (replaces React Query / SWR)
639
-
640
- ```tsx
641
- import { useData } from 'weifuwu/react'
642
-
643
- // Client-only fetch (shows loading skeleton on first load)
644
- function PostList() {
645
- const { data, error, loading, mutate } = useData('/api/posts')
646
- if (loading) return <Skeleton />
647
- return <div>{data.posts.map(p => <PostCard key={p.id} post={p} />)}</div>
648
- }
649
-
650
- // With SSR fallback — data from load.ts, client takes over after hydration
651
- function PostList({ load }: { load: { posts: Post[] } }) {
652
- const { data, mutate } = useData('/api/posts', { fallback: load })
653
- return <div>{data.posts.map(p => <PostCard key={p.id} post={p} />)}</div>
654
- }
655
- ```
656
-
657
- In-memory cache with 60s TTL, concurrent request dedup. `mutate(data)` for optimistic updates, `mutate()` for revalidation.
658
-
659
- #### useQueryState — URL query params
660
-
661
- ```tsx
662
- import { useQueryState } from 'weifuwu/react'
663
-
664
- function SearchPage() {
665
- const [q, setQ] = useQueryState('q', '')
666
- const [page, setPage] = useQueryState('page', '1')
667
- const { data } = useData(`/api/search?q=${q}&page=${page}`)
668
-
669
- return (
670
- <>
671
- <input value={q} onChange={e => { setQ(e.target.value); setPage('1') }} />
672
- <Results items={data?.items} />
673
- <Pagination page={Number(page)} onChange={setPage} />
674
- </>
675
- )
676
- }
677
- ```
678
-
679
- Synced with `window.location.search` via `useSyncExternalStore`. Back/forward navigation updates the state. Changes use `history.replaceState` and dispatch a synthetic `popstate` for reactivity.
680
-
681
- ### Development mode
682
-
683
- Auto-detected when `NODE_ENV !== 'production'`. File watching (`chokidar`), single-file recompilation, WebSocket live reload (`/__weifuwu/livereload`), Tailwind CSS v4 auto-compilation.
684
-
685
- ### Tailwind CSS
686
-
687
- If `ui/app.css` exists with `@import "tailwindcss"`, it's compiled automatically. If not found, one is created. PostCSS + `@tailwindcss/postcss`, zero config.
688
-
689
- ### shadcn/ui
690
-
691
- Works out of the box:
692
-
693
- ```bash
694
- npx shadcn@latest init
695
- # Style: your preference
696
- # Base color: your preference
697
- # CSS file path: ui/app.css
698
- # Import alias: @/ → ./ui/
275
+ const oc = await opencode({ pg, permissions: { bash: { allow: true }, write: { allow: false } } })
276
+ await oc.migrate()
277
+ app.use('/opencode', await oc.router())
278
+ app.ws('/opencode', oc.wsHandler())
699
279
  ```
700
280
 
701
- ---
702
-
703
- ## PostgreSQL
281
+ ### postgres [B]
704
282
 
705
283
  ```ts
706
- import { serve, Router, postgres, pgTable, serial, text, boolean, timestamps, sql } from 'weifuwu'
707
-
708
284
  const pg = postgres() // reads DATABASE_URL
709
- const app = new Router()
710
285
  app.use(pg) // injects ctx.sql
711
286
  ```
712
287
 
713
- ### Type-safe DDL
714
-
715
288
  ```ts
716
- const users = pgTable('_users', {
717
- id: serial('id').primaryKey(),
718
- name: text('name').notNull(),
719
- email: text('email').unique().notNull(),
720
- active: boolean('active').default(true),
721
- ...timestamps(),
722
- })
723
-
289
+ // Type-safe DDL
290
+ const users = pgTable('_users', { id: serial('id').primaryKey(), name: text('name').notNull(), email: text('email').unique().notNull(), active: boolean('active').default(true), ...timestamps() })
724
291
  await users.create()
725
292
  await users.createIndex('email')
726
- ```
727
-
728
- ### BoundTable CRUD
729
293
 
730
- ```ts
731
- const users = pg.table('_users', { /* column defs */ })
732
-
733
- const user = await users.insert({ name: 'Alice' })
734
- const batch = await users.insertMany([{ name: 'Alice' }, { name: 'Bob' }])
735
- const found = await users.read(1)
736
- const { count, data } = await users.readMany({ role: 'admin' }, { orderBy: { name: 'asc' }, limit: 10 })
737
- await users.update(1, { name: 'Bob' })
738
- await users.delete(1)
739
-
740
- // Upsert
741
- await users.upsert({ email: 'alice@test.com' }, 'email')
742
-
743
- // Count
744
- const total = await users.count()
745
- const admins = await users.count({ role: 'admin' })
294
+ // BoundTable CRUD
295
+ const t = pg.table('_users', { ... })
296
+ await t.insert({ name: 'Alice' })
297
+ const { count, data } = await t.readMany({ role: 'admin' }, { orderBy: { name: 'asc' }, limit: 10 })
298
+ await t.upsert({ email: 'alice@test.com' }, 'email')
299
+ await pg.transaction(async (tx) => { const users = pg.table('_users', { ... }).withSql(tx); return users.insert({ name: 'Bob' }) })
746
300
  ```
747
301
 
748
- ### Where helpers
302
+ Where helpers: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `isNull`, `isNotNull`, `like`, `contains`, `in_`, `and`, `or`, `not`.
749
303
 
750
- ```ts
751
- import { eq, gte, contains, and, or } from 'weifuwu'
752
-
753
- const { data } = await users.readMany(
754
- and(eq('role', 'admin'), gte('created_at', '2026-01-01')),
755
- { orderBy: { name: 'asc' } },
756
- )
757
- ```
304
+ ### preferences [A]
758
305
 
759
- | Helper | SQL | Example |
760
- |--------|-----|---------|
761
- | `eq(col, val)` | `= $1` | `eq('level', 'error')` |
762
- | `ne(col, val)` | `!= $1` | `ne('status', 'archived')` |
763
- | `gt(col, val)` | `> $1` | `gt('age', 18)` |
764
- | `gte(col, val)` | `>= $1` | `gte('created_at', '2026-01-01')` |
765
- | `lt(col, val)` | `< $1` | `lt('id', beforeId)` |
766
- | `lte(col, val)` | `<= $1` | `lte('score', 100)` |
767
- | `isNull(col)` | `IS NULL` | `isNull('deleted_at')` |
768
- | `isNotNull(col)` | `IS NOT NULL` | `isNotNull('email')` |
769
- | `like(col, pattern)` | `LIKE $1` | `like('name', 'Alice%')` |
770
- | `contains(col, obj)` | `@> $1::jsonb` | `contains('metadata', { service: 'auth' })` |
771
- | `in_(col, arr)` | `= ANY($1)` | `in_('id', [1, 2, 3])` |
772
- | `and(...conds)` | `(... AND ...)` | `and(eq('a', 1), eq('b', 2))` |
773
- | `or(...conds)` | `(... OR ...)` | `or(eq('a', 1), eq('b', 2))` |
774
- | `not(cond)` | `NOT (...)` | `not(eq('status', 'archived'))` |
775
-
776
- ### Transactions
306
+ Locale detection + theme + translations. `/__lang/:locale` and `/__theme/:theme` auto-routed.
777
307
 
778
308
  ```ts
779
- const result = await pg.transaction(async (tx) => {
780
- const users = pg.table('_users', { ... }).withSql(tx)
781
- const user = await users.insert({ name: 'Alice' })
782
- return user
783
- })
309
+ app.use(preferences({ dir: './locales', locale: { default: 'en' }, theme: { default: 'system' } }))
310
+ // ctx.prefs.locale, ctx.prefs.theme, ctx.t('key'), ctx.setPref('locale', 'zh')
311
+ // GET /__lang/zh 302 + Set-Cookie (or JSON if Accept: application/json)
312
+ // GET /__theme/dark → same pattern
784
313
  ```
785
314
 
786
- ### Column types
787
-
788
- | Builder | DDL | TS Type |
789
- |---------|-----|---------|
790
- | `serial()` | `SERIAL` | `number` |
791
- | `uuid()` | `UUID` | `string` |
792
- | `text()` | `TEXT` | `string` |
793
- | `integer()` | `INTEGER` | `number` |
794
- | `boolean()` | `BOOLEAN` | `boolean` |
795
- | `timestamptz()` | `TIMESTAMPTZ` | `string` |
796
- | `jsonb<T>()` | `JSONB` | `T` |
797
- | `textArray()` | `TEXT[]` | `string[]` |
798
- | `vector(name, dims)` | `vector(N)` | `number[]` |
799
- | `timestamps()` | Two TIMESTAMPTZ columns | `{ created_at, updated_at }` |
800
-
801
- ---
802
-
803
- ## Auth
804
-
805
- ```ts
806
- import { user, postgres } from 'weifuwu'
807
-
808
- const pg = postgres()
809
- const auth = user({ pg, jwtSecret: process.env.JWT_SECRET! })
810
- await auth.migrate()
811
-
812
- app.use('/auth', auth.router())
813
-
814
- // POST /auth/register { email, password, name }
815
- // POST /auth/login { email, password }
315
+ | Option | Default | Description |
316
+ |--------|---------|-------------|
317
+ | `dir` | | Translation JSON directory |
318
+ | `locale.default` | `'en'` | Fallback locale |
319
+ | `locale.cookie` | `'locale'` | Cookie name |
320
+ | `locale.fromAcceptLanguage` | `true` | Detect from header |
321
+ | `theme.default` | `'system'` | `'light'` \| `'dark'` \| `'system'` |
322
+ | `theme.cookie` | `'theme'` | Cookie name |
816
323
 
817
- app.get('/me', auth.middleware(), (req, ctx) =>
818
- Response.json(ctx.user)
819
- )
324
+ ```tsx
325
+ // Client-side no-refresh switching with <Link>
326
+ <Link href="/__lang/zh">中文</Link>
820
327
  ```
821
328
 
822
- ### OAuth2 Server
329
+ ### queue [B]
823
330
 
824
331
  ```ts
825
- const auth = user({ pg, jwtSecret, oauth2: { server: true } })
826
- await auth.migrate()
827
-
828
- // Register OAuth2 client
829
- const client = await auth.registerClient({ name: 'My App', redirectUris: ['https://app.com/cb'] })
830
-
831
- // Authorization code + PKCE flow built-in
332
+ const q = queue({ redis })
333
+ await q.add('send-email', { to: 'user@test.com' }, { cron: '0 8 * * *' })
832
334
  ```
833
335
 
834
- | Grant | Use Case |
835
- |-------|----------|
836
- | `authorization_code` (client_secret) | Server-side apps |
837
- | `authorization_code` (PKCE) | SPA / Mobile apps |
838
- | `client_credentials` | Machine-to-machine |
839
-
840
- ---
841
-
842
- ## WebSocket & Real-time
843
-
844
- ### Server-side
336
+ ### rateLimit [B]
845
337
 
846
338
  ```ts
847
- app.ws('/chat/:room', {
848
- open(ws, ctx) { ws.send(`joined room ${ctx.params.room}`) },
849
- message(ws, ctx, data) { /* handle message */ },
850
- close(ws, ctx) { /* cleanup */ },
851
- error(ws, ctx, err) { /* log */ },
852
- })
339
+ app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min
340
+ app.get('/api', rateLimit({ max: 10 }), handler) // per-route
341
+ app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
342
+ // m.stop() clear interval
853
343
  ```
854
344
 
855
- ### Cross-process with createHub
345
+ ### redis [B]
856
346
 
857
347
  ```ts
858
- import { createHub, redis } from 'weifuwu'
859
-
860
- const hub = createHub({ redis }) // omit redis for in-process only
861
-
862
- app.ws('/chat/:room', {
863
- open(ws, ctx) { hub.join(`room:${ctx.params.room}`, ws) },
864
- message(ws, ctx, data) { hub.broadcast(`room:${ctx.params.room}`, { text: data.toString() }) },
865
- close(ws) { hub.leave(ws) },
866
- })
348
+ const r = redis() // reads REDIS_URL
349
+ app.use(r) // injects ctx.redis
350
+ await ctx.redis.set('key', 'value')
351
+ // r.close() — cleanup
867
352
  ```
868
353
 
869
- ---
870
-
871
- ## AI
872
-
873
- ### Streaming
354
+ ### requestId [A]
874
355
 
875
356
  ```ts
876
- import { aiStream, openai } from 'weifuwu'
877
-
878
- const chat = await aiStream(async (req) => ({
879
- model: openai('gpt-4o'),
880
- messages: (await req.json()).messages,
881
- }))
882
- app.use('/chat', chat.router())
357
+ app.use(requestId())
358
+ // Sets X-Request-ID header on responses, available as ctx.requestId
883
359
  ```
884
360
 
885
- ### AI Agents
361
+ ### seo [D] + seoMiddleware [A]
886
362
 
887
363
  ```ts
888
- import { agent } from 'weifuwu'
889
-
890
- const agents = agent({ pg })
891
- await agents.migrate()
892
- app.use('/api', agents.router())
364
+ app.use(seo({ baseUrl: 'https://example.com', robots: [{ userAgent: '*', allow: '/' }], sitemap: { urls: [{ loc: '/' }] } }))
365
+ // GET /robots.txt, GET /sitemap.xml
893
366
 
894
- await agents.addKnowledge(agentId, 'Title', 'Document content...')
367
+ app.use(seoMiddleware({ headers: { 'X-Robots-Tag': (path) => path.startsWith('/admin') ? 'noindex' : undefined } }))
895
368
  ```
896
369
 
897
- ### DAG Workflow
370
+ ### tenant [C]
898
371
 
899
- ```ts
900
- import { runWorkflow, tool, streamText } from 'weifuwu'
901
- import { z } from 'zod'
372
+ Multi-tenant BaaS with dynamic table API and GraphQL.
902
373
 
903
- const tools = { queryUser: tool({ ... }) }
904
- const wf = runWorkflow({ tools })
374
+ ```ts
375
+ const t = tenant({ pg, usersTable: '_users' })
376
+ await t.migrate()
377
+ app.use('/api', t.middleware()) // → ctx.tenant
378
+ app.use('/api', t.router()) // dynamic CRUD
379
+ app.use('/graphql', t.graphql()) // dynamic GraphQL
905
380
  ```
906
381
 
907
- ---
908
-
909
- ## Data Layer
910
-
911
- ### Redis
382
+ ### upload [A]
912
383
 
913
384
  ```ts
914
- import { redis } from 'weifuwu'
915
-
916
- const r = redis() // reads REDIS_URL
917
- app.use(r) // injects ctx.redis
918
-
919
- await ctx.redis.set('key', 'value')
385
+ app.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760 }), (req, ctx) => {
386
+ // ctx.parsed.files.avatar → { name, type, size, path }
387
+ // ctx.parsed.fields.title 'hello'
388
+ })
920
389
  ```
921
390
 
922
- ### Queue
391
+ ### user [C]
923
392
 
924
393
  ```ts
925
- import { queue, redis } from 'weifuwu'
926
-
927
- const q = queue({ redis })
928
- await q.add('send-email', { to: 'user@test.com' }, { cron: '0 8 * * *' })
394
+ const auth = user({ pg, jwtSecret: process.env.JWT_SECRET! })
395
+ await auth.migrate()
396
+ app.use('/auth', auth.router()) // POST /register, POST /login
397
+ app.get('/me', auth.middleware(), (req, ctx) => Response.json(ctx.user))
929
398
  ```
930
399
 
931
- ---
932
-
933
- ## iii — Worker / Function / Trigger
934
-
935
- Optional module for organizing service logic as Worker + Function + Trigger, plus a pure WebSocket SDK for remote workers.
400
+ ### validate [A]
936
401
 
937
402
  ```ts
938
- import { iii, createWorker, registerWorker } from 'weifuwu'
939
-
940
- const engine = iii({ pg, redis })
941
- app.use('/iii', engine.router())
942
-
943
- const w = createWorker('orders')
944
- w.registerFunction('orders::create', async (payload) => {
945
- return db.query('INSERT INTO orders ...', [payload.items])
403
+ import { z } from 'zod'
404
+ const CreateUser = z.object({ name: z.string().min(1), email: z.string().email() })
405
+ app.post('/users', validate({ body: CreateUser }), (req, ctx) => {
406
+ // ctx.parsed.body — typed & validated
946
407
  })
947
- engine.addWorker(w)
948
-
949
- // Invoke
950
- await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })
951
408
  ```
952
409
 
953
- ### Built-in stream functions
954
-
955
- | Function | Description |
956
- |----------|-------------|
957
- | `stream::set(stream_name, group_id, item_id, data)` | Write + persist + notify |
958
- | `stream::get(stream_name, group_id, item_id)` | Read single item |
959
- | `stream::delete(stream_name, group_id, item_id)` | Delete + notify |
960
- | `stream::list(stream_name, group_id)` | List items in a group |
961
- | `stream::list_groups(stream_name)` | List groups in a stream |
962
- | `stream::list_all()` | List all streams |
963
- | `stream::send(stream_name, group_id, type, data, id?)` | Push event without persisting |
964
-
965
- ### Storage backends
966
-
967
- | Config | Persistence | Broadcast |
968
- |--------|-------------|-----------|
969
- | `iii({})` | In-memory | — |
970
- | `iii({ pg })` | PG table | — |
971
- | `iii({ redis })` | Redis Hash | Redis pub/sub |
972
- | `iii({ pg, redis })` | PG table | Redis pub/sub |
973
-
974
- ### Trigger actions
975
-
976
- | Action | Behavior |
977
- |--------|----------|
978
- | `'sync'` (default) | Wait for result |
979
- | `'void'` | Fire-and-forget |
980
-
981
- ### REST API
982
-
983
- | Path | Description |
984
- |------|-------------|
985
- | `GET /iii/workers` | List connected workers |
986
- | `GET /iii/functions` | List registered functions |
987
- | `GET /iii/triggers` | List registered triggers |
988
- | `POST /iii/trigger/:fnId` | Invoke a function |
989
- | `WS /iii/worker` | Remote worker connection |
990
-
991
410
  ---
992
411
 
993
- ## Multi-tenant BaaS
412
+ ## React SSR (tsx)
994
413
 
995
414
  ```ts
996
- import { tenant } from 'weifuwu'
997
-
998
- const t = tenant({ pg, usersTable: '_users' })
999
- await t.migrate()
1000
-
1001
- app.use('/api', t.middleware()) // → ctx.tenant
1002
- app.use('/api', t.router()) // → dynamic CRUD
1003
- app.use('/graphql', t.graphql()) // → dynamic GraphQL
1004
- ```
1005
-
1006
- ### Dynamic table API
1007
-
1008
- ```json
1009
- POST /api/tables
1010
- { "slug": "articles", "fields": [
1011
- { "name": "title", "type": "string", "required": true },
1012
- { "name": "views", "type": "integer", "default": 0 }
1013
- ]}
415
+ app.use('/', await tsx({ dir: './ui/' }))
1014
416
  ```
1015
417
 
1016
- Field types: `string`, `integer`, `float`, `boolean`, `text`, `datetime`, `date`, `enum`, `json`, `vector`.
1017
-
1018
- ### REST API
1019
-
1020
- | Method | Path | Description |
1021
- |--------|------|-------------|
1022
- | GET/POST | `/sys/tables` | List / create dynamic tables |
1023
- | GET/POST/PATCH/DELETE | `/:slug[/:id]` | Dynamic CRUD |
1024
- | POST/POST | `/:slug/:id/:nested` | Related rows |
1025
-
1026
- ---
1027
-
1028
- ## Messager
1029
-
1030
- Real-time chat with channels, WebSocket, and agent routing.
1031
-
1032
- ```ts
1033
- import { messager, agent, redis } from 'weifuwu'
1034
-
1035
- const msg = messager({ pg, agents, redis: redis() })
1036
- await msg.migrate()
1037
- app.ws('/ws', u.middleware(), msg.wsHandler())
1038
418
  ```
1039
-
1040
- ### Channels & Messages
1041
-
1042
- ```http
1043
- POST /api/channels { name, type, members }
1044
- POST /api/channels/:id/messages { content }
1045
- GET /api/channels/:id/messages
419
+ ui/
420
+ ├── pages/
421
+ │ ├── page.tsx → GET /
422
+ │ ├── layout.tsx → root layout
423
+ │ ├── not-found.tsx → 404
424
+ │ ├── about/page.tsx → GET /about
425
+ │ ├── blog/[slug]/
426
+ │ │ ├── page.tsx → GET /blog/:slug
427
+ │ │ ├── load.ts → server data fetching
428
+ │ │ └── route.ts → API (named exports: POST, PUT...)
429
+ │ ├── blog/layout.tsx → nested layout
430
+ │ └── api/search/
431
+ │ └── route.ts → GET /api/search
432
+ └── components/
1046
433
  ```
1047
434
 
1048
- ### WebSocket events
1049
-
1050
- ```json
1051
- { "type": "message", "channel_id": 1, "content": "Hi" }
1052
- { "type": "typing", "channel_id": 1, "is_typing": true }
1053
- { "type": "read", "channel_id": 1, "last_message_id": 42 }
435
+ ```tsx
436
+ // page.tsx
437
+ export default function Page({ params, query }: { params: { slug: string }; query: Record<string, string> }) {
438
+ const { t } = useCtx()
439
+ return <h1>{t('title')}</h1>
440
+ }
1054
441
  ```
1055
442
 
1056
- ### Programmatic send
1057
-
1058
- ```ts
1059
- await msg.send(channelId, 'System message', { sender_type: 'system' })
443
+ ```tsx
444
+ // layout.tsx
445
+ export default function RootLayout({ children, req }: { children: React.ReactNode; req: Request }) {
446
+ return <html><head/><body><main>{children}</main></body></html>
447
+ }
1060
448
  ```
1061
449
 
1062
- ---
1063
-
1064
- ## LogDB
1065
-
1066
- PostgreSQL-backed structured event logging with monthly partitioning.
1067
-
1068
450
  ```ts
1069
- import { logdb } from 'weifuwu'
1070
-
1071
- const logger = logdb({ pg })
1072
- await logger.migrate()
1073
- app.use('/logs', logger.router())
451
+ // load.ts server-only data fetching
452
+ export default async function load({ params, query }) { return { data: await db.query(params.slug) } }
1074
453
  ```
1075
454
 
1076
- ### REST API
1077
-
1078
- | Method | Path | Description |
1079
- |--------|------|-------------|
1080
- | `POST` | `/` | Create log entry |
1081
- | `GET` | `/` | Query entries (supports `?level=`, `?source=`, `?after=`, `?before=`, `?meta.*=`) |
1082
- | `GET` | `/:id` | Get single entry |
1083
-
1084
- ### Retention
1085
-
1086
455
  ```ts
1087
- await logger.clean(12) // Drop partitions older than 12 months
456
+ // route.ts API co-located with page
457
+ export const POST: Handler = async (req, ctx) => Response.json({ slug: ctx.params.slug })
1088
458
  ```
1089
459
 
1090
- ---
1091
-
1092
- ## SEO
460
+ ### Client-side navigation
1093
461
 
1094
- ```ts
1095
- import { seo, seoMiddleware, seoTags } from 'weifuwu'
1096
-
1097
- app.use(seo({
1098
- baseUrl: 'https://example.com',
1099
- robots: [{ userAgent: '*', allow: '/', disallow: ['/admin'] }],
1100
- sitemap: {
1101
- urls: [{ loc: '/', changefreq: 'daily', priority: 1.0 }],
1102
- async resolve() { /* dynamic URLs */ },
1103
- cacheTTL: 3_600_000,
1104
- },
1105
- }))
1106
-
1107
- // Middleware: X-Robots-Tag header
1108
- app.use(seoMiddleware({ headers: { 'X-Robots-Tag': (path) => path.startsWith('/admin') ? 'noindex' : undefined } }))
462
+ ```tsx
463
+ import { Link, useNavigate, useNavigating } from 'weifuwu/react'
1109
464
 
1110
- // Tag generator for SSR
1111
- const tags = seoTags({ title: 'My Page', description: '...', ogImage: '/og.png', canonical: 'https://...' })
465
+ <Link href="/about" prefetch>About</Link> // client-side nav + prefetch on hover/visible
466
+ const navigate = useNavigate() // programmatic: navigate('/contact')
467
+ const loading = useNavigating() // reactive loading state
1112
468
  ```
1113
469
 
1114
- | Endpoint | Description |
1115
- |----------|-------------|
1116
- | `GET /robots.txt` | Generated robots.txt |
1117
- | `GET /sitemap.xml` | Generated XML sitemap (cached) |
470
+ `navigate()` fetches SSR, extracts `__weifuwu_root`, replaces in-place. `load.ts` runs on server each nav. `/__lang/:locale` and `/__theme/:theme` intercepted for no-refresh switching.
1118
471
 
1119
- ---
472
+ ### Client-side hooks
1120
473
 
1121
- ## Opencode
474
+ ```tsx
475
+ import { useWebsocket, useAction, useData, useQueryState, createStore, Head, setCtx } from 'weifuwu/react'
1122
476
 
1123
- AI programming assistant chat with LLM agents that have filesystem access.
477
+ // WebSocketauto-reconnecting
478
+ const { send, lastMessage, readyState } = useWebsocket('/ws/chat', { onMessage: (d) => console.log(d), reconnect: { maxRetries: 10, delay: 3000 } })
1124
479
 
1125
- ```ts
1126
- import { opencode } from 'weifuwu'
480
+ // Form action
481
+ const { submit, data, error, pending } = useAction('/api/feedback', { method: 'POST' })
482
+ // Auto-reads _csrf cookie, sends as X-CSRF-Token
1127
483
 
1128
- const oc = await opencode({ pg, permissions: { bash: { allow: true }, write: { allow: false } } })
1129
- await oc.migrate()
1130
- app.use('/opencode', await oc.router())
1131
- app.ws('/opencode', oc.wsHandler())
1132
- ```
484
+ // Data fetching cache + dedup + mutate
485
+ const { data, error, loading, mutate } = useData('/api/posts', { fallback: loadData })
1133
486
 
1134
- ---
487
+ // URL query state
488
+ const [q, setQ] = useQueryState('q', '')
489
+ const [page, setPage] = useQueryState('page', '1')
1135
490
 
1136
- ## Deploy
491
+ // Shared state — persists across client navs
492
+ const useStore = createStore({ count: 0 })
493
+ const count = useStore(s => s.count)
1137
494
 
1138
- Self-hosted PaaS: multi-app proxy, zero-downtime updates, auto SSL.
495
+ // Per-page meta tags
496
+ <Head><title>Page Title</title><meta name="description" content="..." /></Head>
1139
497
 
1140
- ```ts
1141
- import { deploy, defineConfig } from 'weifuwu'
1142
-
1143
- const config = defineConfig({
1144
- apps: [{ name: 'api', dir: './api', domain: 'api.example.com', port: 3001 }],
1145
- })
1146
- await deploy(config)
498
+ // Update context (locale switch etc.)
499
+ setCtx({ locale: 'en', prefs: { locale: 'en' } })
1147
500
  ```
1148
501
 
1149
- ---
1150
-
1151
- ## Analytics
1152
-
1153
- In-memory page view tracking with a built-in dashboard. Zero extra dependencies.
502
+ ### Flash messages
1154
503
 
1155
504
  ```ts
1156
- import { analytics } from 'weifuwu'
1157
-
1158
- app.use(analytics()) // mounts middleware + /__analytics + /__analytics/data
1159
- ```
1160
-
1161
- | Endpoint | Description |
1162
- |----------|-------------|
1163
- | `GET /__analytics` | Dashboard — PV trend, top pages, referrers, device breakdown |
1164
- | `GET /__analytics/data?days=7` | Raw JSON data for custom dashboards |
1165
-
1166
- Excluded paths (not recorded): `/__analytics/*`, `/__wfw/*`, `/static/*`.
1167
-
1168
- ### Dashboard
1169
-
1170
- The built-in `/__analytics` page renders a server-generated HTML dashboard with:
1171
-
1172
- - **Summary cards** — total PV, unique pages, mobile/desktop ratio
1173
- - **Bar chart** — daily page views for the selected period
1174
- - **Top pages table** — most visited paths ranked by views
1175
- - **Referrers table** — top referring domains
505
+ // Server
506
+ return ctx.setPref('flash', JSON.stringify({ type: 'success', message: 'Done' })) // 302 + Set-Cookie
1176
507
 
1177
- ### JSON API
1178
-
1179
- ```ts
1180
- // GET /__analytics/data?days=30
1181
- {
1182
- "total_pv": 2847,
1183
- "total_uv": 1231,
1184
- "daily": [{ "date": "2026-06-05", "pv": 520, "uv": 310 }],
1185
- "top_pages": [{ "path": "/tools/uppercase", "pv": 1284 }],
1186
- "referrers": [{ "domain": "google.com", "count": 380 }],
1187
- "devices": { "mobile": 45.2, "desktop": 54.8 }
1188
- }
508
+ // Client (tsx)
509
+ function Toast() { const { prefs } = useCtx(); const flash = prefs?.flash ? JSON.parse(prefs.flash) : null; ... }
1189
510
  ```
1190
511
 
1191
- ### Options
512
+ ### Dev mode
1192
513
 
1193
- ```ts
1194
- app.use(analytics({
1195
- excluded: ['/admin', '/api'], // custom exclude patterns (defaults listed above)
1196
- }))
1197
- ```
514
+ Auto-detected when `NODE_ENV !== 'production'`. File watching, live reload, Tailwind v4 auto-compile.
1198
515
 
1199
516
  ---
1200
517
 
1201
- ## Health check
518
+ ## AI
1202
519
 
1203
520
  ```ts
1204
- import { health } from 'weifuwu'
1205
-
1206
- app.use(health()) // GET /health → 200
1207
-
1208
- // Custom checks
1209
- app.use(health({
1210
- checks: { db: async () => { await pg.sql`SELECT 1`; return { ok: true } } },
1211
- }))
521
+ import { openai, streamText, generateText, streamObject, generateObject, tool, embed, embedMany } from 'weifuwu'
522
+ import { runWorkflow } from 'weifuwu'
1212
523
  ```
1213
524
 
1214
- ---
1215
-
1216
- ## Preferences
525
+ ### Streaming
1217
526
 
1218
527
  ```ts
1219
- import { preferences } from 'weifuwu'
1220
-
1221
- app.use(preferences({
1222
- dir: './locales', // translation directory (optional)
1223
- locale: { default: 'en' }, // locale detection
1224
- theme: { default: 'system' }, // 'light' | 'dark' | 'system'
1225
- }))
1226
-
1227
- // In handlers: ctx.t('greeting') → "Hello"
1228
- // ctx.t('tools.uppercase.title') → "Uppercase" (nested key)
1229
- // ctx.locale → "en"
1230
- // ctx.theme → "light"
1231
- // ctx.prefs → { locale: 'en', theme: 'light' }
1232
- // ctx.setPref('locale', 'zh') → 302 + cookie
1233
- // ctx.setPref('flash', '{"type":"success","message":"Done"}') → flash message
1234
- // ctx.env → { WEIFUWU_PUBLIC_API_URL: '...' } (public env vars)
1235
-
1236
- // In tsx components:
1237
- const { t, locale, theme } = useCtx()
528
+ const chat = await aiStream(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages }))
529
+ app.use('/chat', chat.router().handler())
1238
530
  ```
1239
531
 
1240
- Locale detection priority: cookie → `Accept-Language` → default.
1241
- Theme detection: cookie → default (`'system'`).
1242
- Flash messages: set via `ctx.setPref('flash', ...)` → auto-read from cookie → cleared after rendering.
1243
-
1244
- `ctx.t()` supports dot-path nested keys: `t('tools.uppercase.title')` traverses the JSON structure.
1245
- `ctx.env` exposes `WEIFUWU_PUBLIC_*` environment variables on both server and client (via `useCtx().env`).
1246
-
1247
- ### Language / Theme switching
1248
-
1249
- The middleware automatically handles `/__lang/:locale` and `/__theme/:theme` without needing extra routes:
532
+ ### Agents
1250
533
 
1251
534
  ```ts
1252
- app.use(preferences({ dir: './locales' }))
1253
- // ✓ /__lang/zh, /__lang/en, /__theme/dark, /__theme/light automatically work
1254
- ```
1255
-
1256
- **Server behavior:**
1257
-
1258
- | Mode | Request | Response |
1259
- |------|---------|----------|
1260
- | Redirect | `GET /__lang/zh` (browser) | 302 with `Set-Cookie: locale=zh` back to referer |
1261
- | JSON | `GET /__lang/zh` + `Accept: application/json` | `{ ok: true, locale: 'zh', messages: { ... } }` + `Set-Cookie` |
1262
-
1263
- **Client-side no-refresh switching:**
1264
-
1265
- ```tsx
1266
- import { Link } from 'weifuwu/react'
1267
-
1268
- <Link href="/__lang/zh">中文</Link> // Instant switch — no page reload
1269
- <Link href="/__lang/en">English</Link> // Instant switch — no page reload
1270
- <Link href="/__theme/dark">🌙</Link> // Instant switch — no page reload
535
+ const agents = agent({ pg })
536
+ await agents.migrate()
537
+ app.use('/api', agents.router())
538
+ await agents.addKnowledge(agentId, 'Title', 'content')
1271
539
  ```
1272
540
 
1273
- When using `<Link>` (or `navigate()`), the router intercepts `__lang`/`__theme` URLs:
1274
- 1. Fetches the endpoint with `Accept: application/json`
1275
- 2. Updates `window.__WEIFUWU_CTX` (locale, theme, messages)
1276
- 3. Calls `setCtx()` — React re-renders via `useSyncExternalStore`
1277
- 4. Page content stays intact, only translations/theme update
1278
-
1279
- Plain `<a href="/__lang/zh">` still works via traditional 302 redirect for full backward compatibility.
1280
-
1281
- ### Options
1282
-
1283
- ```ts
1284
- app.use(preferences({
1285
- dir: './locales', // translation JSON directory
1286
- locale: {
1287
- default: 'en', // fallback when no cookie or Accept-Language
1288
- cookie: 'locale', // cookie name (default: 'locale')
1289
- fromAcceptLanguage: true, // detect from Accept-Language header
1290
- },
1291
- theme: {
1292
- default: 'system', // 'light' | 'dark' | 'system'
1293
- cookie: 'theme', // cookie name (default: 'theme')
1294
- },
1295
- }))
1296
-
1297
- ---
1298
-
1299
- ## Email
541
+ ### DAG Workflow
1300
542
 
1301
543
  ```ts
1302
- import { mailer } from 'weifuwu'
1303
-
1304
- const mail = mailer({ host: 'smtp.example.com', port: 587, auth: { user: 'user', pass: 'pass' } })
1305
- await mail.send({ to: 'user@test.com', subject: 'Hello', text: 'Body', html: '<p>Body</p>' })
544
+ const tools = { queryUser: tool({ ... }) }
545
+ const wf = runWorkflow({ tools })
1306
546
  ```
1307
547
 
1308
548
  ---
@@ -1311,50 +551,26 @@ await mail.send({ to: 'user@test.com', subject: 'Hello', text: 'Body', html: '<p
1311
551
 
1312
552
  ```ts
1313
553
  import { createSSEStream, formatSSE, formatSSEData } from 'weifuwu'
1314
-
1315
- async function* events() {
1316
- yield formatSSE('chat', 'Hello')
1317
- yield formatSSE('chat', 'World')
1318
- }
1319
-
554
+ async function* events() { yield formatSSE('chat', 'Hello'); yield formatSSE('chat', 'World') }
1320
555
  app.get('/stream', (req, ctx) => createSSEStream(events()))
1321
556
  ```
1322
557
 
1323
558
  ---
1324
559
 
1325
- ## Utility functions
560
+ ## Utility Functions
561
+
562
+ ### Common
1326
563
 
1327
564
  | Function | Description |
1328
565
  |----------|-------------|
1329
566
  | `loadEnv(path?)` | Load `.env` into `process.env` |
1330
- | `serveStatic(root, options?)` | Static file serving |
1331
- | `getCookies(req)` | Parse cookies from request |
1332
- | `setCookie(res, name, value, opts?)` | Set cookie on response |
1333
- | `deleteCookie(res, name, opts?)` | Delete cookie from response |
1334
- | `createTestServer(handler)` | One-line test server → `{ server, url }` |
1335
- | `createSSEStream(iterable, opts?)` | SSE response from AsyncIterable |
1336
- | `formatSSE(event, data)` | Format SSE event string |
1337
- | `formatSSEData(data)` | Format SSE data string |
1338
- | `runWorkflow(options)` | DAG execution engine as AI SDK Tool |
1339
-
1340
- ### React exports (`weifuwu/react`)
1341
-
1342
- | Hook / Component | Description |
1343
- |-----------------|-------------|
1344
- | `useCtx()` | Unified context — `{ prefs, locale, theme, t, params, query, env }` (requires `preferences` middleware) |
1345
- | `setCtx(value)` | Update context — triggers re-render in all `useCtx()` consumers |
1346
- | `createStore(initial)` | Zustand-compatible shared state — `getState`, `setState`, `subscribe` |
1347
- | `useData(url, opts?)` | SWR-style data fetching — cache, dedup, mutate, fallback |
1348
- | `useQueryState(key, default)` | URL query param sync — `?page=1` via `useSyncExternalStore` |
1349
- | `useAction(url, opts?)` | Async form submission — `{ submit, data, error, pending }` |
1350
- | `useWebsocket(url, opts?)` | Auto-reconnecting WebSocket — `{ send, lastMessage, readyState }` |
1351
- | `useNavigate()` | Client-side navigation callback |
1352
- | `useNavigating()` | Reactive loading state during navigation |
1353
- | `navigate(href)` | Client-side page navigation (imperative) |
1354
- | `Link` | `<Link href prefetch>` — prefetch on hover/visible |
1355
- | `Head` | `<Head>` — per-page `<title>` / `<meta>` merged into `<head>` |
1356
-
1357
- ### AI SDK re-exports
567
+ | `serveStatic(root, opts?)` | Static file serving (20+ MIME, ETag, 304, path traversal protection) |
568
+ | `getCookies(req)` | Parse cookies |
569
+ | `setCookie(res, name, value, opts?)` | Set cookie |
570
+ | `deleteCookie(res, name, opts?)` | Delete cookie |
571
+ | `createTestServer(handler)` | `→ { server, url }` |
572
+
573
+ ### AI re-exports
1358
574
 
1359
575
  ```ts
1360
576
  streamText, generateText, streamObject, generateObject,
@@ -1365,10 +581,9 @@ openai, createOpenAI
1365
581
  ### pgTable helpers
1366
582
 
1367
583
  ```ts
1368
- pgTable(name, columns), pg.table(name, columns),
584
+ pgTable, pg.table,
1369
585
  serial, uuid, text, integer, boolean, timestamptz, jsonb, textArray, vector, timestamps,
1370
- eq, ne, gt, gte, lt, lte, isNull, isNotNull, like, contains, in_, and, or, not,
1371
- PgModule
586
+ eq, ne, gt, gte, lt, lte, isNull, isNotNull, like, contains, in_, and, or, not
1372
587
  ```
1373
588
 
1374
589
  ---
@@ -1390,11 +605,8 @@ describe('hello', () => {
1390
605
  })
1391
606
  ```
1392
607
 
1393
- For end-to-end tests:
1394
-
1395
608
  ```ts
1396
609
  import { createTestServer } from 'weifuwu'
1397
-
1398
610
  const { server, url } = await createTestServer(handler)
1399
611
  const res = await fetch(`${url}/api/ping`)
1400
612
  ```