@softarc/native-federation-orchestrator 4.5.2 → 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.
@@ -1426,19 +1426,71 @@ function createGetRemoteEntries(config, ports) {
1426
1426
  }
1427
1427
  }
1428
1428
 
1429
+ // src/lib/core/1.domain/externals/basis.ts
1430
+ var coverage = (remote) => Object.keys(remote.entries).length;
1431
+ function isBetterBasis(candidate, basis) {
1432
+ if (basis.cached !== candidate.cached) return candidate.cached;
1433
+ return coverage(candidate) > coverage(basis);
1434
+ }
1435
+ function addRemoteToVersion(version, remote, isHost = false) {
1436
+ if (isHost) {
1437
+ version.host = true;
1438
+ version.remotes.unshift(remote);
1439
+ return;
1440
+ }
1441
+ const basis = version.remotes[0];
1442
+ if (basis && !version.host && isBetterBasis(remote, basis)) {
1443
+ version.remotes.unshift(remote);
1444
+ return;
1445
+ }
1446
+ version.remotes.push(remote);
1447
+ }
1448
+ function versionDemands(version) {
1449
+ if (version.remotes.length < 2) return version.remotes;
1450
+ const distinct = /* @__PURE__ */ new Map();
1451
+ for (const remote of version.remotes) {
1452
+ const key = `${remote.requiredVersion}|${remote.strictVersion}|${remote.cached}`;
1453
+ if (!distinct.has(key)) distinct.set(key, remote);
1454
+ }
1455
+ return Array.from(distinct.values());
1456
+ }
1457
+ function findVersionForTag(versions, tag) {
1458
+ let scoped;
1459
+ for (const version of versions) {
1460
+ if (version.tag !== tag) continue;
1461
+ if (version.action !== "scope") return version;
1462
+ scoped ??= version;
1463
+ }
1464
+ return scoped;
1465
+ }
1466
+ function uncoveredEntrypoints(remote, basis) {
1467
+ return Object.keys(remote.entries).filter((entrypoint) => !(entrypoint in basis));
1468
+ }
1469
+ function countUncoveredEntrypoints(remote, basis) {
1470
+ let uncovered = 0;
1471
+ for (const entrypoint in remote.entries) {
1472
+ if (!(entrypoint in basis)) uncovered++;
1473
+ }
1474
+ return uncovered;
1475
+ }
1476
+
1429
1477
  // src/lib/core/2.app/steps/store-remote-entry.ts
