pnpm v12 resolved `pnpm add <pkg>` without a version, and a version-less
`npm:` or `jsr:` dependency, to the `latest` dist-tag even when the `tag`
setting named another one. pnpm v11 reads `tag` from pnpm-workspace.yaml,
the global config, `PNPM_CONFIG_TAG`, and `--config.tag`, and threads it
as `defaultTag` into `parseWantedDependencies` (installSome.ts) and the
resolution context (createResolutionContext.ts).
`Config` now carries `tag`, read from the same layers, with
`Config::default_tag` falling back to `latest`. A brand-new `add` with no
version resolves a non-default tag the way an explicit `add <pkg>@<tag>`
does, so the picked version is also locked. A `jsr:` add and the install
resolver take the same default. `dlx`, `add --config`, `update --latest`,
and `outdated` keep `latest`, as they do in v11.
Tested with e2e tests for the yaml setting, `--config.tag`, and a
version-less `npm:` alias, each seen to fail with `default_tag` forced to
`latest`, plus config unit tests for apply, override, reset, and the
resolved projection.
Part of pnpm/tasks#191
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pnpm v12 read no `requiredScripts` setting, so `pnpm -r run <script>`
ran normally when a selected project lacked a script the workspace
declares required. pnpm v11 fails such a run before dispatching anything
(`throwIfRequiredScriptIsMissing` in
pnpm11/exec/commands/src/runRecursive.ts), with
ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT and the message
`Missing script "<script>" in packages: <names>`.
The config crate now reads `requiredScripts` from pnpm-workspace.yaml
into `Config::required_scripts`. The global config.yaml drops it, as v11
does, since it is not a config-file key there; v11 reads no environment
variable for it either. The recursive run checks it where v11 does,
after `--resume-from` and the dry-run exit, before the hidden-script
filter and the no-script check, and after the `pnpm <script>` shorthand
decides whether to fall back to `exec`. Projects are named by their
manifest name, falling back to their directory.
Tested by a run_recursive e2e module that fails without the check,
including a nameless project listed by its directory and a --filter run
that checks only the selected projects, and a config test for the
workspace and global sources. The requiredScripts docs of both versions
now say the check covers the projects selected for the run.
Part of pnpm/tasks#191.
Correct the v12 guides from agent-skills.md through lockfile.md against
pnpm/crates, and fix the same wrong text on the v11 twins where pnpm11
confirms it. Part of pnpm/tasks#191.
Wrong claims (both versions):
- configuring: the global auth file is auth.ini, not rc; list the
npm_config_* exceptions (workspace dir, userconfig, URL-scoped auth).
- config-dependencies: the hook import used an invalid ESM specifier.
- docker: Example 3 ran fetch --prod and built without an offline install
or pnpm-workspace.yaml.
- errors: drop MISMATCHED_RELEASE_CHANNEL, describe INVALID_NODE_VERSION
through nodeVersion, PEER_DEP_ISSUES needs strictPeerDependencies.
- faq: no lockfile deletion after a readPackage hook, default store order,
junctions depend on symlink privilege.
- filtering: `!./lib` matches one project; use `!./lib/**`.
- git branch lockfiles: every non [A-Za-z0-9_.-] character becomes `!`,
the name is lowercased, merging deletes branch lockfiles, "or" patterns.
- git-worktrees: remove the nonexistent worktree:new and shell/wt.sh.
- global-packages: orphaned dirs are cleaned by add -g (and update -g in
v12), not store prune; document that add -g replaces every group that
contains the package.
- how-peers-are-resolved: current peer directory names.
- completion: built-in completion already covers --filter and scripts.
- feature-comparison: broken JSR anchor.
Wrong claims (v12 only):
- cargo: cargo.indexUrl fails the parse, overrides apply to adding only,
merge the duplicated Cargo index section into Registries.
- global-virtual-store: pnx keeps its own virtual store (dlx part only;
the global-install claims are left for a separate decision).
- installation: the npm package installs on Node.js 18, only get-pnpm
needs 22.13; trim unverified compatibility rows.
Missing facts (v12): agent-skills group filtering and naming, catalogs
(catalogs.default, pnx, --save-catalog, error codes), cargo recursive
add, build-std, resolution keys, completion zsh/pwsh setup, config
dependency location and error codes, include error codes, CI opt-outs and
pnpm ci, finders on list, filtering settings and --no-fail-if-no-match,
global install refusals, trust for npm-registry package managers, Rust
shims, overrides link from aliases.
Stale history (v12): drop v11/v10 version markers and "since v11"
narration in continuous-integration, filtering, finders, git-worktrees,
global-packages, global-virtual-store, how-peers-are-resolved, lockfile,
completion, configuring.
Review follow-up: the Node.js compatibility table keeps the pnpm 10
column; Docker Example 3 builds with all dependencies in a build stage
and ships only production dependencies, with the pnpm-workspace.yaml
caveat (both versions); catalogs keep the bare-path form; v11 global
packages name `pnpm update -g` as cleaning orphans too; the cargo
build-std note says when rust-src must be listed.
Second review: build-std needs a non-empty list; the Docker fetch layer
also depends on the project settings; branch lockfile names keep only
ASCII characters; the conflict merge needs both sides to parse; Rust
shims and package managers are default globalShims participants;
pnpm self-update also cleans orphaned global groups; v12 pnpm login
writes tokens to config.yaml; legacyDirFiltering keeps the subtree
wording (the v12 mismatch is pnpm/tasks#196); the v12 Docker
example copies the files that `include` names before pnpm fetch.
Wrong claims (v12 and the v11 twin where the v11 source agrees):
- proxy precedence: a configured httpsProxy/httpProxy/noProxy wins and the
environment is only a fallback; HTTPS never falls back to HTTP_PROXY
- proxy opt-out: false or null in httpsProxy, httpProxy, or noProxy reads
as unset; only proxy: false turns proxying off
- networkConcurrency auto value is clamped to 64-96, not 16-64
- maxsockets default: unset in v12, 50 in v11
- stateDir on macOS is ~/.local/state/pnpm
- unsafePerm only moves TMPDIR to node_modules/.tmp
- ignoreWorkspaceRootCheck affects pnpm add only; the duplicate CLI
section is removed in favor of the workspaces page
- useRunningStoreServer has no effect and is removed
- .npmrc also supplies TLS, proxy, and local-address settings
- the list of keys a project pnpm-workspace.yaml cannot set now names
scope (both) and macosBackup, ci, tools, globalShims (v12)
- v12: the GVS CI auto-disable sentence, the frozenStore Node.js
requirement, the JavaScript shell emulator, and the dlx global virtual
store claim are corrected
Missing settings (v12): saveExact, embedReadme, scriptsPrependNodePath,
userAgent, lockfileDir, excludeLinksFromLockfile, globalVirtualStoreDir,
externalDependencies, autoInstallPeersFromHighestMatch; loglevel silent;
PNPM_HOME defaults for globalDir/globalBinDir; childConcurrency
negative values; what ci changes and where updateNotifier is skipped.
Index (v12): lists every section on the group pages, including
progress, saveTypes, autoDedupe, maxTarballUnpackedSize, and macosBackup.
Stale history (v12): removed pre-v12 "Added in" markers and rewrote
"Since v10/v11" and "Before v11" sentences as current behavior.
Part of pnpm/tasks#191.
Part of pnpm/tasks#191.
Wrong claims, fixed on the v12 page and on the v11 twin where it had
the same text:
- audit: `--interactive` implies `--fix=override`; `info` is a valid
audit level.
- change check validates the pending intents.
- deploy copies the publish packlist unless `deployAllFiles` is set.
- deprecate: an empty message fails, use `pnpm undeprecate`.
- deprecate, dist-tag: only `--otp` supplies the one-time password.
- dist-tag add defaults to `latest`, not the `tag` setting.
- docs takes exactly one package.
- env: there is no `add` subcommand, and `list` queries the Node.js
mirror only.
- v12 only: clean no longer says it uses Node.js, add no longer
presents `-w` as an alias, the deploy package-manager-pin history
is no longer contradictory, doctor sample shows store v11.
Missing facts:
- add: `--save-prefix`, `--save-build`, `--no-save-peer`,
`--no-save-types`, `--lockfile-only`, `--force`; where Yarn vs npm
and Bun pins go; catalog and configDependencies links.
- access status shorthands; approve-builds error codes, the
placeholder value and the immediate rebuild; audit exit code,
`--optional`, `--production`, and how to make `--ignore` write
`audit.ignore`; `--ignore` exits with code 0 and
`--ignore-unfixable` fails on an advisory without a GHSA ID.
- bugs and docs `--registry`; ci accepts install options;
clean script override and modulesDir; config `--location` type and
`get` output; create name mapping, `--shell-mode`, `--cpu`, `--os`,
`--libc`; dedupe flags; deploy error codes and Dockerfile WORKDIR;
dist-tag aliases and web OTP; env `--global` requirement; exec
recursive options and PNPM_PACKAGE_NAME; fetch `--ignore-pnpmfile`,
lockfile requirement, optional pnpmfile in the Dockerfiles.
- usage, examples, and output for bin, cache, cache list, cache
delete (with the no-argument warning), cache view, cat-file,
cat-index, find-hash, ignored-builds, import, and init
(`initPackageManager`, `initType`, ERR_PNPM_PACKAGE_JSON_EXISTS).
Stale history on v12 pages: removed "Added in" markers for v10 and
v11 and rewrote "Since v11..." passages as current behavior in
access, add, approve-builds, audit, bugs, cache-list-registries,
cache-path, cache-prune, change, ci, config, create, deploy,
deprecate, dist-tag, docs, doctor, help, ignored-builds, and init.
Review follow-up: cat-file now says it rejects the hex digest that
cat-index prints; the settings index links initPackageManager and
initType; `change check` says it plans the release without reporting
it (both versions); `pnpm bin` follows a custom modulesDir.
Part of pnpm/tasks#191.
Wrong claims, fixed in the v12 docs and, where the same text is wrong
for v11, in the v11 docs:
- logout: drop the nonexistent --scope option.
- patch: the edit dir is node_modules/.pnpm_patches/<name>@<version>;
drop the "*" special case.
- patch-commit: fix the ---patches-dir heading.
- prefix: describe the real marker walk instead of "closest package.json".
- setup: setup installs pnpm as a global package and writes pn/pnpx/pnx.
- start, test: "run start" and "run test" are not aliases.
- unlink: drop the system-wide and link --global wording.
- unpublish: a package name is required; --otp has no env var; --registry
wording.
- update: --depth is dependency depth, not directory depth.
- version: --tag-version-prefix defaults to the tagVersionPrefix setting.
- view: the first argument is always the package.
- why: drop --global and --only-projects; --exclude-peers has no effect;
--prod includes optionalDependencies.
- install: recursiveInstall spelling; replace the stale ci-info snippet.
- v12 only: pack-app runtime default and builder download, pnx
provisioning list and cmd.exe typo, tasks sample priority.
Missing flags, settings, and error codes on the v12 pages: install (about
twenty flags), lane, licenses, link, list, outdated, owner, pack-app path
rules, pack, patch, patch-remove, peers, permissions, ping, pipeline, pm,
pnx, prune, publish, rebuild, recursive, remove, repo, root, run,
runtime, sbom, search, self-update, set-script, setup, shim, stage, star,
start, store, tasks, team, test, token, unlink, unpublish, update,
version, view, why, with.
Stale history on the v12 pages: removed "Added in" markers older than
v12 and rewrote "Since v11.x" and "In v11" sentences as current behavior
(login, logout, licenses, link, outdated, patch, pnx, publish, runtime,
sbom, self-update, setup, update, and others).
Review follow-up: patch's default edit directory is named after the
argument as typed (both versions); self-update installs the unscoped
package only for pnpm 12 and newer; only --no-bail is restricted;
npm_package_* covers name, version, config, engines and bin; prefix -g
prints the parent of root -g; --optional no longer claims to override
--no-optional.
* docs: lint prose with Vale
Add Vale with an in-repo style, .vale/styles/pnpm, that turns the
writing rules in AGENTS.md and the Google developer documentation style
guide into lint rules. The stock Vale packages (Google, Microsoft,
RedHat, write-good) were too noisy to block on, for example 1,300
passive-voice hits in the v12 docs alone, so the rules are written for
this repo, following the GitLab model. Each one links the guideline it
enforces.
Levels:
- error, fails CI: dashes, plain-word substitutions, condescending
words, "please", "note that", hedges, puffery, optional plurals,
exclamation marks, ellipses, curly quotes, generic link text, product
spellings, sentence-case headings, repeated words, double spaces,
implementation history in comments; in changesets, bold or italic
emphasis, internal names and release-line names.
- warning: future tense, sentences over 30 words, "instead of" and
"so that" tails in changesets.
- suggestion: passive voice.
.vale.ini scopes the rules. Docs get every rule. Contributor Markdown
and code comments keep dashes and headings as warnings until their
roughly 8,100 em dashes are rewritten. Released changesets stay in
.changeset/ and are not rewritten, so lint-prose.mjs lints only the
changesets added since PROSE_BASE (the PR base in CI).
The prose workflow installs a pinned, checksum-verified Vale release and
runs `pnpm lint:prose`. The rest of this change rewrites the existing
docs, READMEs, contributor Markdown, and code comments to pass the
error-level rules. Only prose and comment text changed. Renamed
headings keep their anchors.
Spelling stays with cspell, which checks identifiers that Vale does not
see.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: keep prose lint from flattening meaning
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The standalone builds embedded Node.js 26.0.0, which predates the
October 2026 CA distrust shipped in Node.js 26.12.1, so a standalone
pnpm still accepted certificates issued under the distrusted
intermediates. Node.js 26.12.1 is already the repo's devEngines
runtime and is in the lockfile.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* chore(release): 11.28.7, pacquet 12.15.0, pnpr 0.1.0-alpha.16
* docs(release): curate 11.28.7, pacquet 12.15.0, and pnpr 0.1.0-alpha.16 release notes
Add lead paragraphs and group the patch entries by what the reader was
doing, with security fixes first on each page.
- v11: lead with the registry metadata name check, pinned pnpm
trustPolicy check, maxTarballUnpackedSize, and the removal of registry
revisions; merge overlapping entries for minimumReleaseAge metadata
revalidation, frozen-lockfile directory dependency checks,
syncInjectedDepsAfterScripts, verifyDepsBeforeRun, pnpm sbom, and
unreadable config settings.
- v12: order minor highlights from config includes and
lockfile.integrityTarget down to interactive update styling; drop the
pnpr-only distribution note from the CLI changelog; merge overlapping
entries for workspace and pnpr install speedups, minimumReleaseAge and
If-Modified-Since revalidation, frozen-lockfile and injected package
checks, verifyDepsBeforeRun, pnpm prune, pnpm pack, pnpm licenses
list, pnpm self-update, pinned pnpm flag forwarding, and WebContainer
fixes.
- pnpr: lead the minor section with the breaking ecosystem URL prefix,
hosted storage layout, artifacts.orgs, and strict config schema
changes; fold dist.pnpr tarball metadata into one entry and drop the
redundant /metrics note; lead the patch section with reserved-username
and auth security fixes.
- Use one issue-link label style across the release pages.
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
## Proposed Changes
- **Rust (`pnpm-deps-restorer`):**
- Thread `swap_lock: Some(swap_lock)` through every `slot_import_opts` branch in `create_virtual_dir_by_snapshot.rs`.
- Stage absent private targets (`!opts.safe_to_skip`) in `import_indexed_dir` and attempt a lock-free `rename_with_retry(&paths.stage, dir_path)` when `!opts.force` before falling back to `swap_lock.hold()`. Under the swap lock, leave a now-completed directory (or non-directory dirent) in place when `!opts.force` via `completed_or_non_dir_target`, or swap onto an incomplete partial directory.
- Hold `swap_lock` before repairing an existing incomplete private target (`repair_existing_dir`, re-checking `marker_present` under the lock) or replacing a non-directory dirent at a private target (`replace_non_dir`).
- **Rust (`pnpm-fs` `DirLock`):**
- Remove `<user_lock_root>/pnpm-dir-locks-<uid>/<hash>.held` on release while holding its exclusive OS lock (`LockedHeldFile::remove_and_unlock`), leaving no `.held` file behind after a clean lock lifecycle.
- Verify via `path_still_names` after `try_lock()` in `try_lock_held_file_once` that the locked descriptor still names `held_path` on disk, reopening if the previous holder unlinked its `.held` file between `open` and `try_lock`, while keeping dead-holder takeover instant (an ungraceful exit leaves `.held` at `held_path`).
- **TypeScript (`@pnpm/fs.indexed-pkg-importer` in `pnpm11`):**
- Replace `rename-overwrite` in `commitStagedDir` with `overwriteDirSync`, which moves an existing target directory aside on both POSIX and Windows (`${pathTemp(newDir)}_swap_${swapCounter++}`) instead of deleting it in place and spinning for 60s when Windows reports `EPERM` on a busy directory rename, and falls back to `renameOverwriteSync` only for non-directory targets.
- Guard `tryExclusiveImport`'s failure cleanup with `sameCreatedDirectory` so it does not `rimrafSync(newDir)` after a concurrent importer has replaced or completed `newDir`.
- Run `'concurrent forced imports into one target all complete and leave the package in place'` on Windows as well as POSIX.
- **TypeScript (`@pnpm/resolving.npm-resolver` in `pnpm11`):**
- Pick before mirroring or caching in `persistFreshMeta` (matching `serveValidatedMeta`) so a malformed fresh packument is not written to the mirror in the background before `pickMatchingVersionFinal` validates it.
Closes https://github.com/pnpm/tasks/issues/156
Related to pnpm/pnpm#17091
Related to pnpm/pnpm#17113
Decide whether `node_modules` is up to date in one place for both `pnpm install` (`optimisticRepeatInstall`) and `verifyDepsBeforeRun`.
- In Rust (`pnpm-package-manager`), combine the static workspace-state and filesystem checks with the content freshness checks into `freshness::check_freshness`, parameterized only by `GatePolicy`, so `check_optimistic_repeat_install_with_state` and `check_deps_status_before_run` call the same decider.
- Share `gvs_build_markers_complete` and `check_strict_dep_builds_for_fast_path` between `install_already_up_to_date` and `repeat_install_verdict`, and share `recorded_workspace_state` with repeat-install test fixtures.
- Fix two Rust gate divergences uncovered by unifying `check_freshness`: `Gate::Install` now rejects drifted `configDependencies`, and `Gate::Run` now rejects a missing branch lockfile when `useGitBranchLockfile` is enabled instead of letting `node_modules/.pnpm/lock.yaml` stand in for it.
- In TypeScript (`@pnpm/deps.status` and `@pnpm/exec.commands`), check `catalogResolutionsAreUpToDate` in `assertWantedLockfileUpToDate`, avoid mutating `config.ignoredOptionalDependencies` in place, share `recordValidatedWorkspaceState` between root-project and workspace checks, respect `opts.lockfileDir` in `checkRootProjectDepsStatus`, and warn when a `verifyDepsBeforeRun` install leaves `node_modules` out of date.
Closes https://github.com/pnpm/tasks/issues/142
Generated with Antigravity (Claude Opus 4.7)
Follow-up to https://github.com/pnpm/pnpm/pull/17175 (for https://github.com/pnpm/tasks/issues/169):
1. Preserve the originating workspace project identity on importer leaf nodes when building and merging reverse dependency trees across dedicated project lockfiles (`sharedWorkspaceLockfile: false`), so unnamed workspace projects (`the root project`) do not collapse into a single leaf.
2. Track the originating per-project package installation path(s) on merged dependent nodes and use them in `renderDependentsParseable` / `render_dependents_parseable` when `--long` is enabled, so `pnpm why --parseable --long` reports each project's own installed package path.
3. Add test coverage in both the TypeScript (v11) and Rust (v12) stacks for unnamed workspace projects, `--parseable --long` with per-project `modulesDir`, and running `pnpm why` from the workspace root without `-r`.
Written by an agent (Antigravity, Gemini).
Under `sharedWorkspaceLockfile: false` and `injectWorkspacePackages: true`, an
injected workspace dependency is copied into the dependent project and covered
by that project's own install, whereas a dependency declared with the `link:`
protocol remains a symlink whose target project still needs its own install.
- In `pnpm-cli` (`projects_to_verify`) and `pnpm-package-manager`
(`WorkspaceProjectIndex::dep_targets`), keep walking `link:` edges when
`!config.shares_one_lockfile() && config.inject_workspace_packages` instead
of dropping all workspace dependency edges.
- In `@pnpm/workspace.projects-graph`, rewrite `link:` prefixes to `file:`
before passing specs to `@pnpm/npm-package-arg`, which otherwise throws
`EUNSUPPORTEDPROTOCOL` and drops `link:` workspace edges from the project
graph used by `--filter` and `selectProjectsToVerify`.
Closes https://github.com/pnpm/tasks/issues/179
When a filtered install runs with `pruneDirectDependencies: true`, the
workspace root is excluded from the installed importer set unless
explicitly selected, and its direct dependencies are not linked.
In pnpm v11, `computeMergedCurrentLockfile` previously copied
`filteredLockfile.importers` for unselected importers not present in
`currentLockfile`. Because `filteredLockfile` retains every importer
snapshot, the workspace root was written to
`node_modules/.pnpm/lock.yaml` with its dependencies populated, causing
`checkDepsStatus` (`verifyDepsBeforeRun`) to treat the root as installed
via the current-lockfile fallback even though `validation.manifests`
left `.` unrecorded. Carry unselected importers that have no prior
`currentLockfile` entry as empty `{ specifiers: {} }` entries on
filtered installs so `importerRecordsDependencies` reports them as not
installed.
In pnpm v12, wire `pruneDirectDependencies` through `pnpm-config`,
`install_dirs` selection, `RunExecution`, and `config_blocks_fast_path`,
and check `importer_records_dependencies` in `require_installed` when
falling back to `node_modules/.pnpm/lock.yaml` for an unrecorded
importer.
Closes https://github.com/pnpm/tasks/issues/181
Written by an agent (Antigravity, Gemini 4 Argon).
In workspaces configured with sharedWorkspaceLockfile: false, recursive
pnpm why now loads each selected project's dedicated lockfile and
packageConfigs modulesDir in both pnpm v11 and pnpm v12 instead of
reading only a single lockfile at the invocation directory.
Closes https://github.com/pnpm/tasks/issues/169
Add the 14 missing pnpm v12 WorkspaceSettings keys
(addMissingPeerTypes, autoDedupe, bootConfig, macosBackup,
nodePackageMap, permissions, pipelineBase, publishWaitTimeout, python,
remoteCache, saveTypes, skills, storeIndexFormat, and tools) to
SETTINGS_OF_OTHER_PNPM_VERSIONS in
pnpm11/config/reader/src/unknownSettings.ts so pnpm v11 annotates them
as pnpm v12 settings instead of offering unrelated spelling suggestions.
Sync the mirrored TYPED_WORKSPACE_MANIFEST_KEYS and
CONFIG_ONLY_SETTING_KEYS slices in pnpm/crates/config/src/known_settings.rs
with unknownSettings.ts (adding modulesDirsByProjectName and replacing
tools with tasks, since tools enters through WorkspaceSettings fields),
and add cross-check tests in both stacks verifying that every
WorkspaceSettings field is either in v11's KNOWN_SETTING_KEYS or in
SETTINGS_OF_OTHER_PNPM_VERSIONS.
Closes https://github.com/pnpm/tasks/issues/167
When pnpm-lock.yaml is missing after a filtered install (or when a covered
project is missing a per-project lockfile under sharedWorkspaceLockfile: false),
the recorded validation proof is not used and the current lockfile stands in as
the wanted lockfile:
1. In pnpm v11 (`@pnpm/deps.status`), gate the skip of
`findProjectMissingFromCurrentLockfile` in `findModulesDirIssue` on a usable
validation proof rather than the presence of `workspaceState.validation`, and
verify in `readCurrentLockfileAsWanted` that the current lockfile records the
direct dependencies of every checked project after a filtered install.
2. In pnpm v12 (`pnpm-package-manager`), check that the current lockfile records
each unproven project and the snapshots of its direct dependencies both when
comparing against a wanted lockfile and when the current lockfile stands in
for a missing wanted lockfile.
Closespnpm/tasks#180
Related to pnpm/pnpm#17150
Handle registry read and environment-write failures in setup's existing
post-install outcome handler. Keep installation separate from environment
persistence, as the missing-shell-config fallback already does.
Print session-local PowerShell assignments and profile instructions when
registry persistence is unavailable. Preserve the original diagnostic and
leave environment conflicts and other failures fatal. Do not attempt to
replace an unreadable persisted PATH or elevate to another Windows user.
Apply the behavior in both CLI versions, with regression tests for read and
write failures and literal quoting of ASCII and typographic apostrophes.
Closespnpm/pnpm#17167.
Unify repeat-install freshness and `verifyDepsBeforeRun` validation across
pnpm v11 and v12 (Closespnpm/tasks#161, pnpm/tasks#157, pnpm/tasks#148):
1. Centralize v11 workspace state updates in `@pnpm/workspace.state`
(`preserveCarriedSettings`) and `@pnpm/installing.commands`
(`workspaceStateUpdate.ts`), preserving carried `dev`/`optional`/`production`
settings when `ignoredWorkspaceStateSettings` ignores them and preserving
v12-recorded `autoDedupe`. A filtered install that prunes direct
dependencies no longer records the workspace root as installed.
2. Hold `{.}...` workspace dependencies for non-recursive `pnpm run`,
`pnpm exec`, and `pnpm restart` inside a workspace project in v11
(`selectProjectsToVerify.ts`).
3. Align v11 recorded validation (`checkRecordedWorkspace`) and legacy
unrecorded validation (`checkModifiedSinceLastValidation`) in
`@pnpm/deps.status` with v12's `check_freshness`, including empty-project
handling, `dedupeDirectDeps`, `dedupePeerDependents`, `autoInstallPeers`,
`excludeLinksFromLockfile`, catalog/override config comparison, and
`opts.lockfileDir` resolution.
4. Forward effective CLI flags (`--lockfile-dir`, `--modules-dir`,
`--virtual-store-dir`, `--store-dir`, `--dir`, `--workspace-root`, `--prod`,
`--dev`, `--no-optional`, `--filter`, `--filter-prod`, `--test-pattern`,
`--changed-files-ignore-pattern`, and `--config.*`) when
`verifyDepsBeforeRun: install` spawns `pnpm install` in both v11 and v12.
5. Align `GatePolicy::dedupe_peers` and empty-project predicates in v12
`pnpm-package-manager`, and run `repeat_install_oracle.rs` across v12, v11,
and mixed v11/v12 writers in CI, with a longer nextest period for the
peer convergence harness that shares the job.
6. Parse `--config.<key>=<value>` with the type the command declares for
`<key>` in v11, so the list and number options forwarded to the spawned
install arrive as lists and numbers; forward `--npmrc-auth-file` in v11
and `--test-pattern` / `--changed-files-ignore-pattern` in v12, so both
forward the same set; and skip the current-lockfile read for a project
whose manifest changed but whose lockfile did not, as v12 does.
7. Resolve relative v11 store overrides against the workspace or project
root before forwarding them to the spawned install. Remove the isolated
linker's duplicate package merge so filtered installs retain unselected
dependencies and drop obsolete selected dependencies from the current
lockfile. Cover both cases with CLI regressions. The restore API also
handles a selected project absent from the previous current lockfile.
8. Record workspace validation from the projects the installers actually
processed. Ignored projects receive no new proof, and empty or entirely
ignored selections leave state untouched. Cover both lockfile modes and
an ignored workspace root.
Add regression tests in both pacquet and TypeScript v11 verifying that
when a dependency declared also as an optional peer had been recorded
peer-resolved in an earlier lockfile, re-resolving one of its parents
retains the dependency on the peerless snapshot instead of dropping it.
Closespnpm/pnpm#17161
With `useLockfile: false`, `linkPackages` built `hoistedWorkspacePackages`
from the selected projects only. It now receives every workspace project
(`ctx.projects`) as `allProjects`, matching `linkIsolatedLayout` in
deps-restorer and `plan_hoist` in pnpm v12.
Closespnpm/tasks#168
PR pnpm/pnpm#15198 added directory validation to reject hoist destinations
that point to symlinks escaping the modules root. However, it also rejected the
hoist root itself (such as `node_modules`) when symlinked to an external directory,
failing with "workspace hoist directory is not a real directory".
Allow a hoist root and its ancestors to be symlinks as long as they lead to an
existing directory. Every directory below a hoist root must still be a real
directory. In the Rust CLI the parent-directory preparation distinguishes the
two kinds through one create-or-validate routine. In the TypeScript CLI the
trusted root is the public hoist directory for destinations inside it and the
destination's modules directory otherwise, shared by the hoist and
deps-restorer packages, so an external virtual store behaves the same way in
both CLIs.
Resolvespnpm/pnpm#17157.
Keep license reporting scoped to importers recorded in the selected lockfile. An unrecorded workspace package contributes no dependencies; it must not crash or inherit the root importer's dependency list.
Require the lockfile in the Rust CLI, matching the existing TypeScript error. Render empty license reports according to the selected output format in both CLIs.
Closespnpm/tasks#174.
Load dedicated lockfiles for selected and reachable workspace projects when
sharedWorkspaceLockfile is false. Re-key their importers to the workspace root
before SBOM collection, and merge package entries without losing required
package status. Reject conflicting package data to avoid emitting an inaccurate
SBOM. Fail when a selected or reachable project lacks a lockfile importer.
Read directory package metadata relative to the project that owns its lockfile.
Fixespnpm/tasks#170.
Fix three `pnpm prune` issues across pnpm v11 and pnpm v12:
1. Extraneous direct dependencies in `node_modules`:
- In pnpm v11, `prune.handler` did not disable `optimisticRepeatInstall`, and `installDeps` could short-circuit via `canReportAlreadyUpToDate` before `headlessInstall` ran `prune()`. In addition, `pruneRemovedDependencies` skipped `prune()` when `currentLockfile` was absent (for example, when a project had zero dependencies). Disable `optimisticRepeatInstall` in `prune.handler`, prevent `canReportAlreadyUpToDate` when `pruneDirectDependencies` or `pruneStore` is set, and pass an empty `currentLockfile` to `prune()` when `pruneDirectDependencies` is enabled and no current lockfile exists.
- In pnpm v12 (`pacquet`), `prune_stale_modules` only unlinked direct dependencies that were present in `current_lockfile` and removed from `wanted_lockfile`, never scanning `node_modules` on disk for extraneous entries when `prune` ran. Add `PruneTreeOptions::direct_dependencies` and `PruneExtraneousDirectDeps` to scan each selected importer's `node_modules` directory during `pnpm prune`, remove entries (and their bin shims and emptied scope directories) that are not wanted direct dependencies, excluded manifest links, or root public hoists, and force `commit_modules_state`'s virtual-store sweep during `prune`.
2. `pnpm --filter <pkg> prune` and `pnpm -r prune` in pnpm v11:
- Separate `cliOptionsTypes` from `rcOptionsTypes` in `@pnpm/installing.commands` `prune` and add `'recursive': Boolean` so the CLI parser accepts `-r` / `--recursive` and `--filter`.
- Do not pass `include` to `validateModules` when `pruneDirectDependencies` is set so `prune --prod` does not purge every workspace project's `node_modules` directory.
- Do not auto-include the workspace root in `shouldInstallWorkspaceRoot` during filtered `prune`.
- Scope `pruneDirectDependencies` to the selected `ctx.projects` and keep all dependency groups in `findOrphanDepPaths` / `getPkgsDepPathsOwnedOnlyByImporters` during a partial workspace `prune` so unselected workspace importers do not lose their devDependencies from the shared virtual store.
3. Workspace selection and `sharedWorkspaceLockfile: false` in pnpm v12:
- Add `CliCommand::Prune(_)` to `CliCommand::recursive_by_default`.
- Route `PrunePipeline` through `select_install_family_plan` (`PerProject`, `Shared`, `Single`) and pass `WorkspaceInstallSelection` (with `install_dirs` restricted to `selected_dirs`) into `Install::run_prune`.
Closes https://github.com/pnpm/tasks/issues/171
Fix `pnpm patch`, `pnpm patch-commit`, `pnpm patch-remove`, and `pnpm install` in workspaces with `sharedWorkspaceLockfile: false` in both pnpm v11 (`pnpm11/`) and pnpm v12 (`pnpm/`):
1. `pnpm patch`: When `sharedWorkspaceLockfile: false` and the target directory has no current lockfile, merge `packages` and `snapshots` across all workspace projects' current lockfiles to resolve the installed package version. Anchor `patchesDir` to `workspaceDir ?? lockfileDir`.
2. `pnpm patch-commit`: Search workspace project `modulesDir`s and `<editDir>/../..` when resolving the patched package directory, write patches relative to `workspaceDir ?? lockfileDir` without mutating `opts.patchedDependencies` to relative paths before delegating to `install`, and update lockfile snapshots across all workspace projects.
3. `pnpm patch-remove`: Resolve patch files relative to `workspaceDir ?? lockfileDir` and run a workspace-wide dedicated-lockfile install when `sharedWorkspaceLockfile: false`.
4. `pnpm install`: Skip per-project unused-patch errors during individual workspace project installs when `sharedWorkspaceLockfile: false`, and verify patches across the union of all workspace project lockfiles once the full workspace install finishes.
Closespnpm/tasks#176
In pnpm v11, externalDependencies and autoInstallPeersFromHighestMatch
were omitted from PnpmSettings, OptionsFromRootManifest, and
TYPED_WORKSPACE_MANIFEST_KEYS, and were not included in the
Pick<Config, ...> option types for install commands.
Setting externalDependencies in pnpm-workspace.yaml therefore emitted
an unrecognized-setting warning and passed a raw string[] through to
`@pnpm/installing.linking.real-hoist`, which called
externalDependencies.has() and crashed hoisted installs with a
TypeError.
- Declare autoInstallPeersFromHighestMatch and externalDependencies on
PnpmSettings, OptionsFromRootManifest, and
TYPED_WORKSPACE_MANIFEST_KEYS (plus its mirror in
pnpm/crates/config/src/known_settings.rs).
- Validate both settings in getOptionsFromPnpmSettings and normalize
externalDependencies into a Set<string> on OptionsFromRootManifest.
- Convert Set config values to arrays in configToRecord so
pnpm config get externalDependencies outputs a JSON array matching
pnpm v12.
- Accept Iterable<string> for externalDependencies in
`@pnpm/installing.linking.real-hoist`.
- Thread autoInstallPeersFromHighestMatch and externalDependencies
through InstallCommandOptions, InstallDepsOptions, and
RecursiveOptions.
Closes https://github.com/pnpm/tasks/issues/166
Separate config dependency bootstrap from install-time reconciliation. Only
reconciliation removes undeclared roots; diagnostic commands must retain them
so they can inspect the existing lockfile.
Share state loading, declared-root retention, and env graph pruning between
config dependency preparation and exclusion pruning. Load state before opening
the store and pass it into resolution. The Rust repeat-install fast path checks
this state so removing the last declaration cannot bypass cleanup.
Reuse existing verification, writing, and materialization for removals. Frozen
installs reject stale roots before mutation. Preserve package-manager roots,
including distinct versions of a package shared with config dependencies.
Keep Rust graph operations in the lockfile crate to avoid a dependency cycle.
Follow-up to pnpm/pnpm#17144.
During install, pnpm applies readPackageHook (built via createReadPackageHook
from overrides, packageExtensions, readPackage hooks,
ignoredOptionalDependencies, and ignoreCompatibilityDb) to each project's
manifest before writing importer specifiers to pnpm-lock.yaml.
When verifyDepsBeforeRun or optimistic repeat install checked a modified
manifest via assertWantedLockfileUpToDate in TypeScript, it compared the
unhooked manifest against wantedLockfile.importers[projectId] and
linkedPackagesAreUpToDate, causing false outdated reports whenever overrides,
packageExtensions, or readPackage hooks rewrote declared ranges. Apply
createReadPackageHook to a cloned copy of the project manifest before
satisfiesPackageManifest and linkedPackagesAreUpToDate, pass
resolution/injectWorkspacePackages/hookedManifests to
linkedPackagesAreUpToDate, reuse parsedOverrides and readPackageHook across
projects when sharedWorkspaceLockfile is true, and isolate per-project
packageConfigs overrides and hookedManifests caches when
sharedWorkspaceLockfile is false.
In the Rust pnpm v12 optimistic repeat install check, check_importer_satisfies
already applied VersionsOverrider, but linked_packages_are_up_to_date still
inspected the un-overridden project manifest. Apply VersionsOverrider before
linked_packages_are_up_to_date so overridden workspace links stay up to date
when a manifest is edited.
Closespnpm/pnpm#17127
A directory dependency whose spec still matches can itself be stale: its own dependencies may have changed without the directory moving. Recurse into directory children when checking local directory dependency freshness, carrying the peer-alias scope of every ancestor and checking each snapshot once per peer scope. Apply the same scope handling to the Rust check. Links have no dependency snapshot and keep their current treatment.
Fixespnpm/pnpm#17128.
---------
Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
An injected package's file: child installed under another alias is
recorded as name@file:<path>. getLocalPath only read refs that start
with file: or link:, so the freshness check could not compare the path
and reported a lockfile pnpm had just written as outdated, failing the
next frozen install. It now also reads the path from a name@file: ref.
Closespnpm/pnpm#17130
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Include the env lockfile's snapshots in the resolved-version set used to
prune workspace exclusion lists. Config dependencies and their transitive
dependencies are recorded in that leading document and were missing from
the main-lockfile-only scan.
Read the env document at the configured root only when exclusion pruning
is enabled. Filter out removed config dependency roots and reuse the env
graph pruner before merging versions with the relevant project lockfiles.
Move the Rust graph helper into the lockfile crate for reuse without a
dependency cycle. Keep manifest writes after the fallible env read.
The earlier per-project-lockfile fix established that pruning needs the
union of all resolved packages. Extend that same boundary to both lockfile
documents, with regression coverage across the lockfile layouts and a
frozen reinstall after pruning.
Closespnpm/pnpm#17142.
A frozen install kept whatever the lockfile recorded for a non-range
child spec of a local directory dependency, comparing only the package
name. A tarball URL or a full git commit names one exact source, and the
lockfile keys the child by that URL or commit, so a spec naming a
different one is now outdated in both CLIs. Git specs are classified
with the git resolver's parser first, so https git URLs and &path:
parameters are handled. Moving selectors and an
unchanged URL keep the recorded resolution on a frozen install, as
decided in pnpm/tasks#145. For a git spec the commit and the path:
subdirectory are compared, not the repository.
Closespnpm/pnpm#17129
A shared-lockfile deploy synthesizes three views of the deployed
workspace: the in-memory install config used during deploy, the deployed
`pnpm-workspace.yaml`, and the deployed `pnpm-lock.yaml` settings. When
those views drifted or when post-deploy lockfile verification
re-compared deployed `<name>@file://...` directory snapshots against raw
on-disk `package.json` manifests without the source workspace's
catalogs, overrides, `packageExtensions`, `readPackage` hooks, or
singleton peer bindings, post-deploy verification and frozen installs
failed. Unused workspace `patchedDependencies` were also copied into the
deployed lockfile and workspace manifest.
Both pnpm v12 and pnpm v11 now project the deploy install config,
deployed `pnpm-workspace.yaml`, and deployed `pnpm-lock.yaml` settings
from a single self-contained specification, prune unused
`patchedDependencies` using the shared applied-patch-keys helper, and
skip raw on-disk manifest re-comparison for deployed `<name>@file://...`
directory snapshots once their snapshot entry is verified.
Closespnpm/tasks#165
* fix: align the pre-switch config read, lockfileDir pin sync, and pre-command argv scan
v11:
- A pnpm-workspace.yaml with an unreadable setting no longer drops its
pin-decision settings (pmOnFail, lockfileDir, ...). After the whole file
fails, those keys are applied alone, as v12 does.
- The env lockfile is read from and written to config.lockfileDir, so a
lockfileDir set in pnpm-workspace.yaml no longer leaves a stray
pnpm-lock.yaml at the workspace root.
v12:
- An undeclared option before the command name is read as a flag without
a value, as v11 reads it, so `pnpm --new-flag install` switches to the
pinned pnpm instead of failing with clap's unknown-argument error.
The triple Config load flagged in the task only happens on the run that
first records the pin, so it is left as is.
Closespnpm/tasks#162
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): read an undeclared option as a flag only right before a command name
Keeps `pnpm --undeclared value config list` ambiguous, so it is not read
as the command `value` and does not slip past the config-command exemption.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): satisfy clippy or_fun_call
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(config): keep pmOnFail when the packages setting cannot be read
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(config): resolve a relative lockfileDir from pnpm-workspace.yaml against the workspace
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Since pnpm/pnpm#17067, packages: records every declared peerDependency
even when resolved from a package's own dependencies rather than bound
in its peer suffix.
Filter descendant declared peer edges through BoundProviders and
getBoundPeerProviders during hashed peer suffix transitive recovery
and deploy singleton peer ancestor walking so unshadowed own
dependencies declared in peerDependencies are not misclassified as
peer bindings.
Closes https://github.com/pnpm/tasks/issues/143
On macOS, Node's fs.lchmodSync opens the path with O_WRONLY | O_SYMLINK,
which fails with EISDIR for every directory. A new store directory that
its owner cannot read, as under a 0o477 umask in a group-writable store,
reached that call and failed the install.
Change the mode through a descriptor opened with O_SEARCH, which needs
no read access, and fchmod it. O_NOFOLLOW and O_DIRECTORY keep the
guarantee that a directory swapped for a symlink is not followed. The
macOS and Linux branches now share one helper and differ only in how
they open the handle and change its mode. The helper compares the owner
with the effective uid, which owns the directories pnpm creates, as pnpm
v12 does.
The existing cafs test for a umask that removes owner read covers it.
CI has no macOS job for the TypeScript CLI, which is why it never ran
there. pnpm v12 already uses O_SEARCH with fchmodat and is not affected.
Part of pnpm/tasks#143.
- Move the peer-shadowing rule and the provider lookup with lockfile-pin
precedence into one owner: provider_for/ProviderPick in peer_provider.rs
and providerFor in v11 hoistPeers.ts.
- Read and write peer suffixes only through PeerBindings (pnpm-lockfile) and
parsePeerBindings (@pnpm/deps.path), with a round-trip test over the
repo's lockfiles.
- v11 picks hoisted optional peers through v12's lockfile tiers.
- Report a cached node's own missing peers in every importer, so a project
no longer borrows an out-of-range peer from another project's subtree
(v12, resolvePeersFromWorkspaceRoot: false).
- Classify optional-peer edges by the bindings the key records, so a prod
install and deploy keep a package's own dependency that is also its
optional peer (v11 and v12).
- Add proptest graph tests on StubResolver.
Reusing a pin only when the owner returns it (step 2) is left out: deciding
reuse after the peer walk cost 40-60% on lockfile-only installs.
With sharedWorkspaceLockfile set to false, checkUnchangedWorkspace returns
undefined when a project's pnpm-lock.yaml is missing and there is no shared
lockfile to restore, so the full check can throw
RUN_CHECK_DEPS_LOCKFILE_NOT_FOUND. The caller then ran validateModifiedWorkspace
with the modified projects only. When no manifest or lockfile was modified that
list is empty, no project lockfile was read, and the gate reported the
workspace as up to date.
The lockfile scan now records the directories without a wanted lockfile, and
with a lockfile per project the gate also checks the projects whose lockfile is
missing, so the per-project reader reports it. Only the projects the install
the gate spawns covers are held: a filtered install leaves the projects it did
not select without a lockfile, and an install with the same filter cannot
create one. The coverage rule is shared with the recorded-validation path.
Closes https://github.com/pnpm/tasks/issues/138
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Concurrent installs into one project raced on the package import's
stage-and-swap: the second importer's rename onto the target failed with
ENOTEMPTY once the first had put its own staged copy in place.
Rust: a forced import stages the package. A target the import found
absent is taken by one rename of the stage, so a fresh forced install
pays no lock. An occupied target is replaced under a per-target swap
lock (keyed by the resolved target path, under the per-user lock
directory), so swaps of one target are serialized. The stage and the
target are exchanged atomically where the platform allows it (a new
pnpm_fs::exchange_dirents: renameat2 RENAME_EXCHANGE on Linux,
renamex_np RENAME_SWAP on macOS), so the target path is never absent
mid-swap there, which is what makes the lock-free rename of an absent
target safe. Elsewhere, and on filesystems without the operation, the
old tree is moved aside by a rename and the stage renamed in; the
vacancy is two renames long, and an importer that saw the target absent
has to stage its whole package before its own rename can land in it.
The old tree is removed afterwards; one that cannot be removed stays
next to the target and the install warns the user to remove it, where
it used to fail before replacing anything. A lock that cannot be taken
in time or at all is logged at debug and the swap proceeds unserialized.
A rename that still collides is accepted when the target already holds
exactly this import, compared by entry set, file content, and file mode
(allowing the linkable hardlink modes the store can produce), and
retried otherwise. Forced imports of shared slots keep the in-place
path: nothing may remove a shared slot.
TypeScript: after a failed staged rename, the destination is accepted when
it holds exactly this import (allImportedEntriesMatchExactly, retried a
few times because a concurrent import mid-swap leaves the destination
absent for a moment), and a preserved node_modules/ moved into the stage
is moved back. mismatchReason compares file modes against the mode the
import would apply.
Closespnpm/tasks#48.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
pnpm v11 reads the global config.yaml from $XDG_CONFIG_HOME/pnpm, or on
macOS from ~/Library/Preferences/pnpm when XDG_CONFIG_HOME is unset.
Four config.reader tests assert built-in defaults such as
minimumReleaseAge 1440, so a developer whose global config sets that
setting saw them fail with their own value. The test file now points
XDG_CONFIG_HOME at an empty temporary directory and removes it after
the run. The isolation stays in this file: setting it in the shared
jest preset would also move git's global config for every suite.
"global config.yaml registries cannot redirect pnpm_config__auth
routes" passed XDG_CONFIG_HOME only in the env given to getConfig, but
the global config dir is resolved from process.env, so the config.yaml
the test wrote was never read and the test passed with the routing
guard removed. It now sets process.env.XDG_CONFIG_HOME while getConfig
runs.
Five tests reimplemented the save/set/try/finally/restore of
XDG_CONFIG_HOME that the file's withEnv helper already provides. They
now call withEnv, which returns the callback's result.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
The hosted store shared one key space between packages, orgs and blobs:
a registry without an org stored packages at the store root, an org's
registry stored them in `<org>/`, a package's blobs sat beside its
document, and a nested image repository sat inside its parent's
directory. So an org collided with an unscoped package of the same name
(an unpublish of that package removed the org's whole namespace), the
implicit `<ecosystem>~<name>` org of a grouped registry was a valid npm
name, and OCI GC and verify needed ad-hoc exclusion lists. On S3,
object_store percent-encodes `~` in keys while the upload sweep compared
raw prefixes, so grouped OCI registries were never swept. This is part
of section A4 of pnpm/tasks#131.
The new layout (pnpr_storage::layout), the same on disk and in a bucket:
layout.json {"layout": 1}
default/packages/<name>/document.json
default/packages/<name>/blobs/<file>
orgs/<org>/packages/...
Every name, org and file name is one segment: `[a-z0-9._-]` as is,
every other byte as `!` and two hex digits. That keeps nested names
siblings, separates names that differ only in case on case-insensitive
filesystems, and leaves nothing for object_store to escape, so a key is
byte for byte the local path. Per-namespace records stay inside their
namespace; records every namespace shares stay at the root. Listing is
one directory read, so the `.package-index` workaround is gone, and the
blob inventory reports package and blob names instead of paths.
Startup refuses a store marked with another layout or an unmarked store
holding data, and marks an empty one. The proxy cache uses the same
package layout and is emptied once when its marker is missing.
`pnpr upgrade-storage` converts an earlier store on disk or in a bucket,
telling org directories from packages by the configured orgs; it moves
blobs before documents so an interrupted run can resume, and refuses a
directory that is both an org and a package. export, verify and
migrate-storage check the layout, and migrate-storage marks its target.
The registry fixtures the pnpm test suites serve are built in the new
layout, and the cached build is keyed by the layout version.
`dedupeInjectedDeps` turns an injected workspace dependency into a
`link:` when the copy's children are all provided by the workspace
package itself. It read the package's children from what this install
resolved. The resolver skips a dependency that is installed and not
updated, so on an install that left the package's dependencies
installed, the map was incomplete and the copy stayed `file:`. A fresh
install, or one with `--lockfile-only`, resolves every dependency and
links it.
The target's dependencies are now the resolved ones plus the locked ones
of the aliases this install skipped.
pnpm v12 already converges here.
Closespnpm/pnpm#17094
When the reader of stdout had exited, pnpm v11 called `process.exit(0)` at
the first failed write. That replaced the command's own exit code and
skipped the work left after the write: `pnpm outdated | true` exited 0,
and `pnpm clean --lockfile | true` printed its first "Removing" line and
stopped before removing anything.
The stdout error handler now ignores EPIPE, so the remaining output is
dropped and the command finishes with its own exit code. pnpm v12 does
the same since pnpm/pnpm#16932.
Closespnpm/tasks#133
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A deploy turns each workspace project the deployed one depends on into
a `file:` package entry, and that entry dropped the project's declared
peers. Every other `packages:` entry records them since
pnpm/pnpm#17067, and pnpm v11's frozen check requires them for a local
directory dependency, so a frozen install of the deploy directory failed
with ERR_PNPM_OUTDATED_LOCKFILE when a copied workspace package declared
a peer and `injectWorkspacePackages` was off. pnpm v12 wrote the same
lockfile, but its frozen check accepted it.
Both stacks now build the entry's `peerDependencies` and
`peerDependenciesMeta` with the function the lockfile writer uses:
`lockfile_peer_blocks` in v12 and `getLockfilePeerDependencies` in v11,
both in deps-resolver.
Closespnpm/pnpm#17093
Three catalog snapshot tests expected `^100.0.0` to resolve to `100.1.0`.
That holds only while `latest` of `@pnpm.e2e/bar` and `@pnpm.e2e/foo`
points at `100.1.0`. Other test files move that tag on the shared registry
mock from parallel Jest workers, and the resolver then picks `100.0.0`.
The tests now use `^100.1.0`, which resolves to `100.1.0` whatever the tag
is. They check how catalog snapshots carry across installs, not range
resolution, so the range itself does not matter to them.
Closespnpm/tasks#152
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A gzip bomb inflates a kilobyte-sized tarball into hundreds of gigabytes
during extraction. pnpm v12 bounds what an archive may unpack to with
maxTarballUnpackedSize since pnpm/pnpm#17054; pnpm v11 had no such
bound, so an install could still exhaust the disk.
pnpm v11 now adds up the sizes that the regular-file entries of a
package archive declare in their tar headers and fails the extraction
with ERR_PNPM_TARBALL_TOO_LARGE once the total passes
maxTarballUnpackedSize (default 4 GiB). Header sizes stand in for
written bytes: the tar parser never yields more payload than a header
declares, so their sum bounds what can reach the store, and checking it
before the payload is read keeps the crossing entry out of the store.
Entries the ignore filter drops count as well. The error is
deterministic for a given archive, so the fetch retry loop does not
retry it. The zip archives that runtimes are installed from are held to
the same limit. The setting name, default, error code, message and hint
match pnpm v12.
pnpm v12's zip extractor bounded each entry by its central-directory
size but not their sum. Every stored zip entry now counts against the
payload ceiling of the TarLimits that tarballs use.
The "Added in" line in the v11 docs assumes the next release is 11.28.7.
Completes https://github.com/pnpm/tasks/issues/81.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
The install keeps a package's links to its own content as links in an
injected copy, following the importer's rules: a link to an imported
path is recreated, a link to a file the import leaves out becomes that
file, and a link to any other directory is left out. The sync after a
script listed in syncInjectedDepsAfterScripts, and the edit watcher,
followed links instead:
- v12: a directory link the script added became a real directory, and
under deployAllFiles it went missing.
- v11: the same materializing in packlist mode, and the sync failed on
a copy that links to one directory twice (EEXIST) or links back to an
ancestor (ELOOP). v11's walk of every file also had no cycle guard,
so an install with deployAllFiles failed on such a link.
Both syncers now read the source with the install's symlink policy and
the copy without following links, record a link by the package-relative
path it points at, and recreate it with the importer's rules. v12 moves
relative_link_target and symlink_file_or_copy from the importer into
pnpm-fs to share them. v11 exports internalSymlinkTarget and
copyInternalSymlink from fs.indexed-pkg-importer, and adds a
preserveSymlinks option and an ancestor guard to the directory
fetcher's walk.
v11's install under deployAllFiles still materializes directory links,
where v12 keeps them. Each sync matches its own install.
Part of pnpm/tasks#147.
Read maintainers from full package metadata when listing owners in both CLI implementations.
The npm registry does not serve the owners endpoint used by the command.
Preserve registry selection, authentication, and error handling. Accept absent maintainers
and display the name when a maintainer has no email.
Allow up to 50 MiB for Rust metadata responses because they contain every published version.
Reject truncated responses explicitly. Update the tests to use the actual metadata protocol.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>