The Rust CLI registers only `--prod` on `install`, so the `pnpm install …`
command the verify-deps-before-run gate reproduces from a production-only
install — built with pnpm's `--production` spelling — was rejected by the
argument parser, aborting every `pnpm run` (Closes pnpm/pnpm#14147).
`--production` is the setting name behind `--prod`, and the TypeScript CLI
accepts it on every command where `--prod` selects dependency groups, so
restore it as an alias there too (`audit`, `licenses` and `outdated` already
had it), and emit the documented `--prod` in the reproduction command in
both stacks.
Closes#14147
The check that separates a rejected modify command line from an install
that ran and skipped a component watched for any Visual Studio installer
process by name. One already running for an unrelated reason would pass
for the one this step launches, putting the run back on the generic
"clang-cl was not installed" error it is meant to sharpen.
Snapshot the matching process ids before the launch and count only ones
that appear afterwards.
The 12.0.0-rc.11 release failed on both win32-arm64 legs with "clang-cl
was not installed under ...\VC\Tools\Llvm", five minutes after the
Visual Studio installer was asked to add the LLVM and ARM64 components.
`--wait` had been added to that `setup.exe modify` call. Microsoft
documents it as bootstrapper-only: "The --wait parameter can only be
passed into the bootstrapper; the installer (setup.exe) doesn't support
it." We invoke the installer at
`C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe`, so
it rejected the command line and installed nothing. The logs show the
call returning in under a second and no installer process alive for the
whole polling window.
The exit-code check could not catch that. `setup.exe` is a
GUI-subsystem binary, so PowerShell's call operator does not wait for it
and never sets $LASTEXITCODE from it — the value tested was still
vswhere's. Check vswhere's own result instead, where the variable
actually means something, and leave the poll loop as the sole
confirmation that the components landed, which is what it was written
for.
Regressed in pnpm/pnpm#14138; the split into separate CLI and NAPI
matrices is unaffected, it only doubled the number of legs running the
step.
`pnpm stage approve` accepted exactly one stage id and wrapped each request in
its own `withOtpHandling`, so releasing a workspace meant one invocation and one
proof of presence per package, in an order the releaser had to work out by hand.
It now takes a list of stage ids, and with none it lists the staged versions and
offers them in a checkbox prompt (the non-interactive case keeps failing with
STAGE_ID_REQUIRED, now with a hint).
The batch runs through a new `OtpSession` in the web-auth package: it keeps the
one-time password a challenge yielded and passes it to every later request,
re-prompting only once the registry stops accepting it — a classic TOTP expires
within a minute, which is exactly the case a whole-workspace approval hits.
`withOtpHandling` is now the single-operation form of that session, so publish
and reject behave as before.
Inside a workspace the selection is sorted through the workspace dependency
graph (the same sequencer the recursive commands use), keyed on the name each
project publishes under, since that is the only name a staged version carries. A
package whose workspace dependency could not be approved is skipped rather than
published against a dependency that never reached the registry. A registry
verdict on one version leaves the rest of the batch running and ends with a
non-zero exit; an authentication failure or a broken connection aborts it.
The registry's description of a staged version decides what a maintainer
publishes, so its id and package name are validated as they came — a hidden
character can neither be stripped into a valid value nor keep a name from
matching a workspace package — and the display-only fields are stripped of the
control characters that could redraw the picker around a selection. The shared
`@pnpm/text.sanitize` package holds that character set for both the staged
picker and `update --interactive`, mirroring the `pnpm-text-sanitize` crate.
Both stacks implement this, down to the partial-batch summary and exit code —
pacquet has no output-with-exit-code channel, so it prints the summary the way
the dispatcher prints command output and exits 1.
The sibling-linking pass for Bit root components (`link_root_component_members`) falls back to linking **every other member of the root** into a member's slot when the materialized copy carries no `package.json`. A Bit workspace component never carries one at engine link time — Bit writes a manifest into the copy only during its own post-install compile, and that manifest is stripped of sibling edges anyway — so on a real Bit workspace every injected member gets the all-member fallback: one symlink per (member, member) pair per peer context.
### Measured on teambit/bit's own workspace
- **270,840** `@teambit/*` symlinks under `node_modules/.pnpm/`, all inside the **812** injected-component slots
- a `@teambit/compositions` variant slot carries **359** sibling links where its lockfile snapshot declares **42** — the other 317 are the fallback
- the inode cost is paid on every install and every CI cache restore; on teambit/bit's CI the persisted `node_modules` tree grew 2.9 GB / 207k symlinks → 3.8 GB / 356k symlinks across the pnpm v12 migration, and per-node workspace attach time went 0.8 → 2.7 min on all 40 e2e nodes
### The fix
A member's own lockfile snapshot — keyed by its exact peer variant — declares precisely the sibling `file:` edges of that copy. A manifest-less member now links those. The source order is: slot `package.json` → member snapshot → all members of the root (only when neither exists; the reachability guarantee is unchanged).
`link_root_component_members` takes the lockfile's `snapshots` map (the link phase already holds it), and `Member` carries its snapshot key.
`pn make-release-description` crashed with "Cannot access 'SPONSORS_FRAGMENT'
before initialization", so pnpm 11.24.0's release notes fell back to the
workflow's diagnostic description.
The script's top-level `await writeReleaseText(repoRoot)` sat above the
`SPONSORS_FRAGMENT` constant it ends up reading. Function declarations hoist,
`const` bindings do not: the entry point ran while the constant was still in
its temporal dead zone. The test suite never saw it because tests import the
module, which leaves the direct-invocation branch unevaluated.
Moves the entry point to the bottom of the module, past every module-level
binding, and covers the script path with a test that spawns it the way the
release job does. The entry point takes an optional workspace directory so
that test can point it at a fixture instead of the repository.
The Rust release built the CLI binary and the NAPI addon back to back in
one matrix leg per target. The two are compiled under different Cargo
profiles — `release` for the CLI, `napi-release` for the addon, which
must keep `panic = "unwind"` so a panic reaching the FFI boundary becomes
a JS exception instead of aborting the host node process. Different
profile means a different target directory, so the two builds shared not
one compiled dependency: every leg paid for the whole graph twice, in
series.
Split the addon into its own `build-rust-napi` matrix over the same eight
targets. Measured on the 12.0.0-rc.9 release run, per-leg build time was:
target CLI addon leg
win32-arm64 508s 369s 17m09s
win32-x64 495s 363s 15m06s
darwin-arm64 477s 241s 12m30s
darwin-x64 461s 235s 12m10s
linux-* 322-351s 217-238s ~10m
The stage is bounded by its slowest leg, so dropping the addon out of
win32-arm64 takes the critical path from ~17 to ~11 minutes and the whole
release run from ~25m30s to roughly 19 minutes. The addon legs run
alongside and gate nothing.
Downstream needs no changes: the new job uploads to
`binaries-rust-napi-<code-target>`, which the `binaries-rust-*` download
pattern the verify, publish and draft-release jobs already use picks up
and merges into the same directory. It is also free of the node-gyp
payload dependency, since only the CLI archives ship `dist/`.
The toolchain prelude the two jobs share — installing cross, the
clang-cl/ARM64 Visual Studio components, the rustup target, and the guard
that the committed PNPM_VERSION matches the tag — moves into a
`rust-release-target` composite action rather than being duplicated.
zizmor flags that action's writes to GITHUB_ENV/GITHUB_PATH because a
composite action cannot see who calls it; the values come from the
Visual Studio install and `vcvarsall`, and the only caller is a release
job running on a maintainer-signed tag, so the audit is suppressed on
that step with that justification.
The Rust CLI's `dist/node_modules` is deployed the same way as the TypeScript
CLI's, so the hoisted linker symlinked its bins too and
`dist/node_modules/.bin/get-pnpm` went missing from the `pnpm` tarball:
tar: package/dist/node_modules/.bin/get-pnpm: Not found in archive
Pin the setting off for that deploy as well, and teach `verifyPayload` to reject
a symlink anywhere in `dist/` — packing drops one silently, which leaves exactly
the hole that function exists to catch.
## Summary
The v11 CLI and the Rust engine, matching #14113 (merged) plus its follow-up #14116.
Two places told users to update pnpm with Corepack:
- The **update notification** printed `corepack use pnpm@<version>` when pnpm ran under Corepack. It now prints pnpm's [standalone install script](https://pnpm.io/installation) — `curl -fsSL https://get.pnpm.io/install.sh | sh -`, or the `Invoke-WebRequest` form on Windows.
- **`pnpm self-update`** refused under Corepack with *"You should update pnpm with corepack"*. It now says it cannot update itself under Corepack, and names the standalone install script as the hint.
The notification also stopped naming `pnpm add -g pnpm`, which `add` refuses outright (`ERR_PNPM_GLOBAL_PNPM_INSTALL` in the TypeScript CLI, `GlobalError::GlobalPnpmInstall` in the Rust one). `pnpm self-update` is named only where it can replace the executable in use — an install another package manager owns is resolved from that manager's bin directory, so a self-update would land beside it and report success while the old pnpm keeps announcing the update:
| `PNPM_HOME` set | under Corepack | suggestion |
| --- | --- | --- |
| yes | no | `pnpm self-update` |
| yes | yes | standalone install script |
| no | either | standalone install script |
The command has one definition per stack — `standaloneInstallCommand` in `@pnpm/cli.meta`, `standalone_install_command` in `pnpm-config` (beside `PNPM_VERSION`) — shared by the notification and the self-update refusal so the two cannot drift.
`dist/node_modules` is built by a `pnpm deploy --config.node-linker=hoisted`,
and the hoisted linker turns `preferSymlinkedExecutables` on, so
`dist/node_modules/.bin` became a directory of symlinks. An npm tarball cannot
carry a symlink, so those bins were dropped from the packed `pnpm` and
`@pnpm/exe` tarballs and the release workflow's artifact verification failed:
tar: package/dist/node_modules/.bin/node-gyp: Not found in archive
Force the deploy to write shell shims, which pack fine, restoring the layout
every release before 11.24.0 shipped.
Merging git branch lockfiles unions their keys, so a dependency that the main branch removed after a branch lockfile was written comes back in the merged result. Resolution normally prunes it again, but --frozen-lockfile skips resolution and fails the manifest check with ERR_PNPM_OUTDATED_LOCKFILE.
Once the branch lockfiles are merged, prune each importer back to what its manifest declares. The declared names are derived per dependency group the way satisfiesPackageManifest derives them, so a dependency that moved between groups is reconciled rather than left in both. Only a peer that no other field declares is auto-installed, so a dependency listed in both devDependencies and peerDependencies keeps the field that declares it rather than being dropped from it. Pruning importer edges can strand packages, so the merged lockfile then goes through pruneSharedLockfile.
The Rust CLI merges branch lockfiles the same way and had the same bug. It gets the same reconciliation, hooked into the install pipeline because the lazily loaded wanted lockfile has no access to the project manifests at load time.
Fixespnpm/pnpm#13966.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
A pnpm below 11.20.0 pins `@pnpm/exe` beside `pnpm` for a v12 version, because
that is the set its own major is installed from — pnpm/pnpm#13599 is where pnpm
learned that v12 publishes the executable as the unscoped package alone. A
project pinning pnpm 12 whose lockfile such a pnpm last wrote therefore carries
an entry every newer pnpm rejects, and `--frozen-lockfile` fails on it from
either side of the disagreement while a plain install rewrites the block and
hides it again. The two versions take turns.
That entry pins the wanted version through the same integrity and cannot change
which pnpm runs, so the frozen refusal now accepts a block that records every
package this pnpm installs from, whatever else it records. An entry pinning
another version is a lockfile that disagrees with the manifest and is still
refused. The predicate that drives the write keeps the exact-set rule, so a
writable install still rewrites the block to the packages this pnpm installs
from — the trade is that `--frozen-lockfile` no longer guarantees that block is
byte-stable against a later plain install.
Related to pnpm/pnpm#14124.
Three ways a project's `packageManagerDependencies` entry could be rejected by
`--frozen-lockfile` while agreeing with the manifest, each leaving a plain
install reporting success without repairing anything.
A bootstrap repair — entries whose resolutions the bootstrap refuses to read
are discarded and re-resolved before the pinned pnpm is installed — went
through the same save path as a genuine update. It now resolves in memory
under a frozen lockfile and leaves the lockfile untouched, and re-resolves the
version the lockfile records rather than the range around it, so the command
runs the pnpm the lockfile pins. `resolve_package_manager_integrities` returns
the env lockfile it resolved, which `install_engine_to_store` installs from
instead of re-reading the one it just wrote, and the switch plan carries the
locked version. `switchCliVersion` does the same on the TypeScript side by
dropping `save` and passing the locked `pmVersion`.
`exact_version` kept the `+<algorithm>.<hash>` build corepack appends. A
`devEngines.packageManager` version carrying one was recorded as the version
to look for, but the registry resolves that spec without the build, so the
recorded `version` never equalled it: the entry was rewritten byte-for-byte on
every install and rejected on every frozen one. The build is now dropped where
the pinned version is read, as `parse_package_manager` already does for the
`packageManager` field; the `specifier` still records the pin as the manifest
writes it, which is what the TypeScript CLI writes.
The up-to-date fast path returns before the install pipeline, which is where
the install family records the pin — the pre-command block defers to it. A pin
added to a project whose dependencies are already installed therefore never
reached the lockfile. The fast path now gives way when the pin still has to be
recorded, and stays a fast path once it is.
The TypeScript CLI records the pin from its pre-command block and compares
versions with semver, so the last two divergences are Rust-only.
Related to pnpm/pnpm#14124.
The compatibility database gained ten dependency additions found by
static analysis of published packages (pnpm/pnpm#13970). The analysis
counted type-only imports, which name no runtime dependency: two of the
ten targeted `estree`, a package that does not exist on the registry
(pnpm/pnpm#13981), and the rest name packages the extended package needs
only to type-check, usually through an already-declared `@types/*`
counterpart.
Installing them is at best dead weight - every `@jest/types` install
pulled in yargs and istanbul-reports - and at worst breaks the
dependent, because each target was requested as `*` and resolved to the
newest release. `@typescript-eslint/types` gained a `typescript`
dependency, putting TypeScript 7 under `@typescript-eslint` 6, where
`ts-api-utils` fails with "Cannot read properties of undefined (reading
'Intrinsic')". `@eslint/eslintrc` gained an `eslint` dependency and
`@types/react-dom` a `react` one, each duplicating a runtime whose
consumers assume a single instance.
Restores `pnpm_compat_package_extensions.json` to the three curated
entries, matching the TypeScript CLI's `pnpmCompatPackageExtensions`
again, and keeps the `@yarnpkg/extensions` sync from the same commit -
that half tracks upstream's curated database and matches the
`@yarnpkg/extensions` version the TypeScript CLI installs. A test pins
that no entry may inject `estree` or a single-instance runtime.
The pnpm v11 workspace-setting validator classified confirmModulesPurge as unknown because this install-only option intentionally sits outside Config and WorkspaceManifest. Register it in the explicit untyped workspace-setting list so pnpm v11 no longer warns that the working setting was ignored.
Pacquet does not implement confirmModulesPurge, so it should continue warning that the key is ignored. Record the key in pacquet's cross-version setting table so its diagnostic names pnpm v11 instead of suggesting enableModulesDir.
Closespnpm/pnpm#14125.
Under `nodeLinker: hoisted`, peer-resolution variants of an **injected directory dependency** (a `file:` snapshot) collapsed onto the first-seen variant, like registry peer variants do since #14039 (and the TypeScript CLI's long-standing `depPathByPkgId` mapping). With #14034, edges declared against a losing variant then resolve to the surviving copy.
For registry packages that dedup is intended: every variant unpacks the same tarball, and peers resolve through the ancestry either way. An injected directory dep is different — each `file:` variant is materialized as its **own copy of the local package with its own peer-resolved dependency set**; that is the point of injecting. Collapsing rewires every dependent of the losing variant onto the survivor's children.
### Symptom
teambit/bit's root-components e2e (`nodeLinker: hoisted`): two Bit root components pin `is-odd@1` and `is-odd@2` across injected copies of the same component (`comp2@file:comp2(is-odd@1.0.0)` / `(is-odd@2.0.0)`). The second root's copy materialized as a symlink into the first root's tree and resolved the wrong peer:
```
AssertionError: expected '1.0.0' to match /^2\./
```
### The fix
Exempt `file:` snapshots from the collapse in the shared identity function — `pkg_id` in pacquet, `getHoisterPkgId` in the TypeScript CLI. Every index over the hoist result (`pkg_locations_by_pkg_id`, `package_ids_by_pkg_id`, `pkgLocationsByPkgId`) derives its keys from that function, so the variants stay apart with no further changes.
The registry collapse stays untouched: it is what stopped the per-path walk from exploding on peer-variant-heavy lockfiles (the OOM #14039 fixed), and `file:` snapshots cannot explode that way — there is one per injected workspace package.
Up to v11, `@pnpm/exe` was the build of pnpm that bundled a Node.js
runtime, so it ran where the JavaScript `pnpm` package could not. From
v12 the `pnpm` package is itself the native executable and needs no
Node.js, which left `@pnpm/exe` an equal-content copy of it, published
and dist-tagged for nothing.
generate-packages.mjs no longer emits the `pnpm-exe` wrapper dir, and
release.yml no longer packs or stages it. The per-platform
`@pnpm/exe.<target>` native packages are unchanged: they carry the
binaries the `pnpm` wrapper resolves.
update-latest.yml would have failed on the first v12 promotion, since it
unconditionally installs and dist-tags `@pnpm/exe` at the promoted
version. Both sites now pick the wrapper by major. The upgrade check
gates on the *starting* version so upgrading a v11 `@pnpm/exe` onto a
v12 release is still covered, and it gained `--allow-build=pnpm` because
from v12 the preinstall that swaps the placeholder bin for the native
one lives on the `pnpm` package -- without it that check would have
validated a placeholder.
No runtime code changes: both stacks' package-to-install selection
already converges on `pnpm` for major >= 12, the update notifier already
prints `pnpm`, and `@pnpm/exe` stays in the bin-resolver and global-alias
lists so v11-and-earlier installs keep working. Only the comments that
justified those behaviors with "published under both names" needed
correcting.
* fix: include sponsors in v12 release descriptions
The v11 release description comes from `pn make-release-description`, which
appends the sponsors table. v12 builds its description in the workflow by
dumping the pending changelog, so its release pages never showed sponsors.
Adds .github/release-sponsors.md — a checked-in fragment carrying the same
platinum and gold tables with release_notes attribution — and cats it onto
RELEASE.md in the Rust release job. A missing fragment warns rather than
fails; the sponsors table is not worth losing a release over.
The fragment is regenerated from pnpm.io's sponsors.json alongside the
READMEs and the v11 release text.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor: read the sponsors table from the shared fragment in v11 too
make-release-description inlined its own copy of the sponsors tables — 109
lines of HTML that had to be regenerated in lockstep with the READMEs. Now
that v12 reads a fragment, v11 can read the same one.
getChangelogEntry goes back to returning just the changelog section, and
writeReleaseText appends the fragment. A missing fragment warns and writes
the description without the table, matching the v12 job: the workflow falls
back to a diagnostic description when this script fails, so throwing here
would trade a missing sponsors table for missing release notes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test: assert the sponsors fragment is appended exactly once
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: add Notion as a platinum sponsor
Regenerated from pnpm.io's sponsors.json. Also picks up Latitude, which was
added to the website earlier but never propagated to the sponsor tables here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: put all three platinum sponsors on one README row
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: put all three platinum sponsors on one release notes row
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Rust CLI computed peer-dependency issues during resolution and then
dropped them on the floor: every call site passed `peer_issues_sink:
None`, and the only other consumer was a `tracing::warn!` the reporter
never shows. `strictPeerDependencies` was parsed and read by nothing.
`pnpm dedupe` was the one command that said anything, via a lockfile
walk of its own after the install.
An install that resolves now ends by applying `peerDependencyRules` to
the issues its resolution left behind and either warning once — the
same line the TypeScript CLI's reporter prints — or failing with
`ERR_PNPM_PEER_DEP_ISSUES` under `strictPeerDependencies`, after the
artifacts are written, the way `ERR_PNPM_IGNORED_BUILDS` already does.
Living at the shared install tail means `add`, `remove`, `update`,
`dedupe`, and `--lockfile-only` all get it, matching where pnpm places
the call in `_installInContext`. Dedupe's own walk goes away as
redundant.
An install that skips resolution — frozen, or an up-to-date lockfile —
still reports nothing, in both stacks. That asymmetry with `dedupe` is
the subject of pnpm/pnpm#14098 and is deliberate: peer issues are a
byproduct of resolution, so whatever ran one reports them, and
`pnpm peers check` is what answers for a tree nobody re-resolved.
Recomputing them on the frozen path would put a second, independent
implementation on the hot path of every CI install and trade an
install-vs-dedupe disagreement for an install-vs-install one. A test in
each stack pins the decision.
The verdict is read off the resolved lockfile rather than off the
resolver's own issue map, because pacquet's `ParentChain` is name-only
and would render every "Wanted:" entry with an empty version. That
makes the lockfile walk shared code, so it moves out of the `peers`
command into a new `pnpm-deps-inspection-peers` crate alongside the
rules filter and the issue renderer — pnpm's `peers-checker` and
`peers-issues-renderer` packages.
Closespnpm/pnpm#14098.
An explicit `<name>@<tag>` update selector was saved into package.json
verbatim, so a manifest could declare `latest` while the lockfile held the
version behind it. Resolving through the `--latest` rewrite instead fixed
that only for `latest`: that path merges the declared range with the
`latest` tag, so `update <name>@canary` would have recorded whatever
`latest` names.
The selector's tag is now resolved on its own, and the version behind it
is what package.json records, keeping the range operator the entry already
declares. That version is also seeded as a resolution preference, so the
lockfile locks the tag's version rather than whatever else the recorded
range admits.
Only a declaration a version can round-trip is rewritten. An entry that
already tracks a dist tag keeps tracking one, and a `catalog:` reference,
a `workspace:` or `npm:` alias, and path or git specifiers stand — the
selector reaches those installs as a preference only.
Closespnpm/pnpm#14092.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
get_version_selector_type normalized an exact version selector with
node_semver::Version::to_string(), whose Display impl writes the build
identifiers back out. pick_package_from_meta uses that normalized string
as a literal key into the packument's versions map, and npm strips build
metadata off a version when it publishes it, so a selector like
1.0.0+build1 could never match and the resolver reported
ERR_PNPM_NO_MATCHING_VERSION.
The build identifiers are now cleared before the version is formatted,
matching semver.valid() / SemVer.version in the version-selector-type
package the TypeScript CLI uses, which excludes build metadata from its
normalized form. Range and dist-tag selectors take other branches and are
untouched.
Closespnpm/pnpm#14096
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
pacquet's dedupe never hard-failed on peer dependency issues: Install.peer_issues_sink
in dedupe.rs was hardcoded to None, and the only post-install handling was a Warn log.
config.strict_peer_dependencies was never read, so the command could not fail regardless
of the setting, unlike the TypeScript CLI's dedupe, which throws ERR_PNPM_PEER_DEP_ISSUES
via reportPeerDependencyIssues.
dedupe.rs now checks strict_peer_dependencies against the same filtered peer issue set
pnpm peers check computes, and fails with ERR_PNPM_PEER_DEP_ISSUES instead of only
warning, reusing the existing --check/DedupeError pattern in the same file.
Fixespnpm/pnpm#14099.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
The workspace-manifest writer's splices are line-oriented, so a block a user
wrote inline — `catalog: { foo: ^1.0.0 }`, `overrides: { foo: 1.0.0 }`,
`minimumReleaseAgeExclude: [foo@1.0.0]` — had no entry lines to splice.
`overrides:` and `auditConfig:` refused the write; every other block spliced a
block-style line in after the inline value and left an unparsable document
behind.
The TypeScript writer round-trips the document through the yaml library and
edits such a block in place, keeping its flow style, its other entries and
their quoting, and any trailing comment. pacquet now does the same: a new
`flow` module parses a single-line flow collection into entry spans and
rebuilds it with one entry upserted, replaced, or dropped, and the entry
splices, the removal splices, the catalog/allowBuilds/patchedDependencies
writers, and the cleanup passes all route through it. The rendering matches
what the yaml library emits (`{ a: 1, b: 2 }`, `[ a, b ]`), so both stacks
produce the same bytes for the same edit — pinned by the new `flow_style`
suite and its mirror in
pnpm11/workspace/workspace-manifest-writer/test/flowStyle.test.ts.
A flow collection spanning several lines can carry comments between its
entries that a one-line rebuild would drop, so those — along with aliases and
scalars standing where a collection belongs — are still refused with
ERR_PNPM_WORKSPACE_MANIFEST_WRITER_UNSUPPORTED_INLINE_BLOCK, now on every
writer rather than only two.
Closespnpm/pnpm#14108
Section 4 of the parity sweep in pnpm/pnpm#14101: settings present in
pacquet's `types` table — so `pnpm config` handled them — that no code
read.
- `updateNotifier`: pacquet had no update notifier at all. `install` and
`add` now ask the project's registry for pnpm's `latest` tag once a day,
emit `pnpm:update-check`, and the default reporter prints the notice when
that version is newer. The cadence lives in `<stateDir>/pnpm-state.json`
under `lastUpdateCheck` — the same file, key, and `toUTCString` format
pnpm writes, so the two CLIs share one throttle. The check is
best-effort: an unreachable registry or an unwritable state dir cannot
fail the install. Two deliberate gaps: an `install` finishing through
the repeat-install fast path returns before the async runtime exists and
is not covered, and the "to update, run" line never names `@pnpm/exe` —
the native binary is published under both names with no marker telling
them apart, and a global install of either replaces the other.
- `legacyDirFiltering`: `use_glob_dir_filtering` was hardcoded on.
- `initAuthorName` / `initAuthorEmail` / `initAuthorUrl` / `initLicense` /
`initVersion`: `pnpm init` now writes them over the scaffold's
placeholders, in place, so the key order is unchanged. The TypeScript
reader dropped `init-version` from the `PNPM_CONFIG_*` env schema to
keep the two `init` implementations aligned; that exclusion goes away
with this commit.
- `maxsockets` was inert in *both* stacks, not just in pacquet: the
reader folded npm's lowercase spelling into `maxSockets` before any
config file or env var had been applied, and `.npmrc` stopped carrying
the key when the non-auth settings moved to yaml. The fold moves after
the env loop, and pacquet gains the alias in both the settings file and
the environment. The default stays `None` in pacquet (uncapped per
origin, bounded by `networkConcurrency`) rather than npm's 50, which
would cap it below the network concurrency it already resolves.
`npmPath` is the fifth item on that list and needs no port: nothing reads
it in the TypeScript CLI either, since publishing stopped shelling out to
the npm CLI. Its one remaining mention — a `Pick` in `recursivePublish`
that no code consumes — is dropped, so the deadness is visible.
The pacquet command harness turns the notifier off for every suite: no
test may reach the registry for pnpm's own `latest` tag or record the
check in the developer's state directory, which the suites leave
un-isolated.
Related to pnpm/pnpm#14101.
Implement the five parity gaps tracked in section 3 of pnpm/pnpm#14101.
Recursive batch publish now packs all eligible projects before sending
anything, groups the standard npm publish documents by registry, and uses
pnpr's atomic batch endpoint once per group. It reuses the existing publish
document, authentication, OTP, lifecycle, and summary paths so single and
batch publishing cannot drift. Staging and provenance remain incompatible
with a multi-package request and fail with explicit diagnostics.
Global outdated accepts the recursive spelling already handled by the
TypeScript CLI. Global approve-builds aggregates pending packages across all
install groups, writes one policy under the global packages directory, clears
the corresponding group manifests, and rebuilds each affected group. Since
the TypeScript implementation rejected the same command, this behavior lands
in both stacks; canonical path checks prevent a tampered group symlink from
redirecting approval or rebuild work outside the global package root.
Recursive run and exec share a bounded worker pool per topological chunk,
preserve result ordering, and stop scheduling later batches after a failure
when bail is enabled. Parallel exec removes the cap, matching pnpm's
--parallel shorthand.
SBOM generation with dedicated project lockfiles constructs a synthetic
in-memory workspace lockfile for the selected projects. Importer paths are
validated and rebased to workspace-relative IDs, package and snapshot graphs
are merged, and metadata lookup searches the selected projects' virtual
stores. This supports both filtered output and split output without changing
the on-disk lockfiles.
Related to pnpm/pnpm#14101.
`pnpm dedupe` declared only `--check` and `--ignore-pnpmfile`, so clap
rejected `pnpm dedupe --lockfile-only` with "unexpected argument". pnpm's
`dedupe` takes `pnpm install`'s rc options, and Renovate now passes
`--lockfile-only` on its dependency-update runs, which fails the whole
update.
Declare the options pnpm documents for `dedupe` beyond `--check`:
`--lockfile-only`, `--ignore-scripts`, `--offline`, and
`--prefer-offline` (each with the generated `--no-` negation).
The install `dedupe` builds hardcoded `lockfileOnly: true`, so it never
materialized `node_modules` — pnpm's `dedupe` is a full install and does.
The flag now drives that field, with `--check` still forcing the
lockfile-only path so a check leaves the working tree untouched.
Closespnpm/pnpm#14107
A pnpmfile that defines `hooks.importPackage` now gets a warning saying the
hook is deprecated and will be removed in the next major version.
The hook lets a pnpmfile take over writing a package into node_modules. It is
a poor fit for where both stacks are heading:
- Defining it drops the installation off the worker-thread importer entirely
(`store/controller/src/storeController/index.ts`), and
`store/create-cafs-store/src/index.ts` shows it also bypasses
`pkgImportMethod` -- including the `willBeBuilt -> clone-or-copy` rule, so a
hook that hardlinks corrupts the store once the package is built.
- Porting it to the Rust CLI would put a JS callback on the hottest phase of
the install, behind the single sequential Node worker that serves pnpmfile
hooks, carrying the whole filesMap per package. It would also hand user JS
the invariants `import_indexed_dir` owns: exclusive-mkdir ownership claiming
for shared slots, marker-based repair, and the macOS quarantine sweep.
- A GitHub-wide code search finds no pnpmfile using it outside copies of the
documentation page, except one dev-server tool that only needs to know where
a package landed and to force an import method.
The warning points at pnpm/pnpm#14101 so anyone who does depend on the hook
can say so before it is removed.
Related to pnpm/pnpm#14101.
The TypeScript CLI accepts `--stream`, `--aggregate-output`,
`--use-stderr`, `--reporter-hide-prefix`, `--ignore-workspace`, and
`--workspace-packages`; pacquet rejected all six. Section 2 of
pnpm/pnpm#14101.
`--stream` is the substantial one. A recursive `run` inherited the
terminal for every project, so pacquet had no way to attribute a line to
the project that wrote it, and `--parallel` — which expands to
`--stream` in pnpm's `run` shorthand table — produced unreadable
interleaved output. `RunScript` grows a `ScriptOutput`: `Inherit` keeps
the old path, `Streamed` pipes the child and republishes each line as a
`pnpm:lifecycle` event through the new `StreamedScript`, which also
absorbs the line pumps `run_lifecycle_hook` already had. The reporter
gains `streamLifecycleOutput`, so the lifecycle stream renders
append-only while the rest of the frame still redraws in place — the
same split pnpm's reporter makes.
`--aggregate-output` buffers a script's events until it exits and then
renders the run as one block, formatting at flush time so the prefix
color wheel advances in print order. `--reporter-hide-prefix` drops the
prefix from the script's own output lines only, leaving the `$ <script>`
echo and the `Done` / `Failed` line labelled. It is a `run` / `exec`
option, so it is scope-validated and hidden like the other
command-scoped globals; a recursive `exec` reads its explicit `false` as
the signal to start prefixing, matching pnpm's
`reporterHidePrefix === false` gate.
`--ignore-workspace` stops the workspace search in `Config::current`, so
`pnpm-workspace.yaml` contributes neither settings nor sibling projects
and a blocked dependency build is not scaffolded into its `allowBuilds`.
`--workspace-packages` is resolved into
`Config::workspace_package_patterns` alongside the manifest's own
`packages`, which `discover_workspace_projects` now takes from the
config rather than re-reading the manifest at every call site.
The five boolean settings also become readable from
`pnpm-workspace.yaml`, the global `config.yaml`, and `PNPM_CONFIG_*`.
This is pacquet-only: the TypeScript CLI already has all six.
After a successful project-local runtime pin, suggest the explicit pnpm shim add command when the trusted global-shim policy enables that runtime and no project-aware shim is installed. Include pnpm setup in the guidance when the global bin directory is not configured.
Persist explicit project-aware shim intent and expected bin names independently of the active shim files. This lets a matching global package occupy the public command while installed without losing the user's opt-in. Reject unrelated global packages that export a reserved bin, and restore recorded shims when the matching package is removed or replaced by a version that drops a bin. Removing a shim explicitly clears both its active files and restoration intent.
Apply global package activation and removal as recoverable mutations across affected command slots and package hash links. Serialize pnpm writers of the global bin directory so ownership checks, publication, rollback, and cleanup cannot race another pnpm transaction. Recognize Windows junction-backed global links during discovery and orphan cleanup.
Bound shim metadata reads and validate persisted package owners and bin names before they can select filesystem paths.
Closespnpm/pnpm#14058.
Ports the four command groups the Rust CLI had no counterpart for,
tracked as section 1 of pnpm/pnpm#14101.
`pnpm get <key>` / `pnpm set <key> <value>` are the top-level spellings
of `pnpm config get` / `pnpm config set`. They carry the same argument
structs and run the same code, so the two spellings cannot drift, and
they join `config` in the set of commands whose reporter writes to
stderr — `pnpm get <key>` prints one value for a script to capture.
`pnpm store status` re-hashes every package the lockfile records against
the store row it was expanded from and fails with
`ERR_PNPM_MODIFIED_DEPENDENCY` listing what no longer matches; `pnpm
store add <pkg>...` resolves and fetches packages into the store without
writing a manifest, a lockfile, or `node_modules`. Both previously
panicked with "Not implemented", and the status variant was reachable
only under the wrong name (`pnpm store store`).
`pnpm env use --global <version>` and `pnpm env list [<selector>]` are
the deprecated Node.js-only front end to `pnpm runtime`: `use` warns and
then takes the same global-install path `runtime set node <version> -g`
does, and `list` enumerates a selector against the configured mirror. An
absent selector reads as the empty one, which the mirror resolver treats
as `latest`, so a bare `pnpm env list` prints the newest version alone —
matching the TypeScript CLI.
`pnpm edit`, `profile`, `token`, and `xmas` are registered so they fail
with `ERR_PNPM_NOT_IMPLEMENTED` naming the npm CLI, instead of reaching
the external-subcommand fallback and failing as a missing script.
Two pieces of supporting reuse rather than a third copy each:
`store add` needs a resolver chain outside an install, which the NAPI
`resolveDependency` path had already built inline, so that chain moves
to `pnpm_resolving_default_resolver::standalone` and both callers share
it; and `store status` needs "does this directory still match the store
row", which is pnpm's `dint.check`, so it joins the store's own
integrity checking as `package_dir_matches_index`.
The TypeScript CLI has all four command groups already, so this is a
pacquet-side catch-up with no counterpart change.
Related to pnpm/pnpm#14101.
Record `lastValidatedTimestamp` as the greater of the validated files'
mtimes and the filesystem clock's current time, read from an unnamed
temporary file before the content check runs.
The mtime baseline alone cannot converge: it is a file mtime truncated
to milliseconds, and `modified_at_or_after` reads a file whose mtime
lands in that same millisecond -- or, on a whole-second filesystem, that
same second -- as possibly-modified, so the file that forced the content
check keeps forcing one on every later run. Post-dating the baseline by
a fixed delta converges but hides an edit made in the interval it skips
over. Raising it to the filesystem's now converges without ever moving
the baseline ahead of the present, which is what pnpm's
`checkDepsStatus` does with `Date.now()`; reading the same now off the
filesystem keeps the wall clock, which can run ahead of the mtime clock,
out of the comparison.
Closespnpm/pnpm#13907
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
The Rust CLI generated a minimal .pnp.cjs loader, but it did not preload that loader for lifecycle scripts or explicit run and exec commands. Ecosystem tools could also observe only a small subset of the PnP API.
Append the workspace loader to NODE_OPTIONS across dependency builds, project lifecycle scripts, run, recursive run, exec, and recursive exec. Preserve existing Node options and use the same workspace-first loader lookup as the TypeScript CLI.
Expand the generated loader with locator, package-information, version, and resolver APIs, plus pnpapi and Module.findPnpApi support. Keep generated package data separate from template markers so lockfile-controlled values cannot be interpreted as template substitutions.
`pnpm update <name>@<version>` treated the requested version three different
ways. A direct dependency records it (in the manifest, or against the range
`--no-save` keeps). A transitive-only target in a single project ignored it
and warned. But the workspace-recursive path seeded preferred versions from
every pinned selector, so the same command silently honored the version in a
workspace and ignored it outside one, while the warning said the opposite.
Drop `createPreferredVersionsFromPinnedUpdateSpecs`. It only ever reached
transitive targets: `parseWantedDependencies` already applies (or explicitly
supersedes) a direct dependency's requested version through the kept-range
path, so nothing else depended on it.
Warning and carrying on was itself the wrong answer. No other package manager
takes that position: npm refuses any version on `update` outright
(`EUPDATEARGS`), and Yarn Berry refuses too; Yarn Classic and Bun accept it
and make it durable by promoting the package to a direct dependency. pnpm was
alone in accepting the argument, doing something else, and exiting 0.
Fail with ERR_PNPM_UPDATE_VERSION_ON_INDIRECT_DEP instead, naming the
selectors and showing the override that does pin a transitive dependency.
Scoped to selectors that name an exact version and that no selected project
declares directly: a range or a tag names no single version to record, so
updating within the dependents' ranges is a reasonable reading of it and those
keep their warning. `--depth 0` still reports NO_PACKAGE_IN_DEPENDENCIES, and
`--latest` still rejects versioned selectors on its own.
pacquet's copy of the warning went through `tracing::warn!`, which is dropped
unless `TRACE` is set, so its users never saw it; what survives emits through
the reporter now.
Version-line scoping of update targets (pnpm/pnpm#14053) is unchanged.
Wires up the pnpm settings the Rust CLI had no `Config` field for — bail,
sort, reverse, recursiveInstall, optional, pending,
ignoreWorkspaceRootCheck, color, packageLock, embedReadme,
skipManifestObfuscation, useBetaCli — from the workspace manifest, the env
overlay and the CLI flags through to the commands that consume them. The
default-parity table's NOT_PORTED list is now empty.
It then implements the behavior behind `shellEmulator`, which was accepted
but ignored. Scripts now run in `deno_task_shell`, an MIT-licensed
cross-platform POSIX shell, instead of `sh -c` / `cmd /d /s /c`, so a
script written for `sh` behaves the same on Windows. The setting reaches
the call sites pnpm threads it through: the `run` command, project
lifecycle scripts and `pnpm:devPreinstall`, dependency build scripts, and
the `version` command's hooks. Publishing, packing, patching, and git
package preparation pass `false`, matching the TypeScript CLI.
`scriptShell` is still validated while the emulator is on — pnpm rejects a
.bat / .cmd shell regardless of the setting — but is not used. An emulated
script runs in pacquet's own process and so has no `ExitStatus`; both
execution paths now return a `ScriptExit`, which answers the two questions
every caller asks: did it succeed, and with which code.
Making install/list/why recursive by default inside a workspace, as pnpm
has them, exposed three gates that used "has a selection" or "is
recursive" to mean "is narrowed": stale-importer pruning, the pnpr skip
for a lockfile that already satisfies the manifest, and the repeat-install
fast path. All three now test for a partial selection, which is pnpm's own
condition.
The TypeScript CLI has implemented all of these settings since before the
port, so this is a pacquet-side catch-up with no counterpart change.
Finishing the test porting tracked in pnpm/pnpm#12101 turned up two places
where pacquet's `update` still differed from pnpm's.
`updateConfig.ignoreDependencies` reached the update itself but neither of
the two places that report what could be updated: `pnpm outdated` listed an
ignored dependency, and `pnpm update --interactive` offered it for
selection. Both now drop the names the setting matches, the way pnpm hands
`ignoreDependencies` to `outdatedDepsOfProjects` from its `outdated` and
interactive-update paths alike.
A recursive `pnpm update --latest --depth 0 <selector>` that matched no
project's dependencies exited 0 having done nothing. pnpm lets `--latest`
return quietly only on the single-project path; the recursive one throws
NO_PACKAGE_IN_DEPENDENCIES once no project is left to mutate, whatever
`--latest` says, and so does pacquet now.
The harness gained the two seams the remaining upstream tests needed:
- `pnpr_fixtures::set_dist_tag` moves a dist tag in a built storage tree,
and `CommandTempCwd::add_mocked_registry_with_own_storage` gives a test a
registry of its own to move it in. The fixture registry otherwise serves
`latest` as the highest published version, which cannot express "the
newer version was published after the install" — the setup behind every
upstream `--latest` case. The packument the last command cached is
dropped with the tag, so the next one resolves against the move.
- `UpdatePrompt` decides how `--interactive` asks the user. The tests
answer it from a script instead of a terminal, the way the upstream suite
mocks `@inquirer/prompts`; `dialoguer` wants a TTY and offered no seam.
Ported with them: the five remaining `recursive.ts` cases, five `update.ts`
cases, three from `interactive.ts`, the `issue-7415.ts` regression at the
level the seam reaches (the outdated collector walking past a resolution
that names no version), and the `outdated` ignore-dependencies case.
`@pnpm.e2e/multi-version-b` stands in for `@zkochan/async-regex-replace`,
which the fixture registry does not carry.
Closespnpm/pnpm#12101
`--ignore-pnpmfile` was the only way to turn pnpmfile hooks off. pnpm
lists `ignore-pnpmfile` among the keys a config file may carry and
exposes `PNPM_CONFIG_IGNORE_PNPMFILE` like every key in its schema; both
now work here, with the flag still ORing on top.
The doc comment claimed the opposite — that pnpm excludes the key from
its config-file keys and only the CLI layer sets it. `pnpmConfigFileKeys`
carries it, so the comment described a limitation of this implementation
as though it were the contract.
Two review notes from pnpm/pnpm#14085 ride along: each checksum
measurement now resolves from scratch, since reading back a lockfile an
up-to-date check declined to rewrite would compare a value to itself, and
a comment saying the order pnpm "fixes" now says "pins".
Part of pnpm/pnpm#12042.
Pacquet's outdated collector bypassed the version-pick policy and read the registry's raw latest version. With the v12 default minimumReleaseAge, interactive update could therefore offer same-day releases that the update resolver would reject, leaving the manifest and lockfile unchanged.
Reuse the configured npm resolver for outdated and interactive-update lookups, and share its construction with update's latest-version path so registry and metadata policy settings cannot drift.
Fixespnpm/pnpm#14004.
Keep settings-only workspace manifests scoped to the root package when checking dependency status, filtering projects, and completing --filter values.
These call sites previously passed an undefined package pattern to the projects reader, activating its recursive default even though config had already defined settings-only workspaces as root-only. Preserve the projects reader default for other callers and apply the root-only fallback only at the three manifest-aware boundaries.
Fixespnpm/pnpm#14047.
The hoister collapses every peer variant of one package version onto a
single node, keyed by the first snapshot key it sees for that version
(pnpm/pnpm#14039 on the Rust side, `toTree`'s `depPathByPkgId` on the
TypeScript side). Only that one key reaches the dep-graph walk, but the
walk kept indexing locations by it and looking them up by the exact key
each edge declares. Every edge on another variant missed.
A workspace project that declared the losing variant lost its entry in
`direct_dependencies_by_importer_id` and in `.package-map.json`, so
under `nodePackageMapType: standard` it could not require a dependency
it declares. A package that depended on one lost the child edge, and
with it the `node_modules/.bin` link the child provides.
Key the indexes — and the lookups — by the peer-suffix-free package id,
which is the identity the hoister collapsed on: `pkg_locations_by_pkg_id`
and `package_ids_by_pkg_id` in pacquet, `pkgLocationsByPkgId` in the
TypeScript CLI. The two stacks derive the id the way their own hoister
does: `PkgNameVerPeer::without_peer` there, `nameVerFromPkgSnapshot` here.
The hoisted-linker regression test the original version of this PR added
is kept, extended to assert the package map.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Prevent pinned recursive updates from leaking across major version lines.
Recursive update target matching treated a pinned selector like
`js-yaml@3.15.1` as targeting the package by name alone, so unrelated
`js-yaml@4.x` entries were re-resolved along with the requested line in
lockfile-only/no-save runs.
Update target matching is version-aware for pinned selectors now, in both
the TypeScript CLI and pacquet: a selector naming an exact version targets
only the copies on that version's major line, or its minor line for a `0.x`
request. Alias selector expansion carries the aliased spec's own version, so
`alias@npm:pkg@1.2.3` scopes `pkg` the same way, and exact pinned selectors
seed preferred versions so the resolver settles on the requested line.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Expose virtualStoreOnly and enableModulesDir through pacquet's PNPM_CONFIG environment overlay so CI and other callers can override committed workspace settings.
Keep npmrcAuthFile on its existing early bootstrap path, which already supports PNPM_CONFIG_NPMRC_AUTH_FILE and must select the auth file before the rest of configuration is loaded.
Related to pnpm/pnpm#12042.