@lerna/publish
Version:
Publish packages in the current project
561 lines (560 loc) • 23.7 kB
JavaScript
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;