pm-dedup-core 2.1.0 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clusterOverrides.js +34 -0
- package/dist/clusterOverrides.js.map +1 -0
- package/dist/clusterOverrides.test.js +84 -0
- package/dist/clusterOverrides.test.js.map +1 -0
- package/dist/createPackageFilter.test.js +0 -1
- package/dist/createPackageFilter.test.js.map +1 -1
- package/dist/definitions/clusterOverrides.d.ts +41 -0
- package/dist/definitions/clusterOverrides.d.ts.map +1 -0
- package/dist/definitions/clusterOverrides.test.d.ts +2 -0
- package/dist/definitions/clusterOverrides.test.d.ts.map +1 -0
- package/dist/definitions/identifyLockstepClusterFixes.d.ts +0 -1
- package/dist/definitions/identifyLockstepClusterFixes.d.ts.map +1 -1
- package/dist/definitions/index.d.ts +2 -0
- package/dist/definitions/index.d.ts.map +1 -1
- package/dist/definitions/planClusterApply.d.ts +6 -2
- package/dist/definitions/planClusterApply.d.ts.map +1 -1
- package/dist/definitions/renderDuplicatesReport.d.ts.map +1 -1
- package/dist/definitions/unconditionalOverrides.d.ts +2 -1
- package/dist/definitions/unconditionalOverrides.d.ts.map +1 -1
- package/dist/identifyLockstepClusterFixes.js +18 -47
- package/dist/identifyLockstepClusterFixes.js.map +1 -1
- package/dist/identifyLockstepClusterFixes.test.js +212 -10
- package/dist/identifyLockstepClusterFixes.test.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/planClusterApply.js +7 -5
- package/dist/planClusterApply.js.map +1 -1
- package/dist/planClusterApply.test.js +28 -19
- package/dist/planClusterApply.test.js.map +1 -1
- package/dist/renderDuplicatesReport.js +0 -5
- package/dist/renderDuplicatesReport.js.map +1 -1
- package/dist/renderDuplicatesReport.test.js +0 -8
- package/dist/renderDuplicatesReport.test.js.map +1 -1
- package/dist/unconditionalOverrides.js +2 -1
- package/dist/unconditionalOverrides.js.map +1 -1
- package/dist/unconditionalOverrides.test.js +0 -1
- package/dist/unconditionalOverrides.test.js.map +1 -1
- package/package.json +1 -1
- package/src/clusterOverrides.test.ts +125 -0
- package/src/clusterOverrides.ts +84 -0
- package/src/createPackageFilter.test.ts +0 -1
- package/src/identifyLockstepClusterFixes.test.ts +244 -10
- package/src/identifyLockstepClusterFixes.ts +23 -67
- package/src/index.ts +9 -0
- package/src/planClusterApply.test.ts +31 -19
- package/src/planClusterApply.ts +10 -5
- package/src/renderDuplicatesReport.test.ts +0 -9
- package/src/renderDuplicatesReport.ts +0 -7
- package/src/unconditionalOverrides.test.ts +0 -1
- package/src/unconditionalOverrides.ts +2 -1
|
@@ -103,9 +103,6 @@ export interface ClusterFix {
|
|
|
103
103
|
anchor: string | null;
|
|
104
104
|
// open ranges that could have reused the anchored version and did not
|
|
105
105
|
reuseFixes: ClusterReuseFix[];
|
|
106
|
-
// members the package manager has to re-resolve, at a version only it can
|
|
107
|
-
// pick: nothing pins them, so the result has to be verified after installing
|
|
108
|
-
floatingMembers: string[];
|
|
109
106
|
// workspace ranges to edit before installing, exact pins included: a pin is a
|
|
110
107
|
// decision, so it is a last resort (see `betterCandidate`), not immutable. A
|
|
111
108
|
// declaration made through an alias key is the exception — it binds
|
|
@@ -126,12 +123,12 @@ const accepts = (version: string, range: string): boolean =>
|
|
|
126
123
|
|
|
127
124
|
interface Candidate {
|
|
128
125
|
version: string;
|
|
126
|
+
// every member that lands on this version, duplicated or not
|
|
127
|
+
converging: Set<string>;
|
|
129
128
|
// duplicated members that collapse onto this version
|
|
130
129
|
convergentMembers: string[];
|
|
131
130
|
// duplicated members that do not, with the third-party ranges to blame
|
|
132
131
|
excluded: Map<string, ClusterExternalConstraint[]>;
|
|
133
|
-
// members the resolver has to move, version unknown
|
|
134
|
-
floatingMembers: string[];
|
|
135
132
|
// workspace pins that would have to be edited to reach this version
|
|
136
133
|
pinsToEdit: ClusterExternalConstraint[];
|
|
137
134
|
// members this version is installed for, among those free to move
|
|
@@ -366,29 +363,6 @@ export const identifyLockstepClusterFixes = (
|
|
|
366
363
|
return floor !== null && semver.eq(floor, version);
|
|
367
364
|
});
|
|
368
365
|
|
|
369
|
-
// Whether a member could hold a version it does not hold today. When every
|
|
370
|
-
// requester asks through an open range (`*`, `0.83 - 0.86`), its current
|
|
371
|
-
// version is an accident of resolution rather than a constraint: the
|
|
372
|
-
// resolver is free to land it elsewhere, and which version that is is not
|
|
373
|
-
// ours to name. Anything exactly pinned, or pinned to the line it already
|
|
374
|
-
// carries (`^0.87.0` on an 0.87.0), is not re-resolvable.
|
|
375
|
-
const isReResolvable = (member: string): boolean => {
|
|
376
|
-
const ranges = externalRangesOn(member);
|
|
377
|
-
if (ranges.length === 0) return true;
|
|
378
|
-
|
|
379
|
-
const floors = ranges.map(rangeFloor);
|
|
380
|
-
if (floors.includes(null)) return false;
|
|
381
|
-
|
|
382
|
-
const highestFloor = floors
|
|
383
|
-
.filter((floor): floor is string => floor !== null)
|
|
384
|
-
.toSorted((a, b) => semver.rcompare(a, b))[0]!;
|
|
385
|
-
|
|
386
|
-
return (
|
|
387
|
-
ranges.every((range) => accepts(highestFloor, range)) &&
|
|
388
|
-
!isInstalled(member, highestFloor)
|
|
389
|
-
);
|
|
390
|
-
};
|
|
391
|
-
|
|
392
366
|
const duplicatedSet = new Set(duplicatedMembers);
|
|
393
367
|
|
|
394
368
|
// Members that end up on `version`, as a fixed point: start from those free
|
|
@@ -397,27 +371,11 @@ export const identifyLockstepClusterFixes = (
|
|
|
397
371
|
// whoever an in-cluster requester pins elsewhere. Iterating matters because
|
|
398
372
|
// such a requester only stops pinning if it converges too, and the family's
|
|
399
373
|
// dependency edges run both ways.
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
const
|
|
405
|
-
new Set(
|
|
406
|
-
clusterMembers.filter(
|
|
407
|
-
(member) =>
|
|
408
|
-
// a duplicated member is what the fix is about: it converges or it
|
|
409
|
-
// is excluded, never "the resolver will sort it out"
|
|
410
|
-
!duplicatedSet.has(member) &&
|
|
411
|
-
!isInstalled(member, version) &&
|
|
412
|
-
!canReResolve(member, version) &&
|
|
413
|
-
isReResolvable(member),
|
|
414
|
-
),
|
|
415
|
-
);
|
|
416
|
-
|
|
417
|
-
const convergentSetFor = (
|
|
418
|
-
version: string,
|
|
419
|
-
floating: Set<string>,
|
|
420
|
-
): Set<string> => {
|
|
374
|
+
//
|
|
375
|
+
// A requester its own requesters only ask for through an open range (`*`,
|
|
376
|
+
// `^21.0.2`) is no exception: a resolver keeps a locked version they still
|
|
377
|
+
// accept, so nothing moves it, and its pins block like any other.
|
|
378
|
+
const convergentSetFor = (version: string): Set<string> => {
|
|
421
379
|
const converging = new Set(
|
|
422
380
|
clusterMembers.filter(
|
|
423
381
|
(member) =>
|
|
@@ -434,8 +392,7 @@ export const identifyLockstepClusterFixes = (
|
|
|
434
392
|
const pinnedElsewhere = (internalByMember.get(member) ?? []).some(
|
|
435
393
|
(internal) =>
|
|
436
394
|
!accepts(version, internal.range) &&
|
|
437
|
-
!converging.has(internal.requesterName)
|
|
438
|
-
!floating.has(internal.requesterName),
|
|
395
|
+
!converging.has(internal.requesterName),
|
|
439
396
|
);
|
|
440
397
|
if (pinnedElsewhere) {
|
|
441
398
|
converging.delete(member);
|
|
@@ -448,17 +405,14 @@ export const identifyLockstepClusterFixes = (
|
|
|
448
405
|
};
|
|
449
406
|
|
|
450
407
|
const candidates = candidateVersions.map((version): Candidate => {
|
|
451
|
-
const
|
|
452
|
-
const converging = convergentSetFor(version, floating);
|
|
408
|
+
const converging = convergentSetFor(version);
|
|
453
409
|
|
|
454
410
|
return {
|
|
455
411
|
version,
|
|
456
|
-
|
|
412
|
+
converging,
|
|
457
413
|
// a pin the user would have to edit makes a candidate a last resort
|
|
458
414
|
pinsToEdit: workspaceConstraints.filter(
|
|
459
|
-
(constraint) =>
|
|
460
|
-
!floating.has(constraint.packageName) &&
|
|
461
|
-
!accepts(version, constraint.range),
|
|
415
|
+
(constraint) => !accepts(version, constraint.range),
|
|
462
416
|
),
|
|
463
417
|
convergentMembers: duplicatedMembers.filter((member) =>
|
|
464
418
|
converging.has(member),
|
|
@@ -534,7 +488,6 @@ export const identifyLockstepClusterFixes = (
|
|
|
534
488
|
excludedMembers: [],
|
|
535
489
|
anchor,
|
|
536
490
|
reuseFixes,
|
|
537
|
-
floatingMembers: [],
|
|
538
491
|
workspaceChanges: [],
|
|
539
492
|
reResolutionSet: [],
|
|
540
493
|
externalConstraints,
|
|
@@ -550,15 +503,22 @@ export const identifyLockstepClusterFixes = (
|
|
|
550
503
|
.filter((constraint) => canMove(constraint.packageName, target))
|
|
551
504
|
.map((constraint) => ({ ...constraint, to: target }));
|
|
552
505
|
|
|
506
|
+
// Editing the pin moves the anchor along with it: a range repointed at the
|
|
507
|
+
// version being left would split the family again.
|
|
508
|
+
const movesAnchor = workspaceChanges.some(
|
|
509
|
+
(change) => change.range === anchor,
|
|
510
|
+
);
|
|
511
|
+
|
|
553
512
|
// re-resolution set: members free to move that are pulled from outside the
|
|
554
513
|
// cluster and do not carry the target version, so a pure-lock copy is
|
|
555
|
-
// impossible. Internal-only members cascade from these.
|
|
556
|
-
|
|
514
|
+
// impossible. Internal-only members cascade from these. A member that is
|
|
515
|
+
// not duplicated also has to survive the converging fixed point: one an
|
|
516
|
+
// excluded sibling pins elsewhere would only become a new duplicate.
|
|
557
517
|
const reResolutionSet = clusterMembers
|
|
558
518
|
.filter(
|
|
559
519
|
(member) =>
|
|
560
520
|
membersWithExternalDependent.has(member) &&
|
|
561
|
-
|
|
521
|
+
(duplicatedSet.has(member) || best.converging.has(member)) &&
|
|
562
522
|
canMove(member, target) &&
|
|
563
523
|
canReResolve(member, target) &&
|
|
564
524
|
!isInstalled(member, target),
|
|
@@ -585,14 +545,10 @@ export const identifyLockstepClusterFixes = (
|
|
|
585
545
|
blockedBy,
|
|
586
546
|
})),
|
|
587
547
|
anchor,
|
|
588
|
-
reuseFixes,
|
|
589
|
-
floatingMembers: best.floatingMembers,
|
|
548
|
+
reuseFixes: movesAnchor ? [] : reuseFixes,
|
|
590
549
|
workspaceChanges,
|
|
591
550
|
reResolutionSet,
|
|
592
|
-
needsRoundTrip:
|
|
593
|
-
reResolutionSet.length > 0 ||
|
|
594
|
-
workspaceChanges.length > 0 ||
|
|
595
|
-
best.floatingMembers.length > 0,
|
|
551
|
+
needsRoundTrip: reResolutionSet.length > 0 || workspaceChanges.length > 0,
|
|
596
552
|
externalConstraints,
|
|
597
553
|
applicable: true,
|
|
598
554
|
});
|
package/src/index.ts
CHANGED
|
@@ -116,6 +116,15 @@ export type {
|
|
|
116
116
|
} from "./planClusterApply.ts";
|
|
117
117
|
export { diffDuplicates } from "./duplicateSnapshot.ts";
|
|
118
118
|
export type { DuplicateDiff, DuplicateSnapshot } from "./duplicateSnapshot.ts";
|
|
119
|
+
export {
|
|
120
|
+
heldOverrides,
|
|
121
|
+
outstandingOverrides,
|
|
122
|
+
reuseKeys,
|
|
123
|
+
} from "./clusterOverrides.ts";
|
|
124
|
+
export type {
|
|
125
|
+
OverrideRoundStates,
|
|
126
|
+
OverrideTargetState,
|
|
127
|
+
} from "./clusterOverrides.ts";
|
|
119
128
|
export { partitionUnconditionalOverrides } from "./unconditionalOverrides.ts";
|
|
120
129
|
export type {
|
|
121
130
|
PartitionedOverrides,
|
|
@@ -14,7 +14,6 @@ const fix = (overrides: Partial<ClusterFix>): ClusterFix => ({
|
|
|
14
14
|
excludedMembers: [],
|
|
15
15
|
anchor: null,
|
|
16
16
|
reuseFixes: [],
|
|
17
|
-
floatingMembers: [],
|
|
18
17
|
workspaceChanges: [],
|
|
19
18
|
reResolutionSet: [],
|
|
20
19
|
externalConstraints: [],
|
|
@@ -106,25 +105,38 @@ describe("planClusterApply", () => {
|
|
|
106
105
|
]);
|
|
107
106
|
});
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
108
|
+
const anchoredFix = fix({
|
|
109
|
+
applicable: true,
|
|
110
|
+
target: "0.87.0",
|
|
111
|
+
convergentMembers: ["mini-metro-config"],
|
|
112
|
+
anchor: "0.84.5",
|
|
113
|
+
reuseFixes: [
|
|
114
|
+
{
|
|
115
|
+
requester: "mini-plugin@1.0.0",
|
|
116
|
+
requesterName: "mini-plugin",
|
|
117
|
+
packageName: "mini-metro-config",
|
|
118
|
+
range: "*",
|
|
119
|
+
from: "0.87.0",
|
|
120
|
+
to: "0.84.5",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("leaves the reuse out when asked, before it can outrank the target", () => {
|
|
126
|
+
const plan = planClusterApply([anchoredFix], { reuse: false });
|
|
127
|
+
|
|
128
|
+
deepStrictEqual(plan.overrides, [
|
|
129
|
+
{
|
|
130
|
+
packageName: "mini-metro-config",
|
|
131
|
+
version: "0.87.0",
|
|
132
|
+
reason: "converge",
|
|
133
|
+
},
|
|
127
134
|
]);
|
|
135
|
+
deepStrictEqual(plan.conflicts, []);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("lets the anchored reuse win over a computed target", () => {
|
|
139
|
+
const plan = planClusterApply([anchoredFix]);
|
|
128
140
|
|
|
129
141
|
deepStrictEqual(plan.overrides, [
|
|
130
142
|
{
|
package/src/planClusterApply.ts
CHANGED
|
@@ -37,9 +37,14 @@ export interface ClusterApplyPlan {
|
|
|
37
37
|
* `excludedMembers` lists.
|
|
38
38
|
*
|
|
39
39
|
* `reuseFixes` come first: they converge on `anchor`, the version the user
|
|
40
|
-
* pinned, and a pin outranks a version the detector merely computed.
|
|
40
|
+
* pinned, and a pin outranks a version the detector merely computed. A package
|
|
41
|
+
* manager that cannot hold a reuse without an override passes `reuse: false`,
|
|
42
|
+
* so they are left out before they can outrank a convergence.
|
|
41
43
|
*/
|
|
42
|
-
export const planClusterApply = (
|
|
44
|
+
export const planClusterApply = (
|
|
45
|
+
fixes: ClusterFix[],
|
|
46
|
+
{ reuse = true }: { reuse?: boolean } = {},
|
|
47
|
+
): ClusterApplyPlan => {
|
|
43
48
|
const manifestEdits: PlannedManifestEdit[] = [];
|
|
44
49
|
const unresolvableChanges: string[] = [];
|
|
45
50
|
const conflicts: ClusterApplyPlan["conflicts"] = [];
|
|
@@ -64,9 +69,9 @@ export const planClusterApply = (fixes: ClusterFix[]): ClusterApplyPlan => {
|
|
|
64
69
|
overrides.set(packageName, { packageName, version, reason });
|
|
65
70
|
};
|
|
66
71
|
|
|
67
|
-
for (const fix of fixes) {
|
|
68
|
-
for (const
|
|
69
|
-
addOverride(
|
|
72
|
+
for (const fix of reuse ? fixes : []) {
|
|
73
|
+
for (const reuseFix of fix.reuseFixes) {
|
|
74
|
+
addOverride(reuseFix.packageName, reuseFix.to, "reuse");
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
|
|
@@ -32,7 +32,6 @@ const clusterFix = (overrides: Partial<ClusterFix> = {}): ClusterFix => ({
|
|
|
32
32
|
excludedMembers: [],
|
|
33
33
|
anchor: null,
|
|
34
34
|
reuseFixes: [],
|
|
35
|
-
floatingMembers: [],
|
|
36
35
|
workspaceChanges: [],
|
|
37
36
|
reResolutionSet: ["metro"],
|
|
38
37
|
externalConstraints: [metroPin],
|
|
@@ -443,13 +442,6 @@ describe("renderDuplicatesReport", () => {
|
|
|
443
442
|
ok(render({ clusterFixes: [clusterFix()] }).includes("(1 member follows)"));
|
|
444
443
|
});
|
|
445
444
|
|
|
446
|
-
it("names the members whose version the resolver picks", () => {
|
|
447
|
-
const output = render({
|
|
448
|
-
clusterFixes: [clusterFix({ floatingMembers: ["metro-config"] })],
|
|
449
|
-
});
|
|
450
|
-
ok(output.includes(" Resolver picks: metro-config"));
|
|
451
|
-
});
|
|
452
|
-
|
|
453
445
|
it("has no Dedupe section for an unfixable cluster", () => {
|
|
454
446
|
const output = render({
|
|
455
447
|
clusterFixes: [
|
|
@@ -678,7 +670,6 @@ describe("renderDuplicatesReport", () => {
|
|
|
678
670
|
nonNpmCount: 0,
|
|
679
671
|
},
|
|
680
672
|
},
|
|
681
|
-
floatingMembers: ["metro-config-long-name"],
|
|
682
673
|
anchor: "0.84.5",
|
|
683
674
|
reuseFixes: [
|
|
684
675
|
{
|
|
@@ -493,13 +493,6 @@ const renderCluster = (
|
|
|
493
493
|
` ${color("dim", "Driven by:")} ${nameList(color, fix.driverMembers)}${follow}`,
|
|
494
494
|
);
|
|
495
495
|
}
|
|
496
|
-
// nothing pins these, so the package manager picks their version: the
|
|
497
|
-
// fixable count above is not a guarantee for them
|
|
498
|
-
if (fix.floatingMembers.length > 0) {
|
|
499
|
-
log(
|
|
500
|
-
` ${color("dim", "Resolver picks:")} ${nameList(color, fix.floatingMembers)}`,
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
496
|
}
|
|
504
497
|
|
|
505
498
|
if (fix.excludedMembers.length > 0) {
|
|
@@ -31,7 +31,8 @@ const accepts = (version: string, range: string): boolean =>
|
|
|
31
31
|
* dependent onto a version it never allowed.
|
|
32
32
|
*
|
|
33
33
|
* The `converge` overrides are safe by construction: the detector only converges
|
|
34
|
-
* a member no third-party range holds elsewhere
|
|
34
|
+
* a member no third-party range holds elsewhere, and only re-resolves one no
|
|
35
|
+
* in-cluster requester pins elsewhere either. A `reuse` override is derived
|
|
35
36
|
* from a single requester's range, and another requester can still reject it.
|
|
36
37
|
*/
|
|
37
38
|
export const partitionUnconditionalOverrides = (
|