weifuwu 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,22 +6,21 @@
6
6
 
7
7
  weifuwu doesn't invent its own request/response abstraction. `Request` and `Response` are the same objects you use in `fetch()` — what you learn in the browser applies directly on the server. `ctx` is the only framework object, and it only carries what the router parsed for you (`params`, `query`).
8
8
 
9
- Features like `tsx()`, WebSocket, GraphQL, and AI streaming all follow the same `(req, ctx) => Response` contract. There is no separate concept for "page route" vs "API route" everything is a handler that returns a `Response`. `tsx()` just generates a `Response` from a React component the same way `router.get()` returns a `Response` from a handler function.
9
+ Everything follows the same `(req, ctx) => Response` contract. The Router handles HTTP routing and WebSocket. All other featuresauth, validation, database, GraphQL, AI, workflow are standalone modules you import and mount with `app.use()`.
10
10
 
11
11
  ## Features
12
12
 
13
13
  - **Web Standard** — `Request` / `Response` / `ReadableStream`, zero abstractions
14
14
  - **Trie router** — static > param > wildcard, sub-router mounting, path params
15
15
  - **Middleware** — global, path-scoped, route-level — onion model, short-circuit
16
- - **Built-in middleware** — `auth()`, `cors()`, `logger()`, `rateLimit()`, `compress()`
16
+ - **Middleware modules** — `auth()`, `cors()`, `logger()`, `rateLimit()`, `compress()`, `validate()`, `upload()`
17
17
  - **React SSR + Hydration** — `tsx({ dir })` — page.tsx / load.ts / layout.tsx / route.ts / not-found.tsx
18
18
  - **WebSocket** — `router.ws()` with upgrade middleware (auth before connect)
19
- - **GraphQL** — `router.graphql()` with GraphiQL IDE
20
- - **AI streaming** — `router.ai()` via Vercel AI SDK
21
- - **AI workflows** — `router.workflow()` — intent-to-execution pipelines with `tool()` + SSE
19
+ - **GraphQL** — `graphql(handler)` sub-Router with GraphiQL IDE
20
+ - **AI streaming** — `ai(handler)` sub-Router via Vercel AI SDK
21
+ - **AI workflows** — `workflow(handler)` sub-Router — intent-to-execution pipelines with `tool()` + SSE
22
+ - **PostgreSQL** — `postgres()` — zod-to-DDL, auto-migration, 6 CRUD methods, `ctx.sql` escape hatch
22
23
  - **Static files** — `serveStatic()` with ETag, 304, MIME, directory index
23
- - **Request validation** — `validate()` with Zod (body / query / params / headers)
24
- - **File upload** — `upload()` multipart parser with disk save, size & type limits
25
24
  - **Cookie** — `getCookies()`, `setCookie()`, `deleteCookie()` — immutable
26
25
  - **Error handling** — global `onError()`
27
26
  - **Zero build** — native TypeScript in Node.js v24+
@@ -31,176 +30,9 @@ Features like `tsx()`, WebSocket, GraphQL, and AI streaming all follow the same
31
30
 
32
31
  ```ts
33
32
  import { serve } from 'weifuwu'
34
-
35
33
  serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
36
34
  ```
37
35
 
