mirror of
				https://github.com/warkanum/monorepo-dep-checker.git
				synced 2025-10-30 23:33:53 +00:00 
			
		
		
		
	Added typescript, test and feature for comma seperated packages
This commit is contained in:
		
							parent
							
								
									9851a91bb3
								
							
						
					
					
						commit
						e522e73b7a
					
				| @ -3,10 +3,13 @@ | ||||
| import { createRequire } from 'module'; | ||||
| import { fileURLToPath } from 'url'; | ||||
| import { dirname, join } from 'path'; | ||||
| import { pathToFileURL } from 'url'; | ||||
| 
 | ||||
| const __filename = fileURLToPath(import.meta.url); | ||||
| const __dirname = dirname(__filename); | ||||
| const require = createRequire(import.meta.url); | ||||
| 
 | ||||
| const { default: run } = await import(join(__dirname, '../dist/index.js')); | ||||
| // Convert the file path to a proper file:// URL
 | ||||
| const modulePath = pathToFileURL(join(__dirname, '../dist/index.js')).href; | ||||
| const { default: run } = await import(modulePath); | ||||
| run().catch(console.error); | ||||
| @ -1,9 +1,96 @@ | ||||
| import globals from "globals"; | ||||
| import pluginJs from "@eslint/js"; | ||||
| import eslintJs from '@eslint/js'; | ||||
| import tsParser from '@typescript-eslint/parser'; | ||||
| 
 | ||||
| import globals from 'globals'; | ||||
| import eslintTs from 'typescript-eslint'; | ||||
| 
 | ||||
