@fulgurjs/federation 5.3.3 → 5.4.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/DESIGN.md +4 -0
  3. package/README.en.md +25 -3
  4. package/README.md +88 -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-ejlY9ddN.d.ts +93 -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 +56 -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-Bua1TCVh.d.ts} +1 -1
  23. package/dist/bridge-host-react.cjs +333 -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-Dd1kkwDb.d.ts} +1 -1
  28. package/dist/bridge-host-vue.cjs +344 -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-BRZQYDPm.d.cts +46 -0
  32. package/dist/bridge-router-core-BRZQYDPm.d.ts +46 -0
  33. package/dist/bridge-router-react.d.ts +73 -0
  34. package/dist/bridge-router-react.js +247 -0
  35. package/dist/bridge-router-vue.d.ts +50 -0
  36. package/dist/bridge-router-vue.js +217 -0
  37. package/dist/bridge-vue.d.ts +2 -2
  38. package/dist/bridge.d.ts +6 -4
  39. package/dist/{chunk-GWJKABE7.js → chunk-43UX3UZE.js} +39 -4
  40. package/dist/chunk-7CQOVLSI.js +274 -0
  41. package/dist/{chunk-DH2QMM4Y.js → chunk-HZGQOW35.js} +51 -2
  42. package/dist/{chunk-ZTRJLCOF.js → chunk-KT3YARK5.js} +27 -3
  43. package/dist/{chunk-LE2G23E7.js → chunk-RXXBTPAY.js} +1 -1
  44. package/dist/{chunk-WD2ON7SN.js → chunk-UFWZPE36.js} +313 -12
  45. package/dist/index.cjs +30 -10
  46. package/dist/index.js +30 -10
  47. package/dist/react.d.ts +51 -4
  48. package/dist/runtime-entry.d.ts +60 -4
  49. package/dist/runtime.js +9 -9
  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
@@ -77,7 +77,15 @@ var BridgeErrorCodes = {
77
77
  /** 桥接准备或生命周期失败(details.phase = getContext | mount | unmount) */
78
78
  BRIDGE_LIFECYCLE_FAILED: "MFU-016",
79
79
  /** 桥接会话参数与 AppContext 不一致(受控 sessionKey 与全局会话矛盾) */
80
- BRIDGE_SESSION_MISMATCH: "MFU-017"
80
+ BRIDGE_SESSION_MISMATCH: "MFU-017",
81
+ /** 路由同步配置/前缀冲突(basePath 非法、同页前缀冲突或重叠) */
82
+ ROUTING_CONFIG_INVALID: "MFU-030",
83
+ /** 路由协议缺失/通道失效(子应用未声明 routing 协议,或已销毁通道被再次使用) */
84
+ ROUTING_PROTOCOL_MISSING: "MFU-031",
85
+ /** 非法导航(目标越界前缀、非法 go 参数、失效实例的请求) */
86
+ ROUTING_NAVIGATION_INVALID: "MFU-032",
87
+ /** 路由准备/同步失败(重定向循环、子应用路由准备失败且已阻止错误页面显示) */
88
+ ROUTING_SYNC_FAILED: "MFU-033"
81
89
  };
82
90
  function invalidBridgeContractError(spec, mod) {
83
91
  const actual = mod === null ? "null" : typeof mod;
@@ -134,6 +142,43 @@ function invalidSessionKeyError(spec, value) {
134
142
  { spec, value: String(value) }
135
143
  );
136
144
  }
