@fulgurjs/federation 5.3.3 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/DESIGN.md +4 -0
  3. package/README.en.md +23 -3
  4. package/README.md +83 -1
  5. package/dist/bridge-app-react.cjs +14 -4
  6. package/dist/bridge-app-react.d.cts +16 -4
  7. package/dist/bridge-app-react.d.ts +16 -4
  8. package/dist/bridge-app-react.js +6 -4
  9. package/dist/bridge-app-vue.cjs +55 -9
  10. package/dist/bridge-app-vue.d.cts +25 -4
  11. package/dist/bridge-app-vue.d.ts +25 -4
  12. package/dist/bridge-app-vue.js +47 -9
  13. package/dist/bridge-core-BOWvVUxO.d.ts +91 -0
  14. package/dist/bridge-core.cjs +9 -1
  15. package/dist/bridge-core.d.cts +8 -1
  16. package/dist/bridge-core.d.ts +8 -1
  17. package/dist/bridge-core.js +2 -2
  18. package/dist/bridge-errors.cjs +55 -3
  19. package/dist/bridge-errors.d.cts +18 -2
  20. package/dist/bridge-errors.d.ts +18 -2
  21. package/dist/bridge-errors.js +11 -3
  22. package/dist/{bridge-host-react-C55RmGll.d.ts → bridge-host-react-CMVd-ym0.d.ts} +1 -1
  23. package/dist/bridge-host-react.cjs +324 -3
  24. package/dist/bridge-host-react.d.cts +2 -0
  25. package/dist/bridge-host-react.d.ts +2 -0
  26. package/dist/bridge-host-react.js +30 -4
  27. package/dist/{bridge-host-vue-BAxDKqGH.d.ts → bridge-host-vue-BuCGlLdS.d.ts} +1 -1
  28. package/dist/bridge-host-vue.cjs +335 -3
  29. package/dist/bridge-host-vue.js +42 -5
  30. package/dist/bridge-react.d.ts +5 -3
  31. package/dist/bridge-router-core-BxBx-reP.d.cts +44 -0
  32. package/dist/bridge-router-core-BxBx-reP.d.ts +44 -0
  33. package/dist/bridge-router-react.d.ts +96 -0
  34. package/dist/bridge-router-react.js +70 -0
  35. package/dist/bridge-router-vue.d.ts +75 -0
  36. package/dist/bridge-router-vue.js +73 -0
  37. package/dist/bridge-vue.d.ts +2 -2
  38. package/dist/bridge.d.ts +6 -4
  39. package/dist/{chunk-WD2ON7SN.js → chunk-BTJGYDHT.js} +303 -2
  40. package/dist/{chunk-GWJKABE7.js → chunk-GF6KOH67.js} +39 -4
  41. package/dist/{chunk-DH2QMM4Y.js → chunk-JQF7KYZF.js} +50 -2
  42. package/dist/{chunk-ZTRJLCOF.js → chunk-OXEAGRPY.js} +27 -3
  43. package/dist/{chunk-LE2G23E7.js → chunk-PRUYHYX2.js} +1 -1
  44. package/dist/chunk-WOTR2FUQ.js +266 -0
  45. package/dist/index.cjs +2 -2
  46. package/dist/index.js +2 -2
  47. package/dist/react.d.ts +51 -4
  48. package/dist/runtime-entry.d.ts +60 -4
  49. package/dist/runtime.js +1 -1
  50. package/examples/bridge/react-host/package-lock.json +105 -104
  51. package/examples/bridge/react-remote/package-lock.json +105 -104
  52. package/examples/bridge/vue-host/package-lock.json +105 -104
  53. package/examples/bridge/vue-remote/package-lock.json +105 -104
  54. package/examples/react/host/package-lock.json +1 -1
  55. package/examples/react/remote/package-lock.json +1 -1
  56. package/examples/vue/host/package-lock.json +1 -1
  57. package/examples/vue/remote/package-lock.json +1 -1
  58. package/package.json +28 -4
  59. package/dist/bridge-core-6uLJiMVa.d.ts +0 -42
