Commit Graph

32 Commits

Author SHA1 Message Date
Zoltan Kochan
fe66535046 fix: converge incremental install by refreshing stale transitive pins (#12472)
When a package is reused from the lockfile, its child edges are taken
verbatim and bypass the preferred-versions walk, so a transitive
dependency can stay pinned to an older version even after a direct
dependency resolved to a higher version that satisfies the same range —
leaving the lockfile non-convergent (an incremental install keeps a
duplicate that a fresh install would not).

The resolver now refreshes such a stale pin to the higher
direct-dependency version during resolution, via `preferredVersion`
(singular), which overrides the EXISTING_VERSION_SELECTOR_WEIGHT stability
bias. The older version is never resolved or fetched, and the incremental
result converges to what a fresh install produces. The pick is anchored to
direct dependencies (which resolve first), so it restores the automatic
dedupe removed in pnpm/pnpm#11110 without reintroducing its
non-determinism, and unlike the post-pass in pnpm/pnpm#11502 it does not
over-fetch.

pacquet is ported in the same change. Its full-subtree lockfile reuse is
coarser than pnpm's per-edge reuse, so it records per importer which direct
deps changed and their resolved versions, declines full-subtree reuse for a
parent that depends on a changed direct dep, and forces the higher version
in the child walk. Range satisfaction uses plain semver (not
prerelease-inclusive), matching pnpm's semver.satisfies(.., true).
2026-06-18 10:59:15 +02:00
Ben Scholzen
96bdd57bf4 fix: dedupe workspace deps with children in single-project mode (#11448)
## Issue

When `injectWorkspacePackages: true` is set and a workspace package depends on another workspace package that has its own dependencies, running `pnpm rm` from inside the dependent package's directory switches the lockfile protocol from `link:` to `file:`.

Reproduction (workspace where `a` depends on workspace `b`, and `b` has any dependency of its own):

```
cd packages/a
pnpm add redis
pnpm rm redis
# pnpm-lock.yaml: a's "b" entry switched from link:../b to file:packages/b
```

## Root Cause

The fix in #10575 added a defensive guard in `dedupeInjectedDeps` that skipped deduplication whenever the target workspace project's children weren't in `dependenciesByProjectId`:

```ts
if (!targetProjectDeps) {
  if (children.length > 0) continue
}
```

In single-project operations (`mutateModulesInSingleProject`, used by `pnpm rm` from inside a package directory) only the operated-on project is resolved. `dependenciesByProjectId` then only has that one project, so the guard fires for any workspace dependency whose target has children, and the protocol stays `file:`.

## Solution

In single-project mode the injected dep is resolved against the same workspace package source, so dedupe is safe — *except* for peer-suffixed depPaths, whose resolution depends on the importer's peer context (a plain `link:` would lose it). The new code dedupes whenever `targetProjectDeps` is missing for a known workspace project and the depPath has no peer suffix. The peer-suffix check compares the depPath against its peer-free `pkgIdWithPatchHash` (depPaths are built as `${pkgIdWithPatchHash}${peerDepGraphHash}`), so it's exact rather than a `(`-substring heuristic.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-17 13:37:09 +02:00
Abdullah Alaqeel
5c12968ad6 fix(update): handle mixed direct and transitive selectors (#12105)
* fix(update): handle mixed direct and transitive selectors

* test(update): strengthen regression test and port to pacquet

The pnpm regression test passed with and without the fix: the fixture's
`latest` dist-tag made a fresh install of `^100.0.0` already resolve to
100.1.0, so the assertion was trivially true. Pin the transitive
dep-of-pkg-with-1-dep to 100.0.0 before install so the test genuinely
fails without the fix and passes with it.

Add pacquet parity regression tests for the same mixed direct/transitive
selector scenario (exact-name and glob forms). pacquet has no equivalent
source change to make — its `update` matches every bare-name/glob
selector against direct deps and locked snapshot names in one pass, so a
direct selector never gates the transitive one — but the behavior is
guarded by tests to lock in pnpm/pnpm#12103 parity.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-17 07:29:57 +02:00
morning-verlu
531f2a307c fix: preserve workspace specs on update (#12140)
## What
- preserve existing `workspace:` dependency specifiers when `updateProjectManifest` saves updated direct dependencies and `preserveWorkspaceProtocol` is enabled
- keep catalog specifiers taking precedence over resolver-normalized specs
- add focused coverage for preserved and normalized local spec behavior
- add a changeset for the published `@pnpm/installing.deps-resolver` change

### pacquet parity
Ported the same fix to pacquet's `update` command. Previously `pacquet update --latest` routed every direct dependency through a registry `latest` lookup, so a `workspace:` local-path dependency (e.g. `workspace:../packages/foo/dist`) was rewritten into a registry version — corrupting the manifest (in the regression test it became `0.0.1-security`). Both `--latest` rewrite sites now skip registry resolution for such specs via `is_workspace_local_path_specifier`, a faithful port of pnpm's `isWorkspaceLocalPathSpecifier`. The gate is unconditional in the `--latest` path because `preserveWorkspaceProtocol` is always on there (its only override derives from `linkWorkspacePackages` under `--workspace`, which cannot be combined with `--latest`).

Fixes #3902

---------

Co-authored-by: morning-verlu <258725120+morning-verlu@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-16 23:38:05 +00:00
Abdullah Alaqeel
817f99dbe5 fix(resolver): stabilize transitivePeerDependencies in dependency cycles (#12286)
## Summary

Fixes lockfile churn where a package's `transitivePeerDependencies` (e.g. `supports-color` via `debug`) could be dropped — and shift between packages — when the package participates in a dependency cycle. Which packages carry a given transitive peer depended on resolution order, so upgrading an unrelated dependency churned the lockfile.

## Root cause

When peer resolution walks into a cycle, the cycle is broken by dropping the repeated package's subtree, so the re-entry occurrence resolves against truncated children and looks peer-free. That occurrence was then recorded as "pure" in `purePkgs` — a verdict keyed by package id, not by context. A later occurrence of the same package, reached through a different parent that *can* see the full subtree, hit the `purePkgs` short-circuit and returned an empty peer set, dropping the transitive peers it should have surfaced. Because the outcome depends on which occurrence is walked first, it was order-dependent.

## Fix

Don't record a cycle re-entry's resolution in `purePkgs` / `peersCache` (a re-entry is detected when the package id already appears in the ancestor chain). Its truncated peer sets aren't authoritative for the package as a whole, so leaving the caches untouched lets later occurrences resolve correctly — or reuse the package's authoritative, non-truncated entry via `findHit`. This is a minimal guard at the cache-population site: it adds no post-resolution pass and does not change `transitivePeerDependencies` for packages that aren't in cycles.

This PR also includes an independent fix: when collecting peer providers from a node's children, match each child's resolved package name in addition to its alias, so `pnpm add my-alias@npm:real-pkg` is visible to peer resolution when `real-pkg` is a peer dependency name.

Both the TypeScript pnpm CLI and the Rust (pacquet) port are updated in parity.

Fixes pnpm/pnpm#5108

Related `transitivePeerDependencies`-instability reports: pnpm/pnpm#5552, pnpm/pnpm#5794, and the `transitivePeerDependencies` aspect of pnpm/pnpm#9992 (the out-of-scope version drift in pnpm/pnpm#9992 is a separate problem and is not addressed here).

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-16 13:39:37 +00:00
David Barratt
3a271413d8 fix: prevent a pinned locked peer provider from leaking to sibling nodes (#12320)
* fix: prevent a pinned locked peer provider from leaking to sibling nodes

When the locked-peer-context pinning introduced in pnpm/pnpm#12083 runs for
a node that has no child dependencies, parentPkgs aliases the parent's
object, so writing the pinned provider into it exposed the provider to every
sibling resolved afterwards. Sibling order follows resolution completion
order, so optional peers of siblings resolved nondeterministically and
"pnpm dedupe --check" failed intermittently in CI.

Copy parentPkgs before pinning so the pin stays scoped to the node and its
own subtree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf: copy parentPkgs only before the first pin write

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-15 01:50:22 +02:00
Zoltan Kochan
9b35a6004e fix(deps-resolver): make shared children resolution deterministic (#12362)
## Summary

- Make shared package child resolution deterministic by choosing the owner by depth, importer order, and parent path instead of async completion timing.
- Keep non-owner and stale occurrences lazy while reusing the current owner children and missing-peer context.
- Port the same behavior to pacquet and add TypeScript plus Rust regression coverage.

## Verification

- `pnpm --filter @pnpm/installing.deps-resolver test test/resolveDependencyTree.test.ts`
- `cargo test -p pacquet-resolving-deps-resolver`
- Pre-push hook: TypeScript typecheck, pnpm bundle, lint, spellcheck, meta lint, cargo fmt, cargo doc, cargo dylint, taplo format check

Fixes pnpm/pnpm#12358
2026-06-12 21:12:58 +02:00
Zoltan Kochan
f648e9b7c4 fix: contain hoisted dependency aliases (GHSA-fr4h-3cph-29xv) (#12343)
* fix: contain hoisted dependency aliases (GHSA-fr4h-3cph-29xv)

The `nodeLinker: hoisted` install restores its dependency graph straight
from the lockfile via `lockfileToHoistedDepGraph`, which joins each
dependency alias under a `node_modules` directory and imports the
package files there. On a frozen / up-to-date lockfile, resolution is
skipped entirely, so the alias validation added for the resolution path
never runs. A crafted lockfile alias such as `../../../escape` could
therefore escape the install root, and reserved aliases such as `.bin`,
`.pnpm`, or `node_modules` could overwrite pnpm-owned layout.

Validate every alias at the hoisted-graph directory sink. The shared
`safeJoinModulesDir` helper now rejects aliases that are not valid npm
package names (path-traversal, absolute, and reserved names) in addition
to its containment check, and the hoisted graph routes its `dep.name`
sink through it. Pacquet mirrors the boundary: `safe_join_modules_dir`
validates the hoister's `dep.0.name` before adding the graph node or
recursing, reusing the same dependency-name rule it already applies to
direct-dependency aliases. Both stacks surface
`ERR_PNPM_INVALID_DEPENDENCY_NAME`.

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

* fix: reject invalid dependency aliases at the lockfile verification gate

Add an always-on, policy-independent structural check to
verifyLockfileResolutions that rejects any importer or package-snapshot
dependency alias that is not a valid npm package name. A dependency
alias becomes a `node_modules/<alias>` directory at link time, so an
alias with path-traversal segments or a reserved name (`.bin`, `.pnpm`,
`node_modules`) could escape the install root or overwrite pnpm-owned
layout.

This complements the linker-sink guards: the verifier runs before any
fetch or filesystem work and covers every node linker at once, while the
sink guards still protect the `trustLockfile` path the verifier skips.
The check runs before the cache lookup so a record written by a version
that predates the rule cannot fast-path around it, and before the
`packages` guard so a tampered importer alias is caught even when nothing
is installed.

`isValidDependencyAlias` is now exported from `@pnpm/installing.deps-resolver`
and reused here. Pacquet mirrors the gate in its lockfile-verification
crate with a matching `ERR_PNPM_INVALID_DEPENDENCY_NAME` verdict.

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

* docs(package-manager): drop redundant explicit intra-doc link target

`is_valid_dependency_alias` is in scope via `use`, so the bare
intra-doc link resolves on its own. The explicit path target tripped
`rustdoc::redundant-explicit-links` under the CI Doc job's
`cargo doc --document-private-items` (the local pre-push hook runs
`cargo doc` without that flag, so it didn't surface).

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

* refactor(lockfile-verification): fold the alias check into the single candidate pass

The dependency-alias check ran as its own full traversal of the lockfile
in addition to collectCandidates' existing pass over every package
snapshot. Fold it into that pass instead: collectCandidates now also
validates each importer and snapshot dependency alias and returns the
invalid ones alongside the resolution-shape violations, so the lockfile
is walked once per verification rather than twice.

Because collectCandidates runs after the verification-cache lookup, the
alias check is now covered by the cache the same way the resolution-shape
check is: a new dependencyAliasCheck cache identity makes a record
written before this rule existed fail canTrustPastCheck, forcing a
re-verification. The shared helper is renamed
withOfflineCheckCacheIdentities and appends both offline-structural-check
identities.

No behavior change for valid lockfiles; the same
ERR_PNPM_INVALID_DEPENDENCY_NAME is thrown for invalid aliases. Mirrored
in pacquet's lockfile-verification crate.

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

* refactor: declare pushInvalidAliases after its caller, trim duplicated comments

Move `pushInvalidAliases` below `collectCandidates`, following the
repo's declare-after-use convention. Collapse the repeated "an alias
becomes a node_modules directory, so a traversal/reserved name escapes
or overwrites layout" explanation that was copied across the verifier,
the hoisted-graph error, and the pacquet mirror down to a single
reference each — the full rationale lives once in the validating sink
(`safeJoinModulesDir` / `safe_join_modules_dir`) and the user-facing
error hints.

---
Written by an agent (Claude Code, claude-fable-5).
2026-06-12 09:46:57 +02:00
David Barratt
29a496ac7c fix: make peer-dependent deduplication deterministic (#12179)
* fix(deps-resolver): make peer-dependent deduplication deterministic

When a peer-suffixed package variant is a subset of two or more mutually
incompatible larger variants, `deduplicateDepPaths` chose which one to
collapse it into based on the order dep paths were inserted into the
per-pkgId set, which reflects importer/resolution order and varies between
platforms. The same workspace could then resolve to different lockfiles on
different machines, making `pnpm dedupe --check` alternate between pass and
fail.

The depth-count sorter `nodeDepsCount(a) - nodeDepsCount(b)` is not a total
order, so equal-count variants keep their (order-dependent) relative
position. Tie-break on the dep path string to give a deterministic winner
regardless of insertion order.

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

* test(deps-resolver): assert resolved depPath is defined before order check

The order-invariance assertion compared two undefined values, which would
pass silently if the depPath never resolved. Assert both are defined first.

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

* feat(pacquet): port dedupePeerDependents collapse

Port pnpm's `dedupePeerDependents` pass (resolvePeers tail block +
`deduplicateAll` / `deduplicateDepPaths` / `nodeDepsCount` /
`isCompatibleAndHasMoreDeps`) into pacquet, carrying the pnpm/pnpm#12179
determinism fix: the collapse target is chosen by a total order over
`(dep count, dep path)` so it no longer depends on importer/resolution
order.

Runs in `resolve_peers_workspace` after `dedupe_injected_deps`, gated on
`config.dedupe_peer_dependents` (default true) threaded through
`WorkspaceResolveOptions`. Duplicate variant groups are reconstructed by
grouping the finished graph on `resolved_package_id` instead of threading
pnpm's `depPathsByPkgId` through the walk. Since pacquet has no unified
post-resolve lockfile pruner, the pass reuses
`dedupe_injected_deps::prune_unreachable` to drop collapsed orphans so
they don't surface in the lockfile.

Both unit tests from pnpm's dedupeDepPaths.test.ts are ported (the
version-mismatch collapse and the importer-order determinism case),
plus end-to-end remap+prune and incompatible-variant coverage.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-08 12:44:55 +02:00
Sharmila
1c73e8303c fix(deps-resolver): prefer locked peer contexts during resolution by default (#12083)
## Summary

Preserve compatible peer contexts already recorded in the lockfile during a
writable re-resolution.

A fresh install still resolves peers normally. When a lockfile already records
multiple valid peer contexts, pnpm keeps those contexts instead of collapsing
them into one compatible context and rewriting unrelated lockfile entries.

## Why

[#12075](https://github.com/pnpm/pnpm/pull/12075) fixed optional-peer candidate
selection: pnpm no longer discards a compatible optional-peer version merely
because it came from the lockfile.

This PR addresses a separate source of lockfile churn. A writable install could
still replace one valid peer context with another valid peer context even when
the existing provider remained present and satisfied the peer range.

Public reproduction:
<https://github.com/sharmila-oai/pnpm-optional-peer-lockfile-repro>

The nested reproduction starts with two valid `vitest@3.2.4` contexts:

```text
context-low  -> vitest@3.2.4(jsdom@26.1.0)
context-high -> vitest@3.2.4(jsdom@27.4.0)
```

Running a writable lockfile regeneration should retain both contexts:

```sh
./reproduce-nested-context.sh
```

## Behavior

pnpm reuses a locked peer provider only when:

- The provider is still present in the current dependency graph.
- The provider still satisfies the peer range.

Current manifest choices remain authoritative. In particular, pnpm does not
replace:

- A newly added direct peer provider.
- An explicitly updated direct peer provider.
- A changed nested provider.
- A direct provider installed through an alias.

The reuse pass runs only when the dependency tree contains locked peer contexts,
so fresh installs do not pay for a second peer-resolution pass.

## Tradeoff

This change favors lockfile stability over reducing the number of peer
contexts. A writable install may retain multiple compatible peer contexts where
a fresh install would select one.

## Implementation

The resolver performs its normal peer-resolution pass first. When the
dependency tree contains locked peer contexts, it performs a second pass that
may reuse compatible provider paths from the lockfile while respecting current
manifest choices.

pacquet now mirrors this behavior. Its lockfile-reuse path rebuilds child
dependencies from the package manifest and skips peer dependencies recorded in
the snapshot, so the peer pass derives each dependency instance's peer context.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-02 22:02:57 +02:00
Sharmila
122ab0a1ed fix(deps-resolver): preserve locked optional peer candidates (#12075)
## Summary

Preserve compatible optional-peer versions already recorded in the lockfile
when pnpm re-resolves a workspace.

## Reproduction

Public reproduction:
<https://github.com/sharmila-oai/pnpm-optional-peer-lockfile-repro>

The workspace contains:

```text
packages/uses-vitest -> vitest@3.2.4
packages/older       -> jsdom@26.1.0
```

Vitest declares `jsdom` as an optional peer dependency.

The committed lockfile was generated when another workspace package also
depended on `jsdom@27.4.0`. At that time, pnpm selected the higher compatible
version for Vitest:

```text
vitest@3.2.4(jsdom@27.4.0)
```

That additional direct dependency was then removed from its `package.json`,
without regenerating the lockfile. This simulates a normal manifest edit.

Running:

```sh
pnpm install --lockfile-only --no-frozen-lockfile
```

unnecessarily rewrites Vitest's still-valid optional-peer context:

```diff
-        version: 3.2.4(jsdom@27.4.0)
+        version: 3.2.4(jsdom@26.1.0)
```

Both versions satisfy Vitest's optional peer range. The existing `27.4.0`
resolution remains valid and should not be discarded while pnpm updates the
lockfile.

## Cause

Preferred versions loaded from the wanted lockfile are stored as weighted
selectors:

```ts
{ selectorType: 'version', weight: EXISTING_VERSION_SELECTOR_WEIGHT }
```

`getHoistableOptionalPeers()` only recognized the plain string form:

```ts
specType === 'version'
```

As a result, it ignored the compatible locked `27.4.0` candidate. It only saw
`26.1.0`, which was rediscovered from `packages/older/package.json`, and
rewrote the peer context.

## Fix

Normalize the selector before checking its type, matching the handling already
used by `hoistPeers()` for required peers:

```ts
const specType = typeof selector === 'string'
  ? selector
  : selector.selectorType
```

This restores lockfile-seeded versions to the candidate set. It does not add a
new preference rule or force pnpm to keep every locked version. Optional-peer
auto-installation continues to choose the highest version satisfying every
recorded peer range.

The equivalent fix is included in pacquet, pnpm's Rust port.

## Validation

- Added matching TypeScript and Rust regression tests.
- Verified the public reproduction against `pnpm@11.4.0` and the patched CLI.
- Ran the focused TypeScript resolver checks and pacquet test, clippy, format,
  and `cargo nextest` checks.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-02 07:30:52 +02:00
Adam Lyrén
6f382f42ee fix: preserve integrity of remote tarball dependencies on re-resolution (#12096)
* fix: preserve integrity of remote tarball dependencies on re-resolution

Re-resolving a remote tarball dependency without re-fetching it (e.g. `pnpm update`)
produced a resolution with no integrity, so the previously recorded integrity was
dropped from the lockfile, breaking later installs with ERR_PNPM_MISSING_TARBALL_INTEGRITY.

Carry the integrity over from the previous lockfile entry when the rebuilt tarball
resolution lost it and the URL is unchanged. This complements #12040, which fixes the
same class of bug in the package-requester layer but does not cover this re-resolution path.

Closes #12067.

* test: cover integrity carryover on tarball re-resolution

* refactor: check integrity before type in the tarball carryover guard

* perf(pacquet): reuse the warm-store tarball on re-resolution instead of re-downloading

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-01 16:06:59 +02:00
Zoltan Kochan
1db05c6fca fix: inconsistent resolution of a peer shared through a diamond (#12081)
* fix: inconsistent resolution of a peer shared through a diamond

When a package peer-depends both another package and one of that
package's own peer dependencies (e.g. @typescript-eslint/eslint-plugin
peer-depends both @typescript-eslint/parser and typescript, and
@typescript-eslint/parser peer-depends typescript), pnpm reused a
hoisted instance of the shared peer that was resolved against a
different version, producing an inconsistent resolution.

Close #12079

* test: cover the peer-diamond resolution in pnpm and pacquet

Add @pnpm.e2e/peer-diamond-* fixtures modeling #12079 (plugin
peer-depends both parser and ts; parser peer-depends ts) and
integration tests on both stacks. The pnpm test guards the fix; the
pacquet test confirms pacquet already resolves the diamond consistently
(its merge always prefers the node's own child).

* docs: fix grammar in changeset (peer-depends on both)
2026-05-31 16:03:12 +02:00
Zoltan Kochan
39101f5e37 fix: hang on cyclic aliased peer dependency (#12018)
- `pnpm i nuxt@npm:nuxt-nightly@5x` (and similar aliased installs) hung at 0% CPU during peer resolution after `resolved N, reused 0, downloaded N, added 0`.
- `resolvePeers.calculateDepPath` only short-circuited cycles whose members included `currentAlias`. When two peers form a mutual cycle (e.g. `vite` ↔ `@vitejs/devtools`) and both hit the `findHit` cache instead of running their own `calculateDepPath`, the cycle surfaced at a level where no participant could break it — a sibling's `calculateDepPath` saw the cycle in the `cycles` argument but kept awaiting `pathsByNodeIdPromises` on cyclic peer node IDs.
- The fix expands `cyclicPeerAliases` to also include any cycle that intersects the current call's pending peers, so awaiting siblings emit the `name@version` peer id and the cached promise gets released.
- Pacquet's `resolve_peers` walks synchronously with an `in_progress` set and returns already-realized `DepPath` values from `find_hit`, so the deadlock does not occur there. A pacquet regression test locks in that the aliased-install + transitive-mutual-peer scenario terminates with the expected graph entries.

Closes #11999.
2026-05-28 13:30:49 +02:00
Zoltan Kochan
aa6149df65 fix: fail by default when a tarball does not match the locked integrity (#11968)
`pnpm install` (non-frozen) used to react to `ERR_PNPM_TARBALL_INTEGRITY` by logging the error, silently re-resolving from the registry, and overwriting the locked integrity. The lockfile's integrity was effectively advisory by default — a compromised registry, proxy, or republished version could substitute attacker-controlled content on a clean machine even though the project shipped a committed `pnpm-lock.yaml`.

Integrity mismatches against the lockfile now fail by default.

The **only** opt-in is **`pnpm install --update-checksums`** — a new flag, narrowly scoped to refreshing the locked integrity values. Mirrors yarn's flag of the same name. A warning still prints when the bypass takes effect so the rewrite stays auditable.

`--force` and `pnpm update` deliberately do **not** bypass the integrity check. They are routine refresh operations; silently overwriting a locked integrity in those flows would erase the protection a committed lockfile is supposed to provide. `--frozen-lockfile` behavior is unchanged. `--fix-lockfile` keeps its documented purpose (filling in missing lockfile entries) and is also not a bypass. Combining `--frozen-lockfile` with `--update-checksums` errors out — frozen mode refuses to rewrite the lockfile, which is exactly what `--update-checksums` is for.

`--update-checksums` also bypasses the resolver's on-disk metadata cache fast path (`pickPackage.ts:271`, `pick_package.rs:531`). Without that, a stale on-disk packument that already contained the pinned version would short-circuit the registry entirely and the flag would silently no-op on dev machines. With the gate, every first-encounter goes through a conditional GET; the in-memory cache is left alone so second-and-onward references within the same install still hit cached fresh data (one network round-trip per *unique* package, not per reference).

## Reported by

Reported privately via the security channel. The reproduction:

1. Publish `example-package@1.0.0` with content `v1` and install with pnpm; lockfile records the `v1` integrity.
2. Replace the registry's tarball+metadata for the same `1.0.0` with content `v2`.
3. On a clean store/cache, run `pnpm install`. Before this fix, pnpm logged `ERR_PNPM_TARBALL_INTEGRITY` but exited 0 with `v2` installed and the lockfile rewritten to the new integrity. After this fix, the same install exits non-zero.

## Prior art

- **npm** ([sebhastian](https://sebhastian.com/npm-err-code-eintegrity/)): hard-fails with `EINTEGRITY`. No dedicated override flag — recovery is `npm cache clean --force`, manually editing the lockfile, or deleting it.
- **yarn** ([Sean C Davis](https://www.seancdavis.com/posts/fix-yarn-integrity-check-failed/)): hard-fails with "Integrity check failed". Has a dedicated **`yarn install --update-checksums`** flag — pnpm now adopts the same name.

## Pacquet parity

Pacquet was already fail-hard on integrity mismatch by default (no auto-repair path to remove). This PR brings the rest of the surface into line so `pnpm install --update-checksums` keeps working when pacquet is the materialization target, and `pacquet install --update-checksums` behaves identically standalone:

- New `--update-checksums` flag on `pacquet install` (`crates/cli/src/cli_args/install.rs`), plumbed through `Install` and `InstallWithFreshLockfile` into the resolver.
- When the flag is set, pacquet skips the frozen-lockfile fast path and routes through the fresh-resolve path so locked integrity values get rewritten from the registry.
- `--frozen-lockfile + --update-checksums` errors with `pacquet_package_manager::frozen_lockfile_with_outdated_lockfile`, mirroring pnpm's `ERR_PNPM_FROZEN_LOCKFILE_WITH_OUTDATED_LOCKFILE`.
- `pacquet_tarball::verify_checksum_error` now carries a help hint pointing at `--update-checksums` and calling out the supply-chain implication, matching the updated pnpm `TarballIntegrityError`.
- The disk fast-path gate is mirrored in `crates/resolving-npm-resolver/src/pick_package.rs:531`, with the flag threaded from `ResolveOptions` → `PickPackageOptions`.
2026-05-27 12:46:16 +02:00
Zoltan Kochan
ad84fffd46 fix: reject path-traversal segments in dependency aliases (#11954)
* fix: reject path-traversal segments in dependency aliases

A transitive registry package can use a dependency-alias key like
`@x/../../../../../.git/hooks` to make `pnpm install` create a symlink
outside the intended `node_modules` directory, since pnpm passes the
alias straight into `path.join(modulesDir, alias)` without checking
that the joined path stays inside `modulesDir`.

Reject aliases that aren't a single `name` or `@scope/name` shape at
manifest-read time (both the importer's manifest and every transitive
package manifest) and re-check at the symlink layer as defense in
depth. Mirror the fix in pacquet's deps-resolver.

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

* fix(pacquet): use raw strings in alias validator tests for dylint

Perfectionist's `prefer-raw-string` lint rejects the two
backslash-escaped test inputs.

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

* refactor: tighten dependency-alias validator to validate-npm-package-name

An alias is the directory name pnpm creates inside `node_modules`, so
the only valid shapes are a single `name` or `@scope/name` consisting
of URL-friendly characters with no leading `.` / `_`, and not equal to
reserved names such as `node_modules`. That's the same
`validForOldPackages` rule `parseWantedDependency` already applies to
CLI-given names — the manifest-read path should match. Route both
stacks through it so `.bin`, `.pnpm`, `node_modules`, `favicon.ico`,
whitespace, and non-URL-friendly characters are all rejected alongside
the path-traversal shapes the narrow validator caught.

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

* refactor: collapse symlink-layer assertion + path.join into safeJoinModulesDir

The two-step pattern of "assert the alias stays in the dir" then "join
the dir and the alias" left it possible for a caller to use the join
without the assertion. Fold them into a single `safeJoinModulesDir`
that returns the joined path and throws on escape, so the check is
unmissable.

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

* test(symlink-dependency): cover the path-equals-dir guard branch

The earlier tests only exercised the `!startsWith` branch with
`'../sibling'` and `'@x/../../../etc'`. Add `''` and `'.'` as alias
cases — both resolve to the modules dir itself and hit the
`resolvedLink === resolvedDir` branch of `safeJoinModulesDir`.

---
Written by an agent (Claude Code, claude-opus-4-7).
2026-05-26 17:25:25 +02:00
Zoltan Kochan
e0bd879dea fix(deps-resolver): restore index-based pairing so git/tarball deps aren't dropped (#11890)
PR #11711 switched updateProjectManifest and the catalog-update loop in
resolveDependencies to look up wantedDependencies by alias, but
parseWantedDependency returns `{ alias: undefined, bareSpecifier }` for
inputs like `pnpm/foo#sha` or tarball URLs whose alias is only known
after fetching the package's package.json. Those entries collided under
the `undefined` Map key, so the alias-keyed lookup of the resolved dep
returned undefined, the filter dropped them from specsToUpsert, and they
silently disappeared from the manifest update and pendingBuilds.

This restored the index-based pairing the code used before #11711.
catalog: preservation isn't affected: it's driven by
rdd.catalogLookup.userSpecifiedBareSpecifier in the spec object, not by
how wantedDep is looked up.

The premise in the removed comment ("linked deps like workspace:* are
excluded from directDependencies") was also wrong — linked deps stay in
directDependencies with isLinkedDependency: true, they're not dropped.

Restores building/commands/test/build/index.ts: rebuilds dependencies,
rebuilds specific dependencies, rebuild with pending option.
2026-05-24 01:17:17 +02:00
Totoro
ae42a7adc1 fix: preserve catalog: protocol references on upgrade (#11711)
* fix: preserve catalog: protocol references on upgrade (issue #11658)

* refactor: address review feedback on catalog: preservation fix

- Fix typo in 3 test assertions (`@pnpm.e2e.foo` → `@pnpm.e2e/foo`)
  that made `.toBeFalsy()` pass vacuously
- Use `Map` for alias→wantedDependency lookup in `updateProjectManifest`
  to match the pattern in `index.ts`

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-24 00:17:00 +02:00
David Barratt
3422cecfd3 fix(installing.deps-resolver): deterministically order cyclic peer suffixes (#11826)
* fix(installing.deps-resolver): deterministically order cyclic peer suffixes (#8155)

`resolveDependencies` was pushing onto `pkgAddresses`, `postponedResolutionsQueue`,
and `postponedPeersResolutionQueue` from inside `Promise.all`-spawned callbacks,
so the order of items in those arrays reflected completion timing rather than
the order of `extendedWantedDeps`. That ordering then flowed downstream into
`resolvePeers` and the cyclic-peer suffix assignment, so two packages with
transitive peer dependencies on each other (e.g. `@aws-sdk/client-sts` and
`@aws-sdk/client-sso-oidc`) flipped between two equally-valid lockfile forms
across consecutive installs.

The fix awaits `Promise.all` to a temporary array and drains it with `for…of`
so the per-edge results land in input order. This matches the existing pattern
200 lines earlier in `resolveDependenciesOfImporters`.

End-to-end repro from the issue (`pnpm add @aws-sdk/client-s3@3.588.0` then
loop `pnpm dedupe --check`): 33/50 failures without the fix → 0/100 with it.

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

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

* test(installing.deps-installer): replace all slashes in mock metadata path

Addresses CodeQL incomplete-string-escaping finding: `replace('/', '%2F')`
only swaps the first occurrence. Scoped names in this test only have one
slash so the behavior is unchanged, but switching to `replaceAll` clears
the warning and is more defensible.

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

* test(installing.deps-installer): assert raw snapshot key order

Removed the .sort() applied to the lockfile snapshot keys in the cyclic
peer determinism test so the comparison reflects the actual order
emitted by the lockfile writer. The deterministic ordering guaranteed
by 7577d47 makes the sorted view and the raw view identical today;
dropping the sort lets the test fail on any future regression that
keeps the key set stable but shuffles the order.

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

* test(installing.deps-installer): drop stray manifest from MutatedProject literal

MutatedProject does not carry a manifest field; it is conveyed via
allProjects in MutateModulesOptions. Passing it inside the install
project literal triggered TS2353 against the InstallDepsMutation shape.

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

* test(installing.deps-installer): rename metas to metaByName

Clearer name for the map keyed by package name, and avoids tripping
cspell on the abbreviation "metas".

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

* chore: retrigger CI

Ubuntu Node.js 22.13.0 hit a transient 404 from Verdaccio's
proxy-to-npm while resolving a transitive peer of
@medusajs/medusa-js@6.1.7 in the pre-existing
"install should not hang on circular peer dependencies"
test (installing/deps-installer/test/install/misc.ts:1247).
Ubuntu Node 24 and Node 26 ran the same code green.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:20:01 +02:00
Zoltan Kochan
4195766f10 feat: tighten minimumReleaseAge — auto-exclude, lockfile verification, and interactive prompt (#11705)
Three coordinated changes that close the silent-bypass gap in loose `minimumReleaseAge` mode AND the discover-by-loop UX problem in strict mode (#10488), plus a parallel hardening of the lockfile verifier:

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

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

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

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

The steady-state flows:

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

### Implementation

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

### Pacquet parity

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

### Closes

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

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

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

### TypeScript

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

### Pacquet

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

### Migration

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

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

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

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

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

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

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

## Test plan

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

## Notes

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

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

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

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

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

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-05-15 08:12:23 +00:00
Peter Goldberg
c2c289094f fix: time-based resolution loses publishedAt on fast path (#11618) 2026-05-14 09:20:51 +00:00
Igor Savin
b61e268d57 feat: add support for github prefix and named registries (#11324)
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>
2026-04-29 12:38:56 +02:00
Zoltan Kochan
72c1e050e9 feat: add pnpm pack-app command for packing CJS entries into standalone executables (#11312)
* fix: give each runtime variant its own global virtual store entry

When a runtime package (e.g. node@runtime:X.Y.Z) uses a variations
resolution, createFullPkgId() in @pnpm/deps.graph-hasher was hashing
the whole VariationsResolution — the same hash on every host — so the
global virtual store path collided between variants. Whichever variant
installed first won, and a later `pnpm add --libc=musl node@runtime:<v>`
silently reused the cached glibc (or macOS/Windows) binary.

The fix threads supportedArchitectures down to createFullPkgId so the
selected variant's integrity is used as the package fingerprint. Two
related cleanups land with it:

- Extract the platform-variant selection logic to @pnpm/resolving.resolver-base
  as selectPlatformVariant/resolvePlatformSelector. The helper's libc
  match also required a fix: a variant with no libc is the "default"
  build, and a request for a non-default libc (e.g. musl) must require
  an exact match so the default variant doesn't silently win.
- @pnpm/installing.package-requester's findResolution now delegates to
  the shared helper, and the new supportedArchitectures param is plumbed
  through calcDepState / calcGraphNodeHash / iterateHashedGraphNodes /
  lockfileToDepGraph and their callers in deps-resolver, deps-restorer,
  deps-installer, graph-builder, and building.after-install.

* feat: add pnpm build-sea command for building Node.js SEA executables

Adds `pnpm build-sea` under @pnpm/releasing.commands. Takes a CommonJS
entry file and a set of target triplets (linux-x64, linux-x64-musl,
linux-arm64, linux-arm64-musl, macos-x64, macos-arm64, win-x64,
win-arm64) and produces a standalone executable per target under
dist-sea/<target>/.

Each target's Node.js runtime is fetched via `pnpm add node@runtime:<v>
--os=<os> --cpu=<arch> --libc=<libc>` into $PNPM_HOME/build-sea/<target>-<v>/
so binaries are hardlinked from the global content-addressable store and
`pnpm store prune` can reclaim them.

Requires Node.js v25.5+ to perform the --build-sea injection. If the
running Node is older, a v25 binary is downloaded and used as the builder
automatically. macOS outputs are ad-hoc signed with codesign (on macOS)
or ldid (when cross-compiling from Linux), which is required because SEA
injection invalidates the binary's existing signature.

* fix(build-sea): reject malformed --target, --output-name and use mkdtemp for config

Addresses Copilot review feedback on the build-sea command:

- parseTarget() previously destructured the target string, silently
  accepting extra `-` segments. Inputs like `linux-x64-musl-../../outside`
  would pass validation and flow into path.join. Validation is now done
  with a strict anchored regex.
- --output-name was passed into path.join() without sanitization, so a
  caller could escape the output directory with path separators or `..`.
  validateOutputName() now rejects anything that isn't a plain basename.
- The per-target SEA config file was written to a predictable path under
  os.tmpdir() (derived from the target name and Date.now()), which is
  unsafe on multi-user systems. It now lives inside a fresh mkdtemp()
  directory and is opened with the exclusive "wx" flag.
- New test cases cover extra-segment targets, uppercase/whitespace
  variants, and the full matrix of invalid --output-name inputs.

* rename: build-sea → pack-app

`build-sea` required knowing what a SEA is. `pack-app` is self-describing,
doesn't collide with pnpm's existing `bin` concept, and parallels the
existing `pack` command.

- Command name: build-sea → pack-app
- Default output dir: dist-sea → dist-app
- Error codes: PACK_APP_* (was BUILD_SEA_*)
- Export/type: packApp / PackAppOptions (was buildSea / BuildSeaOptions)
- Install cache dir: $PNPM_HOME/pack-app (was $PNPM_HOME/build-sea)

The Node.js `--build-sea` flag name itself is unchanged — that's a
Node.js feature and outside this project's naming.

* fix(pack-app): reject directory entries, pin builder to >=25.5, refuse macOS target on Windows

Addresses Copilot review feedback on the pack-app command:

- entry validation now rejects non-file paths (directories, symlinks to
  non-files) with a dedicated PACK_APP_ENTRY_NOT_FILE instead of
  surfacing a less actionable error later in the SEA build.
- DEFAULT_BUILDER_SPEC was the bare major ("25"), which would satisfy
  with 25.0.x if that version is still present — those point releases
  predate --build-sea support. Tightened to ">=25.5.0 <26.0.0" so the
  download is guaranteed to support the flag without ever crossing a
  major.
- adHocSignMacBinary() silently skipped re-signing on Windows hosts.
  Now throws PACK_APP_MACOS_SIGN_UNSUPPORTED_HOST with a hint to build
  the target on macOS/Linux or re-sign manually.
- resolvePlatformSelector() JSDoc now matches what the code actually
  does (picks the first entry when it is not "current"; later entries
  are ignored).
- New test case covers the directory-as-entry rejection.

* refactor(pack-app): switch target OS names to process.platform constants

Previously `pack-app` accepted `macos-*` / `win-*` as the OS portion of a
target triplet and translated them to `darwin` / `win32` internally. The
translation layer made the CLI surface inconsistent with the values that
`pnpm add --os=…` and `supportedArchitectures.os` already use, and added
a small footgun (e.g. users setting `supportedArchitectures: { os: [darwin] }`
but typing `macos-arm64` for pack-app).

The supported target OS set is now `linux | darwin | win32`, matching
`process.platform`. Old inputs like `macos-arm64` or `win-x64` now fail
validation with a clear error pointing to the new naming. The internal
parseTarget helper drops its TARGET_OS_MAP lookup entirely.

This is a change to an unreleased command so there is no back-compat
concern. pnpm's own artifact directory names (`pnpm/artifacts/macos-*/`,
`pnpm/artifacts/win-*/`) are an internal implementation detail and are
not affected by this change.

* feat(pack-app): read defaults from pnpm.app in package.json

Every pack-app flag (--entry, --target, --node-version, --output-dir,
--output-name) can now be preconfigured in the project's package.json
under a new "pnpm.app" object:

  {
    "name": "my-cli",
    "pnpm": {
      "app": {
        "entry": "dist/index.cjs",
        "targets": ["linux-x64", "darwin-arm64", "win32-x64"],
        "nodeVersion": "25",
        "outputDir": "release",
        "outputName": "my-cli"
      }
    }
  }

CLI flags always win. --target replaces the configured list rather than
appending, so a user can narrow the default set at the command line.

The config loader is strict: unknown keys under pnpm.app and any
type-mismatched values throw PACK_APP_INVALID_CONFIG so mistakes surface
at invocation time instead of silently being ignored.

Chose pnpm.app over pnpm.packApp because it's the shorter, cleaner
namespace for anything related to the app bundle (future sibling
commands like run-app / deploy-app could share the same object without
a naming clash). Chose package.json over pnpm-workspace.yaml because
the config is inherently per-project, whereas pnpm-workspace.yaml is
workspace-root-only.

* fix(pack-app): deterministic libc selection and stricter output-name validation

Addresses Copilot review feedback:

- ensureNodeRuntime() now always passes an explicit --libc for linux
  targets. Without a suffix, linux-x64 and linux-arm64 default to
  --libc=glibc instead of letting the user's supportedArchitectures.libc
  config or the host's detected libc decide the variant. The install
  cache directory mirrors this, so glibc and musl variants are always
  distinct (linux-x64-glibc vs linux-x64-musl).
- resolveBuilderBinary() now pins the host libc when downloading a
  builder Node on Linux. A user whose config sets supportedArchitectures.libc
  to musl no longer ends up with a musl Node that the glibc host cannot
  execute.
- validateOutputName() rejects Windows-invalid filename characters
  (<>:"|?* and NUL), Windows reserved device names (CON, NUL, COM1, etc.),
  and names ending in a dot or space — problems surface at invocation
  time rather than during writeFile(outputFile, ...) on Windows.
- lockfileToDepGraph variants tests no longer derive the "host"
  variant from process.platform/process.arch; they always pass an
  explicit supportedArchitectures selector so the expectations hold on
  any CI host (including Alpine/musl).

* chore: add "toctou" to cspell wordlist

`TOCTOU` (time-of-check-to-time-of-use) is the standard term for the
race-condition class the pack-app SEA-config comment describes. Adding
it to the wordlist unblocks the Lint CI step.

* fix: lint
2026-04-20 14:29:49 +02:00
Zoltan Kochan
c86c423bdc fix: preserve pnpm 10 peer suffix encoding for linked paths (#11297)
* fix: preserve pnpm 10 peer suffix encoding for linked paths

The filenamify upgrade from v4 to v7 changed the peer-suffix "version"
token for linked dependency paths: `../packages/b` became `+packages+b`
instead of `packages+b`, causing lockfile churn for workspaces with
packages linked from outside the workspace root.

Replace the filenamify call with a small inline encoder that reproduces
v4's output for link paths, and drop the now-unused dependency.

Closes #11272.

* chore: avoid cspell-flagged word in peer suffix comment

* test: cover linkPathToPeerVersion and clarify its lossy encoding

Address Copilot review feedback on #11297:
- Correct the comment — any leading run of `.` is dropped, not just
  `./` and `../` segments (so `.hidden/pkg` becomes `hidden+pkg`).
- Export the helper and add a focused test that pins the exact token
  output for link paths, so future lockfile-breaking regressions get
  caught by the test suite.

* refactor: extract linkPathToPeerVersion into its own file
2026-04-19 12:57:16 +02: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
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
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
Zoltan Kochan
0d88df854f chore: update all dependencies to latest versions (#11032)
* chore: update all dependencies to latest versions

Update all outdated dependencies across the monorepo catalog and fix
breaking changes from major version bumps.

Notable updates:
- ESLint 9 → 10 (fix custom rule API, disable new no-useless-assignment)
- @stylistic/eslint-plugin 4 → 5 (auto-fixed indent changes)
- @cyclonedx/cyclonedx-library 9 → 10 (adapt to removed SPDX API)
- esbuild 0.25 → 0.27
- TypeScript 5.9.2 → 5.9.3
- Various @types packages, test utilities, and build tools

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

* fix: update unified/remark/mdast imports for v11/v4 API changes

Update imports in get-release-text for the new ESM named exports:
- mdast-util-to-string: default → { toString }
- unified: default → { unified }

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

* fix: resolve typecheck errors from dependency updates

- isexe v4: use named import { sync } instead of default export
- remark-parse/remark-stringify v11: add vfile as packageExtension
  dependency so TypeScript can resolve type declarations
- get-release-text: remove unused @ts-expect-error directives

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

* fix: revert runtime dependency major version bumps

Revert major version bumps for runtime dependencies that are bundled
into pnpm to fix test failures where pnpm add silently fails:
- bin-links: keep ^5.0.0 (was ^6.0.0)
- cli-truncate: keep ^4.0.0 (was ^5.2.0)
- delay: keep ^6.0.0 (was ^7.0.0)
- filenamify: keep ^6.0.0 (was ^7.0.1)
- find-up: keep ^7.0.0 (was ^8.0.0)
- isexe: keep 2.0.0 (was 4.0.0)
- normalize-newline: keep 4.1.0 (was 5.0.0)
- p-queue: keep ^8.1.0 (was ^9.1.0)
- ps-list: keep ^8.1.1 (was ^9.0.0)
- string-length: keep ^6.0.0 (was ^7.0.1)
- symlink-dir: keep ^7.0.0 (was ^9.0.0)
- terminal-link: keep ^4.0.0 (was ^5.0.0)

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

* fix: restore runtime dependency major version bumps

Re-apply all runtime dependency major version bumps that were
previously reverted. All packages maintain their default exports
except isexe v4 which needs named imports.

Updated runtime deps:
- bin-links: ^5.0.0 → ^6.0.0
- cli-truncate: ^4.0.0 → ^5.2.0
- delay: ^6.0.0 → ^7.0.0
- filenamify: ^6.0.0 → ^7.0.1
- find-up: ^7.0.0 → ^8.0.0
- isexe: 2.0.0 → 4.0.0 (fix: use named import { sync })
- normalize-newline: 4.1.0 → 5.0.0
- p-queue: ^8.1.0 → ^9.1.0
- ps-list: ^8.1.1 → ^9.0.0
- string-length: ^6.0.0 → ^7.0.1
- symlink-dir: ^7.0.0 → ^9.0.0
- terminal-link: ^4.0.0 → ^5.0.0

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

* fix: revert tempy to 3.0.0 to fix bundle hang

tempy 3.2.0 pulls in temp-dir 3.0.0 which uses async fs.realpath()
inside its module init. When bundled by esbuild into the __esm lazy
init pattern, this causes a deadlock during module initialization,
making the pnpm binary hang silently on startup.

Keeping tempy at 3.0.0 which uses temp-dir 2.x (sync fs.realpathSync).

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

* docs: add comment explaining why tempy cannot be upgraded

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

* fix: revert nock to 13.3.4 for node-fetch compatibility

nock 14 changed its HTTP interception mechanism in a way that doesn't
properly intercept node-fetch requests, causing audit tests to hang
waiting for responses that are never intercepted.

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

* docs: add comment explaining why nock cannot be upgraded

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

* fix: update symlink-dir imports for v10 ESM named exports

symlink-dir v10 removed the default export and switched to named
exports: { symlinkDir, symlinkDirSync }.

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

* fix: revert @typescript/native-preview to working version

Newer tsgo dev builds (>= 20260318) have a regression where
@types/node cannot be resolved, breaking all node built-in types.

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

* fix: vulnerabilities

* fix: align comment indentation in runLifecycleHook

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

* fix: pin msgpackr to 1.11.8 for TypeScript 5.9 compatibility

msgpackr 1.11.9 has broken type definitions that use Iterable/Iterator
without required type arguments, causing compile errors with TS 5.9.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:28:53 +01:00
Zoltan Kochan
8acf2708c9 refactor: rename deps-resolver and env-installer packages (#11013)
Rename @pnpm/installing.resolve-dependencies to @pnpm/installing.deps-resolver
for consistency with the <domain>.<leaf> naming convention.
2026-03-18 21:52:01 +01:00