145
+ function routingConfigError(spec, reason) {
146
+ return new FgError(
147
+ BridgeErrorCodes.ROUTING_CONFIG_INVALID,
148
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u914D\u7F6E\u65E0\u6CD5\u542F\u7528\u3002
149
+ \u539F\u56E0\uFF1A${reason}
150
+ \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`,
151
+ { spec }
152
+ );
153
+ }
154
+ function routingProtocolError(spec, reason) {
155
+ return new FgError(
156
+ BridgeErrorCodes.ROUTING_PROTOCOL_MISSING,
157
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u901A\u9053\u4E0D\u53EF\u7528\u3002
158
+ \u539F\u56E0\uFF1A${reason}
159
+ \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`,
160
+ { spec }
161
+ );
162
+ }
163
+ function routingNavigationError(spec, reason) {
164
+ return new FgError(
165
+ BridgeErrorCodes.ROUTING_NAVIGATION_INVALID,
166
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684\u4E00\u6B21\u5BFC\u822A\u8BF7\u6C42\u88AB\u62D2\u7EDD\u3002
167
+ \u539F\u56E0\uFF1A${reason}
168
+ \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`,
169
+ { spec }
170
+ );
171
+ }
172
+ function routingSyncError(spec, reason, chain, cause) {
173
+ return new FgError(
174
+ BridgeErrorCodes.ROUTING_SYNC_FAILED,
175
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u5931\u8D25\u3002
176
+ \u539F\u56E0\uFF1A${reason}
177
+ \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`,
178
+ { spec, chain: [...chain] },
179
+ { cause }
180
+ );
181
+ }
137
182
 
138
183
  // src/bridge-core.ts
139
184
  function assertBridgeContract(spec, mod) {
@@ -238,7 +283,275 @@ function withBridgeTimeout(p, ms, spec) {
238
283
  });
239
284
  }
240
285
 
