diff --git a/README.md b/README.md index 9deecf7..dbf02a7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This action compares dependencies between your base branch and current branch, a - šŸ”’ **Package trust levels** - Detects decreases in package trust levels (provenance and trusted publisher status) - šŸ“ˆ **Dependency growth** - Warns when dependency count increases significantly - šŸ“¦ **Install size** - Warns when package size increases significantly -- šŸ”„ **Duplicate versions** - Detects packages with multiple versions installed +- šŸ”„ **Duplicate versions** - Detects packages which gain newly introduced duplicate versions - āš ļø **Module replacements** - Identifies new packages that have community-recommended alternatives ## Usage @@ -44,7 +44,7 @@ jobs: | `pr-number` | The number of the pull request to comment on | Yes | `${{ github.event.pull_request.number }}` | | `dependency-threshold` | Threshold for warning about significant increase in number of dependencies | No | `10` | | `size-threshold` | Threshold (in bytes) for warning about significant increase in package size | No | `100000` | -| `duplicate-threshold` | Threshold for warning about packages with multiple versions | No | `1` | +| `duplicate-threshold` | Threshold for warning about packages which gain newly introduced duplicate versions | No | `1` | | `base-packages` | Glob pattern for base branch pack files (e.g., `"./base-packs/*.tgz"`) | No | None | | `source-packages` | Glob pattern for source branch pack files (e.g., `"./source-packs/*.tgz"`) | No | None | | `pack-size-threshold` | Threshold (in bytes) for warning about significant increase in total pack size. Set to `-1` to always report size changes. | No | `50000` | diff --git a/action.yml b/action.yml index 1e5fc1f..eca713e 100644 --- a/action.yml +++ b/action.yml @@ -37,7 +37,7 @@ inputs: required: false default: '50000' duplicate-threshold: - description: 'Threshold for warning about packages with multiple versions' + description: 'Threshold for warning about packages which gain newly introduced duplicate versions' required: false default: '1' detect-replacements: diff --git a/build/main.js b/build/main.js index b71b55a..c4e08df 100644 --- a/build/main.js +++ b/build/main.js @@ -24615,11 +24615,12 @@ function computeParentPaths(lockfile, duplicateDependencyNames, dependencyMap) { traverse(lockfile.root, visitor); return parentPaths; } -function scanForDuplicates(messages, threshold, dependencyMap, lockfilePath, lockfile) { +function scanForDuplicates(messages, threshold, dependencyMap, baseDependencyMap, lockfilePath, lockfile) { const duplicateRows = []; const duplicateDependencyNames = /* @__PURE__ */ new Set(); for (const [packageName, currentVersionSet] of dependencyMap) { - if (currentVersionSet.size > threshold) { + const baseVersionCount = baseDependencyMap.get(packageName)?.size ?? 0; + if (currentVersionSet.size > threshold && currentVersionSet.size > baseVersionCount) { duplicateDependencyNames.add(packageName); } } @@ -24673,7 +24674,7 @@ function scanForDuplicates(messages, threshold, dependencyMap, lockfilePath, loc \u{1F4A1} To find out what depends on a specific package, run: \`${exampleCommand}\`` : ""; messages.push( - `## \u26A0\uFE0F Duplicate Dependencies (found: ${duplicateRows.length}, threshold: ${threshold}) + `## \u26A0\uFE0F New Duplicate Dependencies (found: ${duplicateRows.length}, threshold: ${threshold}) | \u{1F4E6} Package | \u{1F4CB} Versions | | --- | --- | @@ -25129,6 +25130,7 @@ async function analyzeAndComment() { messages, duplicateThreshold, currentDeps, + baseDeps, lockfilePath, parsedCurrentLock ); diff --git a/src/checks/duplicates.ts b/src/checks/duplicates.ts index 80038a5..5c302ec 100644 --- a/src/checks/duplicates.ts +++ b/src/checks/duplicates.ts @@ -56,6 +56,7 @@ export function scanForDuplicates( messages: string[], threshold: number, dependencyMap: Map>, + baseDependencyMap: Map>, lockfilePath: string, lockfile: ParsedLockFile ): void { @@ -63,7 +64,11 @@ export function scanForDuplicates( const duplicateDependencyNames = new Set(); for (const [packageName, currentVersionSet] of dependencyMap) { - if (currentVersionSet.size > threshold) { + const baseVersionCount = baseDependencyMap.get(packageName)?.size ?? 0; + if ( + currentVersionSet.size > threshold && + currentVersionSet.size > baseVersionCount + ) { duplicateDependencyNames.add(packageName); } } @@ -126,7 +131,7 @@ export function scanForDuplicates( ? `\n\nšŸ’” To find out what depends on a specific package, run: \`${exampleCommand}\`` : ''; messages.push( - `## āš ļø Duplicate Dependencies (found: ${duplicateRows.length}, threshold: ${threshold}) + `## āš ļø New Duplicate Dependencies (found: ${duplicateRows.length}, threshold: ${threshold}) | šŸ“¦ Package | šŸ“‹ Versions | | --- | --- | diff --git a/src/main.ts b/src/main.ts index d34fd47..24d664b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -174,6 +174,7 @@ async function analyzeAndComment(): Promise { messages, duplicateThreshold, currentDeps, + baseDeps, lockfilePath, parsedCurrentLock ); diff --git a/test/checks/__snapshots__/duplicates_test.ts.snap b/test/checks/__snapshots__/duplicates_test.ts.snap index f899c29..4d4f3eb 100644 --- a/test/checks/__snapshots__/duplicates_test.ts.snap +++ b/test/checks/__snapshots__/duplicates_test.ts.snap @@ -1,8 +1,20 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`scanForDuplicates > should only report packages with more versions than the base 1`] = ` +[ + "## āš ļø New Duplicate Dependencies (found: 1, threshold: 1) + +| šŸ“¦ Package | šŸ“‹ Versions | +| --- | --- | +| package-a |
2 versions
  • root-package@1.0.0
    • **package-a@1.0.0**

  • root-package@1.0.0
    • package-b@3.0.0
      • **package-a@1.1.0**

