Ad
 
Learn More
Favicon of pnpm Releases

pnpm Releases

Active

pnpm • Last updated about 2 hours ago

Activity Score

100/100
Recency:30/30
Cadence:30/30
Completeness:30/30
Health:10/10
  • Updated in the last week
  • 10 updates in last 30 days
  • Complete entries with dates, titles, URLs, and summaries

Recent Updates

pnpm 12

Major Changes Git dependencies on known hosts (GitHub, GitLab, Bitbucket) are now treated as identities rather than transport choices. Every representation of the same repository — github:owner/repo , owner/repo , git+https://… , git+ssh://git@… — resolves through the host's canonical HTTPS URL, and the lockfile never records an SSH URL for them. Repositories whose archive endpoint is anonymously reachable resolve to the host's archive (fast tarball download); all others resolve to a git clone of the canonical HTTPS URL, which every machine with access to the repository can fetch. To reach a private hosted repository over SSH, configure the machine (not the project) with git's own URL rewriting, for example: git config --global url. " git@github.com: " .insteadOf https://github.com/ pnpm shells out to git , so the rewrite applies to all of pnpm's git operations automatically. URLs of unknown hosts (self-hosted servers) are unaffected and keep their exact URL, including SSH. URLs with embedded credentials are also kept verbatim and never resolve to a host archive. This removes the network probing that previously decided between HTTPS and SSH at resolution time, which could record a transport that only worked on the machine that happened to run the resolution (e.g. an SSH URL that broke CI runners without SSH keys). A project's pnpm-workspace.yaml may no longer carry a setting pnpm does not recognize. Such a setting used to be ignored in silence — a misspelled minimumReleaseAge dropped the policy it was meant to set, and nothing said so. Now it is reported, suggesting the closest real setting name when the key looks like a typo, and 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 be 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 such a setting, so a broken file can still be inspected and repaired, and pnpm config get <key> prints the value with no warnings at all. Keys the global config file cannot set are likewise split between workspace-only settings (still directed to pnpm-workspace.yaml ) and settings unknown to this version. 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, no matter where the installation walks into the cycle from. Previously the cut depended on the walk path, so installing the same dependencies could produce different lockfiles depending on importer order or resolution order #13846 , and a peer-resolution verdict computed for one occurrence of a cyclic package could be wrongly reused at another #13865 . With canonical cycle breaking the lockfile is a pure function of the dependency graph: repeated installs, reordered importers, and reordered dependencies all produce byte-identical lockfiles. Peer dependencies of packages inside a cycle keep nearest-wins resolution along the canonical order, and a dependency edge that closes a cycle references an occurrence of its target resolved at the importer level. On large cycle-heavy workspaces peer resolution is 2–3× faster, uses about 25% less memory, and produces a substantially smaller lockfile (fewer redundant peer variants). Existing lockfiles keep working: headless ( --frozen-lockfile ) installs consume them unchanged, and installs that skip resolution leave them untouched. The first install that actually re-resolves (for example after a dependency change) re-keys walk-order-dependent peer variants of cyclic packages once. packageImportMethod: auto now tries hardlinks before cloning on Linux. 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 this roughly halves the time an install spends materializing node_modules from a warm store. ext4 installs are unchanged (cloning was never supported there, so auto already hardlinked), and macOS keeps clone-first, where APFS clonefile is the platform's cheap primitive. Cloning remains the fallback when the store refuses hardlinks, and remains available explicitly via packageImportMethod: clone . Under engineStrict , an install 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 v11 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 . Minor Changes Globally installed bins can now follow the project you run them in. The new globalShims setting is a record of package names to policies that selects which globally installed packages get project-aware shims; it defaults to { node: true, deno: true, bun: true } and merges key-wise, so globalShims: { bun: false } switches one default off and globalShims: { typescript: true } adds another package. With the default, a project that pins Node.js through devEngines.runtime or engines.runtime gets the pinned stable release — authenticated against the Node.js release-team signatures — downloaded on first use and run whenever you type node inside the project, with no shell hooks. Candidates that are not signature-verified (Deno, Bun, Node.js prereleases, and ordinary package bins you enable) ask "Do you trust this project?" once per candidate and remember the answer machine-locally; the record values name the policy per package: "auto" (or its shorthand true ) defers to artifact authentication, "always" switches without ever asking (useful in CI), and "prompt" always asks, even for authenticated candidates. Set globalShims: false to disable the feature, or PNPM_SHIM_BYPASS=1 to bypass it for one invocation. On Windows, programs can keep spawning the global node.exe directly, without a shell. pnpm installs the other package managers now, not just itself: npm, Yarn Classic, Yarn Berry, Yarn 6 ( yarnpkg/zpm ), and Bun. Each is resolved and fetched through the trusted package-manager registries, and an npm-published one is verified against npm's signature for its exact version before it is executed. Three things use it: A git-hosted dependency is prepared with the package manager it asks for. Its packageManager / devEngines.packageManager pin is honored, and a yarn.lock written by Yarn Classic no longer gets installed by Yarn Berry. pnpm provides that package manager when the dependency pinned a version, or when the host cannot satisfy what the dependency needs — so a repository built with Yarn now installs on a machine that has only pnpm, while a host that already has a suitable one keeps using its own. pnpm dlx ( pnx ) runs one of them for a single command: pnx yarn@4 install , pnx npm@11 ci , pnx bun@1.3.0 install . Naming a package manager, or a runtime ( node , deno , bun ), there now provisions the real thing instead of installing the npm package that shares its name — unless the specifier locates a package rather than asking for a released version ( pnx yarn@npm:yarn@1.22.22 , pnx yarn@yarnpkg/berry ), which installs what it names — pnx yarn@4 was previously a missing version, since Yarn 4 is published as @yarnpkg/cli-dist , and pnx node@22 now runs that Node.js release rather than a wrapper that downloads one. --package naming a package manager picks which of its commands to run, so pnx --package npm@11 npx create-something runs that npm's npx . pnpm shim add yarn links a yarn command that runs whatever version the current project pins, and pnpm shim rm / pnpm shim ls manage those shims. It works for any package, not only package managers. Shims are never created as a side effect of pnpm setup or an install — a shim shadows the rest of your PATH , so pnpm only writes one when asked. Installing a package manager globally ( pnpm add -g yarn ) now makes it follow a project's pin too, the way a globally installed Node.js already follows devEngines.runtime : the pinned version runs where a project pins one, and the globally installed copy is the fallback everywhere else. An explicit globalShims entry, including false , is left as you set it. pnpm add follows the same rule about what a name means. pnpm add -g yarn@4 installs Yarn Berry — it used to fail, because npm's yarn package stops at Classic — and pnpm add -g node@22 / pnpm add -g deno@2 install that Node.js or Deno release rather than a wrapper package that downloads one. In a project, naming a package manager records which one the project uses instead of installing it as a dependency, and naming a runtime records it under engines.runtime as node@runtime:22 already did. The declaration goes where the package manager reads it. Yarn is started from a project pin by corepack, which reads only packageManager and only accepts an exact version there, so pnpm add yarn@4 resolves the line and writes "packageManager": "yarn@4.18.0" — the same thing corepack use yarn@4 writes, down to the +sha512.… integrity for the Yarn Classic line that corepack pins its tarball with. Every other package manager is recorded in devEngines.packageManager , which holds a range. Only one of the two fields is ever left behind: they declare the same thing, and corepack refuses to run a project whose declarations disagree. A JavaScript package manager on a machine without Node.js gets a managed LTS runtime to run on. What changes for a project coming from v11: pnpm add yarn records the project's package manager instead of installing the npm package that shares the name (that package is still reachable as pnpm add yarn@npm:yarn@1.22.22 ), pnpm add -g yarn installs the current Yarn line rather than Classic, pnpm add -g node / pnpm add -g deno and pnx node / pnx deno install a Node.js or Deno release rather than a wrapper package, and a globally installed package manager defers to a project's pin where there is one. Added an opt-in proof of concept that 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. Configure it with the new remoteSideEffectsCache setting. A workspace names the eligible organization and packages ; everything describing the act of signing — publish , keyId , builderId , trustedKeys , privateKey and the provenance fields — is refused in pnpm-workspace.yaml and read from the global config file or the environment instead. Added the audit.ignorePrune setting. When set to true , pnpm audit --fix removes ignored GHSA entries that no longer appear in the audit report. pnpm init now pins the latest pnpm version, instead of the version of pnpm that ran the command. A project scaffolded by an outdated pnpm therefore no longer inherits that staleness through its own devEngines.packageManager / packageManager pin #7490 . The version is read from the latest tag on the package-manager registries. When that lookup cannot answer — no network, an unreachable or slow registry, offline , or a latest that the minimumReleaseAge / trustPolicy settings reject — pnpm init pins the running version as before, and never fails or hangs on the lookup. A latest that is older than the running pnpm is never pinned either. Allowed pnpm update --patches to refresh registry revisions through a configured pnpr server while retaining locked package versions. Added explicit registry revision selection with <version>+rN and pnpm update --patches for refreshing revision artifacts without changing package versions. Registry-backed lockfile policy checks recognize historical revisions, and pnpr now preserves safe revision histories from upstream registries. Added support for registry replacement tarballs using standard integrity values, explicit revision fields, registry routing from the registries setting, non-redirecting integrity-addressed URLs, canonical safe-integer revision numbers, and pnpr proxying for immutable upstream revision artifacts. Running pnpm setup , pnpm self-update , or a command that modifies the global installation (such as pnpm add --global ) through sudo now fails with ERR_PNPM_SUDO_NOT_SUPPORTED instead of silently operating on the root user's home directory. pnpm keeps global packages and configuration in the invoking user's home directory, so these commands never need root permissions. Read-only global commands (such as pnpm bin --global ) still work under sudo. pnpm stage approve now approves several staged packages at once. Run it without a stage id to pick from the staged versions interactively, or pass a list of stage ids. 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 selected packages are approved in dependency order, and a package whose workspace dependency could not be approved is skipped instead of being published against a dependency that never reached the registry. Patch Changes Deprecated the pnpmfile filterLog hook in pnpm v12. The Rust CLI ignores it and emits a warning. The built-in compatibility database no longer adds dependencies that were detected by static analysis of published packages. Those entries named packages that are only imported for their 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 database keeps its @yarnpkg/extensions entries and pnpm's own curated ones. When no directory above the project accepts a hard link — inside an AI agent sandbox that only grants write access to the project, or a container with just the project mounted writable — the default store is now 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 . Platinum Sponsors Gold Sponsors

Read more →

pnpr 0.1.0-alpha.8

Minor Changes Added an opt-in proof of concept that 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. Configure it with the new remoteSideEffectsCache setting. A workspace names the eligible organization and packages ; everything describing the act of signing — publish , keyId , builderId , trustedKeys , privateKey and the provenance fields — is refused in pnpm-workspace.yaml and read from the global config file or the environment instead. Hosted pnpr registries now serve newly published original artifacts from registry-scoped SHA-512 digest URLs. Allowed pnpm update --patches to refresh registry revisions through a configured pnpr server while retaining locked package versions. Added explicit registry revision selection with <version>+rN and pnpm update --patches for refreshing revision artifacts without changing package versions. Registry-backed lockfile policy checks recognize historical revisions, and pnpr now preserves safe revision histories from upstream registries. Added support for registry replacement tarballs using standard integrity values, explicit revision fields, registry routing from the registries setting, non-redirecting integrity-addressed URLs, canonical safe-integer revision numbers, and pnpr proxying for immutable upstream revision artifacts. Patch Changes Forward patchedDependencies hashes and packageExtensions to pnpr so server-side resolution preserves patches and package extensions in the lockfile and installed packages.

