Import cmd-shim source, tests, and snapshots from pnpm/cmd-shim at
83f9b9449c2adeec26e1cc2d03ef0fe82d2e41ca. Maintaining the shim alongside
its consumers removes the need for coordinated changes across repositories.
Publish the package as `@pnpm/bins.cmd-shim` and replace external dependencies
with workspace links. Preserve the original BSD-2-Clause license, including
its copyright notice, and keep the manifest updater from replacing it with MIT.
Reuse `@pnpm/fs.graceful-fs` and integrate the existing Node.js tests with the
workspace test scripts. Retain the existing snapshots and align the version
with the TypeScript workspace's required 1100.x band.
Compare child dependencies by alias and recursively check child node compatibility when
depPaths differ in isCompatibleAndHasMoreDeps. This enables packages with peer-suffixed
children (such as next depending on styled-jsx with an optional babel-plugin-macros peer)
to be correctly deduplicated across workspace projects.
Closespnpm/pnpm#14800
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Shared child-resolution results can omit peers supplied by the owning
importer's ancestors. A deeper occurrence also cannot safely await the
shallower owner's promise because that can deadlock on dependency cycles.
Before auto-installing peers, collect missing required peers from the
resolved dependency graph and filter providers at each dependency level.
Collect peers with no possible provider directly in one graph pass. For
peers with possible providers, visit each package at most once per name
and stop at subtrees with a provider. Use iterative traversal to handle deep graphs
and dense dependency cycles without enumerating paths. Keep optional-peer
collection and the child-resolution ownership rules unchanged.
Cover two workspace projects sharing a local package at different depths,
both importer orders, strict peer checks, one complete child snapshot,
and a frozen-lockfile reinstall.
Closespnpm/pnpm#14840.
`get_peer_version_range` stripped `workspace:` but left bare shorthand (`workspace:^`/`~`/empty) as an unparseable range that could never satisfy any installed version, so `pnpm peers check` always reported such peers as unmet; falls back to `*` for those tokens instead, matching `workspace-range-resolver`'s existing wildcard handling. Closespnpm/pnpm#14770.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three warm-install optimizations under the global virtual store.
`node_modules/.package-map.json` is written only when
`nodeExperimentalPackageMap` is set. Nothing reads it otherwise: `pnpm run`
and `pnpm exec` only hand it to Node under that setting. The gate was already
on two of the TypeScript writes and not the rest; it is now uniform, and in
one place per stack.
Dependency-graph digests are written straight into a reused buffer instead of
being built as a `serde_json` value and serialized. The digests are unchanged
- they name directories in the store and are shared with the TypeScript CLI -
and two tests hold each hand-written serializer to the `hash_object` of the
value it models.
The derived map of snapshot -> slot suffix is cached in the cache directory,
next to the lockfile-verification cache, as derived state a run may always
recompute. Only the restore path may use it: the key names the lockfile file,
so an install that just resolved would otherwise be served the map of the
lockfile still on disk and would materialize every package the resolve added
at the flat-named fallback slot. The fingerprint covers the lockfile bytes,
the snapshot keys the caller holds, the engine string, the allow-build policy,
the lockfile directory and a format version.
The map gate also covers the hoisted linker, which writes the file from its
own linker, and an install that writes no map removes the one a previous
install left: the map is handed to Node by existence, so one kept across a
dependency change would describe a `node_modules` that has moved on.
Adds a regression test for the completion marker that pnpm/pnpm#14518 relies
on: a slot left half-imported must be repaired, not accepted as materialized.
A warm global-virtual-store restore of the 2710-package benchmark fixture goes
from ~121 ms to ~110 ms over three hyperfine runs.
Windows access denied is ambiguous: an open child handle can prevent a
rename, but permanent ACL and destination conflicts report the same error.
Use a one-second budget for permission errors while retaining one minute
for explicit sharing/lock violations and busy errors. Apply the short
budget to the original operation start and never extend it after a later
error changes kind. Cap sleeps to the deadline and return the last error.
Apply the same policy to v11's synchronous file-rename helper. Add fake-clock
budget tests and native Windows tests for restrictive ACLs, read-only
rename destinations, directory destinations, and temporary child handles.
Enable the Windows shim deletion-error regression test. Isolate the ACL
tests from elevated runner privileges with scoped token changes. Restore the
Node process privileges after the TypeScript ACL assertions.
Remove a redundant return caught by Windows-targeted clippy. Restart the
fetch test worker pool after draining it so later tests can use it.
Closespnpm/pnpm#14682.
The npm resolver returns the manifest object its metadata cache holds, so every
dependency that resolves to the same package version gets the same object.
Resolving a dependency then writes to it: `dependencies` is defaulted, the
read-package hook rewrites dependency fields, a deprecation notice is carried
over from the lockfile, and an `engines.runtime` entry becomes a dependency.
Copy the dependency and peer fields first, down to each peerDependenciesMeta
entry, so one install cannot decide what the next one resolves.
The copy is deliberately shallow and limited to the fields those writes reach.
Deep-cloning the manifest in the resolver instead costs ~37us per manifest
against ~0.1us here, on a path that runs once per resolved dependency edge.
Related to pnpm/pnpm#13988. That issue's exact symptom could not be
reproduced, so this is not claimed as its fix.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Added a new setting `trustPolicyExcludePrune` (default: `false`). When enabled, `pnpm add`, `pnpm update`, and `pnpm remove` prune the entries of `trustPolicyExclude` in `pnpm-workspace.yaml` that the freshly written lockfile no longer resolves: versions that are gone are dropped (an entry is removed once none of its versions remain), and entries for packages that are no longer in the lockfile are removed too. Name patterns (`@scope/*`) are always kept. The cleanup is skipped when the install's lockfile does not cover the whole workspace (`sharedWorkspaceLockfile: false`), since entries another project still needs would look stale.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Registry metadata mirrors were keyed on host[:port], so several registries
served from one host under different path prefixes shared one directory and
could answer with each other's versions, integrity hashes and tarball URLs.
Both stacks now key the mirror on
<scheme>%3A+<host>[+<port>][%2F<path>][%5F<sha256>]. The host and every path
segment are percent-escaped down to [A-Za-z0-9._-], so a `%` or `+` in a key
is always one the encoder wrote and the key can carry no path separator, no
character Windows rejects in a filename, and no glob metacharacter — the
cache commands feed the key to a glob whose matches `pnpm cache delete`
removes. The scheme is included because http and https at one host and path
are two different trust domains: metadata served over http can be rewritten
in transit and must never reach a resolution configured for https. The
separators are percent-escapes because every key pnpm wrote before this
change was a URL host, which can never hold a `%`; no new key can therefore
land on the stale directory of an unrelated one whose hostname held a
separator, which would otherwise let `https://nexus/npm/` read what was
cached for `https://nexus_npm/`. A path that is not all lowercase gets a
sha256 suffix, the guard encodePkgName already applies to package names, so
HFS+ and NTFS cannot merge two registries; a trailing `.` is escaped because
Win32 strips one; and a key too long for a 255-byte filename is replaced by
its own hash. Only the one trailing slash the resolver itself appends is
normalized away; a repeated slash reaches the registry as a distinct request
path and stays in the key.
Every cache directory is renamed, so the first install after upgrading
refetches registry metadata once. The package store is untouched.
`pnpm cache view` decodes the key back to the registry rather than replacing
`+` with `:`, the registry URL is redacted in both stacks' errors, and the
Rust diagnostic codes now match pnpm's.
Closespnpm/pnpm#13558.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
A dependency's `requiresBuild` is read off its published files, which do
not carry the patch yet. A patch that adds `preinstall`, `install`, or
`postinstall` — or a `binding.gyp`, which the lifecycle runner turns into
an implicit `node-gyp rebuild` — therefore left the package looking
build-free: pnpm 12 never ran it and never offered it in
`pnpm approve-builds`, while pnpm 11 ran it without asking for approval
at all.
pnpm 12 previews the patch in memory (`pnpm_patching::preview_patch`)
while it builds `requires_build_map`, so the allow-build gate, the build
graph, `pendingBuilds`, and the side-effects cache key all describe the
package that ends up on disk. Deciding it there rather than after
`apply_patch_to_dir` is what makes the fix reach existing users: the key
derived from the pre-patch answer matches the entry a broken version
already wrote, and that cache hit would suppress the build forever on any
machine that had installed once. The preview answers for the package the
apply would leave: it chains repeated records for one file, drops a path a
later record deletes, resolves header paths the way `apply_one_file` does,
and reports the manifest verbatim so the caller's BOM-tolerant parser sees
what it would on disk.
pnpm 11 recomputes from the patched directory, where its filesystem-based
applier can reach it, through the new `dirRequiresBuild`, which reads the
same two triggers off a directory that `pkg_requires_build` reads in the
Rust stack. The recompute
runs even under `ignoreScripts`, because pnpm 11 must also stop writing a
side-effects entry for a package whose build is still owed: the entry the
patch alone produced would replay on the install that follows an approval,
and the scripts would never get their chance.
`manifest_requires_build` now reads a script's value rather than its key,
matching pnpm v11's `pkgRequiresBuild`. A manifest carrying an empty
`postinstall` runs nothing, and gating it would have asked the user to
approve a build that does not exist.
The preview is answered once per patch rather than once per snapshot: peer
variants of a package share both the extracted manifest and the patch, and
each preview reads and parses two files on the install path.
The preview also answers for a manifest a patch deletes and writes again,
tracks written paths as a set so a delete costs a lookup rather than a scan,
and `dirRequiresBuild` reports no build for a directory it cannot inspect,
as `pkg_requires_build` already did.
A header that spells the manifest in another case resolves through the
filesystem rather than by guessing the platform, so the preview reads the
manifest exactly where the applier writes it.
The preview is skipped for a package whose published manifest already
declares a build: a patch cannot subtract that, so reading and parsing the
patch would change nothing.
The two stacks agree on what a user sees. They still derive the
side-effects cache key differently for a patched package that runs
scripts, so such a package rebuilds on every pnpm 11 install, as it does
today.
Closes https://github.com/pnpm/pnpm/issues/14648
Closespnpm/pnpm#14067
Support `!<pkg>` negation syntax in `--allow-build` and pre-emptive package
arguments in `pnpm approve-builds`.
- `pnpm add --allow-build=!<pkg>` sets `allowBuilds[<pkg>]` to `false`
rather than writing a literal `!<pkg>` key.
- `pnpm approve-builds` processes positional arguments even when no builds
are awaiting approval, and warns about packages that are not awaiting
approval instead of erroring out. The warning goes through the reporter,
so it renders as `[WARN]` and is suppressed by `--reporter=silent`.
- An argument that names no package is rejected before anything is
persisted, so an empty `allowBuilds` key can no longer be written.
- Global installs honor the negation too: `pnpm add -g` used to drop the
denial, leaving the package undecided so the post-install approval
prompt offered the build the user had just rejected.
- A package that was not awaiting approval is not rebuilt: it is not
installed yet, and the rebuild would demand a lockfile the project may
not have.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Import writes its output to the configured lockfile directory, but cleanup
still removed the lockfile beside the project manifest. Use lockfileDir for
cleanup in both CLI versions while keeping foreign-lockfile reads in the
project directory.
Preserve the destination's env document when replacing its dependency graph.
Parse the foreign lockfile before changing the destination. Back up the
destination and restore it on failure in both versions.
Use the configured wanted-lockfile name for backup, replacement, and recovery.
Branch imports must not change the shared lockfile. In v11, seed an empty
branch lockfile to prevent the reader from falling back to the shared graph.
Add new regression tests for preserving the project-local lockfile and the
external env document, failed-import recovery, and branch lockfiles in both
versions.
Closespnpm/pnpm#14563.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
lockfileToHoistedDepGraph replays the current lockfile with `force: true`
and an empty skip list to collect the directories the previous install
left on disk. `force: true` bypasses the installability guard in
fetchDeps, and the local skipFetch fast path only triggers for packages
currentHoistedLocations already places, which a skipped optional
dependency never is. Every one of them therefore reached
storeController.fetchPackage(), and because the replay's promises are
never awaited the downloads continued past "Done".
linkHoistedModules only reads Object.keys(prevGraph), so the replay
needs directory names and nothing else. Give fetchDeps a skipFetching option
that stubs the fetch response instead of calling the store controller,
and pass it when building prevGraph.
Routing the replay through the existing skipFetch branch would not do:
that branch still calls getFilesIndexFilePath, which throws
NO_RESOLUTION_MATCHED for a variations resolution carrying no variant
for the current platform.
Closespnpm/pnpm#14139
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
pnpm declares a `nopt` type for every setting, so any command that lists
a setting in its option types takes it as a bare `--<setting>` flag.
pacquet only knew the flags each command declared to clap plus the
`BARE_SETTING_FLAGS` table, and rejected the rest as unexpected
arguments. Vercel runs every install as `pnpm install --unsafe-perm`, so
every pnpm 12 install there failed; it was masked until 12.3.3 by the
placeholder bin failing first (pnpm/pnpm#14527). `pnpm install
--dangerously-allow-all-builds`, `pnpm add foo --offline`, and `pnpm
install --engine-strict` failed the same way.
Probing every boolean option of every TypeScript command against the
binary found seventeen settings with a `Config` field and no table row.
Add them: dangerously-allow-all-builds, engine-strict,
force-legacy-deploy, frozen-store, ignore-pnpmfile, ignore-scripts,
link-workspace-packages, lockfile-include-tarball-url,
merge-git-branch-lockfiles, node-experimental-package-map, offline,
prefer-frozen-lockfile, prefer-offline, save-workspace-protocol,
shared-workspace-lockfile, unsafe-perm, verify-store-integrity, and
virtual-store-only. A command that declares one of them to clap keeps
its own flag; the rest take the setting from the table.
`linkWorkspacePackages` and `saveWorkspaceProtocol` are a boolean or a
keyword (`deep`, `rolling`), so the table gains a `BooleanOr` arity:
every boolean spelling, plus the keyword. Whether the keyword is
spellable bare follows the `nopt` type pnpm gives the setting:
`[Boolean, 'deep']` makes `--link-workspace-packages=deep` parse, while
`saveWorkspaceProtocol` is `Boolean` alone, so `rolling` reaches it only
through the `--config.` form, as in pnpm 11.
`--virtual-store-only` re-runs the hoist-pattern derivation the yaml
layer applies, and `--no-virtual-store-only` puts back the patterns a
lower layer's `virtualStoreOnly: true` emptied, verbatim: the derivation
keeps them on `Config` when it empties them, since pnpm merges the
command line before it derives and never empties them at all. `sharedWorkspaceLockfile` now records itself in
`explicit_settings` like the other overrides.
The TypeScript `remove` command registers `unsafe-perm` as well, as
pnpm/pnpm#14432 did for `trust-lockfile`; it was the only command where
the two stacks disagreed.
The flags still rejected are command options with no setting behind
them (`update --force`, `add --workspace`, `install --no-save`,
`init --bare`, `licenses list --compatible`, `--parseable` outside the
list family) and need clap arguments on those commands.
Related to pnpm/pnpm#14346.
Record when publishConfig.linkDirectory disables publish-directory links in importer snapshots.
Treat that setting as manifest drift, so installs re-resolve workspace links.
Fixespnpm/pnpm#14488
Pacquet omitted the lifecycle-script flags from its update argument set even though the TypeScript CLI accepts them. Add both boolean forms and merge them into the update config before dispatch, matching the existing install-family behavior.
Fixespnpm/pnpm#14512.
`--config.trust-lockfile=true` already worked on every command, but the
bare `--trust-lockfile` spelling only parsed on install, add and link, so
`pnpm rm foo --trust-lockfile` answered `Unknown option`. Register it on
remove, and on update for the same reason, so the two forms agree.
This grants no capability the `--config.` form did not: it only decides
whether the supply-chain verification pass runs, for every entry.
TypeScript: rcOptionsTypes() in remove.ts picks trust-lockfile,
trust-policy, trust-policy-exclude and trust-policy-ignore-after from
allTypes; cliOptionsTypes() spreads it, and the values already reached
the installer once parsed. update registers the same four names; its
handler already forwarded them through installDeps.
pacquet: trust-lockfile becomes a BARE_SETTING_FLAGS row, next to the
three trust-policy* settings that were already there. Besides remove,
this makes --config.trust-lockfile=<value> and the bare pair reach
Config on add, update, dedupe, link, fetch and rebuild; install and
deploy keep their own clap flag, which wins over the setting. remove
already read config.trust_lockfile, so it needs no change of its own.
---------
Co-authored-by: khai96_ <hvksmr1996@gmail.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
`pnpm add ./pkg`, `pnpm add file:./pkg`, `pnpm add ./pkg-1.0.0.tgz` and
`pnpm add https://host/pkg.tgz` all failed with
`ERR_PNPM_PACKAGE_MANAGER_ADD_RESOLVE_LATEST`: the Rust CLI's `add` only
special-cased an alias-less *git* selector before falling through to
`split_name_spec`, which read every other bare specifier as a registry
package name.
`add` decides the manifest specifier before the install runs, so an
alias-less selector has to learn the package's name from the package
itself. `resolve_aliasless_specifier` now dispatches the way the install's
resolver chain does — git, then a remote tarball URL, then the local
filesystem — and reads the name out of the checkout, the downloaded
archive, or the directory / local tarball. A classified tarball failure is
forwarded transparently, so `add` reports the same code an install of the
same URL does. Resolving a remote tarball downloads it once more than
before: the fetcher never reads the mem cache, and the warm-store reuse is
keyed off the prior lockfile, which cannot hold a URL being added for the
first time.
The local branch dispatches on `is_local_filesystem_specifier`, a new
predicate deliberately narrower than what the local resolver itself claims:
a bare `<a>/<b>` is a hosted-git shorthand and a `<alias>:<pkg>` a
named-registry reference, and the resolver chain only gets away with
claiming those by running the local resolver last.
Auto-cataloging now holds back every project-relative path in both stacks.
A catalog entry is read by every project that references it, so it cannot
hold a path that resolves against the project declaring it: the catalog
resolver refuses a `link:` / `file:` entry with
`ERR_PNPM_CATALOG_ENTRY_INVALID_SPEC`, and — worse — accepts a
`workspace:../pkg` one, leaving every consumer to resolve the relative path
from its own directory. The TypeScript CLI is affected for a local tarball,
which resolves to a version; the `link:` and `workspace:` directory shapes
escaped only because they resolve to none.
Separately, every `TarballError` that names a URL rendered it verbatim, so a
failed fetch of `https://user:pass@host/pkg.tgz` printed the password into
terminal scrollback and CI logs, and the unclassified-failure path printed the
query token of a signed URL — `redact_and_sanitize` keeps the query and
fragment. Each such message now goes through `redact_url_for_display`, and the
unclassified cause is flattened into redacted text rather than attached as a
source, since miette renders every frame of a source chain.
Closespnpm/pnpm#14437
`peerDependencies` is only absent on the `depth: -1` arm of
`DependenciesTreeNode`, which represents linked local packages.
`resolvePeersOfNode` returns early for those nodes, so reads inside it are
already safe. The one reachable read is the `as T` cast in
`inheritedParentPkgBreaksPeerDiamond`, which can land on a linked node when the
consumer both inherits a peer provider from an ancestor and has a linked local
package of the same name and version among its own children.
The regression test builds that graph out of a real `depth: -1` node rather than
a hand-forged `PartialResolvedPackage`, and asserts the resolved graph.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
The pnpm container image intentionally omits a system Node.js installation and relies on project-pinned downloaded runtimes. The TypeScript installer imported the runtime package before dependency builds but linked its binary into the importer only after those builds, so sibling lifecycle scripts could fail with `node: not found`.
Link only direct runtime dependency bins before scheduling dependency lifecycle scripts. Apply the same ordering to fresh and frozen installs while leaving the normal final bin-linking pass unchanged. Pacquet already links importer bins before dependency builds; add an equivalent regression to preserve parity.
Fixespnpm/pnpm#14358.
Reuse the existing URL-scoped registry credential lookup for every Node.js mirror request instead of introducing a separate authentication setting.
This covers release indexes, SHASUMS files and signatures, and runtime archives in both implementations while retaining longest-path-prefix scoping and cross-origin redirect protections. Authenticated checksum metadata bypasses the URL-keyed disk cache so it cannot cross credential contexts.
Closespnpm/pnpm#14334.
getHoistableOptionalPeers bounds its candidates by the workspace root's own
specifier for the package, so the root decides the version an optional peer
is hoisted at. A specifier disjoint from the declared peer range bounded the
candidates down to none: nothing was hoisted, and the importer then fell back
to the root's version through resolvePeersFromWorkspaceRoot, which is the very
version the peer range rejects. A root pinning date-fns 2.30.0 gave an
importer @base-ui/react(date-fns@2.30.0) for an optional date-fns@^4.0.0 peer
while date-fns@4.4.0 sat in the graph, and pnpm peers check then reported that
resolution as unmet.
The root's specifier is now consulted only when it overlaps every range
recorded for the peer. An overlapping pin still bounds the candidates as
before, so the hoisting corrected in pnpm/pnpm#13369 stays; a disjoint one is
dropped and the picker maximizes over the graph again.
Fixespnpm/pnpm#13867
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
pnpm/pnpm#14243 recorded every verified shared build artifact's envelope
digest in the consuming package snapshot, keyed by input key, artifact
owner, and consumer platform fingerprint. Reproducibility is now enforced
by the registry instead: a published artifact occupies a write-once slot
per input key and compatibility set, so the digest a lockfile would have
pinned is the only one that entry can ever serve.
That makes the pins pure lockfile churn. They were per-machine — a
platform fingerprint only the machines sharing it could write — so a
lockfile grew an entry per package, per owner, and per platform in the
team, and no two contributors produced the same diff.
Remove the `artifactPins` snapshot field, the `pnpm update
--build-artifacts` mode that rewrote it, and the pin plumbing through
resolution, materialization, and the pnpr client in both stacks. Stored
artifacts are still reverified against current trust, policy, platform,
and source before reuse; that check now reports a verdict rather than
returning the digest that fed a pin.
The feature was never released, so no changeset records its removal.
Related to pnpm/pnpm#13771.
Resolve virtual store paths against the canonical modules directory so installs through a
symlink do not rewrite another checkout.
Keep partial workspace lockfile-only operations in the resolution path and stop before
materializing dependencies.
Fixespnpm/pnpm#14286
`sideEffectsCache` now carries the whole story — whether a build is
restored, whether one is saved, and the remote tier that shares it
between machines. `remoteSideEffectsCache` broke the rule that a setting
extending another starts with its name, sorting away from the family it
belongs to; uniting the three rather than renaming one follows what the
registries settings did. The remote tier's `organization` becomes `org`,
which is what pnpr calls that namespace and what its endpoints are built
from.
Both spellings shipped in pacquet 12.0.0 but in no released pnpm 11, so
the TypeScript side is a free rename and only pacquet needs the aliases.
Every older spelling keeps working. Precedence is per field rather than
per section, and does not depend on key order: the two spellings
accumulate separately, `organization` is resolved to `org` per source,
and they are combined once after every source has been read. The Rust
side uses an explicit field rather than a serde alias, which would make a
file carrying both keys a duplicate-field parse error.
Two behaviours move toward pacquet, which already had them right:
`sideEffectsCacheReadonly` blocks writing, and setting it alongside
`sideEffectsCache: false` gives a read-only view rather than switching
the cache off. The declaration can also express writing without reading,
which the Rust `cache`/`readonly` pair had no spelling for, so `Config`
gained the two settings its helpers now prefer.
fix(global): stop `update -g` from downgrading, and pnpm from rolling back
`pnpm update -g --latest` resolves the `latest` dist-tag, which points at
an older release than the one installed whenever that came from another
tag (`pnpm add -g <pkg>@next`) or from a major that has not been promoted
to `latest` yet. The group was reinstalled at whatever `latest` named, so
an update moved packages backwards. Both stacks now compare the resolved
versions against the ones already installed and, when any went backwards,
reinstall the group holding those at the version that is there.
The pnpm CLI's own group is exempt from `update -g` regardless: that
install is `self-update`'s, and reinstalling it here would skip the engine
signature check, the broken-release refusal and the "does it run" probe,
then relink the pnpm home's bins to the result. Naming pnpm explicitly
fails with ERR_PNPM_GLOBAL_PNPM_INSTALL, the rule `add -g` already
applied, and an `npm:` alias no longer slips past either. Two engine
groups side by side also stopped dead-ending every later `up -g` on
ERR_PNPM_GLOBAL_BIN_CONFLICT.
The reporter had a second defect the same command exposed: the differ
redraws by moving the cursor up from the end of its frame, so a frame
taller than the terminal walks past the top of the screen and overwrites
the scrollback above it. The overflowed lines are now handed to the
scrollback, and a frame the differ can no longer reach — outgrown,
shrunk under, or reflowed by a resize — is redrawn afresh below rather
than revised. The Rust height probe works on Windows too.
Closespnpm/pnpm#14270
Persist the signed origin metadata for hydrated remote build artifacts in each package's store-index row. This lets later installs reuse a verified artifact without contacting pnpr, while preserving enough evidence to reevaluate the artifact under the consumer's current trust configuration.
Before reuse, verify the envelope signature, signer, owner, package identity, source integrity, input key, compatibility constraints, lockfile pin, builder profile, manifest-to-diff mapping, and the digest and size of every stored CAS blob. Remote entries do not pass through the ordinary local side-effects-cache path when this verification is unavailable or fails.
Track trusted invalid manifests and corrupt remote blobs in a bounded per-channel quarantine stored alongside the package index. Keep transient network, server, and local filesystem failures out of quarantine so they can recover normally. Frozen stores may reuse valid persisted artifacts but do not hydrate, persist, or quarantine.
Apply the same behavior and serialized metadata shape to the TypeScript CLI and pacquet.
Related to pnpm/pnpm#13771.
Teach the Rust pnpm CLI to accept --fix-lockfile and force a full metadata refresh while retaining the versions represented by the current lockfile.
Repair mode parses package and snapshot entries through a tolerant, security-budgeted loader so malformed derived fields do not prevent the repair itself. It keeps only resolution and dependency data used to seed the fresh resolve while retaining a separately sanitized, unprepared view for filtered merges. Normal, repair-seed, and repair-merge loads use isolated caches, generated top-level fields are normalized when malformed, and the exact merged result passes lockfile verification before materialization or save. The same intent is carried over the pnpr protocol, where repair support is negotiated before the request and supported servers bypass frozen and resolution-cache shortcuts.
Closespnpm/pnpm#14250.
Persist verified shared build artifact envelope digests in the package snapshot
that consumed them. Scope each pin by dependency input key, artifact owner, and
consumer platform so compatible variants remain independently reproducible.
Apply pins only after the existing trust and compatibility checks. A missing
pinned artifact is a remote-cache miss and falls back to a local build; frozen
installs enforce pins without mutating the wanted lockfile.
Add `pnpm update --build-artifacts` as an explicit update mode that clears
existing pins, forces materialization, and records only artifacts that complete
verification and hydration without changing package versions. Reject command
combinations that cannot safely write or verify the lockfile.
Implement the lockfile format, install plumbing, update mode, and tests in both
TypeScript pnpm and pacquet.
Related to pnpm/pnpm#13771.
The TypeScript side asked the registry to resolve a range pin even when the
running pnpm already satisfied it, so it recorded a version nobody was running
and switched away from a satisfying one. It also passed the resolved version as
the specifier, overwriting the range the project had asked for with an exact
version.
It now prefers the running version when that satisfies the pin, matching
package_manager_to_sync, and records the range as the specifier the way
resolve_package_manager_integrities already does through its wanted_specifier
argument.
Related to pnpm/pnpm#13932, which made the Rust CLI record a satisfied range pin
this way.
An override governs a direct dependency's specifier whether or not it
rewrites the text. When it repeats the declared range, the overrides hook
leaves the manifest byte-identical, so comparing the hooked manifest with
the one on disk — the test the previous fix used — sees nothing. `pnpm
update` then moved the declaration onto the version it resolved while the
override held the resolver's view at the old range, and the lockfile
recorded a specifier the manifest never shows. The next
`--frozen-lockfile` install rejected the pair.
The overrides hook now also answers which of a manifest's dependencies it
claims, so the manifest writer can recognise a declaration it does not
own even where no text changed. pacquet reaches the same entries through
`ManifestSpecBumps`, whose apply step consults the run's own
`VersionsOverrider` before moving a range.
Closespnpm/pnpm#14224
`pnpm update` rewrote every direct dependency's `package.json` entry to the
range resolution settled on. For a dependency that `overrides` also claims,
the resolver never read that entry: the overrides hook replaced it in the
manifest handed to resolution, so the version that came back answers the
override. Writing it back destroyed what the project actually declared —
most visibly a `catalog:` reference, which became a pinned version and
dropped the project out of the catalog.
The manifest writer now keeps the declaration on disk whenever the hooked
manifest's entry differs from it and the resolver followed the hooked one.
A dependency the run names with a specifier of its own (`pnpm add foo@2`)
still writes that request.
pacquet reaches the same entries through `ManifestSpecBumps`, whose targets
now carry the specifier `package.json` declares; a bump whose lockfile entry
no longer matches it is skipped, leaving the lockfile and the manifest
agreeing.
Closespnpm/pnpm#12115Closespnpm/pnpm#11658
Move the remaining workspace command pipelines from topological chunks to the shared dependency-ready scheduler. Recursive rebuild, per-project install-family commands, pack, publish, stage approval, dependency builds, and project lifecycle scripts can now dispatch each node as soon as its own prerequisites settle instead of waiting at a workspace-wide layer barrier.
Keep concurrency inside the scheduler so ready work cannot over-dispatch, retain the existing command-specific bail and no-bail policies, and wait for already-dispatched command work where the old Promise.all and scoped-thread paths did. Ignored cycles remain deterministic by dropping backward edges in the graph sequencer's flat order.
With all execution consumers migrated, simplify graph sequencing to order plus cycles, replace grouped project sorting with dependency maps, and remove the obsolete run-groups dependency and chunk helpers.
Implements item 2 of pnpm/pnpm#14211.
Integrate the remote side-effects cache proof of concept with dependency installation in both TypeScript pnpm and pacquet.
Plan eligible build candidates before contacting pnpr, batch lookup requests, verify the configured P-256 trust keys and artifact integrity, hydrate verified files into CAFS, and select the restored side-effects map before lifecycle scripts run. Fall back to the ordinary local build on all cache failures.
Configure the feature through the remoteSideEffectsCache setting, assembled by the config reader from the workspace file, the global config file and the environment. A repository declares which organization and packages are eligible and nothing else: everything describing the act of signing stays with the machine holding the key, so a cloned repository cannot turn that key into a signing oracle.
Capture and publish the actual post-build diff from trusted builders. Bound artifact hydration and blob downloads, retain CAFS paths instead of downloaded buffers, and withhold direct store writes from a store opened read-only. Add transactional owner and global storage accounting to pnpr alongside request, response, timeout, streaming-memory, manifest, variant, descriptor, lock-namespace, concurrency, and crash-residue bounds.
Keep the proof of concept organization-scoped and Linux glibc-only. Defer lockfile pinning, persistent quarantine, publisher ownership, and key lifecycle policy to the RFC.
Related to pnpm/rfcs#20.
Allow revision-only updates to delegate dependency resolution to pnpr.
Carry updatePatches across both pnpr clients and map it server-side to pacquet's RefreshRevisions seed policy. Explicit refreshes bypass frozen reuse and pnpr's whole-resolution cache, while the registry resolver revalidates metadata even when its in-memory or disk cache is warm.
Route both pnpm implementations through their existing pnpr install and materialization paths. The Rust update command accepts --pnpr-server, and partial TypeScript workspace refreshes continue locally so unselected importers are not refreshed.
Follow-up to https://github.com/pnpm/pnpm/pull/14175.
Related to https://github.com/pnpm/rfcs/pull/19.
The pnpr resolution protocol omitted patchedDependencies and packageExtensions. Its fast path therefore returned an untransformed lockfile, while bypassing pnpr for these projects would sacrifice the remote-resolution performance the setting exists to provide.
Compute patch hashes on the client and send only the ordered selector-to-hash map to pnpr. The server uses those hashes to key patched snapshots in the lockfile, while the client retains the local paths and applies the patch files during materialization. Send package extensions and unused-patch policy alongside them so pnpr resolves the same transformed dependency graph as local resolution.
Key pnpr's interned configuration and resolution cache by the forwarded transforms, validate patch digests at the protocol boundary, and scan package-extension dependency URLs through the existing fetch allowlist and inline-credential checks. Implement the protocol in both TypeScript and Rust clients and cover it with live server-to-client tests. Both clients validate the returned transform metadata so older servers that ignore the new request fields fail with a stable protocol error instead of silently returning an untransformed lockfile. pnpr advertises transform support in the HTTP response headers, allowing the Rust client to validate compatibility before consuming any package frames. Current servers retain streaming prefetch performance; older servers fail before they can trigger downloads or unbounded hint buffering.
Fixespnpm/pnpm#14177.
`run_link_phase` returned early under `virtual_store_only` before reaching
`LinkVirtualStoreBins`, while `run_build_phase` still ran dependency
lifecycle scripts under that mode. A `postinstall` that invokes a sibling
dependency's bin — `unrs-resolver` calling `napi-postinstall`, for
instance — therefore died with `command not found` (exit 127) under
`pnpm fetch`, because `pnpm_executor::extend_path`'s ancestor-`.bin` walk
had no `.bin` to find. `pnpm install --frozen-lockfile` against the same
lockfile worked: it does not set `virtual_store_only`.
The per-slot bin pass now runs under `virtual_store_only` too. Its links
live inside the virtual store, which is exactly what that mode
materializes; only the importer-facing passes (prune, direct-dependency
symlinks, root components, hoisting, sidecars) stay skipped.
The TypeScript CLI is unaffected — its `buildModules` links a dep node's
bins itself right before running the node's scripts — so it gets the
matching regression test only, plus the `allowBuilds` widening that test
needs on `FetchCommandOptions`.
Closespnpm/pnpm#14174
Implement explicit registry revision selection and revision-only updates from the current revisions RFC.
Parse exact +rN selectors across npm, JSR, aliases, and named registries. Validate historical artifacts against strict integrity-addressed metadata, overlay only immutable manifest fields, and avoid metadata requests during frozen installs unless an active policy requires them.
Add update --patches to refresh artifacts at the currently locked package versions while preserving explicit revision pins and manifest ranges. Mirror the behavior in the Rust CLI, force reimport when the artifact changes under the same package identity, and make pnpr expose only safe rewritten revision histories.
Follow-up to https://github.com/pnpm/pnpm/pull/13475.
Related to https://github.com/pnpm/rfcs/pull/19.
Git preparation was gated only while fetching a package. Once an approved project placed prepared files in the shared store, a different project could link those files without consulting its own allowBuilds policy.
Persist whether preparation was required in each git package's store-index row and check the active project policy before warm-store reuse. Legacy rows without the marker are reused only with explicit approval; otherwise they go through the existing cold fetch and preparation gate. Revalidate in-process git fetch results as well so a long-lived store controller cannot carry approval across policy changes.
Pass the canonical lockfile resolution ID into both git fetch paths so the allowBuilds identity and suggested key stay byte-for-byte exact. Mirror the marker and warm-reuse gate in pacquet using the shared msgpackr-compatible store format.
Closespnpm/pnpm#13965.
Derive applied patch keys from the final pruned lockfile graph. This lets reused lockfile subtrees and unchanged workspace importers count toward validation on full-workspace resolutions.
Closespnpm/pnpm#13692
`pnpm stage approve` accepted exactly one stage id and wrapped each request in
its own `withOtpHandling`, so releasing a workspace meant one invocation and one
proof of presence per package, in an order the releaser had to work out by hand.
It now takes a list of stage ids, and with none it lists the staged versions and
offers them in a checkbox prompt (the non-interactive case keeps failing with
STAGE_ID_REQUIRED, now with a hint).
The batch runs through a new `OtpSession` in the web-auth package: it keeps the
one-time password a challenge yielded and passes it to every later request,
re-prompting only once the registry stops accepting it — a classic TOTP expires
within a minute, which is exactly the case a whole-workspace approval hits.
`withOtpHandling` is now the single-operation form of that session, so publish
and reject behave as before.
Inside a workspace the selection is sorted through the workspace dependency
graph (the same sequencer the recursive commands use), keyed on the name each
project publishes under, since that is the only name a staged version carries. A
package whose workspace dependency could not be approved is skipped rather than
published against a dependency that never reached the registry. A registry
verdict on one version leaves the rest of the batch running and ends with a
non-zero exit; an authentication failure or a broken connection aborts it.
The registry's description of a staged version decides what a maintainer
publishes, so its id and package name are validated as they came — a hidden
character can neither be stripped into a valid value nor keep a name from
matching a workspace package — and the display-only fields are stripped of the
control characters that could redraw the picker around a selection. The shared
`@pnpm/text.sanitize` package holds that character set for both the staged
picker and `update --interactive`, mirroring the `pnpm-text-sanitize` crate.
Both stacks implement this, down to the partial-batch summary and exit code —
pacquet has no output-with-exit-code channel, so it prints the summary the way
the dispatcher prints command output and exits 1.
Merging git branch lockfiles unions their keys, so a dependency that the main branch removed after a branch lockfile was written comes back in the merged result. Resolution normally prunes it again, but --frozen-lockfile skips resolution and fails the manifest check with ERR_PNPM_OUTDATED_LOCKFILE.
Once the branch lockfiles are merged, prune each importer back to what its manifest declares. The declared names are derived per dependency group the way satisfiesPackageManifest derives them, so a dependency that moved between groups is reconciled rather than left in both. Only a peer that no other field declares is auto-installed, so a dependency listed in both devDependencies and peerDependencies keeps the field that declares it rather than being dropped from it. Pruning importer edges can strand packages, so the merged lockfile then goes through pruneSharedLockfile.
The Rust CLI merges branch lockfiles the same way and had the same bug. It gets the same reconciliation, hooked into the install pipeline because the lazily loaded wanted lockfile has no access to the project manifests at load time.
Fixespnpm/pnpm#13966.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
A pnpm below 11.20.0 pins `@pnpm/exe` beside `pnpm` for a v12 version, because
that is the set its own major is installed from — pnpm/pnpm#13599 is where pnpm
learned that v12 publishes the executable as the unscoped package alone. A
project pinning pnpm 12 whose lockfile such a pnpm last wrote therefore carries
an entry every newer pnpm rejects, and `--frozen-lockfile` fails on it from
either side of the disagreement while a plain install rewrites the block and
hides it again. The two versions take turns.
That entry pins the wanted version through the same integrity and cannot change
which pnpm runs, so the frozen refusal now accepts a block that records every
package this pnpm installs from, whatever else it records. An entry pinning
another version is a lockfile that disagrees with the manifest and is still
refused. The predicate that drives the write keeps the exact-set rule, so a
writable install still rewrites the block to the packages this pnpm installs
from — the trade is that `--frozen-lockfile` no longer guarantees that block is
byte-stable against a later plain install.
Related to pnpm/pnpm#14124.
Under `nodeLinker: hoisted`, peer-resolution variants of an **injected directory dependency** (a `file:` snapshot) collapsed onto the first-seen variant, like registry peer variants do since #14039 (and the TypeScript CLI's long-standing `depPathByPkgId` mapping). With #14034, edges declared against a losing variant then resolve to the surviving copy.
For registry packages that dedup is intended: every variant unpacks the same tarball, and peers resolve through the ancestry either way. An injected directory dep is different — each `file:` variant is materialized as its **own copy of the local package with its own peer-resolved dependency set**; that is the point of injecting. Collapsing rewires every dependent of the losing variant onto the survivor's children.
### Symptom
teambit/bit's root-components e2e (`nodeLinker: hoisted`): two Bit root components pin `is-odd@1` and `is-odd@2` across injected copies of the same component (`comp2@file:comp2(is-odd@1.0.0)` / `(is-odd@2.0.0)`). The second root's copy materialized as a symlink into the first root's tree and resolved the wrong peer:
```
AssertionError: expected '1.0.0' to match /^2\./
```
### The fix
Exempt `file:` snapshots from the collapse in the shared identity function — `pkg_id` in pacquet, `getHoisterPkgId` in the TypeScript CLI. Every index over the hoist result (`pkg_locations_by_pkg_id`, `package_ids_by_pkg_id`, `pkgLocationsByPkgId`) derives its keys from that function, so the variants stay apart with no further changes.
The registry collapse stays untouched: it is what stopped the per-path walk from exploding on peer-variant-heavy lockfiles (the OOM #14039 fixed), and `file:` snapshots cannot explode that way — there is one per injected workspace package.
The Rust CLI computed peer-dependency issues during resolution and then
dropped them on the floor: every call site passed `peer_issues_sink:
None`, and the only other consumer was a `tracing::warn!` the reporter
never shows. `strictPeerDependencies` was parsed and read by nothing.
`pnpm dedupe` was the one command that said anything, via a lockfile
walk of its own after the install.
An install that resolves now ends by applying `peerDependencyRules` to
the issues its resolution left behind and either warning once — the
same line the TypeScript CLI's reporter prints — or failing with
`ERR_PNPM_PEER_DEP_ISSUES` under `strictPeerDependencies`, after the
artifacts are written, the way `ERR_PNPM_IGNORED_BUILDS` already does.
Living at the shared install tail means `add`, `remove`, `update`,
`dedupe`, and `--lockfile-only` all get it, matching where pnpm places
the call in `_installInContext`. Dedupe's own walk goes away as
redundant.
An install that skips resolution — frozen, or an up-to-date lockfile —
still reports nothing, in both stacks. That asymmetry with `dedupe` is
the subject of pnpm/pnpm#14098 and is deliberate: peer issues are a
byproduct of resolution, so whatever ran one reports them, and
`pnpm peers check` is what answers for a tree nobody re-resolved.
Recomputing them on the frozen path would put a second, independent
implementation on the hot path of every CI install and trade an
install-vs-dedupe disagreement for an install-vs-install one. A test in
each stack pins the decision.
The verdict is read off the resolved lockfile rather than off the
resolver's own issue map, because pacquet's `ParentChain` is name-only
and would render every "Wanted:" entry with an empty version. That
makes the lockfile walk shared code, so it moves out of the `peers`
command into a new `pnpm-deps-inspection-peers` crate alongside the
rules filter and the issue renderer — pnpm's `peers-checker` and
`peers-issues-renderer` packages.
Closespnpm/pnpm#14098.