286
+ // src/bridge-router-core.ts
287
+ var MAX_REDIRECT_PER_REQUEST = 5;
288
+ var MAX_GO_DELTA = 50;
289
+ function normalizeBasePath(basePath, spec) {
290
+ const bad = (reason) => routingConfigError(spec, reason);
291
+ if (typeof basePath !== "string" || basePath === "") throw bad("basePath \u4E0D\u80FD\u4E3A\u7A7A\u3002");
292
+ if (/^\/+$/u.test(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');
293
+ if (!basePath.startsWith("/")) throw bad(`basePath \u5FC5\u987B\u4EE5 "/" \u5F00\u5934\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002`);
294
+ if (/[?#]/.test(basePath) || basePath.includes("://")) throw bad(`basePath \u4E0D\u80FD\u5305\u542B query/hash/\u534F\u8BAE\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002`);
295
+ 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`);
296
+ if (/\\|\/\/|%(?:2e|2f|5c)/i.test(basePath) || basePath.split("/").some((part) => part === "." || part === "..")) throw bad("basePath \u4E0D\u80FD\u5305\u542B\u7A7A\u8DEF\u5F84\u6BB5\u3001\u8F6C\u4E49\u5206\u9694\u7B26\u6216\u76F8\u5BF9\u8DEF\u5F84\u6BB5\u3002");
297
+ return basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
298
+ }
299
+ function matchesBasePath(hostPathname, basePath) {
300
+ return hostPathname === basePath || hostPathname.startsWith(basePath + "/");
301
+ }
302
+ function overlapsBasePath(a, b) {
303
+ return a === b || a.startsWith(b + "/") || b.startsWith(a + "/");
304
+ }
305
+ function toChildLocation(host, basePath) {
306
+ const rest = host.pathname === basePath ? "/" : host.pathname.slice(basePath.length);
307
+ return { pathname: rest || "/", search: host.search, hash: host.hash };
308
+ }
309
+ function toHostLocation(child, basePath, spec) {
310
+ const childPath = child.pathname === "/" ? "" : child.pathname;
311
+ if (childPath !== "" && !childPath.startsWith("/")) {
312
+ throw routingNavigationError(spec, `\u5BFC\u822A\u76EE\u6807 pathname \u5FC5\u987B\u4EE5 "/" \u5F00\u5934\uFF1A\u5F53\u524D\u4E3A "${child.pathname}"\u3002`);
313
+ }
314
+ return { pathname: basePath + childPath, search: child.search, hash: child.hash };
315
+ }
316
+ function sameLocation(a, b) {
317
+ return a.pathname === b.pathname && a.search === b.search && a.hash === b.hash;
318
+ }
319
+ function assertChildTargetInScope(target, basePath, spec) {
320
+ if (!target || typeof target.pathname !== "string" || typeof target.search !== "string" || typeof target.hash !== "string" || target.search !== "" && !target.search.startsWith("?") || target.hash !== "" && !target.hash.startsWith("#")) throw routingNavigationError(spec, "\u5BFC\u822A\u76EE\u6807\u5FC5\u987B\u63D0\u4F9B pathname\u3001search\u3001hash \u4E09\u6BB5\uFF0Csearch/hash \u5E94\u4E3A\u7A7A\u6216\u4EE5 ?/# \u5F00\u5934\u3002");
321
+ if (/[\x00-\x20\\?#]|%(?:2e|2f|5c)/i.test(target.pathname) || target.pathname.includes("//") || target.pathname.split("/").some((part) => part === "." || part === "..")) {
322
+ 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`);
323
+ }
324
+ const host = toHostLocation(target, basePath, spec);
325
+ if (!matchesBasePath(host.pathname, basePath)) {
326
+ throw routingNavigationError(spec, `\u5BFC\u822A\u76EE\u6807 ${host.pathname} \u4E0D\u5728\u524D\u7F00 ${basePath} \u4E0B\u3002`);
327
+ }
328
+ }
329
+ var activeRoutingPrefixes = /* @__PURE__ */ new Map();
330
+ function acquireRoutingPrefix(basePath, spec) {
331
+ for (const [active] of activeRoutingPrefixes) {
332
+ if ((activeRoutingPrefixes.get(active) ?? 0) > 0 && overlapsBasePath(active, basePath)) {
333
+ 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`);
334
+ }
335
+ }
336
+ activeRoutingPrefixes.set(basePath, (activeRoutingPrefixes.get(basePath) ?? 0) + 1);
337
+ let released = false;
338
+ return () => {
339
+ if (released) return;
340
+ released = true;
341
+ activeRoutingPrefixes.set(basePath, Math.max(0, (activeRoutingPrefixes.get(basePath) ?? 1) - 1));
342
+ };
343
+ }
344
+ function assertBridgeRoutingProtocol(spec, contract) {
345
+ const r = contract?.routing;
346
+ if (!r || r.protocol !== 1) {
347
+ throw routingProtocolError(
348
+ spec,
349
+ `\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`
350
+ );
351
+ }
352
+ }
353
+ var RoutingChannel = class {
354
+ id;
355
+ basePath;
356
+ spec;
357
+ host;
358
+ listeners = /* @__PURE__ */ new Set();
359
+ current;
360
+ lastConfirmed;
361
+ seq = 0;
362
+ inflight = null;
363
+ queue;
364
+ generation = 0;
365
+ disposed = false;
366
+ active = true;
367
+ releasePrefix;
368
+ unsubHost;
369
+ /** 连续内部 replace 计数(有界重定向检测) */
370
+ redirectChain = [];
371
+ redirectTimer;
372
+ /** 诊断观测钩子(测试断言/诊断用) */
373
+ onEvent;
374
+ constructor(id, basePath, host, spec) {
375
+ this.id = id;
376
+ this.spec = spec;
377
+ this.basePath = normalizeBasePath(basePath, spec);
378
+ this.host = host;
379
+ const initial = host.getLocation();
380
+ this.current = matchesBasePath(initial.pathname, this.basePath) ? toChildLocation(initial, this.basePath) : { pathname: "/", search: "", hash: "" };
381
+ this.lastConfirmed = { ...this.current };
382
+ this.releasePrefix = acquireRoutingPrefix(this.basePath, spec);
383
+ this.unsubHost = host.subscribe((loc) => this.handleHostLocation(loc));
384
+ }
385
+ emit(loc) {
386
+ this.current = { ...loc };
387
+ for (const l of [...this.listeners]) {
388
+ try {
389
+ l(this.current);
390
+ } catch (e) {
391
+ 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);
392
+ }
393
+ }
394
+ }
395
+ handleHostLocation(hostLoc) {
396
+ if (this.disposed) return;
397
+ if (!matchesBasePath(hostLoc.pathname, this.basePath)) {
398
+ this.cancelPending();
399
+ this.onEvent?.({ type: "prefix-miss", pathname: hostLoc.pathname });
400
+ return;
401
+ }
402
+ if (!this.active) {
403
+ this.onEvent?.({ type: "paused-host-loc" });
404
+ return;
405
+ }
406
+ const child = toChildLocation(hostLoc, this.basePath);
407
+ if (this.inflight && sameLocation(child, this.inflight.target)) {
408
+ const { seq } = this.inflight;
409
+ this.lastConfirmed = child;
410
+ this.emit(child);
411
+ this.onEvent?.({ type: "request-confirmed", seq });
412
+ return;
413
+ }
414
+ if (this.inflight) {
415
+ const seq = this.inflight.seq;
416
+ this.lastConfirmed = child;
417
+ this.cancelPending();
418
+ this.emit(child);
419
+ this.onEvent?.({ type: "request-superseded", seq });
420
+ return;
421
+ }
422
+ if (!sameLocation(child, this.lastConfirmed)) this.generation++;
423
+ this.lastConfirmed = child;
424
+ this.emit(child);
425
+ this.onEvent?.({ type: "host-broadcast" });
426
+ }
427
+ getLocation() {
428
+ return { ...this.current };
429
+ }
430
+ subscribe(listener) {
431
+ if (this.disposed) {
432
+ 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`);
433
+ }
434
+ this.listeners.add(listener);
435
+ return () => {
436
+ this.listeners.delete(listener);
437
+ };
438
+ }
439
+ cancelPending() {
440
+ this.generation++;
441
+ const pending = this.inflight;
442
+ this.inflight = null;
443
+ if (pending) {
444
+ pending.abort.abort();
445
+ pending.resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
446
+ }
447
+ }
448
+ navigate(target, action) {
449
+ const generation = this.generation;
450
+ const snapshot = { ...target };
451
+ const run = () => generation !== this.generation ? Promise.resolve({ status: "cancelled", location: { ...this.lastConfirmed } }) : this.performNavigation(snapshot, action);
452
+ const task = this.queue ? this.queue.then(run) : run();
453
+ const tail = task.then(() => {
454
+ }, () => {
455
+ });
456
+ this.queue = tail;
457
+ void tail.then(() => {
458
+ if (this.queue === tail) this.queue = void 0;
459
+ });
460
+ return task;
461
+ }
462
+ async performNavigation(target, action) {
463
+ if (this.disposed || !this.active || !matchesBasePath(this.host.getLocation().pathname, this.basePath)) {
464
+ this.onEvent?.({ type: this.disposed ? "rejected-disposed" : "rejected-inactive" });
465
+ return { status: "cancelled", location: { ...this.lastConfirmed } };
466
+ }
467
+ assertChildTargetInScope(target, this.basePath, this.spec);
468
+ if (sameLocation(target, this.lastConfirmed)) {
469
+ this.onEvent?.({ type: "noop" });
470
+ return { status: "committed", location: { ...this.lastConfirmed } };
471
+ }
472
+ if (action === "replace") {
473
+ this.redirectChain.push(target.pathname + target.search + target.hash);
474
+ } else {
475
+ this.redirectChain = [];
476
+ }
477
+ if (this.redirectTimer) clearTimeout(this.redirectTimer);
478
+ this.redirectTimer = setTimeout(() => {
479
+ this.redirectChain = [];
480
+ }, 1e3);
481
+ if (this.redirectChain.length > MAX_REDIRECT_PER_REQUEST) {
482
+ const chain = [...this.redirectChain];
483
+ this.redirectChain = [];
484
+ 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);
485
+ }
486
+ const seq = ++this.seq;
487
+ this.onEvent?.({ type: "request-start", seq, target: { ...target }, action });
488
+ return new Promise((resolve, reject) => {
489
+ const abort = new AbortController();
490
+ this.inflight = { seq, target: { ...target }, resolve, abort };
491
+ Promise.resolve().then(() => abort.signal.aborted ? { status: "cancelled", location: this.host.getLocation() } : this.host.navigate(toHostLocation(target, this.basePath, this.spec), action, { signal: abort.signal })).then((result) => {
492
+ if (!this.inflight || this.inflight.seq !== seq) return;
493
+ if (this.disposed || !this.active) {
494
+ this.inflight = null;
495
+ resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
496
+ return;
497
+ }
498
+ this.inflight = null;
499
+ if (result.status === "committed" && matchesBasePath(result.location.pathname, this.basePath)) {
500
+ const child = toChildLocation(result.location, this.basePath);
501
+ this.lastConfirmed = child;
502
+ this.emit(child);
503
+ } else {
504
+ this.emit({ ...this.lastConfirmed });
505
+ }
506
+ this.onEvent?.({ type: "host-verdict", seq, status: result.status });
507
+ resolve(
508
+ result.status === "committed" && matchesBasePath(result.location.pathname, this.basePath) ? { status: "committed", location: { ...this.lastConfirmed } } : { status: "cancelled", location: { ...this.lastConfirmed } }
509
+ );
510
+ }).catch((cause) => {
511
+ if (this.inflight?.seq === seq) {
512
+ this.inflight = null;
513
+ this.onEvent?.({ type: "host-error", seq });
514
+ this.emit({ ...this.lastConfirmed });
515
+ reject(routingSyncError(this.spec, `\u5BBF\u4E3B\u5BFC\u822A\u6267\u884C\u5931\u8D25\uFF1A${cause instanceof Error ? cause.message : String(cause)}`, [], cause));
516
+ }
517
+ });
518
+ });
519
+ }
520
+ /** 委托宿主历史(有限整数);不创建第二条独立历史 */
521
+ go(delta) {
522
+ if (this.disposed || !this.active || !matchesBasePath(this.host.getLocation().pathname, this.basePath)) return;
523
+ if (typeof delta !== "number" || !Number.isInteger(delta) || Math.abs(delta) > MAX_GO_DELTA) {
524
+ 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`);
525
+ }
526
+ this.host.go(delta);
527
+ }
528
+ /** KeepAlive deactivate:暂停广播与写入;通道与订阅保留(activate 后 resync) */
529
+ setActive(active) {
530
+ if (this.disposed) return;
531
+ if (!active) this.cancelPending();
532
+ this.active = active;
533
+ this.onEvent?.({ type: active ? "resumed" : "paused" });
534
+ if (active) this.handleHostLocation(this.host.getLocation());
535
+ }
536
+ /** 挂载生命周期结束:清理订阅/登记/在飞请求;此后一切写入失效 */
537
+ dispose() {
538
+ if (this.disposed) return;
539
+ this.disposed = true;
540
+ if (this.redirectTimer) clearTimeout(this.redirectTimer);
541
+ this.unsubHost();
542
+ this.releasePrefix();
543
+ this.cancelPending();
544
+ this.listeners.clear();
545
+ this.onEvent?.({ type: "disposed" });
546
+ }
547
+ };
548
+
241
549
  // src/bridge-host-vue.ts
550
+ var assertBridgeRoutingProtocolSpec = (spec, contract) => assertBridgeRoutingProtocol(spec, contract);
551
+ function routingKey(routing) {
552
+ if (!routing) return void 0;
553
+ return routing.basePath + "|" + String(routing.navigation && typeof routing.navigation.navigate === "function");
554
+ }
242
555
  var ERROR_STYLE = {
243
556
  padding: "16px",
244
557
  border: "1px solid #fde2e2",
@@ -316,7 +629,9 @@ function createVueBridgeAppWithLoader(loadRemote) {
316
629
  props: {
317
630
  appProps: { type: Object, required: false, default: void 0 },
318
631
  // 桥接控制参数:undefined(不启用受控会话)/ null(登出态)/ 非空字符串(登录代次)
319
- sessionKey: { type: null, required: false, default: void 0 }
632
+ sessionKey: { type: null, required: false, default: void 0 },
633
+ // URL 同步控制通道(独立于 appProps/Context;省略 = 原 memory 模式)
634
+ routing: { type: Object, required: false, default: void 0 }
320
635
  },
321
636
  setup(props) {
322
637
  const status = (0, import_vue.ref)("idle");
@@ -326,6 +641,8 @@ function createVueBridgeAppWithLoader(loadRemote) {
326
641
  let contract;
327
642
  let containerBlocked = false;
328
643
  let acquiredSession;
644
+ let channel;
645
+ let genAbort;
329
646
  const releaseAcquired = () => {
330
647
  if (acquiredSession !== void 0) {
331
648
  releaseBridgeSession(acquiredSession);
@@ -337,6 +654,10 @@ function createVueBridgeAppWithLoader(loadRemote) {
337
654
  generation++;
338
655
  const el = container.value;
339
656
  releaseAcquired();
657
+ genAbort?.abort();
658
+ genAbort = void 0;
659
+ channel?.dispose();
660
+ channel = void 0;
340
661
  if (contract && el) {
341
662
  const c = contract;
342
663
  contract = void 0;
@@ -378,7 +699,18 @@ function createVueBridgeAppWithLoader(loadRemote) {
378
699
  if (myGeneration !== generation) return;
379
700
  contract = assertBridgeContract(spec, mod);
380
701
  const appPropsSource = props.appProps ? (0, import_vue.toRaw)(props.appProps) : void 0;
381
- await contract.mount(el, { ...appPropsSource ?? {} });
702
+ genAbort = new AbortController();
703
+ let mountOptions = { signal: genAbort.signal };
704
+ if (props.routing) {
705
+ assertBridgeRoutingProtocolSpec(spec, contract);
706
+ const navigation = props.routing.navigation;
707
+ const latestLoc = navigation.getLocation();
708
+ if (myGeneration !== generation) return;
709
+ channel = new RoutingChannel(`bridge-vue:${spec}:${myGeneration}`, props.routing.basePath, navigation, spec);
710
+ mountOptions = { signal: genAbort.signal, routing: channel };
711
+ void latestLoc;
712
+ }
713
+ await contract.mount(el, { ...appPropsSource ?? {} }, mountOptions);
382
714
  if (myGeneration !== generation) return;
383
715
  status.value = "ready";
384
716
  } catch (e) {
@@ -403,6 +735,15 @@ function createVueBridgeAppWithLoader(loadRemote) {
403
735
  (0, import_vue.watch)(() => props.sessionKey, () => {
404
736
  start();
405
737
  });
738
+ (0, import_vue.watch)(() => routingKey(props.routing), (after, before) => {
739
+ if (after !== before) start();
740
+ });
741
+ (0, import_vue.onDeactivated)(() => {
742
+ channel?.setActive(false);
743
+ });
744
+ (0, import_vue.onActivated)(() => {
745
+ channel?.setActive(true);
746
+ });
406
747
  (0, import_vue.onBeforeUnmount)(() => {
407
748
  invalidate();
408
749
  });
@@ -1,3 +1,7 @@
1
+ import {
2
+ RoutingChannel,
3
+ assertBridgeRoutingProtocol
4
+ } from "./chunk-7CQOVLSI.js";
1
5
  import {
2
6
  acquireBridgeSession,
3
7
  assertBridgeContract,
@@ -6,17 +10,22 @@ import {
6
10
  releaseBridgeSession,
7
11
  resolveBridgeContext,
8
12
  withBridgeTimeout
9
- } from "./chunk-LE2G23E7.js";
13
+ } from "./chunk-RXXBTPAY.js";
10
14
  import {
11
15
  bridgeLifecycleError
12
- } from "./chunk-DH2QMM4Y.js";
16
+ } from "./chunk-HZGQOW35.js";
13
17
  import {
14
18
  provideAppContext
15
19
  } from "./chunk-TSYLDY6H.js";
16
20
  import "./chunk-OF2JCXTZ.js";
17
21
 
18
22
  // src/bridge-host-vue.ts
19
- import { defineComponent, h, onBeforeUnmount, onMounted, ref, shallowRef, toRaw, watch } from "vue";
23
+ import { defineComponent, h, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, shallowRef, toRaw, watch } from "vue";
24
+ var assertBridgeRoutingProtocolSpec = (spec, contract) => assertBridgeRoutingProtocol(spec, contract);
25
+ function routingKey(routing) {
26
+ if (!routing) return void 0;
27
+ return routing.basePath + "|" + String(routing.navigation && typeof routing.navigation.navigate === "function");
28
+ }
20
29
  var ERROR_STYLE = {
21
30
  padding: "16px",
22
31
  border: "1px solid #fde2e2",
@@ -94,7 +103,9 @@ function createVueBridgeAppWithLoader(loadRemote) {
94
103
  props: {
95
104
  appProps: { type: Object, required: false, default: void 0 },
96
105
  // 桥接控制参数:undefined(不启用受控会话)/ null(登出态)/ 非空字符串(登录代次)
97
- sessionKey: { type: null, required: false, default: void 0 }
106
+ sessionKey: { type: null, required: false, default: void 0 },
107
+ // URL 同步控制通道(独立于 appProps/Context;省略 = 原 memory 模式)
108
+ routing: { type: Object, required: false, default: void 0 }
98
109
  },
99
110
  setup(props) {
100
111
  const status = ref("idle");
@@ -104,6 +115,8 @@ function createVueBridgeAppWithLoader(loadRemote) {
104
115
  let contract;
105
116
  let containerBlocked = false;
106
117
  let acquiredSession;
118
+ let channel;
119
+ let genAbort;
107
120
  const releaseAcquired = () => {
108
121
  if (acquiredSession !== void 0) {
109
122
  releaseBridgeSession(acquiredSession);
@@ -115,6 +128,10 @@ function createVueBridgeAppWithLoader(loadRemote) {
115
128
  generation++;
116
129
  const el = container.value;
117
130
  releaseAcquired();
131
+ genAbort?.abort();
132
+ genAbort = void 0;
133
+ channel?.dispose();
134
+ channel = void 0;
118
135
  if (contract && el) {
119
136
  const c = contract;
120
137
  contract = void 0;
@@ -156,7 +173,18 @@ function createVueBridgeAppWithLoader(loadRemote) {
156
173
  if (myGeneration !== generation) return;
157
174
  contract = assertBridgeContract(spec, mod);
158
175
  const appPropsSource = props.appProps ? toRaw(props.appProps) : void 0;
159
- await contract.mount(el, { ...appPropsSource ?? {} });
176
+ genAbort = new AbortController();
177
+ let mountOptions = { signal: genAbort.signal };
178
+ if (props.routing) {
179
+ assertBridgeRoutingProtocolSpec(spec, contract);
180
+ const navigation = props.routing.navigation;
181
+ const latestLoc = navigation.getLocation();
182
+ if (myGeneration !== generation) return;
183
+ channel = new RoutingChannel(`bridge-vue:${spec}:${myGeneration}`, props.routing.basePath, navigation, spec);
184
+ mountOptions = { signal: genAbort.signal, routing: channel };
185
+ void latestLoc;
186
+ }
187
+ await contract.mount(el, { ...appPropsSource ?? {} }, mountOptions);
160
188
  if (myGeneration !== generation) return;
161
189
  status.value = "ready";
162
190
  } catch (e) {
@@ -181,6 +209,15 @@ function createVueBridgeAppWithLoader(loadRemote) {
181
209
  watch(() => props.sessionKey, () => {
182
210
  start();
183
211
  });
212
+ watch(() => routingKey(props.routing), (after, before) => {
213
+ if (after !== before) start();
214
+ });
215
+ onDeactivated(() => {
216
+ channel?.setActive(false);
217
+ });
218
+ onActivated(() => {
219
+ channel?.setActive(true);
220
+ });
184
221
  onBeforeUnmount(() => {
185
222
  invalidate();
186
223
  });
@@ -1,12 +1,14 @@
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';
4
- export { A as AppContext, B as BridgeApp } from './bridge-core-6uLJiMVa.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';
5
6
 
6
7
  /** React 宿主桥接工厂:返回 `ComponentType<{ appProps: P; sessionKey?: string | null }>` */
7
8
  declare const createReactBridgeApp: <P = Record<string, unknown>>(spec: string, options?: ReactBridgeAppOptions) => react.ComponentType<{
8
9
  appProps?: P;
9
10
  sessionKey?: string | null;
11
+ routing?: BridgeHostRouting;
10
12
  }>;
11
13
 
12
14
  export { ReactBridgeAppOptions, createReactBridgeApp };
@@ -0,0 +1,46 @@
1
+ /** 统一逻辑位置:pathname 以 / 开头;search 空串或 ? 开头;hash 空串或 # 开头 */
2
+ interface BridgeLocation {
3
+ pathname: string;
4
+ search: string;
5
+ hash: string;
6
+ }
7
+ type BridgeNavigationResult = {
8
+ status: 'committed';
9
+ location: BridgeLocation;
10
+ } | {
11
+ status: 'cancelled';
12
+ location: BridgeLocation;
13
+ };
14
+ /** 宿主导航端口(宿主适配器把 Router 包装成本接口;逻辑路径不含部署 base) */
15
+ interface BridgeHostNavigation {
16
+ getLocation(): BridgeLocation;
17
+ subscribe(listener: (location: BridgeLocation) => void): () => void;
18
+ navigate(target: BridgeLocation, action: 'push' | 'replace', options?: {
19
+ signal?: AbortSignal;
20
+ }): 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
+ /** 挂载控制参数(mount 第三参数;不混入业务 props) */
31
+ interface BridgeMountOptions {
32
+ signal?: AbortSignal;
33
+ routing?: BridgeChildRoute;
34
+ }
35
+ /** 子应用路由协议声明(defineBridgeApp(工厂, { routing: true }) 时写入) */
36
+ interface BridgeRoutingProtocol {
37
+ protocol: 1;
38
+ }
39
+ /** 宿主组件 routing prop(独立控制通道,不进 appProps/Context) */
40
+ interface BridgeHostRouting {
41
+ /** 宿主路由视角的静态绝对路径,如 "/approval";同页各同步实例不得相同或重叠 */
42
+ basePath: string;
43
+ navigation: BridgeHostNavigation;
44
+ }
45
+
46
+ export type { BridgeChildRoute as B, BridgeMountOptions as a, BridgeRoutingProtocol as b, BridgeHostRouting as c };
@@ -0,0 +1,46 @@
1
+ /** 统一逻辑位置:pathname 以 / 开头;search 空串或 ? 开头;hash 空串或 # 开头 */
2
+ interface BridgeLocation {
3
+ pathname: string;
4
+ search: string;
5
+ hash: string;
6
+ }
7
+ type BridgeNavigationResult = {
8
+ status: 'committed';
9
+ location: BridgeLocation;
10
+ } | {
11
+ status: 'cancelled';
12
+ location: BridgeLocation;
13
+ };
14
+ /** 宿主导航端口(宿主适配器把 Router 包装成本接口;逻辑路径不含部署 base) */
15
+ interface BridgeHostNavigation {
16
+ getLocation(): BridgeLocation;
17
+ subscribe(listener: (location: BridgeLocation) => void): () => void;
18
+ navigate(target: BridgeLocation, action: 'push' | 'replace', options?: {
19
+ signal?: AbortSignal;
20
+ }): 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
+ /** 挂载控制参数(mount 第三参数;不混入业务 props) */
31
+ interface BridgeMountOptions {
32
+ signal?: AbortSignal;
33
+ routing?: BridgeChildRoute;
34
+ }
35
+ /** 子应用路由协议声明(defineBridgeApp(工厂, { routing: true }) 时写入) */
36
+ interface BridgeRoutingProtocol {
37
+ protocol: 1;
38
+ }
39
+ /** 宿主组件 routing prop(独立控制通道,不进 appProps/Context) */
40
+ interface BridgeHostRouting {
41
+ /** 宿主路由视角的静态绝对路径,如 "/approval";同页各同步实例不得相同或重叠 */
42
+ basePath: string;
43
+ navigation: BridgeHostNavigation;
44
+ }
45
+
46
+ export type { BridgeChildRoute as B, BridgeMountOptions as a, BridgeRoutingProtocol as b, BridgeHostRouting as c };