Read more →

pnpm 12 RC 11

Minor Changes pnpm stage approve now approves several staged packages at once. Run it without a stage id to pick from the staged versions interactively, or pass a list of stage ids. 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 selected packages are approved in dependency order, and a package whose workspace dependency could not be approved is skipped instead of being published against a dependency that never reached the registry. Patch Changes Under nodeLinker: isolated , a Bit root-component member whose materialized copy carries no package.json now receives sibling symlinks for the dependencies its own lockfile snapshot declares, instead of a symlink to every other member of the root. The all-member fallback remains only when no snapshot exists. The update notification now suggests pnpm self-update when PNPM_HOME manages the pnpm in use, and the standalone install script otherwise — under Corepack, or when another package manager installed pnpm. pnpm self-update under Corepack names the standalone install script too. Platinum Sponsors Gold Sponsors

Read more →

pnpm 11.24

Minor Changes Added global build approvals pnpm/pnpm#14101 . Patch Changes Fixed pnpm v11 incorrectly reporting confirmModulesPurge as unrecognized when set in pnpm-workspace.yaml . The Rust CLI now identifies the unsupported option as a pnpm v11 setting instead of suggesting an unrelated setting. pnpm install --frozen-lockfile no longer fails with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE when the pinned pnpm version recorded in pnpm-lock.yaml has to be re-resolved before it can be installed. It runs the pnpm version the lockfile pins and leaves the lockfile unchanged #14124 . Under nodeLinker: hoisted , peer-resolution variants of an injected directory dependency (a file: snapshot) are materialized as separate copies again instead of collapsing onto the first-seen variant. Each copy keeps its own peer-resolved dependency set, so a project pinning one peer version no longer resolves another project's variant — Bit root components with conflicting peers across injected copies rely on this. Fixed pnpm install --merge-git-branch-lockfiles --frozen-lockfile failing with ERR_PNPM_OUTDATED_LOCKFILE when a branch lockfile predates the removal of a dependency, or its move to another dependency group #13966 . A dependency that no project declares anymore is no longer reinstated by the merge, and the packages it was the only path to are dropped with it. Batch workspace publishing accepts a shared scope-specific credential, rejects mismatched credentials for a registry before publishing, and runs the publish and postpublish scripts after each completed registry group pnpm/pnpm#14101 . The Rust CLI now honors five settings it recognized but ignored: updateNotifier , legacyDirFiltering , initAuthorName / initAuthorEmail / initAuthorUrl , initLicense , and initVersion . pnpm install and pnpm add check once a day for a newer pnpm and print how to get it (turn it off with updateNotifier: false ); a {<dir>} filter selector can go back to matching the subtree below the directory with legacyDirFiltering: true ; and pnpm init writes the configured author, license, and version into the package.json it scaffolds. PNPM_CONFIG_INIT_VERSION is now read as well. maxsockets , npm's spelling of maxSockets , is no longer ignored: both spellings are read from pnpm-workspace.yaml , the global config file, the environment, and the command line, in that increasing order of precedence — a value passed on the command line now wins even when the two sides spelled the setting differently. A lastUpdateCheck timestamp dated in the future — after a clock change, a restored snapshot, or a hand-edited state file — no longer silences the update check until that time comes around. legacyDirFiltering no longer reaches the workspace-root selectors pnpm generates for itself: the !{<workspace-root>} exclusion a recursive run / exec / add / test appends, and the {<workspace-root>} inclusion --workspace-root appends. Read as subtree matches they named every project below the root, so a recursive command under the setting selected nothing at all, and --workspace-root pulled in every project below the root instead of the root alone #14101 . pnpm install --frozen-lockfile no longer fails when pnpm-lock.yaml records the pinned pnpm version alongside an engine package the running pnpm does not install it from. An entry pinning another version is still refused, and a plain install rewrites the block #14124 .

Read more →

pnpm 12 RC 10

Patch Changes Fixed pnpm v11 incorrectly reporting confirmModulesPurge as unrecognized when set in pnpm-workspace.yaml . The Rust CLI now identifies the unsupported option as a pnpm v11 setting instead of suggesting an unrelated setting. A +<algorithm>.<hash> build in a devEngines.packageManager version no longer makes pnpm install --frozen-lockfile fail with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE on a lockfile a plain install kept rewriting identically #14124 . The built-in compatibility database no longer adds dependencies that were detected by static analysis of published packages. Those entries named packages that are only imported for their 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 database keeps its @yarnpkg/extensions entries and pnpm's own curated ones. pnpm install --frozen-lockfile no longer fails with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE when the pinned pnpm version recorded in pnpm-lock.yaml has to be re-resolved before it can be installed. It runs the pnpm version the lockfile pins and leaves the lockfile unchanged #14124 . Under nodeLinker: hoisted , peer-resolution variants of an injected directory dependency (a file: snapshot) are materialized as separate copies again instead of collapsing onto the first-seen variant. Each copy keeps its own peer-resolved dependency set, so a project pinning one peer version no longer resolves another project's variant — Bit root components with conflicting peers across injected copies rely on this. Fixed pnpm install --merge-git-branch-lockfiles --frozen-lockfile failing with ERR_PNPM_OUTDATED_LOCKFILE when a branch lockfile predates the removal of a dependency, or its move to another dependency group #13966 . A dependency that no project declares anymore is no longer reinstated by the merge, and the packages it was the only path to are dropped with it. Record the pnpm version a project pins even when the install has nothing else to do. Adding a devEngines.packageManager (or packageManager ) pin to a project whose dependencies are already installed left packageManagerDependencies unwritten, so pnpm install --frozen-lockfile failed with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE while a plain pnpm install reported "Already up to date" without recording it #14124 . pnpm install --frozen-lockfile no longer fails when pnpm-lock.yaml records the pinned pnpm version alongside an engine package the running pnpm does not install it from. An entry pinning another version is still refused, and a plain install rewrites the block #14124 . Platinum Sponsors Gold Sponsors

Read more →

pnpm 12 RC 9

