@fulgurjs/federation 2.0.3 → 3.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.0(2026-09-24)
4
+
5
+ ### 破坏性变更:应用代码唯一 API 入口 `virtual:fulgurjs-api`
6
+
7
+ 应用代码的一切联邦导入收敛为一个虚拟模块;旧入口从包 exports 白名单删除(import 即解析失败)。
8
+
9
+ **迁移映射**:
10
+
11
+ | 3.x 之前 | 3.0.0 起 |
12
+ |---|---|
13
+ | `import { loadRemote, … } from 'virtual:fulgurjs-runtime'` | `import { loadRemote, … } from 'virtual:fulgurjs-api'` |
14
+ | `import { provideAppContext, getAppContext, requireAppContext } from '@fulgurjs/federation/context'` | 同一来源改为 `'virtual:fulgurjs-api'` |
15
+ | `import { definePages, validatePages } from '@fulgurjs/federation/pages'` | 同一来源改为 `'virtual:fulgurjs-api'` |
16
+ | `import { remoteComponent } from '@fulgurjs/federation/vue'` | 同一来源改为 `'virtual:fulgurjs-api'` |
17
+ | `import remoteSchema from 'virtual:fulgurjs-remote-schema'`(default) | `import { remoteSchema } from 'virtual:fulgurjs-api'`(具名) |
18
+
19
+ 不变(构建期/配置面,非应用代码导入):`vite.config.ts` 的 `import { federation } from '@fulgurjs/federation'`、`fulgurjs.config.ts` 的 `import { defineRepoConfig } from '@fulgurjs/federation/config'`、tsconfig 类型入口 `@fulgurjs/federation/client`。`virtual:fulgurjs-runtime` 保留为插件内部实现细节(门面/容器入口/改写管线引用),不再是公开 API。
20
+
21
+ 技术说明:serve 形态的门面对 runtime 部分转发惰性单例委托(远程页面导入不拉起副本链),prod 形态为静态 re-export(各副本经 `globalThis.__FULGURJS_RUNTIME__` 收敛);`remoteComponent` 在 dev 下调用期惰性加载。类型声明整体聚合到 `virtual:fulgurjs-api`(`@fulgurjs/federation/client`)。
22
+
23
+ ### 破坏性变更:2.x 全部旧入口不再可用
24
+
25
+ `@fulgurjs/federation/pages`、`./context`、`./vue` 子路径的 d.ts/typesVersions 映射同步删除。升级方式:全局搜索上述五个旧来源,按映射表替换为 `virtual:fulgurjs-api`(`fulgurjs init` 生成的模板与核对清单已全部是新写法)。
26
+
27
+
28
+ ## 2.1.0(2026-09-23)
29
+
30
+ ### 兼容性与健壮性强化(WP1~WP8,方案见 docs/兼容性与健壮性强化实施方案.md)
31
+
32
+ - **新增单一 API 入口 `virtual:fulgurjs-api`**:一个虚拟模块拿全联邦 API(runtime 全部公开函数 + `definePages` / `validatePages` + `remoteSchema`);旧入口(`virtual:fulgurjs-runtime`、`@fulgurjs/federation/pages`、`virtual:fulgurjs-remote-schema`)全部继续可用且与新旧入口收敛同一运行时单例。
33
+ - **修复 auto-import 后置注入绕过门面化的一类缺陷**(WP1):unplugin-auto-import 的 vite 适配器硬编码 `enforce: 'post'`,注册在 federation() 之后时其注入的 shared 导入会静态绑定本地副本(双响应性系统:ref 赋值不触发渲染)。修复 = 解析期兜底改道(已被本插件改写过的模块内后置出现的裸 shared specifier → 协商命名空间门面),与插件注册顺序无关。
34
+ - **manifest 契约**(WP4):`fulgurjs-manifest.json` / dev manifest 携带 `schemaVersion: 1`;Node 侧消费端(dts / remote-schema probe / doctor)统一经契约校验器取数;未知主版本拒绝消费并给出诊断(不再静默当空 manifest);2.0.x 无 schemaVersion 形态按 v1 兼容。
35
+ - **修复根相对 remote 地址的资产解析**(WP4):`remotes: { x: { prod: '/xxx' } }` 目录形态 entry 下,manifest 相对资产此前解析到站点根(404);现按 entry 所在目录解析。manifest fetch 增加 8s 超时。
36
+ - **dts 路径边界**(WP5):dev manifest 的 `exposes[].src` 只接受相对路径(拒绝绝对路径 / `..` / 空);`fsRoot` 与目标 realpath 后做包含判定(symlink 逃逸拒绝);异常 remote 只跳过自身不落半截声明;生成声明中的模块名统一合法 TS 字符串序列化。
37
+ - **新增 `devCorsOrigins` / `devFsRoot` 选项**(WP5):dev 跨源访问策略统一(插件端点与 server.cors 同一来源;用户显式 `server.cors` 永远优先;数组按 Origin 反射 allowlist);`devFsRoot: false` 时 dev manifest 不携带本机路径。非 loopback host 下通配 CORS / fsRoot 暴露分别提醒(DEV-011 / DEV-012)。
38
+ - **运行时容错**(WP6):注册表全部无原型字典(`__proto__` / `constructor` 等键不再误读误写原型链);`registerRemote` 参数校验当场抛错(`timeout` 有限正数 / `retries` 0..10 整数 / `breaker` 有限正数;配置期 CFG-009 先拦);熔断 `threshold`/`resetMs` 按 remote 生效(重复注册刷新参数、保留计数状态);重试退避封顶 4s + 随机抖动;entry 动态 import 单一 in-flight(超时≠取消,慢成功后容器 init 恰一次);promise remote 的解析受 timeout 约束;观测 hook(`beforeLoadRemote`/`afterLoadRemote`)抛错只告警不改写加载结果、决策 hook(`resolveShare`)抛错向调用方传播;MFU-001 错误信息对 URL 脱敏(去凭证与 query)。
39
+ - **新增 `parseSpec` 运行时导出**(WP7 顺带修复):类型声明早已存在但 runtime bundle 未导出(导出面漂移),现补齐。
40
+ - **受控诊断 `DEBUG=fulgurjs:*`**(WP8,默认关闭):`FULGURJS_DEBUG` / `DEBUG` 环境变量开启分类诊断(`transform` / `facade` / `manifest`,JSON → stderr);模块路径脱敏(root 内相对路径、root 外仅文件名),不输出源码文本与凭证;替代一切 /tmp 临时日志。
41
+ - **错误码新增**:CFG-009(remote 运行参数非法)、CFG-010(devCorsOrigins 形态非法)、DEV-011 / DEV-012(非 loopback 暴露面提醒),总数 31 → 35(三方一致性门禁自动校验)。
42
+ - **测试与 CI**(WP1~WP3):新增 fixtures `remote-auto` / `host-auto`(auto-import 插件链回归);真实构建单测覆盖双引擎(Rollup 6.4.3 / Rolldown 8.3.0)× 双注册顺序 / manualChunks 对象/函数/无/数组四形态 / 危险环检测器 / manifest 资产存在性;prod-setup.sh 隔离改造(mktemp 专属目录、8999 被占自动选空闲端口、`--stop` 只停自己启动的实例);CI 新增 prod-e2e(runner 内 NGINX)、vite5 每周定时兼容(vite@5.1.4)、tarball consumer smoke(npm pack → 临时 consumer → exports/类型/build/dev 加载)作业。
43
+
44
+
3
45
  ## 2.0.3(2026-09-23)
