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

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.

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