Major Changes A project's pnpm-workspace.yaml may no longer carry a setting pnpm does not recognize. Such a setting used to be ignored in silence — a misspelled minimumReleaseAge dropped the policy it was meant to set, and nothing said so. Now it is reported, suggesting the closest real setting name when the key looks like a typo, and 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 be 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 such a setting, so a broken file can still be inspected and repaired, and pnpm config get <key> prints the value with no warnings at all. Keys the global config file cannot set are likewise split between workspace-only settings (still directed to pnpm-workspace.yaml ) and settings unknown to this version. Minor Changes Added global build approvals pnpm/pnpm#14101 . Added recursive global outdated checks pnpm/pnpm#14101 . pnpm config get and pnpm config list now show the settings pnpm acts on under their documented names: registries shows the registries pnpm resolves from, merged across every source ( .npmrc , pnpm-workspace.yaml , the global config, CLI flags), in the shape the setting is written in: keyed by registry URL, with the default registry declared as the bare @ scope. Built-in routes are included — the @jsr scope and the npmjs and gh prefixes — unless pointed elsewhere. Previously pnpm config get registries printed undefined . update and audit show the effective sections, whichever spelling set them. The deprecated internal spellings ( updateConfig , auditConfig , auditLevel ) are no longer listed. catalogs shows the complete resolved catalog set — the singular catalog block is its default entry — whichever spelling declared it. The registry and @scope:registry entries show the merged routes rather than raw .npmrc values, so they always agree with the registries view. Added support for configuring stateDir in the Rust pnpm CLI pnpm/pnpm#12042 . Added bounded workspace concurrency for recursive run and exec commands pnpm/pnpm#14101 . @pnpm/napi gained reporter output, reverse dependency queries, and lockfile access. install and rebuild accept options.reporter and render pnpm's terminal output — progress line, packages-diff summary, lifecycle output, and the Done in … footer. Rendered output goes to stdout, or to an onOutput callback for a host that writes its own output through JavaScript. New reporting options: hideLifecycleOutput , ignoredBuildsInstructionText , and hideLinkedPkgsDiff . getDependents returns the reverse dependency trees behind pnpm why , annotated with the package.json fields named in manifestFields . renderDependents returns those trees rendered as tree, parseable, or JSON output. readLockfile and writeLockfile read and write pnpm-lock.yaml (or the current lockfile under the virtual store). filterLockfileByImporters returns a lockfile narrowed to what the named importers reach. readModulesManifest returns the .modules.yaml state of an installed node_modules . Top-level lockfile keys pnpm does not define are no longer dropped when a lockfile is loaded and saved, so state a tool records beside pnpm's own keys survives a rewrite. pnpm now supports per-branch lockfiles in its Rust engine: gitBranchLockfile gives each git branch its own pnpm-lock.<branch>.yaml , so two branches can hold different resolutions without conflicting on one file. A branch that has no lockfile yet installs against the shared pnpm-lock.yaml . mergeGitBranchLockfiles (and the --merge-git-branch-lockfiles flag on pnpm install ) folds every branch lockfile back into pnpm-lock.yaml and deletes them, which is what merging a branch into the mainline needs. mergeGitBranchLockfilesBranchPattern (and --merge-git-branch-lockfiles-branch-pattern ) names the branches that merge automatically, so a mainline branch does not have to pass the flag by hand #12042 . Added PNPM_CONFIG_VIRTUAL_STORE_ONLY and PNPM_CONFIG_ENABLE_MODULES_DIR support to the Rust pnpm CLI. Added support for the lockfileDir setting and its --lockfile-dir <dir> flag on pnpm install , add , update , and remove . pnpm-lock.yaml , the root node_modules holding the virtual store, and the config dependencies now live in the given directory, each project is recorded under its path relative to it, and every project keeps its own node_modules of symlinks — so several projects can share one lockfile #12042 . Added support for the preferSymlinkedExecutables setting. On POSIX systems, node_modules/.bin entries are created as symlinks to the executable files instead of shell shims, and NODE_PATH pointing at the virtual store of the workspace root is exported to spawned scripts so they can resolve dependencies from the hoisted store. Like the TypeScript CLI, the setting turns on automatically when nodeLinker is set to hoisted . Added the six CLI flags the TypeScript pnpm CLI accepts but the Rust CLI did not #14101 : --stream prints a recursive command's script output as it arrives, one line at a time, prefixed with the project it came from, instead of letting the scripts write to the terminal directly. --parallel implies it, as in pnpm. --aggregate-output holds each script's streamed output until the script exits and then prints it as one block, so concurrent projects can't interleave. --reporter-hide-prefix drops that project prefix from the scripts' own output lines. On a recursive pnpm exec , the opposite spelling --no-reporter-hide-prefix turns the prefixing on. --use-stderr sends the reporter's output to stderr, leaving stdout for the command's own result. --ignore-workspace runs the command as if the project were standalone: no workspace root is discovered, so pnpm-workspace.yaml contributes neither settings nor sibling projects, and a blocked dependency build is not scaffolded into its allowBuilds . --workspace-packages overrides the packages patterns of pnpm-workspace.yaml for the run. The stream , aggregateOutput , reporterHidePrefix , useStderr , and ignoreWorkspace settings are now read from pnpm-workspace.yaml , the global config.yaml , and their PNPM_CONFIG_* environment variables too. Added support for the shellEmulator setting. With it enabled, the scripts pnpm run executes, a project's own lifecycle scripts, and dependencies' build scripts run in a built-in POSIX shell instead of the platform's ( sh -c , or cmd /d /s /c on Windows), so scripts written for sh behave the same on every OS. scriptShell is not used while the emulator is on. The Rust engine now checks that a package read back from the store is the package it was recorded as. When the tarball's package.json names a different name or version than the store entry was keyed for — a broken lockfile, or a registry serving content that doesn't match its metadata — the install fails with ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE . Set the new strictStorePkgContentCheck setting to false to downgrade the failure to a warning and install from the entry anyway #12042 . pnpm now supports three workspace settings in its Rust engine: includeWorkspaceRoot (and the universal --include-workspace-root / --no-include-workspace-root flags) keeps the workspace root project in a recursive run , exec , add , or test , which otherwise leave it out. ignoreWorkspaceCycles and disallowWorkspaceCycles control the report an install makes when workspace projects depend on each other in a cycle: it is a warning by default, an ERR_PNPM_DISALLOW_WORKSPACE_CYCLES error under disallowWorkspaceCycles , and silent under ignoreWorkspaceCycles #12042 . Added support for the remaining pnpm default settings, including recursive command controls, optional dependency selection, workspace-root checks, color modes, lockfile compatibility, and pack manifest options. Batch workspace publishing accepts a shared scope-specific credential, rejects mismatched credentials for a registry before publishing, and runs the publish and postpublish scripts after each completed registry group pnpm/pnpm#14101 . Added the commands the Rust CLI was still missing: pnpm get <key> and pnpm set <key> <value> — the top-level spellings of pnpm config get and pnpm config set . pnpm store status — reports the packages whose files no longer match the store they were expanded from, failing with ERR_PNPM_MODIFIED_DEPENDENCY ; and pnpm store add <pkg>... — fetches packages into the store without writing a manifest, a lockfile, or node_modules . pnpm env use --global <version> and pnpm env list [<selector>] , the deprecated Node.js-only front end to pnpm runtime . pnpm edit , pnpm profile , pnpm token , and pnpm xmas now fail with ERR_PNPM_NOT_IMPLEMENTED pointing at the npm CLI, instead of being taken for a package script. An install that resolves the dependency graph now reports the unmet peer dependencies it leaves behind, matching the TypeScript CLI. By default it warns once — Issues with peer dependencies found. Run "pnpm peers check" to list them. — and with strictPeerDependencies it fails with ERR_PNPM_PEER_DEP_ISSUES after the artifacts are written, listing every unmet peer. This covers pnpm install , add , remove , update and --lockfile-only ; pnpm dedupe reported the same verdict already, and now shares the reporting with them. peerDependencyRules are applied before the verdict, so a rule that covers every issue leaves nothing to report, and a --filter ed install reports only on the projects it installed. An install that skips resolution — a frozen install, or one whose pnpm-lock.yaml is already up to date — reports nothing, as in the TypeScript CLI; pnpm peers check inspects such a tree #14098 . Added fetchWarnTimeoutMs and fetchMinSpeedKiBps to the Rust pnpm CLI and its N-API bindings. Slow registry metadata requests and tarball downloads now emit pnpm-compatible warnings without exposing URL credentials, query parameters, fragments, or control characters pnpm/pnpm#12042 . Added filtered and split SBOM generation with per-project lockfiles, including reachable workspace projects and incomplete-graph validation pnpm/pnpm#14101 . Patch Changes Kept pending build approvals available after removing an unrelated dependency. Fixed resolving the chcp command on Windows during pnpm setup by looking for chcp.com before chcp pnpm/pnpm#13991 . A custom fetcher can no longer replace the archive integrity that pnpm-lock.yaml pins: the locked value is restored after a canFetch or fetch hook rewrites the resolution, and delegating a locked archive to a directory or git source now fails instead of installing unverified content. The Rust CLI now also loads the pnpmfiles named by the pnpmfile setting (a single path or an ordered list), and hands custom fetchers native localTarball and remoteTarball callbacks — including on a fresh install that has to compute a missing tarball integrity, which is then reused by later offline installs. File maps a fetcher returns are accepted only when they match what those native callbacks extracted. pnpm dedupe accepts the pnpm install options that pnpm documents for it — --lockfile-only , --ignore-scripts , --offline , and --prefer-offline — instead of rejecting them with unexpected argument . Without --lockfile-only , pnpm dedupe now also updates node_modules , as an install does #14107 . pnpm dedupe in the Rust engine now fails with ERR_PNPM_PEER_DEP_ISSUES when strictPeerDependencies is set and unresolved peer dependency issues remain after deduplication, matching the TypeScript CLI #14099 . Previously it only ever printed a warning, regardless of the setting. pnpm deploy --prod and pnpm deploy --no-optional no longer list the excluded dependency groups in the deployed package.json and pnpm-lock.yaml . The deployed lockfile referenced packages that the deploy left out of its graph, so installing in the deploy directory afterwards created dangling symlinks #13623 . pnpm install --dev and pnpm deploy --dev no longer install optional dependencies, and --prod now takes precedence when combined with --dev , matching the TypeScript pnpm CLI. A dependency published with "bin": "" , such as url-loader@1.1.2 , no longer fails the install with ERR_PNPM_CMD_SHIM_PROBE_SHIM_SOURCE #13962 . An empty bin declares no command, as it does in pnpm v11, so no shim is written for the package; a directories.bin entry on the same package is still linked. A dependency pinned to an exact version carrying semver build metadata ( "@parcel/codeframe": "2.0.0-canary.1718+d8408010f" ) installs again instead of failing with ERR_PNPM_NO_MATCHING_VERSION #14096 . npm strips build metadata when it publishes a version, so pnpm strips it from the version it looks up, matching npm and pnpm v11. A package's files entries now match only at the package root, the way npm reads them. A bare src used to also match nested directories such as example/src , so a dependency installed from git could ship the repository's own example app. The same filter decides what pnpm pack and pnpm publish put in a tarball and what pnpm deploy copies, so those stop carrying the extra files too. Exclusions such as !**/__tests__ and !*.map still match at any depth. A package already in the store keeps its old file set until it is fetched again. A pnpm install --filter <selector> run that has nothing to do now reports "Already up to date" without entering the install pipeline, the same way an unfiltered pnpm install already did #14033 . On Windows, upgrading pnpm no longer leaves a stale pnpm.ps1 behind. PowerShell resolves pnpm.ps1 ahead of pnpm.cmd , so a shim written by an older installation kept running the previous version. Linking the pnpm CLI's bins now deletes it #13919 . Settings written to a pnpm-workspace.yaml block that uses inline (flow) YAML — catalog: { foo: ^1.0.0 } , overrides: { foo: 1.0.0 } , minimumReleaseAgeExclude: [foo@1.0.0] — are now edited in place instead of failing or corrupting the file. pnpm audit , pnpm link , pnpm approve-builds , pnpm patch , pnpm add --config , and catalog updates all keep the block's flow style, its other entries, and its comments #14108 . A frozen install no longer rewrites the packageManagerDependencies block of pnpm-lock.yaml . When the pnpm version pinned by devEngines.packageManager (or by packageManager ) is missing from the lockfile or no longer matches it, --frozen-lockfile now fails with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE instead of resolving the version and saving it, so a manifest whose pin was bumped without regenerating the lockfile can no longer pass CI #14009 . When a git-hosted dependency is blocked from running build scripts, the error now suggests an allowBuilds entry that actually approves it. It quoted the bare package name, which never matches a git-hosted package, so following the suggestion left the install failing the same way #14002 . A git dependency installed over HTTPS from a hosted repository now keeps its branch, tag, or version range in the specifier recorded in package.json . It was written back without one, so the next pnpm update moved the dependency to the repository's default branch #13999 . Added support for the globalPnpmfile setting, which names a user-level pnpmfile that runs for every project ahead of the project's own. Like pnpm, it is left out of the lockfile's pnpmfileChecksum , so editing it does not decide whether a lockfile is still current. pnpmfile and globalPnpmfile are now also readable from PNPM_CONFIG_PNPMFILE and PNPM_CONFIG_GLOBAL_PNPMFILE . Fix recursive pnpm update <name>@<version> so an exact pinned update stays scoped to the requested version line: copies of the same package on another major line — or, for a 0.x request, another minor line — keep their locked resolution instead of being re-resolved along with the target. Under nodeLinker: hoisted , a dependency declared against a peer-resolution variant of a package version is no longer dropped from the installed layout. All variants of a version share one hoisted copy, and edges pointing at any of them now resolve to it, so the depending project keeps the package in its .package-map.json and the depending package keeps it in its node_modules/.bin . A repeat pnpm install with nodeLinker: hoisted is a no-op again when a workspace package declares the dependencies #14001 . The hoisted linker installs them into the root node_modules , but the up-to-date check previously looked under each package's own node_modules and reinstalled the whole tree every time. A hoisted install also no longer reports the packages it just wrote as broken. ignorePnpmfile can now be set in pnpm-workspace.yaml and read from PNPM_CONFIG_IGNORE_PNPMFILE , not only passed as --ignore-pnpmfile , so a project or a machine can turn pnpmfile hooks off once instead of adding the flag to every command. The flag still applies on top. As in pnpm, the global config.yaml cannot set it: a pnpmfile belongs to the project that ships it. Fixed pnpm failing to read .modules.yaml files containing long dependency paths #13875 . The manifest is now parsed as JSON (the format pnpm writes it in), falling back to the YAML parser only for manifests written by old pnpm versions. --config.minimum-release-age is honored again, along with --config.minimum-release-age-exclude , --config.minimum-release-age-ignore-missing-time and --config.minimum-release-age-strict . Each overrides the matching pnpm-workspace.yaml setting, and the exclude flag may be repeated to build a list #13929 . An unreadable node_modules/.modules.yaml no longer makes pnpm install delete node_modules and relink every package on each run. The unparsable state file is now reported as an error instead #14062 . pnpm outdated and pnpm update --interactive now leave out the dependencies listed in updateConfig.ignoreDependencies , instead of reporting them and offering them for update. Fixed pnpm outdated and pnpm update --interactive offering versions blocked by minimumReleaseAge pnpm/pnpm#14004 . pnpm pack writes tar entries in the POSIX ustar header form npm uses — ustar\0 magic and the explicit 0 regular-file typeflag — instead of the GNU form with a NUL typeflag, which strict tar readers such as publint mistake for the end-of-archive marker #13924 . Fixed --config.ignore-scripts=true not being honored by CLI commands such as pnpm pack #13986 . pnpm install <pkg> now adds the package, the same as pnpm add <pkg> and matching the JavaScript CLI. It previously ended in a usage error: pnpm i valibot printed error: unexpected argument 'valibot' found instead of saving the dependency #13886 . Fixed Plug'n'Play projects to preload .pnp.cjs for dependency and project lifecycle scripts, pnpm run , and pnpm exec . The generated loader now also exposes the public Yarn PnP API surface. Workspace packages declared with a parent-relative pattern in pnpm-workspace.yaml ( ../shared , ../../docs/* ) are discovered again. They were dropped from the project list, so pnpm list -r and --filter did not see them and a frozen install of a lockfile that already held their importer entries failed with ERR_PNPM_PACKAGE_MANAGER_UNSAFE_IMPORTER_PATH . pnpm pkg get and pnpm pkg set now accept hyphens inside a dot-notation property path, so pnpm pkg get dependencies.some-package-name reads the key instead of failing with ERR_PNPM_UNEXPECTED_TOKEN_IN_PROPERTY_PATH . The bracketed and quoted forms already worked and are unchanged. A path named by the pnpmfile setting that is not on disk now fails with ERR_PNPM_PNPMFILE_NOT_FOUND and names the file, instead of surfacing as a generic pnpmfile execution failure. Discovery of the default .pnpmfile.mjs / .pnpmfile.cjs is unaffected: a project that ships neither still installs normally. pnpm remove now prunes undecided entries ( "set this to true or false" ) from allowBuilds in pnpm-workspace.yaml when sharedWorkspaceLockfile: true and the corresponding packages are removed pnpm/pnpm#13892 . Suggest pnpm shim add <runtime> after pinning a project runtime when no project-aware global shim is installed. Explicit project-aware shims now reject unrelated global bin conflicts and are restored after a matching global package is removed or replaced by a version that drops its bin. pnpm -r update --latest --depth 0 <selector> now fails with ERR_PNPM_NO_PACKAGE_IN_DEPENDENCIES when no project in the workspace declares a matching dependency, instead of silently doing nothing. Fixed repeat installs paying for a full lockfile comparison forever after a modification-time collision. When a package.json was last modified inside the same clock tick that the install recorded as its validation baseline — a fast install, a checkout that copied files with identical timestamps, or any filesystem that keeps only whole-second modification times — the manifest kept reading as possibly-modified, so every later pnpm install and verify-deps-before-run check re-compared the manifests against the lockfile instead of taking the fast path #13907 . The Rust CLI now honors five settings it recognized but ignored: updateNotifier , legacyDirFiltering , initAuthorName / initAuthorEmail / initAuthorUrl , initLicense , and initVersion . pnpm install and pnpm add check once a day for a newer pnpm and print how to get it (turn it off with updateNotifier: false ); a {<dir>} filter selector can go back to matching the subtree below the directory with legacyDirFiltering: true ; and pnpm init writes the configured author, license, and version into the package.json it scaffolds. PNPM_CONFIG_INIT_VERSION is now read as well. maxsockets , npm's spelling of maxSockets , is no longer ignored: both spellings are read from pnpm-workspace.yaml , the global config file, the environment, and the command line, in that increasing order of precedence — a value passed on the command line now wins even when the two sides spelled the setting differently. A lastUpdateCheck timestamp dated in the future — after a clock change, a restored snapshot, or a hand-edited state file — no longer silences the update check until that time comes around. legacyDirFiltering no longer reaches the workspace-root selectors pnpm generates for itself: the !{<workspace-root>} exclusion a recursive run / exec / add / test appends, and the {<workspace-root>} inclusion --workspace-root appends. Read as subtree matches they named every project below the root, so a recursive command under the setting selected nothing at all, and --workspace-root pulled in every project below the root instead of the root alone #14101 . pnpm sbom now honours --filter-prod , the full --filter selector syntax (dependency queries such as pkg... , {dir} and glob paths, [since] change queries, exclusions), and --workspace-root . Selectors that match no project print No projects matched the filters and write no SBOM, and --split emits its per-project SBOMs in a stable order. The universal --fail-if-no-match flag is supported too: any filtered command whose selectors match no workspace project now exits with code 1 #14064 . pnpm sbom now fails with ERR_PNPM_SBOM_MISSING_IMPORTERS when pnpm-lock.yaml has no entry for a selected project, instead of writing an SBOM that under-reports that project's dependencies. Previously this crashed with Cannot read properties of undefined (reading 'devDependencies') . pnpm self-update now rewrites a simple devEngines.packageManager.version range ( ^ / ~ ) to the newly installed version, keeping the operator — matching how pnpm update and pnpm runtime set rewrite ranges. Complex ranges such as >=8.0.0 that the new version satisfies are still left unchanged #13935 . pnpm update now preserves the existing range operator when updating a prerelease dependency. See #7002 . pnpm update <name>@<version> now fails with ERR_PNPM_UPDATE_VERSION_ON_INDIRECT_DEP when the package is not a direct dependency of any selected project, instead of quietly updating it to whatever a fresh install would resolve. There is nowhere to record the version in that case, so the request cannot be honored, and the error points at the overrides entry that does pin a transitive dependency. Ranges and tags are unaffected, and a package that any selected project declares directly still takes its version as before. trustPolicy: no-downgrade no longer aborts the install with ERR_PNPM_MISSING_TIME on registries that serve no per-version time field when minimumReleaseAgeIgnoreMissingTime is set. The trust check reads the same publish dates the minimumReleaseAge check does, so it now honors the same opt-in and skips the affected package with a warning #12446 . minimumReleaseAgeIgnoreMissingTime no longer lets a lockfile entry the registry does not list pass the minimumReleaseAge check during lockfile verification. The opt-in covers a registry that cannot date its releases; a packument that does date every version it lists is saying it never published this one, which stays a hard failure. The missing- time warning now names the check it is reporting on, so a package whose minimumReleaseAge and trustPolicy checks are both skipped warns about both instead of only the first. pnpm update <pkg>@<tag> now saves the version the dist tag resolved to in package.json , keeping the range operator the dependency already declared, instead of saving the tag itself. A dependency declared through a catalog: reference, a workspace: or npm: alias, or a path or git specifier keeps its declaration, and one that already tracks a dist tag records the tag asked for #14092 . pnpm update <pkg>@<version> now updates only the selected packages and leaves unrelated dependencies unchanged. A selector that renames the package it installs — pnpm update <alias>@npm:<pkg>@<version> or the jsr: equivalent — now targets the package the alias installs rather than the alias. pnpm version <bump> with --dry-run no longer edits package.json files. It now only reports the bumps it would make, and skips the working tree check, the version lifecycle scripts, the commit, and the tag pnpm/pnpm#13953 . On Windows, pnpm store path now returns a conventional drive path without the \\?\ verbatim prefix when the project and pnpm home are on different drives #13987 . Platinum Sponsors Gold Sponsors

