@fulgurjs/federation 5.8.0 → 5.9.1
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 +19 -4
- package/README.en.md +60 -24
- package/README.md +60 -24
- package/dist/cli.js +327 -29
- package/dist/index.cjs +2 -2
- package/dist/index.js +2 -2
- package/dist/runtime.js +1 -1
- package/docs/API.en.md +14 -3
- package/docs/API.md +7 -6
- package/docs/webpack-mf-/345/257/271/347/205/247/344/270/216/347/274/272/345/217/243.md +2 -2
- package/examples/README.md +3 -67
- package/examples/templates/README.md +88 -0
- package/examples/templates/react-host-vue-remote/README.md +18 -0
- package/examples/templates/react-host-vue-remote/package.json +11 -0
- package/examples/templates/react-host-vue-remote/pnpm-lock.yaml +1627 -0
- package/examples/templates/react-host-vue-remote/pnpm-workspace.yaml +10 -0
- package/examples/{bridge → templates/react-host-vue-remote}/react-host/README.md +8 -7
- package/examples/{bridge → templates/react-host-vue-remote}/react-host/package.json +1 -1
- package/examples/templates/react-host-vue-remote/react-host/src/fulgurjs/types/bridge-vue-remote.d/_paths.d.ts +4 -0
- package/examples/templates/react-host-vue-remote/react-host/src/fulgurjs/types/bridge-vue-remote.d/bridge.ts +3 -0
- package/examples/templates/react-host-vue-remote/react-host/src/fulgurjs/types/bridge-vue-remote.d.ts +9 -0
- package/examples/templates/react-host-vue-remote/scripts/dev.config.json +6 -0
- package/examples/templates/react-host-vue-remote/scripts/dev.mjs +173 -0
- package/examples/{bridge → templates/react-host-vue-remote}/vue-remote/README.md +10 -5
- package/examples/{bridge → templates/react-host-vue-remote}/vue-remote/package.json +1 -1
- package/examples/{react → templates/react-react}/README.md +9 -13
- package/examples/{react → templates/react-react}/host/README.md +6 -8
- package/examples/{react → templates/react-react}/host/package.json +1 -1
- package/examples/templates/react-react/host/src/fulgurjs/types/react-remote.d/ClickButton.ts +3 -0
- package/examples/templates/react-react/host/src/fulgurjs/types/react-remote.d/_paths.d.ts +4 -0
- package/examples/templates/react-react/host/src/fulgurjs/types/react-remote.d/pages/DetailPage.ts +3 -0
- package/examples/templates/react-react/host/src/fulgurjs/types/react-remote.d/pages/HomePage.ts +3 -0
- package/examples/templates/react-react/host/src/fulgurjs/types/react-remote.d/utils.ts +2 -0
- package/examples/templates/react-react/host/src/fulgurjs/types/react-remote.d.ts +26 -0
- package/examples/templates/react-react/package.json +11 -0
- package/examples/templates/react-react/pnpm-lock.yaml +1537 -0
- package/examples/templates/react-react/pnpm-workspace.yaml +10 -0
- package/examples/{react → templates/react-react}/remote/README.md +7 -6
- package/examples/{react → templates/react-react}/remote/package.json +1 -1
- package/examples/templates/react-react/scripts/dev.config.json +6 -0
- package/examples/templates/react-react/scripts/dev.mjs +173 -0
- package/examples/templates/showcase/README.md +109 -0
- package/examples/templates/showcase/package.json +13 -0
- package/examples/templates/showcase/pnpm-lock.yaml +1858 -0
- package/examples/templates/showcase/pnpm-workspace.yaml +12 -0
- package/examples/templates/showcase/react-host/fulgurjs.config.ts +21 -0
- package/examples/templates/showcase/react-host/index.html +13 -0
- package/examples/templates/showcase/react-host/package.json +26 -0
- package/examples/templates/showcase/react-host/src/GuardBanner.tsx +33 -0
- package/examples/templates/showcase/react-host/src/Layout.tsx +42 -0
- package/examples/templates/showcase/react-host/src/ObsPanel.tsx +44 -0
- package/examples/templates/showcase/react-host/src/demo-log.ts +74 -0
- package/examples/templates/showcase/react-host/src/fulgurjs/types/vue-remote.d/_paths.d.ts +4 -0
- package/examples/templates/showcase/react-host/src/fulgurjs/types/vue-remote.d/bridge.ts +3 -0
- package/examples/templates/showcase/react-host/src/fulgurjs/types/vue-remote.d.ts +7 -0
- package/examples/templates/showcase/react-host/src/main.tsx +33 -0
- package/examples/templates/showcase/react-host/src/pages/AboutPage.tsx +18 -0
- package/examples/templates/showcase/react-host/src/pages/BridgeVuePage.tsx +77 -0
- package/examples/templates/showcase/react-host/src/pages/HomePage.tsx +14 -0
- package/examples/templates/showcase/react-host/src/routing.ts +30 -0
- package/examples/templates/showcase/react-host/tsconfig.json +15 -0
- package/examples/templates/showcase/react-host/vite.config.ts +8 -0
- package/examples/templates/showcase/react-remote/fulgurjs.config.ts +18 -0
- package/examples/templates/showcase/react-remote/index.html +13 -0
- package/examples/templates/showcase/react-remote/package.json +25 -0
- package/examples/templates/showcase/react-remote/src/ChildLayout.tsx +48 -0
- package/examples/templates/showcase/react-remote/src/bridge.tsx +62 -0
- package/examples/templates/showcase/react-remote/src/child-bus.ts +32 -0
- package/examples/templates/showcase/react-remote/src/main.tsx +18 -0
- package/examples/templates/showcase/react-remote/src/pages/Locked.tsx +23 -0
- package/examples/templates/showcase/react-remote/src/pages/OrderDetail.tsx +36 -0
- package/examples/templates/showcase/react-remote/src/pages/OrderList.tsx +110 -0
- package/examples/templates/showcase/react-remote/src/pages/Settings.tsx +50 -0
- package/examples/templates/showcase/react-remote/src/settings-store.ts +11 -0
- package/examples/templates/showcase/react-remote/tsconfig.json +14 -0
- package/examples/templates/showcase/react-remote/vite.config.ts +14 -0
- package/examples/templates/showcase/scripts/dev.config.json +8 -0
- package/examples/templates/showcase/scripts/dev.mjs +173 -0
- package/examples/templates/showcase/vue-host/fulgurjs.config.ts +21 -0
- package/examples/templates/showcase/vue-host/index.html +13 -0
- package/examples/templates/showcase/vue-host/package.json +25 -0
- package/examples/templates/showcase/vue-host/src/App.vue +50 -0
- package/examples/templates/showcase/vue-host/src/GuardBanner.vue +24 -0
- package/examples/templates/showcase/vue-host/src/ObsPanel.vue +60 -0
- package/examples/templates/showcase/vue-host/src/demo-log.ts +49 -0
- package/examples/templates/showcase/vue-host/src/fulgurjs/types/react-remote.d/_paths.d.ts +4 -0
- package/examples/templates/showcase/vue-host/src/fulgurjs/types/react-remote.d/bridge.ts +3 -0
- package/examples/templates/showcase/vue-host/src/fulgurjs/types/react-remote.d.ts +7 -0
- package/examples/templates/showcase/vue-host/src/guard.ts +47 -0
- package/examples/templates/showcase/vue-host/src/main.ts +30 -0
- package/examples/templates/showcase/vue-host/src/pages/AboutPage.vue +17 -0
- package/examples/templates/showcase/vue-host/src/pages/BridgeReactPage.vue +74 -0
- package/examples/templates/showcase/vue-host/src/pages/HomePage.vue +13 -0
- package/examples/templates/showcase/vue-host/src/routing.ts +31 -0
- package/examples/templates/showcase/vue-host/tsconfig.json +14 -0
- package/examples/templates/showcase/vue-remote/fulgurjs.config.ts +17 -0
- package/examples/templates/showcase/vue-remote/index.html +13 -0
- package/examples/templates/showcase/vue-remote/package.json +23 -0
- package/examples/templates/showcase/vue-remote/src/ChildLayout.vue +54 -0
- package/examples/templates/showcase/vue-remote/src/bridge.ts +48 -0
- package/examples/templates/showcase/vue-remote/src/child-bus.ts +32 -0
- package/examples/templates/showcase/vue-remote/src/main.ts +16 -0
- package/examples/templates/showcase/vue-remote/src/pages/Locked.vue +28 -0
- package/examples/templates/showcase/vue-remote/src/pages/OrderDetail.vue +46 -0
- package/examples/templates/showcase/vue-remote/src/pages/OrderList.vue +116 -0
- package/examples/templates/showcase/vue-remote/src/pages/Settings.vue +34 -0
- package/examples/templates/showcase/vue-remote/src/settings-store.ts +13 -0
- package/examples/templates/showcase/vue-remote/tsconfig.json +14 -0
- package/examples/templates/showcase/vue-remote/vite.config.ts +14 -0
- package/examples/templates/vue-host-react-remote/README.md +18 -0
- package/examples/templates/vue-host-react-remote/package.json +11 -0
- package/examples/templates/vue-host-react-remote/pnpm-lock.yaml +1663 -0
- package/examples/templates/vue-host-react-remote/pnpm-workspace.yaml +10 -0
- package/examples/{bridge → templates/vue-host-react-remote}/react-remote/README.md +9 -4
- package/examples/{bridge → templates/vue-host-react-remote}/react-remote/package.json +1 -1
- package/examples/templates/vue-host-react-remote/scripts/dev.config.json +6 -0
- package/examples/templates/vue-host-react-remote/scripts/dev.mjs +173 -0
- package/examples/{bridge → templates/vue-host-react-remote}/vue-host/README.md +8 -7
- package/examples/{bridge → templates/vue-host-react-remote}/vue-host/package.json +1 -1
- package/examples/{bridge → templates/vue-host-react-remote}/vue-host/src/App.vue +1 -1
- package/examples/templates/vue-host-react-remote/vue-host/src/fulgurjs/types/bridge-react-remote.d/_paths.d.ts +4 -0
- package/examples/templates/vue-host-react-remote/vue-host/src/fulgurjs/types/bridge-react-remote.d/bridge.ts +3 -0
- package/examples/templates/vue-host-react-remote/vue-host/src/fulgurjs/types/bridge-react-remote.d.ts +9 -0
- package/examples/{vue → templates/vue-vue}/README.md +9 -13
- package/examples/{vue → templates/vue-vue}/host/README.md +6 -8
- package/examples/{vue → templates/vue-vue}/host/package.json +1 -1
- package/examples/templates/vue-vue/host/src/fulgurjs/types/vue-remote.d/ClickButton.ts +4 -0
- package/examples/templates/vue-vue/host/src/fulgurjs/types/vue-remote.d/_paths.d.ts +4 -0
- package/examples/templates/vue-vue/host/src/fulgurjs/types/vue-remote.d/pages/DetailPage.ts +4 -0
- package/examples/templates/vue-vue/host/src/fulgurjs/types/vue-remote.d/pages/HomePage.ts +4 -0
- package/examples/templates/vue-vue/host/src/fulgurjs/types/vue-remote.d/utils.ts +2 -0
- package/examples/templates/vue-vue/host/src/fulgurjs/types/vue-remote.d.ts +29 -0
- package/examples/templates/vue-vue/host/vite.config.ts +8 -0
- package/examples/templates/vue-vue/package.json +11 -0
- package/examples/templates/vue-vue/pnpm-lock.yaml +1199 -0
- package/examples/templates/vue-vue/pnpm-workspace.yaml +10 -0
- package/examples/{vue → templates/vue-vue}/remote/README.md +7 -6
- package/examples/{vue → templates/vue-vue}/remote/package.json +1 -1
- package/examples/{vue → templates/vue-vue}/remote/src/App.vue +1 -1
- package/examples/templates/vue-vue/scripts/dev.config.json +6 -0
- package/examples/templates/vue-vue/scripts/dev.mjs +173 -0
- package/package.json +4 -4
- package/examples/README.en.md +0 -67
- package/examples/bridge/README.md +0 -36
- package/examples/bridge/react-host/package-lock.json +0 -2466
- package/examples/bridge/react-remote/package-lock.json +0 -2391
- package/examples/bridge/vue-host/package-lock.json +0 -1894
- package/examples/bridge/vue-remote/package-lock.json +0 -1865
- package/examples/react/host/package-lock.json +0 -2391
- package/examples/react/remote/package-lock.json +0 -2333
- package/examples/vue/host/package-lock.json +0 -1865
- package/examples/vue/remote/package-lock.json +0 -1843
- /package/examples/{bridge → templates/react-host-vue-remote}/react-host/fulgurjs.config.ts +0 -0
- /package/examples/{bridge → templates/react-host-vue-remote}/react-host/index.html +0 -0
- /package/examples/{bridge → templates/react-host-vue-remote}/react-host/src/env.d.ts +0 -0
- /package/examples/{bridge → templates/react-host-vue-remote}/react-host/src/host-session.ts +0 -0
- /package/examples/{bridge → templates/react-host-vue-remote}/react-host/src/main.tsx +0 -0
- /package/examples/{bridge → templates/react-host-vue-remote}/react-host/tsconfig.json +0 -0
- /package/examples/{bridge → templates/react-host-vue-remote}/react-host/vite.config.ts +0 -0
- /package/examples/{bridge → templates/react-host-vue-remote}/vue-remote/fulgurjs.config.ts +0 -0
- /package/examples/{bridge → templates/react-host-vue-remote}/vue-remote/index.html +0 -0
- /package/examples/{bridge → templates/react-host-vue-remote}/vue-remote/src/App.vue +0 -0
- /package/examples/{bridge → templates/react-host-vue-remote}/vue-remote/src/bridge.ts +0 -0
- /package/examples/{bridge/react-remote → templates/react-host-vue-remote/vue-remote}/src/env.d.ts +0 -0
- /package/examples/{bridge → templates/react-host-vue-remote}/vue-remote/src/main.ts +0 -0
- /package/examples/{bridge/vue-host → templates/react-host-vue-remote/vue-remote}/tsconfig.json +0 -0
- /package/examples/{bridge → templates/react-host-vue-remote}/vue-remote/vite.config.ts +0 -0
- /package/examples/{react → templates/react-react}/host/fulgurjs.config.ts +0 -0
- /package/examples/{react → templates/react-react}/host/index.html +0 -0
- /package/examples/{react → templates/react-react}/host/src/App.tsx +0 -0
- /package/examples/{react → templates/react-react}/host/src/main.tsx +0 -0
- /package/examples/{react → templates/react-react}/host/src/pages/UtilsDemo.tsx +0 -0
- /package/examples/{react → templates/react-react}/host/src/remotePages.tsx +0 -0
- /package/examples/{bridge/react-remote → templates/react-react/host}/tsconfig.json +0 -0
- /package/examples/{react → templates/react-react}/host/vite.config.ts +0 -0
- /package/examples/{react → templates/react-react}/remote/fulgurjs.config.ts +0 -0
- /package/examples/{react → templates/react-react}/remote/index.html +0 -0
- /package/examples/{react → templates/react-react}/remote/src/App.tsx +0 -0
- /package/examples/{react → templates/react-react}/remote/src/exposes/ClickButton.tsx +0 -0
- /package/examples/{react → templates/react-react}/remote/src/exposes/pages/DetailPage.tsx +0 -0
- /package/examples/{react → templates/react-react}/remote/src/exposes/pages/HomePage.tsx +0 -0
- /package/examples/{react → templates/react-react}/remote/src/exposes/utils.ts +0 -0
- /package/examples/{react → templates/react-react}/remote/src/main.tsx +0 -0
- /package/examples/{react/host → templates/react-react/remote}/tsconfig.json +0 -0
- /package/examples/{react → templates/react-react}/remote/vite.config.ts +0 -0
- /package/examples/{vue/host → templates/showcase/vue-host}/vite.config.ts +0 -0
- /package/examples/{bridge → templates/vue-host-react-remote}/react-remote/fulgurjs.config.ts +0 -0
- /package/examples/{bridge → templates/vue-host-react-remote}/react-remote/index.html +0 -0
- /package/examples/{bridge → templates/vue-host-react-remote}/react-remote/src/bridge.tsx +0 -0
- /package/examples/{bridge/vue-host → templates/vue-host-react-remote/react-remote}/src/env.d.ts +0 -0
- /package/examples/{bridge → templates/vue-host-react-remote}/react-remote/src/main.tsx +0 -0
- /package/examples/{react/remote → templates/vue-host-react-remote/react-remote}/tsconfig.json +0 -0
- /package/examples/{bridge → templates/vue-host-react-remote}/react-remote/vite.config.ts +0 -0
- /package/examples/{bridge → templates/vue-host-react-remote}/vue-host/fulgurjs.config.ts +0 -0
- /package/examples/{bridge → templates/vue-host-react-remote}/vue-host/index.html +0 -0
- /package/examples/{bridge/vue-remote → templates/vue-host-react-remote/vue-host}/src/env.d.ts +0 -0
- /package/examples/{bridge → templates/vue-host-react-remote}/vue-host/src/host-session.ts +0 -0
- /package/examples/{bridge → templates/vue-host-react-remote}/vue-host/src/main.ts +0 -0
- /package/examples/{bridge/vue-remote → templates/vue-host-react-remote/vue-host}/tsconfig.json +0 -0
- /package/examples/{bridge → templates/vue-host-react-remote}/vue-host/vite.config.ts +0 -0
- /package/examples/{vue → templates/vue-vue}/host/fulgurjs.config.ts +0 -0
- /package/examples/{vue → templates/vue-vue}/host/index.html +0 -0
- /package/examples/{vue → templates/vue-vue}/host/src/App.vue +0 -0
- /package/examples/{vue → templates/vue-vue}/host/src/main.ts +0 -0
- /package/examples/{vue → templates/vue-vue}/host/src/pages/HomePage.vue +0 -0
- /package/examples/{vue → templates/vue-vue}/host/src/pages/UtilsDemo.vue +0 -0
- /package/examples/{vue → templates/vue-vue}/host/tsconfig.json +0 -0
- /package/examples/{vue → templates/vue-vue}/remote/fulgurjs.config.ts +0 -0
- /package/examples/{vue → templates/vue-vue}/remote/index.html +0 -0
- /package/examples/{vue → templates/vue-vue}/remote/src/exposes/ClickButton.vue +0 -0
- /package/examples/{vue → templates/vue-vue}/remote/src/exposes/pages/DetailPage.vue +0 -0
- /package/examples/{vue → templates/vue-vue}/remote/src/exposes/pages/HomePage.vue +0 -0
- /package/examples/{vue → templates/vue-vue}/remote/src/exposes/utils.ts +0 -0
- /package/examples/{vue → templates/vue-vue}/remote/src/main.ts +0 -0
- /package/examples/{vue → templates/vue-vue}/remote/tsconfig.json +0 -0
- /package/examples/{vue → templates/vue-vue}/remote/vite.config.ts +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* examples/templates/showcase/react-remote/src/bridge.tsx — React 子应用桥接契约(exposes './bridge')。
|
|
3
|
+
* defineBridgeApp(factory, { routing: true }) 声明路由协议(缺失时宿主启用同步即 MFU-031 占位)。
|
|
4
|
+
* 受控路由 = createReactBridgeRouter(memory data router):工厂返回 RouterProvider 元素;
|
|
5
|
+
* 根路径 / 经 loader redirect 规范化到 /orders,由插件以 replace 同步宿主(无新增历史条目)。
|
|
6
|
+
*/
|
|
7
|
+
import type { ReactElement } from 'react'
|
|
8
|
+
import { redirect } from 'react-router-dom'
|
|
9
|
+
import { defineBridgeApp } from '@fulgurjs/federation/react'
|
|
10
|
+
import { createReactBridgeRouter } from '@fulgurjs/federation/bridge/router/react'
|
|
11
|
+
import ChildLayout from './ChildLayout'
|
|
12
|
+
import OrderList from './pages/OrderList'
|
|
13
|
+
import OrderDetail from './pages/OrderDetail'
|
|
14
|
+
import Settings from './pages/Settings'
|
|
15
|
+
import Locked from './pages/Locked'
|
|
16
|
+
import { bindReporter, reportNav } from './child-bus'
|
|
17
|
+
|
|
18
|
+
/** 初始位置读取用的最小 data router 结构面(避免耦合 react-router 内部类型路径) */
|
|
19
|
+
interface MemoryRouterProbe {
|
|
20
|
+
state: { location: { pathname: string; search: string; hash: string } }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const routes = [
|
|
24
|
+
{ path: '/', loader: () => redirect('/orders') },
|
|
25
|
+
{
|
|
26
|
+
element: <ChildLayout />,
|
|
27
|
+
children: [
|
|
28
|
+
{ path: '/orders', element: <OrderList /> },
|
|
29
|
+
{ path: '/orders/:id', element: <OrderDetail /> },
|
|
30
|
+
{ path: '/settings', element: <Settings /> },
|
|
31
|
+
{ path: '/locked', element: <Locked /> },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
export default defineBridgeApp((props, ctx) => {
|
|
37
|
+
if (!ctx?.routing) {
|
|
38
|
+
throw new Error('本契约需要宿主启用 URL 同步(mount 第三参数 routing)')
|
|
39
|
+
}
|
|
40
|
+
bindReporter(props)
|
|
41
|
+
const init = ctx.routing.getLocation()
|
|
42
|
+
const conn = createReactBridgeRouter(ctx.routing, routes, { signal: ctx.signal })
|
|
43
|
+
reportInitRedirect(conn.element as ReactElement<{ router?: MemoryRouterProbe }>, init.pathname + init.search + init.hash)
|
|
44
|
+
return conn.element
|
|
45
|
+
}, { routing: true })
|
|
46
|
+
|
|
47
|
+
/** 初始位置上报:createMemoryRouter 同步初始化,'/' 的 loader 重定向在微任务内落定,
|
|
48
|
+
* 下一个宏任务读取即为最终初始位置(订阅式一次性上报会在首个用户导航时才误触发)。 */
|
|
49
|
+
function reportInitRedirect(element: ReactElement<{ router?: MemoryRouterProbe }>, initPath: string): void {
|
|
50
|
+
const router = element.props.router
|
|
51
|
+
if (!router) return
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
const current = router.state.location.pathname + router.state.location.search + router.state.location.hash
|
|
54
|
+
reportNav(
|
|
55
|
+
'子应用初始化',
|
|
56
|
+
'init',
|
|
57
|
+
current !== initPath
|
|
58
|
+
? `根路径规范化 replace → ${current}(无新增历史条目)`
|
|
59
|
+
: `初始位置 → ${current}(与宿主深链一致)`,
|
|
60
|
+
)
|
|
61
|
+
}, 0)
|
|
62
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* examples/templates/showcase/react-remote/src/child-bus.ts — 子应用 → 宿主观测台上报总线。
|
|
3
|
+
* 宿主经 appProps 传入稳定回调 onChildEvent;本模块持有该引用并维护挂载计数。
|
|
4
|
+
* 单页单实例假设:同一宿主页同时只挂一个本子应用实例(路由同步前缀互斥的常规形态)。
|
|
5
|
+
*/
|
|
6
|
+
export type DemoChildNavAction = 'push' | 'replace' | 'go' | 'init'
|
|
7
|
+
|
|
8
|
+
export type DemoChildEvent =
|
|
9
|
+
| { type: 'mounted'; mountCount: number }
|
|
10
|
+
| { type: 'nav'; source: string; action: DemoChildNavAction; detail: string }
|
|
11
|
+
|
|
12
|
+
export type DemoChildReporter = (event: DemoChildEvent) => void
|
|
13
|
+
|
|
14
|
+
const bus: { report: DemoChildReporter | undefined } = { report: undefined }
|
|
15
|
+
let mountCount = 0
|
|
16
|
+
|
|
17
|
+
/** 桥接工厂内调用:从 props 快照中取出宿主回调,并上报本次挂载计数 */
|
|
18
|
+
export function bindReporter(props: Record<string, unknown> | undefined): void {
|
|
19
|
+
const candidate = (props as { onChildEvent?: DemoChildReporter } | undefined)?.onChildEvent
|
|
20
|
+
bus.report = typeof candidate === 'function' ? candidate : undefined
|
|
21
|
+
mountCount += 1
|
|
22
|
+
bus.report?.({ type: 'mounted', mountCount })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 导航来源上报(子应用 Link / 子应用 push·replace / 子应用 go / 子应用初始化) */
|
|
26
|
+
export function reportNav(source: string, action: DemoChildNavAction, detail: string): void {
|
|
27
|
+
bus.report?.({ type: 'nav', source, action, detail })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getMountCount(): number {
|
|
31
|
+
return mountCount
|
|
32
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* examples/templates/showcase/react-remote/src/main.tsx — 独立运行入口(仅 dev 占位)。
|
|
3
|
+
* 子应用真实能力经 exposes './bridge' 被宿主加载;直开本页仅作 dev server 健康检查。
|
|
4
|
+
*/
|
|
5
|
+
import { createRoot } from 'react-dom/client'
|
|
6
|
+
import type { ReactElement } from 'react'
|
|
7
|
+
|
|
8
|
+
function Standalone(): ReactElement {
|
|
9
|
+
return (
|
|
10
|
+
<div style={{ fontFamily: "'PingFang SC', 'Microsoft YaHei', sans-serif", padding: 24 }}>
|
|
11
|
+
<h1>react-remote(联邦子应用,5333)</h1>
|
|
12
|
+
<p>本应用通过 expose ./bridge(defineBridgeApp + routing 协议)被宿主加载。</p>
|
|
13
|
+
<p>请打开 Vue 宿主桥接页:<a href="http://localhost:5334/br-react/orders">http://localhost:5334/br-react/orders</a></p>
|
|
14
|
+
</div>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
createRoot(document.getElementById('root')!).render(<Standalone />)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* examples/templates/showcase/react-remote/src/pages/Locked.tsx — 锁定页(守卫放行后才可见)。
|
|
3
|
+
* 前往本页的导航会被宿主守卫(Vue:异步 beforeEach;React:useBlocker)拦截,等待「继续/取消」。
|
|
4
|
+
*/
|
|
5
|
+
import type { ReactElement } from 'react'
|
|
6
|
+
import { Link } from 'react-router-dom'
|
|
7
|
+
import { reportNav } from '../child-bus'
|
|
8
|
+
|
|
9
|
+
export default function Locked(): ReactElement {
|
|
10
|
+
const handleBack = (): void => {
|
|
11
|
+
reportNav('子应用 Link', 'push', '/orders')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<section style={{ padding: '8px 0' }}>
|
|
16
|
+
<h3 style={{ margin: '4px 0' }}>锁定页(子应用路由 /locked)</h3>
|
|
17
|
+
<p style={{ margin: '4px 0' }}>你能看到本页,说明宿主守卫选择了「继续」。</p>
|
|
18
|
+
<p style={{ margin: '4px 0' }}>
|
|
19
|
+
<Link to="/orders" onClick={handleBack}>回工单列表</Link>
|
|
20
|
+
</p>
|
|
21
|
+
</section>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* examples/templates/showcase/react-remote/src/pages/OrderDetail.tsx — 工单详情页(路径参数 /orders/:id)。
|
|
3
|
+
* 「返回列表」走 go(-1)(委托宿主浏览器历史);「回列表」走 push(经通道写宿主 URL)。
|
|
4
|
+
*/
|
|
5
|
+
import type { ReactElement } from 'react'
|
|
6
|
+
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
|
7
|
+
import { reportNav } from '../child-bus'
|
|
8
|
+
|
|
9
|
+
export default function OrderDetail(): ReactElement {
|
|
10
|
+
const params = useParams()
|
|
11
|
+
const location = useLocation()
|
|
12
|
+
const navigate = useNavigate()
|
|
13
|
+
const id = params.id ?? ''
|
|
14
|
+
const queryEcho = location.search === '' ? '(无)' : location.search
|
|
15
|
+
|
|
16
|
+
const handleBack = (): void => {
|
|
17
|
+
reportNav('子应用 go', 'go', '-1')
|
|
18
|
+
navigate(-1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const handleLinkBack = (): void => {
|
|
22
|
+
reportNav('子应用 Link', 'push', '/orders')
|
|
23
|
+
void navigate('/orders')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<section style={{ padding: '8px 0' }}>
|
|
28
|
+
<h3 style={{ margin: '4px 0' }}>工单详情(子应用路由 /orders/:id)</h3>
|
|
29
|
+
<p style={{ margin: '4px 0' }}>工单 ID:<code>{id}</code>;query 回显:<code>{queryEcho}</code></p>
|
|
30
|
+
<div style={{ display: 'flex', gap: 8, margin: '8px 0', flexWrap: 'wrap' }}>
|
|
31
|
+
<button onClick={handleBack}>返回列表(go(-1))</button>
|
|
32
|
+
<button onClick={handleLinkBack}>回列表(push /orders)</button>
|
|
33
|
+
</div>
|
|
34
|
+
</section>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* examples/templates/showcase/react-remote/src/pages/OrderList.tsx — 工单列表页(筛选 + 分页)。
|
|
3
|
+
* 筛选关键词与页码写入子应用路由 query(q=xxx&page=n,中文可输入),
|
|
4
|
+
* 经桥接通道同步到宿主 URL;跳详情再返回时组件重建、状态从 query 还原。
|
|
5
|
+
*/
|
|
6
|
+
import { useMemo, useState } from 'react'
|
|
7
|
+
import type { ChangeEvent, ReactElement } from 'react'
|
|
8
|
+
import { useLocation, useNavigate } from 'react-router-dom'
|
|
9
|
+
import { reportNav } from '../child-bus'
|
|
10
|
+
|
|
11
|
+
interface OrderItem {
|
|
12
|
+
id: number
|
|
13
|
+
title: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ORDERS: OrderItem[] = [
|
|
17
|
+
{ id: 1, title: '登录异常排查' },
|
|
18
|
+
{ id: 2, title: '网络抖动投诉' },
|
|
19
|
+
{ id: 3, title: '权限配置变更' },
|
|
20
|
+
{ id: 4, title: '数据导出失败' },
|
|
21
|
+
{ id: 5, title: '消息推送延迟' },
|
|
22
|
+
{ id: 6, title: '账单核对差异' },
|
|
23
|
+
{ id: 7, title: '接口超时告警' },
|
|
24
|
+
{ id: 8, title: '存储扩容申请' },
|
|
25
|
+
]
|
|
26
|
+
const PAGE_SIZE = 3
|
|
27
|
+
|
|
28
|
+
export default function OrderList(): ReactElement {
|
|
29
|
+
const location = useLocation()
|
|
30
|
+
const navigate = useNavigate()
|
|
31
|
+
const search = new URLSearchParams(location.search)
|
|
32
|
+
const q = search.get('q') ?? ''
|
|
33
|
+
const pageRaw = Number.parseInt(search.get('page') ?? '1', 10)
|
|
34
|
+
const page = Number.isFinite(pageRaw) && pageRaw > 0 ? pageRaw : 1
|
|
35
|
+
const [qInput, setQInput] = useState(q)
|
|
36
|
+
|
|
37
|
+
const filtered = useMemo(
|
|
38
|
+
() => (q === '' ? ORDERS : ORDERS.filter((item) => item.title.includes(q))),
|
|
39
|
+
[q],
|
|
40
|
+
)
|
|
41
|
+
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
|
|
42
|
+
const pageItems = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
|
|
43
|
+
|
|
44
|
+
const buildTarget = (qValue: string, pageValue: number): string =>
|
|
45
|
+
`/orders?page=${pageValue}${qValue === '' ? '' : `&q=${encodeURIComponent(qValue)}`}`
|
|
46
|
+
|
|
47
|
+
const handleQInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
|
48
|
+
setQInput(event.target.value)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleSearch = (): void => {
|
|
52
|
+
const target = buildTarget(qInput.trim(), 1)
|
|
53
|
+
reportNav('子应用 push/replace', 'push', target)
|
|
54
|
+
void navigate(target)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handlePageStep = (delta: number): void => {
|
|
58
|
+
const next = Math.min(totalPages, Math.max(1, page + delta))
|
|
59
|
+
if (next === page) return
|
|
60
|
+
const target = buildTarget(q, next)
|
|
61
|
+
reportNav('子应用 push/replace', 'push', target)
|
|
62
|
+
void navigate(target)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleOpenDetail = (item: OrderItem): void => {
|
|
66
|
+
reportNav('子应用 Link', 'push', `/orders/${item.id}?src=row`)
|
|
67
|
+
void navigate(`/orders/${item.id}?src=row`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handleGoSettings = (): void => {
|
|
71
|
+
reportNav('子应用 Link', 'push', '/settings')
|
|
72
|
+
void navigate('/settings')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const handleGoLocked = (): void => {
|
|
76
|
+
reportNav('子应用 Link', 'push', '/locked')
|
|
77
|
+
void navigate('/locked')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<section style={{ padding: '8px 0' }}>
|
|
82
|
+
<h3 style={{ margin: '4px 0' }}>工单列表(子应用路由 /orders)</h3>
|
|
83
|
+
<p style={{ color: '#666', fontSize: 13, margin: '4px 0' }}>
|
|
84
|
+
当前筛选:q={q === '' ? '(空)' : q},页码:{page}/{totalPages}(来自子应用路由 query)
|
|
85
|
+
</p>
|
|
86
|
+
<div style={{ display: 'flex', gap: 8, margin: '8px 0', flexWrap: 'wrap' }}>
|
|
87
|
+
<input value={qInput} onChange={handleQInputChange} placeholder="输入筛选关键词(支持中文,如:网络)" aria-label="筛选关键词" />
|
|
88
|
+
<button onClick={handleSearch}>筛选(push ?q=&page=1)</button>
|
|
89
|
+
</div>
|
|
90
|
+
<ul style={{ listStyle: 'none', padding: 0, margin: '8px 0' }}>
|
|
91
|
+
{pageItems.map((item) => (
|
|
92
|
+
<li key={item.id} style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '4px 0' }}>
|
|
93
|
+
<span>#{item.id} {item.title}</span>
|
|
94
|
+
<button onClick={() => handleOpenDetail(item)}>查看详情</button>
|
|
95
|
+
</li>
|
|
96
|
+
))}
|
|
97
|
+
{pageItems.length === 0 && <li style={{ color: '#666' }}>无匹配工单</li>}
|
|
98
|
+
</ul>
|
|
99
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center', margin: '8px 0' }}>
|
|
100
|
+
<button disabled={page <= 1} onClick={() => handlePageStep(-1)}>上一页</button>
|
|
101
|
+
<span>第 {page} / {totalPages} 页</span>
|
|
102
|
+
<button disabled={page >= totalPages} onClick={() => handlePageStep(1)}>下一页</button>
|
|
103
|
+
</div>
|
|
104
|
+
<div style={{ display: 'flex', gap: 8, margin: '8px 0', flexWrap: 'wrap' }}>
|
|
105
|
+
<button onClick={handleGoSettings}>前往设置页</button>
|
|
106
|
+
<button onClick={handleGoLocked}>锁定页(触发宿主守卫)</button>
|
|
107
|
+
</div>
|
|
108
|
+
</section>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* examples/templates/showcase/react-remote/src/pages/Settings.tsx — 设置页(本地状态表单,状态刻意不进 URL)。
|
|
3
|
+
* 修改直写模块级 settings-store(write-through):路由往返后仍保留 → 佐证子应用 store 未重建。
|
|
4
|
+
*/
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import type { ChangeEvent, ReactElement } from 'react'
|
|
7
|
+
import { settingsState } from '../settings-store'
|
|
8
|
+
|
|
9
|
+
export default function Settings(): ReactElement {
|
|
10
|
+
const [nickname, setNickname] = useState(settingsState.nickname)
|
|
11
|
+
const [notifyEnabled, setNotifyEnabled] = useState(settingsState.notifyEnabled)
|
|
12
|
+
const [theme, setTheme] = useState(settingsState.theme)
|
|
13
|
+
|
|
14
|
+
const handleNicknameChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
|
15
|
+
setNickname(event.target.value)
|
|
16
|
+
settingsState.nickname = event.target.value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const handleNotifyChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
|
20
|
+
setNotifyEnabled(event.target.checked)
|
|
21
|
+
settingsState.notifyEnabled = event.target.checked
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const handleThemeChange = (event: ChangeEvent<HTMLSelectElement>): void => {
|
|
25
|
+
setTheme(event.target.value as 'light' | 'dark')
|
|
26
|
+
settingsState.theme = event.target.value as 'light' | 'dark'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<section style={{ padding: '8px 0' }}>
|
|
31
|
+
<h3 style={{ margin: '4px 0' }}>设置(子应用路由 /settings,本地状态)</h3>
|
|
32
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, margin: '8px 0', maxWidth: 320 }}>
|
|
33
|
+
<label>昵称:<input value={nickname} onChange={handleNicknameChange} /></label>
|
|
34
|
+
<label>
|
|
35
|
+
<input type="checkbox" checked={notifyEnabled} onChange={handleNotifyChange} /> 接收通知
|
|
36
|
+
</label>
|
|
37
|
+
<label>
|
|
38
|
+
主题:
|
|
39
|
+
<select value={theme} onChange={handleThemeChange}>
|
|
40
|
+
<option value="light">浅色</option>
|
|
41
|
+
<option value="dark">深色</option>
|
|
42
|
+
</select>
|
|
43
|
+
</label>
|
|
44
|
+
</div>
|
|
45
|
+
<p style={{ color: '#666', fontSize: 13, margin: '4px 0' }}>
|
|
46
|
+
本表单为子应用本地状态(不在 URL 中)。路由往返后设置仍在 → 子应用未重挂、store 未重建。
|
|
47
|
+
</p>
|
|
48
|
+
</section>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* examples/templates/showcase/react-remote/src/settings-store.ts — 设置页本地状态(模块级对象,刻意不写入 URL)。
|
|
3
|
+
* 子应用 root 不重挂 → 模块不重建:路由往返后设置仍保留,佐证「内部状态跨路由保留」。
|
|
4
|
+
*/
|
|
5
|
+
export interface SettingsState {
|
|
6
|
+
nickname: string
|
|
7
|
+
notifyEnabled: boolean
|
|
8
|
+
theme: 'light' | 'dark'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const settingsState: SettingsState = { nickname: '张三', notifyEnabled: true, theme: 'light' }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"types": ["vite/client"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src", "fulgurjs.config.ts", "vite.config.ts"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import { federation } from '@fulgurjs/federation'
|
|
4
|
+
import fulgurjsConfig from './fulgurjs.config.ts'
|
|
5
|
+
|
|
6
|
+
export default defineConfig(({ command }) => ({
|
|
7
|
+
base: command === 'build' ? '/react-remote/' : undefined,
|
|
8
|
+
// react-router-dom 必须正常进预构建(同 e2e 实测的 fixtures/remote-react:不 exclude、不 alias):
|
|
9
|
+
// 1) 其传递依赖 cookie 是 CJS-only 包,exclude 后走源码会在浏览器报
|
|
10
|
+
// "does not provide an export named 'parse'";
|
|
11
|
+
// 2) 预构建内的 react 导入不会双实例——插件 dev 会向预构建注入 shared 键外部化
|
|
12
|
+
// resolver(fulgurjs:optimize-shared-external),react 运行时仍协商到 shared singleton 门面。
|
|
13
|
+
plugins: [react(), federation(fulgurjsConfig)],
|
|
14
|
+
}))
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apps": [
|
|
3
|
+
{ "name": "vue-remote", "dir": "vue-remote", "port": 5335 },
|
|
4
|
+
{ "name": "react-remote", "dir": "react-remote", "port": 5333 },
|
|
5
|
+
{ "name": "vue-host", "dir": "vue-host", "port": 5334 },
|
|
6
|
+
{ "name": "react-host", "dir": "react-host", "port": 5336 }
|
|
7
|
+
]
|
|
8
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 模板统一 dev 启动器(规范源)。
|
|
4
|
+
*
|
|
5
|
+
* 本文件经 examples/scripts/sync-template-scripts.mjs 同步到五个模板的 scripts/dev.mjs;
|
|
6
|
+
* 模板内副本必须与规范源逐字节一致(check-catalog.mjs 校验哈希),模板复制出仓库后独立可用——
|
|
7
|
+
* 只依赖同目录 dev.config.json 与 Node 内置模块,不依赖仓库其他文件。
|
|
8
|
+
*
|
|
9
|
+
* 行为契约(与模板 README 一致):
|
|
10
|
+
* - 启动前预检全部端口:任何端口被占用则拒绝启动并指认处理方法(绝不按端口杀别人的进程);
|
|
11
|
+
* - 按 dev.config.json 顺序(远程在前宿主在后)启动各应用,逐个探活;
|
|
12
|
+
* - 任一应用启动失败/超时/中途退出:整组停止(只杀本次启动的子进程),非零退出并打印该应用日志尾部;
|
|
13
|
+
* - Ctrl+C / SIGTERM:清理本次启动的全部子进程后退出;不依赖 PID 文件;
|
|
14
|
+
* - 全部就绪后打印访问入口;探活 = 端口可连接(dev server 已监听),页面可用以浏览器验收为准。
|
|
15
|
+
*
|
|
16
|
+
* 平台:macOS/Linux 实测;Windows 走 taskkill 兜底路径,未经验证(见模板 README)。
|
|
17
|
+
*/
|
|
18
|
+
import { spawn } from 'node:child_process'
|
|
19
|
+
import fs from 'node:fs'
|
|
20
|
+
import net from 'node:net'
|
|
21
|
+
import path from 'node:path'
|
|
22
|
+
import { fileURLToPath } from 'node:url'
|
|
23
|
+
|
|
24
|
+
const templateRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
25
|
+
const { apps, readyTimeoutMs = 120000 } = JSON.parse(
|
|
26
|
+
fs.readFileSync(path.join(templateRoot, 'scripts', 'dev.config.json'), 'utf8'),
|
|
27
|
+
)
|
|
28
|
+
const isTTY = process.stdout.isTTY
|
|
29
|
+
const color = (code, text) => (isTTY ? `\x1b[${code}m${text}\x1b[0m` : text)
|
|
30
|
+
const EXIT = { ok: 0, startupFailed: 1, portConflict: 2, signal: 130 }
|
|
31
|
+
|
|
32
|
+
/** 双栈端口探测(vite 可能只绑 IPv4 或 IPv6 回环) */
|
|
33
|
+
function isPortUp(port) {
|
|
34
|
+
const probe = (host) =>
|
|
35
|
+
new Promise((resolve, reject) => {
|
|
36
|
+
const s = net.connect({ port, host, timeout: 800 })
|
|
37
|
+
s.on('connect', () => { s.destroy(); resolve(true) })
|
|
38
|
+
s.on('error', reject)
|
|
39
|
+
s.on('timeout', () => { s.destroy(); reject(new Error('timeout')) })
|
|
40
|
+
})
|
|
41
|
+
return Promise.any([probe('127.0.0.1'), probe('::1')]).catch(() => false)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const tailOf = new Map()
|
|
45
|
+
const children = new Map()
|
|
46
|
+
let shuttingDown = false
|
|
47
|
+
|
|
48
|
+
function recordOutput(name, chunk) {
|
|
49
|
+
const lines = (tailOf.get(name) ?? [])
|
|
50
|
+
for (const line of String(chunk).split('\n')) {
|
|
51
|
+
if (line === '') continue
|
|
52
|
+
lines.push(line)
|
|
53
|
+
}
|
|
54
|
+
while (lines.length > 60) lines.shift()
|
|
55
|
+
tailOf.set(name, lines)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printAppLine(name, text) {
|
|
59
|
+
process.stdout.write(`${color('2', `[${name}]`)} ${text}\n`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function preflight() {
|
|
63
|
+
const conflicts = []
|
|
64
|
+
for (const app of apps) {
|
|
65
|
+
if (await isPortUp(app.port)) conflicts.push(app)
|
|
66
|
+
}
|
|
67
|
+
if (conflicts.length === 0) return
|
|
68
|
+
console.error(color('31', '[dev] 启动前检查失败:以下端口已被占用(不区分占用者,一律不启动):'))
|
|
69
|
+
for (const app of conflicts) {
|
|
70
|
+
const posix = `lsof -nP -iTCP:${app.port} -sTCP:LISTEN`
|
|
71
|
+
const win = `netstat -ano | findstr :${app.port}`
|
|
72
|
+
console.error(
|
|
73
|
+
` ${app.name} → 端口 ${app.port} 已监听。查看占用:${process.platform === 'win32' ? win : posix};` +
|
|
74
|
+
`结束占用进程,或同步修改 ${app.dir}/package.json 的 dev/preview 端口与宿主 fulgurjs.config.ts 的 remotes dev 地址后重试`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
console.error('[dev] 本启动器只清理自己启动的进程,不会替你结束端口上的既有服务。')
|
|
78
|
+
process.exit(EXIT.portConflict)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function spawnApp(app) {
|
|
82
|
+
const child = spawn('pnpm', ['--dir', app.dir, 'run', 'dev'], {
|
|
83
|
+
cwd: templateRoot,
|
|
84
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
85
|
+
// POSIX:独立进程组,便于整组终止;Windows:shell 解析 pnpm.cmd,树杀交给 taskkill
|
|
86
|
+
detached: true,
|
|
87
|
+
shell: process.platform === 'win32',
|
|
88
|
+
})
|
|
89
|
+
children.set(app.name, child)
|
|
90
|
+
child.stdout.on('data', (c) => { recordOutput(app.name, c); String(c).split('\n').filter(Boolean).forEach((l) => printAppLine(app.name, l)) })
|
|
91
|
+
child.stderr.on('data', (c) => { recordOutput(app.name, c); String(c).split('\n').filter(Boolean).forEach((l) => printAppLine(app.name, l)) })
|
|
92
|
+
return child
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const exitedOnce = (child) =>
|
|
96
|
+
new Promise((resolve) => {
|
|
97
|
+
child.once('exit', (code, signal) => resolve({ code, signal }))
|
|
98
|
+
child.once('error', (e) => resolve({ code: -1, signal: null, error: e }))
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
async function waitReady(app, child) {
|
|
102
|
+
const exited = exitedOnce(child)
|
|
103
|
+
const deadline = Date.now() + readyTimeoutMs
|
|
104
|
+
while (Date.now() < deadline) {
|
|
105
|
+
if (await isPortUp(app.port)) return { ok: true }
|
|
106
|
+
const done = await Promise.race([exited.then(() => 'exit'), new Promise((r) => setTimeout(() => r('tick'), 400))])
|
|
107
|
+
if (done === 'exit') return { ok: false, reason: `进程提前退出(启动命令失败——看下方日志尾部)` }
|
|
108
|
+
}
|
|
109
|
+
return { ok: false, reason: `${Math.round(readyTimeoutMs / 1000)}s 内端口 ${app.port} 未就绪(dev server 未监听)` }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function killTree(appName) {
|
|
113
|
+
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() })
|
|
120
|
+
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() }
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function shutdown(exitCode, reason) {
|
|
128
|
+
if (shuttingDown) return
|
|
129
|
+
shuttingDown = true
|
|
130
|
+
if (reason) console.log(`[dev] ${reason}`)
|
|
131
|
+
console.log(`[dev] 正在停止本次启动的 ${children.size} 个应用…`)
|
|
132
|
+
await Promise.all([...children.keys()].map(killTree))
|
|
133
|
+
console.log('[dev] 已全部停止。')
|
|
134
|
+
process.exit(exitCode)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
process.on('SIGINT', () => void shutdown(EXIT.signal, '[dev] 收到 Ctrl+C'))
|
|
138
|
+
process.on('SIGTERM', () => void shutdown(143, '[dev] 收到 SIGTERM'))
|
|
139
|
+
|
|
140
|
+
await preflight()
|
|
141
|
+
|
|
142
|
+
console.log(`[dev] 模板 ${path.basename(templateRoot)}:按顺序启动 ${apps.map((a) => a.name).join(' → ')}`)
|
|
143
|
+
for (const app of apps) {
|
|
144
|
+
printAppLine(app.name, `启动(pnpm --dir ${app.dir} run dev),等待端口 ${app.port} 就绪…`)
|
|
145
|
+
const child = spawnApp(app)
|
|
146
|
+
const ready = await waitReady(app, child)
|
|
147
|
+
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)
|
|
157
|
+
}
|
|
158
|
+
printAppLine(app.name, `就绪 → http://localhost:${app.port}/`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(color('32', '\n[dev] 全部应用已就绪:'))
|
|
162
|
+
for (const app of [...apps].reverse()) {
|
|
163
|
+
console.log(` http://localhost:${app.port}/ ${app.name}`)
|
|
164
|
+
}
|
|
165
|
+
console.log('[dev] Ctrl+C 停止全部应用(只清理本次启动的进程)。dev server 已监听 ≠ 页面必然可用,验收请以浏览器实际加载为准。\n')
|
|
166
|
+
|
|
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
|
+
await new Promise(() => {})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FederationOptions } from '@fulgurjs/federation'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vue 宿主联邦配置(examples/templates/showcase/vue-host,端口 5334)。
|
|
5
|
+
* 桥接宿主双框架安装合同:vue + react + react-dom 三键全部 singleton——
|
|
6
|
+
* React 子应用的 shared 实例经此协商单例,缺失会表现为 Invalid hook call / 双实例(MFU-010)。
|
|
7
|
+
*/
|
|
8
|
+
export default {
|
|
9
|
+
name: 'vue-host',
|
|
10
|
+
remotes: {
|
|
11
|
+
'react-remote': {
|
|
12
|
+
dev: 'http://localhost:5333',
|
|
13
|
+
prod: '/react-remote',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
shared: {
|
|
17
|
+
vue: { singleton: true },
|
|
18
|
+
react: { singleton: true },
|
|
19
|
+
'react-dom': { singleton: true },
|
|
20
|
+
},
|
|
21
|
+
} satisfies FederationOptions
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" href="data:," />
|
|
7
|
+
<title>vue-host(桥接 URL 同步宿主 × React 子应用,5334)</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fulgurjs-demo-bridge-vue-host",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite --port 5334 --strictPort",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview --port 5334 --strictPort",
|
|
10
|
+
"typecheck": "vue-tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@fulgurjs/federation": "5.9.0",
|
|
14
|
+
"react": "^19.1.0",
|
|
15
|
+
"react-dom": "^19.1.0",
|
|
16
|
+
"vue": "^3.5.13",
|
|
17
|
+
"vue-router": "^4.5.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
21
|
+
"typescript": "^5.8.0",
|
|
22
|
+
"vite": "^6.3.5",
|
|
23
|
+
"vue-tsc": "^2.2.0"
|
|
24
|
+
}
|
|
25
|
+
}
|