1478
+ function createRemoveCachedRemoteEntries(ports) {
1479
+ return (remoteNames) => {
1480
+ if (remoteNames.size === 0) return;
1481
+ for (const remoteName of remoteNames) {
1482
+ ports.remoteInfoRepo.remove(remoteName);
1483
+ ports.scopedExternalsRepo.remove(remoteName);
1484
+ }
1485
+ ports.sharedExternalsRepo.removeFromAllScopes(remoteNames);
1486
+ };
1487
+ }
1430
1488
  function createStoreRemoteEntry(config, ports, logStep) {
1431
1489
  return (remoteEntry, onSharedExternal) => {
1432
- if (remoteEntry?.override) removeCachedRemoteEntry(remoteEntry);
1433
1490
  addRemoteInfoToStorage(remoteEntry);
1434
1491
  addExternalsToStorage(remoteEntry, onSharedExternal);
1435
1492
  addSharedChunksToStorage(remoteEntry);
1436
1493
  };
1437
- function removeCachedRemoteEntry(remoteEntry) {
1438
- ports.remoteInfoRepo.remove(remoteEntry.name);
1439
- ports.scopedExternalsRepo.remove(remoteEntry.name);
1440
- ports.sharedExternalsRepo.removeFromAllScopes(remoteEntry.name);
1441
- }
1442
1494
  function addRemoteInfoToStorage({ name, url, exposes, integrity }) {
1443
1495
  ports.remoteInfoRepo.addOrUpdate(name, {
1444
1496
  scopeUrl: getScope(url),
@@ -1538,14 +1590,35 @@ function createStoreRemoteEntry(config, ports, logStep) {
1538
1590
  // src/lib/core/2.app/steps/process-remote-entries.ts
1539
1591
  function createProcessRemoteEntries(config, ports) {
1540
1592
  const storeRemoteEntry = createStoreRemoteEntry(config, ports, 2);
1593
+ const removeCachedRemoteEntries = createRemoveCachedRemoteEntries(ports);
1541
1594
  return (remoteEntries) => {
1542
1595
  try {
1543
- remoteEntries.forEach((remoteEntry) => storeRemoteEntry(remoteEntry, addSharedExternal));
1596
+ const evictPerEntry = evictOverriddenRemotes(remoteEntries);
1597
+ remoteEntries.forEach((remoteEntry) => {
1598
+ if (remoteEntry?.override && evictPerEntry.has(remoteEntry.name)) {
1599
+ removeCachedRemoteEntries(/* @__PURE__ */ new Set([remoteEntry.name]));
1600
+ }
1601
+ storeRemoteEntry(remoteEntry, addSharedExternal);
1602
+ });
1544
1603
  return Promise.resolve(remoteEntries);
1545
1604
  } catch (e) {
1546
1605
  return Promise.reject(e);
1547
1606
  }
1548
1607
  };
1608
+ function evictOverriddenRemotes(remoteEntries) {
1609
+ const batched = /* @__PURE__ */ new Set();
1610
+ const repeated = /* @__PURE__ */ new Set();
1611
+ const seen = /* @__PURE__ */ new Set();
1612
+ for (const remoteEntry of remoteEntries) {
1613
+ if (!remoteEntry) continue;
1614
+ if (seen.has(remoteEntry.name)) repeated.add(remoteEntry.name);
1615
+ seen.add(remoteEntry.name);
1616
+ if (remoteEntry.override) batched.add(remoteEntry.name);
1617
+ }
1618
+ for (const remoteName of repeated) batched.delete(remoteName);
1619
+ removeCachedRemoteEntries(batched);
1620
+ return repeated;
1621
+ }
1549
1622
  function addSharedExternal(remoteEntry, _sharedInfo, {
1550
1623
  tag,
1551
1624
  remote,
@@ -1554,15 +1627,12 @@ function createProcessRemoteEntries(config, ports) {
1554
1627
  assertSameVersionCompatibility,
1555
1628
  commit
1556
1629
  }) {
1557
- const matchingVersion = cached.versions.find((version) => version.tag === tag);
1630
+ if (scopeType !== "strict") cached.dirty = true;
1631
+ const matchingVersion = findVersionForTag(cached.versions, tag);
1558
1632
  if (matchingVersion) {
1559
1633
  assertSameVersionCompatibility(matchingVersion);
1560
- if (!matchingVersion.host && !!remoteEntry?.host) {
1561
- matchingVersion.host = true;
1562
- matchingVersion.remotes.unshift(remote);
1563
- } else matchingVersion.remotes.push(remote);
1634
+ addRemoteToVersion(matchingVersion, remote, !matchingVersion.host && !!remoteEntry?.host);
1564
1635
  } else {
1565
- if (scopeType !== "strict") cached.dirty = true;
1566
1636
  cached.versions.push({
1567
1637
  tag,
1568
1638
  action: scopeType === "strict" ? "share" : "skip",
@@ -1581,13 +1651,23 @@ var STRICT_SCOPE = "strict";
1581
1651
  // src/lib/core/2.app/steps/determine-shared-externals.ts
1582
1652
  function createDetermineSharedExternals(config, ports) {
1583
1653
  return () => {
1654
+ const memo = /* @__PURE__ */ new Map();
1655
+ const isCompatible = (tag, requiredVersion) => {
1656
+ const key = `${tag}|${requiredVersion}`;
1657
+ let hit = memo.get(key);
1658
+ if (hit === void 0) {
1659
+ hit = ports.versionCheck.isCompatible(tag, requiredVersion);
1660
+ memo.set(key, hit);
1661
+ }
1662
+ return hit;
1663
+ };
1584
1664
  for (const shareScope of ports.sharedExternalsRepo.getScopes()) {
1585
1665
  const sharedExternals = ports.sharedExternalsRepo.getFromScope(shareScope);
1586
1666
  try {
1587
1667
  Object.entries(sharedExternals).filter(([_, e]) => e.dirty).forEach(
1588
1668
  ([name, external]) => ports.sharedExternalsRepo.addOrUpdate(
1589
1669
  name,
1590
- setVersionActions(name, external),
1670
+ setVersionActions(name, external, isCompatible),
1591
1671
  shareScope
1592
1672
  )
1593
1673
  );
@@ -1610,20 +1690,26 @@ function createDetermineSharedExternals(config, ports) {
1610
1690
  }
1611
1691
  return Promise.resolve();
1612
1692
  };
1613
- function uncoveredTears(external, winner) {
1693
+ function uncoveredTears(external, winner, accepts) {
1614
1694
  const basis = winner.remotes[0].entries;
1615
1695
  return external.versions.reduce((sum, v) => {
1616
1696
  if (v === winner) return sum;
1617
- if (!ports.versionCheck.isCompatible(winner.tag, v.remotes[0].requiredVersion)) return sum;
1618
- return sum + v.remotes.reduce((n, r) => n + Object.keys(r.entries).filter((e) => !(e in basis)).length, 0);
1697
+ if (!accepts(v, winner.tag)) return sum;
1698
+ return sum + v.remotes.reduce((n, r) => n + countUncoveredEntrypoints(r, basis), 0);
1619
1699
  }, 0);
1620
1700
  }
1621
- function setVersionActions(externalName, external) {
1701
+ function setVersionActions(externalName, external, isCompatible) {
1622
1702
  if (external.versions.length === 1) {
1623
1703
  external.versions[0].action = "share";
1704
+ applyEntrypointCoveragePolicy(externalName, external);
1624
1705
  external.dirty = false;
1625
1706
  return external;
1626
1707
  }
1708
+ const demands = new Map(
1709
+ external.versions.map((v) => [v, versionDemands(v)])
1710
+ );
1711
+ const accepts = (version, tag) => demands.get(version).every((d) => isCompatible(tag, d.requiredVersion));
1712
+ const objector = (version, tag) => demands.get(version).find((d) => d.strictVersion && !isCompatible(tag, d.requiredVersion));
1627
1713
  let sharedVersion = external.versions.find((v) => v.host);
1628
1714
  if (!sharedVersion && config.profile.latestSharedExternal) {
1629
1715
  sharedVersion = external.versions[0];
@@ -1633,11 +1719,17 @@ function createDetermineSharedExternals(config, ports) {
1633
1719
  let leastTears = Number.MAX_VALUE;
1634
1720
  external.versions.forEach((vA) => {
1635
1721
  const extraDownloads = external.versions.filter(
1636
- (vB) => !vB.remotes[0].cached && vB.remotes[0].strictVersion && !ports.versionCheck.isCompatible(vA.tag, vB.remotes[0].requiredVersion)
1722
+ (vB) => demands.get(vB).some((d) => !d.cached && d.strictVersion && !isCompatible(vA.tag, d.requiredVersion))
1637
1723
  ).length;
1638
- const tears = uncoveredTears(external, vA);
1639
- if (extraDownloads < leastExtraDownloads || extraDownloads === leastExtraDownloads && tears < leastTears) {
1724
+ if (extraDownloads < leastExtraDownloads) {
1640
1725
  leastExtraDownloads = extraDownloads;
1726
+ leastTears = uncoveredTears(external, vA, accepts);
1727
+ sharedVersion = vA;
1728
+ return;
1729
+ }
1730
+ if (extraDownloads > leastExtraDownloads) return;
1731
+ const tears = uncoveredTears(external, vA, accepts);
1732
+ if (tears < leastTears) {
1641
1733
  leastTears = tears;
1642
1734
  sharedVersion = vA;
1643
1735
  }
@@ -1647,39 +1739,81 @@ function createDetermineSharedExternals(config, ports) {
1647
1739
  throw new NFError(`[${externalName}] Could not determine shared version!`);
1648
1740
  }
1649
1741
  external.versions.forEach((v) => {
1650
- if (ports.versionCheck.isCompatible(sharedVersion.tag, v.remotes[0].requiredVersion)) {
1742
+ if (accepts(v, sharedVersion.tag)) {
1651
1743
  v.action = "skip";
1652
1744
  return;
1653
1745
  }
1654
- if (config.strict.strictExternalCompatibility && v.remotes[0].strictVersion) {
1746
+ const strict = objector(v, sharedVersion.tag);
1747
+ if (config.strict.strictExternalCompatibility && strict) {
1655
1748
  config.log.error(
1656
1749
  3,
1657
- `[${v.remotes[0].name}][${externalName}@${v.tag}] Is not compatible with requiredRange '${sharedVersion.remotes[0]?.requiredVersion}' from remote '${sharedVersion.remotes[0].name}'.`
1750
+ `[${strict.name}][${externalName}@${v.tag}] Is not compatible with requiredRange '${strict.requiredVersion}' of shared ${externalName}@${sharedVersion.tag}.`
1658
1751
  );
1659
1752
  throw new NFError(`External ${externalName}@${v.tag} could not be shared.`);
1660
1753
  }
1661
- v.action = v.remotes[0].strictVersion ? "scope" : "skip";
1754
+ v.action = strict ? "scope" : "skip";
1662
1755
  });
1663
1756
  sharedVersion.action = "share";
1664
- if (config.strict.strictEntryPointCoverage) {
1665
- const basis = sharedVersion.remotes[0].entries;
1666
- external.versions.forEach((v) => {
1667
- if (v.action !== "skip") return;
1668
- const uncovered = v.remotes.some(
1669
- (r) => Object.keys(r.entries).some((entrypoint) => !(entrypoint in basis))
1670
- );
1671
- if (uncovered) {
1672
- v.action = "scope";
1673
- config.log.debug(
1674
- 3,
1675
- `[${externalName}@${v.tag}] Scoped: entrypoints not covered by shared ${externalName}@${sharedVersion.tag}.`
1676
- );
1677
- }
1678
- });
1679
- }
1757
+ applyEntrypointCoveragePolicy(externalName, external);
1680
1758
  external.dirty = false;
1681
1759
  return external;
1682
1760
  }
1761
+ function applyEntrypointCoveragePolicy(externalName, external) {
1762
+ const { strictEntryPointCoverage } = config.strict;
1763
+ if (!strictEntryPointCoverage && !config.profile.scopeUncoveredEntrypoints) return;
1764
+ const tears = findTears(external);
1765
+ if (tears.length === 0) return;
1766
+ if (strictEntryPointCoverage) {
1767
+ const { version, remote, uncovered } = tears[0];
1768
+ config.log.error(
1769
+ 3,
1770
+ `[${externalName}@${version.tag}][${remote.name}] Entrypoints not covered by the shared version: ${uncovered.join(", ")}.`
1771
+ );
1772
+ throw new NFError(
1773
+ `External ${externalName} could not be shared without tearing entrypoints.`
1774
+ );
1775
+ }
1776
+ scopeTornRemotes(externalName, external, tears);
1777
+ }
1778
+ function findTears(external) {
1779
+ const shared = external.versions.find((v) => v.action === "share");
1780
+ if (!shared) return [];
1781
+ const basis = shared.remotes[0].entries;
1782
+ const tears = [];
1783
+ for (const version of external.versions) {
1784
+ if (version.action === "scope") continue;
1785
+ version.remotes.forEach((remote, index) => {
1786
+ if (version === shared && index === 0) return;
1787
+ const uncovered = uncoveredEntrypoints(remote, basis);
1788
+ if (uncovered.length > 0) tears.push({ version, remote, uncovered });
1789
+ });
1790
+ }
1791
+ return tears;
1792
+ }
1793
+ function scopeTornRemotes(externalName, external, tears) {
1794
+ const torn = new Set(tears.map((t) => t.remote));
1795
+ const demotedByTag = /* @__PURE__ */ new Map();
1796
+ for (const { version, remote, uncovered } of tears) {
1797
+ const group = demotedByTag.get(version.tag);
1798
+ if (group) group.push(remote);
1799
+ else demotedByTag.set(version.tag, [remote]);
1800
+ config.log.debug(
1801
+ 3,
1802
+ `[${externalName}@${version.tag}][${remote.name}] Scoped: entrypoints not covered by the shared version: ${uncovered.join(", ")}.`
1803
+ );
1804
+ }
1805
+ for (const version of external.versions) {
1806
+ if (version.remotes.some((r) => torn.has(r))) {
1807
+ version.remotes = version.remotes.filter((r) => !torn.has(r));
1808
+ }
1809
+ }
1810
+ external.versions = external.versions.filter((v) => v.remotes.length > 0);
1811
+ for (const [tag, remotes] of demotedByTag) {
1812
+ const scoped = external.versions.find((v) => v.tag === tag && v.action === "scope");
1813
+ if (scoped) scoped.remotes.push(...remotes);
1814
+ else external.versions.push({ tag, host: false, action: "scope", remotes });
1815
+ }
1816
+ }
1683
1817
  }
1684
1818
 
1685
1819
  // src/lib/core/2.app/steps/pooling/pool-graph.ts
@@ -1982,6 +2116,7 @@ function createGenerateImportMap(config, ports) {
1982
2116
  for (const [externalName, external] of Object.entries(sharedExternals)) {
1983
2117
  let override = void 0;
1984
2118
  let overrideScope = void 0;
2119
+ const cachedBefore = cachedFingerprint(external);
1985
2120
  for (const version of external.versions) {
1986
2121
  if (version.action === "scope") {
1987
2122
  for (const remote of version.remotes) {
@@ -2027,7 +2162,7 @@ function createGenerateImportMap(config, ports) {
2027
2162
  }
2028
2163
  for (const [packageName, file] of Object.entries(r.entries)) {
2029
2164
  if (packageName in source.entries) continue;
2030
- if (config.strict.strictEntryPointCoverage) {
2165
+ if (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints) {
2031
2166
  warnUncoveredEntrypoint(shareScope, externalName, r.name, packageName);
2032
2167
  continue;
2033
2168
  }
@@ -2041,9 +2176,14 @@ function createGenerateImportMap(config, ports) {
2041
2176
  addIntegrity(importMap, m.url, m.name, m.file);
2042
2177
  }
2043
2178
  }
2044
- ports.sharedExternalsRepo.addOrUpdate(externalName, external, shareScope);
2179
+ if (cachedFingerprint(external) !== cachedBefore) {
2180
+ ports.sharedExternalsRepo.addOrUpdate(externalName, external, shareScope);
2181
+ }
2045
2182
  }
2046
2183
  }
2184
+ function cachedFingerprint(external) {
2185
+ return external.versions.map((v) => v.remotes.map((r) => r.cached ? 1 : 0).join("")).join("|");
2186
+ }
2047
2187
  function findOverride(external, shareScope, externalName) {
2048
2188
  const sharedVersions = external.versions.filter((v) => v.action === "share");
2049
2189
  const scopedExternalName = `${shareScope}.${externalName}`;
@@ -2075,6 +2215,7 @@ function createGenerateImportMap(config, ports) {
2075
2215
  function addGlobalSharedExternals(importMap, chunkBundles) {
2076
2216
  const sharedExternals = ports.sharedExternalsRepo.getFromScope();
2077
2217
  for (const [externalName, external] of Object.entries(sharedExternals)) {
2218
+ const cachedBefore = cachedFingerprint(external);
2078
2219
  for (const version of external.versions) {
2079
2220
  if (version.action === "skip") continue;
2080
2221
  if (version.action === "scope") {
@@ -2102,31 +2243,39 @@ function createGenerateImportMap(config, ports) {
2102
2243
  }
2103
2244
  registerBundleChunks(chunkBundles, version.remotes[0].name, version.remotes[0].bundle);
2104
2245
  version.remotes[0].cached = true;
2246
+ selfFillUncovered(importMap, chunkBundles, externalName, version.remotes, 1);
2105
2247
  }
2106
2248
  for (const version of external.versions) {
2107
2249
  if (version.action !== "skip") continue;
2108
- const remote = version.remotes[0];
2109
- for (const [packageName, file] of Object.entries(remote.entries)) {
2110
- if (importMap.imports[packageName]) continue;
2111
- if (config.strict.strictEntryPointCoverage) {
2112
- warnUncoveredEntrypoint(GLOBAL_SCOPE, externalName, remote.name, packageName);
2113
- continue;
2114
- }
2115
- const scope = getScope2(GLOBAL_SCOPE, remote.name, externalName);
2116
- const url = join(scope, file);
2117
- addToGlobal(importMap, { [packageName]: url });
2118
- addIntegrity(importMap, url, remote.name, file);
2119
- registerBundleChunks(chunkBundles, remote.name, remote.bundle);
2120
- remote.cached = true;
2121
- }
2250
+ selfFillUncovered(importMap, chunkBundles, externalName, version.remotes);
2251
+ }
2252
+ if (cachedFingerprint(external) !== cachedBefore) {
2253
+ ports.sharedExternalsRepo.addOrUpdate(externalName, external);
2122
2254
  }
2123
- ports.sharedExternalsRepo.addOrUpdate(externalName, external);
2124
2255
  }
2125
2256
  return importMap;
2126
2257
  }
2258
+ function selfFillUncovered(importMap, chunkBundles, externalName, remotes, from = 0) {
2259
+ for (let i = from; i < remotes.length; i++) {
2260
+ const remote = remotes[i];
2261
+ for (const [packageName, file] of Object.entries(remote.entries)) {
2262
+ if (importMap.imports[packageName]) continue;
2263
+ if (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints) {
2264
+ warnUncoveredEntrypoint(GLOBAL_SCOPE, externalName, remote.name, packageName);
2265
+ continue;
2266
+ }
2267
+ const scope = getScope2(GLOBAL_SCOPE, remote.name, externalName);
2268
+ const url = join(scope, file);
2269
+ addToGlobal(importMap, { [packageName]: url });
2270
+ addIntegrity(importMap, url, remote.name, file);
2271
+ registerBundleChunks(chunkBundles, remote.name, remote.bundle);
2272
+ remote.cached = true;
2273
+ }
2274
+ }
2275
+ }
2127
2276
  function warnUncoveredEntrypoint(scope, externalName, remoteName, packageName) {
2128
2277
  const msg = `[${scope}][${externalName}][${remoteName}] Entrypoint '${packageName}' is not covered by the shared version.`;
2129
- if (config.strict.strictImportMap) {
2278
+ if (config.strict.strictEntryPointCoverage || config.strict.strictImportMap) {
2130
2279
  config.log.error(4, msg);
2131
2280
  throw new NFError("Could not create ImportMap.");
2132
2281
  }
@@ -2354,18 +2503,23 @@ function createGetRemoteEntry(config, ports) {
2354
2503
  // src/lib/core/2.app/steps/update-cache.ts
2355
2504
  function createUpdateCache(config, ports) {
2356
2505
  const storeRemoteEntry = createStoreRemoteEntry(config, ports, 8);
2506
+ const removeCachedRemoteEntries = createRemoveCachedRemoteEntries(ports);
2357
2507
  return (remoteEntry) => {
2358
2508
  try {
2359
2509
  const actions = {};
2510
+ if (remoteEntry?.override) removeCachedRemoteEntries(/* @__PURE__ */ new Set([remoteEntry.name]));
2360
2511
  storeRemoteEntry(remoteEntry, (entry, external, ctx) => {
2361
2512
  const { action, sharedVersion } = resolveSharedExternal(entry, external, ctx);
2362
2513
  actions[external.packageName] = { action };
2363
- if (action === "skip" && external.shareScope && sharedVersion?.remotes[0]?.entries) {
2364
- actions[external.packageName].override = resolveOverrideEntries(
2365
- entry,
2366
- external,
2367
- sharedVersion
2368
- );
2514
+ if (action === "skip" && sharedVersion?.remotes[0]?.entries) {
2515
+ actions[external.packageName].covered = Object.keys(sharedVersion.remotes[0].entries);
2516
+ if (external.shareScope) {
2517
+ actions[external.packageName].override = resolveOverrideEntries(
2518
+ entry,
2519
+ external,
2520
+ sharedVersion
2521
+ );
2522
+ }
2369
2523
  }
2370
2524
  });
2371
2525
  return Promise.resolve({ entry: remoteEntry, actions });
@@ -2393,13 +2547,29 @@ function createUpdateCache(config, ports) {
2393
2547
  }
2394
2548
  config.log.warn(8, errorMsg);
2395
2549
  }
2396
- if (action === "skip" && config.strict.strictEntryPointCoverage && sharedVersion && Object.keys(remote.entries).some((e) => !(e in sharedVersion.remotes[0].entries))) {
2397
- action = "scope";
2550
+ if (action === "skip" && sharedVersion) {
2551
+ const uncovered = uncoveredEntrypoints(remote, sharedVersion.remotes[0].entries);
2552
+ if (uncovered.length > 0) {
2553
+ const msg = `[${sharedInfo.shareScope ?? GLOBAL_SCOPE}][${remoteEntry.name}][${sharedInfo.packageName}] Entrypoints not covered by the shared version: ${uncovered.join(", ")}.`;
2554
+ if (config.strict.strictEntryPointCoverage) {
2555
+ config.log.error(8, msg);
2556
+ throw new NFError(`Could not process remote '${remoteEntry.name}'`);
2557
+ }
2558
+ if (config.profile.scopeUncoveredEntrypoints) {
2559
+ config.log.debug(8, msg);
2560
+ action = "scope";
2561
+ }
2562
+ }
2398
2563
  }
2399
- const matchingVersion = cached.versions.find((cached2) => cached2.tag === tag);
2400
- if (matchingVersion) {
2564
+ const matchingVersion = findVersionForTag(cached.versions, tag);
2565
+ if (action === "scope") {
2566
+ remote.cached = true;
2567
+ const scoped = cached.versions.find((v) => v.tag === tag && v.action === "scope");
2568
+ if (scoped) scoped.remotes.push(remote);
2569
+ else cached.versions.push({ tag, action, host: false, remotes: [remote] });
2570
+ } else if (matchingVersion) {
2401
2571
  assertSameVersionCompatibility(matchingVersion);
2402
- matchingVersion.remotes.push(remote);
2572
+ addRemoteToVersion(matchingVersion, remote);
2403
2573
  } else {
2404
2574
  if (!sharedVersion) action = "share";
2405
2575
  remote.cached = action !== "skip";
@@ -2502,22 +2672,24 @@ function createConvertToImportMap(config, ports) {
2502
2672
  return;
2503
2673
  }
2504
2674
  if (actions[external.packageName].action === "skip") {
2505
- const override = actions[external.packageName].override;
2506
- if (!external.shareScope) return;
2675
+ const { override, covered } = actions[external.packageName];
2676
+ if (!external.shareScope) {
2677
+ if (covered) {
2678
+ serveUncovered(remoteEntry, external, covered, remoteEntryScope, importMap);
2679
+ }
2680
+ return;
2681
+ }
2507
2682
  if (override) {
2508
2683
  Object.entries(override).forEach(([packageName, url]) => {
2509
2684
  addToScopes(remoteEntryScope, packageName, url, importMap);
2510
2685
  });
2511
- Object.entries(external.entries).forEach(([packageName, fileName]) => {
2512
- if (packageName in override) return;
2513
- if (config.strict.strictEntryPointCoverage) {
2514
- warnUncoveredEntrypoint(remoteEntry.name, external.packageName, packageName);
2515
- return;
2516
- }
2517
- const url = join(remoteEntryScope, fileName);
2518
- addToScopes(remoteEntryScope, packageName, url, importMap);
2519
- addIntegrity(importMap, url, integrityMap, fileName);
2520
- });
2686
+ serveUncovered(
2687
+ remoteEntry,
2688
+ external,
2689
+ covered ?? Object.keys(override),
2690
+ remoteEntryScope,
2691
+ importMap
2692
+ );
2521
2693
  return;
2522
2694
  }
2523
2695
  log.error(
@@ -2584,9 +2756,22 @@ function createConvertToImportMap(config, ports) {
2584
2756
  });
2585
2757
  return importMap;
2586
2758
  }
2759
+ function serveUncovered(remoteEntry, external, covered, remoteEntryScope, importMap) {
2760
+ const provided = new Set(covered);
2761
+ Object.entries(external.entries).forEach(([packageName, fileName]) => {
2762
+ if (provided.has(packageName)) return;
2763
+ if (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints) {
2764
+ warnUncoveredEntrypoint(remoteEntry.name, external.packageName, packageName);
2765
+ return;
2766
+ }
2767
+ const url = join(remoteEntryScope, fileName);
2768
+ addToScopes(remoteEntryScope, packageName, url, importMap);
2769
+ addIntegrity(importMap, url, remoteEntry.integrity, fileName);
2770
+ });
2771
+ }
2587
2772
  function warnUncoveredEntrypoint(remoteName, externalName, packageName) {
2588
2773
  const msg = `[${remoteName}][${externalName}] Entrypoint '${packageName}' is not covered by the override.`;
2589
- if (config.strict.strictImportMap) {
2774
+ if (config.strict.strictEntryPointCoverage || config.strict.strictImportMap) {
2590
2775
  log.error(9, msg);
2591
2776
  throw new NFError("Could not create ImportMap.");
2592
2777
  }
@@ -2835,16 +3020,21 @@ var createRemoteInfoRepository = (config) => {
2835
3020
  const STORAGE = config.storage("remotes", {});
2836
3021
  if (config.clearStorage) STORAGE.clear();
2837
3022
  const _cache = STORAGE.get() ?? {};
3023
+ let _dirty = false;
2838
3024
  return {
2839
3025
  contains: function(remoteName) {
2840
3026
  return !!_cache[remoteName];
2841
3027
  },
2842
3028
  remove: function(remoteName) {
2843
- delete _cache[remoteName];
3029
+ if (remoteName in _cache) {
3030
+ delete _cache[remoteName];
3031
+ _dirty = true;
3032
+ }
2844
3033
  return this;
2845
3034
  },
2846
3035
  addOrUpdate: function(remoteName, remote) {
2847
3036
  _cache[remoteName] = remote;
3037
+ _dirty = true;
2848
3038
  return this;
2849
3039
  },
2850
3040
  tryGet: function(remoteName) {
@@ -2859,7 +3049,9 @@ var createRemoteInfoRepository = (config) => {
2859
3049
  return _cache;
2860
3050
  },
2861
3051
  commit: function() {
3052
+ if (!_dirty) return this;
2862
3053
  STORAGE.set(_cache);
3054
+ _dirty = false;
2863
3055
  return this;
2864
3056
  }
2865
3057
  };
@@ -2870,14 +3062,19 @@ var createScopedExternalsRepository = (config) => {
2870
3062
  const STORAGE = config.storage("scoped-externals", {});
2871
3063
  if (config.clearStorage) STORAGE.clear();
2872
3064
  const _cache = STORAGE.get() ?? {};
3065
+ let _dirty = false;
2873
3066
  return {
2874
3067
  addExternal: function(remoteName, external, version) {
2875
3068
  if (!_cache[remoteName]) _cache[remoteName] = {};
2876
3069
  _cache[remoteName][external] = version;
3070
+ _dirty = true;
2877
3071
  return this;
2878
3072
  },
2879
3073
  remove: function(remoteName) {
2880
- delete _cache[remoteName];
3074
+ if (remoteName in _cache) {
3075
+ delete _cache[remoteName];
3076
+ _dirty = true;
3077
+ }
2881
3078
  return this;
2882
3079
  },
2883
3080
  getAll: function() {
@@ -2887,7 +3084,9 @@ var createScopedExternalsRepository = (config) => {
2887
3084
  return Optional.of(_cache[remoteName]);
2888
3085
  },
2889
3086
  commit: function() {
3087
+ if (!_dirty) return this;
2890
3088
  STORAGE.set(_cache);
3089
+ _dirty = false;
2891
3090
  return this;
2892
3091
  }
2893
3092
  };
@@ -2900,8 +3099,9 @@ var createSharedExternalsRepository = (config) => {
2900
3099
  { [GLOBAL_SCOPE]: {} }
2901
3100
  );
2902
3101
  if (config.clearStorage) STORAGE.clear();
2903
- const _cache = STORAGE.get();
3102
+ const _cache = STORAGE.get() ?? { [GLOBAL_SCOPE]: {} };
2904
3103
  let _sawPoolTag = false;
3104
+ let _dirty = false;
2905
3105
  return {
2906
3106
  markPoolTagPresent: function() {
2907
3107
  _sawPoolTag = true;
@@ -2915,28 +3115,38 @@ var createSharedExternalsRepository = (config) => {
2915
3115
  addOrUpdate: function(externalName, external, shareScope) {
2916
3116
  if (!_cache[shareScope ?? GLOBAL_SCOPE]) _cache[shareScope ?? GLOBAL_SCOPE] = {};
2917
3117
  _cache[shareScope ?? GLOBAL_SCOPE][externalName] = external;
3118
+ _dirty = true;
2918
3119
  return this;
2919
3120
  },
2920
3121
  getScopes: function(o = { includeGlobal: true }) {
2921
3122
  if (o.includeGlobal) return Object.keys(_cache);
2922
3123
  return Object.keys(_cache).filter((s) => s !== GLOBAL_SCOPE);
2923
3124
  },
2924
- removeFromAllScopes: function(remoteName) {
3125
+ // Batched: one traversal of the graph per init instead of one per overridden remote.
3126
+ removeFromAllScopes: function(remoteNames) {
3127
+ if (remoteNames.size === 0) return;
2925
3128
  Object.values(_cache).forEach((scope) => {
2926
3129
  const removeExternals = [];
2927
3130
  Object.entries(scope).forEach(([name, external]) => {
2928
- const removeVersionIdx = [];
2929
- external.versions.forEach((version, i) => {
2930
- const versionRemoteIDX = version.remotes.findIndex((r) => r.name === remoteName);
2931
- if (~versionRemoteIDX) {
2932
- version.remotes.splice(versionRemoteIDX, 1);
3131
+ let removedVersion = false;
3132
+ for (let i = external.versions.length - 1; i >= 0; i--) {
3133
+ const remotes = external.versions[i].remotes;
3134
+ let keep = 0;
3135
+ for (let r = 0; r < remotes.length; r++) {
3136
+ if (remoteNames.has(remotes[r].name)) continue;
3137
+ remotes[keep++] = remotes[r];
2933
3138
  }
2934
- if (version.remotes.length === 0) removeVersionIdx.push(i);
2935
- });
2936
- if (removeVersionIdx.length > 0) {
2937
- for (let i = removeVersionIdx.length - 1; i >= 0; i--) {
2938
- external.versions.splice(removeVersionIdx[i], 1);
3139
+ if (keep !== remotes.length) {
3140
+ remotes.length = keep;
3141
+ _dirty = true;
2939
3142
  }
3143
+ if (remotes.length === 0) {
3144
+ external.versions.splice(i, 1);
3145
+ removedVersion = true;
3146
+ _dirty = true;
3147
+ }
3148
+ }
3149
+ if (removedVersion) {
2940
3150
  external.dirty = true;
2941
3151
  if (external.versions.length === 0) removeExternals.push(name);
2942
3152
  }
@@ -2960,7 +3170,9 @@ var createSharedExternalsRepository = (config) => {
2960
3170
  return Optional.of(_cache[shareScope ?? GLOBAL_SCOPE]?.[external]);
2961
3171
  },
2962
3172
  commit: function() {
3173
+ if (!_dirty) return this;
2963
3174
  STORAGE.set(_cache);
3175
+ _dirty = false;
2964
3176
  return this;
2965
3177
  }
2966
3178
  };
@@ -2997,17 +3209,21 @@ var createChunkRepository = (config) => {
2997
3209
  const STORAGE = config.storage("shared-chunks", {});
2998
3210
  if (config.clearStorage) STORAGE.clear();
2999
3211
  const _cache = STORAGE.get() ?? {};
3212
+ let _dirty = false;
3000
3213
  return {
3001
3214
  addOrReplace: function(remoteName, bundleName, chunks) {
3002
3215
  if (!_cache[remoteName]) _cache[remoteName] = {};
3003
3216
  _cache[remoteName][bundleName] = chunks;
3217
+ _dirty = true;
3004
3218
  return this;
3005
3219
  },
3006
3220
  tryGet: function(remoteName, bundleName) {
3007
3221
  return Optional.of(_cache[remoteName]?.[bundleName]);
3008
3222
  },
3009
3223
  commit: function() {
3224
+ if (!_dirty) return this;
3010
3225
  STORAGE.set(_cache);
3226
+ _dirty = false;
3011
3227
  return this;
3012
3228
  }
3013
3229
  };
@@ -3213,10 +3429,9 @@ var globalThisStorageEntry = (namespace) => (key, initialValue) => {
3213
3429
  globalThis[namespace] = {};
3214
3430
  }
3215
3431
  const storage = globalThis[namespace];
3216
- if (!storage[key]) storage[key] = initialValue;
3217
3432
  const entry = {
3218
3433
  get() {
3219
- return cloneEntry(key, storage[key]);
3434
+ return cloneEntry(key, key in storage ? storage[key] : initialValue);
3220
3435
  },
3221
3436
  set(value) {
3222
3437
  storage[key] = cloneEntry(key, value);
@@ -3240,6 +3455,7 @@ var createStorageConfig = (override) => ({
3240
3455
  var defaultProfile = {
3241
3456
  latestSharedExternal: false,
3242
3457
  skipInvalidExternalVersions: false,
3458
+ scopeUncoveredEntrypoints: false,
3243
3459
  overrideCachedRemotes: "init-only",
3244
3460
  overrideCachedRemotesIfURLMatches: false
3245
3461
  };