4
46
 
5
47
  ### 修复(production remote CSS manifest / preload)
package/README.md CHANGED
@@ -32,8 +32,8 @@
32
32
  - **HMR 全链路**:remote 改动 → host 页面热更,L1 组件热替换 / L2 状态保留 / L3 错误覆盖与恢复
33
33
  - **零报错纪律**:配置问题启动瞬间三段式报错;联邦失败显式抛错(错误码 + 可执行修复建议),**无任何静默兜底路径**
34
34
  - **CLI(主包内置 bin)**:`fulgurjs init`——`fulgurjs.config.ts` 单配置驱动的迁移生成器(模板 = 真实工程验证形态:vite 配置/路由表/桥/联邦启动器/NGINX conf 全量编码,锚点补丁幂等可续跑);`fulgurjs doctor`——部署面体检(remoteEntry/manifest/HTML 缓存头与形态、CORS、chunk 抽样可达、版本 skew 预演、`--dev` 端口探测)
35
- - **跨应用传值与方法引用**:`@fulgurjs/federation/context` 子路径——`provideAppContext` / `getAppContext` / `requireAppContext`(缺键 `CC-001` 三段式、独立直开远程页 `CC-002` 显式)。宿主桥一次写入页面级单例(user/token/getToken/store/hostApp/locale/events 标准字段 + 项目扩展位),远程 boot 显式校验消费;方法引用两条通道 = context 携带函数引用(热路径直调)+ exposes 方法模块 `loadRemote('remote/api')`(低频重逻辑)。数据语义 = 传输层快照 + 函数引用,非响应式(与乾坤 props 同语义;"实时"靠函数引用拉取 / 宿主 pinia 共享 / 登录刷新三通道)
36
- - **Vue 直渲染**:`remoteComponent('remote/X')`(`@fulgurjs/federation/vue` 子路径)——`defineAsyncComponent + loadRemote` 的标准封装,加载失败显式错误占位(错误码+根因+修法),runtime.js 零框架依赖零体积增量
35
+ - **跨应用传值与方法引用**:`virtual:fulgurjs-api` 导出 `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 共享 / 登录刷新三通道)
36
+ - **Vue 直渲染**:`remoteComponent('remote/X')`(`virtual:fulgurjs-api` 导出)——`defineAsyncComponent + loadRemote` 的标准封装,加载失败显式错误占位(错误码+根因+修法),runtime.js 零框架依赖零体积增量
37
37
  - **CSP 友好**:原生 ESM 加载路径全程无 `eval` / `new Function`,可在严格 CSP(无 `unsafe-eval`)下运行
38
38
  - **全链路错误码体系(30 码)**:CFG/DEV/BLD/MFU/CC 五段 + 手册 §8 码表防漂移校验
39
39
 
@@ -99,13 +99,13 @@ export default defineConfig({
99
99
  import Button from 'remote-a/Button'
100
100
 
101
101
  // 动态导入 / 运行时 API
102
- import { loadRemote, registerRemote, preloadRemote } from 'virtual:fulgurjs-runtime'
102
+ import { loadRemote, registerRemote, preloadRemote } from 'virtual:fulgurjs-api'
103
103
 
104
104
  const Chart = defineAsyncComponent(() => loadRemote('remote-a/Chart').then(m => m.default))
105
105
 
106
106
  // Vue 组件直渲染:remoteComponent = 上行的标准封装
107
107
  // 加载失败显式错误占位(错误码+根因+修法),loading/错误组件可自定义
108
- import { remoteComponent } from '@fulgurjs/federation/vue'
108
+ import { remoteComponent } from 'virtual:fulgurjs-api'
109
109
  const ChartCard = remoteComponent('remote-a/Chart', { retries: 2 })
110
110
 
111
111
  // 构建时地址未知的远程?运行时注册(对齐 webpack promise remote 语义)
@@ -129,6 +129,14 @@ const Panel = await loadRemote('shop/Panel', {
129
129
 
130
130
  > 以上只是最小面。**全部选项(remotes 四形态/shared 九个开关/dts/runtimePlugins…)、运行时 API、CLI、错误码见下方 [API 参考](#api-参考)。**
131
131
 
132
+ **唯一 API 入口(3.0.0 破坏性收敛)**:应用代码的一切联邦导入——运行时函数、context 三函数、`definePages`、`remoteSchema`、`remoteComponent`——**只来自一个虚拟模块**:
133
+
134
+ ```ts
135
+ import { loadRemote, loadShare, preloadRemote, provideAppContext, getAppContext, definePages, remoteSchema, remoteComponent } from 'virtual:fulgurjs-api'
136
+ ```
137
+
138
+ 旧入口(`virtual:fulgurjs-runtime`、`@fulgurjs/federation/{context,pages,vue}` 子路径)**已删除**——包 exports 白名单不再暴露(迁移映射见 CHANGELOG 3.0.0)。`virtual:fulgurjs-runtime` 仅作为插件内部实现细节保留(门面/容器入口/改写管线引用),不进入文档与类型声明。
139
+
132
140
  **没有别的步骤了。** dev 下 remote 跑它自己的 `vite dev`(容器入口 `/@fulgurjs-entry.js` 由插件中间件直出);build 下 expose 自动拆独立 chunk、shared 自动剥离——同一份配置两端通用。
133
141
 
134
142
  ## CLI:init 起步模板 + doctor 部署体检
@@ -177,8 +185,10 @@ import { federation, type FederationOptions } from '@fulgurjs/federation'
177
185
  | `runtimeChunk` | `boolean \| 'single'` | — | 运行时是否拆独立 chunk |
178
186
  | `manifest` | `boolean` | `true` | prod 构建生成 `fulgurjs-manifest.json`(preloadRemote 依赖它) |
179
187
  | `runtimePlugins` | `string[]` | `[]` | 运行时插件模块路径列表(写法见「运行时插件」) |
180
- | `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) |
188
+ | `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 的安全边界——只对可信来源开启 |
181
189
  | `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`) |
190
+ | `devCorsOrigins` | `string[] \| '*'` | `'*'`(现状兼容) | dev 跨源访问策略:插件端点(`/@fulgurjs-entry.js`、`/@fulgurjs-manifest.json`)与 `server.cors` 使用同一来源。缺省或 `'*'` 全放开(非 loopback host 时提醒 DEV-011);数组按 Origin 反射 allowlist(未命中省略头)。用户显式配置的 `server.cors` 永远优先。开/关/自定义三态示例见下方 |
191
+ | `devFsRoot` | `boolean` | `true`(现状兼容) | dev manifest 是否携带 `fsRoot`(remote 根目录本机绝对路径,宿主 dts 类型直连用)。`false` 不写入(本机路径不外发),宿主 dts 降级 any 桩并提示;该字段永不进入 prod manifest。非 loopback host 下默认值会提醒 DEV-012 |
182
192
  | `automaticAsyncBoundary` | — | 恒为 `true` | 接受任意值:TLA 自动异步边界,无需手工 bootstrap |
183
193
  | `dataPrefetch` | — | 恒为 `true` | 接受任意值:`preloadRemote` 始终可用 |
184
194
  | `usedExports` / `ignoreUnusedSharedExports` | — | no-op | 接受并忽略(Rollup/Rolldown 原生 tree-shaking 已覆盖) |
@@ -196,7 +206,7 @@ remotes: {
196
206
  dev: 'http://localhost:5103/remote-b',
197
207
  prod: '/remote-b',
198
208
  shareScope: 'default',
199
- timeout: 15000, // 加载超时 ms
209
+ timeout: 15000, // 加载超时 ms(有限正数,配置期校验 CFG-009)
200
210
  retries: 2, // 失败重试次数
201
211
  fallback: ['http://backup/remote-b'], // 备用 remoteEntry,依次尝试
202
212
  breaker: { threshold: 5, resetMs: 30000 }, // 连续失败熔断
@@ -207,6 +217,26 @@ remotes: {
207
217
  }
208
218
  ```
