@fulgurjs/federation 5.9.1 → 5.9.3

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 (46) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cli.js +141 -21
  3. package/dist/index.cjs +2 -2
  4. package/dist/index.js +2 -2
  5. package/dist/runtime.js +1 -1
  6. package/dist/vue-adapter.cjs +5 -1
  7. package/dist/vue-adapter.js +5 -1
  8. package/docs/API.en.md +1 -1
  9. package/docs/API.md +1 -1
  10. package/examples/templates/README.md +12 -11
  11. package/examples/templates/react-host-vue-remote/README.md +6 -0
  12. package/examples/templates/react-host-vue-remote/pnpm-lock.yaml +7 -7
  13. package/examples/templates/react-host-vue-remote/pnpm-workspace.yaml +1 -1
  14. package/examples/templates/react-host-vue-remote/react-host/package.json +1 -1
  15. package/examples/templates/react-host-vue-remote/scripts/dev.mjs +82 -29
  16. package/examples/templates/react-host-vue-remote/vue-remote/package.json +1 -1
  17. package/examples/templates/react-react/README.md +6 -0
  18. package/examples/templates/react-react/host/package.json +1 -1
  19. package/examples/templates/react-react/pnpm-lock.yaml +7 -7
  20. package/examples/templates/react-react/pnpm-workspace.yaml +1 -1
  21. package/examples/templates/react-react/remote/package.json +1 -1
  22. package/examples/templates/react-react/scripts/dev.mjs +82 -29
  23. package/examples/templates/showcase/README.md +11 -1
  24. package/examples/templates/showcase/pnpm-lock.yaml +11 -11
  25. package/examples/templates/showcase/pnpm-workspace.yaml +1 -1
  26. package/examples/templates/showcase/react-host/index.html +1 -1
  27. package/examples/templates/showcase/react-host/package.json +1 -1
  28. package/examples/templates/showcase/react-remote/package.json +1 -1
  29. package/examples/templates/showcase/scripts/dev.mjs +82 -29
  30. package/examples/templates/showcase/vue-host/package.json +1 -1
  31. package/examples/templates/showcase/vue-remote/index.html +1 -1
  32. package/examples/templates/showcase/vue-remote/package.json +1 -1
  33. package/examples/templates/showcase/vue-remote/src/main.ts +2 -2
  34. package/examples/templates/vue-host-react-remote/README.md +6 -0
  35. package/examples/templates/vue-host-react-remote/pnpm-lock.yaml +7 -7
  36. package/examples/templates/vue-host-react-remote/pnpm-workspace.yaml +1 -1
  37. package/examples/templates/vue-host-react-remote/react-remote/package.json +1 -1
  38. package/examples/templates/vue-host-react-remote/scripts/dev.mjs +82 -29
  39. package/examples/templates/vue-host-react-remote/vue-host/package.json +1 -1
  40. package/examples/templates/vue-vue/README.md +6 -0
  41. package/examples/templates/vue-vue/host/package.json +1 -1
  42. package/examples/templates/vue-vue/pnpm-lock.yaml +7 -7
  43. package/examples/templates/vue-vue/pnpm-workspace.yaml +1 -1
  44. package/examples/templates/vue-vue/remote/package.json +1 -1
  45. package/examples/templates/vue-vue/scripts/dev.mjs +82 -29
  46. package/package.json +1 -1
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview --port 5204 --strictPort"
10
10
  },