38
- ## React pages with tsx()
39
-
40
- ```ts
41
- import { serve, Router } from 'weifuwu'
42
- import { tsx } from 'weifuwu/tsx'
43
-
44
- const app = new Router()
45
- app.use('/', await tsx({ dir: './pages/' }))
46
-
47
- serve(app.handler(), { port: 3000 })
48
- ```
49
-
50
- ### File conventions
51
-
52
- ```
53
- pages/
54
- page.tsx → GET / (React component, default export)
55
- layout.tsx → root layout (HTML shell, receives req/ctx, NOT hydrated)
56
- not-found.tsx → 404 error page (rendered for unmatched routes, wrapped in layout)
57
- about/page.tsx → GET /about
58
- blog/[slug]/
59
- page.tsx → GET /blog/:slug
60
- load.ts → data fetching (server-only, default export)
61
- route.ts → POST /blog/:slug (API, named exports POST/PUT/DELETE/...)
62
- blog/layout.tsx → /blog/* layout (UI structure, receives children, hydrated)
63
- api/search/
64
- route.ts → GET /api/search (standalone API, no page.tsx needed)
65
- ```
66
-
67
- ### page.tsx — page component
68
-
69
- ```tsx
70
- export default function Page({ params, query }: {
71
- params: { slug: string }
72
- query: Record<string, string>
73
- }) {
74
- return <article><h1>{params.slug}</h1></article>
75
- }
76
- ```
77
-
78
- ### load.ts — data fetching (server-only)
79
-
80
- ```ts
81
- import { db } from './db.ts'
82
-
83
- export default async function load({ params, query }: {
84
- params: Record<string, string>
85
- query: Record<string, string>
86
- }) {
87
- const data = await db.query(params.slug)
88
- return { data } // merged into props passed to page.tsx
89
- }
90
- ```
91
-
92
- `load()` runs only on the server. Its return value is merged with `{ params, query }` and passed to the page component. The merged props are serialized as `window.__WEIFUWU_PROPS` for client hydration.
93
-
94
- ### layout.tsx — root layout vs nested layouts
95
-
96
- Two types of layouts, distinguished by their position in the directory tree:
97
-
98
- **Root layout** (`pages/layout.tsx`) — receives `{ children, req, ctx }`:
99
-
100
- ```tsx
101
- export default function RootLayout({ children, req, ctx }: {
102
- children: React.ReactNode
103
- req: Request
104
- ctx: Context
105
- }) {
106
- const theme = req.headers.get('Cookie')?.includes('theme=dark') ? 'dark' : 'light'
107
- return (
108
- <html class={theme}>
109
- <head><title>App</title></head>
110
- <body>
111
- <div id="__weifuwu_root">{children}</div>
112
- </body>
113
- </html>
114
- )
115
- }
116
- ```
117
-
118
- - Controls the full HTML shell (`<html>`, `<head>`, `<body>`)
119
- - Has access to `req`/`ctx` for cookie/header-based customization
120
- - **Not hydrated** — safe to use `req`/`ctx` (never serialized to client)
121
-
122
- **Nested layouts** (`pages/blog/layout.tsx`) — receives only `{ children }`:
123
-
124
- ```tsx
125
- export default function BlogLayout({ children }: { children: React.ReactNode }) {
126
- return <div className="sidebar-layout">{children}</div>
127
- }
128
- ```
129
-
130
- - Provide UI structure (sidebar, nav, search box)
131
- - **Hydrated** on the client — can use `useState`, event handlers
132
- - No access to `req`/`ctx` (not serializable)
133
-
134
- Layouts auto-nest by directory depth — `pages/blog/layout.tsx` wraps `pages/blog/*` pages inside `pages/layout.tsx`.
135
-
136
- ### `TsxContext` and `useTsx()`
137
-
138
- Any component in the tree can access routing context without prop drilling:
139
-
140
- ```tsx
141
- import { useTsx } from 'weifuwu'
142
-
143
- function Sidebar() {
144
- const { params, query, user, parsed } = useTsx()
145
- // params.slug, query.page, user.name — from any depth
146
- return <aside>User: {user?.name}</aside>
147
- }
148
- ```
149
-
150
- Available fields:
151
-
152
- | Field | Source | Description |
153
- |-------|--------|-------------|
154
- | `params` | URL path | Route parameters (`:slug`, `:id`) |
155
- | `query` | URL search | Query string (`?page=1`) |
156
- | `user` | `auth()` middleware | Set by `verify` callback |
157
- | `parsed` | `validate()` / `upload()` | Validated body / uploaded files |
158
-
159
- ### route.ts — API (co-located with page)
160
-
161
- ```ts
162
- export const POST: Handler = async (req, ctx) => {
163
- const body = await req.json()
164
- return Response.json({ ...body, slug: ctx.params.slug })
165
- }
166
- ```
167
-
168
- Route.ts exports `POST`/`PUT`/`DELETE`/`PATCH` (GET is handled by page.tsx). The same `route.ts` file coexists with `page.tsx` in the same directory for handling form submissions or AJAX requests. Standalone `route.ts` (without a co-located `page.tsx`) registers all methods including `GET`.
169
-
170
- ### not-found.tsx — 404 page
171
-
172
- ```tsx
173
- // pages/not-found.tsx
174
- export default function NotFound() {
175
- return <h1 class="text-4xl">404 – Not Found</h1>
176
- }
177
- ```
178
-
179
- Automatically rendered for unmatched routes, wrapped in the full layout chain. Works with `use('/')` mounting and standalone usage.
180
-
181
- ### Usage within a full app
182
-
183
- ```ts
184
- import { serve, Router } from 'weifuwu'
185
- import { tsx } from 'weifuwu/tsx'
186
-
187
- const r = new Router()
188
- r.use('/', await tsx({ dir: './pages/' }))
189
-
190
- // Other features coexist in the same process
191
- r.ws('/chat', { message(ws, _, data) { ws.send(data) } })
192
- r.graphql('/graphql', { schema: `...`, resolvers: { ... } })
193
-
194
- serve(r.handler())
195
- ```
196
-
197
- ```bash
198
- node --watch app.ts # development
199
- node app.ts # production
200
- ```
201
-
202
- No build step, no configuration file — just Node.js and React.
203
-
204
36
  ## Router
