weifuwu 0.74.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/frontend.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 前端 API 核心(weifuwu/ui-dom)
2
2
 
3
- > ⚠️ **`weifuwu/client` 已删除**——本页为历史文档。前端运行时唯一入口是 **`weifuwu/ui-dom`**(UIRouter 纯路由 + uiServe 渲染运行时 + SSR/hydration),权威参考见 **[docs/frontend-ui-dom.md](frontend-ui-dom.md)**。本页的 createApp/router/RouteView 用法已失效,对应新 API 为 `uiServe` / `UIRouter`。
3
+ > ⚠️ **`weifuwu/client` 已并入 `weifuwu/ui-dom`**(`src/client/` 已删除)——本页 import 均用 `weifuwu/ui-dom`。前端运行时唯一入口是 **`weifuwu/ui-dom`**(UIRouter 纯路由 + uiServe 渲染运行时 + SSR/hydration),权威参考见 **[docs/frontend-ui-dom.md](frontend-ui-dom.md)**。
4
4
 
5
5
  > 以下为完整 API 参考,按需查阅。新手建议先阅读 README 的「核心概念」和「快速开始」。
6
6
 
@@ -13,40 +13,37 @@
13
13
  ```js
14
14
  esbuild.build({
15
15
  jsx: 'automatic',
16
- jsxImportSource: 'weifuwu/client',
16
+ jsxImportSource: 'weifuwu/ui-dom',
17
17
  bundle: true,
18
18
  })
19
19
  ```
20
20
 
21
21
  ---
22
22
 
23
- ## createApp — 应用引导
23
+ ## 应用引导(UIRouter + uiServe)
24
24
 
25
25
  ```tsx
26
- import { createApp } from 'weifuwu/client'
26
+ import { UIRouter, uiServe } from 'weifuwu/ui-dom'
27
27
 
28
- const app = createApp()
28
+ const app = new UIRouter()
29
29
 
30
- // 注册中间件
30
+ // 注册中间件(ctx 注入)
31
31
  app.use(middleware1)
32
32
  app.use(middleware2)
33
33
 
34
- // 挂载到 DOM
35
- app.mount('#root', RootComponent)
34
+ // 页面路由(handler = 异步组件)
35
+ app.get('/', () => h(Home, {}))
36
36
 
37
- // 获取当前 ctx
38
- console.log(app.ctx)
39
-
40
- // 销毁
41
- app.destroy()
37
+ // 落地:客户端渲染(hydrate: true 收养 SSR HTML)
38
+ const handle = uiServe(app, { root: '#root' })
42
39
  ```
43
40
 
44
- | 方法 | 说明 |
41
+ | API | 说明 |
45
42
  |------|------|
46
- | `createApp()` | 创建应用实例 |
47
- | `app.use(mw)` | 注册 AppMiddleware |
48
- | `app.mount(selector, RootComponent)` | 挂载到 DOM |
49
- | `app.destroy()` | 卸载应用 |
43
+ | `new UIRouter()` | 创建路由实例 |
44
+ | `app.use(mw)` | 注册中间件(AppMiddleware / UIMiddleware) |
45
+ | `app.get(path, handler)` / `use(prefix, sub)` / `notFound(handler)` | 页面路由 / 子路由树 / 404 |
46
+ | `uiServe(app, { root, hydrate })` | 客户端落地(返回 handle,`handle.close()` 卸载) |
50
47
  | `app.ctx` | 当前 WfuiContext |
51
48
 
52
49
  ---
@@ -54,7 +51,7 @@ app.destroy()
54
51
  ## 组件模型
55
52
 
56
53
  ```tsx
57
- import type { Component, WfuiContext } from 'weifuwu/client'
54
+ import type { Component, WfuiContext } from 'weifuwu/ui-dom'
58
55
 
59
56
  // 两阶段组件:mount(只一次)→ render(每次 dirty/props 变化)
60
57
  const Counter: Component = (_init, ctx) => {
@@ -74,8 +71,8 @@ const Badge: Component = () =>
74
71
  ### 类型流(props 泛型 + ctx 注入)
75
72
 
76
73
  ```tsx
