* docs: lint prose with Vale
Add Vale with an in-repo style, .vale/styles/pnpm, that turns the
writing rules in AGENTS.md and the Google developer documentation style
guide into lint rules. The stock Vale packages (Google, Microsoft,
RedHat, write-good) were too noisy to block on, for example 1,300
passive-voice hits in the v12 docs alone, so the rules are written for
this repo, following the GitLab model. Each one links the guideline it
enforces.
Levels:
- error, fails CI: dashes, plain-word substitutions, condescending
words, "please", "note that", hedges, puffery, optional plurals,
exclamation marks, ellipses, curly quotes, generic link text, product
spellings, sentence-case headings, repeated words, double spaces,
implementation history in comments; in changesets, bold or italic
emphasis, internal names and release-line names.
- warning: future tense, sentences over 30 words, "instead of" and
"so that" tails in changesets.
- suggestion: passive voice.
.vale.ini scopes the rules. Docs get every rule. Contributor Markdown
and code comments keep dashes and headings as warnings until their
roughly 8,100 em dashes are rewritten. Released changesets stay in
.changeset/ and are not rewritten, so lint-prose.mjs lints only the
changesets added since PROSE_BASE (the PR base in CI).
The prose workflow installs a pinned, checksum-verified Vale release and
runs `pnpm lint:prose`. The rest of this change rewrites the existing
docs, READMEs, contributor Markdown, and code comments to pass the
error-level rules. Only prose and comment text changed. Renamed
headings keep their anchors.
Spelling stays with cspell, which checks identifiers that Vale does not
see.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: keep prose lint from flattening meaning
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore(release): 11.28.7, pacquet 12.15.0, pnpr 0.1.0-alpha.16
* docs(release): curate 11.28.7, pacquet 12.15.0, and pnpr 0.1.0-alpha.16 release notes
Add lead paragraphs and group the patch entries by what the reader was
doing, with security fixes first on each page.
- v11: lead with the registry metadata name check, pinned pnpm
trustPolicy check, maxTarballUnpackedSize, and the removal of registry
revisions; merge overlapping entries for minimumReleaseAge metadata
revalidation, frozen-lockfile directory dependency checks,
syncInjectedDepsAfterScripts, verifyDepsBeforeRun, pnpm sbom, and
unreadable config settings.
- v12: order minor highlights from config includes and
lockfile.integrityTarget down to interactive update styling; drop the
pnpr-only distribution note from the CLI changelog; merge overlapping
entries for workspace and pnpr install speedups, minimumReleaseAge and
If-Modified-Since revalidation, frozen-lockfile and injected package
checks, verifyDepsBeforeRun, pnpm prune, pnpm pack, pnpm licenses
list, pnpm self-update, pinned pnpm flag forwarding, and WebContainer
fixes.
- pnpr: lead the minor section with the breaking ecosystem URL prefix,
hosted storage layout, artifacts.orgs, and strict config schema
changes; fold dist.pnpr tarball metadata into one entry and drop the
redundant /metrics note; lead the patch section with reserved-username
and auth security fixes.
- Use one issue-link label style across the release pages.
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
## Proposed Changes
- **Rust (`pnpm-deps-restorer`):**
- Thread `swap_lock: Some(swap_lock)` through every `slot_import_opts` branch in `create_virtual_dir_by_snapshot.rs`.
- Stage absent private targets (`!opts.safe_to_skip`) in `import_indexed_dir` and attempt a lock-free `rename_with_retry(&paths.stage, dir_path)` when `!opts.force` before falling back to `swap_lock.hold()`. Under the swap lock, leave a now-completed directory (or non-directory dirent) in place when `!opts.force` via `completed_or_non_dir_target`, or swap onto an incomplete partial directory.
- Hold `swap_lock` before repairing an existing incomplete private target (`repair_existing_dir`, re-checking `marker_present` under the lock) or replacing a non-directory dirent at a private target (`replace_non_dir`).
- **Rust (`pnpm-fs` `DirLock`):**
- Remove `<user_lock_root>/pnpm-dir-locks-<uid>/<hash>.held` on release while holding its exclusive OS lock (`LockedHeldFile::remove_and_unlock`), leaving no `.held` file behind after a clean lock lifecycle.
- Verify via `path_still_names` after `try_lock()` in `try_lock_held_file_once` that the locked descriptor still names `held_path` on disk, reopening if the previous holder unlinked its `.held` file between `open` and `try_lock`, while keeping dead-holder takeover instant (an ungraceful exit leaves `.held` at `held_path`).
- **TypeScript (`@pnpm/fs.indexed-pkg-importer` in `pnpm11`):**
- Replace `rename-overwrite` in `commitStagedDir` with `overwriteDirSync`, which moves an existing target directory aside on both POSIX and Windows (`${pathTemp(newDir)}_swap_${swapCounter++}`) instead of deleting it in place and spinning for 60s when Windows reports `EPERM` on a busy directory rename, and falls back to `renameOverwriteSync` only for non-directory targets.
- Guard `tryExclusiveImport`'s failure cleanup with `sameCreatedDirectory` so it does not `rimrafSync(newDir)` after a concurrent importer has replaced or completed `newDir`.
- Run `'concurrent forced imports into one target all complete and leave the package in place'` on Windows as well as POSIX.
- **TypeScript (`@pnpm/resolving.npm-resolver` in `pnpm11`):**
- Pick before mirroring or caching in `persistFreshMeta` (matching `serveValidatedMeta`) so a malformed fresh packument is not written to the mirror in the background before `pickMatchingVersionFinal` validates it.
Closes https://github.com/pnpm/tasks/issues/156
Related to pnpm/pnpm#17091
Related to pnpm/pnpm#17113
A frozen install kept whatever the lockfile recorded for a non-range
child spec of a local directory dependency, comparing only the package
name. A tarball URL or a full git commit names one exact source, and the
lockfile keys the child by that URL or commit, so a spec naming a
different one is now outdated in both CLIs. Git specs are classified
with the git resolver's parser first, so https git URLs and &path:
parameters are handled. Moving selectors and an
unchanged URL keep the recorded resolution on a frozen install, as
decided in pnpm/tasks#145. For a git spec the commit and the path:
subdirectory are compared, not the repository.
Closespnpm/pnpm#17129
lockfile.integrityTarget: tar makes the integrity of every package tarball
entry the hash of its uncompressed tar, recorded as settings.integrityTarget.
The lockfile then survives a registry recompressing its tarballs, switching
to tar resources, or being replaced by a mirror that does either. A lockfile
recorded under another target is stale: a frozen install fails with
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH, and any other install re-resolves with
update-checksums semantics so no hash of the old kind carries forward. The
setting is opt-in and the lockfile version is unchanged.
The tarball crate now reports both hashes of every extraction: the resource
after content decoding, and the tar a .tgz decodes to. A pin names one of
them (IntegrityKind::Resource / Tar), or either at resolution, where a locked
or published integrity may be of either kind (ResourceOrTar, sha512 only).
Under a tar pin every gzip member is decoded and nothing may follow the tar.
The resource pin keeps today's behaviour, extracting the first member and
tolerating trailing data, but then records no tar.
Store rows extracted from a .tgz record tarIntegrity and get an alias under
the tar key, in a table pnpm v11 never reads. Store lookups follow aliases,
so a tar-pinned frozen install is served offline by a store that .tgz
installs populated, and resolution takes a version's tar hash from its row
before downloading anything.
Registry revisions now always pin their tar, revision 0 included, and are
fetched with a tar pin, so the digest route can use any content coding.
Revision metadata must list revision 0 and the selected revision, and a
digest-route URL without a revision is rejected. pnpr keys its digest
references by tar hash on every registry format, serves the route as a
negotiated tar resource, and projects revision 0 explicitly. pnpm v11 cannot
extract tar resources, so it now rejects revisions with
ERR_PNPM_REGISTRY_REVISIONS_UNSUPPORTED.
Implements pnpm/rfcs text/0000-tar-integrity-for-every-source.md and
pnpm/rfcs#42.
The version picker ranks the candidates for a range by the summed weight
of their preferences. The lockfile pin weighed 1,000,000, a manifest
specifier 1,000, and a version resolved higher in the dependency chain 1.
A fresh install has no lockfile, so it picks the version the chain
already resolved, while a reinstall kept the pinned version and wrote a
second copy of the package. Replacing a dependency therefore left a
lockfile that a fresh install of the same manifests does not write.
The pin is now the lowest tier: a version named on the command line,
then a manifest specifier, then a version resolved higher in the chain,
then the lockfile pin. Each tier is a digit of a base-1000 number, and
`pinned_by_lockfile` / `pinnedByLockfile` reads the lowest digit where
the code used to compare against the old lockfile weight. A pin that no
other preference competes with still wins against the newest version, so
a re-resolved dependency keeps the locked versions of its own
dependencies (pnpm/pnpm#2191). v12 gets a port of that test.
The pins `pnpm import` reads from another package manager's lockfile and
the version a command names move to the top tier, so they keep
outranking every manifest range. The v11 `audit --fix update` penalties
are derived from the manifest weight, so they keep their place relative
to it. The `dedupe` option of the v11 preferred-versions seed no longer
changes anything and is removed.
The peer convergence harness no longer tracks a divergence for
pnpm/pnpm#17084.
Also fixes an order-dependent v11 test that read a dist tag other tests
move.
Closespnpm/pnpm#17084
A tarball URL under the configured registry is parsed as a registry
version spec, with the name and version read from the URL. Both CLIs read
the name from the whole URL path, so a registry served under a path
(Artifactory's /artifactory/api/npm/<repo>/, pnpr's /npm/) took that path
into the name: https://host/npm/@scope/pkg/-/pkg-1.0.0.tgz became
npm/@scope/pkg.
Both stacks now strip the registry URL's path before reading the name,
matching a whole path prefix only. pnpm v11 drops the external
parse-npm-tarball-url package, whose only caller this was, for a local
parser that behaves like v12's: it declines a file name that does not
start with the package name, and a malformed percent escape.
The stable-range mirror shortcuts (the dominant lockfile version and the
fresh ETag-less mirror) now hold the same conditions as v12's
`range_pick_is_stable`:
- `minimumReleaseAge` rules them out only for a package it applies to.
A package in `minimumReleaseAgeExclude` keeps them, so its range
resolves from the mirror when the lockfile version dominates, instead
of asking the registry.
- An offline or prefer-offline pick never tries them. Such a pick reads
the mirror first, and the shortcuts only re-read the mirror that read
could not pick from.
`decideMirrorShortcuts` reports the release-age answer it computed, and
`mirrorWrittenAfter` takes it, so a pick asks the exclusion policy at
most once.
`maybeUpgradeAbbreviatedMetaForReleaseAge` ran for every pick whose
packument lacks `time`, including a pick that already reads full
metadata. For a registry whose full documents omit `time`, it fetched
the same full document again without validators after every 304, and
`persistUpgradedMeta` rewrote the full mirror with its ETag as
`fullEtag`. The next revalidation of the full mirror then sent no ETag
and downloaded the body. A full document has no more complete form, so
a full metadata pick now skips the upgrade, as pnpm v12's
`release_age_upgrade_needed` does.
`metadataCachePolicy.ts` holds what pnpm v12's `metadata_cache_policy`
decides: the document kind a mirror revalidates as, the conditional
request and the unconditional refetch both request pipelines send, the
release-age upgrade's request, the header line a fetched document is
mirrored with (`mirrorWrite`, now the only writer for fetched
documents), and the uncacheable gate (`mirrorMayAnswer`). `mirrorEtags`,
`holdsFullMetaInAbbreviatedMirror`, `fullEtagOfAbbreviatedMirror`, and
`revalidationOfHeldFullDocument` are gone.
The release-age upgrade of a mirror that holds the full document now
builds the same request as that mirror's revalidation, so
`memoizeFetchMetadata` serves it the revalidation's 304. Before, the two
requests differed only in property order, which gave them different
memo keys and sent the same conditional request twice.
Part of pnpm/tasks#146.
`route.ts` holds the decisions `pickPackage` makes before it reads a
mirror's content, as pnpm v12's `pick_package::route` does:
`readsMirrorFirst`, `decideMirrorShortcuts`, `mirrorWrittenAfter`, the
unvalidated-mirror checks, and `decideCacheHitCheck` for an in-memory
hit. `pickPackage.ts` keeps the reads and the content checks.
`canReuseStableCachedRange` and `isUnverifiedPickSafe` are gone: both
asked the same stable-range question. No behavior change. Table tests
cover each decision.
Part of pnpm/tasks#146.
* perf(npm-resolver): revalidate the full document a release-age upgrade already holds
The minimumReleaseAge upgrade asked for the full document with no
validators. Some registries' full document lacks a complete `time` map,
so the upgraded abbreviated mirror has none either. Each later install
then upgraded it again and downloaded the same body. That happened when
the mirror was older than minimumReleaseAge, and again within one install
when a revalidation returned a changed full document.
Both upgrade sites now send the validators of the full document they
already hold: the full ETag, `modified`, and `Cache-Control: no-cache`
for an uncacheable one.
- maybeUpgradeAbbreviatedMetaForReleaseAge uses `meta.fullEtag`.
- upgradeFreshMetaForReleaseAge uses the response ETag when that response
answered a full request.
An abbreviated document still sends none. The existing notModified
handling keeps the document in hand and marks it. This is the v11 half
of pnpm/pnpm#17041, and the changeset now targets both CLIs.
Part of pnpm/tasks#146.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(npm-resolver): scope the upgrade's 304 comments to the request that gets them
The upgrade request can now carry the held full document's validators.
The NotModifiedWithoutCache comment applies only to a request without
validators. A notModified answer confirms that the held full document is
current.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(npm-resolver): cover three installs over a registry without complete publish times
The first install makes an abbreviated request and a full-document
upgrade. The second and third each revalidate and upgrade with the full
ETag. That is four 304s, and the full document is downloaded once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The npm resolver copied the packument's top-level `name` into the picked
manifest without checking it against the requested package. That copy
exists for the GitHub Packages quirk (a package published as
`@owner/foo` whose manifest says `foo`), but it also let a registry
answer a request for `@scope/a` as `b`. The resolved identity then
became `b@x`, so `allowBuilds` rules for `@scope/a` no longer matched
and rules for `b` did.
Both stacks now fail with ERR_PNPM_REGISTRY_NAME_MISMATCH when the
packument's `name` differs from the requested name, and always pin the
per-version manifest name to the requested name. A packument without a
top-level `name` still resolves, under the requested name.
* chore(release): 11.28.6, pacquet 12.14.0
* docs(release): curate 11.28.6 and pacquet 12.14.0 release notes
Add lead paragraphs and group the patch entries by what the reader was
doing, with security first on the v11 page.
- v11: lead with the NPM_ID_TOKEN audience check and the invalid file
name warning as security fixes; say the `pnpm setup` message was
printed, since v11 reports it as a warning.
- v12: merge the stdout and stderr broken-pipe entries and the two
optional-peer entries; say the Windows Rust toolchain change undoes
the MinGW fallback a MinGW-built pnpm used since 12.13.0; correct the
`browser` packing claim; drop the reference to another release line.
- Use one issue-link label style on both pages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(release): smooth two sentences in the 11.28.6 and 12.14.0 notes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(release): say sideEffectsCache.exclude covers the local and remote cache
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: give sideEffectsCache.exclude an example a cache key cannot cover
An environment variable such as JAVA_HOME could be added to the cache
key, so it does not justify excluding a package. Name the system
libraries a native addon links against instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore(release): 11.28.5, pacquet 12.10.0
* docs(release): curate the 11.28.5 and 12.10.0 release notes
Add a lead paragraph to each page and group the patch entries under
subject headings, with the security fixes first.
- Merge the unpublish path-boundary fix into the unpublish entry, the
legacy metadata cache prune into the metadata cache entry, the two
binary size entries, and the two 64 MiB archive limits on the v11 page.
- Drop the manifest-utils saveType entry from the CLI page: the CLI
only passes saveType values derived from its own flags, so no CLI user
can reach the fixed path.
- Rewrite mechanism-only entries (PAX header validation, bundled
dependency symlinks, custom resolutions under name@version keys, proxy
setting validation) in terms of what the user sees.
- Use one link style for pnpm/pnpm issues.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The mirror is encoded from the parsed document, so nothing read
FetchMetadataResult.jsonText any more. Removing it also drops the
JSON.stringify that normalizing a full document returned to an abbreviated
request paid only to fill that field.
The memo cache still swaps a settled entry for a clone, because the encoded
mirror is memoized on the original result object.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The metadata mirror stored one JSON document per package, so every
resolution parsed every version manifest of every packument it touched:
178MB of JSON for the 1246-package benchmark fixture, of which ~1% (the
picked versions) was ever read. loadMeta was 23% of resolution CPU after
pnpm/pnpm#13504.
The Rust CLI already shipped an indexed mirror layout (headers record,
index record with per-version byte spans, concatenated raw version
fragments); the TypeScript CLI treated those mirrors as cache misses,
so the two stacks thrashed a shared cache. Both CLIs now read and write
the same layout under the format id pnpm-meta-v1, and the metadata
cache directories move from v11 to v12 so a pnpm version that predates
the layout never opens the files at all. The offline legacy-mirror hint
now looks under v11, and pnpm cache prune removes the v11 roots. NDJSON
remains the layout for filterMetadata documents and is still read as a
fallback.
Loading an indexed mirror parses only the two leading records and
builds versions as enumerable parse-once getters over byte spans, so a
manifest keeps one identity across filtered views and hook mutations.
Corruption is treated like an unreadable mirror in both stacks: a
fragment that does not parse throws a typed error, offline resolution
fails with NO_OFFLINE_META, the disk fast paths fall through to the
network (only for errors attributable to the disk copy), and a 304 over
a damaged file escalates to a cache-bypassing refetch that rewrites the
mirror — otherwise the intact etag would keep 304-validating a poisoned
file forever. The declared headers length is bounded, the encoded
mirror form is memoized per fetch result so workspace projects sharing
one in-flight fetch do not each serialize a copy, and cache view reads
through the shared loader in both CLIs.
Resolution of the 1246-package fixture is 16% faster on a warm mirror
(3.82s -> 3.21s, interleaved paired runs, offline); the resulting
lockfile is byte-identical across old-format, new-format, and
mixed-format configurations.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Verify configuration dependency registry resolutions before installing
plugins, with independently checked pinned tarballs. Require config dependencies to resolve from a registry and skip
registry verification when the installed config dependencies already match
the lockfile.
Constrain archive and deployment paths and preserve content-addressed files
when streaming package entries. Bound metadata allocation before the archive
parser buffers it, using an opt-in patch to the upstream tar crate. Bound
publishing manifest and README buffers without imposing a total extraction
disk quota.
Bind audit signatures to lockfile integrity, hash ambiguous URL dependency
directory names, and sanitize license terminal output. Preserve dependency
error context and structured fatal logs. Update affected documentation,
release notes, and regression coverage.
Lockfile verification in both pnpm v11 and v12 now verifies the inner
resolutions of a variations wrapper against the registry, and rejects a
name@version entry whose variations resolution is empty or has a variant
without a resolution. New verifier cache markers keep a verdict recorded
before these checks from being reused.
Custom resolutions under name@version keys stay accepted: a project's
own custom fetcher owns the trust decision for what it fetches.
Fixes https://github.com/pnpm/tasks/issues/86
A specifier that opens with a protocol no resolver supports, such as
Yarn's `patch:got@npm%3A11.8.2#~/.yarn/patches/...` published in
`@yarnpkg/core@4.9.2`, contains a `/`, so the local resolver claims it by
its path shape. Both stacks linked it to a directory that does not
exist. On Windows, pacquet instead failed with a bare `os error 123`:
Rust maps ERROR_INVALID_NAME to ErrorKind::InvalidFilename, so the
missing-directory fallback never ran.
When the local resolver finds that directory missing and the specifier
opens with a `<scheme>:` of two or more characters, both stacks now fail
with ERR_PNPM_UNSUPPORTED_PROTOCOL, naming the protocol and the
specifier, with a hint to replace it through `overrides`. The check runs
only on the missing-directory branch, so successful resolutions do no
extra work. In pacquet the error lives in resolver-base, and the tree
walker recovers it by downcast so the code survives the type-erased
resolver chain.
pnpm_fs::is_not_found treats Windows ERROR_INVALID_NAME as NotFound,
matching Node.js, and the manifest readers and the local resolver use
it. safe_read_package_json_from_dir reports other read failures with the
manifest path.
Closespnpm/pnpm#16590
* chore(release): 11.28.4, pacquet 12.9.1
* docs(release): curate 11.28.4 and pacquet 12.9.1 release notes
Add a lead paragraph to both pages. Move the two credential-leak fixes
in 11.28.4 to the top and group the remaining 27 entries by what the
reader was doing. Merge entries that described one change: the two
INVALID_SETTING validations, the two metadata revalidation fixes, the
two hoisted filtered-install fixes, and, in pacquet 12.9.1, the two
[<since>] selector fixes. Order pacquet 12.9.1 so the `@pnpm/wasm` move,
which WebContainer users must act on, comes first.
Normalize issue links to the [#N] form and link the ETag fix to
pnpm/pnpm#16506.
Every fix was checked against the code released in 11.28.3 and
pacquet 12.9.0; none fixes a defect that never shipped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(release): describe the frozen-lockfile fixes accurately in the 11.28.4 lead
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A metadata response whose Cache-Control forbids reuse (max-age=0,
no-cache, no-store) marks its mirror uncacheable. Since pnpm/pnpm#15722
the next install asked for such a packument without If-None-Match or
If-Modified-Since, so every install downloaded and parsed every
packument again. bit.cloud proxies npm packuments with
"public, max-age=900, no-store", so a Bit workspace installing through
the Node-API addon used about twice the peak memory of 12.7.0 and took
three times as long.
The request for an uncacheable mirror now keeps its validators and
adds a no-cache Cache-Control header. An intermediary must then check
with the origin before it answers, and the origin answers a current
mirror with a 304. Dropping the validators gave no extra protection: an
intermediary that ignores no-cache would serve a stale 200 just as
well. A 304 to a no-cache revalidation is accepted even when its own
Cache-Control forbids reuse. The validator-free retry stays for a
mirror without the flag. A mirror that disappears after a 304 is still
refetched without validators.
Closespnpm/pnpm#16528
* docs(agents): add the documentation and comment principles
Import the principles that transfer from KSXGitHub/perfectionist's
contributor guide, plus four of our own: concise and non-repeating text,
structured text over prose, removal over addition when reviewing or
fixing nitpicks, and a definition of the user-facing text that must not
name implementation details.
The root guide's "Comments" section gains "Register", "Prose that cannot
go stale", "User-facing text", and "Reviewing documentation and
comments". The pacquet style guide adds the Rust specifics (clap and
napi doc comments are user-facing; a default is documented on the
field, not the variant). DOCUMENTATION.md gains the renderer and link
table for each markdown surface.
The guides themselves are brought in line: counts over growable lists
become plain references, a quoted heading becomes an anchor link, and
the size limits are stated once, in the style guide, instead of three
times.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs: keep user-facing text free of implementation names
The `@pnpm/napi` README and type declarations named the Rust engine, the
in-repo package name, and TypeScript internals (`filterPkgsDiff`,
`nameFormatter`) that a user of the addon cannot reach. The pnpmfile
page in both docs trees explained a limitation by the IPC mechanism
behind it. Four changesets named the release line or implementation,
which the package targets already express.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs(crates): cut restating and stale comments in pack, modules-yaml and nine more crates
Apply the comment principles to pack, versioning, pnpr-client,
package-is-installable, fs-packlist, workspace-task-scheduler,
engine-runtime-node-resolver, exportable-manifest, modules-yaml,
pnpr-registry, and pnpr-auth. Removed: call-site comments that repeat
the callee's doc, module rosters of exports that the `pub use` lines
already list, "not yet implemented" notes for behaviour that exists
(`beforePacking`, `hoistedLocations`), body narration, and history
framing. Comment-only; no code changes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs(crates): cut restating and stale comments in the small resolver, catalog and pnpr crates
Apply the comment principles to the local, tarball, default, deno and
jsr resolvers, the catalog crates, detect-libc, config-parse-overrides,
env-replace, git-utils, matcher, diagnostics, and the pnpr policy,
cargo, error, osv and fixture crates. Removed: parentheticals that
repeat a `#[diagnostic(code)]` attribute, counts over lists of
variants, callers and entry points, rosters of callers, doc paragraphs
that repeat a sibling's or the module's text, comments narrating the
match arms or condition below them, and history framing. The
`pnpr-prepare --help` text no longer names the test harness.
Comment-only; no code changes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs(crates): cut restating and stale comments in publish, workspace, registry and graph-hasher
Apply the comment principles to publish, auth-commands, registry,
workspace, lockfile-verification, default-reporter, the git resolver,
deps-inspection, hooks, graph-hasher, package-manifest, and the pnpr
shared-artifacts and route crates. Removed: a module roster of every
export, counts over lists, plan notes for work that landed ("Phase 5",
"Phase 6"), call-site comments repeating the callee's doc, docs
narrating match arms, a default claim on a `#[default]` variant, a
misattached doc, and history framing. Comment-only; no code changes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs(crates): cut restating comments in napi, reporter and pnpr crates
Apply the comment principles to napi, reporter, resolving-resolver-base,
real-hoist, git-fetcher, workspace-manifest-writer, and the pnpr config
and storage crates. Removed: "Mirrors X in index.d.ts" sentences on
`#[napi]` items and other implementation names in addon-facing docs,
stale "not emitted yet" and "when it lands" notes for behaviour that
exists, step-by-step comments narrating the body, emit-site and caller
rosters, docs duplicated from a sibling or the module, a doc attached to
the wrong item, and history framing. Comment-only; no code changes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs: cut restating and stale comments across the remaining crates and pnpm11
Apply the comment principles to the rest of the Rust workspace and to the
TypeScript CLI: the cli crate and its clap definitions, package-manager,
config, patching, the deps resolver, lockfile, pnpr, the npm resolver,
tarball, network, executor, cmd-shim, the benchmark and fixture tooling,
and the pnpm11 packages.
Removed: comments narrating the body, docs repeating a callee's, a
sibling's or the module's text, counts over growable lists, rosters of
callers and exports, "not yet implemented" notes for behaviour that
exists, history framing, and `@param` lines restating a signature.
One exception to comment-only: in `pnpr/crates/pnpr/src/resolver/resolve.rs`
the `Workspace` struct sat inside the doc block written for `resolve`, so
the struct moved above it and each doc now attaches to its own item.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs(crates): cut body narration and duplicated docs in resolver, fs and package-manager
Removed: a numbered "Sequence:" list narrating `ensure_file`'s body and
the paragraph restating its backoff constants, the stress-test module
doc's roster of its own tests, refactor-history paragraphs in the peer
walk's finalize pass, call-site and caller-side copies of a callee's doc
(the `optional` AND-fold stays documented on `register_seeded_package`
and on `ResolvedPackage`), a verbatim quotation of a TypeScript test's
expected output in the pnpr search module doc, and field and digest-input
rosters in the package-manager.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs(deps-restorer): cut plan references and re-attach misplaced docs
Removed the "slice 4" plan references and the comments narrating the
bodies they sat above. The `Auto` downgrade-chain doc moved from
`copy_and_log` to `auto_link`, the function it describes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs(deps-restorer): cut the remaining body narration in the hoist path
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs: cut the last body narration and a history clause in the cmd-shim test
`build_graph.rs` kept a `patches` parameter doc that re-derived what
`BuildOptions::patches` documents. The PowerShell-arguments test on main
said the arguments survived "before the shim ended its batch context
too", which describes the change rather than the behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs(cmd-shim): point the PowerShell-shim link at its current module
Main moved `wants_powershell_shim` into `windows_shim_policy`, so the
shim writer's link to it no longer resolved and `cargo doc` failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs(cli): link the fixture the ignored-build setup installs
The shortened doc named `workspace_with_unapproved_build` in backticks,
which `perfectionist::bare_identifier_reference` rejects for an item that
resolves in scope. It is an intra-doc link now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs(crates): restore two contracts and fix four dangling references
A review of this branch's sweep flagged seven places. Six needed a
change:
- `from_ini` documents again that a relative `cafile=` resolves against
`npmrc_dir`, a precondition its callers have to honour.
- `hash_object` documents again that the private serializer's bytestream
has to match pnpm 11's, because the digest is persisted and read by
both.
- The hooked-manifests note no longer says "above" of a gate that lives
in another module.
- The store-dir race test states its synchronization contract instead of
labelling it "Post-Option-C".
- `GraphNodeHashOptions.nodeVersion` keeps `undefined` as a literal.
- The pnpr streaming module drops the sentence fragment that introduced
its list.
The seventh, a `beforePacking` limitation in exportable-manifest, stays
removed: the note said the pnpmfile bridge cannot supply that hook, and
`pnpm_pack::lifecycle::apply_before_packing` has been running it since
before this branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs(cli): say in `pnpm why --help` that `--exclude-peers` is ignored
Demoting the caveat to an ordinary comment took it out of the help,
which then advertised a flag that does nothing: `WhyArgs::run` never
reads `exclude_peers`, and pnpm 11's `why` handler never forwards its
`excludePeers` either. The doc comment states that behavior in the words
a user sees, and the parity reason stays in a comment beside it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
* docs(cli): drop the doc comment that described the wrong module
The comment described the `pnpm_deps_inspection as deps_tree` re-export
above it, but a doc comment attaches to the item that follows, so rustdoc
showed it as the description of `mod dispatch`. The `use` item already
names the alias, so the comment goes rather than moving.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYTBQo8HY7xD2iz6hjo1CW
---------
Co-authored-by: Claude <noreply@anthropic.com>
A minimumReleaseAge upgrade stores the full packument in the abbreviated
metadata mirror. The full document's ETag cannot validate an abbreviated
request, so the mirror kept no ETag and the next fetch could only send
If-Modified-Since. registry.npmjs.org ignores that header, so every
revalidation of a recently published package downloaded a body, and the
abbreviated body that replaced the cached full document triggered the
upgrade again.
The mirror headers gain a fullEtag field, written only when the
abbreviated mirror holds a full document. A fetch of that mirror asks for
the full document with If-None-Match set to that tag, and a 200 answering
it is stored the same way. etag keeps describing the abbreviated
representation, so the two validators never mix, and an older pnpm reads
such a header as having no ETag.
Both the Rust and TypeScript resolvers change, including the lockfile
verifier's abbreviated fetch, which shares the request builder.
Closespnpm/pnpm#16506
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* chore(release): 11.28.3
* docs(changelog): curate the 11.28.3 release notes
Merge the eleven entries about names such as `constructor` into one,
group the patch changes by what the reader was doing, and add a lead
paragraph.
Drop the zlib-rs entry: that change only touched Cargo.toml, and the
TypeScript CLI does not use the Rust engine. It belongs to the next
pacquet release.
Rewrite the store index entry for CLI users. `ReadOnlyStoreIndex` and
`ImmutableStoreIndex` are library API documented in the
`@pnpm/store.index` changelog.
Say what the store prune mark-phase fix changes for the user.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Add TypeScript ports of the perfectionist rules that the Rust workspace
enforces through dylint.toml to @pnpm/eslint-config, with the same limits,
and fix every finding that needs no refactor.
The size and shape rules (function length, cognitive complexity, nesting,
local bindings, file length) are enforced for packages that already meet
them. The packages that do not are listed in PENDING_SIZE_AND_SHAPE_REFACTOR
in eslint.config.mjs and are refactored in follow-up changes.
The custom rules mirror perfectionist's semantics rather than reusing
sonarjs or max-lines-per-function, so that one limit means the same thing
in both codebases: a closure is a level of the body that contains it, a
nested function declaration is measured on its own, and a test-framework
callback plays the role of a #[test] function.
Bugs found and fixed along the way:
- a registry error body cut at exactly 64 KiB lost its truncation marker
- removing a dependency could leave dangling .bin shims
- constructor/__proto__ names crashed the projects graph, were skipped by
the injected-deps syncer, and were dropped from writeWantedLockfile's
returned lockfile
The CI chunk runner learns to run the node:test suite of the ESLint
config package.
When minimumReleaseAge drops the version a stable dist-tag points to, the
tag could only move to stable versions, so `latest` on a freshly released
1.0.0 fell back to an ancient 0.0.1 even though 1.0.0-beta.4 had been
`latest` until then. A prerelease of the target's major that precedes it
now ranks above a stable version of a lower major. A stable version of the
same major still wins, and non-deprecated versions still win over
deprecated ones.
Fixed in both the Rust resolver and the TypeScript metadata filter.
Closespnpm/pnpm#16388
* chore(release): 11.28.1, pacquet 12.8.0, pnpr 0.1.0-alpha.14
* docs(release): park Rust changelogs and curate release notes
The release bump ran with pnpm 12.7.0, which since pnpm/pnpm#14934
writes the changelog of every private project to a committed
CHANGELOG.md. pacquet, `@pnpm/napi` and `@pnpm/pnpr` are private
in-repo but published, and release.yml and the publish-time changelog
composer both read their parked section. Move the three sections back
to .changeset/changelogs so the GitHub release bodies and the npm
tarball changelogs are not empty. The tooling follow-up is recorded
on pnpm/tasks#38.
Curate the pnpm 11.28.1, pnpm 12.8.0 and pnpr 0.1.0-alpha.14 pages:
add lead paragraphs, group patch entries by subject, merge entries
describing one change, drop internals no CLI user sees, and fix
"instead of" tails, implementation names and link placement. This
addresses both Qodo findings on the catalog entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Carry the original minimumReleaseAge cutoff alongside the tighter time-based
selection cutoff. If the first pick finds no match, search for the lowest
age-eligible match before falling back without the cutoff.
Reuse the existing picker so preferred versions, exclusions and version
guards remain effective. Include both cutoffs in Rust resolved-edge cache
keys. Preserve the original first choice and the existing violation handling
when every matching version is immature.
Fixes https://github.com/pnpm/pnpm/issues/16298
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A relative `file:` specifier resolves against the directory of the manifest
that declares it. A package from the registry or a tarball has no directory
until it is placed on disk, so pnpm fell back to the importer's directory and
failed with ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND. npm and Yarn install the
directory the package ships.
Resolution: before any readPackage hook or override runs, a relative `file:`
entry in the published manifest of a non-directory package is rewritten to
`link:<root>/<path>` when the normalized path stays inside the package. A
tarball target, an absolute or home-relative path, the package's own
directory, and a path leaving the package keep today's behavior. Both
stacks accept a `link:<root>/` target only when every segment is a plain
name (no `.`, `..`, empty segment, backslash or `:`), so neither a
lockfile nor a published manifest can re-anchor the link through a
platform path join. Rewriting
before the hooks means a `file:` specifier an override or a pnpmfile writes
keeps its usual meaning, with no need to keep a pre-hook copy of the
manifest. The resolver records a `link:<root>/...` edge as a link without
running the resolver chain or reading the target, which only exists once
the parent is placed.
Lockfile: the parent snapshot lists the edge as `link:<root>/<path>`. Plain
snapshot `link:` refs are relative to the lockfile directory, so `<root>`
cannot be mistaken for one; a directory literally named `<root>` is not
valid on Windows and unused elsewhere. An older pnpm reading such a lockfile
treats `<root>` as a path segment and leaves a dangling link, which is the
dependency missing, as it was before.
Linking: every consumer of a snapshot `link:` ref resolves `<root>` against
the declaring package's directory. That covers the isolated linker on fresh
and frozen installs, the global virtual store, the hoisted linker, PnP, the
package map, and `list`/`why` paths. The hoister gets a never-hoisted
workspace-kind node for the edge, which keeps it next to its package and
reserves the name there. The GVS hash skips these edges, because the
parent's integrity covers their content, and hashes them identically on the
fresh and frozen paths. Deploy passes the ref through unchanged.
Limits: like any `link:` target, the linked directory's own dependencies are
not installed and its lifecycle scripts do not run. A missing target leaves a
dangling link, as npm does.
`pnpm add ../pkg` saves the directory as a `link:` dependency, which is a
symlink, so Node resolves the package's peers from its own directory and
never from the project that added it. `pnpm link` already warned about this
and suggested `file:`. `pnpm add` said nothing, which is how users kept
running into it.
Both CLIs now share one warning between `link` and `add`. It fires only
for an alias-less directory selector (`../pkg`, `link:../pkg`) whose
manifest declares peer dependencies, and never for `file:`, which injects
the package and resolves its peers from the project.
Closespnpm/pnpm#5523
In offline mode the resolver picked the newest version in the cached
metadata even when that version's tarball was not in the store, so the
install failed with ERR_PNPM_NO_OFFLINE_TARBALL although an in-range
version was already stored.
For an offline range pick, both resolvers now check the store index
(same integrity + name@version key the fetch layer uses). When the
picked version is missing, the packument is narrowed to the store-held
versions and the usual preferences are applied again. When the store
holds none of them, the unrestricted pick is kept so the existing
failure surfaces unchanged. Exact versions and tags are not adjusted.
TypeScript v11 reuses peekManifestFromStore. Rust v12 adds
OfflineStoreView, which memoizes presence per key and the narrowed
packument per route, and wires it into the install resolver chain, the
add/update pre-resolution, the configured registry resolver, the
config dependency resolver and the standalone chain. Both stacks derive the store key from the pinned
integrity, including the sha1 form of a legacy dist.shasum.
Closespnpm/pnpm#10715
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Under `resolutionMode: time-based`, both stacks merge the time-based
cutoff (newest direct dependency publish date plus one hour) and the
`minimumReleaseAge` cutoff into one `publishedBy`, keeping the tighter.
The npm resolver uses that date to pick versions and also to flag
`minimumReleaseAge` violations, so a subdependency newer than only the
time-based cutoff was reported as immature. Strict installs failed with
ERR_PNPM_NO_MATURE_MATCHING_VERSION and loose installs added the version
to `minimumReleaseAgeExclude`.
Keep the merged cutoff for picking. Where the dependency tree collects
resolver-inline violations, check each `minimumReleaseAge` violation
again against the `minimumReleaseAge` cutoff and exclude alone. The
picking cutoff is never later than the `minimumReleaseAge` one, so the
recheck can only drop violations that the time-based cutoff produced.
A deeper alternative is to pass the resolver a separate detection
cutoff. That touches the resolve options and cache keys in both stacks,
so the recheck at the single collection point is the smaller change.
Closespnpm/pnpm#13569
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
StackBlitz WebContainers reject asynchronous fs calls with errors that
inherit from Error but were not created by the native constructor, so
util.types.isNativeError() returns false for them. pnpm used that check
to recognize ENOENT and other codes, so a missing pnpm-lock.yaml was
rethrown and pnpm install failed on any project without a lockfile.
Add isError() to @pnpm/error, which accepts a native error or any
instanceof Error, and use it at every non-test call site. The native
check alone is still needed for Jest, where errors from another realm
fail instanceof. Update the AGENTS.md guidance to match.
Closespnpm/pnpm#15649
When `minimumReleaseAge` is set and the lockfile has no publish date for a
locked package, the resolver skips the store fast path and fetches registry
metadata to run the maturity check. It then returned the registry's manifest
instead of the tarball's. A registry whose metadata disagrees with the tarball
(seen with a Nexus npm group) made an unrelated re-resolution rewrite the
package's recorded `peerDependencies`, for example `'*'` to `11.12.0`, which
`pnpm peers check` then rejected.
The store manifest is now read once up front. When the registry pick confirms
the locked package (same id and integrity), that manifest is returned together
with the publish date from the metadata, which is what the resolver returned
before the maturity check started requiring the registry fetch.
pnpm v12 builds the manifest of a reused locked package from the lockfile, so
it is not affected.
Closespnpm/pnpm#13988
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Registries that omit ETag cannot answer If-None-Match with 304, so a warm
resolve re-downloads every packument. A public mirror younger than five
minutes and stored without an ETag is reused when the cached packument
already satisfies the range. A mirror marked uncacheable or dated in the
future is not reused. Registries that send an ETag still send a
conditional request. pnpm update still fetches current metadata.
Closespnpm/pnpm#13976
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Gabriel Mari S. Flores <Shuuren@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Fetching a git dependency locked to an SSH remote now says to load a key
with ssh-add when git reports a publickey refusal, and still explains how
to re-record a pre-v11.21 entry with pnpm update.
Resolving an SSH URL that fails the same way shows that advice plus a
local HTTPS insteadOf rewrite when the remote logs in as git. The
rewrite uses a shell-safe host and no userinfo, so a password or a
hostile host cannot reach the command.
Closespnpm/pnpm#13743
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
When a metadata response carries Cache-Control max-age=0, no-cache or
no-store, the mirror's header line records uncacheable. The next install
that looks the package up online sends Cache-Control: no-cache without
If-None-Match or If-Modified-Since, so a stale intermediary cannot answer
it with a 304. Offline and prefer-offline still serve the mirror.
Registries that send a positive max-age keep conditional requests.
A 304 to a revalidation whose own Cache-Control forbids reuse is asked
once more without validators. Mirrors written before this change carry
no flag, so without that retry a stale intermediary would keep hiding new
versions from existing users until they pruned the store. A second 304
still serves the mirror.
The flag only governs the on-disk mirror. A packument fetched during an
install stays in the in-memory cache for the rest of that install, so a
registry that always sends max-age=0 is not asked again for every
dependency edge of the same package. Online paths that promote a mirror
into memory skip a flagged mirror, so memory never holds one.
If writing an uncacheable response to the mirror fails, the previous
mirror is deleted. Its header would otherwise carry validators into the
next request.
The TypeScript and Rust Cache-Control parsers agree: directive names are
case-insensitive, max-age is compared numerically, and a qualified
no-cache="field" does not forbid reuse of the document. Only response
headers decide cacheability; an uncacheable field in a registry body is
ignored.
Closespnpm/pnpm#13487
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Part of #15656: this fixes the offline error (section 1 of the issue). The pnpm cache prune warning (section 2) is not addressed here, so the issue stays open.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Default rawWantedDependency to an empty string in parseWantedDependency,
so calling it with undefined or with no arguments no longer throws a
TypeError. Add unit tests for parseWantedDependency and a changeset.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(local-resolver): hard link workspace:../pkg deps under injectWorkspacePackages
injectWorkspacePackages picked file: (hard-linked) resolution for a
workspace: dependency matched by name/semver range, but a workspace:
dependency written as a relative path (workspace:../pkg) went through
the local-resolver's own scheme parser instead, which decided the
protocol from the per-dependency injected flag only and never saw the
injectWorkspacePackages setting. That kept a relative-path workspace:
dependency symlinked even with the setting enabled, while workspace:*
correctly injected it.
Thread injectWorkspacePackages through to that parser in both the
TypeScript CLI and pacquet, the same way the name/semver
workspace-matching path already does, so both forms of a workspace:
specifier agree on whether the dependency is injected.
Tested with a regression test in each stack that fails without the fix
(resolve_workspace_directory_with_inject_workspace_packages in pacquet,
"resolve workspace directory with injectWorkspacePackages" in the
TypeScript CLI), plus a manual reproduction: a two-package workspace
with injectWorkspacePackages: true and a workspace:../b dependency now
records file:../b (hard link) instead of link:../b (symlink) in the
lockfile and node_modules.
Closespnpm/pnpm#10446.
* test: cover relative-path workspace injection at install level
Add install-level regression tests in both stacks for a
`workspace:../project-1` dependency under injectWorkspacePackages. They
disable dedupeInjectedDeps, because a childless injected workspace
project is deduped back to `link:` and would hide the bug.
Also trim the comments that restated the same rationale in three places.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: fix optional dependencies access in the injection test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore(release): 11.28.0, pacquet 12.7.0, pnpr 0.1.0-alpha.13
* docs(release): curate the 11.28.0 and 12.7.0 release pages
Group both pages under subject headings with a lead paragraph that
names the headline changes and the security fixes, and move the
security entries to the top of the patch section.
Merge entries that describe one change: the node shim version files,
ranged selectors without a package.json, SemVer build metadata,
completion injection, custom modulesDir command lookup,
list --only-projects, store status, npm_execpath, catalog and npm-alias
workspace edges, the macOS CA fallback, and the prepare/devPreinstall
skips.
Drop entries that describe no change a published version could show:
the test-only --no-optional entry on both pages, the exported
CreateNewStoreControllerOptions type, the publish availability-probe
fix for a flag first shipping in 12.7.0, and the executable-bit check
the Rust CLI never had wrong.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(release): describe the bin shim part of the modulesDir entry accurately
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
When adding a dependency with an exact version (`pnpm add pkg@<version>`),
honor the requested exact version instead of inheriting the previous range
operator (such as `^`) from an existing entry in `package.json`.
In pnpm v11 (`calcVersionRange`) and pnpm v12 (`calc_version_range`), the
requested specifier's inferred style now takes precedence over the existing
entry's style. An unstyled request (e.g., dist-tags like `latest`) still
preserves the previous range operator.
Closespnpm/pnpm#6040
npm expands `${VAR?}` in .npmrc to the variable's value, or to an empty
string when the variable is unset. Both pnpm stacks looked up `VAR?` as
the variable name, so the placeholder never resolved and a
"Failed to replace env in config" warning was logged.
The Rust env-replace crate treats `${VAR?}` as `${VAR:-}` without
recording it as unresolved. Only a non-empty name without another `?`
or a trailing `-` counts, which keeps `${NAME-?}` free for
pnpm/pnpm#11667. The TypeScript reader gives envReplaceLossy
a `VAR?` entry for each such placeholder, since the parser lives in the
external `@pnpm/config.env-replace` package.
Closespnpm/pnpm#14404
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Several `@` entries in `_auth` each inferred a default-registry route, and
the last one won. With per-scope registries declared in
pnpm-workspace.yaml and one `@` credential per registry, the default ended
up pointing at a scoped registry, and lockfile verification fetched
unscoped packuments from it.
Env `_auth` now collects `@` entries as default candidates. A candidate
whose URL a config file (.npmrc, global or workspace yaml, CLI) assigns to
a scope is dropped: that credential authenticates the scoped registry. If
more than one candidate remains and a config file declared the default,
the declared default is kept. Otherwise the last candidate wins, as
before. The global config `_auth` default follows the same scoped-URL
filter. `_auth` routes themselves never count as scoped declarations, so
`{"X": {"@": ..., "@org": ...}}` still makes X the default.
A scope the env var re-routes no longer counts as declaring its old URL.
In v11 the bootstrap `packageManagerRegistries` runs the same selection
over the trusted `.npmrc` declarations, matching the v12 bootstrap.
The resolution verifier now matches a lockfile tarball URL against the
named-registry prefixes and the configured scope registries, longest
prefix first. `default` is never a candidate, and a package whose scope
has its own registry only matches that scope registry, so a lockfile
cannot move `@a/pkg` onto a registry that also proxies the public one.
Closespnpm/pnpm#15530
A global package installed from a `file:` path that was later deleted
(for example one installed from a temporary directory) made every
`pnpm update --global` abort with ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND,
leaving the remaining groups unchecked.
Both stacks now check each group's `file:` dependencies before
reinstalling it. When a source no longer exists, the group is skipped
with a warning on the global log channel naming the missing path, and
the update continues with the other groups. The skipped group stays
installed and keeps working from what it installed.
The path is resolved by a new `localFilePath` / `local_file_path` helper
in the local resolver, so the check follows the resolver's own rules
for relative, home-relative, and drive-letter paths.
Closespnpm/pnpm#12533
On Windows, the relative branch of fromLocal built normalizedBareSpecifier
from path.relative() without normalize(), so `pnpm add` and `pnpm update`
wrote `file:vendor\library-a.tgz` and `link:..\bar` to package.json and the
lockfile specifier. The id and dependencyPath were already normalized, and
v12 already writes forward slashes.
Closespnpm/pnpm#7497Closespnpm/pnpm#9687