205
37
 
206
38
  ```ts
@@ -245,9 +77,6 @@ app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
245
77
 
246
78
  // Custom header
247
79
  app.use(auth({ header: 'X-API-Key', token: 'my-key' }))
248
-
249
- // Token can also be passed via query string ?access_token=xxx
250
- // Proxy forwards using the same method the client used (header ↔ query)
251
80
  ```
252
81
 
253
82
  ### CORS
@@ -294,16 +123,6 @@ app.use(compress()) // brotli > gzip > deflate
294
123
  app.use(compress({ threshold: 2048 })) // only compress > 2KB
295
124
  ```
296
125
 
297
- ## Static files
298
-
299
- ```ts
300
- import { serveStatic } from 'weifuwu'
301
-
302
- router.get('/static/*', serveStatic('./public'))
303
- ```
304
-
305
- Features: MIME type detection (20+ types), ETag + If-None-Match (304), directory index (index.html), path traversal protection, Cache-Control.
306
-
307
126
  ## Validation
308
127
 
309
128
  ```ts
@@ -321,16 +140,6 @@ router.post('/users',
321
140
  // ctx.parsed.body — typed & validated
322
141
  },
323
142
  )
324
-
325
- // Validate multiple dimensions at once
326
- router.post('/data',
327
- validate({
328
- body: z.object({ value: z.number() }),
329
- query: z.object({ token: z.string() }),
330
- params: z.object({ id: z.string().length(24) }),
331
- }),
332
- handler,
333
- )
334
143
  ```
335
144
 
336
145
  ## File upload
@@ -363,6 +172,92 @@ res = setCookie(res, 'session', 'token', { httpOnly: true, secure: true, maxAge:
363
172
  res = deleteCookie(res, 'session')
364
173
  ```
365
174
 