77
- import type { Component } from 'weifuwu/client'
78
- import type { ApiInjected, RouteInjected } from 'weifuwu/client'
74
+ import type { Component } from 'weifuwu/ui-dom'
75
+ import type { ApiInjected, RouteInjected } from 'weifuwu/ui-dom'
79
76
 
80
77
  // ① props 泛型:JSX 使用时自动类型检查(传错类型编译期报错)
81
78
  interface DeckCardProps { title: string; pages: number }
@@ -92,13 +89,13 @@ const Home: Component<{}, ApiInjected & RouteInjected> = (_init, ctx) => {
92
89
  }
93
90
  // 未声明的注入字段编译期报错——注入从"文档约定"变成"类型保证"
94
91
 
95
- createApp()
92
+ const app = new UIRouter<{}>()
96
93
  .use(api()) // 注入 ctx.api
97
- .use(router({ routes })) // 注入 ctx.route / ctx.app
98
- .mount('#root', Home) // mount 时类型累积完整
94
+ .use(toast()) // 注入 ctx.toast
95
+ uiServe(app, { root: '#root' }) // 类型累积完整(UIRouter<C & O>)
99
96
  ```
100
97
 
101
- > 各中间件的注入接口:`api()` → `ApiInjected`、`auth()` → `AuthInjected`、`ws()` → `WsInjected`、`i18n()` → `I18nInjected`、`router()` → `RouteInjected`(均可从 `weifuwu/client` 导入)。
98
+ > 各中间件的注入接口:`api()` → `ApiInjected`、`auth()` → `AuthInjected`、`ws()` → `WsInjected`、`i18n()` → `I18nInjected`、`router()` → `RouteInjected`(均可从 `weifuwu/ui-dom` 导入)。
102
99
 
103
100
  | 规则 | 说明 |
104
101
  |------|------|
@@ -113,8 +110,8 @@ createApp()
113
110
  ### JSX 工厂
114
111
 
115
112
  ```tsx
116
- // 由 esbuild 自动调用(jsxImportSource: 'weifuwu/client')
117
- import { h, jsx, jsxs, jsxDEV, Fragment } from 'weifuwu/client'
113
+ // 由 esbuild 自动调用(jsxImportSource: 'weifuwu/ui-dom')
114
+ import { h, jsx, jsxs, jsxDEV, Fragment } from 'weifuwu/ui-dom'
118
115
 
119
116
  // h 支持 variadic children
120
117
  h('div', { class: 'x' }, child1, child2)
@@ -131,7 +128,7 @@ h('div', { class: 'x' }, child1, child2)
131
128
  | `Portal` / `createPortal(children, portalKey?)` | 渲染到 `document.body#__wf_portal` 独立容器(弹层/对话框,脱离父级 overflow 裁剪) |
132
129
 
133
130
  ```tsx
134
- import { createPortal } from 'weifuwu/client'
131
+ import { createPortal } from 'weifuwu/ui-dom'
135
132
 
136
133
  // 内容渲染到 body 下的独立容器(不在父组件的 DOM 树内)
137
134
  const Tooltip = (_init, ctx) =>
@@ -754,7 +751,7 @@ const UserProfile: Component = (initProps, ctx) => {
754
751
  }
755
752
  ```
756
753
 
757
- ### async 组件(原生——无需 asyncComponent 包装)
754
+ ### async 组件(原生)
758
755
 
759
756
  组件 = 函数,async 组件 = async 函数:签名与同步组件一致 `(initProps, ctx) => renderFn`,渲染器按「返回值是 Promise」原生判别。数据经闭包注入,渲染无 loading 分支:
760
757
 
@@ -771,9 +768,9 @@ const UserProfile = async (initProps, ctx) => {
771
768
  }
