weifuwu 0.30.1 → 0.31.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 +228 -198
- package/dist/core/router.d.ts +21 -58
- package/dist/core/serve.d.ts +0 -4
- package/dist/core/ws.d.ts +28 -0
- package/dist/index.d.ts +9 -24
- package/dist/index.js +710 -1853
- package/dist/queue/index.d.ts +10 -0
- package/dist/queue/types.d.ts +5 -9
- package/dist/react/client.d.ts +39 -0
- package/dist/react/client.js +300 -0
- package/dist/react/context.d.ts +2 -0
- package/dist/react/error-boundary.d.ts +31 -0
- package/dist/react/hooks.d.ts +6 -0
- package/dist/react/index.d.ts +28 -0
- package/dist/react/index.js +342 -0
- package/dist/react/navigation.d.ts +63 -0
- package/dist/react/navigation.js +154 -0
- package/dist/react/render.d.ts +8 -0
- package/dist/react/route-utils.d.ts +31 -0
- package/dist/react/types.d.ts +46 -0
- package/package.json +34 -6
- package/dist/core/cookie.d.ts +0 -36
- package/dist/core/env.d.ts +0 -69
- package/dist/core/html.d.ts +0 -34
- package/dist/core/sse.d.ts +0 -47
- package/dist/middleware/csrf.d.ts +0 -31
- package/dist/middleware/flash.d.ts +0 -44
- package/dist/middleware/health.d.ts +0 -24
- package/dist/middleware/i18n.d.ts +0 -39
- package/dist/middleware/request-id.d.ts +0 -40
- package/dist/middleware/theme.d.ts +0 -31
- package/dist/middleware/validate.d.ts +0 -32
- package/dist/test/test-utils.d.ts +0 -193
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Web-standard HTTP microframework for Node.js — `(req, ctx) => Response`.
|
|
4
4
|
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
5
|
```bash
|
|
8
6
|
npm install weifuwu
|
|
9
7
|
```
|
|
@@ -18,35 +16,24 @@ serve(app, { port: 3000 })
|
|
|
18
16
|
|
|
19
17
|
## Exports
|
|
20
18
|
|
|
21
|
-
Everything is exported from `'weifuwu'`:
|
|
22
|
-
|
|
23
19
|
### Core
|
|
24
20
|
|
|
25
|
-
| Export |
|
|
26
|
-
|
|
27
|
-
| `serve(app, opts?)` |
|
|
28
|
-
| `Router` |
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `isProd()` | function | `true` if `NODE_ENV` is `production`. |
|
|
32
|
-
| `isBundled()` | function | `true` when running the esbuild bundle (checks `__WFW_BUNDLED__`). |
|
|
33
|
-
| `getPublicEnv()` | function | Returns all `process.env` keys prefixed with `WEIFUWU_PUBLIC_`. |
|
|
34
|
-
| `DEFAULT_MAX_BODY` | const | `10 * 1024 * 1024` (10MB). |
|
|
21
|
+
| Export | Description |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `serve(app, opts?)` | Start HTTP server. Returns `Server` with `port`, `hostname`, `ready`, `close()`. |
|
|
24
|
+
| `Router` | Trie-based HTTP router with WebSocket support. |
|
|
25
|
+
| `HttpError` | `new HttpError(message, status)`. Throw to return that status code. |
|
|
26
|
+
| `DEFAULT_MAX_BODY` | `10 * 1024 * 1024` (10MB). |
|
|
35
27
|
|
|
36
|
-
### Router
|
|
28
|
+
### Router API
|
|
37
29
|
|
|
38
30
|
```ts
|
|
39
31
|
const app = new Router()
|
|
40
32
|
|
|
41
33
|
// HTTP methods
|
|
42
34
|
app.get(path, ...handlers)
|
|
43
|
-
app.post(path, ...handlers)
|
|
44
|
-
app.
|
|
45
|
-
app.delete(path, ...handlers)
|
|
46
|
-
app.patch(path, ...handlers)
|
|
47
|
-
app.head(path, ...handlers)
|
|
48
|
-
app.options(path, ...handlers)
|
|
49
|
-
app.all(path, ...handlers) // match any method
|
|
35
|
+
app.post / put / delete / patch / head / options(path, ...handlers)
|
|
36
|
+
app.all(path, ...handlers) // any method
|
|
50
37
|
|
|
51
38
|
// WebSocket
|
|
52
39
|
app.ws(path, ...middlewares, handler)
|
|
@@ -54,183 +41,260 @@ app.ws(path, ...middlewares, handler)
|
|
|
54
41
|
|
|
55
42
|
// Middleware & mounting
|
|
56
43
|
app.use(middleware) // global middleware
|
|
57
|
-
app.mount(prefix, router) //
|
|
58
|
-
app.onError(handler) //
|
|
59
|
-
app.routes() // debug:
|
|
44
|
+
app.mount(prefix, router) // sub-router at prefix
|
|
45
|
+
app.onError(handler) // error handler: (error, req, ctx) => Response
|
|
46
|
+
app.routes() // debug: list all registered routes
|
|
60
47
|
|
|
61
48
|
// Path params
|
|
62
49
|
app.get('/users/:id', (req, ctx) => {
|
|
63
|
-
|
|
64
|
-
|
|
50
|
+
ctx.params.id // string
|
|
51
|
+
ctx.query.search // from ?search=...
|
|
65
52
|
})
|
|
66
53
|
```
|
|
67
54
|
|
|
68
55
|
### Middleware
|
|
69
56
|
|
|
70
|
-
| Export |
|
|
71
|
-
|
|
72
|
-
| `cors(opts?)` |
|
|
73
|
-
| `helmet(opts?)` |
|
|
74
|
-
| `compress(opts?)` |
|
|
75
|
-
| `rateLimit(opts?)` |
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `health(opts?)` | function→Router | Health check router. Mount at `/_health` or custom path. |
|
|
81
|
-
| `env()` | middleware | Injects `WEIFUWU_PUBLIC_*` env vars into `ctx.env`. |
|
|
82
|
-
| `trace(opts?)` | middleware | Injects trace context into `ctx.trace` (requestId, traceId, startTime, elapsed). |
|
|
83
|
-
|
|
84
|
-
### Route-level tools
|
|
85
|
-
|
|
86
|
-
| Export | Kind | Description |
|
|
87
|
-
|---|---|---|
|
|
88
|
-
| `serveStatic(root, opts?)` | handler | Serve static files. `cacheControl`, `index`, `dotfiles`. |
|
|
89
|
-
| `graphql(options)` | function→Router | GraphQL endpoint. Pass a `GraphQLHandler` function `({ query, variables, ctx })`. |
|
|
90
|
-
|
|
91
|
-
### HTML rendering
|
|
92
|
-
|
|
93
|
-
| Export | Kind | Description |
|
|
94
|
-
|---|---|---|
|
|
95
|
-
| `html(template, ...values)` | tagged template | Auto-escaped HTML builder. Signal/Computed-aware. |
|
|
96
|
-
| `raw(content)` | function | Raw unescaped HTML string (use with caution). |
|
|
97
|
-
|
|
98
|
-
### SSE (Server-Sent Events)
|
|
99
|
-
|
|
100
|
-
| Export | Kind | Description |
|
|
101
|
-
|---|---|---|
|
|
102
|
-
| `createSSEStream()` | function | Create an SSE stream controller for `text/event-stream` responses. |
|
|
103
|
-
| `formatSSE(event, data)` | function | Format a single SSE event string. |
|
|
104
|
-
| `formatSSEData(data)` | function | Format SSE data string. |
|
|
105
|
-
|
|
106
|
-
### Cookies
|
|
107
|
-
|
|
108
|
-
| Export | Kind | Description |
|
|
109
|
-
|---|---|---|
|
|
110
|
-
| `getCookies(req)` | function | Parse cookies from `Request`. Returns `Record<string, string>`. |
|
|
111
|
-
| `setCookie(headers, name, value, opts?)` | function | Set `Set-Cookie` header. `path`, `domain`, `maxAge`, `httpOnly`, `secure`, `sameSite`, `partitioned`. |
|
|
112
|
-
| `deleteCookie(headers, name, opts?)` | function | Delete a cookie (Max-Age=0). |
|
|
57
|
+
| Export | Description |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `cors(opts?)` | CORS headers. `origin`, `methods`, `headers`, `credentials`, `maxAge`. |
|
|
60
|
+
| `helmet(opts?)` | Security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc. |
|
|
61
|
+
| `compress(opts?)` | gzip / brotli / deflate response compression. |
|
|
62
|
+
| `rateLimit(opts?)` | Sliding-window rate limiter. `windowMs`, `max`, `keyGenerator`, Redis backend. |
|
|
63
|
+
| `logger(opts?)` | Request logging. `format: 'short' | 'combined' | 'json'`. |
|
|
64
|
+
| `upload(opts?)` | Multipart file upload via `req.formData()`. Injects `ctx.parsed`. |
|
|
65
|
+
| `serveStatic(root, opts?)` | Static file handler. `cacheControl`, `index`. |
|
|
66
|
+
| `trace(opts?)` | Injects `ctx.trace = { requestId, traceId, elapsed(), startTime }`. |
|
|
113
67
|
|
|
114
|
-
###
|
|
68
|
+
### Tracing
|
|
69
|
+
|
|
70
|
+
| Export | Description |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `currentTraceId()` | Current request's trace ID (AsyncLocalStorage). |
|
|
73
|
+
| `currentTrace()` | Current trace context `{ traceId, startTime }`. |
|
|
74
|
+
| `runWithTrace(id, fn)` | Run `fn` in a trace context. Auto-generates UUID if id is null. |
|
|
75
|
+
| `traceElapsed()` | Milliseconds since trace start. |
|
|
76
|
+
| `trace(opts?)` | Middleware that injects `ctx.trace`. |
|
|
115
77
|
|
|
116
|
-
|
|
117
|
-
|---|---|---|
|
|
118
|
-
| `postgres(opts?)` | middleware+client | Create a Postgres client. `url`, `host`, `port`, `database`, `user`, `password`, `max`. |
|
|
119
|
-
| `MIGRATIONS_TABLE` | const | Migration table name: `_weifuwu_migrations`. |
|
|
78
|
+
### Postgres
|
|
120
79
|
|
|
121
80
|
```ts
|
|
81
|
+
import { postgres, MIGRATIONS_TABLE } from 'weifuwu'
|
|
82
|
+
|
|
122
83
|
const sql = postgres({ url: process.env.DATABASE_URL })
|
|
84
|
+
app.use(sql) // injects ctx.sql
|
|
123
85
|
|
|
124
|
-
|
|
125
|
-
app.use(sql)
|
|
126
|
-
// As query client
|
|
127
|
-
const rows = await sql.client`SELECT * FROM users WHERE id = ${id}`
|
|
86
|
+
const rows = await sql.sql`SELECT * FROM users WHERE id = ${id}`
|
|
128
87
|
|
|
129
88
|
// Migrations
|
|
130
89
|
await sql.migrate({
|
|
131
|
-
'001_init': async (s) =>
|
|
132
|
-
await s`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`
|
|
133
|
-
},
|
|
90
|
+
'001_init': async (s) => await s`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`,
|
|
134
91
|
})
|
|
135
92
|
```
|
|
136
93
|
|
|
137
94
|
### Redis
|
|
138
95
|
|
|
139
|
-
| Export | Kind | Description |
|
|
140
|
-
|---|---|---|
|
|
141
|
-
| `redis(opts?)` | middleware+client | Create a Redis client (ioredis). `url`, `host`, `port`, `password`, `db`. |
|
|
142
|
-
|
|
143
96
|
```ts
|
|
97
|
+
import { redis } from 'weifuwu'
|
|
98
|
+
|
|
144
99
|
const r = redis({ url: process.env.REDIS_URL })
|
|
145
100
|
app.use(r) // injects ctx.redis
|
|
146
|
-
await r.
|
|
101
|
+
await r.redis.set('key', 'value')
|
|
147
102
|
```
|
|
148
103
|
|
|
149
104
|
### Queue & Cron
|
|
150
105
|
|
|
151
|
-
| Export | Kind | Description |
|
|
152
|
-
|---|---|---|
|
|
153
|
-
| `queue(opts?)` | middleware+instance | In-process job queue with cron support. `concurrency`. |
|
|
154
|
-
|
|
155
106
|
```ts
|
|
156
|
-
|
|
107
|
+
import { queue } from 'weifuwu'
|
|
108
|
+
|
|
109
|
+
const q = queue()
|
|
157
110
|
app.use(q) // injects ctx.queue
|
|
158
111
|
|
|
159
|
-
q.process('
|
|
160
|
-
await sendEmail(job.
|
|
112
|
+
q.process('email', async (job) => {
|
|
113
|
+
await sendEmail(job.payload.to)
|
|
161
114
|
})
|
|
162
115
|
|
|
163
116
|
q.cron('cleanup', '0 3 * * *', async () => {
|
|
164
117
|
await cleanupOldSessions()
|
|
165
118
|
})
|
|
166
119
|
|
|
167
|
-
await q.add('
|
|
120
|
+
await q.add('email', { to: 'user@example.com' })
|
|
121
|
+
await q.add('remind', {}, { delay: 60_000 })
|
|
122
|
+
await q.add('report', {}, { schedule: '0 9 * * 1' })
|
|
123
|
+
|
|
124
|
+
q.run() // start processing
|
|
168
125
|
```
|
|
169
126
|
|
|
170
127
|
### WebSocket Hub
|
|
171
128
|
|
|
172
|
-
| Export | Kind | Description |
|
|
173
|
-
|---|---|---|
|
|
174
|
-
| `createHub(opts?)` | function | Pub/Sub hub for cross-connection broadcast. Supports Redis adapter for multi-process. |
|
|
175
|
-
|
|
176
129
|
```ts
|
|
130
|
+
import { createHub } from 'weifuwu'
|
|
131
|
+
|
|
177
132
|
const hub = createHub()
|
|
178
133
|
app.ws('/chat', {
|
|
179
134
|
open(ws, ctx) {
|
|
180
135
|
ctx.ws.join('lobby')
|
|
181
|
-
ctx.ws.json({ type: 'join'
|
|
136
|
+
ctx.ws.json({ type: 'join' })
|
|
182
137
|
},
|
|
183
138
|
message(ws, ctx, data) {
|
|
184
|
-
ctx.ws.sendRoom('lobby', {
|
|
139
|
+
ctx.ws.sendRoom('lobby', { text: data.toString() })
|
|
185
140
|
},
|
|
186
141
|
})
|
|
187
142
|
```
|
|
188
143
|
|
|
189
|
-
###
|
|
144
|
+
### GraphQL
|
|
190
145
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
146
|
+
```ts
|
|
147
|
+
import { graphql } from 'weifuwu'
|
|
148
|
+
|
|
149
|
+
const gql = graphql((req, ctx) => ({
|
|
150
|
+
schema: `
|
|
151
|
+
type Query {
|
|
152
|
+
hello: String
|
|
153
|
+
user(id: ID!): User
|
|
154
|
+
}
|
|
155
|
+
type User {
|
|
156
|
+
id: ID
|
|
157
|
+
name: String
|
|
158
|
+
}
|
|
159
|
+
`,
|
|
160
|
+
resolvers: {
|
|
161
|
+
Query: {
|
|
162
|
+
hello: () => 'world',
|
|
163
|
+
user: (_, { id }) => ctx.sql`SELECT * FROM users WHERE id = ${id}`.then(r => r[0]),
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
graphiql: true,
|
|
167
|
+
}))
|
|
197
168
|
|
|
198
|
-
|
|
169
|
+
app.mount('/graphql', gql)
|
|
170
|
+
```
|
|
199
171
|
|
|
200
|
-
|
|
201
|
-
|---|---|---|
|
|
202
|
-
| `currentTraceId()` | function | Get current request's trace ID (from AsyncLocalStorage). |
|
|
203
|
-
| `currentTrace()` | function | Get current trace context `{ traceId, startTime }`. |
|
|
204
|
-
| `runWithTrace(id, fn)` | function | Run `fn` with a trace context. Auto-generates ID if null. |
|
|
205
|
-
| `traceElapsed()` | function | Milliseconds since trace start. |
|
|
172
|
+
### React SSR
|
|
206
173
|
|
|
207
|
-
|
|
174
|
+
> Requires `react >= 19`, `react-dom >= 19` (optional peerDependencies).
|
|
208
175
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
176
|
+
```bash
|
|
177
|
+
npm install react react-dom
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { react, Link, useServerData, ErrorBoundary } from 'weifuwu'
|
|
182
|
+
import { createElement as h } from 'react'
|
|
183
|
+
|
|
184
|
+
// ── Server ──────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
app.use(react({
|
|
187
|
+
layout: ({ children }) =>
|
|
188
|
+
h('html', null,
|
|
189
|
+
h('head', null, h('title', null, 'My App')),
|
|
190
|
+
h('body', null, h('div', { id: 'root' }, children)),
|
|
191
|
+
),
|
|
192
|
+
}))
|
|
193
|
+
|
|
194
|
+
// Render React components to HTML
|
|
195
|
+
app.get('/', (_req, ctx) =>
|
|
196
|
+
ctx.render(h('h1', null, 'Hello SSR'), {
|
|
197
|
+
head: { title: 'Home' },
|
|
198
|
+
data: { greeting: 'Hello' },
|
|
199
|
+
})
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// Streaming SSR
|
|
203
|
+
app.get('/dashboard', (_req, ctx) =>
|
|
204
|
+
ctx.renderStream(h(Dashboard))
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
// Layout nesting via mount
|
|
208
|
+
const admin = new Router()
|
|
209
|
+
admin.use(react({ layout: AdminLayout }))
|
|
210
|
+
admin.get('/dashboard', (_req, ctx) => ctx.render(h(AdminDashboard)))
|
|
211
|
+
app.mount('/admin', admin)
|
|
212
|
+
|
|
213
|
+
// ── Shared components ──────────────────────────────────
|
|
214
|
+
|
|
215
|
+
// Use the same component on server and client
|
|
216
|
+
function Page() {
|
|
217
|
+
const { greeting } = useServerData<{ greeting: string }>()
|
|
218
|
+
return h('h1', null, greeting)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Link works on both sides: <a> on server, SPA <a> on client
|
|
222
|
+
function Nav() {
|
|
223
|
+
return h('nav', null,
|
|
224
|
+
h(Link, { href: '/' }, 'Home'),
|
|
225
|
+
h(Link, { href: '/users' }, 'Users'),
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ErrorBoundary catches client-side render errors
|
|
230
|
+
function SafeUserProfile() {
|
|
231
|
+
return h(ErrorBoundary, { fallback: h('div', null, 'Error') },
|
|
232
|
+
h(UserProfile),
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Client (SPA navigation) ────────────────────────────
|
|
237
|
+
|
|
238
|
+
// client.ts — bundled separately with esbuild
|
|
239
|
+
import { hydrate, createClientRouter, defineRoute } from 'weifuwu/react/client'
|
|
240
|
+
|
|
241
|
+
const userRoute = defineRoute({
|
|
242
|
+
path: '/users/:id',
|
|
243
|
+
component: UserPage,
|
|
244
|
+
loader: (params) => fetch(`/users/${params.id}?_data`).then(r => r.json()),
|
|
245
|
+
})
|
|
213
246
|
|
|
214
|
-
|
|
247
|
+
const router = createClientRouter([
|
|
248
|
+
{ path: '/', component: HomePage },
|
|
249
|
+
userRoute,
|
|
250
|
+
])
|
|
215
251
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
|
252
|
+
hydrate(router.App)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Key concepts:**
|
|
256
|
+
|
|
257
|
+
| Feature | Description |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `ctx.render(el, opts)` | Render React to HTML. Auto-detects `?_data` → returns JSON. |
|
|
260
|
+
| `ctx.renderStream(el)` | Streaming SSR via `renderToReadableStream`. |
|
|
261
|
+
| `useServerData<T>()` | Access data on both server (via `ctx.render({ data })`) and client (via `loader`). |
|
|
262
|
+
| `Link` | Shared component — `<a>` on server, SPA `<a>` on client. |
|
|
263
|
+
| `Form` | Shared — `<form>` on server, `fetch` + revalidate on client. |
|
|
264
|
+
| `ErrorBoundary` | Catches render errors on client (SSR uses `onError`). |
|
|
265
|
+
| `useNavigation()` | `{ state: 'idle' \| 'loading' }` for progress indicators. |
|
|
266
|
+
| `useParams()` / `useNavigate()` / `useRevalidate()` | Router hooks. |
|
|
267
|
+
| `defineRoute()` | Type-safe route config — captures loader return type. |
|
|
268
|
+
| `createClientRouter()` | Client-side SPA router with loader-based data fetching. |
|
|
269
|
+
| `hydrate(App)` | Hydrate server-rendered HTML on the client. |
|
|
270
|
+
| `head: { title, meta }` | Inject dynamic `<title>` and `<meta>` tags. |
|
|
271
|
+
|
|
272
|
+
**Import paths:**
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
// Server-side
|
|
276
|
+
import { react, Link, Form, ErrorBoundary, useServerData } from 'weifuwu'
|
|
277
|
+
|
|
278
|
+
// Client-side (for browser bundles)
|
|
279
|
+
import { hydrate, createClientRouter, defineRoute, Link } from 'weifuwu/react/client'
|
|
280
|
+
|
|
281
|
+
// Shared primitives (safe for both, pure React — no react-dom import)
|
|
282
|
+
import { Link, Form, useServerData, useParams, useNavigation } from 'weifuwu/react/navigation'
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
See [examples/react-ssr/](examples/react-ssr/) for a complete demo.
|
|
222
286
|
|
|
223
287
|
### Types
|
|
224
288
|
|
|
225
289
|
| Export | Description |
|
|
226
290
|
|---|---|
|
|
227
|
-
| `Context` | `{ params, query, mountPath?,
|
|
291
|
+
| `Context` | `{ params, query, mountPath?, [key: string] }` |
|
|
228
292
|
| `Handler<T>` | `(req: Request, ctx: T) => Response \| Promise<Response>` |
|
|
229
|
-
| `Middleware
|
|
293
|
+
| `Middleware` | `(req, ctx, next) => Response \| Promise<Response>` |
|
|
230
294
|
| `WebSocketHandler` | `{ open?, message?, close?, error? }` |
|
|
231
295
|
| `HttpError` | `Error` subclass with `.status: number` |
|
|
232
296
|
| `Closeable` | `{ close(): Promise<void> }` |
|
|
233
|
-
| `Server` | `{
|
|
297
|
+
| `Server` | `{ close, port, hostname, ready }` |
|
|
234
298
|
| `ServeOptions` | `{ port, hostname, signal, maxBodySize, timeout, keepAliveTimeout, headersTimeout, shutdown }` |
|
|
235
299
|
|
|
236
300
|
## Handler / Middleware patterns
|
|
@@ -238,14 +302,13 @@ app.ws('/chat', {
|
|
|
238
302
|
```ts
|
|
239
303
|
// Handler — standard Web API
|
|
240
304
|
app.get('/api/data', (req, ctx) => {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
return Response.json({ q, id })
|
|
305
|
+
const q = ctx.query.q
|
|
306
|
+
return Response.json({ q })
|
|
244
307
|
})
|
|
245
308
|
|
|
246
309
|
// Async
|
|
247
310
|
app.get('/db/users', async (req, ctx) => {
|
|
248
|
-
const rows = await sql`SELECT * FROM users`
|
|
311
|
+
const rows = await ctx.sql`SELECT * FROM users`
|
|
249
312
|
return Response.json(rows)
|
|
250
313
|
})
|
|
251
314
|
|
|
@@ -264,15 +327,11 @@ app.use(async (req, ctx, next) => {
|
|
|
264
327
|
return res
|
|
265
328
|
})
|
|
266
329
|
|
|
267
|
-
// Route-level middleware (before handler)
|
|
268
|
-
app.get('/admin', validate(bodySchema), authCheck, adminHandler)
|
|
269
|
-
|
|
270
330
|
// Error handler
|
|
271
331
|
app.onError((err, req, ctx) => {
|
|
272
332
|
if (err instanceof HttpError) {
|
|
273
333
|
return Response.json({ error: err.message }, { status: err.status })
|
|
274
334
|
}
|
|
275
|
-
console.error(err)
|
|
276
335
|
return new Response('Internal error', { status: 500 })
|
|
277
336
|
})
|
|
278
337
|
```
|
|
@@ -280,85 +339,52 @@ app.onError((err, req, ctx) => {
|
|
|
280
339
|
## Complete example
|
|
281
340
|
|
|
282
341
|
```ts
|
|
283
|
-
import { serve, Router,
|
|
284
|
-
|
|
285
|
-
loadEnv()
|
|
342
|
+
import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, queue, HttpError } from 'weifuwu'
|
|
286
343
|
|
|
287
344
|
const app = new Router()
|
|
288
345
|
|
|
289
346
|
// Global middleware
|
|
290
|
-
app.use(
|
|
347
|
+
app.use(trace())
|
|
291
348
|
app.use(logger())
|
|
292
349
|
app.use(cors())
|
|
293
350
|
app.use(helmet())
|
|
294
351
|
app.use(compress())
|
|
295
352
|
app.use(rateLimit({ windowMs: 60_000, max: 100 }))
|
|
296
353
|
|
|
297
|
-
// Database
|
|
298
|
-
const sql = postgres(
|
|
299
|
-
const cache = redis({ url: process.env.REDIS_URL })
|
|
300
|
-
const jobs = queue()
|
|
301
|
-
|
|
354
|
+
// Database
|
|
355
|
+
const sql = postgres()
|
|
302
356
|
app.use(sql)
|
|
303
|
-
app.use(cache)
|
|
304
|
-
app.use(jobs)
|
|
305
357
|
|
|
306
358
|
// Routes
|
|
307
|
-
app.get('/', () => html`<h1>weifuwu</h1><p>Running on ${process.version}</p>`)
|
|
308
|
-
|
|
309
359
|
app.get('/api/users', async (req, ctx) => {
|
|
310
|
-
const users = await ctx.sql`SELECT id, name
|
|
360
|
+
const users = await ctx.sql`SELECT id, name FROM users ORDER BY id`
|
|
311
361
|
return Response.json(users)
|
|
312
362
|
})
|
|
313
363
|
|
|
314
|
-
app.get('/api/users/:id', async (req, ctx) => {
|
|
315
|
-
const [user] = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
|
|
316
|
-
if (!user) throw new HttpError('User not found', 404)
|
|
317
|
-
return Response.json(user)
|
|
318
|
-
})
|
|
319
|
-
|
|
320
364
|
app.post('/api/users', async (req, ctx) => {
|
|
321
365
|
const { name, email } = await req.json()
|
|
322
|
-
const [user] = await ctx.sql`INSERT INTO users (name,
|
|
366
|
+
const [user] = await ctx.sql`INSERT INTO users (name,email) VALUES (${name},${email}) RETURNING *`
|
|
323
367
|
return Response.json(user, { status: 201 })
|
|
324
368
|
})
|
|
325
369
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
await sendWelcomeEmail(user.email)
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
app.post('/api/register', async (req, ctx) => {
|
|
334
|
-
const { name, email } = await req.json()
|
|
335
|
-
const [user] = await ctx.sql`INSERT INTO users (name, email) VALUES (${name}, ${email}) RETURNING *`
|
|
336
|
-
await ctx.queue.add('welcome-email', { userId: user.id })
|
|
337
|
-
return Response.json(user, { status: 201 })
|
|
370
|
+
app.get('/api/users/:id', async (req, ctx) => {
|
|
371
|
+
const [user] = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
|
|
372
|
+
if (!user) throw new HttpError('Not found', 404)
|
|
373
|
+
return Response.json(user)
|
|
338
374
|
})
|
|
339
375
|
|
|
340
|
-
// WebSocket
|
|
376
|
+
// WebSocket
|
|
341
377
|
app.ws('/chat', {
|
|
342
|
-
open(ws, ctx) {
|
|
343
|
-
|
|
344
|
-
},
|
|
345
|
-
message(ws, ctx, data) {
|
|
346
|
-
ctx.ws.sendRoom('lobby', { text: data.toString() })
|
|
347
|
-
},
|
|
378
|
+
open(ws, ctx) { ctx.ws.join('lobby') },
|
|
379
|
+
message(ws, ctx, data) { ctx.ws.sendRoom('lobby', { text: data.toString() }) },
|
|
348
380
|
})
|
|
349
381
|
|
|
350
382
|
// Error handling
|
|
351
|
-
app.onError((err
|
|
352
|
-
if (err instanceof HttpError) {
|
|
353
|
-
return Response.json({ error: err.message }, { status: err.status })
|
|
354
|
-
}
|
|
383
|
+
app.onError((err) => {
|
|
384
|
+
if (err instanceof HttpError) return Response.json({ error: err.message }, { status: err.status })
|
|
355
385
|
return Response.json({ error: 'Internal error' }, { status: 500 })
|
|
356
386
|
})
|
|
357
387
|
|
|
358
|
-
// Health check
|
|
359
|
-
import { health } from 'weifuwu'
|
|
360
|
-
app.mount('/_health', health())
|
|
361
|
-
|
|
362
388
|
serve(app, { port: 3000 })
|
|
363
389
|
```
|
|
364
390
|
|
|
@@ -368,27 +394,31 @@ serve(app, { port: 3000 })
|
|
|
368
394
|
weifuwu/
|
|
369
395
|
├── package.json
|
|
370
396
|
├── tsconfig.json
|
|
397
|
+
├── docker-compose.yml ← postgres + redis for tests
|
|
371
398
|
├── scripts/
|
|
372
399
|
│ ├── build.mjs
|
|
373
400
|
│ └── release.mjs
|
|
374
401
|
├── src/
|
|
375
|
-
│ ├── index.ts
|
|
376
|
-
│ ├── types.ts
|
|
377
|
-
├── core/
|
|
378
|
-
│ ├── middleware/
|
|
379
|
-
├── postgres/
|
|
380
|
-
│ ├── redis/
|
|
381
|
-
│ ├──
|
|
382
|
-
│ ├──
|
|
383
|
-
│ ├──
|
|
384
|
-
│
|
|
402
|
+
│ ├── index.ts
|
|
403
|
+
│ ├── types.ts
|
|
404
|
+
│ ├── core/ ← serve, router, ws, trace, logger
|
|
405
|
+
│ ├── middleware/ ← cors, helmet, compress, rate-limit, static, upload
|
|
406
|
+
│ ├── postgres/
|
|
407
|
+
│ ├── redis/
|
|
408
|
+
│ ├── react/ ← react SSR (render, navigation, client)
|
|
409
|
+
│ ├── queue/ ← cron, index, types
|
|
410
|
+
│ ├── graphql.ts
|
|
411
|
+
│ ├── hub.ts
|
|
412
|
+
│ └── test/ ← 131 tests (18 files)
|
|
413
|
+
├── examples/
|
|
414
|
+
│ └── react-ssr/ ← full SPA demo
|
|
385
415
|
└── dist/
|
|
386
416
|
```
|
|
387
417
|
|
|
388
418
|
## Development
|
|
389
419
|
|
|
390
420
|
```bash
|
|
391
|
-
npm run build # esbuild
|
|
421
|
+
npm run build # esbuild → dist/index.js
|
|
392
422
|
npm run typecheck # tsc --noEmit
|
|
393
|
-
npm test #
|
|
423
|
+
npm test # 131 tests (requires docker compose)
|
|
394
424
|
```
|