pnpm-dedup 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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +218 -0
  3. package/dist/applyClusterFixes.js +252 -0
  4. package/dist/applyClusterFixes.js.map +1 -0
  5. package/dist/applyClusterFixes.test.js +474 -0
  6. package/dist/applyClusterFixes.test.js.map +1 -0
  7. package/dist/bin/pnpm-dedupe.js +35 -0
  8. package/dist/bin/pnpm-dedupe.js.map +1 -0
  9. package/dist/bin/pnpm-why-duplicate.js +23 -0
  10. package/dist/bin/pnpm-why-duplicate.js.map +1 -0
  11. package/dist/bin/pnpm-why-duplicate.test.js +57 -0
  12. package/dist/bin/pnpm-why-duplicate.test.js.map +1 -0
  13. package/dist/dedupe.js +86 -0
  14. package/dist/dedupe.js.map +1 -0
  15. package/dist/dedupeCheckConsistency.test.js +149 -0
  16. package/dist/dedupeCheckConsistency.test.js.map +1 -0
  17. package/dist/dedupeEndToEnd.test.js +121 -0
  18. package/dist/dedupeEndToEnd.test.js.map +1 -0
  19. package/dist/dedupePeers.test.js +105 -0
  20. package/dist/dedupePeers.test.js.map +1 -0
  21. package/dist/definitions/applyClusterFixes.d.ts +24 -0
  22. package/dist/definitions/applyClusterFixes.d.ts.map +1 -0
  23. package/dist/definitions/applyClusterFixes.test.d.ts +2 -0
  24. package/dist/definitions/applyClusterFixes.test.d.ts.map +1 -0
  25. package/dist/definitions/bin/pnpm-dedupe.d.ts +3 -0
  26. package/dist/definitions/bin/pnpm-dedupe.d.ts.map +1 -0
  27. package/dist/definitions/bin/pnpm-why-duplicate.d.ts +3 -0
  28. package/dist/definitions/bin/pnpm-why-duplicate.d.ts.map +1 -0
  29. package/dist/definitions/bin/pnpm-why-duplicate.test.d.ts +2 -0
  30. package/dist/definitions/bin/pnpm-why-duplicate.test.d.ts.map +1 -0
  31. package/dist/definitions/dedupe.d.ts +15 -0
  32. package/dist/definitions/dedupe.d.ts.map +1 -0
  33. package/dist/definitions/dedupeCheckConsistency.test.d.ts +2 -0
  34. package/dist/definitions/dedupeCheckConsistency.test.d.ts.map +1 -0
  35. package/dist/definitions/dedupeEndToEnd.test.d.ts +2 -0
  36. package/dist/definitions/dedupeEndToEnd.test.d.ts.map +1 -0
  37. package/dist/definitions/dedupePeers.test.d.ts +2 -0
  38. package/dist/definitions/dedupePeers.test.d.ts.map +1 -0
  39. package/dist/definitions/displayMany.d.ts +16 -0
  40. package/dist/definitions/displayMany.d.ts.map +1 -0
  41. package/dist/definitions/displayMany.test.d.ts +2 -0
  42. package/dist/definitions/displayMany.test.d.ts.map +1 -0
  43. package/dist/definitions/helpers/buildIdentifiedFixesMap.test.d.ts +2 -0
  44. package/dist/definitions/helpers/buildIdentifiedFixesMap.test.d.ts.map +1 -0
  45. package/dist/definitions/helpers/buildPnpmPackagesMap.d.ts +13 -0
  46. package/dist/definitions/helpers/buildPnpmPackagesMap.d.ts.map +1 -0
  47. package/dist/definitions/helpers/buildPnpmPackagesMap.test.d.ts +2 -0
  48. package/dist/definitions/helpers/buildPnpmPackagesMap.test.d.ts.map +1 -0
  49. package/dist/definitions/helpers/collectDependentRanges.d.ts +22 -0
  50. package/dist/definitions/helpers/collectDependentRanges.d.ts.map +1 -0
  51. package/dist/definitions/helpers/collectDependentRanges.test.d.ts +2 -0
  52. package/dist/definitions/helpers/collectDependentRanges.test.d.ts.map +1 -0
  53. package/dist/definitions/helpers/collectPnpmDependents.d.ts +8 -0
  54. package/dist/definitions/helpers/collectPnpmDependents.d.ts.map +1 -0
  55. package/dist/definitions/helpers/collectPnpmDependents.test.d.ts +2 -0
  56. package/dist/definitions/helpers/collectPnpmDependents.test.d.ts.map +1 -0
  57. package/dist/definitions/helpers/duplicateSnapshot.d.ts +3 -0
  58. package/dist/definitions/helpers/duplicateSnapshot.d.ts.map +1 -0
  59. package/dist/definitions/helpers/parsePnpmLockPackages.d.ts +44 -0
  60. package/dist/definitions/helpers/parsePnpmLockPackages.d.ts.map +1 -0
  61. package/dist/definitions/helpers/parsePnpmLockPackages.test.d.ts +2 -0
  62. package/dist/definitions/helpers/parsePnpmLockPackages.test.d.ts.map +1 -0
  63. package/dist/definitions/helpers/pnpmProtocol.d.ts +2 -0
  64. package/dist/definitions/helpers/pnpmProtocol.d.ts.map +1 -0
  65. package/dist/definitions/helpers/pnpmVersion.d.ts +11 -0
  66. package/dist/definitions/helpers/pnpmVersion.d.ts.map +1 -0
  67. package/dist/definitions/helpers/pnpmWorkspaceYaml.d.ts +23 -0
  68. package/dist/definitions/helpers/pnpmWorkspaceYaml.d.ts.map +1 -0
  69. package/dist/definitions/helpers/pnpmWorkspaceYaml.test.d.ts +2 -0
  70. package/dist/definitions/helpers/pnpmWorkspaceYaml.test.d.ts.map +1 -0
  71. package/dist/definitions/helpers/projectDir.d.ts +3 -0
  72. package/dist/definitions/helpers/projectDir.d.ts.map +1 -0
  73. package/dist/definitions/helpers/readInstalledManifest.d.ts +32 -0
  74. package/dist/definitions/helpers/readInstalledManifest.d.ts.map +1 -0
  75. package/dist/definitions/helpers/readInstalledManifest.test.d.ts +2 -0
  76. package/dist/definitions/helpers/readInstalledManifest.test.d.ts.map +1 -0
  77. package/dist/definitions/helpers/runPnpm.d.ts +11 -0
  78. package/dist/definitions/helpers/runPnpm.d.ts.map +1 -0
  79. package/dist/definitions/helpers/tempProjects.d.ts +8 -0
  80. package/dist/definitions/helpers/tempProjects.d.ts.map +1 -0
  81. package/dist/definitions/helpers/toLockstepGraph.d.ts +12 -0
  82. package/dist/definitions/helpers/toLockstepGraph.d.ts.map +1 -0
  83. package/dist/definitions/helpers/toLockstepGraph.test.d.ts +2 -0
  84. package/dist/definitions/helpers/toLockstepGraph.test.d.ts.map +1 -0
  85. package/dist/definitions/helpers/versionsSnapshot.d.ts +3 -0
  86. package/dist/definitions/helpers/versionsSnapshot.d.ts.map +1 -0
  87. package/dist/definitions/identifyClusterFixes.d.ts +7 -0
  88. package/dist/definitions/identifyClusterFixes.d.ts.map +1 -0
  89. package/dist/definitions/identifyClusterFixes.test.d.ts +2 -0
  90. package/dist/definitions/identifyClusterFixes.test.d.ts.map +1 -0
  91. package/dist/definitions/identifyResolutionFixes.test.d.ts +2 -0
  92. package/dist/definitions/identifyResolutionFixes.test.d.ts.map +1 -0
  93. package/dist/definitions/index.d.ts +25 -0
  94. package/dist/definitions/index.d.ts.map +1 -0
  95. package/dist/definitions/pnpmLockTypes.d.ts +42 -0
  96. package/dist/definitions/pnpmLockTypes.d.ts.map +1 -0
  97. package/dist/definitions/readPnpmLock.d.ts +3 -0
  98. package/dist/definitions/readPnpmLock.d.ts.map +1 -0
  99. package/dist/displayMany.js +56 -0
  100. package/dist/displayMany.js.map +1 -0
  101. package/dist/displayMany.test.js +146 -0
  102. package/dist/displayMany.test.js.map +1 -0
  103. package/dist/helpers/buildIdentifiedFixesMap.test.js +27 -0
  104. package/dist/helpers/buildIdentifiedFixesMap.test.js.map +1 -0
  105. package/dist/helpers/buildPnpmPackagesMap.js +23 -0
  106. package/dist/helpers/buildPnpmPackagesMap.js.map +1 -0
  107. package/dist/helpers/buildPnpmPackagesMap.test.js +68 -0
  108. package/dist/helpers/buildPnpmPackagesMap.test.js.map +1 -0
  109. package/dist/helpers/collectDependentRanges.js +118 -0
  110. package/dist/helpers/collectDependentRanges.js.map +1 -0
  111. package/dist/helpers/collectDependentRanges.test.js +211 -0
  112. package/dist/helpers/collectDependentRanges.test.js.map +1 -0
  113. package/dist/helpers/collectPnpmDependents.js +55 -0
  114. package/dist/helpers/collectPnpmDependents.js.map +1 -0
  115. package/dist/helpers/collectPnpmDependents.test.js +78 -0
  116. package/dist/helpers/collectPnpmDependents.test.js.map +1 -0
  117. package/dist/helpers/duplicateSnapshot.js +5 -0
  118. package/dist/helpers/duplicateSnapshot.js.map +1 -0
  119. package/dist/helpers/parsePnpmLockPackages.js +62 -0
  120. package/dist/helpers/parsePnpmLockPackages.js.map +1 -0
  121. package/dist/helpers/parsePnpmLockPackages.test.js +62 -0
  122. package/dist/helpers/parsePnpmLockPackages.test.js.map +1 -0
  123. package/dist/helpers/pnpmProtocol.js +2 -0
  124. package/dist/helpers/pnpmProtocol.js.map +1 -0
  125. package/dist/helpers/pnpmVersion.js +23 -0
  126. package/dist/helpers/pnpmVersion.js.map +1 -0
  127. package/dist/helpers/pnpmWorkspaceYaml.js +48 -0
  128. package/dist/helpers/pnpmWorkspaceYaml.js.map +1 -0
  129. package/dist/helpers/pnpmWorkspaceYaml.test.js +91 -0
  130. package/dist/helpers/pnpmWorkspaceYaml.test.js.map +1 -0
  131. package/dist/helpers/projectDir.js +10 -0
  132. package/dist/helpers/projectDir.js.map +1 -0
  133. package/dist/helpers/readInstalledManifest.js +139 -0
  134. package/dist/helpers/readInstalledManifest.js.map +1 -0
  135. package/dist/helpers/readInstalledManifest.test.js +83 -0
  136. package/dist/helpers/readInstalledManifest.test.js.map +1 -0
  137. package/dist/helpers/runPnpm.js +16 -0
  138. package/dist/helpers/runPnpm.js.map +1 -0
  139. package/dist/helpers/tempProjects.js +85 -0
  140. package/dist/helpers/tempProjects.js.map +1 -0
  141. package/dist/helpers/toLockstepGraph.js +34 -0
  142. package/dist/helpers/toLockstepGraph.js.map +1 -0
  143. package/dist/helpers/toLockstepGraph.test.js +81 -0
  144. package/dist/helpers/toLockstepGraph.test.js.map +1 -0
  145. package/dist/helpers/versionsSnapshot.js +5 -0
  146. package/dist/helpers/versionsSnapshot.js.map +1 -0
  147. package/dist/identifyClusterFixes.js +53 -0
  148. package/dist/identifyClusterFixes.js.map +1 -0
  149. package/dist/identifyClusterFixes.test.js +307 -0
  150. package/dist/identifyClusterFixes.test.js.map +1 -0
  151. package/dist/identifyResolutionFixes.test.js +49 -0
  152. package/dist/identifyResolutionFixes.test.js.map +1 -0
  153. package/dist/index.js +86 -0
  154. package/dist/index.js.map +1 -0
  155. package/dist/pnpmLockTypes.js +7 -0
  156. package/dist/pnpmLockTypes.js.map +1 -0
  157. package/dist/readPnpmLock.js +6 -0
  158. package/dist/readPnpmLock.js.map +1 -0
  159. package/package.json +71 -0
  160. package/src/applyClusterFixes.test.ts +560 -0
  161. package/src/applyClusterFixes.ts +466 -0
  162. package/src/bin/pnpm-dedupe.ts +44 -0
  163. package/src/bin/pnpm-why-duplicate.test.ts +79 -0
  164. package/src/bin/pnpm-why-duplicate.ts +34 -0
  165. package/src/dedupe.ts +124 -0
  166. package/src/dedupeCheckConsistency.test.ts +218 -0
  167. package/src/dedupeEndToEnd.test.ts +168 -0
  168. package/src/dedupePeers.test.ts +175 -0
  169. package/src/displayMany.test.ts +208 -0
  170. package/src/displayMany.ts +104 -0
  171. package/src/helpers/buildIdentifiedFixesMap.test.ts +33 -0
  172. package/src/helpers/buildPnpmPackagesMap.test.ts +119 -0
  173. package/src/helpers/buildPnpmPackagesMap.ts +53 -0
  174. package/src/helpers/collectDependentRanges.test.ts +311 -0
  175. package/src/helpers/collectDependentRanges.ts +185 -0
  176. package/src/helpers/collectPnpmDependents.test.ts +112 -0
  177. package/src/helpers/collectPnpmDependents.ts +79 -0
  178. package/src/helpers/duplicateSnapshot.ts +18 -0
  179. package/src/helpers/parsePnpmLockPackages.test.ts +93 -0
  180. package/src/helpers/parsePnpmLockPackages.ts +99 -0
  181. package/src/helpers/pnpmProtocol.ts +12 -0
  182. package/src/helpers/pnpmVersion.ts +28 -0
  183. package/src/helpers/pnpmWorkspaceYaml.test.ts +140 -0
  184. package/src/helpers/pnpmWorkspaceYaml.ts +77 -0
  185. package/src/helpers/projectDir.ts +14 -0
  186. package/src/helpers/readInstalledManifest.test.ts +124 -0
  187. package/src/helpers/readInstalledManifest.ts +202 -0
  188. package/src/helpers/runPnpm.ts +34 -0
  189. package/src/helpers/tempProjects.ts +94 -0
  190. package/src/helpers/toLockstepGraph.test.ts +98 -0
  191. package/src/helpers/toLockstepGraph.ts +52 -0
  192. package/src/helpers/versionsSnapshot.ts +9 -0
  193. package/src/identifyClusterFixes.test.ts +430 -0
  194. package/src/identifyClusterFixes.ts +114 -0
  195. package/src/identifyResolutionFixes.test.ts +82 -0
  196. package/src/index.ts +159 -0
  197. package/src/pnpmLockTypes.ts +44 -0
  198. package/src/readPnpmLock.ts +7 -0
