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.
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/applyClusterFixes.js +252 -0
- package/dist/applyClusterFixes.js.map +1 -0
- package/dist/applyClusterFixes.test.js +474 -0
- package/dist/applyClusterFixes.test.js.map +1 -0
- package/dist/bin/pnpm-dedupe.js +35 -0
- package/dist/bin/pnpm-dedupe.js.map +1 -0
- package/dist/bin/pnpm-why-duplicate.js +23 -0
- package/dist/bin/pnpm-why-duplicate.js.map +1 -0
- package/dist/bin/pnpm-why-duplicate.test.js +57 -0
- package/dist/bin/pnpm-why-duplicate.test.js.map +1 -0
- package/dist/dedupe.js +86 -0
- package/dist/dedupe.js.map +1 -0
- package/dist/dedupeCheckConsistency.test.js +149 -0
- package/dist/dedupeCheckConsistency.test.js.map +1 -0
- package/dist/dedupeEndToEnd.test.js +121 -0
- package/dist/dedupeEndToEnd.test.js.map +1 -0
- package/dist/dedupePeers.test.js +105 -0
- package/dist/dedupePeers.test.js.map +1 -0
- package/dist/definitions/applyClusterFixes.d.ts +24 -0
- package/dist/definitions/applyClusterFixes.d.ts.map +1 -0
- package/dist/definitions/applyClusterFixes.test.d.ts +2 -0
- package/dist/definitions/applyClusterFixes.test.d.ts.map +1 -0
- package/dist/definitions/bin/pnpm-dedupe.d.ts +3 -0
- package/dist/definitions/bin/pnpm-dedupe.d.ts.map +1 -0
- package/dist/definitions/bin/pnpm-why-duplicate.d.ts +3 -0
- package/dist/definitions/bin/pnpm-why-duplicate.d.ts.map +1 -0
- package/dist/definitions/bin/pnpm-why-duplicate.test.d.ts +2 -0
- package/dist/definitions/bin/pnpm-why-duplicate.test.d.ts.map +1 -0
- package/dist/definitions/dedupe.d.ts +15 -0
- package/dist/definitions/dedupe.d.ts.map +1 -0
- package/dist/definitions/dedupeCheckConsistency.test.d.ts +2 -0
- package/dist/definitions/dedupeCheckConsistency.test.d.ts.map +1 -0
- package/dist/definitions/dedupeEndToEnd.test.d.ts +2 -0
- package/dist/definitions/dedupeEndToEnd.test.d.ts.map +1 -0
- package/dist/definitions/dedupePeers.test.d.ts +2 -0
- package/dist/definitions/dedupePeers.test.d.ts.map +1 -0
- package/dist/definitions/displayMany.d.ts +16 -0
- package/dist/definitions/displayMany.d.ts.map +1 -0
- package/dist/definitions/displayMany.test.d.ts +2 -0
- package/dist/definitions/displayMany.test.d.ts.map +1 -0
- package/dist/definitions/helpers/buildIdentifiedFixesMap.test.d.ts +2 -0
- package/dist/definitions/helpers/buildIdentifiedFixesMap.test.d.ts.map +1 -0
- package/dist/definitions/helpers/buildPnpmPackagesMap.d.ts +13 -0
- package/dist/definitions/helpers/buildPnpmPackagesMap.d.ts.map +1 -0
- package/dist/definitions/helpers/buildPnpmPackagesMap.test.d.ts +2 -0
- package/dist/definitions/helpers/buildPnpmPackagesMap.test.d.ts.map +1 -0
- package/dist/definitions/helpers/collectDependentRanges.d.ts +22 -0
- package/dist/definitions/helpers/collectDependentRanges.d.ts.map +1 -0
- package/dist/definitions/helpers/collectDependentRanges.test.d.ts +2 -0
- package/dist/definitions/helpers/collectDependentRanges.test.d.ts.map +1 -0
- package/dist/definitions/helpers/collectPnpmDependents.d.ts +8 -0
- package/dist/definitions/helpers/collectPnpmDependents.d.ts.map +1 -0
- package/dist/definitions/helpers/collectPnpmDependents.test.d.ts +2 -0
- package/dist/definitions/helpers/collectPnpmDependents.test.d.ts.map +1 -0
- package/dist/definitions/helpers/duplicateSnapshot.d.ts +3 -0
- package/dist/definitions/helpers/duplicateSnapshot.d.ts.map +1 -0
- package/dist/definitions/helpers/parsePnpmLockPackages.d.ts +44 -0
- package/dist/definitions/helpers/parsePnpmLockPackages.d.ts.map +1 -0
- package/dist/definitions/helpers/parsePnpmLockPackages.test.d.ts +2 -0
- package/dist/definitions/helpers/parsePnpmLockPackages.test.d.ts.map +1 -0
- package/dist/definitions/helpers/pnpmProtocol.d.ts +2 -0
- package/dist/definitions/helpers/pnpmProtocol.d.ts.map +1 -0
- package/dist/definitions/helpers/pnpmVersion.d.ts +11 -0
- package/dist/definitions/helpers/pnpmVersion.d.ts.map +1 -0
- package/dist/definitions/helpers/pnpmWorkspaceYaml.d.ts +23 -0
- package/dist/definitions/helpers/pnpmWorkspaceYaml.d.ts.map +1 -0
- package/dist/definitions/helpers/pnpmWorkspaceYaml.test.d.ts +2 -0
- package/dist/definitions/helpers/pnpmWorkspaceYaml.test.d.ts.map +1 -0
- package/dist/definitions/helpers/projectDir.d.ts +3 -0
- package/dist/definitions/helpers/projectDir.d.ts.map +1 -0
- package/dist/definitions/helpers/readInstalledManifest.d.ts +32 -0
- package/dist/definitions/helpers/readInstalledManifest.d.ts.map +1 -0
- package/dist/definitions/helpers/readInstalledManifest.test.d.ts +2 -0
- package/dist/definitions/helpers/readInstalledManifest.test.d.ts.map +1 -0
- package/dist/definitions/helpers/runPnpm.d.ts +11 -0
- package/dist/definitions/helpers/runPnpm.d.ts.map +1 -0
- package/dist/definitions/helpers/tempProjects.d.ts +8 -0
- package/dist/definitions/helpers/tempProjects.d.ts.map +1 -0
- package/dist/definitions/helpers/toLockstepGraph.d.ts +12 -0
- package/dist/definitions/helpers/toLockstepGraph.d.ts.map +1 -0
- package/dist/definitions/helpers/toLockstepGraph.test.d.ts +2 -0
- package/dist/definitions/helpers/toLockstepGraph.test.d.ts.map +1 -0
- package/dist/definitions/helpers/versionsSnapshot.d.ts +3 -0
- package/dist/definitions/helpers/versionsSnapshot.d.ts.map +1 -0
- package/dist/definitions/identifyClusterFixes.d.ts +7 -0
- package/dist/definitions/identifyClusterFixes.d.ts.map +1 -0
- package/dist/definitions/identifyClusterFixes.test.d.ts +2 -0
- package/dist/definitions/identifyClusterFixes.test.d.ts.map +1 -0
- package/dist/definitions/identifyResolutionFixes.test.d.ts +2 -0
- package/dist/definitions/identifyResolutionFixes.test.d.ts.map +1 -0
- package/dist/definitions/index.d.ts +25 -0
- package/dist/definitions/index.d.ts.map +1 -0
- package/dist/definitions/pnpmLockTypes.d.ts +42 -0
- package/dist/definitions/pnpmLockTypes.d.ts.map +1 -0
- package/dist/definitions/readPnpmLock.d.ts +3 -0
- package/dist/definitions/readPnpmLock.d.ts.map +1 -0
- package/dist/displayMany.js +56 -0
- package/dist/displayMany.js.map +1 -0
- package/dist/displayMany.test.js +146 -0
- package/dist/displayMany.test.js.map +1 -0
- package/dist/helpers/buildIdentifiedFixesMap.test.js +27 -0
- package/dist/helpers/buildIdentifiedFixesMap.test.js.map +1 -0
- package/dist/helpers/buildPnpmPackagesMap.js +23 -0
- package/dist/helpers/buildPnpmPackagesMap.js.map +1 -0
- package/dist/helpers/buildPnpmPackagesMap.test.js +68 -0
- package/dist/helpers/buildPnpmPackagesMap.test.js.map +1 -0
- package/dist/helpers/collectDependentRanges.js +118 -0
- package/dist/helpers/collectDependentRanges.js.map +1 -0
- package/dist/helpers/collectDependentRanges.test.js +211 -0
- package/dist/helpers/collectDependentRanges.test.js.map +1 -0
- package/dist/helpers/collectPnpmDependents.js +55 -0
- package/dist/helpers/collectPnpmDependents.js.map +1 -0
- package/dist/helpers/collectPnpmDependents.test.js +78 -0
- package/dist/helpers/collectPnpmDependents.test.js.map +1 -0
- package/dist/helpers/duplicateSnapshot.js +5 -0
- package/dist/helpers/duplicateSnapshot.js.map +1 -0
- package/dist/helpers/parsePnpmLockPackages.js +62 -0
- package/dist/helpers/parsePnpmLockPackages.js.map +1 -0
- package/dist/helpers/parsePnpmLockPackages.test.js +62 -0
- package/dist/helpers/parsePnpmLockPackages.test.js.map +1 -0
- package/dist/helpers/pnpmProtocol.js +2 -0
- package/dist/helpers/pnpmProtocol.js.map +1 -0
- package/dist/helpers/pnpmVersion.js +23 -0
- package/dist/helpers/pnpmVersion.js.map +1 -0
- package/dist/helpers/pnpmWorkspaceYaml.js +48 -0
- package/dist/helpers/pnpmWorkspaceYaml.js.map +1 -0
- package/dist/helpers/pnpmWorkspaceYaml.test.js +91 -0
- package/dist/helpers/pnpmWorkspaceYaml.test.js.map +1 -0
- package/dist/helpers/projectDir.js +10 -0
- package/dist/helpers/projectDir.js.map +1 -0
- package/dist/helpers/readInstalledManifest.js +139 -0
- package/dist/helpers/readInstalledManifest.js.map +1 -0
- package/dist/helpers/readInstalledManifest.test.js +83 -0
- package/dist/helpers/readInstalledManifest.test.js.map +1 -0
- package/dist/helpers/runPnpm.js +16 -0
- package/dist/helpers/runPnpm.js.map +1 -0
- package/dist/helpers/tempProjects.js +85 -0
- package/dist/helpers/tempProjects.js.map +1 -0
- package/dist/helpers/toLockstepGraph.js +34 -0
- package/dist/helpers/toLockstepGraph.js.map +1 -0
- package/dist/helpers/toLockstepGraph.test.js +81 -0
- package/dist/helpers/toLockstepGraph.test.js.map +1 -0
- package/dist/helpers/versionsSnapshot.js +5 -0
- package/dist/helpers/versionsSnapshot.js.map +1 -0
- package/dist/identifyClusterFixes.js +53 -0
- package/dist/identifyClusterFixes.js.map +1 -0
- package/dist/identifyClusterFixes.test.js +307 -0
- package/dist/identifyClusterFixes.test.js.map +1 -0
- package/dist/identifyResolutionFixes.test.js +49 -0
- package/dist/identifyResolutionFixes.test.js.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/pnpmLockTypes.js +7 -0
- package/dist/pnpmLockTypes.js.map +1 -0
- package/dist/readPnpmLock.js +6 -0
- package/dist/readPnpmLock.js.map +1 -0
- package/package.json +71 -0
- package/src/applyClusterFixes.test.ts +560 -0
- package/src/applyClusterFixes.ts +466 -0
- package/src/bin/pnpm-dedupe.ts +44 -0
- package/src/bin/pnpm-why-duplicate.test.ts +79 -0
- package/src/bin/pnpm-why-duplicate.ts +34 -0
- package/src/dedupe.ts +124 -0
- package/src/dedupeCheckConsistency.test.ts +218 -0
- package/src/dedupeEndToEnd.test.ts +168 -0
- package/src/dedupePeers.test.ts +175 -0
- package/src/displayMany.test.ts +208 -0
- package/src/displayMany.ts +104 -0
- package/src/helpers/buildIdentifiedFixesMap.test.ts +33 -0
- package/src/helpers/buildPnpmPackagesMap.test.ts +119 -0
- package/src/helpers/buildPnpmPackagesMap.ts +53 -0
- package/src/helpers/collectDependentRanges.test.ts +311 -0
- package/src/helpers/collectDependentRanges.ts +185 -0
- package/src/helpers/collectPnpmDependents.test.ts +112 -0
- package/src/helpers/collectPnpmDependents.ts +79 -0
- package/src/helpers/duplicateSnapshot.ts +18 -0
- package/src/helpers/parsePnpmLockPackages.test.ts +93 -0
- package/src/helpers/parsePnpmLockPackages.ts +99 -0
- package/src/helpers/pnpmProtocol.ts +12 -0
- package/src/helpers/pnpmVersion.ts +28 -0
- package/src/helpers/pnpmWorkspaceYaml.test.ts +140 -0
- package/src/helpers/pnpmWorkspaceYaml.ts +77 -0
- package/src/helpers/projectDir.ts +14 -0
- package/src/helpers/readInstalledManifest.test.ts +124 -0
- package/src/helpers/readInstalledManifest.ts +202 -0
- package/src/helpers/runPnpm.ts +34 -0
- package/src/helpers/tempProjects.ts +94 -0
- package/src/helpers/toLockstepGraph.test.ts +98 -0
- package/src/helpers/toLockstepGraph.ts +52 -0
- package/src/helpers/versionsSnapshot.ts +9 -0
- package/src/identifyClusterFixes.test.ts +430 -0
- package/src/identifyClusterFixes.ts +114 -0
- package/src/identifyResolutionFixes.test.ts +82 -0
- package/src/index.ts +159 -0
- package/src/pnpmLockTypes.ts +44 -0
- 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
|
+
}
|