@fulgurjs/federation 2.0.2 → 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 +27 -0
- package/README.md +68 -9
- package/client.d.ts +17 -1
- package/dist/{chunk-UDLQALHC.js → chunk-RP6HNIQT.js} +1 -1
- package/dist/cli.js +147 -3
- package/dist/index.cjs +536 -86
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +538 -86
- package/dist/runtime.js +3 -3
- package/dist/vue.cjs +170 -40
- package/dist/vue.js +171 -41
- package/docs/P5-vite7-8/345/205/274/345/256/271/347/237/251/351/230/265.md +4 -2
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
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
|
+
|
|
20
|
+
## 2.0.3(2026-09-23)
|
|
21
|
+
|
|
22
|
+
### 修复(production remote CSS manifest / preload)
|
|
23
|
+
|
|
24
|
+
- **修复 expose manifest 漏报 CSS**:Vite 可将 expose 导入的全局 CSS 归属到其静态依赖 chunk;此前插件只读取 expose facade 自身的 `viteMetadata.importedCss`,导致样式不进入 `fulgurjs-manifest.json`。现在在 `generateBundle` post 阶段递归收集 expose 静态依赖图中的 CSS,再写入对应 expose 条目。
|
|
25
|
+
- **修复根相对 remote 地址**:配置 `prod: '/lowcode'` 时也能生成 `/lowcode/fulgurjs-manifest.json`;保留绝对地址与协议相对地址的 origin/path 语义。
|
|
26
|
+
- **运行时按 expose 加载样式**:`loadRemote('remote/Expose')` 在 manifest 可用时预载该 expose 的 JS chunk 与 CSS,并等待 stylesheet load/error 后再返回模块;仅传 remote 名时保留预载全部 exposes 的行为。CSS 失败报告 `MFU-007`,不阻断 JS 模块加载。
|
|
27
|
+
- **补齐 build 后置转换**:build 阶段允许处理 pre 阶段标记过、但随后由 auto-import 等插件注入新 import 的模块;依赖 `transformModule` 幂等,serve 路径维持原有重复处理守卫。
|
|
28
|
+
- **回归验证**:覆盖静态依赖 chunk 持有 CSS、根相对 manifest URL、请求 expose 的 CSS 预载与等待行为;8662 实际运行态认证弹窗验收 computed `z-index: 5000`。
|
|
29
|
+
|
|
3
30
|
## 2.0.2(2026-09-22)
|
|
4
31
|
|
|
5
32
|
### 修复(D6:双向宿主 devSharedSelf 开启后 prod 构建产物 chunk 循环崩溃)
|
package/README.md
CHANGED
|
@@ -120,11 +120,23 @@ const Panel = await loadRemote('shop/Panel', {
|
|
|
120
120
|
})
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
>
|
|
123
|
+
> **普通源码文件都可以直接这样导入**——包括 exposes 目标文件(远程页面)。远程页面里的该导入会被
|
|
124
124
|
> 插件自动改写为惰性单例委托:求值期零副作用、调用期转发页面级运行时单例,无需关心宿主/远程的区别。
|
|
125
125
|
|
|
126
|
+
> **构建入口文件是边界**:Vite 的 HTML module entry 在 build 时由插件优先内联 init 并直接返回,入口文件自身的 remote import 不会进入改写管线。把 `import('remote-a/Page')` 放在入口导入的普通模块中;不要在 `main.ts` / `main.js` 里直接写 remote import。该边界是有意保留的,不代表入口文件中的语法也会被改写。
|
|
127
|
+
|
|
128
|
+
`loadRemote('remote-a/Page')` 在 remote 配置提供 manifest 时,会先按 expose 预载对应 CSS,再解析返回模块;CSS 请求失败会报告 `MFU-007`,但不会阻断 JS 模块加载。expose 若依赖全局 CSS,需从该 expose 的依赖图中导入,确保资源进入 manifest。
|
|
129
|
+
|
|
126
130
|
> 以上只是最小面。**全部选项(remotes 四形态/shared 九个开关/dts/runtimePlugins…)、运行时 API、CLI、错误码见下方 [API 参考](#api-参考)。**
|
|
127
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
|
+
|
|
128
140
|
**没有别的步骤了。** dev 下 remote 跑它自己的 `vite dev`(容器入口 `/@fulgurjs-entry.js` 由插件中间件直出);build 下 expose 自动拆独立 chunk、shared 自动剥离——同一份配置两端通用。
|
|
129
141
|
|
|
130
142
|
## CLI:init 起步模板 + doctor 部署体检
|
|
@@ -173,8 +185,10 @@ import { federation, type FederationOptions } from '@fulgurjs/federation'
|
|
|
173
185
|
| `runtimeChunk` | `boolean \| 'single'` | — | 运行时是否拆独立 chunk |
|
|
174
186
|
| `manifest` | `boolean` | `true` | prod 构建生成 `fulgurjs-manifest.json`(preloadRemote 依赖它) |
|
|
175
187
|
| `runtimePlugins` | `string[]` | `[]` | 运行时插件模块路径列表(写法见「运行时插件」) |
|
|
176
|
-
| `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 的安全边界——只对可信来源开启 |
|
|
177
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 |
|
|
178
192
|
| `automaticAsyncBoundary` | — | 恒为 `true` | 接受任意值:TLA 自动异步边界,无需手工 bootstrap |
|
|
179
193
|
| `dataPrefetch` | — | 恒为 `true` | 接受任意值:`preloadRemote` 始终可用 |
|
|
180
194
|
| `usedExports` / `ignoreUnusedSharedExports` | — | no-op | 接受并忽略(Rollup/Rolldown 原生 tree-shaking 已覆盖) |
|
|
@@ -192,7 +206,7 @@ remotes: {
|
|
|
192
206
|
dev: 'http://localhost:5103/remote-b',
|
|
193
207
|
prod: '/remote-b',
|
|
194
208
|
shareScope: 'default',
|
|
195
|
-
timeout: 15000, // 加载超时 ms
|
|
209
|
+
timeout: 15000, // 加载超时 ms(有限正数,配置期校验 CFG-009)
|
|
196
210
|
retries: 2, // 失败重试次数
|
|
197
211
|
fallback: ['http://backup/remote-b'], // 备用 remoteEntry,依次尝试
|
|
198
212
|
breaker: { threshold: 5, resetMs: 30000 }, // 连续失败熔断
|
|
@@ -203,6 +217,26 @@ remotes: {
|
|
|
203
217
|
}
|
|
204
218
|
```
|
|
205
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
|
+
|
|
206
240
|
#### shared 的完整选项(SharedHint)
|
|
207
241
|
|
|
208
242
|
```ts
|
|
@@ -238,15 +272,17 @@ import { loadRemote } from 'virtual:fulgurjs-runtime'
|
|
|
238
272
|
|
|
239
273
|
#### 函数总表
|
|
240
274
|
|
|
275
|
+
> 下表全部函数与 `definePages` / `remoteSchema` 都可从**单一入口** `virtual:fulgurjs-api` 导入(推荐写法,见 §2);旧入口全部继续可用且收敛同一运行时单例。
|
|
276
|
+
|
|
241
277
|
> **TS 提示**:`virtual:fulgurjs-runtime` 的类型随包发布。dev 启动时插件自动在类型目录(默认 `src/fulgurjs/types/`,联邦产物集中一个文件夹)生成远程模块声明与运行时类型垫片——src 布局项目零配置即全量生效;手工方式则在 tsconfig `compilerOptions.types` 加 `"@fulgurjs/federation/client"`。
|
|
242
278
|
|
|
243
279
|
| 函数 | 签名 | 说明 |
|
|
244
280
|
|---|---|---|
|
|
245
281
|
| `loadRemote` | `(spec: string, opts?) => Promise<模块命名空间>` | 加载远程模块。`spec = '远程名/./Expose键'`(`./` 可省);opts 见下 |
|
|
246
282
|
| `loadShare` | `(name: string, opts?) => Promise<命名空间>` | 共享模块协商(最高版本胜出/已加载优先/singleton 收敛)。opts:`{ requiredVersion?, singleton?, strictVersion?, shareKey?, shareScope?, fallback? }` |
|
|
247
|
-
| `preloadRemote` | `(spec: string, opts?: { mode?: 'preload' \| 'prefetch' }) => Promise<void>` |
|
|
283
|
+
| `preloadRemote` | `(spec: string, opts?: { mode?: 'preload' \| 'prefetch' }) => Promise<void>` | `remote/Expose` 只预载该 expose 的 chunk + CSS;仅传 remote 名则预载全部 exposes。`preload` 等待 CSS load/error,`prefetch` 低优先级并立即返回 |
|
|
248
284
|
| `getContainer` | `(name: string) => Promise<容器>` | 取远程容器(触发加载 + init),容器协议 `{ name, init, get }` |
|
|
249
|
-
| `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 记录,不会重复初始化同一容器 |
|
|
250
286
|
| `registerShare` | `(scope, name, version, get, opts?) => void` | 手工注册共享模块(一般由 init 模块自动完成) |
|
|
251
287
|
| `initSharing` | `(scopeName?) => ShareScopeMap` | 初始化共享作用域(一般由 init 模块自动完成) |
|
|
252
288
|
| `registerPlugins` | `(plugins: RuntimePlugin[]) => void` | 注册运行时插件(见下) |
|
|
@@ -265,7 +301,11 @@ const Panel = await loadRemote('shop/Panel', {
|
|
|
265
301
|
})
|
|
266
302
|
```
|
|
267
303
|
|
|
268
|
-
####
|
|
304
|
+
#### 运行时插件
|
|
305
|
+
|
|
306
|
+
(`runtimePlugins: ['./src/fulgurjsPlugin.ts']`)
|
|
307
|
+
|
|
308
|
+
> hook 错误契约:`beforeLoadRemote` / `afterLoadRemote` 是**观测 hook**——自身抛错只告警、不改写加载结果;`resolveShare` 是**决策 hook**——显式抛错向调用方传播(绝不静默回退到另一份共享依赖)。
|
|
269
309
|
|
|
270
310
|
```ts
|
|
271
311
|
import type { RuntimePlugin } from 'virtual:fulgurjs-runtime'
|
|
@@ -373,7 +413,7 @@ export default defineRepoConfig({
|
|
|
373
413
|
| `fulgurjs init --config <path>` | 校验配置(CFG 三段式报错)+ 输出各应用 `federation()` 粘贴块、NGINX no-cache 站点模板、8 条通用核对清单 |
|
|
374
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 门禁 |
|
|
375
415
|
|
|
376
|
-
### 6. 错误码总表(
|
|
416
|
+
### 6. 错误码总表(35 个)
|
|
377
417
|
|
|
378
418
|
| 段 | 码 | 含义 |
|
|
379
419
|
|---|---|---|
|
|
@@ -385,6 +425,8 @@ export default defineRepoConfig({
|
|
|
385
425
|
| | `CFG-006` | 孤岛配置(既不提供也不消费) |
|
|
386
426
|
| | `CFG-007` | remotes 对象形式误用 name@ 前缀(整串当 URL 拼接) |
|
|
387
427
|
| | `CFG-008` | shared 非法组合(eager+import:false / shareKey 重复声明) |
|
|
428
|
+
| | `CFG-009` | remotes 运行参数非法(timeout/retries/breaker 非有限正数/超上限) |
|
|
429
|
+
| | `CFG-010` | devCorsOrigins 形态非法(须为 "*" 或 http(s) 来源数组) |
|
|
388
430
|
| DEV 开发期 | `DEV-001` | remote dev server 不可达(manifest 拉取失败) |
|
|
389
431
|
| | `DEV-002` | remote dev manifest 为空或格式不识别 |
|
|
390
432
|
| | `DEV-004` | 已知 UMD-only 依赖不在 optimizeDeps.include(预构建内联本地 vue 风险) |
|
|
@@ -392,6 +434,8 @@ export default defineRepoConfig({
|
|
|
392
434
|
| | `DEV-006` | 宿主/远程插件版本不一致 |
|
|
393
435
|
| | `DEV-009` | 门面/虚拟模块 404(.vite 缓存漂移,需清缓存重启) |
|
|
394
436
|
| | `DEV-010` | dev 冷启动预构建窗口提示(首轮 30~60s 瞬态,非故障) |
|
|
437
|
+
| | `DEV-011` | 非 loopback host + 通配 dev CORS(暴露面扩大提醒) |
|
|
438
|
+
| | `DEV-012` | 非 loopback host + dev manifest 携带 fsRoot(本机路径外发提醒) |
|
|
395
439
|
| BLD 构建期 | `BLD-001` | expose 源文件解析失败 |
|
|
396
440
|
| | `BLD-002` | 构建目标低于 es2022(TLA 需要) |
|
|
397
441
|
| | `BLD-003` | expose 目标组件含必填 props(文档化核对项) |
|
|
@@ -606,7 +650,7 @@ const PREFETCH_REMOTES: string[] = []
|
|
|
606
650
|
行为与边界:
|
|
607
651
|
|
|
608
652
|
- 预载失败**不阻断业务**:runtime 按 MFU-007 语义发出 `window` 的 `fulgurjs:error` 事件并 console 警告(诊断面板⑤可查历史);
|
|
609
|
-
-
|
|
653
|
+
- 预载注入 `<link rel="modulepreload">` 与 `<link rel="stylesheet">`,不执行模块——首次打开页面时才真正初始化容器;`preload` 等待样式 load/error,`prefetch` 低优先级后台加载;
|
|
610
654
|
- 触发时机:宿主桥每次页面加载同步执行(幂等),实际预取发生在浏览器空闲回调中。
|
|
611
655
|
|
|
612
656
|
#### 9.1.4 联邦诊断面板(免登录页,无配置项)
|
|
@@ -632,6 +676,21 @@ const PREFETCH_REMOTES: string[] = []
|
|
|
632
676
|
|
|
633
677
|
以下每一条都在真实企业工程(qiankun → 联邦迁移,3 万模块级)中实际踩到过:
|
|
634
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
|
+
|
|
635
694
|
### 1. 插件升级后,重启 dev server 即可(缓存自动清)
|
|
636
695
|
|
|
637
696
|
vite 对 `node_modules/.vite` 预构建产物下发**一年 immutable 缓存**,插件 dist 更新后旧签名会 404。插件在 dev server 启动时**自动检测版本变化并清除缓存**——你只需要重启 dev server,无需手工 `rm -rf node_modules/.vite`。浏览器侧缓存建议 e2e/验收时换新 profile。
|
|
@@ -698,7 +757,7 @@ const Panel = await loadRemote('shop/Panel', {
|
|
|
698
757
|
|
|
699
758
|
运行时加载失败同样给排查指引(remote dev server 未启动 / 地址配错 / CORS / NGINX 回退),并携带统一错误码:
|
|
700
759
|
|
|
701
|
-
统一错误码体系(CFG/DEV/BLD/MFU
|
|
760
|
+
统一错误码体系(CFG/DEV/BLD/MFU/CC 五段共 35 个)——**完整总表见上方 [API 参考 §6](#6-错误码总表35-个)**;报错文案一律「现象 → 根因 → 修法」三段式。
|
|
702
761
|
|
|
703
762
|
调试出口:`window.__FULGURJS_SCOPE__`(share 协商实时结果)、`window.__FULGURJS_INFO__`(remote 状态/耗时/错误)。
|
|
704
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
|
+
}
|
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\
|
|
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
|
-
|
|
451
|
-
|
|
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,
|