@fulgurjs/federation 5.4.0 → 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 (44) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.en.md +5 -3
  3. package/README.md +14 -9
  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 +18 -41
  30. package/dist/bridge-router-react.js +213 -36
  31. package/dist/bridge-router-vue.d.ts +11 -36
  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 +30 -10
  42. package/dist/index.js +30 -10
  43. package/dist/runtime.js +9 -9
  44. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.4.1
4
+
5
+ - 修复 URL 同步两端适配器将 replace 误报为 push、子应用 go/back/forward 未委托宿主历史,以及连续导航被丢弃的问题。
6
+ - React 宿主等待真实 blocker 的 reset/proceed 与提交位置;canNavigate 改为可选预判。Vue 初始 ready 和导航执行异常不再吞掉,MFU-033 保留 cause,队列失败后可继续导航。
7
+ - 修复 Vite 5/6 配套 plugin-react 4 的跨框架 dev preamble 标志缺失,避免 Vue 宿主加载 React 远程失败;子目录部署示例的 blocker 按逻辑路径判断。
8
+ - 修复 React 18 + RR6 生产渲染的跨共享键 CJS 双实例:提供闭包只豁免自引用,react-dom → react 走同步协商;入口 init 前的同步副本复用现有 fallback 回写规则登记,避免 renderer 与组件使用不同 React。
9
+ - 通道离页、暂停与卸载会作废在飞和排队请求;自定义宿主 navigate 可接收 AbortSignal;子应用接线可传 ctx.signal 自动清理。Vue history base 不再二次剥离;前缀与目标拒绝编码逃逸。
10
+
3
11
  ## 5.4.0
4
12
 
5
13
  - **新增:跨框架桥接 URL 同步(`/bridge/router/*`)**——宿主 URL 表达子应用内部位置:首次深链直达(不闪默认页、不发错接口)、刷新/收藏/新窗口恢复、子应用 Link/RouterLink/router.push 与宿主菜单/前进后退全量同步、根重定向以 replace 规范化(不凭空制造历史)。架构:宿主 Router 是浏览器历史唯一写入方,子应用用受控 memory 路由(Vue `connectVueBridgeRouter` / React `createReactBridgeRouter`,Link/useNavigate/RouterLink 全兼容);独立路由通道承载位置与仲裁(请求编号/代次隔离/外部作废/取消恢复),path/search/hash 三段原样保留(重复键/编码/中文/片段不二次转换),同实例路径变化**不重挂 root、不重建 store、不重载远程**。
package/README.en.md CHANGED
@@ -343,13 +343,15 @@ The bridge defaults to memory routing: internal navigation does not touch the br
343
343
 
344
344
  **Architecture**: the host router is the only writer of browser history; the sub-app uses a controlled memory router; both sides communicate over a dedicated routing channel (not appProps/Context); path/search/hash changes within an instance **do not remount the root, do not rebuild stores, do not reload the remote**.
345
345
 
346
- **Host (Vue Router 4, history or hash mode)**: declare a suffix route (`/approval/:pathMatch(.*)*` — without it detail navigation unmounts the sub-app), add real guards (`beforeEach` rejecting → the channel receives `cancelled`, URL/history/sub-app position unchanged), then `createVueBridgeNavigation(router, { routerBase })` and pass `routing={{ basePath: '/approval', navigation }}` to the bridge component.
346
+ **Host (Vue Router 4, history or hash mode)**: declare a suffix route (`/approval/:pathMatch(.*)*` — without it detail navigation unmounts the sub-app), add real guards (`beforeEach` rejecting → the channel receives `cancelled`, URL/history/sub-app position unchanged), then `createVueBridgeNavigation(router)` (Vue Router already removes its history base) and pass `routing={{ basePath: '/approval', navigation }}` to the bridge component.
347
347
 
348
348
  **Sub-app**: declare the protocol and wire a controlled router —
349
349
  Vue: `defineBridgeApp(async (props, ctx) => { const router = createRouter({ history: createMemoryHistory(), routes }); await connectVueBridgeRouter(ctx.routing!, router).ready; ... app.use(router); return app }, { routing: true })` (await ready BEFORE `app.use(router)` — the install-time initial navigation would otherwise override the deep-link location).
350
350
  React: `createReactBridgeRouter(ctx.routing!, routes).element` — `createMemoryRouter`-based; `Link`/`useNavigate` work unmodified.
351
351
 
