NEWS

pnpm 12 arrives rewritten in Rust and installing npm, Yarn, and Bun for you

The package manager's new major version keeps pnpm 11's commands and lockfile, but gains 2 to 3x faster peer resolution and now provisions its competitors.

pnpm 12 arrives rewritten in Rust and installing npm, Yarn, and Bun for you
Image: Redação iMasters

pnpm 12 went stable on August 26, 2026, and brings a structural change: it's a complete rewrite of the package manager in Rust. According to Zoltan Kochan, the project's lead maintainer, the version bump was deliberately designed to not be a migration. Commands, flags, configurations, and the lockfile format from pnpm 11 remain valid, and the documentation covers both versions in parallel.

In practice, those who already use pnpm daily can upgrade without rewriting CI scripts or touching pnpm-lock.yaml. The list of what actually behaves differently is concentrated on the What's different in pnpm 12 page, and the rest of the release is additive.

How to install

The latest tag on npm still points to the pnpm 11 line. To get 12, you need to use the next-12 tag:

pnpm self-update next-12

Homebrew, winget, Scoop, and Chocolatey don't yet offer version 12 at launch, so those using these installers need to wait or follow the other methods described in Installing pnpm 12.

The performance gain comes from the Rust rewrite

pnpm's historical argument over its competitors has always been speed and disk savings via a hardlink-based store. The Rust rewrite targets exactly the points where the resolver was still heavy.

The most concrete change is in peer resolution for graphs with dependency cycles. pnpm 12 breaks cycles canonically: the members of each cycle are ordered by package id, and the edges that close the cycle are always cut at the same point, regardless of where the installation enters the cycle. The result is that the lockfile becomes a pure function of the dependency graph: reordered importers, reordered dependencies, and repeated installations produce byte-for-byte identical lockfiles, which wasn't the case before (#13846, #13865).

In large workspaces full of cycles, peer resolution became 2 to 3 times faster, uses about 25% less memory, and generates a substantially smaller lockfile. Existing lockfiles continue to work: --frozen-lockfile consumes them without changes.

On Linux, packageImportMethod: auto now tries hardlink before reflink. According to the release, on a btrfs filesystem this practically cuts in half the time installation spends materializing node_modules from a warmed-up store. ext4 doesn't change (cloning was never supported there), and macOS keeps clone-first via APFS clonefile.

pnpm now installs npm, Yarn, and Bun

The change that most affects the routine of those dealing with varied repositories: pnpm 12 now provisions the other package managers (npm, Yarn Classic, Yarn Berry, Yarn 6, and Bun), fetching each one from trusted registries and verifying the npm signature of the exact version before running it.

This unlocks three things:

  • A git-hosted dependency is prepared with the package manager it requests, so a repository built with Yarn installs on a machine that only has pnpm.
  • The pnx command runs a package manager for a single operation: pnx yarn@4 install, pnx npm@11 ci, pnx node@22.
  • pnpm shim add yarn creates a yarn that runs whatever the current project pins.

There's an important naming subtlety: naming a package manager now means the tool itself, not the npm package with the same name. pnpm add -g yarn@4 installs Yarn Berry, and inside a project pnpm add yarn@4 writes "packageManager": "yarn@4.18.0" (the field Corepack reads). To install the npm package called yarn, you need to be explicit: pnpm add yarn@npm:yarn@1.22.22.

Project-aware global bins

A globally installed node, deno, or bun now follows the version the current project pins, without shell hooks or use-style commands. The globalShims setting chooses which global packages get this shim and ships with { node: true, deno: true, bun: true } by default.

A stable Node.js release is authenticated against the release team's signatures and switches without asking. Deno, Bun, prereleases, and common bins ask Do you trust this project? once per project and per candidate, storing the answer locally. PNPM_SHIM_BYPASS=1 bypasses the feature for a single invocation.

Security and reproducibility

Several changes harden the operation, at a time when attacks on npm's supply chain are a recurring concern for those building software in Brazil:

  • Global commands refuse to run under sudo, failing with ERR_PNPM_SUDO_NOT_SUPPORTED instead of silently operating in root's home directory.
  • Unknown configuration in pnpm-workspace.yaml is now reported. Previously, a mistyped minimumReleaseAge would silently vanish, undermining the policy it was supposed to enforce. Now it suggests the closest name and, when the project pins a pnpm version that the running one satisfies, fails with ERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGS.
  • Registry revisions: a registry can serve a replacement artifact for an already published version (a rebuild with a fixed vulnerability) without changing the version number. pnpm addresses the artifact by its full SHA-512 digest and records it in the lockfile as an extra line (revision: 1). A lockfile that hasn't adopted replacements remains byte-for-byte identical to today's.
  • Git dependencies become identities: github:owner/repo, owner/repo, and the git+https/git+ssh variants all resolve to the host's canonical HTTPS URL, and the lockfile never writes an SSH URL for these hosts.

Fix that prevents breakage with TypeScript 7

One fix deserves the attention of those already testing TypeScript 7: the built-in compatibility database stopped adding dependencies detected through static analysis of published packages. These entries named packages that a dependent only imports for types, and installing them could break everything. The example cited: @typescript-eslint/types used to gain a dependency on typescript resolved to the newest release, placing TypeScript 7 under old versions of @typescript-eslint and making ESLint fail with Cannot read properties of undefined (reading 'Intrinsic').

Other features such as the remote side-effects cache (proof of concept, restoring only on Linux/glibc x64 and arm64 for now), batch approval for stage publishing (pnpm stage approve), and audit.ignorePrune also arrive. It's worth noting that some of them (registry revisions, remote cache, audit.ignorePrune, batch approval, and the pnpm init pin) were also delivered in pnpm 11.25; the rest is exclusive to 12 because it depends on the Rust rewrite.

What remains open

The release doesn't bring benchmark numbers comparing pnpm 12 directly with npm and Yarn in the full installation scenario, only the internal peer resolution gains. There's also no date for the latest tag to migrate from pnpm 11 to 12, nor for system installers (Homebrew, winget, Scoop, Chocolatey) to start offering the version. For Brazilian teams running CI with these package managers, this means continuing to explicitly pin the next-12 tag for now. The project asks that issues be reported in the repository.

Translated from the Brazilian Portuguese original · Read the original