@fulgurjs/federation 5.4.0 → 5.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.en.md +6 -4
  3. package/README.md +15 -10
  4. package/dist/bridge-app-react.d.cts +1 -1
  5. package/dist/bridge-app-react.d.ts +1 -1
  6. package/dist/bridge-app-react.js +1 -1
  7. package/dist/bridge-app-vue.d.cts +1 -1
  8. package/dist/bridge-app-vue.d.ts +1 -1
  9. package/dist/bridge-app-vue.js +1 -1
  10. package/dist/{bridge-core-BOWvVUxO.d.ts → bridge-core-ejlY9ddN.d.ts} +3 -1
  11. package/dist/bridge-core.d.cts +1 -1
  12. package/dist/bridge-core.d.ts +1 -1
  13. package/dist/bridge-core.js +2 -2
  14. package/dist/bridge-errors.cjs +5 -4
  15. package/dist/bridge-errors.d.cts +1 -1
  16. package/dist/bridge-errors.d.ts +1 -1
  17. package/dist/bridge-errors.js +1 -1
  18. package/dist/{bridge-host-react-CMVd-ym0.d.ts → bridge-host-react-Bua1TCVh.d.ts} +1 -1
  19. package/dist/bridge-host-react.cjs +54 -45
  20. package/dist/bridge-host-react.d.cts +1 -1
  21. package/dist/bridge-host-react.d.ts +1 -1
  22. package/dist/bridge-host-react.js +3 -3
  23. package/dist/{bridge-host-vue-BuCGlLdS.d.ts → bridge-host-vue-Dd1kkwDb.d.ts} +1 -1
  24. package/dist/bridge-host-vue.cjs +54 -45
  25. package/dist/bridge-host-vue.js +3 -3
  26. package/dist/bridge-react.d.ts +4 -4
  27. package/dist/{bridge-router-core-BxBx-reP.d.ts → bridge-router-core-BRZQYDPm.d.cts} +3 -1
  28. package/dist/{bridge-router-core-BxBx-reP.d.cts → bridge-router-core-BRZQYDPm.d.ts} +3 -1
  29. package/dist/bridge-router-react.d.ts +25 -42
  30. package/dist/bridge-router-react.js +213 -36
  31. package/dist/bridge-router-vue.d.ts +18 -37
  32. package/dist/bridge-router-vue.js +188 -44
  33. package/dist/bridge-vue.d.ts +2 -2
  34. package/dist/bridge.d.ts +5 -5
  35. package/dist/{chunk-GF6KOH67.js → chunk-43UX3UZE.js} +1 -1
  36. package/dist/{chunk-WOTR2FUQ.js → chunk-7CQOVLSI.js} +50 -42
  37. package/dist/{chunk-JQF7KYZF.js → chunk-HZGQOW35.js} +5 -4
  38. package/dist/{chunk-OXEAGRPY.js → chunk-KT3YARK5.js} +1 -1
  39. package/dist/{chunk-PRUYHYX2.js → chunk-RXXBTPAY.js} +1 -1
  40. package/dist/{chunk-BTJGYDHT.js → chunk-UFWZPE36.js} +55 -55
  41. package/dist/index.cjs +42 -10
  42. package/dist/index.js +42 -10
  43. package/dist/runtime.js +9 -9
  44. package/examples/bridge/react-host/package-lock.json +13 -5
  45. package/examples/bridge/react-host/package.json +1 -1
  46. package/examples/bridge/react-remote/package-lock.json +13 -5
  47. package/examples/bridge/react-remote/package.json +1 -1
  48. package/examples/bridge/vue-host/package-lock.json +13 -5
  49. package/examples/bridge/vue-host/package.json +1 -1
  50. package/examples/bridge/vue-remote/package-lock.json +13 -5
  51. package/examples/bridge/vue-remote/package.json +1 -1
  52. package/examples/react/host/package-lock.json +13 -4
  53. package/examples/react/host/package.json +1 -1
  54. package/examples/react/remote/package-lock.json +13 -4
  55. package/examples/react/remote/package.json +1 -1
  56. package/examples/vue/host/package-lock.json +13 -4
  57. package/examples/vue/host/package.json +1 -1
  58. package/examples/vue/remote/package-lock.json +13 -4
  59. package/examples/vue/remote/package.json +1 -1
  60. package/package.json +1 -1
