@fulgurjs/federation 6.8.7 → 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.
- package/CHANGELOG.md +10 -0
- package/README.en.md +3 -0
- package/README.md +3 -0
- package/dist/application-core-BuhXQmLF.d.cts +16 -0
- package/dist/application-core-DWI3eKme.d.ts +16 -0
- package/dist/bridge-app-react.cjs +177 -9
- package/dist/bridge-app-react.d.cts +15 -3
- package/dist/bridge-app-react.d.ts +15 -3
- package/dist/bridge-app-react.js +101 -9
- package/dist/bridge-app-vue.cjs +138 -12
- package/dist/bridge-app-vue.d.cts +20 -5
- package/dist/bridge-app-vue.d.ts +20 -5
- package/dist/bridge-app-vue.js +62 -12
- package/dist/bridge-host-react.d.cts +1 -1
- package/dist/bridge-host-react.d.ts +1 -1
- package/dist/bridge-host-vue.d.cts +1 -1
- package/dist/bridge-host-vue.d.ts +1 -1
- package/dist/chunk-6OB47FSR.js +81 -0
- package/dist/cli.js +349 -23
- package/dist/index.cjs +7 -6
- package/dist/index.js +7 -6
- package/dist/react-adapter.d.cts +1 -1
- package/dist/react-adapter.d.ts +1 -1
- package/dist/react.d.ts +29 -4
- package/dist/react.js +1 -0
- package/dist/{remote-types-xf1B6_pK.d.ts → remote-types-COmBhjRc.d.ts} +1 -1
- package/dist/{remote-types-58hdbx6x.d.cts → remote-types-CStGOJwu.d.cts} +1 -1
- package/dist/runtime.js +1 -1
- package/dist/vue-adapter.d.cts +1 -1
- package/dist/vue-adapter.d.ts +1 -1
- package/dist/vue-store.cjs +40 -0
- package/dist/vue-store.d.cts +4 -0
- package/dist/vue-store.d.ts +4 -0
- package/dist/vue-store.js +15 -0
- package/dist/vue.d.ts +37 -6
- package/dist/vue.js +2 -0
- package/docs/en/reference/api.md +38 -2
- package/docs/en/reference/types.md +27 -0
- package/docs/zh/reference/api.md +46 -2
- package/docs/zh/reference/types.md +27 -0
- package/examples/templates/README.md +3 -0
- package/examples/templates/react-host-vue-remote/README.md +24 -0
- package/examples/templates/react-host-vue-remote/package.json +2 -1
- package/examples/templates/react-host-vue-remote/pnpm-lock.yaml +7 -7
- package/examples/templates/react-host-vue-remote/pnpm-workspace.yaml +1 -1
- package/examples/templates/react-host-vue-remote/react-host/package.json +1 -1
- package/examples/templates/react-host-vue-remote/react-host/src/application.tsx +62 -0
- package/examples/templates/react-host-vue-remote/react-host/src/main.tsx +2 -62
- package/examples/templates/react-host-vue-remote/scripts/prod.mjs +101 -0
- package/examples/templates/react-host-vue-remote/vue-remote/README.md +17 -16
- package/examples/templates/react-host-vue-remote/vue-remote/fulgurjs.config.ts +2 -2
- package/examples/templates/react-host-vue-remote/vue-remote/package.json +1 -1
- package/examples/templates/react-host-vue-remote/vue-remote/src/AboutPage.vue +1 -0
- package/examples/templates/react-host-vue-remote/vue-remote/src/App.vue +15 -5
- package/examples/templates/react-host-vue-remote/vue-remote/src/HomePage.vue +1 -0
- package/examples/templates/react-host-vue-remote/vue-remote/src/application.ts +19 -0
- package/examples/templates/react-host-vue-remote/vue-remote/src/main.ts +2 -15
- package/examples/templates/react-react/README.md +25 -1
- package/examples/templates/react-react/host/package.json +1 -1
- package/examples/templates/react-react/host/src/application.tsx +33 -0
- package/examples/templates/react-react/host/src/main.tsx +3 -32
- package/examples/templates/react-react/package.json +2 -1
- package/examples/templates/react-react/pnpm-lock.yaml +7 -7
- package/examples/templates/react-react/pnpm-workspace.yaml +1 -1
- package/examples/templates/react-react/remote/package.json +1 -1
- package/examples/templates/react-react/remote/src/application.tsx +11 -0
- package/examples/templates/react-react/remote/src/main.tsx +3 -10
- package/examples/templates/react-react/scripts/prod.mjs +101 -0
- package/examples/templates/showcase/README.md +35 -7
- package/examples/templates/showcase/package.json +2 -1
- package/examples/templates/showcase/pnpm-lock.yaml +11 -11
- package/examples/templates/showcase/pnpm-workspace.yaml +1 -1
- package/examples/templates/showcase/react-host/package.json +1 -1
- package/examples/templates/showcase/react-host/src/application.tsx +28 -0
- package/examples/templates/showcase/react-host/src/demo-log.ts +5 -8
- package/examples/templates/showcase/react-host/src/main.tsx +2 -33
- package/examples/templates/showcase/react-host/src/routing.ts +2 -2
- package/examples/templates/showcase/react-remote/fulgurjs.config.ts +1 -1
- package/examples/templates/showcase/react-remote/package.json +1 -1
- package/examples/templates/showcase/react-remote/src/ChildLayout.tsx +3 -2
- package/examples/templates/showcase/react-remote/src/{bridge.tsx → application.tsx} +21 -21
- package/examples/templates/showcase/react-remote/src/child-bus.ts +23 -23
- package/examples/templates/showcase/react-remote/src/main.tsx +2 -18
- package/examples/templates/showcase/react-remote/src/pages/Locked.tsx +2 -1
- package/examples/templates/showcase/react-remote/src/pages/OrderDetail.tsx +2 -1
- package/examples/templates/showcase/react-remote/src/pages/OrderList.tsx +2 -1
- package/examples/templates/showcase/scripts/prod.mjs +101 -0
- package/examples/templates/showcase/vue-host/package.json +1 -1
- package/examples/templates/showcase/vue-host/src/application.ts +28 -0
- package/examples/templates/showcase/vue-host/src/demo-log.ts +4 -7
- package/examples/templates/showcase/vue-host/src/main.ts +2 -30
- package/examples/templates/showcase/vue-host/src/routing.ts +2 -2
- package/examples/templates/showcase/vue-remote/fulgurjs.config.ts +1 -1
- package/examples/templates/showcase/vue-remote/package.json +1 -1
- package/examples/templates/showcase/vue-remote/src/ChildLayout.vue +3 -2
- package/examples/templates/showcase/vue-remote/src/{bridge.ts → application.ts} +10 -13
- package/examples/templates/showcase/vue-remote/src/child-bus.ts +23 -23
- package/examples/templates/showcase/vue-remote/src/main.ts +2 -16
- package/examples/templates/showcase/vue-remote/src/pages/Locked.vue +2 -1
- package/examples/templates/showcase/vue-remote/src/pages/OrderDetail.vue +2 -1
- package/examples/templates/showcase/vue-remote/src/pages/OrderList.vue +2 -1
- package/examples/templates/vue-host-react-remote/README.md +24 -0
- package/examples/templates/vue-host-react-remote/package.json +2 -1
- package/examples/templates/vue-host-react-remote/pnpm-lock.yaml +7 -7
- package/examples/templates/vue-host-react-remote/pnpm-workspace.yaml +1 -1
- package/examples/templates/vue-host-react-remote/react-remote/README.md +17 -17
- package/examples/templates/vue-host-react-remote/react-remote/fulgurjs.config.ts +2 -2
- package/examples/templates/vue-host-react-remote/react-remote/package.json +1 -1
- package/examples/templates/vue-host-react-remote/react-remote/src/{bridge.tsx → application.tsx} +13 -24
- package/examples/templates/vue-host-react-remote/react-remote/src/main.tsx +2 -7
- package/examples/templates/vue-host-react-remote/scripts/prod.mjs +101 -0
- package/examples/templates/vue-host-react-remote/vue-host/package.json +1 -1
- package/examples/templates/vue-host-react-remote/vue-host/src/application.ts +5 -0
- package/examples/templates/vue-host-react-remote/vue-host/src/main.ts +2 -4
- package/examples/templates/vue-vue/README.md +25 -1
- package/examples/templates/vue-vue/host/package.json +1 -1
- package/examples/templates/vue-vue/host/src/application.ts +54 -0
- package/examples/templates/vue-vue/host/src/main.ts +3 -50
- package/examples/templates/vue-vue/package.json +2 -1
- package/examples/templates/vue-vue/pnpm-lock.yaml +7 -7
- package/examples/templates/vue-vue/pnpm-workspace.yaml +1 -1
- package/examples/templates/vue-vue/remote/package.json +1 -1
- package/examples/templates/vue-vue/remote/src/application.ts +6 -0
- package/examples/templates/vue-vue/remote/src/main.ts +3 -5
- package/examples/templates/vue-vue/scripts/prod.mjs +101 -0
- package/package.json +14 -9
- package/examples/templates/react-host-vue-remote/react-host/src/fulgurjs/types/bridge-vue-remote/metadata.json +0 -15
- package/examples/templates/react-host-vue-remote/react-host/src/fulgurjs/types/bridge-vue-remote/modules.d.ts +0 -14
- package/examples/templates/react-host-vue-remote/react-host/src/fulgurjs/types/bridge-vue-remote/registry.d.ts +0 -10
- package/examples/templates/react-host-vue-remote/vue-remote/src/bridge.ts +0 -57
- package/examples/templates/showcase/react-host/src/fulgurjs/types/vue-remote/metadata.json +0 -16
- package/examples/templates/showcase/react-host/src/fulgurjs/types/vue-remote/modules.d.ts +0 -71
- package/examples/templates/showcase/react-host/src/fulgurjs/types/vue-remote/registry.d.ts +0 -10
- package/examples/templates/showcase/vue-host/src/fulgurjs/types/react-remote/metadata.json +0 -16
- package/examples/templates/showcase/vue-host/src/fulgurjs/types/react-remote/modules.d.ts +0 -79
- package/examples/templates/showcase/vue-host/src/fulgurjs/types/react-remote/registry.d.ts +0 -10
- package/examples/templates/vue-host-react-remote/vue-host/src/fulgurjs/types/bridge-react-remote/metadata.json +0 -16
- package/examples/templates/vue-host-react-remote/vue-host/src/fulgurjs/types/bridge-react-remote/modules.d.ts +0 -10
- 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
|
|
86
|
-
| `
|
|
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 }`
|
|
90
|
-
| `
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
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/` 是插件生成目录,按远程实际版本同步,不维护人工伪造的声明或旧缓存。
|
|
@@ -11,8 +11,8 @@ importers:
|
|
|
11
11
|
react-host:
|
|
12
12
|
dependencies:
|
|
13
13
|
'@fulgurjs/federation':
|
|
14
|
-
specifier: 6.
|
|
15
|
-
version: 6.
|
|
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.
|
|
49
|
-
version: 6.
|
|
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.
|
|
80
|
-
version: 6.
|
|
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.
|
|
111
|
-
version: 6.
|
|
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.
|
|
530
|
-
resolution: {
|
|
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.
|
|
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.
|
|
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:
|
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
})
|
|
69
|
+
logNav('浏览器前进后退', window.location.pathname + window.location.search)
|
|
70
|
+
}, { signal })
|
|
74
71
|
}
|
|
@@ -1,33 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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('桥接路由端口未初始化:请先在
|
|
18
|
+
if (!routing) throw new Error('桥接路由端口未初始化:请先在 application.tsx 调用 initBridgeRouting(router)')
|
|
19
19
|
return routing
|
|
20
20
|
}
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Outlet, useNavigate } from 'react-router-dom'
|
|
7
7
|
import type { ReactElement } from 'react'
|
|
8
|
-
import {
|
|
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
|
|
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=演示&page=2</button>
|
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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 {
|
|
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
|
|
36
|
-
|
|
37
|
-
const init = ctx
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
31
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
+
}
|