Skip to content
This repository has been archived by the owner on May 21, 2021. It is now read-only.

Commit

Permalink
Fix package manifest typing
Browse files Browse the repository at this point in the history
  • Loading branch information
rekmarks committed May 12, 2021
1 parent af8d750 commit aceedd7
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 95 deletions.
66 changes: 35 additions & 31 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7437,16 +7437,17 @@ function validateActionInputs(inputs) {
* Reads the assumed JSON file at the given path, attempts to parse it, and
* returns the resulting object.
*
* Throws if failing to read or parse, or if the parsed JSON value is falsy.
* Throws if failing to read or parse, or if the parsed JSON value is not a
* plain object.
*
* @param paths - The path segments pointing to the JSON file. Will be passed
* to path.join().
* @returns The object corresponding to the parsed JSON file.
*/
async function readJsonFile(path) {
async function readJsonObjectFile(path) {
const obj = JSON.parse(await external_fs_.promises.readFile(path, 'utf8'));
if (!obj) {
throw new Error(`Assumed JSON file at path "${path}" parsed to a falsy value.`);
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
throw new Error(`Assumed JSON file at path "${path}" parsed to a non-object value.`);
}
return obj;
}
Expand Down Expand Up @@ -7721,7 +7722,10 @@ async function getMetadataForAllPackages(rootDir = WORKSPACE_ROOT, packagesDir =
await Promise.all(packagesDirContents.map(async (packageDir) => {
const packagePath = external_path_default().join(packagesPath, packageDir);
if ((await external_fs_.promises.lstat(packagePath)).isDirectory()) {
const manifest = await getPackageManifest(packagePath);
const manifest = await getPackageManifest(packagePath, [
FieldNames.Name,
FieldNames.Version,
]);
result[manifest.name] = {
dirName: packageDir,
manifest,
Expand Down Expand Up @@ -7900,27 +7904,23 @@ function getUpdatedDependencyField(dependencyObject, packagesToUpdate, newVersio
* @returns The object corresponding to the parsed package.json file.
*/
async function getPackageManifest(containingDirPath, fieldsToValidate) {
const manifest = await readJsonFile(external_path_default().join(containingDirPath, PACKAGE_JSON));
validatePackageManifest(manifest, containingDirPath, fieldsToValidate);
return manifest;
const manifest = await readJsonObjectFile(external_path_default().join(containingDirPath, PACKAGE_JSON));
return validatePackageManifest(containingDirPath, manifest, fieldsToValidate);
}
/**
* Validates a manifest by ensuring that the given fields are properly formatted
* if present. Fields that are required by the `PackageManifest` interface must be
* present if specified.
* Validates a manifest by ensuring that the given fields are present and
* properly formatted.
*
* @see PackageManifest - For fields that must be present if specified.
* @param manifest - The manifest to validate.
* @param manifestDirPath - The path to the directory containing the
* package.json file.
* manifest file.
* @param manifest - The manifest to validate.
* @param fieldsToValidate - The manifest fields that will be validated.
* @returns The unmodified manifest, with validated fields typed correctly.
*/
function validatePackageManifest(manifest, manifestDirPath, fieldsToValidate = [
FieldNames.Name,
FieldNames.Version,
]) {
function validatePackageManifest(manifestDirPath, manifest, fieldsToValidate) {
if (fieldsToValidate.length === 0) {
return;
return manifest;
}
const _fieldsToValidate = new Set(fieldsToValidate);
// Just for logging purposes
Expand All @@ -7939,17 +7939,20 @@ function validatePackageManifest(manifest, manifestDirPath, fieldsToValidate = [
throw new Error(`${getErrorMessagePrefix(FieldNames.Version)} is not a valid SemVer version: ${manifest[FieldNames.Version]}`);
}
if (_fieldsToValidate.has(FieldNames.Private) &&
FieldNames.Private in manifest &&
typeof manifest[FieldNames.Private] !== 'boolean') {
throw new Error(`${getErrorMessagePrefix(FieldNames.Private)} must be a boolean if present. Received: ${manifest[FieldNames.Private]}`);
}
if (_fieldsToValidate.has(FieldNames.Workspaces) &&
FieldNames.Workspaces in manifest &&
(!Array.isArray(manifest[FieldNames.Workspaces]) ||
if (_fieldsToValidate.has(FieldNames.Workspaces)) {
if (!Array.isArray(manifest[FieldNames.Workspaces]) ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
manifest[FieldNames.Workspaces].length === 0)) {
throw new Error(`${getErrorMessagePrefix(FieldNames.Workspaces)} must be a non-empty array if present. Received: ${manifest[FieldNames.Workspaces]}`);
manifest[FieldNames.Workspaces].length === 0) {
throw new Error(`${getErrorMessagePrefix(FieldNames.Workspaces)} must be a non-empty array if present. Received: ${manifest[FieldNames.Workspaces]}`);
}
if (manifest[FieldNames.Private] !== true) {
throw new Error(`${getErrorMessagePrefix(FieldNames.Private)} must be "true" if "${FieldNames.Workspaces}" is present. Received: ${manifest[FieldNames.Private]}`);
}
}
return manifest;
}
/**
* Type guard for checking if an update specification is a monorepo update
Expand Down Expand Up @@ -8000,11 +8003,8 @@ async function performUpdate(actionInputs) {
const [tags] = await getTags();
const rootManifest = await getPackageManifest(WORKSPACE_ROOT, [
FieldNames.Version,
FieldNames.Private,
FieldNames.Workspaces,
]);
const { version: currentVersion } = rootManifest;
const isMonorepo = rootManifest.private === true && rootManifest.workspaces;
// Compute the new version and version diff from the inputs and root manifest
let newVersion, versionDiff;
if (actionInputs.ReleaseType) {
Expand All @@ -8015,12 +8015,16 @@ async function performUpdate(actionInputs) {
newVersion = actionInputs.ReleaseVersion;
versionDiff = diff_default()(currentVersion, newVersion);
}
if (isMonorepo) {
await updateMonorepo(newVersion, versionDiff, rootManifest, repositoryUrl, tags);
if (FieldNames.Workspaces in rootManifest) {
console.log('Project appears to have workspaces. Applying monorepo workflow.');
await updateMonorepo(newVersion, versionDiff, validatePackageManifest(WORKSPACE_ROOT, rootManifest, [
FieldNames.Private,
FieldNames.Workspaces,
]), repositoryUrl, tags);
}
else {
validatePackageManifest(rootManifest, WORKSPACE_ROOT, [FieldNames.Name]);
await updatePolyrepo(newVersion, rootManifest, repositoryUrl);
console.log('Project does not appear to have any workspaces. Applying polyrepo workflow.');
await updatePolyrepo(newVersion, validatePackageManifest(WORKSPACE_ROOT, rootManifest, [FieldNames.Name]), repositoryUrl);
}
(0,core.setOutput)('NEW_VERSION', newVersion);
}
Expand Down
42 changes: 34 additions & 8 deletions src/package-operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jest.mock('./utils', () => {
const actualModule = jest.requireActual('./utils');
return {
...actualModule,
readJsonFile: jest.fn(),
readJsonObjectFile: jest.fn(),
WORKSPACE_ROOT: 'root',
};
});
Expand Down Expand Up @@ -68,7 +68,7 @@ describe('package-operations', () => {
let readJsonFileMock: jest.SpyInstance;

beforeEach(() => {
readJsonFileMock = jest.spyOn(utils, 'readJsonFile');
readJsonFileMock = jest.spyOn(utils, 'readJsonObjectFile');
});

it('gets and returns a valid manifest', async () => {
Expand All @@ -78,13 +78,18 @@ describe('package-operations', () => {
return { ...validManifest };
});

expect(await getPackageManifest('fooPath')).toStrictEqual(validManifest);
expect(
await getPackageManifest('fooPath', [
FieldNames.Name,
FieldNames.Version,
]),
).toStrictEqual(validManifest);
});

it('gets and returns a valid manifest, with different fields specified', async () => {
const validManifest: Readonly<Record<string, unknown>> = {
name: 'fooName',
private: false,
private: true,
version: '1.0.0',
workspaces: ['bar'],
};
Expand Down Expand Up @@ -130,8 +135,12 @@ describe('package-operations', () => {
return { version: '1.0.0' };
});

await expect(getPackageManifest('fooPath')).rejects.toThrow(/"name"/u);
await expect(getPackageManifest('fooPath')).rejects.toThrow(/"name"/u);
await expect(
getPackageManifest('fooPath', [FieldNames.Name, FieldNames.Version]),
).rejects.toThrow(/"name"/u);
await expect(
getPackageManifest('fooPath', [FieldNames.Name, FieldNames.Version]),
).rejects.toThrow(/"name"/u);
await expect(
getPackageManifest('fooPath', [FieldNames.Name]),
).rejects.toThrow(/"name"/u);
Expand All @@ -147,7 +156,9 @@ describe('package-operations', () => {
return { version: 'badVersion' };
});

await expect(getPackageManifest('fooPath')).rejects.toThrow(/"version"/u);
await expect(
getPackageManifest('fooPath', [FieldNames.Name, FieldNames.Version]),
).rejects.toThrow(/"version"/u);
await expect(
getPackageManifest('fooPath', [FieldNames.Version]),
).rejects.toThrow(/"version"/u);
Expand Down Expand Up @@ -177,6 +188,21 @@ describe('package-operations', () => {
await expect(
getPackageManifest('fooPath', [FieldNames.Workspaces]),
).rejects.toThrow(/"workspaces"/u);

readJsonFileMock
.mockImplementationOnce(async () => {
return { workspaces: ['a'] };
})
.mockImplementationOnce(async () => {
return { workspaces: ['a'], private: false };
});

await expect(
getPackageManifest('fooPath', [FieldNames.Workspaces]),
).rejects.toThrow(/"private" .* must be "true" .*"workspaces"/u);
await expect(
getPackageManifest('fooPath', [FieldNames.Workspaces]),
).rejects.toThrow(/"private" .* must be "true" .*"workspaces"/u);
});
});

Expand Down Expand Up @@ -217,7 +243,7 @@ describe('package-operations', () => {
}) as any);

jest
.spyOn(utils, 'readJsonFile')
.spyOn(utils, 'readJsonObjectFile')
.mockImplementation(getMockReadJsonFile());
});

Expand Down
89 changes: 53 additions & 36 deletions src/package-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { didPackageChange } from './git-operations';
import {
isTruthyString,
isValidSemver,
readJsonFile,
readJsonObjectFile,
WORKSPACE_ROOT,
writeJsonFile,
} from './utils';
Expand Down Expand Up @@ -36,6 +36,11 @@ export interface PackageManifest
readonly [FieldNames.Workspaces]?: string[];
}

export interface ValidatedPackageManifest extends PackageManifest {
readonly [FieldNames.Private]: boolean;
readonly [FieldNames.Workspaces]: string[];
}

export interface PackageMetadata {
readonly dirName: string;
readonly manifest: PackageManifest;
Expand Down Expand Up @@ -76,7 +81,10 @@ export async function getMetadataForAllPackages(
const packagePath = pathUtils.join(packagesPath, packageDir);

if ((await fs.lstat(packagePath)).isDirectory()) {
const manifest = await getPackageManifest(packagePath);
const manifest = await getPackageManifest(packagePath, [
FieldNames.Name,
FieldNames.Version,
]);
result[manifest.name] = {
dirName: packageDir,
manifest,
Expand Down Expand Up @@ -313,41 +321,42 @@ function getUpdatedDependencyField(
* @param fieldsToValidate - The manifest fields that will be validated.
* @returns The object corresponding to the parsed package.json file.
*/
export async function getPackageManifest<T extends keyof PackageManifest>(
export async function getPackageManifest<T extends FieldNames>(
containingDirPath: string,
fieldsToValidate?: T[],
): Promise<Pick<PackageManifest, T>> {
const manifest = await readJsonFile(
fieldsToValidate: T[],
): Promise<Pick<PackageManifest, T> & Partial<PackageManifest>> {
const manifest: Partial<PackageManifest> = await readJsonObjectFile(
pathUtils.join(containingDirPath, PACKAGE_JSON),
);

validatePackageManifest(manifest, containingDirPath, fieldsToValidate);
return manifest as Pick<PackageManifest, T>;
return validatePackageManifest(containingDirPath, manifest, fieldsToValidate);
}

/**
* Validates a manifest by ensuring that the given fields are properly formatted
* if present. Fields that are required by the `PackageManifest` interface must be
* present if specified.
* Validates a manifest by ensuring that the given fields are present and
* properly formatted.
*
* @see PackageManifest - For fields that must be present if specified.
* @param manifest - The manifest to validate.
* @param manifestDirPath - The path to the directory containing the
* package.json file.
* manifest file.
* @param manifest - The manifest to validate.
* @param fieldsToValidate - The manifest fields that will be validated.
* @returns The unmodified manifest, with validated fields typed correctly.
*/
export function validatePackageManifest(
manifest: Partial<PackageManifest>,
export function validatePackageManifest<
T extends Partial<PackageManifest>,
U extends FieldNames
>(
manifestDirPath: string,
fieldsToValidate: (keyof PackageManifest)[] = [
FieldNames.Name,
FieldNames.Version,
],
): void {
manifest: T,
fieldsToValidate: U[],
): T & Pick<ValidatedPackageManifest, U> {
if (fieldsToValidate.length === 0) {
return;
return manifest as T & Pick<ValidatedPackageManifest, U>;
}
const _fieldsToValidate = new Set(fieldsToValidate);
const _fieldsToValidate: Set<keyof PackageManifest> = new Set(
fieldsToValidate,
);

// Just for logging purposes
const legiblePath = getTruncatedPath(manifestDirPath);
Expand Down Expand Up @@ -381,7 +390,6 @@ export function validatePackageManifest(

if (
_fieldsToValidate.has(FieldNames.Private) &&
FieldNames.Private in manifest &&
typeof manifest[FieldNames.Private] !== 'boolean'
) {
throw new Error(
Expand All @@ -393,21 +401,30 @@ export function validatePackageManifest(
);
}

if (
_fieldsToValidate.has(FieldNames.Workspaces) &&
FieldNames.Workspaces in manifest &&
(!Array.isArray(manifest[FieldNames.Workspaces]) ||
if (_fieldsToValidate.has(FieldNames.Workspaces)) {
if (
!Array.isArray(manifest[FieldNames.Workspaces]) ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
manifest[FieldNames.Workspaces]!.length === 0)
) {
throw new Error(
`${getErrorMessagePrefix(
FieldNames.Workspaces,
)} must be a non-empty array if present. Received: ${
manifest[FieldNames.Workspaces]
}`,
);
manifest[FieldNames.Workspaces]!.length === 0
) {
throw new Error(
`${getErrorMessagePrefix(
FieldNames.Workspaces,
)} must be a non-empty array if present. Received: ${
manifest[FieldNames.Workspaces]
}`,
);
}

if (manifest[FieldNames.Private] !== true) {
throw new Error(
`${getErrorMessagePrefix(FieldNames.Private)} must be "true" if "${
FieldNames.Workspaces
}" is present. Received: ${manifest[FieldNames.Private]}`,
);
}
}
return manifest as T & Pick<ValidatedPackageManifest, U>;
}

/**
Expand Down
Loading

0 comments on commit aceedd7

Please sign in to comment.