@@ -0,0 +1,96 @@
1
+ import { ReactElement } from 'react';
2
+ import { RouteObject } from 'react-router-dom';
3
+
4
+ /** 统一逻辑位置:pathname 以 / 开头;search 空串或 ? 开头;hash 空串或 # 开头 */
5
+ interface BridgeLocation {
6
+ pathname: string;
7
+ search: string;
8
+ hash: string;
9
+ }
10
+ type BridgeNavigationResult = {
11
+ status: 'committed';
12
+ location: BridgeLocation;
13
+ } | {
14
+ status: 'cancelled';
15
+ location: BridgeLocation;
16
+ };
17
+ /** 宿主导航端口(宿主适配器把 Router 包装成本接口;逻辑路径不含部署 base) */
18
+ interface BridgeHostNavigation {
19
+ getLocation(): BridgeLocation;
20
+ subscribe(listener: (location: BridgeLocation) => void): () => void;
21
+ navigate(target: BridgeLocation, action: 'push' | 'replace'): Promise<BridgeNavigationResult>;
22
+ go(delta: number): void;
23
+ }
24
+ /** 子应用路由通道(桥接宿主按挂载代次创建,随 mount 第三参数交给子应用) */
25
+ interface BridgeChildRoute {
26
+ getLocation(): BridgeLocation;
27
+ subscribe(listener: (location: BridgeLocation) => void): () => void;
28
+ navigate(target: BridgeLocation, action: 'push' | 'replace'): Promise<BridgeNavigationResult>;
29
+ go(delta: number): void;
30
+ }
31
+
32
+ /**
33
+ * React Router 路由适配(/bridge/router/react 按需入口;URL 同步任务书 §3.1/§3.3)。
34
+ *
35
+ * 实际支持模式(原型实证,任务书 §1 结论 2):
36
+ * - 宿主:data router(createBrowserRouter / createHashRouter + RouterProvider)。
37
+ * router.navigate 被 blocker 拦截时静默返回 void(dist 源码核实)——取消用
38
+ * 「与 useBlocker 同一取消策略预判」:canNavigate(target) 返回 false 即 cancelled,
39
+ * URL/历史零副作用;菜单/POP 等其他来源仍由树内 useBlocker 真实拦截。
40
+ * - 子应用:createMemoryRouter(RR 6.11+)受控路由——Link/useNavigate 全兼容,
41
+ * 通道广播与本地导航双向桥接,取消时乐观回滚。
42
+ * - declarative 模式(BrowserRouter)无 blocker/提交语义,不支持 URL 同步(文档声明)。
43
+ */
44
+
45
+ /** 宿主取消策略(与树内 useBlocker 共用同一谓词,单一事实源) */
46
+ type ReactBridgeCancelPolicy = (next: BridgeLocation) => boolean;
47
+ /** data router 的最小结构面(避免耦合 RR 内部类型版本) */
48
+ interface ReactDataRouterLike {
49
+ state: {
50
+ location: {
51
+ pathname: string;
52
+ search: string;
53
+ hash: string;
54
+ };
55
+ };
56
+ subscribe(cb: () => void): () => void;
57
+ navigate(to: string | number, opts?: {
58
+ replace?: boolean;
59
+ }): void | Promise<void>;
60
+ }
61
+ /**
62
+ * 宿主侧:把 RR data router 包装为宿主导航端口。
63
+ *
64
+ * ```tsx
65
+ * const router = createBrowserRouter(routes, { basename })
66
+ * const navigation = createReactBridgeNavigation(router, {
67
+ * basename, // = createBrowserRouter 的 basename(含它;RR location.pathname 不剥 basename)
68
+ * canNavigate: (next) => !next.pathname.startsWith('/approval/secret'), // 与 useBlocker 同一谓词
69
+ * })
70
+ * ```
71
+ */
72
+ declare function createReactBridgeNavigation(router: ReactDataRouterLike, options?: {
73
+ canNavigate?: ReactBridgeCancelPolicy;
74
+ basename?: string;
75
+ }): BridgeHostNavigation;
76
+ interface ReactBridgeRouterConnection {
77
+ /** RouterProvider 元素:子应用在自己的 root 内渲染 */
78
+ element: ReactElement;
79
+ /** 清理本接线(通道订阅与本地监听;通道本身由宿主生命周期销毁) */
80
+ dispose(): void;
81
+ }
82
+ /**
83
+ * 子应用侧:把子应用自己的 routes 接到通道,返回 RouterProvider 元素。
84
+ *
85
+ * ```tsx
86
+ * export default defineBridgeApp((props, ctx) => {
87
+ * const conn = createReactBridgeRouter(ctx.routing, routes)
88
+ * const root = createRoot(el)
89
+ * root.render(conn.element)
90
+ * return { mount: () => {}, unmount: () => { conn.dispose(); root.unmount() } }
91
+ * }, { routing: true })
92
+ * ```
93
+ */
94
+ declare function createReactBridgeRouter(routing: BridgeChildRoute, routes: RouteObject[]): ReactBridgeRouterConnection;
95
+
96
+ export { type ReactBridgeCancelPolicy, type ReactBridgeRouterConnection, type ReactDataRouterLike, createReactBridgeNavigation, createReactBridgeRouter };
@@ -0,0 +1,70 @@
1
+ // src/bridge-router-react.tsx
2
+ import { createElement } from "react";
3
+ import { RouterProvider, createMemoryRouter } from "react-router-dom";
4
+ function createReactBridgeNavigation(router, options = {}) {
5
+ const routerBase = options.basename && options.basename !== "/" ? options.basename.replace(/\/$/, "") : "";
6
+ const toLogic = () => {
7
+ const l = router.state.location;
8
+ const pathname = routerBase && l.pathname.startsWith(routerBase) ? l.pathname.slice(routerBase.length) || "/" : l.pathname;
9
+ return { pathname, search: l.search, hash: l.hash };
10
+ };
11
+ return {
12
+ getLocation: toLogic,
13
+ subscribe(listener) {
14
+ return router.subscribe(() => listener(toLogic()));
15
+ },
16
+ async navigate(target, action) {
17
+ if (options.canNavigate && !options.canNavigate(target)) {
18
+ return { status: "cancelled", location: toLogic() };
19
+ }
20
+ await router.navigate((routerBase && routerBase !== "/" ? routerBase : "") + target.pathname + target.search + target.hash, { replace: action === "replace" });
21
+ return { status: "committed", location: toLogic() };
22
+ },
23
+ go(delta) {
24
+ void router.navigate(delta);
25
+ }
26
+ };
27
+ }
28
+ function createReactBridgeRouter(routing, routes) {
29
+ const init = routing.getLocation();
30
+ const router = createMemoryRouter(routes, {
31
+ initialEntries: [init.pathname + init.search + init.hash]
32
+ });
33
+ let syncing = false;
34
+ let disposed = false;
35
+ const unsub = routing.subscribe((loc) => {
36
+ if (disposed) return;
37
+ const next = loc.pathname + loc.search + loc.hash;
38
+ const cur = router.state.location;
39
+ if (next !== cur.pathname + cur.search + cur.hash) void router.navigate(next, { replace: true });
40
+ });
41
+ const unwatch = router.subscribe(() => {
42
+ if (disposed || syncing) return;
43
+ const cur = router.state.location;
44
+ const chan = routing.getLocation();
45
+ if (cur.pathname + cur.search + cur.hash === chan.pathname + chan.search + chan.hash) return;
46
+ syncing = true;
47
+ void routing.navigate({ pathname: cur.pathname, search: cur.search, hash: cur.hash }, "push").then((result) => {
48
+ syncing = false;
49
+ if (result.status === "cancelled" && !disposed) {
50
+ const c = result.location;
51
+ const next = c.pathname + c.search + c.hash;
52
+ const cur2 = router.state.location;
53
+ if (next !== cur2.pathname + cur2.search + cur2.hash) void router.navigate(next, { replace: true });
54
+ }
55
+ });
56
+ });
57
+ return {
58
+ element: createElement(RouterProvider, { router }),
59
+ dispose() {
60
+ if (disposed) return;
61
+ disposed = true;
62
+ unwatch();
63
+ unsub();
64
+ }
65
+ };
66
+ }
67
+ export {
68
+ createReactBridgeNavigation,
69
+ createReactBridgeRouter
70
+ };
@@ -0,0 +1,75 @@
1
+ import { Router } from 'vue-router';
2
+
3
+ /** 统一逻辑位置:pathname 以 / 开头;search 空串或 ? 开头;hash 空串或 # 开头 */
4
+ interface BridgeLocation {
5
+ pathname: string;
6
+ search: string;
7
+ hash: string;
8
+ }
9
+ type BridgeNavigationResult = {
10
+ status: 'committed';
11
+ location: BridgeLocation;
12
+ } | {
13
+ status: 'cancelled';
14
+ location: BridgeLocation;
15
+ };
16
+ /** 宿主导航端口(宿主适配器把 Router 包装成本接口;逻辑路径不含部署 base) */
17
+ interface BridgeHostNavigation {
18
+ getLocation(): BridgeLocation;
19
+ subscribe(listener: (location: BridgeLocation) => void): () => void;
20
+ navigate(target: BridgeLocation, action: 'push' | 'replace'): Promise<BridgeNavigationResult>;
21
+ go(delta: number): void;
22
+ }
23
+ /** 子应用路由通道(桥接宿主按挂载代次创建,随 mount 第三参数交给子应用) */
24
+ interface BridgeChildRoute {
25
+ getLocation(): BridgeLocation;
26
+ subscribe(listener: (location: BridgeLocation) => void): () => void;
27
+ navigate(target: BridgeLocation, action: 'push' | 'replace'): Promise<BridgeNavigationResult>;
28
+ go(delta: number): void;
29
+ }
30
+
31
+ /**
32
+ * Vue Router 4 路由适配(/bridge/router/vue 按需入口;URL 同步任务书 §3.1/§3.3)。
33
+ *
34
+ * 宿主侧:createVueBridgeNavigation(router) 把 Vue Router 实例包装为宿主导航端口——
35
+ * push/replace 落定 NavigationFailure | undefined,failure 即真实取消(守卫拒绝),
36
+ * 不以「函数返回」冒充提交成功。
37
+ *
38
+ * 子应用侧:connectVueBridgeRouter(routing, router) 把子应用自己的 memory router
39
+ * 接到通道上——RouterLink/router.push 等真实入口经 afterEach 桥为通道请求(宿主仲裁,
40
+ * 取消时回滚最后确认位置);通道广播 replace 应用到本地 router。
41
+ * 初始位置已由通道就绪;`ready` 落定前 afterEach 不发起请求(原型实证:install 的
42
+ * 初始导航竞态,见任务书 §1 原型结论 1)。
43
+ *
44
+ * 本入口按需导入 vue-router(peerDependenciesMeta.optional);默认 /bridge、/runtime、
45
+ * /react 不加载本模块。
46
+ */
47
+
48
+ /** Vue Router base(createWebHistory/createWebHashHistory 的 base 参数);逻辑路径 = 实际路径去 base */
49
+ interface VueBridgeNavigationOptions {
50
+ routerBase?: string;
51
+ }
52
+ /** 宿主侧:把 Vue Router 实例包装为宿主导航端口(宿主 Router 是唯一历史写入方) */
53
+ declare function createVueBridgeNavigation(router: Router, options?: VueBridgeNavigationOptions): BridgeHostNavigation;
54
+ interface VueBridgeRouterConnection {
55
+ /** 初始位置就绪(含初始 push 与首次广播应用);ready 前后的清理都必须可用 */
56
+ ready: Promise<void>;
57
+ /** 清理本接线(通道订阅与本地监听;通道本身由宿主生命周期销毁) */
58
+ dispose(): void;
59
+ }
60
+ /**
61
+ * 子应用侧:把子应用自己的 memory router 接到通道。
62
+ *
63
+ * ```ts
64
+ * export default defineBridgeApp((props, ctx) => {
65
+ * const router = createRouter({ history: createMemoryHistory(), routes })
66
+ * const conn = connectVueBridgeRouter(ctx.routing, router)
67
+ * const app = createApp(App, props)
68
+ * app.use(router)
69
+ * return app
70
+ * }, { routing: true })
71
+ * ```
72
+ */
73
+ declare function connectVueBridgeRouter(routing: BridgeChildRoute, router: Router): VueBridgeRouterConnection;
74
+
75
+ export { type VueBridgeNavigationOptions, type VueBridgeRouterConnection, connectVueBridgeRouter, createVueBridgeNavigation };
@@ -0,0 +1,73 @@
1
+ // src/bridge-router-vue.ts
2
+ function createVueBridgeNavigation(router, options = {}) {
3
+ const routerBase = options.routerBase && options.routerBase !== "/" ? options.routerBase.replace(/\/$/, "") : "";
4
+ const toLogic = () => {
5
+ const raw = new URL(router.currentRoute.value.fullPath || "/", "http://f.invalid");
6
+ const pathname = routerBase && raw.pathname.startsWith(routerBase) ? raw.pathname.slice(routerBase.length) || "/" : raw.pathname;
7
+ return { pathname, search: raw.search, hash: raw.hash };
8
+ };
9
+ return {
10
+ getLocation: toLogic,
11
+ subscribe(listener) {
12
+ return router.afterEach(() => listener(toLogic()));
13
+ },
14
+ async navigate(target, action) {
15
+ const to = target.pathname + target.search + target.hash;
16
+ try {
17
+ const failure = action === "replace" ? await router.replace(to) : await router.push(to);
18
+ if (failure) return { status: "cancelled", location: toLogic() };
19
+ return { status: "committed", location: toLogic() };
20
+ } catch {
21
+ return { status: "cancelled", location: toLogic() };
22
+ }
23
+ },
24
+ go(delta) {
25
+ router.go(delta);
26
+ }
27
+ };
28
+ }
29
+ function connectVueBridgeRouter(routing, router) {
30
+ let syncing = false;
31
+ let disposed = false;
32
+ let started = false;
33
+ const unsub = routing.subscribe((loc) => {
34
+ if (disposed) return;
35
+ const next = loc.pathname + loc.search + loc.hash;
36
+ if (router.currentRoute.value.fullPath !== next) void router.replace(next);
37
+ });
38
+ const stopWatch = router.afterEach((to) => {
39
+ if (disposed || syncing || !started) return;
40
+ const chan = routing.getLocation();
41
+ const toFull = to.fullPath;
42
+ if (toFull === chan.pathname + chan.search + chan.hash) return;
43
+ const raw = new URL(toFull, "http://f.invalid");
44
+ syncing = true;
45
+ void routing.navigate({ pathname: raw.pathname, search: raw.search, hash: raw.hash }, "push").then((result) => {
46
+ syncing = false;
47
+ if (result.status === "cancelled" && !disposed) {
48
+ const c = result.location;
49
+ const cur = c.pathname + c.search + c.hash;
50
+ if (router.currentRoute.value.fullPath !== cur) void router.replace(cur);
51
+ }
52
+ });
53
+ });
54
+ const init = routing.getLocation();
55
+ const ready = router.push(init.pathname + init.search + init.hash).then(() => {
56
+ started = true;
57
+ }).catch(() => {
58
+ started = true;
59
+ });
60
+ return {
61
+ ready,
62
+ dispose() {
63
+ if (disposed) return;
64
+ disposed = true;
65
+ stopWatch();
66
+ unsub();
67
+ }
68
+ };
69
+ }
70
+ export {
71
+ connectVueBridgeRouter,
72
+ createVueBridgeNavigation
73
+ };
@@ -1,6 +1,6 @@
1
1
  import * as vue from 'vue';
