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
package/src/dedupe.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
countDuplicatedPackages,
|
|
3
|
+
createPackageFilter,
|
|
4
|
+
diffVersionsSnapshots,
|
|
5
|
+
renderDedupeSummary,
|
|
6
|
+
} from "pm-dedup-core";
|
|
7
|
+
import type { PackageFilterOptions } from "pm-dedup-core";
|
|
8
|
+
import { applyClusterFixes } from "./applyClusterFixes.ts";
|
|
9
|
+
import { lockPathOf, resolvePnpmProjectDir } from "./helpers/projectDir.ts";
|
|
10
|
+
import { runPnpm } from "./helpers/runPnpm.ts";
|
|
11
|
+
import { readVersionsSnapshot } from "./helpers/versionsSnapshot.ts";
|
|
12
|
+
|
|
13
|
+
// `--dry-run` and `--check` print the same plan and write nothing; only the exit
|
|
14
|
+
// code differs, so `--check` can gate CI while `--dry-run` stays informational.
|
|
15
|
+
export type DedupeMode = "apply" | "check" | "dry-run";
|
|
16
|
+
|
|
17
|
+
export interface DedupeOptions {
|
|
18
|
+
mode?: DedupeMode;
|
|
19
|
+
// false makes the cluster pass write plain overrides instead of convergence
|
|
20
|
+
// ones, forcing every requester onto the version rather than only the ones
|
|
21
|
+
// whose range accepts it
|
|
22
|
+
convergenceOverrides?: boolean;
|
|
23
|
+
// restricts which packages may be touched, for deduplicating a large lockfile
|
|
24
|
+
// a family at a time
|
|
25
|
+
filter?: PackageFilterOptions;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const residualsMessage = "`pnpm dedupe` would also change the lockfile.";
|
|
29
|
+
|
|
30
|
+
// `pnpm dedupe` merges everything it can and knows nothing of the filter, so
|
|
31
|
+
// running it would undo the point of a filtered run.
|
|
32
|
+
const filteredResidualsMessage =
|
|
33
|
+
"`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
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `pnpm dedupe` only merges what it can resolve to a single version on its own:
|
|
37
|
+
* it never widens a workspace range, and it never repoints an edge that resolved
|
|
38
|
+
* past a version the family already carries. Cluster fixes cover exactly that
|
|
39
|
+
* gap, so they run first and `pnpm dedupe` finishes the residuals.
|
|
40
|
+
*/
|
|
41
|
+
export function dedupe({
|
|
42
|
+
mode = "apply",
|
|
43
|
+
convergenceOverrides = true,
|
|
44
|
+
filter,
|
|
45
|
+
}: DedupeOptions = {}): void {
|
|
46
|
+
const dryRun = mode !== "apply";
|
|
47
|
+
const projectDir = resolvePnpmProjectDir();
|
|
48
|
+
if (projectDir === null) return;
|
|
49
|
+
const lockPath = lockPathOf(projectDir);
|
|
50
|
+
const filtered = !createPackageFilter(filter).selectsEverything;
|
|
51
|
+
|
|
52
|
+
// Read before anything runs: both the cluster pass and `pnpm dedupe` rewrite
|
|
53
|
+
// the lockfile, and the summary compares the whole run against this.
|
|
54
|
+
const before = readVersionsSnapshot(lockPath);
|
|
55
|
+
|
|
56
|
+
// What the run merged, reported once the lockfile has settled — `pnpm dedupe`
|
|
57
|
+
// included, since it is part of what this command does.
|
|
58
|
+
const summarize = (): void => {
|
|
59
|
+
const after = readVersionsSnapshot(lockPath);
|
|
60
|
+
renderDedupeSummary({
|
|
61
|
+
deduped: diffVersionsSnapshots(before, after),
|
|
62
|
+
remainingDuplicates: countDuplicatedPackages(after),
|
|
63
|
+
whyCommand: "pnpm-why-duplicate",
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Read-only, so it can run before the cluster pass. It has to, for a dry run:
|
|
68
|
+
// the plan report names the residuals, and only this knows about them. A
|
|
69
|
+
// filtered run does not run `pnpm dedupe` at all, so what it would change is
|
|
70
|
+
// not this command's business.
|
|
71
|
+
const probe =
|
|
72
|
+
dryRun && !filtered
|
|
73
|
+
? runPnpm(["dedupe", "--check"], { cwd: projectDir })
|
|
74
|
+
: null;
|
|
75
|
+
|
|
76
|
+
const residuals = ((): string | undefined => {
|
|
77
|
+
if (filtered) return filteredResidualsMessage;
|
|
78
|
+
if (probe !== null && probe.status !== 0) return residualsMessage;
|
|
79
|
+
return undefined;
|
|
80
|
+
})();
|
|
81
|
+
|
|
82
|
+
const outcome = applyClusterFixes({
|
|
83
|
+
projectDir,
|
|
84
|
+
dryRun,
|
|
85
|
+
convergenceOverrides,
|
|
86
|
+
filter,
|
|
87
|
+
packageManagerResiduals: residuals,
|
|
88
|
+
});
|
|
89
|
+
|
|
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
|
+
if (mode === "check") {
|
|
97
|
+
process.exitCode =
|
|
98
|
+
outcome.plannedChangeCount > 0 || (probe !== null && probe.status !== 0)
|
|
99
|
+
? 1
|
|
100
|
+
: 0;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (mode === "dry-run") return;
|
|
104
|
+
|
|
105
|
+
if (filtered) {
|
|
106
|
+
// Applying an override needs a real resolution, and `pnpm dedupe` is the
|
|
107
|
+
// only one that performs it — so a filtered run that changed something has
|
|
108
|
+
// already merged whatever else pnpm could reach on its own.
|
|
109
|
+
if (outcome.status === "applied" || outcome.status === "kept-overrides") {
|
|
110
|
+
console.log(
|
|
111
|
+
"`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
|
+
);
|
|
113
|
+
}
|
|
114
|
+
console.log(filteredResidualsMessage);
|
|
115
|
+
summarize();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = runPnpm(["dedupe"], { cwd: projectDir });
|
|
120
|
+
if (result.status !== null) {
|
|
121
|
+
process.exitCode = result.status;
|
|
122
|
+
}
|
|
123
|
+
summarize();
|
|
124
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { describe, it } from "bun:test";
|
|
2
|
+
import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { buildIdentifiedFixesMap } from "pm-dedup-core";
|
|
8
|
+
import {
|
|
9
|
+
buildPnpmPackagesMap,
|
|
10
|
+
collectPnpmDependents,
|
|
11
|
+
filterDuplicatesPnpmPackagesMap,
|
|
12
|
+
parsePnpmLockPackages,
|
|
13
|
+
readPnpmLock,
|
|
14
|
+
} from "./index.ts";
|
|
15
|
+
|
|
16
|
+
// These tests shell out to the real `pnpm` and require either network access or
|
|
17
|
+
// a warm pnpm store; they are skipped when pnpm is not on PATH. `--lockfile-only`
|
|
18
|
+
// keeps pnpm from writing node_modules, and `--frozen-lockfile` / `--check` never
|
|
19
|
+
// rewrite the lockfile, so pnpm runs against the committed fixtures in place
|
|
20
|
+
// without modifying them (asserted below). `pnpm dedupe --check` only flags the
|
|
21
|
+
// safely-mergeable subset of duplicates, so the invariant is: everything pnpm
|
|
22
|
+
// would dedupe is also reported by our listing.
|
|
23
|
+
|
|
24
|
+
const pnpmAvailable =
|
|
25
|
+
spawnSync("pnpm", ["--version"], { encoding: "utf8" }).status === 0;
|
|
26
|
+
|
|
27
|
+
const fixturePath = (scenario: string): string =>
|
|
28
|
+
fileURLToPath(new URL(`../test/fixtures/${scenario}`, import.meta.url));
|
|
29
|
+
|
|
30
|
+
interface PnpmRun {
|
|
31
|
+
status: number | null;
|
|
32
|
+
output: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const runPnpm = (cwd: string, args: string[]): PnpmRun => {
|
|
36
|
+
const result = spawnSync(
|
|
37
|
+
"pnpm",
|
|
38
|
+
[...args, "--lockfile-only", "--prefer-offline", "--ignore-scripts"],
|
|
39
|
+
{ cwd, encoding: "utf8", timeout: 120_000 },
|
|
40
|
+
);
|
|
41
|
+
if (result.error) {
|
|
42
|
+
throw result.error;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
status: result.status,
|
|
46
|
+
output: `${result.stdout}\n${result.stderr}`,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// `pnpm dedupe --check` prints a tree of changes; each merged dependency is on a
|
|
51
|
+
// branch line `├── name fromVersion → toVersion`. We collect those names.
|
|
52
|
+
const parseDedupedPackages = (output: string): string[] => {
|
|
53
|
+
const names = new Set<string>();
|
|
54
|
+
for (const line of output.split("\n")) {
|
|
55
|
+
if (!line.includes("→")) continue;
|
|
56
|
+
const name = line
|
|
57
|
+
.replace(/^[\s│├└─]+/u, "")
|
|
58
|
+
.trim()
|
|
59
|
+
.split(/\s+/u)[0];
|
|
60
|
+
if (name) {
|
|
61
|
+
names.add(name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return [...names].toSorted();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const duplicateNames = (scenario: string): string[] => {
|
|
68
|
+
const lock = readPnpmLock(join(fixturePath(scenario), "pnpm-lock.yaml"));
|
|
69
|
+
const duplicates = filterDuplicatesPnpmPackagesMap(
|
|
70
|
+
buildPnpmPackagesMap(parsePnpmLockPackages(lock)),
|
|
71
|
+
);
|
|
72
|
+
return Object.keys(duplicates).toSorted();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const fixTargets = (scenario: string, packageName: string): string[] => {
|
|
76
|
+
const lock = readPnpmLock(join(fixturePath(scenario), "pnpm-lock.yaml"));
|
|
77
|
+
const duplicates = filterDuplicatesPnpmPackagesMap(
|
|
78
|
+
buildPnpmPackagesMap(parsePnpmLockPackages(lock)),
|
|
79
|
+
);
|
|
80
|
+
const fixes = buildIdentifiedFixesMap(
|
|
81
|
+
duplicates,
|
|
82
|
+
collectPnpmDependents(lock, Object.keys(duplicates)),
|
|
83
|
+
);
|
|
84
|
+
return (fixes.get(packageName) ?? []).map((fix) => fix.to);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const lockContent = (dir: string): string =>
|
|
88
|
+
readFileSync(join(dir, "pnpm-lock.yaml"), "utf8");
|
|
89
|
+
|
|
90
|
+
// Defensive: `--lockfile-only` should never create node_modules, but if a future
|
|
91
|
+
// pnpm version does, do not leave it in the committed fixture directory.
|
|
92
|
+
const assertPristine = (dir: string, lockBefore: string): void => {
|
|
93
|
+
strictEqual(
|
|
94
|
+
lockContent(dir),
|
|
95
|
+
lockBefore,
|
|
96
|
+
"pnpm must not change the lockfile",
|
|
97
|
+
);
|
|
98
|
+
rmSync(join(dir, "node_modules"), { recursive: true, force: true });
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const suite = pnpmAvailable ? describe : describe.skip;
|
|
102
|
+
|
|
103
|
+
suite("pnpm dedupe --check vs listDuplicates", () => {
|
|
104
|
+
// `duplicated-typescript-eslint-dedupe-peers` is the same dependency with
|
|
105
|
+
// `dedupePeers: true`, which flattens the peer suffixes in the lockfile: pnpm
|
|
106
|
+
// must still merge the same packages, and we must still list them.
|
|
107
|
+
const mergeable: { scenario: string; expectedFlagged: string[] }[] = [
|
|
108
|
+
{
|
|
109
|
+
scenario: "duplicated-typescript-eslint",
|
|
110
|
+
expectedFlagged: [
|
|
111
|
+
"@typescript-eslint/tsconfig-utils",
|
|
112
|
+
"@typescript-eslint/types",
|
|
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.
|
|
157
|
+
// `wildcard-not-reused` is the widest case: a `*` range resolved to 0.87.0
|
|
158
|
+
// instead of the installed 0.84.5, duplicating the metro family (see
|
|
159
|
+
// wildcardNotReused.test.ts). pnpm will not undo it either.
|
|
160
|
+
const unmergeableByPnpm: {
|
|
161
|
+
scenario: string;
|
|
162
|
+
expectedDuplicate: string;
|
|
163
|
+
expectedFixTargets?: string[];
|
|
164
|
+
}[] = [
|
|
165
|
+
{
|
|
166
|
+
scenario: "duplicated-babel-frame",
|
|
167
|
+
expectedDuplicate: "@babel/code-frame",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
scenario: "duplicated-printable-shell-command",
|
|
171
|
+
expectedDuplicate: "printable-shell-command",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
scenario: "mergeable-alias",
|
|
175
|
+
expectedDuplicate: "printable-shell-command",
|
|
176
|
+
expectedFixTargets: ["printable-shell-command@5.0.7"],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
scenario: "mergeable-alias-dedupe-peers",
|
|
180
|
+
expectedDuplicate: "printable-shell-command",
|
|
181
|
+
expectedFixTargets: ["printable-shell-command@5.0.7"],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
scenario: "wildcard-not-reused",
|
|
185
|
+
expectedDuplicate: "metro-config",
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
for (const {
|
|
190
|
+
scenario,
|
|
191
|
+
expectedDuplicate,
|
|
192
|
+
expectedFixTargets,
|
|
193
|
+
} of unmergeableByPnpm) {
|
|
194
|
+
it(`flags nothing for ${scenario} but still lists ${expectedDuplicate}`, () => {
|
|
195
|
+
const dir = fixturePath(scenario);
|
|
196
|
+
const lockBefore = lockContent(dir);
|
|
197
|
+
|
|
198
|
+
const install = runPnpm(dir, ["install", "--frozen-lockfile"]);
|
|
199
|
+
strictEqual(install.status, 0, install.output);
|
|
200
|
+
assertPristine(dir, lockBefore);
|
|
201
|
+
|
|
202
|
+
const check = runPnpm(dir, ["dedupe", "--check"]);
|
|
203
|
+
strictEqual(
|
|
204
|
+
check.status,
|
|
205
|
+
0,
|
|
206
|
+
`dedupe --check should find nothing to merge\n${check.output}`,
|
|
207
|
+
);
|
|
208
|
+
assertPristine(dir, lockBefore);
|
|
209
|
+
|
|
210
|
+
deepStrictEqual(parseDedupedPackages(check.output), []);
|
|
211
|
+
ok(duplicateNames(scenario).includes(expectedDuplicate));
|
|
212
|
+
deepStrictEqual(
|
|
213
|
+
fixTargets(scenario, expectedDuplicate),
|
|
214
|
+
expectedFixTargets ?? [],
|
|
215
|
+
);
|
|
216
|
+
}, 180_000);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { afterEach, describe, it } from "bun:test";
|
|
2
|
+
import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { buildPnpmPackagesMap } from "./helpers/buildPnpmPackagesMap.ts";
|
|
8
|
+
import { readDuplicateSnapshot } from "./helpers/duplicateSnapshot.ts";
|
|
9
|
+
import { parsePnpmLockPackages } from "./helpers/parsePnpmLockPackages.ts";
|
|
10
|
+
import { createTempProjects } from "./helpers/tempProjects.ts";
|
|
11
|
+
import { readPnpmLock } from "./readPnpmLock.ts";
|
|
12
|
+
|
|
13
|
+
// The shipped tool against a real pnpm: a copy of the
|
|
14
|
+
// `exact-pin-forces-downgrade` fixture, a real `pnpm install`, then the built
|
|
15
|
+
// `pnpm-dedupe` bin as a user would run it. Every other test stubs the
|
|
16
|
+
// resolution step, so this is the only one that can tell whether pnpm holds the
|
|
17
|
+
// deduplicated result on its own once the transient override is removed again —
|
|
18
|
+
// the step the tool takes before reporting "applied" — and pnpm is asked for its
|
|
19
|
+
// own account of the tree at the end rather than only our reading of it.
|
|
20
|
+
//
|
|
21
|
+
// Requires network access or a warm pnpm store; skipped when pnpm is not on PATH.
|
|
22
|
+
|
|
23
|
+
const pnpmAvailable =
|
|
24
|
+
spawnSync("pnpm", ["--version"], { encoding: "utf8" }).status === 0;
|
|
25
|
+
|
|
26
|
+
const fixtureDir = fileURLToPath(
|
|
27
|
+
new URL("../test/fixtures/exact-pin-forces-downgrade", import.meta.url),
|
|
28
|
+
);
|
|
29
|
+
// the built bin, the one the `bin` field points at: from `src` it is one level
|
|
30
|
+
// up, and the compiled copy of this file sits beside it
|
|
31
|
+
const dedupeBin = ((): string => {
|
|
32
|
+
const built = ["../dist/bin/pnpm-dedupe.js", "./bin/pnpm-dedupe.js"]
|
|
33
|
+
.map((binPath) => fileURLToPath(new URL(binPath, import.meta.url)))
|
|
34
|
+
.find((binPath) => existsSync(binPath));
|
|
35
|
+
if (!built) {
|
|
36
|
+
throw new Error("pnpm-dedupe is not built: run `bun run build` first");
|
|
37
|
+
}
|
|
38
|
+
return built;
|
|
39
|
+
})();
|
|
40
|
+
|
|
41
|
+
// expo-camera's peers (expo, react-native) would drag a few hundred packages in
|
|
42
|
+
// and duplicates of their own, none of which this fixture is about.
|
|
43
|
+
const workspaceYamlContent = "autoInstallPeers: false\n";
|
|
44
|
+
|
|
45
|
+
interface Run {
|
|
46
|
+
status: number | null;
|
|
47
|
+
output: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const projects = createTempProjects("pnpm-dedup-e2e-");
|
|
51
|
+
|
|
52
|
+
afterEach(projects.cleanup);
|
|
53
|
+
|
|
54
|
+
const run = (cwd: string, command: string, args: string[]): Run => {
|
|
55
|
+
const result = spawnSync(command, args, {
|
|
56
|
+
cwd,
|
|
57
|
+
encoding: "utf8",
|
|
58
|
+
timeout: 120_000,
|
|
59
|
+
});
|
|
60
|
+
if (result.error) {
|
|
61
|
+
throw result.error;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
status: result.status,
|
|
65
|
+
output: `${result.stdout}\n${result.stderr}`,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const install = (cwd: string, ...args: string[]): Run =>
|
|
70
|
+
run(cwd, "pnpm", [
|
|
71
|
+
"install",
|
|
72
|
+
"--prefer-offline",
|
|
73
|
+
"--ignore-scripts",
|
|
74
|
+
...args,
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
// node, as the bin's own shebang asks for
|
|
78
|
+
const dedupe = (cwd: string, ...args: string[]): Run =>
|
|
79
|
+
run(cwd, "node", [dedupeBin, ...args]);
|
|
80
|
+
|
|
81
|
+
const lockPath = (dir: string): string => join(dir, "pnpm-lock.yaml");
|
|
82
|
+
|
|
83
|
+
const resolutionsOf = (dir: string, packageName: string): string[] => {
|
|
84
|
+
const lock = readPnpmLock(lockPath(dir));
|
|
85
|
+
const packagesMap = buildPnpmPackagesMap(parsePnpmLockPackages(lock));
|
|
86
|
+
return (packagesMap[packageName] ?? []).map(({ resolution }) => resolution);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const suite = pnpmAvailable ? describe : describe.skip;
|
|
90
|
+
|
|
91
|
+
suite("pnpm-dedupe end to end", () => {
|
|
92
|
+
it("converges the cluster and leaves pnpm holding it without an override", () => {
|
|
93
|
+
const dir = projects.create();
|
|
94
|
+
// only the manifest: the lockfile has to be the real resolution pnpm writes
|
|
95
|
+
// here, not the trimmed one the unit-test fixture commits
|
|
96
|
+
cpSync(join(fixtureDir, "package.json"), join(dir, "package.json"));
|
|
97
|
+
writeFileSync(join(dir, "pnpm-workspace.yaml"), workspaceYamlContent);
|
|
98
|
+
|
|
99
|
+
const installed = install(dir);
|
|
100
|
+
strictEqual(installed.status, 0, installed.output);
|
|
101
|
+
|
|
102
|
+
// `@yudiel/react-qr-scanner` pins barcode-detector at exactly 3.0.3 while
|
|
103
|
+
// expo-camera's `^3.0.0` resolved to 3.2.2, and the pair drags zxing-wasm
|
|
104
|
+
// along: four resolutions for two packages.
|
|
105
|
+
const before = readDuplicateSnapshot(lockPath(dir));
|
|
106
|
+
strictEqual(before.size, 4, [...before].join(", "));
|
|
107
|
+
ok(before.has("barcode-detector@3.0.3"));
|
|
108
|
+
ok(before.has("barcode-detector@3.2.2"));
|
|
109
|
+
|
|
110
|
+
const applied = dedupe(dir);
|
|
111
|
+
strictEqual(applied.status, 0, applied.output);
|
|
112
|
+
|
|
113
|
+
// the tool's own verdict: the convergence override went in, came back out,
|
|
114
|
+
// and the result survived the re-resolution without it
|
|
115
|
+
ok(applied.output.includes('"barcode-detector@": "3.0.3"'), applied.output);
|
|
116
|
+
ok(
|
|
117
|
+
applied.output.includes(
|
|
118
|
+
"Removing the overrides and re-resolving to check the result holds",
|
|
119
|
+
),
|
|
120
|
+
applied.output,
|
|
121
|
+
);
|
|
122
|
+
// what it deduped, named: two packages, one copy of each merged away
|
|
123
|
+
ok(
|
|
124
|
+
applied.output.includes("Deduped 2 packages, 2 copies merged:"),
|
|
125
|
+
applied.output,
|
|
126
|
+
);
|
|
127
|
+
ok(
|
|
128
|
+
applied.output.includes(
|
|
129
|
+
"barcode-detector: 2 versions (3.0.3, 3.2.2) -> 1 version (3.0.3)",
|
|
130
|
+
),
|
|
131
|
+
applied.output,
|
|
132
|
+
);
|
|
133
|
+
ok(applied.output.includes("No duplicate left."), applied.output);
|
|
134
|
+
|
|
135
|
+
// what matters is the lockfile pnpm was left with
|
|
136
|
+
strictEqual(readDuplicateSnapshot(lockPath(dir)).size, 0);
|
|
137
|
+
deepStrictEqual(resolutionsOf(dir, "barcode-detector"), [
|
|
138
|
+
"barcode-detector@3.0.3",
|
|
139
|
+
]);
|
|
140
|
+
// barcode-detector 3.2.2 pinned zxing-wasm at exactly 3.1.3; 3.0.3 asks for
|
|
141
|
+
// `^2.1.2`, so converging onto it has to bring the 2.x line back — a 3.1.3
|
|
142
|
+
// left standing would be a range the installed tree contradicts.
|
|
143
|
+
const zxing = resolutionsOf(dir, "zxing-wasm");
|
|
144
|
+
strictEqual(zxing.length, 1, zxing.join(", "));
|
|
145
|
+
ok(zxing[0]?.startsWith("zxing-wasm@2."), zxing.join(", "));
|
|
146
|
+
|
|
147
|
+
// no standing override is left behind, in a file otherwise untouched
|
|
148
|
+
strictEqual(
|
|
149
|
+
readFileSync(join(dir, "pnpm-workspace.yaml"), "utf8"),
|
|
150
|
+
workspaceYamlContent,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// and pnpm itself agrees, from the manifests alone
|
|
154
|
+
const frozen = install(dir, "--frozen-lockfile");
|
|
155
|
+
strictEqual(frozen.status, 0, frozen.output);
|
|
156
|
+
|
|
157
|
+
// pnpm's own account of the installed tree, not ours: one copy, reached
|
|
158
|
+
// from both dependents
|
|
159
|
+
const list = run(dir, "pnpm", ["list", "--depth", "Infinity"]);
|
|
160
|
+
strictEqual(list.status, 0, list.output);
|
|
161
|
+
ok(list.output.includes("barcode-detector@3.0.3 [deduped]"), list.output);
|
|
162
|
+
strictEqual(list.output.includes("barcode-detector@3.2.2"), false);
|
|
163
|
+
|
|
164
|
+
const check = dedupe(dir, "--check");
|
|
165
|
+
strictEqual(check.status, 0, check.output);
|
|
166
|
+
ok(check.output.includes("Nothing to dedupe."), check.output);
|
|
167
|
+
}, 180_000);
|
|
168
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it } from "bun:test";
|
|
2
|
+
import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { buildIdentifiedFixesMap } from "pm-dedup-core";
|
|
5
|
+
import type { PackagesMap } from "./helpers/buildPnpmPackagesMap.ts";
|
|
6
|
+
import { parsePackageId } from "./helpers/parsePnpmLockPackages.ts";
|
|
7
|
+
import {
|
|
8
|
+
buildPnpmPackagesMap,
|
|
9
|
+
collectPnpmDependents,
|
|
10
|
+
filterDuplicatesPnpmPackagesMap,
|
|
11
|
+
parsePnpmLockPackages,
|
|
12
|
+
readPnpmLock,
|
|
13
|
+
} from "./index.ts";
|
|
14
|
+
|
|
15
|
+
// Both fixtures declare the same single dependency; only the `dedupePeers`
|
|
16
|
+
// setting differs. With it enabled pnpm writes peer suffixes as plain
|
|
17
|
+
// `name@version` instead of repeating the peer's own resolution, so snapshot
|
|
18
|
+
// ids lose their nesting:
|
|
19
|
+
// default: pkg@1(peer@2(eslint@10)(typescript@6))(eslint@10)(typescript@6)
|
|
20
|
+
// dedupePeers: pkg@1(peer@2)(eslint@10)(typescript@6)
|
|
21
|
+
const defaultScenario = "duplicated-typescript-eslint";
|
|
22
|
+
const dedupePeersScenario = "duplicated-typescript-eslint-dedupe-peers";
|
|
23
|
+
|
|
24
|
+
// `mergeable-alias` pins the same package twice, once through an npm alias
|
|
25
|
+
// (`npm:printable-shell-command@5.0.7`) and once through a range that also
|
|
26
|
+
// accepts that pin. It is the same pair: identical manifests, `dedupePeers`
|
|
27
|
+
// only set in `mergeable-alias-dedupe-peers`.
|
|
28
|
+
const mergeableDefaultScenario = "mergeable-alias";
|
|
29
|
+
const mergeableDedupePeersScenario = "mergeable-alias-dedupe-peers";
|
|
30
|
+
|
|
31
|
+
const lockFor = (scenario: string) =>
|
|
32
|
+
readPnpmLock(
|
|
33
|
+
fileURLToPath(
|
|
34
|
+
new URL(`../test/fixtures/${scenario}/pnpm-lock.yaml`, import.meta.url),
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const maxParenDepth = (id: string): number => {
|
|
39
|
+
let depth = 0;
|
|
40
|
+
let max = 0;
|
|
41
|
+
for (const char of id) {
|
|
42
|
+
if (char === "(") {
|
|
43
|
+
depth += 1;
|
|
44
|
+
max = Math.max(max, depth);
|
|
45
|
+
} else if (char === ")") {
|
|
46
|
+
depth -= 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return max;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const snapshotIds = (scenario: string): string[] =>
|
|
53
|
+
Object.keys(lockFor(scenario).snapshots ?? {});
|
|
54
|
+
|
|
55
|
+
const duplicatesFor = (scenario: string): PackagesMap =>
|
|
56
|
+
filterDuplicatesPnpmPackagesMap(
|
|
57
|
+
buildPnpmPackagesMap(parsePnpmLockPackages(lockFor(scenario))),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const fixesFor = (
|
|
61
|
+
scenario: string,
|
|
62
|
+
): ReturnType<typeof buildIdentifiedFixesMap> => {
|
|
63
|
+
const lock = lockFor(scenario);
|
|
64
|
+
const duplicates = duplicatesFor(scenario);
|
|
65
|
+
return buildIdentifiedFixesMap(
|
|
66
|
+
duplicates,
|
|
67
|
+
collectPnpmDependents(lock, Object.keys(duplicates)),
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
describe("dedupePeers lockfiles", () => {
|
|
72
|
+
it("uses a fixture generated with the setting enabled", () => {
|
|
73
|
+
strictEqual(lockFor(dedupePeersScenario).settings?.dedupePeers, true);
|
|
74
|
+
strictEqual(lockFor(defaultScenario).settings?.dedupePeers, undefined);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("flattens the peer suffixes the default settings nest", () => {
|
|
78
|
+
ok(
|
|
79
|
+
snapshotIds(defaultScenario).some((id) => maxParenDepth(id) > 1),
|
|
80
|
+
"the default-settings fixture should contain nested peer suffixes",
|
|
81
|
+
);
|
|
82
|
+
deepStrictEqual(
|
|
83
|
+
snapshotIds(dedupePeersScenario).filter((id) => maxParenDepth(id) > 1),
|
|
84
|
+
[],
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("parses name and version out of a flattened peer suffix", () => {
|
|
89
|
+
const suffixed = snapshotIds(dedupePeersScenario).filter((id) =>
|
|
90
|
+
id.includes("("),
|
|
91
|
+
);
|
|
92
|
+
ok(suffixed.length > 0);
|
|
93
|
+
for (const id of suffixed) {
|
|
94
|
+
const { name, version } = parsePackageId(id);
|
|
95
|
+
ok(!name.includes("("), `name should not keep the suffix: ${id}`);
|
|
96
|
+
ok(/^\d/u.test(version), `version should be resolved: ${id}`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("groups every peer context of a resolution into installations", () => {
|
|
101
|
+
const { packages, installationsByResolution } = parsePnpmLockPackages(
|
|
102
|
+
lockFor(dedupePeersScenario),
|
|
103
|
+
);
|
|
104
|
+
const utils = installationsByResolution.get(
|
|
105
|
+
"@typescript-eslint/utils@8.67.0",
|
|
106
|
+
);
|
|
107
|
+
ok(utils);
|
|
108
|
+
ok(utils.every((id) => id.startsWith("@typescript-eslint/utils@8.67.0")));
|
|
109
|
+
strictEqual(
|
|
110
|
+
[...installationsByResolution.keys()].every((resolution) =>
|
|
111
|
+
packages.has(resolution),
|
|
112
|
+
),
|
|
113
|
+
true,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("reports the same duplicated packages as the default-settings fixture", () => {
|
|
118
|
+
deepStrictEqual(
|
|
119
|
+
Object.keys(duplicatesFor(dedupePeersScenario)).toSorted(),
|
|
120
|
+
Object.keys(duplicatesFor(defaultScenario)).toSorted(),
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("resolves dependents for every duplicate, so fixes can be identified", () => {
|
|
125
|
+
const lock = lockFor(dedupePeersScenario);
|
|
126
|
+
const duplicates = duplicatesFor(dedupePeersScenario);
|
|
127
|
+
const packageNames = Object.keys(duplicates);
|
|
128
|
+
const dependents = collectPnpmDependents(lock, packageNames);
|
|
129
|
+
|
|
130
|
+
for (const name of packageNames) {
|
|
131
|
+
ok(dependents.get(name)?.length, `expected dependents for ${name}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// identifyResolutionFixes throws on a missing dependent, which is what a
|
|
135
|
+
// peer-suffix parsing regression would cause here.
|
|
136
|
+
const fixes = buildIdentifiedFixesMap(duplicates, dependents);
|
|
137
|
+
deepStrictEqual([...fixes.keys()].toSorted(), packageNames.toSorted());
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("dedupePeers with an identified fix", () => {
|
|
142
|
+
it("flattens the peer suffixes of the pinned pair too", () => {
|
|
143
|
+
ok(
|
|
144
|
+
snapshotIds(mergeableDefaultScenario).some((id) => maxParenDepth(id) > 1),
|
|
145
|
+
);
|
|
146
|
+
deepStrictEqual(
|
|
147
|
+
snapshotIds(mergeableDedupePeersScenario).filter(
|
|
148
|
+
(id) => maxParenDepth(id) > 1,
|
|
149
|
+
),
|
|
150
|
+
[],
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("identifies the version every declared range accepts", () => {
|
|
155
|
+
deepStrictEqual(
|
|
156
|
+
fixesFor(mergeableDedupePeersScenario).get("printable-shell-command"),
|
|
157
|
+
[
|
|
158
|
+
{
|
|
159
|
+
mergeableResolutions: [
|
|
160
|
+
"printable-shell-command@5.0.7",
|
|
161
|
+
"printable-shell-command@5.3.1",
|
|
162
|
+
],
|
|
163
|
+
to: "printable-shell-command@5.0.7",
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("identifies the same fixes as the default-settings fixture", () => {
|
|
170
|
+
deepStrictEqual(
|
|
171
|
+
[...fixesFor(mergeableDedupePeersScenario)],
|
|
172
|
+
[...fixesFor(mergeableDefaultScenario)],
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
});
|