diff options
Diffstat (limited to 'packages/monorepo-scripts/src')
-rw-r--r-- | packages/monorepo-scripts/src/prepublish_checks.ts | 121 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/publish.ts | 46 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/remove_tags.ts | 57 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/types.ts | 13 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/utils/changelog_utils.ts | 53 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/utils/npm_utils.ts | 28 | ||||
-rw-r--r-- | packages/monorepo-scripts/src/utils/utils.ts | 110 |
7 files changed, 317 insertions, 111 deletions
diff --git a/packages/monorepo-scripts/src/prepublish_checks.ts b/packages/monorepo-scripts/src/prepublish_checks.ts index 2c096d8f6..1c4ee1fc6 100644 --- a/packages/monorepo-scripts/src/prepublish_checks.ts +++ b/packages/monorepo-scripts/src/prepublish_checks.ts @@ -1,9 +1,128 @@ import * as _ from 'lodash'; import { exec as execAsync } from 'promisify-child-process'; +import semver = require('semver'); +import semverSort = require('semver-sort'); import { constants } from './constants'; +import { changelogUtils } from './utils/changelog_utils'; +import { npmUtils } from './utils/npm_utils'; import { utils } from './utils/utils'; +async function prepublishChecksAsync(): Promise<void> { + const shouldIncludePrivate = false; + const updatedPublicLernaPackages = await utils.getUpdatedLernaPackagesAsync(shouldIncludePrivate); + + await checkCurrentVersionMatchesLatestPublishedNPMPackageAsync(updatedPublicLernaPackages); + await checkChangelogFormatAsync(updatedPublicLernaPackages); + await checkGitTagsForNextVersionAndDeleteIfExistAsync(updatedPublicLernaPackages); + await checkPublishRequiredSetupAsync(); +} + +async function checkGitTagsForNextVersionAndDeleteIfExistAsync( + updatedPublicLernaPackages: LernaPackage[], +): Promise<void> { + const packageNames = _.map(updatedPublicLernaPackages, lernaPackage => lernaPackage.package.name); + const localGitTags = await utils.getLocalGitTagsAsync(); + const localTagVersionsByPackageName = await utils.getGitTagsByPackageNameAsync(packageNames, localGitTags); + + const remoteGitTags = await utils.getRemoteGitTagsAsync(); + const remoteTagVersionsByPackageName = await utils.getGitTagsByPackageNameAsync(packageNames, remoteGitTags); + + for (const lernaPackage of updatedPublicLernaPackages) { + const currentVersion = lernaPackage.package.version; + const packageName = lernaPackage.package.name; + const packageLocation = lernaPackage.location; + const nextVersion = await utils.getNextPackageVersionAsync(currentVersion, packageName, packageLocation); + + const localTagVersions = localTagVersionsByPackageName[packageName]; + if (_.includes(localTagVersions, nextVersion)) { + const tagName = `${packageName}@${nextVersion}`; + await utils.removeLocalTagAsync(tagName); + } + + const remoteTagVersions = remoteTagVersionsByPackageName[packageName]; + if (_.includes(remoteTagVersions, nextVersion)) { + const tagName = `:refs/tags/${packageName}@${nextVersion}`; + await utils.removeRemoteTagAsync(tagName); + } + } +} + +async function checkCurrentVersionMatchesLatestPublishedNPMPackageAsync( + updatedPublicLernaPackages: LernaPackage[], +): Promise<void> { + utils.log('Check package versions against npmjs.org...'); + const versionMismatches = []; + for (const lernaPackage of updatedPublicLernaPackages) { + const packageName = lernaPackage.package.name; + const packageVersion = lernaPackage.package.version; + const packageRegistryJsonIfExists = await npmUtils.getPackageRegistryJsonIfExistsAsync(packageName); + if (_.isUndefined(packageRegistryJsonIfExists)) { + continue; // noop for packages not yet published to NPM + } + const allVersionsIncludingUnpublished = npmUtils.getPreviouslyPublishedVersions(packageRegistryJsonIfExists); + const sortedVersions = semverSort.desc(allVersionsIncludingUnpublished); + const latestNPMVersion = sortedVersions[0]; + if (packageVersion !== latestNPMVersion) { + versionMismatches.push({ + packageJsonVersion: packageVersion, + npmVersion: latestNPMVersion, + packageName, + }); + } + } + if (!_.isEmpty(versionMismatches)) { + utils.log(`Found version mismatches between package.json and NPM published versions (might be unpublished).`); + _.each(versionMismatches, versionMismatch => { + utils.log( + `${versionMismatch.packageName}: ${versionMismatch.packageJsonVersion} package.json, ${ + versionMismatch.npmVersion + } on NPM`, + ); + }); + throw new Error(`Please fix the above package.json/NPM inconsistencies.`); + } +} + +async function checkChangelogFormatAsync(updatedPublicLernaPackages: LernaPackage[]): Promise<void> { + utils.log('Check CHANGELOGs for inconsistencies...'); + const changeLogInconsistencies = []; + for (const lernaPackage of updatedPublicLernaPackages) { + const packageName = lernaPackage.package.name; + const changelog = changelogUtils.getChangelogOrCreateIfMissing(packageName, lernaPackage.location); + + const currentVersion = lernaPackage.package.version; + if (!_.isEmpty(changelog)) { + const lastEntry = changelog[0]; + const doesLastEntryHaveTimestamp = !_.isUndefined(lastEntry.timestamp); + if (semver.lt(lastEntry.version, currentVersion)) { + changeLogInconsistencies.push({ + packageJsonVersion: currentVersion, + changelogVersion: lastEntry.version, + packageName, + }); + } else if (semver.gt(lastEntry.version, currentVersion) && doesLastEntryHaveTimestamp) { + // Remove incorrectly added timestamp + delete changelog[0].timestamp; + // Save updated CHANGELOG.json + await changelogUtils.writeChangelogJsonFileAsync(lernaPackage.location, changelog); + utils.log(`${packageName}: Removed timestamp from latest CHANGELOG.json entry.`); + } + } + } + if (!_.isEmpty(changeLogInconsistencies)) { + utils.log(`CHANGELOG versions cannot below package.json versions:`); + _.each(changeLogInconsistencies, inconsistency => { + utils.log( + `${inconsistency.packageName}: ${inconsistency.packageJsonVersion} package.json, ${ + inconsistency.changelogVersion + } CHANGELOG.json`, + ); + }); + throw new Error('Fix the above inconsistencies to continue.'); + } +} + async function checkPublishRequiredSetupAsync(): Promise<void> { // check to see if logged into npm before publishing try { @@ -65,7 +184,7 @@ async function checkPublishRequiredSetupAsync(): Promise<void> { } } -checkPublishRequiredSetupAsync().catch(err => { +prepublishChecksAsync().catch(err => { utils.log(err.message); process.exit(1); }); diff --git a/packages/monorepo-scripts/src/publish.ts b/packages/monorepo-scripts/src/publish.ts index 2efbc8bf2..72c6c0a71 100644 --- a/packages/monorepo-scripts/src/publish.ts +++ b/packages/monorepo-scripts/src/publish.ts @@ -1,18 +1,17 @@ #!/usr/bin/env node import * as promisify from 'es6-promisify'; -import * as fs from 'fs'; import * as _ from 'lodash'; import * as moment from 'moment'; import opn = require('opn'); -import * as path from 'path'; import { exec as execAsync, spawn } from 'promisify-child-process'; import * as prompt from 'prompt'; +import semver = require('semver'); import semverDiff = require('semver-diff'); import semverSort = require('semver-sort'); import { constants } from './constants'; -import { Changelog, PackageToVersionChange, SemVerIndex, VersionChangelog } from './types'; +import { PackageToVersionChange, SemVerIndex, VersionChangelog } from './types'; import { changelogUtils } from './utils/changelog_utils'; import { utils } from './utils/utils'; @@ -119,25 +118,23 @@ async function updateChangeLogsAsync(updatedPublicLernaPackages: LernaPackage[]) const packageToVersionChange: PackageToVersionChange = {}; for (const lernaPackage of updatedPublicLernaPackages) { const packageName = lernaPackage.package.name; - const changelogJSONPath = path.join(lernaPackage.location, 'CHANGELOG.json'); - const changelogJSON = utils.getChangelogJSONOrCreateIfMissing(changelogJSONPath); - let changelog: Changelog; - try { - changelog = JSON.parse(changelogJSON); - } catch (err) { - throw new Error( - `${lernaPackage.package.name}'s CHANGELOG.json contains invalid JSON. Please fix and try again.`, - ); - } + let changelog = changelogUtils.getChangelogOrCreateIfMissing(packageName, lernaPackage.location); const currentVersion = lernaPackage.package.version; - const shouldAddNewEntry = changelogUtils.shouldAddNewChangelogEntry(currentVersion, changelog); + const shouldAddNewEntry = changelogUtils.shouldAddNewChangelogEntry( + lernaPackage.package.name, + currentVersion, + changelog, + ); if (shouldAddNewEntry) { // Create a new entry for a patch version with generic changelog entry. - const nextPatchVersion = utils.getNextPatchVersion(currentVersion); + const nextPatchVersionIfValid = semver.inc(currentVersion, 'patch'); + if (_.isNull(nextPatchVersionIfValid)) { + throw new Error(`Encountered invalid semver version: ${currentVersion} for package: ${packageName}`); + } const newChangelogEntry: VersionChangelog = { timestamp: TODAYS_TIMESTAMP, - version: nextPatchVersion, + version: nextPatchVersionIfValid, changes: [ { note: 'Dependencies updated', @@ -145,7 +142,7 @@ async function updateChangeLogsAsync(updatedPublicLernaPackages: LernaPackage[]) ], }; changelog = [newChangelogEntry, ...changelog]; - packageToVersionChange[packageName] = semverDiff(currentVersion, nextPatchVersion); + packageToVersionChange[packageName] = semverDiff(currentVersion, nextPatchVersionIfValid); } else { // Update existing entry with timestamp const lastEntry = changelog[0]; @@ -160,14 +157,11 @@ async function updateChangeLogsAsync(updatedPublicLernaPackages: LernaPackage[]) } // Save updated CHANGELOG.json - fs.writeFileSync(changelogJSONPath, JSON.stringify(changelog, null, '\t')); - await utils.prettifyAsync(changelogJSONPath, constants.monorepoRootPath); + await changelogUtils.writeChangelogJsonFileAsync(lernaPackage.location, changelog); utils.log(`${packageName}: Updated CHANGELOG.json`); // Generate updated CHANGELOG.md const changelogMd = changelogUtils.generateChangelogMd(changelog); - const changelogMdPath = path.join(lernaPackage.location, 'CHANGELOG.md'); - fs.writeFileSync(changelogMdPath, changelogMd); - await utils.prettifyAsync(changelogMdPath, constants.monorepoRootPath); + await changelogUtils.writeChangelogMdFileAsync(lernaPackage.location, changelogMd); utils.log(`${packageName}: Updated CHANGELOG.md`); } @@ -217,12 +211,16 @@ async function lernaPublishAsync(packageToVersionChange: { [name: string]: strin } function updateVersionNumberIfNeeded(currentVersion: string, proposedNextVersion: string): string { + const updatedVersionIfValid = semver.inc(currentVersion, 'patch'); + if (_.isNull(updatedVersionIfValid)) { + throw new Error(`Encountered invalid semver: ${currentVersion}`); + } if (proposedNextVersion === currentVersion) { - return utils.getNextPatchVersion(currentVersion); + return updatedVersionIfValid; } const sortedVersions = semverSort.desc([proposedNextVersion, currentVersion]); if (sortedVersions[0] !== proposedNextVersion) { - return utils.getNextPatchVersion(currentVersion); + return updatedVersionIfValid; } return proposedNextVersion; } diff --git a/packages/monorepo-scripts/src/remove_tags.ts b/packages/monorepo-scripts/src/remove_tags.ts deleted file mode 100644 index 50e413495..000000000 --- a/packages/monorepo-scripts/src/remove_tags.ts +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env node - -import * as _ from 'lodash'; -import * as path from 'path'; -import { exec as execAsync } from 'promisify-child-process'; -import semverSort = require('semver-sort'); - -import { constants } from './constants'; -import { Changelog } from './types'; -import { utils } from './utils/utils'; - -(async () => { - const shouldIncludePrivate = true; - const updatedPublicLernaPackages = await utils.getUpdatedLernaPackagesAsync(shouldIncludePrivate); - - for (const lernaPackage of updatedPublicLernaPackages) { - const packageName = lernaPackage.package.name; - const currentVersion = lernaPackage.package.version; - const changelogJSONPath = path.join(lernaPackage.location, 'CHANGELOG.json'); - // Private packages don't have changelogs, and their versions are always incremented - // by a patch version. - const changelogJSONIfExists = utils.getChangelogJSONIfExists(changelogJSONPath); - - let latestChangelogVersion: string; - if (!_.isUndefined(changelogJSONIfExists)) { - let changelogs: Changelog; - try { - changelogs = JSON.parse(changelogJSONIfExists); - } catch (err) { - throw new Error( - `${lernaPackage.package.name}'s CHANGELOG.json contains invalid JSON. Please fix and try again.`, - ); - } - latestChangelogVersion = changelogs[0].version; - } else { - latestChangelogVersion = utils.getNextPatchVersion(currentVersion); - } - - const sortedVersions = semverSort.desc([latestChangelogVersion, currentVersion]); - if (sortedVersions[0] === latestChangelogVersion && latestChangelogVersion !== currentVersion) { - const tagName = `${packageName}@${latestChangelogVersion}`; - try { - await execAsync(`git tag -d ${tagName}`, { cwd: constants.monorepoRootPath }); - utils.log(`removed tag: ${tagName}`); - } catch (err) { - if (_.includes(err.message, 'not found')) { - utils.log(`Could not find tag: ${tagName}`); - } else { - throw err; - } - } - } - } -})().catch(err => { - utils.log(err); - process.exit(1); -}); diff --git a/packages/monorepo-scripts/src/types.ts b/packages/monorepo-scripts/src/types.ts index 36fb923b3..61bd75732 100644 --- a/packages/monorepo-scripts/src/types.ts +++ b/packages/monorepo-scripts/src/types.ts @@ -27,3 +27,16 @@ export enum SemVerIndex { export interface PackageToVersionChange { [name: string]: string; } + +export interface PackageRegistryJson { + versions: { + [version: string]: any; + }; + time: { + [version: string]: string; + }; +} + +export interface GitTagsByPackageName { + [packageName: string]: string[]; +} diff --git a/packages/monorepo-scripts/src/utils/changelog_utils.ts b/packages/monorepo-scripts/src/utils/changelog_utils.ts index edfe65a80..dbafb6f16 100644 --- a/packages/monorepo-scripts/src/utils/changelog_utils.ts +++ b/packages/monorepo-scripts/src/utils/changelog_utils.ts @@ -1,6 +1,11 @@ +import * as fs from 'fs'; import * as _ from 'lodash'; import * as moment from 'moment'; +import * as path from 'path'; +import { exec as execAsync } from 'promisify-child-process'; +import semver = require('semver'); +import { constants } from '../constants'; import { Change, Changelog, VersionChangelog } from '../types'; const CHANGELOG_MD_HEADER = ` @@ -44,12 +49,58 @@ export const changelogUtils = { return changelogMd; }, - shouldAddNewChangelogEntry(currentVersion: string, changelog: Changelog): boolean { + shouldAddNewChangelogEntry(packageName: string, currentVersion: string, changelog: Changelog): boolean { if (_.isEmpty(changelog)) { return true; } const lastEntry = changelog[0]; + if (semver.lt(lastEntry.version, currentVersion)) { + throw new Error( + `Found CHANGELOG version lower then current package version. ${packageName} current: ${currentVersion}, Changelog: ${ + lastEntry.version + }`, + ); + } const isLastEntryCurrentVersion = lastEntry.version === currentVersion; return isLastEntryCurrentVersion; }, + getChangelogJSONIfExists(changelogPath: string): string | undefined { + try { + const changelogJSON = fs.readFileSync(changelogPath, 'utf-8'); + return changelogJSON; + } catch (err) { + return undefined; + } + }, + getChangelogOrCreateIfMissing(packageName: string, packageLocation: string): Changelog { + const changelogJSONPath = path.join(packageLocation, 'CHANGELOG.json'); + let changelogJsonIfExists = this.getChangelogJSONIfExists(changelogJSONPath); + if (_.isUndefined(changelogJsonIfExists)) { + // If none exists, create new, empty one. + changelogJsonIfExists = '[]'; + fs.writeFileSync(changelogJSONPath, changelogJsonIfExists); + } + let changelog: Changelog; + try { + changelog = JSON.parse(changelogJsonIfExists); + } catch (err) { + throw new Error(`${packageName}'s CHANGELOG.json contains invalid JSON. Please fix and try again.`); + } + return changelog; + }, + async writeChangelogJsonFileAsync(packageLocation: string, changelog: Changelog): Promise<void> { + const changelogJSONPath = path.join(packageLocation, 'CHANGELOG.json'); + fs.writeFileSync(changelogJSONPath, JSON.stringify(changelog, null, '\t')); + await this.prettifyAsync(changelogJSONPath, constants.monorepoRootPath); + }, + async writeChangelogMdFileAsync(packageLocation: string, changelogMdString: string): Promise<void> { + const changelogMarkdownPath = path.join(packageLocation, 'CHANGELOG.md'); + fs.writeFileSync(changelogMarkdownPath, changelogMdString); + await this.prettifyAsync(changelogMarkdownPath, constants.monorepoRootPath); + }, + async prettifyAsync(filePath: string, cwd: string): Promise<void> { + await execAsync(`prettier --write ${filePath} --config .prettierrc`, { + cwd, + }); + }, }; diff --git a/packages/monorepo-scripts/src/utils/npm_utils.ts b/packages/monorepo-scripts/src/utils/npm_utils.ts new file mode 100644 index 000000000..cc1e046e7 --- /dev/null +++ b/packages/monorepo-scripts/src/utils/npm_utils.ts @@ -0,0 +1,28 @@ +import 'isomorphic-fetch'; +import * as _ from 'lodash'; + +import { PackageRegistryJson } from '../types'; + +const NPM_REGISTRY_BASE_URL = 'https://registry.npmjs.org'; +const SUCCESS_STATUS = 200; +const NOT_FOUND_STATUS = 404; + +export const npmUtils = { + async getPackageRegistryJsonIfExistsAsync(packageName: string): Promise<PackageRegistryJson | undefined> { + const url = `${NPM_REGISTRY_BASE_URL}/${packageName}`; + const response = await fetch(url); + + if (response.status === NOT_FOUND_STATUS) { + return undefined; + } else if (response.status !== SUCCESS_STATUS) { + throw new Error(`Request to ${url} failed. Check your internet connection and that npmjs.org is up.`); + } + const packageRegistryJson = await response.json(); + return packageRegistryJson; + }, + getPreviouslyPublishedVersions(packageRegistryJson: PackageRegistryJson): string[] { + const timeWithOnlyVersions = _.omit(packageRegistryJson.time, ['modified', 'created']); + const versions = _.keys(timeWithOnlyVersions); + return versions; + }, +}; diff --git a/packages/monorepo-scripts/src/utils/utils.ts b/packages/monorepo-scripts/src/utils/utils.ts index 0b8ac4c0b..20bc57bae 100644 --- a/packages/monorepo-scripts/src/utils/utils.ts +++ b/packages/monorepo-scripts/src/utils/utils.ts @@ -1,27 +1,17 @@ -import * as fs from 'fs'; import lernaGetPackages = require('lerna-get-packages'); import * as _ from 'lodash'; import { exec as execAsync } from 'promisify-child-process'; +import semver = require('semver'); import { constants } from '../constants'; -import { UpdatedPackage } from '../types'; +import { GitTagsByPackageName, UpdatedPackage } from '../types'; + +import { changelogUtils } from './changelog_utils'; export const utils = { log(...args: any[]): void { console.log(...args); // tslint:disable-line:no-console }, - getNextPatchVersion(currentVersion: string): string { - const versionSegments = currentVersion.split('.'); - const patch = _.parseInt(_.last(versionSegments) as string); - const newPatch = patch + 1; - const newPatchVersion = `${versionSegments[0]}.${versionSegments[1]}.${newPatch}`; - return newPatchVersion; - }, - async prettifyAsync(filePath: string, cwd: string): Promise<void> { - await execAsync(`prettier --write ${filePath} --config .prettierrc`, { - cwd, - }); - }, async getUpdatedLernaPackagesAsync(shouldIncludePrivate: boolean): Promise<LernaPackage[]> { const updatedPublicPackages = await this.getLernaUpdatedPackagesAsync(shouldIncludePrivate); const updatedPackageNames = _.map(updatedPublicPackages, pkg => pkg.name); @@ -43,22 +33,86 @@ export const utils = { } return updatedPackages; }, - getChangelogJSONIfExists(changelogPath: string): string | undefined { - try { - const changelogJSON = fs.readFileSync(changelogPath, 'utf-8'); - return changelogJSON; - } catch (err) { - return undefined; + async getNextPackageVersionAsync( + currentVersion: string, + packageName: string, + packageLocation: string, + ): Promise<string> { + let nextVersionIfValid; + const changelog = changelogUtils.getChangelogOrCreateIfMissing(packageName, packageLocation); + if (_.isEmpty(changelog)) { + nextVersionIfValid = semver.inc(currentVersion, 'patch'); + } + const lastEntry = changelog[0]; + nextVersionIfValid = semver.eq(lastEntry.version, currentVersion) + ? semver.inc(currentVersion, 'patch') + : lastEntry.version; + if (_.isNull(nextVersionIfValid)) { + throw new Error(`Encountered invalid semver: ${currentVersion} associated with ${packageName}`); } + return nextVersionIfValid; }, - getChangelogJSONOrCreateIfMissing(changelogPath: string): string { - const changelogIfExists = this.getChangelogJSONIfExists(changelogPath); - if (_.isUndefined(changelogIfExists)) { - // If none exists, create new, empty one. - const emptyChangelogJSON = JSON.stringify([]); - fs.writeFileSync(changelogPath, emptyChangelogJSON); - return emptyChangelogJSON; + async getRemoteGitTagsAsync(): Promise<string[]> { + const result = await execAsync(`git ls-remote --tags`, { + cwd: constants.monorepoRootPath, + }); + const tagsString = result.stdout; + const tagOutputs: string[] = tagsString.split('\n'); + const tags = _.compact( + _.map(tagOutputs, tagOutput => { + const tag = tagOutput.split('refs/tags/')[1]; + // Tags with `^{}` are duplicateous so we ignore them + // Source: https://stackoverflow.com/questions/15472107/when-listing-git-ls-remote-why-theres-after-the-tag-name + if (_.endsWith(tag, '^{}')) { + return undefined; + } + return tag; + }), + ); + return tags; + }, + async getLocalGitTagsAsync(): Promise<string[]> { + const result = await execAsync(`git tags`, { + cwd: constants.monorepoRootPath, + }); + const tagsString = result.stdout; + const tags = tagsString.split('\n'); + return tags; + }, + async getGitTagsByPackageNameAsync(packageNames: string[], gitTags: string[]): Promise<GitTagsByPackageName> { + const tagVersionByPackageName: GitTagsByPackageName = {}; + _.each(gitTags, tag => { + const packageNameIfExists = _.find(packageNames, name => { + return _.includes(tag, `${name}@`); + }); + if (_.isUndefined(packageNameIfExists)) { + return; // ignore tags not related to a package we care about. + } + const splitTag = tag.split(`${packageNameIfExists}@`); + if (splitTag.length !== 2) { + throw new Error(`Unexpected tag name found: ${tag}`); + } + const version = splitTag[1]; + (tagVersionByPackageName[packageNameIfExists] || (tagVersionByPackageName[packageNameIfExists] = [])).push( + version, + ); + }); + return tagVersionByPackageName; + }, + async removeLocalTagAsync(tagName: string): Promise<void> { + const result = await execAsync(`git tag -d ${tagName}`, { + cwd: constants.monorepoRootPath, + }); + if (!_.isEmpty(result.stderr)) { + throw new Error(`Failed to delete local git tag. Got err: ${result.stderr}`); + } + }, + async removeRemoteTagAsync(tagName: string): Promise<void> { + const result = await execAsync(`git push origin ${tagName}`, { + cwd: constants.monorepoRootPath, + }); + if (!_.isEmpty(result.stderr)) { + throw new Error(`Failed to delete remote git tag. Got err: ${result.stderr}`); } - return changelogIfExists; }, }; |