Read more →

pnpm 11.23

Minor Changes pnpm config get and pnpm config list now show the settings pnpm acts on under their documented names: registries shows the registries pnpm resolves from, merged across every source ( .npmrc , pnpm-workspace.yaml , the global config, CLI flags), in the shape the setting is written in: keyed by registry URL, with the default registry declared as the bare @ scope. Built-in routes are included — the @jsr scope and the npmjs and gh prefixes — unless pointed elsewhere. Previously pnpm config get registries printed undefined . update and audit show the effective sections, whichever spelling set them. The deprecated internal spellings ( updateConfig , auditConfig , auditLevel ) are no longer listed. catalogs shows the complete resolved catalog set — the singular catalog block is its default entry — whichever spelling declared it. The registry and @scope:registry entries show the merged routes rather than raw .npmrc values, so they always agree with the registries view. Settings that no supported pnpm version recognizes get their own warning. A key in the global config file that this version of pnpm does not read is no longer reported with advice to move it to a project-level pnpm-workspace.yaml (where it would be ignored too); the warning now says the setting is not recognized by this version of pnpm, names the pnpm version that does read it when there is one (for example, globalShims is a pnpm v12 setting), and suggests the closest real setting name when the key looks like a typo. Unrecognized and non-camelCase keys in a project's pnpm-workspace.yaml , previously ignored silently, are now reported the same way. pnpm config get <key> and pnpm get <key> no longer print config-load warnings, so a script capturing the value gets the value alone. The importPackage pnpmfile hook is deprecated. pnpm now prints a warning when a pnpmfile defines it, and the hook will be removed in the next major version. It also opts the installation out of the parallel package importer, making installation slower. If you rely on this hook, comment on #14101 . node_modules/.modules.yaml no longer records the registries an install resolved from, and the recorded copy is dropped from the file on the first install that rewrites it. It dated from the lockfile format that spelled a dependency's path relative to its registry, where reading an installed tree meant knowing the registries it was installed with. Dependency paths have not carried a registry for several major versions, and the recorded copy outlived its use: pnpm list , pnpm why , and single-project installs preferred it over the project's own configuration, so a project whose registry had changed since its last install was still read through the old one. They now use the configured registries, like every other command already did. When enableGlobalVirtualStore is on, every process pnpm spawns for the project ( pnpm run , pnpm exec , lifecycle scripts) now receives a NODE_PATH pointing at the project's hoisted node_modules , plus a NODE_OPTIONS --import flag that registers a resolve hook restoring NODE_PATH lookups for ESM imports. Dependencies that import undeclared ("phantom") packages keep resolving under the global virtual store — for both CommonJS and ESM — without installing the @pnpm/plugin-esm-node-path config dependency pnpm/pnpm#9618 . Tools run by pnpm dlx resolve such dependencies too: the JS CLI passes them the same environment, while the Rust CLI's dlx cache is self-contained, so its layout already exposes them. A registry can now declare that its abbreviated metadata carries the time field, so resolutionMode: time-based reads the full metadata document only from the registries that need it: resolutionMode : time-based registries : https://npm.internal.example/ : supportsTimeField : true registry.npmjs.org omits time from abbreviated metadata, so a time-based resolution has to fall back to the much larger full document. That fallback used to be all-or-nothing: registrySupportsTimeField answered for every registry at once, so a project resolving from both the public registry and a Verdaccio instance either paid for full metadata everywhere or claimed a time field npmjs does not serve. The answer is now per registry, and registrySupportsTimeField remains the answer for every registry that does not declare one. The declaration is also sent to a pnpr server, which applies it to the resolution it runs on the client's behalf. A pnpr resolve request now carries the client's registries the way the registries setting declares them — keyed by URL, with the scopes routed to each, the bare-specifier prefix each answers to, and each one's serverType — in place of the prefix map it used to send. The server routes them through the same inversion the config reader runs, so a pnpr-served install resolves a scoped dependency from the registry that scope is routed to, which it previously could not: only the default registry and the prefix-addressed ones reached the server. A declared serverType reaches it too, so the tarball URLs pnpr omits from the lockfile match the ones the client reconstructs. Built-in scope routes the project has not pointed elsewhere are not declared, so a pnpr server's allowlist is not asked about npm.jsr.io on requests that resolve no JSR package. A registry a request only declares is no longer refused up front for being off the server's allowlist — a client describes its whole configuration, including scopes a given resolve never reaches, so a stray @scope:registry in a developer's ~/.npmrc no longer fails every install against a pnpr server that does not serve it. The boundary moves to the fetch itself: an origin the resolve does reach is refused before the request leaves the server, with the same message. This changes the resolve and verify-lockfile request bodies. A pnpr server and its clients have to be on matching versions; the protocol is still experimental and unversioned. The registries setting now declares a registry once, keyed by its URL, with everything about that registry in the entry: how it lays out tarball URLs, the scopes routed to it, and the bare-specifier prefix it answers to. registries : https://artifactory.example.com/artifactory/api/npm/npm-virtual/ : serverType : artifactory scopes : ['@acme', '@acme-internal'] prefix : work serverType tells pnpm how the registry lays out its tarball URLs, which decides whether a URL can be omitted from pnpm-lock.yaml : undeclared (the default) — strict. Only the exact canonical URL is treated as reconstructible. npm — the registry behaves like registry.npmjs.org , which also serves a scoped package from its percent-encoded path. Declare this for a faithful mirror or caching proxy of the public registry so its tarball URLs can be omitted too. artifactory — JFrog Artifactory repeats the scope in a scoped package's tarball filename ( @acme/widget/-/@acme/widget-1.0.0.tgz ) where the npm registry strips it ( @acme/widget/-/widget-1.0.0.tgz ). Declaring it lets pnpm rebuild that URL, so it is omitted from pnpm-lock.yaml instead of being written out for every scoped package pnpm/get-npm-tarball-url#16 . scopes lists the @ -prefixed scopes that resolve from this registry. A bare '@' is the scope-less default registry, the one the registry setting names. prefix is the alias a dependency addresses this registry by, as in "foo": "work:^1.0.0" . The layout is never inferred from the registry URL, so nothing changes unless you declare it; registry.npmjs.org continues to behave as npm without being declared. Because the lockfile depends on serverType , it is read from pnpm-workspace.yaml only — a serverType in the global config.yaml is ignored, so one developer's machine cannot shape a lockfile their collaborators read back with a different layout. Credentials are rejected in this setting, in a key as well as in a field, and still belong in .npmrc . An entry that routes nothing to itself and matches no configured registry is reported as a warning rather than silently ignored. Migrating The older registries shape, a map of <scope>: <url> strings, still works and needs no change: registries : ' @acme ' : https://npm.acme.example/ namedRegistries is deprecated in favor of the prefix field, and is still read for prefixes registries does not declare. toLockfileResolution and isCanonicalRegistryTarballUrl now take their registry and layout as an options object rather than positional arguments, so @pnpm/lockfile.utils and @pnpm/resolving.tarball-url get a major bump. An install that had to re-hash store files to verify them now reports it. If that cost more than a second, it says how long — The integrity of N files was checked in 2.5s. — and if it was quick but covered more than a thousand files, it names the cause instead: their timestamps changed since the store recorded them, which a backup tool, an antivirus scan or a copied store can do. Added virtualStoreType , which names where the virtual store lives — one store per machine, or one per project: virtualStoreType : global # or: project It is the canonical spelling of enableGlobalVirtualStore , which keeps working. When a project sets both, virtualStoreType wins. It can also be set through PNPM_CONFIG_VIRTUAL_STORE_TYPE and read back with pnpm config get virtualStoreType . The default is unchanged — project , so the shared store stays opt-in. The setting is independent of nodeLinker . isolated and pnp both work with either store type, and hoisted writes no virtual store at all, so it is unaffected. Patch Changes pnpm add --allow-build now adds to the allowBuilds entries already in pnpm-workspace.yaml instead of replacing them #13872 . Kept pending build approvals available after removing an unrelated dependency. pnpm approve-builds now removes onlyBuiltDependencies , onlyBuiltDependenciesFile , neverBuiltDependencies , and ignoredBuiltDependencies from pnpm-workspace.yaml when it writes allowBuilds . Those settings were replaced by allowBuilds in pnpm 11 and silently ignored since, so a workspace migrated from pnpm 10 kept them around looking active. pnpm audit no longer reports a patched version that was never published or is deprecated. The inferred patched range (e.g. >=4.17.24 from <=4.17.23 ) is now checked against the registry packument, and the report is corrected to the lowest non-deprecated published version that satisfies it (e.g. >=4.18.1 when 4.17.24 does not exist and 4.18.0 is deprecated). When no published version satisfies the range, the report shows Patched versions: None . This also prevents pnpm audit --fix from adding overrides or minimumReleaseAgeExclude entries for patches that do not exist #13824 . pnpm audit --fix and pnpm audit --fix update no longer add a minimumReleaseAgeExclude entry when the registry packument shows that the minimum patched version was never published. Previously such entries were written for versions that do not exist, which would have let a later publish of that version bypass the minimumReleaseAge gate #11563 . The --json output of pnpm audit now returns patched_versions: null for advisories whose inferred patch is not available (never published, skipped, yanked, or deprecated), making it easier for tooling to distinguish "no fix available" from "fix available at version X". Fixed pnpm patch-commit in project and edit paths containing non-ASCII characters. The package and bump pickers of pnpm change now size their page from the terminal height instead of always showing 7 rows. They fall back to 7 rows when the terminal height is unknown pnpm/pnpm#13815 . Canceling a pnpm change prompt with Ctrl-c no longer prints a stack trace. It reports Change canceled and exits with a success status, like the other interactive commands #13814 . Re-fetch full registry metadata when minimumReleaseAge is enabled and an abbreviated packument's time map omits timestamps for some versions. This prevents mature versions from being filtered out and resolution from falling back to the lowest matching version pnpm/pnpm#13741 . A config dependency carrying an inline integrity (the <version>+<integrity> form, or the object form without a tarball ) now takes its tarball URL from the registry's packument instead of deriving it from the registry URL, so migrating one costs an extra metadata request. On a registry that serves tarballs from a path pnpm cannot derive, GitLab's group endpoint for one, installing such a config dependency failed with a 404 while the same package installed fine as a regular dependency #13765 . Fixed PNPM_CONFIG_NODE_VERSION being ignored when setting the Node.js version used for compatibility checks. A custom fetcher can no longer replace the archive integrity that pnpm-lock.yaml pins: the locked value is restored after a canFetch or fetch hook rewrites the resolution, and delegating a locked archive to a directory or git source now fails instead of installing unverified content. The Rust CLI now also loads the pnpmfiles named by the pnpmfile setting (a single path or an ordered list), and hands custom fetchers native localTarball and remoteTarball callbacks — including on a fresh install that has to compute a missing tarball integrity, which is then reused by later offline installs. File maps a fetcher returns are accepted only when they match what those native callbacks extracted. Fixed an issue where running pnpm dedupe --check in projects with nodeLinker: hoisted would cause dependencies to be moved out of node_modules into node_modules/.ignored . pnpm deploy --prod and pnpm deploy --no-optional no longer list the excluded dependency groups in the deployed package.json and pnpm-lock.yaml . The deployed lockfile referenced packages that the deploy left out of its graph, so installing in the deploy directory afterwards created dangling symlinks #13623 . Don't treat files like license16.json as a package license when deciding if the workspace LICENSE file should be included in the packed package. pnpm exec --recursive --no-reporter-hide-prefix no longer prints a blank prefixed line after each chunk of a command's output, and no longer splits a line in two when it straddles a chunk boundary. Fixed 404 errors when installing from a registry that serves scoped packages only from a percent-encoded path, such as GitHub Enterprise Server. Outside registry.npmjs.org , a tarball URL that encodes the scope separator as %2f or %2F is no longer mistaken for one that pnpm can rebuild from the package name, version, and registry, so it is kept in pnpm-lock.yaml and requested verbatim on the next install #13534 . Fixed trustPolicyExclude and minimumReleaseAgeExclude being ignored when set to a single string instead of a list. The value was read one character at a time, so the exclusion never matched the package it named — and a * anywhere in it matched every package, silently switching the policy off. pnpm init now pins the exact pnpm version instead of a ^ range, and records it in the packageManager field alongside devEngines.packageManager . Corepack reads only packageManager and accepts nothing but an exact version, so it rejected the generated package.json with "expected a semver version" pnpm/pnpm#13969 . A package created inside an existing workspace is still left unpinned — it follows the pin at the workspace root — and --no-init-package-manager still scaffolds a manifest without any pin. In pnpm 12, pnpm init also honors initType and its --init-type flag, so the manifest it writes is the same one pnpm 11 writes. Fixed an issue where package overrides were written into the metadata cache, causing removed overrides to keep applying on subsequent installs pnpm/pnpm#13918 . On Windows, upgrading pnpm no longer leaves a stale pnpm.ps1 behind. PowerShell resolves pnpm.ps1 ahead of pnpm.cmd , so a shim written by an older installation kept running the previous version. Linking the pnpm CLI's bins now deletes it #13919 . Fixed an inconsistency where minimumReleaseAgeExclude (and trustPolicyExclude ) wildcard/bare-name rules behaved differently in the evaluator and normalizer. A bare rule now consistently evaluates as matching every version, preventing unexpected behavior and silent widening of version policy exemptions when pnpm rewrites the workspace manifest pnpm/pnpm#13725 . A frozen install no longer rewrites the packageManagerDependencies block of pnpm-lock.yaml . When the pnpm version pinned by devEngines.packageManager (or by packageManager ) is missing from the lockfile or no longer matches it, --frozen-lockfile now fails with ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE instead of resolving the version and saving it, so a manifest whose pin was bumped without regenerating the lockfile can no longer pass CI #14009 . A git dependency installed over HTTPS from a hosted repository now keeps its branch, tag, or version range in the specifier recorded in package.json . It was written back without one, so the next pnpm update moved the dependency to the repository's default branch #13999 . Fixed pnpm update --global --latest failing with a 404 error when a globally installed package was not added from the registry by name. Packages installed from a local path ( link: / file: ), a git repository, a tarball URL, an npm: alias, or a named registry now keep their spec during a global update instead of being looked up by name in the default registry. See #12854 . Fix recursive pnpm update <name>@<version> so an exact pinned update stays scoped to the requested version line: copies of the same package on another major line — or, for a 0.x request, another minor line — keep their locked resolution instead of being re-resolved along with the target. Under nodeLinker: hoisted , a dependency declared against a peer-resolution variant of a package version is no longer dropped from the installed layout. All variants of a version share one hoisted copy, and edges pointing at any of them now resolve to it, so the depending project keeps the package in its .package-map.json and the depending package keeps it in its node_modules/.bin . Fixed pnpm install --merge-git-branch-lockfiles deleting the per-branch lockfiles when the lockfile setting is false . Such an install never reads them, so it has nothing to merge them into and now leaves them alone. Fixed pnpm install sometimes not exiting after printing Done in Xs #12297 . Fixed pnpm failing to read .modules.yaml files containing long dependency paths #13875 . The manifest is now parsed as JSON (the format pnpm writes it in), falling back to the YAML parser only for manifests written by old pnpm versions. With preferSymlinkedExecutables , NODE_PATH again points at the virtual store of the workspace root when pnpm is run from inside a workspace package, so scripts can resolve dependencies that live only in the hoisted store #13912 . Reduced registry metadata requests during dependency resolution by reusing cached metadata when lockfile preferences prove that no uncached version can win pnpm/pnpm#13976 . pnpm pkg get and pnpm pkg set now accept hyphens inside a dot-notation property path, so pnpm pkg get dependencies.some-package-name reads the key instead of failing with ERR_PNPM_UNEXPECTED_TOKEN_IN_PROPERTY_PATH . The bracketed and quoted forms already worked and are unchanged. A resolve request now carries the client's resolutionMode , so an install delegated to a pnpr server picks versions the way the client would. time-based and lowest-direct reached the server as nothing at all, leaving it on its highest default: the returned lockfile pinned the highest satisfying version of every dependency, and the setting appeared to be ignored. This adds a field to the resolve request body. A server older than its client ignores it and keeps resolving highest ; the protocol is still experimental and unversioned. Fixed pnpm installs using pnpr to honor the client's autoInstallPeers , dedupePeers , and excludeLinksFromLockfile settings pnpm/pnpm#13389 . pnpm remove now prunes undecided entries ( "set this to true or false" ) from allowBuilds in pnpm-workspace.yaml when sharedWorkspaceLockfile: true and the corresponding packages are removed pnpm/pnpm#13892 . Fixed workspace discovery for pnpm-workspace.yaml files without a packages field so commands only consider the workspace root instead of recursively scanning nested projects #14047 . A runtime installed through devEngines.runtime now matches the host when supportedArchitectures lists several platforms. Listing os: [darwin, linux] and cpu: [x64, arm64] used to install the runtime built for the first entry of each list, so a machine running Linux on arm64 got a macOS x64 Node.js that could not execute #13898 . pnpm sbom now fails with ERR_PNPM_SBOM_MISSING_IMPORTERS when pnpm-lock.yaml has no entry for a selected project, instead of writing an SBOM that under-reports that project's dependencies. Previously this crashed with Cannot read properties of undefined (reading 'devDependencies') . pnpm self-update now rewrites a simple devEngines.packageManager.version range ( ^ / ~ ) to the newly installed version, keeping the operator — matching how pnpm update and pnpm runtime set rewrite ranges. Complex ranges such as >=8.0.0 that the new version satisfies are still left unchanged #13935 . pnpm self-update <tag> no longer downgrades when the dist-tag points at the pnpm version already running and that version is younger than minimumReleaseAge . The maturity cutoff moved the tag back to the previous mature release, so pnpm self-update next-12 on v12.0.0-rc.4 switched to v12.0.0-rc.3. pnpm set-script now updates package.json instead of failing with ERR_PNPM_NOT_IMPLEMENTED pnpm/pnpm#13956 . pnpm update now preserves the existing range operator when updating a prerelease dependency. See #7002 . Installs are faster in workspaces that declare inter-workspace dependencies with plain ranges ( "*" , "^1.2.3" ) rather than the workspace: protocol. With preferWorkspacePackages enabled, linking such a dependency no longer makes a registry request that cannot change the outcome — and workspace packages that were never published no longer cost a 404 on every install. Added fetchWarnTimeoutMs and fetchMinSpeedKiBps to the Rust pnpm CLI and its N-API bindings. Slow registry metadata requests and tarball downloads now emit pnpm-compatible warnings without exposing URL credentials, query parameters, fragments, or control characters pnpm/pnpm#12042 . An override change is now absorbed by the fast lockfile update even when another, unchanged override uses the catalog: protocol. Previously any catalog: -valued override forced a full re-resolution whenever the override list changed, which could move unrelated packages in the lockfile (for example after pnpm audit --fix added an override). Packed workspace package manifests now preserve dependency order, making repeated pnpm pack output deterministic #10167 . pnpm update <name>@<version> now fails with ERR_PNPM_UPDATE_VERSION_ON_INDIRECT_DEP when the package is not a direct dependency of any selected project, instead of quietly updating it to whatever a fresh install would resolve. There is nowhere to record the version in that case, so the request cannot be honored, and the error points at the overrides entry that does pin a transitive dependency. Ranges and tags are unaffected, and a package that any selected project declares directly still takes its version as before. trustPolicy: no-downgrade no longer aborts the install with ERR_PNPM_MISSING_TIME on registries that serve no per-version time field when minimumReleaseAgeIgnoreMissingTime is set. The trust check reads the same publish dates the minimumReleaseAge check does, so it now honors the same opt-in and skips the affected package with a warning #12446 . minimumReleaseAgeIgnoreMissingTime no longer lets a lockfile entry the registry does not list pass the minimumReleaseAge check during lockfile verification. The opt-in covers a registry that cannot date its releases; a packument that does date every version it lists is saying it never published this one, which stays a hard failure. The missing- time warning now names the check it is reporting on, so a package whose minimumReleaseAge and trustPolicy checks are both skipped warns about both instead of only the first. pnpm update <pkg>@<version> now updates only the selected packages and leaves unrelated dependencies unchanged. A selector that renames the package it installs — pnpm update <alias>@npm:<pkg>@<version> or the jsr: equivalent — now targets the package the alias installs rather than the alias. Fixed verifyDepsBeforeRun being ignored when set to install , warn , error , or prompt through the PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN environment variable or the --config.verify-deps-before-run flag #13816 . Only the boolean values were accepted before, so a string value was silently dropped. pnpm version <bump> with --dry-run no longer edits package.json files. It now only reports the bumps it would make, and skips the working tree check, the version lifecycle scripts, the commit, and the tag pnpm/pnpm#13953 . Platinum Sponsors Gold Sponsors

