Commit Graph

112 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
Maël Nison
0474a9c3b1 feat: add support for package maps (#12430)
Generate a Node.js package map at `node_modules/.package-map.json` on every
isolated or hoisted install, including under the global virtual store, so that
third-party tooling can start experimenting with package maps. The file is
serialized compactly.

Two settings control how the map is consumed:

- `node-experimental-package-map` (default: off): inject
  `--experimental-package-map` into `NODE_OPTIONS` for the Node.js scripts pnpm
  runs — dependency lifecycle scripts, `pnpm exec`, and `pnpm run` (including
  recursive runs).
- `node-package-map-type` (`standard` | `loose`): choose between a strict map
  and one that tolerates hoisting-like access.

Covered by both the pnpm CLI and the pacquet (Rust) implementation.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-18 11:51:50 +02:00
Zoltan Kochan
c112b6106b feat(install): add --dry-run option (npm-style preview) (#12449)
## Description

Adds a `--dry-run` option to `pnpm install` with **npm-style preview semantics**: it runs a full dependency resolution and reports what a real install **would** add/remove/update, but writes **nothing** to disk (no lockfile, no `node_modules`, no `.modules.yaml`, no workspace-state file) and **always exits 0**.

```
$ pnpm install --dry-run
Dry run complete. A real install would make the following changes (nothing was written to disk):

Importers
.
  + is-negative 1.0.0

Packages
+ is-negative@1.0.0
```

When the lockfile is already up to date it prints `Dry run complete. pnpm-lock.yaml is up to date; a real install would make no changes.`

Resolves https://github.com/pnpm/pnpm/issues/7340.

### Why this shape

An earlier attempt (#12270, now closed) implemented `--dry-run` as `--frozen-lockfile --lockfile-only` — i.e. a fail-on-drift *lockfile validator*. That collides with the well-established meaning of `--dry-run` across npm/yarn ("preview, never fail") and duplicated existing behaviour (`pnpm install --frozen-lockfile --lockfile-only` already does that). This PR implements the intuitive preview meaning instead.

### How it works (pnpm)

- Reuses the existing `lockfileCheck` callback (resolve fully, skip the lockfile write, hand back the before/after wanted lockfile) plus `lockfileOnly` (skip `node_modules`, the workspace-state file, and metadata-cache writes).
- The frozen/headless fast path is disabled whenever `lockfileCheck` is set, so a check-only install always resolves and never materialises anything.
- The before/after lockfiles are diffed (reusing the dedupe diff engine, now exported as `calcDedupeCheckIssues`) and rendered into the report.
- `--dry-run` with a configured pnpr server is rejected (that path resolves/links through the server).

### Pacquet

Ported in the second commit — `pacquet install --dry-run` forces the fresh-resolve path, skips every write (a new `dry_run` flag on `InstallWithFreshLockfile` skips the `pnpm-lock.yaml` save), and a new `dry_run` module diffs the existing lockfile against the freshly-resolved one and prints the same report.
2026-06-16 19:12:56 +02:00
C. Spencer Beggs
302a2f7d2c fix(config/reader): don't warn when packageManager and devEngines.packageManager match (#12287)
When `package.json` sets both `packageManager` and `devEngines.packageManager` to the same pnpm version with the same integrity hash pnpm prints a spurious warning on every command. For example, a `package.json` file that looks like:

```json
{
	"packageManager": "pnpm@11.5.1+sha512.93f7b57422ea7068257235b4c16eb60762eb68e1dc23723199cc739043ea9be2c4143274a399d8c6defa2b1176226d9ca1c4b63482d6200c1a8fbaa78c1d1485",
	"devEngines": {
		"packageManager": {
			"name": "pnpm",
			"version": "11.5.1+sha512.93f7b57422ea7068257235b4c16eb60762eb68e1dc23723199cc739043ea9be2c4143274a399d8c6defa2b1176226d9ca1c4b63482d6200c1a8fbaa78c1d1485",
			"onFail": "ignore"
		},
		"runtime": [
			{
				"name": "node",
				"version": "26.3.0",
				"onFail": "ignore"
			}
		]
	}
}
```

Issues a warning on every `pnpm` command:

> Cannot use both "packageManager" and "devEngines.packageManager" in package.json. "packageManager" will be ignored.

## Root cause

`getWantedPackageManager` compares the two fields to decide whether to warn, but the two sides were normalized differently:

  - `parsePackageManager` **strips the integrity hash** from the legacy `packageManager` field → `11.5.1`
  - the `devEngines.packageManager` version was compared **with its hash intact** → `11.5.1+sha512.93f7b57…`

So, `"11.5.1" !== "11.5.1+sha512…"` was always true and the warning fired, even for identical specs. An earlier fix in #11307 only suppressed the warning when *neither* side carried a hash.

## Fix

`parsePackageManager` now also returns the hash (via a shared `splitPackageManagerVersion`), and `getPackageManagerConflictWarning` compares the fields structurally. The warning is suppressed **only when the two specifiers are identical** (name + version + hash, both-absent counts as equal):

| name | version | hash | result |
|------|---------|------|--------|
| same | same | both absent, or both present & equal |  no warning |
| same | same | present on **one side only** | ⚠️ generic "Cannot use both…" |
| same | same | both present & **differ** | ⚠️ "…contradictory integrity hashes" |
| same | **differ** | — | ⚠️ "…different versions of pnpm" |
| **differ** | — | — | ⚠️ "…different package managers" |

A hash on only one side is still a divergence — dropping the ignored `packageManager` field would lose that hash — so it warns with the original generic message. Two contradictory hashes for one version (a likely wrong-hash mistake) get a dedicated message. The generic single message is otherwise replaced by one tailored to each conflict, each ending with `"packageManager" will be ignored`.

Closes #12028.

---------

Signed-off-by: C. Spencer Beggs <spencer@beggs.codes>
Signed-off-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-16 15:49:00 +02:00
Zoltan Kochan
1e82e001cd chore(release): 11.7.0 (#12414) 2026-06-15 08:37:08 +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
Victor Sumner
61810aa684 feat: add --frozen-store for installs against a read-only store (#12190)
## What

Adds an opt-in `frozenStore` / `--frozen-store` setting (default `false`) that lets `pnpm install --offline --frozen-lockfile` run against a package store that lives on a **read-only filesystem** — a Nix store, a read-only bind mount, an OCI layer.

## Why

A normal install fails against such a store **not** because it writes package content, but because it unconditionally:

1. opens the SQLite `index.db` in WAL mode, which needs to create `-shm`/`-wal` sidecars in the store directory; and
2. writes a project-registry entry under the store.

Both fail with `attempt to write a readonly database` / `EROFS` on a read-only store directory, even when the store is complete and the lockfile is frozen. This blocks any deployment that wants an immutable, content-addressed store.

## How

When `frozenStore` is enabled, pnpm opens `index.db` through the SQLite **`immutable=1`** URI — which tells SQLite the file cannot change underneath it, so it bypasses the WAL/`-shm` sidecar machinery entirely and reads the raw file with zero sidecar creation — and suppresses every store-write path. Pair it with `--offline --frozen-lockfile` against a fully-populated store. It is incompatible with two settings that would write into the store, and each throws a clear config-conflict error before any network or store access: **`--force`** (which bypasses the no-write-on-hit skip → `ERR_PNPM_CONFIG_CONFLICT_FROZEN_STORE_WITH_FORCE`) and a configured **pnpr server** (which would fetch and write packages into the store → `ERR_PNPM_FROZEN_STORE_INCOMPATIBLE_WITH_PNPR`).

> Plain `SQLITE_OPEN_READ_ONLY` is **not** sufficient: opening a WAL-mode db read-only still tries to create the `-shm` sidecar, which fails on a read-only *directory*. `immutable=1` is the load-bearing piece.

> **Node.js requirement:** `node:sqlite` only passes `SQLITE_OPEN_URI` to SQLite (so the `immutable=1` query is honored rather than treated as part of a literal filename) starting in **v22.15.0** (22.x line), **v23.11.0**, and every **v24+**. pnpm's `engines` floor is `>=22.13`, so on a runtime older than that the frozen open is detected up front and fails with a clear `ERR_PNPM_FROZEN_STORE_UNSUPPORTED_NODE` instead of SQLite's cryptic "unable to open database file". (pacquet uses rusqlite with an explicit `SQLITE_OPEN_URI` flag, so it has no such floor.)

> The `immutable=1` URI path is also percent-encoded (`%`→`%25`, `?`→`%3f`, `#`→`%23`, in that order, leaving `/` literal) so a store path containing those characters doesn't truncate the path or inject a spurious query parameter — applied identically in both stacks.

### Build backstop under the global virtual store

Under the global virtual store (default), a package's directory lives **inside** the store (`{storeDir}/links/...`). Applying a patch or running an allowlisted lifecycle script writes into that directory — so on a frozen store it would crash mid-build with a raw `EROFS`. A fully-seeded store never reaches the build step (patched/built packages are imported from the side-effects cache and filtered out by the `isBuilt` gate), so any residual build candidate means the seed is **missing that package's build output**.

`buildModules` now refuses up front with `ERR_PNPM_FROZEN_STORE_NEEDS_BUILD` and actionable guidance ("rebuild the seed with their scripts enabled, or remove them from `onlyBuiltDependencies`") instead of failing cryptically once a script starts. The check is gated on the global virtual store — under the isolated linker, slot directories live in the writable project-local store, so builds there are fine. Non-allowlisted scripts never run, so they are not treated as a blocking write.

Bin-linking has its own read-only-store edge under the global virtual store. On a **warm** checkout (the project's `.bin/<name>` already points at the seed target) `linkBin` returns before touching the store, so it is write-free. But on a **fresh** checkout it (re)creates the bin and calls `fixBin`, whose `chmod` targets the bin's **source file inside the store** (`{storeDir}/links/...`) — which is refused with `EPERM`/`EACCES` on a read-only store, even though a complete seed already ships that bin executable (so the `chmod` is redundant). `@pnpm/bins.linker` now wraps that call in `ensureExecutable`: it swallows the refusal when the target is already executable and rethrows otherwise, so bin-linking is write-free against a frozen store on a cold checkout too, while a genuinely non-executable bin (a broken seed) still surfaces as an error.

The blocking predicate distinguishes the two write kinds: a **patch** is applied regardless of `ignoreScripts`, so a patched package is always blocked; a **lifecycle script** is suppressed under `ignoreScripts`, so an allowlisted build-requiring package is *not* blocked when scripts are off (it would write nothing). This avoids falsely rejecting a valid `--ignore-scripts` frozen install. **Optional dependencies are exempt**: a build or patch failure on an optional dependency is non-fatal at runtime, so a seed missing an optional package's build output skips that build (emitting the `skipped-optional-dependency` log) instead of blocking the install — in both stacks.

### Both stacks (parity rule)

**TypeScript pnpm CLI**
- Config plumbing (`@pnpm/config.reader`): `frozen-store` type, config-file key, default, `Config.frozenStore`.
- The read-only open branch (`@pnpm/store.index`): `immutable=1`, read-only statements, throwing mutators.
- Wiring through `@pnpm/store.controller` and `@pnpm/store.connection-manager` to the sole `StoreIndex` construction site.
- Gating the project-registry write (`@pnpm/installing.context`).
- The `--force` / pnpr-server conflict guards (`@pnpm/installing.deps-installer`) and CLI surface (`@pnpm/installing.commands`).
- **After-install rebuild** (`@pnpm/building.after-install`): the post-install rebuild opens its `StoreIndex` immutably under the flag, so re-reading the store for a rebuild never attempts a writable open against the frozen store.
- **Worker fix:** `@pnpm/worker` opens its *own* writable `StoreIndex` on every `readPkgFromCafs` cache hit, so a pure read crashed on a frozen store. `frozenStore` is threaded through to `getStoreIndex` and keyed into its connection cache.
- **Build backstop** (`@pnpm/building.during-install`): `buildModules` throws `ERR_PNPM_FROZEN_STORE_NEEDS_BUILD` for a GVS slot that would build/patch on a frozen store, honoring `ignoreScripts`; threaded from `@pnpm/installing.deps-installer`.
- **Bin-linking on a read-only store** (`@pnpm/bins.linker`): `linkBin` wraps the `fixBin` chmod in `ensureExecutable`, which tolerates `EPERM`/`EACCES` when the bin's store-resident source is already executable (a complete seed) and rethrows otherwise — so a fresh checkout against a frozen store links bins without crashing on the redundant chmod. Catch-on-failure keeps the writable hot path at zero added syscalls.

**Rust pacquet**
- A dedicated `open_immutable` / `shared_immutable_in` opens via `immutable=1`, selected only under the flag. Plain `open_readonly` keeps the ordinary `SQLITE_OPEN_READ_ONLY` open (WAL locking intact) because normal installs read the index while the same process's `StoreIndexWriter` writes it concurrently — an immutable connection skips all locking and change detection, so a concurrent writer would make those reads undefined.
- `--frozen-store` CLI flag + `frozenStore` workspace-yaml setting.
- The store-index writer is replaced with a drain-and-drop stub (`spawn_disabled`) and `init_store_dir_best_effort` is skipped under the flag.
- **Build backstop:** `build_modules` returns `BuildModulesError::FrozenStoreNeedsBuild` (`ERR_PNPM_FROZEN_STORE_NEEDS_BUILD`) under the same GVS + frozen-store condition, threaded from `config.frozen_store`. The gate keys off `should_run_scripts` (which already folds the allow-build policy), so it is correct without an explicit ignore-scripts branch — pacquet has no configurable ignore-scripts mode yet.

pacquet already separated read-only index access (`shared_readonly_in`, or `shared_immutable_in` under the flag) from writes, so it never had the worker-conflation bug; the flag makes the "no writes attempted" contract explicit and gates the remaining best-effort write attempts.

## Testing

- **TS:** `store.index` frozen-mode-on-`0555`-directory test (reads work, writes throw `ERR_PNPM_FROZEN_STORE_WRITE`) plus a path-with-`?` open test — both gated on the runtime's immutable-URI support, with a complementary test asserting `ERR_PNPM_FROZEN_STORE_UNSUPPORTED_NODE` fires where that support is absent (the CI Node 22.13.0 path); `config.reader` round-trip; `deps-installer` `--force` and pnpr-server conflict guards; `worker`/`package-requester` unit tests. End-to-end on a `chmod -R 0555` store: install succeeds, `node_modules` materializes, no `-shm`/`-wal`/`-journal` sidecars; negative control without the flag fails as expected; incomplete store → clean offline error.
- **pacquet:** `open_immutable_reads_wal_db_on_readonly_directory` unit test plus the `immutable_sqlite_uri` encoding test and a path-with-`?` open test; yaml + CLI fold tests; **integration test** `frozen_store_installs_against_a_read_only_store` — primes a store, `chmod 0555` the tree, runs `install --frozen-lockfile --frozen-store --offline`, asserts success + materialized `node_modules` + zero sidecars. Confirmed load-bearing by reverting the `immutable=1` fix (test then fails).
- **Build backstop (both stacks):** `building/during-install` unit tests — approved-build-not-cached and patched-not-cached refuse; cached, non-allowlisted, and non-GVS cases pass through; **approved-build-under-`ignoreScripts` passes through while patched-under-`ignoreScripts` still refuses** — and the matching pacquet `build_modules` tests (`frozen_store_gvs_patch_not_seeded_refuses` + GVS-off / frozen-off controls). Each confirmed load-bearing by disabling the relevant guard and watching the corresponding test fail.
- **Bin-linking (`bins/linker`):** `ensureExecutable` tests with `fixBin` mocked to reject with `EPERM` — an already-executable bin source resolves (and `fixBin` is asserted called, so it isn't the warm skip-guard passing), a non-executable one rethrows `EPERM`. Confirmed end-to-end by running the built `linkBins` against a real `chflags uchg`-immutable store: the executable-seed case resolves and links the bin, the non-executable-seed control throws `EPERM`.

A changeset is included with `"pnpm": minor` and `"@pnpm/bins.linker": patch` (the read-only-store bin-linking fix).
2026-06-12 08:36:08 +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
615c6694e1 feat: support URL-scoped registry auth via npm_config_// and pnpm_config_// env vars (#12338)
* feat: support URL-scoped registry auth via npm_config_// and pnpm_config_// env vars

Adds a file-free way to configure registry authentication, e.g.
  npm_config_//registry.npmjs.org/:_authToken=<token>
  pnpm_config_//registry.npmjs.org/:_authToken=<token>

These are host-scoped by construction — the registry the value applies to is
encoded in the (trusted) variable name — so they cannot be redirected to
another host by repository-controlled config. The env value is trusted: it
overrides a project/workspace .npmrc but is still overridden by CLI options.
pnpm_config_ wins over npm_config_ for the same key.

* feat(pacquet): support URL-scoped registry auth via npm_config_// and pnpm_config_// env vars

Pacquet parity for the same feature on the JS side: read URL-scoped registry
credentials from npm_config_//… and pnpm_config_//… environment variables
(e.g. npm_config_//registry.npmjs.org/:_authToken=<token>).

These are trusted (sourced from the environment, not the repository) and
host-scoped by construction, so they sit at the top of the .npmrc precedence
chain — above the project .npmrc. pnpm_config_ wins over npm_config_ for the
same key. Adds an EnvVar::vars() enumeration capability (default empty, so
existing fakes keep compiling; production providers override it).

* fix(pacquet): avoid Unicode ellipsis in a line comment (dylint)

* fix: exclude tokenHelper from URL-scoped env auth; add case-insensitive tests

Address review feedback on pnpm/pnpm#12338:
- A `//host/:tokenHelper` env var would land in authConfig but trip the
  TOKEN_HELPER_IN_PROJECT_CONFIG guard (which only trusts the user .npmrc),
  incorrectly failing. tokenHelper names an executable, so it is now excluded
  from the env-scoped layer entirely.
- Add tests for case-insensitive prefix matching and the tokenHelper exclusion.
- Add a 'text' language hint to the changeset's fenced block (MD040).

* fix(pacquet): avoid panics on non-UTF-8 / non-ASCII env var names

Address CodeRabbit review on the pacquet env-auth code:
- EnvVar::vars() used std::env::vars(), which panics if any env var name or
  value is not valid UTF-8. Iterate vars_os() and skip non-UTF-8 entries,
  matching var()'s .ok() behavior. (SystemEnv and Host.)
- parse_url_scoped_env_name sliced with name[..prefix.len()], which panics
  when the byte index lands inside a multi-byte char. Use boundary-checked
  name.get(..) instead.
- Add a regression test with non-ASCII env var names.

* test: cover env-auth precedence and pacquet end-to-end wiring

Fill the coverage gaps in the URL-scoped env-auth feature:
- JS: assert a CLI-provided //host/:_authToken still beats the same env var
  (workspace < env < CLI), and that non-token cred fields work while a
  non-URL-scoped env key is ignored.
- pacquet: add end-to-end tests through the full config load — that a
  npm_config_//… var is honored and outranks a project .npmrc token for the
  same host, and that the prefix is matched case-insensitively. FakeEnv now
  enumerates via vars() so the env-scoped reader sees the fixture.
2026-06-12 00:41:09 +02:00
Zoltan Kochan
bc9ed78f48 fix: clearer warning when a project .npmrc uses env variables in registry/auth settings (#12333)
* fix: clearer warning when a project .npmrc uses env variables in registry/auth settings

The previous warning only said the setting was ignored. It now explains why
(the project .npmrc is committed to the repository and must not expand secrets
into request destinations or credentials) and how to fix it: move the value to a
trusted source such as the user-level ~/.npmrc or via pnpm config set, with a
link to the docs.

The suggested 'pnpm config set' example is only shown when the key has no
${...} placeholder, so the snippet is always safe to copy-paste (a shell would
otherwise expand a placeholder embedded in the key). The wording does not claim
a specific destination file.

* fix: only suggest a pnpm config set command for shell-safe keys

The key embedded in the warning's suggested 'pnpm config set' command comes
from a repository-controlled .npmrc. The previous guard only suppressed the
example for keys containing a ${...} placeholder, but a shell also expands
$(...), backticks and $VAR inside double quotes — so a crafted key could turn
the suggested copy-paste command into command execution. The example is now
emitted only for keys made up entirely of shell-inert characters.
2026-06-11 19:39:15 +02:00
Zoltan Kochan
b7195db5c8 chore(release): 11.5.3 (#12305) 2026-06-10 12:40:29 +02:00
Zoltan Kochan
822beb5fa0 fix: harden package-manager bootstrap metadata (#12296)
- Resolve package-manager bootstrap metadata through trusted user/CLI registries and trusted network config, defaulting to the public npm registry instead of project/workspace registry settings.
- Apply that bootstrap config in `switchCliVersion()` and `syncEnvLockfile()` so repository `.npmrc` proxy/TLS/configByUri values cannot steer package-manager bootstrap traffic.
- Validate repository-provided package-manager env-lockfile entries before auto-switch install/execution: dependency paths must be registry package paths and package records must use integrity-only resolutions.
- Preserve the fast path for fully resolved, valid package-manager metadata; incomplete metadata is still resolved through trusted bootstrap registries.
- Handle peer-suffixed package-manager snapshots by looking up `packages` entries with `removeSuffix(depPath)` while keeping `snapshots` keyed by the full dep path.
2026-06-10 00:30:31 +02:00
Zoltan Kochan
1017c36776 fix: block untrusted request destination env expansion (#12291)
Fixes CAND-PNPM-122 / GHSA-3qhv-2rgh-x77r by making environment expansion trust-aware for registry/auth config and request destinations.

- Stops project `.npmrc` from expanding `${...}` placeholders in registry/proxy request destinations, URL-scoped keys, and registry credential values.
- Stops repository-controlled `pnpm-workspace.yaml` from expanding `${...}` placeholders in request destinations: `registry`, `registries`, `namedRegistries`, and `pnprServer`.
- Preserves env expansion for trusted user/global/auth.ini/CLI/global config/env config so existing token, registry, and pnpr server setup flows continue to work.
- Ports the same trust boundary to pacquet for dependency-management commands.
2026-06-09 22:29:15 +02:00
minbang
027196babe fix: parse bare color as boolean flag (#12162)
* fix: parse bare color as boolean flag

Bare --color was treated as an enum-only option, so nopt could
consume the following flag as its value. This prevented command-specific
shorthands such as run --parallel from expanding when --color preceded
them.

Accept boolean values for the color option, and add parser/config
regression coverage for the shorthand case.

Co-authored-by: OpenAI Codex <codex@openai.com>

* test: remove tautological color type metadata test

Per review feedback, the assertion only restated the type metadata; the
behavior is already covered by the parser regression test and the
color-normalization test.

* fix: keep with current dispatch working after a boolean global flag

Bare `--color` now parses as a boolean flag, which enables forms like
`pnpm --color with current <cmd>`. The `with current` rewrite skipped any
occurrence whose preceding token was a long flag without `=`, assuming it
consumed `with` as its value. For boolean flags that is wrong, so the
lookup returned -1 and pnpm threw MISSING_WITH_CURRENT_CMD.

Skip only when the preceding long option actually takes a value, based on
the option type metadata; boolean flags and `--no-` negations no longer
hide the command.

---------

Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-08 15:04:19 +02:00
Zoltan Kochan
894ea6af2c feat: deprecate $ version references in overrides (#12262)
Using the "$" syntax in overrides (e.g. "react": "$react") now emits a
deprecation warning. The syntax still resolves as before. Catalogs are the
recommended replacement: reference a catalog entry with the "catalog:"
protocol.

Refs pnpm/pnpm#12160
2026-06-08 14:00:05 +02:00
marko1olo
3537020817 fix: respect pmOnFail ignore in self-update (#12231)
* fix: respect pmOnFail ignore in self-update

* fix: preserve devEngines lockfile writes

* fix: restore unrelated whitespace hunks

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-08 00:15:38 +02:00
marko1olo
6b5d91a4cc fix(config): restore globalconfig lookup (#12252)
* fix(config): restore globalconfig lookup

* refactor(config): derive globalconfig path from a single shared helper

Extract getGlobalConfigPath into @pnpm/config.reader and use it for both
the reader's warning message and 'config get globalconfig' so the reported
path cannot drift from the file pnpm actually reads. Add an e2e test that
writes a setting to the reported path and reads it back.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-07 13:25:52 +02:00
Zoltan Kochan
4b4d38361c chore(release): 11.5.2 (#12207) 2026-06-05 08:27:41 +02:00
Zoltan Kochan
a017bf3394 refactor: rename the agent client and agent setting to pnpr (#12155)
* refactor: rename the agent client + setting to pnpr

The pnpm-side client and its config setting still carried the old
"agent" name after the server moved to pnpr. Align both with pnpr (and
with pacquet, which already uses `pnprServer`):

- Move `agent/client` → `pnpr/client` and rename the package
  `@pnpm/agent.client` → `@pnpm/pnpr.client` (exported `AgentProject`
  type → `PnprProject`).
- Rename the config setting `agent` → `pnprServer` (`--pnpr-server`
  CLI flag), matching pacquet's setting name.
- Rename the internal install-path symbols and the user-facing log /
  error strings that mentioned "pnpm agent" to "pnpr".

No behavioral change — only names. The e2e suite now drives
`--config.pnprServer`.

* fix: forward optionalDependencies to the pnpr server

`PnprProject` and the install-request body only carried `dependencies`
and `devDependencies`, so a project's `optionalDependencies` were
dropped on the way to the pnpr server — it resolved as if they didn't
exist, producing a different lockfile than the local resolver.

Thread `optionalDependencies` through the client request shape, the
deps-installer single-project and workspace request builders, and the
pnpr server (`InstallRequestProject` / `InstallRequest` + the throwaway
manifest it writes for resolution). Adds an e2e case asserting an
optional dependency is resolved through `pnprServer`.
2026-06-03 12:01:48 +02:00
Zoltan Kochan
b741d91e67 chore(release): 11.5.0 (#12068) 2026-05-29 17:26:13 +02:00
Zoltan Kochan
a39a83d19e feat: support nodeLinker: hoisted on fresh installs + add hoistingLimits setting (#12041)
## 1. Support `nodeLinker: hoisted` on the fresh-lockfile install path (pacquet)

Closes #11871. Until now pacquet's `Install::run` hard-refused `nodeLinker: hoisted` without a checked-in lockfile (`ERR_PNPM_…UNSUPPORTED_FRESH_INSTALL_NODE_LINKER`).

- Extracted a shared `run_hoisted_linker` helper from the frozen path's hoisted branch (walker → `link_hoisted_modules` → `SymlinkDirectDependencies { link_only: true }` → `pkg_root_by_key` → walker-skip folding), so both install paths run identical logic.
- Fresh path now threads `node_linker` + `supported_architectures`, hands `CreateVirtualStore` the real linker (populating `cas_paths_by_pkg_id`), branches on `is_hoisted`, and returns `hoisted_locations` so `.modules.yaml` round-trips.
- Removed the guard and the dead `UnsupportedFreshInstallNodeLinker` error variant.

Ported upstream's `hoistedNodeLinker/install.ts` into `crates/cli/tests/hoisted_node_linker.rs` (real tests for the core layout, no-lockfile, `externalDependencies`, `autoInstallPeers`, and `hoistingLimits`; the rest stubbed as `known_failures` against `pnpm add`/update (#433) and build-phase (#11870) gaps), and ticked the boxes in `plans/TEST_PORTING.md`.

## 2. Add the `hoistingLimits` setting (pnpm CLI **and** pacquet)

Revives the stale #6468 (closes #6457) and brings both stacks to parity. `hoistingLimits` mirrors yarn's `nmHoistingLimits`: `none` (default — hoist as far as possible), `workspaces` (hoist only as far as each workspace package), `dependencies` (hoist only up to each workspace package's direct deps). It was previously a programmatic-only option in pnpm (no config surface) and a pacquet-only raw-map yaml field.

**pnpm CLI:** `config/reader` (`types.ts` enum + `Config.ts` + `configFileKey.ts`), `installing/linking/real-hoist`'s new `getHoistingLimits` (mode → the `@yarnpkg/nm` hoister's per-locator border map), and the install/add/recursive command option lists. Tests: `hoistedNodeLinker/install.ts` (`dependencies` mode) + `real-hoist` `getHoistingLimits` unit tests. Changeset included (minor).

**pacquet:** replaced the raw-map config with the same enum; added `get_hoisting_limits` (port of `getHoistingLimits`); and **fixed `real-hoist`'s border semantics** — a name in the limits marks a *border* whose descendants stay nested beneath it, not a leaf to block. (The earlier leaf-blocking behavior was the divergence flagged while porting; its unit tests were rewritten to the corrected semantics.)
2026-05-29 01:46:25 +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
f2a4d2caef chore(release): 11.3.0 (#11894) 2026-05-24 02:23:07 +02:00
modten
3b62f9da31 feat(publish): add --skip-manifest-obfuscation flag for pack/publish (#11393)
* feat(publish): add preserve-manifest-fields option

* fix(publish): omit pnpm field when preserveManifestFields is enabled

The preserve-manifest-fields option was deep-cloning the entire manifest,
which leaked the pnpm-specific `pnpm` field into packed/published manifests.
The PR description explicitly calls for this field to remain stripped;
align the implementation, tests, help text, and changeset accordingly.

* refactor(publish): rename preserve-manifest-fields to skip-manifest-obfuscation

The original name implied the flag preserves *all* manifest fields, which
isn't true — the pnpm-specific `pnpm` field is still stripped, and
`publishConfig` / workspace-protocol / catalog rewriting still happen. The
flag is really an escape hatch from pnpm's manifest mangling, so name it
that way. Help text and changeset updated to match.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-24 02:15:18 +02:00
Zoltan Kochan
212315de16 fix: cap lockfile verification memory and add trustLockfile opt-out (#11878)
* fix: cap lockfile verification memory and add trustLockfile opt-out

Verifying a multi-thousand-entry lockfile against `minimumReleaseAge`
or `trustPolicy: no-downgrade` retained every fetched packument in a
per-install cache for the entire install. On large workspaces this
OOM'd CI runners with a 2GB heap cap. Project both caches down to just
the fields each check reads (per-version trust evidence + the `time`
map for trust; package-level `modified` + version-name set for the
abbreviated shortcut) so the bulk packument is GC'd as soon as the
fetch returns.

Also adds a `trustLockfile` setting (default `false`) that skips the
verification pass entirely for environments where the lockfile is
already part of the trusted base. Mirrored in pacquet. Closes #11860.

* perf: share resolver packument cache with the lockfile verifier

The verifier kept its own per-install dedup Maps and re-fetched every
packument the resolver had already pulled during the same install.
Plumb the resolver's per-install `PackageMetaCache` through to the
verifier (via `createNpmResolutionVerifier` / `build_resolution_verifiers`)
so a name already in the resolver's LRU short-circuits the verifier's
disk/network round-trip — fast path only, the cached document is
projected for the trust check so the verifier's memory footprint stays
bounded.

In pnpm, `installing/client` now constructs one LRU and hands it to
both `createResolver` and `createResolutionVerifiers`. In pacquet, the
`InMemoryPackageMetaCache` is lifted to `Install::dispatch` and passed
to both `build_resolution_verifiers` and `InstallWithFreshLockfile`.
2026-05-23 20:33:03 +02:00
Zoltan Kochan
0fb723323f chore(release): 11.2.0 (#11764) 2026-05-20 12:41:09 +02:00
Zoltan Kochan
ced20cbe71 fix(config/reader): only sync registries.default to registry when workspace contributes it (#11754)
The sync introduced in #11744 unconditionally overwrote the unnormalized
registry value parsed from .npmrc with the normalized registries.default,
causing a trailing slash to appear in config.registry when the user only
configured a registry in .npmrc.

Restrict the sync to cases where pnpm-workspace.yaml actually contributes
a default registry different from what .npmrc provided.
2026-05-20 01:06:52 +02:00
shiminshen
3687b0e180 fix(config/reader): resolve relative cafile path against the .npmrc directory (#11726)
* fix(config/reader): resolve relative cafile path against the .npmrc directory

`cafile=<relative-path>` in `.npmrc` was being read via `fs.readFileSync`,
which resolves relative paths against `process.cwd()`. When pnpm is invoked
from a different cwd than the project (e.g. `pnpm --dir <project> install`
in CI wrappers and monorepo scripts), the CA file silently failed to load:
the `try/catch` in the loader dropped the CA list, the install proceeded
without the configured CA, and the user only saw TLS errors against a
private registry — with no log line tying back to the wrong path.

Resolve relative `cafile` values in `readAndFilterNpmrc` against
`path.dirname(filePath)` of the .npmrc that declared the key, before
`loadCAFile` reads the file. Absolute paths (the dominant CI shape) and
CLI `--cafile` are unchanged.

Ref: #11624

* refactor(config/reader): tighten cafile-fix comments

The test name and the linked issue already describe the failure mode,
so the 4-line preamble on the test and the 5-line in-line comment on
the implementation were re-narrating what the tests document.

---
Written by an agent (Claude Code, claude-opus-4-7).

* fix(pacquet/config): resolve relative cafile path against the .npmrc directory

Pacquet's `NpmrcAuth::from_ini` used to store the `cafile=` value
verbatim and pass it to `std::fs::read_to_string` at apply time. A
relative path therefore resolved against the process cwd, so a
project `.npmrc` containing `cafile=certs/ca.pem` reached via
`pacquet --dir <proj>` from a different cwd silently failed to load
the CA — same failure mode as pnpm/pnpm#11624 on the TypeScript side,
which the parent commit fixed by resolving against `path.dirname` of
the `.npmrc`.

Mirrors the parent commit on the pacquet side:

- `NpmrcAuth::from_ini` now takes the directory the `.npmrc` was
  loaded from. A relative non-empty `cafile=` value is resolved
  against that directory via `npmrc_dir.join(...)`; empty and
  absolute values pass through unchanged.

- `Config::current` tracks which of `start_dir` / home dir actually
  provided the `.npmrc` text and passes that path through.

- The `load_cafile` doc comment that documented "matches pnpm's
  surprising cwd-resolution behavior" is gone; that caveat was
  current only as long as pnpm itself had the bug.

- Existing tests updated mechanically to pass `Path::new("")` for
  the new parameter; four new tests cover the resolution branches
  (relative resolves, absolute passes through, empty passes through,
  end-to-end load via `apply_to` with a real tempdir-based fixture).

---
Written by an agent (Claude Code, claude-opus-4-7).

* fix(pacquet/config): add trailing commas inside multi-line assert_eq! macros

`perfectionist::macro-trailing-comma` is enforced via dylint at CI and
ran clean before the cafile port. Rustfmt reflowed two `assert_eq!`
calls in `parses_strict_ssl_true_and_false` onto multiple lines when
the `Path::new("")` argument made the line too long, but did not add
the trailing comma the dylint rule wants on the last macro argument.

---
Written by an agent (Claude Code, claude-opus-4-7).

---------

Co-authored-by: shiminshen <16914659+shiminshen@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-20 00:32:18 +02:00
Minijus L
d1b340f3fe fix: synchronize default registry from pnpm-workspace.yaml for login/logout commands (#11744)
Closes #10099
2026-05-20 00:25:54 +02:00
Zoltan Kochan
cd80b2c8ae chore(release): 11.1.3 (#11717) 2026-05-18 15:42:32 +02:00
shiminshen
8df408c901 fix(config): warn when package.json has a legacy "pnpm" field with migrated settings (#11680)
* fix(config): warn when package.json has a legacy "pnpm" field

In v11, pnpm stopped reading settings from the `pnpm` field of package.json
(#10086). Most former pnpm-field settings now live in `pnpm-workspace.yaml`;
a few (e.g. `onlyBuiltDependencies`, `executionEnv`) were removed entirely.
Until now the old field was silently ignored, so users upgrading from v10
had no signal that their overrides or patched dependencies had stopped
taking effect.

Emit a warning whenever the `pnpm` field contains any key that pnpm no
longer reads from package.json. The check is an allowlist (only `pnpm.app`,
consumed by `pnpm pack-app`, is still active), so the warning won't go
stale as new settings are added or removed in future versions. The message
points users at https://pnpm.io/settings rather than prescribing a single
fix, since the new home depends on the key.

Closes #11677.

* fix(config): only warn for migrated pnpm-field keys, not unrelated ones

Previously the warning fired for every key under `pnpm` except `app`,
which would surface false positives for third-party tooling that
piggybacks on the `pnpm` namespace. Switch to an explicit denylist of
the v10 settings that moved to pnpm-workspace.yaml, matching the PR's
stated contract.

---------

Co-authored-by: Damon <damon@deeplearning.ai>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-18 00:13:49 +02:00
shiminshen
ba2c8844c9 fix(config): apply pmOnFail default to devEngines.packageManager (singular) (#11682)
* fix(config): apply pmOnFail default to devEngines.packageManager (singular)

The pnpm v11 release notes document the `pmOnFail` default as `download`
(via the migration table that maps `managePackageManagerVersions: true` →
`pmOnFail: download (default)`). The legacy `packageManager` field already
gets that default applied at the central onFail-resolution site, but the
singular form of `devEngines.packageManager` short-circuited it by setting
`onFail = 'error'` inside `parseDevEnginesPackageManager`, so projects that
pinned a different pnpm via `devEngines.packageManager` saw a hard version
mismatch instead of an auto-download.

Drop that local `?? 'error'` and let the central default apply. The array
form of `devEngines.packageManager` keeps its own per-element defaults
('error' for the last entry, 'ignore' for the rest) — those reflect
explicit prioritisation by the user, not a system-wide fallback. Explicit
`onFail` values are still honored everywhere.

Closes #11676.

* chore: fix spelling (prioritisation → prioritization)

cspell flagged the British spelling at pre-push.

---------

Co-authored-by: Damon <damon@deeplearning.ai>
2026-05-17 14:47:59 +02:00
Sean Kenneth Doherty
020ac45d3d fix: tolerate padded auth base64 (#11694)
* fix: tolerate padded auth base64

* fix: avoid regex in auth padding normalization
2026-05-17 13:24:24 +02:00
Tom Hale
d3f8408def fix: global installs respect build policy from global config.yaml when GVS is enabled (#11363)
* fix(config.reader): move GVS allowBuilds default after globalDepsBuildConfig re-application

The GVS default allowBuilds = {} was applied too early — before
workspace manifest settings were read and before .npmrc values
(dangerouslyAllowAllBuilds) were re-applied via globalDepsBuildConfig.

This caused hasDependencyBuildOptions() to return true (because {}} is
not null), blocking restoration of .npmrc values. Global installs
with GVS enabled would silently skip all build scripts even when
the config explicitly allowed them.

This fix moves the GVS default to after both workspace manifest
reading and globalDepsBuildConfig re-application, so that:
1. Workspace manifest allowBuilds takes precedence (if present)
2. .npmrc dangerously-allow-all-builds is properly restored
3. Empty {}} is only applied as a last resort

Closes #9249

* fix(config.reader): apply Copilot suggestion for GVS allowBuilds guard

From PR review discussion_r3141002317:

- Replace hasDependencyBuildOptions() == null with hasDependencyBuildOptions()
  so the GVS default only applies when no build policy at all is
  configured (not even dangerouslyAllowAllBuilds). This is cleaner because
  the condition now matches the re-application guard on the line
  immediately before it.

- Add regression test verifying that dangerouslyAllowAllBuilds with GVS
  preserves allowBuilds when no global workspace manifest exists.

* docs: update changeset

* fix(config.reader): address PR review feedback

- Fix unreachable GVS allowBuilds default: hasDependencyBuildOptions()
  always returns true after globalDepsBuildConfig re-applies defaults
  (dangerouslyAllowAllBuilds: false is != null). Replace with explicit
  allowBuilds == null && dangerouslyAllowAllBuilds !== true check.
- Rename .npmrc references to global config.yaml in changeset, comments,
  and test names (zkochan: v11 reads from global rc file, not .npmrc).
- Add try/finally env cleanup for XDG_CONFIG_HOME and PNPM_HOME in tests.
- Add test for workspace manifest allowBuilds precedence over config.yaml.

* fix(config.reader): fix GVS workspace manifest test

- Use import.meta.dirname/global/v11 for globalPkgDir (matches env.PNPM_HOME)
- Fix assertion: dangerouslyAllowAllBuilds coexists with allowBuilds
- Clean up global/v11 directory in finally block to prevent test leakage

* fix(config.reader): use object form for workspace manifest allowBuilds; clean up parent global/ dir

Fixes two PR #11363 review threads:
1. allowBuilds in workspace manifest must be Record<string, boolean>,
   not array — createAllowBuildFunction uses Object.entries()
2. Remove empty config/reader/test/global/ directory after test

* fix(config.reader): address production review nits

- Update changeset: use camelCase dangerouslyAllowAllBuilds (YAML key, not .npmrc)
- Add enableGlobalVirtualStore assertion to first GVS test
- Add comment explaining dangerouslyAllowAllBuilds coexistence on config object

* fix(config.reader): address Copilot review — env safety, GLOBAL_LAYOUT_VERSION, try/finally

- Move XDG_CONFIG_HOME mutation and file setup inside try blocks
  so env is always restored even if setup throws
- Replace hard-coded v11 with GLOBAL_LAYOUT_VERSION import
- Fix corrupted try/finally in workspace manifest precedence test
  (missing finally block and mangled expect line from prior bad edit)
- Reword comment: enableGlobalVirtualStore defaults to true for
  global installs, not \"when not in CI\"

* fix(config.reader): address last 3 Copilot review threads — comment wording, cleanup placement, test rename

* fix(config.reader): fix block-scoped globalDir leak in GVS test

* fix: address Copilot review #4194783789 — restore auth test, fix naming, remove artifacts

* Remove local dev tooling — not part of this PR

* Remove PR.md — issue context is in the PR description

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Tom Hale <tom@hale.net>
2026-05-16 02:13:42 +02:00
Zoltan Kochan
a62f959242 fix(config): drop unresolved ${VAR} placeholders from .npmrc auth values (#11526)
Closes #11513.

`actions/setup-node` writes `_authToken=${NODE_AUTH_TOKEN}` to `.npmrc`. When the user relies on OIDC trusted publishing without setting `NODE_AUTH_TOKEN`, pnpm previously passed the literal placeholder through verbatim — so any time OIDC fallback failed, pnpm sent `Authorization: Bearer ${NODE_AUTH_TOKEN}` to the registry and the publish came back as a 404.

This worked in v10 because `pnpm publish` shelled out to `npm publish`, whose own OIDC flow handled the case.

The fix lives in `@pnpm/config.env-replace@4.1.0`, which adds an `envReplaceLossy` variant that returns `{ value, unresolved }` instead of throwing. Unresolved `${VAR}` placeholders become `''` and are reported back as a list — leaving OIDC trusted publishing as the sole auth source. Resolvable placeholders and `${VAR-default}` / `${VAR:-default}` fallbacks elsewhere in the same string still expand normally, so a value like `pre-${SET}-mid-${UNSET}-${OTHER-default}-post` now produces `pre-AAA-mid--default-post` rather than dropping every placeholder.

Also treats `{ KEY: undefined }` in the env object the same as a missing key (the `Record<string, string | undefined>` contract), so a `${KEY-default}` reaches the fallback in that case.

### Changes

- `@pnpm/config.env-replace` catalog bumped from `^3.0.2` → `^4.1.0` (`pnpm-workspace.yaml`, `pnpm-lock.yaml`)
- `config/reader/src/loadNpmrcFiles.ts` — `substituteEnv` now calls `envReplaceLossy` and pushes one warning per unresolved placeholder
- `config/reader/test/index.ts` + `parseCreds.test.ts` — regression tests covering the OIDC case, mixed resolvable/unresolved placeholders, explicit-undefined env values, and `parseCreds({ authToken: '' })`
- `.changeset/oidc-unresolved-env-placeholder.md` — patch bump for `@pnpm/config.reader` and `pnpm`
- `pacquet/crates/config/{env_replace.rs, npmrc_auth.rs, npmrc_auth/tests.rs}` — mirrors the lossy semantics in pacquet's local `env_replace_lossy`, with matching test coverage
2026-05-15 16:25:28 +02:00
Zoltan Kochan
8a80235c7b chore(release): 11.1.2 2026-05-14 13:31:53 +02:00
Zoltan Kochan
732312f49e chore(release): 11.1.0 2026-05-11 19:56:10 +02:00
Zoltan Kochan
e1e29c1520 feat: add --no-runtime to skip installing runtime entries (#11557)
Adds a `--no-runtime` flag (config: `runtime: boolean`, default `true`) that suppresses install of runtime entries declared via `devEngines.runtime` (the `runtime:` protocol) **without modifying the lockfile**.

The lockfile keeps the runtime entry, so frozen-lockfile validation still passes; only the runtime fetch and `.bin` linking are skipped. Useful in CI matrices where the runtime is provisioned externally (e.g. via `pnpm runtime -g set node <version>`) before `pnpm install` runs.

The existing `--runtime-on-fail=ignore` is unsuitable for this case: it mutates the manifest and regenerates the lockfile to drop the runtime entry, which trips frozen-lockfile validation. The two flags are orthogonal and serve different purposes.

### Implementation

The hook lives in the lockfile filter stage:

- `lockfile/filtering/src/filterImporter.ts` — strips `runtime:` refs from the importer's deps maps when `skipRuntimes` is set.
- `lockfile/filtering/src/filterLockfileByImportersAndEngine.ts` — new `skipRuntimes?: boolean` option; runtime-protocol direct deps are dropped before they enter `pickedPackages`, so they never reach the dep graph or bin-linker. Applies to all runtimes (`node`, `deno`, `bun`) since they share the `runtime:` protocol prefix.

The option is plumbed through `installing/deps-restorer`, `installing/deps-installer`, and `installing/commands` to the user-facing `pnpm install --no-runtime` flag.

### Example

```json
// package.json
{
  "devEngines": {
    "runtime": {
      "name": "node",
      "version": "22.13.0",
      "onFail": "download"
    }
  }
}
```

Local dev: `pnpm install` — installs node 22.13.0 as before.

CI matrix entry:
```yaml
- run: pn runtime -g set node ${{ matrix.node }}
- run: pn install --no-runtime
```

The lockfile is unchanged; the matrix's externally-provisioned node is used.
2026-05-09 17:16:27 +00:00
Zoltan Kochan
f2b28f85ff chore(release): 11.0.9 2026-05-09 02:06:35 +02:00
Zoltan Kochan
6925be3b90 fix(config): honor NPM_CONFIG_USERCONFIG as a low-priority fallback (#11545)
* fix(config): honor NPM_CONFIG_USERCONFIG as a low-priority fallback

Restores compatibility with environments that point npm at a custom
.npmrc via NPM_CONFIG_USERCONFIG (e.g. actions/setup-node writing to
${runner.temp}/.npmrc), which silently broke after the v11 env var
prefix change. PNPM-prefixed env vars and npmrcAuthFile from the
global config.yaml continue to take precedence.

Closes #11539

* fix(config): treat empty NPM_CONFIG_USERCONFIG as unset

`??` accepts an empty string as a defined value, so an exported but
unset NPM_CONFIG_USERCONFIG would short-circuit the fallback chain and
make normalizePath('') resolve to process.cwd(). Mirror readEnvVar's
empty-string-to-undefined coercion via a readNpmEnvVar helper so the
fallback to ~/.npmrc works as expected.
2026-05-08 16:00:38 +02:00
Zoltan Kochan
0c3ef0ec94 chore(release): 11.0.7 2026-05-07 00:21:03 +02:00
Zoltan Kochan
7bcfd970e9 fix(config): align scoped registry resolution between config get and publish (#11494)
Fixes [#11492](https://github.com/pnpm/pnpm/issues/11492).

In pnpm v11 a scoped registry resolved to different URLs depending on which command read it:

- `pnpm config get @<scope>:registry` returned the value from `.npmrc`
- `pnpm publish` used the value from `pnpm-workspace.yaml`'s `registries` block

When the two sources disagreed, `pnpm publish` silently targeted the URL from `pnpm-workspace.yaml`, even though `pnpm config get` reported a different (and seemingly authoritative) URL — so users could publish to the wrong registry without any indication.

This PR makes `pnpm config get @<scope>:registry` read from the merged `Config.registries` map (the same map `publish` and the resolvers use) before falling back to `authConfig`. Both commands now report and use the same URL.
2026-05-06 17:06:18 +02:00
Zoltan Kochan
fcec623c00 fix(config): allow user-level preferences in global config.yaml (#11477)
Moves 20 user-level preference settings from the workspace-only exclusion list into the global config allowlist (`config/reader/src/configFileKey.ts`):

- Shell / scripts: `scriptShell`, `shellEmulator`
- Notifications & UI: `updateNotifier`, `useStderr`
- Trust policy (already DLX-inherited as user-level posture): `trustPolicy`, `trustPolicyExclude`, `trustPolicyIgnoreAfter`
- Store / virtual store: `globalVirtualStoreDir`, `virtualStoreDir`, `virtualStoreDirMaxLength`, `verifyStoreIntegrity`, `sideEffectsCache`, `sideEffectsCacheReadonly`
- Build / dep verification: `strictDepBuilds`, `verifyDepsBeforeRun`
- Misc personal/system prefs: `stateDir`, `registrySupportsTimeField`, `initPackageManager`, `initType`, `agent`

These are personal/system preferences rather than workspace structure. In v10 they could be set in `~/.npmrc`. v11 silently dropped them from both `~/.npmrc` and the new global `config.yaml`, leaving `pnpm-workspace.yaml` as the only working location — which the issue author rightly points out is impractical for system-level defaults like `scriptShell`.

After this change:
- Settings in `~/.config/pnpm/config.yaml` are applied instead of being filtered out by `isConfigFileKey` (`config/reader/src/index.ts:296`).
- `pnpm config set --location global scriptShell <path>` succeeds instead of throwing `ConfigSetUnsupportedYamlConfigKeyError` (same predicate used in `config/commands/src/configSet.ts:237`).

`pmOnFail` and `runtimeOnFail` are intentionally left workspace-only because they would cause lockfile divergence between contributors when set globally. `~/.npmrc` support for non-auth/non-network keys is also intentionally not restored — the team has moved those settings to YAML config.

Closes #11474.
2026-05-06 01:44:46 +02:00
Zoltan Kochan
280cf7b858 Merge branch 'release/11.0' 2026-05-05 20:12:28 +02:00
Zoltan Kochan
65f9327014 chore(release): 11.0.6 2026-05-05 19:50:32 +02:00
Dami Oyeniyi
5f34a8d028 fix: reject invalid overrides values (#11380)
* fix: reject invalid overrides values

* fix: improve overrides validation error messages
2026-05-05 19:38:06 +02:00
Zoltan Kochan
8fdd9a9a6f feat(config.reader): export getNetworkConfigs and getDefaultCreds (#11471)
Expose the helpers that build the `configByUri` registry-config map
from a flat rawConfig-style auth dict so that downstream consumers
(e.g. third-party clients of `@pnpm/installing.client` /
`@pnpm/store.connection-manager`) can produce the same configByUri
shape pnpm itself uses, without re-implementing the parsing logic.
2026-05-05 19:37:54 +02:00
Zoltan Kochan
2de318b2e9 fix(config): warn on ignored settings in global config.yaml (#11470)
- pnpm v11 silently drops local-only settings (e.g. `nodeLinker`, `hoistPattern`, `linkWorkspacePackages`) when they appear in the global `config.yaml`. Users had no way to tell their global configuration was being ignored.
- The reader now emits a warning that names the ignored keys and the path of the global config file, and directs users to either move the settings to a project-level `pnpm-workspace.yaml` or share them across projects via [config dependencies](https://pnpm.io/11.x/config-dependencies).
- Allowed keys (e.g. `dangerouslyAllowAllBuilds`, proxy settings) continue to be accepted with no warning.

close #11429
2026-05-05 19:37:46 +02:00