Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
989562e
feat(arborist): apply patchedDependencies during reify
manzoorwanijk May 29, 2026
b15ca4e
feat(config): add patches-dir and patch relax flags
manzoorwanijk May 29, 2026
8cf21e5
feat(arborist): re-extract on patch change and validate patch hash in…
manzoorwanijk May 29, 2026
cea9cad
feat(patch): add npm patch command (add/commit/ls/rm)
manzoorwanijk May 29, 2026
160592c
test(arborist): unit tests for patch apply and selector matching
manzoorwanijk May 29, 2026
c498041
fix(arborist): clear stale patch records when a selector is removed
manzoorwanijk May 29, 2026
ceaa9d4
test(patch): integration tests for command, reify apply, and selectors
manzoorwanijk May 29, 2026
8105b4a
fix(patch): harden apply pipeline and tighten selector handling
manzoorwanijk May 29, 2026
84366f4
fix(patch): contain patches-dir writes, reject non-registry version m…
manzoorwanijk May 29, 2026
d277ccb
fix(patch): clear node.patched on ignored failure, contain rm deletes…
manzoorwanijk May 29, 2026
e756a49
test(patch): make full arborist suite pass at 100% coverage
manzoorwanijk May 29, 2026
af41b8a
fix(arborist): revalidate patch file existence and integrity in reify
manzoorwanijk May 29, 2026
bd1700f
fix(arborist): fail loudly on optional patch errors and reject patche…
manzoorwanijk May 29, 2026
de2ca7e
fix(arborist): seal linked-strategy patch guard at reify and re-code …
manzoorwanijk May 29, 2026
8398bcc
feat(publish): strip patchedDependencies from the published registry …
manzoorwanijk May 29, 2026
2ab1bd8
feat(ls): annotate patched dependencies in npm ls output
manzoorwanijk May 29, 2026
ab167cf
feat(patch): enforce allow-unused-patches and ignore-patch-failures a…
manzoorwanijk May 29, 2026
76bcd93
feat(arborist): apply patches under install-strategy=linked via a con…
manzoorwanijk May 30, 2026
105dbe1
fix(patch): honor relax flags across all reify commands, fail loudly …
manzoorwanijk May 30, 2026
55db1e8
refactor(patch): drop unused diffDirs exclude option and dedupe the p…
manzoorwanijk May 30, 2026
e48da1d
fix(arborist): re-extract a dependency when its patch is removed so t…
manzoorwanijk May 30, 2026
5545c39
test(arborist): cover patch removal under install-strategy=linked
manzoorwanijk May 30, 2026
947ef57
fix(patch): patch a registry dep even when a consumer node is edgeles…
manzoorwanijk May 30, 2026
329048e
feat(arborist): warn when patchedDependencies upgrades the lockfile t…
manzoorwanijk May 30, 2026
9b67b22
fix(arborist): use the edge-based registry check on the install path …
manzoorwanijk May 30, 2026
4ccd1ad
test(smoke): add patch to the no-args command-list snapshot
manzoorwanijk May 30, 2026
53f2c95
feat(patch): exclude package.json from the diff and warn when an edit…
manzoorwanijk Jun 4, 2026
26264da
feat(patch): add npm patch update to rebase a patch onto a new version
manzoorwanijk Jun 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ graph LR;
npmcli-arborist-->bin-links;
npmcli-arborist-->cacache;
npmcli-arborist-->common-ancestor-path;
npmcli-arborist-->diff;
npmcli-arborist-->gar-promise-retry["@gar/promise-retry"];
npmcli-arborist-->hosted-git-info;
npmcli-arborist-->isaacs-string-locale-compare["@isaacs/string-locale-compare"];
Expand Down
69 changes: 69 additions & 0 deletions docs/lib/content/commands/npm-patch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
title: npm-patch
section: 1
description: Apply local patches to installed dependencies
---

### Synopsis

<!-- AUTOGENERATED USAGE DESCRIPTIONS -->

### Description

