Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 91 additions & 11 deletions build/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -24383,17 +24383,24 @@ var visitorKeys = [
["peerDependency", "peerDependencies"],
["optionalDependency", "optionalDependencies"]
];
function traverse(node, visitor) {
function traverseInternal(node, visitor, path2) {
for (const [visitorKey, nodeKey] of visitorKeys) {
if (visitor[visitorKey]) {
const newPath = [...path2, node];
for (const dep of node[nodeKey]) {
if (visitor[visitorKey](dep, node) !== false) {
traverse(dep, visitor);
if (path2.includes(dep)) {
continue;
}
if (visitor[visitorKey](dep, node, newPath) !== false) {
traverseInternal(dep, visitor, newPath);
}
}
}
}
}
function traverse(node, visitor) {
return traverseInternal(node, visitor, []);
}

// node_modules/lockparse/lib/main.js
var typeMap = {
Expand Down Expand Up @@ -24854,16 +24861,83 @@ function getLsCommand(lockfilePath, packageName) {
}
return void 0;
}
function scanForDuplicates(messages, threshold, dependencyMap, lockfilePath) {
function computeParentPaths(lockfile, duplicateDependencyNames, dependencyMap) {
const parentPaths = /* @__PURE__ */ new Map();
const visitorFn = (node, _parent, path2) => {
if (!duplicateDependencyNames.has(node.name) || !path2) {
return;
}
const versionSet = dependencyMap.get(node.name);
if (!versionSet) {
return;
}
const nodeKey = `${node.name}@${node.version}`;
if (parentPaths.has(nodeKey)) {
return;
}
const parentPath = path2.map((node2) => `${node2.name}@${node2.version}`);
parentPaths.set(nodeKey, parentPath);
};
const visitor = {
dependency: visitorFn,
devDependency: visitorFn,
optionalDependency: visitorFn
};
traverse(lockfile.root, visitor);
return parentPaths;
}
function scanForDuplicates(messages, threshold, dependencyMap, lockfilePath, lockfile) {
const duplicateRows = [];
const duplicateDependencyNames = /* @__PURE__ */ new Set();
for (const [packageName, currentVersionSet] of dependencyMap) {
if (currentVersionSet.size > threshold) {
const versions = Array.from(currentVersionSet).sort();
duplicateRows.push(
`| ${packageName} | ${currentVersionSet.size} versions | ${versions.join(", ")} |`
);
duplicateDependencyNames.add(packageName);
}
}
if (duplicateDependencyNames.size === 0) {
return;
}
const parentPaths = computeParentPaths(
lockfile,
duplicateDependencyNames,
dependencyMap
);
for (const name of duplicateDependencyNames) {
const versionSet = dependencyMap.get(name);
if (!versionSet) {
continue;
}
const versions = Array.from(versionSet).sort();
const detailsLines = [];
for (const version of versions) {
const pathKey = `${name}@${version}`;
const pathArray = parentPaths.get(pathKey);
if (pathArray && pathArray.length > 0) {
const maxDepth = 6;
const totalDepth = pathArray.length + 1;
let displayPath;
if (totalDepth > maxDepth) {
displayPath = [
...pathArray.slice(0, 2),
"...",
...pathArray.slice(-2)
];
} else {
displayPath = pathArray;
}
let nestedList = `<li>**${name}@${version}**</li>`;
for (let i = displayPath.length - 1; i >= 0; i--) {
nestedList = `<li>${displayPath[i]}<ul>${nestedList}</ul></li>`;
}
detailsLines.push(`<ul>${nestedList}</ul>`);
} else {
detailsLines.push(`**${name}@${version}**`);
}
}
const detailsContent = detailsLines.join("<br>");
const collapsibleSection = `<details><summary>${versionSet.size} version${versionSet.size > 1 ? "s" : ""}</summary><br>${detailsContent}<br></details>`;
duplicateRows.push(`| ${name} | ${collapsibleSection} |`);
}
if (duplicateRows.length > 0) {
const exampleCommand = getLsCommand(lockfilePath, "example-package");
const helpMessage = exampleCommand ? `
Expand All @@ -24872,8 +24946,8 @@ function scanForDuplicates(messages, threshold, dependencyMap, lockfilePath) {
messages.push(
`## \u26A0\uFE0F Duplicate Dependencies (threshold: ${threshold})

| \u{1F4E6} Package | \u{1F522} Version Count | \u{1F4CB} Versions |
| --- | --- | --- |
| \u{1F4E6} Package | \u{1F4CB} Versions |
| --- | --- |
${duplicateRows.join("\n")}${helpMessage}`
);
}
Expand Down Expand Up @@ -25210,7 +25284,13 @@ async function run() {
currentDeps,
baseDeps
);
scanForDuplicates(messages, duplicateThreshold, currentDeps, lockfilePath);
scanForDuplicates(
messages,
duplicateThreshold,
currentDeps,
lockfilePath,
parsedCurrentLock
);
await scanForDependencySize(
messages,
sizeThreshold,
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@types/node": "^24.9.0",
"esbuild": "^0.25.11",
"eslint": "^9.38.0",
"lockparse": "^0.3.0",
"lockparse": "^0.5.0",
"module-replacements": "^2.9.0",
"pkg-types": "^2.3.0",
"prettier": "^3.6.2",
Expand Down
101 changes: 94 additions & 7 deletions src/checks/duplicates.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {type ParsedLockFile, traverse, type VisitorFn} from 'lockparse';

function getLsCommand(
lockfilePath: string,
packageName: string
Expand All @@ -17,22 +19,107 @@ function getLsCommand(
return undefined;
}

function computeParentPaths(
lockfile: ParsedLockFile,
duplicateDependencyNames: Set<string>,
dependencyMap: Map<string, Set<string>>
): Map<string, string[]> {
const parentPaths = new Map<string, string[]>();

const visitorFn: VisitorFn = (node, _parent, path) => {
if (!duplicateDependencyNames.has(node.name) || !path) {
return;
}
const versionSet = dependencyMap.get(node.name);
if (!versionSet) {
return;
}
const nodeKey = `${node.name}@${node.version}`;
if (parentPaths.has(nodeKey)) {
return;
}
const parentPath = path.map((node) => `${node.name}@${node.version}`);
parentPaths.set(nodeKey, parentPath);
};
const visitor = {
dependency: visitorFn,
devDependency: visitorFn,
optionalDependency: visitorFn
};

traverse(lockfile.root, visitor);

return parentPaths;
}

export function scanForDuplicates(
messages: string[],
threshold: number,
dependencyMap: Map<string, Set<string>>,
lockfilePath: string
lockfilePath: string,
lockfile: ParsedLockFile
): void {
const duplicateRows: string[] = [];
const duplicateDependencyNames = new Set<string>();

for (const [packageName, currentVersionSet] of dependencyMap) {
if (currentVersionSet.size > threshold) {
const versions = Array.from(currentVersionSet).sort();
duplicateRows.push(
`| ${packageName} | ${currentVersionSet.size} versions | ${versions.join(', ')} |`
);
duplicateDependencyNames.add(packageName);
}
}

if (duplicateDependencyNames.size === 0) {
return;
}

const parentPaths = computeParentPaths(
lockfile,
duplicateDependencyNames,
dependencyMap
);

for (const name of duplicateDependencyNames) {
const versionSet = dependencyMap.get(name);
if (!versionSet) {
continue;
}
const versions = Array.from(versionSet).sort();

const detailsLines: string[] = [];
for (const version of versions) {
const pathKey = `${name}@${version}`;
const pathArray = parentPaths.get(pathKey);
if (pathArray && pathArray.length > 0) {
const maxDepth = 6;
const totalDepth = pathArray.length + 1;

let displayPath: string[];
if (totalDepth > maxDepth) {
displayPath = [
...pathArray.slice(0, 2),
'...',
...pathArray.slice(-2)
];
} else {
displayPath = pathArray;
}

let nestedList = `<li>**${name}@${version}**</li>`;
for (let i = displayPath.length - 1; i >= 0; i--) {
nestedList = `<li>${displayPath[i]}<ul>${nestedList}</ul></li>`;
}
detailsLines.push(`<ul>${nestedList}</ul>`);
} else {
detailsLines.push(`**${name}@${version}**`);
}
}

const detailsContent = detailsLines.join('<br>');
const collapsibleSection = `<details><summary>${versionSet.size} version${versionSet.size > 1 ? 's' : ''}</summary><br>${detailsContent}<br></details>`;

duplicateRows.push(`| ${name} | ${collapsibleSection} |`);
}

if (duplicateRows.length > 0) {
const exampleCommand = getLsCommand(lockfilePath, 'example-package');
const helpMessage = exampleCommand
Expand All @@ -41,8 +128,8 @@ export function scanForDuplicates(
messages.push(
`## ⚠️ Duplicate Dependencies (threshold: ${threshold})

| 📦 Package | 🔢 Version Count | 📋 Versions |
| --- | --- | --- |
| 📦 Package | 📋 Versions |
| --- | --- |
${duplicateRows.join('\n')}${helpMessage}`
);
}
Expand Down
8 changes: 7 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,13 @@ async function run(): Promise<void> {
currentDeps,
baseDeps
);
scanForDuplicates(messages, duplicateThreshold, currentDeps, lockfilePath);
scanForDuplicates(
messages,
duplicateThreshold,
currentDeps,
lockfilePath,
parsedCurrentLock
);

await scanForDependencySize(
messages,
Expand Down
25 changes: 25 additions & 0 deletions test/checks/__snapshots__/duplicates_test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`scanForDuplicates > should report duplicates when threshold is exceeded 1`] = `
[
"## ⚠️ Duplicate Dependencies (threshold: 1)

| 📦 Package | 📋 Versions |
| --- | --- |
| package-a | <details><summary>2 versions</summary><br><ul><li>root-package@1.0.0<ul><li>**package-a@1.0.0**</li></ul></li></ul><br><ul><li>root-package@1.0.0<ul><li>package-b@2.0.0<ul><li>**package-a@1.1.0**</li></ul></li></ul></li></ul><br></details> |

💡 To find out what depends on a specific package, run: \`npm ls example-package\`",
]
`;

exports[`scanForDuplicates > should truncate long parent paths in the report 1`] = `
[
"## ⚠️ Duplicate Dependencies (threshold: 1)

| 📦 Package | 📋 Versions |
| --- | --- |
| package-a | <details><summary>2 versions</summary><br><ul><li>root-package@1.0.0<ul><li>**package-a@1.0.0**</li></ul></li></ul><br><ul><li>root-package@1.0.0<ul><li>package-0@1.0.0<ul><li>...<ul><li>package-18@1.0.0<ul><li>package-19@1.0.0<ul><li>**package-a@1.1.0**</li></ul></li></ul></li></ul></li></ul></li></ul></li></ul><br></details> |

💡 To find out what depends on a specific package, run: \`npm ls example-package\`",
]
`;
Loading