126 Commits
Author SHA1 Message Date
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
6b5b8ac53a chore(release): 11.28.9, pacquet 12.16.0, pnpr 0.1.0-alpha.17, get-pnpm 0.0.10 (#17406)
* chore(release): 11.28.9, pacquet 12.16.0, pnpr 0.1.0-alpha.17, get-pnpm 0.0.10

* docs(release): curate the 11.28.9, 12.16.0, and pnpr alpha.17 release notes

- Add a lead line to each page and group the patch entries by what the
  reader was doing, with security fixes first.
- Merge the duplicate strictSsl/localAddress entries and the repeat-install
  setting entries into one list per page.
- pnpr: split the removals into commands, flag aliases, and camelCase
  config keys, and say that pnpr no longer serves PUT /-/pnpm/v1/publish,
  so batch publishing needs pnpm 11.28.9 or 12.16.0.
- Drop the "Updated dependencies: get-pnpm" line from the pnpm v12 page
  and list the get-pnpm credential fix first.

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-27 00:08:12 +01:00
Zoltan Kochan 740a49c50f fix(install): record the settings the repeat-install fast path missed (#17391)
The Config classification test from pnpm/pnpm#17383 listed settings a
full install acts on and the fast path never compared. These now go into
the workspace state, and the fast path and the run check compare them:

- `virtualStoreDirMaxLength`: a full install renames the virtual store
  slots.
- `dangerouslyAllowAllBuilds`: turned on, it allows the builds the last
  install ignored.
- `trustLockfile`: turned off, the lockfile no install verified has to be
  verified.
- `globalVirtualStoreDir` (v12 only): a full install links from the new
  directory.

The first three exist in both CLIs and are fixed in both. An unrecorded
value reads as the default, so a state written before this change keeps
the fast path for anyone on the defaults, and each CLI reads the other's
state without seeing a change. v11 writes `virtualStoreDirMaxLength` and
`dangerouslyAllowAllBuilds` with every state, so v12 writes them too.
`trustLockfile` and `globalVirtualStoreDir` are written only while they
differ from the default.

The run check ignores `virtualStoreDirMaxLength` and
`globalVirtualStoreDir`, as it ignores `virtualStoreDir`: a script runs the
same whatever the virtual store's slots are named or where they live.

Part of pnpm/tasks#219.
2026-10-26 21:16:25 +01:00
6761624450 fix(lockfile): key packages by the registry they resolve from (#17259)
A package resolved through a <prefix>: specifier was keyed
<name>@<prefix>:<version>, while the same package reached through a
scope route or the default registry kept <name>@<version>. One tarball
from one registry was then installed twice. A package is now keyed by
the registry it resolved from, in both pnpm v11 and pnpm v12.

Keying:
- A registry with a declared prefix (registries.<url>.prefix or
  namedRegistries) keys every package resolved from it with that
  prefix, however the dependency was written. Built-in aliases qualify
  only the specifiers that spell them, so routing a scope to
  registry.npmjs.org does not re-key the lockfile as npmjs:.
- Several declared prefixes for one URL key its packages by the
  lexicographically smallest. Registry URLs are compared with scheme
  and host case-insensitive and the path ending in a slash.
- pnpr requests declare the smallest prefix per URL, so the server
  mints the client's keys.

Migration:
- A packages: key without the prefix its registry declares, or with a
  non-canonical one, marks the lockfile stale for a resolving install.
  The reuse gate refuses such keys and the re-resolved edges mint the
  current key at the locked version. A re-keyed parent keeps its locked
  child references.
- Frozen installs keep the recorded keys. Their local-directory check
  reads children through the configured registries.
- The workspace state records the declared prefixes and the routes that
  reach them only after an install verified the lockfile's keys, so
  the repeat-install shortcut cannot skip the migration. A changed
  lockfile is checked again. The pre-run check of pnpm run / pnpm exec
  ignores the record: a script runs the same either way, and a frozen
  install would otherwise reinstall before every script.

Resolver:
- Locked-version pins, the stale-pin refresh, the locked-optional
  guard, stale peer pins, the cross-importer reuse check, and the
  revision-refresh pin read registry-qualified versions the way they
  read plain ones.

pnpr requests still carry one prefix per URL, so a second alias of a
registry does not resolve on the server (pnpm/tasks#204).

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 10:05:07 +01:00
Zoltan Kochan f2b319c6fb fix(install): refuse a modules dir linked from another store on add (#17264)
pnpm v12 let `pnpm add`, `pnpm remove`, and `pnpm update` silently purge
and reinstall `node_modules` when `.modules.yaml` recorded a different
store, virtual store directory, or layout version. Its port of
`validateModules` carried only the hoist-pattern and
`virtualStoreDirMaxLength` checks, and `pnpm update` ran as a plain
install, so it skipped even those.

pnpm v11 (`validateModules` and `checkCompatibility` in
pnpm11/installing/deps-installer/src/install/) fails every mutation
other than a plain install with ERR_PNPM_MODULES_BREAKING_CHANGE,
ERR_PNPM_UNEXPECTED_STORE, or ERR_PNPM_UNEXPECTED_VIRTUAL_STORE, and only
`pnpm install` takes the purge path. `installsOnly` is false for an
install with `update` set, so `pnpm update` errors too.

`check_modules_settings_diff` now runs the `checkCompatibility` checks
after the `*_DIFF` ones, in v11's order. Paths compare lexically,
case-insensitively on Windows, like `path.relative`, and the
neighbouring `v3` store is accepted as v11 does. The hoist-pattern
checks skip a `virtualStoreOnly` manifest, as v11 does. The error text
and help match v11's default reporter. `pnpm update` now passes
`installs_only: false`. A mutation other than a plain install never
purges. It also fails on a `nodeLinker` change
(ERR_PNPM_NODE_LINKER_DIFF) and on an unreadable `.modules.yaml`
(ERR_PNPM_MODULES_BREAKING_CHANGE). v11 keeps the tree there and records
the new linker, which leaves the old layout's packages behind for good
(pnpm/tasks#203). Purging would drop links a filtered mutation does not
recreate. The layout-drift check compares the store paths the way the
errors do.

The errors tell the user to run `pnpm install`, but in both CLIs the
optimistic repeat install reported "Already up to date" because the
workspace state recorded neither `storeDir` nor `virtualStoreDir`. Both
stacks now record the configured values and compare them like the other
settings, so that install purges and recreates `node_modules`. The fast
path reads no extra file.

Each configured setting is recorded as the directory the installer
resolves it to, with the store version appended to `storeDir`, so a
relative and an absolute spelling compare equal. In v11 `storeDir`
resolves against the workspace directory, else the project, as the store
controller does, and `virtualStoreDir` against the lockfile directory. An
unconfigured setting stays unset. Under a global virtual store, v12
records the configured `virtualStoreDir`, not the global store. `pnpm run` and `pnpm exec` verification
ignore both settings and carry the recorded ones forward, as they do for
the dependency groups, which also keeps a deployed project passing it.
v11's store controller writes the resolved store into the options it is
given, so `installDeps` passes it a copy, and the recursive install
snapshots the value before its context does the same.

The layout version is not recorded: it has been 5 since before the
workspace state existed, so only a hand-edited `.modules.yaml` can
disagree with it. The confirmModulesPurge prompt stays v11-only.

The modules_dir_compatibility e2e module covers add, remove, and update
against a changed `storeDir`, add against a changed `virtualStoreDir`
and layout version, and the plain `pnpm install` that then recreates the
tree. installing/commands/test/storeLocationChange.ts covers the same
flow in v11, that an unchanged repeat install stays on the fast path,
and that single-project and recursive installs, and a project with its
own lockfile inside a workspace, record the configured `storeDir`.

Part of pnpm/tasks#191.
2026-10-24 19:58:39 +02:00
Zoltan Kochanandgithub-actions[bot] 91774e72a0 chore(release): 11.28.7, pacquet 12.15.0, pnpr 0.1.0-alpha.16 (#17187)
* chore(release): 11.28.7, pacquet 12.15.0, pnpr 0.1.0-alpha.16

* docs(release): curate 11.28.7, pacquet 12.15.0, and pnpr 0.1.0-alpha.16 release notes

Add lead paragraphs and group the patch entries by what the reader was
doing, with security fixes first on each page.

- v11: lead with the registry metadata name check, pinned pnpm
  trustPolicy check, maxTarballUnpackedSize, and the removal of registry
  revisions; merge overlapping entries for minimumReleaseAge metadata
  revalidation, frozen-lockfile directory dependency checks,
  syncInjectedDepsAfterScripts, verifyDepsBeforeRun, pnpm sbom, and
  unreadable config settings.
- v12: order minor highlights from config includes and
  lockfile.integrityTarget down to interactive update styling; drop the
  pnpr-only distribution note from the CLI changelog; merge overlapping
  entries for workspace and pnpr install speedups, minimumReleaseAge and
  If-Modified-Since revalidation, frozen-lockfile and injected package
  checks, verifyDepsBeforeRun, pnpm prune, pnpm pack, pnpm licenses
  list, pnpm self-update, pinned pnpm flag forwarding, and WebContainer
  fixes.
- pnpr: lead the minor section with the breaking ecosystem URL prefix,
  hosted storage layout, artifacts.orgs, and strict config schema
  changes; fold dist.pnpr tarball metadata into one entry and drop the
  redundant /metrics note; lead the patch section with reserved-username
  and auth security fixes.
- Use one issue-link label style across the release pages.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-21 12:15:12 +02:00
Zoltan Kochan 1af222ed19 refactor: unify up-to-date checks (#17181)
Decide whether `node_modules` is up to date in one place for both `pnpm install` (`optimisticRepeatInstall`) and `verifyDepsBeforeRun`.

- In Rust (`pnpm-package-manager`), combine the static workspace-state and filesystem checks with the content freshness checks into `freshness::check_freshness`, parameterized only by `GatePolicy`, so `check_optimistic_repeat_install_with_state` and `check_deps_status_before_run` call the same decider.
- Share `gvs_build_markers_complete` and `check_strict_dep_builds_for_fast_path` between `install_already_up_to_date` and `repeat_install_verdict`, and share `recorded_workspace_state` with repeat-install test fixtures.
- Fix two Rust gate divergences uncovered by unifying `check_freshness`: `Gate::Install` now rejects drifted `configDependencies`, and `Gate::Run` now rejects a missing branch lockfile when `useGitBranchLockfile` is enabled instead of letting `node_modules/.pnpm/lock.yaml` stand in for it.
- In TypeScript (`@pnpm/deps.status` and `@pnpm/exec.commands`), check `catalogResolutionsAreUpToDate` in `assertWantedLockfileUpToDate`, avoid mutating `config.ignoredOptionalDependencies` in place, share `recordValidatedWorkspaceState` between root-project and workspace checks, respect `opts.lockfileDir` in `checkRootProjectDepsStatus`, and warn when a `verifyDepsBeforeRun` install leaves `node_modules` out of date.

Closes https://github.com/pnpm/tasks/issues/142

Generated with Antigravity (Claude Opus 4.7)
2026-10-21 07:16:10 +02:00
Zoltan Kochan 1fdda1ec2f fix: hold link: workspace dependencies under dedicated lockfiles with injection (#17177)
Under `sharedWorkspaceLockfile: false` and `injectWorkspacePackages: true`, an
injected workspace dependency is copied into the dependent project and covered
by that project's own install, whereas a dependency declared with the `link:`
protocol remains a symlink whose target project still needs its own install.

- In `pnpm-cli` (`projects_to_verify`) and `pnpm-package-manager`
  (`WorkspaceProjectIndex::dep_targets`), keep walking `link:` edges when
  `!config.shares_one_lockfile() && config.inject_workspace_packages` instead
  of dropping all workspace dependency edges.
- In `@pnpm/workspace.projects-graph`, rewrite `link:` prefixes to `file:`
  before passing specs to `@pnpm/npm-package-arg`, which otherwise throws
  `EUNSUPPORTEDPROTOCOL` and drops `link:` workspace edges from the project
  graph used by `--filter` and `selectProjectsToVerify`.

Closes https://github.com/pnpm/tasks/issues/179
2026-10-21 07:15:08 +02:00
Zoltan Kochan 7164fc7f27 fix: finish repeat-install and verifyDepsBeforeRun unification across v11 and v12 (#17150)
Unify repeat-install freshness and `verifyDepsBeforeRun` validation across
pnpm v11 and v12 (Closes pnpm/tasks#161, pnpm/tasks#157, pnpm/tasks#148):

1. Centralize v11 workspace state updates in `@pnpm/workspace.state`
   (`preserveCarriedSettings`) and `@pnpm/installing.commands`
   (`workspaceStateUpdate.ts`), preserving carried `dev`/`optional`/`production`
   settings when `ignoredWorkspaceStateSettings` ignores them and preserving
   v12-recorded `autoDedupe`. A filtered install that prunes direct
   dependencies no longer records the workspace root as installed.
2. Hold `{.}...` workspace dependencies for non-recursive `pnpm run`,
   `pnpm exec`, and `pnpm restart` inside a workspace project in v11
   (`selectProjectsToVerify.ts`).
3. Align v11 recorded validation (`checkRecordedWorkspace`) and legacy
   unrecorded validation (`checkModifiedSinceLastValidation`) in
   `@pnpm/deps.status` with v12's `check_freshness`, including empty-project
   handling, `dedupeDirectDeps`, `dedupePeerDependents`, `autoInstallPeers`,
   `excludeLinksFromLockfile`, catalog/override config comparison, and
   `opts.lockfileDir` resolution.
4. Forward effective CLI flags (`--lockfile-dir`, `--modules-dir`,
   `--virtual-store-dir`, `--store-dir`, `--dir`, `--workspace-root`, `--prod`,
   `--dev`, `--no-optional`, `--filter`, `--filter-prod`, `--test-pattern`,
   `--changed-files-ignore-pattern`, and `--config.*`) when
   `verifyDepsBeforeRun: install` spawns `pnpm install` in both v11 and v12.
5. Align `GatePolicy::dedupe_peers` and empty-project predicates in v12
   `pnpm-package-manager`, and run `repeat_install_oracle.rs` across v12, v11,
   and mixed v11/v12 writers in CI, with a longer nextest period for the
   peer convergence harness that shares the job.
6. Parse `--config.<key>=<value>` with the type the command declares for
   `<key>` in v11, so the list and number options forwarded to the spawned
   install arrive as lists and numbers; forward `--npmrc-auth-file` in v11
   and `--test-pattern` / `--changed-files-ignore-pattern` in v12, so both
   forward the same set; and skip the current-lockfile read for a project
   whose manifest changed but whose lockfile did not, as v12 does.
7. Resolve relative v11 store overrides against the workspace or project
   root before forwarding them to the spawned install. Remove the isolated
   linker's duplicate package merge so filtered installs retain unselected
   dependencies and drop obsolete selected dependencies from the current
   lockfile. Cover both cases with CLI regressions. The restore API also
   handles a selected project absent from the previous current lockfile.
8. Record workspace validation from the projects the installers actually
   processed. Ignored projects receive no new proof, and empty or entirely
   ignored selections leave state untouched. Cover both lockfile modes and
   an ignored workspace root.
2026-10-20 20:24:27 +02:00
Zoltan Kochan ebbb1b3e1a fix: sync injected copies after listed scripts without a shared lockfile or with a publish directory (#17083)
With sharedWorkspaceLockfile: false, each project records the copies
injected into it in its own node_modules/.modules.yaml, relative to the
project. The sync after a script listed in syncInjectedDepsAfterScripts
read only the workspace root's .modules.yaml, so it found no copies and
they kept the files from the last install. The edit watcher that
publishes while such a script runs had the same gap. Both CLIs had it.

The sync now reads every project's .modules.yaml as well when the
lockfile is not shared, and resolves each listed copy against the
directory of the manifest that lists it. It finds the projects with the
workspace's packages patterns and each project's own modules directory,
as the install does, and syncs a source's bins once across every
project that holds a copy. The sync after lifecycle
scripts during an install keeps reading only the root's: with a lockfile
per project, the install syncs those copies once every project is
installed.

v11's sync after a listed script also looked up the copies of a package
that publishes from publishConfig.directory under the project root,
while the install lists them under the publish directory. It now looks
under both, as v12 does, and skips a publish directory that has not been
built yet.

The new copy-equivalence tests check, in both CLIs, that every writer of
an injected copy leaves the files the install selects: the packlist, or
every file under deployAllFiles. The writers are the install with the
refresh after prepare, the sync after a listed script, the same after a
source edit, and a frozen install into a clean tree, across shared and
per-project lockfiles, both node linkers, and deployAllFiles. Without
the fix, the four per-project-lockfile cases fail in each CLI.

Part of pnpm/tasks#145.
2026-10-18 13:40:18 +02:00
Zoltan Kochan a7fb3f069d fix: sync only the package files into injected copies after scripts (#17033)
An injected copy is built by the install from the package files only
(the files field and .npmignore) unless deployAllFiles is set. Every
refresh that runs after a project's own scripts read the source with
every file instead, so the copy gained files the install had left out:
- v12: the sync after lifecycle scripts during install, after `pnpm run`
  and task-pipeline scripts named in syncInjectedDepsAfterScripts, and
  the edit watcher that publishes while such a script runs.
- v11: the same `pnpm run` sync and watcher, and the re-import after
  lifecycle scripts during install.

pnpm/pnpm#16684 fixed the same thing for the dedicated-lockfile sync.
These paths now read the source with includeOnlyPackageFiles set to
!deployAllFiles too, so every writer of an injected copy selects the
same files (pnpm/tasks#145, property B).

The packlist lists a symlinked directory as one entry, where the walk
of every file lists the files under it. The syncers expand such an
entry, so a symlinked directory still reaches the copy as its files.

v12 groups the bin-linking settings of SyncInjectedDeps into
BinLinkSettings to stay under the struct field limit.
2026-10-17 14:48:24 +02:00
Max Schwenk dd95acd592 fix(gvs): keep the importer's parentURL on the resolve hook context (#16987)
Node.js keeps one context object for the whole resolve hook chain and
merges whatever a hook passes to nextResolve into it, both for
module.registerHooks() and for module.register(). The NODE_PATH fallback
retries a bare specifier with a synthetic parentURL inside each NODE_PATH
entry. When every retry failed and the hook rethrew the original error, a
hook registered after it, which runs first, read the synthetic parentURL
from its context. A tsconfig paths resolver then looked for the alias
target under the project's node_modules and failed with
ERR_MODULE_NOT_FOUND.

The hook now saves the importer's parentURL before the retries and puts
it back on the context when they finish, whether a retry resolves or not.
The retries still pass { ...context, parentURL } to nextResolve, so the
caller's export conditions are kept. The TypeScript and Rust CLIs embed
the same hook source, and the shared golden flag is regenerated.

The new tests run the scenario from the issue in both stacks: a hook
loaded with --import after the NODE_OPTIONS flag resolves an `@/` alias
from context.parentURL. The test hook falls back to module.register()
where module.registerHooks() is missing, so the Node.js 22.13 CI job
covers the async loader variant as well.

Closes pnpm/pnpm#16979
2026-10-16 21:01:37 +02:00
Zain QureshiandClaude Opus 5.5 7162c6f5ec docs: fix stale usage examples in six package READMEs (#16959)
* docs: fix stale usage examples in six package READMEs

The usage examples imported default exports these packages no longer
have. They now import the named exports, call the current function
names, and pass the options the current signatures accept.

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

* docs: import path in the lifecycle example, drop @pnpm/logger from the changeset

@pnpm/logger is in versioning.ignore, so a changeset entry for it fails
pnpm change check. The lifecycle example used path without importing it
and require in an ES module.

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

* docs: single backslashes in the store path output, plain changeset wording

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:19:38 +02:00
fe4a0c2323 chore(release): 11.28.6, pacquet 12.14.0 (#16952)
* chore(release): 11.28.6, pacquet 12.14.0

* docs(release): curate 11.28.6 and pacquet 12.14.0 release notes

Add lead paragraphs and group the patch entries by what the reader was
doing, with security first on the v11 page.

- v11: lead with the NPM_ID_TOKEN audience check and the invalid file
  name warning as security fixes; say the `pnpm setup` message was
  printed, since v11 reports it as a warning.
- v12: merge the stdout and stderr broken-pipe entries and the two
  optional-peer entries; say the Windows Rust toolchain change undoes
  the MinGW fallback a MinGW-built pnpm used since 12.13.0; correct the
  `browser` packing claim; drop the reference to another release line.
- Use one issue-link label style on both pages.

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

* docs(release): smooth two sentences in the 11.28.6 and 12.14.0 notes

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

* docs(release): say sideEffectsCache.exclude covers the local and remote cache

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

* docs: give sideEffectsCache.exclude an example a cache key cannot cover

An environment variable such as JAVA_HOME could be added to the cache
key, so it does not justify excluding a package. Name the system
libraries a native addon links against instead.

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-15 14:35:42 +02:00
68112bada4 chore(release): 11.28.5, pacquet 12.10.0 (#16637)
* chore(release): 11.28.5, pacquet 12.10.0

* docs(release): curate the 11.28.5 and 12.10.0 release notes

Add a lead paragraph to each page and group the patch entries under
subject headings, with the security fixes first.

- Merge the unpublish path-boundary fix into the unpublish entry, the
  legacy metadata cache prune into the metadata cache entry, the two
  binary size entries, and the two 64 MiB archive limits on the v11 page.
- Drop the manifest-utils saveType entry from the CLI page: the CLI
  only passes saveType values derived from its own flags, so no CLI user
  can reach the fixed path.
- Rewrite mechanism-only entries (PAX header validation, bundled
  dependency symlinks, custom resolutions under name@version keys, proxy
  setting validation) in terms of what the user sees.
- Use one link style for pnpm/pnpm issues.

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-06 02:00:03 +02:00
Zoltan Kochan 68445b254a fix: harden dependency verification and package handling (#16613)
Verify configuration dependency registry resolutions before installing
plugins, with independently checked pinned tarballs. Require config dependencies to resolve from a registry and skip
registry verification when the installed config dependencies already match
the lockfile.

Constrain archive and deployment paths and preserve content-addressed files
when streaming package entries. Bound metadata allocation before the archive
parser buffers it, using an opt-in patch to the upstream tar crate. Bound
publishing manifest and README buffers without imposing a total extraction
disk quota.

Bind audit signatures to lockfile integrity, hash ambiguous URL dependency
directory names, and sanitize license terminal output. Preserve dependency
error context and structured fatal logs. Update affected documentation,
release notes, and regression coverage.
2026-10-05 22:09:01 +02:00
Zoltan Kochan 103c4cada2 fix(prepare-package): prevent intermediate symlink traversal in subpath extraction (#16612)
When preparing a git dependency with a `#path:` subpath fragment,
intermediate symlink segments pointing outside the repository checkout
could allow files outside the cloned repository directory to be copied
into the store during package preparation.

Resolve and canonicalize both the repository root and joined paths using
`fs.realpathSync` in `safeJoinPath`, verifying containment within the
cloned checkout. Also add unit tests in TypeScript and Rust (`safe_join_path`)
to ensure traversal through intermediate symlinks is rejected.

Fixes https://github.com/pnpm/tasks/issues/82
2026-10-05 19:44:41 +02:00
DexandDextheking1 07478c573a fix(dlx): report a clear error when --package is given without a command (#16306)
Running `pnpm dlx --package=<pkg>` with no command installed the package
and then crashed trying to run an empty command. The handler now throws
ERR_PNPM_DLX_MISSING_COMMAND before doing any work, matching the v12
CLI MissingCommand error for the same invocation. Only v11 is affected;
v12 already rejects this case.

---------

Co-authored-by: Dextheking1 <Dextheking1@users.noreply.github.com>
2026-10-05 13:49:51 +02:00
88d67feb0d fix: forward config flags to install spawned by verifyDepsBeforeRun (#16558)
The install that verifyDepsBeforeRun spawns before `pnpm run` and `pnpm exec`
now receives the parent's `--config.<key>=<value>` options. Settings from
config files and pnpm_config_* environment variables already reached the
child; command-line ones did not, so e.g. `--config.lockfile-dir` was lost and
the install wrote its lockfile into the project.

Both CLIs forward the options as given, at the single place that spawns the
install, so the recheck after waiting for another gate's install gets them too.
The confirmation prompt shown for `verifyDepsBeforeRun: prompt` omits them,
since a `--config.//registry/:_authToken=...` value would otherwise be printed.
pnpm v11 does not forward `--config.dir`: the install already starts in the
resolved directory, where a relative value would resolve a second time.

pnpm v11 carries the options on the config context (`rawCliConfig`) rather than
on cliOptions, which would make them a setting and an explicitly set key.

Closes https://github.com/pnpm/tasks/issues/60

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 00:49:42 +02:00
5dafb09800 chore(release): 11.28.4, pacquet 12.9.1 (#16567)
* chore(release): 11.28.4, pacquet 12.9.1

* docs(release): curate 11.28.4 and pacquet 12.9.1 release notes

Add a lead paragraph to both pages. Move the two credential-leak fixes
in 11.28.4 to the top and group the remaining 27 entries by what the
reader was doing. Merge entries that described one change: the two
INVALID_SETTING validations, the two metadata revalidation fixes, the
two hoisted filtered-install fixes, and, in pacquet 12.9.1, the two
[<since>] selector fixes. Order pacquet 12.9.1 so the `@pnpm/wasm` move,
which WebContainer users must act on, comes first.

Normalize issue links to the [#N] form and link the ETag fix to
pnpm/pnpm#16506.

Every fix was checked against the code released in 11.28.3 and
pacquet 12.9.0; none fixes a defect that never shipped.

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

* docs(release): describe the frozen-lockfile fixes accurately in the 11.28.4 lead

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-03 22:13:02 +02:00
KhảiandClaude Opus 5 1606ba03fb docs: add the documentation and comment principles and apply them (#16516)
* docs(agents): add the documentation and comment principles

Import the principles that transfer from KSXGitHub/perfectionist's
contributor guide, plus four of our own: concise and non-repeating text,
structured text over prose, removal over addition when reviewing or
fixing nitpicks, and a definition of the user-facing text that must not
name implementation details.

The root guide's "Comments" section gains "Register", "Prose that cannot
go stale", "User-facing text", and "Reviewing documentation and
comments". The pacquet style guide adds the Rust specifics (clap and
napi doc comments are user-facing; a default is documented on the
field, not the variant). DOCUMENTATION.md gains the renderer and link
table for each markdown surface.

The guides themselves are brought in line: counts over growable lists
become plain references, a quoted heading becomes an anchor link, and
the size limits are stated once, in the style guide, instead of three
times.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs: keep user-facing text free of implementation names

The `@pnpm/napi` README and type declarations named the Rust engine, the
in-repo package name, and TypeScript internals (`filterPkgsDiff`,
`nameFormatter`) that a user of the addon cannot reach. The pnpmfile
page in both docs trees explained a limitation by the IPC mechanism
behind it. Four changesets named the release line or implementation,
which the package targets already express.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs(crates): cut restating and stale comments in pack, modules-yaml and nine more crates

Apply the comment principles to pack, versioning, pnpr-client,
package-is-installable, fs-packlist, workspace-task-scheduler,
engine-runtime-node-resolver, exportable-manifest, modules-yaml,
pnpr-registry, and pnpr-auth. Removed: call-site comments that repeat
the callee's doc, module rosters of exports that the `pub use` lines
already list, "not yet implemented" notes for behaviour that exists
(`beforePacking`, `hoistedLocations`), body narration, and history
framing. Comment-only; no code changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs(crates): cut restating and stale comments in the small resolver, catalog and pnpr crates

Apply the comment principles to the local, tarball, default, deno and
jsr resolvers, the catalog crates, detect-libc, config-parse-overrides,
env-replace, git-utils, matcher, diagnostics, and the pnpr policy,
cargo, error, osv and fixture crates. Removed: parentheticals that
repeat a `#[diagnostic(code)]` attribute, counts over lists of
variants, callers and entry points, rosters of callers, doc paragraphs
that repeat a sibling's or the module's text, comments narrating the
match arms or condition below them, and history framing. The
`pnpr-prepare --help` text no longer names the test harness.
Comment-only; no code changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs(crates): cut restating and stale comments in publish, workspace, registry and graph-hasher

Apply the comment principles to publish, auth-commands, registry,
workspace, lockfile-verification, default-reporter, the git resolver,
deps-inspection, hooks, graph-hasher, package-manifest, and the pnpr
shared-artifacts and route crates. Removed: a module roster of every
export, counts over lists, plan notes for work that landed ("Phase 5",
"Phase 6"), call-site comments repeating the callee's doc, docs
narrating match arms, a default claim on a `#[default]` variant, a
misattached doc, and history framing. Comment-only; no code changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs(crates): cut restating comments in napi, reporter and pnpr crates

Apply the comment principles to napi, reporter, resolving-resolver-base,
real-hoist, git-fetcher, workspace-manifest-writer, and the pnpr config
and storage crates. Removed: "Mirrors X in index.d.ts" sentences on
`#[napi]` items and other implementation names in addon-facing docs,
stale "not emitted yet" and "when it lands" notes for behaviour that
exists, step-by-step comments narrating the body, emit-site and caller
rosters, docs duplicated from a sibling or the module, a doc attached to
the wrong item, and history framing. Comment-only; no code changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs: cut restating and stale comments across the remaining crates and pnpm11

Apply the comment principles to the rest of the Rust workspace and to the
TypeScript CLI: the cli crate and its clap definitions, package-manager,
config, patching, the deps resolver, lockfile, pnpr, the npm resolver,
tarball, network, executor, cmd-shim, the benchmark and fixture tooling,
and the pnpm11 packages.

Removed: comments narrating the body, docs repeating a callee's, a
sibling's or the module's text, counts over growable lists, rosters of
callers and exports, "not yet implemented" notes for behaviour that
exists, history framing, and `@param` lines restating a signature.

One exception to comment-only: in `pnpr/crates/pnpr/src/resolver/resolve.rs`
the `Workspace` struct sat inside the doc block written for `resolve`, so
the struct moved above it and each doc now attaches to its own item.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs(crates): cut body narration and duplicated docs in resolver, fs and package-manager

Removed: a numbered "Sequence:" list narrating `ensure_file`'s body and
the paragraph restating its backoff constants, the stress-test module
doc's roster of its own tests, refactor-history paragraphs in the peer
walk's finalize pass, call-site and caller-side copies of a callee's doc
(the `optional` AND-fold stays documented on `register_seeded_package`
and on `ResolvedPackage`), a verbatim quotation of a TypeScript test's
expected output in the pnpr search module doc, and field and digest-input
rosters in the package-manager.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs(deps-restorer): cut plan references and re-attach misplaced docs

Removed the "slice 4" plan references and the comments narrating the
bodies they sat above. The `Auto` downgrade-chain doc moved from
`copy_and_log` to `auto_link`, the function it describes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs(deps-restorer): cut the remaining body narration in the hoist path

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs: cut the last body narration and a history clause in the cmd-shim test

`build_graph.rs` kept a `patches` parameter doc that re-derived what
`BuildOptions::patches` documents. The PowerShell-arguments test on main
said the arguments survived "before the shim ended its batch context
too", which describes the change rather than the behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs(cmd-shim): point the PowerShell-shim link at its current module

Main moved `wants_powershell_shim` into `windows_shim_policy`, so the
shim writer's link to it no longer resolved and `cargo doc` failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs(cli): link the fixture the ignored-build setup installs

The shortened doc named `workspace_with_unapproved_build` in backticks,
which `perfectionist::bare_identifier_reference` rejects for an item that
resolves in scope. It is an intra-doc link now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs(crates): restore two contracts and fix four dangling references

A review of this branch's sweep flagged seven places. Six needed a
change:

- `from_ini` documents again that a relative `cafile=` resolves against
  `npmrc_dir`, a precondition its callers have to honour.
- `hash_object` documents again that the private serializer's bytestream
  has to match pnpm 11's, because the digest is persisted and read by
  both.
- The hooked-manifests note no longer says "above" of a gate that lives
  in another module.
- The store-dir race test states its synchronization contract instead of
  labelling it "Post-Option-C".
- `GraphNodeHashOptions.nodeVersion` keeps `undefined` as a literal.
- The pnpr streaming module drops the sentence fragment that introduced
  its list.

The seventh, a `beforePacking` limitation in exportable-manifest, stays
removed: the note said the pnpmfile bridge cannot supply that hook, and
`pnpm_pack::lifecycle::apply_before_packing` has been running it since
before this branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs(cli): say in `pnpm why --help` that `--exclude-peers` is ignored

Demoting the caveat to an ordinary comment took it out of the help,
which then advertised a flag that does nothing: `WhyArgs::run` never
reads `exclude_peers`, and pnpm 11's `why` handler never forwards its
`excludePeers` either. The doc comment states that behavior in the words
a user sees, and the parity reason stays in a comment beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

* docs(cli): drop the doc comment that described the wrong module

The comment described the `pnpm_deps_inspection as deps_tree` re-export
above it, but a doc comment attaches to the item that follows, so rustdoc
showed it as the description of `mod dispatch`. The `use` item already
names the alias, so the comment goes rather than moving.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-02 20:28:21 +02:00
88bf5e4886 perf: watch every script's process group with one watchdog (#16490)
Without a controlling terminal, and in a concurrent recursive run of
pnpm v12, each script pnpm starts leads a process group of its own.
Since pnpm/pnpm#15564 a `sh` stood watch over each such group so that
it dies with pnpm. That doubled the processes a recursive run starts:
`pnpm -r run` over 50 projects started 100 shells, and on macOS each
bare `sh` spawn also walked PATH.

One watchdog per pnpm process now watches every group. It starts with
the first group pnpm creates. pnpm writes `+<pgid>` to its stdin when
it starts watching a group and `-<pgid>` once it is done with it. End
of input means pnpm died, and the watchdog kills every group still
listed, so the guarantee of pnpm/pnpm#15564 holds for any number of
concurrent groups. The watchdog keeps the groups in one string, so a
release costs two pattern expansions rather than a loop over every
group. A watchdog that died is replaced and takes over the groups
still watched: pacquet on its next write, pnpm v11 as soon as it exits.

Both runners change: pacquet's GroupWatch behind spawn_child, and
watchProcessGroup in pnpm v11's npm-lifecycle, whose watchdog and pipe
are unref'd so they never keep pnpm alive.

Closes pnpm/pnpm#16489

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:12:43 +02:00
Zoltan Kochan 446b26f085 fix(run): run the script when the pre-run install fails (#16465)
When verifyDepsBeforeRun is `install` (the default) and node_modules are
out of date, `pnpm run` and `pnpm exec` start `pnpm install` first. In a
sandbox with a read-only store or no network that install cannot succeed,
so no script could run at all.

A failed install now prints a warning that node_modules may be out of sync
and the command continues, the same outcome as the `warn` action. The
install has already printed its own error. The warning is fixed text, so it
echoes no filter selectors, and it names `verifyDepsBeforeRun: false` as the
way to skip the install.

An install ended by a signal, or by Ctrl+C on Windows (exit code
0xC000013A), still stops the command, so an interrupt is never turned into
a script run. The `prompt` action shares the install path, so a confirmed
prompt behaves the same way.

Implemented in both the TypeScript CLI and pacquet, with the settings docs
for both versions updated.

Related to https://github.com/orgs/pnpm/discussions/16212
2026-10-01 13:51:21 +02:00
e0597e47ec fix(exec): hold the workspace dependencies of a filtered selection to the install check (#16268)
A filtered install leaves the workspace dependencies of the projects it
selected without a modules directory, but the install the
verifyDepsBeforeRun gate spawns for a filtered run or exec selects them
(each selector gets "..."). The status check only looked at the projects
the command selected, so it reported the state up to date and the
dependency stayed uninstalled.

Both stacks now verify the selected projects plus their transitive
workspace dependencies, computed at the gate from the graphs the
selection was drawn from. A --filter-prod-only project walks the
prod-pruned graph. When a negated selector is present, the set is
intersected with the projects the spawned install selects, because
negated selectors reach that install unchanged; otherwise the gate would
require an excluded project and install on every run. The v12 helper
returns early when verification is disabled.

In v11 the computation lives in exec.commands (selectProjectsToVerify)
and is passed to checkDepsStatus as selectedProjectsGraph, leaving
deps.status unchanged. In v12 the graph walks the recursive commands
share move to cli_args/recursive/dependencies.rs.

Refs pnpm/tasks#45.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:19:07 +02:00
Zoltan Kochan 9179ae3339 refactor(ts): meet size and shape limits in 15 packages (#16456)
* refactor(ts): meet size and shape limits in 5 packages

Refactor 5 TypeScript packages to satisfy strict size and shape lint limits
(function length, nesting, cognitive complexity, local declarations):
- `@pnpm/installing.linking.modules-cleaner`
- `@pnpm/installing.linking.real-hoist`
- `@pnpm/lockfile.utils`
- `@pnpm/modules-mounter.daemon`
- `@pnpm/releasing.exportable-manifest`

Remove them from PENDING_SIZE_AND_SHAPE_REFACTOR in eslint.config.mjs.

* refactor(ts): meet size and shape limits in 10 packages

Refactor 10 TypeScript packages to satisfy strict size and shape lint limits
(function length, nesting, cognitive complexity, local declarations):
- `@pnpm/bins.cmd-shim`
- `@pnpm/engine.runtime.commands`
- `@pnpm/exec.lifecycle`
- `@pnpm/fetching.git-fetcher`
- `@pnpm/fetching.tarball-fetcher`
- `@pnpm/fs.indexed-pkg-importer`
- `@pnpm/hooks.pnpmfile`
- `@pnpm/installing.context`
- `@pnpm/resolving.git-resolver`
- `@pnpm/store.commands`

Graduate them from PENDING_SIZE_AND_SHAPE_REFACTOR in eslint.config.mjs.

* fix(ts): address review feedback on shims and lockfile comments

* test(cmd-shim): add regression test for isShimBasedirAnchorCurrent with absolute targets
2026-10-01 04:53:20 +02:00
Zoltan Kochan d3613e5d9e fix(gvs): run entry points loaded by CommonJS require hooks (#16448)
Any `--import` flag in NODE_OPTIONS makes Node.js run the main entry
point through the ESM loader, on every Node.js version. The ESM loader
rejects extensions that only CommonJS require hooks handle, so
`ts-node index.ts` failed with ERR_UNKNOWN_FILE_EXTENSION once the
global virtual store injected its NODE_PATH loader flag. Node.js 24+
only appeared unaffected because it strips TypeScript types natively.

The resolve hook now marks an entry point that the ESM loader cannot
classify as CommonJS, which hands it back to the CommonJS loader, as
Node.js does without `--import`. `.js` entry points and entry points
without an extension are left alone because Node.js may detect module
syntax in them. The hook stays resolve-only: registering any synchronous
load hook breaks require.extensions for CommonJS mains on Node.js 22.15.
The hook is now installed even when NODE_PATH is empty, since the flag
alone already routes the entry point through the ESM loader.

Closes pnpm/pnpm#16436
2026-09-30 19:12:41 +02:00
de9611cf28 chore(release): 11.28.3 (#16444)
* chore(release): 11.28.3

* docs(changelog): curate the 11.28.3 release notes

Merge the eleven entries about names such as `constructor` into one,
group the patch changes by what the reader was doing, and add a lead
paragraph.

Drop the zlib-rs entry: that change only touched Cargo.toml, and the
TypeScript CLI does not use the Rust engine. It belongs to the next
pacquet release.

Rewrite the store index entry for CLI users. `ReadOnlyStoreIndex` and
`ImmutableStoreIndex` are library API documented in the
`@pnpm/store.index` changelog.

Say what the store prune mark-phase fix changes for the user.

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-09-30 16:56:44 +02:00
Zoltan Kochan 4edeef9176 style(ts): port the perfectionist lint rules to ESLint (#16415)
Add TypeScript ports of the perfectionist rules that the Rust workspace
enforces through dylint.toml to @pnpm/eslint-config, with the same limits,
and fix every finding that needs no refactor.

The size and shape rules (function length, cognitive complexity, nesting,
local bindings, file length) are enforced for packages that already meet
them. The packages that do not are listed in PENDING_SIZE_AND_SHAPE_REFACTOR
in eslint.config.mjs and are refactored in follow-up changes.

The custom rules mirror perfectionist's semantics rather than reusing
sonarjs or max-lines-per-function, so that one limit means the same thing
in both codebases: a closure is a level of the body that contains it, a
nested function declaration is measured on its own, and a test-framework
callback plays the role of a #[test] function.

Bugs found and fixed along the way:
- a registry error body cut at exactly 64 KiB lost its truncation marker
- removing a dependency could leave dangling .bin shims
- constructor/__proto__ names crashed the projects graph, were skipped by
  the injected-deps syncer, and were dropped from writeWantedLockfile's
  returned lockfile

The CI chunk runner learns to run the node:test suite of the ESLint
config package.
2026-09-30 15:10:08 +02:00
Gabriel Mari S. FloresandCursor Agent a04402ff60 fix(npm-lifecycle): keep waiting for a zombie whose other threads still run (#16392)
Linux reports state Z both for a real zombie and for a process whose
main thread has exited while other threads keep running. The group wait
treated every Z as done, so pnpm exited while those threads were still
running. A Z member now counts as live when its task directory lists
more than one thread. A task directory that cannot be read does not.

refs pnpm/tasks#56

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-09-30 00:37:02 +02:00
57e7b9b76a chore(release): 11.28.2, pacquet 12.8.1 (#16349)
* chore(release): 11.28.2, pacquet 12.8.1

* docs(release): curate the 11.28.2 and 12.8.1 release notes

Add the lead paragraph the previous release pages carry, order the
pacquet entries by impact (failing installs and damaged files first,
then wrong lockfiles, speed, and command behavior), lead the exec-bit
entry with what the reader sees, and use the same issue-link style in
every entry.

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-09-28 18:32:59 +02:00
15a5da5864 fix: skip the pre-run install for a project with nothing to install (#16317)
The verify-deps-before-run gate spawned an install whenever no workspace
state file existed, even for projects that give an install nothing to
do. That install only wrote node_modules and pnpm-lock.yaml, which
became visible in directories the workspace packages patterns leave
out after pnpm/pnpm#15125.

The gate now returns up to date when no state file exists and no
project the install would cover declares a dependency, a required peer
that autoInstallPeers would fetch, or an install lifecycle script
(preinstall through postprepare, or an implicit binding.gyp build) that
ignoreScripts does not disable. pnpm:devPreinstall counts only in the
workspace root, the only place an install runs it. Under a shared
lockfile every workspace project is checked, and the projects are walked
only when the root manifest itself has nothing to install. With separate
lockfiles a non-recursive run checks only its own project. A
non-recursive pnpm 11 command gets no allProjects, so the gate reads the
workspace itself, as checkDepsStatus does.

A readPackage hook can add dependencies, so any pnpmfile the install
would load (looked up from the lockfile directory) keeps the install.
A workspace that cannot be read, or a lockfileDir pinned away from the
project, also keeps it. Projects with lifecycle scripts still
install on the first run, so those scripts keep running on first use.

Closes pnpm/pnpm#16313

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:19:44 +02:00
9ce8c2e734 chore(release): 11.28.1, pacquet 12.8.0, pnpr 0.1.0-alpha.14 (#16312)
* chore(release): 11.28.1, pacquet 12.8.0, pnpr 0.1.0-alpha.14

* docs(release): park Rust changelogs and curate release notes

The release bump ran with pnpm 12.7.0, which since pnpm/pnpm#14934
writes the changelog of every private project to a committed
CHANGELOG.md. pacquet, `@pnpm/napi` and `@pnpm/pnpr` are private
in-repo but published, and release.yml and the publish-time changelog
composer both read their parked section. Move the three sections back
to .changeset/changelogs so the GitHub release bodies and the npm
tarball changelogs are not empty. The tooling follow-up is recorded
on pnpm/tasks#38.

Curate the pnpm 11.28.1, pnpm 12.8.0 and pnpr 0.1.0-alpha.14 pages:
add lead paragraphs, group patch entries by subject, merge entries
describing one change, drop internals no CLI user sees, and fix
"instead of" tails, implementation names and link placement. This
addresses both Qodo findings on the catalog entry.

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-09-28 08:57:31 +02:00
Zoltan Kochan 360afab03f fix(prepare-package): prepare a lockfile-less pnpm workspace with pnpm (#16301)
preferred-pm recognized pnpm only by a pnpm-lock.yaml, so a git-hosted
dependency that is a pnpm workspace without a committed lockfile fell
back to npm. npm then honoured the dependency's own .npmrc settings,
such as ignore-scripts, and could skip the build.

preferred-pm 5.0.1 also looks for pnpm-workspace.yaml once no lockfile
in the package directory names a package manager. That matches the
order the v12 git fetcher uses since pnpm/pnpm#15660, where a lockfile
still wins over the workspace file.

The test asserts which lockfile the prepare step leaves behind. It only
fails without the fix when no ancestor of the test directory holds a
pnpm-lock.yaml, because preferred-pm searches parent directories for
one.

Closes pnpm/pnpm#14011
2026-09-28 00:28:28 +02:00
1cb40c797f fix: publish injected dependency edits while scripts run (#15881)
syncInjectedDepsAfterScripts refreshed injected copies only after the
script exited, so a long-running dev server never saw source changes.
While a listed script runs, changed files are now copied into each
injected directory as new files, so watchers see the write. The poll
stops before the hardlink sync that follows the script. Watch setup is
best effort, so an unreadable modules manifest does not stop the script.

In pnpm v12, a mutable local directory that nothing builds is hardlinked
under the auto import method, as the v11 directory fetcher already
does, so an in-place edit reaches the injected copy. Explicit import
methods, packages that will be built, and deploy isolation keep their
existing behavior.

Closes pnpm/pnpm#4410

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:35:08 +02:00
Zoltan Kochan 9aea7e930a fix(run): refuse the implicit install that would drop ignored pnpm-field settings (#16282)
The verify-deps-before-run gate spawns `pnpm install` when node_modules
are out of sync. When the root package.json still keeps overrides,
packageExtensions, patchedDependencies or ignoredOptionalDependencies in
its `pnpm` field, which pnpm no longer reads, the lockfile records
settings the effective config can never reproduce. Every `pnpm run` then
found the tree stale and rewrote the lockfile without them, silently
dropping security overrides.

The gate now refuses that install (in prompt mode, once the user
confirms it) and fails with ERR_PNPM_VERIFY_DEPS_BEFORE_RUN and asks to move the settings to
pnpm-workspace.yaml. Migrated keys the lockfile does not record keep the
existing behavior. Fixed in both the Rust and the TypeScript CLI.

Closes pnpm/pnpm#16278
2026-09-27 20:36:00 +02:00
1c9760c5fc fix(exec): install only the projects a filtered run or exec selected (#16211)
`pnpm --filter <selector> run|exec` checks the workspace state before it runs
the command and spawns an install when that state is outdated. The spawned
install carried no project selection, so it covered every project of the
workspace instead of the ones the command was limited to. And a state that
records a filtered install exempted every project from the modules-directory
requirement, so the gate answered "already up to date" for a project the
earlier filtered install had not materialized and ran the command without
installing it.

Forward the `--filter` and `--filter-prod` selectors to the install the gate
spawns, each extended with `...` so the workspace projects a selected project
depends on are installed too, and hold the projects the gated command selected to the
modules-directory requirement, in both the TypeScript and the Rust CLI.

Closes pnpm/pnpm#11865.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:58:40 +02:00
Zoltan Kochan 266cbd9615 fix(run): pass script arguments through cmd unchanged on Windows (#16262)
`pnpm run` appended the arguments after the script name with JSON string
quoting when cmd parsed the script. cmd then expanded `%VAR%`, and the
target program's C runtime received doubled backslashes and `\t` for a tab.

Both CLIs now quote each argument the way npm does: quoted for the C
runtime, then with every cmd metacharacter escaped with `^`. When the
script's first word resolves to a .cmd or .bat file on the script's PATH,
such as a node_modules/.bin shim, the arguments are escaped a second time
because the batch file parses them again. The command that receives the
arguments is the last one of a chain, and the lookup searches the script
directory before PATH, as cmd does. Line breaks, which cmd cannot carry,
are still passed as the two characters `\n` / `\r`.

pnpm prints the script with its arguments quoted the POSIX way on every
platform, so the `^` escapes stay out of the `$` line, the lifecycle log
event and error messages. npm_lifecycle_script keeps the command that
runs.

Closes pnpm/pnpm#16257
2026-09-27 13:57:54 +02:00
Zoltan Kochan 11b9393080 fix: recognize WebContainers' async fs errors (#16253)
StackBlitz WebContainers reject asynchronous fs calls with errors that
inherit from Error but were not created by the native constructor, so
util.types.isNativeError() returns false for them. pnpm used that check
to recognize ENOENT and other codes, so a missing pnpm-lock.yaml was
rethrown and pnpm install failed on any project without a lockfile.

Add isError() to @pnpm/error, which accepts a native error or any
instanceof Error, and use it at every non-test call site. The native
check alone is still needed for Jest, where errors from another realm
fail instanceof. Update the AGENTS.md guidance to match.

Closes pnpm/pnpm#15649
2026-09-27 12:02:05 +02:00
16f56b91be fix: honor scriptShell when shellEmulator is set (#15865)
A configured scriptShell is spawned for pnpm run and lifecycle scripts even when shellEmulator is also enabled. The built-in emulator does not implement bash export or relative script paths the way Git Bash does, and the reported config sets both options, so the emulator was hiding the shell the user configured.

shellEmulator still selects the built-in shell when scriptShell is unset. A blank scriptShell is unset on both runners, so the fallback shell and the extra-argument quoting match. The shell is still selected first, so a .bat or .cmd scriptShell is rejected before anything runs.

Extra arguments are JSON-quoted only when cmd will parse the script (windowsVerbatimArguments). A non-cmd scriptShell, including a path such as C:\Program Files\Git\bin\bash.exe, gets POSIX quoting and is spawned without verbatim arguments, so the path stays one argument.

PATH, bin lookup, and auth are not changed.

Closes pnpm/pnpm#14719

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:23:16 +02:00
01e7232d7f fix(run): honor tasks dependsOn for each script a RegExp selector matches (#16203)
With `tasks` declared, a recursive run with a RegExp selector now seeds
one task per script the selector matches in each project, so each
resolves the `dependsOn` declared for its own name, as a run of that
script by name does. The selector string itself matched no `tasks`
entry, so its task fell back to `^<selector>` and the matched scripts'
declared dependencies never ran.

Resolving the union of the matched scripts' entries onto one
per-project task was rejected. That task runs its scripts concurrently,
so a matched `test` could not wait for a matched `build`. A `^build`
entry also pulled in a `build` task that the dependency project's
selector task ran again.

A project the selector matches nothing in keeps a requested
pass-through task under the selector name, with no dependencies.
Without `tasks`, or with an exact `tasks` entry under the selector
string, the selector stays a single task per project.

The hidden-script check runs per project over the requested tasks. A
selector that matches only hidden scripts in a project still fails, but
a hidden script alongside visible ones is filtered as before. A
requested task that a `dependsOn` declaration targets is exempt, like
any `dependsOn` reference. The exempt set is every dependency of a task
with a `tasks` entry, since a task without one only has the default
`^<name>` edge. It is read from the full graph before sequencing, in
declaration direction when `--reverse` inverted the edges.

`--resume-from` anchors on every task requested in the anchor project
when the invocation's own task is absent, which is the case for an
expanded selector. Summary keys qualify every task but the one
named by the invocation, so expanded tasks do not share a key.

Closes pnpm/pnpm#15596

Co-authored-by: Dex <4175097+Dextheking1@users.noreply.github.com>

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:35:12 +02:00
Zoltan Kochan ad525f26db refactor: move the pnpm/components packages into the monorepo (#16214)
The remaining packages of the pnpm/components Bit workspace now live
under pnpm11/ and are published from here, keeping their npm names:

  @pnpm/config.env-replace           -> pnpm11/config/env-replace
  @pnpm/log.group                    -> pnpm11/log/group
  @pnpm/network.agent                -> pnpm11/network/agent
  @pnpm/network.ca-file              -> pnpm11/network/ca-file
  @pnpm/network.config               -> pnpm11/network/config
  @pnpm/network.proxy-agent          -> pnpm11/network/proxy-agent
  @pnpm/os.env.path-extender         -> pnpm11/os/env/path-extender
  @pnpm/os.env.path-extender-posix   -> pnpm11/os/env/path-extender-posix
  @pnpm/os.env.path-extender-windows -> pnpm11/os/env/path-extender-windows

lex-comparator and nerf-dart were already moved under new names, and the
Bit env component is build tooling with no counterpart here.

The sources are carried over from pnpm/components at 42ec0f7, which
includes the Windows fix that rewrites a PNPM_HOME value stored with the
wrong registry type. pnpm v12 got the same fix in pnpm/pnpm#15746, so
its changeset now also covers pnpm v11, which bundles the workspace copy.

Porting to the monorepo's ES module setup:

- relative imports carry .js and type re-exports are marked `type`;
- tests use jest.unstable_mockModule and import through the package;
- string.prototype.matchall and graceful-fs give way to built-ins;
- safe-execa and lru-cache move to the catalog versions (0.3, 11);
- network.config depends on @pnpm/config.registry-auth-key;
- the strict-ssl proxy test uses node:https and proxy v4 instead of
  node-fetch and proxy v1.

The packages start at 1100.0.0 to sit in the pnpm11 version band.
2026-09-26 23:33:47 +02:00
79c2bfc352 fix: exit with the script's status after Ctrl+C (#15854)
dash holds a terminal SIGINT until the foreground command exits, then
dies from that signal even when the command already returned a status.
`pnpm run` treated that shell death as a lifecycle failure and lost the
script's code.

A Bourne shell now runs the script behind a trap that re-raises SIGINT
when the command itself died from it (status 130) and otherwise lets the
script continue, as bash does. A deliberate exit of 130 is
indistinguishable from that report, so it is re-raised too. A second
interrupt always re-raises, so a loop of builtins can still be stopped.

Closes pnpm/pnpm#9945

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 15:40:46 +02:00
Altay 7c1b501d2d fix(git-fetcher): skip unapproved builds while preparing a git dependency (#15465)
A git-hosted dependency that has to be built is prepared by running its
package manager's install in a temporary checkout. With strictDepBuilds on
by default, that nested pnpm install failed with ERR_PNPM_IGNORED_BUILDS as
soon as one of the dependency's own dependencies had a build script, and
nobody can approve builds in that checkout. The outer install then failed
with ERR_PNPM_PREPARE_PACKAGE.

The prepare scripts now run with pnpm_config_strict_dep_builds=false. No
additional build runs: unapproved builds in the checkout are skipped and
reported, as they are without strictDepBuilds.

Closes pnpm/pnpm#9764
2026-09-26 03:11:21 +02:00
deca7295a4 fix: build an injected workspace dependency's publish directory before relying on it (#15702)
* **Bug Fixes**
  * Fixed installation of injected workspace dependencies whose configured publish directory is created by a `prepare` script. Built files are copied from the publish directory to the expected package location.
  * Explicit `file:` dependencies retain their project-root layout, with built files remaining in the configured subdirectory.
  * Reinstalling after a publish directory is removed preserves an existing injected copy.
  * Frozen installs no longer report these dependencies as outdated before the publish directory is built.
  * Workspace packages publishing from a configured directory are recognized during installation and lockfile freshness checks.
  * Updated injected package command links to remove stale entries and reflect rebuilt packages.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-09-26 01:35:50 +02:00
Altay c36eb58c77 fix(lifecycle): stop reading script output a background process holds open (#15638)
A lifecycle script that starts a process in the background and exits
leaves that process holding the script's stdout and stderr pipes. pnpm
waited for end of output on those pipes, so an install hung for as long
as the background process lived (an esbuild service started by a
`prepare` build, for example).

Both stacks now give the pumps one second after the script exits to
reach the end of its output, then stop reading. v12 closes a gate that
drops anything read later, and v11 destroys the child's output streams.

Closes pnpm/pnpm#5730
2026-09-25 23:04:13 +02:00
c317678744 fix(gvs): serialize builds into a shared global virtual store slot (#15634)
Concurrent installs that resolve to the same global virtual store slot
ran its lifecycle scripts in the one shared directory at once. A script
that rewrites its own files, such as esbuild's install.js, then failed in
one install, and that install removed the slot the other installs had
already linked.

Builds into a global virtual store slot now hold a directory lock at
`<slot>/.pnpm-build.lock`, in both pnpm v11 and v12. A build that finds
the slot's `.pnpm-needs-build` marker gone once it gets the lock skips
the build, since another install finished it. A build marks the slot
"started" before it writes into it, and a waiter that finds that mark
skips the build too, since another install's build of it failed or died. An install also takes
the lock before it re-imports a slot that still carries the marker,
which would otherwise overwrite a build in progress, and so does a
side-effects cache hit that re-imports its overlay into the slot. A
patched package whose scripts --ignore-scripts withheld keeps its marker
until the scripts have run.

A failed build no longer removes the slot. It leaves the "started"
`.pnpm-needs-build` marker instead, which the next install already treats
as an interrupted build: it re-imports the pristine files and builds
again.

The TypeScript directory lock moves from `@pnpm/exec.commands` into a new
`@pnpm/fs.dir-lock` package so the build step can use it.

Closes pnpm/pnpm#15568

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:15:41 +02:00
b341f5ada2 chore(release): 11.28.0, pacquet 12.7.0, pnpr 0.1.0-alpha.13 (#15647)
* chore(release): 11.28.0, pacquet 12.7.0, pnpr 0.1.0-alpha.13

* docs(release): curate the 11.28.0 and 12.7.0 release pages

Group both pages under subject headings with a lead paragraph that
names the headline changes and the security fixes, and move the
security entries to the top of the patch section.

Merge entries that describe one change: the node shim version files,
ranged selectors without a package.json, SemVer build metadata,
completion injection, custom modulesDir command lookup,
list --only-projects, store status, npm_execpath, catalog and npm-alias
workspace edges, the macOS CA fallback, and the prepare/devPreinstall
skips.

Drop entries that describe no change a published version could show:
the test-only --no-optional entry on both pages, the exported
CreateNewStoreControllerOptions type, the publish availability-probe
fix for a flag first shipping in 12.7.0, and the executable-bit check
the Rust CLI never had wrong.

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

* docs(release): describe the bin shim part of the modulesDir entry accurately

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-09-25 12:01:04 +02:00
e8cfd554dc fix: pass /d /s /c to a cmd.exe scriptShell on Windows (#15509)
A scriptShell pointing at cmd.exe was spawned with `-c`, like any custom
shell. cmd.exe ignores `-c` and takes the first `/c` on its command line as
its switch, so a script such as `node install/can-compile` ran `an-compile`.

When the configured scriptShell's basename is `cmd` or `cmd.exe` on Windows,
pass `/d /s /c` and the verbatim script, as the default ComSpec path already
does. Applied to both the v11 lifecycle runner and the v12 executor.

Closes pnpm/pnpm#7181

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:15:18 +02:00
Zoltan KochanandClaude Opus 5.5 16af1b1665 fix(store): tighten the gypfile recheck of stored requiresBuild (#15635)
Follow-up to the review of pnpm/pnpm#14900.

- The v11 worker parsed the store's package.json with a bare
  JSON.parse, so a manifest starting with a byte-order mark failed to
  parse and kept the stale `requiresBuild: true`. It now uses
  `parseJsonBufferSync`, which strips the mark, as store ingestion does.
- Both stacks answer from the manifest's install scripts before
  scanning the file index, and the TypeScript check no longer copies
  the index.
- The predicate is renamed to `stored_requires_build_needs_manifest_check`
  / `storedRequiresBuildNeedsManifestCheck`, and its docs state the
  current contract.
- `checkBindingGyp`'s JSDoc states that its caller decides whether the
  synthesized script applies.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:48:32 +02:00
d44a0978a6 fix: read gypfile: false as an opt-out of the implicit node-gyp build (#14900)
pnpm synthesizes a `node-gyp rebuild` install script for a package that ships a `binding.gyp` and declares no `install` or `preinstall` script. npm reads `gypfile: false` as that package's opt-out from the synthesized script (https://docs.npmjs.com/cli/v12/configuring-npm/package-json#gypfile). This commit implements that feature in pnpm so such a dependency needs no `allowBuilds` entry and is no longer listed under "Ignored build scripts". 

`better-sqlite3` v13 sets `gypfile: false` because it ships prebuilts on every supported platform. It's trying to avoid install script policy errors, and the dependencies required by node-gyp (python, c++). On pnpm, without this change, it triggers the script policy.

Because everything keyed off `requiresBuild` is decided before the build phase applies a patch, the patch preview answers for the whole patched package rather than for the paths the patch writes: a patch that drops `gypfile: false` puts the `binding.gyp` the package already shipped back in scope, and a patch that deletes the `binding.gyp` or the manifest takes the trigger, or the opt-out, away with it.

Store-index rows written before this change recorded a lone `binding.gyp` as build work, and their bundled manifest dropped `gypfile`. A stored `requiresBuild: true` whose only trigger is a `binding.gyp` and whose bundled manifest has no `gypfile` is rechecked against the package's `package.json` in the store, so a warm store picks up the opt-out without a refetch.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:22:34 +02:00