| /** @type {import('eslint').Linter.Config[]} */ | ||||
| export default [ | ||||
|   {languageOptions: { globals: globals.browser }}, | ||||
|   pluginJs.configs.recommended, | ||||
| const tsFiles = ['{src,lib}/**/*.{ts,tsx}']; | ||||
| 
 | ||||
| const languageOptions = { | ||||
|   globals: { | ||||
|     ...globals.node, | ||||
|     ...globals.jest, | ||||
|   }, | ||||
|   ecmaVersion: 2023, | ||||
|   sourceType: 'module', | ||||
| }; | ||||
| 
 | ||||
| const rules = { | ||||
|   '@typescript-eslint/no-use-before-define': 'off', | ||||
|   'require-await': 'off', | ||||
|   'no-duplicate-imports': 'error', | ||||
|   'no-unneeded-ternary': 'error', | ||||
|   'prefer-object-spread': 'error', | ||||
|   '@typescript-eslint/array-type': 'off', | ||||
|   '@typescript-eslint/ban-ts-comment': 'off', | ||||
|   '@typescript-eslint/no-explicit-any': 'off', | ||||
|   '@typescript-eslint/no-unused-vars': [ | ||||
|     'error', | ||||
|     { | ||||
|       ignoreRestSiblings: true, | ||||
|       args: 'none', | ||||
|     }, | ||||
|   ], | ||||
| }; | ||||
| 
 | ||||
| const commonIgnores = [ | ||||
|   'node_modules', | ||||
|   '**/node_modules', | ||||
|   '**/dist/*', | ||||
|   'docs/*', | ||||
|   'build/*', | ||||
|   'dist/*', | ||||
|   'next.config.mjs', | ||||
| ]; | ||||
| 
 | ||||
| const customTypescriptConfig = { | ||||
|   files: tsFiles, | ||||
|   plugins: { | ||||
|     'import/parsers': tsParser, | ||||
|   }, | ||||
|   languageOptions: { | ||||
|     ...languageOptions, | ||||
|     parser: tsParser, | ||||
|     parserOptions: { | ||||
|       project: './tsconfig.json', | ||||
|       sourceType: 'module', | ||||
|     }, | ||||
|   }, | ||||
|   settings: { | ||||
|     'import/parsers': { | ||||
|       '@typescript-eslint/parser': ['.ts'], | ||||
|     }, | ||||
|   }, | ||||
|   rules: { | ||||
| 
 | ||||
|     ...rules, | ||||
|   }, | ||||
|   ignores: commonIgnores, | ||||
| }; | ||||
| 
 | ||||
| // Add the files for applying the recommended TypeScript configs
 | ||||
| // only for the Typescript files.
 | ||||
| // This is necessary when we have the multiple extensions files
 | ||||
| // (e.g. .ts, .tsx, .js, .cjs, .mjs, etc.).
 | ||||
| const recommendedTypeScriptConfigs = [ | ||||
|   ...eslintTs.configs.recommended.map((config) => ({ | ||||
|     ...config, | ||||
|     files: tsFiles, | ||||
|     rules: { | ||||
|       ...config.rules, | ||||
|       ...rules, | ||||
|     }, | ||||
|     ignores: commonIgnores, | ||||
|   })), | ||||
|   ...eslintTs.configs.stylistic.map((config) => ({ | ||||
|     ...config, | ||||
|     files: tsFiles, | ||||
|     rules: { | ||||
|       ...config.rules, | ||||
|       ...rules, | ||||
|     }, | ||||
|     ignores: commonIgnores, | ||||
|   })), | ||||
| ]; | ||||
| 
 | ||||
| export default [...recommendedTypeScriptConfigs, customTypescriptConfig]; | ||||
|  | ||||
							
								
								
									
										23
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								package.json
									
									
									
									
									
								
							| @ -18,8 +18,8 @@ | ||||
|     "scripts": { | ||||
|         "build": "vite build", | ||||
|         "prepublishOnly": "npm run build", | ||||
|         "test": "vitest run", | ||||
|         "lint": "eslint src" | ||||
|         "test": "vitest run --silent=false", | ||||
|         "lint": "eslint ./src" | ||||
|     }, | ||||
|     "keywords": [ | ||||
|         "monorepo", | ||||
| @ -42,11 +42,17 @@ | ||||
|         "yargs": "^17.7.2" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@eslint/js": "^9.15.0", | ||||
|         "eslint": "^8.57.1", | ||||
|         "globals": "^15.12.0", | ||||
|         "vite": "^5.0.0", | ||||
|         "vitest": "^1.0.0" | ||||
|         "@eslint/js": "^9.16.0", | ||||
|         "@types/node": "^22.10.1", | ||||
|         "@types/semver": "~7.5.8", | ||||
|         "@types/yargs": "~17.0.33", | ||||
|         "eslint": "^9.16.0", | ||||
|         "globals": "^15.13.0", | ||||
|         "prettier-eslint": "^16.3.0", | ||||
|         "typescript-eslint": "^8.18.0", | ||||
|         "typesync": "^0.14.0", | ||||
|         "vite": "^5.4.11", | ||||
|         "vitest": "^1.6.0" | ||||
|     }, | ||||
|     "engines": { | ||||
|         "node": ">=14.16" | ||||
| @ -58,5 +64,6 @@ | ||||
|     "bugs": { | ||||
|         "url": "https://github.com/warkanum/monorepo-dep-checker/issues" | ||||
|     }, | ||||
|     "homepage": "https://github.com/warkanum/monorepo-dep-checker#readme" | ||||
|     "homepage": "https://github.com/warkanum/monorepo-dep-checker#readme", | ||||
|     "packageManager": "pnpm@9.6.0+sha256.dae0f7e822c56b20979bb5965e3b73b8bdabb6b8b8ef121da6d857508599ca35" | ||||
| } | ||||
							
								
								
									
										671
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										671
									
								
								src/index.js
									
									
									
									
									
								
							| @ -1,671 +0,0 @@ | ||||
| import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| import chalk from 'chalk'; | ||||
| import semver from 'semver'; | ||||
| import yargs from 'yargs'; | ||||
| import { hideBin } from 'yargs/helpers'; | ||||
| 
 | ||||
| class DependencyChecker { | ||||
|   constructor(appPackageJsonPath, packagesDir) { | ||||
|     this.appPackageJsonPath = appPackageJsonPath; | ||||
|     this.packagesDir = packagesDir; | ||||
|     this.packageJsonFiles = []; | ||||
|     this.dependencyMap = new Map(); | ||||
|     this.workspacePackages = new Set(); | ||||
|   } | ||||
| 
 | ||||
|   findWorkspacePackages() { | ||||
|     const entries = fs.readdirSync(this.packagesDir, { withFileTypes: true }); | ||||
| 
 | ||||
|     entries.forEach((entry) => { | ||||
|       if (entry.isDirectory()) { | ||||
|         const packageJsonPath = path.join(this.packagesDir, entry.name, 'package.json'); | ||||
|         if (fs.existsSync(packageJsonPath)) { | ||||
|           const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); | ||||
|           this.workspacePackages.add(packageJson.name); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   isWorkspaceDep(version) { | ||||
|     return version?.startsWith('workspace:') || version === '*'; | ||||
|   } | ||||
| 
 | ||||
|   isWorkspacePackage(packageName) { | ||||
|     return this.workspacePackages.has(packageName); | ||||
|   } | ||||
| 
 | ||||
|   shouldIncludeDependency(depName, version, packageJson) { | ||||
|     // Skip if it's a workspace dependency
 | ||||
|     if (this.isWorkspaceDep(version)) return false; | ||||
| 
 | ||||
|     // Skip if the dependency is a workspace package
 | ||||
|     if (this.isWorkspacePackage(depName)) return false; | ||||
| 
 | ||||
|     // Skip if the package itself is referenced as a workspace dependency
 | ||||
|     const allWorkspaceDeps = { | ||||
|       ...packageJson.dependencies, | ||||
|       ...packageJson.devDependencies, | ||||
|       ...packageJson.peerDependencies, | ||||
|     }; | ||||
| 
 | ||||
|     return !Object.entries(allWorkspaceDeps).some( | ||||
|       ([name, ver]) => name === depName && this.isWorkspaceDep(ver) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   getSemverVersion(version) { | ||||
|     // Handle workspace protocol
 | ||||
|     if (version.startsWith('workspace:')) { | ||||
|       // Extract actual version from the workspace package
 | ||||
|       const workspaceVersion = version.replace('workspace:', ''); | ||||
|       if (workspaceVersion === '*') return null; | ||||
|       // If it's a specific version after workspace:, use that
 | ||||
|       if (semver.valid(workspaceVersion)) return workspaceVersion; | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     // Handle other special cases
 | ||||
|     if (version === '*' || version === 'latest') return null; | ||||
| 
 | ||||
|     // Remove any leading special characters (^, ~, etc)
 | ||||
|     const cleanVersion = version.replace(/^[~^]/, ''); | ||||
| 
 | ||||
|     // Try to parse as semver
 | ||||
|     try { | ||||
|       if (semver.valid(cleanVersion)) return cleanVersion; | ||||
|       return null; | ||||
|     } catch { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   checkMissingDependencies() { | ||||
|     console.log(chalk.bold('\nChecking for missing dependencies...\n')); | ||||
| 
 | ||||
|     this.findWorkspacePackages(); | ||||
| 
 | ||||
|     const appPackageJson = JSON.parse(fs.readFileSync(this.appPackageJsonPath, 'utf8')); | ||||
|     const appDeps = { | ||||
|       ...appPackageJson.dependencies, | ||||
|       ...appPackageJson.peerDependencies, | ||||
|     }; | ||||
| 
 | ||||
|     const missingDeps = new Map(); | ||||
|     const uniqueMissingDeps = new Set(); | ||||
|     const uniqueExtraDeps = new Set(); | ||||
| 
 | ||||
|     this.packageJsonFiles | ||||
|       .filter((file) => file !== this.appPackageJsonPath) | ||||
|       .forEach((filePath) => { | ||||
|         const packageJson = JSON.parse(fs.readFileSync(filePath, 'utf8')); | ||||
|         const packageName = packageJson.name; | ||||
|         const packageDeps = { | ||||
|           ...packageJson.dependencies, | ||||
|           ...packageJson.peerDependencies, | ||||
|         }; | ||||
| 
 | ||||
|         const missing = []; | ||||
|         const extraDeps = []; | ||||
| 
 | ||||
|         Object.entries(packageDeps).forEach(([dep, version]) => { | ||||
|           if (!appDeps[dep] && this.shouldIncludeDependency(dep, version, packageJson)) { | ||||
|             missing.push({ | ||||
|               name: dep, | ||||
|               version, | ||||
|             }); | ||||
|             uniqueMissingDeps.add(dep); | ||||
|           } | ||||
|         }); | ||||
| 
 | ||||
|         Object.entries(appDeps).forEach(([dep, version]) => { | ||||
|           if (!packageDeps[dep] && this.shouldIncludeDependency(dep, version, appPackageJson)) { | ||||
|             extraDeps.push({ | ||||
|               name: dep, | ||||
|               version, | ||||
|             }); | ||||
|             uniqueExtraDeps.add(dep); | ||||
|           } | ||||
|         }); | ||||
| 
 | ||||
|         if (missing.length > 0 || extraDeps.length > 0) { | ||||
|           missingDeps.set(packageName, { | ||||
|             // eslint-disable-next-line no-undef
 | ||||
|             path: path.relative(process.cwd(), filePath), | ||||
|             missing, | ||||
|             extraDeps, | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|     if (missingDeps.size === 0) { | ||||
|       console.log(chalk.green('✓ All dependencies are properly synchronized\n')); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     missingDeps.forEach(({ path: pkgPath, missing, extraDeps }, packageName) => { | ||||
|       if (missing.length > 0 || extraDeps.length > 0) { | ||||
|         console.log(chalk.yellow(`\n${packageName} (${chalk.gray(pkgPath)}):`)); | ||||
| 
 | ||||
|         if (missing.length > 0) { | ||||
|           console.log(chalk.red('  Missing from main app:')); | ||||
|           missing.forEach(({ name, version }) => { | ||||
|             console.log(`    - ${name}@${version}`); | ||||
|           }); | ||||
|         } | ||||
| 
 | ||||
|         if (extraDeps.length > 0) { | ||||
|           console.log(chalk.blue('  Not used by package but in main app:')); | ||||
|           extraDeps.forEach(({ name, version }) => { | ||||
|             console.log(`    - ${name}@${version}`); | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     if (uniqueMissingDeps.size > 0 || uniqueExtraDeps.size > 0) { | ||||
|       console.log(chalk.bold('\nSummary:')); | ||||
|       console.log(`Packages with dependency mismatches: ${missingDeps.size}`); | ||||
|       if (uniqueMissingDeps.size > 0) { | ||||
|         console.log( | ||||
|           chalk.red(`Unique dependencies missing from main app: ${uniqueMissingDeps.size}`) | ||||
|         ); | ||||
|         console.log(chalk.gray('  ' + Array.from(uniqueMissingDeps).join(', '))); | ||||
|       } | ||||
|       if (uniqueExtraDeps.size > 0) { | ||||
|         console.log( | ||||
|           chalk.blue(`Unique unused dependencies from main app: ${uniqueExtraDeps.size}`) | ||||
|         ); | ||||
|         console.log(chalk.gray('  ' + Array.from(uniqueExtraDeps).join(', '))); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   compareDependencyVersions(version1, version2) { | ||||
|     const v1 = this.getSemverVersion(version1); | ||||
|     const v2 = this.getSemverVersion(version2); | ||||
| 
 | ||||
|     // If either version can't be parsed, return 'unknown'
 | ||||
|     if (!v1 || !v2) return 'unknown'; | ||||
| 
 | ||||
|     try { | ||||
|       return semver.diff(v1, v2); | ||||
|     } catch { | ||||
|       return 'unknown'; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   getVersionLabel(version) { | ||||
|     if (version.startsWith('workspace:')) { | ||||
|       return `${chalk.cyan('workspace:')}${version.slice(10)}`; | ||||
|     } | ||||
|     return version; | ||||
|   } | ||||
| 
 | ||||
|   findPackageJsonFiles() { | ||||
|     this.packageJsonFiles.push(this.appPackageJsonPath); | ||||
| 
 | ||||
|     const entries = fs.readdirSync(this.packagesDir, { withFileTypes: true }); | ||||
| 
 | ||||
|     entries.forEach((entry) => { | ||||
|       if (entry.isDirectory()) { | ||||
|         const packageJsonPath = path.join(this.packagesDir, entry.name, 'package.json'); | ||||
|         if (fs.existsSync(packageJsonPath)) { | ||||
|           this.packageJsonFiles.push(packageJsonPath); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   analyzeDependencies() { | ||||
|     this.packageJsonFiles.forEach((filePath) => { | ||||
|       const packageJson = JSON.parse(fs.readFileSync(filePath, 'utf8')); | ||||
|       const normalDeps = packageJson.dependencies || {}; | ||||
|       const peerDeps = packageJson.peerDependencies || {}; | ||||
|       const packageName = packageJson.name; | ||||
|       const isMainApp = filePath === this.appPackageJsonPath; | ||||
| 
 | ||||
|       [ | ||||
|         { deps: normalDeps, type: 'normal' }, | ||||
|         { deps: peerDeps, type: 'peer' }, | ||||
|       ].forEach(({ deps, type }) => { | ||||
|         Object.entries(deps).forEach(([dep, version]) => { | ||||
|           if (!this.dependencyMap.has(dep)) { | ||||
|             this.dependencyMap.set(dep, { | ||||
|               versions: new Map(), | ||||
|               usedAsNormal: false, | ||||
|               usedAsPeer: false, | ||||
|             }); | ||||
|           } | ||||
| 
 | ||||
|           const depInfo = this.dependencyMap.get(dep); | ||||
|           if (!depInfo.versions.has(version)) { | ||||
|             depInfo.versions.set(version, { | ||||
|               packages: new Set(), | ||||
|               usages: new Set(), | ||||
|             }); | ||||
|           } | ||||
| 
 | ||||
|           const versionInfo = depInfo.versions.get(version); | ||||
|           versionInfo.packages.add(packageName); | ||||
|           versionInfo.usages.add(`${type}${isMainApp ? ' (main app)' : ''}`); | ||||
| 
 | ||||
|           if (type === 'normal') depInfo.usedAsNormal = true; | ||||
|           if (type === 'peer') depInfo.usedAsPeer = true; | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   displayVersionDifferences() { | ||||
|     console.log(chalk.bold('\nVersion Differences Summary:\n')); | ||||
| 
 | ||||
|     let hasDifferences = false; | ||||
|     const conflictingDeps = []; | ||||
| 
 | ||||
|     this.dependencyMap.forEach((depInfo, dep) => { | ||||
|       if (depInfo.versions.size > 1) { | ||||
|         hasDifferences = true; | ||||
|         const versions = Array.from(depInfo.versions.entries()).map(([version, info]) => ({ | ||||
|           version, | ||||
|           packages: Array.from(info.packages), | ||||
|           paths: Array.from(info.packages).map((pkg) => { | ||||
|             const filePath = this.packageJsonFiles.find((file) => { | ||||
|               const json = JSON.parse(fs.readFileSync(file, 'utf8')); | ||||
|               return json.name === pkg; | ||||
|             }); | ||||
|             return { | ||||
|               package: pkg, | ||||
|               // eslint-disable-next-line no-undef
 | ||||
|               path: path.relative(process.cwd(), filePath), | ||||
|             }; | ||||
|           }), | ||||
|           usages: Array.from(info.usages), | ||||
|         })); | ||||
| 
 | ||||
|         // Create comparison matrix
 | ||||
|         const comparisons = []; | ||||
|         for (let i = 0; i < versions.length; i++) { | ||||
|           for (let j = i + 1; j < versions.length; j++) { | ||||
|             const v1 = versions[i]; | ||||
|             const v2 = versions[j]; | ||||
|             const comparison = { | ||||
|               version1: v1.version, | ||||
|               version2: v2.version, | ||||
|               packages1: v1.paths, | ||||
|               packages2: v2.paths, | ||||
|               semverDiff: this.compareDependencyVersions(v1.version, v2.version), | ||||
|             }; | ||||
|             comparisons.push(comparison); | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         conflictingDeps.push({ | ||||
|           name: dep, | ||||
|           versions, | ||||
|           comparisons, | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     if (!hasDifferences) { | ||||
|       console.log(chalk.green('✓ No version differences found across packages\n')); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     conflictingDeps.forEach(({ name, comparisons }) => { | ||||
|       console.log(chalk.yellow(`\n${name}:`)); | ||||
| 
 | ||||
|       comparisons.forEach(({ version1, version2, packages1, packages2, semverDiff }) => { | ||||
|         console.log(chalk.cyan(`\n  Difference (${semverDiff || 'unknown'}):`)); | ||||
|         console.log(`    ${this.getVersionLabel(version1)} vs ${this.getVersionLabel(version2)}`); | ||||
| 
 | ||||
|         console.log('\n    Packages using', chalk.green(this.getVersionLabel(version1)), ':'); | ||||
|         packages1.forEach(({ package: pkg, path: filePath }) => { | ||||
|           console.log(`      - ${chalk.blue(pkg)} (${chalk.gray(filePath)})`); | ||||
|         }); | ||||
| 
 | ||||
|         console.log('\n    Packages using', chalk.green(this.getVersionLabel(version2)), ':'); | ||||
|         packages2.forEach(({ package: pkg, path: filePath }) => { | ||||
|           console.log(`      - ${chalk.blue(pkg)} (${chalk.gray(filePath)})`); | ||||
|         }); | ||||
| 
 | ||||
|         // Suggest recommended action based on version types
 | ||||
|         console.log('\n    Recommended action:'); | ||||
|         if (version1.startsWith('workspace:') || version2.startsWith('workspace:')) { | ||||
|           console.log(chalk.blue('    ℹ️  Workspace dependency - No action needed')); | ||||
|         } else { | ||||
|           switch (semverDiff) { | ||||
|             case 'major': | ||||
|               console.log( | ||||
|                 chalk.red('    ⚠️  Major version difference - Manual review recommended') | ||||
|               ); | ||||
|               break; | ||||
|             case 'minor': | ||||
|               console.log( | ||||
|                 chalk.yellow('    ℹ️  Minor version difference - Consider updating to latest') | ||||
|               ); | ||||
|               break; | ||||
|             case 'patch': | ||||
|               console.log(chalk.green('    ✓ Patch version difference - Safe to update to latest')); | ||||
|               break; | ||||
|             default: | ||||
|               console.log(chalk.gray('    ℹ️  Version difference analysis not available')); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     // Print total counts
 | ||||
|     console.log(chalk.bold('\nSummary Statistics:')); | ||||
|     const totalComparisons = conflictingDeps.reduce((acc, dep) => acc + dep.comparisons.length, 0); | ||||
|     console.log(`Dependencies with conflicts: ${conflictingDeps.length}`); | ||||
|     console.log(`Total version comparisons: ${totalComparisons}`); | ||||
| 
 | ||||
|     // Show severity breakdown
 | ||||
|     const severityCount = conflictingDeps.reduce((acc, dep) => { | ||||
|       dep.comparisons.forEach(({ version1, version2, semverDiff }) => { | ||||
|         if (version1.startsWith('workspace:') || version2.startsWith('workspace:')) { | ||||
|           acc.workspace = (acc.workspace || 0) + 1; | ||||
|         } else { | ||||
|           acc[semverDiff || 'unknown'] = (acc[semverDiff || 'unknown'] || 0) + 1; | ||||
|         } | ||||
|       }); | ||||
|       return acc; | ||||
|     }, {}); | ||||
| 
 | ||||
|     console.log('\nSeverity breakdown:'); | ||||
|     if (severityCount.workspace) | ||||
|       console.log(chalk.blue(`  Workspace dependencies: ${severityCount.workspace}`)); | ||||
|     if (severityCount.major) console.log(chalk.red(`  Major differences: ${severityCount.major}`)); | ||||
|     if (severityCount.minor) | ||||
|       console.log(chalk.yellow(`  Minor differences: ${severityCount.minor}`)); | ||||
|     if (severityCount.patch) | ||||
|       console.log(chalk.green(`  Patch differences: ${severityCount.patch}`)); | ||||
|     if (severityCount.unknown) | ||||
|       console.log(chalk.gray(`  Unknown differences: ${severityCount.unknown}`)); | ||||
|   } | ||||
| 
 | ||||
|   displayResults(format = 'text') { | ||||
|     // First show the version differences summary
 | ||||
|     if (format === 'text') { | ||||
|       this.displayVersionDifferences(); | ||||
|     } | ||||
| 
 | ||||
|     // Then show the full analysis
 | ||||
|     if (format === 'json') { | ||||
|       const output = { | ||||
|         summary: { | ||||
|           conflicts: Array.from(this.dependencyMap.entries()) | ||||
|             .filter(([, depInfo]) => depInfo.versions.size > 1) | ||||
|             .map(([dep, depInfo]) => ({ | ||||
|               name: dep, | ||||
|               versions: Array.from(depInfo.versions.entries()).map(([version, info]) => ({ | ||||
|                 version, | ||||
|                 packages: Array.from(info.packages), | ||||
|                 paths: Array.from(info.packages).map((pkg) => { | ||||
|                   const filePath = this.packageJsonFiles.find((file) => { | ||||
|                     const json = JSON.parse(fs.readFileSync(file, 'utf8')); | ||||
|                     return json.name === pkg; | ||||
|                   }); | ||||
|                   return { | ||||
|                     package: pkg, | ||||
|                     // eslint-disable-next-line no-undef
 | ||||
|                     path: path.relative(process.cwd(), filePath), | ||||
|                   }; | ||||
|                 }), | ||||
|                 usages: Array.from(info.usages), | ||||
|               })), | ||||
|             })), | ||||
|         }, | ||||
|         fullAnalysis: {}, | ||||
|       }; | ||||
| 
 | ||||
|       this.dependencyMap.forEach((depInfo, dep) => { | ||||
|         output.fullAnalysis[dep] = { | ||||
|           versions: Object.fromEntries( | ||||
|             Array.from(depInfo.versions.entries()).map(([version, info]) => [ | ||||
|               version, | ||||
|               { | ||||
|                 packages: Array.from(info.packages), | ||||
|                 usages: Array.from(info.usages), | ||||
|               }, | ||||
|             ]) | ||||
|           ), | ||||
|           usedAsNormal: depInfo.usedAsNormal, | ||||
|           usedAsPeer: depInfo.usedAsPeer, | ||||
|         }; | ||||
|       }); | ||||
| 
 | ||||
|       console.log(JSON.stringify(output, null, 2)); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // Original detailed display
 | ||||
|     console.log(chalk.bold('\nDetailed Dependencies Analysis:\n')); | ||||
| 
 | ||||
|     this.dependencyMap.forEach((depInfo, dep) => { | ||||
|       console.log(chalk.blue(`\n${dep}:`)); | ||||
| 
 | ||||
|       if (depInfo.versions.size > 1) { | ||||
|         console.log(chalk.yellow('⚠️  Multiple versions detected:')); | ||||
|       } | ||||
| 
 | ||||
|       const usageTypes = []; | ||||
|       if (depInfo.usedAsNormal) usageTypes.push('normal dependency'); | ||||
|       if (depInfo.usedAsPeer) usageTypes.push('peer dependency'); | ||||
|       console.log(chalk.cyan(`Used as: ${usageTypes.join(' and ')}`)); | ||||
| 
 | ||||
|       depInfo.versions.forEach((versionInfo, version) => { | ||||
|         console.log(`  ${chalk.green(version)}`); | ||||
|         console.log(`    Usage types:`); | ||||
|         versionInfo.usages.forEach((usage) => { | ||||
|           console.log(`      - ${usage}`); | ||||
|         }); | ||||
|         console.log(`    Packages:`); | ||||
|         versionInfo.packages.forEach((pkg) => { | ||||
|           console.log(`      - ${pkg}`); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   async updateDependencies(dryRun = false) { | ||||
|     console.log(chalk.bold('\nUpdating dependencies...\n')); | ||||
| 
 | ||||
|     // Read the main app's package.json
 | ||||
|     const appPackageJson = JSON.parse(fs.readFileSync(this.appPackageJsonPath, 'utf8')); | ||||
|     const appDependencies = { | ||||
|       ...appPackageJson.dependencies, | ||||
|       ...appPackageJson.devDependencies, | ||||
|       ...appPackageJson.peerDependencies | ||||
|     }; | ||||
| 
 | ||||
|     const updates = []; | ||||
| 
 | ||||
|     // Process each package.json except the main app
 | ||||
|     this.packageJsonFiles | ||||
|       .filter(filePath => filePath !== this.appPackageJsonPath) | ||||
|       .forEach(filePath => { | ||||
|         const packageJson = JSON.parse(fs.readFileSync(filePath, 'utf8')); | ||||
|         let hasUpdates = false; | ||||
| 
 | ||||
|         // Helper function to update dependencies of a specific type
 | ||||
|         const updateDependencySection = (section) => { | ||||
|           if (!packageJson[section]) return; | ||||
| 
 | ||||
|           Object.entries(packageJson[section]).forEach(([dep, version]) => { | ||||
|             // Skip workspace dependencies
 | ||||
|             if (version.startsWith('workspace:')) return; | ||||
|              | ||||
|             // If the dependency exists in the main app, sync the version
 | ||||
|             if (appDependencies[dep]) { | ||||
|               const appVersion = appDependencies[dep]; | ||||
|               if (version !== appVersion) { | ||||
|                 if (!dryRun) { | ||||
|                   packageJson[section][dep] = appVersion; | ||||
|                 } | ||||
|                 updates.push({ | ||||
|                   package: packageJson.name, | ||||
|                   dependency: dep, | ||||
|                   from: version, | ||||
|                   to: appVersion, | ||||
|                   type: section | ||||
|                 }); | ||||
|                 hasUpdates = true; | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         }; | ||||
| 
 | ||||
|         // Update all dependency sections
 | ||||
|         updateDependencySection('dependencies'); | ||||
|         updateDependencySection('devDependencies'); | ||||
|         updateDependencySection('peerDependencies'); | ||||
| 
 | ||||
|         // Write updated package.json if there were changes
 | ||||
|         if (hasUpdates && !dryRun) { | ||||
|           fs.writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n'); | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|     // Display updates
 | ||||
|     if (updates.length === 0) { | ||||
|       console.log(chalk.green('No updates needed - all versions match the main app')); | ||||
|     } else { | ||||
|       updates.forEach(({ package: pkgName, dependency, from, to, type }) => { | ||||
|         console.log( | ||||
|           chalk.green( | ||||
|             `${dryRun ? '[DRY RUN] Would update' : 'Updated'} ${dependency} in ${pkgName} (${type})\n` + | ||||
|             `  ${chalk.red(from)} → ${chalk.green(to)}` | ||||
|           ) | ||||
|         ); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     return updates; | ||||
|   } | ||||
| 
 | ||||
|   async run({ | ||||
|     update = false, | ||||
|     dryRun = false, | ||||
|     format = 'text', | ||||
|     checkVersions = false, | ||||
|     checkMissing = false, | ||||
|   } = {}) { | ||||
|     if (checkMissing) { | ||||
|       this.findPackageJsonFiles(); | ||||
|       this.checkMissingDependencies(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (checkVersions) { | ||||
|       this.findPackageJsonFiles(); | ||||
|       this.analyzeDependencies(); | ||||
|       this.displayVersionDifferences(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (update) { | ||||
|       this.findPackageJsonFiles(); | ||||
|       this.analyzeDependencies(); | ||||
|       await this.updateDependencies(dryRun); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // Default behavior - show full analysis
 | ||||
|     this.findPackageJsonFiles(); | ||||
|     this.analyzeDependencies(); | ||||
|     this.displayResults(format); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // CLI implementation
 | ||||
| const run = async () => { | ||||
|   // eslint-disable-next-line no-undef
 | ||||
|   const argv = yargs(hideBin(process.argv)) | ||||
|     .usage('Usage: $0 [options]') | ||||
|     .option('app', { | ||||
|       alias: 'a', | ||||
|       describe: 'Path to main app package.json', | ||||
|       type: 'string', | ||||
|       default: './package.json', | ||||
|     }) | ||||
|     .option('packages', { | ||||
|       alias: 'p', | ||||
|       describe: 'Path to packages directory', | ||||
|       type: 'string', | ||||
|       default: './packages', | ||||
|     }) | ||||
|     .option('update', { | ||||
|       alias: 'u', | ||||
|       describe: 'Update dependencies to highest compatible version', | ||||
|       type: 'boolean', | ||||
|       default: false, | ||||
|     }) | ||||
|     .option('dry-run', { | ||||
|       alias: 'd', | ||||
|       describe: 'Show what would be updated without making changes', | ||||
|       type: 'boolean', | ||||
|       default: false, | ||||
|     }) | ||||
|     .option('check-versions', { | ||||
|       alias: 'v', | ||||
|       describe: 'Check for version differences between packages', | ||||
|       type: 'boolean', | ||||
|       default: false, | ||||
|     }) | ||||
|     .option('check-missing', { | ||||
|       alias: 'm', | ||||
|       describe: 'Check for dependencies missing between app and packages', | ||||
|       type: 'boolean', | ||||
|       default: false, | ||||
|     }) | ||||
|     .option('format', { | ||||
|       alias: 'f', | ||||
|       describe: 'Output format (text or json)', | ||||
|       choices: ['text', 'json'], | ||||
|       default: 'text', | ||||
|     }) | ||||
|     .help() | ||||
|     .alias('help', 'h') | ||||
|     .example('$0 --check-versions', 'Show only version differences') | ||||
|     .example('$0 --check-missing', 'Show missing dependencies') | ||||
|     .example('$0 --update --dry-run', 'Show what would be updated').argv; | ||||
| 
 | ||||
|   // eslint-disable-next-line no-undef
 | ||||
|   const appPackageJson = path.resolve(process.cwd(), argv.app); | ||||
|   // eslint-disable-next-line no-undef
 | ||||
|   const packagesDir = path.resolve(process.cwd(), argv.packages); | ||||
| 
 | ||||
|   // Validate paths
 | ||||
|   if (!fs.existsSync(appPackageJson)) { | ||||
|     console.error(chalk.red(`Error: Main package.json not found at ${appPackageJson}`)); | ||||
|     // eslint-disable-next-line no-undef
 | ||||
|     process.exit(1); | ||||
|   } | ||||
| 
 | ||||
|   if (!fs.existsSync(packagesDir)) { | ||||
|     console.error(chalk.red(`Error: Packages directory not found at ${packagesDir}`)); | ||||
|     // eslint-disable-next-line no-undef
 | ||||
|     process.exit(1); | ||||
|   } | ||||
| 
 | ||||
|   const checker = new DependencyChecker(appPackageJson, packagesDir); | ||||
|   await checker.run({ | ||||
|     update: argv.update, | ||||
|     dryRun: argv.dryRun, | ||||
|     format: argv.format, | ||||
|     checkVersions: argv.checkVersions, | ||||
|     checkMissing: argv.checkMissing, | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| // Export both the class and run function
 | ||||
| export { DependencyChecker, run as default }; | ||||
| 
 | ||||
| // eslint-disable-next-line no-undef
 | ||||
| if (process.argv[1] === import.meta.url.slice(7)) { | ||||
|   run(); | ||||
| } | ||||
							
								
								
									
										159
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,159 @@ | ||||
| import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| import chalk from 'chalk'; | ||||
| import yargs from 'yargs'; | ||||
| import { hideBin } from 'yargs/helpers'; | ||||
| import { DependencyChecker } from './lib'; | ||||
| export {DependencyChecker} | ||||
| 
 | ||||
| const cli = async () => { | ||||
|   const yargsInstance = yargs(hideBin(process.argv)) | ||||
|     .usage('Usage: $0 [options]') | ||||
|     .option('app', { | ||||
|       alias: 'a', | ||||
|       describe: 'Path to main app package.json', | ||||
|       type: 'string', | ||||
|       default: './package.json', | ||||
|     }) | ||||
|     .option('packages', { | ||||
|       alias: 'p', | ||||
|       describe: 'Comma-separated list of paths to package.json files or directories containing packages', | ||||
|       type: 'string', | ||||
|       default: './packages', | ||||
|     }) | ||||
|     .option('update', { | ||||
|       alias: 'u', | ||||
|       describe: 'Update dependencies to highest compatible version', | ||||
|       type: 'boolean', | ||||
|       default: false, | ||||
|     }) | ||||
|     .option('dry-run', { | ||||
|       alias: 'd', | ||||
|       describe: 'Show what would be updated without making changes', | ||||
|       type: 'boolean', | ||||
|       default: false, | ||||
|     }) | ||||
|     .option('check-versions', { | ||||
|       alias: 'v', | ||||
|       describe: 'Check for version differences between packages', | ||||
|       type: 'boolean', | ||||
|       default: false, | ||||
|     }) | ||||
|     .option('check-missing', { | ||||
|       alias: 'm', | ||||
|       describe: 'Check for dependencies missing between app and packages', | ||||
|       type: 'boolean', | ||||
|       default: false, | ||||
|     }) | ||||
|     .option('format', { | ||||
|       alias: 'f', | ||||
|       describe: 'Output format (text or json)', | ||||
|       choices: ['text', 'json'], | ||||
|       default: 'text', | ||||
|       type:'string' | ||||
|     }) | ||||
|     .example('$0 --check-versions', 'Show only version differences') | ||||
|     .example('$0 --check-missing', 'Show missing dependencies') | ||||
|     .example('$0 --update --dry-run', 'Show what would be updated') | ||||
|     .example('$0 --packages ./packages,./other-packages', 'Check multiple package directories') | ||||
|     .example('$0 --packages ./pkg1/package.json,./pkg2/package.json', 'Check specific package.json files') | ||||
|     .example('$0 --packages ./packages,./other/package.json', 'Mix of directories and files') | ||||
|     .epilogue('For more information, visit: https://github.com/warkanum/monorepo-dep-checker') | ||||
|     .wrap(Math.min(120, process.stdout.columns)) | ||||
|     .version() | ||||
|     .help() | ||||
|     .alias('help', 'h') | ||||
|     .alias('version', 'V'); | ||||
| 
 | ||||
|   // Parse arguments
 | ||||
|   const argv = await yargsInstance.parse(); | ||||
| 
 | ||||
|   // If help or version was requested, exit early
 | ||||
|   // yargs.argv internally tracks if help or version was requested
 | ||||
|   if (argv.help || argv.version) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   // Resolve paths
 | ||||
|   const appPackageJson = path.resolve(process.cwd(), argv.app); | ||||
|   const packagesInput = argv.packages; | ||||
| 
 | ||||
|   // Validate main app package.json
 | ||||
|   if (!fs.existsSync(appPackageJson)) { | ||||
|     console.error(chalk.red(`Error: Main package.json not found at ${appPackageJson}`)); | ||||
|     process.exit(1); | ||||
|   } | ||||
| 
 | ||||
|   // Validate all package paths
 | ||||
|   const paths = packagesInput.split(',').map(p => path.resolve(process.cwd(), p.trim())); | ||||
|   let hasErrors = false; | ||||
| 
 | ||||
|   // biome-ignore lint/complexity/noForEach: Ease of reading and preference
 | ||||
|   paths.forEach(inputPath => { | ||||
|     try { | ||||
|       if (!fs.existsSync(inputPath)) { | ||||
|         console.error(chalk.red(`Error: Path not found: ${inputPath}`)); | ||||
|         hasErrors = true; | ||||
|       } else { | ||||
|         const stats = fs.statSync(inputPath); | ||||
|         if (!stats.isDirectory() && !(stats.isFile() && inputPath.endsWith('package.json'))) { | ||||
|           console.error(chalk.red(`Error: Path must be a directory or package.json file: ${inputPath}`)); | ||||
|           hasErrors = true; | ||||
|         } | ||||
|       } | ||||
|     } catch (error:any) { | ||||
|       console.error(chalk.red(`Error accessing path ${inputPath}: ${error?.message}`)); | ||||
|       hasErrors = true; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   if (hasErrors) { | ||||
|     process.exit(1); | ||||
|   } | ||||
| 
 | ||||
|   // Validate mutually exclusive options
 | ||||
|   const exclusiveOptions = ['update', 'check-versions', 'check-missing'] | ||||
|     .filter(opt => argv[opt]) | ||||
|     .length; | ||||
| 
 | ||||
|   if (exclusiveOptions > 1) { | ||||
|     console.error(chalk.red('Error: --update, --check-versions, and --check-missing are mutually exclusive')); | ||||
|     process.exit(1); | ||||
|   } | ||||
| 
 | ||||
|   // Validate dry-run is only used with update
 | ||||
|   if (argv.dryRun && !argv.update) { | ||||
|     console.error(chalk.yellow('Warning: --dry-run has no effect without --update')); | ||||
|   } | ||||
| 
 | ||||
|   const checker = new DependencyChecker(appPackageJson, packagesInput); | ||||
|    | ||||
|   try { | ||||
|     await checker.run({ | ||||
|       update: argv.update, | ||||
|       dryRun: argv.dryRun, | ||||
|       format: argv.format as any, | ||||
|       checkVersions: argv.checkVersions, | ||||
|       checkMissing: argv.checkMissing, | ||||
|     }); | ||||
|   } catch (error:any) { | ||||
|     console.error(chalk.red('Error during execution:')); | ||||
|     console.error(error?.message); | ||||
|     if (argv.format === 'json') { | ||||
|       console.log(JSON.stringify({ error: error?.message })); | ||||
|     } | ||||
|     process.exit(1); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| // Export both the CLI function and run it if this is the main module
 | ||||
| export { cli as default }; | ||||
| 
 | ||||
| if (process.argv[1] === import.meta.url.slice(7)) { | ||||
|   cli().catch(error => { | ||||
|     console.error(chalk.red('Unexpected error:')); | ||||
|     console.error(error); | ||||
|     process.exit(1); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										786
									
								
								src/lib.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										786
									
								
								src/lib.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,786 @@ | ||||
| import fs, { type Dirent } from "fs"; | ||||
| import path from "path"; | ||||
| import chalk from "chalk"; | ||||
| import semver from "semver"; | ||||
| 
 | ||||
| interface PackageJson { | ||||
|   name: string; | ||||
|   dependencies?: Record<string, string>; | ||||
|   devDependencies?: Record<string, string>; | ||||
|   peerDependencies?: Record<string, string>; | ||||
| } | ||||
| 
 | ||||
| interface RunOptions { | ||||
|   update?: boolean; | ||||
|   dryRun?: boolean; | ||||
|   format?: "text" | "json"; | ||||
|   checkVersions?: boolean; | ||||
|   checkMissing?: boolean; | ||||
| } | ||||
| 
 | ||||
| interface VersionInfo { | ||||
|   packages: Set<string>; | ||||
|   usages: Set<string>; | ||||
| } | ||||
| 
 | ||||
| interface DependencyInfo { | ||||
|   versions: Map<string, VersionInfo>; | ||||
|   usedAsNormal: boolean; | ||||
|   usedAsPeer: boolean; | ||||
| } | ||||
| 
 | ||||
| interface PackagePathInfo { | ||||
|   package: string; | ||||
|   path: string; | ||||
| } | ||||
| 
 | ||||
| interface DependencyComparison { | ||||
|   version1: string; | ||||
|   version2: string; | ||||
|   packages1: PackagePathInfo[]; | ||||
|   packages2: PackagePathInfo[]; | ||||
|   semverDiff: string; | ||||
| } | ||||
| 
 | ||||
| interface ConflictingDep { | ||||
|   name: string; | ||||
|   versions: { | ||||
|     version: string; | ||||
|     packages: string[]; | ||||
|     paths: PackagePathInfo[]; | ||||
|     usages: string[]; | ||||
|   }[]; | ||||
|   comparisons: DependencyComparison[]; | ||||
| } | ||||
| 
 | ||||
| interface DependencyUpdate { | ||||
|   package: string; | ||||
|   dependency: string; | ||||
|   from: string; | ||||
|   to: string; | ||||
|   type: string; | ||||
| } | ||||
| 
 | ||||
| interface MissingDepsInfo { | ||||
|   path: string; | ||||
|   missing: Array<{ name: string; version: string }>; | ||||
|   extraDeps: Array<{ name: string; version: string }>; | ||||
| } | ||||
| 
 | ||||
| type DependencyTypes = "dependencies" | "devDependencies" | "peerDependencies"; | ||||
| 
 | ||||
| class DependencyChecker { | ||||
|   private readonly appPackageJsonPath: string; | ||||
|   private readonly packagesInput: string; | ||||
|   private packageJsonFiles: string[]; | ||||
|   private dependencyMap: Map<string, DependencyInfo>; | ||||
|   private workspacePackages: Set<string>; | ||||
| 
 | ||||
|   constructor(appPackageJsonPath: string, packagesInput: string) { | ||||
|     this.appPackageJsonPath = appPackageJsonPath; | ||||
|     this.packagesInput = packagesInput; | ||||
|     this.packageJsonFiles = []; | ||||
|     this.dependencyMap = new Map(); | ||||
|     this.workspacePackages = new Set(); | ||||
|   } | ||||
| 
 | ||||
|   private findWorkspacePackages(): void { | ||||
|     const paths = this.packagesInput.split(",").map((p) => p.trim()); | ||||
| 
 | ||||
|     paths.forEach((inputPath) => { | ||||
|       if (fs.statSync(inputPath).isDirectory()) { | ||||
|         const entries = fs.readdirSync(inputPath, { withFileTypes: true }); | ||||
|         entries.forEach((entry: Dirent) => { | ||||
|           if (entry.isDirectory()) { | ||||
|             const packageJsonPath = path.join( | ||||
|               inputPath, | ||||
|               entry.name, | ||||
|               "package.json" | ||||
|             ); | ||||
|             if (fs.existsSync(packageJsonPath)) { | ||||
|               const packageJson = JSON.parse( | ||||
|                 fs.readFileSync(packageJsonPath, "utf8") | ||||
|               ) as PackageJson; | ||||
|               this.workspacePackages.add(packageJson.name); | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|       } else if (inputPath.endsWith("package.json")) { | ||||
|         const packageJson = JSON.parse( | ||||
|           fs.readFileSync(inputPath, "utf8") | ||||
|         ) as PackageJson; | ||||
|         this.workspacePackages.add(packageJson.name); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private findPackageJsonFiles(): void { | ||||
|     this.packageJsonFiles = [this.appPackageJsonPath]; | ||||
|     const paths = this.packagesInput.split(",").map((p) => p.trim()); | ||||
| 
 | ||||
|     paths.forEach((inputPath) => { | ||||
|       if (fs.statSync(inputPath).isDirectory()) { | ||||
|         const entries = fs.readdirSync(inputPath, { withFileTypes: true }); | ||||
|         entries.forEach((entry: Dirent) => { | ||||
|           if (entry.isDirectory()) { | ||||
|             const packageJsonPath = path.join( | ||||
|               inputPath, | ||||
|               entry.name, | ||||
|               "package.json" | ||||
|             ); | ||||
|             if (fs.existsSync(packageJsonPath)) { | ||||
|               this.packageJsonFiles.push(packageJsonPath); | ||||
|             } | ||||
|           } | ||||
|         }); | ||||
|       } else if (inputPath.endsWith("package.json")) { | ||||
|         this.packageJsonFiles.push(inputPath); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private isWorkspaceDep(version?: string): boolean { | ||||
|     return !!version && (version.startsWith("workspace:") || version === "*"); | ||||
|   } | ||||
| 
 | ||||
|   private isWorkspacePackage(packageName: string): boolean { | ||||
|     return this.workspacePackages.has(packageName); | ||||
|   } | ||||
| 
 | ||||
|   private shouldIncludeDependency( | ||||
|     depName: string, | ||||
|     version: string, | ||||
|     packageJson: PackageJson | ||||
|   ): boolean { | ||||
|     if (this.isWorkspaceDep(version)) return false; | ||||
|     if (this.isWorkspacePackage(depName)) return false; | ||||
| 
 | ||||
|     const allWorkspaceDeps = { | ||||
|       ...packageJson.dependencies, | ||||
|       ...packageJson.devDependencies, | ||||
|       ...packageJson.peerDependencies, | ||||
|     }; | ||||
| 
 | ||||
|     return !Object.entries(allWorkspaceDeps).some( | ||||
|       ([name, ver]) => name === depName && this.isWorkspaceDep(ver) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   private getSemverVersion(version: string): string | null { | ||||
|     if (version.startsWith("workspace:")) { | ||||
|       const workspaceVersion = version.replace("workspace:", ""); | ||||
|       if (workspaceVersion === "*") return null; | ||||
|       if (semver.valid(workspaceVersion)) return workspaceVersion; | ||||
|       return null; | ||||
|     } | ||||
| 
 | ||||
|     if (version === "*" || version === "latest") return null; | ||||
| 
 | ||||
|     const cleanVersion = version.replace(/^[~^]/, ""); | ||||
| 
 | ||||
|     try { | ||||
|       if (semver.valid(cleanVersion)) return cleanVersion; | ||||
|       return null; | ||||
|     } catch { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private checkMissingDependencies(): void { | ||||
|     console.log(chalk.bold("\nChecking for missing dependencies...\n")); | ||||
| 
 | ||||
|     this.findWorkspacePackages(); | ||||
| 
 | ||||
|     const appPackageJson = JSON.parse( | ||||
|       fs.readFileSync(this.appPackageJsonPath, "utf8") | ||||
|     ) as PackageJson; | ||||
|     const appDeps = { | ||||
|       ...appPackageJson.dependencies, | ||||
|       ...appPackageJson.devDependencies, | ||||
|       ...appPackageJson.peerDependencies, | ||||
|     }; | ||||
| 
 | ||||
|     const missingDeps = new Map<string, MissingDepsInfo>(); | ||||
|     const uniqueMissingDeps = new Set<string>(); | ||||
|     const uniqueExtraDeps = new Set<string>(); | ||||
| 
 | ||||
|     this.packageJsonFiles | ||||
|       .filter((file) => file !== this.appPackageJsonPath) | ||||
|       .forEach((filePath) => { | ||||
|         const packageJson = JSON.parse( | ||||
|           fs.readFileSync(filePath, "utf8") | ||||
|         ) as PackageJson; | ||||
|         const packageName = packageJson.name; | ||||
| 
 | ||||
|         const packageDeps = { | ||||
|           ...packageJson.dependencies, | ||||
|           ...packageJson.peerDependencies, | ||||
|         }; | ||||
| 
 | ||||
|         const missing: Array<{ name: string; version: string }> = []; | ||||
|         const extraDeps: Array<{ name: string; version: string }> = []; | ||||
| 
 | ||||
|         Object.entries(packageDeps).forEach(([dep, version]) => { | ||||
|           if ( | ||||
|             (!appDeps[dep] && | ||||
|               this.shouldIncludeDependency(dep, version, packageJson)) || | ||||
|             (packageJson.peerDependencies?.[dep] && !appDeps[dep]) | ||||
|           ) { | ||||
|             missing.push({ name: dep, version }); | ||||
|             uniqueMissingDeps.add(dep); | ||||
|           } | ||||
|         }); | ||||
| 
 | ||||
|         Object.entries(appDeps).forEach(([dep, version]) => { | ||||
|           if ( | ||||
|             !packageDeps[dep] && | ||||
|             this.shouldIncludeDependency(dep, version, appPackageJson) | ||||
|           ) { | ||||
|             extraDeps.push({ name: dep, version }); | ||||
|             uniqueExtraDeps.add(dep); | ||||
|           } | ||||
|         }); | ||||
| 
 | ||||
|         if (missing.length > 0 || extraDeps.length > 0) { | ||||
|           missingDeps.set(packageName, { | ||||
|             path: path.relative(process.cwd(), filePath), | ||||
|             missing, | ||||
|             extraDeps, | ||||
|           }); | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|     if (missingDeps.size === 0) { | ||||
|       console.log( | ||||
|         chalk.green("✓ All dependencies are properly synchronized\n") | ||||
|       ); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     missingDeps.forEach( | ||||
|       ({ path: pkgPath, missing, extraDeps }, packageName) => { | ||||
|         if (missing.length > 0 || extraDeps.length > 0) { | ||||
|           console.log( | ||||
|             chalk.yellow(`\n${packageName} (${chalk.gray(pkgPath)}):`) | ||||
|           ); | ||||
| 
 | ||||
|           if (missing.length > 0) { | ||||
|             console.log(chalk.red("  Missing from main app:")); | ||||
|             missing.forEach(({ name, version }) => { | ||||
|               console.log(`    - ${name}@${version}`); | ||||
|             }); | ||||
|           } | ||||
| 
 | ||||
|           if (extraDeps.length > 0) { | ||||
|             console.log(chalk.blue("  Not used by package but in main app:")); | ||||
|             extraDeps.forEach(({ name, version }) => { | ||||
|               console.log(`    - ${name}@${version}`); | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
| 
 | ||||
|     if (uniqueMissingDeps.size > 0 || uniqueExtraDeps.size > 0) { | ||||
|       console.log(chalk.bold("\nSummary:")); | ||||
|       console.log(`Packages with dependency mismatches: ${missingDeps.size}`); | ||||
|       if (uniqueMissingDeps.size > 0) { | ||||
|         console.log( | ||||
|           chalk.red( | ||||
|             `Unique dependencies missing from main app: ${uniqueMissingDeps.size}` | ||||
|           ) | ||||
|         ); | ||||
|         console.log( | ||||
|           chalk.gray("  " + Array.from(uniqueMissingDeps).join(", ")) | ||||
|         ); | ||||
|       } | ||||
|       if (uniqueExtraDeps.size > 0) { | ||||
|         console.log( | ||||
|           chalk.blue( | ||||
|             `Unique unused dependencies from main app: ${uniqueExtraDeps.size}` | ||||
|           ) | ||||
|         ); | ||||
|         console.log(chalk.gray("  " + Array.from(uniqueExtraDeps).join(", "))); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private compareDependencyVersions( | ||||
|     version1: string, | ||||
|     version2: string | ||||
|   ): string { | ||||
|     const v1 = this.getSemverVersion(version1); | ||||
|     const v2 = this.getSemverVersion(version2); | ||||
| 
 | ||||
|     if (!v1 || !v2) return "unknown"; | ||||
| 
 | ||||
|     try { | ||||
|       const diff = semver.diff(v1, v2); | ||||
|       return diff || "unknown"; | ||||
|     } catch { | ||||
|       return "unknown"; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private getVersionLabel(version: string): string { | ||||
|     if (version.startsWith("workspace:")) { | ||||
|       return `${chalk.cyan("workspace:")}${version.slice(10)}`; | ||||
|     } | ||||
|     return version; | ||||
|   } | ||||
| 
 | ||||
|   private analyzeDependencies(): void { | ||||
|     this.findWorkspacePackages(); | ||||
| 
 | ||||
|     this.packageJsonFiles.forEach((filePath) => { | ||||
|       const packageJson = JSON.parse( | ||||
|         fs.readFileSync(filePath, "utf8") | ||||
|       ) as PackageJson; | ||||
|       const normalDeps = packageJson.dependencies || {}; | ||||
|       const peerDeps = packageJson.peerDependencies || {}; | ||||
|       const devDeps = packageJson.devDependencies || {}; | ||||
|       const packageName = packageJson.name; | ||||
|       const isMainApp = filePath === this.appPackageJsonPath; | ||||
| 
 | ||||
|       // Process all dependency types
 | ||||
|       [ | ||||
|         { deps: normalDeps, type: "normal" }, | ||||
|         { deps: peerDeps, type: "peer" }, | ||||
|         { deps: devDeps, type: "dev" }, | ||||
|       ].forEach(({ deps, type }) => { | ||||
|         Object.entries(deps).forEach(([dep, version]) => { | ||||
|           if (!this.dependencyMap.has(dep)) { | ||||
|             this.dependencyMap.set(dep, { | ||||
|               versions: new Map(), | ||||
|               usedAsNormal: false, | ||||
|               usedAsPeer: false, | ||||
|             }); | ||||
|           } | ||||
| 
 | ||||
|           const depInfo = this.dependencyMap.get(dep)!; | ||||
|           if (!depInfo.versions.has(version)) { | ||||
|             depInfo.versions.set(version, { | ||||
|               packages: new Set(), | ||||
|               usages: new Set(), | ||||
|             }); | ||||
|           } | ||||
| 
 | ||||
|           const versionInfo = depInfo.versions.get(version)!; | ||||
|           versionInfo.packages.add(packageName); | ||||
|           versionInfo.usages.add(`${type}${isMainApp ? " (main app)" : ""}`); | ||||
| 
 | ||||
|           if (type === "normal") depInfo.usedAsNormal = true; | ||||
|           if (type === "peer") depInfo.usedAsPeer = true; | ||||
| 
 | ||||
|           // Track workspace dependencies explicitly
 | ||||
|           if (this.isWorkspaceDep(version)) { | ||||
|             versionInfo.usages.add("workspace"); | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   private displayVersionDifferences(): void { | ||||
|     console.log(chalk.bold("\nVersion Differences Summary:\n")); | ||||
| 
 | ||||
|     let hasDifferences = false; | ||||
|     const conflictingDeps: ConflictingDep[] = []; | ||||
| 
 | ||||
|     this.dependencyMap.forEach((depInfo, dep) => { | ||||
|       if (depInfo.versions.size > 1) { | ||||
|         hasDifferences = true; | ||||
|         const versions = Array.from(depInfo.versions.entries()).map( | ||||
|           ([version, info]) => ({ | ||||
|             version, | ||||
|             packages: Array.from(info.packages), | ||||
|             paths: Array.from(info.packages).map((pkg) => { | ||||
|               const filePath = this.packageJsonFiles.find((file) => { | ||||
|                 const json = JSON.parse( | ||||
|                   fs.readFileSync(file, "utf8") | ||||
|                 ) as PackageJson; | ||||
|                 return json.name === pkg; | ||||
|               }); | ||||
|               return { | ||||
|                 package: pkg, | ||||
|                 path: path.relative(process.cwd(), filePath || ""), | ||||
|               }; | ||||
|             }), | ||||
|             usages: Array.from(info.usages), | ||||
|           }) | ||||
|         ); | ||||
| 
 | ||||
|         const comparisons: DependencyComparison[] = []; | ||||
|         for (let i = 0; i < versions.length; i++) { | ||||
|           for (let j = i + 1; j < versions.length; j++) { | ||||
|             const v1 = versions[i]; | ||||
|             const v2 = versions[j]; | ||||
|             comparisons.push({ | ||||
|               version1: v1.version, | ||||
|               version2: v2.version, | ||||
|               packages1: v1.paths, | ||||
|               packages2: v2.paths, | ||||
|               semverDiff: this.compareDependencyVersions( | ||||
|                 v1.version, | ||||
|                 v2.version | ||||
|               ), | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         conflictingDeps.push({ | ||||
|           name: dep, | ||||
|           versions, | ||||
|           comparisons, | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     if (!hasDifferences) { | ||||
|       console.log( | ||||
|         chalk.green("✓ No version differences found across packages\n") | ||||
|       ); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     conflictingDeps.forEach(({ name, comparisons }) => { | ||||
|       console.log(chalk.yellow(`\n${name}:`)); | ||||
| 
 | ||||
|       comparisons.forEach( | ||||
|         ({ version1, version2, packages1, packages2, semverDiff }) => { | ||||
|           console.log( | ||||
|             chalk.cyan(`\n  Difference (${semverDiff || "unknown"}):`) | ||||
|           ); | ||||
|           console.log( | ||||
|             `    ${this.getVersionLabel(version1)} vs ${this.getVersionLabel(version2)}` | ||||
|           ); | ||||
| 
 | ||||
|           console.log( | ||||
|             "\n    Packages using", | ||||
|             chalk.green(this.getVersionLabel(version1)), | ||||
|             ":" | ||||
|           ); | ||||
|           packages1.forEach(({ package: pkg, path: filePath }) => { | ||||
|             console.log(`      - ${chalk.blue(pkg)} (${chalk.gray(filePath)})`); | ||||
|           }); | ||||
| 
 | ||||
|           console.log( | ||||
|             "\n    Packages using", | ||||
|             chalk.green(this.getVersionLabel(version2)), | ||||
|             ":" | ||||
|           ); | ||||
|           packages2.forEach(({ package: pkg, path: filePath }) => { | ||||
|             console.log(`      - ${chalk.blue(pkg)} (${chalk.gray(filePath)})`); | ||||
|           }); | ||||
| 
 | ||||
|           console.log("\n    Recommended action:"); | ||||
|           if ( | ||||
|             version1.startsWith("workspace:") || | ||||
|             version2.startsWith("workspace:") | ||||
|           ) { | ||||
|             console.log( | ||||
|               chalk.blue("    ℹ️  Workspace dependency - No action needed") | ||||
|             ); | ||||
|           } else { | ||||
|             switch (semverDiff) { | ||||
|               case "major": | ||||
|                 console.log( | ||||
|                   chalk.red( | ||||
|                     "    ⚠️  Major version difference - Manual review recommended" | ||||
|                   ) | ||||
|                 ); | ||||
|                 break; | ||||
|               case "minor": | ||||
|                 console.log( | ||||
|                   chalk.yellow( | ||||
|                     "    ℹ️  Minor version difference - Consider updating to latest" | ||||
|                   ) | ||||
|                 ); | ||||
|                 break; | ||||
|               case "patch": | ||||
|                 console.log( | ||||
|                   chalk.green( | ||||
|                     "    ✓ Patch version difference - Safe to update to latest" | ||||
|                   ) | ||||
|                 ); | ||||
|                 break; | ||||
|               default: | ||||
|                 console.log( | ||||
|                   chalk.gray( | ||||
|                     "    ℹ️  Version difference analysis not available" | ||||
|                   ) | ||||
|                 ); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     console.log(chalk.bold("\nSummary Statistics:")); | ||||
|     const totalComparisons = conflictingDeps.reduce( | ||||
|       (acc, dep) => acc + dep.comparisons.length, | ||||
|       0 | ||||
|     ); | ||||
|     console.log(`Dependencies with conflicts: ${conflictingDeps.length}`); | ||||
|     console.log(`Total version comparisons: ${totalComparisons}`); | ||||
| 
 | ||||
|     const severityCount: Record<string, number> = {}; | ||||
|     // biome-ignore lint/complexity/noForEach: <explanation>
 | ||||
|     conflictingDeps.forEach((dep) => { | ||||
|       dep.comparisons.forEach(({ version1, version2, semverDiff }) => { | ||||
|         if ( | ||||
|           version1.startsWith("workspace:") || | ||||
|           version2.startsWith("workspace:") | ||||
|         ) { | ||||
|           severityCount.workspace = (severityCount.workspace || 0) + 1; | ||||
|         } else { | ||||
|           severityCount[semverDiff || "unknown"] = | ||||
|             (severityCount[semverDiff || "unknown"] || 0) + 1; | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     console.log("\nSeverity breakdown:"); | ||||
|     if (severityCount.workspace) | ||||
|       console.log( | ||||
|         chalk.blue(`  Workspace dependencies: ${severityCount.workspace}`) | ||||
|       ); | ||||
|     if (severityCount.major) | ||||
|       console.log(chalk.red(`  Major differences: ${severityCount.major}`)); | ||||
|     if (severityCount.minor) | ||||
|       console.log(chalk.yellow(`  Minor differences: ${severityCount.minor}`)); | ||||
|     if (severityCount.patch) | ||||
|       console.log(chalk.green(`  Patch differences: ${severityCount.patch}`)); | ||||
|     if (severityCount.unknown) | ||||
|       console.log( | ||||
|         chalk.gray(`  Unknown differences: ${severityCount.unknown}`) | ||||
|       ); | ||||
|   } | ||||
| 
 | ||||
|   private async updateDependencies( | ||||
|     dryRun = false | ||||
|   ): Promise<DependencyUpdate[]> { | ||||
|     console.log(chalk.bold("\nUpdating dependencies...\n")); | ||||
| 
 | ||||
|     const appPackageJson = JSON.parse( | ||||
|       fs.readFileSync(this.appPackageJsonPath, "utf8") | ||||
|     ) as PackageJson; | ||||
|     const appDependencies = { | ||||
|       ...appPackageJson.dependencies, | ||||
|       ...appPackageJson.devDependencies, | ||||
|       ...appPackageJson.peerDependencies, | ||||
|     }; | ||||
| 
 | ||||
|     const updates: DependencyUpdate[] = []; | ||||
| 
 | ||||
|     this.packageJsonFiles | ||||
|       .filter((filePath) => filePath !== this.appPackageJsonPath) | ||||
|       .forEach((filePath) => { | ||||
|         const packageJson = JSON.parse( | ||||
|           fs.readFileSync(filePath, "utf8") | ||||
|         ) as PackageJson; | ||||
|         let hasUpdates = false; | ||||
| 
 | ||||
|         const updateDependencySection = (section: DependencyTypes): void => { | ||||
|           if (!packageJson[section]) return; | ||||
| 
 | ||||
|           Object.entries(packageJson[section]!).forEach(([dep, version]) => { | ||||
|             if (version.startsWith("workspace:")) return; | ||||
| 
 | ||||
|             if (appDependencies[dep]) { | ||||
|               const appVersion = appDependencies[dep]; | ||||
|               if (version !== appVersion) { | ||||
|                 if (!dryRun) { | ||||
|                   packageJson[section]![dep] = appVersion; | ||||
|                 } | ||||
|                 updates.push({ | ||||
|                   package: packageJson.name, | ||||
|                   dependency: dep, | ||||
|                   from: version, | ||||
|                   to: appVersion, | ||||
|                   type: section, | ||||
|                 }); | ||||
|                 hasUpdates = true; | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         }; | ||||
| 
 | ||||
|         updateDependencySection("dependencies"); | ||||
|         updateDependencySection("devDependencies"); | ||||
|         updateDependencySection("peerDependencies"); | ||||
| 
 | ||||
|         if (hasUpdates && !dryRun) { | ||||
|           fs.writeFileSync( | ||||
|             filePath, | ||||
|             JSON.stringify(packageJson, null, 2) + "\n" | ||||
|           ); | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|     if (updates.length === 0) { | ||||
|       console.log( | ||||
|         chalk.green("No updates needed - all versions match the main app") | ||||
|       ); | ||||
|     } else { | ||||
|       updates.forEach(({ package: pkgName, dependency, from, to, type }) => { | ||||
|         console.log( | ||||
|           chalk.green( | ||||
|             `${dryRun ? "[DRY RUN] Would update" : "Updated"} ${dependency} in ${pkgName} (${type})\n` + | ||||
|               `  ${chalk.red(from)} → ${chalk.green(to)}` | ||||
|           ) | ||||
|         ); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     return updates; | ||||
|   } | ||||
| 
 | ||||
|   private displayResults(format: "text" | "json" = "text"): void { | ||||
|     if (format === "json") { | ||||
|       const output = { | ||||
|         summary: { | ||||
|           conflicts: Array.from(this.dependencyMap.entries()) | ||||
|             .filter(([, depInfo]) => depInfo.versions.size > 1) | ||||
|             .map(([dep, depInfo]) => ({ | ||||
|               name: dep, | ||||
|               versions: Array.from(depInfo.versions.entries()).map( | ||||
|                 ([version, info]) => ({ | ||||
|                   version, | ||||
|                   packages: Array.from(info.packages), | ||||
|                   paths: Array.from(info.packages).map((pkg) => { | ||||
|                     const filePath = this.packageJsonFiles.find((file) => { | ||||
|                       const json = JSON.parse( | ||||
|                         fs.readFileSync(file, "utf8") | ||||
|                       ) as PackageJson; | ||||
|                       return json.name === pkg; | ||||
|                     }); | ||||
|                     return { | ||||
|                       package: pkg, | ||||
|                       path: path.relative(process.cwd(), filePath || ""), | ||||
|                     }; | ||||
|                   }), | ||||
|                   usages: Array.from(info.usages), | ||||
|                   isWorkspace: this.isWorkspaceDep(version), | ||||
|                 }) | ||||
|               ), | ||||
|             })), | ||||
|         }, | ||||
|         fullAnalysis: Object.fromEntries( | ||||
|           Array.from(this.dependencyMap.entries()).map(([dep, depInfo]) => [ | ||||
|             dep, | ||||
|             { | ||||
|               versions: Object.fromEntries( | ||||
|                 Array.from(depInfo.versions.entries()).map( | ||||
|                   ([version, info]) => [ | ||||
|                     version, | ||||
|                     { | ||||
|                       packages: Array.from(info.packages), | ||||
|                       usages: Array.from(info.usages), | ||||
|                       isWorkspace: this.isWorkspaceDep(version), | ||||
|                     }, | ||||
|                   ] | ||||
|                 ) | ||||
|               ), | ||||
|               usedAsNormal: depInfo.usedAsNormal, | ||||
|               usedAsPeer: depInfo.usedAsPeer, | ||||
|             }, | ||||
|           ]) | ||||
|         ), | ||||
|       }; | ||||
| 
 | ||||
|       console.log(JSON.stringify(output, null, 2)); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (format === "text") { | ||||
|       this.displayVersionDifferences(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const output = { | ||||
|       summary: { | ||||
|         conflicts: Array.from(this.dependencyMap.entries()) | ||||
|           .filter(([, depInfo]) => depInfo.versions.size > 1) | ||||
|           .map(([dep, depInfo]) => ({ | ||||
|             name: dep, | ||||
|             versions: Array.from(depInfo.versions.entries()).map( | ||||
|               ([version, info]) => ({ | ||||
|                 version, | ||||
|                 packages: Array.from(info.packages), | ||||
|                 paths: Array.from(info.packages).map((pkg) => { | ||||
|                   const filePath = this.packageJsonFiles.find((file) => { | ||||
|                     const json = JSON.parse( | ||||
|                       fs.readFileSync(file, "utf8") | ||||
|                     ) as PackageJson; | ||||
|                     return json.name === pkg; | ||||
|                   }); | ||||
|                   return { | ||||
|                     package: pkg, | ||||
|                     path: path.relative(process.cwd(), filePath || ""), | ||||
|                   }; | ||||
|                 }), | ||||
|                 usages: Array.from(info.usages), | ||||
|               }) | ||||
|             ), | ||||
|           })), | ||||
|       }, | ||||
|       fullAnalysis: Object.fromEntries( | ||||
|         Array.from(this.dependencyMap.entries()).map(([dep, depInfo]) => [ | ||||
|           dep, | ||||
|           { | ||||
|             versions: Object.fromEntries( | ||||
|               Array.from(depInfo.versions.entries()).map(([version, info]) => [ | ||||
|                 version, | ||||
|                 { | ||||
|                   packages: Array.from(info.packages), | ||||
|                   usages: Array.from(info.usages), | ||||
|                 }, | ||||
|               ]) | ||||
|             ), | ||||
|             usedAsNormal: depInfo.usedAsNormal, | ||||
|             usedAsPeer: depInfo.usedAsPeer, | ||||
|           }, | ||||
|         ]) | ||||
|       ), | ||||
|     }; | ||||
| 
 | ||||
|     console.log(JSON.stringify(output, null, 2)); | ||||
|   } | ||||
| 
 | ||||
|   public async run(options: RunOptions = {}): Promise<void> { | ||||
|     const { | ||||
|       update = false, | ||||
|       dryRun = false, | ||||
|       format = "text", | ||||
|       checkVersions = false, | ||||
|       checkMissing = false, | ||||
|     } = options; | ||||
| 
 | ||||
|     if (checkMissing) { | ||||
|       this.findPackageJsonFiles(); | ||||
|       this.checkMissingDependencies(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (checkVersions) { | ||||
|       this.findPackageJsonFiles(); | ||||
|       this.analyzeDependencies(); | ||||
|       this.displayVersionDifferences(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (update) { | ||||
|       this.findPackageJsonFiles(); | ||||
|       this.analyzeDependencies(); | ||||
|       await this.updateDependencies(dryRun); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     this.findPackageJsonFiles(); | ||||
|     this.analyzeDependencies(); | ||||
|     this.displayResults(format); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export { DependencyChecker }; | ||||
							
								
								
									
										13
									
								
								tests/app/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								tests/app/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| { | ||||
|     "name": "test-app", | ||||
|     "version": "1.0.0", | ||||
|     "dependencies": { | ||||
|       "react": "^18.2.0", | ||||
|       "lodash": "^4.17.21", | ||||
|       "express": "^4.18.2" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|       "typescript": "^5.0.0", | ||||
|       "jest": "^29.0.0" | ||||
|     } | ||||
|   } | ||||
							
								
								
									
										156
									
								
								tests/dependency-checker.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								tests/dependency-checker.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,156 @@ | ||||
| import { describe, test, expect, beforeEach, vi } from 'vitest'; | ||||
| import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| import { DependencyChecker } from '../src/lib'; | ||||
| 
 | ||||
| // Mock console.log to prevent output during tests
 | ||||
| const _log = console.log | ||||
| console.log = vi.fn(); | ||||
| console.error = vi.fn(); | ||||
| 
 | ||||
| describe('DependencyChecker', () => { | ||||
|   const appPackageJsonPath = path.resolve(__dirname, './app/package.json'); | ||||
|   const packagesPath = path.resolve(__dirname, './packages'); | ||||
|   let checker: DependencyChecker; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     checker = new DependencyChecker(appPackageJsonPath, packagesPath); | ||||
|     // Clear console mocks before each test
 | ||||
|     console.log = vi.fn(); | ||||
|     console.error = vi.fn(); | ||||
|   }); | ||||
| 
 | ||||
|   test('should correctly identify version differences', async () => { | ||||
|     await checker.run({ checkVersions: true }); | ||||
|      | ||||
|     expect(console.log).toHaveBeenCalledWith( | ||||
|       expect.stringContaining('Version Differences Summary') | ||||
|     ); | ||||
| 
 | ||||
|     // Verify major version difference detection
 | ||||
|     const calls = (console.log as jest.Mock).mock.calls.map(call => call[0]).join('\n'); | ||||
|     expect(calls).toContain('Major version difference'); | ||||
|   }); | ||||
| 
 | ||||
|   test('should detect missing dependencies', async () => { | ||||
|     await checker.run({ checkMissing: true }); | ||||
|      | ||||
|     expect(console.log).toHaveBeenCalledWith( | ||||
|       expect.stringContaining('Checking for missing dependencies') | ||||
|     ); | ||||
|   }); | ||||
| 
 | ||||
|   test('should handle update with dry run', async () => { | ||||
|     const originalContent = fs.readFileSync( | ||||
|       path.join(packagesPath, 'pkg1/package.json'), | ||||
|       'utf8' | ||||
|     ); | ||||
|      | ||||
|     await checker.run({ update: true, dryRun: true }); | ||||
|      | ||||
|     const newContent = fs.readFileSync( | ||||
|       path.join(packagesPath, 'pkg1/package.json'), | ||||
|       'utf8' | ||||
|     ); | ||||
|      | ||||
|     expect(originalContent).toBe(newContent); | ||||
|     expect(console.log).toHaveBeenCalledWith( | ||||
|       expect.stringContaining('[DRY RUN]') | ||||
|     ); | ||||
|   }); | ||||
| 
 | ||||
|   test('should output JSON format correctly', async () => { | ||||
|     const consoleSpy = vi.spyOn(console, 'log'); | ||||
|      | ||||
|     await checker.run({ format: 'json' }); | ||||
|      | ||||
|     const lastCall = consoleSpy.mock.calls[consoleSpy.mock.calls.length - 1][0]; | ||||
|      | ||||
|     expect(() => JSON.parse(lastCall)).not.toThrow(); | ||||
|     const output = JSON.parse(lastCall); | ||||
|     expect(output).toHaveProperty('summary'); | ||||
|     expect(output).toHaveProperty('fullAnalysis'); | ||||
|   }); | ||||
| 
 | ||||
|   test('should handle multiple package paths', async () => { | ||||
|     const multiPathChecker = new DependencyChecker( | ||||
|       appPackageJsonPath, | ||||
|       `${packagesPath},${path.join(packagesPath, 'pkg1/package.json')}` | ||||
|     ); | ||||
|      | ||||
|     await multiPathChecker.run({ checkVersions: true }); | ||||
|      | ||||
|     // Verify that dependencies from both paths are processed
 | ||||
|     const calls = (console.log as jest.Mock).mock.calls.map(call => call[0]).join('\n'); | ||||
|     expect(calls).toContain('Version Differences Summary'); | ||||
|     expect(calls).toContain('pkg1'); | ||||
|   }); | ||||
| 
 | ||||
|   test('should identify workspace dependencies', async () => { | ||||
|     // First create test files with workspace deps
 | ||||
|     const pkg3Path = path.join(packagesPath, 'pkg3/package.json'); | ||||
|     const pkg3Content = { | ||||
|       name: "pkg3", | ||||
|       version: "1.0.0", | ||||
|       dependencies: { | ||||
|         "workspace-pkg": "workspace:*", | ||||
|         "react": "^18.2.0" | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|      // Ensure directory exists
 | ||||
|      fs.mkdirSync(path.dirname(pkg3Path), { recursive: true }); | ||||
|      fs.writeFileSync(pkg3Path, JSON.stringify(pkg3Content, null, 2)); | ||||
| 
 | ||||
|     // Run with default options to get full analysis
 | ||||
|     await checker.run({ format: 'json' }); | ||||
|      | ||||
|     // Get the last console.log call
 | ||||
|     const calls = (console.log as jest.Mock).mock.calls; | ||||
|     expect(calls.length).toBeGreaterThan(0); | ||||
|      | ||||
|     const lastCall = calls[calls.length - 1][0]; | ||||
|     expect(() => JSON.parse(lastCall)).not.toThrow(); | ||||
|      | ||||
|     const output = JSON.parse(lastCall); | ||||
|     expect(output.fullAnalysis).toHaveProperty('workspace-pkg'); | ||||
|      | ||||
|     // Clean up
 | ||||
|     fs.unlinkSync(pkg3Path); | ||||
|   }); | ||||
| 
 | ||||
| 
 | ||||
|   test('should handle errors gracefully', async () => { | ||||
|     const invalidChecker = new DependencyChecker( | ||||
|       'invalid/path/package.json', | ||||
|       packagesPath | ||||
|     ); | ||||
|      | ||||
|     await expect(invalidChecker.run()).rejects.toThrow(); | ||||
|   }); | ||||
| 
 | ||||
|   test('should handle empty package directories', async () => { | ||||
|     // Create a temporary empty directory
 | ||||
|     const emptyDir = path.join(__dirname, 'empty'); | ||||
|     fs.mkdirSync(emptyDir, { recursive: true }); | ||||
|      | ||||
|     const emptyChecker = new DependencyChecker(appPackageJsonPath, emptyDir); | ||||
|     await emptyChecker.run({ checkVersions: true }); | ||||
|      | ||||
|     // Clean up
 | ||||
|     fs.rmdirSync(emptyDir); | ||||
|      | ||||
|     // Verify appropriate handling of empty directory
 | ||||
|     const calls = (console.log as jest.Mock).mock.calls.map(call => call[0]).join('\n'); | ||||
|     expect(calls).toContain('No version differences found'); | ||||
|   }); | ||||
| 
 | ||||
|   test('should detect version conflicts correctly', async () => { | ||||
|     await checker.run({ checkVersions: true }); | ||||
|      | ||||
|     const calls = (console.log as jest.Mock).mock.calls.map(call => call[0]).join('\n'); | ||||
|     expect(calls).toMatch(/Difference \((?:major|minor|patch)\)/); | ||||
|   }); | ||||
| 
 | ||||
| 
 | ||||
| }); | ||||
							
								
								
									
										11
									
								
								tests/packages/pkg1/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								tests/packages/pkg1/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| { | ||||
|   "name": "pkg1", | ||||
|   "version": "1.0.0", | ||||
|   "dependencies": { | ||||
|     "react": "^17.0.2", | ||||
|     "lodash": "^4.17.21" | ||||
|   }, | ||||
|   "peerDependencies": { | ||||
|     "typescript": "^4.9.0" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								tests/packages/pkg2/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								tests/packages/pkg2/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| { | ||||
|     "name": "pkg2", | ||||
|     "version": "1.0.0", | ||||
|     "dependencies": { | ||||
|       "express": "^4.17.1",  | ||||
|       "moment": "^2.29.4"    | ||||
|     } | ||||
|   } | ||||
							
								
								
									
										20
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| { | ||||
|   "include": ["*.d.ts", "./src"], | ||||
|   "exclude": ["public", "dist", "./dist", "node_modules", "vite.config.js"], | ||||
|   "compilerOptions": { | ||||
|     "types": ["node"], | ||||
|     "target": "ES6", | ||||
|     "lib": ["DOM", "ESNext"], | ||||
|     "module": "ESNext", | ||||
|     "moduleResolution": "Node", | ||||
|     "jsx": "react-jsx", | ||||
|     "resolveJsonModule": true, | ||||
|     "esModuleInterop": true, | ||||
|     "skipLibCheck": true, | ||||
|     "noEmit": true, | ||||
|     "strict": true, | ||||
|     "paths": { | ||||
|       "@/*": ["./*"] | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -4,10 +4,12 @@ import { resolve } from 'path'; | ||||
| export default defineConfig({ | ||||
|   build: { | ||||
|     lib: { | ||||
|       entry: resolve(__dirname, 'src/index.js'), | ||||
|       // eslint-disable-next-line no-undef
 | ||||
|       entry: resolve(__dirname, 'src/index.ts'), | ||||
|       formats: ['es'], | ||||
|       fileName: 'index' | ||||
|     }, | ||||
|     ssr:true, | ||||
|     rollupOptions: { | ||||
|       external: [ | ||||
|         'fs', | ||||
| @ -17,10 +19,12 @@ export default defineConfig({ | ||||
|         'semver', | ||||
|         'yargs', | ||||
|         'yargs/helpers' | ||||
|       ] | ||||
|       ], | ||||
|     }, | ||||
|     target: 'node14', | ||||
|      | ||||
|     outDir: 'dist', | ||||
|      | ||||
|     emptyOutDir: true, | ||||
|     sourcemap: true | ||||
|   } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user