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.
Files changed (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/buildIdentifiedFixesMap.js +6 -0
  4. package/dist/buildIdentifiedFixesMap.js.map +1 -0
  5. package/dist/buildLockstepClusters.js +119 -0
  6. package/dist/buildLockstepClusters.js.map +1 -0
  7. package/dist/buildLockstepClusters.test.js +71 -0
  8. package/dist/buildLockstepClusters.test.js.map +1 -0
  9. package/dist/clusterLabel.js +59 -0
  10. package/dist/clusterLabel.js.map +1 -0
  11. package/dist/clusterLabel.test.js +56 -0
  12. package/dist/clusterLabel.test.js.map +1 -0
  13. package/dist/compareCandidateVersions.js +18 -0
  14. package/dist/compareCandidateVersions.js.map +1 -0
  15. package/dist/compareCandidateVersions.test.js +32 -0
  16. package/dist/compareCandidateVersions.test.js.map +1 -0
  17. package/dist/createPackageFilter.js +81 -0
  18. package/dist/createPackageFilter.js.map +1 -0
  19. package/dist/createPackageFilter.test.js +115 -0
  20. package/dist/createPackageFilter.test.js.map +1 -0
  21. package/dist/definitions/buildIdentifiedFixesMap.d.ts +4 -0
  22. package/dist/definitions/buildIdentifiedFixesMap.d.ts.map +1 -0
  23. package/dist/definitions/buildLockstepClusters.d.ts +8 -0
  24. package/dist/definitions/buildLockstepClusters.d.ts.map +1 -0
  25. package/dist/definitions/buildLockstepClusters.test.d.ts +2 -0
  26. package/dist/definitions/buildLockstepClusters.test.d.ts.map +1 -0
  27. package/dist/definitions/clusterLabel.d.ts +8 -0
  28. package/dist/definitions/clusterLabel.d.ts.map +1 -0
  29. package/dist/definitions/clusterLabel.test.d.ts +2 -0
  30. package/dist/definitions/clusterLabel.test.d.ts.map +1 -0
  31. package/dist/definitions/compareCandidateVersions.d.ts +10 -0
  32. package/dist/definitions/compareCandidateVersions.d.ts.map +1 -0
  33. package/dist/definitions/compareCandidateVersions.test.d.ts +2 -0
  34. package/dist/definitions/compareCandidateVersions.test.d.ts.map +1 -0
  35. package/dist/definitions/createPackageFilter.d.ts +45 -0
  36. package/dist/definitions/createPackageFilter.d.ts.map +1 -0
  37. package/dist/definitions/createPackageFilter.test.d.ts +2 -0
  38. package/dist/definitions/createPackageFilter.test.d.ts.map +1 -0
  39. package/dist/definitions/duplicateSnapshot.d.ts +8 -0
  40. package/dist/definitions/duplicateSnapshot.d.ts.map +1 -0
  41. package/dist/definitions/fileSnapshot.d.ts +7 -0
  42. package/dist/definitions/fileSnapshot.d.ts.map +1 -0
  43. package/dist/definitions/findProjectRoot.d.ts +27 -0
  44. package/dist/definitions/findProjectRoot.d.ts.map +1 -0
  45. package/dist/definitions/findProjectRoot.test.d.ts +2 -0
  46. package/dist/definitions/findProjectRoot.test.d.ts.map +1 -0
  47. package/dist/definitions/identifyLockstepClusterFixes.d.ts +89 -0
  48. package/dist/definitions/identifyLockstepClusterFixes.d.ts.map +1 -0
  49. package/dist/definitions/identifyLockstepClusterFixes.test.d.ts +2 -0
  50. package/dist/definitions/identifyLockstepClusterFixes.test.d.ts.map +1 -0
  51. package/dist/definitions/identifyResolutionFixes.d.ts +28 -0
  52. package/dist/definitions/identifyResolutionFixes.d.ts.map +1 -0
  53. package/dist/definitions/identifyResolutionFixes.test.d.ts +2 -0
  54. package/dist/definitions/identifyResolutionFixes.test.d.ts.map +1 -0
  55. package/dist/definitions/index.d.ts +44 -0
  56. package/dist/definitions/index.d.ts.map +1 -0
  57. package/dist/definitions/packageDependenciesUtils.d.ts +47 -0
  58. package/dist/definitions/packageDependenciesUtils.d.ts.map +1 -0
  59. package/dist/definitions/packageDependenciesUtils.test.d.ts +2 -0
  60. package/dist/definitions/packageDependenciesUtils.test.d.ts.map +1 -0
  61. package/dist/definitions/packageFilterArgs.d.ts +74 -0
  62. package/dist/definitions/packageFilterArgs.d.ts.map +1 -0
  63. package/dist/definitions/packageFilterArgs.test.d.ts +2 -0
  64. package/dist/definitions/packageFilterArgs.test.d.ts.map +1 -0
  65. package/dist/definitions/packageStyles.d.ts +14 -0
  66. package/dist/definitions/packageStyles.d.ts.map +1 -0
  67. package/dist/definitions/packageStyles.test.d.ts +2 -0
  68. package/dist/definitions/packageStyles.test.d.ts.map +1 -0
  69. package/dist/definitions/parseBinArgs.d.ts +14 -0
  70. package/dist/definitions/parseBinArgs.d.ts.map +1 -0
  71. package/dist/definitions/parseBinArgs.test.d.ts +2 -0
  72. package/dist/definitions/parseBinArgs.test.d.ts.map +1 -0
  73. package/dist/definitions/planClusterApply.d.ts +39 -0
  74. package/dist/definitions/planClusterApply.d.ts.map +1 -0
  75. package/dist/definitions/planClusterApply.test.d.ts +2 -0
  76. package/dist/definitions/planClusterApply.test.d.ts.map +1 -0
  77. package/dist/definitions/renderApplyPlan.d.ts +22 -0
  78. package/dist/definitions/renderApplyPlan.d.ts.map +1 -0
  79. package/dist/definitions/renderApplyPlan.test.d.ts +2 -0
  80. package/dist/definitions/renderApplyPlan.test.d.ts.map +1 -0
  81. package/dist/definitions/renderDedupeSummary.d.ts +22 -0
  82. package/dist/definitions/renderDedupeSummary.d.ts.map +1 -0
  83. package/dist/definitions/renderDedupeSummary.test.d.ts +2 -0
  84. package/dist/definitions/renderDedupeSummary.test.d.ts.map +1 -0
  85. package/dist/definitions/renderDuplicatesReport.d.ts +39 -0
  86. package/dist/definitions/renderDuplicatesReport.d.ts.map +1 -0
  87. package/dist/definitions/renderDuplicatesReport.test.d.ts +2 -0
  88. package/dist/definitions/renderDuplicatesReport.test.d.ts.map +1 -0
  89. package/dist/definitions/reportColors.d.ts +25 -0
  90. package/dist/definitions/reportColors.d.ts.map +1 -0
  91. package/dist/definitions/reportColors.test.d.ts +2 -0
  92. package/dist/definitions/reportColors.test.d.ts.map +1 -0
  93. package/dist/definitions/reportText.d.ts +29 -0
  94. package/dist/definitions/reportText.d.ts.map +1 -0
  95. package/dist/definitions/selectExplainedPackages.d.ts +22 -0
  96. package/dist/definitions/selectExplainedPackages.d.ts.map +1 -0
  97. package/dist/definitions/selectExplainedPackages.test.d.ts +2 -0
  98. package/dist/definitions/selectExplainedPackages.test.d.ts.map +1 -0
  99. package/dist/definitions/unconditionalOverrides.d.ts +24 -0
  100. package/dist/definitions/unconditionalOverrides.d.ts.map +1 -0
  101. package/dist/definitions/unconditionalOverrides.test.d.ts +2 -0
  102. package/dist/definitions/unconditionalOverrides.test.d.ts.map +1 -0
  103. package/dist/definitions/versionsSnapshot.d.ts +40 -0
  104. package/dist/definitions/versionsSnapshot.d.ts.map +1 -0
  105. package/dist/definitions/versionsSnapshot.test.d.ts +2 -0
  106. package/dist/definitions/versionsSnapshot.test.d.ts.map +1 -0
  107. package/dist/definitions/workspaceManifest.d.ts +21 -0
  108. package/dist/definitions/workspaceManifest.d.ts.map +1 -0
  109. package/dist/definitions/workspaceManifest.test.d.ts +2 -0
  110. package/dist/definitions/workspaceManifest.test.d.ts.map +1 -0
  111. package/dist/duplicateSnapshot.js +10 -0
  112. package/dist/duplicateSnapshot.js.map +1 -0
  113. package/dist/fileSnapshot.js +21 -0
  114. package/dist/fileSnapshot.js.map +1 -0
  115. package/dist/findProjectRoot.js +48 -0
  116. package/dist/findProjectRoot.js.map +1 -0
  117. package/dist/findProjectRoot.test.js +53 -0
  118. package/dist/findProjectRoot.test.js.map +1 -0
  119. package/dist/identifyLockstepClusterFixes.js +352 -0
  120. package/dist/identifyLockstepClusterFixes.js.map +1 -0
  121. package/dist/identifyLockstepClusterFixes.test.js +372 -0
  122. package/dist/identifyLockstepClusterFixes.test.js.map +1 -0
  123. package/dist/identifyResolutionFixes.js +102 -0
  124. package/dist/identifyResolutionFixes.js.map +1 -0
  125. package/dist/identifyResolutionFixes.test.js +70 -0
  126. package/dist/identifyResolutionFixes.test.js.map +1 -0
  127. package/dist/index.js +24 -0
  128. package/dist/index.js.map +1 -0
  129. package/dist/packageDependenciesUtils.js +91 -0
  130. package/dist/packageDependenciesUtils.js.map +1 -0
  131. package/dist/packageDependenciesUtils.test.js +539 -0
  132. package/dist/packageDependenciesUtils.test.js.map +1 -0
  133. package/dist/packageFilterArgs.js +65 -0
  134. package/dist/packageFilterArgs.js.map +1 -0
  135. package/dist/packageFilterArgs.test.js +93 -0
  136. package/dist/packageFilterArgs.test.js.map +1 -0
  137. package/dist/packageStyles.js +33 -0
  138. package/dist/packageStyles.js.map +1 -0
  139. package/dist/packageStyles.test.js +58 -0
  140. package/dist/packageStyles.test.js.map +1 -0
  141. package/dist/parseBinArgs.js +28 -0
  142. package/dist/parseBinArgs.js.map +1 -0
  143. package/dist/parseBinArgs.test.js +62 -0
  144. package/dist/parseBinArgs.test.js.map +1 -0
  145. package/dist/planClusterApply.js +69 -0
  146. package/dist/planClusterApply.js.map +1 -0
  147. package/dist/planClusterApply.test.js +152 -0
  148. package/dist/planClusterApply.test.js.map +1 -0
  149. package/dist/renderApplyPlan.js +51 -0
  150. package/dist/renderApplyPlan.js.map +1 -0
  151. package/dist/renderApplyPlan.test.js +106 -0
  152. package/dist/renderApplyPlan.test.js.map +1 -0
  153. package/dist/renderDedupeSummary.js +38 -0
  154. package/dist/renderDedupeSummary.js.map +1 -0
  155. package/dist/renderDedupeSummary.test.js +91 -0
  156. package/dist/renderDedupeSummary.test.js.map +1 -0
  157. package/dist/renderDuplicatesReport.js +375 -0
  158. package/dist/renderDuplicatesReport.js.map +1 -0
  159. package/dist/renderDuplicatesReport.test.js +598 -0
  160. package/dist/renderDuplicatesReport.test.js.map +1 -0
  161. package/dist/reportColors.js +51 -0
  162. package/dist/reportColors.js.map +1 -0
  163. package/dist/reportColors.test.js +49 -0
  164. package/dist/reportColors.test.js.map +1 -0
  165. package/dist/reportText.js +29 -0
  166. package/dist/reportText.js.map +1 -0
  167. package/dist/selectExplainedPackages.js +42 -0
  168. package/dist/selectExplainedPackages.js.map +1 -0
  169. package/dist/selectExplainedPackages.test.js +55 -0
  170. package/dist/selectExplainedPackages.test.js.map +1 -0
  171. package/dist/unconditionalOverrides.js +36 -0
  172. package/dist/unconditionalOverrides.js.map +1 -0
  173. package/dist/unconditionalOverrides.test.js +71 -0
  174. package/dist/unconditionalOverrides.test.js.map +1 -0
  175. package/dist/versionsSnapshot.js +49 -0
  176. package/dist/versionsSnapshot.js.map +1 -0
  177. package/dist/versionsSnapshot.test.js +79 -0
  178. package/dist/versionsSnapshot.test.js.map +1 -0
  179. package/dist/workspaceManifest.js +63 -0
  180. package/dist/workspaceManifest.js.map +1 -0
  181. package/dist/workspaceManifest.test.js +159 -0
  182. package/dist/workspaceManifest.test.js.map +1 -0
  183. package/package.json +68 -0
  184. package/src/buildIdentifiedFixesMap.ts +22 -0
  185. package/src/buildLockstepClusters.test.ts +90 -0
  186. package/src/buildLockstepClusters.ts +155 -0
  187. package/src/clusterLabel.test.ts +79 -0
  188. package/src/clusterLabel.ts +75 -0
  189. package/src/compareCandidateVersions.test.ts +39 -0
  190. package/src/compareCandidateVersions.ts +23 -0
  191. package/src/createPackageFilter.test.ts +156 -0
  192. package/src/createPackageFilter.ts +142 -0
  193. package/src/duplicateSnapshot.ts +24 -0
  194. package/src/fileSnapshot.ts +28 -0
  195. package/src/findProjectRoot.test.ts +66 -0
  196. package/src/findProjectRoot.ts +61 -0
  197. package/src/identifyLockstepClusterFixes.test.ts +462 -0
  198. package/src/identifyLockstepClusterFixes.ts +602 -0
  199. package/src/identifyResolutionFixes.test.ts +133 -0
  200. package/src/identifyResolutionFixes.ts +183 -0
  201. package/src/index.ts +123 -0
  202. package/src/packageDependenciesUtils.test.ts +593 -0
  203. package/src/packageDependenciesUtils.ts +156 -0
  204. package/src/packageFilterArgs.test.ts +116 -0
  205. package/src/packageFilterArgs.ts +98 -0
  206. package/src/packageStyles.test.ts +77 -0
  207. package/src/packageStyles.ts +42 -0
  208. package/src/parseBinArgs.test.ts +73 -0
  209. package/src/parseBinArgs.ts +33 -0
  210. package/src/planClusterApply.test.ts +163 -0
  211. package/src/planClusterApply.ts +105 -0
  212. package/src/renderApplyPlan.test.ts +136 -0
  213. package/src/renderApplyPlan.ts +95 -0
  214. package/src/renderDedupeSummary.test.ts +121 -0
  215. package/src/renderDedupeSummary.ts +78 -0
  216. package/src/renderDuplicatesReport.test.ts +724 -0
  217. package/src/renderDuplicatesReport.ts +645 -0
  218. package/src/reportColors.test.ts +57 -0
  219. package/src/reportColors.ts +78 -0
  220. package/src/reportText.ts +58 -0
  221. package/src/selectExplainedPackages.test.ts +67 -0
  222. package/src/selectExplainedPackages.ts +79 -0
  223. package/src/unconditionalOverrides.test.ts +99 -0
  224. package/src/unconditionalOverrides.ts +60 -0
  225. package/src/versionsSnapshot.test.ts +146 -0
  226. package/src/versionsSnapshot.ts +93 -0
  227. package/src/workspaceManifest.test.ts +220 -0
  228. package/src/workspaceManifest.ts +100 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Christophe Hurpeau <christophe@hurpeau.com> (https://christophe.hurpeau.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ <h1 align="center">
2
+ pm-dedup-core
3
+ </h1>
4
+
5
+ <p align="center">
6
+ package manager utils
7
+ </p>
8
+
9
+ <p align="center">
10
+ <a href="https://npmjs.org/package/pm-dedup-core"><img src="https://img.shields.io/npm/v/pm-dedup-core.svg?style=flat-square" alt="npm version"></a>
11
+ <a href="https://npmjs.org/package/pm-dedup-core"><img src="https://img.shields.io/npm/dw/pm-dedup-core.svg?style=flat-square" alt="npm downloads"></a>
12
+ <a href="https://npmjs.org/package/pm-dedup-core"><img src="https://img.shields.io/node/v/pm-dedup-core.svg?style=flat-square" alt="node version"></a>
13
+ <a href="https://npmjs.org/package/pm-dedup-core"><img src="https://img.shields.io/npm/types/pm-dedup-core.svg?style=flat-square" alt="types"></a>
14
+ </p>
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install --save pm-dedup-core
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ The package-manager-agnostic core behind
25
+ [bun-dedup](../bun-dedup), [pnpm-dedup](../pnpm-dedup) and
26
+ [yarn-berry-deduplicate](../yarn-berry-deduplicate): everything about finding
27
+ duplicates, deciding what can be merged and rendering the result lives here.
28
+ The per-manager packages only parse their lockfile into the shapes below and
29
+ write the result back.
30
+
31
+ It is published so those packages can depend on it, not as a general-purpose
32
+ library: this is internal API and it changes with them. There is no default
33
+ export — import the named ones.
34
+
35
+ ```js
36
+ import {
37
+ buildLockstepClusters,
38
+ createPackageFilter,
39
+ findProjectRoot,
40
+ identifyResolutionFixes,
41
+ } from "pm-dedup-core";
42
+
43
+ const projectDir = findProjectRoot({ lockfileName: "bun.lock" }); // null when there is none
44
+ const filter = createPackageFilter({
45
+ includeScopes: ["@babel"],
46
+ exclude: ["@babel/runtime"],
47
+ });
48
+
49
+ filter.selects("@babel/core"); // true
50
+ filter.rejectionReason("lodash"); // why it is left alone, for the report
51
+ ```
52
+
53
+ ### What a package manager has to supply
54
+
55
+ The core never touches the filesystem for you and never guesses a lockfile path.
56
+ A caller reads its own lockfile and hands over:
57
+
58
+ - a **resolutions** list — one entry per resolved copy of a package
59
+ (`ResolutionEntry`, whose `package` is `npm` or another protocol).
60
+ - a **dependents map** — for each package name, who requires it and with which
61
+ range (`ResolutionDependentsMap`). This is where the managers differ most:
62
+ see the peer-dependency notes in each package's README.
63
+ - for the cluster pass, a **lockstep graph** (`LockstepGraph`), the dependency
64
+ edges between resolved copies.
65
+
66
+ ### Areas
67
+
68
+ | Area | Exports |
69
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
70
+ | Project root | `findProjectRoot`, `resolveProjectDir` |
71
+ | CLI grammar | `parseBinArgs`, `packageFilterParseArgsOptions`, `packageFilterUsage`, `whyDuplicateParseArgsOptions`, `whyDuplicateUsage`, `toWhyDuplicateRequest` |
72
+ | Package filtering | `createPackageFilter`, `selectPackages`, `selectClusterFixes`, `describeSkippedClusterFix`, `selectExplainedPackages` |
73
+ | Names and ranges | `PackageDescriptorNameUtils`, `PackageDependencyDescriptorUtils`, `isNpmProtocol`, `isSemverComparable`, `createCandidateVersionComparator` |
74
+ | Fix identification | `identifyResolutionFixes`, `buildLockstepClusters`, `identifyLockstepClusterFixes`, `buildIdentifiedFixesMap`, `clusterLabel` |
75
+ | Applying | `planClusterApply`, `applyWorkspaceRangeEdit`, `nextSelector`, `partitionUnconditionalOverrides`, `captureFiles`, `restoreFiles` |
76
+ | Measuring a run | `buildVersionsSnapshot`, `diffVersionsSnapshots`, `countDuplicatedPackages`, `diffDuplicates` |
77
+ | Rendering | `renderDuplicatesReport`, `renderApplyPlan`, `renderDedupeSummary`, `createColorize`, `shouldColorize`, `stylePackageName`, `stylePackageReference` |
78
+
79
+ Two of these carry most of the behaviour:
80
+
81
+ - **`identifyResolutionFixes`** finds the merges reachable one package at a
82
+ time: a copy whose dependents' ranges all accept a version already in the
83
+ lockfile. This is what the lockfile pass applies.
84
+ - **`identifyLockstepClusterFixes`** finds the rest. `buildLockstepClusters`
85
+ groups packages that are always published at the same version, and a
86
+ duplicate inside such a family only goes away when the whole family moves —
87
+ which needs a real resolution, hence the separate cluster pass.
88
+
89
+ `resolveProjectDir` is the bin-facing half of `findProjectRoot`: it prints the
90
+ reason and sets `process.exitCode` when there is no lockfile, and returns
91
+ `null`, so callers guard with `if (projectDir === null) return;`. The notice
92
+ naming the lockfile goes to stderr and only when the root is not the working
93
+ directory, keeping a redirected report parseable.
94
+
95
+ ### Colors
96
+
97
+ `shouldColorize(stream)` follows the terminal: `NO_COLOR` and a piped stream turn
98
+ colors off, `FORCE_COLOR` turns them on. Every renderer takes the resulting
99
+ `Colorize`, so a report piped to a file is plain text.
100
+
101
+ ### Runtime
102
+
103
+ Node only (`>=22.18.0`), so the pnpm and yarn tools can depend on it: no
104
+ `Bun.*`, no `bun` imports. Tests run under `bun test` like the rest of the
105
+ monorepo.
@@ -0,0 +1,6 @@
1
+ import { identifyResolutionFixes, } from "./identifyResolutionFixes.js";
2
+ export const buildIdentifiedFixesMap = (duplicatesResolutionsMap, dependents) => new Map(Object.entries(duplicatesResolutionsMap).map(([packageName, resolutions]) => [
3
+ packageName,
4
+ identifyResolutionFixes(resolutions, dependents),
5
+ ]));
6
+ //# sourceMappingURL=buildIdentifiedFixesMap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildIdentifiedFixesMap.js","sourceRoot":"","sources":["../src/buildIdentifiedFixesMap.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,uBAAuB,GACxB,MAAM,8BAA8B,CAAC;AAItC,MAAM,CAAC,MAAM,uBAAuB,GAAG,CACrC,wBAAwC,EACxC,UAAsC,EACR,EAAE,CAChC,IAAI,GAAG,CACL,MAAM,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC,GAAG,CAC1C,CAAC,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC;IAC9B,WAAW;IACX,uBAAuB,CAAC,WAAW,EAAE,UAAU,CAAC;CACjD,CACF,CACF,CAAC"}
@@ -0,0 +1,119 @@
1
+ import semver from "semver";
2
+ const createUnionFind = (names) => {
3
+ const parent = new Map(names.map((name) => [name, name]));
4
+ const find = (value) => {
5
+ let root = value;
6
+ while (parent.get(root) !== root) {
7
+ root = parent.get(root);
8
+ }
9
+ let cursor = value;
10
+ while (parent.get(cursor) !== root) {
11
+ const next = parent.get(cursor);
12
+ parent.set(cursor, root);
13
+ cursor = next;
14
+ }
15
+ return root;
16
+ };
17
+ const union = (a, b) => {
18
+ const rootA = find(a);
19
+ const rootB = find(b);
20
+ if (rootA !== rootB) {
21
+ parent.set(rootA, rootB);
22
+ }
23
+ };
24
+ return { find, union };
25
+ };
26
+ // npm versions installed for each name, normalized, for anchor lookups.
27
+ const collectNpmVersions = (graph) => {
28
+ const versionsByName = new Map();
29
+ for (const [name, resolutions] of Object.entries(graph)) {
30
+ const versions = new Set();
31
+ for (const resolution of resolutions) {
32
+ if (!resolution.isNpm)
33
+ continue;
34
+ const parsed = semver.parse(resolution.version);
35
+ if (parsed)
36
+ versions.add(parsed.version);
37
+ }
38
+ versionsByName.set(name, versions);
39
+ }
40
+ return versionsByName;
41
+ };
42
+ // A dependency edge `A@version --(range)--> B` is "co-version" when the range
43
+ // is anchored on the same version A itself carries, regardless of the caret /
44
+ // tilde operator (`^8.61.0`, `8.61.0` and `~8.61.0` are all co-version with
45
+ // `8.61.0`). This distinguishes a lockstep-published family member from an
46
+ // ordinary external dependency such as `semver`/`debug`.
47
+ //
48
+ // The anchor also has to be a version of B the lock actually installs, or the
49
+ // equality is a coincidence rather than a lockstep pin: `d@1.0.1` requesting
50
+ // `type@^1.0.1` anchors on `1.0.1`, yet the only installed `type` versions are
51
+ // `1.2.0` and `2.7.2`, so `d` and `type` do not move together. Lockfiles that
52
+ // store resolved versions instead of ranges (pnpm) satisfy this by
53
+ // construction; it brings the range-storing ones in line.
54
+ const isCoVersion = (version, range, depVersions) => {
55
+ const min = (() => {
56
+ try {
57
+ return semver.minVersion(range);
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ })();
63
+ if (min === null)
64
+ return false;
65
+ return semver.eq(min, version) && depVersions.has(min.version);
66
+ };
67
+ // Detect families of packages published in lockstep (versions move together)
68
+ // via union-find over the realized resolution graph. Names A and B are unioned
69
+ // when a real dependency edge A->B exists AND across every installed resolution
70
+ // where A requests B they carry the same version, that version being one the
71
+ // lock installs for B. Returns connected components
72
+ // of size > 1, members sorted, components ordered by their first member.
73
+ export const buildLockstepClusters = (graph) => {
74
+ const names = Object.keys(graph);
75
+ const unionFind = createUnionFind(names);
76
+ const versionsByName = collectNpmVersions(graph);
77
+ for (const [name, resolutions] of Object.entries(graph)) {
78
+ const observationsByDep = new Map();
79
+ for (const resolution of resolutions) {
80
+ if (!resolution.isNpm)
81
+ continue;
82
+ for (const [depName, range] of Object.entries(resolution.dependencies)) {
83
+ if (depName === name)
84
+ continue;
85
+ // edge must point to a package that actually exists in the lock
86
+ if (!graph[depName])
87
+ continue;
88
+ let observations = observationsByDep.get(depName);
89
+ if (!observations) {
90
+ observations = [];
91
+ observationsByDep.set(depName, observations);
92
+ }
93
+ observations.push({ version: resolution.version, range });
94
+ }
95
+ }
96
+ for (const [depName, observations] of observationsByDep) {
97
+ const depVersions = versionsByName.get(depName) ?? new Set();
98
+ const allCoVersion = observations.every((observation) => isCoVersion(observation.version, observation.range, depVersions));
99
+ if (allCoVersion) {
100
+ unionFind.union(name, depName);
101
+ }
102
+ }
103
+ }
104
+ const componentsByRoot = new Map();
105
+ for (const name of names) {
106
+ const root = unionFind.find(name);
107
+ let members = componentsByRoot.get(root);
108
+ if (!members) {
109
+ members = [];
110
+ componentsByRoot.set(root, members);
111
+ }
112
+ members.push(name);
113
+ }
114
+ return [...componentsByRoot.values()]
115
+ .filter((members) => members.length > 1)
116
+ .map((members) => members.toSorted((a, b) => a.localeCompare(b)))
117
+ .toSorted((a, b) => a[0].localeCompare(b[0]));
118
+ };
119
+ //# sourceMappingURL=buildLockstepClusters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildLockstepClusters.js","sourceRoot":"","sources":["../src/buildLockstepClusters.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { buildLockstepClusters, } from "./buildLockstepClusters.js";
3
+ const npm = (version, dependencies = {}) => ({
4
+ version,
5
+ isNpm: true,
6
+ dependencies,
7
+ });
8
+ describe("buildLockstepClusters", () => {
9
+ it("unions a co-versioned family across caret and exact pins", () => {
10
+ // a@1 and a@2 both request b/c at their own version; b requests c at its
11
+ // own version. `noise` is requested at an unrelated version by everyone.
12
+ const graph = {
13
+ a: [
14
+ npm("1.0.0", { b: "1.0.0", c: "^1.0.0", noise: "^3.0.0" }),
15
+ npm("2.0.0", { b: "2.0.0", c: "^2.0.0", noise: "^3.0.0" }),
16
+ ],
17
+ b: [npm("1.0.0", { c: "1.0.0" }), npm("2.0.0", { c: "2.0.0" })],
18
+ c: [npm("1.0.0"), npm("2.0.0")],
19
+ noise: [npm("3.1.0"), npm("3.2.0")],
20
+ };
21
+ const clusters = buildLockstepClusters(graph);
22
+ expect(clusters).toEqual([["a", "b", "c"]]);
23
+ });
24
+ it("does not union when a version observation breaks co-version", () => {
25
+ // a@2 requests b at b's matching version, but a@1 requests b at a stale
26
+ // version: not every observation co-versions, so no union.
27
+ const graph = {
28
+ a: [npm("1.0.0", { b: "2.0.0" }), npm("2.0.0", { b: "2.0.0" })],
29
+ b: [npm("1.0.0"), npm("2.0.0")],
30
+ };
31
+ expect(buildLockstepClusters(graph)).toEqual([]);
32
+ });
33
+ it("does not union when the anchor version is not installed", () => {
34
+ // Real case: `d@1.0.1` requests `type@^1.0.1`, so the range anchors on
35
+ // d's own version by coincidence — no `type@1.0.1` is installed, the
36
+ // request resolves to `type@1.2.0`, and the two do not move together.
37
+ const graph = {
38
+ d: [npm("1.0.1", { type: "^1.0.1" })],
39
+ esniff: [npm("2.0.1", { d: "^1.0.1", type: "^2.7.2" })],
40
+ type: [npm("1.2.0"), npm("2.7.2")],
41
+ };
42
+ expect(buildLockstepClusters(graph)).toEqual([]);
43
+ });
44
+ it("unions when the anchor version is installed alongside others", () => {
45
+ // Same shape, but the lock does carry `b@1.0.0`: the pin is real, and the
46
+ // duplicate `b@1.2.0` is what the cluster pass exists to collapse.
47
+ const graph = {
48
+ a: [npm("1.0.0", { b: "^1.0.0" })],
49
+ b: [npm("1.0.0"), npm("1.2.0")],
50
+ };
51
+ expect(buildLockstepClusters(graph)).toEqual([["a", "b"]]);
52
+ });
53
+ it("keeps unioning pnpm-style edges that store the resolved version", () => {
54
+ // pnpm records the resolved version, so the anchor is installed by
55
+ // construction and detection is unchanged.
56
+ const graph = {
57
+ a: [npm("7.28.0", { b: "7.28.0" })],
58
+ b: [npm("7.28.0")],
59
+ };
60
+ expect(buildLockstepClusters(graph)).toEqual([["a", "b"]]);
61
+ });
62
+ it("ignores edges to packages absent from the graph and non-npm resolutions", () => {
63
+ const graph = {
64
+ a: [npm("1.0.0", { missing: "1.0.0", b: "1.0.0" })],
65
+ b: [npm("1.0.0")],
66
+ w: [{ version: "", isNpm: false, dependencies: { a: "1.0.0" } }],
67
+ };
68
+ expect(buildLockstepClusters(graph)).toEqual([["a", "b"]]);
69
+ });
70
+ });
71
+ //# sourceMappingURL=buildLockstepClusters.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildLockstepClusters.test.js","sourceRoot":"","sources":["../src/buildLockstepClusters.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ const leadingPrefixes = (member) => {
2
+ const scopeEnd = member.startsWith("@") ? member.indexOf("/") : -1;
3
+ const scope = scopeEnd === -1 ? null : member.slice(0, scopeEnd);
4
+ const rest = scope === null ? member : member.slice(scopeEnd + 1);
5
+ const segments = rest.split("-");
6
+ const withinRest = segments.map((_segment, index) => {
7
+ const joined = segments.slice(0, index + 1).join("-");
8
+ return scope === null ? joined : `${scope}/${joined}`;
9
+ });
10
+ return scope === null ? withinRest : [scope, ...withinRest];
11
+ };
12
+ const covers = (prefix, member) => member === prefix ||
13
+ member.startsWith(`${prefix}-`) ||
14
+ member.startsWith(`${prefix}/`);
15
+ /**
16
+ * The prefix shared by the most members. Ties go to the longest prefix, so a
17
+ * family named after a scope wins over the bare package name it also matches.
18
+ */
19
+ const bestGroup = (members) => {
20
+ const candidates = [...new Set(members.flatMap(leadingPrefixes))];
21
+ let best = null;
22
+ for (const prefix of candidates.toSorted((a, b) => a.localeCompare(b))) {
23
+ const covered = members.filter((member) => covers(prefix, member));
24
+ if (best === null ||
25
+ covered.length > best.covered.length ||
26
+ (covered.length === best.covered.length &&
27
+ prefix.length > best.prefix.length)) {
28
+ best = { prefix, covered };
29
+ }
30
+ }
31
+ return best;
32
+ };
33
+ const formatGroup = ({ prefix, covered }) => {
34
+ if (covered.length === 1 && covered[0] === prefix)
35
+ return prefix;
36
+ if (prefix.startsWith("@") && !prefix.includes("/"))
37
+ return `${prefix}/*`;
38
+ return covered.includes(prefix) ? `${prefix}*` : `${prefix}-*`;
39
+ };
40
+ /**
41
+ * A readable identity for a lockstep cluster, derived from the prefixes its
42
+ * members share: `@typescript-eslint/* (+1 more)`, `metro* + @react-native/*`.
43
+ * A second group is only worth naming when it covers more than one member;
44
+ * anything left over is counted, never guessed at.
45
+ */
46
+ export const clusterLabel = (members) => {
47
+ const first = bestGroup(members);
48
+ if (first === null)
49
+ return "";
50
+ const groups = [first];
51
+ const remaining = members.filter((member) => !covers(first.prefix, member));
52
+ const second = remaining.length > 0 ? bestGroup(remaining) : null;
53
+ if (second !== null && second.covered.length > 1)
54
+ groups.push(second);
55
+ const uncovered = members.filter((member) => !groups.some((group) => covers(group.prefix, member))).length;
56
+ const label = groups.map(formatGroup).join(" + ");
57
+ return uncovered === 0 ? label : `${label} (+${uncovered} more)`;
58
+ };
59
+ //# sourceMappingURL=clusterLabel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clusterLabel.js","sourceRoot":"","sources":["../src/clusterLabel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import { describe, it } from "bun:test";
2
+ import { strictEqual } from "node:assert/strict";
3
+ import { clusterLabel } from "./clusterLabel.js";
4
+ const typescriptEslint = [
5
+ "@typescript-eslint/eslint-plugin",
6
+ "@typescript-eslint/parser",
7
+ "@typescript-eslint/project-service",
8
+ "@typescript-eslint/scope-manager",
9
+ "@typescript-eslint/tsconfig-utils",
10
+ "@typescript-eslint/type-utils",
11
+ "@typescript-eslint/types",
12
+ "@typescript-eslint/typescript-estree",
13
+ "@typescript-eslint/utils",
14
+ "@typescript-eslint/visitor-keys",
15
+ "typescript-eslint",
16
+ ];
17
+ const metroFamily = [
18
+ "@react-native/asset-utils",
19
+ "@react-native/babel-plugin-codegen",
20
+ "@react-native/babel-preset",
21
+ "@react-native/codegen",
22
+ "metro",
23
+ "metro-cache",
24
+ "metro-config",
25
+ "metro-core",
26
+ "ob1",
27
+ "react-native",
28
+ ];
29
+ describe("clusterLabel", () => {
30
+ it("names a family by its shared scope", () => {
31
+ strictEqual(clusterLabel(typescriptEslint), "@typescript-eslint/* (+1 more)");
32
+ });
33
+ it("names the two largest groups and counts the rest", () => {
34
+ strictEqual(clusterLabel(metroFamily), "@react-native/* + metro* (+2 more)");
35
+ });
36
+ it("uses a dash prefix when no member carries the prefix itself", () => {
37
+ strictEqual(clusterLabel([
38
+ "babel-plugin-syntax-hermes-parser",
39
+ "hermes-estree",
40
+ "hermes-parser",
41
+ ]), "hermes-* (+1 more)");
42
+ });
43
+ it("keeps the member name when it is the prefix", () => {
44
+ strictEqual(clusterLabel(["mini-metro", "mini-metro-config"]), "mini-metro*");
45
+ });
46
+ it("names a single member exactly", () => {
47
+ strictEqual(clusterLabel(["metro"]), "metro");
48
+ });
49
+ it("does not name a second group that covers one member", () => {
50
+ strictEqual(clusterLabel(["metro", "metro-config", "ob1"]), "metro* (+1 more)");
51
+ });
52
+ it("returns an empty label for no members", () => {
53
+ strictEqual(clusterLabel([]), "");
54
+ });
55
+ });
56
+ //# sourceMappingURL=clusterLabel.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clusterLabel.test.js","sourceRoot":"","sources":["../src/clusterLabel.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,MAAM,gBAAgB,GAAG;IACvB,kCAAkC;IAClC,2BAA2B;IAC3B,oCAAoC;IACpC,kCAAkC;IAClC,mCAAmC;IACnC,+BAA+B;IAC/B,0BAA0B;IAC1B,sCAAsC;IACtC,0BAA0B;IAC1B,iCAAiC;IACjC,mBAAmB;CACpB,CAAC;AAEF,MAAM,WAAW,GAAG;IAClB,2BAA2B;IAC3B,oCAAoC;IACpC,4BAA4B;IAC5B,uBAAuB;IACvB,OAAO;IACP,aAAa;IACb,cAAc;IACd,YAAY;IACZ,KAAK;IACL,cAAc;CACf,CAAC;AAEF,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,WAAW,CACT,YAAY,CAAC,gBAAgB,CAAC,EAC9B,gCAAgC,CACjC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,WAAW,CACT,YAAY,CAAC,WAAW,CAAC,EACzB,oCAAoC,CACrC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,WAAW,CACT,YAAY,CAAC;YACX,mCAAmC;YACnC,eAAe;YACf,eAAe;SAChB,CAAC,EACF,oBAAoB,CACrB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,WAAW,CACT,YAAY,CAAC,CAAC,YAAY,EAAE,mBAAmB,CAAC,CAAC,EACjD,aAAa,CACd,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,WAAW,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,WAAW,CACT,YAAY,CAAC,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,CAAC,EAC9C,kBAAkB,CACnB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,WAAW,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,18 @@
1
+ import semver from "semver";
2
+ /**
3
+ * A version satisfying every dependent holds the maximal count, so it always
4
+ * sorts first: "the highest version that satisfies everyone" falls out of this
5
+ * ordering instead of needing a pass of its own.
6
+ */
7
+ export const createCandidateVersionComparator = ({ satisfiedCountOf } = {}) => (versionA, versionB) => {
8
+ if (satisfiedCountOf) {
9
+ const satisfiesA = satisfiedCountOf(versionA);
10
+ const satisfiesB = satisfiedCountOf(versionB);
11
+ if (satisfiesB > satisfiesA)
12
+ return 1;
13
+ if (satisfiesB < satisfiesA)
14
+ return -1;
15
+ }
16
+ return semver.rcompare(versionA, versionB);
17
+ };
18
+ //# sourceMappingURL=compareCandidateVersions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compareCandidateVersions.js","sourceRoot":"","sources":["../src/compareCandidateVersions.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAO5B;;;;GAIG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAC3C,CAAC,EAAE,gBAAgB,EAAE,GAAsC,EAAE,EAAE,EAAE,CACjE,CAAC,QAAgB,EAAE,QAAgB,EAAU,EAAE;IAC7C,IAAI,gBAAgB,EAAE,CAAC;QACrB,MAAM,UAAU,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAC9C,MAAM,UAAU,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAC9C,IAAI,UAAU,GAAG,UAAU;YAAE,OAAO,CAAC,CAAC;QACtC,IAAI,UAAU,GAAG,UAAU;YAAE,OAAO,CAAC,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC7C,CAAC,CAAC"}
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createCandidateVersionComparator } from "./compareCandidateVersions.js";
3
+ const satisfiedCounts = new Map([
4
+ ["1.0.0", 1],
5
+ ["1.5.0", 3],
6
+ ["2.0.0", 3],
7
+ ["3.0.0", 2],
8
+ ]);
9
+ const satisfiedCountOf = (version) => satisfiedCounts.get(version) ?? 0;
10
+ describe("createCandidateVersionComparator", () => {
11
+ it("puts the version satisfying the most dependents first", () => {
12
+ const compare = createCandidateVersionComparator({ satisfiedCountOf });
13
+ expect(["1.0.0", "3.0.0", "1.5.0"].toSorted(compare)).toEqual([
14
+ "1.5.0",
15
+ "3.0.0",
16
+ "1.0.0",
17
+ ]);
18
+ });
19
+ it("breaks a tie on the highest version", () => {
20
+ const compare = createCandidateVersionComparator({ satisfiedCountOf });
21
+ expect(["1.5.0", "2.0.0"].toSorted(compare)).toEqual(["2.0.0", "1.5.0"]);
22
+ });
23
+ it("ranks on semver alone without a satisfied count", () => {
24
+ const compare = createCandidateVersionComparator();
25
+ expect(["1.0.0", "3.0.0", "1.5.0"].toSorted(compare)).toEqual([
26
+ "3.0.0",
27
+ "1.5.0",
28
+ "1.0.0",
29
+ ]);
30
+ });
31
+ });
32
+ //# sourceMappingURL=compareCandidateVersions.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compareCandidateVersions.test.js","sourceRoot":"","sources":["../src/compareCandidateVersions.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,gCAAgC,EAAE,MAAM,+BAA+B,CAAC;AAEjF,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC;IAC9B,CAAC,OAAO,EAAE,CAAC,CAAC;IACZ,CAAC,OAAO,EAAE,CAAC,CAAC;IACZ,CAAC,OAAO,EAAE,CAAC,CAAC;IACZ,CAAC,OAAO,EAAE,CAAC,CAAC;CACb,CAAC,CAAC;AACH,MAAM,gBAAgB,GAAG,CAAC,OAAe,EAAU,EAAE,CACnD,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpC,QAAQ,CAAC,kCAAkC,EAAE,GAAG,EAAE;IAChD,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,OAAO,GAAG,gCAAgC,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC;QAEvE,MAAM,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;YAC5D,OAAO;YACP,OAAO;YACP,OAAO;SACR,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,OAAO,GAAG,gCAAgC,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC;QAEvE,MAAM,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,OAAO,GAAG,gCAAgC,EAAE,CAAC;QAEnD,MAAM,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;YAC5D,OAAO;YACP,OAAO;YACP,OAAO;SACR,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,81 @@
1
+ import picomatch from "picomatch";
2
+ import { clusterLabel } from "./clusterLabel.js";
3
+ const scopePattern = (scope) => `${scope.startsWith("@") ? scope : `@${scope}`}/**`;
4
+ const toPatterns = (names, scopes) => [
5
+ ...names,
6
+ ...scopes.map(scopePattern),
7
+ ];
8
+ // picomatch throws on an empty pattern list, and "no pattern" has to mean "no
9
+ // opinion" rather than "matches nothing".
10
+ const compile = (patterns) => patterns.length === 0 ? () => false : picomatch(patterns);
11
+ /**
12
+ * Selects the packages a dedupe run is allowed to touch. Deduplicating a large
13
+ * lockfile in one pass produces a diff no one reviews, so every tool exposes
14
+ * this as the way to work through it a family at a time.
15
+ *
16
+ * A package is selected when the include list is empty or one of its patterns
17
+ * matches, and no exclude pattern matches — exclusion always wins, so a scope
18
+ * can be selected with a few of its packages held back.
19
+ *
20
+ * Patterns are globs with path semantics (`*` stops at `/`), the same as the
21
+ * `why-duplicate` filters: `@babel/*` matches `@babel/core`, a bare `*` does
22
+ * not. npm names carry no glob metacharacter, so a plain name is still an
23
+ * exact match.
24
+ */
25
+ export const createPackageFilter = ({ include = [], exclude = [], includeScopes = [], excludeScopes = [], } = {}) => {
26
+ const includePatterns = toPatterns(include, includeScopes);
27
+ const excludePatterns = toPatterns(exclude, excludeScopes);
28
+ const isIncluded = compile(includePatterns);
29
+ const isExcluded = compile(excludePatterns);
30
+ const rejectionReason = (packageName) => {
31
+ if (isExcluded(packageName))
32
+ return "excluded";
33
+ if (includePatterns.length > 0 && !isIncluded(packageName)) {
34
+ return "not selected";
35
+ }
36
+ return undefined;
37
+ };
38
+ return {
39
+ selectsEverything: includePatterns.length === 0 && excludePatterns.length === 0,
40
+ selects: (packageName) => rejectionReason(packageName) === undefined,
41
+ rejectionReason,
42
+ };
43
+ };
44
+ // A map keyed by package name is where the filter applies exactly: an unselected
45
+ // package keeps every resolution it has, rather than being partly rewritten.
46
+ export const selectPackages = (packages, filter) => filter.selectsEverything
47
+ ? packages
48
+ : Object.fromEntries(Object.entries(packages).filter(([packageName]) => filter.selects(packageName)));
49
+ /**
50
+ * A cluster fix moves its whole family in lockstep, so it is kept only when
51
+ * every member is selected: applying it for part of a family would move the
52
+ * rest anyway, past the filter the user set. Select a family with a scope, or
53
+ * by naming all of its members.
54
+ */
55
+ export const selectClusterFixes = (fixes, filter) => {
56
+ if (filter.selectsEverything)
57
+ return { selected: fixes, skipped: [] };
58
+ const selected = [];
59
+ const skipped = [];
60
+ for (const fix of fixes) {
61
+ const blockedBy = fix.members.filter((member) => !filter.selects(member));
62
+ if (blockedBy.length === 0) {
63
+ selected.push(fix);
64
+ }
65
+ else {
66
+ skipped.push({ fix, blockedBy });
67
+ }
68
+ }
69
+ return { selected, skipped };
70
+ };
71
+ // A family can block on a dozen members, and naming them all buries the point.
72
+ const namedBlockers = 3;
73
+ // Phrased for the `skipped` list of an apply plan, so both tools explain a
74
+ // filtered-out family the same way.
75
+ export const describeSkippedClusterFix = ({ fix, blockedBy, }) => {
76
+ const named = blockedBy.slice(0, namedBlockers).join(", ");
77
+ const rest = blockedBy.length - namedBlockers;
78
+ const blockers = rest > 0 ? `${named} (+${rest} more)` : named;
79
+ return `cluster ${clusterLabel(fix.members)}: ${blockers} not selected by the filter, and a family only converges as a whole`;
80
+ };
81
+ //# sourceMappingURL=createPackageFilter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createPackageFilter.js","sourceRoot":"","sources":["../src/createPackageFilter.ts"],"names":[],"mappings":"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"}