@softarc/native-federation-orchestrator 4.5.3 → 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 (29) hide show
  1. package/README.md +2 -2
  2. package/fesm2022/@softarc/native-federation-orchestrator.mjs +1087 -250
  3. package/fesm2022/@softarc/native-federation-orchestrator.mjs.map +4 -4
  4. package/fesm2022/module-federation.mjs +34 -20
  5. package/fesm2022/module-federation.mjs.map +3 -3
  6. package/fesm2022/node.mjs +1084 -247
  7. package/fesm2022/node.mjs.map +4 -4
  8. package/package.json +1 -1
  9. package/quickstart.mjs +15 -15
  10. package/types/lib/core/1.domain/externals/basis.d.ts +17 -2
  11. package/types/lib/core/1.domain/externals/version.contract.d.ts +1 -0
  12. package/types/lib/core/1.domain/remote-entry/remote-entry.contract.d.ts +1 -0
  13. package/types/lib/core/2.app/driver-ports/init/drivers.contract.d.ts +2 -0
  14. package/types/lib/core/2.app/driver-ports/init/for-determining-shared-externals.port.d.ts +8 -1
  15. package/types/lib/core/2.app/driver-ports/init/for-marking-pools-for-reelection.port.d.ts +6 -0
  16. package/types/lib/core/2.app/driver-ports/init/for-pooling-shared-externals.port.d.ts +3 -1
  17. package/types/lib/core/2.app/driving-ports/for-shared-chunks-storage.port.d.ts +1 -0
  18. package/types/lib/core/2.app/driving-ports/for-shared-externals-storage.port.d.ts +1 -2
  19. package/types/lib/core/2.app/steps/apply-winner.d.ts +18 -0
  20. package/types/lib/core/2.app/steps/pooling/anchoring.d.ts +69 -0
  21. package/types/lib/core/2.app/steps/pooling/mark-pools-for-reelection.d.ts +5 -0
  22. package/types/lib/core/2.app/steps/pooling/pool-dynamic-externals.d.ts +3 -1
  23. package/types/lib/core/2.app/steps/pooling/pool-graph.d.ts +10 -5
  24. package/types/lib/core/2.app/steps/pooling/pool-shared-externals.d.ts +30 -1
  25. package/types/lib/core/2.app/steps/pooling/pool-views.d.ts +63 -0
  26. package/types/lib/core/2.app/steps/pooling/pool.types.d.ts +30 -1
  27. package/types/lib/core/2.app/steps/pooling/pool.util.d.ts +15 -0
  28. package/types/lib/core/2.app/steps/store-remote-entry.d.ts +1 -1
  29. 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
@@ -1463,13 +1463,33 @@ function findVersionForTag(versions, tag) {
1463
1463
  }
1464
1464
  return scoped;
1465
1465
  }
