Replace Python's first-index search with explicit package namespace claims.
Normalize exact names and prefixes using Python package name semantics,
reject ambiguous claims, and select one index before fetching metadata.
Record canonical routing inputs in Python lockfiles so route changes
invalidate replay. Restricted registries do not implicitly enable PyPI.
Use local resolution for restricted or multiple routes because the pnpr
Python resolution protocol currently represents one whole index.
Reuse pnpr's package pattern validation and preserve existing npm scope
and prefix routing and Cargo's single-index configuration. Python support
is experimental, so release this change as a patch.
Make existing Cargo diagnostic assertions tolerate output wrapping and
isolate the import and pack plugin fixtures from user npm scope routes.
`pnpm add` reads a `pkg:` selector as the Package URL specification
defines it and routes it to the ecosystem its type names: npm to
package.json, cargo to Cargo.toml, and pypi to pyproject.toml. Each
purl is rewritten into the selector that ecosystem's own protocol
already takes, so it inherits that path's flags, defaults, and errors
instead of growing a second set.
Every decoded component is validated before the pieces are joined back
together. A name followed by a spec is how pnpm spells an npm alias, so
a name or a version carrying a decoded at-sign or colon would otherwise
redirect the install to another package. Qualifiers and subpaths are
rejected rather than dropped: a repository_url qualifier or a subpath
that pnpm ignores installs something other than what the selector names.
AddArgs::package_names now holds the selectors the npm add path receives
rather than the ones argv carried, which is what lets a purl reach the
--global and --config forms as well. AddPipeline keeps only the selectors routed
to another ecosystem, so the split has a single home, and
dispatch_install::add delegates that split to a named step to stay
within the local-binding limit perfectionist enforces.
A purl always names a package to install. pnpm add node@22.0.0 records a
runtime and pnpm add npm@11.0.0 records the project's package manager,
both from a bare name a person typed, so the package-manager and runtime
recognition skips a selector a purl was rewritten into.
That mark belongs to the request rather than to its text. package_names
holds AddRequest values, each carrying its own selector and whether a purl
was rewritten into it, so two requests that normalize to one selector keep
their own meanings and no second list has to be matched against or kept in
sync.
A rejected selector is quoted back by what it decodes to, because an
encoded separator would otherwise hide a user:pass@ authority from a check
made on the raw text. What decoding leaves unreadable to that check is
stopped at instead: a colon followed by an at-sign is how a URL spells
userinfo, and nothing pnpm installs pairs the two past its protocol, so no
list of the characters that can sit between a scheme and its slashes has to
be complete.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
`supportedArchitectures` stood for the cross product of its `os`, `cpu`
and `libc` axes, which cannot express the platform set a workspace
usually ships: Linux x64, macOS arm64 and Windows x64 is three platforms,
but as axes it is six, including the three nobody asked for. The setting
now also reads as a list of the platforms themselves.
An entry parses either as pnpm's own `<os>-<cpu>[-<libc>]` spelling, the
one `pnpm pack-app --target` already takes, or as the Rust target triple
of the same machine, the one uv and the rest of the Python tooling use.
Both canonicalize to one value, so two spellings of a platform are one
platform rather than two. On Linux the C library field also carries a
Python wheel ABI baseline, as in `linux-x64-manylinux_2_28`, which is the
Python-specific dimension the axes have nowhere to put. A Linux platform
that names no C library is the glibc platform, as `linux-x64` means
wherever else it is written.
This lets `python.platforms` go. It named the same thing in a spelling of
its own, in a per-ecosystem setting, while the setting that says which
platforms an install prepares for was already there. `pylock.toml` is now
resolved for the platforms `supportedArchitectures` names, so a workspace
declares its platforms once for its npm and its Python dependencies both.
The axes keep their meaning for a project that writes them, Python
included, where they stand for every platform they cross into.
The optional-dependency check judges a listed platform as a whole rather
than an axis at a time: `[linux-x64, darwin-arm64]` refuses a package
built for darwin x64, where the axes take it because each axis matches on
its own. The dlx cache key and the runtime-archive selector read the new
form too, the latter preferring the listed platform this machine is over
mixing one axis with another.
`python.platforms` and `python.pythonVersions` have not been released, so
this reuses their pending changeset rather than adding a migration.
Related to pnpm/pnpm#14945
Let the members of a uv workspace opt into one Python environment.
A workspace root that declares [tool.uv.workspace] can set
`shared-environment = true` under [tool.pnpm.python]. pnpm then resolves
every member as one graph into one pylock.toml and one .venv at the
workspace root, the way uv resolves a workspace, while each project keeps
an environment of its own by default. Sharing is opt-in because uv's
model, which always shares, forces a repository whose projects genuinely
conflict to split into separate workspaces; per-project resolution needs
no such split.
The installer now prepares units rather than projects: a unit is a
project on its own or the members of a shared workspace, with one
interpreter, one lockfile and one environment. Workspace::memberships
groups the selected projects by the directory they install into, and
selecting any member of a shared environment prepares the whole
membership. The lockfile records the members under tool.pnpm.members and
the interpreter range they accept together as requires-python, so a
member joining or leaving invalidates it. A single-project unit records
neither, so existing lockfiles stay valid.
Two members that require versions of one distribution no offered
release satisfies at once are refused with an error naming both, wrapped
around pubgrub's report, which names the project as one root and cannot
say which members disagree.
An add without --filter in a workspace now discovers the workspace
around the edited project instead of that manifest alone: whether the
project shares an environment, and which projects it may take from the
repository, are declared around it.
pnpm run and pnpm exec resolve the environment the way the install
assigned it: the nearest directory with a manifest is the project, the
nearest workspace declared at or above it is the one it belongs to, and
only a member of a workspace that shares its environment uses the root's
.venv. The dynamic metadata of a shared workspace's members is prepared
with the interpreter the members share, and an add rereads every member
of the units it edits once the workspace lock is held.
The workspace state file is renamed into place through
pnpm_fs::rename_with_retry, so two pnpm processes installing one
workspace at once no longer fail on Windows when the second rename finds
the first still holding the file.
Follows up on item 14 of pnpm/pnpm#14945. Closespnpm/pnpm#15015.
Use the existing packageImportMethod setting and importer for unchanged Python
wheel files in project and isolated build environments. Remove python.linkMode
and PythonLinkMode rather than maintaining a second method vocabulary and
fallback implementation.
Return the actual import method from the shared low-level importer so Python
can restore wheel executable permissions without chmodding hardlinked store
files. Scope Python fallback tiers per source filesystem and environment, and
use private imports for isolated build requirements. Respect unpacked wheel permissions even when a data filename ends in
the CAS -exec suffix. Preserve private generated metadata and rewritten scripts.
Update the pending Python file-sharing release note and internal documentation.
Cover workspace settings, CLI overrides, auto selection, strict clone failures,
write isolation, and unpacked executable permissions.
Related to pnpm/pnpm#14945 and pnpm/pnpm#15006.
Import unchanged Python wheel files using the existing reflink library and add
python.linkMode with copy, hardlink, and reflink modes. Default to copy-on-write
cloning with a copy fallback to preserve isolation from runtime writes.
Keep generated metadata and scripts requiring content or permission changes
independent. Reserve generated RECORD destinations before deferred imports so
wheel data cannot collide with installed metadata. Apply the setting to project
and isolated build environments.
Reuse verified wheel RECORD hashes for unchanged files, preserve executable
permissions for unpacked wheels, and cache copy fallback per source filesystem
within each environment. Fall back to private copies when restricted containers
deny cloning. Reuse the shared filesystem reflink capability in both importers.
Website documentation is in pnpm/pnpm.io#936.
Addresses file sharing from item 14 of pnpm/pnpm#14945. The issue remains open
for its other requests, including environment directory placement.
The Python participant resolved, locked and materialized every project in
the workspace, whatever `--filter` selected, and `pnpm add pypi:` refused a
filtered selection outright.
The participant now reads every discovered manifest once, so the projects
the selection leaves out still answer what a selected project's declared
sources need, and prepares only the selected ones. A project sharing a
directory with an npm workspace project follows that project's selection,
because the npm project's name is what the workspace knows the directory
by. A project in a directory of its own runs through the same selector
engine the npm side uses, against the distribution it declares, its path,
and the `[tool.uv.sources]` edges to its siblings.
An empty `--fail-if-no-match` selection is no longer raised while
resolving the npm projects: the install decides once the Python selection
is known, so a selector that names only a Python project is a match, and a
workspace whose projects are all Python is not reported as holding none.
Dynamic metadata is now prepared only for the projects the install reads,
and `pnpm add pypi:` names the directory it found no Python project in
instead of failing on a missing file.
Closes item 12 of pnpm/pnpm#14945
A project whose `requires-python` no interpreter on the machine satisfied
was reported and left uninstalled, and a `.python-version` naming a version
the machine did not have was warned about. Four of the eight repositories in
pnpm/pnpm#14945 pin an interpreter that way, so the install stopped before
resolving anything.
pnpm now installs one. The builds are python-build-standalone's, which uv,
rye, hatch and mise install too, so the interpreter a Python developer
expects is the one that arrives. A release holds an interpreter of every
version line it supports, and its `SHA256SUMS` is both the list of what pnpm
can install and the digest every download is checked against; it is cached
for a day. The newest build the project accepts is the one installed, so a
range takes the newest release inside it and a pin takes the version it
names.
An interpreter lands under `<store>/python/`, shared by every project and
repository on the machine, and is found there afterwards like any other
interpreter: a later install uses it without reading the release at all, an
offline one included. It is unpacked beside where it belongs and moved
there, so a directory under `python` is one an install can use and never a
download that stopped halfway, and two installs racing for one build end
with the interpreter either of them unpacked.
`downloads: never` keeps pnpm from installing any, and an offline install
installs none. Both report the project, saying which of the two refused.
`downloadUrl` names a mirror of the releases.
Related to pnpm/pnpm#14945.
Add extra Python index URLs with ordered first-index selection, per-URL
credentials, cached 404 responses, and offline reuse. Do not merge versions
across indexes or fall back after authorization and other non-404 failures.
Apply PEP 508 overrides and constraints throughout Python dependency
resolution and locked graph validation. Inherit uv dependency rules from
the declared workspace root and combine them with pnpm workspace settings.
Persist sanitized index URLs and normalized dependency rules in lockfile
inputs so frozen installs reject changes to resolution settings.
Keep resolution local when the pnpr protocol cannot represent these inputs.
Keep project dependency rules separate from isolated build requirements.
Reuse the network crate’s authentication encoder alongside the shared HTTP,
archive-store, hashing, and filesystem infrastructure.
Related to pnpm/pnpm#14945, item 9.
Resolve Python git and direct wheel requirements in the client rather than
silently replacing uv source declarations with index packages. Preserve
manifest version constraints, extras, markers, and workspace inheritance.
Pin git sources with PEP 751 vcs records and record wheel SHA-256 hashes.
Reuse the isolated PEP 517 builder, verified wheel CAS ingestion, git
transport policy, and atomic cache publication. Require approval before
running code from a git dependency or its build requirements. Preserve
submodules in cached checkouts and prevent recursive build dependencies
from deadlocking.
Keep source identity in wheel reuse and in lockfiles covering multiple
target environments. Record PEP 610 origin metadata for git and URL installs.
Let pnpr return a specific client-resolution response for transitive URLs.
Add CLI coverage for direct and uv sources, git revisions and subdirectories,
legacy projects, workspace inheritance, integrity and identity failures,
build approval, recursive builds, submodules, platform-specific sources,
backtracking between direct and index dependencies, requirements-file URL sources, and frozen offline replay. Document the supported forms and add a pacquet
minor changeset.
Related to pnpm/pnpm#14945, item 6.
`python.executable` named one interpreter for the whole workspace, and a
project whose `requires-python` that interpreter did not satisfy failed the
install. A monorepo whose projects support different Python versions had no
way to express that, and the `.python-version` file four of the eight
repositories in pnpm/pnpm#14945 pin their interpreter with was not read at
all.
Each project now gets the first interpreter this machine has that its
`requires-python` accepts. `python.executable` still names one for every
project when a workspace wants that, and is the only interpreter tried when
it is set, so its range check stays a hard error.
The search runs in two rounds so that a project constraining nothing starts
one interpreter, as it did when pnpm always ran `python3`. The first round
is `python3`, `python`, and the requested version by name (the launcher's
`-3.13` on Windows); the second reads the PATH for every `python3.<minor>`
and asks the Windows launcher what it can run, newest first. Each is a
candidate by its own path, so one minor version installed in two directories
is two candidates, while paths reaching the same file are one. Probes and
the scan are cached per install, so a workspace of projects that accept the
same interpreter pays for one.
Interpreters inside the workspace are not searched. An install started from
a script has the workspace's `node_modules/.bin` on its PATH, where a
dependency could otherwise ship the interpreter that installs a project.
The nearest `.python-version` file, searched from the project up to the
workspace root, asks for a version that `3.13` reads as every 3.13.x. It is
a preference rather than a requirement: pnpm cannot install an interpreter
yet, so a pin this machine cannot meet warns and installs with one the
project's own range accepts instead of failing an install that worked
before. The file belongs to pyenv and others too, so a line naming a
distribution rather than a version is reported and ignored.
When nothing fits, the error names the range, the pin, and every
interpreter that was tried with the version it reported.
Related to pnpm/pnpm#14945.
pnpm v12 resolves Cargo dependencies itself rather than shelling out to
cargo, so it is a second implementation of cargo's resolver, and being a
drop-in replacement means producing the versions cargo produces rather than
merely producing a lockfile. Unit tests pin that against synthetic indexes,
which is where the rules are easiest to state and easiest to get subtly
wrong: the resolver fixes in pnpm/pnpm#14952, pnpm/pnpm#14960,
pnpm/pnpm#14961 and pnpm/pnpm#14962 were each confirmed by hand-checking a
real workspace against cargo after the unit tests were already green.
This is that hand-check, written down. For each workspace in the corpus it
resolves twice, compares the locked crates, and asks cargo whether it
accepts pnpm's lockfile with `--locked` in a copy pnpm never touched, so
the lockfile is measured apart from the source replacement written beside
it. Requirements are open rather than pinned: both resolvers read the same
index on the same day, so an upstream release changes what they agree on,
not whether they agree.
A workspace declares whether the two are expected to agree yet. A known
gap reports as such and does not fail the run, but does fail if the two
start agreeing, so a fixed issue cannot leave a stale expectation behind.
`feature-activated-deps` is the one gap today, tracking pnpm/pnpm#14978.
It runs on a daily cron rather than per-PR because it reads the live index,
where a yank or a release can change a result with nothing in this repo
changing.
A requirement naming a Python project in the repository was resolved from
the index. Where the index served nothing under that name the resolution
failed with an empty version set, and where it served something the
install silently got that package instead of the project being edited.
Such a requirement is now satisfied by the project it names, declared
under `[tool.uv.sources]` as `{ workspace = true }` or a path, the table
Python workspaces already write. A member inherits the table its
workspace root declares, and `[tool.uv.workspace]` says which projects a
workspace contains; without one, every project pnpm discovered is one it
may link. A requirement naming a project in the workspace that no source
declares is refused, because resolving it from the index would install
different code under the same name.
Projects are built by running the PEP 517 backend each one declares, in
an environment holding only what that backend asked for, including the
requirements a backend can name only once it can see the project. This
replaces reading the source layout out of the manifest: uv runs the
backend, and a manifest cannot say where a poetry-core project keeps its
modules, what a build hook generates, or what maturin compiles. The
project's own package is built the same way, so a dynamic version is now
the version its backend computes rather than a reason to skip it.
A backend is code from the index that a build executes, so pnpm runs one
only where `allowBuilds` names it, as it does for a dependency's build
scripts. The key is the Package URL `pkg:pypi/hatchling`, because that
setting is shared with npm and both indexes publish names such as
`esbuild`, `ruff` and `black`. An install that has not approved a backend
does not build the projects needing it, which `strictDepBuilds` makes an
error.
A built project is installed editable unless the source says otherwise,
and recorded in `pylock.toml` as a PEP 751 directory package whose path
is relative to the lockfile. PEP 610's `direct_url.json` records where it
came from, which a wheel built from a directory cannot carry itself.
A build backend may write to stdout, so the host helper answers pnpm on a
copy of the descriptor and gives the operation stderr. Reading the built
wheel belongs to the interpreter it is installed for, not to the
environment the backend needed.
Related to pnpm/pnpm#14945.
pnpm pack now writes tarball entries grouped by file extension and file name, the order npm-packlist uses to shrink archives. Packages with many same-named files, such as project template collections, pack much smaller: create-vite-extra went from 823 KB to 89 KB. A workspace LICENSE file or a composed CHANGELOG.md is no longer appended at the end of the archive
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every file an index lists is parsed when a distribution's candidates are
read, so one release publishing a `Requires-Python` that is not a version
specifier aborted the whole resolution. `openpyxl` 3.0.0 through 3.0.7
publish `>=3.6,`, which put `openpyxl` out of reach of every project, at
any version: a release is immutable, so nothing the project does can
correct the value.
An unreadable `Requires-Python` is now read as if the release declared no
interpreter range, which is what pip does. All three readers of the field
agree on that: the index page a candidate comes from, the wheel metadata a
resolution steps through, and the marker a solved lockfile writes.
Closespnpm/pnpm#14910
RUSTSEC-2026-0285: rustls accepted TLS 1.3 handshake messages sent at the
wrong encryption level when they followed a key-changing message in the same
record, where RFC 8446 section 5.1 requires the connection be closed with an
`unexpected_message` alert. The handshake transcript stays authenticated, so
this cannot be used to alter or complete a handshake. The effect is that a
peer can send handshake messages in plaintext that should have been
encrypted and rustls does not reject the connection.
`cargo update -p rustls` stops at 0.23.43, still inside the affected range,
so the bump is pinned with `--precise 0.23.45`. That carries aws-lc-rs,
aws-lc-sys and rustls-webpki forward with it. aws-lc-sys compiles C in its
build script, so its jump from 0.41 to 0.45 is the part of this worth
watching on the macOS and Windows runners.
Claude-Session: https://claude.ai/code/session_01L9wCbFDne39yxkpZQLVZmQ
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.
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.
* 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>
* 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
`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`.
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
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
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
Reuse package manifest paths returned by Cargo metadata during workspace
root discovery. A 65-package workspace needs one metadata invocation rather
than 65. Preserve discovery of independent nested workspaces.
Canonicalize discovered manifest paths and Cargo metadata paths before
membership filtering so relative paths and lexical aliases reuse metadata.
Queue directory paths and open each from the pinned workspace root. Native
whole-path opens reject every symlink/reparse point: openat2 with
RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS on Linux, O_NOFOLLOW_ANY on macOS 11+,
and NtCreateFile with OBJ_DONT_REPARSE on Windows. Keep handles bounded and
issue one navigation open per directory without ascending or comparing file
identities. Narrow unsafe wrappers use existing libc/windows-sys dependencies.
Older Unix kernels and other Unix platforms use a component-by-component
no-follow fallback with bounded handles but depth-dependent open counts.
Detect older macOS kernels before using the flag, because they may ignore
unknown open flags. Skip transiently removed/unreadable nested directories.
Normalize expected test paths for macOS temporary-directory aliases and Windows
short paths. Verify the limited-descriptor child test actually runs one test.
Cache generated Cargo checksum files in the existing store index, keyed by
the archive checksum and the verified source-file mapping. Validate the
cached checksum CAS file before reuse. Regenerate missing or corrupted
entries and invalidate the cache when source mappings or archive checksums
change. Keep source integrity checks and materialized-slot repair intact.
On the repository's Cargo-only dependency graph, 35 interleaved warm runs
measure 90.5 ms for pnpm install versus 176.0 ms for cargo fetch. Synthetic
warm installs fall from 138.3 ms to 29.1 ms with the same release settings.
Add browse=true to the npm and Cargo search APIs. Reuse hosted discovery,
routing, authorization, result projection, and pagination. Skip upstream
search in browse mode. Preserve ordinary empty-search behavior.
Cover pagination and totals, named registry mounts, registry and package
access restrictions, routed-away packages, and zero upstream requests.
Document the endpoints and add a pnpr minor release note.
Serialize capped libsql registration transactions on the shared connection.
Use immediate transactions and bounded retries for SQLite lock conflicts
across independent backends. Retain the atomic counter as the user-limit
authority. Cache the bcrypt hash within each registration request.
Test independent databases opening the same file, lock release, extended
SQLite error codes, and retry behavior through the real libsql HTTP client.
Close temporary-file handles before atomic replacement and retry transient
Windows file locks. This fixes concurrent task-cache publication failures.
Add registration and Windows cache-write patch release notes.
A Cargo workspace that points a crate at a git revision, through
`[patch.crates-io]` or a git dependency, failed to install: every
non-registry source in `Cargo.lock` was rejected before it was fetched.
pnpm now checks out the commit the lockfile pins and vendors the crate
into the store the way `cargo vendor` lays a git package out — the
package's own directory, a `.cargo-checksum.json` with a null `package`,
and a Cargo directory source that replaces the git source. `cargo` then
builds the pinned revision offline, and `--frozen-lockfile` leaves the
lockfile and its revision untouched. Vendored git packages get their own
`.pnpm/crates/git` directory: a git package and a registry crate can
share a name and version, and one directory source cannot hold both.
A vendored manifest resolves the `workspace = true` fields its repository
supplied and drops the `[workspace]` table, so the directory stands alone
the way `cargo vendor`'s does. The store slot is keyed by the commit, so
a second install links it without cloning again, offline included.
Resolving a workspace that declares `[patch]` or `[replace]` is refused
rather than silently resolved without it: `cargo metadata` does not
report either table, so the lockfile would name the replaced package.
Closespnpm/pnpm#14691
Registry metadata mirrors were keyed on host[:port], so several registries
served from one host under different path prefixes shared one directory and
could answer with each other's versions, integrity hashes and tarball URLs.
Both stacks now key the mirror on
<scheme>%3A+<host>[+<port>][%2F<path>][%5F<sha256>]. The host and every path
segment are percent-escaped down to [A-Za-z0-9._-], so a `%` or `+` in a key
is always one the encoder wrote and the key can carry no path separator, no
character Windows rejects in a filename, and no glob metacharacter — the
cache commands feed the key to a glob whose matches `pnpm cache delete`
removes. The scheme is included because http and https at one host and path
are two different trust domains: metadata served over http can be rewritten
in transit and must never reach a resolution configured for https. The
separators are percent-escapes because every key pnpm wrote before this
change was a URL host, which can never hold a `%`; no new key can therefore
land on the stale directory of an unrelated one whose hostname held a
separator, which would otherwise let `https://nexus/npm/` read what was
cached for `https://nexus_npm/`. A path that is not all lowercase gets a
sha256 suffix, the guard encodePkgName already applies to package names, so
HFS+ and NTFS cannot merge two registries; a trailing `.` is escaped because
Win32 strips one; and a key too long for a 255-byte filename is replaced by
its own hash. Only the one trailing slash the resolver itself appends is
normalized away; a repeated slash reaches the registry as a distinct request
path and stays in the key.
Every cache directory is renamed, so the first install after upgrading
refetches registry metadata once. The package store is untouched.
`pnpm cache view` decodes the key back to the registry rather than replacing
`+` with `:`, the registry URL is redacted in both stacks' errors, and the
Rust diagnostic codes now match pnpm's.
Closespnpm/pnpm#13558.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
A run record is the account `pnpm pipeline` gave of one run: testimony about
something that happened once, not derived data a replica can rebuild. It lived
on the replica that received the submission, so behind a load balancer the
pipeline UI showed a different history depending on which replica answered,
and a scale-to-zero container took its records with it.
Run records now live in the hosted store, beside the packages every replica
shares. They keep their append-only rule across replicas: the create-only
write the object store and the local backend both offer decides the winner, so
a run id recorded on one replica is refused on every other.
The staged-publish namespace already needed read, create-only write,
compare-and-set replace, remove and list on a reserved namespace of the hosted
store. That is now the backend's record API, addressed by namespace and key,
with staged publishes and run records as its two callers rather than a second
copy of the plumbing. `Storage` names the namespaces, as it already did for
`.staged`.
On a local storage root the records move from `pipeline-runs/v0` to the
reserved `.pipeline-runs/v0`, which also takes them out of reach of a package
named `pipeline-runs`.
Related to pnpm/pnpm#12199.
Add operator-configured OIDC providers, explicit subject/claim bindings, and
separate browser and workload authentication paths to pnpr. Use the approved
openidconnect dependency for discovery, authorization code exchange, and
signature verification, with pnpr checks for authorized party and token times.
Keep OIDC sessions short-lived and out of the persistent token stores. Bind
browser login to a signed HttpOnly Secure cookie, PKCE, nonce, and one-use state.
Anonymous login starts allocate no server-side state. Bound callback attempts
and concurrent exchanges. Bound successful sessions,
discovery response sizes, deadlines, and JWKS refreshes. Restrict discovery
destinations to public IP addresses through the shared system DNS resolver.
Keep valid cached keys available while a refresh performs network I/O.
Redact callback query parameters from request logs.
Accept prefixed workload ID tokens as registry credentials without token-store
lookups, restricting them to exact packages in an explicitly named hosted npm registry and enforcing the
normal publish ACL. These credentials cannot mint durable pnpr tokens or use
account, unpublish, batch, or other protocol endpoints.
Document provider setup, limitations, and a GitHub Actions workflow that needs
no long-lived registry secret. Cover signed-token validation, claim constraints,
login replay and expiry, discovery caching, and request restrictions with tests.
Complete the OCI registry's remaining hosted and upstream workflows.
Reuse the verified streaming cache and publish journal. OCI batch entries
carry a base64 manifest and reference layers uploaded through /v2/ first.
Upstream tokens are cached by repository with bounded expiry and capacity;
credentials are restricted to the configured origin and trusted token service.
Bind short-lived OCI credentials to the original token's hash and the
addressed registry path. Check the original token's restrictions and current
registry policy on subsequent requests. Scoped tokens cannot access other
pnpr APIs, and mounts require pull scope for the source repository.
Maintain a separate filesystem package index, migrating legacy stores once
at startup. Catalog requests can find nested repositories without walking
through the parent's layer population. Prune index entries after removal
or failed creation. Uncached manifest probes use upstream HEAD, falling back to a verified GET
when legacy registries omit the digest header. Integrity failures abort shared
blob streams, and OCI batch validation preserves the original registry errors.
Explicit blob deletion reserves a marker in the repository document before
removing bytes. Increment its generation so a staged or recovered publish
cannot restore a manifest referencing the deleted content. An interrupted
deletion blocks new publishes until offline oci-gc finishes it. All replicas
must be upgraded before using online deletion. Filesystem stores retain
their existing exclusive-owner requirement.
Add regression coverage for scope and expiry checks, credential revocation,
cache poisoning, upstream authentication boundaries, cumulative upload
limits, nested catalogs, batch rollback, interrupted deletion, and a
manifest commit racing blob deletion on another replica.
Closespnpm/pnpm#14630.
Adds OCI as a fourth ecosystem beside npm, Cargo, and Python: a hosted
image registry that docker, podman, and skopeo push to and pull from.
The distribution API cannot be moved under a path prefix. A client
derives the API root from the image reference's host, so
`pnpr.example.com/acme/app:1.0` always requests
`/v2/acme/app/manifests/1.0`. `/v2/` therefore mounts at the host root
whatever else is served, and the repository name alone selects the
registry through the declared-provenance rules the other surfaces use.
Artifactory has to burn the first path segment as a repository key for
want of that invariant; pnpr does not, so the image name stays the image
name. `/oci/v2/` and `/oci/~<name>/v2/` are served too, for podman and
containerd, whose registry configuration does accept a path.
Repository names are `/`-joined, which the pattern language had no shape
for. `PackagePattern::parse` now takes the ecosystem and offers each one
only the wildcard its names can carry: npm keeps `@scope/*` and `@*/*`,
images get `<namespace>/*` over a single leading component, and Cargo
and PyPI get neither, since a flat name could never match one. A
`packages:` key is normalized through that language rather than through
the name rules, so a wildcard key parses as itself.
Blob uploads stream to a local file across requests instead of buffering
in memory, and are verified against the promised digest before anything
is stored. The manifest write is the commit point and rides the existing
publish journal; blobs land outside it, content-addressed and invisible
until a manifest names them, which is what leaves unreferenced blobs for
a collector rather than half-publishing a release. A tag carries the
time it last moved, so a transaction recovered after a crash cannot drag
one back to an older manifest.
`Basic` credentials now carry a token as the password under any
username, which is how `docker login` sends them, and `GET /v2/`
challenges an anonymous caller even where reads are open: a client
settles its authentication scheme on that one response, so a 200 would
leave it no way to authenticate a push.
Verified end to end against docker 29.7.2 (login, push, pull, run),
podman 5.8.4, and skopeo 1.22.2, plus an anonymous pull and a push
refused after logout. The two client stacks take different upload paths
and between them cover both: moby sends a blob monolithically, while
containers/image chunks it.
Not yet served: proxying an upstream image registry, blob collection,
the referrers API, cross-repository blob mounts, and ranged blob
downloads.
Compose frozen installation, affected-project selection, workspace task
scheduling, output caching, and run reporting in pnpm pipeline. Add a
polling watch agent and an authenticated pnpr run-record service.
Keep completed task results separate from Cargo incremental state.
Opted-in Cargo tasks share immutable snapshots across linked worktrees,
materialize private writable targets, and always execute. Keep snapshot
storage separate from installed-package storage so eviction cannot break
existing builds or installations.
Key Cargo snapshots by repository inputs and compilation environment.
Invalidate local-unit and build-script freshness after relocation, and
invalidate freshness after content changes even when mtimes are retained.
Use staged, integrity-checked restoration and coordinate pipeline writers
through locks outside the disposable cache.
Document the prototype's input, storage, and remote-service limitations.
Add regression coverage for worktree reuse, relocation, deletion,
concurrency, copy fallback, and invalid snapshot handling.
Related to pnpm/rfcs#22, pnpm/rfcs#23, and pnpm/rfcs#25.
writeEnvLockfile refused a symlinked pnpm-lock.yaml at the read, before
anything had decided whether the write would change a byte. An install
recording config dependencies therefore failed in the build sandboxes
that stage the lockfile as a symlink, even when the env document it
would write matched the one on disk.
Both stacks now order the env write the way the main-document write is
already ordered: follow the symlink on read, build the combined
document, return early when it equals what is on disk, and refuse the
symlink at the publication step. EnvLockfile::write mirrors
save_value_to_path; writeEnvLockfile mirrors writeLockfileDoc.
Dropping the no-follow read left it with no callers on either side, and
libc was in pnpm-lockfile's dependencies only for its O_NOFOLLOW and
ELOOP handling.
The BOM half of the issue is TypeScript-only: extractMainDocument
rewrites CRLF but does not strip a BOM, so a BOM-prefixed lockfile was
taken for a main document and the old env document ended up inside it.
extract_main_document normalizes through normalize_lockfile_content, so
the Rust test added alongside is a regression guard rather than a fix.
Closespnpm/pnpm#14372
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
`pnpm add jsr:@scope/pkg` read `jsr:` as the package name and failed with
ERR_PNPM_INVALID_DEPENDENCY_NAME.
Replace the prefix-string scan with a `ProtocolSelector` parse that gives
each protocol the treatment pnpm gives it:
- `npm:<name>[@<spec>]` is the plain registry request it wraps, so it pins
and saves the range `<name>[@<spec>]` would.
- `jsr:@<scope>/<name>[@<selector>]` keys the entry by the JSR name and
saves the picked version back under the protocol (`jsr:^1.2.3`), which is
what the TypeScript CLI records. The version is picked through the
npm-shaped `@jsr/<scope>__<name>` name the `@jsr` registry serves.
- `workspace:` is read with `WorkspaceSpec`, so only the alias form
`workspace:<name>@<range>` names a package. Ranges and paths, Windows
drive letters and UNC shares included, fall through untouched with no path
sniffing of their own.
- `catalog:` is dropped: the text after it names a catalog, not a package,
so it carries no manifest key.
A protocol that spells out a name pnpm cannot declare a dependency under
(`npm:`, `npm:../evil`) is now rejected by name instead of reaching the
resolver as an empty or invalid alias.
Also documents why `workspace_save_specifier` resolves against `*` instead
of the typed range, since that line reads as an oversight and is not one.
Closespnpm/pnpm#14590.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Workspace discovery passed directory patterns to wax without the POSIX path
normalization that pnpm11 receives from tinyglobby. This could omit projects
from installs and recursive commands, or include projects a negation excluded.
Add a POSIX normalization entry point to pnpm-fs, sharing the existing dot and
parent-component reduction with native-path normalization. POSIX separators
are interpreted consistently on every platform, preserving glob backslashes.
Normalize each configured pattern before selecting the existing literal,
one-level wildcard, or generic glob discovery path. Preserve the original
pattern in invalid-glob diagnostics.
The TypeScript implementation already normalizes these paths through
tinyglobby. Add matching TypeScript regression coverage alongside Rust
filesystem/discovery tests and a CLI install/list/recursive-script test.
Fixespnpm/pnpm#14571.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Add a workspace setting for the Cargo sparse-index URL and thread it through local and pnpr-accelerated resolution. Preserve that source in Cargo.lock, read the registry download template, and configure Cargo to use pnpm's vendored directory for the selected registry.
Take the registry source as an argument to the resolver rather than defaulting to crates.io, so local and accelerated resolution record the same source. Accept a lockfile's default-registry source only when the configured index is crates.io, and reject a dependency that names a third-party registry rather than every registry but crates.io.
Keep crates.io as the default and preserve its existing cache and authentication behavior. Bound the registry configuration document, and skip the registry entirely for a workspace with no registry crates. Share Cargo download-template expansion and sparse-index prefixes between the client and pnpr.
Related to pnpm/pnpm#14599.
Centralize npm, Cargo, and Python package-name validation and normalization in `pnpr-package-name`. Make storage and cache keys canonical by construction, and keep journal recovery keyed by the recorded ecosystem.
This removes protocol-crate dependencies from `pnpr-config` and eliminates duplicate normalization in server and resolver call sites.
Related to pnpm/pnpm#14599.
Resolution lived inside the CLI, where it could only run beside an
interpreter: the pubgrub provider read a registry that downloads wheels
and asks `host.py` what they require. A registry server resolving on a
client's behalf can do neither, so the rules move to
`pnpm-python-resolver` and the fetching stays with whoever can do it.
`step` runs one pubgrub pass and either solves the project or names the
one distribution or wheel it still needs, which is the shape the CLI's
loop already had.
`POST /-/pnpr/v0/resolve` reads `"ecosystem": "pypi"` beside npm and
Cargo. The body carries the project's requirements and the interpreter
they are for; the answer is the `pylock.toml` the client writes. pnpr
reads the metadata file an index publishes beside each wheel (PEP 658,
either spelling), falls back to the wheel itself only for an index that
publishes none, and keeps what it read for every client that follows —
so the client stops downloading whole wheels for versions a resolution
then rejects.
The client verifies the answer rather than trusting it: the lockfile has
to record the inputs this install asked about, its wheels are downloaded
and checked against the index's digests, and the project is re-solved
against the metadata of what actually arrived. Server-side, a read that
does not match the digest the index published is refused before it
reaches the solver.
Reads are bounded and scoped as the Cargo path's are: an allowlisted
index origin, per-project route classification, single-flighted cold
reads, and caps on the distributions, metadata reads, each response, and
the bytes one request holds. Asking a server which ecosystems it
resolves is one memo for both ecosystems now, keyed by server and
ecosystem.
Related to pnpm/pnpm#14599.
`POST /-/pnpr/v0/resolve` served npm alone. It now serves every
ecosystem from one address, with the body naming which one it speaks:
an absent `ecosystem` field is npm, as it was for every client written
before this, and `"ecosystem": "cargo"` carries a `cargo metadata`
document plus the sparse index to resolve it against.
The server walks that index in waves — asking pacquet's Cargo resolver
which crate names are still missing, fetching those, repeating — and
answers with the rendered `Cargo.lock`. Index files are cached under the
server's cache directory with the `packument_ttl` npm metadata gets, in
a namespace keyed by registry origin and by the caller's route scope, so
a private index cached for one caller's credential never satisfies
another's fetch. The registry a request names is checked against the
route allowlist before any fetch, and every credential comes from the
server's route policy rather than the request, as on the npm surface.
`pnpm install` and `pnpm add crate:<name>` use it whenever `pnprServer`
is set, so a cold Cargo install no longer fetches one index file per
crate in the graph. The handshake advertises the ecosystems the server
reads; a server that does not name `cargo` leaves the client resolving
locally, which keeps a new pnpm working against an older pnpr. The
request carries the dependency graph alone: `cargo metadata` reports
absolute paths in `manifest_path`, `workspace_root`, every target's
`src_path`, and the package ids, and none of it takes part in
resolution.
There are no per-package frames for Cargo. Resolution is a single
pubgrub solve rather than an incrementally-yielding tree walk, so
nothing is known before everything is, and the response is the terminal
`done` frame alone.
Related to pnpm/pnpm#14599.
The Cargo and Python surfaces reserved a blob, finalized it, then updated
the project document, all outside the commit journal the npm surface uses.
A crash between the finalize and the document write left a blob nothing
mentions: harmless to readers, but inconsistent, and a second copy of a
problem npm had already solved.
The journal now carries a document of any ecosystem. Each entry records
the ecosystem beside the computed document bytes, and the caller supplies
a `HostedDocuments` merge that both the commit and startup recovery run
for that format, so the merge rule stays with the surface that owns it.
Committing is one operation for every surface — seal, apply, remove the
entry — and the apply is exactly what recovery runs, so the npm publish
flow no longer keeps an inline second copy of it.
`store_hosted_artifact` now stages the blob and the document it computed
and commits them together. A blob whose immutable slot another writer
already owns is left out of the document, and a transaction that ends up
recording nothing writes no document at all; the surface reports that as
the duplicate publish it is. A post-seal apply that fails partway re-runs
once from the journal, as the npm flow did inline before.
Related to pnpm/pnpm#14599.
Add Cargo and Python registry protocols to pnpr. Concrete registries declare
an ecosystem, and routing filters mixed router sources by the requested
protocol. Ecosystem prefixes provide default and named registry endpoints
while preserving the original npm aliases.
Reuse hosted document and blob storage, package access rules, upstream
metadata caching, and verified artifact streaming across the new surfaces.
Keep upstream artifact cache identities bound to metadata checksums. Check
metadata before cache lookup so changed or removed entries take effect.
Honor package-specific Cargo privacy rules when advertising auth-required.
Require authentication before reading Python upload bodies. Bound multipart
boundaries and use the existing regex crate for delimiter searches, checking
boundary suffixes so binary lookalikes remain part of the uploaded file.
Reject Cargo archive traversal, links, oversized decompressed streams, and
manifests whose package identity differs from the publication metadata.
Reject package configuration keys that collide after normalization.
Apply route allowlists to Cargo and Python metadata and artifact fetches,
including redirects. Require secure same-origin destinations for configured
headers. Rebuild headers on every approved redirect so cross-origin metadata
and artifact downloads succeed without forwarding upstream credentials.
Share this redirect loop with the existing network metadata fetcher and retain
its request-budget guard while response bodies are read. Transfer permits to
streamed artifact responses so body consumption or cancellation releases them.
Keep one pnpr timeout budget across redirects and the final response body.
Stream through a bounded producer whose deadline runs independently of
downstream polling. Reject hosted Python blobs absent from publication metadata.
Abort publication on an immutable object-store conflict and remove the
conflicting staged file. The filesystem backend remains single-writer;
replicated deployments use the object-store backend. Journaled publication
across all registry surfaces remains tracked in pnpm/pnpm#14599.
Add protocol unit tests and HTTP regressions for publication, routing,
authentication, content negotiation, caching, and integrity validation.
Exercise the multi-ecosystem architecture through a usable Python
integration, not test-only metadata writers.
Keep Python requirement, marker, lockfile and environment semantics
separate from npm and Cargo. Reuse the install-wide HTTP/auth budget,
verified artifact ingestion, CAS and store index.
Extract a shared install lifecycle without ecosystem-specific branches.
Native tasks declare metadata footprints and return prepared projections.
Settle all work before publishing Cargo or Python state, and reverse
attempted publications before restoring metadata on failure.
Retain resources when rollback fails so recovery remains possible.
Enroll npm with its existing in-place materialization semantics, and
keep npm-only dispatch on its early path. Do not unify native resolvers,
lockfile formats, target identity or package layouts.
Consolidate archive ingestion below the ecosystem boundary. Share cache
validation, authenticated requests, extraction retries and publication
across tarballs and ZIPs, retaining tar streaming and ZIP decoding.
Test the shared contracts across both formats and preserve npm fast paths.
Use standard pylock.toml, independently validated with uv.
New features target pnpm v12 only. Shared archive URL-redaction fixes
also cover pnpm v11.
Related to pnpm/pnpm#14566 and pnpm/rfcs#34.
node-semver 2.2.0 parses a partial version after `<=` as the `-0`
prerelease of the version it names, so `<=16` becomes `<=16.0.0-0` and
matches nothing in the 16 line, 16.0.0 included. A peer dependency
declared `>=0.11 <=3` was reported unmet by 3.0.1, and with
`strictPeerDependencies` the install failed. Every range comparison
pacquet makes goes through the same parser, so the bug is not specific
to peers. Closespnpm/pnpm#14419.
The crate reaches ~83 `Range::parse` call sites across ~40 crates, so a
per-call-site rewrite would be both large and easy to regress past.
Upstream has not released since 2.2.0 (February 2025) and has an open
PR untouched since March 2025, so a version bump is not available
either. The fix is a `[patch.crates-io]` entry pointing at
`pnpm/node-semver-rs`, pinned by rev.
The fork carries two commits. The first expands a partial `<=` the way
npm's `semver` does in `replaceXRange`: `<=7.x` is `<8.0.0-0`, `<=0.7.x`
is `<0.8.0-0`, and `<=*` is `*`. The second makes `Ord for Bound` a
total order — an inclusive and an exclusive bound on the same version
each reported itself as the lesser, so `Range::intersect` returned a
different set depending on argument order, and `<1.2.3` intersected
with `1.2.3` yielded `1.2.3`. `intersect`, `allows_all` and `allows_any`
all read that order, and pacquet uses all three. Upstream's own suite
went from one pre-existing failure to green.
`deny.toml` allowlists that one repository as a git source rather than
the whole `pnpm` org.
`semver-include-prerelease` already worked around the `<=` bug by
rewriting the bound before `node_semver` saw it; that half of
`npm_upper_bound` is now redundant and is dropped. The `<` half stays:
npm reads `<18` as `<18.0.0-0`, which only `includePrerelease`
evaluation can tell apart from `<18.0.0`.
The TypeScript CLI resolves ranges with npm's `semver`, which already
expands `<=` correctly, so it needs no counterpart change.
Update `diffy` to 0.5.2 so pnpm v12 can parse patch file headers with CRLF line endings. Add a regression test that applies a CRLF-encoded patch to an LF target file.
Fixespnpm/pnpm#14557.
Move repository manifest enumeration into pnpm-workspace and expose its result as a basename-indexed inventory. The CLI populates that inventory lazily only after non-Node.js installation is enabled, allowing Cargo and future ecosystem installers to share one traversal without putting allocations, filesystem work, or synchronization on npm-only installs.
Keep discovery within the concurrently-polled ecosystem future so Node.js resolution does not wait for the repository scan. Cargo retains ownership of interpreting Cargo.toml files and reducing them to Cargo workspace roots.
Related to pnpm/pnpm#14566.