Read more →

pnpm 12 RC 8

Minor Changes packageImportMethod: auto now tries hardlinks before cloning on Linux. 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 this roughly halves the time an install spends materializing node_modules from a warm store. ext4 installs are unchanged (cloning was never supported there, so auto already hardlinked), and macOS keeps clone-first, where APFS clonefile is the platform's cheap primitive. Cloning remains the fallback when the store refuses hardlinks, and remains available explicitly via packageImportMethod: clone . This ships with pnpm 12 only: pnpm 11's importer deliberately keeps clone-first, since changing what the default materializes on disk is not a point-release change. Patch Changes pnpm approve-builds now removes onlyBuiltDependencies , onlyBuiltDependenciesFile , neverBuiltDependencies , and ignoredBuiltDependencies from pnpm-workspace.yaml when it writes allowBuilds . Those settings were replaced by allowBuilds in pnpm 11 and silently ignored since, so a workspace migrated from pnpm 10 kept them around looking active. pnpm audit no longer reports a patched version that was never published or is deprecated. The inferred patched range (e.g. >=4.17.24 from <=4.17.23 ) is now checked against the registry packument, and the report is corrected to the lowest non-deprecated published version that satisfies it (e.g. >=4.18.1 when 4.17.24 does not exist and 4.18.0 is deprecated). When no published version satisfies the range, the report shows Patched versions: None . This also prevents pnpm audit --fix from adding overrides or minimumReleaseAgeExclude entries for patches that do not exist #13824 . pnpm audit --fix and pnpm audit --fix update no longer add a minimumReleaseAgeExclude entry when the registry packument shows that the minimum patched version was never published. Previously such entries were written for versions that do not exist, which would have let a later publish of that version bypass the minimumReleaseAge gate #11563 . The --json output of pnpm audit now returns patched_versions: null for advisories whose inferred patch is not available (never published, skipped, yanked, or deprecated), making it easier for tooling to distinguish "no fix available" from "fix available at version X". Re-fetch full registry metadata when minimumReleaseAge is enabled and an abbreviated packument's time map omits timestamps for some versions. This prevents mature versions from being filtered out and resolution from falling back to the lowest matching version pnpm/pnpm#13741 . A config dependency carrying an inline integrity (the <version>+<integrity> form, or the object form without a tarball ) now takes its tarball URL from the registry's packument instead of deriving it from the registry URL, so migrating one costs an extra metadata request. On a registry that serves tarballs from a path pnpm cannot derive, GitLab's group endpoint for one, installing such a config dependency failed with a 404 while the same package installed fine as a regular dependency #13765 . Don't treat files like license16.json as a package license when deciding if the workspace LICENSE file should be included in the packed package. Reduced warm update overhead by limiting virtual-store bin linking and ignored-script build bookkeeping to packages materialized by the current install. pnpm init now pins the exact pnpm version instead of a ^ range, and records it in the packageManager field alongside devEngines.packageManager . Corepack reads only packageManager and accepts nothing but an exact version, so it rejected the generated package.json with "expected a semver version" pnpm/pnpm#13969 . A package created inside an existing workspace is still left unpinned — it follows the pin at the workspace root — and --no-init-package-manager still scaffolds a manifest without any pin. In pnpm 12, pnpm init also honors initType and its --init-type flag, so the manifest it writes is the same one pnpm 11 writes. node-linker=hoisted installs no longer produce broken layouts on graphs with version conflicts. Three hoister fixes, aligning with @yarnpkg/nm (which the TypeScript CLI delegates to): A version-conflicted package depended on by several packages kept its conflicting transitive dependencies under only one of the dependents, so requiring them through any other dependent resolved the wrong (root-hoisted) version — for example an ESM parse-entities@4 resolving character-entities-legacy v1 instead of v3, which crashes with ERR_IMPORT_ATTRIBUTE_MISSING on Node.js 22. Hoist decisions are now made per parent path on decoupled copies (ports upstream's decoupleGraphNode ). Peer-resolution variants of one package version now collapse onto a single copy (ports pnpm v11's depPathByPkgId mapping) instead of conflict-nesting a copy under every dependent — on peer-variant-heavy graphs (such as bit 's) the old behavior also made the per-path walk explode. Hoisting no longer shadows names a subtree resolves through an ancestor directory: a candidate is refused when a nearer ancestor holds a different version of its name (upstream's "filled by parent" scan) or when the hoist root's subtree already resolves that name from above (upstream's usedDependencies gate). pnpm update --no-save <pkg>@<version> now keeps the manifest's declared importer specifier in pnpm-lock.yaml when the requested version satisfies that range, so a subsequent --frozen-lockfile install no longer fails because the lockfile records the requested version as the specifier. Reduced registry metadata requests during dependency resolution by reusing cached metadata when lockfile preferences prove that no uncached version can win pnpm/pnpm#13976 . Improved install performance: the store-index writer's shutdown now overlaps the install's final lockfile and .modules.yaml writes instead of extending the install's tail. A setting in the global config.yaml that pnpm does not read from that file, or that is written in kebab-case instead of camelCase, is now reported instead of being ignored silently. A forced full re-resolution (config changes the fast lockfile update cannot absorb, such as a changed override or packageExtensions ) no longer moves dependencies whose recorded versions still satisfy their ranges. The prior lockfile now pins each still-satisfied edge even when its recorded subtree cannot be reused wholesale, so open ranges like @types/node: "*" keep their locked versions instead of collapsing onto the highest locked version and churning the lockfile. Improved fresh resolution performance when package metadata is already cached. Improved fresh installs by reusing the store index and verified-files cache during dependency materialization. A runtime installed through devEngines.runtime now matches the host when supportedArchitectures lists several platforms. Listing os: [darwin, linux] and cpu: [x64, arm64] used to install the runtime built for the first entry of each list, so a machine running Linux on arm64 got a macOS x64 Node.js that could not execute #13898 . pnpm self-update <tag> no longer downgrades when the dist-tag points at the pnpm version already running and that version is younger than minimumReleaseAge . The maturity cutoff moved the tag back to the previous mature release, so pnpm self-update next-12 on v12.0.0-rc.4 switched to v12.0.0-rc.3. Improved install performance: large tarballs are now verified and extracted while they download, so the biggest packages — whose downloads finish last — no longer add their whole extraction to the end of the install.

