import batchPackages = require('@lerna/batch-packages'); import * as fs from 'fs'; import * as _ from 'lodash'; import { exec as execAsync } from 'promisify-child-process'; import semver = require('semver'); import { constants } from '../constants'; import { GitTagsByPackageName, Package, PackageJSON, UpdatedPackage } from '../types'; import { changelogUtils } from './changelog_utils'; export const utils = { log(...args: any[]): void { console.log(...args); // tslint:disable-line:no-console }, getTopologicallySortedPackages(rootDir: string): Package[] { const packages = utils.getPackages(rootDir); const batchedPackages: PackageJSON[] = _.flatten(batchPackages(_.map(packages, pkg => pkg.packageJson), false)); const topsortedPackages: Package[] = _.map( batchedPackages, (pkg: PackageJSON) => _.find(packages, pkg1 => pkg1.packageJson.name === pkg.name) as Package, ); return topsortedPackages; }, getPackages(rootDir: string): Package[] { const rootPackageJsonString = fs.readFileSync(`${rootDir}/package.json`, 'utf8'); const rootPackageJson = JSON.parse(rootPackageJsonString); if (_.isUndefined(rootPackageJson.workspaces)) { throw new Error(`Did not find 'workspaces' key in root package.json`); } const packages = []; for (const workspace of rootPackageJson.workspaces) { // HACK: Remove allowed wildcards from workspace entries. // This might be entirely comprehensive. const workspacePath = workspace.replace('*', '').replace('**/*', ''); const subpackageNames = fs.readdirSync(`${rootDir}/${workspacePath}`); for (const subpackageName of subpackageNames) { if (_.startsWith(subpackageName, '.')) { continue; } const pathToPackageJson = `${rootDir}/${workspacePath}${subpackageName}`; try { const packageJsonString = fs.readFileSync(`${pathToPackageJson}/package.json`, 'utf8'); const packageJson = JSON.parse(packageJsonString); const pkg = { location: pathToPackageJson, packageJson, }; packages.push(pkg); } catch (err) { // Couldn't find a 'package.json' for package. Skipping. } } } return packages; }, async getPackagesByNameAsync(packageNames: string[]): Promise { const allPackages = utils.getPackages(constants.monorepoRootPath); const updatedPackages = _.filter(allPackages, pkg => { return _.includes(packageNames, pkg.packageJson.name); }); return updatedPackages; }, async getUpdatedPackagesAsync(shouldIncludePrivate: boolean): Promise { const updatedPublicPackages = await utils.getLernaUpdatedPackagesAsync(shouldIncludePrivate); const updatedPackageNames = _.map(updatedPublicPackages, pkg => pkg.name); const allPackages = utils.getPackages(constants.monorepoRootPath); const updatedPackages = _.filter(allPackages, pkg => { return _.includes(updatedPackageNames, pkg.packageJson.name); }); return updatedPackages; }, async getLernaUpdatedPackagesAsync(shouldIncludePrivate: boolean): Promise { const result = await execAsync(`${constants.lernaExecutable} updated --json`, { cwd: constants.monorepoRootPath, }); const updatedPackages = JSON.parse(result.stdout); if (!shouldIncludePrivate) { const updatedPublicPackages = _.filter(updatedPackages, updatedPackage => !updatedPackage.private); return updatedPublicPackages; } return updatedPackages; }, async getNextPackageVersionAsync( currentVersion: string, packageName: string, packageLocation: string, ): Promise { let nextVersionIfValid; const changelog = changelogUtils.getChangelogOrCreateIfMissing(packageName, packageLocation); if (_.isEmpty(changelog)) { nextVersionIfValid = semver.inc(currentVersion, 'patch'); } const lastEntry = changelog[0]; if (semver.gt(currentVersion, lastEntry.version)) { throw new Error(`Package.json version cannot be greater then last CHANGELOG entry. Check: ${packageName}`); } 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; }, async getRemoteGitTagsAsync(): Promise { 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 { const result = await execAsync(`git tag`, { cwd: constants.monorepoRootPath, }); const tagsString = result.stdout; const tags = tagsString.split('\n'); return tags; }, async getGitTagsByPackageNameAsync(packageNames: string[], gitTags: string[]): Promise { 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 { try { await execAsync(`git tag -d ${tagName}`, { cwd: constants.monorepoRootPath, }); } catch (err) { throw new Error(`Failed to delete local git tag. Got err: ${err}`); } utils.log(`Removed local tag: ${tagName}`); }, async removeRemoteTagAsync(tagName: string): Promise { try { await execAsync(`git push origin ${tagName}`, { cwd: constants.monorepoRootPath, }); } catch (err) { throw new Error(`Failed to delete remote git tag. Got err: ${err}`); } utils.log(`Removed remote tag: ${tagName}`); }, };