| + +šŸ’” To find out what depends on a specific package, run: \`npm ls example-package\`", +] +`; + exports[`scanForDuplicates > should report duplicates when threshold is exceeded 1`] = ` [ - "## āš ļø Duplicate Dependencies (found: 1, threshold: 1) + "## āš ļø New Duplicate Dependencies (found: 1, threshold: 1) | šŸ“¦ Package | šŸ“‹ Versions | | --- | --- | @@ -14,7 +26,7 @@ exports[`scanForDuplicates > should report duplicates when threshold is exceeded exports[`scanForDuplicates > should truncate long parent paths in the report 1`] = ` [ - "## āš ļø Duplicate Dependencies (found: 1, threshold: 1) + "## āš ļø New Duplicate Dependencies (found: 1, threshold: 1) | šŸ“¦ Package | šŸ“‹ Versions | | --- | --- | diff --git a/test/checks/duplicates_test.ts b/test/checks/duplicates_test.ts index f133055..082baa7 100644 --- a/test/checks/duplicates_test.ts +++ b/test/checks/duplicates_test.ts @@ -28,6 +28,7 @@ describe('scanForDuplicates', () => { messages, threshold, dependencyMap, + new Map(), lockfilePath, lockfile ); @@ -84,6 +85,7 @@ describe('scanForDuplicates', () => { messages, threshold, dependencyMap, + new Map(), lockfilePath, lockfile ); @@ -140,6 +142,7 @@ describe('scanForDuplicates', () => { messages, threshold, dependencyMap, + new Map(), lockfilePath, lockfile ); @@ -203,6 +206,189 @@ describe('scanForDuplicates', () => { messages, threshold, dependencyMap, + new Map(), + lockfilePath, + lockfile + ); + + expect(messages).toMatchSnapshot(); + }); + + it('should not report duplicates which already exist in the base', () => { + const messages: string[] = []; + const threshold = 1; + const dependencyMap = new Map>([ + ['package-a', new Set(['1.0.0', '1.1.0'])] + ]); + const baseDependencyMap = new Map>([ + ['package-a', new Set(['1.0.0', '1.1.0'])] + ]); + const lockfilePath = 'package-lock.json'; + const lockfile: ParsedLockFile = { + type: 'npm', + packages: [], + root: { + name: 'root-package', + version: '1.0.0', + dependencies: [], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [] + } + }; + + scanForDuplicates( + messages, + threshold, + dependencyMap, + baseDependencyMap, + lockfilePath, + lockfile + ); + + expect(messages).toHaveLength(0); + }); + + it('should not report duplicates when the version count is unchanged', () => { + const messages: string[] = []; + const threshold = 1; + const dependencyMap = new Map>([ + ['package-a', new Set(['1.0.0', '1.2.0'])] + ]); + const baseDependencyMap = new Map>([ + ['package-a', new Set(['1.0.0', '1.1.0'])] + ]); + const lockfilePath = 'package-lock.json'; + const lockfile: ParsedLockFile = { + type: 'npm', + packages: [], + root: { + name: 'root-package', + version: '1.0.0', + dependencies: [], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [] + } + }; + + scanForDuplicates( + messages, + threshold, + dependencyMap, + baseDependencyMap, + lockfilePath, + lockfile + ); + + expect(messages).toHaveLength(0); + }); + + it('should not report duplicates when versions are removed', () => { + const messages: string[] = []; + const threshold = 1; + const dependencyMap = new Map>([ + ['package-a', new Set(['1.0.0', '1.1.0'])] + ]); + const baseDependencyMap = new Map>([ + ['package-a', new Set(['1.0.0', '1.1.0', '1.2.0'])] + ]); + const lockfilePath = 'package-lock.json'; + const lockfile: ParsedLockFile = { + type: 'npm', + packages: [], + root: { + name: 'root-package', + version: '1.0.0', + dependencies: [], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [] + } + }; + + scanForDuplicates( + messages, + threshold, + dependencyMap, + baseDependencyMap, + lockfilePath, + lockfile + ); + + expect(messages).toHaveLength(0); + }); + + it('should only report packages with more versions than the base', () => { + const messages: string[] = []; + const threshold = 1; + const dependencyMap = new Map>([ + ['package-a', new Set(['1.0.0', '1.1.0'])], + ['package-c', new Set(['2.0.0', '2.1.0'])] + ]); + const baseDependencyMap = new Map>([ + ['package-a', new Set(['1.0.0'])], + ['package-c', new Set(['2.0.0', '2.1.0'])] + ]); + const lockfilePath = 'package-lock.json'; + const packageA: ParsedDependency = { + name: 'package-a', + version: '1.0.0', + dependencies: [], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [] + }; + const packageAAlt: ParsedDependency = { + name: 'package-a', + version: '1.1.0', + dependencies: [], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [] + }; + const packageC: ParsedDependency = { + name: 'package-c', + version: '2.0.0', + dependencies: [], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [] + }; + const packageCAlt: ParsedDependency = { + name: 'package-c', + version: '2.1.0', + dependencies: [], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [] + }; + const packageB: ParsedDependency = { + name: 'package-b', + version: '3.0.0', + dependencies: [packageAAlt, packageCAlt], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [] + }; + const lockfile: ParsedLockFile = { + type: 'npm', + packages: [packageA, packageAAlt, packageB, packageC, packageCAlt], + root: { + name: 'root-package', + version: '1.0.0', + dependencies: [packageA, packageB, packageC], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [] + } + }; + + scanForDuplicates( + messages, + threshold, + dependencyMap, + baseDependencyMap, lockfilePath, lockfile );