@fulgurjs/federation 2.0.3 → 2.1.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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.0(2026-09-23)
4
+
5
+ ### 兼容性与健壮性强化(WP1~WP8,方案见 docs/兼容性与健壮性强化实施方案.md)
6
+
7
+ - **新增单一 API 入口 `virtual:fulgurjs-api`**:一个虚拟模块拿全联邦 API(runtime 全部公开函数 + `definePages` / `validatePages` + `remoteSchema`);旧入口(`virtual:fulgurjs-runtime`、`@fulgurjs/federation/pages`、`virtual:fulgurjs-remote-schema`)全部继续可用且与新旧入口收敛同一运行时单例。
8
+ - **修复 auto-import 后置注入绕过门面化的一类缺陷**(WP1):unplugin-auto-import 的 vite 适配器硬编码 `enforce: 'post'`,注册在 federation() 之后时其注入的 shared 导入会静态绑定本地副本(双响应性系统:ref 赋值不触发渲染)。修复 = 解析期兜底改道(已被本插件改写过的模块内后置出现的裸 shared specifier → 协商命名空间门面),与插件注册顺序无关。
9
+ - **manifest 契约**(WP4):`fulgurjs-manifest.json` / dev manifest 携带 `schemaVersion: 1`;Node 侧消费端(dts / remote-schema probe / doctor)统一经契约校验器取数;未知主版本拒绝消费并给出诊断(不再静默当空 manifest);2.0.x 无 schemaVersion 形态按 v1 兼容。
10
+ - **修复根相对 remote 地址的资产解析**(WP4):`remotes: { x: { prod: '/xxx' } }` 目录形态 entry 下,manifest 相对资产此前解析到站点根(404);现按 entry 所在目录解析。manifest fetch 增加 8s 超时。
11
+ - **dts 路径边界**(WP5):dev manifest 的 `exposes[].src` 只接受相对路径(拒绝绝对路径 / `..` / 空);`fsRoot` 与目标 realpath 后做包含判定(symlink 逃逸拒绝);异常 remote 只跳过自身不落半截声明;生成声明中的模块名统一合法 TS 字符串序列化。
12
+ - **新增 `devCorsOrigins` / `devFsRoot` 选项**(WP5):dev 跨源访问策略统一(插件端点与 server.cors 同一来源;用户显式 `server.cors` 永远优先;数组按 Origin 反射 allowlist);`devFsRoot: false` 时 dev manifest 不携带本机路径。非 loopback host 下通配 CORS / fsRoot 暴露分别提醒(DEV-011 / DEV-012)。
13
+ - **运行时容错**(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)。
14
+ - **新增 `parseSpec` 运行时导出**(WP7 顺带修复):类型声明早已存在但 runtime bundle 未导出(导出面漂移),现补齐。
15
+ - **受控诊断 `DEBUG=fulgurjs:*`**(WP8,默认关闭):`FULGURJS_DEBUG` / `DEBUG` 环境变量开启分类诊断(`transform` / `facade` / `manifest`,JSON → stderr);模块路径脱敏(root 内相对路径、root 外仅文件名),不输出源码文本与凭证;替代一切 /tmp 临时日志。
16
+ - **错误码新增**:CFG-009(remote 运行参数非法)、CFG-010(devCorsOrigins 形态非法)、DEV-011 / DEV-012(非 loopback 暴露面提醒),总数 31 → 35(三方一致性门禁自动校验)。
17
+ - **测试与 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 加载)作业。
18
+
19
+
3
20
  ## 2.0.3(2026-09-23)
4
21
 
5
22
  ### 修复(production remote CSS manifest / preload)
package/README.md CHANGED
@@ -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 入口(推荐)**:一个虚拟模块拿全联邦 API——
133
+
134
+ ```ts
135
+ import { loadRemote, loadShare, preloadRemote, definePages, remoteSchema } from 'virtual:fulgurjs-api'
136
+ ```
137
+
138
+ 旧入口(`virtual:fulgurjs-runtime`、`@fulgurjs/federation/pages` 的 `definePages`、`virtual:fulgurjs-remote-schema`)**全部继续可用**,新旧入口取得的是同一个运行时单例(`globalThis.__FULGURJS_RUNTIME__`);类型声明同样聚合在 `@fulgurjs/federation/client`。
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
@@ -242,6 +272,8 @@ import { loadRemote } from 'virtual:fulgurjs-runtime'
242
272
 