175
+ ## Static files
176
+
177
+ ```ts
178
+ import { serveStatic } from 'weifuwu'
179
+
180
+ router.get('/static/*', serveStatic('./public'))
181
+ ```
182
+
183
+ Features: MIME type detection (20+ types), ETag + If-None-Match (304), directory index (index.html), path traversal protection, Cache-Control.
184
+
185
+ ## PostgreSQL
186
+
187
+ Built-in PostgreSQL — zero config, zero ORM, zero migration files.
188
+
189
+ ```ts
190
+ import { serve, Router, postgres } from 'weifuwu'
191
+ import { z } from 'zod'
192
+
193
+ const app = new Router()
194
+ const pg = postgres()
195
+
196
+ const User = pg.table('users', {
197
+ id: z.number().optional(), // → SERIAL PRIMARY KEY
198
+ name: z.string().min(1), // → TEXT NOT NULL
199
+ email: z.string().email(), // → TEXT NOT NULL
200
+ age: z.number().optional(), // → INTEGER
201
+ })
202
+
203
+ await pg.migrate()
204
+ // Auto-creates tables / adds missing columns via information_schema
205
+ app.use(pg) // injects ctx.sql into handlers
206
+ ```
207
+
208
+ ### 6 methods — HTTP semantics
209
+
210
+ ```ts
211
+ User.get(1) // GET /users/:id
212
+ User.list({ name: 'a' }, // GET /users?name=a
213
+ { limit: 10, offset: 0, sort: { id: 'desc' } })
214
+ // → { rows: User[], count: number }
215
+
216
+ User.create({ name: 'A', email: 'a@b.com' }) // POST /users
217
+ User.patch(1, { name: 'B' }) // PATCH /users/:id
218
+ User.remove(1) // DELETE /users/:id
219
+ ```
220
+
221
+ Every method validates input against your zod schema automatically. Complex queries use `ctx.sql`:
222
+
223
+ ```ts
224
+ app.get('/users/stats', async (req, ctx) => {
225
+ const rows = await ctx.sql`
226
+ SELECT u.*, count(p.id) as posts
227
+ FROM users u LEFT JOIN posts p ON p.user_id = u.id
228
+ GROUP BY u.id
229
+ `
230
+ return Response.json(rows)
231
+ })
232
+ ```
233
+
234
+ ### Migration-free sync
235
+
236
+ `pg.migrate()` queries `information_schema.columns` and only runs the DDL needed:
237
+
238
+ - **Table missing** → `CREATE TABLE IF NOT EXISTS`
239
+ - **Column missing** → `ALTER TABLE ADD COLUMN IF NOT EXISTS`
240
+ - **Existing** → no-op
241
+
242
+ Safe for production: never drops or alters existing columns. Destructive operations (rename, type change, drop) are done via `ctx.sql`.
243
+
244
+ ### Connection lifecycle
245
+
246
+ ```ts
247
+ const pg = postgres() // reads DATABASE_URL
248
+ const pg = postgres('postgres://...') // explicit connection
249
+ const pg = postgres({ signal: ac.signal }) // abort → sql.end()
250
+ await pg.close() // explicit close
251
+ ```
252
+
253
+ ### Primary keys
254
+
255
+ | zod field | PostgreSQL |
256
+ |-----------|-----------|
257
+ | `id: z.number().optional()` | `SERIAL PRIMARY KEY` |
258
+ | `id: z.string().uuid().optional()` | `UUID PRIMARY KEY DEFAULT gen_random_uuid()` |
259
+ | `id: z.string()` | `TEXT PRIMARY KEY` (you pass the value) |
260
+
366
261
  ## WebSocket
367
262
 
368
263
  ```ts
@@ -397,69 +292,313 @@ app.ws('/secure',
397
292
 
398
293
  ## GraphQL
399
294
 
295
+ GraphQL endpoint with GraphiQL IDE. Mount as a sub-Router:
296
+
400
297
  ```ts
298
+ import { serve, Router, graphql } from 'weifuwu'
299
+
401
300
  const app = new Router()
402
- .graphql('/graphql', {
403
- schema: `
404
- type Query { hello: String }
405
- type Mutation { setMessage(msg: String!): String }
406
- `,
407
- resolvers: {
408
- Query: { hello: () => 'world' },
409
- Mutation: { setMessage: (_, { msg }) => msg },
410
- },
411
- graphiql: true,
412
- })
301
+ app.use('/graphql', graphql(() => ({
302
+ schema: `
303
+ type Query { hello: String }
304
+ type Mutation { setMessage(msg: String!): String }
305
+ `,
306
+ resolvers: {
307
+ Query: { hello: () => 'world' },
308
+ Mutation: { setMessage: (_, { msg }) => msg },
309
+ },
310
+ graphiql: true,
311
+ })))
413
312
 
414
313
  serve(app.handler(), { port: 3000 })
