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
+302
-74
No files matched your search
@@ -1,5 +1,11 @@
|
||||
# @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
|
||||
|
||||
### 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
|
||||
- 🏗️ Full workspace package support
|
||||
- 📊 Clear, actionable summaries
|
||||
- 🚦 CI gating via exit codes (`--fail-on-diff`, `--fail-on-missing`)
|
||||
- 🚀 Fast, zero-config setup
|
||||
|
||||
## Installation
|
||||
@@ -98,6 +99,32 @@ Get results in JSON format for further processing:
|
||||
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
|
||||
|
||||
```bash
|
||||
@@ -108,9 +135,12 @@ Options:
|
||||
--dry-run, -d Show what would be updated without making changes
|
||||
--check-versions Check for version differences between packages
|
||||
--check-missing Check for dependencies missing between app and packages
|
||||
--format Output format (text or json) [default: "text"]
|
||||
--help Show help
|
||||
--version Show version number
|
||||
--format, -f Output format (text or json) [default: "text"]
|
||||
--strict, -s Only update range deps (~, ^, >=) when they are incompatible
|
||||
--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
|
||||
@@ -142,6 +172,17 @@ Options:
|
||||
2. Use `--dry-run` before applying updates
|
||||
3. Review major version differences manually
|
||||
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
|
||||
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
@@ -17,8 +17,9 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build",
|
||||
"prepublishOnly": "npm run typecheck && npm run build",
|
||||
"test": "vitest run --silent=false",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint ./src"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -50,6 +51,7 @@
|
||||
"eslint": "^9.26.0",
|
||||
"globals": "^16.0.0",
|
||||
"prettier-eslint": "^16.4.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.32.0",
|
||||
"typesync": "^0.14.3",
|
||||
"vite": "^6.3.5",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
packages:
|
||||
- .
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
+39
-1
@@ -53,6 +53,16 @@ const cli = async () => {
|
||||
default: 'text',
|
||||
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', {
|
||||
alias: 's',
|
||||
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 ./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 --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')
|
||||
.wrap(Math.min(120, process.stdout.columns))
|
||||
.version()
|
||||
@@ -134,17 +146,43 @@ const cli = async () => {
|
||||
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);
|
||||
|
||||
try {
|
||||
await checker.run({
|
||||
const result = await checker.run({
|
||||
update: argv.update,
|
||||
dryRun: argv.dryRun,
|
||||
format: argv.format as any,
|
||||
checkVersions: argv.checkVersions,
|
||||
checkMissing: argv.checkMissing,
|
||||
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) {
|
||||
console.error(chalk.red('Error during execution:'));
|
||||
console.error(error?.message);
|
||||
|
||||
+134
-67
@@ -17,6 +17,21 @@ interface RunOptions {
|
||||
checkVersions?: boolean;
|
||||
checkMissing?: 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 {
|
||||
@@ -77,6 +92,10 @@ class DependencyChecker {
|
||||
private dependencyMap: Map<string, DependencyInfo>;
|
||||
private workspacePackages: Set<string>;
|
||||
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) {
|
||||
this.appPackageJsonPath = appPackageJsonPath;
|
||||
@@ -85,9 +104,50 @@ class DependencyChecker {
|
||||
this.dependencyMap = new Map();
|
||||
this.workspacePackages = new Set();
|
||||
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 {
|
||||
if (this.workspacesScanned) return;
|
||||
this.workspacesScanned = true;
|
||||
|
||||
const paths = this.packagesInput.split(",").map((p) => p.trim());
|
||||
|
||||
paths.forEach((inputPath) => {
|
||||
@@ -101,24 +161,21 @@ class DependencyChecker {
|
||||
"package.json"
|
||||
);
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(packageJsonPath, "utf8")
|
||||
) as PackageJson;
|
||||
this.workspacePackages.add(packageJson.name);
|
||||
this.workspacePackages.add(
|
||||
this.readPackageJson(packageJsonPath).name
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (inputPath.endsWith("package.json")) {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(inputPath, "utf8")
|
||||
) as PackageJson;
|
||||
this.workspacePackages.add(packageJson.name);
|
||||
this.workspacePackages.add(this.readPackageJson(inputPath).name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private findPackageJsonFiles(): void {
|
||||
this.packageJsonFiles = [this.appPackageJsonPath];
|
||||
this.packagePathByName = null;
|
||||
const paths = this.packagesInput.split(",").map((p) => p.trim());
|
||||
|
||||
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"));
|
||||
|
||||
this.findWorkspacePackages();
|
||||
|
||||
const appPackageJson = JSON.parse(
|
||||
fs.readFileSync(this.appPackageJsonPath, "utf8")
|
||||
) as PackageJson;
|
||||
const appPackageJson = this.readPackageJson(this.appPackageJsonPath);
|
||||
const appDeps = {
|
||||
...appPackageJson.dependencies,
|
||||
...appPackageJson.devDependencies,
|
||||
@@ -227,9 +282,7 @@ class DependencyChecker {
|
||||
this.packageJsonFiles
|
||||
.filter((file) => file !== this.appPackageJsonPath)
|
||||
.forEach((filePath) => {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(filePath, "utf8")
|
||||
) as PackageJson;
|
||||
const packageJson = this.readPackageJson(filePath);
|
||||
const packageName = packageJson.name;
|
||||
|
||||
const packageDeps = {
|
||||
@@ -273,7 +326,7 @@ class DependencyChecker {
|
||||
console.log(
|
||||
chalk.green("✓ All dependencies are properly synchronized\n")
|
||||
);
|
||||
return;
|
||||
return { missing: 0, extra: 0 };
|
||||
}
|
||||
|
||||
missingDeps.forEach(
|
||||
@@ -322,6 +375,17 @@ class DependencyChecker {
|
||||
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(
|
||||
@@ -349,12 +413,13 @@ class DependencyChecker {
|
||||
}
|
||||
|
||||
private analyzeDependencies(): void {
|
||||
if (this.analyzed) return;
|
||||
this.analyzed = true;
|
||||
|
||||
this.findWorkspacePackages();
|
||||
|
||||
this.packageJsonFiles.forEach((filePath) => {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(filePath, "utf8")
|
||||
) as PackageJson;
|
||||
const packageJson = this.readPackageJson(filePath);
|
||||
const normalDeps = packageJson.dependencies || {};
|
||||
const peerDeps = packageJson.peerDependencies || {};
|
||||
const devDeps = packageJson.devDependencies || {};
|
||||
@@ -413,18 +478,10 @@ class DependencyChecker {
|
||||
([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 || ""),
|
||||
};
|
||||
}),
|
||||
paths: Array.from(info.packages).map((pkg) => ({
|
||||
package: pkg,
|
||||
path: path.relative(process.cwd(), this.getPackagePath(pkg)),
|
||||
})),
|
||||
usages: Array.from(info.usages),
|
||||
})
|
||||
);
|
||||
@@ -581,9 +638,7 @@ class DependencyChecker {
|
||||
): Promise<DependencyUpdate[]> {
|
||||
console.log(chalk.bold("\nUpdating dependencies...\n"));
|
||||
|
||||
const appPackageJson = JSON.parse(
|
||||
fs.readFileSync(this.appPackageJsonPath, "utf8")
|
||||
) as PackageJson;
|
||||
const appPackageJson = this.readPackageJson(this.appPackageJsonPath);
|
||||
const appDependencies = {
|
||||
...appPackageJson.dependencies,
|
||||
...appPackageJson.devDependencies,
|
||||
@@ -595,9 +650,7 @@ class DependencyChecker {
|
||||
this.packageJsonFiles
|
||||
.filter((filePath) => filePath !== this.appPackageJsonPath)
|
||||
.forEach((filePath) => {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(filePath, "utf8")
|
||||
) as PackageJson;
|
||||
const packageJson = this.readPackageJson(filePath);
|
||||
let hasUpdates = false;
|
||||
|
||||
const updateDependencySection = (section: DependencyTypes): void => {
|
||||
@@ -704,18 +757,10 @@ class DependencyChecker {
|
||||
([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 || ""),
|
||||
};
|
||||
}),
|
||||
paths: Array.from(info.packages).map((pkg) => ({
|
||||
package: pkg,
|
||||
path: path.relative(process.cwd(), this.getPackagePath(pkg)),
|
||||
})),
|
||||
usages: Array.from(info.usages),
|
||||
isWorkspace: this.isWorkspaceDep(version),
|
||||
})
|
||||
@@ -752,7 +797,7 @@ class DependencyChecker {
|
||||
this.displayVersionDifferences();
|
||||
}
|
||||
|
||||
public async run(options: RunOptions = {}): Promise<void> {
|
||||
public async run(options: RunOptions = {}): Promise<RunResult> {
|
||||
const {
|
||||
update = false,
|
||||
dryRun = false,
|
||||
@@ -760,34 +805,56 @@ class DependencyChecker {
|
||||
checkVersions = false,
|
||||
checkMissing = false,
|
||||
strict = false,
|
||||
failOnDiff = false,
|
||||
failOnMissing = false,
|
||||
} = options;
|
||||
|
||||
this.strictMode = strict;
|
||||
this.findPackageJsonFiles();
|
||||
|
||||
const result: RunResult = {
|
||||
conflicts: 0,
|
||||
missing: 0,
|
||||
extra: 0,
|
||||
updates: 0,
|
||||
failed: false,
|
||||
};
|
||||
|
||||
if (checkMissing) {
|
||||
this.findPackageJsonFiles();
|
||||
this.checkMissingDependencies();
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkVersions) {
|
||||
this.findPackageJsonFiles();
|
||||
const counts = this.checkMissingDependencies();
|
||||
result.missing = counts.missing;
|
||||
result.extra = counts.extra;
|
||||
} else if (checkVersions) {
|
||||
this.analyzeDependencies();
|
||||
this.displayVersionDifferences();
|
||||
return;
|
||||
}
|
||||
|
||||
if (update) {
|
||||
this.findPackageJsonFiles();
|
||||
} else if (update) {
|
||||
this.analyzeDependencies();
|
||||
await this.updateDependencies(dryRun);
|
||||
return;
|
||||
result.updates = (await this.updateDependencies(dryRun)).length;
|
||||
} else {
|
||||
this.analyzeDependencies();
|
||||
this.displayResults(format);
|
||||
}
|
||||
|
||||
this.findPackageJsonFiles();
|
||||
this.analyzeDependencies();
|
||||
this.displayResults(format);
|
||||
// A --fail-on-* gate may need analysis the selected mode did not run.
|
||||
if (failOnMissing && !checkMissing) {
|
||||
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 type { RunOptions, RunResult };
|
||||
@@ -152,5 +152,74 @@ describe('DependencyChecker', () => {
|
||||
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",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
Reference in new issue
Block a user