@fulgurjs/federation 6.8.6 → 6.9.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.
Files changed (139) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.en.md +3 -0
  3. package/README.md +3 -0
  4. package/dist/application-core-BuhXQmLF.d.cts +16 -0
  5. package/dist/application-core-DWI3eKme.d.ts +16 -0
  6. package/dist/bridge-app-react.cjs +177 -9
  7. package/dist/bridge-app-react.d.cts +15 -3
  8. package/dist/bridge-app-react.d.ts +15 -3
  9. package/dist/bridge-app-react.js +101 -9
  10. package/dist/bridge-app-vue.cjs +138 -12
  11. package/dist/bridge-app-vue.d.cts +20 -5
  12. package/dist/bridge-app-vue.d.ts +20 -5
  13. package/dist/bridge-app-vue.js +62 -12
  14. package/dist/bridge-host-react.d.cts +1 -1
  15. package/dist/bridge-host-react.d.ts +1 -1
  16. package/dist/bridge-host-vue.d.cts +1 -1
  17. package/dist/bridge-host-vue.d.ts +1 -1
  18. package/dist/chunk-6OB47FSR.js +81 -0
  19. package/dist/cli.js +350 -24
  20. package/dist/index.cjs +7 -6
  21. package/dist/index.js +7 -6
  22. package/dist/react-adapter.d.cts +1 -1
  23. package/dist/react-adapter.d.ts +1 -1
  24. package/dist/react.d.ts +29 -4
  25. package/dist/react.js +1 -0
  26. package/dist/{remote-types-xf1B6_pK.d.ts → remote-types-COmBhjRc.d.ts} +1 -1
  27. package/dist/{remote-types-58hdbx6x.d.cts → remote-types-CStGOJwu.d.cts} +1 -1
  28. package/dist/runtime.js +1 -1
  29. package/dist/vue-adapter.d.cts +1 -1
  30. package/dist/vue-adapter.d.ts +1 -1
  31. package/dist/vue-store.cjs +40 -0
  32. package/dist/vue-store.d.cts +4 -0
  33. package/dist/vue-store.d.ts +4 -0
  34. package/dist/vue-store.js +15 -0
  35. package/dist/vue.d.ts +37 -6
  36. package/dist/vue.js +2 -0
  37. package/docs/en/reference/api.md +38 -2
  38. package/docs/en/reference/types.md +27 -0
  39. package/docs/zh/reference/api.md +46 -2
  40. package/docs/zh/reference/types.md +27 -0
  41. package/examples/templates/README.md +3 -0
  42. package/examples/templates/react-host-vue-remote/README.md +24 -0
  43. package/examples/templates/react-host-vue-remote/package.json +2 -1
  44. package/examples/templates/react-host-vue-remote/pnpm-lock.yaml +7 -7
  45. package/examples/templates/react-host-vue-remote/pnpm-workspace.yaml +1 -1
  46. package/examples/templates/react-host-vue-remote/react-host/package.json +1 -1
  47. package/examples/templates/react-host-vue-remote/react-host/src/application.tsx +62 -0
  48. package/examples/templates/react-host-vue-remote/react-host/src/main.tsx +2 -62
  49. package/examples/templates/react-host-vue-remote/scripts/prod.mjs +101 -0
  50. package/examples/templates/react-host-vue-remote/vue-remote/README.md +17 -16
  51. package/examples/templates/react-host-vue-remote/vue-remote/fulgurjs.config.ts +2 -2
  52. package/examples/templates/react-host-vue-remote/vue-remote/package.json +1 -1
  53. package/examples/templates/react-host-vue-remote/vue-remote/src/AboutPage.vue +1 -0
  54. package/examples/templates/react-host-vue-remote/vue-remote/src/App.vue +15 -5
  55. package/examples/templates/react-host-vue-remote/vue-remote/src/HomePage.vue +1 -0
  56. package/examples/templates/react-host-vue-remote/vue-remote/src/application.ts +19 -0
  57. package/examples/templates/react-host-vue-remote/vue-remote/src/main.ts +2 -15
  58. package/examples/templates/react-react/README.md +25 -1
  59. package/examples/templates/react-react/host/package.json +1 -1
  60. package/examples/templates/react-react/host/src/application.tsx +33 -0
  61. package/examples/templates/react-react/host/src/main.tsx +3 -32
  62. package/examples/templates/react-react/package.json +2 -1
  63. package/examples/templates/react-react/pnpm-lock.yaml +7 -7
  64. package/examples/templates/react-react/pnpm-workspace.yaml +1 -1
  65. package/examples/templates/react-react/remote/package.json +1 -1
  66. package/examples/templates/react-react/remote/src/application.tsx +11 -0
  67. package/examples/templates/react-react/remote/src/main.tsx +3 -10
  68. package/examples/templates/react-react/scripts/prod.mjs +101 -0
  69. package/examples/templates/showcase/README.md +35 -7
  70. package/examples/templates/showcase/package.json +2 -1
  71. package/examples/templates/showcase/pnpm-lock.yaml +11 -11
  72. package/examples/templates/showcase/pnpm-workspace.yaml +1 -1
  73. package/examples/templates/showcase/react-host/package.json +1 -1
  74. package/examples/templates/showcase/react-host/src/application.tsx +28 -0
  75. package/examples/templates/showcase/react-host/src/demo-log.ts +5 -8
  76. package/examples/templates/showcase/react-host/src/main.tsx +2 -33
  77. package/examples/templates/showcase/react-host/src/routing.ts +2 -2
  78. package/examples/templates/showcase/react-remote/fulgurjs.config.ts +1 -1
  79. package/examples/templates/showcase/react-remote/package.json +1 -1
  80. package/examples/templates/showcase/react-remote/src/ChildLayout.tsx +3 -2
  81. package/examples/templates/showcase/react-remote/src/{bridge.tsx → application.tsx} +21 -21
  82. package/examples/templates/showcase/react-remote/src/child-bus.ts +23 -23
  83. package/examples/templates/showcase/react-remote/src/main.tsx +2 -18
  84. package/examples/templates/showcase/react-remote/src/pages/Locked.tsx +2 -1
  85. package/examples/templates/showcase/react-remote/src/pages/OrderDetail.tsx +2 -1
  86. package/examples/templates/showcase/react-remote/src/pages/OrderList.tsx +2 -1
  87. package/examples/templates/showcase/scripts/prod.mjs +101 -0
  88. package/examples/templates/showcase/vue-host/package.json +1 -1
  89. package/examples/templates/showcase/vue-host/src/application.ts +28 -0
  90. package/examples/templates/showcase/vue-host/src/demo-log.ts +4 -7
  91. package/examples/templates/showcase/vue-host/src/main.ts +2 -30
  92. package/examples/templates/showcase/vue-host/src/routing.ts +2 -2
  93. package/examples/templates/showcase/vue-remote/fulgurjs.config.ts +1 -1
  94. package/examples/templates/showcase/vue-remote/package.json +1 -1
  95. package/examples/templates/showcase/vue-remote/src/ChildLayout.vue +3 -2
  96. package/examples/templates/showcase/vue-remote/src/{bridge.ts → application.ts} +10 -13
  97. package/examples/templates/showcase/vue-remote/src/child-bus.ts +23 -23
  98. package/examples/templates/showcase/vue-remote/src/main.ts +2 -16
  99. package/examples/templates/showcase/vue-remote/src/pages/Locked.vue +2 -1
  100. package/examples/templates/showcase/vue-remote/src/pages/OrderDetail.vue +2 -1
  101. package/examples/templates/showcase/vue-remote/src/pages/OrderList.vue +2 -1
  102. package/examples/templates/vue-host-react-remote/README.md +24 -0
  103. package/examples/templates/vue-host-react-remote/package.json +2 -1
  104. package/examples/templates/vue-host-react-remote/pnpm-lock.yaml +7 -7
  105. package/examples/templates/vue-host-react-remote/pnpm-workspace.yaml +1 -1
  106. package/examples/templates/vue-host-react-remote/react-remote/README.md +17 -17
  107. package/examples/templates/vue-host-react-remote/react-remote/fulgurjs.config.ts +2 -2
  108. package/examples/templates/vue-host-react-remote/react-remote/package.json +1 -1
  109. package/examples/templates/vue-host-react-remote/react-remote/src/{bridge.tsx → application.tsx} +13 -24
  110. package/examples/templates/vue-host-react-remote/react-remote/src/main.tsx +2 -7
  111. package/examples/templates/vue-host-react-remote/scripts/prod.mjs +101 -0
  112. package/examples/templates/vue-host-react-remote/vue-host/package.json +1 -1
  113. package/examples/templates/vue-host-react-remote/vue-host/src/application.ts +5 -0
  114. package/examples/templates/vue-host-react-remote/vue-host/src/main.ts +2 -4
  115. package/examples/templates/vue-vue/README.md +25 -1
  116. package/examples/templates/vue-vue/host/package.json +1 -1
  117. package/examples/templates/vue-vue/host/src/application.ts +54 -0
  118. package/examples/templates/vue-vue/host/src/main.ts +3 -50
  119. package/examples/templates/vue-vue/package.json +2 -1
  120. package/examples/templates/vue-vue/pnpm-lock.yaml +7 -7
  121. package/examples/templates/vue-vue/pnpm-workspace.yaml +1 -1
  122. package/examples/templates/vue-vue/remote/package.json +1 -1
  123. package/examples/templates/vue-vue/remote/src/application.ts +6 -0
  124. package/examples/templates/vue-vue/remote/src/main.ts +3 -5
  125. package/examples/templates/vue-vue/scripts/prod.mjs +101 -0
  126. package/package.json +14 -9
  127. package/examples/templates/react-host-vue-remote/react-host/src/fulgurjs/types/bridge-vue-remote/metadata.json +0 -15
  128. package/examples/templates/react-host-vue-remote/react-host/src/fulgurjs/types/bridge-vue-remote/modules.d.ts +0 -14
  129. package/examples/templates/react-host-vue-remote/react-host/src/fulgurjs/types/bridge-vue-remote/registry.d.ts +0 -10
  130. package/examples/templates/react-host-vue-remote/vue-remote/src/bridge.ts +0 -57
  131. package/examples/templates/showcase/react-host/src/fulgurjs/types/vue-remote/metadata.json +0 -16
  132. package/examples/templates/showcase/react-host/src/fulgurjs/types/vue-remote/modules.d.ts +0 -71
  133. package/examples/templates/showcase/react-host/src/fulgurjs/types/vue-remote/registry.d.ts +0 -10
  134. package/examples/templates/showcase/vue-host/src/fulgurjs/types/react-remote/metadata.json +0 -16
  135. package/examples/templates/showcase/vue-host/src/fulgurjs/types/react-remote/modules.d.ts +0 -79
  136. package/examples/templates/showcase/vue-host/src/fulgurjs/types/react-remote/registry.d.ts +0 -10
  137. package/examples/templates/vue-host-react-remote/vue-host/src/fulgurjs/types/bridge-react-remote/metadata.json +0 -16
  138. package/examples/templates/vue-host-react-remote/vue-host/src/fulgurjs/types/bridge-react-remote/modules.d.ts +0 -10
  139. package/examples/templates/vue-host-react-remote/vue-host/src/fulgurjs/types/bridge-react-remote/registry.d.ts +0 -10