@@ -0,0 +1,208 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { ok } from "node:assert/strict";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { ClusterFix } from "pm-dedup-core";
5
+ import { displayMany } from "./displayMany.ts";
6
+ import {
7
+ buildPnpmPackagesMap,
8
+ filterDuplicatesPnpmPackagesMap,
9
+ } from "./helpers/buildPnpmPackagesMap.ts";
10
+ import { collectDependentRanges } from "./helpers/collectDependentRanges.ts";
11
+ import { parsePnpmLockPackages } from "./helpers/parsePnpmLockPackages.ts";
12
+ import type { ManifestReader } from "./helpers/readInstalledManifest.ts";
13
+ import { createManifestReader } from "./helpers/readInstalledManifest.ts";
14
+ import { identifyClusterFixes } from "./identifyClusterFixes.ts";
15
+ import { readPnpmLock } from "./readPnpmLock.ts";
16
+
17
+ const fixturesBase = (rel: string) =>
18
+ fileURLToPath(new URL(rel, import.meta.url));
19
+
20
+ // The report layout is covered in pm-dedup-core (`renderDuplicatesReport`); these
21
+ // only check that the pnpm lockfile model is mapped onto it correctly.
22
+ const renderDuplicates = (
23
+ scenario: string,
24
+ clusterFixes?: ClusterFix[],
25
+ readManifest: ManifestReader = () => undefined,
26
+ ): string => {
27
+ const lock = readPnpmLock(
28
+ fixturesBase(`../test/fixtures/${scenario}/pnpm-lock.yaml`),
29
+ );
30
+ const packagesMap = buildPnpmPackagesMap(parsePnpmLockPackages(lock));
31
+ const duplicates = filterDuplicatesPnpmPackagesMap(packagesMap);
32
+
33
+ let buffer = "";
34
+ displayMany({
35
+ title: "duplicates",
36
+ duplicatesPackagesMap: duplicates,
37
+ dependents: collectDependentRanges(
38
+ lock,
39
+ new Set(Object.keys(duplicates)),
40
+ readManifest,
41
+ ),
42
+ totalDependencies: Object.keys(packagesMap).length,
43
+ clusterFixes,
44
+ details: true,
45
+ color: false,
46
+ log: (message = "") => {
47
+ buffer += `${message}\n`;
48
+ },
49
+ });
50
+ return buffer;
51
+ };
52
+
53
+ // `exact-pin-forces-downgrade` commits its virtual store, so the report runs on
54
+ // the real declared ranges: an exact 3.0.3 against expo-camera's `^3.0.0`.
55
+ const downgradeScenario = "exact-pin-forces-downgrade";
56
+ const downgradeDir = fixturesBase(`../test/fixtures/${downgradeScenario}`);
57
+
58
+ const renderDowngrade = (): string => {
59
+ const lock = readPnpmLock(`${downgradeDir}/pnpm-lock.yaml`);
60
+ const readManifest = createManifestReader(downgradeDir);
61
+ return renderDuplicates(
62
+ downgradeScenario,
63
+ identifyClusterFixes(
64
+ lock,
65
+ buildPnpmPackagesMap(parsePnpmLockPackages(lock)),
66
+ readManifest,
67
+ ),
68
+ readManifest,
69
+ );
70
+ };
71
+
72
+ const lastLine = (output: string): string =>
73
+ output.trimEnd().split("\n").at(-1)!;
74
+
75
+ const clusterFix = (overrides: Partial<ClusterFix> = {}): ClusterFix => ({
76
+ members: ["metro", "metro-config"],
77
+ duplicatedMembers: ["metro", "metro-config"],
78
+ memberVersions: {
79
+ metro: { versions: ["0.84.5", "0.87.0"], nonNpmCount: 0 },
80
+ "metro-config": { versions: ["0.84.5", "0.87.0"], nonNpmCount: 0 },
81
+ },
82
+ target: "0.87.0",
83
+ direction: "up",
84
+ convergentMembers: ["metro", "metro-config"],
85
+ driverMembers: ["metro"],
86
+ excludedMembers: [],
87
+ anchor: null,
88
+ reuseFixes: [],
89
+ floatingMembers: [],
90
+ workspaceChanges: [],
91
+ reResolutionSet: ["metro"],
92
+ externalConstraints: [],
93
+ needsRoundTrip: true,
94
+ applicable: true,
95
+ ...overrides,
96
+ });
97
+
98
+ const singletonFix = (overrides: Partial<ClusterFix> = {}): ClusterFix => ({
99
+ ...clusterFix(),
100
+ members: ["@babel/code-frame"],
101
+ duplicatedMembers: ["@babel/code-frame"],
102
+ memberVersions: {
103
+ "@babel/code-frame": { versions: ["7.26.2", "7.29.7"], nonNpmCount: 0 },
104
+ },
105
+ target: "7.29.7",
106
+ direction: "up",
107
+ convergentMembers: ["@babel/code-frame"],
108
+ driverMembers: ["@babel/code-frame"],
109
+ reResolutionSet: [],
110
+ needsRoundTrip: false,
111
+ ...overrides,
112
+ });
113
+
114
+ describe("displayMany", () => {
115
+ it("groups every dependent under the version it resolved to", () => {
116
+ const output = renderDuplicates("duplicated-babel-frame");
117
+ ok(output.startsWith("Found "));
118
+ ok(output.includes("@babel/code-frame — 2 versions"));
119
+ ok(output.includes('package.json in dependencies requires "7.26.2"'));
120
+ // pnpm defers the per-package merge to `pnpm dedupe`
121
+ ok(!output.includes("can be deduped"));
122
+ });
123
+
124
+ it("renders the printable-shell-command duplicate", () => {
125
+ const output = renderDuplicates("duplicated-printable-shell-command");
126
+ ok(output.includes("printable-shell-command — 2 versions"));
127
+ ok(output.includes(" 5.0.7"));
128
+ });
129
+
130
+ it("reports no duplicates for a clean lockfile", () => {
131
+ expect(renderDuplicates("simple")).toBe(
132
+ "No duplicates found\n\nFound 1 dependency, 0 duplicates, 0 dedupable.\n",
133
+ );
134
+ });
135
+
136
+ it("maps a cluster of one to the versions that would collapse", () => {
137
+ const output = renderDuplicates("duplicated-babel-frame", [singletonFix()]);
138
+ ok(
139
+ output.includes(
140
+ "@babel/code-frame — 2 versions, can be deduped to 7.29.7 (upgrade)",
141
+ ),
142
+ );
143
+ ok(output.includes(" 7.26.2 can be deduped to 7.29.7 (upgrade)"));
144
+ // a lone package is no family, so it gets no cluster section
145
+ ok(!output.includes("Lockstep clusters:"));
146
+ });
147
+
148
+ it("counts the whole lockfile and names the pnpm command", () => {
149
+ const output = renderDuplicates("duplicated-babel-frame", [singletonFix()]);
150
+ ok(
151
+ lastLine(output).startsWith(
152
+ "Found 39 dependencies, 1 duplicate, at least 1 dedupable (deduping may remove more). Run `pnpm-dedupe`",
153
+ ),
154
+ );
155
+ });
156
+
157
+ it("says nothing collapses when the cluster of one is unfixable", () => {
158
+ const output = renderDuplicates("duplicated-babel-frame", [
159
+ singletonFix({
160
+ target: null,
161
+ direction: "none",
162
+ convergentMembers: [],
163
+ driverMembers: [],
164
+ applicable: false,
165
+ }),
166
+ ]);
167
+ ok(!output.includes("can be deduped"));
168
+ ok(lastLine(output).includes("0 dedupable"));
169
+ });
170
+
171
+ it("files a declared range under the version it resolved to", () => {
172
+ const output = renderDowngrade();
173
+ ok(
174
+ output.includes(
175
+ ' - expo-camera@57.0.3 requires "^3.0.0"',
176
+ ),
177
+ );
178
+ ok(
179
+ output.includes(' - @yudiel/react-qr-scanner@2.3.1 requires "3.0.3"'),
180
+ );
181
+ });
182
+
183
+ it("names the downgrade a converging dependent has to take", () => {
184
+ const output = renderDowngrade();
185
+ ok(output.includes(" 3.2.2 can be deduped to 3.0.3 (downgrade)"));
186
+ // zxing-wasm cannot converge: `^2.1.2` against an exact 3.1.3
187
+ ok(lastLine(output).includes("2 duplicates, at least 1 dedupable"));
188
+ });
189
+
190
+ it("forwards the cluster fixes", () => {
191
+ const output = renderDuplicates("duplicated-babel-frame", [clusterFix()]);
192
+ ok(output.includes("Lockstep clusters:"));
193
+ ok(
194
+ output.includes(
195
+ "cluster 1 — metro* [2 packages, 2 duplicated, 2 fixable]:",
196
+ ),
197
+ );
198
+ // the family's own members are not in this lockfile's duplicates, so the
199
+ // count — which follows the listed packages — stays at zero
200
+ ok(lastLine(output).includes("0 dedupable"));
201
+ });
202
+
203
+ it("renders a cluster from a lockfile with no per-package duplicate", () => {
204
+ const output = renderDuplicates("simple", [clusterFix()]);
205
+ ok(output.startsWith("No duplicates found"));
206
+ ok(output.includes("Lockstep clusters:"));
207
+ });
208
+ });
@@ -0,0 +1,104 @@
1
+ import { renderDuplicatesReport } from "pm-dedup-core";
2
+ import type {
3
+ ClusterFix,
4
+ DuplicateDedupeView,
5
+ DuplicatePackageView,
6
+ DuplicatesReportTitle,
7
+ } from "pm-dedup-core";
8
+ import type { PackagesMap } from "./helpers/buildPnpmPackagesMap.ts";
9
+ import type { DependentRangesMap } from "./helpers/collectDependentRanges.ts";
10
+
11
+ export interface DisplayManyOptions {
12
+ title: DuplicatesReportTitle;
13
+ // headline for a report whose packages are not duplicates
14
+ notice?: string;
15
+ duplicatesPackagesMap: PackagesMap;
16
+ // the declared ranges, not the lockfile's resolved versions: the report has
17
+ // to show what every requester asked for, which is what the fixes reason on
18
+ dependents: DependentRangesMap;
19
+ totalDependencies: number;
20
+ clusterFixes?: ClusterFix[];
21
+ details?: boolean;
22
+ color?: boolean;
23
+ log?: (message?: string) => void;
24
+ }
25
+
26
+ // A cluster of one is the package's own fix: nothing else converges with it, so
27
+ // it is rendered in the package's block instead of the cluster section.
28
+ const isSingleton = (fix: ClusterFix): boolean => fix.members.length === 1;
29
+
30
+ const toDedupeViews = (
31
+ packageName: string,
32
+ clusterFixes: ClusterFix[],
33
+ ): DuplicateDedupeView[] =>
34
+ clusterFixes
35
+ .filter(
36
+ (fix) =>
37
+ isSingleton(fix) &&
38
+ fix.members[0] === packageName &&
39
+ fix.applicable &&
40
+ fix.target !== null,
41
+ )
42
+ .map((fix) => ({
43
+ // the target is what the copies merge into; the other installed versions
44
+ // are what goes away
45
+ from: (fix.memberVersions[packageName]?.versions ?? []).filter(
46
+ (version) => version !== fix.target,
47
+ ),
48
+ to: fix.target!,
49
+ direction: fix.direction,
50
+ }))
51
+ .filter((view) => view.from.length > 0);
52
+
53
+ const toPackageViews = ({
54
+ duplicatesPackagesMap,
55
+ dependents,
56
+ clusterFixes = [],
57
+ }: DisplayManyOptions): DuplicatePackageView[] =>
58
+ Object.entries(duplicatesPackagesMap).map(([packageName, resolutions]) => {
59
+ if (!resolutions) {
60
+ throw new Error(
61
+ `Unexpected error: no resolutions found for package ${packageName}`,
62
+ );
63
+ }
64
+
65
+ return {
66
+ packageName,
67
+ resolutions: resolutions.map(
68
+ ({ resolution, package: pnpmPackage, installations }) => ({
69
+ resolution,
70
+ version: pnpmPackage.type === "npm" ? pnpmPackage.version : undefined,
71
+ installations,
72
+ }),
73
+ ),
74
+ dependents: (dependents.get(packageName) ?? []).map((dependent) => ({
75
+ // the range alone does not say which declaration it comes from when the
76
+ // requester reaches the package through a key of another name
77
+ requester:
78
+ dependent.aliasKey === undefined
79
+ ? dependent.key
80
+ : `${dependent.key} (as "${dependent.aliasKey}")`,
81
+ range: dependent.range,
82
+ resolvedVersion: dependent.resolvedVersion,
83
+ peer: dependent.peer,
84
+ })),
85
+ dedupe: toDedupeViews(packageName, clusterFixes),
86
+ };
87
+ });
88
+
89
+ export const displayMany = (options: DisplayManyOptions): void => {
90
+ renderDuplicatesReport({
91
+ title: options.title,
92
+ notice: options.notice,
93
+ packages: toPackageViews(options),
94
+ totalDependencies: options.totalDependencies,
95
+ clusterFixes: (options.clusterFixes ?? []).filter(
96
+ (fix) => !isSingleton(fix),
97
+ ),
98
+ dedupeCommand: "pnpm-dedupe",
99
+ whyCommand: "pnpm-why-duplicate",
100
+ details: options.details,
101
+ color: options.color,
102
+ log: options.log,
103
+ });
104
+ };
@@ -0,0 +1,33 @@
1
+ import { describe, it } from "bun:test";
2
+ import { ok, strictEqual } from "node:assert/strict";
3
+ import { buildIdentifiedFixesMap } from "pm-dedup-core";
4
+ import type { PackageResolution, PackagesMap } from "./buildPnpmPackagesMap.ts";
5
+ import type { DependentsMap } from "./collectPnpmDependents.ts";
6
+
7
+ describe("buildIdentifiedFixesMap", () => {
8
+ it("builds a map even when there are no fixes", () => {
9
+ const fakeResolution: PackageResolution = {
10
+ resolution: "pkg@1.0.0",
11
+ package: {
12
+ type: "npm",
13
+ name: "pkg",
14
+ resolution: "pkg@1.0.0",
15
+ version: "1.0.0",
16
+ },
17
+ installations: ["pkg@1.0.0"],
18
+ };
19
+
20
+ const duplicates: PackagesMap = {
21
+ pkg: [fakeResolution],
22
+ };
23
+
24
+ const dependents: DependentsMap = new Map();
25
+
26
+ const map = buildIdentifiedFixesMap(duplicates, dependents);
27
+
28
+ ok(map instanceof Map);
29
+ strictEqual(map.has("pkg"), true);
30
+ strictEqual(Array.isArray(map.get("pkg")), true);
31
+ strictEqual((map.get("pkg") || []).length, 0);
32
+ });
33
+ });
@@ -0,0 +1,119 @@
1
+ import { describe, it } from "bun:test";
2
+ import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
3
+ import { fileURLToPath } from "node:url";
4
+ import { readPnpmLock } from "../readPnpmLock.ts";
5
+ import {
6
+ buildPnpmPackagesMap,
7
+ filterDuplicatesPnpmPackagesMap,
8
+ } from "./buildPnpmPackagesMap.ts";
9
+ import { parsePnpmLockPackages } from "./parsePnpmLockPackages.ts";
10
+
11
+ const fixturesBase = (rel: string) =>
12
+ fileURLToPath(new URL(rel, import.meta.url));
13
+
14
+ const buildFor = (scenario: string) =>
15
+ buildPnpmPackagesMap(
16
+ parsePnpmLockPackages(
17
+ readPnpmLock(
18
+ fixturesBase(`../../test/fixtures/${scenario}/pnpm-lock.yaml`),
19
+ ),
20
+ ),
21
+ );
22
+
23
+ const versionsOf = (
24
+ packagesMap: ReturnType<typeof buildPnpmPackagesMap>,
25
+ name: string,
26
+ ): Set<string> =>
27
+ new Set(
28
+ (packagesMap[name] ?? []).map((resolution) => resolution.package.version),
29
+ );
30
+
31
+ describe("buildPnpmPackagesMap", () => {
32
+ it("reports no duplicates for the simple fixture", () => {
33
+ const packagesMap = buildFor("simple");
34
+ strictEqual(packagesMap.semver?.length, 1);
35
+ strictEqual(
36
+ Object.keys(filterDuplicatesPnpmPackagesMap(packagesMap)).length,
37
+ 0,
38
+ );
39
+ });
40
+
41
+ it("reports no duplicates for one-dependency-of-dependency", () => {
42
+ const packagesMap = buildFor("one-dependency-of-dependency");
43
+ strictEqual(
44
+ Object.keys(filterDuplicatesPnpmPackagesMap(packagesMap)).length,
45
+ 0,
46
+ );
47
+ });
48
+
49
+ it("detects the duplicated @babel/code-frame versions", () => {
50
+ const duplicates = filterDuplicatesPnpmPackagesMap(
51
+ buildFor("duplicated-babel-frame"),
52
+ );
53
+ const versions = versionsOf(duplicates, "@babel/code-frame");
54
+ strictEqual(versions.size, 2);
55
+ ok(versions.has("7.26.2"));
56
+ });
57
+
58
+ it("detects the duplicated printable-shell-command versions", () => {
59
+ const duplicates = filterDuplicatesPnpmPackagesMap(
60
+ buildFor("duplicated-printable-shell-command"),
61
+ );
62
+ ok(duplicates["printable-shell-command"]);
63
+ strictEqual(duplicates["printable-shell-command"].length, 2);
64
+ });
65
+
66
+ it("detects the metro family a non-reused wildcard duplicated", () => {
67
+ // `@tamagui/metro-plugin` requests metro-config/metro-transform-worker with
68
+ // `*`; pnpm resolved them to 0.87.0 instead of reusing the 0.84.5 pulled by
69
+ // the root `metro: 0.84.5` pin, so the whole family is installed twice.
70
+ const duplicates = filterDuplicatesPnpmPackagesMap(
71
+ buildFor("wildcard-not-reused"),
72
+ );
73
+ const family = Object.keys(duplicates).filter(
74
+ (name) => name === "metro" || name.startsWith("metro-"),
75
+ );
76
+ strictEqual(family.length, 14);
77
+ for (const name of family) {
78
+ deepStrictEqual(
79
+ versionsOf(duplicates, name),
80
+ new Set(["0.84.5", "0.87.0"]),
81
+ );
82
+ }
83
+ });
84
+
85
+ it("detects at least the known @typescript-eslint duplicates", () => {
86
+ const duplicates = filterDuplicatesPnpmPackagesMap(
87
+ buildFor("duplicated-typescript-eslint"),
88
+ );
89
+ // Acceptance floor: the set of detected duplicates must include these.
90
+ const required = [
91
+ "@typescript-eslint/types",
92
+ "@typescript-eslint/scope-manager",
93
+ "@typescript-eslint/utils",
94
+ "@eslint/plugin-kit",
95
+ ];
96
+ for (const name of required) {
97
+ ok(duplicates[name], `expected ${name} to be detected as duplicate`);
98
+ strictEqual(duplicates[name].length, 2);
99
+ }
100
+ const typesVersions = versionsOf(duplicates, "@typescript-eslint/types");
101
+ ok(typesVersions.has("8.59.1"));
102
+ ok(typesVersions.has("8.61.0"));
103
+ });
104
+
105
+ // `aliased-swapped-names`: identity comes from the package id, so the two real
106
+ // `typescript` copies meet and the `@typescript/typescript6` the `typescript`
107
+ // key points at stays on its own.
108
+ it("identifies packages by their id when alias keys are swapped", () => {
109
+ const duplicates = filterDuplicatesPnpmPackagesMap(
110
+ buildFor("aliased-swapped-names"),
111
+ );
112
+
113
+ deepStrictEqual(Object.keys(duplicates), ["typescript"]);
114
+ deepStrictEqual(
115
+ versionsOf(duplicates, "typescript"),
116
+ new Set(["5.9.3", "7.0.2"]),
117
+ );
118
+ });
119
+ });
@@ -0,0 +1,53 @@
1
+ import type {
2
+ NpmPackage,
3
+ Package,
4
+ ParsedPnpmPackages,
5
+ } from "./parsePnpmLockPackages.ts";
6
+
7
+ export interface PackageResolution {
8
+ resolution: string;
9
+ package: Package;
10
+ installations: string[];
11
+ }
12
+
13
+ export interface NpmPackageResolution extends PackageResolution {
14
+ package: NpmPackage;
15
+ }
16
+
17
+ export type PackagesMap = Record<string, PackageResolution[]>;
18
+
19
+ // Several packages can share the same name but different versions (duplicates).
20
+ // When a single resolution is installed in several peer contexts, every context
21
+ // is listed in `installations`.
22
+ export function buildPnpmPackagesMap({
23
+ packages,
24
+ installationsByResolution,
25
+ }: ParsedPnpmPackages): PackagesMap {
26
+ const packagesMap: PackagesMap = {};
27
+
28
+ for (const pkg of packages.values()) {
29
+ if (!packagesMap[pkg.name]) {
30
+ packagesMap[pkg.name] = [];
31
+ }
32
+
33
+ packagesMap[pkg.name]!.push({
34
+ resolution: pkg.resolution,
35
+ package: pkg,
36
+ installations: installationsByResolution.get(pkg.resolution) ?? [
37
+ pkg.resolution,
38
+ ],
39
+ });
40
+ }
41
+
42
+ return packagesMap;
43
+ }
44
+
45
+ export function filterDuplicatesPnpmPackagesMap(
46
+ packagesMap: PackagesMap,
47
+ ): PackagesMap {
48
+ return Object.fromEntries(
49
+ Object.entries(packagesMap).filter(
50
+ ([, resolutions]) => resolutions.length > 1,
51
+ ),
52
+ );
53
+ }