Frontend Development 26 min read

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.

TikTok Frontend Technology Team
TikTok Frontend Technology Team
TikTok Frontend Technology Team
Deep Dive into Lerna Publish: Initialization, Scenarios, and Execution Flow

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 version

Note: 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.

JavaScriptMonorepolernanpmPackage Managementpublish
TikTok Frontend Technology Team
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.