Read more →

pnpr 0.1.0-alpha.7

Minor Changes Packument responses now carry a Last-Modified header derived from the document's time.modified , so a client's release-age check can learn the package-level last-publish bound from a cheap HEAD probe instead of downloading the metadata body. A pnpr resolve request now carries the client's registries the way the registries setting declares them — keyed by URL, with the scopes routed to each, the bare-specifier prefix each answers to, and each one's serverType — in place of the prefix map it used to send. The server routes them through the same inversion the config reader runs, so a pnpr-served install resolves a scoped dependency from the registry that scope is routed to, which it previously could not: only the default registry and the prefix-addressed ones reached the server. A declared serverType reaches it too, so the tarball URLs pnpr omits from the lockfile match the ones the client reconstructs. Built-in scope routes the project has not pointed elsewhere are not declared, so a pnpr server's allowlist is not asked about npm.jsr.io on requests that resolve no JSR package. A registry a request only declares is no longer refused up front for being off the server's allowlist — a client describes its whole configuration, including scopes a given resolve never reaches, so a stray @scope:registry in a developer's ~/.npmrc no longer fails every install against a pnpr server that does not serve it. The boundary moves to the fetch itself: an origin the resolve does reach is refused before the request leaves the server, with the same message. This changes the resolve and verify-lockfile request bodies. A pnpr server and its clients have to be on matching versions; the protocol is still experimental and unversioned. A resolve request now carries the client's resolutionMode , so an install delegated to a pnpr server picks versions the way the client would. time-based and lowest-direct reached the server as nothing at all, leaving it on its highest default: the returned lockfile pinned the highest satisfying version of every dependency, and the setting appeared to be ignored. This adds a field to the resolve request body. A server older than its client ignores it and keeps resolving highest ; the protocol is still experimental and unversioned. Patch Changes Fixed pnpm installs using pnpr to honor the client's autoInstallPeers , dedupePeers , and excludeLinksFromLockfile settings pnpm/pnpm#13389 .

