UNPKG

@better-builds/lets-version

Version:

A package that reads your conventional commits and git history and recommends (or applies) a SemVer version bump for you

143 lines (142 loc) 7.98 kB
import appRootPath from 'app-root-path'; import semver from 'semver'; import semverUtils from 'semver-utils'; import { fixCWD } from './cwd.js'; import { gitCurrentSHA } from './git.js'; import { buildLocalDependencyGraph } from './localDependencyGraph.js'; import { BumpRecommendation, BumpType, ReleaseAsPresets } from './types.js'; import { isPackageJSONDependencyKeySupported } from './util.js'; /** * Given a parsed packageInfo object and some parameters, * performs a semver.inc() */ export async function getBumpRecommendationForPackageInfo(packageInfo, from, bumpType, parentBump, releaseAs, preid, uniqify = false, cwd = appRootPath.toString()) { const fixedCWD = fixCWD(cwd); const isExactRelease = Boolean(semver.coerce(releaseAs)); let isPrerelease = false; let bumpTypeToUse = bumpType; if (isExactRelease) bumpTypeToUse = BumpType.EXACT; else { switch (releaseAs) { case ReleaseAsPresets.ALPHA: case ReleaseAsPresets.BETA: isPrerelease = true; bumpTypeToUse = BumpType.PRERELEASE; preid = preid || releaseAs; break; case ReleaseAsPresets.MAJOR: bumpTypeToUse = BumpType.MAJOR; break; case ReleaseAsPresets.MINOR: bumpTypeToUse = BumpType.MINOR; break; case ReleaseAsPresets.PATCH: bumpTypeToUse = BumpType.PATCH; break; default: break; } } if (preid) isPrerelease = true; const newBump = isExactRelease ? releaseAs : semver.inc(packageInfo.version, isPrerelease ? 'prerelease' : bumpTypeToUse === BumpType.PATCH ? 'patch' : bumpTypeToUse === BumpType.MINOR ? 'minor' : 'major', undefined, preid); const fromToUse = isPrerelease || releaseAs ? packageInfo.version : from; let to = Boolean(fromToUse) && newBump ? newBump : packageInfo.version; if (uniqify) { to += `.${await gitCurrentSHA(fixedCWD)}`; } return new BumpRecommendation(packageInfo, fromToUse, to, bumpTypeToUse, parentBump); } /** * Applies bumps to top-level packages, then attempts to recursively * synchronize package versions and applies bumps if a package hasn't already * been bumped (but might receive one as a result from this operation) */ export async function synchronizeBumps(bumps, bumpsByPackageName, allPackages, releaseAs, preid, uniqify, saveExact, updatePeer, updateOptional, cwd = appRootPath.toString()) { const fixedCWD = fixCWD(cwd); const clonedBumpsByPackageName = new Map(bumpsByPackageName.entries()); const packagesByName = new Map(bumps.map(b => [b.packageInfo.name, b.packageInfo])); const depGraph = await buildLocalDependencyGraph(allPackages); for (const bump of clonedBumpsByPackageName.values()) { const toWrite = packagesByName.get(bump.packageInfo.name); if (!toWrite) continue; const [existingSemver] = semverUtils.parseRange(toWrite.version); const existingTagDoesNotMatch = existingSemver?.release ? !existingSemver.release.toLowerCase().includes(bump.bumpTypeName) : true; // we want to fully respect the prerelease or "releaseAs" changeover, // or fallback to using the version number that's largest const version = existingTagDoesNotMatch || semver.gt(bump.to, toWrite.version) ? bump.to : toWrite.version; bump.packageInfo.version = version; bump.packageInfo.pkg.version = version; // we have the dep graph. we need to find all the packages that have the "bump" // package as a dependency and apply at least the same bump as the parent // (or defer to whichever existing bump the child has, if it's larger) const dependents = depGraph.filter(node => node.deps.some(dep => dep.name === bump.packageInfo.name)); for (const dependent of dependents) { // we now need to update the version of the dep for each dependent, // ensuring we keep the correct semver range marker (if it's present) for (const dependentPjsonKey of Object.keys(dependent.pkg)) { if (!isPackageJSONDependencyKeySupported(dependentPjsonKey, updatePeer, updateOptional)) continue; for (const dependentDepName of Object.keys(dependent.pkg[dependentPjsonKey] ?? {})) { if (dependentDepName !== bump.packageInfo.name) continue; // @ts-expect-error - silence tsc because accessors here are safe, as we've already checked for key existence const existingdependentDepSemver = String(dependent.pkg[dependentPjsonKey][dependentDepName]); // we add some looseness for monorepos and package managers // that allow for prefixes syntaxes const semverRegexWithPrefixes = /^((catalog|workspace):)?(.*)$/; const [, prefix, , actualDeclaredSemver = ''] = semverRegexWithPrefixes.exec(existingdependentDepSemver) ?? []; const [semverDetails] = semverUtils.parseRange(actualDeclaredSemver); if (!prefix && !semverDetails) { throw new Error(`unable to synchronize deps because ${dependent.name} has a bad semver specified for ${dependentDepName} of ${existingdependentDepSemver}`); } const semverDetailsToUse = semverDetails ?? {}; const { operator = '' } = semverDetailsToUse; let operatorTouse = operator; const useExactVersion = releaseAs === ReleaseAsPresets.ALPHA || releaseAs === ReleaseAsPresets.BETA || preid || saveExact; if (useExactVersion) { operatorTouse = ''; } else if (operatorTouse && !operatorTouse.startsWith('>=') && !operatorTouse.startsWith('^') && !operatorTouse.startsWith('~')) { operatorTouse = '^'; } // a prefixed semver for use with certain package managers // might not translate into valid semver (it could be empty), // so we should only change the package.json file here // if it was an actual parsed thing if (semverDetails) { // @ts-expect-error - silence tsc because accessors here are safe, as we've already checked for key existence dependent.pkg[dependentPjsonKey][dependentDepName] = `${operatorTouse}${bump.to}`; } const existingChildBumpRec = bumpsByPackageName.get(dependent.name); const childBumpRec = await getBumpRecommendationForPackageInfo(dependent, dependent.version, Math.max(bump.type, existingChildBumpRec?.type ?? bump.type), bump, releaseAs, preid, uniqify, fixedCWD); clonedBumpsByPackageName.set(childBumpRec.packageInfo.name, childBumpRec); const recursedResults = await synchronizeBumps([childBumpRec], clonedBumpsByPackageName, allPackages, releaseAs, preid, uniqify, saveExact, updatePeer, updateOptional, fixedCWD); recursedResults.bumps.forEach(b => { clonedBumpsByPackageName.set(b.packageInfo.name, b); }); } } } } return { bumps: Array.from(clonedBumpsByPackageName.values()), bumpsByPackageName: clonedBumpsByPackageName, packages: Array.from(clonedBumpsByPackageName.values()).map(bump => bump.packageInfo), }; }