1466
- function uncoveredEntrypoints(remote, basis) {
1467
- return Object.keys(remote.entries).filter((entrypoint) => !(entrypoint in basis));
1466
+ function versionEntries(version) {
1467
+ return collectEntries(version, void 0);
1468
1468
  }
1469
- function countUncoveredEntrypoints(remote, basis) {
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) {
1470
1490
  let uncovered = 0;
1471
1491
  for (const entrypoint in remote.entries) {
1472
- if (!(entrypoint in basis)) uncovered++;
1492
+ if (!covered.has(entrypoint)) uncovered++;
1473
1493
  }
1474
1494
  return uncovered;
1475
1495
  }
@@ -1481,6 +1501,7 @@ function createRemoveCachedRemoteEntries(ports) {
1481
1501
  for (const remoteName of remoteNames) {
1482
1502
  ports.remoteInfoRepo.remove(remoteName);
1483
1503
  ports.scopedExternalsRepo.remove(remoteName);
1504
+ ports.sharedChunksRepo.remove(remoteName);
1484
1505
  }
1485
1506
  ports.sharedExternalsRepo.removeFromAllScopes(remoteNames);
1486
1507
  };
@@ -1506,7 +1527,6 @@ function createStoreRemoteEntry(config, ports, logStep) {
1506
1527
  const tag = resolveVersion(remoteEntry, external);
1507
1528
  if (tag === null) return;
1508
1529
  if (external.singleton) {
1509
- if (external.pool?.trim()) ports.sharedExternalsRepo.markPoolTagPresent();
1510
1530
  onSharedExternal(remoteEntry, external, sharedExternalContext(remoteEntry, external, tag));
1511
1531
  } else {
1512
1532
  addScopedExternal(remoteEntry, external, tag);
@@ -1648,8 +1668,129 @@ function createProcessRemoteEntries(config, ports) {
1648
1668
  var GLOBAL_SCOPE = "__GLOBAL__";
1649
1669
  var STRICT_SCOPE = "strict";
1650
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
+
1651
1791
  // src/lib/core/2.app/steps/determine-shared-externals.ts
1652
1792
  function createDetermineSharedExternals(config, ports) {
1793
+ const applyWinner = createApplyWinner(config);
1653
1794
  return () => {
1654
1795
  const memo = /* @__PURE__ */ new Map();
1655
1796
  const isCompatible = (tag, requiredVersion) => {
@@ -1661,16 +1802,20 @@ function createDetermineSharedExternals(config, ports) {
1661
1802
  }
1662
1803
  return hit;
1663
1804
  };
1805
+ const touched = /* @__PURE__ */ new Map();
1664
1806
  for (const shareScope of ports.sharedExternalsRepo.getScopes()) {
1665
1807
  const sharedExternals = ports.sharedExternalsRepo.getFromScope(shareScope);
1666
1808
  try {
1667
- Object.entries(sharedExternals).filter(([_, e]) => e.dirty).forEach(
1668
- ([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(
1669
1812
  name,
1670
1813
  setVersionActions(name, external, isCompatible),
1671
1814
  shareScope
1672
- )
1673
- );
1815
+ );
1816
+ elected.add(name);
1817
+ });
1818
+ if (elected.size > 0) touched.set(shareScope, elected);
1674
1819
  } catch (error) {
1675
1820
  config.log.error(
1676
1821
  3,
@@ -1688,28 +1833,25 @@ function createDetermineSharedExternals(config, ports) {
1688
1833
  );
1689
1834
  }
1690
1835
  }
1691
- return Promise.resolve();
1836
+ return Promise.resolve(touched);
1692
1837
  };
1693
1838
  function uncoveredTears(external, winner, accepts) {
1694
- const basis = winner.remotes[0].entries;
1839
+ const basis = versionEntries(winner);
1695
1840
  return external.versions.reduce((sum, v) => {
1696
1841
  if (v === winner) return sum;
1697
1842
  if (!accepts(v, winner.tag)) return sum;
1698
- return sum + v.remotes.reduce((n, r) => n + countUncoveredEntrypoints(r, basis), 0);
1843
+ return v.remotes.reduce(
1844
+ (n, r) => r.servedBy ? n : n + countUncoveredEntrypoints(r, basis),
1845
+ sum
1846
+ );
1699
1847
  }, 0);
1700
1848
  }
1701
1849
  function setVersionActions(externalName, external, isCompatible) {
1702
1850
  if (external.versions.length === 1) {
1703
- external.versions[0].action = "share";
1704
- applyEntrypointCoveragePolicy(externalName, external);
1705
- external.dirty = false;
1706
- return external;
1851
+ return applyWinner(externalName, external, external.versions[0], isCompatible);
1707
1852
  }
1708
- const demands = new Map(
1709
- external.versions.map((v) => [v, versionDemands(v)])
1710
- );
1711
- const accepts = (version, tag) => demands.get(version).every((d) => isCompatible(tag, d.requiredVersion));
1712
- const objector = (version, tag) => demands.get(version).find((d) => d.strictVersion && !isCompatible(tag, d.requiredVersion));
1853
+ const acceptance = versionAcceptance(external, isCompatible);
1854
+ const { accepts } = acceptance;
1713
1855
  let sharedVersion = external.versions.find((v) => v.host);
1714
1856
  if (!sharedVersion && config.profile.latestSharedExternal) {
1715
1857
  sharedVersion = external.versions[0];
@@ -1717,10 +1859,30 @@ function createDetermineSharedExternals(config, ports) {
1717
1859
  if (!sharedVersion) {
1718
1860
  let leastExtraDownloads = Number.MAX_VALUE;
1719
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);
1720
1879
  external.versions.forEach((vA) => {
1721
- const extraDownloads = external.versions.filter(
1722
- (vB) => demands.get(vB).some((d) => !d.cached && d.strictVersion && !isCompatible(vA.tag, d.requiredVersion))
1723
- ).length;
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
+ );
1724
1886
  if (extraDownloads < leastExtraDownloads) {
1725
1887
  leastExtraDownloads = extraDownloads;
1726
1888
  leastTears = uncoveredTears(external, vA, accepts);
@@ -1738,84 +1900,304 @@ function createDetermineSharedExternals(config, ports) {
1738
1900
  if (!sharedVersion) {
1739
1901
  throw new NFError(`[${externalName}] Could not determine shared version!`);
1740
1902
  }
1741
- external.versions.forEach((v) => {
1742
- if (accepts(v, sharedVersion.tag)) {
1743
- v.action = "skip";
1744
- 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
+ }
1745
1942
  }
1746
- const strict = objector(v, sharedVersion.tag);
1747
- if (config.strict.strictExternalCompatibility && strict) {
1748
- config.log.error(
1749
- 3,
1750
- `[${strict.name}][${externalName}@${v.tag}] Is not compatible with requiredRange '${strict.requiredVersion}' of shared ${externalName}@${sharedVersion.tag}.`
1751
- );
1752
- 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 });
1753
1957
  }
1754
- v.action = strict ? "scope" : "skip";
1755
- });
1756
- sharedVersion.action = "share";
1757
- applyEntrypointCoveragePolicy(externalName, external);
1758
- external.dirty = false;
1759
- return external;
1958
+ }
1760
1959
  }
1761
- function applyEntrypointCoveragePolicy(externalName, external) {
1762
- const { strictEntryPointCoverage } = config.strict;
1763
- if (!strictEntryPointCoverage && !config.profile.scopeUncoveredEntrypoints) return;
1764
- const tears = findTears(external);
1765
- if (tears.length === 0) return;
1766
- if (strictEntryPointCoverage) {
1767
- const { version, remote, uncovered } = tears[0];
1768
- config.log.error(
1769
- 3,
1770
- `[${externalName}@${version.tag}][${remote.name}] Entrypoints not covered by the shared version: ${uncovered.join(", ")}.`
1771
- );
1772
- throw new NFError(
1773
- `External ${externalName} could not be shared without tearing entrypoints.`
1774
- );
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
+ }
1775
1974
  }
1776
- scopeTornRemotes(externalName, external, tears);
1777
1975
  }
1778
- function findTears(external) {
1779
- const shared = external.versions.find((v) => v.action === "share");
1780
- if (!shared) return [];
1781
- const basis = shared.remotes[0].entries;
1782
- const tears = [];
1783
- for (const version of external.versions) {
1784
- if (version.action === "scope") continue;
1785
- version.remotes.forEach((remote, index) => {
1786
- if (version === shared && index === 0) return;
1787
- const uncovered = uncoveredEntrypoints(remote, basis);
1788
- if (uncovered.length > 0) tears.push({ version, remote, uncovered });
1789
- });
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
+ }
1790
1990
  }
1791
- return tears;
1792
1991
  }
1793
- function scopeTornRemotes(externalName, external, tears) {
1794
- const torn = new Set(tears.map((t) => t.remote));
1795
- const demotedByTag = /* @__PURE__ */ new Map();
1796
- for (const { version, remote, uncovered } of tears) {
1797
- const group = demotedByTag.get(version.tag);
1798
- if (group) group.push(remote);
1799
- else demotedByTag.set(version.tag, [remote]);
1800
- config.log.debug(
1801
- 3,
1802
- `[${externalName}@${version.tag}][${remote.name}] Scoped: entrypoints not covered by the shared version: ${uncovered.join(", ")}.`
1803
- );
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
+ }
1804
2004
  }
1805
- for (const version of external.versions) {
1806
- if (version.remotes.some((r) => torn.has(r))) {
1807
- version.remotes = version.remotes.filter((r) => !torn.has(r));
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);
1808
2045
  }
1809
2046
  }
1810
- external.versions = external.versions.filter((v) => v.remotes.length > 0);
1811
- for (const [tag, remotes] of demotedByTag) {
1812
- const scoped = external.versions.find((v) => v.tag === tag && v.action === "scope");
1813
- if (scoped) scoped.remotes.push(...remotes);
1814
- else external.versions.push({ tag, host: false, action: "scope", remotes });
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]);
1815
2066
  }
1816
2067
  }
