Lewati ke konten utama

pnpm 12.0

· Satu menit membaca
Zoltan Kochan
Lead maintainer of pnpm

pnpm 12 is stable. It is a rewrite of pnpm in Rust, and it is deliberately not a migration: the commands, flags, settings, and lockfile format of pnpm 11 all carry over, and the documentation describes both versions.

The short list of things that genuinely behave differently is in What's different in pnpm 12. This post covers what pnpm 12 adds that pnpm 11 never shipped.

Installing it

latest on npm still points at the pnpm 11 line, so pnpm 12 is installed from the next-12 tag:

pnpm self-update next-12

See Installing pnpm 12 for the other ways, including without Node.js. Homebrew, winget, Scoop, and Chocolatey don't offer it yet.

Breaking changes

Git dependencies are identities

For repositories on GitHub, GitLab, and Bitbucket, a specifier now names a repository rather than choosing a transport. github:owner/repo, owner/repo, git+https://…, and git+ssh://git@… all resolve through the host's canonical HTTPS URL, and the lockfile never records an SSH URL for those hosts. To reach a private hosted repository over SSH, configure the machine with git's own URL rewriting:

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

pnpm shells out to git, so the rewrite applies to all of its git operations. Unknown hosts keep their exact URL, SSH included, and a URL with embedded credentials is kept verbatim and never resolves to a host archive. Details in How git dependencies are resolved.

An unrecognized setting in pnpm-workspace.yaml is reported

A setting pnpm does not recognize used to be ignored in silence — a misspelled minimumReleaseAge dropped the policy it was meant to set, and nothing said so. It is now reported, with the closest real setting name suggested when the key looks like a typo.

It fails the command with ERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGS when the project pins a pnpm version the running pnpm satisfies: with the pin honored, the setting cannot have been meant for a different pnpm version, so it is a mistake to fix rather than a key to ignore. Everywhere else it is a warning, so a project that has yet to be cleaned up keeps working. The pnpm config subcommands never fail on it, so a broken file can still be inspected and repaired.

Lockfiles of cyclic dependency graphs

Dependency cycles are now broken canonically during peer resolution: the members of each cycle are ordered by package id, and the edges that close a cycle are always cut at the same place, wherever the installation walks into the cycle from.