2
- import { V as VueBridgeAppOptions } from './bridge-host-vue-BAxDKqGH.js';
3
- export { A as AppContext, B as BridgeApp } from './bridge-core-6uLJiMVa.js';
2
+ import { V as VueBridgeAppOptions } from './bridge-host-vue-BuCGlLdS.js';
3
+ export { A as AppContext, a as BridgeApp } from './bridge-core-BOWvVUxO.js';
4
4
 
5
5
  /** Vue 宿主桥接工厂:返回 `{ appProps: P; sessionKey?: string | null }` 形态的包装组件 */
6
6
  declare const createVueBridgeApp: <P = Record<string, unknown>>(spec: string, options?: VueBridgeAppOptions) => vue.Component;
package/dist/bridge.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import * as react from 'react';
2
- import { R as ReactBridgeAppOptions } from './bridge-host-react-C55RmGll.js';
3
- export { B as BridgeErrorFallback } from './bridge-host-react-C55RmGll.js';
2
+ import { B as BridgeHostRouting } from './bridge-core-BOWvVUxO.js';
3
+ export { A as AppContext, a as BridgeApp } from './bridge-core-BOWvVUxO.js';
4
+ import { R as ReactBridgeAppOptions } from './bridge-host-react-CMVd-ym0.js';
5
+ export { B as BridgeErrorFallback } from './bridge-host-react-CMVd-ym0.js';
4
6
  import * as vue from 'vue';