@@ -1,68 +1,245 @@
1
1
  // src/bridge-router-react.tsx
2
2
  import { createElement } from "react";
3
3
  import { RouterProvider, createMemoryRouter } from "react-router-dom";
4
+
5
+ // src/runtime/errors.ts
6
+ var FgError = class extends Error {
7
+ code;
8
+ details;
9
+ constructor(code, message, details, options) {
10
+ super(`[fulgurjs:${code}] ${message}`, options);
11
+ this.name = "FgError";
12
+ this.code = code;
13
+ this.details = details;
14
+ }
15
+ };
16
+
17
+ // src/bridge-errors.ts
18
+ var BridgeErrorCodes = {
19
+ /** 桥接契约非法(./bridge 默认导出缺 mount/unmount 或不是函数) */
20
+ BRIDGE_INVALID_CONTRACT: "MFU-015",
21
+ /** 桥接准备或生命周期失败(details.phase = getContext | mount | unmount) */
22
+ BRIDGE_LIFECYCLE_FAILED: "MFU-016",
23
+ /** 桥接会话参数与 AppContext 不一致(受控 sessionKey 与全局会话矛盾) */
24
+ BRIDGE_SESSION_MISMATCH: "MFU-017",
25
+ /** 路由同步配置/前缀冲突(basePath 非法、同页前缀冲突或重叠) */
26
+ ROUTING_CONFIG_INVALID: "MFU-030",
27
+ /** 路由协议缺失/通道失效(子应用未声明 routing 协议,或已销毁通道被再次使用) */
28
+ ROUTING_PROTOCOL_MISSING: "MFU-031",
29
+ /** 非法导航(目标越界前缀、非法 go 参数、失效实例的请求) */
30
+ ROUTING_NAVIGATION_INVALID: "MFU-032",
31
+ /** 路由准备/同步失败(重定向循环、子应用路由准备失败且已阻止错误页面显示) */
32
+ ROUTING_SYNC_FAILED: "MFU-033"
33
+ };
34
+ function routingSyncError(spec, reason, chain, cause) {
35
+ return new FgError(
36
+ BridgeErrorCodes.ROUTING_SYNC_FAILED,
37
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u5931\u8D25\u3002
38
+ \u539F\u56E0\uFF1A${reason}
39
+ \u4FEE\u6CD5\uFF1A\u68C0\u67E5\u8DEF\u7531\u5B88\u536B\u3001\u52A0\u8F7D\u5668\u4E0E\u5BFC\u822A\u5F02\u5E38\uFF1B\u91CD\u5B9A\u5411\u94FE\u4E0D\u5E94\u6210\u73AF\u3002\u76EE\u6807\u94FE\uFF1A${chain.join(" \u2192 ")}\u3002`,
40
+ { spec, chain: [...chain] },
41
+ { cause }
42
+ );
43
+ }
44
+
45
+ // src/bridge-router-core.ts
46
+ function sameLocation(a, b) {
47
+ return a.pathname === b.pathname && a.search === b.search && a.hash === b.hash;
48
+ }
49
+
50
+ // src/bridge-router-sync.ts
51
+ function connectChildNavigation(routing, apply, onError) {
52
+ let disposed = false;
53
+ let generation = 0;
54
+ let tail = Promise.resolve();
55
+ let confirmed = routing.getLocation();
56
+ let pending;
57
+ const restore = () => apply(routing.getLocation());
58
+ const unsub = routing.subscribe((location) => {
59
+ if (disposed) return;
60
+ const changed = !sameLocation(location, confirmed);
61
+ confirmed = location;
62
+ if (pending && sameLocation(location, pending)) return;
63
+ if (changed) generation++;
64
+ tail = tail.then(() => disposed ? void 0 : restore()).catch(onError);
65
+ });
66
+ return {
67
+ enqueue(prepare, action) {
68
+ const version = generation;
69
+ const cancelled = () => ({ status: "cancelled", location: routing.getLocation() });
70
+ const task = tail.then(async () => {
71
+ if (disposed || version !== generation) return cancelled();
72
+ try {
73
+ const target = await prepare();
74
+ if (disposed) return cancelled();
75
+ if (version !== generation) {
76
+ await restore();
77
+ return cancelled();
78
+ }
79
+ if (!target) return cancelled();
80
+ pending = target;
81
+ const result = await routing.navigate(target, action);
82
+ if (!disposed) await restore();
83
+ return result;
84
+ } catch (cause) {
85
+ if (!disposed) {
86
+ try {
87
+ await restore();
88
+ } catch (restoreError) {
89
+ onError(restoreError);
90
+ }
91
+ }
92
+ throw routingSyncError("child-router", `\u5B50\u5E94\u7528\u8DEF\u7531\u51C6\u5907\u6216\u540C\u6B65\u5931\u8D25\uFF1A${cause instanceof Error ? cause.message : String(cause)}`, [], cause);
93
+ } finally {
94
+ pending = void 0;
95
+ }
96
+ });
97
+ tail = task.then(() => {
98
+ }, () => {
99
+ });
100
+ return task;
101
+ },
102
+ dispose() {
103
+ disposed = true;
104
+ generation++;
105
+ unsub();
106
+ }
107
+ };
108
+ }
109
+
110
+ // src/bridge-router-react.tsx
4
111
  function createReactBridgeNavigation(router, options = {}) {
5
- const routerBase = options.basename && options.basename !== "/" ? options.basename.replace(/\/$/, "") : "";
112
+ const base = options.basename && options.basename !== "/" ? options.basename.replace(/\/$/, "") : "";
6
113
  const toLogic = () => {
7
114
  const l = router.state.location;
8
- const pathname = routerBase && l.pathname.startsWith(routerBase) ? l.pathname.slice(routerBase.length) || "/" : l.pathname;
115
+ const pathname = base && (l.pathname === base || l.pathname.startsWith(base + "/")) ? l.pathname.slice(base.length) || "/" : l.pathname;
9
116
  return { pathname, search: l.search, hash: l.hash };
10
117
  };
11
118
  return {
12
119
  getLocation: toLogic,
13
120
  subscribe(listener) {
14
- return router.subscribe(() => listener(toLogic()));
121
+ let previous = toLogic();
122
+ return router.subscribe(() => {
123
+ const next = toLogic();
124
+ if (!sameLocation(previous, next)) {
125
+ previous = next;
126
+ listener(next);
127
+ }
128
+ });
15
129
  },
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() };
130
+ async navigate(target, action, context) {
131
+ if (context?.signal?.aborted || options.canNavigate && !options.canNavigate(target)) return { status: "cancelled", location: toLogic() };
132
+ const initial = router.state.location;
133
+ return new Promise((resolve, reject) => {
134
+ let finished = false;
135
+ let returned = false;
136
+ let blocked = false;
137
+ let unwatch = () => {
138
+ };
139
+ const finish = (status) => {
140
+ if (finished) return;
141
+ finished = true;
142
+ unwatch();
143
+ context?.signal?.removeEventListener("abort", abort);
144
+ resolve({ status, location: toLogic() });
145
+ };
146
+ const abort = () => {
147
+ finish("cancelled");
148
+ for (const blocker of router.state.blockers?.values() ?? []) if (blocker.state === "blocked") blocker.reset?.();
149
+ const loading = router.state.navigation;
150
+ if (loading && loading.state !== "idle" && loading.location?.pathname === base + target.pathname) {
151
+ const current = toLogic();
152
+ void Promise.resolve().then(() => {
153
+ const latest = router.state.navigation;
154
+ if (!latest || latest.state === "idle" || latest.location?.pathname !== base + target.pathname) return;
155
+ return router.navigate(current.pathname + current.search + current.hash, { replace: true });
156
+ }).catch((error) => console.error("[fulgurjs] \u53D6\u6D88\u5BBF\u4E3B\u5F85\u63D0\u4EA4\u5BFC\u822A\u5931\u8D25\uFF1A", error));
157
+ }
158
+ };
159
+ const check = () => {
160
+ if (finished) return;
161
+ if (context?.signal?.aborted) {
162
+ abort();
163
+ return;
164
+ }
165
+ const blockers = [...router.state.blockers?.values() ?? []];
166
+ if (blockers.some((b) => b.state === "blocked" || b.state === "proceeding")) {
167
+ blocked = true;
168
+ return;
169
+ }
170
+ if (router.state.navigation && router.state.navigation.state !== "idle") return;
171
+ const actual = router.state.location;
172
+ const committed = actual.key !== initial.key || !sameLocation(actual, initial);
173
+ if (committed) finish("committed");
174
+ else if (returned || blocked) finish("cancelled");
175
+ };
176
+ unwatch = router.subscribe(check);
177
+ context?.signal?.addEventListener("abort", abort, { once: true });
178
+ Promise.resolve().then(() => {
179
+ if (finished || context?.signal?.aborted) return;
180
+ return router.navigate(target.pathname + target.search + target.hash, { replace: action === "replace" });
181
+ }).then(() => {
182
+ returned = true;
183
+ check();
184
+ }, (error) => {
185
+ if (finished) return;
186
+ finished = true;
187
+ unwatch();
188
+ context?.signal?.removeEventListener("abort", abort);
189
+ reject(error);
190
+ });
191
+ });
22
192
  },
23
193
  go(delta) {
24
- void router.navigate(delta);
194
+ void Promise.resolve().then(() => router.navigate(delta)).catch((error) => console.error("[fulgurjs] \u5BBF\u4E3B\u5386\u53F2\u5BFC\u822A\u5931\u8D25\uFF1A", error));
25
195
  }
26
196
  };
27
197
  }
28
- function createReactBridgeRouter(routing, routes) {
198
+ function createReactBridgeRouter(routing, routes, options = {}) {
29
199
  const init = routing.getLocation();
30
- const router = createMemoryRouter(routes, {
31
- initialEntries: [init.pathname + init.search + init.hash]
32
- });
33
- let syncing = false;
200
+ const router = createMemoryRouter(routes, { initialEntries: [init.pathname + init.search + init.hash] });
201
+ const navigate = router.navigate.bind(router);
34
202
  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;
203
+ const sync = connectChildNavigation(routing, async (loc) => {
43
204
  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
- }
205
+ if (!sameLocation(cur, loc)) await navigate(loc.pathname + loc.search + loc.hash, { replace: true });
206
+ }, (error) => console.error("[fulgurjs] \u5B50\u5E94\u7528 React \u8DEF\u7531\u540C\u6B65\u5931\u8D25\uFF1A", error));
207
+ router.navigate = ((to, options2) => {
208
+ if (typeof to === "number") {
209
+ if (!disposed) routing.go(to);
210
+ return Promise.resolve();
211
+ }
212
+ const task = sync.enqueue(async () => {
213
+ await navigate(to, options2);
214
+ const l = router.state.location;
215
+ return { pathname: l.pathname, search: l.search, hash: l.hash };
216
+ }, options2?.replace ? "replace" : "push").then(() => {
55
217
  });
218
+ void task.catch((error) => console.error("[fulgurjs] \u5B50\u5E94\u7528 React \u8DEF\u7531\u540C\u6B65\u5931\u8D25\uFF1A", error));
219
+ return task;
56
220
  });
57
- return {
221
+ let unwatch = () => {
222
+ };
223
+ if (!router.state.initialized) unwatch = router.subscribe(() => {
224
+ if (!router.state.initialized) return;
225
+ unwatch();
226
+ const actual = router.state.location;
227
+ if (!sameLocation(actual, init)) void sync.enqueue(async () => actual, "replace").catch((error) => console.error("[fulgurjs] \u521D\u59CB React \u8DEF\u7531\u540C\u6B65\u5931\u8D25\uFF1A", error));
228
+ });
229
+ const connection = {
58
230
  element: createElement(RouterProvider, { router }),
59
231
  dispose() {
60
232
  if (disposed) return;
233
+ options.signal?.removeEventListener("abort", connection.dispose);
61
234
  disposed = true;
62
235
  unwatch();
63
- unsub();
236
+ sync.dispose();
237
+ router.dispose();
64
238
  }
65
239
  };
240
+ options.signal?.addEventListener("abort", connection.dispose, { once: true });
241
+ if (options.signal?.aborted) connection.dispose();
242
+ return connection;
66
243
  }
67
244
  export {
68
245
  createReactBridgeNavigation,
@@ -17,7 +17,9 @@ type BridgeNavigationResult = {
17
17
  interface BridgeHostNavigation {
18
18
  getLocation(): BridgeLocation;
19
19
  subscribe(listener: (location: BridgeLocation) => void): () => void;
20
- navigate(target: BridgeLocation, action: 'push' | 'replace'): Promise<BridgeNavigationResult>;
20
+ navigate(target: BridgeLocation, action: 'push' | 'replace', options?: {
21
+ signal?: AbortSignal;
22
+ }): Promise<BridgeNavigationResult>;
21
23
  go(delta: number): void;
22
24
  }
23
25
  /** 子应用路由通道(桥接宿主按挂载代次创建,随 mount 第三参数交给子应用) */
@@ -27,49 +29,28 @@ interface BridgeChildRoute {
27
29
  navigate(target: BridgeLocation, action: 'push' | 'replace'): Promise<BridgeNavigationResult>;
28
30
  go(delta: number): void;
29
31
  }
32
+ /** 宿主组件 routing prop(独立控制通道,不进 appProps/Context) */
33
+ interface BridgeHostRouting {
34
+ /** 宿主路由视角的静态绝对路径,如 "/approval";同页各同步实例不得相同或重叠 */
35
+ basePath: string;
36
+ navigation: BridgeHostNavigation;
37
+ }
30
38
 
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
- */
39
+ /** Vue Router 4 的宿主导航端口与子应用 memory router 接线(按需入口)。 */
47
40
 
48
- /** Vue Router base(createWebHistory/createWebHashHistory 的 base 参数);逻辑路径 = 实际路径去 base */
49
41
  interface VueBridgeNavigationOptions {
42
+ /** 保留配置兼容;Vue Router fullPath 已剥离 history base,不再二次剥离。 */
50
43
  routerBase?: string;
51
44
  }
52
- /** 宿主侧:把 Vue Router 实例包装为宿主导航端口(宿主 Router 是唯一历史写入方) */
53
- declare function createVueBridgeNavigation(router: Router, options?: VueBridgeNavigationOptions): BridgeHostNavigation;
45
+ declare function createVueBridgeNavigation(router: Router, _options?: VueBridgeNavigationOptions): BridgeHostNavigation;
54
46
  interface VueBridgeRouterConnection {
55
- /** 初始位置就绪(含初始 push 与首次广播应用);ready 前后的清理都必须可用 */
47
+ /** 必须 await ready 后再 app.use(router),初始错误会拒绝此 Promise。 */
56
48
  ready: Promise<void>;
57
- /** 清理本接线(通道订阅与本地监听;通道本身由宿主生命周期销毁) */
58
49
  dispose(): void;
59
50
  }
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;
51
+ /** 接线限子应用自己的 memory router;dispose 恢复原始导航方法。 */
52
+ declare function connectVueBridgeRouter(routing: BridgeChildRoute, router: Router, options?: {
53
+ signal?: AbortSignal;
54
+ }): VueBridgeRouterConnection;
74
55
 
75
- export { type VueBridgeNavigationOptions, type VueBridgeRouterConnection, connectVueBridgeRouter, createVueBridgeNavigation };
56
+ export { type BridgeHostRouting, type VueBridgeNavigationOptions, type VueBridgeRouterConnection, connectVueBridgeRouter, createVueBridgeNavigation };
@@ -1,24 +1,131 @@
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 };
1
+ // src/runtime/errors.ts
2
+ var FgError = class extends Error {
3
+ code;
4
+ details;
5
+ constructor(code, message, details, options) {
6
+ super(`[fulgurjs:${code}] ${message}`, options);
7
+ this.name = "FgError";
8
+ this.code = code;
9
+ this.details = details;
10
+ }
11
+ };
12
+
13
+ // src/bridge-errors.ts
14
+ var BridgeErrorCodes = {
15
+ /** 桥接契约非法(./bridge 默认导出缺 mount/unmount 或不是函数) */
16
+ BRIDGE_INVALID_CONTRACT: "MFU-015",
17
+ /** 桥接准备或生命周期失败(details.phase = getContext | mount | unmount) */
18
+ BRIDGE_LIFECYCLE_FAILED: "MFU-016",
19
+ /** 桥接会话参数与 AppContext 不一致(受控 sessionKey 与全局会话矛盾) */
20
+ BRIDGE_SESSION_MISMATCH: "MFU-017",
21
+ /** 路由同步配置/前缀冲突(basePath 非法、同页前缀冲突或重叠) */
22
+ ROUTING_CONFIG_INVALID: "MFU-030",
23
+ /** 路由协议缺失/通道失效(子应用未声明 routing 协议,或已销毁通道被再次使用) */
24
+ ROUTING_PROTOCOL_MISSING: "MFU-031",
25
+ /** 非法导航(目标越界前缀、非法 go 参数、失效实例的请求) */
26
+ ROUTING_NAVIGATION_INVALID: "MFU-032",
27
+ /** 路由准备/同步失败(重定向循环、子应用路由准备失败且已阻止错误页面显示) */
28
+ ROUTING_SYNC_FAILED: "MFU-033"
29
+ };
30
+ function routingSyncError(spec, reason, chain, cause) {
31
+ return new FgError(
32
+ BridgeErrorCodes.ROUTING_SYNC_FAILED,
33
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u5931\u8D25\u3002
34
+ \u539F\u56E0\uFF1A${reason}
35
+ \u4FEE\u6CD5\uFF1A\u68C0\u67E5\u8DEF\u7531\u5B88\u536B\u3001\u52A0\u8F7D\u5668\u4E0E\u5BFC\u822A\u5F02\u5E38\uFF1B\u91CD\u5B9A\u5411\u94FE\u4E0D\u5E94\u6210\u73AF\u3002\u76EE\u6807\u94FE\uFF1A${chain.join(" \u2192 ")}\u3002`,
36
+ { spec, chain: [...chain] },
37
+ { cause }
38
+ );
39
+ }
40
+
41
+ // src/bridge-router-core.ts
42
+ function sameLocation(a, b) {
43
+ return a.pathname === b.pathname && a.search === b.search && a.hash === b.hash;
44
+ }
45
+
46
+ // src/bridge-router-sync.ts
47
+ function connectChildNavigation(routing, apply, onError) {
48
+ let disposed = false;
49
+ let generation = 0;
50
+ let tail = Promise.resolve();
51
+ let confirmed = routing.getLocation();
52
+ let pending;
53
+ const restore = () => apply(routing.getLocation());
54
+ const unsub = routing.subscribe((location) => {
55
+ if (disposed) return;
56
+ const changed = !sameLocation(location, confirmed);
57
+ confirmed = location;
58
+ if (pending && sameLocation(location, pending)) return;
59
+ if (changed) generation++;
60
+ tail = tail.then(() => disposed ? void 0 : restore()).catch(onError);
61
+ });
62
+ return {
63
+ enqueue(prepare, action) {
64
+ const version = generation;
65
+ const cancelled = () => ({ status: "cancelled", location: routing.getLocation() });
66
+ const task = tail.then(async () => {
67
+ if (disposed || version !== generation) return cancelled();
68
+ try {
69
+ const target = await prepare();
70
+ if (disposed) return cancelled();
71
+ if (version !== generation) {
72
+ await restore();
73
+ return cancelled();
74
+ }
75
+ if (!target) return cancelled();
76
+ pending = target;
77
+ const result = await routing.navigate(target, action);
78
+ if (!disposed) await restore();
79
+ return result;
80
+ } catch (cause) {
81
+ if (!disposed) {
82
+ try {
83
+ await restore();
84
+ } catch (restoreError) {
85
+ onError(restoreError);
86
+ }
87
+ }
88
+ throw routingSyncError("child-router", `\u5B50\u5E94\u7528\u8DEF\u7531\u51C6\u5907\u6216\u540C\u6B65\u5931\u8D25\uFF1A${cause instanceof Error ? cause.message : String(cause)}`, [], cause);
89
+ } finally {
90
+ pending = void 0;
91
+ }
92
+ });
93
+ tail = task.then(() => {
94
+ }, () => {
95
+ });
96
+ return task;
97
+ },
98
+ dispose() {
99
+ disposed = true;
100
+ generation++;
101
+ unsub();
102
+ }
8
103
  };
104
+ }
105
+
106
+ // src/bridge-router-vue.ts
107
+ var locationFromPath = (path) => {
108
+ const raw = new URL(path || "/", "http://f.invalid");
109
+ return { pathname: raw.pathname, search: raw.search, hash: raw.hash };
110
+ };
111
+ function createVueBridgeNavigation(router, _options = {}) {
112
+ const toLogic = () => locationFromPath(router.currentRoute.value.fullPath);
9
113
  return {
10
114
  getLocation: toLogic,
11
115
  subscribe(listener) {
12
- return router.afterEach(() => listener(toLogic()));
116
+ return router.afterEach((_to, _from, failure) => {
117
+ if (!failure) listener(toLogic());
118
+ });
13
119
  },
14
- async navigate(target, action) {
15
- const to = target.pathname + target.search + target.hash;
120
+ async navigate(target, action, options) {
121
+ if (options?.signal?.aborted) return { status: "cancelled", location: toLogic() };
122
+ const removeGuard = router.beforeResolve(() => options?.signal?.aborted ? false : void 0);
16
123
  try {
124
+ const to = target.pathname + target.search + target.hash;
17
125
  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() };
126
+ return { status: failure ? "cancelled" : "committed", location: toLogic() };
127
+ } finally {
128
+ removeGuard();
22
129
  }
23
130
  },
24
131
  go(delta) {
@@ -26,46 +133,83 @@ function createVueBridgeNavigation(router, options = {}) {
26
133
  }
27
134
  };
28
135
  }
29
- function connectVueBridgeRouter(routing, router) {
30
- let syncing = false;
136
+ function connectVueBridgeRouter(routing, router, options = {}) {
137
+ const push = router.push;
138
+ const replace = router.replace;
139
+ const go = router.go;
140
+ const back = router.back;
141
+ const forward = router.forward;
31
142
  let disposed = false;
32
- let started = false;
33
- const unsub = routing.subscribe((loc) => {
143
+ const report = (error) => console.error("[fulgurjs] \u5B50\u5E94\u7528 Vue \u8DEF\u7531\u540C\u6B65\u5931\u8D25\uFF1A", error);
144
+ const apply = async (loc) => {
145
+ const path = loc.pathname + loc.search + loc.hash;
146
+ if (router.currentRoute.value.fullPath !== path) {
147
+ const failure = await replace.call(router, path);
148
+ if (failure && router.currentRoute.value.fullPath !== path) throw routingSyncError("vue-router", "\u5B50\u5E94\u7528\u5B88\u536B\u62D2\u7EDD\u5E94\u7528\u5BBF\u4E3B\u786E\u8BA4\u7684\u4F4D\u7F6E\uFF1B\u8BF7\u5728\u5BBF\u4E3B\u4FA7\u8BBE\u7F6E\u53D6\u6D88\u5B88\u536B\u3002", []);
149
+ }
150
+ };
151
+ const sync = connectChildNavigation(routing, (loc) => ready.then(() => apply(loc)), report);
152
+ const init = routing.getLocation();
153
+ const ready = push.call(router, init.pathname + init.search + init.hash).then(async (failure) => {
34
154
  if (disposed) return;
35
- const next = loc.pathname + loc.search + loc.hash;
36
- if (router.currentRoute.value.fullPath !== next) void router.replace(next);
155
+ if (routing.getLocation().pathname + routing.getLocation().search + routing.getLocation().hash !== init.pathname + init.search + init.hash) {
156
+ await apply(routing.getLocation());
157
+ return;
158
+ }
159
+ if (failure) throw routingSyncError("vue-router", "\u5B50\u5E94\u7528\u521D\u59CB\u8DEF\u7531\u88AB\u5B88\u536B\u53D6\u6D88\u3002", []);
160
+ const target = locationFromPath(router.currentRoute.value.fullPath);
161
+ if (router.currentRoute.value.fullPath !== init.pathname + init.search + init.hash) {
162
+ await routing.navigate(target, "replace");
163
+ await apply(routing.getLocation());
164
+ }
165
+ }).catch((cause) => {
166
+ throw routingSyncError("vue-router", `\u521D\u59CB\u8DEF\u7531\u51C6\u5907\u5931\u8D25\uFF1A${cause instanceof Error ? cause.message : String(cause)}`, [], cause);
37
167
  });
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);
168
+ const intercept = (native, action) => (to) => {
169
+ const task = (async () => {
170
+ let failure;
171
+ const result = await sync.enqueue(async () => {
172
+ await ready;
173
+ failure = await native.call(router, to);
174
+ return failure ? void 0 : locationFromPath(router.currentRoute.value.fullPath);
175
+ }, typeof to === "object" && to.replace ? "replace" : action);
176
+ if (failure) return failure;
177
+ if (result.status === "cancelled") {
178
+ const remove = router.beforeEach(() => false);
179
+ try {
180
+ return await native.call(router, to);
181
+ } finally {
182
+ remove();
183
+ }
51
184
  }
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 {
185
+ })();
186
+ void task.catch(report);
187
+ return task;
188
+ };
189
+ router.push = intercept(push, "push");
190
+ router.replace = intercept(replace, "replace");
191
+ router.go = (delta) => {
192
+ if (!disposed) routing.go(delta);
193
+ };
194
+ router.back = () => router.go(-1);
195
+ router.forward = () => router.go(1);
196
+ const connection = {
61
197
  ready,
62
198
  dispose() {
63
199
  if (disposed) return;
64
200
  disposed = true;
65
- stopWatch();
66
- unsub();
201
+ options.signal?.removeEventListener("abort", connection.dispose);
202
+ sync.dispose();
203
+ router.push = push;
204
+ router.replace = replace;
205
+ router.go = go;
206
+ router.back = back;
207
+ router.forward = forward;
67
208
  }
68
209
  };
210
+ options.signal?.addEventListener("abort", connection.dispose, { once: true });
211
+ if (options.signal?.aborted) connection.dispose();
212
+ return connection;
69
213
  }
70
214
  export {
71
215
  connectVueBridgeRouter,
@@ -1,6 +1,6 @@
1
1
  import * as vue from 'vue';
2
- import { V as VueBridgeAppOptions } from './bridge-host-vue-BuCGlLdS.js';
3
- export { A as AppContext, a as BridgeApp } from './bridge-core-BOWvVUxO.js';
2
+ import { V as VueBridgeAppOptions } from './bridge-host-vue-Dd1kkwDb.js';
3
+ export { A as AppContext, a as BridgeApp } from './bridge-core-ejlY9ddN.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,10 +1,10 @@
1
1
  import * as react from 'react';
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';
2
+ import { B as BridgeHostRouting } from './bridge-core-ejlY9ddN.js';
3
+ export { A as AppContext, a as BridgeApp } from './bridge-core-ejlY9ddN.js';
4
+ import { R as ReactBridgeAppOptions } from './bridge-host-react-Bua1TCVh.js';
5
+ export { B as BridgeErrorFallback } from './bridge-host-react-Bua1TCVh.js';
6
6
  import * as vue from 'vue';
7
- import { V as VueBridgeAppOptions } from './bridge-host-vue-BuCGlLdS.js';
7
+ import { V as VueBridgeAppOptions } from './bridge-host-vue-Dd1kkwDb.js';
8
8
 
9
9
  /** Vue 宿主桥接工厂:返回 `{ appProps: P; sessionKey?: string | null }` 形态的包装组件 */
10
10
  declare const createVueBridgeApp: <P = Record<string, unknown>>(spec: string, options?: VueBridgeAppOptions) => vue.Component;
@@ -10,7 +10,7 @@ import {
10
10
  releaseBridgeSession,
11
11
  resolveBridgeContext,
12
12
  withBridgeTimeout
13
- } from "./chunk-BTJGYDHT.js";
13
+ } from "./chunk-UFWZPE36.js";
14
14
 
15
15
  // src/bridge-host-vue.ts
16
16
  import { defineComponent, h, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, shallowRef, toRaw, watch } from "vue";