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.
Files changed (50) hide show
  1. package/dist/clusterOverrides.js +34 -0
  2. package/dist/clusterOverrides.js.map +1 -0
  3. package/dist/clusterOverrides.test.js +84 -0
  4. package/dist/clusterOverrides.test.js.map +1 -0
  5. package/dist/createPackageFilter.test.js +0 -1
  6. package/dist/createPackageFilter.test.js.map +1 -1
  7. package/dist/definitions/clusterOverrides.d.ts +41 -0
  8. package/dist/definitions/clusterOverrides.d.ts.map +1 -0
  9. package/dist/definitions/clusterOverrides.test.d.ts +2 -0
  10. package/dist/definitions/clusterOverrides.test.d.ts.map +1 -0
  11. package/dist/definitions/identifyLockstepClusterFixes.d.ts +0 -1
  12. package/dist/definitions/identifyLockstepClusterFixes.d.ts.map +1 -1
  13. package/dist/definitions/index.d.ts +2 -0
  14. package/dist/definitions/index.d.ts.map +1 -1
  15. package/dist/definitions/planClusterApply.d.ts +6 -2
  16. package/dist/definitions/planClusterApply.d.ts.map +1 -1
  17. package/dist/definitions/renderDuplicatesReport.d.ts.map +1 -1
  18. package/dist/definitions/unconditionalOverrides.d.ts +2 -1
  19. package/dist/definitions/unconditionalOverrides.d.ts.map +1 -1
  20. package/dist/identifyLockstepClusterFixes.js +18 -47
  21. package/dist/identifyLockstepClusterFixes.js.map +1 -1
  22. package/dist/identifyLockstepClusterFixes.test.js +212 -10
  23. package/dist/identifyLockstepClusterFixes.test.js.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/planClusterApply.js +7 -5
  27. package/dist/planClusterApply.js.map +1 -1
  28. package/dist/planClusterApply.test.js +28 -19
  29. package/dist/planClusterApply.test.js.map +1 -1
  30. package/dist/renderDuplicatesReport.js +0 -5
  31. package/dist/renderDuplicatesReport.js.map +1 -1
  32. package/dist/renderDuplicatesReport.test.js +0 -8
  33. package/dist/renderDuplicatesReport.test.js.map +1 -1
  34. package/dist/unconditionalOverrides.js +2 -1
  35. package/dist/unconditionalOverrides.js.map +1 -1
  36. package/dist/unconditionalOverrides.test.js +0 -1
  37. package/dist/unconditionalOverrides.test.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/clusterOverrides.test.ts +125 -0
  40. package/src/clusterOverrides.ts +84 -0
  41. package/src/createPackageFilter.test.ts +0 -1
  42. package/src/identifyLockstepClusterFixes.test.ts +244 -10
  43. package/src/identifyLockstepClusterFixes.ts +23 -67
  44. package/src/index.ts +9 -0
  45. package/src/planClusterApply.test.ts +31 -19
  46. package/src/planClusterApply.ts +10 -5
  47. package/src/renderDuplicatesReport.test.ts +0 -9
  48. package/src/renderDuplicatesReport.ts +0 -7
  49. package/src/unconditionalOverrides.test.ts +0 -1
  50. 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
- // Members whose version the resolver will choose: they hold a version the
401
- // family is leaving, and nothing pins them to it. Their current pins do not
402
- // block the family — the install round-trip moves them — but the version
403
- // they land on has to be verified afterwards, not asserted here.
404
- const floatingFor = (version: string): Set<string> =>
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 floating = floatingFor(version);
452
- const converging = convergentSetFor(version, floating);
408
+ const converging = convergentSetFor(version);
453
409
 
454
410
  return {
455
411
  version,
456
- floatingMembers: [...floating].toSorted((a, b) => a.localeCompare(b)),
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
- const floatingSet = new Set(best.floatingMembers);
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
- !floatingSet.has(member) &&
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
- it("lets the anchored reuse win over a computed target", () => {
110
- const plan = planClusterApply([
111
- fix({
112
- applicable: true,
113
- target: "0.87.0",
114
- convergentMembers: ["mini-metro-config"],
115
- anchor: "0.84.5",
116
- reuseFixes: [
117
- {
118
- requester: "mini-plugin@1.0.0",
119
- requesterName: "mini-plugin",
120
- packageName: "mini-metro-config",
121
- range: "*",
122
- from: "0.87.0",
123
- to: "0.84.5",
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
  {
@@ -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 = (fixes: ClusterFix[]): ClusterApplyPlan => {
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 reuse of fix.reuseFixes) {
69
- addOverride(reuse.packageName, reuse.to, "reuse");
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,6 @@ const fixWith = (constraints: ClusterExternalConstraint[]): ClusterFix => ({
31
31
  excludedMembers: [],
32
32
  anchor: null,
33
33
  reuseFixes: [],
34
- floatingMembers: [],
35
34
  workspaceChanges: [],
36
35
  reResolutionSet: [],
37
36
  externalConstraints: constraints,
@@ -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. A `reuse` override is derived
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 = (