mirror of
https://github.com/warkanum/monorepo-dep-checker.git
synced 2026-10-31 06:08:57 +00:00
feat(cli): add CI gating flags for dependency checks
* Introduced --fail-on-diff and --fail-on-missing options * Updated run() to return a result summary * Enhanced README and tests to cover new functionality
This commit is contained in:
1 parent
c593038155
commit
ab8ec5a69c
8 files changed
+303
-75
No files matched your search
@@ -1,5 +1,11 @@
|
|||||||
# @warkypublic/monorepo-dep-checker
|
# @warkypublic/monorepo-dep-checker
|
||||||
|
|
||||||
|
## 1.0.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Added --fail-on-diff and --fail-on-missing CI gating flags, with run() now returning a result summary instead of void.
|
||||||
|
|
||||||
## 1.0.5
|
## 1.0.5
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ A powerful CLI tool for managing and checking dependencies across packages in a
|
|||||||
- 🔄 Update dependencies to the highest compatible version
|
- 🔄 Update dependencies to the highest compatible version
|
||||||
- 🏗️ Full workspace package support
|
- 🏗️ Full workspace package support
|
||||||
- 📊 Clear, actionable summaries
|
- 📊 Clear, actionable summaries
|
||||||
|
- 🚦 CI gating via exit codes (`--fail-on-diff`, `--fail-on-missing`)
|
||||||
- 🚀 Fast, zero-config setup
|
- 🚀 Fast, zero-config setup
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -98,6 +99,32 @@ Get results in JSON format for further processing:
|
|||||||
dep-check --check-versions --format json
|
dep-check --check-versions --format json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 6. CI Gating
|
||||||
|
|
||||||
|
Exit with code 1 when the check finds a problem, so a pipeline fails:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fail if any dependency resolves to more than one version
|
||||||
|
dep-check --check-versions --fail-on-diff
|
||||||
|
|
||||||
|
# Fail if any package depends on something the main app does not declare
|
||||||
|
dep-check --check-missing --fail-on-missing
|
||||||
|
|
||||||
|
# Both gates in one run
|
||||||
|
dep-check --fail-on-diff --fail-on-missing
|
||||||
|
```
|
||||||
|
|
||||||
|
| Exit code | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `0` | No gate was tripped |
|
||||||
|
| `1` | A gate was tripped, or the run errored |
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- A gate runs whatever analysis it needs, even outside its own mode — `--check-missing --fail-on-diff` still detects version conflicts.
|
||||||
|
- Failure messages go to **stderr**, so `--format json` keeps stdout parseable.
|
||||||
|
- Gates cannot be combined with `--update`: `--update` rewrites the tree mid-run, which would leave the exit code describing a state that no longer exists.
|
||||||
|
|
||||||
## Command Line Options
|
## Command Line Options
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -108,9 +135,12 @@ Options:
|
|||||||
--dry-run, -d Show what would be updated without making changes
|
--dry-run, -d Show what would be updated without making changes
|
||||||
--check-versions Check for version differences between packages
|
--check-versions Check for version differences between packages
|
||||||
--check-missing Check for dependencies missing between app and packages
|
--check-missing Check for dependencies missing between app and packages
|
||||||
--format Output format (text or json) [default: "text"]
|
--format, -f Output format (text or json) [default: "text"]
|
||||||
--help Show help
|
--strict, -s Only update range deps (~, ^, >=) when they are incompatible
|
||||||
--version Show version number
|
--fail-on-diff Exit 1 if any dependency resolves to more than one version
|
||||||
|
--fail-on-missing Exit 1 if a package depends on something the main app does not
|
||||||
|
--help, -h Show help
|
||||||
|
--version, -V Show version number
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -142,6 +172,17 @@ Options:
|
|||||||
2. Use `--dry-run` before applying updates
|
2. Use `--dry-run` before applying updates
|
||||||
3. Review major version differences manually
|
3. Review major version differences manually
|
||||||
4. Keep workspace dependencies consistent across packages
|
4. Keep workspace dependencies consistent across packages
|
||||||
|
5. Gate CI with `--check-versions --fail-on-diff` so drift cannot merge
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm typecheck # tsc --noEmit
|
||||||
|
pnpm lint # eslint ./src
|
||||||
|
pnpm test # vitest
|
||||||
|
pnpm build # vite build -> dist/
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@warkypublic/monorepo-dep-checker",
|
"name": "@warkypublic/monorepo-dep-checker",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"description": "A CLI tool to check and manage dependencies across packages in a monorepo",
|
"description": "A CLI tool to check and manage dependencies across packages in a monorepo",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
@@ -17,8 +17,9 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run typecheck && npm run build",
|
||||||
"test": "vitest run --silent=false",
|
"test": "vitest run --silent=false",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "eslint ./src"
|
"lint": "eslint ./src"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
"eslint": "^9.26.0",
|
"eslint": "^9.26.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"prettier-eslint": "^16.4.1",
|
"prettier-eslint": "^16.4.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.32.0",
|
"typescript-eslint": "^8.32.0",
|
||||||
"typesync": "^0.14.3",
|
"typesync": "^0.14.3",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
packages:
|
||||||
|
- .
|
||||||
|
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
+39
-1
@@ -53,6 +53,16 @@ const cli = async () => {
|
|||||||
default: 'text',
|
default: 'text',
|
||||||
type:'string'
|
type:'string'
|
||||||
})
|
})
|
||||||
|
.option('fail-on-diff', {
|
||||||
|
describe: 'Exit with code 1 if any dependency resolves to more than one version (CI gating)',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
.option('fail-on-missing', {
|
||||||
|
describe: 'Exit with code 1 if any package depends on something the main app does not (CI gating)',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
.option('strict', {
|
.option('strict', {
|
||||||
alias: 's',
|
alias: 's',
|
||||||
describe: 'Strict mode: only update packages with version ranges (~, ^, >=) if they are incompatible',
|
describe: 'Strict mode: only update packages with version ranges (~, ^, >=) if they are incompatible',
|
||||||
@@ -66,6 +76,8 @@ const cli = async () => {
|
|||||||
.example('$0 --packages ./packages,./other-packages', 'Check multiple package directories')
|
.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 ./pkg1/package.json,./pkg2/package.json', 'Check specific package.json files')
|
||||||
.example('$0 --packages ./packages,./other/package.json', 'Mix of directories and files')
|
.example('$0 --packages ./packages,./other/package.json', 'Mix of directories and files')
|
||||||
|
.example('$0 --check-versions --fail-on-diff', 'Fail CI on version conflicts')
|
||||||
|
.example('$0 --check-missing --fail-on-missing', 'Fail CI on unsynchronised dependencies')
|
||||||
.epilogue('For more information, visit: https://github.com/warkanum/monorepo-dep-checker')
|
.epilogue('For more information, visit: https://github.com/warkanum/monorepo-dep-checker')
|
||||||
.wrap(Math.min(120, process.stdout.columns))
|
.wrap(Math.min(120, process.stdout.columns))
|
||||||
.version()
|
.version()
|
||||||
@@ -134,17 +146,43 @@ const cli = async () => {
|
|||||||
console.error(chalk.yellow('Warning: --dry-run has no effect without --update'));
|
console.error(chalk.yellow('Warning: --dry-run has no effect without --update'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The gates report on the tree as found; --update rewrites it mid-run, which
|
||||||
|
// would leave the exit code describing a state that no longer exists.
|
||||||
|
if (argv.update && (argv.failOnDiff || argv.failOnMissing)) {
|
||||||
|
console.error(
|
||||||
|
chalk.red('Error: --fail-on-diff and --fail-on-missing cannot be combined with --update')
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const checker = new DependencyChecker(appPackageJson, packagesInput);
|
const checker = new DependencyChecker(appPackageJson, packagesInput);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await checker.run({
|
const result = await checker.run({
|
||||||
update: argv.update,
|
update: argv.update,
|
||||||
dryRun: argv.dryRun,
|
dryRun: argv.dryRun,
|
||||||
format: argv.format as any,
|
format: argv.format as any,
|
||||||
checkVersions: argv.checkVersions,
|
checkVersions: argv.checkVersions,
|
||||||
checkMissing: argv.checkMissing,
|
checkMissing: argv.checkMissing,
|
||||||
strict: argv.strict,
|
strict: argv.strict,
|
||||||
|
failOnDiff: argv.failOnDiff,
|
||||||
|
failOnMissing: argv.failOnMissing,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (result.failed) {
|
||||||
|
// stderr, so --format json keeps stdout parseable.
|
||||||
|
if (argv.failOnDiff && result.conflicts > 0) {
|
||||||
|
console.error(
|
||||||
|
chalk.red(`\nFailed: ${result.conflicts} dependenc${result.conflicts === 1 ? 'y' : 'ies'} with conflicting versions`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (argv.failOnMissing && result.missing > 0) {
|
||||||
|
console.error(
|
||||||
|
chalk.red(`\nFailed: ${result.missing} dependenc${result.missing === 1 ? 'y' : 'ies'} missing from the main app`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
} catch (error:any) {
|
} catch (error:any) {
|
||||||
console.error(chalk.red('Error during execution:'));
|
console.error(chalk.red('Error during execution:'));
|
||||||
console.error(error?.message);
|
console.error(error?.message);
|
||||||
|
|||||||
+134
-67
@@ -17,6 +17,21 @@ interface RunOptions {
|
|||||||
checkVersions?: boolean;
|
checkVersions?: boolean;
|
||||||
checkMissing?: boolean;
|
checkMissing?: boolean;
|
||||||
strict?: boolean;
|
strict?: boolean;
|
||||||
|
failOnDiff?: boolean;
|
||||||
|
failOnMissing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RunResult {
|
||||||
|
/** Dependencies resolved to more than one version across packages. */
|
||||||
|
conflicts: number;
|
||||||
|
/** Unique dependencies used by a package but absent from the main app. */
|
||||||
|
missing: number;
|
||||||
|
/** Unique dependencies in the main app that no package uses. */
|
||||||
|
extra: number;
|
||||||
|
/** Dependency versions rewritten (or that would be, under --dry-run). */
|
||||||
|
updates: number;
|
||||||
|
/** True when a --fail-on-* gate was tripped. */
|
||||||
|
failed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VersionInfo {
|
interface VersionInfo {
|
||||||
@@ -77,6 +92,10 @@ class DependencyChecker {
|
|||||||
private dependencyMap: Map<string, DependencyInfo>;
|
private dependencyMap: Map<string, DependencyInfo>;
|
||||||
private workspacePackages: Set<string>;
|
private workspacePackages: Set<string>;
|
||||||
private strictMode: boolean;
|
private strictMode: boolean;
|
||||||
|
private packageJsonCache: Map<string, PackageJson>;
|
||||||
|
private packagePathByName: Map<string, string> | null;
|
||||||
|
private workspacesScanned: boolean;
|
||||||
|
private analyzed: boolean;
|
||||||
|
|
||||||
constructor(appPackageJsonPath: string, packagesInput: string) {
|
constructor(appPackageJsonPath: string, packagesInput: string) {
|
||||||
this.appPackageJsonPath = appPackageJsonPath;
|
this.appPackageJsonPath = appPackageJsonPath;
|
||||||
@@ -85,9 +104,50 @@ class DependencyChecker {
|
|||||||
this.dependencyMap = new Map();
|
this.dependencyMap = new Map();
|
||||||
this.workspacePackages = new Set();
|
this.workspacePackages = new Set();
|
||||||
this.strictMode = false;
|
this.strictMode = false;
|
||||||
|
this.packageJsonCache = new Map();
|
||||||
|
this.packagePathByName = null;
|
||||||
|
this.workspacesScanned = false;
|
||||||
|
this.analyzed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a package.json once and reuse it. Every lookup in this class goes
|
||||||
|
* through here, so a package.json is read from disk at most once per run.
|
||||||
|
*/
|
||||||
|
private readPackageJson(filePath: string): PackageJson {
|
||||||
|
const cached = this.packageJsonCache.get(filePath);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
fs.readFileSync(filePath, "utf8")
|
||||||
|
) as PackageJson;
|
||||||
|
this.packageJsonCache.set(filePath, packageJson);
|
||||||
|
return packageJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a package name to the package.json that declares it. Built once
|
||||||
|
* from the cache instead of rescanning every file per lookup. First
|
||||||
|
* declaration wins, matching the previous find()-based behaviour.
|
||||||
|
*/
|
||||||
|
private getPackagePath(packageName: string): string {
|
||||||
|
if (!this.packagePathByName) {
|
||||||
|
this.packagePathByName = new Map();
|
||||||
|
this.packageJsonFiles.forEach((filePath) => {
|
||||||
|
const { name } = this.readPackageJson(filePath);
|
||||||
|
if (name && !this.packagePathByName!.has(name)) {
|
||||||
|
this.packagePathByName!.set(name, filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.packagePathByName.get(packageName) || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private findWorkspacePackages(): void {
|
private findWorkspacePackages(): void {
|
||||||
|
if (this.workspacesScanned) return;
|
||||||
|
this.workspacesScanned = true;
|
||||||
|
|
||||||
const paths = this.packagesInput.split(",").map((p) => p.trim());
|
const paths = this.packagesInput.split(",").map((p) => p.trim());
|
||||||
|
|
||||||
paths.forEach((inputPath) => {
|
paths.forEach((inputPath) => {
|
||||||
@@ -101,24 +161,21 @@ class DependencyChecker {
|
|||||||
"package.json"
|
"package.json"
|
||||||
);
|
);
|
||||||
if (fs.existsSync(packageJsonPath)) {
|
if (fs.existsSync(packageJsonPath)) {
|
||||||
const packageJson = JSON.parse(
|
this.workspacePackages.add(
|
||||||
fs.readFileSync(packageJsonPath, "utf8")
|
this.readPackageJson(packageJsonPath).name
|
||||||
) as PackageJson;
|
);
|
||||||
this.workspacePackages.add(packageJson.name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (inputPath.endsWith("package.json")) {
|
} else if (inputPath.endsWith("package.json")) {
|
||||||
const packageJson = JSON.parse(
|
this.workspacePackages.add(this.readPackageJson(inputPath).name);
|
||||||
fs.readFileSync(inputPath, "utf8")
|
|
||||||
) as PackageJson;
|
|
||||||
this.workspacePackages.add(packageJson.name);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private findPackageJsonFiles(): void {
|
private findPackageJsonFiles(): void {
|
||||||
this.packageJsonFiles = [this.appPackageJsonPath];
|
this.packageJsonFiles = [this.appPackageJsonPath];
|
||||||
|
this.packagePathByName = null;
|
||||||
const paths = this.packagesInput.split(",").map((p) => p.trim());
|
const paths = this.packagesInput.split(",").map((p) => p.trim());
|
||||||
|
|
||||||
paths.forEach((inputPath) => {
|
paths.forEach((inputPath) => {
|
||||||
@@ -206,14 +263,12 @@ class DependencyChecker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkMissingDependencies(): void {
|
private checkMissingDependencies(): { missing: number; extra: number } {
|
||||||
console.log(chalk.bold("\nChecking for missing dependencies...\n"));
|
console.log(chalk.bold("\nChecking for missing dependencies...\n"));
|
||||||
|
|
||||||
this.findWorkspacePackages();
|
this.findWorkspacePackages();
|
||||||
|
|
||||||
const appPackageJson = JSON.parse(
|
const appPackageJson = this.readPackageJson(this.appPackageJsonPath);
|
||||||
fs.readFileSync(this.appPackageJsonPath, "utf8")
|
|
||||||
) as PackageJson;
|
|
||||||
const appDeps = {
|
const appDeps = {
|
||||||
...appPackageJson.dependencies,
|
...appPackageJson.dependencies,
|
||||||
...appPackageJson.devDependencies,
|
...appPackageJson.devDependencies,
|
||||||
@@ -227,9 +282,7 @@ class DependencyChecker {
|
|||||||
this.packageJsonFiles
|
this.packageJsonFiles
|
||||||
.filter((file) => file !== this.appPackageJsonPath)
|
.filter((file) => file !== this.appPackageJsonPath)
|
||||||
.forEach((filePath) => {
|
.forEach((filePath) => {
|
||||||
const packageJson = JSON.parse(
|
const packageJson = this.readPackageJson(filePath);
|
||||||
fs.readFileSync(filePath, "utf8")
|
|
||||||
) as PackageJson;
|
|
||||||
const packageName = packageJson.name;
|
const packageName = packageJson.name;
|
||||||
|
|
||||||
const packageDeps = {
|
const packageDeps = {
|
||||||
@@ -273,7 +326,7 @@ class DependencyChecker {
|
|||||||
console.log(
|
console.log(
|
||||||
chalk.green("✓ All dependencies are properly synchronized\n")
|
chalk.green("✓ All dependencies are properly synchronized\n")
|
||||||
);
|
);
|
||||||
return;
|
return { missing: 0, extra: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
missingDeps.forEach(
|
missingDeps.forEach(
|
||||||
@@ -322,6 +375,17 @@ class DependencyChecker {
|
|||||||
console.log(chalk.gray(" " + Array.from(uniqueExtraDeps).join(", ")));
|
console.log(chalk.gray(" " + Array.from(uniqueExtraDeps).join(", ")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { missing: uniqueMissingDeps.size, extra: uniqueExtraDeps.size };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dependencies that resolved to more than one version across packages. */
|
||||||
|
private countConflicts(): number {
|
||||||
|
let conflicts = 0;
|
||||||
|
this.dependencyMap.forEach((depInfo) => {
|
||||||
|
if (depInfo.versions.size > 1) conflicts++;
|
||||||
|
});
|
||||||
|
return conflicts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private compareDependencyVersions(
|
private compareDependencyVersions(
|
||||||
@@ -349,12 +413,13 @@ class DependencyChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private analyzeDependencies(): void {
|
private analyzeDependencies(): void {
|
||||||
|
if (this.analyzed) return;
|
||||||
|
this.analyzed = true;
|
||||||
|
|
||||||
this.findWorkspacePackages();
|
this.findWorkspacePackages();
|
||||||
|
|
||||||
this.packageJsonFiles.forEach((filePath) => {
|
this.packageJsonFiles.forEach((filePath) => {
|
||||||
const packageJson = JSON.parse(
|
const packageJson = this.readPackageJson(filePath);
|
||||||
fs.readFileSync(filePath, "utf8")
|
|
||||||
) as PackageJson;
|
|
||||||
const normalDeps = packageJson.dependencies || {};
|
const normalDeps = packageJson.dependencies || {};
|
||||||
const peerDeps = packageJson.peerDependencies || {};
|
const peerDeps = packageJson.peerDependencies || {};
|
||||||
const devDeps = packageJson.devDependencies || {};
|
const devDeps = packageJson.devDependencies || {};
|
||||||
@@ -413,18 +478,10 @@ class DependencyChecker {
|
|||||||
([version, info]) => ({
|
([version, info]) => ({
|
||||||
version,
|
version,
|
||||||
packages: Array.from(info.packages),
|
packages: Array.from(info.packages),
|
||||||
paths: Array.from(info.packages).map((pkg) => {
|
paths: Array.from(info.packages).map((pkg) => ({
|
||||||
const filePath = this.packageJsonFiles.find((file) => {
|
package: pkg,
|
||||||
const json = JSON.parse(
|
path: path.relative(process.cwd(), this.getPackagePath(pkg)),
|
||||||
fs.readFileSync(file, "utf8")
|
})),
|
||||||
) as PackageJson;
|
|
||||||
return json.name === pkg;
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
package: pkg,
|
|
||||||
path: path.relative(process.cwd(), filePath || ""),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
usages: Array.from(info.usages),
|
usages: Array.from(info.usages),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -581,9 +638,7 @@ class DependencyChecker {
|
|||||||
): Promise<DependencyUpdate[]> {
|
): Promise<DependencyUpdate[]> {
|
||||||
console.log(chalk.bold("\nUpdating dependencies...\n"));
|
console.log(chalk.bold("\nUpdating dependencies...\n"));
|
||||||
|
|
||||||
const appPackageJson = JSON.parse(
|
const appPackageJson = this.readPackageJson(this.appPackageJsonPath);
|
||||||
fs.readFileSync(this.appPackageJsonPath, "utf8")
|
|
||||||
) as PackageJson;
|
|
||||||
const appDependencies = {
|
const appDependencies = {
|
||||||
...appPackageJson.dependencies,
|
...appPackageJson.dependencies,
|
||||||
...appPackageJson.devDependencies,
|
...appPackageJson.devDependencies,
|
||||||
@@ -595,9 +650,7 @@ class DependencyChecker {
|
|||||||
this.packageJsonFiles
|
this.packageJsonFiles
|
||||||
.filter((filePath) => filePath !== this.appPackageJsonPath)
|
.filter((filePath) => filePath !== this.appPackageJsonPath)
|
||||||
.forEach((filePath) => {
|
.forEach((filePath) => {
|
||||||
const packageJson = JSON.parse(
|
const packageJson = this.readPackageJson(filePath);
|
||||||
fs.readFileSync(filePath, "utf8")
|
|
||||||
) as PackageJson;
|
|
||||||
let hasUpdates = false;
|
let hasUpdates = false;
|
||||||
|
|
||||||
const updateDependencySection = (section: DependencyTypes): void => {
|
const updateDependencySection = (section: DependencyTypes): void => {
|
||||||
@@ -704,18 +757,10 @@ class DependencyChecker {
|
|||||||
([version, info]) => ({
|
([version, info]) => ({
|
||||||
version,
|
version,
|
||||||
packages: Array.from(info.packages),
|
packages: Array.from(info.packages),
|
||||||
paths: Array.from(info.packages).map((pkg) => {
|
paths: Array.from(info.packages).map((pkg) => ({
|
||||||
const filePath = this.packageJsonFiles.find((file) => {
|
package: pkg,
|
||||||
const json = JSON.parse(
|
path: path.relative(process.cwd(), this.getPackagePath(pkg)),
|
||||||
fs.readFileSync(file, "utf8")
|
})),
|
||||||
) as PackageJson;
|
|
||||||
return json.name === pkg;
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
package: pkg,
|
|
||||||
path: path.relative(process.cwd(), filePath || ""),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
usages: Array.from(info.usages),
|
usages: Array.from(info.usages),
|
||||||
isWorkspace: this.isWorkspaceDep(version),
|
isWorkspace: this.isWorkspaceDep(version),
|
||||||
})
|
})
|
||||||
@@ -752,7 +797,7 @@ class DependencyChecker {
|
|||||||
this.displayVersionDifferences();
|
this.displayVersionDifferences();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run(options: RunOptions = {}): Promise<void> {
|
public async run(options: RunOptions = {}): Promise<RunResult> {
|
||||||
const {
|
const {
|
||||||
update = false,
|
update = false,
|
||||||
dryRun = false,
|
dryRun = false,
|
||||||
@@ -760,34 +805,56 @@ class DependencyChecker {
|
|||||||
checkVersions = false,
|
checkVersions = false,
|
||||||
checkMissing = false,
|
checkMissing = false,
|
||||||
strict = false,
|
strict = false,
|
||||||
|
failOnDiff = false,
|
||||||
|
failOnMissing = false,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
this.strictMode = strict;
|
this.strictMode = strict;
|
||||||
|
this.findPackageJsonFiles();
|
||||||
|
|
||||||
|
const result: RunResult = {
|
||||||
|
conflicts: 0,
|
||||||
|
missing: 0,
|
||||||
|
extra: 0,
|
||||||
|
updates: 0,
|
||||||
|
failed: false,
|
||||||
|
};
|
||||||
|
|
||||||
if (checkMissing) {
|
if (checkMissing) {
|
||||||
this.findPackageJsonFiles();
|
const counts = this.checkMissingDependencies();
|
||||||
this.checkMissingDependencies();
|
result.missing = counts.missing;
|
||||||
return;
|
result.extra = counts.extra;
|
||||||
}
|
} else if (checkVersions) {
|
||||||
|
|
||||||
if (checkVersions) {
|
|
||||||
this.findPackageJsonFiles();
|
|
||||||
this.analyzeDependencies();
|
this.analyzeDependencies();
|
||||||
this.displayVersionDifferences();
|
this.displayVersionDifferences();
|
||||||
return;
|
} else if (update) {
|
||||||
}
|
|
||||||
|
|
||||||
if (update) {
|
|
||||||
this.findPackageJsonFiles();
|
|
||||||
this.analyzeDependencies();
|
this.analyzeDependencies();
|
||||||
await this.updateDependencies(dryRun);
|
result.updates = (await this.updateDependencies(dryRun)).length;
|
||||||
return;
|
} else {
|
||||||
|
this.analyzeDependencies();
|
||||||
|
this.displayResults(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.findPackageJsonFiles();
|
// A --fail-on-* gate may need analysis the selected mode did not run.
|
||||||
this.analyzeDependencies();
|
if (failOnMissing && !checkMissing) {
|
||||||
this.displayResults(format);
|
const counts = this.checkMissingDependencies();
|
||||||
|
result.missing = counts.missing;
|
||||||
|
result.extra = counts.extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failOnDiff && !this.analyzed) {
|
||||||
|
this.analyzeDependencies();
|
||||||
|
this.displayVersionDifferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.conflicts = this.countConflicts();
|
||||||
|
result.failed =
|
||||||
|
(failOnDiff && result.conflicts > 0) ||
|
||||||
|
(failOnMissing && result.missing > 0);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { DependencyChecker };
|
export { DependencyChecker };
|
||||||
|
export type { RunOptions, RunResult };
|
||||||
@@ -152,5 +152,74 @@ describe('DependencyChecker', () => {
|
|||||||
expect(calls).toMatch(/Difference \((?:major|minor|patch)\)/);
|
expect(calls).toMatch(/Difference \((?:major|minor|patch)\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should report conflict and missing counts from run()', async () => {
|
||||||
|
const versionResult = await checker.run({ checkVersions: true });
|
||||||
|
// react (^18.2.0 vs ^17.0.2) and typescript (^5.0.0 vs ^4.9.0) both differ.
|
||||||
|
expect(versionResult.conflicts).toBeGreaterThan(0);
|
||||||
|
expect(versionResult.failed).toBe(false);
|
||||||
|
|
||||||
});
|
const missingChecker = new DependencyChecker(appPackageJsonPath, packagesPath);
|
||||||
|
const missingResult = await missingChecker.run({ checkMissing: true });
|
||||||
|
// pkg2 depends on moment, which the app does not declare.
|
||||||
|
expect(missingResult.missing).toBeGreaterThan(0);
|
||||||
|
expect(missingResult.failed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should flag failure when --fail-on-diff finds conflicts', async () => {
|
||||||
|
const result = await checker.run({ checkVersions: true, failOnDiff: true });
|
||||||
|
|
||||||
|
expect(result.conflicts).toBeGreaterThan(0);
|
||||||
|
expect(result.failed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should flag failure when --fail-on-missing finds gaps', async () => {
|
||||||
|
const result = await checker.run({ checkMissing: true, failOnMissing: true });
|
||||||
|
|
||||||
|
expect(result.missing).toBeGreaterThan(0);
|
||||||
|
expect(result.failed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should run the analysis a gate needs even outside its own mode', async () => {
|
||||||
|
// checkMissing mode does not analyse versions; the gate must still work.
|
||||||
|
const result = await checker.run({ checkMissing: true, failOnDiff: true });
|
||||||
|
|
||||||
|
expect(result.conflicts).toBeGreaterThan(0);
|
||||||
|
expect(result.failed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not fail a clean tree', async () => {
|
||||||
|
const cleanDir = path.join(__dirname, 'clean');
|
||||||
|
fs.mkdirSync(cleanDir, { recursive: true });
|
||||||
|
|
||||||
|
const cleanChecker = new DependencyChecker(appPackageJsonPath, cleanDir);
|
||||||
|
const result = await cleanChecker.run({
|
||||||
|
checkVersions: true,
|
||||||
|
failOnDiff: true,
|
||||||
|
failOnMissing: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.rmSync(cleanDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
expect(result.conflicts).toBe(0);
|
||||||
|
expect(result.missing).toBe(0);
|
||||||
|
expect(result.failed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should read each package.json at most once per run', async () => {
|
||||||
|
const readSpy = vi.spyOn(fs, 'readFileSync');
|
||||||
|
|
||||||
|
await checker.run({ format: 'json', failOnMissing: true });
|
||||||
|
|
||||||
|
const reads = readSpy.mock.calls
|
||||||
|
.map((call) => String(call[0]))
|
||||||
|
.filter((file) => file.endsWith('package.json'));
|
||||||
|
|
||||||
|
const perFile = new Map<string, number>();
|
||||||
|
reads.forEach((file) => perFile.set(file, (perFile.get(file) || 0) + 1));
|
||||||
|
|
||||||
|
readSpy.mockRestore();
|
||||||
|
|
||||||
|
expect(perFile.size).toBeGreaterThan(0);
|
||||||
|
Array.from(perFile.values()).forEach((count) => expect(count).toBe(1));
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"lib": ["DOM", "ESNext"],
|
"lib": ["DOM", "ESNext"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Bundler",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|||||||
Reference in new issue
Block a user