415
314
  ```
416
315
 
316
+ The handler receives `(req, ctx)` so you can customize the schema based on the request.
317
+
417
318
  ## AI streaming
418
319
 
320
+ Server-sent event streaming via the Vercel AI SDK:
321
+
419
322
  ```ts
323
+ import { serve, Router, ai } from 'weifuwu'
420
324
  import { openai } from '@ai-sdk/openai'
421
325
 
422
326
  const app = new Router()
423
- .ai('/chat', async (req) => {
424
- const { messages } = await req.json()
425
- return {
426
- model: openai('gpt-4o'),
427
- messages,
428
- }
429
- })
327
+ app.use('/chat', ai(async (req, ctx) => {
328
+ const { messages } = await req.json()
329
+ return { model: openai('gpt-4o'), messages }
330
+ }))
430
331
 
431
332
  serve(app.handler(), { port: 3000 })
432
333
  ```
433
334
 
434
335
  ## Workflow
435
336
 
436
- Define tools (business capabilities) and let AI generate and execute multi-step workflows.
337
+ Define business capabilities as **Tools** (`tool()`), then chain them into **workflows** for AI-driven multi-step execution. Works with or without an LLM — hand-write the workflow JSON or let AI generate it from a goal.
437
338
 
438
339
  ```ts
439
- import { Router, tool, createWorkflowEngine } from 'weifuwu'
340
+ import { Router, tool, workflow } from 'weifuwu'
440
341
  import { z } from 'zod'
441
342
 
343
+ // 1. Define tools (business capabilities)
442
344
  const tools = {
443
345
  queryUser: tool({
444
- description: '查询用户信息,返回 email, name',
346
+ description: 'Query user info, returns email, name',
445
347
  inputSchema: z.object({ userId: z.string() }),
446
348
  execute: async ({ userId }) => ({ id: userId, email: 'user@test.com', name: 'Test' }),
447
349
  }),
350
+ sendEmail: tool({
351
+ description: 'Send an email',
352
+ inputSchema: z.object({ to: z.string(), subject: z.string() }),
353
+ execute: async ({ to, subject }) => ({ sent: true }),
354
+ }),
448
355
  }
449
356
 
357
+ // 2. Mount workflow sub-router
450
358
  const app = new Router()
359
+ app.use('/agent', workflow(() => ({ tools })))
360
+ // POST /agent { nodes: [...] } → 200 { workflow: {...}, result: ... }
451
361
 
452
- // Router method — accepts { nodes } or { goal } with model
453
- app.workflow('/api/agent', { tools })
362
+ // With SSE streaming:
363
+ app.use('/agent-stream', workflow(() => ({ tools, stream: true })))
364
+ // POST /agent-stream { nodes: [...] }
365
+ // → 200 { workflowId: "xxx", eventsUrl: "/xxx/events" }
366
+ // GET /agent-stream/:workflowId/events
367
+ // → SSE: workflow-start → node-start → node-end → complete
454
368
 
