pm-dedup-core 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 +105 -0
- package/dist/buildIdentifiedFixesMap.js +6 -0
- package/dist/buildIdentifiedFixesMap.js.map +1 -0
- package/dist/buildLockstepClusters.js +119 -0
- package/dist/buildLockstepClusters.js.map +1 -0
- package/dist/buildLockstepClusters.test.js +71 -0
- package/dist/buildLockstepClusters.test.js.map +1 -0
- package/dist/clusterLabel.js +59 -0
- package/dist/clusterLabel.js.map +1 -0
- package/dist/clusterLabel.test.js +56 -0
- package/dist/clusterLabel.test.js.map +1 -0
- package/dist/compareCandidateVersions.js +18 -0
- package/dist/compareCandidateVersions.js.map +1 -0
- package/dist/compareCandidateVersions.test.js +32 -0
- package/dist/compareCandidateVersions.test.js.map +1 -0
- package/dist/createPackageFilter.js +81 -0
- package/dist/createPackageFilter.js.map +1 -0
- package/dist/createPackageFilter.test.js +115 -0
- package/dist/createPackageFilter.test.js.map +1 -0
- package/dist/definitions/buildIdentifiedFixesMap.d.ts +4 -0
- package/dist/definitions/buildIdentifiedFixesMap.d.ts.map +1 -0
- package/dist/definitions/buildLockstepClusters.d.ts +8 -0
- package/dist/definitions/buildLockstepClusters.d.ts.map +1 -0
- package/dist/definitions/buildLockstepClusters.test.d.ts +2 -0
- package/dist/definitions/buildLockstepClusters.test.d.ts.map +1 -0
- package/dist/definitions/clusterLabel.d.ts +8 -0
- package/dist/definitions/clusterLabel.d.ts.map +1 -0
- package/dist/definitions/clusterLabel.test.d.ts +2 -0
- package/dist/definitions/clusterLabel.test.d.ts.map +1 -0
- package/dist/definitions/compareCandidateVersions.d.ts +10 -0
- package/dist/definitions/compareCandidateVersions.d.ts.map +1 -0
- package/dist/definitions/compareCandidateVersions.test.d.ts +2 -0
- package/dist/definitions/compareCandidateVersions.test.d.ts.map +1 -0
- package/dist/definitions/createPackageFilter.d.ts +45 -0
- package/dist/definitions/createPackageFilter.d.ts.map +1 -0
- package/dist/definitions/createPackageFilter.test.d.ts +2 -0
- package/dist/definitions/createPackageFilter.test.d.ts.map +1 -0
- package/dist/definitions/duplicateSnapshot.d.ts +8 -0
- package/dist/definitions/duplicateSnapshot.d.ts.map +1 -0
- package/dist/definitions/fileSnapshot.d.ts +7 -0
- package/dist/definitions/fileSnapshot.d.ts.map +1 -0
- package/dist/definitions/findProjectRoot.d.ts +27 -0
- package/dist/definitions/findProjectRoot.d.ts.map +1 -0
- package/dist/definitions/findProjectRoot.test.d.ts +2 -0
- package/dist/definitions/findProjectRoot.test.d.ts.map +1 -0
- package/dist/definitions/identifyLockstepClusterFixes.d.ts +89 -0
- package/dist/definitions/identifyLockstepClusterFixes.d.ts.map +1 -0
- package/dist/definitions/identifyLockstepClusterFixes.test.d.ts +2 -0
- package/dist/definitions/identifyLockstepClusterFixes.test.d.ts.map +1 -0
- package/dist/definitions/identifyResolutionFixes.d.ts +28 -0
- package/dist/definitions/identifyResolutionFixes.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 +44 -0
- package/dist/definitions/index.d.ts.map +1 -0
- package/dist/definitions/packageDependenciesUtils.d.ts +47 -0
- package/dist/definitions/packageDependenciesUtils.d.ts.map +1 -0
- package/dist/definitions/packageDependenciesUtils.test.d.ts +2 -0
- package/dist/definitions/packageDependenciesUtils.test.d.ts.map +1 -0
- package/dist/definitions/packageFilterArgs.d.ts +74 -0
- package/dist/definitions/packageFilterArgs.d.ts.map +1 -0
- package/dist/definitions/packageFilterArgs.test.d.ts +2 -0
- package/dist/definitions/packageFilterArgs.test.d.ts.map +1 -0
- package/dist/definitions/packageStyles.d.ts +14 -0
- package/dist/definitions/packageStyles.d.ts.map +1 -0
- package/dist/definitions/packageStyles.test.d.ts +2 -0
- package/dist/definitions/packageStyles.test.d.ts.map +1 -0
- package/dist/definitions/parseBinArgs.d.ts +14 -0
- package/dist/definitions/parseBinArgs.d.ts.map +1 -0
- package/dist/definitions/parseBinArgs.test.d.ts +2 -0
- package/dist/definitions/parseBinArgs.test.d.ts.map +1 -0
- package/dist/definitions/planClusterApply.d.ts +39 -0
- package/dist/definitions/planClusterApply.d.ts.map +1 -0
- package/dist/definitions/planClusterApply.test.d.ts +2 -0
- package/dist/definitions/planClusterApply.test.d.ts.map +1 -0
- package/dist/definitions/renderApplyPlan.d.ts +22 -0
- package/dist/definitions/renderApplyPlan.d.ts.map +1 -0
- package/dist/definitions/renderApplyPlan.test.d.ts +2 -0
- package/dist/definitions/renderApplyPlan.test.d.ts.map +1 -0
- package/dist/definitions/renderDedupeSummary.d.ts +22 -0
- package/dist/definitions/renderDedupeSummary.d.ts.map +1 -0
- package/dist/definitions/renderDedupeSummary.test.d.ts +2 -0
- package/dist/definitions/renderDedupeSummary.test.d.ts.map +1 -0
- package/dist/definitions/renderDuplicatesReport.d.ts +39 -0
- package/dist/definitions/renderDuplicatesReport.d.ts.map +1 -0
- package/dist/definitions/renderDuplicatesReport.test.d.ts +2 -0
- package/dist/definitions/renderDuplicatesReport.test.d.ts.map +1 -0
- package/dist/definitions/reportColors.d.ts +25 -0
- package/dist/definitions/reportColors.d.ts.map +1 -0
- package/dist/definitions/reportColors.test.d.ts +2 -0
- package/dist/definitions/reportColors.test.d.ts.map +1 -0
- package/dist/definitions/reportText.d.ts +29 -0
- package/dist/definitions/reportText.d.ts.map +1 -0
- package/dist/definitions/selectExplainedPackages.d.ts +22 -0
- package/dist/definitions/selectExplainedPackages.d.ts.map +1 -0
- package/dist/definitions/selectExplainedPackages.test.d.ts +2 -0
- package/dist/definitions/selectExplainedPackages.test.d.ts.map +1 -0
- package/dist/definitions/unconditionalOverrides.d.ts +24 -0
- package/dist/definitions/unconditionalOverrides.d.ts.map +1 -0
- package/dist/definitions/unconditionalOverrides.test.d.ts +2 -0
- package/dist/definitions/unconditionalOverrides.test.d.ts.map +1 -0
- package/dist/definitions/versionsSnapshot.d.ts +40 -0
- package/dist/definitions/versionsSnapshot.d.ts.map +1 -0
- package/dist/definitions/versionsSnapshot.test.d.ts +2 -0
- package/dist/definitions/versionsSnapshot.test.d.ts.map +1 -0
- package/dist/definitions/workspaceManifest.d.ts +21 -0
- package/dist/definitions/workspaceManifest.d.ts.map +1 -0
- package/dist/definitions/workspaceManifest.test.d.ts +2 -0
- package/dist/definitions/workspaceManifest.test.d.ts.map +1 -0
- package/dist/duplicateSnapshot.js +10 -0
- package/dist/duplicateSnapshot.js.map +1 -0
- package/dist/fileSnapshot.js +21 -0
- package/dist/fileSnapshot.js.map +1 -0
- package/dist/findProjectRoot.js +48 -0
- package/dist/findProjectRoot.js.map +1 -0
- package/dist/findProjectRoot.test.js +53 -0
- package/dist/findProjectRoot.test.js.map +1 -0
- package/dist/identifyLockstepClusterFixes.js +352 -0
- package/dist/identifyLockstepClusterFixes.js.map +1 -0
- package/dist/identifyLockstepClusterFixes.test.js +372 -0
- package/dist/identifyLockstepClusterFixes.test.js.map +1 -0
- package/dist/identifyResolutionFixes.js +102 -0
- package/dist/identifyResolutionFixes.js.map +1 -0
- package/dist/identifyResolutionFixes.test.js +70 -0
- package/dist/identifyResolutionFixes.test.js.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/packageDependenciesUtils.js +91 -0
- package/dist/packageDependenciesUtils.js.map +1 -0
- package/dist/packageDependenciesUtils.test.js +539 -0
- package/dist/packageDependenciesUtils.test.js.map +1 -0
- package/dist/packageFilterArgs.js +65 -0
- package/dist/packageFilterArgs.js.map +1 -0
- package/dist/packageFilterArgs.test.js +93 -0
- package/dist/packageFilterArgs.test.js.map +1 -0
- package/dist/packageStyles.js +33 -0
- package/dist/packageStyles.js.map +1 -0
- package/dist/packageStyles.test.js +58 -0
- package/dist/packageStyles.test.js.map +1 -0
- package/dist/parseBinArgs.js +28 -0
- package/dist/parseBinArgs.js.map +1 -0
- package/dist/parseBinArgs.test.js +62 -0
- package/dist/parseBinArgs.test.js.map +1 -0
- package/dist/planClusterApply.js +69 -0
- package/dist/planClusterApply.js.map +1 -0
- package/dist/planClusterApply.test.js +152 -0
- package/dist/planClusterApply.test.js.map +1 -0
- package/dist/renderApplyPlan.js +51 -0
- package/dist/renderApplyPlan.js.map +1 -0
- package/dist/renderApplyPlan.test.js +106 -0
- package/dist/renderApplyPlan.test.js.map +1 -0
- package/dist/renderDedupeSummary.js +38 -0
- package/dist/renderDedupeSummary.js.map +1 -0
- package/dist/renderDedupeSummary.test.js +91 -0
- package/dist/renderDedupeSummary.test.js.map +1 -0
- package/dist/renderDuplicatesReport.js +375 -0
- package/dist/renderDuplicatesReport.js.map +1 -0
- package/dist/renderDuplicatesReport.test.js +598 -0
- package/dist/renderDuplicatesReport.test.js.map +1 -0
- package/dist/reportColors.js +51 -0
- package/dist/reportColors.js.map +1 -0
- package/dist/reportColors.test.js +49 -0
- package/dist/reportColors.test.js.map +1 -0
- package/dist/reportText.js +29 -0
- package/dist/reportText.js.map +1 -0
- package/dist/selectExplainedPackages.js +42 -0
- package/dist/selectExplainedPackages.js.map +1 -0
- package/dist/selectExplainedPackages.test.js +55 -0
- package/dist/selectExplainedPackages.test.js.map +1 -0
- package/dist/unconditionalOverrides.js +36 -0
- package/dist/unconditionalOverrides.js.map +1 -0
- package/dist/unconditionalOverrides.test.js +71 -0
- package/dist/unconditionalOverrides.test.js.map +1 -0
- package/dist/versionsSnapshot.js +49 -0
- package/dist/versionsSnapshot.js.map +1 -0
- package/dist/versionsSnapshot.test.js +79 -0
- package/dist/versionsSnapshot.test.js.map +1 -0
- package/dist/workspaceManifest.js +63 -0
- package/dist/workspaceManifest.js.map +1 -0
- package/dist/workspaceManifest.test.js +159 -0
- package/dist/workspaceManifest.test.js.map +1 -0
- package/package.json +68 -0
- package/src/buildIdentifiedFixesMap.ts +22 -0
- package/src/buildLockstepClusters.test.ts +90 -0
- package/src/buildLockstepClusters.ts +155 -0
- package/src/clusterLabel.test.ts +79 -0
- package/src/clusterLabel.ts +75 -0
- package/src/compareCandidateVersions.test.ts +39 -0
- package/src/compareCandidateVersions.ts +23 -0
- package/src/createPackageFilter.test.ts +156 -0
- package/src/createPackageFilter.ts +142 -0
- package/src/duplicateSnapshot.ts +24 -0
- package/src/fileSnapshot.ts +28 -0
- package/src/findProjectRoot.test.ts +66 -0
- package/src/findProjectRoot.ts +61 -0
- package/src/identifyLockstepClusterFixes.test.ts +462 -0
- package/src/identifyLockstepClusterFixes.ts +602 -0
- package/src/identifyResolutionFixes.test.ts +133 -0
- package/src/identifyResolutionFixes.ts +183 -0
- package/src/index.ts +123 -0
- package/src/packageDependenciesUtils.test.ts +593 -0
- package/src/packageDependenciesUtils.ts +156 -0
- package/src/packageFilterArgs.test.ts +116 -0
- package/src/packageFilterArgs.ts +98 -0
- package/src/packageStyles.test.ts +77 -0
- package/src/packageStyles.ts +42 -0
- package/src/parseBinArgs.test.ts +73 -0
- package/src/parseBinArgs.ts +33 -0
- package/src/planClusterApply.test.ts +163 -0
- package/src/planClusterApply.ts +105 -0
- package/src/renderApplyPlan.test.ts +136 -0
- package/src/renderApplyPlan.ts +95 -0
- package/src/renderDedupeSummary.test.ts +121 -0
- package/src/renderDedupeSummary.ts +78 -0
- package/src/renderDuplicatesReport.test.ts +724 -0
- package/src/renderDuplicatesReport.ts +645 -0
- package/src/reportColors.test.ts +57 -0
- package/src/reportColors.ts +78 -0
- package/src/reportText.ts +58 -0
- package/src/selectExplainedPackages.test.ts +67 -0
- package/src/selectExplainedPackages.ts +79 -0
- package/src/unconditionalOverrides.test.ts +99 -0
- package/src/unconditionalOverrides.ts +60 -0
- package/src/versionsSnapshot.test.ts +146 -0
- package/src/versionsSnapshot.ts +93 -0
- package/src/workspaceManifest.test.ts +220 -0
- package/src/workspaceManifest.ts +100 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createCandidateVersionComparator } from "./compareCandidateVersions.ts";
|
|
3
|
+
|
|
4
|
+
const satisfiedCounts = new Map([
|
|
5
|
+
["1.0.0", 1],
|
|
6
|
+
["1.5.0", 3],
|
|
7
|
+
["2.0.0", 3],
|
|
8
|
+
["3.0.0", 2],
|
|
9
|
+
]);
|
|
10
|
+
const satisfiedCountOf = (version: string): number =>
|
|
11
|
+
satisfiedCounts.get(version) ?? 0;
|
|
12
|
+
|
|
13
|
+
describe("createCandidateVersionComparator", () => {
|
|
14
|
+
it("puts the version satisfying the most dependents first", () => {
|
|
15
|
+
const compare = createCandidateVersionComparator({ satisfiedCountOf });
|
|
16
|
+
|
|
17
|
+
expect(["1.0.0", "3.0.0", "1.5.0"].toSorted(compare)).toEqual([
|
|
18
|
+
"1.5.0",
|
|
19
|
+
"3.0.0",
|
|
20
|
+
"1.0.0",
|
|
21
|
+
]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("breaks a tie on the highest version", () => {
|
|
25
|
+
const compare = createCandidateVersionComparator({ satisfiedCountOf });
|
|
26
|
+
|
|
27
|
+
expect(["1.5.0", "2.0.0"].toSorted(compare)).toEqual(["2.0.0", "1.5.0"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("ranks on semver alone without a satisfied count", () => {
|
|
31
|
+
const compare = createCandidateVersionComparator();
|
|
32
|
+
|
|
33
|
+
expect(["1.0.0", "3.0.0", "1.5.0"].toSorted(compare)).toEqual([
|
|
34
|
+
"3.0.0",
|
|
35
|
+
"1.5.0",
|
|
36
|
+
"1.0.0",
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import semver from "semver";
|
|
2
|
+
|
|
3
|
+
export interface CandidateVersionComparatorOptions {
|
|
4
|
+
// omit to rank on semver alone
|
|
5
|
+
satisfiedCountOf?: (version: string) => number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A version satisfying every dependent holds the maximal count, so it always
|
|
10
|
+
* sorts first: "the highest version that satisfies everyone" falls out of this
|
|
11
|
+
* ordering instead of needing a pass of its own.
|
|
12
|
+
*/
|
|
13
|
+
export const createCandidateVersionComparator =
|
|
14
|
+
({ satisfiedCountOf }: CandidateVersionComparatorOptions = {}) =>
|
|
15
|
+
(versionA: string, versionB: string): number => {
|
|
16
|
+
if (satisfiedCountOf) {
|
|
17
|
+
const satisfiesA = satisfiedCountOf(versionA);
|
|
18
|
+
const satisfiesB = satisfiedCountOf(versionB);
|
|
19
|
+
if (satisfiesB > satisfiesA) return 1;
|
|
20
|
+
if (satisfiesB < satisfiesA) return -1;
|
|
21
|
+
}
|
|
22
|
+
return semver.rcompare(versionA, versionB);
|
|
23
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, it } from "bun:test";
|
|
2
|
+
import { deepStrictEqual, strictEqual } from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
createPackageFilter,
|
|
5
|
+
describeSkippedClusterFix,
|
|
6
|
+
selectClusterFixes,
|
|
7
|
+
} from "./createPackageFilter.ts";
|
|
8
|
+
import type { ClusterFix } from "./identifyLockstepClusterFixes.ts";
|
|
9
|
+
|
|
10
|
+
const clusterFix = (members: string[]): ClusterFix => ({
|
|
11
|
+
members,
|
|
12
|
+
duplicatedMembers: members,
|
|
13
|
+
memberVersions: {},
|
|
14
|
+
target: null,
|
|
15
|
+
direction: "none",
|
|
16
|
+
convergentMembers: [],
|
|
17
|
+
driverMembers: [],
|
|
18
|
+
excludedMembers: [],
|
|
19
|
+
anchor: null,
|
|
20
|
+
reuseFixes: [],
|
|
21
|
+
floatingMembers: [],
|
|
22
|
+
workspaceChanges: [],
|
|
23
|
+
reResolutionSet: [],
|
|
24
|
+
externalConstraints: [],
|
|
25
|
+
needsRoundTrip: false,
|
|
26
|
+
applicable: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("createPackageFilter", () => {
|
|
30
|
+
it("selects everything when nothing is asked for", () => {
|
|
31
|
+
const filter = createPackageFilter();
|
|
32
|
+
|
|
33
|
+
strictEqual(filter.selectsEverything, true);
|
|
34
|
+
strictEqual(filter.selects("lodash"), true);
|
|
35
|
+
strictEqual(filter.selects("@babel/core"), true);
|
|
36
|
+
strictEqual(filter.rejectionReason("lodash"), undefined);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("keeps only the included names", () => {
|
|
40
|
+
const filter = createPackageFilter({ include: ["lodash", "@babel/core"] });
|
|
41
|
+
|
|
42
|
+
strictEqual(filter.selectsEverything, false);
|
|
43
|
+
strictEqual(filter.selects("lodash"), true);
|
|
44
|
+
strictEqual(filter.selects("@babel/core"), true);
|
|
45
|
+
strictEqual(filter.selects("react"), false);
|
|
46
|
+
strictEqual(filter.rejectionReason("react"), "not selected");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("drops the excluded names and keeps the rest", () => {
|
|
50
|
+
const filter = createPackageFilter({ exclude: ["lodash"] });
|
|
51
|
+
|
|
52
|
+
strictEqual(filter.selects("react"), true);
|
|
53
|
+
strictEqual(filter.selects("lodash"), false);
|
|
54
|
+
strictEqual(filter.rejectionReason("lodash"), "excluded");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("lets an exclusion hold back part of an included scope", () => {
|
|
58
|
+
const filter = createPackageFilter({
|
|
59
|
+
includeScopes: ["@babel"],
|
|
60
|
+
exclude: ["@babel/runtime"],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
strictEqual(filter.selects("@babel/core"), true);
|
|
64
|
+
strictEqual(filter.selects("@babel/runtime"), false);
|
|
65
|
+
strictEqual(filter.rejectionReason("@babel/runtime"), "excluded");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("reads a scope with or without its leading @", () => {
|
|
69
|
+
const withAt = createPackageFilter({ includeScopes: ["@babel"] });
|
|
70
|
+
const withoutAt = createPackageFilter({ includeScopes: ["babel"] });
|
|
71
|
+
|
|
72
|
+
for (const filter of [withAt, withoutAt]) {
|
|
73
|
+
strictEqual(filter.selects("@babel/core"), true);
|
|
74
|
+
strictEqual(filter.selects("@babel/plugin-transform-runtime"), true);
|
|
75
|
+
strictEqual(filter.selects("babel-jest"), false);
|
|
76
|
+
strictEqual(filter.selects("@babel-other/core"), false);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("excludes a whole scope", () => {
|
|
81
|
+
const filter = createPackageFilter({ excludeScopes: ["@types"] });
|
|
82
|
+
|
|
83
|
+
strictEqual(filter.selects("@types/node"), false);
|
|
84
|
+
strictEqual(filter.selects("typescript"), true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("matches a name holding glob-adjacent characters literally", () => {
|
|
88
|
+
const filter = createPackageFilter({
|
|
89
|
+
include: ["lodash.merge", "@scope/a-b_c"],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
strictEqual(filter.selects("lodash.merge"), true);
|
|
93
|
+
strictEqual(filter.selects("@scope/a-b_c"), true);
|
|
94
|
+
strictEqual(filter.selects("lodashXmerge"), false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("stops a single star at the scope separator", () => {
|
|
98
|
+
const filter = createPackageFilter({ include: ["@babel/*"] });
|
|
99
|
+
|
|
100
|
+
strictEqual(filter.selects("@babel/core"), true);
|
|
101
|
+
strictEqual(filter.selects("babel"), false);
|
|
102
|
+
|
|
103
|
+
const everyUnscoped = createPackageFilter({ include: ["*"] });
|
|
104
|
+
strictEqual(everyUnscoped.selects("lodash"), true);
|
|
105
|
+
strictEqual(everyUnscoped.selects("@babel/core"), false);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("selectClusterFixes", () => {
|
|
110
|
+
it("passes every fix through when the filter selects everything", () => {
|
|
111
|
+
const fixes = [clusterFix(["@babel/core", "@babel/types"])];
|
|
112
|
+
|
|
113
|
+
deepStrictEqual(selectClusterFixes(fixes, createPackageFilter()), {
|
|
114
|
+
selected: fixes,
|
|
115
|
+
skipped: [],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("keeps a fix whose whole family is selected", () => {
|
|
120
|
+
const fix = clusterFix(["@babel/core", "@babel/types"]);
|
|
121
|
+
|
|
122
|
+
const { selected, skipped } = selectClusterFixes(
|
|
123
|
+
[fix],
|
|
124
|
+
createPackageFilter({ includeScopes: ["@babel"] }),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
deepStrictEqual(selected, [fix]);
|
|
128
|
+
deepStrictEqual(skipped, []);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("skips a partly selected family and names what blocked it", () => {
|
|
132
|
+
const fix = clusterFix(["@babel/core", "@babel/types"]);
|
|
133
|
+
|
|
134
|
+
const { selected, skipped } = selectClusterFixes(
|
|
135
|
+
[fix],
|
|
136
|
+
createPackageFilter({ include: ["@babel/types"] }),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
deepStrictEqual(selected, []);
|
|
140
|
+
deepStrictEqual(skipped, [{ fix, blockedBy: ["@babel/core"] }]);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("describeSkippedClusterFix", () => {
|
|
145
|
+
it("names the family and counts the blockers past the first few", () => {
|
|
146
|
+
const members = ["a", "b", "c", "d", "e"];
|
|
147
|
+
|
|
148
|
+
strictEqual(
|
|
149
|
+
describeSkippedClusterFix({
|
|
150
|
+
fix: clusterFix(members),
|
|
151
|
+
blockedBy: members,
|
|
152
|
+
}),
|
|
153
|
+
"cluster a (+4 more): a, b, c (+2 more) not selected by the filter, and a family only converges as a whole",
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import picomatch from "picomatch";
|
|
2
|
+
import { clusterLabel } from "./clusterLabel.ts";
|
|
3
|
+
import type { ClusterFix } from "./identifyLockstepClusterFixes.ts";
|
|
4
|
+
|
|
5
|
+
export interface PackageFilterOptions {
|
|
6
|
+
// glob patterns matched against the full npm name, `@scope/name` included
|
|
7
|
+
include?: string[];
|
|
8
|
+
exclude?: string[];
|
|
9
|
+
// bare scope names, with or without the leading `@`
|
|
10
|
+
includeScopes?: string[];
|
|
11
|
+
excludeScopes?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PackageFilter {
|
|
15
|
+
// nothing was asked for, so every package is selected and callers holding a
|
|
16
|
+
// whole lockfile can skip the pass entirely
|
|
17
|
+
selectsEverything: boolean;
|
|
18
|
+
selects: (packageName: string) => boolean;
|
|
19
|
+
// why a package is left alone, for reports; undefined when it is selected
|
|
20
|
+
rejectionReason: (packageName: string) => string | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const scopePattern = (scope: string): string =>
|
|
24
|
+
`${scope.startsWith("@") ? scope : `@${scope}`}/**`;
|
|
25
|
+
|
|
26
|
+
const toPatterns = (names: string[], scopes: string[]): string[] => [
|
|
27
|
+
...names,
|
|
28
|
+
...scopes.map(scopePattern),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// picomatch throws on an empty pattern list, and "no pattern" has to mean "no
|
|
32
|
+
// opinion" rather than "matches nothing".
|
|
33
|
+
const compile = (patterns: string[]): ((name: string) => boolean) =>
|
|
34
|
+
patterns.length === 0 ? () => false : picomatch(patterns);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Selects the packages a dedupe run is allowed to touch. Deduplicating a large
|
|
38
|
+
* lockfile in one pass produces a diff no one reviews, so every tool exposes
|
|
39
|
+
* this as the way to work through it a family at a time.
|
|
40
|
+
*
|
|
41
|
+
* A package is selected when the include list is empty or one of its patterns
|
|
42
|
+
* matches, and no exclude pattern matches — exclusion always wins, so a scope
|
|
43
|
+
* can be selected with a few of its packages held back.
|
|
44
|
+
*
|
|
45
|
+
* Patterns are globs with path semantics (`*` stops at `/`), the same as the
|
|
46
|
+
* `why-duplicate` filters: `@babel/*` matches `@babel/core`, a bare `*` does
|
|
47
|
+
* not. npm names carry no glob metacharacter, so a plain name is still an
|
|
48
|
+
* exact match.
|
|
49
|
+
*/
|
|
50
|
+
export const createPackageFilter = ({
|
|
51
|
+
include = [],
|
|
52
|
+
exclude = [],
|
|
53
|
+
includeScopes = [],
|
|
54
|
+
excludeScopes = [],
|
|
55
|
+
}: PackageFilterOptions = {}): PackageFilter => {
|
|
56
|
+
const includePatterns = toPatterns(include, includeScopes);
|
|
57
|
+
const excludePatterns = toPatterns(exclude, excludeScopes);
|
|
58
|
+
const isIncluded = compile(includePatterns);
|
|
59
|
+
const isExcluded = compile(excludePatterns);
|
|
60
|
+
|
|
61
|
+
const rejectionReason = (packageName: string): string | undefined => {
|
|
62
|
+
if (isExcluded(packageName)) return "excluded";
|
|
63
|
+
if (includePatterns.length > 0 && !isIncluded(packageName)) {
|
|
64
|
+
return "not selected";
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
selectsEverything:
|
|
71
|
+
includePatterns.length === 0 && excludePatterns.length === 0,
|
|
72
|
+
selects: (packageName) => rejectionReason(packageName) === undefined,
|
|
73
|
+
rejectionReason,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// A map keyed by package name is where the filter applies exactly: an unselected
|
|
78
|
+
// package keeps every resolution it has, rather than being partly rewritten.
|
|
79
|
+
export const selectPackages = <T>(
|
|
80
|
+
packages: Record<string, T>,
|
|
81
|
+
filter: PackageFilter,
|
|
82
|
+
): Record<string, T> =>
|
|
83
|
+
filter.selectsEverything
|
|
84
|
+
? packages
|
|
85
|
+
: Object.fromEntries(
|
|
86
|
+
Object.entries(packages).filter(([packageName]) =>
|
|
87
|
+
filter.selects(packageName),
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
export interface SkippedClusterFix {
|
|
92
|
+
fix: ClusterFix;
|
|
93
|
+
// the members that are not selected, so a fix doing nothing is never silent
|
|
94
|
+
blockedBy: string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SelectedClusterFixes {
|
|
98
|
+
selected: ClusterFix[];
|
|
99
|
+
skipped: SkippedClusterFix[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A cluster fix moves its whole family in lockstep, so it is kept only when
|
|
104
|
+
* every member is selected: applying it for part of a family would move the
|
|
105
|
+
* rest anyway, past the filter the user set. Select a family with a scope, or
|
|
106
|
+
* by naming all of its members.
|
|
107
|
+
*/
|
|
108
|
+
export const selectClusterFixes = (
|
|
109
|
+
fixes: ClusterFix[],
|
|
110
|
+
filter: PackageFilter,
|
|
111
|
+
): SelectedClusterFixes => {
|
|
112
|
+
if (filter.selectsEverything) return { selected: fixes, skipped: [] };
|
|
113
|
+
|
|
114
|
+
const selected: ClusterFix[] = [];
|
|
115
|
+
const skipped: SkippedClusterFix[] = [];
|
|
116
|
+
|
|
117
|
+
for (const fix of fixes) {
|
|
118
|
+
const blockedBy = fix.members.filter((member) => !filter.selects(member));
|
|
119
|
+
if (blockedBy.length === 0) {
|
|
120
|
+
selected.push(fix);
|
|
121
|
+
} else {
|
|
122
|
+
skipped.push({ fix, blockedBy });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { selected, skipped };
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// A family can block on a dozen members, and naming them all buries the point.
|
|
130
|
+
const namedBlockers = 3;
|
|
131
|
+
|
|
132
|
+
// Phrased for the `skipped` list of an apply plan, so both tools explain a
|
|
133
|
+
// filtered-out family the same way.
|
|
134
|
+
export const describeSkippedClusterFix = ({
|
|
135
|
+
fix,
|
|
136
|
+
blockedBy,
|
|
137
|
+
}: SkippedClusterFix): string => {
|
|
138
|
+
const named = blockedBy.slice(0, namedBlockers).join(", ");
|
|
139
|
+
const rest = blockedBy.length - namedBlockers;
|
|
140
|
+
const blockers = rest > 0 ? `${named} (+${rest} more)` : named;
|
|
141
|
+
return `cluster ${clusterLabel(fix.members)}: ${blockers} not selected by the filter, and a family only converges as a whole`;
|
|
142
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Every resolution that is one copy too many, as `name@version` ids. Sets, not
|
|
2
|
+
// counts: a change that removes one duplicate and introduces another is not an
|
|
3
|
+
// improvement, and a count would hide it.
|
|
4
|
+
export type DuplicateSnapshot = Set<string>;
|
|
5
|
+
|
|
6
|
+
export interface DuplicateDiff {
|
|
7
|
+
removed: string[];
|
|
8
|
+
added: string[];
|
|
9
|
+
improved: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const diffDuplicates = (
|
|
13
|
+
before: DuplicateSnapshot,
|
|
14
|
+
after: DuplicateSnapshot,
|
|
15
|
+
): DuplicateDiff => {
|
|
16
|
+
const removed = [...before].filter((resolution) => !after.has(resolution));
|
|
17
|
+
const added = [...after].filter((resolution) => !before.has(resolution));
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
removed: removed.toSorted((a, b) => a.localeCompare(b)),
|
|
21
|
+
added: added.toSorted((a, b) => a.localeCompare(b)),
|
|
22
|
+
improved: added.length === 0 && removed.length > 0,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export interface FileSnapshot {
|
|
4
|
+
path: string;
|
|
5
|
+
// undefined when the file did not exist: restoring deletes it again
|
|
6
|
+
content: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const readIfExists = (path: string): string | undefined => {
|
|
10
|
+
try {
|
|
11
|
+
return readFileSync(path, "utf8");
|
|
12
|
+
} catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const captureFiles = (paths: string[]): FileSnapshot[] =>
|
|
18
|
+
paths.map((path) => ({ path, content: readIfExists(path) }));
|
|
19
|
+
|
|
20
|
+
export const restoreFiles = (snapshots: FileSnapshot[]): void => {
|
|
21
|
+
for (const { path, content } of snapshots) {
|
|
22
|
+
if (content === undefined) {
|
|
23
|
+
rmSync(path, { force: true });
|
|
24
|
+
} else {
|
|
25
|
+
writeFileSync(path, content);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { afterEach, describe, it } from "bun:test";
|
|
2
|
+
import { strictEqual } from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { findProjectRoot } from "./findProjectRoot.ts";
|
|
7
|
+
|
|
8
|
+
const lockfileName = "pm-tools.lock";
|
|
9
|
+
|
|
10
|
+
const dirs: string[] = [];
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
for (const dir of dirs.splice(0)) {
|
|
14
|
+
rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const tempRoot = (): string => {
|
|
19
|
+
const dir = mkdtempSync(join(tmpdir(), `find-project-root-${process.pid}-`));
|
|
20
|
+
dirs.push(dir);
|
|
21
|
+
return dir;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const makeDir = (root: string, relativePath: string): string => {
|
|
25
|
+
const dir = join(root, relativePath);
|
|
26
|
+
mkdirSync(dir, { recursive: true });
|
|
27
|
+
return dir;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const makeLockfile = (dir: string): void => {
|
|
31
|
+
writeFileSync(join(dir, lockfileName), "");
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("findProjectRoot", () => {
|
|
35
|
+
it("finds the lockfile in cwd", () => {
|
|
36
|
+
const root = tempRoot();
|
|
37
|
+
makeLockfile(root);
|
|
38
|
+
|
|
39
|
+
strictEqual(findProjectRoot({ lockfileName, cwd: root }), root);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("walks up to an ancestor holding the lockfile", () => {
|
|
43
|
+
const root = tempRoot();
|
|
44
|
+
makeLockfile(root);
|
|
45
|
+
const nested = makeDir(root, join("packages", "app", "src"));
|
|
46
|
+
|
|
47
|
+
strictEqual(findProjectRoot({ lockfileName, cwd: nested }), root);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("stops at the nearest one when two ancestors hold a lockfile", () => {
|
|
51
|
+
const root = tempRoot();
|
|
52
|
+
makeLockfile(root);
|
|
53
|
+
const nestedProject = makeDir(root, "examples/embedded");
|
|
54
|
+
makeLockfile(nestedProject);
|
|
55
|
+
const nested = makeDir(nestedProject, "src");
|
|
56
|
+
|
|
57
|
+
strictEqual(findProjectRoot({ lockfileName, cwd: nested }), nestedProject);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns null when no ancestor up to the filesystem root has one", () => {
|
|
61
|
+
const root = tempRoot();
|
|
62
|
+
const nested = makeDir(root, "packages/app");
|
|
63
|
+
|
|
64
|
+
strictEqual(findProjectRoot({ lockfileName, cwd: nested }), null);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { createColorize, shouldColorize } from "./reportColors.ts";
|
|
4
|
+
|
|
5
|
+
export interface ProjectRootOptions {
|
|
6
|
+
lockfileName: string;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The nearest directory holding the lockfile, starting at `cwd` and walking up
|
|
12
|
+
* to the filesystem root; null when there is none.
|
|
13
|
+
*
|
|
14
|
+
* Nearest wins, which is the rule every package manager applies: a nested
|
|
15
|
+
* project carrying its own lockfile keeps resolving to itself rather than to the
|
|
16
|
+
* workspace above it.
|
|
17
|
+
*/
|
|
18
|
+
export const findProjectRoot = ({
|
|
19
|
+
lockfileName,
|
|
20
|
+
cwd = process.cwd(),
|
|
21
|
+
}: ProjectRootOptions): string | null => {
|
|
22
|
+
let dir = cwd;
|
|
23
|
+
for (;;) {
|
|
24
|
+
if (existsSync(join(dir, lockfileName))) return dir;
|
|
25
|
+
const parent = dirname(dir);
|
|
26
|
+
if (parent === dir) return null;
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `findProjectRoot` for a bin: the project root the run operates on, whatever
|
|
33
|
+
* subdirectory it was invoked from.
|
|
34
|
+
*
|
|
35
|
+
* Returns null when there is no project to work on — as `parseBinArgs` does, the
|
|
36
|
+
* reason is printed and the exit code already says so, leaving the caller
|
|
37
|
+
* nothing to do but return.
|
|
38
|
+
*
|
|
39
|
+
* The notice goes to stderr, and only when the root is not `cwd` — a run from
|
|
40
|
+
* the root prints exactly what it always did, and a redirected report stays
|
|
41
|
+
* parseable.
|
|
42
|
+
*/
|
|
43
|
+
export const resolveProjectDir = ({
|
|
44
|
+
lockfileName,
|
|
45
|
+
cwd = process.cwd(),
|
|
46
|
+
}: ProjectRootOptions): string | null => {
|
|
47
|
+
const projectDir = findProjectRoot({ lockfileName, cwd });
|
|
48
|
+
|
|
49
|
+
if (projectDir === null) {
|
|
50
|
+
console.error(`No ${lockfileName} found in ${cwd} or any parent directory`);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (projectDir !== cwd) {
|
|
56
|
+
const colorize = createColorize(shouldColorize(process.stderr));
|
|
57
|
+
console.error(colorize("dim", `using ${join(projectDir, lockfileName)}`));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return projectDir;
|
|
61
|
+
};
|