Commit Graph

305 Commits

Author SHA1 Message Date
Zoltan Kochan
93458600a8 chore(release): 11.8.0 (#12492)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-18 12:17:52 +02:00
Allan Kimmer Jensen
1495cb080c feat(sbom): per-package SBOM generation with --out and --split (#12097)
* feat(sbom): add monorepo workspace support for SBOM generation

When --filter selects a single workspace, the SBOM root component now
uses that workspace's name, version, description, license, and author
instead of the workspace root's metadata. Author, repository, and
license fall back to the root manifest when the workspace package
doesn't define them.

Workspace inter-dependencies (workspace: protocol) and their transitive
dependencies are now included in the SBOM. Dev-only workspace deps are
correctly excluded when --prod is used, and lockfile-only mode skips
workspace resolution entirely to avoid unexpected disk reads.

* fix(sbom): add recursiveByDefault so --filter works in workspaces

Without recursiveByDefault, the sbom command in a workspace didn't
enter the recursive code path that populates selectedProjectsGraph from
--filter. The handler received no filter info and always used the
workspace root manifest for the root component.

* feat(sbom): add --out and --split for per-package SBOM generation

Add --out <path> to write SBOM to a file instead of stdout. Supports
%s (package name) and %v (version) placeholders. When %s is present,
generates one SBOM per workspace package automatically.

Add --split to output NDJSON (one compact JSON per line) to stdout,
for piping into tooling that processes multiple SBOMs.

Add compact option to CycloneDX and SPDX serializers so NDJSON lines
are single-line JSON without re-parsing.

Sanitize %s and %v values to prevent path traversal in output paths.

* fix: use sbom-out instead of sboms to pass spellcheck

* fix(sbom): expand %v in single-output path and fix workspace dep parent relationships

Two bugs found by CodeRabbit:

1. --out with %v but no %s wrote a literal %v filename. Now both %s
   and %v are expanded in the single-output path using the SBOM root
   component's name and version.

2. The lockfile walker used rootPurl as parent for every importer,
   including additional workspace dep importers. This caused
   shared-lib's external deps (like is-odd) to appear as direct
   deps of the root package. Now each importer's walk uses the
   correct parent PURL based on whether it's an original or
   workspace dep importer.

* fix(sbom): fall back to workspace root manifest for filtered package metadata

Read the root manifest from rootProjectManifest/rootProjectManifestDir instead
of opts.dir so license/author/repository/description fall back to the workspace
root when a filtered package omits them. Add the missing rootDescription
fallback. Fix the per-package CycloneDX test assertions to match the standard
group/name split for scoped names.

* fix(sbom): harden path handling and fix lockfile-only/versionless workspace deps

- Neutralize '.', '..' and blank segments in --out path templates so a crafted
  package name/version cannot escape the output directory.
- Skip lockfile link: targets that resolve outside the workspace root, and guard
  the workspace manifest reads with a containment check, preventing arbitrary
  package.json reads from a malicious lockfile.
- Honor --lockfile-only inside collectSbomComponents so workspace links and their
  transitive deps are no longer traversed.
- Include workspace packages that omit a version (default to 0.0.0, matching the
  root component).
- Bound workspace manifest reads with p-limit to avoid EMFILE on large monorepos.

* fix(sbom): error on per-package output path collisions and use O(n) workspace BFS

- Throw SBOM_OUT_PATH_COLLISION when two workspace packages sanitize to the same
  --out path instead of silently overwriting one SBOM with another.
- Replace the resolveWorkspaceDeps BFS queue.shift() (O(n) per dequeue) with a
  moving head index for O(n) traversal on large workspaces.

* fix(sbom): strip control chars from output paths and skip unreadable workspace importers

- Strip ASCII control characters (incl. newlines) in sanitizePathSegment so a
  crafted package name/version cannot inject lines into the --split --out summary
  or produce confusing filenames.
- Skip walking a reachable workspace importer when its package info is missing
  (e.g. unreadable manifest) instead of misattributing its deps to the root.
- Bound importer-walker fan-out with p-limit to avoid resource pressure on large
  workspaces.

* perf(sbom): reuse workspace manifests across split outputs and use a Set for path collisions

- Read workspace package manifests once into SharedContext (keyed by importer id)
  from the project graph, so split mode no longer re-reads them from disk for
  every emitted SBOM.
- Track written --out paths in a Set instead of Array#includes to make collision
  detection O(1) per package rather than O(n2) overall.

* fix(sbom): support %s in single-project --out, harden importer lookup, cache root license

- Only treat %s in --out as per-package (split) mode when a workspace project
  graph is present; in a single-project repo %s/%v interpolate from the root
  component on the single-output path. Adds a regression test.
- Use an own-property check for lockfile importer existence so a crafted link:
  target cannot follow inherited keys (e.g. toString) via the prototype chain.
- Fast-path resolveRootLicense when the manifest already declares an SPDX license
  and cache the workspace-root license in SharedContext, avoiding redundant
  on-disk LICENSE probing per emitted SBOM.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-17 09:43:27 +02:00
btea
293921a788 feat(view): support searching package.json upward when package name is omitted (#11696)
* feat(view): support searching package.json upward when package name is omitted

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: apply review

* fix: apply review

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: update

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-17 01:04:25 +02:00
Abdullah Alaqeel
61969fbddf fix(deps-status): detect lockfile-only changes (#12106)
## Summary

Fixes `pnpm install` with `optimisticRepeatInstall` incorrectly returning `Already up to date` when `pnpm-lock.yaml` changed but project manifests did not.

Fixes #12100.

## Root Cause

`checkDepsStatus` used modified manifest mtimes as the only signal for whether it needed to validate dependency status. If no manifest was newer than `workspaceState.lastValidatedTimestamp`, it returned `upToDate: true` before checking whether the wanted lockfile had changed.

That skipped lockfile validation for workflows like:

- `git checkout HEAD~1 -- pnpm-lock.yaml`
- restoring only `pnpm-lock.yaml` from a stash
- external tools rewriting the lockfile without touching manifests

## Changes

- Check wanted lockfile mtimes before taking the optimistic fast path.
- If any wanted lockfile is missing or newer than the workspace state timestamp, validate all projects instead of only modified manifests.
- Add a regression test proving a lockfile-only change does not skip wanted-lockfile validation.
- Add a patch changeset for `@pnpm/deps.status` and `pnpm`.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-16 22:04:07 +00:00
Truffle
6d35338691 fix: detect changes inside file: dependencies on repeat install (pacquet + pnpm) (#12317)
## Summary

- `pnpm install` reports "Already up to date" after edits inside a `file:` dependency's directory or after repacking a `file:` tarball. This is a v11 regression from the `optimisticRepeatInstall` default flip in pnpm/pnpm#11158. Fixes pnpm/pnpm#11795.
- `checkDepsStatus` gains a `treatLocalFileDepsAsOutdated` option: when set, any project manifest declaring a local file dependency makes the check report not up to date. `installDeps` sets it on the optimistic fast path, so projects with local file dependencies always run a real install, which refetches those dependencies (the v10 behavior).
- The predicate covers `file:` specs, path-prefixed specs (`./`, `../`, `~/`, absolute POSIX paths, and Windows drive paths including drive-relative ones like `C:dir`, matching the local resolver's `isFilespec`), and bare tarball file names (`vendor/pkg.tgz`). It is deliberately narrower than the local resolver's bare-path matching: a bare `user/repo` is statically indistinguishable from a git shorthand at this layer, and matching it would kill the fast path for every project with git dependencies, so protocol-carrying and URL specs stay on the fast path.
- `pnpm.overrides` entries are scanned with the same predicate: an override mapping to a local file spec redirects every matching dependency in the graph to that directory, so it has the same blind spot as a direct local file dependency. Registry and `link:` overrides keep the fast path.
- The option is caller-scoped on purpose. `verifyDepsBeforeRun` also consumes `checkDepsStatus`, and treating `file:` deps as always stale there would force a reinstall before every `pnpm run`. Its behavior is unchanged, and a regression test pins that.
- pacquet port in the same commit: `check_optimistic_repeat_install` bails unconditionally on `file:` specifiers, because its only caller is the install command, the one consumer that sets the flag upstream. `link:` specifiers are excluded on both sides: they are symlinked, so changes inside them flow through without a reinstall.

## Why

Both branches of `checkDepsStatus` are blind to content changes inside a `file:` dependency. The workspace branch exits early with `upToDate: true` when no project manifest's mtime moved, without ever reaching `linkedPackagesAreUpToDate`. The non-workspace branch exits at the manifest-vs-lockfile mtime gate the same way. Editing a source file inside a `file:` dependency bumps neither, so the fast path can never see it; the fix has to bail before those gates rather than refine them. This is the fix shape (a) I proposed in my diagnosis on the issue thread ([comment](https://github.com/pnpm/pnpm/issues/11795#issuecomment-4504177744)): the cost is a full resolution on repeat installs only for projects that declare `file:` dependencies, which is exactly what v10 did.

The manifest-only comparison in `@pnpm/lockfile.verification` (`allProjectsAreUpToDate`) is intentional for the install-proper path and asserted by its tests, so this PR leaves it untouched.

## Checks

- `pnpm --filter @pnpm/deps.status test test/checkDepsStatus.test.ts` (31 passed, 13 new)
- `pnpm --filter @pnpm/deps.status run compile` and `pnpm --filter @pnpm/installing.commands run compile` (tsgo + eslint clean)
- `cargo test -p pacquet-package-manager optimistic_repeat_install` (51 passed, 7 new; run in a rust:1.95.0 container)
- `cargo fmt --check -p pacquet-package-manager`
- `RUSTDOCFLAGS="-D warnings" cargo doc -p pacquet-package-manager --no-deps`

---
Written by an agent (Claude Code, claude-fable-5).

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-16 22:31:45 +02:00
Allan Kimmer Jensen
dcededcd27 feat(sbom): mark dev-only components with CycloneDX scope "excluded" (#12442)
Components reachable only through devDependencies now get
`scope: "excluded"` plus the `cdx:npm:package:development` property in
CycloneDX output. The `excluded` scope documents non-runtime/test usage
(valid in every exported spec version, 1.5/1.6/1.7); the property is the
CycloneDX npm-taxonomy marker emitted by `@cyclonedx/cyclonedx-npm`, so
both modern and existing consumers are covered. Runtime-reachable
components (ProdOnly, DevAndProd, and installed optionalDependencies)
omit both and default to `required`.
2026-06-16 20:29:31 +02:00
minbang
3188ae7026 fix: accept loose peer ranges in peers check (#12150)
Run peer dependency range checks with semver loose parsing while preserving
prerelease inclusion.

This prevents pnpm peers check from reporting react-native-reanimated@4.4.0 as
unmet for ranges like >=3.16.0 || >=4.0.0-.

Add a lockfile regression fixture for #12149 and a patch changeset for the peer
checker and pnpm CLI.

Closes #12149.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-06-16 20:20:53 +02:00
Zoltan Kochan
1e82e001cd chore(release): 11.7.0 (#12414) 2026-06-15 08:37:08 +02:00
Dasa Paddock
da248c3eef fix(view): update dist tarball unpackedSize to compute value as power of 10 (#12342) 2026-06-14 15:19:50 +02:00
tsushanth
8dcd9a055c fix(installing.commands): show only names in checkbox summary (#12393)
Closes pnpm/pnpm#12386
2026-06-14 11:50:27 +02:00
Zoltan Kochan
681b593eb2 fix: support scope-specific registry auth tokens (#12392)
pnpm can now use different auth tokens for different package scopes, even when those scopes use the same registry URL.

Previously, auth was selected only by registry URL. If `@org-a` and `@org-b` both used `https://npm.pkg.github.com/`, they had to share the same token. This caused problems for registries that issue tokens per organization or per scope.

Configure a scope-specific token by adding the package scope after the registry URL in the auth key:

```ini
@org-a:registry=https://npm.pkg.github.com/
@org-b:registry=https://npm.pkg.github.com/

//npm.pkg.github.com/:@org-a:_authToken=${ORG_A_TOKEN}
//npm.pkg.github.com/:@org-b:_authToken=${ORG_B_TOKEN}

//npm.pkg.github.com/:_authToken=${FALLBACK_TOKEN}
```

`pnpm login --registry=https://npm.pkg.github.com --scope=@org-a` writes the token to the same scope-specific auth key.

When installing or publishing `@org-a/*`, pnpm uses `ORG_A_TOKEN`. For `@org-b/*`, pnpm uses `ORG_B_TOKEN`. Packages without a matching scope continue to use the registry-wide fallback token.
2026-06-14 11:43:30 +02:00
Zoltan Kochan
93e974a7ce test(deps.compliance): use synthetic package in audit update test (#12368) 2026-06-12 23:57:11 +02:00
Zoltan Kochan
a31faa7c19 chore: update dependencies (#12346)
* chore: update dependencies

Update all catalog dependencies to their latest versions, except those
held back by pnpm's supported Node.js floor (>=22.13) or known issues;
each held-back entry now carries a comment in pnpm-workspace.yaml
explaining why.

Notable changes:
- msgpackr 1.11.8 -> 2.0.4 (unpinned; types compile again and the
  store-index output is byte-compatible with 1.x in both directions)
- typescript 5.9.3 -> 6.0.3, esbuild 0.28, commitlint 21,
  concurrently 10, eslint plugin majors (autofixed one import-sort
  error they introduced)
- open 11, memoize 11, cli-truncate 6, pidtree 1, @yarnpkg/core 4.8,
  @rushstack/worker-pool 0.7.18
- removed unused nock devDependency and the deprecated @types/tar stub
- bole stays on 5: bole 6 is ESM-only and under Jest the workspace
  logger's ESM copy and the published @pnpm/logger's CJS bole 5 no
  longer share the globalThis.$$bole output registry, breaking
  reporter assertions

Held back due to Node >=22.13 support floor: ssri 14,
write-file-atomic 8, validate-npm-package-name 8,
normalize-package-data 9, npm-packlist 11, ini 7 (need ^22.22.2),
undici 8 (needs >=22.19), cspell 10 (needs >=22.18; bumped to 9.8.0
instead).

* chore: add changeset for updated dependency ranges

Patch-bump every published package whose runtime dependency or peer
dependency range changed in the dependency update, following the
precedent of commit 09cf46f6 (update @pnpm/logger in peer
dependencies).
2026-06-12 08:27:37 +02:00
Zoltan Kochan
53b105416f chore(release): 11.6.0 (#12336)
* chore(release): 11.6.0

* docs: update CHANGELOG.md

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-12 00:53:15 +02:00
Zoltan Kochan
d976edf4ec perf: content-check modified manifests and fall back to the current lockfile on the repeat-install fast path (pacquet + pnpm) (#12315)
## Why

On [benchmarks.vlt.sh](https://benchmarks.vlt.sh/) (2026-06-10 run, pacquet 0.11.2), pacquet ranked **8th–9th of 10** in every `lockfile+node_modules` variation — slower than pnpm, npm, yarn and vlt — e.g. astro: pacquet 936 ms vs pnpm 502 ms; babylon: pacquet 9.08 s vs pnpm 0.85 s. It also trailed vlt/npm in the `node_modules` and `cache+node_modules` variations (astro 1.5 s / 0.7 s, babylon 8.9 s / 6.4 s).

### Root cause

Tracing the actual runner (a `pacquet` PATH shim logging per-invocation file stats) showed the harness's prepare step rewrites `package.json` with **identical content but a fresh mtime** before every timed run, while `clean_all_cache` wipes `~/.cache/pnpm` (the packument cache and `lockfile-verified.jsonl`), and the `node_modules` variations additionally delete `pnpm-lock.yaml`.

- **pnpm**: `checkDepsStatus`'s modified-manifests branch re-checks the *content* against the lockfile (`assertWantedLockfileUpToDate`, `assertLockfilesEqual`, `linkedPackagesAreUpToDate`) and reports "Already up to date" with zero network — ~0.5 s is just Node startup. Verified locally: with all caches wiped and the network blocked, `pnpm install` still prints "Already up to date" in 228 ms.
- **pacquet**: the optimistic repeat-install check bailed on *any* newer manifest mtime, fell into the full pipeline, and the awaited `minimumReleaseAge` lockfile-verification gate — its verdict cache wiped — re-fetched **one packument per locked package** per run: 0.94 s on astro, 9.1 s on babylon.
- With `pnpm-lock.yaml` deleted, both stacks pay a similar fan-out on the synthesized-from-current lockfile (`tryLockfileVerificationCache` bails before the content-hash index when the lockfile file can't be stat'd), which is why even pnpm needs 2.2–11.6 s there.

## What

**Commit 1 — port the modified-manifests branch of `checkDepsStatus`** (at pnpm/pnpm@cc4ff817aa) into `optimistic_repeat_install`:

- a manifest whose mtime is newer than `lastValidatedTimestamp` is re-checked against the wanted lockfile instead of invalidating the fast path: lockfile-settings drift (`getOutdatedLockfileSetting`), per-importer `satisfiesPackageManifest`, and a port of `linkedPackagesAreUpToDate` for workspace links (`isLocalFileDepUpdated` for `file:` directory specifiers is not ported — those conservatively fall through to the full install);
- `assertLockfilesEqual` runs when the wanted lockfile is newer than the reference (workspace: `lastValidatedTimestamp`; single-project: the current lockfile's mtime, mirroring upstream's branch shapes);
- the workspace branch refreshes `lastValidatedTimestamp` after a passing content check, like upstream's `updateWorkspaceState` call;
- the frozen-dispatch freshness gate is split into reusable pieces (`parse_config_overrides`, `check_lockfile_settings_drift`, `check_importer_satisfies`) shared with the new check, and the per-importer slice is no longer hard-wired to the root importer.

**Commit 2 — treat the current lockfile as the wanted one when `pnpm-lock.yaml` is missing (pacquet)** (requested by @zkochan): when `node_modules` is intact, `<virtual_store_dir>/lock.yaml` — the record of what the previous install materialized — stands in as the wanted lockfile for the same content checks, and `pnpm-lock.yaml` is regenerated from it (byte-identical to what the full install's synthesize-from-current path would write) before the fast path reports "Already up to date". Single-project installs with no lockfile on either side still refuse the fast path; `lockfile: false` skips the regeneration; a manifest that no longer matches (e.g. `pacquet add`) still takes the full resolve.

## Validation

Re-ran the actual vlt.sh harness (same scripts, ubuntu-24.04-arm runner) with the patched binary swapped into the npm-installed pacquet; all hyperfine runs exited 0:

| fixture, variation | pacquet 0.11.2 (official run) | patched | pnpm (same validation run) |
|---|---|---|---|
| astro, `lockfile+node_modules` | 935.6 ms (rank 9/10) | **38–39 ms** | 599–621 ms |
| babylon, `lockfile+node_modules` | 9 084 ms (rank 8/10) | **86.6 ms ± 0.6** | 767.7 ms |
| astro, `node_modules` | 1 501 ms (rank 4/10) | **41.2 ms ± 0.8** | 2 226 ms |
| astro, `cache+node_modules` | 704 ms (rank 5/10) | **42.9 ms ± 0.9** | 2 017 ms |
| babylon, `node_modules` | 8 962 ms (rank 6/10) | **107.8 ms ± 1.0** | 11 566 ms |

After this change only aube (~5 ms) and bun (~8 ms) stay ahead in these five variations.

`cargo nextest run -p pacquet-package-manager` (438 tests), `-p pacquet-cli` install suites, workspace clippy `-D warnings`, dylint, fmt, taplo and `typos pacquet` are clean. New tests cover the touched-but-identical manifest, a manifest that adds a dependency, a diverged wanted-vs-current lockfile, the state-timestamp refresh, linked siblings inside/outside the manifest range, lockfile regeneration (modified and unmodified manifests, workspace state bump), and `lockfile: false`. Two offline e2e tests additionally pin the "zero network, zero pipeline" property through `Install::run`'s real dispatch: a real install, registry dropped, caches wiped, repeat install pointed at a dead port — both verified discriminating by temporarily disabling the content check.

Two existing tests were adjusted: `fresh_install_records_lockfile_verification_for_mtime_bypassed_noop` now disables the optimistic check explicitly so it keeps guarding the verification-cache wiring it was written for, and `optimistic_repeat_install_does_not_short_circuit_when_lockfile_missing` now passes `lockfile: None` (matching the CLI contract for a missing file) and documents that the guard requires *both* lockfiles to be absent.

**Commit `1ee88c5107` — the same fallback in the pnpm CLI** (`@pnpm/deps.status` + `@pnpm/installing.commands`): `checkDepsStatus` lets the current lockfile stand in when `pnpm-lock.yaml` is missing (workspace shared-lockfile branch and single-project branch), runs the same content checks against it, and returns it as `wantedLockfileToRestore`; `installDeps` writes `pnpm-lock.yaml` back from it before reporting "Already up to date". Guard rails: no lockfile on either side still refuses the fast path, `useLockfile: false` skips the restore, a failed restore falls through to the full install, and the stand-in is disabled under `useGitBranchLockfile` (there a missing plain `pnpm-lock.yaml` is the steady state and the branch lockfile may legitimately differ from the current one). Verified with the bundled CLI: install → delete `pnpm-lock.yaml` → `pnpm install --registry=http://127.0.0.1:9/` prints "Already up to date" in 29 ms and restores the lockfile byte-identically. Covered by 5 new `checkDepsStatus` unit tests and an `installing/commands` integration test that runs the repeat install against a dead registry. Changeset bumps `@pnpm/deps.status`, `@pnpm/installing.commands`, and `pnpm` (minor).
2026-06-10 21:24:47 +02:00
Zoltan Kochan
b7195db5c8 chore(release): 11.5.3 (#12305) 2026-06-10 12:40:29 +02:00
Zoltan Kochan
bf1b731ee6 fix: harden allowBuilds artifact approvals (#12294)
## Summary

Package-name `allowBuilds` entries no longer approve lifecycle scripts for artifacts whose identity a name cannot pin: git, git-hosted tarball, direct tarball, and local directory dependencies. To approve such an artifact explicitly, use its peer-suffix-free lockfile depPath as the `allowBuilds` key — error hints, `pnpm ignored-builds`, and `pnpm approve-builds` print exactly that key.

- `AllowBuild` policy functions identify packages by `DepPath` instead of caller-supplied name/version. The policy parses name and version out of the depPath itself, so name-keyed rules can never be fed an identity that disagrees with the gated artifact. `AllowBuildContext` carries only an explicit `trustPackageIdentity` override, used to evaluate a previously recorded policy under its original semantics when detecting revoked approvals.
- Identity trust is derived from the depPath shape: a registry-style depPath (`name@semver`) is a trusted identity. This is sound because lockfile verification runs a new unconditional, offline structural pass that rejects lockfiles where such a key is backed by a git, directory, or git-hosted tarball resolution (`ERR_PNPM_RESOLUTION_SHAPE_MISMATCH`), while the npm resolution verifier already binds explicit tarball URLs of semver-keyed entries to the registry's own `dist.tarball` unconditionally. The pass runs inside the existing candidate walk and participates in the verification cache key (`resolutionShapeCheck`) on both the gate's and the fresh-resolve record paths, so the stat-only cache fast path stays sound and records written before the rule existed are re-verified.
- Installs detect approvals that were revoked (or stopped applying) for git/tarball artifacts and surface those packages as ignored builds; approvals granted for previously ignored builds trigger a rebuild of exactly those packages.
- `preparePackage` always treats the fetched manifest as an untrusted identity: it requires a `pkgResolutionId` and gates on the synthesized `name@<resolution id>` depPath. scp-style git URLs are normalized to `ssh://` form in resolution ids, and the git fetcher reuses `createGitHostedPkgId` from the resolver instead of re-deriving ids.
- Under the global virtual store, `pnpm rebuild` locates a projection created before the approval was granted by following the project's node_modules link, since the projection hash includes the allowBuilds verdict (relocating the projection instead is tracked in https://github.com/pnpm/pnpm/issues/12302).
- New shared helpers: `removePeersSuffix()` in `@pnpm/deps.path` (string-level peer-suffix stripping for user-written keys) and `allowBuildKeyFromIgnoredBuild()` in `@pnpm/building.policy` (the key under which an ignored build is approved).
- pacquet mirrors the whole policy: `AllowBuildPolicy::check(dep_path)` derives trust from the dep path, the git-fetcher allow-build closures take only the dep path, `pacquet-lockfile-verification` gains the same structural pass, error code, and cache identity, and dep-path keys normalize via `remove_suffix`.
- `shell-quote` is overridden to 1.8.4 (GHSA-w7jw-789q-3m8p / CVE-2026-9277).
- Test-harness fix: a module-level drain keeps the global log stream flowing in the deps-installer lifecycle tests, so reporter assertions no longer receive the buffered backlog of earlier installs that ran without a reporter.
2026-06-10 12:05:28 +02:00
Dasa Paddock
46fd26afc9 feat(view): show deprecation warning and bin info (#12271) 2026-06-10 08:22:06 +02:00
Zoltan Kochan
5f2bb9f5ba fix(security): verify npm registry signature before spawning a package-manager binary (#12292)
pnpm can be made to download and execute a native binary through two **repository-controlled** inputs, neither of which was authenticated before this change:

1. **pacquet install engine** — declaring `pacquet` (or `@pnpm/pacquet`) in `configDependencies` (in `pnpm-workspace.yaml`) opts in to pnpm's Rust install engine, and pnpm spawns the platform binary `@pacquet/<platform>-<arch>` during `pnpm install`.
2. **package-manager version switch** — the `packageManager` / `devEngines.packageManager` field makes pnpm download and run a specific pnpm version. This is **on by default** (`onFail` defaults to `download`) and also covers `pnpm self-update` and `pnpm with`.

In both cases the repository also controls the lockfile integrity and the registry the bytes are fetched from (via `.npmrc`), so matching the lockfile integrity proves nothing — it matches the hash the attacker wrote. A cloned, untrusted repository could therefore execute an arbitrary native binary just by running a normal pnpm command.

## Fix (corepack-style registry-signature verification)

pnpm now verifies the **npm registry signature** of the bytes it is about to spawn, **over the installed integrity**, against npm's public signing keys that **ship embedded in the pnpm CLI** (exactly as corepack does). If the bytes on disk were substituted or tampered with, npm's real signature does not validate over them.

- New reusable `verifyInstalledPackageSignatures()` in `@pnpm/deps.security.signatures` verifies `name@version:integrity` against `dist.signatures` using the embedded keys.
- Because the keys are **embedded** (not fetched), a registry the user did not vouch for cannot supply its own keypair to forge a signature. The signed packument is fetched from the **configured** registry, so an **npm mirror works transparently** — it proxies the same signed packument, with no configuration. There is intentionally **no runtime override or off-switch** for the keys.
- **pacquet** (`installing/commands`): verifies the `pacquet` shim and the host platform binary. It **fails the command** if the signature does not verify or cannot be checked (e.g. registry unreachable); the only graceful fallback to pnpm's own engine is when pacquet has no binary for the current platform.
- **pnpm engine** (`engine/pm/commands`): verifies `pnpm`, `@pnpm/exe`, and the host platform binary, **only on a store cache miss** (an actual download), so it adds no network round trip to every command. It **fails closed** — any verification failure, including an unreachable registry, refuses the version switch rather than running an unverified binary.

## Keeping the embedded keys fresh

The embedded keys live in a generated file. `deps/security/signatures/scripts/update-npm-signing-keys.mjs` keeps them in sync with npm's keys endpoint (`pnpm check:npm-signing-keys` / `--update`), and the **create-release-pr** workflow runs the check as a gate, so a key rotation cannot silently break verification — a stale key set blocks the release until refreshed.

## Pacquet parity

pacquet gained `configDependencies` support on `main` (#12285), but it has **no install-engine-spawn sink** — pacquet *is* the engine, and it does not select/spawn an alternate engine from `configDependencies` (its only config-dependency code-execution path is `updateConfig` plugin pnpmfiles, which it shares with pnpm and which this advisory does not cover). So CAND-PNPM-097 has no pacquet-side analog; no pacquet code change is needed.
2026-06-09 23:37:20 +02:00
Abdullah Alaqeel
97e1982d28 fix: detect enableGlobalVirtualStore toggle in workspace state check (#12147)
* fix: detect enableGlobalVirtualStore toggle in workspace state check (#12142)

The optimisticRepeatInstall fast path bypasses validateModules entirely,
so toggling enableGlobalVirtualStore off was not detected — pnpm printed
"Already up to date" without reinstalling deps in the new virtual store
location.

Add enableGlobalVirtualStore to WORKSPACE_STATE_SETTING_KEYS so
checkDepsStatus detects the change and falls through to the full
mutateModules path (which includes the virtualStoreDir compatibility
check and purge).

* refactor: bidirectional settings comparison in checkDepsStatus

Iterate over WORKSPACE_STATE_SETTING_KEYS instead of
Object.entries(workspaceState.settings) so that settings absent from
legacy state files (e.g. enableGlobalVirtualStore) are still compared
against the current config. Removes the ad-hoc allowBuilds null-check
block since the main loop now covers it with the ?? {} coalescing.

* fix(workspace-state): track enableGlobalVirtualStore in pacquet state

Port pnpm/pnpm#12147 to pacquet. pnpm's checkDepsStatus now iterates the
full WORKSPACE_STATE_SETTING_KEYS list, so a key pacquet omits from
.pnpm-workspace-state-v1.json is read as `undefined` and compared
against pnpm's resolved config. `enableGlobalVirtualStore` resolves to a
concrete default (true, or false under CI), so omitting it would make
pnpm reinstall after a pacquet install — and pacquet itself never
detected the toggle (its own copy of pnpm/pnpm#12142).

Add `enable_global_virtual_store` to WorkspaceStateSettings;
current_settings writes `then_some(true)` (omit when off, mirroring
pnpm omitting its undefined default); settings_match coerces
`None`/`Some(false)` as equal before comparing, matching the existing
allow_builds/package_extensions handling.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-08 17:15:49 +02:00
Zoltan Kochan
4b4d38361c chore(release): 11.5.2 (#12207) 2026-06-05 08:27:41 +02:00
Zoltan Kochan
0f509d055f chore(release): 11.5.1 (#12126) 2026-06-02 08:07:46 +02:00
Abdullah Alaqeel
719cc21c6f fix(audit): prune path traversal (#12087)
* fix(audit): prune path traversal

* fix(audit): memoize placeholder set before recursion to preserve cycle reachability

The reachable-vulnerabilities getter returned a non-memoized empty Set for
back-edges, causing incomplete results for nodes in dependency cycles. Memoize
the result Set immediately so the same mutable placeholder is returned for
back-edges and filled as recursion unwinds.

* fix(audit): only memoize acyclic reachability subtrees

The placeholder-before-recursion approach only made the SCC entry node's
reachable set correct; non-entry cycle members were memoized with an
under-approximated set, dropping valid audit paths reached through them.
Cache a node's reachable vulnerabilities only when no descendant back-edges
to an ancestor; recompute cycle-touching nodes per query.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-01 15:02:09 +02:00
Zoltan Kochan
b741d91e67 chore(release): 11.5.0 (#12068) 2026-05-29 17:26:13 +02:00
Marvin Hagemeister
49e6074644 test: replace @pnpm/registry-mock with an in-repo in-process registry (#11927)
Replace the external `@pnpm/registry-mock` (Verdaccio) test dependency with an in-repo, in-process registry that serves package fixtures to **both** the pacquet Rust tests and the pnpm CLI (Jest) tests. No separately managed registry process is needed.

### How it works

- **Fixtures** live at `registry/.fixtures/packages/<name>/<version>/…`, moved verbatim from [`pnpm/registry-mock`](https://github.com/pnpm/registry-mock) (keyed by each `package.json`'s `name`+`version`).
- **`pnpm-registry-fixtures`** builds verdaccio-shaped storage from those fixtures; the in-tree **`pnpm-registry`** crate serves it.
  - Files whose names differ only by case (`@pnpm.e2e/with-same-file-in-different-cases`) and `bundleDependencies` trees are composed **in memory** by the builder, since neither can be committed to the working tree.
- **pacquet**: `pacquet-testing-utils`' `TestRegistry` starts the server lazily (once per process) in proxy mode, serving `@pnpm.e2e` fixtures locally and falling through to the npm uplink for real packages (`is-positive`, `is-negative`, …) — matching how registry-mock behaved.
- **pnpm CLI**: the `with-registry` Jest `globalSetup` builds storage from the fixtures via the new `pnpm-registry-prepare` binary (built from source in the Test CI job) and serves it with `pnpm-registry`. `REGISTRY_MOCK_PORT` / `REGISTRY_MOCK_CREDENTIALS` / `getIntegrity` now come from `@pnpm/testing.registry-mock`.

### Result

`@pnpm/registry-mock` is removed from every manifest, the catalog, and `packageExtensions`; `cargo test` / `cargo nextest run` / `just test` and the pnpm CLI Jest suites all run registry-backed tests without launching Verdaccio.
2026-05-29 14:35:45 +02:00
Abdullah Alaqeel
2cadfb5d3d refactor: replace enquirer with @inquirer/prompts (#11942)
Replaces the unmaintained `enquirer` package with `@inquirer/prompts` for all interactive CLI prompts. Fixes the `update -i` scrolling overflow bug where long choice lists were clipped in the terminal.

Fixes #6643

## User-facing changes

- **`pnpm update -i` / `pnpm update -i --latest`**: Scrolling now works correctly when many packages are available; the new library uses visual-line-aware pagination via `usePagination`
- **`pnpm audit --fix -i`**: Same scrolling fix for vulnerability selection
- **`pnpm approve-builds`**: Interactive build approval prompts updated
- **`pnpm patch`**: Version selection and "apply to all" prompts updated
- **`pnpm patch-remove`**: Patch removal selection updated
- **`pnpm publish`**: Branch confirmation prompt updated
- **`pnpm login`**: Credential prompts updated
- **`pnpm run` / `pnpm exec`** (with `verifyDepsBeforeRun=prompt`): Confirmation prompt updated

## Internal changes

- `OtpEnquirer` DI interface changed from `{ prompt }` to `{ input }`
- `LoginEnquirer` DI interface changed from `{ prompt }` to `{ input, password }`
- `enquirer` removed from catalog and all 8 package.json files
- `@inquirer/prompts` v8.4.3 added to catalog and all 8 package.json files
- Removed `OtpPromptOptions` and `OtpPromptResponse` exports from `@pnpm/network.web-auth` (no longer needed)

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-28 17:53:52 +02:00
Zoltan Kochan
72d997cc34 chore(release): 11.4.0 (#11989) 2026-05-27 15:15:01 +02:00
Zoltan Kochan
a23956e3ab fix(config/reader): pin unscoped per-registry settings to their source's registry at load time (#11953)
* fix(config/reader): drop user-level default auth when workspace overrides registry

When a workspace `.npmrc` overrides `registry=` to a different value than the
user's `~/.npmrc` or `~/.config/pnpm/auth.ini` would have set, do not bind
unscoped/default credentials (`_authToken`, `_auth`, `username`/`_password`)
from the user-level config to the workspace-selected registry. The previous
behavior leaked user-trusted credentials to whatever registry an untrusted
workspace `.npmrc` pointed at. Reported by JUNYI LIU.

* chore(cspell): allow JUNYI in changeset and tests

* fix(config/reader): also defend when pnpm-workspace.yaml overrides registry

Move the rebind defense to after all config layers (CLI, env vars,
pnpm-workspace.yaml, .npmrc) have settled. Compare the final resolved
default registry against what the user-level config alone would produce,
and skip the check entirely if the user requested a registry via CLI/env
themselves.

* feat(config/reader): deprecate unscoped authentication credentials

Emit a per-file warning whenever an .npmrc or auth.ini contains an
unscoped auth value (_authToken, _auth, username, _password,
tokenHelper). URL-scoped tokens have been npm's recommended pattern
since npm@9, and unscoped credentials are slated for removal in a
future major. The warning fires independently of whether the rebind
defense rejects the credentials, so users see the deprecation even when
their setup happens to be safe today.

* refactor(config/reader): rescope unscoped credentials at load time instead of detecting rebinds post-merge

Each .npmrc / auth.ini / CLI source's unscoped credential keys
(_authToken, _auth, username, _password, tokenHelper) are rewritten to
their URL-scoped equivalent during load, using the same source's
registry= value (or the npmjs default if it declares none). A later
layer overriding registry= can no longer rebind a credential to its own
registry — the credential is already pinned to the URL its author
intended.

This removes the post-merge source-tracking defense and replaces it
with the simpler per-source normalization. Each rescope emits a
deprecation warning so users migrate to writing the URL-scoped form
directly.

* refactor(network/auth-header): drop empty-string default-registry slot

After load-time rescoping, no source can populate configByUri[''] —
every credential is either URL-scoped from the start or rewritten to
the URL-scoped form during the .npmrc / auth.ini / CLI parse. The
runtime fallback that re-keyed configByUri[''] onto the merged default
registry, and the publish-side fallback that read it, are both dead
code.

Removed:
- empty-string handling in getAuthHeadersFromCreds, including its
  defaultRegistry parameter
- defaultRegistry parameter from createGetAuthHeaderByURI
- the corresponding dedicated unit test
- the configByUri['']?.creds fallback in publishPackedPkg.ts
- empty-key assertions in config/reader tests

Updated all ~16 call sites of createGetAuthHeaderByURI to drop the now
unused second argument.

* feat(config/reader): extend per-source rescoping to client TLS cert/key

The same trust-boundary issue that affected unscoped credentials applies
to client TLS settings: an unscoped cert=/key= would be presented to
whatever registry the merged config settles on, even if a later layer
(workspace .npmrc, pnpm-workspace.yaml, CLI flag) overrode it. The
existing rescope helper now also rewrites unscoped `cert` and `key`
to their URL-scoped form, pinning them to the registry their author
named in the same source.

`ca`/`cafile` are intentionally left unscoped: they're trust anchors,
not credentials, and corporate MITM-proxy setups depend on them
applying to every HTTPS request. The default-registry override can't
weaponize an unscoped CA — the attacker would need a cert signed by it.

`certfile`/`keyfile` (file-path variants) are not rescoped either:
`certfile` isn't read unscoped by pnpm today (asymmetric vs. `keyfile`
in NPM_AUTH_SETTINGS), and supporting only one of them would be
confusing. Users wanting the path form can write it URL-scoped
directly.

* chore(config/reader): remove dead unscoped `keyfile` allowlist entry

`keyfile` was listed in NPM_AUTH_SETTINGS so unscoped `keyfile=<path>`
passed the .npmrc filter and ended up in authConfig — but nothing in
the codebase ever read it from there. The dispatcher uses `opts.key`
(inline PEM) and `configByUri[host].tls.key` (URL-scoped path/inline
content), neither of which is populated from unscoped `keyfile=`.

`certfile` was already absent from the allowlist for the same reason,
so this also removes the asymmetry between the two file-path variants.
URL-scoped `//host/:certfile=...` and `//host/:keyfile=...` continue
to work via `tryParseSslKey` and are unaffected.

* test(network/auth-header): drop test for removed default-registry slot

This test exercised the configByUri[''] re-keying path that was
removed in the rescope-at-load refactor. With createGetAuthHeaderByURI
no longer accepting a defaultRegistry parameter and unscoped
credentials no longer reaching the merged config, the scenario the
test described is structurally unreachable.

* fix(config/reader): handle empty/invalid registry value in rescope

Two CI fixes:

1. When a source's `registry=` resolves to an empty string (e.g. an
   unresolved `${ENV_VAR}` placeholder), `new URL(...)` inside
   `nerfDart` throws. Guard the call with try/catch: drop the
   unscoped per-registry keys (a bare token has nowhere safe to bind)
   and emit a warning naming the offending source.

2. Update `.npmrc does not load pnpm settings` to expect the rescoped
   form of unscoped `_authToken`/`username` in `authConfig` — they
   now appear as `//registry.npmjs.org/:_authToken` etc. since the
   test's .npmrc declares no `registry=` of its own.

* chore(cspell): allow "rescoping"

* test(installing/deps-installer): drop "legacy way" auth test

This test passed credentials via the configByUri[''] empty-string slot,
which the auth-header layer re-keyed to the merged default registry at
request time. That slot was removed in the rescope-at-load refactor —
credentials are now always URL-scoped before they reach configByUri,
so the empty-key entry is unreachable from any code path.

The scenario the test covered (basicAuth via username/password) is
already exercised by the existing "installing a package that need
authentication, using password" test using the URL-scoped form.
2026-05-26 16:46:50 +02:00
Puneet Dixit
35d235542e fix: validate devEngines runtime onFail (#11822)
Fixes #11818

## Summary

`devEngines.runtime` / `engines.runtime` entries with `onFail: error` or `warn` silently did nothing — only `onFail: download` had any effect. This PR wires up validation for all three supported runtimes (node, deno, bun).

- Add `getSystemDenoVersion` / `getSystemBunVersion` and a generic `getSystemRuntimeVersion(name)` dispatcher in the runtime-version helper package.
- Walk each runtime entry in the manifest during pnpm startup, compare to the live system runtime, and throw `ERR_PNPM_BAD_RUNTIME_VERSION` (or warn) on a mismatch. Invalid ranges (e.g. `"invalid range"`) are reported instead of crashing `semver.minVersion`. Missing runtimes ("no Node.js on the system") get the same error path.
- The shell-out for deno/bun only runs when the manifest configures them AND `onFail` is `error`/`warn`. `download`/`ignore` short-circuit, and projects with no runtime pin pay nothing. Memoized per runtime.
- `pnpm --version`, `pnpm --help`, and `pnpm <cmd> --global` are exempt from the check.
- Rename `@pnpm/engine.runtime.system-node-version` → `@pnpm/engine.runtime.system-version` to match its broader scope; hoist `RuntimeName` / `RUNTIME_NAMES` / `isRuntimeAlias` to `@pnpm/types` so callers don't need to depend on `pkg-manifest.utils` just for the alias check.

## Tests

- `pnpm --filter pnpm run compile`
- `pnpm --filter pnpm exec jest packageManagerCheck.test` — 42 passing. New coverage: node/deno/bun version mismatch, invalid range, missing range, multi-entry runtime arrays, `engines.runtime` path (not just `devEngines.runtime`), and the `pnpm --version` exemption.
- `pnpm --filter @pnpm/engine.runtime.system-version test` — 10 passing, 100% statement coverage; unit tests for each helper and the dispatcher.
- Manual end-to-end smoke tests against the rebuilt bundle for deno and bun version mismatch.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

## Release Notes

* **New Features**
  * Added runtime version validation for Node.js, Deno, and Bun. The system now enforces `devEngines.runtime` and `engines.runtime` declarations with configurable failure behavior (`error`, `warn`, or `ignore`).
  * Enhanced error messages for runtime version mismatches with helpful suggestions for overrides.

* **Improvements**
  * Improved system runtime detection and version checking across multiple runtime environments.

---------

Co-authored-by: Puneet Dixit <236133619+puneetdixit200@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-26 10:29:40 +02:00
Zoltan Kochan
ae2175829a feat(registry-access): extract dist-tag + adduser helpers, dogfood from tests (#11926)
* feat(registry-access): extract setDistTag and dogfood from tests

Add `@pnpm/registry-access.commands#setDistTag` — the low-level PUT to
`/-/package/:pkg/dist-tags/:tag`. The CLI `dist-tag add` handler now
calls it instead of issuing the fetch inline.

Tests in this monorepo now use a thin new package
`@pnpm/testing.registry-mock` (REGISTRY_MOCK_PORT + REGISTRY_MOCK_CREDENTIALS
baked in) that delegates to `setDistTag`, replacing `addDistTag` from
`@pnpm/registry-mock`. That dropped helper relied on
`anonymous-npm-registry-client` and a verdaccio-era
fetch-then-DELETE-then-PUT dance that is no longer needed against
pnpm-registry.

39 test files swapped from `@pnpm/registry-mock` to
`@pnpm/testing.registry-mock`.

* fix: move setDistTag to its own package to break tsconfig project-reference cycle

testing/registry-mock → registry-access.commands → releasing/commands
→ installing/commands → installing/deps-installer → testing/registry-mock.

Extract setDistTag into @pnpm/registry-access.set-dist-tag (only depends
on @pnpm/error, @pnpm/network.fetch, @pnpm/npm-package-arg). Both
@pnpm/registry-access.commands and @pnpm/testing.registry-mock import
from it. Cycle gone.

* feat(registry-access): extract addUser helper, dogfood from login + tests

Add @pnpm/registry-access.add-user — a small helper that PUTs to
/-/user/org.couchdb.user:<name> and returns { token }. The CLI's
classicLogin (pnpm login fallback path) now calls it, and tests
use it via @pnpm/testing.registry-mock instead of the legacy
addUser from @pnpm/registry-mock.

Swapped 3 call sites: globalSetup.js, installing/deps-installer's
auth.ts, and pnpm/test/dlx.ts. AddUserHttpError exposes status +
text + parsed-json-if-applicable + headers so the CLI can still
do its OTP detection. One webauth-OTP login test mock had to be
adjusted to provide its body via `text` (JSON-stringified) rather
than `json` only, since the helper consumes the body via `text()`.

* refactor: consolidate set-dist-tag + add-user helpers into one @pnpm/registry-access.client package

One shared package is better than splitting per endpoint. Future endpoints
(publish, deprecate, etc.) can land here without another wrapper.

No behavioral change — same setDistTag and addUser exports as before,
just under one roof. Callers updated: registry-access.commands,
auth.commands, testing.registry-mock.

* fix(registry-access): sort imports
2026-05-25 14:01:00 +02:00
Zoltan Kochan
f2a4d2caef chore(release): 11.3.0 (#11894) 2026-05-24 02:23:07 +02:00
Alessio Attilio
22cb743672 feat: implement native 'pnpm repo' command (#11505)
* feat: implement native 'pnpm repo' command

* fix(deps.inspection.commands): preserve repository.directory in fetchPackageInfo

`fetchPackageInfo` flattened `repository` to its URL string, dropping
`directory`. `pnpm repo <pkg>` therefore couldn't append the monorepo
subdirectory for registry packages even though `pickRepoUrl` supported
it. Keep the original repository value so the URL builder receives both
`url` and `directory`.

Also add the missing changeset for the `pnpm repo` command.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-24 00:14:04 +02:00
Zoltan Kochan
501681044e chore(release): 11.2.2 (#11817) 2026-05-21 15:45:17 +02:00
Zoltan Kochan
11a43b15da chore(release): 11.2.1 (#11777) 2026-05-20 16:51:13 +02:00
Zoltan Kochan
0fb723323f chore(release): 11.2.0 (#11764) 2026-05-20 12:41:09 +02:00
Tim Haines
04a2c9c610 test(outdated): add regression test for minimumReleaseAge (#11699)
Adds a regression test for #11698. The underlying fix already shipped as part of #11705 (which removed `strictPublishedByCheck` entirely and routed maturity decisions through `policyViolation`), so this PR now lands only the dedicated test that locks in the behavior.

## What the test covers

`deps/inspection/commands/test/outdated/minimumReleaseAge.test.ts`:

- **Baseline** — without an age policy, `pnpm outdated` reports `is-negative@2.1.0` as an available upgrade (sanity check that the fixture actually has outdated deps).
- **Regression** — with `minimumReleaseAge` set to a cutoff so far in the past that every published version is immature, `pnpm outdated` reports nothing: `exitCode === 0` and `2.1.0` does not appear. Before #11705 this test went red because the non-strict resolver fallback re-picked the immature `latest` ignoring `publishedBy`.

The `allImmatureMinimumReleaseAge = Date.now() / (60 * 1000)` trick (cutoff = epoch in minutes) is date-independent and matches the technique already used in the install-side `minimumReleaseAge` suite.

## Why a test-only PR

The original PR proposed flipping `strictPublishedByCheck` in `createManifestGetter`, but #11705 deleted that option entirely and replaced it with an always-defer model (`policyViolation` flows through `ResolveResult` → `getManifest` returns `null` on `MINIMUM_RELEASE_AGE_VIOLATION`). The test was the durable contribution; preserving it as a regression gate is worth keeping.
2026-05-20 01:02:49 +02:00
Zoltan Kochan
1627943d2a feat(outdated): include node, deno, and bun runtimes (#11739)
`pnpm outdated` and `pnpm update --interactive` previously skipped runtime dependencies (`node`/`deno`/`bun` installed via the `runtime:` protocol). Both commands go through `outdatedDepsOfProjects` → `outdated()`, and the inner loop bailed out for anything `parseBareSpecifier` couldn't parse — which is everything `runtime:`-shaped. A runtime was only ever reported if the current install differed from the wanted lockfile entry, so the latest available version was never surfaced. The same gap silently affected `jsr:` and named-registry deps too.

Commits, smallest fix first → progressively cleaner architecture:

1. **`feat(outdated)`** — minimal fix: special-case runtime deps in `outdated.ts` so they appear in the table and the interactive update picker.
2. **`refactor(outdated)`** — per-resolver dispatch. Each protocol resolver gets its own "what's the latest?" function; `@pnpm/resolving.default-resolver` composes them.
3. **`refactor(outdated)`** — rename to `resolveLatest` (the function returns info regardless of whether the dep is outdated; "outdated" described a state, not an action).
4. **`refactor(outdated)`** — let the local-resolver own the `link:`/`file:` skip, drop the matching short-circuit in `outdated.ts`.
5. **`refactor(outdated)`** — slim `LatestQuery` / `LatestInfo` to the bare essentials; move `pickRegistryForPackage` into the npm-resolver where it belongs; derive `current`/`wanted` display from `pkgSnapshot.version` in `outdated.ts`.
6. **`chore(outdated)`** — drop stale tsconfig project reference left behind by #5.
7. **`refactor(outdated)`** — drop `wantedRef` from the query; resolvers detect protocol from `bareSpecifier` alone.

## Final architecture

`@pnpm/resolving.resolver-base` defines a single tiny protocol:

```ts
interface LatestQuery {
  wantedDependency: WantedDependency
  compatible?: boolean
}

interface LatestInfo {
  latestManifest?: PackageManifest
}

type ResolveLatestFunction = (query: LatestQuery, opts: ResolveOptions) =>
  Promise<LatestInfo | undefined>
```

- `undefined` from a resolver means "I don't claim this dep — try the next one."
- `{}` means "I claim it, but I can't tell you what's latest" (policy-blocked, network unavailable, or a protocol with no concept of latest — git/tarball).
- `{ latestManifest }` is the happy path.

Each protocol resolver (npm/jsr/named-registry, git, tarball, local, node/bun/deno runtimes) exports its own `resolveLatest*` function alongside its `resolve*`. `@pnpm/resolving.default-resolver` composes them into a single first-match dispatcher, surfaced through `@pnpm/installing.client` as `createResolver(...).resolveLatest`.

`outdated.ts` is protocol-agnostic: dispatches, then derives `current`/`wanted` display from `pkgSnapshot.version` (falling back to the raw ref for URL-shaped refs where the URL is the only diff signal between commits), uses raw `wantedRef !== currentRef` for the lockfile-shifted check, and pulls `packageName` from `dp.parse(relativeDepPath).name` so aliased deps still report under the real package name.

Per-resolver responsibilities:
- **npm-resolver** (`resolveLatestFromNpm` / `resolveLatestFromJsr` / `resolveLatestFromNamedRegistry`): match their respective spec shapes, call the matching `resolveFromX` with `'latest'` (or the original spec under `--compatible`), handle `MINIMUM_RELEASE_AGE_VIOLATION` and `ERR_PNPM_NO_MATCHING_VERSION` so policy-blocked deps don't surface as available updates. Picks the per-package registry internally via its ctx.
- **node/bun/deno runtime resolvers**: claim deps via `bareSpecifier.startsWith('runtime:')` + alias match, query their release sources for the latest version (only the version — no asset-hash fetches), return `{ latestManifest }`.
- **git / tarball resolvers**: claim deps via spec shape, return `{}` (no concept of "latest"); the caller still surfaces a ref-mismatch report if the lockfile shifted to a different commit/URL.
- **local-resolver**: returns `undefined` so `link:`/`file:`/`workspace:` deps fall through and get silently skipped.
2026-05-19 19:15:07 +02:00
Zoltan Kochan
c8d8fde6ca feat(config-deps): support optionalDependencies with platform filtering (#11725)
Extends `configDependencies` to resolve and install one level of `optionalDependencies`, with `os` / `cpu` / `libc` platform filtering applied at install time. Closes the prerequisite called out in #11723: this is what makes the esbuild/swc-style platform-binary pattern viable for config dependencies (e.g. shipping pacquet as a config dep with native binaries via `optionalDependencies`).

### What lands

- **Resolution** (`resolveOptionalSubdeps.ts`, wired into `resolveConfigDeps` and `resolveAndInstallConfigDeps`): after each top-level config dep resolves, walks one level of `optionalDependencies`, resolves each, and records them in the env lockfile with `os`/`cpu`/`libc` preserved. The parent's snapshot gets `optionalDependencies: { … }`. All variants are recorded regardless of host platform, so the env lockfile stays portable across machines.
- **Install** (`installConfigDeps.ts`): after the parent is installed into its GVS leaf, fetches each platform-compatible subdep into its own GVS leaf and creates a sibling symlink inside the parent leaf's `node_modules/`. Node's `realpath`-based resolution then makes `require('pkg-platform-arch')` from inside the parent resolve correctly. Stale siblings are pruned, so platform changes between runs produce a clean layout.
- **GVS hash** (new `calcGlobalVirtualStorePathWithSubdeps` in `graph-hasher`): the parent's GVS leaf hash now folds in the optional subdeps' full pkg ids. Without this, changing a subdep version while keeping the parent pinned would land in the same leaf and silently overwrite the sibling symlinks. The leaf function keeps its original "no children" contract; the new function is a separate entry point that pacquet can mirror cleanly.
- **Re-install detection**: the "skip if already installed" check compares the existing `.pnpm-config/{name}` symlink's `realpath` against the expected GVS leaf, not the package.json's name/version. With subdep versions now feeding the leaf hash, name/version alone isn't sufficient. The check only short-circuits the parent's re-import and re-symlink — `installOptionalSubdeps` always runs so platform-specific siblings get pruned and relinked when the host's effective platform changes (Rosetta x64 ↔ arm64, etc.).
- **Exact versions only**: subdep specifiers must be valid semver exact versions (e.g. `"1.2.3"`). Ranges (`"^1.0.0"`) and tags (`"latest"`) are rejected up-front with a `CONFIG_DEP_OPTIONAL_NOT_EXACT` error. With the parent pinned by integrity, the subdep's resolved version mustn't drift between machines.
- **Error handling**: optional-subdep resolution failures are logged via `skippedOptionalDependencyLogger` with `reason: 'resolution_failure'` (same shape as `installing/deps-resolver`) and the install continues — except for `ERR_PNPM_TRUST_DOWNGRADE`, which is a security signal that must still abort the install.

### Scope

Only one level deep. Transitive `dependencies` and lifecycle scripts remain unsupported — pacquet doesn't need them yet, and they carry meaningful security and complexity tradeoffs that deserve a separate discussion.

The env lockfile schema needs no changes: `LockfilePackageInfo` already carries `os`/`cpu`/`libc`, and `LockfilePackageSnapshot.optionalDependencies` already exists for recording the parent→child edge.

## Known limitation

If a workspace already had a resolved config dep in the env lockfile (`snapshots[pkgKey] = {}`) before this PR, optional subdeps won't be retroactively discovered on subsequent installs. Workaround: `pnpm update <pkg>` (or remove + re-add). In practice no published package today relies on `optionalDependencies` in a config dep — they couldn't, since the feature didn't exist — so the practical exposure is narrow. See the inline review thread for the design rationale.
2026-05-19 01:29:25 +02:00
Zoltan Kochan
cd80b2c8ae chore(release): 11.1.3 (#11717) 2026-05-18 15:42:32 +02:00
Zoltan Kochan
4195766f10 feat: tighten minimumReleaseAge — auto-exclude, lockfile verification, and interactive prompt (#11705)
Three coordinated changes that close the silent-bypass gap in loose `minimumReleaseAge` mode AND the discover-by-loop UX problem in strict mode (#10488), plus a parallel hardening of the lockfile verifier:

1. **Auto-collect into `minimumReleaseAgeExclude` (loose mode)** — fresh resolutions that fall back to a version newer than the cutoff are auto-recorded into the workspace manifest's `minimumReleaseAgeExclude`. A single info message lists what was persisted. The workspace manifest writer dedupes against existing entries.

2. **Lockfile verifier runs in loose mode too** — `createNpmResolutionVerifier` no longer gates on `minimumReleaseAgeStrict`. With auto-collect keeping the exclude list explicit, every accepted-immature pin must be on the list — same contract strict mode enforces. Lockfiles produced under a weaker (or absent) policy that still hold immature entries are rejected the same way strict mode would.

3. **Strict mode prompts on the aggregate set instead of throwing on the first** — the resolver always collects every immature direct and transitive in one pass; the install command's `handleResolutionPolicyViolations` checkpoint decides what to do with the set. Interactive (TTY) prompts the user once with the full list (default = No) and asks whether to add them all to `minimumReleaseAgeExclude` and proceed. Approve → install continues, persisted at the end. Decline → resolution aborts before the lockfile, package.json, or modules dir is touched. Non-interactive (CI) keeps `ERR_PNPM_NO_MATURE_MATCHING_VERSION` as the exit code but lists every offending entry instead of just the first one the resolver happened to hit.

4. **The lockfile verifier now also covers `trustPolicy: 'no-downgrade'`.** The same post-resolution gate that re-checks `minimumReleaseAge` on lockfile entries now re-runs `failIfTrustDowngraded` for every npm-registry entry whose name isn't on `trustPolicyExclude`. The two checks share a single full-metadata fetch per package, so the extra coverage doesn't cost an extra round trip when both policies are active. Resolver-time trust checks still run as before — this just closes the gap when an entry bypasses resolution (peek path, `--frozen-lockfile`, restored CI cache).

The steady-state flows:

- **Loose mode, `pnpm add foo@immature`**: lockfile clean, verifier no-op, resolver picks via lowest-version fallback, `foo@immature` lands in `minimumReleaseAgeExclude`, install succeeds. Subsequent `pnpm install --frozen-lockfile` in CI verifies against the populated list and succeeds.
- **Strict mode (interactive), security bump to `next@15.5.9`**: resolver collects `next@15.5.9` AND every immature `@next/swc-*@15.5.9` shim. pnpm prompts once with the full list. User approves → install completes, all entries persisted in `pnpm-workspace.yaml`. CI then runs the populated config cleanly.
- **Strict mode (non-interactive / CI)**: aborts with `ERR_PNPM_NO_MATURE_MATCHING_VERSION` listing every immature entry's `name@version` and publish time — no more discover-by-loop dance.
- **Teammate commits a poisoned lockfile**: single-policy batches reject with `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` (or `ERR_PNPM_TRUST_DOWNGRADE`); a batch that trips both policies escalates to the generic `ERR_PNPM_LOCKFILE_RESOLUTION_VERIFICATION` and lists each entry's per-policy code in the breakdown.

### Implementation

- The npm resolver always falls back to the lowest matching version when no mature version satisfies the range, and flags the result with `ResolveResult.policyViolation` instead of throwing `NO_MATURE_MATCHING_VERSION`. `deferImmatureDecision` and `strictPublishedByCheck` are gone — every caller (install, dlx, outdated, self-update) inspects the violation and decides what to do.
- `policyViolation` flows from `ResolveResult` → `PackageResponse.body.policyViolation` → a shared accumulator in `ResolutionContext` → the `resolutionPolicyViolations` field on `resolveDependencyTree`'s return → out through `mutateModules` / `addDependenciesToPackage` to the install command.
- The violation type lives in `@pnpm/resolving.resolver-base` as `ResolutionPolicyViolation`; the npm resolver exports the two built-in codes (`MINIMUM_RELEASE_AGE_VIOLATION_CODE`, `TRUST_DOWNGRADE_VIOLATION_CODE`) as constants so consumers reference one source of truth.
- `handleResolutionPolicyViolations` runs between `resolveDependencyTree` and `resolvePeers` — the resolver-agnostic checkpoint where the install command's plan prompts (TTY) or aborts (no-TTY) with the full violation list.
- `setupPolicyHandlers` (in `installing/commands/src/policyHandlers.ts`) composes per-policy handlers behind a uniform plan interface: each handler has its own `handleResolutionPolicyViolations` (filter by code, decide what to do) and `pickManifestUpdates` (return a typed `WorkspaceManifestPolicyUpdates` patch the install command spreads into `updateWorkspaceManifest`). Today the only registered handler is `createMinimumReleaseAgeHandler` — strict + TTY prompts via `enquirer`, strict no-TTY throws `ERR_PNPM_NO_MATURE_MATCHING_VERSION` with every entry listed, loose mode auto-persists at the tail. Strict + `--no-save` is rejected up-front via `ERR_PNPM_STRICT_MIN_RELEASE_AGE_REQUIRES_SAVE`. Future policies plug in via a sibling factory + push into the handlers list, with no changes to `installDeps.ts` / `recursive.ts`.
- `installDeps` / `recursive` drain `pickManifestUpdates` after install and spread the patch into `updateWorkspaceManifest`. Plain `pnpm install` (no `--update`, no params) now still updates the workspace manifest when any handler contributes a patch. The `install` command's CLI schema gained `save: Boolean` so `--no-save` actually flows through to `opts.save = false` instead of being silently dropped by nopt.
- `makeResolutionStrict` (in `installing/client`) wraps a `ResolveFunction` and rethrows any `policyViolation` as a `PnpmError`. Used by `dlx` and `self-update` under strict `minimumReleaseAge` OR `trustPolicy: 'no-downgrade'`, since one-shot callers have nowhere to defer a violation to. Violation-code → error-code mapping lives in one place so future violation kinds get consistent UX.
- `createNpmResolutionVerifier` extends its check to `trustPolicy: 'no-downgrade'` — same per-entry fan-out, same cache key, sharing the full-metadata fetch with the maturity check. Trust-fetch errors now propagate up so the violation reason carries the underlying message (network code, 404 detail) instead of a generic "metadata is unavailable".
- `verifyLockfileResolutions`'s aggregate throw uses the per-policy code when every violation in the batch shares it, and escalates to a generic `LOCKFILE_RESOLUTION_VERIFICATION` (with per-entry codes in the breakdown) for mixed batches.
- The pnpm agent path refuses installs under `trustPolicy: 'no-downgrade'` (`ERR_PNPM_TRUST_POLICY_INCOMPATIBLE_WITH_AGENT`) — the agent has no server-side counterpart to that check yet, so silently allowing it would land a lockfile the local verifier would later reject. `minimumReleaseAge` is forwarded to the agent and enforced server-side, so that combination is fine.

### Pacquet parity

Pacquet only carries a stub reference to `minimumReleaseAgeExclude` (see `pacquet/crates/package-manager/src/version_policy.rs`); the broader `minimumReleaseAge` and `trustPolicy` policies aren't ported yet, so this feature is outside pacquet's current surface area. It'll come along when pacquet ports the policies.

### Closes

- Closes #10488 (resolves the discover-by-loop dance for security bumps without needing `withTransitives`).
2026-05-18 09:51:11 +02:00
Zoltan Kochan
5dc8be8a42 fix(graph-hasher): resolve GVS engine per-snapshot for runtime-pinned deps (#11693)
Closes #11690.

A dependency that declares `engines.runtime` in its manifest carries the desugared `dependencies.node: 'runtime:<version>'` pin in the lockfile, and pnpm's bin linker spawns that dep's lifecycle scripts through the pinned Node downloaded into `<pkgDir>/node_modules/node/`. The GVS hash and the side-effects-cache key prefix were still anchored to the install-wide runtime — so the pinning snapshot's slot encoded the wrong Node major, and a reinstall on the same host could read the cached side-effects under a key whose `<platform>;<arch>;node<major>` triple disagreed with the Node the build actually ran on.

Per-snapshot resolution now matches what `bins/linker` already does on a per-package basis: a snapshot's own pin wins; the install-wide value (from #11689's `findRuntimeNodeVersion`) is the fallback.

### TypeScript

- `deps/graph-hasher/src/index.ts:72-77` — adds `readSnapshotRuntimePin(children)`: pulls the bare Node version from a graph node's `children.node` entry when that points at a `node@runtime:<version>` snapshot. Factors out a small `extractRuntimeNodeVersion(snapshotKey)` parser shared with `findRuntimeNodeVersion`.
- `deps/graph-hasher/src/index.ts:115-116,245-246` — `calcDepState` and `calcGraphNodeHash` consult `readSnapshotRuntimePin(graph[depPath].children)` first and only fall back to the install-wide `nodeVersion` parameter when the snapshot doesn't pin its own Node. No caller changes required — install-wide fallback continues to be computed via `findRuntimeNodeVersion(Object.keys(graph))` at each call site.
- **Refactor (separate commit):** `findRuntimeNodeVersion` moved from `@pnpm/engine.runtime.system-node-version` to `@pnpm/deps.graph-hasher` (along with the new `readSnapshotRuntimePin`). `system-node-version` is about probing the *host* Node — `getSystemNodeVersion`, `engineName`. The lockfile-shape parsers fit better next to the package that actually composes the engine string. Every caller already depended on graph-hasher, so no new deps; six packages drop the now-unused dependency on `system-node-version`.

### Pacquet

- `pacquet/crates/package-manager/src/install_frozen_lockfile.rs:1309-1345` — new `find_own_runtime_node_major(snapshot)` reads a snapshot's `dependencies` for a `node` entry with `Prefix::Runtime`, returning the bare major.
- `pacquet/crates/package-manager/src/virtual_store_layout.rs:178-205` — `VirtualStoreLayout::new` resolves engine per-snapshot inside the hash loop via `engine_name(own_major, None, None)` when the snapshot pins, otherwise inherits the install-wide `engine` argument.

### Migration

Snapshots of dependencies that declare their own `engines.runtime` re-hash under that dep's pinned Node instead of the install-wide value. Old slots become prune-eligible on next install.
2026-05-17 13:25:05 +02:00
Zoltan Kochan
3ddde2b975 fix(pacquet): align GVS slot layout with pnpm (#11689)
## Summary

Adds three end-to-end **GVS parity tests** under `pacquet/crates/cli/tests/pnpm_compatibility.rs` that run `pnpm install` and `pacquet install --frozen-lockfile` against the same workspace + lockfile with `enableGlobalVirtualStore: true`, then diff the resulting `<store>/v11/links/` slot trees. The tests surfaced three independent divergences, each fixed in its own commit set:

1. **`<store>/v11/links` prefix.** `getStorePath` appends `STORE_VERSION` (`v11`) to the configured `storeDir` before `extendInstallOptions.ts:352` joins `'links'` onto it, so pnpm's GVS lives at `<store>/v11/links/` — pacquet's `StoreDir::links()` was one level shallower, joining onto `self.root`. Same gap on `projects()`. Anchored both under `self.v11()` so the on-disk paths agree.

2. **GVS engine-name resolution.** `ENGINE_NAME` was computed from `process.version`, which is wrong in two cases:
   - **`@pnpm/exe` SEA bundle.** The bundle has its own embedded Node, not the `node` on PATH that runs lifecycle scripts. Two pnpm installs on the same machine (one SEA, one npm-package) therefore disagreed on the cache key, partitioning the side-effects cache and the global virtual store.
   - **`engines.runtime` / `devEngines.runtime` pin.** When a project pins a Node version, pnpm downloads that Node into `node_modules/node/` and uses it to run lifecycle scripts. But the hash still anchored to whichever Node ran pnpm itself, not to the pinned Node.

   `@pnpm/engine.runtime.system-node-version` now exports `engineName(nodeVersion?)` and `findRuntimeNodeVersion(snapshotKeys)`. The override has priority; otherwise the helper falls through to `getSystemNodeVersion()` — which already prefers shell `node --version` over `process.version` in SEA contexts — and finally to `process.version` as a last resort. `@pnpm/deps.graph-hasher`'s `calcDepState`, `calcGraphNodeHash`, and `iterateHashedGraphNodes` accept an optional `nodeVersion`. Every install-side caller (`deps.graph-builder`, `installing.deps-resolver`, `installing.deps-restorer`, `installing.deps-installer/install/link`, `building.during-install`, `building.after-install`) derives the project's pinned runtime via `findRuntimeNodeVersion` once per invocation and forwards it. The legacy `ENGINE_NAME` constant in `@pnpm/constants` is unchanged so external consumers and existing tests keep working.

   Pacquet mirrors this with `find_runtime_node_major` in `install_frozen_lockfile.rs` — it scans the lockfile's `snapshots:` map for a `node@runtime:<version>` entry and uses that major outright, only falling back to the host probe when no pin is present.

3. **Slot bin-shim layout.** Pacquet was emitting `.cmd` / `.ps1` shims on every host platform, even though pnpm only writes them on Windows ([`@zkochan/cmd-shim` `createCmdFile: isWindows`](https://github.com/pnpm/cmd-shim/blob/0d79ca9534/src/index.ts#L32) + `bins/linker`'s [`POWER_SHELL_IS_SUPPORTED = IS_WINDOWS`](https://github.com/pnpm/pnpm/blob/29a42efc3b/bins/linker/src/index.ts#L28) gate). Pacquet also excluded the slot's own package from the slot-local `node_modules/.bin/` based on a stale assumption ("which pnpm doesn't"), but pnpm's [`linkBinsOfDependencies`](https://github.com/pnpm/pnpm/blob/29a42efc3b/building/during-install/src/index.ts#L272-L298) appends `depNode` to the bin-source list unconditionally, so a leaf package like `hello-world-js-bin` writes a self-shim at `<slot>/node_modules/<pkg>/node_modules/.bin/<pkg>`. Both behaviors now match pnpm.

## Test plan

- [x] `cargo nextest run -p pacquet-cli --test pnpm_compatibility` — 5 active tests pass, 1 ignored (see below)
- [x] `cargo nextest run -p pacquet-store-dir -p pacquet-config -p pacquet-cmd-shim -p pacquet-package-manager` — 600+ tests pass after the prefix / bin-shim updates
- [x] `same_global_virtual_store_layout_pure_js` — pacquet & pnpm produce byte-identical `<store>/v11/links/` trees for `@pnpm.e2e/hello-world-js-bin-parent`
- [x] `same_global_virtual_store_layout_diamond` — same for `pkg-with-1-dep` + `parent-of-pkg-with-1-dep`, verifying `calc_dep_graph_hash` memoization parity
- [x] Three new TS unit tests in `engine/runtime/system-node-version/test/` cover the `engineName(version)` override branch and `findRuntimeNodeVersion`'s extraction rule (with and without peer suffix)
- [ ] `same_global_virtual_store_layout_with_approved_postinstall` is currently `#[ignore]`d. It requires pnpm and pacquet to agree on the `<platform>;<arch>;node<major>` triple in the engine-included hash branch. The `pnpm/setup` action on CI installs an `@pnpm/exe` SEA bundle whose embedded Node (node26) differs from the runner's PATH `node` (node24), so the digests don't line up. The pnpm-side fix in this PR resolves `engineName()` via `getSystemNodeVersion()` which prefers the shell `node`, so once a published pnpm version with the fix reaches `pnpm/setup` the test will pass without modification — re-enable it then. The other two GVS parity tests are unaffected since they exercise the engine-agnostic branch.

## Notes

- Two pacquet integration tests in `package-manager/src/install/tests.rs` had hard-coded `<store_dir>/projects/` assertions; updated to `<store_dir>/v11/projects/` to follow the prefix fix.
- The `link_bins_rewrites_when_only_sh_flavor_exists` cmd-shim test is now `#[cfg(windows)]` — the upgrade-recovery scenario it exercises is meaningless on Unix where `.cmd`/`.ps1` are no longer written in the first place.
- Review feedback addressed: (a) test YAML helper now guarantees a trailing newline before appending GVS keys; (b) `findRuntimeNodeVersion` calls in `installing/deps-restorer/` switched from `Object.keys(graph)` (install-dir-keyed in that module) to extracting `depPath` per node, with the computation lifted out of the recursion; (c) `dlx.e2e.ts`'s `jest.unstable_mockModule` against `@pnpm/engine.runtime.system-node-version` now forwards every exported symbol so transitive importers of `engineName` don't break.
- Known caveat: pacquet's non-lockfile install path (`run_with_readdir`) still excludes the slot's own bin via `link_bins_excluding`. That path runs only for the legacy flat layout where GVS parity isn't a constraint, so it's deliberately out of scope here.
- Known caveat tracked in #11690: when a dependency's own manifest declares `engines.runtime`, the resolver desugars it into a regular `dependencies.node: 'runtime:<v>'` entry on that package, so the **deps** portion of the hash captures it on both sides. The **engine** portion is still install-wide rather than per-snapshot, so cached side-effects for dep-pinned runtimes can be reused under the wrong host Node. pnpm has this same gap today; closing it on both sides requires per-snapshot engine resolution and is outside this PR's scope.
2026-05-16 23:58:53 +02:00
btea
b6e2c8c5ac fix(engine.pm.commands): honor minimumReleaseAgeExclude in self-update (#11664)
* fix(engine.pm.commands): honor minimumReleaseAgeExclude in self-update

* refactor(config.version-policy): centralize publishedBy policy derivation

Extract the publishedBy / publishedByExclude derivation duplicated across
selfUpdate, dlx, outdated, and deps-resolver into a new
`getPublishedByPolicy()` helper, and the version-policy error rewrap
into `createPackageVersionPolicyOrThrow()`.

Also adds the global self-update test branch (no wantedPackageManager)
requested in PR review, and harmonizes the dlx/outdated error code
for invalid minimumReleaseAgeExclude patterns with install/self-update.

* style(config.version-policy): rename 'callsite' to 'call site' to satisfy cspell

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-15 08:12:23 +00:00
Zoltan Kochan
8a80235c7b chore(release): 11.1.2 2026-05-14 13:31:53 +02:00
Benjamin Staneck
180aee9ba5 fix: handle lockfile conflicts in optimistic install (#11605)
* fix: handle lockfile conflicts in optimistic install

* test: move sharedWorkspaceLockfile out of WorkspaceState.settings

`sharedWorkspaceLockfile` is not in `WORKSPACE_STATE_SETTING_KEYS`, so
placing it inside `WorkspaceState.settings` broke type checking. Pass
it directly on the `CheckDepsStatusOptions` instead.

* chore: add lockfile/fs project reference to installing/commands tsconfig

`@pnpm/lockfile.fs` was added as a runtime dependency but the tsconfig
project references were not updated. meta-updater enforces this in CI.

* perf: restore optimistic-repeat-install fast-path for conflict-free state

The first iteration of the conflict-detection fix unconditionally read
pnpm-lock.yaml on every install - once in installDeps and again inside
checkDepsStatus - defeating the point of optimisticRepeatInstall, which
was to skip reading the lockfile entirely when nothing changed.

Restore the fast path by:

- Dropping the redundant lockfile read from installDeps. checkDepsStatus
  already returns upToDate: false when the lockfile is conflicted, so
  the pre-check was dead weight.
- Gating the conflict check inside checkDepsStatus on the lockfile's
  mtime: if it hasn't been touched since the last successful install,
  it cannot have grown conflict markers, so the read is skipped.

Conflict markers introduced after a successful install (e.g. via
git pull/merge) still update the lockfile mtime, so the correctness
fix is preserved.

* perf: make lockfile-conflict check synchronous

findConflictedLockfileDir awaits its work serially with no concurrent
operations to interleave, so the async overhead (Promise.all microtasks,
event-loop hops) buys nothing. Convert to a plain for-loop with
fs.statSync and a new wantedLockfileHasMergeConflictsSync export.

Also resolves a test-mock issue: the previous version called safeStat
from deps/status, which is jest-mocked across the test file. The mocked
safeStat returned undefined (or stale stats from earlier tests), causing
the conflict check to silently no-op. Switching to fs.statSync bypasses
the mock and gets the real mtime of the temp lockfile the regression
tests write.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-14 10:03:32 +00:00
Zoltan Kochan
50b33c1e6b fix: address open CodeQL findings (#11609)
Resolves the 15 open alerts on https://github.com/pnpm/pnpm/security/code-scanning by addressing all four categories that CodeQL flagged.

### Prototype-polluting assignment (3 alerts, product code)
- `pkg-manifest/utils/src/convertEnginesRuntimeToDependencies.ts`: the inner write now dispatches over a literal `switch` on `runtimeName`, so the assignment is always keyed by `'node' | 'deno' | 'bun'`.
- `pkg-manifest/utils/src/updateProjectManifestObject.ts`: added an `isProtoPollutionKey` barrier at the top of the loop so `packageSpec.alias` can never reach the dynamic property write with `__proto__` / `constructor` / `prototype`.
- `installing/deps-installer/src/uninstall/removeDeps.ts`: the package list is filtered through `isProtoPollutionKey` once up front, and the dependency record is captured into a local before the loop.

### Polynomial ReDoS (2 alerts)
- `deps/inspection/list/src/renderDependentsTree.ts`: `replace(/\n+$/, '')` swapped for a constant-time `charCodeAt` trim.
- `resolving/npm-resolver/src/fetch.ts`: removed the super-linear-backtracking `semverRegex` and replaced it with an O(n) `stripTrailingSemverSuffix` that splits on the rightmost `@` and `semver.valid`s, with a digit-block fallback so `foo1.0.0`-style names still produce the existing "Did you mean foo?" hint.

### Bad code sanitization (8 alerts, test infrastructure)
- `__utils__/test-ipc-server/src/TestIpcServer.ts`: the `JSON.stringify(...).slice(1, -1)` smell at the source of all 8 test-file alerts is gone. Both `sendLineScript` and `generateSendStdinScript` now build the JS source with plain `JSON.stringify` and delegate shell wrapping to a new `wrapNodeEval` helper that escapes `\\` and `"` for the outer double-quoted shell argument.

### Incomplete sanitization (2 alerts, test file)
- `releasing/commands/test/publish/oidcProvenance.test.ts`: `.replace('/', '%2f')` → `.replaceAll(...)` on both flagged lines.
2026-05-13 00:50:59 +02:00
Zoltan Kochan
9a327522ce chore(release): 11.1.1 2026-05-12 12:56:32 +02:00
Zoltan Kochan
02e9cf5b67 fix(deps.status): skip engine check when scanning workspace projects (#11592)
* fix(deps.status): skip engine check when scanning workspace projects

checkDepsStatus (run by verifyDepsBeforeRun) called findWorkspaceProjects
without a nodeVersion, so the engine check fell back to the system Node from
PATH and emitted spurious "Unsupported engine" warnings before scripts ran.
The actual install still does engine validation; a status check shouldn't.

* test(pnpm): cover engine warning regression for verifyDepsBeforeRun

* docs(changeset): clarify scope of the skipped validation

* test(pnpm): also check stderr for unsupported engine warning
2026-05-12 00:22:57 +02:00
Zoltan Kochan
732312f49e chore(release): 11.1.0 2026-05-11 19:56:10 +02:00
btea
8543b89fb2 feat: view command display published time (#11532)
* **New Features**
  * `pnpm view` now shows publish age (e.g., "published 2 hours ago") and, when available, publisher attribution ("by …"); invalid or future timestamps fall back to "just now".
  * Console styling for package metadata (name/version, license, counts, links, dependencies, maintainers) was improved for readability.

* **Tests**
  * Added tests verifying the published timestamp and publisher attribution appear in `pnpm view` output.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-11 12:28:04 +02:00