Commit Graph
424 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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
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
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 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 7b0d6873a6 feat: pin tar hashes with lockfile.integrityTarget and address revisions by tar digest (#17092)
lockfile.integrityTarget: tar makes the integrity of every package tarball
entry the hash of its uncompressed tar, recorded as settings.integrityTarget.
The lockfile then survives a registry recompressing its tarballs, switching
to tar resources, or being replaced by a mirror that does either. A lockfile
recorded under another target is stale: a frozen install fails with
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH, and any other install re-resolves with
update-checksums semantics so no hash of the old kind carries forward. The
setting is opt-in and the lockfile version is unchanged.

The tarball crate now reports both hashes of every extraction: the resource
after content decoding, and the tar a .tgz decodes to. A pin names one of
them (IntegrityKind::Resource / Tar), or either at resolution, where a locked
or published integrity may be of either kind (ResourceOrTar, sha512 only).
Under a tar pin every gzip member is decoded and nothing may follow the tar.
The resource pin keeps today's behaviour, extracting the first member and
tolerating trailing data, but then records no tar.

Store rows extracted from a .tgz record tarIntegrity and get an alias under
the tar key, in a table pnpm v11 never reads. Store lookups follow aliases,
so a tar-pinned frozen install is served offline by a store that .tgz
installs populated, and resolution takes a version's tar hash from its row
before downloading anything.

Registry revisions now always pin their tar, revision 0 included, and are
fetched with a tar pin, so the digest route can use any content coding.
Revision metadata must list revision 0 and the selected revision, and a
digest-route URL without a revision is rejected. pnpr keys its digest
references by tar hash on every registry format, serves the route as a
negotiated tar resource, and projects revision 0 explicitly. pnpm v11 cannot
extract tar resources, so it now rejects revisions with
ERR_PNPM_REGISTRY_REVISIONS_UNSUPPORTED.

Implements pnpm/rfcs text/0000-tar-integrity-for-every-source.md and
pnpm/rfcs#42.
2026-10-18 17:36:30 +02:00
Zoltan Kochan 430d892741 feat(pnpr)!: serve every ecosystem under its own prefix (#17087)
pnpr served npm at the server root when npm was the only configured
ecosystem and moved it under /npm/ as soon as a second ecosystem was
added. Adding an ecosystem therefore broke every .npmrc registry URL,
every tarball URL already written into a lockfile, named-registry URLs,
and OIDC workload publish paths, and /npm meant the package `npm` on one
server and the ecosystem prefix on another. This settles section A1 of
pnpm/tasks#131: every ecosystem is always prefixed.

- Ecosystem::base_path is now independent of the configuration. The
  per-configuration Registries::base_path and is_only_ecosystem are gone,
  and the routes, the resolver's tarball URLs, OIDC workload paths and
  pnpr's own-origin route classification all use it.
- The default image registry still answers at /v2/ on the host root; an
  image client cannot be pointed at a prefix. The registry directory keeps
  its `prefixed` and `namedPrefixed` flags for the web UI.
- The read-only token allowlist matched the npm audit endpoints at the
  root only, so read-only tokens were already refused for audits on
  multi-ecosystem servers. It now matches them under the npm prefix.
- /metrics no longer switches between Prometheus metrics and the npm
  package `metrics` on the Accept header; the package is /npm/metrics.
- The pnpm v11 and v12 test harnesses, the registry-mock crate and the
  integrated benchmark address the fixture registry at /npm/ and keep the
  server origin for the pnpr protocol endpoints. The benchmark asks each
  revision mock's registry directory whether npm is prefixed, so it can
  still compare against a pnpr revision from before this change.
2026-10-18 16:55:55 +02:00
Zoltan Kochan cfae1ffae7 fix(resolver): let a lockfile pin break ties instead of outranking the graph (#17090)
The version picker ranks the candidates for a range by the summed weight
of their preferences. The lockfile pin weighed 1,000,000, a manifest
specifier 1,000, and a version resolved higher in the dependency chain 1.
A fresh install has no lockfile, so it picks the version the chain
already resolved, while a reinstall kept the pinned version and wrote a
second copy of the package. Replacing a dependency therefore left a
lockfile that a fresh install of the same manifests does not write.

The pin is now the lowest tier: a version named on the command line,
then a manifest specifier, then a version resolved higher in the chain,
then the lockfile pin. Each tier is a digit of a base-1000 number, and
`pinned_by_lockfile` / `pinnedByLockfile` reads the lowest digit where
the code used to compare against the old lockfile weight. A pin that no
other preference competes with still wins against the newest version, so
a re-resolved dependency keeps the locked versions of its own
dependencies (pnpm/pnpm#2191). v12 gets a port of that test.

The pins `pnpm import` reads from another package manager's lockfile and
the version a command names move to the top tier, so they keep
outranking every manifest range. The v11 `audit --fix update` penalties
are derived from the manifest weight, so they keep their place relative
to it. The `dedupe` option of the v11 preferred-versions seed no longer
changes anything and is removed.

The peer convergence harness no longer tracks a divergence for
pnpm/pnpm#17084.

Also fixes an order-dependent v11 test that read a dist tag other tests
move.

Closes pnpm/pnpm#17084
2026-10-18 16:39:03 +02:00
Zoltan Kochan a57f15c103 fix(deps-resolver): move an auto-installed peer when any provider rejects its pin (#17077)
A workspace project's auto-installed peer kept its locked version as long
as one other project's specifier for that package still accepted it. When
a third project then added a higher version, a fresh install picked that
higher version but an install with the lockfile did not, so the result
depended on the order in which the projects were edited.

Both stacks now release the pin when any overlapping specifier rejects it.
A released peer is resolved again from the importer's preferred versions,
and those still gave the lockfile's weight to every other locked version.
A version another project locks under an exact specifier therefore
outranked a higher version that only a new specifier names. Releasing a
pin now also removes the lockfile's weight from the versions a project
names exactly. Their specifier still weights them the way a fresh install
does. A version locked under a range keeps the lockfile's weight, so the
peer still lands where such a provider is locked.

Moving the peer down when a project adds a lower version is unchanged. A
fresh install of the same manifests picks that version too, and
pnpm/tasks#61 relies on it.

The peer convergence harness no longer tracks a divergence for this
scenario.

Closes pnpm/pnpm#17058
2026-10-18 11:48:08 +02:00
Zoltan Kochan 9a09d43423 fix(lockfile): check an injected package's dependencies on a frozen install (#17065)
A frozen v11 install skipped comparing the dependencies of an injected
workspace package with its lockfile snapshot, because that comparison
used its own spec rules and reported lockfiles the resolver had just
written as outdated. With per-project lockfiles nothing else catches a
changed dependency, so the stale versions were installed.

The comparison now takes its rules from the resolver, as v12's does:
- the children are those getNonDevWantedDependencies walks, after the
  read-package hook and prepareDependencyManifest (peers the parent
  supplies dropped, engines.runtime added);
- recorded peers are the declared peers (getPeerSet().declared),
  catalog-aware; an entry written before every declared peer was recorded
  is accepted too;
- a locked version is judged by lockedVersionSatisfies, built on
  getRangeOfLockedPackage, and a spec it cannot judge falls back to a
  name match;
- an optional child the frozen install could not resolve is accepted,
  unless the lockfile records it as required;
- a directory without a package.json is described as the resolver
  describes it.

A spec the lockfile cannot judge (a tag, a git or tarball spec) is
accepted only on an explicit --frozen-lockfile install, which never
resolves; any other install resolves it again, as v11 always did. The
lockfile does not record the spec an injected child was resolved from.
The flag that limited unresolved optional dependencies to that install
is renamed to frozenLockfile (frozen_lockfile in v12).

With the false positives gone, the frozen install no longer skips the
check, except the one pnpm deploy runs: a deploy lockfile records bound
peers as plain dependencies and already carries the effects of hooks and
overrides, so it cannot be judged against the manifests.

v12 gets three related fixes: a dependency that became optional is outdated
on a frozen install, and a directory without a package.json is read as
the resolver reads it (its name alone), so a frozen install accepts the
lockfile written for it. A changed git, tarball, or tag spec of an injected
child is now resolved again instead of keeping the old version.

Closes https://github.com/pnpm/pnpm/issues/17008
2026-10-18 09:46:10 +02:00
Zoltan KochanandClaude Opus 5.5 0cef240132 fix(deps-resolver): record an injected workspace dependency as it resolves (#17075)
A plain install kept a workspace dependency's previously recorded
`link:` whenever the run did not target it, even when the injected copy
now binds a peer the linked project does not provide. The guard was
added for pnpm/pnpm#10433, whose flips came from two spurious
mismatches: a same-version peer-suffix decoration, which the dedupe
pass now absorbs (pnpm/pnpm#12800), and re-resolution paths the dedupe
pass did not reach. With the guard off, the update scenario from
pnpm/pnpm#10433 keeps its `link:` in both stacks, so dedupe alone now
covers it. The guard's only remaining effect was that an incremental
install recorded a stale `link:` that a fresh install does not, which
left the injected package unable to find the peer its consumer provides.

The guard and the inputs only it consumed are removed in both stacks:
v12's `preserved_link_version`, `LockfileImporterReuse` and the
install path's `guard_*` fields, and v11's preserve-prior-link branch
with its previous-ref and targeted-alias bookkeeping. The importer entry
now follows the dedupe pass, which the workspaces docs already describe.

The v11 ancestor-divergence test now expects the injected copy, a new
v12 CLI test checks that an incremental install records the copy a
fresh one does, and the peer convergence harness drops this divergence.
The two rows still differing there come from pnpm/pnpm#17058's moved
auto-installed peer, whose divergence now owns the injected entries
that follow from it.

Closes pnpm/pnpm#17057

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:45:00 +02:00
Zoltan Kochan 7151c3ecbb refactor(deps-resolver): carry each package's peers in one PeerSet (#17070)
A package's declared peers and the peers it takes lived apart:
`ResolvedPackage::peer_dependencies` and the graph edges held only the
peers taken, and the lockfile writer re-read the manifest to recover the
declared ones and dereference their `catalog:` ranges itself.

`PeerSet` now holds both, and only `PeerSet::new` derives the peers taken
from the declared ones and the children owner's shadowing decision.
`extract_peer_dependencies` builds it with every declared range
dereferenced, `ResolvedPackage` and `ResolvedDependencyEdges` carry it,
and the writer records `declared()` instead of recomputing it. The
directory-dependency freshness check derives its peers taken through
the same type.

v11's `ResolvedPackage` likewise replaces `peerDependencies` and
`declaredPeerDependencies` with one `peers: PeerSet`, built by
`getPeerSet`. Its lockfile writer records `peers.declared` and the peer
resolution reads `peers.taken`.

Lockfiles are unchanged. One edge moves from the writer to the resolver:
a misconfigured `catalog:` peer range that a workspace package's own
dependency supplies now fails resolution like every other catalog
misconfiguration, where the writer used to record the raw range.

Refs pnpm/tasks#149
2026-10-18 00:31:51 +02:00
Zoltan KochanandClaude Opus 5.5 0133beef54 fix(deps-resolver): record every declared peer in the lockfile (#17067)
A package that lists the same name in `dependencies` and as a peer takes
it as a peer only in a scope that shadows the dependency. The lockfile's
`packages:` entry was written from that per-scope peer set, so an entry
first written where nothing provided the peer had no record of it. A
later install that added a provider read the package as peerless and
left the peer unbound, while a fresh install bound it.

Both stacks now record every declared peer (minus the package's own name
and its `optionalDependencies`):

- v12 writes `declared_peer_dependencies` into the entry. Lockfile reuse
  re-resolves a package whose recorded peer its snapshot also serves as a
  child unless the current scope shadows it, since only the real
  manifest tells whether that child is its own dependency.
- The fast lockfile update no longer adds a direct dependency under a
  name the lockfile records as a peer, in both stacks: the new name can
  change what the peer binds to.
- v12's directory-dependency freshness check decides shadowing in the
  importer's scope instead of reading it off the recorded peers, and
  accepts entries written before this change.
- v11 records `declaredPeerDependencies` from the resolved package. Its
  locked-package shortcut already re-reads the manifest whenever the
  entry records peers, and its freshness check already expected every
  peer to be recorded.

The peer convergence harness entry for this divergence is removed, and
the harness gains a fixture where another project provides the peer.

Closes pnpm/pnpm#17056

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 23:13:12 +02:00
5b68320870 fix(overrides): measure a workspace: path in an override from the workspace (#17016)
`pnpm.overrides` is written once, at the workspace root, and applied to
every package that matches. `file:`, `link:` and bare-path values are
anchored at the root and rendered again for each package the override
rewrites. A `workspace:` path was not, so `workspace:./packages/target`
was measured from the consuming package. Without injection the install
left a dangling link; with `injectWorkspacePackages` it failed because
the directory does not exist.

The overrider now claims a value starting with `workspace:.`, the only
`workspace:` shape the resolver reads as a path, and renders it relative
to each package with a leading `./`, so it keeps reaching the local
resolver under the `workspace:` protocol. A manifest with no directory
keeps the value as declared, because an absolute `workspace:/...` would
be read as a version range.

An override in a file listed under `include` has its `workspace:` path
rebased from that file to the workspace root, as `file:` and `link:`
values are.

Present in both pnpm v11 and v12, so fixed in both.

Closes pnpm/pnpm#17010

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:26:05 +02:00
Zoltan Kochan cf6770e47c fix: run the workspace root's pnpmfile for projects with their own lockfiles (#17047)
With sharedWorkspaceLockfile: false, each project's install looked for
pnpmfiles only in its own directory, so the workspace root's
.pnpmfile.cjs (and the config-dependency plugin pnpmfiles installed at
the root) never ran: no readPackage, no afterAllResolved, no
pnpmfileChecksum. pnpm v11 merges the hooks it loaded at the root into
each project's.

load_install_pnpmfiles loads, for a project with its own lockfile, the
project's pnpmfiles followed by the root's (global pnpmfile once). As
in v11, the project's hooks run first, and pnpmfileChecksum covers the
project's pnpmfiles when the project side defines hooks, else the
root's, so both write the same lockfile.

With the root's readPackage running, a consumer's lockfile records an
injected sibling as the hook rewrote it. The lockfile freshness check
read that sibling's manifest from disk, unhooked, because a
per-project install holds only its own project. check_lockfile_freshness
now applies packageExtensions and readPackage to the manifests of the
dependencies the lockfile records as injected before comparing, for a
project with its own lockfile only. Lockfile::injected_dependencies
returns the recorded directory and spec for that.
2026-10-17 18:05:14 +02:00
Zoltan Kochan 9204d64b8c fix(deps.status): record which projects each install proved installed (#17034)
verifyDepsBeforeRun compared file mtimes with one workspace-wide
lastValidatedTimestamp and a filteredInstall flag. A filtered install
refreshed the timestamp for the projects it left out, so an edit to one
of them went unnoticed, and an edit that kept its mtime was never seen.

The workspace state now writes the validation records pnpm v12 writes:
the digest of each project's manifest and of the lockfile it was proven
against, keyed by importer id, plus a stat signature and digest for the
lockfile, patches, and pnpmfiles. Manifests are hashed the way v12
hashes them, so either CLI reuses the other's records. A filtered
install vouches only for its selection and carries the other records
while the workspace-wide inputs stand.

The check holds a filtered command's selection, and for an unfiltered
command every stale project plus an unrecorded one where it runs. A held
project installed from another lockfile, or never installed, is
outdated. A state without records keeps the mtime check, and a passing
check records it.

Part of pnpm/tasks#139 and pnpm/tasks#142.
2026-10-17 15:47:47 +02:00
Zoltan KochanandClaude Opus 5.5 94d5ca6856 fix: apply trustPolicy to the pnpm version a project pins (#17038)
pnpm v12 resolves the pinned pnpm under the project's `trustPolicy`,
`trustPolicyExclude`, and `trustPolicyIgnoreAfter`, as it resolves any
other dependency. pnpm v11 resolved it with no trust policy at all, both
when switching to the pinned version and when recording a pin the
running pnpm already satisfies. A version whose trust evidence was
downgraded therefore failed under one CLI and ran under the other
(https://github.com/pnpm/pnpm/issues/16319). The trust failure is the
intended behavior, so v11 now matches v12.

`resolveManifestDependencies` and `resolvePackageManagerIntegrities` take
the three settings (`PackageTrustPolicy`) and pass them to
`resolveDependencies`. `switchCliVersion` and `syncEnvLockfile` pass them
from the config. The store controller for the switch is already created
from the full config, so `trustPolicy: no-downgrade` already fetches the
full metadata the check reads.

The trustPolicy docs of both versions now say the check covers the
pinned pnpm.

Part of https://github.com/pnpm/tasks/issues/144

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:00:53 +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
Zoltan Kochan d71dd6c23f refactor(deps-resolver): pick hoisted peer providers through one tiered rule (#17023)
The required and optional peer hoists each carried their own "highest
qualifying version" loop and their own idea of which versions to try
first. Both now go through one function that takes an ordered list of
version tiers and a range check:

- required: the versions this run resolved, then every version;
  includePrerelease against the one wanted range.
- optional: locked and resolved, then resolved, then locked, then every
  version; strict against every recorded range and the root's range.

In pnpm v12 the hoist module moves to `peer_provider.rs` and the tiers
become a `ProviderTiers` type, replacing `optional_peer_version_tiers`.
pnpm v11's `hoistPeers.ts` gets the same `pickProviderVersion` shape.
The picks do not change, except that among equal versions that differ
only in build metadata, the v12 optional picker now keeps the first
selector, like the required one.

This is the first step of pnpm/tasks#143. The pickers are now one place
that later steps can grow into the single peer-provider owner.
pnpm v11's optional picker still has no lockfile tiers. That is a
parity difference this change keeps as it is.
2026-10-17 12:06:25 +02:00
Zoltan Kochan 9bb8f52b67 fix(git-resolver): recognize the gist host (#16997)
pnpm v12's HostedGit parser did not implement the gist host that
hosted-git-info defines. `gist:<id>` failed to resolve as a dependency,
a gist URL was treated as an unknown host, and `pnpm pkg fix` left a
`gist:` repository unnormalized while pnpm v11 and npm rewrite it.

HostedGit now has a Gist type. Every URL form omits the owner, as
upstream's gist templates do, the archive is
codeload.github.com/gist/<id>/tar.gz/<commit>, and the docs URL is the
gist page. Unlike upstream, the gist https and git forms keep
URL-embedded credentials, so a credentialed gist URL still reaches
`git ls-remote` with them. `pnpm pkg fix` drops them, as upstream does.
An ownerless /<id>/raw/... file URL is rejected; upstream reads it as
the gist `raw`.

`pnpm add` keyed a hosted git source on type, user, and project. A gist
URL may carry an owner that the shortcut lacks, so both CLIs warned
about a replacement from a different source when re-adding the same
gist. The key is now the shortcut without its committish.

`pnpm sbom` dropped the `gist:` shorthand in both CLIs only to keep them
in step. Both now expand it to the gist's https URL.

Closes pnpm/tasks#137
2026-10-17 01:26:01 +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
Zoltan Kochan 9e13c41ec4 fix: keep importers of projects missing from disk in filtered installs (#16936)
A filtered install whose selection matched every project on disk was
treated as a full install, so it pruned lockfile importers of workspace
projects that are listed but not on disk, as in a sparse checkout.

pnpm v12:
- A --filter selection now always requests its importers, so the
  stale-importer prune is skipped and the filtered lockfile merge runs.
  The workspace state still records the install as unfiltered when every
  project was selected.
- The filtered merge keeps importers the previous lockfile records for
  projects outside the workspace walk, plus their packages and catalog
  entries. Before, it dropped them even for a narrower filter.

pnpm v11:
- pruneLockfileImporters defaults to false when a filter is given.
- The lockfile catalogs keep the entries used by importers that the
  resolution left untouched. Before, a narrower filter kept the
  importer but dropped its catalog entries.

Closes https://github.com/pnpm/pnpm/issues/16931
2026-10-15 01:10:14 +02:00
Dipun MistryandZoltan Kochan f2d8a2f3cb fix(hoist): preserve workspace links beside auto-installed peers (#16937)
Closes pnpm/pnpm#16935.

Workspace hoist selection previously reserved aliases from resolved graph entries. An automatically installed peer could therefore claim a workspace package's name, even though no importer declared that package as a non-link direct dependency. On an unchanged dedupe, stale-link cleanup then removed the workspace hoist.

Read non-link direct dependency aliases from every wanted-lockfile importer in both v11 linker paths and require that list at the workspace hoist boundary. In the hoisted linker, also reserve aliases already occupying the root directory, so a skipped workspace symlink does not leave behind an invalid workspace-only executable. This keeps filtered and frozen installs consistent. Add a v11 regression test for install, dedupe, and frozen reinstall across hoist layouts, plus a v12 test that guards the existing behavior.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-15 00:47:44 +02:00
Zoltan Kochan b63e347017 fix(resolver): re-split lockfile-pinned children when a dependency becomes a peer (#16912)
A newly resolved package reads the children the prior lockfile pinned
for it (pnpm/pnpm#13839). When the scope it resolves them in provides a
name that a pinned package lists both as a dependency and as an optional
peer, the pinned split no longer applies, but the lockfile entry cannot
say so: an entry that kept the dependency omits the peer. So installing
in two steps kept the old split, while installing everything at once,
and pnpm v11 in both cases, wrote the peer.

Before reading the pins, the owner walks the pinned subtree. For a
package with a locked dependency or peer whose presence in scope
differs from the scope the pins were recorded under, it reads the
package's metadata and compares the split under the new scope with the
locked one. When any differs, the owner resolves its children instead.
A plain reinstall has no such owner.
2026-10-14 09:58:10 +02:00
Zoltan Kochan 08d5424a33 fix(resolver): decide a dependency that is also an optional peer at its children owner (#16903)
A package that lists a dependency also as an optional peer takes it as
a peer when a parent provides it, and keeps it as a dependency
otherwise. When the package is reached through several paths, the
shallowest one should decide.

TypeScript resolver: the peer list came from the first path and the
children from the shallowest one, so the dependency was dropped from
both. The path that takes over the children now also sets the peers.

Rust resolver: the shallowest path reused the children recorded by a
deeper one, so the lockfile differed. Reuse now also requires the two
scopes to agree on names that some package lists both as a dependency
and as a peer.

Closes pnpm/pnpm#16901
2026-10-13 20:28:57 +02:00
Zoltan KochanandClaude Opus 5.5 931d626a41 fix(remove): unlink a removed link: dependency under the hoisted linker (#16887)
* fix(remove): unlink a removed link: dependency under the hoisted linker

The hoisted linker's orphan diff only covers the package directories it
imports, and the stale direct-dependency prune that the isolated linker
runs is skipped for the hoisted layout. A removed `link:` dependency
therefore kept its symlink in node_modules.

Before linking the hoisted tree, unlink every direct-dependency symlink
that the wanted lockfile no longer records at the same version. Only
symlinks are removed; real directories stay with the orphan diff.

Fixed in both the Rust CLI and the TypeScript v11 CLI.

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

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

* refactor(deps-restorer): move the hoisted linker's stale-link passes into a module

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

* fix(deps-restorer): do not unlink a stale link through a symlinked scope directory

The Rust cleanup already refuses to remove a scoped alias whose `@scope`
directory is not a real directory. Apply the same guard in v11, so a
symlinked or junctioned `@scope` cannot redirect the unlink outside the
modules directory.

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

* test: check the lockfile and a frozen reinstall after removing a hoisted link: dependency

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:26:43 +02:00
Altay c60133d86c perf(install): speed up frozen injected workspace installs (#16824)
Avoid replacing an injected directory when its regular files are still
hardlinked to the current source and its entries match. Keep the existing
refresh path for mismatches, links, forced imports and build isolation.
Preserve import reporting and add a frozen-repeat scenario to the existing
integrated benchmark and CI comparison.

Closes pnpm/tasks#121.
2026-10-10 19:16:11 +02:00
0f215fea17 fix(resolver): preserve workspace optional peer providers (#16707)
Keep the workspace protocol in both resolvers when an optional peer selects
the version of a workspace dependency declared at the root. The optional peer previously
resolved from the registry using only the selected version.

Reuse the root dependency lookup for aliases. Preserve the existing version
selection when the peer requires a different version.

Closes pnpm/pnpm#16706.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:35:03 +02:00
Zoltan KochanandClaude Opus 5.5 797d717011 fix(hoist): remove each parent of stale workspace links once (#16693)
* fix(hoist): remove each parent of stale workspace links once

pruneStaleWorkspaceHoists cleaned up the empty parents of every stale
workspace link in parallel. Links under one scope share the scope
directory, so it was removed by several concurrent rmdir calls. On
Windows, a removal of a directory whose delete is still pending fails
with EPERM, which failed the install. The hoisted linker unlinks every
workspace hoist before relinking, so any install that did not take the
up-to-date fast path hit this.

The cleanup now runs once per parent directory. A workspace alias has at
most a scope directory above it, so distinct parents never overlap.

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

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

* test(hoist): type the stale workspace link fixture keys

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:50:48 +02:00
ed6e822f45 fix(install): sync only the package files into injected copies with dedicated lockfiles (#16684)
The split-lockfile sync added in pnpm/pnpm#15685 patches each injected
copy from its source project after all selected projects are installed.
It read the source directory with every file, while the install builds
the copy from the package files only (the files field and .npmignore)
unless deployAllFiles is set. So when the injected project was part of
the same run, the copy picked up files such as tsconfig.json and the
project's own pnpm-lock.yaml.

The sync now reads the source with includeOnlyPackageFiles set to
!deployAllFiles, as the install's directory fetch does. The target is
still read in full, so files that an earlier sync added are removed.
The sync after a project's own scripts with a shared lockfile keeps
reading every file.

Fixes https://github.com/pnpm/pnpm/issues/16683

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:58:34 +02:00
d1e8874891 fix(overrides): apply removal overrides to optional peers declared only in peerDependenciesMeta (#16682)
The resolver treats a peerDependenciesMeta entry with optional: true
and no peerDependencies entry as an optional "*" peer. The versions
overrider only iterated peerDependencies, so a removal override never
reached such a peer and a sibling workspace project could still
satisfy it.

Both overriders now also match each peerDependenciesMeta name that
peerDependencies does not declare against the overrides as "*", and
drop the meta entry when the chosen override is "-". In pacquet the
apply_to_arc fast path checks the same entries, so the manifest is
cloned and rewritten.

The workspace regression tests from pnpm/pnpm#15199 now also run
against @pnpm.e2e/abc-optional-peers-meta-only.

Fixes https://github.com/pnpm/pnpm/issues/16681

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:57:39 +02:00
14921801c9 fix(install): keep lockfile catalogs under catalogPrune (#16680)
catalogPrune dropped catalog entries that the lockfile left on disk
still records when a workspace project was missing from disk: on v11
for installs that skip resolution (frozen, CI, lockfile up to date)
and for remove, and on v12 for remove. The next frozen install then
failed with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH. The prune now keeps
those entries, except when `lockfile` is false.

Fixes https://github.com/pnpm/pnpm/issues/16679

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:55:41 +02:00
ab1e504f8a fix(config-deps): apply minimumReleaseAge when resolving config dependencies (#16670)
Verifying locked config dependencies against the registry enforces
minimumReleaseAge, but resolving them did not. A config dependency range
therefore resolved to a version that a later clean frozen install
rejected.

Keep the age gate on verification, since a config dependency's pnpmfile
runs on every install, and apply the same cutoff and
minimumReleaseAgeExclude when resolving config dependencies and their
optional subdependencies, in both the Rust and TypeScript CLIs. A
version+integrity pin resolves without the cutoff, as verification
already skips pins. Config
dependencies are resolved before any updateConfig hook runs, so only an
exclude in pnpm-workspace.yaml can cover one.

Closes pnpm/pnpm#16660

---------

Signed-off-by: C. Spencer Beggs <spencer@beggs.codes>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:09:28 +02:00
Zoltan KochanandClaude Opus 5.5 f4b9616fa9 fix(deps.graph-builder): relink a global virtual store slot the current lockfile records (#16663)
A project's current lockfile vouches only for slots that project alone
writes. A global virtual store slot is shared, so another project's
interrupted install may have re-created it without its child links.
Such a slot now stays in the dependency graph as reusable instead of
up to date, so the linker restores its children.

skipGvsInternalLinking (pnpm/pnpm#11073) still skips linking when the
install fetches nothing, because reusable nodes do not count as added.
This matches pnpm v12 after pnpm/pnpm#16649.

Related to https://github.com/pnpm/pnpm/issues/16642

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 01:12:43 +02:00
Altay 5b87f8fb55 fix(resolver): reject a locked npm alias for a dependency's plain range (#16658)
Lockfiles written before pnpm/pnpm#15503 record an optional peer that the
package also depends on as the peer's provider. When that provider is an
npm alias of another package, the snapshot keeps it, for example
`ajv-formats@3.0.1(@redocly/ajv@8.18.3)` with
`optionalDependencies: { ajv: '@redocly/ajv@8.18.3' }`.

Since pnpm/pnpm#15503 the name is resolved as a regular dependency again,
from its plain range (`ajv: ^8.0.0`). The check that decides whether the
locked ref still satisfies that range compared only the version, so the
alias was accepted. The resolver then either kept the other package under
the alias or, when it looks the locked version up in the registry as the
default minimumReleaseAge makes it do, asked for `ajv@8.18.3` and failed
with ERR_PNPM_NO_MATCHING_VERSION. Overrides with a version range in the
selector reach this path because they skip the fast override rewrite.

A plain range now requires the locked package to have the alias's own
name, in both the TypeScript resolver (getRangeOfLockedPackage) and the
Rust resolver (prior_child_key). Non-range specifiers such as git and
tarball URLs keep their existing handling.

Closes pnpm/pnpm#16654
2026-10-06 18:28:40 +02:00
1b125b259e fix: keep deprecated and hasBin in the lockfile on install --fix-lockfile (#6784)
`pnpm install --fix-lockfile` cleared `deprecated` and `hasBin` from every
package entry before re-resolving, and the resolver did not regenerate
them: v11 reuses locked entries without reading the manifest (so `hasBin`
became false and `deprecated` vanished), and v12 lost any deprecation the
registry metadata does not carry because the seed lockfile it carries
deprecations from had been cleared.

Keep both fields in the repair seed in v11 (forgetRegeneratedLockfileFields)
and v12 (Lockfile::prepare_for_fix). Entries that fail to decode are still
narrowed to their resolution by the v12 repair loader.

Closes pnpm/pnpm#6600

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:36:16 +02:00