@softarc/native-federation-orchestrator 4.5.2 → 4.6.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.
Files changed (32) hide show
  1. package/README.md +2 -2
  2. package/fesm2022/@softarc/native-federation-orchestrator.mjs +1310 -257
  3. package/fesm2022/@softarc/native-federation-orchestrator.mjs.map +4 -4
  4. package/fesm2022/module-federation.mjs +27 -8
  5. package/fesm2022/module-federation.mjs.map +3 -3
  6. package/fesm2022/node.mjs +1307 -254
  7. package/fesm2022/node.mjs.map +4 -4
  8. package/fesm2022/options.mjs +5 -10
  9. package/fesm2022/options.mjs.map +2 -2
  10. package/package.json +2 -2
  11. package/quickstart.mjs +15 -15
  12. package/types/lib/core/1.domain/externals/basis.d.ts +22 -0
  13. package/types/lib/core/1.domain/externals/version.contract.d.ts +1 -0
  14. package/types/lib/core/1.domain/remote-entry/remote-entry.contract.d.ts +2 -0
  15. package/types/lib/core/2.app/config/mode.contract.d.ts +1 -0
  16. package/types/lib/core/2.app/driver-ports/init/drivers.contract.d.ts +2 -0
  17. package/types/lib/core/2.app/driver-ports/init/for-determining-shared-externals.port.d.ts +8 -1
  18. package/types/lib/core/2.app/driver-ports/init/for-marking-pools-for-reelection.port.d.ts +6 -0
  19. package/types/lib/core/2.app/driver-ports/init/for-pooling-shared-externals.port.d.ts +3 -1
  20. package/types/lib/core/2.app/driving-ports/for-shared-chunks-storage.port.d.ts +1 -0
  21. package/types/lib/core/2.app/driving-ports/for-shared-externals-storage.port.d.ts +2 -3
  22. package/types/lib/core/2.app/steps/apply-winner.d.ts +18 -0
  23. package/types/lib/core/2.app/steps/pooling/anchoring.d.ts +69 -0
  24. package/types/lib/core/2.app/steps/pooling/mark-pools-for-reelection.d.ts +5 -0
  25. package/types/lib/core/2.app/steps/pooling/pool-dynamic-externals.d.ts +3 -1
  26. package/types/lib/core/2.app/steps/pooling/pool-graph.d.ts +10 -5
  27. package/types/lib/core/2.app/steps/pooling/pool-shared-externals.d.ts +30 -1
  28. package/types/lib/core/2.app/steps/pooling/pool-views.d.ts +63 -0
  29. package/types/lib/core/2.app/steps/pooling/pool.types.d.ts +30 -1
  30. package/types/lib/core/2.app/steps/pooling/pool.util.d.ts +15 -0
  31. package/types/lib/core/2.app/steps/store-remote-entry.d.ts +15 -7
  32. package/types/lib/testing/pooling/no-tear.d.ts +37 -0
