@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.
- package/CHANGELOG.md +8 -0
- package/README.en.md +5 -3
- package/README.md +14 -9
- 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 +18 -41
- package/dist/bridge-router-react.js +213 -36
- package/dist/bridge-router-vue.d.ts +11 -36
- 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 +30 -10
- package/dist/index.js +30 -10
- package/dist/runtime.js +9 -9
- package/package.json +1 -1
package/dist/bridge-host-vue.cjs
CHANGED
|
@@ -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\
|
|
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\
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
435
|
-
|
|
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
|
-
|
|
479
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/bridge-host-vue.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RoutingChannel,
|
|
3
3
|
assertBridgeRoutingProtocol
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
13
|
+
} from "./chunk-RXXBTPAY.js";
|
|
14
14
|
import {
|
|
15
15
|
bridgeLifecycleError
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-HZGQOW35.js";
|
|
17
17
|
import {
|
|
18
18
|
provideAppContext
|
|
19
19
|
} from "./chunk-TSYLDY6H.js";
|
package/dist/bridge-react.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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
|
|
|
7
7
|
/** React 宿主桥接工厂:返回 `ComponentType<{ appProps: P; sessionKey?: string | null }>` */
|
|
8
8
|
declare const createReactBridgeApp: <P = Record<string, unknown>>(spec: string, options?: ReactBridgeAppOptions) => react.ComponentType<{
|
|
@@ -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'
|
|
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 第三参数交给子应用) */
|
|
@@ -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'
|
|
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 第三参数交给子应用) */
|
|
@@ -18,7 +18,9 @@ type BridgeNavigationResult = {
|
|
|
18
18
|
interface BridgeHostNavigation {
|
|
19
19
|
getLocation(): BridgeLocation;
|
|
20
20
|
subscribe(listener: (location: BridgeLocation) => void): () => void;
|
|
21
|
-
navigate(target: BridgeLocation, action: 'push' | 'replace'
|
|
21
|
+
navigate(target: BridgeLocation, action: 'push' | 'replace', options?: {
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
}): Promise<BridgeNavigationResult>;
|
|
22
24
|
go(delta: number): void;
|
|
23
25
|
}
|
|
24
26
|
/** 子应用路由通道(桥接宿主按挂载代次创建,随 mount 第三参数交给子应用) */
|
|
@@ -29,68 +31,43 @@ interface BridgeChildRoute {
|
|
|
29
31
|
go(delta: number): void;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
/**
|
|
33
|
-
* React Router 路由适配(/bridge/router/react 按需入口;URL 同步任务书 §3.1/§3.3)。
|
|
34
|
-
*
|
|
35
|
-
* 实际支持模式(原型实证,任务书 §1 结论 2):
|
|
36
|
-
* - 宿主:data router(createBrowserRouter / createHashRouter + RouterProvider)。
|
|
37
|
-
* router.navigate 被 blocker 拦截时静默返回 void(dist 源码核实)——取消用
|
|
38
|
-
* 「与 useBlocker 同一取消策略预判」:canNavigate(target) 返回 false 即 cancelled,
|
|
39
|
-
* URL/历史零副作用;菜单/POP 等其他来源仍由树内 useBlocker 真实拦截。
|
|
40
|
-
* - 子应用:createMemoryRouter(RR 6.11+)受控路由——Link/useNavigate 全兼容,
|
|
41
|
-
* 通道广播与本地导航双向桥接,取消时乐观回滚。
|
|
42
|
-
* - declarative 模式(BrowserRouter)无 blocker/提交语义,不支持 URL 同步(文档声明)。
|
|
43
|
-
*/
|
|
34
|
+
/** React Router data router 的宿主端口与子应用 memory router 接线(按需入口)。 */
|
|
44
35
|
|
|
45
|
-
/** 宿主取消策略(与树内 useBlocker 共用同一谓词,单一事实源) */
|
|
46
36
|
type ReactBridgeCancelPolicy = (next: BridgeLocation) => boolean;
|
|
47
|
-
/** data router 的最小结构面(避免耦合 RR 内部类型版本) */
|
|
48
37
|
interface ReactDataRouterLike {
|
|
49
38
|
state: {
|
|
50
39
|
location: {
|
|
51
40
|
pathname: string;
|
|
52
41
|
search: string;
|
|
53
42
|
hash: string;
|
|
43
|
+
key?: string;
|
|
44
|
+
};
|
|
45
|
+
navigation?: {
|
|
46
|
+
state: string;
|
|
47
|
+
location?: BridgeLocation;
|
|
54
48
|
};
|
|
49
|
+
blockers?: ReadonlyMap<string, {
|
|
50
|
+
state: string;
|
|
51
|
+
reset?: () => void;
|
|
52
|
+
}>;
|
|
55
53
|
};
|
|
56
54
|
subscribe(cb: () => void): () => void;
|
|
57
55
|
navigate(to: string | number, opts?: {
|
|
58
56
|
replace?: boolean;
|
|
59
57
|
}): void | Promise<void>;
|
|
60
58
|
}
|
|
61
|
-
/**
|
|
62
|
-
* 宿主侧:把 RR data router 包装为宿主导航端口。
|
|
63
|
-
*
|
|
64
|
-
* ```tsx
|
|
65
|
-
* const router = createBrowserRouter(routes, { basename })
|
|
66
|
-
* const navigation = createReactBridgeNavigation(router, {
|
|
67
|
-
* basename, // = createBrowserRouter 的 basename(含它;RR location.pathname 不剥 basename)
|
|
68
|
-
* canNavigate: (next) => !next.pathname.startsWith('/approval/secret'), // 与 useBlocker 同一谓词
|
|
69
|
-
* })
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
59
|
+
/** canNavigate 是可选预判;最终结果同时核对真实 blocker 与已提交位置。 */
|
|
72
60
|
declare function createReactBridgeNavigation(router: ReactDataRouterLike, options?: {
|
|
73
61
|
canNavigate?: ReactBridgeCancelPolicy;
|
|
74
62
|
basename?: string;
|
|
75
63
|
}): BridgeHostNavigation;
|
|
76
64
|
interface ReactBridgeRouterConnection {
|
|
77
|
-
/** RouterProvider 元素:子应用在自己的 root 内渲染 */
|
|
78
65
|
element: ReactElement;
|
|
79
|
-
/** 清理本接线(通道订阅与本地监听;通道本身由宿主生命周期销毁) */
|
|
80
66
|
dispose(): void;
|
|
81
67
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
* export default defineBridgeApp((props, ctx) => {
|
|
87
|
-
* const conn = createReactBridgeRouter(ctx.routing, routes)
|
|
88
|
-
* const root = createRoot(el)
|
|
89
|
-
* root.render(conn.element)
|
|
90
|
-
* return { mount: () => {}, unmount: () => { conn.dispose(); root.unmount() } }
|
|
91
|
-
* }, { routing: true })
|
|
92
|
-
* ```
|
|
93
|
-
*/
|
|
94
|
-
declare function createReactBridgeRouter(routing: BridgeChildRoute, routes: RouteObject[]): ReactBridgeRouterConnection;
|
|
68
|
+
/** 返回 RouterProvider 元素,直接作为 defineBridgeApp 工厂的 ReactElement 返回值。 */
|
|
69
|
+
declare function createReactBridgeRouter(routing: BridgeChildRoute, routes: RouteObject[], options?: {
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
}): ReactBridgeRouterConnection;
|
|
95
72
|
|
|
96
73
|
export { type ReactBridgeCancelPolicy, type ReactBridgeRouterConnection, type ReactDataRouterLike, createReactBridgeNavigation, createReactBridgeRouter };
|
|
@@ -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,
|