pnpm 12.8

pnpm 12.8 warns when pnpm pack or pnpm publish would ship a .env file, installs sharedWorkspaceLockfile: false workspaces concurrently, applies every setting passed as --config.<name>=<value>, and no longer leaves the Windows terminal stuck after Ctrl+C in a script. This release also carries a long list of bug fixes.

Minor Changes#

Warning about .env files in packages#

pnpm pack and pnpm publish now warn when the tarball includes a .env or .env.* file that the files field of package.json does not list (#7826). Templates such as .env.example are not reported. List the file in files to publish it on purpose, or exclude it in .npmignore or .gitignore.

Concurrent installs without a shared lockfile#

In a workspace with sharedWorkspaceLockfile: false, pnpm install now installs projects concurrently, up to workspaceConcurrency at a time (#14480). A project is resolved, fetched, and written to its virtual store without waiting for the workspace projects it depends on. It waits for them only before it links its dependencies and runs its lifecycle scripts, so its scripts still run after theirs. A project with a preinstall or pnpm:devPreinstall script, or with an injected or file: workspace dependency, waits for its workspace dependencies before it starts.

The installs also share package metadata, lockfile verification, and store caches, so they use less CPU and memory when several projects depend on the same packages.

Related fixes for such workspaces:

  • With enableGlobalVirtualStore, each project keeps its current lockfile and its hidden hoisted dependencies in its own node_modules/.pnpm, so a repeat install no longer relinks the other projects' packages.
  • pnpm rebuild, pnpm approve-builds, and pnpm ignored-builds work on the current project's node_modules (#9402).
  • An injected workspace package that has lifecycle scripts is hard linked into the projects that depend on it, so later edits reach them (#9828).

Every setting accepted as --config.<name>#

Every setting pnpm supports can now be set with --config.<name>=<value> on the command line, not only the ones whose command also carries a matching flag (#16276). Before, pnpm install --config.frozen-lockfile=true dropped the setting and rewrote pnpm-lock.yaml as though the install had not been frozen.

Settings given on the command line, such as --registry and --store-dir, now also take precedence over the values a pnpmfile updateConfig hook sets (#14063).

Ctrl+C on Windows#

Interrupting a script with Ctrl+C on Windows no longer leaves the terminal stuck (#14860). A script that runs through a batch shim, as vite dev does through vite.CMD, made cmd.exe wait forever on its "Terminate batch job (Y/N)?" answer, and every following keystroke went to that prompt. pnpm now ends a cmd.exe script shell once it has sat for a second after the interrupt with nothing running under it. A script that takes longer to shut down is still waited for, and a second Ctrl+C ends the script's shell at once.

Other changes#

  • pnpm pack honors --silent, --reporter=silent, and --loglevel=silent to hide the tarball contents and summary (#10297).
  • An in-place edit to the source of an injected workspace package now shows up in its injected copy, unless a build writes to that package or packageImportMethod is set (#4410). Scripts listed in syncInjectedDepsAfterScripts now update injected dependencies while they run, so a dev server watching the injected package sees each change before the script exits.
  • pnpm update --global reinstalls the global packages that pnpm 10 installed into <global-dir>/5, so their commands are linked again and pnpm list --global lists them. Once every package is migrated, pnpm deletes the previous directory (#11528).
  • pnpm install runs the install hooks of a config dependency plugin's pnpmfile, including readPackage, afterAllResolved, and custom resolvers. Before, only its updateConfig hook ran.
  • Scripts see the npm_command environment variable (#16265) and an npm_config_node_gyp that points at the bundled node-gyp (#16270).

Patch Changes#

Installing#

  • pnpm install --dev and pnpm fetch --dev install the optional dependencies of devDependencies, such as the platform binaries of Biome and oxlint (#9678).
  • pnpm install --offline and pnpm add --offline resolve a range to the newest matching version whose tarball is already in the store (#10715).
  • An offline install that fails on a metadata cache from before pnpm 12.4 explains that one online install repopulates it, with the ERR_PNPM_NO_OFFLINE_META code (#15656).
  • Installing a git-hosted dependency that has to be built no longer fails on unapproved build scripts of its own dependencies (#9764), and a git-hosted pnpm workspace without a committed lockfile is detected as a pnpm project (#14011).
  • pnpm install refreshes dependencies when a local file: directory changes its dependencies (#4623).
  • An optional dependency whose install script fails is removed from node_modules (#8756).
  • With nodeLinker: hoisted, pnpm install restores a deleted workspace project's node_modules, and clears orphaned package directories that an interrupted install left behind (#13676).
  • Installing through a pnpr server records the pnpmfile checksum in the lockfile (#14460) and links a workspace project at its publishConfig.directory.

Resolving and linking#

  • pnpm install no longer aborts on a huge allocation when peer ranges combine overlapping || alternatives (#15867).
  • A registry package with a file: dependency on a directory inside itself, such as "@types/css-tree": "file:./typings/css-tree", installs as it does with npm and Yarn (#9141).
  • An npm: alias written by overrides stays in place when pnpm re-resolves the aliased dependency (#16309).
  • A peer dependency no longer resolves to two different versions for one package (#12098), and an optional peer is no longer resolved from another workspace project's package when that causes bogus unmet peer errors (#13989).
  • pnpm dedupe no longer changes the lockfile on every run when a nested peer is provided through an npm alias (#15709).
  • With resolutionMode: time-based and minimumReleaseAge both set, a subdependency is no longer reported as too new when only the time-based cutoff excludes it (#13569, #16298).
  • A transient metadata fetch failure is retried and no longer reported as TRUST_DOWNGRADE or MINIMUM_RELEASE_AGE_VIOLATION (#12031).
  • pnpm's built-in package compatibility database no longer applies to a project's own manifest, so a project named like vue-loader no longer gains dependencies (#11700).
  • Packages in an external virtualStoreDir can resolve the project's hoisted direct dependencies. Run pnpm install --force to repair an existing installation (#5652).
  • The bins of auto-installed peer dependencies are linked into the workspace root's node_modules/.bin (#8511).

Lockfiles#

  • pnpm install --frozen-lockfile works on a detached HEAD with gitBranchLockfile (#7672), and accepts a lockfile without an importer entry for a workspace package that has no dependencies (#15875).
  • pnpm install fails with ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY when an importer references a dependency version with no snapshot entry (#14764).
  • On CI, an explicit preferFrozenLockfile: true no longer lets the install update an outdated lockfile (#9072).

Workspaces and injected packages#

  • pnpm install no longer creates a node_modules symlink inside the publishConfig.directory of a package linked with linkDirectory, where a build tool cleaning its output could delete dependency files (#16226).
  • An injected workspace dependency that publishes from a publishConfig.directory built by its own prepare script installs correctly (#7811).
  • injectWorkspacePackages treats a dependency declared with a relative path, such as workspace:../foo, the same way as a workspace:* one (#10446).
  • Workspace discovery skips dot-prefixed directories, so ** no longer matches projects inside .cache (#16250).
  • pnpm import keeps the versions pinned by a yarn.lock inside a workspace project (#4385).

Store and caches#

  • Files imported from the store follow the umask of the install (#3807), and files already in a shared store keep their owner, group, and mode (#12765).
  • Repairing a store file modified through a hard link keeps its inode on Linux and macOS, so other projects are healed at the same time (#3445).
  • pnpm warns when it cannot hard link from the store in the pnpm home and falls back to a store on the project's filesystem (#14505).
  • The side-effects cache restores symlinks that a build script creates. After upgrading, every package with a build script is built once more (#12859).
  • Concurrent installs that share a global virtual store build a package in its slot one at a time (#15568).
  • A warm install reuses on-disk metadata for five minutes when the registry sends no ETag (#13976), and honors Cache-Control for registry metadata (#13487) and tarball URL dependencies (#15648).

Patched dependencies#

  • pnpm install repairs a lockfile whose patch_hash paths disagree with patchedDependencies, and --frozen-lockfile fails on it with ERR_PNPM_INCONSISTENT_PATCH_HASH (#15336).
  • A missing patch file fails the install with ERR_PNPM_PATCH_NOT_FOUND (#5268).
  • With nodeLinker: hoisted, a shared copy of a patched dependency is patched only once (#7565).
  • engineStrict checks the patched package.json (#9603).
  • pnpm patch applies the existing patch to a git-hosted dependency (#9699).

Adding and updating#

  • pnpm add <dir> warns when the directory declares peer dependencies (#5523).
  • pnpm add --save-types skips deprecated @types/* stubs such as @types/typescript (#15636).
  • pnpm version, pnpm add, and pnpm pkg set keep JSON5 style in package.json5 (#15717).

Running scripts#

  • pnpm run and pnpm exec no longer auto-install when the root package.json still keeps overrides or similar settings in its pnpm field. They fail and ask to move the settings to pnpm-workspace.yaml (#16278).
  • When verifyDepsBeforeRun installs before a filtered command, it installs only the selected projects (#11865).
  • pnpm -r run /regexp/ honors tasks dependsOn (#15596).
  • pnpm run exits with the code of a script that handles Ctrl+C (#9945), and no longer hangs when a background process keeps a finished script's output open (#5730).
  • scriptShell is used even when shellEmulator is enabled (#14719).
  • With enableGlobalVirtualStore, dependency build scripts see the workspace root's node_modules/.bin (#15652).
  • Install scripts find the bundled node-gyp when pnpm runs through a symlink (#15694).
  • Commands run through a dependency's own node_modules/.bin no longer fail with MODULE_NOT_FOUND (#10189).
  • pnpx --version and pnpm dlx --version print the pnpm version (#16259).

Publishing and deploying#

  • pnpm pack and pnpm publish ship a file that files names even when another entry excludes its directory (#16213), and prune directories that a files exclusion names (#15738).
  • pnpm publish waits at least 5 minutes for the registry to answer, fixing "409 Conflict - Failed to save packument" errors (#11454).
  • pnpm deploy --prod works with a devEngines.runtime that uses onFail: download (#15703).
  • pnpm deploy copies workspace dependencies instead of hard linking them to their sources (#12176), and pnpm deploy --legacy no longer leaves broken links (#9575).

Configuration#

  • pnpm config set --location=project inside a workspace package writes to the workspace root's pnpm-workspace.yaml (#13757).
  • PNPM_CONFIG_WORKSPACE_DIR is read like other settings (#16275).
  • An invalid base64 _password fails with ERR_PNPM_AUTH_INVALID_BASE64 (#16273).
  • proxy=false turns proxying off even when proxy environment variables are set.
  • A pnpmfile fetchers hook runs once per package (#15584, #15025), a custom resolver's package is re-fetched when its integrity changes (#15670), and invalid readPackage results are rejected (#15730, #15705).

Global packages and pnpm versions#

  • Signals such as SIGTERM reach the pnpm version pnpm switches to and the one pnpm with runs (#9948).
  • On arm64 musl Linux, switching to a pinned pnpm older than 12 runs the JavaScript package (#10443).
  • Global shims work when pnpm runs through a relative symlink, as with Homebrew (#15691).
  • pnpm env remove --global deletes Node.js versions in pnpm's store (#8357).
  • pnpm self-update no longer suggests a downgrade when minimumReleaseAge holds back latest (#12006).

Windows#

  • pnpm run passes arguments to the script as typed, without %VAR% expansion or doubled backslashes (#16257).
  • pnpm.exe runs without the Visual C++ Redistributable (#15723).
  • .cmd shims keep a % in the project path (#15716) and run tools with non-ASCII paths (#6999, #16217). Bin shims run from Cygwin again (#12845).
  • Installing pnpm with npm writes node_modules/.bin shims that run pnpm.exe (#15688).
  • Wildcard workspace patterns work when the workspace is on a different drive than the pnpm cache (#16239).
  • pnpm setup no longer writes pn.ps1, pnpx.ps1, and pnx.ps1, which failed under restrictive execution policies (#8444), no longer panics on non-ASCII environment variable names (#15684), and expands nested %VAR% references in PNPM_HOME (#13236).

Inspecting dependencies and output#

  • pnpm audit fails on unresolvable dependency references (#13638).
  • pnpm licenses list reports actual on-disk locations with hoisting (#8589).
  • pnpm root prints the configured modulesDir (#9113).
  • With --loglevel warn or error, the full output of a failed install script is printed.
  • Resolution errors show their cause, such as invalid peer certificate: UnknownIssuer (#9556), and SSH Permission denied (publickey) errors suggest ssh-add -l (#13743).
  • Lockfile verification errors carry specific codes and suggest relaxing a policy only when that could help (#14411).

See the v12.8.0 release notes for the complete list of changes.