import { CliTree, CliTrees, objectPropertiesToCliTrees, writeCliTree } from '@cirrusct/cli-utils'; import { Logger } from '@cirrusct/logging'; import { BuildCommandOptions } from '@cirrusct/mr-build'; import { createError, MrCommandHandler, MrCommandHandlerArgs, MrCommandResults, MrPackage, MrProject, } from '@cirrusct/mr-core'; import * as semver from 'semver'; import { isBoolean } from 'util'; import { CommandName } from './manifest'; import { shellPublishPackage } from './shellExec'; import { PublishCommandConfigSettings, PublishCommandOptions, PublishVersionMode } from './types'; interface PublishInfo { mrPackage: MrPackage; targetVersion: string; publishedVersion: string; } const DefaultVersionMode: PublishVersionMode = 'independent'; const getTargetVersion = async ( monoRepo: MrProject, options: PublishCommandOptions, mode: PublishVersionMode, entryPackage?: MrPackage ) => { const pkg = mode === 'common' ? monoRepo.rootPackage : entryPackage; if (options.targetVersion) { return options.targetVersion; } else if (options.bump) { // bumping current version in package.json // considering bumping against publish version, but would need to re-query that here or re-factor to a more efficient // way to avoid query more than once const release = isBoolean(options.bump) && options.bump === true ? 'patch' : options.bump; return semver.inc(pkg.version, release); } return pkg.version; }; const writePackageVersion = (pkg: MrPackage, version: string) => { const writer = pkg.createWriter(); writer.version = version; writer.write(); }; const getReferencedByDeep = (pkg: MrPackage): MrPackage[] => { const refMap: { [name: string]: MrPackage } = {}; pkg.referencedBylocalPackages.forEach(r => { refMap[r.name] = r; const refRefs = getReferencedByDeep(r); refRefs.forEach(rr => { refMap[rr.name] = rr; }); }); return pkg.project.sortPackagesMap(refMap); }; const writePublishInfo = (publishInfoMap: { [name: string]: PublishInfo }, logger: Logger, isDryRun = false) => { const keys = Object.keys(publishInfoMap); // update package dependencies to new published version of the dep for (const key of keys) { const pi = publishInfoMap[key]; const pkg = publishInfoMap[key].mrPackage; // Determine any local dependencies for this package that are in the queue to be publishsed, // so we can update the package.json to reference the new version const depRefsToUpdate = pkg.localDirectDependencyPackages.reduce((acc, cur) => { if (publishInfoMap[cur.name]) { const dep = pkg.getDependency(cur.name); const depPii = publishInfoMap[cur.name]; const v = dep.getTargetRangeDeclaration(depPii.targetVersion); acc.push({ ...depPii, targetVersionRange: v }); } return acc; }, []); logger.debug( { package: pkg.name, targetVersion: pi.targetVersion, publishedVersion: pi.publishedVersion, dryRun: isDryRun, }, 'Writing package.json with updated Version info' ); logger.trace( { package: pkg.name, changedDependencies: depRefsToUpdate .map(drtu => `${drtu.mrPackage.name}@${drtu.targetVersionRange}`) .join(', '), dryRun: isDryRun, }, 'Writing package.json dependencies' ); if (!isDryRun) { const writer = pkg.createWriter(); writer.version = pi.targetVersion; if (depRefsToUpdate.length > 0) { depRefsToUpdate.forEach(drtu => { writer.setDependencyVersion(drtu.mrPackage.name, drtu.targetVersionRange); }); } // write package.json writer.write(); } else { logger.info({ package: pkg.name, version: pi.targetVersion }, 'Writing package.json (Dry Run)'); if (depRefsToUpdate.length > 0) { logger.info(`Writing ${pkg.name} Dependencies (Dry Run)`); depRefsToUpdate.forEach(drtu => { logger.debug( { package: drtu.mrPackage.name, version: drtu.targetVersionRange }, `Writing Dependency (Dry Run)` ); }); } } } }; const getCommonPublishInfo = async ( args: MrCommandHandlerArgs ): Promise<{ [name: string]: PublishInfo }> => { const { options = {}, logger, session } = args; const { project } = session; const targetVersion = await getTargetVersion(project, options, 'common'); // filter private packages or where configured to ignore const packages = project.packages.filter(p => { if (p.isPrivate) { return false; } const cmdConfig = p.config.getCommandConfig(CommandName); return !(cmdConfig && cmdConfig.value.ignore === true); }); const publishInfo: { [name: string]: PublishInfo } = {}; // Verify published version and build list of packages to publish for (const p of packages) { // make sure none of the packages has a published version greater than target, throw if so const publishedVersion = await p.getLatestPublishedVersion(); if (!publishedVersion || semver.lt(publishedVersion, targetVersion)) { publishInfo[p.name] = { mrPackage: p, targetVersion, publishedVersion, }; } else if (semver.gt(publishedVersion, targetVersion)) { throw createError( `Published version of ${publishedVersion} is greater than target version ${targetVersion} for package '${p.name}'` ); } } return publishInfo; }; const getIndependendPublishInfo = async ( args: MrCommandHandlerArgs ): Promise<{ [name: string]: PublishInfo }> => { const { options = {}, logger, packages, session } = args; const { project } = session; const publishInfo: { [name: string]: PublishInfo } = {}; const addPackagesPi = async (pkgs: MrPackage[], publishOptions: PublishCommandOptions = { bump: 'patch' }) => { for (const p of pkgs) { if (!publishInfo[p.name]) { const targetVersion = await getTargetVersion(project, publishOptions, 'independent', p); const publishedVersion = await p.getLatestPublishedVersion(); if (publishedVersion && semver.lte(targetVersion, publishedVersion)) { throw createError( `Published version of ${publishedVersion} is greater than or equal to target version ${targetVersion} for package: ${p.name}` ); } publishInfo[p.name] = { mrPackage: p, targetVersion, publishedVersion, }; } } }; // collect publish info for each await addPackagesPi(packages, options); if (options.publishRefBy) { // get all mr packages that reference a package we're publishing const referencedByPkgs = packages.reduce((acc, cur) => { acc = acc.concat(getReferencedByDeep(cur)); return acc; }, []); // add referencedBy packages to be published; we'll bump patch version for these await addPackagesPi(referencedByPkgs, { bump: 'patch' }); } return publishInfo; }; export const handleCommand: MrCommandHandler = async args => { debugger; const { session, mrService } = args; const { project } = session; const commandConfig = project.config.getCommandConfig(CommandName); const commandConfigSettings = commandConfig ? commandConfig.value : {}; const versionMode = commandConfigSettings.versionMode || DefaultVersionMode; const { options, logger } = args; logger.info( { versionMode, ...options, }, 'Publishing' ); const publishInfo: { [name: string]: PublishInfo } = versionMode === 'common' ? await getCommonPublishInfo(args) : await getIndependendPublishInfo(args); const isDryRun = !!options.dryRun; // write package.json files writePublishInfo(publishInfo, logger, isDryRun); logger.info('Building Packages to publish'); // build all packages first, so if any fail we should error out for (const piKey of Object.keys(publishInfo)) { const pi = publishInfo[piKey]; if (isDryRun) { logger.info(`Building package '${pi.mrPackage.name}' (dry run)`); } else { // build package await mrService.buildService.buildPackage(pi.mrPackage, undefined, { shellTsc: options.shellTsc, }); } } logger.info('Publishing Packages'); for (const piKey of Object.keys(publishInfo)) { const pi = publishInfo[piKey]; // publish it await shellPublishPackage( pi.mrPackage, pi.targetVersion, logger.child({ runner: 'shellPublishRunner' }), isDryRun ); } // Update root package.json if using common version if (versionMode === 'common') { const targetVersion = await getTargetVersion(project, options, 'common'); logger.debug( { rootPackage: project.rootPackage.name, version: targetVersion, isDryRun, }, 'Writing root package version' ); if (!isDryRun) { writePackageVersion(project.rootPackage, targetVersion); } } };