@softarc/native-federation-orchestrator 4.5.1 → 4.5.3

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/fesm2022/node.mjs CHANGED
@@ -1490,16 +1490,21 @@ var createRemoteInfoRepository = (config) => {
1490
1490
  const STORAGE = config.storage("remotes", {});
1491
1491
  if (config.clearStorage) STORAGE.clear();
1492
1492
  const _cache = STORAGE.get() ?? {};
1493
+ let _dirty = false;
1493
1494
  return {
1494
1495
  contains: function(remoteName) {
1495
1496
  return !!_cache[remoteName];
1496
1497
  },
1497
1498
  remove: function(remoteName) {
1498
- delete _cache[remoteName];
1499
+ if (remoteName in _cache) {
1500
+ delete _cache[remoteName];
1501
+ _dirty = true;
1502
+ }
1499
1503
  return this;
1500
1504
  },
1501
1505
  addOrUpdate: function(remoteName, remote) {
1502
1506
  _cache[remoteName] = remote;
1507
+ _dirty = true;
1503
1508
  return this;
1504
1509
  },
1505
1510
  tryGet: function(remoteName) {
@@ -1514,7 +1519,9 @@ var createRemoteInfoRepository = (config) => {
1514
1519
  return _cache;
1515
1520
  },
1516
1521
  commit: function() {
1522
+ if (!_dirty) return this;
1517
1523
  STORAGE.set(_cache);
1524
+ _dirty = false;
1518
1525
  return this;
1519
1526
  }
1520
1527
  };
@@ -1525,14 +1532,19 @@ var createScopedExternalsRepository = (config) => {
1525
1532
  const STORAGE = config.storage("scoped-externals", {});
1526
1533
  if (config.clearStorage) STORAGE.clear();
1527
1534
  const _cache = STORAGE.get() ?? {};
1535
+ let _dirty = false;
1528
1536
  return {
1529
1537
  addExternal: function(remoteName, external, version) {
1530
1538
  if (!_cache[remoteName]) _cache[remoteName] = {};
1531
1539
  _cache[remoteName][external] = version;
1540
+ _dirty = true;
1532
1541
  return this;
1533
1542
  },
1534
1543
  remove: function(remoteName) {
1535
- delete _cache[remoteName];
1544
+ if (remoteName in _cache) {
1545
+ delete _cache[remoteName];
1546
+ _dirty = true;
1547
+ }
1536
1548
  return this;
1537
1549
  },
1538
1550
  getAll: function() {
@@ -1542,7 +1554,9 @@ var createScopedExternalsRepository = (config) => {
1542
1554
  return Optional.of(_cache[remoteName]);
1543
1555
  },
1544
1556
  commit: function() {
1557
+ if (!_dirty) return this;
1545
1558
  STORAGE.set(_cache);
1559
+ _dirty = false;
1546
1560
  return this;
1547
1561
  }
1548
1562
  };
@@ -1559,8 +1573,9 @@ var createSharedExternalsRepository = (config) => {
1559
1573
  { [GLOBAL_SCOPE]: {} }
1560
1574
  );
1561
1575
  if (config.clearStorage) STORAGE.clear();
1562
- const _cache = STORAGE.get();
1576
+ const _cache = STORAGE.get() ?? { [GLOBAL_SCOPE]: {} };
1563
1577
  let _sawPoolTag = false;
1578
+ let _dirty = false;
1564
1579
  return {
1565
1580
  markPoolTagPresent: function() {
1566
1581
  _sawPoolTag = true;
@@ -1574,28 +1589,38 @@ var createSharedExternalsRepository = (config) => {
1574
1589
  addOrUpdate: function(externalName, external, shareScope) {
1575
1590
  if (!_cache[shareScope ?? GLOBAL_SCOPE]) _cache[shareScope ?? GLOBAL_SCOPE] = {};
1576
1591
  _cache[shareScope ?? GLOBAL_SCOPE][externalName] = external;
1592
+ _dirty = true;
1577
1593
  return this;
1578
1594
  },
1579
1595
  getScopes: function(o = { includeGlobal: true }) {
1580
1596
  if (o.includeGlobal) return Object.keys(_cache);
1581
1597
  return Object.keys(_cache).filter((s) => s !== GLOBAL_SCOPE);
1582
1598
  },
1583
- removeFromAllScopes: function(remoteName) {
1599
+ // Batched: one traversal of the graph per init instead of one per overridden remote.
1600
+ removeFromAllScopes: function(remoteNames) {
1601
+ if (remoteNames.size === 0) return;
1584
1602
  Object.values(_cache).forEach((scope) => {
1585
1603
  const removeExternals = [];
1586
1604
  Object.entries(scope).forEach(([name, external]) => {
1587
- const removeVersionIdx = [];
1588
- external.versions.forEach((version, i) => {
1589
- const versionRemoteIDX = version.remotes.findIndex((r) => r.name === remoteName);
1590
- if (~versionRemoteIDX) {
1591
- version.remotes.splice(versionRemoteIDX, 1);
1605
+ let removedVersion = false;
1606
+ for (let i = external.versions.length - 1; i >= 0; i--) {
1607
+ const remotes = external.versions[i].remotes;
1608
+ let keep = 0;
1609
+ for (let r = 0; r < remotes.length; r++) {
1610
+ if (remoteNames.has(remotes[r].name)) continue;
1611
+ remotes[keep++] = remotes[r];
1592
1612
  }
1593
- if (version.remotes.length === 0) removeVersionIdx.push(i);
1594
- });
1595
- if (removeVersionIdx.length > 0) {
1596
- for (let i = removeVersionIdx.length - 1; i >= 0; i--) {
1597
- external.versions.splice(removeVersionIdx[i], 1);
1613
+ if (keep !== remotes.length) {
1614
+ remotes.length = keep;
1615
+ _dirty = true;
1616
+ }
1617
+ if (remotes.length === 0) {
1618
+ external.versions.splice(i, 1);
1619
+ removedVersion = true;
1620
+ _dirty = true;
1598
1621
  }
1622
+ }
1623
+ if (removedVersion) {
1599
1624
  external.dirty = true;
1600
1625
  if (external.versions.length === 0) removeExternals.push(name);
1601
1626
  }
@@ -1619,7 +1644,9 @@ var createSharedExternalsRepository = (config) => {
1619
1644
  return Optional.of(_cache[shareScope ?? GLOBAL_SCOPE]?.[external]);
1620
1645
  },
1621
1646
  commit: function() {
1647
+ if (!_dirty) return this;
1622
1648
  STORAGE.set(_cache);
1649
+ _dirty = false;
1623
1650
  return this;
1624
1651
  }
1625
1652
  };
@@ -1630,17 +1657,21 @@ var createChunkRepository = (config) => {
1630
1657
  const STORAGE = config.storage("shared-chunks", {});
1631
1658
  if (config.clearStorage) STORAGE.clear();
1632
1659
  const _cache = STORAGE.get() ?? {};
1660
+ let _dirty = false;
1633
1661
  return {
1634
1662
  addOrReplace: function(remoteName, bundleName, chunks) {
1635
1663
  if (!_cache[remoteName]) _cache[remoteName] = {};
1636
1664
  _cache[remoteName][bundleName] = chunks;
1665
+ _dirty = true;
1637
1666
  return this;
1638
1667
  },
1639
1668
  tryGet: function(remoteName, bundleName) {
1640
1669
  return Optional.of(_cache[remoteName]?.[bundleName]);
1641
1670
  },
1642
1671
  commit: function() {
1672
+ if (!_dirty) return this;
1643
1673
  STORAGE.set(_cache);
1674
+ _dirty = false;
1644
1675
  return this;
1645
1676
  }
1646
1677
  };
@@ -1830,10 +1861,9 @@ var globalThisStorageEntry = (namespace) => (key, initialValue) => {
1830
1861
  globalThis[namespace] = {};
1831
1862
  }
1832
1863
  const storage = globalThis[namespace];
1833
- if (!storage[key]) storage[key] = initialValue;
1834
1864
  const entry = {
1835
1865
  get() {
1836
- return cloneEntry(key, storage[key]);
1866
+ return cloneEntry(key, key in storage ? storage[key] : initialValue);
1837
1867
  },
1838
1868
  set(value) {
1839
1869
  storage[key] = cloneEntry(key, value);
@@ -1857,6 +1887,7 @@ var createStorageConfig = (override) => ({
1857
1887
  var defaultProfile = {
1858
1888
  latestSharedExternal: false,
1859
1889
  skipInvalidExternalVersions: false,
1890
+ scopeUncoveredEntrypoints: false,
1860
1891
  overrideCachedRemotes: "init-only",
1861
1892
  overrideCachedRemotesIfURLMatches: false
1862
1893
  };
@@ -1868,13 +1899,15 @@ var createModeConfig = (override) => {
1868
1899
  strictExternalCompatibility: override.strict,
1869
1900
  strictExternalSameVersionCompatibility: override.strict,
1870
1901
  strictExternalVersion: override.strict,
1871
- strictImportMap: override.strict
1902
+ strictImportMap: override.strict,
1903
+ strictEntryPointCoverage: override.strict
1872
1904
  } : {
1873
1905
  strictRemoteEntry: override.strict?.strictRemoteEntry ?? false,
1874
1906
  strictExternalCompatibility: override.strict?.strictExternalCompatibility ?? false,
1875
1907
  strictExternalSameVersionCompatibility: override.strict?.strictExternalSameVersionCompatibility ?? false,
1876
1908
  strictExternalVersion: override.strict?.strictExternalVersion ?? false,
1877
- strictImportMap: override.strict?.strictImportMap ?? false
1909
+ strictImportMap: override.strict?.strictImportMap ?? false,
1910
+ strictEntryPointCoverage: override.strict?.strictEntryPointCoverage ?? false
1878
1911
  };
1879
1912
  return {
1880
1913
  strict: strictnessConfig,
@@ -2020,19 +2053,71 @@ function createGetRemoteEntries(config, ports) {
2020
2053
  }
2021
2054
  }
2022
2055
 
2056
+ // src/lib/core/1.domain/externals/basis.ts
2057
+ var coverage = (remote) => Object.keys(remote.entries).length;
2058
+ function isBetterBasis(candidate, basis) {
2059
+ if (basis.cached !== candidate.cached) return candidate.cached;
2060
+ return coverage(candidate) > coverage(basis);
2061
+ }
2062
+ function addRemoteToVersion(version, remote, isHost = false) {
2063
+ if (isHost) {
2064
+ version.host = true;
2065
+ version.remotes.unshift(remote);
2066
+ return;
2067
+ }
2068
+ const basis = version.remotes[0];
2069
+ if (basis && !version.host && isBetterBasis(remote, basis)) {
2070
+ version.remotes.unshift(remote);
2071
+ return;
2072
+ }
2073
+ version.remotes.push(remote);
2074
+ }
2075
+ function versionDemands(version) {
2076
+ if (version.remotes.length < 2) return version.remotes;
2077
+ const distinct = /* @__PURE__ */ new Map();
2078
+ for (const remote of version.remotes) {
2079
+ const key = `${remote.requiredVersion}|${remote.strictVersion}|${remote.cached}`;
2080
+ if (!distinct.has(key)) distinct.set(key, remote);
2081
+ }
2082
+ return Array.from(distinct.values());
2083
+ }
2084
+ function findVersionForTag(versions, tag) {
2085
+ let scoped;
2086
+ for (const version of versions) {
2087
+ if (version.tag !== tag) continue;
2088
+ if (version.action !== "scope") return version;
2089
+ scoped ??= version;
2090
+ }
2091
+ return scoped;
2092
+ }
2093
+ function uncoveredEntrypoints(remote, basis) {
2094
+ return Object.keys(remote.entries).filter((entrypoint) => !(entrypoint in basis));
2095
+ }
2096
+ function countUncoveredEntrypoints(remote, basis) {
2097
+ let uncovered = 0;
2098
+ for (const entrypoint in remote.entries) {
2099
+ if (!(entrypoint in basis)) uncovered++;
2100
+ }
2101
+ return uncovered;
2102
+ }
2103
+
2023
2104
  // src/lib/core/2.app/steps/store-remote-entry.ts
2105
+ function createRemoveCachedRemoteEntries(ports) {
2106
+ return (remoteNames) => {
2107
+ if (remoteNames.size === 0) return;
2108
+ for (const remoteName of remoteNames) {
2109
+ ports.remoteInfoRepo.remove(remoteName);
2110
+ ports.scopedExternalsRepo.remove(remoteName);
2111
+ }
2112
+ ports.sharedExternalsRepo.removeFromAllScopes(remoteNames);
2113
+ };
2114
+ }
2024
2115
  function createStoreRemoteEntry(config, ports, logStep) {
2025
2116
  return (remoteEntry, onSharedExternal) => {
2026
- if (remoteEntry?.override) removeCachedRemoteEntry(remoteEntry);
2027
2117
  addRemoteInfoToStorage(remoteEntry);
2028
2118
  addExternalsToStorage(remoteEntry, onSharedExternal);
2029
2119
  addSharedChunksToStorage(remoteEntry);
2030
2120
  };
2031
- function removeCachedRemoteEntry(remoteEntry) {
2032
- ports.remoteInfoRepo.remove(remoteEntry.name);
2033
- ports.scopedExternalsRepo.remove(remoteEntry.name);
2034
- ports.sharedExternalsRepo.removeFromAllScopes(remoteEntry.name);
2035
- }
2036
2121
  function addRemoteInfoToStorage({ name, url, exposes, integrity }) {
2037
2122
  ports.remoteInfoRepo.addOrUpdate(name, {
2038
2123
  scopeUrl: getScope(url),
@@ -2132,14 +2217,35 @@ function createStoreRemoteEntry(config, ports, logStep) {
2132
2217
  // src/lib/core/2.app/steps/process-remote-entries.ts
2133
2218
  function createProcessRemoteEntries(config, ports) {
2134
2219
  const storeRemoteEntry = createStoreRemoteEntry(config, ports, 2);
2220
+ const removeCachedRemoteEntries = createRemoveCachedRemoteEntries(ports);
2135
2221
  return (remoteEntries) => {
2136
2222
  try {
2137
- remoteEntries.forEach((remoteEntry) => storeRemoteEntry(remoteEntry, addSharedExternal));
2223
+ const evictPerEntry = evictOverriddenRemotes(remoteEntries);
2224
+ remoteEntries.forEach((remoteEntry) => {
2225
+ if (remoteEntry?.override && evictPerEntry.has(remoteEntry.name)) {
2226
+ removeCachedRemoteEntries(/* @__PURE__ */ new Set([remoteEntry.name]));
2227
+ }
2228
+ storeRemoteEntry(remoteEntry, addSharedExternal);
2229
+ });
2138
2230
  return Promise.resolve(remoteEntries);
2139
2231
  } catch (e) {
2140
2232
  return Promise.reject(e);
2141
2233
  }
2142
2234
  };
2235
+ function evictOverriddenRemotes(remoteEntries) {
2236
+ const batched = /* @__PURE__ */ new Set();
2237
+ const repeated = /* @__PURE__ */ new Set();
2238
+ const seen = /* @__PURE__ */ new Set();
2239
+ for (const remoteEntry of remoteEntries) {
2240
+ if (!remoteEntry) continue;
2241
+ if (seen.has(remoteEntry.name)) repeated.add(remoteEntry.name);
2242
+ seen.add(remoteEntry.name);
2243
+ if (remoteEntry.override) batched.add(remoteEntry.name);
2244
+ }
2245
+ for (const remoteName of repeated) batched.delete(remoteName);
2246
+ removeCachedRemoteEntries(batched);
2247
+ return repeated;
2248
+ }
2143
2249
  function addSharedExternal(remoteEntry, _sharedInfo, {
2144
2250
  tag,
2145
2251
  remote,
@@ -2148,15 +2254,12 @@ function createProcessRemoteEntries(config, ports) {
2148
2254
  assertSameVersionCompatibility,
2149
2255
  commit
2150
2256
  }) {
2151
- const matchingVersion = cached2.versions.find((version) => version.tag === tag);
2257
+ if (scopeType !== "strict") cached2.dirty = true;
2258
+ const matchingVersion = findVersionForTag(cached2.versions, tag);
2152
2259
  if (matchingVersion) {
2153
2260
  assertSameVersionCompatibility(matchingVersion);
2154
- if (!matchingVersion.host && !!remoteEntry?.host) {
2155
- matchingVersion.host = true;
2156
- matchingVersion.remotes.unshift(remote);
2157
- } else matchingVersion.remotes.push(remote);
2261
+ addRemoteToVersion(matchingVersion, remote, !matchingVersion.host && !!remoteEntry?.host);
2158
2262
  } else {
2159
- if (scopeType !== "strict") cached2.dirty = true;
2160
2263
  cached2.versions.push({
2161
2264
  tag,
2162
2265
  action: scopeType === "strict" ? "share" : "skip",
@@ -2171,13 +2274,23 @@ function createProcessRemoteEntries(config, ports) {
2171
2274
  // src/lib/core/2.app/steps/determine-shared-externals.ts
2172
2275
  function createDetermineSharedExternals(config, ports) {
2173
2276
  return () => {
2277
+ const memo = /* @__PURE__ */ new Map();
2278
+ const isCompatible = (tag, requiredVersion) => {
2279
+ const key = `${tag}|${requiredVersion}`;
2280
+ let hit = memo.get(key);
2281
+ if (hit === void 0) {
2282
+ hit = ports.versionCheck.isCompatible(tag, requiredVersion);
2283
+ memo.set(key, hit);
2284
+ }
2285
+ return hit;
2286
+ };
2174
2287
  for (const shareScope of ports.sharedExternalsRepo.getScopes()) {
2175
2288
  const sharedExternals = ports.sharedExternalsRepo.getFromScope(shareScope);
2176
2289
  try {
2177
2290
  Object.entries(sharedExternals).filter(([_, e]) => e.dirty).forEach(
2178
2291
  ([name, external]) => ports.sharedExternalsRepo.addOrUpdate(
2179
2292
  name,
2180
- setVersionActions(name, external),
2293
+ setVersionActions(name, external, isCompatible),
2181
2294
  shareScope
2182
2295
  )
2183
2296
  );
@@ -2200,24 +2313,47 @@ function createDetermineSharedExternals(config, ports) {
2200
2313
  }
2201
2314
  return Promise.resolve();
2202
2315
  };
2203
- function setVersionActions(externalName, external) {
2316
+ function uncoveredTears(external, winner, accepts) {
2317
+ const basis = winner.remotes[0].entries;
2318
+ return external.versions.reduce((sum, v) => {
2319
+ if (v === winner) return sum;
2320
+ if (!accepts(v, winner.tag)) return sum;
2321
+ return sum + v.remotes.reduce((n, r) => n + countUncoveredEntrypoints(r, basis), 0);
2322
+ }, 0);
2323
+ }
2324
+ function setVersionActions(externalName, external, isCompatible) {
2204
2325
  if (external.versions.length === 1) {
2205
2326
  external.versions[0].action = "share";
2327
+ applyEntrypointCoveragePolicy(externalName, external);
2206
2328
  external.dirty = false;
2207
2329
  return external;
2208
2330
  }
2331
+ const demands = new Map(
2332
+ external.versions.map((v) => [v, versionDemands(v)])
2333
+ );
2334
+ const accepts = (version, tag) => demands.get(version).every((d) => isCompatible(tag, d.requiredVersion));
2335
+ const objector = (version, tag) => demands.get(version).find((d) => d.strictVersion && !isCompatible(tag, d.requiredVersion));
2209
2336
  let sharedVersion = external.versions.find((v) => v.host);
2210
2337
  if (!sharedVersion && config.profile.latestSharedExternal) {
2211
2338
  sharedVersion = external.versions[0];
2212
2339
  }
2213
2340
  if (!sharedVersion) {
2214
2341
  let leastExtraDownloads = Number.MAX_VALUE;
2342
+ let leastTears = Number.MAX_VALUE;
2215
2343
  external.versions.forEach((vA) => {
2216
2344
  const extraDownloads = external.versions.filter(
2217
- (vB) => !vB.remotes[0].cached && vB.remotes[0].strictVersion && !ports.versionCheck.isCompatible(vA.tag, vB.remotes[0].requiredVersion)
2345
+ (vB) => demands.get(vB).some((d) => !d.cached && d.strictVersion && !isCompatible(vA.tag, d.requiredVersion))
2218
2346
  ).length;
2219
2347
  if (extraDownloads < leastExtraDownloads) {
2220
2348
  leastExtraDownloads = extraDownloads;
2349
+ leastTears = uncoveredTears(external, vA, accepts);
2350
+ sharedVersion = vA;
2351
+ return;
2352
+ }
2353
+ if (extraDownloads > leastExtraDownloads) return;
2354
+ const tears = uncoveredTears(external, vA, accepts);
2355
+ if (tears < leastTears) {
2356
+ leastTears = tears;
2221
2357
  sharedVersion = vA;
2222
2358
  }
2223
2359
  });
@@ -2226,23 +2362,81 @@ function createDetermineSharedExternals(config, ports) {
2226
2362
  throw new NFError(`[${externalName}] Could not determine shared version!`);
2227
2363
  }
2228
2364
  external.versions.forEach((v) => {
2229
- if (ports.versionCheck.isCompatible(sharedVersion.tag, v.remotes[0].requiredVersion)) {
2365
+ if (accepts(v, sharedVersion.tag)) {
2230
2366
  v.action = "skip";
2231
2367
  return;
2232
2368
  }
2233
- if (config.strict.strictExternalCompatibility && v.remotes[0].strictVersion) {
2369
+ const strict = objector(v, sharedVersion.tag);
2370
+ if (config.strict.strictExternalCompatibility && strict) {
2234
2371
  config.log.error(
2235
2372
  3,
2236
- `[${v.remotes[0].name}][${externalName}@${v.tag}] Is not compatible with requiredRange '${sharedVersion.remotes[0]?.requiredVersion}' from remote '${sharedVersion.remotes[0].name}'.`
2373
+ `[${strict.name}][${externalName}@${v.tag}] Is not compatible with requiredRange '${strict.requiredVersion}' of shared ${externalName}@${sharedVersion.tag}.`
2237
2374
  );
2238
2375
  throw new NFError(`External ${externalName}@${v.tag} could not be shared.`);
2239
2376
  }
2240
- v.action = v.remotes[0].strictVersion ? "scope" : "skip";
2377
+ v.action = strict ? "scope" : "skip";
2241
2378
  });
2242
2379
  sharedVersion.action = "share";
2380
+ applyEntrypointCoveragePolicy(externalName, external);
2243
2381
  external.dirty = false;
2244
2382
  return external;
2245
2383
  }
2384
+ function applyEntrypointCoveragePolicy(externalName, external) {
2385
+ const { strictEntryPointCoverage } = config.strict;
2386
+ if (!strictEntryPointCoverage && !config.profile.scopeUncoveredEntrypoints) return;
2387
+ const tears = findTears(external);
2388
+ if (tears.length === 0) return;
2389
+ if (strictEntryPointCoverage) {
2390
+ const { version, remote, uncovered } = tears[0];
2391
+ config.log.error(
2392
+ 3,
2393
+ `[${externalName}@${version.tag}][${remote.name}] Entrypoints not covered by the shared version: ${uncovered.join(", ")}.`
2394
+ );
2395
+ throw new NFError(
2396
+ `External ${externalName} could not be shared without tearing entrypoints.`
2397
+ );
2398
+ }
2399
+ scopeTornRemotes(externalName, external, tears);
2400
+ }
2401
+ function findTears(external) {
2402
+ const shared = external.versions.find((v) => v.action === "share");
2403
+ if (!shared) return [];
2404
+ const basis = shared.remotes[0].entries;
2405
+ const tears = [];
2406
+ for (const version of external.versions) {
2407
+ if (version.action === "scope") continue;
2408
+ version.remotes.forEach((remote, index) => {
2409
+ if (version === shared && index === 0) return;
2410
+ const uncovered = uncoveredEntrypoints(remote, basis);
2411
+ if (uncovered.length > 0) tears.push({ version, remote, uncovered });
2412
+ });
2413
+ }
2414
+ return tears;
2415
+ }
2416
+ function scopeTornRemotes(externalName, external, tears) {
2417
+ const torn = new Set(tears.map((t) => t.remote));
2418
+ const demotedByTag = /* @__PURE__ */ new Map();
2419
+ for (const { version, remote, uncovered } of tears) {
2420
+ const group = demotedByTag.get(version.tag);
2421
+ if (group) group.push(remote);
2422
+ else demotedByTag.set(version.tag, [remote]);
2423
+ config.log.debug(
2424
+ 3,
2425
+ `[${externalName}@${version.tag}][${remote.name}] Scoped: entrypoints not covered by the shared version: ${uncovered.join(", ")}.`
2426
+ );
2427
+ }
2428
+ for (const version of external.versions) {
2429
+ if (version.remotes.some((r) => torn.has(r))) {
2430
+ version.remotes = version.remotes.filter((r) => !torn.has(r));
2431
+ }
2432
+ }
2433
+ external.versions = external.versions.filter((v) => v.remotes.length > 0);
2434
+ for (const [tag, remotes] of demotedByTag) {
2435
+ const scoped = external.versions.find((v) => v.tag === tag && v.action === "scope");
2436
+ if (scoped) scoped.remotes.push(...remotes);
2437
+ else external.versions.push({ tag, host: false, action: "scope", remotes });
2438
+ }
2439
+ }
2246
2440
  }
2247
2441
 
2248
2442
  // src/lib/core/2.app/steps/pooling/pool-graph.ts
@@ -2347,9 +2541,6 @@ function buildPools(sharedExternals, useAutoExternalPooling, log) {
2347
2541
  }
2348
2542
 
2349
2543
  // src/lib/core/2.app/steps/pooling/pool.util.ts
2350
- function versionForRemote(member, remote) {
2351
- return member.external.versions.find((v) => v.remotes.some((r) => r.name === remote));
2352
- }
2353
2544
  function remotesInPool(members) {
2354
2545
  return [
2355
2546
  ...new Set(members.flatMap((m) => m.external.versions.flatMap((v) => v.remotes.map((r) => r.name))))
@@ -2357,57 +2548,12 @@ function remotesInPool(members) {
2357
2548
  }
2358
2549
 
2359
2550
  // src/lib/core/2.app/steps/pooling/pool-shared-externals.ts
2360
- function poolWinners(members) {
2361
- return members.map((member) => {
2362
- const share = member.external.versions.find((v) => v.action === "share");
2363
- return {
2364
- member,
2365
- tag: share?.tag ?? "",
2366
- providers: new Set((share?.remotes ?? []).map((r) => r.name))
2367
- };
2368
- });
2369
- }
2370
- function selectCoverageAnchor(winners) {
2371
- const coverage = /* @__PURE__ */ new Map();
2372
- for (const winner of winners)
2373
- for (const remote of winner.providers) coverage.set(remote, (coverage.get(remote) ?? 0) + 1);
2374
- if (coverage.size === 0) return void 0;
2375
- const anchorRemote = [...coverage.keys()].sort(
2376
- (a, b) => coverage.get(b) - coverage.get(a) || a.localeCompare(b)
2377
- )[0];
2378
- return {
2379
- anchorRemote,
2380
- tagPerMember: Object.fromEntries(
2381
- winners.filter((w) => w.providers.has(anchorRemote)).map((w) => [w.member.name, w.tag])
2382
- )
2383
- };
2384
- }
2385
- function classifyPoolConservative(members, anchor) {
2386
- const classification = /* @__PURE__ */ new Map();
2387
- for (const remote of remotesInPool(members)) {
2388
- let coverageForced = false;
2389
- let verdict = "follow";
2390
- for (const member of members) {
2391
- const version = versionForRemote(member, remote);
2392
- if (!version) continue;
2393
- const anchorTag = anchor.tagPerMember[member.name];
2394
- if (!anchorTag) {
2395
- coverageForced = true;
2396
- continue;
2397
- }
2398
- if (version.tag === anchorTag) continue;
2399
- if (version.action === "scope") {
2400
- verdict = "scope-incompat";
2401
- break;
2402
- }
2403
- }
2404
- classification.set(
2405
- remote,
2406
- verdict === "scope-incompat" ? "scope-incompat" : coverageForced ? "scope-coverage" : "follow"
2407
- );
2408
- }
2409
- classification.set(anchor.anchorRemote, "follow");
2410
- return classification;
2551
+ function islandedRemotes(members) {
2552
+ const islanded = /* @__PURE__ */ new Set();
2553
+ for (const member of members)
2554
+ for (const version of member.external.versions)
2555
+ if (version.action === "scope") for (const remote of version.remotes) islanded.add(remote.name);
2556
+ return islanded;
2411
2557
  }
2412
2558
  function createPoolSharedExternals(config, ports) {
2413
2559
  return () => {
@@ -2443,73 +2589,57 @@ function createPoolSharedExternals(config, ports) {
2443
2589
  if (members.length < 2) return;
2444
2590
  const allRemotes = remotesInPool(members);
2445
2591
  if (allRemotes.length < 2) return;
2446
- const anchor = selectCoverageAnchor(poolWinners(members));
2447
- if (!anchor) return;
2448
- const classification = classifyPoolConservative(members, anchor);
2592
+ const islanded = islandedRemotes(members);
2449
2593
  config.log.debug(
2450
2594
  3,
2451
- `[${scope}][pool:${poolName}] members=[${members.map((m) => m.name).join(", ")}], anchor=${anchor.anchorRemote}, remotes={${allRemotes.map((r) => `${r}:${classification.get(r)}`).join(", ")}}`
2595
+ `[${scope}][pool:${poolName}] ${members.length} members across ${allRemotes.length} remotes, islanded={${[...islanded].join(", ") || "\u2205"}}
2596
+ ` + members.map((m) => ` - ${m.name}`).join("\n")
2452
2597
  );
2453
- const incompatForced = allRemotes.some((r) => classification.get(r) === "scope-incompat");
2454
- if (incompatForced && config.strict.strictExternalCompatibility) {
2598
+ if (islanded.size > 0 && config.strict.strictExternalCompatibility) {
2455
2599
  config.log.error(
2456
2600
  3,
2457
- `[${scope}][pool:${poolName}] A remote is version-incompatible with anchor '${anchor.anchorRemote}'.`
2601
+ `[${scope}][pool:${poolName}] version-incompatible remotes cannot be pooled: {${[...islanded].join(", ")}}.`
2458
2602
  );
2459
2603
  throw new NFError(`Could not pool '${poolName}' in scope ${scope}.`);
2460
2604
  }
2461
2605
  for (const member of members) {
2462
- const rebuilt = rebuildMember(member, anchor, classification);
2606
+ const rebuilt = rebuildMember(member, islanded);
2463
2607
  warnIfScopedOnly(poolName, member.name, rebuilt, scope);
2464
2608
  ports.sharedExternalsRepo.addOrUpdate(member.name, rebuilt, scope);
2465
2609
  }
2466
2610
  }
2467
2611
  function warnIfScopedOnly(poolName, memberName, rebuilt, scope) {
2468
2612
  if (rebuilt.versions.some((v) => v.action === "share")) return;
2469
- const copies = rebuilt.versions.reduce((n, v) => n + v.remotes.length, 0);
2613
+ const consumers = new Set(rebuilt.versions.flatMap((v) => v.remotes.map((r) => r.name))).size;
2614
+ if (consumers < 2) return;
2470
2615
  config.log.warn(
2471
2616
  3,
2472
- `[${scope}][pool:${poolName}] '${memberName}' is scoped-only \u2014 no coherent shared build provides it; ${copies} remotes download their own copy.`
2617
+ `[${scope}][pool:${poolName}] '${memberName}' is scoped-only \u2014 no coherent shared build provides it; ${consumers} remotes download their own copy.`
2473
2618
  );
2474
2619
  }
2475
- function rebuildMember(member, anchor, classification) {
2476
- const anchorTag = anchor.tagPerMember[member.name];
2620
+ function rebuildMember(member, islanded) {
2477
2621
  const entries = member.external.versions.flatMap(
2478
- (v) => v.remotes.map((meta) => ({ remote: meta.name, tag: v.tag, host: v.host, meta }))
2479
- );
2480
- if (!anchorTag) {
2481
- return {
2482
- dirty: false,
2483
- versions: entries.length > 0 ? [
2484
- {
2485
- tag: entries[0].tag,
2486
- host: false,
2487
- action: "scope",
2488
- remotes: entries.map((e) => e.meta)
2489
- }
2490
- ] : []
2491
- };
2492
- }
2493
- const anchorMeta = entries.find(
2494
- (e) => e.remote === anchor.anchorRemote && e.tag === anchorTag
2495
- ).meta;
2496
- const anchorHost = member.external.versions.find((v) => v.tag === anchorTag)?.host ?? false;
2497
- const scopes = (e) => {
2498
- const cls = classification.get(e.remote);
2499
- return cls === "scope-incompat" || cls === "scope-coverage" && e.tag !== anchorTag;
2500
- };
2501
- const shareOnAnchor = entries.filter(
2502
- (e) => e.remote !== anchor.anchorRemote && e.tag === anchorTag && !scopes(e)
2622
+ (v) => v.remotes.map((meta) => ({ remote: meta.name, tag: v.tag, host: v.host, action: v.action, meta }))
2503
2623
  );
2504
- const shareVersion = {
2505
- tag: anchorTag,
2506
- host: anchorHost,
2507
- action: "share",
2508
- remotes: [anchorMeta, ...shareOnAnchor.map((e) => e.meta)]
2509
- };
2624
+ const isScoped = (e) => islanded.has(e.remote) || e.action === "scope";
2625
+ let scoped = entries.filter(isScoped);
2626
+ let clean = entries.filter((e) => !isScoped(e));
2627
+ if (!clean.some((e) => e.action === "share")) {
2628
+ scoped = [...scoped, ...clean];
2629
+ clean = [];
2630
+ }
2631
+ const shareEntries = clean.filter((e) => e.action === "share");
2632
+ const shareVersion = shareEntries.length > 0 ? [
2633
+ {
2634
+ tag: shareEntries[0].tag,
2635
+ host: shareEntries[0].host,
2636
+ action: "share",
2637
+ remotes: shareEntries.map((e) => e.meta)
2638
+ }
2639
+ ] : [];
2510
2640
  const skipByTag = /* @__PURE__ */ new Map();
2511
- for (const e of entries) {
2512
- if (e.tag === anchorTag || scopes(e)) continue;
2641
+ for (const e of clean) {
2642
+ if (e.action !== "skip") continue;
2513
2643
  const version = skipByTag.get(e.tag) ?? {
2514
2644
  tag: e.tag,
2515
2645
  host: e.host,
@@ -2519,16 +2649,25 @@ function createPoolSharedExternals(config, ports) {
2519
2649
  version.remotes.push(e.meta);
2520
2650
  skipByTag.set(e.tag, version);
2521
2651
  }
2522
- const scoped = entries.filter((e) => scopes(e));
2523
- const scopeVersion = scoped.length > 0 ? [{ tag: anchorTag, host: false, action: "scope", remotes: scoped.map((e) => e.meta) }] : [];
2652
+ const scopeByTag = /* @__PURE__ */ new Map();
2653
+ for (const e of scoped) {
2654
+ const version = scopeByTag.get(e.tag) ?? {
2655
+ tag: e.tag,
2656
+ host: false,
2657
+ action: "scope",
2658
+ remotes: []
2659
+ };
2660
+ version.remotes.push(e.meta);
2661
+ scopeByTag.set(e.tag, version);
2662
+ }
2524
2663
  return {
2525
2664
  dirty: false,
2526
- versions: [shareVersion, ...skipByTag.values(), ...scopeVersion]
2665
+ versions: [...shareVersion, ...skipByTag.values(), ...scopeByTag.values()]
2527
2666
  };
2528
2667
  }
2529
2668
  }
2530
2669
 
2531
- // node_modules/.pnpm/@softarc+native-federation@4.3.1_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/domain/core/chunk.js
2670
+ // node_modules/.pnpm/@softarc+native-federation@4.3.2_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/domain/core/chunk.js
2532
2671
  var CHUNK_PREFIX = "@nf-internal";
2533
2672
  function toChunkImport(fileName) {
2534
2673
  if (fileName.startsWith("./")) {
@@ -2592,6 +2731,7 @@ function createGenerateImportMap(config, ports) {
2592
2731
  for (const [externalName, external] of Object.entries(sharedExternals)) {
2593
2732
  let override = void 0;
2594
2733
  let overrideScope = void 0;
2734
+ const cachedBefore = cachedFingerprint(external);
2595
2735
  for (const version of external.versions) {
2596
2736
  if (version.action === "scope") {
2597
2737
  for (const remote of version.remotes) {
@@ -2635,14 +2775,30 @@ function createGenerateImportMap(config, ports) {
2635
2775
  for (const m of mappings) {
2636
2776
  addToScope(importMap, rScope, { [m.packageName]: m.url });
2637
2777
  }
2778
+ for (const [packageName, file] of Object.entries(r.entries)) {
2779
+ if (packageName in source.entries) continue;
2780
+ if (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints) {
2781
+ warnUncoveredEntrypoint(shareScope, externalName, r.name, packageName);
2782
+ continue;
2783
+ }
2784
+ const url = join(rScope, file);
2785
+ addToScope(importMap, rScope, { [packageName]: url });
2786
+ addIntegrity(importMap, url, r.name, file);
2787
+ r.cached = true;
2788
+ }
2638
2789
  });
2639
2790
  for (const m of mappings) {
2640
2791
  addIntegrity(importMap, m.url, m.name, m.file);
2641
2792
  }
2642
2793
  }
2643
- ports.sharedExternalsRepo.addOrUpdate(externalName, external, shareScope);
2794
+ if (cachedFingerprint(external) !== cachedBefore) {
2795
+ ports.sharedExternalsRepo.addOrUpdate(externalName, external, shareScope);
2796
+ }
2644
2797
  }
2645
2798
  }
2799
+ function cachedFingerprint(external) {
2800
+ return external.versions.map((v) => v.remotes.map((r) => r.cached ? 1 : 0).join("")).join("|");
2801
+ }
2646
2802
  function findOverride(external, shareScope, externalName) {
2647
2803
  const sharedVersions = external.versions.filter((v) => v.action === "share");
2648
2804
  const scopedExternalName = `${shareScope}.${externalName}`;
@@ -2674,6 +2830,7 @@ function createGenerateImportMap(config, ports) {
2674
2830
  function addGlobalSharedExternals(importMap, chunkBundles) {
2675
2831
  const sharedExternals = ports.sharedExternalsRepo.getFromScope();
2676
2832
  for (const [externalName, external] of Object.entries(sharedExternals)) {
2833
+ const cachedBefore = cachedFingerprint(external);
2677
2834
  for (const version of external.versions) {
2678
2835
  if (version.action === "skip") continue;
2679
2836
  if (version.action === "scope") {
@@ -2701,11 +2858,44 @@ function createGenerateImportMap(config, ports) {
2701
2858
  }
2702
2859
  registerBundleChunks(chunkBundles, version.remotes[0].name, version.remotes[0].bundle);
2703
2860
  version.remotes[0].cached = true;
2861
+ selfFillUncovered(importMap, chunkBundles, externalName, version.remotes, 1);
2862
+ }
2863
+ for (const version of external.versions) {
2864
+ if (version.action !== "skip") continue;
2865
+ selfFillUncovered(importMap, chunkBundles, externalName, version.remotes);
2866
+ }
2867
+ if (cachedFingerprint(external) !== cachedBefore) {
2868
+ ports.sharedExternalsRepo.addOrUpdate(externalName, external);
2704
2869
  }
2705
- ports.sharedExternalsRepo.addOrUpdate(externalName, external);
2706
2870
  }
2707
2871
  return importMap;
2708
2872
  }
2873
+ function selfFillUncovered(importMap, chunkBundles, externalName, remotes, from = 0) {
2874
+ for (let i = from; i < remotes.length; i++) {
2875
+ const remote = remotes[i];
2876
+ for (const [packageName, file] of Object.entries(remote.entries)) {
2877
+ if (importMap.imports[packageName]) continue;
2878
+ if (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints) {
2879
+ warnUncoveredEntrypoint(GLOBAL_SCOPE, externalName, remote.name, packageName);
2880
+ continue;
2881
+ }
2882
+ const scope = getScope2(GLOBAL_SCOPE, remote.name, externalName);
2883
+ const url = join(scope, file);
2884
+ addToGlobal(importMap, { [packageName]: url });
2885
+ addIntegrity(importMap, url, remote.name, file);
2886
+ registerBundleChunks(chunkBundles, remote.name, remote.bundle);
2887
+ remote.cached = true;
2888
+ }
2889
+ }
2890
+ }
2891
+ function warnUncoveredEntrypoint(scope, externalName, remoteName, packageName) {
2892
+ const msg = `[${scope}][${externalName}][${remoteName}] Entrypoint '${packageName}' is not covered by the shared version.`;
2893
+ if (config.strict.strictEntryPointCoverage || config.strict.strictImportMap) {
2894
+ config.log.error(4, msg);
2895
+ throw new NFError("Could not create ImportMap.");
2896
+ }
2897
+ config.log.warn(4, msg);
2898
+ }
2709
2899
  function notifyDuplicateGlobalExternal(externalName) {
2710
2900
  if (config.strict.strictImportMap) {
2711
2901
  config.log.error(4, `[${externalName}] Shared external has multiple shared versions.`);
@@ -2889,18 +3079,23 @@ function createGetRemoteEntry(config, ports) {
2889
3079
  // src/lib/core/2.app/steps/update-cache.ts
2890
3080
  function createUpdateCache(config, ports) {
2891
3081
  const storeRemoteEntry = createStoreRemoteEntry(config, ports, 8);
3082
+ const removeCachedRemoteEntries = createRemoveCachedRemoteEntries(ports);
2892
3083
  return (remoteEntry) => {
2893
3084
  try {
2894
3085
  const actions = {};
3086
+ if (remoteEntry?.override) removeCachedRemoteEntries(/* @__PURE__ */ new Set([remoteEntry.name]));
2895
3087
  storeRemoteEntry(remoteEntry, (entry, external, ctx) => {
2896
3088
  const { action, sharedVersion } = resolveSharedExternal(entry, external, ctx);
2897
3089
  actions[external.packageName] = { action };
2898
- if (action === "skip" && external.shareScope && sharedVersion?.remotes[0]?.entries) {
2899
- actions[external.packageName].override = resolveOverrideEntries(
2900
- entry,
2901
- external,
2902
- sharedVersion
2903
- );
3090
+ if (action === "skip" && sharedVersion?.remotes[0]?.entries) {
3091
+ actions[external.packageName].covered = Object.keys(sharedVersion.remotes[0].entries);
3092
+ if (external.shareScope) {
3093
+ actions[external.packageName].override = resolveOverrideEntries(
3094
+ entry,
3095
+ external,
3096
+ sharedVersion
3097
+ );
3098
+ }
2904
3099
  }
2905
3100
  });
2906
3101
  return Promise.resolve({ entry: remoteEntry, actions });
@@ -2928,10 +3123,29 @@ function createUpdateCache(config, ports) {
2928
3123
  }
2929
3124
  config.log.warn(8, errorMsg);
2930
3125
  }
2931
- const matchingVersion = cached2.versions.find((cached3) => cached3.tag === tag);
2932
- if (matchingVersion) {
3126
+ if (action === "skip" && sharedVersion) {
3127
+ const uncovered = uncoveredEntrypoints(remote, sharedVersion.remotes[0].entries);
3128
+ if (uncovered.length > 0) {
3129
+ const msg = `[${sharedInfo.shareScope ?? GLOBAL_SCOPE}][${remoteEntry.name}][${sharedInfo.packageName}] Entrypoints not covered by the shared version: ${uncovered.join(", ")}.`;
3130
+ if (config.strict.strictEntryPointCoverage) {
3131
+ config.log.error(8, msg);
3132
+ throw new NFError(`Could not process remote '${remoteEntry.name}'`);
3133
+ }
3134
+ if (config.profile.scopeUncoveredEntrypoints) {
3135
+ config.log.debug(8, msg);
3136
+ action = "scope";
3137
+ }
3138
+ }
3139
+ }
3140
+ const matchingVersion = findVersionForTag(cached2.versions, tag);
3141
+ if (action === "scope") {
3142
+ remote.cached = true;
3143
+ const scoped = cached2.versions.find((v) => v.tag === tag && v.action === "scope");
3144
+ if (scoped) scoped.remotes.push(remote);
3145
+ else cached2.versions.push({ tag, action, host: false, remotes: [remote] });
3146
+ } else if (matchingVersion) {
2933
3147
  assertSameVersionCompatibility(matchingVersion);
2934
- matchingVersion.remotes.push(remote);
3148
+ addRemoteToVersion(matchingVersion, remote);
2935
3149
  } else {
2936
3150
  if (!sharedVersion) action = "share";
2937
3151
  remote.cached = action !== "skip";
@@ -2992,8 +3206,6 @@ function createPoolDynamicExternals(config) {
2992
3206
  const memberActions = members.map((name) => actions[name].action);
2993
3207
  if (memberActions.includes("scope")) {
2994
3208
  members.forEach(scope);
2995
- } else if (memberActions.includes("share") && memberActions.includes("skip")) {
2996
- members.filter((name) => actions[name].action === "share").forEach(scope);
2997
3209
  }
2998
3210
  }
2999
3211
  }
@@ -3036,12 +3248,24 @@ function createConvertToImportMap(config, ports) {
3036
3248
  return;
3037
3249
  }
3038
3250
  if (actions[external.packageName].action === "skip") {
3039
- const override = actions[external.packageName].override;
3040
- if (!external.shareScope) return;
3251
+ const { override, covered } = actions[external.packageName];
3252
+ if (!external.shareScope) {
3253
+ if (covered) {
3254
+ serveUncovered(remoteEntry, external, covered, remoteEntryScope, importMap);
3255
+ }
3256
+ return;
3257
+ }
3041
3258
  if (override) {
3042
3259
  Object.entries(override).forEach(([packageName, url]) => {
3043
3260
  addToScopes(remoteEntryScope, packageName, url, importMap);
3044
3261
  });
3262
+ serveUncovered(
3263
+ remoteEntry,
3264
+ external,
3265
+ covered ?? Object.keys(override),
3266
+ remoteEntryScope,
3267
+ importMap
3268
+ );
3045
3269
  return;
3046
3270
  }
3047
3271
  log.error(
@@ -3108,6 +3332,27 @@ function createConvertToImportMap(config, ports) {
3108
3332
  });
3109
3333
  return importMap;
3110
3334
  }
3335
+ function serveUncovered(remoteEntry, external, covered, remoteEntryScope, importMap) {
3336
+ const provided = new Set(covered);
3337
+ Object.entries(external.entries).forEach(([packageName, fileName]) => {
3338
+ if (provided.has(packageName)) return;
3339
+ if (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints) {
3340
+ warnUncoveredEntrypoint(remoteEntry.name, external.packageName, packageName);
3341
+ return;
3342
+ }
3343
+ const url = join(remoteEntryScope, fileName);
3344
+ addToScopes(remoteEntryScope, packageName, url, importMap);
3345
+ addIntegrity(importMap, url, remoteEntry.integrity, fileName);
3346
+ });
3347
+ }
3348
+ function warnUncoveredEntrypoint(remoteName, externalName, packageName) {
3349
+ const msg = `[${remoteName}][${externalName}] Entrypoint '${packageName}' is not covered by the override.`;
3350
+ if (config.strict.strictEntryPointCoverage || config.strict.strictImportMap) {
3351
+ log.error(9, msg);
3352
+ throw new NFError("Could not create ImportMap.");
3353
+ }
3354
+ log.warn(9, msg);
3355
+ }
3111
3356
  function addIntegrity(importMap, url, integrityMap, file) {
3112
3357
  const hash = integrityMap?.[file];
3113
3358
  if (!hash) return;