The lockfile therefore becomes a pure function of the dependency graph — reordered importers, reordered dependencies, and repeated installs all produce byte-identical lockfiles, which they could not before (#13846, #13865). On large cycle-heavy workspaces peer resolution is 2–3× faster, uses about 25% less memory, and produces a substantially smaller lockfile.

Existing lockfiles keep working: --frozen-lockfile consumes them unchanged, and an install that skips resolution leaves them untouched. The first install that actually re-resolves re-keys the walk-order-dependent peer variants of cyclic packages once. See How peers are resolved.

A reflink materializes a new inode and copies extent bookkeeping inside the filesystem's metadata trees, where a hardlink is one directory entry — on btrfs that roughly halves the time an install spends materializing node_modules from a warm store. So on Linux, auto now tries the hardlink first.

ext4 is unchanged (cloning was never supported there, so auto already hardlinked), and macOS keeps clone-first, where APFS clonefile is the platform's cheap primitive. On Linux, cloning becomes the second rung rather than the first, so a store that refuses a hardlink still gets a clone; packageImportMethod: clone still asks for one outright.

engineStrict follows the edge, not the subtree

Under engineStrict, an install now fails when an incompatible package is reached through a regular dependencies edge of an installable package, even when that whole subtree hangs off an optionalDependencies entry. pnpm 11 installs the package and emits an install-check warning instead. Packages reachable only through optional edges, or through a package that was itself skipped, are still skipped in both versions (#13286).

New features

Not all of these are exclusive to pnpm 12. Registry revisions, the remote side-effects cache, audit.ignorePrune, batch staged approval, and the pnpm init latest-tag pin ship in pnpm 11.25 as well; the rest are v12 only, because they belong to the Rust rewrite.

Project-aware global bins

A globally installed node, deno, or bun follows the version the current project pins, instead of always running the globally installed one — no shell hooks, no use-style command. The new globalShims setting picks which globally installed packages get such a shim; it defaults to { node: true, deno: true, bun: true } and merges key-wise, so globalShims: { typescript: true } adds one without restating the rest.

A stable Node.js release is authenticated against the Node.js release team's signatures and switches without asking. Everything else — Deno, Bun, Node.js prereleases, ordinary package bins you enable — asks Do you trust this project? once per project and per candidate, and remembers the answer machine-locally. PNPM_SHIM_BYPASS=1 bypasses the feature for one invocation. See Project-aware global bins.

pnpm installs the other package managers

pnpm now provisions npm, Yarn Classic, Yarn Berry, Yarn 6 (yarnpkg/zpm), and Bun, each fetched through the trusted package-manager registries, and each npm-published one verified against npm's signature for its exact version before it runs.

Three things use it. A git-hosted dependency is prepared with the package manager it asks for, so a repository built with Yarn installs on a machine that only has pnpm. pnx runs one for a single command — pnx yarn@4 install, pnx npm@11 ci, pnx node@22. And pnpm shim add yarn links a yarn that runs whatever the current project pins.

Naming a package manager therefore means the tool rather than the npm package that shares its name: pnpm add -g yarn@4 installs Yarn Berry, and in a project pnpm add yarn@4 records "packageManager": "yarn@4.18.0" — what Corepack reads — while every other package manager is recorded in devEngines.packageManager. A specifier that locates a package still installs what it names (pnpm add yarn@npm:yarn@1.22.22). See Other package managers.

Registry revisions

A registry can serve a replacement artifact for an already-published version — a rebuild with a vulnerability patched out — without changing the version number and without rewriting the bytes the canonical name@version URL has always served. pnpm calls each such artifact a revision, addresses it by its complete SHA-512 digest, and records it in the lockfile as one extra line:

pnpm-lock.yaml
packages:
lodash@4.17.21:
resolution:
integrity: sha512-<replacement-digest>
revision: 1

An entry with no revision is revision 0, the original — which is what every entry pnpm has ever written means, so a lockfile that has adopted no replacements is byte-identical to today's.

A dependency or override may pin a revision explicitly as <version>+rN, and pnpm update --patches refreshes the locked artifacts without changing a single version. pnpr serves revisions for the packages it hosts and proxies them for an upstream registry that advertises them. See Registry revisions.

pnpm init pins the latest pnpm

pnpm init pins the latest released pnpm rather than the version that ran the command, so a project scaffolded by an outdated pnpm no longer inherits that staleness through its own pin (#7490). If the latest lookup cannot answer — no network, a slow registry, offline, or a latest that minimumReleaseAge or trustPolicy rejects — the running version is pinned as before. The lookup never fails or hangs the command.

Batch approval for staged publishing

pnpm stage approve approves several staged packages at once. Run it with no stage id to pick from the staged versions interactively, or pass a list. The whole batch is approved with a single one-time password, and pnpm asks for a new one only once the registry stops accepting it. Inside a workspace the packages are approved in dependency order, and one whose workspace dependency could not be approved is skipped rather than published against a dependency that never reached the registry.

audit.ignorePrune

Set audit.ignorePrune: true and pnpm audit --fix removes the ignored GHSA entries that no longer appear in the audit report, so a list of tolerated advisories stops accumulating entries for dependencies that are long gone.

Global commands refuse to run under sudo

pnpm setup, pnpm self-update, and any command that modifies the global installation now fail with ERR_PNPM_SUDO_NOT_SUPPORTED under sudo, instead of silently operating on the root user's home directory. pnpm keeps global packages and configuration in the invoking user's home, so these commands never need root. Read-only global commands such as pnpm bin --global still work.

A remote side-effects cache (proof of concept)

An opt-in proof of concept lets installs reuse a dependency's build output across machines, by publishing and restoring signed, organization-scoped artifacts through pnpr instead of running the lifecycle scripts locally.

A repository names only the eligible remoteSideEffectsCache.organization and packages; everything describing the act of signing is refused in pnpm-workspace.yaml and read from the global config file or the environment instead, so a cloned repository cannot turn the machine's key into a signing oracle. Every cache failure falls back to the ordinary local build. It restores on Linux/glibc x64 and arm64 only for now — see Shared side-effects cache.

Fixes worth knowing about

The compatibility database drops its static-analysis entries

The built-in compatibility database no longer adds dependencies that were detected by static analysis of published packages. Those entries named packages a dependent only imports for its types, so installing them was at best unnecessary and at worst broke the dependent: @typescript-eslint/types gained a typescript dependency resolved to the newest release, which put TypeScript 7 under older @typescript-eslint versions and made ESLint fail with Cannot read properties of undefined (reading 'Intrinsic'). The @yarnpkg/extensions entries and pnpm's own curated ones stay.

A store inside the project when nothing above it is linkable

When no directory above the project accepts a hard link — an AI agent sandbox that grants write access only to the project, or a container with just the project mounted writable — the default store is created at <project>/node_modules/.pnpm-store instead of in the pnpm home directory. In those environments the home store is either read-only or on another volume, which forces every package to be copied instead of hard linked (#13525).

The filterLog pnpmfile hook is deprecated

pnpm 12 ignores hooks.filterLog and warns when a pnpmfile defines it. Use loglevel to choose how much pnpm reports.

Feedback

Please report any issues you run into.