772
769
  ```
773
770
 
774
- - **客户端**:首次渲染占位 → 工厂 resolve 后整树重渲染补全(SPA);数据经 `ctx.data` 缓存(hydration 时从 `__DATA__` 同步命中,不重跑请求)
771
+ - **客户端**:首次渲染占位(`Placeholder`)→ 工厂 resolve 后整树重渲染补全;`_asyncDef` 按实例缓存(diff 传递继承,补全不重跑工厂)——N 处实例 = N 次工厂调用,数据走 `ctx.data` 则零成本(缓存 + 并发合并)
775
772
  - **服务端**:`ctx.ui.ssr()` 直接 await 工厂 → 数据进 HTML(无占位)
776
- - 工厂缓存绑定页面上下文:路由导航/登录登出时自动失效,工厂以新 ctx 重新执行
773
+ - **占位显示**:无边界 → null;`<Suspense fallback={...}>` 边界 → 子树内占位处显示 fallback(可选)
777
774
  - 会变的数据:初始值 seed 自服务端数据(`$.count = data.count`),交互改 `$`;初始状态必须确定性(禁止 `window.innerWidth` 直接初始化 → SSR/hydration mismatch)
778
775
 
779
776
  ---
@@ -781,15 +778,15 @@ const UserProfile = async (initProps, ctx) => {
781
778
  ## 前端类型
782
779
 
783
780
  ```tsx
784
- import type { VNode, VNodeType, Component, WfuiContext, AppMiddleware, RouteDef } from 'weifuwu/client'
785
- import type { ApiClient, ApiOptions, ApiRequestOptions, ApiError } from 'weifuwu/client'
786
- import type { AuthClient, AuthOptions } from 'weifuwu/client'
787
- import type { ErrorBoundaryProps } from 'weifuwu/client'
788
- import type { I18nOptions, I18nState, LocalePackage } from 'weifuwu/client'
789
- import type { PopupPositionOptions, PopupPosition } from 'weifuwu/client'
781
+ import type { VNode, VNodeType, Component, WfuiContext, AppMiddleware, RouteDef } from 'weifuwu/ui-dom'
782
+ import type { ApiClient, ApiOptions, ApiRequestOptions, ApiError } from 'weifuwu/ui-dom'
783
+ import type { AuthClient, AuthOptions } from 'weifuwu/ui-dom'
784
+ import type { ErrorBoundaryProps } from 'weifuwu/ui-dom'
785
+ import type { I18nOptions, I18nState, LocalePackage } from 'weifuwu/ui-dom'
786
+ import type { PopupPositionOptions, PopupPosition } from 'weifuwu/ui-dom'
790
787
  import type { ConfirmProps, ConfirmOptions } from 'weifuwu/components'
791
788
  import type { ToastOptions, ToastPosition } from 'weifuwu/components'
792
- import type { RouterOptions } from 'weifuwu/client'
789
+ import type { RouterOptions } from 'weifuwu/ui-dom'
793
790
  ```
794
791
 
795
792
  | 类型 | 说明 |
package/docs/realtime.md CHANGED
@@ -70,13 +70,12 @@ app.get('/page', (req, ctx) => ctx.ui.html`
70
70
  ```ts
71
71
  app.get('/app.js', (req, ctx) => ctx.ui.js('./src/main.tsx')) // 相对路径
72
72
 
73
- > ⚠️ **weifuwu/client 已删除**——前端运行时唯一入口为 `weifuwu/ui-dom`,见 [frontend-ui-dom.md](frontend-ui-dom.md)。
74
- app.get('/app.js', (req, ctx) => ctx.ui.js('weifuwu/client')) // 或包名
73
+ app.get('/app.js', (req, ctx) => ctx.ui.js('weifuwu/ui-dom')) // 或包名(exports map 解析)
75
74
  ```
76
75
 
77
76
  使用 esbuild 编译:
78
77
  - `bundle: true`, `format: 'esm'`, `platform: 'browser'`
79
- - `jsx: 'automatic'`, `jsxImportSource: 'weifuwu/client'`
78
+ - `jsx: 'automatic'`, `jsxImportSource: 'weifuwu/ui-dom'`
80
79
  - 带 mtime 缓存验证(开发时编辑文件后自动失效)
