Skip to content

RFC: Native Dependency Patching#862

Open
manzoorwanijk wants to merge 1 commit into
npm:mainfrom
manzoorwanijk:rfc-native-dependency-patching
Open

RFC: Native Dependency Patching#862
manzoorwanijk wants to merge 1 commit into
npm:mainfrom
manzoorwanijk:rfc-native-dependency-patching

Conversation

@manzoorwanijk
Copy link
Copy Markdown

Summary

Adds first-class, install-time patching of installed dependencies to the npm CLI, on parity with pnpm patch, yarn patch, and bun patch. Introduces npm patch / npm patch ls / npm patch-commit / npm patch-remove, a patchedDependencies field in package.json, and a patched.{path,integrity} record in package-lock.json (lockfileVersion: 4). Patches apply during Arborist's reify step, uniformly across every supported install-strategy (hoisted, nested, shallow, linked).

Why now

The third-party patch-package is currently the only path to dependency patching for npm users, and it is structurally limited:

  • Silently disabled by --ignore-scripts. patch-package runs as a postinstall script. In environments that disable lifecycle scripts — increasingly common in hardened CI and after recent supply-chain incidents like the Shai-Hulud worm (Sept/Nov 2025) — declared patches simply do not apply, with no error and no warning. Production code can be installed missing fixes that are committed in the project.
  • Broken with workspaces (ds300/patch-package#277).
  • Broken with install-strategy=linked (ds300/patch-package#595).
  • Unmaintained.

The headline outcome: reproducible, source-controlled dependency hotfixes that survive --ignore-scripts and work across every npm install strategy and across workspaces.

Relationship to #94

This RFC is a direct response to #94 (closed in 2020 as a footgun). The 2020 proposal was an ad-hoc npm install --patch foo.patch flag with no manifest record, no lockfile linkage, no transitive-dep support, and no failure-mode story; @isaacs's footgun objection was correct for that shape. This RFC is structured the opposite way — explicit manifest, lockfile-hashed, fail-loud-by-default, version-gated, publish-isolated. A row-by-row response is in the RFC's Prior Art → #94 section.

See the RFC for the full design, alternatives considered, implementation plan, tests, and unresolved questions.


Disclosure: Claude Code was used to draft this PR description and the initial version of this RFC and to iterate on it during review.

@manzoorwanijk manzoorwanijk requested a review from a team as a code owner May 3, 2026 14:44
@james-pre
Copy link
Copy Markdown

james-pre commented May 11, 2026

Hey @manzoorwanijk,

There are a few questions I have:

  1. How are multiple patches for a single package handled? Let's say I depend on duck-quack-js and want to apply a patch to add a new feature from a PR and another patch to fix a security issue.

  2. How are patches required for dependents handled? Take for example this dependency chain:

  • @ducks/quack patches duck-quack-js to add a feature PR that is required at runtime
  • @ducks/mallard depends on @ducks/quack. npm install @ducks/quack needs to result in the patch being applied otherwise we get runtime errors.
  1. Why not wrap everything under a single npm patch command (npm patch add, ... ls, ... rm, ... commit)?

@manzoorwanijk
Copy link
Copy Markdown
Author

Thanks @james-pre - three good questions.

1. Multiple patches for the same package.

Punted in v1: see Unresolved Questions and Bikeshedding item 6 ("Stacking patches"). The RFC matches at most one patch per resolved node, and stacking is left as an additive extension (selector value becomes an array). The reason for deferring is that with fuzz=0 (also v1 default), stack order is load-bearing and composition rules need their own design pass - does order 1 → 2 produce the same tree as 2 → 1? what does npm patch-remove do with a stack? etc.

Practical workaround for now: merge the two diffs into a single .patch file (cat a.patch b.patch | <git-apply-style-merge>, or hand-merge). Not pretty, but unblocks the case until the stacking design lands.

2. Patches needed by dependants / transitive consumers.

This is intentionally not supported, and the constraint is by design rather than oversight. From the RFC:

  • patchedDependencies is honoured only in the root package.json of the consuming project.
  • npm publish / npm pack strip patchedDependencies from the published manifest and exclude the <patches-dir>/ from the tarball.

So in your example, if @ducks/quack publishes a patchedDependencies entry for duck-quack-js, it never travels through the registry, and consumers of @ducks/mallard will not have the patch applied. This is deliberate: a published package being able to silently mutate its consumers' transitive dependency trees is a supply-chain abuse vector we shouldn't introduce. (It would be a strictly bigger lever than postinstall scripts - applied silently, inside the install pipeline, with no --ignore-scripts-style escape hatch.)

The right tool for "I want consumers of @ducks/quack to get a patched duck-quack-js" is Alternative 2 in the RFC: @ducks/quack declares a normal dependency on a published @ducks/duck-quack-js fork (or uses an overrides entry that the consumer also opts into in their root). That makes the patched code a real registry artifact with a normal version, auditable in the lockfile.

3. Why not consolidate under npm patch <subcommand>?

Fair point - the current mix (npm patch, npm patch-commit, npm patch-remove, npm patch ls) is inconsistent, and npm patch add/ls/rm/commit would be more uniform and more idiomatic for npm (compare npm pkg get/set/delete/fix, npm cache add/clean/verify/ls, npm team create/destroy/add/rm/ls). I followed pnpm/yarn naming originally for prior-art familiarity, but you're right that npm's own convention is the better fit.

I'd propose:

  • npm patch <pkg> - shorthand for npm patch add <pkg> (the most common entry point, kept short).
  • npm patch add <pkg> - start an edit session.
  • npm patch commit <edit-dir> - finalise (no dash; aligns with npm pkg set, etc).
  • npm patch ls - list registered patches.
  • npm patch rm <pkg>[@<version>] - remove a patch.

Happy to update the RFC to use this shape if there's broader agreement.

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.

2 participants