pm-dedup-core 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/buildIdentifiedFixesMap.js +6 -0
  4. package/dist/buildIdentifiedFixesMap.js.map +1 -0
  5. package/dist/buildLockstepClusters.js +119 -0
  6. package/dist/buildLockstepClusters.js.map +1 -0
  7. package/dist/buildLockstepClusters.test.js +71 -0
  8. package/dist/buildLockstepClusters.test.js.map +1 -0
  9. package/dist/clusterLabel.js +59 -0
  10. package/dist/clusterLabel.js.map +1 -0
  11. package/dist/clusterLabel.test.js +56 -0
  12. package/dist/clusterLabel.test.js.map +1 -0
  13. package/dist/compareCandidateVersions.js +18 -0
  14. package/dist/compareCandidateVersions.js.map +1 -0
  15. package/dist/compareCandidateVersions.test.js +32 -0
  16. package/dist/compareCandidateVersions.test.js.map +1 -0
  17. package/dist/createPackageFilter.js +81 -0
  18. package/dist/createPackageFilter.js.map +1 -0
  19. package/dist/createPackageFilter.test.js +115 -0
  20. package/dist/createPackageFilter.test.js.map +1 -0
  21. package/dist/definitions/buildIdentifiedFixesMap.d.ts +4 -0
  22. package/dist/definitions/buildIdentifiedFixesMap.d.ts.map +1 -0
  23. package/dist/definitions/buildLockstepClusters.d.ts +8 -0
  24. package/dist/definitions/buildLockstepClusters.d.ts.map +1 -0
  25. package/dist/definitions/buildLockstepClusters.test.d.ts +2 -0
  26. package/dist/definitions/buildLockstepClusters.test.d.ts.map +1 -0
  27. package/dist/definitions/clusterLabel.d.ts +8 -0
  28. package/dist/definitions/clusterLabel.d.ts.map +1 -0
  29. package/dist/definitions/clusterLabel.test.d.ts +2 -0
  30. package/dist/definitions/clusterLabel.test.d.ts.map +1 -0
  31. package/dist/definitions/compareCandidateVersions.d.ts +10 -0
  32. package/dist/definitions/compareCandidateVersions.d.ts.map +1 -0
  33. package/dist/definitions/compareCandidateVersions.test.d.ts +2 -0
  34. package/dist/definitions/compareCandidateVersions.test.d.ts.map +1 -0
  35. package/dist/definitions/createPackageFilter.d.ts +45 -0
  36. package/dist/definitions/createPackageFilter.d.ts.map +1 -0
  37. package/dist/definitions/createPackageFilter.test.d.ts +2 -0
  38. package/dist/definitions/createPackageFilter.test.d.ts.map +1 -0
  39. package/dist/definitions/duplicateSnapshot.d.ts +8 -0
  40. package/dist/definitions/duplicateSnapshot.d.ts.map +1 -0
  41. package/dist/definitions/fileSnapshot.d.ts +7 -0
  42. package/dist/definitions/fileSnapshot.d.ts.map +1 -0
  43. package/dist/definitions/findProjectRoot.d.ts +27 -0
  44. package/dist/definitions/findProjectRoot.d.ts.map +1 -0
  45. package/dist/definitions/findProjectRoot.test.d.ts +2 -0
  46. package/dist/definitions/findProjectRoot.test.d.ts.map +1 -0
  47. package/dist/definitions/identifyLockstepClusterFixes.d.ts +89 -0
  48. package/dist/definitions/identifyLockstepClusterFixes.d.ts.map +1 -0
  49. package/dist/definitions/identifyLockstepClusterFixes.test.d.ts +2 -0
  50. package/dist/definitions/identifyLockstepClusterFixes.test.d.ts.map +1 -0
  51. package/dist/definitions/identifyResolutionFixes.d.ts +28 -0
  52. package/dist/definitions/identifyResolutionFixes.d.ts.map +1 -0
  53. package/dist/definitions/identifyResolutionFixes.test.d.ts +2 -0
  54. package/dist/definitions/identifyResolutionFixes.test.d.ts.map +1 -0
  55. package/dist/definitions/index.d.ts +44 -0
  56. package/dist/definitions/index.d.ts.map +1 -0
  57. package/dist/definitions/packageDependenciesUtils.d.ts +47 -0
  58. package/dist/definitions/packageDependenciesUtils.d.ts.map +1 -0
  59. package/dist/definitions/packageDependenciesUtils.test.d.ts +2 -0
  60. package/dist/definitions/packageDependenciesUtils.test.d.ts.map +1 -0
  61. package/dist/definitions/packageFilterArgs.d.ts +74 -0
  62. package/dist/definitions/packageFilterArgs.d.ts.map +1 -0
  63. package/dist/definitions/packageFilterArgs.test.d.ts +2 -0
  64. package/dist/definitions/packageFilterArgs.test.d.ts.map +1 -0
  65. package/dist/definitions/packageStyles.d.ts +14 -0
  66. package/dist/definitions/packageStyles.d.ts.map +1 -0
  67. package/dist/definitions/packageStyles.test.d.ts +2 -0
  68. package/dist/definitions/packageStyles.test.d.ts.map +1 -0
  69. package/dist/definitions/parseBinArgs.d.ts +14 -0
  70. package/dist/definitions/parseBinArgs.d.ts.map +1 -0
  71. package/dist/definitions/parseBinArgs.test.d.ts +2 -0
  72. package/dist/definitions/parseBinArgs.test.d.ts.map +1 -0
  73. package/dist/definitions/planClusterApply.d.ts +39 -0
  74. package/dist/definitions/planClusterApply.d.ts.map +1 -0
  75. package/dist/definitions/planClusterApply.test.d.ts +2 -0
  76. package/dist/definitions/planClusterApply.test.d.ts.map +1 -0
  77. package/dist/definitions/renderApplyPlan.d.ts +22 -0
  78. package/dist/definitions/renderApplyPlan.d.ts.map +1 -0
  79. package/dist/definitions/renderApplyPlan.test.d.ts +2 -0
  80. package/dist/definitions/renderApplyPlan.test.d.ts.map +1 -0
  81. package/dist/definitions/renderDedupeSummary.d.ts +22 -0
  82. package/dist/definitions/renderDedupeSummary.d.ts.map +1 -0
  83. package/dist/definitions/renderDedupeSummary.test.d.ts +2 -0
  84. package/dist/definitions/renderDedupeSummary.test.d.ts.map +1 -0
  85. package/dist/definitions/renderDuplicatesReport.d.ts +39 -0
  86. package/dist/definitions/renderDuplicatesReport.d.ts.map +1 -0
  87. package/dist/definitions/renderDuplicatesReport.test.d.ts +2 -0
  88. package/dist/definitions/renderDuplicatesReport.test.d.ts.map +1 -0
  89. package/dist/definitions/reportColors.d.ts +25 -0
  90. package/dist/definitions/reportColors.d.ts.map +1 -0
  91. package/dist/definitions/reportColors.test.d.ts +2 -0
  92. package/dist/definitions/reportColors.test.d.ts.map +1 -0
  93. package/dist/definitions/reportText.d.ts +29 -0
  94. package/dist/definitions/reportText.d.ts.map +1 -0
  95. package/dist/definitions/selectExplainedPackages.d.ts +22 -0
  96. package/dist/definitions/selectExplainedPackages.d.ts.map +1 -0
  97. package/dist/definitions/selectExplainedPackages.test.d.ts +2 -0
  98. package/dist/definitions/selectExplainedPackages.test.d.ts.map +1 -0
  99. package/dist/definitions/unconditionalOverrides.d.ts +24 -0
  100. package/dist/definitions/unconditionalOverrides.d.ts.map +1 -0
  101. package/dist/definitions/unconditionalOverrides.test.d.ts +2 -0
  102. package/dist/definitions/unconditionalOverrides.test.d.ts.map +1 -0
  103. package/dist/definitions/versionsSnapshot.d.ts +40 -0
  104. package/dist/definitions/versionsSnapshot.d.ts.map +1 -0
  105. package/dist/definitions/versionsSnapshot.test.d.ts +2 -0
  106. package/dist/definitions/versionsSnapshot.test.d.ts.map +1 -0
  107. package/dist/definitions/workspaceManifest.d.ts +21 -0
  108. package/dist/definitions/workspaceManifest.d.ts.map +1 -0
  109. package/dist/definitions/workspaceManifest.test.d.ts +2 -0
  110. package/dist/definitions/workspaceManifest.test.d.ts.map +1 -0
  111. package/dist/duplicateSnapshot.js +10 -0
  112. package/dist/duplicateSnapshot.js.map +1 -0
  113. package/dist/fileSnapshot.js +21 -0
  114. package/dist/fileSnapshot.js.map +1 -0
  115. package/dist/findProjectRoot.js +48 -0
  116. package/dist/findProjectRoot.js.map +1 -0
  117. package/dist/findProjectRoot.test.js +53 -0
  118. package/dist/findProjectRoot.test.js.map +1 -0
  119. package/dist/identifyLockstepClusterFixes.js +352 -0
  120. package/dist/identifyLockstepClusterFixes.js.map +1 -0
  121. package/dist/identifyLockstepClusterFixes.test.js +372 -0
  122. package/dist/identifyLockstepClusterFixes.test.js.map +1 -0
  123. package/dist/identifyResolutionFixes.js +102 -0
  124. package/dist/identifyResolutionFixes.js.map +1 -0
  125. package/dist/identifyResolutionFixes.test.js +70 -0
  126. package/dist/identifyResolutionFixes.test.js.map +1 -0
  127. package/dist/index.js +24 -0
  128. package/dist/index.js.map +1 -0
  129. package/dist/packageDependenciesUtils.js +91 -0
  130. package/dist/packageDependenciesUtils.js.map +1 -0
  131. package/dist/packageDependenciesUtils.test.js +539 -0
  132. package/dist/packageDependenciesUtils.test.js.map +1 -0
  133. package/dist/packageFilterArgs.js +65 -0
  134. package/dist/packageFilterArgs.js.map +1 -0
  135. package/dist/packageFilterArgs.test.js +93 -0
  136. package/dist/packageFilterArgs.test.js.map +1 -0
  137. package/dist/packageStyles.js +33 -0
  138. package/dist/packageStyles.js.map +1 -0
  139. package/dist/packageStyles.test.js +58 -0
  140. package/dist/packageStyles.test.js.map +1 -0
  141. package/dist/parseBinArgs.js +28 -0
  142. package/dist/parseBinArgs.js.map +1 -0
  143. package/dist/parseBinArgs.test.js +62 -0
  144. package/dist/parseBinArgs.test.js.map +1 -0
  145. package/dist/planClusterApply.js +69 -0
  146. package/dist/planClusterApply.js.map +1 -0
  147. package/dist/planClusterApply.test.js +152 -0
  148. package/dist/planClusterApply.test.js.map +1 -0
  149. package/dist/renderApplyPlan.js +51 -0
  150. package/dist/renderApplyPlan.js.map +1 -0
  151. package/dist/renderApplyPlan.test.js +106 -0
  152. package/dist/renderApplyPlan.test.js.map +1 -0
  153. package/dist/renderDedupeSummary.js +38 -0
  154. package/dist/renderDedupeSummary.js.map +1 -0
  155. package/dist/renderDedupeSummary.test.js +91 -0
  156. package/dist/renderDedupeSummary.test.js.map +1 -0
  157. package/dist/renderDuplicatesReport.js +375 -0
  158. package/dist/renderDuplicatesReport.js.map +1 -0
  159. package/dist/renderDuplicatesReport.test.js +598 -0
  160. package/dist/renderDuplicatesReport.test.js.map +1 -0
  161. package/dist/reportColors.js +51 -0
  162. package/dist/reportColors.js.map +1 -0
  163. package/dist/reportColors.test.js +49 -0
  164. package/dist/reportColors.test.js.map +1 -0
  165. package/dist/reportText.js +29 -0
  166. package/dist/reportText.js.map +1 -0
  167. package/dist/selectExplainedPackages.js +42 -0
  168. package/dist/selectExplainedPackages.js.map +1 -0
  169. package/dist/selectExplainedPackages.test.js +55 -0
  170. package/dist/selectExplainedPackages.test.js.map +1 -0
  171. package/dist/unconditionalOverrides.js +36 -0
  172. package/dist/unconditionalOverrides.js.map +1 -0
  173. package/dist/unconditionalOverrides.test.js +71 -0
  174. package/dist/unconditionalOverrides.test.js.map +1 -0
  175. package/dist/versionsSnapshot.js +49 -0
  176. package/dist/versionsSnapshot.js.map +1 -0
  177. package/dist/versionsSnapshot.test.js +79 -0
  178. package/dist/versionsSnapshot.test.js.map +1 -0
  179. package/dist/workspaceManifest.js +63 -0
  180. package/dist/workspaceManifest.js.map +1 -0
  181. package/dist/workspaceManifest.test.js +159 -0
  182. package/dist/workspaceManifest.test.js.map +1 -0
  183. package/package.json +68 -0
  184. package/src/buildIdentifiedFixesMap.ts +22 -0
  185. package/src/buildLockstepClusters.test.ts +90 -0
  186. package/src/buildLockstepClusters.ts +155 -0
  187. package/src/clusterLabel.test.ts +79 -0
  188. package/src/clusterLabel.ts +75 -0
  189. package/src/compareCandidateVersions.test.ts +39 -0
  190. package/src/compareCandidateVersions.ts +23 -0
  191. package/src/createPackageFilter.test.ts +156 -0
  192. package/src/createPackageFilter.ts +142 -0
  193. package/src/duplicateSnapshot.ts +24 -0
  194. package/src/fileSnapshot.ts +28 -0
  195. package/src/findProjectRoot.test.ts +66 -0
  196. package/src/findProjectRoot.ts +61 -0
  197. package/src/identifyLockstepClusterFixes.test.ts +462 -0
  198. package/src/identifyLockstepClusterFixes.ts +602 -0
  199. package/src/identifyResolutionFixes.test.ts +133 -0
  200. package/src/identifyResolutionFixes.ts +183 -0
  201. package/src/index.ts +123 -0
  202. package/src/packageDependenciesUtils.test.ts +593 -0
  203. package/src/packageDependenciesUtils.ts +156 -0
  204. package/src/packageFilterArgs.test.ts +116 -0
  205. package/src/packageFilterArgs.ts +98 -0
  206. package/src/packageStyles.test.ts +77 -0
  207. package/src/packageStyles.ts +42 -0
  208. package/src/parseBinArgs.test.ts +73 -0
  209. package/src/parseBinArgs.ts +33 -0
  210. package/src/planClusterApply.test.ts +163 -0
  211. package/src/planClusterApply.ts +105 -0
  212. package/src/renderApplyPlan.test.ts +136 -0
  213. package/src/renderApplyPlan.ts +95 -0
  214. package/src/renderDedupeSummary.test.ts +121 -0
  215. package/src/renderDedupeSummary.ts +78 -0
  216. package/src/renderDuplicatesReport.test.ts +724 -0
  217. package/src/renderDuplicatesReport.ts +645 -0
  218. package/src/reportColors.test.ts +57 -0
  219. package/src/reportColors.ts +78 -0
  220. package/src/reportText.ts +58 -0
  221. package/src/selectExplainedPackages.test.ts +67 -0
  222. package/src/selectExplainedPackages.ts +79 -0
  223. package/src/unconditionalOverrides.test.ts +99 -0
  224. package/src/unconditionalOverrides.ts +60 -0
  225. package/src/versionsSnapshot.test.ts +146 -0
  226. package/src/versionsSnapshot.ts +93 -0
  227. package/src/workspaceManifest.test.ts +220 -0
  228. package/src/workspaceManifest.ts +100 -0
