Commit Graph
1122 Commits
Author SHA1 Message Date
Zoltan KochanandClaude Opus 5.5 6d736a02a8 fix(add): resolve a version-less dependency to the tag setting (#17260)
pnpm v12 resolved `pnpm add <pkg>` without a version, and a version-less
`npm:` or `jsr:` dependency, to the `latest` dist-tag even when the `tag`
setting named another one. pnpm v11 reads `tag` from pnpm-workspace.yaml,
the global config, `PNPM_CONFIG_TAG`, and `--config.tag`, and threads it
as `defaultTag` into `parseWantedDependencies` (installSome.ts) and the
resolution context (createResolutionContext.ts).

`Config` now carries `tag`, read from the same layers, with
`Config::default_tag` falling back to `latest`. A brand-new `add` with no
version resolves a non-default tag the way an explicit `add <pkg>@<tag>`
does, so the picked version is also locked. A `jsr:` add and the install
resolver take the same default. `dlx`, `add --config`, `update --latest`,
and `outdated` keep `latest`, as they do in v11.

Tested with e2e tests for the yaml setting, `--config.tag`, and a
version-less `npm:` alias, each seen to fail with `default_tag` forced to
`latest`, plus config unit tests for apply, override, reset, and the
resolved projection.

Part of pnpm/tasks#191

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 09:46:18 +02:00
Zoltan Kochan 359ff10948 fix(run): honor requiredScripts in recursive runs (#17257)
pnpm v12 read no `requiredScripts` setting, so `pnpm -r run <script>`
ran normally when a selected project lacked a script the workspace
declares required. pnpm v11 fails such a run before dispatching anything
(`throwIfRequiredScriptIsMissing` in
pnpm11/exec/commands/src/runRecursive.ts), with
ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT and the message
`Missing script "<script>" in packages: <names>`.

The config crate now reads `requiredScripts` from pnpm-workspace.yaml
into `Config::required_scripts`. The global config.yaml drops it, as v11
does, since it is not a config-file key there; v11 reads no environment
variable for it either. The recursive run checks it where v11 does,
after `--resume-from` and the dry-run exit, before the hidden-script
filter and the no-script check, and after the `pnpm <script>` shorthand
decides whether to fall back to `exec`. Projects are named by their
manifest name, falling back to their directory.

Tested by a run_recursive e2e module that fails without the check,
including a nameless project listed by its directory and a --filter run
that checks only the selected projects, and a config test for the
workspace and global sources. The requiredScripts docs of both versions
now say the check covers the projects selected for the run.

Part of pnpm/tasks#191.
2026-10-23 02:18:06 +02:00
Zoltan Kochan 4e6351952d docs: correct the guides from agent skills to lockfile (#17248)
Correct the v12 guides from agent-skills.md through lockfile.md against
pnpm/crates, and fix the same wrong text on the v11 twins where pnpm11
confirms it. Part of pnpm/tasks#191.

Wrong claims (both versions):
- configuring: the global auth file is auth.ini, not rc; list the
  npm_config_* exceptions (workspace dir, userconfig, URL-scoped auth).
- config-dependencies: the hook import used an invalid ESM specifier.
- docker: Example 3 ran fetch --prod and built without an offline install
  or pnpm-workspace.yaml.
- errors: drop MISMATCHED_RELEASE_CHANNEL, describe INVALID_NODE_VERSION
  through nodeVersion, PEER_DEP_ISSUES needs strictPeerDependencies.
- faq: no lockfile deletion after a readPackage hook, default store order,
  junctions depend on symlink privilege.
- filtering: `!./lib` matches one project; use `!./lib/**`.
- git branch lockfiles: every non [A-Za-z0-9_.-] character becomes `!`,
  the name is lowercased, merging deletes branch lockfiles, "or" patterns.
- git-worktrees: remove the nonexistent worktree:new and shell/wt.sh.
- global-packages: orphaned dirs are cleaned by add -g (and update -g in
  v12), not store prune; document that add -g replaces every group that
  contains the package.
- how-peers-are-resolved: current peer directory names.
- completion: built-in completion already covers --filter and scripts.
- feature-comparison: broken JSR anchor.

Wrong claims (v12 only):
- cargo: cargo.indexUrl fails the parse, overrides apply to adding only,
  merge the duplicated Cargo index section into Registries.
- global-virtual-store: pnx keeps its own virtual store (dlx part only;
  the global-install claims are left for a separate decision).
- installation: the npm package installs on Node.js 18, only get-pnpm
  needs 22.13; trim unverified compatibility rows.

Missing facts (v12): agent-skills group filtering and naming, catalogs
(catalogs.default, pnx, --save-catalog, error codes), cargo recursive
add, build-std, resolution keys, completion zsh/pwsh setup, config
dependency location and error codes, include error codes, CI opt-outs and
pnpm ci, finders on list, filtering settings and --no-fail-if-no-match,
global install refusals, trust for npm-registry package managers, Rust
shims, overrides link from aliases.

Stale history (v12): drop v11/v10 version markers and "since v11"
narration in continuous-integration, filtering, finders, git-worktrees,
global-packages, global-virtual-store, how-peers-are-resolved, lockfile,
completion, configuring.

Review follow-up: the Node.js compatibility table keeps the pnpm 10
column; Docker Example 3 builds with all dependencies in a build stage
and ships only production dependencies, with the pnpm-workspace.yaml
caveat (both versions); catalogs keep the bare-path form; v11 global
packages name `pnpm update -g` as cleaning orphans too; the cargo
build-std note says when rust-src must be listed.

Second review: build-std needs a non-empty list; the Docker fetch layer
also depends on the project settings; branch lockfile names keep only
ASCII characters; the conflict merge needs both sides to parse; Rust
shims and package managers are default globalShims participants;
pnpm self-update also cleans orphaned global groups; v12 pnpm login
writes tokens to config.yaml; legacyDirFiltering keeps the subtree
wording (the v12 mismatch is pnpm/tasks#196); the v12 Docker
example copies the files that `include` names before pnpm fetch.
2026-10-23 02:16:47 +02:00
Zoltan Kochan 52dce0f4dc docs(settings): correct defaults and document missing settings (#17246)
Wrong claims (v12 and the v11 twin where the v11 source agrees):
- proxy precedence: a configured httpsProxy/httpProxy/noProxy wins and the
  environment is only a fallback; HTTPS never falls back to HTTP_PROXY
- proxy opt-out: false or null in httpsProxy, httpProxy, or noProxy reads
  as unset; only proxy: false turns proxying off
- networkConcurrency auto value is clamped to 64-96, not 16-64
- maxsockets default: unset in v12, 50 in v11
- stateDir on macOS is ~/.local/state/pnpm
- unsafePerm only moves TMPDIR to node_modules/.tmp
- ignoreWorkspaceRootCheck affects pnpm add only; the duplicate CLI
  section is removed in favor of the workspaces page
- useRunningStoreServer has no effect and is removed
- .npmrc also supplies TLS, proxy, and local-address settings
- the list of keys a project pnpm-workspace.yaml cannot set now names
  scope (both) and macosBackup, ci, tools, globalShims (v12)
- v12: the GVS CI auto-disable sentence, the frozenStore Node.js
  requirement, the JavaScript shell emulator, and the dlx global virtual
  store claim are corrected

Missing settings (v12): saveExact, embedReadme, scriptsPrependNodePath,
userAgent, lockfileDir, excludeLinksFromLockfile, globalVirtualStoreDir,
externalDependencies, autoInstallPeersFromHighestMatch; loglevel silent;
PNPM_HOME defaults for globalDir/globalBinDir; childConcurrency
negative values; what ci changes and where updateNotifier is skipped.

Index (v12): lists every section on the group pages, including
progress, saveTypes, autoDedupe, maxTarballUnpackedSize, and macosBackup.

Stale history (v12): removed pre-v12 "Added in" markers and rewrote
"Since v10/v11" and "Before v11" sentences as current behavior.

Part of pnpm/tasks#191.
2026-10-23 01:38:49 +02:00
Zoltan Kochan b3eee14a96 docs(cli): correct the command reference from access to init (#17247)
Part of pnpm/tasks#191.

Wrong claims, fixed on the v12 page and on the v11 twin where it had
the same text:
- audit: `--interactive` implies `--fix=override`; `info` is a valid
  audit level.
- change check validates the pending intents.
- deploy copies the publish packlist unless `deployAllFiles` is set.
- deprecate: an empty message fails, use `pnpm undeprecate`.
- deprecate, dist-tag: only `--otp` supplies the one-time password.
- dist-tag add defaults to `latest`, not the `tag` setting.
- docs takes exactly one package.
- env: there is no `add` subcommand, and `list` queries the Node.js
  mirror only.
- v12 only: clean no longer says it uses Node.js, add no longer
  presents `-w` as an alias, the deploy package-manager-pin history
  is no longer contradictory, doctor sample shows store v11.

Missing facts:
- add: `--save-prefix`, `--save-build`, `--no-save-peer`,
  `--no-save-types`, `--lockfile-only`, `--force`; where Yarn vs npm
  and Bun pins go; catalog and configDependencies links.
- access status shorthands; approve-builds error codes, the
  placeholder value and the immediate rebuild; audit exit code,
  `--optional`, `--production`, and how to make `--ignore` write
  `audit.ignore`; `--ignore` exits with code 0 and
  `--ignore-unfixable` fails on an advisory without a GHSA ID.
- bugs and docs `--registry`; ci accepts install options;
  clean script override and modulesDir; config `--location` type and
  `get` output; create name mapping, `--shell-mode`, `--cpu`, `--os`,
  `--libc`; dedupe flags; deploy error codes and Dockerfile WORKDIR;
  dist-tag aliases and web OTP; env `--global` requirement; exec
  recursive options and PNPM_PACKAGE_NAME; fetch `--ignore-pnpmfile`,
  lockfile requirement, optional pnpmfile in the Dockerfiles.
- usage, examples, and output for bin, cache, cache list, cache
  delete (with the no-argument warning), cache view, cat-file,
  cat-index, find-hash, ignored-builds, import, and init
  (`initPackageManager`, `initType`, ERR_PNPM_PACKAGE_JSON_EXISTS).

Stale history on v12 pages: removed "Added in" markers for v10 and
v11 and rewrote "Since v11..." passages as current behavior in
access, add, approve-builds, audit, bugs, cache-list-registries,
cache-path, cache-prune, change, ci, config, create, deploy,
deprecate, dist-tag, docs, doctor, help, ignored-builds, and init.

Review follow-up: cat-file now says it rejects the hex digest that
cat-index prints; the settings index links initPackageManager and
initType; `change check` says it plans the release without reporting
it (both versions); `pnpm bin` follows a custom modulesDir.
2026-10-23 00:49:08 +02:00
Zoltan Kochan cb3f29bc0a docs(cli): correct the command reference from install to with (#17251)
Part of pnpm/tasks#191.

Wrong claims, fixed in the v12 docs and, where the same text is wrong
for v11, in the v11 docs:
- logout: drop the nonexistent --scope option.
- patch: the edit dir is node_modules/.pnpm_patches/<name>@<version>;
  drop the "*" special case.
- patch-commit: fix the ---patches-dir heading.
- prefix: describe the real marker walk instead of "closest package.json".
- setup: setup installs pnpm as a global package and writes pn/pnpx/pnx.
- start, test: "run start" and "run test" are not aliases.
- unlink: drop the system-wide and link --global wording.
- unpublish: a package name is required; --otp has no env var; --registry
  wording.
- update: --depth is dependency depth, not directory depth.
- version: --tag-version-prefix defaults to the tagVersionPrefix setting.
- view: the first argument is always the package.
- why: drop --global and --only-projects; --exclude-peers has no effect;
  --prod includes optionalDependencies.
- install: recursiveInstall spelling; replace the stale ci-info snippet.
- v12 only: pack-app runtime default and builder download, pnx
  provisioning list and cmd.exe typo, tasks sample priority.

Missing flags, settings, and error codes on the v12 pages: install (about
twenty flags), lane, licenses, link, list, outdated, owner, pack-app path
rules, pack, patch, patch-remove, peers, permissions, ping, pipeline, pm,
pnx, prune, publish, rebuild, recursive, remove, repo, root, run,
runtime, sbom, search, self-update, set-script, setup, shim, stage, star,
start, store, tasks, team, test, token, unlink, unpublish, update,
version, view, why, with.

Stale history on the v12 pages: removed "Added in" markers older than
v12 and rewrote "Since v11.x" and "In v11" sentences as current behavior
(login, logout, licenses, link, outdated, patch, pnx, publish, runtime,
sbom, self-update, setup, update, and others).

Review follow-up: patch's default edit directory is named after the
argument as typed (both versions); self-update installs the unscoped
package only for pnpm 12 and newer; only --no-bail is restricted;
npm_package_* covers name, version, config, engines and bin; prefix -g
prints the parent of root -g; --optional no longer claims to override
--no-optional.
2026-10-23 00:47:42 +02:00
Zoltan Kochanandgithub-actions[bot] 71d90a06a3 chore(release): 11.28.8 (#17256)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-22 23:45:10 +02:00
Zoltan KochanandClaude Opus 5.5 6f95957063 docs: lint prose with Vale (#17238)
* docs: lint prose with Vale

Add Vale with an in-repo style, .vale/styles/pnpm, that turns the
writing rules in AGENTS.md and the Google developer documentation style
guide into lint rules. The stock Vale packages (Google, Microsoft,
RedHat, write-good) were too noisy to block on, for example 1,300
passive-voice hits in the v12 docs alone, so the rules are written for
this repo, following the GitLab model. Each one links the guideline it
enforces.

Levels:
- error, fails CI: dashes, plain-word substitutions, condescending
  words, "please", "note that", hedges, puffery, optional plurals,
  exclamation marks, ellipses, curly quotes, generic link text, product
  spellings, sentence-case headings, repeated words, double spaces,
  implementation history in comments; in changesets, bold or italic
  emphasis, internal names and release-line names.
- warning: future tense, sentences over 30 words, "instead of" and
  "so that" tails in changesets.
- suggestion: passive voice.

.vale.ini scopes the rules. Docs get every rule. Contributor Markdown
and code comments keep dashes and headings as warnings until their
roughly 8,100 em dashes are rewritten. Released changesets stay in
.changeset/ and are not rewritten, so lint-prose.mjs lints only the
changesets added since PROSE_BASE (the PR base in CI).

The prose workflow installs a pinned, checksum-verified Vale release and
runs `pnpm lint:prose`. The rest of this change rewrites the existing
docs, READMEs, contributor Markdown, and code comments to pass the
error-level rules. Only prose and comment text changed. Renamed
headings keep their anchors.

Spelling stays with cspell, which checks identifiers that Vale does not
see.

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

* docs: keep prose lint from flattening meaning

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:48:09 +02:00
Zoltan KochanandClaude Fable 5.1 5154bf82ae fix(exe): embed Node.js 26.12.1 in the standalone builds (#17228)
The standalone builds embedded Node.js 26.0.0, which predates the
October 2026 CA distrust shipped in Node.js 26.12.1, so a standalone
pnpm still accepted certificates issued under the distrusted
intermediates. Node.js 26.12.1 is already the repo's devEngines
runtime and is in the lockfile.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-22 18:32:26 +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 33ad6f4f98 fix(importer): coordinate non-forced private imports and bound DirLock held files (#17182)
## Proposed Changes

- **Rust (`pnpm-deps-restorer`):**
  - Thread `swap_lock: Some(swap_lock)` through every `slot_import_opts` branch in `create_virtual_dir_by_snapshot.rs`.
  - Stage absent private targets (`!opts.safe_to_skip`) in `import_indexed_dir` and attempt a lock-free `rename_with_retry(&paths.stage, dir_path)` when `!opts.force` before falling back to `swap_lock.hold()`. Under the swap lock, leave a now-completed directory (or non-directory dirent) in place when `!opts.force` via `completed_or_non_dir_target`, or swap onto an incomplete partial directory.
  - Hold `swap_lock` before repairing an existing incomplete private target (`repair_existing_dir`, re-checking `marker_present` under the lock) or replacing a non-directory dirent at a private target (`replace_non_dir`).
- **Rust (`pnpm-fs` `DirLock`):**
  - Remove `<user_lock_root>/pnpm-dir-locks-<uid>/<hash>.held` on release while holding its exclusive OS lock (`LockedHeldFile::remove_and_unlock`), leaving no `.held` file behind after a clean lock lifecycle.
  - Verify via `path_still_names` after `try_lock()` in `try_lock_held_file_once` that the locked descriptor still names `held_path` on disk, reopening if the previous holder unlinked its `.held` file between `open` and `try_lock`, while keeping dead-holder takeover instant (an ungraceful exit leaves `.held` at `held_path`).
- **TypeScript (`@pnpm/fs.indexed-pkg-importer` in `pnpm11`):**
  - Replace `rename-overwrite` in `commitStagedDir` with `overwriteDirSync`, which moves an existing target directory aside on both POSIX and Windows (`${pathTemp(newDir)}_swap_${swapCounter++}`) instead of deleting it in place and spinning for 60s when Windows reports `EPERM` on a busy directory rename, and falls back to `renameOverwriteSync` only for non-directory targets.
  - Guard `tryExclusiveImport`'s failure cleanup with `sameCreatedDirectory` so it does not `rimrafSync(newDir)` after a concurrent importer has replaced or completed `newDir`.
  - Run `'concurrent forced imports into one target all complete and leave the package in place'` on Windows as well as POSIX.
- **TypeScript (`@pnpm/resolving.npm-resolver` in `pnpm11`):**
  - Pick before mirroring or caching in `persistFreshMeta` (matching `serveValidatedMeta`) so a malformed fresh packument is not written to the mirror in the background before `pickMatchingVersionFinal` validates it.

Closes https://github.com/pnpm/tasks/issues/156
Related to pnpm/pnpm#17091
Related to pnpm/pnpm#17113
2026-10-21 07:16:47 +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 0edcc9b1fd fix(cli): preserve project identity and paths when merging why trees (#17180)
Follow-up to https://github.com/pnpm/pnpm/pull/17175 (for https://github.com/pnpm/tasks/issues/169):

1. Preserve the originating workspace project identity on importer leaf nodes when building and merging reverse dependency trees across dedicated project lockfiles (`sharedWorkspaceLockfile: false`), so unnamed workspace projects (`the root project`) do not collapse into a single leaf.
2. Track the originating per-project package installation path(s) on merged dependent nodes and use them in `renderDependentsParseable` / `render_dependents_parseable` when `--long` is enabled, so `pnpm why --parseable --long` reports each project's own installed package path.
3. Add test coverage in both the TypeScript (v11) and Rust (v12) stacks for unnamed workspace projects, `--parseable --long` with per-project `modulesDir`, and running `pnpm why` from the workspace root without `-r`.

Written by an agent (Antigravity, Gemini).
2026-10-21 07:15:47 +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 c9b824d9d3 fix: require workspace root install after filtered install with pruneDirectDependencies (#17179)
When a filtered install runs with `pruneDirectDependencies: true`, the
workspace root is excluded from the installed importer set unless
explicitly selected, and its direct dependencies are not linked.

In pnpm v11, `computeMergedCurrentLockfile` previously copied
`filteredLockfile.importers` for unselected importers not present in
`currentLockfile`. Because `filteredLockfile` retains every importer
snapshot, the workspace root was written to
`node_modules/.pnpm/lock.yaml` with its dependencies populated, causing
`checkDepsStatus` (`verifyDepsBeforeRun`) to treat the root as installed
via the current-lockfile fallback even though `validation.manifests`
left `.` unrecorded. Carry unselected importers that have no prior
`currentLockfile` entry as empty `{ specifiers: {} }` entries on
filtered installs so `importerRecordsDependencies` reports them as not
installed.

In pnpm v12, wire `pruneDirectDependencies` through `pnpm-config`,
`install_dirs` selection, `RunExecution`, and `config_blocks_fast_path`,
and check `importer_records_dependencies` in `require_installed` when
falling back to `node_modules/.pnpm/lock.yaml` for an unrecorded
importer.

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

Written by an agent (Antigravity, Gemini 4 Argon).
2026-10-21 01:22:06 +02:00
Zoltan Kochan 46aa5131f1 fix(why): query dedicated project lockfiles when sharedWorkspaceLockfile is false (#17175)
In workspaces configured with sharedWorkspaceLockfile: false, recursive
pnpm why now loads each selected project's dedicated lockfile and
packageConfigs modulesDir in both pnpm v11 and pnpm v12 instead of
reading only a single lockfile at the invocation directory.

Closes https://github.com/pnpm/tasks/issues/169
2026-10-21 01:12:25 +02:00
Zoltan Kochan 1332461e87 fix(config): sync cross-version setting tables between v11 and v12 (#17176)
Add the 14 missing pnpm v12 WorkspaceSettings keys
(addMissingPeerTypes, autoDedupe, bootConfig, macosBackup,
nodePackageMap, permissions, pipelineBase, publishWaitTimeout, python,
remoteCache, saveTypes, skills, storeIndexFormat, and tools) to
SETTINGS_OF_OTHER_PNPM_VERSIONS in
pnpm11/config/reader/src/unknownSettings.ts so pnpm v11 annotates them
as pnpm v12 settings instead of offering unrelated spelling suggestions.

Sync the mirrored TYPED_WORKSPACE_MANIFEST_KEYS and
CONFIG_ONLY_SETTING_KEYS slices in pnpm/crates/config/src/known_settings.rs
with unknownSettings.ts (adding modulesDirsByProjectName and replacing
tools with tasks, since tools enters through WorkspaceSettings fields),
and add cross-check tests in both stacks verifying that every
WorkspaceSettings field is either in v11's KNOWN_SETTING_KEYS or in
SETTINGS_OF_OTHER_PNPM_VERSIONS.

Closes https://github.com/pnpm/tasks/issues/167
2026-10-21 00:20:57 +02:00
Zoltan Kochan 9dc367c201 fix: require current-lockfile proof for filtered installs without a wanted lockfile (#17174)
When pnpm-lock.yaml is missing after a filtered install (or when a covered
project is missing a per-project lockfile under sharedWorkspaceLockfile: false),
the recorded validation proof is not used and the current lockfile stands in as
the wanted lockfile:

1. In pnpm v11 (`@pnpm/deps.status`), gate the skip of
   `findProjectMissingFromCurrentLockfile` in `findModulesDirIssue` on a usable
   validation proof rather than the presence of `workspaceState.validation`, and
   verify in `readCurrentLockfileAsWanted` that the current lockfile records the
   direct dependencies of every checked project after a filtered install.
2. In pnpm v12 (`pnpm-package-manager`), check that the current lockfile records
   each unproven project and the snapshots of its direct dependencies both when
   comparing against a wanted lockfile and when the current lockfile stands in
   for a missing wanted lockfile.

Closes pnpm/tasks#180
Related to pnpm/pnpm#17150
2026-10-21 00:20:39 +02:00
Zoltan Kochan 674c3088b9 fix(setup): allow installation without registry access (#17170)
Handle registry read and environment-write failures in setup's existing
post-install outcome handler. Keep installation separate from environment
persistence, as the missing-shell-config fallback already does.

Print session-local PowerShell assignments and profile instructions when
registry persistence is unavailable. Preserve the original diagnostic and
leave environment conflicts and other failures fatal. Do not attempt to
replace an unreadable persisted PATH or elevate to another Windows user.

Apply the behavior in both CLI versions, with regression tests for read and
write failures and literal quoting of ASCII and typographic apostrophes.

Closes pnpm/pnpm#17167.
2026-10-20 22:26:48 +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 97266ee69b test: cover re-resolving dependencies that are also optional peers (#17166)
Add regression tests in both pacquet and TypeScript v11 verifying that
when a dependency declared also as an optional peer had been recorded
peer-resolved in an earlier lockfile, re-resolving one of its parents
retains the dependency on the peerless snapshot instead of dropping it.

Closes pnpm/pnpm#17161
2026-10-20 19:45:05 +02:00
Zoltan Kochan f319714a35 fix(install): hoist every workspace project on a filtered install without a lockfile (#17162)
With `useLockfile: false`, `linkPackages` built `hoistedWorkspacePackages`
from the selected projects only. It now receives every workspace project
(`ctx.projects`) as `allProjects`, matching `linkIsolatedLayout` in
deps-restorer and `plan_hoist` in pnpm v12.

Closes pnpm/tasks#168
2026-10-20 19:28:26 +02:00
Zoltan Kochan 245e7599e3 fix(hoist): allow symlinked node_modules to point to existing directory (#17158)
PR pnpm/pnpm#15198 added directory validation to reject hoist destinations
that point to symlinks escaping the modules root. However, it also rejected the
hoist root itself (such as `node_modules`) when symlinked to an external directory,
failing with "workspace hoist directory is not a real directory".

Allow a hoist root and its ancestors to be symlinks as long as they lead to an
existing directory. Every directory below a hoist root must still be a real
directory. In the Rust CLI the parent-directory preparation distinguishes the
two kinds through one create-or-validate routine. In the TypeScript CLI the
trusted root is the public hoist directory for destinations inside it and the
destination's modules directory otherwise, shared by the hoist and
deps-restorer packages, so an external virtual store behaves the same way in
both CLIs.

Resolves pnpm/pnpm#17157.
2026-10-20 19:28:07 +02:00
Zoltan Kochan e4d91d6d4c fix(licenses): align lockfile and empty result handling (#17165)
Keep license reporting scoped to importers recorded in the selected lockfile. An unrecorded workspace package contributes no dependencies; it must not crash or inherit the root importer's dependency list.

Require the lockfile in the Rust CLI, matching the existing TypeScript error. Render empty license reports according to the selected output format in both CLIs.

Closes pnpm/tasks#174.
2026-10-20 19:18:42 +02:00
Zoltan Kochan b6b53dbf28 fix: generate v11 SBOMs from dedicated workspace lockfiles (#17163)
Load dedicated lockfiles for selected and reachable workspace projects when
sharedWorkspaceLockfile is false. Re-key their importers to the workspace root
before SBOM collection, and merge package entries without losing required
package status. Reject conflicting package data to avoid emitting an inaccurate
SBOM. Fail when a selected or reachable project lacks a lockfile importer.
Read directory package metadata relative to the project that owns its lockfile.

Fixes pnpm/tasks#170.
2026-10-20 19:05:37 +02:00
Zoltan Kochan 473cccecef fix(prune): remove extraneous direct deps and honor workspace selection (#17154)
Fix three `pnpm prune` issues across pnpm v11 and pnpm v12:

1. Extraneous direct dependencies in `node_modules`:
   - In pnpm v11, `prune.handler` did not disable `optimisticRepeatInstall`, and `installDeps` could short-circuit via `canReportAlreadyUpToDate` before `headlessInstall` ran `prune()`. In addition, `pruneRemovedDependencies` skipped `prune()` when `currentLockfile` was absent (for example, when a project had zero dependencies). Disable `optimisticRepeatInstall` in `prune.handler`, prevent `canReportAlreadyUpToDate` when `pruneDirectDependencies` or `pruneStore` is set, and pass an empty `currentLockfile` to `prune()` when `pruneDirectDependencies` is enabled and no current lockfile exists.
   - In pnpm v12 (`pacquet`), `prune_stale_modules` only unlinked direct dependencies that were present in `current_lockfile` and removed from `wanted_lockfile`, never scanning `node_modules` on disk for extraneous entries when `prune` ran. Add `PruneTreeOptions::direct_dependencies` and `PruneExtraneousDirectDeps` to scan each selected importer's `node_modules` directory during `pnpm prune`, remove entries (and their bin shims and emptied scope directories) that are not wanted direct dependencies, excluded manifest links, or root public hoists, and force `commit_modules_state`'s virtual-store sweep during `prune`.

2. `pnpm --filter <pkg> prune` and `pnpm -r prune` in pnpm v11:
   - Separate `cliOptionsTypes` from `rcOptionsTypes` in `@pnpm/installing.commands` `prune` and add `'recursive': Boolean` so the CLI parser accepts `-r` / `--recursive` and `--filter`.
   - Do not pass `include` to `validateModules` when `pruneDirectDependencies` is set so `prune --prod` does not purge every workspace project's `node_modules` directory.
   - Do not auto-include the workspace root in `shouldInstallWorkspaceRoot` during filtered `prune`.
   - Scope `pruneDirectDependencies` to the selected `ctx.projects` and keep all dependency groups in `findOrphanDepPaths` / `getPkgsDepPathsOwnedOnlyByImporters` during a partial workspace `prune` so unselected workspace importers do not lose their devDependencies from the shared virtual store.

3. Workspace selection and `sharedWorkspaceLockfile: false` in pnpm v12:
   - Add `CliCommand::Prune(_)` to `CliCommand::recursive_by_default`.
   - Route `PrunePipeline` through `select_install_family_plan` (`PerProject`, `Shared`, `Single`) and pass `WorkspaceInstallSelection` (with `install_dirs` restricted to `selected_dirs`) into `Install::run_prune`.

Closes https://github.com/pnpm/tasks/issues/171
2026-10-20 17:07:56 +02:00
Zoltan Kochan f8ba6e18e7 fix(patching): support patching in workspaces with sharedWorkspaceLockfile false (#17155)
Fix `pnpm patch`, `pnpm patch-commit`, `pnpm patch-remove`, and `pnpm install` in workspaces with `sharedWorkspaceLockfile: false` in both pnpm v11 (`pnpm11/`) and pnpm v12 (`pnpm/`):

1. `pnpm patch`: When `sharedWorkspaceLockfile: false` and the target directory has no current lockfile, merge `packages` and `snapshots` across all workspace projects' current lockfiles to resolve the installed package version. Anchor `patchesDir` to `workspaceDir ?? lockfileDir`.
2. `pnpm patch-commit`: Search workspace project `modulesDir`s and `<editDir>/../..` when resolving the patched package directory, write patches relative to `workspaceDir ?? lockfileDir` without mutating `opts.patchedDependencies` to relative paths before delegating to `install`, and update lockfile snapshots across all workspace projects.
3. `pnpm patch-remove`: Resolve patch files relative to `workspaceDir ?? lockfileDir` and run a workspace-wide dedicated-lockfile install when `sharedWorkspaceLockfile: false`.
4. `pnpm install`: Skip per-project unused-patch errors during individual workspace project installs when `sharedWorkspaceLockfile: false`, and verify patches across the union of all workspace project lockfiles once the full workspace install finishes.

Closes pnpm/tasks#176
2026-10-20 16:56:23 +02:00
Zoltan Kochan 81b146c1f9 fix(v11): recognize externalDependencies and autoInstallPeersFromHighestMatch in pnpm-workspace.yaml (#17153)
In pnpm v11, externalDependencies and autoInstallPeersFromHighestMatch
were omitted from PnpmSettings, OptionsFromRootManifest, and
TYPED_WORKSPACE_MANIFEST_KEYS, and were not included in the
Pick<Config, ...> option types for install commands.

Setting externalDependencies in pnpm-workspace.yaml therefore emitted
an unrecognized-setting warning and passed a raw string[] through to
`@pnpm/installing.linking.real-hoist`, which called
externalDependencies.has() and crashed hoisted installs with a
TypeError.

- Declare autoInstallPeersFromHighestMatch and externalDependencies on
  PnpmSettings, OptionsFromRootManifest, and
  TYPED_WORKSPACE_MANIFEST_KEYS (plus its mirror in
  pnpm/crates/config/src/known_settings.rs).
- Validate both settings in getOptionsFromPnpmSettings and normalize
  externalDependencies into a Set<string> on OptionsFromRootManifest.
- Convert Set config values to arrays in configToRecord so
  pnpm config get externalDependencies outputs a JSON array matching
  pnpm v12.
- Accept Iterable<string> for externalDependencies in
  `@pnpm/installing.linking.real-hoist`.
- Thread autoInstallPeersFromHighestMatch and externalDependencies
  through InstallCommandOptions, InstallDepsOptions, and
  RecursiveOptions.

Closes https://github.com/pnpm/tasks/issues/166
2026-10-20 15:00:37 +02:00
Zoltan Kochan d11a2a6a5a fix(config): centralize reconciliation of config dependencies (#17149)
Separate config dependency bootstrap from install-time reconciliation. Only
reconciliation removes undeclared roots; diagnostic commands must retain them
so they can inspect the existing lockfile.

Share state loading, declared-root retention, and env graph pruning between
config dependency preparation and exclusion pruning. Load state before opening
the store and pass it into resolution. The Rust repeat-install fast path checks
this state so removing the last declaration cannot bypass cleanup.

Reuse existing verification, writing, and materialization for removals. Frozen
installs reject stale roots before mutation. Preserve package-manager roots,
including distinct versions of a package shared with config dependencies.
Keep Rust graph operations in the lockfile crate to avoid a dependency cycle.

Follow-up to pnpm/pnpm#17144.
2026-10-20 12:46:21 +02:00
Zoltan Kochan dc34aeb06e fix(deps.status): apply overrides and hooks when checking modified manifests (#17148)
During install, pnpm applies readPackageHook (built via createReadPackageHook
from overrides, packageExtensions, readPackage hooks,
ignoredOptionalDependencies, and ignoreCompatibilityDb) to each project's
manifest before writing importer specifiers to pnpm-lock.yaml.

When verifyDepsBeforeRun or optimistic repeat install checked a modified
manifest via assertWantedLockfileUpToDate in TypeScript, it compared the
unhooked manifest against wantedLockfile.importers[projectId] and
linkedPackagesAreUpToDate, causing false outdated reports whenever overrides,
packageExtensions, or readPackage hooks rewrote declared ranges. Apply
createReadPackageHook to a cloned copy of the project manifest before
satisfiesPackageManifest and linkedPackagesAreUpToDate, pass
resolution/injectWorkspacePackages/hookedManifests to
linkedPackagesAreUpToDate, reuse parsedOverrides and readPackageHook across
projects when sharedWorkspaceLockfile is true, and isolate per-project
packageConfigs overrides and hookedManifests caches when
sharedWorkspaceLockfile is false.

In the Rust pnpm v12 optimistic repeat install check, check_importer_satisfies
already applied VersionsOverrider, but linked_packages_are_up_to_date still
inspected the un-overridden project manifest. Apply VersionsOverrider before
linked_packages_are_up_to_date so overridden workspace links stay up to date
when a manifest is edited.

Closes pnpm/pnpm#17127
2026-10-20 11:04:07 +02:00
55842484d6 fix(lockfile): check transitive local directory dependencies on a frozen install (#17141)
A directory dependency whose spec still matches can itself be stale: its own dependencies may have changed without the directory moving. Recurse into directory children when checking local directory dependency freshness, carrying the peer-alias scope of every ancestor and checking each snapshot once per peer scope. Apply the same scope handling to the Rust check. Links have no dependency snapshot and keep their current treatment.

Fixes pnpm/pnpm#17128.

---------

Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:06:04 +02:00
d5a07e00bb fix(lockfile): accept an aliased file: child of an injected package (#17136)
An injected package's file: child installed under another alias is
recorded as name@file:<path>. getLocalPath only read refs that start
with file: or link:, so the freshness check could not compare the path
and reported a lockfile pnpm had just written as outdated, failing the
next frozen install. It now also reads the path from a name@file: ref.

Closes pnpm/pnpm#17130

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 10:04:39 +02:00
Zoltan Kochan e5f2b88e10 fix(install): preserve config dependency exclusions during pruning (#17144)
Include the env lockfile's snapshots in the resolved-version set used to
prune workspace exclusion lists. Config dependencies and their transitive
dependencies are recorded in that leading document and were missing from
the main-lockfile-only scan.

Read the env document at the configured root only when exclusion pruning
is enabled. Filter out removed config dependency roots and reuse the env
graph pruner before merging versions with the relevant project lockfiles.
Move the Rust graph helper into the lockfile crate for reuse without a
dependency cycle. Keep manifest writes after the fallible env read.

The earlier per-project-lockfile fix established that pruning needs the
union of all resolved packages. Extend that same boundary to both lockfile
documents, with regression coverage across the lockfile layouts and a
frozen reinstall after pruning.

Closes pnpm/pnpm#17142.
2026-10-20 10:03:00 +02:00
Zoltan Kochan c89c01651f fix(lockfile): reject a changed tarball URL or git commit under a local directory dep (#17143)
A frozen install kept whatever the lockfile recorded for a non-range
child spec of a local directory dependency, comparing only the package
name. A tarball URL or a full git commit names one exact source, and the
lockfile keys the child by that URL or commit, so a spec naming a
different one is now outdated in both CLIs. Git specs are classified
with the git resolver's parser first, so https git URLs and &path:
parameters are handled. Moving selectors and an
unchanged URL keep the recorded resolution on a frozen install, as
decided in pnpm/tasks#145. For a git spec the commit and the path:
subdirectory are compared, not the repository.

Closes pnpm/pnpm#17129
2026-10-20 09:41:51 +02:00
Zoltan Kochan 78943e002b fix(deploy): unify self-containment synthesis and preserve deployed directory resolutions (#17140)
A shared-lockfile deploy synthesizes three views of the deployed
workspace: the in-memory install config used during deploy, the deployed
`pnpm-workspace.yaml`, and the deployed `pnpm-lock.yaml` settings. When
those views drifted or when post-deploy lockfile verification
re-compared deployed `<name>@file://...` directory snapshots against raw
on-disk `package.json` manifests without the source workspace's
catalogs, overrides, `packageExtensions`, `readPackage` hooks, or
singleton peer bindings, post-deploy verification and frozen installs
failed. Unused workspace `patchedDependencies` were also copied into the
deployed lockfile and workspace manifest.

Both pnpm v12 and pnpm v11 now project the deploy install config,
deployed `pnpm-workspace.yaml`, and deployed `pnpm-lock.yaml` settings
from a single self-contained specification, prune unused
`patchedDependencies` using the shared applied-patch-keys helper, and
skip raw on-disk manifest re-comparison for deployed `<name>@file://...`
directory snapshots once their snapshot entry is verified.

Closes pnpm/tasks#165
2026-10-19 23:30:44 +02:00
Zoltan KochanandClaude Opus 5.5 e3b510dbfd fix: align the pre-switch config read, lockfileDir pin sync, and pre-command argv scan (#17134)
* fix: align the pre-switch config read, lockfileDir pin sync, and pre-command argv scan

v11:
- A pnpm-workspace.yaml with an unreadable setting no longer drops its
  pin-decision settings (pmOnFail, lockfileDir, ...). After the whole file
  fails, those keys are applied alone, as v12 does.
- The env lockfile is read from and written to config.lockfileDir, so a
  lockfileDir set in pnpm-workspace.yaml no longer leaves a stray
  pnpm-lock.yaml at the workspace root.

v12:
- An undeclared option before the command name is read as a flag without
  a value, as v11 reads it, so `pnpm --new-flag install` switches to the
  pinned pnpm instead of failing with clap's unknown-argument error.

The triple Config load flagged in the task only happens on the run that
first records the pin, so it is left as is.

Closes pnpm/tasks#162

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

* fix(cli): read an undeclared option as a flag only right before a command name

Keeps `pnpm --undeclared value config list` ambiguous, so it is not read
as the command `value` and does not slip past the config-command exemption.

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

* fix(cli): satisfy clippy or_fun_call

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

* fix(config): keep pmOnFail when the packages setting cannot be read

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

* fix(config): resolve a relative lockfileDir from pnpm-workspace.yaml against the workspace

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:02:25 +02:00
Zoltan Kochan 26980c1615 fix: check peer suffix bindings in hashed peer recovery and deploy (#17124)
Since pnpm/pnpm#17067, packages: records every declared peerDependency
even when resolved from a package's own dependencies rather than bound
in its peer suffix.

Filter descendant declared peer edges through BoundProviders and
getBoundPeerProviders during hashed peer suffix transitive recovery
and deploy singleton peer ancestor walking so unshadowed own
dependencies declared in peerDependencies are not misclassified as
peer bindings.

Closes https://github.com/pnpm/tasks/issues/143
2026-10-19 13:08:47 +02:00
Juan Manuel Martinez Ortiz 5f8b57dec9 fix(store): grant group bits to unreadable directories on macOS (#17104)
On macOS, Node's fs.lchmodSync opens the path with O_WRONLY | O_SYMLINK,
which fails with EISDIR for every directory. A new store directory that
its owner cannot read, as under a 0o477 umask in a group-writable store,
reached that call and failed the install.

Change the mode through a descriptor opened with O_SEARCH, which needs
no read access, and fchmod it. O_NOFOLLOW and O_DIRECTORY keep the
guarantee that a directory swapped for a symlink is not followed. The
macOS and Linux branches now share one helper and differ only in how
they open the handle and change its mode. The helper compares the owner
with the effective uid, which owns the directories pnpm creates, as pnpm
v12 does.

The existing cafs test for a umask that removes owner read covers it.
CI has no macOS job for the TypeScript CLI, which is why it never ran
there. pnpm v12 already uses O_SEARCH with fchmodat and is not affected.
2026-10-19 02:21:58 +02:00
Zoltan Kochan ca18902a25 refactor(deps-resolver): one owner for peer provider decisions (#17114)
Part of pnpm/tasks#143.

- Move the peer-shadowing rule and the provider lookup with lockfile-pin
  precedence into one owner: provider_for/ProviderPick in peer_provider.rs
  and providerFor in v11 hoistPeers.ts.
- Read and write peer suffixes only through PeerBindings (pnpm-lockfile) and
  parsePeerBindings (@pnpm/deps.path), with a round-trip test over the
  repo's lockfiles.
- v11 picks hoisted optional peers through v12's lockfile tiers.
- Report a cached node's own missing peers in every importer, so a project
  no longer borrows an out-of-range peer from another project's subtree
  (v12, resolvePeersFromWorkspaceRoot: false).
- Classify optional-peer edges by the bindings the key records, so a prod
  install and deploy keep a package's own dependency that is also its
  optional peer (v11 and v12).
- Add proptest graph tests on StubResolver.

Reusing a pin only when the owner returns it (step 2) is left out: deciding
reuse after the peer walk cost 40-60% on lockfile-only installs.
2026-10-19 02:17:20 +02:00
Anzal Husain AbidiandZoltan Kochan 60bd9778ec fix(deps-status): report a missing project lockfile as outdated with sharedWorkspaceLockfile false (#17017)
With sharedWorkspaceLockfile set to false, checkUnchangedWorkspace returns
undefined when a project's pnpm-lock.yaml is missing and there is no shared
lockfile to restore, so the full check can throw
RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND. The caller then ran validateModifiedWorkspace
with the modified projects only. When no manifest or lockfile was modified that
list is empty, no project lockfile was read, and the gate reported the
workspace as up to date.

The lockfile scan now records the directories without a wanted lockfile, and
with a lockfile per project the gate also checks the projects whose lockfile is
missing, so the per-project reader reports it. Only the projects the install
the gate spawns covers are held: a filtered install leaves the projects it did
not select without a lockfile, and an install with the same filter cannot
create one. The coverage rule is shared with the recorded-validation path.

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

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-18 23:10:53 +02:00
Alessio AttilioandZoltan Kochan b9e1749f48 fix(importer): tolerate concurrent package replacement (#17091)
Concurrent installs into one project raced on the package import's
stage-and-swap: the second importer's rename onto the target failed with
ENOTEMPTY once the first had put its own staged copy in place.

Rust: a forced import stages the package. A target the import found
absent is taken by one rename of the stage, so a fresh forced install
pays no lock. An occupied target is replaced under a per-target swap
lock (keyed by the resolved target path, under the per-user lock
directory), so swaps of one target are serialized. The stage and the
target are exchanged atomically where the platform allows it (a new
pnpm_fs::exchange_dirents: renameat2 RENAME_EXCHANGE on Linux,
renamex_np RENAME_SWAP on macOS), so the target path is never absent
mid-swap there, which is what makes the lock-free rename of an absent
target safe. Elsewhere, and on filesystems without the operation, the
old tree is moved aside by a rename and the stage renamed in; the
vacancy is two renames long, and an importer that saw the target absent
has to stage its whole package before its own rename can land in it.
The old tree is removed afterwards; one that cannot be removed stays
next to the target and the install warns the user to remove it, where
it used to fail before replacing anything. A lock that cannot be taken
in time or at all is logged at debug and the swap proceeds unserialized.
A rename that still collides is accepted when the target already holds
exactly this import, compared by entry set, file content, and file mode
(allowing the linkable hardlink modes the store can produce), and
retried otherwise. Forced imports of shared slots keep the in-place
path: nothing may remove a shared slot.

TypeScript: after a failed staged rename, the destination is accepted when
it holds exactly this import (allImportedEntriesMatchExactly, retried a
few times because a concurrent import mid-swap leaves the destination
absent for a moment), and a preserved node_modules/ moved into the stage
is moved back. mismatchReason compares file modes against the mode the
import would apply.

Closes pnpm/tasks#48.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-18 23:10:01 +02:00
Juan Manuel Martinez OrtizandZoltan Kochan 67be8f1522 test(config.reader): keep the global config.yaml out of the tests (#17108)
pnpm v11 reads the global config.yaml from $XDG_CONFIG_HOME/pnpm, or on
macOS from ~/Library/Preferences/pnpm when XDG_CONFIG_HOME is unset.
Four config.reader tests assert built-in defaults such as
minimumReleaseAge 1440, so a developer whose global config sets that
setting saw them fail with their own value. The test file now points
XDG_CONFIG_HOME at an empty temporary directory and removes it after
the run. The isolation stays in this file: setting it in the shared
jest preset would also move git's global config for every suite.

"global config.yaml registries cannot redirect pnpm_config__auth
routes" passed XDG_CONFIG_HOME only in the env given to getConfig, but
the global config dir is resolved from process.env, so the config.yaml
the test wrote was never read and the test passed with the routing
guard removed. It now sets process.env.XDG_CONFIG_HOME while getConfig
runs.

Five tests reimplemented the save/set/try/finally/restore of
XDG_CONFIG_HOME that the file's withEnv helper already provides. They
now call withEnv, which returns the callback's result.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-18 21:53:34 +02:00
Zoltan Kochan 85f7fa1e98 feat(pnpr)!: give the hosted store a layout with one place per thing (#17100)
The hosted store shared one key space between packages, orgs and blobs:
a registry without an org stored packages at the store root, an org's
registry stored them in `<org>/`, a package's blobs sat beside its
document, and a nested image repository sat inside its parent's
directory. So an org collided with an unscoped package of the same name
(an unpublish of that package removed the org's whole namespace), the
implicit `<ecosystem>~<name>` org of a grouped registry was a valid npm
name, and OCI GC and verify needed ad-hoc exclusion lists. On S3,
object_store percent-encodes `~` in keys while the upload sweep compared
raw prefixes, so grouped OCI registries were never swept. This is part
of section A4 of pnpm/tasks#131.

The new layout (pnpr_storage::layout), the same on disk and in a bucket:

  layout.json                          {"layout": 1}
  default/packages/<name>/document.json
  default/packages/<name>/blobs/<file>
  orgs/<org>/packages/...

Every name, org and file name is one segment: `[a-z0-9._-]` as is,
every other byte as `!` and two hex digits. That keeps nested names
siblings, separates names that differ only in case on case-insensitive
filesystems, and leaves nothing for object_store to escape, so a key is
byte for byte the local path. Per-namespace records stay inside their
namespace; records every namespace shares stay at the root. Listing is
one directory read, so the `.package-index` workaround is gone, and the
blob inventory reports package and blob names instead of paths.

Startup refuses a store marked with another layout or an unmarked store
holding data, and marks an empty one. The proxy cache uses the same
package layout and is emptied once when its marker is missing.
`pnpr upgrade-storage` converts an earlier store on disk or in a bucket,
telling org directories from packages by the configured orgs; it moves
blobs before documents so an interrupted run can resume, and refuses a
directory that is both an org and a package. export, verify and
migrate-storage check the layout, and migrate-storage marks its target.

The registry fixtures the pnpm test suites serve are built in the new
layout, and the cached build is keyed by the layout version.
2026-10-18 21:48:58 +02:00
Zoltan Kochan 0b82b33a58 fix(deps-resolver): dedupe an injected dependency against the target's locked deps too (#17106)
`dedupeInjectedDeps` turns an injected workspace dependency into a
`link:` when the copy's children are all provided by the workspace
package itself. It read the package's children from what this install
resolved. The resolver skips a dependency that is installed and not
updated, so on an install that left the package's dependencies
installed, the map was incomplete and the copy stayed `file:`. A fresh
install, or one with `--lockfile-only`, resolves every dependency and
links it.

The target's dependencies are now the resolved ones plus the locked ones
of the aliases this install skipped.

pnpm v12 already converges here.

Closes pnpm/pnpm#17094
2026-10-18 21:18:40 +02:00
Zoltan KochanandClaude Opus 5.5 e5a924c0df fix: finish the command when stdout is a closed pipe (#17107)
When the reader of stdout had exited, pnpm v11 called `process.exit(0)` at
the first failed write. That replaced the command's own exit code and
skipped the work left after the write: `pnpm outdated | true` exited 0,
and `pnpm clean --lockfile | true` printed its first "Removing" line and
stopped before removing anything.

The stdout error handler now ignores EPIPE, so the remaining output is
dropped and the command finishes with its own exit code. pnpm v12 does
the same since pnpm/pnpm#16932.

Closes pnpm/tasks#133

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:42:29 +02:00
Zoltan Kochan 78219981bb fix(deploy): record the peers of the workspace packages a deploy copies (#17098)
A deploy turns each workspace project the deployed one depends on into
a `file:` package entry, and that entry dropped the project's declared
peers. Every other `packages:` entry records them since
pnpm/pnpm#17067, and pnpm v11's frozen check requires them for a local
directory dependency, so a frozen install of the deploy directory failed
with ERR_PNPM_OUTDATED_LOCKFILE when a copied workspace package declared
a peer and `injectWorkspacePackages` was off. pnpm v12 wrote the same
lockfile, but its frozen check accepted it.

Both stacks now build the entry's `peerDependencies` and
`peerDependenciesMeta` with the function the lockfile writer uses:
`lockfile_peer_blocks` in v12 and `getLockfilePeerDependencies` in v11,
both in deps-resolver.

Closes pnpm/pnpm#17093
2026-10-18 20:02:19 +02:00
Zoltan KochanandClaude Opus 5.5 4d50c05418 test(deps-installer): make catalog snapshot tests independent of the latest tag (#17097)
Three catalog snapshot tests expected `^100.0.0` to resolve to `100.1.0`.
That holds only while `latest` of `@pnpm.e2e/bar` and `@pnpm.e2e/foo`
points at `100.1.0`. Other test files move that tag on the shared registry
mock from parallel Jest workers, and the resolver then picks `100.0.0`.

The tests now use `^100.1.0`, which resolves to `100.1.0` whatever the tag
is. They check how catalog snapshots carry across installs, not range
resolution, so the range itself does not matter to them.

Closes pnpm/tasks#152

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:11:46 +02:00
Juan Manuel Martinez OrtizandZoltan Kochan ae349ec502 feat: cap the unpacked size of package archives with maxTarballUnpackedSize in v11 and for zips (#17014)
A gzip bomb inflates a kilobyte-sized tarball into hundreds of gigabytes
during extraction. pnpm v12 bounds what an archive may unpack to with
maxTarballUnpackedSize since pnpm/pnpm#17054; pnpm v11 had no such
bound, so an install could still exhaust the disk.

pnpm v11 now adds up the sizes that the regular-file entries of a
package archive declare in their tar headers and fails the extraction
with ERR_PNPM_TARBALL_TOO_LARGE once the total passes
maxTarballUnpackedSize (default 4 GiB). Header sizes stand in for
written bytes: the tar parser never yields more payload than a header
declares, so their sum bounds what can reach the store, and checking it
before the payload is read keeps the crossing entry out of the store.
Entries the ignore filter drops count as well. The error is
deterministic for a given archive, so the fetch retry loop does not
retry it. The zip archives that runtimes are installed from are held to
the same limit. The setting name, default, error code, message and hint
match pnpm v12.

pnpm v12's zip extractor bounded each entry by its central-directory
size but not their sum. Every stored zip entry now counts against the
payload ceiling of the TarLimits that tarballs use.

The "Added in" line in the v11 docs assumes the next release is 11.28.7.

Completes https://github.com/pnpm/tasks/issues/81.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-18 19:05:42 +02:00
Zoltan Kochan 453870cc3f fix: keep a package's links to its own files in synced injected copies (#17088)
The install keeps a package's links to its own content as links in an
injected copy, following the importer's rules: a link to an imported
path is recreated, a link to a file the import leaves out becomes that
file, and a link to any other directory is left out. The sync after a
script listed in syncInjectedDepsAfterScripts, and the edit watcher,
followed links instead:
- v12: a directory link the script added became a real directory, and
  under deployAllFiles it went missing.
- v11: the same materializing in packlist mode, and the sync failed on
  a copy that links to one directory twice (EEXIST) or links back to an
  ancestor (ELOOP). v11's walk of every file also had no cycle guard,
  so an install with deployAllFiles failed on such a link.

Both syncers now read the source with the install's symlink policy and
the copy without following links, record a link by the package-relative
path it points at, and recreate it with the importer's rules. v12 moves
relative_link_target and symlink_file_or_copy from the importer into
pnpm-fs to share them. v11 exports internalSymlinkTarget and
copyInternalSymlink from fs.indexed-pkg-importer, and adds a
preserveSymlinks option and an ancestor guard to the directory
fetcher's walk.

v11's install under deployAllFiles still materializes directory links,
where v12 keeps them. Each sync matches its own install.

Part of pnpm/tasks#147.
2026-10-18 18:52:01 +02:00
JongKyung LeeandZoltan Kochan 391a972739 fix(owner): list maintainers from package metadata (#16953)
Read maintainers from full package metadata when listing owners in both CLI implementations.
The npm registry does not serve the owners endpoint used by the command.

Preserve registry selection, authentication, and error handling. Accept absent maintainers
and display the name when a maintainer has no email.

Allow up to 50 MiB for Rust metadata responses because they contain every published version.
Reject truncated responses explicitly. Update the tests to use the actual metadata protocol.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-18 18:32:33 +02:00