Back to monorepo orchestration Target affected workspaces Configure turbo pipelines Speed up type-checking

Migrating from Lerna to Turborepo

Lerna was the original JavaScript monorepo tool, and many repositories still run on it: lerna bootstrap to link packages, lerna run build to execute scripts, and lerna version plus lerna publish to release. Modern package managers now handle linking natively, and teams often want Turborepo's caching and task graph for builds. The migration is less about replacing one tool than about splitting Lerna's three jobs — linking, task running and releasing — and giving each to the tool that does it best. Done in that order, every step is small, reversible and verifiable on its own. This guide maps every Lerna command to its replacement, walks through a staged migration, and covers the release workflow, which is the part that needs the most care.

What Lerna was doing for you

Lerna combined three responsibilities that are now handled separately:

Lerna responsibilities and their modern replacements Maps Lerna's linking, task running and versioning/publishing features to package manager workspaces, Turborepo and Changesets or lerna publish. Lerna command Replacement Linking packages lerna bootstrap (removed in v7) pnpm / npm / yarn workspaces Running scripts lerna run build turbo run build Changed packages lerna changed / --since turbo --filter=...[ref] Versioning lerna version Changesets (or keep lerna version) Publishing lerna publish changeset publish (or keep lerna publish)
Split Lerna's jobs: workspaces link, Turborepo runs tasks, a release tool versions and publishes.

lerna bootstrap was removed in Lerna 7; repositories still using it are pinned to old Lerna versions. Since Lerna 6, lerna run already delegates to Nx's task runner under the hood, which is why some teams stay on Lerna for task running. Moving to Turborepo is a choice about the task runner and its caching model, as compared in Choosing a Monorepo Task Runner.

Step 1: let the package manager link packages

If the repository still uses lerna bootstrap, switch to native workspaces first. With pnpm:

# pnpm-workspace.yaml
packages:
  - "packages/*"

Convert internal dependency ranges to the workspace protocol so local packages are always linked:

{
  "dependencies": {
    "@acme/core": "workspace:^"
  }
}

Then remove bootstrap from scripts and CI, delete nested lockfiles Lerna created, and run a single pnpm install at the root. The workspace setup is covered in Workspace Configuration Deep Dive and the protocol in Using the workspace: Protocol Correctly.

Step 2: replace lerna run with Turborepo

Install Turborepo and describe the tasks Lerna used to run:

pnpm add -D -w turbo
{
  "$schema": "https://turborepo.com/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", "lib/**"]
    },
    "test": {
      "dependsOn": ["build"]
    },
    "lint": {}
  }
}

Map commands one to one:

Lerna Turborepo
lerna run build turbo run build
lerna run build --scope @acme/web turbo run build --filter=@acme/web
lerna run build --scope @acme/web --include-dependencies turbo run build --filter=@acme/web...
lerna run test --since main turbo run test --filter=...[main]
lerna run build --stream turbo run build (streams by default)
lerna exec -- rm -rf dist pnpm -r exec rm -rf dist

lerna run ordered tasks by package dependencies; Turborepo does the same through dependsOn: ["^build"], but per task, so lint can run everywhere immediately while build waits for dependencies. Update the root package.json scripts at the same time, so developers who type pnpm build get the new runner without learning new commands on day one.

Staged migration from Lerna Move linking to workspaces, task running to Turborepo, verify outputs, then decide on the release tool, finally remove Lerna. workspaces link packages remove lerna bootstrap one install at the root turbo runs tasks replace lerna run in scripts and CI verify outputs diff dist/ before and after choose release tool Changesets or keep lerna publish highest-risk step remove lerna lerna.json and dependency
Each stage is shippable on its own; releases move last because they carry the most risk.

Step 3: decide how to release

Releases are where Lerna still does real work: computing version bumps from changes or conventional commits, updating internal dependency ranges, tagging, generating changelogs and publishing. You have two sound options.

Keep Lerna for releases only. Modern Lerna works with pnpm workspaces for lerna version and lerna publish. Set "npmClient": "pnpm" in lerna.json so publishing goes through pnpm and workspace: ranges are rewritten. This is the lowest-risk option when your release process is working and nobody wants to change it.

{
  "$schema": "node_modules/lerna/schemas/lerna-schema.json",
  "version": "independent",
  "npmClient": "pnpm",
  "command": {
    "version": { "conventionalCommits": true, "message": "chore(release): publish" },
    "publish": { "registry": "https://registry.npmjs.org/" }
  }
}

Move to Changesets. Changesets records intended version bumps as small Markdown files in pull requests, which makes release intent reviewable and works naturally with pnpm workspaces. Lerna's fixed versioning maps to a Changesets fixed group; independent is the Changesets default. The workflow is covered in Automating Releases with Changesets, and the choice between modes in Choosing Fixed vs Independent Versioning in a Monorepo.

