@fulgurjs/federation 5.0.4 → 5.1.1
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 +18 -0
- package/DESIGN.md +11 -0
- package/README.en.md +356 -0
- package/README.md +117 -4
- package/dist/chunk-4PLNAYZX.js +84 -0
- package/dist/cli.js +4 -1
- package/dist/host-pages-core-BFxp-IOj.d.cts +37 -0
- package/dist/host-pages-core-DLfMeDrO.d.ts +37 -0
- package/dist/index.cjs +282 -27
- package/dist/index.js +282 -27
- package/dist/react-adapter.cjs +521 -0
- package/dist/react-adapter.d.cts +117 -0
- package/dist/react-adapter.d.ts +117 -0
- package/dist/react-adapter.js +295 -0
- package/dist/react.d.ts +442 -0
- package/dist/react.js +10 -0
- package/dist/runtime-entry.d.ts +29 -17
- package/dist/runtime.js +13 -13
- package/dist/vue-adapter.cjs +66 -50
- package/dist/vue-adapter.d.cts +4 -25
- package/dist/vue-adapter.d.ts +4 -25
- package/dist/vue-adapter.js +14 -66
- package/examples/README.md +5 -0
- package/examples/react-host/README.md +28 -0
- package/examples/react-host/fulgurjs.config.ts +19 -0
- package/examples/react-host/index.html +11 -0
- package/examples/react-host/package.json +23 -0
- package/examples/react-host/src/App.tsx +66 -0
- package/examples/react-host/src/federation/pages.data.ts +21 -0
- package/examples/react-host/src/main.tsx +8 -0
- package/examples/react-host/tsconfig.json +14 -0
- package/examples/react-host/vite.config.ts +8 -0
- package/examples/react-remote/fulgurjs.config.ts +17 -0
- package/examples/react-remote/index.html +11 -0
- package/examples/react-remote/package.json +22 -0
- package/examples/react-remote/src/Button.tsx +15 -0
- package/examples/react-remote/src/main.tsx +6 -0
- package/examples/react-remote/src/pages/Detail.tsx +10 -0
- package/examples/react-remote/src/pages/Home.tsx +14 -0
- package/examples/react-remote/src/utils.ts +3 -0
- package/examples/react-remote/tsconfig.json +14 -0
- package/examples/react-remote/vite.config.ts +8 -0
- package/package.json +30 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.1(2026-09-28)
|
|
4
|
+
|
|
5
|
+
- **修复:同实例会话切换(React)**——已挂载的 `useLoadRemote`/`remoteComponent`/页面组件此前不观察 `AppContext.sessionKey`,宿主换账号后同实例不重载。现在渲染期读取当前登录代次,`sessionKey` 变化即在同一实例上重走加载生命周期(不重挂载、不产生第二份 React;同会话 rerender 不重载;`undefined→A`、`A→B`、`A→登出→B` 均覆盖;Vue KeepAlive 退出语义不受影响)。
|
|
6
|
+
- **修复:tsconfig paths 判定语义化**——宽松声明是否让位于精确轨,现在按 JSONC 语义解析(注释/无关配置/纯 exact 键不再误判),沿 extends 链继承,排除 `tsconfig.node.json`(node 上下文不影响应用导入);指向非插件精确目录的映射会跳过宽松声明并给出诊断。
|
|
7
|
+
- **修复:类型降级残留失效转发文件**——`devFsRoot:false` 或源码不可达降级时同步清理插件自有的 `<remote>.d/` 精确轨目录;同一工程内「精确→降级→恢复」全程真实编译通过(此前残留转发文件导致 TS2307)。`dts:false` 明确为只停不删。
|
|
8
|
+
- **修复:生产重试 URL 污染成功加载**——remoteEntry 的重试 helper 此前按调用次数 cache-bust(第二次成功加载也被改写成 retry URL,模块重复求值、单例身份分裂)。现在为失败驱动的 per-URL 状态机:成功永不改写(身份保持),仅真实失败后的下一次尝试变更 URL(`fulgurjs_retry=N` 单调递增,已带 query 用 `&` 拼接)。dev 容器 loader 与运行时入口语义不受影响。
|
|
9
|
+
- **修复:Vite 8 依赖预构建外部化**——Vite 8 的 rolldown 优化器对 `optimizeDeps.esbuildOptions.plugins` 仅执行 resolve(不执行 load),共享键外部化桩不可加载(UNLOADABLE_DEPENDENCY),远程 React 协商链全断。现在同时注入 `optimizeDeps.rolldownOptions.plugins`(识别兼容层产出的 namespace 前缀 id;门面 URL external;`isEntry` 放行预构建入口);Vite ≤ 7 行为不变。
|
|
10
|
+
- **测试与文档**:Playwright 项目与 spec 文件一一对应(此前 Vue 项目重复执行 React 用例);R15 类型检查改为独立负向用例矩阵(遗漏必填字段/错误字段类型/错误回调签名/函数参数,各断言预期诊断)+ 真实生成器 any 降级编译链;跨框架普通模块断言改真实导出成员与计算值;错误监听前置到导航前;英文 README 重写为独立完整手册(全部公共 API/字段/错误码/边界,不依赖中文补全)。React 18.0.0 精确下界补验(隔离工程 dev 全链)。
|
|
11
|
+
|
|
12
|
+
## 5.1.0(2026-09-28)
|
|
13
|
+
|
|
14
|
+
- **React 完整支持(浏览器客户端)**:新增 `@fulgurjs/federation/react` 入口——`remoteComponent`(pending/错误占位与错误边界内置、timeout 适配层超时、不用 React.lazy 的失败缓存陷阱)、`useLoadRemote`(代次守卫的模块 hook:StrictMode 双 effect/快速切换/慢请求晚返回/卸载后返回只允许最新有效请求写状态)、`RemoteErrorBoundary`(页面级兜底 + resetKeys)、`createReactHostPages`(与 Vue 共用同一份页面表数据与 definePages R1–R5 校验;不提供 keepAliveNames)。peer 新增可选 `react`/`react-dom`(`>=18 <20`);纯 React 项目零 Vue 依赖、纯 Vue 项目零 React 依赖(静态导入图与 tarball 消费双向守护)。共享 `react`/`react-dom` singleton:dev 期预构建外部化自动改道 jsx-runtime/jsx-dev-runtime 内部引用,prod 期 CJS require 垫片覆盖 `react-dom/client` 子路径;Hooks/StrictMode/Context 跨端单实例经真实浏览器 e2e 验证(React 19.3;18 隔离验证见验收报告)。
|
|
15
|
+
- **修复:失败恢复穿透浏览器 ESM 失败缓存**(Vue/React 通用)——同 URL 的失败 `import()` 会被浏览器 module map 缓存为失败(重试零网络请求)。运行时入口(`entryFailCounts` + `fulgurjs_retry=N` query)、dev 容器 expose loader(字面量主路径保持 vite URL 规范化一致 + `@vite-ignore` 重试分支)、prod remoteEntry 产物(`__fgR` 包装)三处统一实现「失败后的重试变更 URL」;服务恢复后点击重试真实重新拉取(此前仅整页刷新可恢复)。
|
|
16
|
+
- **修复:`react-adapter` 产物内联 react 的隐患**——peerDependencies 曾漏列 react/react-dom 导致 tsup 未外置;本版 peer 完整(此前版本无 React 入口,无实际影响面)。
|
|
17
|
+
- **开发类型双轨(Vue/React 通用)**:此前 ambient declare module 内的相对 re-export 是 TS2439 非法声明,被用户工程常规 skipLibCheck 静默吞成 any。现零配置生成合法带体宽松声明(可解析);新增精确轨 `<types>/<remote>.d/` 目录转发模块,宿主 tsconfig 配一段 `"paths": { "<remote>/*": ["<types目录>/<remote>.d/*"] }` 即获得源码级类型(错误 props/参数编译失败);配置了 paths 的远程自动跳过同名宽松声明避免遮蔽。
|
|
18
|
+
- **完整英文 README(README.en.md)**:与中文 README 同源的当前用法全量文档(含 React API/41 错误码/边界/懒加载口径),随 npm 包发布;npm 默认 README 仍为中文,两份顶部互链。
|
|
19
|
+
- 内部:vue-adapter 纯页面解析提取为共用的 host-pages-core.ts(Vue 行为与既有断言保持);新增 fixtures/host-react + fixtures/remote-react、examples/react-host + examples/react-remote、React dev/fault/prod e2e 套件与 CI 矩阵接线;prod-setup 隔离 NGINX 增加 /host-react 与 /remote-react 子路径。
|
|
20
|
+
|
|
3
21
|
## 5.0.4(2026-09-28)
|
|
4
22
|
|
|
5
23
|
- **修复开发类型生成的地址解析**:按宿主开发服务的 origin(包含协议)解析协议相对与同源相对的 remote dev 地址,避免页面可加载却因 `new URL()` 缺基址而跳过类型生成。新增真实 HTTP manifest 回归测试;5.0.3 的 any 降级声明修复保留。
|
package/DESIGN.md
CHANGED
|
@@ -159,6 +159,17 @@ remote 样式改动立即生效;host 自身业务 HMR 不受影响。
|
|
|
159
159
|
- **dev 与 prod(NGINX)两套截图各自备齐**,手册按环境归档引用
|
|
160
160
|
- 手动冒烟(如后台接口连通、NGINX reload)同样截图留证
|
|
161
161
|
|
|
162
|
+
## 6.1 双框架浏览器入口(5.1.0 React 支持)
|
|
163
|
+
|
|
164
|
+
- 构建期入口 `@fulgurjs/federation`(dist/index.js)不变。
|
|
165
|
+
- Vue 浏览器入口 `@fulgurjs/federation/runtime` → dist/runtime-entry.js → runtime.js(框架无关内核)+ context/pages + vue-adapter。
|
|
166
|
+
- React 浏览器入口 `@fulgurjs/federation/react` → dist/react.js(gen-runtime-entry.mjs 生成薄壳)→ 同一 runtime.js + context/pages + react-adapter(类型面 dist/react.d.ts 由 tsup 从 src/react.ts 生成;双入口独立 tsup 构建,防止 dts 共享 chunk)。
|
|
167
|
+
- 纯解析核心 `host-pages-core.ts` 自 vue-adapter 提取共用(definePages 接入/最长前缀/base/参数匹配/会话代次判定);Vue 保留组件命名缓存与 KeepAlive 语义,React 侧组件缓存仅按「新非空 sessionKey」重建且不提供 keepAliveNames。
|
|
168
|
+
- 开发态:`transform.rewriteRuntimeEntryImports` 同时识别 /runtime 与 /react(remoteSchema 拆分共用,expose 目标分别指向 `virtual:fulgurjs-api-facade` / `virtual:fulgurjs-api-facade-react`);`virtual.genApiFacade(framework)` 分别接两套适配器;纯 React 静态图无 Vue、纯 Vue 图无 React(runtime-entry-graph.test 双向守护,tarball smoke 以独立 React consumer 复核)。
|
|
169
|
+
- 共享子路径:`react`/`react-dom` singleton 协商 + dev 期 optimize-shared-external 预构建外部化(jsx-runtime 内部 require 改道门面)+ prod 期 cjsRequireRewrite;peer 声明 `react >=18 <20`(optional)——peer 缺失会导致 tsup 不外置、react 内联进适配器产物(双实例风险,实测教训)。
|
|
170
|
+
- 失败恢复:runtime `importEntry`(entryFailCounts)+ dev 容器 loader(字面量主路径 + `@vite-ignore` 重试分支;字面量保证与 vite importAnalysis 重写形态一致,拼接表达式会经 injectQuery 产生 `?import` 变体 URL 与内部静态 import 形成双模块实例)+ prod remoteEntry 产物后处理(`__fgR` 包装)三处统一「失败后重试变更 URL(fulgurjs_retry=N)穿透浏览器 module map 失败缓存」。
|
|
171
|
+
- 开发类型双轨:零配置 ambient(带体 any,可解析;简写 ambient 会 shadow paths 命中的转发文件,TS2439/TS2709 语言限制见 dts.ts 注释)+ 精确轨 `remote.d/` 目录 .ts 转发模块(export *)配 tsconfig paths;宿主配了 paths 的远程自动跳过同名 ambient。
|
|
172
|
+
|
|
162
173
|
## 7. 文档交付物
|
|
163
174
|
|
|
164
175
|
**唯一权威文档 = 仓库根 `README.md`**(随 npm 包发布,GitHub 与 npm 双端可读);迁移路径见 `docs/迁移指南.md`。内容组织:
|
package/README.en.md
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# @fulgurjs/federation
|
|
2
|
+
|
|
3
|
+
[简体中文](./README.md) | English
|
|
4
|
+
|
|
5
|
+
> **fulgurjs** — Latin for "lightning · flash of light".
|
|
6
|
+
> A Vite plugin that makes Module Federation work out of the box: **one config shape per project, separate dev & prod engines, semantics aligned with webpack Module Federation**, with first-class browser support for both Vue 3 and React 18/19.
|
|
7
|
+
|
|
8
|
+
  
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Why
|
|
13
|
+
|
|
14
|
+
| | webpack MF | other vite MF solutions | **@fulgurjs/federation** |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| dev experience | separate builds required | manual bootstrap usually required | ✅ dual dev-server direct wiring, zero manual async boundaries |
|
|
17
|
+
| prod artifacts | ✅ | often missing or degraded | ✅ build-time rewriting, stable remoteEntry filename + manifest |
|
|
18
|
+
| semantic parity | 100% | incomplete (version negotiation / singleton / fault tolerance often missing) | ✅ aligned clause-by-clause with webpack semantics, e2e-verified |
|
|
19
|
+
| **UMD / CJS-only deps** | DIY | **commonly unusable** | ✅ automatic (dep-optimizer externalization + build-time require shims) |
|
|
20
|
+
| remote load failures | raw errors | usually missing | ✅ retry / circuit breaker / timeout built in + explicit `fallbackModule` degradation |
|
|
21
|
+
| failure recovery | reload the page | usually missing | ✅ retries vary the URL after a real failure, so they penetrate the browser's failed-import cache and genuinely re-fetch |
|
|
22
|
+
| runtime size | ~40KB+ | varies | **gzip < 9KB** (framework-neutral core; adapters are separate) |
|
|
23
|
+
| misconfiguration | hard to debug | cryptic | three-part diagnostics: symptom / cause / fix |
|
|
24
|
+
|
|
25
|
+
## 2. Feature overview
|
|
26
|
+
|
|
27
|
+
- **Full exposes / remotes / shared semantics** — `name@url` syntax, key renaming, promise-based remotes, full-semver `requiredVersion`, version negotiation (highest wins), singleton / strictVersion, loaded versions are never replaced, multi-version coexistence, `shareKey` redirection, multiple share scopes
|
|
28
|
+
- **UMD / CJS-only deps out of the box** — element-plus, avue and other UMD/CJS-only packages simply go into `optimizeDeps.include`; in dev the plugin re-routes shared keys inside pre-bundled output to negotiation facades (esbuild path on Vite ≤ 7, rolldown plugin on Vite ≥ 8), in build CJS `require(<shared>)` calls are redirected to shims — dual-runtime immune
|
|
29
|
+
- **Automatic async boundaries** — top-level await injected automatically (es2022+); no webpack-style manual `import('./bootstrap')`
|
|
30
|
+
- **Stable artifacts** — remoteEntry keeps a fixed filename (content changes every build → **must be `no-cache`**; only content-hashed chunks may be cached long); `fulgurjs-manifest.json` asset manifest; one chunk per expose
|
|
31
|
+
- **Fault tolerance (webpack MF 2.0 errorLoadRemote aligned)** — retry / circuit breaker / timeout built in; `loadRemote(spec, { retries, fallbackModule })` per-call overrides; on failure the fallback module is returned and the error event is still emitted (**never silent**; without `fallbackModule` the error re-throws)
|
|
32
|
+
- **Real failure recovery** — browsers cache failed dynamic imports per URL (a retry of the same URL never reaches the network). After a real failure the runtime varies the URL (`fulgurjs_retry=N`) across remote-entry loading, dev container loaders and the prod remoteEntry, so "service recovered → click retry" genuinely re-fetches. Successful modules are never re-requested with a varied URL — module identity and singletons are preserved
|
|
33
|
+
- **Enhancements** — dev type generation (dual-track, see §8.6), manifest-driven `preloadRemote()`, runtime plugin hooks (`beforeLoadRemote` / `afterLoadRemote` / `onRemoteError` / `resolveShare`)
|
|
34
|
+
- **Full HMR chain** — remote edits propagate to the host page: component hot swap, state retention, error overlay and recovery
|
|
35
|
+
- **Zero-silent-failure discipline** — config problems fail at startup with three-part diagnostics; federation failures throw explicitly (error code + actionable fix); no silent fallback paths
|
|
36
|
+
- **CLI** — `fulgurjs init` / `explain` / `check-pages` / `doctor` (see §7)
|
|
37
|
+
- **Optional remote init lifecycle** — `federation({ setup })`: `setup(context)` runs once per app, `onSession(context)` runs once per host `sessionKey`; failures are explicit and retryable
|
|
38
|
+
- **Host page adapter** — one page table shared by routing and layout: URL resolution, longest-prefix remote attribution, R1–R5 validation, component cache keyed by login generation, skeleton/error placeholders, keep-alive names (Vue only)
|
|
39
|
+
- **Cross-app context** — `provideAppContext` / `getAppContext` / `requireAppContext` / `clearAppContext`; transport snapshot + function references (not reactive); account switching carried by `onSession` without page reloads
|
|
40
|
+
- **Vue direct rendering** — `remoteComponent('remote/X')` on the runtime entry: `defineAsyncComponent + loadRemote` wrapper with explicit error placeholder; runtime core stays framework-free
|
|
41
|
+
- **Full React support (browser)** — dedicated `@fulgurjs/federation/react` entry: `remoteComponent`, `useLoadRemote`, `RemoteErrorBoundary`, `createReactHostPages`; shared `react`/`react-dom` singletons with hooks/StrictMode/Context verified single-instance; mounted components follow `sessionKey` changes without remounting; pure-React projects install zero Vue, pure-Vue projects install zero React
|
|
42
|
+
- **CSP friendly** — no `eval` / `new Function` anywhere in loading paths
|
|
43
|
+
- **Error-code system (41 codes)** — CFG / DEV / BLD / MFU / CC segments, drift-checked against the code registry (see §11)
|
|
44
|
+
|
|
45
|
+
## 3. Installation & requirements
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pnpm add -D @fulgurjs/federation
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- Vite ≥ 5.1 (tested through 8.x; Vite 8 uses the rolldown dep-optimizer path automatically)
|
|
52
|
+
- Node ≥ 18
|
|
53
|
+
- Vue ≥ 3.2.0 and/or React `>=18.0.0 <20` — all three are **optional peers**; install only the framework you use
|
|
54
|
+
- Chrome 108+ (native top-level await)
|
|
55
|
+
|
|
56
|
+
`@fulgurjs/federation/runtime` and `@fulgurjs/federation/react` are **ESM-only** browser entries (no `require()`). The build-time main entry supports both ESM and CJS.
|
|
57
|
+
|
|
58
|
+
## 4. Project shape: two files per app
|
|
59
|
+
|
|
60
|
+
Every app root owns one `fulgurjs.config.ts` whose **default export is the federation options object itself**; `vite.config.ts` wires it once:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// vite.config.ts
|
|
64
|
+
import { defineConfig } from 'vite'
|
|
65
|
+
import react from '@vitejs/plugin-react' // or @vitejs/plugin-vue
|
|
66
|
+
import federation from '@fulgurjs/federation'
|
|
67
|
+
import fulgurjsConfig from './fulgurjs.config.ts'
|
|
68
|
+
|
|
69
|
+
export default defineConfig({ plugins: [react(), federation(fulgurjsConfig)] })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
An optional named export `hostPages = { pages, remotePrefixes }` is read by the CLI only; the same pure-data module feeds the browser adapter. Page-data modules must stay pure data (no framework/router/browser imports) so the CLI can evaluate them.
|
|
73
|
+
|
|
74
|
+
Removed in 5.0.0 and not coming back: the aggregate config chain (`root` + `apps[]`, the `/config` entry, `loadRepoConfig`, `federationOptionsForApp`, CLI `--app`), and the no-op options `remoteType`, `library`, `automaticAsyncBoundary`, `dataPrefetch`, `usedExports`, `ignoreUnusedSharedExports` — any of these now fail with `CFG-011` plus migration hints.
|
|
75
|
+
|
|
76
|
+
## 5. Quick start — React
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// remote: fulgurjs.config.ts
|
|
80
|
+
import type { FederationOptions } from '@fulgurjs/federation'
|
|
81
|
+
|
|
82
|
+
export default {
|
|
83
|
+
name: 'remote-react',
|
|
84
|
+
exposes: {
|
|
85
|
+
'./Button': './src/Button.tsx',
|
|
86
|
+
'./utils': './src/utils.ts',
|
|
87
|
+
'./pages/home': './src/pages/Home.tsx',
|
|
88
|
+
},
|
|
89
|
+
shared: {
|
|
90
|
+
react: { singleton: true },
|
|
91
|
+
'react-dom': { singleton: true },
|
|
92
|
+
},
|
|
93
|
+
} satisfies FederationOptions
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Host consumption — one import point, three usage shapes:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { remoteComponent, useLoadRemote, createReactHostPages, remoteSchema } from '@fulgurjs/federation/react'
|
|
100
|
+
import { pages, remotePrefixes } from './src/federation/pages.data'
|
|
101
|
+
|
|
102
|
+
// ① Component — create the factory at module top level (never inside render).
|
|
103
|
+
// Loading starts on first render. retry rebuilds the load attempt.
|
|
104
|
+
const RemoteButton = remoteComponent<{ label: string; onClick?: () => void }>('remote-react/Button', {
|
|
105
|
+
fallback: <p>Loading remote button…</p>,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// ② Plain module — generation-guarded hook
|
|
109
|
+
type Utils = { formatMoney(v: number, currency?: string): string }
|
|
110
|
+
|
|
111
|
+
// ③ Page table — same verb as Vue; render the component from your router
|
|
112
|
+
const hp = createReactHostPages({ pages, remotePrefixes, schema: remoteSchema })
|
|
113
|
+
const RemoteHome = hp.component('remote-react/pages/home')
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Data flow for host state (context):** the host provides context (`provideAppContext`, including a non-sensitive `sessionKey`) and then triggers its own re-render (React state / router). Mounted remote components and hooks observe the new `sessionKey` on that render and re-run their load lifecycle — A→B account switching works on the same mounted instance without remounting. `AppContext` is a plain snapshot: the plugin does not subscribe to it reactively; the host must trigger the render. `beforeLoad` (page tables) runs before every actual load attempt to refresh context. Logout: call `clearAppContext()` before unmounting authed UI.
|
|
117
|
+
|
|
118
|
+
Runnable examples: [`examples/react-host`](./examples/react-host) + [`examples/react-remote`](./examples/react-remote) (installed from the npm registry, no links). In-repo e2e fixtures: `fixtures/host-react` / `fixtures/remote-react`.
|
|
119
|
+
|
|
120
|
+
## 6. Quick start — Vue
|
|
121
|
+
|
|
122
|
+
Three integration paths (plain module / multi-page `createHostPages` / `setup` + `AppContext`) are documented in the Chinese README §快速开始;the API is identical to the tables below, imported from `@fulgurjs/federation/runtime`. The Vue-specific extras are `createHostPages` (with `keepAliveNames`) and the Vue `remoteComponent` options (`loadingComponent` / `errorComponent` / `delay`).
|
|
123
|
+
|
|
124
|
+
Cross-framework **plain TS modules** work in both directions (a Vue host can load a React remote's `utils` and vice versa) — direct Vue↔React component rendering in one tree is out of scope.
|
|
125
|
+
|
|
126
|
+
## 7. CLI
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npx fulgurjs init # scaffold fulgurjs.config.ts; never rewrites other files
|
|
130
|
+
npx fulgurjs explain # interpret the effective federation shape + load chain
|
|
131
|
+
npx fulgurjs check-pages \
|
|
132
|
+
--manifest remote-a=https://cdn.example.com/remote-a/fulgurjs-manifest.json \
|
|
133
|
+
--require-verified # page-table ↔ remote manifest contract check (CI gate)
|
|
134
|
+
npx fulgurjs doctor --site https://example.com # deployment health check
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`check-pages`: "confirmed missing" (error, non-zero) is distinct from "unverifiable" (source unreachable — honestly reported, non-zero with `--require-verified`); no fallback to stale local dist output.
|
|
138
|
+
|
|
139
|
+
## 8. API reference
|
|
140
|
+
|
|
141
|
+
### 8.1 `@fulgurjs/federation/react` — React entry
|
|
142
|
+
|
|
143
|
+
Re-exports the common runtime API of §8.2 **except** the Vue-only items (`remoteComponent` Vue options form, `createHostPages`, `keepAliveNames`), plus:
|
|
144
|
+
|
|
145
|
+
#### `remoteComponent<Props>(spec, options?)` → `ComponentType<Props & { ref? }>`
|
|
146
|
+
|
|
147
|
+
| Option | Type / default | Semantics |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `fallback` | `ReactNode`, default `null` | placeholder while this load is pending (distinct from the failure placeholder) |
|
|
150
|
+
| `error` | `ReactNode` or `(error, retry) => ReactNode`, default built-in Chinese placeholder | shown on load failure **or** subtree render error; the function receives the real error and a working retry |
|
|
151
|
+
| `retries` | `number` (integer 0–10), default follows `loadRemote` (2) | passthrough; invalid values throw at factory call |
|
|
152
|
+
| `timeout` | `number` (ms), default none | adapter-level wait cap for this component load; does **not** cancel the issued shared request; late results never overwrite the settled state and produce no unhandled rejections |
|
|
153
|
+
|
|
154
|
+
- Factory creation and page-table declaration have **zero load side effects**; loading starts on first render via `loadRemote` (container negotiation + optional setup/onSession)
|
|
155
|
+
- Not built on `React.lazy`: a lazy instance caches its failed promise and an error-boundary reset alone cannot recover; this implementation's retry rebuilds the load attempt (already-cached successful modules are not re-downloaded)
|
|
156
|
+
- Export validation: the default export must be a function/class/`memo`/`forwardRef` component; strings/numbers/empty namespaces fail explicitly
|
|
157
|
+
- `ref` passthrough works for `forwardRef` exports (verified on React 18 and 19)
|
|
158
|
+
- Render exceptions are caught by the built-in boundary and reported separately from network/export errors; the boundary does not catch event-handler or async-callback errors (React semantics)
|
|
159
|
+
- **Session switching:** mounted instances read the current `AppContext.sessionKey` on every render; when the host provides new context and re-renders, the load lifecycle re-runs for the new session on the same instance (no remount, no second React). Same-session re-renders do not reload
|
|
160
|
+
- The built-in placeholder shows error code + real cause + fix + a working 重试 (retry) button
|
|
161
|
+
|
|
162
|
+
#### `useLoadRemote<Module>(spec, options?)` → `{ data, error, loading, reload }`
|
|
163
|
+
|
|
164
|
+
- `data: Module | undefined`, `error: unknown` (always `undefined` when no error), `loading: boolean`, `reload: () => Promise<void>`
|
|
165
|
+
- Options: `shareScope`, `retries`, `fallbackModule` (explicit degradation — failures return the fallback value instead of writing `error`)
|
|
166
|
+
- Uniform state contract: first load, spec/option/session change and explicit `reload` all enter `data=undefined, error=undefined, loading=true`; the current attempt writes `data` on success or `error` on failure and clears `loading`; stale attempts never write
|
|
167
|
+
- Generation guards: fast A→B switching, late slow responses, consecutive reloads, unmount-during-flight and StrictMode double effects can only write from the latest valid request
|
|
168
|
+
- `reload` re-runs the lifecycle (onSession dedup by generation) but never re-downloads cached successful modules; resolves normally (failures surface in `error`, never an unhandled rejection)
|
|
169
|
+
- Session-aware: re-runs when `sessionKey` changes; same-session re-renders don't
|
|
170
|
+
|
|
171
|
+
#### `RemoteErrorBoundary`
|
|
172
|
+
|
|
173
|
+
Standalone page-level boundary. Props: `children`, `fallback` (node or `({ error, reset }) => ReactNode`), `onError(error, info)`, `resetKeys` (reset when any entry changes). `reset` only clears boundary state; if a child holds a failed cache (e.g. your own `React.lazy`), rebuilding the attempt is the caller's job — `remoteComponent`'s built-in retry already does both. It never sees errors already consumed by `remoteComponent`'s inner boundary.
|
|
174
|
+
|
|
175
|
+
#### `createReactHostPages(options)` → `{ pages, resolve(path), component(spec) }`
|
|
176
|
+
|
|
177
|
+
- Data options (identical to Vue): `pages`, `remotePrefixes`, `deriveSpec`, `schema`, `strict`, `base`
|
|
178
|
+
- Display options (same semantics as `remoteComponent`): `fallback`, `error`, `retries`, `timeout`; plus `beforeLoad: () => void | Promise<void>` — runs before **every actual load attempt** (including retries) so the host can refresh context; never at table creation
|
|
179
|
+
- `component<P>(spec)` returns a React component type; the component cache is keyed by spec + login generation (rebuilt only on a new non-empty `sessionKey`; logout → `undefined` does not rebuild). Module-level caching of `component(spec)` results is supported — mounted pages still follow session changes
|
|
180
|
+
- No `keepAliveNames` / no keep-alive promise (Vue-specific); routing is not a runtime dependency — render `component(spec)` output from your router (React Router examples in `examples/react-host`; route params reach remote pages as props)
|
|
181
|
+
- Cross-framework Context: host and remote get the **same Context object** through the same expose instance; the plugin does not auto-bridge arbitrary React Contexts
|
|
182
|
+
|
|
183
|
+
### 8.2 Runtime API — `@fulgurjs/federation/runtime` (Vue apps) and common functions on `/react`
|
|
184
|
+
|
|
185
|
+
| Function | Signature | Semantics |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| `loadRemote` | `<T = Record<string, any>>(spec: string, opts?: { shareScope?: string; retries?: number; fallbackModule?: () => any }) => Promise<T>` | spec = `<remote>/<expose-key-without-./>`. Goes through container negotiation and the optional setup/onSession lifecycle. Modules cached per `remote@scope#module`; failures uncached and retryable. `fallbackModule` returns your module on failure **and** still emits the error event |
|
|
188
|
+
| `loadShare` | `(name: string, opts?: LoadShareOptions) => Promise<any>` | shared-deps negotiation: `requiredVersion` (semver or `false`), `singleton`, `strictVersion`, `shareKey`, `shareScope`, `fallback: () => Promise<any>`. Highest satisfying version wins; loaded versions never replaced; singleton keeps one instance (warns MFU-010 when the reused version doesn't satisfy `requiredVersion`; throws MFU-003 with `strictVersion`) |
|
|
189
|
+
| `initSharing` | `(scopeName?: string) => ShareScopeMap` (default `'default'`) | creates/returns the share scope map (usually called for you by the injected init) |
|
|
190
|
+
| `registerShare` | `(scopeName, name, version, get: () => Promise<any>, opts?: { from?, eager?, loaded? }) => void` | register a provided shared module at runtime; first registration of a version wins |
|
|
191
|
+
| `registerRemote` / `registerRemotes` | `(config: RemoteConfig) => void` / `((list: RemoteConfig[]) => void)` | runtime registration: `{ name, entry, shareScope?, timeout?, retries?, fallback?, breaker?, promise? }`. Promise-based remotes pass `promise: () => Promise<container>` |
|
|
192
|
+
| `registerPlugins` | `(plugins: RuntimePlugin[]) => void` | register runtime plugins; each `init(hooks)` may set `resolveShare`, `beforeLoadRemote({ remote, module })`, `afterLoadRemote({ remote, module, module_ns })`, `onRemoteError({ remote, error })`. Observer-hook failures warn but never break loading |
|
|
193
|
+
| `preloadRemote` | `(spec: string, opts?: { mode?: 'preload' \| 'prefetch' }) => Promise<void>` | manifest-driven preload of entry + expose chunks + CSS; `'prefetch'` = low priority. No lifecycle side effects (setup/onSession are NOT run) |
|
|
194
|
+
| `getContainer` | `(name: string) => Promise<any>` | acquire the initialized container |
|
|
195
|
+
| `getRuntime` | `() => FgRuntime` | the page-level runtime singleton (`globalThis.__FULGURJS_RUNTIME__`) |
|
|
196
|
+
| `parseSpec` | `(spec: string) => { remote, module }` | synchronous spec parsing |
|
|
197
|
+
| `shareScopeMap` | `ShareScopeMap` | live registry (debug surface: `window.__FULGURJS_SCOPE__`) |
|
|
198
|
+
| `unwrapDefault` | `(ns: any) => any` | ESM/CJS default-interop helper |
|
|
199
|
+
| `version` | `string` | plugin/runtime version |
|
|
200
|
+
| `clearSessionState` | `() => void` | invalidate all remotes' session signals and onSession dedup state (called by `clearAppContext`) |
|
|
201
|
+
|
|
202
|
+
Remote-registration config fields: `timeout` (ms, default 15000 — ends the caller's wait, never cancels the issued import), `retries` (0–10, default 2), `fallback: string[]` (spare entry URLs), `breaker: { threshold, resetMs }` (default 5 / 30s).
|
|
203
|
+
|
|
204
|
+
### 8.3 Plugin options — `federation(options)`
|
|
205
|
+
|
|
206
|
+
| Option | Type / default | Notes |
|
|
207
|
+
|---|---|---|
|
|
208
|
+
| `name` | `string`, **required** | container name; unique per page; `/^[a-zA-Z][\w.-]*$/` |
|
|
209
|
+
| `exposes` | `Record<string, string \| { import, name? }>` | key normalized to `./Key`; stable chunk name optional |
|
|
210
|
+
| `remotes` | `Record<string, string \| RemoteEntryConfig \| (() => Promise<any>)>` | string = url or `name@url`; object = `{ external?, dev?, prod?, timeout?, retries?, fallback?, breaker?, shareScope? }`; function = promise-based remote (runtime-register instead) |
|
|
211
|
+
| `shared` | `string[]` or `Record<string, string \| SharedHint>` | see below |
|
|
212
|
+
| `setup` | `string` | module path; must default-export `setup(context)`, optional named `onSession(context)` |
|
|
213
|
+
| `shareScope` | `string`, default `'default'` | default scope for provides |
|
|
214
|
+
| `filename` | `string`, default `'fulgurjs-remoteEntry.js'` | fixed remoteEntry filename |
|
|
215
|
+
| `manifest` | `boolean`, default `true` | emit `fulgurjs-manifest.json` |
|
|
216
|
+
| `dts` | `boolean \| { dir?, mode?: 'source' \| 'shim' }`, default `true` | dev type generation (see §8.6) |
|
|
217
|
+
| `devSharedSelf` | `boolean`, default inferred | pure remotes & dual-role apps: `true` (dev shared rewriting); pure hosts: `false` |
|
|
218
|
+
| `devCorsOrigins` | `'*'` or `string[]` | dev endpoints + server.cors share the policy; explicit user `server.cors` wins |
|
|
219
|
+
| `devFsRoot` | `boolean`, default `true` | dev manifest carries local fsRoot for type direct-connect; `false` → host falls back to `any` stubs |
|
|
220
|
+
| `runtimePlugins` | `string[]` | modules default-exporting a `RuntimePlugin` |
|
|
221
|
+
|
|
222
|
+
`SharedHint` fields: `import` (local specifier or `false` = pure consumer), `packageName` (infer `requiredVersion` from a different package name), `requiredVersion` (semver or `false`), `singleton`, `strictVersion` (default: `true` when a local fallback exists and not singleton, webpack-aligned), `shareKey`, `shareScope`, `eager`, `version`.
|
|
223
|
+
|
|
224
|
+
### 8.4 Lifecycle — `setup` / `onSession`
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
// federation({ setup: './src/fulgurjs/setup.ts' })
|
|
228
|
+
export default async function setup(ctx: { appContext: Record<string, any>; sessionKey?: string; signal: AbortSignal }) {
|
|
229
|
+
// app-level: once per app, before the first business module is returned
|
|
230
|
+
}
|
|
231
|
+
export async function onSession(ctx: { appContext: any; sessionKey: string; signal: AbortSignal }) {
|
|
232
|
+
// session-level: once per host sessionKey (login generation); re-login re-runs, logout invalidates
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
- Failures reject the triggering `loadRemote` (MFU-011/012) and are retryable; already-succeeded stages are not re-run
|
|
237
|
+
- `signal` aborts on logout/session change — check `signal.aborted` before writing async results
|
|
238
|
+
- `preloadRemote` / `getContainer` never trigger the lifecycle
|
|
239
|
+
- Remote declares `onSession` → the host **must** provide a non-empty `sessionKey` (MFU-013); never use a token as sessionKey
|
|
240
|
+
- No-setup remotes (plain public components) load normally without any context
|
|
241
|
+
|
|
242
|
+
### 8.5 AppContext — cross-app values
|
|
243
|
+
|
|
244
|
+
- `provideAppContext(partial)` — merge-write the page-level singleton (idempotent; later writes win). Host bridge calls it after login and re-calls on account change; then triggers its own re-render
|
|
245
|
+
- `getAppContext()` — read the snapshot (`CC-002` if loaded outside the host federation)
|
|
246
|
+
- `requireAppContext(...keys)` — validated read; missing keys → `CC-001` with got/expected/example
|
|
247
|
+
- `clearAppContext()` — delete context + invalidate session signals/dedup (module and share caches, and completed app-level setup, are preserved). Logout must call it before unmounting authed UI
|
|
248
|
+
- Standard fields: `user`, `getToken()`, `store` (host pinia), `hostApp` (host Vue app), `locale`, `events`, `sessionKey` — plus arbitrary extension keys. Transport snapshot + function references; not reactive
|
|
249
|
+
|
|
250
|
+
### 8.6 Dev types (dual-track)
|
|
251
|
+
|
|
252
|
+
- Zero config: ambient declarations per expose — imports resolve, exports typed `any`; setup entry never generates declarations
|
|
253
|
+
- Precise track: add `"paths": { "<remote>/*": ["<typesDir>/<remote>.d/*"] }` to any app `tsconfig*.json` (except `tsconfig.node.json`); imports then resolve through forwarder modules to **source-level types** (wrong props/arguments fail compilation). Remotes covered by paths automatically skip their loose declaration to avoid shadowing
|
|
254
|
+
- `devFsRoot: false` or unreachable source: degrades to resolvable `any` declarations and cleans stale precise-track files (precise → degrade → restore cycles compile cleanly)
|
|
255
|
+
- `dts: false` stops generation without deleting existing output; `dts.dir` relocates; `mode: 'shim'` gives loose IDE-clean placeholders
|
|
256
|
+
- Precise track requires the host and remote to share a filesystem (same-machine dev); verified bounds: React 18.0.0–19.x with matching @types
|
|
257
|
+
|
|
258
|
+
## 9. Artifacts, endpoints & caching
|
|
259
|
+
|
|
260
|
+
| Artifact | Cache policy |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `fulgurjs-remoteEntry.js` (fixed filename, content changes every build) | **`no-cache`** |
|
|
263
|
+
| content-hashed chunks / CSS | `immutable` long cache |
|
|
264
|
+
| `fulgurjs-manifest.json` | `no-cache` (consumed by `preloadRemote` / `check-pages` / `doctor`) |
|
|
265
|
+
| dev endpoints `/@fulgurjs-entry.js` / `/@fulgurjs-manifest.json` | `no-cache`, CORS per `devCorsOrigins` |
|
|
266
|
+
|
|
267
|
+
Lazy-loading measurement layers: ① nothing until first render of a remote component/page; ② container entry + shared metadata on first load; ③ the expose chunk; ④ shared-dependency bodies (negotiated, possibly already loaded). `preloadRemote(spec)` fetches ②③④ without executing lifecycle code. Verify with real network records — count URLs and transferred bytes per layer, cold cache vs revisit.
|
|
268
|
+
|
|
269
|
+
## 10. Debugging surfaces
|
|
270
|
+
|
|
271
|
+
- `window.__FULGURJS_SCOPE__` — live share-scope registry
|
|
272
|
+
- `window.__FULGURJS_INFO__` — per-remote status/latency/errors + `errors` log
|
|
273
|
+
- `DEBUG=fulgurjs:*` — controlled pipeline diagnostics (off by default)
|
|
274
|
+
- Runtime diagnostics are emitted in Chinese by design (language policy); codes are stable identifiers listed below
|
|
275
|
+
|
|
276
|
+
## 11. Error codes (41)
|
|
277
|
+
|
|
278
|
+
| Segment | Code | Meaning |
|
|
279
|
+
|---|---|---|
|
|
280
|
+
| CFG | `CFG-001` | name missing or invalid |
|
|
281
|
+
| | `CFG-002` | exposes shape invalid |
|
|
282
|
+
| | `CFG-003` | remotes shape invalid / illegal key characters |
|
|
283
|
+
| | `CFG-004` | shared shape invalid |
|
|
284
|
+
| | `CFG-005` | remotes key collides with a shared key |
|
|
285
|
+
| | `CFG-006` | island config (neither provides nor consumes) |
|
|
286
|
+
| | `CFG-007` | `name@` prefix misuse in object-form remotes |
|
|
287
|
+
| | `CFG-008` | shared illegal combo (eager+import:false / duplicate shareKey) |
|
|
288
|
+
| | `CFG-009` | remote runtime params invalid (timeout/retries/breaker) |
|
|
289
|
+
| | `CFG-010` | devCorsOrigins invalid |
|
|
290
|
+
| | `CFG-011` | removed no-op option (any value errors with migration hints) |
|
|
291
|
+
| | `CFG-012` | setup config invalid / reserved expose key squatted |
|
|
292
|
+
| DEV | `DEV-001` | remote dev server unreachable (manifest fetch failed) |
|
|
293
|
+
| | `DEV-002` | remote dev manifest empty or unrecognized |
|
|
294
|
+
| | `DEV-004` | known UMD-only dep missing from optimizeDeps.include |
|
|
295
|
+
| | `DEV-005` | remotes dev URL port not listening |
|
|
296
|
+
| | `DEV-006` | host/remote plugin version mismatch |
|
|
297
|
+
| | `DEV-009` | facade/virtual module 404 (.vite cache drift — clear and restart) |
|
|
298
|
+
| | `DEV-010` | dev cold-start pre-bundle window notice (transient) |
|
|
299
|
+
| | `DEV-011` | non-loopback host + wildcard dev CORS reminder |
|
|
300
|
+
| | `DEV-012` | non-loopback host + fsRoot disclosure reminder |
|
|
301
|
+
| BLD | `BLD-001` | expose source resolution failed |
|
|
302
|
+
| | `BLD-002` | build target below es2022 |
|
|
303
|
+
| | `BLD-003` | expose target declares required props (documented checklist) |
|
|
304
|
+
| | `BLD-006` | array-form output prevents automatic facade chunk isolation |
|
|
305
|
+
| MFU | `MFU-001` | remote container/module load failure (network / timeout / retries exhausted / breaker) |
|
|
306
|
+
| | `MFU-002` | remoteEntry self-reported name mismatch |
|
|
307
|
+
| | `MFU-003` | strictVersion requirement not satisfied |
|
|
308
|
+
| | `MFU-004` | shared module missing with no local fallback |
|
|
309
|
+
| | `MFU-005` | same container re-initialized with a different share scope |
|
|
310
|
+
| | `MFU-006` | requested module not exposed by the remote |
|
|
311
|
+
| | `MFU-007` | preload failed (non-blocking) |
|
|
312
|
+
| | `MFU-008` | unknown remote |
|
|
313
|
+
| | `MFU-009` | loaded module has no exports at all |
|
|
314
|
+
| | `MFU-010` | reused singleton version doesn't satisfy the consumer requirement (warn-once) |
|
|
315
|
+
| | `MFU-011` | setup entry export shape invalid |
|
|
316
|
+
| | `MFU-012` | setup/onSession threw (retryable; only the failed stage resets) |
|
|
317
|
+
| | `MFU-013` | onSession declared but host sessionKey missing |
|
|
318
|
+
| | `MFU-014` | setup/onSession synchronously re-loading the same remote (deadlock guard) |
|
|
319
|
+
| CC | `CC-001` | AppContext required key missing (got/expected/example) |
|
|
320
|
+
| | `CC-002` | runtime singleton unavailable (standalone remote page) |
|
|
321
|
+
|
|
322
|
+
## 12. Boundaries (explicitly not supported)
|
|
323
|
+
|
|
324
|
+
- Support covers **browser-client** federation for Vue 3 and React 18–19. Not supported: SSR, React Server Components, Next.js full-stack, React Native, Node-side remote loading, direct Vue↔React component rendering in one tree, JS sandbox, CSS isolation
|
|
325
|
+
- React side does not promise component keep-alive (`keepAliveNames` is Vue-only); re-opened pages still reuse downloaded modules
|
|
326
|
+
- Cross-origin Fast Refresh: remote React components update via the remote dev server's HMR push; after a cold start the first round often needs a host refresh — component-state retention across the federation boundary is not promised
|
|
327
|
+
- Not compatible with originjs `virtual:__federation__` legacy imports
|
|
328
|
+
- No browser DevTools extension (the `window.__FULGURJS_*` surfaces serve debugging)
|
|
329
|
+
|
|
330
|
+
## 13. Documentation & examples
|
|
331
|
+
|
|
332
|
+
- [Migration guide (Chinese)](https://github.com/chenmingye/fulgurjs-federation/blob/master/docs/迁移指南.md) — a real qiankun → federation migration (seven steps + acceptance checklist)
|
|
333
|
+
- [webpack MF comparison & gaps (Chinese)](https://github.com/chenmingye/fulgurjs-federation/blob/master/docs/webpack-mf-对照与缺口.md)
|
|
334
|
+
- [Sandbox boundary audit (Chinese)](https://github.com/chenmingye/fulgurjs-federation/blob/master/docs/沙箱边界审计.md)
|
|
335
|
+
- [`DESIGN.md`](https://github.com/chenmingye/fulgurjs-federation/blob/master/DESIGN.md) — architecture and alignment tables
|
|
336
|
+
- Examples: [`examples/react-host`](./examples/react-host) + [`examples/react-remote`](./examples/react-remote) (React, registry-installable) · [`examples/host`](./examples/host) + [`examples/remote-a`](./examples/remote-a) (Vue config samples)
|
|
337
|
+
|
|
338
|
+
## 14. Development & testing
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
pnpm --dir packages/plugin install && pnpm --dir packages/plugin build
|
|
342
|
+
for app in fixtures/host-vue fixtures/remote-a fixtures/remote-b fixtures/remote-react fixtures/host-react e2e; do pnpm --dir "$app" install; done
|
|
343
|
+
|
|
344
|
+
pnpm test:unit # full unit suite
|
|
345
|
+
pnpm test:dev # dev e2e (Vue)
|
|
346
|
+
pnpm test:prod # prod e2e (NGINX, isolated instance)
|
|
347
|
+
node e2e/scripts/react-types-check.mjs # dual-track dev types + negative matrix
|
|
348
|
+
node e2e/scripts/react-negative-check.mjs # N08/N10/N11 negative checks
|
|
349
|
+
bash e2e/scripts/prod-setup.sh # build all fixtures + isolated NGINX
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
CI: unit + dual typecheck + build gates (gzip, error-code consistency); e2e Vue+React suites across Vite 6.4.3 / 7.3.6 / 8.3.0; scheduled Vite 5.1 floor job; prod-e2e; tarball consumer smoke (Vue + React).
|
|
353
|
+
|
|
354
|
+
## License
|
|
355
|
+
|
|
356
|
+
[MIT](./LICENSE) © chenmingye (Jason)
|
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# @fulgurjs/federation
|
|
2
2
|
|
|
3
|
+
> 简体中文 | [English](./README.en.md)
|
|
4
|
+
|
|
3
5
|
> **fulgurjs** — 拉丁语「闪电 · 辉光」。
|
|
4
|
-
> 一个把 Vite 模块联邦做到开箱即用的插件:**一套配置,dev / prod 双引擎,语义对齐 Webpack Module Federation
|
|
6
|
+
> 一个把 Vite 模块联邦做到开箱即用的插件:**一套配置,dev / prod 双引擎,语义对齐 Webpack Module Federation**,Vue 3 与 React 18/19 双生态浏览器端支持。
|
|
5
7
|
|
|
6
8
|
  
|
|
7
9
|
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
- **宿主页面适配器(可选)**:`createHostPages({ pages, remotePrefixes, ... })`——一份页面表供宿主路由与布局共用;URL 解析(含 base 剥离)、最长前缀远程归属、`definePages` R1–R5 校验、异步组件缓存(会话切换自动重建)、骨架屏/错误占位、保活名称内置
|
|
37
39
|
- **跨应用传值与方法引用**:`@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 会话同步承担,不依赖页面刷新)
|
|
38
40
|
- **Vue 直渲染**:`remoteComponent('remote/X')`(`@fulgurjs/federation/runtime` 导出)——`defineAsyncComponent + loadRemote` 的标准封装,加载失败显式错误占位(错误码+根因+修法),runtime.js 零框架依赖零体积增量
|
|
41
|
+
- **React 完整支持(浏览器端)**:`@fulgurjs/federation/react` 独立入口——`remoteComponent`(含 Suspense 占位/错误占位/重试,不用 React.lazy 的失败缓存陷阱)、`useLoadRemote`(代次守卫的模块 hook)、`RemoteErrorBoundary`(页面级兜底)、`createReactHostPages`(与 Vue 同源页面表与 R1–R5 校验);共享 `react`/`react-dom` singleton 协商,Hooks/StrictMode/Context 跨端同实例(dev 预构建外部化 + prod CJS 垫片自动处理 `react/jsx-runtime`、`react-dom/client` 子路径);纯 React 项目零 Vue 依赖、纯 Vue 项目零 React 依赖
|
|
39
42
|
- **CSP 友好**:原生 ESM 加载路径全程无 `eval` / `new Function`,可在严格 CSP(无 `unsafe-eval`)下运行
|
|
40
43
|
- **全链路错误码体系(41 码)**:CFG/DEV/BLD/MFU/CC 五段 + 手册 §6 码表防漂移校验
|
|
41
44
|
|
|
@@ -45,7 +48,61 @@
|
|
|
45
48
|
pnpm add -D @fulgurjs/federation
|
|
46
49
|
```
|
|
47
50
|
|
|
48
|
-
要求:Vite ≥ 5.1(实测至 8.x)、Node ≥ 18、Vue 3
|
|
51
|
+
要求:Vite ≥ 5.1(实测至 8.x)、Node ≥ 18、Vue 3 和/或 React 18–19(均为可选 peer——按所用框架安装)、浏览器 Chrome 108+(TLA 原生支持)。
|
|
52
|
+
|
|
53
|
+
## 快速开始(React 应用)
|
|
54
|
+
|
|
55
|
+
React 宿主与远程使用同一套「每应用两份文件」的配置形态,唯一区别是浏览器导入点:`@fulgurjs/federation/react`(同时提供通用运行时 API 与 React 适配 API,纯 React 项目不需要安装 Vue)。
|
|
56
|
+
|
|
57
|
+
远程(`fulgurjs.config.ts`,默认导出直接是联邦选项):
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import type { FederationOptions } from '@fulgurjs/federation'
|
|
61
|
+
|
|
62
|
+
export default {
|
|
63
|
+
name: 'remote-react',
|
|
64
|
+
exposes: {
|
|
65
|
+
'./Button': './src/Button.tsx',
|
|
66
|
+
'./utils': './src/utils.ts',
|
|
67
|
+
'./pages/home': './src/pages/Home.tsx',
|
|
68
|
+
},
|
|
69
|
+
shared: {
|
|
70
|
+
react: { singleton: true },
|
|
71
|
+
'react-dom': { singleton: true },
|
|
72
|
+
},
|
|
73
|
+
} satisfies FederationOptions
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// vite.config.ts —— React 插件照常在前,联邦只加一行
|
|
78
|
+
import { defineConfig } from 'vite'
|
|
79
|
+
import react from '@vitejs/plugin-react'
|
|
80
|
+
import federation from '@fulgurjs/federation'
|
|
81
|
+
import fulgurjsConfig from './fulgurjs.config'
|
|
82
|
+
|
|
83
|
+
export default defineConfig({ plugins: [react(), federation(fulgurjsConfig)] })
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
宿主消费(普通组件 / 普通模块 / 页面表三选一或混用):
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { remoteComponent, useLoadRemote, createReactHostPages, remoteSchema } from '@fulgurjs/federation/react'
|
|
90
|
+
import { pages, remotePrefixes } from './src/federation/pages.data' // 纯数据模块(CLI 与浏览器共用)
|
|
91
|
+
|
|
92
|
+
// ① 普通组件:工厂放模块顶层(不能在 render 内重复调用工厂);首次渲染才加载
|
|
93
|
+
const RemoteButton = remoteComponent<{ label: string; onClick?: () => void }>('remote-react/Button', {
|
|
94
|
+
fallback: <p>正在加载远程按钮…</p>,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// ② 普通模块:useLoadRemote(data/error/loading/reload;错误无默认占位,由宿主决定 UI)
|
|
98
|
+
type Utils = { formatMoney(v: number, currency?: string): string }
|
|
99
|
+
|
|
100
|
+
// ③ 页面表:与 Vue 相同动词 component(spec),路由层渲染
|
|
101
|
+
const hp = createReactHostPages({ pages, remotePrefixes, schema: remoteSchema })
|
|
102
|
+
const RemoteHome = hp.component('remote-react/pages/home')
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
完整可复制工程见 [`examples/react-host`](./examples/react-host) 与 [`examples/react-remote`](./examples/react-remote);API 精确语义(含 timeout/retry/StrictMode/Context/错误恢复)见 [§8.1 React 适配 API](#81-react-适配-api--fulgurjsfederationreact)。
|
|
49
106
|
|
|
50
107
|
## 快速开始:三条接入路径
|
|
51
108
|
|
|
@@ -410,7 +467,7 @@ import { loadRemote } from '@fulgurjs/federation/runtime'
|
|
|
410
467
|
|
|
411
468
|
#### 函数总表
|
|
412
469
|
|
|
413
|
-
> 下表全部函数与 `definePages` / `remoteSchema` / `provideAppContext` 等 context 函数 / `remoteComponent` / `createHostPages`
|
|
470
|
+
> 下表全部函数与 `definePages` / `remoteSchema` / `provideAppContext` 等 context 函数 / `remoteComponent` / `createHostPages` 都从 Vue 入口 `@fulgurjs/federation/runtime` 导入(见 §2);旧入口已删除。**React 浏览器应用请使用 `@fulgurjs/federation/react`**(通用函数同名提供 + §8.1 的 React 适配 API;不含本表的 Vue 专属项 `remoteComponent` Vue 形态 / `createHostPages` / `keepAliveNames`)。
|
|
414
471
|
|
|
415
472
|
> **TS 提示**:`@fulgurjs/federation/runtime` 的类型随包发布,由包的 `exports` 和 `typesVersions` 直接解析;不需要 `client` 类型垫片。dev 启动时插件仅在类型目录(默认 `src/fulgurjs/types/`)生成远程 exposes 的类型声明。
|
|
416
473
|
|
|
@@ -638,6 +695,58 @@ const FederatedAmisForm = remoteComponent('demo-host/AmisFormRouterPage', {
|
|
|
638
695
|
- `vue` 为**可选 peerDependency**(`peerDependenciesMeta.optional`):只使用包根(Vite 插件)时无需安装;应用使用 `/runtime` 时需要安装 Vue,因为该入口导出 `remoteComponent`。内部 `runtime.js` 仍不导入 Vue,体积零增量;
|
|
639
696
|
- 运行时实例经 `globalThis.__FULGURJS_RUNTIME__` 页面级单例复用,与 `@fulgurjs/federation/runtime` 的导入殊途同归,无需额外接线。
|
|
640
697
|
|
|
698
|
+
### 8.1 React 适配 API — `@fulgurjs/federation/react`
|
|
699
|
+
|
|
700
|
+
React 浏览器应用唯一导入点:同时导出通用运行时 API(`loadRemote`/`preloadRemote`/`provideAppContext`/`definePages`/`remoteSchema` 等,与 `/runtime` 的通用面一致)与下列 React 适配 API。**不包含** Vue 的 `remoteComponent` 选项形态、`createHostPages`、`keepAliveNames`。
|
|
701
|
+
|
|
702
|
+
#### `remoteComponent<Props>(spec, options?)`
|
|
703
|
+
|
|
704
|
+
返回可渲染的 React 组件类型(`Props` 约束 JSX 使用;类型参数是编译期合同,不是运行时校验)。工厂与页面表声明**零加载副作用**;首次渲染才 `loadRemote`(经容器协商与可选 setup/onSession),内部自带 pending 占位、错误占位与错误边界——最简用法无需手写 Suspense/`React.lazy`。**不用 `React.lazy`**:lazy 实例缓存失败的 Promise,仅重置错误边界无法恢复;本实现的 retry 会重建加载尝试(已成功的模块经运行时缓存不会重复下载)。
|
|
705
|
+
|
|
706
|
+
| 选项 | 类型与默认 | 语义 |
|
|
707
|
+
|---|---|---|
|
|
708
|
+
| `fallback` | `ReactNode`,默认 `null` | 本次加载 pending 时的占位(区别于失败占位) |
|
|
709
|
+
| `error` | `ReactNode` 或 `(error, retry) => ReactNode`,默认内置中文占位 | 加载失败或子树渲染错误的展示;渲染函数收到真实错误与可用的重试 |
|
|
710
|
+
| `retries` | `number`,沿用 `loadRemote` 默认(2) | 透传重试次数(0–10 整数,非法值工厂调用期抛错) |
|
|
711
|
+
| `timeout` | `number`(ms),默认不设适配层超时 | 本次组件加载等待上限;超时只结束本次等待,**不取消**已发出的共享请求;迟到的成功/失败不覆盖终态、不产生未处理 rejection |
|
|
712
|
+
|
|
713
|
+
- 组件导出校验:默认导出(或模块本身)必须是函数组件 / class / `memo` / `forwardRef` 等合法组件类型;字符串、数字、空命名空间显式报错(不渲染空白成功页)
|
|
714
|
+
- `ref` 透传:`forwardRef` 导出可正确接收 ref(React 18/19 实测);普通函数组件传 ref 遵循 React 标准行为
|
|
715
|
+
- 渲染期异常由内置边界捕获并与网络/导出错误**分开记录与展示**(文案区分「加载失败」与「渲染出错」);ErrorBoundary 不捕获事件处理器与任意异步回调异常——这两类错误遵循 React 自身语义
|
|
716
|
+
- 内置默认错误占位包含:错误码(FgError 的 `code`,无码渲染错误显示 `UNKNOWN`)、真实根因 message、可执行修法与「重试」按钮
|
|
717
|
+
- 失败恢复真实穿透浏览器 ESM 失败缓存:运行时对入口 URL 与容器 expose loader 均在失败后的重试上变更 URL(`fulgurjs_retry=N`),服务恢复后点击重试可真实重新拉取(不是只在 mock 下可恢复)
|
|
718
|
+
|
|
719
|
+
#### `useLoadRemote<Module>(spec, options?)`
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
const { data, error, loading, reload } = useLoadRemote<Utils>('remote-react/utils')
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
- 返回 `{ data: Module | undefined, error: unknown, loading: boolean, reload: () => Promise<void> }`;`error` 无错误时恒为 `undefined`
|
|
726
|
+
- `options`:`shareScope` / `retries` / `fallbackModule`(透传 `loadRemote`;配置 `fallbackModule` 是显式声明的行为——失败返回兜底值而非写 error)
|
|
727
|
+
- 按字段比较依赖(调用方每次 render 新建 options 对象不会无限重载);spec/选项变化时清理旧数据进入新请求
|
|
728
|
+
- 每轮 effect 与 `reload` 有独立代次:快速 A→B、慢请求晚返回、连续 reload、卸载后返回、StrictMode 双 effect 都只允许最新有效请求写状态;不宣称重复 effect 从未发生(运行时缓存去重网络与生命周期)
|
|
729
|
+
- `reload` 重走生命周期与失败重试,但已成功缓存的模块不会重新下载;`Promise<void>` 正常结束(按钮 `onClick` 调用不产生未处理拒绝)
|
|
730
|
+
- `AppContext` 不是 React 状态订阅:宿主读到新的非空 `sessionKey` 时由**宿主自身状态/路由**触发重新渲染(`createHostPages` 的组件缓存会在新登录代次自动重建,触发新代次 `onSession`)
|
|
731
|
+
|
|
732
|
+
#### `RemoteErrorBoundary`
|
|
733
|
+
|
|
734
|
+
独立页面级兜底边界。props:`children`、`fallback`(节点或 `({ error, reset }) => ReactNode`)、`onError(error, info)`、`resetKeys`(任一变化重置边界状态,受控重试常用形态 `resetKeys={[retryEpoch]}`)。`reset` 只重置边界状态;子树若持有失败缓存(如外部 `React.lazy`)还需由调用方重建加载尝试——插件自带 `remoteComponent` 的重试已完成两者。内置 `remoteComponent` 的错误边界已消费自身错误,外层 `RemoteErrorBoundary` 看不到内层已处理的异常;想改某个远程组件的占位请用该组件自己的 `error` 选项。
|
|
735
|
+
|
|
736
|
+
#### `createReactHostPages(options)`
|
|
737
|
+
|
|
738
|
+
与 Vue 侧共用同一份页面表数据与 `definePages` R1–R5 校验(5.1.0 起纯解析提取为共用内核);返回 `{ pages, resolve(path), component(spec) }`——`component(spec)` 返回 React 组件类型,路由层用 JSX / `createElement` 渲染即可(不提供 `.element()` 同义入口)。
|
|
739
|
+
|
|
740
|
+
- `options` 数据项:`pages / remotePrefixes / deriveSpec / schema / strict / base`(语义与 Vue 完全一致);React 展示项:`fallback / error / retries / timeout`(语义与 `remoteComponent` 一致)+ `beforeLoad`(每次实际加载尝试前执行,供宿主刷新 context;页面表创建时不执行)
|
|
741
|
+
- `resolve` 保持 base 剥离、最长前缀、参数解码(坏 `%` 序列只让该次匹配失败)、query/hash、无匹配返回 `null`
|
|
742
|
+
- 组件缓存按 spec 与登录代次复用;**仅新的非空 `sessionKey` 到来时重建**(登出变 `undefined` 不重建——与 Vue 侧同语义);换账号后重新加载触发新代次 `onSession`
|
|
743
|
+
- React 侧不提供 `keepAliveNames`(不承诺组件保活);路由不是插件的运行时依赖——示例用 React Router 7(`path` 在路由表声明、`element` 渲染 `component(spec)` 产物;带参数路由经 `useParams`/`useSearchParams` 传给远程页面 props)
|
|
744
|
+
- 跨框架共享 Context:宿主与远程消费方经**同一 expose 实例**拿到同一 Context 对象(如远程 `expose './theme-context'` 导出 `createContext` 实例,宿主 `useLoadRemote` 取得后作 Provider,远程组件 `useContext` 读到宿主值);插件不自动桥接任意 React Context——必须显式共享该对象
|
|
745
|
+
|
|
746
|
+
#### React 的 dev 类型
|
|
747
|
+
|
|
748
|
+
`@fulgurjs/federation/react` 的 `.tsx`/`.ts` expose 与 Vue 共用同一套 dev 类型生成(目录、`dts:false`、`dts.dir`、setup 过滤、`devFsRoot:false` 降级全部一致),并新增**双轨**形态:零配置时生成可解析的宽松声明(导出为 `any`);在宿主任一 `tsconfig*.json` 配置一段 `"paths": { "<remote>/*": ["<types目录>/<remote>.d/*"] }` 后,同形态导入即解析到转发模块获得**源码级类型**(props/函数签名精确,错误 props/参数编译失败)——配置了 paths 的远程会自动跳过同名宽松声明避免遮蔽,启用说明见生成目录内 `_paths.d.ts`。
|
|
749
|
+
|
|
641
750
|
### 9. `AppContext` — 跨应用传值与方法引用(`@fulgurjs/federation/runtime`)
|
|
642
751
|
|
|
643
752
|
宿主向子应用传值、子应用向宿主反向注册方法,一律走这条一等公民通道(对标乾坤 `props`,但带类型与错误契约)——不再各自挂 `window.*` 裸口子。
|
|
@@ -996,7 +1105,10 @@ const Panel = await loadRemote('shop/Panel', {
|
|
|
996
1105
|
|
|
997
1106
|
## 边界(明确不支持)
|
|
998
1107
|
|
|
999
|
-
-
|
|
1108
|
+
- Vue 3 与 React 18–19 的**浏览器客户端**联邦为支持面;不支持 SSR / React Server Components / Next.js 全栈 / React Native / Node 服务端加载远程 / Vue 与 React 组件直接混渲染(同一页面同时用两套框架渲染组件树)。两框架各自纯项目互不引入对方;跨框架消费**纯 TS 模块**(如 Vue 宿主加载 React 远程的 utils)可用
|
|
1109
|
+
- React 侧不承诺组件保活:`createReactHostPages` 不提供 `keepAliveNames`(Vue 的 KeepAlive 专属);页面表里的 `keepAlive` 字段在 React 侧只作普通扩展位。重复打开已下载页面的模块复用照常
|
|
1110
|
+
- 跨源 Fast Refresh:远程 React 组件经远程 dev server 的 `@vite/client` 推送热更(实测 dev server 冷启动后首轮 ws 常未连稳,此时刷新宿主页面可见新代码);跨联邦边界的组件状态保留不作承诺
|
|
1111
|
+
- 不兼容 originjs 的 `virtual:__federation__` 旧写法
|
|
1000
1112
|
- 不支持 SSR(检测到即警告并禁用钩子)
|
|
1001
1113
|
- 无浏览器 DevTools 扩展(提供 `window.__FULGURJS_SCOPE__ / __FULGURJS_INFO__` 调试面)
|
|
1002
1114
|
- 无 JS 沙箱 / CSS 隔离——联邦是同 realm 共存架构,靠 shared 单例协商防止双运行时(详见 `docs/沙箱边界审计.md` 的三维度实测)
|
|
@@ -1007,6 +1119,7 @@ const Panel = await loadRemote('shop/Panel', {
|
|
|
1007
1119
|
- [`docs/webpack-mf-对照与缺口.md`](https://github.com/chenmingye/fulgurjs-federation/blob/master/docs/webpack-mf-对照与缺口.md) — webpack MF 逐项对照与明确不支持清单
|
|
1008
1120
|
- [`docs/沙箱边界审计.md`](https://github.com/chenmingye/fulgurjs-federation/blob/master/docs/沙箱边界审计.md) — CSS / 全局变量 / 公共依赖三维度互扰实测
|
|
1009
1121
|
- [`DESIGN.md`](https://github.com/chenmingye/fulgurjs-federation/blob/master/DESIGN.md) — 架构设计、对齐总表、测试与验收方案
|
|
1122
|
+
- 可复制示例:[`examples/react-host`](./examples/react-host) + [`examples/react-remote`](./examples/react-remote)(React,npm registry 安装即跑);[`examples/host`](./examples/host) + [`examples/remote-a`](./examples/remote-a)(Vue 配置样例);仓库内 e2e 回归夹具见 `fixtures/`(link: 本地插件)
|
|
1010
1123
|
|
|
1011
1124
|
## 开发与测试
|
|
1012
1125
|
|