243
273
  #### 函数总表
244
274
 
275
+ > 下表全部函数与 `definePages` / `remoteSchema` 都可从**单一入口** `virtual:fulgurjs-api` 导入(推荐写法,见 §2);旧入口全部继续可用且收敛同一运行时单例。
276
+
245
277
  > **TS 提示**:`virtual:fulgurjs-runtime` 的类型随包发布。dev 启动时插件自动在类型目录(默认 `src/fulgurjs/types/`,联邦产物集中一个文件夹)生成远程模块声明与运行时类型垫片——src 布局项目零配置即全量生效;手工方式则在 tsconfig `compilerOptions.types` 加 `"@fulgurjs/federation/client"`。
246
278
 
247
279
  | 函数 | 签名 | 说明 |
@@ -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,7 +301,11 @@ 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
311
  import type { RuntimePlugin } from 'virtual:fulgurjs-runtime'
@@ -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(文档化核对项) |
@@ -636,6 +676,21 @@ const PREFETCH_REMOTES: string[] = []
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,8 @@
1
1
  /**
2
- * virtual:fulgurjs-runtime 客户端类型声明。
2
+ * virtual:fulgurjs-runtime / virtual:fulgurjs-api 客户端类型声明。
3
+ *
4
+ * `virtual:fulgurjs-api` 是单一 API 门面(推荐入口):一个虚拟模块拿全联邦 API——
5
+ * runtime 全部函数 + definePages/validatePages + remoteSchema;类型由本声明聚合。
3
6
  *
4
7
  * 用法(二选一):
5
8
  * 1. dev 启动后插件自动在类型目录(默认 src/fulgurjs/types/,无 src 布局回退 .fulgurjs/types/)
@@ -131,3 +134,16 @@ declare module 'virtual:fulgurjs-runtime' {
131
134
  const runtimeDefault: FgRuntime
132
135
  export default runtimeDefault
133
136
  }
137
+
138
+ /**
139
+ * WP7:单一 API 门面。
140
+ * 与 runtime 旧入口共享同一单例(globalThis.__FULGURJS_RUNTIME__);definePages/validatePages
141
+ * 来自 '@fulgurjs/federation/pages',remoteSchema 为远程 exposes 清单(dev 探针结果;
142
+ * build 为空 schema)。
143
+ */
144
+ declare module 'virtual:fulgurjs-api' {
145
+ export * from 'virtual:fulgurjs-runtime'
146
+ export { definePages, validatePages } from '@fulgurjs/federation/pages'
147
+ /** 远程 exposes 清单(dev:异步 probe 结果;build:空对象,路由存在性校验按 R3 降级) */
148
+ export const remoteSchema: { [remoteKey: string]: { exposes: string[]; exists: boolean } }
149
+ }
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var RUNTIME_VERSION = "2.0.3";
2
+ var RUNTIME_VERSION = "2.1.0";
3
3
 
4
4
  export {
5
5
  RUNTIME_VERSION
package/dist/cli.js CHANGED
@@ -206,7 +206,7 @@ async function inspectConfig(configPath) {
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
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()");
209
+ out.push("5. \u8054\u90A6 API \u7EDF\u4E00\u4ECE\u5355\u4E00\u5165\u53E3\u5BFC\u5165\uFF1Aimport { loadRemote, definePages, remoteSchema } from 'virtual:fulgurjs-api'\uFF08\u65E7\u5165\u53E3 virtual:fulgurjs-runtime / @fulgurjs/federation/pages / virtual:fulgurjs-remote-schema \u4ECD\u53EF\u7528\u4E14\u6536\u655B\u540C\u4E00\u8FD0\u884C\u65F6\u5355\u4F8B\uFF1B\u8FDC\u7A0B\u9875\u9762\u76F4\u63A5\u9759\u6001\u5BFC\u5165\u4EFB\u4E00\u5165\u53E3\u90FD\u4F1A\u88AB\u81EA\u52A8\u6539\u5199\u4E3A\u60F0\u6027\u5355\u4F8B\u59D4\u6258\uFF0C\u7279\u9700\u76F4\u53D6\u5168\u5C40\u5355\u4F8B\u65F6\u7528 getRuntime()\uFF09");
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,