81
80
 
82
81
  ### ctx.ui.css — CSS 编译
@@ -129,10 +128,11 @@ app.get('/blog/:slug', async (req, ctx) => {
129
128
  服务端 HTML + `window.__DATA__`(ctx.data 种子)到达客户端后,`mount(..., { hydrate: true })` **收养现有 DOM**(不重建、不闪跳),只接线事件/ref/$:
130
129
 
131
130
  ```ts
132
- import { createApp } from 'weifuwu/client'
131
+ import { UIRouter, uiServe } from 'weifuwu/ui-dom'
133
132
 
134
- createApp()
135
- .mount('#root', BlogPage, { hydrate: true }) // 容器已有服务端 HTML
133
+ const app = new UIRouter()
134
+ app.get('/blog/:slug', (loc, ctx) => h(BlogPage, { slug: ctx.params.slug }))
135
+ uiServe(app, { root: '#root', hydrate: true }) // 容器已有服务端 HTML → 收养
136
136
  ```
137
137
 
138
138
  - **游标收养**:元素/文本按位置匹配现有 DOM;tag 不匹配 → 局部替换;文本不一致 → 就地修正;服务端多余节点 → 收尾清理
@@ -140,32 +140,27 @@ createApp()
140
140
  - hydration 后 `$`/dirty/事件全量可用(与纯 SPA 无差别)
141
141
  - 诚实裁剪:Portal 内容就地收养(不移动到 `#__wf_portal`);渲染期非确定性(Date/random)会导致 mismatch(dev 警告)
142
142
 
143
- ### uiSsr — 路由级 SSR(声明即渲染)
143
+ ### 路由级 SSR(UIRouter 两端共享)
144
144
 
145
- 共享路由定义,前后端同一份声明——后端匹配即自动 SSR,无需手写 handler/模板/序列化:
145
+ 同一份 UIRouter 路由定义,后端匹配即自动 SSR,无需手写 handler/模板/序列化:
146
146
 
147
147
  ```tsx
148
- // routes.tsx —— 前后端共用
149
- import type { RouteDef } from 'weifuwu/client'
150
- import { BlogPage } from './pages/BlogPage.tsx'
151
-
152
- export const routes: RouteDef[] = [
153
- { path: '/blog/:slug', component: BlogPage, title: '博客' },
154
- ]
155
-
156
- // server.ts —— 一行中间件:GET 匹配 → 注入 ctx.route.params → await 组件工厂 → 完整 HTML + __DATA__ + bundle
157
- import { uiSsr } from 'weifuwu'
158
- app.use(uiSsr({ routes, bundle: '/static/blog.js' }))
159
-
160
- // blog-hydrate.ts —— 客户端:同一份 routes,router() 注入 ctx.route.params(两端同源)
161
- createApp()
162
- .use(router({ routes }))
163
- .mount('#root', routes[0].component, { hydrate: true })
148
+ // router.ts —— 前后端共用
149
+ import { UIRouter, h } from 'weifuwu/ui-dom'
150
+
151
+ export const app = new UIRouter()
152
+ app.get('/blog/:slug', (loc, ctx) => h(BlogPage, { slug: ctx.params.slug }))
153
+
154
+ // server.ts —— ssrPage 服务端落地:完整 HTML + __DATA__ + styles
155
+ import { ssrPage } from 'weifuwu/ui-dom'
156
+ const { page } = await ssrPage(app, { url: '/blog/hello', styles: ['/components.css'] })
157
+
158
+ // client.ts —— uiServe 客户端收养(hydrate: true)
159
+ uiServe(app, { root: '#root', hydrate: true })
164
160
  ```
165
161
 
166
- - 组件工厂读 `ctx.route.params`(`/blog/:slug` → `ctx.route.params.slug`)——后端 uiSsr / 前端 router **同源注入**
167
- - 未匹配 → next()(交给 API/静态/404);非 GET → next()
168
- - 可自定义 `title` / `template`
162
+ - 组件工厂读 `ctx.params`(`/blog/:slug` → `ctx.params.slug`)——两端同源注入
163
+ - `ssrPage` 选项:`styles`(stylesheet link)/ `title` / `lang` / `rootId`
169
164
 
170
165
  ### weifuwu/dev — 服务端直接跑 .tsx
171
166
 
@@ -180,7 +175,7 @@ Node 原生 TS 只剥离类型(不支持 JSX)。`weifuwu/dev` 注册 esbuild
180
175
  }
181
176
  ```
182
177
 
183
- - 前后端同一 JSX 运行时(`jsxImportSource: weifuwu/client`)→ 两端 VNode 一致 → hydration 可靠
178
+ - 前后端同一 JSX 运行时(`jsxImportSource: weifuwu/ui-dom`)→ 两端 VNode 一致 → hydration 可靠
184
179
  - 与 `ctx.ui.js` 前端动态编译同一理念:无构建、无产物、改代码即生效
185
180
 
186
181
  ---
package/docs/saas.md CHANGED
@@ -12,8 +12,9 @@
12
12
  ```ts
13
13
  import { rateLimit } from 'weifuwu'
14
14
 
15
- app.use(redis()) // 依赖 ctx.redis
16
- app.use(rateLimit({ windowMs: 60_000, max: 100 })) // 全局限流(默认固定窗口)
15
+ const r = redis() // 注意:REDIS_URL 缺 env 构造抛错
16
+ app.use(r) // 依赖 ctx.redis
17
+ app.use(rateLimit({ redis: r.redis, windowMs: 60_000, max: 100 })) // redis 必传(模式 A 显式注入)
17
18
 
18
19
  app.get('/api/search', async (req, ctx) => {
19
20
  await ctx.limit('search', { max: 30, windowMs: 60_000 }) // 手动限流,超限抛 429
@@ -77,7 +78,7 @@ app.post('/secure', (req, ctx) => { ctx.auth.requireAuth(); ... })
77
78
 
78
79
 
79
80
  > ⚠️ **weifuwu/client 已删除**——前端运行时唯一入口为 `weifuwu/ui-dom`,见 [frontend-ui-dom.md](frontend-ui-dom.md)。
80
- - **安全基线**:scrypt 密码哈希(per-user salt + timing-safe,异步不阻塞);access token = HMAC-SHA256 JWT(与 `weifuwu/client` 的 `auth()` 天然配对);refresh token = 不透明随机串,DB 只存哈希,logout/轮换即撤销
81
+ - **安全基线**:scrypt 密码哈希(per-user salt + timing-safe,异步不阻塞);access token = HMAC-SHA256 JWT(与 `weifuwu/ui-dom` 的 `auth()` 天然配对);refresh token = 不透明随机串,DB 只存哈希,logout/轮换即撤销
81
82
  - **防枚举**:登录失败统一 401(不泄露邮箱是否存在)
82
83
  - **`ctx.auth` 方法面**:`register` / `login` / `logout` / `requireAuth` / `setPassword(userId, newPwd)` / `createToken(type, payload, { ttlSeconds })`(邮箱验证/密码重置自接)
83
84
  - **多租户感知**:`issueSession` 的 token payload 携带 `tenantId`(来自 `user.tenant`)——中间件自动注入 `ctx.tenantId`,并将会话字段(userId/tenantId/email/name/role)合并到 `ctx.auth`,多租户应用免写 token 解码/租户中间件(数据隔离 SQL 是应用职责)
@@ -117,7 +118,8 @@ ctx.msg.sendTo('u2', { type: 'mention' }) // 用户维度点对点
117
118
  ```ts
118
119
  import { queue } from 'weifuwu'
119
120
 
120
- const q = queue() // 默认 REDIS_URL
121
+ const r = redis(); app.use(r) // Redis 中间件(注入 ctx.redis)
122
+ const q = queue({ redis: r.redis }) // queue 必传 Redis(池命令走轮询连接)
121
123
  app.use(q) // 注入 ctx.queue
122
124
 
123
125
  app.post('/api/generate', async (req, ctx) => {
@@ -184,10 +186,10 @@ app.post('/api/approve', async (req, ctx) => {
184
186
  })
185
187
  ```
186
188
 
187
- 前端解码(`weifuwu/client`):
189
+ 前端解码(`weifuwu/ui-dom`):
188
190
 
189
191
  ```ts
190
- import { aiStream } from 'weifuwu/client'
192
+ import { aiStream } from 'weifuwu/ui-dom'
191
193
 
192
194
  const handle = aiStream('/api/chat', { messages }, {
193
195
  onToken: (text) => { /* 增量 append 到消息 */ },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.74.0",
4
+ "version": "0.75.0",
5
5
  "description": "AI SaaS framework — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {
@@ -1,41 +0,0 @@
1
- /**
2
- * uiSsr — 路由级 SSR 中间件:前端路由表驱动的自动服务端渲染
3
- *
4
- * 开发者只声明路由(path + component),GET 匹配即自动 SSR:
5
- * 匹配 → 注入 params → ctx.ui.ssr 渲染组件(async 工厂 await)→ 页面模板 + __DATA__
6
- * 未匹配 → next()(交给后续 API/静态/404)
7
- *
8
- * 与客户端 router() 共用同一份路由定义(route-match.ts),SPA/SSR 一个声明。
9
- */
10
- import type { Middleware } from '../types.ts';
11
- import type { RouteDef } from '../ui-dom/types.ts';
12
- export interface UiSsrOptions {
13
- /** 前端路由定义(与 router() 共享同一份) */
14
- routes: RouteDef[];
15
- /** 客户端 bundle 路径(自动注入 <script src>,如 '/static/app.js') */
16
- bundle?: string;
17
- /** CSS 路径数组(自动注入 <link rel="stylesheet">,如 ['/static/style.css']) */
18
- styles?: string[];
19
- /** 自定义 title(默认取路由定义 title 或 'weifuwu') */
20
- title?: (ctx: {
21
- params: Record<string, string>;
22
- def: RouteDef;
23
- }) => string;
24
- /** 自定义页面模板:返回完整 HTML(默认内置,含 head/root/__DATA__/styles/bundle) */
25
- template?: (parts: {
26
- html: string;
27
- dataScript: string;
28
- title: string;
29
- bundle?: string;
30
- styles?: string[];
31
- }) => string;
32
- }
33
- /**
34
- * 路由级 SSR 中间件:
35
- *
36
- * ```ts
37
- * const app = new Router()
38
- * app.use(uiSsr({ routes, bundle: '/static/app.js' }))
39
- * ```
40
- */
41
- export declare function uiSsr(opts: UiSsrOptions): Middleware;
@@ -1,22 +0,0 @@
1
- /**
2
- * 路由匹配纯函数 — 客户端 router 与服务端 uiSsr 共用(无 DOM 依赖)
3
- *
4
- * 支持路径参数 :id、通配符 *、嵌套路由(children → chain)。
5
- * 与后端 Router(Trie)独立——本模块服务于"前端路由表驱动的页面"。
6
- */
7
- import type { RouteDef } from './types.ts';
8
- export interface FlattenedRoute {
9
- re: RegExp;
10
- keys: string[];
11
- def: RouteDef;
12
- chain: RouteDef[];
13
- }
14
- export declare function joinPaths(a: string, b: string): string;
15
- export declare function compilePath(path: string): {
16
- re: RegExp;
17
- keys: string[];
18
- };
19
- export declare function flattenRoutes(routes: RouteDef[], basePath?: string, chain?: RouteDef[]): FlattenedRoute[];
20
- export declare function matchRoute(path: string, routes: FlattenedRoute[]): FlattenedRoute | null;
21
- /** 从匹配结果提取路径参数 */
22
- export declare function extractParams(path: string, match: FlattenedRoute): Record<string, string>;