Added typescript, test and feature for comma seperated packages

This commit is contained in:
Warky 2024-12-10 20:31:14 +02:00
parent 9851a91bb3
commit e522e73b7a
12 changed files with 1274 additions and 691 deletions

View File

@ -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);

View File

@ -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];

View File

@ -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"
}

View File

@ -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
View 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
View 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
View 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"
}
}

View 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)\)/);
});
});

View 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"
}
}

View 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
View 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": {
"@/*": ["./*"]
}
}
}

View File

@ -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
}