@@ -1283,7 +1283,7 @@ var createInitFlow = ({
1283
1283
  adapters,
1284
1284
  config
1285
1285
  }) => {
1286
- return (remotesOrManifestUrl) => flow.getRemoteEntries(remotesOrManifestUrl).then(flow.processRemoteEntries).then(flow.determineSharedExternals).then(flow.poolSharedExternals).then(flow.generateImportMap).then((importMap) => flow.commitChanges(importMap, { override: true })).then(flow.exposeModuleLoader).then((loadRemoteModule) => ({
1286
+ return (remotesOrManifestUrl) => flow.getRemoteEntries(remotesOrManifestUrl).then(flow.processRemoteEntries).then(flow.markPoolsForReelection).then(flow.determineSharedExternals).then(flow.poolSharedExternals).then(flow.generateImportMap).then((importMap) => flow.commitChanges(importMap, { override: true })).then(flow.exposeModuleLoader).then((loadRemoteModule) => ({
1287
1287
  config,
1288
1288
  adapters,
1289
1289
  loadRemoteModule
@@ -1426,19 +1426,92 @@ 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 versionEntries(version) {
1467
+ return collectEntries(version, void 0);
1468
+ }
1469
+ function committedEntries(version) {
1470
+ return collectEntries(version, (remote) => remote.cached);
1471
+ }
1472
+ function forEachVersionEntry(version, accepts, visit) {
1473
+ for (const remote of version.remotes) {
1474
+ if (remote.servedBy) continue;
1475
+ if (accepts && !accepts(remote)) continue;
1476
+ for (const entrypoint in remote.entries) visit(entrypoint, remote);
1477
+ }
1478
+ }
1479
+ function collectEntries(version, accepts) {
1480
+ const entries = /* @__PURE__ */ new Map();
1481
+ forEachVersionEntry(version, accepts, (entrypoint, remote) => {
1482
+ if (!entries.has(entrypoint)) entries.set(entrypoint, remote);
1483
+ });
1484
+ return entries;
1485
+ }
1486
+ function uncoveredEntrypoints(remote, covered) {
1487
+ return Object.keys(remote.entries).filter((entrypoint) => !covered.has(entrypoint));
1488
+ }
1489
+ function countUncoveredEntrypoints(remote, covered) {
1490
+ let uncovered = 0;
1491
+ for (const entrypoint in remote.entries) {
1492
+ if (!covered.has(entrypoint)) uncovered++;
1493
+ }
1494
+ return uncovered;
1495
+ }
1496
+
1429
1497
  // src/lib/core/2.app/steps/store-remote-entry.ts
1498
+ function createRemoveCachedRemoteEntries(ports) {
1499
+ return (remoteNames) => {
1500
+ if (remoteNames.size === 0) return;
1501
+ for (const remoteName of remoteNames) {
1502
+ ports.remoteInfoRepo.remove(remoteName);
1503
+ ports.scopedExternalsRepo.remove(remoteName);
1504
+ ports.sharedChunksRepo.remove(remoteName);
1505
+ }
1506
+ ports.sharedExternalsRepo.removeFromAllScopes(remoteNames);
1507
+ };
1508
+ }
1430
1509
  function createStoreRemoteEntry(config, ports, logStep) {
1431
1510
  return (remoteEntry, onSharedExternal) => {
1432
- if (remoteEntry?.override) removeCachedRemoteEntry(remoteEntry);
1433
1511
  addRemoteInfoToStorage(remoteEntry);
1434
1512
  addExternalsToStorage(remoteEntry, onSharedExternal);
1435
1513
  addSharedChunksToStorage(remoteEntry);
1436
1514
  };
1437
- function removeCachedRemoteEntry(remoteEntry) {
1438
- ports.remoteInfoRepo.remove(remoteEntry.name);
1439
- ports.scopedExternalsRepo.remove(remoteEntry.name);
1440
- ports.sharedExternalsRepo.removeFromAllScopes(remoteEntry.name);
1441
- }
1442
1515
  function addRemoteInfoToStorage({ name, url, exposes, integrity }) {
1443
1516
  ports.remoteInfoRepo.addOrUpdate(name, {
1444
1517
  scopeUrl: getScope(url),
@@ -1454,7 +1527,6 @@ function createStoreRemoteEntry(config, ports, logStep) {
1454
1527
  const tag = resolveVersion(remoteEntry, external);
1455
1528
  if (tag === null) return;
1456
1529
  if (external.singleton) {
1457
- if (external.pool?.trim()) ports.sharedExternalsRepo.markPoolTagPresent();
1458
1530
  onSharedExternal(remoteEntry, external, sharedExternalContext(remoteEntry, external, tag));
1459
1531
  } else {
1460
1532
  addScopedExternal(remoteEntry, external, tag);
@@ -1538,14 +1610,35 @@ function createStoreRemoteEntry(config, ports, logStep) {
1538
1610
  // src/lib/core/2.app/steps/process-remote-entries.ts
1539
1611
  function createProcessRemoteEntries(config, ports) {
1540
1612
  const storeRemoteEntry = createStoreRemoteEntry(config, ports, 2);
1613
+ const removeCachedRemoteEntries = createRemoveCachedRemoteEntries(ports);
1541
1614
  return (remoteEntries) => {
1542
1615
  try {
1543
- remoteEntries.forEach((remoteEntry) => storeRemoteEntry(remoteEntry, addSharedExternal));
1616
+ const evictPerEntry = evictOverriddenRemotes(remoteEntries);
1617
+ remoteEntries.forEach((remoteEntry) => {
1618
+ if (remoteEntry?.override && evictPerEntry.has(remoteEntry.name)) {
1619
+ removeCachedRemoteEntries(/* @__PURE__ */ new Set([remoteEntry.name]));
1620
+ }
1621
+ storeRemoteEntry(remoteEntry, addSharedExternal);
1622
+ });
1544
1623
  return Promise.resolve(remoteEntries);
1545
1624
  } catch (e) {
1546
1625
  return Promise.reject(e);
1547
1626
  }
1548
1627
  };
1628
+ function evictOverriddenRemotes(remoteEntries) {
1629
+ const batched = /* @__PURE__ */ new Set();
1630
+ const repeated = /* @__PURE__ */ new Set();
1631
+ const seen = /* @__PURE__ */ new Set();
1632
+ for (const remoteEntry of remoteEntries) {
1633
+ if (!remoteEntry) continue;
1634
+ if (seen.has(remoteEntry.name)) repeated.add(remoteEntry.name);
1635
+ seen.add(remoteEntry.name);
1636
+ if (remoteEntry.override) batched.add(remoteEntry.name);
1637
+ }
1638
+ for (const remoteName of repeated) batched.delete(remoteName);
1639
+ removeCachedRemoteEntries(batched);
1640
+ return repeated;
1641
+ }
1549
1642
  function addSharedExternal(remoteEntry, _sharedInfo, {
1550
1643
  tag,
1551
1644
  remote,
@@ -1554,15 +1647,12 @@ function createProcessRemoteEntries(config, ports) {
1554
1647
  assertSameVersionCompatibility,
1555
1648
  commit
1556
1649
  }) {
1557
- const matchingVersion = cached.versions.find((version) => version.tag === tag);
1650
+ if (scopeType !== "strict") cached.dirty = true;
1651
+ const matchingVersion = findVersionForTag(cached.versions, tag);
1558
1652
  if (matchingVersion) {
1559
1653
  assertSameVersionCompatibility(matchingVersion);
1560
- if (!matchingVersion.host && !!remoteEntry?.host) {
1561
- matchingVersion.host = true;
1562
- matchingVersion.remotes.unshift(remote);
1563
- } else matchingVersion.remotes.push(remote);
1654
+ addRemoteToVersion(matchingVersion, remote, !matchingVersion.host && !!remoteEntry?.host);
1564
1655
  } else {
1565
- if (scopeType !== "strict") cached.dirty = true;
1566
1656
  cached.versions.push({
1567
1657
  tag,
1568
1658
  action: scopeType === "strict" ? "share" : "skip",
@@ -1578,19 +1668,154 @@ function createProcessRemoteEntries(config, ports) {
1578
1668
  var GLOBAL_SCOPE = "__GLOBAL__";
1579
1669
  var STRICT_SCOPE = "strict";
1580
1670
 
1671
+ // src/lib/core/2.app/steps/apply-winner.ts
1672
+ function versionAcceptance(external, isCompatible) {
1673
+ const demands = new Map(
1674
+ external.versions.map((v) => [v, versionDemands(v)])
1675
+ );
1676
+ return {
1677
+ accepts: (version, tag) => demands.get(version).every((d) => isCompatible(tag, d.requiredVersion)),
1678
+ objector: (version, tag) => demands.get(version).find((d) => d.strictVersion && !isCompatible(tag, d.requiredVersion))
1679
+ };
1680
+ }
1681
+ function createApplyWinner(config) {
1682
+ return function applyWinner(externalName, external, winner, isCompatible, acceptance) {
1683
+ if (external.versions.length > 1) {
1684
+ const { accepts, objector } = acceptance ?? versionAcceptance(external, isCompatible);
1685
+ const rebuilt = [];
1686
+ for (const v of external.versions) {
1687
+ rebuilt.push(v);
1688
+ if (accepts(v, winner.tag)) {
1689
+ v.action = "skip";
1690
+ continue;
1691
+ }
1692
+ const strict = objector(v, winner.tag);
1693
+ if (config.strict.strictExternalCompatibility && strict) {
1694
+ config.log.error(
1695
+ 3,
1696
+ `[${strict.name}][${externalName}@${v.tag}] Is not compatible with requiredRange '${strict.requiredVersion}' of shared ${externalName}@${winner.tag}.`
1697
+ );
1698
+ throw new NFError(`External ${externalName}@${v.tag} could not be shared.`);
1699
+ }
1700
+ if (v === winner) continue;
1701
+ if (!strict) {
1702
+ v.action = "skip";
1703
+ continue;
1704
+ }
1705
+ const objecting = new Set(
1706
+ v.remotes.filter((r) => r.strictVersion && !isCompatible(winner.tag, r.requiredVersion))
1707
+ );
1708
+ if (objecting.size === v.remotes.length) {
1709
+ v.action = "scope";
1710
+ continue;
1711
+ }
1712
+ v.remotes = v.remotes.filter((r) => !objecting.has(r));
1713
+ v.action = "skip";
1714
+ rebuilt.push({ tag: v.tag, host: false, action: "scope", remotes: [...objecting] });
1715
+ }
1716
+ const merged = /* @__PURE__ */ new Map();
1717
+ external.versions = rebuilt.filter((v) => {
1718
+ const first = merged.get(`${v.tag}|${v.action}`);
1719
+ if (!first) {
1720
+ merged.set(`${v.tag}|${v.action}`, v);
1721
+ return true;
1722
+ }
1723
+ first.remotes.push(...v.remotes);
1724
+ return false;
1725
+ });
1726
+ }
1727
+ winner.action = "share";
1728
+ applyEntrypointCoveragePolicy(externalName, external);
1729
+ external.dirty = false;
1730
+ return external;
1731
+ };
1732
+ function applyEntrypointCoveragePolicy(externalName, external) {
1733
+ const { strictEntryPointCoverage } = config.strict;
1734
+ if (!strictEntryPointCoverage && !config.profile.scopeUncoveredEntrypoints) return;
1735
+ const tears = findTears(external);
1736
+ if (tears.length === 0) return;
1737
+ if (strictEntryPointCoverage) {
1738
+ const { version, remote, uncovered } = tears[0];
1739
+ config.log.error(
1740
+ 3,
1741
+ `[${externalName}@${version.tag}][${remote.name}] Entrypoints not covered by the shared version: ${uncovered.join(", ")}.`
1742
+ );
1743
+ throw new NFError(
1744
+ `External ${externalName} could not be shared without tearing entrypoints.`
1745
+ );
1746
+ }
1747
+ scopeTornRemotes(externalName, external, tears);
1748
+ }
1749
+ function findTears(external) {
1750
+ const shared = external.versions.find((v) => v.action === "share");
1751
+ if (!shared) return [];
1752
+ const basis = versionEntries(shared);
1753
+ const tears = [];
1754
+ for (const version of external.versions) {
1755
+ if (version.action === "scope") continue;
1756
+ if (version === shared) continue;
1757
+ version.remotes.forEach((remote) => {
1758
+ if (remote.servedBy) return;
1759
+ const uncovered = uncoveredEntrypoints(remote, basis);
1760
+ if (uncovered.length > 0) tears.push({ version, remote, uncovered });
1761
+ });
1762
+ }
1763
+ return tears;
1764
+ }
1765
+ function scopeTornRemotes(externalName, external, tears) {
1766
+ const torn = new Set(tears.map((t) => t.remote));
1767
+ const demotedByTag = /* @__PURE__ */ new Map();
1768
+ for (const { version, remote, uncovered } of tears) {
1769
+ const group = demotedByTag.get(version.tag);
1770
+ if (group) group.push(remote);
1771
+ else demotedByTag.set(version.tag, [remote]);
1772
+ config.log.debug(
1773
+ 3,
1774
+ `[${externalName}@${version.tag}][${remote.name}] Scoped: entrypoints not covered by the shared version: ${uncovered.join(", ")}.`
1775
+ );
1776
+ }
1777
+ for (const version of external.versions) {
1778
+ if (version.remotes.some((r) => torn.has(r))) {
1779
+ version.remotes = version.remotes.filter((r) => !torn.has(r));
1780
+ }
1781
+ }
1782
+ external.versions = external.versions.filter((v) => v.remotes.length > 0);
1783
+ for (const [tag, remotes] of demotedByTag) {
1784
+ const scoped = external.versions.find((v) => v.tag === tag && v.action === "scope");
1785
+ if (scoped) scoped.remotes.push(...remotes);
1786
+ else external.versions.push({ tag, host: false, action: "scope", remotes });
1787
+ }
1788
+ }
1789
+ }
1790
+
1581
1791
  // src/lib/core/2.app/steps/determine-shared-externals.ts
1582
1792
  function createDetermineSharedExternals(config, ports) {
1793
+ const applyWinner = createApplyWinner(config);
1583
1794
  return () => {
1795
+ const memo = /* @__PURE__ */ new Map();
1796
+ const isCompatible = (tag, requiredVersion) => {
1797
+ const key = `${tag}|${requiredVersion}`;
1798
+ let hit = memo.get(key);
1799
+ if (hit === void 0) {
1800
+ hit = ports.versionCheck.isCompatible(tag, requiredVersion);
1801
+ memo.set(key, hit);
1802
+ }
1803
+ return hit;
1804
+ };
1805
+ const touched = /* @__PURE__ */ new Map();
1584
1806
  for (const shareScope of ports.sharedExternalsRepo.getScopes()) {
1585
1807
  const sharedExternals = ports.sharedExternalsRepo.getFromScope(shareScope);
1586
1808
  try {
1587
- Object.entries(sharedExternals).filter(([_, e]) => e.dirty).forEach(
1588
- ([name, external]) => ports.sharedExternalsRepo.addOrUpdate(
1809
+ const elected = /* @__PURE__ */ new Set();
1810
+ Object.entries(sharedExternals).filter(([_, e]) => e.dirty).forEach(([name, external]) => {
1811
+ ports.sharedExternalsRepo.addOrUpdate(
1589
1812
  name,
1590
- setVersionActions(name, external),
1813
+ setVersionActions(name, external, isCompatible),
1591
1814
  shareScope
1592
- )
1593
- );
1815
+ );
1816
+ elected.add(name);
1817
+ });
1818
+ if (elected.size > 0) touched.set(shareScope, elected);
1594
1819
  } catch (error) {
1595
1820
  config.log.error(
1596
1821
  3,
@@ -1608,22 +1833,25 @@ function createDetermineSharedExternals(config, ports) {
1608
1833
  );
1609
1834
  }
1610
1835
  }
1611
- return Promise.resolve();
1836
+ return Promise.resolve(touched);
1612
1837
  };
1613
- function uncoveredTears(external, winner) {
1614
- const basis = winner.remotes[0].entries;
1838
+ function uncoveredTears(external, winner, accepts) {
1839
+ const basis = versionEntries(winner);
1615
1840
  return external.versions.reduce((sum, v) => {
1616
1841
  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);
1842
+ if (!accepts(v, winner.tag)) return sum;
1843
+ return v.remotes.reduce(
1844
+ (n, r) => r.servedBy ? n : n + countUncoveredEntrypoints(r, basis),
1845
+ sum
1846
+ );
1619
1847
  }, 0);
1620
1848
  }
1621
- function setVersionActions(externalName, external) {
1849
+ function setVersionActions(externalName, external, isCompatible) {
1622
1850
  if (external.versions.length === 1) {
1623
- external.versions[0].action = "share";
1624
- external.dirty = false;
1625
- return external;
1851
+ return applyWinner(externalName, external, external.versions[0], isCompatible);
1626
1852
  }
1853
+ const acceptance = versionAcceptance(external, isCompatible);
1854
+ const { accepts } = acceptance;
1627
1855
  let sharedVersion = external.versions.find((v) => v.host);
1628
1856
  if (!sharedVersion && config.profile.latestSharedExternal) {
1629
1857
  sharedVersion = external.versions[0];
@@ -1631,13 +1859,39 @@ function createDetermineSharedExternals(config, ports) {
1631
1859
  if (!sharedVersion) {
1632
1860
  let leastExtraDownloads = Number.MAX_VALUE;
1633
1861
  let leastTears = Number.MAX_VALUE;
1862
+ const selfServing = new Map(
1863
+ external.versions.map((v) => {
1864
+ const groups = /* @__PURE__ */ new Map();
1865
+ for (const remote of v.remotes) {
1866
+ if (remote.cached || !remote.strictVersion) continue;
1867
+ const group = groups.get(remote.requiredVersion);
1868
+ if (group) group.copies++;
1869
+ else
1870
+ groups.set(remote.requiredVersion, {
1871
+ requiredVersion: remote.requiredVersion,
1872
+ copies: 1
1873
+ });
1874
+ }
1875
+ return [v, [...groups.values()]];
1876
+ })
1877
+ );
1878
+ const costOf = (version, tag) => selfServing.get(version).reduce((n, g) => isCompatible(tag, g.requiredVersion) ? n : n + g.copies, 0);
1634
1879
  external.versions.forEach((vA) => {
1635
- const extraDownloads = external.versions.filter(
1636
- (vB) => !vB.remotes[0].cached && vB.remotes[0].strictVersion && !ports.versionCheck.isCompatible(vA.tag, vB.remotes[0].requiredVersion)
1637
- ).length;
1638
- const tears = uncoveredTears(external, vA);
1639
- if (extraDownloads < leastExtraDownloads || extraDownloads === leastExtraDownloads && tears < leastTears) {
1880
+ const extraDownloads = external.versions.reduce(
1881
+ // A copy of the winner is never redirected, so it never self-serves however its own range
1882
+ // reads — see `applyWinner`, which does not split the winner either.
1883
+ (sum, vB) => vB === vA ? sum : sum + costOf(vB, vA.tag),
1884
+ 0
1885
+ );
1886
+ if (extraDownloads < leastExtraDownloads) {
1640
1887
  leastExtraDownloads = extraDownloads;
1888
+ leastTears = uncoveredTears(external, vA, accepts);
1889
+ sharedVersion = vA;
1890
+ return;
1891
+ }
1892
+ if (extraDownloads > leastExtraDownloads) return;
1893
+ const tears = uncoveredTears(external, vA, accepts);
1894
+ if (tears < leastTears) {
1641
1895
  leastTears = tears;
1642
1896
  sharedVersion = vA;
1643
1897
  }
@@ -1646,40 +1900,302 @@ function createDetermineSharedExternals(config, ports) {
1646
1900
  if (!sharedVersion) {
1647
1901
  throw new NFError(`[${externalName}] Could not determine shared version!`);
1648
1902
  }
1649
- external.versions.forEach((v) => {
1650
- if (ports.versionCheck.isCompatible(sharedVersion.tag, v.remotes[0].requiredVersion)) {
1651
- v.action = "skip";
1652
- return;
1903
+ return applyWinner(externalName, external, sharedVersion, isCompatible, acceptance);
1904
+ }
1905
+ }
1906
+
1907
+ // src/lib/core/2.app/steps/pooling/pool-views.ts
1908
+ function liveBuilds(members, islanded) {
1909
+ return walkBuilds(members, islanded, true);
1910
+ }
1911
+ function committedView(members) {
1912
+ const global = /* @__PURE__ */ new Map();
1913
+ forEachGlobalClaim(members, void 0, (specifier, tag, meta) => {
1914
+ if (!global.has(specifier))
1915
+ global.set(specifier, { tag, remote: meta.name, file: meta.entries[specifier] });
1916
+ });
1917
+ return { builds: walkBuilds(members, void 0, false), global };
1918
+ }
1919
+ function walkBuilds(members, islanded, skipScoped) {
1920
+ const builds = /* @__PURE__ */ new Map();
1921
+ for (const member of members) {
1922
+ const versions = member.external.versions;
1923
+ for (let v = 0; v < versions.length; v++) {
1924
+ const version = versions[v];
1925
+ if (skipScoped && version.action === "scope") continue;
1926
+ const remotes = version.remotes;
1927
+ for (let r = 0; r < remotes.length; r++) {
1928
+ const meta = remotes[r];
1929
+ if (islanded?.has(meta.name)) continue;
1930
+ let own = builds.get(meta.name);
1931
+ if (!own) {
1932
+ builds.set(
1933
+ meta.name,
1934
+ own = { coverage: /* @__PURE__ */ new Map(), tags: /* @__PURE__ */ new Map(), instance: /* @__PURE__ */ new Map() }
1935
+ );
1936
+ }
1937
+ if (!own.instance.has(member.name)) own.instance.set(member.name, version.tag);
1938
+ for (const specifier in meta.entries) {
1939
+ own.coverage.set(specifier, meta.entries[specifier]);
1940
+ if (!own.tags.has(specifier)) own.tags.set(specifier, version.tag);
1941
+ }
1653
1942
  }
1654
- if (config.strict.strictExternalCompatibility && v.remotes[0].strictVersion) {
1655
- config.log.error(
1656
- 3,
1657
- `[${v.remotes[0].name}][${externalName}@${v.tag}] Is not compatible with requiredRange '${sharedVersion.remotes[0]?.requiredVersion}' from remote '${sharedVersion.remotes[0].name}'.`
1658
- );
1659
- throw new NFError(`External ${externalName}@${v.tag} could not be shared.`);
1943
+ }
1944
+ }
1945
+ return builds;
1946
+ }
1947
+ function ownCopies(members, only) {
1948
+ const own = /* @__PURE__ */ new Map();
1949
+ for (const member of members) {
1950
+ for (const version of member.external.versions) {
1951
+ for (const meta of version.remotes) {
1952
+ if (only && !only.has(meta.name)) continue;
1953
+ let copies = own.get(meta.name);
1954
+ if (!copies) own.set(meta.name, copies = []);
1955
+ if (!copies.some((c) => c.member === member.name))
1956
+ copies.push({ member: member.name, tag: version.tag, entries: meta.entries });
1660
1957
  }
1661
- v.action = v.remotes[0].strictVersion ? "scope" : "skip";
1662
- });
1663
- 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
- );
1958
+ }
1959
+ }
1960
+ return own;
1961
+ }
1962
+ function consumedMembers(members) {
1963
+ const consumed = /* @__PURE__ */ new Map();
1964
+ for (const member of members) {
1965
+ const versions = member.external.versions;
1966
+ for (let v = 0; v < versions.length; v++) {
1967
+ const remotes = versions[v].remotes;
1968
+ for (let r = 0; r < remotes.length; r++) {
1969
+ const name = remotes[r].name;
1970
+ const list = consumed.get(name);
1971
+ if (!list) consumed.set(name, [member.name]);
1972
+ else if (list[list.length - 1] !== member.name) list.push(member.name);
1973
+ }
1974
+ }
1975
+ }
1976
+ return consumed;
1977
+ }
1978
+ function consumedSpecifiers(members) {
1979
+ const consumed = /* @__PURE__ */ new Map();
1980
+ for (const member of members) {
1981
+ const versions = member.external.versions;
1982
+ for (let v = 0; v < versions.length; v++) {
1983
+ const remotes = versions[v].remotes;
1984
+ for (let r = 0; r < remotes.length; r++) {
1985
+ const meta = remotes[r];
1986
+ let own = consumed.get(meta.name);
1987
+ if (!own) consumed.set(meta.name, own = /* @__PURE__ */ new Set());
1988
+ for (const specifier in meta.entries) own.add(specifier);
1989
+ }
1990
+ }
1991
+ }
1992
+ return consumed;
1993
+ }
1994
+ function coversWholePool(members, build, islanded) {
1995
+ const served = /* @__PURE__ */ new Set();
1996
+ const wanted = /* @__PURE__ */ new Set();
1997
+ for (const member of members) {
1998
+ for (const version of member.external.versions) {
1999
+ for (const meta of version.remotes) {
2000
+ if (islanded.has(meta.name)) continue;
2001
+ const into = meta.name === build ? served : wanted;
2002
+ for (const specifier in meta.entries) into.add(specifier);
2003
+ }
2004
+ }
2005
+ }
2006
+ for (const specifier of wanted) if (!served.has(specifier)) return false;
2007
+ return true;
2008
+ }
2009
+ function servingBuilds(members, islanded) {
2010
+ const serving = /* @__PURE__ */ new Map();
2011
+ for (const member of members) {
2012
+ const shared = member.external.versions.find((v) => v.action === "share");
2013
+ const basis = shared?.remotes.find((r) => !islanded.has(r.name));
2014
+ if (basis) serving.set(member.name, basis.name);
2015
+ }
2016
+ return serving;
2017
+ }
2018
+ function basisPerMember(members, islanded, dedupsElsewhere) {
2019
+ const basis = /* @__PURE__ */ new Map();
2020
+ for (const member of members) {
2021
+ const winner = member.external.versions.find((v) => v.action === "share");
2022
+ if (!winner) continue;
2023
+ const own = winner.remotes.find(
2024
+ (r) => !islanded?.has(r.name) && !dedupsElsewhere?.(r.name)
2025
+ );
2026
+ if (own) basis.set(member.name, own.name);
2027
+ }
2028
+ return basis;
2029
+ }
2030
+ function hostRemotes(members) {
2031
+ const hosts = /* @__PURE__ */ new Set();
2032
+ for (const member of members) {
2033
+ for (const version of member.external.versions) {
2034
+ if (version.host && version.remotes.length > 0) hosts.add(version.remotes[0].name);
2035
+ }
2036
+ }
2037
+ return hosts;
2038
+ }
2039
+ function arrivalOrder(members) {
2040
+ const arrival = /* @__PURE__ */ new Map();
2041
+ for (const member of members) {
2042
+ for (const version of member.external.versions) {
2043
+ for (const meta of version.remotes) {
2044
+ if (!arrival.has(meta.name)) arrival.set(meta.name, arrival.size);
2045
+ }
2046
+ }
2047
+ }
2048
+ return arrival;
2049
+ }
2050
+ function sharedTagPerSpecifier(members, islanded) {
2051
+ const shared = /* @__PURE__ */ new Map();
2052
+ forEachGlobalClaim(members, islanded, (specifier, tag) => {
2053
+ if (!shared.has(specifier)) shared.set(specifier, tag);
2054
+ });
2055
+ return shared;
2056
+ }
2057
+ function forEachGlobalClaim(members, islanded, visit) {
2058
+ const accepts = islanded ? (meta) => !islanded.has(meta.name) : void 0;
2059
+ const claim = (version) => forEachVersionEntry(version, accepts, (specifier, meta) => visit(specifier, version.tag, meta));
2060
+ for (const member of members) {
2061
+ const versions = member.external.versions;
2062
+ const winner = versions.find((v) => v.action === "share");
2063
+ if (winner) claim(winner);
2064
+ for (let v = 0; v < versions.length; v++) {
2065
+ if (versions[v].action === "skip") claim(versions[v]);
2066
+ }
2067
+ }
2068
+ }
2069
+
2070
+ // src/lib/core/2.app/steps/pooling/anchoring.ts
2071
+ function acceptanceTable(members, isCompatible) {
2072
+ const table = /* @__PURE__ */ new Map();
2073
+ for (const member of members) {
2074
+ const tags = [];
2075
+ for (const version of member.external.versions) tags.push(version.tag);
2076
+ for (const version of member.external.versions) {
2077
+ for (const meta of version.remotes) {
2078
+ let byMember = table.get(meta.name);
2079
+ if (!byMember) table.set(meta.name, byMember = /* @__PURE__ */ new Map());
2080
+ if (byMember.has(member.name)) continue;
2081
+ const accepted = /* @__PURE__ */ new Set();
2082
+ for (let t = 0; t < tags.length; t++) {
2083
+ if (isCompatible(tags[t], meta.requiredVersion)) accepted.add(tags[t]);
1677
2084
  }
1678
- });
2085
+ byMember.set(member.name, accepted);
2086
+ }
1679
2087
  }
1680
- external.dirty = false;
1681
- return external;
1682
2088
  }
2089
+ return table;
2090
+ }
2091
+ function covers(coverage2, consumed) {
2092
+ for (const specifier of consumed) if (!coverage2.has(specifier)) return false;
2093
+ return true;
2094
+ }
2095
+ function acceptsAll(acceptance, build, consumer, consumed) {
2096
+ const byMember = acceptance.get(consumer);
2097
+ if (!byMember) return false;
2098
+ for (let i = 0; i < consumed.length; i++) {
2099
+ const offered = build.get(consumed[i]);
2100
+ if (offered === void 0) return false;
2101
+ if (!byMember.get(consumed[i])?.has(offered)) return false;
2102
+ }
2103
+ return true;
2104
+ }
2105
+ function isWitnessed(specifiers, shared, builds) {
2106
+ const wanted = [...specifiers];
2107
+ for (const specifier of wanted) if (!shared.has(specifier)) return false;
2108
+ for (const [, build] of builds) {
2109
+ let matches = true;
2110
+ for (let i = 0; i < wanted.length; i++) {
2111
+ if (build.tags.get(wanted[i]) !== shared.get(wanted[i])) {
2112
+ matches = false;
2113
+ break;
2114
+ }
2115
+ }
2116
+ if (matches) return true;
2117
+ }
2118
+ return false;
2119
+ }
2120
+ function explainSelfServe(remote, wants, specifiers, pool) {
2121
+ let closest;
2122
+ let fewest = Number.MAX_SAFE_INTEGER;
2123
+ let gap = wants[0] ?? "";
2124
+ for (const [build, view] of pool.builds) {
2125
+ if (build === remote) continue;
2126
+ let missing;
2127
+ let count = 0;
2128
+ for (const specifier of specifiers) {
2129
+ if (view.coverage.has(specifier)) continue;
2130
+ count++;
2131
+ missing ??= specifier;
2132
+ }
2133
+ if (count >= fewest) continue;
2134
+ fewest = count;
2135
+ closest = build;
2136
+ gap = missing ?? rejectedMember(view.instance, remote, wants, pool.acceptance) ?? gap;
2137
+ }
2138
+ return { gap, closest };
2139
+ }
2140
+ function rejectedMember(offered, consumer, wants, acceptance) {
2141
+ const accepts = acceptance.get(consumer);
2142
+ if (!accepts) return void 0;
2143
+ for (const member of wants) {
2144
+ const tag = offered.get(member);
2145
+ if (tag === void 0) return member;
2146
+ if (!accepts.get(member)?.has(tag)) return `${member}@${tag}`;
2147
+ }
2148
+ return void 0;
2149
+ }
2150
+ function assignAnchors(input) {
2151
+ const { builds, acceptance, consumedMembers: consumedMembers2, hosts, arrival } = input;
2152
+ const assignment = /* @__PURE__ */ new Map();
2153
+ const byPriority = (a, b) => (arrival.get(a) ?? Number.MAX_SAFE_INTEGER) - (arrival.get(b) ?? Number.MAX_SAFE_INTEGER) || a.localeCompare(b);
2154
+ const candidates = [...builds.keys()].sort(byPriority);
2155
+ const pending = new Set([...consumedMembers2.keys()].sort(byPriority));
2156
+ const anchored = /* @__PURE__ */ new Set();
2157
+ const canServe = (candidate, consumer) => {
2158
+ const build = builds.get(candidate);
2159
+ if (!covers(build.coverage, input.consumedSpecifiers.get(consumer) ?? [])) return false;
2160
+ return acceptsAll(acceptance, build.instance, consumer, consumedMembers2.get(consumer) ?? []);
2161
+ };
2162
+ const electAnchor = (anchor, served) => {
2163
+ pending.delete(anchor);
2164
+ assignment.set(anchor, void 0);
2165
+ for (const consumer of served) {
2166
+ pending.delete(consumer);
2167
+ anchored.add(consumer);
2168
+ assignment.set(consumer, anchor);
2169
+ }
2170
+ };
2171
+ for (const host of [...hosts].sort(byPriority)) {
2172
+ const served = [];
2173
+ if (builds.has(host)) {
2174
+ for (const consumer of pending) {
2175
+ if (consumer !== host && canServe(host, consumer)) served.push(consumer);
2176
+ }
2177
+ }
2178
+ electAnchor(host, served);
2179
+ }
2180
+ for (; ; ) {
2181
+ let best;
2182
+ let bestServed = [];
2183
+ for (const candidate of candidates) {
2184
+ if (anchored.has(candidate)) continue;
2185
+ const served = [];
2186
+ for (const consumer of pending) {
2187
+ if (consumer !== candidate && canServe(candidate, consumer)) served.push(consumer);
2188
+ }
2189
+ if (served.length > bestServed.length) {
2190
+ best = candidate;
2191
+ bestServed = served;
2192
+ }
2193
+ }
2194
+ if (best === void 0) break;
2195
+ electAnchor(best, bestServed);
2196
+ }
2197
+ for (const consumer of pending) assignment.set(consumer, void 0);
2198
+ return assignment;
1683
2199
  }
1684
2200
 
1685
2201
  // src/lib/core/2.app/steps/pooling/pool-graph.ts
@@ -1722,34 +2238,56 @@ function createDSU() {
1722
2238
  }
1723
2239
  var extNode = (name) => `ext\0${name}`;
1724
2240
  var tagNode = (remote, tag) => `tag\0${remote}\0${tag}`;
1725
- var scopeNode = (scope) => `scope\0${scope}`;
2241
+ var scopeNode = (remote, scope) => `scope\0${remote}\0${scope}`;
1726
2242
  function autoScope(name, useAutoExternalPooling) {
1727
2243
  return useAutoExternalPooling ? SCOPED_PACKAGE.exec(name)?.[1] : void 0;
1728
2244
  }
2245
+ function owningPackage(name) {
2246
+ const depth = name.startsWith("@") ? 2 : 1;
2247
+ let cut = -1;
2248
+ for (let seen = 0; seen < depth; seen++) {
2249
+ cut = name.indexOf("/", cut + 1);
2250
+ if (cut === -1) return void 0;
2251
+ }
2252
+ return name.slice(0, cut);
2253
+ }
1729
2254
  function groupByMembership(candidates, log) {
1730
2255
  const dsu = createDSU();
1731
2256
  const tagged = /* @__PURE__ */ new Set();
1732
- const poolable = [];
2257
+ const joined = /* @__PURE__ */ new Set();
2258
+ const tagRules = /* @__PURE__ */ new Set();
2259
+ for (const candidate of candidates) {
2260
+ if (candidate.scope === void 0) continue;
2261
+ for (const edge of candidate.tags) tagRules.add(`${edge.remote}\0${candidate.scope}`);
2262
+ }
1733
2263
  for (const candidate of candidates) {
1734
- let joined = false;
1735
- if (candidate.scope !== void 0) {
1736
- dsu.union(extNode(candidate.name), scopeNode(candidate.scope));
1737
- joined = true;
1738
- }
1739
2264
  for (const edge of candidate.tags) {
1740
2265
  dsu.union(extNode(candidate.name), tagNode(edge.remote, edge.tag));
1741
2266
  tagged.add(candidate.name);
1742
- joined = true;
2267
+ joined.add(candidate.name);
2268
+ }
2269
+ if (candidate.scope === void 0) continue;
2270
+ for (const remote of candidate.remotes) {
2271
+ if (tagRules.has(`${remote}\0${candidate.scope}`)) continue;
2272
+ dsu.union(extNode(candidate.name), scopeNode(remote, candidate.scope));
2273
+ joined.add(candidate.name);
1743
2274
  }
1744
- if (joined) poolable.push({ name: candidate.name, value: candidate.value });
2275
+ }
2276
+ const declared = new Set(candidates.map((c) => c.name));
2277
+ for (const candidate of candidates) {
2278
+ const owner = owningPackage(candidate.name);
2279
+ if (owner !== void 0 && declared.has(owner))
2280
+ dsu.union(extNode(candidate.name), extNode(owner));
1745
2281
  }
1746
2282
  const byComponent = /* @__PURE__ */ new Map();
1747
- for (const entry of poolable) {
1748
- const root = dsu.component(extNode(entry.name));
1749
- (byComponent.get(root) ?? byComponent.set(root, []).get(root)).push(entry);
2283
+ for (const candidate of candidates) {
2284
+ const root = dsu.component(extNode(candidate.name));
2285
+ const members = byComponent.get(root) ?? byComponent.set(root, []).get(root);
2286
+ members.push({ name: candidate.name, value: candidate.value });
1750
2287
  }
1751
2288
  const pools = [];
1752
2289
  for (const members of byComponent.values()) {
2290
+ if (!members.some((m) => joined.has(m.name))) continue;
1753
2291
  members.sort((a, b) => a.name.localeCompare(b.name));
1754
2292
  if (members.length < 2) {
1755
2293
  const only = members[0];
@@ -1777,6 +2315,7 @@ function buildPools(sharedExternals, useAutoExternalPooling, log) {
1777
2315
  return tag ? [{ remote: r.name, tag }] : [];
1778
2316
  })
1779
2317
  ),
2318
+ remotes: external.versions.flatMap((v) => v.remotes.map((r) => r.name)),
1780
2319
  value: { name, external }
1781
2320
  })
1782
2321
  );
@@ -1784,28 +2323,131 @@ function buildPools(sharedExternals, useAutoExternalPooling, log) {
1784
2323
  }
1785
2324
 
1786
2325
  // src/lib/core/2.app/steps/pooling/pool.util.ts
2326
+ function lazy(make) {
2327
+ let value;
2328
+ return () => value ??= make();
2329
+ }
1787
2330
  function remotesInPool(members) {
1788
2331
  return [
1789
2332
  ...new Set(members.flatMap((m) => m.external.versions.flatMap((v) => v.remotes.map((r) => r.name))))
1790
2333
  ];
1791
2334
  }
2335
+ function poolableScopes(config, repo) {
2336
+ const { useAutoExternalPooling } = config.feature;
2337
+ return {
2338
+ useAutoExternalPooling,
2339
+ scopes: repo.getScopes().filter(
2340
+ (scope) => repo.scopeType(scope) !== "strict" && (useAutoExternalPooling || repo.hasPoolTag(scope))
2341
+ )
2342
+ };
2343
+ }
1792
2344
 
1793
2345
  // src/lib/core/2.app/steps/pooling/pool-shared-externals.ts
1794
2346
  function islandedRemotes(members) {
1795
- const islanded = /* @__PURE__ */ new Set();
2347
+ const islanded = /* @__PURE__ */ new Map();
1796
2348
  for (const member of members)
1797
2349
  for (const version of member.external.versions)
1798
- if (version.action === "scope") for (const remote of version.remotes) islanded.add(remote.name);
2350
+ if (version.action === "scope") {
2351
+ for (const remote of version.remotes)
2352
+ if (!islanded.has(remote.name))
2353
+ islanded.set(remote.name, {
2354
+ kind: "incompatible",
2355
+ member: member.name,
2356
+ tag: version.tag
2357
+ });
2358
+ }
1799
2359
  return islanded;
1800
2360
  }
1801
- function createPoolSharedExternals(config, ports) {
1802
- return () => {
1803
- const { useAutoExternalPooling } = config.feature;
1804
- if (!useAutoExternalPooling && !ports.sharedExternalsRepo.hasPoolTag()) {
1805
- return Promise.resolve();
2361
+ var basisFor = (members, islanded, serving) => basisPerMember(members, islanded, (remote) => {
2362
+ const build = serving.get(remote);
2363
+ return build !== void 0 && build !== remote;
2364
+ });
2365
+ function servedPerRemote(consumed, serving, basis) {
2366
+ const served = /* @__PURE__ */ new Map();
2367
+ for (const [remote, build] of serving) {
2368
+ const byMember = /* @__PURE__ */ new Map();
2369
+ for (const name of consumed.get(remote) ?? []) {
2370
+ if (basis.get(name) !== build) byMember.set(name, build);
2371
+ }
2372
+ if (byMember.size > 0) served.set(remote, byMember);
2373
+ }
2374
+ return served;
2375
+ }
2376
+ var anyAnchorStored = (members) => members.some((m) => m.external.versions.some((v) => v.remotes.some((r) => r.servedBy !== void 0)));
2377
+ function findTornRemotes(members, islanded, basis, served, hosts) {
2378
+ const { suspect, selfServed } = movedRemotes(members, islanded, basis, served, hosts);
2379
+ if (suspect.size === 0) return [];
2380
+ const builds = liveBuilds(members, islanded);
2381
+ const shared = sharedTagPerSpecifier(members, islanded);
2382
+ const own = ownCopies(members, suspect);
2383
+ const torn = [];
2384
+ for (const remote of suspect) {
2385
+ const copies = own.get(remote) ?? [];
2386
+ const mine = selfServed.get(remote);
2387
+ const anchors = served.get(remote);
2388
+ const ownTags = /* @__PURE__ */ new Map();
2389
+ const resolved = /* @__PURE__ */ new Map();
2390
+ for (const copy of copies) {
2391
+ const from = mine?.has(copy.member) ? remote : anchors?.get(copy.member);
2392
+ for (const specifier in copy.entries) {
2393
+ if (!ownTags.has(specifier)) ownTags.set(specifier, copy.tag);
2394
+ const tag = from === remote ? copy.tag : from !== void 0 ? builds.get(from)?.tags.get(specifier) : shared.get(specifier);
2395
+ if (tag !== void 0 && !resolved.has(specifier)) resolved.set(specifier, tag);
2396
+ }
2397
+ }
2398
+ if (resolved.size === 0) continue;
2399
+ if (ships(ownTags, resolved)) continue;
2400
+ let witnessed = false;
2401
+ for (const [, build] of builds) {
2402
+ if (ships(build.tags, resolved)) {
2403
+ witnessed = true;
2404
+ break;
2405
+ }
1806
2406
  }
1807
- for (const scope of ports.sharedExternalsRepo.getScopes()) {
1808
- if (ports.sharedExternalsRepo.scopeType(scope) === "strict") continue;
2407
+ if (!witnessed) {
2408
+ torn.push({
2409
+ remote,
2410
+ combination: [...resolved].map(([specifier, tag]) => `${specifier}@${tag}`).join(", ")
2411
+ });
2412
+ }
2413
+ }
2414
+ return torn;
2415
+ }
2416
+ var ships = (build, resolved) => {
2417
+ if (!build) return false;
2418
+ for (const [specifier, tag] of resolved) if (build.get(specifier) !== tag) return false;
2419
+ return true;
2420
+ };
2421
+ function movedRemotes(members, islanded, basis, served, hosts) {
2422
+ const suspect = /* @__PURE__ */ new Set();
2423
+ const selfServed = /* @__PURE__ */ new Map();
2424
+ const add = (remote) => {
2425
+ if (!islanded.has(remote) && !hosts.has(remote)) suspect.add(remote);
2426
+ };
2427
+ for (const remote of served.keys()) add(remote);
2428
+ for (const member of members) {
2429
+ const unpublished = !basis.has(member.name);
2430
+ for (const version of member.external.versions) {
2431
+ const scoped = version.action === "scope";
2432
+ if (!unpublished && !scoped) continue;
2433
+ for (const meta of version.remotes) {
2434
+ add(meta.name);
2435
+ const servesItself = scoped || unpublished && served.get(meta.name)?.get(member.name) === void 0;
2436
+ if (!servesItself) continue;
2437
+ let mine = selfServed.get(meta.name);
2438
+ if (!mine) selfServed.set(meta.name, mine = /* @__PURE__ */ new Set());
2439
+ mine.add(member.name);
2440
+ }
2441
+ }
2442
+ }
2443
+ return { suspect, selfServed };
2444
+ }
2445
+ function createPoolSharedExternals(config, ports) {
2446
+ return (touched) => {
2447
+ const { useAutoExternalPooling, scopes } = poolableScopes(config, ports.sharedExternalsRepo);
2448
+ for (const scope of scopes) {
2449
+ const touchedInScope = touched?.get(scope);
2450
+ if (touched && !touchedInScope) continue;
1809
2451
  const sharedExternals = ports.sharedExternalsRepo.getFromScope(scope);
1810
2452
  try {
1811
2453
  for (const [poolName, members] of buildPools(
@@ -1813,6 +2455,7 @@ function createPoolSharedExternals(config, ports) {
1813
2455
  useAutoExternalPooling,
1814
2456
  config.log
1815
2457
  )) {
2458
+ if (touchedInScope && !members.some((m) => touchedInScope.has(m.name))) continue;
1816
2459
  poolFamily(poolName, members, scope);
1817
2460
  }
1818
2461
  } catch (error) {
@@ -1833,45 +2476,138 @@ function createPoolSharedExternals(config, ports) {
1833
2476
  const allRemotes = remotesInPool(members);
1834
2477
  if (allRemotes.length < 2) return;
1835
2478
  const islanded = islandedRemotes(members);
2479
+ const consumed = consumedMembers(members);
2480
+ const hosts = hostRemotes(members);
2481
+ const views = {
2482
+ consumedSpecifiers: lazy(() => consumedSpecifiers(members)),
2483
+ acceptance: lazy(() => acceptanceTable(members, ports.versionCheck.isCompatible)),
2484
+ arrival: lazy(() => arrivalOrder(members))
2485
+ };
1836
2486
  config.log.debug(
1837
2487
  3,
1838
- `[${scope}][pool:${poolName}] ${members.length} members across ${allRemotes.length} remotes, islanded={${[...islanded].join(", ") || "\u2205"}}
2488
+ `[${scope}][pool:${poolName}] ${members.length} members across ${allRemotes.length} remotes, incompatible={${[...islanded.keys()].join(", ") || "\u2205"}}
1839
2489
  ` + members.map((m) => ` - ${m.name}`).join("\n")
1840
2490
  );
1841
- if (islanded.size > 0 && config.strict.strictExternalCompatibility) {
2491
+ const assign = () => {
2492
+ const serving2 = assignServingBuilds(members, islanded, consumed, hosts, views);
2493
+ const basis2 = basisFor(members, islanded, serving2);
2494
+ return { serving: serving2, basis: basis2, served: servedPerRemote(consumed, serving2, basis2) };
2495
+ };
2496
+ let { serving, basis, served } = assign();
2497
+ if (islanded.size === 0 && serving.size === 0 && !anyAnchorStored(members)) return;
2498
+ for (; ; ) {
2499
+ const torn = findTornRemotes(members, islanded, basis, served, hosts);
2500
+ if (torn.length === 0) break;
2501
+ for (const { remote, combination } of torn)
2502
+ islanded.set(remote, { kind: "torn", combination });
2503
+ ({ serving, basis, served } = assign());
2504
+ }
2505
+ const incompatible = [...islanded].filter(([, cause]) => cause.kind === "incompatible").map(([remote]) => remote);
2506
+ if (config.strict.strictExternalCompatibility && incompatible.length > 0) {
1842
2507
  config.log.error(
1843
2508
  3,
1844
- `[${scope}][pool:${poolName}] version-incompatible remotes cannot be pooled: {${[...islanded].join(", ")}}.`
2509
+ `[${scope}][pool:${poolName}] version-incompatible remotes cannot be pooled: {${incompatible.join(", ")}}.`
1845
2510
  );
1846
2511
  throw new NFError(`Could not pool '${poolName}' in scope ${scope}.`);
1847
2512
  }
2513
+ for (const [remote, cause] of islanded) {
2514
+ const scoped = consumed.get(remote)?.length ?? 0;
2515
+ config.log.warn(3, `[${scope}][pool:${poolName}] ${islandWarning(remote, cause, scoped)}`);
2516
+ }
1848
2517
  for (const member of members) {
1849
- const rebuilt = rebuildMember(member, islanded);
1850
- warnIfScopedOnly(poolName, member.name, rebuilt, scope);
2518
+ const rebuilt = rebuildMember(member, islanded, served, basis.get(member.name));
2519
+ warnIfScopedOnly(poolName, member, rebuilt, islanded, scope);
1851
2520
  ports.sharedExternalsRepo.addOrUpdate(member.name, rebuilt, scope);
1852
2521
  }
1853
2522
  }
1854
- function warnIfScopedOnly(poolName, memberName, rebuilt, scope) {
2523
+ function assignServingBuilds(members, islanded, consumed, hosts, views) {
2524
+ for (; ; ) {
2525
+ const bases = servingBuilds(members, islanded);
2526
+ const distinct = new Set(bases.values());
2527
+ if (distinct.size === 1 && bases.size === members.length) {
2528
+ const sole = [...distinct][0];
2529
+ if (coversWholePool(members, sole, islanded)) return /* @__PURE__ */ new Map();
2530
+ }
2531
+ const builds = liveBuilds(members, islanded);
2532
+ const shared = sharedTagPerSpecifier(members, islanded);
2533
+ const consumedSpec = views.consumedSpecifiers();
2534
+ const needAnchor = /* @__PURE__ */ new Map();
2535
+ const needSpecifiers = /* @__PURE__ */ new Map();
2536
+ for (const [remote, wants] of consumed) {
2537
+ if (islanded.has(remote)) continue;
2538
+ if (isWitnessed(consumedSpec.get(remote) ?? [], shared, builds)) continue;
2539
+ needAnchor.set(remote, wants);
2540
+ needSpecifiers.set(remote, consumedSpec.get(remote) ?? /* @__PURE__ */ new Set());
2541
+ }
2542
+ if (needAnchor.size === 0) return /* @__PURE__ */ new Map();
2543
+ const acceptance = views.acceptance();
2544
+ const assignment = assignAnchors({
2545
+ builds,
2546
+ acceptance,
2547
+ consumedSpecifiers: needSpecifiers,
2548
+ consumedMembers: needAnchor,
2549
+ hosts,
2550
+ arrival: views.arrival()
2551
+ });
2552
+ const anchors = /* @__PURE__ */ new Set();
2553
+ for (const build of assignment.values()) if (build !== void 0) anchors.add(build);
2554
+ const serving = /* @__PURE__ */ new Map();
2555
+ let scoped = false;
2556
+ for (const [remote, wants] of needAnchor) {
2557
+ const assigned = assignment.get(remote);
2558
+ const build = assigned ?? (anchors.has(remote) || hosts.has(remote) ? remote : void 0);
2559
+ if (build === void 0) {
2560
+ islanded.set(remote, {
2561
+ kind: "uncovered",
2562
+ ...explainSelfServe(remote, wants, needSpecifiers.get(remote), { builds, acceptance })
2563
+ });
2564
+ scoped = true;
2565
+ continue;
2566
+ }
2567
+ serving.set(remote, build);
2568
+ }
2569
+ if (!scoped) return serving;
2570
+ }
2571
+ }
2572
+ function warnIfScopedOnly(poolName, member, rebuilt, islanded, scope) {
1855
2573
  if (rebuilt.versions.some((v) => v.action === "share")) return;
1856
- const consumers = new Set(rebuilt.versions.flatMap((v) => v.remotes.map((r) => r.name))).size;
2574
+ const consumers = new Set(
2575
+ rebuilt.versions.filter((v) => v.action === "scope").flatMap((v) => v.remotes.map((r) => r.name))
2576
+ ).size;
1857
2577
  if (consumers < 2) return;
2578
+ const provider = member.external.versions.find((v) => v.action === "share");
2579
+ if (provider?.remotes.some((r) => islanded.has(r.name))) return;
1858
2580
  config.log.warn(
1859
2581
  3,
1860
- `[${scope}][pool:${poolName}] '${memberName}' is scoped-only \u2014 no coherent shared build provides it; ${consumers} remotes download their own copy.`
2582
+ `[${scope}][pool:${poolName}] '${member.name}' is scoped-only \u2014 no coherent shared build provides it; ${consumers} remotes download their own copy.`
1861
2583
  );
1862
2584
  }
1863
- function rebuildMember(member, islanded) {
1864
- const entries = member.external.versions.flatMap(
1865
- (v) => v.remotes.map((meta) => ({ remote: meta.name, tag: v.tag, host: v.host, action: v.action, meta }))
1866
- );
1867
- const isScoped = (e) => islanded.has(e.remote) || e.action === "scope";
1868
- let scoped = entries.filter(isScoped);
1869
- let clean = entries.filter((e) => !isScoped(e));
1870
- if (!clean.some((e) => e.action === "share")) {
1871
- scoped = [...scoped, ...clean];
1872
- clean = [];
1873
- }
1874
- const shareEntries = clean.filter((e) => e.action === "share");
2585
+ function rebuildMember(member, islanded, served, basis) {
2586
+ let scoped = [];
2587
+ let clean = [];
2588
+ for (const version of member.external.versions) {
2589
+ for (const meta of version.remotes) {
2590
+ const entry = {
2591
+ remote: meta.name,
2592
+ tag: version.tag,
2593
+ host: version.host,
2594
+ action: version.action,
2595
+ meta
2596
+ };
2597
+ (islanded.has(entry.remote) || entry.action === "scope" ? scoped : clean).push(entry);
2598
+ }
2599
+ }
2600
+ for (const entry of scoped) delete entry.meta.servedBy;
2601
+ for (const entry of clean) {
2602
+ const build = served.get(entry.remote)?.get(member.name);
2603
+ if (build !== void 0) entry.meta.servedBy = build;
2604
+ else delete entry.meta.servedBy;
2605
+ }
2606
+ if (basis === void 0) {
2607
+ scoped = [...scoped, ...clean.filter((e) => e.meta.servedBy === void 0)];
2608
+ clean = clean.filter((e) => e.meta.servedBy !== void 0).map((e) => ({ ...e, action: "skip" }));
2609
+ }
2610
+ const shareEntries = clean.filter((e) => e.action === "share").sort((a, b) => Number(b.remote === basis) - Number(a.remote === basis));
1875
2611
  const shareVersion = shareEntries.length > 0 ? [
1876
2612
  {
1877
2613
  tag: shareEntries[0].tag,
@@ -1880,37 +2616,70 @@ function createPoolSharedExternals(config, ports) {
1880
2616
  remotes: shareEntries.map((e) => e.meta)
1881
2617
  }
1882
2618
  ] : [];
1883
- const skipByTag = /* @__PURE__ */ new Map();
1884
- for (const e of clean) {
1885
- if (e.action !== "skip") continue;
1886
- const version = skipByTag.get(e.tag) ?? {
1887
- tag: e.tag,
1888
- host: e.host,
1889
- action: "skip",
1890
- remotes: []
1891
- };
1892
- version.remotes.push(e.meta);
1893
- skipByTag.set(e.tag, version);
1894
- }
1895
- const scopeByTag = /* @__PURE__ */ new Map();
1896
- for (const e of scoped) {
1897
- const version = scopeByTag.get(e.tag) ?? {
1898
- tag: e.tag,
1899
- host: false,
1900
- action: "scope",
1901
- remotes: []
1902
- };
1903
- version.remotes.push(e.meta);
1904
- scopeByTag.set(e.tag, version);
1905
- }
2619
+ const byTag = (entries, action) => {
2620
+ const versions = /* @__PURE__ */ new Map();
2621
+ for (const entry of entries) {
2622
+ const version = versions.get(entry.tag) ?? {
2623
+ tag: entry.tag,
2624
+ host: entry.host,
2625
+ action,
2626
+ remotes: []
2627
+ };
2628
+ version.remotes.push(entry.meta);
2629
+ versions.set(entry.tag, version);
2630
+ }
2631
+ return [...versions.values()];
2632
+ };
1906
2633
  return {
1907
2634
  dirty: false,
1908
- versions: [...shareVersion, ...skipByTag.values(), ...scopeByTag.values()]
2635
+ versions: [
2636
+ ...shareVersion,
2637
+ ...byTag(
2638
+ clean.filter((e) => e.action === "skip"),
2639
+ "skip"
2640
+ ),
2641
+ ...byTag(scoped, "scope")
2642
+ ].sort((a, b) => ports.versionCheck.compare(b.tag, a.tag))
1909
2643
  };
1910
2644
  }
1911
2645
  }
2646
+ function islandWarning(remote, cause, scoped) {
2647
+ switch (cause.kind) {
2648
+ case "incompatible":
2649
+ return `'${remote}' is islanded: the resolver scoped its '${cause.member}@${cause.tag}', so all ${scoped} members it imports are scoped for it.`;
2650
+ case "torn":
2651
+ return `'${remote}' serves its own family: the mapping would have handed it ${cause.combination}, which no build shipped together, so all ${scoped} members it imports are scoped for it.`;
2652
+ default: {
2653
+ const closest = cause.closest ? `closest is '${cause.closest}'` : "no other build in the pool serves any of it";
2654
+ return `'${remote}' serves its own family: no shared build offers every entrypoint it imports at a version it accepts \u2014 '${cause.gap}' is the gap, ${closest}. All ${scoped} members it imports are scoped for it.`;
2655
+ }
2656
+ }
2657
+ }
1912
2658
 
1913
- // node_modules/.pnpm/@softarc+native-federation@4.3.2_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/domain/core/build-notification-options.contract.js
2659
+ // src/lib/core/2.app/steps/pooling/mark-pools-for-reelection.ts
2660
+ function createMarkPoolsForReelection(config, ports) {
2661
+ return () => {
2662
+ const { useAutoExternalPooling, scopes } = poolableScopes(config, ports.sharedExternalsRepo);
2663
+ for (const scope of scopes) {
2664
+ const sharedExternals = ports.sharedExternalsRepo.getFromScope(scope);
2665
+ if (!Object.values(sharedExternals).some((external) => external.dirty)) continue;
2666
+ let spread = 0;
2667
+ for (const [, members] of buildPools(sharedExternals, useAutoExternalPooling)) {
2668
+ if (!members.some((m) => m.external.dirty)) continue;
2669
+ for (const member of members)
2670
+ if (!member.external.dirty) {
2671
+ member.external.dirty = true;
2672
+ spread++;
2673
+ }
2674
+ }
2675
+ if (spread > 0)
2676
+ config.log.debug(3, `[${scope}] ${spread} pool member(s) marked dirty for re-election.`);
2677
+ }
2678
+ return Promise.resolve();
2679
+ };
2680
+ }
2681
+
2682
+ // node_modules/.pnpm/@softarc+native-federation@4.4.0_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/domain/core/build-notification-options.contract.js
1914
2683
  var BuildNotificationType = /* @__PURE__ */ ((BuildNotificationType2) => {
1915
2684
  BuildNotificationType2["COMPLETED"] = "federation-rebuild-complete";
1916
2685
  BuildNotificationType2["ERROR"] = "federation-rebuild-error";
@@ -1918,7 +2687,7 @@ var BuildNotificationType = /* @__PURE__ */ ((BuildNotificationType2) => {
1918
2687
  return BuildNotificationType2;
1919
2688
  })(BuildNotificationType || {});
1920
2689
 
1921
- // node_modules/.pnpm/@softarc+native-federation@4.3.2_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/domain/core/chunk.js
2690
+ // node_modules/.pnpm/@softarc+native-federation@4.4.0_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/domain/core/chunk.js
1922
2691
  var CHUNK_PREFIX = "@nf-internal";
1923
2692
  function toChunkImport(fileName) {
1924
2693
  if (fileName.startsWith("./")) {
@@ -1979,9 +2748,10 @@ function createGenerateImportMap(config, ports) {
1979
2748
  }
1980
2749
  function processshareScope(importMap, shareScope, chunkBundles) {
1981
2750
  const sharedExternals = ports.sharedExternalsRepo.getFromScope(shareScope);
2751
+ const index = buildServedIndex(sharedExternals);
1982
2752
  for (const [externalName, external] of Object.entries(sharedExternals)) {
1983
2753
  let override = void 0;
1984
- let overrideScope = void 0;
2754
+ const cachedBefore = cachedFingerprint(external);
1985
2755
  for (const version of external.versions) {
1986
2756
  if (version.action === "scope") {
1987
2757
  for (const remote of version.remotes) {
@@ -1997,37 +2767,53 @@ function createGenerateImportMap(config, ports) {
1997
2767
  continue;
1998
2768
  }
1999
2769
  version.remotes[0].cached = true;
2000
- let source = version.remotes[0];
2001
- let sourceScope = getScope2(shareScope, source.name, externalName);
2002
- if (version.action === "share") {
2003
- registerBundleChunks(chunkBundles, source.name, source.bundle);
2004
- } else if (version.action === "skip") {
2770
+ let serving = version;
2771
+ if (version.action === "skip") {
2005
2772
  if (!override) {
2006
2773
  override = findOverride(external, shareScope, externalName) ?? "NOT_AVAILABLE";
2007
2774
  }
2008
2775
  if (override !== "NOT_AVAILABLE") {
2009
- if (!overrideScope)
2010
- overrideScope = getScope2(shareScope, override.remotes[0].name, externalName);
2011
- source = override.remotes[0];
2012
- sourceScope = overrideScope;
2013
- override.remotes[0].cached = true;
2776
+ serving = override;
2014
2777
  version.remotes[0].cached = false;
2015
2778
  }
2016
2779
  }
2017
- const mappings = Object.entries(source.entries).map(([packageName, file]) => ({
2018
- packageName,
2019
- url: join(sourceScope, file),
2020
- name: source.name,
2021
- file
2022
- }));
2780
+ const provided = versionEntries(serving);
2781
+ const mappings = [];
2782
+ for (const [packageName, provider] of provided) {
2783
+ const file = provider.entries[packageName];
2784
+ mappings.push({
2785
+ packageName,
2786
+ url: join(getScope2(shareScope, provider.name, externalName), file),
2787
+ name: provider.name,
2788
+ file
2789
+ });
2790
+ provider.cached = true;
2791
+ if (version.action === "share") {
2792
+ registerBundleChunks(chunkBundles, provider.name, provider.bundle);
2793
+ }
2794
+ }
2023
2795
  version.remotes.forEach((r) => {
2024
2796
  const rScope = getScope2(shareScope, r.name, externalName);
2797
+ if (index && r.servedBy) {
2798
+ const files = index.get(r.servedBy);
2799
+ const servedScope = getScope2(shareScope, r.servedBy, externalName);
2800
+ for (const specifier of Object.keys(r.entries)) {
2801
+ const source = files?.get(specifier);
2802
+ if (!source) continue;
2803
+ const url = join(servedScope, source.file);
2804
+ addToScope(importMap, rScope, { [specifier]: url });
2805
+ addIntegrity(importMap, url, r.servedBy, source.file);
2806
+ registerBundleChunks(chunkBundles, r.servedBy, source.bundle);
2807
+ source.meta.cached = true;
2808
+ }
2809
+ return;
2810
+ }
2025
2811
  for (const m of mappings) {
2026
2812
  addToScope(importMap, rScope, { [m.packageName]: m.url });
2027
2813
  }
2028
2814
  for (const [packageName, file] of Object.entries(r.entries)) {
2029
- if (packageName in source.entries) continue;
2030
- if (config.strict.strictEntryPointCoverage) {
2815
+ if (provided.has(packageName)) continue;
2816
+ if (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints) {
2031
2817
  warnUncoveredEntrypoint(shareScope, externalName, r.name, packageName);
2032
2818
  continue;
2033
2819
  }
@@ -2041,9 +2827,14 @@ function createGenerateImportMap(config, ports) {
2041
2827
  addIntegrity(importMap, m.url, m.name, m.file);
2042
2828
  }
2043
2829
  }
2044
- ports.sharedExternalsRepo.addOrUpdate(externalName, external, shareScope);
2830
+ if (cachedFingerprint(external) !== cachedBefore) {
2831
+ ports.sharedExternalsRepo.addOrUpdate(externalName, external, shareScope);
2832
+ }
2045
2833
  }
2046
2834
  }
2835
+ function cachedFingerprint(external) {
2836
+ return external.versions.map((v) => v.remotes.map((r) => r.cached ? 1 : 0).join("")).join("|");
2837
+ }
2047
2838
  function findOverride(external, shareScope, externalName) {
2048
2839
  const sharedVersions = external.versions.filter((v) => v.action === "share");
2049
2840
  const scopedExternalName = `${shareScope}.${externalName}`;
@@ -2072,10 +2863,68 @@ function createGenerateImportMap(config, ports) {
2072
2863
  }
2073
2864
  config.log.warn(4, `ShareScope external ${scopedExternalName} has multiple shared versions.`);
2074
2865
  }
2866
+ function buildServedIndex(externals) {
2867
+ let anyServed = false;
2868
+ for (const external of Object.values(externals)) {
2869
+ for (const version of external.versions) {
2870
+ if (version.remotes.some((r) => r.servedBy)) anyServed = true;
2871
+ }
2872
+ }
2873
+ if (!anyServed) return void 0;
2874
+ const index = /* @__PURE__ */ new Map();
2875
+ for (const external of Object.values(externals)) {
2876
+ for (const version of external.versions) {
2877
+ for (const remote of version.remotes) {
2878
+ let own = index.get(remote.name);
2879
+ if (!own) index.set(remote.name, own = /* @__PURE__ */ new Map());
2880
+ for (const [specifier, file] of Object.entries(remote.entries)) {
2881
+ if (!own.has(specifier))
2882
+ own.set(specifier, { file, bundle: remote.bundle, meta: remote });
2883
+ }
2884
+ }
2885
+ }
2886
+ }
2887
+ return index;
2888
+ }
2889
+ function collectServed(into, index, ctx, externalName, version) {
2890
+ for (const consumer of version.remotes) {
2891
+ if (!consumer.servedBy) continue;
2892
+ const files = index.get(consumer.servedBy);
2893
+ const scope = getScope2(ctx, consumer.name, externalName);
2894
+ for (const specifier of Object.keys(consumer.entries)) {
2895
+ const source = files?.get(specifier);
2896
+ if (!source) {
2897
+ config.log.warn(
2898
+ 4,
2899
+ `[${ctx}][${externalName}][${consumer.name}] '${consumer.servedBy}' does not serve '${specifier}'.`
2900
+ );
2901
+ continue;
2902
+ }
2903
+ into.push({ scope, specifier, source, from: consumer.servedBy });
2904
+ }
2905
+ }
2906
+ }
2907
+ function flushServed(importMap, chunkBundles, served) {
2908
+ for (const { scope, specifier, source, from } of served) {
2909
+ const url = join(getScope2(GLOBAL_SCOPE, from), source.file);
2910
+ if (importMap.imports[specifier] === url) continue;
2911
+ addToScope(importMap, scope, { [specifier]: url });
2912
+ addIntegrity(importMap, url, from, source.file);
2913
+ registerBundleChunks(chunkBundles, from, source.bundle);
2914
+ source.meta.cached = true;
2915
+ }
2916
+ }
2075
2917
  function addGlobalSharedExternals(importMap, chunkBundles) {
2076
2918
  const sharedExternals = ports.sharedExternalsRepo.getFromScope();
2919
+ const index = buildServedIndex(sharedExternals);
2920
+ const served = [];
2921
+ const cachedBefore = /* @__PURE__ */ new Map();
2077
2922
  for (const [externalName, external] of Object.entries(sharedExternals)) {
2923
+ cachedBefore.set(externalName, cachedFingerprint(external));
2078
2924
  for (const version of external.versions) {
2925
+ if (index && version.action !== "scope") {
2926
+ collectServed(served, index, GLOBAL_SCOPE, externalName, version);
2927
+ }
2079
2928
  if (version.action === "skip") continue;
2080
2929
  if (version.action === "scope") {
2081
2930
  for (const remote of version.remotes) {
@@ -2094,39 +2943,64 @@ function createGenerateImportMap(config, ports) {
2094
2943
  notifyDuplicateGlobalExternal(externalName);
2095
2944
  continue;
2096
2945
  }
2097
- const scope = getScope2(GLOBAL_SCOPE, version.remotes[0].name, externalName);
2098
- for (const [packageName, file] of Object.entries(version.remotes[0].entries)) {
2099
- const url = join(scope, file);
2100
- addToGlobal(importMap, { [packageName]: url });
2101
- addIntegrity(importMap, url, version.remotes[0].name, file);
2102
- }
2103
- registerBundleChunks(chunkBundles, version.remotes[0].name, version.remotes[0].bundle);
2104
- version.remotes[0].cached = true;
2946
+ mergeVersionEntries(importMap, chunkBundles, externalName, version);
2105
2947
  }
2106
2948
  for (const version of external.versions) {
2107
2949
  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
- }
2950
+ selfFillUncovered(importMap, chunkBundles, externalName, version.remotes);
2951
+ }
2952
+ }
2953
+ flushServed(importMap, chunkBundles, served);
2954
+ for (const [externalName, external] of Object.entries(sharedExternals)) {
2955
+ if (cachedFingerprint(external) !== cachedBefore.get(externalName)) {
2956
+ ports.sharedExternalsRepo.addOrUpdate(externalName, external);
2122
2957
  }
2123
- ports.sharedExternalsRepo.addOrUpdate(externalName, external);
2124
2958
  }
2125
2959
  return importMap;
2126
2960
  }
2961
+ function mergeVersionEntries(importMap, chunkBundles, externalName, version) {
2962
+ const scopes = /* @__PURE__ */ new Map();
2963
+ for (const [packageName, remote] of versionEntries(version)) {
2964
+ if (importMap.imports[packageName]) {
2965
+ config.log.debug(
2966
+ 4,
2967
+ `[${GLOBAL_SCOPE}][${externalName}][${remote.name}] Entrypoint '${packageName}' is already mapped by another external, keeping '${importMap.imports[packageName]}'.`
2968
+ );
2969
+ continue;
2970
+ }
2971
+ let scope = scopes.get(remote.name);
2972
+ if (scope === void 0) {
2973
+ scopes.set(remote.name, scope = getScope2(GLOBAL_SCOPE, remote.name, externalName));
2974
+ registerBundleChunks(chunkBundles, remote.name, remote.bundle);
2975
+ remote.cached = true;
2976
+ }
2977
+ const file = remote.entries[packageName];
2978
+ const url = join(scope, file);
2979
+ addToGlobal(importMap, { [packageName]: url });
2980
+ addIntegrity(importMap, url, remote.name, file);
2981
+ }
2982
+ }
2983
+ function selfFillUncovered(importMap, chunkBundles, externalName, remotes) {
2984
+ for (const remote of remotes) {
2985
+ if (remote.servedBy) continue;
2986
+ for (const [packageName, file] of Object.entries(remote.entries)) {
2987
+ if (importMap.imports[packageName]) continue;
2988
+ if (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints) {
2989
+ warnUncoveredEntrypoint(GLOBAL_SCOPE, externalName, remote.name, packageName);
2990
+ continue;
2991
+ }
2992
+ const scope = getScope2(GLOBAL_SCOPE, remote.name, externalName);
2993
+ const url = join(scope, file);
2994
+ addToGlobal(importMap, { [packageName]: url });
2995
+ addIntegrity(importMap, url, remote.name, file);
2996
+ registerBundleChunks(chunkBundles, remote.name, remote.bundle);
2997
+ remote.cached = true;
2998
+ }
2999
+ }
3000
+ }
2127
3001
  function warnUncoveredEntrypoint(scope, externalName, remoteName, packageName) {
2128
3002
  const msg = `[${scope}][${externalName}][${remoteName}] Entrypoint '${packageName}' is not covered by the shared version.`;
2129
- if (config.strict.strictImportMap) {
3003
+ if (config.strict.strictEntryPointCoverage || config.strict.strictImportMap) {
2130
3004
  config.log.error(4, msg);
2131
3005
  throw new NFError("Could not create ImportMap.");
2132
3006
  }
@@ -2354,18 +3228,24 @@ function createGetRemoteEntry(config, ports) {
2354
3228
  // src/lib/core/2.app/steps/update-cache.ts
2355
3229
  function createUpdateCache(config, ports) {
2356
3230
  const storeRemoteEntry = createStoreRemoteEntry(config, ports, 8);
3231
+ const removeCachedRemoteEntries = createRemoveCachedRemoteEntries(ports);
2357
3232
  return (remoteEntry) => {
2358
3233
  try {
2359
3234
  const actions = {};
3235
+ if (remoteEntry?.override) removeCachedRemoteEntries(/* @__PURE__ */ new Set([remoteEntry.name]));
2360
3236
  storeRemoteEntry(remoteEntry, (entry, external, ctx) => {
2361
- const { action, sharedVersion } = resolveSharedExternal(entry, external, ctx);
3237
+ const { action, provided, sameVersion } = resolveSharedExternal(entry, external, ctx);
2362
3238
  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
- );
3239
+ if (action === "skip" && provided) {
3240
+ actions[external.packageName].covered = Array.from(provided.keys());
3241
+ if (sameVersion) actions[external.packageName].sameVersion = true;
3242
+ if (external.shareScope) {
3243
+ actions[external.packageName].override = resolveOverrideEntries(
3244
+ entry,
3245
+ external,
3246
+ provided
3247
+ );
3248
+ }
2369
3249
  }
2370
3250
  });
2371
3251
  return Promise.resolve({ entry: remoteEntry, actions });
@@ -2393,78 +3273,188 @@ function createUpdateCache(config, ports) {
2393
3273
  }
2394
3274
  config.log.warn(8, errorMsg);
2395
3275
  }
2396
- if (action === "skip" && config.strict.strictEntryPointCoverage && sharedVersion && Object.keys(remote.entries).some((e) => !(e in sharedVersion.remotes[0].entries))) {
2397
- action = "scope";
3276
+ const provided = sharedVersion ? sharedInfo.shareScope ? versionEntries(sharedVersion) : committedEntries(sharedVersion) : void 0;
3277
+ const sameVersion = sharedVersion?.tag === tag;
3278
+ if (action === "skip" && provided && !sameVersion) {
3279
+ const uncovered = uncoveredEntrypoints(remote, provided);
3280
+ if (uncovered.length > 0) {
3281
+ const msg = `[${sharedInfo.shareScope ?? GLOBAL_SCOPE}][${remoteEntry.name}][${sharedInfo.packageName}] Entrypoints not covered by the shared version: ${uncovered.join(", ")}.`;
3282
+ if (config.strict.strictEntryPointCoverage) {
3283
+ config.log.error(8, msg);
3284
+ throw new NFError(`Could not process remote '${remoteEntry.name}'`);
3285
+ }
3286
+ if (config.profile.scopeUncoveredEntrypoints) {
3287
+ config.log.debug(8, msg);
3288
+ action = "scope";
3289
+ }
3290
+ }
2398
3291
  }
2399
- const matchingVersion = cached.versions.find((cached2) => cached2.tag === tag);
2400
- if (matchingVersion) {
3292
+ const matchingVersion = findVersionForTag(cached.versions, tag);
3293
+ if (action === "scope") {
3294
+ remote.cached = true;
3295
+ const scoped = cached.versions.find((v) => v.tag === tag && v.action === "scope");
3296
+ if (scoped) scoped.remotes.push(remote);
3297
+ else cached.versions.push({ tag, action, host: false, remotes: [remote] });
3298
+ } else if (matchingVersion) {
2401
3299
  assertSameVersionCompatibility(matchingVersion);
2402
- matchingVersion.remotes.push(remote);
3300
+ addRemoteToVersion(matchingVersion, remote);
2403
3301
  } else {
2404
3302
  if (!sharedVersion) action = "share";
2405
3303
  remote.cached = action !== "skip";
2406
3304
  cached.versions.push({ tag, action, host: false, remotes: [remote] });
2407
3305
  }
2408
3306
  commit();
2409
- return { action, sharedVersion };
2410
- }
2411
- function resolveOverrideEntries(remoteEntry, external, sharedVersion) {
2412
- return ports.remoteInfoRepo.tryGet(sharedVersion.remotes[0].name).map(
2413
- (remote) => Object.fromEntries(
2414
- Object.entries(sharedVersion.remotes[0].entries).map(([packageName, file]) => [
2415
- packageName,
2416
- join(remote.scopeUrl, file)
2417
- ])
2418
- )
2419
- ).orThrow(() => {
3307
+ return { action, provided, sameVersion };
3308
+ }
3309
+ function resolveOverrideEntries(remoteEntry, external, provided) {
3310
+ return Object.fromEntries(
3311
+ Array.from(provided, ([packageName, provider]) => [
3312
+ packageName,
3313
+ join(
3314
+ providerScopeUrl(remoteEntry, external, provider.name),
3315
+ provider.entries[packageName]
3316
+ )
3317
+ ])
3318
+ );
3319
+ }
3320
+ function providerScopeUrl(remoteEntry, external, providerName) {
3321
+ return ports.remoteInfoRepo.tryGet(providerName).map((remote) => remote.scopeUrl).orThrow(() => {
2420
3322
  config.log.error(
2421
3323
  8,
2422
3324
  `[${external.shareScope ?? GLOBAL_SCOPE}][${remoteEntry.name}][${external.packageName}@${external.version}][override] Remote name not found in cache.`
2423
3325
  );
2424
- return new NFError(
2425
- `Could not find override url from remote ${sharedVersion.remotes[0].name}`
2426
- );
3326
+ return new NFError(`Could not find override url from remote ${providerName}`);
2427
3327
  });
2428
3328
  }
2429
3329
  }
2430
3330
 
2431
3331
  // src/lib/core/2.app/steps/pooling/pool-dynamic-externals.ts
2432
- function createPoolDynamicExternals(config) {
3332
+ function createPoolDynamicExternals(config, ports) {
2433
3333
  return ({ entry, actions }) => {
2434
3334
  const { useAutoExternalPooling } = config.feature;
2435
- if (!useAutoExternalPooling && !(entry.shared ?? []).some((e) => e.pool?.trim())) {
2436
- return Promise.resolve({ entry, actions });
2437
- }
2438
- const byScope = /* @__PURE__ */ new Map();
3335
+ const declared = /* @__PURE__ */ new Map();
2439
3336
  for (const external of entry.shared ?? []) {
2440
3337
  const name = external.packageName;
2441
3338
  if (!external.singleton || !actions[name]) continue;
2442
3339
  if (external.shareScope === STRICT_SCOPE) continue;
2443
- const tag = external.pool?.trim();
2444
3340
  const shareScope = external.shareScope ?? GLOBAL_SCOPE;
2445
- const candidates = byScope.get(shareScope) ?? [];
2446
- candidates.push({
2447
- name,
2448
- scope: autoScope(name, useAutoExternalPooling),
2449
- tags: tag ? [{ remote: entry.name, tag }] : [],
2450
- value: name
2451
- });
2452
- byScope.set(shareScope, candidates);
3341
+ let names = declared.get(shareScope);
3342
+ if (!names) declared.set(shareScope, names = /* @__PURE__ */ new Set());
3343
+ names.add(name);
2453
3344
  }
3345
+ if (declared.size === 0) return Promise.resolve({ entry, actions });
2454
3346
  const scope = (name) => {
2455
3347
  actions[name].action = "scope";
2456
3348
  delete actions[name].override;
2457
3349
  };
2458
- for (const candidates of byScope.values()) {
2459
- for (const members of groupByMembership(candidates).values()) {
2460
- const memberActions = members.map((name) => actions[name].action);
2461
- if (memberActions.includes("scope")) {
2462
- members.forEach(scope);
3350
+ for (const [shareScope, names] of declared) {
3351
+ if (!useAutoExternalPooling && !ports.sharedExternalsRepo.hasPoolTag(shareScope)) continue;
3352
+ const committed = ports.sharedExternalsRepo.getFromScope(shareScope);
3353
+ for (const pool of buildPools(committed, useAutoExternalPooling).values()) {
3354
+ const mine = pool.filter((member) => names.has(member.name));
3355
+ if (mine.length === 0) continue;
3356
+ if (mine.some((member) => actions[member.name].action === "scope")) {
3357
+ mine.forEach((member) => scope(member.name));
3358
+ continue;
2463
3359
  }
3360
+ const asked = gateViews(entry.name, pool);
3361
+ const anchor = anchorFor(entry.name, asked);
3362
+ if (anchor === "witnessed") continue;
3363
+ if (anchor === void 0) {
3364
+ const reason = explainDynamic(entry.name, asked);
3365
+ const closest = reason.closest ? `closest is '${reason.closest}'` : "no committed build serves any of it";
3366
+ config.log.warn(
3367
+ 8,
3368
+ `[${shareScope}] '${entry.name}' serves its own family: no committed build offers every entrypoint it imports at a version it accepts \u2014 '${reason.gap}' is the gap, ${closest}. All ${mine.length} members it imports are scoped for it.`
3369
+ );
3370
+ mine.forEach((member) => scope(member.name));
3371
+ continue;
3372
+ }
3373
+ redirect(entry.name, anchor, pool, asked.view, actions, shareScope);
2464
3374
  }
2465
3375
  }
2466
3376
  return Promise.resolve({ entry, actions });
2467
3377
  };
3378
+ function gateViews(remote, pool) {
3379
+ return {
3380
+ pool,
3381
+ view: committedView(pool),
3382
+ wants: consumedMembers(pool).get(remote) ?? [],
3383
+ specifiers: consumedSpecifiers(pool).get(remote) ?? /* @__PURE__ */ new Set(),
3384
+ acceptance: lazy(() => acceptanceTable(pool, ports.versionCheck.isCompatible))
3385
+ };
3386
+ }
3387
+ function anchorFor(remote, { pool, view, wants, specifiers, acceptance }) {
3388
+ const shared = /* @__PURE__ */ new Map();
3389
+ for (const [specifier, source] of view.global) shared.set(specifier, source.tag);
3390
+ if (isWitnessed(specifiers, shared, view.builds)) return "witnessed";
3391
+ const basis = basisPerMember(pool);
3392
+ for (const build of candidateOrder(pool, view)) {
3393
+ if (build === remote) continue;
3394
+ const candidate = view.builds.get(build);
3395
+ if (!servesItsOwnFamily(build, pool, basis)) continue;
3396
+ if (!covers(candidate.coverage, specifiers)) continue;
3397
+ if (!acceptsAll(acceptance(), candidate.instance, remote, wants)) continue;
3398
+ return build;
3399
+ }
3400
+ return void 0;
3401
+ }
3402
+ function candidateOrder(pool, view) {
3403
+ const serving = /* @__PURE__ */ new Set();
3404
+ for (const source of view.global.values()) serving.add(source.remote);
3405
+ const hosts = hostRemotes(pool);
3406
+ const rank = (build) => serving.has(build) ? 0 : hosts.has(build) ? 1 : 2;
3407
+ return [...view.builds.keys()].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
3408
+ }
3409
+ function servesItsOwnFamily(build, pool, basis) {
3410
+ let ships2 = 0;
3411
+ let winsAll = true;
3412
+ let allScoped = true;
3413
+ for (const member of pool) {
3414
+ for (const version of member.external.versions) {
3415
+ const meta = version.remotes.find((r) => r.name === build);
3416
+ if (!meta) continue;
3417
+ if (meta.servedBy !== void 0) return false;
3418
+ ships2++;
3419
+ if (version.action !== "scope") allScoped = false;
3420
+ if (basis.get(member.name) !== build) winsAll = false;
3421
+ }
3422
+ }
3423
+ return ships2 > 0 && (winsAll || allScoped);
3424
+ }
3425
+ function explainDynamic(remote, { view, wants, specifiers, acceptance }) {
3426
+ return explainSelfServe(remote, wants, specifiers, {
3427
+ builds: view.builds,
3428
+ acceptance: acceptance()
3429
+ });
3430
+ }
3431
+ function redirect(remote, anchor, pool, view, actions, shareScope) {
3432
+ const files = view.builds.get(anchor).coverage;
3433
+ const scopeUrl = ports.remoteInfoRepo.tryGet(anchor).get()?.scopeUrl;
3434
+ if (!scopeUrl) {
3435
+ config.log.warn(
3436
+ 8,
3437
+ `[${shareScope}][${remote}] '${anchor}' is not in the cache, so its files cannot be mapped.`
3438
+ );
3439
+ return;
3440
+ }
3441
+ for (const member of pool) {
3442
+ const action = actions[member.name];
3443
+ if (!action || action.action !== "skip") continue;
3444
+ const own = member.external.versions.flatMap((v) => v.remotes).find((r) => r.name === remote);
3445
+ if (!own) continue;
3446
+ const specifiers = Object.keys(own.entries);
3447
+ if (specifiers.length === 0) continue;
3448
+ const override = {};
3449
+ for (const specifier of specifiers) {
3450
+ if (view.global.get(specifier)?.remote === anchor) continue;
3451
+ const file = files.get(specifier);
3452
+ if (file) override[specifier] = join(scopeUrl, file);
3453
+ }
3454
+ action.covered = specifiers;
3455
+ if (Object.keys(override).length > 0) action.override = override;
3456
+ }
3457
+ }
2468
3458
  }
2469
3459
 
2470
3460
  // src/lib/core/2.app/steps/convert-to-import-map.ts
@@ -2502,22 +3492,25 @@ function createConvertToImportMap(config, ports) {
2502
3492
  return;
2503
3493
  }
2504
3494
  if (actions[external.packageName].action === "skip") {
2505
- const override = actions[external.packageName].override;
2506
- if (!external.shareScope) return;
3495
+ const { override, covered, sameVersion } = actions[external.packageName];
2507
3496
  if (override) {
2508
3497
  Object.entries(override).forEach(([packageName, url]) => {
2509
3498
  addToScopes(remoteEntryScope, packageName, url, importMap);
2510
3499
  });
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
- });
3500
+ serveUncovered(
3501
+ remoteEntry,
3502
+ external,
3503
+ covered ?? Object.keys(override),
3504
+ remoteEntryScope,
3505
+ importMap,
3506
+ sameVersion
3507
+ );
3508
+ return;
3509
+ }
3510
+ if (!external.shareScope) {
3511
+ if (covered) {
3512
+ serveUncovered(remoteEntry, external, covered, remoteEntryScope, importMap, sameVersion);
3513
+ }
2521
3514
  return;
2522
3515
  }
2523
3516
  log.error(
@@ -2584,9 +3577,22 @@ function createConvertToImportMap(config, ports) {
2584
3577
  });
2585
3578
  return importMap;
2586
3579
  }
3580
+ function serveUncovered(remoteEntry, external, covered, remoteEntryScope, importMap, sameVersion = false) {
3581
+ const provided = new Set(covered);
3582
+ Object.entries(external.entries).forEach(([packageName, fileName]) => {
3583
+ if (provided.has(packageName)) return;
3584
+ if (!sameVersion && (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints)) {
3585
+ warnUncoveredEntrypoint(remoteEntry.name, external.packageName, packageName);
3586
+ return;
3587
+ }
3588
+ const url = join(remoteEntryScope, fileName);
3589
+ addToScopes(remoteEntryScope, packageName, url, importMap);
3590
+ addIntegrity(importMap, url, remoteEntry.integrity, fileName);
3591
+ });
3592
+ }
2587
3593
  function warnUncoveredEntrypoint(remoteName, externalName, packageName) {
2588
3594
  const msg = `[${remoteName}][${externalName}] Entrypoint '${packageName}' is not covered by the override.`;
2589
- if (config.strict.strictImportMap) {
3595
+ if (config.strict.strictEntryPointCoverage || config.strict.strictImportMap) {
2590
3596
  log.error(9, msg);
2591
3597
  throw new NFError("Could not create ImportMap.");
2592
3598
  }
@@ -2607,6 +3613,7 @@ var createInitDrivers = ({
2607
3613
  }) => ({
2608
3614
  getRemoteEntries: createGetRemoteEntries(config, adapters),
2609
3615
  processRemoteEntries: createProcessRemoteEntries(config, adapters),
3616
+ markPoolsForReelection: createMarkPoolsForReelection(config, adapters),
2610
3617
  determineSharedExternals: createDetermineSharedExternals(config, adapters),
2611
3618
  poolSharedExternals: createPoolSharedExternals(config, adapters),
2612
3619
  generateImportMap: createGenerateImportMap(config, adapters),
@@ -2614,7 +3621,7 @@ var createInitDrivers = ({
2614
3621
  exposeModuleLoader: createExposeModuleLoader(config, adapters),
2615
3622
  getRemoteEntry: createGetRemoteEntry(config, adapters),
2616
3623
  updateCache: createUpdateCache(config, adapters),
2617
- poolDynamicExternals: createPoolDynamicExternals(config),
3624
+ poolDynamicExternals: createPoolDynamicExternals(config, adapters),
2618
3625
  convertToImportMap: createConvertToImportMap(config, adapters)
2619
3626
  });
2620
3627
  var INIT_FLOW_FACTORY = ({
@@ -2725,7 +3732,7 @@ var createManifestProvider = () => {
2725
3732
  };
2726
3733
  };
2727
3734
 
2728
- // node_modules/.pnpm/@softarc+native-federation@4.3.2_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/utils/normalize.js
3735
+ // node_modules/.pnpm/@softarc+native-federation@4.4.0_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/utils/normalize.js
2729
3736
  function inferPackageFromSecondary(secondary) {
2730
3737
  const parts = secondary.split("/");
2731
3738
  if (secondary.startsWith("@") && parts.length >= 2) {
@@ -2734,7 +3741,7 @@ function inferPackageFromSecondary(secondary) {
2734
3741
  return parts[0];
2735
3742
  }
2736
3743
 
2737
- // node_modules/.pnpm/@softarc+native-federation@4.3.2_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/core/output/densify-externals.js
3744
+ // node_modules/.pnpm/@softarc+native-federation@4.4.0_typescript@6.0.3/node_modules/@softarc/native-federation/dist/lib/core/output/densify-externals.js
2738
3745
  function isDense(entry) {
2739
3746
  return "entries" in entry;
2740
3747
  }
@@ -2835,16 +3842,21 @@ var createRemoteInfoRepository = (config) => {
2835
3842
  const STORAGE = config.storage("remotes", {});
2836
3843
  if (config.clearStorage) STORAGE.clear();
2837
3844
  const _cache = STORAGE.get() ?? {};
3845
+ let _dirty = false;
2838
3846
  return {
2839
3847
  contains: function(remoteName) {
2840
3848
  return !!_cache[remoteName];
2841
3849
  },
2842
3850
  remove: function(remoteName) {
2843
- delete _cache[remoteName];
3851
+ if (remoteName in _cache) {
3852
+ delete _cache[remoteName];
3853
+ _dirty = true;
3854
+ }
2844
3855
  return this;
2845
3856
  },
2846
3857
  addOrUpdate: function(remoteName, remote) {
2847
3858
  _cache[remoteName] = remote;
3859
+ _dirty = true;
2848
3860
  return this;
2849
3861
  },
2850
3862
  tryGet: function(remoteName) {
@@ -2859,7 +3871,9 @@ var createRemoteInfoRepository = (config) => {
2859
3871
  return _cache;
2860
3872
  },
2861
3873
  commit: function() {
3874
+ if (!_dirty) return this;
2862
3875
  STORAGE.set(_cache);
3876
+ _dirty = false;
2863
3877
  return this;
2864
3878
  }
2865
3879
  };
@@ -2870,14 +3884,19 @@ var createScopedExternalsRepository = (config) => {
2870
3884
  const STORAGE = config.storage("scoped-externals", {});
2871
3885
  if (config.clearStorage) STORAGE.clear();
2872
3886
  const _cache = STORAGE.get() ?? {};
3887
+ let _dirty = false;
2873
3888
  return {
2874
3889
  addExternal: function(remoteName, external, version) {
2875
3890
  if (!_cache[remoteName]) _cache[remoteName] = {};
2876
3891
  _cache[remoteName][external] = version;
3892
+ _dirty = true;
2877
3893
  return this;
2878
3894
  },
2879
3895
  remove: function(remoteName) {
2880
- delete _cache[remoteName];
3896
+ if (remoteName in _cache) {
3897
+ delete _cache[remoteName];
3898
+ _dirty = true;
3899
+ }
2881
3900
  return this;
2882
3901
  },
2883
3902
  getAll: function() {
@@ -2887,7 +3906,9 @@ var createScopedExternalsRepository = (config) => {
2887
3906
  return Optional.of(_cache[remoteName]);
2888
3907
  },
2889
3908
  commit: function() {
3909
+ if (!_dirty) return this;
2890
3910
  STORAGE.set(_cache);
3911
+ _dirty = false;
2891
3912
  return this;
2892
3913
  }
2893
3914
  };
@@ -2900,14 +3921,19 @@ var createSharedExternalsRepository = (config) => {
2900
3921
  { [GLOBAL_SCOPE]: {} }
2901
3922
  );
2902
3923
  if (config.clearStorage) STORAGE.clear();
2903
- const _cache = STORAGE.get();
2904
- let _sawPoolTag = false;
3924
+ const _cache = STORAGE.get() ?? { [GLOBAL_SCOPE]: {} };
3925
+ let _dirty = false;
2905
3926
  return {
2906
- markPoolTagPresent: function() {
2907
- _sawPoolTag = true;
2908
- },
2909
- hasPoolTag: function() {
2910
- return _sawPoolTag;
3927
+ // Read from the cache rather than remembered from this init's entries: a warm init may not refetch
3928
+ // the tagged remote at all, and pooling has to coordinate its pool anyway. Exits on the first hit.
3929
+ // Per share scope, because a pool never spans one: a tag in another scope is no reason to pool here.
3930
+ hasPoolTag: function(shareScope) {
3931
+ const scope = _cache[shareScope ?? GLOBAL_SCOPE];
3932
+ if (!scope) return false;
3933
+ for (const external of Object.values(scope))
3934
+ for (const version of external.versions)
3935
+ for (const remote of version.remotes) if (remote.pool?.trim()) return true;
3936
+ return false;
2911
3937
  },
2912
3938
  getFromScope: function(shareScope) {
2913
3939
  return { ..._cache[shareScope ?? GLOBAL_SCOPE] };
@@ -2915,28 +3941,41 @@ var createSharedExternalsRepository = (config) => {
2915
3941
  addOrUpdate: function(externalName, external, shareScope) {
2916
3942
  if (!_cache[shareScope ?? GLOBAL_SCOPE]) _cache[shareScope ?? GLOBAL_SCOPE] = {};
2917
3943
  _cache[shareScope ?? GLOBAL_SCOPE][externalName] = external;
3944
+ _dirty = true;
2918
3945
  return this;
2919
3946
  },
2920
3947
  getScopes: function(o = { includeGlobal: true }) {
2921
3948
  if (o.includeGlobal) return Object.keys(_cache);
2922
3949
  return Object.keys(_cache).filter((s) => s !== GLOBAL_SCOPE);
2923
3950
  },
2924
- removeFromAllScopes: function(remoteName) {
3951
+ // Batched: one traversal of the graph per init instead of one per overridden remote.
3952
+ removeFromAllScopes: function(remoteNames) {
3953
+ if (remoteNames.size === 0) return;
2925
3954
  Object.values(_cache).forEach((scope) => {
2926
3955
  const removeExternals = [];
2927
3956
  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);
3957
+ let removedCopy = false;
3958
+ for (let i = external.versions.length - 1; i >= 0; i--) {
3959
+ const version = external.versions[i];
3960
+ const remotes = version.remotes;
3961
+ const host = version.host ? remotes[0]?.name : void 0;
3962
+ let keep = 0;
3963
+ for (let r = 0; r < remotes.length; r++) {
3964
+ if (remoteNames.has(remotes[r].name)) continue;
3965
+ remotes[keep++] = remotes[r];
2933
3966
  }
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);
3967
+ if (keep !== remotes.length) {
3968
+ remotes.length = keep;
3969
+ removedCopy = true;
3970
+ _dirty = true;
3971
+ if (host !== void 0 && remoteNames.has(host)) version.host = false;
3972
+ }
3973
+ if (remotes.length === 0) {
3974
+ external.versions.splice(i, 1);
3975
+ _dirty = true;
2939
3976
  }
3977
+ }
3978
+ if (removedCopy) {
2940
3979
  external.dirty = true;
2941
3980
  if (external.versions.length === 0) removeExternals.push(name);
2942
3981
  }
@@ -2960,7 +3999,9 @@ var createSharedExternalsRepository = (config) => {
2960
3999
  return Optional.of(_cache[shareScope ?? GLOBAL_SCOPE]?.[external]);
2961
4000
  },
2962
4001
  commit: function() {
4002
+ if (!_dirty) return this;
2963
4003
  STORAGE.set(_cache);
4004
+ _dirty = false;
2964
4005
  return this;
2965
4006
  }
2966
4007
  };
@@ -2997,17 +4038,29 @@ var createChunkRepository = (config) => {
2997
4038
  const STORAGE = config.storage("shared-chunks", {});
2998
4039
  if (config.clearStorage) STORAGE.clear();
2999
4040
  const _cache = STORAGE.get() ?? {};
4041
+ let _dirty = false;
3000
4042
  return {
3001
4043
  addOrReplace: function(remoteName, bundleName, chunks) {
3002
4044
  if (!_cache[remoteName]) _cache[remoteName] = {};
3003
4045
  _cache[remoteName][bundleName] = chunks;
4046
+ _dirty = true;
4047
+ return this;
4048
+ },
4049
+ // Whole-remote: a build that stops chunking a bundle omits the key, so `addOrReplace` cannot clear it.
4050
+ remove: function(remoteName) {
4051
+ if (remoteName in _cache) {
4052
+ delete _cache[remoteName];
4053
+ _dirty = true;
4054
+ }
3004
4055
  return this;
3005
4056
  },
3006
4057
  tryGet: function(remoteName, bundleName) {
3007
4058
  return Optional.of(_cache[remoteName]?.[bundleName]);
3008
4059
  },
3009
4060
  commit: function() {
4061
+ if (!_dirty) return this;
3010
4062
  STORAGE.set(_cache);
4063
+ _dirty = false;
3011
4064
  return this;
3012
4065
  }
3013
4066
  };
@@ -3213,10 +4266,9 @@ var globalThisStorageEntry = (namespace) => (key, initialValue) => {
3213
4266
  globalThis[namespace] = {};
3214
4267
  }
3215
4268
  const storage = globalThis[namespace];
3216
- if (!storage[key]) storage[key] = initialValue;
3217
4269
  const entry = {
3218
4270
  get() {
3219
- return cloneEntry(key, storage[key]);
4271
+ return cloneEntry(key, key in storage ? storage[key] : initialValue);
3220
4272
  },
3221
4273
  set(value) {
3222
4274
  storage[key] = cloneEntry(key, value);
@@ -3240,6 +4292,7 @@ var createStorageConfig = (override) => ({
3240
4292
  var defaultProfile = {
3241
4293
  latestSharedExternal: false,
3242
4294
  skipInvalidExternalVersions: false,
4295
+ scopeUncoveredEntrypoints: false,
3243
4296
  overrideCachedRemotes: "init-only",
3244
4297
  overrideCachedRemotesIfURLMatches: false
3245
4298
  };