@weifuwujs/weifuwu 0.9.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.
Files changed (122) hide show
  1. package/README.md +605 -0
  2. package/dist/app.d.ts +122 -0
  3. package/dist/app.d.ts.map +1 -0
  4. package/dist/app.js +484 -0
  5. package/dist/app.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +506 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/compose.d.ts +12 -0
  11. package/dist/compose.d.ts.map +1 -0
  12. package/dist/compose.js +29 -0
  13. package/dist/compose.js.map +1 -0
  14. package/dist/env.d.ts +12 -0
  15. package/dist/env.d.ts.map +1 -0
  16. package/dist/env.js +46 -0
  17. package/dist/env.js.map +1 -0
  18. package/dist/group.d.ts +32 -0
  19. package/dist/group.d.ts.map +1 -0
  20. package/dist/group.js +66 -0
  21. package/dist/group.js.map +1 -0
  22. package/dist/helpers.d.ts +37 -0
  23. package/dist/helpers.d.ts.map +1 -0
  24. package/dist/helpers.js +101 -0
  25. package/dist/helpers.js.map +1 -0
  26. package/dist/index.d.ts +15 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +9 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/ioredis.d.ts +16 -0
  31. package/dist/ioredis.d.ts.map +1 -0
  32. package/dist/ioredis.js +42 -0
  33. package/dist/ioredis.js.map +1 -0
  34. package/dist/methods.d.ts +3 -0
  35. package/dist/methods.d.ts.map +1 -0
  36. package/dist/methods.js +10 -0
  37. package/dist/methods.js.map +1 -0
  38. package/dist/middleware/cors.d.ts +24 -0
  39. package/dist/middleware/cors.d.ts.map +1 -0
  40. package/dist/middleware/cors.js +77 -0
  41. package/dist/middleware/cors.js.map +1 -0
  42. package/dist/middleware/index.d.ts +7 -0
  43. package/dist/middleware/index.d.ts.map +1 -0
  44. package/dist/middleware/index.js +4 -0
  45. package/dist/middleware/index.js.map +1 -0
  46. package/dist/middleware/logger.d.ts +27 -0
  47. package/dist/middleware/logger.d.ts.map +1 -0
  48. package/dist/middleware/logger.js +48 -0
  49. package/dist/middleware/logger.js.map +1 -0
  50. package/dist/middleware/static.d.ts +27 -0
  51. package/dist/middleware/static.d.ts.map +1 -0
  52. package/dist/middleware/static.js +150 -0
  53. package/dist/middleware/static.js.map +1 -0
  54. package/dist/node.d.ts +27 -0
  55. package/dist/node.d.ts.map +1 -0
  56. package/dist/node.js +193 -0
  57. package/dist/node.js.map +1 -0
  58. package/dist/path.d.ts +9 -0
  59. package/dist/path.d.ts.map +1 -0
  60. package/dist/path.js +31 -0
  61. package/dist/path.js.map +1 -0
  62. package/dist/postgres.d.ts +19 -0
  63. package/dist/postgres.d.ts.map +1 -0
  64. package/dist/postgres.js +27 -0
  65. package/dist/postgres.js.map +1 -0
  66. package/dist/react-router.d.ts +31 -0
  67. package/dist/react-router.d.ts.map +1 -0
  68. package/dist/react-router.js +25 -0
  69. package/dist/react-router.js.map +1 -0
  70. package/dist/resources.d.ts +35 -0
  71. package/dist/resources.d.ts.map +1 -0
  72. package/dist/resources.js +54 -0
  73. package/dist/resources.js.map +1 -0
  74. package/dist/router.d.ts +30 -0
  75. package/dist/router.d.ts.map +1 -0
  76. package/dist/router.js +122 -0
  77. package/dist/router.js.map +1 -0
  78. package/dist/types.d.ts +124 -0
  79. package/dist/types.d.ts.map +1 -0
  80. package/dist/types.js +2 -0
  81. package/dist/types.js.map +1 -0
  82. package/dist/vite.d.ts +21 -0
  83. package/dist/vite.d.ts.map +1 -0
  84. package/dist/vite.js +101 -0
  85. package/dist/vite.js.map +1 -0
  86. package/dist/websocket/accept.d.ts +22 -0
  87. package/dist/websocket/accept.d.ts.map +1 -0
  88. package/dist/websocket/accept.js +104 -0
  89. package/dist/websocket/accept.js.map +1 -0
  90. package/dist/websocket/close-code.d.ts +22 -0
  91. package/dist/websocket/close-code.d.ts.map +1 -0
  92. package/dist/websocket/close-code.js +35 -0
  93. package/dist/websocket/close-code.js.map +1 -0
  94. package/dist/websocket/connection.d.ts +71 -0
  95. package/dist/websocket/connection.d.ts.map +1 -0
  96. package/dist/websocket/connection.js +287 -0
  97. package/dist/websocket/connection.js.map +1 -0
  98. package/dist/websocket/index.d.ts +7 -0
  99. package/dist/websocket/index.d.ts.map +1 -0
  100. package/dist/websocket/index.js +4 -0
  101. package/dist/websocket/index.js.map +1 -0
  102. package/dist/websocket/types.d.ts +66 -0
  103. package/dist/websocket/types.d.ts.map +1 -0
  104. package/dist/websocket/types.js +2 -0
  105. package/dist/websocket/types.js.map +1 -0
  106. package/docs/ai.md +175 -0
  107. package/docs/data.md +81 -0
  108. package/docs/migrating.md +116 -0
  109. package/docs/react-router.md +187 -0
  110. package/docs/styling.md +69 -0
  111. package/package.json +144 -0
  112. package/templates/react-router/_gitignore +3 -0
  113. package/templates/react-router/app/app.css +1 -0
  114. package/templates/react-router/app/context.ts +4 -0
  115. package/templates/react-router/app/root.tsx +50 -0
  116. package/templates/react-router/app/routes/home.tsx +17 -0
  117. package/templates/react-router/app/routes.ts +3 -0
  118. package/templates/react-router/app/server.ts +29 -0
  119. package/templates/react-router/package.json +29 -0
  120. package/templates/react-router/react-router.config.ts +5 -0
  121. package/templates/react-router/tsconfig.json +19 -0
  122. package/templates/react-router/vite.config.ts +6 -0
