weifuwu 0.19.9 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +280 -98
- package/cli/template/app.ts +3 -8
- package/cli/template/index.ts +1 -3
- package/cli/template/ui/{page.tsx → app/page.tsx} +2 -2
- package/cli.ts +197 -40
- package/dist/agent/rest.d.ts +1 -0
- package/dist/agent/run.d.ts +1 -0
- package/dist/auth.d.ts +8 -0
- package/dist/cli.js +170 -30
- package/dist/client-router.d.ts +1 -1
- package/dist/cors.d.ts +10 -0
- package/dist/csrf.d.ts +4 -2
- package/dist/env.d.ts +6 -1
- package/dist/graphql.d.ts +4 -0
- package/dist/html-shell.d.ts +1 -0
- package/dist/index.d.ts +17 -17
- package/dist/index.js +1898 -2243
- package/dist/live.d.ts +4 -1
- package/dist/logger.d.ts +16 -0
- package/dist/messager/agent.d.ts +6 -1
- package/dist/postgres/client.d.ts +2 -0
- package/dist/postgres/index.d.ts +2 -2
- package/dist/postgres/module.d.ts +5 -3
- package/dist/postgres/schema/table.d.ts +8 -2
- package/dist/postgres/types.d.ts +31 -8
- package/dist/preferences.d.ts +6 -2
- package/dist/queue/types.d.ts +14 -3
- package/dist/rate-limit.d.ts +2 -2
- package/dist/react.d.ts +2 -0
- package/dist/react.js +57 -0
- package/dist/redis/types.d.ts +5 -3
- package/dist/request-id.d.ts +4 -2
- package/dist/router.d.ts +33 -19
- package/dist/serve.d.ts +13 -2
- package/dist/ssr.d.ts +20 -1
- package/dist/stream.d.ts +0 -1
- package/dist/tailwind.d.ts +3 -1
- package/dist/test-utils.d.ts +64 -0
- package/dist/trace.d.ts +15 -0
- package/dist/types.d.ts +3 -3
- package/dist/use-agent-stream.d.ts +27 -0
- package/dist/user/index.d.ts +1 -1
- package/dist/user/types.d.ts +5 -2
- package/package.json +3 -3
- package/cli/template/.weifuwu/ssr/96f5704e.js +0 -481
- package/cli/template/.weifuwu/ssr/fae6ecbe.js +0 -14
- package/dist/agent/migrate.d.ts +0 -6
- package/dist/cms/admin.d.ts +0 -3
- package/dist/cms/api.d.ts +0 -3
- package/dist/cms/client.d.ts +0 -2
- package/dist/cms/content.d.ts +0 -36
- package/dist/cms/index.d.ts +0 -2
- package/dist/cms/media.d.ts +0 -17
- package/dist/cms/types.d.ts +0 -93
- package/dist/dist/agent/client.d.ts +0 -2
- package/dist/dist/agent/index.d.ts +0 -2
- package/dist/dist/agent/migrate.d.ts +0 -6
- package/dist/dist/agent/rest.d.ts +0 -13
- package/dist/dist/agent/run.d.ts +0 -17
- package/dist/dist/agent/types.d.ts +0 -51
- package/dist/dist/ai/workflow.d.ts +0 -14
- package/dist/dist/analytics.d.ts +0 -15
- package/dist/dist/client-locale.d.ts +0 -5
- package/dist/dist/client-pref.d.ts +0 -3
- package/dist/dist/client-state.d.ts +0 -22
- package/dist/dist/client-theme.d.ts +0 -7
- package/dist/dist/compress.d.ts +0 -6
- package/dist/dist/cookie.d.ts +0 -12
- package/dist/dist/deploy/config.d.ts +0 -2
- package/dist/dist/deploy/gateway.d.ts +0 -2
- package/dist/dist/deploy/index.d.ts +0 -4
- package/dist/dist/deploy/manager.d.ts +0 -16
- package/dist/dist/deploy/process.d.ts +0 -14
- package/dist/dist/deploy/types.d.ts +0 -62
- package/dist/dist/head.d.ts +0 -6
- package/dist/dist/helmet.d.ts +0 -18
- package/dist/dist/iii/client.d.ts +0 -2
- package/dist/dist/iii/index.d.ts +0 -4
- package/dist/dist/iii/register-worker.d.ts +0 -10
- package/dist/dist/iii/rest.d.ts +0 -3
- package/dist/dist/iii/stream.d.ts +0 -82
- package/dist/dist/iii/types.d.ts +0 -133
- package/dist/dist/iii/worker.d.ts +0 -2
- package/dist/dist/iii/ws.d.ts +0 -29
- package/dist/dist/index.js +0 -8180
- package/dist/dist/messager/agent.d.ts +0 -6
- package/dist/dist/messager/client.d.ts +0 -2
- package/dist/dist/messager/index.d.ts +0 -2
- package/dist/dist/messager/migrate.d.ts +0 -2
- package/dist/dist/messager/rest.d.ts +0 -15
- package/dist/dist/messager/types.d.ts +0 -56
- package/dist/dist/messager/ws.d.ts +0 -14
- package/dist/dist/preferences.d.ts +0 -14
- package/dist/dist/react.d.ts +0 -12
- package/dist/dist/react.js +0 -637
- package/dist/dist/request-id.d.ts +0 -6
- package/dist/dist/seo.d.ts +0 -39
- package/dist/dist/ssr/compile.d.ts +0 -2
- package/dist/dist/ssr/error-boundary.d.ts +0 -2
- package/dist/dist/ssr/index.d.ts +0 -7
- package/dist/dist/ssr/index.js +0 -933
- package/dist/dist/ssr/layout.d.ts +0 -2
- package/dist/dist/ssr/live.d.ts +0 -6
- package/dist/dist/ssr/not-found.d.ts +0 -2
- package/dist/dist/ssr/ssr.d.ts +0 -2
- package/dist/dist/ssr/stream.d.ts +0 -14
- package/dist/dist/ssr/tailwind.d.ts +0 -2
- package/dist/dist/tenant/client.d.ts +0 -2
- package/dist/dist/tenant/graphql.d.ts +0 -3
- package/dist/dist/tenant/index.d.ts +0 -2
- package/dist/dist/tenant/migrate.d.ts +0 -6
- package/dist/dist/tenant/rest.d.ts +0 -3
- package/dist/dist/tenant/schema.d.ts +0 -5
- package/dist/dist/tenant/types.d.ts +0 -48
- package/dist/dist/tenant/utils.d.ts +0 -10
- package/dist/dist/types.d.ts +0 -19
- package/dist/dist/use-flash-message.d.ts +0 -1
- package/dist/i18n.d.ts +0 -6
- package/dist/logdb/migrate.d.ts +0 -5
- package/dist/messager/migrate.d.ts +0 -2
- package/dist/middleware.d.ts +0 -21
- package/dist/opencode/migrate.d.ts +0 -2
- package/dist/postgres/migrate.d.ts +0 -3
- package/dist/postgres/table.d.ts +0 -4
- package/dist/root-layout.d.ts +0 -4
- package/dist/tenant/migrate.d.ts +0 -6
- package/dist/tsx-instance.d.ts +0 -43
- package/dist/tsx.d.ts +0 -8
- package/dist/user/migrate.d.ts +0 -6
- package/dist/workflow/engine.d.ts +0 -7
- package/dist/workflow/index.d.ts +0 -8
- package/dist/workflow/llm.d.ts +0 -10
- package/dist/workflow/nodes.d.ts +0 -10
- package/dist/workflow/reference.d.ts +0 -3
- package/dist/workflow/route.d.ts +0 -11
- package/dist/workflow/sse.d.ts +0 -2
- package/dist/workflow/tool.d.ts +0 -8
- package/dist/workflow/types.d.ts +0 -86
- /package/cli/template/ui/{app.css → app/globals.css} +0 -0
- /package/cli/template/ui/{layout.tsx → app/layout.tsx} +0 -0
- /package/opencode/ui/{app.css → app/globals.css} +0 -0
- /package/opencode/ui/{layout.tsx → app/layout.tsx} +0 -0
- /package/opencode/ui/{page.tsx → app/page.tsx} +0 -0
package/README.md
CHANGED
|
@@ -15,11 +15,9 @@ serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import { serve, Router,
|
|
18
|
+
import { serve, Router, ssr } from 'weifuwu'
|
|
19
19
|
const app = new Router()
|
|
20
|
-
app.use(
|
|
21
|
-
app.use(rootLayout('./ui'))
|
|
22
|
-
app.get('/', ssr('./ui/pages/home.tsx'))
|
|
20
|
+
app.use('/', ssr({ dir: './ui' }))
|
|
23
21
|
serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
|
|
24
22
|
```
|
|
25
23
|
|
|
@@ -27,6 +25,17 @@ serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
|
|
|
27
25
|
npx weifuwu init my-app && cd my-app && npm run dev
|
|
28
26
|
```
|
|
29
27
|
|
|
28
|
+
## CLI
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx weifuwu init my-app # Full project (SSR + i18n + theme + WS demo)
|
|
32
|
+
npx weifuwu init my-api --minimal # Minimal HTTP project (2 files)
|
|
33
|
+
npx weifuwu init my-api --skip-install # Skip npm install
|
|
34
|
+
npx weifuwu dev # Start dev server (auto-detect index.ts)
|
|
35
|
+
npx weifuwu generate module my-mod # Scaffold middleware module + test
|
|
36
|
+
npx weifuwu version # Print version
|
|
37
|
+
```
|
|
38
|
+
|
|
30
39
|
---
|
|
31
40
|
|
|
32
41
|
## Core Concepts
|
|
@@ -44,11 +53,14 @@ await server.ready
|
|
|
44
53
|
| `hostname` | `string` | `'0.0.0.0'` | Listen address |
|
|
45
54
|
| `signal` | `AbortSignal` | — | Shutdown on abort |
|
|
46
55
|
| `websocket` | `WsUpgradeHandler` | — | WebSocket upgrade handler |
|
|
47
|
-
| `maxBodySize` | `number` |
|
|
56
|
+
| `maxBodySize` | `number` | `10MB` | Max body bytes (0 = unlimited) |
|
|
57
|
+
| `timeout` | `number` | `30_000` | Socket inactivity timeout (ms) |
|
|
58
|
+
| `keepAliveTimeout` | `number` | `5_000` | Keep-Alive idle timeout (ms) |
|
|
59
|
+
| `headersTimeout` | `number` | `6_000` | Headers read timeout (ms) |
|
|
48
60
|
| `shutdown` | `boolean` | `true` | Auto SIGTERM/SIGINT |
|
|
49
61
|
|
|
50
62
|
```ts
|
|
51
|
-
interface Server { stop: () => void
|
|
63
|
+
interface Server { stop: () => Promise<void>; readonly port: number; readonly hostname: string; ready: Promise<void> }
|
|
52
64
|
const { server, url } = await createTestServer(handler)
|
|
53
65
|
```
|
|
54
66
|
|
|
@@ -60,9 +72,24 @@ app.get('/hello/:name', (req, ctx) => Response.json({ message: `Hello, ${ctx.par
|
|
|
60
72
|
app.post('/data', async (req, ctx) => { const body = await req.json(); return Response.json(body, { status: 201 }) })
|
|
61
73
|
app.use('/admin', authMW) // path-scoped middleware
|
|
62
74
|
app.use('/admin', adminRouter) // sub-router (flattened into parent trie)
|
|
63
|
-
app.ws('/echo', {
|
|
75
|
+
app.ws('/echo', {
|
|
76
|
+
open(ws, ctx) { ctx.ws.json({ type: 'connected' }) },
|
|
77
|
+
message(ws, ctx, data) { ctx.ws.json({ echo: data.toString() }) },
|
|
78
|
+
})
|
|
79
|
+
app.ws('/chat', {
|
|
80
|
+
open(ws, ctx) { ctx.ws.join('room') },
|
|
81
|
+
message(ws, ctx, data) { ctx.ws.sendRoom('room', JSON.parse(data.toString())) },
|
|
82
|
+
})
|
|
64
83
|
app.onError((err, req, ctx) => Response.json({ error: err.message }, { status: 500 }))
|
|
65
84
|
|
|
85
|
+
// Debug: list all registered routes
|
|
86
|
+
console.log(app.routes())
|
|
87
|
+
// [ 'GET /hello/:name', 'POST /data', 'WS /echo', 'WS /chat' ]
|
|
88
|
+
|
|
89
|
+
// Cross-process WebSocket broadcast (Redis)
|
|
90
|
+
import { createHub } from 'weifuwu'
|
|
91
|
+
app.wsHub(createHub({ redis: redis() }))
|
|
92
|
+
|
|
66
93
|
const handler = app.handler()
|
|
67
94
|
const wsHandler = app.websocketHandler()
|
|
68
95
|
serve(handler, { port: 3000, websocket: wsHandler })
|
|
@@ -119,14 +146,33 @@ The `ctx` object accumulates properties as it passes through the middleware chai
|
|
|
119
146
|
| `queue` | `queue()` | `Queue` | Job queue |
|
|
120
147
|
| `prefs` | `preferences()` | `{ locale, theme }` | User preferences (locale, theme) |
|
|
121
148
|
| `deploy` | `deploy()` | `{ appName? }` | Deploy gateway info |
|
|
122
|
-
| `layoutStack` | `
|
|
149
|
+
| `layoutStack` | `ssr()` internal | `LayoutEntry[]` | React layout component stack |
|
|
123
150
|
| `loaderData` | User middleware | `Record<string, unknown>` | SSR data passed to client |
|
|
124
151
|
| `user` | `auth()` / `user().middleware()` | `{ id?: string }` | Authenticated user |
|
|
125
152
|
| `parsed` | `validate()` / `upload()` | `{ body, query, params, headers, files }` | Validated/parsed request data |
|
|
126
153
|
| `t` | `preferences()` | `(key) => string` | Translation function |
|
|
127
154
|
| `setPref` | `preferences()` | `(key, val) => Response` | Set preference cookie + redirect |
|
|
128
|
-
| `compiledTailwindCss` | `
|
|
129
|
-
| `tailwindCssUrl` | `
|
|
155
|
+
| `compiledTailwindCss` | `ssr()` internal | `string` | Compiled CSS content (internal) |
|
|
156
|
+
| `tailwindCssUrl` | `ssr()` internal | `string` | Compiled CSS route URL (internal) |
|
|
157
|
+
|
|
158
|
+
### Type-Safe Context
|
|
159
|
+
|
|
160
|
+
Middleware-injected properties are **automatically typed** through chained `use()` calls:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const app = new Router()
|
|
164
|
+
.use(csrf()) // → Router<Context & { csrfToken: string }>
|
|
165
|
+
.use(requestId()) // → Router<Context & { csrfToken, requestId }>
|
|
166
|
+
.use(postgres()) // → Router<Context & { csrfToken, requestId, sql }>
|
|
167
|
+
|
|
168
|
+
app.get('/me', (_req, ctx) => {
|
|
169
|
+
ctx.csrfToken // ✅ string (IDE autocomplete)
|
|
170
|
+
ctx.requestId // ✅ string
|
|
171
|
+
ctx.sql`SELECT 1` // ✅ Sql<{}>
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Each module exports an `XxxInjected` type (e.g. `PostgresInjected`, `UserInjected`) for composing custom context types. `Context` is an interface — modules augment it via `declare module` for ambient compatibility.
|
|
130
176
|
|
|
131
177
|
---
|
|
132
178
|
|
|
@@ -137,7 +183,7 @@ All modules follow one of **2 patterns** — learn these and you know every modu
|
|
|
137
183
|
| Pattern | How to mount | Example |
|
|
138
184
|
|---------|-------------|---------|
|
|
139
185
|
| `[α]` | `app.use(mod())` | `compress()`, `preferences()`, `postgres()` |
|
|
140
|
-
| `[β]` | `app.use('/path', mod())` | `health()`, `graphql(handler)`, `user()` |
|
|
186
|
+
| `[β]` | `app.use('/path', mod())` | `health()`, `ssr({dir})`, `graphql(handler)`, `user()` |
|
|
141
187
|
|
|
142
188
|
### Pattern α — Middleware
|
|
143
189
|
|
|
@@ -167,6 +213,98 @@ app.use('/', a) // dashboard
|
|
|
167
213
|
|
|
168
214
|
---
|
|
169
215
|
|
|
216
|
+
## Request Tracing & Logging
|
|
217
|
+
|
|
218
|
+
Every request gets a **trace ID** via `AsyncLocalStorage`, injected into responses as `X-Trace-Id`. W3C `traceparent` headers are forwarded.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { currentTraceId } from 'weifuwu'
|
|
222
|
+
|
|
223
|
+
app.get('/api', (req, ctx) => {
|
|
224
|
+
console.log('Handling request', currentTraceId()) // f240a3f3-60e2-...
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Structured logging** — `logger({ format: 'json' })` outputs JSON to stderr with `traceId`, `timestamp`, `elapsed_ms`:
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{"level":"info","message":"request","method":"GET","path":"/api/users","status":200,"elapsed_ms":42,"traceId":"f240a3f3-...","timestamp":"2025-01-15T10:30:00.000Z"}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Default format is `'short'` (human-readable). `'combined'` includes query strings.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## AI Observability
|
|
239
|
+
|
|
240
|
+
Agent runs are **automatically logged** to `_agent_runs`. Dashboard endpoints provide analytics:
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
GET /agents/:id/runs?days=7 → [{ input, output, tokens_in, tokens_out, elapsed_ms, status, trace_id, ... }]
|
|
244
|
+
GET /agents/:id/runs/summary?days=7 → { total, success, error, success_rate, tokens_in, tokens_out, avg_elapsed_ms, p95_elapsed_ms }
|
|
245
|
+
GET /opencode/sessions/:id/usage → { message_count, tokens_in, tokens_out, tokens_total }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Non-streaming runs log full token data; streaming runs log `status: 'stream'`.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Agent ↔ Messager Streaming
|
|
253
|
+
|
|
254
|
+
Agent replies in messager channels now stream **token-by-token** via WebSocket:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
// Backend — automatic when agents are attached to messager
|
|
258
|
+
const msg = messager({ pg, agents: agent({ pg, model }) })
|
|
259
|
+
app.ws('/ws', msg.wsHandler())
|
|
260
|
+
// Agent replies stream to: hub.broadcast({ type: 'agent_stream', data: { token, full } })
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
// Frontend — React hook
|
|
265
|
+
import { useAgentStream } from 'weifuwu/react'
|
|
266
|
+
|
|
267
|
+
const { getAgentText, isAgentStreaming, stream } = useAgentStream({
|
|
268
|
+
wsPath: '/ws',
|
|
269
|
+
channelId: 1,
|
|
270
|
+
})
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Multi-round conversation context: the last 10 channel messages are automatically injected into agent calls.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Test Utilities
|
|
278
|
+
|
|
279
|
+
Chainable test helper for HTTP-level testing without starting a server:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { testApp } from 'weifuwu'
|
|
283
|
+
|
|
284
|
+
const app = testApp()
|
|
285
|
+
app.use(postgres({ connection: TEST_DB }))
|
|
286
|
+
app.get('/users/:id', (req, ctx) => Response.json({ id: ctx.params.id, user: ctx.user }))
|
|
287
|
+
|
|
288
|
+
const res = await app
|
|
289
|
+
.getReq('/users/42?name=Alice')
|
|
290
|
+
.withUser({ id: 1 })
|
|
291
|
+
.header('X-Custom', 'val')
|
|
292
|
+
.body({ data: 'test' })
|
|
293
|
+
.send()
|
|
294
|
+
|
|
295
|
+
assert.equal(res.status, 200)
|
|
296
|
+
assert.deepEqual(await res.json(), { id: '42', user: { id: 1 } })
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
| Method | Description |
|
|
300
|
+
|--------|-------------|
|
|
301
|
+
| `app.getReq(path)` `postReq` `putReq` `patchReq` `deleteReq` | Start building a request |
|
|
302
|
+
| `.withUser(u)` `.withTenant(t)` `.with(ctx)` | Simulate middleware injection |
|
|
303
|
+
| `.header(k,v)` `.body(data)` `.rawBody(str)` | Set request properties |
|
|
304
|
+
| `.send()` → `TestResponse` | Execute and get `{ status, headers, json(), text() }` |
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
170
308
|
## Module Reference
|
|
171
309
|
|
|
172
310
|
### agent [β]
|
|
@@ -398,23 +536,30 @@ Restart=always
|
|
|
398
536
|
| `buildCommand` | — | Build command |
|
|
399
537
|
| `ports` | — | `[port, port+1]` for blue-green |
|
|
400
538
|
|
|
401
|
-
### errorBoundary(path) [β]
|
|
402
539
|
|
|
403
|
-
Wraps child routes in an error boundary. If a page or middleware throws, the error component is rendered via SSR with head injection (CSS, theme, context) and layout wrapping.
|
|
404
|
-
|
|
405
|
-
```ts
|
|
406
|
-
app.use('/blog', errorBoundary('./blog-error.tsx'))
|
|
407
|
-
```
|
|
408
540
|
|
|
409
|
-
|
|
541
|
+
### graphql [β]
|
|
410
542
|
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
543
|
+
```ts
|
|
544
|
+
const handler: GraphQLHandler = () => ({
|
|
545
|
+
schema: `type Query { hello: String }`,
|
|
546
|
+
resolvers: { Query: { hello: () => 'world' } },
|
|
547
|
+
graphiql: true, // GET / returns GraphiQL IDE
|
|
548
|
+
maxDepth: 10, // max query nesting (default 10, 0 = disable)
|
|
549
|
+
timeout: 30_000, // execution timeout in ms
|
|
550
|
+
})
|
|
551
|
+
app.use('/graphql', graphql(handler))
|
|
415
552
|
```
|
|
416
553
|
|
|
417
|
-
|
|
554
|
+
| Option | Type | Default | Description |
|
|
555
|
+
|--------|------|---------|-------------|
|
|
556
|
+
| `schema` | `string \| GraphQLSchema` | — | SDL string or pre-built schema |
|
|
557
|
+
| `resolvers` | `object` | — | Resolver map |
|
|
558
|
+
| `rootValue` | `any` | — | Root value for queries |
|
|
559
|
+
| `context` | `(req, ctx) => object` | — | Per-request context factory |
|
|
560
|
+
| `graphiql` | `boolean` | `false` | Serve GraphiQL IDE at GET / |
|
|
561
|
+
| `maxDepth` | `number` | `10` | Max query nesting depth |
|
|
562
|
+
| `timeout` | `number` | `30_000` | Execution timeout (ms) |
|
|
418
563
|
|
|
419
564
|
### health [β]
|
|
420
565
|
|
|
@@ -483,16 +628,7 @@ await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'
|
|
|
483
628
|
| `.migrate()` | DB setup |
|
|
484
629
|
| `.shutdown()` | Clean shutdown |
|
|
485
630
|
|
|
486
|
-
### layout(path) [β]
|
|
487
|
-
|
|
488
|
-
Compiles a `.tsx` file and returns middleware that pushes the layout component onto `ctx.layoutStack`. Pages rendered by `ssr()` consume this stack. Use after `rootLayout()` to extend a shared structure.
|
|
489
|
-
|
|
490
|
-
```ts
|
|
491
|
-
app.use(rootLayout('./ui'))
|
|
492
|
-
app.use(layout('./extra.tsx')) // appended after rootLayout
|
|
493
|
-
```
|
|
494
631
|
|
|
495
|
-
Layout components receive `{ children }` (the child page or nested layout). Multiple layouts wrap from outer to inner in `use()` order.
|
|
496
632
|
|
|
497
633
|
### logdb [β]
|
|
498
634
|
|
|
@@ -566,17 +702,7 @@ await msg.send(channelId, 'System message', { sender_type: 'system', sender_id:
|
|
|
566
702
|
| `.send(channel, content, opts?)` | Send message to channel |
|
|
567
703
|
| `.close()` | Cleanup |
|
|
568
704
|
|
|
569
|
-
### notFound(path) [β]
|
|
570
|
-
|
|
571
|
-
Returns a catch-all handler for 404 pages. When a path is given, the component is rendered via SSR with layout support. Falls back to plain text if compilation fails or no path given.
|
|
572
705
|
|
|
573
|
-
```ts
|
|
574
|
-
// No path — plain text
|
|
575
|
-
app.all('/*', notFound())
|
|
576
|
-
|
|
577
|
-
// Path to a .tsx component — renders via SSR
|
|
578
|
-
app.all('/*', notFound('./not-found.tsx'))
|
|
579
|
-
```
|
|
580
706
|
|
|
581
707
|
### opencode [β]
|
|
582
708
|
|
|
@@ -621,52 +747,70 @@ app.use(pg) // injects ctx.sql
|
|
|
621
747
|
| `ssl` | `boolean\|object` | — | SSL options |
|
|
622
748
|
| `idle_timeout` | `number` | `30` | Idle timeout (seconds) |
|
|
623
749
|
| `connect_timeout` | `number` | `30` | Connection timeout |
|
|
750
|
+
| `statementTimeout` | `number` | `30_000` | Per-statement timeout (ms, 0 = disable) |
|
|
751
|
+
| `onQuery` | `(query, ms, rows) => void` | — | Query logging callback |
|
|
624
752
|
|
|
625
753
|
```ts
|
|
626
754
|
// Raw SQL via tagged template
|
|
627
755
|
await pg.sql`SELECT * FROM users WHERE email = ${email}`
|
|
628
756
|
|
|
629
|
-
//
|
|
630
|
-
import {
|
|
757
|
+
// Define a table — one API, sql pre-bound
|
|
758
|
+
import { serial, text, boolean, timestamps } from 'weifuwu'
|
|
631
759
|
|
|
632
|
-
const users =
|
|
760
|
+
const users = pg.table('_users', {
|
|
633
761
|
id: serial('id').primaryKey(),
|
|
634
762
|
name: text('name').notNull(),
|
|
635
763
|
email: text('email').unique().notNull(),
|
|
636
764
|
active: boolean('active').default(true),
|
|
637
765
|
...timestamps(),
|
|
638
766
|
})
|
|
639
|
-
await users.create(
|
|
640
|
-
await users.createIndex(
|
|
641
|
-
|
|
642
|
-
//
|
|
643
|
-
|
|
644
|
-
await
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
767
|
+
await users.create() // DDL — no need to pass sql
|
|
768
|
+
await users.createIndex('email')
|
|
769
|
+
|
|
770
|
+
// CRUD — sql already bound
|
|
771
|
+
await users.insert({ name: 'Alice' })
|
|
772
|
+
const { count, data } = await users.readMany({ role: 'admin' }, { orderBy: { name: 'asc' }, limit: 10 })
|
|
773
|
+
await users.upsert({ email: 'alice@test.com' }, 'email')
|
|
774
|
+
|
|
775
|
+
// Reuse schema without redefining fields
|
|
776
|
+
import { pgTable } from 'weifuwu'
|
|
777
|
+
const usersSchema = pgTable('_users', { id: serial('id'), name: text('name') }) // define once
|
|
778
|
+
const users = pg.table(usersSchema) // bind — no field duplication
|
|
779
|
+
|
|
780
|
+
// Transactions — with auto-retry on deadlock/serialization failure
|
|
652
781
|
await pg.transaction(async (sql) => {
|
|
653
|
-
const
|
|
654
|
-
return
|
|
655
|
-
})
|
|
782
|
+
const txUsers = users.withSql(sql)
|
|
783
|
+
return txUsers.insert({ name: 'Bob' })
|
|
784
|
+
}, { maxRetries: 3 })
|
|
656
785
|
|
|
657
786
|
// Soft delete — automatic if deleted_at column exists
|
|
658
|
-
await
|
|
659
|
-
await
|
|
660
|
-
await
|
|
787
|
+
await users.delete(1) // SET deleted_at = NOW()
|
|
788
|
+
await users.hardDelete(1) // DELETE FROM
|
|
789
|
+
await users.read(1) // auto-filters deleted_at IS NULL (use withDeleted: true to include)
|
|
661
790
|
|
|
662
791
|
// JSONB queries
|
|
663
792
|
const logs = pg.table('logs', { meta: jsonb<{ service: string }>('meta') })
|
|
664
793
|
await logs.readMany(contains('meta', { service: 'auth' }))
|
|
665
794
|
|
|
795
|
+
// Connection pool visibility
|
|
796
|
+
console.log(pg.poolStats()) // { active: 3, idle: 7, waiting: 0, max: 10 }
|
|
797
|
+
|
|
798
|
+
// Migration tracking
|
|
799
|
+
await pg.migrate() // creates _weifuwu_migrations
|
|
800
|
+
await pg.markMigrated('myModule') // idempotent
|
|
801
|
+
const done = await pg.isMigrated('myModule')
|
|
802
|
+
|
|
666
803
|
// Partitioned tables
|
|
667
804
|
await logs.create({ partitionBy: partitionBy('range', 'created_at') })
|
|
668
805
|
```
|
|
669
806
|
|
|
807
|
+
**When to use pgTable vs pg.table:**
|
|
808
|
+
| API | Use when |
|
|
809
|
+
|-----|---------|
|
|
810
|
+
| `pg.table('t', cols)` | You have `pg` available (factory, handler, migrate) |
|
|
811
|
+
| `pg.table(schema)` | Reusing a schema without duplicating field definitions |
|
|
812
|
+
| `pgTable('t', cols)` | No `pg` reference (utility modules, standalone schema files) |
|
|
813
|
+
|
|
670
814
|
| Column builder | Type | Notes |
|
|
671
815
|
|---------------|------|-------|
|
|
672
816
|
| `serial(name)` | `number` | Auto-increment |
|
|
@@ -681,27 +825,27 @@ await logs.create({ partitionBy: partitionBy('range', 'created_at') })
|
|
|
681
825
|
|
|
682
826
|
**Column modifiers:** `.primaryKey()`, `.notNull()`, `.nullable()`, `.default(val)`, `.unique()`, `.references(table, column?, onDelete?)`.
|
|
683
827
|
|
|
684
|
-
**
|
|
828
|
+
**CRUD methods:**
|
|
685
829
|
|
|
686
830
|
| Method | Description |
|
|
687
831
|
|--------|-------------|
|
|
688
832
|
| `insert(data)` | INSERT + RETURNING \*, returns the inserted row |
|
|
689
833
|
| `insertMany(data)` | Bulk INSERT + RETURNING \*, returns rows |
|
|
690
|
-
| `read(id, opts?)` | SELECT by primary key
|
|
834
|
+
| `read(id, opts?)` | SELECT by detected primary key + auto soft-delete filter |
|
|
691
835
|
| `readMany(where?, opts?)` | Filtered query with `{ count, data }` — auto-filters soft-deleted |
|
|
692
|
-
| `update(id, data)` | UPDATE by
|
|
836
|
+
| `update(id, data)` | UPDATE by primary key + RETURNING \*, returns updated row |
|
|
693
837
|
| `updateMany(where, data)` | Bulk UPDATE, returns affected row count |
|
|
694
838
|
| `delete(id)` | Soft delete if `deleted_at` exists, else hard delete |
|
|
695
839
|
| `hardDelete(id)` | Always DELETE FROM |
|
|
696
840
|
| `deleteMany(where)` | Soft bulk delete if `deleted_at` exists |
|
|
697
841
|
| `hardDeleteMany(where)` | Always DELETE FROM |
|
|
698
842
|
| `upsert(data, conflict)` | INSERT ON CONFLICT DO UPDATE, returns row |
|
|
699
|
-
| `count(where?)` | SELECT COUNT(\*) |
|
|
843
|
+
| `count(where?)` | SELECT COUNT(\*) — auto-filters soft-deleted |
|
|
700
844
|
| `create(opts?)` | CREATE TABLE IF NOT EXISTS |
|
|
701
845
|
| `drop(opts?)` | DROP TABLE IF EXISTS |
|
|
702
846
|
| `createIndex(columns, opts?)` | CREATE INDEX |
|
|
703
847
|
| `createUniqueIndex(columns)` | CREATE UNIQUE INDEX |
|
|
704
|
-
| `withSql(sql)` | Returns
|
|
848
|
+
| `withSql(sql)` | Returns copy bound to a different sql (for transactions) |
|
|
705
849
|
|
|
706
850
|
**Where helpers** — composable query conditions:
|
|
707
851
|
|
|
@@ -825,20 +969,7 @@ app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID()
|
|
|
825
969
|
| `header` | `string` | `'X-Request-ID'` | Header name to read/write |
|
|
826
970
|
| `generator` | `() => string` | `crypto.randomUUID()` | ID generator |
|
|
827
971
|
|
|
828
|
-
### rootLayout(dir) [α]
|
|
829
|
-
|
|
830
|
-
One-stop middleware for a page structure. Compiles `layout.tsx` and `app.css` from the given directory, and in dev mode registers vendor bundle, HMR WebSocket, and file watcher.
|
|
831
|
-
|
|
832
|
-
```ts
|
|
833
|
-
app.use(rootLayout('./ui'))
|
|
834
|
-
// Scans ./ui/layout.tsx → layout component
|
|
835
|
-
// Scans ./ui/app.css → tailwind CSS at /{mountPath}/__wfw/style/{hash}.css
|
|
836
|
-
// Dev: vendor bundle + HMR WS + file watcher
|
|
837
|
-
```
|
|
838
972
|
|
|
839
|
-
- Sets `ctx.layoutStack` (consumed by `ssr()`)
|
|
840
|
-
- Registers CSS route at `/__wfw/style/:hash.css` — Tailwind CSS compiled via `@tailwindcss/postcss` if `app.css` exists in the dir. Hash is content-based, ensuring cache invalidation on changes.
|
|
841
|
-
- Each `rootLayout` instance is independent — sub-routes can define their own
|
|
842
973
|
|
|
843
974
|
### seo [β] + seoMiddleware [α]
|
|
844
975
|
|
|
@@ -858,21 +989,64 @@ Also exports `seoTags(config)` for generating meta/og/twitter tags as an HTML st
|
|
|
858
989
|
| `sitemap` | `SitemapConfig` | — | Sitemap configuration (urls, resolve, cacheTTL) |
|
|
859
990
|
| `headers` | `SeoHeadersConfig` | — | Response headers (e.g. `X-Robots-Tag`) |
|
|
860
991
|
|
|
861
|
-
### ssr(
|
|
992
|
+
### ssr({ dir }) [β]
|
|
993
|
+
|
|
994
|
+
One-stop Server-Side Rendering. Accepts a directory and returns a Router that handles all SSR routes, tailwind CSS, hydration bundles, and livereload — using Next.js-style file conventions.
|
|
862
995
|
|
|
863
|
-
|
|
996
|
+
```ts
|
|
997
|
+
import { Router, ssr } from 'weifuwu'
|
|
998
|
+
const app = new Router()
|
|
999
|
+
app.use('/', ssr({ dir: './ui' }))
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
**Directory conventions (Next.js-style):**
|
|
1003
|
+
|
|
1004
|
+
```
|
|
1005
|
+
./ui/
|
|
1006
|
+
├── app/ ← only this directory affects routing
|
|
1007
|
+
│ ├── globals.css ← tailwind CSS + CSS variables (optional)
|
|
1008
|
+
│ ├── layout.tsx → root layout (wraps all pages)
|
|
1009
|
+
│ ├── page.tsx → GET /
|
|
1010
|
+
│ ├── not-found.tsx → 404 page (optional)
|
|
1011
|
+
│ ├── error.tsx → error boundary (optional)
|
|
1012
|
+
│ ├── about/
|
|
1013
|
+
│ │ ├── page.tsx → GET /about
|
|
1014
|
+
│ │ └── layout.tsx → group layout
|
|
1015
|
+
│ └── posts/
|
|
1016
|
+
│ ├── page.tsx → GET /posts
|
|
1017
|
+
│ └── [id]/
|
|
1018
|
+
│ └── page.tsx → GET /posts/:id
|
|
1019
|
+
├── components/ ← shared components (does not affect routing)
|
|
1020
|
+
└── lib/ ← utilities (does not affect routing)
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
| Location | Route |
|
|
1024
|
+
|----------|-------|
|
|
1025
|
+
| `app/page.tsx` | `GET /` |
|
|
1026
|
+
| `app/[param]/page.tsx` | `GET /:param` |
|
|
1027
|
+
| `app/layout.tsx` | Root layout (wraps all pages in its subtree) |
|
|
1028
|
+
| `app/not-found.tsx` | 404 fallback for that subtree |
|
|
1029
|
+
| `app/error.tsx` | Error boundary for that subtree |
|
|
1030
|
+
| `app/globals.css` | Tailwind CSS entry (compiled via `@tailwindcss/postcss`) |
|
|
1031
|
+
|
|
1032
|
+
**How it works:**
|
|
1033
|
+
|
|
1034
|
+
- Each page is lazy-resolved on first request — only the `page.tsx` and its layout chain are compiled
|
|
1035
|
+
- Hydration bundle generated per-page at `/__ssr/{hash}.js`
|
|
1036
|
+
- Tailwind CSS served at `/__wfw/style/{hash}.css` (cached, content-hashed)
|
|
1037
|
+
- Dev mode: vendor bundle, HMR WebSocket, file watcher — all automatic
|
|
1038
|
+
- Page components and layouts are compiled via esbuild at runtime — no build step needed
|
|
864
1039
|
|
|
865
1040
|
```ts
|
|
866
|
-
|
|
1041
|
+
// Multiple independent SSR directories
|
|
1042
|
+
app.use('/', ssr({ dir: './www' }))
|
|
1043
|
+
app.use('/admin', ssr({ dir: './admin' }))
|
|
1044
|
+
|
|
1045
|
+
// API routes coexist normally
|
|
1046
|
+
app.get('/api/ping', () => Response.json({ pong: true }))
|
|
867
1047
|
```
|
|
868
1048
|
|
|
869
|
-
|
|
870
|
-
- Reads `ctx.layoutStack` (set by `rootLayout()` or `layout()`) and wraps the component from outer to inner
|
|
871
|
-
- Injects hydration script pointing to the auto-generated client bundle at `/__ssr/{entryId}.js`
|
|
872
|
-
- Injects `<link>` for tailwind CSS from `ctx.tailwindCssUrl` (set by `rootLayout()`)
|
|
873
|
-
- Serializes middleware-injected `ctx` data to `window.__WEIFUWU_CTX` for client-side hydration
|
|
874
|
-
- **Dev mode:** uses `createRoot` instead of `hydrateRoot` — all hooks (`useState`, `useEffect`) work correctly; SSR content is still streamed for fast first paint
|
|
875
|
-
- **Prod mode:** uses `hydrateRoot` for full SSR hydration
|
|
1049
|
+
Layout components receive `{ children }` and wrap from outer to inner:
|
|
876
1050
|
|
|
877
1051
|
### tenant [β]
|
|
878
1052
|
|
|
@@ -1084,9 +1258,9 @@ function Toast() {
|
|
|
1084
1258
|
|
|
1085
1259
|
### Dev mode
|
|
1086
1260
|
|
|
1087
|
-
Auto-detected when `NODE_ENV !== 'production'`. `
|
|
1261
|
+
Auto-detected when `NODE_ENV !== 'production'`. `ssr({dir})` automatically registers vendor bundle, HMR WebSocket, and file watcher. No explicit setup needed.
|
|
1088
1262
|
|
|
1089
|
-
When a `.tsx` or `.css` file changes under the `
|
|
1263
|
+
When a `.tsx` or `.css` file changes under the `ssr` dir, the browser hot-updates without refreshing — `useState` values are preserved. Layout changes trigger a full page reload.
|
|
1090
1264
|
|
|
1091
1265
|
---
|
|
1092
1266
|
|
|
@@ -1125,9 +1299,10 @@ Every public symbol can be imported from `'weifuwu'`:
|
|
|
1125
1299
|
### Core
|
|
1126
1300
|
|
|
1127
1301
|
```ts
|
|
1128
|
-
serve, createTestServer, Router,
|
|
1302
|
+
serve, createTestServer, Router, ssr,
|
|
1129
1303
|
Context, Handler, Middleware, ErrorHandler, ServeOptions, Server,
|
|
1130
|
-
loadEnv
|
|
1304
|
+
loadEnv, testApp, TestApp, TestRequest, TestResponse,
|
|
1305
|
+
currentTraceId, currentTrace, runWithTrace, traceElapsed, TraceContext,
|
|
1131
1306
|
```
|
|
1132
1307
|
|
|
1133
1308
|
### Middleware modules
|
|
@@ -1143,6 +1318,7 @@ preferences, serveStatic
|
|
|
1143
1318
|
postgres, PostgresOptions, PostgresClient,
|
|
1144
1319
|
redis, RedisOptions, RedisClient,
|
|
1145
1320
|
queue, QueueOptions, QueueJob, Queue,
|
|
1321
|
+
PostgresInjected, RedisInjected, QueueInjected,
|
|
1146
1322
|
// Schema helpers — importable alongside postgres:
|
|
1147
1323
|
pgTable, SQL, sql,
|
|
1148
1324
|
ColumnBuilder, serial, uuid, text, integer, boolean, boolean_, timestamptz, jsonb, textArray, vector,
|
|
@@ -1158,8 +1334,10 @@ TsxContext, useLoaderData,
|
|
|
1158
1334
|
useWebsocket, useAction, useFetch, useQueryState, createStore,
|
|
1159
1335
|
Link, useNavigate, useNavigating, addInterceptor,
|
|
1160
1336
|
useLocale, useTheme, applyTheme, useFlashMessage,
|
|
1337
|
+
useAgentStream,
|
|
1161
1338
|
Head
|
|
1162
1339
|
```
|
|
1340
|
+
export type { UseAgentStreamOptions, UseAgentStreamReturn, AgentStreamState } from 'weifuwu/react'
|
|
1163
1341
|
|
|
1164
1342
|
### AI SDK (re-exported from `ai`)
|
|
1165
1343
|
|
|
@@ -1172,12 +1350,16 @@ openai, createOpenAI
|
|
|
1172
1350
|
### Other modules
|
|
1173
1351
|
|
|
1174
1352
|
```ts
|
|
1175
|
-
health, analytics, seo, seoMiddleware, seoTags,
|
|
1353
|
+
preferences, health, analytics, seo, seoMiddleware, seoTags,
|
|
1176
1354
|
user, mailer, graphql, aiStream, runWorkflow,
|
|
1177
1355
|
logdb, messager, agent, iii, createWorker, registerWorker,
|
|
1178
1356
|
opencode, deploy, defineConfig,
|
|
1357
|
+
testApp, TestApp, TestRequest, TestResponse,
|
|
1179
1358
|
getCookies, setCookie, deleteCookie,
|
|
1180
|
-
createSSEStream, formatSSE, formatSSEData
|
|
1359
|
+
createSSEStream, formatSSE, formatSSEData,
|
|
1360
|
+
currentTraceId, currentTrace, runWithTrace, traceElapsed,
|
|
1361
|
+
createHub, Hub, HubOptions,
|
|
1362
|
+
DEFAULT_MAX_BODY, MIGRATIONS_TABLE,
|
|
1181
1363
|
```
|
|
1182
1364
|
|
|
1183
1365
|
---
|
package/cli/template/app.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Router, ssr, rootLayout, preferences } from '../../index.ts'
|
|
3
|
-
|
|
4
|
-
const _ui = join(import.meta.dirname, 'ui')
|
|
5
|
-
const _loc = join(import.meta.dirname, 'locales')
|
|
1
|
+
import { Router, ssr, preferences } from '../../index.ts'
|
|
6
2
|
|
|
7
3
|
export const app = new Router()
|
|
8
|
-
app.use(
|
|
9
|
-
app.use(preferences({ dir:
|
|
4
|
+
app.use('/', ssr({ dir: './ui' }))
|
|
5
|
+
app.use(preferences({ dir: './locales', locale: { default: 'en' }, theme: { default: 'system' } }))
|
|
10
6
|
app.use(async (req, ctx, next) => {
|
|
11
7
|
ctx.loaderData = {
|
|
12
8
|
features: [
|
|
@@ -17,6 +13,5 @@ app.use(async (req, ctx, next) => {
|
|
|
17
13
|
}
|
|
18
14
|
return next(req, ctx)
|
|
19
15
|
})
|
|
20
|
-
app.get('/', ssr(join(_ui, 'page.tsx')))
|
|
21
16
|
app.get('/api/ping', () => Response.json({ pong: true, time: new Date().toISOString() }))
|
|
22
17
|
app.ws('/ws/echo', { message(ws, _ctx, data) { ws.send(`echo: ${data}`) } })
|
package/cli/template/index.ts
CHANGED
|
@@ -3,6 +3,4 @@ import { app } from './app.ts'
|
|
|
3
3
|
|
|
4
4
|
loadEnv()
|
|
5
5
|
const port = Number(process.env.PORT) || 3000
|
|
6
|
-
|
|
7
|
-
await server.ready
|
|
8
|
-
console.log(`Listening on http://localhost:${server.port}`)
|
|
6
|
+
serve(app.handler(), { port, websocket: app.websocketHandler() })
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
-
import { useWebsocket, useLoaderData, useLocale, useTheme } from '
|
|
3
|
-
import Greeting from '
|
|
2
|
+
import { useWebsocket, useLoaderData, useLocale, useTheme } from '../../../../react.ts'
|
|
3
|
+
import Greeting from '../components/Greeting.tsx'
|
|
4
4
|
|
|
5
5
|
export default function Home() {
|
|
6
6
|
const [input, setInput] = useState('')
|