@@ -1,5 +1,8 @@
1
1
  # 双向桥接 URL 同步(examples/templates/showcase)
2
2
 
3
+ > 本目录是 6.9.0 源码候选:已改用 defineApplication,尚未测试和发布。源码依赖已锁定 6.9.0;正式发布前不要拿旧 registry 包运行这份代码。验收先使用本次候选 tarball,发布后再按下列冻结安装命令验证正式包。
4
+
5
+
3
6
  `@fulgurjs/federation` 桥接 URL 同步的双向旗舰演示(API 见[文档中心](https://github.com/chenmingye/fulgurjs-federation/blob/master/docs/README.md)):
4
7
  两个方向的宿主 × 子应用各一套——
5
8
 
@@ -82,12 +85,12 @@ curl --noproxy '*' -s -o /dev/null -w '%{http_code}\n' http://localhost:5335/@fu
82
85
  | 插件公开 API | 入口 | 本演示接线位置 |
83
86
  |---|---|---|
84
87
  | `createVueBridgeApp(spec, options)` | `@fulgurjs/federation/vue` | `vue-host/src/pages/BridgeReactPage.vue`(`<RemoteReactBridge :routing="routing" :app-props="appProps" />`) |
85
- | `createVueBridgeNavigation(router)` | `@fulgurjs/federation/vue` | `vue-host/src/routing.ts`(模块级单例端口,main.ts 组装后 `initBridgeRouting(router)`) |
86
- | `connectVueBridgeRouter(routing, router, { signal })` → `await conn.ready` 后 `app.use(router)` | `@fulgurjs/federation/vue` | `vue-remote/src/bridge.ts`(异步工厂:初始 push 落定后再 install,顺序不能反) |
88
+ | `createVueBridgeNavigation(router)` | `@fulgurjs/federation/vue` | `vue-host/src/routing.ts`(宿主导航端口,application.ts 组装后 `initBridgeRouting(router)`) |
89
+ | `defineApplication(factory, { routing: true })` → 返回 `{ app, router }` | `@fulgurjs/federation/vue` | `vue-remote/src/application.ts`(根据 mode 选择 web/memory history;插件等待初始路由并安装) |
87
90
  | `createReactBridgeApp(spec, options)` | `@fulgurjs/federation/react` | `react-host/src/pages/BridgeVuePage.tsx`(`<RemoteVueBridge routing={routing} appProps={appProps} />`) |
88
91
  | `createReactBridgeNavigation(router, { canNavigate?, basename? })`(仅 data router) | `@fulgurjs/federation/react` | `react-host/src/routing.ts`(`createBrowserRouter` 实例组装后初始化) |
89
- | `createReactBridgeRouter(routing, routes, { signal })` → `{ element }` 直接作为工厂返回值 | `@fulgurjs/federation/react` | `react-remote/src/bridge.tsx`(`/` 经 loader `redirect('/orders')` 以 replace 规范化) |
90
- | `defineBridgeApp(factory, { routing: true })`(协议声明,缺失即 MFU-031) | `@fulgurjs/federation/vue`(Vue)/ `@fulgurjs/federation/react`(React) | `vue-remote/src/bridge.ts`、`react-remote/src/bridge.tsx`(工厂内校验 `ctx?.routing`,`ctx.signal` 传入接线自动 dispose) |
92
+ | `createReactBridgeRouter(routing, routes, { signal })` → `{ element, dispose }` 作为工厂返回值 | `@fulgurjs/federation/react` | `react-remote/src/application.tsx`(`/` 经 loader `redirect('/orders')` 以 replace 规范化) |
93
+ | `defineApplication(factory, { routing: true })`(协议声明,缺失即 MFU-031) | `@fulgurjs/federation/vue`(Vue)/ `@fulgurjs/federation/react`(React) | `vue-remote/src/application.ts`、`react-remote/src/application.tsx`(实例 mode/signal;独立与桥接复用同一套路由) |
91
94
  | `BridgeHostRouting`(`{ basePath, navigation }` 组件 prop,独立控制通道不进 appProps) | 类型面见 [API 参考](https://github.com/chenmingye/fulgurjs-federation/blob/master/docs/zh/reference/api.md) | `vue-host/src/routing.ts`、`react-host/src/routing.ts`(basePath 常量 `BRIDGE_BASE_PATH`) |
92
95
  | `BridgeChildRoute` / `BridgeLocation`(通道与位置契约) | `@fulgurjs/federation/vue`、`@fulgurjs/federation/react` | 由插件随 mount 第三参数注入子应用;两端仓库源码 `packages/plugin/src/bridge-router-core.ts` |
93
96
 
@@ -103,7 +106,7 @@ curl --noproxy '*' -s -o /dev/null -w '%{http_code}\n' http://localhost:5335/@fu
103
106
  ## 构建 / 类型检查
104
107
 
105
108
  ```bash
106
- # 4 个工程均独立通过(在各自目录执行)
109
+ # 在各应用目录执行;这是验收命令,不代表本轮已经通过
107
110
  pnpm run types # 远程类型同步/验证(宿主同步远程声明;提供方验证生成)
108
111
  pnpm run typecheck # React 工程 tsc --noEmit;Vue 工程 vue-tsc --noEmit
109
112
  pnpm run build
@@ -117,7 +120,7 @@ pnpm run build
117
120
  2. 宿主 `fulgurjs.config.ts` 里 `remotes` 的 dev 地址(`vue-host` 指向 react-remote 的 5333;`react-host` 指向 vue-remote 的 5335);
118
121
  3. 根 `scripts/dev.config.json` 里该应用的 `port`(**必改**:启动器预检/探活都读它)。
119
122
 
120
- 生产部署形态(重要):两个宿主的路由都没有配置 base,**不能部署到同一站点的根**(两份 `index.html` 会互相覆盖)。按两个站点部署——
123
+ 生产部署形态(重要):两个宿主的默认构建 base 都是根,**不能部署到同一站点的根**(两份 `index.html` 会互相覆盖)。按两个站点部署——
121
124
 
122
125
  - 站点 A:`vue-host/dist` 部署在根,`react-remote/dist` 部署在同站 `/react-remote/` 子路径(vue-host 的 prod 地址);
123
126
  - 站点 B:`react-host/dist` 部署在另一个站点的根,`vue-remote/dist` 部署在该站 `/vue-remote/` 子路径(react-host 的 prod 地址);
@@ -127,4 +130,29 @@ pnpm run build
127
130
 
128
131
  ## 查看应用运行模式
129
132
 
130
- 子应用显示“当前应用被桥接:true”;Vue 宿主观测台显示“当前宿主被桥接:false”。两者同页并存也不会串模式。Vue 在组件 setup 中调用 `hasBridgeMount(getCurrentInstance()?.appContext.app)`;React 在桥接工厂中查询 `hasBridgeMount(ctx)`,通过 props 传给布局。独立 createApp 实例的查询结果为 false;本模板的子应用独立入口只提供健康检查壳,不展示业务布局。该值描述实例的运行方式,不是当前挂载数量。
133
+ 子应用显示“当前应用被桥接:true”;Vue 宿主观测台显示“当前宿主被桥接:false”。两者同页并存也不会串模式。Vue 在组件 setup 中调用 `hasBridgeMount(getCurrentInstance()?.appContext.app)`;React 在桥接工厂中查询 `hasBridgeMount(ctx)`,通过 props 传给布局。独立启动的查询结果为 false;独立子应用也显示同一套工单列表、详情、设置和导航,不使用健康检查壳。该值描述实例的运行方式,不是当前挂载数量。
134
+
135
+ ## 一键运行生产产物
136
+
137
+ ```bash
138
+ pnpm build
139
+ pnpm prod
140
+ ```
141
+
142
+ `prod` 只服务本次 dist,不启动远程开发服务器。它复用 scripts/dev.config.json 的宿主端口,从实际构建入口推导 base,并把远程静态产物挂在同源子路径。无需为本地复现手写 Nginx;部署真实服务器时仍按部署指南配置。多宿主模板分别启动各宿主地址,不把两个宿主的根资源混在一起。先停止 dev,端口占用会失败并停止本次已起服务;不会关闭已有占用进程。Ctrl+C 关闭本次生产服务。
143
+
144
+ ## 插件代码阅读地图
145
+
146
+ | 文件 | 职责 |
147
+ |---|---|
148
+ | `react-host/src/application.tsx` | 本应用唯一装配;main 只负责调用 start |
149
+ | `react-remote/src/application.tsx` | 本应用唯一装配;main 只负责调用 start |
150
+ | `vue-host/src/application.ts` | 本应用唯一装配;main 只负责调用 start |
151
+ | `vue-remote/src/application.ts` | 本应用唯一装配;main 只负责调用 start |
152
+ | `react-host/fulgurjs.config.ts` | 本应用的远程地址、exposes、实际共享依赖 |
153
+ | `react-remote/fulgurjs.config.ts` | 本应用的远程地址、exposes、实际共享依赖 |
154
+ | `vue-host/fulgurjs.config.ts` | 本应用的远程地址、exposes、实际共享依赖 |
155
+ | `vue-remote/fulgurjs.config.ts` | 本应用的远程地址、exposes、实际共享依赖 |
156
+ | `scripts/dev.mjs` / `scripts/prod.mjs` | 统一开发/生产启动器;业务接入不复制它们到应用源码 |
157
+
158
+ `application` 是新增的插件装配文件,`main` 是已有入口接线;页面组件、业务路由、权限和数据不是每个项目都必写的联邦配置。跨框架基础组合不逐页登记子应用菜单;逐页远程页面模板的页面表仅服务它所展示的逐页场景。`src/fulgurjs/types/` 是插件生成目录,按远程实际版本同步,不维护人工伪造的声明或旧缓存。
@@ -8,6 +8,7 @@
8
8
  "dev:vue-host": "pnpm --dir vue-host dev",
9
9
  "dev:react-host": "pnpm --dir react-host dev",
10
10
  "dev": "node scripts/dev.mjs",
11
- "build": "pnpm -r build"
11
+ "build": "pnpm -r build",
12
+ "prod": "node scripts/prod.mjs"
12
13
  }
13
14
  }
@@ -11,8 +11,8 @@ importers:
11
11
  react-host:
12
12
  dependencies:
13
13
  '@fulgurjs/federation':
14
- specifier: 6.8.5
15
- version: 6.8.5(react-dom@19.3.0(react@19.3.0))(react-router-dom@7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(vite@6.4.4)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))
14
+ specifier: 6.9.0
15
+ version: 6.9.0(react-dom@19.3.0(react@19.3.0))(react-router-dom@7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(vite@6.4.4)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))
16
16
  react:
17
17
  specifier: ^19.1.0
18
18
  version: 19.3.0
@@ -45,8 +45,8 @@ importers:
45
45
  react-remote:
46
46
  dependencies:
47
47
  '@fulgurjs/federation':
48
- specifier: 6.8.5
49
- version: 6.8.5(react-dom@19.3.0(react@19.3.0))(react-router-dom@7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(vite@6.4.4)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))
48
+ specifier: 6.9.0
49
+ version: 6.9.0(react-dom@19.3.0(react@19.3.0))(react-router-dom@7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(vite@6.4.4)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))
50
50
  react:
51
51
  specifier: ^19.1.0
52
52
  version: 19.3.0
@@ -76,8 +76,8 @@ importers:
76
76
  vue-host:
77
77
  dependencies:
78
78
  '@fulgurjs/federation':
79
- specifier: 6.8.5
80
- version: 6.8.5(react-dom@19.3.0(react@19.3.0))(react-router-dom@7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(vite@6.4.4)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))
79
+ specifier: 6.9.0
80
+ version: 6.9.0(react-dom@19.3.0(react@19.3.0))(react-router-dom@7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(vite@6.4.4)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))
81
81
  react:
82
82
  specifier: ^19.1.0
83
83
  version: 19.3.0
@@ -107,8 +107,8 @@ importers:
107
107
  vue-remote:
108
108
  dependencies:
109
109
  '@fulgurjs/federation':
110
- specifier: 6.8.5
111
- version: 6.8.5(react-dom@19.3.0(react@19.3.0))(react-router-dom@7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(vite@6.4.4)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))
110
+ specifier: 6.9.0
111
+ version: 6.9.0(react-dom@19.3.0(react@19.3.0))(react-router-dom@7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(vite@6.4.4)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))
112
112
  vue:
113
113
  specifier: ^3.5.13
114
114
  version: 3.5.43(typescript@5.9.3)
@@ -526,8 +526,8 @@ packages:
526
526
  cpu: [x64]
527
527
  os: [win32]
528
528
 
529
- '@fulgurjs/federation@6.8.5':
530
- resolution: {integrity: sha512-RsaN4n2ZgGBtI9zVQxDclS3QxYVKAZiCGipXR3umreAg651tIYCmtqkVjqPELSCPW9tg/rYQvzzs/SOV6ZiQfg==}
529
+ '@fulgurjs/federation@6.9.0':
530
+ resolution: {tarball: https://registry.npmjs.org/@fulgurjs/federation/-/federation-6.9.0.tgz}
531
531
  engines: {node: '>=18'}
532
532
  hasBin: true
533
533
  peerDependencies:
@@ -1348,7 +1348,7 @@ snapshots:
1348
1348
  '@esbuild/win32-x64@0.27.7':
1349
1349
  optional: true
1350
1350
 
1351
- '@fulgurjs/federation@6.8.5(react-dom@19.3.0(react@19.3.0))(react-router-dom@7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(vite@6.4.4)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))':
1351
+ '@fulgurjs/federation@6.9.0(react-dom@19.3.0(react@19.3.0))(react-router-dom@7.18.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(vite@6.4.4)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))':
1352
1352
  dependencies:
1353
1353
  es-module-lexer: 1.7.0
1354
1354
  esbuild: 0.27.7
@@ -4,7 +4,7 @@ packages:
4
4
  - react-host
5
5
  - react-remote
6
6
  minimumReleaseAgeExclude:
7
- - '@fulgurjs/federation@6.8.5'
7
+ - '@fulgurjs/federation@6.9.0'
8
8
  # pnpm >=12 构建脚本批准(12 起弃用 onlyBuiltDependencies;pnpm 9-11 忽略本键:
9
9
  # esbuild >=0.16 二进制经 optionalDependencies 平台包分发,postinstall 仅校验,不阻塞安装)
10
10
  allowBuilds:
@@ -11,7 +11,7 @@
11
11
  "types": "fulgurjs types"
12
12
  },
13
13
  "dependencies": {
14
- "@fulgurjs/federation": "6.8.5",
14
+ "@fulgurjs/federation": "6.9.0",
15
15
  "react": "^19.1.0",
16
16
  "react-dom": "^19.1.0",
17
17
  "react-router-dom": "^7.1.0",
@@ -0,0 +1,28 @@
1
+ import { defineApplication } from '@fulgurjs/federation/react'
2
+ import { createBrowserRouter, RouterProvider } from 'react-router-dom'
3
+ import Layout from './Layout'
4
+ import HomePage from './pages/HomePage'
5
+ import AboutPage from './pages/AboutPage'
6
+ import BridgeVuePage from './pages/BridgeVuePage'
7
+ import { initBridgeRouting } from './routing'
8
+ import { installPopStateLogger } from './demo-log'
9
+
10
+ export default defineApplication((_props, { signal }) => {
11
+ const router = createBrowserRouter([
12
+ {
13
+ path: '/',
14
+ element: <Layout />,
15
+ children: [
16
+ { index: true, element: <HomePage /> },
17
+ { path: 'about', element: <AboutPage /> },
18
+ // catch-all:子应用挂载在 /br-vue 前缀下,内部导航全部命中同一条路由记录
19
+ { path: 'br-vue/*', element: <BridgeVuePage /> },
20
+ ],
21
+ },
22
+ ], { basename: import.meta.env.BASE_URL })
23
+
24
+ initBridgeRouting(router)
25
+ installPopStateLogger(signal)
26
+
27
+ return { element: <RouterProvider router={router} />, dispose: () => router.dispose() }
28
+ })
@@ -62,13 +62,10 @@ export function handleChildEvent(event: DemoChildEvent): void {
62
62
  log(event.source, `${event.action} ${event.detail}`)
63
63
  }
64
64
 
65
- let popInstalled = false
66
-
67
- /** 浏览器前进后退(POP)在 main.tsx 安装一次;push/replace 由各自来源显式标注 */
68
- export function installPopStateLogger(): void {
69
- if (popInstalled) return
70
- popInstalled = true
65
+ /** POP 观测属于本应用实例;卸载后由 signal 移除,不遗留窗口监听。 */
66
+ export function installPopStateLogger(signal: AbortSignal): void {
67
+ if (signal.aborted) return
71
68
  window.addEventListener('popstate', () => {
72
- log('浏览器前进后退', window.location.pathname + window.location.search)
73
- })
69
+ logNav('浏览器前进后退', window.location.pathname + window.location.search)
70
+ }, { signal })
74
71
  }
@@ -1,33 +1,2 @@
1
- /**
2
- * examples/templates/showcase/react-host/src/main.tsx — 宿主入口。
3
- * data router(createBrowserRouter + RouterProvider)是 URL 同步的前提(declarative 模式无取消语义);
4
- * catch-all 路由(/br-vue/* 单条记录渲染同一组件)保证子应用内部导航不重挂宿主组件。
5
- * 注意:刻意不包 StrictMode——桥接契约工厂在每次 mount 时执行,StrictMode 的双 effect
6
- * 会让子应用挂载计数翻倍,干扰「mount 次数恒为 1」演示(生产语义不受影响)。
7
- */
8
- import { createRoot } from 'react-dom/client'
9
- import { createBrowserRouter, RouterProvider } from 'react-router-dom'
10
- import Layout from './Layout'
11
- import HomePage from './pages/HomePage'
12
- import AboutPage from './pages/AboutPage'
13
- import BridgeVuePage from './pages/BridgeVuePage'
14
- import { initBridgeRouting } from './routing'
15
- import { installPopStateLogger } from './demo-log'
16
-
17
- const router = createBrowserRouter([
18
- {
19
- path: '/',
20
- element: <Layout />,
21
- children: [
22
- { index: true, element: <HomePage /> },
23
- { path: 'about', element: <AboutPage /> },
24
- // catch-all:子应用挂载在 /br-vue 前缀下,内部导航全部命中同一条路由记录
25
- { path: 'br-vue/*', element: <BridgeVuePage /> },
26
- ],
27
- },
28
- ])
29
-
30
- initBridgeRouting(router)
31
- installPopStateLogger()
32
-
33
- createRoot(document.getElementById('root')!).render(<RouterProvider router={router} />)
1
+ import application from './application'
2
+ void application.start('#root').catch(console.error)
@@ -11,10 +11,10 @@ export const BRIDGE_BASE_PATH = '/br-vue'
11
11
  let routing: BridgeHostRouting | undefined
12
12
 
13
13
  export function initBridgeRouting(router: Parameters<typeof createReactBridgeNavigation>[0]): void {
14
- routing = { basePath: BRIDGE_BASE_PATH, navigation: createReactBridgeNavigation(router) }
14
+ routing = { basePath: BRIDGE_BASE_PATH, navigation: createReactBridgeNavigation(router, { basename: import.meta.env.BASE_URL }) }
15
15
  }
16
16
 
17
17
  export function getBridgeRouting(): BridgeHostRouting {
18
- if (!routing) throw new Error('桥接路由端口未初始化:请先在 main.tsx 调用 initBridgeRouting(router)')
18
+ if (!routing) throw new Error('桥接路由端口未初始化:请先在 application.tsx 调用 initBridgeRouting(router)')
19
19
  return routing
20
20
  }
@@ -9,7 +9,7 @@ export default {
9
9
  name: 'react-remote',
10
10
  filename: 'fulgurjs-remoteEntry.js',
11
11
  exposes: {
12
- './bridge': './src/bridge.tsx',
12
+ './bridge': './src/application.tsx',
13
13
  },
14
14
  shared: {
15
15
  react: { singleton: true },
@@ -11,7 +11,7 @@
11
11
  "types": "fulgurjs types"
12
12
  },
13
13
  "dependencies": {
14
- "@fulgurjs/federation": "6.8.5",
14
+ "@fulgurjs/federation": "6.9.0",
15
15
  "react": "^19.1.0",
16
16
  "react-dom": "^19.1.0",
17
17
  "react-router-dom": "^7.1.0"
@@ -5,9 +5,10 @@
5
5
  */
6
6
  import { Outlet, useNavigate } from 'react-router-dom'
7
7
  import type { ReactElement } from 'react'
8
- import { getMountCount, reportNav } from './child-bus'
8
+ import { useChildReporter } from './child-bus'
9
9
 
10
10
  export default function ChildLayout({ bridged = false }: { bridged?: boolean }): ReactElement {
11
+ const { getMountCount, reportNav } = useChildReporter()
11
12
  const navigate = useNavigate()
12
13
 
13
14
  const handlePushDemo = (): void => {
@@ -35,7 +36,7 @@ export default function ChildLayout({ bridged = false }: { bridged?: boolean }):
35
36
  <div style={{ borderTop: '1px dashed #ccc', marginTop: 8, paddingTop: 8 }}>
36
37
  <p data-demo-bridge-mode>当前应用被桥接:{String(bridged)}</p>
37
38
  <p style={{ margin: '4px 0' }}>
38
- <strong>React 子应用</strong>(受控 memory 路由)· 本会话挂载次数:{getMountCount()}
39
+ <strong>React 子应用</strong>({bridged ? '受控 memory' : '独立 web'} 路由)· 本会话挂载次数:{getMountCount()}
39
40
  </p>
40
41
  <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', margin: '8px 0' }}>
41
42
  <button onClick={handlePushDemo}>子应用 push /orders?q=演示&amp;page=2</button>
@@ -1,18 +1,12 @@
1
- /**
2
- * examples/templates/showcase/react-remote/src/bridge.tsx — React 子应用桥接契约(exposes './bridge')。
3
- * defineBridgeApp(factory, { routing: true }) 声明路由协议(缺失时宿主启用同步即 MFU-031 占位)。
4
- * 受控路由 = createReactBridgeRouter(memory data router):工厂返回 RouterProvider 元素;
5
- * 宿主未提供路由通道时由插件统一抛 MFU-034(工厂无需手写守卫);根路径 / 经 loader
6
- * redirect 规范化到 /orders,由插件以 replace 同步宿主(无新增历史条目)。
7
- */
8
- import { redirect } from 'react-router-dom'
9
- import { defineBridgeApp, createReactBridgeRouter, hasBridgeMount } from '@fulgurjs/federation/react'
1
+ // 独立与桥接使用同一套真实业务路由;每次工厂创建自己的 router。
2
+ import { createBrowserRouter, RouterProvider, redirect } from 'react-router-dom'
3
+ import { defineApplication, createReactBridgeRouter, hasBridgeMount } from '@fulgurjs/federation/react'
10
4
  import ChildLayout from './ChildLayout'
11
5
  import OrderList from './pages/OrderList'
12
6
  import OrderDetail from './pages/OrderDetail'
13
7
  import Settings from './pages/Settings'
14
8
  import Locked from './pages/Locked'
15
- import { bindReporter, reportNav } from './child-bus'
9
+ import { createChildReporter, ChildReporterContext, type ChildProps, type ChildReporter } from './child-bus'
16
10
 
17
11
  /** 初始位置读取用的最小 data router 结构面(避免耦合 react-router 内部类型路径) */
18
12
  interface MemoryRouterProbe {
@@ -32,27 +26,33 @@ const routes = [
32
26
  },
33
27
  ]
34
28
 
35
- export default defineBridgeApp((props, ctx) => {
36
- bindReporter(props)
37
- const init = ctx!.routing!.getLocation()
29
+ export default defineApplication<ChildProps>((props, ctx) => {
30
+ const reporter = createChildReporter(props, ctx.signal)
31
+ const init = ctx.routing?.getLocation()
38
32
  // 工厂上下文只属于当前实例;通过 props 传给布局,不写页面全局模式。
39
33
  const instanceRoutes = routes.map(route => route.element
40
34
  ? { ...route, element: <ChildLayout bridged={hasBridgeMount(ctx)} /> }
41
35
  : route)
42
- const conn = createReactBridgeRouter(ctx!.routing!, instanceRoutes, { signal: ctx!.signal })
43
- // 冷加载(模块级预热未落定的慢路径)时 element 是 LazyRouterProviderHost 包装、无 router
44
- // prop——同步读 props.router 会静默跳过初始位置上报。routerReady 是就绪合同:fast 路径
45
- // 同步已 resolve、slow 路径在惰性接线完成时 resolve,两条路径行为一致。
46
- void conn.routerReady.then((router) => reportInitRedirect(router, init.pathname + init.search + init.hash))
47
- return conn.element
36
+ if (ctx.mode === 'standalone') {
37
+ const router = createBrowserRouter(instanceRoutes, { basename: import.meta.env.BASE_URL })
38
+ return { element: <ChildReporterContext.Provider value={reporter}><RouterProvider router={router} /></ChildReporterContext.Provider>, dispose: () => router.dispose() }
39
+ }
40
+ const conn = createReactBridgeRouter(ctx.routing!, instanceRoutes, { signal: ctx.signal })
41
+ // 迟到的就绪回调不得向已卸载实例上报;拒绝由实际挂载链处理。
42
+ void conn.routerReady.then(router => {
43
+ if (!ctx.signal.aborted && init) reportInitRedirect(router, init.pathname + init.search + init.hash, ctx.signal, reporter)
44
+ }, () => {})
45
+ return { element: <ChildReporterContext.Provider value={reporter}>{conn.element}</ChildReporterContext.Provider>, dispose: conn.dispose }
46
+
48
47
  }, { routing: true })
49
48
 
50
49
  /** 初始位置上报:createMemoryRouter 同步初始化,'/' 的 loader 重定向在微任务内落定,
51
50
  * 下一个宏任务读取即为最终初始位置(订阅式一次性上报会在首个用户导航时才误触发)。 */
52
- function reportInitRedirect(router: MemoryRouterProbe, initPath: string): void {
51
+ function reportInitRedirect(router: MemoryRouterProbe, initPath: string, signal: AbortSignal, reporter: ChildReporter): void {
53
52
  setTimeout(() => {
53
+ if (signal.aborted) return
54
54
  const current = router.state.location.pathname + router.state.location.search + router.state.location.hash
55
- reportNav(
55
+ reporter.reportNav(
56
56
  '子应用初始化',
57
57
  'init',
58
58
  current !== initPath
@@ -1,32 +1,32 @@
1
- /**
2
- * examples/templates/showcase/react-remote/src/child-bus.ts — 子应用 → 宿主观测台上报总线。
3
- * 宿主经 appProps 传入稳定回调 onChildEvent;本模块持有该引用并维护挂载计数。
4
- * 单页单实例假设:同一宿主页同时只挂一个本子应用实例(路由同步前缀互斥的常规形态)。
5
- */
6
- export type DemoChildNavAction = 'push' | 'replace' | 'go' | 'init'
1
+ /** 应用实例自己的观测回调,通过原生 provide/Context 传给实际消费树。 */
2
+ import { createContext, useContext } from 'react'
7
3
 
4
+ export type DemoChildNavAction = 'push' | 'replace' | 'go' | 'init'
8
5
  export type DemoChildEvent =
9
6
  | { type: 'mounted'; mountCount: number }
10
7
  | { type: 'nav'; source: string; action: DemoChildNavAction; detail: string }
11
-
12
8
  export type DemoChildReporter = (event: DemoChildEvent) => void
13
-
14
- const bus: { report: DemoChildReporter | undefined } = { report: undefined }
15
- let mountCount = 0
16
-
17
- /** 桥接工厂内调用:从 props 快照中取出宿主回调,并上报本次挂载计数 */
18
- export function bindReporter(props: Record<string, unknown> | undefined): void {
19
- const candidate = (props as { onChildEvent?: DemoChildReporter } | undefined)?.onChildEvent
20
- bus.report = typeof candidate === 'function' ? candidate : undefined
21
- mountCount += 1
22
- bus.report?.({ type: 'mounted', mountCount })
9
+ export interface ChildReporter {
10
+ reportNav(source: string, action: DemoChildNavAction, detail: string): void
11
+ getMountCount(): number
23
12
  }
24
-
25
- /** 导航来源上报(子应用 Link / 子应用 push·replace / 子应用 go / 子应用初始化) */
26
- export function reportNav(source: string, action: DemoChildNavAction, detail: string): void {
27
- bus.report?.({ type: 'nav', source, action, detail })
13
+ export interface ChildProps { onChildEvent?: DemoChildReporter }
14
+ // 仅观测编号跨挂载累加;回调和所属实例不放在模块全局。
15
+ let nextMount = 0
16
+ export function createChildReporter(props: ChildProps, signal: AbortSignal): ChildReporter {
17
+ const mountCount = ++nextMount
18
+ if (!signal.aborted) props.onChildEvent?.({ type: 'mounted', mountCount })
19
+ return {
20
+ getMountCount: () => mountCount,
21
+ reportNav(source, action, detail) {
22
+ if (!signal.aborted) props.onChildEvent?.({ type: 'nav', source, action, detail })
23
+ },
24
+ }
28
25
  }
29
26
 
30
- export function getMountCount(): number {
31
- return mountCount
27
+ export const ChildReporterContext = createContext<ChildReporter | null>(null)
28
+ export function useChildReporter(): ChildReporter {
29
+ const reporter = useContext(ChildReporterContext)
30
+ if (!reporter) throw new Error('子应用观测台未装配;从 application 工厂启动本应用。')
31
+ return reporter
32
32
  }
@@ -1,18 +1,2 @@
1
- /**
2
- * examples/templates/showcase/react-remote/src/main.tsx — 独立运行入口(仅 dev 占位)。
3
- * 子应用真实能力经 exposes './bridge' 被宿主加载;直开本页仅作 dev server 健康检查。
4
- */
5
- import { createRoot } from 'react-dom/client'
6
- import type { ReactElement } from 'react'
7
-
8
- function Standalone(): ReactElement {
9
- return (
10
- <div style={{ fontFamily: "'PingFang SC', 'Microsoft YaHei', sans-serif", padding: 24 }}>
11
- <h1>react-remote(联邦子应用,5333)</h1>
12
- <p>本应用通过 expose ./bridge(defineBridgeApp + routing 协议)被宿主加载。</p>
13
- <p>请打开 Vue 宿主桥接页:<a href="http://localhost:5334/br-react/orders">http://localhost:5334/br-react/orders</a></p>
14
- </div>
15
- )
16
- }
17
-
18
- createRoot(document.getElementById('root')!).render(<Standalone />)
1
+ import application from './application'
2
+ void application.start('#root').catch(console.error)
@@ -4,9 +4,10 @@
4
4
  */
5
5
  import type { ReactElement } from 'react'
6
6
  import { Link } from 'react-router-dom'
7
- import { reportNav } from '../child-bus'
7
+ import { useChildReporter } from '../child-bus'
8
8
 
9
9
  export default function Locked(): ReactElement {
10
+ const { reportNav } = useChildReporter()
10
11
  const handleBack = (): void => {
11
12
  reportNav('子应用 Link', 'push', '/orders')
12
13
  }
@@ -4,9 +4,10 @@
4
4
  */
5
5
  import type { ReactElement } from 'react'
6
6
  import { useLocation, useNavigate, useParams } from 'react-router-dom'
7
- import { reportNav } from '../child-bus'
7
+ import { useChildReporter } from '../child-bus'
8
8
 
9
9
  export default function OrderDetail(): ReactElement {
10
+ const { reportNav } = useChildReporter()
10
11
  const params = useParams()
11
12
  const location = useLocation()
12
13
  const navigate = useNavigate()
@@ -6,7 +6,7 @@
6
6
  import { useMemo, useState } from 'react'
7
7
  import type { ChangeEvent, ReactElement } from 'react'
8
8
  import { useLocation, useNavigate } from 'react-router-dom'
9
- import { reportNav } from '../child-bus'
9
+ import { useChildReporter } from '../child-bus'
10
10
 
11
11
  interface OrderItem {
12
12
  id: number
@@ -26,6 +26,7 @@ const ORDERS: OrderItem[] = [
26
26
  const PAGE_SIZE = 3
27
27
 
28
28
  export default function OrderList(): ReactElement {
29
+ const { reportNav } = useChildReporter()
29
30
  const location = useLocation()
30
31
  const navigate = useNavigate()
31
32
  const search = new URLSearchParams(location.search)
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /** Standalone template production launcher. Uses only Node and the existing dev.config.json. */
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import http from 'node:http'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
9
+ const { apps } = JSON.parse(fs.readFileSync(path.join(root, 'scripts/dev.config.json'), 'utf8'))
10
+ if (!Array.isArray(apps) || !apps.length) throw new Error('dev.config.json 未登记应用。')
11
+
12
+ const mime = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.woff': 'font/woff', '.woff2': 'font/woff2', '.map': 'application/json' }
13
+ const inside = (file, directory) => file === directory || file.startsWith(directory + path.sep)
14
+ const mounts = apps.map(app => {
15
+ const dir = path.resolve(root, app.dir, 'dist')
16
+ if (!inside(dir, root)) throw new Error(`${app.name}: 构建目录超出模板。`)
17
+ const index = path.join(dir, 'index.html')
18
+ if (!fs.existsSync(index)) throw new Error(`${app.name}: 缺少 dist/index.html,请先 pnpm build。`)
19
+ const html = fs.readFileSync(index, 'utf8')
20
+ const tags = [...html.matchAll(/<script\b[^>]*>/gi)]
21
+ const tag = tags.find(item => /\btype=["']module["']/i.test(item[0]))?.[0]
22
+ const src = tag?.match(/\bsrc=["']([^"']+)["']/i)?.[1]
23
+ if (!src || /^(?:https?:)?\/\//.test(src)) throw new Error(`${app.name}: 无可本地挂载的模块入口。`)
24
+ // Resolve the actual emitted file suffix, rather than guessing base from an app name.
25
+ const pathname = decodeURIComponent(new URL(src, 'http://template.local/').pathname)
26
+ const parts = pathname.split('/').filter(Boolean)
27
+ const bases = []
28
+ for (let i = 0; i < parts.length; i++) {
29
+ const file = path.resolve(dir, ...parts.slice(i))
30
+ if (inside(file, dir) && fs.existsSync(file) && fs.statSync(file).isFile()) bases.push('/' + parts.slice(0, i).join('/') + (i ? '/' : ''))
31
+ }
32
+ if (bases.length !== 1) throw new Error(`${app.name}: 无法唯一对应构建入口 ${src};检查 Vite base 和干净构建输出。`)
33
+ return { ...app, dir, prefix: bases[0] }
34
+ })
35
+ const hosts = mounts.filter(app => app.host)
36
+ if (!hosts.length) throw new Error('dev.config.json 未登记 host:true 的宿主。')
37
+ const ports = new Set()
38
+ const servers = []
39
+ let stopping = false
40
+ function stop(code = 0) {
41
+ if (stopping) return
42
+ stopping = true
43
+ for (const server of servers) server.close()
44
+ const timer = setTimeout(() => {
45
+ for (const server of servers) server.closeAllConnections?.()
46
+ process.exit(code)
47
+ }, 1000)
48
+ timer.unref()
49
+ process.exitCode = code
50
+ }
51
+ process.once('SIGINT', () => stop(0))
52
+ process.once('SIGTERM', () => stop(0))
53
+ process.once('uncaughtException', error => { console.error(error); stop(1) })
54
+
55
+ for (const host of hosts) {
56
+ if (!Number.isInteger(host.port) || host.port < 1 || host.port > 65535 || ports.has(host.port)) throw new Error(`无效或重复宿主端口:${host.port}`)
57
+ ports.add(host.port)
58
+ const site = [host, ...mounts.filter(app => !app.host)].sort((a, b) => b.prefix.length - a.prefix.length)
59
+ const prefixes = new Set()
60
+ for (const app of site) {
61
+ if (prefixes.has(app.prefix)) throw new Error(`${host.name}: 构建 base ${app.prefix} 冲突;宿主和远程不能部署在同一路径。`)
62
+ prefixes.add(app.prefix)
63
+ }
64
+ const server = http.createServer((req, res) => {
65
+ try {
66
+ const url = new URL(req.url, `http://localhost:${host.port}`)
67
+ if (url.pathname === '/' && host.prefix !== '/') {
68
+ res.writeHead(302, { location: host.prefix, 'cache-control': 'no-cache' })
69
+ return res.end()
70
+ }
71
+ const app = site.find(item => url.pathname === item.prefix.slice(0, -1) || url.pathname.startsWith(item.prefix))
72
+ if (!app) { res.writeHead(404); return res.end('未登记此部署路径') }
73
+ const relative = decodeURIComponent(url.pathname.slice(app.prefix.length)) || 'index.html'
74
+ let file = path.resolve(app.dir, relative)
75
+ if (!inside(file, app.dir)) { res.writeHead(403); return res.end() }
76
+ if (!fs.existsSync(file) || fs.statSync(file).isDirectory()) {
77
+ if (path.extname(relative)) { res.writeHead(404); return res.end('资源不存在;不会回退成HTML') }
78
+ file = path.join(app.dir, 'index.html')
79
+ }
80
+ const stable = /(?:^|\/)(?:index\.html|fulgurjs-(?:remoteEntry\.js|manifest\.json)|_app\.config\.js)$/.test(file)
81
+ res.writeHead(200, {
82
+ 'content-type': mime[path.extname(file)] ?? 'application/octet-stream',
83
+ 'cache-control': stable ? 'no-cache' : 'public, max-age=31536000, immutable',
84
+ 'access-control-allow-origin': '*',
85
+ })
86
+ res.end(fs.readFileSync(file))
87
+ } catch (error) {
88
+ console.error(`生产请求失败:${error.message}`)
89
+ if (!res.headersSent) res.writeHead(400)
90
+ res.end('无效请求或资源不可读')
91
+ }
92
+ })
93
+ servers.push(server)
94
+ server.on('error', error => {
95
+ console.error(`${host.name}:${host.port} 启动失败:${error.message};停止本次全部生产服务器,不会按端口杀已有进程。`)
96
+ stop(1)
97
+ })
98
+ server.listen(host.port, '127.0.0.1', () => {
99
+ if (!stopping) console.log(`生产宿主 ${host.name}: http://localhost:${host.port}${host.prefix}(仅dist,无开发服务器)`)
100
+ })
101
+ }
@@ -11,7 +11,7 @@
11
11
  "types": "fulgurjs types"
12
12
  },
13
13
  "dependencies": {
14
- "@fulgurjs/federation": "6.8.5",
14
+ "@fulgurjs/federation": "6.9.0",
15
15
  "react": "^19.1.0",
16
16
  "react-dom": "^19.1.0",
17
17
  "vue": "^3.5.13",