weifuwu 0.28.2 → 0.30.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 +394 -0
- package/dist/core/cookie.d.ts +36 -0
- package/dist/core/env.d.ts +69 -0
- package/dist/core/html.d.ts +34 -0
- package/dist/core/logger.d.ts +16 -0
- package/dist/core/router.d.ts +95 -0
- package/dist/core/serve.d.ts +36 -0
- package/dist/core/sse.d.ts +47 -0
- package/dist/core/trace.d.ts +95 -0
- package/dist/graphql.d.ts +16 -0
- package/dist/hub.d.ts +36 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +3387 -0
- package/dist/middleware/compress.d.ts +20 -0
- package/dist/middleware/cors.d.ts +25 -0
- package/dist/middleware/csrf.d.ts +31 -0
- package/dist/middleware/flash.d.ts +44 -0
- package/dist/middleware/health.d.ts +24 -0
- package/dist/middleware/helmet.d.ts +33 -0
- package/dist/middleware/i18n.d.ts +39 -0
- package/dist/middleware/rate-limit.d.ts +44 -0
- package/dist/middleware/request-id.d.ts +40 -0
- package/dist/middleware/static.d.ts +23 -0
- package/dist/middleware/theme.d.ts +31 -0
- package/dist/middleware/upload.d.ts +55 -0
- package/dist/middleware/validate.d.ts +32 -0
- package/dist/postgres/client.d.ts +4 -0
- package/dist/postgres/index.d.ts +3 -0
- package/dist/postgres/module.d.ts +12 -0
- package/dist/postgres/types.d.ts +42 -0
- package/dist/queue/cron.d.ts +9 -0
- package/dist/queue/index.d.ts +2 -0
- package/dist/queue/types.d.ts +61 -0
- package/dist/redis/client.d.ts +2 -0
- package/dist/redis/index.d.ts +2 -0
- package/dist/redis/types.d.ts +17 -0
- package/dist/test/test-utils.d.ts +193 -0
- package/dist/types.d.ts +82 -0
- package/package.json +31 -9
- package/index.d.ts +0 -8
- package/index.js +0 -26
package/README.md
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# weifuwu
|
|
2
|
+
|
|
3
|
+
Web-standard HTTP microframework for Node.js — `(req, ctx) => Response`.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install weifuwu
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { serve, Router } from 'weifuwu'
|
|
13
|
+
|
|
14
|
+
const app = new Router()
|
|
15
|
+
app.get('/', () => new Response('Hello'))
|
|
16
|
+
serve(app, { port: 3000 })
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Exports
|
|
20
|
+
|
|
21
|
+
Everything is exported from `'weifuwu'`:
|
|
22
|
+
|
|
23
|
+
### Core
|
|
24
|
+
|
|
25
|
+
| Export | Kind | Description |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `serve(app, opts?)` | function | Start HTTP server. Takes a `Router`. Returns `Server`. |
|
|
28
|
+
| `Router` | class | Trie-based HTTP router with WebSocket support. |
|
|
29
|
+
| `loadEnv(path?)` | function | Load `.env` file into `process.env`. |
|
|
30
|
+
| `isDev()` | function | `true` unless `NODE_ENV` is `production` or `test`. |
|
|
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). |
|
|
35
|
+
|
|
36
|
+
### Router
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const app = new Router()
|
|
40
|
+
|
|
41
|
+
// HTTP methods
|
|
42
|
+
app.get(path, ...handlers)
|
|
43
|
+
app.post(path, ...handlers)
|
|
44
|
+
app.put(path, ...handlers)
|
|
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
|
|
50
|
+
|
|
51
|
+
// WebSocket
|
|
52
|
+
app.ws(path, ...middlewares, handler)
|
|
53
|
+
// handler: { open?, message?, close?, error? }
|
|
54
|
+
|
|
55
|
+
// Middleware & mounting
|
|
56
|
+
app.use(middleware) // global middleware
|
|
57
|
+
app.mount(prefix, router) // mount sub-router at prefix
|
|
58
|
+
app.onError(handler) // global error handler
|
|
59
|
+
app.routes() // debug: string[] of all registered routes
|
|
60
|
+
|
|
61
|
+
// Path params
|
|
62
|
+
app.get('/users/:id', (req, ctx) => {
|
|
63
|
+
const id = ctx.params.id // string
|
|
64
|
+
const q = ctx.query.search // from ?search=...
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Middleware
|
|
69
|
+
|
|
70
|
+
| Export | Kind | Description |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `cors(opts?)` | middleware | CORS headers. `origin`, `methods`, `headers`, `credentials`, `maxAge`. |
|
|
73
|
+
| `helmet(opts?)` | middleware | Security headers (CSP, HSTS, X-Frame-Options, etc). |
|
|
74
|
+
| `compress(opts?)` | middleware | gzip / brotli / deflate response compression. |
|
|
75
|
+
| `rateLimit(opts?)` | middleware | Sliding-window rate limiter. `windowMs`, `max`, `keyGenerator`. |
|
|
76
|
+
| `requestId(opts?)` | middleware | Generates `X-Request-ID` header. `header`, `generator`. |
|
|
77
|
+
| `logger(opts?)` | middleware | Structured request logging. `format`, `level`, `stream`. |
|
|
78
|
+
| `validate(schemas?)` | middleware | Zod-based request validation. `{ body?, query?, params?, headers? }`. |
|
|
79
|
+
| `upload(opts?)` | middleware | Multipart file upload. `maxSize`, `dest`, `preservePath`. |
|
|
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). |
|
|
113
|
+
|
|
114
|
+
### Postgres
|
|
115
|
+
|
|
116
|
+
| Export | Kind | Description |
|
|
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`. |
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const sql = postgres({ url: process.env.DATABASE_URL })
|
|
123
|
+
|
|
124
|
+
// As middleware — injects ctx.sql
|
|
125
|
+
app.use(sql)
|
|
126
|
+
// As query client
|
|
127
|
+
const rows = await sql.client`SELECT * FROM users WHERE id = ${id}`
|
|
128
|
+
|
|
129
|
+
// Migrations
|
|
130
|
+
await sql.migrate({
|
|
131
|
+
'001_init': async (s) => {
|
|
132
|
+
await s`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Redis
|
|
138
|
+
|
|
139
|
+
| Export | Kind | Description |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `redis(opts?)` | middleware+client | Create a Redis client (ioredis). `url`, `host`, `port`, `password`, `db`. |
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const r = redis({ url: process.env.REDIS_URL })
|
|
145
|
+
app.use(r) // injects ctx.redis
|
|
146
|
+
await r.client.set('key', 'value')
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Queue & Cron
|
|
150
|
+
|
|
151
|
+
| Export | Kind | Description |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `queue(opts?)` | middleware+instance | In-process job queue with cron support. `concurrency`. |
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const q = queue({ concurrency: 3 })
|
|
157
|
+
app.use(q) // injects ctx.queue
|
|
158
|
+
|
|
159
|
+
q.process('send-email', async (job) => {
|
|
160
|
+
await sendEmail(job.data.to)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
q.cron('cleanup', '0 3 * * *', async () => {
|
|
164
|
+
await cleanupOldSessions()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
await q.add('send-email', { to: 'user@example.com' })
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### WebSocket Hub
|
|
171
|
+
|
|
172
|
+
| Export | Kind | Description |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| `createHub(opts?)` | function | Pub/Sub hub for cross-connection broadcast. Supports Redis adapter for multi-process. |
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const hub = createHub()
|
|
178
|
+
app.ws('/chat', {
|
|
179
|
+
open(ws, ctx) {
|
|
180
|
+
ctx.ws.join('lobby')
|
|
181
|
+
ctx.ws.json({ type: 'join', room: 'lobby' })
|
|
182
|
+
},
|
|
183
|
+
message(ws, ctx, data) {
|
|
184
|
+
ctx.ws.sendRoom('lobby', { type: 'chat', text: data.toString() })
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Theme & I18n
|
|
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`. |
|
|
197
|
+
|
|
198
|
+
### Tracing
|
|
199
|
+
|
|
200
|
+
| Export | Kind | Description |
|
|
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. |
|
|
206
|
+
|
|
207
|
+
### Error handling
|
|
208
|
+
|
|
209
|
+
| Export | Kind | Description |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `HttpError` | class | `new HttpError(message, status)`. Throw to return that status. |
|
|
212
|
+
| `app.onError(handler)` | Router method | Global error handler: `(error, req, ctx) => Response`. |
|
|
213
|
+
|
|
214
|
+
### Testing
|
|
215
|
+
|
|
216
|
+
| Export | Kind | Description |
|
|
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. |
|
|
222
|
+
|
|
223
|
+
### Types
|
|
224
|
+
|
|
225
|
+
| Export | Description |
|
|
226
|
+
|---|---|
|
|
227
|
+
| `Context` | `{ params, query, mountPath?, user?, loaderData?, env?, [key: string] }` |
|
|
228
|
+
| `Handler<T>` | `(req: Request, ctx: T) => Response \| Promise<Response>` |
|
|
229
|
+
| `Middleware<In, Out>` | `(req, ctx, next) => Response \| Promise<Response>` |
|
|
230
|
+
| `WebSocketHandler` | `{ open?, message?, close?, error? }` |
|
|
231
|
+
| `HttpError` | `Error` subclass with `.status: number` |
|
|
232
|
+
| `Closeable` | `{ close(): Promise<void> }` |
|
|
233
|
+
| `Server` | `{ stop, close, port, hostname, ready }` |
|
|
234
|
+
| `ServeOptions` | `{ port, hostname, signal, maxBodySize, timeout, keepAliveTimeout, headersTimeout, shutdown }` |
|
|
235
|
+
|
|
236
|
+
## Handler / Middleware patterns
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// Handler — standard Web API
|
|
240
|
+
app.get('/api/data', (req, ctx) => {
|
|
241
|
+
const { q } = ctx.query
|
|
242
|
+
const id = ctx.params.id
|
|
243
|
+
return Response.json({ q, id })
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Async
|
|
247
|
+
app.get('/db/users', async (req, ctx) => {
|
|
248
|
+
const rows = await sql`SELECT * FROM users`
|
|
249
|
+
return Response.json(rows)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
// Throwing HttpError
|
|
253
|
+
app.get('/item/:id', (req, ctx) => {
|
|
254
|
+
const item = db.get(ctx.params.id)
|
|
255
|
+
if (!item) throw new HttpError('Not found', 404)
|
|
256
|
+
return Response.json(item)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// Middleware
|
|
260
|
+
app.use(async (req, ctx, next) => {
|
|
261
|
+
const start = Date.now()
|
|
262
|
+
const res = await next(req, ctx)
|
|
263
|
+
console.log(`${req.method} ${req.url} ${Date.now() - start}ms`)
|
|
264
|
+
return res
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// Route-level middleware (before handler)
|
|
268
|
+
app.get('/admin', validate(bodySchema), authCheck, adminHandler)
|
|
269
|
+
|
|
270
|
+
// Error handler
|
|
271
|
+
app.onError((err, req, ctx) => {
|
|
272
|
+
if (err instanceof HttpError) {
|
|
273
|
+
return Response.json({ error: err.message }, { status: err.status })
|
|
274
|
+
}
|
|
275
|
+
console.error(err)
|
|
276
|
+
return new Response('Internal error', { status: 500 })
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Complete example
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import { serve, Router, loadEnv, cors, helmet, compress, logger, requestId, rateLimit, postgres, redis, queue, HttpError, html } from 'weifuwu'
|
|
284
|
+
|
|
285
|
+
loadEnv()
|
|
286
|
+
|
|
287
|
+
const app = new Router()
|
|
288
|
+
|
|
289
|
+
// Global middleware
|
|
290
|
+
app.use(requestId())
|
|
291
|
+
app.use(logger())
|
|
292
|
+
app.use(cors())
|
|
293
|
+
app.use(helmet())
|
|
294
|
+
app.use(compress())
|
|
295
|
+
app.use(rateLimit({ windowMs: 60_000, max: 100 }))
|
|
296
|
+
|
|
297
|
+
// Database & services
|
|
298
|
+
const sql = postgres({ url: process.env.DATABASE_URL })
|
|
299
|
+
const cache = redis({ url: process.env.REDIS_URL })
|
|
300
|
+
const jobs = queue()
|
|
301
|
+
|
|
302
|
+
app.use(sql)
|
|
303
|
+
app.use(cache)
|
|
304
|
+
app.use(jobs)
|
|
305
|
+
|
|
306
|
+
// Routes
|
|
307
|
+
app.get('/', () => html`<h1>weifuwu</h1><p>Running on ${process.version}</p>`)
|
|
308
|
+
|
|
309
|
+
app.get('/api/users', async (req, ctx) => {
|
|
310
|
+
const users = await ctx.sql`SELECT id, name, email FROM users ORDER BY id`
|
|
311
|
+
return Response.json(users)
|
|
312
|
+
})
|
|
313
|
+
|
|
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
|
+
app.post('/api/users', async (req, ctx) => {
|
|
321
|
+
const { name, email } = await req.json()
|
|
322
|
+
const [user] = await ctx.sql`INSERT INTO users (name, email) VALUES (${name}, ${email}) RETURNING *`
|
|
323
|
+
return Response.json(user, { status: 201 })
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
// Background job
|
|
327
|
+
jobs.process('welcome-email', async (job) => {
|
|
328
|
+
const { userId } = job.data
|
|
329
|
+
const [user] = await sql.client`SELECT * FROM users WHERE id = ${userId}`
|
|
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 })
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// WebSocket chat
|
|
341
|
+
app.ws('/chat', {
|
|
342
|
+
open(ws, ctx) {
|
|
343
|
+
ctx.ws.join('lobby')
|
|
344
|
+
},
|
|
345
|
+
message(ws, ctx, data) {
|
|
346
|
+
ctx.ws.sendRoom('lobby', { text: data.toString() })
|
|
347
|
+
},
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// Error handling
|
|
351
|
+
app.onError((err, req, ctx) => {
|
|
352
|
+
if (err instanceof HttpError) {
|
|
353
|
+
return Response.json({ error: err.message }, { status: err.status })
|
|
354
|
+
}
|
|
355
|
+
return Response.json({ error: 'Internal error' }, { status: 500 })
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
// Health check
|
|
359
|
+
import { health } from 'weifuwu'
|
|
360
|
+
app.mount('/_health', health())
|
|
361
|
+
|
|
362
|
+
serve(app, { port: 3000 })
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Project Structure
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
weifuwu/
|
|
369
|
+
├── package.json
|
|
370
|
+
├── tsconfig.json
|
|
371
|
+
├── scripts/
|
|
372
|
+
│ ├── build.mjs
|
|
373
|
+
│ └── release.mjs
|
|
374
|
+
├── src/
|
|
375
|
+
│ ├── index.ts ← all exports
|
|
376
|
+
│ ├── types.ts ← Context, Handler, Middleware, HttpError
|
|
377
|
+
├── core/ ← serve, router, trace, env, logger, cookie, sse, html
|
|
378
|
+
│ ├── middleware/ ← cors, compress, helmet, rate-limit, static, validate, upload, health, theme, i18n, flash, csrf, request-id
|
|
379
|
+
├── postgres/ ← postgres client, migrations, module injection
|
|
380
|
+
│ ├── redis/ ← redis client
|
|
381
|
+
│ ├── queue/ ← job queue, cron
|
|
382
|
+
│ ├── graphql.ts ← schema-first GraphQL handler
|
|
383
|
+
│ ├── hub.ts ← pub/sub hub (WebSocket rooms)
|
|
384
|
+
│ └── test/ ← 324 tests
|
|
385
|
+
└── dist/
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Development
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
npm run build # esbuild bundle → dist/index.js
|
|
392
|
+
npm run typecheck # tsc --noEmit
|
|
393
|
+
npm test # node --test (324 tests, 0 failures)
|
|
394
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Options for setting a cookie. All fields map to standard Set-Cookie attributes. */
|
|
2
|
+
export interface CookieOptions {
|
|
3
|
+
domain?: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
maxAge?: number;
|
|
6
|
+
expires?: Date;
|
|
7
|
+
httpOnly?: boolean;
|
|
8
|
+
secure?: boolean;
|
|
9
|
+
sameSite?: 'strict' | 'lax' | 'none';
|
|
10
|
+
}
|
|
11
|
+
/** Parse cookies from a Request's `Cookie` header.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const cookies = getCookies(req)
|
|
16
|
+
* console.log(cookies.session_id) // value or undefined
|
|
17
|
+
* ``` */
|
|
18
|
+
export declare function getCookies(req: Request): Record<string, string>;
|
|
19
|
+
/** Set a cookie on a Response.
|
|
20
|
+
*
|
|
21
|
+
* Appends a `Set-Cookie` header to the existing response headers.
|
|
22
|
+
* Returns a new Response with the added header.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const res = new Response('ok')
|
|
27
|
+
* return setCookie(res, 'session', token, { httpOnly: true, path: '/' })
|
|
28
|
+
* ``` */
|
|
29
|
+
export declare function setCookie(res: Response, name: string, value: string, options?: CookieOptions): Response;
|
|
30
|
+
/** Delete a cookie by setting `Max-Age=0`.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* return deleteCookie(res, 'session')
|
|
35
|
+
* ``` */
|
|
36
|
+
export declare function deleteCookie(res: Response, name: string, options?: Omit<CookieOptions, 'maxAge'>): Response;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Context, Middleware } from '../types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Get all public environment variables (those prefixed with `WEIFUWU_PUBLIC_`),
|
|
4
|
+
* with the prefix stripped.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const pub = getPublicEnv()
|
|
8
|
+
* // WEIFUWU_PUBLIC_API_URL=http://api.example.com → { API_URL: 'http://api.example.com' }
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare function getPublicEnv(): Record<string, string>;
|
|
12
|
+
declare module '../types.ts' {
|
|
13
|
+
interface Context {
|
|
14
|
+
env?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether this code is running from the compiled `dist/index.js` bundle.
|
|
19
|
+
* `false` when running TypeScript source directly (dev workflow in weifuwu repo).
|
|
20
|
+
*
|
|
21
|
+
* Used by modules that need to resolve package-internal files differently
|
|
22
|
+
* depending on whether they are compiled (published npm package) or raw TS.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isBundled(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Whether `NODE_ENV` is explicitly set to `'development'`.
|
|
27
|
+
*
|
|
28
|
+
* Used for dev-only features: HMR, livereload, React `createRoot` (not hydrate).
|
|
29
|
+
* **Not** the opposite of {@link isProd} — when `NODE_ENV` is unset, both return `false`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isDev(): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Whether `NODE_ENV` is explicitly set to `'production'`.
|
|
34
|
+
*
|
|
35
|
+
* Used for production-only behavior: plain-text 404, suppressed warnings, minified output.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isProd(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Load environment variables from a `.env` file into `process.env`.
|
|
40
|
+
*
|
|
41
|
+
* Does **not** override existing `process.env` values.
|
|
42
|
+
* Supports quoted values and inline comments.
|
|
43
|
+
*
|
|
44
|
+
* @param path - Path to `.env` file (default: `'.env'` relative to cwd).
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { loadEnv } from 'weifuwu'
|
|
48
|
+
* loadEnv()
|
|
49
|
+
* console.log(process.env.PORT)
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function loadEnv(path?: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Public env middleware.
|
|
55
|
+
*
|
|
56
|
+
* Injects `ctx.env` with all environment variables prefixed with `WEIFUWU_PUBLIC_`,
|
|
57
|
+
* with the prefix stripped. Safe to expose to the client.
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { env } from 'weifuwu'
|
|
61
|
+
* app.use(env())
|
|
62
|
+
*
|
|
63
|
+
* // .env: WEIFUWU_PUBLIC_API_URL=https://api.example.com
|
|
64
|
+
* // ctx: ctx.env.API_URL === 'https://api.example.com'
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function env(): Middleware<Context, Context & {
|
|
68
|
+
env: Record<string, string>;
|
|
69
|
+
}>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe HTML rendering via tagged template literals.
|
|
3
|
+
*
|
|
4
|
+
* Auto-escapes interpolated values to prevent XSS.
|
|
5
|
+
* Use `raw()` for trusted HTML that should not be escaped.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { html, raw } from '@weifuwujs/core'
|
|
9
|
+
*
|
|
10
|
+
* html`<h1>${title}</h1>` // auto-escaped
|
|
11
|
+
* html`<div>${raw(trustedHtml)}</div>` // unescaped
|
|
12
|
+
* html`<ul>${items.map(i => html`<li>${i}</li>`)}</ul>` // arrays
|
|
13
|
+
* html`${isAdmin && html`<button>Admin</button>`}` // conditionals
|
|
14
|
+
* html`<div>${html`<span>nested</span>`}</div>` // nested (safe)
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
interface RawHtml {
|
|
18
|
+
_raw: string;
|
|
19
|
+
}
|
|
20
|
+
export type HtmlValue = string | number | boolean | null | undefined | HtmlValue[] | RawHtml;
|
|
21
|
+
/**
|
|
22
|
+
* Tagged template literal for safe HTML.
|
|
23
|
+
*
|
|
24
|
+
* Interpolated values are auto-escaped. Use {@link raw} to bypass escaping
|
|
25
|
+
* for trusted HTML. Nested `html` calls are safe (not double-escaped).
|
|
26
|
+
*/
|
|
27
|
+
export declare function html(strings: TemplateStringsArray, ...values: HtmlValue[]): string;
|
|
28
|
+
/**
|
|
29
|
+
* Bypass HTML escaping for trusted content.
|
|
30
|
+
*
|
|
31
|
+
* Can be used standalone or inside {@link html} tagged templates.
|
|
32
|
+
*/
|
|
33
|
+
export declare function raw(content: string): HtmlValue;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Middleware, Context } from '../types.ts';
|
|
2
|
+
export interface LoggerOptions {
|
|
3
|
+
/** 'short' = method + path + status + ms, 'combined' = short + query string, 'json' = structured stderr JSON */
|
|
4
|
+
format?: 'short' | 'combined' | 'json';
|
|
5
|
+
}
|
|
6
|
+
export interface LogEvent {
|
|
7
|
+
level: 'info' | 'warn' | 'error';
|
|
8
|
+
message: string;
|
|
9
|
+
method?: string;
|
|
10
|
+
path?: string;
|
|
11
|
+
status?: number;
|
|
12
|
+
elapsed_ms?: number;
|
|
13
|
+
traceId?: string;
|
|
14
|
+
timestamp?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function logger(options?: LoggerOptions): Middleware<Context, Context>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { WebSocket, Context, Handler, Middleware, ErrorHandler } from '../types.ts';
|
|
2
|
+
import { type IncomingMessage } from 'node:http';
|
|
3
|
+
import type { Duplex } from 'node:stream';
|
|
4
|
+
import { type Hub } from '../hub.ts';
|
|
5
|
+
declare module '../types.ts' {
|
|
6
|
+
interface Context {
|
|
7
|
+
ws: {
|
|
8
|
+
/** Per-connection state object */
|
|
9
|
+
state: Record<string, unknown>;
|
|
10
|
+
/** Send JSON to this connection */
|
|
11
|
+
json(data: unknown): void;
|
|
12
|
+
/** Join a room */
|
|
13
|
+
join(room: string): void;
|
|
14
|
+
/** Leave a room */
|
|
15
|
+
leave(room: string): void;
|
|
16
|
+
/** Broadcast to a room */
|
|
17
|
+
sendRoom(room: string, data: unknown): void;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
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
|
+
export declare class Router<T extends Context = Context> {
|
|
29
|
+
private root;
|
|
30
|
+
private wsRoot;
|
|
31
|
+
private globalMws;
|
|
32
|
+
private errorHandler?;
|
|
33
|
+
private _hasWildcard;
|
|
34
|
+
private _hub?;
|
|
35
|
+
private _wss?;
|
|
36
|
+
/** Track which ctx fields have been injected so far (for dependency checking). */
|
|
37
|
+
private _ctxFields;
|
|
38
|
+
private get wss();
|
|
39
|
+
private get hub();
|
|
40
|
+
/** Inject a custom hub (e.g. with Redis for cross-process broadcast). */
|
|
41
|
+
wsHub(hub: Hub): this;
|
|
42
|
+
/** Global middleware — accumulates types into Router<T>. */
|
|
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
|
+
*/
|
|
54
|
+
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
|
+
onError(handler: ErrorHandler<T>): Router<T>;
|
|
74
|
+
private _route;
|
|
75
|
+
/** Internal route registration — no type constraints (used by _mountRouter). */
|
|
76
|
+
private _routeImpl;
|
|
77
|
+
ws(path: string, ...args: [...Middleware<T, T>[], WebSocketHandler]): Router<T>;
|
|
78
|
+
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
|
+
websocketHandler(): WsUpgradeHandler;
|
|
84
|
+
private _mountRouter;
|
|
85
|
+
private _collect;
|
|
86
|
+
private _collectWs;
|
|
87
|
+
private splitPath;
|
|
88
|
+
private matchTrie;
|
|
89
|
+
private matchWsTrie;
|
|
90
|
+
private handleError;
|
|
91
|
+
private _notFoundResponse;
|
|
92
|
+
private handle;
|
|
93
|
+
private runChain;
|
|
94
|
+
}
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
|
+
import { Router } from './router.ts';
|
|
3
|
+
export interface ServeOptions {
|
|
4
|
+
port?: number;
|
|
5
|
+
hostname?: string;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
/** Max request body size in bytes. Default: 10MB. Set to 0 for unlimited. */
|
|
8
|
+
maxBodySize?: number;
|
|
9
|
+
/** Socket timeout in ms (inactivity). Default: 30_000. */
|
|
10
|
+
timeout?: number;
|
|
11
|
+
/** Keep-Alive idle timeout in ms. Default: 5_000. */
|
|
12
|
+
keepAliveTimeout?: number;
|
|
13
|
+
/** Headers timeout in ms (must be > keepAliveTimeout). Default: 6_000. */
|
|
14
|
+
headersTimeout?: number;
|
|
15
|
+
shutdown?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface Server {
|
|
18
|
+
stop: (timeoutMs?: number) => Promise<void>;
|
|
19
|
+
/** Alias for `stop()`. Prefer this for consistency with other modules. */
|
|
20
|
+
close: (timeoutMs?: number) => Promise<void>;
|
|
21
|
+
readonly port: number;
|
|
22
|
+
readonly hostname: string;
|
|
23
|
+
ready: Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/** Default max body size: 10MB. Set maxBodySize: 0 for unlimited. */
|
|
26
|
+
export declare const DEFAULT_MAX_BODY: number;
|
|
27
|
+
export declare function readBody(req: IncomingMessage, maxSize?: number): Promise<Buffer>;
|
|
28
|
+
export declare function createRequest(req: IncomingMessage, body: Buffer): [Request, Record<string, string>];
|
|
29
|
+
export declare function sendResponse(res: ServerResponse, response: Response, opts?: {
|
|
30
|
+
traceId?: string | null;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
export declare function createTestServer(router: Router, options?: ServeOptions): Promise<{
|
|
33
|
+
server: Server;
|
|
34
|
+
url: string;
|
|
35
|
+
}>;
|
|
36
|
+
export declare function serve(router: Router, options?: ServeOptions): Server;
|