5
- import { V as VueBridgeAppOptions } from './bridge-host-vue-BAxDKqGH.js';
6
- export { A as AppContext, B as BridgeApp } from './bridge-core-6uLJiMVa.js';
7
+ import { V as VueBridgeAppOptions } from './bridge-host-vue-BuCGlLdS.js';
7
8
 
8
9
  /** Vue 宿主桥接工厂:返回 `{ appProps: P; sessionKey?: string | null }` 形态的包装组件 */
9
10
  declare const createVueBridgeApp: <P = Record<string, unknown>>(spec: string, options?: VueBridgeAppOptions) => vue.Component;
@@ -11,6 +12,7 @@ declare const createVueBridgeApp: <P = Record<string, unknown>>(spec: string, op
11
12
  declare const createReactBridgeApp: <P = Record<string, unknown>>(spec: string, options?: ReactBridgeAppOptions) => react.ComponentType<{
12
13
  appProps?: P;
13
14
  sessionKey?: string | null;
15
+ routing?: BridgeHostRouting;
14
16
  }>;
15
17
 
16
18
  export { ReactBridgeAppOptions, VueBridgeAppOptions, createReactBridgeApp, createVueBridgeApp };
@@ -1138,7 +1138,15 @@ var BridgeErrorCodes = {
1138
1138
  /** 桥接准备或生命周期失败(details.phase = getContext | mount | unmount) */
1139
1139
  BRIDGE_LIFECYCLE_FAILED: "MFU-016",
1140
1140
  /** 桥接会话参数与 AppContext 不一致(受控 sessionKey 与全局会话矛盾) */
1141
- BRIDGE_SESSION_MISMATCH: "MFU-017"
1141
+ BRIDGE_SESSION_MISMATCH: "MFU-017",
1142
+ /** 路由同步配置/前缀冲突(basePath 非法、同页前缀冲突或重叠) */
1143
+ ROUTING_CONFIG_INVALID: "MFU-030",
1144
+ /** 路由协议缺失/通道失效(子应用未声明 routing 协议,或已销毁通道被再次使用) */
1145
+ ROUTING_PROTOCOL_MISSING: "MFU-031",
1146
+ /** 非法导航(目标越界前缀、非法 go 参数、失效实例的请求) */
1147
+ ROUTING_NAVIGATION_INVALID: "MFU-032",
1148
+ /** 路由准备/同步失败(重定向循环、子应用路由准备失败且已阻止错误页面显示) */
1149
+ ROUTING_SYNC_FAILED: "MFU-033"
1142
1150
  };
1143
1151
  function invalidBridgeContractError(spec, mod) {
1144
1152
  const actual = mod === null ? "null" : typeof mod;
@@ -1195,6 +1203,42 @@ function invalidSessionKeyError(spec, value) {
1195
1203
  { spec, value: String(value) }
1196
1204
  );
1197
1205
  }
1206
+ function routingConfigError(spec, reason) {
1207
+ return new FgError(
1208
+ BridgeErrorCodes.ROUTING_CONFIG_INVALID,
1209
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u914D\u7F6E\u65E0\u6CD5\u542F\u7528\u3002
1210
+ \u539F\u56E0\uFF1A${reason}
1211
+ \u4FEE\u6CD5\uFF1A\u4FEE\u6B63 routing.basePath \u4E3A\u5BBF\u4E3B\u8DEF\u7531\u89C6\u89D2\u7684\u9759\u6001\u7EDD\u5BF9\u8DEF\u5F84\uFF08\u5982 "/approval"\uFF09\uFF0C\u5E76\u4FDD\u8BC1\u540C\u9875\u5404\u540C\u6B65\u5B9E\u4F8B\u524D\u7F00\u4E92\u4E0D\u91CD\u53E0\uFF1B\u5BBF\u4E3B\u8DEF\u7531\u9700\u58F0\u660E\u5BF9\u5E94\u7684\u540E\u7F00\u5339\u914D\uFF08Vue "/approval/:pathMatch(.*)*"\u3001React "/approval/*"\uFF09\u3002`,
1212
+ { spec }
1213
+ );
1214
+ }
1215
+ function routingProtocolError(spec, reason) {
1216
+ return new FgError(
1217
+ BridgeErrorCodes.ROUTING_PROTOCOL_MISSING,
1218
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u901A\u9053\u4E0D\u53EF\u7528\u3002
1219
+ \u539F\u56E0\uFF1A${reason}
1220
+ \u4FEE\u6CD5\uFF1A\u5B50\u5E94\u7528\u4EE5 defineBridgeApp(\u5DE5\u5382, { routing: true }) \u58F0\u660E\u8DEF\u7531\u534F\u8BAE\u5E76\u5728\u5DE5\u5382\u7B2C\u4E8C\u53C2\u6570\u63A5\u6536 { signal, routing } \u63A5\u7EBF\u53D7\u63A7\u8DEF\u7531\uFF1B\u901A\u9053\u968F\u6302\u8F7D\u751F\u547D\u5468\u671F\u521B\u5EFA\uFF0C\u9500\u6BC1\u540E\u4E0D\u5F97\u590D\u7528\uFF08\u91CD\u6302\u4F1A\u5F97\u5230\u65B0\u901A\u9053\uFF09\u3002`,
1221
+ { spec }
1222
+ );
1223
+ }
1224
+ function routingNavigationError(spec, reason) {
1225
+ return new FgError(
1226
+ BridgeErrorCodes.ROUTING_NAVIGATION_INVALID,
1227
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684\u4E00\u6B21\u5BFC\u822A\u8BF7\u6C42\u88AB\u62D2\u7EDD\u3002
1228
+ \u539F\u56E0\uFF1A${reason}
1229
+ \u4FEE\u6CD5\uFF1A\u5B50\u5E94\u7528\u5BFC\u822A\u76EE\u6807\u5FC5\u987B\u4F4D\u4E8E\u81EA\u8EAB basePath \u524D\u7F00\u5185\uFF08\u8DE8\u524D\u7F00\u8BF7\u7528\u5BBF\u4E3B\u83DC\u5355/\u5BBF\u4E3B\u80FD\u529B\uFF09\uFF1Bgo \u53C2\u6570\u4E3A\u6709\u9650\u6574\u6570\u4E14 |delta| \u2264 50\u3002`,
1230
+ { spec }
1231
+ );
1232
+ }
1233
+ function routingSyncError(spec, reason, chain) {
1234
+ return new FgError(
1235
+ BridgeErrorCodes.ROUTING_SYNC_FAILED,
1236
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u5728\u91CD\u5B9A\u5411\u94FE\u4E0A\u8D85\u51FA\u4E0A\u9650\uFF08${chain.length} \u6B21\uFF09\u3002
1237
+ \u539F\u56E0\uFF1A${reason}
1238
+ \u4FEE\u6CD5\uFF1A\u68C0\u67E5\u5B50\u5E94\u7528\u8DEF\u7531\u7684\u91CD\u5B9A\u5411\u89C4\u5219\u662F\u5426\u4E92\u76F8\u6210\u73AF\uFF1B\u540C\u6B65\u91CD\u5B9A\u5411\u4F7F\u7528 replace \u4E14\u4E0D\u5F97\u5BF9\u540C\u4E00\u4F4D\u7F6E\u91CD\u590D\u53D1\u8D77\u3002\u76EE\u6807\u94FE\uFF1A${chain.join(" \u2192 ")}\u3002`,
1239
+ { spec, chain: [...chain] }
1240
+ );
1241
+ }
1198
1242
 
1199
1243
  // src/bridge-core.ts
1200
1244
  function assertBridgeContract(spec, mod) {
@@ -1299,6 +1343,261 @@ function withBridgeTimeout(p, ms, spec) {
1299
1343
  });
1300
1344
  }
