aboutsummaryrefslogtreecommitdiffstats
path: root/packages/monorepo-scripts/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/monorepo-scripts/src')
-rw-r--r--packages/monorepo-scripts/src/constants.ts3
-rw-r--r--packages/monorepo-scripts/src/convert_changelogs.ts99
-rw-r--r--packages/monorepo-scripts/src/deps_versions.ts4
-rw-r--r--packages/monorepo-scripts/src/find_unused_dependencies.ts34
-rw-r--r--packages/monorepo-scripts/src/globals.d.ts24
-rw-r--r--packages/monorepo-scripts/src/postpublish_utils.ts123
-rw-r--r--packages/monorepo-scripts/src/prepublish_checks.ts189
-rw-r--r--packages/monorepo-scripts/src/publish.ts323
-rw-r--r--packages/monorepo-scripts/src/test_installation.ts176
-rw-r--r--packages/monorepo-scripts/src/types.ts45
-rw-r--r--packages/monorepo-scripts/src/utils.ts20
-rw-r--r--packages/monorepo-scripts/src/utils/changelog_utils.ts106
-rw-r--r--packages/monorepo-scripts/src/utils/configs.ts8
-rw-r--r--packages/monorepo-scripts/src/utils/npm_utils.ts29
-rw-r--r--packages/monorepo-scripts/src/utils/utils.ts167
15 files changed, 934 insertions, 416 deletions
diff --git a/packages/monorepo-scripts/src/constants.ts b/packages/monorepo-scripts/src/constants.ts
index 74387a159..e5d3348bd 100644
--- a/packages/monorepo-scripts/src/constants.ts
+++ b/packages/monorepo-scripts/src/constants.ts
@@ -2,4 +2,7 @@ import * as path from 'path';
export const constants = {
monorepoRootPath: path.join(__dirname, '../../..'),
+ stagingWebsite: 'http://staging-0xproject.s3-website-us-east-1.amazonaws.com',
+ lernaExecutable: path.join('node_modules', '@0x-lerna-fork', 'lerna', 'cli.js'),
+ githubPersonalAccessToken: process.env.GITHUB_PERSONAL_ACCESS_TOKEN_0X_JS,
};
diff --git a/packages/monorepo-scripts/src/convert_changelogs.ts b/packages/monorepo-scripts/src/convert_changelogs.ts
deleted file mode 100644
index b5be14ed8..000000000
--- a/packages/monorepo-scripts/src/convert_changelogs.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-#!/usr/bin/env node
-/**
- * TEMPORARY SCRIPT
- * This script exists to migrate the legacy CHANGELOG.md to the canonical CHANGELOG.md
- * TODO: Remove after migration is successful and committed.
- */
-
-import * as fs from 'fs';
-import lernaGetPackages = require('lerna-get-packages');
-import * as _ from 'lodash';
-import * as moment from 'moment';
-import * as path from 'path';
-import { exec as execAsync } from 'promisify-child-process';
-
-import { constants } from './constants';
-import { Changelog, Changes, UpdatedPackage } from './types';
-import { utils } from './utils';
-
-const HEADER_PRAGMA = '##';
-
-(async () => {
- const allLernaPackages = lernaGetPackages(constants.monorepoRootPath);
- const publicLernaPackages = _.filter(allLernaPackages, pkg => !pkg.package.private);
- for (const lernaPackage of publicLernaPackages) {
- const changelogMdIfExists = getChangelogMdIfExists(lernaPackage.package.name, lernaPackage.location);
- if (_.isUndefined(changelogMdIfExists)) {
- throw new Error(`${lernaPackage.package.name} should have CHANGELOG.md b/c it's public. Add one.`);
- }
-
- const lines = changelogMdIfExists.split('\n');
- const changelogs: Changelog[] = [];
- let changelog: Changelog = {
- version: '',
- changes: [],
- };
- /**
- * Example MD entry:
- * ## v0.3.1 - _March 18, 2018_
- *
- * * Add TS types for `yargs` (#400)
- */
- for (const line of lines) {
- if (_.startsWith(line, `${HEADER_PRAGMA} `)) {
- let version = line.substr(4).split(' - ')[0];
- if (version === '0.x.x') {
- version = utils.getNextPatchVersion(lernaPackage.package.version);
- }
- const dateStr = line.split('_')[1];
- let date;
- if (!_.includes(dateStr, 'TBD')) {
- date = moment(dateStr, 'MMMM D, YYYY');
- }
- changelog = {
- version,
- changes: [],
- };
- if (!_.isUndefined(date)) {
- changelog.timestamp = date.unix();
- }
- if (!_.includes(dateStr, 'TBD')) {
- changelog.isPublished = true;
- }
- changelogs.push(changelog);
- } else if (_.includes(line, '* ')) {
- const note = line.split('* ')[1].split(' (#')[0];
- const prChunk = line.split(' (#')[1];
- let pr;
- if (!_.isUndefined(prChunk)) {
- pr = prChunk.split(')')[0];
- }
- const changes: Changes = {
- note,
- };
- if (!_.isUndefined(pr)) {
- changes.pr = _.parseInt(pr);
- }
- changelog.changes.push(changes);
- }
- }
- const changelogJSON = JSON.stringify(changelogs, null, 4);
- const changelogJSONPath = `${lernaPackage.location}/CHANGELOG.json`;
- fs.writeFileSync(changelogJSONPath, changelogJSON);
- await utils.prettifyAsync(changelogJSONPath, constants.monorepoRootPath);
- }
-})().catch(err => {
- utils.log(err.stdout);
- process.exit(1);
-});
-
-function getChangelogMdIfExists(packageName: string, location: string): string | undefined {
- const changelogPath = path.join(location, 'CHANGELOG.md');
- let changelogMd: string;
- try {
- changelogMd = fs.readFileSync(changelogPath, 'utf-8');
- return changelogMd;
- } catch (err) {
- return undefined;
- }
-}
diff --git a/packages/monorepo-scripts/src/deps_versions.ts b/packages/monorepo-scripts/src/deps_versions.ts
index 07292a160..1053906b7 100644
--- a/packages/monorepo-scripts/src/deps_versions.ts
+++ b/packages/monorepo-scripts/src/deps_versions.ts
@@ -5,7 +5,7 @@ import * as fs from 'fs';
import { sync as globSync } from 'glob';
import * as _ from 'lodash';
-import { utils } from './utils';
+import { utils } from './utils/utils';
interface Dependencies {
[depName: string]: string;
@@ -19,6 +19,7 @@ interface VersionsByDependency {
const PACKAGE_JSON_GLOB = '../*/package.json';
+// tslint:disable:no-unused-variable
function getDependencies(path: string): Dependencies {
const file = fs.readFileSync(path).toString();
const parsed = JSON.parse(file);
@@ -52,3 +53,4 @@ _.map(versionsByDependency, (versions: Versions, depName: string) => {
});
}
});
+// tslint:disable:no-unused-variable
diff --git a/packages/monorepo-scripts/src/find_unused_dependencies.ts b/packages/monorepo-scripts/src/find_unused_dependencies.ts
new file mode 100644
index 000000000..4bb4b7dc5
--- /dev/null
+++ b/packages/monorepo-scripts/src/find_unused_dependencies.ts
@@ -0,0 +1,34 @@
+#!/usr/bin/env node
+
+import * as depcheckAsync from 'depcheck';
+import * as _ from 'lodash';
+
+import { constants } from './constants';
+import { utils } from './utils/utils';
+
+// For some reason, `depcheck` hangs on some packages. Add them here.
+const IGNORE_PACKAGES = ['@0xproject/sol-compiler'];
+
+(async () => {
+ utils.log('*** NOTE: Not all deps listed here are actually not required. ***');
+ utils.log("*** `depcheck` isn't perfect so double check before actually removing any. ***\n");
+ const packages = utils.getPackages(constants.monorepoRootPath);
+ for (const pkg of packages) {
+ if (_.includes(IGNORE_PACKAGES, pkg.packageJson.name)) {
+ continue; // skip
+ }
+ utils.log(`Checking ${pkg.packageJson.name} for unused deps. This might take a while...`);
+
+ const configs = {};
+ const { dependencies } = await depcheckAsync(pkg.location, configs);
+ if (!_.isEmpty(dependencies)) {
+ _.each(dependencies, dep => {
+ utils.log(dep);
+ });
+ }
+ utils.log('\n');
+ }
+})().catch(err => {
+ utils.log(err);
+ process.exit(1);
+});
diff --git a/packages/monorepo-scripts/src/globals.d.ts b/packages/monorepo-scripts/src/globals.d.ts
index c5898d0f5..2b25802ee 100644
--- a/packages/monorepo-scripts/src/globals.d.ts
+++ b/packages/monorepo-scripts/src/globals.d.ts
@@ -3,27 +3,15 @@ declare module 'publish-release';
declare module 'es6-promisify';
declare module 'semver-diff';
+declare module 'prompt' {
+ const start: () => void;
+ const get: (promptMessages: string[], callback: (err: Error, result: string) => void) => void;
+}
+
// semver-sort declarations
declare module 'semver-sort' {
const desc: (versions: string[]) => string[];
}
-declare interface LernaPackage {
- location: string;
- package: {
- private?: boolean;
- version: string;
- name: string;
- main?: string;
- config?: {
- additionalTsTypings?: string[];
- };
- };
-}
-declare function lernaGetPackages(path: string): LernaPackage[];
-// lerna-get-packages declarations
-declare module 'lerna-get-packages' {
- export = lernaGetPackages;
-}
-
declare module 'promisify-child-process';
+declare module '@lerna/batch-packages';
diff --git a/packages/monorepo-scripts/src/postpublish_utils.ts b/packages/monorepo-scripts/src/postpublish_utils.ts
index 236b54379..8e445a045 100644
--- a/packages/monorepo-scripts/src/postpublish_utils.ts
+++ b/packages/monorepo-scripts/src/postpublish_utils.ts
@@ -4,13 +4,12 @@ import * as fs from 'fs';
import * as _ from 'lodash';
import * as path from 'path';
import * as publishRelease from 'publish-release';
-import semverSort = require('semver-sort');
import { constants } from './constants';
-import { utils } from './utils';
+import { configs } from './utils/configs';
+import { utils } from './utils/utils';
const publishReleaseAsync = promisify(publishRelease);
-const githubPersonalAccessToken = process.env.GITHUB_PERSONAL_ACCESS_TOKEN_0X_JS;
const generatedDocsDirectoryName = 'generated_docs';
export interface PostpublishConfigs {
@@ -36,7 +35,7 @@ export const postpublishUtils = {
throw new Error('version field required in package.json. Cannot publish release notes to Github.');
}
const postpublishConfig = _.get(packageJSON, 'config.postpublish', {});
- const configs: PostpublishConfigs = {
+ const postpublishConfigs: PostpublishConfigs = {
cwd,
packageName: packageJSON.name,
version: packageJSON.version,
@@ -50,54 +49,56 @@ export const postpublishUtils = {
s3StagingBucketPath: _.get(postpublishConfig, 'docPublishConfigs.s3StagingBucketPath'),
},
};
- return configs;
+ return postpublishConfigs;
},
async runAsync(packageJSON: any, tsConfigJSON: any, cwd: string): Promise<void> {
- const configs = this.generateConfig(packageJSON, tsConfigJSON, cwd);
- const release = await this.publishReleaseNotesAsync(
- configs.cwd,
- configs.packageName,
- configs.version,
- configs.assets,
+ if (configs.IS_LOCAL_PUBLISH) {
+ return;
+ }
+ const postpublishConfigs = postpublishUtils.generateConfig(packageJSON, tsConfigJSON, cwd);
+ await postpublishUtils.publishReleaseNotesAsync(
+ postpublishConfigs.packageName,
+ postpublishConfigs.version,
+ postpublishConfigs.assets,
);
if (
- !_.isUndefined(configs.docPublishConfigs.s3BucketPath) ||
- !_.isUndefined(configs.docPublishConfigs.s3StagingBucketPath)
+ !_.isUndefined(postpublishConfigs.docPublishConfigs.s3BucketPath) ||
+ !_.isUndefined(postpublishConfigs.docPublishConfigs.s3StagingBucketPath)
) {
utils.log('POSTPUBLISH: Release successful, generating docs...');
await postpublishUtils.generateAndUploadDocsAsync(
- configs.cwd,
- configs.docPublishConfigs.fileIncludes,
- configs.version,
- configs.docPublishConfigs.s3BucketPath,
+ postpublishConfigs.cwd,
+ postpublishConfigs.docPublishConfigs.fileIncludes,
+ postpublishConfigs.version,
+ postpublishConfigs.docPublishConfigs.s3BucketPath,
);
} else {
utils.log(`POSTPUBLISH: No S3Bucket config found for ${packageJSON.name}. Skipping doc JSON generation.`);
}
},
- async publishDocsToStagingAsync(packageJSON: any, tsConfigJSON: any, cwd: string) {
- const configs = this.generateConfig(packageJSON, tsConfigJSON, cwd);
- if (_.isUndefined(configs.docPublishConfigs.s3StagingBucketPath)) {
+ async publishDocsToStagingAsync(packageJSON: any, tsConfigJSON: any, cwd: string): Promise<void> {
+ const postpublishConfigs = postpublishUtils.generateConfig(packageJSON, tsConfigJSON, cwd);
+ if (_.isUndefined(postpublishConfigs.docPublishConfigs.s3StagingBucketPath)) {
utils.log('config.postpublish.docPublishConfigs.s3StagingBucketPath entry in package.json not found!');
return;
}
utils.log('POSTPUBLISH: Generating docs...');
await postpublishUtils.generateAndUploadDocsAsync(
- configs.cwd,
- configs.docPublishConfigs.fileIncludes,
- configs.version,
- configs.docPublishConfigs.s3StagingBucketPath,
+ postpublishConfigs.cwd,
+ postpublishConfigs.docPublishConfigs.fileIncludes,
+ postpublishConfigs.version,
+ postpublishConfigs.docPublishConfigs.s3StagingBucketPath,
);
},
- async publishReleaseNotesAsync(cwd: string, packageName: string, version: string, assets: string[]): Promise<void> {
- const notes = this.getReleaseNotes(packageName);
- const releaseName = this.getReleaseName(packageName, version);
- const tag = this.getTag(packageName, version);
- const finalAssets = this.adjustAssetPaths(cwd, assets);
+ async publishReleaseNotesAsync(packageName: string, version: string, assets: string[]): Promise<void> {
+ const notes = postpublishUtils.getReleaseNotes(packageName, version);
+ const releaseName = postpublishUtils.getReleaseName(packageName, version);
+ const tag = postpublishUtils.getTag(packageName, version);
+ postpublishUtils.adjustAssetPaths(assets);
utils.log('POSTPUBLISH: Releasing ', releaseName, '...');
- const result = await publishReleaseAsync({
- token: githubPersonalAccessToken,
+ await publishReleaseAsync({
+ token: constants.githubPersonalAccessToken,
owner: '0xProject',
repo: '0x-monorepo',
tag,
@@ -109,9 +110,8 @@ export const postpublishUtils = {
reuseDraftOnly: false,
assets,
});
- this.updateChangelogIsPublished(packageName);
},
- getReleaseNotes(packageName: string) {
+ getReleaseNotes(packageName: string, version: string): string {
const packageNameWithNamespace = packageName.replace('@0xproject/', '');
const changelogJSONPath = path.join(
constants.monorepoRootPath,
@@ -122,45 +122,34 @@ export const postpublishUtils = {
const changelogJSON = fs.readFileSync(changelogJSONPath, 'utf-8');
const changelogs = JSON.parse(changelogJSON);
const latestLog = changelogs[0];
- if (_.isUndefined(latestLog.isPublished)) {
- let notes = '';
- _.each(latestLog.changes, change => {
- notes += `* ${change.note}`;
- if (change.pr) {
- notes += ` (${change.pr})`;
- }
- notes += `\n`;
- });
- return notes;
+ // We sanity check that the version for the changelog notes we are about to publish to Github
+ // correspond to the new version of the package.
+ if (version !== latestLog.version) {
+ throw new Error('Expected CHANGELOG.json latest entry version to coincide with published version.');
}
- return 'N/A';
- },
- updateChangelogIsPublished(packageName: string) {
- const packageNameWithNamespace = packageName.replace('@0xproject/', '');
- const changelogJSONPath = path.join(
- constants.monorepoRootPath,
- 'packages',
- packageNameWithNamespace,
- 'CHANGELOG.json',
- );
- const changelogJSON = fs.readFileSync(changelogJSONPath, 'utf-8');
- const changelogs = JSON.parse(changelogJSON);
- const latestLog = changelogs[0];
- latestLog.isPublished = true;
- changelogs[0] = latestLog;
- fs.writeFileSync(changelogJSONPath, JSON.stringify(changelogs, null, '\t'));
+ let notes = '';
+ _.each(latestLog.changes, change => {
+ notes += `* ${change.note}`;
+ if (change.pr) {
+ notes += ` (#${change.pr})`;
+ }
+ notes += `\n`;
+ });
+ return notes;
},
- getTag(packageName: string, version: string) {
+ getTag(packageName: string, version: string): string {
return `${packageName}@${version}`;
},
getReleaseName(subPackageName: string, version: string): string {
const releaseName = `${subPackageName} v${version}`;
return releaseName;
},
- adjustAssetPaths(cwd: string, assets: string[]) {
+ // Asset paths should described from the monorepo root. This method prefixes
+ // the supplied path with the absolute path to the monorepo root.
+ adjustAssetPaths(assets: string[]): string[] {
const finalAssets: string[] = [];
_.each(assets, (asset: string) => {
- finalAssets.push(`${cwd}/${asset}`);
+ finalAssets.push(`${constants.monorepoRootPath}/${asset}`);
});
return finalAssets;
},
@@ -173,14 +162,20 @@ export const postpublishUtils = {
// HACK: tsconfig.json needs wildcard directory endings as `/**/*`
// but TypeDoc needs it as `/**` in order to pick up files at the root
if (_.endsWith(includePath, '/**/*')) {
+ // tslint:disable-next-line:custom-no-magic-numbers
includePath = includePath.slice(0, -2);
}
return includePath;
});
return fileIncludesAdjusted;
},
- async generateAndUploadDocsAsync(cwd: string, fileIncludes: string[], version: string, S3BucketPath: string) {
- const fileIncludesAdjusted = this.adjustFileIncludePaths(fileIncludes, cwd);
+ async generateAndUploadDocsAsync(
+ cwd: string,
+ fileIncludes: string[],
+ version: string,
+ S3BucketPath: string,
+ ): Promise<void> {
+ const fileIncludesAdjusted = postpublishUtils.adjustFileIncludePaths(fileIncludes, cwd);
const projectFiles = fileIncludesAdjusted.join(' ');
const jsonFilePath = `${cwd}/${generatedDocsDirectoryName}/index.json`;
const result = await execAsync(
diff --git a/packages/monorepo-scripts/src/prepublish_checks.ts b/packages/monorepo-scripts/src/prepublish_checks.ts
new file mode 100644
index 000000000..683c26094
--- /dev/null
+++ b/packages/monorepo-scripts/src/prepublish_checks.ts
@@ -0,0 +1,189 @@
+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 { Package } from './types';
+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 updatedPublicPackages = await utils.getUpdatedPackagesAsync(shouldIncludePrivate);
+
+ await checkCurrentVersionMatchesLatestPublishedNPMPackageAsync(updatedPublicPackages);
+ await checkChangelogFormatAsync(updatedPublicPackages);
+ await checkGitTagsForNextVersionAndDeleteIfExistAsync(updatedPublicPackages);
+ await checkPublishRequiredSetupAsync();
+}
+
+async function checkGitTagsForNextVersionAndDeleteIfExistAsync(updatedPublicPackages: Package[]): Promise<void> {
+ const packageNames = _.map(updatedPublicPackages, pkg => pkg.packageJson.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 pkg of updatedPublicPackages) {
+ const currentVersion = pkg.packageJson.version;
+ const packageName = pkg.packageJson.name;
+ const packageLocation = pkg.location;
+ const nextVersion = await utils.getNextPackageVersionAsync(currentVersion, packageName, packageLocation);
+
+ const remoteTagVersions = remoteTagVersionsByPackageName[packageName];
+ if (_.includes(remoteTagVersions, nextVersion)) {
+ const tagName = `:refs/tags/${packageName}@${nextVersion}`;
+ await utils.removeRemoteTagAsync(tagName);
+ }
+
+ const localTagVersions = localTagVersionsByPackageName[packageName];
+ if (_.includes(localTagVersions, nextVersion)) {
+ const tagName = `${packageName}@${nextVersion}`;
+ await utils.removeLocalTagAsync(tagName);
+ }
+ }
+}
+
+async function checkCurrentVersionMatchesLatestPublishedNPMPackageAsync(
+ updatedPublicPackages: Package[],
+): Promise<void> {
+ utils.log('Check package versions against npm registry...');
+ const versionMismatches = [];
+ for (const pkg of updatedPublicPackages) {
+ const packageName = pkg.packageJson.name;
+ const packageVersion = pkg.packageJson.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(updatedPublicPackages: Package[]): Promise<void> {
+ utils.log('Check CHANGELOGs for inconsistencies...');
+ const changeLogInconsistencies = [];
+ for (const pkg of updatedPublicPackages) {
+ const packageName = pkg.packageJson.name;
+ const changelog = changelogUtils.getChangelogOrCreateIfMissing(packageName, pkg.location);
+
+ const currentVersion = pkg.packageJson.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(pkg.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 {
+ // HACK: for some reason on some setups, the `npm whoami` will not recognize a logged-in user
+ // unless run with `sudo` (i.e Fabio's NVM setup) but is fine for others (Jacob's NVM setup).
+ utils.log('Checking that the user is logged in on npm...');
+ await execAsync(`sudo npm whoami`);
+ } catch (err) {
+ throw new Error('You must be logged into npm in the commandline to publish. Run `npm login` and try again.');
+ }
+
+ // Check to see if Git personal token setup
+ if (_.isUndefined(constants.githubPersonalAccessToken)) {
+ throw new Error(
+ 'You must have a Github personal access token set to an envVar named `GITHUB_PERSONAL_ACCESS_TOKEN_0X_JS`. Add it then try again.',
+ );
+ }
+
+ // Check Yarn version is 1.X
+ utils.log('Checking the yarn version...');
+ const result = await execAsync(`yarn --version`);
+ const version = result.stdout;
+ const versionSegments = version.split('.');
+ const majorVersion = _.parseInt(versionSegments[0]);
+ if (majorVersion < 1) {
+ throw new Error('Your yarn version must be v1.x or higher. Upgrade yarn and try again.');
+ }
+
+ // Check that `aws` commandline tool is installed
+ try {
+ utils.log('Checking that aws CLI tool is installed...');
+ await execAsync(`aws help`);
+ } catch (err) {
+ throw new Error('You must have `awscli` commandline tool installed. Install it and try again.');
+ }
+
+ // Check that `aws` credentials are setup
+ try {
+ utils.log('Checking that aws credentials are configured...');
+ await execAsync(`aws sts get-caller-identity`);
+ } catch (err) {
+ throw new Error('You must setup your AWS credentials by running `aws configure`. Do this and try again.');
+ }
+
+ utils.log('Checking that git branch is up to date with upstream...');
+ await execAsync('git fetch');
+ const res = await execAsync('git status -bs'); // s - short format, b - branch info
+ /**
+ * Possible outcomes
+ * ## branch_name...origin/branch_name [behind n]
+ * ## branch_name...origin/branch_name [ahead n]
+ * ## branch_name...origin/branch_name
+ */
+ const gitShortStatusHeader = res.stdout.split('\n')[0];
+ if (gitShortStatusHeader.includes('behind')) {
+ throw new Error('Your branch is behind upstream. Please pull before publishing.');
+ } else if (gitShortStatusHeader.includes('ahead')) {
+ throw new Error('Your branch is ahead of upstream. Please push before publishing.');
+ }
+}
+
+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 d749ec630..6ff0c9bef 100644
--- a/packages/monorepo-scripts/src/publish.ts
+++ b/packages/monorepo-scripts/src/publish.ts
@@ -1,217 +1,214 @@
#!/usr/bin/env node
-import * as fs from 'fs';
-import lernaGetPackages = require('lerna-get-packages');
+import * as promisify from 'es6-promisify';
import * as _ from 'lodash';
import * as moment from 'moment';
-import * as path from 'path';
-import { exec as execAsync, spawn } from 'promisify-child-process';
-import semverDiff = require('semver-diff');
+import opn = require('opn');
+import { exec as execAsync } from 'promisify-child-process';
+import * as prompt from 'prompt';
+import semver = require('semver');
import semverSort = require('semver-sort');
import { constants } from './constants';
-import { Changelog, Changes, SemVerIndex, UpdatedPackage } from './types';
-import { utils } from './utils';
+import { Package, PackageToNextVersion, VersionChangelog } from './types';
+import { changelogUtils } from './utils/changelog_utils';
+import { configs } from './utils/configs';
+import { utils } from './utils/utils';
-const IS_DRY_RUN = process.env.IS_DRY_RUN === 'true';
+const DOC_GEN_COMMAND = 'docs:json';
+const NPM_NAMESPACE = '@0xproject/';
const TODAYS_TIMESTAMP = moment().unix();
-const LERNA_EXECUTABLE = './node_modules/lerna/bin/lerna.js';
-const semverNameToIndex: { [semver: string]: number } = {
- patch: SemVerIndex.Patch,
- minor: SemVerIndex.Minor,
- major: SemVerIndex.Major,
+const packageNameToWebsitePath: { [name: string]: string } = {
+ '0x.js': '0xjs',
+ 'web3-wrapper': 'web3_wrapper',
+ contracts: 'contracts',
+ connect: 'connect',
+ 'json-schemas': 'json-schemas',
+ 'sol-compiler': 'sol-compiler',
+ 'sol-cov': 'sol-cov',
+ subproviders: 'subproviders',
+ 'order-utils': 'order-utils',
+ 'ethereum-types': 'ethereum-types',
};
+async function confirmAsync(message: string): Promise<void> {
+ prompt.start();
+ const result = await promisify(prompt.get)([message]);
+ const didConfirm = result[message] === 'y';
+ if (!didConfirm) {
+ utils.log('Publish process aborted.');
+ process.exit(0);
+ }
+}
+
(async () => {
- const updatedPublicPackages = await getPublicLernaUpdatedPackagesAsync();
- const updatedPackageNames = _.map(updatedPublicPackages, pkg => pkg.name);
+ // Fetch public, updated Lerna packages
+ const shouldIncludePrivate = true;
+ const allUpdatedPackages = await utils.getUpdatedPackagesAsync(shouldIncludePrivate);
+
+ if (!configs.IS_LOCAL_PUBLISH) {
+ await confirmAsync(
+ 'THIS IS NOT A TEST PUBLISH! You are about to publish one or more packages to npm. Are you sure you want to continue? (y/n)',
+ );
+ await confirmDocPagesRenderAsync(allUpdatedPackages);
+ }
+
+ // Update CHANGELOGs
+ const updatedPublicPackages = _.filter(allUpdatedPackages, pkg => !pkg.packageJson.private);
+ const updatedPublicPackageNames = _.map(updatedPublicPackages, pkg => pkg.packageJson.name);
+ utils.log(`Will update CHANGELOGs and publish: \n${updatedPublicPackageNames.join('\n')}\n`);
+ const packageToNextVersion = await updateChangeLogsAsync(updatedPublicPackages);
+
+ const updatedPrivatePackages = _.filter(allUpdatedPackages, pkg => pkg.packageJson.private);
+ _.each(updatedPrivatePackages, pkg => {
+ const currentVersion = pkg.packageJson.version;
+ const packageName = pkg.packageJson.name;
+ const nextPatchVersionIfValid = semver.inc(currentVersion, 'patch');
+ if (!_.isNull(nextPatchVersionIfValid)) {
+ packageToNextVersion[packageName] = nextPatchVersionIfValid;
+ } else {
+ throw new Error(`Encountered invalid semver version: ${currentVersion} for package: ${packageName}`);
+ }
+ });
+
+ // Push changelog changes to Github
+ if (!configs.IS_LOCAL_PUBLISH) {
+ await pushChangelogsToGithubAsync();
+ }
+
+ // Call LernaPublish
+ utils.log('Version updates to apply:');
+ _.each(packageToNextVersion, (versionChange: string, packageName: string) => {
+ utils.log(`${packageName} -> ${versionChange}`);
+ });
+ utils.log(`Calling 'lerna publish'...`);
+ await lernaPublishAsync(packageToNextVersion);
+})().catch(err => {
+ utils.log(err);
+ process.exit(1);
+});
+
+async function confirmDocPagesRenderAsync(packages: Package[]): Promise<void> {
+ // push docs to staging
+ utils.log("Upload all docJson's to S3 staging...");
+ await execAsync(`yarn stage_docs`, { cwd: constants.monorepoRootPath });
- const allLernaPackages = lernaGetPackages(constants.monorepoRootPath);
- const updatedPublicLernaPackages = _.filter(allLernaPackages, pkg => {
- return _.includes(updatedPackageNames, pkg.package.name);
+ // deploy website to staging
+ utils.log('Deploy website to staging...');
+ const pathToWebsite = `${constants.monorepoRootPath}/packages/website`;
+ await execAsync(`yarn deploy_staging`, { cwd: pathToWebsite });
+
+ const packagesWithDocs = _.filter(packages, pkg => {
+ const scriptsIfExists = pkg.packageJson.scripts;
+ if (_.isUndefined(scriptsIfExists)) {
+ throw new Error('Found a public package without any scripts in package.json');
+ }
+ return !_.isUndefined(scriptsIfExists[DOC_GEN_COMMAND]);
});
- const updatedPublicLernaPackageNames = _.map(updatedPublicLernaPackages, pkg => pkg.package.name);
- utils.log(`Will update CHANGELOGs and publish: \n${updatedPublicLernaPackageNames.join('\n')}\n`);
-
- const packageToVersionChange: { [name: string]: string } = {};
- for (const lernaPackage of updatedPublicLernaPackages) {
- const packageName = lernaPackage.package.name;
- const changelogJSONPath = path.join(lernaPackage.location, 'CHANGELOG.json');
- const changelogJSON = getChangelogJSONOrCreateIfMissing(lernaPackage.package.name, changelogJSONPath);
- let changelogs: Changelog[];
- try {
- changelogs = JSON.parse(changelogJSON);
- } catch (err) {
+ _.each(packagesWithDocs, pkg => {
+ const name = pkg.packageJson.name;
+ const nameWithoutPrefix = _.startsWith(name, NPM_NAMESPACE) ? name.split('@0xproject/')[1] : name;
+ const docSegmentIfExists = packageNameToWebsitePath[nameWithoutPrefix];
+ if (_.isUndefined(docSegmentIfExists)) {
throw new Error(
- `${lernaPackage.package.name}'s CHANGELOG.json contains invalid JSON. Please fix and try again.`,
+ `Found package '${name}' with doc commands but no corresponding docSegment in monorepo_scripts
+package.ts. Please add an entry for it and try again.`,
);
}
+ const link = `${constants.stagingWebsite}/docs/${docSegmentIfExists}`;
+ // tslint:disable-next-line:no-floating-promises
+ opn(link);
+ });
+
+ await confirmAsync('Do all the doc pages render properly? (y/n)');
+}
+
+async function pushChangelogsToGithubAsync(): Promise<void> {
+ await execAsync(`git add . --all`, { cwd: constants.monorepoRootPath });
+ await execAsync(`git commit -m "Updated CHANGELOGS"`, { cwd: constants.monorepoRootPath });
+ await execAsync(`git push`, { cwd: constants.monorepoRootPath });
+ utils.log(`Pushed CHANGELOG updates to Github`);
+}
- const currentVersion = lernaPackage.package.version;
- const shouldAddNewEntry = shouldAddNewChangelogEntry(changelogs);
+async function updateChangeLogsAsync(updatedPublicPackages: Package[]): Promise<PackageToNextVersion> {
+ const packageToNextVersion: PackageToNextVersion = {};
+ for (const pkg of updatedPublicPackages) {
+ const packageName = pkg.packageJson.name;
+ let changelog = changelogUtils.getChangelogOrCreateIfMissing(packageName, pkg.location);
+
+ const currentVersion = pkg.packageJson.version;
+ const shouldAddNewEntry = changelogUtils.shouldAddNewChangelogEntry(
+ pkg.packageJson.name,
+ currentVersion,
+ changelog,
+ );
if (shouldAddNewEntry) {
// Create a new entry for a patch version with generic changelog entry.
- const nextPatchVersion = utils.getNextPatchVersion(currentVersion);
- const newChangelogEntry: Changelog = {
+ 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',
},
],
};
- changelogs = [newChangelogEntry, ...changelogs];
- packageToVersionChange[packageName] = semverDiff(currentVersion, nextPatchVersion);
+ changelog = [newChangelogEntry, ...changelog];
+ packageToNextVersion[packageName] = nextPatchVersionIfValid;
} else {
// Update existing entry with timestamp
- const lastEntry = changelogs[0];
+ const lastEntry = changelog[0];
if (_.isUndefined(lastEntry.timestamp)) {
lastEntry.timestamp = TODAYS_TIMESTAMP;
}
// Check version number is correct.
const proposedNextVersion = lastEntry.version;
lastEntry.version = updateVersionNumberIfNeeded(currentVersion, proposedNextVersion);
- changelogs[0] = lastEntry;
- packageToVersionChange[packageName] = semverDiff(currentVersion, lastEntry.version);
+ changelog[0] = lastEntry;
+ packageToNextVersion[packageName] = lastEntry.version;
}
// Save updated CHANGELOG.json
- fs.writeFileSync(changelogJSONPath, JSON.stringify(changelogs, null, 4));
- await utils.prettifyAsync(changelogJSONPath, constants.monorepoRootPath);
+ await changelogUtils.writeChangelogJsonFileAsync(pkg.location, changelog);
utils.log(`${packageName}: Updated CHANGELOG.json`);
// Generate updated CHANGELOG.md
- const changelogMd = generateChangelogMd(changelogs);
- const changelogMdPath = path.join(lernaPackage.location, 'CHANGELOG.md');
- fs.writeFileSync(changelogMdPath, changelogMd);
- await utils.prettifyAsync(changelogMdPath, constants.monorepoRootPath);
+ const changelogMd = changelogUtils.generateChangelogMd(changelog);
+ await changelogUtils.writeChangelogMdFileAsync(pkg.location, changelogMd);
utils.log(`${packageName}: Updated CHANGELOG.md`);
}
- if (!IS_DRY_RUN) {
- await execAsync(`git add . --all`, { cwd: constants.monorepoRootPath });
- await execAsync(`git commit -m "Updated CHANGELOGS"`, { cwd: constants.monorepoRootPath });
- await execAsync(`git push`, { cwd: constants.monorepoRootPath });
- utils.log(`Pushed CHANGELOG updates to Github`);
- }
-
- utils.log('Version updates to apply:');
- _.each(packageToVersionChange, (versionChange: string, packageName: string) => {
- utils.log(`${packageName} -> ${versionChange}`);
- });
- utils.log(`Calling 'lerna publish'...`);
- await lernaPublishAsync(packageToVersionChange);
-})().catch(err => {
- utils.log(err);
- process.exit(1);
-});
-
-async function lernaPublishAsync(packageToVersionChange: { [name: string]: string }) {
- // HACK: Lerna publish does not provide a way to specify multiple package versions via
- // flags so instead we need to interact with their interactive prompt interface.
- const child = spawn('lerna', ['publish', '--registry=https://registry.npmjs.org/'], {
- cwd: constants.monorepoRootPath,
- });
- let shouldPrintOutput = false;
- child.stdout.on('data', (data: Buffer) => {
- const output = data.toString('utf8');
- if (shouldPrintOutput) {
- utils.log(output);
- }
- const isVersionPrompt = _.includes(output, 'Select a new version');
- if (isVersionPrompt) {
- const outputStripLeft = output.split('new version for ')[1];
- const packageName = outputStripLeft.split(' ')[0];
- let versionChange = packageToVersionChange[packageName];
- const isPrivatePackage = _.isUndefined(versionChange);
- if (isPrivatePackage) {
- versionChange = 'patch'; // Always patch updates to private packages.
- }
- const semVerIndex = semverNameToIndex[versionChange];
- child.stdin.write(`${semVerIndex}\n`);
- }
- const isFinalPrompt = _.includes(output, 'Are you sure you want to publish the above changes?');
- if (isFinalPrompt && !IS_DRY_RUN) {
- child.stdin.write(`y\n`);
- // After confirmations, we want to print the output to watch the `lerna publish` command
- shouldPrintOutput = true;
- } else if (isFinalPrompt && IS_DRY_RUN) {
- utils.log(
- `Submitted all versions to Lerna but since this is a dry run, did not confirm. You need to CTRL-C to exit.`,
- );
- }
- });
+ return packageToNextVersion;
}
-async function getPublicLernaUpdatedPackagesAsync(): Promise<UpdatedPackage[]> {
- const result = await execAsync(`${LERNA_EXECUTABLE} updated --json`, { cwd: constants.monorepoRootPath });
- const updatedPackages = JSON.parse(result.stdout);
- const updatedPublicPackages = _.filter(updatedPackages, updatedPackage => !updatedPackage.private);
- return updatedPublicPackages;
+async function lernaPublishAsync(packageToNextVersion: { [name: string]: string }): Promise<void> {
+ const packageVersionString = _.map(packageToNextVersion, (nextVersion: string, packageName: string) => {
+ return `${packageName}@${nextVersion}`;
+ }).join(',');
+ let lernaPublishCmd = `node ${constants.lernaExecutable} publish --cdVersions=${packageVersionString} --registry=${
+ configs.NPM_REGISTRY_URL
+ } --yes`;
+ if (configs.IS_LOCAL_PUBLISH) {
+ lernaPublishCmd += ` --skip-git`;
+ }
+ utils.log('Lerna is publishing...');
+ await execAsync(lernaPublishCmd, { cwd: constants.monorepoRootPath });
}
-function updateVersionNumberIfNeeded(currentVersion: string, proposedNextVersion: string) {
+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;
}
-
-function getChangelogJSONOrCreateIfMissing(packageName: string, changelogPath: string): string {
- let changelogJSON: string;
- try {
- changelogJSON = fs.readFileSync(changelogPath, 'utf-8');
- return changelogJSON;
- } catch (err) {
- // If none exists, create new, empty one.
- const emptyChangelogJSON = JSON.stringify([], null, 4);
- fs.writeFileSync(changelogPath, emptyChangelogJSON);
- return emptyChangelogJSON;
- }
-}
-
-function shouldAddNewChangelogEntry(changelogs: Changelog[]): boolean {
- if (_.isEmpty(changelogs)) {
- return true;
- }
- const lastEntry = changelogs[0];
- return !!lastEntry.isPublished;
-}
-
-function generateChangelogMd(changelogs: Changelog[]): string {
- let changelogMd = `<!--
-This file is auto-generated using the monorepo-scripts package. Don't edit directly.
-Edit the package's CHANGELOG.json file only.
--->
-
-CHANGELOG
- `;
-
- _.each(changelogs, changelog => {
- if (_.isUndefined(changelog.timestamp)) {
- throw new Error(
- 'All CHANGELOG.json entries must be updated to include a timestamp before generating their MD version',
- );
- }
- const date = moment(`${changelog.timestamp}`, 'X').format('MMMM D, YYYY');
- const title = `\n## v${changelog.version} - _${date}_\n\n`;
- changelogMd += title;
-
- let changes = '';
- _.each(changelog.changes, change => {
- let line = ` * ${change.note}`;
- if (!_.isUndefined(change.pr)) {
- line += ` (#${change.pr})`;
- }
- line += '\n';
- changes += line;
- });
- changelogMd += `${changes}`;
- });
-
- return changelogMd;
-}
diff --git a/packages/monorepo-scripts/src/test_installation.ts b/packages/monorepo-scripts/src/test_installation.ts
index 195b64b2a..87c4ad1d7 100644
--- a/packages/monorepo-scripts/src/test_installation.ts
+++ b/packages/monorepo-scripts/src/test_installation.ts
@@ -1,58 +1,150 @@
#!/usr/bin/env node
import * as fs from 'fs';
-import lernaGetPackages = require('lerna-get-packages');
import * as _ from 'lodash';
+import * as mkdirp from 'mkdirp';
import * as path from 'path';
import { exec as execAsync } from 'promisify-child-process';
import * as rimraf from 'rimraf';
+import { promisify } from 'util';
-import { utils } from './utils';
+import { Package } from './types';
+import { utils } from './utils/utils';
+
+// Packages might not be runnable if they are command-line tools or only run in browsers.
+const UNRUNNABLE_PACKAGES = [
+ '@0xproject/abi-gen',
+ '@0xproject/sra-report',
+ '@0xproject/react-shared',
+ '@0xproject/react-docs',
+];
+
+const mkdirpAsync = promisify(mkdirp);
+const rimrafAsync = promisify(rimraf);
+const writeFileAsync = promisify(fs.writeFile);
+
+interface PackageErr {
+ packageName: string;
+ error: ExecError;
+}
+
+interface ExecError {
+ message: string;
+ stack: string;
+ stderr: string;
+ stdout: string;
+}
+
+// returns the index for the given package name.
+function findPackageIndex(packages: Package[], packageName: string): number {
+ return _.findIndex(packages, pkg => pkg.packageJson.name === packageName);
+}
+
+function logIfDefined(x: any): void {
+ if (!_.isUndefined(x)) {
+ utils.log(x);
+ }
+}
(async () => {
+ const IS_LOCAL_PUBLISH = process.env.IS_LOCAL_PUBLISH === 'true';
+ const registry = IS_LOCAL_PUBLISH ? 'http://localhost:4873/' : 'https://registry.npmjs.org/';
const monorepoRootPath = path.join(__dirname, '../../..');
- const lernaPackages = lernaGetPackages(monorepoRootPath);
+ const packages = utils.getPackages(monorepoRootPath);
const installablePackages = _.filter(
- lernaPackages,
- lernaPackage =>
- !lernaPackage.package.private &&
- !_.isUndefined(lernaPackage.package.main) &&
- lernaPackage.package.main.endsWith('.js'),
+ packages,
+ pkg => !pkg.packageJson.private && !_.isUndefined(pkg.packageJson.main) && pkg.packageJson.main.endsWith('.js'),
);
- for (const installableLernaPackage of installablePackages) {
- const packagePath = installableLernaPackage.location;
- const packageName = installableLernaPackage.package.name;
- utils.log(`Testing ${packageName}`);
- let result = await execAsync('npm pack', { cwd: packagePath });
- const packedPackageFileName = result.stdout.trim();
- const testDirectory = path.join(monorepoRootPath, '../test-env');
- fs.mkdirSync(testDirectory);
- result = await execAsync('yarn init --yes', { cwd: testDirectory });
- utils.log(`Installing ${packedPackageFileName}`);
- result = await execAsync(`yarn add ${packagePath}/${packedPackageFileName}`, { cwd: testDirectory });
- const indexFilePath = path.join(testDirectory, 'index.ts');
- fs.writeFileSync(indexFilePath, `import * as Package from '${packageName}'`);
- const tsConfig = {
- compilerOptions: {
- typeRoots: ['node_modules/@0xproject/typescript-typings/types', 'node_modules/@types'],
- module: 'commonjs',
- target: 'es5',
- lib: ['es2017', 'dom'],
- declaration: true,
- noImplicitReturns: true,
- pretty: true,
- strict: true,
- },
- include: ['index.ts'],
- };
- const tsconfigFilePath = path.join(testDirectory, 'tsconfig.json');
- fs.writeFileSync(tsconfigFilePath, JSON.stringify(tsConfig, null, 4));
- utils.log(`Compiling ${packageName}`);
- await execAsync('../node_modules/typescript/bin/tsc', { cwd: testDirectory });
- utils.log(`Successfully compiled with ${packageName} as a dependency`);
- rimraf.sync(testDirectory);
+ utils.log('Testing packages:');
+ _.map(installablePackages, pkg => utils.log(`* ${pkg.packageJson.name}`));
+ // Run all package tests asynchronously and push promises into an array so
+ // we can wait for all of them to resolve.
+ const promises: Array<Promise<void>> = [];
+ const errors: PackageErr[] = [];
+ for (const installablePackage of installablePackages) {
+ const packagePromise = testInstallPackageAsync(monorepoRootPath, registry, installablePackage).catch(error => {
+ errors.push({ packageName: installablePackage.packageJson.name, error });
+ });
+ promises.push(packagePromise);
+ }
+ await Promise.all(promises);
+ if (errors.length > 0) {
+ // We sort error messages according to package topology so that we can
+ // them in a more intuitive order. E.g. if package A has an error and
+ // package B imports it, the tests for both package A and package B will
+ // fail. But package B only fails because of an error in package A.
+ // Since the error in package A is the root cause, we log it first.
+ const topologicallySortedPackages = utils.getTopologicallySortedPackages(monorepoRootPath);
+ const topologicallySortedErrors = _.sortBy(errors, packageErr =>
+ findPackageIndex(topologicallySortedPackages, packageErr.packageName),
+ );
+ _.forEach(topologicallySortedErrors, packageError => {
+ utils.log(`ERROR in package ${packageError.packageName}:`);
+ logIfDefined(packageError.error.message);
+ logIfDefined(packageError.error.stderr);
+ logIfDefined(packageError.error.stdout);
+ logIfDefined(packageError.error.stack);
+ });
+ process.exit(0);
}
})().catch(err => {
- utils.log(err.stdout);
- process.exit(1);
+ utils.log(`Unexpected error: ${err.message}`);
+ process.exit(0);
});
+
+async function testInstallPackageAsync(
+ monorepoRootPath: string,
+ registry: string,
+ installablePackage: Package,
+): Promise<void> {
+ const changelogPath = path.join(installablePackage.location, 'CHANGELOG.json');
+ const lastChangelogVersion = JSON.parse(fs.readFileSync(changelogPath).toString())[0].version;
+ const packageName = installablePackage.packageJson.name;
+ utils.log(`Testing ${packageName}@${lastChangelogVersion}`);
+ const packageDirName = path.join(...(packageName + '-test').split('/'));
+ const testDirectory = path.join(
+ monorepoRootPath,
+ 'packages',
+ 'monorepo-scripts',
+ '.installation-test',
+ packageDirName,
+ );
+ await rimrafAsync(testDirectory);
+ await mkdirpAsync(testDirectory);
+ await execAsync('yarn init --yes', { cwd: testDirectory });
+ const npmrcFilePath = path.join(testDirectory, '.npmrc');
+ await writeFileAsync(npmrcFilePath, `registry=${registry}`);
+ utils.log(`Installing ${packageName}@${lastChangelogVersion}`);
+ await execAsync(`npm install --save ${packageName}@${lastChangelogVersion} --registry=${registry}`, {
+ cwd: testDirectory,
+ });
+ const indexFilePath = path.join(testDirectory, 'index.ts');
+ await writeFileAsync(indexFilePath, `import * as Package from '${packageName}';\nconsole.log(Package);\n`);
+ const tsConfig = {
+ compilerOptions: {
+ typeRoots: ['node_modules/@0xproject/typescript-typings/types', 'node_modules/@types'],
+ module: 'commonjs',
+ target: 'es5',
+ lib: ['es2017', 'dom'],
+ declaration: true,
+ noImplicitReturns: true,
+ pretty: true,
+ strict: true,
+ },
+ include: ['index.ts'],
+ };
+ const tsconfigFilePath = path.join(testDirectory, 'tsconfig.json');
+ await writeFileAsync(tsconfigFilePath, JSON.stringify(tsConfig, null, '\t'));
+ utils.log(`Compiling ${packageName}`);
+ const tscBinaryPath = path.join(monorepoRootPath, './node_modules/typescript/bin/tsc');
+ await execAsync(tscBinaryPath, { cwd: testDirectory });
+ utils.log(`Successfully compiled with ${packageName} as a dependency`);
+ const isUnrunnablePkg = _.includes(UNRUNNABLE_PACKAGES, packageName);
+ if (!isUnrunnablePkg) {
+ const transpiledIndexFilePath = path.join(testDirectory, 'index.js');
+ utils.log(`Running test script with ${packageName} imported`);
+ await execAsync(`node ${transpiledIndexFilePath}`);
+ utils.log(`Successfilly ran test script with ${packageName} imported`);
+ }
+ await rimrafAsync(testDirectory);
+}
diff --git a/packages/monorepo-scripts/src/types.ts b/packages/monorepo-scripts/src/types.ts
index 7adec202f..d9e1dfabb 100644
--- a/packages/monorepo-scripts/src/types.ts
+++ b/packages/monorepo-scripts/src/types.ts
@@ -4,21 +4,48 @@ export interface UpdatedPackage {
private: boolean;
}
-export interface Changes {
+export interface Change {
note: string;
pr?: number;
}
-export interface Changelog {
+export type Changelog = VersionChangelog[];
+
+export interface VersionChangelog {
timestamp?: number;
version: string;
- changes: Changes[];
- isPublished?: boolean;
+ changes: Change[];
+}
+
+export interface PackageToNextVersion {
+ [name: string]: string;
+}
+
+export interface PackageRegistryJson {
+ versions: {
+ [version: string]: any;
+ };
+ time: {
+ [version: string]: string;
+ };
+}
+
+export interface GitTagsByPackageName {
+ [packageName: string]: string[];
+}
+
+export interface PackageJSON {
+ private?: boolean;
+ version: string;
+ name: string;
+ main?: string;
+ scripts?: { [command: string]: string };
+ config?: {
+ additionalTsTypings?: string[];
+ };
}
-export enum SemVerIndex {
- Invalid,
- Patch,
- Minor,
- Major,
+export interface Package {
+ location: string;
+ packageJson: PackageJSON;
}
diff --git a/packages/monorepo-scripts/src/utils.ts b/packages/monorepo-scripts/src/utils.ts
deleted file mode 100644
index 9aa37e272..000000000
--- a/packages/monorepo-scripts/src/utils.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import * as _ from 'lodash';
-import { exec as execAsync, spawn } from 'promisify-child-process';
-
-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) {
- await execAsync(`prettier --write ${filePath} --config .prettierrc`, {
- cwd,
- });
- },
-};
diff --git a/packages/monorepo-scripts/src/utils/changelog_utils.ts b/packages/monorepo-scripts/src/utils/changelog_utils.ts
new file mode 100644
index 000000000..4781b3b7d
--- /dev/null
+++ b/packages/monorepo-scripts/src/utils/changelog_utils.ts
@@ -0,0 +1,106 @@
+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 = `
+<!--
+changelogUtils.file is auto-generated using the monorepo-scripts package. Don't edit directly.
+Edit the package's CHANGELOG.json file only.
+-->
+
+CHANGELOG
+`;
+
+export const changelogUtils = {
+ getChangelogMdTitle(versionChangelog: VersionChangelog): string {
+ if (_.isUndefined(versionChangelog.timestamp)) {
+ throw new Error(
+ 'All CHANGELOG.json entries must be updated to include a timestamp before generating their MD version',
+ );
+ }
+ const date = moment(`${versionChangelog.timestamp}`, 'X').format('MMMM D, YYYY');
+ const title = `\n## v${versionChangelog.version} - _${date}_\n\n`;
+ return title;
+ },
+ getChangelogMdChange(change: Change): string {
+ let line = ` * ${change.note}`;
+ if (!_.isUndefined(change.pr)) {
+ line += ` (#${change.pr})`;
+ }
+ return line;
+ },
+ generateChangelogMd(changelog: Changelog): string {
+ let changelogMd = CHANGELOG_MD_HEADER;
+ _.each(changelog, versionChangelog => {
+ const title = changelogUtils.getChangelogMdTitle(versionChangelog);
+ changelogMd += title;
+ const changelogVersionLines = _.map(
+ versionChangelog.changes,
+ changelogUtils.getChangelogMdChange.bind(changelogUtils),
+ );
+ changelogMd += `${_.join(changelogVersionLines, '\n')}`;
+ });
+
+ return changelogMd;
+ },
+ 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 = changelogUtils.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 changelogUtils.prettifyAsync(changelogJSONPath, constants.monorepoRootPath);
+ },
+ async writeChangelogMdFileAsync(packageLocation: string, changelogMdString: string): Promise<void> {
+ const changelogMarkdownPath = path.join(packageLocation, 'CHANGELOG.md');
+ fs.writeFileSync(changelogMarkdownPath, changelogMdString);
+ await changelogUtils.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/configs.ts b/packages/monorepo-scripts/src/utils/configs.ts
new file mode 100644
index 000000000..e579bdb7c
--- /dev/null
+++ b/packages/monorepo-scripts/src/utils/configs.ts
@@ -0,0 +1,8 @@
+const IS_LOCAL_PUBLISH = process.env.IS_LOCAL_PUBLISH === 'true';
+const LOCAL_NPM_REGISTRY_URL = 'http://localhost:4873';
+const REMOTE_NPM_REGISTRY_URL = 'https://registry.npmjs.org';
+
+export const configs = {
+ IS_LOCAL_PUBLISH,
+ NPM_REGISTRY_URL: IS_LOCAL_PUBLISH ? LOCAL_NPM_REGISTRY_URL : REMOTE_NPM_REGISTRY_URL,
+};
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..363e31fbb
--- /dev/null
+++ b/packages/monorepo-scripts/src/utils/npm_utils.ts
@@ -0,0 +1,29 @@
+import 'isomorphic-fetch';
+import * as _ from 'lodash';
+
+import { PackageRegistryJson } from '../types';
+
+import { configs } from './configs';
+
+const SUCCESS_STATUS = 200;
+const NOT_FOUND_STATUS = 404;
+
+export const npmUtils = {
+ async getPackageRegistryJsonIfExistsAsync(packageName: string): Promise<PackageRegistryJson | undefined> {
+ const url = `${configs.NPM_REGISTRY_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 npm registry 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
new file mode 100644
index 000000000..26ac801bd
--- /dev/null
+++ b/packages/monorepo-scripts/src/utils/utils.ts
@@ -0,0 +1,167 @@
+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) {
+ utils.log(`Couldn't find a 'package.json' for ${subpackageName}. Skipping.`);
+ }
+ }
+ }
+ return packages;
+ },
+ async getUpdatedPackagesAsync(shouldIncludePrivate: boolean): Promise<Package[]> {
+ 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<UpdatedPackage[]> {
+ 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<string> {
+ 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<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 tag`, {
+ 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> {
+ 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<void> {
+ 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}`);
+ },
+};