352
- **Host (React Router)**: data routers only (`createBrowserRouter`/`createHashRouter` + `RouterProvider`); `createReactBridgeNavigation(router, { basename, canNavigate })`. Pass the same predicate to `canNavigate` and to an in-tree `useBlocker` — sub-app-originated navigations are cancelled by the port pre-check (zero URL/history side effects), menu/POP sources are blocked by the real blocker. Declarative `BrowserRouter` has no cancellation semantics and is not supported. Requires react-router ≥ 6.11.
352
+ **Host (React Router)**: data routers only (`createBrowserRouter`/`createHashRouter` + `RouterProvider`); `createReactBridgeNavigation(router, { basename, canNavigate })`. `canNavigate` is an optional early rejection policy. The adapter also observes the real blocker: it waits for `reset()` (cancelled) or a committed navigation after `proceed()`. Resolving `router.navigate()` alone does not imply a commit. Declarative `BrowserRouter` has no cancellation semantics and is not supported. Requires react-router ≥ 6.11.
353
+
354
+ **Lifecycle and navigation**: both child connectors accept an optional third argument `{ signal?: AbortSignal }`; pass `ctx.signal` to dispose on session invalidation, or call `connection.dispose()` yourself. Push and replace retain their history action; numeric navigation delegates to the host history. Concurrent requests are serialized and superseded requests are invalidated. Navigation errors reject with MFU-033 and preserve `cause`; they are not reported as cancellation. Custom host ports receive an optional third argument `{ signal }` and must check it before asynchronous commits.
353
355
 
354
356
  **Contract highlights**: `basePath` is a static absolute path from the host-router perspective (segment-matched; conflicting/overlapping prefixes rejected, `MFU-030`); location is compared and preserved as three raw strings (duplicate query keys, encoding, fragments survive without re-encoding); cancellation never auto-retries; session switch (`sessionKey→null`) invalidates the old channel — late navigations are rejected and never write the URL; KeepAlive-cached instances pause routing writes; enabling sync against a contract without `{ routing: true }` shows `MFU-031` instead of silently falling back to memory; escaping targets and illegal `go` arguments → `MFU-032`; redirect loops beyond 5 internal replaces → `MFU-033` with the chain attached. Router libraries are optional peers consumed only through the two opt-in entries (`/bridge/router/vue`, `/bridge/router/react`, each gated ≤ 4096B gzip); the default entries never load a router library. Not promised: SSR/RSC, cross-window, nested multi-level bridge routing proxies, TanStack Router and other libraries (extend via the `BridgeHostNavigation`/`BridgeChildRoute` ports).
355
357
 
@@ -420,7 +422,7 @@ Lazy-loading measurement layers: ① nothing until first render of a remote comp
420
422
  | MFU | `MFU-030` | Bridge URL-sync config invalid / prefix conflict (illegal basePath: empty, root, query/hash/wildcard; overlapping active prefixes) |
421
423
  | MFU | `MFU-031` | Bridge routing protocol missing / channel destroyed (sub-app not declared with `{ routing: true }`; disposed channel reused) |
422
424
  | MFU | `MFU-032` | Bridge illegal navigation (target escaping its own prefix, illegal `go` argument, request on a dead channel) |
423
- | MFU | `MFU-033` | Bridge routing sync failed (internal replace chain exceeds the bound of 5, chain attached; no silent fallback to memory) |
425
+ | MFU | `MFU-033` | Bridge routing preparation/sync failed (redirect limit or navigation exception, chain/cause attached; no silent fallback to memory) |
424
426
  | CC | `CC-001` | AppContext required key missing (got/expected/example) |
425
427
  | | `CC-002` | runtime singleton unavailable (standalone remote page) |
426
428
 
