+1 (405) 383-8943

From Yarn 1 to pnpm in a mature Ember app

Most of the mature Ember apps we look at still install with Yarn 1. That is not a criticism; it was the right default for years, and it kept working long after it stopped being maintained. But Yarn Classic is in maintenance-only status, its resolution behaviour is frozen, and — more practically — its flat, hoisted node_modules quietly forgives dependency mistakes that newer build tooling does not. Teams usually discover this halfway through an Embroider or Vite migration, when an import that resolved fine for eight years suddenly does not. The package manager is rarely the project anyone wants. It is often the blocker under the project you actually want.

This is a description of how we sequence that move, and when we tell a client to leave it alone.

Why it comes up now

Three pressures, usually at once.

The first is correctness. Yarn Classic hoists everything it can to the top of node_modules, so any package can require anything that happens to be installed, declared or not. Over a decade, an app accumulates dozens of these undeclared dependencies. Classic ember-cli builds never cared. Embroider and Vite resolve modules the way Node and bundlers actually specify, and they surface every one of those omissions as a build error. The errors are real; they were just invisible before.

The second is disk and CI time. A pnpm store with hard links is dramatically smaller than N copies of an Ember app's dependency tree, and installs on a warm store are fast. On a repo with several in-repo addons the difference is not marginal.

The third is ecosystem drift. Newer addons, the v2 addon format, and most of the current Ember blueprints assume a package manager with real workspace support and correct peer dependency handling. Staying on Yarn 1 means an increasing number of "just follow the README" instructions do not apply to you.

None of that is urgent on its own. It becomes urgent when it is sitting between you and an upgrade you have already committed to.

Do the dependency audit before the migration

The mistake is switching package managers and then triaging the fallout under time pressure. Invert it. Spend a day finding out what is actually undeclared while you are still on a working build.

Run a dependency checker over the app and each in-repo addon and read the output by hand — the false-positive rate on any of these tools is high in Ember codebases, because build-time-only packages and addon-provided imports both look like ghosts. What you are producing is a list: packages imported in source but missing from package.json, packages declared but never used, and packages relied on transitively through some addon's own dependency tree.

Fix the first category immediately, on Yarn 1, as its own set of PRs. Adding a missing dependency entry is a safe, boring change that ships today and is trivially reviewable. By the time you switch package managers, the strict-resolution failures should be mostly pre-empted rather than discovered.

The switch itself

The mechanical part is smaller than people expect: delete node_modules and yarn.lock, run pnpm import to seed a lockfile from the old one so you keep your existing resolved versions, then pnpm install. Pin the package manager in package.json via the packageManager field so CI and every laptop agree. Update every script, Dockerfile, CI step, and contributor doc that says yarn — grep for it, you will find more than you think, including in git hooks and deploy images.

Seeding from the old lockfile matters. Do not take a package-manager change and a mass dependency bump in the same commit; if something breaks you want exactly one candidate explanation.

Then deal with what strictness exposes. Expect three shapes of failure:

Missing declarations you did not catch. Add them. Same fix as above, just later.

Peer dependency warnings that are now errors. Ember addons declare peers loosely and sometimes wrongly. Resolve genuine version conflicts properly; for an addon whose peer range is simply stale and known to be fine, use pnpm.peerDependencyRules in package.json to silence that one case, with a comment saying why and a link to the upstream issue. Blanket-ignoring all peer warnings gives back exactly the correctness you came for.

An addon that genuinely cannot resolve under a strict tree. Rare, and usually an unmaintained one reaching into another package's internals. pnpm.overrides or a targeted patch buys time. This is also useful information: an addon that only works under hoisting is an addon on your replacement list, and that is a separate piece of work with its own justification.

Workspaces, if you have in-repo addons

If the repo already contains addons under lib/, pnpm workspaces are the payoff rather than an extra cost. Declare them in pnpm-workspace.yaml, let each addon own its real dependencies, and link them with workspace:* from the app. Each package's dependencies become explicit and separately upgradeable, which is the same property the v2 addon format wants from you anyway. Teams planning that conversion generally find this step pays for itself twice.

If you have no in-repo addons, skip workspaces. Do not introduce a monorepo layout you do not need.

CI, caching, and rollback

Cache the pnpm store rather than node_modules, keyed on the lockfile. Use pnpm install --frozen-lockfile in CI so a drifted lockfile fails loudly instead of resolving something new. Run the branch through a full CI cycle, then leave it running nightly for a week before merging — install-related breakage often shows up in the least-exercised jobs, like a production Docker build or a scheduled deploy, rather than in the test suite.

Rollback is cheap and worth stating up front: the old yarn.lock is in git history, and reverting the branch restores it exactly. That fact is usually what gets the change approved.

When not to do this

If the app builds, ships, and has no Embroider, Vite, or v2-addon work planned, Yarn 1 will keep working for a while yet. Migrating the package manager on its own buys you tidiness and some CI minutes, and costs a week of engineering attention. We would rather you spent that week on the LTS gap.

The case changes the moment a build modernization is on the roadmap. Then the dependency audit is work you will do regardless, the strictness is the thing doing the auditing for you, and it is much better to face those errors deliberately in a quiet week than in the middle of the migration you actually care about.

If you are weighing a build modernization and want to know how much undeclared-dependency debt is sitting underneath it, that is the kind of thing an assessment answers in days rather than guesses at.