@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/docs/ai.md ADDED
@@ -0,0 +1,175 @@
1
+ # AI 集成:`ctx.ai` 与 AI SDK
2
+
3
+ weifuwu 不封装 AI SDK。AI SDK([`ai`](https://ai-sdk.dev))是**无状态函数库**
4
+ (`generateText` / `streamText` / `embedMany` / `ToolLoopAgent`),没有连接、没有生命周期。
5
+ 因此 `ctx.ai` 的最佳形态是**用 `resource` 注入模型注册表**:
6
+
7
+ - 模型与 key 集中一处,按环境切换,测试可替换(`ai/test` 的 mock 模型);
8
+ - `generateText` / `streamText` 等函数保持原生 import,享受 SDK 全部能力与类型;
9
+ - 懒初始化:只有真正用到模型时才创建 provider。
10
+
11
+ ## 安装
12
+
13
+ ```bash
14
+ npm i ai zod @ai-sdk/openai # provider 按需选择:@ai-sdk/anthropic、@ai-sdk/google 等
15
+ ```
16
+
17
+ 任何 OpenAI 兼容端点(DeepSeek、Qwen 等)都可通过 `createOpenAI({ baseURL })` 接入;
18
+ Vercel AI Gateway 也可直接用模型字符串(如 `'openai/gpt-4o-mini'`)。
19
+
20
+ ## 配方(完整可编译)
21
+
22
+ ```ts ai
23
+ import { createApp, HttpError, json, resource } from '@weifuwujs/weifuwu'
24
+ import { redis } from '@weifuwujs/weifuwu/ioredis'
25
+ import {
26
+ convertToModelMessages,
27
+ embedMany,
28
+ generateText,
29
+ Output,
30
+ streamText,
31
+ } from 'ai'
32
+ import type { EmbeddingModel, LanguageModel, UIMessage } from 'ai'
33
+ import { openai } from '@ai-sdk/openai'
34
+ import { z } from 'zod'
35
+
36
+ // 1) 模型注册表:懒初始化、可注入、可测试替换
37
+ const ai = resource('ai', () => ({
38
+ model: openai(process.env.AI_MODEL ?? 'gpt-4o-mini') as LanguageModel,
39
+ embed: openai.textEmbeddingModel(
40
+ process.env.AI_EMBED_MODEL ?? 'text-embedding-3-small',
41
+ ) as EmbeddingModel,
42
+ }))
43
+
44
+ // 2) 上游错误 → HTTP 语义:RetryError 解包、429 透传 retry-after、认证失败脱敏
45
+ function toHttpError(error: unknown): HttpError {
46
+ let current: unknown = error
47
+ for (let depth = 0; depth < 3 && current instanceof Error; depth += 1) {
48
+ const status = (current as { statusCode?: unknown }).statusCode
49
+ if (typeof status === 'number') {
50
+ if (status === 401 || status === 403) {
51
+ return new HttpError(502, '模型服务认证失败')
52
+ }
53
+ if (status === 429) {
54
+ const retryAfter = (
55
+ current as { responseHeaders?: Record<string, string> }
56
+ ).responseHeaders?.['retry-after']
57
+ return new HttpError(429, '请求过于频繁,请稍后重试', {
58
+ headers:
59
+ retryAfter === undefined ? undefined : { 'retry-after': retryAfter },
60
+ })
61
+ }
62
+ return new HttpError(502, '模型服务异常')
63
+ }
64
+ current = (current as { lastError?: unknown }).lastError
65
+ }
66
+ throw error
67
+ }
68
+
69
+ const app = createApp()
70
+ .use(ai)
71
+ .use(redis()) // 可选:embedding 缓存
72
+ // 文本 / 结构化输出:进入响应前失败 → 标准 HTTP 错误
73
+ .post('/api/extract', async (req, ctx) => {
74
+ const { text } = (await req.json()) as { text: string }
75
+ try {
76
+ const { output } = await generateText({
77
+ model: ctx.ai.model,
78
+ output: Output.object({
79
+ schema: z.object({ title: z.string(), tags: z.array(z.string()) }),
80
+ }),
81
+ prompt: `提取标题与标签:\n${text}`,
82
+ })
83
+ return json(output)
84
+ } catch (error) {
85
+ throw toHttpError(error)
86
+ }
87
+ })
88
+ // useChat:UI 消息流(客户端见下)
89
+ .post('/api/chat', async (req, ctx) => {
90
+ const { messages } = (await req.json()) as { messages: UIMessage[] }
91
+ return streamText({
92
+ model: ctx.ai.model,
93
+ messages: await convertToModelMessages(messages),
94
+ onError: ({ error }) => console.error('[ai]', error),
95
+ }).toUIMessageStreamResponse()
96
+ })
97
+ // 纯文本流:curl / fetch / SSE 客户端都能直接用
98
+ .post('/api/completion', async (req, ctx) => {
99
+ const { prompt } = (await req.json()) as { prompt: string }
100
+ return streamText({
101
+ model: ctx.ai.model,
102
+ prompt,
103
+ onError: ({ error }) => console.error('[ai]', error),
104
+ }).toTextStreamResponse()
105
+ })
106
+ // embedding + 缓存:相同输入不再调模型
107
+ .post('/api/embed', async (req, ctx) => {
108
+ const { text } = (await req.json()) as { text: string }
109
+ const key = `embed:${encodeURIComponent(text)}`
110
+ const cached = await ctx.redis.get(key)
111
+ if (cached !== null) return json({ vector: JSON.parse(cached) })
112
+ const { embeddings } = await embedMany({ model: ctx.ai.embed, values: [text] })
113
+ const vector = embeddings[0]
114
+ await ctx.redis.set(key, JSON.stringify(vector), 'EX', 3600)
115
+ return json({ vector })
116
+ })
117
+
118
+ export default app
119
+ ```
120
+
121
+ ## 前端:`useChat` 对接
122
+
123
+ React Router 应用里(`app/routes/chat.tsx`):
124
+
125
+ ```tsx
126
+ import { useChat } from '@ai-sdk/react'
127
+ import { DefaultChatTransport } from 'ai'
128
+
129
+ export default function Chat() {
130
+ const { messages, sendMessage, status } = useChat({
131
+ transport: new DefaultChatTransport({ api: '/api/chat' }),
132
+ })
133
+
134
+ return (
135
+ <form
136
+ onSubmit={(event) => {
137
+ event.preventDefault()
138
+ const input = event.currentTarget.elements.namedItem('prompt')
139
+ if (!(input instanceof HTMLInputElement)) return
140
+ sendMessage({ text: input.value })
141
+ input.value = ''
142
+ }}
143
+ >
144
+ {messages.map((message) => (
145
+ <article key={message.id}>
146
+ <b>{message.role}</b>
147
+ {message.parts.map((part, index) =>
148
+ part.type === 'text' ? <p key={index}>{part.text}</p> : null,
149
+ )}
150
+ </article>
151
+ ))}
152
+ <input name="prompt" disabled={status === 'streaming'} />
153
+ </form>
154
+ )
155
+ }
156
+ ```
157
+
158
+ 流式 Response 是 weifuwu 的原生形态:handler 直接 `return` 流即可,不需要 adapter。
159
+
160
+ ## 必须知道的几件事
161
+
162
+ - **重试与 `RetryError`**:AI SDK 默认 `maxRetries: 2`,且遵守 `retry-after`——上游限流时
163
+ 请求可能挂十几秒,最终抛出的 `RetryError` 包着 `APICallError`。上面的 `toHttpError`
164
+ 会自动解包;API 路由里通常建议 `maxRetries: 0`(把重试留给客户端),或显式设置预算。
165
+ - **流开始后不能改状态码**:`streamText` 一旦开始输出,错误只能走流内(`onError` /
166
+ UI 消息流的 error part)。所以"状态码映射"只对进入流之前的失败生效,
167
+ 这也是上面对 `/api/extract` 用 try/catch、对流式路由用 `onError` 的原因。
168
+ - **key 只在服务端**:不要放进 loader 返回值或客户端组件;`ctx.ai` 本来就只在 handler 里。
169
+ - **限流 / 配额**:用 `ctx.redis` 的 `INCR` + `EXPIRE` 就是最小可用实现(见
170
+ [docs/data.md](./docs/data.md))。
171
+ - **测试不需要 key**:`ai/test` 的 `MockLanguageModelV4` / `MockEmbeddingModelV4`
172
+ 可完全离线断言文本、用量、流式分块;provider 真链路也可以起一个本地
173
+ OpenAI 兼容 mock(SSE)后用 `createOpenAI({ baseURL })` 指向它。
174
+ - **`ctx.ai` 不封装函数**:不做 `ctx.ai.text()` 之类的门面,避免绑定 SDK 版本节奏;
175
+ 新能力(`ToolLoopAgent`、`Output`、`useChat`)随 `npm i ai` 直接可用。
package/docs/data.md ADDED
@@ -0,0 +1,81 @@
1
+ # 数据资源:`ctx.sql` 与 `ctx.redis`
2
+
3
+ `resource()` 是懒初始化的共享资源中间件:首个请求时创建、进程内单例、
4
+ `app.close()` 时清理。官方提供两个子路径:
5
+
6
+ | 子路径 | 注入 | 默认连接串 | 关闭动作 |
7
+ | --- | --- | --- | --- |
8
+ | `@weifuwujs/weifuwu/postgres` | `ctx.sql`(postgres.js) | `DATABASE_URL` | `sql.end()` |
9
+ | `@weifuwujs/weifuwu/ioredis` | `ctx.redis`(ioredis) | `REDIS_URL` | `quit()`(等 `end` 事件) |
10
+
11
+ 两者都是 `peerDependencies`(optional):不用就不会安装、不会加载,
12
+ 根入口也不受牵连。
13
+
14
+ ## 快速开始
15
+
16
+ ```bash
17
+ npm i postgres ioredis
18
+
19
+ # 本地服务(仓库自带 compose:pg18 + redis7)
20
+ docker compose up -d
21
+ # postgres://root:123456@127.0.0.1:5432/demo
22
+ # redis://127.0.0.1:6379
23
+ ```
24
+
25
+ ```ts db
26
+ import { createApp, json } from '@weifuwujs/weifuwu'
27
+ import { postgres } from '@weifuwujs/weifuwu/postgres'
28
+ import { redis } from '@weifuwujs/weifuwu/ioredis'
29
+
30
+ const app = createApp()
31
+ .use(postgres()) // ctx.sql,默认读 DATABASE_URL
32
+ .use(redis()) // ctx.redis,默认读 REDIS_URL
33
+ .get('/users', async (req, ctx) => {
34
+ const cached = await ctx.redis.get('users')
35
+ if (cached) return json(JSON.parse(cached))
36
+
37
+ const rows =
38
+ await ctx.sql<{ id: number; name: string }[]>`select id, name from users`
39
+ await ctx.redis.set('users', JSON.stringify(rows), 'EX', 30)
40
+ return json(rows)
41
+ })
42
+
43
+ export default app
44
+ ```
45
+
46
+ 也可以显式传连接串与 options:
47
+
48
+ ```ts
49
+ app.use(postgres('postgres://user:pass@host:5432/db', { max: 10 }))
50
+ app.use(redis('redis://host:6379', { lazyConnect: true }))
51
+ ```
52
+
53
+ ## 生命周期
54
+
55
+ - **懒创建**:首个请求才连接(此时 `.env` 已加载);并发首请求只建一个连接。
56
+ - **失败不缓存**:首次建连失败 → 500(原始错误进 `onError` / 日志),下次请求重试。
57
+ - **优雅关闭**:`app.close()` 或 CLI 的 `SIGINT` / `SIGTERM` → 按 LIFO 执行清理。
58
+ ioredis 的连接不会吊住进程(有 SIGTERM e2e 钉住)。
59
+ - **手动控制**:`const db = postgres(); db.peek()` / `await db.load()` / `await db.close()`。
60
+
61
+ ## 自定义资源
62
+
63
+ ```ts
64
+ import { createApp, resource } from '@weifuwujs/weifuwu'
65
+
66
+ const mailer = resource('mailer', () => createMailer(), {
67
+ close: (client) => client.close(),
68
+ })
69
+
70
+ const app = createApp().use(mailer) // ctx.mailer
71
+ ```
72
+
73
+ ## 说明与坑
74
+
75
+ - **事务不自动包裹**:需要时在 handler 里用 `ctx.sql.begin(...)`,避免中间件隐式行为。
76
+ - **池/重连策略沿用客户端默认**:通过 options 透传(如 `{ max: 10 }`、`{ lazyConnect: true }`)。
77
+ - **关闭语义**:`await app.close()` 返回时,redis 连接已到 `end` 状态(v6 的 `quit()`
78
+ 本身在 `end` 事件之前 resolve,weifuwu 等事件,1s 兜底)。
79
+ - **守卫**:根入口不静态加载 `postgres` / `ioredis`(`test/root-entry.test.ts` 用
80
+ ESM resolve 钩子证明)。
81
+ - **测试**:`test/db.test.ts` 连真库;服务不可达时显式 skip。先 `docker compose up -d`。
@@ -0,0 +1,116 @@
1
+ # 迁移到 weifuwu 全栈
2
+
3
+ 两条路径:**A. 从官方 `node-custom-server` 模板迁移**(Express 适配器 →
4
+ weifuwu 单端口);**B. 从纯 weifuwu 迁移**(加 React Router 页面层)。
5
+
6
+ ## A. 从官方 `node-custom-server` 模板迁移
7
+
8
+ 官方模板用 Express(`@react-router/express`)做自定义服务器,且 dev 的 HMR
9
+ 跑在独立端口。weifuwu 不需要 Express,dev/prod 同一个端口。
10
+
11
+ | 官方模板 | weifuwu |
12
+ | --- | --- |
13
+ | `server.js` + `server/app.ts` | `app/server.ts`(默认导出 App,CLI 负责 listen) |
14
+ | `@react-router/express` 适配器 | `@weifuwujs/weifuwu/react-router` 的 `reactRouter()` |
15
+ | `vite.config.ts` 手写插件/中间件 | `defineConfig()`(自动注册 RR 插件与 SSR 入口) |
16
+ | `react-router dev/build/start` | `weifuwu dev/build/start` |
17
+ | HMR 独立端口(24678) | 同一端口(`server.ws.server`) |
18
+
19
+ 步骤:
20
+
21
+ 1. 保留 `app/`(路由、`root.tsx`)与 `react-router.config.ts`;
22
+ 2. 删除 `server.js`、`server/app.ts` 以及 express / `@react-router/express`
23
+ 依赖;
24
+ 3. 新建 `app/server.ts`:
25
+
26
+ ```ts migration-after
27
+ import { createApp, json } from '@weifuwujs/weifuwu'
28
+ import { reactRouter } from '@weifuwujs/weifuwu/react-router'
29
+
30
+ export default createApp()
31
+ .get('/api/health', () => json({ ok: true }))
32
+ .notFound(reactRouter())
33
+ ```
34
+
35
+ 4. `vite.config.ts`:
36
+
37
+ ```ts
38
+ import { defineConfig } from '@weifuwujs/weifuwu/vite'
39
+
40
+ export default defineConfig({})
41
+ ```
42
+
43
+ 5. `package.json`:
44
+
45
+ ```json
46
+ {
47
+ "scripts": {
48
+ "dev": "weifuwu dev",
49
+ "build": "weifuwu build",
50
+ "start": "weifuwu start",
51
+ "typegen": "weifuwu typegen",
52
+ "typecheck": "weifuwu typegen && tsc -p tsconfig.json"
53
+ }
54
+ }
55
+ ```
56
+
57
+ 6. 需要把 weifuwu 中间件注入 RR loader 时,加
58
+ `reactRouter({ getLoadContext })`(写法见
59
+ [react-router.md](./react-router.md#context-bridgeweifuwu-中间件--rr-loader))。
60
+
61
+ 更省事的做法:`npx @weifuwujs/weifuwu init my-app` 生成骨架,
62
+ 再把原来的 `app/` 拷进去。
63
+
64
+ ## B. 从纯 weifuwu 迁移
65
+
66
+ 迁移前的服务入口通常长这样:
67
+
68
+ ```js
69
+ // server.js(node server.js 启动)
70
+ import { createApp, json } from '@weifuwujs/weifuwu'
71
+
72
+ const app = createApp().get('/api/health', () => json({ ok: true }))
73
+ app.listen(3000)
74
+ ```
75
+
76
+ 步骤:
77
+
78
+ 1. 用 `npx @weifuwujs/weifuwu init my-app` 生成工程骨架,把现有路由
79
+ (`.get/.post/...` 与中间件)搬进 `app/server.ts`;
80
+ 2. 去掉 `app.listen(...)`,改为**默认导出 App**;在末尾加
81
+ `.notFound(reactRouter())` 接管页面;
82
+ 3. 把页面组件放进 `app/routes/`,在 `app/routes.ts` 注册;
83
+ 4. `npm run dev` 验证 API 与页面同端口可用;`npm run typecheck`
84
+ 会先跑 typegen 再检查类型(单独生成用 `npm run typegen`)。
85
+
86
+ ## C. 从 0.8 全家桶迁到 0.9 内核 + 生态包
87
+
88
+ 0.8 内置的用户系统 / 任务队列 / 站内信 / 单源 API 已外迁为独立包;**0.9 起旧子路径与子命令不再提供
89
+ (硬切断,无 shim)**。迁移 = 换导入 + 换 bin,业务代码只需改动 `ctx` 注入处。
90
+
91
+ 导入映射:
92
+
93
+ | 0.8(内置子路径) | 0.9(独立包) |
94
+ | --- | --- |
95
+ | `@weifuwujs/weifuwu/auth` | `@weifuwujs/auth` |
96
+ | `@weifuwujs/weifuwu/tasks` | `@weifuwujs/tasks` |
97
+ | `@weifuwujs/weifuwu/messages` | `@weifuwujs/messages` |
98
+ | `@weifuwujs/weifuwu/api` | `@weifuwujs/api` |
99
+
100
+ bin 映射(`weifuwu` 子命令 → 独立 bin,参数不变):
101
+
102
+ | 0.8 | 0.9 |
103
+ | --- | --- |
104
+ | `weifuwu auth migrate` / `user:list` / `user:disable` / `user:reset-password` | `weifuwu-auth migrate` / `user:list` / `user:disable` / `user:reset-password` |
105
+ | `weifuwu tasks migrate` / `work` / `list` / `retry` / `prune` | `weifuwu-tasks migrate` / `work` / `list` / `retry` / `prune` |
106
+ | `weifuwu messages migrate` | `weifuwu-messages migrate` |
107
+
108
+ `init --auth` / `init --messages` 叠加旗标已删除:`init` 只生成内核骨架,叠加能力按各包 README 接入。
109
+ store 选项(`--store`)随命令移动到对应包 bin(如 `weifuwu-tasks migrate --store postgres`)。
110
+
111
+ ## 迁移后自检
112
+
113
+ - `npm run typegen && npx tsc -p tsconfig.json` → 0 error;
114
+ - `npm run dev` 与 `npm run build && npm run start` 行为一致;
115
+ - `/api/health` 与一条页面路由都能访问;
116
+ - 反代场景加 `--trust-proxy`,见 [react-router.md](./react-router.md#运维)。
@@ -0,0 +1,187 @@
1
+ # React Router 全栈集成
2
+
3
+ weifuwu 把 React Router v8(framework mode)作为**页面层**接入,自身提供
4
+ API/WebSocket/中间件层。目标是:一个进程、一个端口,dev 与 prod 行为一致。
5
+
6
+ ## 架构
7
+
8
+ ```
9
+ 请求 ──▶ weifuwu HTTP server ──分支──▶ weifuwu 路由(/api、/ws、静态文件)
10
+ └──▶ 未命中 ──▶ reactRouter() ──▶ RR SSR/数据加载
11
+ ```
12
+
13
+ - `app/server.ts` **默认导出 weifuwu App**,只描述应用,不负责 `listen`;
14
+ 进程与端口由 `weifuwu` CLI 拥有。
15
+ - RR 挂载点为 `app.notFound(reactRouter())`:weifuwu 中间件在外圈对页面
16
+ 同样生效;未命中的请求(含所有页面 URL)交给 RR。
17
+ - 构建时 `react-router build` 的 SSR 入口就是 `app/server.ts`,
18
+ `build/server/index.js` 即 weifuwu 应用。
19
+
20
+ ## 接入
21
+
22
+ ```ts
23
+ import { createApp, json } from '@weifuwujs/weifuwu'
24
+ import { reactRouter } from '@weifuwujs/weifuwu/react-router'
25
+
26
+ export default createApp()
27
+ .get('/api/health', () => json({ ok: true }))
28
+ .notFound(reactRouter())
29
+ ```
30
+
31
+ Vite 侧使用预设(自动注册 RR 插件、设置 SSR 入口、把 weifuwu 内联进
32
+ SSR bundle):
33
+
34
+ ```ts
35
+ import { defineConfig } from '@weifuwujs/weifuwu/vite'
36
+
37
+ export default defineConfig({})
38
+ ```
39
+
40
+ ## 表单与动作(无 JS 可用)
41
+
42
+ 原生 `<form method="post">` 提交时,**React Router 的 index 路由不处理 POST**
43
+ (用 `react-router dev` 官方 dev server 同样返回 405:`route "root"` 没有
44
+ `action`)。因此表单动作要挂在**有 path 的路由上**:
45
+
46
+ ```ts
47
+ // app/routes.ts
48
+ export default [
49
+ index('routes/home.tsx'),
50
+ route('actions/submit', 'routes/submit-action.tsx'), // action 端点
51
+ ] satisfies RouteConfig
52
+ ```
53
+
54
+ ```tsx
55
+ // 页面:原生表单提交到动作端点
56
+ <Form method="post" action="/actions/submit">…</Form>
57
+ ```
58
+
59
+ 动作端点按 **POST-redirect-GET** 收尾(成功回列表页;失败用
60
+ `?error=` 回列表页,保证无 JS 也能看到错误):
61
+
62
+ ```tsx
63
+ export async function action({ request }: Route.ActionArgs) {
64
+ const form = await request.formData()
65
+ try {
66
+ await doSomething(form)
67
+ } catch (error) {
68
+ return redirect(`/?error=${encodeURIComponent(String(error))}`)
69
+ }
70
+ return redirect('/')
71
+ }
72
+ ```
73
+
74
+ weifuwu 只在路由**完全未匹配**时才把请求交给 RR 兜底处理器:POST 到
75
+ 框架已注册的 GET 路径(如 `/api/health`)仍返回 405,语义不变。
76
+
77
+ ## 单端口 HMR
78
+
79
+ `weifuwu dev` 用 Vite `middlewareMode` + `server.ws.server`,把 Vite HMR
80
+ 挂在 weifuwu 的 HTTP server 上;upgrade 按协议分流(`vite-hmr`/`vite-ping`
81
+ 给 Vite,其余给应用 WebSocket)。因此不需要 RR 默认的 24678 端口。
82
+
83
+ ## context bridge(weifuwu 中间件 → RR loader)
84
+
85
+ weifuwu 的 `Context` 与 RR 的 loader context 是两套对象,用
86
+ `RouterContextProvider` 显式桥接:
87
+
88
+ ```ts
89
+ import { createContext } from 'react-router'
90
+ import type { Middleware } from '@weifuwujs/weifuwu'
91
+ import { RouterContextProvider } from 'react-router'
92
+
93
+ type AppContext = Context & { greeting: string }
94
+
95
+ const withGreeting: Middleware<Context, AppContext> = async (req, ctx, next) =>
96
+ next(req, { ...ctx, greeting: 'hello' })
97
+
98
+ export default createApp()
99
+ .use(withGreeting)
100
+ .notFound(
101
+ reactRouter<AppContext>({
102
+ getLoadContext(_request, ctx) {
103
+ const loadContext = new RouterContextProvider()
104
+ loadContext.set(greetingContext, ctx.greeting)
105
+ return loadContext
106
+ },
107
+ }),
108
+ )
109
+ ```
110
+
111
+ loader 里用 `context.get(greetingContext)` 读取(见
112
+ `examples/react-router/app/`)。
113
+
114
+ ## 类型:`+types` 与 typegen
115
+
116
+ - RR typegen 生成 `.react-router/types/**`;路由模块用
117
+ `import type { Route } from './+types/<route>'`,配合
118
+ `Route.LoaderArgs` / `Route.ComponentProps` / `Route.ErrorBoundaryProps`。
119
+ - `weifuwu dev` 内置 typegen watch(RR 插件自带),文件变更即时更新类型。
120
+ - CI/一次性检查:`weifuwu typegen && tsc -p tsconfig.json`
121
+ (模板的 `npm run typecheck` 即此命令)。
122
+ - tsconfig 需要 `rootDirs: [".", "./.react-router/types"]`;
123
+ 若源码用 Node 原生 `.ts` 导入,再加 `allowImportingTsExtensions`。
124
+
125
+ ## dev 派发与保留命名空间
126
+
127
+ dev 请求先判断 Vite 命名空间,其余 **app-first**(与 prod 顺序一致):
128
+
129
+ - Vite 保留:`/@*`、`/app/**`、`/node_modules/**`、`/.vite/**`、`/__*`,
130
+ 以及 `public/` 下**已存在**的文件;
131
+ - 其余(API、WebSocket、页面 URL)由 weifuwu App 处理,未命中再进 RR。
132
+
133
+ 因此不要把你的 weifuwu 路由放在上述保留前缀下。
134
+
135
+ ## 故障降级
136
+
137
+ 若 RR/Vite 配置损坏(例如 `routes.ts` 语法错误)导致 `createServer` 失败:
138
+
139
+ - `weifuwu dev` **进程不退出**,进入降级模式:`public/` 静态文件继续由
140
+ 内置静态中间件提供,weifuwu 路由经 Node 原生 import 加载 `app/server.ts`
141
+ 继续服务;
142
+ - 此时 RR 页面不可用(依赖 Vite 管线);
143
+ - dev 下会安装 `unhandledRejection` 护栏(仅记录日志,避免 RR typegen
144
+ watcher 的未处理异常杀进程)。修好配置后建议重启 dev。
145
+
146
+ ## 运维
147
+
148
+ ### `.env`
149
+
150
+ `weifuwu start` 会加载(真实环境变量优先,文件只补缺):
151
+
152
+ ```
153
+ .env < .env.local < .env.${NODE_ENV} < .env.${NODE_ENV}.local
154
+ ```
155
+
156
+ dev 下的 `.env` 由 RR/Vite 的 dotenv 负责。
157
+
158
+ ### 反向代理(trustProxy)
159
+
160
+ `weifuwu start --trust-proxy`(或 dev 同参数)后,`X-Forwarded-Proto` /
161
+ `X-Forwarded-Host` 会参与构造 `request.url`(forwarded-host 优先于 Host)。
162
+ 未开启时这两个头被忽略,避免伪造。也可在代码里用
163
+ `toNodeHandler(app, { trustProxy: true })`。
164
+
165
+ ### 静态缓存
166
+
167
+ 生产静态策略(`weifuwu start`):
168
+
169
+ - `build/client/assets/**`(带 hash):`Cache-Control: public, max-age=31536000, immutable`;
170
+ - 其余 `build/client/**`(如 `robots.txt`):`public, max-age=3600`。
171
+
172
+ ## 部署检查清单
173
+
174
+ 1. `weifuwu build` 产出 `build/client` 与 `build/server`;
175
+ 2. `NODE_ENV=production weifuwu start`(同端口同时服务页面与 API);
176
+ 3. 反代时加 `--trust-proxy`,并按需设置 `.env.production`;
177
+ 4. 健康检查 `/api/health`(不要只探页面)。
178
+
179
+ ## 已知限制
180
+
181
+ - `app/server.ts` 顶层副作用会在 dev HMR 时重跑:长生命周期资源
182
+ (连接池等)请放独立模块。
183
+ - `unhandledRejection` 护栏会吞掉(并记录)dev 下所有未处理异步错误。
184
+ - 降级模式下 RR 页面不可用。
185
+ - **index 路由不处理 POST**:表单动作要挂在有 path 的路由上,见"表单与动作"。
186
+ - POST 到框架已注册的路径(GET 路由)返回 405,不会落到 RR 兜底处理器
187
+ (避免 API 路径的 405 语义被页面渲染覆盖)。
@@ -0,0 +1,69 @@
1
+ # 样式与 UI
2
+
3
+ ## Tailwind CSS(`weifuwu init` 默认)
4
+
5
+ `init` 生成的工程已接好 Tailwind CSS v4,无需额外配置:
6
+
7
+ | 文件 | 作用 |
8
+ | --- | --- |
9
+ | `app/app.css` | `@import 'tailwindcss';`(在模块图内,才会被 Vite 处理) |
10
+ | `app/root.tsx` | `import './app.css'`,`<Links />` 输出样式表 |
11
+ | `vite.config.ts` | `defineConfig({ plugins: [tailwindcss()] })` |
12
+
13
+ - 自定义主题:直接在 `app/app.css` 里用 v4 的 `@theme` / `@custom-variant`,
14
+ 不需要 `tailwind.config.js`。
15
+ - 动态拼接的类名(如 `` `bg-${color}-500` ``)不会被探测到,属于 Tailwind
16
+ 通用限制;枚举映射或安全名单处理。
17
+ - Vite 降级模式下(`weifuwu dev` 启动失败转为纯 Node)页面无样式,
18
+ 与「降级模式下 RR 页面不可用」是同一类边界。
19
+
20
+ ## shadcn/ui(官方路径,一条命令)
21
+
22
+ 模板已满足 shadcn 的前置条件:Tailwind v4、TypeScript、`~/*` 别名
23
+ (`tsconfig.json` 的 `paths` + weifuwu 在 dev/prod 两侧的解析器,
24
+ 见 `src/vite.ts` 的 `weifuwu:tilde-app-alias`)。
25
+
26
+ ```bash
27
+ weifuwu init my-app && cd my-app && npm install
28
+
29
+ # 初始化(交互式;非交互:-y -b radix -p nova)
30
+ npx shadcn@latest init
31
+
32
+ # 添加组件
33
+ npx shadcn@latest add button
34
+ ```
35
+
36
+ 实测(`shadcn@4.21.0`,`-y -b radix -p nova --no-monorepo`):
37
+
38
+ - `init` 正确识别并沿用 `~/components`、`~/lib/utils` 别名,生成
39
+ `components.json` 与 `app/lib/utils.ts`,并把主题变量追加进 `app/app.css`
40
+ (`@import "shadcn/tailwind.css"`、`@theme inline`、`:root` 变量)——
41
+ 这是 shadcn 的正常行为。
42
+ - `add button` 后渲染组件,`weifuwu build` / `weifuwu start` 通过,
43
+ 组件用到的 utility 出现在构建 CSS 里,SSR 输出的页面包含组件。
44
+
45
+ 注意事项:
46
+
47
+ - `components.json` 的 aliases 用 `~/...`(weifuwu 已支持);不要改成 `@/`,
48
+ 否则需要自己补 alias 配置。
49
+ - 不要改 `vite.config.ts`:Tailwind 插件已经在了。
50
+ - 组件代码归你所有(这正是 shadcn 的模型)。weifuwu **不** 预置任何组件或
51
+ 主题;升级组件用 `npx shadcn@latest add <name> --reinstall` 等官方命令。
52
+ - CLI 的 preset / 参数会随版本演进(例如 preset 名称),以 `npx shadcn@latest init --help`
53
+ 为准。
54
+
55
+ ### 兼容性测试
56
+
57
+ - 离线回归:`test/init.test.ts` 的「shadcn 形态的组件可构建并渲染」用例,
58
+ 验证 `~/` 解析、`cn()` 形态、组件 utility 进 CSS、SSR 渲染。
59
+ - 真 CLI e2e(需网络,默认跳过):
60
+
61
+ ```bash
62
+ SHADCN_E2E=1 node --test test/shadcn.test.ts
63
+ ```
64
+
65
+ ## 其他 UI 方案
66
+
67
+ Tailwind 只是构建管线,不是设计系统:用自研组件、其他组件库
68
+ (自行 `npm install`)都可以;不想要样式管线时删掉 `app/app.css` 与
69
+ `vite.config.ts` 里的 `tailwindcss()` 即可。