weifuwu 0.18.0 → 0.18.2

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
@@ -15,11 +15,14 @@ serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
15
15
  ```
16
16
 
17
17
  ```ts
18
- import { serve, Router, tsx, preferences } from 'weifuwu'
18
+ import { serve, Router, preferences } from 'weifuwu'
19
+ import { ssr, layout, liveReload } from 'weifuwu/ssr'
19
20
  const app = new Router()
20
21
  app.use(preferences({ dir: './locales' }))
21
- app.use('/', await tsx({ dir: './ui' }))
22
- serve(app.handler(), { port: 3000 })
22
+ app.use(layout('./layouts/root.tsx'))
23
+ app.get('/', ssr('./pages/home.tsx'))
24
+ app.use(liveReload({ dirs: ['./pages', './layouts'] }))
25
+ serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
23
26
  ```
24
27
 
25
28
  ```bash
@@ -88,26 +91,54 @@ app.get('/admin', mw, handler) // route-level
88
91
 
89
92
  ## Module Patterns
90
93
 
91
- All modules follow one of 6 patterns. The pattern letter is marked in each module's heading.
94
+ All modules follow one of **2 patterns** learn these and you know every module.
92
95
 
93
96
  | Pattern | How to mount | Example |
94
97
  |---------|-------------|---------|
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)` |
98
+ | `[α]` | `app.use(mod())` | `compress()`, `preferences()`, `postgres()` |
99
+ | `[β]` | `app.use('/path', mod())` | `health()`, `graphql(handler)`, `user()` |
100
+
101
+ ### Pattern α Middleware
102
+
103
+ ```ts
104
+ app.use(compress()) // basic
105
+ const pg = postgres() // with extras: .sql, .table, .migrate(), .close()
106
+ app.use(pg)
107
+ app.use(rateLimit({ max: 100 })) // with .stop()
108
+ ```
109
+
110
+ ### Pattern β — Router
111
+
112
+ ```ts
113
+ app.use('/health', health()) // with path
114
+ app.use('/graphql', graphql(handler))
115
+ app.use('/logs', logdb({ pg })) // with .log(), .migrate()
116
+ app.use('/auth', user({ pg, jwtSecret })) // with .middleware(), .register()
117
+ app.ws('/ws', messager({ pg }).wsHandler())
118
+ ```
119
+
120
+ β modules can also be mounted **without a path** — internal routes (`/__xxx`) are inaccessible to the user:
121
+ ```ts
122
+ app.use(liveReload({ dirs: ['./pages'] })) // no path, /__weifuwu/livereload
123
+ ```
124
+
125
+ β modules that need **separate middleware** use `.middleware()`:
126
+ ```ts
127
+ const a = analytics()
128
+ app.use(a.middleware()) // tracking
129
+ app.use('/', a) // dashboard
130
+ ```
100
131
 
101
132
  ---
102
133
 
103
134
  ## Module Reference
104
135
 
105
- ### agent [C]
136
+ ### agent [β]
106
137
 
107
138
  ```ts
108
139
  const a = agent({ pg, model: openai('gpt-4o'), embeddingModel: openai.embedding('text-embedding-3-small') })
109
140
  await a.migrate()
110
- app.use('/api', a.router())
141
+ app.use('/api', a)
111
142
  await a.addKnowledge(agentId, 'Title', 'some knowledge content')
112
143
  a.run(agentId, { input: 'summarize the data', stream: true })
113
144
  ```
@@ -124,24 +155,30 @@ a.run(agentId, { input: 'summarize the data', stream: true })
124
155
  |--------|-------------|
125
156
  | `.run(agentId, { input, stream?, messages? })` | Execute agent with input |
126
157
  | `.addKnowledge(agentId, title, content)` | Add knowledge document |
127
- | `.router()` | REST + WS API |
158
+ | `.migrate()` | DB setup |
128
159
  | `.close()` | Cleanup |
129
160
 
130
- ### aiStream [E]
161
+ ### aiStream [β]
162
+
163
+ Creates an AI streaming chat endpoint using the Vercel AI SDK.
131
164
 
132
165
  ```ts
133
166
  const chat = await aiStream(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages }))
134
- app.use('/chat', chat.router().handler())
167
+ app.use('/chat', chat)
135
168
  ```
136
169
 
137
- ### analytics [C]
170
+ | Param | Type | Description |
171
+ |-------|------|-------------|
172
+ | `handler` | `(req, ctx) => AIStreamOptions \| Promise<AIStreamOptions>` | Returns AI SDK options (model, messages, schema, etc.) |
173
+
174
+ ### analytics [β]
138
175
 
139
176
  In-memory or PostgreSQL page view tracking with built-in dashboard.
140
177
 
141
178
  ```ts
142
179
  const a = analytics()
143
180
  app.use(a.middleware())
144
- app.use('/', a.router()) // GET /__analytics (dashboard), GET /__analytics/data?days=7 (JSON)
181
+ app.use('/', a) // GET /__analytics (dashboard), GET /__analytics/data?days=7 (JSON)
145
182
  ```
146
183
 
147
184
  | Option | Type | Default | Description |
@@ -154,10 +191,10 @@ app.use('/', a.router()) // GET /__analytics (dashboard), GET /__analytics
154
191
  const a = analytics({ pg })
155
192
  await a.migrate()
156
193
  app.use(a.middleware())
157
- app.use('/', a.router())
194
+ app.use('/', a) // dashboard routes
158
195
  ```
159
196
 
160
- ### auth [A]
197
+ ### auth [α]
161
198
 
162
199
  ```ts
163
200
  app.use(auth({ token: 'sk-123' })) // static token
@@ -173,7 +210,7 @@ app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
173
210
  | `verify` | `(token, req) => object\|null` | — | Verify function, return value sets `ctx.user` |
174
211
  | `proxy` | `string` | — | Auth service URL to proxy requests to |
175
212
 
176
- ### compress [A]
213
+ ### compress [α]
177
214
 
178
215
  ```ts
179
216
  app.use(compress()) // brotli > gzip > deflate (min 1KB)
@@ -185,7 +222,7 @@ app.use(compress({ threshold: 2048, level: 4 })) // custom threshold and le
185
222
  | `threshold` | `number` | `1024` | Minimum byte size to compress |
186
223
  | `level` | `number` | `6` | Compression level (zlib) |
187
224
 
188
- ### cors [A]
225
+ ### cors [α]
189
226
 
190
227
  ```ts
191
228
  app.use(cors()) // allow all
@@ -203,7 +240,7 @@ app.use(cors({ credentials: true, maxAge: 3600 }))
203
240
  | `credentials` | `boolean` | `false` | Allow cookies/credentials |
204
241
  | `maxAge` | `number` | — | Preflight cache duration (seconds) |
205
242
 
206
- ### csrf [A]
243
+ ### csrf [α]
207
244
 
208
245
  ```ts
209
246
  app.use(csrf())
@@ -234,10 +271,10 @@ const server = await deploy(config)
234
271
  // server.apps.list(), server.apps.status(name), server.apps.deploy(name)
235
272
  ```
236
273
 
237
- ### health [D]
274
+ ### health [β]
238
275
 
239
276
  ```ts
240
- app.use(health({ path: '/health' }))
277
+ app.use('/health', health())
241
278
  // Returns 200 on success, 503 when check throws
242
279
  ```
243
280
 
@@ -246,7 +283,7 @@ app.use(health({ path: '/health' }))
246
283
  | `path` | `string` | `'/health'` | Health check endpoint |
247
284
  | `check` | `() => Promise<void>` | — | Async function; throws → 503 |
248
285
 
249
- ### helmet [A]
286
+ ### helmet [α]
250
287
 
251
288
  15 security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
252
289
 
@@ -267,12 +304,14 @@ app.use(helmet({ contentSecurityPolicy: "default-src 'self'", xFrameOptions: 'DE
267
304
  | `crossOriginOpenerPolicy` | — | COOP header |
268
305
  | `crossOriginResourcePolicy` | — | CORP header |
269
306
 
270
- ### iii [C] — Worker / Function / Trigger
307
+ ### iii [β] — Worker / Function / Trigger
308
+
309
+ Distributed function execution with WebSocket workers, triggers, and Redis streams.
271
310
 
272
311
  ```ts
273
312
  import { createWorker } from 'weifuwu'
274
313
  const engine = iii({ pg, redis })
275
- app.use('/iii', engine.router())
314
+ app.use('/iii', engine)
276
315
  app.ws('/iii', engine.wsHandler())
277
316
 
278
317
  const w = createWorker('orders')
@@ -281,6 +320,12 @@ engine.addWorker(w)
281
320
  await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })
282
321
  ```
283
322
 
323
+ | Option | Type | Default | Description |
324
+ |--------|------|---------|-------------|
325
+ | `pg` | `object` | — | PostgreSQL client for persistent triggers |
326
+ | `redis` | `object` | — | Redis client for streams |
327
+ | `streamTTL` | `number` | `3600` | Redis stream key TTL (seconds, 0 = no expiry) |
328
+
284
329
  | Method | Description |
285
330
  |--------|-------------|
286
331
  | `.addWorker(w)` | Register a worker |
@@ -289,19 +334,18 @@ await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'
289
334
  | `.listWorkers()` | List registered workers |
290
335
  | `.listFunctions()` | List registered functions |
291
336
  | `.listTriggers()` | List registered triggers |
292
- | `.router()` | REST + WS API |
293
337
  | `.wsHandler()` | WebSocket handler |
294
338
  | `.migrate()` | DB setup |
295
339
  | `.shutdown()` | Clean shutdown |
296
340
 
297
- ### logdb [C]
341
+ ### logdb [β]
298
342
 
299
343
  PostgreSQL structured event logging with monthly partitioning.
300
344
 
301
345
  ```ts
302
346
  const logger = logdb({ pg })
303
347
  await logger.migrate()
304
- app.use('/logs', logger.router())
348
+ app.use('/logs', logger)
305
349
  await logger.clean(12) // drop partitions older than 12 months
306
350
  await logger.log({ level: 'info', source: 'app', message: 'hello', metadata: { userId: 1 } })
307
351
  ```
@@ -317,13 +361,17 @@ await logger.log({ level: 'info', source: 'app', message: 'hello', metadata: { u
317
361
  | GET | `/` | Query (`?level=`, `?source=`, `?after=`, `?before=`, `?meta.*=`) |
318
362
  | GET | `/:id` | Get single entry |
319
363
 
320
- ### logger [A]
364
+ ### logger [α]
321
365
 
322
366
  ```ts
323
367
  app.use(logger()) // GET /hello 200 5ms
324
368
  app.use(logger({ format: 'combined' })) // with query params
325
369
  ```
326
370
 
371
+ | Option | Type | Default | Description |
372
+ |--------|------|---------|-------------|
373
+ | `format` | `'short' \| 'combined'` | `'short'` | Log format: path only, or path + query params |
374
+
327
375
  ### mailer
328
376
 
329
377
  ```ts