209
219
 
220
+ #### devCorsOrigins / devFsRoot 三态示例
221
+
222
+ ```ts
223
+ // ① 开(默认/现状):全放开——跨 dev-server 协作开箱即用;非 loopback host 时提醒 DEV-011/012
224
+ federation({ name: 'remote-a', exposes: { './Button': './src/Button.vue' } })
225
+
226
+ // ② 显式全开:同 ①,但不再提醒(声明"我知情")
227
+ federation({ name: 'remote-a', exposes: { './Button': './src/Button.vue' }, devCorsOrigins: '*' })
228
+
229
+ // ③ 自定义 allowlist:仅列出的宿主来源可跨源访问联邦端点与源码模块
230
+ federation({
231
+ name: 'remote-a',
232
+ exposes: { './Button': './src/Button.vue' },
233
+ devCorsOrigins: ['http://localhost:5100', 'https://team.example.com'],
234
+ devFsRoot: false, // 同时不把本机绝对路径写进 dev manifest(宿主 dts 降级 any 桩并提示)
235
+ })
236
+ ```
237
+
238
+ 行为边界:`devCorsOrigins` 只作用于 dev(build 产物不受影响);用户显式配置的 `server.cors` 永远优先于插件注入的 cors 选项;端点对未命中来源只是省略 `Access-Control-Allow-Origin` 响应头(同源请求不受任何影响)。`devFsRoot: false` 只影响 dev manifest 的 `fsRoot` 字段(该字段永不进入 prod manifest)。
239
+
210
240
  #### shared 的完整选项(SharedHint)
211
241
 
212
242
  ```ts
@@ -229,20 +259,22 @@ shared: {
229
259
 
230
260
  版本裁决语义对齐 webpack:满足 requiredVersion 的最高版本胜出;已加载版本永不替换;singleton 收敛到唯一实例(skew 告警 MFU-010);strictVersion 不满足抛 MFU-003。
231
261
 
232
- ### 2. 运行时 API — `virtual:fulgurjs-runtime`
262
+ ### 2. 运行时 API — `virtual:fulgurjs-api`
233
263
 
234
264
  **任何文件都直接静态导入**——宿主页面、exposes 目标文件(远程页面)都一样,插件自动保证同一页面只有一个运行时实例(远程页面里的导入会被自动改写为惰性单例委托):
235
265
 
236
266
  ```ts
237
267
  // 宿主页面、远程页面,写法完全一致
238
- import { loadRemote } from 'virtual:fulgurjs-runtime'
268
+ import { loadRemote } from 'virtual:fulgurjs-api'
239
269
  ```
240
270
 
241
271
  > 仍可绕过代理直取全局单例(等价,调试用):`(globalThis as any).__FULGURJS_RUNTIME__`。
242
272
 
243
273
  #### 函数总表
244
274
 
245
- > **TS 提示**:`virtual:fulgurjs-runtime` 的类型随包发布。dev 启动时插件自动在类型目录(默认 `src/fulgurjs/types/`,联邦产物集中一个文件夹)生成远程模块声明与运行时类型垫片——src 布局项目零配置即全量生效;手工方式则在 tsconfig `compilerOptions.types` 加 `"@fulgurjs/federation/client"`。
275
+ > 下表全部函数与 `definePages` / `remoteSchema` / `provideAppContext` 等 context 函数 / `remoteComponent` 都从**唯一入口** `virtual:fulgurjs-api` 导入(见 §2);旧入口已删除。
276
+
277
+ > **TS 提示**:`virtual:fulgurjs-api` 的类型随包发布。dev 启动时插件自动在类型目录(默认 `src/fulgurjs/types/`,联邦产物集中一个文件夹)生成远程模块声明与运行时类型垫片——src 布局项目零配置即全量生效;手工方式则在 tsconfig `compilerOptions.types` 加 `"@fulgurjs/federation/client"`。
246
278
 
247
279
  | 函数 | 签名 | 说明 |
248
280
  |---|---|---|
@@ -250,7 +282,7 @@ import { loadRemote } from 'virtual:fulgurjs-runtime'
250
282
  | `loadShare` | `(name: string, opts?) => Promise<命名空间>` | 共享模块协商(最高版本胜出/已加载优先/singleton 收敛)。opts:`{ requiredVersion?, singleton?, strictVersion?, shareKey?, shareScope?, fallback? }` |
251
283
  | `preloadRemote` | `(spec: string, opts?: { mode?: 'preload' \| 'prefetch' }) => Promise<void>` | `remote/Expose` 只预载该 expose 的 chunk + CSS;仅传 remote 名则预载全部 exposes。`preload` 等待 CSS load/error,`prefetch` 低优先级并立即返回 |
252
284
  | `getContainer` | `(name: string) => Promise<容器>` | 取远程容器(触发加载 + init),容器协议 `{ name, init, get }` |
253
- | `registerRemote` / `registerRemotes` | `(config \| list) => void` | 运行时注册远程(promise remote / 动态地址)。`RemoteInput`:`{ name, entry, shareScope?, timeout?, retries?, fallback?, breaker? }` |
285
+ | `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 记录,不会重复初始化同一容器 |
254
286
  | `registerShare` | `(scope, name, version, get, opts?) => void` | 手工注册共享模块(一般由 init 模块自动完成) |
