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 +232 -122
- package/cli.ts +7 -6
- package/dist/agent/types.d.ts +2 -2
- package/dist/ai.d.ts +1 -3
- package/dist/analytics.d.ts +1 -2
- package/dist/cli.js +7 -6
- package/dist/graphql.d.ts +1 -3
- package/dist/iii/types.d.ts +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +766 -1244
- package/dist/logdb/types.d.ts +1 -2
- package/dist/messager/types.d.ts +2 -2
- package/dist/opencode/types.d.ts +1 -2
- package/dist/router.d.ts +1 -0
- package/dist/ssr/compile.d.ts +2 -0
- package/dist/ssr/error-boundary.d.ts +2 -0
- package/dist/ssr/index.d.ts +7 -0
- package/dist/ssr/index.js +936 -0
- package/dist/ssr/layout.d.ts +2 -0
- package/dist/ssr/live.d.ts +6 -0
- package/dist/ssr/not-found.d.ts +2 -0
- package/dist/ssr/ssr.d.ts +3 -0
- package/dist/ssr/stream.d.ts +14 -0
- package/dist/ssr/tailwind.d.ts +2 -0
- package/dist/tenant/types.d.ts +2 -2
- package/dist/types.d.ts +5 -0
- package/dist/user/types.d.ts +1 -2
- package/package.json +4 -3
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,
|
|
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('/
|
|
22
|
-
|
|
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
|
|
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
|
-
| `[
|
|
96
|
-
| `[
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 [
|
|
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
|
|
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
|
-
| `.
|
|
158
|
+
| `.migrate()` | DB setup |
|
|
128
159
|
| `.close()` | Cleanup |
|
|
129
160
|
|
|
130
|
-
### aiStream [
|
|
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
|
|
167
|
+
app.use('/chat', chat)
|
|
135
168
|
```
|
|
136
169
|
|
|
137
|
-
|
|
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
|
|
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
|
|
194
|
+
app.use('/', a) // dashboard routes
|
|
158
195
|
```
|
|
159
196
|
|
|
160
|
-
### auth [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
274
|
+
### health [β]
|
|
238
275
|
|
|
239
276
|
```ts
|
|
240
|
-
app.use(
|
|
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 [
|
|
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 [
|
|
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
|
|
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 [
|
|
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
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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',
|
|
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` | `
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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
|
-
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## React SSR (weifuwu/ssr)
|
|
572
|
+
|
|
573
|
+
Import from `'weifuwu/ssr'`:
|
|
515
574
|
|
|
516
575
|
```ts
|
|
517
|
-
|
|
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
|
-
|
|
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
|
|
691
|
+
app.use('/api', t) // dynamic CRUD
|
|
534
692
|
app.use('/graphql', t.graphql()) // dynamic GraphQL
|
|
535
693
|
```
|
|
536
694
|
|
|
537
|
-
|
|
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 [
|
|
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
|
|
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 [
|
|
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.
|
|
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
|
|
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<{
|
|
746
|
-
return <
|
|
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
|
|
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
|
-
|
|
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
|
|