@@ -337,25 +385,32 @@ await mail.send({ to: 'user@test.com', subject: 'Hello', text: 'Body', html: '<p
337
385
  | `from` | `string` | — | Default sender address |
338
386
  | `send` | `function` | — | Custom send function (alternative to transport) |
339
387
 
340
- ### messager [C]
388
+ ### messager [β]
341
389
 
342
390
  Real-time chat with channels, WebSocket, agent routing.
343
391
 
344
392
  ```ts
345
393
  const msg = messager({ pg, agents, redis: redis() })
346
394
  await msg.migrate()
395
+ app.use('/api', msg)
347
396
  app.ws('/ws', msg.wsHandler())
348
397
  await msg.send(channelId, 'System message', { sender_type: 'system', sender_id: 'bot' })
349
398
  ```
350
399
 
400
+ | Option | Type | Default | Description |
401
+ |--------|------|---------|-------------|
402
+ | `pg` | `object` | — | PostgreSQL client |
403
+ | `agents` | `AgentModule` | — | Agent module for routing |
404
+ | `webhookTimeout` | `number` | — | Webhook timeout |
405
+ | `redis` | `object` | — | Redis client |
406
+
351
407
  | Method | Description |
352
408
  |--------|-------------|
353
409
  | `.wsHandler()` | WebSocket handler (channels, typing, read receipts) |
354
410
  | `.send(channel, content, opts?)` | Send message to channel |
355
- | `.router()` | REST API |
356
411
  | `.close()` | Cleanup |
357
412
 
358
- ### opencode [C]
413
+ ### opencode [β]
359
414
 
360
415
  AI programming assistant.
361
416
 
@@ -367,14 +422,14 @@ const oc = await opencode({
367
422
  permissions: { bash: { allow: true }, write: { allow: false } },
368
423
  })
369
424
  await oc.migrate()
370
- app.use('/opencode', await oc.router())
425
+ app.use('/opencode', oc)
371
426
  app.ws('/opencode', oc.wsHandler())
372
427
  ```
373
428
 
374
429
  | Option | Type | Default | Description |
375
430
  |--------|------|---------|-------------|
376
431
  | `pg` | `object` | — | PostgreSQL client |
377
- | `model` | `object` | — | AI model |
432
+ | `model` | `string` | — | AI model name (e.g. `'gpt-4o'`, `'deepseek-v4-flash'`) |
378
433
  | `baseURL` | `string` | — | OpenAI-compatible API base URL |
379
434
  | `apiKey` | `string` | — | API key for the model |
380
435
  | `workspace` | `string` | — | Project directory |
@@ -382,7 +437,7 @@ app.ws('/opencode', oc.wsHandler())
382
437
  | `skills` | `object[]` | — | Custom skill definitions |
383
438
  | `permissions` | `object` | — | Tool permission rules |
384
439
 
385
- ### postgres [B]
440
+ ### postgres [α]
386
441
 
387
442
  ```ts
388
443
  const pg = postgres() // reads DATABASE_URL
@@ -413,7 +468,7 @@ await pg.transaction(async (tx) => { const users = pg.table('_users', { ... }).w
413
468
 
414
469
  Where helpers: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `isNull`, `isNotNull`, `like`, `contains`, `in_`, `and`, `or`, `not`.
415
470
 
416
- ### preferences [A]
471
+ ### preferences [α]
417
472
 
418
473
  Locale detection + theme + translations. `/__lang/:locale` and `/__theme/:theme` auto-routed.
419
474
 
@@ -444,7 +499,7 @@ const { theme, resolvedTheme, setTheme } = useTheme()
444
499
  // resolvedTheme resolves 'system' → 'dark'|'light' based on prefers-color-scheme
445
500
  ```
446
501
 
447
- ### queue [B]
502
+ ### queue [α]
448
503
 
449
504
  ```ts
450
505
  const q = queue({ redis })
@@ -467,7 +522,7 @@ await q.add('send-email', { to: 'user@test.com' }, { cron: '0 8 * * *' })
467
522
  | `.stop()` | Stop processing |
468
523
  | `.close()` | Cleanup |
469
524
 
470
- ### rateLimit [B]
525
+ ### rateLimit [α]
471
526
 
472
527
  ```ts
473
528
  app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min
@@ -484,7 +539,7 @@ app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' })
484
539
  | `key` | `(req) => string` | IP-based | Key function |
485
540
  | `message` | `string` | `'Too Many Requests'` | 429 response body |
486
541
 
487
- ### redis [B]
542
+ ### redis [α]
488
543
 
489
544
  ```ts
490
545
  const r = redis() // reads REDIS_URL
@@ -498,7 +553,7 @@ await ctx.redis.set('key', 'value')
498
553
  | `url` | `string` | `REDIS_URL` env | Redis connection string |
499
554
  | (all ioredis options) | — | — | Passed directly to ioredis |
500
555
 
501
- ### requestId [A]
556
+ ### requestId [α]
502
557
 
503
558
  ```ts
504
559
  app.use(requestId())
@@ -511,10 +566,106 @@ app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID()
511
566
  | `header` | `string` | `'X-Request-ID'` | Header name to read/write |
512
567
  | `generator` | `() => string` | `crypto.randomUUID()` | ID generator |
513
568
 
514
- ### seo [D] + seoMiddleware [A]
569
+ ---
570
+
571
+ ## React SSR (weifuwu/ssr)
572
+
573
+ Import from `'weifuwu/ssr'`:
515
574
 
516
575
  ```ts
517
- app.use(seo({ baseUrl: 'https://example.com', robots: [{ userAgent: '*', allow: '/' }], sitemap: { urls: [{ loc: '/' }] } }))
576
+ import { ssr, layout, liveReload, errorBoundary, notFound, tailwind } from 'weifuwu/ssr'
577
+ ```
578
+
579
+ ### ssr(path) [β]
580
+
581
+ Compiles a `.tsx` file and returns a Router handler that renders the React component to HTML with streaming, client bundle injection, and context serialization.
582
+
583
+ ```ts
584
+ app.get('/about', ssr('./pages/about.tsx'))
585
+ ```
586
+
587
+ - Compiles via esbuild at runtime (no build step)
588
+ - Reads `ctx.layoutStack` (set by `layout()` middleware) and wraps the component from outer to inner
589
+ - Injects hydration script pointing to the auto-generated client bundle at `/__ssr/[hash].js`
590
+ - Serializes middleware-injected `ctx` data to `window.__WEIFUWU_CTX` for client-side hydration
591
+ - Dev mode: injects live reload WebSocket script
592
+
593
+ ### layout(path) [β]
594
+
595
+ Compiles a `.tsx` file and returns middleware that pushes the layout component onto `ctx.layoutStack`. Pages rendered by `ssr()` consume this stack.
596
+
597
+ ```ts
598
+ app.use(layout('./layouts/root.tsx')) // outermost
599
+ app.use('/blog', layout('./layouts/blog.tsx')) // inner
600
+ ```
601
+
602
+ Layout components receive `{ children }` (the child page or nested layout). Multiple layouts wrap from outer to inner in `use()` order.
603
+
604
+ ```tsx
605
+ // layouts/root.tsx
606
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
607
+ return <html><head/><body><main>{children}</main></body></html>
608
+ }
609
+ ```
610
+
611
+ ### liveReload(opts) [β]
612
+
613
+ Returns a `Router` that registers a WebSocket endpoint at `/__weifuwu/livereload` and starts a file watcher on the given directories. When a `.tsx` file changes, it clears the compile cache and broadcasts a reload to all connected browsers.
614
+
615
+ ```ts
616
+ if (process.env.NODE_ENV !== 'production') {
617
+ app.use(liveReload({ dirs: ['./pages', './layouts'] }))
618
+ }
619
+ ```
620
+
621
+ Mount without a path — the internal `/__weifuwu/livereload` route is invisible to the user. The `ssr()` function automatically injects the client-side WS script in dev mode.
622
+
623
+ | Option | Type | Default | Description |
624
+ |--------|------|---------|-------------|
625
+ | `dirs` | `string[]` | — | Directories to watch for `.tsx` changes |
626
+
627
+ Returns `Router & { close: () => void }` — call `.close()` to stop the watcher.
628
+
629
+ ### errorBoundary(path) [β]
630
+
631
+ Wraps child routes in an error boundary. If a page or middleware throws, the error component is rendered instead.
632
+
633
+ ```ts
634
+ app.use('/blog', errorBoundary('./blog-error.tsx'))
635
+ ```
636
+
637
+ The error component receives `{ error, reset }` as props:
638
+
639
+ ```tsx
640
+ export default function BlogError({ error, reset }: { error: Error; reset: () => void }) {
641
+ return <div><h2>Error</h2><p>{error.message}</p></div>
642
+ }
643
+ ```
644
+
645
+ Error boundaries nest — the nearest one up the middleware chain catches the error.
646
+
647
+ ### notFound(path) [β]
648
+
649
+ Returns a catch-all handler for 404 pages. Typically registered last:
650
+
651
+ ```ts
652
+ app.all('/*', notFound('./not-found.tsx'))
653
+ ```
654
+
655
+ ### tailwind(path) [α]
656
+
657
+ Compiles Tailwind CSS v4 via `@tailwindcss/postcss` and serves it at `/__wfw/style.css`. In dev mode, watches the CSS file for changes.
658
+
659
+ ```ts
660
+ app.use(tailwind('./app.css'))
661
+ ```
662
+
663
+ When `tailwind()` middleware is detected, `ssr()` automatically injects `<link rel="stylesheet" href="/__wfw/style.css" />` into the HTML `<head>`.
664
+
665
+ ### seo [β] + seoMiddleware [α]
666
+
667
+ ```ts
668
+ app.use('/', seo({ baseUrl: 'https://example.com', robots: [{ userAgent: '*', allow: '/' }], sitemap: { urls: [{ loc: '/' }] } }))
518
669
  // GET /robots.txt, GET /sitemap.xml
519
670
 
520
671
  app.use(seoMiddleware({ headers: { 'X-Robots-Tag': (path) => path.startsWith('/admin') ? 'noindex' : undefined } }))
@@ -522,7 +673,14 @@ app.use(seoMiddleware({ headers: { 'X-Robots-Tag': (path) => path.startsWith('/a
522
673
 
523
674
  Also exports `seoTags(config)` for generating meta/og/twitter tags as an HTML string.
524
675
 
525
- ### tenant [C]
676
+ | Option | Type | Default | Description |
677
+ |--------|------|---------|-------------|
678
+ | `baseUrl` | `string` | — | Base URL for sitemap URLs |
679
+ | `robots` | `RobotsRule[]` | `[{ userAgent: '*', allow: '/' }]` | Robots.txt rules |
680
+ | `sitemap` | `SitemapConfig` | — | Sitemap configuration (urls, resolve, cacheTTL) |
681
+ | `headers` | `SeoHeadersConfig` | — | Response headers (e.g. `X-Robots-Tag`) |
682
+
683
+ ### tenant [β]
526
684
 
527
685
  Multi-tenant BaaS with dynamic table API and GraphQL.
528
686
 
@@ -530,11 +688,16 @@ Multi-tenant BaaS with dynamic table API and GraphQL.
530
688
  const t = tenant({ pg, usersTable: '_users' })
531
689
  await t.migrate()
532
690
  app.use('/api', t.middleware()) // → ctx.tenant
533
- app.use('/api', t.router()) // dynamic CRUD
691
+ app.use('/api', t) // dynamic CRUD
534
692
  app.use('/graphql', t.graphql()) // dynamic GraphQL
535
693
  ```
536
694
 
537
- ### upload [A]
695
+ | Option | Type | Default | Description |
696
+ |--------|------|---------|-------------|
697
+ | `pg` | `object` | — | PostgreSQL client |
698
+ | `usersTable` | `string` | — | Users table name for tenant membership lookup |
699
+
700
+ ### upload [α]
538
701
 
539
702
  ```ts
540
703
  app.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760, allowedTypes: ['image/jpeg', 'image/png'] }), (req, ctx) => {
@@ -550,14 +713,14 @@ app.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760, allowedT
550
713
  | `maxFileSize` | `number` | — | Max bytes per file |
551
714
  | `allowedTypes` | `string[]` | — | Allowed MIME types |
552
715
 
553
- ### user [C]
716
+ ### user [β]
554
717
 
555
718
  Authentication: register, login, JWT, OAuth2.
556
719
 
557
720
  ```ts
558
721
  const auth = user({ pg, jwtSecret: process.env.JWT_SECRET! })
559
722
  await auth.migrate()
560
- app.use('/auth', auth.router()) // POST /register, POST /login, OAuth2 routes
723
+ app.use('/auth', auth) // POST /register, POST /login, OAuth2 routes
561
724
  app.get('/me', auth.middleware(), (req, ctx) => Response.json(ctx.user))
562
725
  ```
563
726
 
@@ -574,10 +737,9 @@ app.get('/me', auth.middleware(), (req, ctx) => Response.json(ctx.user))
574
737
  | `.register(data)` | Register a new user programmatically |
575
738
  | `.login(data)` | Log in programmatically |
576
739
  | `.verify(token)` | Verify JWT token |
577
- | `.router()` | REST routes (register, login, OAuth2) |
578
740
  | `.middleware()` | JWT verify middleware — sets `ctx.user` |
579
741
 
580
- ### validate [A]
742
+ ### validate [α]
581
743
 
582
744
  ```ts
583
745
  import { z } from 'zod'
@@ -590,58 +752,6 @@ app.post('/users', validate({ body: CreateUser, query: z.object({ ref: z.string(
590
752
  })
591
753
  // Validation failure: returns 400 with { error: 'Validation failed', issues: [...] }
592
754
  ```
593
-
594
- ---
595
-
596
- ## React SSR (tsx)
597
-
598
- ```ts
599
- app.use('/', await tsx({ dir: './ui/' }))
600
- ```
601
-
602
- ```
603
- ui/
604
- ├── pages/
605
- │ ├── page.tsx → GET /
606
- │ ├── layout.tsx → root layout
607
- │ ├── not-found.tsx → 404
608
- │ ├── about/page.tsx → GET /about
609
- │ ├── blog/[slug]/
610
- │ │ ├── page.tsx → GET /blog/:slug
611
- │ │ ├── load.ts → server data fetching
612
- │ │ └── route.ts → API (named exports: POST, PUT...)
613
- │ ├── blog/layout.tsx → nested layout
614
- │ └── api/search/
615
- │ └── route.ts → GET /api/search
616
- └── components/
617
- ```
618
-
619
- ```tsx
620
- // page.tsx
621
- export default function Page() {
622
- const { t } = useLocale()
623
- const data = useLoaderData()
624
- return <h1>{t('title') ?? data.title}</h1>
625
- }
626
- ```
627
-
628
- ```tsx
629
- // layout.tsx
630
- export default function RootLayout({ children }: { children: React.ReactNode }) {
631
- return <html><head/><body><main>{children}</main></body></html>
632
- }
633
- ```
634
-
635
- ```ts
636
- // load.ts — server-only data fetching
637
- export default async function load({ params, query }) { return { data: await db.query(params.slug) } }
638
- ```
639
-
640
- ```ts
641
- // route.ts — API co-located with page
642
- export const POST: Handler = async (req, ctx) => Response.json({ slug: ctx.params.slug })
643
- ```
644
-
645
755
  ### Client-side navigation
646
756
 
647
757
  ```tsx
@@ -652,7 +762,7 @@ const navigate = useNavigate() // programmatic: navigate('/contact
652
762
  const loading = useNavigating() // reactive loading state
653
763
  ```
654
764
 
655
- `navigate()` fetches SSR, extracts `__weifuwu_root`, replaces in-place. `load.ts` runs on server each nav.
765
+ `navigate()` fetches SSR, extracts `__weifuwu_root`, replaces in-place. Middleware runs on server each nav — data is always fresh.
656
766
 
657
767
  **Preference URLs** (`/__lang/`, `/__theme/`) are intercepted by modular interceptors registered via `addInterceptor()` — no page reload needed. Importing `useLocale` or `useTheme` registers the interceptor automatically.
658
768
 
@@ -737,16 +847,18 @@ function ThemeToggle() {
737
847
 
738
848
  **`applyTheme(theme)`** — DOM-only theme application. Sets `data-theme` on `<html>`, registers `matchMedia` listener for `'system'`. Used by the interceptor; exported for custom scenarios.
739
849
 
740
- **`useLoaderData()`** — Returns the data returned by `load.ts`. Update-triggered; re-renders on SPA navigation.
850
+ **`useLoaderData()`** — Returns middleware-injected data from the request context. Works identically on server (SSR) and client (hydration/SPA). Re-renders on SPA navigation.
741
851
 
742
852
  ```tsx
743
853
  import { useLoaderData } from 'weifuwu/react'
744
854
  function Page() {
745
- const data = useLoaderData<{ post: { title: string } }>()
746
- return <h1>{data.post.title}</h1>
855
+ const data = useLoaderData<{ posts: Post[] }>()
856
+ return <ul>{data.posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
747
857
  }
748
858
  ```
749
859
 
860
+ On the server, data flows from middleware → `ctx` → `ctx.loaderData` (serialized). On the client, it's restored from `window.__WEIFUWU_CTX`. Under the hood, `useLoaderData()` uses `AsyncLocalStorage` on the server and `window.__WEIFUWU_CTX` on the client — no SSR-specific code needed in your components.
861
+
750
862
  **`addInterceptor(fn)`** — Register a URL interceptor. Interceptors run before SPA navigation; if one returns `true`, `navigate()` skips the fetch-and-swap.
751
863
 
752
864
  ```ts
@@ -778,7 +890,19 @@ function Toast() {
778
890
 
779
891
  ### Dev mode
780
892
 
781
- Auto-detected when `NODE_ENV !== 'production'`. File watching, live reload, Tailwind v4 auto-compile.
893
+ Auto-detected when `NODE_ENV !== 'production'`. File watching + live reload via `liveReload()`:
894
+
895
+ ```ts
896
+ import { liveReload } from 'weifuwu/ssr'
897
+
898
+ if (process.env.NODE_ENV !== 'production') {
899
+ app.use(liveReload({ dirs: ['./pages', './layouts'] }))
900
+ }
901
+ ```
902
+
903
+ When a `.tsx` file changes, `ssr()` clears its compile cache and the browser auto-refreshes. No process restart needed.
904
+
905
+ Tailwind v4 auto-compile via `tailwind()` middleware:
782
906
 
783
907
  ---
784
908
 
@@ -789,21 +913,7 @@ import { openai, streamText, generateText, streamObject, generateObject, tool, e
789
913
  import { runWorkflow } from 'weifuwu'
790
914
  ```
791
915
 
792
- ### Streaming
793
-
794
- ```ts
795
- const chat = await aiStream(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages }))
796
- app.use('/chat', chat.router().handler())
797
- ```
798
-
799
- ### Agents
800
-
801
- ```ts
802
- const agents = agent({ pg })
803
- await agents.migrate()
804
- app.use('/api', agents.router())
805
- await agents.addKnowledge(agentId, 'Title', 'content')
806
- ```
916
+ For AI streaming endpoints see [`aiStream`](#aistream-β). For AI agent APIs see [`agent`](#agent-β).
807
917
 
808
918
  ### DAG Workflow
809
919