Read more →

pnpm 12 RC 7

Minor Changes node_modules/.modules.yaml no longer records the registries an install resolved from, and the recorded copy is dropped from the file on the first install that rewrites it. It dated from the lockfile format that spelled a dependency's path relative to its registry, where reading an installed tree meant knowing the registries it was installed with. Dependency paths have not carried a registry for several major versions, and the recorded copy outlived its use: pnpm list , pnpm why , and single-project installs preferred it over the project's own configuration, so a project whose registry had changed since its last install was still read through the old one. They now use the configured registries, like every other command already did. When enableGlobalVirtualStore is on, every process pnpm spawns for the project ( pnpm run , pnpm exec , lifecycle scripts) now receives a NODE_PATH pointing at the project's hoisted node_modules , plus a NODE_OPTIONS --import flag that registers a resolve hook restoring NODE_PATH lookups for ESM imports. Dependencies that import undeclared ("phantom") packages keep resolving under the global virtual store — for both CommonJS and ESM — without installing the @pnpm/plugin-esm-node-path config dependency pnpm/pnpm#9618 . Tools run by pnpm dlx resolve such dependencies too: the JS CLI passes them the same environment, while the Rust CLI's dlx cache is self-contained, so its layout already exposes them. A registry can now declare that its abbreviated metadata carries the time field, so resolutionMode: time-based reads the full metadata document only from the registries that need it: resolutionMode : time-based registries : https://npm.internal.example/ : supportsTimeField : true registry.npmjs.org omits time from abbreviated metadata, so a time-based resolution has to fall back to the much larger full document. That fallback used to be all-or-nothing: registrySupportsTimeField answered for every registry at once, so a project resolving from both the public registry and a Verdaccio instance either paid for full metadata everywhere or claimed a time field npmjs does not serve. The answer is now per registry, and registrySupportsTimeField remains the answer for every registry that does not declare one. The declaration is also sent to a pnpr server, which applies it to the resolution it runs on the client's behalf. A pnpr resolve request now carries the client's registries the way the registries setting declares them — keyed by URL, with the scopes routed to each, the bare-specifier prefix each answers to, and each one's serverType — in place of the prefix map it used to send. The server routes them through the same inversion the config reader runs, so a pnpr-served install resolves a scoped dependency from the registry that scope is routed to, which it previously could not: only the default registry and the prefix-addressed ones reached the server. A declared serverType reaches it too, so the tarball URLs pnpr omits from the lockfile match the ones the client reconstructs. Built-in scope routes the project has not pointed elsewhere are not declared, so a pnpr server's allowlist is not asked about npm.jsr.io on requests that resolve no JSR package. A registry a request only declares is no longer refused up front for being off the server's allowlist — a client describes its whole configuration, including scopes a given resolve never reaches, so a stray @scope:registry in a developer's ~/.npmrc no longer fails every install against a pnpr server that does not serve it. The boundary moves to the fetch itself: an origin the resolve does reach is refused before the request leaves the server, with the same message. This changes the resolve and verify-lockfile request bodies. A pnpr server and its clients have to be on matching versions; the protocol is still experimental and unversioned. A resolve request now carries the client's resolutionMode , so an install delegated to a pnpr server picks versions the way the client would. time-based and lowest-direct reached the server as nothing at all, leaving it on its highest default: the returned lockfile pinned the highest satisfying version of every dependency, and the setting appeared to be ignored. This adds a field to the resolve request body. A server older than its client ignores it and keeps resolving highest ; the protocol is still experimental and unversioned. The registries setting now declares a registry once, keyed by its URL, with everything about that registry in the entry: how it lays out tarball URLs, the scopes routed to it, and the bare-specifier prefix it answers to. registries : https://artifactory.example.com/artifactory/api/npm/npm-virtual/ : serverType : artifactory scopes : ['@acme', '@acme-internal'] prefix : work serverType tells pnpm how the registry lays out its tarball URLs, which decides whether a URL can be omitted from pnpm-lock.yaml : undeclared (the default) — strict. Only the exact canonical URL is treated as reconstructible. npm — the registry behaves like registry.npmjs.org , which also serves a scoped package from its percent-encoded path. Declare this for a faithful mirror or caching proxy of the public registry so its tarball URLs can be omitted too. artifactory — JFrog Artifactory repeats the scope in a scoped package's tarball filename ( @acme/widget/-/@acme/widget-1.0.0.tgz ) where the npm registry strips it ( @acme/widget/-/widget-1.0.0.tgz ). Declaring it lets pnpm rebuild that URL, so it is omitted from pnpm-lock.yaml instead of being written out for every scoped package pnpm/get-npm-tarball-url#16 . scopes lists the @ -prefixed scopes that resolve from this registry. A bare '@' is the scope-less default registry, the one the registry setting names. prefix is the alias a dependency addresses this registry by, as in "foo": "work:^1.0.0" . The layout is never inferred from the registry URL, so nothing changes unless you declare it; registry.npmjs.org continues to behave as npm without being declared. Because the lockfile depends on serverType , it is read from pnpm-workspace.yaml only — a serverType in the global config.yaml is ignored, so one developer's machine cannot shape a lockfile their collaborators read back with a different layout. Credentials are rejected in this setting, in a key as well as in a field, and still belong in .npmrc . An entry that routes nothing to itself and matches no configured registry is reported as a warning rather than silently ignored. Migrating The older registries shape, a map of <scope>: <url> strings, still works and needs no change: registries : ' @acme ' : https://npm.acme.example/ namedRegistries is deprecated in favor of the prefix field, and is still read for prefixes registries does not declare. toLockfileResolution and isCanonicalRegistryTarballUrl now take their registry and layout as an options object rather than positional arguments, so @pnpm/lockfile.utils and @pnpm/resolving.tarball-url get a major bump. Added virtualStoreType , which names where the virtual store lives — one store per machine, or one per project: virtualStoreType : global # or: project It is the canonical spelling of enableGlobalVirtualStore , which keeps working. When a project sets both, virtualStoreType wins. It can also be set through PNPM_CONFIG_VIRTUAL_STORE_TYPE and read back with pnpm config get virtualStoreType . The default is unchanged — project , so the shared store stays opt-in. The setting is independent of nodeLinker . isolated and pnp both work with either store type, and hoisted writes no virtual store at all, so it is unaffected. Patch Changes Fixed pnpm patch-commit in project and edit paths containing non-ASCII characters. Fixed 404 errors when installing from a registry that serves scoped packages only from a percent-encoded path, such as GitHub Enterprise Server. Outside registry.npmjs.org , a tarball URL that encodes the scope separator as %2f or %2F is no longer mistaken for one that pnpm can rebuild from the package name, version, and registry, so it is kept in pnpm-lock.yaml and requested verbatim on the next install #13534 . Fixed an inconsistency where minimumReleaseAgeExclude (and trustPolicyExclude ) wildcard/bare-name rules behaved differently in the evaluator and normalizer. A bare rule now consistently evaluates as matching every version, preventing unexpected behavior and silent widening of version policy exemptions when pnpm rewrites the workspace manifest pnpm/pnpm#13725 . Fixed pnpm update --global --latest failing with a 404 error when a globally installed package was not added from the registry by name. Packages installed from a local path ( link: / file: ), a git repository, a tarball URL, an npm: alias, or a named registry now keep their spec during a global update instead of being looked up by name in the default registry. See #12854 . pnpm outdated and pnpm update --interactive now dereference catalog: specifiers before querying the registry. A catalog entry that is an npm alias ( '@types/zkochan__table': npm:@types/table@6.3.2 ) no longer fails with ERR_PNPM_OUTDATED_REGISTRY_ERROR for the alias key, and pnpm outdated --compatible compares against the range the catalog holds instead of skipping the dependency. A failed packument request now reports the status the registry returned ( 404 Not Found ) instead of "error decoding response body". Installs with a cold cache are significantly faster: lockfile verification no longer delays resolution or downloads and re-checks far less data over the network, and downloaded packages are linked while the remaining downloads are still in flight. Fixed pnpm installs using pnpr to honor the client's autoInstallPeers , dedupePeers , and excludeLinksFromLockfile settings pnpm/pnpm#13389 . The three registry lookups are now named for what they are keyed by, so that none of them is called registries — a name the registries setting itself has taken: before after Config.registries Config.registriesByScope Config.namedRegistries Config.registriesByPrefix Config.registryOptions Config.registryOptionsByUrl The same rename applies to the RegistryContext fields, the Registries and NamedRegistries types (now RegistriesByScope and RegistriesByPrefix ), normalizeRegistries / normalizeNamedRegistries (now normalizeRegistriesByScope / normalizeRegistriesByPrefix ), and the BUILTIN_NAMED_REGISTRIES constant (now BUILTIN_REGISTRIES_BY_PREFIX ). This is an internal rename: no setting, error code, lockfile field, or .pnpmfile.cjs hook field changes. A preResolution hook still reads ctx.registries , which is the name pacquet passes as well. The registries and namedRegistries settings are read under the names users write them. The pnpr resolve request sends registriesByPrefix where it sent namedRegistries . A pnpr server and its clients must be on matching versions, which is already the case for an experimental server. An install that had to re-hash store files to verify them now reports it. If that cost more than a second, it says how long — The integrity of N files was checked in 2.5s. — and if it was quick but covered more than a thousand files, it names the cause instead: their timestamps changed since the store recorded them, which a backup tool, an antivirus scan or a copied store can do. Installs are faster in workspaces that declare inter-workspace dependencies with plain ranges ( "*" , "^1.2.3" ) rather than the workspace: protocol. With preferWorkspacePackages enabled, linking such a dependency no longer makes a registry request that cannot change the outcome — and workspace packages that were never published no longer cost a 404 on every install. An override change is now absorbed by the fast lockfile update even when another, unchanged override uses the catalog: protocol. Previously any catalog: -valued override forced a full re-resolution whenever the override list changed, which could move unrelated packages in the lockfile (for example after pnpm audit --fix added an override). Reduced peak memory usage when installing large packages. A tarball whose compressed size is at least 16 MiB, or whose registry-reported unpacked size is at least 64 MiB, is now extracted by streaming the decompression directly into the content-addressable store instead of materializing the whole decompressed archive in memory, and its large files are hashed and written to the store incrementally. pnpm why and pnpm list no longer print stray [90m -style codes in their trees when the terminal supports colors. The bolded labels — the searched package in pnpm why , the project header and the matched package in pnpm list — dropped the escape byte of the styles they already carried, leaving the color codes as visible text.