11
11
  "dependencies": {
12
- "@fulgurjs/federation": "5.9.0",
12
+ "@fulgurjs/federation": "5.9.2",
13
13
  "react": "^19.1.0",
14
14
  "react-dom": "^19.1.0",
15
15
  "react-router-dom": "^7.1.0"
@@ -11,8 +11,8 @@ importers:
11
11
  host:
12
12
  dependencies:
13
13
  '@fulgurjs/federation':
14
- specifier: 5.9.0
15
- version: 5.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.3)
14
+ specifier: 5.9.2
15
+ version: 5.9.2(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.0
46
- version: 5.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.3)
45
+ specifier: 5.9.2
46
+ version: 5.9.2(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.0':
468
- resolution: {integrity: sha512-9dNKsdR8KwGCWLhMRHIYQ8oslI6MiOVqKIyYjhbhYmT7FGBwlPu4tV6v3SQPOEak4m65yqsnWeiqM6zsO55kZQ==}
467
+ '@fulgurjs/federation@5.9.2':
468
+ resolution: {integrity: sha512-uLjNX4wBmTfSfJmPKl34bJzdfwpFhliQEufxZtvPXvqI6QJAMxbVWXYs7AzbTI5OsbD3a8KSopov0pNx4dWZqQ==}
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.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.3)':
1172
+ '@fulgurjs/federation@5.9.2(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 || 5.9.2'
6
6
  # pnpm >=12 构建脚本批准(12 起弃用 onlyBuiltDependencies;pnpm 9-11 忽略本键:
7
7
  # esbuild >=0.16 二进制经 optionalDependencies 平台包分发,postinstall 仅校验,不阻塞安装)
8
8
  allowBuilds:
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview --port 5203 --strictPort"
10
10
  },
11
11
  "dependencies": {
12
- "@fulgurjs/federation": "5.9.0",
12
+ "@fulgurjs/federation": "5.9.2",
13
13
  "react": "^19.1.0",
14
14
  "react-dom": "^19.1.0"
15
15
  },
@@ -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
- `结束占用进程,或同步修改 ${app.dir}/package.json 的 dev/preview 端口与宿主 fulgurjs.config.ts 的 remotes dev 地址后重试`,
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 || child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
115
- return new Promise((resolve) => {
116
- const force = setTimeout(() => {
117
- try { process.kill(process.platform === 'win32' ? child.pid : -child.pid, 'SIGKILL') } catch { /* 已退出 */ }
118
- }, 3000).unref()
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 (process.platform === 'win32') spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' })
122
- else process.kill(-child.pid, 'SIGTERM')
123
- } catch { clearTimeout(force); resolve() }
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, '[dev] 收到 Ctrl+C'))
138
- process.on('SIGTERM', () => void shutdown(143, '[dev] 收到 SIGTERM'))
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
- console.error(color('31', `\n[dev] 启动失败:${app.name} —— ${ready.reason}`))
149
- const tail = tailOf.get(app.name) ?? []
150
- if (tail.length) {
151
- console.error(color('31', `[dev] ${app.name} 日志尾部(最后 ${tail.length} 行):`))
152
- for (const line of tail) console.error(` [${app.name}] ${line}`)
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.8.0「桥接 URL 同步」(`/bridge/router/*`,README §8.3)的双向旗舰演示:
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.0
15
- version: 5.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.3)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))
14
+ specifier: 5.9.2
15
+ version: 5.9.2(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.0
49
- version: 5.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.3)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))
48
+ specifier: 5.9.2
49
+ version: 5.9.2(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.0
80
- version: 5.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.3)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))
79
+ specifier: 5.9.2
80
+ version: 5.9.2(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.0
111
- version: 5.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.3)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))
110
+ specifier: 5.9.2
111
+ version: 5.9.2(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.0':
530
- resolution: {integrity: sha512-9dNKsdR8KwGCWLhMRHIYQ8oslI6MiOVqKIyYjhbhYmT7FGBwlPu4tV6v3SQPOEak4m65yqsnWeiqM6zsO55kZQ==}
529
+ '@fulgurjs/federation@5.9.2':
530
+ resolution: {integrity: sha512-uLjNX4wBmTfSfJmPKl34bJzdfwpFhliQEufxZtvPXvqI6QJAMxbVWXYs7AzbTI5OsbD3a8KSopov0pNx4dWZqQ==}
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.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.3)(vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)))(vue@3.5.43(typescript@5.9.3))':
1351
+ '@fulgurjs/federation@5.9.2(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 || 5.9.2'
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 子应用,5336)</title>
7
+ <title>react-host(桥接 URL 同步宿主 × Vue 子应用,默认端口 5336)</title>
8
8
  </head>
