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,560 @@
1
+ import { afterEach, describe, it } from "bun:test";
2
+ import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
3
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import type { ClusterFix, DuplicateSnapshot } from "pm-dedup-core";
7
+ import { applyClusterFixes } from "./applyClusterFixes.ts";
8
+ import { createTempProjects } from "./helpers/tempProjects.ts";
9
+
10
+ const fix = (overrides: Partial<ClusterFix>): ClusterFix => ({
11
+ members: [],
12
+ duplicatedMembers: [],
13
+ memberVersions: {},
14
+ target: null,
15
+ direction: "none",
16
+ convergentMembers: [],
17
+ driverMembers: [],
18
+ excludedMembers: [],
19
+ anchor: null,
20
+ reuseFixes: [],
21
+ floatingMembers: [],
22
+ workspaceChanges: [],
23
+ reResolutionSet: [],
24
+ externalConstraints: [],
25
+ needsRoundTrip: false,
26
+ applicable: false,
27
+ ...overrides,
28
+ });
29
+
30
+ const manifestContent = [
31
+ "{",
32
+ ' "name": "root",',
33
+ ' "devDependencies": {',
34
+ ' "metro": "0.84.5"',
35
+ " }",
36
+ "}",
37
+ "",
38
+ ].join("\n");
39
+
40
+ const workspaceYamlContent = "# keep me\nresolutionMode: time-based\n";
41
+
42
+ const projects = createTempProjects("pnpm-dedup-apply-");
43
+
44
+ const makeProject = (files: Record<string, string>): string => {
45
+ const dir = projects.create();
46
+ for (const [name, content] of Object.entries(files)) {
47
+ writeFileSync(join(dir, name), content);
48
+ }
49
+ return dir;
50
+ };
51
+
52
+ afterEach(projects.cleanup);
53
+
54
+ const read = (dir: string, name: string): string =>
55
+ readFileSync(join(dir, name), "utf8");
56
+
57
+ const snapshot = (...resolutions: string[]): DuplicateSnapshot =>
58
+ new Set(resolutions);
59
+
60
+ describe("applyClusterFixes", () => {
61
+ const metroFix = fix({
62
+ applicable: true,
63
+ target: "0.87.0",
64
+ convergentMembers: ["metro-config"],
65
+ workspaceChanges: [
66
+ {
67
+ requester: "package.json in devDependencies",
68
+ requesterName: undefined,
69
+ packageName: "metro",
70
+ range: "0.84.5",
71
+ to: "0.87.0",
72
+ workspace: { path: ".", depType: "devDependencies" },
73
+ },
74
+ ],
75
+ });
76
+
77
+ it("keeps the workspace edit and never writes an override when it is enough", () => {
78
+ const dir = makeProject({
79
+ "package.json": manifestContent,
80
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
81
+ "pnpm-workspace.yaml": workspaceYamlContent,
82
+ });
83
+
84
+ let duplicates = snapshot("metro-config@0.84.5", "metro-config@0.87.0");
85
+ const outcome = applyClusterFixes({
86
+ projectDir: dir,
87
+ color: false,
88
+ log: () => undefined,
89
+ pnpmVersion: () => "11.17.0",
90
+ readFixes: () => [metroFix],
91
+ readDuplicates: () => duplicates,
92
+ resolve: () => {
93
+ // pnpm's part: with the pin widened, the 0.84.5 subtree has no reason
94
+ // to exist any more
95
+ if (read(dir, "package.json").includes('"metro": "0.87.0"')) {
96
+ duplicates = snapshot();
97
+ }
98
+ return 0;
99
+ },
100
+ });
101
+
102
+ strictEqual(outcome.status, "applied");
103
+ strictEqual(outcome.after.size, 0);
104
+ ok(read(dir, "package.json").includes('"metro": "0.87.0"'));
105
+ strictEqual(read(dir, "pnpm-workspace.yaml"), workspaceYamlContent);
106
+ });
107
+
108
+ const leafFix = fix({
109
+ applicable: true,
110
+ target: "2.0.0",
111
+ convergentMembers: ["leaf"],
112
+ });
113
+
114
+ it("removes the overrides again once pnpm holds the result on its own", () => {
115
+ const dir = makeProject({
116
+ "package.json": manifestContent,
117
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
118
+ "pnpm-workspace.yaml": workspaceYamlContent,
119
+ });
120
+
121
+ let duplicates = snapshot("leaf@1.0.0", "leaf@2.0.0");
122
+ let converged = false;
123
+ const logs: string[] = [];
124
+
125
+ const outcome = applyClusterFixes({
126
+ projectDir: dir,
127
+ color: false,
128
+ log: (message = "") => logs.push(message),
129
+ pnpmVersion: () => "11.13.0",
130
+ readFixes: () => [leafFix],
131
+ readDuplicates: () => duplicates,
132
+ resolve: () => {
133
+ if (read(dir, "pnpm-workspace.yaml").includes('"leaf@"')) {
134
+ converged = true;
135
+ }
136
+ // sticky: pnpm keeps a locked resolution that still satisfies the range
137
+ duplicates = converged ? snapshot() : duplicates;
138
+ return 0;
139
+ },
140
+ });
141
+
142
+ strictEqual(outcome.status, "applied");
143
+ deepStrictEqual(outcome.stickyOverrides, []);
144
+ strictEqual(read(dir, "pnpm-workspace.yaml"), workspaceYamlContent);
145
+ ok(logs.some((line) => line.includes("Removing the overrides")));
146
+ });
147
+
148
+ it("keeps the overrides, explaining why in the file and in the result", () => {
149
+ const dir = makeProject({
150
+ "package.json": manifestContent,
151
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
152
+ "pnpm-workspace.yaml": workspaceYamlContent,
153
+ });
154
+
155
+ const duplicated = snapshot("leaf@1.0.0", "leaf@2.0.0");
156
+ let duplicates = duplicated;
157
+ const logs: string[] = [];
158
+
159
+ const outcome = applyClusterFixes({
160
+ projectDir: dir,
161
+ color: false,
162
+ log: (message = "") => logs.push(message),
163
+ pnpmVersion: () => "11.17.0",
164
+ readFixes: () => [leafFix],
165
+ readDuplicates: () => duplicates,
166
+ resolve: () => {
167
+ // the duplicate only stays away while the override is there
168
+ duplicates = read(dir, "pnpm-workspace.yaml").includes('"leaf@"')
169
+ ? snapshot()
170
+ : duplicated;
171
+ return 0;
172
+ },
173
+ });
174
+
175
+ strictEqual(outcome.status, "kept-overrides");
176
+ deepStrictEqual(
177
+ outcome.stickyOverrides.map((override) => override.packageName),
178
+ ["leaf"],
179
+ );
180
+
181
+ const workspaceYaml = read(dir, "pnpm-workspace.yaml");
182
+ ok(workspaceYaml.includes('"leaf@": "2.0.0"'));
183
+ ok(workspaceYaml.includes("# Added by pnpm-dedup."));
184
+ ok(
185
+ workspaceYaml.includes(
186
+ "# handled without a standing override: https://github.com",
187
+ ),
188
+ );
189
+ // the user's own content survives the write
190
+ ok(workspaceYaml.includes("# keep me"));
191
+ ok(workspaceYaml.includes("resolutionMode: time-based"));
192
+
193
+ ok(logs.some((line) => line.includes("github.com/christophehurpeau")));
194
+ ok(logs.some((line) => line.includes("Kept in")));
195
+ });
196
+
197
+ it("writes plain overrides and skips the version gate when convergence is disabled", () => {
198
+ const dir = makeProject({
199
+ "package.json": manifestContent,
200
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
201
+ "pnpm-workspace.yaml": workspaceYamlContent,
202
+ });
203
+
204
+ let duplicates = snapshot("leaf@1.0.0", "leaf@2.0.0");
205
+ let plainKeyWritten = false;
206
+
207
+ const outcome = applyClusterFixes({
208
+ projectDir: dir,
209
+ color: false,
210
+ log: () => undefined,
211
+ convergenceOverrides: false,
212
+ // a pnpm too old for convergence overrides still writes plain ones
213
+ pnpmVersion: () => "11.12.0",
214
+ readFixes: () => [leafFix],
215
+ readDuplicates: () => duplicates,
216
+ resolve: () => {
217
+ if (read(dir, "pnpm-workspace.yaml").includes('"leaf": "2.0.0"')) {
218
+ plainKeyWritten = true;
219
+ duplicates = snapshot();
220
+ }
221
+ return 0;
222
+ },
223
+ });
224
+
225
+ strictEqual(outcome.status, "applied");
226
+ ok(plainKeyWritten);
227
+ strictEqual(read(dir, "pnpm-workspace.yaml"), workspaceYamlContent);
228
+ });
229
+
230
+ // a plain override has no range condition, so one the detector proposed from
231
+ // a single requester's range cannot be written when another rejects it
232
+ it("drops a plain override a third-party range rejects", () => {
233
+ const dir = makeProject({
234
+ "package.json": manifestContent,
235
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
236
+ });
237
+ const logs: string[] = [];
238
+
239
+ const outcome = applyClusterFixes({
240
+ projectDir: dir,
241
+ color: false,
242
+ log: (message = "") => logs.push(message),
243
+ convergenceOverrides: false,
244
+ pnpmVersion: () => "11.17.0",
245
+ readFixes: () => [
246
+ fix({
247
+ anchor: "0.84.5",
248
+ reuseFixes: [
249
+ {
250
+ requester: "@tamagui/metro-plugin@1.0.0",
251
+ requesterName: "@tamagui/metro-plugin",
252
+ packageName: "metro-config",
253
+ range: "*",
254
+ from: "0.87.0",
255
+ to: "0.84.5",
256
+ },
257
+ ],
258
+ externalConstraints: [
259
+ {
260
+ requester: "@react-native/community-cli-plugin@0.87.0",
261
+ requesterName: "@react-native/community-cli-plugin",
262
+ packageName: "metro-config",
263
+ range: "^0.87.0",
264
+ },
265
+ ],
266
+ }),
267
+ ],
268
+ readDuplicates: () => snapshot("metro-config@0.84.5"),
269
+ resolve: () => {
270
+ throw new Error("a dropped override must not resolve");
271
+ },
272
+ });
273
+
274
+ strictEqual(outcome.status, "nothing-to-do");
275
+ ok(logs.some((line) => line.includes("Skipped override")));
276
+ });
277
+
278
+ it("reverts everything when the re-resolution fails", () => {
279
+ const dir = makeProject({
280
+ "package.json": manifestContent,
281
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
282
+ });
283
+
284
+ const outcome = applyClusterFixes({
285
+ projectDir: dir,
286
+ color: false,
287
+ log: () => undefined,
288
+ pnpmVersion: () => "11.17.0",
289
+ readFixes: () => [metroFix],
290
+ readDuplicates: () =>
291
+ snapshot("metro-config@0.84.5", "metro-config@0.87.0"),
292
+ resolve: () => 1,
293
+ });
294
+
295
+ strictEqual(outcome.status, "reverted");
296
+ strictEqual(read(dir, "package.json"), manifestContent);
297
+ strictEqual(existsSync(join(dir, "pnpm-workspace.yaml")), false);
298
+ });
299
+
300
+ const metroFamilyFix = fix({
301
+ ...metroFix,
302
+ members: ["metro", "metro-config"],
303
+ duplicatedMembers: ["metro-config"],
304
+ });
305
+
306
+ it("skips a cluster the filter only selects part of", () => {
307
+ const dir = makeProject({
308
+ "package.json": manifestContent,
309
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
310
+ });
311
+ const logs: string[] = [];
312
+
313
+ const outcome = applyClusterFixes({
314
+ projectDir: dir,
315
+ color: false,
316
+ dryRun: true,
317
+ log: (message = "") => logs.push(message),
318
+ pnpmVersion: () => "11.17.0",
319
+ filter: { include: ["metro-config"] },
320
+ readFixes: () => [metroFamilyFix],
321
+ readDuplicates: () => snapshot("metro-config@0.84.5"),
322
+ resolve: () => {
323
+ throw new Error("a dry run must not resolve");
324
+ },
325
+ });
326
+
327
+ strictEqual(outcome.plannedChangeCount, 0);
328
+ strictEqual(read(dir, "package.json"), manifestContent);
329
+ ok(logs.some((line) => line.includes("metro not selected by the filter")));
330
+ });
331
+
332
+ it("keeps a cluster the filter selects whole", () => {
333
+ const dir = makeProject({
334
+ "package.json": manifestContent,
335
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
336
+ });
337
+
338
+ const outcome = applyClusterFixes({
339
+ projectDir: dir,
340
+ color: false,
341
+ dryRun: true,
342
+ log: () => undefined,
343
+ pnpmVersion: () => "11.17.0",
344
+ filter: { include: ["metro*"] },
345
+ readFixes: () => [metroFamilyFix],
346
+ readDuplicates: () => snapshot("metro-config@0.84.5"),
347
+ resolve: () => {
348
+ throw new Error("a dry run must not resolve");
349
+ },
350
+ });
351
+
352
+ ok(outcome.plannedChangeCount > 0);
353
+ });
354
+
355
+ it("writes nothing on a dry run", () => {
356
+ const dir = makeProject({
357
+ "package.json": manifestContent,
358
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
359
+ });
360
+ const logs: string[] = [];
361
+
362
+ const outcome = applyClusterFixes({
363
+ projectDir: dir,
364
+ color: false,
365
+ dryRun: true,
366
+ log: (message = "") => logs.push(message),
367
+ pnpmVersion: () => "11.17.0",
368
+ readFixes: () => [metroFix],
369
+ readDuplicates: () => snapshot("metro-config@0.84.5"),
370
+ resolve: () => {
371
+ throw new Error("a dry run must not resolve");
372
+ },
373
+ });
374
+
375
+ strictEqual(outcome.status, "dry-run");
376
+ strictEqual(read(dir, "package.json"), manifestContent);
377
+ ok(logs.some((line) => line.includes('"0.84.5" -> "0.87.0"')));
378
+ ok(logs.some((line) => line.startsWith("Would apply:")));
379
+ ok(outcome.plannedChangeCount > 0);
380
+ });
381
+
382
+ it("renders repo-relative paths and the transient override file", () => {
383
+ const dir = makeProject({
384
+ "package.json": manifestContent,
385
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
386
+ });
387
+ const logs: string[] = [];
388
+
389
+ applyClusterFixes({
390
+ projectDir: dir,
391
+ color: false,
392
+ dryRun: true,
393
+ log: (message = "") => logs.push(message),
394
+ pnpmVersion: () => "11.17.0",
395
+ readFixes: () => [metroFix],
396
+ readDuplicates: () => snapshot("metro-config@0.84.5"),
397
+ resolve: () => {
398
+ throw new Error("a dry run must not resolve");
399
+ },
400
+ });
401
+
402
+ const output = logs.join("\n");
403
+ ok(output.includes("package.json:"));
404
+ // never the absolute temp path
405
+ ok(!output.includes(dir));
406
+ ok(
407
+ output.includes(
408
+ "pnpm-workspace.yaml (transient, removed once the result is verified):",
409
+ ),
410
+ );
411
+ ok(output.includes("Run `pnpm-dedupe` to apply."));
412
+ });
413
+
414
+ it("names what `pnpm dedupe` itself would still change", () => {
415
+ const dir = makeProject({
416
+ "package.json": manifestContent,
417
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
418
+ });
419
+ const logs: string[] = [];
420
+
421
+ applyClusterFixes({
422
+ projectDir: dir,
423
+ color: false,
424
+ dryRun: true,
425
+ packageManagerResiduals: "`pnpm dedupe` would also change the lockfile.",
426
+ log: (message = "") => logs.push(message),
427
+ pnpmVersion: () => "11.17.0",
428
+ readFixes: () => [metroFix],
429
+ readDuplicates: () => snapshot("metro-config@0.84.5"),
430
+ resolve: () => {
431
+ throw new Error("a dry run must not resolve");
432
+ },
433
+ });
434
+
435
+ ok(logs.includes("`pnpm dedupe` would also change the lockfile."));
436
+ });
437
+
438
+ it("reports nothing to dedupe on a dry run with an empty plan", () => {
439
+ const dir = makeProject({
440
+ "package.json": manifestContent,
441
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
442
+ });
443
+ const logs: string[] = [];
444
+
445
+ const outcome = applyClusterFixes({
446
+ projectDir: dir,
447
+ color: false,
448
+ dryRun: true,
449
+ log: (message = "") => logs.push(message),
450
+ pnpmVersion: () => "11.17.0",
451
+ readFixes: () => [],
452
+ readDuplicates: () => snapshot(),
453
+ resolve: () => {
454
+ throw new Error("a dry run must not resolve");
455
+ },
456
+ });
457
+
458
+ strictEqual(outcome.status, "dry-run");
459
+ strictEqual(outcome.plannedChangeCount, 0);
460
+ ok(logs.includes("Nothing to dedupe."));
461
+ });
462
+
463
+ it("plans nothing on a dry run when pnpm cannot apply it", () => {
464
+ const dir = makeProject({
465
+ "package.json": manifestContent,
466
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
467
+ });
468
+ const logs: string[] = [];
469
+
470
+ const outcome = applyClusterFixes({
471
+ projectDir: dir,
472
+ color: false,
473
+ dryRun: true,
474
+ log: (message = "") => logs.push(message),
475
+ pnpmVersion: () => "9.0.0",
476
+ readFixes: () => [metroFix],
477
+ readDuplicates: () => snapshot("metro-config@0.84.5"),
478
+ resolve: () => {
479
+ throw new Error("a dry run must not resolve");
480
+ },
481
+ });
482
+
483
+ // nothing is applicable, so `--check` has nothing to fail on
484
+ strictEqual(outcome.plannedChangeCount, 0);
485
+ ok(logs.some((line) => line.includes("Cluster fixes need pnpm >=")));
486
+ ok(logs.includes("Nothing to dedupe."));
487
+ });
488
+
489
+ it("does nothing on a pnpm without convergence overrides", () => {
490
+ const dir = makeProject({
491
+ "package.json": manifestContent,
492
+ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
493
+ });
494
+ const logs: string[] = [];
495
+
496
+ const outcome = applyClusterFixes({
497
+ projectDir: dir,
498
+ color: false,
499
+ log: (message = "") => logs.push(message),
500
+ pnpmVersion: () => "11.12.0",
501
+ readFixes: () => [metroFix],
502
+ readDuplicates: () => snapshot("metro-config@0.84.5"),
503
+ resolve: () => {
504
+ throw new Error("an unsupported pnpm must not resolve");
505
+ },
506
+ });
507
+
508
+ strictEqual(outcome.status, "not-supported");
509
+ strictEqual(read(dir, "package.json"), manifestContent);
510
+ ok(logs.some((line) => line.includes("11.13.0")));
511
+ });
512
+
513
+ // End to end from the committed fixture: the detector, the plan and the file
514
+ // the override would land in, with nothing stubbed but the pnpm version.
515
+ it("plans a convergence override for a lone duplicate", () => {
516
+ const logs: string[] = [];
517
+
518
+ const outcome = applyClusterFixes({
519
+ projectDir: fileURLToPath(
520
+ new URL("../test/fixtures/exact-pin-forces-downgrade", import.meta.url),
521
+ ),
522
+ dryRun: true,
523
+ log: (message = "") => logs.push(message),
524
+ pnpmVersion: () => "11.17.0",
525
+ resolve: () => {
526
+ throw new Error("a dry run must not resolve");
527
+ },
528
+ });
529
+
530
+ strictEqual(outcome.status, "dry-run");
531
+ strictEqual(outcome.plannedChangeCount, 1);
532
+ ok(logs.some((line) => line.includes("pnpm-workspace.yaml")));
533
+ ok(
534
+ logs.some((line) =>
535
+ line.includes('"barcode-detector@": "3.0.3" (converge)'),
536
+ ),
537
+ );
538
+ // zxing-wasm has no covering version, so nothing is planned for it
539
+ ok(!logs.some((line) => line.includes("zxing-wasm")));
540
+ });
541
+
542
+ it("reports having nothing to do when no fix is applicable", () => {
543
+ const dir = makeProject({ "pnpm-lock.yaml": "lockfileVersion: '9.0'\n" });
544
+
545
+ strictEqual(
546
+ applyClusterFixes({
547
+ projectDir: dir,
548
+ color: false,
549
+ log: () => undefined,
550
+ pnpmVersion: () => "11.17.0",
551
+ readFixes: () => [fix({ applicable: false })],
552
+ readDuplicates: () => snapshot("leaf@1.0.0"),
553
+ resolve: () => {
554
+ throw new Error("nothing to apply must not resolve");
555
+ },
556
+ }).status,
557
+ "nothing-to-do",
558
+ );
559
+ });
560
+ });