@fulgurjs/federation 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +61 -6
- package/client.d.ts +17 -1
- package/dist/{chunk-RQPLFDDT.js → chunk-RP6HNIQT.js} +1 -1
- package/dist/cli.js +147 -3
- package/dist/index.cjs +488 -60
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +490 -60
- package/dist/runtime.js +3 -3
- package/dist/vue.cjs +98 -30
- package/dist/vue.js +99 -31
- package/package.json +6 -6
package/dist/vue.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
ErrorCodes,
|
|
3
3
|
FgError
|
|
4
4
|
} from "./chunk-3VBYOVH4.js";
|
|
5
|
-
import "./chunk-
|
|
5
|
+
import "./chunk-RP6HNIQT.js";
|
|
6
6
|
|
|
7
7
|
// src/vue.ts
|
|
8
8
|
import { defineAsyncComponent, defineComponent, h } from "vue";
|
|
@@ -210,8 +210,10 @@ var DEFAULT_TIMEOUT = 15e3;
|
|
|
210
210
|
var DEFAULT_RETRIES = 2;
|
|
211
211
|
var BREAKER_THRESHOLD = 5;
|
|
212
212
|
var BREAKER_RESET_MS = 3e4;
|
|
213
|
+
var BACKOFF_MAX_MS = 4e3;
|
|
214
|
+
var BACKOFF_JITTER = 0.2;
|
|
213
215
|
function createRuntime() {
|
|
214
|
-
const shareScopeMap2 =
|
|
216
|
+
const shareScopeMap2 = /* @__PURE__ */ Object.create(null);
|
|
215
217
|
const remotes = /* @__PURE__ */ new Map();
|
|
216
218
|
const plugins = [];
|
|
217
219
|
const loadedModules = /* @__PURE__ */ new Map();
|
|
@@ -228,7 +230,7 @@ function createRuntime() {
|
|
|
228
230
|
}
|
|
229
231
|
}
|
|
230
232
|
};
|
|
231
|
-
const getScope = (name) => shareScopeMap2[name] ??=
|
|
233
|
+
const getScope = (name) => shareScopeMap2[name] ??= /* @__PURE__ */ Object.create(null);
|
|
232
234
|
function emitError(info) {
|
|
233
235
|
try {
|
|
234
236
|
hooks.onRemoteError?.(info);
|
|
@@ -244,22 +246,49 @@ function createRuntime() {
|
|
|
244
246
|
}
|
|
245
247
|
function registerShare2(scopeName, name, version, get, opts = {}) {
|
|
246
248
|
const scope = getScope(scopeName || "default");
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
+
const own = Object.hasOwn(scope, name) ? scope[name] : void 0;
|
|
250
|
+
let byName = own && typeof own === "object" ? own : void 0;
|
|
251
|
+
if (!byName) {
|
|
252
|
+
byName = /* @__PURE__ */ Object.create(null);
|
|
253
|
+
scope[name] = byName;
|
|
254
|
+
}
|
|
255
|
+
if (Object.hasOwn(byName, version) && byName[version]) {
|
|
249
256
|
return;
|
|
250
257
|
}
|
|
251
258
|
byName[version] = { version, get, from: opts.from ?? "unknown", eager: !!opts.eager, loaded: !!opts.loaded };
|
|
252
259
|
}
|
|
260
|
+
function assertRemoteParams(r) {
|
|
261
|
+
const check = (v, name, int10) => {
|
|
262
|
+
if (v === void 0) return;
|
|
263
|
+
const ok = typeof v === "number" && (int10 ? Number.isInteger(v) && v >= 0 && v <= 10 : Number.isFinite(v) && v > 0);
|
|
264
|
+
if (!ok) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`[fulgurjs] registerRemote("${r.name}"): ${name} must be a ${int10 ? "integer 0..10" : "finite positive number"}, got ${String(v)}`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
check(r.timeout, "timeout");
|
|
271
|
+
check(r.retries, "retries", true);
|
|
272
|
+
if (r.breaker) {
|
|
273
|
+
check(r.breaker.threshold, "breaker.threshold");
|
|
274
|
+
check(r.breaker.resetMs, "breaker.resetMs");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
253
277
|
function registerRemotes2(list) {
|
|
254
278
|
for (const r of list) {
|
|
279
|
+
assertRemoteParams(r);
|
|
255
280
|
const prev = remotes.get(r.name);
|
|
256
281
|
remotes.set(r.name, {
|
|
257
282
|
timeout: DEFAULT_TIMEOUT,
|
|
258
283
|
retries: DEFAULT_RETRIES,
|
|
259
284
|
...prev,
|
|
260
285
|
...r,
|
|
286
|
+
// 熔断参数随最新配置刷新;计数状态(fails/openUntil)跨注册保留——半开窗口不被重置
|
|
287
|
+
breakerThreshold: r.breaker?.threshold ?? prev?.breakerThreshold ?? BREAKER_THRESHOLD,
|
|
288
|
+
breakerResetMs: r.breaker?.resetMs ?? prev?.breakerResetMs ?? BREAKER_RESET_MS,
|
|
261
289
|
state: prev?.state ?? "idle",
|
|
262
290
|
breakerState: prev?.breakerState ?? { fails: 0, openUntil: 0 },
|
|
291
|
+
entryInflight: prev?.entryInflight ?? /* @__PURE__ */ new Map(),
|
|
263
292
|
debug: prev?.debug ?? { entry: r.entry, status: "idle" }
|
|
264
293
|
});
|
|
265
294
|
}
|
|
@@ -333,6 +362,14 @@ function createRuntime() {
|
|
|
333
362
|
entry.loaded = true;
|
|
334
363
|
return entry.get();
|
|
335
364
|
}
|
|
365
|
+
function sanitizeUrl(u) {
|
|
366
|
+
try {
|
|
367
|
+
const x = new URL(u, typeof location < "u" ? location.href : "http://f.invalid");
|
|
368
|
+
return x.protocol + "//" + x.host + x.pathname;
|
|
369
|
+
} catch {
|
|
370
|
+
return String(u).replace(/[?#].*$/, "");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
336
373
|
function withTimeout(p, ms, label) {
|
|
337
374
|
return new Promise((resolve, reject) => {
|
|
338
375
|
const t = setTimeout(() => reject(new Error(`timeout after ${ms}ms: ${label}`)), ms);
|
|
@@ -365,14 +402,16 @@ function createRuntime() {
|
|
|
365
402
|
}
|
|
366
403
|
}
|
|
367
404
|
async function importEntry(url, remote) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
),
|
|
373
|
-
remote.timeout ?? DEFAULT_TIMEOUT,
|
|
374
|
-
`load remoteEntry ${url}`
|
|
405
|
+
const cached = remote.entryInflight.get(url);
|
|
406
|
+
const inflight = cached ?? import(
|
|
407
|
+
/* @vite-ignore */
|
|
408
|
+
url
|
|
375
409
|
);
|
|
410
|
+
if (!cached) {
|
|
411
|
+
remote.entryInflight.set(url, inflight);
|
|
412
|
+
inflight.catch(() => remote.entryInflight.delete(url));
|
|
413
|
+
}
|
|
414
|
+
return withTimeout(inflight, remote.timeout ?? DEFAULT_TIMEOUT, `load remoteEntry ${url}`);
|
|
376
415
|
}
|
|
377
416
|
async function acquireContainer(remote, overrides) {
|
|
378
417
|
const scopeKey = remote.shareScope || "default";
|
|
@@ -403,7 +442,11 @@ function createRuntime() {
|
|
|
403
442
|
const loaders = [];
|
|
404
443
|
if (remote.promise) {
|
|
405
444
|
loaders.push(async () => {
|
|
406
|
-
const resolved = await
|
|
445
|
+
const resolved = await withTimeout(
|
|
446
|
+
remote.promise(),
|
|
447
|
+
remote.timeout ?? DEFAULT_TIMEOUT,
|
|
448
|
+
`resolve promise remote "${remote.name}"`
|
|
449
|
+
);
|
|
407
450
|
const container2 = typeof resolved === "string" ? await importEntry(resolved, remote) : resolved;
|
|
408
451
|
validateContainerInterface(container2, remote);
|
|
409
452
|
return container2;
|
|
@@ -434,25 +477,26 @@ function createRuntime() {
|
|
|
434
477
|
lastErr = err;
|
|
435
478
|
if (err?.code) throw err;
|
|
436
479
|
if (attempt < maxRetries) {
|
|
437
|
-
await sleep(200 * 2 ** attempt);
|
|
480
|
+
await sleep(Math.min(BACKOFF_MAX_MS, 200 * 2 ** attempt) * (1 - BACKOFF_JITTER * (1 - 2 * Math.random())));
|
|
438
481
|
}
|
|
439
482
|
}
|
|
440
483
|
}
|
|
441
484
|
if (succeeded) break;
|
|
442
485
|
}
|
|
443
486
|
if (!succeeded || !container) {
|
|
487
|
+
const shown = sanitizeUrl(remote.entry);
|
|
444
488
|
const hints = [];
|
|
445
489
|
if (/^https?:\/\//.test(remote.entry) && typeof location !== "undefined") {
|
|
446
|
-
hints.push(`1. is the remote dev server running?
|
|
447
|
-
hints.push(`2. is the URL correct in federation({ remotes })? dev
|
|
448
|
-
hints.push(`3. CORS: the remote dev server must allow cross-origin requests (
|
|
490
|
+
hints.push(`1. is the remote dev server running? open ${shown} in a browser \u2014 it must return JS, not HTML`);
|
|
491
|
+
hints.push(`2. is the URL correct in federation({ remotes })? dev/prod entries can differ`);
|
|
492
|
+
hints.push(`3. CORS: the remote dev server must allow cross-origin requests (check its server.cors)`);
|
|
449
493
|
} else {
|
|
450
|
-
hints.push(`1. is the remote deployed
|
|
451
|
-
hints.push(`2. NGINX/CDN routing: the entry path must serve the remoteEntry JS
|
|
494
|
+
hints.push(`1. is the remote deployed? open ${shown} in a browser \u2014 it must return JS`);
|
|
495
|
+
hints.push(`2. NGINX/CDN routing: the entry path must serve the remoteEntry JS (check try_files)`);
|
|
452
496
|
}
|
|
453
497
|
throw new FgError(
|
|
454
498
|
ErrorCodes.REMOTE_LOAD_FAILED,
|
|
455
|
-
`failed to load remote "${remote.name}" from ${
|
|
499
|
+
`failed to load remote "${remote.name}" from ${shown}: ${String(lastErr?.message ?? lastErr)}
|
|
456
500
|
` + hints.join("\n"),
|
|
457
501
|
{ remote: remote.name }
|
|
458
502
|
);
|
|
@@ -484,8 +528,8 @@ function createRuntime() {
|
|
|
484
528
|
remote.debug.status = "failed";
|
|
485
529
|
remote.debug.error = String(err?.message ?? err);
|
|
486
530
|
b.fails += 1;
|
|
487
|
-
if (b.fails >=
|
|
488
|
-
b.openUntil = Date.now() +
|
|
531
|
+
if (b.fails >= remote.breakerThreshold) {
|
|
532
|
+
b.openUntil = Date.now() + remote.breakerResetMs;
|
|
489
533
|
b.fails = 0;
|
|
490
534
|
}
|
|
491
535
|
if (err instanceof FgError) {
|
|
@@ -503,7 +547,7 @@ function createRuntime() {
|
|
|
503
547
|
throw wrapped;
|
|
504
548
|
}
|
|
505
549
|
}
|
|
506
|
-
function
|
|
550
|
+
function parseSpec2(spec) {
|
|
507
551
|
const idx = spec.indexOf("/");
|
|
508
552
|
if (idx === -1) return { remote: spec, module: "" };
|
|
509
553
|
return { remote: spec.slice(0, idx), module: normalizeModuleName(spec.slice(idx + 1)) };
|
|
@@ -513,8 +557,12 @@ function createRuntime() {
|
|
|
513
557
|
return m.startsWith(".") ? m : `./${m}`;
|
|
514
558
|
}
|
|
515
559
|
async function loadRemote2(spec, opts) {
|
|
516
|
-
const { remote: name, module } =
|
|
517
|
-
|
|
560
|
+
const { remote: name, module } = parseSpec2(spec);
|
|
561
|
+
try {
|
|
562
|
+
hooks.beforeLoadRemote?.({ remote: name, module });
|
|
563
|
+
} catch (hookErr) {
|
|
564
|
+
console.warn("[fulgurjs] beforeLoadRemote hook error (ignored):", hookErr);
|
|
565
|
+
}
|
|
518
566
|
const remote = remotes.get(name);
|
|
519
567
|
if (!remote) {
|
|
520
568
|
throw new FgError(
|
|
@@ -562,7 +610,11 @@ function createRuntime() {
|
|
|
562
610
|
}
|
|
563
611
|
throw err;
|
|
564
612
|
}
|
|
565
|
-
|
|
613
|
+
try {
|
|
614
|
+
hooks.afterLoadRemote?.({ remote: name, module, module_ns: ns });
|
|
615
|
+
} catch (hookErr) {
|
|
616
|
+
console.warn("[fulgurjs] afterLoadRemote hook error (ignored):", hookErr);
|
|
617
|
+
}
|
|
566
618
|
if (ns && typeof ns === "object" && Object.keys(ns).length === 0) {
|
|
567
619
|
const err = new FgError(
|
|
568
620
|
ErrorCodes.EMPTY_EXPORTS,
|
|
@@ -582,7 +634,7 @@ function createRuntime() {
|
|
|
582
634
|
return acquireContainer(remote);
|
|
583
635
|
}
|
|
584
636
|
async function preloadRemote2(spec, opts = {}) {
|
|
585
|
-
const { remote: name, module } =
|
|
637
|
+
const { remote: name, module } = parseSpec2(spec);
|
|
586
638
|
const remote = remotes.get(name);
|
|
587
639
|
if (!remote) {
|
|
588
640
|
throw new FgError(ErrorCodes.REMOTE_UNKNOWN, `unknown remote "${name}"`, { remote: name });
|
|
@@ -634,15 +686,19 @@ function createRuntime() {
|
|
|
634
686
|
const resolveAssetUrl = (asset) => {
|
|
635
687
|
const pageUrl = typeof document !== "undefined" ? document.baseURI : void 0;
|
|
636
688
|
const base = new URL(remote.entry, pageUrl || "http://fulgurjs.invalid/");
|
|
689
|
+
if (!base.pathname.endsWith("/") && !base.pathname.split("/").pop().includes(".")) {
|
|
690
|
+
base.pathname += "/";
|
|
691
|
+
}
|
|
637
692
|
return new URL(asset, base).href;
|
|
638
693
|
};
|
|
639
694
|
try {
|
|
640
695
|
if (remote.manifestUrl) {
|
|
641
696
|
let manifestPromise = remoteManifests.get(remote.manifestUrl);
|
|
642
697
|
if (!manifestPromise) {
|
|
643
|
-
manifestPromise = fetch(remote.manifestUrl, {
|
|
644
|
-
|
|
645
|
-
|
|
698
|
+
manifestPromise = fetch(remote.manifestUrl, {
|
|
699
|
+
cache: "no-cache",
|
|
700
|
+
signal: AbortSignal.timeout(8e3)
|
|
701
|
+
}).then((res) => res.ok ? res.json() : void 0);
|
|
646
702
|
remoteManifests.set(remote.manifestUrl, manifestPromise);
|
|
647
703
|
manifestPromise.then(
|
|
648
704
|
(manifest2) => {
|
|
@@ -653,6 +709,17 @@ function createRuntime() {
|
|
|
653
709
|
}
|
|
654
710
|
const manifest = await manifestPromise;
|
|
655
711
|
if (manifest) {
|
|
712
|
+
const sv = manifest.schemaVersion;
|
|
713
|
+
if (typeof sv === "number" && (!Number.isInteger(sv) || sv > 1)) {
|
|
714
|
+
emitError({
|
|
715
|
+
remote: name,
|
|
716
|
+
error: new FgError(ErrorCodes.PRELOAD_FAILED, `unsupported manifest schemaVersion ${String(sv)} (expected 1)`, {
|
|
717
|
+
remote: name
|
|
718
|
+
})
|
|
719
|
+
});
|
|
720
|
+
await inject(remote.entry, "modulepreload");
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
656
723
|
await inject(remote.entry, "modulepreload");
|
|
657
724
|
const exposes = manifest.exposes;
|
|
658
725
|
const entries = Array.isArray(exposes) ? exposes.filter((item) => item && typeof item === "object").map((item) => [String(item.name ?? ""), item]) : exposes && typeof exposes === "object" ? Object.entries(exposes) : [];
|
|
@@ -710,7 +777,7 @@ function createRuntime() {
|
|
|
710
777
|
loadRemote: loadRemote2,
|
|
711
778
|
getContainer: getContainer2,
|
|
712
779
|
preloadRemote: preloadRemote2,
|
|
713
|
-
parseSpec
|
|
780
|
+
parseSpec: parseSpec2
|
|
714
781
|
};
|
|
715
782
|
}
|
|
716
783
|
var g = globalThis;
|
|
@@ -729,6 +796,7 @@ var loadShare = runtime.loadShare;
|
|
|
729
796
|
var loadRemote = runtime.loadRemote;
|
|
730
797
|
var getContainer = runtime.getContainer;
|
|
731
798
|
var preloadRemote = runtime.preloadRemote;
|
|
799
|
+
var parseSpec = runtime.parseSpec;
|
|
732
800
|
var shareScopeMap = runtime.shareScopeMap;
|
|
733
801
|
|
|
734
802
|
// src/vue.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fulgurjs/federation",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vite Module Federation with full Webpack MF parity. Dev & production, no compromise.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,13 +62,13 @@
|
|
|
62
62
|
"CHANGELOG.md",
|
|
63
63
|
"DESIGN.md",
|
|
64
64
|
"examples",
|
|
65
|
-
"docs
|
|
66
|
-
"docs/webpack-mf
|
|
67
|
-
"docs
|
|
68
|
-
"docs/P5-vite7-8
|
|
65
|
+
"docs/迁移指南.md",
|
|
66
|
+
"docs/webpack-mf-对照与缺口.md",
|
|
67
|
+
"docs/沙箱边界审计.md",
|
|
68
|
+
"docs/P5-vite7-8兼容矩阵.md"
|
|
69
69
|
],
|
|
70
70
|
"scripts": {
|
|
71
|
-
"build": "cp ../../README.md README.md && mkdir -p docs && cp ../../docs
|
|
71
|
+
"build": "cp ../../README.md README.md && mkdir -p docs && cp ../../docs/迁移指南.md ../../docs/webpack-mf-对照与缺口.md ../../docs/沙箱边界审计.md ../../docs/P5-vite7-8兼容矩阵.md docs/ && cp ../../CHANGELOG.md CHANGELOG.md && cp ../../DESIGN.md DESIGN.md && rm -rf examples && cp -r ../../examples examples && esbuild src/runtime/index.ts --bundle --format=esm --target=es2022 --minify --outfile=dist/runtime.js && node scripts/gen-runtime.mjs && tsup src/index.ts src/pages.ts src/config.ts src/vue.ts src/context.ts --format esm,cjs --dts --external vite --target node18 --clean && esbuild src/cli.ts --bundle --format=esm --target=node18 --platform=node --outfile=dist/cli.js && esbuild src/runtime/index.ts --bundle --format=esm --target=es2022 --minify --outfile=dist/runtime.js && node scripts/check-gzip.mjs && node scripts/check-manual-codes.mjs",
|
|
72
72
|
"dev": "tsup --watch",
|
|
73
73
|
"test": "vitest run",
|
|
74
74
|
"typecheck": "tsc --noEmit",
|