455
- // Or manual execution
456
- const engine = createWorkflowEngine({ tools })
457
- const result = await engine.execute({ nodes: [
458
- { id: 's1', tool: 'set', input: { name: 'msg', value: 'hello' } },
459
- { id: 'g1', tool: 'get', input: { name: 'msg' } },
460
- ]})
369
+ // With LLM model (generates workflow from goal):
370
+ app.use('/agent-llm', workflow(() => ({
371
+ tools,
372
+ model: openai('gpt-4o'),
373
+ })))
374
+ // POST /agent-llm { goal: "给用户123发欢迎邮件" }
375
+ // ← LLM generates → executes → returns result
461
376
  ```
462
377
 
378
+ ### Tool
379
+
380
+ ```ts
381
+ import { tool } from 'weifuwu'
382
+ import { z } from 'zod'
383
+
384
+ const myTool = tool({
385
+ description: '做什么的,返回什么',
386
+ inputSchema: z.object({ key: z.string() }),
387
+ execute: async (input, ctx) => {
388
+ return { result: input.key }
389
+ },
390
+ })
391
+ ```
392
+
393
+ `ctx.onStream` 用于流式推送(如 LLM token 输出):
394
+
395
+ ```ts
396
+ const llmTool = tool({
397
+ description: '生成文本',
398
+ inputSchema: z.object({ prompt: z.string() }),
399
+ execute: async (input, ctx) => {
400
+ const stream = await openai.chat.completions.create({ ... })
401
+ let full = ''
402
+ for await (const chunk of stream) {
403
+ full += chunk.choices[0]?.delta?.content || ''
404
+ ctx.onStream?.({ type: 'llm-stream', chunk, accumulated: full })
405
+ }
406
+ return { text: full }
407
+ },
408
+ })
409
+ ```
410
+
411
+ ### Core Nodes
412
+
413
+ 7 built-in node types:
414
+
415
+ | Node | Purpose | Input |
416
+ |------|---------|-------|
417
+ | `call` | Call a tool or sub-workflow | `{ tool: "name", args: {...} }` or `{ function: "name", args: {...} }` |
418
+ | `set` | Declare or assign a variable | `{ name: "x", value: 42 }` |
419
+ | `get` | Read a variable | `{ name: "x" }` |
420
+ | `eval` | Evaluate an expression | `{ expression: "$var.x + 1" }` |
421
+ | `if` | Conditional branch | `{ conditions: [{ test: ..., body: [nodes] }] }` |
422
+ | `while` | Loop | `{ condition: "$var.i < 5" }, body: [nodes]` |
423
+ | `http` | HTTP request | `{ url: "https://...", method: "GET" }` |
424
+
425
+ ### Variable Reference Syntax
426
+
427
+ | Pattern | Meaning | Example |
428
+ |---------|---------|---------|
429
+ | `$var.x` | Variable `x` | `$var.counter` |
430
+ | `$nodes.u.output` | Full output of node `u` | `$nodes.u.output` |
431
+ | `$nodes.u.output.field` | Specific field | `$nodes.u.output.email` |
432
+ | `$input.userId` | Workflow input param | `$input.userId` |
433
+ | `42`, `true`, `"hello"` | Literal values | Passed as-is |
434
+
435
+ ### Engine API
436
+
437
+ For programmatic use outside of Router:
438
+
439
+ ```ts
440
+ import { createWorkflowEngine, createSSEManager } from 'weifuwu'
441
+
442
+ const sse = createSSEManager()
443
+ const engine = createWorkflowEngine({ tools, sseManager: sse })
444
+
445
+ // Sync execution
446
+ const result = await engine.execute({ nodes: [...] })
447
+
448
+ // Async execution with SSE
449
+ engine.runAsync('wf-1', { nodes: [...] })
450
+ ```
451
+
452
+ ### SSE Events
453
+
454
+ ```ts
455
+ const sse = createSSEManager()
456
+ const stream = sse.createStream('wf-1')
457
+
458
+ const reader = stream.getReader()
459
+ // event: workflow-start — { workflowId, goal }
460
+ // event: node-start — { nodeId, tool, input }
461
+ // event: node-end — { nodeId, output }
462
+ // event: llm-stream — { nodeId, chunk, accumulated }
463
+ // event: complete — { result, duration }
464
+ // event: error — { error }
465
+ ```
466
+
467
+ ### Sub-workflows
468
+
469
+ Define reusable sub-workflows in the `functions` field:
470
+
471
+ ```json
472
+ {
473
+ "functions": {
474
+ "double": {
475
+ "inputSchema": { "type": "object", "properties": { "x": { "type": "number" } } },
476
+ "workflow": {
477
+ "nodes": [
478
+ { "id": "calc", "tool": "eval", "input": { "expression": "$input.x * 2" } }
479
+ ]
480
+ }
481
+ }
482
+ },
483
+ "nodes": [
484
+ { "id": "call_double", "tool": "call", "input": { "function": "double", "args": { "x": 21 } } }
485
+ ]
486
+ }
487
+ ```
488
+
489
+ ## React pages with tsx()
490
+
491
+ ```ts
492
+ import { serve, Router } from 'weifuwu'
493
+ import { tsx } from 'weifuwu/tsx'
494
+
495
+ const app = new Router()
496
+ app.use('/', await tsx({ dir: './pages/' }))
497
+
498
+ serve(app.handler(), { port: 3000 })
499
+ ```
500
+
501
+ ### File conventions
502
+
503
+ ```
504
+ pages/
505
+ page.tsx → GET / (React component, default export)
506
+ layout.tsx → root layout (HTML shell, receives req/ctx, NOT hydrated)
507
+ not-found.tsx → 404 error page (rendered for unmatched routes, wrapped in layout)
508
+ about/page.tsx → GET /about
509
+ blog/[slug]/
510
+ page.tsx → GET /blog/:slug
511
+ load.ts → data fetching (server-only, default export)
512
+ route.ts → POST /blog/:slug (API, named exports POST/PUT/DELETE/...)
513
+ blog/layout.tsx → /blog/* layout (UI structure, receives children, hydrated)
514
+ api/search/
515
+ route.ts → GET /api/search (standalone API, no page.tsx needed)
516
+ ```
517
+
518
+ ### page.tsx — page component
519
+
520
+ ```tsx
521
+ export default function Page({ params, query }: {
522
+ params: { slug: string }
523
+ query: Record<string, string>
524
+ }) {
525
+ return <article><h1>{params.slug}</h1></article>
526
+ }
527
+ ```
528
+
529
+ ### load.ts — data fetching (server-only)
530
+
531
+ ```ts
532
+ export default async function load({ params, query }: {
533
+ params: Record<string, string>
534
+ query: Record<string, string>
535
+ }) {
536
+ const data = await db.query(params.slug)
537
+ return { data } // merged into props passed to page.tsx
538
+ }
539
+ ```
540
+
541
+ ### layout.tsx
542
+
543
+ **Root layout** (`pages/layout.tsx`) — receives `{ children, req, ctx }`:
544
+
545
+ ```tsx
546
+ export default function RootLayout({ children, req, ctx }: {
547
+ children: React.ReactNode
548
+ req: Request
549
+ ctx: Context
550
+ }) {
551
+ return (
552
+ <html>
553
+ <head><title>App</title></head>
554
+ <body><div id="__weifuwu_root">{children}</div></body>
555
+ </html>
556
+ )
557
+ }
558
+ ```
559
+
560
+ **Nested layouts** (`pages/blog/layout.tsx`) — receives only `{ children }`.
561
+
562
+ ### route.ts — API (co-located with page)
563
+
564
+ ```ts
565
+ export const POST: Handler = async (req, ctx) => {
566
+ const body = await req.json()
567
+ return Response.json({ ...body, slug: ctx.params.slug })
568
+ }
569
+ ```
570
+
571
+ ### not-found.tsx — 404 page
572
+
573
+ ```tsx
574
+ export default function NotFound() {
575
+ return <h1 class="text-4xl">404 – Not Found</h1>
576
+ }
577
+ ```
578
+
579
+ ## Usage within a full app
580
+
581
+ ```ts
582
+ import { serve, Router, ai, graphql, workflow } from 'weifuwu'
583
+ import { tsx } from 'weifuwu/tsx'
584
+
585
+ const app = new Router()
586
+ app.use('/', await tsx({ dir: './pages/' }))
587
+ app.use('/chat', ai(async (req) => ({ model: openai('gpt-4o'), messages: (await req.json()).messages })))
588
+ app.use('/graphql', graphql(() => ({ schema: `type Query { hello: String }`, resolvers: { Query: { hello: () => 'world' } } })))
589
+ app.use('/agent', workflow(() => ({ tools: myTools, stream: true })))
590
+ app.ws('/chat', { message(ws, _, data) { ws.send(data) } })
591
+
592
+ serve(app.handler())
593
+ ```
594
+
595
+ ```bash
596
+ node --watch app.ts # development
597
+ node app.ts # production
598
+ ```
599
+
600
+ No build step, no configuration file — just Node.js.
601
+
463
602
  ## Graceful shutdown
464
603
 
465
604
  ```ts
@@ -470,7 +609,6 @@ const ac = new AbortController()
470
609
  let server: Server
471
610
 
472
611
  process.on('SIGTERM', () => {
473
- console.log('shutting down…')
474
612
  ac.abort()
475
613
  server.stop()
476
614
  })
@@ -480,7 +618,6 @@ server = serve((req, ctx) => new Response('Hello'), {
480
618
  signal: ac.signal,
481
619
  })
482
620
  await server.ready
483
- console.log(`listening on :${server.port}`)
484
621
  ```
485
622
 
486
623
  ### Using with WebSocket
@@ -519,10 +656,6 @@ Returns `{ stop, port, hostname, ready }`.
519
656
 
520
657
  ### `tsx(options)`
521
658
 
522
- ```ts
523
- import { tsx } from 'weifuwu/tsx'
524
- ```
525
-
526
659
  | Option | Default | Description |
527
660
  |--------|---------|-------------|
528
661
  | `dir` | — | Pages directory path |
@@ -536,33 +669,43 @@ Returns `Promise<Router>`.
536
669
  | `get/post/put/delete/patch/head/options/all(path, ...mws, handler)` | Route registration |
537
670
  | `use(mw)` / `use(path, mw)` / `use(path, subRouter)` | Middleware / sub-router |
538
671
  | `ws(path, ...mws, handler)` | WebSocket route |
539
- | `graphql(path, ...mws, options)` | GraphQL endpoint |
540
- | `ai(path, ...mws, handler)` | AI streaming |
541
672
  | `onError(handler)` | Global error handler |
542
673
  | `handler()` | Returns `(req, ctx) => Response` for `serve()` |
543
674
  | `websocketHandler()` | Returns upgrade handler for `serve({ websocket })` |
544
675
 
545
- ### Built-in middleware
676
+ ### Middleware modules
546
677
 
547
- | Function | Description |
548
- |----------|-------------|
678
+ | Import | Description |
679
+ |--------|-------------|
549
680
  | `auth(options)` | Bearer token / custom header / verify / proxy |
550
681
  | `cors(options?)` | CORS with preflight, origin whitelist, credentials |
551
682
  | `logger(options?)` | Request logging with duration |
552
683
  | `rateLimit(options?)` | In-memory rate limiting with headers |
553
684
  | `compress(options?)` | Brotli / Gzip / Deflate compression |
685
+ | `validate(schemas)` | Zod validation middleware |
686
+ | `upload(options?)` | Multipart file upload middleware |
687
+
688
+ ### Sub-Router modules (mount via `app.use()`)
689
+
690
+ | Import | Description |
691
+ |--------|-------------|
692
+ | `postgres(options?)` | PostgreSQL connection + auto-migration + 6 CRUD methods |
693
+ | `graphql(handler)` | GraphQL endpoint (GET/POST + GraphiQL) |
694
+ | `ai(handler)` | AI streaming endpoint (POST) |
695
+ | `workflow(handler)` | Workflow engine (POST + SSE) |
554
696
 
555
697
  ### Utilities
556
698
 
557
699
  | Function | Description |
558
700
  |----------|-------------|
559
701
  | `serveStatic(root, options?)` | Static file serving handler |
560
- | `validate(schemas)` | Zod validation middleware |
561
- | `upload(options?)` | Multipart file upload middleware |
562
702
  | `getCookies(req)` | Parse Cookie header → object |
563
703
  | `setCookie(res, name, value, options?)` | Set cookie (returns new Response) |
564
704
  | `deleteCookie(res, name)` | Delete cookie (returns new Response) |
565
705
  | `useTsx()` | Hook returning `{ params, query, user, parsed }` from `TsxContext` |
706
+ | `createWorkflowEngine(options)` | Programmatic workflow engine |
707
+ | `createSSEManager()` | SSE event manager for workflows |
708
+ | `tool(def)` | Define a workflow tool |
566
709
 
567
710
  Import `useTsx` and `TsxContext` from `'weifuwu'`.
568
711