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>
* 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>
* 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`.
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.
* 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>
* 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>
* 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>
* 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>
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
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.
* 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.
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.
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.
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.
- 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
* fix(config): accept uppercase PNPM_CONFIG_* env vars
Env vars are case-sensitive on macOS/Linux, so PNPM_CONFIG_USERCONFIG —
the rename suggested by the v11 migration guide — was silently ignored
because parseEnvVars only matched lowercase pnpm_config_*. Also wire the
env var into the early npmrcAuthFile lookup so it actually decides which
user-level .npmrc gets read.
Closes#11465
* chore: add changeset for npmrc auth file env-var load order
* test: cover lowercase pnpm_config_npmrc_auth_file env var
Matches the exact env var name reported in #11465. Without the early
env-var lookup before loadNpmrcConfig, this case is parsed too late
to actually load the custom .npmrc.
* test: lock precedence when both lowercase and uppercase env vars are set
Fixes#10594, catalogs not being read from the workspace when using the `catalog:` protocol with the `pnpm dlx` / `pnpx` command, resulting in a catalog entry not found error.
Added e2e tests to check if the workspace config is actually loaded. Also added that pnpm dlx reads the retry options from the workspace (Could potentially put that in a separate PR)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit
* **Bug Fixes**
* Fixed catalog resolution when using the `catalog:` protocol with `pnpm dlx` / `pnpx` so catalogs are correctly read from the workspace.
* **New Features**
* `dlx` now inherits workspace catalog and fetch retry/timeout settings so CLI runs respect those local configs.
* **Tests**
* Added tests validating catalog inheritance and failure cases for `dlx` catalog resolution.
* **Chores**
* Updated changeset metadata to mark related packages for patch releases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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.
- 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
* fix(config): accept uppercase PNPM_CONFIG_* env vars
Env vars are case-sensitive on macOS/Linux, so PNPM_CONFIG_USERCONFIG —
the rename suggested by the v11 migration guide — was silently ignored
because parseEnvVars only matched lowercase pnpm_config_*. Also wire the
env var into the early npmrcAuthFile lookup so it actually decides which
user-level .npmrc gets read.
Closes#11465
* chore: add changeset for npmrc auth file env-var load order
* test: cover lowercase pnpm_config_npmrc_auth_file env var
Matches the exact env var name reported in #11465. Without the early
env-var lookup before loadNpmrcConfig, this case is parsed too late
to actually load the custom .npmrc.
* test: lock precedence when both lowercase and uppercase env vars are set
Fixes#10594, catalogs not being read from the workspace when using the `catalog:` protocol with the `pnpm dlx` / `pnpx` command, resulting in a catalog entry not found error.
Added e2e tests to check if the workspace config is actually loaded. Also added that pnpm dlx reads the retry options from the workspace (Could potentially put that in a separate PR)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit
* **Bug Fixes**
* Fixed catalog resolution when using the `catalog:` protocol with `pnpm dlx` / `pnpx` so catalogs are correctly read from the workspace.
* **New Features**
* `dlx` now inherits workspace catalog and fetch retry/timeout settings so CLI runs respect those local configs.
* **Tests**
* Added tests validating catalog inheritance and failure cases for `dlx` catalog resolution.
* **Chores**
* Updated changeset metadata to mark related packages for patch releases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* fix(config): default minimumReleaseAgeStrict to true when user sets minimumReleaseAge
Without this, a user-set `minimumReleaseAge` would silently fall back to
installing an immature version when no mature version satisfied the
requested range, making the setting look like it had no effect (#11433).
The built-in default of `minimumReleaseAge` (1440) stays non-strict for
backward compatibility, and an explicit `minimumReleaseAgeStrict: false`
is still respected.
* chore(changeset): downgrade to patch
* fix(config): apply minimumReleaseAgeStrict default after env var parsing
Move the strict-default logic to run after `parseEnvVars` so
`pnpm_config_minimum_release_age` is also covered.
* test(config): also assert minimumReleaseAge in the strict=false test
* fix(config): default minimumReleaseAgeStrict to true when user sets minimumReleaseAge
Without this, a user-set `minimumReleaseAge` would silently fall back to
installing an immature version when no mature version satisfied the
requested range, making the setting look like it had no effect (#11433).
The built-in default of `minimumReleaseAge` (1440) stays non-strict for
backward compatibility, and an explicit `minimumReleaseAgeStrict: false`
is still respected.
* chore(changeset): downgrade to patch
* fix(config): apply minimumReleaseAgeStrict default after env var parsing
Move the strict-default logic to run after `parseEnvVars` so
`pnpm_config_minimum_release_age` is also covered.
* test(config): also assert minimumReleaseAge in the strict=false test
* fix: sync packageManager and devEngines.packageManager on self-update
When `package.json` declares both `packageManager` and
`devEngines.packageManager`, `pnpm self-update` previously bumped only
the latter — leaving Corepack (which reads `packageManager`) pinned to
the old version until a manual edit.
Now, when `packageManager` pins pnpm, both fields are rewritten to the
new exact version on update: `packageManager` to `pnpm@<version>`
(without an integrity hash) and `devEngines.packageManager.version` to
the same exact `<version>` (dropping any range operator). When only
`devEngines.packageManager` is declared, the existing range-preserving
behavior is unchanged.
Closes#11388
* refactor: export and reuse parsePackageManager from @pnpm/config.reader
Drop the inline duplicate in self-updater and use the existing
parser from config.reader. Same parsing rules (strips integrity
hash, rejects URL-style refs).
* refactor: collapse devEngines.packageManager array/object branches
Resolve to the underlying pnpm entry first (whether the field is an
array or an object) and run the version-update logic once, instead of
duplicating it across both branches.
* fix: sync packageManager and devEngines.packageManager on self-update
When `package.json` declares both `packageManager` and
`devEngines.packageManager`, `pnpm self-update` previously bumped only
the latter — leaving Corepack (which reads `packageManager`) pinned to
the old version until a manual edit.
Now, when `packageManager` pins pnpm, both fields are rewritten to the
new exact version on update: `packageManager` to `pnpm@<version>`
(without an integrity hash) and `devEngines.packageManager.version` to
the same exact `<version>` (dropping any range operator). When only
`devEngines.packageManager` is declared, the existing range-preserving
behavior is unchanged.
Closes#11388
* refactor: export and reuse parsePackageManager from @pnpm/config.reader
Drop the inline duplicate in self-updater and use the existing
parser from config.reader. Same parsing rules (strips integrity
hash, rejects URL-style refs).
* refactor: collapse devEngines.packageManager array/object branches
Resolve to the underlying pnpm entry first (whether the field is an
array or an object) and run the version-update logic once, instead of
duplicating it across both branches.
This is consistent with #9358, but implements support for the GitHub Packages npm registry and, more broadly, for vlt-style https://docs.vlt.sh/cli/registries for any registry.
This PR adds a built-in gh: specifier that resolves against the GitHub Packages npm registry, plus a namedRegistries config key so a project can map its own aliases to arbitrary registries. A project can mix public npm packages and private GitHub Packages (or self-hosted) ones without applying a scope-wide registry override to every @scope/* package.
- pnpm add gh:@acme/private writes "@acme/private": "gh:^1.0.0" and resolves from https://npm.pkg.github.com/.
- pnpm add gh:@acme/private@^1.0.0 (with or without an alias) is also supported. Aliased form writes "my-alias": "gh:@acme/private@^1.0.0".
- Auth comes from the existing per-URL .npmrc mechanism, e.g. //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}. No new auth surface.
- @github is intentionally not defaulted to https://npm.pkg.github.com/ - hardcoding that would hijack installs of the public @github/* packages on npmjs.org (e.g. @github/relative-time-element) for users without a scope-wide override. Use gh: to install from GitHub Packages, or configure @github:registry=... yourself if that's really what you want.
- Additional named registries (a self-hosted proxy, GitHub Enterprise Server, etc.) can be configured in pnpm-workspace.yaml:
```yml
namedRegistries:
gh: https://npm.pkg.github.example.com/ # optional: overrides the built-in `gh` alias for GHES
work: https://npm.work.example.com/
```
- Then work:@corp/lib@^2.0.0 resolves against https://npm.work.example.com/, and the built-in gh alias can be redirected to a GHES host.
- Env-var substitution (${VAR}) is supported in namedRegistries values, mirroring the .npmrc convention.
- Reserved alias names (npm, jsr, github, workspace, catalog, file, git, http, https, link, patch, and related git host shorthands) cannot be redefined as user-named registries - the resolver throws ERR_PNPM_RESERVED_NAMED_REGISTRY_ALIAS at startup rather than silently shadowing another protocol. Malformed URLs throw ERR_PNPM_INVALID_NAMED_REGISTRY_URL at startup too, instead of failing as a confusing 404 during resolution.
- On publish, createExportableManifest strips any named-registry prefix (both the built-in gh: and any user-configured alias) so npm and yarn consumers can still resolve the dependency via their own scope-registry configuration - mirroring the user-facing requirement when installing such a dep without the prefix.
The prefix is gh: rather than github: because github: is reserved by npm-package-arg / hosted-git-info as a git host shorthand (e.g. github:owner/repo) - reusing it would be a deviation from the specs used by the npm CLI. gh: is shorter, matches vlt's convention, and cannot collide with any existing npm scheme.
Unlike jsr:, gh: (and any other named-registry alias) does not rewrite the package name - gh:@acme/foo resolves @acme/foo from the GitHub Packages registry as-is. This also means npm/yarn consumers see the original name after the prefix is stripped on publish.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* chore: upgrade @typescript/native-preview to 7.0.0-dev.20260421.2
- Add explicit `types: ["node"]` to the shared tsconfig because tsgo
20260421 no longer auto-acquires `@types/*` from `node_modules`.
- Refactor test files to explicitly import jest globals (`describe`,
`it`, `test`, `expect`, `beforeEach`, etc.) from `@jest/globals`
instead of relying on `@types/jest` ambient declarations. Under the
new tsgo build, `import { jest } from '@jest/globals'` shadows the
ambient `jest` namespace, breaking `@types/jest`'s `declare var
describe: jest.Describe;` globals.
- Add `@jest/globals` to each package's devDependencies where tests
now import from it, and add `@types/node` to packages that need it
but were relying on hoisted resolution.
- Replace `fail()` calls with `throw new Error(...)` since `fail` is
no longer globally available.
* chore: fix remaining tsgo type-strictness errors
- Strip `as <PnpmType>` casts on objects passed to toMatchObject /
toStrictEqual / toEqual; @jest/globals rejects the typed objects
(which include AsymmetricMatchers) vs. the repo-specific type.
- Type `jest.fn<...>()` explicitly where the mock's signature matters
for toHaveBeenCalledWith.
- Replace `beforeEach(() => X)` with `beforeEach(() => { X })` so the
return value is void, as the stricter jest typing requires.
- Use `expect.objectContaining({...})` in one place where the full
expected object triggered stricter type resolution.
- Cast `prompt.mock.calls` arg through `as unknown as Record<...>[]`
for patch.test.ts's nested-array matchers.
- Fix off-by-one `<reference path>` in pnpm/test/getConfig.test.ts
that only surfaced now.
- Move `@jest/globals` from devDependencies to dependencies in the
two `__utils__` packages that import it from `src/`.
- Clean up unused imports from the @jest/globals migration.
* chore: address Copilot review on #11332
- Move misplaced `@jest/globals` imports to the top import block in
checkEngine, run.ts, and workspace/root-finder tests where the
script dropped them below executable code.
- Replace `try { await x(); throw new Error('should have thrown') } catch`
in bins/linker, lockfile/fs, and resolving/local-resolver tests with
`await expect(x()).rejects.toMatchObject({...})`. The old pattern
swallowed an unrelated `throw` if the under-test call silently
succeeded, which would fail on the catch-block assertion with a
misleading message.
* chore: upgrade @typescript/native-preview to 7.0.0-dev.20260421.2
- Add explicit `types: ["node"]` to the shared tsconfig because tsgo
20260421 no longer auto-acquires `@types/*` from `node_modules`.
- Refactor test files to explicitly import jest globals (`describe`,
`it`, `test`, `expect`, `beforeEach`, etc.) from `@jest/globals`
instead of relying on `@types/jest` ambient declarations. Under the
new tsgo build, `import { jest } from '@jest/globals'` shadows the
ambient `jest` namespace, breaking `@types/jest`'s `declare var
describe: jest.Describe;` globals.
- Add `@jest/globals` to each package's devDependencies where tests
now import from it, and add `@types/node` to packages that need it
but were relying on hoisted resolution.
- Replace `fail()` calls with `throw new Error(...)` since `fail` is
no longer globally available.
* chore: fix remaining tsgo type-strictness errors
- Strip `as <PnpmType>` casts on objects passed to toMatchObject /
toStrictEqual / toEqual; @jest/globals rejects the typed objects
(which include AsymmetricMatchers) vs. the repo-specific type.
- Type `jest.fn<...>()` explicitly where the mock's signature matters
for toHaveBeenCalledWith.
- Replace `beforeEach(() => X)` with `beforeEach(() => { X })` so the
return value is void, as the stricter jest typing requires.
- Use `expect.objectContaining({...})` in one place where the full
expected object triggered stricter type resolution.
- Cast `prompt.mock.calls` arg through `as unknown as Record<...>[]`
for patch.test.ts's nested-array matchers.
- Fix off-by-one `<reference path>` in pnpm/test/getConfig.test.ts
that only surfaced now.
- Move `@jest/globals` from devDependencies to dependencies in the
two `__utils__` packages that import it from `src/`.
- Clean up unused imports from the @jest/globals migration.
* chore: address Copilot review on #11332
- Move misplaced `@jest/globals` imports to the top import block in
checkEngine, run.ts, and workspace/root-finder tests where the
script dropped them below executable code.
- Replace `try { await x(); throw new Error('should have thrown') } catch`
in bins/linker, lockfile/fs, and resolving/local-resolver tests with
`await expect(x()).rejects.toMatchObject({...})`. The old pattern
swallowed an unrelated `throw` if the under-test call silently
succeeded, which would fail on the catch-block assertion with a
misleading message.