package/README.md ADDED
@@ -0,0 +1,605 @@
1
+ # @weifuwujs/weifuwu
2
+
3
+ 一个基于 Node.js 与 Fetch API 的极简、类型安全 Web 框架。
4
+
5
+ 请求处理函数就是 `(req: Request, ctx) => Response | Promise<Response>`,
6
+ 没有自定义的请求/响应对象,也没有隐式魔法。
7
+
8
+ ```ts
9
+ import { createApp, json } from '@weifuwujs/weifuwu'
10
+
11
+ const app = createApp()
12
+
13
+ app.get('/', () => new Response('Hello weifuwu!'))
14
+ app.get('/users/:id', (req, ctx) => json({ id: ctx.params.id }))
15
+
16
+ app.listen(3000, () => console.log('http://localhost:3000'))
17
+ ```
18
+
19
+ ## 定位
20
+
21
+ **Node 上最干净的 fetch 全栈内核。**
22
+
23
+ 内核只回答「请求如何流过类型安全的函数管线」,不回答「业务上你该有什么」:
24
+
25
+ - **管线**:handler / 洋葱中间件 / 类型安全路由 / `app.fetch` 纯函数入口;
26
+ - **实时**:WebSocket 与 HTTP 共用路由、参数与中间件;
27
+ - **数据**:`resource` 内核 + 懒加载 `ctx.sql` / `ctx.redis`;
28
+ - **全栈**:React Router 单端口 SSR + CLI(init / dev / build / start / typegen / routes);
29
+ - **测试**:`app.request()` 不需要监听端口。
30
+
31
+ auth / tasks / messages / 单源 API 已全部外迁为独立包(`@weifuwujs/*`),只通过公开的
32
+ `ctx` 与中间件接入;旧子路径将在 0.9 移除,迁移路径见 [docs/migrating.md](./docs/migrating.md)。
33
+
34
+ ## 特性
35
+
36
+ - **Fetch 原生**:`Request` / `Response` / `Headers` 就是标准 Web API,`app.fetch(request)` 是纯函数入口,方便测试与跨运行时复用。
37
+ - **类型安全的路由参数**:`/users/:id` 会自动推导 `ctx.params.id: string`。
38
+ - **洋葱模型中间件**:`(req, ctx, next) => ...`,可短路、可替换上下文。
39
+ - **上下文类型累积**:`app.use(mw)` 注入的字段会一路传递到后面的 handler。
40
+ - **内核极简**:路由 / 中间件 / 上下文只用 Node 内置模块;WebSocket 基于 `ws`,首次升级时懒加载;业务能力全部外迁为 `@weifuwujs/*` 生态包,内核保持零生态依赖。
41
+ - **WebSocket**:基于 `ws` 的服务端实现(握手 / 分片 / 关闭 / ping-pong 由成熟内核承担),与路由、参数、中间件风格一致。
42
+ - **数据资源**:`resource` 内核 + `./postgres`、`./ioredis` 子路径(懒加载 `ctx.sql` / `ctx.redis`、优雅关闭),见 [docs/data.md](./docs/data.md)。
43
+ - **AI 集成**:用 `resource` 注入模型注册表(`ctx.ai`),`streamText` 流式 Response 直接返回;含 `useChat`、结构化输出、错误映射与 embedding 缓存配方,见 [docs/ai.md](./docs/ai.md)。
44
+ - **内置中间件**:logger、cors、serveStatic。
45
+ - **默认样式管线**:脚手架(`init`)自带 Tailwind CSS v4(`@tailwindcss/vite`),dev/prod 一致;shadcn/ui 接入见 [docs/styling.md](./docs/styling.md)。
46
+ - **原生 TypeScript**:Node 24+ 可直接运行 `.ts` 源码;发布产物为 ESM + `.d.ts`。
47
+
48
+ ## 生态包(可选)
49
+
50
+ 内核之外的业务能力都是独立包,按需安装;它们只依赖内核公开的 `ctx` 与中间件接入,可单独发布与升级。
51
+
52
+ - **单源 API**:[`@weifuwujs/api`](./packages/api/README.md)——一份类型描述符同时生成 REST 路由与 GraphQL schema。
53
+ - **用户系统**:[`@weifuwujs/auth`](./packages/auth/README.md)——scrypt、会话轮换、防枚举、限流、Origin、无 JS 表单页,memory/sqlite/postgres store + 契约套件。
54
+ - **任务队列**:[`@weifuwujs/tasks`](./packages/tasks/README.md)——至少一次 + 幂等键、指数退避/死信、memory/sqlite/postgres/redis 四驱动 + 契约套件、`weifuwu-tasks work` 独立进程。
55
+ - **站内信**:[`@weifuwujs/messages`](./packages/messages/README.md)——幂等发送、REST 收件箱、WS 实时 + 断线 `since` 补投按 id 去重、memory/sqlite/postgres 契约套件、本地/Redis 可插拔总线。
56
+
57
+ ## 要求
58
+
59
+ - Node.js **>= 24**(用于原生运行 TypeScript,以及稳定的 Fetch API)。
60
+
61
+ ## React Router 全栈
62
+
63
+ `@weifuwujs/weifuwu` 内置 React Router v8(framework mode)集成:
64
+ **单进程、单端口**同时提供 RR SSR 页面、weifuwu API 与 WebSocket,
65
+ dev 与 prod 行为一致,路由类型由 RR typegen 生成。
66
+
67
+ ```bash quickstart
68
+ # 1. 生成项目(内置模板:React Router + Tailwind,无需手工拷贝示例)
69
+ npx @weifuwujs/weifuwu init my-app
70
+ cd my-app
71
+ npm install
72
+
73
+ # 2. 开发:Vite HMR 与 weifuwu API 共用一个端口
74
+ npm run dev # http://localhost:3000
75
+
76
+ # 3. 生产:构建 + 启动(同样一个端口)
77
+ npm run build
78
+ npm run start
79
+
80
+ # 4. 类型:生成 RR 路由类型并检查
81
+ npm run typecheck
82
+ ```
83
+
84
+ 接入本身只有一行——把 RR 作为 weifuwu 的 notFound 处理器:
85
+
86
+ ```ts
87
+ import { createApp, json } from '@weifuwujs/weifuwu'
88
+ import { reactRouter } from '@weifuwujs/weifuwu/react-router'
89
+
90
+ export default createApp()
91
+ .get('/api/health', () => json({ ok: true }))
92
+ .notFound(reactRouter()) // 未命中 API 的请求交给 React Router
93
+ ```
94
+
95
+ - 详细设计:[docs/react-router.md](./docs/react-router.md)(单端口 HMR、
96
+ context bridge、typegen、保留命名空间、dev 降级、env/trustProxy/缓存、部署)
97
+ - 迁移指南:[docs/migrating.md](./docs/migrating.md)(官方 custom-server /
98
+ 纯 weifuwu 两条路径)
99
+
100
+ <!-- cli -->
101
+ | 命令 | 说明 |
102
+ | --- | --- |
103
+ | `init` | 生成新项目(React Router + Tailwind + weifuwu) |
104
+ | `dev` | 启动开发服务器(Vite + React Router SSR) |
105
+ | `build` | 构建生产产物(react-router build) |
106
+ | `start` | 启动生产服务器 |
107
+ | `typegen` | 生成 React Router 类型 |
108
+ | `routes` | 打印路由表(react-router routes) |
109
+
110
+ 用户系统命令已外迁为 `weifuwu-auth`(`migrate` / `user:list` / `user:disable` / `user:reset-password`);
111
+ 任务队列为 `weifuwu-tasks`(`migrate` / `work` / `list` / `retry` / `prune`);站内信为 `weifuwu-messages`(`migrate`)。
112
+ 见 [packages/auth](./packages/auth/README.md)、[packages/tasks](./packages/tasks/README.md)、[packages/messages](./packages/messages/README.md)。
113
+
114
+ | 选项 | 说明 |
115
+ | --- | --- |
116
+ | `--port <port>` | 监听端口,默认 3000 |
117
+ | `--host <host>` | 监听地址 |
118
+ | `--root <dir>` | 项目根目录,默认当前目录 |
119
+ | `--trust-proxy` | 信任 `X-Forwarded-Proto` / `X-Forwarded-Host`(反向代理场景) |
120
+ <!-- /cli -->
121
+
122
+ ## 稳定性
123
+
124
+ 公共 API(`exports` 子路径与其具名导出)遵循 [STABILITY.md](./STABILITY.md) 的 semver 与弃用承诺:
125
+ 新增为 minor、破坏性变更为 major、弃用至少保留一个完整 minor;Node 支持、peer 范围与安全联系同样在该文件。
126
+ 贡献流程与门禁命令见 [CONTRIBUTING.md](./CONTRIBUTING.md)。
127
+
128
+ ## 安装
129
+
130
+ ```bash
131
+ npm install @weifuwujs/weifuwu
132
+ ```
133
+
134
+ ## 核心类型
135
+
136
+ ```ts
137
+ export interface Context {
138
+ params: Record<string, string>
139
+ query: Record<string, string>
140
+ [key: string]: unknown // allow arbitrary middleware-injected data
141
+ }
142
+
143
+ export type Handler<T extends object = Context> = (
144
+ req: Request,
145
+ ctx: T,
146
+ ) => Response | Promise<Response>
147
+
148
+ export type Middleware<In extends Context = Context, Out extends In = In> = {
149
+ (req: Request, ctx: In, next: Handler<Out>): Response | Promise<Response>
150
+ }
151
+ ```
152
+
153
+ ## 路由
154
+
155
+ ```ts
156
+ const app = createApp()
157
+
158
+ app.get('/users/:id', (req, ctx) => json({ id: ctx.params.id }))
159
+ app.post('/users', async (req) => json(await req.json(), { status: 201 }))
160
+ app.put('/users/:id', handler)
161
+ app.patch('/users/:id', handler)
162
+ app.delete('/users/:id', handler)
163
+ app.head('/users/:id', handler)
164
+ app.options('/users/:id', handler)
165
+
166
+ // 任意方法 / 所有标准方法
167
+ app.on('PURGE', '/cache/:key', handler)
168
+ app.all('/webhook', handler)
169
+ ```
170
+
171
+ 路径语法:
172
+
173
+ | 写法 | 含义 | 示例 |
174
+ | --- | --- | --- |
175
+ | `/users` | 静态路径 | `/users` |
176
+ | `/users/:id` | 命名参数,写入 `ctx.params.id` | `/users/42` |
177
+ | `/files/*` | 通配剩余路径,写入 `ctx.params['*']` | `/files/a/b.txt` |
178
+ | `/files/*rest` | 带名字的通配 | `/files/a/b.txt` → `params.rest` |
179
+
180
+ 匹配优先级为:静态段 > `:param` > `*`。尾斜杠会被归一化(`/a/` 等价于 `/a`)。
181
+
182
+ 自动行为:
183
+
184
+ - `HEAD` 请求在缺少 `HEAD` 路由时回退到 `GET`,且不发送响应体。
185
+ - 路径存在但方法不匹配时返回 `405`,并带上 `Allow` 头。
186
+ - 未显式注册 `OPTIONS` 时自动返回 `204` + `Allow`。
187
+
188
+ ### 路由分组
189
+
190
+ ```ts
191
+ app.group('/api', (api) => {
192
+ api.get('/users/:id', (req, ctx) => json(ctx.params.id)) // GET /api/users/:id
193
+ api.group('/v2', (v2) => {
194
+ v2.get('/ping', () => new Response('pong')) // GET /api/v2/ping
195
+ })
196
+ })
197
+ ```
198
+
199
+ 分组前缀中的参数也会进入类型:
200
+
201
+ ```ts
202
+ app.group('/api/:version', (api) => {
203
+ api.get('/users/:id', (req, ctx) => {
204
+ ctx.params.version // string
205
+ ctx.params.id // string
206
+ return json(ctx.params)
207
+ })
208
+ })
209
+ ```
210
+
211
+ ## 中间件
212
+
213
+ 中间件按注册顺序执行,形成洋葱模型:
214
+
215
+ ```ts
216
+ import { logger } from '@weifuwujs/weifuwu/middleware'
217
+
218
+ const app = createApp()
219
+ .use(logger())
220
+ .use(async (req, ctx, next) => {
221
+ const start = Date.now()
222
+ const res = await next(req, ctx) // 交给下一层
223
+ res.headers.set('x-duration', `${Date.now() - start}ms`)
224
+ return res
225
+ })
226
+ .get('/', () => new Response('ok'))
227
+ ```
228
+
229
+ - 不调用 `next` 即可短路(例如鉴权失败直接返回 401)。
230
+ - `next(req, ctx)` 可以传入新的 `req` / `ctx`,实现函数式的上下文替换;不传则沿用当前值。
231
+ - 同一个中间件重复调用 `next` 会返回 500。
232
+
233
+ ### 注入上下文并获得类型
234
+
235
+ 推荐把中间件声明为带 `In` / `Out` 泛型的常量,`Out` 中的字段会累积到后续 handler:
236
+
237
+ ```ts
238
+ import type { Context, Middleware } from '@weifuwujs/weifuwu'
239
+
240
+ const withUser: Middleware<Context, Context & { user: User }> = async (
241
+ req,
242
+ ctx,
243
+ next,
244
+ ) => {
245
+ const user = await verify(req)
246
+ return next(req, { ...ctx, user })
247
+ }
248
+
249
+ const app = createApp().use(withUser)
250
+
251
+ app.get('/me', (req, ctx) => json(ctx.user)) // ctx.user: User
252
+ ```
253
+
254
+ > **注意**:TypeScript 无法从内联中间件内部对 `next()` 的调用来推断 `Out`。
255
+ > 需要先把中间件赋值给带类型的变量/常量(如上),再传给 `use` / 路由。
256
+
257
+ 路由级中间件同样支持类型累积(最多显式推导两个中间件,更多则回退为不累积类型):
258
+
259
+ ```ts
260
+ app.get('/admin/:id', withUser, requireAdmin, (req, ctx) => {
261
+ ctx.params.id // string
262
+ ctx.user // User
263
+ return json(ctx.user)
264
+ })
265
+ ```
266
+
267
+ ### 路径中间件
268
+
269
+ ```ts
270
+ app.use('/api', authMiddleware)
271
+ ```
272
+
273
+ 路径中间件在进入时会剥离前缀(Express 风格),调用 `next()` 时还原;
274
+ 因此可以这样挂载静态资源:
275
+
276
+ ```ts
277
+ app.use('/assets', serveStatic('./public')) // /assets/a.png -> ./public/a.png
278
+ ```
279
+
280
+ ## 上下文
281
+
282
+ ```ts
283
+ app.get('/search', (req, ctx) => {
284
+ ctx.params // 路由参数
285
+ ctx.query // { q: 'hello' },重复的 key 取第一个值
286
+ return json({ q: ctx.query.q })
287
+ })
288
+ ```
289
+
290
+ 由于 `Context` 带有索引签名,中间件可以注入任意字段而不需要改动框架类型。
291
+ 也可以使用声明合并让字段全局可见:
292
+
293
+ ```ts
294
+ declare module '@weifuwujs/weifuwu' {
295
+ interface Context {
296
+ user: User
297
+ }
298
+ }
299
+ ```
300
+
301
+ ## 错误处理
302
+
303
+ `HttpError` 会被自动转换为对应状态的响应(4xx 暴露 message,5xx 不暴露):
304
+
305
+ ```ts
306
+ import { HttpError } from '@weifuwujs/weifuwu'
307
+
308
+ app.get('/users/:id', async (req, ctx) => {
309
+ const user = await db.find(ctx.params.id)
310
+ if (!user) throw new HttpError(404, 'User not found')
311
+ return json(user)
312
+ })
313
+ ```
314
+
315
+ 自定义错误响应与 404:
316
+
317
+ ```ts
318
+ app.onError((error, req, ctx) => {
319
+ console.error(error)
320
+ return json({ error: 'Something went wrong' }, { status: 500 })
321
+ })
322
+
323
+ app.notFound((req) => json({ error: 'Not Found' }, { status: 404 }))
324
+ ```
325
+
326
+ `onError` 返回 `undefined` 时会退回默认行为;如果 `onError` 自身抛错,
327
+ 框架会记录该错误并返回 500。
328
+
329
+ ## 内置中间件
330
+
331
+ 从子路径导入,避免核心包引入 `node:fs`:
332
+
333
+ ```ts
334
+ import { logger, cors, serveStatic } from '@weifuwujs/weifuwu/middleware'
335
+ ```
336
+
337
+ ### `logger(options?)`
338
+
339
+ 输出访问日志,默认格式 `GET /path 200 1.2ms`。
340
+
341
+ | 选项 | 类型 | 说明 |
342
+ | --- | --- | --- |
343
+ | `log` | `(message: string) => void` | 输出函数,默认 `console.log` |
344
+ | `format` | `(info: LogInfo) => string` | 自定义格式 |
345
+
346
+ `LogInfo` 包含 `method` / `path` / `status` / `duration` / `request` /
347
+ `response`,handler 抛错时还带 `error`(原始错误),可据此做结构化日志:
348
+
349
+ ```ts
350
+ app.use(logger({ format: (info) => JSON.stringify({ ...info, error: undefined, message: info.error instanceof Error ? info.error.message : undefined }) }))
351
+ ```
352
+
353
+ ### `cors(options?)`
354
+
355
+ | 选项 | 类型 | 默认值 | 说明 |
356
+ | --- | --- | --- | --- |
357
+ | `origin` | `string \| string[] \| (origin) => string \| null` | `'*'` | 允许的来源 |
358
+ | `methods` | `string \| string[]` | 常见方法全集 | `Access-Control-Allow-Methods` |
359
+ | `allowedHeaders` | `string \| string[]` | 回显请求头 | `Access-Control-Allow-Headers` |
360
+ | `exposedHeaders` | `string \| string[]` | — | `Access-Control-Expose-Headers` |
361
+ | `credentials` | `boolean` | `false` | 为 `true` 且 origin 为 `'*'` 时回显 Origin |
362
+ | `maxAge` | `number` | — | 预检缓存秒数 |
363
+
364
+ 预检请求(`OPTIONS` + `Access-Control-Request-Method`)会被直接以 `204` 响应。
365
+
366
+ ### `serveStatic(root, options?)`
367
+
368
+ | 选项 | 类型 | 默认值 | 说明 |
369
+ | --- | --- | --- | --- |
370
+ | `index` | `string \| false` | `'index.html'` | 目录默认文件 |
371
+ | `fallback` | `string` | — | 文件不存在时回退(SPA) |
372
+ | `maxAge` | `number` | — | `Cache-Control: max-age` 秒数 |
373
+ | `immutable` | `boolean` | `false` | 追加 `immutable` |
374
+ | `dotfiles` | `'allow' \| 'ignore' \| 'deny'` | `'ignore'` | 点文件处理策略 |
375
+ | `headers` | `Record<string, string>` | — | 附加响应头 |
376
+ | `etag` | `boolean` | `true` | 生成并为匹配请求返回 304 |
377
+
378
+ 未命中文件时调用 `next()`,因此可以与 API 路由共存;同时内置路径穿越防护。
379
+
380
+ ## WebSocket
381
+
382
+ 基于 `ws` 的服务端实现,路由与 HTTP 共用一套路径语法(`/chat/:room`、`*` 通配、分组前缀),
383
+ 握手/分片/关闭/ping-pong 由成熟内核承担。
384
+
385
+ `app.ws(path, ..., factory)` 的终端是一个**工厂**:每连接调用一次,可以异步,返回生命周期回调。
386
+ 工厂体就是 open 钩子,闭包就是连接级状态:
387
+
388
+ ```ts
389
+ const app = createApp()
390
+
391
+ app.ws('/chat/:room', async (socket, ctx) => {
392
+ const room = ctx.params.room
393
+ await join(room, socket) // 异步初始化安全:期间消息会被缓冲
394
+
395
+ const timer = setInterval(() => socket.ping(), 30_000)
396
+ return {
397
+ onMessage: (socket, message) => broadcast(room, message.data), // 串行 await,自带背压
398
+ onClose: () => { clearInterval(timer); leave(room, socket) }, // 恰好一次
399
+ onError: (socket, error) => log.warn(error),
400
+ }
401
+ })
402
+
403
+ app.listen(3000)
404
+ ```
405
+
406
+ 框架保证:
407
+
408
+ - **工厂结算前消息不丢**(缓冲后按序投递),因此 `await` 之后再处理消息是安全的;
409
+ - `onMessage` 按到达顺序串行 await;抛错走 `onError` → `app.onError`,默认以 1011 关闭;
410
+ - `onClose` 恰好一次(正常关闭 / 错误 / terminate),且排在已入队的 `onMessage` 之后;
411
+ - 工厂返回 `void` 表示纯流式模式(自行 `socket.on` / `for await`);dev 下若既没返回生命周期
412
+ 也没有消息消费者,会给出警告,避免消息被静默丢弃。
413
+
414
+ ```ts
415
+ // 纯流式:自行消费(for await 放在分离任务里,避免阻塞缓冲投递)
416
+ app.ws('/echo', (socket) => {
417
+ void (async () => {
418
+ for await (const message of socket) socket.send(message.data)
419
+ })()
420
+ })
421
+ ```
422
+
423
+ ### 连接 API
424
+
425
+ 回调之外,`socket` 仍暴露完整的事件与异步迭代能力(底层逃生口):
426
+
427
+ | 成员 | 说明 |
428
+ | --- | --- |
429
+ | `socket.send(data)` | 发送 `string`(文本)或 `Uint8Array`/`ArrayBuffer`(二进制);返回 `false` 表示缓冲区已满 |
430
+ | `socket.ping(data?)` / `socket.pong(data?)` | 控制帧;收到 ping 默认自动回 pong |
431
+ | `socket.close(code?, reason?)` | 发起关闭握手,超时(默认 5s)后强制断开 |
432
+ | `socket.terminate()` | 立即断开 |
433
+ | `socket.readyState` | `'open'` / `'closing'` / `'closed'` |
434
+ | `socket.protocol` | 协商出的子协议 |
435
+ | `socket.remoteAddress` / `socket.bufferedAmount` | 对端地址 / 待发送字节数 |
436
+ | 事件 | `message` / `close` / `error` / `ping` / `pong`(支持 `on` / `once` / `off`)|
437
+ | `for await (const { data, binary } of socket)` | 异步迭代消息 |
438
+
439
+ ### 中间件与拒绝升级
440
+
441
+ HTTP 与 WebSocket 共用 `Middleware` 类型;同一个中间件实例可以两处复用
442
+ (结果类型写 `Response | void`),返回 `Response` 即拒绝升级(写回 HTTP 响应):
443
+
444
+ ```ts
445
+ import type { Context, Middleware } from '@weifuwujs/weifuwu'
446
+
447
+ const withToken: Middleware<Context, Context & { token: string }, Response | void> =
448
+ async (req, ctx, next) => {
449
+ const token = req.headers.get('authorization')
450
+ if (!token) return new Response('Unauthorized', { status: 401 })
451
+ return next(req, { ...ctx, token })
452
+ }
453
+
454
+ app.get('/me', withToken, (req, ctx) => json(ctx.token)) // HTTP:401 或继续
455
+ app.ws('/secure/:id', withToken, (socket, ctx) => { // WS:401 拒绝升级或继续
456
+ ctx.token // string
457
+ ctx.params.id // string
458
+ socket.send(ctx.token)
459
+ return {}
460
+ })
461
+
462
+ // 全局 WS 中间件(只作用于 upgrade,HTTP 中间件不会自动作用)
463
+ app.wsUse(async (req, ctx, next) => next(req, ctx))
464
+ ```
465
+
466
+ HTTP 专用中间件保持默认(`R = Response`),因此可以直接读取下游响应:
467
+
468
+ ```ts
469
+ const withHeader: Middleware<Context, Context> = async (req, ctx, next) => {
470
+ const response = await next(req, ctx)
471
+ response.headers.set('x-trace', '1')
472
+ return response
473
+ }
474
+ ```
475
+
476
+ ### 路由选项
477
+
478
+ ```ts
479
+ app.ws(
480
+ '/room/:id',
481
+ {
482
+ protocols: ['chat.v2'], // 子协议,按客户端优先级挑选
483
+ maxMessageSize: 1 << 20, // 单条消息上限,默认 4 MiB,超出关闭码 1009
484
+ autoPong: true, // 自动回 pong,默认 true
485
+ closeTimeout: 5000, // 关闭握手超时
486
+ pingInterval: 30_000, // 服务端心跳 ping
487
+ idleTimeout: 120_000, // 空闲超时
488
+ headers: { 'x-custom': '1' }, // 追加到 101 响应的头
489
+ },
490
+ (socket, ctx) => {
491
+ socket.send(ctx.params.id)
492
+ return {}
493
+ },
494
+ )
495
+ ```
496
+
497
+ ### 接入方式
498
+
499
+ `app.listen()` 与 `serve()` 会自动挂载 `request` + `upgrade`;手动创建 server 时用 `app.attach(server)`:
500
+
501
+ ```ts
502
+ import { createServer } from 'node:http'
503
+
504
+ const server = createServer()
505
+ app.attach(server) // HTTP + WebSocket
506
+ server.listen(3000)
507
+ ```
508
+
509
+ 也可以直接调用 `app.upgrade(request, socket, head)` 自行接入任意 HTTP 服务器。
510
+
511
+ ## 部署到 Node
512
+
513
+ ```ts
514
+ import { createApp } from '@weifuwujs/weifuwu'
515
+
516
+ const app = createApp().get('/', () => new Response('ok'))
517
+
518
+ // 方式一:直接监听
519
+ app.listen(3000, () => console.log('ready'))
520
+
521
+ // 方式二:手动创建 server
522
+ import { createServer } from 'node:http'
523
+ createServer(app.handler).listen(3000)
524
+
525
+ // 方式三:serve 辅助函数(支持反向代理)
526
+ import { serve } from '@weifuwujs/weifuwu/node'
527
+ serve(app, { port: 3000, trustProxy: true })
528
+ ```
529
+
530
+ - `app.fetch(request)`:纯 Fetch 入口,可用于测试或部署到任意 Fetch 运行时。
531
+ - `app.handler`:Node 的 `(req, res)` 处理函数。
532
+ - `serve` / `app.listen`:创建并启动 `http.Server`。
533
+ - `trustProxy: true` 时会读取 `X-Forwarded-Proto` / `X-Forwarded-Host`。
534
+
535
+ ### 环境变量(CLI)
536
+
537
+ `weifuwu dev` 与 `weifuwu start` 都会加载 `.env` 系列到 `process.env`,
538
+ 真实环境变量优先(文件只补缺)。优先级从低到高:
539
+
540
+ ```
541
+ .env < .env.local < .env.${mode} < .env.${mode}.local
542
+ ```
543
+
544
+ `mode` 由 `NODE_ENV` 决定(dev 默认 `development`,start 默认 `production`)。
545
+ 降级模式(Vite 初始化失败)同样适用,与正常启动保持一致。
546
+
547
+ dev 模式另有一个可选逃生口:
548
+
549
+ - `WEIFUWU_FS_ALLOW`:逗号分隔的额外文件系统路径,追加进 Vite dev 白名单。
550
+ 仅在依赖被提升到项目根之外(monorepo、`file:` 链接)仍报 403 时使用;正常项目无需设置。
551
+
552
+ ## 测试
553
+
554
+ `app.request()` 直接走内部 `fetch`,不需要启动端口:
555
+
556
+ ```ts
557
+ import { test } from 'node:test'
558
+ import assert from 'node:assert/strict'
559
+ import { createApp, json } from '@weifuwujs/weifuwu'
560
+
561
+ test('GET /users/:id', async () => {
562
+ const app = createApp().get('/users/:id', (req, ctx) => json({ id: ctx.params.id }))
563
+ const res = await app.request('/users/42')
564
+ assert.deepEqual(await res.json(), { id: '42' })
565
+ })
566
+ ```
567
+
568
+ ## 项目结构
569
+
570
+ ```
571
+ src/
572
+ types.ts Handler / Middleware / Context / 类型工具
573
+ app.ts App:路由注册、中间件、mount、fetch / upgrade / listen
574
+ group.ts 路由分组
575
+ router.ts 路径树路由器
576
+ compose.ts 洋葱模型组合(HTTP / WebSocket)
577
+ path.ts 路径工具
578
+ helpers.ts json / text / html / redirect / HttpError
579
+ resources.ts 资源中间件内核(懒初始化 / 单例 / 优雅关闭)
580
+ postgres.ts ./postgres:ctx.sql(postgres.js,懒加载)
581
+ ioredis.ts ./ioredis:ctx.redis(ioredis,懒加载)
582
+ env.ts .env 加载(dev / start 使用)
583
+ node.ts Node HTTP 适配(Request/Response <-> IncomingMessage,upgrade)
584
+ cli.ts weifuwu CLI:init / dev / build / start / typegen / routes
585
+ react-router.ts React Router SSR 中间件(虚拟模块加载 build)
586
+ vite.ts defineConfig:注册 RR 插件与 SSR 入口
587
+ middleware/ logger / cors / serveStatic
588
+ websocket/ 基于 ws:握手校验、连接适配、关闭码
589
+ test/ node:test 测试(含类型级断言)
590
+ examples/ basic.ts / rest.ts / websocket.ts / react-router
591
+ templates/ weifuwu init 的工程模板(React Router + Tailwind)
592
+ plan/ 计划:plan.md(写作规范)/ websocket.md / react-router.md / engines.md / styling.md / resources.md
593
+ ```
594
+
595
+ ## 开发
596
+
597
+ ```bash
598
+ npm test # node --test,直接运行 TS 测试
599
+ npm run typecheck # 类型检查(含类型级测试)
600
+ npm run build # 输出 ESM + d.ts 到 dist/
601
+ ```
602
+
603
+ ## License
604
+
605
+ ISC