`npm patch` lets you apply small, local modifications to an installed
dependency and have them re-applied automatically on every install. Patches
are declared in the `patchedDependencies` field of your root `package.json`,
stored as plain unified diffs under the `patches/` directory, and recorded with
a content hash in `package-lock.json`.

Because patches are applied during the install itself, they work regardless of
`install-strategy`, apply to transitive dependencies, and are **not** disabled
by `--ignore-scripts`.

The bare form `npm patch <pkg>` is shorthand for `npm patch add <pkg>`. A
package literally named like a subcommand must use the explicit form, e.g.
`npm patch add add`.

* `npm patch add <pkg>[@<version>]`

Prepares a package for editing. npm extracts a clean copy of the resolved
package tarball into a temporary directory outside `node_modules` and prints
its path. Edit the files there, then run `npm patch commit`.

If more than one version of `<pkg>` is installed, re-run with an exact
selector such as `npm patch add lodash@4.17.21`.

* `npm patch commit <edit-dir>`

Diffs the edited directory against a clean copy of the original tarball,
writes the unified diff to `<patches-dir>/<name>@<version>.patch`, adds the
entry to `patchedDependencies`, and updates `package-lock.json`.

* `npm patch ls`

Lists registered patches and how many installed nodes each one matches.

* `npm patch rm <pkg>[@<version>]`

Removes the matching entries from `patchedDependencies`, deletes the patch
file when no other entry references it, and updates `package-lock.json`. If
`<version>` is omitted, all entries for `<pkg>` are removed.

### 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 flags relax this for one-off cases: `--allow-unused-patches` and
`--ignore-patch-failures`.

### Configuration

<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->
## See Also

* [npm install](/commands/npm-install)
* [npm ci](/commands/npm-ci)
* [package-lock.json](/configuring-npm/package-lock-json)
* [config](/commands/npm-config)
3 changes: 3 additions & 0 deletions docs/lib/content/nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@
- title: npm pack
url: /commands/npm-pack
description: Create a tarball from a package
- title: npm patch
url: /commands/npm-patch
description: Apply local patches to installed dependencies
- title: npm ping
url: /commands/npm-ping
description: Ping npm registry
Expand Down
3 changes: 3 additions & 0 deletions lib/commands/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const auditError = require('../utils/audit-error.js')
const { log, output } = require('proc-log')
const reifyFinish = require('../utils/reify-finish.js')
const resolveAllowScripts = require('../utils/resolve-allow-scripts.js')
const { patchRelaxOpts } = require('../utils/cli-only-flag.js')
const VerifySignatures = require('../utils/verify-signatures.js')

class Audit extends ArboristWorkspaceCmd {
Expand Down Expand Up @@ -62,6 +63,8 @@ class Audit extends ArboristWorkspaceCmd {
const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm)
const opts = {
...this.npm.flatOptions,
// audit fix reifies, so honor the cli-only patch relax flags
...patchRelaxOpts(this.npm.config),
audit: true,
path: this.npm.prefix,
reporter,
Expand Down
9 changes: 9 additions & 0 deletions lib/commands/ci.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ class CI extends ArboristWorkspaceCmd {
})
}

// npm ci is always strict about patches; the relax flags are not accepted
for (const flag of ['allow-unused-patches', 'ignore-patch-failures']) {
if (this.npm.config.find(flag) === 'cli') {
throw Object.assign(new Error(`The --${flag} flag is not allowed with \`npm ci\`.`), {
code: 'ECIPATCHFLAG',
})
}
}

const dryRun = this.npm.config.get('dry-run')
const ignoreScripts = this.npm.config.get('ignore-scripts')
const where = this.npm.prefix
Expand Down
2 changes: 2 additions & 0 deletions lib/commands/dedupe.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const reifyFinish = require('../utils/reify-finish.js')
const resolveAllowScripts = require('../utils/resolve-allow-scripts.js')
const { patchRelaxOpts } = require('../utils/cli-only-flag.js')
const ArboristWorkspaceCmd = require('../arborist-cmd.js')

