* chore(release): 11.28.9, pacquet 12.16.0, pnpr 0.1.0-alpha.17, get-pnpm 0.0.10
* docs(release): curate the 11.28.9, 12.16.0, and pnpr alpha.17 release notes
- Add a lead line to each page and group the patch entries by what the
reader was doing, with security fixes first.
- Merge the duplicate strictSsl/localAddress entries and the repeat-install
setting entries into one list per page.
- pnpr: split the removals into commands, flag aliases, and camelCase
config keys, and say that pnpr no longer serves PUT /-/pnpm/v1/publish,
so batch publishing needs pnpm 11.28.9 or 12.16.0.
- Drop the "Updated dependencies: get-pnpm" line from the pnpm v12 page
and list the get-pnpm credential fix first.
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>
`force_symlink_inner` retargets a stale link by unlinking it and
reissuing the create. Nothing bounded that step: when the create
reported `AlreadyExists` again, the link was read, found stale again,
unlinked again, and the function called itself again. On a filesystem
that answers the unlink with `NotFound` but keeps serving the entry (a
`node_modules` mapped from a lockfile by a FUSE daemon does this for an
entry it synthesizes), the recursion only ended when the rayon thread
ran out of its 2 MiB stack, and the host process died with SIGSEGV.
Seen as a crash of Bit's CI runner during a rebuild of install-script
packages against such a mount.
`TriedOnce` gains `retargeted`, set on the retry after the unlink. A
link that still reads as stale on that retry is reported through the
create's own error, with the link, its current target and the wanted
target in the message, so the caller sees which entry the filesystem
would not let go of.
The test's mock create puts the stale link back before reporting the
conflict, which is what the caller observes on such a filesystem.
Without the bound the test aborts with a stack overflow.
The repeat-install oracle now edits overrides, a pnpmfile hook,
patchedDependencies, dedupePeers, a workspace project's bin and a scope's
registry route, and checks every gate against a full install after each
edit. Its snapshot records each importer's .bin entries and the files of
every registry dependency, and a selection's view adds its .bin entries and
the lockfile snapshots it reaches.
Two gate bugs it found are fixed:
- With dedicated lockfiles, the install fast path and the run check read
patch files from the project's own directory, while the config hashes
them from the workspace directory, so an edited patch went unnoticed.
Both now take the paths from Config::patch_file_paths.
- pnpm v11 records each patch file as an absolute path and pnpm v12 as
configured, so each took the other's record for a changed setting. Both
now resolve the recorded paths against the workspace directory before
comparing them.
The run gate compares workspace-wide inputs, so after an edit outside the
manifests the oracle allows it to refuse a selection the edit leaves as it
is. The route scenario is checked against the install in place, because
the routes a lockfile was resolved through live in the workspace state.
The bin scenario skips dedicated lockfiles until pnpm/tasks#224 is fixed.
Related to pnpm/tasks#219.
With nodeLinker: hoisted and autoInstallPeers: false, a package that two
workspace projects use with different peer versions got only one copy,
so the second project resolved the first project's peer. pnpm v11 nests
the second variant; this was a v12-only regression.
The hoister now keeps a peer-bearing node's own snapshot key as its
reference. A node without peer names still collapses onto the
first-seen variant, as pnpm v11's depPathByPkgId does, which keeps
peer-variant-heavy lockfiles from exploding the per-path walk. When
autoInstallPeers is on, peer names are empty and every variant
collapses as before.
The hoisted dep-graph walk indexes package locations by exact snapshot
key as well as by package id. Importer direct dependencies and child
edges resolve to their own variant's copy first, then fall back to the
package id for variants the hoister collapsed.
Keeping the collapse and adding yarn's peer check to the hoister's
dedup path also places the copies correctly, but the walker then sees
two copies with one snapshot key. The hoisted linker treats those as
duplicates, builds one, and symlinks the other, which cross-links the
variants again.
On a large real lockfile with autoInstallPeers off, the hoisted tree
grows from 8358 to 17157 nodes. The yarn-parity alternative produces
17570, so the growth is the copies peer resolution requires.
Closespnpm/pnpm#17395
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The real-path guard from pnpm/pnpm#16619 accepted only files whose real path
lies under the project directory. When publishConfig.directory is reached
through a symlink, every file in it resolves outside the project, so pack
and publish shipped only package.json.
Bound the packed files and the bundled dependencies by the real paths of
both the resolution root and the packed directory. Links that resolve
outside both stay excluded. Resolve bundled dependencies from the real
packed directory upward, then from the resolution root when the packed
directory lies outside it, so dependencies in the publish directory's own
node_modules and in the project's node_modules are both bundled. The
fallback applies to the resolution root alone: a bundled package's own
dependencies resolve from its real directory the way Node resolves them,
never from the publish directory's node_modules.
pnpm v12 is not affected: it keeps regular files under the packed directory
and compares symlink targets against the canonical package directory.
Closespnpm/pnpm#17390
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
When the live frame outgrew the terminal, the overflow handover shrank
the frame to the committed lines and a fresh differ redrew every
visible line. A frame that grows by one line per event, like
`pnpm run --stream` output, rewrote the whole screen for every line.
The handover now diffs the whole uncommitted frame against the one on
screen, which still fits the terminal, so only changed and new lines
are written and the overflow scrolls away. The restarted differ takes
the visible lines as already drawn. Fixed in both the v12 reporter and
the v11 live output.
Closespnpm/pnpm#17387
The global `config.yaml` and `PNPM_CONFIG_*` may set the legacy `proxy`
and `noproxy` keys, and pnpm v12 overlays them onto its bootstrap. The
v11 bootstrap and get-pnpm read them from the `.npmrc` only, so a
`proxy: false` in `config.yaml` left the environment proxy in effect.
Both now take every proxy key from the first layer that names it.
get-pnpm also reads the global `config.yaml` the way pnpm does: a file
it cannot read or parse throws instead of being skipped, `%NAME%`
references in `LOCALAPPDATA` and `XDG_CONFIG_HOME` are expanded on
Windows (one that names no variable throws), and `${NAME-default}` /
`${NAME:-default}` placeholders fall back to their defaults.
Covered by a v11 bootstrap test and four get-pnpm tests; each fails
with its fix removed.
Related to https://github.com/pnpm/tasks/issues/215
When a workspace package renames or removes a bin entry in package.json,
its link: reference in pnpm-lock.yaml stays identical, so repeat
installs only linked the current bin commands and left stale shims in
dependent projects' node_modules/.bin and in the workspace root's
node_modules/.bin when hoist-workspace-packages is enabled.
Track the command names claimed during each project bin-linking pass in
both pnpm v12 (pnpm-cmd-shim + pnpm-deps-restorer) and pnpm v11
(@pnpm/bins.linker + @pnpm/installing.deps-restorer +
@pnpm/installing.deps-installer), and prune any unclaimed symlinks or
pnpm-generated shims (including .cmd, .ps1, and .exe siblings on
Windows) from real project .bin directories once the pass completes.
Claim interrupt relay slots before spawning child processes and
register child process groups with the group watchdog from pre_exec
before execve so signals arriving while spawn is in flight are queued
and delivered.
Fixes https://github.com/pnpm/tasks/issues/211
The Config classification test from pnpm/pnpm#17383 listed settings a
full install acts on and the fast path never compared. These now go into
the workspace state, and the fast path and the run check compare them:
- `virtualStoreDirMaxLength`: a full install renames the virtual store
slots.
- `dangerouslyAllowAllBuilds`: turned on, it allows the builds the last
install ignored.
- `trustLockfile`: turned off, the lockfile no install verified has to be
verified.
- `globalVirtualStoreDir` (v12 only): a full install links from the new
directory.
The first three exist in both CLIs and are fixed in both. An unrecorded
value reads as the default, so a state written before this change keeps
the fast path for anyone on the defaults, and each CLI reads the other's
state without seeing a change. v11 writes `virtualStoreDirMaxLength` and
`dangerouslyAllowAllBuilds` with every state, so v12 writes them too.
`trustLockfile` and `globalVirtualStoreDir` are written only while they
differ from the default.
The run check ignores `virtualStoreDirMaxLength` and
`globalVirtualStoreDir`, as it ignores `virtualStoreDir`: a script runs the
same whatever the virtual store's slots are named or where they live.
Part of pnpm/tasks#219.
The worker pool and the default workspaceConcurrency took the CPU count
from os.availableParallelism(), which counts the CPUs in the process's
affinity mask. In a container with a cgroup CPU quota smaller than that
mask, such as a Kubernetes pod with a 4-CPU limit on a 233-CPU node,
pnpm 11 created 232 workers per install, and four concurrent installs
exhausted a 16 GiB memory limit while the workers were still loading.
Node's libuv does try to apply the quota, but its cgroup v1 parser
looks for ":cpu," and keeps "cpuacct:" in the path it builds, so it
never finds the quota file there, and both it and Rust's standard
library give up when the cgroup path from /proc/self/cgroup is not
mounted under /sys/fs/cgroup, as happens with a host cgroup namespace
and a bind-mounted controller.
A new package, `@pnpm/os.available-parallelism`, reads the quota itself:
the smallest limit/period walking from the process's cgroup up to the
hierarchy root, for cgroup v2 (cpu.max) and v1 (cpu.cfs_quota_us and
cpu.cfs_period_us), rounded down and floored at 1 like Rust's
std::thread::available_parallelism, which pnpm 12 already uses. The
cgroup directory is located through /proc/self/mountinfo, which says
which cgroup each mount shows, so a container that sees only its own
cgroup bind-mounted at /sys/fs/cgroup is read correctly; the standard
mount points are the fallback. The worker pool and the config reader's
workspace-concurrency default both use it, so the two stay in step.
pnpm 12 is unaffected: every Rust pool is sized with
std::thread::available_parallelism, which honours the quota.
Closespnpm/pnpm#17382
A plain install that selected only some workspace projects purged the
shared root modules directory when a layout setting changed, but relinked
only the selected projects. The projects it skipped lost their
dependencies: under the hoisted linker they live in the root modules
directory, and under the isolated linker their links point into the
purged virtual store.
Both CLIs now recreate the modules directory only for an install of every
project. A filtered install fails with the drift's error instead. This
covers nodeLinker, hoistPattern, publicHoistPattern,
virtualStoreDirMaxLength, the store, the virtual store, and the layout
version. In v11 a project installed with other dependency groups still
purges as before.
In v12 the unsafe-target error for a filtered install is unreachable now
and is removed. The refusal accepts some drift that the layout check
flags, such as a tree linked from the sibling v3 store. For that case the
install clears is_inconsistent, so it merges into the recorded current
lockfile and .modules.yaml.
Closespnpm/tasks#217.
pnpm v11 built the proxy settings of the package-manager bootstrap
(`pnpm self-update` and the download of the pnpm version pinned in
`packageManager`) from the trusted `.npmrc` files and the `HTTPS_PROXY`
family of environment variables only. `httpProxy`, `httpsProxy`, and
`noProxy` set in the global `config.yaml`, through `PNPM_CONFIG_*`, or as
`--config.httpsProxy` applied to ordinary registry requests but not to
the bootstrap. pnpm v12 already overlays those layers onto the
bootstrap proxy.
`createPackageManagerNetworkConfig` now picks each proxy key from the
first layer that names it, in the project precedence: command line
(`configFromCliOpts`, camelCased so both spellings), then
`PNPM_CONFIG_*`, then the global `config.yaml`, then the trusted
`.npmrc`. The existing cascade then runs over the result: the legacy
`proxy` key, the https-to-http fallback, and the unprefixed environment
variables. A layer that names a key with a value that reads as unset
masks the lower layers and falls through to the environment, as in v12.
For `noProxy`, only a host list or `true` counts as configured: an
empty string, `false`, or an array (which `PNPM_CONFIG_NO_PROXY` parses
JSON into) falls through to the `noproxy` spelling and then `NO_PROXY`,
as ordinary requests and v12 do.
The environment's proxy settings are read up front by
`readEnvProxySettings` instead of being mirrored in `applyEnvSetting`
like `strictSsl`: the bootstrap's `httpProxy` derives from its
`httpsProxy`, so overwriting the resolved value after the cascade ran
would leave the two inconsistent. `applyGlobalYamlConfig` returns the
file's three proxy settings next to `strictSsl` and `localAddress`, and
`OptionsFromRootManifest` types `noProxy`. The project
`pnpm-workspace.yaml` stays out of the bootstrap.
Covered by seven tests next to the "package manager bootstrap" tests in
`pnpm11/config/reader/test/index.ts`. With the overlays removed, the
four precedence tests fail; with the `noProxy` check removed, the
three fallback tests fail.
get-pnpm: `pnpmProxyConfig` resolves the proxy for the Corepack
download the same way: `PNPM_CONFIG_*` over the global `config.yaml`
over the user `.npmrc`, each key from the first source that names it,
then the legacy `proxy` / `noproxy` keys and the environment variables.
Empty, `false`, and `null` read as unset; `proxy=false` turns the proxy
off, environment included; `noProxy: true` becomes Node's `*`. The
project `.npmrc` and `pnpm-workspace.yaml` stay out. There is no
command-line layer, since Corepack hands the launcher pnpm's own
arguments. `corepackDownloadOptions` returns the result as `proxy`,
also for `COREPACK_NPM_REGISTRY`, and `downloadPnpmExecutable` hands it
to `useProxyFromEnv`, which now takes the proxy variables as a
parameter and still defaults to `process.env`. Covered by unit tests of
`pnpmProxyConfig` and `corepackDownloadOptions`, and by two download
tests against a forwarding proxy; with the proxy dropped from either
function, three of them fail.
Closes https://github.com/pnpm/tasks/issues/215
A contract test reads the fields of `Config` from its source and requires
each to be classified: recorded in the workspace state, ruling the fast
path out, reaching it through another input, not an install input, or a
known gap. A recorded field is proven by changing it and asserting the
install gate reports its setting key, and a blocking field by asserting
the fast path is ruled out. A new `Config` field fails the test until it
is classified.
The gaps the classification found were checked with the CLI: changing
`virtualStoreDirMaxLength`, `globalVirtualStoreDir`, or turning on
`dangerouslyAllowAllBuilds`, or turning off `skipRuntimes` leaves the fast path reporting the tree up
to date while a full install changes it.
Also drop the `dedupe_peers` value every freshness caller passed through
from the config (`GatePolicy`, `CheckLockfileSettingsDriftOptions` and
the content checks). The checks now read it from the config, so no gate
can compare the lockfile against a hard-coded value again, the bug fixed
by pnpm/pnpm#17328.
Part of pnpm/tasks#219.
`pnpm store prune` keeps the packages listed in each registered project's
installed lockfile, which it finds under the project's modules directory.
A workspace that gives each project its own lockfile can move one
project's modules directory through `packageConfigs.<name>.modulesDir`.
The prune ignored that entry, missed the lockfile, and fell back to the
hard-link count, so copied, cloned, patched, and built packages of that
project could be removed.
The prune now reads `packageConfigs`, `sharedWorkspaceLockfile`, and
`lockfileDir` next to `modulesDir` and applies the entry naming the
project, as the install does: only when the workspace does not share one
lockfile. pnpm 12 already registers each project of such a workspace on
its own, so no registration change is needed.
Closespnpm/tasks#214
macOS has no `pipe2`, so Rust's std creates a pipe and sets `FD_CLOEXEC`
on it afterwards. When a parallel run spawns its scripts from several
threads, a script forked in between inherits the write end of the
watchdog's lifeline. After pnpm dies that script still holds the pipe
open, the watchdog never reads end of input, and no group is killed.
Every spawn of the executor crate now holds a shared lock and the
watchdog's spawn an exclusive one, so no fork happens while the watchdog's pipe is
inheritable, and the watchdog cannot inherit another child's pipes
either, such as the `ps` listing's output read on cancellation.
Closespnpm/tasks#216
`recorded_registry_routes_after_install` took a bare `resolved_all: bool`,
which its callers passed as `false` or as an unnamed expression. A
`ResolvedImporters` enum names the three cases the rule distinguishes:
every importer, the selected ones, or none. The name avoids the lockfile
crate's `LockfileResolution`, which is a resolution kind.
Addresses the CodeRabbit nitpick on https://github.com/pnpm/pnpm/pull/17373,
which merged before the change was pushed.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
checkLinkedPackagesAreUpToDate creates one local directory checker per
importer. Its checks share the `visited` map and the recorded peers, so a
snapshot reached through several direct dependencies is walked once per
peer scope instead of once per direct dependency.
The visited key (snapshot plus peer scope) and cycle handling are
unchanged. A revisit returns true while the first visitor may still be
checking the snapshot. That is safe because the importer's results are
combined with AND, so the first visitor's failure still marks the importer
outdated. This is the same contract siblings within one walk already had.
The recorded peers are collected on the first check, so an importer with
no local directory dependency does not scan the locked packages.
The one-shot localDirectoryDepIsUpToDate wrapper had no callers left and
is removed.
On the issue's 200-package file: DAG, a repeat install went from ~1.75s to
~0.37s and a frozen install from ~1.0s to ~0.35s.
Closespnpm/pnpm#17374
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(install): refuse nodeLinker changes on v11 mutations
* fix(install): keep node_modules on a filtered install after a nodeLinker change
A plain install that selected the workspace root purged the root modules
directory on a nodeLinker mismatch even when other workspace projects were
not selected. Under the hoisted linker those projects keep their
dependencies in the root modules directory, and the filtered install does
not relink them, so they lost their dependencies.
The purge for a nodeLinker mismatch now runs only when the install covers
every workspace project. Otherwise it fails with ERR_PNPM_NODE_LINKER_DIFF.
Adds tests for the whole-workspace purge and the root-only refusal, and
documents the error in the v11 docs.
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>
createPackageManagerNetworkConfig now reads `https-proxy`, `http-proxy`,
and `proxy` as unset unless they are a non-empty string. `false`, `null`,
and an empty value fall through to the next setting and then the
environment, matching resolveProxySettings for installs and pnpm v12.
An explicit `proxy=false` remains the way to turn the proxy off, and it now also stops the
download's `httpProxy` from falling back to `HTTP_PROXY`, as installs do.
The engine identity check of the pinned pnpm now takes its network
settings from getPackageManagerBootstrapConfig, like the store controller
that downloads it, instead of the project's proxy, TLS, and auth settings.
Closespnpm/tasks#193
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The v12 command reference (pnpm/docs/cli) had drifted from the clap
definitions, and the error codes the docs name had drifted from the ones
the source emits. Nothing caught either.
Two unit tests in pnpm-cli now compare the docs with the code, following
the settings test from pnpm/pnpm#17269. docs_command_pages maps every
visible clap command to its page and checks both directions: a flag
without a heading, and a heading for a flag the command rejects. Pages
may delegate to another page, import .mdx partials, and document a
--[no-] pair with either side; a flag that spells a setting is claimed
by ConfigOverrides::extract before clap sees it, so the test asks that
entry point, and the -s shorthand is accepted everywhere too.
docs_error_codes checks that every ERR_PNPM_* code the docs name is
emitted by a source file under pnpm/, with the migration guides exempt,
and keeps errors.md in alphabetical order. Each test keeps a list of
today's gaps, and fixing a gap means deleting its entry.
The gaps the tests found are fixed: add documents its include, lockfile,
dedupe, and script flags; deploy names the install options it accepts; remove
documents --[no-]trust-lockfile; import documents --pnpr-server; recursive.md no longer
presents --link-workspace-packages as a recursive option; the
frozenStore section drops the v11-only ERR_PNPM_FROZEN_STORE_INCOMPLETE.
errors.md in both docs trees opens with what a code is for, lists its
codes in alphabetical order, and adds the codes users meet most. The
doctor page stops describing v11 (no skipped store check, no @pnpm/exe
install method), and the exec page states that pnpm parses its own
options up to the command name on either side of `exec`.
DOCUMENTATION.md gets the reference-page templates and the rules the
three drift tests parse.
Part of https://github.com/pnpm/tasks/issues/191.
A plain lockfile key (`@scope/a@1.0.0`) records no registry, so the
reuse gate read its registry from the current scope route. Routing the
scope to another registry was invisible: the lockfile stayed fresh, the
install went headless, and the locked versions and children were
fetched from the new registry with the old integrity. The install
failed where resolving against the new registry would succeed.
The workspace state now records the scope routes an install resolved
the lockfile under (registryRoutes). A resolving unfiltered install
records the routes it ran under; a frozen or filtered install keeps the
previous record while a plain key still sits under a moved route. The
default registry is not recorded: switching it is routinely a mirror
switch and would resolve every unscoped package again.
The registry context carries those routes as the provenance of plain
keys. The reuse gate reads a plain key's registry from them, so a key
under a moved scope is refused, loses its pin, and resolves again from
its range against the new registry. In pacquet the subtree-reuse walk
also refuses a subtree holding a moved key, because a reused parent
seeds its children with exact pins and only its fresh manifest carries
the ranges. A plain key under a moved route marks the lockfile stale
for a resolving install; --frozen-lockfile installs it as recorded.
A route change is a settings drift for the repeat-install shortcut.
The route: entries of registryPrefixes are redundant with the new
record and are dropped (that record is unreleased).
Closes https://github.com/pnpm/tasks/issues/209
Under nodeLinker: hoisted, both CLIs reuse a package at a location the
previous install's hoistedLocations records. Nothing marked an install in
progress, so an install that stopped after replacing a recorded location
but before writing .modules.yaml left the old record behind. Reverting the
change then reused the replaced files.
Before the linker touches the tree, both CLIs now rewrite .modules.yaml
without every recorded location the install does not reuse as is. A
repeat install that reuses everything writes nothing extra.
A narrow window remains between the final .modules.yaml write and the
current lockfile write, which this change keeps in their existing order.
Closespnpm/tasks#182
Both CLIs built the network config for self-update and the pinned-pnpm
download (the package-manager bootstrap) from the trusted `.npmrc`
files and the command line's `.npmrc`-spelled tokens only, so
`strictSsl` / `localAddress` set in the global `config.yaml` or through
`PNPM_CONFIG_STRICT_SSL` / `PNPM_CONFIG_LOCAL_ADDRESS` applied to
ordinary registry requests but not to the bootstrap. Both sources are
trusted, so the bootstrap now reads them with the project precedence:
trusted `.npmrc` < global `config.yaml` < environment < command line.
The project `pnpm-workspace.yaml` stays out of the bootstrap.
v12: `WorkspaceSettings::apply_tls_to` no longer consumes its fields,
and `Config::load_sources` applies it to `package_manager_bootstrap.tls`
where it already overlays the proxy settings: in
`apply_bootstrap_settings` (global `config.yaml`), `apply_env_settings`
(`PNPM_CONFIG_*`), and `apply_cli_setting_values`. The last one is what
makes `--config.localAddress` reach the bootstrap: the CLI kebab-cases
the key into `setting_values`, while `npmrc_tokens` keeps it as given
and the `.npmrc` parser does not know the camelCase spelling.
v11: `createPackageManagerNetworkConfig` resolves `strictSsl` and
`localAddress` as `configFromCliOpts` (camelCased, so both spellings),
then the global `config.yaml`, then `trustedConfig`. The command line
comes first because its kebab-case tokens also sit inside
`trustedConfig`, where the global yaml would otherwise override them.
`applyEnvSetting` mirrors the two settings onto
`packageManagerNetworkConfig` as it does for `registry`; it already
skips keys the command line set. `applyGlobalYamlConfig` returns the
file's two settings next to the registries, so
`addSettingsFromWorkspaceManifestToConfig` returns the manifest's
settings (copying the command line over them instead of mutating them)
and `PnpmSettings` types `strictSsl` and `localAddress`, which are yaml
settings since b65204762a. `TYPED_WORKSPACE_MANIFEST_KEYS` and its Rust
mirror list them too.
Covered by `tests::tls_settings` in pnpm-config and the "package
manager bootstrap" tests in `pnpm11/config/reader/test/index.ts`; with
the overlays removed, three tests fail in each stack.
Closes https://github.com/pnpm/tasks/issues/201
The settings a repository must not control (ci, stateDir, scope, globalDir,
globalBinDir, macosBackup, globalShims, tools, symlinkedLockfileTargetDirs)
move from WorkspaceSettings into a new MachineSettings struct. A project's
pnpm-workspace.yaml and a config dependency's entry parse into
WorkspaceSettings only, so clear_machine_only_settings and the separate
globalShims clearing go away. A new machine setting can no longer reach a
project file because someone forgot to add it to a list.
The global config.yaml, PNPM_CONFIG_*, the command line, and the
updateConfig hook round-trip read into TrustedSettings, which holds both
halves. The global file is read once per half rather than through
serde(flatten), which buffers values untyped and would break scalar
settings such as `tag: 1.0`. The machine half is parsed only when the file
mentions one of its keys.
The pnpm-home pnpm-workspace.yaml keeps setting globalShims. A global
command reads that one key from it explicitly.
The refused-key warning and the `pnpm config set` refusal now cover every
MachineSettings key, derived from the struct. Keys that
`pnpm config set --global` cannot write (tools) are routed to the global
config.yaml and their PNPM_CONFIG_* variable.
updateConfig hooks keep reading and writing machine settings as before,
except the two already hidden from them, because hook code runs as the user.
Closespnpm/tasks#207
The git fetchers wrote store-index rows with no bundled manifest, so the
warm prefetch re-derived `requiresBuild` from the row's files on every
install, reading and parsing `package.json` once per git dependency, and
the cached git prepare check could read it again.
`queue_files_index` now reads the manifest once, derives `requiresBuild`
from it and the file list, and stores the normalized manifest in the row,
as pnpm v11 does. Rows with a bundled manifest keep their stored
`requiresBuild`; only rows written by older releases are re-derived.
`normalize_bundled_manifest` moves from `pnpm-tarball` to
`pnpm-package-manifest` so the git fetcher can use it. The
manifest-plus-files build check moves there too as
`requires_build_from_files`, next to a new
`read_package_json_from_cas_paths` that `requires_build_from_cas_paths`
and the warm git prepare check now share.
Closespnpm/tasks#188
An optional dependency that fails to resolve is skipped unless the wanted
lockfile already holds an entry that satisfies it. The check only accepted
plain semver ranges, so a specifier with a named-registry prefix such as
`work:^1.0.0` was always skipped and its locked entries were dropped.
Both stacks now let the existing lockfile-reuse spec parser
(`locked_child_admits` in v12, `getRangeOfLockedPackage` in v11) judge
the entries. The parser unwraps a declared prefix or built-in alias to its
range and registry, and it matches only entries locked from that registry.
Closes https://github.com/pnpm/tasks/issues/210
Windows Rust CI aborted pnpr-auth libsql tests with 0xc0000005 (four
times recently: registration_cap_is_strict_across_backend_instances
three times, sync_server::bulk_revocation_refreshes_a_replica_whose_tokens_went_elsewhere
once).
libsql's LibsqlConnection::drop calls disconnect() and then the inner
local::Connection's own Drop calls disconnect() again. Both see the
drop_ref Arc as unique, so sqlite3_close_v2 runs a second time on a
handle SQLite already freed. Usually it reads freed memory as "closed"
and reports misuse. When another thread reused that memory, it read
garbage and crashed. The tests that crashed run several connections
concurrently, which makes reuse likely.
Upstream fixed it in tursodatabase/libsql#2261 (released in 0.10.0),
which nulls the handle after the first close. pnpr stays on the pnpm
fork because tursodatabase/libsql#2301 is still unreleased, so the fix
is cherry-picked onto the fork's pnpm-0.9.30 branch.
The new test installs an SQLite log callback, drops one connection and
asserts no misuse was logged. It logged two before the fork bump.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The CAS prune removed a store file whose hard-link count was 1. A
package imported with clone or copy leaves its store files at one link
while a project still uses it. That is every patched or built package,
every package under packageImportMethod: copy, and every package of a
macOS clone. So the prune removed them, and the next install downloaded
them again.
The prune now also keeps every file that a registered project's
installed lockfile references:
- For each registered project it reads <modulesDir>/.pnpm/lock.yaml and
the lock.yaml in the virtualStoreDir recorded in
<modulesDir>/.modules.yaml. modulesDir comes from the nearest
pnpm-workspace.yaml at or above the registered directory.
- Each package's store index keys come from the install's own key
functions: both keys of a git-hosted package, the manifest-keyed and
plain keys of a runtime, and every variant of a variations
resolution.
- Every file of each row is kept, including each side-effects entry.
- A project with no installed lockfile and a package with no row add
nothing, so the hard-link check still decides for them.
- A lockfile of an unsupported version is skipped with a warning. A
lockfile, .modules.yaml, pnpm-workspace.yaml, or store index row that
cannot be read fails the prune with
ERR_PNPM_STORE_DIR_PRUNE_IN_USE_FILES. Registry errors keep their own
codes.
In pnpm 12, StoreDir::prune_keeping runs a caller's collector under the
prune lock, and the CLI supplies the collector because store-dir does
not depend on the lockfile crate. pnpm 11 collects before any removal.
pnpm 11 and pnpm 12 share one store. pnpm 12 keys a runtime row with
its synthesized manifest, so the pnpm 11 collector also keeps
package-manifest keys whose tail it references. It scans the index keys
only when a lockfile holds a runtime.
A modulesDir set only on the command line, in the environment, in the
global config, or in packageConfigs is not seen.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The legacy directory matcher rejected equal paths, so a directory selector
skipped its own project and a negated selector failed to exclude it.
Treat an empty relative path as a match in the shared directory helper.
Cover inclusion, exclusion, sibling-prefix boundaries, and recursive exec.
pnpm v11 already includes the directory itself and needs no change.
Closespnpm/tasks#196.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Reject `-T` on `add` with an error that names `--save-types` and `--tilde`.
`add` has both `--save-types` and `--tilde`. Accepting `-T` would force pnpm to
guess between two long options that are both valid for the command, so the
parser reports the ambiguity and points at the explicit spellings instead.
`-T` is declared as a hidden short flag on `add` and rejected in
`validate_command_scoped_global_options` after clap parses. Going through clap
keeps short clusters (`-DT`), attached values (`-CT`, `--save-prefix=-T`) and
tokens after `--` handled by clap's own rules. The `install <pkg>` -> `add`
rewrite runs before parsing, so `install foo -T` gets the same error. The flag
is hidden, so `-T` does not appear in the help text.
This change is v12-only. pnpm v11 does not expose both options on `add`.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tats-u <12870451+tats-u@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A pnpr resolve request carries the client's registries as declarations
keyed by URL. Each declaration held one prefix, so when a project routed
two prefixes to one URL (for example through namedRegistries), only the
smallest reached the server. A dependency or lockfile entry using the
other prefix then failed on a delegated install.
The request protocol adds an `aliases` list next to `prefix`. `prefix`
stays the smallest prefix, the one lockfile keys carry, so the server keys
packages the way the client does and re-keys a lockfile entry written with
another declared prefix. `aliases` exists only in the request protocol.
pnpm-workspace.yaml refuses it in both CLIs, so no new setting is added
to v11.
The v11 config reader now defines prefix routes as own data properties,
so a `__proto__` prefix is kept instead of hitting the inherited setter.
Closespnpm/tasks#204.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Extend PHILOSOPHY.md, pnpr/AGENTS.md, REVIEW_GUIDE.md, .coderabbit.yaml, and
.pr_agent.toml so the repository philosophy applies to pnpr and establishes
that pnpr is pre-1.0 on the alpha prerelease lane (experimental per
PHILOSOPHY.md), consolidating storage layouts, internal record formats,
configuration keys, and CLI flags in place before 1.0 without
backward-compatibility fallbacks, aliases, or migration commands between alpha
builds.
- Remove pnpr upgrade-storage and Command::UpgradeStorage.
- Remove legacy .pnpr-journal recovery path and journal manifest serde aliases
(packument_file, tarballs).
- Remove StoredDerivedTar::Legacy and DerivedTar::scanned in .tar-integrity
records.
- Remove the redundant /v1 segment from compiler_cache_path.
- Remove CLI subcommand and flag aliases (pnpr config-check,
--packument-ttl-secs, --no-check-hashes, --min-age-secs, --max-age-secs) and
the PNPR_PACKUMENT_TTL_SECS env fallback.
- Remove YAML configuration snake_case, *_secs, maxage, and backend.postgresql
serde aliases, standardizing on camelCase.
The registry mock proxies real npm packages through its npmjs upstream.
pnpr's breaker opens after two consecutive upstream failures and stays
open for 5m, which outlasts pnpm's fetch retries. One network blip on a
Windows runner (an "error sending request" and a 504) tripped it, and the
retries of optionalDependencies.ts all got 503 "circuit open", failing
TS CI on main (run 40241451781).
With maxFails: 0 every retry reaches npmjs again.
Part of https://github.com/pnpm/tasks/issues/213.
- Store `.team-rosters` and `.rule-overrides` records under `<ecosystem>/<local_name>.json` regardless of whether the registry is declared at top level or under an `<ecosystem>:` block in `pnpr.yaml`, and remove the legacy flat-key fallback and `HostedBackend::remove_record_if_current`.
- Store package tombstones under `.tombstones/<ecosystem>/<scope>/<name>.json` via `package_segments` instead of flat `!`-escaped filenames.
- Normalize upstream names to `<ecosystem>/<local_name>` in `compute_upstream_cache_namespace` so moving an upstream between top-level `registries:` and an `<ecosystem>:` group does not invalidate its cache digest, and use `CanonicalPackageName::ecosystem()` directly in `Store::package_dir` and `S3Store::package_prefix`.
- Drop redundant `/v0`, `/v1`, and `pnpr-` prefixes from storage and cache directories (`.tombstones`, `.team-rosters`, `.rule-overrides`, `.scim-users`, `.pipeline-runs`, `.uploads`, `.journal`, `.staging`, `.artifacts`, `shared-artifacts`, `resolver-store`, `resolver-cache`) and migrate legacy keys in `pnpr upgrade-storage`.
pnpm/pnpm#17355 moved pnpr's hosted storage under default/<ecosystem>/,
so getIntegrity and hostedBlobPath looked in default/packages/ and every
v11 test that calls them failed on main.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A fix flow writes overrides or updates the lockfile but never records
ignores, so `--ignore` and `--ignore-unfixable` were silently dropped next
to `--fix` (any method) or `--interactive`, which implies `--fix`. Both
CLIs now fail with ERR_PNPM_AUDIT_FIX_WITH_IGNORE before loading the
lockfile or contacting the registry, naming the flags that were passed.
Part of https://github.com/pnpm/tasks/issues/191
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Separate hosted storage namespaces by ecosystem (`default/<ecosystem>/` and
`orgs/<org>/<ecosystem>/`) and map package names to path segments per
ecosystem:
- Hosted registries declared under an `<ecosystem>:` group without an
explicit `org:` now default their `org` to their own registry `name`
instead of `<ecosystem>~<name>`.
- Hosted registries in different ecosystems may share an `org:` without
colliding; the collision check in `pnpr-config` compares
`(org, ecosystem)`.
- `layout::package_segments` and `layout::decode_package_segments` take an
`Ecosystem`: npm maps scoped `@<scope>/<name>` to `[@<scope>, <name>]`
and unscoped to `[@, <name>]`; Cargo and PyPI map to `[@, <name>]`; OCI
maps multi-component `c_1/.../c_k` to `[@<c_1>, ..., @<c_{k-1}>, <c_k>]`
and single-component names to `[@, <name>]`.
- `pnpr upgrade-storage` converts legacy hosted stores into the
per-ecosystem layout and maps nested OCI repositories into multi-segment
`@<c_i>` directories.
Closes https://github.com/pnpm/tasks/issues/213
pnpr serves dist.tarball as <registry>/<name>@<version> on .tgz
registries since pnpm/pnpm#17340. pnpm v12 rebuilds those URLs for a
registry declared with `serverType: pnpr`, so it leaves them out of the
lockfile. The TypeScript CLI knew only npm and artifactory, so it wrote a
host-specific tarball URL for every package installed from pnpr.
Teach it the pnpr layout, matching v12: the package name unencoded and
the version with its build metadata. getNpmTarballUrl is the single
source of the shape, so the lockfile writer, the reader, and the fast
override rewrite agree by construction.
The e2e tests that assert a lockfile without tarball URLs now declare
`serverType: pnpr` for the mock registry, as the v12 tests do.
The deps-installer testDefaults and the shared command-handler
DEFAULT_OPTS declare `serverType: pnpr` for the mock registry. Config
dependencies do not read serverType in either CLI, so the env lockfile
tests expect the pnpr URL.
pnpm/pnpm#17351 moved pnpr's batch publish to PUT /-/pnpr/v0/publish,
so `pnpm publish --batch` now sends there, as v12 does.
The fast lockfile rewrite for override and catalog changes now gets
registryOptionsByUrl, so it applies the declared serverType.
The registry mock's getIntegrity and hostedBlobPath read pnpr's
packages/@<scope>/<name>/ storage layout from pnpm/pnpm#17353.
This is a maintainer-approved exception to the bug-fix-only policy for
the TypeScript CLI, for compatibility with pnpr.
A scoped package was one escaped segment, so `@pnpm-testing/fresh` was
stored as `packages/!40pnpm-testing!2ffresh/` and its tarball as
`blobs/fresh-1.0.0.tgz`, repeating a name the directory already holds.
The hosted store and the proxy cache now place every package two
directories below `packages/`:
- `@<scope>/<name>/` for a scoped name, `@/<name>/` for any other. No npm
scope is empty, so `@` cannot collide with a scope.
- A `.tgz`, `.sigstore` or `.crate` blob named `<basename>-<rest>` is
stored as `<rest>`, so a tarball is `blobs/1.0.0.tgz`. Such a file that
does not start with `<basename>-` (a non-canonical upstream tarball in
the proxy cache) is stored with a leading `@`, which `encode_segment`
never writes, so the two forms cannot collide. Every other file (OCI
digests, PyPI wheels and sdists) keeps its name.
- The rule keys on the extension rather than the ecosystem because
`pnpr verify` and the image maintenance walk namespaces read from
storage, where the ecosystem is not known.
- The prefix is not stripped when the rest would be empty or start with
`.`, so `foo-..tgz` cannot become a hidden `..tgz` file.
Each segment is still `encode_segment`ed, so names that differ only in
case stay apart on a case-insensitive filesystem. The mapping lives in
`layout.rs` with strict inverses; every backend and listing goes through
it, so no caller of the storage API changed.
`LAYOUT_VERSION` stays 1: pnpr is experimental, and an existing store is
republished rather than converted. `pnpr upgrade-storage` writes the new
paths when converting a pre-marker store, and the fixture fingerprint
hashes a sample path so cached fixture trees in the old layout are
rebuilt.
Mount batch::serve_ecosystem_publish at both /-/pnpr/v0/publish and
/npm/-/pnpr/v0/publish, remove the duplicate npm-only serve_batch_publish
handler on /-/pnpm/v1/publish, and point pnpm publish --batch at
/-/pnpr/v0/publish.
Since batch::serve_ecosystem_publish defaults entries without an explicit
`ecosystem` field to npm, mounting it on `/npm/-/pnpr/v0/publish` as well as
the server root lets `pnpm publish --batch` and cross-ecosystem batch
publishes share a single implementation and journaled commit path.
Closespnpm/tasks#208
Written by an agent (Antigravity, Gemini 4 Argon).
Mount DELETE handlers on /{name}, /@{scope}/{name}, /{name}@{version},
/@{scope}/{name}@{version}, /{name}/-/{filename}, and
/@{scope}/{name}/-/{filename} alongside the legacy /-rev/{rev} routes,
and accept bare /-/user/{name} on PUT and GET while still stripping the
legacy org.couchdb.user: prefix.
Related to pnpm/tasks#208.
Written by an agent (Antigravity, Gemini 4 Argon).
The subtree-reuse walk keyed its memo and cycle detection on a depth
bucket, so under a finite `--depth` a dependency cycle only closed once
the walk had gone round it once per level: `pnpm update --depth=<n>`
held a memo entry per node per level and recursed `n` frames deep. On a
1,600-package lockfile `--depth=9999` passed 6 GB before being killed,
and `--depth=30000` overflowed the stack.
Compute instead, once per update scope, each snapshot node's shortest
distance to an update target, by a breadth-first search from the targets
over the reversed snapshot graph. A node visited at depth `d` has a
target within the ceiling `n` iff `d + k <= n` for its distance `k`, and
when that fails for a node it fails for every node below it. The walk
checks it before the memo, so the memoised answers are purely structural
and need neither a depth nor an importer in their key, and the walk's
cost never depends on `--depth`.
Fixespnpm/pnpm#17329
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Rewrite served `dist.tarball` URLs on `.tgz` registries to
`{public_url}/{pkg}@{version}` for every addressable version (matching
tar registries), so `serverType: pnpr` drops canonical tarball URLs from
`pnpm-lock.yaml` on both `.tgz` and tar registries while keeping
`{public_url}/{pkg}/-/{filename}` served for existing lockfiles and
older clients.
- Emit `{public_url}/{pkg}@{version}` in `pnpr-upstream` for all
addressable versions and normalize `<pkg>@{version}` tarball URLs to
`<scopeless-name>-<version>.tgz` in `tarball_basename_for_package` for
chained `pnpr` instances.
- Accept round-tripped `{public_url}/{pkg}@{version}` URLs in
`check_tarball_immutable` during `PUT /:pkg/-rev/:rev` and restore the
stored `.tgz` URL on disk.
- Route `DELETE /:pkg@:version/-rev/:rev` and
`DELETE /@:scope/:pkg@:version/-rev/:rev` to `unpublish_version_tarball`
while rejecting reserved `+r<digits>` revision suffixes.
- Fall back to the version parsed from a canonical `<name>-<version>.tgz`
request in `expected_tarball_dist` so chained `pnpr` proxies resolve
non-canonical upstream filenames, and preserve `<name>@<version>`
routes in `TarballRouter`.
Related to pnpm/tasks#208.
get-pnpm moves from pnpm/get.pnpm.io into pnpm/npm/get-pnpm, at 0.0.9
(get.pnpm.io be4e983, where its history stays).
- The pnpm v12 npm package depends on it with `workspace:*`, so a pnpm
release bundles get-pnpm's source as of that release. A wrapper fix no
longer needs a get-pnpm release, a catalog bump, and a
minimumReleaseAge exclusion first. bundle-node-gyp builds it before
the deploy injects its build output.
- It is released on its own: a `get-pnpm` product in bump.ts and
create-release-pr.yml (off by default), `get-pnpm@<version>` tags,
and build-get-pnpm/publish-get-pnpm jobs in release.yml. It is private
in the workspace so the TypeScript release's recursive publish skips
it; the packed manifest drops the flag.
- Its runtime dependencies stay exact, through a `get-pnpm` named
catalog. Its lint is the root ESLint config. Its TLS tests use the
fixture the Rust network tests use.
- The npm-wrapper CI job runs its tests on Node.js 24 and 22, beside
the wrapper's. The forwarding proxy in its download tests forwards
only to the servers the tests start.
npm's registry signing keys were pinned in five places: v11, v12,
get-pnpm, and get.pnpm.io's install.sh and install.ps1. Only v11's copy
was refreshed, by the release PR, and the two repositories handled a
withdrawn key in opposite ways. They now come from
.config/npm-signing-keys.json:
- .github/scripts/update-npm-signing-keys.mjs refreshes it from npm on
every release PR, not only v11 ones. A key npm stops listing stays,
marked withdrawn.
- The meta-updater renders it into the v11, v12, and get-pnpm copies,
so `pnpm lint:meta` fails on a copy edited by hand. pnpm v11 and v12
keep trusting withdrawn keys, so packages signed before a rotation
verify. get-pnpm drops them, as get.pnpm.io's installers do.
- get.pnpm.io's update script is to read this file.
`pnpm audit --ignore` and `pnpm audit --ignore-unfixable` returned exit
code 0 right after writing the ignore list, so CI passed even when
vulnerabilities that were not ignored remained. Both CLIs now remove
every GHSA in the merged ignore list (the configured ignores plus the
new ones) from the report and apply the same audit-level check a plain
`pnpm audit` uses. The printed message is unchanged.
v11: the threshold filter is extracted into filterAdvisoriesByAuditLevel
and shared by the report and ignore paths.
v12: ignore_vulnerabilities returns the merged list it wrote, and
filter_ignored_advisories delegates to a new remove_advisories_by_ghsa.
Part of https://github.com/pnpm/tasks/issues/191
An importer that the wanted lockfile does not list falls back to
`all_locked_peer_versions`, which walks every snapshot and recovers the
peers of every hashed suffix. `LockedPeers::of` ran that walk once per
such importer, so N new workspace projects repeated the whole-lockfile
scan N times.
The result does not depend on the importer, so `WorkspaceTreeCtx` now
holds it in a `OnceLock` and every unlisted importer shares the same
`Arc`. pnpm v11 already shares this result, through the
`UNLISTED_IMPORTER` key in `getLockedPeerVersionsOfImporter`.
Closespnpm/pnpm#17311
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
`pnpm ci` handed its options to `pnpm clean`, which reads `--lockfile` as
"also remove pnpm-lock.yaml". For `ci`, `--lockfile` is the `lockfile`
setting, so the flag (and `--config.lockfile=true` in the TypeScript CLI)
deleted the lockfile and the frozen install then failed with
ERR_PNPM_NO_LOCKFILE.
`ci` now runs the clean step with lockfile removal off in both CLIs. The
Rust CLI no longer flattens clean's arguments into `ci`, so `--lockfile`
reaches the config layer as the `lockfile` setting, as in the TypeScript
CLI, and clean's `-l` shorthand is no longer accepted by `ci`.
A changed lockfile write refuses a symlinked pnpm-lock.yaml, because the
write would land on whatever file the link points at. Build systems that
stage a project's manifest and lockfile as links back to a source tree
need the update to reach that source file.
The new symlinkedLockfileTargetDirs setting lists the directories such a
write may reach. The write resolves the whole link chain with
canonicalize and, if the final target is inside a listed directory,
atomically replaces it, so every link stays in place and the target
keeps its mode. Any other target is refused, as is every changed link
when the list is empty. A cycle still fails, and an unchanged lockfile
is still never written. The refusal messages name the setting.
The list is a directory allowlist rather than a boolean because a global
true would apply to every project on the machine, including cloned
repositories the user does not trust. The listed directories are
canonicalized too. Entries may start with ~/, and relative entries are
dropped, because the global config and the environment resolve them
against the working directory.
The setting is machine-only: pnpm reads it from the global config.yaml
and PNPM_CONFIG_SYMLINKED_LOCKFILE_TARGET_DIRS, never from pnpm-workspace.yaml,
and updateConfig hooks can neither read nor change it.
The repository commits the symlink, so letting it also allow the write
would let a cloned repository overwrite any file the user can write,
even with scripts disabled.
pnpm import resolves a symlinked lockfile before backing it up, and
copies the target to its backup rather than moving it, so the imported
lockfile reaches the target and the link survives.
The policy is a SymlinkedLockfile enum that every lockfile writer takes,
so no writer can skip the setting. The main lockfile writers and the env
lockfile writer read it from the config. The napi writeLockfile export and
pnpr's scratch lockfile have no config and keep refusing.
v11 lists symlinkedLockfileTargetDirs among the pnpm v12 settings in its
unknown-setting warning.
Closespnpm/pnpm#17222