@fulgurjs/federation 5.3.2 → 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 (67) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/DESIGN.md +4 -0
  3. package/README.en.md +26 -4
  4. package/README.md +85 -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-TOT3P7AK.js → chunk-BTJGYDHT.js} +412 -27
  40. package/dist/{chunk-MMXKQN2H.js → chunk-GF6KOH67.js} +39 -4
  41. package/dist/{chunk-DH2QMM4Y.js → chunk-JQF7KYZF.js} +50 -2
  42. package/dist/{chunk-X4F2BHNM.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 +136 -15
  46. package/dist/index.js +136 -15
  47. package/dist/react.d.ts +56 -4
  48. package/dist/runtime-entry.d.ts +65 -4
  49. package/dist/runtime.js +18 -18
  50. package/examples/bridge/react-host/package-lock.json +107 -106
  51. package/examples/bridge/react-host/package.json +1 -1
  52. package/examples/bridge/react-remote/package-lock.json +107 -106
  53. package/examples/bridge/react-remote/package.json +1 -1
  54. package/examples/bridge/vue-host/package-lock.json +107 -106
  55. package/examples/bridge/vue-host/package.json +1 -1
  56. package/examples/bridge/vue-remote/package-lock.json +107 -106
  57. package/examples/bridge/vue-remote/package.json +1 -1
  58. package/examples/react/host/package-lock.json +3 -3
  59. package/examples/react/host/package.json +1 -1
  60. package/examples/react/remote/package-lock.json +3 -3
  61. package/examples/react/remote/package.json +1 -1
  62. package/examples/vue/host/package-lock.json +3 -3
  63. package/examples/vue/host/package.json +1 -1
  64. package/examples/vue/remote/package-lock.json +3 -3
  65. package/examples/vue/remote/package.json +1 -1
  66. package/package.json +28 -4
  67. package/dist/bridge-core-6uLJiMVa.d.ts +0 -42
@@ -252,6 +252,14 @@ function createRuntime() {
252
252
  const remotes = /* @__PURE__ */ new Map();
253
253
  const plugins = [];
254
254
  const reportedSingletonSkews = /* @__PURE__ */ new Set();
255
+ const resolvedShareSnapshots = /* @__PURE__ */ new Map();
256
+ const shareRequestKey = (name, opts) => JSON.stringify([
257
+ opts.shareScope || "default",
258
+ opts.shareKey || name,
259
+ opts.requiredVersion ?? null,
260
+ !!opts.singleton,
261
+ !!opts.strictVersion
262
+ ]);
255
263
  const loadedModules = /* @__PURE__ */ new Map();
256
264
  const remoteManifests = /* @__PURE__ */ new Map();
257
265
  const stylesheetLoads = /* @__PURE__ */ new WeakMap();
@@ -472,14 +480,16 @@ function createRuntime() {
472
480
  }
473
481
  function registerPlugins2(list) {
474
482
  plugins.push(...list);
483
+ resolvedShareSnapshots.clear();
475
484
  applyPlugins();
476
485
  }
477
- async function loadShare2(name, opts = {}) {
486
+ function selectShareEntry(name, opts, readyOnly = false) {
478
487
  const scopeName = opts.shareScope || "default";
479
488
  const shareKey = opts.shareKey || name;
480
489
  const scope = getScope(scopeName);
481
490
  const byName = scope[shareKey] || {};
482
- const versions = Object.keys(byName);
491
+ const versions = Object.keys(byName).filter((v) => !readyOnly || opts.singleton || byName[v].value !== void 0);
492
+ if (readyOnly && versions.length === 0) return void 0;
483
493
  const req = opts.requiredVersion;
484
494
  const satisfying = versions.filter(
485
495
  (v) => req === void 0 || req === false || satisfies(v, req)
@@ -512,49 +522,121 @@ function createRuntime() {
512
522
  pick = [...satisfying].sort(compareVersions).pop();
513
523
  if (!pick) {
514
524
  if (opts.strictVersion) {
515
- const err2 = new FgError(
525
+ const err = new FgError(
516
526
  ErrorCodes.SHARE_STRICT_VERSION,
517
527
  `\u73B0\u8C61\uFF1A\u5171\u4EAB\u4F5C\u7528\u57DF "${scopeName}" \u4E2D\u627E\u4E0D\u5230\u7B26\u5408\u8981\u6C42\u7684 "${shareKey}"\u3002
518
528
  \u539F\u56E0\uFF1A\u8981\u6C42\u7248\u672C ${req}\uFF0C\u5F53\u524D\u53EF\u7528\u7248\u672C\u4E3A ${versions.join("\u3001") || "\u65E0"}\u3002
519
529
  \u4FEE\u6CD5\uFF1A\u7EDF\u4E00\u5BBF\u4E3B\u4E0E\u8FDC\u7A0B\u7684\u4F9D\u8D56\u7248\u672C\uFF0C\u6216\u4FEE\u6B63 shared.requiredVersion\u3002`
520
530
  );
521
- emitError({ remote: shareKey, error: err2 });
522
- throw err2;
531
+ emitError({ remote: shareKey, error: err });
532
+ throw err;
523
533
  }
524
- if (opts.fallback) return opts.fallback();
525
- const err = new FgError(
526
- ErrorCodes.SHARE_NOT_AVAILABLE,
527
- `\u73B0\u8C61\uFF1A\u65E0\u6CD5\u52A0\u8F7D\u5171\u4EAB\u4F9D\u8D56 "${shareKey}"\u3002
528
- \u539F\u56E0\uFF1A\u4F5C\u7528\u57DF "${scopeName}" \u4E2D\u6CA1\u6709\u7B26\u5408 ${req ?? "\u4EFB\u610F\u7248\u672C"} \u8981\u6C42\u7684\u7248\u672C\uFF0C\u4E14\u672A\u914D\u7F6E\u672C\u5730\u526F\u672C\u3002
529
- \u4FEE\u6CD5\uFF1A\u5728\u5BBF\u4E3B\u6216\u8FDC\u7A0B\u6CE8\u518C\u8BE5\u4F9D\u8D56\uFF0C\u6216\u4E3A shared \u914D\u7F6E\u53EF\u7528\u7684\u672C\u5730\u526F\u672C\u3002`,
530
- { shareKey, requiredVersion: req }
531
- );
532
- emitError({ remote: shareKey, error: err });
533
- throw err;
534
+ return void 0;
534
535
  }
535
536
  }
536
- let entry = byName[pick];
537
+ return byName[pick];
538
+ }
539
+ function registerFallbackInstance(name, opts, instance) {
540
+ const localVersion = opts.localVersion;
541
+ if (!localVersion || instance === void 0) return;
542
+ const scopeName = opts.shareScope || "default";
543
+ const key = opts.shareKey || name;
544
+ const scope = getScope(scopeName);
545
+ const byName = Object.hasOwn(scope, key) ? scope[key] : void 0;
546
+ if (byName && Object.keys(byName).length > 0) {
547
+ try {
548
+ const pick = selectShareEntry(name, opts);
549
+ if (pick && pick.value === void 0 && pick.version === localVersion) {
550
+ pick.value = instance;
551
+ pick.loaded = true;
552
+ }
553
+ } catch {
554
+ }
555
+ return;
556
+ }
557
+ registerShare2(scopeName, key, localVersion, () => Promise.resolve(instance), {
558
+ from: "fulgurjs:local-fallback",
559
+ loaded: true
560
+ });
561
+ const fresh = scope[key]?.[localVersion];
562
+ if (fresh) fresh.value = instance;
563
+ }
564
+ async function loadShare2(name, opts = {}) {
565
+ const scopeName = opts.shareScope || "default";
566
+ const shareKey = opts.shareKey || name;
567
+ const byName = getScope(scopeName)[shareKey] || {};
568
+ let entry = selectShareEntry(name, opts);
569
+ if (!entry) {
570
+ if (opts.fallback) {
571
+ const instance2 = await opts.fallback();
572
+ registerFallbackInstance(name, opts, instance2);
573
+ return instance2;
574
+ }
575
+ const err = new FgError(
576
+ ErrorCodes.SHARE_NOT_AVAILABLE,
577
+ `\u73B0\u8C61\uFF1A\u65E0\u6CD5\u52A0\u8F7D\u5171\u4EAB\u4F9D\u8D56 "${shareKey}"\u3002
578
+ \u539F\u56E0\uFF1A\u4F5C\u7528\u57DF "${scopeName}" \u4E2D\u6CA1\u6709\u7B26\u5408 ${opts.requiredVersion ?? "\u4EFB\u610F\u7248\u672C"} \u8981\u6C42\u7684\u7248\u672C\uFF0C\u4E14\u672A\u914D\u7F6E\u672C\u5730\u526F\u672C\u3002
579
+ \u4FEE\u6CD5\uFF1A\u5728\u5BBF\u4E3B\u6216\u8FDC\u7A0B\u6CE8\u518C\u8BE5\u4F9D\u8D56\uFF0C\u6216\u4E3A shared \u914D\u7F6E\u53EF\u7528\u7684\u672C\u5730\u526F\u672C\u3002`,
580
+ { shareKey, requiredVersion: opts.requiredVersion }
581
+ );
582
+ emitError({ remote: shareKey, error: err });
583
+ throw err;
584
+ }
537
585
  if (hooks.resolveShare) {
538
586
  const picked = await hooks.resolveShare({
539
587
  shareKey,
540
588
  shareScope: scopeName,
541
- requiredVersion: req,
589
+ requiredVersion: opts.requiredVersion,
542
590
  picked: entry,
543
591
  available: Object.values(byName)
544
592
  });
545
593
  if (picked) entry = picked;
546
594
  }
595
+ if (entry.value !== void 0) {
596
+ resolvedShareSnapshots.set(shareRequestKey(name, opts), entry);
597
+ return entry.value;
598
+ }
547
599
  entry.loaded = true;
548
- const instance = await entry.get();
549
- entry.value = instance;
550
- return instance;
600
+ if (!entry.pendingGet) {
601
+ try {
602
+ entry.pendingGet = entry.get();
603
+ } catch (e) {
604
+ entry.pendingGet = Promise.reject(e);
605
+ }
606
+ }
607
+ const pending = entry.pendingGet;
608
+ let instance;
609
+ try {
610
+ instance = await pending;
611
+ } catch (e) {
612
+ if (entry.value !== void 0) {
613
+ resolvedShareSnapshots.set(shareRequestKey(name, opts), entry);
614
+ return entry.value;
615
+ }
616
+ entry.loaded = false;
617
+ throw e;
618
+ } finally {
619
+ if (entry.pendingGet === pending) entry.pendingGet = void 0;
620
+ }
621
+ if (entry.value === void 0) entry.value = instance;
622
+ resolvedShareSnapshots.set(shareRequestKey(name, opts), entry);
623
+ return entry.value;
551
624
  }
552
625
  function getLoadedShare2(name, opts = {}) {
553
- const shareKey = opts.shareKey || name;
554
- const byName = getScope(opts.shareScope || "default")[shareKey] || {};
555
- const ready = Object.keys(byName).filter((v) => byName[v].value !== void 0);
556
- if (ready.length === 0) return void 0;
557
- return byName[ready.sort(compareVersions).pop()].value;
626
+ if (hooks.resolveShare) return resolvedShareSnapshots.get(shareRequestKey(name, opts))?.value;
627
+ return selectShareEntry(name, opts, true)?.value;
628
+ }
629
+ function pinLoadedShare2(name, opts, localVersion, instance) {
630
+ if (instance === void 0) return;
631
+ let entry;
632
+ try {
633
+ entry = selectShareEntry(name, opts);
634
+ } catch {
635
+ return;
636
+ }
637
+ if (!entry || entry.value !== void 0 || entry.version !== localVersion) return;
638
+ entry.value = instance;
639
+ entry.loaded = true;
558
640
  }
559
641
  function sanitizeUrl(u) {
560
642
  try {
@@ -986,6 +1068,7 @@ ${hints.join("\n")}`,
986
1068
  registerPlugins: registerPlugins2,
987
1069
  loadShare: loadShare2,
988
1070
  getLoadedShare: getLoadedShare2,
1071
+ pinLoadedShare: pinLoadedShare2,
989
1072
  loadRemote: loadRemote2,
990
1073
  getContainer: getContainer2,
991
1074
  preloadRemote: preloadRemote2,
@@ -1007,6 +1090,7 @@ var registerRemote = runtime.registerRemote;
1007
1090
  var registerPlugins = runtime.registerPlugins;
1008
1091
  var loadShare = runtime.loadShare;
1009
1092
  var getLoadedShare = runtime.getLoadedShare;
1093
+ var pinLoadedShare = runtime.pinLoadedShare;
1010
1094
  var loadRemote = runtime.loadRemote;
1011
1095
  var getContainer = runtime.getContainer;
1012
1096
  var preloadRemote = runtime.preloadRemote;
@@ -1054,7 +1138,15 @@ var BridgeErrorCodes = {
1054
1138
  /** 桥接准备或生命周期失败(details.phase = getContext | mount | unmount) */
1055
1139
  BRIDGE_LIFECYCLE_FAILED: "MFU-016",
1056
1140
  /** 桥接会话参数与 AppContext 不一致(受控 sessionKey 与全局会话矛盾) */
1057
- 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"
1058
1150
  };
1059
1151
  function invalidBridgeContractError(spec, mod) {
1060
1152
  const actual = mod === null ? "null" : typeof mod;
@@ -1111,6 +1203,42 @@ function invalidSessionKeyError(spec, value) {
1111
1203
  { spec, value: String(value) }
1112
1204
  );
1113
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
+ }
1114
1242
 
1115
1243
  // src/bridge-core.ts
1116
1244
  function assertBridgeContract(spec, mod) {
@@ -1215,6 +1343,261 @@ function withBridgeTimeout(p, ms, spec) {
1215
1343
  });
1216
1344
  }
1217
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
+
1218
1601
  export {
1219
1602
  loadRemote,
1220
1603
  provideAppContext,
@@ -1225,5 +1608,7 @@ export {
1225
1608
  resolveBridgeContext,
1226
1609
  assertControlledSessionKey,
1227
1610
  assertBridgeOptions,
1228
- withBridgeTimeout
1611
+ withBridgeTimeout,
1612
+ assertBridgeRoutingProtocol,
1613
+ RoutingChannel
1229
1614
  };
@@ -1,17 +1,24 @@
1
1
  import {
2
+ RoutingChannel,
2
3
  acquireBridgeSession,
3
4
  assertBridgeContract,
4
5
  assertBridgeOptions,
6
+ assertBridgeRoutingProtocol,
5
7
  assertControlledSessionKey,
6
8
  bridgeLifecycleError,
7
9
  provideAppContext,
8
10
  releaseBridgeSession,
9
11
  resolveBridgeContext,
10
12
  withBridgeTimeout
11
- } from "./chunk-TOT3P7AK.js";
13
+ } from "./chunk-BTJGYDHT.js";
12
14
 
13
15
  // src/bridge-host-vue.ts
14
- import { defineComponent, h, onBeforeUnmount, onMounted, ref, shallowRef, toRaw, watch } from "vue";
16
+ import { defineComponent, h, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, shallowRef, toRaw, watch } from "vue";
17
+ var assertBridgeRoutingProtocolSpec = (spec, contract) => assertBridgeRoutingProtocol(spec, contract);
18
+ function routingKey(routing) {
19
+ if (!routing) return void 0;
20
+ return routing.basePath + "|" + String(routing.navigation && typeof routing.navigation.navigate === "function");
21
+ }
15
22
  var ERROR_STYLE = {
16
23
  padding: "16px",
17
24
  border: "1px solid #fde2e2",
@@ -89,7 +96,9 @@ function createVueBridgeAppWithLoader(loadRemote) {
89
96
  props: {
90
97
  appProps: { type: Object, required: false, default: void 0 },
91
98
  // 桥接控制参数:undefined(不启用受控会话)/ null(登出态)/ 非空字符串(登录代次)
92
- sessionKey: { type: null, required: false, default: void 0 }
99
+ sessionKey: { type: null, required: false, default: void 0 },
100
+ // URL 同步控制通道(独立于 appProps/Context;省略 = 原 memory 模式)
101
+ routing: { type: Object, required: false, default: void 0 }
93
102
  },
94
103
  setup(props) {
95
104
  const status = ref("idle");
@@ -99,6 +108,8 @@ function createVueBridgeAppWithLoader(loadRemote) {
99
108
  let contract;
100
109
  let containerBlocked = false;
101
110
  let acquiredSession;
111
+ let channel;
112
+ let genAbort;
102
113
  const releaseAcquired = () => {
103
114
  if (acquiredSession !== void 0) {
104
115
  releaseBridgeSession(acquiredSession);
@@ -110,6 +121,10 @@ function createVueBridgeAppWithLoader(loadRemote) {
110
121
  generation++;
111
122
  const el = container.value;
112
123
  releaseAcquired();
124
+ genAbort?.abort();
125
+ genAbort = void 0;
126
+ channel?.dispose();
127
+ channel = void 0;
113
128
  if (contract && el) {
114
129
  const c = contract;
115
130
  contract = void 0;
@@ -151,7 +166,18 @@ function createVueBridgeAppWithLoader(loadRemote) {
151
166
  if (myGeneration !== generation) return;
152
167
  contract = assertBridgeContract(spec, mod);
153
168
  const appPropsSource = props.appProps ? toRaw(props.appProps) : void 0;
154
- await contract.mount(el, { ...appPropsSource ?? {} });
169
+ genAbort = new AbortController();
170
+ let mountOptions = { signal: genAbort.signal };
171
+ if (props.routing) {
172
+ assertBridgeRoutingProtocolSpec(spec, contract);
173
+ const navigation = props.routing.navigation;
174
+ const latestLoc = navigation.getLocation();
175
+ if (myGeneration !== generation) return;
176
+ channel = new RoutingChannel(`bridge-vue:${spec}:${myGeneration}`, props.routing.basePath, navigation, spec);
177
+ mountOptions = { signal: genAbort.signal, routing: channel };
178
+ void latestLoc;
179
+ }
180
+ await contract.mount(el, { ...appPropsSource ?? {} }, mountOptions);
155
181
  if (myGeneration !== generation) return;
156
182
  status.value = "ready";
157
183
  } catch (e) {
@@ -176,6 +202,15 @@ function createVueBridgeAppWithLoader(loadRemote) {
176
202
  watch(() => props.sessionKey, () => {
177
203
  start();
178
204
  });
205
+ watch(() => routingKey(props.routing), (after, before) => {
206
+ if (after !== before) start();
207
+ });
208
+ onDeactivated(() => {
209
+ channel?.setActive(false);
210
+ });
211
+ onActivated(() => {
212
+ channel?.setActive(true);
213
+ });
179
214
  onBeforeUnmount(() => {
180
215
  invalidate();
181
216
  });