@fulgurjs/federation 2.0.2 → 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/dist/vue.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  ErrorCodes,
3
3
  FgError
4
4
  } from "./chunk-3VBYOVH4.js";
5
- import "./chunk-UDLQALHC.js";
5
+ import "./chunk-RP6HNIQT.js";
6
6
 
7
7
  // src/vue.ts
8
8
  import { defineAsyncComponent, defineComponent, h } from "vue";
@@ -210,11 +210,15 @@ 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();
220
+ const remoteManifests = /* @__PURE__ */ new Map();
221
+ const stylesheetLoads = /* @__PURE__ */ new WeakMap();
218
222
  const containerInitScopes = /* @__PURE__ */ new WeakMap();
219
223
  const hooks = {};
220
224
  const applyPlugins = () => {
@@ -226,7 +230,7 @@ function createRuntime() {
226
230
  }
227
231
  }
228
232
  };
229
- const getScope = (name) => shareScopeMap2[name] ??= {};
233
+ const getScope = (name) => shareScopeMap2[name] ??= /* @__PURE__ */ Object.create(null);
230
234
  function emitError(info) {
231
235
  try {
232
236
  hooks.onRemoteError?.(info);
@@ -242,22 +246,49 @@ function createRuntime() {
242
246
  }
243
247
  function registerShare2(scopeName, name, version, get, opts = {}) {
244
248
  const scope = getScope(scopeName || "default");
245
- const byName = scope[name] ??= {};
246
- if (byName[version]) {
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]) {
247
256
  return;
248
257
  }
249
258
  byName[version] = { version, get, from: opts.from ?? "unknown", eager: !!opts.eager, loaded: !!opts.loaded };
250
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
+ }
251
277
  function registerRemotes2(list) {
252
278
  for (const r of list) {
279
+ assertRemoteParams(r);
253
280
  const prev = remotes.get(r.name);
254
281
  remotes.set(r.name, {
255
282
  timeout: DEFAULT_TIMEOUT,
256
283
  retries: DEFAULT_RETRIES,
257
284
  ...prev,
258
285
  ...r,
286
+ // 熔断参数随最新配置刷新;计数状态(fails/openUntil)跨注册保留——半开窗口不被重置
287
+ breakerThreshold: r.breaker?.threshold ?? prev?.breakerThreshold ?? BREAKER_THRESHOLD,
288
+ breakerResetMs: r.breaker?.resetMs ?? prev?.breakerResetMs ?? BREAKER_RESET_MS,
259
289
  state: prev?.state ?? "idle",
260
290
  breakerState: prev?.breakerState ?? { fails: 0, openUntil: 0 },
291
+ entryInflight: prev?.entryInflight ?? /* @__PURE__ */ new Map(),
261
292
  debug: prev?.debug ?? { entry: r.entry, status: "idle" }
262
293
  });
263
294
  }
@@ -331,6 +362,14 @@ function createRuntime() {
331
362
  entry.loaded = true;
332
363
  return entry.get();
333
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
+ }
334
373
  function withTimeout(p, ms, label) {
335
374
  return new Promise((resolve, reject) => {
336
375
  const t = setTimeout(() => reject(new Error(`timeout after ${ms}ms: ${label}`)), ms);
@@ -363,14 +402,16 @@ function createRuntime() {
363
402
  }
364
403
  }
365
404
  async function importEntry(url, remote) {
366
- return withTimeout(
367
- import(
368
- /* @vite-ignore */
369
- url
370
- ),
371
- remote.timeout ?? DEFAULT_TIMEOUT,
372
- `load remoteEntry ${url}`
405
+ const cached = remote.entryInflight.get(url);
406
+ const inflight = cached ?? import(
407
+ /* @vite-ignore */
408
+ url
373
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}`);
374
415
  }
375
416
  async function acquireContainer(remote, overrides) {
376
417
  const scopeKey = remote.shareScope || "default";
@@ -401,7 +442,11 @@ function createRuntime() {
401
442
  const loaders = [];
402
443
  if (remote.promise) {
403
444
  loaders.push(async () => {
404
- const resolved = await remote.promise();
445
+ const resolved = await withTimeout(
446
+ remote.promise(),
447
+ remote.timeout ?? DEFAULT_TIMEOUT,
448
+ `resolve promise remote "${remote.name}"`
449
+ );
405
450
  const container2 = typeof resolved === "string" ? await importEntry(resolved, remote) : resolved;
406
451
  validateContainerInterface(container2, remote);
407
452
  return container2;
@@ -432,25 +477,26 @@ function createRuntime() {
432
477
  lastErr = err;
433
478
  if (err?.code) throw err;
434
479
  if (attempt < maxRetries) {
435
- await sleep(200 * 2 ** attempt);
480
+ await sleep(Math.min(BACKOFF_MAX_MS, 200 * 2 ** attempt) * (1 - BACKOFF_JITTER * (1 - 2 * Math.random())));
436
481
  }
437
482
  }
438
483
  }
439
484
  if (succeeded) break;
440
485
  }
441
486
  if (!succeeded || !container) {
487
+ const shown = sanitizeUrl(remote.entry);
442
488
  const hints = [];
443
489
  if (/^https?:\/\//.test(remote.entry) && typeof location !== "undefined") {
444
- hints.push(`1. is the remote dev server running? try opening ${remote.entry} in the browser \u2014 it must return JS, not HTML/an error`);
445
- hints.push(`2. is the URL correct in federation({ remotes })? dev and prod entries can differ`);
446
- hints.push(`3. CORS: the remote dev server must allow cross-origin requests (fulgurjs-federation enables server.cors automatically; a custom server config may have disabled it)`);
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)`);
447
493
  } else {
448
- hints.push(`1. is the remote deployed and reachable? try opening ${remote.entry} in the browser \u2014 it must return JS`);
449
- hints.push(`2. NGINX/CDN routing: the entry path must serve the remoteEntry JS file (check try_files / fallback rules)`);
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)`);
450
496
  }
451
497
  throw new FgError(
452
498
  ErrorCodes.REMOTE_LOAD_FAILED,
453
- `failed to load remote "${remote.name}" from ${remote.entry}: ${String(lastErr?.message ?? lastErr)}
499
+ `failed to load remote "${remote.name}" from ${shown}: ${String(lastErr?.message ?? lastErr)}
454
500
  ` + hints.join("\n"),
455
501
  { remote: remote.name }
456
502
  );
@@ -482,8 +528,8 @@ function createRuntime() {
482
528
  remote.debug.status = "failed";
483
529
  remote.debug.error = String(err?.message ?? err);
484
530
  b.fails += 1;
485
- if (b.fails >= BREAKER_THRESHOLD) {
486
- b.openUntil = Date.now() + BREAKER_RESET_MS;
531
+ if (b.fails >= remote.breakerThreshold) {
532
+ b.openUntil = Date.now() + remote.breakerResetMs;
487
533
  b.fails = 0;
488
534
  }
489
535
  if (err instanceof FgError) {
@@ -501,7 +547,7 @@ function createRuntime() {
501
547
  throw wrapped;
502
548
  }
503
549
  }
504
- function parseSpec(spec) {
550
+ function parseSpec2(spec) {
505
551
  const idx = spec.indexOf("/");
506
552
  if (idx === -1) return { remote: spec, module: "" };
507
553
  return { remote: spec.slice(0, idx), module: normalizeModuleName(spec.slice(idx + 1)) };
@@ -511,8 +557,12 @@ function createRuntime() {
511
557
  return m.startsWith(".") ? m : `./${m}`;
512
558
  }
513
559
  async function loadRemote2(spec, opts) {
514
- const { remote: name, module } = parseSpec(spec);
515
- hooks.beforeLoadRemote?.({ remote: name, module });
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
+ }
516
566
  const remote = remotes.get(name);
517
567
  if (!remote) {
518
568
  throw new FgError(
@@ -523,7 +573,8 @@ function createRuntime() {
523
573
  }
524
574
  let container;
525
575
  try {
526
- container = await acquireContainer(remote, { retries: opts?.retries });
576
+ const stylesReady = module && remote.manifestUrl ? preloadRemote2(spec) : Promise.resolve();
577
+ [container] = await Promise.all([acquireContainer(remote, { retries: opts?.retries }), stylesReady]);
527
578
  } catch (err) {
528
579
  if (opts?.fallbackModule) {
529
580
  console.error(`[fulgurjs] loadRemote("${spec}") failed; returning fallbackModule (\u663E\u5F0F\u964D\u7EA7\uFF0C\u9519\u8BEF\u5DF2\u900F\u51FA)`, err);
@@ -559,7 +610,11 @@ function createRuntime() {
559
610
  }
560
611
  throw err;
561
612
  }
562
- hooks.afterLoadRemote?.({ remote: name, module, module_ns: ns });
613
+ try {
614
+ hooks.afterLoadRemote?.({ remote: name, module, module_ns: ns });
615
+ } catch (hookErr) {
616
+ console.warn("[fulgurjs] afterLoadRemote hook error (ignored):", hookErr);
617
+ }
563
618
  if (ns && typeof ns === "object" && Object.keys(ns).length === 0) {
564
619
  const err = new FgError(
565
620
  ErrorCodes.EMPTY_EXPORTS,
@@ -579,15 +634,44 @@ function createRuntime() {
579
634
  return acquireContainer(remote);
580
635
  }
581
636
  async function preloadRemote2(spec, opts = {}) {
582
- const { remote: name } = parseSpec(spec);
637
+ const { remote: name, module } = parseSpec2(spec);
583
638
  const remote = remotes.get(name);
584
639
  if (!remote) {
585
640
  throw new FgError(ErrorCodes.REMOTE_UNKNOWN, `unknown remote "${name}"`, { remote: name });
586
641
  }
587
642
  const mode = opts.mode ?? "preload";
643
+ const waitForStylesheet = (link, href) => {
644
+ if (link.sheet) return Promise.resolve();
645
+ const pending = stylesheetLoads.get(link);
646
+ if (pending) return pending;
647
+ const loaded = new Promise((resolve) => {
648
+ const finish = () => {
649
+ link.removeEventListener("load", finish);
650
+ link.removeEventListener("error", failed);
651
+ resolve();
652
+ };
653
+ const failed = () => {
654
+ emitError({
655
+ remote: name,
656
+ error: new FgError(ErrorCodes.PRELOAD_FAILED, `failed to load remote stylesheet ${href}`, {
657
+ remote: name
658
+ })
659
+ });
660
+ finish();
661
+ };
662
+ link.addEventListener("load", finish, { once: true });
663
+ link.addEventListener("error", failed, { once: true });
664
+ if (link.sheet) finish();
665
+ });
666
+ stylesheetLoads.set(link, loaded);
667
+ return loaded;
668
+ };
588
669
  const inject = (href, as) => {
589
- if (typeof document === "undefined") return;
590
- if (document.querySelector(`link[href="${href}"]`)) return;
670
+ if (typeof document === "undefined") return Promise.resolve();
671
+ const existing = document.querySelector(`link[href="${href}"]`);
672
+ if (existing) {
673
+ return as === "style" && mode === "preload" ? stylesheetLoads.get(existing) ?? Promise.resolve() : Promise.resolve();
674
+ }
591
675
  const link = document.createElement("link");
592
676
  link.rel = as === "style" ? "stylesheet" : "modulepreload";
593
677
  link.href = href;
@@ -595,24 +679,69 @@ function createRuntime() {
595
679
  ;
596
680
  link.fetchPriority = "low";
597
681
  }
682
+ const loaded = as === "style" ? waitForStylesheet(link, href) : Promise.resolve();
598
683
  document.head.appendChild(link);
684
+ return as === "style" && mode === "preload" ? loaded : Promise.resolve();
685
+ };
686
+ const resolveAssetUrl = (asset) => {
687
+ const pageUrl = typeof document !== "undefined" ? document.baseURI : void 0;
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
+ }
692
+ return new URL(asset, base).href;
599
693
  };
600
694
  try {
601
695
  if (remote.manifestUrl) {
602
- const res = await fetch(remote.manifestUrl, { cache: "no-cache" });
603
- if (res.ok) {
604
- const manifest = await res.json();
605
- inject(remote.entry, "modulepreload");
606
- const exposes = manifest.exposes ?? {};
607
- for (const key of Object.keys(exposes)) {
608
- const item = exposes[key];
609
- if (item?.file) inject(new URL(item.file, remote.entry).href, "modulepreload");
610
- for (const css of item?.css ?? []) inject(new URL(css, remote.entry).href, "style");
696
+ let manifestPromise = remoteManifests.get(remote.manifestUrl);
697
+ if (!manifestPromise) {
698
+ manifestPromise = fetch(remote.manifestUrl, {
699
+ cache: "no-cache",
700
+ signal: AbortSignal.timeout(8e3)
701
+ }).then((res) => res.ok ? res.json() : void 0);
702
+ remoteManifests.set(remote.manifestUrl, manifestPromise);
703
+ manifestPromise.then(
704
+ (manifest2) => {
705
+ if (!manifest2) remoteManifests.delete(remote.manifestUrl);
706
+ },
707
+ () => remoteManifests.delete(remote.manifestUrl)
708
+ );
709
+ }
710
+ const manifest = await manifestPromise;
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
+ }
723
+ await inject(remote.entry, "modulepreload");
724
+ const exposes = manifest.exposes;
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) : [];
726
+ const selected = module ? entries.filter(([key, item]) => normalizeModuleName(String(item?.name ?? key)) === module) : entries;
727
+ const cssLoads = [];
728
+ for (const [, item] of selected) {
729
+ if (typeof item?.file === "string") {
730
+ await inject(resolveAssetUrl(item.file), "modulepreload");
731
+ }
732
+ if (Array.isArray(item?.css)) {
733
+ for (const css of item.css) {
734
+ if (typeof css !== "string") continue;
735
+ const loading = inject(resolveAssetUrl(css), "style");
736
+ if (mode === "preload") cssLoads.push(loading);
737
+ }
738
+ }
611
739
  }
740
+ await Promise.all(cssLoads);
612
741
  return;
613
742
  }
614
743
  }
615
- inject(remote.entry, "modulepreload");
744
+ await inject(remote.entry, "modulepreload");
616
745
  } catch (err) {
617
746
  emitError({
618
747
  remote: name,
@@ -648,7 +777,7 @@ function createRuntime() {
648
777
  loadRemote: loadRemote2,
649
778
  getContainer: getContainer2,
650
779
  preloadRemote: preloadRemote2,
651
- parseSpec
780
+ parseSpec: parseSpec2
652
781
  };
653
782
  }
654
783
  var g = globalThis;
@@ -667,6 +796,7 @@ var loadShare = runtime.loadShare;
667
796
  var loadRemote = runtime.loadRemote;
668
797
  var getContainer = runtime.getContainer;
669
798
  var preloadRemote = runtime.preloadRemote;
799
+ var parseSpec = runtime.parseSpec;
670
800
  var shareScopeMap = runtime.shareScopeMap;
671
801
 
672
802
  // src/vue.ts
@@ -1,7 +1,8 @@
1
- # P5:fixtures Vite 7/8 兼容矩阵(2026-09-14 完成)
1
+ # P5:fixtures Vite 7/8 兼容矩阵(2026-09-14 完成,2026-09-23 补充)
2
2
 
3
3
  > 结论先行:**Vite 7.3.6 与 8.3.0 下 fixtures 三件套 dev+prod e2e 全部通过**,无需代码改动。
4
4
  > 兼容矩阵至此覆盖 Vite 5.1.4 / 5.2.12 / 6.4.3 / 7.3.6 / 8.3.0。
5
+ > 2026-09-23 补充:Vite 8.3.0(Rolldown)下,expose 的静态依赖 chunk 持有 CSS 时,manifest 仍能收集到该 CSS;针对性回归测试通过。
5
6
 
6
7
  ## 测试方法
7
8
 
@@ -29,8 +30,9 @@
29
30
  等钩子签名在 7/8 无破坏性变化;
30
31
  - dev 协作引擎依赖的 `server.middlewares`、`pluginContainer.resolveId` 行为一致;
31
32
  - build 引擎(rollup 原生输出 + `emitFile chunk`)在 7/8 下产物结构不变。
33
+ - `packages/plugin/tests/build-manifest-css.test.ts` 在 Vite 8.3.0/Rolldown 下验证了共享静态依赖 chunk 上的 CSS 会进入 expose manifest;这是针对 CSS 注入链的构建测试,不等同于完整的 Rolldown dev/prod e2e 矩阵。
32
34
 
33
35
  ## 遗留
34
36
 
35
- - Vite 8 原生 rolldown 分支(`rolldown-vite`)不在本矩阵范围,待上游合并主线后再跑一轮。
37
+ - Vite 8 原生 Rolldown 的完整 fixtures dev/prod e2e 矩阵仍未执行;当前仅 CSS manifest 构建回归已验证。
36
38
  - fixtures 只覆盖 vue 生态;react fixture 若将来补齐需同步扩 7/8 矩阵。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fulgurjs/federation",
3
- "version": "2.0.2",
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/\u8fc1\u79fb\u6307\u5357.md",
66
- "docs/webpack-mf-\u5bf9\u7167\u4e0e\u7f3a\u53e3.md",
67
- "docs/\u6c99\u7bb1\u8fb9\u754c\u5ba1\u8ba1.md",
68
- "docs/P5-vite7-8\u517c\u5bb9\u77e9\u9635.md"
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/\u8fc1\u79fb\u6307\u5357.md ../../docs/webpack-mf-\u5bf9\u7167\u4e0e\u7f3a\u53e3.md ../../docs/\u6c99\u7bb1\u8fb9\u754c\u5ba1\u8ba1.md ../../docs/P5-vite7-8\u517c\u5bb9\u77e9\u9635.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",
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",