weifuwu 0.30.0 → 0.30.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -203
- 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 +2 -24
- package/dist/index.js +396 -1880
- package/dist/queue/index.d.ts +10 -0
- package/dist/queue/types.d.ts +5 -9
- package/package.json +4 -5
- package/dist/ai/provider.d.ts +0 -45
- package/dist/ai/stream.d.ts +0 -13
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -178
- 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/react/index.js +0 -2573
- 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,145 @@ 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
|
|
115
69
|
|
|
116
|
-
| Export |
|
|
117
|
-
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
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`. |
|
|
77
|
+
|
|
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
|
-
###
|
|
190
|
-
|
|
191
|
-
| Export | Kind | Description |
|
|
192
|
-
|---|---|---|
|
|
193
|
-
| `theme(opts?)` | middleware+Router | Theme middleware. Injects `ctx.theme` with `get()`, `set(theme)`, `toggle()`. Routes: `/__theme/:theme`. |
|
|
194
|
-
| `i18n(opts?)` | middleware+Router | I18n middleware. Injects `ctx.i18n` with `locale`, `t(key, params?)`, `set(locale)`. `dir` for locale JSON files. Routes: `/__lang/:locale`. |
|
|
195
|
-
| `flash()` | middleware | Flash messages. Injects `ctx.flash` with `set(msg)`, `get()`. One-time messages across redirects. |
|
|
196
|
-
| `csrf()` | middleware | CSRF protection. Injects `ctx.csrf` with `token`. Auto-validates on `POST`/`PUT`/`PATCH`/`DELETE`. |
|
|
144
|
+
### GraphQL
|
|
197
145
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
}))
|
|
215
168
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
| `testApp(app)` | function | Create a test client for a Router. Returns `{ get, post, put, delete, patch }` for `(path, opts?)`. |
|
|
219
|
-
| `createTestServer(app, opts?)` | function | Start a test server on a random port. Returns `{ server, url }`. |
|
|
220
|
-
| `createTestDb()` | function | Create an ephemeral Postgres test database. |
|
|
221
|
-
| `withTestDb(fn)` | function | Run `fn(sql)` with a temp database, auto-cleanup. |
|
|
169
|
+
app.mount('/graphql', gql)
|
|
170
|
+
```
|
|
222
171
|
|
|
223
172
|
### Types
|
|
224
173
|
|
|
225
174
|
| Export | Description |
|
|
226
175
|
|---|---|
|
|
227
|
-
| `Context` | `{ params, query, mountPath?,
|
|
176
|
+
| `Context` | `{ params, query, mountPath?, [key: string] }` |
|
|
228
177
|
| `Handler<T>` | `(req: Request, ctx: T) => Response \| Promise<Response>` |
|
|
229
|
-
| `Middleware
|
|
178
|
+
| `Middleware` | `(req, ctx, next) => Response \| Promise<Response>` |
|
|
230
179
|
| `WebSocketHandler` | `{ open?, message?, close?, error? }` |
|
|
231
180
|
| `HttpError` | `Error` subclass with `.status: number` |
|
|
232
181
|
| `Closeable` | `{ close(): Promise<void> }` |
|
|
233
|
-
| `Server` | `{
|
|
182
|
+
| `Server` | `{ close, port, hostname, ready }` |
|
|
234
183
|
| `ServeOptions` | `{ port, hostname, signal, maxBodySize, timeout, keepAliveTimeout, headersTimeout, shutdown }` |
|
|
235
184
|
|
|
236
185
|
## Handler / Middleware patterns
|
|
@@ -238,14 +187,13 @@ app.ws('/chat', {
|
|
|
238
187
|
```ts
|
|
239
188
|
// Handler — standard Web API
|
|
240
189
|
app.get('/api/data', (req, ctx) => {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
return Response.json({ q, id })
|
|
190
|
+
const q = ctx.query.q
|
|
191
|
+
return Response.json({ q })
|
|
244
192
|
})
|
|
245
193
|
|
|
246
194
|
// Async
|
|
247
195
|
app.get('/db/users', async (req, ctx) => {
|
|
248
|
-
const rows = await sql`SELECT * FROM users`
|
|
196
|
+
const rows = await ctx.sql`SELECT * FROM users`
|
|
249
197
|
return Response.json(rows)
|
|
250
198
|
})
|
|
251
199
|
|
|
@@ -264,15 +212,11 @@ app.use(async (req, ctx, next) => {
|
|
|
264
212
|
return res
|
|
265
213
|
})
|
|
266
214
|
|
|
267
|
-
// Route-level middleware (before handler)
|
|
268
|
-
app.get('/admin', validate(bodySchema), authCheck, adminHandler)
|
|
269
|
-
|
|
270
215
|
// Error handler
|
|
271
216
|
app.onError((err, req, ctx) => {
|
|
272
217
|
if (err instanceof HttpError) {
|
|
273
218
|
return Response.json({ error: err.message }, { status: err.status })
|
|
274
219
|
}
|
|
275
|
-
console.error(err)
|
|
276
220
|
return new Response('Internal error', { status: 500 })
|
|
277
221
|
})
|
|
278
222
|
```
|
|
@@ -280,85 +224,52 @@ app.onError((err, req, ctx) => {
|
|
|
280
224
|
## Complete example
|
|
281
225
|
|
|
282
226
|
```ts
|
|
283
|
-
import { serve, Router,
|
|
284
|
-
|
|
285
|
-
loadEnv()
|
|
227
|
+
import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, queue, HttpError } from 'weifuwu'
|
|
286
228
|
|
|
287
229
|
const app = new Router()
|
|
288
230
|
|
|
289
231
|
// Global middleware
|
|
290
|
-
app.use(
|
|
232
|
+
app.use(trace())
|
|
291
233
|
app.use(logger())
|
|
292
234
|
app.use(cors())
|
|
293
235
|
app.use(helmet())
|
|
294
236
|
app.use(compress())
|
|
295
237
|
app.use(rateLimit({ windowMs: 60_000, max: 100 }))
|
|
296
238
|
|
|
297
|
-
// Database
|
|
298
|
-
const sql = postgres(
|
|
299
|
-
const cache = redis({ url: process.env.REDIS_URL })
|
|
300
|
-
const jobs = queue()
|
|
301
|
-
|
|
239
|
+
// Database
|
|
240
|
+
const sql = postgres()
|
|
302
241
|
app.use(sql)
|
|
303
|
-
app.use(cache)
|
|
304
|
-
app.use(jobs)
|
|
305
242
|
|
|
306
243
|
// Routes
|
|
307
|
-
app.get('/', () => html`<h1>weifuwu</h1><p>Running on ${process.version}</p>`)
|
|
308
|
-
|
|
309
244
|
app.get('/api/users', async (req, ctx) => {
|
|
310
|
-
const users = await ctx.sql`SELECT id, name
|
|
245
|
+
const users = await ctx.sql`SELECT id, name FROM users ORDER BY id`
|
|
311
246
|
return Response.json(users)
|
|
312
247
|
})
|
|
313
248
|
|
|
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
249
|
app.post('/api/users', async (req, ctx) => {
|
|
321
250
|
const { name, email } = await req.json()
|
|
322
|
-
const [user] = await ctx.sql`INSERT INTO users (name,
|
|
251
|
+
const [user] = await ctx.sql`INSERT INTO users (name,email) VALUES (${name},${email}) RETURNING *`
|
|
323
252
|
return Response.json(user, { status: 201 })
|
|
324
253
|
})
|
|
325
254
|
|
|
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 })
|
|
255
|
+
app.get('/api/users/:id', async (req, ctx) => {
|
|
256
|
+
const [user] = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
|
|
257
|
+
if (!user) throw new HttpError('Not found', 404)
|
|
258
|
+
return Response.json(user)
|
|
338
259
|
})
|
|
339
260
|
|
|
340
|
-
// WebSocket
|
|
261
|
+
// WebSocket
|
|
341
262
|
app.ws('/chat', {
|
|
342
|
-
open(ws, ctx) {
|
|
343
|
-
|
|
344
|
-
},
|
|
345
|
-
message(ws, ctx, data) {
|
|
346
|
-
ctx.ws.sendRoom('lobby', { text: data.toString() })
|
|
347
|
-
},
|
|
263
|
+
open(ws, ctx) { ctx.ws.join('lobby') },
|
|
264
|
+
message(ws, ctx, data) { ctx.ws.sendRoom('lobby', { text: data.toString() }) },
|
|
348
265
|
})
|
|
349
266
|
|
|
350
267
|
// Error handling
|
|
351
|
-
app.onError((err
|
|
352
|
-
if (err instanceof HttpError) {
|
|
353
|
-
return Response.json({ error: err.message }, { status: err.status })
|
|
354
|
-
}
|
|
268
|
+
app.onError((err) => {
|
|
269
|
+
if (err instanceof HttpError) return Response.json({ error: err.message }, { status: err.status })
|
|
355
270
|
return Response.json({ error: 'Internal error' }, { status: 500 })
|
|
356
271
|
})
|
|
357
272
|
|
|
358
|
-
// Health check
|
|
359
|
-
import { health } from 'weifuwu'
|
|
360
|
-
app.mount('/_health', health())
|
|
361
|
-
|
|
362
273
|
serve(app, { port: 3000 })
|
|
363
274
|
```
|
|
364
275
|
|
|
@@ -368,27 +279,28 @@ serve(app, { port: 3000 })
|
|
|
368
279
|
weifuwu/
|
|
369
280
|
├── package.json
|
|
370
281
|
├── tsconfig.json
|
|
282
|
+
├── docker-compose.yml ← postgres + redis for tests
|
|
371
283
|
├── scripts/
|
|
372
284
|
│ ├── build.mjs
|
|
373
285
|
│ └── release.mjs
|
|
374
286
|
├── src/
|
|
375
|
-
│ ├── index.ts
|
|
376
|
-
│ ├── types.ts
|
|
377
|
-
├── core/
|
|
378
|
-
│ ├── middleware/
|
|
379
|
-
├── postgres/
|
|
380
|
-
│ ├── redis/
|
|
381
|
-
│ ├── queue/
|
|
382
|
-
│ ├── graphql.ts
|
|
383
|
-
│ ├── hub.ts
|
|
384
|
-
│ └── test/
|
|
287
|
+
│ ├── index.ts
|
|
288
|
+
│ ├── types.ts
|
|
289
|
+
│ ├── core/ ← serve, router, ws, trace, logger
|
|
290
|
+
│ ├── middleware/ ← cors, helmet, compress, rate-limit, static, upload
|
|
291
|
+
│ ├── postgres/
|
|
292
|
+
│ ├── redis/
|
|
293
|
+
│ ├── queue/ ← cron, index, types
|
|
294
|
+
│ ├── graphql.ts
|
|
295
|
+
│ ├── hub.ts
|
|
296
|
+
│ └── test/ ← 122 tests (18 files)
|
|
385
297
|
└── dist/
|
|
386
298
|
```
|
|
387
299
|
|
|
388
300
|
## Development
|
|
389
301
|
|
|
390
302
|
```bash
|
|
391
|
-
npm run build # esbuild
|
|
303
|
+
npm run build # esbuild → dist/index.js
|
|
392
304
|
npm run typecheck # tsc --noEmit
|
|
393
|
-
npm test #
|
|
305
|
+
npm test # 122 tests (requires docker compose)
|
|
394
306
|
```
|
package/dist/core/router.d.ts
CHANGED
|
@@ -1,30 +1,17 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { type IncomingMessage } from 'node:http';
|
|
3
|
-
import type { Duplex } from 'node:stream';
|
|
1
|
+
import type { Context, Handler, Middleware, ErrorHandler } from '../types.ts';
|
|
4
2
|
import { type Hub } from '../hub.ts';
|
|
3
|
+
import { type WebSocketHandler, type WsUpgradeHandler } from './ws.ts';
|
|
5
4
|
declare module '../types.ts' {
|
|
6
5
|
interface Context {
|
|
7
6
|
ws: {
|
|
8
|
-
/** Per-connection state object */
|
|
9
7
|
state: Record<string, unknown>;
|
|
10
|
-
/** Send JSON to this connection */
|
|
11
8
|
json(data: unknown): void;
|
|
12
|
-
/** Join a room */
|
|
13
9
|
join(room: string): void;
|
|
14
|
-
/** Leave a room */
|
|
15
10
|
leave(room: string): void;
|
|
16
|
-
/** Broadcast to a room */
|
|
17
11
|
sendRoom(room: string, data: unknown): void;
|
|
18
12
|
};
|
|
19
13
|
}
|
|
20
14
|
}
|
|
21
|
-
export type WebSocketHandler = {
|
|
22
|
-
open?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
|
|
23
|
-
message?: (ws: WebSocket, ctx: Context, data: string | Buffer) => void | Promise<void>;
|
|
24
|
-
close?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
|
|
25
|
-
error?: (ws: WebSocket, ctx: Context, error: Error) => void | Promise<void>;
|
|
26
|
-
};
|
|
27
|
-
type WsUpgradeHandler = (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
|
28
15
|
export declare class Router<T extends Context = Context> {
|
|
29
16
|
private root;
|
|
30
17
|
private wsRoot;
|
|
@@ -33,63 +20,39 @@ export declare class Router<T extends Context = Context> {
|
|
|
33
20
|
private _hasWildcard;
|
|
34
21
|
private _hub?;
|
|
35
22
|
private _wss?;
|
|
36
|
-
/** Track which ctx fields have been injected so far (for dependency checking). */
|
|
37
23
|
private _ctxFields;
|
|
38
24
|
private get wss();
|
|
39
25
|
private get hub();
|
|
40
|
-
/** Inject a custom hub (e.g. with Redis for cross-process broadcast). */
|
|
41
26
|
wsHub(hub: Hub): this;
|
|
42
|
-
|
|
43
|
-
use<Out extends Context>(mw: Middleware<Context, Out>): Router<T & Out>;
|
|
44
|
-
/**
|
|
45
|
-
* Mount a sub-router at the given path prefix.
|
|
46
|
-
* All routes from the sub-router are registered with the prefix.
|
|
47
|
-
*
|
|
48
|
-
* ```ts
|
|
49
|
-
* const admin = new Router()
|
|
50
|
-
* admin.get('/dashboard', handler)
|
|
51
|
-
* app.mount('/admin', admin) // → GET /admin/dashboard
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
27
|
+
use(mw: Middleware<Context, Context>): Router<T>;
|
|
54
28
|
mount(path: string, router: Router<Context>): Router<T>;
|
|
55
|
-
/**
|
|
56
|
-
* Check a middleware's dependency metadata and emit warnings if
|
|
57
|
-
* required fields haven't been injected yet.
|
|
58
|
-
* Attach __meta to a middleware function:
|
|
59
|
-
*
|
|
60
|
-
* ```ts
|
|
61
|
-
* mw.__meta = { injects: ['sql'], depends: ['session'] }
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
private _checkMiddlewareMeta;
|
|
65
|
-
get(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
|
|
66
|
-
post(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
|
|
67
|
-
put(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
|
|
68
|
-
delete(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
|
|
69
|
-
patch(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
|
|
70
|
-
head(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
|
|
71
|
-
options(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
|
|
72
|
-
all(path: string, ...args: [...Middleware<T, T>[], Handler<T> | Router<Context>]): Router<T>;
|
|
73
29
|
onError(handler: ErrorHandler<T>): Router<T>;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
30
|
+
get(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
|
|
31
|
+
post(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
|
|
32
|
+
put(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
|
|
33
|
+
delete(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
|
|
34
|
+
patch(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
|
|
35
|
+
head(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
|
|
36
|
+
options(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
|
|
37
|
+
all(path: string, ...rest: [...Middleware[], Handler | Router<Context>]): Router<T>;
|
|
38
|
+
ws(path: string, ...args: [...Middleware[], WebSocketHandler]): Router<T>;
|
|
78
39
|
handler(): Handler<T>;
|
|
79
|
-
/** Returns a human-readable list of all registered routes. Useful for debugging. */
|
|
80
|
-
routes(): string[];
|
|
81
|
-
private _collectRoutes;
|
|
82
|
-
private _collectWsRoutes;
|
|
83
40
|
websocketHandler(): WsUpgradeHandler;
|
|
41
|
+
routes(): string[];
|
|
42
|
+
private _route;
|
|
43
|
+
private _routeImpl;
|
|
84
44
|
private _mountRouter;
|
|
85
45
|
private _collect;
|
|
86
46
|
private _collectWs;
|
|
87
47
|
private splitPath;
|
|
48
|
+
private _collectRoutes;
|
|
49
|
+
private _collectWsRoutes;
|
|
88
50
|
private matchTrie;
|
|
51
|
+
private _wildcardMatch;
|
|
52
|
+
private _resolveMatch;
|
|
89
53
|
private matchWsTrie;
|
|
90
|
-
private handleError;
|
|
91
|
-
private _notFoundResponse;
|
|
92
54
|
private handle;
|
|
55
|
+
private handleError;
|
|
93
56
|
private runChain;
|
|
57
|
+
private _checkMiddlewareMeta;
|
|
94
58
|
}
|
|
95
|
-
export {};
|
package/dist/core/serve.d.ts
CHANGED
|
@@ -29,8 +29,4 @@ export declare function createRequest(req: IncomingMessage, body: Buffer): [Requ
|
|
|
29
29
|
export declare function sendResponse(res: ServerResponse, response: Response, opts?: {
|
|
30
30
|
traceId?: string | null;
|
|
31
31
|
}): Promise<void>;
|
|
32
|
-
export declare function createTestServer(router: Router, options?: ServeOptions): Promise<{
|
|
33
|
-
server: Server;
|
|
34
|
-
url: string;
|
|
35
|
-
}>;
|
|
36
32
|
export declare function serve(router: Router, options?: ServeOptions): Server;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket upgrade + connection lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Handles the HTTP-to-WS upgrade and per-connection state.
|
|
5
|
+
* Used internally by Router — not exported to end users.
|
|
6
|
+
*/
|
|
7
|
+
import { WebSocketServer } from 'ws';
|
|
8
|
+
import type { WebSocket, Context, Handler, Middleware } from '../types.ts';
|
|
9
|
+
import type { IncomingMessage } from 'node:http';
|
|
10
|
+
import type { Duplex } from 'node:stream';
|
|
11
|
+
import { type Hub } from '../hub.ts';
|
|
12
|
+
/** WebSocket lifecycle handler. */
|
|
13
|
+
export type WebSocketHandler = {
|
|
14
|
+
open?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
|
|
15
|
+
message?: (ws: WebSocket, ctx: Context, data: string | Buffer) => void | Promise<void>;
|
|
16
|
+
close?: (ws: WebSocket, ctx: Context) => void | Promise<void>;
|
|
17
|
+
error?: (ws: WebSocket, ctx: Context, error: Error) => void | Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
export type WsUpgradeHandler = (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
|
20
|
+
export declare function nodeReqHeadersToRecord(headers: IncomingMessage['headers']): Record<string, string>;
|
|
21
|
+
export declare function upgradeSocket(wss: WebSocketServer, req: IncomingMessage, socket: Duplex, head: Buffer, handler: WebSocketHandler, ctx: Context, hub: Hub): void;
|
|
22
|
+
type WsMatch = {
|
|
23
|
+
handler: WebSocketHandler;
|
|
24
|
+
middlewares: Middleware[];
|
|
25
|
+
params: Record<string, string>;
|
|
26
|
+
};
|
|
27
|
+
export declare function createWsUpgradeHandler(wss: WebSocketServer, hub: Hub, matchWsFn: (segments: string[]) => WsMatch | null, globalMws: Middleware[], runChainFn: (mws: Middleware[], h: Handler, req: Request, ctx: Context) => Promise<Response>): WsUpgradeHandler;
|
|
28
|
+
export {};
|