Read more →

Uniform Changelog API

Access pnpm changelog updates through our uniform API. Same JSON structure across all sources — no adapter-specific parsing needed.

API Endpoint
GET https://watchchangelog.com/api/v1/entries?source=pnpm.releases
Response Sample
{
  "source": "pnpm.releases",
  "vendor": "pnpm",
  "id": "tag:github.com,2008:Repository/50565430/v12.0.0",
  "published_at": "2026-08-26T15:13:51.000Z",
  "title": "pnpm 12",
  "url": "https://github.com/pnpm/pnpm/releases/tag/v12.0.0",
  "summary": "Major Changes Git dependencies on known hosts (GitHub, GitLab, Bitbucket) are now treated as identities rather than transport choices. Every representation of the same repository — github:owner/repo , owner/repo , git+https://… , git+ssh://git@… — resolves through the host's canonical HTTPS URL, and the lockfile never records an SSH URL for them. Repositories whose archive endpoint is anonymously reachable resolve to the host's archive (fast tarball download); all others resolve to a git clone of the canonical HTTPS URL, which every machine with access to the repository can fetch. To reach a private hosted repository over SSH, configure the machine (not the project) with git's own URL rewriting, for example: git config --global url. \" git@github.com: \" .insteadOf https://github.com/ pnpm shells out to git , so the rewrite applies to all of pnpm's git operations automatically. URLs of unknown hosts (self-hosted servers) are unaffected and keep their exact URL, including SSH. URLs with embedded credentials are also kept verbatim and never resolve to a host archive. This removes the network probing that previously decided between HTTPS and SSH at resolution time, which could record a transport that only worked on the machine that happened to run the resolution (e.g. an SSH URL that broke CI runners without SSH keys). A project's pnpm-workspace.yaml may no longer carry a setting pnpm does not recognize. Such a setting used to be ignored in silence — a misspelled minimumReleaseAge dropped the policy it was meant to set, and nothing said so. Now it is reported, suggesting the closest real setting name when the key looks like a typo, and 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 be 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 such a setting, so a broken file can still be inspected and repaired, and pnpm config get <key> prints the value with no warnings at all. Keys the global config file cannot set are likewise split between workspace-only settings (still directed to pnpm-workspace.yaml ) and settings unknown to this version. 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, no matter where the installation walks into the cycle from. Previously the cut depended on the walk path, so installing the same dependencies could produce different lockfiles depending on importer order or resolution order #13846 , and a peer-resolution verdict computed for one occurrence of a cyclic package could be wrongly reused at another #13865 . With canonical cycle breaking the lockfile is a pure function of the dependency graph: repeated installs, reordered importers, and reordered dependencies all produce byte-identical lockfiles. Peer dependencies of packages inside a cycle keep nearest-wins resolution along the canonical order, and a dependency edge that closes a cycle references an occurrence of its target resolved at the importer level. On large cycle-heavy workspaces peer resolution is 2–3× faster, uses about 25% less memory, and produces a substantially smaller lockfile (fewer redundant peer variants). Existing lockfiles keep working: headless ( --frozen-lockfile ) installs consume them unchanged, and installs that skip resolution leave them untouched. The first install that actually re-resolves (for example after a dependency change) re-keys walk-order-dependent peer variants of cyclic packages once. packageImportMethod: auto now tries hardlinks before cloning on Linux. 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 this roughly halves the time an install spends materializing node_modules from a warm store. ext4 installs are unchanged (cloning was never supported there, so auto already hardlinked), and macOS keeps clone-first, where APFS clonefile is the platform's cheap primitive. Cloning remains the fallback when the store refuses hardlinks, and remains available explicitly via packageImportMethod: clone . Under engineStrict , an install 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 v11 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 . Minor Changes Globally installed bins can now follow the project you run them in. The new globalShims setting is a record of package names to policies that selects which globally installed packages get project-aware shims; it defaults to { node: true, deno: true, bun: true } and merges key-wise, so globalShims: { bun: false } switches one default off and globalShims: { typescript: true } adds another package. With the default, a project that pins Node.js through devEngines.runtime or engines.runtime gets the pinned stable release — authenticated against the Node.js release-team signatures — downloaded on first use and run whenever you type node inside the project, with no shell hooks. Candidates that are not signature-verified (Deno, Bun, Node.js prereleases, and ordinary package bins you enable) ask \"Do you trust this project?\" once per candidate and remember the answer machine-locally; the record values name the policy per package: \"auto\" (or its shorthand true ) defers to artifact authentication, \"always\" switches without ever asking (useful in CI), and \"prompt\" always asks, even for authenticated candidates. Set globalShims: false to disable the feature, or PNPM_SHIM_BYPASS=1 to bypass it for one invocation. On Windows, programs can keep spawning the global node.exe directly, without a shell. pnpm installs the other package managers now, not just itself: npm, Yarn Classic, Yarn Berry, Yarn 6 ( yarnpkg/zpm ), and Bun. Each is resolved and fetched through the trusted package-manager registries, and an npm-published one is verified against npm's signature for its exact version before it is executed. Three things use it: A git-hosted dependency is prepared with the package manager it asks for. Its packageManager / devEngines.packageManager pin is honored, and a yarn.lock written by Yarn Classic no longer gets installed by Yarn Berry. pnpm provides that package manager when the dependency pinned a version, or when the host cannot satisfy what the dependency needs — so a repository built with Yarn now installs on a machine that has only pnpm, while a host that already has a suitable one keeps using its own. pnpm dlx ( pnx ) runs one of them for a single command: pnx yarn@4 install , pnx npm@11 ci , pnx bun@1.3.0 install . Naming a package manager, or a runtime ( node , deno , bun ), there now provisions the real thing instead of installing the npm package that shares its name — unless the specifier locates a package rather than asking for a released version ( pnx yarn@npm:yarn@1.22.22 , pnx yarn@yarnpkg/berry ), which installs what it names — pnx yarn@4 was previously a missing version, since Yarn 4 is published as @yarnpkg/cli-dist , and pnx node@22 now runs that Node.js release rather than a wrapper that downloads one. --package naming a package manager picks which of its commands to run, so pnx --package npm@11 npx create-something runs that npm's npx . pnpm shim add yarn links a yarn command that runs whatever version the current project pins, and pnpm shim rm / pnpm shim ls manage those shims. It works for any package, not only package managers. Shims are never created as a side effect of pnpm setup or an install — a shim shadows the rest of your PATH , so pnpm only writes one when asked. Installing a package manager globally ( pnpm add -g yarn ) now makes it follow a project's pin too, the way a globally installed Node.js already follows devEngines.runtime : the pinned version runs where a project pins one, and the globally installed copy is the fallback everywhere else. An explicit globalShims entry, including false , is left as you set it. pnpm add follows the same rule about what a name means. pnpm add -g yarn@4 installs Yarn Berry — it used to fail, because npm's yarn package stops at Classic — and pnpm add -g node@22 / pnpm add -g deno@2 install that Node.js or Deno release rather than a wrapper package that downloads one. In a project, naming a package manager records which one the project uses instead of installing it as a dependency, and naming a runtime records it under engines.runtime as node@runtime:22 already did. The declaration goes where the package manager reads it. Yarn is started from a project pin by corepack, which reads only packageManager and only accepts an exact version there, so pnpm add yarn@4 resolves the line and writes \"packageManager\": \"yarn@4.18.0\" — the same thing corepack use yarn@4 writes, down to the +sha512.… integrity for the Yarn Classic line that corepack pins its tarball with. Every other package manager is recorded in devEngines.packageManager , which holds a range. Only one of the two fields is ever left behind: they declare the same thing, and corepack refuses to run a project whose declarations disagree. A JavaScript package manager on a machine without Node.js gets a managed LTS runtime to run on. What changes for a project coming from v11: pnpm add yarn records the project's package manager instead of installing the npm package that shares the name (that package is still reachable as pnpm add yarn@npm:yarn@1.22.22 ), pnpm add -g yarn installs the current Yarn line rather than Classic, pnpm add -g node / pnpm add -g deno and pnx node / pnx deno install a Node.js or Deno release rather than a wrapper package, and a globally installed package manager defers to a project's pin where there is one. Added an opt-in proof of concept that 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. Configure it with the new remoteSideEffectsCache setting. A workspace names the eligible organization and packages ; everything describing the act of signing — publish , keyId , builderId , trustedKeys , privateKey and the provenance fields — is refused in pnpm-workspace.yaml and read from the global config file or the environment instead. Added the audit.ignorePrune setting. When set to true , pnpm audit --fix removes ignored GHSA entries that no longer appear in the audit report. pnpm init now pins the latest pnpm version, instead of the version of pnpm that ran the command. A project scaffolded by an outdated pnpm therefore no longer inherits that staleness through its own devEngines.packageManager / packageManager pin #7490 . The version is read from the latest tag on the package-manager registries. When that lookup cannot answer — no network, an unreachable or slow registry, offline , or a latest that the minimumReleaseAge / trustPolicy settings reject — pnpm init pins the running version as before, and never fails or hangs on the lookup. A latest that is older than the running pnpm is never pinned either. Allowed pnpm update --patches to refresh registry revisions through a configured pnpr server while retaining locked package versions. Added explicit registry revision selection with <version>+rN and pnpm update --patches for refreshing revision artifacts without changing package versions. Registry-backed lockfile policy checks recognize historical revisions, and pnpr now preserves safe revision histories from upstream registries. Added support for registry replacement tarballs using standard integrity values, explicit revision fields, registry routing from the registries setting, non-redirecting integrity-addressed URLs, canonical safe-integer revision numbers, and pnpr proxying for immutable upstream revision artifacts. Running pnpm setup , pnpm self-update , or a command that modifies the global installation (such as pnpm add --global ) through sudo now fails with ERR_PNPM_SUDO_NOT_SUPPORTED instead of silently operating on the root user's home directory. pnpm keeps global packages and configuration in the invoking user's home directory, so these commands never need root permissions. Read-only global commands (such as pnpm bin --global ) still work under sudo. pnpm stage approve now approves several staged packages at once. Run it without a stage id to pick from the staged versions interactively, or pass a list of stage ids. 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 selected packages are approved in dependency order, and a package whose workspace dependency could not be approved is skipped instead of being published against a dependency that never reached the registry. Patch Changes Deprecated the pnpmfile filterLog hook in pnpm v12. The Rust CLI ignores it and emits a warning. The built-in compatibility database no longer adds dependencies that were detected by static analysis of published packages. Those entries named packages that are only imported for their 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 database keeps its @yarnpkg/extensions entries and pnpm's own curated ones. When no directory above the project accepts a hard link — inside an AI agent sandbox that only grants write access to the project, or a container with just the project mounted writable — the default store is now 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 . Platinum Sponsors Gold Sponsors",
  "tags": [
    "pnpm",
    "pnpm.releases",
    "package-manager",
    "nodejs",
    "open-source"
  ]
}

Get Your Free API Key

Sign up to access the full changelog API. All public sources are free — no credit card required.

Sign Up Free →

Tags:

package-managernodejsopen-source

Related Sources

Favicon

 

  
  
Favicon

 

  
  
Favicon

 

  
  

Share: