RFC: Native Dependency Patching#862
Conversation
|
Hey @manzoorwanijk, There are a few questions I have:
|
|
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 Practical workaround for now: merge the two diffs into a single 2. Patches needed by dependants / transitive consumers. This is intentionally not supported, and the constraint is by design rather than oversight. From the RFC:
So in your example, if The right tool for "I want consumers of 3. Why not consolidate under Fair point - the current mix ( I'd propose:
Happy to update the RFC to use this shape if there's broader agreement. |
|
The rename is done — |
|
@owlstronaut, while this RFC is still in review, is it fine to create a draft PR of the implementation in the CLI repo for demonstration? |
Absolutely. Love the idea |
…atch-failure carve-out, lockfile-version pin upgrade)
|
Here is the first PR (draft) npm/cli#9439. The PR is feature complete, and the only part left is to support patching for aliases via |
|
Under this proposal, what would be the practical workflow for updating an existing patch that still applies to a new version? With patch-package, it still attempts to apply patches to newer versions (but prints a warning), so you just run With the proposed native patching, I think you'd have to do something like this: npm install <pkg>@new-version --allow-unused-patches
npm patch add <pkg>@new-version
pushd /path/printed/by/npm/patch/add
git apply --ignore-whitespace /path/to/project/patches/<pkg>@old-version.patch
popd
npm patch commit /path/printed/by/npm/patch/add
npm patch rm <pkg>@old-version...and then commit the changes. It would be great if there was a better way like an explicit |
|
|
Added |
…lent partial application
… floor, and align patch-update prose with the implementation
#291) Part of native dependency patching ([npm/rfcs#862](npm/rfcs#862)). This force-excludes the patch files declared in the root package's `patchedDependencies` from the packed file list, even when they are listed in `files`. ## Why `patchedDependencies` maps a dependency selector to a project-local patch file (e.g. `"abbrev@2.0.0": "patches/abbrev@2.0.0.patch"`). Those patches are a property of the project, not something a consumer of the published package applies. Without this, publishing a patched project would ship the patch files — and, once pacote strips the `patchedDependencies` field from the tarball's `package.json`, they would be dangling, unreferenced files. Excluding them keeps the published tarball clean. ## How In `PackWalker.processPackage`, when the walker is the project root and the manifest declares `patchedDependencies`, each declared patch file path is pushed onto the strict (un-overridable) rule set, so it is excluded even if `files` lists it. Design choices: - **Exact files, not directories.** Only the declared patch files are excluded — never their directory. A dedicated `patches/` dir becomes empty and drops out naturally, but a patch that lives in a shared directory (e.g. `src/foo.patch`) does not take the rest of `src/` down with it. - **`--patches-dir` honored for free.** The location is read straight off the `patchedDependencies` values, which already encode wherever the patches were written. - **Root-only.** `patchedDependencies` is root-only state, so the block is gated to the project root and never prunes a bundled dependency's files. - **Path safety.** Absolute paths and paths that escape the package root are skipped (they are never packed anyway). - **Warns** when a `files` entry pulled a patch file in (directly or via its directory), so the override is not silent. ## References Part of - npm/rfcs#862 Related to - npm/cli#9439 - npm/pacote#497
Part of native dependency patching ([npm/rfcs#862](npm/rfcs#862)). When packing a `directory` spec (the `npm publish` / `npm pack` path), this strips a top-level `patchedDependencies` field from the `package.json` written **into the tarball**. ## Why `patchedDependencies` declares project-local patches against installed dependencies. It is honored only in a root manifest, so it is meaningless to consumers of a published package and should never travel through the registry. The published *packument* manifest is already stripped in `libnpmpublish`; this closes the other half — the `package.json` inside the tarball itself — so `npm pack --dry-run` and the published tarball no longer carry the field. It pairs with the npm-packlist change that excludes the patch files themselves; together they guarantee a patched project publishes clean. ## How `DirFetcher` packs the raw on-disk files via `tar.c`, so the tarball's `package.json` is the literal file on disk — there is no manifest seam to edit. The new `#tarOptions()`: 1. Reads the on-disk `package.json` (after `prepare`) via `@npmcli/package-json`. If it has no `patchedDependencies`, returns the existing options unchanged — **non-patched packs are byte-for-byte identical to before**. 2. Otherwise deletes the field and re-serializes preserving the original indent, newline, and key order (the indent/newline symbols `@npmcli/package-json` attaches; `JSON.stringify` ignores them), writes the stripped copy to a temp dir, and removes the temp dir if the write fails. 3. Sets node-tar's `onWriteEntry` to redirect **only** the top-level `package.json` entry's `absolute` at the stripped copy and fix its `stat.size`/`nlink`. `onWriteEntry` runs before the header and the file's hardlink check, so the override is honored; every other file is untouched. 4. The temp dir is removed once the tar source stream emits `end`/`error`, so it outlives content consumption. No behavior change for any package without `patchedDependencies`. ## References Part of - npm/rfcs#862 Related to - npm/cli#9439 - npm/npm-packlist#291
Summary
Adds first-class, install-time patching of installed dependencies to the npm CLI, on parity with
pnpm patch,yarn patch, andbun patch. Introduces a newnpm patchcommand with subcommandsadd/commit/update/ls/rm(withnpm patch <pkg>as shorthand fornpm patch add <pkg>), apatchedDependenciesfield inpackage.json, and apatched.{path,integrity}record inpackage-lock.json(lockfileVersion: 4). Patches apply during Arborist's reify step, uniformly across every supportedinstall-strategy(hoisted,nested,shallow,linked).Why now
The third-party
patch-packageis currently the only path to dependency patching for npm users, and it is structurally limited:--ignore-scripts.patch-packageruns as apostinstallscript. In environments that disable lifecycle scripts — increasingly common in hardened CI and after recent supply-chain incidents like theShai-Huludworm (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.install-strategy=linked(ds300/patch-package#595).The headline outcome: reproducible, source-controlled dependency hotfixes that survive
--ignore-scriptsand 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.patchflag 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.