diff --git a/src/analyze/dependencies.ts b/src/analyze/dependencies.ts index 4e05926..182af55 100644 --- a/src/analyze/dependencies.ts +++ b/src/analyze/dependencies.ts @@ -1,161 +1,20 @@ -import colors from 'picocolors'; import {analyzePackageModuleType} from '../compute-type.js'; import type { - PackageJsonLike, ReportPluginResult, Message, Stats, AnalysisContext } from '../types.js'; -import type {FileSystem} from '../file-system.js'; import {normalizePath} from '../utils/path.js'; +import {getPackageJson} from '../utils/package-json.js'; -interface DependencyNode { - name: string; - version: string; - // TODO (43081j): make this an array or something structured one day - path: string; // Path in dependency tree (e.g., "root > package-a > package-b") - parent?: string; // Parent package name - depth: number; // Depth in dependency tree - packagePath: string; // File system path to package.json -} - -interface DuplicateDependency { - name: string; - versions: DependencyNode[]; - severity: 'exact' | 'conflict' | 'resolvable'; - potentialSavings?: number; - suggestions?: string[]; -} - -/** - * Detects duplicate dependencies from a list of dependency nodes - */ -function detectDuplicates( - dependencyNodes: DependencyNode[] -): DuplicateDependency[] { - const duplicates: DuplicateDependency[] = []; - const packageGroups = new Map(); - - // Group dependencies by name - for (const node of dependencyNodes) { - if (!packageGroups.has(node.name)) { - packageGroups.set(node.name, []); - } - packageGroups.get(node.name)?.push(node); - } - - // Find packages with multiple versions - for (const [packageName, nodes] of packageGroups) { - if (nodes.length > 1) { - const duplicate = analyzeDuplicate(packageName, nodes); - if (duplicate) { - duplicates.push(duplicate); - } - } - } - - return duplicates; -} - -/** - * Analyzes a group of nodes for the same package to determine duplicate type - */ -function analyzeDuplicate( - packageName: string, - nodes: DependencyNode[] -): DuplicateDependency | null { - // Skip root package - if (packageName === 'root' || nodes.some((n) => n.name === 'root')) { - return null; - } - - const uniqueVersions = new Set(nodes.map((n) => n.version)); - - let severity: 'exact' | 'conflict' | 'resolvable'; - - // If more than one version, it's a conflict - if (uniqueVersions.size === 1) { - severity = 'exact'; - } else { - severity = 'conflict'; - } - - return { - name: packageName, - versions: nodes, - severity, - potentialSavings: calculatePotentialSavings(nodes), - suggestions: generateSuggestions(nodes) - }; -} - -/** - * Calculates potential savings from deduplication - */ -function calculatePotentialSavings(nodes: DependencyNode[]): number { - // For now, return a simple estimate based on number of duplicates - // TODO: Implement actual size calculation - return nodes.length - 1; -} - -/** - * Generates suggestions for resolving duplicates - */ -function generateSuggestions(nodes: DependencyNode[]): string[] { - const suggestions: string[] = []; - - // Group by version to identify the most common version - const versionCounts = new Map(); - for (const node of nodes) { - versionCounts.set(node.version, (versionCounts.get(node.version) || 0) + 1); - } - - const mostCommonVersion = Array.from(versionCounts.entries()).sort( - (a, b) => b[1] - a[1] - )[0]; - - if (mostCommonVersion && mostCommonVersion[1] > 1) { - suggestions.push( - `Consider standardizing on version ${mostCommonVersion[0]} (used by ${mostCommonVersion[1]} dependencies)` - ); - } - - // Suggest checking for newer versions of consuming packages - const uniqueParents = new Set(nodes.map((n) => n.parent).filter(Boolean)); - if (uniqueParents.size > 1) { - suggestions.push( - `Check if newer versions of consuming packages (${Array.from(uniqueParents).join(', ')}) would resolve this duplicate` - ); - } - - return suggestions; -} - -/** - * Attempts to parse a `package.json` file - */ -async function parsePackageJson( - fileSystem: FileSystem, - path: string -): Promise { - try { - return JSON.parse(await fileSystem.readFile(path)); - } catch { - return null; - } -} - -// Keep the existing tarball analysis for backward compatibility export async function runDependencyAnalysis( context: AnalysisContext ): Promise { const packageFiles = await context.fs.listPackageFiles(); - const rootDir = await context.fs.getRootDir(); - const messages: Message[] = []; - // Find root package.json - const pkg = await parsePackageJson(context.fs, '/package.json'); + const messages: Message[] = []; + const pkg = context.packageFile; if (!pkg) { throw new Error('No package.json found.'); @@ -164,43 +23,19 @@ export async function runDependencyAnalysis( const installSize = await context.fs.getInstallSize(); const prodDependencies = Object.keys(pkg.dependencies || {}).length; const devDependencies = Object.keys(pkg.devDependencies || {}).length; - const stats: Stats = { - name: pkg.name, - version: pkg.version, - installSize, - dependencyCount: { - production: prodDependencies, - development: devDependencies, - esm: 0, - cjs: 0, - duplicate: 0 - } - }; let cjsDependencies = 0; let esmDependencies = 0; - const dependencyNodes: DependencyNode[] = []; // Recursively traverse dependencies async function traverse( packagePath: string, - parent: string | undefined, depth: number, pathInTree: string ) { - const depPkg = await parsePackageJson(context.fs, packagePath); + const depPkg = await getPackageJson(context.fs, packagePath); if (!depPkg || !depPkg.name) return; - // Record this node - dependencyNodes.push({ - name: depPkg.name, - version: depPkg.version || 'unknown', - path: pathInTree, - parent, - depth, - packagePath - }); - // Only count CJS/ESM for non-root packages if (depth > 0) { const type = analyzePackageModuleType(depPkg); @@ -221,7 +56,7 @@ export async function runDependencyAnalysis( if (!packageMatch) { for (const packageFile of packageFiles) { - const depPkg = await parsePackageJson(context.fs, packageFile); + const depPkg = await getPackageJson(context.fs, packageFile); if (depPkg !== null && depPkg.name === depName) { packageMatch = packageFile; break; @@ -230,102 +65,25 @@ export async function runDependencyAnalysis( } if (packageMatch) { - await traverse( - packageMatch, - depPkg.name, - depth + 1, - pathInTree + ' > ' + depName - ); + await traverse(packageMatch, depth + 1, pathInTree + ' > ' + depName); } } } // Start traversal from root - await traverse('/package.json', undefined, 0, 'root'); - - // Collect all dependency instances for duplicate detection - // This ensures we find all versions, even those in nested node_modules - // TODO (43081j): don't do this. we're re-traversing most files just to - // find the ones that don't exist in the parent package's dependency list. - // there must be a better way - for (const file of packageFiles) { - const rootPackageJsonPath = normalizePath(rootDir) + '/package.json'; - if (file === rootPackageJsonPath) { - continue; - } - - try { - const depPkg = await parsePackageJson(context.fs, file); - if (!depPkg || !depPkg.name) { - continue; - } - - // Check if we already have this exact package in our dependency nodes - const alreadyExists = dependencyNodes.some( - (node) => node.packagePath === file - ); + await traverse('/package.json', 0, 'root'); - if (!alreadyExists) { - // Extract path information from the file path - const normalizedFile = normalizePath(file); - const pathParts = normalizedFile.split('/node_modules/'); - if (pathParts.length > 1) { - const packageDirName = pathParts[pathParts.length - 1].replace( - '/package.json', - '' - ); - const parentDirName = pathParts[pathParts.length - 2] - ?.split('/') - .pop(); - - dependencyNodes.push({ - name: depPkg.name, - version: depPkg.version || 'unknown', - path: packageDirName, - parent: parentDirName, - depth: pathParts.length - 1, - packagePath: file - }); - } - } - } catch { - // Skip invalid package.json files - } - } - - // Detect duplicates from the collected dependency nodes - const duplicateDependencies = detectDuplicates(dependencyNodes); - - stats.dependencyCount.cjs = cjsDependencies; - stats.dependencyCount.esm = esmDependencies; - - if (duplicateDependencies.length > 0) { - stats.dependencyCount.duplicate = duplicateDependencies.length; - - for (const duplicate of duplicateDependencies) { - const severityColor = - duplicate.severity === 'exact' ? colors.blue : colors.yellow; - - let message = `${severityColor('[duplicate dependency]')} ${colors.bold(duplicate.name)} has ${duplicate.versions.length} installed versions:`; - - for (const version of duplicate.versions) { - message += `\n ${colors.gray(version.version)} via ${colors.gray(version.path)}`; - } - - if (duplicate.suggestions && duplicate.suggestions.length > 0) { - message += '\nSuggestions:'; - for (const suggestion of duplicate.suggestions) { - message += ` ${colors.blue('💡')} ${colors.gray(suggestion)}`; - } - } - - messages.push({ - message, - severity: 'warning', - score: 0 - }); + const stats: Partial = { + name: pkg.name, + version: pkg.version, + installSize, + dependencyCount: { + production: prodDependencies, + development: devDependencies, + esm: esmDependencies, + cjs: cjsDependencies } - } + }; return {stats, messages}; } diff --git a/src/analyze/duplicate-dependencies.ts b/src/analyze/duplicate-dependencies.ts index 1343b34..441ec76 100644 --- a/src/analyze/duplicate-dependencies.ts +++ b/src/analyze/duplicate-dependencies.ts @@ -1,6 +1,6 @@ import colors from 'picocolors'; import {ParsedLockFile, traverse, VisitorFn} from 'lockparse'; -import {AnalysisContext, Message, ReportPluginResult} from '../types.js'; +import {AnalysisContext, Message, ReportPluginResult, Stats} from '../types.js'; interface Version { version: string; @@ -105,8 +105,17 @@ async function computeParents( 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 {messages}; + return {stats, messages}; } for (const [packageName, duplicate] of duplicateDependencies) { @@ -133,7 +142,7 @@ function exportOutput(duplicateDependencies: Map) { }); } - return {messages}; + return {stats, messages}; } /** diff --git a/src/analyze/report.ts b/src/analyze/report.ts index fb8670d..92d968d 100644 --- a/src/analyze/report.ts +++ b/src/analyze/report.ts @@ -5,7 +5,6 @@ import type {FileSystem} from '../file-system.js'; import type { Options, ReportPlugin, - Stat, Stats, Message, AnalysisContext @@ -41,19 +40,6 @@ async function computeInfo(fileSystem: FileSystem) { export async function report(options: Options) { const {root = process.cwd()} = options ?? {}; - const extraStats: Stat[] = []; - const stats: Stats = { - name: 'unknown', - version: 'unknown', - dependencyCount: { - production: 0, - development: 0, - cjs: 0, - duplicate: 0, - esm: 0 - }, - extraStats - }; const messages: Message[] = []; const fileSystem = new LocalFileSystem(root); @@ -90,6 +76,18 @@ export async function report(options: Options) { packageFile ?? undefined ); + const stats: Stats = { + name: packageFile.name, + version: packageFile.version, + dependencyCount: { + production: 0, + development: 0, + cjs: 0, + esm: 0 + }, + extraStats: [] + }; + const context: AnalysisContext = { fs: fileSystem, root, diff --git a/src/test/__snapshots__/cli.test.ts.snap b/src/test/__snapshots__/cli.test.ts.snap index 82bec78..dae0f6a 100644 --- a/src/test/__snapshots__/cli.test.ts.snap +++ b/src/test/__snapshots__/cli.test.ts.snap @@ -6,11 +6,12 @@ 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) -│ ES Modules 100% (1 ESM, 0 CJS) +│ Package Name mock-package +│ Version 1.0.0 +│ Install Size 53.0 B +│ Dependencies 1 (1 production, 0 development) +│ ES Modules 100% (1 ESM, 0 CJS) +│ Duplicate Dependency Count 0 │ ● Results: │ @@ -28,11 +29,12 @@ 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) -│ ES Modules 100% (1 ESM, 0 CJS) +│ Package Name mock-package +│ Version 1.0.0 +│ Install Size 53.0 B +│ Dependencies 1 (1 production, 0 development) +│ ES Modules 100% (1 ESM, 0 CJS) +│ Duplicate Dependency Count 0 │ ● Results: │ diff --git a/src/test/__snapshots__/duplicate-dependencies.test.ts.snap b/src/test/__snapshots__/duplicate-dependencies.test.ts.snap index 1b1f9f6..0298ac2 100644 --- a/src/test/__snapshots__/duplicate-dependencies.test.ts.snap +++ b/src/test/__snapshots__/duplicate-dependencies.test.ts.snap @@ -1,83 +1,28 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Duplicate Dependency Detection > should detect exact duplicate dependencies 1`] = ` +exports[`Duplicate Dependency Detection > should detect multiple versions 1`] = ` { "messages": [ { - "message": "[duplicate dependency] shared-lib has 2 installed versions: - 2.0.0 via root > package-a > shared-lib - 2.0.0 via root > package-b > shared-lib -Suggestions: 💡 Consider standardizing on version 2.0.0 (used by 2 dependencies) 💡 Check if newer versions of consuming packages (package-a, package-b) would resolve this duplicate", + "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": { - "dependencyCount": { - "cjs": 4, - "development": 0, - "duplicate": 1, - "esm": 0, - "production": 2, - }, - "installSize": 1151, - "name": "test-package", - "version": "1.0.0", - }, -} -`; - -exports[`Duplicate Dependency Detection > should detect version conflicts 1`] = ` -{ - "messages": [ - { - "message": "[duplicate dependency] shared-lib has 3 installed versions: - 1.0.0 via root > package-a > shared-lib - 1.0.0 via root > package-b > shared-lib - 2.0.0 via shared-lib -Suggestions: 💡 Consider standardizing on version 1.0.0 (used by 2 dependencies) 💡 Check if newer versions of consuming packages (package-a, package-b) would resolve this duplicate", - "score": 0, - "severity": "warning", - }, - ], - "stats": { - "dependencyCount": { - "cjs": 4, - "development": 0, - "duplicate": 1, - "esm": 0, - "production": 2, - }, - "installSize": 1416, - "name": "test-package", - "version": "1.0.0", - }, -} -`; - -exports[`Duplicate Dependency Detection > should generate suggestions for duplicates 1`] = ` -{ - "messages": [ - { - "message": "[duplicate dependency] shared-lib has 2 installed versions: - 2.0.0 via root > package-a > shared-lib - 2.0.0 via root > package-b > shared-lib -Suggestions: 💡 Consider standardizing on version 2.0.0 (used by 2 dependencies) 💡 Check if newer versions of consuming packages (package-a, package-b) would resolve this duplicate", - "score": 0, - "severity": "warning", - }, - ], - "stats": { - "dependencyCount": { - "cjs": 4, - "development": 0, - "duplicate": 1, - "esm": 0, - "production": 2, - }, - "installSize": 1151, - "name": "test-package", - "version": "1.0.0", + "extraStats": [ + { + "label": "Duplicate Dependency Count", + "name": "duplicateDependencyCount", + "value": 1, + }, + ], }, } `; @@ -86,16 +31,13 @@ exports[`Duplicate Dependency Detection > should not detect duplicates when ther { "messages": [], "stats": { - "dependencyCount": { - "cjs": 1, - "development": 0, - "duplicate": 0, - "esm": 0, - "production": 1, - }, - "installSize": 262, - "name": "test-package", - "version": "1.0.0", + "extraStats": [ + { + "label": "Duplicate Dependency Count", + "name": "duplicateDependencyCount", + "value": 0, + }, + ], }, } `; diff --git a/src/test/analyze/__snapshots__/dependencies.test.ts.snap b/src/test/analyze/__snapshots__/dependencies.test.ts.snap index 87613df..55f7fde 100644 --- a/src/test/analyze/__snapshots__/dependencies.test.ts.snap +++ b/src/test/analyze/__snapshots__/dependencies.test.ts.snap @@ -7,7 +7,6 @@ exports[`analyzeDependencies (local) > should analyze dependencies correctly 1`] "dependencyCount": { "cjs": 1, "development": 1, - "duplicate": 0, "esm": 1, "production": 2, }, @@ -25,7 +24,6 @@ exports[`analyzeDependencies (local) > should handle empty project 1`] = ` "dependencyCount": { "cjs": 0, "development": 0, - "duplicate": 0, "esm": 0, "production": 0, }, @@ -43,7 +41,6 @@ exports[`analyzeDependencies (local) > should handle missing node_modules 1`] = "dependencyCount": { "cjs": 0, "development": 0, - "duplicate": 0, "esm": 0, "production": 1, }, @@ -61,7 +58,6 @@ exports[`analyzeDependencies (local) > should handle symlinks 1`] = ` "dependencyCount": { "cjs": 0, "development": 0, - "duplicate": 0, "esm": 1, "production": 1, }, diff --git a/src/test/analyze/dependencies.test.ts b/src/test/analyze/dependencies.test.ts index c49fdfc..267156d 100644 --- a/src/test/analyze/dependencies.test.ts +++ b/src/test/analyze/dependencies.test.ts @@ -30,7 +30,6 @@ describe('analyzeDependencies (local)', () => { dependencyCount: { cjs: 0, esm: 0, - duplicate: 0, production: 0, development: 0 }, @@ -105,6 +104,12 @@ describe('analyzeDependencies (local)', () => { type: 'commonjs' } ]; + //update package json on context + context.packageFile.dependencies = { + 'cjs-package': '1.0.0', + 'esm-package': '1.0.0' + }; + context.packageFile.devDependencies = {'dev-package': '1.0.0'}; await createTestPackageWithDependencies(tempDir, rootPackage, dependencies); @@ -113,6 +118,10 @@ describe('analyzeDependencies (local)', () => { }); it('should handle symlinks', async () => { + //update package json on context + context.packageFile.dependencies = { + 'test-package': '1.0.0' + }; // Create root package await createTestPackage( tempDir, @@ -147,6 +156,10 @@ describe('analyzeDependencies (local)', () => { }); it('should handle missing node_modules', async () => { + //update package json on context + context.packageFile.dependencies = { + 'test-package': '1.0.0' + }; await createTestPackage(tempDir, { name: 'test-package', version: '1.0.0', diff --git a/src/test/custom-manifests.test.ts b/src/test/custom-manifests.test.ts index 574401f..588404f 100644 --- a/src/test/custom-manifests.test.ts +++ b/src/test/custom-manifests.test.ts @@ -25,7 +25,6 @@ describe('Custom Manifests', () => { dependencyCount: { cjs: 0, esm: 0, - duplicate: 0, production: 0, development: 0 }, diff --git a/src/test/duplicate-dependencies.test.ts b/src/test/duplicate-dependencies.test.ts index b0b0751..ce0404e 100644 --- a/src/test/duplicate-dependencies.test.ts +++ b/src/test/duplicate-dependencies.test.ts @@ -1,16 +1,9 @@ import {describe, it, expect, beforeEach, afterEach} from 'vitest'; -import {runDependencyAnalysis} from '../analyze/dependencies.js'; import {LocalFileSystem} from '../local-file-system.js'; -import { - createTempDir, - cleanupTempDir, - createTestPackage, - createTestPackageWithDependencies, - type TestPackage -} from './utils.js'; +import {createTempDir, cleanupTempDir} from './utils.js'; import type {AnalysisContext} from '../types.js'; -import fs from 'node:fs/promises'; -import path from 'node:path'; +import {runDuplicateDependencyAnalysis} from '../analyze/duplicate-dependencies.js'; +import {ParsedDependency} from 'lockparse'; describe('Duplicate Dependency Detection', () => { let tempDir: string; @@ -20,6 +13,62 @@ describe('Duplicate Dependency Detection', () => { 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: '.', @@ -30,7 +79,6 @@ describe('Duplicate Dependency Detection', () => { dependencyCount: { production: 0, development: 0, - duplicate: 0, esm: 0, cjs: 0 }, @@ -38,11 +86,18 @@ describe('Duplicate Dependency Detection', () => { }, lockfile: { type: 'npm', - packages: [], + packages: [ + testPkg, + packageA, + packageB, + packageC, + sharedLibv1, + sharedLibv2 + ], root: { - name: 'test-package', + name: 'root-package', version: '1.0.0', - dependencies: [], + dependencies: [testPkg], devDependencies: [], optionalDependencies: [], peerDependencies: [] @@ -53,169 +108,81 @@ describe('Duplicate Dependency Detection', () => { version: '1.0.0' } }; - }); - - afterEach(async () => { - await cleanupTempDir(tempDir); - }); - - it('should detect exact duplicate dependencies', async () => { - const rootPackage: TestPackage = { - name: 'test-package', - version: '1.0.0', - dependencies: { - 'package-a': '1.0.0', - 'package-b': '1.0.0' - } - }; - - const dependencies: TestPackage[] = [ - { - name: 'package-a', - version: '1.0.0', - dependencies: { - 'shared-lib': '2.0.0' - } - }, - { - name: 'package-b', - version: '1.0.0', - dependencies: { - 'shared-lib': '2.0.0' - } - }, - { - name: 'shared-lib', - version: '2.0.0' - } - ]; - await createTestPackageWithDependencies(tempDir, rootPackage, dependencies); - - const stats = await runDependencyAnalysis(context); + const stats = await runDuplicateDependencyAnalysis(context); expect(stats).toMatchSnapshot(); }); - it('should detect version conflicts', async () => { - const rootPackage: TestPackage = { - name: 'test-package', + it('should not detect duplicates when there are none', async () => { + const sharedLibv1: ParsedDependency = { + name: 'shared-lib', version: '1.0.0', - dependencies: { - 'package-a': '1.0.0', - 'package-b': '1.0.0' - } + dependencies: [], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [] }; - // Create root package - await createTestPackage(tempDir, rootPackage, {createNodeModules: true}); - - // Create package-a with shared-lib v1.0.0 - const packageADir = path.join(tempDir, 'node_modules', 'package-a'); - await fs.mkdir(packageADir); - await createTestPackage(packageADir, { + const packageA: ParsedDependency = { name: 'package-a', version: '1.0.0', - dependencies: { - 'shared-lib': '1.0.0' - } - }); - - // Create package-b with shared-lib v2.0.0 - const packageBDir = path.join(tempDir, 'node_modules', 'package-b'); - await fs.mkdir(packageBDir); - await createTestPackage(packageBDir, { + dependencies: [sharedLibv1], + devDependencies: [], + peerDependencies: [], + optionalDependencies: [] + }; + const packageB: ParsedDependency = { name: 'package-b', version: '1.0.0', - dependencies: { - 'shared-lib': '2.0.0' - } - }); - - // Create shared-lib v1.0.0 - const sharedLibV1Dir = path.join(tempDir, 'node_modules', 'shared-lib'); - await fs.mkdir(sharedLibV1Dir); - await createTestPackage(sharedLibV1Dir, { - name: 'shared-lib', - version: '1.0.0' - }); - - // Create shared-lib v2.0.0 in a nested location - const sharedLibV2Dir = path.join( - tempDir, - 'node_modules', - 'package-b', - 'node_modules', - 'shared-lib' - ); - await fs.mkdir(sharedLibV2Dir, {recursive: true}); - await createTestPackage(sharedLibV2Dir, { - name: 'shared-lib', - version: '2.0.0' - }); - - const stats = await runDependencyAnalysis(context); - - expect(stats).toMatchSnapshot(); - }); - - it('should not detect duplicates when there are none', async () => { - const rootPackage: TestPackage = { - name: 'test-package', - version: '1.0.0', - dependencies: { - 'package-a': '1.0.0' - } + dependencies: [sharedLibv1], + devDependencies: [], + peerDependencies: [], + optionalDependencies: [] }; - - const dependencies: TestPackage[] = [ - { - name: 'package-a', - version: '1.0.0' - } - ]; - - await createTestPackageWithDependencies(tempDir, rootPackage, dependencies); - - const stats = await runDependencyAnalysis(context); - - expect(stats).toMatchSnapshot(); - }); - - it('should generate suggestions for duplicates', async () => { - const rootPackage: TestPackage = { + const testPkg: ParsedDependency = { name: 'test-package', version: '1.0.0', - dependencies: { - 'package-a': '1.0.0', - 'package-b': '1.0.0' - } + dependencies: [packageA, packageB], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [] }; - - const dependencies: TestPackage[] = [ - { - name: 'package-a', - version: '1.0.0', - dependencies: { - 'shared-lib': '2.0.0' - } + //set the context + context = { + fs: fileSystem, + root: '.', + messages: [], + stats: { + name: 'unknown', + version: 'unknown', + dependencyCount: { + production: 0, + development: 0, + esm: 0, + cjs: 0 + }, + extraStats: [] }, - { - name: 'package-b', - version: '1.0.0', - dependencies: { - 'shared-lib': '2.0.0' + lockfile: { + type: 'npm', + packages: [testPkg, packageA, packageB, sharedLibv1], + root: { + name: 'root-package', + version: '1.0.0', + dependencies: [testPkg], + devDependencies: [], + optionalDependencies: [], + peerDependencies: [] } }, - { - name: 'shared-lib', - version: '2.0.0' + packageFile: { + name: 'test-package', + version: '1.0.0' } - ]; - - await createTestPackageWithDependencies(tempDir, rootPackage, dependencies); + }; - const stats = await runDependencyAnalysis(context); + const stats = await runDuplicateDependencyAnalysis(context); expect(stats).toMatchSnapshot(); }); diff --git a/src/types.ts b/src/types.ts index c0a1f27..4cb3ff9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,7 +24,6 @@ export interface Stats { development: number; cjs: number; esm: number; - duplicate: number; }; extraStats?: Stat[]; } @@ -54,7 +53,7 @@ export interface Replacement { } export interface ReportPluginResult { - stats?: Stats; + stats?: Partial; messages: Message[]; } diff --git a/src/utils/package-json.ts b/src/utils/package-json.ts index 09ca0e0..f8d50a0 100644 --- a/src/utils/package-json.ts +++ b/src/utils/package-json.ts @@ -4,12 +4,13 @@ import type {FileSystem} from '../file-system.js'; import type {PackageJsonLike} from '../types.js'; export async function getPackageJson( - fileSystem: FileSystem + fileSystem: FileSystem, + path: string = '/package.json' ): Promise { let packageJsonText: string; try { - packageJsonText = await fileSystem.readFile('/package.json'); + packageJsonText = await fileSystem.readFile(path); } catch { // No package.json found return null;