Whichever you choose, run one release with --dry-run (or changeset status --verbose) and compare the planned versions with what Lerna would have done before switching for real.

Hoisting habits that surface during the move

Lerna's bootstrap --hoist placed shared dependencies at the repository root, much like npm's flat layout. Repositories that relied on it for years usually contain imports of packages that are declared only at the root or only in a sibling package. Moving to pnpm workspaces exposes those as Cannot find module errors, which can make the first migration step look far riskier than it is. The errors are phantom dependencies, not migration bugs, and the fix is to declare each import in the package that makes it, as described in Fixing Phantom Dependencies After Switching to pnpm.

A second habit is relying on Lerna's environment variables in scripts. LERNA_PACKAGE_NAME and LERNA_ROOT_PATH are set during lerna run and absent under Turborepo. Replace them with values the package manager provides — npm_package_name is set by pnpm and npm for every script — or with explicit arguments. Search for LERNA_ across the repository before switching task runners so none are missed.

Caching changes the definition of done

Lerna ran every requested task every time. Turborepo skips tasks whose inputs have not changed, which is the point of the migration — and a new source of surprises for scripts that have side effects. A build script that also uploads source maps, or a test script that writes to a shared database, will not run on a cache hit. Move side effects into tasks marked "cache": false (for example, a separate upload:sourcemaps task), and make sure every cached task declares its outputs so a cache hit restores the files later steps need. Treat any script that does more than transform inputs into outputs as a candidate for splitting before you enable remote caching.

Classifying scripts before enabling caching Pure build and test scripts can be cached with declared outputs; scripts with side effects become uncached tasks; long-running scripts become persistent tasks. A script Lerna used to run what does it do besides produce files? Cache it declare outputs nothing else cache: false uploads, deploys, notifications side effects persistent task dev servers, watchers never exits
Decide for each Lerna-era script whether it is pure, has side effects, or never exits.

Verifying the migration

# Build everything with Lerna on the old branch and save outputs
git stash && npx lerna run build && tar -czf /tmp/before.tgz packages/*/dist && git stash pop

# Build with Turborepo, bypassing cache, and compare
pnpm turbo run build --force
mkdir -p /tmp/before && tar -xzf /tmp/before.tgz -C /tmp/before
diff -r /tmp/before/packages packages --exclude=node_modules --exclude=src | head

Differences usually come from scripts that relied on Lerna-specific environment variables (LERNA_PACKAGE_NAME, LERNA_ROOT_PATH) or from ordering: a task that was implicitly ordered by Lerna but has no dependsOn in turbo.json.

Worked example: a five-year-old Lerna repository

A repository with 23 packages uses Lerna 4 with bootstrap and hoisting, lerna run in CI, and lerna publish with conventional commits. The team first moves to pnpm workspaces and fixes eleven phantom dependencies that hoisting had hidden. Next, turbo run replaces lerna run in CI, and remote caching cuts median pipeline time from 18 to 6 minutes. For releases, they keep lerna publish for two release cycles with npmClient: pnpm, then move to Changesets once developers are used to adding changeset files. The final pull request deletes lerna.json and the Lerna dependency.

Prevention and CI/CD guardrails

  • Migrate in stages — linking, task running, releasing — each in its own pull request.
  • Compare build outputs before and after the task runner switch.
  • Dry-run releases before changing the release tool.
  • Replace Lerna environment variables in scripts with explicit arguments or package manager variables.

Frequently Asked Questions

Is Lerna deprecated? No. Lerna is maintained by the Nx team, and lerna version and lerna publish remain widely used. What was removed is bootstrap, add and link, because package managers handle them.

Can I use Turborepo and keep lerna publish? Yes. They do not overlap: Turborepo runs builds and tests, Lerna versions and publishes. Many repositories run exactly this combination.

Do I have to change package versions during the migration? No. Package versions and published history are unaffected by changing the task runner. Only the release tool change can affect how future versions are computed, which is why it goes last and gets a dry run.

What about lerna changed and lerna diff? turbo run build --filter=...[origin/main] --dry lists the packages a change affects, and Changesets' changeset status lists packages with pending releases. Plain git diff --stat origin/main -- packages/<name> replaces lerna diff for a single package.

Should I migrate to Nx instead, since Lerna already uses it? It is a reasonable option: nx init in a Lerna repository keeps Lerna commands working and exposes Nx's full feature set. Choose Turborepo if you prefer its configuration model or already use it elsewhere; the linking and release steps in this guide apply to both.

Related

Choosing a Monorepo Task Runner