1817
2068
  }
1818
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]);
2084
+ }
2085
+ byMember.set(member.name, accepted);
2086
+ }
2087
+ }
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;
2199
+ }
2200
+
1819
2201
  // src/lib/core/2.app/steps/pooling/pool-graph.ts
1820
2202
  var SCOPED_PACKAGE = /^@([^/]+)\//;
1821
2203
  function createDSU() {
@@ -1856,34 +2238,56 @@ function createDSU() {
1856
2238
  }
1857
2239
  var extNode = (name) => `ext\0${name}`;
1858
2240
  var tagNode = (remote, tag) => `tag\0${remote}\0${tag}`;
1859
- var scopeNode = (scope) => `scope\0${scope}`;
2241
+ var scopeNode = (remote, scope) => `scope\0${remote}\0${scope}`;
1860
2242
  function autoScope(name, useAutoExternalPooling) {
1861
2243
  return useAutoExternalPooling ? SCOPED_PACKAGE.exec(name)?.[1] : void 0;
1862
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
+ }
1863
2254
  function groupByMembership(candidates, log) {
1864
2255
  const dsu = createDSU();
1865
2256
  const tagged = /* @__PURE__ */ new Set();
1866
- 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
+ }
1867
2263
  for (const candidate of candidates) {
1868
- let joined = false;
1869
- if (candidate.scope !== void 0) {
1870
- dsu.union(extNode(candidate.name), scopeNode(candidate.scope));
1871
- joined = true;
1872
- }
1873
2264
  for (const edge of candidate.tags) {
1874
2265
  dsu.union(extNode(candidate.name), tagNode(edge.remote, edge.tag));
1875
2266
  tagged.add(candidate.name);
1876
- 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);
1877
2274
  }
1878
- 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));
1879
2281
  }
1880
2282
  const byComponent = /* @__PURE__ */ new Map();
1881
- for (const entry of poolable) {
1882
- const root = dsu.component(extNode(entry.name));
1883
- (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 });
1884
2287
  }
1885
2288
  const pools = [];
1886
2289
  for (const members of byComponent.values()) {
2290
+ if (!members.some((m) => joined.has(m.name))) continue;
1887
2291
  members.sort((a, b) => a.name.localeCompare(b.name));
1888
2292
  if (members.length < 2) {
1889
2293
  const only = members[0];
@@ -1911,6 +2315,7 @@ function buildPools(sharedExternals, useAutoExternalPooling, log) {
1911
2315
  return tag ? [{ remote: r.name, tag }] : [];
1912
2316
  })
1913
2317
  ),
