Commit Graph
14936 Commits
Author SHA1 Message Date
dependabot[bot] 6027072099 chore(deps): bump the github-actions group across 1 directory with 2 updates
Bumps the github-actions group with 2 updates in the / directory: [vedantmgoyal9/winget-releaser](https://github.com/vedantmgoyal9/winget-releaser) and [pnpm/update](https://github.com/pnpm/update).


Updates `vedantmgoyal9/winget-releaser` from 7bd472be23763def6e16bd06cc8b1cdfab0e2fd5 to b7370d4d152efdce803a26cc6ca9eee58cb2ad54
- [Release notes](https://github.com/vedantmgoyal9/winget-releaser/releases)
- [Commits](https://github.com/vedantmgoyal9/winget-releaser/compare/7bd472be23763def6e16bd06cc8b1cdfab0e2fd5...b7370d4d152efdce803a26cc6ca9eee58cb2ad54)

Updates `pnpm/update` from fcaa952ef94bad62067a94ac2e5c6234b648e259 to 2457663d4150026edd3adb62b660eb85571bc7cb
- [Commits](https://github.com/pnpm/update/compare/fcaa952ef94bad62067a94ac2e5c6234b648e259...2457663d4150026edd3adb62b660eb85571bc7cb)

---
updated-dependencies:
- dependency-name: pnpm/update
  dependency-version: 2457663d4150026edd3adb62b660eb85571bc7cb
  dependency-type: direct:production
  dependency-group: github-actions
- dependency-name: vedantmgoyal9/winget-releaser
  dependency-version: a8fff44b123f115ce4c21543edf8b677376fc7e0
  dependency-type: direct:production
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-30 11:52:21 +00:00
405581614c fix(config): honor publishBranch and doctor timeout (#17399)
Read publishBranch from pnpm-workspace.yaml and PNPM_CONFIG_PUBLISH_BRANCH,
with --publish-branch taking precedence, and give the pnpm doctor registry
ping a fixed 15 s timeout instead of fetchTimeout, matching pnpm v11.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:11:29 +01:00
230defacaa fix(catalog): reuse a catalog entry set to the requested dist-tag (#17441)
A catalog entry set to a dist-tag, such as `beta` or `latest`, already
says which tag to follow. When `pnpm add` asks for that tag, save the
dependency as `catalog:` and leave the entry as written.

v12 compared the entry with the resolved range only, so prefer mode kept
the dependency direct and strict mode failed with a mismatch. It now
reuses an entry that equals the dist-tag typed in the selector. Only an
explicit tag the registry resolved matches: a versionless re-add does not
match an entry set to the configured default tag, and a tag that
linkWorkspacePackages resolves to a local package keeps its workspace:
specifier.

v11 saved `catalog:` but rewrote the entry to the tag's current version.
The explicit-dist-tag pin added for `pnpm update foo@beta` on an entry set
to a version also fired when the tag came from the catalog entry itself,
because the resolver swaps the entry in as the wanted specifier.
`getCatalogUpdatesOfProject` now skips the update in that case. This also
covers a plain `pnpm add foo`.

Closes pnpm/tasks#225.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:10:00 +01:00
a85c8f719f fix(cli): honor directory flags in dlx and create (#17479)
`-C/--dir` is documented as "run as if pnpm was started in <path>", but
dlx, pnx, and create spawned the executed command in the process cwd in
both CLIs, ignoring `--dir` and `--workspace-root`.

pnpm v12: dispatch dlx and create with the CLI-directory anchor
(`cli_dir`), which is the `--dir`/`-w` directory when one was given on the
command line and the invocation directory otherwise. SpawnEnv borrows
that path for the spawn cwd.

pnpm v11: the dlx handler spawns in `cliOptions.dir ?? process.cwd()`,
matching single-project exec. `-w` already writes the workspace root into
`cliOptions.dir`. create delegates to the dlx handler with its options
unchanged.

Tests:
- Rust: the bin's cwd, forwarded arguments, and output location for
  dlx and create, with `-C`, `-w`, and no flag, in direct and shell mode.
- v11 e2e: dlx with `-C`, `-w`, and no flag.
- v11 unit: create forwards `cliOptions` to the dlx handler.

---------

Signed-off-by: BayLee4 <63376748+BayLee4@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 11:17:04 +01:00
Zoltan Kochanandgithub-actions[bot] 7b693e2727 chore: update dependencies, Node.js, pnpm, and GitHub Actions (#17506)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-30 10:39:28 +01:00
74b5e3ed48 fix(deps-restorer): keep a project's own nested copy as its direct dependency (#17499)
importer_direct_deps set a workspace project's direct dependency to the
first location of the snapshot anywhere in the tree. When two projects
each hold a nested copy of the same snapshot, the second project's
direct dependency pointed at the first project's copy. Linking the
direct dependencies then moved the second project's own copy to
.ignored_<name> and put a symlink in its place. The next install's
package_present_at rejected the symlink and imported the copy again,
so every repeat install did the same work.

A project's direct dependency is now the copy the hoister placed in
that project's own node_modules, when that copy holds the resolved
snapshot. Otherwise the snapshot's first recorded location still wins,
and package-id matches come last for a variant the hoister collapsed.
pnpm 11
reads a project's direct dependencies off that project's own hierarchy,
so this matches it.

Lifecycle scripts ran only in a snapshot's first recorded copy, and the
other projects' copies got the build output through the symlinks. They
now run in every distinct copy, as pnpm 11 builds each node of its
hoisted graph. Only the first copy seeds the side-effects cache.

Since #17397 nests a project's own peer variant instead of collapsing
it, many more workspaces hit this.

Closes pnpm/pnpm#17498

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-30 10:38:12 +01:00
Zoltan Kochan 63be32b832 perf(deps-restorer): stat a store file once for the hardlink mode check (#17502)
The umask check added for pnpm/pnpm#3807 stat'ed the store file before
every hardlink, so a file shared by many packages was stat'ed once per
link. A linkable answer is now remembered per source path for the
process. Only linkable answers are cached: a mismatched entry copies,
and that copy costs more than the stat.

Closes pnpm/pnpm#17500
2026-10-30 10:35:51 +01:00
Aquaticat 0f10a9f61b docs(recursive): say that --no-bail skips the dependents of a failed task (#17484)
The paragraph said all tasks finish with --no-bail.
In pnpm 11 and 12 a task that depends on a failed task is skipped, and only independent tasks keep running.
pnpm 10 still runs the dependent, so the versioned 10.x page on pnpm.io is unchanged.
Reproduction: https://github.com/Aquaticat/pnpm-no-bail-dependents-repro

Written by an agent (Claude Code, claude-sonnet-5-5).
2026-10-30 02:22:47 +01:00
luffy ead5fee8fa fix(lockfile): preserve __proto__ workspace importers when merging (#17467)
Construct the merged importer map with Object.fromEntries so a workspace
directory named __proto__ remains an own property. Plain assignment invokes
the inherited prototype setter and silently drops that importer on write.

Cover either input, both inputs, branch-lockfile reads, and the bundled CLI.
Related to pnpm/tasks#59 (importer item only).
2026-10-30 02:19:33 +01:00
Zoltan Kochan 040043f907 fix: check directory dependencies beneath a registry package (#17493)
An override or a package extension can give a registry package a local
directory dependency. Both lockfile freshness checks walked directory
packages only from each importer's dependency map, descending through
directory packages, so such a directory was never visited. An edit to its
manifest left the lockfile unchanged, and a frozen install accepted it.

Both checks now also walk every directory package that a package other
than a directory depends on, for the importers that reach it. Its peer
scope is the aliases of every package above it plus the checking
importer's own. Peer scopes that differ produce different snapshot keys,
so every path to one key shares the scope that decides its peers. A
`pnpm deploy` entry is skipped as before. pnpm 12 hooks these packages'
manifests with `packageExtensions` and `readPackage`, as the resolver
does, only for the packages the checked importers reach, and follows
`optionalDependencies` edges only when the `optional` setting includes
them.

The lockfile does not record whether the parent declared the directory
with `file:` or `workspace:`, and pnpm 12 can read different manifests
for the two, such as for a workspace project that publishes from a
subdirectory. The snapshot must match both readings, so a stale lockfile
is never accepted. Where the readings differ, a lockfile the resolver
just wrote fails the check until the lockfile records the declaration
(pnpm/tasks#228). pnpm 11 reads one manifest for both.

The repeat-install fast path already yields here: the workspace state
records each lockfile's local directories (pnpm/pnpm#17481).

Closes https://github.com/pnpm/pnpm/issues/17486
2026-10-30 01:21:36 +01:00
4cb926c546 fix(runtime): choose stable Node.js latest releases (#17492)
Resolve the Node.js `latest` selector to the newest stable entry of the
release index. When the index has no stable entry, which is what the
nightly, rc, and other prerelease channel indexes look like, fall back
to the first entry, so `node@nightly` keeps resolving to the newest
nightly build.

The fallback keeps the exported resolveNodeVersion/resolveNodeVersions
contract intact: callers that pass a prerelease mirror URL without a
release channel still get a version for `latest`.

Closes pnpm/pnpm#17490

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 23:35:11 +01:00
Zoltan Kochan 253a95eea9 fix(cmd-shim): run a .cmd bin when MSYS argument conversion is off (#17491)
The sh shim for a `.cmd`/`.bat` target doubled cmd's switch to `//C` under
MSYS, relying on argument conversion to turn it back into `/C`. With
conversion off (`MSYS_NO_PATHCONV=1` in Git for Windows, or
`MSYS2_ARG_CONV_EXCL='*'`), and under Cygwin, cmd.exe received `//C`, read
no switch, and started an interactive prompt.

The shim now passes `/C` as is and, under MSYS/Cygwin, exports
`MSYS2_ARG_CONV_EXCL` with the switch prepended to the caller's own
exclusions. The switch reaches cmd.exe intact in every mode, and the
target arguments keep the caller's conversion setting. Since the arguments
no longer differ between branches, the duplicated MSYS exec block is gone.
The first standalone `/c` or `/k` is upper-cased first, because an exclusion
entry matches as a case-sensitive prefix and `/c` would also exclude drive
paths such as `/c/Users`. Arguments after it are the command cmd.exe runs
and keep their spelling.

A warm reinstall keeps a shim whose target marker matches, so the reuse
check now also rejects a shim that still hands `cmd` a doubled switch.

Closes pnpm/pnpm#17480
2026-10-29 23:14:30 +01:00
39bb7b93c5 fix(install): relink a missing workspace dependency on a repeat install (#17487)
The repeat-install fast path only treated a direct dependency link as
broken when the entry existed but its target was gone. A deleted entry
read as healthy, the recorded workspace state still looked fresh, and
`pnpm install` answered "Already up to date" while a declared
`workspace:` dependency stayed unlinked.

In both CLIs the gate now also stands down when a local link the last
install made is missing. A project's own entry for a `workspace:` or
`link:` spec is the trigger; the wanted lockfile then decides, because
an override or a readPackage hook can drop or rewrite the declared spec
and an injected dependency resolves to a skippable `file:` snapshot.
The entry counts as missing only when:

- no root entry resolves to the target where the linker may have put
  it there instead: under the hoisted linker the same alias, under
  dedupeDirectDeps with a shared lockfile the same alias in v12 and
  any alias the root importer links in v11. The target the manifest names is tried first,
  so a healthy sibling costs no lockfile read;
- the project's lockfile records the alias with a `link:` version: the
  shared wanted lockfile, or the project's own under
  sharedWorkspaceLockfile: false, with the current lockfile standing in
  for a deleted wanted one;
- the linker symlinks direct dependencies (`symlink` is on and the
  linker is isolated or hoisted);
- the project is one the gate holds, so a filtered install is not sent
  down the full path by a project it would not link.

Closes pnpm/pnpm#17485

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Copilot-Session: e2524a8c-40f0-421e-a31e-62880b61ef02
2026-10-29 22:19:04 +01:00
Khải 22290afecb refactor(fs): make the copy permissions one type on every target (#17348)
`CopyPermissions` was a type alias: `std::fs::Permissions` off WASI, and a
bare `u32` on it, because WASI metadata omits the permission bits and the
host supplies the mode instead. Callers therefore wrote the same thing
twice, once per target:

    #[cfg(unix)]
    let permissions = Permissions::from_mode(0o755);
    #[cfg(target_os = "wasi")]
    let permissions = 0o755;

and a trait in `cmd-shim` had a parameter whose type changed with the
target, while two files aliased the one name over the other with
`use pnpm_fs::CopyPermissions as Permissions;`.

It is a newtype now, wrapping what each platform's own calls hand over:
the bits where a platform has them, the read-only attribute on Windows,
which is the whole of `fs::Permissions` there. The vocabulary is
`of_metadata` and `copy_permissions` to read, `from_mode` and `mode` for
the bits, `apply_to_file` and `apply_to_path` to write. The platform
difference lives in the one module, and the callers name one type.

`pack`'s archive writer shows it at its clearest: one arm wrote the mode
back through `fs::Permissions` and another through a free
`set_file_permissions`, and both are now one `from_mode(mode)
.apply_to_file(...)` call.

A WASI imitation of `fs::Permissions` was the alternative. It keeps two
distinct types, so the branching only moves: wherever std's own API wants
the real `fs::Permissions`, an imitation cannot be passed. Nor could it be
made interchangeable, since `PermissionsExt` does not exist on WASI. A
bits-only type was the other alternative, and Windows rules it out: the
read-only attribute is all `fs::Permissions` carries there, so POSIX bits
cannot represent what a copy preserves.

That retires two lint suppressions the divide forced, both of the form
`#[allow(clippy::trivially_copy_pass_by_ref, reason = "Shares the native
permissions signature")]`, which existed only because `u32` is `Copy`
and `Permissions` is not.

`mode` masks the file type off, where `PermissionsExt::mode` reports it.
Every reader of those bits tests or sets the low ones, so the masking
changes no result.

`apply_to_path` follows a final symlink off WASI and refuses one on WASI,
the divergence the two call sites it replaced already had. Its doc had
claimed it followed one everywhere.
2026-10-29 21:49:12 +01:00
Zoltan Kochan d25d8c823c fix(executor): make process lifetime follow one contract (#17470)
Make the lifetime of the processes pnpm starts follow one contract

What a command leaves running now outlives pnpm exactly when it would
outlive the command run without pnpm, as with cargo, `turbo exec`, npm,
`deno task`, just, and moon. The contract, with the tests behind each of
its rows, is in `pnpm/crates/executor/PROCESS_LIFETIME.md`, and
`pnpm/AGENTS.md` makes changes to process handling go through it.

pnpm used to apply a multi-task orchestrator's policy to every command:
on Windows its Job Object ended everything a script had started whenever
the command failed or the user pressed Ctrl+C, including a browser opened
by the `open` package (#17465), after the same class of bug in #14356 and
#16628.

Single commands:
- Every exit pnpm makes on its own goes through `pnpm_executor::exit` or
  `before_exit`, which release the Job Object first. `clippy.toml` bans
  `std::process::exit` elsewhere. A closing console, and an interrupt
  while pnpm runs no command, release it too. An interrupt that a running
  command outlasted every escalation of still ends the tree.
- The Job Object still ends the tree when pnpm is killed or crashes, so a
  cancelled CI job leaves no dev server holding its port.
  `CREATE_BREAKAWAY_FROM_JOB` stays exempt (#16628).

Multi-task runs:
- A task that ends on its own keeps its leftovers, as pnpm 11 and Nx do.
- A bail stops the tasks still running. On Unix they get SIGTERM once,
  and SIGKILL after a two-second grace for whatever is still provably the
  task: a group that still has a member, a child pnpm has not reaped, and
  descendants from a fresh scan. On Linux descendants are pinned through
  pidfds, so a reused pid is never hit. pnpm waits out the grace before
  it exits. The registration is dropped when its child is reaped, and
  kept as the child's group alone while pnpm waits for that group after
  an interrupt.
- On Windows each such task runs in a nested Job Object, which the bail
  terminates. That reaches detached processes whose parent has exited,
  which `taskkill /T` missed, without walking parent links.

Trade-off: a child joins a job it is assigned to by hand right after its
creation, so a process it starts in that moment is not covered. Closing
the window needs `PROC_THREAD_ATTRIBUTE_JOB_LIST`, which stable Rust's
`Command` cannot pass yet. Creating the child suspended instead would
overwrite a caller's creation flags, and resuming it would mean a walk of
every thread on the system for each child.

Tests: a differential matrix covers `exec`, `run`, `dlx`, and lifecycle
scripts after success and failure. The tests also cover Ctrl+C, a closing
terminal, repeated Ctrl+C, pnpm being killed, breakaway, leftovers of
finished and failed tasks in recursive runs, the graceful bail, and the
job-based bail. The Ctrl+C and terminal-close tests are differential on
both platforms, through a pseudo-terminal on Unix and a private console
on Windows.

Closes pnpm/pnpm#17465
2026-10-29 21:33:29 +01:00
Zoltan KochanandClaude Opus 5.5 efb0201348 fix: name the project in the invalid dependency name error (#17488)
The ERR_PNPM_INVALID_DEPENDENCY_NAME error for a direct dependency said
only "The current package". In a workspace with dozens of projects the
user cannot tell which manifest holds the bad key. Both CLIs now name
the project's directory.

Refs pnpm/pnpm#17475

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:24:02 +01:00
Zoltan Kochan bcf44db5bf fix: check local dependencies a readPackage hook adds in the repeat-install check (#17481)
The repeat-install fast path found local tarballs and directories in the
declared project manifests. A pnpmfile `readPackage` hook that added one
to any package was invisible, so a replaced tarball left "Already up to
date" in place. Both CLIs had the gap.

The workspace state now records, per lockfile digest, the local tarballs
and the directories that lockfile resolves
(`validation.localDependencies`), with each tarball's signature in
`validation.files`. The list is a function of the lockfile content, so a
write reuses the entry recorded for an unchanged digest. pnpm v12 takes
it from the lockfile the install holds parsed, and pnpm v11 reads the
lockfile only for a digest with no entry. A refresh carries the entries
of the lockfiles its kept projects still name, with their recorded
signatures, and drops the others. The fast path reads no lockfile: v12
checks each recorded tarball's signature and refuses on a recorded
directory that is not a workspace project, and v11 refuses on any such
entry, as it does for a declared local dependency. A state with no entry
for a lockfile its projects were proven against, such as one written
before this change, refuses the fast path, and the install that follows
records the entry.

Testing it showed a second gap, in the full install of both CLIs. With
`injectWorkspacePackages` and per-project lockfiles, a project reaches a
local tarball through the injected workspace project, and its own
lockfile records it. The check that keeps the lockfile when no local
tarball changed looked at direct dependencies only, so that project's
lockfile kept the old version and integrity. The check now walks every
package the installed importers reach, as the frozen verification does.

`resolveLocalTarballPath` moves from `@pnpm/lockfile.verification` to
`@pnpm/lockfile.utils`, which re-exports it, so the commands layer can
use it without a new dependency.

Closes pnpm/tasks#227
2026-10-29 20:06:04 +01:00
1264fc02cd fix(pacquet): read --env-file files the way node --env-file does (#17421)
--env-file was meant to replace a node --env-file or dotenv-cli wrapper,
but it parsed with dotenvy, which expands $VAR and rejects values
Node.js accepts. The expansion was never a design decision: it came with
the parser. It was also wrong for names containing `_`.

Port Node.js's Dotenv::ParseContent (src/node_dotenv.cc, unchanged from
Node.js 22 through 26) instead. Values are no longer expanded, no content
is rejected, invalid UTF-8 becomes U+FFFD, and a value ends at a NUL
byte, as Node.js does. ERR_PNPM_ENV_FILE_PARSE goes away. The loader
still drops a leading UTF-8 byte-order mark, which Node.js keeps as part
of the first key.

The parser tests compare against util.parseEnv, which runs the same
Node.js parser, on hand-written and generated input.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:47:48 +01:00
Zoltan Kochan a8f6645525 fix(package-manager): install an edit to an injected directory outside the patterns (#17483)
The repeat-install fast path assumed that with a shared lockfile every
injected dependency is a workspace project whose manifest it compares.
An injected directory outside the workspace patterns has no importer,
so an edit to it was never installed, and the next frozen install
failed on the stale lockfile.

The gate now yields for an injected directory no project vouches for,
reading the lockfile only when a project declares a `workspace:` path.
The local tarball scan reports the declaration, a `catalog:` specifier
is read through the catalogs first, and the path test is the local
resolver's `is_filespec`, so `workspace:~` and `workspace:^` are
ranges while the Windows forms count as paths. A `workspace:` path
added by an override or a package extension counts as well.
A path such a setting adds to a registry package's dependency is
outside the gate: https://github.com/pnpm/pnpm/issues/17486.

Closes pnpm/pnpm#17476.
2026-10-29 19:45:08 +01:00
1e4018869a fix(cli): install the pinned pnpm for the host when supportedArchitectures excludes it (#17381)
The engine install passed the project's supportedArchitectures to
run_install, so a project that installs for other platforms skipped the
host's `@pnpm/exe` binary and pnpm fell back to the running version. Both
engine install paths (the shared slot and the private install) now pass
None.

Such an install left the shared slot without the host's platform package,
and the slot path does not depend on supportedArchitectures. A slot that
fails to link now falls through to the install instead of returning the
link error, so the fixed pnpm repairs it.

The broader rebuild proposed in pnpm/tasks#67, a from-scratch engine
config, is left out: an allowlist silently loses every user setting it
does not list, and no other cloned project setting was found to break the
engine install.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:39:18 +01:00
Alessio Attilio cdb74a3743 fix(config): accept TLS flags and code parse errors (#17401) 2026-10-29 18:14:37 +01:00
Zoltan Kochan cf2556e97c test(cli): cover packageExtensions on an injected directory outside the workspace patterns (#17474)
A `workspace:../dir` dependency outside the workspace patterns is a
directory package no project describes. pnpm 12.16.1 skipped the manifest
hooks for it under a shared lockfile, so `packageExtensions` that added a
dependency or a peer to it failed `pnpm install --frozen-lockfile` right
after the install that wrote the lockfile. pnpm/pnpm#17431 removed that
early return. This adds the test for the shape the issue reports, in both
lockfile layouts, and links the issue from that fix's changeset.

Closes pnpm/pnpm#17473
2026-10-29 16:51:34 +01:00
Zoltan KochanandClaude Fable 5.1 f7d7e6173a ci(rust): run dylint with cargo-dylint 6.3.0 (#17478)
rust-lang/rust-clippy renamed its default branch from `master` to
`main`. cargo-dylint 6.0.4 builds its driver by cloning that repository
and checking out `master`, so the Dylint job fails with "`revparse_ext`
failed for `master`" whenever the driver cache misses, which it did on
every Linux run today. dylint 6.3.0 clones `main`.

The pinned perfectionist revision still builds against dylint_linting
6.0.4. cargo-dylint only refuses a library built by a newer version, and
6.3.0 loads this one: with it, a reintroduced derive-order error in
`pnpm-lockfile` is reported as before.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-29 15:30:14 +01:00
Zoltan Kochan e55d6c7c9e fix(package-manager): record declared manifests and hook modified ones in the repeat-install check (#17471)
The workspace state's validation records a digest of each project manifest,
and the next run compares it with the manifest on disk. Since pnpm/pnpm#17030
the install recorded the manifests after `packageExtensions` and the
pnpmfile's `readPackage` hook, so with any hook every project looked modified.
The content check then compared the raw manifest with an importer resolved
from the hooked one and failed whenever the hook rewrote a specifier, and the
full install recorded the hooked digests again.

The fix separates the two views:

- The state records only declared manifests (read from disk, or as a
  mutating command writes them). `DeclaredManifests` carries them to every
  state writer and to the check. Nothing converts a hooked slice into one,
  so a digest can no longer come from the wrong view.
- The content check, which compares a modified manifest with the lockfile,
  now reads it as the resolver does: `packageExtensions`, then the
  pnpmfile's `readPackage` through the install's `hook_one_manifest`, then
  the overrides. Comparing the raw manifest could also report a false "up
  to date", when the raw specifier equals the locked one but the hook
  rewrites it. The pnpmfile loads lazily, so the Node worker starts only
  when a manifest changed. The check is synchronous, so it blocks on the
  hooks with `block_in_place` on the multi-thread runtime, or on a runtime
  of its own outside one, and defers to the install on a current-thread
  runtime. `ManifestHooks::InMemory` carries a caller's in-memory hooks
  (deploy, pnpr lockfiles, the Node-API binding) to the check.

pnpm v11 clones each project manifest before hooking it, so it records the
declared ones. It already runs the hooks in its own content check
(pnpm/pnpm#17148), so it is not affected.

Closes pnpm/pnpm#17468
2026-10-29 13:26:25 +01:00
1aeca4fa11 fix(version): honor gitTagVersion setting (#17461)
pnpm version always created a git commit and tag in v12 because the
gitTagVersion setting was never read. Add Config::git_tag_version
(default true), read from pnpm-workspace.yaml, the global config, and
PNPM_CONFIG_GIT_TAG_VERSION, and skip the commit and tag when it is
false, matching pnpm 11.

pnpm 11 ignores git-tag-version in .npmrc, npm_config_git_tag_version,
and the "pnpm" field of package.json (checked against 11.28.9), so v12
does not read those either.

MacosBackupConfig moves into its own module to keep settings.rs within
the file-length limit.

Closes pnpm/pnpm#17460

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:46:57 +01:00
Zoltan Kochan 710a8311ba fix(gvs): hash a dependency cycle as one unit (#17469)
A global-virtual-store slot is named by the hash of the package's
dependency graph, but the hasher broke a cycle by hashing a node that
re-entered through its own ancestor with its children truncated. A cycle
member reached second therefore hashed its partner by id only. When a
dependency of the partner changed, the partner moved to a new slot while
the member kept its slot and its link to the partner's old slot, so the
project loaded two copies of the partner.

Both hashers now find the strongly connected components of the graph
(Tarjan) and hash each cycle as one unit:

- a node outside any cycle keeps hash({ id, deps });
- each cycle member starts with a label hash({ id, deps }) over its
  children outside the cycle, refined with hash({ color, deps }) over
  its children inside the cycle until a round no longer splits a group;
- a member's digest is hash({ member: label, component: hash(shape) }),
  where shape maps each label to its in-cycle children's labels.

The refinement keeps one package in two peer contexts inside one cycle
on separate slots unless their unrolled trees are identical. The result
no longer depends on walk order, so the Rust warm-up pass that pinned
that order (warm_deps_state_cache) and the comments that justified
lockfile-order walks are removed. A package reached twice on one path
without a graph cycle is no longer truncated either.

Slots of packages in or above a cycle move once.

Closes pnpm/pnpm#17466
2026-10-29 12:46:00 +01:00
Zoltan Kochan 436fbedfcb feat(cli): support runtime, package manager, Cargo, and Python dependencies in outdated and update (#17449)
* feat(cli): show and update runtime and package manager versions in outdated and update

Include runtime (node, deno, bun) and package manager (pnpm, npm, yarn, bun)
declarations from devEngines, engines.runtime, and packageManager in pnpm
outdated and pnpm update (including --interactive and --latest).

Wire NodeResolver, DenoResolver, BunResolver, and YarnResolver into the shared
DefaultResolver chain built by create_configured_registry_resolver so both
outdated and update resolve runtime: and github:yarnpkg/berry-dist specifiers
alongside registry packages.

* fix(cli): address review feedback on engine outdated and update

* fix(cli): refine engine lockfile sync, group deduplication, and outdated lockfile check

* fix(cli): preserve engine outdated candidates with distinct specifiers

* feat(cli): support Cargo and Python dependencies in outdated and update

* fix(cli): preserve ecosystem selectors across recursive update and scope lockfile checks

* fix(cli): require Cargo workspace membership when resolving ancestor workspace root

* fix(cli): use segment-aware Cargo glob matching for workspace membership

* perf(cli): avoid exponential backtracking on repeated ** Cargo workspace globs

* fix(cli): reduce outdated future stack size for Windows clippy

* fix(cli): preserve sibling Cargo workspace dependencies during targeted update

* fix(cli): bound workspace member manifest paths and support subtable dependency edits

* fix(cli): snapshot Cargo manifests directly from staged edit paths

* fix(cli): support dotted Cargo keys and refine ecosystem outdated/update handling

* fix(cli): deduplicate Cargo update edits by physical manifest target

* fix(cli): use inclusive >= for rewritten Cargo > bounds and validate standalone outdated projects

* fix(cli): discover implicit path-dependency Cargo workspace members

* fix(cli): apply recursive filters and unified grouping to standalone ecosystem projects

* fix(cli): populate Cargo dependency graph edges for standalone recursive filters

* fix(cli): resolve aliased Cargo workspace dependencies in standalone graph

* fix(cli): support single-quoted Cargo subtables and inherited implicit member paths
2026-10-29 11:52:37 +01:00
be4aa48567 chore: update dependencies, Node.js, pnpm, and GitHub Actions (#17464)
* chore: update dependencies, Node.js, pnpm, and GitHub Actions

* fix: cap the minimatch override and embed Node.js 26.13.0 in the exe

The minimatch security override mapped 9.x to '>=10.2.4', so the
refresh resolved eslint-plugin-import-x to minimatch 11.0.2, which no
longer exports makeRe and breaks loading the ESLint config. Cap it at
^10.2.4.

The standalone builds still embedded node@26.12.1, which this update
removes as the repo runtime and from the lockfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:51:06 +01:00
Zoltan Kochanandgithub-actions[bot] 867bf8fa01 chore(release): get-pnpm 0.0.11 (#17458)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
get-pnpm@0.0.11
2026-10-29 11:06:55 +01:00
Zoltan KochanandClaude Opus 5.5 e7b88a6a62 fix(release): release get-pnpm without forcing a pnpm release (#17462)
The pnpm v12 wrapper (pacquet) declared get-pnpm as `workspace:*`. The
release engine materializes that as the exact current version, so any
get-pnpm bump invalidated the range and forced a dependent bump of
pacquet (and `@pnpm/napi` through its fixed group). A get-pnpm-only
release PR therefore also released pnpm and consumed every pending
pacquet changeset.

The range constrains nothing users see: the published `pnpm` manifest
drops `dependencies`, and get-pnpm reaches the wrapper only as source
injected by `pnpm deploy`. `workspace:~` materializes as `~0.0.x`,
which accepts get-pnpm's patch releases, so get-pnpm releases on its
own again.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 00:56:37 +01:00
Zoltan Kochan 08cc4f3138 feat: support dependency-type pseudo-selectors and wildcards in overrides (#17457)
Add Dependency Selector Syntax (DSS) pseudo-selectors (`:prod`,
`:optional`, `:peer`) and `*` wildcard patterns to `overrides` in pnpm v12.

- Parse trailing `:prod`, `:optional`, and `:peer` pseudo-selectors on
  target selectors in `pnpm-config-parse-overrides`.
- Support `*` glob patterns on target and parent package names in
  `pnpm-package-manager` override matching, with specificity ordering:
  exact name > wildcard pattern, typed selector > untyped selector, and
  narrower version range > broader range.
- When an override removes an entry from `optionalDependencies` (`"-"`),
  also remove any duplicate declaration of that package from `dependencies`
  on the same manifest (matching `ignoredOptionalDependencies` behavior).
- Stop copying non-peer-range overrides from `peerDependencies` into
  `dependencies`. Replacing a peer dependency with a direct dependency
  is expressed by combining `packageExtensions` with
  `"<parent>><peer>:peer": "-"` in `overrides`.
- Document the new selector syntax and mark `ignoredOptionalDependencies`
  and `peerDependencyRules` as deprecated in v12 docs while preserving
  backward compatibility.

Closes pnpm/pnpm#5391
2026-10-28 22:11:28 +01:00
Zoltan Kochan 4a263ca48f docs: add Cloudflare and Syntax as sponsors (#17452) 2026-10-28 15:43:49 +01:00
491b003976 fix: create project files with the umask mode instead of 0600 (#17283)
`tempfile` creates every temp file 0600, and `pnpm_fs::write_atomic`
kept that for new files, so any writer that didn't think about modes
left project files owner-only: `pnpm-workspace.yaml` (an edit also
tightened an existing file), `pnpm deploy` output, patch files, the patch
state, the lockfile `dedupe --check` restores, `.cargo/config.toml`, and
every new file written through `write_atomic`. A Docker image built as
root and run as another user could not read them. pnpm 11 follows the
umask.

- `write_atomic` gives a new file 0666 less the umask; an existing file
  keeps its mode, and its replacement is staged at that mode so the
  contents never become readable by more users.
- `write_atomic_private_if_new` keeps 0600-when-new for files that may
  hold a credential: `.npmrc`/`auth.ini` from `pnpm config set`, and
  caches of URLs that can carry userinfo (tarball HTTP, Python index and
  wheel, Cargo sparse registry, pnpr upstream indexes).
- `write_atomic_private` is unchanged.
- The hand-rolled private-temp-file writers in dedupe, patch-commit,
  patch state and deploy call `write_atomic`; package-manifest and pack
  stage at the existing mode through `named_tempfile_with_mode_in`.
- clippy `disallowed-methods` rejects `NamedTempFile::new_in` and
  `Builder::tempfile_in` outside the `pnpm_fs` helpers, so new writers
  must choose a mode.
- A CLI test runs install, patch and patch-commit under umask 022 and
  fails on any owner-only file in the project.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 14:52:46 +01:00
Zoltan KochanandClaude Opus 5.5 aedf221073 fix(deps-restorer): write links from the real path of a symlinked node_modules (#17451)
An install run from a project whose `node_modules` is a symlink (for
example to another project's `node_modules`) computed the relative
targets of direct-dependency and hoisted links from the symlink's
lexical location. The OS resolves them from the real directory, so with
the global virtual store every recreated link dangled while the install
still exited 0.

Direct-dependency links and both hoist directories are now written from
the real path whenever a symlink lies between the project (or lockfile)
directory and the modules dir, matching the `realpathMissing` of the
modules dir in pnpm v11, which does not have this bug. Ordinary layouts
keep their lexical link contents.

Closes https://github.com/pnpm/pnpm/issues/17448

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 14:49:49 +01:00
Zoltan Kochan 0591f7c8cf test: cover a readPackage peer on an injected project in a filtered frozen install (#17450)
pnpm/pnpm#17447 reports `pnpm install --frozen-lockfile --filter app`
rejecting a fresh lockfile when a pnpmfile `readPackage` hook adds a
peer to an injected workspace package. A filtered install passes only
the selected projects to the lockfile check, so the injected project is
read from its directory and must be hooked the way the resolver hooked
it. pnpm/pnpm#17431 made the check apply the resolver's hook chain to
those manifests, which fixes this case too; 12.16.1 still fails and
pnpm 11.28.9 passes.

The test fails with the issue's "local dependency is outdated" error
when the check skips the hooks for such a directory. The changeset of
pnpm/pnpm#17431 now names this case and links the issue.

Closes pnpm/pnpm#17447
2026-10-28 14:48:45 +01:00
621427757f fix(update): refresh the subtrees of packages selected with --latest (#17446)
With package selectors, `update --latest` now re-resolves the locked
subtree of every matched direct dependency of the updated project, so an
unchanged parent no longer keeps an outdated compatible child pinned.
Unmatched direct dependencies, the packages excluded aliases resolve to,
their subtrees, and peers keep their pins, matching pnpm v11.

The lockfile reachability walk used for pruning takes a root and an
edge filter so both callers share it.

Closes pnpm/pnpm#17444

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 14:47:58 +01:00
Zoltan Kochan c0f945bf60 feat(get-pnpm): install without touching PATH with --no-modify-path (#17349)
`npx get-pnpm --no-modify-path`, or PNPM_NO_MODIFY_PATH set to anything
but "", "0", or "false", runs `pnpm setup --no-modify-path`
(pnpm/pnpm#17317). pnpm is installed into PNPM_HOME, the shell config
and the Windows registry are left alone, and setup prints the lines to
add. The uninstall hint then names only PNPM_HOME.

The option first shipped in pnpm 12.16.0. For an older version, the
install stops before setup changes anything.

From pnpm 11, a project `setup` script replaces the built-in `pnpm
setup`, so get-pnpm now runs `pnpm pm setup` on the versions that have
the prefix (pnpm 11, and 12.1.0 on).

This is get-pnpm's half of pnpm/get.pnpm.io#80, moved here with the
package; install.sh and install.ps1 keep theirs in that PR. The
argument parsing moves from runCli into parseCliArgs, which the unit
tests cover.
2026-10-28 10:56:57 +01:00
Zoltan Kochanandgithub-actions[bot] 5f72b48a7e chore: update dependencies, Node.js, pnpm, and GitHub Actions (#17445)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-28 10:08:02 +01:00
Zoltan Kochan 63c0b8eede chore: patch deepmerge@4.3.1 and ignore GHSA-gcqx-w48m-w2h4 (#17443)
deepmerge has no patched release upstream for GHSA-gcqx-w48m-w2h4. Patch
deepmerge@4.3.1 to reject __proto__ keys by name in mergeObject() across
index.js, dist/cjs.js, and dist/umd.js (TehShrike/deepmerge#274), and
ignore GHSA-gcqx-w48m-w2h4 in auditConfig.

Also use recentTimestamp() across checkDepsStatus.test.ts so mocked
sub-second mtimes never land on an exact second when Date.now() % 1000 === 0.
2026-10-28 01:41:11 +01:00
Zoltan Kochan 5fb5fba3a2 feat(pnpr): store only the zstd tar for hosted tar registries (#17437)
A hosted registry that declares `tarballs: tar` is only ever served
through tar registries, so the published `.tgz` has no reader there.
Publish now verifies the `.tgz`, records its tar hash as before, and
stores the tar compressed with zstd at level 19, within the window pnpm
decodes, as `<name>-<version>.tar.zst` in place of the `.tgz`. A `.tgz`
that does not decode in full fails the publish with a 400.

The version route serves the stored bytes to a zstd client and the tar
decoded to any other, which the compression layer gzips when asked. These
versions skip the per-replica level-19 variant cache.

Config loading rejects a router without `tarballs: tar` that lists such a
registry, since it would have no `.tgz` to serve. Unpublish and packument
rewrites remove the stored tar, `pnpr verify` counts it as the version's
blob instead of an orphan, and `pnpr export` writes it.

Decided in pnpm/tasks#226.
2026-10-28 01:40:51 +01:00
Zoltan Kochan 64588da2d9 fix(workspace-state): keep the state file readable to other users (#17438)
The workspace state file was written through `private_named_tempfile_in`,
so `node_modules/.pnpm-workspace-state-v1.json` ended up 0600 while pnpm 11
left it 0644 (`fs.writeFile`). Another user could not read it, so the
deps-status check before `pnpm run` treated `node_modules` as stale and
reinstalled. A Docker image that installs as root and runs as a non-root
user hits this on every start.

Add `pnpm_fs::umask_named_tempfile_in`, which creates the temp file with
0666 narrowed by the umask, and use it for the state file. The write
stays without an fsync, as before.
2026-10-28 01:31:28 +01:00
38e9755f66 fix(add): save resolved npm alias ranges and pin dist-tags in catalogs (#17404)
An npm alias added with `pnpm add` now saves the range of the version
it resolved to, matching v11's calcSpecifier: `v2@npm:foo` records
`npm:foo@^1.0.0` and `v2@npm:foo@^100.0.0` records `npm:foo@^100.1.0`.
A re-add of an alias without a version keeps the existing range.

A dist-tag other than `latest` that `pnpm add` writes into a catalog
entry records the exact version the tag points to, in every catalog mode,
as v11's getCatalogUpdatesOfProject does. The manifest specifier of an
uncataloged dependency keeps its range. A dependency added without a
version counts as naming the configured `tag` setting, as v11's
parseWantedDependencies substitutes it. `latest` and aliased tags are not
pinned, matching v11's isExplicitDistTagSpecifier.

Related to pnpm/tasks#191.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:52:00 +01:00
Zoltan Kochan b0e25ede59 fix: read a file: dependency's manifest with the resolver's hooks in lockfile checks (#17431)
The resolver runs every resolved manifest through one chain: package
extensions and the compatibility database, the pnpmfile's `readPackage`
with the resolution's directory, then overrides and the
`ignoredOptionalDependencies` removal. The lockfile freshness check read
a directory package that no workspace project describes, such as a
`file:` dependency outside the workspace or anything nested below one,
with only the overrides applied. So a `readPackage` hook that changed
such a package's dependencies failed `pnpm install --frozen-lockfile`
right after the install that wrote the lockfile, and an
`ignoredOptionalDependencies` entry for such a package's optional
dependency made every repeat install resolve again. pnpm 11 handles
both.

`ManifestTransformHooks::apply` is now the chain, and the resolver's
walk calls it. The check builds the same hooks from the config and,
before its synchronous walk, follows the same directory edges level by
level, hooking each manifest it will compare under the declaration that
reaches it. This replaces
`hooked_injected_manifests`, which covered only the direct injected
dependencies of a project with its own lockfile, and only
`packageExtensions` and `readPackage`.

Part of pnpm/tasks#220 (item 1, the manifest a checker reads).
2026-10-27 17:42:34 +01:00
Zoltan Kochanandgithub-actions[bot] 7535582cd7 chore: update dependencies, Node.js, pnpm, and GitHub Actions (#17436)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-27 17:00:36 +01:00
c7b46aedef chore(release): pacquet 12.16.1 (#17433)
* chore(release): pacquet 12.16.1

* docs(release): curate the pacquet 12.16.1 release notes

Add a lead line, order the entries by impact (hang, packed files,
broken links, then behavior and output fixes), and restate the
peer-dependency and injected-symlink entries by what users see.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(release): mark the old pnpm unpublish suggestion as past behavior

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
v12.16.1
2026-10-27 14:32:53 +01:00
Zoltan KochanandClaude Opus 5.5 44d5fbcfe0 test(install): assert a renamed workspace command's old name is gone (#17432)
The oracle's renamed-bin scenario checked only that the dependent links
the new name. Both CLIs now also remove the old one, pnpm v11 since
pnpm/pnpm#17371, so the scenario asserts that too, for every layout and
driver.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 14:02:34 +01:00
Zoltan Kochan 0619870c34 fix(packlist): apply files entries that name a file after globs (#17429)
The parity proptest from pnpm/pnpm#17415 found
`files: ["**", "!dist/index.js", "dist/**"]` packing dist/index.js, which
npm-packlist and `@pnpm/fs.packlist` exclude. npm-packlist keeps entries
that name an existing file in its strict rules, applied after the rules
directories and globs produce, so a later `dist/**` cannot re-include a
file an exclusion names, and a named file ships past a later glob
exclusion. The Rust port kept every entry in one ordered list. The pack
dotenv warning now counts a literal entry naming the path as listing it.

File entries now go into StrictRules, appended after the ordinary rules.
Strict exclusions come after strict inclusions, so an exclusion naming a
file always wins over an entry naming it. npm-packlist lets the earlier of
the two win, which packs `lib/secret.js` for
`["lib", "lib/secret.js", "!lib/secret.js"]`. v12 keeps excluding it,
recorded as DeliberateDivergence::FilesFieldExclusionNamingFile with a
witness.

The failing seed is saved in proptest-regressions so every run replays it.
2026-10-27 14:00:45 +01:00
samiur shahin 4201601a4a fix: accept --cache-dir as a CLI setting (#17428)
Add `cache-dir` to `BARE_SETTING_FLAGS` as a text setting, so
`--cache-dir <dir>` and `--cache-dir=<dir>` are pulled out of argv before
clap sees them and recorded in the CLI layer's setting values, the same path
`--config.cache-dir=<dir>` takes. Config loading then sets `cacheDir` and
records it as an explicit setting.

No command declares its own `--cache-dir` option and no global option uses
the name, so claiming it on every command line is safe.

Fixes pnpm/pnpm#17422
2026-10-27 12:39:36 +01:00
Zoltan Kochan 3f4e9fcf13 fix: unify pinned-pnpm switch and record decision across entry points (#17427)
Closes pnpm/tasks#222.

Unify the pinned-pnpm switch and pin-recording decision across all CLI entry
points (commands, `--version`, unknown flags, `self-update`, and spawned
installs) in both pnpm v12 (`pnpm/`) and pnpm v11 (`pnpm11/`):

- Replace `VersionArgv`, `UnparsedArgv`, `PinFlags`, and `SwitchInput` in v12
  with a single `Invocation::from_argv(argv, entry_point)` model.
- Extract `apply_cli_layers(config, invocation, config_overrides, dir)` so
  pre-command loads and the command's full config load apply `--config.<key>`
  overrides and CLI flags at the same `lockfileDir`, forwarding CLI config
  into the switched pnpm, `verifyDepsBeforeRun` installs, pin recording, and
  bootstrap downloads.
- Introduce a pure `decide(pin, settings, invocation)` (`decidePinAction` in
  v11) function returning `Switch | Record(at) | Nothing` used by all entry
  points including `self-update`.
- Load the full config at most once and reuse the config loaded during pin
  recording for the subsequent command execution.
- Add the shared `fixtures/pin-decision.json` test matrix and integration
  tests (`pin_decision_matrix`, `cli_config_reaches_every_child`, and
  `pin_recorded_only_from_full_config_at_lockfile_dir`) in both stacks.
2026-10-27 12:28:54 +01:00
9039e2b614 fix(importer): fall back to copy when overlayfs refuses to move a package aside (#17426)
overwriteDirSync moves an existing package directory aside before
renaming the staged one in. On overlayfs, renaming a directory from a
lower layer fails with EXDEV, which happens when node_modules is copied
in from an earlier Docker build stage and pnpm prune re-imports a
package. Treat EXDEV like EPERM/EACCES/EBUSY: copy a backup aside and
remove the target in place. If that removal fails partway, copy the
missing entries back from the backup.

Closes pnpm/pnpm#17424

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:45:35 +01:00