feat(patch): native dependency patching (npm patch add/commit/ls/rm)#9439
Draft
manzoorwanijk wants to merge 26 commits into
Draft
feat(patch): native dependency patching (npm patch add/commit/ls/rm)#9439manzoorwanijk wants to merge 26 commits into
manzoorwanijk wants to merge 26 commits into
Conversation
…atches, surface ls ambiguity
…, scope ls ambiguity to ranges
…s under install-strategy=linked
…raw patch FS errors
…s cli-only and reject them in npm ci
…tent-addressed side-store
…on skipped linked patches, and exclude store nodes from the registry check
…roject-root containment check
…he patched files are reverted
…s (linked store, extraneous)
…too and cover the mixed registry/file: case
This was referenced May 30, 2026
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.
Implements native dependency patching per RFC #862: a first-class way to apply small, local modifications to an installed dependency and have them re-applied automatically on every install, with no external tooling or postinstall scripts.
Patches are declared in a new
patchedDependenciesfield of the rootpackage.json, stored as plain unified diffs underpatches/, and recorded with a content hash inpackage-lock.json. Because the patch is applied during the install itself, it works for transitive dependencies, across everyinstall-strategy, and is not disabled by--ignore-scripts.The
npm patchcommandA new command with four subcommands (and a bare
npm patch <pkg>shorthand foradd):npm patch add <pkg>[@<version>]— extracts a clean copy of the resolved registry tarball into a temp directory outsidenode_modulesand prints the path to edit. Ambiguous when multiple versions are installed; the error lists the exact selectors to retry with.npm patch commit <edit-dir>— diffs the edited directory against a fresh copy of the original tarball, writes<patches-dir>/<name>@<version>.patch, adds thepatchedDependenciesentry, and reifies to apply the patch and record its integrity in the lockfile.npm patch ls— lists registered patches and how many installed nodes each matches (flagging overlapping range selectors that conflict on a node).npm patch rm <pkg>[@<version>]— removes the matching entries, deletes the patch file when no other entry references it, and reifies to revert the files.Install-time apply pipeline
Patch resolution and application live in Arborist so every install path honors them:
resolvePatchedDependenciesresolves the rootpatchedDependenciesmap against the ideal tree, attachingnode.patched = { path, integrity }to each matched node. Selector precedence is exact > range-subset > name-only, with ambiguous overlapping ranges surfaced as a hard error.diff.jsforces re-extraction when a node's patch integrity changes, and re-extracts to revert when a previously-patched node loses its selector (patchRemoved).install-strategy=linkedis supported via a content-addressed side-store: the store key is suffixed with the patch identity (+patch) so a patched and unpatched copy of the same version coexist without collision. A failed patch under linked strategy is always a hard error (the side-store cannot represent unpatched contents at a patched key without later installs silently trusting it).Lockfile
Patches require
lockfileVersion: 4so that older npm clients abort rather than silently installing unpatched code. When any node is patched, npm writes version 4 and warns if this upgrades a lower pinnedlockfile-version(the safety gate cannot be honored otherwise).npm cirevalidates each patch's existence and integrity against the lockfile before installing.Failure modes
By default any patch problem is a hard error that aborts the install: a patch that fails to apply, a registered patch that matches no installed package, a missing patch file, or a patch whose hash does not match the lockfile. Two CLI-only relax flags cover one-off cases —
--allow-unused-patchesand--ignore-patch-failures— and are rejected innpm ciand when set anywhere other than the command line.Non-registry dependencies
Patches need a stable registry tarball as their baseline, so a dependency reached through a non-registry consumer edge (
file:,git:,http(s):) is rejected withEPATCHNONREGISTRY, both bynpm patch addand at install time. The check is edge-based (the consuming spec's type), not node-based, so it does not falsely reject edgeless nodes such as linked-store entries or extraneous installs, which are still registry deps.npm:registry aliases are correctly classified as registry deps and are supported by the install engine; thenpm patch add <alias>ergonomics will land in a fast-follow.Publish / pack
patchedDependenciesis stripped from the published registry manifest so the field never leaks to consumers of the package.Other surfaces
npm lsannotates patched dependencies in its output.patches-dir,edit-dir,ignore-existing,keep-edit-dir, plus the two relax flags.npm-patchman page and nav entry.Tests
Unit and integration coverage for the command, the apply pipeline, selector matching, linked-strategy apply/removal, lockfile validation, publish stripping, and the relax flags. Arborist and CLI suites pass at 100% coverage.
References
Implements npm/rfcs#862