feat(cli): ship only the dependency patches a code bundle applies [RED-893] [ship] - #1446
Merged
Merged
Conversation
A code bundle carries the workspace's whole pnpm `patchedDependencies` map but only a subset of its members, so a patch whose package belongs to an unbundled member applies to nothing. pnpm 10+ then aborts the lockfile pruner's temp-dir install with ERR_PNPM_UNUSED_PATCH and the unpruned lockfile ships instead — the outcome pruning exists to avoid. Pass --config.allowUnusedPatches=true so the prune produces a lockfile. The now-unused declaration still travels in the bundle; filtering it out follows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…[RED-893] Adds patched-dependencies.ts: given a bundle's declaring config plus the lockfile before and after pruning, it works out which `patchedDependencies` declarations no longer apply and produces the rewritten config and lockfile bytes that drop them. Nothing calls it yet. pnpm records each declaration's hash in the lockfile's `patchedDependencies` section and repeats it as `patch_hash=<hash>` wherever the patch was applied. The section mirrors the config rather than the dependency graph, so pruning leaves an unused declaration in place and the marker is what tells the two apart. The test is differential — the marker must be present before pruning and absent after — so a lockfile whose markers cannot be read drops nothing instead of dropping everything. Every other uncertainty declines the same way, leaving the bundle untouched: an unreadable config or lockfile, both declaration sites populated at once (pnpm honors only one, and which one depends on the major), a declaration the project's own lockfile never recorded, or a rewrite whose reparsed result does not match the original minus exactly the removed keys. Patch files are dropped only by normalized archive path, and only when no surviving declaration references them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wires the patch planner into the bundler: after the lockfile is pruned, declarations the pruned lockfile shows no longer apply are removed from the bundled pnpm config, their patch files are left out of the archive, and the matching lockfile entries go with them. The whole plan is applied in one pass of map mutation, because a half-applied filter ships a config and lockfile that disagree and fails the remote install either way it lands. Only a `.patch` directly under the conventional `patches/` directory is ever removed. A declared path can name any bundled file — an .npmrc carrying registry auth, a pnpmfile the lockfile records a checksum for, a fixture a check's own include glob pulled in — so a declaration pointing elsewhere loses its config and lockfile entry while the file stays. Whatever the step does or declines to do, it then checks what actually ships: if the bundled config still declares a patch the bundled lockfile does not record, the CLI says so, since that pair is what fails the runner's install. A rewritten root manifest ships as a synthesized file, so it is hashed the way on-disk manifests are, with `version` stripped — hashing it verbatim would discard the runner's dependency cache on a release bump that changed no install input. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sorccu
force-pushed
the
simo/red-893-prune-unused-pnpm-patches
branch
from
August 24, 2026 21:43
2faa9e3 to
2b1e36f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear: RED-893
Affected Components
What and why
A code bundle ships a subset of the workspace but the workspace's entire
pnpm
patchedDependenciesmap. A patch whose package belongs to an unbundledmember therefore applies to nothing, and pnpm 10/11 abort with
ERR_PNPM_UNUSED_PATCHas soon as they re-resolve.The victim today is the lockfile pruner's own temp-dir install (RED-886): for
any patched workspace that bundles a subset of itself, pruning fails and the
unpruned lockfile ships with a warning — the outcome pruning exists to prevent.
Three commits, one per phase:
--config.allowUnusedPatches=trueon the pnpm lockfile-only install, sothe prune produces a lockfile instead of failing.
patched-dependencies.ts, a pure planner that decides which declarationsno longer apply and produces the rewritten config/lockfile bytes.
Bundler.finalize(), between pruning and embedded-packagematerialization, mirroring the existing
filterTarballsByLockfilepattern.How "unused" is decided
pnpm records each declaration's hash in the lockfile's
patchedDependenciessection and repeats it as
patch_hash=<hash>wherever the patch was applied.The section mirrors the config, not the dependency graph, so pruning leaves an
unused declaration in place — the marker is what tells the two apart.
The test is differential: the marker must be present in the original
lockfile and absent from the pruned one. Absence alone would mean a lockfile
whose markers we cannot read classifies every declaration as unused and strips
them all, which is the one outcome that silently unpatches dependencies.
Notes for the Reviewer
The risk to weigh throughout is dropping a declaration that is genuinely in
force. Everything below is shaped by that.
path is user input that reaches a
delete, and it can name any bundled file —an
.npmrccarrying registry auth, a pnpmfile the lockfile records a checksumfor, or a
.patchfixture a check's ownincludeglob pulled in. Onlypatches/<name>.patchis removable; a patch kept elsewhere loses its configand lockfile entry while the file stays (inert). The directory constant is
shared with
getAutoIncludesso the two cannot drift.produced it: a declaration the project's own lockfile never recorded (the pnpm
that wrote it may not read that declaration site — pnpm 11 ignores the
package.jsonpnpmfield, andpnpm-workspace.yamlis pnpm 10+); bothdeclaration sites populated at once (pnpm honors one wholesale and which one
depends on the major); any unreadable config or lockfile; any rewrite whose
reparsed result is not the original minus exactly the removed keys.
pair that fails the remote install (
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH, orENOENTon a missing patch file). The apply step is pure map mutation over analready-computed plan — no IO, no parsing.
written; a test asserts they are byte-identical after bundling.
is hashed the way on-disk manifests are, with
versionstripped. Hashing itverbatim would discard the runner's dependency cache on a release bump that
changed no install input.
byte-identical to what pnpm itself writes for a config that never declared the
patch, and a real-pnpm test asserts exactly that so a future pnpm formatting
change fails the suite rather than shipping a subtly different lockfile.
Known residual, not addressed here: a rewritten
pnpm-workspace.yamlis notitself a cache-hash input, so the key tracks it only via the pruned lockfile
bytes. Closing that means adding a record type to a hash format with a
documented cross-language mirror, which wants coordinating separately.
pnpm 8/9 are EOL and out of scope. Bun and Yarn are unaffected: bun does not
raise this error, and Yarn Berry encodes patches as
patch:locators inside thelockfile, so pruning already removes them.
Verification
Behaviour verified empirically against pnpm 10.33.4, 10.34.5 and 11.23.0.
Coverage includes real-pnpm tests at three levels: the prune succeeding on a
patched partial-workspace bundle, the YAML edit reproducing pnpm's own bytes,
and an end-to-end case driving
Bundlerover a committed fixture and assertingthe produced tarball drops the unused patch file, its declaration and its
lockfile entry while keeping the still-applied one. Every fail-safe branch is
mutation-tested. Full unit suite: 2269 passing.
No new dependencies.
Not marked
[ship]— release timing is yours to call.