2318
+ remotes: external.versions.flatMap((v) => v.remotes.map((r) => r.name)),
1914
2319
  value: { name, external }
1915
2320
  })
1916
2321
  );
@@ -1918,28 +2323,131 @@ function buildPools(sharedExternals, useAutoExternalPooling, log) {
1918
2323
  }
1919
2324
 
1920
2325
  // src/lib/core/2.app/steps/pooling/pool.util.ts
2326
+ function lazy(make) {
2327
+ let value;
2328
+ return () => value ??= make();
2329
+ }
1921
2330
  function remotesInPool(members) {
1922
2331
  return [
1923
2332
  ...new Set(members.flatMap((m) => m.external.versions.flatMap((v) => v.remotes.map((r) => r.name))))
1924
2333
  ];
1925
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
+ }
1926
2344
 
1927
2345
  // src/lib/core/2.app/steps/pooling/pool-shared-externals.ts
1928
2346
  function islandedRemotes(members) {
1929
- const islanded = /* @__PURE__ */ new Set();
2347
+ const islanded = /* @__PURE__ */ new Map();
1930
2348
  for (const member of members)
1931
2349
  for (const version of member.external.versions)
1932
- 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
+ }
1933
2359
  return islanded;
1934
2360
  }
1935
- function createPoolSharedExternals(config, ports) {
1936
- return () => {
1937
- const { useAutoExternalPooling } = config.feature;
1938
- if (!useAutoExternalPooling && !ports.sharedExternalsRepo.hasPoolTag()) {
1939
- 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
+ }
1940
2397
  }
1941
- for (const scope of ports.sharedExternalsRepo.getScopes()) {
1942
- if (ports.sharedExternalsRepo.scopeType(scope) === "strict") continue;
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
+ }
2406
+ }
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;
1943
2451
  const sharedExternals = ports.sharedExternalsRepo.getFromScope(scope);
1944
2452
  try {
1945
2453
  for (const [poolName, members] of buildPools(
@@ -1947,6 +2455,7 @@ function createPoolSharedExternals(config, ports) {
1947
2455
  useAutoExternalPooling,
1948
2456
  config.log
1949
2457
  )) {
2458
+ if (touchedInScope && !members.some((m) => touchedInScope.has(m.name))) continue;
1950
2459
  poolFamily(poolName, members, scope);
1951
2460
  }
1952
2461
  } catch (error) {
@@ -1967,45 +2476,138 @@ function createPoolSharedExternals(config, ports) {
1967
2476
  const allRemotes = remotesInPool(members);
1968
2477
  if (allRemotes.length < 2) return;
1969
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
+ };
1970
2486
  config.log.debug(
1971
2487
  3,
1972
- `[${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"}}
1973
2489
  ` + members.map((m) => ` - ${m.name}`).join("\n")
1974
2490
  );
1975
- 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) {
1976
2507
  config.log.error(
1977
2508
  3,
1978
- `[${scope}][pool:${poolName}] version-incompatible remotes cannot be pooled: {${[...islanded].join(", ")}}.`
2509
+ `[${scope}][pool:${poolName}] version-incompatible remotes cannot be pooled: {${incompatible.join(", ")}}.`
1979
2510
  );
1980
2511
  throw new NFError(`Could not pool '${poolName}' in scope ${scope}.`);
1981
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
+ }
1982
2517
  for (const member of members) {
1983
- const rebuilt = rebuildMember(member, islanded);
1984
- warnIfScopedOnly(poolName, member.name, rebuilt, scope);
2518
+ const rebuilt = rebuildMember(member, islanded, served, basis.get(member.name));
2519
+ warnIfScopedOnly(poolName, member, rebuilt, islanded, scope);
1985
2520
  ports.sharedExternalsRepo.addOrUpdate(member.name, rebuilt, scope);
1986
2521
  }
1987
2522
  }
1988
- 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) {
1989
2573
  if (rebuilt.versions.some((v) => v.action === "share")) return;
1990
- 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;
1991
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;
1992
2580
  config.log.warn(
1993
2581
  3,
1994
- `[${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.`
1995
2583
  );
1996
2584
  }
