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