Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compel users to release new versions of dependencies alongside their dependents #102

Merged
merged 13 commits into from
Oct 11, 2023
Merged
110 changes: 110 additions & 0 deletions src/package-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ describe('package-manifest', () => {
version: new SemVer('1.2.3'),
workspaces: [],
private: false,
dependencies: {},
peerDependencies: {},
};
await fs.promises.writeFile(manifestPath, JSON.stringify(unvalidated));

Expand All @@ -41,6 +43,68 @@ describe('package-manifest', () => {
version: new SemVer('1.2.3'),
workspaces: [],
private: true,
dependencies: {},
peerDependencies: {},
};
await fs.promises.writeFile(manifestPath, JSON.stringify(unvalidated));

expect(await readPackageManifest(manifestPath)).toStrictEqual({
unvalidated,
validated,
});
});
});

it('reads a package manifest where "dependencies" has valid values', async () => {
await withSandbox(async (sandbox) => {
const manifestPath = path.join(sandbox.directoryPath, 'package.json');
const unvalidated = {
name: 'foo',
version: '1.2.3',
private: true,
dependencies: {
a: '1.0.0',
},
};
const validated = {
name: 'foo',
version: new SemVer('1.2.3'),
workspaces: [],
private: true,
dependencies: {
a: '1.0.0',
},
peerDependencies: {},
};
await fs.promises.writeFile(manifestPath, JSON.stringify(unvalidated));

expect(await readPackageManifest(manifestPath)).toStrictEqual({
unvalidated,
validated,
});
});
});

it('reads a package manifest where "peerDependencies" has valid values', async () => {
await withSandbox(async (sandbox) => {
const manifestPath = path.join(sandbox.directoryPath, 'package.json');
const unvalidated = {
name: 'foo',
version: '1.2.3',
private: true,
peerDependencies: {
a: '1.0.0',
},
};
const validated = {
name: 'foo',
version: new SemVer('1.2.3'),
workspaces: [],
private: true,
dependencies: {},
peerDependencies: {
a: '1.0.0',
},
};
await fs.promises.writeFile(manifestPath, JSON.stringify(unvalidated));

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

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

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

Expand Down Expand Up @@ -204,6 +274,46 @@ describe('package-manifest', () => {
});
});

it('throws if any of the "dependencies" has a non SemVer-compatible version string', async () => {
await withSandbox(async (sandbox) => {
const manifestPath = path.join(sandbox.directoryPath, 'package.json');
await fs.promises.writeFile(
manifestPath,
JSON.stringify({
name: 'foo',
version: '1.0.0',
dependencies: {
a: 12345,
},
}),
);

await expect(readPackageManifest(manifestPath)).rejects.toThrow(
'The value of "dependencies" in the manifest for "foo" must be a valid dependencies field',
);
});
});

it('throws if any of the "peerDependencies" has a non SemVer-compatible version string', async () => {
await withSandbox(async (sandbox) => {
const manifestPath = path.join(sandbox.directoryPath, 'package.json');
await fs.promises.writeFile(
manifestPath,
JSON.stringify({
name: 'foo',
version: '1.0.0',
peerDependencies: {
a: 12345,
},
}),
);

await expect(readPackageManifest(manifestPath)).rejects.toThrow(
'The value of "peerDependencies" in the manifest for "foo" must be a valid peerDependencies field',
);
});
});

it('throws if "workspaces" is not an array of strings', async () => {
await withSandbox(async (sandbox) => {
const manifestPath = path.join(sandbox.directoryPath, 'package.json');
Expand Down
81 changes: 81 additions & 0 deletions src/package-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ManifestFieldNames as PackageManifestFieldNames,
ManifestDependencyFieldNames as PackageManifestDependenciesFieldNames,
} from '@metamask/action-utils';
import { isPlainObject } from '@metamask/utils';
import { readJsonObjectFile } from './fs';
import { isTruthyString } from './misc-utils';
import { isValidSemver, SemVer } from './semver';
Expand All @@ -29,6 +30,11 @@ export type ValidatedPackageManifest = {
readonly [PackageManifestFieldNames.Version]: SemVer;
readonly [PackageManifestFieldNames.Private]: boolean;
readonly [PackageManifestFieldNames.Workspaces]: string[];
readonly [PackageManifestDependenciesFieldNames.Production]: Record<
string,
string
>;
readonly [PackageManifestDependenciesFieldNames.Peer]: Record<string, string>;
};

/**
Expand Down Expand Up @@ -83,6 +89,14 @@ const schemata = {
validate: isValidPackageManifestPrivateField,
errorMessage: 'must be true or false (if present)',
},
[PackageManifestDependenciesFieldNames.Production]: {
validate: isValidPackageManifestDependenciesField,
errorMessage: 'must be a valid dependencies field',
},
[PackageManifestDependenciesFieldNames.Peer]: {
validate: isValidPackageManifestDependenciesField,
errorMessage: 'must be a valid peerDependencies field',
},
};

/**
Expand Down Expand Up @@ -256,6 +270,61 @@ export function readPackageManifestPrivateField(
return value ?? false;
}

/**
* Type guard to ensure that the value of the "dependencies" or "peerDependencies" field of a manifest is
* valid.
*
* @param depsValue - The value to check.
* @returns Whether the value is has valid values.
*/
function isValidPackageManifestDependenciesField(
depsValue: unknown,
): depsValue is Record<string, string> {
return (
depsValue === undefined ||
(isPlainObject(depsValue) &&
Object.entries(depsValue).every(([pkgName, version]) => {
return (
isTruthyString(pkgName) && isValidPackageManifestVersionField(version)
);
}))
);
}

/**
* Retrieves and validates the "dependencies" or "peerDependencies" fields within the package manifest
* object.
*
* @param manifest - The manifest object.
* @param parentDirectory - The directory in which the manifest lives.
* @param fieldName - The field name "dependencies" or "peerDependencies".
* @returns The value of the "dependencies" or "peerDependencies" field.
* @throws If the value of the field is not valid.
*/
export function readPackageManifestDependenciesField(
manifest: UnvalidatedPackageManifest,
parentDirectory: string,
fieldName:
| PackageManifestDependenciesFieldNames.Production
| PackageManifestDependenciesFieldNames.Peer,
): Record<string, string> {
const value = manifest[fieldName];
const schema = schemata[fieldName];

if (!schema.validate(value)) {
throw new Error(
buildPackageManifestFieldValidationErrorMessage({
manifest,
parentDirectory,
fieldName,
verbPhrase: schema.errorMessage,
}),
);
}

return value || {};
}

/**
* Reads the package manifest at the given path, verifying key data within the
* manifest.
Expand All @@ -281,12 +350,24 @@ export async function readPackageManifest(manifestPath: string): Promise<{
unvalidated,
parentDirectory,
);
const dependencies = readPackageManifestDependenciesField(
unvalidated,
parentDirectory,
PackageManifestDependenciesFieldNames.Production,
);
const peerDependencies = readPackageManifestDependenciesField(
unvalidated,
parentDirectory,
PackageManifestDependenciesFieldNames.Peer,
);

const validated = {
[PackageManifestFieldNames.Name]: name,
[PackageManifestFieldNames.Version]: version,
[PackageManifestFieldNames.Workspaces]: workspaces,
[PackageManifestFieldNames.Private]: privateValue,
[PackageManifestDependenciesFieldNames.Production]: dependencies,
[PackageManifestDependenciesFieldNames.Peer]: peerDependencies,
};

return { unvalidated, validated };
Expand Down
Loading
Loading