// dedupe duplicated packages, or find them in the tree
Expand Down Expand Up @@ -47,6 +48,7 @@ class Dedupe extends ArboristWorkspaceCmd {
save: false,
workspaces: this.workspaceNames,
allowScripts: allowScriptsPolicy,
...patchRelaxOpts(this.npm.config),
}
const arb = new Arborist(opts)
await arb.dedupe(opts)
Expand Down
3 changes: 3 additions & 0 deletions lib/commands/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const checks = require('npm-install-checks')
const reifyFinish = require('../utils/reify-finish.js')
const resolveAllowScripts = require('../utils/resolve-allow-scripts.js')
const strictAllowScriptsPreflight = require('../utils/strict-allow-scripts-preflight.js')
const { patchRelaxOpts } = require('../utils/cli-only-flag.js')
const ArboristWorkspaceCmd = require('../arborist-cmd.js')

class Install extends ArboristWorkspaceCmd {
Expand Down Expand Up @@ -151,6 +152,8 @@ class Install extends ArboristWorkspaceCmd {
add: args,
workspaces: this.workspaceNames,
allowScripts: allowScriptsPolicy,
// patch relax flags are honored only when passed on the command line
...patchRelaxOpts(this.npm.config),
}

// Root lifecycle scripts only run for a bare `npm install` in a local project. `preinstall` runs *before* Arborist touches the filesystem so that scripts can bootstrap the environment (e.g. set up private-registry auth, generate files consumed during resolution) before dependencies are fetched or unpacked. The remaining scripts run after reify as they did before.
Expand Down
4 changes: 4 additions & 0 deletions lib/commands/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const pkgJson = require('@npmcli/package-json')
const semver = require('semver')
const reifyFinish = require('../utils/reify-finish.js')
const resolveAllowScripts = require('../utils/resolve-allow-scripts.js')
const { patchRelaxOpts } = require('../utils/cli-only-flag.js')
const ArboristWorkspaceCmd = require('../arborist-cmd.js')

class Link extends ArboristWorkspaceCmd {
Expand Down Expand Up @@ -70,6 +71,7 @@ class Link extends ArboristWorkspaceCmd {
const Arborist = require('@npmcli/arborist')
const globalOpts = {
...this.npm.flatOptions,
...patchRelaxOpts(this.npm.config),
Arborist,
path: globalTop,
global: true,
Expand Down Expand Up @@ -119,6 +121,7 @@ class Link extends ArboristWorkspaceCmd {
const { policy: allowScriptsPolicy } = await resolveAllowScripts(this.npm)
const localArb = new Arborist({
...this.npm.flatOptions,
...patchRelaxOpts(this.npm.config),
prune: false,
path: this.npm.prefix,
save,
Expand All @@ -145,6 +148,7 @@ class Link extends ArboristWorkspaceCmd {
const Arborist = require('@npmcli/arborist')
const arb = new Arborist({
...this.npm.flatOptions,
...patchRelaxOpts(this.npm.config),
Arborist,
path: globalTop,
global: true,
Expand Down
9 changes: 9 additions & 0 deletions lib/commands/ls.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,11 @@ const getHumanOutputItem = (node, { args, chalk, global, long }) => {
? ' ' + chalk.dim('overridden')
: ''
) +
(
node.patched
? ' ' + chalk.cyan(`[patched: ${node.patched.path}]`)
: ''
) +
(isGitNode(node) ? ` (${node.resolved})` : '') +
(node.isLink ? ` -> ${relativePrefix}${targetLocation}` : '') +
(long ? `\n${node.package.description || ''}` : '')
Expand Down Expand Up @@ -389,6 +394,10 @@ const getJsonOutputItem = (node, { global, long }) => {
item.invalid = node[_invalid]
}

if (node.patched) {
item.patched = node.patched.path
}

if (node[_missing] && !isOptional(node)) {
item.required = node[_required]
item.missing = true
Expand Down
Loading
Loading