1301
1345
 
1346
+ // src/bridge-router-core.ts
1347
+ var MAX_REDIRECT_PER_REQUEST = 5;
1348
+ var MAX_GO_DELTA = 50;
1349
+ function normalizeBasePath(basePath, spec) {
1350
+ const bad = (reason) => routingConfigError(spec, reason);
1351
+ if (typeof basePath !== "string" || basePath === "") throw bad("basePath \u4E0D\u80FD\u4E3A\u7A7A\u3002");
1352
+ if (basePath === "/") throw bad('basePath \u4E0D\u80FD\u662F\u6839 "/"\u2014\u2014\u6839\u8DEF\u5F84\u5C5E\u4E8E\u5BBF\u4E3B\u81EA\u8EAB\u9875\u9762\uFF0C\u8BF7\u4E3A\u540C\u6B65\u5B9E\u4F8B\u5206\u914D\u72EC\u7ACB\u524D\u7F00\u3002');
1353
+ if (!basePath.startsWith("/")) throw bad(`basePath \u5FC5\u987B\u4EE5 "/" \u5F00\u5934\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002`);
1354
+ if (/[?#]/.test(basePath) || basePath.includes("://")) throw bad(`basePath \u4E0D\u80FD\u5305\u542B query/hash/\u534F\u8BAE\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002`);
1355
+ if (basePath.includes("*") || basePath.includes(":")) throw bad(`basePath \u4E0D\u63A5\u53D7\u901A\u914D\u7B26\u6216\u53C2\u6570\u6A21\u677F\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002\u8BF7\u4F7F\u7528\u9759\u6001\u7EDD\u5BF9\u8DEF\u5F84\u3002`);
1356
+ return basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
1357
+ }
1358
+ function matchesBasePath(hostPathname, basePath) {
1359
+ return hostPathname === basePath || hostPathname.startsWith(basePath + "/");
1360
+ }
1361
+ function overlapsBasePath(a, b) {
1362
+ return a === b || a.startsWith(b + "/") || b.startsWith(a + "/");
1363
+ }
1364
+ function toChildLocation(host, basePath) {
1365
+ const rest = host.pathname === basePath ? "/" : host.pathname.slice(basePath.length);
1366
+ return { pathname: rest || "/", search: host.search, hash: host.hash };
1367
+ }
1368
+ function toHostLocation(child, basePath, spec) {
1369
+ const childPath = child.pathname === "/" ? "" : child.pathname;
1370
+ if (childPath !== "" && !childPath.startsWith("/")) {
1371
+ throw routingNavigationError(spec, `\u5BFC\u822A\u76EE\u6807 pathname \u5FC5\u987B\u4EE5 "/" \u5F00\u5934\uFF1A\u5F53\u524D\u4E3A "${child.pathname}"\u3002`);
1372
+ }
1373
+ return { pathname: basePath + childPath, search: child.search, hash: child.hash };
1374
+ }
1375
+ function sameLocation(a, b) {
1376
+ return a.pathname === b.pathname && a.search === b.search && a.hash === b.hash;
1377
+ }
1378
+ function assertChildTargetInScope(target, basePath, spec) {
1379
+ if (target.pathname.includes("\0") || target.pathname.split("/").includes("..")) {
1380
+ throw routingNavigationError(spec, `\u5BFC\u822A\u76EE\u6807\u8BD5\u56FE\u9003\u9038\u81EA\u8EAB\u524D\u7F00\uFF1Apathname "${target.pathname}"\u3002\u8DE8\u524D\u7F00\u5BFC\u822A\u8BF7\u901A\u8FC7\u5BBF\u4E3B\u83DC\u5355\u7B49\u5BBF\u4E3B\u80FD\u529B\u5B8C\u6210\u3002`);
1381
+ }
1382
+ const host = toHostLocation(target, basePath, spec);
1383
+ if (!matchesBasePath(host.pathname, basePath)) {
1384
+ throw routingNavigationError(spec, `\u5BFC\u822A\u76EE\u6807 ${host.pathname} \u4E0D\u5728\u524D\u7F00 ${basePath} \u4E0B\u3002`);
1385
+ }
1386
+ }
1387
+ var activeRoutingPrefixes = /* @__PURE__ */ new Map();
1388
+ function acquireRoutingPrefix(basePath, spec) {
1389
+ for (const [active] of activeRoutingPrefixes) {
1390
+ if ((activeRoutingPrefixes.get(active) ?? 0) > 0 && overlapsBasePath(active, basePath)) {
1391
+ throw routingConfigError(spec, `\u540C\u9875\u5DF2\u6709\u5360\u7528\u524D\u7F00 "${active}" \u7684\u540C\u6B65\u5B9E\u4F8B\uFF0C\u65B0\u5B9E\u4F8B "${spec}" \u7684 basePath "${basePath}" \u4E0E\u5176\u91CD\u53E0\u3002`);
1392
+ }
1393
+ }
1394
+ activeRoutingPrefixes.set(basePath, (activeRoutingPrefixes.get(basePath) ?? 0) + 1);
1395
+ let released = false;
1396
+ return () => {
1397
+ if (released) return;
1398
+ released = true;
1399
+ activeRoutingPrefixes.set(basePath, Math.max(0, (activeRoutingPrefixes.get(basePath) ?? 1) - 1));
1400
+ };
1401
+ }
1402
+ function assertBridgeRoutingProtocol(spec, contract) {
1403
+ const r = contract?.routing;
1404
+ if (!r || r.protocol !== 1) {
1405
+ throw routingProtocolError(
1406
+ spec,
1407
+ `\u8FDC\u7A0B\u5951\u7EA6\u672A\u58F0\u660E\u8DEF\u7531\u534F\u8BAE\uFF08\u671F\u671B routing: { protocol: 1 }\uFF0C\u5B9E\u9645 ${r === void 0 ? "\u7F3A\u5931" : `protocol=${String(r?.protocol)}`}\uFF09\u3002\u5F00\u542F URL \u540C\u6B65\u4E0D\u80FD\u9759\u9ED8\u9000\u56DE memory \u6A21\u5F0F\u3002`
1408
+ );
1409
+ }
1410
+ }
1411
+ var RoutingChannel = class {
1412
+ id;
1413
+ basePath;
1414
+ spec;
1415
+ host;
1416
+ listeners = /* @__PURE__ */ new Set();
1417
+ current;
1418
+ lastConfirmed;
1419
+ seq = 0;
1420
+ inflight = null;
1421
+ disposed = false;
1422
+ active = true;
1423
+ releasePrefix;
1424
+ unsubHost;
1425
+ /** 连续内部 replace 计数(有界重定向检测) */
1426
+ redirectChain = [];
1427
+ redirectTimer;
1428
+ /** 诊断观测钩子(测试断言/诊断用) */
1429
+ onEvent;
1430
+ constructor(id, basePath, host, spec) {
1431
+ this.id = id;
1432
+ this.spec = spec;
1433
+ this.basePath = normalizeBasePath(basePath, spec);
1434
+ this.host = host;
1435
+ this.current = toChildLocation(host.getLocation(), this.basePath);
1436
+ this.lastConfirmed = { ...this.current };
1437
+ this.releasePrefix = acquireRoutingPrefix(this.basePath, spec);
1438
+ this.unsubHost = host.subscribe((loc) => this.handleHostLocation(loc));
1439
+ }
1440
+ emit(loc) {
1441
+ this.current = { ...loc };
1442
+ for (const l of [...this.listeners]) {
1443
+ try {
1444
+ l(this.current);
1445
+ } catch (e) {
1446
+ console.error("[fulgurjs] \u5B50\u5E94\u7528\u8DEF\u7531\u8BA2\u9605\u8005\u5F02\u5E38\uFF08\u5DF2\u9694\u79BB\uFF0C\u4E0D\u5F71\u54CD\u5BBF\u4E3B\uFF09\uFF1A", e);
1447
+ }
1448
+ }
1449
+ }
1450
+ handleHostLocation(hostLoc) {
1451
+ if (this.disposed) return;
1452
+ if (!matchesBasePath(hostLoc.pathname, this.basePath)) {
1453
+ this.onEvent?.({ type: "prefix-miss", pathname: hostLoc.pathname });
1454
+ return;
1455
+ }
1456
+ if (!this.active) {
1457
+ this.onEvent?.({ type: "paused-host-loc" });
1458
+ return;
1459
+ }
1460
+ const child = toChildLocation(hostLoc, this.basePath);
1461
+ if (this.inflight && sameLocation(child, this.inflight.target)) {
1462
+ const { seq, resolve } = this.inflight;
1463
+ this.inflight = null;
1464
+ this.lastConfirmed = child;
1465
+ this.emit(child);
1466
+ this.onEvent?.({ type: "request-confirmed", seq });
1467
+ resolve({ status: "committed", location: { ...child } });
1468
+ return;
1469
+ }
1470
+ if (this.inflight) {
1471
+ const { seq, resolve } = this.inflight;
1472
+ this.inflight = null;
1473
+ this.lastConfirmed = child;
1474
+ this.emit(child);
1475
+ this.onEvent?.({ type: "request-superseded", seq });
1476
+ resolve({ status: "cancelled", location: { ...child } });
1477
+ return;
1478
+ }
1479
+ this.lastConfirmed = child;
1480
+ this.emit(child);
1481
+ this.onEvent?.({ type: "host-broadcast" });
1482
+ }
1483
+ getLocation() {
1484
+ return { ...this.current };
1485
+ }
1486
+ subscribe(listener) {
1487
+ if (this.disposed) {
1488
+ throw routingProtocolError(this.spec, `\u901A\u9053 ${this.id} \u5DF2\u9500\u6BC1\uFF0C\u4E0D\u80FD\u518D\u8BA2\u9605\uFF08\u65E7\u5B9E\u4F8B\u7684\u8FDF\u5230\u8BA2\u9605\u5FC5\u987B\u5931\u6548\uFF1B\u91CD\u6302\u4F1A\u5F97\u5230\u65B0\u901A\u9053\uFF09\u3002`);
1489
+ }
1490
+ this.listeners.add(listener);
1491
+ return () => {
1492
+ this.listeners.delete(listener);
1493
+ };
1494
+ }
1495
+ async navigate(target, action) {
1496
+ if (this.disposed || !this.active) {
1497
+ this.onEvent?.({ type: this.disposed ? "rejected-disposed" : "rejected-inactive" });
1498
+ return { status: "cancelled", location: { ...this.lastConfirmed } };
1499
+ }
1500
+ assertChildTargetInScope(target, this.basePath, this.spec);
1501
+ if (sameLocation(target, this.lastConfirmed)) {
1502
+ this.onEvent?.({ type: "noop" });
1503
+ return { status: "committed", location: { ...this.lastConfirmed } };
1504
+ }
1505
+ if (action === "replace") {
1506
+ this.redirectChain.push(target.pathname + target.search + target.hash);
1507
+ } else {
1508
+ this.redirectChain = [];
1509
+ }
1510
+ if (this.redirectTimer) clearTimeout(this.redirectTimer);
1511
+ this.redirectTimer = setTimeout(() => {
1512
+ this.redirectChain = [];
1513
+ }, 1e3);
1514
+ if (this.redirectChain.length > MAX_REDIRECT_PER_REQUEST) {
1515
+ const chain = [...this.redirectChain];
1516
+ this.redirectChain = [];
1517
+ throw routingSyncError(this.spec, "\u540C\u4E00\u901A\u9053\u7684\u8FDE\u7EED replace \u91CD\u5B9A\u5411\u8D85\u8FC7\u4E0A\u9650\uFF08\u7591\u4F3C\u5B50\u5E94\u7528\u8DEF\u7531\u4E92\u76F8\u6210\u73AF\uFF09\u3002", chain);
1518
+ }
1519
+ if (this.inflight) {
1520
+ const prev = this.inflight;
1521
+ await new Promise((resolve) => {
1522
+ const timer = setInterval(() => {
1523
+ if (this.inflight !== prev) {
1524
+ clearInterval(timer);
1525
+ resolve();
1526
+ }
1527
+ }, 0);
1528
+ });
1529
+ if (this.disposed || !this.active) {
1530
+ return { status: "cancelled", location: { ...this.lastConfirmed } };
1531
+ }
1532
+ if (sameLocation(target, this.lastConfirmed)) {
1533
+ return { status: "committed", location: { ...this.lastConfirmed } };
1534
+ }
1535
+ }
1536
+ const seq = ++this.seq;
1537
+ this.onEvent?.({ type: "request-start", seq, target: { ...target }, action });
1538
+ return new Promise((resolve) => {
1539
+ this.inflight = { seq, target: { ...target }, resolve };
1540
+ this.host.navigate(toHostLocation(target, this.basePath, this.spec), action).then((result) => {
1541
+ if (!this.inflight || this.inflight.seq !== seq) return;
1542
+ if (this.disposed || !this.active) {
1543
+ this.inflight = null;
1544
+ resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
1545
+ return;
1546
+ }
1547
+ this.inflight = null;
1548
+ if (result.status === "committed") {
1549
+ const child = toChildLocation(result.location, this.basePath);
1550
+ this.lastConfirmed = child;
1551
+ this.emit(child);
1552
+ } else {
1553
+ this.emit({ ...this.lastConfirmed });
1554
+ }
1555
+ this.onEvent?.({ type: "host-verdict", seq, status: result.status });
1556
+ resolve(
1557
+ result.status === "committed" ? { status: "committed", location: { ...this.lastConfirmed } } : { status: "cancelled", location: { ...this.lastConfirmed } }
1558
+ );
1559
+ }).catch(() => {
1560
+ if (this.inflight?.seq === seq) {
1561
+ this.inflight = null;
1562
+ this.onEvent?.({ type: "host-error", seq });
1563
+ this.emit({ ...this.lastConfirmed });
1564
+ resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
1565
+ }
1566
+ });
1567
+ });
1568
+ }
1569
+ /** 委托宿主历史(有限整数);不创建第二条独立历史 */
1570
+ go(delta) {
1571
+ if (this.disposed || !this.active) return;
1572
+ if (typeof delta !== "number" || !Number.isInteger(delta) || Math.abs(delta) > MAX_GO_DELTA) {
1573
+ throw routingNavigationError(this.spec, `go \u53C2\u6570\u5FC5\u987B\u662F\u6709\u9650\u6574\u6570\uFF08|delta| \u2264 ${MAX_GO_DELTA}\uFF09\uFF1A\u5F53\u524D\u4E3A ${String(delta)}\u3002`);
1574
+ }
1575
+ this.host.go(delta);
1576
+ }
1577
+ /** KeepAlive deactivate:暂停广播与写入;通道与订阅保留(activate 后 resync) */
1578
+ setActive(active) {
1579
+ if (this.disposed) return;
1580
+ this.active = active;
1581
+ this.onEvent?.({ type: active ? "resumed" : "paused" });
1582
+ if (active) this.handleHostLocation(this.host.getLocation());
1583
+ }
1584
+ /** 挂载生命周期结束:清理订阅/登记/在飞请求;此后一切写入失效 */
1585
+ dispose() {
1586
+ if (this.disposed) return;
1587
+ this.disposed = true;
1588
+ if (this.redirectTimer) clearTimeout(this.redirectTimer);
1589
+ this.unsubHost();
1590
+ this.releasePrefix();
1591
+ if (this.inflight) {
1592
+ const { resolve } = this.inflight;
1593
+ this.inflight = null;
1594
+ resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
1595
+ }
1596
+ this.listeners.clear();
1597
+ this.onEvent?.({ type: "disposed" });
1598
+ }
1599
+ };
1600
+
1302
1601
  export {
1303
1602
  loadRemote,
1304
1603
  provideAppContext,
@@ -1309,5 +1608,7 @@ export {
1309
1608
  resolveBridgeContext,
1310
1609
  assertControlledSessionKey,
1311
1610
  assertBridgeOptions,
1312
- withBridgeTimeout
1611
+ withBridgeTimeout,
1612
+ assertBridgeRoutingProtocol,
1613
+ RoutingChannel
1313
1614
  };