UNPKG

@lerna/publish

Version:

Publish packages in the current project

561 lines (560 loc) 23.7 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var import_core = require("@lerna/core"); var import_chalk = __toESM(require("chalk")); var import_dedent = __toESM(require("dedent")); var import_fs = __toESM(require("fs")); var import_minimatch = __toESM(require("minimatch")); var import_os = __toESM(require("os")); var import_p_map = __toESM(require("p-map")); var import_p_pipe = __toESM(require("p-pipe")); var import_p_reduce = __toESM(require("p-reduce")); var import_p_waterfall = __toESM(require("p-waterfall")); var import_path = __toESM(require("path")); var import_semver = __toESM(require("semver")); const childProcess = require("@lerna/child-process"); const { getCurrentBranch } = require("./lib/get-current-branch"); const { gitAdd } = require("./lib/git-add"); const { gitCommit } = require("./lib/git-commit"); const { gitPush } = require("./lib/git-push"); const { gitTag } = require("./lib/git-tag"); const { isBehindUpstream } = require("./lib/is-behind-upstream"); const { remoteBranchExists } = require("./lib/remote-branch-exists"); const { isBreakingChange } = require("./lib/is-breaking-change"); const { isAnythingCommitted } = require("./lib/is-anything-committed"); const { makePromptVersion } = require("./lib/prompt-version"); const { createRelease, createReleaseClient } = require("./lib/create-release"); const { updateLockfileVersion } = require("./lib/update-lockfile-version"); module.exports = function factory(argv) { return new VersionCommand(argv); }; class VersionCommand extends import_core.Command { get otherCommandConfigs() { return ["publish"]; } get requiresGit() { return this.commitAndTag || this.pushToRemote || this.options.allowBranch || this.options.conventionalCommits; } configureProperties() { super.configureProperties(); const { amend, commitHooks = true, gitRemote = "origin", gitTagVersion = true, granularPathspec = true, push = true, signGitCommit, signoffGitCommit, signGitTag, forceGitTag, tagVersionPrefix = "v" } = this.options; this.gitRemote = gitRemote; this.tagPrefix = tagVersionPrefix; this.commitAndTag = gitTagVersion; this.pushToRemote = gitTagVersion && amend !== true && push; this.releaseClient = this.pushToRemote && this.options.createRelease && createReleaseClient(this.options.createRelease); this.releaseNotes = []; if (this.releaseClient && this.options.conventionalCommits !== true) { throw new import_core.ValidationError("ERELEASE", "To create a release, you must enable --conventional-commits"); } if (this.releaseClient && this.options.changelog === false) { throw new import_core.ValidationError("ERELEASE", "To create a release, you cannot pass --no-changelog"); } this.gitOpts = { amend, commitHooks, granularPathspec, signGitCommit, signoffGitCommit, signGitTag, forceGitTag }; this.savePrefix = this.options.exact ? "" : "^"; } initialize() { if (!this.project.isIndependent()) { this.logger.info("current version", this.project.version); } if (this.requiresGit) { if (!isAnythingCommitted(this.execOpts)) { throw new import_core.ValidationError( "ENOCOMMIT", "No commits in this repository. Please commit something before using version." ); } this.currentBranch = getCurrentBranch(this.execOpts); if (this.currentBranch === "HEAD") { throw new import_core.ValidationError( "ENOGIT", "Detached git HEAD, please checkout a branch to choose versions." ); } if (this.pushToRemote && !remoteBranchExists(this.gitRemote, this.currentBranch, this.execOpts)) { throw new import_core.ValidationError( "ENOREMOTEBRANCH", import_dedent.default` 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) => (0, import_minimatch.default)(this.currentBranch, x))) { throw new import_core.ValidationError( "ENOTALLOWED", import_dedent.default` 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) { throw new import_core.ValidationError( "EBEHIND", import_dedent.default` ${message} Please merge remote changes into '${this.currentBranch}' with 'git pull' ` ); } this.logger.warn("EBEHIND", `${message}, exiting`); return false; } } 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 import_core.ValidationError( "ENOTALLOWED", import_dedent.default` --conventional-prerelease cannot be combined with --conventional-graduate. ` ); } this.updates = (0, import_core.collectUpdates)( this.packageGraph.rawPackageList, this.packageGraph, this.execOpts, this.options ).filter((node) => { if (node.pkg.private && this.options.private === false) { return false; } if (!node.version) { if (node.pkg.private) { this.logger.info("version", "Skipping unversioned private package %j", node.name); } else { throw new import_core.ValidationError( "ENOVERSION", import_dedent.default` A version field is required in ${node.name}'s package.json file. If you wish to keep the package unversioned, it must be made private. ` ); } } return !!node.version; }); if (!this.updates.length) { this.logger.success(`No changed packages to ${this.composed ? "publish" : "version"}`); return false; } this.hasRootedLeaf = this.packageGraph.has(this.project.manifest.name); if (this.hasRootedLeaf && !this.composed) { this.logger.info("version", "rooted leaf detected, skipping synthetic root lifecycles"); } this.runPackageLifecycle = (0, import_core.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); const tasks = [ () => this.getVersionsForUpdates(), (versions) => this.setUpdatesForVersions(versions), () => this.confirmVersions() ]; if (this.commitAndTag && this.gitOpts.amend !== true) { const { forcePublish, conventionalCommits, conventionalGraduate } = this.options; const checkUncommittedOnly = forcePublish || conventionalCommits && conventionalGraduate; const check = checkUncommittedOnly ? import_core.throwIfUncommitted : import_core.checkWorkingTree; tasks.unshift(() => check(this.execOpts)); } else { this.logger.warn("version", "Skipping working tree validation, proceed at your own risk"); } return (0, import_p_waterfall.default)(tasks); } 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, { tags: this.tags, releaseNotes: this.releaseNotes }, { gitRemote: this.options.gitRemote, execOpts: this.execOpts } ) ); } else { this.logger.info("execute", "Skipping releases"); } return (0, import_p_waterfall.default)(tasks).then(() => { if (!this.composed) { this.logger.success("version", "finished"); } return { updates: this.updates, updatesVersions: this.updatesVersions }; }); } getVersionsForUpdates() { const independentVersions = this.project.isIndependent(); const { bump, conventionalCommits, preid } = this.options; const repoVersion = bump ? import_semver.default.clean(bump) : ""; const increment = bump && !import_semver.default.valid(bump) ? bump : ""; const resolvePrereleaseId = (existingPreid) => preid || existingPreid || "alpha"; const makeGlobalVersionPredicate = (nextVersion) => { this.globalVersion = nextVersion; return () => nextVersion; }; let predicate; if (repoVersion) { predicate = makeGlobalVersionPredicate((0, import_core.applyBuildMetadata)(repoVersion, this.options.buildMetadata)); } else if (increment && independentVersions) { predicate = (node) => (0, import_core.applyBuildMetadata)( import_semver.default.inc(node.version, increment, resolvePrereleaseId(node.prereleaseId)), this.options.buildMetadata ); } else if (increment) { const prereleaseId = (0, import_core.prereleaseIdFromVersion)(this.project.version); const nextVersion = (0, import_core.applyBuildMetadata)( import_semver.default.inc(this.project.version, 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 prereleaseId = (0, import_core.prereleaseIdFromVersion)(this.project.version); const node = { version: this.project.version, prereleaseId }; predicate = makePromptVersion(resolvePrereleaseId, this.options.buildMetadata); predicate = predicate(node).then(makeGlobalVersionPredicate); } return Promise.resolve(predicate).then((getVersion) => this.reduceVersions(getVersion)); } reduceVersions(getVersion) { const iterator = (versionMap, node) => Promise.resolve(getVersion(node)).then((version) => versionMap.set(node.name, version)); return (0, import_p_reduce.default)(this.updates, iterator, /* @__PURE__ */ new Map()); } getPrereleasePackageNames() { const prereleasePackageNames = (0, import_core.getPackagesForOption)(this.options.conventionalPrerelease); const isCandidate = prereleasePackageNames.has("*") ? () => true : (node, name) => prereleasePackageNames.has(name); return (0, import_core.collectPackages)(this.packageGraph, { isCandidate }).map((pkg) => pkg.name); } 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((0, import_core.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); } }; let chain = Promise.resolve(); if (type === "fixed") { chain = chain.then(() => this.setGlobalVersionFloor()); } chain = chain.then( () => this.reduceVersions( (node) => (0, import_core.recommendVersion)(node, type, { changelogPreset, rootPath, tagPrefix: this.tagPrefix, prereleaseId: getPrereleaseId(node), conventionalBumpPrerelease, buildMetadata }) ) ); if (type === "fixed") { chain = chain.then((versions) => { this.globalVersion = this.setGlobalVersionCeiling(versions); return versions; }); } return chain; } setGlobalVersionFloor() { const globalVersion = this.project.version; for (const node of this.updates) { if (import_semver.default.lt(node.version, globalVersion)) { this.logger.verbose( "version", `Overriding version of ${node.name} from ${node.version} to ${globalVersion}` ); node.pkg.set("version", globalVersion); } } } setGlobalVersionCeiling(versions) { let highestVersion = this.project.version; versions.forEach((bump) => { if (bump && import_semver.default.gt(bump, highestVersion)) { highestVersion = bump; } }); versions.forEach((_, name) => versions.set(name, highestVersion)); return highestVersion; } setUpdatesForVersions(versions) { if (this.project.isIndependent() || versions.size === this.packageGraph.size) { this.updatesVersions = versions; } else { let hasBreakingChange; for (const [name, bump] of versions) { hasBreakingChange = hasBreakingChange || isBreakingChange(this.packageGraph.get(name).version, bump); } if (hasBreakingChange) { this.updates = Array.from(this.packageGraph.values()); if (this.options.private === false) { this.updates = this.updates.filter((node) => !node.pkg.private); } this.updatesVersions = new Map(this.updates.map((node) => [node.name, this.globalVersion])); } else { this.updatesVersions = versions; } } this.packagesToVersion = this.updates.map((node) => node.pkg); } confirmVersions() { const changes = this.packagesToVersion.map((pkg) => { let line = ` - ${pkg.name}: ${pkg.version} => ${this.updatesVersions.get(pkg.name)}`; if (pkg.private) { line += ` (${import_chalk.default.red("private")})`; } return line; }); (0, import_core.output)(""); (0, import_core.output)("Changes:"); (0, import_core.output)(changes.join(import_os.default.EOL)); (0, import_core.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 (0, import_core.promptConfirmation)(message); } updatePackageVersions() { const { conventionalCommits, changelogPreset, changelog = true } = this.options; const independentVersions = this.project.isIndependent(); const rootPath = this.project.manifest.location; const changedFiles = /* @__PURE__ */ new Set(); let chain = Promise.resolve(); if (!this.hasRootedLeaf) { chain = chain.then(() => this.runRootLifecycle("preversion")); } const actions = [ (pkg) => this.runPackageLifecycle(pkg, "preversion").then(() => pkg), // manifest may be mutated by any previous lifecycle (pkg) => pkg.refresh(), (pkg) => { pkg.set("version", this.updatesVersions.get(pkg.name)); for (const [depName, resolved] of this.packageGraph.get(pkg.name).localDependencies) { const depVersion = this.updatesVersions.get(depName); if (depVersion && resolved.type !== "directory") { pkg.updateLocalDependency(resolved, depVersion, this.savePrefix); } } return Promise.all([updateLockfileVersion(pkg), pkg.serialize()]).then(([lockfilePath]) => { changedFiles.add(pkg.manifestLocation); if (lockfilePath) { changedFiles.add(lockfilePath); } return pkg; }); }, (pkg) => this.runPackageLifecycle(pkg, "version").then(() => pkg) ]; if (conventionalCommits && changelog) { const type = independentVersions ? "independent" : "fixed"; actions.push( (pkg) => (0, import_core.updateChangelog)(pkg, type, { changelogPreset, rootPath, tagPrefix: this.tagPrefix }).then(({ logPath, newEntry }) => { changedFiles.add(logPath); if (independentVersions) { this.releaseNotes.push({ name: pkg.name, notes: newEntry }); } return pkg; }) ); } const mapUpdate = (0, import_p_pipe.default)(...actions); chain = chain.then( () => (0, import_core.runTopologically)(this.packagesToVersion, mapUpdate, { concurrency: this.concurrency, rejectCycles: this.options.rejectCycles }) ); if (!independentVersions) { this.project.version = this.globalVersion; if (conventionalCommits && changelog) { chain = chain.then( () => (0, import_core.updateChangelog)(this.project.manifest, "root", { changelogPreset, rootPath, tagPrefix: this.tagPrefix, version: this.globalVersion }).then(({ logPath, newEntry }) => { changedFiles.add(logPath); this.releaseNotes.push({ name: "fixed", notes: newEntry }); }) ); } chain = chain.then( () => Promise.resolve(this.project.serializeConfig()).then((lernaConfigLocation) => { changedFiles.add(lernaConfigLocation); }) ); } const npmClientArgsRaw = this.options.npmClientArgs || []; const npmClientArgs = npmClientArgsRaw.reduce((args, arg) => args.concat(arg.split(/\s|,/)), []); if (this.options.npmClient === "pnpm") { chain = chain.then(() => { this.logger.verbose("version", "Updating root pnpm-lock.yaml"); return childProcess.exec("pnpm", ["install", "--lockfile-only", "--ignore-scripts", ...npmClientArgs], this.execOpts).then(() => { const lockfilePath = import_path.default.join(this.project.rootPath, "pnpm-lock.yaml"); changedFiles.add(lockfilePath); }); }); } if (this.options.npmClient === "yarn") { chain = chain.then(() => childProcess.execSync("yarn", ["--version"], this.execOpts)).then((yarnVersion) => { this.logger.verbose("version", `Detected yarn version ${yarnVersion}`); if (import_semver.default.gte(yarnVersion, "2.0.0")) { this.logger.verbose("version", "Updating root yarn.lock"); return childProcess.exec("yarn", ["install", "--mode", "update-lockfile", ...npmClientArgs], { ...this.execOpts, env: { ...process.env, YARN_ENABLE_SCRIPTS: false } }).then(() => { const lockfilePath = import_path.default.join(this.project.rootPath, "yarn.lock"); changedFiles.add(lockfilePath); }); } }); } if (this.options.npmClient === "npm" || !this.options.npmClient) { const lockfilePath = import_path.default.join(this.project.rootPath, "package-lock.json"); if (import_fs.default.existsSync(lockfilePath)) { chain = chain.then(() => { this.logger.verbose("version", "Updating root package-lock.json"); return childProcess.exec( "npm", ["install", "--package-lock-only", "--ignore-scripts", ...npmClientArgs], this.execOpts ).then(() => { changedFiles.add(lockfilePath); }); }); } } if (!this.hasRootedLeaf) { chain = chain.then(() => this.runRootLifecycle("version")); } if (this.commitAndTag) { chain = chain.then(() => gitAdd(Array.from(changedFiles), this.gitOpts, this.execOpts)); } return chain; } commitAndTagUpdates() { let chain = Promise.resolve(); if (this.project.isIndependent()) { chain = chain.then(() => this.gitCommitAndTagVersionForUpdates()); } else { chain = chain.then(() => this.gitCommitAndTagVersion()); } chain = chain.then((tags) => { this.tags = tags; }); chain = chain.then( () => (0, import_p_map.default)(this.packagesToVersion, (pkg) => this.runPackageLifecycle(pkg, "postversion")) ); if (!this.hasRootedLeaf) { chain = chain.then(() => this.runRootLifecycle("postversion")); } return chain; } gitCommitAndTagVersionForUpdates() { const tags = this.packagesToVersion.map((pkg) => `${pkg.name}@${this.updatesVersions.get(pkg.name)}`); const subject = this.options.message || "Publish"; const message = tags.reduce((msg, tag) => `${msg}${import_os.default.EOL} - ${tag}`, `${subject}${import_os.default.EOL}`); return Promise.resolve().then(() => gitCommit(message, this.gitOpts, this.execOpts)).then( () => Promise.all(tags.map((tag) => gitTag(tag, this.gitOpts, this.execOpts, this.options.gitTagCommand))) ).then(() => tags); } 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; return Promise.resolve().then(() => gitCommit(message, this.gitOpts, this.execOpts)).then(() => gitTag(tag, this.gitOpts, this.execOpts, this.options.gitTagCommand)).then(() => [tag]); } gitPushToRemote() { this.logger.info("git", "Pushing tags..."); return gitPush(this.gitRemote, this.currentBranch, this.execOpts); } } module.exports.VersionCommand = VersionCommand;