1997
- function rebuildMember(member, islanded) {
1998
- const entries = member.external.versions.flatMap(
1999
- (v) => v.remotes.map((meta) => ({ remote: meta.name, tag: v.tag, host: v.host, action: v.action, meta }))
2000
- );
2001
- const isScoped = (e) => islanded.has(e.remote) || e.action === "scope";
2002
- let scoped = entries.filter(isScoped);
2003
- let clean = entries.filter((e) => !isScoped(e));
2004
- if (!clean.some((e) => e.action === "share")) {
2005
- scoped = [...scoped, ...clean];
2006
- clean = [];
2007
- }
2008
- 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));
2009
2611
  const shareVersion = shareEntries.length > 0 ? [
2010
2612
  {
2011
2613
  tag: shareEntries[0].tag,
@@ -2014,37 +2616,70 @@ function createPoolSharedExternals(config, ports) {
2014
2616
  remotes: shareEntries.map((e) => e.meta)
2015
2617
  }
2016
2618
  ] : [];
2017
- const skipByTag = /* @__PURE__ */ new Map();
2018
- for (const e of clean) {
2019
- if (e.action !== "skip") continue;
2020
- const version = skipByTag.get(e.tag) ?? {
2021
- tag: e.tag,
2022
- host: e.host,
2023
- action: "skip",
2024
- remotes: []
2025
- };
2026
- version.remotes.push(e.meta);
2027
- skipByTag.set(e.tag, version);
2028
- }
2029
- const scopeByTag = /* @__PURE__ */ new Map();
2030
- for (const e of scoped) {
2031
- const version = scopeByTag.get(e.tag) ?? {
2032
- tag: e.tag,
2033
- host: false,
2034
- action: "scope",
2035
- remotes: []
2036
- };
2037
- version.remotes.push(e.meta);
2038
- scopeByTag.set(e.tag, version);
2039
- }
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
+ };
2040
2633
  return {
2041
2634
  dirty: false,
2042
- 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))
2043
2643
  };
2044
2644
  }
2045
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
+ }
2658
+
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
+ }
2046
2681
 
