mirror of
https://github.com/warkanum/monorepo-dep-checker.git
synced 2025-05-18 18:57:29 +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 { createRequire } from 'module';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
|
import { pathToFileURL } from 'url';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
const require = createRequire(import.meta.url);
|
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
|
||||||
run().catch(console.error);
|
const modulePath = pathToFileURL(join(__dirname, '../dist/index.js')).href;
|
||||||
|
const { default: run } = await import(modulePath);
|
||||||
|
run().catch(console.error);
|
101
eslint.config.js
101
eslint.config.js
@ -1,9 +1,96 @@
|
|||||||
import globals from "globals";
|
import eslintJs from '@eslint/js';
|
||||||
import pluginJs from "@eslint/js";
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
|
||||||
|
import globals from 'globals';
|
||||||
|
import eslintTs from 'typescript-eslint';
|
||||||
|
|
||||||
/** @type {import('eslint').Linter.Config[]} */
|
const tsFiles = ['{src,lib}/**/*.{ts,tsx}'];
|
||||||
export default [
|
|
||||||
{languageOptions: { globals: globals.browser }},
|
const languageOptions = {
|
||||||
pluginJs.configs.recommended,
|
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];
|
||||||
|
25
package.json
25
package.json
@ -18,8 +18,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run build",
|
||||||
"test": "vitest run",
|
"test": "vitest run --silent=false",
|
||||||
"lint": "eslint src"
|
"lint": "eslint ./src"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"monorepo",
|
"monorepo",
|
||||||
@ -42,11 +42,17 @@
|
|||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.15.0",
|
"@eslint/js": "^9.16.0",
|
||||||
"eslint": "^8.57.1",
|
"@types/node": "^22.10.1",
|
||||||
"globals": "^15.12.0",
|
"@types/semver": "~7.5.8",
|
||||||
"vite": "^5.0.0",
|
"@types/yargs": "~17.0.33",
|
||||||
"vitest": "^1.0.0"
|
"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": {
|
"engines": {
|
||||||
"node": ">=14.16"
|
"node": ">=14.16"
|
||||||
@ -58,5 +64,6 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/warkanum/monorepo-dep-checker/issues"
|
"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({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/index.js'),
|
// eslint-disable-next-line no-undef
|
||||||
|
entry: resolve(__dirname, 'src/index.ts'),
|
||||||
formats: ['es'],
|
formats: ['es'],
|
||||||
fileName: 'index'
|
fileName: 'index'
|
||||||
},
|
},
|
||||||
|
ssr:true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: [
|
external: [
|
||||||
'fs',
|
'fs',
|
||||||
@ -17,10 +19,12 @@ export default defineConfig({
|
|||||||
'semver',
|
'semver',
|
||||||
'yargs',
|
'yargs',
|
||||||
'yargs/helpers'
|
'yargs/helpers'
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
target: 'node14',
|
target: 'node14',
|
||||||
|
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
|
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
sourcemap: true
|
sourcemap: true
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user