package/README.md CHANGED
@@ -663,7 +663,7 @@ CLI 解析同一份配置值;dev/prod 的 URL 选择规则与 `federation({ re
663
663
  | | `MFU-030` | 桥接路由同步配置/前缀冲突(basePath 非法:空/根/带 query·hash·通配、同页重叠前缀登记) |
664
664
  | | `MFU-031` | 桥接路由协议缺失/通道失效(子应用未以 `{ routing: true }` 声明协议、通道销毁后复用) |
665
665
  | | `MFU-032` | 桥接非法导航(子应用导航目标越界自身前缀、`go` 参数非法、失效通道的请求被拒绝) |
666
- | | `MFU-033` | 桥接路由同步失败(重定向链超出上限,附目标链;不静默回退 memory) |
666
+ | | `MFU-033` | 桥接路由准备/同步失败(重定向超限或导航异常,附目标链/cause;不静默回退 memory) |
667
667
  | CC 跨应用上下文 | `CC-001` | AppContext 必需字段缺失(三段式:got/expected/example,修法指向宿主桥 `provideAppContext`) |
668
668
  | | `CC-002` | 运行时单例不可用(独立直开远程页;修法 = 经宿主联邦加载,时序契约 bridge → 远程 setup → 页面模块) |
669
669
 
@@ -903,7 +903,7 @@ router.beforeEach((to) => (to.path.startsWith('/approval/secret') ? false : unde
903
903
  <script setup lang="ts">
904
904
  import { createVueBridgeApp } from '@fulgurjs/federation/bridge/vue'
905
905
  import { createVueBridgeNavigation, type BridgeHostRouting } from '@fulgurjs/federation/bridge/router/vue'
906
- const navigation = createVueBridgeNavigation(router) // routerBase 用于 createWebHistory(base) 场景
906
+ const navigation = createVueBridgeNavigation(router) // Vue Router fullPath 已是逻辑路径,无需传部署 base
907
907
  const routing: BridgeHostRouting = { basePath: '/approval', navigation }
908
908
  const RemoteApp = createVueBridgeApp('remote/bridge-routed', { /* 同 8.2 */ })
909
909
  </script>
@@ -920,11 +920,12 @@ import { createRouter, createMemoryHistory, RouterView } from 'vue-router'
920
920
  import { defineBridgeApp } from '@fulgurjs/federation/runtime'
921
921
  import { connectVueBridgeRouter } from '@fulgurjs/federation/bridge/router/vue'
922
922
  export default defineBridgeApp(async (props, ctx) => {
923
+ if (!ctx?.routing) throw new Error('本契约需要宿主启用 URL 同步')
923
924
  const router = createRouter({ history: createMemoryHistory(), routes: [
924
925
  { path: '/list', component: List },
925
926
  { path: '/detail/:id', component: Detail },
926
927
  ] })
927
- await connectVueBridgeRouter(ctx.routing!, router).ready // 初始 push 落定后再 install(顺序不能反)
928
+ await connectVueBridgeRouter(ctx.routing!, router, { signal: ctx.signal }).ready // 初始 push 落定后再 install(顺序不能反)
928
929
  const app = createApp({ setup: () => () => h(RouterView) }, props)
929
930
  app.use(router)
930
931
  return app
@@ -934,21 +935,25 @@ export default defineBridgeApp(async (props, ctx) => {
934
935
  ```tsx
935
936
  // bridge.tsx(React 子应用):createReactBridgeRouter 返回 RouterProvider 元素
936
937
  import { createReactBridgeRouter } from '@fulgurjs/federation/bridge/router/react'
937
- export default defineBridgeApp((_props, ctx) =>
938
- createReactBridgeRouter(ctx.routing!, [
938
+ import { defineBridgeApp } from '@fulgurjs/federation/react'
939
+ export default defineBridgeApp((_props, ctx) => {
940
+ if (!ctx?.routing) throw new Error('本契约需要宿主启用 URL 同步')
941
+ return createReactBridgeRouter(ctx.routing, [
939
942
  { path: '/list', element: <List /> },
940
943
  { path: '/detail/:id', element: <Detail /> },
941
- ]).element, { routing: true })
944
+ ], { signal: ctx.signal }).element
945
+ }, { routing: true })
942
946
  ```
943
947
 
944
- 宿主端 React Router 仅支持 **data router 模式**(`createBrowserRouter` / `createHashRouter` + `RouterProvider`):`createReactBridgeNavigation(router, { basename, canNavigate })`。`canNavigate` 与树内 `useBlocker` 传同一谓词——子应用发起的导航被拒绝时由端口预判返回 cancelled(URL/历史零副作用),菜单/POP 等其他来源由 useBlocker 真实拦截。declarative 模式(BrowserRouter)无取消语义,不支持。React Router 要求 ≥ 6.11(createMemoryRouter)。
948
+ 宿主端 React Router 仅支持 **data router 模式**(`createBrowserRouter` / `createHashRouter` + `RouterProvider`):`createReactBridgeNavigation(router, { basename, canNavigate })`。`canNavigate` 可选,仅作提前拒绝;端口观察真实 `useBlocker` 状态,等待 `reset()` 返回 cancelled、`proceed()` 后实际位置提交返回 committed,不能只凭 navigate 的 Promise 落定判成功。declarative 模式(BrowserRouter)无取消语义,不支持。React Router 要求 ≥ 6.11(createMemoryRouter)。
945
949
 
946
950
  **③ 行为契约与边界**:
947
951
 
948
952
  - **basePath**:宿主路由视角的静态绝对路径(拒绝空/根/带 query·hash·通配符,`MFU-030`);按路径段匹配(`/approval` 命中 `/approval/detail/1`,不命中 `/approval-old`);同页各同步实例前缀不得相同或重叠。`/approval` 对应子应用 `/`;根重定向由子应用路由定义、以 replace 规范化(不凭空多一条历史)。
949
- - **Vite base 与路由分层**:部署在 `/erp/` 时 Vite base/宿主 Router base 是 `/erp/`,bridge basePath 仍是 `/approval`(Vue 传 `routerBase`、React 传 `basename`);适配器输出的逻辑路径不含部署前缀,不会拼出 `/erp/erp/...`。子目录部署 + hash 模式内层片段见 e2e fixtures(`fixtures/host-bridge-*/`,可运行参考实现)。
953
+ - **Vite base 与路由分层**:部署在 `/erp/` 时 Vite base/宿主 Router base 是 `/erp/`,bridge basePath 仍是 `/approval`(Vue Router 已自动剥离 history base,React 端口传 `basename`);适配器输出的逻辑路径不含部署前缀,不会拼出 `/erp/erp/...`。子目录部署 + hash 模式内层片段见 e2e fixtures(`fixtures/host-bridge-*/`,可运行参考实现)。
950
954
  - **位置三段全等**:search/hash 原样保留(重复 query 键、编码、中文、片段不二次 decode/encode);仅参数变化也同步,且不重挂。
951
- - **取消语义**:Vue Router 4 的 push/replace 落定 `NavigationFailure` 即真实取消;React Router data router 用 canNavigate 预判(与 useBlocker 同谓词)+ 树内 blocker。取消后 URL、历史、子应用位置保持最后确认状态,**绝不自动重试**(`router.push` 的函数返回不冒充提交成功)。
955
+ - **取消语义**:Vue Router 4 的 push/replace 落定 `NavigationFailure` 即真实取消;React Router data router 等待真实 blocker 的取消/放行,`canNavigate` 仅为可选提前拒绝。取消后 URL、历史、子应用位置保持最后确认状态,**绝不自动重试**(`router.push` 的函数返回不冒充提交成功)。
956
+ - **导航与错误**:子应用 push/replace 保留原动作,go/back/forward 委托宿主历史;连续请求串行落定,外部导航作废旧的在飞与排队请求。守卫/加载器/端口执行异常拒绝 Promise(MFU-033,保留 cause),不会伪装 cancelled。两端子路由接线的第三参数 `{ signal?: AbortSignal }` 默认为空;推荐传 `ctx.signal` 自动 dispose,未传时由子应用显式调用连接的 `dispose()`。自定义 `BridgeHostNavigation.navigate(target, action, { signal })` 应在异步提交前复核可选 signal,已 aborted 时禁止迟到写入。
952
957
  - **会话与生命周期**:换账号/登出(`sessionKey→null`)作废旧通道——旧通道的导航一律 cancelled、不写 URL、不复活子应用;unmount 后迟到通知失效(通道销毁,再订阅得 `MFU-031`)。KeepAlive 缓存离页的实例暂停路由写入(不抢占 URL、不销毁通道,激活重同步)。同一容器 unmount 抛错的持久封锁(BN09)不因路由绕过。
953
958
  - **协议校验**:宿主启用 routing 而子应用未声明 `{ routing: true }`(契约 `routing: { protocol: 1 }`)→ `MFU-031` 占位,**不静默退回 memory 假装深链成功**。
954
959
  - **非法导航与循环**:目标越界自身前缀(`../`、跨前缀)、非法 `go` 参数 → `MFU-032`;连续内部 replace 超过 5 次(重定向环)→ `MFU-033`(附目标链,不静默回入口)。
@@ -1,6 +1,6 @@
1
1
  import { ReactElement } from 'react';
2
2
  import { BridgeApp } from './bridge-core.cjs';
3
- import { B as BridgeChildRoute } from './bridge-router-core-BxBx-reP.cjs';
3
+ import { B as BridgeChildRoute } from './bridge-router-core-BRZQYDPm.cjs';
4
4
  import './context.cjs';
5
5
 
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import { ReactElement } from 'react';
2
2
  import { BridgeApp } from './bridge-core.js';
3
- import { B as BridgeChildRoute } from './bridge-router-core-BxBx-reP.js';
3
+ import { B as BridgeChildRoute } from './bridge-router-core-BRZQYDPm.js';
4
4
  import './context.js';
5
5
 
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  bridgeLifecycleError
3
- } from "./chunk-JQF7KYZF.js";
3
+ } from "./chunk-HZGQOW35.js";
4
4
  import "./chunk-OF2JCXTZ.js";
5
5
 
6
6
  // src/bridge-app-react.ts
@@ -1,6 +1,6 @@
1
1
  import { App } from 'vue';
2
2
  import { BridgeApp } from './bridge-core.cjs';
3
- import { B as BridgeChildRoute } from './bridge-router-core-BxBx-reP.cjs';
3
+ import { B as BridgeChildRoute } from './bridge-router-core-BRZQYDPm.cjs';
4
4
  import './context.cjs';
5
5
 
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import { App } from 'vue';
2
2
  import { BridgeApp } from './bridge-core.js';
3
- import { B as BridgeChildRoute } from './bridge-router-core-BxBx-reP.js';
3
+ import { B as BridgeChildRoute } from './bridge-router-core-BRZQYDPm.js';
4
4
  import './context.js';
5
5
 
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  bridgeLifecycleError
3
- } from "./chunk-JQF7KYZF.js";
3
+ } from "./chunk-HZGQOW35.js";
4
4
  import "./chunk-OF2JCXTZ.js";
5
5
 
6
6
  // src/bridge-app-vue.ts
@@ -15,7 +15,9 @@ type BridgeNavigationResult = {
15
15
  interface BridgeHostNavigation {
16
16
  getLocation(): BridgeLocation;
17
17
  subscribe(listener: (location: BridgeLocation) => void): () => void;
18
- navigate(target: BridgeLocation, action: 'push' | 'replace'): Promise<BridgeNavigationResult>;
18
+ navigate(target: BridgeLocation, action: 'push' | 'replace', options?: {
19
+ signal?: AbortSignal;
20
+ }): Promise<BridgeNavigationResult>;
19
21
  go(delta: number): void;
20
22
  }
21
23
  /** 子应用路由通道(桥接宿主按挂载代次创建,随 mount 第三参数交给子应用) */
@@ -1,5 +1,5 @@
1
1
  import { AppContext } from './context.cjs';
2
- import { a as BridgeMountOptions, b as BridgeRoutingProtocol } from './bridge-router-core-BxBx-reP.cjs';
2
+ import { a as BridgeMountOptions, b as BridgeRoutingProtocol } from './bridge-router-core-BRZQYDPm.cjs';
3
3
 
4
4
  /** 子应用桥接契约(远程 ./bridge 模块的默认导出;任务书 §3.2) */
5
5
  interface BridgeApp {
@@ -1,5 +1,5 @@
1
1
  import { AppContext } from './context.js';
2
- import { a as BridgeMountOptions, b as BridgeRoutingProtocol } from './bridge-router-core-BxBx-reP.js';
2
+ import { a as BridgeMountOptions, b as BridgeRoutingProtocol } from './bridge-router-core-BRZQYDPm.js';
3
3
 
4
4
  /** 子应用桥接契约(远程 ./bridge 模块的默认导出;任务书 §3.2) */
5
5
  interface BridgeApp {
@@ -6,8 +6,8 @@ import {
6
6
  releaseBridgeSession,
7
7
  resolveBridgeContext,
8
8
  withBridgeTimeout
9
- } from "./chunk-PRUYHYX2.js";
10
- import "./chunk-JQF7KYZF.js";
9
+ } from "./chunk-RXXBTPAY.js";
10
+ import "./chunk-HZGQOW35.js";
11
11
  import "./chunk-TSYLDY6H.js";
12
12
  import "./chunk-OF2JCXTZ.js";
13
13
  export {
@@ -143,13 +143,14 @@ function routingNavigationError(spec, reason) {
143
143
  { spec }
144
144
  );
145
145
  }
146
- function routingSyncError(spec, reason, chain) {
146
+ function routingSyncError(spec, reason, chain, cause) {
147
147
  return new FgError(
148
148
  BridgeErrorCodes.ROUTING_SYNC_FAILED,
149
- `\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
149
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u5931\u8D25\u3002
150
150
  \u539F\u56E0\uFF1A${reason}
151
- \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`,
152
- { spec, chain: [...chain] }
151
+ \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`,
152
+ { spec, chain: [...chain] },
153
+ { cause }
153
154
  );
154
155
  }
155
156
  // Annotate the CommonJS export names for ESM import in node:
@@ -47,6 +47,6 @@ declare function routingProtocolError(spec: string, reason: string): FgError;
47
47
  /** MFU-032:非法导航(越界目标、非法 go 参数、失效通道请求被拒绝) */
48
48
  declare function routingNavigationError(spec: string, reason: string): FgError;
49
49
  /** MFU-033:路由准备/同步失败(重定向循环等;不伪装成功也不静默回退 memory) */
50
- declare function routingSyncError(spec: string, reason: string, chain: string[]): FgError;
50
+ declare function routingSyncError(spec: string, reason: string, chain: string[], cause?: unknown): FgError;
51
51
 
52
52
  export { BridgeErrorCodes, type BridgePhase, bridgeLifecycleError, bridgeSessionMismatchError, invalidBridgeContractError, invalidSessionKeyError, routingConfigError, routingNavigationError, routingProtocolError, routingSyncError };
@@ -47,6 +47,6 @@ declare function routingProtocolError(spec: string, reason: string): FgError;
47
47
  /** MFU-032:非法导航(越界目标、非法 go 参数、失效通道请求被拒绝) */
