Skip to content

Commit 9e24bcc

Browse files
authored
relax dependency semver check for inter-repo dependencies (#1516)
1 parent a7208ab commit 9e24bcc

File tree

5 files changed

+145
-31
lines changed

5 files changed

+145
-31
lines changed

scripts/components/dependencies_validator.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ void describe('Dependency validator', () => {
130130
},
131131
(err: Error) => {
132132
assert.ok(
133-
err.message.includes('is declared using inconsistent versions')
133+
err.message.includes(
134+
'dependency declarations must all the on the same semver range'
135+
)
134136
);
135137
assert.ok(err.message.includes('glob'));
136138
assert.ok(err.message.includes('zod'));
@@ -142,6 +144,33 @@ void describe('Dependency validator', () => {
142144
);
143145
});
144146

147+
void it('can detect inconsistent major versions of repo packages', async () => {
148+
const packagePaths = await glob(
149+
'scripts/components/test-resources/inter-repo-dependency-version-consistency-test-packages/*'
150+
);
151+
const validator = await new DependenciesValidator(
152+
packagePaths,
153+
{},
154+
[],
155+
execaMock as never
156+
);
157+
158+
await assert.rejects(
159+
() => validator.validate(),
160+
(err: Error) => {
161+
assert.ok(
162+
err.message.includes(
163+
'dependency declarations must all be on the same major version'
164+
)
165+
);
166+
assert.ok(err.message.includes('package1'));
167+
assert.ok(err.message.includes('package2'));
168+
assert.ok(err.message.includes('package3'));
169+
return true;
170+
}
171+
);
172+
});
173+
145174
void it('can detect inconsistent dependency declarations of linked dependencies', async () => {
146175
await assert.rejects(
147176
async () => {

scripts/components/dependencies_validator.ts

Lines changed: 97 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { execa as _execa } from 'execa';
22
import { EOL } from 'os';
3-
import { readPackageJson } from './package-json/package_json.js';
3+
import { PackageJson, readPackageJson } from './package-json/package_json.js';
44

55
export type DependencyRule =
66
| {
@@ -22,6 +22,15 @@ type DependencyViolation = {
2222
dependencyName: string;
2323
};
2424

25+
type DependencyDeclaration = {
26+
dependentPackageName: string;
27+
version: string;
28+
};
29+
30+
type DependencyVersionPredicate = (
31+
declarations: DependencyDeclaration[]
32+
) => true | string;
33+
2534
/**
2635
* Validates dependencies.
2736
*
@@ -33,6 +42,8 @@ type DependencyViolation = {
3342
* in order to assert consistency.
3443
*/
3544
export class DependenciesValidator {
45+
private repoPackageJsons: PackageJson[] | undefined = undefined;
46+
private repoPackageNames: string[] | undefined = undefined;
3647
/**
3748
* Creates dependency validator
3849
* @param packagePaths paths of packages to validate
@@ -61,16 +72,10 @@ export class DependenciesValidator {
6172
*/
6273
private async validateDependencyVersionsConsistency(): Promise<void> {
6374
console.log('Checking dependency versions consistency');
64-
const packageJsons = await Promise.all(
65-
this.packagePaths.map((packagePath) => readPackageJson(packagePath))
66-
);
75+
const packageJsons = await this.getRepoPackageJsons();
6776

6877
type DependencyVersionsUsage = {
69-
allVersions: Set<string>;
70-
allDeclarations: Array<{
71-
packageName: string;
72-
version: string;
73-
}>;
78+
allDeclarations: DependencyDeclaration[];
7479
};
7580

7681
const dependencyVersionsUsages: Record<string, DependencyVersionsUsage> =
@@ -80,23 +85,21 @@ export class DependenciesValidator {
8085
packageJson.dependencies,
8186
packageJson.devDependencies,
8287
packageJson.peerDependencies,
83-
].forEach((dependencies) => {
84-
if (dependencies) {
85-
for (const dependencyName of Object.keys(dependencies)) {
86-
const dependencyVersion = dependencies[dependencyName];
88+
].forEach((dependency) => {
89+
if (dependency) {
90+
for (const dependencyName of Object.keys(dependency)) {
91+
const dependencyVersion = dependency[dependencyName];
8792
let dependencyVersionsUsage =
8893
dependencyVersionsUsages[dependencyName];
8994
if (!dependencyVersionsUsage) {
9095
dependencyVersionsUsage = {
91-
allVersions: new Set(),
9296
allDeclarations: [],
9397
};
9498
dependencyVersionsUsages[dependencyName] =
9599
dependencyVersionsUsage;
96100
}
97-
dependencyVersionsUsage.allVersions.add(dependencyVersion);
98101
dependencyVersionsUsage.allDeclarations.push({
99-
packageName: packageJson.name,
102+
dependentPackageName: packageJson.name,
100103
version: dependencyVersion,
101104
});
102105
}
@@ -105,20 +108,18 @@ export class DependenciesValidator {
105108
}
106109

107110
const errors: Array<string> = [];
108-
for (const dependencyName of Object.keys(dependencyVersionsUsages)) {
109-
const dependencyVersionUsage = dependencyVersionsUsages[dependencyName];
110-
111-
/**
112-
* TODO: This is a temporary fix for execa version inconsistency. Remove this once execa version is fixed.
113-
* Issue: https://github.com/aws-amplify/amplify-backend/issues/962
114-
*/
115-
if (
116-
dependencyVersionUsage.allVersions.size > 1 /* Issue-962 Start */ &&
117-
dependencyName !== 'execa' /* Issue-962 End */
118-
) {
111+
for (const [dependencyName, dependencyVersionUsage] of Object.entries(
112+
dependencyVersionsUsages
113+
)) {
114+
const validationResult = (
115+
await this.getPackageVersionDeclarationPredicate(dependencyName)
116+
)(dependencyVersionUsage.allDeclarations);
117+
if (typeof validationResult === 'string') {
119118
errors.push(
120-
`Dependency ${dependencyName} is declared using inconsistent versions ${JSON.stringify(
121-
dependencyVersionUsage.allDeclarations
119+
`${validationResult}${EOL}${JSON.stringify(
120+
dependencyVersionUsage.allDeclarations,
121+
null,
122+
2
122123
)}`
123124
);
124125
}
@@ -127,7 +128,7 @@ export class DependenciesValidator {
127128
const allLinkedVersions: Set<string> = new Set();
128129
for (const dependencyName of linkedDependencySpec) {
129130
const dependencyVersionUsage = dependencyVersionsUsages[dependencyName];
130-
dependencyVersionUsage.allVersions.forEach((version) =>
131+
dependencyVersionUsage.allDeclarations.forEach(({ version }) =>
131132
allLinkedVersions.add(version)
132133
);
133134
}
@@ -238,4 +239,70 @@ export class DependenciesValidator {
238239

239240
return dependencies;
240241
}
242+
243+
private getPackageVersionDeclarationPredicate = async (
244+
packageName: string
245+
): Promise<DependencyVersionPredicate> => {
246+
if (packageName === 'execa') {
247+
// @aws-amplify/plugin-types can depend on execa@^5.1.1 as a workaround for https://github.com/aws-amplify/amplify-backend/issues/962
248+
// all other packages must depend on execa@^8.0.1
249+
// this can be removed once execa is patched
250+
return (declarations) => {
251+
const validationResult = declarations.every(
252+
({ dependentPackageName, version }) =>
253+
(dependentPackageName === '@aws-amplify/plugin-types' &&
254+
version === '^5.1.1') ||
255+
version === '^8.0.1'
256+
);
257+
return (
258+
validationResult ||
259+
`${packageName} dependency declarations must depend on version ^8.0.1 except in @aws-amplify/plugin-types where it must depend on ^5.1.1.`
260+
);
261+
};
262+
} else if ((await this.getRepoPackageNames()).includes(packageName)) {
263+
// repo packages only need consistent major versions
264+
return (declarations) => {
265+
if (declarations.length === 0) {
266+
return true;
267+
}
268+
const baselineMajorVersion = declarations
269+
.at(0)!
270+
.version.split('.')
271+
.at(0)!;
272+
const validationResult = declarations.every(({ version }) =>
273+
version.startsWith(baselineMajorVersion)
274+
);
275+
return (
276+
validationResult ||
277+
`${packageName} dependency declarations must all be on the same major version`
278+
);
279+
};
280+
}
281+
// default behavior for all other packages is that they must be on the same version
282+
return (declarations) => {
283+
const versionSet = new Set(declarations.map(({ version }) => version));
284+
return (
285+
versionSet.size === 1 ||
286+
`${packageName} dependency declarations must all the on the same semver range`
287+
);
288+
};
289+
};
290+
291+
private getRepoPackageJsons = async () => {
292+
if (!this.repoPackageJsons) {
293+
this.repoPackageJsons = await Promise.all(
294+
this.packagePaths.map((packagePath) => readPackageJson(packagePath))
295+
);
296+
}
297+
return this.repoPackageJsons;
298+
};
299+
300+
private getRepoPackageNames = async () => {
301+
if (!this.repoPackageNames) {
302+
this.repoPackageNames = (await this.getRepoPackageJsons()).map(
303+
(packageJson) => packageJson.name
304+
);
305+
}
306+
return this.repoPackageNames;
307+
};
241308
}
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+
"package3": "^1.0.0"
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "package2",
3+
"dependencies": {
4+
"package3": "^2.0.0"
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "package3",
3+
"dependencies": {
4+
"glob": "^10.3.10"
5+
}
6+
}

0 commit comments

Comments
 (0)