Follow-up to pnpm/pnpm#15019 for the review items that had no inline
thread when it merged.
pnpm run and pnpm exec compare the configured workspace path and the
command's directory with links resolved, so a workspace reached through
a link still finds the environment its members share.
An add without --filter reads the workspace around the edited project
only when a [tool.uv.workspace] is declared at or above it, which is
where a project can take siblings from the repository or share their
environment. A project outside any declared workspace is read alone, as
it was before, so the common add does not walk the whole workspace.
Shared membership reuses the workspace each project belongs to from the
scope reading, rather than walking ancestors again per member. The
disagreement search compares distinct ranges rather than every member's
requirement, so many members asking for one range add no pairs.
Related to pnpm/pnpm#15015.
Item 14 of pnpm/pnpm#14945, the placement half. Every Python project pnpm
installed grew a `.pnpm/python-envs/` directory of generations beside its
`.venv` link, so airflow's 141 projects got 141 generation directories
where the repository maintains three environments.
Generations now live under `<store>/python-envs/<project>/`, one directory
per project named by the short hash of its canonical path, and `.venv` is
the only thing left in the project: an absolute link to the generation the
project currently runs. Publication is unchanged, an atomic symlink rename
in the project directory, and so is rollback, which republishes the previous
link. The link is a symlink or junction, so the store and the project need
not share a filesystem. Where they do, the clone and hardlink methods of
`packageImportMethod` now always have the environment on the store's
filesystem.
A `.venv` is managed when it links to a generation directory of any
project directory in the store, not only this project's, so a project that
moves keeps its environment rather than being refused as unmanaged. A link
into the project's own `.pnpm/python-envs`, where 12.4 published
environments, is managed too, so an upgrade relinks it on the next install
and leaves the old generations for the user to delete. A link whose target
is gone is replaced: the store can now be removed without the project, and
a link to nothing protects nothing. Under frozenStore, which promises that
nothing under the store is written, generations stay beside the project in
the 12.4 layout, with its real-directory checks. A link to any other directory, or a
`.venv` that is a real directory, is refused as before. The checks that
the project's `.pnpm` is a real directory go away with the directory: an
install no longer writes into the project's `.pnpm` at all.
On Windows the link holds the absolute target through the new
`pnpm_fs::force_absolute_symlink_dir`. The existing helper writes true
symlinks relative to the link, which node_modules relies on and which a
link into a store elsewhere on the disk cannot survive a move with.
Generation collection stays open, as `pnpm/plans/PYTHON_SPIKE.md` records;
a stale generation is now in the store rather than in the project.
Closespnpm/pnpm#15014.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Python override entries now accept `pkg:pypi/<distribution>[@<version>]` identities. pnpm normalizes them at the configuration boundary into the existing PEP 508 requirement representation.
PURL qualifiers and namespaces are rejected. Existing PEP 508 overrides and constraints retain their current behavior.
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.
`manifest` is optional in the `ResolveResult` a pnpmfile `resolvers`
hook returns. pnpm 11 awaits the fetch and reads the bundled manifest
when the resolver supplies none; pacquet had no equivalent, so
`walk::fallback_manifest` synthesized an identity-only manifest and
`extract_children` found no children. The package installed alone, with
no dependencies, no engines, exit 0 and no warning.
`PrefetchingResolver` already fetches an archive during resolution to
learn an integrity the resolution does not carry. Widen that to the
manifest: one read settles both, shared across every edge that resolves
to the same content.
The read now verifies the bytes against a pinned hash. Discovering an
integrity has nothing to check against, but a resolution that already
pins one is read here only for its manifest, and unchecked bytes would
put a tampered archive in charge of the dependency walk. The native
cache key carries the pinned hash too, so a read that verifies is never
served the bytes of one that could not.
Closespnpm/pnpm#15000
Record the maintainer's project philosophy as a shared reference for
implementation, triage, and review. Explain how feature value, abstractions,
compatibility, and user-selectable tradeoffs fit pnpm's core requirements.
Add a repository implementation workflow that checks existing capabilities,
prioritizes deduplication, assesses architectural impact, and validates through
the existing testing and review skills. Update review guidance to require
major releases for stable breaking changes.
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.
Extract the existing PR review-and-fix workflow into a repository skill and
invoke it from the git-wt hook. Commit and push verified fixes automatically,
then reuse the pull-requests skill to follow CI and review rounds to completion.
Preserve the existing PR status and leave merging to the maintainer.
Move the shared conflict-resolution script into the pull-requests skill.
Add task concurrency groups to pnpm v12. A task in pnpm-workspace.yaml
names a `concurrencyGroup`, and `concurrencyGroups` gives each group a
limit: at most that many tasks of the group run at once on the machine,
counted across every pnpm process. A task past the limit waits for a
running one to finish and prints who holds the slots.
A group's pool is N slot files under `<stateDir>/run-slots/<group>/`. A
task takes an exclusive advisory lock on one in `run_stages` and holds it
for every stage of its script, so the single-project run, the recursive
run, and the tasks a `pnpm pipeline` runs in-process are all counted,
each project's task separately. The operating system releases the lock
when the holder ends, however it ends, so a killed holder never leaves a
stale slot. `pnpm exec` names no task and is not gated.
A holder lists its group in `PNPM_HELD_CONCURRENCY_GROUPS` for the
scripts it spawns. A nested `pnpm run` of a task in the same group runs
under the slot its parent holds, which is what keeps a script that calls
`pnpm run` from waiting on itself under a limit of one.
`concurrencyGroups` merges entry by entry across the configuration
layers, so a machine can lower one group's limit through
`PNPM_CONFIG_CONCURRENCY_GROUPS` without restating the rest.
Every process that is meant to be counted has to start as
`pnpm <script>`, so the root manifest gains scripts that wrap the just
recipes and node scripts (`test:rust`, `test:rust-affected`,
`test:rust-smoke`, `check:rust`, `lint:rust`, `ready:rust`,
`pre-push:rust`), the pre-push hook runs its Rust checks through one,
and the agent guide, the testing-changes skill, and the contributing
guide name the pnpm scripts first.
This repository's own task declarations wait for a pnpm release that
knows the field: the pinned CLI rejects an unknown task setting, so
adding `concurrencyGroup` to pnpm-workspace.yaml now would break every
command for it.
Item 5 of pnpm/pnpm#14945. Two of the eight repositories surveyed there
depend on a distribution that publishes no wheel at all: sentry needs
`python-u2flib-server`, which has 15 source distributions and no wheels,
and marimo needs `lzstring`, whose wheels stop at 1.0.3. Both failed
with an empty version set that said nothing about why.
A release whose wheels the target accepts none of is now offered as the
source distribution the index serves beside it, ranked after every
wheel, so a wheel always wins and the archive is downloaded only where
nothing else installs. `.tar.gz` and `.zip` archives go through the
shared artifact pipeline as `ArchiveStoreProjection::RawArchive`, which
gives them the digest check, retries, store dedup and offline replay the
wheel path already has. Their files are then copied out of the CAS into
a temporary tree, so a backend that writes beside its sources is not
writing into content every project on the machine shares.
What the release requires is read from the wheel the build produces,
because a source distribution declares it nowhere else. The build is
therefore part of resolution rather than of installation:
`Step::NeedMetadata` dispatches on the candidate kind, and
`--lockfile-only` builds too. Because that wheel is the answer,
`finish_build` no longer holds every build to its directory's manifest.
`Contract::Interpreter` says the built wheel is the contract, which is
what lets a `setup.py`-only archive with dependencies build at all;
workspace projects and git checkouts keep `Contract::Manifest`.
Every archive URL goes through `validate_url` before it is fetched, at
the lockfile boundary and again before a build starts: the store reads a
`file:` URL from the filesystem, and a `pylock.toml` is untrusted input,
so without that check a lockfile could name an archive on this machine
and have its build backend run. The wheel path already refuses such a
URL outright.
A build runs the release's own code, so it is approved the way a git
dependency is, under `allowBuilds`. pnpm builds only with the
interpreter running the install, so a project that locks for several
environments and reaches a release none of them has a wheel for is
refused rather than locked from a wheel built for the wrong platform.
Where one environment does have a wheel and another does not, both land
on one PEP 751 package and each environment takes what it can use, which
is why `Solved::source_key` deliberately does not tell a wheel and a
source distribution of one release apart.
pnpr has no interpreter, so it offers source candidates but hands the
project back to the client as soon as a solution needs one built. The
client's pnpr fallback now recognizes any "must be resolved by the
client" answer rather than only the direct-URL one.
Separately, a failed resolution says which of its causes the empty
version set was: no index publishes the distribution, its releases
publish nothing this interpreter can install, or the project's own
requirements select none of the versions it offers. Reading an index
page therefore records why each release it left out was left out.
A source distribution is built again on every install rather than kept
as the wheel it produced; `plans/PYTHON_SPIKE.md` records that limit.
Related to pnpm/pnpm#14945.
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.
Separate lockfile dependency traversal from build activation for weak Cargo
dependency-feature references. Include the referenced optional dependency
and propagate its requested features recursively in the lockfile graph.
Keep unselected optional dependencies inactive.
Cover nested weak references across targets, compare generated lockfiles
with Cargo, and verify locked offline builds compile only active dependencies.
Add default and no-default uuid workspaces to the live equivalence corpus.
Isolate Cargo homes outside install workspaces from local user configuration.
The shared resolver also serves pnpr. Cargo support is Rust-only, so the
TypeScript pnpm v11 CLI is unaffected.
Closespnpm/pnpm#14978.
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
`read_file` snapshots a metadata path by opening it with `openat` and then
checking that what it opened is a regular file. Opening a FIFO read-only
blocks until a writer arrives, and the type check never runs, so a
`package.json`, `pnpm-lock.yaml` or `pyproject.toml` that is a FIFO hung
the command before preparation began.
Adding `O_NONBLOCK` makes the open return for a FIFO or a device, so the
type check rejects it with the error it already has for a path that is not
a regular file or a symlink. A regular file is unaffected: the flag does
not change the semantics of reading one.
The regression test captures a FIFO on a thread and waits on a channel, so
a reintroduction fails in 30 seconds with a clear message rather than
hanging until nextest's timeout.
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.
Prepare Python projects with bounded concurrency after selecting their
interpreters and workspace sources. Collect all results before reporting an
error, preserving discovery order for publication and error selection.
Share fresh registry resolutions within one install using normalized lockfile
inputs (including overrides, constraints, and extra indexes), requires-python,
and the interpreter report as the key. A per-key mutex
allows one resolution to populate the cache while identical projects wait.
Existing lockfiles retain their replay and frozen validation paths. Local
sources and direct URLs do not share fresh resolutions. Environment installs
seed and validate the shared lockfile through the existing acceptance path.
Make backend environment sharing safe for concurrent projects. Siblings wait
for one environment to finish; recursive builds track their own active keys
and reuse completed nested environments without waiting on other backend chains.
This preserves cycle diagnostics
and avoids dependency cycles between in-flight cache entries.
Add regression coverage for identical projects, frozen lockfile validation,
project dependency-rule isolation, concurrent slot replenishment, and settling failed preparation without
publishing lockfiles. Python installation exists only in pnpm v12.
Replace benchmark artifacts on reruns so the combined report and privileged
comment workflow consume current samples. Seed distinct whole-second manifest,
lockfile, and validation times in the synchronous fast-path test; equal
whole-second mtimes conservatively trigger a content check, so the prior
fixture could fail when its initial mtime landed on a second boundary.
Related to https://github.com/pnpm/pnpm/issues/14945 (item 13).
The repository's skills live in .agents/skills. Codex discovers that
directory on its own (verified with `codex debug prompt-input` on codex-cli
0.154.0, which lists a skill placed there). Claude Code does not: it scans
only .claude/skills, so the repo's skills were invisible to it unless each
contributor symlinked them into ~/.claude/skills by hand.
Add .claude/skills as a symlink to ../.agents/skills. Claude Code follows it
and lists the skills behind it, so the skills stay in one place and every
agent reads the same copy.
.claude was ignored wholesale, which would have kept the symlink untracked.
Narrow the rule so the root .claude directory and the skills symlink are
tracked while everything else under it, such as settings.local.json, stays
ignored.
No .codex/skills symlink: Codex already reads .agents/skills, so it would
only add a second path to the same files.
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.
`pnpm add pypi:<package>` built `<dir>/pyproject.toml` unconditionally and
handed it to `manifest::add`, which read it with a bare `.into_diagnostic()?`.
At a workspace root without a root `pyproject.toml` the whole diagnostic was
`No such file or directory (os error 2)`: no path, no hint at what was
missing.
`plan_add` now checks for the manifest before it builds the install task, the
way the `crate:` counterpart in `cargo_deps::add::plan` already does, and
reports the path it looked for along with a help line pointing at a directory
that has a `pyproject.toml`. Checking at plan time also keeps the failure
ahead of the interpreter and the lockfile, so nothing is written.
The check is `try_exists` rather than `is_file`, which answers false for
every manifest that is not a regular file and not just an absent one. A
`pyproject.toml` that is a directory, or one whose metadata cannot be read,
keeps the error the filesystem gave.
The two reads that could still surface an unlabeled OS error now carry
`read {path}` context, matching `read_project_manifests`.
Related to pnpm/pnpm#14945 (item 11).
The WHEEL file's Tag fields are informational. Installers select a wheel on
its filename, and a wheel whose filename tags were changed after the build
routinely keeps the tags it was built with, so requiring the two to agree
rejected valid published wheels such as mysql-connector-python. pip and uv
install them.
The filename's tags are still checked against the target interpreter before a
wheel is downloaded or a built wheel is accepted, so the inspect request no
longer needs the filename at all.
Related to pnpm/pnpm#14945 (item 10).
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.
Treat workspace Python groups and extras as defaults that apply only when
provided by each project. Add independent per-project overrides through
[tool.pnpm.python]. Empty overrides disable the corresponding default;
explicit unknown names still fail.
Share extra selection between static manifests and prepared dynamic metadata.
Normalize extra names using the existing Python requirement types. Preserve
strict included-group validation and existing production/development
projections. Compute validated production and development requirements once
per project for locking and borrow their selected slice for the environment.
Dependency requirements still select local distribution extras
independently of the source project's own installation settings.
Move selection and group expansion into a manifest module to keep the
manifest implementation within the repository's file-size convention.
Related to pnpm/pnpm#14945 (item 8).
Agents routinely opened a pull request, pushed, and ended the turn: CI
results and review rounds arrive minutes later and nothing in the repo
said the task was still open. AGENTS.md documented how to open a PR and
how to resolve a thread, but never that a push leaves work outstanding.
Add a `pull-requests` skill covering the path a change takes. Open the
PR as a draft, because CI runs on a draft here and the reviewers do not,
so a failing check or a bug the author's own pass catches costs one push
rather than a review round. Mark it ready once the checks are green and
that pass is clean.
From there, run a loop after every push: wait for the checks, read the
whole round including the findings a reviewer leaves only in its summary
comment, verify each one before acting, reply on every thread with the
commit that fixed it once that commit is on the remote branch, keep the
title and description current because they become the squash commit, and
push again. A round counts as finished only when every reviewer's
summary names the head commit, since a round that has not started looks
exactly like one that found nothing.
The skill also covers conflicts that appear while the PR waits, and why
the fix is a rebase: main requires linear history and has merge commits
disabled. It covers opening a PR for an issue, where the timeline
cross-reference says nothing about what was done, so the issue gets a
comment naming the approach and whether it closes the issue whole or in
part. And it covers the traps: rounds land on the previous round's
fixes, resolving a thread needs GraphQL, and a fork run cancelled by the
approval sweep is not a failure.
The skill names no reviewer. The roster changes; what an author does
with a finding does not.
Replace the PR bullets in AGENTS.md with a pointer to the skill and the
rules that have to hold even when it is not loaded: open as a draft, a
push is not the end of the task, and agent-authored content is signed.
Prepare dynamic metadata before constructing the Python dependency graph.
Use prepare_metadata_for_build_wheel where available and build a wheel as
the PEP 517 fallback. Retain fallback wheels for noneditable installation
with the same interpreter. Preserve hook-prepared metadata directories for noneditable builds. Validate
static dependency and interpreter declarations before resolution, and reject
final wheels whose dependencies, interpreter support, or extras differ from
the prepared metadata. Reuse the prepared manifests and workspace scopes when resolving sources.
Evaluate active platform markers and requested extras during source traversal.
Discover requirements.txt through the shared workspace inventory and treat
requirements-only directories as dependency projects without building the
directory itself or generating a pyproject.toml. Included files contribute
to the requirement inputs used for frozen-lockfile validation.
Backend requirements retain the existing allowBuilds policy. Metadata
preparation fails when approval is missing because resolution cannot
continue without running the backend. Dynamic metadata is currently
prepared for each project's selected interpreter and refreshed if dynamic
interpreter support changes that selection. External, undiscovered path sources
still require static metadata. Adding dependencies to dynamic metadata is
not part of this change.
Related to pnpm/pnpm#14945 (item 7).
`pnpm/CONTRIBUTING.md` required `just ready` before every commit, explicitly
including documentation and comment edits: a full `cargo nextest run` over
~11,500 tests for changes that cannot break them. The root guide's "never run
all tests" rule was scoped to the TypeScript sections, so it read as not
applying to Rust.
The checks now match what a change can break. Formatter, typos, and
workspace-wide check and lint before every commit, since those are cheap and
catch the cross-crate breakage a scoped selection hides, plus the tests for
what the diff affects. The full local run is reserved for changes whose blast
radius cannot be named, because CI already runs the suite on three platforms.
`just test-affected` resolves changed files to packages through `cargo
metadata` and runs them through `run-rust-tests.mjs`, expanding any `pnpr-*`
selection so feature unification does not silently skip backend tests, and
refusing to guess when a change reaches files every crate compiles against.
Selection is crate-level rather than `rdeps()`-based: `pnpm-cli` holds a third
of the workspace's tests and sits downstream of nearly everything, so `rdeps()`
selects 84% or more of the suite for any core crate. Restructuring that target
is tracked in pnpm/pnpm#14984.
Crate-level selection leaves the dependents unrun, so the new `smoke` profile
runs in their place: one end-to-end test per area of CLI behavior, triggered by
the dependent set rather than added to every run. Membership is by behavior
area rather than code coverage, since nearly every end-to-end test walks the
same install path. An exact-name filterset fails open, so a test checks every
entry against the suite sources.
The `testing-changes` skill carries the selection cookbook and the gotchas that
make a scoped run lie.
Related to pnpm/pnpm#14984.
Limit survivor ownership reads to command names that cleanup could actually remove. Snapshot replaced groups before activation and retain fail-closed scanning whenever an existing command is dropped, while allowing same-command global upgrades to proceed past unrelated incomplete groups.
Apply the same ownership rules to pnpm v11 and v12. Canonicalize the v11 hash-link target and parent before computing rollback links on macOS. Isolate package-manager provisioning tests from user-level Yarn path overrides.
Reuse the install build-approval helper for global installs and dlx.
Keep a lockfile in each isolated dlx cache entry so approved packages
can be rebuilt before running the requested binary. Reload persisted
approval decisions into the rebuild configuration.
Recheck pending builds on cache hits in both v11 and v12. Refresh
caches that do not have the lockfile required for rebuilding in both stacks.
Cover noninteractive installs, approval and build artifacts, caller
settings isolation, and reuse of an approved cache entry.
Closespnpm/pnpm#14943.
`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.
`build_graph` collected every importer's `dependencies`,
`devDependencies`, and `optionalDependencies` as walk roots, ignoring the
group filter the rest of the install applies. Thread the install's
`dependency_groups` into it so a package reachable only through an
excluded group never enters the build graph, and reuse
`ProjectSnapshot::dependencies_by_groups` rather than re-deriving the
group-to-map mapping here.
This closes the second half of pnpm/pnpm#14864. Its reported symptom --
a `--prod` install running a dev-only package's `postinstall` -- is
already gone in 12.4.2, because pnpm/pnpm#14896 stopped materializing
dev-only snapshots under `--prod` and an unmaterialized package has no
directory to run scripts in. The same holds for the mirror case, a
`--dev` install and a production dependency's script. The build graph
itself still depended on that incidental fact; now it does not. No
changeset: 12.4.2 already behaves this way for users.
The e2e test is the reporter's reproduction, kept as a regression guard
for the behavior as a whole. Its marker file is written under `INIT_CWD`
rather than the script's cwd, which is the package's virtual-store
directory and so does not exist at all on a production install.
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.
`path_patched_lockfile_resolution_does_not_require_git` narrows PATH to
the rustc sysroot's `bin` to prove the install needs no `git`. A
`build.rustc-wrapper` in the developer's own Cargo configuration reaches
the spawned `cargo` as `RUSTC_WRAPPER`, and its binary is not on that
PATH, so the test failed for anyone who configures one:
cargo generate-lockfile failed: could not execute process
`sccache .../bin/rustc -vV` (never executed)
An empty `RUSTC_WRAPPER` turns the wrapper off for the child.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cargo` lets semver-incompatible versions of a crate coexist, so the
resolver shards a crate by compatibility line: pubgrub picks one version
per package, and a line is what "one version" means here. The line a
dependency resolved against was chosen eagerly, from the newest version
meeting the requirement, and the solver only ever saw that one. A
requirement spanning several lines, such as `>=1, <3`, was therefore
pinned to the newest before solving began, and a dead end there failed
the whole resolution although an older line resolved. `cargo` backtracks
and picks the older version.
A requirement met on more than one line now resolves through a choice
package of its own, whose versions are the newest version on each line and
which depends on the line that version names. pubgrub reaches for the
largest version first, so the newest line is tried first and an older one
is reached by ordinary backtracking, which is `cargo`'s heuristic. A
requirement met on a single line still names that line directly, so
nothing changes for the ranges almost every manifest writes.
Index discovery widens to match: every line a requirement admits can be
selected, so the versions on each are walked and what they need is
fetched. Feature selections are folded into every admissible line, since
the solver may settle on any of them, while the graph is walked through
the line it actually settled on.
Checked against `cargo` itself on the real index. For a workspace asking
for `rand >=0.6, <0.8.0` in one member and `rand 0.6` in another, both
resolve to the same 28 packages, `rand` 0.6.5 and 0.7.3 among them.
Closespnpm/pnpm#14962
`pylock.toml` was resolved for the machine that ran the install: one
marker environment, one wheel tag order, one wheel per distribution. A
lockfile a repository commits could therefore not serve Linux CI and
macOS or Windows contributors at once, which every repository surveyed in
pnpm/pnpm#14945 needs.
`python.platforms` and `python.pythonVersions` now name the environments
`pylock.toml` is resolved for. Every platform is paired with every
version, and a list left empty is the platform or the version of the
interpreter running the install, so declaring neither keeps the previous
behavior. Each environment is resolved on its own and the answers are
merged into one lockfile: a package pins the wheel every environment
takes for it and carries the PEP 751 `marker` saying which of them
install it. `[tool.pnpm]` records the declared environments, so changing
them resolves the project again.
The interpreter reports what a declared environment resolves as, through
the same host helper that reports its own tags and markers. A platform is
named as a Rust target triple, or as an architecture and the libc
baseline its wheels are built against; `linux`, `macos` and `windows` are
short names for the most common three. An architecture the triple spells
differently from the wheel tag is translated, and a baseline outside the
glibc 2 or musl 1 series is refused. Two names for one environment are
one environment, told apart by what the interpreter reports them as.
`pnpm install` narrows the lockfile to the environment its interpreter
matches: it installs the packages that environment's marker selects and,
for each, the pinned wheel whose tags the interpreter prefers. An
interpreter none of the declared environments stand for is refused, since
nothing in the lockfile answers for it.
A declared environment answers for a platform and a Python version and
nothing else. A requirement whose marker it leaves undecided is refused
rather than locked under a claim the resolution did not make. That covers
a requirement gated on the kernel release, and one gated on a patch
release of a minor version declared without one.
Declared environments are resolved locally, since a pnpr server answers
for one interpreter. An index page is fetched once per run and read back
from its cache for every environment after the first.
Separately, the rule that decides whether a lockfile's marker names the
full interpreter version counted only release segments, so a locked
package requiring `<3.12.0.post1` was claimed for every 3.12 patch
release. A bound whose version falls between two releases now tells them
apart.
Related to pnpm/pnpm#14945.
`cargo` records where a dependency said to look, not where the package was
fetched from. A dependency that names no registry belongs to crates.io even
when `[source.crates-io]` is replaced and another registry serves it, while
one that names the configured registry belongs to that registry. An index
entry names no registry when it means its own, so it belongs wherever the
crate that pulled it in belongs.
pnpm recorded the configured registry for every crate, so `cargo` wanted to
rewrite the source of every crates.io dependency and `--locked` failed
against a lockfile pnpm had just produced.
The source is now resolved per crate by walking the graph from the
workspace manifests and carrying each dependency's declared registry down
to the entries that name none. A crate required from two registries cannot
be expressed in one lockfile entry and is reported rather than silently
recorded as one of them.
Reading an existing lockfile accepts both, since both are what `cargo`
writes, and rejects any other registry as before.
`cargo.indexUrl` has not shipped, so its pending changeset carries the
correction rather than a new one.
Closespnpm/pnpm#14961
Cargo metadata without dependencies does not expose root source overrides.
Route workspaces with entries in patch or replace tables to the isolated
Cargo resolver before attempting native or pnpr resolution. Validate declared Git
override transports using the same policy as direct Git dependencies.
Constrain Cargo Git fetching to supported protocols before transitive
dependencies can execute helpers. Share the Git protocol policy while
preserving configured bans and narrower inherited allowlists.
Reuse the existing resolver isolation and native dependency-edit transaction.
Cover path and pinned Git patches, missing-lock generation, adding a registry
crate to an existing patched lock, revision changes with managed sources,
frozen behavior, rollback, checkout helper isolation, and offline Cargo
validation. Also verify legacy path replacement package identities.
Share timestamp ordering across freshness-check fixtures after coverage
and Windows CI exposed equal manifest and lockfile timestamp collisions.
Seed the catalog regression with equal whole-second file timestamps.
The fallback still requires the default cargo.indexUrl. The companion
website PR documents this limit and Cargo configuration and offline behavior.
Closespnpm/pnpm#14950.
pnpm installed a Python project's dependencies but never the project
itself, so `import <project>` failed after an install and the commands in
`[project.scripts]` were missing from the environment. Every repository
surveyed in pnpm/pnpm#14945 needs the project importable to run anything.
The environment now also holds the project's own package. pnpm does not
build it: it writes a `.pth` file pointing at the project's source tree
plus the `.dist-info` an installed distribution records (METADATA,
entry_points.txt, a direct_url.json marking it editable, INSTALLER and
RECORD). That is what an editable install is, so an edit to a module takes
effect without another install.
A project is packaged when it declares a `[build-system]`, and
`tool.uv.package` overrides that either way, which is how uv decides too.
Modules are imported from the package directories the build backend
declares in `[tool.hatch.build]`, `[tool.hatch.build.targets.wheel]` or
`[tool.setuptools] package-dir`; a project that declares none uses `src`
when it has such a directory, and the project directory otherwise. Running
a backend's PEP 660 `build_editable` hook would need an isolated build
environment and the network, neither of which this integration has, so the
layout is read from the manifest instead.
A project whose version is dynamic gets its dependencies and a warning,
since pnpm cannot record a version only a build backend knows. So does one
whose `[tool.setuptools] package-dir` maps a package into a directory that
no `sys.path` entry stands in for.
A `.pth` file names one directory per line, and Python runs a line that
starts with `import`, so a declared package directory carrying a line break
or reaching outside the project is refused rather than installed. PEP 621
reserves `console_scripts` and `gui_scripts` in `[project.entry-points]`,
and declaring either there is now an error naming the table to use instead.
The wheel writer in `host.py` became an `Environment` class so the
project's package writes its files, scripts and RECORD through the same
collision checks and entry point validation as a wheel.
Related to pnpm/pnpm#14945.
Sparse-index discovery walked each dependency edge on its own, keyed by
name, requirement, and the features that edge asked for. Resolution
unifies those edges per package. For ordinary activation the per-edge
walk is a safe over-approximation, because activation is monotone and the
union of the per-edge closures equals the closure of the union.
Weak activation breaks that equality. One edge activates the optional
dependency and another asks for `dep?/feature`, so neither edge alone
turns the weak feature on. A crate only the union activates never had its
index file fetched, and resolution then failed on a graph Cargo resolves.
Discovery now accumulates a feature selection and the selectable versions
per package key, the same key resolution uses, and walks a package again
whenever either grows. The per-edge set stays as a fast path: a repeated
edge contributes no new versions and no new features, so it can be
skipped without consulting the union.
Closespnpm/pnpm#14960
Initialize recursive submodules in the Cargo Git checkout before scanning and importing packages. Git resolves relative URLs against each parent repository and checks out the committed Gitlinks, without following the current branch tips.
Constrain submodule Git processes with GIT_ALLOW_PROTOCOL, intersecting the supported transports with any caller-supplied allowlist and the effective Git protocol policies. The allowlist rejects helper transports even when user configuration explicitly enables them, and applies recursively and after URL rewrites. Exclude submodule .git files from the vendored sources.
Version the Cargo Git store slot to avoid reusing previously cached incomplete crates. The first online install refetches these entries. Later offline installs reuse the new slots.
Test nested relative submodules, pinning after a branch moves, Cargo checksum coverage and tamper rejection, locked offline Cargo builds with an empty Cargo home, offline store reuse, legacy cache invalidation, unsafe helper transport rejection, and preservation of caller transport restrictions, configured file/SSH bans, and recursive user-policy restrictions, and case-insensitive policy values.
Validation: just ready (11,340 tests passed), focused regression/security/cache tests, and a regression experiment that fails when submodule fetching is removed.
Closespnpm/pnpm#14951.
Cargo resolution offers the solver every non-yanked version on a crate's
compatibility line, including prereleases outside the requirement that
brought the crate in. Building the constraints for such a candidate
failed outright when one of its dependencies had no non-yanked version in
the index, so a whole solvable graph was rejected. Installing Rspack hit
this through napi 3.0.0-beta.6, which needs `napi-build ^3.0.0-beta`
while napi-build 3.0.0-beta.0 is yanked.
A dependency the index cannot meet now maps to a `PackageKey` of its own
that no version is ever registered under. The solver treats the candidate
that needs it as a plain incompatibility, backtracks past it, and names
it in the report when nothing else resolves. That replaces the
early-return skip for a dependency naming a crate the index does not
carry, which lost the reason for the dead end.
Feature unification walks the selected graph with a wider feature set
than the solve used, so it can reach a dependency no version meets too.
It now skips that dependency and lets the next round rule the candidate
out, and the graph validation that follows treats it as a solution to
retry rather than an error.
Closespnpm/pnpm#14952
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`pnpm install --prod` recorded only the groups it installed in
pnpm-lock.yaml, so the next `pnpm install --frozen-lockfile` rejected
the lockfile the previous command had written. `--dev` truncated the
same way.
The resolve pass now walks every direct dependency group whenever the
run writes pnpm-lock.yaml, matching pnpm 11: the lockfile describes the
manifests, not the filter a run was invoked under. `pacquet deploy
--legacy` keeps resolving only what it installs, since it leaves the
workspace lockfile alone and a wider resolve leaked dev dependencies
into the deployed virtual store.
Because the built graph is now wider than what a filtered run installs,
the materialization closure narrows it back down to `included`.
MaterializationScope built that closure only under a `--filter`
selection, so `pnpm install --prod` materialized the whole graph into
the virtual store and pruned the surplus afterwards -- `Packages: +89`
in 1.56s on a project with eslint and typescript in devDependencies,
where pnpm 11 reports 2. It now builds the closure whenever the resolve
pass walked a group the run does not install.
`pnpm prune` is `pnpm install` plus direct-dependency pruning, so its
--prod/--dev/--no-optional tests were asserting the truncated lockfile
this fixes; they now assert pnpm 11's behavior.
Closespnpm/pnpm#14912.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Default an absent git crate package.version to Cargo's 0.0.0 while rejecting malformed explicit values and preserving inherited package/dependency fields.
Use Cargo generate-lockfile for metadata containing git dependencies rather than passing git sources to the sparse registry resolver. Retain native resolution for registry/path-only graphs and reject unsupported custom registry resolution. Run the selected toolchain Cargo from its sysroot with an explicit manifest path. Copy only audited artifact and Rust-version resolver settings from checkout configuration so executable credential helpers and forced environment commands in the checkout cannot run. Leave checkout configuration untouched. Existing transaction snapshots restore manifests and lockfiles after failed additions.
Merge locked registry dependencies from the selected toolchain's rust-src standard library Cargo.lock using crates.io source semantics independently of the project download registry when workspace or ancestor Cargo configuration enables build-std. Share identical package slots and reject conflicting identities/checksums.
Closespnpm/pnpm#14944.
A wheel whose `Requires-Dist` pnpm cannot parse failed the whole
resolution, so one broken release took its distribution down at every
version the project could otherwise install. The release is immutable, so
nothing the project does corrects it.
Such a release is now unavailable to the solver, which backtracks to one
whose requirements pnpm can read, and reports the unreadable requirement
when no release works. uv marks a version with unreadable metadata
unavailable the same way.
The direct-URL form keeps its error. `read_requirement` tells a line pnpm
cannot parse apart from one it parses and does not support: every release
declaring a URL requirement names the same unsupported feature, so
quietly installing an older release would hide what the project has to
change. `parse_requirement`, which reads what a project itself declares,
is unchanged.
Follows up pnpm/pnpm#14946.
pnpm outdated --long fills the Details column with the package homepage again
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
pnpm 12 parsed a Git-conflicted pnpm-lock.yaml as plain YAML, failed, warned
"Ignoring broken lockfile", and resolved everything again from the manifests.
That can pick versions present on neither conflict side, produce large unrelated
lockfile changes, and require registry access the conflicted lockfile made
unnecessary.
pnpm-lockfile now splits a conflicted wanted lockfile into its two sides, parses
each, and merges them with mergeLockfileChanges, mirroring pnpm 11's
autofixMergeConflicts. The split validates every marker transition and requires
the last conflict to be closed, so a file that only looks conflicted keeps
surfacing its original parse error instead of being cut at the wrong lines. It
validates before it allocates, so a large marker-free lockfile that fails to
parse no longer pays two input-sized copies to be told there is no conflict.
The recovery is more than a log line: the file on disk still holds its markers,
so the install has to write the merge back. load_wanted_lockfile records that a
merge happened and the flag turns off the auto-frozen dispatch, the fast
lockfile update, and the pnpr exchange-free shortcut, each of which can finish
without writing pnpm-lock.yaml. pnpm 11 gates the same three on
lockfileHadConflicts. An explicit --frozen-lockfile still takes the frozen path,
as it does on pnpm 11.
The split is of the whole file, not of the extracted main document, so a
conflict in the leading env document — the configDependencies and
packageManager bootstrap pnpm records ahead of the graph — is recovered too.
That conflict previously failed the install outright in EnvLockfile::read, and
save_value_to_path copied its markers forward into every lockfile pnpm wrote,
since it preserves that document as raw text. EnvLockfile::read now recovers
through the same path with a new merge_env_lockfile_changes, and the wanted
loader counts an env-document conflict so the install still writes the file
back, and only when that conflict actually merges. The writer merges a
conflicted env document, and refuses to write one whose sides do not merge
rather than copying the markers into a file it is writing to repair. A hunk that
swallows the separator recovers the main document only; the env installer
regenerates the env document. pnpm 11 shares this blind spot; this diverges from
it deliberately.
add, remove and update pass their install the loader rather than an
already-loaded lockfile, so the install reports the merge it performs and gates
on it, and a run that returns before installing anything claims nothing. The
three lockfile fields those commands carried are one CommandLockfile.
Closespnpm/pnpm#14880
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deploy-generated directory resolutions omitted `hasBin` from their lockfile metadata, so the precomputed has-bin set dropped every workspace dependency slot and the deployed project's `node_modules/.bin` directory came out empty.
Record definitive bin metadata when generating workspace directory rows. Probe directory resolutions whose metadata is absent for compatibility with older lockfiles while continuing to skip resolutions explicitly marked `hasBin: false`.
Closespnpm/pnpm#14899
Reading a distribution's candidates failed the whole page when one file
it lists could not be read: an unreadable wheel filename, a file with no
SHA-256 digest, a URL that is not HTTP(S), or a wheel belonging to
another distribution. A page lists every release a distribution ever
published, so one such file put the distribution out of reach at every
version, including the versions a project can install.
Such a file is now left out, with a debug log naming it and the reason,
the way a wheel built for another interpreter already is. uv skips a file
it cannot read the same way. What a lockfile pins keeps its hard errors:
a pinned wheel pnpm cannot read is still refused rather than skipped.
Follows up pnpm/pnpm#14920.
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>