Skip to main content

What's different in pnpm 12

· 7 min read
Zoltan Kochan
Lead maintainer of pnpm

pnpm 12 is a rewrite of pnpm in Rust, and it is stable. Upgrading should not feel like a migration. Apart from the differences below, it keeps the commands, flags, settings, and lockfile format of pnpm 11, and the documentation applies to both versions.

Seven things differ. Six of them change a result, and one, a removed flag, fails outright. This post collects them in one place.

Project-aware global bins

A globally installed node, deno, or bun now runs the version the current project pins.

A project pins a runtime through devEngines.runtime or by installing the runtime as a dependency. Run node inside that project and you get the pinned version. Run it outside any project and you get the global one. You no longer need a separate version manager for this.

The setting that controls this is globalShims, and the full description is in Project-aware global bins.

Git dependency resolution

For repositories on GitHub, GitLab, and Bitbucket, a specifier now only says which repository you want, not how to reach it. All of these name the same dependency and resolve the same way:

kevva/is-positive
github:kevva/is-positive
git+https://github.com/kevva/is-positive.git
git+ssh://git@github.com/kevva/is-positive.git

Each resolves through the host's HTTPS URL, and pnpm never records an SSH URL for those hosts. How a machine reaches the host is that machine's Git configuration, not something the project decides. A lockfile written on a laptop with SSH keys installs on a CI runner without them.

pnpm 11 tried transports in turn and could record git@github.com:owner/repo.git, which then failed for everyone without a key for that host. If an older lockfile has such an entry, run pnpm update <package> once to re-resolve it. pnpm does not rewrite the entry on its own. The lockfile is the record of what to install, and pnpm leaves it alone until you ask.

To reach private repositories over SSH, tell Git to rewrite the URL:

git config --global url."git@github.com:".insteadOf https://github.com/

pnpm shells out to git, so the rewrite applies to every Git operation pnpm runs. Unknown hosts and URLs with embedded credentials are covered in How Git dependencies are resolved.

Naming a package manager

pnpm 12 can install the other package managers: npm, Yarn Classic, Yarn Berry, Yarn 6 (yarnpkg/zpm), and Bun. As a result, naming one of them means the tool, not the npm package that shares its name.

Those npm packages are not the tool. yarn on npm stops at Classic. Yarn 4 is published as @yarnpkg/cli-dist. Yarn 6 is not on npm at all. node and deno on npm are wrappers that download a build. So pnx yarn@4 install used to fail with a missing version, and pnpm add -g yarn gave you Yarn 1.

pnpm 11pnpm 12
pnpm add yarninstalls the npm package yarnrecords the project's package manager in packageManager / devEngines.packageManager
pnpm add -g yarninstalls Yarn Classicinstalls the current Yarn line
pnpm add -g node / pnpm add -g denoinstalls a wrapper packageinstalls that Node.js or Deno release
pnx node@22 / pnx denoruns the wrapper packageruns that release
a globally installed package manageralways the global copydefers to a project's pin where there is one

A specifier that points at a specific package still installs that package. pnpm add yarn@npm:yarn@1.22.22 gives you the npm package, and pnx yarn@yarnpkg/berry runs Yarn from its repository.

Two more things follow. A git-hosted dependency is built with the package manager its own repository asks for, so a repository that uses Yarn installs on a machine that has only pnpm. And pnpm shim add yarn links a yarn command that runs whatever version the current project pins. pnpm never creates such a shim as a side effect of pnpm setup or an install, because the shim shadows the rest of your PATH.

The full description is in Other package managers.

Lockfiles of cyclic dependency graphs

pnpm 12 breaks dependency cycles at a fixed place. It orders the packages in a cycle by package ID and always cuts the same edge, no matter where the install entered the cycle. pnpm 11 cut wherever it happened to walk in.

The lockfile now depends only on the dependency graph. Reorder the packages globs in pnpm-workspace.yaml, reorder entries in package.json, or install twice, and you get the same bytes. In pnpm 11 a project with cycles could get a different lockfile each time. Workspaces with many cycles also resolve peers 2–3× faster and use about 25% less memory. Their lockfiles shrink too, because a package inside a cycle no longer gets a separate peer variant for every path that reaches it.

Existing lockfiles keep working. --frozen-lockfile installs them as they are, and an install that doesn't re-resolve leaves them untouched. The first install that does re-resolve rewrites the peer variants of cyclic packages, so expect a one-time lockfile diff. Details in How peers are resolved.

On Linux, the default packageImportMethod: auto now tries a hardlink before a reflink. Hardlinks are cheaper to create, and on btrfs this roughly halves the time an install spends materializing node_modules from a warm store.

Cloning is still the second choice. A store that refuses a hardlink gets a clone, and a store that refuses that too gets a copy. packageImportMethod: clone still asks for a clone outright. Pick that if you edit files inside node_modules, because a hardlinked file is the store's file. Nothing changes on ext4, which never supported cloning, so auto already hardlinked there. macOS keeps clone-first because APFS clonefile is fast.

pnpm 11 keeps clone-first on Linux. Changing what the default writes to disk is not something for a point release.

engineStrict and optional subtrees

With engineStrict on, pnpm 12 fails the install when a package that is being installed depends, through regular dependencies, on a package with an incompatible engine. It no longer matters that the whole subtree sits under an optionalDependencies entry. pnpm 11 installs the package and prints an install-check warning.

What gets skipped has not changed. A package reachable only through optional edges, or through a package that was itself skipped, is still skipped in both versions (#13286).

pnpm install --resolution-only is gone

pnpm 12 does not implement this flag and rejects it:

error: unexpected argument '--resolution-only' found

The flag existed to print peer dependency issues. pnpm peers check does that instead. It reads the issues from the lockfile, so it needs neither a re-resolution nor an install:

pnpm peers check

If a CI script calls --resolution-only, this is the one change on this page that stops a build instead of changing a result. Grep for it before you switch.

Trying it

latest on npm still points at the pnpm 11 line, so install pnpm 12 from the next-12 tag. Homebrew, winget, Scoop, and Chocolatey don't offer it yet. Installing pnpm 12 lists the ways to install it.

Please report any issues you run into.