feat: packageExtensions for root-owned dependency manifest repairs (#9496)
Implements package manifest extensions per [RFC
#889](https://github.com/npm/rfcs/pull/889): a root-only
`packageExtensions` field in `package.json` that applies declarative
repairs to third-party dependency manifests **before** Arborist
finalizes the ideal tree. It lets a project add missing
`dependencies`/`optionalDependencies`, add or correct
`peerDependencies`, and mark peers optional via `peerDependenciesMeta`,
without forking and republishing a package.
```json
{
"packageExtensions": {
"broken-package@1": {
"dependencies": { "missing-runtime-dep": "^2.0.0" }
},
"typescript-plugin@4.3.0": {
"peerDependencies": { "typescript": ">=5" },
"peerDependenciesMeta": { "typescript": { "optional": true } }
}
}
}
```
## Why
`install-strategy=linked` gives installs strong package boundaries,
which is also what makes adoption hard: a package only sees what it
actually declared, so one that worked under a hoisted layout because a
dependency happened to be hoisted above it can fail. A root-level
dependency masks this under hoisting but does not make the package
available inside the isolated boundary of the importer — the repair has
to be attached to the broken package's manifest before its edges are
resolved. This is the pre-resolution complement to `overrides` (which
needs an existing edge to retarget) and to [native dependency patching
#9439](https://github.com/npm/cli/pull/9439) (which edits package
contents after resolution).
## The field
Each key is a package selector: a name with an optional semver range
(`foo`, `foo@1`, `@scope/foo@^2.3.0`). Selectors match a candidate's own
manifest `name`/`version` (the underlying name for aliases) and reject
dist-tag, git, file, URL, and `npm:` specs. At most one selector may
match a candidate. Honored only in the root `package.json` (the
workspace root); the field in dependencies and non-root workspaces, and
selectors matching a workspace member, are ignored with a warning —
matching the root-authority model of `overrides`.
## Merge semantics
Only the four resolution-affecting fields may be extended.
- `dependencies`/`optionalDependencies` add a missing name only;
providing a name already declared in either field is an error (use
`overrides` to change a version), which also forbids moving a name
between the two.
- `peerDependencies` shallow-merges by name, replacing an existing
range.
- `peerDependenciesMeta` merges by name then key (e.g. add `optional:
true`); every meta entry must have a corresponding `peerDependencies`
entry.
- Deletion (`null`/`false`/`"-"`) is not supported.
The extension applies to a per-tree manifest copy: the shared
pacote/cache manifest is never mutated, the installed
`node_modules/<pkg>/package.json` is not rewritten, and
`bundleDependencies` is unchanged. `overrides` still controls the final
resolution target of an extension-created edge.
## Lockfile
The root entry stores a canonical `packageExtensionsHash`, and each
affected entry stores minimal provenance (`packageExtensionsApplied`);
effective dependency metadata is recorded as usual. Extension state
forces `lockfileVersion: 4` so older npm clients abort rather than
silently dropping the repaired graph. `npm install` re-resolves affected
packages when the rule set changes; `npm ci` validates the hash,
selector conflicts, and stale provenance before trusting the locked
metadata.
## Visibility
`npm explain` appends `(added by
packageExtensions["foo@1"].dependencies.bar)` to the edge; `npm ls`
annotates the node and `npm ls --json` includes
`packageExtensionsApplied`. Publishing a non-private package containing
the field warns that it does not affect consumers.
## Notes
- `lockfileVersion: 4` is shared with native dependency patching
([#9439](https://github.com/npm/cli/pull/9439)) as a common "older npm
must not silently drop this" tripwire; both bump only when their own
state is present. Whichever lands second should reuse the same
`maxLockfileVersion`/bump constants rather than introduce a competing
version.
- Opt-in and additive, so it can ship in a minor release.
## References
Implements https://github.com/npm/rfcs/pull/889 M
Manzoor Wani committed
ce7681fe7dbcc20abb5f1379558e14ddd069654f
Parent: 5ddf6cc
Committed by GitHub <noreply@github.com>
on 6/18/2026, 5:54:56 PM