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,311 @@
|
|
|
1
|
+
import { describe, it } from "bun:test";
|
|
2
|
+
import { deepStrictEqual, strictEqual } from "node:assert/strict";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { readPnpmLock } from "../readPnpmLock.ts";
|
|
5
|
+
import { collectDependentRanges } from "./collectDependentRanges.ts";
|
|
6
|
+
import type { ManifestReader } from "./readInstalledManifest.ts";
|
|
7
|
+
import { createManifestReader } from "./readInstalledManifest.ts";
|
|
8
|
+
|
|
9
|
+
const fixturesBase = (rel: string) =>
|
|
10
|
+
fileURLToPath(new URL(rel, import.meta.url));
|
|
11
|
+
|
|
12
|
+
const lockFor = (scenario: string) =>
|
|
13
|
+
readPnpmLock(fixturesBase(`../../test/fixtures/${scenario}/pnpm-lock.yaml`));
|
|
14
|
+
|
|
15
|
+
const rangesFor = (
|
|
16
|
+
scenario: string,
|
|
17
|
+
packageNames: string[],
|
|
18
|
+
readManifest: ManifestReader = () => undefined,
|
|
19
|
+
) =>
|
|
20
|
+
collectDependentRanges(
|
|
21
|
+
lockFor(scenario),
|
|
22
|
+
new Set(packageNames),
|
|
23
|
+
readManifest,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// In `wildcard-not-reused`, `@tamagui/metro-plugin@1.139.4` declares
|
|
27
|
+
// `metro-config: "*"` and `metro-transform-worker: "*"`. The lockfile only
|
|
28
|
+
// stores the version pnpm picked (0.87.0), so the wildcard is only visible
|
|
29
|
+
// through the installed manifest.
|
|
30
|
+
const wildcardScenario = "wildcard-not-reused";
|
|
31
|
+
const wildcardPackages = ["metro-config", "metro-transform-worker"];
|
|
32
|
+
const wildcardDependent = "@tamagui/metro-plugin@1.139.4";
|
|
33
|
+
|
|
34
|
+
const readWildcardManifest: ManifestReader = (name, version) =>
|
|
35
|
+
name === "@tamagui/metro-plugin" && version === "1.139.4"
|
|
36
|
+
? { dependencies: { "metro-config": "*", "metro-transform-worker": "*" } }
|
|
37
|
+
: undefined;
|
|
38
|
+
|
|
39
|
+
// `exact-pin-forces-downgrade` commits its virtual store, so the ranges come
|
|
40
|
+
// from the real reader: `@yudiel/react-qr-scanner` pins `barcode-detector`
|
|
41
|
+
// exactly at 3.0.3 while `expo-camera` declares `^3.0.0` and resolved 3.2.2.
|
|
42
|
+
const downgradeScenario = "exact-pin-forces-downgrade";
|
|
43
|
+
const readDowngradeManifest = createManifestReader(
|
|
44
|
+
fixturesBase(`../../test/fixtures/${downgradeScenario}`),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const rangeOf = (
|
|
48
|
+
ranges: ReturnType<typeof collectDependentRanges>,
|
|
49
|
+
packageName: string,
|
|
50
|
+
dependentKey: string,
|
|
51
|
+
): string | undefined =>
|
|
52
|
+
ranges.get(packageName)?.find((dependent) => dependent.key === dependentKey)
|
|
53
|
+
?.range;
|
|
54
|
+
|
|
55
|
+
describe("collectDependentRanges", () => {
|
|
56
|
+
it("takes a direct dependency's range from the importer specifier", () => {
|
|
57
|
+
deepStrictEqual(rangesFor("simple", ["semver"]).get("semver"), [
|
|
58
|
+
{
|
|
59
|
+
key: "package.json in dependencies",
|
|
60
|
+
range: "7.7.3",
|
|
61
|
+
resolvedVersion: "7.7.3",
|
|
62
|
+
workspace: { path: ".", depType: "dependencies" },
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("ignores packages that are not in the requested set", () => {
|
|
68
|
+
deepStrictEqual(
|
|
69
|
+
[...rangesFor("duplicated-babel-frame", ["@babel/code-frame"]).keys()],
|
|
70
|
+
["@babel/code-frame"],
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("reads a transitive dependent's declared range from its manifest", () => {
|
|
75
|
+
const ranges = rangesFor(
|
|
76
|
+
wildcardScenario,
|
|
77
|
+
wildcardPackages,
|
|
78
|
+
readWildcardManifest,
|
|
79
|
+
);
|
|
80
|
+
for (const name of wildcardPackages) {
|
|
81
|
+
strictEqual(rangeOf(ranges, name, wildcardDependent), "*");
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("falls back to the resolved version when the manifest is unavailable", () => {
|
|
86
|
+
const ranges = rangesFor(wildcardScenario, wildcardPackages);
|
|
87
|
+
for (const name of wildcardPackages) {
|
|
88
|
+
strictEqual(rangeOf(ranges, name, wildcardDependent), "0.87.0");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// pnpm folds a resolved peer into the snapshot's `dependencies`, so the edge is
|
|
93
|
+
// there — but only as the version it resolved to. The range stays in the
|
|
94
|
+
// `packages:` entry, and read from there a peer constraint needs no
|
|
95
|
+
// `node_modules`: without it every one of these read as an exact pin of 10.5.0,
|
|
96
|
+
// and no version could satisfy them all.
|
|
97
|
+
it("reads a peer range from the lockfile when no manifest is available", () => {
|
|
98
|
+
const ranges = rangesFor("duplicated-typescript-eslint", ["eslint"]);
|
|
99
|
+
|
|
100
|
+
strictEqual(
|
|
101
|
+
rangeOf(ranges, "eslint", "@eslint-community/eslint-utils@4.9.1"),
|
|
102
|
+
"^6.0.0 || ^7.0.0 || >=8.0.0",
|
|
103
|
+
);
|
|
104
|
+
strictEqual(
|
|
105
|
+
rangeOf(ranges, "eslint", "@typescript-eslint/eslint-plugin@8.61.0"),
|
|
106
|
+
"^8.57.0 || ^9.0.0 || ^10.0.0",
|
|
107
|
+
);
|
|
108
|
+
// declared optional, and kept: pnpm only writes the edge once something
|
|
109
|
+
// provides the peer, so an edge that exists is a peer that was provided
|
|
110
|
+
strictEqual(rangeOf(ranges, "eslint", "@eslint/js@10.0.1"), "^10.0.0");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// pnpm resolves a peer like any other edge, so nothing in the snapshot tells a
|
|
114
|
+
// peer requester apart from one holding a copy of its own — the report has to
|
|
115
|
+
it("marks a dependent whose range came from a peer declaration", () => {
|
|
116
|
+
const dependents =
|
|
117
|
+
rangesFor("duplicated-typescript-eslint", ["eslint"]).get("eslint") ?? [];
|
|
118
|
+
|
|
119
|
+
strictEqual(
|
|
120
|
+
dependents.find(
|
|
121
|
+
(dependent) => dependent.key === "eslint-plugin-n@17.24.0",
|
|
122
|
+
)?.peer,
|
|
123
|
+
true,
|
|
124
|
+
);
|
|
125
|
+
// a real dependency edge onto the same package stays unmarked
|
|
126
|
+
strictEqual(
|
|
127
|
+
rangesFor("duplicated-typescript-eslint", ["@eslint/plugin-kit"])
|
|
128
|
+
.get("@eslint/plugin-kit")
|
|
129
|
+
?.find((dependent) => dependent.key === "eslint@10.5.0")?.peer,
|
|
130
|
+
undefined,
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// the manifest is read first, so a name a package declares as both a
|
|
135
|
+
// dependency and a peer keeps the dependency's range
|
|
136
|
+
it("prefers a manifest declaration over the lockfile peer block", () => {
|
|
137
|
+
strictEqual(
|
|
138
|
+
rangeOf(
|
|
139
|
+
rangesFor("duplicated-typescript-eslint", ["eslint"], (name, version) =>
|
|
140
|
+
name === "@eslint-community/eslint-utils" && version === "4.9.1"
|
|
141
|
+
? { dependencies: { eslint: "^10.1.0" } }
|
|
142
|
+
: undefined,
|
|
143
|
+
),
|
|
144
|
+
"eslint",
|
|
145
|
+
"@eslint-community/eslint-utils@4.9.1",
|
|
146
|
+
),
|
|
147
|
+
"^10.1.0",
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("separates a caret dependent's range from the version it resolved to", () => {
|
|
152
|
+
const ranges = rangesFor(
|
|
153
|
+
downgradeScenario,
|
|
154
|
+
["barcode-detector"],
|
|
155
|
+
readDowngradeManifest,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
deepStrictEqual(ranges.get("barcode-detector"), [
|
|
159
|
+
{
|
|
160
|
+
key: "@yudiel/react-qr-scanner@2.3.1",
|
|
161
|
+
range: "3.0.3",
|
|
162
|
+
resolvedVersion: "3.0.3",
|
|
163
|
+
requesterName: "@yudiel/react-qr-scanner",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
key: "expo-camera@57.0.3",
|
|
167
|
+
range: "^3.0.0",
|
|
168
|
+
resolvedVersion: "3.2.2",
|
|
169
|
+
requesterName: "expo-camera",
|
|
170
|
+
},
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// `aliased-swapped-names`: the importer reaches the real `typescript` through
|
|
175
|
+
// the `@typescript/native` key and hands the `typescript` key to another
|
|
176
|
+
// package. The dependent has to be filed under the npm name the specifier
|
|
177
|
+
// targets, with the range and the version the alias resolved to, and the key
|
|
178
|
+
// it was declared under — that key is what makes the declaration binding
|
|
179
|
+
// rather than a range the dedupe may rewrite.
|
|
180
|
+
it("resolves an importer alias whose key names another package", () => {
|
|
181
|
+
const ranges = rangesFor("aliased-swapped-names", [
|
|
182
|
+
"typescript",
|
|
183
|
+
"@typescript/typescript6",
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
deepStrictEqual(ranges.get("typescript"), [
|
|
187
|
+
{
|
|
188
|
+
key: "package.json in devDependencies",
|
|
189
|
+
range: "7.0.2",
|
|
190
|
+
resolvedVersion: "7.0.2",
|
|
191
|
+
aliasKey: "@typescript/native",
|
|
192
|
+
workspace: { path: ".", depType: "devDependencies" },
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
key: "tool@1.0.0",
|
|
196
|
+
range: "5.9.3",
|
|
197
|
+
resolvedVersion: "5.9.3",
|
|
198
|
+
requesterName: "tool",
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
deepStrictEqual(ranges.get("@typescript/typescript6"), [
|
|
203
|
+
{
|
|
204
|
+
key: "package.json in devDependencies",
|
|
205
|
+
range: "6.0.2",
|
|
206
|
+
resolvedVersion: "6.0.2",
|
|
207
|
+
aliasKey: "typescript",
|
|
208
|
+
workspace: { path: ".", depType: "devDependencies" },
|
|
209
|
+
},
|
|
210
|
+
]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// pnpm writes an aliased importer's `version` as `realName@version`, which is
|
|
214
|
+
// not a version: carrying it through would put a string no semver call accepts
|
|
215
|
+
// where the resolved version belongs.
|
|
216
|
+
it("strips the alias target from an importer's resolved version", () => {
|
|
217
|
+
deepStrictEqual(
|
|
218
|
+
rangesFor("mergeable-alias", ["printable-shell-command"])
|
|
219
|
+
.get("printable-shell-command")
|
|
220
|
+
?.filter((dependent) => dependent.workspace !== undefined),
|
|
221
|
+
[
|
|
222
|
+
{
|
|
223
|
+
key: "package.json in devDependencies",
|
|
224
|
+
range: "^5.0.0",
|
|
225
|
+
resolvedVersion: "5.3.1",
|
|
226
|
+
workspace: { path: ".", depType: "devDependencies" },
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
key: "package.json in devDependencies",
|
|
230
|
+
range: "5.0.7",
|
|
231
|
+
resolvedVersion: "5.0.7",
|
|
232
|
+
aliasKey: "printable-shell-command-pinned",
|
|
233
|
+
workspace: { path: ".", depType: "devDependencies" },
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("keeps the exact ranges a package declares for its own family", () => {
|
|
240
|
+
const ranges = rangesFor(
|
|
241
|
+
wildcardScenario,
|
|
242
|
+
wildcardPackages,
|
|
243
|
+
readWildcardManifest,
|
|
244
|
+
);
|
|
245
|
+
for (const name of wildcardPackages) {
|
|
246
|
+
strictEqual(rangeOf(ranges, name, "metro@0.84.5"), "0.84.5");
|
|
247
|
+
strictEqual(rangeOf(ranges, name, "metro@0.87.0"), "0.87.0");
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// A `workspace:`, `catalog:`, `file:` or `link:` specifier names something
|
|
252
|
+
// other than the npm package sharing its key, and a dist-tag carries no
|
|
253
|
+
// comparable range. Both would read as unsatisfiable ranges rather than as
|
|
254
|
+
// "no constraint", which suppresses merges the real dependents allow.
|
|
255
|
+
it("leaves out importer specifiers that are not npm ranges", () => {
|
|
256
|
+
const ranges = rangesFor("protocol-dependents", [
|
|
257
|
+
"helper",
|
|
258
|
+
"semver",
|
|
259
|
+
"picocolors",
|
|
260
|
+
"left-pad",
|
|
261
|
+
"kleur",
|
|
262
|
+
"chalk",
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
// only the snapshot dependent of `app` survives for semver; the importer
|
|
266
|
+
// declares it as `latest`
|
|
267
|
+
deepStrictEqual(ranges.get("semver"), [
|
|
268
|
+
{
|
|
269
|
+
key: "app@1.0.0",
|
|
270
|
+
range: "7.8.1",
|
|
271
|
+
resolvedVersion: "7.8.1",
|
|
272
|
+
requesterName: "app",
|
|
273
|
+
},
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
for (const name of ["helper", "picocolors", "left-pad", "kleur", "chalk"]) {
|
|
277
|
+
strictEqual(ranges.has(name), false);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// A transitive dependent whose installed manifest declares a non-npm
|
|
282
|
+
// protocol falls back to the version it actually got, rather than carrying
|
|
283
|
+
// `workspace:*` into a semver comparison.
|
|
284
|
+
it("falls back to the resolved version when the manifest range is not comparable", () => {
|
|
285
|
+
const readWorkspaceManifest: ManifestReader = (name, version) =>
|
|
286
|
+
name === "@tamagui/metro-plugin" && version === "1.139.4"
|
|
287
|
+
? {
|
|
288
|
+
dependencies: {
|
|
289
|
+
"metro-config": "workspace:*",
|
|
290
|
+
"metro-transform-worker": "*",
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
: undefined;
|
|
294
|
+
|
|
295
|
+
const ranges = rangesFor(
|
|
296
|
+
wildcardScenario,
|
|
297
|
+
wildcardPackages,
|
|
298
|
+
readWorkspaceManifest,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
strictEqual(
|
|
302
|
+
rangeOf(ranges, "metro-config", wildcardDependent),
|
|
303
|
+
// the resolved version, not `*` and not `workspace:*`
|
|
304
|
+
"0.87.0",
|
|
305
|
+
);
|
|
306
|
+
strictEqual(
|
|
307
|
+
rangeOf(ranges, "metro-transform-worker", wildcardDependent),
|
|
308
|
+
"*",
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PackageDependencyDescriptorUtils,
|
|
3
|
+
isSemverComparable,
|
|
4
|
+
} from "pm-dedup-core";
|
|
5
|
+
import type { ClusterWorkspaceRef } from "pm-dedup-core";
|
|
6
|
+
import type { PnpmLockFile } from "../pnpmLockTypes.ts";
|
|
7
|
+
import {
|
|
8
|
+
parsePackageId,
|
|
9
|
+
resolveSnapshotDependency,
|
|
10
|
+
} from "./parsePnpmLockPackages.ts";
|
|
11
|
+
import type { PnpmProtocol } from "./pnpmProtocol.ts";
|
|
12
|
+
import type {
|
|
13
|
+
InstalledManifest,
|
|
14
|
+
ManifestReader,
|
|
15
|
+
} from "./readInstalledManifest.ts";
|
|
16
|
+
|
|
17
|
+
export interface DependentRange {
|
|
18
|
+
key: string;
|
|
19
|
+
range: string;
|
|
20
|
+
// version the requester actually got
|
|
21
|
+
resolvedVersion?: string;
|
|
22
|
+
// npm name of the requester, or undefined for an importer (workspace) one
|
|
23
|
+
requesterName?: string;
|
|
24
|
+
// the key the declaration sits under, when it names another package
|
|
25
|
+
// (`"@typescript/native": "npm:typescript@7.0.2"`). The range constrains
|
|
26
|
+
// `typescript` all the same, but the declaration is not `typescript`'s
|
|
27
|
+
aliasKey?: string;
|
|
28
|
+
// set for importer dependents only: where the range is declared on disk
|
|
29
|
+
workspace?: ClusterWorkspaceRef;
|
|
30
|
+
// the range comes from `peerDependencies`. pnpm resolves the peer like any
|
|
31
|
+
// other edge, so the requester constrains the version without holding a copy
|
|
32
|
+
// of its own, and the report has to name the declaration it comes from
|
|
33
|
+
peer?: true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type DependentRangesMap = Map<string, DependentRange[]>;
|
|
37
|
+
|
|
38
|
+
const snapshotDepTypes = ["dependencies", "optionalDependencies"] as const;
|
|
39
|
+
const manifestDepTypes = [
|
|
40
|
+
"dependencies",
|
|
41
|
+
"optionalDependencies",
|
|
42
|
+
"peerDependencies",
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
// a declaration semver cannot read says nothing about the version; the caller
|
|
46
|
+
// falls back to the version this requester actually got
|
|
47
|
+
const declaredRange = (
|
|
48
|
+
depName: string,
|
|
49
|
+
declared: string,
|
|
50
|
+
): string | undefined => {
|
|
51
|
+
const parsed = PackageDependencyDescriptorUtils.parse<PnpmProtocol>(
|
|
52
|
+
depName,
|
|
53
|
+
declared,
|
|
54
|
+
);
|
|
55
|
+
return isSemverComparable(parsed) ? parsed.selector : undefined;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// which declaration a range was read from, so a peer requester can be told
|
|
59
|
+
// apart from one holding a copy of its own
|
|
60
|
+
interface RangeSource {
|
|
61
|
+
range: string;
|
|
62
|
+
peer?: true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const manifestRange = (
|
|
66
|
+
manifest: InstalledManifest | undefined,
|
|
67
|
+
depName: string,
|
|
68
|
+
): RangeSource | undefined => {
|
|
69
|
+
if (!manifest) return undefined;
|
|
70
|
+
for (const depType of manifestDepTypes) {
|
|
71
|
+
const declared = manifest[depType]?.[depName];
|
|
72
|
+
if (declared === undefined) continue;
|
|
73
|
+
const range = declaredRange(depName, declared);
|
|
74
|
+
if (range === undefined) return undefined;
|
|
75
|
+
return depType === "peerDependencies" ? { range, peer: true } : { range };
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The peer range as the lockfile itself records it.
|
|
82
|
+
*
|
|
83
|
+
* pnpm resolves peers before writing the lockfile and folds each resolved peer
|
|
84
|
+
* into the snapshot's `dependencies` — which is why a peer edge is found here at
|
|
85
|
+
* all, unlike yarn's, where the fold lives only in the virtual packages the
|
|
86
|
+
* lockfile omits. But the snapshot carries the resolved *version*; the range
|
|
87
|
+
* stays behind in the `packages:` entry. Without it, seventeen plugins declaring
|
|
88
|
+
* `eslint: "^6.0.0 || ^7.0.0 || >=8.0.0"` and the like all read as exact pins of
|
|
89
|
+
* whatever got installed, and no version can satisfy them all — every merge
|
|
90
|
+
* ruled out by ranges that never said so.
|
|
91
|
+
*
|
|
92
|
+
* `packages:` keys carry no peer suffix, so the resolved id addresses the entry
|
|
93
|
+
* directly. `depName` is the key the requester declared, which is what an
|
|
94
|
+
* aliased peer sits under.
|
|
95
|
+
*/
|
|
96
|
+
const lockfilePeerRange = (
|
|
97
|
+
lock: PnpmLockFile,
|
|
98
|
+
dependent: { name: string; version: string },
|
|
99
|
+
depName: string,
|
|
100
|
+
): RangeSource | undefined => {
|
|
101
|
+
const declared =
|
|
102
|
+
lock.packages?.[`${dependent.name}@${dependent.version}`]
|
|
103
|
+
?.peerDependencies?.[depName];
|
|
104
|
+
if (declared === undefined) return undefined;
|
|
105
|
+
const range = declaredRange(depName, declared);
|
|
106
|
+
return range === undefined ? undefined : { range, peer: true };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Collect, for each duplicated package, the real semver range every dependent
|
|
111
|
+
* declares. Direct (importer) dependents carry their specifier; transitive
|
|
112
|
+
* dependents' ranges are read from their installed manifest (the pnpm lockfile
|
|
113
|
+
* only stores the resolved version, not the range). When a manifest is missing
|
|
114
|
+
* (project not installed), the resolved version is used as an exact fallback.
|
|
115
|
+
*/
|
|
116
|
+
export const collectDependentRanges = (
|
|
117
|
+
lock: PnpmLockFile,
|
|
118
|
+
duplicatePackageNames: Set<string>,
|
|
119
|
+
readManifest: ManifestReader,
|
|
120
|
+
): DependentRangesMap => {
|
|
121
|
+
const rangesMap: DependentRangesMap = new Map();
|
|
122
|
+
|
|
123
|
+
const add = (name: string, dependent: DependentRange): void => {
|
|
124
|
+
if (!duplicatePackageNames.has(name)) return;
|
|
125
|
+
let dependents = rangesMap.get(name);
|
|
126
|
+
if (!dependents) {
|
|
127
|
+
dependents = [];
|
|
128
|
+
rangesMap.set(name, dependents);
|
|
129
|
+
}
|
|
130
|
+
dependents.push(dependent);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (const [importerPath, project] of Object.entries(lock.importers ?? {})) {
|
|
134
|
+
(
|
|
135
|
+
["dependencies", "devDependencies", "optionalDependencies"] as const
|
|
136
|
+
).forEach((depType) => {
|
|
137
|
+
const deps = project[depType];
|
|
138
|
+
if (!deps) return;
|
|
139
|
+
for (const [depName, { specifier, version }] of Object.entries(deps)) {
|
|
140
|
+
const parsed = PackageDependencyDescriptorUtils.parse<PnpmProtocol>(
|
|
141
|
+
depName,
|
|
142
|
+
specifier,
|
|
143
|
+
);
|
|
144
|
+
// `workspace:` and `catalog:` name something other than the npm package
|
|
145
|
+
// sharing this key, so they constrain no npm version
|
|
146
|
+
if (!isSemverComparable(parsed)) continue;
|
|
147
|
+
add(parsed.npmName, {
|
|
148
|
+
key: `${importerPath === "." ? "package.json" : importerPath} in ${depType}`,
|
|
149
|
+
range: parsed.selector,
|
|
150
|
+
// an aliased importer entry stores `realName@version`, not a version
|
|
151
|
+
resolvedVersion: resolveSnapshotDependency(depName, version).version,
|
|
152
|
+
...(parsed.isAlias ? { aliasKey: parsed.key } : {}),
|
|
153
|
+
workspace: { path: importerPath, depType },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const [snapshotKey, snapshot] of Object.entries(lock.snapshots ?? {})) {
|
|
160
|
+
const dependent = parsePackageId(snapshotKey);
|
|
161
|
+
snapshotDepTypes.forEach((depType) => {
|
|
162
|
+
const deps = snapshot[depType];
|
|
163
|
+
if (!deps) return;
|
|
164
|
+
for (const [depName, depValue] of Object.entries(deps)) {
|
|
165
|
+
const { name, version } = resolveSnapshotDependency(depName, depValue);
|
|
166
|
+
if (!duplicatePackageNames.has(name)) continue;
|
|
167
|
+
const source =
|
|
168
|
+
manifestRange(
|
|
169
|
+
readManifest(dependent.name, dependent.version),
|
|
170
|
+
depName,
|
|
171
|
+
) ?? lockfilePeerRange(lock, dependent, depName);
|
|
172
|
+
add(name, {
|
|
173
|
+
key: `${dependent.name}@${dependent.version}`,
|
|
174
|
+
range: source?.range ?? version,
|
|
175
|
+
resolvedVersion: version,
|
|
176
|
+
requesterName: dependent.name,
|
|
177
|
+
...(name === depName ? {} : { aliasKey: depName }),
|
|
178
|
+
...(source?.peer ? { peer: source.peer } : {}),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return rangesMap;
|
|
185
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
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 { collectPnpmDependents } from "./collectPnpmDependents.ts";
|
|
6
|
+
|
|
7
|
+
const fixturesBase = (rel: string) =>
|
|
8
|
+
fileURLToPath(new URL(rel, import.meta.url));
|
|
9
|
+
|
|
10
|
+
const lockFor = (scenario: string) =>
|
|
11
|
+
readPnpmLock(fixturesBase(`../../test/fixtures/${scenario}/pnpm-lock.yaml`));
|
|
12
|
+
|
|
13
|
+
describe("collectPnpmDependents", () => {
|
|
14
|
+
it("collects the importer specifier (range) for a direct dependency", () => {
|
|
15
|
+
const dependents = collectPnpmDependents(lockFor("simple"));
|
|
16
|
+
deepStrictEqual(dependents.get("semver"), [
|
|
17
|
+
{ key: "package.json in dependencies", version: "7.7.3" },
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("collects both importer range and transitive resolved versions", () => {
|
|
22
|
+
const dependents = collectPnpmDependents(
|
|
23
|
+
lockFor("duplicated-babel-frame"),
|
|
24
|
+
["@babel/code-frame"],
|
|
25
|
+
);
|
|
26
|
+
const codeFrame = dependents.get("@babel/code-frame");
|
|
27
|
+
ok(codeFrame);
|
|
28
|
+
// direct importer dependency carries the specifier range
|
|
29
|
+
ok(
|
|
30
|
+
codeFrame.some(
|
|
31
|
+
(dependent) =>
|
|
32
|
+
dependent.key === "package.json in dependencies" &&
|
|
33
|
+
dependent.version === "7.26.2",
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
// transitive dependents carry a resolved version
|
|
37
|
+
ok(codeFrame.some((dependent) => dependent.version === "7.29.7"));
|
|
38
|
+
ok(codeFrame.length >= 2);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// A `workspace:`, `catalog:`, `file:` or `link:` specifier names something
|
|
42
|
+
// other than the npm package sharing its key, and a dist-tag carries no
|
|
43
|
+
// comparable range. Recording them as ranges makes `semver.satisfies` answer
|
|
44
|
+
// "not satisfied" for every candidate, suppressing merges the real dependents
|
|
45
|
+
// allow.
|
|
46
|
+
it("leaves out importer specifiers that are not npm ranges", () => {
|
|
47
|
+
const dependents = collectPnpmDependents(lockFor("protocol-dependents"));
|
|
48
|
+
|
|
49
|
+
deepStrictEqual([...dependents.keys()].toSorted(), ["app", "semver"]);
|
|
50
|
+
|
|
51
|
+
// the importer declares `semver` as `latest`, so only the snapshot dependent
|
|
52
|
+
// of `app` remains
|
|
53
|
+
deepStrictEqual(dependents.get("semver"), [
|
|
54
|
+
{ key: "app@1.0.0", version: "7.8.1" },
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
for (const packageName of [
|
|
58
|
+
"helper",
|
|
59
|
+
"picocolors",
|
|
60
|
+
"left-pad",
|
|
61
|
+
"kleur",
|
|
62
|
+
"chalk",
|
|
63
|
+
]) {
|
|
64
|
+
strictEqual(dependents.has(packageName), false);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps the range an aliased importer specifier declares", () => {
|
|
69
|
+
const dependents = collectPnpmDependents(lockFor("mergeable-alias"), [
|
|
70
|
+
"printable-shell-command",
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
deepStrictEqual(
|
|
74
|
+
(dependents.get("printable-shell-command") ?? [])
|
|
75
|
+
.filter((dependent) => dependent.key.startsWith("package.json"))
|
|
76
|
+
.map((dependent) => dependent.version)
|
|
77
|
+
.toSorted(),
|
|
78
|
+
["5.0.7", "^5.0.0"],
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// `aliased-swapped-names`: the importer declares `"@typescript/native":
|
|
83
|
+
// "npm:typescript@7.0.2"` and `"typescript":
|
|
84
|
+
// "npm:@typescript/typescript6@6.0.2"`, so each key names the other's package.
|
|
85
|
+
// Filing either under its key would put an exact pin on the wrong package.
|
|
86
|
+
it("files a swapped alias under the package its specifier targets", () => {
|
|
87
|
+
const dependents = collectPnpmDependents(lockFor("aliased-swapped-names"));
|
|
88
|
+
|
|
89
|
+
deepStrictEqual(dependents.get("typescript"), [
|
|
90
|
+
{ key: "package.json in devDependencies", version: "7.0.2" },
|
|
91
|
+
{ key: "tool@1.0.0", version: "5.9.3" },
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
deepStrictEqual(dependents.get("@typescript/typescript6"), [
|
|
95
|
+
{ key: "package.json in devDependencies", version: "6.0.2" },
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("restricts results to onlyPackageNames", () => {
|
|
100
|
+
const dependents = collectPnpmDependents(
|
|
101
|
+
lockFor("duplicated-printable-shell-command"),
|
|
102
|
+
["printable-shell-command"],
|
|
103
|
+
);
|
|
104
|
+
strictEqual(
|
|
105
|
+
[...dependents.keys()].every(
|
|
106
|
+
(name) => name === "printable-shell-command",
|
|
107
|
+
),
|
|
108
|
+
true,
|
|
109
|
+
);
|
|
110
|
+
ok(dependents.get("printable-shell-command"));
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PackageDependencyDescriptorUtils,
|
|
3
|
+
isSemverComparable,
|
|
4
|
+
} from "pm-dedup-core";
|
|
5
|
+
import type { PnpmLockFile } from "../pnpmLockTypes.ts";
|
|
6
|
+
import {
|
|
7
|
+
parsePackageId,
|
|
8
|
+
resolveSnapshotDependency,
|
|
9
|
+
} from "./parsePnpmLockPackages.ts";
|
|
10
|
+
import type { PnpmProtocol } from "./pnpmProtocol.ts";
|
|
11
|
+
|
|
12
|
+
export interface Dependent {
|
|
13
|
+
key: string;
|
|
14
|
+
version: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type DependentsMap = Map<string, Dependent[]>;
|
|
18
|
+
|
|
19
|
+
const importerDepTypes = [
|
|
20
|
+
"dependencies",
|
|
21
|
+
"devDependencies",
|
|
22
|
+
"optionalDependencies",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
const snapshotDepTypes = ["dependencies", "optionalDependencies"] as const;
|
|
26
|
+
|
|
27
|
+
export function collectPnpmDependents(
|
|
28
|
+
lock: PnpmLockFile,
|
|
29
|
+
onlyPackageNames?: string[],
|
|
30
|
+
): DependentsMap {
|
|
31
|
+
const dependentsMap: DependentsMap = new Map();
|
|
32
|
+
|
|
33
|
+
const add = (name: string, dependent: Dependent): void => {
|
|
34
|
+
if (onlyPackageNames && !onlyPackageNames.includes(name)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
let dependents = dependentsMap.get(name);
|
|
38
|
+
if (!dependents) {
|
|
39
|
+
dependents = [];
|
|
40
|
+
dependentsMap.set(name, dependents);
|
|
41
|
+
}
|
|
42
|
+
dependents.push(dependent);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (const [importerPath, project] of Object.entries(lock.importers ?? {})) {
|
|
46
|
+
importerDepTypes.forEach((depType) => {
|
|
47
|
+
const deps = project[depType];
|
|
48
|
+
if (!deps) return;
|
|
49
|
+
for (const [depName, { specifier }] of Object.entries(deps)) {
|
|
50
|
+
const parsedDep = PackageDependencyDescriptorUtils.parse<PnpmProtocol>(
|
|
51
|
+
depName,
|
|
52
|
+
specifier,
|
|
53
|
+
);
|
|
54
|
+
// `workspace:` and `catalog:` name something other than the npm package
|
|
55
|
+
// sharing this key, so they constrain no npm version
|
|
56
|
+
if (!isSemverComparable(parsedDep)) continue;
|
|
57
|
+
add(parsedDep.npmName, {
|
|
58
|
+
key: `${importerPath === "." ? "package.json" : importerPath} in ${depType}`,
|
|
59
|
+
version: parsedDep.selector,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const [snapshotKey, snapshot] of Object.entries(lock.snapshots ?? {})) {
|
|
66
|
+
const { name, version } = parsePackageId(snapshotKey);
|
|
67
|
+
const key = `${name}@${version}`;
|
|
68
|
+
snapshotDepTypes.forEach((depType) => {
|
|
69
|
+
const deps = snapshot[depType];
|
|
70
|
+
if (!deps) return;
|
|
71
|
+
for (const [depName, depValue] of Object.entries(deps)) {
|
|
72
|
+
const resolved = resolveSnapshotDependency(depName, depValue);
|
|
73
|
+
add(resolved.name, { key, version: resolved.version });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return dependentsMap;
|
|
79
|
+
}
|