Skip to content

feat(patch): native dependency patching (npm patch add/commit/ls/rm)#9439

Draft
manzoorwanijk wants to merge 26 commits into
npm:latestfrom
manzoorwanijk:feat/native-dependency-patching
Draft

feat(patch): native dependency patching (npm patch add/commit/ls/rm)#9439
manzoorwanijk wants to merge 26 commits into
npm:latestfrom
manzoorwanijk:feat/native-dependency-patching

Conversation

@manzoorwanijk
Copy link
Copy Markdown
Contributor

@manzoorwanijk manzoorwanijk commented May 30, 2026

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 patchedDependencies field of the root package.json, stored as plain unified diffs under patches/, and recorded with a content hash in package-lock.json. Because the patch is applied during the install itself, it works for transitive dependencies, across every install-strategy, and is not disabled by --ignore-scripts.

The npm patch command

A new command with four subcommands (and a bare npm patch <pkg> shorthand for add):

  • npm patch add <pkg>[@<version>] — extracts a clean copy of the resolved registry tarball into a temp directory outside node_modules and 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 the patchedDependencies entry, 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:

  • resolvePatchedDependencies resolves the root patchedDependencies map against the ideal tree, attaching node.patched = { path, integrity } to each matched node. Selector precedence is exact > range-subset > name-only, with ambiguous overlapping ranges surfaced as a hard error.
  • reify applies the diff after extraction and records the patched integrity in the lockfile. diff.js forces 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=linked is 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: 4 so 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 pinned lockfile-version (the safety gate cannot be honored otherwise). npm ci revalidates 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-patches and --ignore-patch-failures — and are rejected in npm ci and 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 with EPATCHNONREGISTRY, both by npm patch add and 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; the npm patch add <alias> ergonomics will land in a fast-follow.

Publish / pack

patchedDependencies is stripped from the published registry manifest so the field never leaks to consumers of the package.

Other surfaces

  • npm ls annotates patched dependencies in its output.
  • New config: patches-dir, edit-dir, ignore-existing, keep-edit-dir, plus the two relax flags.
  • New npm-patch man 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.

Follow-up work

To keep this PR scoped to the patching engine and the core add/commit/ls/rm commands, two pieces of the RFC are deliberately deferred to focused follow-ups. Both are purely additive ergonomics — nothing in this PR depends on them, and the capabilities they wrap are already achievable with the commands shipped here.

  • npm patch update — rebases an existing patch onto a new version of a package (bump the dep, run npm patch update). It introduces machinery not present anywhere in this PR: a lockfile-only edit path (every mutation here goes through Arborist reify, whereas update must rewrite package-lock.json without touching node_modules), a 3-way merge (git apply --3way) with a conflict path finalized by npm patch commit, and an atomic manifest/patch-file/lockfile transaction. It also has a large independent test surface. Until it lands, the same result is reachable via npm patch rm <pkg>@<old>npm patch add <pkg>@<new> → re-apply edits → npm patch commit. Note: update is not yet in the command's subcommand list, so npm patch update currently routes to npm patch add update.

  • npm patch add <alias> ergonomics for npm: registry aliases — the install engine already treats npm: aliases as registry dependencies and applies a hand-written <alias>@<version> selector correctly today. What remains is the add/commit convenience: resolving the alias to its real name@version tarball as the baseline and keying the written selector on the alias name. Currently npm patch add <alias> resolves the alias name as a real package and fails.

References

Implements npm/rfcs#862

…on skipped linked patches, and exclude store nodes from the registry check
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant