@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/dist/vue.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  ErrorCodes,
3
3
  FgError
4
4
  } from "./chunk-3VBYOVH4.js";
5
- import "./chunk-RQPLFDDT.js";
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 byName = scope[name] ??= {};
248
- 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]) {
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
- return withTimeout(
369
- import(
370
- /* @vite-ignore */
371
- url
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 remote.promise();
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? try opening ${remote.entry} in the browser \u2014 it must return JS, not HTML/an error`);
447
- hints.push(`2. is the URL correct in federation({ remotes })? dev and prod entries can differ`);
448
- 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)`);
449
493
  } else {
450
- hints.push(`1. is the remote deployed and reachable? try opening ${remote.entry} in the browser \u2014 it must return JS`);
451
- 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)`);
452
496
  }
453
497
  throw new FgError(
454
498
  ErrorCodes.REMOTE_LOAD_FAILED,
455
- `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)}
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 >= BREAKER_THRESHOLD) {
488
- b.openUntil = Date.now() + BREAKER_RESET_MS;
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 parseSpec(spec) {
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 } = parseSpec(spec);
517
- 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
+ }
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
- 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
+ }
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 } = parseSpec(spec);
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, { cache: "no-cache" }).then(
644
- (res) => res.ok ? res.json() : void 0
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",
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",