Skip to content

Commit 3556dee

Browse files
authored
Compel users to release new versions of dependencies alongside their dependents (#102)
* Compel users to release new versions of dependencies alongside their dependents * Fix missingDependencies filter * Amend test description * Add more unit tests * Refactor unit test thrown errors * Add new test case * Add deps to validateManifest * Use existing package manifest deps types * Fix package manifest unit tests * Add more test cases to handle version specifier null or intentionally-skip * Fix remove useless deps check * fix deps validation
1 parent ec1d91f commit 3556dee

File tree

5 files changed

+674
-17
lines changed

5 files changed

+674
-17
lines changed

src/package-manifest.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ describe('package-manifest', () => {
1818
version: new SemVer('1.2.3'),
1919
workspaces: [],
2020
private: false,
21+
dependencies: {},
22+
peerDependencies: {},
2123
};
2224
await fs.promises.writeFile(manifestPath, JSON.stringify(unvalidated));
2325

@@ -41,6 +43,68 @@ describe('package-manifest', () => {
4143
version: new SemVer('1.2.3'),
4244
workspaces: [],
4345
private: true,
46+
dependencies: {},
47+
peerDependencies: {},
48+
};
49+
await fs.promises.writeFile(manifestPath, JSON.stringify(unvalidated));
50+
51+
expect(await readPackageManifest(manifestPath)).toStrictEqual({
52+
unvalidated,
53+
validated,
54+
});
55+
});
56+
});
57+
58+
it('reads a package manifest where "dependencies" has valid values', async () => {
59+
await withSandbox(async (sandbox) => {
60+
const manifestPath = path.join(sandbox.directoryPath, 'package.json');
61+
const unvalidated = {
62+
name: 'foo',
63+
version: '1.2.3',
64+
private: true,
65+
dependencies: {
66+
a: '1.0.0',
67+
},
68+
};
69+
const validated = {
70+
name: 'foo',
71+
version: new SemVer('1.2.3'),
72+
workspaces: [],
73+
private: true,
74+
dependencies: {
75+
a: '1.0.0',
76+
},
77+
peerDependencies: {},
78+
};
79+
await fs.promises.writeFile(manifestPath, JSON.stringify(unvalidated));
80+
81+
expect(await readPackageManifest(manifestPath)).toStrictEqual({
82+
unvalidated,
83+
validated,
84+
});
85+
});
86+
});
87+
88+
it('reads a package manifest where "peerDependencies" has valid values', async () => {
89+
await withSandbox(async (sandbox) => {
90+
const manifestPath = path.join(sandbox.directoryPath, 'package.json');
91+
const unvalidated = {
92+
name: 'foo',
93+
version: '1.2.3',
94+
private: true,
95+
peerDependencies: {
96+
a: '1.0.0',
97+
},
98+
};
99+
const validated = {
100+
name: 'foo',
101+
version: new SemVer('1.2.3'),
102+
workspaces: [],
103+
private: true,
104+
dependencies: {},
105+
peerDependencies: {
106+
a: '1.0.0',
107+
},
44108
};
45109
await fs.promises.writeFile(manifestPath, JSON.stringify(unvalidated));
46110

@@ -64,6 +128,8 @@ describe('package-manifest', () => {
64128
version: new SemVer('1.2.3'),
65129
workspaces: [],
66130
private: false,
131+
dependencies: {},
132+
peerDependencies: {},
67133
};
68134
await fs.promises.writeFile(manifestPath, JSON.stringify(unvalidated));
69135

@@ -88,6 +154,8 @@ describe('package-manifest', () => {
88154
version: new SemVer('1.2.3'),
89155
workspaces: ['packages/*'],
90156
private: true,
157+
dependencies: {},
158+
peerDependencies: {},
91159
};
92160
await fs.promises.writeFile(manifestPath, JSON.stringify(unvalidated));
93161

@@ -111,6 +179,8 @@ describe('package-manifest', () => {
111179
version: new SemVer('1.2.3'),
112180
workspaces: [],
113181
private: false,
182+
dependencies: {},
183+
peerDependencies: {},
114184
};
115185
await fs.promises.writeFile(manifestPath, JSON.stringify(unvalidated));
116186

@@ -204,6 +274,46 @@ describe('package-manifest', () => {
204274
});
205275
});
206276

277+
it('throws if any of the "dependencies" has a non SemVer-compatible version string', async () => {
278+
await withSandbox(async (sandbox) => {
279+
const manifestPath = path.join(sandbox.directoryPath, 'package.json');
280+
await fs.promises.writeFile(
281+
manifestPath,
282+
JSON.stringify({
283+
name: 'foo',
284+
version: '1.0.0',
285+
dependencies: {
286+
a: 12345,
287+
},
288+
}),
289+
);
290+
291+
await expect(readPackageManifest(manifestPath)).rejects.toThrow(
292+
'The value of "dependencies" in the manifest for "foo" must be a valid dependencies field',
293+
);
294+
});
295+
});
296+
297+
it('throws if any of the "peerDependencies" has a non SemVer-compatible version string', async () => {
298+
await withSandbox(async (sandbox) => {
299+
const manifestPath = path.join(sandbox.directoryPath, 'package.json');
300+
await fs.promises.writeFile(
301+
manifestPath,
302+
JSON.stringify({
303+
name: 'foo',
304+
version: '1.0.0',
305+
peerDependencies: {
306+
a: 12345,
307+
},
308+
}),
309+
);
310+
311+
await expect(readPackageManifest(manifestPath)).rejects.toThrow(
312+
'The value of "peerDependencies" in the manifest for "foo" must be a valid peerDependencies field',
313+
);
314+
});
315+
});
316+
207317
it('throws if "workspaces" is not an array of strings', async () => {
208318
await withSandbox(async (sandbox) => {
209319
const manifestPath = path.join(sandbox.directoryPath, 'package.json');

src/package-manifest.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
ManifestFieldNames as PackageManifestFieldNames,
44
ManifestDependencyFieldNames as PackageManifestDependenciesFieldNames,
55
} from '@metamask/action-utils';
6+
import { isPlainObject } from '@metamask/utils';
67
import { readJsonObjectFile } from './fs';
78
import { isTruthyString } from './misc-utils';
89
import { isValidSemver, SemVer } from './semver';
@@ -29,6 +30,11 @@ export type ValidatedPackageManifest = {
2930
readonly [PackageManifestFieldNames.Version]: SemVer;
3031
readonly [PackageManifestFieldNames.Private]: boolean;
3132
readonly [PackageManifestFieldNames.Workspaces]: string[];
33+
readonly [PackageManifestDependenciesFieldNames.Production]: Record<
34+
string,
35+
string
36+
>;
37+
readonly [PackageManifestDependenciesFieldNames.Peer]: Record<string, string>;
3238
};
3339

3440
/**
@@ -83,6 +89,14 @@ const schemata = {
8389
validate: isValidPackageManifestPrivateField,
8490
errorMessage: 'must be true or false (if present)',
8591
},
92+
[PackageManifestDependenciesFieldNames.Production]: {
93+
validate: isValidPackageManifestDependenciesField,
94+
errorMessage: 'must be a valid dependencies field',
95+
},
96+
[PackageManifestDependenciesFieldNames.Peer]: {
97+
validate: isValidPackageManifestDependenciesField,
98+
errorMessage: 'must be a valid peerDependencies field',
99+
},
86100
};
87101

88102
/**
@@ -256,6 +270,61 @@ export function readPackageManifestPrivateField(
256270
return value ?? false;
257271
}
258272

273+
/**
274+
* Type guard to ensure that the value of the "dependencies" or "peerDependencies" field of a manifest is
275+
* valid.
276+
*
277+
* @param depsValue - The value to check.
278+
* @returns Whether the value is has valid values.
279+
*/
280+
function isValidPackageManifestDependenciesField(
281+
depsValue: unknown,
282+
): depsValue is Record<string, string> {
283+
return (
284+
depsValue === undefined ||
285+
(isPlainObject(depsValue) &&
286+
Object.entries(depsValue).every(([pkgName, version]) => {
287+
return (
288+
isTruthyString(pkgName) && isValidPackageManifestVersionField(version)
289+
);
290+
}))
291+
);
292+
}
293+
294+
/**
295+
* Retrieves and validates the "dependencies" or "peerDependencies" fields within the package manifest
296+
* object.
297+
*
298+
* @param manifest - The manifest object.
299+
* @param parentDirectory - The directory in which the manifest lives.
300+
* @param fieldName - The field name "dependencies" or "peerDependencies".
301+
* @returns The value of the "dependencies" or "peerDependencies" field.
302+
* @throws If the value of the field is not valid.
303+
*/
304+
export function readPackageManifestDependenciesField(
305+
manifest: UnvalidatedPackageManifest,
306+
parentDirectory: string,
307+
fieldName:
308+
| PackageManifestDependenciesFieldNames.Production
309+
| PackageManifestDependenciesFieldNames.Peer,
310+
): Record<string, string> {
311+
const value = manifest[fieldName];
312+
const schema = schemata[fieldName];
313+
314+
if (!schema.validate(value)) {
315+
throw new Error(
316+
buildPackageManifestFieldValidationErrorMessage({
317+
manifest,
318+
parentDirectory,
319+
fieldName,
320+
verbPhrase: schema.errorMessage,
321+
}),
322+
);
323+
}
324+
325+
return value || {};
326+
}
327+
259328
/**
260329
* Reads the package manifest at the given path, verifying key data within the
261330
* manifest.
@@ -281,12 +350,24 @@ export async function readPackageManifest(manifestPath: string): Promise<{
281350
unvalidated,
282351
parentDirectory,
283352
);
353+
const dependencies = readPackageManifestDependenciesField(
354+
unvalidated,
355+
parentDirectory,
356+
PackageManifestDependenciesFieldNames.Production,
357+
);
358+
const peerDependencies = readPackageManifestDependenciesField(
359+
unvalidated,
360+
parentDirectory,
361+
PackageManifestDependenciesFieldNames.Peer,
362+
);
284363

285364
const validated = {
286365
[PackageManifestFieldNames.Name]: name,
287366
[PackageManifestFieldNames.Version]: version,
288367
[PackageManifestFieldNames.Workspaces]: workspaces,
289368
[PackageManifestFieldNames.Private]: privateValue,
369+
[PackageManifestDependenciesFieldNames.Production]: dependencies,
370+
[PackageManifestDependenciesFieldNames.Peer]: peerDependencies,
290371
};
291372

292373
return { unvalidated, validated };

0 commit comments

Comments
 (0)