pnpm-dedup 2.2.1 → 2.2.2
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.
- package/README.md +15 -5
- package/dist/applyClusterFixes.js +103 -80
- package/dist/applyClusterFixes.js.map +1 -1
- package/dist/applyClusterFixes.test.js +257 -29
- package/dist/applyClusterFixes.test.js.map +1 -1
- package/dist/bin/pnpm-why-duplicate.test.js +17 -8
- package/dist/bin/pnpm-why-duplicate.test.js.map +1 -1
- package/dist/dedupe.js +15 -4
- package/dist/dedupe.js.map +1 -1
- package/dist/dedupeCheckConsistency.test.js +53 -59
- package/dist/dedupeCheckConsistency.test.js.map +1 -1
- package/dist/dedupeEndToEnd.test.js +3 -3
- package/dist/dedupeEndToEnd.test.js.map +1 -1
- package/dist/dedupePeers.test.js +1 -1
- package/dist/dedupePeers.test.js.map +1 -1
- package/dist/definitions/applyClusterFixes.d.ts +6 -3
- package/dist/definitions/applyClusterFixes.d.ts.map +1 -1
- package/dist/definitions/dedupe.d.ts.map +1 -1
- package/dist/definitions/helpers/describeUnheldOverride.d.ts +10 -0
- package/dist/definitions/helpers/describeUnheldOverride.d.ts.map +1 -0
- package/dist/definitions/helpers/describeUnheldOverride.test.d.ts +2 -0
- package/dist/definitions/helpers/describeUnheldOverride.test.d.ts.map +1 -0
- package/dist/definitions/helpers/pnpmWorkspaceYaml.d.ts +1 -5
- package/dist/definitions/helpers/pnpmWorkspaceYaml.d.ts.map +1 -1
- package/dist/definitions/pnpmLockTypes.d.ts +1 -0
- package/dist/definitions/pnpmLockTypes.d.ts.map +1 -1
- package/dist/definitions/readPnpmLock.d.ts +7 -0
- package/dist/definitions/readPnpmLock.d.ts.map +1 -1
- package/dist/definitions/readPnpmLock.test.d.ts +2 -0
- package/dist/definitions/readPnpmLock.test.d.ts.map +1 -0
- package/dist/displayMany.test.js +3 -4
- package/dist/displayMany.test.js.map +1 -1
- package/dist/helpers/buildIdentifiedFixesMap.test.js +1 -1
- package/dist/helpers/buildIdentifiedFixesMap.test.js.map +1 -1
- package/dist/helpers/buildPnpmPackagesMap.test.js +1 -1
- package/dist/helpers/buildPnpmPackagesMap.test.js.map +1 -1
- package/dist/helpers/collectDependentRanges.test.js +1 -1
- package/dist/helpers/collectDependentRanges.test.js.map +1 -1
- package/dist/helpers/collectPnpmDependents.test.js +1 -1
- package/dist/helpers/collectPnpmDependents.test.js.map +1 -1
- package/dist/helpers/describeUnheldOverride.js +30 -0
- package/dist/helpers/describeUnheldOverride.js.map +1 -0
- package/dist/helpers/describeUnheldOverride.test.js +80 -0
- package/dist/helpers/describeUnheldOverride.test.js.map +1 -0
- package/dist/helpers/parsePnpmLockPackages.test.js +1 -1
- package/dist/helpers/parsePnpmLockPackages.test.js.map +1 -1
- package/dist/helpers/pnpmWorkspaceYaml.js +2 -17
- package/dist/helpers/pnpmWorkspaceYaml.js.map +1 -1
- package/dist/helpers/pnpmWorkspaceYaml.test.js +3 -5
- package/dist/helpers/pnpmWorkspaceYaml.test.js.map +1 -1
- package/dist/helpers/readInstalledManifest.test.js +1 -1
- package/dist/helpers/readInstalledManifest.test.js.map +1 -1
- package/dist/helpers/toLockstepGraph.test.js +1 -1
- package/dist/helpers/toLockstepGraph.test.js.map +1 -1
- package/dist/identifyClusterFixes.test.js +22 -41
- package/dist/identifyClusterFixes.test.js.map +1 -1
- package/dist/identifyResolutionFixes.test.js +8 -7
- package/dist/identifyResolutionFixes.test.js.map +1 -1
- package/dist/readPnpmLock.js +21 -2
- package/dist/readPnpmLock.js.map +1 -1
- package/dist/readPnpmLock.test.js +43 -0
- package/dist/readPnpmLock.test.js.map +1 -0
- package/package.json +4 -3
- package/src/applyClusterFixes.test.ts +398 -36
- package/src/applyClusterFixes.ts +178 -125
- package/src/bin/pnpm-why-duplicate.test.ts +31 -8
- package/src/dedupe.ts +22 -7
- package/src/dedupeCheckConsistency.test.ts +84 -100
- package/src/dedupeEndToEnd.test.ts +84 -77
- package/src/dedupePeers.test.ts +1 -1
- package/src/displayMany.test.ts +4 -4
- package/src/helpers/buildIdentifiedFixesMap.test.ts +1 -1
- package/src/helpers/buildPnpmPackagesMap.test.ts +1 -1
- package/src/helpers/collectDependentRanges.test.ts +1 -1
- package/src/helpers/collectPnpmDependents.test.ts +1 -1
- package/src/helpers/describeUnheldOverride.test.ts +100 -0
- package/src/helpers/describeUnheldOverride.ts +53 -0
- package/src/helpers/parsePnpmLockPackages.test.ts +1 -1
- package/src/helpers/pnpmWorkspaceYaml.test.ts +2 -5
- package/src/helpers/pnpmWorkspaceYaml.ts +5 -20
- package/src/helpers/readInstalledManifest.test.ts +1 -1
- package/src/helpers/toLockstepGraph.test.ts +1 -1
- package/src/identifyClusterFixes.test.ts +27 -58
- package/src/identifyResolutionFixes.test.ts +8 -7
- package/src/pnpmLockTypes.ts +1 -0
- package/src/readPnpmLock.test.ts +65 -0
- package/src/readPnpmLock.ts +26 -2
package/src/applyClusterFixes.ts
CHANGED
|
@@ -6,10 +6,13 @@ import {
|
|
|
6
6
|
createPackageFilter,
|
|
7
7
|
describeSkippedClusterFix,
|
|
8
8
|
diffDuplicates,
|
|
9
|
+
heldOverrides,
|
|
10
|
+
outstandingOverrides,
|
|
9
11
|
partitionUnconditionalOverrides,
|
|
10
12
|
planClusterApply,
|
|
11
13
|
renderApplyPlan,
|
|
12
14
|
restoreFiles,
|
|
15
|
+
reuseKeys,
|
|
13
16
|
selectClusterFixes,
|
|
14
17
|
shouldColorize,
|
|
15
18
|
} from "pm-dedup-core";
|
|
@@ -18,12 +21,17 @@ import type {
|
|
|
18
21
|
ClusterFix,
|
|
19
22
|
DuplicateSnapshot,
|
|
20
23
|
FileSnapshot,
|
|
24
|
+
OverrideTargetState,
|
|
21
25
|
PackageFilterOptions,
|
|
22
26
|
PlannedManifestEdit,
|
|
23
27
|
PlannedOverride,
|
|
24
28
|
SelectedClusterFixes,
|
|
29
|
+
VersionsSnapshot,
|
|
25
30
|
} from "pm-dedup-core";
|
|
26
31
|
import { buildPnpmPackagesMap } from "./helpers/buildPnpmPackagesMap.ts";
|
|
32
|
+
import { collectDependentRanges } from "./helpers/collectDependentRanges.ts";
|
|
33
|
+
import type { DependentRangesMap } from "./helpers/collectDependentRanges.ts";
|
|
34
|
+
import { describeUnheldOverride } from "./helpers/describeUnheldOverride.ts";
|
|
27
35
|
import { readDuplicateSnapshot } from "./helpers/duplicateSnapshot.ts";
|
|
28
36
|
import { parsePnpmLockPackages } from "./helpers/parsePnpmLockPackages.ts";
|
|
29
37
|
import {
|
|
@@ -35,6 +43,7 @@ import { addOverrides, overrideKey } from "./helpers/pnpmWorkspaceYaml.ts";
|
|
|
35
43
|
import { lockPathOf } from "./helpers/projectDir.ts";
|
|
36
44
|
import { createManifestReader } from "./helpers/readInstalledManifest.ts";
|
|
37
45
|
import { runPnpm } from "./helpers/runPnpm.ts";
|
|
46
|
+
import { readVersionsSnapshot } from "./helpers/versionsSnapshot.ts";
|
|
38
47
|
import { identifyClusterFixes } from "./identifyClusterFixes.ts";
|
|
39
48
|
import { readPnpmLock } from "./readPnpmLock.ts";
|
|
40
49
|
|
|
@@ -43,7 +52,6 @@ const issuesUrl = "https://github.com/christophehurpeau/pm-tools/issues";
|
|
|
43
52
|
export type ClusterApplyStatus =
|
|
44
53
|
| "applied"
|
|
45
54
|
| "dry-run"
|
|
46
|
-
| "kept-overrides"
|
|
47
55
|
| "not-supported"
|
|
48
56
|
| "nothing-to-do"
|
|
49
57
|
| "reverted";
|
|
@@ -52,7 +60,7 @@ export interface ClusterApplyOutcome {
|
|
|
52
60
|
status: ClusterApplyStatus;
|
|
53
61
|
before: DuplicateSnapshot;
|
|
54
62
|
after: DuplicateSnapshot;
|
|
55
|
-
// overrides the
|
|
63
|
+
// overrides the result did not hold without: reported, never left behind
|
|
56
64
|
stickyOverrides: PlannedOverride[];
|
|
57
65
|
// how many edits the plan holds, so `--check` gates without re-planning
|
|
58
66
|
plannedChangeCount: number;
|
|
@@ -66,6 +74,11 @@ export interface ApplyClusterFixesOptions {
|
|
|
66
74
|
resolve?: () => number | null;
|
|
67
75
|
readFixes?: (projectDir: string) => ClusterFix[];
|
|
68
76
|
readDuplicates?: (lockPath: string) => DuplicateSnapshot;
|
|
77
|
+
readVersions?: (lockPath: string) => VersionsSnapshot;
|
|
78
|
+
readDependentRanges?: (
|
|
79
|
+
projectDir: string,
|
|
80
|
+
packageNames: Set<string>,
|
|
81
|
+
) => DependentRangesMap;
|
|
69
82
|
pnpmVersion?: () => string | null;
|
|
70
83
|
// false writes plain overrides instead, which pnpm applies to every requester
|
|
71
84
|
// whatever range it declares
|
|
@@ -99,36 +112,29 @@ const describeOverride = (
|
|
|
99
112
|
): string =>
|
|
100
113
|
`"${overrideKey(override.packageName, convergence)}": "${override.version}"`;
|
|
101
114
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"Removing these makes `pnpm dedupe` resolve them back to a duplicate, so they",
|
|
111
|
-
"are what holds the deduplicated result. Keeping them is a workaround: no fix",
|
|
112
|
-
"was found that survives on its own. Please report this cluster so it can be",
|
|
113
|
-
`handled without a standing override: ${issuesUrl}`,
|
|
114
|
-
];
|
|
115
|
-
|
|
116
|
-
interface ApplyState {
|
|
117
|
-
duplicates: DuplicateSnapshot;
|
|
118
|
-
// one key per open range still resolving away from the version the workspace
|
|
119
|
-
// anchors its family at
|
|
120
|
-
reuses: Set<string>;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const reuseKeys = (fixes: ClusterFix[]): Set<string> =>
|
|
124
|
-
new Set(
|
|
125
|
-
fixes.flatMap((fix) =>
|
|
126
|
-
fix.reuseFixes.map(
|
|
127
|
-
(reuse) => `${reuse.requesterName}>${reuse.packageName}@${reuse.to}`,
|
|
128
|
-
),
|
|
129
|
-
),
|
|
115
|
+
const defaultReadDependentRanges = (
|
|
116
|
+
projectDir: string,
|
|
117
|
+
packageNames: Set<string>,
|
|
118
|
+
): DependentRangesMap =>
|
|
119
|
+
collectDependentRanges(
|
|
120
|
+
readPnpmLock(lockPathOf(projectDir)),
|
|
121
|
+
packageNames,
|
|
122
|
+
createManifestReader(projectDir),
|
|
130
123
|
);
|
|
131
124
|
|
|
125
|
+
// Each round only drops overrides, so this bounds a run that keeps losing one to
|
|
126
|
+
// a companion it held alongside.
|
|
127
|
+
const maxOverrideRounds = 3;
|
|
128
|
+
|
|
129
|
+
type ApplyState = OverrideTargetState;
|
|
130
|
+
|
|
131
|
+
interface DroppedOverride {
|
|
132
|
+
override: PlannedOverride;
|
|
133
|
+
// whether it held while the overrides were there, and only came apart once
|
|
134
|
+
// pnpm resolved without them
|
|
135
|
+
heldWithOverrides: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
132
138
|
export const applyClusterFixes = ({
|
|
133
139
|
projectDir,
|
|
134
140
|
dryRun = false,
|
|
@@ -141,6 +147,8 @@ export const applyClusterFixes = ({
|
|
|
141
147
|
resolve = () => runPnpm(["dedupe"], { cwd: projectDir }).status,
|
|
142
148
|
readFixes = defaultReadFixes,
|
|
143
149
|
readDuplicates = readDuplicateSnapshot,
|
|
150
|
+
readVersions = readVersionsSnapshot,
|
|
151
|
+
readDependentRanges = defaultReadDependentRanges,
|
|
144
152
|
pnpmVersion = () => readPnpmVersion(),
|
|
145
153
|
convergenceOverrides = true,
|
|
146
154
|
filter,
|
|
@@ -167,7 +175,10 @@ export const applyClusterFixes = ({
|
|
|
167
175
|
|
|
168
176
|
const { selected: fixes, skipped: filteredOut } =
|
|
169
177
|
readSelectedFixes(projectDir);
|
|
170
|
-
|
|
178
|
+
// pnpm resolves an open range to the highest copy still in the tree, so a
|
|
179
|
+
// reuse of the pinned version comes apart as soon as its override is removed.
|
|
180
|
+
// The report still names those ranges; the plan does not chase them.
|
|
181
|
+
const plan = planClusterApply(fixes, { reuse: false });
|
|
171
182
|
|
|
172
183
|
const skipped = [
|
|
173
184
|
...filteredOut.map(describeSkippedClusterFix),
|
|
@@ -309,6 +320,7 @@ export const applyClusterFixes = ({
|
|
|
309
320
|
// gone from its output once the edge points at the anchored version.
|
|
310
321
|
const readState = (): ApplyState => ({
|
|
311
322
|
duplicates: readDuplicates(lockPath),
|
|
323
|
+
versions: readVersions(lockPath),
|
|
312
324
|
reuses: reuseKeys(readSelectedFixes(projectDir).selected),
|
|
313
325
|
});
|
|
314
326
|
|
|
@@ -346,121 +358,162 @@ export const applyClusterFixes = ({
|
|
|
346
358
|
)!;
|
|
347
359
|
|
|
348
360
|
const remaining = readState();
|
|
349
|
-
|
|
350
|
-
// while the edge it repoints is still resolving elsewhere.
|
|
351
|
-
const outstanding = plannedOverrides.filter(
|
|
352
|
-
(override) =>
|
|
353
|
-
[...remaining.duplicates].some((resolution) =>
|
|
354
|
-
resolution.startsWith(`${override.packageName}@`),
|
|
355
|
-
) ||
|
|
356
|
-
[...remaining.reuses].some((key) =>
|
|
357
|
-
key.endsWith(`>${override.packageName}@${override.version}`),
|
|
358
|
-
),
|
|
359
|
-
);
|
|
360
|
-
|
|
361
|
-
if (outstanding.length === 0) {
|
|
362
|
-
return {
|
|
363
|
-
status: "applied",
|
|
364
|
-
before,
|
|
365
|
-
after: remaining.duplicates,
|
|
366
|
-
stickyOverrides: [],
|
|
367
|
-
plannedChangeCount: changeCount,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
361
|
+
const outstanding = outstandingOverrides(plannedOverrides, remaining);
|
|
370
362
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
addOverrides(
|
|
382
|
-
workspaceYamlBefore.content,
|
|
383
|
-
new Map(
|
|
384
|
-
outstanding.map((override) => [override.packageName, override.version]),
|
|
385
|
-
),
|
|
386
|
-
{ convergence: convergenceOverrides },
|
|
387
|
-
),
|
|
388
|
-
);
|
|
363
|
+
const keptOutcome = (
|
|
364
|
+
after: DuplicateSnapshot,
|
|
365
|
+
stickyOverrides: PlannedOverride[],
|
|
366
|
+
): ClusterApplyOutcome => ({
|
|
367
|
+
status: "applied",
|
|
368
|
+
before,
|
|
369
|
+
after,
|
|
370
|
+
stickyOverrides,
|
|
371
|
+
plannedChangeCount: changeCount,
|
|
372
|
+
});
|
|
389
373
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
revertTo(withoutOverridesSnapshot);
|
|
393
|
-
return unchanged("reverted", changeCount);
|
|
374
|
+
if (outstanding.length === 0) {
|
|
375
|
+
return keptOutcome(remaining.duplicates, []);
|
|
394
376
|
}
|
|
395
377
|
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (resolve() !== 0) {
|
|
402
|
-
log(" `pnpm dedupe` failed without the overrides — reverting");
|
|
378
|
+
// The range edits already held on their own, so dropping the overrides keeps
|
|
379
|
+
// them: only a run that made none of those has nothing left to show.
|
|
380
|
+
const withoutOverrides = (
|
|
381
|
+
stickyOverrides: PlannedOverride[],
|
|
382
|
+
): ClusterApplyOutcome => {
|
|
403
383
|
revertTo(withoutOverridesSnapshot);
|
|
404
|
-
return
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const returnedDuplicates = diffDuplicates(
|
|
409
|
-
withOverrides.duplicates,
|
|
410
|
-
after.duplicates,
|
|
411
|
-
).added;
|
|
412
|
-
const returnedReuses = [...after.reuses].filter(
|
|
413
|
-
(key) => !withOverrides.reuses.has(key),
|
|
414
|
-
);
|
|
384
|
+
return plan.manifestEdits.length > 0
|
|
385
|
+
? keptOutcome(readDuplicates(lockPath), stickyOverrides)
|
|
386
|
+
: { ...unchanged("reverted", changeCount), stickyOverrides };
|
|
387
|
+
};
|
|
415
388
|
|
|
416
|
-
|
|
389
|
+
const heldIn = (
|
|
390
|
+
overrides: PlannedOverride[],
|
|
391
|
+
end: ApplyState,
|
|
392
|
+
): PlannedOverride[] => heldOverrides(overrides, { start: remaining, end });
|
|
393
|
+
|
|
394
|
+
// Overrides are scaffolding: they are written, pnpm resolves with them, and
|
|
395
|
+
// what counts is what is still there once they are removed again.
|
|
396
|
+
const overrideRound = (
|
|
397
|
+
overrides: PlannedOverride[],
|
|
398
|
+
): {
|
|
399
|
+
heldWith: PlannedOverride[];
|
|
400
|
+
heldAfter: PlannedOverride[];
|
|
401
|
+
after: ApplyState;
|
|
402
|
+
} | null => {
|
|
403
|
+
log(
|
|
404
|
+
`Adding ${convergenceOverrides ? "convergence" : "plain"} overrides to pnpm-workspace.yaml:`,
|
|
405
|
+
);
|
|
406
|
+
for (const override of overrides) {
|
|
407
|
+
log(
|
|
408
|
+
` ${describeOverride(override, convergenceOverrides)} (${override.reason})`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
417
411
|
writeFileSync(
|
|
418
412
|
workspaceYamlPath,
|
|
419
413
|
addOverrides(
|
|
420
414
|
workspaceYamlBefore.content,
|
|
421
415
|
new Map(
|
|
422
|
-
|
|
423
|
-
override.packageName,
|
|
424
|
-
override.version,
|
|
425
|
-
]),
|
|
416
|
+
overrides.map((override) => [override.packageName, override.version]),
|
|
426
417
|
),
|
|
427
|
-
{
|
|
428
|
-
convergence: convergenceOverrides,
|
|
429
|
-
comment: stickyOverrideReason.join("\n"),
|
|
430
|
-
},
|
|
418
|
+
{ convergence: convergenceOverrides },
|
|
431
419
|
),
|
|
432
420
|
);
|
|
433
421
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
422
|
+
const withOverrides = resolveAndCheck("the overrides");
|
|
423
|
+
if (withOverrides === null) return null;
|
|
424
|
+
const heldWith = heldIn(overrides, withOverrides);
|
|
425
|
+
|
|
426
|
+
log("Removing the overrides and re-resolving to check the result holds:");
|
|
427
|
+
restoreFiles([workspaceYamlBefore]);
|
|
428
|
+
const after = resolveAndCheck("removing the overrides");
|
|
429
|
+
if (after === null) return null;
|
|
430
|
+
|
|
431
|
+
return { heldWith, heldAfter: heldIn(overrides, after), after };
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// A member nothing duplicates only moves for its family's sake, so the
|
|
435
|
+
// family's duplicated members already say what did not merge.
|
|
436
|
+
const reportDropped = (dropped: DroppedOverride[]): void => {
|
|
437
|
+
const reported = dropped.filter(({ override }) =>
|
|
438
|
+
[...remaining.duplicates].some((resolution) =>
|
|
439
|
+
resolution.startsWith(`${override.packageName}@`),
|
|
440
|
+
),
|
|
441
|
+
);
|
|
442
|
+
if (reported.length === 0) return;
|
|
443
|
+
|
|
444
|
+
const ranges = readDependentRanges(
|
|
445
|
+
projectDir,
|
|
446
|
+
new Set(reported.map(({ override }) => override.packageName)),
|
|
447
|
+
);
|
|
448
|
+
const logGroup = (title: string, entries: DroppedOverride[]): void => {
|
|
449
|
+
if (entries.length === 0) return;
|
|
450
|
+
log(title);
|
|
451
|
+
for (const { override } of entries) {
|
|
452
|
+
log(` ${override.packageName} onto ${override.version}:`);
|
|
453
|
+
for (const line of describeUnheldOverride(
|
|
454
|
+
override,
|
|
455
|
+
ranges.get(override.packageName) ?? [],
|
|
456
|
+
)) {
|
|
457
|
+
log(` ${line}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
};
|
|
439
461
|
|
|
462
|
+
logGroup(
|
|
463
|
+
"Not merged even with an override:",
|
|
464
|
+
reported.filter((entry) => !entry.heldWithOverrides),
|
|
465
|
+
);
|
|
466
|
+
logGroup(
|
|
467
|
+
"pnpm resolves these back without an override, so they are not merged:",
|
|
468
|
+
reported.filter((entry) => entry.heldWithOverrides),
|
|
469
|
+
);
|
|
440
470
|
log(
|
|
441
|
-
`
|
|
471
|
+
`No override is left behind. If one of these should merge, please report it at ${issuesUrl}`,
|
|
442
472
|
);
|
|
443
|
-
|
|
444
|
-
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const dropped: DroppedOverride[] = [];
|
|
476
|
+
let candidates = outstanding;
|
|
477
|
+
for (let round = 1; round <= maxOverrideRounds; round++) {
|
|
478
|
+
if (round > 1) {
|
|
479
|
+
log("Retrying with only the overrides that held:");
|
|
480
|
+
restoreFiles(withoutOverridesSnapshot);
|
|
445
481
|
}
|
|
446
|
-
|
|
447
|
-
|
|
482
|
+
|
|
483
|
+
const result = overrideRound(candidates);
|
|
484
|
+
if (result === null) return withoutOverrides([]);
|
|
485
|
+
|
|
486
|
+
const { heldWith, heldAfter, after } = result;
|
|
487
|
+
for (const override of candidates) {
|
|
488
|
+
if (!heldAfter.includes(override)) {
|
|
489
|
+
dropped.push({
|
|
490
|
+
override,
|
|
491
|
+
heldWithOverrides: heldWith.includes(override),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
448
494
|
}
|
|
449
495
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
496
|
+
if (heldAfter.length === candidates.length) {
|
|
497
|
+
reportDropped(dropped);
|
|
498
|
+
return keptOutcome(
|
|
499
|
+
after.duplicates,
|
|
500
|
+
dropped.map(({ override }) => override),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
candidates = heldAfter;
|
|
504
|
+
if (candidates.length === 0) break;
|
|
457
505
|
}
|
|
458
506
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
507
|
+
log(
|
|
508
|
+
candidates.length === 0
|
|
509
|
+
? " No override holds on its own — reverting them"
|
|
510
|
+
: ` Overrides still came apart after ${maxOverrideRounds} rounds — reverting them`,
|
|
511
|
+
);
|
|
512
|
+
// what is left only held alongside overrides the last round dropped
|
|
513
|
+
dropped.push(
|
|
514
|
+
...candidates.map((override) => ({ override, heldWithOverrides: true })),
|
|
515
|
+
);
|
|
516
|
+
const outcome = withoutOverrides(dropped.map(({ override }) => override));
|
|
517
|
+
reportDropped(dropped);
|
|
518
|
+
return outcome;
|
|
466
519
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ok, strictEqual } from "node:assert/strict";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { cpSync, mkdirSync, realpathSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { afterEach, describe, it } from "node:test";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { createTempProjects } from "../helpers/tempProjects.ts";
|
|
7
8
|
|
|
@@ -58,11 +59,11 @@ describe("pnpm-why-duplicate", () => {
|
|
|
58
59
|
const fromRoot = runBin("pnpm-why-duplicate", dir, ["--details"]);
|
|
59
60
|
const fromNested = runBin("pnpm-why-duplicate", nested, ["--details"]);
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
strictEqual(fromNested.status, 0);
|
|
63
|
+
strictEqual(fromNested.stdout, fromRoot.stdout);
|
|
64
|
+
ok(fromNested.stdout.includes("printable-shell-command"));
|
|
65
|
+
strictEqual(fromNested.stderr.trim(), lockNotice(dir));
|
|
66
|
+
strictEqual(fromRoot.stderr, "");
|
|
66
67
|
});
|
|
67
68
|
|
|
68
69
|
it("names the lockfile it could not find and exits 1", () => {
|
|
@@ -70,10 +71,32 @@ describe("pnpm-why-duplicate", () => {
|
|
|
70
71
|
|
|
71
72
|
const result = runBin("pnpm-why-duplicate", dir, []);
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
strictEqual(result.status, 1);
|
|
75
|
+
strictEqual(
|
|
76
|
+
result.stderr.trim(),
|
|
75
77
|
`No pnpm-lock.yaml found in ${realpathSync(dir)} or any parent directory`,
|
|
76
78
|
);
|
|
77
79
|
});
|
|
78
80
|
});
|
|
81
|
+
|
|
82
|
+
it("reports on a lockfile that also resolves the package manager", () => {
|
|
83
|
+
const result = runBin(
|
|
84
|
+
"pnpm-why-duplicate",
|
|
85
|
+
project("package-manager-document"),
|
|
86
|
+
["--details"],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
strictEqual(result.stderr, "");
|
|
90
|
+
strictEqual(result.status, 0);
|
|
91
|
+
strictEqual(
|
|
92
|
+
result.stdout,
|
|
93
|
+
runBin(
|
|
94
|
+
"pnpm-why-duplicate",
|
|
95
|
+
project("duplicated-printable-shell-command"),
|
|
96
|
+
["--details"],
|
|
97
|
+
).stdout,
|
|
98
|
+
);
|
|
99
|
+
ok(result.stdout.includes("printable-shell-command"));
|
|
100
|
+
ok(!result.stdout.includes("@pnpm/exe"));
|
|
101
|
+
});
|
|
79
102
|
});
|
package/src/dedupe.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import {
|
|
2
4
|
countDuplicatedPackages,
|
|
3
5
|
createPackageFilter,
|
|
@@ -32,6 +34,23 @@ const residualsMessage = "`pnpm dedupe` would also change the lockfile.";
|
|
|
32
34
|
const filteredResidualsMessage =
|
|
33
35
|
"`pnpm dedupe` was not run: it ignores the package filter. Run `pnpm dedupe` yourself, or this command without a filter, to finish the rest.";
|
|
34
36
|
|
|
37
|
+
// The comment pnpm-dedup 2.2 wrote above the overrides it kept. Those still
|
|
38
|
+
// apply on every resolution, so a run cannot tell what holds without them.
|
|
39
|
+
const leftoverOverridesMarker = "# Added by pnpm-dedup.";
|
|
40
|
+
|
|
41
|
+
const warnOnLeftoverOverrides = (projectDir: string): void => {
|
|
42
|
+
const workspaceYamlPath = join(projectDir, "pnpm-workspace.yaml");
|
|
43
|
+
if (
|
|
44
|
+
!existsSync(workspaceYamlPath) ||
|
|
45
|
+
!readFileSync(workspaceYamlPath, "utf8").includes(leftoverOverridesMarker)
|
|
46
|
+
) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
console.log(
|
|
50
|
+
`pnpm-workspace.yaml still has overrides an earlier pnpm-dedup kept (under "${leftoverOverridesMarker.slice(2)}"). This version never keeps any: remove them and run again to see what holds on its own.`,
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
35
54
|
/**
|
|
36
55
|
* `pnpm dedupe` only merges what it can resolve to a single version on its own:
|
|
37
56
|
* it never widens a workspace range, and it never repoints an edge that resolved
|
|
@@ -79,6 +98,8 @@ export function dedupe({
|
|
|
79
98
|
return undefined;
|
|
80
99
|
})();
|
|
81
100
|
|
|
101
|
+
warnOnLeftoverOverrides(projectDir);
|
|
102
|
+
|
|
82
103
|
const outcome = applyClusterFixes({
|
|
83
104
|
projectDir,
|
|
84
105
|
dryRun,
|
|
@@ -87,12 +108,6 @@ export function dedupe({
|
|
|
87
108
|
packageManagerResiduals: residuals,
|
|
88
109
|
});
|
|
89
110
|
|
|
90
|
-
if (outcome.status === "kept-overrides") {
|
|
91
|
-
console.log(
|
|
92
|
-
`${outcome.stickyOverrides.length} override(s) left in pnpm-workspace.yaml — see the comment above them`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
111
|
if (mode === "check") {
|
|
97
112
|
process.exitCode =
|
|
98
113
|
outcome.plannedChangeCount > 0 || (probe !== null && probe.status !== 0)
|
|
@@ -106,7 +121,7 @@ export function dedupe({
|
|
|
106
121
|
// Applying an override needs a real resolution, and `pnpm dedupe` is the
|
|
107
122
|
// only one that performs it — so a filtered run that changed something has
|
|
108
123
|
// already merged whatever else pnpm could reach on its own.
|
|
109
|
-
if (outcome.status === "applied"
|
|
124
|
+
if (outcome.status === "applied") {
|
|
110
125
|
console.log(
|
|
111
126
|
"`pnpm dedupe` ran to apply the fixes above and merged what it could on its own: the filter bounds the edits made here, not pnpm's resolution.",
|
|
112
127
|
);
|