@fulgurjs/federation 5.9.1 → 5.9.2
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 +8 -0
- package/dist/cli.js +141 -21
- package/dist/index.cjs +2 -2
- package/dist/index.js +2 -2
- package/dist/runtime.js +1 -1
- package/docs/API.en.md +1 -1
- package/docs/API.md +1 -1
- package/examples/templates/README.md +12 -11
- package/examples/templates/react-host-vue-remote/README.md +6 -0
- 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/scripts/dev.mjs +82 -29
- package/examples/templates/react-host-vue-remote/vue-remote/package.json +1 -1
- package/examples/templates/react-react/README.md +6 -0
- package/examples/templates/react-react/host/package.json +1 -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/scripts/dev.mjs +82 -29
- package/examples/templates/showcase/README.md +11 -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/index.html +1 -1
- package/examples/templates/showcase/react-host/package.json +1 -1
- package/examples/templates/showcase/react-remote/package.json +1 -1
- package/examples/templates/showcase/scripts/dev.mjs +82 -29
- package/examples/templates/showcase/vue-host/package.json +1 -1
- package/examples/templates/showcase/vue-remote/index.html +1 -1
- package/examples/templates/showcase/vue-remote/package.json +1 -1
- package/examples/templates/showcase/vue-remote/src/main.ts +2 -2
- package/examples/templates/vue-host-react-remote/README.md +6 -0
- 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/package.json +1 -1
- package/examples/templates/vue-host-react-remote/scripts/dev.mjs +82 -29
- package/examples/templates/vue-host-react-remote/vue-host/package.json +1 -1
- package/examples/templates/vue-vue/README.md +6 -0
- package/examples/templates/vue-vue/host/package.json +1 -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/scripts/dev.mjs +82 -29
- package/package.json +1 -1
|
@@ -11,8 +11,8 @@ importers:
|
|
|
11
11
|
host:
|
|
12
12
|
dependencies:
|
|
13
13
|
'@fulgurjs/federation':
|
|
14
|
-
specifier: 5.9.
|
|
15
|
-
version: 5.9.
|
|
14
|
+
specifier: 5.9.1
|
|
15
|
+
version: 5.9.1(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.3)
|
|
16
16
|
react:
|
|
17
17
|
specifier: ^19.1.0
|
|
18
18
|
version: 19.3.0
|
|
@@ -42,8 +42,8 @@ importers:
|
|
|
42
42
|
remote:
|
|
43
43
|
dependencies:
|
|
44
44
|
'@fulgurjs/federation':
|
|
45
|
-
specifier: 5.9.
|
|
46
|
-
version: 5.9.
|
|
45
|
+
specifier: 5.9.1
|
|
46
|
+
version: 5.9.1(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.3)
|
|
47
47
|
react:
|
|
48
48
|
specifier: ^19.1.0
|
|
49
49
|
version: 19.3.0
|
|
@@ -464,8 +464,8 @@ packages:
|
|
|
464
464
|
cpu: [x64]
|
|
465
465
|
os: [win32]
|
|
466
466
|
|
|
467
|
-
'@fulgurjs/federation@5.9.
|
|
468
|
-
resolution: {integrity: sha512-
|
|
467
|
+
'@fulgurjs/federation@5.9.1':
|
|
468
|
+
resolution: {integrity: sha512-c6/fTaIG08IRA6KJ+F05YnNh2PtVDPV7+frA1WGgAOHuR1uw5ijwaZjsIOSDENuS37ISiTeznv0fl+Yh9WDhQQ==}
|
|
469
469
|
engines: {node: '>=18'}
|
|
470
470
|
hasBin: true
|
|
471
471
|
peerDependencies:
|
|
@@ -1169,7 +1169,7 @@ snapshots:
|
|
|
1169
1169
|
'@esbuild/win32-x64@0.27.7':
|
|
1170
1170
|
optional: true
|
|
1171
1171
|
|
|
1172
|
-
'@fulgurjs/federation@5.9.
|
|
1172
|
+
'@fulgurjs/federation@5.9.1(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.3)':
|
|
1173
1173
|
dependencies:
|
|
1174
1174
|
es-module-lexer: 1.7.0
|
|
1175
1175
|
esbuild: 0.27.7
|
|
@@ -2,7 +2,7 @@ packages:
|
|
|
2
2
|
- host
|
|
3
3
|
- remote
|
|
4
4
|
minimumReleaseAgeExclude:
|
|
5
|
-
- '@fulgurjs/federation@5.7.1 || 5.8.0 || 5.9.0'
|
|
5
|
+
- '@fulgurjs/federation@5.7.1 || 5.8.0 || 5.9.0 || 5.9.1'
|
|
6
6
|
# pnpm >=12 构建脚本批准(12 起弃用 onlyBuiltDependencies;pnpm 9-11 忽略本键:
|
|
7
7
|
# esbuild >=0.16 二进制经 optionalDependencies 平台包分发,postinstall 仅校验,不阻塞安装)
|
|
8
8
|
allowBuilds:
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
* 行为契约(与模板 README 一致):
|
|
10
10
|
* - 启动前预检全部端口:任何端口被占用则拒绝启动并指认处理方法(绝不按端口杀别人的进程);
|
|
11
11
|
* - 按 dev.config.json 顺序(远程在前宿主在后)启动各应用,逐个探活;
|
|
12
|
-
* -
|
|
12
|
+
* - 每个进程从 spawn 起就进入全生命周期监督(启动前/探活中/部分就绪/全组就绪/关闭中):
|
|
13
|
+
* 任一应用在任何阶段异常退出 → 整组停止(只杀本次启动的子进程),非零退出并打印该应用日志尾部;
|
|
14
|
+
* 不存在「探活通过后、全组就绪前」的监督空窗;
|
|
13
15
|
* - Ctrl+C / SIGTERM:清理本次启动的全部子进程后退出;不依赖 PID 文件;
|
|
16
|
+
* - 停止采用 POSIX 进程组信号:即使包管理器父进程已退出,同组的孙进程(dev server)也会被终止,
|
|
17
|
+
* 不留下仍监听端口的孤儿;Windows 走 taskkill /T /F 兜底路径(父进程已退出时无法追溯孙进程,见 README);
|
|
14
18
|
* - 全部就绪后打印访问入口;探活 = 端口可连接(dev server 已监听),页面可用以浏览器验收为准。
|
|
15
19
|
*
|
|
16
20
|
* 平台:macOS/Linux 实测;Windows 走 taskkill 兜底路径,未经验证(见模板 README)。
|
|
@@ -71,7 +75,8 @@ async function preflight() {
|
|
|
71
75
|
const win = `netstat -ano | findstr :${app.port}`
|
|
72
76
|
console.error(
|
|
73
77
|
` ${app.name} → 端口 ${app.port} 已监听。查看占用:${process.platform === 'win32' ? win : posix};` +
|
|
74
|
-
|
|
78
|
+
`结束占用进程,或同步修改以下整组配置后重试:${app.dir}/package.json 的 dev/preview --port、` +
|
|
79
|
+
`宿主 fulgurjs.config.ts 的 remotes dev 地址、scripts/dev.config.json 里该应用的 port`,
|
|
75
80
|
)
|
|
76
81
|
}
|
|
77
82
|
console.error('[dev] 本启动器只清理自己启动的进程,不会替你结束端口上的既有服务。')
|
|
@@ -89,6 +94,18 @@ function spawnApp(app) {
|
|
|
89
94
|
children.set(app.name, child)
|
|
90
95
|
child.stdout.on('data', (c) => { recordOutput(app.name, c); String(c).split('\n').filter(Boolean).forEach((l) => printAppLine(app.name, l)) })
|
|
91
96
|
child.stderr.on('data', (c) => { recordOutput(app.name, c); String(c).split('\n').filter(Boolean).forEach((l) => printAppLine(app.name, l)) })
|
|
97
|
+
// spawn 起即全生命周期监督:探活中、部分就绪、全组就绪、关闭中任何阶段退出都不漏
|
|
98
|
+
child.once('exit', (code, signal) => {
|
|
99
|
+
if (!shuttingDown) {
|
|
100
|
+
void failStartup(
|
|
101
|
+
app.name,
|
|
102
|
+
`意外退出(退出码 ${code ?? 'null'}${signal ? `,信号 ${signal}` : ''})——整组停止;若是启动命令失败,看下方日志尾部`,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
child.once('error', (e) => {
|
|
107
|
+
if (!shuttingDown) void failStartup(app.name, `进程无法启动:${e.message}(检查 pnpm 是否可用:pnpm -v)`)
|
|
108
|
+
})
|
|
92
109
|
return child
|
|
93
110
|
}
|
|
94
111
|
|
|
@@ -109,33 +126,79 @@ async function waitReady(app, child) {
|
|
|
109
126
|
return { ok: false, reason: `${Math.round(readyTimeoutMs / 1000)}s 内端口 ${app.port} 未就绪(dev server 未监听)` }
|
|
110
127
|
}
|
|
111
128
|
|
|
129
|
+
/**
|
|
130
|
+
* 整组终止:向进程组发信号(detached 子进程是组长;组长已退出时组内孙进程仍在,组信号依然送达)。
|
|
131
|
+
* 不检查直接子进程是否已退出——那会跳过组信号、留下仍监听端口的孙进程。
|
|
132
|
+
* Windows 用 taskkill /T /F 树杀;父进程已退出时 taskkill 无法追溯,属平台兜底限制。
|
|
133
|
+
*/
|
|
112
134
|
function killTree(appName) {
|
|
113
135
|
const child = children.get(appName)
|
|
114
|
-
if (!child
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
child.once('exit', () => { clearTimeout(force); resolve() })
|
|
136
|
+
if (!child) return Promise.resolve()
|
|
137
|
+
const pid = child.pid
|
|
138
|
+
const isWin = process.platform === 'win32'
|
|
139
|
+
const directDead = child.exitCode !== null || child.signalCode !== null
|
|
140
|
+
const signalGroup = (hard) => {
|
|
120
141
|
try {
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
if (isWin) {
|
|
143
|
+
if (!directDead) spawn('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' })
|
|
144
|
+
} else {
|
|
145
|
+
process.kill(-pid, hard ? 'SIGKILL' : 'SIGTERM')
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
/* ESRCH:组内已无进程 */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
let done = false
|
|
153
|
+
const finish = () => {
|
|
154
|
+
if (done) return
|
|
155
|
+
done = true
|
|
156
|
+
clearTimeout(forceTimer)
|
|
157
|
+
resolve()
|
|
158
|
+
}
|
|
159
|
+
// 这些定时器必须是 ref 的:直接子进程已退出时它们是事件循环里唯一的句柄,
|
|
160
|
+
// unref 会让循环提前 drain(进程以退出码 13 结束),shutdown 的收尾就永远走不完
|
|
161
|
+
const forceTimer = setTimeout(() => {
|
|
162
|
+
signalGroup(true)
|
|
163
|
+
setTimeout(finish, 500)
|
|
164
|
+
}, 3000)
|
|
165
|
+
if (!directDead) child.once('exit', finish)
|
|
166
|
+
signalGroup(false)
|
|
167
|
+
if (directDead || isWin) {
|
|
168
|
+
// 直接子进程已退出(或 Windows 树杀无法等 exit 事件):给组信号固定传播宽限
|
|
169
|
+
setTimeout(finish, 500)
|
|
170
|
+
}
|
|
124
171
|
})
|
|
125
172
|
}
|
|
126
173
|
|
|
174
|
+
/** 统一失败通道:任何阶段的失败都从这里走(shuttingDown 幂等,多事件并发不冲突) */
|
|
175
|
+
async function failStartup(name, reason) {
|
|
176
|
+
if (shuttingDown) return
|
|
177
|
+
console.error(color('31', `\n[dev] 启动失败:${name} —— ${reason}`))
|
|
178
|
+
const tail = tailOf.get(name) ?? []
|
|
179
|
+
if (tail.length) {
|
|
180
|
+
console.error(color('31', `[dev] ${name} 日志尾部(最后 ${tail.length} 行):`))
|
|
181
|
+
for (const line of tail) console.error(` [${name}] ${line}`)
|
|
182
|
+
}
|
|
183
|
+
await shutdown(EXIT.startupFailed, `[dev] 正在停止本次启动的其余应用…`)
|
|
184
|
+
}
|
|
185
|
+
|
|
127
186
|
async function shutdown(exitCode, reason) {
|
|
128
187
|
if (shuttingDown) return
|
|
129
188
|
shuttingDown = true
|
|
130
189
|
if (reason) console.log(`[dev] ${reason}`)
|
|
131
190
|
console.log(`[dev] 正在停止本次启动的 ${children.size} 个应用…`)
|
|
191
|
+
// 看门狗:清理最多等 15s,绝不因单个 killTree 挂起而无法退出
|
|
192
|
+
const watchdog = setTimeout(() => process.exit(exitCode), 15000)
|
|
193
|
+
watchdog.unref()
|
|
132
194
|
await Promise.all([...children.keys()].map(killTree))
|
|
195
|
+
clearTimeout(watchdog)
|
|
133
196
|
console.log('[dev] 已全部停止。')
|
|
134
197
|
process.exit(exitCode)
|
|
135
198
|
}
|
|
136
199
|
|
|
137
|
-
process.on('SIGINT', () => void shutdown(EXIT.signal, '
|
|
138
|
-
process.on('SIGTERM', () => void shutdown(143, '
|
|
200
|
+
process.on('SIGINT', () => void shutdown(EXIT.signal, '收到 Ctrl+C'))
|
|
201
|
+
process.on('SIGTERM', () => void shutdown(143, '收到 SIGTERM'))
|
|
139
202
|
|
|
140
203
|
await preflight()
|
|
141
204
|
|
|
@@ -145,29 +208,19 @@ for (const app of apps) {
|
|
|
145
208
|
const child = spawnApp(app)
|
|
146
209
|
const ready = await waitReady(app, child)
|
|
147
210
|
if (!ready.ok) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
await Promise.all([...children.keys()].map(killTree))
|
|
155
|
-
console.error(`[dev] 已停止本次启动的全部进程(${children.size} 个)。修复上面的问题后重新运行 pnpm dev。`)
|
|
156
|
-
process.exit(EXIT.startupFailed)
|
|
211
|
+
await failStartup(app.name, ready.reason)
|
|
212
|
+
}
|
|
213
|
+
if (shuttingDown) {
|
|
214
|
+
// 失败通道已接管(含并发事件抢先置位的情况):停在这里等 shutdown 退出,绝不宣布就绪
|
|
215
|
+
await new Promise(() => {})
|
|
157
216
|
}
|
|
158
217
|
printAppLine(app.name, `就绪 → http://localhost:${app.port}/`)
|
|
159
218
|
}
|
|
160
219
|
|
|
161
|
-
console.log(color('32', '\n[dev]
|
|
220
|
+
console.log(color('32', '\n[dev] 全部应用已就绪(持续监督中,任一应用退出会整组停止):'))
|
|
162
221
|
for (const app of [...apps].reverse()) {
|
|
163
222
|
console.log(` http://localhost:${app.port}/ ${app.name}`)
|
|
164
223
|
}
|
|
165
224
|
console.log('[dev] Ctrl+C 停止全部应用(只清理本次启动的进程)。dev server 已监听 ≠ 页面必然可用,验收请以浏览器实际加载为准。\n')
|
|
166
225
|
|
|
167
|
-
// 运行期任何子应用退出(崩溃/被外部杀掉)→ 整组停止,不让半残组合冒充正常运行
|
|
168
|
-
for (const [name, child] of children) {
|
|
169
|
-
child.once('exit', (code) => {
|
|
170
|
-
if (!shuttingDown) void shutdown(EXIT.startupFailed, `[dev] ${name} 意外退出(退出码 ${code})——停止其余应用`)
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
226
|
await new Promise(() => {})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 双向桥接 URL 同步(examples/templates/showcase)
|
|
2
2
|
|
|
3
|
-
`@fulgurjs/federation` 5.
|
|
3
|
+
`@fulgurjs/federation` 5.9.1「桥接 URL 同步」(`/bridge/router/*`,README §8.3)的双向旗舰演示:
|
|
4
4
|
两个方向的宿主 × 子应用各一套——
|
|
5
5
|
|
|
6
6
|
- **vue-host(:5334)× react-remote(:5333)**:Vue Router 4 宿主 + `createReactBridgeRouter` React 子应用
|
|
@@ -107,3 +107,13 @@ curl --noproxy '*' -s -o /dev/null -w '%{http_code}\n' http://localhost:5335/@fu
|
|
|
107
107
|
pnpm run typecheck # React 工程 tsc --noEmit;Vue 工程 vue-tsc --noEmit
|
|
108
108
|
pnpm run build
|
|
109
109
|
```
|
|
110
|
+
|
|
111
|
+
## 改端口(整组同步,漏一处启动器会被旧端口卡住)
|
|
112
|
+
|
|
113
|
+
四个应用的端口出现在三处,改任何一个都要整组同步:
|
|
114
|
+
|
|
115
|
+
1. 各应用 `package.json` 的 `dev` 与 `preview` 两个脚本的 `--port`(每应用两处);
|
|
116
|
+
2. 宿主 `fulgurjs.config.ts` 里 `remotes` 的 dev 地址(`vue-host` 指向 react-remote 的 5333;`react-host` 指向 vue-remote 的 5335);
|
|
117
|
+
3. 根 `scripts/dev.config.json` 里该应用的 `port`(**必改**:启动器预检/探活都读它)。
|
|
118
|
+
|
|
119
|
+
生产部署地址(各应用 `prod`)是站点路径,与 dev 端口无关,改端口不要动它。改完自查:`grep -rn "533" --include="*.json" --include="*.ts" .` 不应再出现旧端口。
|
|
@@ -11,8 +11,8 @@ importers:
|
|
|
11
11
|
react-host:
|
|
12
12
|
dependencies:
|
|
13
13
|
'@fulgurjs/federation':
|
|
14
|
-
specifier: 5.9.
|
|
15
|
-
version: 5.9.
|
|
14
|
+
specifier: 5.9.1
|
|
15
|
+
version: 5.9.1(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.3)(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: 5.9.
|
|
49
|
-
version: 5.9.
|
|
48
|
+
specifier: 5.9.1
|
|
49
|
+
version: 5.9.1(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.3)(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: 5.9.
|
|
80
|
-
version: 5.9.
|
|
79
|
+
specifier: 5.9.1
|
|
80
|
+
version: 5.9.1(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.3)(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: 5.9.
|
|
111
|
-
version: 5.9.
|
|
110
|
+
specifier: 5.9.1
|
|
111
|
+
version: 5.9.1(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.3)(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@5.9.
|
|
530
|
-
resolution: {integrity: sha512-
|
|
529
|
+
'@fulgurjs/federation@5.9.1':
|
|
530
|
+
resolution: {integrity: sha512-c6/fTaIG08IRA6KJ+F05YnNh2PtVDPV7+frA1WGgAOHuR1uw5ijwaZjsIOSDENuS37ISiTeznv0fl+Yh9WDhQQ==}
|
|
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@5.9.
|
|
1351
|
+
'@fulgurjs/federation@5.9.1(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.3)(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@5.7.1 || 5.8.0 || 5.9.0'
|
|
7
|
+
- '@fulgurjs/federation@5.7.1 || 5.8.0 || 5.9.0 || 5.9.1'
|
|
8
8
|
# pnpm >=12 构建脚本批准(12 起弃用 onlyBuiltDependencies;pnpm 9-11 忽略本键:
|
|
9
9
|
# esbuild >=0.16 二进制经 optionalDependencies 平台包分发,postinstall 仅校验,不阻塞安装)
|
|
10
10
|
allowBuilds:
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" href="data:," />
|
|
7
|
-
<title>react-host(桥接 URL 同步宿主 × Vue
|
|
7
|
+
<title>react-host(桥接 URL 同步宿主 × Vue 子应用,默认端口 5336)</title>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="root"></div>
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
* 行为契约(与模板 README 一致):
|
|
10
10
|
* - 启动前预检全部端口:任何端口被占用则拒绝启动并指认处理方法(绝不按端口杀别人的进程);
|
|
11
11
|
* - 按 dev.config.json 顺序(远程在前宿主在后)启动各应用,逐个探活;
|
|
12
|
-
* -
|
|
12
|
+
* - 每个进程从 spawn 起就进入全生命周期监督(启动前/探活中/部分就绪/全组就绪/关闭中):
|
|
13
|
+
* 任一应用在任何阶段异常退出 → 整组停止(只杀本次启动的子进程),非零退出并打印该应用日志尾部;
|
|
14
|
+
* 不存在「探活通过后、全组就绪前」的监督空窗;
|
|
13
15
|
* - Ctrl+C / SIGTERM:清理本次启动的全部子进程后退出;不依赖 PID 文件;
|
|
16
|
+
* - 停止采用 POSIX 进程组信号:即使包管理器父进程已退出,同组的孙进程(dev server)也会被终止,
|
|
17
|
+
* 不留下仍监听端口的孤儿;Windows 走 taskkill /T /F 兜底路径(父进程已退出时无法追溯孙进程,见 README);
|
|
14
18
|
* - 全部就绪后打印访问入口;探活 = 端口可连接(dev server 已监听),页面可用以浏览器验收为准。
|
|
15
19
|
*
|
|
16
20
|
* 平台:macOS/Linux 实测;Windows 走 taskkill 兜底路径,未经验证(见模板 README)。
|
|
@@ -71,7 +75,8 @@ async function preflight() {
|
|
|
71
75
|
const win = `netstat -ano | findstr :${app.port}`
|
|
72
76
|
console.error(
|
|
73
77
|
` ${app.name} → 端口 ${app.port} 已监听。查看占用:${process.platform === 'win32' ? win : posix};` +
|
|
74
|
-
|
|
78
|
+
`结束占用进程,或同步修改以下整组配置后重试:${app.dir}/package.json 的 dev/preview --port、` +
|
|
79
|
+
`宿主 fulgurjs.config.ts 的 remotes dev 地址、scripts/dev.config.json 里该应用的 port`,
|
|
75
80
|
)
|
|
76
81
|
}
|
|
77
82
|
console.error('[dev] 本启动器只清理自己启动的进程,不会替你结束端口上的既有服务。')
|
|
@@ -89,6 +94,18 @@ function spawnApp(app) {
|
|
|
89
94
|
children.set(app.name, child)
|
|
90
95
|
child.stdout.on('data', (c) => { recordOutput(app.name, c); String(c).split('\n').filter(Boolean).forEach((l) => printAppLine(app.name, l)) })
|
|
91
96
|
child.stderr.on('data', (c) => { recordOutput(app.name, c); String(c).split('\n').filter(Boolean).forEach((l) => printAppLine(app.name, l)) })
|
|
97
|
+
// spawn 起即全生命周期监督:探活中、部分就绪、全组就绪、关闭中任何阶段退出都不漏
|
|
98
|
+
child.once('exit', (code, signal) => {
|
|
99
|
+
if (!shuttingDown) {
|
|
100
|
+
void failStartup(
|
|
101
|
+
app.name,
|
|
102
|
+
`意外退出(退出码 ${code ?? 'null'}${signal ? `,信号 ${signal}` : ''})——整组停止;若是启动命令失败,看下方日志尾部`,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
child.once('error', (e) => {
|
|
107
|
+
if (!shuttingDown) void failStartup(app.name, `进程无法启动:${e.message}(检查 pnpm 是否可用:pnpm -v)`)
|
|
108
|
+
})
|
|
92
109
|
return child
|
|
93
110
|
}
|
|
94
111
|
|
|
@@ -109,33 +126,79 @@ async function waitReady(app, child) {
|
|
|
109
126
|
return { ok: false, reason: `${Math.round(readyTimeoutMs / 1000)}s 内端口 ${app.port} 未就绪(dev server 未监听)` }
|
|
110
127
|
}
|
|
111
128
|
|
|
129
|
+
/**
|
|
130
|
+
* 整组终止:向进程组发信号(detached 子进程是组长;组长已退出时组内孙进程仍在,组信号依然送达)。
|
|
131
|
+
* 不检查直接子进程是否已退出——那会跳过组信号、留下仍监听端口的孙进程。
|
|
132
|
+
* Windows 用 taskkill /T /F 树杀;父进程已退出时 taskkill 无法追溯,属平台兜底限制。
|
|
133
|
+
*/
|
|
112
134
|
function killTree(appName) {
|
|
113
135
|
const child = children.get(appName)
|
|
114
|
-
if (!child
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
child.once('exit', () => { clearTimeout(force); resolve() })
|
|
136
|
+
if (!child) return Promise.resolve()
|
|
137
|
+
const pid = child.pid
|
|
138
|
+
const isWin = process.platform === 'win32'
|
|
139
|
+
const directDead = child.exitCode !== null || child.signalCode !== null
|
|
140
|
+
const signalGroup = (hard) => {
|
|
120
141
|
try {
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
if (isWin) {
|
|
143
|
+
if (!directDead) spawn('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore' })
|
|
144
|
+
} else {
|
|
145
|
+
process.kill(-pid, hard ? 'SIGKILL' : 'SIGTERM')
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
/* ESRCH:组内已无进程 */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
let done = false
|
|
153
|
+
const finish = () => {
|
|
154
|
+
if (done) return
|
|
155
|
+
done = true
|
|
156
|
+
clearTimeout(forceTimer)
|
|
157
|
+
resolve()
|
|
158
|
+
}
|
|
159
|
+
// 这些定时器必须是 ref 的:直接子进程已退出时它们是事件循环里唯一的句柄,
|
|
160
|
+
// unref 会让循环提前 drain(进程以退出码 13 结束),shutdown 的收尾就永远走不完
|
|
161
|
+
const forceTimer = setTimeout(() => {
|
|
162
|
+
signalGroup(true)
|
|
163
|
+
setTimeout(finish, 500)
|
|
164
|
+
}, 3000)
|
|
165
|
+
if (!directDead) child.once('exit', finish)
|
|
166
|
+
signalGroup(false)
|
|
167
|
+
if (directDead || isWin) {
|
|
168
|
+
// 直接子进程已退出(或 Windows 树杀无法等 exit 事件):给组信号固定传播宽限
|
|
169
|
+
setTimeout(finish, 500)
|
|
170
|
+
}
|
|
124
171
|
})
|
|
125
172
|
}
|
|
126
173
|
|
|
174
|
+
/** 统一失败通道:任何阶段的失败都从这里走(shuttingDown 幂等,多事件并发不冲突) */
|
|
175
|
+
async function failStartup(name, reason) {
|
|
176
|
+
if (shuttingDown) return
|
|
177
|
+
console.error(color('31', `\n[dev] 启动失败:${name} —— ${reason}`))
|
|
178
|
+
const tail = tailOf.get(name) ?? []
|
|
179
|
+
if (tail.length) {
|
|
180
|
+
console.error(color('31', `[dev] ${name} 日志尾部(最后 ${tail.length} 行):`))
|
|
181
|
+
for (const line of tail) console.error(` [${name}] ${line}`)
|
|
182
|
+
}
|
|
183
|
+
await shutdown(EXIT.startupFailed, `[dev] 正在停止本次启动的其余应用…`)
|
|
184
|
+
}
|
|
185
|
+
|
|
127
186
|
async function shutdown(exitCode, reason) {
|
|
128
187
|
if (shuttingDown) return
|
|
129
188
|
shuttingDown = true
|
|
130
189
|
if (reason) console.log(`[dev] ${reason}`)
|
|
131
190
|
console.log(`[dev] 正在停止本次启动的 ${children.size} 个应用…`)
|
|
191
|
+
// 看门狗:清理最多等 15s,绝不因单个 killTree 挂起而无法退出
|
|
192
|
+
const watchdog = setTimeout(() => process.exit(exitCode), 15000)
|
|
193
|
+
watchdog.unref()
|
|
132
194
|
await Promise.all([...children.keys()].map(killTree))
|
|
195
|
+
clearTimeout(watchdog)
|
|
133
196
|
console.log('[dev] 已全部停止。')
|
|
134
197
|
process.exit(exitCode)
|
|
135
198
|
}
|
|
136
199
|
|
|
137
|
-
process.on('SIGINT', () => void shutdown(EXIT.signal, '
|
|
138
|
-
process.on('SIGTERM', () => void shutdown(143, '
|
|
200
|
+
process.on('SIGINT', () => void shutdown(EXIT.signal, '收到 Ctrl+C'))
|
|
201
|
+
process.on('SIGTERM', () => void shutdown(143, '收到 SIGTERM'))
|
|
139
202
|
|
|
140
203
|
await preflight()
|
|
141
204
|
|
|
@@ -145,29 +208,19 @@ for (const app of apps) {
|
|
|
145
208
|
const child = spawnApp(app)
|
|
146
209
|
const ready = await waitReady(app, child)
|
|
147
210
|
if (!ready.ok) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
await Promise.all([...children.keys()].map(killTree))
|
|
155
|
-
console.error(`[dev] 已停止本次启动的全部进程(${children.size} 个)。修复上面的问题后重新运行 pnpm dev。`)
|
|
156
|
-
process.exit(EXIT.startupFailed)
|
|
211
|
+
await failStartup(app.name, ready.reason)
|
|
212
|
+
}
|
|
213
|
+
if (shuttingDown) {
|
|
214
|
+
// 失败通道已接管(含并发事件抢先置位的情况):停在这里等 shutdown 退出,绝不宣布就绪
|
|
215
|
+
await new Promise(() => {})
|
|
157
216
|
}
|
|
158
217
|
printAppLine(app.name, `就绪 → http://localhost:${app.port}/`)
|
|
159
218
|
}
|
|
160
219
|
|
|
161
|
-
console.log(color('32', '\n[dev]
|
|
220
|
+
console.log(color('32', '\n[dev] 全部应用已就绪(持续监督中,任一应用退出会整组停止):'))
|
|
162
221
|
for (const app of [...apps].reverse()) {
|
|
163
222
|
console.log(` http://localhost:${app.port}/ ${app.name}`)
|
|
164
223
|
}
|
|
165
224
|
console.log('[dev] Ctrl+C 停止全部应用(只清理本次启动的进程)。dev server 已监听 ≠ 页面必然可用,验收请以浏览器实际加载为准。\n')
|
|
166
225
|
|
|
167
|
-
// 运行期任何子应用退出(崩溃/被外部杀掉)→ 整组停止,不让半残组合冒充正常运行
|
|
168
|
-
for (const [name, child] of children) {
|
|
169
|
-
child.once('exit', (code) => {
|
|
170
|
-
if (!shuttingDown) void shutdown(EXIT.startupFailed, `[dev] ${name} 意外退出(退出码 ${code})——停止其余应用`)
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
226
|
await new Promise(() => {})
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" href="data:," />
|
|
7
|
-
<title>vue-remote(桥接 URL
|
|
7
|
+
<title>vue-remote(桥接 URL 同步子应用,默认端口 5335)</title>
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="app"></div>
|
|
@@ -7,10 +7,10 @@ import { createApp, h } from 'vue'
|
|
|
7
7
|
createApp({
|
|
8
8
|
render: () =>
|
|
9
9
|
h('div', { style: "font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; padding: 24px" }, [
|
|
10
|
-
h('h1', null, 'vue-remote
|
|
10
|
+
h('h1', null, 'vue-remote(联邦子应用,默认端口 5335,改端口见 README「改端口」清单)'),
|
|
11
11
|
h('p', null, '本应用通过 expose ./bridge(defineBridgeApp + routing 协议)被宿主加载。'),
|
|
12
12
|
h('p', null, [
|
|
13
|
-
h('a', { href: 'http://localhost:5336/br-vue/orders' }, '打开 React
|
|
13
|
+
h('a', { href: 'http://localhost:5336/br-vue/orders' }, '打开 React 宿主桥接页(默认 5336)'),
|
|
14
14
|
]),
|
|
15
15
|
]),
|
|
16
16
|
}).mount('#app')
|
|
@@ -16,3 +16,9 @@ pnpm dev
|
|
|
16
16
|
- 无 JS 沙箱或自动 CSS 隔离,子应用全局样式和 body 副作用需要自行管理。
|
|
17
17
|
|
|
18
18
|
环境要求见[模板指南](../README.md)。所有依赖来自 registry,不需要本仓库源码或其他模板。
|
|
19
|
+
|
|
20
|
+
## 改端口(四处必须同步,漏一处启动器会被旧端口卡住)
|
|
21
|
+
|
|
22
|
+
- 改react-remote(默认 5303):① `react-remote/package.json` 的 `dev` 与 `preview` 两个脚本的 `--port 5303`;② `vue-host/fulgurjs.config.ts` 里 `remotes['bridge-react-remote'].dev` 的 `http://localhost:5303`;③ 根 `scripts/dev.config.json` 里 react-remote 的 `port`(**必改**:启动器预检/探活都读它);④ 本 README 顶部端口表。
|
|
23
|
+
- 改vue-host(默认 5314):① `vue-host/package.json` 的 `dev`/`preview` `--port 5314`;② `scripts/dev.config.json`;③ 本 README 端口表。
|
|
24
|
+
- 生产部署地址(`prod`,如 `/react-remote`)是站点路径,与 dev 端口无关,改端口不要动它。改名(容器名)不建议:还需同步 spec 前缀与页面表。
|
|
@@ -11,8 +11,8 @@ importers:
|
|
|
11
11
|
react-remote:
|
|
12
12
|
dependencies:
|
|
13
13
|
'@fulgurjs/federation':
|
|
14
|
-
specifier: 5.9.
|
|
15
|
-
version: 5.9.
|
|
14
|
+
specifier: 5.9.1
|
|
15
|
+
version: 5.9.1(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.3)(vue@3.5.43(typescript@5.9.3))
|
|
16
16
|
react:
|
|
17
17
|
specifier: ^19.1.0
|
|
18
18
|
version: 19.3.0
|
|
@@ -42,8 +42,8 @@ importers:
|
|
|
42
42
|
vue-host:
|
|
43
43
|
dependencies:
|
|
44
44
|
'@fulgurjs/federation':
|
|
45
|
-
specifier: 5.9.
|
|
46
|
-
version: 5.9.
|
|
45
|
+
specifier: 5.9.1
|
|
46
|
+
version: 5.9.1(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.3)(vue@3.5.43(typescript@5.9.3))
|
|
47
47
|
react:
|
|
48
48
|
specifier: ^19.1.0
|
|
49
49
|
version: 19.3.0
|
|
@@ -467,8 +467,8 @@ packages:
|
|
|
467
467
|
cpu: [x64]
|
|
468
468
|
os: [win32]
|
|
469
469
|
|
|
470
|
-
'@fulgurjs/federation@5.9.
|
|
471
|
-
resolution: {integrity: sha512-
|
|
470
|
+
'@fulgurjs/federation@5.9.1':
|
|
471
|
+
resolution: {integrity: sha512-c6/fTaIG08IRA6KJ+F05YnNh2PtVDPV7+frA1WGgAOHuR1uw5ijwaZjsIOSDENuS37ISiTeznv0fl+Yh9WDhQQ==}
|
|
472
472
|
engines: {node: '>=18'}
|
|
473
473
|
hasBin: true
|
|
474
474
|
peerDependencies:
|
|
@@ -1221,7 +1221,7 @@ snapshots:
|
|
|
1221
1221
|
'@esbuild/win32-x64@0.27.7':
|
|
1222
1222
|
optional: true
|
|
1223
1223
|
|
|
1224
|
-
'@fulgurjs/federation@5.9.
|
|
1224
|
+
'@fulgurjs/federation@5.9.1(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.3)(vue@3.5.43(typescript@5.9.3))':
|
|
1225
1225
|
dependencies:
|
|
1226
1226
|
es-module-lexer: 1.7.0
|
|
1227
1227
|
esbuild: 0.27.7
|
|
@@ -2,7 +2,7 @@ packages:
|
|
|
2
2
|
- vue-host
|
|
3
3
|
- react-remote
|
|
4
4
|
minimumReleaseAgeExclude:
|
|
5
|
-
- '@fulgurjs/federation@5.7.1 || 5.8.0 || 5.9.0'
|
|
5
|
+
- '@fulgurjs/federation@5.7.1 || 5.8.0 || 5.9.0 || 5.9.1'
|
|
6
6
|
# pnpm >=12 构建脚本批准(12 起弃用 onlyBuiltDependencies;pnpm 9-11 忽略本键:
|
|
7
7
|
# esbuild >=0.16 二进制经 optionalDependencies 平台包分发,postinstall 仅校验,不阻塞安装)
|
|
8
8
|
allowBuilds:
|