Skip to content

Commit f89b027

Browse files
committed
feat: display dependency paths when showing duplicates
This shows the path to each version of a duplicate package.
1 parent c0eaaa1 commit f89b027

File tree

4 files changed

+105
-13
lines changed

4 files changed

+105
-13
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"@types/node": "^24.9.0",
4545
"esbuild": "^0.25.11",
4646
"eslint": "^9.38.0",
47-
"lockparse": "^0.3.0",
47+
"lockparse": "^0.4.0",
4848
"module-replacements": "^2.9.0",
4949
"pkg-types": "^2.3.0",
5050
"prettier": "^3.6.2",

src/checks/duplicates.ts

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {type ParsedLockFile, traverse, type ParsedDependency} from 'lockparse';
2+
13
function getLsCommand(
24
lockfilePath: string,
35
packageName: string
@@ -17,20 +19,104 @@ function getLsCommand(
1719
return undefined;
1820
}
1921

22+
function getParentPath(
23+
node: ParsedDependency,
24+
parentMap: WeakMap<ParsedDependency, ParsedDependency> | undefined
25+
): string[] {
26+
const parentPath: string[] = [];
27+
if (!parentMap) {
28+
return parentPath;
29+
}
30+
let currentParent = parentMap.get(node);
31+
while (currentParent) {
32+
parentPath.push(`${currentParent.name}@${currentParent.version}`);
33+
currentParent = parentMap.get(currentParent);
34+
}
35+
return parentPath;
36+
}
37+
38+
function computeParentPaths(
39+
lockfile: ParsedLockFile,
40+
duplicateDependencyNames: Set<string>,
41+
dependencyMap: Map<string, Set<string>>
42+
): Map<string, string> {
43+
const parentPaths = new Map<string, string>();
44+
45+
const visitorFn = (
46+
node: ParsedDependency,
47+
_parent: ParsedDependency | null,
48+
parentMap?: WeakMap<ParsedDependency, ParsedDependency>
49+
) => {
50+
if (!duplicateDependencyNames.has(node.name)) {
51+
return;
52+
}
53+
const versionSet = dependencyMap.get(node.name);
54+
if (!versionSet) {
55+
return;
56+
}
57+
const parentPath = getParentPath(node, parentMap);
58+
parentPaths.set(`${node.name}@${node.version}`, parentPath.join(' -> '));
59+
};
60+
const visitor = {
61+
dependency: visitorFn,
62+
devDependency: visitorFn,
63+
peerDependency: visitorFn,
64+
optionalDependency: visitorFn
65+
};
66+
for (const pkg of lockfile.packages) {
67+
visitorFn(pkg, null);
68+
traverse(pkg, visitor);
69+
}
70+
71+
return parentPaths;
72+
}
73+
2074
export function scanForDuplicates(
2175
messages: string[],
2276
threshold: number,
2377
dependencyMap: Map<string, Set<string>>,
24-
lockfilePath: string
78+
lockfilePath: string,
79+
lockfile: ParsedLockFile
2580
): void {
2681
const duplicateRows: string[] = [];
82+
const duplicateDependencyNames = new Set<string>();
83+
2784
for (const [packageName, currentVersionSet] of dependencyMap) {
2885
if (currentVersionSet.size > threshold) {
29-
const versions = Array.from(currentVersionSet).sort();
30-
duplicateRows.push(
31-
`| ${packageName} | ${currentVersionSet.size} versions | ${versions.join(', ')} |`
32-
);
86+
duplicateDependencyNames.add(packageName);
87+
}
88+
}
89+
90+
if (duplicateDependencyNames.size === 0) {
91+
return;
92+
}
93+
94+
const parentPaths = computeParentPaths(
95+
lockfile,
96+
duplicateDependencyNames,
97+
dependencyMap
98+
);
99+
100+
for (const name of duplicateDependencyNames) {
101+
const versionSet = dependencyMap.get(name);
102+
if (!versionSet) {
103+
continue;
104+
}
105+
const versions = Array.from(versionSet).sort();
106+
107+
// Build collapsible details showing where each version comes from
108+
const detailsLines: string[] = [];
109+
for (const version of versions) {
110+
const pathKey = `${name}@${version}`;
111+
const path = parentPaths.get(pathKey);
112+
const pathDisplay = path || '(root)';
113+
detailsLines.push(`**${version}**: ${pathDisplay}`);
33114
}
115+
116+
const detailsContent = detailsLines.join(' \n');
117+
const collapsibleSection = `<details><summary>${versionSet.size} version${versionSet.size > 1 ? 's' : ''}</summary>\n\n${detailsContent}\n\n</details>`;
118+
119+
duplicateRows.push(`| ${name} | ${collapsibleSection} |`);
34120
}
35121

36122
if (duplicateRows.length > 0) {
@@ -41,8 +127,8 @@ export function scanForDuplicates(
41127
messages.push(
42128
`## ⚠️ Duplicate Dependencies (threshold: ${threshold})
43129
44-
| 📦 Package | 🔢 Version Count | 📋 Versions |
45-
| --- | --- | --- |
130+
| 📦 Package | 📋 Versions |
131+
| --- | --- |
46132
${duplicateRows.join('\n')}${helpMessage}`
47133
);
48134
}

src/main.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,13 @@ async function run(): Promise<void> {
143143
currentDeps,
144144
baseDeps
145145
);
146-
scanForDuplicates(messages, duplicateThreshold, currentDeps, lockfilePath);
146+
scanForDuplicates(
147+
messages,
148+
duplicateThreshold,
149+
currentDeps,
150+
lockfilePath,
151+
parsedCurrentLock
152+
);
147153

148154
await scanForDependencySize(
149155
messages,

0 commit comments

Comments
 (0)