9
9
  <body>
10
10
  <div id="root"></div>
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@fulgurjs/federation": "5.9.0",
13
+ "@fulgurjs/federation": "5.9.2",
14
14
  "react": "^19.1.0",
15
15
  "react-dom": "^19.1.0",
16
16
  "react-router-dom": "^7.1.0",
@@ -10,7 +10,7 @@
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@fulgurjs/federation": "5.9.0",
13
+ "@fulgurjs/federation": "5.9.2",
14
14
  "react": "^19.1.0",
15
15
  "react-dom": "^19.1.0",
16
16
  "react-router-dom": "^7.1.0"
@@ -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
- `结束占用进程,或同步修改 ${app.dir}/package.json 的 dev/preview 端口与宿主 fulgurjs.config.ts 的 remotes dev 地址后重试`,
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 || child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
115
- return new Promise((resolve) => {
116
- const force = setTimeout(() => {
117
- try { process.kill(process.platform === 'win32' ? child.pid : -child.pid, 'SIGKILL') } catch { /* 已退出 */ }
118
- }, 3000).unref()
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 (process.platform === 'win32') spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' })
122
- else process.kill(-child.pid, 'SIGTERM')
123
- } catch { clearTimeout(force); resolve() }
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, '[dev] 收到 Ctrl+C'))
138
- process.on('SIGTERM', () => void shutdown(143, '[dev] 收到 SIGTERM'))
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
- console.error(color('31', `\n[dev] 启动失败:${app.name} —— ${ready.reason}`))
149
- const tail = tailOf.get(app.name) ?? []
150
- if (tail.length) {
151
- console.error(color('31', `[dev] ${app.name} 日志尾部(最后 ${tail.length} 行):`))
152
- for (const line of tail) console.error(` [${app.name}] ${line}`)
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(() => {})
@@ -10,7 +10,7 @@
10
10
  "typecheck": "vue-tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@fulgurjs/federation": "5.9.0",
13
+ "@fulgurjs/federation": "5.9.2",
14
14
  "react": "^19.1.0",
15
15
  "react-dom": "^19.1.0",
16
16
  "vue": "^3.5.13",
@@ -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 同步子应用,5335)</title>
7
+ <title>vue-remote(桥接 URL 同步子应用,默认端口 5335)</title>
8
8
  </head>
9
9
  <body>
10
10
  <div id="app"></div>
@@ -10,7 +10,7 @@
10
10
  "typecheck": "vue-tsc --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@fulgurjs/federation": "5.9.0",
13
+ "@fulgurjs/federation": "5.9.2",
14
14
  "vue": "^3.5.13",
15
15
  "vue-router": "^4.5.0"
16
16
  },
@@ -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(联邦子应用,5335)'),
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 宿主桥接页(5336)'),
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.0
15
- version: 5.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.3)(vue@3.5.43(typescript@5.9.3))
14
+ specifier: 5.9.2
15
+ version: 5.9.2(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.0
46
- version: 5.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.3)(vue@3.5.43(typescript@5.9.3))
45
+ specifier: 5.9.2
46
+ version: 5.9.2(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.0':
471
- resolution: {integrity: sha512-9dNKsdR8KwGCWLhMRHIYQ8oslI6MiOVqKIyYjhbhYmT7FGBwlPu4tV6v3SQPOEak4m65yqsnWeiqM6zsO55kZQ==}
470
+ '@fulgurjs/federation@5.9.2':
471
+ resolution: {integrity: sha512-uLjNX4wBmTfSfJmPKl34bJzdfwpFhliQEufxZtvPXvqI6QJAMxbVWXYs7AzbTI5OsbD3a8KSopov0pNx4dWZqQ==}
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.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.3)(vue@3.5.43(typescript@5.9.3))':
1224
+ '@fulgurjs/federation@5.9.2(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