Skip to content

Commit 075f9d1

Browse files
authored
Check local package version consistency (#343)
1 parent 969e169 commit 075f9d1

File tree

9 files changed

+501
-257
lines changed

9 files changed

+501
-257
lines changed

README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
[![npm version][npm-image]][npm-url]
44
[![CI][ci-image]][ci-url]
55

6-
This CLI tool checks to ensure that dependencies are on consistent versions across a monorepo / yarn workspace. For example, every package in a workspace that has a dependency on `eslint` should specify the same version for it.
6+
This CLI tool enforces the following aspects of consistency across a monorepo / yarn workspace:
7+
8+
1. Dependencies are on consistent versions. For example, every package in a workspace that has a dependency on `eslint` should specify the same version for it.
9+
2. Dependencies on local packages use the local packages directly instead of older versions of them. For example, if one package `package1` in a workspace depends on another package `package2` in the workspace, `package1` should request the current version of `package2`.
710

811
## Motivation
912

@@ -27,9 +30,9 @@ To run, use this command and optionally pass the path to the workspace root (whe
2730
yarn check-dependency-version-consistency .
2831
```
2932

30-
If there are no dependency mismatches, the program will exit with success.
33+
If there are no inconsistencies, the program will exit with success.
3134

32-
If there are any dependency mismatches, the program will exit with failure and output the mismatching versions.
35+
If there are any inconsistencies, the program will exit with failure and output the mismatching versions.
3336

3437
## Example
3538

@@ -57,6 +60,9 @@ If there are any dependency mismatches, the program will exit with failure and o
5760
"name": "package1",
5861
"devDependencies": {
5962
"eslint": "^8.0.0"
63+
},
64+
"dependencies": {
65+
"package2": "^0.0.0"
6066
}
6167
}
6268
```
@@ -66,6 +72,7 @@ If there are any dependency mismatches, the program will exit with failure and o
6672
```json
6773
{
6874
"name": "package2",
75+
"version": "1.0.0",
6976
"devDependencies": {
7077
"eslint": "^7.0.0"
7178
}
@@ -86,14 +93,21 @@ If there are any dependency mismatches, the program will exit with failure and o
8693
Output:
8794

8895
```pt
89-
Found 1 dependency with mismatching versions across the workspace. Fix with `--fix`.
96+
Found 2 dependencies with mismatching versions across the workspace. Fix with `--fix`.
9097
╔════════╤════════╤════════════════════╗
9198
║ eslint │ Usages │ Packages ║
9299
╟────────┼────────┼────────────────────╢
93100
║ ^8.0.0 │ 1 │ package1 ║
94101
╟────────┼────────┼────────────────────╢
95102
║ ^7.0.0 │ 2 │ package2, package3 ║
96103
╚════════╧════════╧════════════════════╝
104+
╔══════════╤════════╤══════════╗
105+
║ package2 │ Usages │ Packages ║
106+
╟──────────┼────────┼──────────╢
107+
║ 1.0.0 │ 1 │ package2 ║
108+
╟──────────┼────────┼──────────╢
109+
║ ^0.0.0 │ 1 │ package1 ║
110+
╚══════════╧════════╧══════════╝
97111
```
98112

99113
## Options

lib/dependency-versions.ts

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Package } from './package.js';
44

55
export type DependenciesToVersionsSeen = Map<
66
string,
7-
{ package: Package; version: string }[]
7+
{ package: Package; version: string; isLocalPackageVersion: boolean }[]
88
>;
99

1010
export type MismatchingDependencyVersions = Array<{
@@ -36,7 +36,7 @@ export function calculateVersionsForEachDependency(
3636
): DependenciesToVersionsSeen {
3737
const dependenciesToVersionsSeen: DependenciesToVersionsSeen = new Map<
3838
string,
39-
{ package: Package; version: string }[]
39+
{ package: Package; version: string; isLocalPackageVersion: boolean }[]
4040
>();
4141
for (const package_ of packages) {
4242
recordDependencyVersionsForPackageJson(
@@ -51,6 +51,16 @@ function recordDependencyVersionsForPackageJson(
5151
dependenciesToVersionsSeen: DependenciesToVersionsSeen,
5252
package_: Package
5353
) {
54+
if (package_.packageJson.name && package_.packageJson.version) {
55+
recordDependencyVersion(
56+
dependenciesToVersionsSeen,
57+
package_.packageJson.name,
58+
package_.packageJson.version,
59+
package_,
60+
true
61+
);
62+
}
63+
5464
if (package_.packageJson.dependencies) {
5565
for (const [dependency, dependencyVersion] of Object.entries(
5666
package_.packageJson.dependencies
@@ -82,7 +92,8 @@ function recordDependencyVersion(
8292
dependenciesToVersionsSeen: DependenciesToVersionsSeen,
8393
dependency: string,
8494
version: string,
85-
package_: Package
95+
package_: Package,
96+
isLocalPackageVersion = false
8697
) {
8798
if (!dependenciesToVersionsSeen.has(dependency)) {
8899
dependenciesToVersionsSeen.set(dependency, []);
@@ -91,41 +102,79 @@ function recordDependencyVersion(
91102
/* istanbul ignore if */
92103
if (list) {
93104
// `list` should always exist at this point, this if statement is just to please TypeScript.
94-
list.push({ package: package_, version });
105+
list.push({ package: package_, version, isLocalPackageVersion });
95106
}
96107
}
97108

98109
export function calculateMismatchingVersions(
99110
dependencyVersions: DependenciesToVersionsSeen
100111
): MismatchingDependencyVersions {
101-
return [...dependencyVersions.keys()].sort().flatMap((dependency) => {
102-
const versionList = dependencyVersions.get(dependency);
103-
/* istanbul ignore if */
104-
if (!versionList) {
105-
// `versionList` should always exist at this point, this if statement is just to please TypeScript.
106-
return [];
107-
}
112+
// Loop through all dependencies seen.
113+
return [...dependencyVersions.entries()]
114+
.sort()
115+
.flatMap(([dependency, versionObjectsForDep]) => {
116+
/* istanbul ignore if */
117+
if (!versionObjectsForDep) {
118+
// Should always exist at this point, this if statement is just to please TypeScript.
119+
return [];
120+
}
108121

109-
const uniqueVersions = [
110-
...new Set(versionList.map((object) => object.version)),
111-
].sort();
122+
// Calculate unique versions seen for this dependency.
123+
const uniqueVersions = [
124+
...new Set(
125+
versionObjectsForDep
126+
.filter((versionObject) => !versionObject.isLocalPackageVersion)
127+
.map((versionObject) => versionObject.version)
128+
),
129+
].sort();
112130

113-
if (uniqueVersions.length > 1) {
114-
const uniqueVersionsWithInfo = uniqueVersions.map((uniqueVersion) => {
115-
const matchingVersions = versionList.filter(
116-
(object) => object.version === uniqueVersion
131+
// If we saw more than one unique version for this dependency, we found an inconsistency.
132+
if (uniqueVersions.length > 1) {
133+
const uniqueVersionsWithInfo = versionsObjectsWithSortedPackages(
134+
uniqueVersions,
135+
versionObjectsForDep
117136
);
118-
return {
119-
version: uniqueVersion,
120-
packages: matchingVersions
121-
.map((object) => object.package)
122-
.sort(Package.comparator),
123-
};
124-
});
125-
return { dependency, versions: uniqueVersionsWithInfo };
126-
}
137+
return { dependency, versions: uniqueVersionsWithInfo };
138+
}
139+
140+
// If we saw a local package version that isn't compatible with the local package's actual version, we found an inconsistency.
141+
const localPackageVersions = versionObjectsForDep
142+
.filter((object) => object.isLocalPackageVersion)
143+
.map((object) => object.version);
144+
if (
145+
localPackageVersions.length === 1 &&
146+
uniqueVersions.length === 1 &&
147+
!semver.satisfies(localPackageVersions[0], uniqueVersions[0])
148+
) {
149+
const uniqueVersionsWithInfo = versionsObjectsWithSortedPackages(
150+
[...uniqueVersions, ...localPackageVersions],
151+
versionObjectsForDep
152+
);
153+
return { dependency, versions: uniqueVersionsWithInfo };
154+
}
127155

128-
return [];
156+
return [];
157+
});
158+
}
159+
160+
function versionsObjectsWithSortedPackages(
161+
versions: string[],
162+
versionObjects: {
163+
package: Package;
164+
version: string;
165+
isLocalPackageVersion: boolean;
166+
}[]
167+
) {
168+
return versions.map((version) => {
169+
const matchingVersionObjects = versionObjects.filter(
170+
(versionObject) => versionObject.version === version
171+
);
172+
return {
173+
version,
174+
packages: matchingVersionObjects
175+
.map((object) => object.package)
176+
.sort(Package.comparator),
177+
};
129178
});
130179
}
131180

@@ -255,6 +304,8 @@ export function fixMismatchingVersions(
255304

256305
if (isFixed) {
257306
fixed.push(mismatchingVersion);
307+
} else {
308+
notFixed.push(mismatchingVersion);
258309
}
259310
}
260311

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"workspaces": [
3+
"*"
4+
]
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "package1",
3+
"dependencies": {
4+
"package2": "^0.0.0"
5+
}
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "package2",
3+
"version": "1.0.0"
4+
}

test/fixtures/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,7 @@ export const FIXTURE_PATH_PACKAGE_MISSING_NAME = join(
4040
FIXTURE_PATH,
4141
'package-missing-name'
4242
);
43+
export const FIXTURE_PATH_INCONSISTENT_LOCAL_PACKAGE_VERSION = join(
44+
FIXTURE_PATH,
45+
'inconsistent-local-package-version'
46+
);
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"name": "foo1"
2+
"name": "foo1",
3+
"version": "1.2.3"
34
}

test/fixtures/valid/package1/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "package1",
33
"dependencies": {
44
"foo": "1.2.3",
5+
"foo1": "^1.0.0",
56
"bar": "^4.5.6"
67
},
78
"devDependencies": {

0 commit comments

Comments
 (0)