Commit the pnpm-managed Cargo source block. The block is a function of
Cargo.lock, so an install regenerates it byte for byte and the tracked
`.cargo/config.toml` stops reporting as modified after every install. A new
Rust CI step fails if the committed block and the lockfile drift apart.
Cargo now resolves every crate through `.pnpm/crates` and falls back to its
own registry nowhere, so each job that runs cargo needs an install behind it.
Eight did not have one: both cargo-unused jobs, the micro-benchmark, the
integrated benchmark's build and executor jobs, and the three release builds.
The new `install-crates` action gives them one and narrows the JavaScript half
with `--filter pacquet`, which a filter does not do to the Cargo half.
The release builds go through `cross`, which mounts the checkout at
`/project`. pnpm links each crate into `.pnpm/crates` with a relative symlink,
so a store outside the checkout stops resolving under that mount. Those jobs
install into a store inside the checkout instead, on Linux, the one host where
cross containerizes the build at all.
The two workflows that commit no longer discard the block. It is tracked
content now, and a run that changes it should carry the change.
Claude-Session: https://claude.ai/code/session_01Gg6uVUzLw1MniCLC81TQjP
Claude-Session: https://claude.ai/code/session_01HwJS1pAz9HHQpJWEJAiaUu
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Give Python installation an independent crate boundary. Keep project
discovery and command adaptation in the CLI, and pass manifest paths plus
install and add options into the installer.
Move manifest handling, resolution, wheel fetching, interpreter operations,
and environment publication and rollback together. Preserve the existing
unit and CLI integration coverage and execution-path behavior.
Move pnpr ecosystem capability caching into the client crate so Cargo and
Python installation share the same process-wide cache without depending
on the CLI. Cover concurrent reuse and cached failures with tests.
Remove Python version/requirement type dependencies from the CLI while
retaining its resolver dependency for command-line specifier parsing.
Reject invalid Python save prefixes before preparing an install, preserving
the accepted values and default. Include regression coverage and a release note.
Follow-up to pnpm/pnpm#14816.
taplo padded TOML with spaces so that things line up in a column, which makes
unrelated lines show up in diffs. Two separate options did this.
`align_entries` padded the key of every entry so the `=` signs share a column.
In `[workspace.dependencies]` that meant padding every entry out to the width of
the longest crate name, so adding or renaming one crate reflowed the whole
block. `align_comments` did the same for trailing comments, so the clippy lint
tables in the root manifest repadded every comment whenever a lint name grew.
Set both to false and reformat the 107 files taplo covers. The diff is
whitespace only: `git diff -w` reports `.taplo.toml` as the only file that
differs. Verified with both the locally installed taplo 0.10.0 and the 0.8.1
that CI pins, so `pnpm ci:toml-fmt` passes on both.
Move npm package-name validation into a dependency-free crate and replace
alias validation's forwarding function with a reexport of that validator.
Migrate validation consumers directly to the owning crate. Remove the full
dependency resolver from global scanning and dependency restoration, and
remove parser dependencies from crates that only validate names. Preserve
the old parser and resolver public paths through reexports.
Keep validation behavior and existing tests unchanged. Global scanning's
transitive internal production dependencies decrease from 34 crates to 6.
Follow-up to pnpm/pnpm#14807.
Give wildcard matching and GitHub Actions dependency handling independent
crate boundaries. Preserve config's matcher reexport and keep CLI opt-in
policy separate from workflow processing.
Replace duplicate literal-star matchers in global listing and versioning
with the shared compiled matcher, preserving each caller's negation and
empty-selector behavior. Remove configuration from workspace filtering's
production dependency graph and move YAML dependencies out of the CLI.
Validate planned workflow edits against freshly read action text after
asynchronous Git lookups. Reject stale ranges without overwriting the
changed workflow. Redact homepage credentials and restrict GitHub server
URLs to HTTPS or loopback HTTP in both CLI versions, with regression tests
and a shared release note.
Follow-up to pnpm/pnpm#14804.
* chore: install the Rust dependencies with pnpm
Turn on `cargo.enabled` so `pnpm install` installs the crates `Cargo.lock`
pins alongside the JavaScript dependencies. pnpm links the registry crates
into `.pnpm/crates/crates-io` and the git-sourced ones into
`.pnpm/crates/git`, then writes a source replacement block into
`.cargo/config.toml` that points Cargo at both.
The `node-semver` fork stays patched in. pnpm installs git-sourced crates
since pnpm/pnpm#14694, which shipped in the 12.4.1 the repository pins, so
enabling this no longer costs the fork's `<=` range and `Ord for Bound`
fixes.
Document the workflow, including the two things a contributor hits first:
`pnpm install` shells out to `cargo`, so it fails when `cargo` is off
`PATH`, and the generated block leaves the tracked `.cargo/config.toml`
modified after every install.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDxJNVEeyUBb4pcepEpGcj
* ci: initialize Rust before pnpm installs dependencies
Every `pnpm install` now shells out to `cargo metadata`, and there is no
way to opt one out: `cargo.enabled` is read from `pnpm-workspace.yaml`
only, no CLI flag or `PNPM_CONFIG_*` variable overrides it, and `--filter`
does not narrow the Cargo half. So each job whose install runs gets the
pinned toolchain first. `pnpm/setup` installs unless told not to,
`pnpm/update` runs its own install, and `pnpm pipeline` installs before it
runs anything.
The step has to precede the install rather than follow it. The rustup
action ends with `git restore .`, which would otherwise wipe the source
replacement block back out of `.cargo/config.toml`.
Keep that block out of the two workflows that commit. It points at the
gitignored `.pnpm/crates`, so a commit carrying it would break every
Rust-only job and every checkout that has not installed. Both workflows
also decide whether to commit at all by reading `git status --porcelain`,
which the block would make non-empty on every run. Each discards it right
before that check: the release PR in a step of its own, the lockfile
update through the `post-update` command pnpm/update runs between its
install and its commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDxJNVEeyUBb4pcepEpGcj
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Declare CI pipelines and root task scripts, and invoke them from the
TypeScript, Rust, reusable test, and pnpr build workflows. Let pipelines
perform frozen installs and order TypeScript compilation before linting.
Keep existing test runners responsible for affected-package selection,
file-level sharding, and summary generation. Read shard settings from the
workflow environment, with explicit command-line arguments taking precedence.
Run root tasks with --include-workspace-root --full: the scripts live in the
root manifest only, and the test runners already select their own scope. Pass
--no-cache so a task that later declares outputs cannot skip a check.
Keep the dependency install in its own step in the two jobs whose commands are
timed for Bencher, so the reported duration stays a test duration. Preserve
existing job gates and artifact handoffs.
Split Rust production modules and test suites across the repository to keep
production files within 400 code lines and test files within 800. Group
modules by responsibility and test scenarios while retaining public APIs
and existing test binaries.
Move Node release public keys into individual data assets and update their
generator. Preserve key bytes, fingerprints, and ordering. Update module
imports, documentation links, and snapshot locations after extraction.
Document the limits and verify them with the proposed upstream rule's
lexer without depending on an unmerged lint.
Related to pnpm/pnpm#14562.
* fix(changeset): pnpr's ecosystem route prefixes break nothing published
The intent declared a major bump, which the release engine applied as
plain semver: 0.1.0-alpha.10 escalated its stable target to 1.0.0 and
restarted the prerelease counter, so pnpr was heading for 1.0.0-alpha.0.
The `/npm/`, `/cargo/` and `/pypi/` prefixes only address a registry that
serves more than one ecosystem, and the ability to serve more than one
arrives in this same release. No deployed pnpr can be broken by them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGJYxrAARz5kpZnESxm3xd
* docs(skills): add a release-notes curation skill
The wording rules for one changeset live in CLAUDE.md. Nothing describes
the set: that the composed section under .changeset/changelogs is the
release page and the only surviving copy once the bump deletes the
intents, that a defect introduced and fixed inside one release window
has no reader, that entries naming the same change from two pull
requests should be merged, or that a major intent on a 0.x package jumps
it to 1.0.0.
Calibrate the per-entry advice against release pages worth reading.
esbuild titles every entry and pairs it with before and after output. uv
keeps one idea per bullet and names a flag, file, or platform in each.
Playwright leads with a few highlights before its flat list, and uv's
category headings give a skimmer somewhere to stop. pnpm's composer
emits no headings beyond Major, Minor, and Patch, so the skill asks for
the same structure to be built out of the entry order.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGJYxrAARz5kpZnESxm3xd
* chore(release): pacquet 12.4.1, pnpr 0.1.0-alpha.11
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGJYxrAARz5kpZnESxm3xd
* docs(changelog): curate the 12.4.1 and pnpr 0.1.0-alpha.11 release pages
The composed sections were a flat dump of one intent per pull request:
forty three pacquet entries and thirty four pnpr ones, several
describing the same change from different angles, several describing a
regression that never reached a published version, and all of them in
whatever order the intent filenames happened to sort in. pacquet opened
on a hard-link-limit edge case with the `ignoredOptionalDependencies`
and `linkWorkspacePackages` bugs buried mid-list; pnpr stated the new
ecosystem route prefixes twice and spread one container registry across
seven entries.
Merge the entries that describe one user-visible change, drop the notes
for defects introduced and fixed inside this release window, trim the
prose that only a contributor would care about, normalize the issue
links, and order each page from the failures that break an install down
to the wording fixes. pacquet goes from forty three entries to thirty
six, pnpr from thirty four to nineteen.
Give `@pnpm/napi` a line as well. It bumps through its fixed group with
pacquet and had no intent of its own, so it was shipping a bare
`## 12.4.1` heading.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGJYxrAARz5kpZnESxm3xd
* docs(changelog): open each release page with a summary line
Deno opens a release post with one sentence naming the top few changes,
so a reader decides from that line whether to read on. The composed
section has room for the same: `tail -n +2` in the release workflow
keeps a paragraph placed under the version heading, and it becomes the
first line of the GitHub release body.
Widen the sources behind the skill while here. Yarn's pages are the
conventional-commit log with the PR appended, which is the shape pnpm's
uncurated output already has, so name it as the thing being fixed.
Gitea publishes the ranking the ordering section was reaching for and
puts SECURITY second, which the ranking had left out entirely; note that
the Major, Minor and Patch headings cut across it, so a security fix on
a patch bump lands below every feature and the lead paragraph has to
carry it. SQLite and Gitea calibrate the one-sentence default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGJYxrAARz5kpZnESxm3xd
* docs: address the review threads on the release pages
The `updateConfig` entry said "A setting nothing set is left out", a
reduced relative clause that a reader has to parse twice in a published
release note.
The version check told the reader to read the line for each released
package, which skips the one most easily missed: `@pnpm/napi` carries no
intent of its own and rides pacquet's fixed group, and release.yml fails
the build when the two have drifted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGJYxrAARz5kpZnESxm3xd
* docs(changelog): group the release pages by subject
Thirty six patch entries under one heading is a wall. A reader looking
for whether their install bug is fixed has to read every line about
sbom timestamps and help text to find out.
Name the groups instead. pnpm 12.4.1 splits into installing, resolving
and linking, performance, scripts and tasks, commands, configuration,
Windows, and messages; pnpr's minor changes into registries, container
images, publishing, builds, and discovery. The order inside and between
groups is unchanged.
Both consumers tolerate the extra heading level. release.yml writes the
Rust release body with `tail -n +2`, and getChangelogEntry slices
between headings of the same depth as the version heading, so a `####`
neither ends the slice nor is dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGJYxrAARz5kpZnESxm3xd
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The loader's documentation lists what it refuses from a cache entry a
hostile repository can write, and ended on what it does not refuse: a
suffix naming another dependency-set variant of the same
`name@version`. Stopping there reads as an open question and invites the
next reader to re-derive the answer.
It is not the lockfile that bounds it. The lockfile pins the name and
the version, which is what the prefix check enforces, but the digest
picks between variants and the shared store holds variants other
projects derived - with their child links, not this lockfile's.
What bounds it is downstream: `slot_contents_complete` probes every
child link the snapshot's own dependencies imply before treating a slot
as materialized, so a foreign variant is rewritten rather than reused.
The cost of a suffix that lies is a slot written at the wrong path, not
a package linked against the wrong dependencies. Says so, and says which
check does it.
Claude-Session: https://claude.ai/code/session_015YZGn6jMwYeMveHf8RHKfU
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: enforce 40-line production Rust functions
Adopt perfectionist::overly_long_function across the Rust workspace with
tests exempt. Refactor all existing violations without legacy suppressions
and place helper contracts at their declarations.
Related to pnpm/pnpm#14562.
* chore: omit release note for internal Rust refactor
Resolve main and HEAD once and build their Rust client/server pairs in
parallel while retaining the per-commit binary caches. Build the executor
in a third independent job. Exclude the peer-heavy fixture from the shared
npm proxy cache and invalidate potentially incomplete cache entries. Transfer the
binaries and shared benchmark executor in tar archives to preserve
executable permissions.
Run each scenario on a separate runner, keeping all compared revisions
within the same hyperfine invocation. Build the TypeScript CLI only for
the peer-heavy scenario. Aggregate scenario artifacts into the existing
report and Bencher formats, and propagate the peer-heavy gate result
after publishing measurements.
This changes CI orchestration only and needs no package changeset.
`copy_file` used `fs::copy`, whose destination open follows a symlink.
A link squatting at an import target therefore had its referent
overwritten with store content, and when the referent did not exist yet
it was created — pnpm writing a file at whatever path the link named,
outside the tree it owns. The link tiers never did this: `link(2)` and
`ioctl(FICLONE)` fail `EEXIST` on the squatter and reach
`recover_from_concurrent_import`, so the same corrupt target produced
opposite outcomes depending on `packageImportMethod`.
The target is now created with `O_EXCL`, which never follows a symlink,
so the squatter surfaces as `AlreadyExists` and takes the path the link
tiers already take.
`link_file`'s stat short-circuit hid the overwrite whenever the referent
existed, since that stat follows the link and returns early. It does not
cover the dangling case, nor the `Placement::Fresh` import that skips
the stat.
The mode is asserted from the source through the opened file, because
`fs::copy` propagated it as part of the copy and an exclusive create
would otherwise leave a `0o600` store entry world-readable in
`node_modules`.
Two comments claimed the fresh-lockfile path is handed no current
lockfile, so the presence check's resolution comparison could not run
there. It does run there. `install/run.rs` loads the current lockfile and
`materialize.rs` threads it into both install paths, so `pnpm add`
compares the recorded resolution against the wanted one exactly as a
frozen install does.
`resolution_changed_at` returns `false` when there is no previous graph
to compare against, which is an install with no current lockfile or one
whose `packages:` map is empty, not a whole install path.
A test pins the behavior so the claim cannot drift again: it retouches
the integrity recorded in `node_modules/.pnpm/lock.yaml`, runs
`pnpm add`, and asserts that package was imported again while an
unchanged sibling was left alone. It fails without the comparison.
Claude-Session: https://claude.ai/code/session_014cNCc8LjujhGgdN85d1RAq
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`get_peer_version_range` stripped `workspace:` but left bare shorthand (`workspace:^`/`~`/empty) as an unparseable range that could never satisfy any installed version, so `pnpm peers check` always reported such peers as unmet; falls back to `*` for those tokens instead, matching `workspace-range-resolver`'s existing wildcard handling. Closespnpm/pnpm#14770.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
Under `nodeLinker: hoisted`, every install re-imports every package. `link_hoisted_modules` calls `import_indexed_dir` with `force: true` for each node, and with an existing tree that arm is `stage_and_swap`: copy the whole package into a `_pacquet-stage_` directory, rename it over the old one, for all of them. A repeat install of a 1,881-package hoisted workspace (`packageImportMethod: copy`) took 18.4 s against 10.2 s for the fresh fill and printed `Packages: +1881 -174`; pnpm 11.26 on the same tree takes 3.1 s and prints `-174`. The `+312 -63` in pnpm/pnpm#14278's report is the same behaviour.
pnpm 11 avoids this in `lockfileToHoistedDepGraph`:
```js
const skipFetch = opts.currentHoistedLocations?.[depPath]?.includes(depLocation) &&
await dirHasPackageJsonWithVersion(path.join(opts.lockfileDir, depLocation), pkgVersion)
```
and marks such a node `isBuilt` so the build step leaves it alone too. pacquet writes `hoistedLocations` into `.modules.yaml` but never reads it back; the current lockfile is consulted only to build `prev_graph` for orphan removal.
## Change
- `LockfileToHoistedDepGraphOptions` gains `current_hoisted_locations`; the walker marks a node `present` when the previous install recorded it at this directory and the directory holds a `package.json` of the expected version (the recorded manifest version where the lockfile carries one, else the dep-path version). A `force` walk never marks anything present.
- `link_hoisted_modules` does not import a present node. Its bins are still linked and orphan removal is unchanged.
- The build phase sees only the packages the linker imported (`hoisted_build_snapshots` replaces `CreateVirtualStore`'s materialized list for the hoisted linker, whose snapshots all survive its skip filter), so lifecycle scripts do not run again over an already built directory and `pendingBuilds` is not padded with them. Exceptions, so the build policy is still applied where it has to be: `pnpm rebuild` / `approve-builds`, an `allowBuilds` change since the previous install (an ignored build newly allowed, an approval withdrawn, or any recorded entry flipped), and a package the previous install recorded as ignored or pending.
- Never present: a directory (`file:`) dependency, whose source changes without its version changing, and a patched package, whose patch is applied on a fresh copy.
- Never present either: a package whose resolution the current lockfile records differently from the wanted one, since a dep path carries only a name and a version and survives a change of tarball URL, integrity or revision; and a package reached through a link, since the linker writes real directories of regular files and `import_indexed_dir` clears a link out of that slot. The resolution comparison is skipped on the fresh-lockfile path, which is handed no current lockfile.
- The hoisted linker emits the `pnpm:stats` `added` count as the number of packages it imported; `CreateVirtualStore` emits it for the hoisted linker only on a `virtualStoreOnly` install, which never reaches the linker.
- `hoistedLocations` from the previous `.modules.yaml` is threaded through both install paths.
One deliberate difference from pnpm 11's `dirHasPackageJsonWithVersion`: a directory whose `package.json` cannot be read is treated as absent (pnpm 11 falls back to "the directory exists"), so an interrupted import is repaired rather than skipped.
---------
Co-authored-by: EJ Campbell <ejc3@fb.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The layout cache validates a suffix it reads back off disk by checking
that it starts with the `<scope>/<name>/<version>/` its snapshot key
implies. A prefix is not a whole name: `@scope/foo/1.2.3/../../../../tmp`
passes that check, and `join_global_virtual_store_path` then walks it
back out of the store, because splitting a suffix into components is
that function's job and judging them is not.
`cacheDir` is settable from a repository's own `pnpm-workspace.yaml`, so
a hostile checkout can ship the entry that gets read. The last component
must now be the hex digest `calc_graph_node_hash` produces, which is a
name rather than a route.
Two things found alongside it. `AllowBuildPolicy::fingerprint` has no
caller - the cache key hashes the gating set through
`GvsHasher::write_gating_set` - and its documentation claimed the cache
keys on it, which would have misled the next person to change either
one. And the cache write no longer `sync_all`s: it runs on the miss
path, after the map has already been derived, so the fsync is latency
spent on the install the cache exists to speed up, and a torn entry is
something `load` already refuses and re-derives.
No changeset: the layout cache landed in pnpm/pnpm#14511 and has not
been released.
Claude-Session: https://claude.ai/code/session_015YZGn6jMwYeMveHf8RHKfU
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Linking a project's bins read every shim path before writing it, to find
out whether something already occupied it. In a `node_modules/.bin` this
run created, nothing can, and the read is 19 failing opens per project
on a warm restore of the issue's 76-project workspace.
`create_dir_all_reporting` says whether this run created the bin
directory. When it did, the shim write goes first and the read happens
only if the exclusive create finds an entry after all, which keeps the
answer right when a concurrent install populates the directory in
between. Anywhere else the read still goes first, so an ordinary
reinstall does not pay a failed create for every shim already in place.
Per warm restore of the repro: opens 3,509 -> 2,032. Wall time and
system CPU are unchanged within noise on an M-series Mac, which does not
reproduce the gap the issue reports.
Related to pnpm/pnpm#14540.
The Rust directory matcher only supported `*` and complete-segment `**`,
while pnpm v11 uses micromatch and accepts patterns such as `?` and `[ab]`.
Extend the matcher with `?` and bracket expressions instead of delegating
to a general-purpose glob crate. `wax` accepts a different dialect from the
micromatch call upstream makes, and the matcher normalizes `\` to `/`
before matching, so no metacharacter can be escaped for it. Measured
against micromatch 4.0.8, delegating to `wax` both over- and under-matched
real directory paths: `$` is a `wax` wildcard, so `pkg$a` also selected
`pkga`; a directory literally named `pkg[1]` or `pkg{1}` stopped matching
its own path; and `[!ab]` / `[^ab]` are inverted between the two, because
picomatch negates with `^` and treats `!` as an ordinary set member.
The matcher now follows picomatch: `[abc]`, `[a-c]`, `^` negates, `!` is a
member, a leading `]` is a member, an unterminated `[` is a literal, and a
candidate equal to the pattern matches before any wildcard applies, which
is what keeps a metacharacter-named directory selectable by its path. A
randomized differential run of 4000 cases against micromatch reports no
mismatches.
The pattern is parsed once per selector rather than once per project.
---------
Signed-off-by: cuishuang <imcusg@gmail.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Allow automatic imports to continue when Unix link permissions are denied.
Copy errors still surface, and explicit link methods retain their existing behavior.
Fixespnpm/pnpm#14780.
A filesystem caps how many names one file may carry: 1024 on NTFS,
65000 on ext4. pnpm's store is shared across projects, so a popular
entry accumulates a name per project that installs it and eventually
reaches the cap. Nothing in either stack recognised the resulting
`EMLINK` / `ERROR_TOO_MANY_LINKS`, so an explicit
`packageImportMethod: hardlink` failed the install on it, and `auto`
read it as the filesystem refusing links and retired the hardlink tier
for the rest of the process — one popular file downgrading every
package after it.
The limit belongs to the source file, not to the filesystem, so it is
the one link failure where copying is both necessary and sufficient: no
retry and no other tier can add a name to that file, and every other
file still links. Both import methods now copy it and carry on, which
is what pnpm v11's `linkOrCopy` ends up doing.
`std` maps `EMLINK` and `ERROR_TOO_MANY_LINKS` to
`io::ErrorKind::TooManyLinks`, so the check needs no raw code and the
tests run on every platform.
Three warm-install optimizations under the global virtual store.
`node_modules/.package-map.json` is written only when
`nodeExperimentalPackageMap` is set. Nothing reads it otherwise: `pnpm run`
and `pnpm exec` only hand it to Node under that setting. The gate was already
on two of the TypeScript writes and not the rest; it is now uniform, and in
one place per stack.
Dependency-graph digests are written straight into a reused buffer instead of
being built as a `serde_json` value and serialized. The digests are unchanged
- they name directories in the store and are shared with the TypeScript CLI -
and two tests hold each hand-written serializer to the `hash_object` of the
value it models.
The derived map of snapshot -> slot suffix is cached in the cache directory,
next to the lockfile-verification cache, as derived state a run may always
recompute. Only the restore path may use it: the key names the lockfile file,
so an install that just resolved would otherwise be served the map of the
lockfile still on disk and would materialize every package the resolve added
at the flat-named fallback slot. The fingerprint covers the lockfile bytes,
the snapshot keys the caller holds, the engine string, the allow-build policy,
the lockfile directory and a format version.
The map gate also covers the hoisted linker, which writes the file from its
own linker, and an install that writes no map removes the one a previous
install left: the map is handed to Node by existence, so one kept across a
dependency change would describe a `node_modules` that has moved on.
Adds a regression test for the completion marker that pnpm/pnpm#14518 relies
on: a slot left half-imported must be repaired, not accepted as materialized.
A warm global-virtual-store restore of the 2710-package benchmark fixture goes
from ~121 ms to ~110 ms over three hyperfine runs.
Windows access denied is ambiguous: an open child handle can prevent a
rename, but permanent ACL and destination conflicts report the same error.
Use a one-second budget for permission errors while retaining one minute
for explicit sharing/lock violations and busy errors. Apply the short
budget to the original operation start and never extend it after a later
error changes kind. Cap sleeps to the deadline and return the last error.
Apply the same policy to v11's synchronous file-rename helper. Add fake-clock
budget tests and native Windows tests for restrictive ACLs, read-only
rename destinations, directory destinations, and temporary child handles.
Enable the Windows shim deletion-error regression test. Isolate the ACL
tests from elevated runner privileges with scoped token changes. Restore the
Node process privileges after the TypeScript ACL assertions.
Remove a redundant return caught by Windows-targeted clippy. Restart the
fetch test worker pool after draining it so later tests can use it.
Closespnpm/pnpm#14682.
Avoid the JVM-dependent Android platform verifier in the standalone CLI.
Cover certificate verification with a local TLS handshake regression that
reproduces the panic under Android emulation without the fix.
Closespnpm/pnpm#14777
## Summary
`Auto` and `CloneOrCopy` treated every `PermissionDenied` from a hardlink or reflink as a malformed call and propagated it. Rust maps both `EACCES` and `EPERM` to that kind, and `EPERM` is what a filesystem returns when it will not perform the operation at all:
- `link(2)` inside a FUSE mount without hardlinks. EdenFS answers every `link()` between two paths in the checkout with errno 1 (measured: EdenFS to EdenFS `EPERM`, EdenFS to btrfs `EXDEV`, btrfs to btrfs succeeds).
- `ioctl(FICLONE)` inside a user namespace with idmapped mounts, which is pnpm/pnpm#14722.
A first install in an EdenFS checkout usually survives, because the store-to-checkout links fail `EXDEV` first and retire the tier before any `file:` package is reached. A repeat install re-imports only the `file:` packages, whose source and target are both inside the checkout, so its first hardlink attempt is `EPERM` and the install dies:
```
× installing dependencies
╰─▶ failed to import ".../nest/libs/auto-logging/src/custom-event-metadata.ts" to
".../node_modules/.pnpm/@nest+auto-logging@file+..._pacquet-stage_.../src/custom-event-metadata.ts":
Operation not permitted (os error 1)
```
Reproduced on 12.3.4 on three hosts, 5 of 5 repeat installs; the same tree under 10.26 and 11.26 installs repeatedly, because pnpm's `linkOrCopy` copies on any hardlink failure except `EEXIST`.
## Change
- `is_call_error` tells the two errnos apart by the raw code: `EPERM` now downgrades the tier the way `EXDEV` and `EOPNOTSUPP` do; `EACCES` keeps propagating.
- The explicit `hardlink` method still surfaces `EPERM`. A filesystem that refuses links would copy every package, which is the disk cost `hardlink` is chosen to avoid, so the error names a method the user can change instead.
- The two syscalls move behind an `FsHardLink` / `FsReflink` seam with a `Host` provider (the DI convention from `CODE_STYLE_GUIDE.md`), so the tests can hand the ladder the errors a CI runner's disk cannot produce. `link_file` and `import_into_fresh_target` keep their signatures.
An explicit `packageImportMethod: clone` or `hardlink` is unchanged and still surfaces `EPERM`; the changeset says so.
---------
Co-authored-by: EJ Campbell <ejc3@fb.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
pnpm 12 ignored Yarn's `workspaces` field in the root package.json
without a word, so a repo converted from Yarn installed as a single
project with no hint about the missing pnpm-workspace.yaml. This brings
v12 in line with pnpm 11's warning, same wording, same trigger.
Related to pnpm/pnpm#2255
---------
Co-authored-by: aron-intframe <powermirror5599@gmail.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cli): honor lockfile:false with onFail download
When devEngines.packageManager.onFail is download, pnpm was still writing
packageManagerDependencies into a project pnpm-lock.yaml even with
lockfile: false or --no-lockfile. Skip the project env-lockfile sync in
that case, and keep version switching by recording the downloaded engine
under the global env instead (Rust) / resolving with save:false (TypeScript).
Fixespnpm/pnpm#14728
Written by an agent (Cursor, GPT).
* fix(cli): gate the pin record on the resolved lockfile setting
The v11 half read `config.lockfile`, which stays unset unless the user
spells it out, so `--no-package-lock` and `packageLock: false` still wrote
a project `pnpm-lock.yaml`. `config.useLockfile` is the resolved value, and
it matches what pacquet's `config.lockfile` already folds `package_lock`
into.
With no lockfile to read, `switchCliVersion` had no recorded resolution to
compare a range pin against, so it asked the registry on every command even
when the running pnpm satisfied the pin. It now returns as soon as the
running pnpm satisfies the pin, which is the version it would have picked
anyway — what pacquet already decides without a request.
Adds filesystem-backed regression tests to both stacks: a pacquet install
and a v11 version switch, each asserting that no project lockfile appears
while the switch still happens.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dr2xYPYA1KseJ8sWgyvrBG
* test(cli): assert the pin still switches with the lockfile turned off
The two disabled-lockfile cases both assert an absence: no plan, and no
project lockfile. Making `lockfile: false` skip switching altogether would
satisfy both, and the target-level test calls `switch_target` directly, so
it would not notice either.
Asserting it end to end is not available: the mocked registry's pnpm
package carries no platform binaries, so a real download switch stops at
`ERR_PNPM_PNPM_ENGINE_IDENTITY_UNVERIFIABLE` before anything observable.
The plan is where the decision is legible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dr2xYPYA1KseJ8sWgyvrBG
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fast lockfile update writes a new importer edge from the version the
lockfile already holds, and its version pick promised to refuse an alias
recorded under a peer-suffixed key, because which peer variant the edge
would take is the resolver's call. It read the `packages:` block, whose
keys never carry a peer suffix in lockfile v9, so the guard could not
fire: a package that had only ever been resolved against a peer looked
like a bare `name@version`, the edge was written at that bare version,
and no snapshot existed for it. The importer then linked
`node_modules/.pnpm/name@version/`, which the virtual store never
creates, leaving a dangling symlink and "Cannot find module" errors.
Read the `snapshots:` block instead. Its keys are the ones an importer
edge can name, and the peer-suffixed ones are exactly the shape the
guard was written for. The TypeScript CLI walks its merged in-memory
`packages` map, keyed by full dep path, which is why pnpm 11 resolves
this case correctly and pnpm 12 did not.
The new-importer path carries an end-to-end test of its own, because it
is the shape the issue reports.
The pick reports whether a snapshot names the version it settled on with
a peer suffix rather than refusing to settle at all, because a peer
suffix only blocks the records that are written bare. Three paths read
it: a new edge in an existing importer, a whole new importer, and a
range move onto another locked version. Each refuses a flagged pick,
with a unit test per path and an end-to-end test that promotes
`@pnpm.e2e/abc` from a transitive to a direct dependency and checks that
the recorded version names a snapshot the lockfile holds and links to a
package on disk. An edge that stays on the version it already names
keeps its suffix, so a narrowed range on a peer-dependent direct
dependency is still absorbed without resolving. The override version
pick shares the function and follows it to `snapshots:`; its own plan
already refused peer-suffixed targets, so its behavior is unchanged.
Two existing tests inserted a registry-qualified key into `packages:`
alone; they now mirror it into `snapshots:`, as any lockfile pnpm
writes does.
Closespnpm/pnpm#14714.
The Rust resolver passed the ordinary dependency-resolution preferences,
including every workspace manifest range, into peer hoisting. The hoister
could combine a satisfying locked peer version with a sibling's incompatible
range, allowing the resolver to choose the newer major. The next dedupe pass
fell back to the original peer range, causing the lockfile to alternate.
Filter non-version selectors when building a missing peer's preference bucket.
This retains the existing update-policy filtering and shared preference maps,
without a second traversal of the lockfile. pnpm v11 does not feed manifest
ranges into peer hoisting and v11.26.0 passes the original reproduction.
Add CLI coverage for stable repeated dedupe/check runs, repair of an
incompatible peer reference, and updates that withhold an old peer pin.
Closespnpm/pnpm#14697.
`pnpm update <name>@<version>` rewrites package.json before the install
(update.rs `matched_direct_rewrite`), so a versioned selector was written
verbatim and the declared operator was lost. The version is now recorded
through `calc_version_range` under the operator the manifest already pins,
reusing the npm-alias splitter so an aliased entry keeps its prefix. The
requested version is still seeded into the resolution, keyed by the name
the entry resolves under rather than its alias, so the lockfile records it
even when the kept range admits a newer version. The alias splitter the
bump path shares recognizes `jsr:` alongside `npm:`, so a `jsr:` entry keeps
its prefix on a requested version and a plain update moves its range.
The npm resolver's `calc_specifier` now prefers the previous specifier's
operator over the requested one, as the TypeScript `calcVersionRange`
does. That path only runs under `update --latest`, which now passes the
manifest entry as the previous specifier; without that a prerelease range
such as `^3.0.0-rc.0` lost its operator.
pnpm 11 is unaffected: `parseWantedDependencies` supplies `prevSpecifier`
from the manifest and the resolver computes the importer specifier.
Fixespnpm/pnpm#14745
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Omit empty package authors from CycloneDX and SPDX SBOM output while preserving nonblank author
metadata in pnpm v11 and v12. Empty names produce invalid SPDX actor strings and useless
CycloneDX author objects. Drop them in the one manifest extractor each stack already routes
every author through, and cover the behavior with Rust CLI, extractor, and command-level
regression tests.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
A frozen install prefetches every lockfile snapshot's store-index row
before the plan runs, and under verifyStoreIntegrity that prefetch
ran check_pkg_files_integrity over every row: one stat per CAS file of
every package in the lockfile. On a warm global-virtual-store restore
the plan then skips nearly every slot, so the ~9k stats on the issue's
76-project repro guarded imports that never happened. pnpm 11 verifies
only the packages it fetches.
The prefetch gains a PrefetchIntegrityCheck mode. The frozen install
uses Deferred: rows are decoded with their maps built but their files
unchecked, and after the plan CreateVirtualStore::settle_prefetch runs
PrefetchResult::verify_rows over the keys of the rows whose files the
install may import, on rayon in a spawn_blocking task, dropping every
row that fails so its snapshot falls through to the per-snapshot
lookup and re-fetch exactly as an eager failure did. Those rows are
the survivors plus any skipped snapshot whose row carries a
side-effects overlay, because the build phase's cache hit materializes
that overlay into the slot. The other skipped snapshots keep their
rows unchecked: nothing imports their files, and their manifests and
requiresBuild flags are read from the row, not the CAS. The
fresh-resolve reuse_from_warm_store path keeps the eager check since
every row it reads is materialized.
In store-dir, check_pkg_files_integrity is split into
defer_pkg_files_integrity (map building, shared with
build_file_maps_from_index) and PendingFilesCheck::verify (the stats
and hashes, with the verified-files cache), so the eager and deferred
paths share one loop. The split costs a filename clone per file in the
map build.
Per warm restore of the repro: stat 9,182 -> 838; system CPU median
1.66 s -> 1.54 s; wall time unchanged within noise on an M-series Mac.
Related to pnpm/pnpm#14540.
Extends the pacquet::install::phase events over the last opaque
segment of the install: project linking, virtual-store prune,
modules.yaml write, current-lockfile save, and workspace-state write
each report their elapsed time, completing the phase decomposition
the recent install-performance series was measured with. Measured on
the benchmark fixtures the apply tail is prune 4 ms + modules.yaml
5 ms + current-lockfile save 26 ms + workspace state 14 ms - all
final-phase writes with nothing left to overlap them with, recorded
here so future work starts from the numbers.
Related to pnpm/pnpm#14231.
Preserving a package's nested `node_modules/` across a forced re-import
moved it with a plain `fs::rename`. In a Docker build that rename fails
with `EXDEV`: overlayfs refuses to rename a directory that still lives
on a lower layer, even when source and destination are siblings, so a
`pnpm fetch` in an earlier layer left the install unable to move what it
had fetched. The install aborted with "Invalid cross-device link".
`pnpm_fs::rename_even_across_devices` falls back to copying the tree
when the kernel reports the paths as cross-device, mirroring pnpm v11's
`renameEvenAcrossDevices`. The copy lands next to the destination and is
renamed into place rather than written over it, so a destination the
staged import already filled still reports its collision and the caller
merges the two directories as it does on one device.
`copy_dirent` recreates symlinks instead of following them, which a
`node_modules/` full of virtual-store links requires; `git-fetcher`'s
checkout copy was doing the same thing and now shares it.
Closespnpm/pnpm#14758
`pnpm audit --fix` and the `minimumReleaseAgeStrict` approval prompt no longer drop the comments of `minimumReleaseAgeExclude` when they append an entry to the list in `pnpm-workspace.yaml`. The rest of the list is now left as written.
The `trustPolicyExcludePrune` and `minimumReleaseAgeExcludePrune` cleanups leave the comments of the entries they keep in place, too.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
`pnpm sbom --sbom-format spdx` wrote `creationInfo.created` with fractional
seconds, which SPDX 2.3 (6.9) forbids — the field must be
YYYY-MM-DDThh:mm:ssZ. Strict SPDX consumers such as spdx-tools rejected the
document.
Truncate the SPDX creationInfo.created timestamp to whole seconds in both
SBOM implementations: the Rust v12 CLI (SecondsFormat::Secs instead of
Millis) and the legacy TypeScript v11 CLI (strip the .toISOString()
fractional-second suffix). The CycloneDX metadata.timestamp in each, which
does allow fractional seconds, is unchanged.
Closespnpm/pnpm#14684
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Parent-relative importer IDs reach the symlink writer as joined paths
containing unresolved parent components. pathdiff counts those components
as directory levels, producing too many parent traversals in the link.
Normalize both inputs in pnpm-fs's relative_path helper using the existing
lexical_normalize utility. This requires no filesystem lookup and preserves
the Windows cross-root fallback. Preserve leading parent traversal after a
Windows drive-relative prefix; only an actual root suppresses it. Add Windows
regressions for both lexical normalization and relative-path calculation.
Cover link creation and reuse at the filesystem layer, plus fresh, frozen,
and forced CLI installs with isolated and hoisted layouts, two workspace
root depths, and scoped and unscoped dependencies. Both new regressions
failed before the fix and passed afterward.
Closespnpm/pnpm#14726.
Run the workspace manifest prune pass after every install that writes the lockfile.
The pass lives in Install::run_inner, the hook every success path of the install driver returns through, because the frozen --lockfile-only finisher rewrites the lockfile after the fast update absorbs a removed dependency and returns before materialization. run_inner_impl reports an InstallRunOutcome so the repeat-install short-circuit, which writes nothing, skips the lockfile re-read. The pass runs only for whole-workspace runs that may touch the workspace manifest: add, update and remove run it themselves after the install.
Adds pacquet integration regressions for install, dedupe, and a removed dependency for pnpm/pnpm#14759.
---------
Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
A side-effects row is recorded whenever a build script ran, including a
build that changed nothing inside the package directory. `calculate_diff`
returns `added: None, deleted: None` for that case and the row is stored
anyway, so a package whose whole build effect lands elsewhere -- a
git-hook installer, a script seeding a shared download cache -- gets an
empty row.
`build_side_effects_maps` turned every row into an overlay, so an empty
one became an overlay equal to the base files and the `is_built` gate
took it as a hit. The scripts were skipped and nothing was materialized
in their place: the package directory after such an install is
byte-identical to the pristine tarball, and the build's effect never
happens at all. The cache key does not include the consuming project, so
this is not limited to reinstalls -- a project's first install can hit a
row an unrelated project seeded on the same machine.
pnpm 11 drops the row instead. Its `checkPkgFilesIntegrity` adds to
`sideEffectsMaps` only under `if (added) ... else if (deleted)`, so an
empty row matches neither branch, `sideEffectsCacheKey` is never set,
`isBuilt` stays false, and the build runs. Both stacks write the same
empty row and only their read paths differed, so this drops it at the
same point pnpm 11 does. An entry's presence now means there is build
output to materialize, which is what the `is_built` gate already assumed.
A row carrying only `deleted` still counts as a hit. That build did
happen and its effect is reproducible from the row.
`prefetch.rs` already filters an empty `side_effects_maps` out of the
prefetch result, so a package whose only row is dropped contributes no
entry at all and the frozen-install fast path in `build_phase` still
sees an empty table.
Closespnpm/pnpm#14717
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Fixes https://github.com/pnpm/pnpm/issues/14718
* **Bug Fixes**
* `--no-bail` now works for both single-script and multiple-script selections made with non-recursive patterns.
* Matching scripts continue running after one fails when `--no-bail` is enabled.
* Failure summaries now list failed scripts in selection order and consistently report the overall command failure.
* Running commands from subdirectories with non-npm project manifests now correctly finds scripts in the enclosing npm project.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Legacy deploy initializes its target with a fresh wanted lockfile. Without
source snapshots, resolution chooses the newest registry version even when the
source workspace lockfile already pins a satisfying version.
Build a preferred-version seed from source wanted lockfile snapshots only for
legacy, lockfile-enabled deploys. Keep the target lockfile carrier and output
paths unchanged, and keep the copied post-hook manifest as the sole `.`
importer. Missing or malformed source lockfiles continue to fresh-resolve;
lockfile-disabled and shared/frozen deploys do not receive the seed.
Keep workspace discovery anchored at the source so workspace packages and
pnpmfile hooks continue to work. Add regression coverage for the pinned
version, importer shape, target-local paths, source-lockfile immutability,
hooks, and fallback behavior.
Fixespnpm/pnpm#13857
`InstallFrozenLockfile::run` bound the install-scoped git source cache
as its own local, which put the method one binding over the cap that
main now enforces. The materialization plan already owns the layout and
link options the install context borrows, so the cache lives there too.
Claude-Session: https://claude.ai/code/session_01GKhjCP4gc5tkVkPVqg4gZi
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The local-prefix walk stops at a `Cargo.toml` or a `pyproject.toml` so
`pnpm add crate:...` edits the nearest Cargo manifest rather than a
`package.json` above it. That bound applied to every command, so in a
directory holding only one of those manifests `pnpm run` reported
`ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND`, and `pnpm bin` and `pnpm root`
printed a `node_modules` path that does not exist — the symptoms
pnpm/pnpm#14622 reported for a plain subdirectory, reintroduced for a
Rust or Python subfolder of a Node project.
The walk now takes its markers from the command. `run`, the script
shortcuts, `exec`, `bin`, `root`, `pkg`, `set-script`, `clean`, and
`purge` have `package.json` or `node_modules` as their only subject,
neither of which a Cargo or Python package has, so they resolve over
pnpm 11's marker set and carry on to the npm project that encloses one.
Every other command keeps the wider set.
While here, move the three imports only
`a_file_whose_name_is_not_utf8_is_refused` uses into that test's body.
The test is gated to non-macOS unix, so as module-level imports they
were unused on macOS, where clippy rejects them.
Closespnpm/pnpm#14664
Share Git source acquisition across resolution and materialization using an
installation-owned cache keyed by the verbatim repository URL and full commit
SHA. Exclude the package subdirectory from the source key, while preserving
package identities and store-index keys.
Coordinate each key independently and retain either the verified checkout or
its acquisition failure for the installation. A failed acquisition drops its
partial checkout before publishing the error. Keep the cache out of persistent
CLI/NAPI state so another installation starts with a fresh cache.
Copy the complete checkout, including Git metadata, before preparing a package.
Preserve symlinks and executable permissions without using writable hardlinks.
This retains repository context for scripts while isolating package preparation
and packlist processing. The trade-off is an additional temporary snapshot and
local copying; no cross-install Git cache is introduced.
Test concurrent and sequential reuse, repository/commit/URL isolation,
preparation isolation, failure propagation, cleanup, and warm package-store
reuse. Also scope a platform-specific test's imports to the platforms that
use them so strict macOS lint can run.
Closespnpm/pnpm#14725.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Bring every production function under the 12-binding cap and enable the
`too_many_local_bindings` dylint rule (`max_bindings = 12`,
`exempt_tests = true`).
Each oversized function reads its inputs' fields in place, takes owned
fields before the borrows it needs, and hands its remaining locals to a
named step or a typed view: the virtual-store creation, the workspace and
peer resolvers, the tarball ingestion, the npm resolver's mirror and
result assembly, the NAPI bindings, the config load, the pnpr resolver
and OCI/publish handlers, and the CLI commands. Behavior, error precedence
and allocations are unchanged.
pnpr's streamed resolve gained `#![recursion_limit = "256"]`: with the
engine's futures split into steps, rustc overflowed proving the spawned
future `Send`. The config-to-executor `scriptsPrependNodePath` mapping now
lives once in deps-restorer.
Related to pnpm/pnpm#14562.
Bring pnpm-package-manager fully under the twelve-name cap.
Run update, add and remove as steps over one Copy view of their inputs,
building each install through one function and taking the recursive
selection as SelectedProjects. Bundle the add's resolution inputs and
shared picker and caches, and resolve a selector as parse, save
specifier and catalog outcome. Build the lockfile's importers, nodes
and metadata as steps. Share the prefetch context with its spawned
tasks through an Arc. Extract the remaining single steps: the patch
candidate scan and git-hosted re-fetch, the client tarball download,
the repeat-install manifest drift, the local-tarball lookup, the spec
bump sink, the override rewrite target, the stale-convergence ranking
and the early materializer's required edges.
Related to pnpm/pnpm#14562.