Commit Graph

2771 Commits

Author SHA1 Message Date
Zoltan Kochan
e21ff63267 feat: add minimumReleaseAgeLoose setting
When true (the default), pnpm falls back to versions that don't meet the
minimumReleaseAge constraint if no mature versions satisfy the range
being resolved.
2026-04-09 12:14:30 +02:00
Brandon Cheng
9b0a460c8d fix: non-deterministic resolution causing pnpm dedupe --check to unexpectedly fail (#11110)
* test: ensure prerelease weighting is correct

* fix: use higher weight for package versions already in lockfile

* test: remove fundamentally incompatible test

* fix(test): use undici MockAgent instead of nock for HTTP mocking

nock only patches Node's built-in http/https modules, but pnpm uses
undici for HTTP requests. Replace nock with @pnpm/testing.mock-agent
(which wraps undici's MockAgent) so the regression test actually
intercepts registry metadata requests.

* fix(benchmarks): show errors from store populate step

The populate step redirected both stdout and stderr to /dev/null,
hiding the actual error when pnpm install fails during benchmarks.

* fix(benchmarks): replace deprecated packages in benchmark fixture

The old fixture used deprecated babel 6, gulp, and other legacy
packages whose transitive dependencies (e.g. es-abstract) are missing
the "time" field in registry metadata, causing ERR_PNPM_MISSING_TIME
with time-based resolution mode.

Replace with modern equivalents (babel 7, webpack 5, MUI, Redux
Toolkit, etc.) that maintain a similar dependency tree size (~1300
packages) while using well-maintained packages with proper registry
metadata.

* fix(benchmarks): drop eslint plugins that pull in es-abstract

eslint-plugin-react, eslint-plugin-import, and eslint-plugin-jsx-a11y
transitively depend on es-abstract, whose registry metadata lacks the
"time" field. Replace them with eslint-plugin-prettier to avoid
ERR_PNPM_MISSING_TIME with time-based resolution.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-04-07 23:46:27 +02:00
Zoltan Kochan
7721d2e7f0 feat(audit): add patched versions to minimumReleaseAgeExclude on audit --fix (#11216)
When `pnpm audit --fix` adds overrides to fix vulnerabilities, it now
also adds the minimum patched version for each advisory to
`minimumReleaseAgeExclude` in pnpm-workspace.yaml. This allows
`pnpm install` to install the security fix without waiting for it to
satisfy the minimumReleaseAge constraint.

Closes #10263
2026-04-07 16:38:57 +02:00
Zoltan Kochan
51b04c3e9a refactor!: remove ignoreDepScripts and neverBuiltDependencies (#11220)
* refactor: remove ignoreDepScripts and neverBuiltDependencies settings

These settings are redundant in v11:
- `ignore-dep-scripts` is superseded by the default behavior of `allowBuilds`
- `neverBuiltDependencies` was already dead code, replaced by `allowBuilds`

* chore: add changeset for removed ignore-dep-scripts setting
2026-04-07 13:41:13 +02:00
Khải
16cfde66ec feat: pnpm logout (#11213)
* feat(auth): implement `pnpm logout` command

Adds a new `pnpm logout` command that logs users out of npm registries.
The command revokes the authentication token on the registry via
DELETE /-/user/token/{token}, then removes it from the local auth.ini
config file. Token revocation is best-effort: local cleanup always
proceeds even if the registry is unreachable or doesn't support
revocation.

Uses the same dependency injection pattern as `pnpm login` for
comprehensive testability.

https://claude.ai/code/session_016fw5sdGFtBiB9QapMKEuXa

* fix(auth): address review feedback on pnpm logout

- Rename revokeToken to tryRevokeToken for self-documenting code
- Extract token removal into removeTokenFromAuthIni function
- Remove redundant comments that restate function names
- Fix toHaveProperty to use array syntax for keys containing dots
  (avoids Jest property path parsing pitfall)
- Add globalWarn when token is found in authConfig but not in auth.ini,
  informing the user it must be removed manually from .npmrc
- Add tests for the .npmrc-only warning case

https://claude.ai/code/session_016fw5sdGFtBiB9QapMKEuXa

* fix(auth): fix Windows CI failure in logout test

Use path.join in test expectations for the warning message path,
since path.join produces backslashes on Windows.

https://claude.ai/code/session_016fw5sdGFtBiB9QapMKEuXa

* refactor(auth): use jest.fn() for fetch assertions in logout tests

Replace manual fetchedUrls arrays with jest.fn() mocks and use
toHaveBeenCalledWith for cleaner, more idiomatic assertions.

https://claude.ai/code/session_016fw5sdGFtBiB9QapMKEuXa

* refactor: destructure `context`

* refactor: literal types for `method`

* refactor(auth): test cleanup per review feedback

- Rename mockFetch to fetch for shorthand property syntax
- Use platform-aware configDir in warning tests instead of
  path.join on Unix-style paths

https://claude.ai/code/session_016fw5sdGFtBiB9QapMKEuXa

* style(auth): remove redundant return in createMockResponse arrow

Single-statement return-with-braces arrow function converted to
expression-body form.

https://claude.ai/code/session_016fw5sdGFtBiB9QapMKEuXa

* fix(auth): address Copilot review on pnpm logout

- Send Authorization: Bearer header in the DELETE token revocation
  request, otherwise the registry returns 401 and the token is not
  actually revoked
- Make tryRevokeToken return a boolean indicating whether the token
  was actually revoked, and use it to choose the right warning when
  the token is not in auth.ini
- Drop the misleading "(token removed locally)" suffix from the
  registry-failure log messages, since the local removal may not
  happen
- Extract getRegistryConfigKey and safeReadIniFile from login.ts and
  logout.ts into a shared module to prevent the two commands from
  drifting apart over time
- Add tests asserting the Authorization header is sent and that the
  warning correctly distinguishes between revoked and not-revoked
  cases

https://claude.ai/code/session_016fw5sdGFtBiB9QapMKEuXa

* fix(auth): throw on logout when nothing actually happened

When the registry rejects the token revocation AND the token is not
in auth.ini, neither side effect of logout actually happened — the
user is still authenticated locally and on the registry. Throwing
an ERR_PNPM_LOGOUT_FAILED error in this case avoids the misleading
"Logged out of ..." success message and gives a non-zero exit code.

https://claude.ai/code/session_016fw5sdGFtBiB9QapMKEuXa

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-07 11:19:42 +02:00
Zoltan Kochan
853be661d8 feat: add native pnpm dist-tag command (#11218)
Implement dist-tag ls, add, and rm subcommands natively instead of
delegating to npm. Follows the same pattern as the recently added
deprecate and unpublish commands.
2026-04-07 10:22:03 +02:00
Alessio Attilio
2c90d4061d feat: add native pnpm deprecate and undeprecate commands (#11120)
Adds native `pnpm deprecate` and `pnpm undeprecate` commands that interact with the npm registry directly instead of delegating to the npm CLI.

## Changes

- **New package `@pnpm/registry-access.commands`** — a new top-level domain for commands that manage packages on the registry
- **`pnpm deprecate <package>[@<version>] <message>`** — sets a deprecation message on matching versions
- **`pnpm undeprecate <package>[@<version>]`** — removes deprecation from already-deprecated versions
- Updated `pnpm-workspace.yaml`, CLI registration, and meta-updater config
- Removed `deprecate` from the not-implemented commands list
- Added changeset

## Implementation Details

- `registry-access/commands/src/deprecation/common.ts` — shared logic (auth, registry fetch, version matching)
- `registry-access/commands/src/deprecation/deprecate.ts` — deprecate command handler
- `registry-access/commands/src/deprecation/undeprecate.ts` — undeprecate command handler
- Uses `pickRegistryForPackage` for per-scope registry support
- Uses `createFetchFromRegistry`/`fetchWithDispatcher` for proxy/TLS support
- Uses `@pnpm/npm-package-arg` for package spec parsing
- Reuses `PackageMeta`/`PackageInRegistry` types from `@pnpm/resolving.registry.types`
- Sends minimal payload (only `name` + modified `versions.deprecated` fields) to the registry

## Usage

```bash
# Deprecate all versions
pnpm deprecate my-package "This package is deprecated"

# Deprecate a specific version
pnpm deprecate my-package@1.0.0 "Use v2 instead"

# Undeprecate all versions
pnpm undeprecate my-package

# Undeprecate a specific version
pnpm undeprecate my-package@1.0.0
```

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-04-06 15:30:11 +02:00
Zoltan Kochan
96704a1c58 refactor(config): rename rawConfig to authConfig, add nodeDownloadMirrors, simplify config reader (#11194)
Major cleanup of the config system after migrating settings from `.npmrc` to `pnpm-workspace.yaml`.

### Config reader simplification
- Remove `checkUnknownSetting` (dead code, always `false`)
- Trim `npmConfigTypes` from ~127 to ~67 keys (remove unused npm config keys)
- Replace `rcOptions` iteration over all type keys with direct construction from defaults + auth overlay
- Remove `rcOptionsTypes` parameter from `getConfig()` and its assembly chain

### Rename `rawConfig` to `authConfig`
- `rawConfig` was a confusing mix of auth data and general settings
- Non-auth settings are already on the typed `Config` object — stop duplicating them in `rawConfig`
- Rename `rawConfig` → `authConfig` across the codebase to clarify it only contains auth/registry data from `.npmrc`

### Remove `rawConfig` from non-auth consumers
- **Lifecycle hooks**: replace `rawConfig: object` with `userAgent?: string` — only user-agent was read
- **Fetchers**: remove unused `rawConfig` from git fetcher, binary fetcher, tarball fetcher, prepare-package
- **Update command**: use `opts.production/dev/optional` instead of `rawConfig.*`
- **`pnpm init`**: accept typed init properties instead of parsing `rawConfig`

### Add `nodeDownloadMirrors` setting
- New `nodeDownloadMirrors?: Record<string, string>` on `PnpmSettings` and `Config`
- Replaces the `node-mirror:<channel>` pattern that was stored in `rawConfig`
- Configured in `pnpm-workspace.yaml`:
  ```yaml
  nodeDownloadMirrors:
    release: https://my-mirror.example.com/download/release/
  ```
- Remove unused `rawConfig` from deno-resolver and bun-resolver

### Refactor `pnpm config get/list`
- New `configToRecord()` builds display data from typed Config properties on the fly
- Excludes sensitive internals (`authInfos`, `sslConfigs`, etc.)
- Non-types keys (e.g., `package-extensions`) resolve through `configToRecord` instead of direct property access
- Delete `processConfig.ts` (replaced by `configToRecord.ts`)

### Pre-push hook improvement
- Add `compile-only` (`tsgo --build`) to pre-push hook to catch type errors before push
2026-04-04 20:33:43 +02:00
sotanengel
c7203b99ad feat!: set default minimumReleaseAge to 1 day (1440 minutes) (#11158)
set default minimumReleaseAge to 1 day

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-04-04 13:26:22 +02:00
Zoltan Kochan
8bba5c3858 refactor(config): only read auth/registry from .npmrc, add registries to pnpm-workspace.yaml (#11189)
Replace the unmaintained @pnpm/npm-conf package with a purpose-built
module that reads only auth/registry-related settings from .npmrc files
using read-ini-file + @pnpm/config.env-replace (both already deps).

All non-registry settings (hoist-pattern, node-linker, etc.) are now
only read from pnpm-workspace.yaml, CLI options, or environment
variables. Registry-related settings (auth tokens, registry URLs,
SSL certs, proxy settings) continue to be read from .npmrc for
migration compatibility, and can also be set in pnpm-workspace.yaml.

New modules:
- loadNpmrcFiles.ts: reads .npmrc from standard locations, filters to
  auth/registry keys, returns structured layers
- npmConfigTypes.ts: inlined npm config type definitions
- npmDefaults.ts: inlined npm defaults (registry, unsafe-perm, etc.)
2026-04-04 02:44:12 +02:00
Khải
de3dc74439 feat(auth): keyboard event to open browser (#11148)
* feat(auth): add "Press ENTER to open in browser" during web authentication

During web-based authentication (login, publish), users can now press
ENTER to open the authentication URL in their default browser. The
background token polling continues uninterrupted, so users who prefer
to authenticate on their phone can still do so without pressing anything.

The implementation uses Node's readline module (not raw mode), so Ctrl+C
and Ctrl+Z continue to work normally. It is fully error-tolerant: if the
keyboard listener or browser opening fails, a warning is printed and the
polling continues.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix(auth): inject readline and execFile directly, not wrapper functions

Address review feedback:
- Remove defaultListenForEnter and defaultOpenBrowser wrapper functions
- Inject readline module and execFile function directly via context
- DEFAULT_CONTEXT now references modules directly (no closures)
- Use switch for platform detection, default = no browser prompt
- Rename pollWithBrowserOpen → offerToOpenBrowser (clearer name)
- Add platform-specific tests (darwin, win32, linux, freebsd)
- Use PassThrough streams for stdin mocks in tests

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix(auth): fix CI type errors in test mocks

- Type jest.fn() mocks for readline.createInterface properly
- Use PassThrough streams for stdin mocks in releasing/commands tests

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* refactor(auth): use generic Stdin parameter to eliminate PassThrough in tests

Per review feedback, add a generic Stdin type parameter to context
interfaces. This ties process.stdin and readline.createInterface together
through the same type, so tests can use simple { isTTY: true } mocks
instead of requiring PassThrough streams.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix(auth): propagate Stdin generic to releasing/commands OtpContext

The OtpContext in releasing/commands extends BaseOtpContext from
web-auth. Now that BaseOtpContext is generic, the local OtpContext
and publishWithOtpHandling must also be generic so tests can use
simple stdin mocks without PassThrough.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix: sort imports in releasing/commands otp.ts

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* refactor(auth): use .bind() for readline injection instead of generics

Per review feedback, revert the generic Stdin approach and instead use
readline.createInterface.bind(null, { input: process.stdin }) as the
injectable dependency. This avoids generics proliferation while keeping
the context clean — no arrow functions or closures in DEFAULT_CONTEXT.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* feat(publish): add "Press ENTER to open in browser" during publish OTP

Wire up createReadlineInterface and execFile in the publish
SHARED_CONTEXT so that pnpm publish also offers to open the browser
during web-based OTP authentication.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix(auth): improve browser-open prompt message

Change "Press ENTER to open in browser..." to
"Press ENTER to open the URL in your browser."

The old message implied the user should press Enter. The new wording
presents it as an available action, not an instruction — users can
also scan the QR code or copy-paste the URL.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* style: remove unnecessary arrow wrapper around createMockReadlineInterface

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* docs: explain why Enter keypress is fire-and-forget, not awaited

Add a comment explaining that only pollPromise is awaited — the Enter
listener is intentionally not part of a Promise.all. This prevents a
future refactor from reintroducing the npm bug where authentication
blocks until Enter is pressed, even when the user authenticates on
another device.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* docs: add permalink to npm's Promise.all bug in comment

Link to the specific npm-profile commit (d1a48be4259) so the comment
remains accurate even if npm fixes the bug in the future.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix: correct line numbers in npm-profile permalink (L85-L98)

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* style: apply review suggestion for npm-profile permalink format

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* style: remove duplicate line in npm-profile comment

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix: shadow global process instead of renaming to proc

Destructure as `process` (not `proc`) so the global `process` is
shadowed, preventing accidental direct access to it.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix: merge process fields in test mock contexts

Restructure createMockContext to merge process fields instead of
replacing the entire object. Tests that only need to override
platform or stdin no longer need to redundantly provide the other.

Also adds a test for undefined platform (default: case).

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix: use Omit+Partial for process overrides in test mock contexts

The process field spread `...overrides?.process` merges at runtime but
TypeScript still requires all fields in the override type. Fix by typing
the process override as Partial via Omit<..., 'process'> & { process?: Partial<...> }.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* refactor: extract a type alias

* refactor: extract MockContextOverrides type alias in remaining tests

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* refactor(auth): extract process types, use NodeJS.Platform, clean up tests

- Extract OfferToOpenBrowserProcess interface from inline process type
- Extract LoginProcess interface from inline process type in LoginContext
- Use NodeJS.Platform instead of string for platform fields (prevents typos)
- Rename simulateEnter → simulateEnterKeypress (clarify it's the key)
- Convert single-return functions to arrow expressions in tests
- Update test descriptions to say "Enter key" / "Enter keypress"

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* refactor(auth): rename offerToOpenBrowser → promptBrowserOpen

Per review feedback, "offer to open browser" was mouthful. Renamed
function, file, and all associated types (OfferToOpenBrowser* →
PromptBrowserOpen*).

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* docs: drop "IMPORTANT"

* refactor(auth): extract OtpProcess interface from inline process type

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix(auth): validate authUrl before passing to execFile

On Windows, cmd.exe re-parses execFile arguments with full shell
grammar, so metacharacters (&, |, ^, etc.) in the URL would be
interpreted as operators. Validate that authUrl is a well-formed
http(s) URL before passing it to the platform browser command.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* test(auth): add regression test for URLs with query parameters on win32

Verifies that URLs containing & and other query string characters are
passed through to execFile as-is on the win32 platform.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix(auth): escape cmd.exe metacharacters in Windows browser open URL

On Windows, cmd.exe re-parses execFile arguments and treats & | < > ^ %
as operators. Escape these with ^ so query strings in auth URLs
(e.g. ?token=abc&redirect=...) are not split by cmd.exe.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix(auth): use canonicalized URL and expand cmd.exe escape set

- Use parsedUrl.href (canonicalized by new URL()) instead of the raw
  authUrl string, ensuring percent-encoding of spaces and special chars.
- Expand cmd.exe metacharacter escaping to include () and ! in addition
  to & | < > ^ %, covering grouping operators and delayed expansion.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* docs(auth): document Windows browser-opening edge cases

Explain why cmd /c start is used instead of ShellExecuteW (not
callable from Node.js without a native addon), why alternatives
like explorer.exe, rundll32, and PowerShell are unreliable, and
note that a Rust/N-API addon could replace this in the future.

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

* fix: fix cspell errors in Windows browser-open comment

Reword to avoid unknown words "rundll" and "metacharacter".

https://claude.ai/code/session_01UtDnjrNQ2Cc3GLAPR8BrrW

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-03 01:36:00 +02:00
Zoltan Kochan
6b3d87a4ca perf: optimize undici connection settings and tarball buffering (#11151)
- Enable Happy Eyeballs (`autoSelectFamily`) for faster dual-stack (IPv4/IPv6) connection establishment
- Increase keep-alive timeouts (30s idle, 10min max) to reduce connection churn during install
- Set optimized global dispatcher so requests without custom options still benefit
- Pre-allocate `SharedArrayBuffer` for tarball downloads when `Content-Length` is known, avoiding intermediate chunk array and double-copy
2026-03-31 00:33:42 +02:00
Zoltan Kochan
9b1e5da6f7 fix: auto import mode falls through to hardlinks on ENOTSUP (#11150)
The ENOTSUP fallback in createClonePkg() silently converted clone
failures to file copies, preventing the auto-importer from detecting
that cloning is not supported and falling through to hardlinks.

On filesystems without reflink support (e.g. ext4 on Linux CI),
this caused every file to be copied instead of hardlinked — a 2-9x
regression for install operations on large projects.

The fix uses a raw clone (without ENOTSUP fallback) for the auto-mode
probe. If the filesystem doesn't support cloning, the error propagates
and the auto-importer falls through to hardlinks. Once cloning is
confirmed to work, subsequent packages use the full clone importer
with ENOTSUP fallback for transient failures under heavy parallel I/O.
2026-03-30 18:25:58 +02:00
Zoltan Kochan
f92517bec1 feat: error on deprecated CLI options instead of warning
Remove special handling for --independent-leaves, --lock, and
--resolution-strategy. They are now treated as unknown options.
2026-03-30 14:29:53 +02:00
Zoltan Kochan
00dcdfd38d feat: add pnpm pm prefix to force built-in commands (#11147)
- Added `pnpm pm <command>` syntax that always runs the built-in pnpm command, bypassing any same-named script in `package.json`
- When a project defines a script like `"clean": "rm -rf dist"`, `pnpm clean` runs that script, but `pnpm pm clean` runs the built-in clean command
- This applies to all overridable commands: `clean`, `purge`, `rebuild`, `deploy`, `setup`
2026-03-30 09:51:04 +02:00
Zoltan Kochan
2df8b71467 refactor(config): stop shelling out to npm for auth settings (#11146)
* refactor(config): stop shelling out to npm for auth settings

Read and write auth-related settings (registry, tokens, credentials,
scoped registries) directly to INI config files instead of delegating
to `npm config`. Removes the @pnpm/exec.run-npm dependency from
@pnpm/config.commands.

* fix(config): give pnpm global rc priority over ~/.npmrc for auth settings

Auth settings from the pnpm global rc file (e.g. ~/.config/pnpm/rc) now
override ~/.npmrc in rawConfig. This ensures tokens written by `pnpm login`
are correctly picked up by `pnpm publish`, since login writes to the pnpm
global rc but ~/.npmrc previously took priority in the npm-conf chain.

* chore: remove @pnpm/exec.run-npm package

No longer used after removing npm config CLI delegation.

* chore: remove accidentally committed __typecheck__/tsconfig.json

* fix(config): narrow non-string rejection to credential keys, add priority test

Non-string value rejection now only applies to credential keys (_auth,
_authToken, _password, username), registry URLs, and scoped/registry-
prefixed keys — not to INI settings like strict-ssl, proxy, or ca that
can legitimately have boolean/null values.

Added a test verifying that auth tokens from the pnpm global rc take
priority over ~/.npmrc.
2026-03-29 23:28:23 +02:00
Zoltan Kochan
6c480a4375 perf: replace node-fetch with undici (#10537)
Replace node-fetch with native undici for HTTP requests throughout pnpm.

Key changes:
- Replace node-fetch with undici's fetch() and dispatcher system
- Replace @pnpm/network.agent with a new dispatcher module in @pnpm/network.fetch
- Cache dispatchers via LRU cache keyed by connection parameters
- Handle proxies via undici ProxyAgent instead of http/https-proxy-agent
- Convert test mocking from nock to undici MockAgent where applicable
- Add minimatch@9 override to fix ESM incompatibility with brace-expansion
2026-03-29 12:44:00 +02:00
Alessio Attilio
d8be9706d9 fix: respect frozen-lockfile flag when migrating config dependencies (#11067)
* fix: respect frozen-lockfile flag when migrating config dependencies

* fix: throw FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE when installing config deps with --frozen-lockfile

* fix: correct changeset package name and clean up minor issues

- Fix changeset referencing non-existent @pnpm/config.deps-installer
  (should be @pnpm/installing.env-installer)
- Fix merge artifact in AGENTS.md
- Revert unnecessary Promise.all refactoring in migrateConfigDeps.ts
- Remove extra blank line in test file

* fix: move frozenLockfile check to call site and add missing tests

Move the frozenLockfile check from migrateConfigDepsToLockfile() to
normalizeForInstall() to minimize the number of check points.

Add unit tests for all frozenLockfile code paths:
- installConfigDeps: migration fails with frozenLockfile
- resolveAndInstallConfigDeps: old-format migration, new-format
  resolution, and up-to-date lockfile success
- resolveConfigDeps: fails with frozenLockfile

* refactor: consolidate duplicate frozenLockfile checks in resolveAndInstallConfigDeps

Merge two identical frozenLockfile throw statements into a single check
covering both lockfileChanged and depsToResolve conditions.

* Delete respect-frozen-lockfile.md

* refactor: order fields

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-03-28 18:17:52 +01:00
Zoltan Kochan
64393a3148 refactor: suggest "pnpm peers check" instead of rendering peer issues tree during install (#11133)
Instead of rendering the full peer dependency issues tree during installation,
suggest users run "pnpm peers check" to view the issues. Remove the now-unused
@pnpm/installing.render-peer-issues package.
2026-03-28 16:06:59 +01:00
Zoltan Kochan
f871365adb feat!: use cleaner output for script execution (#11132)
* feat: use yarn-like output for script execution

Print `$ command` instead of `> pkg@version stage path\n> command`.
Show project name and path only when running in a different directory.

* fix: sort chalk dependency after @pnpm packages

* refactor: remove project info line from run output

* chore: add changeset

* refactor: print script command line to stderr

The `$ command` line is metadata, not program output. Printing it to
stderr keeps stdout clean for piping, matching bun's behavior.

* chore: update changeset to major
2026-03-28 15:58:03 +01:00
Zoltan Kochan
366cabeec8 fix: stop setting npm_config_ env vars during lifecycle scripts (#11116)
* fix: stop setting npm_config_ env vars from pnpm config during lifecycle scripts

Update @pnpm/npm-lifecycle to 1100.0.0-0 which no longer dumps the
entire pnpm config as npm_config_* environment variables. This fixes
npm warnings about unknown config when lifecycle scripts invoke npm.

Only well-known npm_* env vars are now set, matching Yarn's behavior.

* fix: fix spellcheck in changeset

* chore: remove obsolete @pnpm/npm-lifecycle patch file

* fix: pass npm_config_user_agent via extraEnv in lifecycle scripts

The npm-lifecycle makeEnv() strips all npm_* vars from process.env,
so npm_config_user_agent must be explicitly passed via extraEnv.

* chore: mark changeset as major (breaking change)
2026-03-27 19:02:07 +01:00
Alessio Attilio
d3d6938414 feat: add native view/info/show/v command (#11064)
* feat: add native view/info command

* test: add unit tests for native view command

* fix(view): support ranges, aliases, and tags

* chore: update lockfile and tsconfig

* refactor(view): reuse pickPackageFromMeta from npm-resolver

- Share version resolution logic with the npm-resolver instead of
  reimplementing tag/range/version matching in the view command.
- Export pickPackageFromMeta and pickVersionByVersionRange from
  @pnpm/resolving.npm-resolver.
- Remove redundant double HTTP fetch (metadata already contains all
  version data).
- Remove duplicate author/repository fields from PackageInRegistry
  (already inherited from BaseManifest).
- Consolidate four changesets into one.
- Revert unrelated .gitignore change.
- Drop direct semver dependency from deps.inspection.commands.

* refactor(view): reuse fetchMetadataFromFromRegistry from npm-resolver

Use the npm-resolver's fetchMetadataFromFromRegistry instead of
hand-rolled fetch logic. This fixes:
- Broken URL encoding for scoped packages (@scope/pkg)
- Missing auth header, proxy, SSL, and retry config
- Duplicated fetch + error handling code

Also pass proper Config options (rawConfig, userAgent, SSL, proxy,
retry, timeout) through to createFetchFromRegistry and
createGetAuthHeaderByURI so the view command works with private
registries and corporate proxies.

* test(view): improve test coverage for view command

Add tests for:
- non-registry spec rejection (git URLs)
- no matching version error
- version range resolution (^1.0.0)
- dist-tag resolution (latest)
- nested field selection (dist.shasum)
- field selection with --json
- text output format (header, dist section, dist-tags)
- scoped package lookup (@pnpm.e2e/pkg-with-1-dep)
- deps count / deps: none in header
- object field rendering as JSON

* revert: undo rename of @pnpm/resolving.registry.types

The rename from @pnpm/resolving.registry.types to
@pnpm/registry.types (and the move from resolving/registry/types/
to registry/types/) is a separate refactoring concern unrelated to
the view command. Revert all rename-related changes.

Keep the legitimate type additions to PackageInRegistry:
maintainers, contributors, and dist.unpackedSize.

* revert: restore pnpm-workspace.yaml (remove registry/* glob)

* fix(view): handle edge cases in formatBytes and unpackedSize

- Use explicit null check for unpackedSize so 0 B is still rendered
- Add TB/PB units and clamp index to prevent undefined output

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-03-27 19:01:10 +01:00
Khải
d4a1d734b6 feat: pnpm login (#11094)
* refactor: extract web auth QR code and polling into @pnpm/network.web-auth

Extract generateQrCode() and pollForWebAuthToken() from releasing/commands
into a new shared package so that both `pnpm publish` and the upcoming
`pnpm login` can reuse the web-based authentication flow with QR code
display and doneUrl polling.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* feat: implement `pnpm login` command

Add `pnpm login` (and `pnpm adduser` alias) for authenticating with npm
registries. The command:

- Tries web-based login first (POST /-/v1/login), displaying a QR code
  and polling for the token using @pnpm/network.web-auth
- Falls back to classic username/password/email login (PUT /-/user/
  org.couchdb.user:<username>) when web login is not supported (404/405)
- Saves the received auth token to the user's global rc file

Also fixes a tsgo build issue in releasing/commands where
OtpWebAuthFetchOptions was used as a local type alias but was only
available as a re-exported name.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* fix: resolve spellcheck issues in login test

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* fix: correct alphabetical ordering for meta-updater

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* chore: add meta-updater generated tsconfig files

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* fix: add explicit return type to prompt mock for tsgo compatibility

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* fix: use @pnpm/network.fetch instead of globalThis.fetch

Switch from globalThis.fetch to fetchWithAgent from @pnpm/network.fetch
so that pnpm login respects proxy settings (httpProxy/httpsProxy/noProxy),
custom SSL certificates (ca/cert/key), strictSsl, and retry configuration.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor: improve login fetch types and use URL constructor

- Type LoginContext.fetch using WebAuthFetchOptions/WebAuthFetchResponse
  from @pnpm/network.web-auth, extended with text() and wider method
- Replace regex-based URL construction with new URL() constructor
- Remove redundant LoginFetchInit type

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor: match publish pattern for dependency injection

- Static DEFAULT_CONTEXT constant instead of createDefaultContext factory
- context = DEFAULT_CONTEXT default parameter instead of context?: Partial
- Destructure context in function signatures for natural calling
- Use plain fetch from @pnpm/network.fetch (like SHARED_CONTEXT in publish)
- Context contains only side-effect functions and modules, not config

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor: use typeof fetch instead of custom fetch types

Remove LoginFetchOptions and LoginFetchResponse. Type LoginContext.fetch
as typeof fetch from @pnpm/network.fetch directly, eliminating all casts.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* fix: remove placeholder username from login success message

Web login doesn't return a username, so just report the registry.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor: use tempDir from @pnpm/prepare instead of manual tmp dirs

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* chore: update tsconfig references for @pnpm/prepare

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor: inject readSettings/writeSettings for fully pure tests

Add readSettings and writeSettings to LoginContext so tests need no
filesystem side effects. Remove @pnpm/prepare devDependency.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor: remove DEFAULT_CONTEXT from tests, use pure test context

Tests now construct their own TEST_CONTEXT with all no-op mocks,
eliminating any reliance on real side-effectful functions.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* test: use distinct opts per test, assert URLs and config paths

Each test now uses a different registry and configDir to verify URL
construction, config key generation, and save path are correct for
non-default options.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* test: throw on unexpected mock calls instead of silent fallbacks

All mock functions in TEST_CONTEXT now throw on unexpected calls,
ensuring tests fail loudly if the code makes unanticipated side effects.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* test: use IANA-reserved example.com domains in test URLs

Replace custom.registry.io and private.reg.co with example.com and
example.org (RFC 2606 reserved) to prevent domain squatting risks.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* test: use deterministic Date mock instead of native Date

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* test: assert globalInfo calls, throw on unexpected ones

Default globalInfo in TEST_CONTEXT now throws. Each test overrides it
to capture messages and asserts the expected output.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* fix: use inferred type for fetch url parameter in tests

Drop explicit `string` annotation so the parameter matches the
`RequestInfo` type expected by the fetch signature.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* fix: resolve type errors in login test mock fetch

Use mockResponse helper with `as any` cast to satisfy the Response
type, and String(url) for RequestInfo-to-string conversion.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* chore: add tsconfig.lint.tsbuildinfo to .gitignore

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor: replace typeof fetch with explicit LoginFetchResponse/LoginFetchOptions types

Derive the fetch signature from actual call-site usage instead of
coupling to the concrete @pnpm/network.fetch type. This lets test
mocks return plain objects without casts.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* chore: gitignore generated pn/pnpx/pnx artifacts

These files are created by setup.js during preinstall and should not
be tracked.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor: remove unnecessary backwards-compat aliases from otp.ts

Remove Otp-prefixed re-exports (OtpWebAuthFetchOptions,
OtpWebAuthFetchResponse, OtpWebAuthTimeoutError) that only existed as
backwards-compatibility shims. Update the test to import directly from
@pnpm/network.web-auth. Restore the named OtpDate interface that was
unnecessarily inlined.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* test(web-auth): add comprehensive unit tests for @pnpm/network.web-auth

Add dependency-injected unit tests covering:
- WebAuthTimeoutError: properties, code, hint, message
- generateQrCode: basic output and input differentiation
- pollForWebAuthToken: happy path, fetch argument passing,
  Retry-After handling (valid, non-finite, null, sub-interval,
  capped to remaining timeout, timeout during retry wait),
  error recovery (fetch throws, non-ok response, json parse error,
  missing token, empty token, multiple consecutive errors),
  custom timeout, poll interval timing

All tests use fake Date.now() and setTimeout — no real timers or
side effects.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* fix(web-auth): fix TS2339 compile errors in test assertions

Replace `.catch((e: WebAuthTimeoutError) => e)` pattern with
`rejects.toMatchObject()` to avoid `string | WebAuthTimeoutError`
union type issue when accessing `.timeout` property.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* feat(web-auth,login): extract shared OTP handling and add OTP support to login

- Create `withOtpHandling<T>()` in `@pnpm/network.web-auth` that wraps
  any operation with EOTP challenge detection, web auth flow, and
  classic OTP prompting.
- Refactor `publishWithOtpHandling` to delegate to the shared function.
- Add OTP handling to `pnpm login`'s classic (CouchDB) login flow:
  detects 401 + `www-authenticate: otp` header and retries with the
  OTP code (or web auth token) in the `npm-otp` header.
- Remove overly strict `this: this` constraints from WebAuthFetchResponse
  interfaces to improve cross-package type compatibility.
- Add 13 unit tests for `withOtpHandling` (classic + webauth flows).
- Add 4 login OTP tests (classic OTP, webauth OTP, non-401, non-otp 401).

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* fix(login): use word-boundary regex for URL assertion in test

Replace `m.includes(url)` with a regex that checks the URL is
bounded by whitespace or string boundaries, addressing the CodeQL
"incomplete URL substring sanitization" finding.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor(login): use toContainEqual + stringMatching for URL assertion

Replace manual `.some()` with Jest's `toContainEqual(expect.stringMatching(...))`
for better error messages on failure.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor(web-auth): use expect.any(String) instead of typeof check

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor(web-auth): consolidate multi-property assertions

Use toMatchObject and toEqual instead of separate per-property expects.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* docs: explain why npm-auth-type header is sent unconditionally

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor: remove unused re-exports and add missing test coverage

Remove dead re-exports of OtpHandlingPromptOptions and
OtpHandlingPromptResponse from releasing/commands/src/publish/otp.ts.

Add tests for:
- LOGIN_MISSING_CREDENTIALS (empty username in classic login)
- LOGIN_NO_TOKEN (registry returns success without token)
- LOGIN_INVALID_RESPONSE (web login returns incomplete response)
- isWebLoginNotSupported with 405 status code

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor(login): rename readSettings/writeSettings to safeReadIniFile/writeIniFile

Use the actual function names in the LoginContext interface instead of
abstract names, matching the implementations they wrap.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor(otp): remove unnecessary re-exports from otp.ts

OtpNonInteractiveError, OtpSecondChallengeError, and OtpHandlingEnquirer
were re-exported only for the test file, which can import them directly
from @pnpm/network.web-auth.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor(otp): remove unused SHARED_CONTEXT re-export

All consumers already import SHARED_CONTEXT directly from
./utils/shared-context.js, making this re-export dead code.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor(login): extract LoginDate and LoginEnquirer interfaces

Extract named interfaces for the Date and enquirer members of
LoginContext instead of inlining their types.

https://claude.ai/code/session_01YHYqGAAmZ1a9XMWoV7nG4S

* refactor: stop renaming

Claude Code Web didn't rename them thoroughly, so I had to do it myself

* docs: correct the lines

Why did Claude Code Web misaligned?

* refactor: strictly type `LoginFetchOptions.headers`

* docs: remove redundant comments

* refactor: inline `npm-otp`

* refactor: inline `headers`

* feat: add `WebLoginError.responseText`

* refactor: rename `statusCode` into `httpStatus`

* refactor(login): extract ClassicLoginError subclass from PnpmError

Extract the LOGIN_FAILED error into a dedicated ClassicLoginError class
with httpStatus and responseText properties, matching the WebLoginError
pattern.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: remove unnecessary import

* docs(changeset): correct a changeset

* docs(changeset): re-add `releasing.commands`

* refactor(web-auth): split monolithic test file into per-module files

Split index.test.ts into four files matching the source structure:
- WebAuthTimeoutError.test.ts
- generateQrCode.test.ts
- pollForWebAuthToken.test.ts
- withOtpHandling.test.ts

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: remove unnecessary `as const`

* refactor: remove unnecessary `as const`

* chore: undo Claude's BS

* refactor: extract `LoginEnquirerOptions`

* refactor: move types closer to their usesites

* refactor: remove simple type alias

* fix: type errors

* refactor(login): inject readIniFile instead of safeReadIniFile in context

The context object should only contain external dependencies. safeReadIniFile
is a local wrapper, not an external dependency, so inject readIniFile (from
read-ini-file) instead and pass it to safeReadIniFile as a parameter.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* test(login): add coverage for safeReadIniFile ENOENT handling

Test that login succeeds with empty settings when the config file does
not exist (ENOENT), and that non-ENOENT errors (e.g. EACCES) are
properly propagated.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: fix ugliness

* refactor: just pass context object

* refactor: destructure `context`

* refactor: pass the `context` object

* refactor: destructure `context`

* refactor: pass `context` object directly

* refactor: remove unnecessary parenthesis

* fix: remove unused import

* refactor: remove unnecessary parentheses from single-param arrows in tests

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: extract `LoginFetchResponseHeaders`

* fix(login): remove inline default from --registry option description

No other pnpm command includes "(default: ...)" in option descriptions.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor(tests): enforce realistic mock response behavior

- Add createMockResponse helpers that enforce single body consumption
  (calling text() or json() twice, or both, throws an error)
- Default headers.get to throwing on unexpected calls, forcing tests
  to explicitly provide headers when the code under test reads them
- Replace all inline response objects with createMockResponse calls

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* fix: formatting

* refactor: reuse

* docs: clarify what the error is actually about

* docs: consistent error message

* refactor: use consistent error message convention in test mocks

Capitalize and use "Unexpected call to <thing>" pattern instead of
AI-generated "unexpected X call" messages.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: expand inline process mock objects to multi-line

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor(login): extract PnpmError subclasses and use stricter test assertions

Extract LoginNonInteractiveError, LoginInvalidResponseError,
LoginMissingCredentialsError, and LoginNoTokenError subclasses instead
of throwing PnpmError directly.

Update test assertions to use the const promise pattern with
toHaveProperty checks on both code and message, matching the
convention used elsewhere in the codebase.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: undo ai's nonsensical deletion

* refactor: simplify

* refactor: rename OtpHandling* types to Otp* for brevity

OtpHandlingContext → OtpContext
OtpHandlingEnquirer → OtpEnquirer
OtpHandlingPromptOptions → OtpPromptOptions
OtpHandlingPromptResponse → OtpPromptResponse

The OtpHandling prefix was named after the function (withOtpHandling)
rather than the domain concept.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: extract `OtpDate`

* refactor: reuse

* fix: eslint

* refactor: add OtpRequiredError with body validation and globalWarn

- Add OtpRequiredError class with static fromUnknown() that validates
  the EOTP error body shape and returns either a validated error or an
  OtpBodyWarning when fields have unexpected types
- Add globalWarn to OtpContext so withOtpHandling can warn on bad body
  shapes instead of silently dropping them
- Update throwIfOtpRequired in login.ts to pass raw body through so
  validation happens in withOtpHandling via fromUnknown
- Add tests for bad body shapes (wrong types for authUrl/doneUrl)
- Add tests for OtpRequiredError.fromUnknown
- Propagate globalWarn through LoginContext, DEFAULT_CONTEXT,
  SHARED_CONTEXT, and all test mocks

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* docs: remove misleading comment from throwIfOtpRequired

The comment referenced downstream machinery (OtpRequiredError.fromUnknown)
that the reader shouldn't need to know about at this call site.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: replace Object.assign hack with OtpRequiredError in throwIfOtpRequired

throwIfOtpRequired now validates the raw response body via
OtpRequiredError.fromUnknown and throws a proper OtpRequiredError
instead of monkey-patching properties onto a plain Error.

withOtpHandling skips re-validation when the caught error is already
an OtpRequiredError instance.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* chore(git): revert an imperfect fix

This reverts commit f91efc1d9e.

* chore(git): revert would-be irrelevant change

This reverts commit 646c09cc66.

* chore(git): revert an imperfect fix

This reverts commit 45ff1ca601.

* refactor: replace Object.assign hack with ArtificialOtpError

Add ArtificialOtpError class that implements OtpError and validates
unknown body shapes via fromUnknownBody static method, warning on
unexpected types instead of silently dropping them.

Add globalWarn to OtpContext and propagate through LoginContext,
DEFAULT_CONTEXT, SHARED_CONTEXT, and all test mocks.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: rename ArtificialOtpError to SyntheticOtpError

"Synthetic" better conveys that the error is programmatically
constructed from raw data, not that it's fake.

Also fix grammatical error in JSDoc ("meant to thrown" → "meant to be thrown").

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* fix: eslint

Claude Code Web got it wrong this time

(or maybe because it inherited from my sketch diff? I'm not sure)

* fix: eslint

Ah! I got it. Claude Code Web was at fault here: It renamed "artificial"
to "synthetic" without re-ordering

Dumb AI!

* fix: formatting

Once again caused by Claude Code.

Anyway,

The exact equivalent refactor should have been `void warnings.push(msg)`,
if you really want to be pedantic, that is.

TypeScript, however, allows a `void` function to return any type. Reason
being that they shall all be discarded anyway.

* refactor: remove unnecessary re-assignment

* test: remove unnecessary assertion

* refactor: make default globalInfo and globalWarn mocks throw on unexpected calls

Replace no-op defaults with throwing mocks in createOtpMockContext
and createMockContext. Tests that expect these to be called now
explicitly override them.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: use toEqual with stringContaining for array assertions

Replace toHaveLength + indexed toContain pairs with single
toEqual([expect.stringContaining(...)]) assertions.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: replace globalInfo no-ops with jest.fn() and add assertions

For error tests: remove globalInfo override entirely, letting the
default throwing mock catch unexpected calls.

For success tests: use jest.fn() and assert globalInfo was called
with the expected arguments.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: replace manual array collectors with jest.fn()

Replace infoMessages/warnings arrays and push callbacks with
jest.fn() and assertions on .mock.calls. This is more idiomatic
and eliminates the boilerplate array + push pattern.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: replace remaining globalInfo no-ops with jest.fn() in otp.test.ts

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* fix(test): throw on unexpected second call instead of returning 'never'

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* fix(test): add missing globalInfo assertion in classic OTP test

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* fix(test): add missing globalInfo assertion in otp webauth polling test

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* fix(test): add @jest/globals import for jest.fn()

jest is not a global in ESM mode (--experimental-vm-modules).
Add import { jest } from '@jest/globals' to all test files using
jest.fn(), and add @jest/globals devDependency to network/web-auth.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* chore(deps): update lockfile

* fix: eslint

* fix(test): add globalInfo mock to EACCES readIniFile test

The test triggers web login (which calls globalInfo with the QR code)
before reaching readIniFile. Without a globalInfo override, the
default throwing mock causes the test to fail at the wrong point.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* fix(test): add missing globalInfo assertion in EACCES readIniFile test

Extract inline jest.fn() to const and assert it was called with
the web login QR code URL.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: convert functions with 3+ args to params objects

Per the style guide: "Functions should have no more than two or three
arguments. If a function needs more parameters, use a single options
object instead."

- withOtpHandling(operation, context, fetchOptions) → withOtpHandling({ operation, context, fetchOptions })
- pollForWebAuthToken(doneUrl, context, fetchOptions, timeoutMs) → pollForWebAuthToken({ doneUrl, context, fetchOptions, timeoutMs })
- webLogin(registry, fetchOptions, context) → webLogin({ registry, fetchOptions, context })
- classicLogin(registry, context, fetchOptions) → classicLogin({ registry, context, fetchOptions })

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: sort params object properties alphabetically

Sort interface properties, function signature destructuring, and
call site arguments in alphabetical order to match the convention
used by publishWithOtpHandling.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* refactor: adopt otp.test.ts patterns in login and web-auth tests

- Build context and opts as separate variables, then call login/
  withOtpHandling/pollForWebAuthToken on a clean line
- Add createMockContext to login.test.ts
- Convert createMockContext to arrow functions (single return
  expression), keep createMockResponse as function declaration
  (has local state)

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* fix: eslint

* refactor: inline the one-off function

* fix(login): avoid sending 'npm-otp: undefined' header on initial request

When otp is undefined (first attempt before OTP challenge), the header
'npm-otp': undefined could be coerced to the string "undefined" by
some HTTP implementations. Use conditional spread instead.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* docs(login): explain why npm-otp header is conditionally spread

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* docs(otp): explain why otp: undefined is safe in publishOptions spread

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* fix(test): use path.join in assertions for Windows compatibility

path.join produces backslashes on Windows, so hardcoded forward-slash
paths in assertions fail on Windows CI.

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

* fix: import order — standard library before external deps

https://claude.ai/code/session_0191GhgPWiD5TroLMoXAmkaZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-27 12:00:22 +01:00
Zoltan Kochan
130a44d682 docs: fix changeset 2026-03-26 16:02:38 +01:00
Burra Karthikeya
b1ad9c7d83 feat(auth): prepend 'Bearer' to auth token generated by tokenHelper (#11097)
* fix(auth-header): decode _password from base64 for default registry auth

* fix(auth): prepend 'Bearer ' to auth token generated by tokenHelper

* test: skip flaky parallel dlx test on Node 25

* fix(auth): improve tokenHelper Bearer prefix with validation and generic scheme detection

- Throw an error when the token helper returns an empty token instead of
  producing an invalid "Bearer " header
- Use a generic auth scheme regex instead of hardcoding only Bearer/Basic,
  so other schemes (Token, Negotiate, etc.) are preserved as-is
- Add tests for raw token prefixing, existing scheme preservation, and
  empty token error

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-03-26 15:33:02 +01:00
Vamsik
659e0ea0cc fix(lockfile): handle non-semver versions in lockfile merger without crashing (#11102)
* fix(lockfile): handle non-semver versions in lockfile merger without crashing

* Apply suggestion from @Copilot

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

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-26 15:21:23 +01:00
Zoltan Kochan
0e8042e6dc revert: "feat: add allowBuildsOfTrustedDeps setting (true by default) (#11078)"
This reverts commit 5a3dc4ab2f.
2026-03-26 15:19:24 +01:00
Victor Sumner
f40177fd09 fix(link-bins): skip relinking bins that already point at the correct target (#11069)
## Summary

`linkBin()` unconditionally calls `cmdShim()` / `symlinkDir()` even when the target bin already points at the correct path. This causes redundant I/O on repeated installs and `EACCES` failures when the bin directory lives on a read-only filesystem (Docker layer caching, CI prewarm, NFS mounts).

This PR adds a check at the top of `linkBin()` that verifies the existing bin before skipping:

- **Symlinks**: `readlink` target is compared against `cmd.path`
- **Cmd-shim files**: checked via `isShimPointingAt()` from `@zkochan/cmd-shim` v9, which embeds a `# cmd-shim-target=<path>` marker in every generated sh shim
- Files larger than 4KB (binaries) are never skipped — they are not cmd-shims

Stale or incorrect bins (wrong target, missing marker, different provider) are always rewritten.

Follows up on feedback from #11020.

## Changes

- `bins/linker/src/index.ts` — add target verification check in `linkBin()`
- `bins/linker/test/index.ts` — tests for skip and rewrite behavior
- `pnpm-workspace.yaml` — upgrade `@zkochan/cmd-shim` to v9

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-03-26 13:44:17 +00:00
Burra Karthikeya
fb8962f3a5 fix(auth-header): decode _password from base64 for default registry auth (#11089)
* fix(auth-header): decode _password from base64 for default registry auth

* refactor: extract basicAuth helper to deduplicate password decoding

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-03-26 01:10:22 +00:00
Victor Sumner
ee9fe5853e perf(importer): skip staging directory and write package.json as completion marker (#11088)
## Problem

The indexed package importer always creates a staging temp directory, imports files there, then renames to the final location. For cold installs where the target doesn't exist (the common case), the staging + rename is unnecessary overhead.

## Solution

- **Fast path**: callers already verify the target package is missing before calling `importIndexedDir`, so we can write directly into the final directory and skip the temp dir + rename. Falls back to the atomic staging path on EEXIST (concurrent import race) or when `keepModulesDir` is set (hoisted linker needs to merge existing `node_modules`).

- **Completion marker**: `package.json` is written last by `tryImportIndexedDir`, so `pkgExistsAtTargetDir()` (which checks for `package.json`) won't consider a partially-imported directory as complete after a crash.

- **Atomic copy**: the copy import path (non-COW filesystems) uses a temp file + `renameOverwriteSync` for the `package.json` write, since `copyFileSync` is not atomic. Hard links and reflinks are inherently atomic. This is expressed via the `Importer` interface (`importFile` + `importFileAtomic`), passed as the first argument to `importIndexedDir`.

- **Synthetic package.json**: packages that lack a `package.json` (e.g. injected Bit workspace packages) now get a synthetic empty `{}` added to the store, so the completion marker works universally.

- **DRY**: extracted `retryWithSanitizedFilenames()` to deduplicate the ENOENT handler used by both the fast path and staging path.
2026-03-25 23:16:08 +01:00
Zoltan Kochan
5a3dc4ab2f feat: add allowBuildsOfTrustedDeps setting (true by default) (#11078)
* feat: load default trusted deps list from @pnpm/plugin-trusted-deps

Add a new `use-default-trusted-deps` setting (default: true) that
automatically loads a curated list of known-good packages into
`allowBuilds` from @pnpm/plugin-trusted-deps. User-configured
allowBuilds entries take precedence over the defaults. Set
`use-default-trusted-deps=false` to disable.

* fix: use catalog reference for @pnpm/plugin-trusted-deps

* fix: use default import for @pnpm/plugin-trusted-deps CJS compat

The package uses Object.defineProperty for DEFAULT_ALLOW_BUILDS,
which Node.js/Jest ESM interop can't detect as a named export.
Switch to a default import to fix test failures.

* fix: use named ESM import from @pnpm/plugin-trusted-deps@0.3.0-1

The package now ships an ESM entry point with proper named exports,
so we can use a clean named import instead of the default import
workaround.

* fix: update @pnpm/plugin-trusted-deps to 0.3.0-2

Uses static JSON import attributes in ESM entry, fixing the bundle
issue where createRequire resolved paths relative to the bundle
output instead of the original package.

* refactor: rename setting to allow-builds-for-trusted-deps

* test: disable default trusted deps in approveBuilds tests

The tests assert exact allowBuilds contents, so the default trusted
list must be disabled to avoid polluting the expected values.

* fix: don't persist default trusted deps list to pnpm-workspace.yaml

Track the user's original allowBuilds separately as userAllowBuilds
before merging the default trusted list. Use userAllowBuilds when
writing back to pnpm-workspace.yaml to avoid persisting the ~370
default entries from @pnpm/plugin-trusted-deps.

* refactor: rename setting to allow-builds-of-trusted-deps

* docs: use camelCase for setting name in changeset

* fix: include userAllowBuilds in install command opts types

Without this, userAllowBuilds wasn't passed through to
handleIgnoredBuilds, causing the default trusted list to be
written to pnpm-workspace.yaml during e2e tests.

* fix: set userAllowBuilds to empty object when user has no config

When the user has no allowBuilds configured, userAllowBuilds was
undefined, causing handleIgnoredBuilds to fall back to the merged
allowBuilds (with defaults). Use empty object instead so the
fallback doesn't trigger.

* fix: read allowBuilds from workspace manifest when writing back

Instead of tracking userAllowBuilds separately (which gets stale
when other code writes to pnpm-workspace.yaml mid-install), read
the current allowBuilds directly from pnpm-workspace.yaml before
writing. This avoids persisting the default trusted list and
preserves entries written by --allow-build earlier in the flow.

Also update e2e test expectation: esbuild is now in the default
trusted list, so it builds instead of being ignored.

* chore: update tsconfig references for new dependencies

* test: disable default trusted deps in approveBuilds e2e install

The execPnpmInstall helper runs the bundled CLI which picks up
the default allowBuildsOfTrustedDeps=true. This causes extra
placeholder entries in pnpm-workspace.yaml that break assertions.

* fix: revert approveBuilds to use config-based allowBuilds

approveBuilds.handler should use opts.allowBuilds from getConfig()
(which excludes trusted deps defaults when disabled) rather than
reading the workspace manifest. The handler's job is to write
approve/deny decisions, not merge with auto-populated placeholders.

* test: add config reader tests for allowBuildsOfTrustedDeps

Cover: (1) default enabled with trusted defaults merged,
(2) user allowBuilds overrides defaults, (3) setting
allow-builds-of-trusted-deps=false disables the merge.
2026-03-25 16:42:36 +01:00
Victor Sumner
878a7734a0 perf(cafs): skip rename on cold CAS writes, use atomic rename for recovery (#11087)
## Problem

Every file extracted to the CAS goes through a temp-file-plus-rename cycle: `writeFile(temp, buffer)` then `renameOverwriteSync(temp, fileDest)`. For a typical cold install with ~30k files, this adds ~30k extra rename syscalls.

## Solution

Use `writeFileExclusive()` with `{ flag: 'wx' }` (O_CREAT|O_EXCL) to write directly to the final CAS path when the file doesn't exist — skipping the temp+rename overhead. For recovery paths (corrupt/partial files, EEXIST races), fall back to the existing atomic temp+rename via `optimisticRenameOverwrite`.

### Write paths

- **File doesn't exist (common cold-install path)** → `writeFileExclusive` writes directly, no rename
- **File exists with correct integrity** → return immediately, no write
- **File exists with wrong integrity (corruption/crash)** → atomic temp+rename recovery
- **EEXIST (concurrent write)** → verify integrity; if OK return, otherwise atomic temp+rename recovery

### Concurrent safety

- `writeFileExclusive` (`O_CREAT|O_EXCL`) ensures only one process creates a given CAS file
- Recovery overwrites use the battle-tested `optimisticRenameOverwrite` + `pathTemp` for atomic replacement
- `verifyFileIntegrity` is non-destructive (no `unlinkSync` on mismatch), safe when another process may be mid-write
- A crash mid-`writeFileExclusive` can leave a partial file, recovered on next access via atomic temp+rename

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-03-25 15:20:34 +01:00
Victor Sumner
6656baaea3 fix(cafs): update locker cache when file exists with correct integrity (#11085)
* fix(cafs): update locker cache when file exists with correct integrity

The CAS locker cache was not updated when a file already existed on disk
with correct integrity. This caused repeated verifyFileIntegrity calls
on subsequent lookups within the same process, adding unnecessary I/O.

* fix(test): assert locker cache value not just key existence

Strengthen the test to verify locker.get() returns the correct
checkedAt timestamp, not just that the key exists.
2026-03-25 02:12:00 +01:00
Victor Sumner
f8e6774273 perf(cafs): optimize hot path string operations (#11086)
* perf(cafs): optimize hot path string operations

Replace path.join with string concatenation in contentPathFromHex and
getFilePathByModeInCafs. These functions are called ~30k times per
install and the simpler string operations avoid path.join's argument
validation overhead.

Increase gunzipSync chunk size from default 16KB to 128KB for faster
tarball decompression with fewer zlib iterations.

* refactor: remove dead Buffer.isBuffer check in tarball path

tarballBuffer is typed as Buffer, so the isBuffer/Buffer.from
fallback was unreachable dead code.

* docs: add comments explaining path.join bypass and chunkSize choice

Address review feedback:
- Explain why string concat is used instead of path.join in CAS hot path
- Document why 128KB chunkSize was chosen (microbenchmarks, diminishing
  returns at larger sizes, bounded memory cost)

* fix: cspell — use 'Benchmarks' instead of 'Microbenchmarks'

* fix(cafs): restore Buffer.isBuffer check for worker thread compatibility

The structured clone algorithm converts Buffer to Uint8Array when sent
via postMessage to worker threads. parseTarball relies on
Buffer.prototype.toString('utf8', ...) which doesn't exist on
Uint8Array — Uint8Array.toString() returns comma-separated decimal
values, causing parseOctal to misparse tar headers.
2026-03-25 02:08:19 +01:00
btea
a1807b11d3 fix(workspace): treat catalog refs in workspace overrides as used during cleanupUnusedCatalogs (#11075)
* fix(workspace): treat catalog refs in workspace overrides as used during cleanupUnusedCatalogs

* fix: update

* fix: update
2026-03-24 16:43:13 +01:00
Zoltan Kochan
606f53e78f feat: add dedupePeers option to reduce peer dependency duplication (#11071)
* feat: add `dedupePeers` option to reduce peer dependency duplication

When enabled, this option applies two optimizations to peer dependency resolution:

1. Version-only peer suffixes: Uses name@version instead of full dep paths
   (including nested peer suffixes) when building peer identity hashes.
   This eliminates deeply nested suffixes like (foo@1.0.0(bar@2.0.0)).

2. Transitive peer pruning: Only directly declared peer dependencies are
   included in a package's suffix. Transitive peers from children are not
   propagated upward, preventing combinatorial explosion while maintaining
   correct node_modules layout.

The option is scoped per-project: each workspace project defines a peer
resolution environment, and all packages within that project's tree share
that environment. Projects with different peer versions correctly produce
different instances.

Closes #11070

* fix: pass dedupePeers to getOutdatedLockfileSetting and use spread for lockfile write

The frozen install path (used by approve-builds) calls getOutdatedLockfileSetting
but was missing the dedupePeers parameter. This caused a false LOCKFILE_CONFIG_MISMATCH
error because the lockfile had the key written (as undefined/null via YAML serialization)
while the check function received undefined for the config value.

Fix: pass dedupePeers to the settings check call, and use spread syntax to only write
the dedupePeers key to lockfile settings when it's truthy (avoiding undefined keys).

* fix: write dedupePeers to lockfile like other settings

Write the value directly instead of spread syntax, and use the same
!= null guard pattern as autoInstallPeers in the settings checker.

* test: add integration test for dedupePeers in peerDependencies.ts

* fix: only write dedupePeers to lockfile when enabled

When dedupePeers is false (default), don't write it to lockfile settings.
This avoids adding a new key to every lockfile.

* test: simplify dedupePeers test assertions

* test: check exact snapshot keys in dedupePeers integration test

* test: add workspace test for dedupePeers with different peer versions

* fix: keep transitive peers in suffix with version-only IDs

Instead of pruning transitive peers entirely (which prevented per-project
differentiation), keep them but use version-only identifiers. This way:

- Packages like abc-grand-parent still get a peer suffix when different
  projects provide different peer versions (correct per-project isolation)
- But the suffixes use name@version instead of full dep paths, eliminating
  the nested parentheses that cause combinatorial explosion

* refactor: extract peerNodeIdToPeerId helper in resolvePeers

* refactor: simplify peerNodeIdToPeerId return

* fix: pin peer-a dist tag in dedupePeers tests for CI stability

* fix: address review comments

- Register dedupe-peers in config schema, types, and defaults so
  .npmrc/pnpm-workspace.yaml settings are parsed correctly
- Use Boolean() comparison in settings checker so enabling dedupePeers
  on a pre-existing lockfile triggers re-resolution
- Fix changeset text and test names: transitive peers are still
  propagated, just with version-only IDs (no nested dep paths)
2026-03-24 13:51:17 +01:00
Victor Sumner
615bd240eb perf: skip redundant GVS internal linking on warm reinstall (#11073)
* perf: skip redundant GVS internal linking on warm reinstall

When GVS is enabled and the store is warm (added === 0), skip
re-creating internal symlinks, re-linking bins inside the GVS store,
and re-importing packages since they already persist outside
node_modules/. Also filter directPkgDirs by hasBin to avoid
unnecessary package.json reads when linking direct dep bins.

* fix: preserve link: deps in hasBin filter for bin linking

The hasBin filter was dropping directories not present in the dep graph
(e.g. link: dependencies), which would silently break bin linking for
linked local packages that expose binaries.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-03-24 01:22:12 +00:00
Zoltan Kochan
263a8bce95 feat: add pnpm peers check command (#11061)
Adds a `--check-peers` flag to `pnpm list` that detects unmet and
missing peer dependency issues by reading the lockfile. This allows
users to check for peer dependency problems without triggering a
full resolution, which is especially useful in CI or after pulling
a lockfile from another developer.

Closes #7087
2026-03-23 10:31:09 +01:00
Zoltan Kochan
54ffb948bd refactor: add recursiveByDefault property to CommandDefinition (#11062)
Replace the hardcoded command name list in main.ts with a declarative
recursiveByDefault property on CommandDefinition. Each command that
should run workspace-wide by default now exports this property.

Also adds recursiveByDefault to list, ll, and why commands.
2026-03-22 16:04:20 +01:00
zybo
e9318ce974 fix: use ENOENT check instead of which.sync for command-not-found on Windows (#11004)
* fix: use ENOENT check instead of which.sync for command-not-found on Windows

On Windows, `which.sync()` only checks if a command exists in PATH,
not whether it actually executed successfully. This caused false
"Command not found" errors when a command exists but exits with a
non-zero code. Use the same `spawn ENOENT` check across all platforms,
which is reliable thanks to cross-spawn used by execa.

Closes #11000

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

* fix: resolve prependPaths against exec prefix for correct Windows command lookup

The previous ENOENT-only approach doesn't work on Windows because execa 9.x
uses cross-spawn only for command parsing, not spawning. This means cross-spawn's
ENOENT hook (hookChildProcess) never fires, and non-existent commands wrapped as
`cmd.exe /c <command>` exit with code 1 instead of emitting ENOENT.

Restore the which.sync fallback for Windows, but fix the original #11000 bug by
resolving relative prependPaths (like ./node_modules/.bin) against the exec prefix
instead of relying on process.cwd(). This ensures correct path resolution in
--filter contexts where the command runs in a different package directory.

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

---------

Co-authored-by: zubeyralmaho <zubeyralmaho@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 13:28:54 +01:00
Alessio Attilio
d5be835735 feat: implement native recursive version command (#10879)
* feat: implement non-interactive version command

* fix: address review issues in version command

- Fix changeset package name to @pnpm/releasing.commands
- Use writeProjectManifest instead of writeJsonFile to preserve formatting
- Remove dead updateWorkspaceDependencies placeholder function
- Remove unused imports (path, ProjectManifest, writeJsonFile)
- Add expect.assertions(1) to prevent silent test pass on no-throw

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 11:51:54 +00:00
Sumit Kumar
449dacf02e fix(link-bins): apply bin ownership overrides in conflict resolution (#10975)
BIN_OWNER_OVERRIDES was only used in checkGlobalBinConflicts for global
installs. This change applies the same ownership rules in
compareCommandsInConflict so that conflict resolution is consistent
between global conflict checking and actual bin linking.

This ensures packages like npm get priority for bins like npx even in
non-global installs.

Closes #10850

* test(link-bins): add missing fixture for bin-owner-override test

* refactor: extract BIN_OWNER_OVERRIDES to @pnpm/package-bins

Move shared logic to avoid code duplication between link-bins
and checkGlobalBinConflicts.

* fix(link-bins): use regex for Windows path compatibility in test

* refactor(link-bins): remove redundant ownName field

pkgOwnsBin already handles the binName === pkgName case, making
the ownName field and its associated checks redundant.

* Change versioning to patch for bins resolver and linker

Added BIN_OWNER_OVERRIDES and pkgOwnsBin to @pnpm/bins.resolver for improved conflict resolution in bin linking.

* test: remove node_modules from bin-owner-override fixture

Move fixture packages to the directory root instead of nesting them
inside node_modules, avoiding committing node_modules to the repo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 11:50:56 +00:00
Brandon Cheng
6557dc09f9 fix: clearCache function in @pnpm/resolving.npm-resolver (#11050)
* test: add test for `clearCache` function in `@pnpm/resolving.npm-resolver`

* fix: clear pMemoize when clearing NPM resolver `clearCache` function
2026-03-22 01:48:25 +01:00
Brandon Cheng
f98a2db373 fix: invalid specifiers for peers on all non-exact version selectors (#11049)
* test: add test for hoist peers when given all range version selectors

* fix: invalid specifiers for peers on non-string version selectors

In tests, the bare specifier for the `@pnpm.e2e/peer-a` dependency
became ` || 1.0.0`. This was because the `versions` array could be
empty, causing the `.join(' || ')` operation to execute on a holey
array.

This caused a test in `installing/commands/test/update/update.ts` to
fail.
2026-03-22 01:47:12 +01:00
Brandon Cheng
831f574330 fix: propagate error cause when throwing PnpmError in @pnpm/npm-resolver (#10990)
* fix: show error cause when failing to read metadata

* fix: correct changeset package name and add cause assertion tests

- Fix changeset to reference @pnpm/resolving.npm-resolver (not @pnpm/npm-resolver)
- Add PnpmError cause unit tests in @pnpm/error
- Fix npm-resolver tests to actually verify cause on thrown errors
  (.toThrow() only checks message, not cause/hint/code properties)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:59:52 +01:00
Brandon Cheng
41dc031a67 test: use resolution-mode=highest in tests (#10989)
* fix: configure default resolution-mode to highest in pkg-manager/core

* test: update catalog tests for resolution-mode=highest

* test: fix `--fix-lockfile` test for new resolution-mode default

```
  ● fix broken lockfile with --fix-lockfile

    expect(received).toBeTruthy()

    Received: undefined

      55 |   const lockfile: LockfileFile = readYamlFileSync(WANTED_LOCKFILE)
      56 |   expect(Object.keys(lockfile.packages as PackageSnapshots)).toHaveLength(2)
    > 57 |   expect(lockfile.packages?.['@types/semver@5.3.31']).toBeTruthy()
         |                                                       ^
      58 |   expect(lockfile.packages?.['@types/semver@5.3.31']?.resolution).toEqual({
      59 |     integrity: 'sha512-WBv5F9HrWTyG800cB9M3veCVkFahqXN7KA7c3VUCYZm/xhNzzIFiXiq+rZmj75j7GvWelN3YNrLX7FjtqBvhMw==',
      60 |   })

      at Object.<anonymous> (test/install/fixLockfile.ts:57:55)
```

* test: fix lockfile conflict test

  ● a lockfile v6 with merge conflicts is autofixed

    expect(received).toHaveProperty(path, value)

    Expected path: "version"

    Expected value: "100.1.0"
    Received value: "101.0.0"

      1284 |
      1285 |   const lockfile = project.readLockfile()
    > 1286 |   expect(lockfile.importers?.['.'].dependencies?.['@pnpm.e2e/dep-of-pkg-with-1-dep']).toHaveProperty('version', '100.1.0')
           |                                                                                       ^
      1287 | })
      1288 |
      1289 | test('a lockfile with duplicate keys is fixed', async () => {

      at Object.<anonymous> (test/lockfile.ts:1286:87)

* test: fix deploy shared lockfile test

  ● deploy with a shared lockfile that has peer dependencies suffix in workspace package dependency paths

    expect(received).toMatchObject(expected)

    - Expected  - 6
    + Received  + 1

    @@ -1,11 +1,11 @@
      Object {
        "importers": Object {
          "packages/project-0": Object {
            "dependencies": Object {
              "project-1": Object {
    -           "version": "file:packages/project-1(is-negative@1.0.0)(project-2@file:packages/project-2(is-positive@1.0.0))",
    +           "version": "file:packages/project-1(is-negative@2.1.0)(project-2@file:packages/project-2(is-positive@1.0.0))",
              },
              "project-2": Object {
                "version": "file:packages/project-2(is-positive@1.0.0)",
              },
            },
    @@ -31,13 +31,8 @@
              "type": "directory",
            },
          },
        },
        "snapshots": Object {
    -     "project-1@file:packages/project-1(is-negative@1.0.0)(project-2@file:packages/project-2(is-positive@1.0.0))": Object {
    -       "dependencies": Object {
    -         "project-2": "file:packages/project-2(is-positive@1.0.0)",
    -       },
    -     },
          "project-2@file:packages/project-2(is-positive@1.0.0)": Object {},
        },
      }

      950 |     workspaceDir: process.cwd(),
      951 |   })
    > 952 |   expect(assertProject('.').readLockfile()).toMatchObject({
          |                                             ^
      953 |     importers: {
      954 |       'packages/project-0': {
      955 |         dependencies: {

      at Object.<anonymous> (test/shared-lockfile.test.ts:952:45)

* test: fix injectLocalPackages test
2026-03-21 23:21:04 +01:00
Brandon Cheng
021f70d0b0 fix: handle non-string version selectors in hoistPeers (#11048)
* test: add test for version selector with weight in hoistPeers

* fix: handle non-string version selectors in hoistPeers
2026-03-21 23:17:24 +01:00
btea
2f98ec84f4 feat: store prune displays the total size of removed files (#11047)
* feat: store prune displays the total size of removed files

* test: update
2026-03-21 20:01:58 +01:00
Zoltan Kochan
9b801c888d fix: check allowBuild for packages with cached side-effects (#11039)
Closes #11035

## Summary

### Root cause fix: don't apply cached side-effects for unapproved packages
When importing packages from the store, side-effects cache was applied for any package not explicitly denied (`allowBuild !== false`). This meant unapproved packages (`allowBuild === undefined`) got cached build artifacts, setting `isBuilt: true` and bypassing the `allowBuild` check in `buildModules`.

**Fix:** Only apply side-effects cache when `allowBuild` returns `true` (explicitly approved). Changed in three locations:
- `installing/deps-restorer/src/index.ts` (isolated linker)
- `installing/deps-restorer/src/linkHoistedModules.ts` (hoisted linker)
- `installing/deps-installer/src/install/link.ts` (non-headless install)

### Revocation detection
When a package's build approval is revoked between installs (was `true` in `.modules.yaml`, now undefined), detect it in `mutateModules` and add to `ignoredBuilds` so `strictDepBuilds` fails.

### Status messages in `_rebuild`
Users now see what happened to each package during rebuild:
- `pkg@version: built successfully`
- `pkg@version: skipped (no build scripts)`
- `pkg@version: skipped (not allowed)`
- `pkg@version: reused from store cache`

And during install:
- `pkg@version: reused from store (side effects cache)`

### `buildSelectedPkgs` fixes
- Preserve `storeDir`, `virtualStoreDir`, `virtualStoreDirMaxLength` from existing `.modules.yaml` instead of overwriting with config-derived values (which caused "reinstall from scratch" prompt)
- Write `allowBuilds` to `.modules.yaml` so GVS doesn't detect a mismatch on next install
- Merge `ignoredBuilds` with existing entries for packages not being rebuilt
2026-03-21 12:51:24 +01:00
Zoltan Kochan
9fc552d37a fix: update GVS symlinks after approve-builds by running install (#11043)
Fixes #11042

- **Root cause**: When `enableGlobalVirtualStore` is true and `allowBuilds` is not configured, `createAllowBuildFunction()` returned `undefined`, causing all GVS hashes to include `ENGINE_NAME`. When `approve-builds` later configured `allowBuilds`, the hash didn't change because the engine was already included.
- **Fix**: Default `allowBuilds` to `{}` in GVS mode so hashes are engine-agnostic by default, and have `approve-builds` call `install.handler()` in GVS mode instead of the low-level `install()` function, so it properly handles workspaces and updates symlinks.
- **Refactor**: Broke circular dependencies between `building/commands`, `installing/commands`, and `global/commands` using dependency injection via a `commands` map passed as the third argument to command handlers. Added `CommandHandler` and `CommandHandlerMap` types to `@pnpm/cli.command`.

## Changes

### Architecture
- Command handlers now receive a `commands` map as an optional third argument `(opts, params, commands?)`
- The CLI dispatcher in `main.ts` passes the full commands map to every handler
- Handlers that need other commands (e.g., `globalAdd` needs `approve-builds`, `recursive` needs `rebuild`) access them from this map
- This replaces direct cross-package imports that would create circular dependencies

### Packages changed
- `@pnpm/cli.command` — new `CommandHandler` and `CommandHandlerMap` types
- `@pnpm/building.commands` — `approve-builds` uses `install.handler` for GVS
- `@pnpm/global.commands` — removed `building/commands` dependency; receives `approve-builds` via commands map
- `@pnpm/installing.commands` — receives `rebuild` via commands map instead of direct import
- `@pnpm/installing.deps-installer` / `@pnpm/installing.deps-restorer` — default `allowBuilds` to `{}` in GVS mode
- `pnpm` CLI — dispatcher passes commands map to all handlers
2026-03-21 12:50:46 +01:00