48
48
  declare function routingNavigationError(spec: string, reason: string): FgError;
49
49
  /** MFU-033:路由准备/同步失败(重定向循环等;不伪装成功也不静默回退 memory) */
50
- declare function routingSyncError(spec: string, reason: string, chain: string[]): FgError;
50
+ declare function routingSyncError(spec: string, reason: string, chain: string[], cause?: unknown): FgError;
51
51
 
52
52
  export { BridgeErrorCodes, type BridgePhase, bridgeLifecycleError, bridgeSessionMismatchError, invalidBridgeContractError, invalidSessionKeyError, routingConfigError, routingNavigationError, routingProtocolError, routingSyncError };
@@ -8,7 +8,7 @@ import {
8
8
  routingNavigationError,
9
9
  routingProtocolError,
10
10
  routingSyncError
11
- } from "./chunk-JQF7KYZF.js";
11
+ } from "./chunk-HZGQOW35.js";
12
12
  import "./chunk-OF2JCXTZ.js";
13
13
  export {
14
14
  BridgeErrorCodes,
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { A as AppContext } from './bridge-core-BOWvVUxO.js';
2
+ import { A as AppContext } from './bridge-core-ejlY9ddN.js';
3
3
 
4
4
  /**
5
5
  * React 宿主桥接工厂(/bridge、/bridge/react 导出 createReactBridgeApp;任务书 §3.4)。
@@ -169,13 +169,14 @@ function routingNavigationError(spec, reason) {
169
169
  { spec }
170
170
  );
171
171
  }
172
- function routingSyncError(spec, reason, chain) {
172
+ function routingSyncError(spec, reason, chain, cause) {
173
173
  return new FgError(
174
174
  BridgeErrorCodes.ROUTING_SYNC_FAILED,
175
- `\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
175
+ `\u73B0\u8C61\uFF1A\u6865\u63A5\u5E94\u7528 "${spec}" \u7684 URL \u540C\u6B65\u5931\u8D25\u3002
176
176
  \u539F\u56E0\uFF1A${reason}
177
- \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`,
178
- { spec, chain: [...chain] }
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 }
179
180
  );
180
181
  }
181
182
 
@@ -288,10 +289,11 @@ var MAX_GO_DELTA = 50;
288
289
  function normalizeBasePath(basePath, spec) {
289
290
  const bad = (reason) => routingConfigError(spec, reason);
290
291
  if (typeof basePath !== "string" || basePath === "") throw bad("basePath \u4E0D\u80FD\u4E3A\u7A7A\u3002");
291
- 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');
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');
292
293
  if (!basePath.startsWith("/")) throw bad(`basePath \u5FC5\u987B\u4EE5 "/" \u5F00\u5934\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002`);
293
294
  if (/[?#]/.test(basePath) || basePath.includes("://")) throw bad(`basePath \u4E0D\u80FD\u5305\u542B query/hash/\u534F\u8BAE\uFF1A\u5F53\u524D\u4E3A "${basePath}"\u3002`);
294
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");
295
297
  return basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
296
298
  }
297
299
  function matchesBasePath(hostPathname, basePath) {
@@ -315,7 +317,8 @@ function sameLocation(a, b) {
315
317
  return a.pathname === b.pathname && a.search === b.search && a.hash === b.hash;
316
318
  }
317
319
  function assertChildTargetInScope(target, basePath, spec) {
318
- if (target.pathname.includes("\0") || target.pathname.split("/").includes("..")) {
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 === "..")) {
319
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`);
320
323
  }
321
324
  const host = toHostLocation(target, basePath, spec);
@@ -357,6 +360,8 @@ var RoutingChannel = class {
357
360
  lastConfirmed;
358
361
  seq = 0;
359
362
  inflight = null;
363
+ queue;
364
+ generation = 0;
360
365
  disposed = false;
361
366
  active = true;
362
367
  releasePrefix;
@@ -371,7 +376,8 @@ var RoutingChannel = class {
371
376
  this.spec = spec;
372
377
  this.basePath = normalizeBasePath(basePath, spec);
373
378
  this.host = host;
374
- this.current = toChildLocation(host.getLocation(), this.basePath);
379
+ const initial = host.getLocation();
380
+ this.current = matchesBasePath(initial.pathname, this.basePath) ? toChildLocation(initial, this.basePath) : { pathname: "/", search: "", hash: "" };
375
381
  this.lastConfirmed = { ...this.current };
376
382
  this.releasePrefix = acquireRoutingPrefix(this.basePath, spec);
377
383
  this.unsubHost = host.subscribe((loc) => this.handleHostLocation(loc));
@@ -389,6 +395,7 @@ var RoutingChannel = class {
389
395
  handleHostLocation(hostLoc) {
390
396
  if (this.disposed) return;
391
397
  if (!matchesBasePath(hostLoc.pathname, this.basePath)) {
398
+ this.cancelPending();
392
399
  this.onEvent?.({ type: "prefix-miss", pathname: hostLoc.pathname });
393
400
  return;
394
401
  }
@@ -398,23 +405,21 @@ var RoutingChannel = class {
398
405
  }
399
406
  const child = toChildLocation(hostLoc, this.basePath);
400
407
  if (this.inflight && sameLocation(child, this.inflight.target)) {
401
- const { seq, resolve } = this.inflight;
402
- this.inflight = null;
408
+ const { seq } = this.inflight;
403
409
  this.lastConfirmed = child;
404
410
  this.emit(child);
405
411
  this.onEvent?.({ type: "request-confirmed", seq });
406
- resolve({ status: "committed", location: { ...child } });
407
412
  return;
408
413
  }
409
414
  if (this.inflight) {
410
- const { seq, resolve } = this.inflight;
411
- this.inflight = null;
415
+ const seq = this.inflight.seq;
412
416
  this.lastConfirmed = child;
417
+ this.cancelPending();
413
418
  this.emit(child);
414
419
  this.onEvent?.({ type: "request-superseded", seq });
415
- resolve({ status: "cancelled", location: { ...child } });
416
420
  return;
417
421
  }
422
+ if (!sameLocation(child, this.lastConfirmed)) this.generation++;
418
423
  this.lastConfirmed = child;
419
424
  this.emit(child);
420
425
  this.onEvent?.({ type: "host-broadcast" });
@@ -431,8 +436,31 @@ var RoutingChannel = class {
431
436
  this.listeners.delete(listener);
432
437
  };
433
438
  }
434
- async navigate(target, action) {
435
- if (this.disposed || !this.active) {
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)) {
436
464
  this.onEvent?.({ type: this.disposed ? "rejected-disposed" : "rejected-inactive" });
437
465
  return { status: "cancelled", location: { ...this.lastConfirmed } };
438
466
  }
@@ -455,28 +483,12 @@ var RoutingChannel = class {
455
483
  this.redirectChain = [];
456
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);
457
485
  }
458
- if (this.inflight) {
459
- const prev = this.inflight;
460
- await new Promise((resolve) => {
461
- const timer = setInterval(() => {
462
- if (this.inflight !== prev) {
463
- clearInterval(timer);
464
- resolve();
465
- }
466
- }, 0);
467
- });
468
- if (this.disposed || !this.active) {
469
- return { status: "cancelled", location: { ...this.lastConfirmed } };
470
- }
471
- if (sameLocation(target, this.lastConfirmed)) {
472
- return { status: "committed", location: { ...this.lastConfirmed } };
473
- }
474
- }
475
486
  const seq = ++this.seq;
476
487
  this.onEvent?.({ type: "request-start", seq, target: { ...target }, action });
477
- return new Promise((resolve) => {
478
- this.inflight = { seq, target: { ...target }, resolve };
479
- this.host.navigate(toHostLocation(target, this.basePath, this.spec), action).then((result) => {
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) => {
480
492
  if (!this.inflight || this.inflight.seq !== seq) return;
481
493
  if (this.disposed || !this.active) {
482
494
  this.inflight = null;
@@ -484,7 +496,7 @@ var RoutingChannel = class {
484
496
  return;
485
497
  }
486
498
  this.inflight = null;
487
- if (result.status === "committed") {
499
+ if (result.status === "committed" && matchesBasePath(result.location.pathname, this.basePath)) {
488
500
  const child = toChildLocation(result.location, this.basePath);
489
501
  this.lastConfirmed = child;
490
502
  this.emit(child);
@@ -493,21 +505,21 @@ var RoutingChannel = class {
493
505
  }
494
506
  this.onEvent?.({ type: "host-verdict", seq, status: result.status });
495
507
  resolve(
496
- result.status === "committed" ? { status: "committed", location: { ...this.lastConfirmed } } : { status: "cancelled", location: { ...this.lastConfirmed } }
508
+ result.status === "committed" && matchesBasePath(result.location.pathname, this.basePath) ? { status: "committed", location: { ...this.lastConfirmed } } : { status: "cancelled", location: { ...this.lastConfirmed } }
497
509
  );
498
- }).catch(() => {
510
+ }).catch((cause) => {
499
511
  if (this.inflight?.seq === seq) {
500
512
  this.inflight = null;
501
513
  this.onEvent?.({ type: "host-error", seq });
502
514
  this.emit({ ...this.lastConfirmed });
503
- resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
515
+ reject(routingSyncError(this.spec, `\u5BBF\u4E3B\u5BFC\u822A\u6267\u884C\u5931\u8D25\uFF1A${cause instanceof Error ? cause.message : String(cause)}`, [], cause));
504
516
  }
505
517
  });
506
518
  });
507
519
  }
508
520
  /** 委托宿主历史(有限整数);不创建第二条独立历史 */
509
521
  go(delta) {
510
- if (this.disposed || !this.active) return;
522
+ if (this.disposed || !this.active || !matchesBasePath(this.host.getLocation().pathname, this.basePath)) return;
511
523
  if (typeof delta !== "number" || !Number.isInteger(delta) || Math.abs(delta) > MAX_GO_DELTA) {
512
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`);
513
525
  }
@@ -516,6 +528,7 @@ var RoutingChannel = class {
516
528
  /** KeepAlive deactivate:暂停广播与写入;通道与订阅保留(activate 后 resync) */
517
529
  setActive(active) {
518
530
  if (this.disposed) return;
531
+ if (!active) this.cancelPending();
519
532
  this.active = active;
520
533
  this.onEvent?.({ type: active ? "resumed" : "paused" });
521
534
  if (active) this.handleHostLocation(this.host.getLocation());
@@ -527,11 +540,7 @@ var RoutingChannel = class {
527
540
  if (this.redirectTimer) clearTimeout(this.redirectTimer);
528
541
  this.unsubHost();
529
542
  this.releasePrefix();
530
- if (this.inflight) {
531
- const { resolve } = this.inflight;
532
- this.inflight = null;
533
- resolve({ status: "cancelled", location: { ...this.lastConfirmed } });
534
- }
543
+ this.cancelPending();
535
544
  this.listeners.clear();
536
545
  this.onEvent?.({ type: "disposed" });
537
546
  }
@@ -1,6 +1,6 @@
1
1
  import { ReactNode, ComponentType } from 'react';
2
2
  import { AppContext } from './context.cjs';
3
- import { c as BridgeHostRouting } from './bridge-router-core-BxBx-reP.cjs';
3
+ import { c as BridgeHostRouting } from './bridge-router-core-BRZQYDPm.cjs';
4
4
 
5
5
  /**
6
6
  * React 宿主桥接工厂(/bridge、/bridge/react 导出 createReactBridgeApp;任务书 §3.4)。
@@ -1,6 +1,6 @@
1
1
  import { ReactNode, ComponentType } from 'react';
2
2
  import { AppContext } from './context.js';
3
- import { c as BridgeHostRouting } from './bridge-router-core-BxBx-reP.js';
3
+ import { c as BridgeHostRouting } from './bridge-router-core-BRZQYDPm.js';
4
4
 
5
5
  /**
6
6
  * React 宿主桥接工厂(/bridge、/bridge/react 导出 createReactBridgeApp;任务书 §3.4)。
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  RoutingChannel,
3
3
  assertBridgeRoutingProtocol
4
- } from "./chunk-WOTR2FUQ.js";
4
+ } from "./chunk-7CQOVLSI.js";
5
5
  import {
6
6
  acquireBridgeSession,
7
7
  assertBridgeContract,
@@ -10,10 +10,10 @@ import {
10
10
  releaseBridgeSession,
11
11
  resolveBridgeContext,
12
12
  withBridgeTimeout
13
- } from "./chunk-PRUYHYX2.js";
13
+ } from "./chunk-RXXBTPAY.js";
14
14
  import {
15
15
  bridgeLifecycleError
16
- } from "./chunk-JQF7KYZF.js";
16
+ } from "./chunk-HZGQOW35.js";
17
17
  import {
18
18
  provideAppContext
19
19
  } from "./chunk-TSYLDY6H.js";
@@ -1,5 +1,5 @@
1
1
  import { Component } from 'vue';
2
- import { A as AppContext } from './bridge-core-BOWvVUxO.js';
2
+ import { A as AppContext } from './bridge-core-ejlY9ddN.js';
3
3
 
4
4
  /**
5
5
  * Vue 宿主桥接工厂(/bridge、/bridge/vue 导出 createVueBridgeApp;任务书 §3.4)。