From e965e07abfd71bf0ae6ad37657d83715e0b8d437 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:24:35 +0100 Subject: [PATCH] feat: remove duplicate dependencies check This isn't really actionable information so it just adds a _lot_ of noise, given almost all of us have duplicates deeply in our `node_modules`. Instead of adding piles of complexity to try get this right, I think we're better just not doing it. --- src/analyze/duplicate-dependencies.ts | 172 ---------------- src/analyze/report.ts | 2 - src/test/__snapshots__/cli.test.ts.snap | 18 +- .../duplicate-dependencies.test.ts.snap | 43 ---- src/test/duplicate-dependencies.test.ts | 185 ------------------ 5 files changed, 8 insertions(+), 412 deletions(-) delete mode 100644 src/analyze/duplicate-dependencies.ts delete mode 100644 src/test/__snapshots__/duplicate-dependencies.test.ts.snap delete mode 100644 src/test/duplicate-dependencies.test.ts diff --git a/src/analyze/duplicate-dependencies.ts b/src/analyze/duplicate-dependencies.ts deleted file mode 100644 index c420ea2..0000000 --- a/src/analyze/duplicate-dependencies.ts +++ /dev/null @@ -1,172 +0,0 @@ -import {styleText} from 'node:util'; -import {ParsedLockFile, traverse, VisitorFn} from 'lockparse'; -import {AnalysisContext, Message, ReportPluginResult, Stats} from '../types.js'; - -interface Version { - version: string; - parents: string[]; -} - -/** - * Outputs packages with duplicate versions and suggest possible fixes - * @param context - */ -export async function runDuplicateDependencyAnalysis( - context: AnalysisContext -): Promise { - const lockfile = context.lockfile; - - if (!lockfile) { - throw new Error('No lock file found.'); - } - - const duplicateDependencies = resolveDuplicateDependencies(lockfile); - await computeParents(lockfile, duplicateDependencies); - return exportOutput(duplicateDependencies); -} - -/** - * Computes a map of package names to their unique versions using the lock file - * It returns just the packages with multiple versions - * @param lockfile - */ -function resolveDuplicateDependencies( - lockfile: ParsedLockFile -): Map { - const resolvedDependencies: Map = new Map(); - for (const pkg of lockfile.packages) { - const entry: Version = { - version: pkg.version, - parents: [] - }; - if (!resolvedDependencies.has(pkg.name)) { - resolvedDependencies.set(pkg.name, [entry]); - } else { - const packageEntries = resolvedDependencies.get(pkg.name); - if ( - packageEntries && - !packageEntries.some((x) => x.version === pkg.version) - ) { - packageEntries.push(entry); - } - } - } - - // find all the packages that have more than one version - const duplicateDependencies: Map = new Map(); - for (const [packageName, versions] of resolvedDependencies) { - if (versions.length <= 1) { - continue; - } - duplicateDependencies.set(packageName, versions); - } - return duplicateDependencies; -} - -/** - * Compute all the parent packages that use each duplicate dependency - * @param lockfile - * @param duplicateDependencies - */ -async function computeParents( - lockfile: ParsedLockFile, - duplicateDependencies: Map -) { - const visitorFn: VisitorFn = (node, parent, _path) => { - if (!duplicateDependencies.has(node.name) || !parent) { - return; - } - const resolvedVersions = duplicateDependencies.get(node.name); - if (!resolvedVersions) { - return; - } - - // get the correct version - const version = resolvedVersions.find((x) => x.version === node.version); - if (!version) { - return; - } - - const parentPath = `${parent.name}@${parent.version}`; - if (version.parents.includes(parentPath)) { - return; - } - - version.parents.push(parentPath); - }; - const visitor = { - dependency: visitorFn, - devDependency: visitorFn, - optionalDependency: visitorFn - }; - - traverse(lockfile.root, visitor); -} - -function exportOutput(duplicateDependencies: Map) { - const messages: Message[] = []; - const stats: Partial = { - extraStats: [ - { - name: 'duplicateDependencyCount', - value: duplicateDependencies.size, - label: 'Duplicate Dependency Count' - } - ] - }; - if (duplicateDependencies.size === 0) { - return {stats, messages}; - } - - for (const [packageName, duplicate] of duplicateDependencies) { - let message = `${styleText('green', '[duplicate dependency]')} ${styleText('bold', packageName)} has ${duplicate.length} installed versions:`; - - for (const version of duplicate) { - message += `\n${styleText('yellow', version.version)} via the following ${version.parents.length} package(s) ${styleText('blue', version.parents.join(', '))}`; - } - - const suggestions = generateSuggestionsForDuplicate(duplicate); - - if (suggestions.length > 0) { - message += '\n💡 Suggestions'; - for (const suggestion of suggestions) { - message += `${styleText('gray', suggestion)}`; - } - } - message += '\n'; - messages.push({ - message, - severity: 'warning', - score: 0 - }); - } - - return {stats, messages}; -} - -/** - * Generates suggestions for resolving duplicates - */ -function generateSuggestionsForDuplicate( - resolvedVersions: Version[] -): string[] { - const suggestions: string[] = []; - - // Find the package version with the most parents - const mostCommonVersion = resolvedVersions.sort( - (a, b) => b.parents.length - a.parents.length - )[0]; - - if (mostCommonVersion?.parents.length > 1) { - suggestions.push( - `\n- Consider standardizing on version ${mostCommonVersion.version} as this version is the most commonly used.` - ); - } - - // Suggest checking for newer versions of consuming packages - suggestions.push( - `\n- Consider upgrading consuming packages as this may resolve this duplicate version.` - ); - - return suggestions; -} diff --git a/src/analyze/report.ts b/src/analyze/report.ts index 37e631e..89bae7f 100644 --- a/src/analyze/report.ts +++ b/src/analyze/report.ts @@ -15,7 +15,6 @@ import {runDependencyAnalysis} from './dependencies.js'; import {runPlugins} from '../plugin-runner.js'; import {getPackageJson, detectLockfile} from '../utils/package-json.js'; import {parse as parseLockfile} from 'lockparse'; -import {runDuplicateDependencyAnalysis} from './duplicate-dependencies.js'; import {runCoreJsAnalysis} from './core-js.js'; import {runWebFeaturesCodemodsAnalysis} from './web-features-codemods.js'; @@ -23,7 +22,6 @@ const plugins: ReportPlugin[] = [ runPublint, runReplacements, runDependencyAnalysis, - runDuplicateDependencyAnalysis, runCoreJsAnalysis, runWebFeaturesCodemodsAnalysis ]; diff --git a/src/test/__snapshots__/cli.test.ts.snap b/src/test/__snapshots__/cli.test.ts.snap index bd6e3f4..3e7ef4a 100644 --- a/src/test/__snapshots__/cli.test.ts.snap +++ b/src/test/__snapshots__/cli.test.ts.snap @@ -6,11 +6,10 @@ exports[`CLI > should display package report 1`] = ` ┌ Analyzing... │ ● Summary -│ Package Name mock-package -│ Version 1.0.0 -│ Install Size 53.0 B -│ Dependencies 1 (1 production, 0 development) -│ Duplicate Dependency Count 0 +│ Package Name mock-package +│ Version 1.0.0 +│ Install Size 53.0 B +│ Dependencies 1 (1 production, 0 development) │ ● Results: │ @@ -28,11 +27,10 @@ exports[`CLI > should run successfully with default options 1`] = ` ┌ Analyzing... │ ● Summary -│ Package Name mock-package -│ Version 1.0.0 -│ Install Size 53.0 B -│ Dependencies 1 (1 production, 0 development) -│ Duplicate Dependency Count 0 +│ Package Name mock-package +│ Version 1.0.0 +│ Install Size 53.0 B +│ Dependencies 1 (1 production, 0 development) │ ● Results: │ diff --git a/src/test/__snapshots__/duplicate-dependencies.test.ts.snap b/src/test/__snapshots__/duplicate-dependencies.test.ts.snap deleted file mode 100644 index 0298ac2..0000000 --- a/src/test/__snapshots__/duplicate-dependencies.test.ts.snap +++ /dev/null @@ -1,43 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Duplicate Dependency Detection > should detect multiple versions 1`] = ` -{ - "messages": [ - { - "message": "[duplicate dependency] shared-lib has 2 installed versions: -1.0.0 via the following 2 package(s) package-a@1.0.0, package-c@1.0.0 -2.0.0 via the following 1 package(s) package-b@1.0.0 -💡 Suggestions -- Consider standardizing on version 1.0.0 as this version is the most commonly used. -- Consider upgrading consuming packages as this may resolve this duplicate version. -", - "score": 0, - "severity": "warning", - }, - ], - "stats": { - "extraStats": [ - { - "label": "Duplicate Dependency Count", - "name": "duplicateDependencyCount", - "value": 1, - }, - ], - }, -} -`; - -exports[`Duplicate Dependency Detection > should not detect duplicates when there are none 1`] = ` -{ - "messages": [], - "stats": { - "extraStats": [ - { - "label": "Duplicate Dependency Count", - "name": "duplicateDependencyCount", - "value": 0, - }, - ], - }, -} -`; diff --git a/src/test/duplicate-dependencies.test.ts b/src/test/duplicate-dependencies.test.ts deleted file mode 100644 index 37c6a7d..0000000 --- a/src/test/duplicate-dependencies.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -import {describe, it, expect, beforeEach, afterEach} from 'vitest'; -import {LocalFileSystem} from '../local-file-system.js'; -import {createTempDir, cleanupTempDir} from './utils.js'; -import type {AnalysisContext} from '../types.js'; -import {runDuplicateDependencyAnalysis} from '../analyze/duplicate-dependencies.js'; -import {ParsedDependency} from 'lockparse'; - -describe('Duplicate Dependency Detection', () => { - let tempDir: string; - let fileSystem: LocalFileSystem; - let context: AnalysisContext; - - beforeEach(async () => { - tempDir = await createTempDir(); - fileSystem = new LocalFileSystem(tempDir); - }); - - afterEach(async () => { - await cleanupTempDir(tempDir); - }); - - it('should detect multiple versions', async () => { - const sharedLibv1: ParsedDependency = { - name: 'shared-lib', - version: '1.0.0', - dependencies: [], - devDependencies: [], - optionalDependencies: [], - peerDependencies: [] - }; - const sharedLibv2: ParsedDependency = { - name: 'shared-lib', - version: '2.0.0', - dependencies: [], - devDependencies: [], - optionalDependencies: [], - peerDependencies: [] - }; - const packageA: ParsedDependency = { - name: 'package-a', - version: '1.0.0', - dependencies: [sharedLibv1], - devDependencies: [], - peerDependencies: [], - optionalDependencies: [] - }; - const packageB: ParsedDependency = { - name: 'package-b', - version: '1.0.0', - dependencies: [sharedLibv2], - devDependencies: [], - peerDependencies: [], - optionalDependencies: [] - }; - const packageC: ParsedDependency = { - name: 'package-c', - version: '1.0.0', - dependencies: [sharedLibv1], - devDependencies: [], - peerDependencies: [], - optionalDependencies: [] - }; - const testPkg: ParsedDependency = { - name: 'test-package', - version: '1.0.0', - dependencies: [packageA, packageB, packageC], - devDependencies: [], - optionalDependencies: [], - peerDependencies: [] - }; - //set the context - context = { - fs: fileSystem, - root: '.', - messages: [], - stats: { - name: 'unknown', - version: 'unknown', - dependencyCount: { - production: 0, - development: 0 - }, - extraStats: [] - }, - lockfile: { - type: 'npm', - packages: [ - testPkg, - packageA, - packageB, - packageC, - sharedLibv1, - sharedLibv2 - ], - root: { - name: 'root-package', - version: '1.0.0', - dependencies: [testPkg], - devDependencies: [], - optionalDependencies: [], - peerDependencies: [] - } - }, - packageFile: { - name: 'test-package', - version: '1.0.0' - } - }; - - const stats = await runDuplicateDependencyAnalysis(context); - - expect(stats).toMatchSnapshot(); - }); - - it('should not detect duplicates when there are none', async () => { - const sharedLibv1: ParsedDependency = { - name: 'shared-lib', - version: '1.0.0', - dependencies: [], - devDependencies: [], - optionalDependencies: [], - peerDependencies: [] - }; - - const packageA: ParsedDependency = { - name: 'package-a', - version: '1.0.0', - dependencies: [sharedLibv1], - devDependencies: [], - peerDependencies: [], - optionalDependencies: [] - }; - const packageB: ParsedDependency = { - name: 'package-b', - version: '1.0.0', - dependencies: [sharedLibv1], - devDependencies: [], - peerDependencies: [], - optionalDependencies: [] - }; - const testPkg: ParsedDependency = { - name: 'test-package', - version: '1.0.0', - dependencies: [packageA, packageB], - devDependencies: [], - optionalDependencies: [], - peerDependencies: [] - }; - //set the context - context = { - fs: fileSystem, - root: '.', - messages: [], - stats: { - name: 'unknown', - version: 'unknown', - dependencyCount: { - production: 0, - development: 0 - }, - extraStats: [] - }, - lockfile: { - type: 'npm', - packages: [testPkg, packageA, packageB, sharedLibv1], - root: { - name: 'root-package', - version: '1.0.0', - dependencies: [testPkg], - devDependencies: [], - optionalDependencies: [], - peerDependencies: [] - } - }, - packageFile: { - name: 'test-package', - version: '1.0.0' - } - }; - - const stats = await runDuplicateDependencyAnalysis(context); - - expect(stats).toMatchSnapshot(); - }); -});