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