@fulgurjs/federation 4.0.0 → 4.2.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **fulgurjs** — 拉丁语「闪电 · 辉光」。
4
4
  > 一个把 Vite 模块联邦做到开箱即用的插件:**一套配置,dev / prod 双引擎,语义对齐 Webpack Module Federation**。
5
5
 
6
- ![tests](https://img.shields.io/badge/tests-173%20%2B%20e2e-green) ![runtime](https://img.shields.io/badge/runtime%20gzip-%3C%205KB-blue) ![vite](https://img.shields.io/badge/vite-5%20%7C%206%20%7C%207%20%7C%208-purple)
6
+ ![tests](https://img.shields.io/badge/tests-369%20%2B%20e2e-green) ![runtime](https://img.shields.io/badge/runtime%20gzip-%3C%208KB-blue) ![vite](https://img.shields.io/badge/vite-5%20%7C%206%20%7C%207%20%7C%208-purple)
7
7
 
8
8
  ---
9
9
 
@@ -19,7 +19,7 @@
19
19
  | 运行时体积 | ~40KB+ | 不等 | **gzip < 5KB** |
20
20
  | 配置出错时 | 难排查 | 报错晦涩 | 三段式报错:`got / expected / example` |
21
21
 
22
- **真实工程验证**:某企业级 mes 系统(admin 宿主 + bpm/lowcode 两个子应用,21+6 页)已全量迁移——27 页双环境(dev 双 server / prod NGINX)控制台零报错,逐页写操作闭环(新增/编辑/删除/发布/导入导出/审批流)与原 qiankun 版本逐项一致,首用者从零接线全程有文档可依(见[迁移指南](#文档))。
22
+ **真实工程验证**:某企业级 mes 系统(admin 宿主 + bpm/lowcode 两个子应用,21+6 页)已全量迁移,三个应用各自维护项目根目录的 `fulgurjs.config.ts`、Vite 一处 `federation(fulgurjsConfig)` 接入——27 页双环境(dev 双 server / prod NGINX)控制台零报错,逐页写操作闭环(新增/编辑/删除/发布/导入导出/审批流)与原 qiankun 版本逐项一致,首用者从零接线全程有文档可依(见[迁移指南](#文档))。
23
23
 
24
24
  ## 特性
25
25
 
@@ -31,11 +31,13 @@
31
31
  - **增强能力**:dts 类型直连(dev 补全直达 remote 源码)、`preloadRemote()` manifest 驱动精确预载、runtimePlugins 钩子
32
32
  - **HMR 全链路**:remote 改动 → host 页面热更,L1 组件热替换 / L2 状态保留 / L3 错误覆盖与恢复
33
33
  - **零报错纪律**:配置问题启动瞬间三段式报错;联邦失败显式抛错(错误码 + 可执行修复建议),**无任何静默兜底路径**
34
- - **CLI(主包内置 bin)**:`fulgurjs init`——`fulgurjs.config.ts` 单配置驱动的迁移生成器(模板 = 真实工程验证形态:vite 配置/路由表/桥/联邦启动器/NGINX conf 全量编码,锚点补丁幂等可续跑);`fulgurjs doctor`——部署面体检(remoteEntry/manifest/HTML 缓存头与形态、CORS、chunk 抽样可达、版本 skew 预演、`--dev` 端口探测)
35
- - **跨应用传值与方法引用**:`@fulgurjs/federation/runtime` 导出 `provideAppContext` / `getAppContext` / `requireAppContext`(缺键 `CC-001` 三段式、独立直开远程页 `CC-002` 显式)。宿主桥一次写入页面级单例(user/token/getToken/store/hostApp/locale/events 标准字段 + 项目扩展位),远程 boot 显式校验消费;方法引用两条通道 = context 携带函数引用(热路径直调)+ exposes 方法模块 `loadRemote('remote/api')`(低频重逻辑)。数据语义 = 传输层快照 + 函数引用,非响应式(与乾坤 props 同语义;"实时"靠函数引用拉取 / 宿主 pinia 共享 / 登录刷新三通道)`provideAppContext` / `getAppContext` / `requireAppContext`(缺键 `CC-001` 三段式、独立直开远程页 `CC-002` 显式)。宿主桥一次写入页面级单例(user/token/getToken/store/hostApp/locale/events 标准字段 + 项目扩展位),远程 boot 显式校验消费;方法引用两条通道 = context 携带函数引用(热路径直调)+ exposes 方法模块 `loadRemote('remote/api')`(低频重逻辑)。数据语义 = 传输层快照 + 函数引用,非响应式(与乾坤 props 同语义;"实时"靠函数引用拉取 / 宿主 pinia 共享 / 登录刷新三通道)
34
+ - **CLI(主包内置 bin)**:`fulgurjs init`——**单项目** `fulgurjs.config.ts` 起步模板与校验(默认导出直接是 `federation()` 选项;输出 `federation(fulgurjsConfig)` 接入块与核对清单,**不改写任何项目文件**);`fulgurjs explain`——本应用有效联邦形态与加载链解释器(纯本地,按实际选项判角色,免 `--app`);`fulgurjs check-pages`——宿主页面表 ↔ 远程 manifest exposes 契约核对(`--manifest`/`--site` 指定来源并如实报告;CI 可嵌,确定性错误与 `--require-verified` 均非零退出);`fulgurjs doctor`——部署面体检(remoteEntry/manifest/HTML 缓存头与形态、CORS、chunk 抽样可达、版本 skew 预演、`--dev` 端口探测)
35
+ - **远程初始化生命周期(可选)**:`federation({ setup })` 显式声明初始化入口——默认导出 `setup(context)` 应用级执行一次、可选具名导出 `onSession(context)` 按宿主 `sessionKey` 去重执行(换账号/重登自动重跑,退出 `clearAppContext` 清理会话状态);失败显式报错可重试(`MFU-011~014`),`preloadRemote`/`getContainer` 无副作用。不配置 `setup` 时零行为零体积
36
+ - **宿主页面适配器(可选)**:`createHostPages({ pages, remotePrefixes, ... })`——一份页面表供宿主路由与布局共用;URL 解析(含 base 剥离)、最长前缀远程归属、`definePages` R1–R5 校验、异步组件缓存(会话切换自动重建)、骨架屏/错误占位、保活名称内置
37
+ - **跨应用传值与方法引用**:`@fulgurjs/federation/runtime` 导出 `provideAppContext` / `getAppContext` / `requireAppContext` / `clearAppContext`(缺键 `CC-001` 三段式、独立直开远程页 `CC-002` 显式)。宿主桥写入页面级单例(user/getToken/store/hostApp/locale/sessionKey/events 标准字段 + 项目扩展位),远程 setup/onSession 显式校验消费;方法引用两条通道 = context 携带函数引用(热路径直调)+ exposes 方法模块 `loadRemote('remote/api')`(低频重逻辑)。数据语义 = 传输层快照 + 函数引用,非响应式(与乾坤 props 同语义;"实时"靠函数引用拉取 / 宿主 pinia 共享承担,同页换账号由 onSession 会话同步承担,不依赖页面刷新)
36
38
  - **Vue 直渲染**:`remoteComponent('remote/X')`(`@fulgurjs/federation/runtime` 导出)——`defineAsyncComponent + loadRemote` 的标准封装,加载失败显式错误占位(错误码+根因+修法),runtime.js 零框架依赖零体积增量
37
39
  - **CSP 友好**:原生 ESM 加载路径全程无 `eval` / `new Function`,可在严格 CSP(无 `unsafe-eval`)下运行
38
- - **全链路错误码体系(30 码)**:CFG/DEV/BLD/MFU/CC 五段 + 手册 §8 码表防漂移校验
40
+ - **全链路错误码体系(41 码)**:CFG/DEV/BLD/MFU/CC 五段 + 手册 §6 码表防漂移校验
39
41
 
40
42
  ## 安装
41
43
 
@@ -45,119 +47,257 @@ pnpm add -D @fulgurjs/federation
45
47
 
46
48
  要求:Vite ≥ 5.1(实测至 8.x)、Node ≥ 18、Vue 3、浏览器 Chrome 108+(TLA 原生支持)。
47
49
 
48
- ## 快速开始
50
+ ## 快速开始:三条接入路径
49
51
 
50
- ### Remote(提供方)
52
+ **推荐接入形态(4.2.0 起)——每项目一份 `fulgurjs.config.ts`,Vite 只注册一次插件**:
51
53
 
52
54
  ```ts
53
- // vite.config.ts
54
- import { defineConfig } from 'vite'
55
- import vue from '@vitejs/plugin-vue'
56
- import { federation } from '@fulgurjs/federation'
57
-
58
- export default defineConfig({
59
- plugins: [
60
- vue(),
61
- federation({
62
- name: 'remote-a',
63
- exposes: {
64
- './Button': './src/components/Button.vue',
65
- './utils': './src/utils/index.ts',
66
- },
67
- shared: {
68
- vue: { singleton: true },
69
- },
70
- }),
71
- ],
55
+ // my-app/fulgurjs.config.ts —— 默认导出直接可传给 federation()(satisfies 做编译期形状检查)
56
+ import type { FederationOptions } from '@fulgurjs/federation'
57
+
58
+ export default {
59
+ name: 'my-app',
60
+ exposes: { './pages/home': './src/views/Home.vue' },
61
+ remotes: { 'remote-a': { dev: 'http://localhost:5174/remote-a', prod: '/remote-a' } },
62
+ shared: { vue: { singleton: true } },
63
+ } satisfies FederationOptions
64
+
65
+ // my-app/vite.config.ts —— 联邦相关的全部代码就这两行(其余 Vite 配置原样保留)
66
+ import federation from '@fulgurjs/federation'
67
+ import fulgurjsConfig from './fulgurjs.config'
68
+ // plugins: [ ...原有插件, federation(fulgurjsConfig) ]
69
+ ```
70
+
71
+ `npx fulgurjs init` 生成该模板;`npx fulgurjs explain` / `check-pages` 直接读它(宿主应用另以
72
+ **具名导出 `hostPages`** 提供 CLI 核对用的页面数据,与运行时页面表同一份数据模块)。宿主与远程
73
+ 即使分属互不相邻的仓库也各自独立:只声明对方 URL 与容器名,不依赖共同父目录或对方源码。
74
+ 下面三条路径按需选路,**不必全做**,互相独立、可组合(普通小应用也可以不建配置文件、
75
+ 直接在 vite.config.ts 里写 `federation({ name, ... })`):
76
+
77
+ - **路径 ①:暴露并加载普通模块**——任何 Vue 组件或 TS/JS 函数模块,跨应用共享。不需要桥、不需要页面表、不需要任何初始化协议。
78
+ - **路径 ②:宿主多页面接入**——宿主有一批路由要映射到远程页面。用 `createHostPages` 一份页面表解决 URL 解析/组件缓存/骨架屏/错误占位/保活名称。
79
+ - **路径 ③:远程业务页需要宿主环境**——远程页面依赖全局组件注册、用户/权限/字典等启动期初始化。用 `setup`/`onSession` 声明式初始化 + `AppContext` 传值。
80
+
81
+ ### 路径 ①:暴露并加载普通模块(无 setup、无桥、无页面表)
82
+
83
+ **谁配置**:远程应用 vite.config.ts 写 `exposes`;宿主 vite.config.ts 写 `remotes`。**谁调用**:消费方业务代码。**何时执行**:`loadRemote` 只取得模块导出,**调用导出函数仍由业务代码决定**——expose ≠ 自动执行。
84
+
85
+ ```ts
86
+ // ── remote-a/vite.config.ts ──
87
+ federation({
88
+ name: 'remote-a',
89
+ exposes: {
90
+ './Button': './src/components/Button.vue', // Vue 组件
91
+ './math': './src/math.ts', // 普通 TS 模块
92
+ },
93
+ shared: { vue: { singleton: true } },
72
94
  })
95
+
96
+ // ── remote-a/src/math.ts:普通 TS 文件,无任何联邦 API ──
97
+ export function add(a: number, b: number) { return a + b }
98
+
99
+ // ── host(消费方)代码 ──
100
+ import { loadRemote, remoteComponent } from '@fulgurjs/federation/runtime'
101
+
102
+ // 组件:remoteComponent 直渲染(defineAsyncComponent 标准封装,失败显式错误占位)
103
+ const RemoteButton = remoteComponent('remote-a/Button')
104
+
105
+ // 普通 TS 模块:loadRemote 返回【模块命名空间】——这一步只是加载,add 尚未执行
106
+ const mod = await loadRemote('remote-a/math')
107
+ mod.add(1, 2) // ← 显式调用才执行
108
+ ```
109
+
110
+ 最小文件树(只有路径 ① 时):
111
+
112
+ ```text
113
+ remote-a/
114
+ vite.config.ts # federation({ name, exposes })
115
+ src/components/Button.vue
116
+ src/math.ts
117
+ host/
118
+ vite.config.ts # federation({ name, remotes: { 'remote-a': ... } })
119
+ src/App.vue # remoteComponent(...) / loadRemote(...)
73
120
  ```
74
121
 
75
- ### Host(消费方)
122
+ 验证:启动双方 dev server,宿主渲染出远程组件即通。失败时看错误占位的三段式文案(错误码 + 根因 + 修法),常见为 `MFU-001`(远程未启动/地址错)与 `MFU-006`(exposes 键名不一致)。
123
+
124
+ ### 路径 ②:宿主多页面接入(`createHostPages`)
125
+
126
+ 宿主有一批路由要落到远程页面时,用页面适配器替代手写路由/加载样板:**一份页面表**供宿主路由与布局共用,URL 解析、最长前缀远程归属、异步组件缓存、骨架屏、错误占位、保活名称全部内置。
76
127
 
77
128
  ```ts
78
- export default defineConfig({
79
- plugins: [
80
- vue(),
81
- federation({
82
- name: 'my-host',
83
- remotes: {
84
- // 一个地址,dev/prod 自动切换;也可拆开写 { dev, prod }
85
- 'remote-a': 'http://localhost:5101',
86
- },
87
- shared: {
88
- vue: { singleton: true },
89
- },
90
- }),
129
+ // host/src/fulgurjs/host/pages.ts
130
+ import { createHostPages, definePages, remoteSchema } from '@fulgurjs/federation/runtime'
131
+
132
+ export const hostPages = createHostPages({
133
+ pages: [
134
+ { route: '/remote-a/home', name: 'RemoteAHome', title: '首页' },
135
+ // 带参路由须显式 spec(剥参推导会与列表页 expose 键收敛相同,R1 校验拦截)
136
+ { route: '/remote-a/detail/:id', name: 'RemoteADetail', spec: 'pages/remote-a/detail', title: '详情' },
91
137
  ],
138
+ remotePrefixes: { '/remote-a/': 'remote-a' }, // 最长前缀匹配
139
+ schema: remoteSchema, // dev 探针校验 exposes 存在性
140
+ // base: '/main', // 站点有 base 前缀时声明,resolve 自动剥离
92
141
  })
142
+
143
+ hostPages.pages // 原页面记录(供 vue-router 注册)
144
+ hostPages.resolve(path) // { page, remote, spec, params } | null
145
+ hostPages.component(spec) // 异步页面组件(同 spec 复用)
146
+ hostPages.keepAliveNames // keepAlive 页面的组件 name(KeepAlive include 用)
147
+ ```
148
+
149
+ ```ts
150
+ // host 路由注册(同一份 hostPages 对象)
151
+ children: hostPages.pages.map((p) => ({
152
+ path: p.route, name: p.name,
153
+ props: (r) => ({ ...r.params, ...r.query }),
154
+ component: hostPages.component(hostPages.resolve(p.route)!.spec),
155
+ }))
156
+ // 布局内容区:<component :is="hostPages.component(resolved.spec)" :key="fullPath" v-bind="props" />
93
157
  ```
94
158
 
95
- ### 消费远程模块
159
+ ### 路径 ③:远程业务页需要宿主环境(`setup` + `AppContext` + 会话切换)
160
+
161
+ 远程页面依赖「全局组件注册、用户/权限/字典」等启动期初始化时,在远程的 `federation()` 配置里**显式声明**一个初始化入口文件——插件保证它的执行时序,宿主不再手写「loadRemote 启动器并调用」:
96
162
 
97
163
  ```ts
98
- // 静态导入:直接写 remote 名/expose 名,类型补全直达源码
99
- import Button from 'remote-a/Button'
164
+ // ── remote-a/vite.config.ts:setup 单独声明;普通 exposes 语义不变 ──
165
+ federation({
166
+ name: 'remote-a',
167
+ exposes: { './pages/remote-a/home': './src/views/Home.vue' },
168
+ setup: './src/fulgurjs/setup.ts', // ← 可选项;缺省时无任何初始化行为
169
+ })
100
170
 
101
- // 动态导入 / 运行时 API
102
- import { loadRemote, registerRemote, preloadRemote } from '@fulgurjs/federation/runtime'
171
+ // ── remote-a/src/fulgurjs/setup.ts ──
172
+ import type { RemoteSetupContext } from '@fulgurjs/federation/runtime'
103
173
 
104
- const Chart = defineAsyncComponent(() => loadRemote('remote-a/Chart').then(m => m.default))
174
+ // 默认导出:应用级初始化,同一容器只执行一次(全局组件/样式/locale 注册)
175
+ export default async function setup(context: RemoteSetupContext) {
176
+ const { hostApp } = context.appContext
177
+ // hostApp 全局组件注册、全局样式 import 等
178
+ }
105
179
 
106
- // Vue 组件直渲染:remoteComponent = 上行的标准封装
107
- // 加载失败显式错误占位(错误码+根因+修法),loading/错误组件可自定义
108
- import { remoteComponent } from '@fulgurjs/federation/runtime'
109
- const ChartCard = remoteComponent('remote-a/Chart', { retries: 2 })
180
+ // 可选具名导出:会话级初始化,按宿主 sessionKey 去重(换账号/重登自动重跑)
181
+ export async function onSession(context: RemoteSetupContext) {
182
+ // 同步当前用户、权限、字典;await 之后写状态前检查 context.signal.aborted
183
+ }
184
+ ```
110
185
 
111
- // 构建时地址未知的远程?运行时注册(对齐 webpack promise remote 语义)
112
- registerRemote({ name: 'shop', entry: await (await fetch('/api/remote-url')).text() })
113
- await preloadRemote('shop') // 按 manifest 精确预载 chunk + CSS
186
+ ```ts
187
+ // ── host:先提供 context(含 sessionKey),之后正常 loadRemote —— 初始化自动发生 ──
188
+ import { provideAppContext, loadRemote, clearAppContext } from '@fulgurjs/federation/runtime'
114
189
 
115
- // 远程可能部署不稳定?显式 fallback 模块 + 单次调用重试覆盖
116
- // (失败时错误事件仍会发出,但返回 fallback 组件而非抛错)
117
- const Panel = await loadRemote('shop/Panel', {
118
- retries: 3,
119
- fallbackModule: () => import('./PanelFallback.vue'),
120
- })
190
+ // 登录成功后(每次成功登录/重登生成新的非敏感 sessionKey,不是 token):
191
+ provideAppContext({ user, getToken, store, hostApp, locale, sessionKey: 's-101-1730...', events })
192
+
193
+ const Page = await loadRemote('remote-a/pages/remote-a/home')
194
+ // ↑ 内部时序:取得容器 → init(共享协商)→ 执行 remote-a 的 setup(一次)→
195
+ // 执行 onSession(本 sessionKey 首次)→ 返回页面模块。任一步失败显式报错(MFU-011~014)。
196
+
197
+ // 退出登录时:
198
+ clearAppContext() // 清 context + 作废会话信号/onSession 去重(下次登录必须重跑 onSession)
121
199
  ```
122
200
 
123
- > **普通源码文件都可以直接这样导入**——包括 exposes 目标文件(远程页面)。远程页面里的该导入会被
124
- > 插件自动改写为惰性单例委托:求值期零副作用、调用期转发页面级运行时单例,无需关心宿主/远程的区别。
201
+ **执行时机总表(路径 ③)**:
202
+
203
+ | 动作 | setup(默认导出) | onSession(具名导出) |
204
+ |---|---|---|
205
+ | 谁声明 | 远程 `federation({ setup })` | 同一 setup 文件的具名导出(可选) |
206
+ | 谁触发 | 宿主首次 `loadRemote('remote-a/任何模块')` | 同左,且宿主须已提供 `sessionKey` |
207
+ | 执行次数 | 每容器一次(并发共享同一 Promise) | 每个 sessionKey 一次;换账号/重登重跑;退出清理后重跑 |
208
+ | 失败行为 | 该次 loadRemote 拒绝(MFU-012),仅清失败缓存可重试 | 同左(缺 sessionKey 报 MFU-013) |
209
+ | 预载 | `preloadRemote` 只下载资源,**不执行** | 同左 |
210
+ | `getContainer` / `loadRemote('remote-a')` | 不执行(只取容器) | 不执行 |
125
211
 
126
- > **构建入口文件是边界**:Vite 的 HTML module entry 在 build 时由插件优先内联 init 并直接返回,入口文件自身的 remote import 不会进入改写管线。把 `import('remote-a/Page')` 放在入口导入的普通模块中;不要在 `main.ts` / `main.js` 里直接写 remote import。该边界是有意保留的,不代表入口文件中的语法也会被改写。
212
+ **数据从哪里来**:`context.appContext` 就是宿主 `provideAppContext` 写入的同一页面级对象(同浏览器页面直接共享,无网络传输);`user` 是提供时快照、`getToken()` 每次调用取最新值、`store` 是宿主 pinia 实例引用。dev/prod 行为一致。
127
213
 
128
- `loadRemote('remote-a/Page')` 在 remote 配置提供 manifest 时,会先按 expose 预载对应 CSS,再解析返回模块;CSS 请求失败会报告 `MFU-007`,但不会阻断 JS 模块加载。expose 若依赖全局 CSS,需从该 expose 的依赖图中导入,确保资源进入 manifest。
214
+ > 只有配置在 `setup` 的文件才是生命周期入口。插件**不扫描目录、不按文件名猜测、不执行其他 TS exposes**——普通 TS 模块仍按路径 ① 的语义「加载不等于调用」。旧的「expose 启动器 + 宿主手动 loadRemote 并调用」写法继续可用(便于分批迁移),但不再是推荐接法(见[迁移指南](#文档))。
129
215
 
130
- > 以上只是最小面。**全部选项(remotes 四形态/shared 九个开关/dts/runtimePlugins…)、运行时 API、CLI、错误码见下方 [API 参考](#api-参考)。**
216
+ ### 通用边界(三条路径都适用)
131
217
 
132
- **唯一 API 入口(4.0.0)**:应用代码的一切联邦导入——运行时函数、context 三函数、`definePages`、`remoteSchema`、`remoteComponent`——**只来自物理子路径**:
218
+ **唯一 API 入口**:应用代码的一切联邦导入——运行时函数、context 函数(含 `clearAppContext`)、`definePages`、`remoteSchema`、`remoteComponent`、`createHostPages`——**只来自物理子路径**:
133
219
 
134
220
  ```ts
135
- import { loadRemote, loadShare, preloadRemote, provideAppContext, getAppContext, definePages, remoteSchema, remoteComponent } from '@fulgurjs/federation/runtime'
221
+ import { loadRemote, provideAppContext, getAppContext, requireAppContext, clearAppContext, definePages, createHostPages, remoteSchema, remoteComponent } from '@fulgurjs/federation/runtime'
136
222
  ```
137
223
 
138
- 旧应用入口 `virtual:fulgurjs-api` 已删除;`virtual:fulgurjs-runtime` 仅供插件内部使用。旧 context/pages/vue 子路径也不再公开(迁移映射见 CHANGELOG 4.0.0)。
224
+ `/runtime` 是 ESM 应用入口,导出 `remoteComponent`/`createHostPages`,所以**使用该入口的应用需要安装 Vue**(Vue 为可选 peer:只用包根或 `/config` 时无需安装;`runtime.js` 内核本身零 Vue 依赖零体积增量)。`remoteSchema` 必须以具名静态导入取得 dev 探针结果;命名空间导入、动态导入和 re-export 不触发探针拆写,未经过插件转换时该值是空表。dev 下 remote 跑它自己的 `vite dev`(容器入口 `/@fulgurjs-entry.js` 由插件中间件直出);build 下 expose 自动拆独立 chunk、shared 自动剥离——同一份配置两端通用。
139
225
 
140
- `/runtime` 是 ESM 应用入口,包含 `remoteComponent`,所以使用此入口的应用需安装 Vue。`remoteSchema` 必须以具名静态导入取得 dev 探针结果;命名空间导入、动态导入和 re-export 不触发探针拆写。未经过插件转换时该值是空表。包根与 `/config` 仍可在 Node 配置中使用。
226
+ **导入改写边界**:宿主/远程的任何普通源码文件都可以直接静态导入 `/runtime`——包括 exposes 目标文件(远程页面),其导入会被插件自动改写为惰性单例委托(求值期零副作用)。**构建入口文件是边界**:Vite 的 HTML module entry 在 build 时由插件优先内联 init 并直接返回,入口文件自身的 remote import 不会进入改写管线;把 remote 动态导入放在入口导入的普通模块中,不要写在 `main.ts` / `main.js` 里。
141
227
 
142
- dev 下 remote 跑它自己的 `vite dev`(容器入口 `/@fulgurjs-entry.js` 由插件中间件直出);build 下 expose 自动拆独立 chunk、shared 自动剥离——同一份配置两端通用。
228
+ **CSS 预载**:`loadRemote('remote-a/Page')` 在 remote 提供 manifest 时,先按 expose 预载对应 CSS 再解析返回模块;CSS 请求失败报 `MFU-007` 但不阻断 JS 模块加载。expose 依赖的全局 CSS 需从该 expose 的依赖图中导入(路径 ③ 的 setup 文件导入全局样式是该场景的标准位置),确保资源进入 manifest。
143
229
 
144
- ## CLI:init 起步模板 + doctor 部署体检
230
+ > 以上是最常用面。**全部选项、运行时 API、CLI、错误码见下方 [API 参考](#api-参考)。**
231
+
232
+ ## CLI:init 起步模板 / explain 配置解释 / check-pages 页面契约 / doctor 部署体检
145
233
 
146
234
  ```bash
147
- # 1) 生成带注释的 fulgurjs.config.ts 起步模板(宿主/远程/页面路由表/部署形态,单文件可入库)
148
- npx fulgurjs init # 已存在则拒绝,--force 覆盖
235
+ # 1) 生成单项目 fulgurjs.config.ts 起步模板(默认导出直接是 federation() 选项;已存在则拒绝,--force 覆盖)
236
+ npx fulgurjs init
149
237
  # 样例:examples/fulgurjs.config.example.ts(通用字段示例)
150
238
 
151
- # 2) 校验配置并输出可直接粘贴的样板:各应用 federation() 块、NGINX no-cache 站点模板、接入核对清单
239
+ # 2) 校验配置并输出接入块:federation(fulgurjsConfig) 两行接法 + 通用核对清单(纯打印,不写文件)
152
240
  npx fulgurjs init --config fulgurjs.config.ts
153
241
 
154
- # 3) 部署体检(CI 可嵌):缓存头/资源形态/CORS/chunk 可达/版本 skew
242
+ # 3) 配置解释器(纯本地无网络):角色(按实际选项判定,双向联邦显示「双角色」)/remotes/exposes/
243
+ # setup/shared/页面映射/devSharedSelf 来源/加载链;单项目形态免 --app
244
+ npx fulgurjs explain # --json 供 CI;聚合配置需 --config <聚合文件> --app <应用名>
245
+
246
+ # 4) 页面契约核对:宿主页面表 ↔ 远程 manifest exposes(宿主项目内运行;manifest 来源
247
+ # 优先级 --manifest > --site/prod 推导,输出实际命中来源;确定性错误非零退出,
248
+ # 远程不可达报「无法验证」而非通过,--require-verified 时无法验证也非零)
249
+ npx fulgurjs check-pages --site http://your-site
250
+ npx fulgurjs check-pages --manifest remote-a=/abs/fulgurjs-manifest.json --require-verified
251
+
252
+ # 5) 部署体检(CI 可嵌):缓存头/资源形态/CORS/chunk 可达/版本 skew
155
253
  npx fulgurjs doctor --base http://your-site --apps app-a,app-b
156
254
  npx fulgurjs doctor --base http://localhost:5173 --apps app-a --dev
157
255
  ```
158
256
 
159
- **插件保持项目无关**:init 不改写任何项目文件,不内置任何具体项目的模板或补丁;权限路由、
160
- 远程启动器等项目集成细节由各项目按 init 输出的通用核对清单自行落地。
257
+ **插件保持项目无关**:init 不改写任何项目文件、不生成项目源码(不生成桥/路由/启动器/NGINX 文件——
258
+ NGINX 内容仅作为**打印样板**随旧聚合配置输出);权限路由、项目侧桥与页面表等集成细节由各项目
259
+ 按 init 输出的通用核对清单自行落地。
260
+
261
+ ### 每项目一份配置:`fulgurjs.config.ts` + `federation(fulgurjsConfig)`(默认主路径)
262
+
263
+ `fulgurjs.config.ts` 的默认导出**直接就是 `federation()` 的选项对象**(`satisfies FederationOptions`
264
+ 编译期形状检查,无运行时包装函数),Vite 只导入本项目常量并注册一次插件:
265
+
266
+ ```ts
267
+ // my-app/fulgurjs.config.ts —— 本项目自己的配置;键直接属于 federation 选项
268
+ import type { FederationOptions } from '@fulgurjs/federation'
269
+
270
+ export default {
271
+ name: 'my-app',
272
+ exposes: { './pages/home': './src/views/Home.vue' },
273
+ // 反向消费宿主时才写 remotes;配置的是地址,不依赖对方源码目录
274
+ remotes: { 'remote-a': { dev: 'http://localhost:5174/remote-a', prod: '/remote-a' } },
275
+ setup: './src/fulgurjs/setup.ts', // 可选(§10)
276
+ shared: { vue: { singleton: true, requiredVersion: '^3.4.0' } },
277
+ } satisfies FederationOptions
278
+
279
+ // my-app/vite.config.ts —— 联邦相关行(原有 Vite 配置原样保留)
280
+ import federation from '@fulgurjs/federation'
281
+ import fulgurjsConfig from './fulgurjs.config'
282
+ // plugins: [ ...原有插件, federation(fulgurjsConfig) ]
283
+ ```
284
+
285
+ 规则与边界:
286
+
287
+ - 各项目 `vite.config.ts` 中**不得也不需要**出现 `loadRepoConfig` / `federationOptionsForApp` /
288
+ `fileURLToPath(new URL(...))` / 父目录配置路径 / 按字符串查应用名——CLI 内部有自己的加载器,
289
+ 项目侧永远只见「导入一个常量、调用一次插件」;
290
+ - 宿主应用的页面核对数据以**具名导出 `hostPages`**(`{ pages, remotePrefixes, deriveSpec? }`)提供,
291
+ 与运行时 `createHostPages` 消费同一份数据模块(页面表唯一手工维护位置);Vite 只消费默认导出,
292
+ `pages` 等非插件字段不会误传给 `federation()`;
293
+ - base / dev 端口 / 代理 / 插件顺序等继续归各项目 `vite.config.ts`,不复制进第二套配置;
294
+ - 同一 monorepo 中的应用也各自持有配置;宿主与远程分属不同仓库时各自独立构建、部署、诊断。
295
+
296
+ > **历史兼容(不推荐)**:4.1.0 的聚合配置(`root + apps[]`,`defineRepoConfig` /
297
+ > `loadRepoConfig` / `federationOptionsForApp` 三层转换)保留一段兼容期——CLI 自动识别旧形状
298
+ > 并保持 4.1.0 行为(`explain`/`check-pages` 需 `--app`),已使用聚合配置的项目升级不会立即报错,
299
+ > 但文档主路径、`init` 模板与本节示例一律是单项目形态。迁移 = 拆出各应用的 `name/remotes/
300
+ > exposes/setup/shared` 到各自项目根,删除父目录聚合文件。
161
301
 
162
302
  ## API 参考
163
303
 
@@ -177,21 +317,22 @@ import { federation, type FederationOptions } from '@fulgurjs/federation'
177
317
  |---|---|---|---|
178
318
  | `name` | `string` **必填** | — | 容器名。同页面宿主/远程必须唯一(也是 uniqueName);须匹配 `/^[a-zA-Z][\w.-]*$/` |
179
319
  | `filename` | `string` | `'fulgurjs-remoteEntry.js'` | prod 容器入口文件名(固定文件名,CDN 可长缓存) |
180
- | `exposes` | `Record<string, string \| { import: string; name?: string }>` | — | 对外暴露模块:键 `'./X'`,值源文件路径;`name` 为稳定 chunk 文件名 |
320
+ | `exposes` | `Record<string, string \| { import: string; name?: string }>` | — | 对外暴露模块:键 `'./X'`,值源文件路径;`name` 为稳定 chunk 文件名。键不得占用内部保留键 `./__fulgurjs_setup__`(CFG-012) |
321
+ | `setup` | `string` | —(无初始化行为) | **可选远程初始化入口**:相对应用根的 TS/JS 模块路径。默认导出 `setup(context)` 应用级执行一次(容器首次被加载业务模块前);可选具名导出 `onSession(context)` 按宿主 `sessionKey` 去重执行。其余导出不作为生命周期入口。执行时序/去重/失败重试/错误码见 §10 |
181
322
  | `remotes` | `Record<string, string \| RemoteEntryConfig \| (() => Promise<any>)>` | — | 消费的远程,三种形态见下表 |
182
323
  | `shared` | `string[] \| Record<string, string \| SharedHint>` | — | 共享依赖;字符串简写 = requiredVersion(缺省从本应用 package.json 推断) |
183
324
  | `shareScope` | `string` | `'default'` | 默认共享作用域名 |
184
- | `remoteType` | `string` | `'module'` | 仅支持 `'module'`(传其它值告警并回落) |
185
- | `library` | `{ type?: string }` | — | 仅接受 esm/module(webpack 宿主 interop 尚未支持,告警) |
325
+ | `remoteType` | `'module'` | `'module'` | 仅支持 `'module'`(类型即字面量);传其它值配置期报 CFG-011(webpack script/var 互操作未实现,不产出看似成功实为 module 的构建) |
326
+ | `library` | `{ type?: 'module' \| 'esm' }` | — | 仅接受 esm/module;其它值配置期报 CFG-011(webpack UMD/var 输出互操作未实现) |
186
327
  | `runtime` | `string \| false` | 内置运行时 | 自定义运行时模块路径;`false` 禁用内置运行时 |
187
328
  | `runtimeChunk` | `boolean \| 'single'` | — | 运行时是否拆独立 chunk |
188
329
  | `manifest` | `boolean` | `true` | prod 构建生成 `fulgurjs-manifest.json`(preloadRemote 依赖它) |
189
330
  | `runtimePlugins` | `string[]` | `[]` | 运行时插件模块路径列表(写法见「运行时插件」) |
190
331
  | `dts` | `boolean \| { dir?: string; mode?: 'source' \| 'shim' }` | `true` | dev 下拉取远程 manifest 生成类型声明——宿主写 `import X from 'remote-a/X'` 获得类型。**产物写入 `src/fulgurjs/types/`(联邦产物集中一个文件夹;无 src 布局回退 `.fulgurjs/types`)**,src 布局项目 tsconfig 零配置即生效;`{ dir }` 自定义位置;`mode: 'source'`(默认)跨工程源码直连(补全/跳转直达远程源码,VSCode 打开生成物可能显示工程外文件诊断);`mode: 'shim'` 宽松占位(不引用源文件,IDE 全程干净,无源码级补全——见 §9.1.5)。**注意**:两种 mode 都要读取 remote 本机源码来枚举导出名(shim 亦然),manifest 的 `fsRoot`/`src` 经过路径边界校验(相对路径、无 `..`、realpath 不得越出 fsRoot),但 `dts` 不是不可信 manifest 的安全边界——只对可信来源开启 |
191
- | `devSharedSelf` | `boolean` | 纯远程 `true`;有 `remotes` 的宿主 `false` | dev 下自身源码(含依赖)是否参与 shared 协商改写。**双向联邦**(既 expose 又消费 remote)的宿主/远程**必设 `true`**,否则 prod 双 vue 实例(症状:被消费页面渲染上下文错乱 / `'ce'` / renderSlot null)。2.0.1 起 build 下该路径的协商门面自动隔离进插件专属 chunk(`fulgurjs-runtime` + `fulgurjs-shared-<key>`),与用户 `manualChunks` 强制分组正交、不再产生 chunk 循环依赖(D6 修复,症状曾是 `SyntaxError: Unexpected token '<'` + `TypeError: _e is not a function`) |
332
+ | `devSharedSelf` | `boolean` | 提供 `exposes`(或 `setup`)的应用 `true`;纯宿主(只消费)`false`;显式配置永远优先 | dev 下自身源码(含依赖)是否参与 shared 协商改写。双向联邦(既 expose 又消费 remote)默认即 `true`——无需再背诵显式配置(4.1.0 起按角色推断,§12.4;此前默认 false 曾是已知错误配置的来源)。build 下该路径的协商门面自动隔离进插件专属 chunk(`fulgurjs-runtime` + `fulgurjs-shared-<key>`),与用户 `manualChunks` 强制分组正交、不产生 chunk 循环依赖(D6 修复) |
192
333
  | `devCorsOrigins` | `string[] \| '*'` | `'*'`(现状兼容) | dev 跨源访问策略:插件端点(`/@fulgurjs-entry.js`、`/@fulgurjs-manifest.json`)与 `server.cors` 使用同一来源。缺省或 `'*'` 全放开(非 loopback host 时提醒 DEV-011);数组按 Origin 反射 allowlist(未命中省略头)。用户显式配置的 `server.cors` 永远优先。开/关/自定义三态示例见下方 |
193
334
  | `devFsRoot` | `boolean` | `true`(现状兼容) | dev manifest 是否携带 `fsRoot`(remote 根目录本机绝对路径,宿主 dts 类型直连用)。`false` 不写入(本机路径不外发),宿主 dts 降级 any 桩并提示;该字段永不进入 prod manifest。非 loopback host 下默认值会提醒 DEV-012 |
194
- | `automaticAsyncBoundary` | — | 恒为 `true` | 接受任意值:TLA 自动异步边界,无需手工 bootstrap |
335
+ | `automaticAsyncBoundary` | — | 恒为 `true` | 传 `false` 配置期报 CFG-011(TLA 自动异步边界无手工 bootstrap 模式可关闭);缺省或 `true` 接受 |
195
336
  | `dataPrefetch` | — | 恒为 `true` | 接受任意值:`preloadRemote` 始终可用 |
196
337
  | `usedExports` / `ignoreUnusedSharedExports` | — | no-op | 接受并忽略(Rollup/Rolldown 原生 tree-shaking 已覆盖) |
197
338
 
@@ -274,16 +415,16 @@ import { loadRemote } from '@fulgurjs/federation/runtime'
274
415
 
275
416
  #### 函数总表
276
417
 
277
- > 下表全部函数与 `definePages` / `remoteSchema` / `provideAppContext` 等 context 函数 / `remoteComponent` 都从**唯一入口** `@fulgurjs/federation/runtime` 导入(见 §2);旧入口已删除。
418
+ > 下表全部函数与 `definePages` / `remoteSchema` / `provideAppContext` 等 context 函数 / `remoteComponent` / `createHostPages` 都从**唯一入口** `@fulgurjs/federation/runtime` 导入(见 §2);旧入口已删除。
278
419
 
279
420
  > **TS 提示**:`@fulgurjs/federation/runtime` 的类型随包发布,由包的 `exports` 和 `typesVersions` 直接解析;不需要 `client` 类型垫片。dev 启动时插件仅在类型目录(默认 `src/fulgurjs/types/`)生成远程 exposes 的类型声明。
280
421
 
281
422
  | 函数 | 签名 | 说明 |
282
423
  |---|---|---|
283
- | `loadRemote` | `(spec: string, opts?) => Promise<模块命名空间>` | 加载远程模块。`spec = '远程名/./Expose键'`(`./` 可省);opts 见下 |
424
+ | `loadRemote` | `(spec: string, opts?) => Promise<模块命名空间>` | 加载远程模块。`spec = '远程名/./Expose键'`(`./` 可省)。远程配置了 `setup` 时,该函数是初始化生命周期的**统一触发入口**(容器 init 后、返回模块前执行 setup/onSession,见 §10);`loadRemote('remote')` 只取容器不执行初始化。opts 见下 |
284
425
  | `loadShare` | `(name: string, opts?) => Promise<命名空间>` | 共享模块协商(最高版本胜出/已加载优先/singleton 收敛)。opts:`{ requiredVersion?, singleton?, strictVersion?, shareKey?, shareScope?, fallback? }` |
285
426
  | `preloadRemote` | `(spec: string, opts?: { mode?: 'preload' \| 'prefetch' }) => Promise<void>` | `remote/Expose` 只预载该 expose 的 chunk + CSS;仅传 remote 名则预载全部 exposes。`preload` 等待 CSS load/error,`prefetch` 低优先级并立即返回 |
286
- | `getContainer` | `(name: string) => Promise<容器>` | 取远程容器(触发加载 + init),容器协议 `{ name, init, get }` |
427
+ | `getContainer` | `(name: string) => Promise<容器>` | 取远程容器(触发加载 + init),容器协议 `{ name, init, get }`;**不执行 setup/onSession**。直接访问 `container.get()` 同样不保证执行初始化——需要生命周期的加载一律走 `loadRemote` |
287
428
  | `registerRemote` / `registerRemotes` | `(config \| list) => void` | 运行时注册远程(promise remote / 动态地址)。`RemoteInput`:`{ name, entry, shareScope?, timeout?, retries?, fallback?, breaker? }`。参数校验:`timeout` 有限正数、`retries` 0..10 整数、`breaker.threshold/resetMs` 有限正数——非法值**注册当场抛错**(配置文件路径在配置期即报 CFG-009);重复注册时 entry/timeout/retries/breaker 参数按最新配置刷新,熔断计数状态保留。`timeout` 语义:超时只代表"调用方不再等待",浏览器不会取消已发出的动态 import——后续调用复用同一 in-flight 记录,不会重复初始化同一容器 |
288
429
  | `registerShare` | `(scope, name, version, get, opts?) => void` | 手工注册共享模块(一般由 init 模块自动完成) |
289
430
  | `initSharing` | `(scopeName?) => ShareScopeMap` | 初始化共享作用域(一般由 init 模块自动完成) |
@@ -330,7 +471,7 @@ export default {
330
471
  | 出口 | 内容 |
331
472
  |---|---|
332
473
  | `window.__FULGURJS_SCOPE__` | share scope 实时协商结果(键 → 版本 → `{ get, from, loaded }`) |
333
- | `window.__FULGURJS_INFO__` | `{ remotes: { [名]: { entry, status, lastLoadMs, error } }, errors: [] }` |
474
+ | `window.__FULGURJS_INFO__` | `{ remotes: { [名]: { entry, status, lastLoadMs, error, setup } }, errors: [] }`——`setup` ∈ none/pending/ready/failed |
334
475
  | `window.__FULGURJS_APP_CONFIG__` | W4 全局配置镜像 |
335
476
  | `window` 事件 `fulgurjs:error` | `CustomEvent<{ remote, error }>`,所有远程加载/共享错误都会发出 |
336
477
 
@@ -372,50 +513,51 @@ export const PAGES = definePages(
372
513
 
373
514
  同子路径的类型:`PageRouteLike`(路由条目形状)、`PagesOptions`(校验选项,含 `deriveSpec` / `remotes` / `schema` / `strict`)、`PageViolation`(`validatePages` 的返回条目,含 `level` 与说明)、`RemoteSchemaEntry`(`schema` 里每个远程的条目形状)。
374
515
 
375
- ### 4. `fulgurjs.config.ts` — CLI 单配置文件(`@fulgurjs/federation/config`)
516
+ ### 4. `fulgurjs.config.ts` — 每项目一份的联邦配置(默认形态)
376
517
 
377
518
  ```ts
378
- import { defineRepoConfig } from '@fulgurjs/federation/config'
379
-
380
- export default defineRepoConfig({
381
- root: process.cwd(), // 工程根(monorepo 根或单应用仓库根)
382
- apps: [
383
- {
384
- path: 'apps/host', // 相对 root 的应用目录
385
- name: 'host-app', // 联邦容器名
386
- port: 5173, // dev 端口
387
- base: '/', // 部署/dev 的 URL 前缀
388
- deployDir: 'main', // 部署目录名(缺省取 base 去斜杠)
389
- host: {
390
- remotePrefixes: { '/remote-a/': 'remote-a' },
391
- remotes: { 'remote-a': { dev: 'http://localhost:5174/remote-a', prod: '/remote-a' } },
392
- pages: [{ route: '/remote-a/home', name: 'RemoteAHome', spec?: 'pages/remote-a/home', title: '首页' }],
393
- },
394
- remote: { // 该应用同时是远程时(双向联邦)
395
- exposes: { './pages/remote-a/home': './src/views/Home.vue' },
396
- remotes: { /* 反向消费 */ },
397
- },
398
- shared: { vue: { singleton: true } },
399
- },
400
- ],
401
- deploy: { webRoot: '/var/www/your-site', listen: 8080 }, // 仅供 init 输出 NGINX 样板
402
- })
519
+ // my-app/fulgurjs.config.ts —— 默认导出直接可传给 federation();无 root/apps[]/角色壳
520
+ import type { FederationOptions } from '@fulgurjs/federation'
521
+
522
+ export default {
523
+ name: 'my-app', // 联邦容器名(必填)
524
+ exposes: { './pages/home': './src/views/Home.vue' },
525
+ remotes: { 'remote-a': { dev: 'http://localhost:5174/remote-a', prod: '/remote-a' } },
526
+ setup: './src/fulgurjs/setup.ts', // 可选:远程初始化入口(§10)
527
+ shared: { vue: { singleton: true } },
528
+ devSharedSelf: true, // 可选:显式覆盖;缺省按角色推断(提供 exposes/setup → true)
529
+ } satisfies FederationOptions
530
+
531
+ // ── 以下具名导出仅供 CLI explain/check-pages 读取,不是 federation() 的参数 ──
532
+ // 宿主应用:页面表与运行时 createHostPages 消费同一份数据模块(唯一手工维护位置)
533
+ // import { pages, remotePrefixes, deriveSpec } from './src/fulgurjs/host/pages.data'
534
+ // export const hostPages = { pages, remotePrefixes, deriveSpec }
403
535
  ```
404
536
 
405
- 程序化加载:`loadRepoConfig(configPath): Promise<RepoConfig>` —— 读 `fulgurjs.config.ts` / `.js` / `.json` 并做 CFG 段校验(CLI 内部同款;配置文件里 `@fulgurjs/federation/config` 的导入会被重写为包内绝对路径,故在工程依赖装好之前也能加载)。
537
+ CLI 内部加载器(`loadAppConfig`)以**原配置文件为解析基准** esbuild-bundle 读取:支持项目内
538
+ 相对导入的纯 TS/JS 数据模块(extensionless 可)、Node ≥ 18、CJS/ESM 双形态;缺失文件、无
539
+ `name`、字段形状不对、expose/setup 指向项目外或不存在文件等均三段式报错。运行时(Vite)与
540
+ CLI 解析同一份配置值;dev/prod 的 URL 选择规则与 `federation({ remotes })` 一致(§1)。
541
+
542
+ **历史兼容(4.1.0 聚合配置,`@fulgurjs/federation/config` 子路径)**:`defineRepoConfig({ root,
543
+ apps[] })` 形态自动识别并保持 4.1.0 行为;程序化加载 `loadRepoConfig(configPath): Promise<RepoConfig>`
544
+ 与转换 `federationOptionsForApp(config, appPathOrName): FederationOptions` 继续可用(应用不存在时
545
+ 报出可用清单;同键不同地址冲突报错)。同子路径类型:`RepoConfig`/`UserConfig`/`AppConfig`/
546
+ `HostConfig`/`RemoteConfig`/`DeployConfig`/`PageEntry`/`RemoteAddress`。兼容不等于推荐——
547
+ 新项目一律用上方单项目形态。
406
548
 
407
- 同子路径的类型:`RepoConfig`(整个配置文件)/`UserConfig`(`defineRepoConfig` 的入参形状,字段全可选)/`AppConfig`(`apps[]` 的一个应用)/`HostConfig`(应用的 `host` 段,宿主角色)/`RemoteConfig`(应用的 `remote` 段,远程角色)/`DeployConfig`(`deploy` 段)/`PageEntry`(`host.pages[]` 的一条页面)/`RemoteAddress`(`remotes` 值的 `{ dev, prod, external }` 形态)。
408
- ```
409
549
 
410
550
  ### 5. CLI 命令参考
411
551
 
412
552
  | 命令 | 说明 |
413
553
  |---|---|
414
- | `fulgurjs init` | 在当前目录生成带注释的 `fulgurjs.config.ts` 起步模板;`--template <path>` 指定输出路径;已存在拒绝覆盖,`--force` 强制 |
415
- | `fulgurjs init --config <path>` | 校验配置(CFG 三段式报错)+ 输出各应用 `federation()` 粘贴块、NGINX no-cache 站点模板、8 条通用核对清单 |
554
+ | `fulgurjs init` | 在当前目录生成**单项目** `fulgurjs.config.ts` 起步模板(默认导出 = `federation()` 选项 + 可选 `hostPages` 具名导出示例);`--template <path>` 指定输出路径;已存在拒绝覆盖,`--force` 强制。init **只生成配置起步模板**,不生成桥/路由/启动器/NGINX 文件 |
555
+ | `fulgurjs init --config <path>` | 校验配置(CFG 三段式报错)+ 输出 `federation(fulgurjsConfig)` 接入块与通用核对清单(纯打印)。旧聚合配置自动识别,保持 4.1.0 输出(各应用粘贴块 + NGINX no-cache **打印样板**) |
556
+ | `fulgurjs explain [--config <path>] [--app <名>]` | 配置解释器(纯本地、无网络、不读 token/环境秘密):应用角色(**按实际 federation 选项判定**——配 `remotes` 即消费、配 `exposes`/`setup` 即提供,两者均有=双角色,如双向联邦的 BPM)、有效 remotes、公开 exposes、内部 setup、shared、页面 spec 映射与数据来源、`devSharedSelf` 最终值及来源、加载链。单项目形态免 `--app`;聚合配置需 `--app <应用目录名或容器名>`。`--json` 供 CI |
557
+ | `fulgurjs check-pages [--config <path>] [--app <宿主名>] [--site <URL>] [--manifest <r>=<路径\|URL>]... [--require-verified]` | 页面契约核对:宿主页面表(单项目 = `hostPages` 具名导出;聚合 = `host.pages`)↔ 远程 manifest exposes。manifest 来源优先级 **`--manifest`(可多次、文件路径或 URL) > `--site`/消费方 prod 地址推导 > 本地 dist(仅聚合形态回退)**,输出每个 remote 的实际命中来源(防止旧本地 dist 冒充线上核对)。报告未知 remote、映射到未消费远程、缺失 expose、路由冲突(R1–R5);**确定性错误退出码 1**,远程不可达报「无法验证」,`--require-verified` 时无法验证也非零(CI 严格模式,避免 0 条核对显示通过)。`--json` 供 CI |
416
558
  | `fulgurjs doctor --base <URL> --apps <a,b,c>` | 部署体检:remoteEntry/manifest/index.html 的 200/no-cache/JS 形态、CORS、chunk 抽样可达、版本 skew 预演。`--dev` 检查 dev 容器入口;`--json` 输出 JSON(CI 断言);`--chunk-sample N` 控制抽样数(默认 16)。**退出码:有 FAIL 即 1**,可直接做 CI 门禁 |
417
559
 
418
- ### 6. 错误码总表(35 个)
560
+ ### 6. 错误码总表(41 个)
419
561
 
420
562
  | 段 | 码 | 含义 |
421
563
  |---|---|---|
@@ -429,6 +571,8 @@ export default defineRepoConfig({
429
571
  | | `CFG-008` | shared 非法组合(eager+import:false / shareKey 重复声明) |
430
572
  | | `CFG-009` | remotes 运行参数非法(timeout/retries/breaker 非有限正数/超上限) |
431
573
  | | `CFG-010` | devCorsOrigins 形态非法(须为 "*" 或 http(s) 来源数组) |
574
+ | | `CFG-011` | 不支持的互操作选项硬报错(remoteType/library.type 非 module 系、automaticAsyncBoundary=false——不再静默回落) |
575
+ | | `CFG-012` | setup 配置非法(路径为空/非字符串,或 exposes 占用内部保留键 `./__fulgurjs_setup__`) |
432
576
  | DEV 开发期 | `DEV-001` | remote dev server 不可达(manifest 拉取失败) |
433
577
  | | `DEV-002` | remote dev manifest 为空或格式不识别 |
434
578
  | | `DEV-004` | 已知 UMD-only 依赖不在 optimizeDeps.include(预构建内联本地 vue 风险) |
@@ -452,8 +596,12 @@ export default defineRepoConfig({
452
596
  | | `MFU-008` | 未知远程 |
453
597
  | | `MFU-009` | 加载到的模块没有任何导出 |
454
598
  | | `MFU-010` | singleton 共享版本漂移(使用作用域版本,告警) |
599
+ | | `MFU-011` | setup 生命周期入口导出形态非法(默认导出/具名 onSession 不是函数;报实际类型/预期签名/修法) |
600
+ | | `MFU-012` | setup/onSession 执行抛错(该次 loadRemote 拒绝;仅清失败阶段缓存,可直接重试,已成功的阶段不重复) |
601
+ | | `MFU-013` | 远程声明 onSession 但宿主 AppContext 缺 sessionKey(登录代次;禁止用 token 充当) |
602
+ | | `MFU-014` | setup/onSession 同步段内递归 loadRemote 同一远程(自等待死锁防线) |
455
603
  | CC 跨应用上下文 | `CC-001` | AppContext 必需字段缺失(三段式:got/expected/example,修法指向宿主桥 `provideAppContext`) |
456
- | | `CC-002` | 运行时单例不可用(独立直开远程页;修法 = 经宿主联邦加载,时序契约 bridge → federatedBoot → loadRemote) |
604
+ | | `CC-002` | 运行时单例不可用(独立直开远程页;修法 = 经宿主联邦加载,时序契约 bridge → 远程 setup → 页面模块) |
457
605
 
458
606
  错误排查三段式文案见「配置出错?报错看得懂」一节(下文);`fulgurjs doctor` 可提前把部署面的 MFU-001 类问题拦在上线前。
459
607
 
@@ -466,7 +614,7 @@ export default defineRepoConfig({
466
614
  | prod | `/<base>/fulgurjs-remoteEntry.js` | 固定文件名容器入口(内容每次构建变——**必须 no-cache**) |
467
615
  | prod | `/<base>/fulgurjs-manifest.json` | expose chunk/CSS 清单(preloadRemote 消费,**no-cache**) |
468
616
 
469
- NGINX 部署模板(no-cache 规则 + 深链回退)用 `fulgurjs init --config` 自动生成。
617
+ NGINX no-cache 规则(remoteEntry/manifest/index.html)与深链回退是联邦部署通用知识:`fulgurjs init --config` 在**旧聚合配置形态**下把它作为打印样板输出(不写文件),单项目形态按下方规则自行落位。
470
618
 
471
619
  ### 8. `remoteComponent` — Vue 远程组件直渲染(`@fulgurjs/federation/runtime`)
472
620
 
@@ -502,20 +650,27 @@ const FederatedAmisForm = remoteComponent('demo-host/AmisFormRouterPage', {
502
650
  宿主向子应用传值、子应用向宿主反向注册方法,一律走这条一等公民通道(对标乾坤 `props`,但带类型与错误契约)——不再各自挂 `window.*` 裸口子。
503
651
 
504
652
  ```ts
505
- // —— 宿主桥(host/src/fulgurjs/host/bridge.ts):登录完成后一次性提供 ——
506
- import { provideAppContext } from '@fulgurjs/federation/runtime'
653
+ // —— 宿主桥(host/src/fulgurjs/host/bridge.ts):登录态同步(可多次调用,幂等 merge) ——
654
+ import { provideAppContext, clearAppContext } from '@fulgurjs/federation/runtime'
507
655
 
508
656
  provideAppContext({
509
- user, // 宿主登录用户原始形态
657
+ user, // 宿主登录用户原始形态(当时快照)
510
658
  getToken, // 取最新 token(拉取式防过期)
511
659
  store: piniaInstance, // 宿主 pinia:子应用 useUserStore(ctx.store) 共享响应式状态
512
660
  hostApp: app, // 宿主 Vue App 实例:全局组件/指令注册目标
513
661
  locale, // EP locale 等 UI 配置
662
+ sessionKey: 's-101-1730…', // 非敏感登录代次 ID:每次成功登录/重登生成新值;
663
+ // token 刷新但会话未变时沿用。远程 onSession 按它去重,
664
+ // 缺失时声明了 onSession 的远程报 MFU-013。不是 token、不做授权凭证
514
665
  events: { main: mainEvents }, // 事件/方法池:宿主提供 main;子应用反向注册 bpm.* / lowcode.*
515
666
  // 只传有真实消费的键。项目自定义键经扩展位按需自行提供(如 baseUrl: '/demo')
516
667
  })
517
668
 
518
- // —— 远程 boot(exposes/federatedBoot.ts):显式校验消费 ——
669
+ // 退出登录时清理:删 context + 作废远程会话信号/onSession 去重状态
670
+ // (不重置远程模块缓存、共享模块图与应用级 setup 注册)
671
+ clearAppContext()
672
+
673
+ // —— 远程 setup/onSession:显式校验消费 ——
519
674
  import { requireAppContext } from '@fulgurjs/federation/runtime'
520
675
 
521
676
  const { store, user, hostApp } = requireAppContext('store', 'user', 'hostApp')
@@ -534,19 +689,20 @@ getAppContext().events!.bpm = { formEvent, formSubmitEvent }
534
689
 
535
690
  | 字段 | 类型 | 语义 | 写方 |
536
691
  |---|---|---|---|
537
- | `user` | `Record<string, any>` | 宿主登录用户原始形态 | 宿主桥(只读约定) |
692
+ | `user` | `Record<string, any>` | 宿主登录用户原始形态(提供时快照) | 宿主桥(只读约定;登录态变化时重新 provide 覆盖) |
538
693
  | `getToken` | `() => string \| undefined` | **取最新 token**(拉取式调用,永不过期;context 不提供一次性 token 快照字段) | 宿主桥(只读约定) |
539
694
  | `store` | `unknown`(运行时为宿主 pinia) | 子应用挂载/读取宿主共享响应式状态 | 宿主桥(只读约定) |
540
695
  | `hostApp` | Vue App 实例(同 realm 直引用) | 全局组件/指令注册目标 | 宿主桥(只读约定) |
541
696
  | `locale` | `unknown` | EP locale 等 UI 配置 | 宿主桥(只读约定) |
697
+ | `sessionKey` | `string` | 非敏感登录代次 ID:每次成功登录/重登生成新值;token 刷新但会话未变时沿用。远程 onSession 按它去重(同一代次只执行一次,换代自动重跑);退出 `clearAppContext` 后必须重跑。生成责任在宿主登录流程;**不得用真实 token 充当**,也不作为授权凭证 | 宿主桥(登录后) |
542
698
  | `events` | `Record<string, any>` | 事件/方法池:`events.main.*` 宿主提供、`events.bpm.*` / `events.lowcode.*` 子应用反向注册 | 宿主桥建池,子应用挂载 |
543
699
  | (扩展位) | `[key: string]: unknown` | 项目自定义键(`formUrl` / `baseUrl` 等按需自行提供,模板默认不传) | 宿主桥;远程 boot 只增不改宿主键 |
544
700
 
545
701
  变更语义与时序契约:
546
702
 
547
703
  - `provide` = 顶层 merge(后写覆盖,幂等可多次);约定「宿主先写标准字段,远程只增不改宿主键」;嵌套对象(如 `events`)是**引用共享**,子应用挂属性即时可见(同 realm 直引用);
548
- - 时序契约:**bridge(provide)→ federatedBoot(require)→ 页面 loadRemote**——违反即在 boot 处显式失败(CC-001),不静默;
549
- - 数据语义 = **传输层快照 + 函数引用,非响应式**(与乾坤 props 同语义)。"实时"由三条正规通道承担:① `getToken()` / `events.main.*` 函数引用每次调用执行宿主最新闭包;② `context.store` 把宿主 pinia 递给子应用(共享响应式实例);③ 登录态变更 = 重新登录 = 页面刷新 = bridge 重跑全新 context。context 本体不做 Vue reactive(runtime 框架无关 + gzip 红线 + 跨包 proxy 双份陷阱);"中途变更需通知"的场景:等真实需求出现再设计(当前无此场景,不预留空 API)。
704
+ - 时序契约:**bridge(provide)→ 远程 setup/onSession(require)→ 页面模块返回**——违反即在初始化处显式失败(CC-001 / MFU-013),不静默;
705
+ - 数据语义 = **传输层快照 + 函数引用,非响应式**(与乾坤 props 同语义)。"实时"由两条正规通道承担:① `getToken()` / `events.main.*` 函数引用每次调用执行宿主最新闭包;② `context.store` 把宿主 pinia 递给子应用(共享响应式实例)。**同页换账号(退出→B 登录→再打开远程页)不依赖页面刷新**:宿主重新 provide 最新 context + `sessionKey`,远程 `onSession` 检测到新代次自动重跑,同步用户/权限/字典等会话状态(见 §10)。context 本体不做 Vue reactive(runtime 框架无关 + gzip 红线 + 跨包 proxy 双份陷阱);"中途变更需通知"的场景:等真实需求出现再设计(当前无此场景,不预留空 API)。
550
706
 
551
707
  方法引用两条通道:
552
708
 
@@ -578,7 +734,7 @@ const res = await getDictItems('sex')
578
734
 
579
735
  ### 9.1 乾坤功能融合:保活 / 骨架屏 / 空闲预载 / 诊断面板(宿主与模板侧能力)
580
736
 
581
- 这些能力随 `fulgurjs init` 生成的模板直接带出(手工集成的项目按下述接入点自行落位),插件 runtime.js 零参与。配置面总览:
737
+ 这些能力全部是**项目侧**能力(手工集成的项目按下述接入点自行落位;`fulgurjs init` 只生成配置起步模板,不生成这些项目文件),插件 runtime.js 零参与。配置面总览:
582
738
 
583
739
  | 能力 | 配置项 | 类型 | 默认值 | 配置位置 |
584
740
  |---|---|---|---|---|
@@ -630,17 +786,13 @@ const res = await getDictItems('sex')
630
786
 
631
787
  | 属性 | 类型 | 默认值 | 说明 |
632
788
  |---|---|---|---|
633
- | `host.prefetch`(fulgurjs.config.ts,集成器项目) | `'all' \| string[] \| false` | `'all'` | `'all'` = 预载全部 remotes 键;`string[]` = 指定名单;`false` = 关闭。init 生成 bridge.ts 时注入 |
634
- | `PREFETCH_REMOTES`(手工项目,bridge.ts 顶部常量) | `string[]` | 全部 remotes 键 | 与 vite.config `remotes` 键一致 |
789
+ | `PREFETCH_REMOTES`(宿主项目,`src/fulgurjs/host/bridge.ts` 顶部常量) | `string[]` | 全部 remotes 键 | 与 `federation({ remotes })` 键一致;置空数组即关闭 |
635
790
 
636
- ```ts
637
- // 集成器项目:fulgurjs.config.ts → apps[].host.prefetch(改后删已生成 bridge.ts 重跑 fulgurjs init)
638
- prefetch: 'all' // 默认:全部 remote
639
- prefetch: ['mes-bpm'] // 部分 remote
640
- prefetch: false // 关闭
791
+ > 插件配置面(`FederationOptions`)**没有** `host.prefetch` 字段——4.1.0 前文档曾声称该配置存在,属错误描述,已订正。预载名单就是宿主桥里的常量,改名单只改这一个地方。
641
792
 
642
- // 手工项目:src/fulgurjs/host/bridge.ts 顶部常量
643
- const PREFETCH_REMOTES: string[] = ['mes-bpm', 'mes-lowcode'] // 默认(init 生成形态)
793
+ ```ts
794
+ // src/fulgurjs/host/bridge.ts 顶部常量
795
+ const PREFETCH_REMOTES: string[] = ['mes-bpm', 'mes-lowcode'] // 默认:全部 remote
644
796
 
645
797
  // 只预载部分 remote
646
798
  const PREFETCH_REMOTES: string[] = ['mes-bpm']
@@ -674,6 +826,85 @@ const PREFETCH_REMOTES: string[] = []
674
826
  - 升级插件版本后若 `import ... from '@fulgurjs/federation/runtime'` 报 ts(2307):是 IDE 的 TS 服务缓存了旧包——`Restart TS Server`(⌘⇧P)或重开窗口即可。3.0.0 起旧子路径(`@fulgurjs/federation/{context,pages,vue}`)已从包 exports 删除,按迁移映射改为 `@fulgurjs/federation/runtime`。
675
827
  - **根治红波浪线**:`federation({ dts: { mode: 'shim' } })` —— 生成物不再引用跨工程源文件(宽松占位形态),IDE 全程干净;取舍是失去"跳转直达远程源码"的补全能力(默认 `source` 不变,按项目偏好选择)。
676
828
 
829
+ ### 10. `createHostPages` 与 `setup`/`onSession` — 宿主页面适配器与远程初始化(`@fulgurjs/federation/runtime`)
830
+
831
+ #### 10.1 `createHostPages(options)` — 宿主页面适配器
832
+
833
+ ```ts
834
+ import { createHostPages } from '@fulgurjs/federation/runtime'
835
+
836
+ export const hostPages = createHostPages({
837
+ pages: [/* PageRouteLike[]:宿主路由与布局共用的唯一页面来源 */],
838
+ remotePrefixes: { '/remote-a/': 'remote-a' }, // 必填;最长前缀匹配
839
+ deriveSpec: (route) => `pages/${...}`, // 可选;缺省 = 去首段 + 剥 :参数 段
840
+ schema: remoteSchema, // 可选;dev 探针结果(R3 校验),build 为空表诚实降级
841
+ strict: true, // 可选;ERROR 级校验失败默认 throw
842
+ base: '/main', // 可选;resolve 时剥离的站点 base 前缀
843
+ beforeLoad: () => { /* 每次页面模块实际加载前执行(同步/异步);宿主在此提供最新 context */ },
844
+ loadingComponent: MySkeleton, // 可选;缺省无骨架
845
+ errorComponent: MyError, // 可选;缺省 = 内置三段式错误占位
846
+ delay: 200, // 可选;骨架屏延迟 ms
847
+ })
848
+ ```
849
+
850
+ | 返回成员 | 说明 |
851
+ |---|---|
852
+ | `pages` | 原页面记录(不经改写,宿主路由注册直接用) |
853
+ | `resolve(path)` | `{ page, remote, spec, params } \| null`——兼容 base 前缀与深链;参数解码失败只让该次匹配失败(console 提示),不抛错 |
854
+ | `component(spec)` | 异步页面组件(`<remote>/<exposes键>` 完整 spec);同 spec 复用。**会话感知**:AppContext.sessionKey 变化(换账号/重登/退出)后自动重建组件——下一次渲染重新走 `beforeLoad → loadRemote`,触发新代次的 `onSession`(模块本体经 loadRemote 缓存复用,不重复下载)。无 sessionKey 的用法缓存永不失效 |
855
+ | `keepAliveNames` | `keepAlive: true` 页面的组件 name(与实际被 KeepAlive 缓存的包装组件一致,直接绑 `<keep-alive :include>`) |
856
+
857
+ 行为契约:
858
+
859
+ - 页面表经 `definePages` 校验(R1 剥参冲突 / R2 重复 spec / R3 spec 存在性 / R4 遮蔽与重复 / R5 重名),行为与独立使用完全一致;
860
+ - `remotePrefixes` **最长前缀优先**(不再是"先到先得");页面路由无匹配前缀在 `createHostPages` 创建期即报错(fail fast);
861
+ - 组件是**本地包装组件**(稳定 name 供 KeepAlive include 匹配),不修改远程模块导出的组件对象(它可能是跨页面共享的模块实例);attrs/slots 全量透传;
862
+ - 加载顺序:`beforeLoad` → 远程可选 `setup`/`onSession`(loadRemote 生命周期)→ 页面模块;失败进 `errorComponent` 错误态(显式错误码/根因/修法),不静默回退。
863
+
864
+ #### 10.2 `federation({ setup })` — 远程初始化生命周期
865
+
866
+ ```ts
867
+ // 远程 vite.config.ts
868
+ federation({ name: 'remote-a', exposes: { ... }, setup: './src/fulgurjs/setup.ts' })
869
+ ```
870
+
871
+ ```ts
872
+ // remote-a/src/fulgurjs/setup.ts —— 只有下面两个函数名有自动生命周期语义
873
+ import type { RemoteSetupContext, RemoteSetupModule } from '@fulgurjs/federation/runtime'
874
+
875
+ export default async function setup(context: RemoteSetupContext) {
876
+ // 应用级一次性注册:全局组件/指令、全局样式、locale 注入、宿主 app 上的插件安装
877
+ // context.appContext = 调用时的 AppContext 快照;context.signal = 生命周期信号
878
+ }
879
+ export async function onSession(context: RemoteSetupContext) {
880
+ // 会话级同步:当前用户、权限、字典、token 相关缓存
881
+ const data = await fetchUserDicts()
882
+ if (context.signal.aborted) return // ← await 之后写状态前必须检查:期间可能已换账号/退出
883
+ writeToStores(data)
884
+ }
885
+ ```
886
+
887
+ `RemoteSetupContext`:`{ appContext: Readonly<AppContext>, sessionKey?: string, signal: AbortSignal }`。`appContext` 是**调用时**从页面级 AppContext 取得的快照(不保存永不更新的旧引用);`signal` 在登录代次变化或退出清理时失效。
888
+
889
+ 固定契约:
890
+
891
+ | 维度 | 语义 |
892
+ |---|---|
893
+ | 触发入口 | `loadRemote('remote/模块')` 是**统一入口**(`remoteComponent` 与宿主页面适配器同源)。`loadRemote('remote')`、`getContainer()`、`preloadRemote()`、直调 `container.get()` 都**不执行**初始化 |
894
+ | 时序 | 取得容器 → `init(shareScope)`(共享作用域收养)→ **setup** → **onSession** → 返回业务模块。setup 模块自身的导入在该阶段完成共享协商 |
895
+ | 执行次数(setup) | 每容器一次;并发调用共享同一 Promise;成功后不重复。`preloadRemote` 只预取资源不执行 |
896
+ | 执行次数(onSession) | 按非敏感 `sessionKey` 去重:同一登录代次一次;新代次先作废旧 `signal`,再按远程**串行**衔接旧调用与新调用(防两账号异步写入交错);`clearAppContext()` 失效去重状态,下次登录必须重跑。应用级 setup 不因退出/换代重复执行 |
897
+ | sessionKey | 宿主登录流程每次成功登录/重登生成新代次(非敏感 ID,禁止用 token);token 刷新但会话未变时沿用。**有 onSession 却缺 sessionKey → MFU-013**,不凭用户对象引用猜测身份;无 onSession 的远程无需 sessionKey |
898
+ | 导出校验 | 必须默认导出函数;具名 `onSession` 可选且必须是函数;其他导出不作为入口。违反 → MFU-011(报实际类型/预期签名/修法) |
899
+ | 失败与重试 | setup/onSession 抛错 → 该次 `loadRemote` 拒绝(MFU-012);**只清失败阶段的缓存**(setup 失败重试从 setup 开始;onSession 失败只重跑会话段),已成功的阶段不重复。`fallbackModule` 不掩盖初始化失败 |
900
+ | 自递归 | setup/onSession 同步段内 `loadRemote(同 remote/…)` → MFU-014(该调用会等待自身形成死锁)。异步段内的同远程递归无法精确归因,表现为挂起——不要在初始化内加载同远程模块 |
901
+ | dev/prod 一致 | dev 容器(中间件直出)与 prod 容器(构建产物)携带同一 setup 元数据(容器上的 `__fulgurjsSetup` 字段 + manifest 的 `setup` 字段);内部 expose 键 `./__fulgurjs_setup__` 不出现在 dts 类型与公开文档 exposes 清单中 |
902
+ | 错误码 | MFU-011 导出非法 / MFU-012 执行失败 / MFU-013 缺 sessionKey / MFU-014 自递归;全部带 remote 名、模块路径/阶段、实际结果、预期与修法,不记录 token |
903
+
904
+ 旧的「`exposes: { './federatedBoot': ... }` + 宿主手动 `loadRemote` 并调用」写法**继续可用**(旧项目分批迁移),但不再是推荐接法——见[迁移指南](#文档)的兼容说明。
905
+
906
+
907
+
677
908
  ## ⚠️ 首次使用避坑指南(真实迁移项目踩坑实录)
678
909
 
679
910
  以下每一条都在真实企业工程(qiankun → 联邦迁移,3 万模块级)中实际踩到过:
@@ -759,7 +990,7 @@ const Panel = await loadRemote('shop/Panel', {
759
990
 
760
991
  运行时加载失败同样给排查指引(remote dev server 未启动 / 地址配错 / CORS / NGINX 回退),并携带统一错误码:
761
992
 
762
- 统一错误码体系(CFG/DEV/BLD/MFU/CC 五段共 35 个)——**完整总表见上方 [API 参考 §6](#6-错误码总表35-个)**;报错文案一律「现象 → 根因 → 修法」三段式。
993
+ 统一错误码体系(CFG/DEV/BLD/MFU/CC 五段共 41 个)——**完整总表见上方 [API 参考 §6](#6-错误码总表41-个)**;报错文案一律「现象 → 根因 → 修法」三段式。
763
994
 
764
995
  调试出口:`window.__FULGURJS_SCOPE__`(share 协商实时结果)、`window.__FULGURJS_INFO__`(remote 状态/耗时/错误)。
765
996
 
@@ -785,7 +1016,7 @@ const Panel = await loadRemote('shop/Panel', {
785
1016
  pnpm --dir packages/plugin install && pnpm --dir packages/plugin build
786
1017
  for app in fixtures/host-vue fixtures/remote-a fixtures/remote-b e2e; do pnpm --dir "$app" install; done
787
1018
 
788
- pnpm test:unit # 单测(188)
1019
+ pnpm test:unit # 单测(369)
789
1020
  pnpm test:dev # dev e2e(10)
790
1021
  pnpm test:prod # prod e2e(8,需 NGINX,见 e2e/scripts/prod-setup.sh)
791
1022
  pnpm test # unit + dev + prod 全跑