Deep Dive into Lerna Publish: Initialization, Scenarios, and Execution Flow
This article explains how the Lerna publish command works, detailing the different publishing scenarios, the internal initialization steps such as configureProperties and initialize, and the final execution phase that validates npm access, updates dependencies, packs packages, and publishes them to the npm registry.
Lerna is a popular monorepo management tool for front‑end projects. The lerna publish command is the final step in the release process, responsible for publishing the selected packages to the npm registry.
There are several publishing modes:
lerna publish
# lerna version + lerna publish from-git
lerna publish from-git
# publish packages that have an annotated tag on the current commit
lerna publish from-packages
# publish packages whose
package.json
version is newer than the registry versionNote: packages with private: true in package.json are never published.
Publish Scenarios
Publish packages that have changed since the last release (default lerna publish ).
Publish packages that have an annotated git tag on the current commit ( lerna publish from-git ).
Publish packages whose package.json version was updated and does not exist in the registry ( lerna publish from-package ).
Publish unversioned test packages and their dependents.
Configure Properties (configureProperties)
configureProperties() {
const { exact, gitHead, gitReset, tagVersionPrefix = "v", verifyAccess } = this.options;
// validate --git-head usage
if (this.requiresGit && gitHead) {
throw new ValidationError("EGITHEAD", "--git-head is only allowed with 'from-package' positional");
}
// set version prefix
this.savePrefix = exact ? "" : "^";
// custom tag prefix
this.tagPrefix = tagVersionPrefix;
// handle --no-git-reset
this.gitReset = gitReset !== false;
// npm access verification
this.verifyAccess = verifyAccess !== false;
// generate a random npm session id
this.npmSession = crypto.randomBytes(8).toString("hex");
}The method simply reads CLI options and prepares internal fields such as version prefix, tag prefix, git reset flag, access verification flag, and a random npm session identifier.
Initialization (initialize)
The initialize method performs three major steps:
Initialize npm configuration parameters.
Execute different logic depending on the chosen publishing scenario.
Process the result of the previous step.
initialize() {
// 1. npm config
this.conf = npmConf({
lernaCommand: "publish",
_auth: this.options.legacyAuth,
npmSession: this.npmSession,
npmVersion: this.userAgent,
otp: this.options.otp,
registry: this.options.registry,
"ignore-prepublish": this.options.ignorePrepublish,
"ignore-scripts": this.options.ignoreScripts,
});
// 2. Choose scenario
let chain = Promise.resolve();
if (this.options.bump === "from-git") {
chain = chain.then(() => this.detectFromGit());
} else if (this.options.bump === "from-package") {
chain = chain.then(() => this.detectFromPackage());
} else if (this.options.canary) {
chain = chain.then(() => this.detectCanaryVersions());
} else {
chain = chain.then(() => versionCommand(this.argv));
}
// 3. Handle result
return chain.then(result => {
if (!result) return false;
if (!result.updates.length) {
this.logger.success("No changed packages to publish");
return false;
}
this.updates = result.updates.filter(node => !node.pkg.private);
this.updatesVersions = new Map(result.updatesVersions);
this.packagesToPublish = this.updates.map(node => node.pkg);
if (result.needsConfirmation) return this.confirmPublish();
return true;
});
}The method builds an npm configuration object, decides which detection routine to run ( detectFromGit , detectFromPackage , or detectCanaryVersions ), and finally filters out private packages, stores version information, and optionally asks for user confirmation.
Scenario Implementations
from‑git
detectFromGit() {
const matchingPattern = this.project.isIndependent() ? "*@*" : `${this.tagPrefix}*.*.*`;
let chain = Promise.resolve();
chain = chain.then(() => this.verifyWorkingTreeClean());
chain = chain.then(() => getCurrentTags(this.execOpts, matchingPattern));
chain = chain.then(taggedPackageNames => {
if (!taggedPackageNames.length) {
this.logger.notice("from-git", "No tagged release found");
return [];
}
if (this.project.isIndependent()) {
return taggedPackageNames.map(name => this.packageGraph.get(name));
}
return getTaggedPackages(this.packageGraph, this.project.rootPath, this.execOpts);
});
chain = chain.then(updates => updates.filter(node => !node.pkg.private));
return chain.then(updates => {
const updatesVersions = updates.map(node => [node.name, node.version]);
return { updates, updatesVersions, needsConfirmation: true };
});
}This routine checks that the git working tree is clean, extracts tags that match the version pattern, resolves the corresponding packages, removes private ones, and returns the list together with a confirmation flag.
from‑package
detectFromPackage() {
let chain = Promise.resolve();
chain = chain.then(() => this.verifyWorkingTreeClean());
chain = chain.then(() => getUnpublishedPackages(this.packageGraph, this.conf.snapshot));
chain = chain.then(unpublished => {
if (!unpublished.length) {
this.logger.notice("from-package", "No unpublished release found");
}
return unpublished;
});
return chain.then(updates => {
const updatesVersions = updates.map(node => [node.name, node.version]);
return { updates, updatesVersions, needsConfirmation: true };
});
}It validates the git tree, uses the npm snapshot to find packages whose version is not yet published, filters out private packages, and returns the data.
--canary (test releases)
detectCanaryVersions() {
const { cwd } = this.execOpts;
const { bump = "prepatch", preid = "alpha", ignoreChanges, forcePublish, includeMergedTags } = this.options;
const release = bump.startsWith("pre") ? bump.replace("release", "patch") : `pre${bump}`;
let chain = Promise.resolve();
chain = chain.then(() => this.verifyWorkingTreeClean());
chain = chain.then(() => collectUpdates(this.packageGraph.rawPackageList, this.packageGraph, this.execOpts, {
bump: "prerelease",
canary: true,
ignoreChanges,
forcePublish,
includeMergedTags,
}).filter(node => !node.pkg.private));
const makeVersion = fallback => ({ lastVersion = fallback, refCount, sha }) => {
const nextVersion = semver.inc(lastVersion.replace(this.tagPrefix, ""), release.replace("pre", ""));
return `${nextVersion}-${preid}.${Math.max(0, refCount - 1)}+${sha}`;
};
if (this.project.isIndependent()) {
chain = chain.then(updates => pMap(updates, node =>
describeRef({ match: `${node.name}@*`, cwd }, includeMergedTags)
.then(makeVersion(node.version))
.then(version => [node.name, version])
)).then(updatesVersions => ({ updates, updatesVersions }));
} else {
chain = chain.then(updates =>
describeRef({ match: `${this.tagPrefix}*.*.*`, cwd }, includeMergedTags)
.then(makeVersion(this.project.version))
.then(version => updates.map(node => [node.name, version]))
)).then(updatesVersions => ({ updates, updatesVersions }));
}
return chain.then(({ updates, updatesVersions }) => ({ updates, updatesVersions, needsConfirmation: true }));
}The canary mode creates a pre‑release version based on the last released version, the commit SHA, and a counter, handling both independent and fixed versioning strategies.
Execution (execute)
execute() {
let chain = Promise.resolve();
// 1. Verify npm registry, access and license
chain = chain.then(() => this.prepareRegistryActions());
chain = chain.then(() => this.prepareLicenseActions());
if (this.options.canary) {
chain = chain.then(() => this.updateCanaryVersions());
}
// 2. Update local dependency links and gitHead
chain = chain.then(() => this.resolveLocalDependencyLinks());
chain = chain.then(() => this.annotateGitHead());
// 3. Write changes to disk
chain = chain.then(() => this.serializeChanges());
// 4. Pack packages
chain = chain.then(() => this.packUpdated());
// 5. Publish packed tarballs
chain = chain.then(() => this.publishPacked());
if (this.gitReset) {
chain = chain.then(() => this.resetChanges());
}
return chain.then(() => {
const count = this.packagesToPublish.length;
const message = this.packagesToPublish.map(pkg => ` - ${pkg.name}@${pkg.version}`);
output("Successfully published:");
output(message.join(os.EOL));
this.logger.success("published", "%d %s", count, count === 1 ? "package" : "packages");
});
}The execution phase validates npm access (including optional 2FA), checks for missing licenses, updates inter‑package dependencies, records the git commit hash, writes the updated package.json files, creates tarballs with npm pack -like logic, and finally publishes each tarball using the internal @evocateur/libnpmpublish library.
Key Helper Methods
prepareRegistryActions : verifies the npm registry URL, obtains the npm username, checks package access, and determines if two‑factor authentication is required.
prepareLicenseActions : warns about packages missing a LICENSE file.
resolveLocalDependencyLinks : updates workspace packages that depend on other workspace packages to the new version.
annotateGitHead : adds the current commit SHA to each package’s gitHead field.
serializeChanges : writes the modified package.json files to disk.
packUpdated : creates tarballs for each package in topological order.
publishPacked : publishes the tarballs to npm, handling temporary tags and OTP if required.
Overall, the article walks through the complete source‑level flow of lerna publish , from command‑line options to the final npm registry update, providing a clear mental model for developers working with large JavaScript monorepos.
TikTok Frontend Technology Team
We are the TikTok Frontend Technology Team, serving TikTok and multiple ByteDance product lines, focused on building frontend infrastructure and exploring community technologies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.