weifuwu 0.3.0 → 0.5.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 CHANGED
@@ -6,21 +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
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
21
23
  - **Static files** — `serveStatic()` with ETag, 304, MIME, directory index
22
- - **Request validation** — `validate()` with Zod (body / query / params / headers)
23
- - **File upload** — `upload()` multipart parser with disk save, size & type limits
24
24
  - **Cookie** — `getCookies()`, `setCookie()`, `deleteCookie()` — immutable
25
25
  - **Error handling** — global `onError()`
26
26
  - **Zero build** — native TypeScript in Node.js v24+
@@ -30,176 +30,9 @@ Features like `tsx()`, WebSocket, GraphQL, and AI streaming all follow the same
30
30
 
31
31
  ```ts
32
32
  import { serve } from 'weifuwu'
33
-
34
33
  serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
35
34
  ```
36
35
 
37
- ## React pages with tsx()
38
-
39
- ```ts
40
- import { serve, Router } from 'weifuwu'
41
- import { tsx } from 'weifuwu/tsx'
42
-
43
- const app = new Router()
44
- app.use('/', await tsx({ dir: './pages/' }))
45
-
46
- serve(app.handler(), { port: 3000 })
47
- ```
48
-
49
- ### File conventions
50
-
51
- ```
52
- pages/
53
- page.tsx → GET / (React component, default export)
54
- layout.tsx → root layout (HTML shell, receives req/ctx, NOT hydrated)
55
- not-found.tsx → 404 error page (rendered for unmatched routes, wrapped in layout)
56
- about/page.tsx → GET /about
57
- blog/[slug]/
58
- page.tsx → GET /blog/:slug
59
- load.ts → data fetching (server-only, default export)
60
- route.ts → POST /blog/:slug (API, named exports POST/PUT/DELETE/...)
61
- blog/layout.tsx → /blog/* layout (UI structure, receives children, hydrated)
62
- api/search/
63
- route.ts → GET /api/search (standalone API, no page.tsx needed)
64
- ```
65
-
66
- ### page.tsx — page component
67
-
68
- ```tsx
69
- export default function Page({ params, query }: {
70
- params: { slug: string }
71
- query: Record<string, string>
72
- }) {
73
- return <article><h1>{params.slug}</h1></article>
74
- }
75
- ```
76
-
77
- ### load.ts — data fetching (server-only)
78
-
79
- ```ts
80
- import { db } from './db.ts'
81
-
82
- export default async function load({ params, query }: {
83
- params: Record<string, string>
84
- query: Record<string, string>
85
- }) {
86
- const data = await db.query(params.slug)
87
- return { data } // merged into props passed to page.tsx
88
- }
89
- ```
90
-
91
- `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.
92
-
93
- ### layout.tsx — root layout vs nested layouts
94
-
95
- Two types of layouts, distinguished by their position in the directory tree:
96
-
97
- **Root layout** (`pages/layout.tsx`) — receives `{ children, req, ctx }`:
98
-
99
- ```tsx
100
- export default function RootLayout({ children, req, ctx }: {
101
- children: React.ReactNode
102
- req: Request
103
- ctx: Context
104
- }) {
105
- const theme = req.headers.get('Cookie')?.includes('theme=dark') ? 'dark' : 'light'
106
- return (
107
- <html class={theme}>
108
- <head><title>App</title></head>
109
- <body>
110
- <div id="__weifuwu_root">{children}</div>
111
- </body>
112
- </html>
113
- )
114
- }
115
- ```
116
-
117
- - Controls the full HTML shell (`<html>`, `<head>`, `<body>`)
118
- - Has access to `req`/`ctx` for cookie/header-based customization
119
- - **Not hydrated** — safe to use `req`/`ctx` (never serialized to client)
120
-
121
- **Nested layouts** (`pages/blog/layout.tsx`) — receives only `{ children }`:
122
-
123
- ```tsx
124
- export default function BlogLayout({ children }: { children: React.ReactNode }) {
125
- return <div className="sidebar-layout">{children}</div>
126
- }
127
- ```
128
-
129
- - Provide UI structure (sidebar, nav, search box)
130
- - **Hydrated** on the client — can use `useState`, event handlers
131
- - No access to `req`/`ctx` (not serializable)
132
-
133
- Layouts auto-nest by directory depth — `pages/blog/layout.tsx` wraps `pages/blog/*` pages inside `pages/layout.tsx`.
134
-
135
- ### `TsxContext` and `useTsx()`
136
-
137
- Any component in the tree can access routing context without prop drilling:
138
-
139
- ```tsx
140
- import { useTsx } from 'weifuwu'
141
-
142
- function Sidebar() {
143
- const { params, query, user, parsed } = useTsx()
144
- // params.slug, query.page, user.name — from any depth
145
- return <aside>User: {user?.name}</aside>
146
- }
147
- ```
148
-
149
- Available fields:
150
-
151
- | Field | Source | Description |
152
- |-------|--------|-------------|
153
- | `params` | URL path | Route parameters (`:slug`, `:id`) |
154
- | `query` | URL search | Query string (`?page=1`) |
155
- | `user` | `auth()` middleware | Set by `verify` callback |
156
- | `parsed` | `validate()` / `upload()` | Validated body / uploaded files |
157
-
158
- ### route.ts — API (co-located with page)
159
-
160
- ```ts
161
- export const POST: Handler = async (req, ctx) => {
162
- const body = await req.json()
163
- return Response.json({ ...body, slug: ctx.params.slug })
164
- }
165
- ```
166
-
167
- 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`.
168
-
169
- ### not-found.tsx — 404 page
170
-
171
- ```tsx
172
- // pages/not-found.tsx
173
- export default function NotFound() {
174
- return <h1 class="text-4xl">404 – Not Found</h1>
175
- }
176
- ```
177
-
178
- Automatically rendered for unmatched routes, wrapped in the full layout chain. Works with `use('/')` mounting and standalone usage.
179
-
180
- ### Usage within a full app
181
-
182
- ```ts
183
- import { serve, Router } from 'weifuwu'
184
- import { tsx } from 'weifuwu/tsx'
185
-
186
- const r = new Router()
187
- r.use('/', await tsx({ dir: './pages/' }))
188
-
189
- // Other features coexist in the same process
190
- r.ws('/chat', { message(ws, _, data) { ws.send(data) } })
191
- r.graphql('/graphql', { schema: `...`, resolvers: { ... } })
192
-
193
- serve(r.handler())
194
- ```
195
-
196
- ```bash
197
- node --watch app.ts # development
198
- node app.ts # production
199
- ```
200
-
201
- No build step, no configuration file — just Node.js and React.
202
-
203
36
  ## Router
204
37
 
205
38
  ```ts
@@ -244,9 +77,6 @@ app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
244
77
 
245
78
  // Custom header
246
79
  app.use(auth({ header: 'X-API-Key', token: 'my-key' }))
247
-
248
- // Token can also be passed via query string ?access_token=xxx
249
- // Proxy forwards using the same method the client used (header ↔ query)
250
80
  ```
251
81
 
252
82
  ### CORS
@@ -293,16 +123,6 @@ app.use(compress()) // brotli > gzip > deflate
293
123
  app.use(compress({ threshold: 2048 })) // only compress > 2KB
294
124
  ```
295
125
 
296
- ## Static files
297
-
298
- ```ts
299
- import { serveStatic } from 'weifuwu'
300
-
301
- router.get('/static/*', serveStatic('./public'))
302
- ```
303
-
304
- Features: MIME type detection (20+ types), ETag + If-None-Match (304), directory index (index.html), path traversal protection, Cache-Control.
305
-
306
126
  ## Validation
307
127
 
308
128
  ```ts
@@ -320,16 +140,6 @@ router.post('/users',
320
140
  // ctx.parsed.body — typed & validated
321
141
  },
322
142
  )
323
-
324
- // Validate multiple dimensions at once
325
- router.post('/data',
326
- validate({
327
- body: z.object({ value: z.number() }),
328
- query: z.object({ token: z.string() }),
329
- params: z.object({ id: z.string().length(24) }),
330
- }),
331
- handler,
332
- )
333
143
  ```
334
144
 
335
145
  ## File upload
@@ -362,6 +172,92 @@ res = setCookie(res, 'session', 'token', { httpOnly: true, secure: true, maxAge:
362
172
  res = deleteCookie(res, 'session')
363
173
  ```
364
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
+
365
261
  ## WebSocket
366
262
 
367
263
  ```ts
@@ -396,40 +292,313 @@ app.ws('/secure',
396
292
 
397
293
  ## GraphQL
398
294
 
295
+ GraphQL endpoint with GraphiQL IDE. Mount as a sub-Router:
296
+
399
297
  ```ts
298
+ import { serve, Router, graphql } from 'weifuwu'
299
+
400
300
  const app = new Router()
401
- .graphql('/graphql', {
402
- schema: `
403
- type Query { hello: String }
404
- type Mutation { setMessage(msg: String!): String }
405
- `,
406
- resolvers: {
407
- Query: { hello: () => 'world' },
408
- Mutation: { setMessage: (_, { msg }) => msg },
409
- },
410
- graphiql: true,
411
- })
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
+ })))
412
312
 
413
313
  serve(app.handler(), { port: 3000 })
414
314
  ```
415
315
 
316
+ The handler receives `(req, ctx)` so you can customize the schema based on the request.
317
+
416
318
  ## AI streaming
417
319
 
320
+ Server-sent event streaming via the Vercel AI SDK:
321
+
418
322
  ```ts
323
+ import { serve, Router, ai } from 'weifuwu'
419
324
  import { openai } from '@ai-sdk/openai'
420
325
 
421
326
  const app = new Router()
422
- .ai('/chat', async (req) => {
423
- const { messages } = await req.json()
424
- return {
425
- model: openai('gpt-4o'),
426
- messages,
327
+ app.use('/chat', ai(async (req, ctx) => {
328
+ const { messages } = await req.json()
329
+ return { model: openai('gpt-4o'), messages }
330
+ }))
331
+
332
+ serve(app.handler(), { port: 3000 })
333
+ ```
334
+
335
+ ## Workflow
336
+
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.
338
+
339
+ ```ts
340
+ import { Router, tool, workflow } from 'weifuwu'
341
+ import { z } from 'zod'
342
+
343
+ // 1. Define tools (business capabilities)
344
+ const tools = {
345
+ queryUser: tool({
346
+ description: 'Query user info, returns email, name',
347
+ inputSchema: z.object({ userId: z.string() }),
348
+ execute: async ({ userId }) => ({ id: userId, email: 'user@test.com', name: 'Test' }),
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
+ }),
355
+ }
356
+
357
+ // 2. Mount workflow sub-router
358
+ const app = new Router()
359
+ app.use('/agent', workflow(() => ({ tools })))
360
+ // POST /agent { nodes: [...] } → 200 { workflow: {...}, result: ... }
361
+
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
368
+
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
376
+ ```
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 })
427
405
  }
428
- })
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/' }))
429
497
 
430
498
  serve(app.handler(), { port: 3000 })
431
499
  ```
432
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
+
433
602
  ## Graceful shutdown
434
603
 
435
604
  ```ts
@@ -440,7 +609,6 @@ const ac = new AbortController()
440
609
  let server: Server
441
610
 
442
611
  process.on('SIGTERM', () => {
443
- console.log('shutting down…')
444
612
  ac.abort()
445
613
  server.stop()
446
614
  })
@@ -450,7 +618,6 @@ server = serve((req, ctx) => new Response('Hello'), {
450
618
  signal: ac.signal,
451
619
  })
452
620
  await server.ready
453
- console.log(`listening on :${server.port}`)
454
621
  ```
455
622
 
456
623
  ### Using with WebSocket
@@ -489,10 +656,6 @@ Returns `{ stop, port, hostname, ready }`.
489
656
 
490
657
  ### `tsx(options)`
491
658
 
492
- ```ts
493
- import { tsx } from 'weifuwu/tsx'
494
- ```
495
-
496
659
  | Option | Default | Description |
497
660
  |--------|---------|-------------|
498
661
  | `dir` | — | Pages directory path |
@@ -506,33 +669,43 @@ Returns `Promise<Router>`.
506
669
  | `get/post/put/delete/patch/head/options/all(path, ...mws, handler)` | Route registration |
507
670
  | `use(mw)` / `use(path, mw)` / `use(path, subRouter)` | Middleware / sub-router |
508
671
  | `ws(path, ...mws, handler)` | WebSocket route |
509
- | `graphql(path, ...mws, options)` | GraphQL endpoint |
510
- | `ai(path, ...mws, handler)` | AI streaming |
511
672
  | `onError(handler)` | Global error handler |
512
673
  | `handler()` | Returns `(req, ctx) => Response` for `serve()` |
513
674
  | `websocketHandler()` | Returns upgrade handler for `serve({ websocket })` |
514
675
 
515
- ### Built-in middleware
676
+ ### Middleware modules
516
677
 
517
- | Function | Description |
518
- |----------|-------------|
678
+ | Import | Description |
679
+ |--------|-------------|
519
680
  | `auth(options)` | Bearer token / custom header / verify / proxy |
520
681
  | `cors(options?)` | CORS with preflight, origin whitelist, credentials |
521
682
  | `logger(options?)` | Request logging with duration |
522
683
  | `rateLimit(options?)` | In-memory rate limiting with headers |
523
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) |
524
696
 
525
697
  ### Utilities
526
698
 
527
699
  | Function | Description |
528
700
  |----------|-------------|
529
701
  | `serveStatic(root, options?)` | Static file serving handler |
530
- | `validate(schemas)` | Zod validation middleware |
531
- | `upload(options?)` | Multipart file upload middleware |
532
702
  | `getCookies(req)` | Parse Cookie header → object |
533
703
  | `setCookie(res, name, value, options?)` | Set cookie (returns new Response) |
534
704
  | `deleteCookie(res, name)` | Delete cookie (returns new Response) |
535
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 |
536
709
 
537
710
  Import `useTsx` and `TsxContext` from `'weifuwu'`.
538
711