2047
- // 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
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
2048
2683
  var BuildNotificationType = /* @__PURE__ */ ((BuildNotificationType2) => {
2049
2684
  BuildNotificationType2["COMPLETED"] = "federation-rebuild-complete";
2050
2685
  BuildNotificationType2["ERROR"] = "federation-rebuild-error";
@@ -2052,7 +2687,7 @@ var BuildNotificationType = /* @__PURE__ */ ((BuildNotificationType2) => {
2052
2687
  return BuildNotificationType2;
2053
2688
  })(BuildNotificationType || {});
2054
2689
 
2055
- // 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
2056
2691
  var CHUNK_PREFIX = "@nf-internal";
2057
2692
  function toChunkImport(fileName) {
2058
2693
  if (fileName.startsWith("./")) {
@@ -2113,9 +2748,9 @@ function createGenerateImportMap(config, ports) {
2113
2748
  }
2114
2749
  function processshareScope(importMap, shareScope, chunkBundles) {
2115
2750
  const sharedExternals = ports.sharedExternalsRepo.getFromScope(shareScope);
2751
+ const index = buildServedIndex(sharedExternals);
2116
2752
  for (const [externalName, external] of Object.entries(sharedExternals)) {
2117
2753
  let override = void 0;
2118
- let overrideScope = void 0;
2119
2754
  const cachedBefore = cachedFingerprint(external);
2120
2755
  for (const version of external.versions) {
2121
2756
  if (version.action === "scope") {
@@ -2132,36 +2767,52 @@ function createGenerateImportMap(config, ports) {
2132
2767
  continue;
2133
2768
  }
2134
2769
  version.remotes[0].cached = true;
2135
- let source = version.remotes[0];
2136
- let sourceScope = getScope2(shareScope, source.name, externalName);
2137
- if (version.action === "share") {
2138
- registerBundleChunks(chunkBundles, source.name, source.bundle);
2139
- } else if (version.action === "skip") {
2770
+ let serving = version;
2771
+ if (version.action === "skip") {
2140
2772
  if (!override) {
2141
2773
  override = findOverride(external, shareScope, externalName) ?? "NOT_AVAILABLE";
2142
2774
  }
2143
2775
  if (override !== "NOT_AVAILABLE") {
2144
- if (!overrideScope)
2145
- overrideScope = getScope2(shareScope, override.remotes[0].name, externalName);
2146
- source = override.remotes[0];
2147
- sourceScope = overrideScope;
2148
- override.remotes[0].cached = true;
2776
+ serving = override;
2149
2777
  version.remotes[0].cached = false;
2150
2778
  }
2151
2779
  }
2152
- const mappings = Object.entries(source.entries).map(([packageName, file]) => ({
2153
- packageName,
2154
- url: join(sourceScope, file),
2155
- name: source.name,
2156
- file
2157
- }));
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
+ }
2158
2795
  version.remotes.forEach((r) => {
2159
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
+ }
2160
2811
  for (const m of mappings) {
2161
2812
  addToScope(importMap, rScope, { [m.packageName]: m.url });
2162
2813
  }
2163
2814
  for (const [packageName, file] of Object.entries(r.entries)) {
2164
- if (packageName in source.entries) continue;
2815
+ if (provided.has(packageName)) continue;
2165
2816
  if (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints) {
2166
2817
  warnUncoveredEntrypoint(shareScope, externalName, r.name, packageName);
2167
2818
  continue;
@@ -2212,11 +2863,68 @@ function createGenerateImportMap(config, ports) {
2212
2863
  }
2213
2864
  config.log.warn(4, `ShareScope external ${scopedExternalName} has multiple shared versions.`);
2214
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
+ }
2215
2917
  function addGlobalSharedExternals(importMap, chunkBundles) {
2216
2918
  const sharedExternals = ports.sharedExternalsRepo.getFromScope();
2919
+ const index = buildServedIndex(sharedExternals);
2920
+ const served = [];
2921
+ const cachedBefore = /* @__PURE__ */ new Map();
2217
2922
  for (const [externalName, external] of Object.entries(sharedExternals)) {
2218
- const cachedBefore = cachedFingerprint(external);
2923
+ cachedBefore.set(externalName, cachedFingerprint(external));
2219
2924
  for (const version of external.versions) {
2925
+ if (index && version.action !== "scope") {
2926
+ collectServed(served, index, GLOBAL_SCOPE, externalName, version);
2927
+ }
2220
2928
  if (version.action === "skip") continue;
2221
2929
  if (version.action === "scope") {
2222
2930
  for (const remote of version.remotes) {
@@ -2235,29 +2943,46 @@ function createGenerateImportMap(config, ports) {
2235
2943
  notifyDuplicateGlobalExternal(externalName);
2236
2944
  continue;
2237
2945
  }
2238
- const scope = getScope2(GLOBAL_SCOPE, version.remotes[0].name, externalName);
2239
- for (const [packageName, file] of Object.entries(version.remotes[0].entries)) {
2240
- const url = join(scope, file);
2241
- addToGlobal(importMap, { [packageName]: url });
2242
- addIntegrity(importMap, url, version.remotes[0].name, file);
2243
- }
2244
- registerBundleChunks(chunkBundles, version.remotes[0].name, version.remotes[0].bundle);
2245
- version.remotes[0].cached = true;
2246
- selfFillUncovered(importMap, chunkBundles, externalName, version.remotes, 1);
2946
+ mergeVersionEntries(importMap, chunkBundles, externalName, version);
2247
2947
  }
2248
2948
  for (const version of external.versions) {
2249
2949
  if (version.action !== "skip") continue;
2250
2950
  selfFillUncovered(importMap, chunkBundles, externalName, version.remotes);
2251
2951
  }
2252
- if (cachedFingerprint(external) !== cachedBefore) {
2952
+ }
2953
+ flushServed(importMap, chunkBundles, served);
2954
+ for (const [externalName, external] of Object.entries(sharedExternals)) {
2955
+ if (cachedFingerprint(external) !== cachedBefore.get(externalName)) {
2253
2956
  ports.sharedExternalsRepo.addOrUpdate(externalName, external);
2254
2957
  }
2255
2958
  }
2256
2959
  return importMap;
2257
2960
  }
2258
- function selfFillUncovered(importMap, chunkBundles, externalName, remotes, from = 0) {
2259
- for (let i = from; i < remotes.length; i++) {
2260
- const remote = remotes[i];
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;
2261
2986
  for (const [packageName, file] of Object.entries(remote.entries)) {
2262
2987
  if (importMap.imports[packageName]) continue;
2263
2988
  if (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints) {
@@ -2509,15 +3234,16 @@ function createUpdateCache(config, ports) {
2509
3234
  const actions = {};
2510
3235
  if (remoteEntry?.override) removeCachedRemoteEntries(/* @__PURE__ */ new Set([remoteEntry.name]));
2511
3236
  storeRemoteEntry(remoteEntry, (entry, external, ctx) => {
2512
- const { action, sharedVersion } = resolveSharedExternal(entry, external, ctx);
3237
+ const { action, provided, sameVersion } = resolveSharedExternal(entry, external, ctx);
2513
3238
  actions[external.packageName] = { action };
2514
- if (action === "skip" && sharedVersion?.remotes[0]?.entries) {
2515
- actions[external.packageName].covered = Object.keys(sharedVersion.remotes[0].entries);
3239
+ if (action === "skip" && provided) {
3240
+ actions[external.packageName].covered = Array.from(provided.keys());
3241
+ if (sameVersion) actions[external.packageName].sameVersion = true;
2516
3242
  if (external.shareScope) {
2517
3243
  actions[external.packageName].override = resolveOverrideEntries(
2518
3244
  entry,
2519
3245
  external,
2520
- sharedVersion
3246
+ provided
2521
3247
  );
2522
3248
  }
2523
3249
  }
@@ -2547,8 +3273,10 @@ function createUpdateCache(config, ports) {
2547
3273
  }
2548
3274
  config.log.warn(8, errorMsg);
2549
3275
  }
2550
- if (action === "skip" && sharedVersion) {
2551
- const uncovered = uncoveredEntrypoints(remote, sharedVersion.remotes[0].entries);
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);
2552
3280
  if (uncovered.length > 0) {
2553
3281
  const msg = `[${sharedInfo.shareScope ?? GLOBAL_SCOPE}][${remoteEntry.name}][${sharedInfo.packageName}] Entrypoints not covered by the shared version: ${uncovered.join(", ")}.`;
2554
3282
  if (config.strict.strictEntryPointCoverage) {
@@ -2576,65 +3304,157 @@ function createUpdateCache(config, ports) {
2576
3304
  cached.versions.push({ tag, action, host: false, remotes: [remote] });
2577
3305
  }
2578
3306
  commit();
2579
- return { action, sharedVersion };
2580
- }
2581
- function resolveOverrideEntries(remoteEntry, external, sharedVersion) {
2582
- return ports.remoteInfoRepo.tryGet(sharedVersion.remotes[0].name).map(
2583
- (remote) => Object.fromEntries(
2584
- Object.entries(sharedVersion.remotes[0].entries).map(([packageName, file]) => [
2585
- packageName,
2586
- join(remote.scopeUrl, file)
2587
- ])
2588
- )
2589
- ).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(() => {
2590
3322
  config.log.error(
2591
3323
  8,
2592
3324
  `[${external.shareScope ?? GLOBAL_SCOPE}][${remoteEntry.name}][${external.packageName}@${external.version}][override] Remote name not found in cache.`
2593
3325
  );
2594
- return new NFError(
2595
- `Could not find override url from remote ${sharedVersion.remotes[0].name}`
2596
- );
3326
+ return new NFError(`Could not find override url from remote ${providerName}`);
2597
3327
  });
2598
3328
  }
2599
3329
  }
2600
3330
 
2601
3331
  // src/lib/core/2.app/steps/pooling/pool-dynamic-externals.ts
2602
- function createPoolDynamicExternals(config) {
3332
+ function createPoolDynamicExternals(config, ports) {
2603
3333
  return ({ entry, actions }) => {
2604
3334
  const { useAutoExternalPooling } = config.feature;
2605
- if (!useAutoExternalPooling && !(entry.shared ?? []).some((e) => e.pool?.trim())) {
2606
- return Promise.resolve({ entry, actions });
2607
- }
2608
- const byScope = /* @__PURE__ */ new Map();
3335
+ const declared = /* @__PURE__ */ new Map();
2609
3336
  for (const external of entry.shared ?? []) {
2610
3337
  const name = external.packageName;
2611
3338
  if (!external.singleton || !actions[name]) continue;
2612
3339
  if (external.shareScope === STRICT_SCOPE) continue;
2613
- const tag = external.pool?.trim();
2614
3340
  const shareScope = external.shareScope ?? GLOBAL_SCOPE;
2615
- const candidates = byScope.get(shareScope) ?? [];
2616
- candidates.push({
2617
- name,
2618
- scope: autoScope(name, useAutoExternalPooling),
2619
- tags: tag ? [{ remote: entry.name, tag }] : [],
2620
- value: name
2621
- });
2622
- byScope.set(shareScope, candidates);
3341
+ let names = declared.get(shareScope);
3342
+ if (!names) declared.set(shareScope, names = /* @__PURE__ */ new Set());
3343
+ names.add(name);
2623
3344
  }
3345
+ if (declared.size === 0) return Promise.resolve({ entry, actions });
2624
3346
  const scope = (name) => {
2625
3347
  actions[name].action = "scope";
2626
3348
  delete actions[name].override;
2627
3349
  };
2628
- for (const candidates of byScope.values()) {
2629
- for (const members of groupByMembership(candidates).values()) {
2630
- const memberActions = members.map((name) => actions[name].action);
2631
- if (memberActions.includes("scope")) {
2632
- 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;
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;
2633
3372
  }
3373
+ redirect(entry.name, anchor, pool, asked.view, actions, shareScope);
2634
3374
  }
2635
3375
  }
2636
3376
  return Promise.resolve({ entry, actions });
2637
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
+ }
2638
3458
  }
2639
3459
 
2640
3460
  // src/lib/core/2.app/steps/convert-to-import-map.ts
@@ -2672,13 +3492,7 @@ function createConvertToImportMap(config, ports) {
2672
3492
  return;
2673
3493
  }
2674
3494
  if (actions[external.packageName].action === "skip") {
2675
- const { override, covered } = actions[external.packageName];
2676
- if (!external.shareScope) {
2677
- if (covered) {
2678
- serveUncovered(remoteEntry, external, covered, remoteEntryScope, importMap);
2679
- }
2680
- return;
2681
- }
3495
+ const { override, covered, sameVersion } = actions[external.packageName];
2682
3496
  if (override) {
2683
3497
  Object.entries(override).forEach(([packageName, url]) => {
2684
3498
  addToScopes(remoteEntryScope, packageName, url, importMap);
@@ -2688,10 +3502,17 @@ function createConvertToImportMap(config, ports) {
2688
3502
  external,
2689
3503
  covered ?? Object.keys(override),
2690
3504
  remoteEntryScope,
2691
- importMap
3505
+ importMap,
3506
+ sameVersion
2692
3507
  );
2693
3508
  return;
2694
3509
  }
3510
+ if (!external.shareScope) {
3511
+ if (covered) {
3512
+ serveUncovered(remoteEntry, external, covered, remoteEntryScope, importMap, sameVersion);
3513
+ }
3514
+ return;
3515
+ }
2695
3516
  log.error(
2696
3517
  9,
2697
3518
  `[${remoteEntry.name}][${external.packageName}] shareScope skip has no override.`
@@ -2756,11 +3577,11 @@ function createConvertToImportMap(config, ports) {
2756
3577
  });
2757
3578
  return importMap;
2758
3579
  }
2759
- function serveUncovered(remoteEntry, external, covered, remoteEntryScope, importMap) {
3580
+ function serveUncovered(remoteEntry, external, covered, remoteEntryScope, importMap, sameVersion = false) {
2760
3581
  const provided = new Set(covered);
2761
3582
  Object.entries(external.entries).forEach(([packageName, fileName]) => {
2762
3583
  if (provided.has(packageName)) return;
2763
- if (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints) {
3584
+ if (!sameVersion && (config.strict.strictEntryPointCoverage || config.profile.scopeUncoveredEntrypoints)) {
2764
3585
  warnUncoveredEntrypoint(remoteEntry.name, external.packageName, packageName);
2765
3586
  return;
2766
3587
  }
@@ -2792,6 +3613,7 @@ var createInitDrivers = ({
2792
3613
  }) => ({
2793
3614
  getRemoteEntries: createGetRemoteEntries(config, adapters),
2794
3615
  processRemoteEntries: createProcessRemoteEntries(config, adapters),
3616
+ markPoolsForReelection: createMarkPoolsForReelection(config, adapters),
2795
3617
  determineSharedExternals: createDetermineSharedExternals(config, adapters),
2796
3618
  poolSharedExternals: createPoolSharedExternals(config, adapters),
2797
3619
  generateImportMap: createGenerateImportMap(config, adapters),
@@ -2799,7 +3621,7 @@ var createInitDrivers = ({
2799
3621
  exposeModuleLoader: createExposeModuleLoader(config, adapters),
2800
3622
  getRemoteEntry: createGetRemoteEntry(config, adapters),
2801
3623
  updateCache: createUpdateCache(config, adapters),
2802
- poolDynamicExternals: createPoolDynamicExternals(config),
3624
+ poolDynamicExternals: createPoolDynamicExternals(config, adapters),
2803
3625
  convertToImportMap: createConvertToImportMap(config, adapters)
2804
3626
  });
2805
3627
  var INIT_FLOW_FACTORY = ({
@@ -2910,7 +3732,7 @@ var createManifestProvider = () => {
2910
3732
  };
2911
3733
  };
2912
3734
 
2913
- // 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
2914
3736
  function inferPackageFromSecondary(secondary) {
2915
3737
  const parts = secondary.split("/");
2916
3738
  if (secondary.startsWith("@") && parts.length >= 2) {
@@ -2919,7 +3741,7 @@ function inferPackageFromSecondary(secondary) {
2919
3741
  return parts[0];
2920
3742
  }
2921
3743
 
2922
- // 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
2923
3745
  function isDense(entry) {
2924
3746
  return "entries" in entry;
2925
3747
  }
@@ -3100,14 +3922,18 @@ var createSharedExternalsRepository = (config) => {
3100
3922
  );
3101
3923
  if (config.clearStorage) STORAGE.clear();
3102
3924
  const _cache = STORAGE.get() ?? { [GLOBAL_SCOPE]: {} };
3103
- let _sawPoolTag = false;
3104
3925
  let _dirty = false;
3105
3926
  return {
3106
- markPoolTagPresent: function() {
3107
- _sawPoolTag = true;
3108
- },
3109
- hasPoolTag: function() {
3110
- 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;
3111
3937
  },
3112
3938
  getFromScope: function(shareScope) {
3113
3939
  return { ..._cache[shareScope ?? GLOBAL_SCOPE] };
@@ -3128,9 +3954,11 @@ var createSharedExternalsRepository = (config) => {
3128
3954
  Object.values(_cache).forEach((scope) => {
3129
3955
  const removeExternals = [];
3130
3956
  Object.entries(scope).forEach(([name, external]) => {
3131
- let removedVersion = false;
3957
+ let removedCopy = false;
3132
3958
  for (let i = external.versions.length - 1; i >= 0; i--) {
3133
- const remotes = external.versions[i].remotes;
3959
+ const version = external.versions[i];
3960
+ const remotes = version.remotes;
3961
+ const host = version.host ? remotes[0]?.name : void 0;
3134
3962
  let keep = 0;
3135
3963
  for (let r = 0; r < remotes.length; r++) {
3136
3964
  if (remoteNames.has(remotes[r].name)) continue;
@@ -3138,15 +3966,16 @@ var createSharedExternalsRepository = (config) => {
3138
3966
  }
3139
3967
  if (keep !== remotes.length) {
3140
3968
  remotes.length = keep;
3969
+ removedCopy = true;
3141
3970
  _dirty = true;
3971
+ if (host !== void 0 && remoteNames.has(host)) version.host = false;
3142
3972
  }
3143
3973
  if (remotes.length === 0) {
3144
3974
  external.versions.splice(i, 1);
3145
- removedVersion = true;
3146
3975
  _dirty = true;
3147
3976
  }
3148
3977
  }
3149
- if (removedVersion) {
3978
+ if (removedCopy) {
3150
3979
  external.dirty = true;
3151
3980
  if (external.versions.length === 0) removeExternals.push(name);
3152
3981
  }
@@ -3217,6 +4046,14 @@ var createChunkRepository = (config) => {
3217
4046
  _dirty = true;
3218
4047
  return this;
3219
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
+ }
4055
+ return this;
4056
+ },
3220
4057
  tryGet: function(remoteName, bundleName) {
3221
4058
  return Optional.of(_cache[remoteName]?.[bundleName]);
3222
4059
  },