UNPKG

lerna

Version:

Lerna is a fast, modern build system for managing and publishing multiple JavaScript/TypeScript packages from the same repository

812 lines (807 loc) 28.1 kB
import { gitPush } from "./chunk-7CE6W7VR.js"; import { gitTag } from "./chunk-NLQX4D3A.js"; import { isAnythingCommitted } from "./chunk-UGM6LAJ4.js"; import { isBehindUpstream } from "./chunk-NFRKDJAE.js"; import { isBreakingChange } from "./chunk-BLBEQEOI.js"; import { makePromptVersion } from "./chunk-4SBEQS72.js"; import { remoteBranchExists } from "./chunk-WKFTBBVB.js"; import { updateLockfileVersion } from "./chunk-DZSBX7VM.js"; import { createRelease, createReleaseClient } from "./chunk-IKWGB6FU.js"; import { getCurrentBranch } from "./chunk-IXOVJCOO.js"; import { gitAdd } from "./chunk-3JVNTSAN.js"; import { gitCommit } from "./chunk-YNT6CFMU.js"; import { Command, ValidationError, applyBuildMetadata, checkWorkingTree, collectProjectUpdates, collectProjects, colorize, createRunner, exec, execPackageManager, execPackageManagerSync, formatJSON, getPackage, getPackagesForOption, npmlog_default, output, prereleaseIdFromVersion, promptConfirmation, recommendVersion, runProjectsTopologically, throwIfUncommitted, updateChangelog } from "./chunk-WC2B4V4E.js"; // libs/commands/version/src/index.ts import dedent from "dedent"; import execa from "execa"; import fs2 from "fs"; import minimatch from "minimatch"; import os from "os"; import pMap from "p-map"; import path2 from "path"; import semver from "semver"; // libs/commands/version/src/lib/update-bun-lockfile.ts import fs from "fs"; import path from "path"; var LOCKFILE_NAMES = ["bun.lockb", "bun.lock"]; var BACKUP_SUFFIX = ".lerna-backup"; async function updateBunLockfile({ rootPath, npmClientArgs, runScriptsOnLockfileUpdate, execOpts }) { const candidates = LOCKFILE_NAMES.map((name) => path.join(rootPath, name)); const staleLockfiles = candidates.filter((candidate) => fs.existsSync(candidate)); if (staleLockfiles.length === 0) { npmlog_default.verbose( "version", `No bun lockfile (${LOCKFILE_NAMES.join(", ")}) found at the repo root, skipping lockfile update` ); return []; } npmlog_default.verbose("version", `Updating root ${staleLockfiles.map((p) => path.basename(p)).join(", ")}`); const backups = []; try { for (const lockfilePath of staleLockfiles) { const backupPath = `${lockfilePath}${BACKUP_SUFFIX}`; fs.renameSync(lockfilePath, backupPath); backups.push([lockfilePath, backupPath]); } await execPackageManager( "bun", [ "install", "--lockfile-only", !runScriptsOnLockfileUpdate ? "--ignore-scripts" : "", ...npmClientArgs ].filter(Boolean), execOpts ); } catch (err) { restoreBackups(backups, candidates); npmlog_default.error( "version", "Failed to update the bun lockfile via `bun install --lockfile-only`. The original lockfile(s) have been restored. Ensure bun is installed and on your PATH." ); throw err; } for (const [, backupPath] of backups) { try { fs.unlinkSync(backupPath); } catch (cleanupErr) { npmlog_default.warn("version", `Could not remove lockfile backup ${backupPath}: ${cleanupErr}`); } } const changedFiles = new Set(staleLockfiles); for (const candidate of candidates) { if (fs.existsSync(candidate)) { changedFiles.add(candidate); } } return Array.from(changedFiles); } function restoreBackups(backups, candidates) { try { for (const candidate of candidates) { if (fs.existsSync(candidate)) { fs.unlinkSync(candidate); } } for (const [lockfilePath, backupPath] of backups) { if (fs.existsSync(backupPath)) { fs.renameSync(backupPath, lockfilePath); } } } catch (restoreErr) { npmlog_default.error( "version", `Failed to restore the original bun lockfile(s): ${restoreErr}. Backup files with the ${BACKUP_SUFFIX} suffix may remain next to them for manual recovery.` ); } } // libs/commands/version/src/index.ts function factory(argv, preInitializedProjectData) { return new VersionCommand(argv, preInitializedProjectData); } var VersionCommand = class extends Command { commitAndTag; pushToRemote; allowBranch; gitRemote; tagPrefix; releaseClient; releaseNotes; gitOpts; savePrefix; currentBranch; updates = []; tags; globalVersion; hasRootedLeaf; runPackageLifecycle; runRootLifecycle; updatesVersions; packagesToVersion; projectsWithPackage = []; premajorVersionBump; get otherCommandConfigs() { return ["publish"]; } get requiresGit() { return !!(this.commitAndTag || this.pushToRemote || this.options.allowBranch || this.options.conventionalCommits); } /** * Due to lerna publish's legacy of being backwards compatible with running versioning and publishing * in a single step, we need to be able to receive any project data which might already exist from the * publish command (in the case that it invokes the version command from within its implementation details). */ constructor(argv, preInitializedProjectData) { super(argv, { skipValidations: false, preInitializedProjectData }); } configureProperties() { super.configureProperties(); const { amend, commitHooks = true, gitRemote = "origin", gitTagVersion = true, granularPathspec = true, push = true, signGitCommit, signoffGitCommit, signGitTag, forceGitTag, tagVersionPrefix = "v", premajorVersionBump = "default", message } = this.options; this.gitRemote = gitRemote; this.tagPrefix = tagVersionPrefix; this.commitAndTag = gitTagVersion; this.pushToRemote = gitTagVersion && amend !== true && push; const overrideMessage = amend && !!message; this.premajorVersionBump = premajorVersionBump; this.releaseClient = this.pushToRemote && this.options.createRelease && createReleaseClient(this.options.createRelease); this.releaseNotes = []; if (this.releaseClient && this.options.conventionalCommits !== true) { throw new ValidationError("ERELEASE", "To create a release, you must enable --conventional-commits"); } if (this.releaseClient && this.options.changelog === false) { throw new ValidationError("ERELEASE", "To create a release, you cannot pass --no-changelog"); } this.gitOpts = { amend, commitHooks, granularPathspec, signGitCommit, signoffGitCommit, signGitTag, forceGitTag, overrideMessage }; this.savePrefix = this.options.exact ? "" : "^"; } async initialize() { if (!this.project.isIndependent()) { this.logger.info("current version", this.project.version); } if (this.requiresGit) { if (!isAnythingCommitted(this.execOpts)) { throw new ValidationError( "ENOCOMMIT", "No commits in this repository. Please commit something before using version." ); } this.currentBranch = getCurrentBranch(this.execOpts); if (this.currentBranch === "HEAD") { throw new ValidationError( "ENOGIT", "Detached git HEAD, please checkout a branch to choose versions." ); } if (this.pushToRemote && !remoteBranchExists(this.gitRemote, this.currentBranch, this.execOpts)) { throw new ValidationError( "ENOREMOTEBRANCH", dedent` Branch '${this.currentBranch}' doesn't exist in remote '${this.gitRemote}'. If this is a new branch, please make sure you push it to the remote first. ` ); } if (this.options.allowBranch && ![].concat(this.options.allowBranch).some((x) => minimatch(this.currentBranch, x))) { throw new ValidationError( "ENOTALLOWED", dedent` Branch '${this.currentBranch}' is restricted from versioning due to allowBranch config. Please consider the reasons for this restriction before overriding the option. ` ); } if (this.commitAndTag && this.pushToRemote && isBehindUpstream(this.gitRemote, this.currentBranch, this.execOpts)) { const message = `Local branch '${this.currentBranch}' is behind remote upstream ${this.gitRemote}/${this.currentBranch}`; if (this.options.ci && this.options.ciBehindBehavior === "skip") { this.logger.warn("EBEHIND", `${message}, exiting`); return false; } throw new ValidationError( "EBEHIND", dedent` ${message} Please merge remote changes into '${this.currentBranch}' with 'git pull' ` ); } } else { this.logger.notice( "FYI", "git repository validation has been skipped, please ensure your version bumps are correct" ); } if (this.options.conventionalPrerelease && this.options.conventionalGraduate) { throw new ValidationError( "ENOTALLOWED", dedent` --conventional-prerelease cannot be combined with --conventional-graduate. ` ); } this.projectsWithPackage = Object.values(this.projectGraph.nodes).filter((node) => !!node.package); this.updates = collectProjectUpdates( this.projectsWithPackage, this.projectGraph, this.execOpts, this.options ).filter((node) => { const pkg = getPackage(node); if (pkg.private && this.options.private === false) { return false; } if (!pkg.version) { if (pkg.private) { this.logger.info("version", "Skipping unversioned private package %j", pkg.name); } else { throw new ValidationError( "ENOVERSION", dedent` A version field is required in ${pkg.name}'s package.json file. If you wish to keep the package unversioned, it must be made private. ` ); } } return !!pkg.version; }); if (!this.updates.length) { this.logger.success("", `No changed packages to ${this.composed ? "publish" : "version"}`); return false; } this.hasRootedLeaf = !!this.projectGraph.nodes[this.project.manifest.name]; if (this.hasRootedLeaf && !this.composed) { this.logger.info("version", "rooted leaf detected, skipping synthetic root lifecycles"); } this.runPackageLifecycle = createRunner({ ...this.options, stdio: "inherit" }); this.runRootLifecycle = /^(pre|post)?version$/.test(process.env["npm_lifecycle_event"]) ? (stage) => { this.logger.warn("lifecycle", "Skipping root %j because it has already been called", stage); } : (stage) => this.runPackageLifecycle(this.project.manifest, stage); if (this.commitAndTag && this.gitOpts.amend !== true) { const { forcePublish, conventionalCommits, conventionalGraduate } = this.options; const checkUncommittedOnly = forcePublish || conventionalCommits && conventionalGraduate; const check = checkUncommittedOnly ? throwIfUncommitted : checkWorkingTree; await check(this.execOpts); } else { this.logger.warn("version", "Skipping working tree validation, proceed at your own risk"); } const versions = await this.getVersionsForUpdates(); this.setUpdatesForVersions(versions); return this.confirmVersions(); } async execute() { const tasks = [() => this.updatePackageVersions()]; if (this.commitAndTag) { tasks.push(() => this.commitAndTagUpdates()); } else { this.logger.info("execute", "Skipping git tag/commit"); } if (this.pushToRemote) { tasks.push(() => this.gitPushToRemote()); } else { this.logger.info("execute", "Skipping git push"); } if (this.releaseClient) { this.logger.info("execute", "Creating releases..."); tasks.push( () => createRelease( this.releaseClient, { type: this.options.createRelease, tags: this.tags, tagVersionSeparator: this.options.tagVersionSeparator || "@", releaseNotes: this.releaseNotes }, { gitRemote: this.options.gitRemote, execOpts: this.execOpts } ) ); } else { this.logger.info("execute", "Skipping releases"); } for (const task of tasks) { await task(); } if (!this.composed) { this.logger.success("version", "finished"); } return { updates: this.updates, updatesVersions: this.updatesVersions }; } /** * Gets a mapping of project names to their package's new version. * @returns {Promise<Record<string, string>>} A map of project names to their package's new versions */ getVersionsForUpdates() { const independentVersions = this.project.isIndependent(); const { bump, conventionalCommits, preid } = this.options; const repoVersion = bump ? semver.clean(bump) : ""; const increment = bump && !semver.valid(bump) ? bump : ""; const resolvePrereleaseId = (existingPreid) => preid || existingPreid || "alpha"; const makeGlobalVersionPredicate = (nextVersion) => { this.globalVersion = nextVersion; return () => nextVersion; }; let predicate; if (repoVersion) { predicate = makeGlobalVersionPredicate(applyBuildMetadata(repoVersion, this.options.buildMetadata)); } else if (increment && independentVersions) { predicate = (node) => applyBuildMetadata( semver.inc(node.version, increment, resolvePrereleaseId(node.prereleaseId)), this.options.buildMetadata ); } else if (increment) { const baseVersion = this.project.version; const prereleaseId = prereleaseIdFromVersion(baseVersion); const nextVersion = applyBuildMetadata( semver.inc(baseVersion, increment, resolvePrereleaseId(prereleaseId)), this.options.buildMetadata ); predicate = makeGlobalVersionPredicate(nextVersion); } else if (conventionalCommits) { return this.recommendVersions(resolvePrereleaseId); } else if (independentVersions) { predicate = makePromptVersion(resolvePrereleaseId, this.options.buildMetadata); } else { const baseVersion = this.project.version; const prereleaseId = prereleaseIdFromVersion(baseVersion); const node = { version: baseVersion, prereleaseId }; const prompt = makePromptVersion(resolvePrereleaseId, this.options.buildMetadata); predicate = prompt(node).then(makeGlobalVersionPredicate); } return Promise.resolve(predicate).then( (getVersion) => this.reduceVersions((node) => { const pkg = getPackage(node); return getVersion({ version: pkg.version, name: pkg.name, prereleaseId: prereleaseIdFromVersion(pkg.version) }); }) ); } async reduceVersions(getVersion) { const versionMap = /* @__PURE__ */ new Map(); for (const node of this.updates) { const version = await Promise.resolve(getVersion(node)); versionMap.set(node.name, version); } return versionMap; } getPrereleasePackageNames() { const prereleasePackageNames = getPackagesForOption(this.options.conventionalPrerelease); const isCandidate = prereleasePackageNames.has("*") ? () => true : (node, name) => prereleasePackageNames.has(name); return collectProjects(this.projectsWithPackage, this.projectGraph, { isCandidate }).map( (pkg) => pkg.name ); } async recommendVersions(resolvePrereleaseId) { const independentVersions = this.project.isIndependent(); const { buildMetadata, changelogPreset, conventionalGraduate, conventionalBumpPrerelease } = this.options; const rootPath = this.project.manifest.location; const type = independentVersions ? "independent" : "fixed"; const prereleasePackageNames = this.getPrereleasePackageNames(); const graduatePackageNames = Array.from(getPackagesForOption(conventionalGraduate)); const shouldPrerelease = (name) => prereleasePackageNames && prereleasePackageNames.includes(name); const shouldGraduate = (name) => graduatePackageNames.includes("*") || graduatePackageNames.includes(name); const getPrereleaseId = (node) => { if (!shouldGraduate(node.name) && (shouldPrerelease(node.name) || node.prereleaseId)) { return resolvePrereleaseId(node.prereleaseId); } return void 0; }; if (type === "fixed") { this.setGlobalVersionFloor(); } const versions = await this.reduceVersions((node) => { const pkg = getPackage(node); return recommendVersion( pkg, type, { changelogPreset, rootPath, tagPrefix: this.tagPrefix, prereleaseId: getPrereleaseId({ name: node.name, prereleaseId: prereleaseIdFromVersion(pkg.version) }), conventionalBumpPrerelease, buildMetadata }, this.premajorVersionBump ); }); if (type === "fixed") { this.globalVersion = await this.setGlobalVersionCeiling(versions); } return versions; } setGlobalVersionFloor() { const globalVersion = this.project.version; for (const node of this.updates) { const pkg = getPackage(node); if (semver.lt(pkg.version, globalVersion)) { this.logger.verbose( "version", `Overriding version of ${pkg.name} from ${pkg.version} to ${globalVersion}` ); pkg.set("version", globalVersion); } } } setGlobalVersionCeiling(versions) { let highestVersion = this.project.version; versions.forEach((bump) => { if (bump && semver.gt(bump, highestVersion)) { highestVersion = bump; } }); versions.forEach((_, name) => versions.set(name, highestVersion)); return highestVersion; } setUpdatesForVersions(versions) { if (this.project.isIndependent() || versions.size === this.projectsWithPackage.length) { this.updatesVersions = versions; } else { let hasBreakingChange; for (const [name, bump] of versions) { const pkg = getPackage(this.projectGraph.nodes[name]); hasBreakingChange = hasBreakingChange || isBreakingChange(pkg.version, bump); } if (hasBreakingChange) { this.updates = this.projectsWithPackage; if (this.options.private === false) { this.updates = this.updates.filter((node) => !getPackage(node).private); } this.updatesVersions = new Map(this.updates.map((node) => [node.name, this.globalVersion])); } else { this.updatesVersions = versions; } } this.packagesToVersion = this.updates.map((node) => getPackage(node)); } confirmVersions() { if (this.options.json) { const updatedProjectsJson = formatJSON(this.updates, ({ name }) => ({ newVersion: this.updatesVersions.get(name) })); output(updatedProjectsJson); } else { const changes = this.updates.map((node) => { const pkg = getPackage(node); let line = ` - ${pkg.name}: ${pkg.version} => ${this.updatesVersions.get(node.name)}`; if (pkg.private) { line += ` (${colorize("red", "private")})`; } return line; }); output(""); output("Changes:"); output(changes.join(os.EOL)); output(""); } if (this.options.yes) { this.logger.info("auto-confirmed", ""); return true; } const message = this.composed ? "Are you sure you want to publish these packages?" : "Are you sure you want to create these versions?"; return promptConfirmation(message); } async updatePackageVersions() { const { conventionalCommits, changelogPreset, changelogEntryAdditionalMarkdown, changelog = true, runScriptsOnLockfileUpdate = false, syncDistVersion = false } = this.options; const independentVersions = this.project.isIndependent(); const rootPath = this.project.manifest.location; const changedFiles = /* @__PURE__ */ new Set(); if (!this.hasRootedLeaf) { await this.runRootLifecycle("preversion"); } const actions = [ (node) => this.runPackageLifecycle(getPackage(node), "preversion").then(() => node), // manifest may be mutated by any previous lifecycle (node) => getPackage(node).refresh().then(() => node), (node) => { const pkg = getPackage(node); pkg.version = this.updatesVersions.get(node.name); this.updateDependencies(node); return Promise.all([ updateLockfileVersion(pkg), pkg.serialize(), pkg.syncDistVersion(syncDistVersion) ]).then(([lockfilePath]) => { changedFiles.add(pkg.manifestLocation); if (lockfilePath) { changedFiles.add(lockfilePath); } return node; }); }, (node) => this.runPackageLifecycle(getPackage(node), "version").then(() => node) ]; if (conventionalCommits && changelog) { const type = independentVersions ? "independent" : "fixed"; actions.push( (node) => updateChangelog(getPackage(node), type, { changelogPreset, changelogEntryAdditionalMarkdown, rootPath, tagPrefix: this.tagPrefix }).then(({ logPath, newEntry }) => { changedFiles.add(logPath); if (independentVersions) { this.releaseNotes.push({ name: getPackage(node).name, notes: newEntry }); } return node; }) ); } const mapUpdate = async (node) => { let result = node; for (const action of actions) { result = await action(result); } return result; }; await runProjectsTopologically(this.updates, this.projectGraph, mapUpdate, { concurrency: this.concurrency, rejectCycles: this.options.rejectCycles }); if (!independentVersions) { this.project.version = this.globalVersion; if (conventionalCommits && changelog) { const { logPath, newEntry } = await updateChangelog(this.project.manifest, "root", { changelogPreset, changelogEntryAdditionalMarkdown, rootPath, tagPrefix: this.tagPrefix, version: this.globalVersion }); changedFiles.add(logPath); this.releaseNotes.push({ name: "fixed", notes: newEntry }); } const lernaConfigLocation = await Promise.resolve(this.project.serializeConfig()); changedFiles.add(lernaConfigLocation); } const npmClientArgsRaw = this.options.npmClientArgs || []; const npmClientArgs = npmClientArgsRaw.reduce( (args, arg) => args.concat(arg.split(/\s|,/)), [] ); if (!this.hasRootedLeaf) { await this.runRootLifecycle("version"); } if (this.options.npmClient === "pnpm") { this.logger.verbose("version", "Updating root pnpm-lock.yaml"); await execPackageManager( "pnpm", [ "install", "--lockfile-only", !runScriptsOnLockfileUpdate ? "--ignore-scripts" : "", ...npmClientArgs ].filter(Boolean), this.execOpts ); const lockfilePath = path2.join(this.project.rootPath, "pnpm-lock.yaml"); changedFiles.add(lockfilePath); } if (this.options.npmClient === "bun") { const bunLockfiles = await updateBunLockfile({ rootPath: this.project.rootPath, npmClientArgs, runScriptsOnLockfileUpdate, execOpts: this.execOpts }); for (const lockfilePath of bunLockfiles) { changedFiles.add(lockfilePath); } } if (this.options.npmClient === "yarn") { const yarnVersion = execPackageManagerSync("yarn", ["--version"], this.execOpts); this.logger.verbose("version", `Detected yarn version ${yarnVersion}`); if (semver.gte(yarnVersion, "2.0.0")) { this.logger.verbose("version", "Updating root yarn.lock"); await execPackageManager("yarn", ["install", "--mode", "update-lockfile", ...npmClientArgs], { ...this.execOpts, env: { ...process.env, YARN_ENABLE_SCRIPTS: "false" } }); const lockfilePath = path2.join(this.project.rootPath, "yarn.lock"); changedFiles.add(lockfilePath); } } if (this.options.npmClient === "npm" || !this.options.npmClient) { const lockfilePath = path2.join(this.project.rootPath, "package-lock.json"); if (fs2.existsSync(lockfilePath)) { this.logger.verbose("version", "Updating root package-lock.json"); await exec( "npm", [ "install", "--package-lock-only", !runScriptsOnLockfileUpdate ? "--ignore-scripts" : "", ...npmClientArgs ].filter(Boolean), this.execOpts ); changedFiles.add(lockfilePath); } } if (this.commitAndTag) { await gitAdd(Array.from(changedFiles), this.gitOpts, this.execOpts); } } updateDependencies(node) { const dependencies = this.projectGraph.localPackageDependencies[node.name] || []; const pkg = getPackage(node); dependencies.forEach((dep) => { const depVersion = this.updatesVersions.get(dep.target); if ( // only update if the dependency version is being changed depVersion && // don't overwrite local file: specifiers, they only change during publish dep.targetResolvedNpaResult.type !== "directory" ) { pkg.updateLocalDependency(dep.targetResolvedNpaResult, depVersion, this.savePrefix); } }); } async commitAndTagUpdates() { let tags = []; if (this.project.isIndependent()) { tags = await this.gitCommitAndTagVersionForUpdates(); } else { tags = await this.gitCommitAndTagVersion(); } this.tags = tags; await pMap(this.packagesToVersion, (pkg) => this.runPackageLifecycle(pkg, "postversion")); if (!this.hasRootedLeaf) { await this.runRootLifecycle("postversion"); } } async gitCommitAndTagVersionForUpdates() { const tagVersionSeparator = this.options.tagVersionSeparator || "@"; const tags = this.updates.map((node) => { const pkg = getPackage(node); return `${pkg.name}${tagVersionSeparator}${this.updatesVersions.get(node.name)}`; }); const subject = this.options.message || "Publish"; const message = tags.reduce((msg, tag) => `${msg}${os.EOL} - ${tag}`, `${subject}${os.EOL}`); if (await this.hasChanges()) { await gitCommit(message, this.gitOpts, this.execOpts); } if (this.gitOpts.signGitTag) { for (const tag of tags) await gitTag(tag, this.gitOpts, this.execOpts, this.options.gitTagCommand); } else { await Promise.all( tags.map((tag) => gitTag(tag, this.gitOpts, this.execOpts, this.options.gitTagCommand)) ); } return tags; } async gitCommitAndTagVersion() { const version = this.globalVersion; const tag = `${this.tagPrefix}${version}`; const message = this.options.message ? this.options.message.replace(/%s/g, tag).replace(/%v/g, version) : tag; if (await this.hasChanges()) { await gitCommit(message, this.gitOpts, this.execOpts); } await gitTag(tag, this.gitOpts, this.execOpts, this.options.gitTagCommand); return [tag]; } gitPushToRemote() { this.logger.info("git", "Pushing tags..."); return gitPush(this.gitRemote, this.currentBranch, this.execOpts); } async hasChanges() { try { await execa("git", ["diff", "--staged", "--quiet"], { stdio: "pipe", ...this.execOpts, cwd: this.execOpts.cwd // force it to a string }); } catch (e) { return true; } return void 0; } }; var commonJsExport = Object.assign(factory, { VersionCommand }); var src_default = commonJsExport; export { factory, VersionCommand, commonJsExport, src_default };