255
287
  | `initSharing` | `(scopeName?) => ShareScopeMap` | 初始化共享作用域(一般由 init 模块自动完成) |
256
288
  | `registerPlugins` | `(plugins: RuntimePlugin[]) => void` | 注册运行时插件(见下) |
@@ -269,10 +301,14 @@ const Panel = await loadRemote('shop/Panel', {
269
301
  })
270
302
  ```
271
303
 
272
- #### 运行时插件(`runtimePlugins: ['./src/fulgurjsPlugin.ts']`)
304
+ #### 运行时插件
305
+
306
+ (`runtimePlugins: ['./src/fulgurjsPlugin.ts']`)
307
+
308
+ > hook 错误契约:`beforeLoadRemote` / `afterLoadRemote` 是**观测 hook**——自身抛错只告警、不改写加载结果;`resolveShare` 是**决策 hook**——显式抛错向调用方传播(绝不静默回退到另一份共享依赖)。
273
309
 
274
310
  ```ts
275
- import type { RuntimePlugin } from 'virtual:fulgurjs-runtime'
311
+ import type { RuntimePlugin } from 'virtual:fulgurjs-api'
276
312
 
277
313
  export default {
278
314
  name: 'my-plugin',
@@ -296,13 +332,13 @@ export default {
296
332
  | `window.__FULGURJS_APP_CONFIG__` | W4 全局配置镜像 |
297
333
  | `window` 事件 `fulgurjs:error` | `CustomEvent<{ remote, error }>`,所有远程加载/共享错误都会发出 |
298
334
 
299
- ### 3. `definePages` — 宿主页面路由表(`@fulgurjs/federation/pages`)
335
+ ### 3. `definePages` — 宿主页面路由表(`virtual:fulgurjs-api`)
300
336
 
301
337
  宿主把「URL 路径 → 远程 exposes 键」的映射表交给它校验,带参路由的静默冲突在启动期报错而不是运行时加载错组件:
302
338
 
303
339
  ```ts
304
- import { definePages } from '@fulgurjs/federation/pages'
305
- import remoteSchema from 'virtual:fulgurjs-remote-schema' // dev 自动生成;build 恒为空(诚实降级)
340
+ import { definePages } from 'virtual:fulgurjs-api'
341
+ import { remoteSchema } from 'virtual:fulgurjs-api' // dev 自动生成;build 恒为空(诚实降级)
306
342
 
307
343
  export const PAGES = definePages(
308
344
  [
@@ -377,7 +413,7 @@ export default defineRepoConfig({
377
413
  | `fulgurjs init --config <path>` | 校验配置(CFG 三段式报错)+ 输出各应用 `federation()` 粘贴块、NGINX no-cache 站点模板、8 条通用核对清单 |
378
414
  | `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 门禁 |
379
415
 
380
- ### 6. 错误码总表(31 个)
416
+ ### 6. 错误码总表(35 个)
381
417
 
382
418
  | 段 | 码 | 含义 |
383
419
  |---|---|---|
@@ -389,6 +425,8 @@ export default defineRepoConfig({
389
425
  | | `CFG-006` | 孤岛配置(既不提供也不消费) |
390
426
  | | `CFG-007` | remotes 对象形式误用 name@ 前缀(整串当 URL 拼接) |
391
427
  | | `CFG-008` | shared 非法组合(eager+import:false / shareKey 重复声明) |
428
+ | | `CFG-009` | remotes 运行参数非法(timeout/retries/breaker 非有限正数/超上限) |
429
+ | | `CFG-010` | devCorsOrigins 形态非法(须为 "*" 或 http(s) 来源数组) |
392
430
  | DEV 开发期 | `DEV-001` | remote dev server 不可达(manifest 拉取失败) |
393
431
  | | `DEV-002` | remote dev manifest 为空或格式不识别 |
394
432
  | | `DEV-004` | 已知 UMD-only 依赖不在 optimizeDeps.include(预构建内联本地 vue 风险) |
@@ -396,6 +434,8 @@ export default defineRepoConfig({
396
434
  | | `DEV-006` | 宿主/远程插件版本不一致 |
397
435
  | | `DEV-009` | 门面/虚拟模块 404(.vite 缓存漂移,需清缓存重启) |
398
436
  | | `DEV-010` | dev 冷启动预构建窗口提示(首轮 30~60s 瞬态,非故障) |
437
+ | | `DEV-011` | 非 loopback host + 通配 dev CORS(暴露面扩大提醒) |
438
+ | | `DEV-012` | 非 loopback host + dev manifest 携带 fsRoot(本机路径外发提醒) |
399
439
  | BLD 构建期 | `BLD-001` | expose 源文件解析失败 |
400
440
  | | `BLD-002` | 构建目标低于 es2022(TLA 需要) |
401
441
  | | `BLD-003` | expose 目标组件含必填 props(文档化核对项) |
@@ -426,10 +466,10 @@ export default defineRepoConfig({
426
466
 
427
467
  NGINX 部署模板(no-cache 规则 + 深链回退)用 `fulgurjs init --config` 自动生成。
428
468
 
429
- ### 8. `remoteComponent` — Vue 远程组件直渲染(`@fulgurjs/federation/vue`)
469
+ ### 8. `remoteComponent` — Vue 远程组件直渲染(`virtual:fulgurjs-api`)
430
470
 
431
471
  ```ts
432
- import { remoteComponent } from '@fulgurjs/federation/vue'
472
+ import { remoteComponent } from 'virtual:fulgurjs-api'
433
473
 
434
474
  const FederatedBusinessForm = remoteComponent('demo-host/FormRouterPage')
435
475
  const FederatedAmisForm = remoteComponent('demo-host/AmisFormRouterPage', {
@@ -453,15 +493,15 @@ const FederatedAmisForm = remoteComponent('demo-host/AmisFormRouterPage', {
453
493
  - **无任何兜底/降级**(H3 零兜底):加载失败显式进错误态;不传 `errorComponent` 时渲染内置占位(错误码 + 根因 + 修法三段式文案),`window` 的 `fulgurjs:error` 事件由 runtime 层照常发出;
454
494
  - 模块去重沿用 `loadRemote` 内部 Promise 缓存——同 spec 多组件实例只加载一次容器模块;
455
495
  - `vue` 为**可选 peerDependency**(`peerDependenciesMeta.optional`):仅使用 `./vue` 子路径时才需要安装 Vue;runtime.js 保持框架无关(不 import vue),体积零增量;
456
- - 运行时实例经 `globalThis.__FULGURJS_RUNTIME__` 页面级单例复用,与 `virtual:fulgurjs-runtime` 的导入殊途同归,无需额外接线。
496
+ - 运行时实例经 `globalThis.__FULGURJS_RUNTIME__` 页面级单例复用,与 `virtual:fulgurjs-api` 的导入殊途同归,无需额外接线。
457
497
 
458
- ### 9. `AppContext` — 跨应用传值与方法引用(`@fulgurjs/federation/context`)
498
+ ### 9. `AppContext` — 跨应用传值与方法引用(`virtual:fulgurjs-api`)
459
499
 
460
500
  宿主向子应用传值、子应用向宿主反向注册方法,一律走这条一等公民通道(对标乾坤 `props`,但带类型与错误契约)——不再各自挂 `window.*` 裸口子。
461
501
 
462
502
  ```ts
463
503
  // —— 宿主桥(host/src/fulgurjs/host/bridge.ts):登录完成后一次性提供 ——
464
- import { provideAppContext } from '@fulgurjs/federation/context'
504
+ import { provideAppContext } from 'virtual:fulgurjs-api'
465
505
 
466
506
  provideAppContext({
467
507
  user, // 宿主登录用户原始形态
@@ -474,14 +514,14 @@ provideAppContext({
474
514
  })
475
515
 
476
516
  // —— 远程 boot(exposes/federatedBoot.ts):显式校验消费 ——
477
- import { requireAppContext } from '@fulgurjs/federation/context'
517
+ import { requireAppContext } from 'virtual:fulgurjs-api'
478
518
 
479
519
  const { store, user, hostApp } = requireAppContext('store', 'user', 'hostApp')
480
520
  // 缺任一键 → [fulgurjs:CC-001] 三段式抛错(got / expected / example 指向宿主桥);
481
521
  // 页面无运行时单例(独立直开远程页)→ [fulgurjs:CC-002] 显式,修法 = 经宿主联邦加载。
482
522
 
483
523
  // —— 远程页面读点 ——
484
- import { getAppContext } from '@fulgurjs/federation/context'
524
+ import { getAppContext } from 'virtual:fulgurjs-api'
485
525
  const dict = getAppContext().events?.main?.getDictItems?.('sex')
486
526
 
487
527
  // —— 子应用反向注册方法给宿主(页面 onUnmounted 时记得摘除,见迁移指南「页面卸载清理清单」)——
@@ -629,13 +669,28 @@ const PREFETCH_REMOTES: string[] = []
629
669
  #### 9.1.5 IDE 说明(`src/fulgurjs/` 目录的红波浪线)
630
670
 
631
671
  - `types/` 下的 `*.d.ts` 是**插件每次 dev 自动生成**的类型直连声明(勿手改):内部 `export * from '../../../demo-app-xxx/src/***.vue'` 指向**兄弟工程的源码**。命令行 `vue-tsc --noEmit`(走本应用 tsconfig,skipLibCheck 生效)为 **0 错误**;但 **VSCode/Volar 在打开这些 d.ts 时**可能把工程外 .vue 用推断项目(inferred project,无 tsconfig 上下文)展开检查,显示大片"找不到模块 '@/...'"——**仅编辑器显示问题,不影响命令行检查与构建**,不打开 `types/` 生成物即无感。
632
- - 升级插件版本后若 `import '@fulgurjs/federation/context'` 报 ts(2307):是 IDE 的 TS 服务缓存了旧包——`Restart TS Server`(⌘⇧P)或重开窗口即可。
672
+ - 升级插件版本后若 `import ... from 'virtual:fulgurjs-api'` 报 ts(2307):是 IDE 的 TS 服务缓存了旧包——`Restart TS Server`(⌘⇧P)或重开窗口即可。3.0.0 起旧子路径(`@fulgurjs/federation/{context,pages,vue}`)已从包 exports 删除,按迁移映射改为 `virtual:fulgurjs-api`。
633
673
  - **根治红波浪线**:`federation({ dts: { mode: 'shim' } })` —— 生成物不再引用跨工程源文件(宽松占位形态),IDE 全程干净;取舍是失去"跳转直达远程源码"的补全能力(默认 `source` 不变,按项目偏好选择)。
634
674
 
635
675
  ## ⚠️ 首次使用避坑指南(真实迁移项目踩坑实录)
636
676
 
637
677
  以下每一条都在真实企业工程(qiankun → 联邦迁移,3 万模块级)中实际踩到过:
638
678
 
679
+ ### 0. 受控诊断(DEBUG=fulgurjs:*,默认关闭)
680
+
681
+ 排查改写/门面/manifest 问题时开启结构化诊断(JSON 行 → stderr,不写文件):
682
+
683
+ ```bash
684
+ # 全部分类
685
+ FULGURJS_DEBUG='fulgurjs:*' pnpm dev # 或 DEBUG='fulgurjs:*'
686
+ # 只开一个分类(transform / facade / manifest)
687
+ FULGURJS_DEBUG='fulgurjs:transform' pnpm dev
688
+ # build 同样适用
689
+ FULGURJS_DEBUG='fulgurjs:*' pnpm build 2>fulgurjs-debug.log
690
+ ```
691
+
692
+ 输出示例:`[fulgurjs:debug:transform] {"stage":"pre","mode":"build","module":"src/pages/a.ts"}`、`[fulgurjs:debug:facade] {"stage":"config","facadeDynamic":true,"manualChunks":"object"}`。分类:`transform`(改写命中与阶段)、`facade`(门面形态/闭包归组)、`manifest`(expose 与 CSS 收集)。脱敏约定:模块路径 root 内显示相对路径、root 外只留文件名,不输出源码文本与 query/凭证。
693
+
639
694
  ### 1. 插件升级后,重启 dev server 即可(缓存自动清)
640
695
 
641
696
  vite 对 `node_modules/.vite` 预构建产物下发**一年 immutable 缓存**,插件 dist 更新后旧签名会 404。插件在 dev server 启动时**自动检测版本变化并清除缓存**——你只需要重启 dev server,无需手工 `rm -rf node_modules/.vite`。浏览器侧缓存建议 e2e/验收时换新 profile。
@@ -702,7 +757,7 @@ const Panel = await loadRemote('shop/Panel', {
702
757
 
703
758
  运行时加载失败同样给排查指引(remote dev server 未启动 / 地址配错 / CORS / NGINX 回退),并携带统一错误码:
704
759
 
705
- 统一错误码体系(CFG/DEV/BLD/MFU 四段共 31 个)——**完整总表见上方 [API 参考 §6](#6-错误码总表31-个)**;报错文案一律「现象 → 根因 → 修法」三段式。
760
+ 统一错误码体系(CFG/DEV/BLD/MFU/CC 五段共 35 个)——**完整总表见上方 [API 参考 §6](#6-错误码总表35-个)**;报错文案一律「现象 → 根因 → 修法」三段式。
706
761
 
707
762
  调试出口:`window.__FULGURJS_SCOPE__`(share 协商实时结果)、`window.__FULGURJS_INFO__`(remote 状态/耗时/错误)。
708
763
 
package/client.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  /**
2
- * virtual:fulgurjs-runtime 客户端类型声明。
2
+ * virtual:fulgurjs-api 客户端类型声明(3.0.0 起唯一公开运行时入口)。
3
+ *
4
+ * 应用代码的一切联邦导入——runtime 函数、context 三函数(provideAppContext/
5
+ * getAppContext/requireAppContext)、definePages/validatePages、remoteSchema、
6
+ * remoteComponent——全部来自 'virtual:fulgurjs-api'。旧入口
7
+ * (virtual:fulgurjs-runtime、@fulgurjs/federation/{context,pages,vue})已删除。
8
+ * 类型由本声明聚合(script 文件:顶层无 import/export,环境模块声明才生效)。
3
9
  *
4
10
  * 用法(二选一):
5
11
  * 1. dev 启动后插件自动在类型目录(默认 src/fulgurjs/types/,无 src 布局回退 .fulgurjs/types/)
@@ -13,7 +19,7 @@
13
19
  * 对外类型(LoadRemoteOptions 等)经 'virtual:fulgurjs-runtime' 模块本身导出,
14
20
  * 用法:import type { LoadRemoteOptions } from 'virtual:fulgurjs-runtime'。
15
21
  */
16
- declare module 'virtual:fulgurjs-runtime' {
22
+ declare module 'virtual:fulgurjs-api' {
17
23
  /** shared 协商条目(window.__FULGURJS_SCOPE__ 内的形态) */
18
24
  export interface ShareEntry {
19
25
  version: string
@@ -130,4 +136,45 @@ declare module 'virtual:fulgurjs-runtime' {
130
136
  /** 与 globalThis.__FULGURJS_RUNTIME__ 同一实例(方法面冻结) */
131
137
  const runtimeDefault: FgRuntime
132
138
  export default runtimeDefault
139
+
140
+ // ── 跨应用上下文(原 @fulgurjs/federation/context,3.0.0 并入)──
141
+ export function provideAppContext(config: Partial<AppContext> & Record<string, unknown>): void
142
+ export function getAppContext(): AppContext
143
+ export function requireAppContext(...keys: string[]): AppContext
144
+
145
+ // ── 页面路由表(原 @fulgurjs/federation/pages,3.0.0 并入)──
146
+ export interface PageRouteLike {
147
+ route: string
148
+ name?: string
149
+ spec?: string
150
+ title?: string
151
+ keepAlive?: boolean
152
+ }
153
+ export interface PagesOptions {
154
+ pages: PageRouteLike[]
155
+ remoteSchema?: { [remoteKey: string]: { exposes: string[]; exists: boolean } }
156
+ remoteNameOf?: (route: string) => string
157
+ exposeOf?: (route: string, remoteName: string) => string
158
+ }
159
+ export interface PageViolation {
160
+ level: 'ERROR' | 'WARN'
161
+ route: string
162
+ reason: string
163
+ }
164
+ export function validatePages(pages: PageRouteLike[], opts?: PagesOptions): PageViolation[]
165
+ export function definePages<P extends PageRouteLike[]>(pages: P, opts?: PagesOptions): P
166
+
167
+ // ── Vue 远程组件直渲染(原 @fulgurjs/federation/vue,3.0.0 并入)──
168
+ export interface RemoteComponentOptions {
169
+ loadingComponent?: unknown
170
+ errorComponent?: unknown
171
+ retries?: number
172
+ delay?: number
173
+ timeout?: number
174
+ }
175
+ export function remoteComponent(spec: string, opts?: RemoteComponentOptions): unknown
176
+
177
+ // ── 远程 exposes 清单(dev 异步 probe;build 为空表,路由校验按 R3 降级)──
178
+ export const remoteSchema: { [remoteKey: string]: { exposes: string[]; exists: boolean } }
133
179
  }
180
+
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var RUNTIME_VERSION = "2.0.3";
2
+ var RUNTIME_VERSION = "3.0.0";
3
3
 
4
4
  export {
5
5
  RUNTIME_VERSION
package/dist/cli.js CHANGED
@@ -205,8 +205,8 @@ async function inspectConfig(configPath) {
205
205
  out.push("2. expose \u4E00\u5F8B\u6307\u5411\u72EC\u7ACB\u9875\uFF08\u9875\u9762\u4ECE\u8DEF\u7531\u53D6\u53C2\uFF09\uFF1B\u7EC4\u4EF6\u9700\u8981\u5FC5\u586B props \u65F6\u7ED9\u9ED8\u8BA4\u503C\uFF08BLD-003\uFF09");
206
206
  out.push("3. shared \u91CC vue / vue-router / pinia \u5EFA\u8BAE singleton: true\u2014\u2014\u8DE8\u5E94\u7528\u5FC5\u987B\u540C\u5B9E\u4F8B\uFF08\u5168\u5C40\u54CD\u5E94\u6027\u3001getActivePinia\u3001\u8DEF\u7531\u6CE8\u5165\uFF09");
207
207
  out.push("4. \u8FDC\u7A0B\u7684\u5168\u5C40\u526F\u4F5C\u7528\uFF08\u5168\u5C40\u7EC4\u4EF6/\u6307\u4EE4/\u542F\u52A8\u671F\u521D\u59CB\u5316\uFF09\u5C01\u88C5\u4E3A\u542F\u52A8\u5668\u6A21\u5757\u5E76 expose\uFF0C\u5BBF\u4E3B\u5728 loadRemote \u9875\u9762\u524D\u8C03\u7528\uFF1B");
208
- out.push(" \u8DE8\u5E94\u7528\u4F20\u503C\uFF08locale/store/\u4E8B\u4EF6\u7B49\uFF09\u7EDF\u4E00\u8D70 @fulgurjs/federation/context\uFF1A\u5BBF\u4E3B provideAppContext \u4E00\u6B21\u5199\u5165\uFF0C\u8FDC\u7A0B boot \u7528 getAppContext / requireAppContext \u6D88\u8D39");
209
- out.push("5. \u8FDC\u7A0B\u9875\u9762\u53EF\u76F4\u63A5\u9759\u6001\u5BFC\u5165 virtual:fulgurjs-runtime\uFF08\u63D2\u4EF6\u81EA\u52A8\u6539\u5199\u4E3A\u60F0\u6027\u5355\u4F8B\u59D4\u6258\uFF09\uFF1B\u7279\u9700\u76F4\u53D6\u5168\u5C40\u5355\u4F8B\u65F6\u7528 getRuntime()");
208
+ out.push(" \u8DE8\u5E94\u7528\u4F20\u503C\uFF08locale/store/\u4E8B\u4EF6\u7B49\uFF09\u7EDF\u4E00\u8D70 virtual:fulgurjs-api \u7684 context \u51FD\u6570\uFF1A\u5BBF\u4E3B provideAppContext \u4E00\u6B21\u5199\u5165\uFF0C\u8FDC\u7A0B boot \u7528 getAppContext / requireAppContext \u6D88\u8D39");
209
+ out.push("5. \u5E94\u7528\u4EE3\u7801\u552F\u4E00 API \u5165\u53E3\uFF1Aimport { loadRemote, provideAppContext, getAppContext, definePages, remoteSchema, remoteComponent } from 'virtual:fulgurjs-api'\u2014\u20143.0.0 \u8D77\u65E7\u5165\u53E3\uFF08virtual:fulgurjs-runtime / @fulgurjs/federation \u7684 context/pages/vue \u5B50\u8DEF\u5F84\uFF09\u5DF2\u5220\u9664\uFF0C\u4E00\u5F8B\u6539\u7528\u672C\u5165\u53E3");
210
210
  out.push('6. dev \u51B7\u542F\u52A8\u9996\u8F6E 30~60s \u6709\u9884\u6784\u5EFA\u7A97\u53E3\uFF08\u77AC\u65F6 504/"ce"\uFF0CDEV-010\uFF09\uFF1A\u5148\u771F\u5B9E\u6253\u5F00\u9875\u9762\u9884\u70ED\u518D\u505A\u65AD\u8A00');
211
211
  out.push("7. \u90E8\u7F72\u540E\u4F53\u68C0\uFF1Afulgurjs doctor --base <URL> --apps <\u5E94\u7528...>\uFF08\u7F13\u5B58\u5934/\u8D44\u6E90\u5F62\u6001/CORS/chunk \u53EF\u8FBE/\u7248\u672C skew\uFF09");
212
212
  out.push("8. \u90E8\u7F72\u8BED\u4E49\uFF1AremoteEntry/manifest/index.html \u5FC5\u987B no-cache\uFF08\u4E25\u7981 immutable\uFF09\uFF1B\u5E26 hash \u7684 assets \u957F\u7F13\u5B58");
@@ -280,6 +280,129 @@ import { resolve } from "node:path";
280
280
  // src/doctor.ts
281
281
  import http from "node:http";
282
282
  import https from "node:https";
283
+
284
+ // src/manifest.ts
285
+ var MANIFEST_SCHEMA_VERSION = 1;
286
+ var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
287
+ function validAssetRef(v) {
288
+ if (typeof v !== "string" || v.length === 0) return false;
289
+ if (/[\s\u0000-\u001f]/.test(v)) return false;
290
+ if (/^https?:\/\/[^/@\s]+:[^/@\s]+@/.test(v)) return false;
291
+ return true;
292
+ }
293
+ function validExposeName(v) {
294
+ return typeof v === "string" && /^\.[\w./-]+$/.test(v);
295
+ }
296
+ function parseManifest(input) {
297
+ const issues = [];
298
+ if (!isPlainObject(input)) {
299
+ return { issues: [{ field: "$", message: "manifest \u5FC5\u987B\u662F JSON \u5BF9\u8C61", got: typeof input }] };
300
+ }
301
+ const m = input;
302
+ let schemaVersion = MANIFEST_SCHEMA_VERSION;
303
+ if (m.schemaVersion !== void 0) {
304
+ if (typeof m.schemaVersion !== "number" || !Number.isInteger(m.schemaVersion)) {
305
+ issues.push({ field: "schemaVersion", message: "\u5FC5\u987B\u662F\u6574\u6570", got: m.schemaVersion });
306
+ } else if (m.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
307
+ return {
308
+ issues: [
309
+ {
310
+ field: "schemaVersion",
311
+ message: `\u4E0D\u652F\u6301\u7684 manifest \u4E3B\u7248\u672C\uFF08\u5F53\u524D\u6D88\u8D39\u7AEF\u652F\u6301 ${MANIFEST_SCHEMA_VERSION}\uFF09`,
312
+ got: m.schemaVersion
313
+ }
314
+ ],
315
+ unsupportedVersion: m.schemaVersion
316
+ };
317
+ }
318
+ }
319
+ if (typeof m.name !== "string" || m.name.length === 0) {
320
+ issues.push({ field: "name", message: "\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32", got: m.name });
321
+ }
322
+ const exposes = m.exposes;
323
+ const shared = m.shared;
324
+ const sharedIssues = validateShared(shared);
325
+ issues.push(...sharedIssues);
326
+ if (Array.isArray(exposes)) {
327
+ if (m.devServer !== true) {
328
+ issues.push({ field: "devServer", message: "dev \u6570\u7EC4\u5F62\u6001 exposes \u5FC5\u987B\u4F34\u968F devServer: true", got: m.devServer });
329
+ }
330
+ if (typeof m.base !== "string" || !/^\/(?:.*\/)?$/.test(m.base)) {
331
+ issues.push({ field: "base", message: "\u5FC5\u987B\u662F\u4EE5 / \u5F00\u5934\u4E14 / \u7ED3\u5C3E\u7684\u8DEF\u5F84", got: m.base });
332
+ }
333
+ if (!validAssetRef(m.entry)) {
334
+ issues.push({ field: "entry", message: "\u5FC5\u987B\u662F\u5408\u6CD5\u8D44\u6E90\u5730\u5740", got: m.entry });
335
+ }
336
+ for (const [i, e] of exposes.entries()) {
337
+ if (!isPlainObject(e)) {
338
+ issues.push({ field: `exposes[${i}]`, message: "\u5FC5\u987B\u662F\u5BF9\u8C61", got: typeof e });
339
+ continue;
340
+ }
341
+ if (!validExposeName(e.name)) {
342
+ issues.push({ field: `exposes[${i}].name`, message: '\u5FC5\u987B\u662F "./xxx" \u5F62\u6001\u7684 expose \u540D', got: e.name });
343
+ }
344
+ if (typeof e.src !== "string" || e.src.length === 0) {
345
+ issues.push({ field: `exposes[${i}].src`, message: "dev \u5F62\u6001\u5FC5\u987B\u63D0\u4F9B\u6E90\u7801\u76F8\u5BF9\u8DEF\u5F84", got: e.src });
346
+ }
347
+ if (!validAssetRef(e.file)) {
348
+ issues.push({ field: `exposes[${i}].file`, message: "\u5FC5\u987B\u662F\u53EF\u8BF7\u6C42\u7684\u6A21\u5757\u5730\u5740", got: e.file });
349
+ }
350
+ }
351
+ } else if (isPlainObject(exposes)) {
352
+ if (!validAssetRef(m.entry)) {
353
+ issues.push({ field: "entry", message: "\u5FC5\u987B\u662F\u5408\u6CD5\u8D44\u6E90\u5730\u5740\uFF08\u76F8\u5BF9\u57FA\u51C6 = \u6240\u5728\u76EE\u5F55\uFF09", got: m.entry });
354
+ }
355
+ for (const [key, v] of Object.entries(exposes)) {
356
+ if (!validExposeName(key)) {
357
+ issues.push({ field: `exposes["${key}"]`, message: '\u952E\u5FC5\u987B\u662F "./xxx" \u5F62\u6001\u7684 expose \u540D' });
358
+ }
359
+ if (!isPlainObject(v)) {
360
+ issues.push({ field: `exposes["${key}"]`, message: "\u503C\u5FC5\u987B\u662F\u5BF9\u8C61", got: typeof v });
361
+ continue;
362
+ }
363
+ if (!validAssetRef(v.file)) {
364
+ issues.push({ field: `exposes["${key}"].file`, message: "\u5FC5\u987B\u662F\u8D44\u6E90\u8DEF\u5F84\uFF08\u76F8\u5BF9 entry \u76EE\u5F55\uFF09", got: v.file });
365
+ }
366
+ if (v.css !== void 0) {
367
+ if (!Array.isArray(v.css)) {
368
+ issues.push({ field: `exposes["${key}"].css`, message: "\u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4", got: typeof v.css });
369
+ } else {
370
+ v.css.forEach((css, i) => {
371
+ if (!validAssetRef(css)) {
372
+ issues.push({ field: `exposes["${key}"].css[${i}]`, message: "\u5FC5\u987B\u662F\u8D44\u6E90\u8DEF\u5F84", got: css });
373
+ }
374
+ });
375
+ }
376
+ }
377
+ }
378
+ } else {
379
+ issues.push({ field: "exposes", message: "\u5FC5\u987B\u662F\u6570\u7EC4\uFF08dev\uFF09\u6216\u5BF9\u8C61\uFF08prod\uFF09", got: typeof exposes });
380
+ }
381
+ if (issues.length > 0) return { issues };
382
+ return { manifest: input, issues };
383
+ }
384
+ function validateShared(shared) {
385
+ if (shared === void 0) return [];
386
+ const issues = [];
387
+ if (!Array.isArray(shared)) {
388
+ return [{ field: "shared", message: "\u5FC5\u987B\u662F\u6570\u7EC4", got: typeof shared }];
389
+ }
390
+ for (const [i, s] of shared.entries()) {
391
+ if (!isPlainObject(s)) {
392
+ issues.push({ field: `shared[${i}]`, message: "\u5FC5\u987B\u662F\u5BF9\u8C61", got: typeof s });
393
+ continue;
394
+ }
395
+ if (typeof s.name !== "string" || s.name.length === 0) {
396
+ issues.push({ field: `shared[${i}].name`, message: "\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32", got: s.name });
397
+ }
398
+ if (typeof s.version !== "string" || s.version.length === 0) {
399
+ issues.push({ field: `shared[${i}].version`, message: "\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32", got: s.version });
400
+ }
401
+ }
402
+ return issues;
403
+ }
404
+
405
+ // src/doctor.ts
283
406
  function fetchHeadOrGet(url, method = "GET") {
284
407
  return new Promise((resolve2, reject) => {
285
408
  const mod = url.startsWith("https:") ? https : http;
@@ -447,8 +570,29 @@ async function runDoctor(opts) {
447
570
  let manifest;
448
571
  if (manifestRes.res?.status === 200) {
449
572
  try {
450
- manifest = JSON.parse(manifestRes.res.body);
451
- perAppShared.push({ app, shared: manifest.shared ?? [] });
573
+ const parsed = parseManifest(JSON.parse(manifestRes.res.body));
574
+ if (parsed.unsupportedVersion) {
575
+ checks.push({
576
+ app,
577
+ item: "manifest \u5951\u7EA6",
578
+ level: "FAIL",
579
+ symptom: `fulgurjs-manifest.json schemaVersion=${parsed.unsupportedVersion} \u4E0D\u53D7\u5F53\u524D doctor \u652F\u6301\uFF08\u652F\u6301 1\uFF09`,
580
+ cause: "\u90E8\u7F72\u4EA7\u7269\u7531\u66F4\u9AD8\u4E3B\u7248\u672C\u7684\u63D2\u4EF6\u751F\u6210",
581
+ fix: "\u7528\u4E0E\u4EA7\u7269\u5339\u914D\u7684 @fulgurjs/federation \u7248\u672C\u8FD0\u884C doctor\uFF0C\u6216\u91CD\u65B0\u6784\u5EFA\u90E8\u7F72"
582
+ });
583
+ } else if (parsed.issues.length > 0) {
584
+ checks.push({
585
+ app,
586
+ item: "manifest \u5951\u7EA6",
587
+ level: "FAIL",
588
+ symptom: `fulgurjs-manifest.json \u5951\u7EA6\u6821\u9A8C\u5931\u8D25\uFF1A${parsed.issues.map((x) => `${x.field}: ${x.message}`).join("\uFF1B")}`,
589
+ cause: "\u4EA7\u7269\u4E0D\u5B8C\u6574\u3001\u88AB\u4E2D\u95F4\u5C42\u6539\u5199\uFF0C\u6216\u7531\u4E0D\u517C\u5BB9\u7248\u672C\u751F\u6210",
590
+ fix: "\u91CD\u65B0\u6784\u5EFA\u90E8\u7F72\uFF1B\u786E\u8BA4 nginx \u672A\u5BF9\u8BE5\u8DEF\u5F84\u505A sub/\u62FC\u63A5\u6539\u5199"
591
+ });
592
+ } else {
593
+ manifest = parsed.manifest;
594
+ perAppShared.push({ app, shared: manifest.shared ?? [] });
595
+ }
452
596
  } catch {
453
597
  checks.push({
454
598
  app,