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
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { describe, it } from "bun:test";
|
|
2
1
|
import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
|
|
3
2
|
import { spawnSync } from "node:child_process";
|
|
4
3
|
import { readFileSync, rmSync } from "node:fs";
|
|
5
4
|
import { join } from "node:path";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { buildIdentifiedFixesMap } from "pm-dedup-core";
|
|
8
8
|
import {
|
|
@@ -14,15 +14,24 @@ import {
|
|
|
14
14
|
} from "./index.ts";
|
|
15
15
|
|
|
16
16
|
// These tests shell out to the real `pnpm` and require either network access or
|
|
17
|
-
// a warm pnpm store
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
// a warm pnpm store. `--lockfile-only` keeps pnpm from writing node_modules, and
|
|
18
|
+
// `--frozen-lockfile` / `--check` never rewrite the lockfile, so pnpm runs
|
|
19
|
+
// against the committed fixtures in place without modifying them (asserted
|
|
20
|
+
// below). The invariant is: everything `pnpm dedupe` would merge is also
|
|
21
|
+
// reported by our listing.
|
|
22
|
+
//
|
|
23
|
+
// What `dedupe` merges changes between pnpm minors, so the expectations hold for
|
|
24
|
+
// pnpm 12 only and the suite is skipped under any other major. The fixtures cannot
|
|
25
|
+
// pin it through `packageManager`: pnpm 12 records the pinned version in the
|
|
26
|
+
// lockfile, which breaks their byte-exactness.
|
|
27
|
+
|
|
28
|
+
const SUPPORTED_PNPM_MAJOR = 12;
|
|
29
|
+
|
|
30
|
+
const pnpmMajor = ((): number | null => {
|
|
31
|
+
const result = spawnSync("pnpm", ["--version"], { encoding: "utf8" });
|
|
32
|
+
if (result.status !== 0) return null;
|
|
33
|
+
return Number(result.stdout.trim().split(".")[0]);
|
|
34
|
+
})();
|
|
26
35
|
|
|
27
36
|
const fixturePath = (scenario: string): string =>
|
|
28
37
|
fileURLToPath(new URL(`../test/fixtures/${scenario}`, import.meta.url));
|
|
@@ -47,17 +56,26 @@ const runPnpm = (cwd: string, args: string[]): PnpmRun => {
|
|
|
47
56
|
};
|
|
48
57
|
};
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
const withoutPeerSuffix = (version: string): string =>
|
|
60
|
+
version.split("(")[0] ?? "";
|
|
61
|
+
|
|
62
|
+
// `pnpm dedupe --check` prints a tree of changes, one `├── name from → to` branch
|
|
63
|
+
// per changed dependency. A change that keeps the version only re-resolves peers,
|
|
64
|
+
// which is not a merge.
|
|
65
|
+
const parseMergedPackages = (output: string): string[] => {
|
|
53
66
|
const names = new Set<string>();
|
|
54
67
|
for (const line of output.split("\n")) {
|
|
55
68
|
if (!line.includes("→")) continue;
|
|
56
|
-
const name = line
|
|
69
|
+
const [name, from, , to] = line
|
|
57
70
|
.replace(/^[\s│├└─]+/u, "")
|
|
58
71
|
.trim()
|
|
59
|
-
.split(/\s+/u)
|
|
60
|
-
if (
|
|
72
|
+
.split(/\s+/u);
|
|
73
|
+
if (
|
|
74
|
+
name &&
|
|
75
|
+
from &&
|
|
76
|
+
to &&
|
|
77
|
+
withoutPeerSuffix(from) !== withoutPeerSuffix(to)
|
|
78
|
+
) {
|
|
61
79
|
names.add(name);
|
|
62
80
|
}
|
|
63
81
|
}
|
|
@@ -98,70 +116,35 @@ const assertPristine = (dir: string, lockBefore: string): void => {
|
|
|
98
116
|
rmSync(join(dir, "node_modules"), { recursive: true, force: true });
|
|
99
117
|
};
|
|
100
118
|
|
|
101
|
-
const suite =
|
|
119
|
+
const suite = pnpmMajor === SUPPORTED_PNPM_MAJOR ? describe : describe.skip;
|
|
102
120
|
|
|
103
121
|
suite("pnpm dedupe --check vs listDuplicates", () => {
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
],
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
scenario: "duplicated-typescript-eslint-dedupe-peers",
|
|
117
|
-
expectedFlagged: [
|
|
118
|
-
"@typescript-eslint/tsconfig-utils",
|
|
119
|
-
"@typescript-eslint/types",
|
|
120
|
-
],
|
|
121
|
-
},
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
for (const { scenario, expectedFlagged } of mergeable) {
|
|
125
|
-
it(`flags the mergeable subset of ${scenario}, all of which listDuplicates reports`, () => {
|
|
126
|
-
const dir = fixturePath(scenario);
|
|
127
|
-
const lockBefore = lockContent(dir);
|
|
128
|
-
|
|
129
|
-
const install = runPnpm(dir, ["install", "--frozen-lockfile"]);
|
|
130
|
-
strictEqual(install.status, 0, install.output);
|
|
131
|
-
assertPristine(dir, lockBefore);
|
|
132
|
-
|
|
133
|
-
const check = runPnpm(dir, ["dedupe", "--check"]);
|
|
134
|
-
ok(
|
|
135
|
-
check.status !== 0,
|
|
136
|
-
`dedupe --check should flag issues\n${check.output}`,
|
|
137
|
-
);
|
|
138
|
-
assertPristine(dir, lockBefore);
|
|
139
|
-
|
|
140
|
-
deepStrictEqual(parseDedupedPackages(check.output), expectedFlagged);
|
|
141
|
-
|
|
142
|
-
const duplicates = duplicateNames(scenario);
|
|
143
|
-
for (const name of expectedFlagged) {
|
|
144
|
-
ok(
|
|
145
|
-
duplicates.includes(name),
|
|
146
|
-
`listDuplicates should report ${name} that pnpm dedupe flags`,
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
}, 180_000);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Duplicates pnpm does not merge: `dedupe --check` exits 0 and flags nothing,
|
|
153
|
-
// yet listDuplicates still reports the duplicate. The `mergeable-alias*`
|
|
154
|
-
// fixtures go one step further: every declared range accepts the aliased
|
|
155
|
-
// 5.0.7 pin, so we do identify a merge target — one pnpm will never apply,
|
|
156
|
-
// because merging means downgrading the range from 5.3.1.
|
|
122
|
+
// pnpm merges none of these duplicates, yet listDuplicates reports each one.
|
|
123
|
+
// In the `duplicated-typescript-eslint*` fixtures (`-dedupe-peers` is the same
|
|
124
|
+
// dependency with `dedupePeers: true`, which flattens the peer suffixes) pnpm
|
|
125
|
+
// re-resolves `@pob/eslint-config`'s auto-installed `eslint` peer to 9.39.5,
|
|
126
|
+
// the newest version that also satisfies eslint-plugin-react, and leaves the
|
|
127
|
+
// @typescript-eslint packages duplicated; pnpm 11.26 still merged them.
|
|
128
|
+
// The `mergeable-alias*` fixtures go one step further: every declared range
|
|
129
|
+
// accepts the aliased 5.0.7 pin, so we do identify a merge target — one pnpm
|
|
130
|
+
// will never apply, because merging means downgrading the range from 5.3.1.
|
|
157
131
|
// `wildcard-not-reused` is the widest case: a `*` range resolved to 0.87.0
|
|
158
132
|
// instead of the installed 0.84.5, duplicating the metro family (see
|
|
159
|
-
// wildcardNotReused.test.ts). pnpm will not undo it either
|
|
160
|
-
|
|
133
|
+
// wildcardNotReused.test.ts). pnpm will not undo it either; it only re-resolves
|
|
134
|
+
// the optional `supports-color` peer of metro-symbolicate.
|
|
135
|
+
const scenarios: {
|
|
161
136
|
scenario: string;
|
|
162
137
|
expectedDuplicate: string;
|
|
163
138
|
expectedFixTargets?: string[];
|
|
164
139
|
}[] = [
|
|
140
|
+
{
|
|
141
|
+
scenario: "duplicated-typescript-eslint",
|
|
142
|
+
expectedDuplicate: "@typescript-eslint/types",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
scenario: "duplicated-typescript-eslint-dedupe-peers",
|
|
146
|
+
expectedDuplicate: "@typescript-eslint/types",
|
|
147
|
+
},
|
|
165
148
|
{
|
|
166
149
|
scenario: "duplicated-babel-frame",
|
|
167
150
|
expectedDuplicate: "@babel/code-frame",
|
|
@@ -186,33 +169,34 @@ suite("pnpm dedupe --check vs listDuplicates", () => {
|
|
|
186
169
|
},
|
|
187
170
|
];
|
|
188
171
|
|
|
189
|
-
for (const {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
172
|
+
for (const { scenario, expectedDuplicate, expectedFixTargets } of scenarios) {
|
|
173
|
+
it(
|
|
174
|
+
`merges nothing in ${scenario} but still lists ${expectedDuplicate}`,
|
|
175
|
+
{ timeout: 180_000 },
|
|
176
|
+
() => {
|
|
177
|
+
const dir = fixturePath(scenario);
|
|
178
|
+
const lockBefore = lockContent(dir);
|
|
179
|
+
|
|
180
|
+
const install = runPnpm(dir, ["install", "--frozen-lockfile"]);
|
|
181
|
+
strictEqual(install.status, 0, install.output);
|
|
182
|
+
assertPristine(dir, lockBefore);
|
|
183
|
+
|
|
184
|
+
// exits 1 on a peer-only re-resolution too
|
|
185
|
+
const check = runPnpm(dir, ["dedupe", "--check"]);
|
|
186
|
+
ok(
|
|
187
|
+
check.status === 0 ||
|
|
188
|
+
check.output.includes("ERR_PNPM_DEDUPE_CHECK_ISSUES"),
|
|
189
|
+
check.output,
|
|
190
|
+
);
|
|
191
|
+
assertPristine(dir, lockBefore);
|
|
192
|
+
|
|
193
|
+
deepStrictEqual(parseMergedPackages(check.output), []);
|
|
194
|
+
ok(duplicateNames(scenario).includes(expectedDuplicate));
|
|
195
|
+
deepStrictEqual(
|
|
196
|
+
fixTargets(scenario, expectedDuplicate),
|
|
197
|
+
expectedFixTargets ?? [],
|
|
198
|
+
);
|
|
199
|
+
},
|
|
200
|
+
);
|
|
217
201
|
}
|
|
218
202
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { afterEach, describe, it } from "bun:test";
|
|
2
1
|
import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
|
|
3
2
|
import { spawnSync } from "node:child_process";
|
|
4
3
|
import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
4
|
import { join } from "node:path";
|
|
5
|
+
import { afterEach, describe, it } from "node:test";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { buildPnpmPackagesMap } from "./helpers/buildPnpmPackagesMap.ts";
|
|
8
8
|
import { readDuplicateSnapshot } from "./helpers/duplicateSnapshot.ts";
|
|
@@ -89,80 +89,87 @@ const resolutionsOf = (dir: string, packageName: string): string[] => {
|
|
|
89
89
|
const suite = pnpmAvailable ? describe : describe.skip;
|
|
90
90
|
|
|
91
91
|
suite("pnpm-dedupe end to end", () => {
|
|
92
|
-
it(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
92
|
+
it(
|
|
93
|
+
"converges the cluster and leaves pnpm holding it without an override",
|
|
94
|
+
{ timeout: 180_000 },
|
|
95
|
+
() => {
|
|
96
|
+
const dir = projects.create();
|
|
97
|
+
// only the manifest: the lockfile has to be the real resolution pnpm writes
|
|
98
|
+
// here, not the trimmed one the unit-test fixture commits
|
|
99
|
+
cpSync(join(fixtureDir, "package.json"), join(dir, "package.json"));
|
|
100
|
+
writeFileSync(join(dir, "pnpm-workspace.yaml"), workspaceYamlContent);
|
|
101
|
+
|
|
102
|
+
const installed = install(dir);
|
|
103
|
+
strictEqual(installed.status, 0, installed.output);
|
|
104
|
+
|
|
105
|
+
// `@yudiel/react-qr-scanner` pins barcode-detector at exactly 3.0.3 while
|
|
106
|
+
// expo-camera's `^3.0.0` resolved to 3.2.2, and the pair drags zxing-wasm
|
|
107
|
+
// along: four resolutions for two packages.
|
|
108
|
+
const before = readDuplicateSnapshot(lockPath(dir));
|
|
109
|
+
strictEqual(before.size, 4, [...before].join(", "));
|
|
110
|
+
ok(before.has("barcode-detector@3.0.3"));
|
|
111
|
+
ok(before.has("barcode-detector@3.2.2"));
|
|
112
|
+
|
|
113
|
+
const applied = dedupe(dir);
|
|
114
|
+
strictEqual(applied.status, 0, applied.output);
|
|
115
|
+
|
|
116
|
+
// the tool's own verdict: the convergence override went in, came back out,
|
|
117
|
+
// and the result survived the re-resolution without it
|
|
118
|
+
ok(
|
|
119
|
+
applied.output.includes('"barcode-detector@": "3.0.3"'),
|
|
120
|
+
applied.output,
|
|
121
|
+
);
|
|
122
|
+
ok(
|
|
123
|
+
applied.output.includes(
|
|
124
|
+
"Removing the overrides and re-resolving to check the result holds",
|
|
125
|
+
),
|
|
126
|
+
applied.output,
|
|
127
|
+
);
|
|
128
|
+
// what it deduped, named: two packages, one copy of each merged away
|
|
129
|
+
ok(
|
|
130
|
+
applied.output.includes("Deduped 2 packages, 2 copies merged:"),
|
|
131
|
+
applied.output,
|
|
132
|
+
);
|
|
133
|
+
ok(
|
|
134
|
+
applied.output.includes(
|
|
135
|
+
"barcode-detector: 2 versions (3.0.3, 3.2.2) -> 1 version (3.0.3)",
|
|
136
|
+
),
|
|
137
|
+
applied.output,
|
|
138
|
+
);
|
|
139
|
+
ok(applied.output.includes("No duplicate left."), applied.output);
|
|
140
|
+
|
|
141
|
+
// what matters is the lockfile pnpm was left with
|
|
142
|
+
strictEqual(readDuplicateSnapshot(lockPath(dir)).size, 0);
|
|
143
|
+
deepStrictEqual(resolutionsOf(dir, "barcode-detector"), [
|
|
144
|
+
"barcode-detector@3.0.3",
|
|
145
|
+
]);
|
|
146
|
+
// barcode-detector 3.2.2 pinned zxing-wasm at exactly 3.1.3; 3.0.3 asks for
|
|
147
|
+
// `^2.1.2`, so converging onto it has to bring the 2.x line back — a 3.1.3
|
|
148
|
+
// left standing would be a range the installed tree contradicts.
|
|
149
|
+
const zxing = resolutionsOf(dir, "zxing-wasm");
|
|
150
|
+
strictEqual(zxing.length, 1, zxing.join(", "));
|
|
151
|
+
ok(zxing[0]?.startsWith("zxing-wasm@2."), zxing.join(", "));
|
|
152
|
+
|
|
153
|
+
// no standing override is left behind, in a file otherwise untouched
|
|
154
|
+
strictEqual(
|
|
155
|
+
readFileSync(join(dir, "pnpm-workspace.yaml"), "utf8"),
|
|
156
|
+
workspaceYamlContent,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// and pnpm itself agrees, from the manifests alone
|
|
160
|
+
const frozen = install(dir, "--frozen-lockfile");
|
|
161
|
+
strictEqual(frozen.status, 0, frozen.output);
|
|
162
|
+
|
|
163
|
+
// pnpm's own account of the installed tree, not ours: one copy, reached
|
|
164
|
+
// from both dependents
|
|
165
|
+
const list = run(dir, "pnpm", ["list", "--depth", "Infinity"]);
|
|
166
|
+
strictEqual(list.status, 0, list.output);
|
|
167
|
+
ok(list.output.includes("barcode-detector@3.0.3 [deduped]"), list.output);
|
|
168
|
+
strictEqual(list.output.includes("barcode-detector@3.2.2"), false);
|
|
169
|
+
|
|
170
|
+
const check = dedupe(dir, "--check");
|
|
171
|
+
strictEqual(check.status, 0, check.output);
|
|
172
|
+
ok(check.output.includes("Nothing to dedupe."), check.output);
|
|
173
|
+
},
|
|
174
|
+
);
|
|
168
175
|
});
|
package/src/dedupePeers.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it } from "bun:test";
|
|
2
1
|
import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { buildIdentifiedFixesMap } from "pm-dedup-core";
|
|
5
5
|
import type { PackagesMap } from "./helpers/buildPnpmPackagesMap.ts";
|
package/src/displayMany.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ok, strictEqual } from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import type { ClusterFix } from "pm-dedup-core";
|
|
5
5
|
import { displayMany } from "./displayMany.ts";
|
|
@@ -86,7 +86,6 @@ const clusterFix = (overrides: Partial<ClusterFix> = {}): ClusterFix => ({
|
|
|
86
86
|
excludedMembers: [],
|
|
87
87
|
anchor: null,
|
|
88
88
|
reuseFixes: [],
|
|
89
|
-
floatingMembers: [],
|
|
90
89
|
workspaceChanges: [],
|
|
91
90
|
reResolutionSet: ["metro"],
|
|
92
91
|
externalConstraints: [],
|
|
@@ -128,7 +127,8 @@ describe("displayMany", () => {
|
|
|
128
127
|
});
|
|
129
128
|
|
|
130
129
|
it("reports no duplicates for a clean lockfile", () => {
|
|
131
|
-
|
|
130
|
+
strictEqual(
|
|
131
|
+
renderDuplicates("simple"),
|
|
132
132
|
"No duplicates found\n\nFound 1 dependency, 0 duplicates, 0 dedupable.\n",
|
|
133
133
|
);
|
|
134
134
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it } from "bun:test";
|
|
2
1
|
import { ok, strictEqual } from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
3
|
import { buildIdentifiedFixesMap } from "pm-dedup-core";
|
|
4
4
|
import type { PackageResolution, PackagesMap } from "./buildPnpmPackagesMap.ts";
|
|
5
5
|
import type { DependentsMap } from "./collectPnpmDependents.ts";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it } from "bun:test";
|
|
2
1
|
import { deepStrictEqual, strictEqual } from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { readPnpmLock } from "../readPnpmLock.ts";
|
|
5
5
|
import { collectDependentRanges } from "./collectDependentRanges.ts";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it } from "bun:test";
|
|
2
1
|
import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { readPnpmLock } from "../readPnpmLock.ts";
|
|
5
5
|
import { collectPnpmDependents } from "./collectPnpmDependents.ts";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { deepStrictEqual } from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { describeUnheldOverride } from "./describeUnheldOverride.ts";
|
|
4
|
+
|
|
5
|
+
describe("describeUnheldOverride", () => {
|
|
6
|
+
it("names a requester once, whatever its peer contexts", () => {
|
|
7
|
+
deepStrictEqual(
|
|
8
|
+
describeUnheldOverride(
|
|
9
|
+
{ packageName: "lightningcss", version: "1.30.1", reason: "converge" },
|
|
10
|
+
[
|
|
11
|
+
// two installations of one requester, one already repointed
|
|
12
|
+
{
|
|
13
|
+
key: "react-native-css@3.1.0-rc.0",
|
|
14
|
+
range: ">=1.27.0",
|
|
15
|
+
resolvedVersion: "1.30.1",
|
|
16
|
+
requesterName: "react-native-css",
|
|
17
|
+
peer: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
key: "react-native-css@3.1.0-rc.0",
|
|
21
|
+
range: ">=1.27.0",
|
|
22
|
+
resolvedVersion: "1.33.0",
|
|
23
|
+
requesterName: "react-native-css",
|
|
24
|
+
peer: true,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: "react-native-css@3.1.0-rc.0",
|
|
28
|
+
range: ">=1.27.0",
|
|
29
|
+
resolvedVersion: "1.33.0",
|
|
30
|
+
requesterName: "react-native-css",
|
|
31
|
+
peer: true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "vite@8.3.0",
|
|
35
|
+
range: "^1.33.0",
|
|
36
|
+
resolvedVersion: "1.33.0",
|
|
37
|
+
requesterName: "vite",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
),
|
|
41
|
+
[
|
|
42
|
+
'vite@8.3.0 requires "^1.33.0"',
|
|
43
|
+
'react-native-css@3.1.0-rc.0 requires ">=1.27.0" (peer), is provided 1.33.0',
|
|
44
|
+
],
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("lists the ranges that reject the version first", () => {
|
|
49
|
+
deepStrictEqual(
|
|
50
|
+
describeUnheldOverride(
|
|
51
|
+
{
|
|
52
|
+
packageName: "@commitlint/types",
|
|
53
|
+
version: "21.0.1",
|
|
54
|
+
reason: "converge",
|
|
55
|
+
},
|
|
56
|
+
[
|
|
57
|
+
{
|
|
58
|
+
key: "@commitlint/format@21.0.1",
|
|
59
|
+
range: "^21.0.1",
|
|
60
|
+
resolvedVersion: "21.1.0",
|
|
61
|
+
requesterName: "@commitlint/format",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: "@commitlint/rules@21.1.0",
|
|
65
|
+
range: "^21.1.0",
|
|
66
|
+
resolvedVersion: "21.1.0",
|
|
67
|
+
requesterName: "@commitlint/rules",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: "@pob/root@27.11.0",
|
|
71
|
+
range: "21.0.1",
|
|
72
|
+
resolvedVersion: "21.0.1",
|
|
73
|
+
requesterName: "@pob/root",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
),
|
|
77
|
+
[
|
|
78
|
+
'@commitlint/rules@21.1.0 requires "^21.1.0"',
|
|
79
|
+
'@commitlint/format@21.0.1 requires "^21.0.1", resolves 21.1.0 without an override',
|
|
80
|
+
],
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("names an importer by its manifest and dependency type", () => {
|
|
85
|
+
deepStrictEqual(
|
|
86
|
+
describeUnheldOverride(
|
|
87
|
+
{ packageName: "leaf", version: "2.0.0", reason: "converge" },
|
|
88
|
+
[
|
|
89
|
+
{
|
|
90
|
+
key: "package.json in devDependencies",
|
|
91
|
+
range: "1.0.0",
|
|
92
|
+
resolvedVersion: "1.0.0",
|
|
93
|
+
workspace: { path: ".", depType: "devDependencies" },
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
),
|
|
97
|
+
['package.json in devDependencies requires "1.0.0"'],
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PlannedOverride } from "pm-dedup-core";
|
|
2
|
+
import semver from "semver";
|
|
3
|
+
import type { DependentRange } from "./collectDependentRanges.ts";
|
|
4
|
+
|
|
5
|
+
const rejects = (version: string, range: string): boolean =>
|
|
6
|
+
semver.validRange(range) !== null &&
|
|
7
|
+
!semver.satisfies(version, range, { includePrerelease: true });
|
|
8
|
+
|
|
9
|
+
const describeDependent = (
|
|
10
|
+
dependent: DependentRange & { resolvedVersion: string },
|
|
11
|
+
version: string,
|
|
12
|
+
): string => {
|
|
13
|
+
const declared = `${dependent.key} requires "${dependent.range}"`;
|
|
14
|
+
if (rejects(version, dependent.range)) {
|
|
15
|
+
return dependent.peer ? `${declared} (peer)` : declared;
|
|
16
|
+
}
|
|
17
|
+
// pnpm provides a peer from the parent's context rather than resolving it
|
|
18
|
+
return dependent.peer
|
|
19
|
+
? `${declared} (peer), is provided ${dependent.resolvedVersion}`
|
|
20
|
+
: `${declared}, resolves ${dependent.resolvedVersion} without an override`;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Why an override did not hold, as one line per edge still resolving away from
|
|
25
|
+
* its version, the ranges that reject it first: those are what blocks the merge.
|
|
26
|
+
*
|
|
27
|
+
* Several peer contexts of one requester share a key, so lines are counted once.
|
|
28
|
+
*/
|
|
29
|
+
export const describeUnheldOverride = (
|
|
30
|
+
override: PlannedOverride,
|
|
31
|
+
dependents: DependentRange[],
|
|
32
|
+
): string[] => {
|
|
33
|
+
const offVersion = dependents.filter(
|
|
34
|
+
(dependent): dependent is DependentRange & { resolvedVersion: string } =>
|
|
35
|
+
dependent.resolvedVersion !== undefined &&
|
|
36
|
+
dependent.resolvedVersion !== override.version,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const blockersFirst = offVersion.toSorted(
|
|
40
|
+
(a, b) =>
|
|
41
|
+
Number(rejects(override.version, b.range)) -
|
|
42
|
+
Number(rejects(override.version, a.range)) ||
|
|
43
|
+
a.key.localeCompare(b.key),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return [
|
|
47
|
+
...new Set(
|
|
48
|
+
blockersFirst.map((dependent) =>
|
|
49
|
+
describeDependent(dependent, override.version),
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
];
|
|
53
|
+
};
|