#!/usr/bin/env node 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 semverDiff = require('semver-diff'); import semverSort = require('semver-sort'); import { Changelog, Changes, UpdatedPackage } from './types'; import { utils } from './utils'; const MONOREPO_ROOT_PATH = path.join(__dirname, '../../..'); const TODAYS_TIMESTAMP = moment().unix(); (async () => { const updatedPublicPackages = await getPublicLernaUpdatedPackagesAsync(); const updatedPackageNames = _.map(updatedPublicPackages, pkg => pkg.name); const allLernaPackages = lernaGetPackages(MONOREPO_ROOT_PATH); const relevantLernaPackages = _.filter(allLernaPackages, pkg => { return _.includes(updatedPackageNames, pkg.package.name); }); const packageToVersionChange: { [name: string]: string } = {}; _.each(relevantLernaPackages, lernaPackage => { 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) { throw new Error( `${lernaPackage.package.name}'s CHANGELOG.json contains invalid JSON. Please fix and try again.`, ); } const currentVersion = lernaPackage.package.version; const shouldAddNewEntry = shouldAddNewChangelogEntry(changelogs); if (shouldAddNewEntry) { // Create a new entry for a patch version with generic changelog entry. const nextPatchVersion = utils.getNextPatchVersion(currentVersion); const newChangelogEntry: Changelog = { timestamp: TODAYS_TIMESTAMP, version: nextPatchVersion, changes: [ { note: 'Dependencies updated', }, ], }; changelogs = [newChangelogEntry, ...changelogs]; packageToVersionChange[packageName] = semverDiff(currentVersion, nextPatchVersion); } else { // Update existing entry with timestamp const lastEntry = changelogs[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); } // Save updated CHANGELOG.json fs.writeFileSync(changelogJSONPath, JSON.stringify(changelogs, null, '\t')); // Generate updated CHANGELOG.md const changelogMd = generateChangelogMd(changelogs); const changelogMdPath = path.join(lernaPackage.location, 'CHANGELOG.md'); fs.writeFileSync(changelogMdPath, changelogMd); }); utils.log(`All CHANGELOGS successfully updated. Please commit and push these changes to development.`); utils.log(`New package versions are:`); _.each(packageToVersionChange, (versionChange: string, pkgName: string) => { utils.log(`${pkgName}: ${versionChange}`); }); })().catch(err => { utils.log(err); process.exit(1); }); async function getPublicLernaUpdatedPackagesAsync(): Promise { const result = await execAsync(`./node_modules/lerna/bin/lerna.js updated --json`, { cwd: MONOREPO_ROOT_PATH }); const updatedPackages = JSON.parse(result.stdout); const updatedPublicPackages = _.filter(updatedPackages, updatedPackage => !updatedPackage.private); return updatedPublicPackages; } function updateVersionNumberIfNeeded(currentVersion: string, proposedNextVersion: string) { if (proposedNextVersion === currentVersion) { return utils.getNextPatchVersion(currentVersion); } const sortedVersions = semverSort.desc([proposedNextVersion, currentVersion]); if (sortedVersions[0] !== proposedNextVersion) { return utils.getNextPatchVersion(currentVersion); } 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([]); 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 = ` CHANGELOG `; _.each(changelogs, changelog => { 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; }