@fulgurjs/federation 5.2.5 → 5.3.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 +14 -0
- package/DESIGN.md +7 -0
- package/README.en.md +86 -4
- package/README.md +125 -3
- package/dist/bridge-app-react.cjs +185 -0
- package/dist/bridge-app-react.d.cts +37 -0
- package/dist/bridge-app-react.d.ts +37 -0
- package/dist/bridge-app-react.js +115 -0
- package/dist/bridge-app-vue.cjs +111 -0
- package/dist/bridge-app-vue.d.cts +34 -0
- package/dist/bridge-app-vue.d.ts +34 -0
- package/dist/bridge-app-vue.js +51 -0
- package/dist/bridge-core-6uLJiMVa.d.ts +42 -0
- package/dist/bridge-core.cjs +224 -0
- package/dist/bridge-core.d.cts +51 -0
- package/dist/bridge-core.d.ts +51 -0
- package/dist/bridge-core.js +21 -0
- package/dist/bridge-errors.cjs +114 -0
- package/dist/bridge-errors.d.cts +36 -0
- package/dist/bridge-errors.d.ts +36 -0
- package/dist/bridge-errors.js +15 -0
- package/dist/bridge-host-react-C55RmGll.d.ts +35 -0
- package/dist/bridge-host-react.cjs +392 -0
- package/dist/bridge-host-react.d.cts +45 -0
- package/dist/bridge-host-react.d.ts +45 -0
- package/dist/bridge-host-react.js +183 -0
- package/dist/bridge-host-vue-BAxDKqGH.d.ts +32 -0
- package/dist/bridge-host-vue.cjs +426 -0
- package/dist/bridge-host-vue.d.cts +39 -0
- package/dist/bridge-host-vue.d.ts +39 -0
- package/dist/bridge-host-vue.js +211 -0
- package/dist/bridge-react.d.ts +12 -0
- package/dist/bridge-react.js +3 -0
- package/dist/bridge-vue.d.ts +8 -0
- package/dist/bridge-vue.js +3 -0
- package/dist/bridge.d.ts +16 -0
- package/dist/bridge.js +5 -0
- package/dist/chunk-E55P4TIY.js +76 -0
- package/dist/chunk-EH34L46P.js +207 -0
- package/dist/chunk-FVYGSLAQ.js +179 -0
- package/dist/chunk-LWJBHUJQ.js +114 -0
- package/dist/chunk-OF2JCXTZ.js +15 -0
- package/dist/chunk-TSYLDY6H.js +64 -0
- package/dist/chunk-UJFCPQGN.js +1210 -0
- package/dist/context.js +8 -64
- package/dist/index.cjs +84 -8
- package/dist/index.js +84 -8
- package/dist/react.d.ts +44 -2
- package/dist/react.js +2 -0
- package/dist/runtime-entry.d.ts +41 -2
- package/dist/runtime-entry.js +1 -1
- package/dist/runtime.js +1 -1
- package/dist/vue.js +2 -0
- package/docs/webpack-mf-/345/257/271/347/205/247/344/270/216/347/274/272/345/217/243.md +1 -1
- package/docs//350/277/201/347/247/273/346/214/207/345/215/227.md +1 -1
- package/examples/README.en.md +20 -0
- package/examples/README.md +20 -0
- package/examples/bridge/README.md +36 -0
- package/examples/bridge/react-host/README.md +23 -0
- package/examples/bridge/react-host/fulgurjs.config.ts +20 -0
- package/examples/bridge/react-host/index.html +11 -0
- package/examples/bridge/react-host/package-lock.json +2457 -0
- package/examples/bridge/react-host/package.json +24 -0
- package/examples/bridge/react-host/src/env.d.ts +7 -0
- package/examples/bridge/react-host/src/host-session.ts +26 -0
- package/examples/bridge/react-host/src/main.tsx +62 -0
- package/examples/bridge/react-host/tsconfig.json +13 -0
- package/examples/bridge/react-host/vite.config.ts +9 -0
- package/examples/bridge/react-remote/README.md +20 -0
- package/examples/bridge/react-remote/fulgurjs.config.ts +17 -0
- package/examples/bridge/react-remote/index.html +11 -0
- package/examples/bridge/react-remote/package-lock.json +2382 -0
- package/examples/bridge/react-remote/package.json +24 -0
- package/examples/bridge/react-remote/src/bridge.tsx +66 -0
- package/examples/bridge/react-remote/src/env.d.ts +7 -0
- package/examples/bridge/react-remote/src/main.tsx +7 -0
- package/examples/bridge/react-remote/tsconfig.json +13 -0
- package/examples/bridge/react-remote/vite.config.ts +9 -0
- package/examples/bridge/vue-host/README.md +24 -0
- package/examples/bridge/vue-host/fulgurjs.config.ts +22 -0
- package/examples/bridge/vue-host/index.html +11 -0
- package/examples/bridge/vue-host/package-lock.json +1885 -0
- package/examples/bridge/vue-host/package.json +24 -0
- package/examples/bridge/vue-host/src/App.vue +70 -0
- package/examples/bridge/vue-host/src/env.d.ts +7 -0
- package/examples/bridge/vue-host/src/host-session.ts +29 -0
- package/examples/bridge/vue-host/src/main.ts +4 -0
- package/examples/bridge/vue-host/tsconfig.json +13 -0
- package/examples/bridge/vue-host/vite.config.ts +9 -0
- package/examples/bridge/vue-remote/README.md +19 -0
- package/examples/bridge/vue-remote/fulgurjs.config.ts +16 -0
- package/examples/bridge/vue-remote/index.html +11 -0
- package/examples/bridge/vue-remote/package-lock.json +1856 -0
- package/examples/bridge/vue-remote/package.json +21 -0
- package/examples/bridge/vue-remote/src/App.vue +12 -0
- package/examples/bridge/vue-remote/src/bridge.ts +57 -0
- package/examples/bridge/vue-remote/src/env.d.ts +7 -0
- package/examples/bridge/vue-remote/src/main.ts +15 -0
- package/examples/bridge/vue-remote/tsconfig.json +13 -0
- package/examples/bridge/vue-remote/vite.config.ts +9 -0
- package/package.json +43 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.3.0
|
|
4
|
+
|
|
5
|
+
- **新增:跨框架桥接 `/bridge` —— 子应用级 Vue↔React 双向互嵌**(README §8.2;实现合同见 `docs/跨框架桥接实施任务书-20260929.md`)
|
|
6
|
+
- 子应用侧:`defineBridgeApp`(`/runtime` 与 `/react` 同名双导出)——工厂接收 props 快照、返回装配完整的 VueApp / ReactElement,契约负责按容器跟踪、挂载/卸载与清理;React 侧用提交探针兑现「首次根提交完成才算挂载成功」,`react-dom/client` 在实际 mount 时才动态加载。
|
|
7
|
+
- 宿主侧:`createVueBridgeApp` / `createReactBridgeApp`(`/bridge/vue`、`/bridge/react` 分离推荐入口 + `/bridge` 聚合兼容入口)——受控 `sessionKey` 会话代次、`getContext` 同步快照校验(拒绝 thenable/非对象,`MFU-016 phase: getContext`)、`appProps` 挂载时浅拷贝快照(嵌套对象/函数保留原引用)、默认中文错误占位(「重试加载 / 刷新页面重试」)。
|
|
8
|
+
- 会话语义:页面级单会话登记(同页多实例单会话约束,冲突 `MFU-017`);换账号/登出(`sessionKey → null`)立即作废旧代次并卸载;换代先 `clearAppContext` 保证零旧账号残留;已进入 `loadRemote` 的工作不冒充取消——迟到结果按代次丢弃,远程 `onSession` 遵守既有 `signal.aborted` 契约。
|
|
9
|
+
- 错误码:`MFU-015`(桥接契约非法)/ `MFU-016`(桥接准备或生命周期失败,`details.phase` 区分 getContext/mount/unmount)/ `MFU-017`(会话参数与 AppContext 不一致),码表三方一致校验扩展至 `bridge-errors.ts`。
|
|
10
|
+
- 新增错误码共 3 个(总表 41 → 44);README 中英双语 §8.2/§12、DESIGN §6.2、webpack MF 对照与迁移指南同步。
|
|
11
|
+
- 隔离边界如实声明:桥接只隔离两棵组件树的挂卸边界——无 realm/CSS 隔离、子应用内部错误不冒泡宿主边界、子应用 memory 路由不与宿主 URL 同步;组件级混渲染继续不支持。
|
|
12
|
+
- 使用合同:桥接宿主必须同时安装 vue + react + react-dom 并将 shared 三键全部 singleton;纯 Vue / 纯 React 项目零对方依赖不受影响。
|
|
13
|
+
- 修复:dev 跨源场景 react-refresh shim 的自引用顶层 await 死锁(无 @vitejs/plugin-react 的宿主消费 React 远程时页面永久挂起;shim 自身不再参与导入改写);为无 plugin-react preamble 的宿主注入首个 http(s) dev 远程 origin 的 react-refresh preamble + 页面级单例发布。
|
|
14
|
+
- 新增错误码后 gzip 门禁同步:`bridge-host-vue.js` / `bridge-host-react.js` 各 ≤ 4096B(zlib level9,框架外置);`runtime.js` ≤ 9216B、`react-adapter.js` ≤ 4096B 维持不变(本轮内核零改动)。
|
|
15
|
+
- 示例:`examples/bridge/{vue-host,react-host,vue-remote,react-remote}` 四个独立工程(registry 正式包消费,双向各一对)。
|
|
16
|
+
|
|
3
17
|
## 5.2.5
|
|
4
18
|
|
|
5
19
|
- **修复:远程示例(examples/{vue,react}/remote)子路径生产部署的 modulepreload 404**——Vite 的 preload helper 会把依赖链接转成**根绝对路径**(`"/"+dep`),remote 以默认 base `/` 构建时,modulepreload 会打到宿主站点的根 `/assets/`(SPA fallback 回 HTML → MIME 错误)。`vite.config.ts` 现按 `command === 'build'` 自动切换 `base` 为 `/vue-remote/`、`/react-remote/`(dev 不受影响);两份 README 的部署说明同步订正(不再声称"相对路径天然适配子路径")。运行时代码无变更。
|
package/DESIGN.md
CHANGED
|
@@ -170,6 +170,13 @@ remote 样式改动立即生效;host 自身业务 HMR 不受影响。
|
|
|
170
170
|
- 失败恢复:runtime `importEntry`(entryFailCounts)+ dev 容器 loader(字面量主路径 + `@vite-ignore` 重试分支;字面量保证与 vite importAnalysis 重写形态一致,拼接表达式会经 injectQuery 产生 `?import` 变体 URL 与内部静态 import 形成双模块实例)+ prod remoteEntry 产物后处理(`__fgR` 包装)三处统一「失败后重试变更 URL(fulgurjs_retry=N)穿透浏览器 module map 失败缓存」。
|
|
171
171
|
- 开发类型双轨:零配置 ambient(带体 any,可解析;简写 ambient 会 shadow paths 命中的转发文件,TS2439/TS2709 语言限制见 dts.ts 注释)+ 精确轨 `remote.d/` 目录 .ts 转发模块(export *)配 tsconfig paths;宿主配了 paths 的远程自动跳过同名 ambient。
|
|
172
172
|
|
|
173
|
+
## 6.2 跨框架桥接入口(5.3.0 /bridge)
|
|
174
|
+
|
|
175
|
+
- 新增源文件:`bridge-errors.ts`(MFU-015/016/017 码表)→ `bridge-core.ts`(契约校验/页面级会话登记/上下文代次校验/timeout,框架无关、零 vue/react 导入)→ `bridge-app-vue.ts` + `bridge-app-react.ts`(子应用侧 defineBridgeApp;React 侧 react-dom/client 在 mount 时动态 import,首提交门控 = FirstCommitGate + CommitProbe)→ `bridge-host-vue.ts` + `bridge-host-react.ts`(宿主工厂,注入 loadRemote,多根 fragment 保稳定挂载容器 + 兄弟节点占位)→ `bridge.ts` / `bridge-vue.ts` / `bridge-react.ts`(三个入口源,dist JS 壳由 gen-runtime-entry.mjs 生成)。
|
|
176
|
+
- dist 壳矩阵:`/runtime` 与 `/react` 各加 `defineBridgeApp` 再导出(零对向框架);`/bridge/vue`、`/bridge/react` 为分离推荐入口(各自只绑一个宿主适配器);`/bridge` 为聚合兼容入口。dev 门面:`genApiFacade` 双面各加 defineBridgeApp;新增 `genBridgeFacade(framework)` 三个虚拟模块(仅 expose 目标改写走门面,宿主页直接消费 node_modules dist 壳)。
|
|
177
|
+
- 会话语义:页面级桥接会话登记(acquire/release,冲突 MFU-017)先于任何全局写入;`resolveBridgeContext` 校验 getter 快照(形态/thenable/会话一致)→ 通过后才 provideAppContext;换代先 clearAppContext(零旧账号残留)。宿主组件按受控 sessionKey 驱动代次状态机(gen 计数丢弃迟到结果)。
|
|
178
|
+
- 守护:`bridge.test.ts` + `bridge-host.test.ts`(jsdom 真实挂载)+ runtime-entry-graph / client-types 批准清单扩充(bridge 三入口零对向框架、bridge-app-react 无静态 react-dom/client)+ gzip 门禁(bridge-host-vue/react ≤ 4096B)。
|
|
179
|
+
|
|
173
180
|
## 7. 文档交付物
|
|
174
181
|
|
|
175
182
|
**唯一权威文档 = 仓库根 `README.md`**(随 npm 包发布,GitHub 与 npm 双端可读);迁移路径见 `docs/迁移指南.md`。内容组织:
|
package/README.en.md
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
- **Vue direct rendering** — `remoteComponent('remote/X')` on the runtime entry: `defineAsyncComponent + loadRemote` wrapper with explicit error placeholder; runtime core stays framework-free
|
|
41
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
42
|
- **CSP friendly** — no `eval` / `new Function` anywhere in loading paths
|
|
43
|
-
- **Error-code system (
|
|
43
|
+
- **Error-code system (44 codes)** — CFG / DEV / BLD / MFU / CC segments, drift-checked against the code registry (see §11)
|
|
44
44
|
|
|
45
45
|
## 3. Installation & requirements
|
|
46
46
|
|
|
@@ -257,6 +257,84 @@ Type generation supports string or array `extends` (later entries override earli
|
|
|
257
257
|
- `dts: false` stops generation without deleting existing output; `dts.dir` relocates; `mode: 'shim'` gives loose IDE-clean placeholders
|
|
258
258
|
- 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
|
|
259
259
|
|
|
260
|
+
### 8.7 Cross-framework bridge — `/bridge` (sub-app-level Vue↔React, 5.3.0+)
|
|
261
|
+
|
|
262
|
+
**Scope**: whole-app mount/unmount embedding both ways — a Vue 3 host mounts a React 18/19 sub-app, and a React host mounts a Vue 3 sub-app. Component-level conversion, host↔sub-app URL sync, Angular, SSR/RSC, JS sandbox, CSS isolation are out of scope (§12).
|
|
263
|
+
|
|
264
|
+
#### Entries & import graph
|
|
265
|
+
|
|
266
|
+
```text
|
|
267
|
+
build @fulgurjs/federation -> the Vite plugin (unchanged)
|
|
268
|
+
Vue sub-app @fulgurjs/federation/runtime -> defineBridgeApp (zero React)
|
|
269
|
+
React sub-app @fulgurjs/federation/react -> defineBridgeApp (zero Vue; react-dom/client loads at mount time)
|
|
270
|
+
bridge host @fulgurjs/federation/bridge/vue -> createVueBridgeApp (recommended for Vue hosts; zero React)
|
|
271
|
+
@fulgurjs/federation/bridge/react -> createReactBridgeApp (recommended for React hosts; zero Vue)
|
|
272
|
+
@fulgurjs/federation/bridge -> aggregate (kept for compatibility; dev native ESM executes both host adapters)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**The split entries are the recommended usage**: a Vue host that only uses `createVueBridgeApp` never executes the React host adapter — in dev native ESM and in the production bundle (asserted by e2e request graphs). The aggregate `/bridge` tree-shakes in production but has no such guarantee in dev.
|
|
276
|
+
|
|
277
|
+
**Dual-framework install contract (required)**: the bridge host installs `vue` + `react` + `react-dom` and configures all three as `singleton: true` in `shared`. Sub-apps install and share only their own framework. Pure single-framework projects are unaffected. Missing singletons is a usage violation — the plugin runs the negotiation mechanism honestly (double-instance symptoms such as Invalid hook call are documented, not intercepted).
|
|
278
|
+
|
|
279
|
+
#### Sub-app side: `defineBridgeApp` (`/runtime` and `/react`, same name)
|
|
280
|
+
|
|
281
|
+
The remote's `./bridge` expose module **default-exports** the contract object; the plugin validates that `mount`/`unmount` are functions (`MFU-015` otherwise).
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
// Vue sub-app src/bridge.ts
|
|
285
|
+
import { createApp } from 'vue'
|
|
286
|
+
import { createMemoryHistory, createRouter } from 'vue-router'
|
|
287
|
+
import { defineBridgeApp } from '@fulgurjs/federation/runtime'
|
|
288
|
+
export default defineBridgeApp((props) => {
|
|
289
|
+
const app = createApp(App, props)
|
|
290
|
+
app.use(createRouter({ history: createMemoryHistory(), routes }))
|
|
291
|
+
return app
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
// React sub-app src/bridge.tsx
|
|
297
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
298
|
+
import { defineBridgeApp } from '@fulgurjs/federation/react'
|
|
299
|
+
export default defineBridgeApp((props) => <MemoryRouter><App {...props} /></MemoryRouter>)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Contract semantics (`BridgeApp`):
|
|
303
|
+
- `mount(el, props?): void | Promise<void>` — returning `void` means the first root commit completed synchronously (Vue); a Promise keeps the host pending until the first root commit completes (React uses a built-in commit probe; `root.render()` returning does **not** count as success). Failure before the first commit must throw/reject (host turns it into `MFU-016`, `details.phase: 'mount'`) after cleaning up any created root.
|
|
304
|
+
- `unmount(el): void` — synchronously invalidates the current generation for that container and cleans up; unknown containers are a no-op. Unmounting while pending immediately invalidates the in-flight generation: late results must not revive DOM, overwrite host state, or produce unhandled rejections. An `unmount` throw is reported as `MFU-016` (`phase: 'unmount'`); the container's cleanup state is uncertain and must not be reused for a new root directly.
|
|
305
|
+
- Contract instances are keyed **per container element**; double-mount on the same container is rejected (`MFU-016`).
|
|
306
|
+
- Errors inside the sub-app after the first commit belong to **the sub-app's own error boundary** — host boundaries cannot catch cross-root render errors.
|
|
307
|
+
|
|
308
|
+
#### Host side: `createVueBridgeApp` / `createReactBridgeApp`
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
// Vue host
|
|
312
|
+
import { createVueBridgeApp } from '@fulgurjs/federation/bridge/vue'
|
|
313
|
+
const RemoteReactApp = createVueBridgeApp('bridge-react-remote/bridge', {
|
|
314
|
+
retries: 1,
|
|
315
|
+
getContext: () => getLatestHostContext(), // your own synchronous pure getter
|
|
316
|
+
})
|
|
317
|
+
// <RemoteReactApp :session-key="loginKey" :app-props="{ userId, onReady }" />
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
// React host
|
|
322
|
+
import { createReactBridgeApp } from '@fulgurjs/federation/bridge/react'
|
|
323
|
+
const RemoteVueApp = createReactBridgeApp('bridge-vue-remote/bridge', { getContext: () => getLatestHostContext() })
|
|
324
|
+
// <RemoteVueApp sessionKey={loginKey} appProps={{ userId, onReady }} />
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
| Item | `createVueBridgeApp` | `createReactBridgeApp` |
|
|
328
|
+
|---|---|---|
|
|
329
|
+
| Factory options | `loadingComponent?` `errorComponent?` (receives `error`; full takeover) `retries?` (0–10) `timeout?` `getContext?` | `fallback?` `error?` (node or `(error, retry) => ReactNode`) `retries?` `timeout?` `getContext?` |
|
|
330
|
+
| Component props | `appProps: P` + `sessionKey?: string \| null` (control prop, never mixed into business props) | same |
|
|
331
|
+
| Error placeholder | Chinese diagnostic (code + root cause + fix) with retry / full-reload buttons | same |
|
|
332
|
+
|
|
333
|
+
- **`appProps` snapshot**: shallow-copied top-level fields at mount time; nested objects, reactive stores and functions keep their original references. Later top-level replacements are not tracked (use `:key` / React `key` to remount). Cross-root inheritance (Vue provide/inject, Pinia, React Context, routers) does not happen — pass what is needed explicitly.
|
|
334
|
+
- **`getContext`**: a synchronous, side-effect-free getter called before each actual load (first load, retry, session switch). Non-object/thenable returns → `MFU-016` (`phase: 'getContext'`). The bridge validates the snapshot's `sessionKey` against the controlled value (`MFU-017` on mismatch, without writing global state), then writes `provideAppContext` itself. On generation change the bridge clears the previous account context first (zero residue).
|
|
335
|
+
- **Controlled `sessionKey`**: accepts `undefined` (no controlled validation) / `null` (logged out: unmount immediately, keep the container empty, stop loading) / non-empty string (login generation). Illegal values → `MFU-017`.
|
|
336
|
+
- **Multi-instance**: several same-spec instances coexist (per-el keying); `AppContext` is a page-level singleton — all controlled instances on a page must share the same session (`MFU-017` otherwise). React StrictMode double-effect is safe. Vue `<KeepAlive>` deactivation is **not** an unmount. Late results from invalidated generations are dropped by generation guards; a remote `onSession` must honor the existing `signal.aborted` contract.
|
|
337
|
+
|
|
260
338
|
## 9. Artifacts, endpoints & caching
|
|
261
339
|
|
|
262
340
|
| Artifact | Cache policy |
|
|
@@ -275,7 +353,7 @@ Lazy-loading measurement layers: ① nothing until first render of a remote comp
|
|
|
275
353
|
- `DEBUG=fulgurjs:*` — controlled pipeline diagnostics (off by default)
|
|
276
354
|
- Runtime diagnostics are emitted in Chinese by design (language policy); codes are stable identifiers listed below
|
|
277
355
|
|
|
278
|
-
## 11. Error codes (
|
|
356
|
+
## 11. Error codes (44)
|
|
279
357
|
|
|
280
358
|
| Segment | Code | Meaning |
|
|
281
359
|
|---|---|---|
|
|
@@ -318,12 +396,16 @@ Lazy-loading measurement layers: ① nothing until first render of a remote comp
|
|
|
318
396
|
| | `MFU-012` | setup/onSession threw (retryable; only the failed stage resets) |
|
|
319
397
|
| | `MFU-013` | onSession declared but host sessionKey missing |
|
|
320
398
|
| | `MFU-014` | setup/onSession synchronously re-loading the same remote (deadlock guard) |
|
|
399
|
+
| | `MFU-015` | bridge contract invalid (`./bridge` default export missing non-function mount/unmount; fix points to `defineBridgeApp`) |
|
|
400
|
+
| | `MFU-016` | bridge preparation or lifecycle failure (`details.phase` = getContext/mount/unmount; cause keeps the sub-app's original error) |
|
|
401
|
+
| | `MFU-017` | bridge session mismatch (controlled sessionKey vs AppContext / illegal value / page-level single-session conflict) |
|
|
321
402
|
| CC | `CC-001` | AppContext required key missing (got/expected/example) |
|
|
322
403
|
| | `CC-002` | runtime singleton unavailable (standalone remote page) |
|
|
323
404
|
|
|
324
405
|
## 12. Boundaries (explicitly not supported)
|
|
325
406
|
|
|
326
|
-
- 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
|
|
407
|
+
- 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. **Cross-framework boundary (5.3.0+)**: sub-app-level embedding is supported (§8.7 `/bridge`); direct component-level Vue↔React rendering in one tree is not (that is the product of framework-conversion libraries). Pure single-framework projects keep zero cross-dependency
|
|
408
|
+
- **Bridge isolation boundary (declared honestly in §8.7)**: bridging isolates only the mount/unmount edge of the two component trees — no browser realm isolation. Remote global CSS, `body`/`html` styles, global variables, and DOM rendered outside the container via React Portal / Vue Teleport still affect the host; `unmount` cannot revoke CSS the browser already loaded. Sub-app internal errors do not bubble into host error boundaries (cross-root). Sub-apps use memory routing — v1 does **not** sync to the host URL (refreshing does not restore the sub-app's internal path)
|
|
327
409
|
- React side does not promise component keep-alive (`keepAliveNames` is Vue-only); re-opened pages still reuse downloaded modules
|
|
328
410
|
- 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
|
|
329
411
|
- Not compatible with originjs `virtual:__federation__` legacy imports
|
|
@@ -335,7 +417,7 @@ Lazy-loading measurement layers: ① nothing until first render of a remote comp
|
|
|
335
417
|
- [webpack MF comparison & gaps (Chinese)](https://github.com/chenmingye/fulgurjs-federation/blob/master/docs/webpack-mf-对照与缺口.md)
|
|
336
418
|
- [Sandbox boundary audit (Chinese)](https://github.com/chenmingye/fulgurjs-federation/blob/master/docs/沙箱边界审计.md)
|
|
337
419
|
- [`DESIGN.md`](https://github.com/chenmingye/fulgurjs-federation/blob/master/DESIGN.md) — architecture and alignment tables
|
|
338
|
-
- Examples: [`examples/vue/{host,remote}`](./examples) + [`examples/react/{host,remote}`](./examples)
|
|
420
|
+
- Examples: [`examples/vue/{host,remote}`](./examples) + [`examples/react/{host,remote}`](./examples) + [`examples/bridge/*`](./examples) — copy-and-run projects, registry-installable (see the examples entry page)
|
|
339
421
|
|
|
340
422
|
## 14. Development & testing
|
|
341
423
|
|
package/README.md
CHANGED
|
@@ -39,8 +39,9 @@
|
|
|
39
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 会话同步承担,不依赖页面刷新)
|
|
40
40
|
- **Vue 直渲染**:`remoteComponent('remote/X')`(`@fulgurjs/federation/runtime` 导出)——`defineAsyncComponent + loadRemote` 的标准封装,加载失败显式错误占位(错误码+根因+修法+**重试加载/刷新页面重试**),runtime.js 零框架依赖零体积增量
|
|
41
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 依赖
|
|
42
|
+
- **跨框架桥接(子应用级,5.3.0 起)**:Vue 3 宿主嵌入 React 18/19 子应用、React 宿主嵌入 Vue 子应用——子应用以 `defineBridgeApp` 导出 `mount/unmount` 契约,宿主用 `createVueBridgeApp` / `createReactBridgeApp`(`/bridge` 入口,推荐 `/bridge/vue`、`/bridge/react` 分离入口)像普通组件一样挂载;受控 `sessionKey` 会话代次、`appProps` 快照 + 函数引用、首次根提交语义、加载/挂载失败占位与恢复、同页多实例与 StrictMode 安全内置;双框架 shared singleton 配方强制(纯项目零对方依赖不受影响)
|
|
42
43
|
- **CSP 友好**:原生 ESM 加载路径全程无 `eval` / `new Function`,可在严格 CSP(无 `unsafe-eval`)下运行
|
|
43
|
-
- **全链路错误码体系(
|
|
44
|
+
- **全链路错误码体系(44 码)**:CFG/DEV/BLD/MFU/CC 五段 + 手册 §6 码表防漂移校验
|
|
44
45
|
|
|
45
46
|
## 安装
|
|
46
47
|
|
|
@@ -135,6 +136,10 @@ import fulgurjsConfig from './fulgurjs.config'
|
|
|
135
136
|
- **路径 ②:宿主多页面接入**——宿主有一批路由要映射到远程页面。用 `createHostPages` 一份页面表解决 URL 解析/组件缓存/骨架屏/错误占位/保活名称。
|
|
136
137
|
- **路径 ③:远程业务页需要宿主环境**——远程页面依赖全局组件注册、用户/权限/字典等启动期初始化。用 `setup`/`onSession` 声明式初始化 + `AppContext` 传值。
|
|
137
138
|
|
|
139
|
+
### 跨框架桥接(第 4 条路径:Vue 宿主嵌 React 子应用 / React 宿主嵌 Vue 子应用)
|
|
140
|
+
|
|
141
|
+
需要**整站级**跨框架嵌入(子应用自带路由与状态、整站挂载/卸载)时,用 `/bridge` 入口——完整 API 见 [§8.2](#82-跨框架桥接-api--bridge),最小示例见 `examples/bridge/`(Vue 宿主×React 远程、React 宿主×Vue 远程双向各一对)。组件级混渲染(Vue 模板里直接渲染 React 组件)**不支持**,那是框架桥接库的产品。
|
|
142
|
+
|
|
138
143
|
### 路径 ①:暴露并加载普通模块(无 setup、无桥、无页面表)
|
|
139
144
|
|
|
140
145
|
**谁配置**:远程应用 vite.config.ts 写 `exposes`;宿主 vite.config.ts 写 `remotes`。**谁调用**:消费方业务代码。**何时执行**:`loadRemote` 只取得模块导出,**调用导出函数仍由业务代码决定**——expose ≠ 自动执行。
|
|
@@ -607,7 +612,7 @@ CLI 解析同一份配置值;dev/prod 的 URL 选择规则与 `federation({ re
|
|
|
607
612
|
| `fulgurjs check-pages [--config <path>] [--site <URL>] [--manifest <r>=<路径\|URL>]... [--require-verified]` | 页面契约核对:宿主页面表(`hostPages` 具名导出)↔ 远程 manifest exposes。manifest 来源优先级 **`--manifest`(可多次、文件路径或 URL) > `--site`/消费方 prod 地址推导**(显式来源失败不回退、无本地 dist 兜底),输出每个 remote 的实际命中来源(防止旧本地 dist 冒充线上核对)。报告未知 remote、映射到未消费远程、缺失 expose、路由冲突(R1–R5);**确定性错误退出码 1**,远程不可达报「无法验证」,`--require-verified` 时无法验证也非零(CI 严格模式,避免 0 条核对显示通过)。`--json` 供 CI |
|
|
608
613
|
| `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 门禁 |
|
|
609
614
|
|
|
610
|
-
### 6. 错误码总表(
|
|
615
|
+
### 6. 错误码总表(44 个)
|
|
611
616
|
|
|
612
617
|
| 段 | 码 | 含义 |
|
|
613
618
|
|---|---|---|
|
|
@@ -650,6 +655,9 @@ CLI 解析同一份配置值;dev/prod 的 URL 选择规则与 `federation({ re
|
|
|
650
655
|
| | `MFU-012` | setup/onSession 执行抛错(该次 loadRemote 拒绝;仅清失败阶段缓存,可直接重试,已成功的阶段不重复) |
|
|
651
656
|
| | `MFU-013` | 远程声明 onSession 但宿主 AppContext 缺 sessionKey(登录代次;禁止用 token 充当) |
|
|
652
657
|
| | `MFU-014` | setup/onSession 同步段内递归 loadRemote 同一远程(自等待死锁防线) |
|
|
658
|
+
| | `MFU-015` | 桥接契约非法(`./bridge` 默认导出缺 mount/unmount 或非函数;修法指向 defineBridgeApp) |
|
|
659
|
+
| | `MFU-016` | 桥接准备或生命周期失败(`details.phase` 区分 getContext/mount/unmount;根因含子应用原始错误) |
|
|
660
|
+
| | `MFU-017` | 桥接会话参数与 AppContext 不一致(受控 sessionKey 与全局会话矛盾、非法值(空串/数字)、页面级单会话冲突) |
|
|
653
661
|
| CC 跨应用上下文 | `CC-001` | AppContext 必需字段缺失(三段式:got/expected/example,修法指向宿主桥 `provideAppContext`) |
|
|
654
662
|
| | `CC-002` | 运行时单例不可用(独立直开远程页;修法 = 经宿主联邦加载,时序契约 bridge → 远程 setup → 页面模块) |
|
|
655
663
|
|
|
@@ -750,6 +758,119 @@ const { data, error, loading, reload } = useLoadRemote<Utils>('remote-react/util
|
|
|
750
758
|
|
|
751
759
|
类型生成支持字符串或数组 `extends`(后项覆盖前项)、指向目录的 `references`,并按声明文件目录解析继承路径。`baseUrl` 与 `paths` 独立继承。多个实际应用上下文的远程 `paths` 接管不一致时,会保留默认宽松声明并给出中文提示;需要精确类型时请统一这些应用配置。生命周期错误 `MFU-012` 的 `cause` 保留 setup/onSession 抛出的原始异常。
|
|
752
760
|
|
|
761
|
+
### 8.2 跨框架桥接 API — `/bridge`(子应用级 Vue↔React 互嵌,5.3.0 起)
|
|
762
|
+
|
|
763
|
+
**产品范围**:整站挂载/卸载的双向嵌入——Vue 3 宿主嵌 React 18/19 子应用、React 18/19 宿主嵌 Vue 3 子应用。组件级互转、宿主与子应用 URL 同步、Angular、SSR/RSC、JS 沙箱、CSS 隔离不在支持面(见 §12)。
|
|
764
|
+
|
|
765
|
+
#### 入口与导入图
|
|
766
|
+
|
|
767
|
+
```text
|
|
768
|
+
构建期 @fulgurjs/federation -> 插件(不变)
|
|
769
|
+
Vue 子应用 @fulgurjs/federation/runtime -> defineBridgeApp(零 React)
|
|
770
|
+
React 子应用 @fulgurjs/federation/react -> defineBridgeApp(零 Vue;react-dom/client 实际 mount 时才加载)
|
|
771
|
+
桥接宿主 @fulgurjs/federation/bridge/vue -> createVueBridgeApp(推荐:Vue 宿主,零 React)
|
|
772
|
+
@fulgurjs/federation/bridge/react -> createReactBridgeApp(推荐:React 宿主,零 Vue)
|
|
773
|
+
@fulgurjs/federation/bridge -> 聚合入口(兼容保留;dev 原生 ESM 会同时执行两个宿主适配器)
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
**推荐用法是分离入口**:只用 `createVueBridgeApp` 的宿主页在 dev 首屏与生产产物中都不执行 React 宿主适配器,反之亦然(e2e 断言请求图)。聚合 `/bridge` 在生产可摇树、在 dev 无摇树保证——文档与示例默认分离入口。
|
|
777
|
+
|
|
778
|
+
**双框架安装合同(必须)**:桥接宿主同时安装 `vue` + `react` + `react-dom`,shared 三键全部 `singleton: true`:
|
|
779
|
+
|
|
780
|
+
```ts
|
|
781
|
+
// 桥接宿主 fulgurjs.config.ts
|
|
782
|
+
shared: {
|
|
783
|
+
vue: { singleton: true },
|
|
784
|
+
react: { singleton: true },
|
|
785
|
+
'react-dom': { singleton: true },
|
|
786
|
+
}
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
子应用只装并共享自己的框架(Vue 子应用:`vue`;React 子应用:`react` + `react-dom`)。纯 Vue / 纯 React 项目的零对方依赖承诺不受影响。共享子路径(`react/jsx-runtime`、`react/jsx-dev-runtime`、`react-dom/client`)由 shared 机制协商单实例;宿主侧另需为 `react`、`react-dom` 配置 shared(子路径协商依赖父键)。缺 singleton 的真实症状(Invalid hook call、双实例)见 §6 错误码表 `MFU-010` 与避坑指南——插件按协商机制如实运行,不拦截配置违例。
|
|
790
|
+
|
|
791
|
+
#### 子应用侧:`defineBridgeApp`(`/runtime` 与 `/react` 同名双导出)
|
|
792
|
+
|
|
793
|
+
远程 expose `./bridge` 的模块**默认导出**契约对象;插件校验 `mount`/`unmount` 均为函数,否则 `MFU-015`:
|
|
794
|
+
|
|
795
|
+
```ts
|
|
796
|
+
// Vue 子应用 src/bridge.ts —— fulgurjs.config exposes: { './bridge': './src/bridge.ts' }
|
|
797
|
+
import { createApp } from 'vue'
|
|
798
|
+
import { createMemoryHistory, createRouter } from 'vue-router'
|
|
799
|
+
import { defineBridgeApp } from '@fulgurjs/federation/runtime'
|
|
800
|
+
import App from './App.vue'
|
|
801
|
+
|
|
802
|
+
export default defineBridgeApp((props) => {
|
|
803
|
+
const app = createApp(App, props)
|
|
804
|
+
app.use(createRouter({ history: createMemoryHistory(), routes }))
|
|
805
|
+
return app // 返回装配完整的 VueApp;mount/unmount 由契约负责
|
|
806
|
+
})
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
```tsx
|
|
810
|
+
// React 子应用 src/bridge.tsx
|
|
811
|
+
import { MemoryRouter } from 'react-router-dom'
|
|
812
|
+
import { defineBridgeApp } from '@fulgurjs/federation/react'
|
|
813
|
+
|
|
814
|
+
export default defineBridgeApp((props) => (
|
|
815
|
+
<MemoryRouter><App {...props} /></MemoryRouter>
|
|
816
|
+
))
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
契约语义(`BridgeApp` 接口,双方入口共享同一类型定义):
|
|
820
|
+
|
|
821
|
+
- `mount(el, props?): void | Promise<void>`——返回 `void` 表示首次根提交已同步完成(Vue 同步 mount);返回 Promise 时宿主保持 pending 直到首次根提交后完成(React 由契约内建提交探针兑现,`root.render()` 返回**不**算成功)。首次提交前的失败必须抛错/拒绝(宿主转 `MFU-016`,`details.phase: 'mount'`)并清理已创建的 app/root。
|
|
822
|
+
- `unmount(el): void`——同步使该容器代次失效并清理;未知容器为 no-op。pending 时卸载立即作废本轮代次,迟到的成功/失败不得复活 DOM、改写宿主状态或产生未处理拒绝。unmount 抛错由宿主捕获报 `MFU-016`(`phase: 'unmount'`),该容器清理状态不确定,不得直接在同一 el 上再挂新实例。
|
|
823
|
+
- 契约实例**按容器 el 分键**:同一契约多处挂载互不干扰;同一容器未卸载再次 mount 拒绝(`MFU-016`,容器已被占用)且不覆盖原实例。
|
|
824
|
+
- 首次根提交后的子应用内部错误由**子应用自己的错误边界**负责——宿主 ErrorBoundary/errorCaptured 捕不到跨 root 的渲染错误,插件不冒充兜底(§4.4 语义,README 不承诺「宿主兜底子应用一切错误」)。
|
|
825
|
+
|
|
826
|
+
#### 宿主侧工厂(`/bridge/vue` 与 `/bridge/react`)
|
|
827
|
+
|
|
828
|
+
```ts
|
|
829
|
+
// Vue 宿主
|
|
830
|
+
import { createVueBridgeApp } from '@fulgurjs/federation/bridge/vue'
|
|
831
|
+
import { getLatestHostContext } from './host-context' // 宿主自有的同步纯 getter
|
|
832
|
+
|
|
833
|
+
const RemoteReactApp = createVueBridgeApp('bridge-react-remote/bridge', {
|
|
834
|
+
retries: 1,
|
|
835
|
+
getContext: () => getLatestHostContext(),
|
|
836
|
+
})
|
|
837
|
+
// 模板:<RemoteReactApp :session-key="loginKey" :app-props="{ userId, onReady }" />
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
```tsx
|
|
841
|
+
// React 宿主
|
|
842
|
+
import { createReactBridgeApp } from '@fulgurjs/federation/bridge/react'
|
|
843
|
+
const RemoteVueApp = createReactBridgeApp('bridge-vue-remote/bridge', {
|
|
844
|
+
getContext: () => getLatestHostContext(),
|
|
845
|
+
})
|
|
846
|
+
// JSX:<RemoteVueApp sessionKey={loginKey} appProps={{ userId, onReady }} />
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
| 项 | `createVueBridgeApp`(Vue 宿主) | `createReactBridgeApp`(React 宿主) |
|
|
850
|
+
|---|---|---|
|
|
851
|
+
| 工厂选项 | `loadingComponent?` `errorComponent?`(收到 `error` prop,完全接管) `retries?`(0–10 整数) `timeout?`(正有限 ms) `getContext?` | `fallback?`(pending 占位) `error?`(节点或 `(error, retry) => ReactNode`) `retries?` `timeout?` `getContext?` |
|
|
852
|
+
| 返回组件 props | `appProps: P`(业务数据)+ `sessionKey?: string \| null`(控制参数,不混入业务 props) | 同左,`ComponentType<{ appProps: P; sessionKey?: string \| null }>` |
|
|
853
|
+
| 泛型 | `createVueBridgeApp<P>(spec, options?)`,P 只约束 `appProps` | 同左 |
|
|
854
|
+
| spec | 完整 `<remote>/<expose>`,与 `remoteComponent` 同一解析规则;无 remotePrefixes/schema/deriveSpec | 同左 |
|
|
855
|
+
| 默认错误占位 | 中文诊断(错误码+根因+修法)+「重试加载 / 刷新页面重试」 | 同左 |
|
|
856
|
+
|
|
857
|
+
- **`appProps` 快照语义**:挂载时浅拷贝顶层字段传入,嵌套对象/响应式 store/函数保留原引用;之后的顶层替换**不追踪、不重渲染子应用**,需要重置用 `:key`/key 重建。宿主新闭包不会自动传给子应用——实时读取宿主状态请传稳定回调(内部读 ref/store)或主动重挂。跨 root 不继承宿主 provide/inject、Pinia、React Context 或路由——需要的数据经 `appProps`、AppContext、共享实例或子应用自装。
|
|
858
|
+
- **`getContext`**:无副作用的**同步** getter,在首次、重试及换会话的实际加载前调用;返回快照对象(拒绝 Promise/thenable 与非对象——`MFU-016`,`phase: 'getContext'`)。桥接层先校验快照 `sessionKey` 与受控值一致(不一致 `MFU-017`,且不写全局),**校验通过后由桥接层调用 `provideAppContext`**——getter 本身不写全局。未提供 getter 时校验现有 `AppContext.sessionKey` 必须与受控值一致。换代时桥接层先 `clearAppContext()` 清旧账号独有字段再写新快照,保证零旧账号残留。
|
|
859
|
+
- **`sessionKey` 受控语义**:只接受 `undefined`(不启用受控会话)/`null`(登出态:立即卸载、保持空容器、不再 loadRemote)/非空字符串(登录代次)。空字符串、数字等非法值按 `MFU-017` 拒绝挂载。
|
|
860
|
+
|
|
861
|
+
| 触发 | 行为 |
|
|
862
|
+
|---|---|
|
|
863
|
+
| 首次渲染,`sessionKey` 非空字符串 | getContext(若提供)→ 校验快照/现有 context → 桥接层 provideAppContext → loadRemote → 契约校验 → `contract.mount(el, appProps 快照)`;远程 onSession 用同一代次 |
|
|
864
|
+
| 首次渲染,`sessionKey` 省略 | 不启用受控校验;仍可提供快照或复用现有 AppContext;远程声明 onSession 时按 runtime 既有规则(无 sessionKey → `MFU-013`) |
|
|
865
|
+
| `sessionKey` A→B | 推荐宿主先置 null 等卸载、`clearAppContext()` 后再更新;直接 A→B 时包装组件先作废并卸载 A、确认完成后才写 B 的 context 并挂载 |
|
|
866
|
+
| `sessionKey` → `null` | 立即作废旧加载并卸载;保持空容器不再请求;宿主随后 `clearAppContext()` 并移除/禁用缓存的私有页面 |
|
|
867
|
+
| 同会话重渲染 / 只换 `appProps` 引用 | 不重挂、不重复 loadRemote;业务数据仍是上次挂载快照 |
|
|
868
|
+
| 点错误占位「重试加载」 | 同页重建尝试(成功模块走运行时缓存;失败入口按现有机制换 URL 重取) |
|
|
869
|
+
|
|
870
|
+
- **多实例与页面级单会话**:同页多个同 spec 实例并存合法(契约按 el 分键);`AppContext` 是页面级单例——同页所有受控桥接实例必须同一会话,后挂实例与活跃实例代次不一致按 `MFU-017` 拒绝(不让两实例互相覆盖身份)。不承诺同页同时承载两个账号。
|
|
871
|
+
- **DOM 所有权**:包装组件只创建并保持稳定的空挂载容器;pending/error 占位是它的兄弟节点,宿主重渲染不 patch 子应用 root 内部。React 宿主 StrictMode 双 effect(mount→cleanup→mount)安全。Vue `<KeepAlive>` 的 deactivate 不是卸载——缓存页中的子应用保有 root 与状态;需要离页即销毁就别缓存该页,登出流程应同时移除缓存的私有页面。
|
|
872
|
+
- **旧请求不冒充取消**:已进入 `loadRemote` 的工作不因桥接层作废而被取消——迟到的旧结果按代次丢弃(不 mount、不覆盖、无未处理拒绝);远程 `onSession` 必须遵守既有 `signal.aborted` 契约(异步等待后、写私有状态前检查信号)。
|
|
873
|
+
|
|
753
874
|
### 9. `AppContext` — 跨应用传值与方法引用(`@fulgurjs/federation/runtime`)
|
|
754
875
|
|
|
755
876
|
宿主向子应用传值、子应用向宿主反向注册方法,一律走这条一等公民通道(对标乾坤 `props`,但带类型与错误契约)——不再各自挂 `window.*` 裸口子。
|
|
@@ -1108,7 +1229,8 @@ const Panel = await loadRemote('shop/Panel', {
|
|
|
1108
1229
|
|
|
1109
1230
|
## 边界(明确不支持)
|
|
1110
1231
|
|
|
1111
|
-
- Vue 3 与 React 18–19 的**浏览器客户端**联邦为支持面;不支持 SSR / React Server Components / Next.js 全栈 / React Native / Node
|
|
1232
|
+
- Vue 3 与 React 18–19 的**浏览器客户端**联邦为支持面;不支持 SSR / React Server Components / Next.js 全栈 / React Native / Node 服务端加载远程。**跨框架边界(5.3.0 起)**:子应用级互嵌**已支持**(§8.2 `/bridge`);**组件级混渲染**(Vue 模板直接渲染 React 组件或反之)不支持——那是 veaury 类框架桥接库的产品。两框架各自纯项目互不引入对方;跨框架消费**纯 TS 模块**(如 Vue 宿主加载 React 远程的 utils)可用
|
|
1233
|
+
- **桥接的隔离边界(§8.2 如实声明)**:桥接只隔离两棵组件树的挂卸边界,不提供浏览器 realm 隔离——远程全局 CSS、`body`/`html` 样式、全局变量、经 React Portal / Vue Teleport 渲染到容器外的 DOM 仍影响宿主,`unmount` 不承诺撤销浏览器已加载的共享 CSS(样式命名空间与全局副作用清理是接入方责任)。子应用内部错误不冒泡进宿主错误边界(跨 root);子应用路由用 memory 路由,v1 **不与宿主 URL 同步**(刷新不恢复子应用内部路径,不计为深链)
|
|
1112
1234
|
- React 侧不承诺组件保活:`createReactHostPages` 不提供 `keepAliveNames`(Vue 的 KeepAlive 专属);页面表里的 `keepAlive` 字段在 React 侧只作普通扩展位。重复打开已下载页面的模块复用照常
|
|
1113
1235
|
- 跨源 Fast Refresh(5.2.0 修复):远程 React 组件修改(文本/样式/Hooks 结构不变的兼容改动)自动热更新到正在显示的宿主页面并保留组件本地状态,普通 TS 模块修改自动传播到引用它的组件边界——零手动刷新(插件保证全页单一 react-refresh 实例)。React Refresh 不兼容的导出/Hooks 结构变化、Vite 要求 full-reload 的改动按框架标准重新挂载/整页刷新;不承诺任意改动保活
|
|
1114
1236
|
- 不兼容 originjs 的 `virtual:__federation__` 旧写法
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/bridge-app-react.ts
|
|
31
|
+
var bridge_app_react_exports = {};
|
|
32
|
+
__export(bridge_app_react_exports, {
|
|
33
|
+
defineBridgeApp: () => defineBridgeApp
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(bridge_app_react_exports);
|
|
36
|
+
var import_react = require("react");
|
|
37
|
+
|
|
38
|
+
// src/runtime/errors.ts
|
|
39
|
+
var FgError = class extends Error {
|
|
40
|
+
code;
|
|
41
|
+
details;
|
|
42
|
+
constructor(code, message, details, options) {
|
|
43
|
+
super(`[fulgurjs:${code}] ${message}`, options);
|
|
44
|
+
this.name = "FgError";
|
|
45
|
+
this.code = code;
|
|
46
|
+
this.details = details;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/bridge-errors.ts
|
|
51
|
+
var BridgeErrorCodes = {
|
|
52
|
+
/** 桥接契约非法(./bridge 默认导出缺 mount/unmount 或不是函数) */
|
|
53
|
+
BRIDGE_INVALID_CONTRACT: "MFU-015",
|
|
54
|
+
/** 桥接准备或生命周期失败(details.phase = getContext | mount | unmount) */
|
|
55
|
+
BRIDGE_LIFECYCLE_FAILED: "MFU-016",
|
|
56
|
+
/** 桥接会话参数与 AppContext 不一致(受控 sessionKey 与全局会话矛盾) */
|
|
57
|
+
BRIDGE_SESSION_MISMATCH: "MFU-017"
|
|
58
|
+
};
|
|
59
|
+
function bridgeLifecycleError(phase, spec, cause, extra) {
|
|
60
|
+
const raw = cause instanceof Error ? cause.message : String(cause ?? "unknown");
|
|
61
|
+
const fixes = {
|
|
62
|
+
getContext: "\u4FEE\u6CD5: \u2460 getContext \u5FC5\u987B\u662F\u540C\u6B65\u3001\u65E0\u526F\u4F5C\u7528\u7684 getter\uFF08\u4E0D\u5F97\u8FD4\u56DE Promise/thenable\uFF09\uFF1B\n \u2461 \u8FD4\u56DE\u503C\u987B\u662F\u5305\u542B\u672C\u6B21\u4F1A\u8BDD\u5FEB\u7167\u7684\u5BF9\u8C61\uFF08\u53D7\u63A7 sessionKey \u65F6\u987B\u542B\u76F8\u540C sessionKey\uFF09\uFF1B\n \u2462 \u5F02\u6B65\u83B7\u53D6\u7528\u6237\u8D44\u6599\u8BF7\u5728\u5BBF\u4E3B\u5B8C\u6210\u540E\u518D\u8BA9\u6865\u63A5\u7EC4\u4EF6\u8FDB\u5165\u53EF\u6302\u8F7D\u72B6\u6001\u3002",
|
|
63
|
+
mount: "\u4FEE\u6CD5: \u2460 \u67E5\u770B\u6839\u56E0\u4E2D\u5B50\u5E94\u7528 mount \u7684\u539F\u59CB\u9519\u8BEF\uFF08\u5DE5\u5382\u629B\u9519\u6216\u9996\u6B21\u6E32\u67D3\u5931\u8D25\uFF09\uFF1B\n \u2461 \u540C\u4E00\u5BB9\u5668\u672A\u5378\u8F7D\u524D\u91CD\u590D mount \u662F\u5951\u7EA6\u8FDD\u4F8B\u2014\u2014\u5148 unmount \u518D mount\uFF1B\n \u2462 \u4FEE\u590D\u540E\u70B9\u51FB\u300C\u91CD\u8BD5\u52A0\u8F7D\u300D\u5728\u540C\u9875\u91CD\u5EFA\u6302\u8F7D\u3002",
|
|
64
|
+
unmount: "\u4FEE\u6CD5: \u2460 \u6839\u56E0\u6765\u81EA\u5B50\u5E94\u7528 unmount \u5B9E\u73B0\uFF08app.unmount/root.unmount \u629B\u9519\uFF09\uFF1B\n \u2461 \u8BE5\u5BB9\u5668\u6E05\u7406\u72B6\u6001\u4E0D\u786E\u5B9A\u2014\u2014\u4E0D\u8981\u76F4\u63A5\u5728\u540C\u4E00 el \u4E0A\u542F\u52A8\u65B0\u5B9E\u4F8B\uFF1B\n \u2462 \u5237\u65B0\u9875\u9762\u53EF\u5F7B\u5E95\u6E05\u7406\uFF0C\u6B8B\u7559\u8D44\u6E90\u8BF7\u5982\u5B9E\u6392\u67E5\uFF08\u4E8B\u4EF6\u8BA2\u9605/\u5B9A\u65F6\u5668/\u5168\u5C40\u526F\u4F5C\u7528\uFF09\u3002"
|
|
65
|
+
};
|
|
66
|
+
return new FgError(
|
|
67
|
+
BridgeErrorCodes.BRIDGE_LIFECYCLE_FAILED,
|
|
68
|
+
`\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 ${phase} \u9636\u6BB5\u5931\u8D25\u3002
|
|
69
|
+
\u6839\u56E0: ${raw}
|
|
70
|
+
` + fixes[phase],
|
|
71
|
+
{ phase, spec, cause: String(raw), ...extra },
|
|
72
|
+
{ cause }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/bridge-app-react.ts
|
|
77
|
+
var entriesByEl = /* @__PURE__ */ new WeakMap();
|
|
78
|
+
var FirstCommitGate = class extends import_react.Component {
|
|
79
|
+
state = { error: null };
|
|
80
|
+
static getDerivedStateFromError(error) {
|
|
81
|
+
return { error };
|
|
82
|
+
}
|
|
83
|
+
componentDidCatch(error) {
|
|
84
|
+
this.props.onError(error);
|
|
85
|
+
if (this.props.phaseRef.current === "committed") throw error;
|
|
86
|
+
}
|
|
87
|
+
render() {
|
|
88
|
+
if (this.props.phaseRef.current === "committed") return this.props.children;
|
|
89
|
+
return this.state.error === null ? this.props.children : null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
function CommitProbe({ children, onCommit }) {
|
|
93
|
+
(0, import_react.useEffect)(() => {
|
|
94
|
+
onCommit();
|
|
95
|
+
}, []);
|
|
96
|
+
return children ?? null;
|
|
97
|
+
}
|
|
98
|
+
function defineBridgeApp(factory) {
|
|
99
|
+
return {
|
|
100
|
+
mount(el, props) {
|
|
101
|
+
if (entriesByEl.has(el)) {
|
|
102
|
+
throw bridgeLifecycleError("mount", "bridge", "\u540C\u4E00\u5BB9\u5668 el \u5DF2\u6302\u8F7D\u672C\u6865\u63A5\u5E94\u7528\uFF08\u5BB9\u5668\u5DF2\u88AB\u5360\u7528\uFF09\u3002", {
|
|
103
|
+
reason: "container-occupied"
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const snapshot = { ...props ?? {} };
|
|
107
|
+
let element;
|
|
108
|
+
try {
|
|
109
|
+
element = factory(snapshot);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
throw bridgeLifecycleError("mount", "bridge", e instanceof Error ? e : String(e));
|
|
112
|
+
}
|
|
113
|
+
if (element === null || typeof element !== "object") {
|
|
114
|
+
throw bridgeLifecycleError(
|
|
115
|
+
"mount",
|
|
116
|
+
"bridge",
|
|
117
|
+
`defineBridgeApp \u5DE5\u5382\u8FD4\u56DE\u503C\u4E0D\u662F ReactElement\uFF08\u5F53\u524D ${element === null ? "null" : typeof element}\uFF09\u3002`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const entry = { status: "pending" };
|
|
121
|
+
entriesByEl.set(el, entry);
|
|
122
|
+
const promise = (async () => {
|
|
123
|
+
const reactDomClient = await import("react-dom/client");
|
|
124
|
+
if (entry.status === "abort" || entriesByEl.get(el) !== entry) return;
|
|
125
|
+
const root = reactDomClient.createRoot(el);
|
|
126
|
+
entry.root = root;
|
|
127
|
+
entry.status = "live";
|
|
128
|
+
const phaseRef = { current: "pending" };
|
|
129
|
+
await new Promise((resolve, reject) => {
|
|
130
|
+
let settled = false;
|
|
131
|
+
const onError = (e) => {
|
|
132
|
+
if (settled) return;
|
|
133
|
+
settled = true;
|
|
134
|
+
reject(bridgeLifecycleError("mount", "bridge", e instanceof Error ? e : String(e)));
|
|
135
|
+
};
|
|
136
|
+
const onCommit = () => {
|
|
137
|
+
if (settled) return;
|
|
138
|
+
settled = true;
|
|
139
|
+
phaseRef.current = "committed";
|
|
140
|
+
resolve();
|
|
141
|
+
};
|
|
142
|
+
root.render(
|
|
143
|
+
(0, import_react.createElement)(
|
|
144
|
+
FirstCommitGate,
|
|
145
|
+
{ onError, phaseRef },
|
|
146
|
+
(0, import_react.createElement)(CommitProbe, { onCommit, children: element })
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
})();
|
|
151
|
+
return promise.then(
|
|
152
|
+
void 0,
|
|
153
|
+
(e) => {
|
|
154
|
+
if (entry.root && entry.status === "live") {
|
|
155
|
+
try {
|
|
156
|
+
entry.root.unmount();
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (entriesByEl.get(el) === entry) entriesByEl.delete(el);
|
|
161
|
+
throw e;
|
|
162
|
+
}
|
|
163
|
+
).then(() => void 0);
|
|
164
|
+
},
|
|
165
|
+
unmount(el) {
|
|
166
|
+
const entry = entriesByEl.get(el);
|
|
167
|
+
if (!entry) return;
|
|
168
|
+
entriesByEl.delete(el);
|
|
169
|
+
if (entry.status === "pending" || !entry.root) {
|
|
170
|
+
entry.status = "abort";
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
entry.status = "abort";
|
|
174
|
+
try {
|
|
175
|
+
entry.root.unmount();
|
|
176
|
+
} catch (e) {
|
|
177
|
+
throw bridgeLifecycleError("unmount", "bridge", e instanceof Error ? e : String(e));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
183
|
+
0 && (module.exports = {
|
|
184
|
+
defineBridgeApp
|
|
185
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { BridgeApp } from './bridge-core.cjs';
|
|
3
|
+
import './context.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* React 子应用桥接适配(/react 导出 defineBridgeApp;任务书 §3.3 D2)。
|
|
7
|
+
*
|
|
8
|
+
* 工厂形态:接收 props 快照,返回 ReactElement;helper 负责 createRoot(el).render(...) /
|
|
9
|
+
* root.unmount() 与按容器 el 的实例跟踪。
|
|
10
|
+
*
|
|
11
|
+
* 首次根提交语义(任务书 §3.2/§3.3,BN02):
|
|
12
|
+
* - root.render() 返回不代表首次提交完成——helper 用「提交探针 + 首次渲染错误边界」兑现
|
|
13
|
+
* Promise 语义:mount 的 Promise 在探针 effect(首次提交后触发)时才 resolve;
|
|
14
|
+
* - 首次提交前的渲染错误经错误边界拒绝 mount 并清理 root,宿主转 MFU-016(phase: mount);
|
|
15
|
+
* - 首次提交后的错误不再由门控吞掉(settled 后原样上抛,交回 React 原生语义),
|
|
16
|
+
* 子应用内部错误由子应用自己的错误边界负责(§4.4)。
|
|
17
|
+
*
|
|
18
|
+
* react-dom/client 在实际 mount 时按需动态取得(BN04/BR01):
|
|
19
|
+
* 只使用 /react 既有 API 的消费者不因本文件存在而提前加载 react-dom/client 与桥接挂载逻辑。
|
|
20
|
+
*
|
|
21
|
+
* 本文件零 Vue:/react 导入图隔离由 tests/runtime-entry-graph.test.ts 守护。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** React 子应用工厂:接收挂载时 props 快照,返回 ReactElement(路由等由调用方自行包裹) */
|
|
25
|
+
type ReactBridgeAppFactory = (props: Record<string, unknown>) => ReactElement;
|
|
26
|
+
/**
|
|
27
|
+
* 定义 React 子应用的桥接契约(远程 ./bridge 模块默认导出)。
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* import { MemoryRouter } from 'react-router-dom'
|
|
31
|
+
* import { defineBridgeApp } from '@fulgurjs/federation/react'
|
|
32
|
+
* export default defineBridgeApp((props) => <MemoryRouter><App {...props} /></MemoryRouter>)
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
declare function defineBridgeApp(factory: ReactBridgeAppFactory): BridgeApp;
|
|
36
|
+
|
|
37
|
+
export { BridgeApp, type ReactBridgeAppFactory, defineBridgeApp };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { BridgeApp } from './bridge-core.js';
|
|
3
|
+
import './context.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* React 子应用桥接适配(/react 导出 defineBridgeApp;任务书 §3.3 D2)。
|
|
7
|
+
*
|
|
8
|
+
* 工厂形态:接收 props 快照,返回 ReactElement;helper 负责 createRoot(el).render(...) /
|
|
9
|
+
* root.unmount() 与按容器 el 的实例跟踪。
|
|
10
|
+
*
|
|
11
|
+
* 首次根提交语义(任务书 §3.2/§3.3,BN02):
|
|
12
|
+
* - root.render() 返回不代表首次提交完成——helper 用「提交探针 + 首次渲染错误边界」兑现
|
|
13
|
+
* Promise 语义:mount 的 Promise 在探针 effect(首次提交后触发)时才 resolve;
|
|
14
|
+
* - 首次提交前的渲染错误经错误边界拒绝 mount 并清理 root,宿主转 MFU-016(phase: mount);
|
|
15
|
+
* - 首次提交后的错误不再由门控吞掉(settled 后原样上抛,交回 React 原生语义),
|
|
16
|
+
* 子应用内部错误由子应用自己的错误边界负责(§4.4)。
|
|
17
|
+
*
|
|
18
|
+
* react-dom/client 在实际 mount 时按需动态取得(BN04/BR01):
|
|
19
|
+
* 只使用 /react 既有 API 的消费者不因本文件存在而提前加载 react-dom/client 与桥接挂载逻辑。
|
|
20
|
+
*
|
|
21
|
+
* 本文件零 Vue:/react 导入图隔离由 tests/runtime-entry-graph.test.ts 守护。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** React 子应用工厂:接收挂载时 props 快照,返回 ReactElement(路由等由调用方自行包裹) */
|
|
25
|
+
type ReactBridgeAppFactory = (props: Record<string, unknown>) => ReactElement;
|
|
26
|
+
/**
|
|
27
|
+
* 定义 React 子应用的桥接契约(远程 ./bridge 模块默认导出)。
|
|
28
|
+
*
|
|
29
|
+
* ```tsx
|
|
30
|
+
* import { MemoryRouter } from 'react-router-dom'
|
|
31
|
+
* import { defineBridgeApp } from '@fulgurjs/federation/react'
|
|
32
|
+
* export default defineBridgeApp((props) => <MemoryRouter><App {...props} /></MemoryRouter>)
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
declare function defineBridgeApp(factory: ReactBridgeAppFactory): BridgeApp;
|
|
36
|
+
|
|
37
|
+
export { BridgeApp, type ReactBridgeAppFactory, defineBridgeApp };
|