@@ -0,0 +1,133 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type {
3
+ NpmResolutionPackage,
4
+ ResolutionDependent,
5
+ ResolutionEntry,
6
+ } from "./identifyResolutionFixes.ts";
7
+ import { identifyResolutionFixes } from "./identifyResolutionFixes.ts";
8
+
9
+ const npm = (
10
+ name: string,
11
+ version: string,
12
+ ): ResolutionEntry<NpmResolutionPackage> => ({
13
+ resolution: `${name}@${version}`,
14
+ package: { type: "npm", name, version },
15
+ });
16
+
17
+ const workspace = (name: string): ResolutionEntry => ({
18
+ resolution: `${name}@workspace`,
19
+ package: { type: "workspace", name },
20
+ });
21
+
22
+ const dependentsOf = (
23
+ name: string,
24
+ ranges: string[],
25
+ ): Map<string, ResolutionDependent[]> =>
26
+ new Map([[name, ranges.map((version) => ({ version }))]]);
27
+
28
+ describe("identifyResolutionFixes", () => {
29
+ it("ignores a dependent whose declaration semver cannot read", () => {
30
+ // read as a range, `workspace:*` satisfies neither candidate and the merge
31
+ // the real dependent allows would be suppressed
32
+ const dependents = new Map<string, ResolutionDependent[]>([
33
+ [
34
+ "lib",
35
+ [{ version: "^1.0.0" }, { version: "workspace:*", nonSemver: true }],
36
+ ],
37
+ ]);
38
+
39
+ expect(
40
+ identifyResolutionFixes(
41
+ [npm("lib", "1.0.0"), npm("lib", "1.2.0")],
42
+ dependents,
43
+ ),
44
+ ).toEqual([
45
+ { mergeableResolutions: ["lib@1.0.0", "lib@1.2.0"], to: "lib@1.2.0" },
46
+ ]);
47
+ });
48
+
49
+ it("vouches for no merge when every dependent is unreadable", () => {
50
+ const dependents = new Map<string, ResolutionDependent[]>([
51
+ ["lib", [{ version: "workspace:*", nonSemver: true }]],
52
+ ]);
53
+
54
+ expect(
55
+ identifyResolutionFixes(
56
+ [npm("lib", "1.0.0"), npm("lib", "1.2.0")],
57
+ dependents,
58
+ ),
59
+ ).toEqual([]);
60
+ });
61
+
62
+ it("returns nothing for fewer than two resolutions", () => {
63
+ expect(identifyResolutionFixes([], new Map())).toEqual([]);
64
+ expect(identifyResolutionFixes([npm("lib", "1.0.0")], new Map())).toEqual(
65
+ [],
66
+ );
67
+ });
68
+
69
+ it("merges every resolution onto the highest version satisfying all dependents", () => {
70
+ const fixes = identifyResolutionFixes(
71
+ [npm("lib", "1.0.0"), npm("lib", "1.2.0")],
72
+ dependentsOf("lib", ["^1.0.0", "^1.2.0"]),
73
+ );
74
+
75
+ expect(fixes).toEqual([
76
+ { mergeableResolutions: ["lib@1.0.0", "lib@1.2.0"], to: "lib@1.2.0" },
77
+ ]);
78
+ });
79
+
80
+ it("partitions greedily when no version satisfies every dependent", () => {
81
+ const fixes = identifyResolutionFixes(
82
+ [npm("lib", "1.0.0"), npm("lib", "1.2.0"), npm("lib", "2.0.0")],
83
+ dependentsOf("lib", ["^1.0.0", "^1.2.0", "^2.0.0"]),
84
+ );
85
+
86
+ expect(fixes).toEqual([
87
+ { mergeableResolutions: ["lib@1.0.0", "lib@1.2.0"], to: "lib@1.2.0" },
88
+ ]);
89
+ });
90
+
91
+ it("refuses a merge that would drop a dependent the merged resolution served", () => {
92
+ // `>=1.0.0` accepts 2.0.0, but merging 1.2.0 into it would strand `^1.0.0`
93
+ const fixes = identifyResolutionFixes(
94
+ [npm("lib", "1.0.0"), npm("lib", "1.2.0"), npm("lib", "2.0.0")],
95
+ dependentsOf("lib", [">=1.0.0", "^1.0.0", "2.0.0"]),
96
+ );
97
+
98
+ expect(fixes).toEqual([
99
+ { mergeableResolutions: ["lib@1.0.0", "lib@1.2.0"], to: "lib@1.2.0" },
100
+ ]);
101
+ });
102
+
103
+ it("ignores resolutions that are not npm packages", () => {
104
+ const fixes = identifyResolutionFixes(
105
+ [npm("lib", "1.0.0"), workspace("lib"), npm("lib", "1.2.0")],
106
+ dependentsOf("lib", ["^1.0.0", "^1.2.0"]),
107
+ );
108
+
109
+ expect(fixes).toEqual([
110
+ { mergeableResolutions: ["lib@1.0.0", "lib@1.2.0"], to: "lib@1.2.0" },
111
+ ]);
112
+ });
113
+
114
+ // Reachable as soon as every requester declares the package through something
115
+ // semver cannot read: those declarations are left out of the map, and no range
116
+ // is left to vouch for a merge. Reading the empty set as "one candidate covers
117
+ // every dependent" would merge the whole package on no evidence at all.
118
+ it("proposes nothing for a package with no collected dependents", () => {
119
+ expect(
120
+ identifyResolutionFixes(
121
+ [npm("lib", "1.0.0"), npm("lib", "1.2.0")],
122
+ new Map(),
123
+ ),
124
+ ).toEqual([]);
125
+
126
+ expect(
127
+ identifyResolutionFixes(
128
+ [npm("lib", "1.0.0"), npm("lib", "1.2.0")],
129
+ new Map([["lib", []]]),
130
+ ),
131
+ ).toEqual([]);
132
+ });
133
+ });
@@ -0,0 +1,183 @@
1
+ import semver from "semver";
2
+ import { createCandidateVersionComparator } from "./compareCandidateVersions.ts";
3
+
4
+ export interface ResolutionPackage {
5
+ type: string;
6
+ name: string;
7
+ }
8
+
9
+ export interface NpmResolutionPackage extends ResolutionPackage {
10
+ type: "npm";
11
+ version: string;
12
+ }
13
+
14
+ export interface ResolutionEntry<
15
+ P extends ResolutionPackage = ResolutionPackage,
16
+ > {
17
+ resolution: string;
18
+ package: P;
19
+ }
20
+
21
+ export interface ResolutionDependent {
22
+ // the range this dependent declares
23
+ version: string;
24
+ // the declaration is not a semver range — a git url, a tarball, `file:`,
25
+ // `workspace:`, `patch:`. It is kept in the map so the report can name the
26
+ // requester of a resolution nothing else explains, but no candidate version
27
+ // can be tested against it, so it takes no part in a merge decision.
28
+ nonSemver?: boolean;
29
+ }
30
+
31
+ export type ResolutionDependentsMap<
32
+ D extends ResolutionDependent = ResolutionDependent,
33
+ > = Map<string, D[]>;
34
+
35
+ export interface ResolutionFix {
36
+ mergeableResolutions: string[];
37
+ to: string;
38
+ }
39
+
40
+ const isNpmResolution = (
41
+ resolution: ResolutionEntry,
42
+ ): resolution is ResolutionEntry<NpmResolutionPackage> =>
43
+ resolution.package.type === "npm";
44
+
45
+ /**
46
+ * Dedupe opportunities reachable one package at a time. A family of co-versioned
47
+ * packages whose duplicate survives only because a few members resolve high is
48
+ * invisible here and is handled by `identifyLockstepClusterFixes`.
49
+ */
50
+ export const identifyResolutionFixes = <D extends ResolutionDependent>(
51
+ resolutions: ResolutionEntry[],
52
+ dependents: ResolutionDependentsMap<D>,
53
+ ): ResolutionFix[] => {
54
+ if (resolutions.length <= 1) {
55
+ return [];
56
+ }
57
+
58
+ const onlyNpmResolutions = resolutions.filter(isNpmResolution);
59
+
60
+ const fixes: ResolutionFix[] = [];
61
+
62
+ const packageNames = new Set(
63
+ resolutions.map((resolution) => resolution.package.name),
64
+ );
65
+
66
+ for (const packageName of packageNames) {
67
+ const npmVersions = onlyNpmResolutions
68
+ .filter((resolution) => resolution.package.name === packageName)
69
+ .map((resolution) => ({
70
+ resolution,
71
+ version: resolution.package.version,
72
+ satisfies: new Set<D>(),
73
+ }));
74
+
75
+ // Every requester can declare the package through something semver cannot
76
+ // read — a git url, `file:`, `workspace:` — and such a declaration is
77
+ // dropped here rather than treated as a range nothing satisfies, which
78
+ // would suppress the merges the real dependents allow. Once they are gone
79
+ // nothing may vouch for a merge at all, and the "one candidate covers every
80
+ // dependent" test below would read the empty set as full coverage.
81
+ const packageDependents = dependents
82
+ .get(packageName)
83
+ ?.filter((dependent) => !dependent.nonSemver);
84
+ if (!packageDependents || packageDependents.length === 0) continue;
85
+
86
+ packageDependents.forEach((dependent) => {
87
+ for (const { version, satisfies } of npmVersions) {
88
+ if (
89
+ semver.satisfies(version, dependent.version, {
90
+ includePrerelease: true,
91
+ })
92
+ ) {
93
+ satisfies.add(dependent);
94
+ }
95
+ }
96
+ });
97
+
98
+ const byVersionAscending = (
99
+ resolutionA: string,
100
+ resolutionB: string,
101
+ ): number => {
102
+ const versionA = npmVersions.find(
103
+ (candidate) => candidate.resolution.resolution === resolutionA,
104
+ )?.version;
105
+ const versionB = npmVersions.find(
106
+ (candidate) => candidate.resolution.resolution === resolutionB,
107
+ )?.version;
108
+ return versionA && versionB ? semver.compare(versionA, versionB) : 0;
109
+ };
110
+
111
+ const satisfiedCountByVersion = new Map(
112
+ npmVersions.map(({ version, satisfies }) => [version, satisfies.size]),
113
+ );
114
+ const compareCandidates = createCandidateVersionComparator({
115
+ satisfiedCountOf: (version) => satisfiedCountByVersion.get(version) ?? 0,
116
+ });
117
+
118
+ // the best-ranked candidate satisfies everyone whenever any candidate does
119
+ const [bestCandidate] = npmVersions.toSorted((candidateA, candidateB) =>
120
+ compareCandidates(candidateA.version, candidateB.version),
121
+ );
122
+
123
+ if (bestCandidate?.satisfies.size === packageDependents.length) {
124
+ const mergeableResolutions = npmVersions
125
+ .map((candidate) => candidate.resolution.resolution)
126
+ .toSorted(byVersionAscending);
127
+
128
+ if (mergeableResolutions.length > 1) {
129
+ fixes.push({
130
+ mergeableResolutions,
131
+ to: bestCandidate.resolution.resolution,
132
+ });
133
+ }
134
+
135
+ continue;
136
+ }
137
+
138
+ // no single candidate satisfies all dependents — do greedy assignment from highest -> lowest
139
+ npmVersions.sort((candidateA, candidateB) =>
140
+ semver.rcompare(candidateA.version, candidateB.version),
141
+ );
142
+ const unassigned = new Set<D>(packageDependents);
143
+
144
+ for (const candidate of npmVersions) {
145
+ const newlyAssigned = new Set<D>(
146
+ [...candidate.satisfies].filter((dependent) =>
147
+ unassigned.has(dependent),
148
+ ),
149
+ );
150
+ if (newlyAssigned.size === 0) continue;
151
+
152
+ const mergeable = npmVersions.filter((other) => {
153
+ if (other === candidate) return false;
154
+ const intersects = [...other.satisfies].some((dependent) =>
155
+ newlyAssigned.has(dependent),
156
+ );
157
+ if (!intersects) return false;
158
+ // merging is safe only when the candidate satisfies every dependent the
159
+ // resolution being merged away was serving
160
+ return [...other.satisfies].every((dependent) =>
161
+ candidate.satisfies.has(dependent),
162
+ );
163
+ });
164
+
165
+ const mergeableResolutions = [
166
+ candidate.resolution.resolution,
167
+ ...mergeable.map((other) => other.resolution.resolution),
168
+ ].toSorted(byVersionAscending);
169
+
170
+ if (mergeableResolutions.length > 1) {
171
+ fixes.push({
172
+ mergeableResolutions,
173
+ to: candidate.resolution.resolution,
174
+ });
175
+ }
176
+
177
+ for (const dependent of newlyAssigned) unassigned.delete(dependent);
178
+ if (unassigned.size === 0) break;
179
+ }
180
+ }
181
+
182
+ return fixes;
183
+ };
package/src/index.ts ADDED
@@ -0,0 +1,123 @@
1
+ export {
2
+ PackageDescriptorNameUtils,
3
+ PackageDependencyDescriptorUtils,
4
+ isNpmProtocol,
5
+ isSemverComparable,
6
+ } from "./packageDependenciesUtils.ts";
7
+ export type {
8
+ PackageDescriptorName,
9
+ PackageDependencyDescriptor,
10
+ } from "./packageDependenciesUtils.ts";
11
+ export { buildLockstepClusters } from "./buildLockstepClusters.ts";
12
+ export type {
13
+ LockstepGraph,
14
+ LockstepResolution,
15
+ } from "./buildLockstepClusters.ts";
16
+ export { identifyLockstepClusterFixes } from "./identifyLockstepClusterFixes.ts";
17
+ export type {
18
+ ClusterDependent,
19
+ ClusterDependentsMap,
20
+ ClusterExcludedMember,
21
+ ClusterExternalConstraint,
22
+ ClusterFix,
23
+ ClusterMember,
24
+ ClusterMemberVersions,
25
+ ClusterMembersMap,
26
+ ClusterReuseFix,
27
+ ClusterWorkspaceChange,
28
+ ClusterWorkspaceRef,
29
+ } from "./identifyLockstepClusterFixes.ts";
30
+ export { identifyResolutionFixes } from "./identifyResolutionFixes.ts";
31
+ export type {
32
+ NpmResolutionPackage,
33
+ ResolutionDependent,
34
+ ResolutionDependentsMap,
35
+ ResolutionEntry,
36
+ ResolutionFix,
37
+ ResolutionPackage,
38
+ } from "./identifyResolutionFixes.ts";
39
+ export { buildIdentifiedFixesMap } from "./buildIdentifiedFixesMap.ts";
40
+ export type { ResolutionsMap } from "./buildIdentifiedFixesMap.ts";
41
+ export {
42
+ createPackageFilter,
43
+ describeSkippedClusterFix,
44
+ selectClusterFixes,
45
+ selectPackages,
46
+ } from "./createPackageFilter.ts";
47
+ export type {
48
+ PackageFilter,
49
+ PackageFilterOptions,
50
+ SelectedClusterFixes,
51
+ SkippedClusterFix,
52
+ } from "./createPackageFilter.ts";
53
+ export {
54
+ packageFilterParseArgsOptions,
55
+ packageFilterUsage,
56
+ toPackageFilterOptions,
57
+ toWhyDuplicateRequest,
58
+ whyDuplicateParseArgsOptions,
59
+ whyDuplicateUsage,
60
+ } from "./packageFilterArgs.ts";
61
+ export type {
62
+ PackageFilterArgValues,
63
+ WhyDuplicateArgValues,
64
+ WhyDuplicateRequest,
65
+ } from "./packageFilterArgs.ts";
66
+ export { parseBinArgs } from "./parseBinArgs.ts";
67
+ export { findProjectRoot, resolveProjectDir } from "./findProjectRoot.ts";
68
+ export type { ProjectRootOptions } from "./findProjectRoot.ts";
69
+ export { createCandidateVersionComparator } from "./compareCandidateVersions.ts";
70
+ export type { CandidateVersionComparatorOptions } from "./compareCandidateVersions.ts";
71
+ export { clusterLabel } from "./clusterLabel.ts";
72
+ export { createColorize, shouldColorize } from "./reportColors.ts";
73
+ export type { Colorize, PaletteStyle, ReportStyle } from "./reportColors.ts";
74
+ export { stylePackageName, stylePackageReference } from "./packageStyles.ts";
75
+ export { renderDuplicatesReport } from "./renderDuplicatesReport.ts";
76
+ export type {
77
+ DuplicateDedupeView,
78
+ DuplicateDependentView,
79
+ DuplicatePackageView,
80
+ DuplicateResolutionView,
81
+ DuplicatesReportOptions,
82
+ DuplicatesReportTitle,
83
+ } from "./renderDuplicatesReport.ts";
84
+ export { selectExplainedPackages } from "./selectExplainedPackages.ts";
85
+ export type {
86
+ ExplainedPackagesSelection,
87
+ SelectExplainedPackagesOptions,
88
+ } from "./selectExplainedPackages.ts";
89
+ export { renderApplyPlan } from "./renderApplyPlan.ts";
90
+ export type {
91
+ ApplyPlanFileChange,
92
+ ApplyPlanOptions,
93
+ ApplyPlanSummary,
94
+ } from "./renderApplyPlan.ts";
95
+ export { renderDedupeSummary } from "./renderDedupeSummary.ts";
96
+ export type { DedupeSummaryOptions } from "./renderDedupeSummary.ts";
97
+ export {
98
+ buildVersionsSnapshot,
99
+ countDuplicatedPackages,
100
+ diffVersionsSnapshots,
101
+ } from "./versionsSnapshot.ts";
102
+ export type {
103
+ DedupedPackage,
104
+ SnapshotPackage,
105
+ VersionsSnapshot,
106
+ } from "./versionsSnapshot.ts";
107
+ export { captureFiles, restoreFiles } from "./fileSnapshot.ts";
108
+ export type { FileSnapshot } from "./fileSnapshot.ts";
109
+ export { applyWorkspaceRangeEdit, nextSelector } from "./workspaceManifest.ts";
110
+ export type { WorkspaceRangeEdit } from "./workspaceManifest.ts";
111
+ export { planClusterApply } from "./planClusterApply.ts";
112
+ export type {
113
+ ClusterApplyPlan,
114
+ PlannedManifestEdit,
115
+ PlannedOverride,
116
+ } from "./planClusterApply.ts";
117
+ export { diffDuplicates } from "./duplicateSnapshot.ts";
118
+ export type { DuplicateDiff, DuplicateSnapshot } from "./duplicateSnapshot.ts";
119
+ export { partitionUnconditionalOverrides } from "./unconditionalOverrides.ts";
120
+ export type {
121
+ PartitionedOverrides,
122
+ RejectedOverride,
123
+ } from "./unconditionalOverrides.ts";