You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: packageExtensions for root-owned dependency manifest repairs (#9496)
Implements package manifest extensions per [RFC
#889](npm/rfcs#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](#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](#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 npm/rfcs#889
Copy file name to clipboardExpand all lines: docs/lib/content/configuring-npm/package-json.md
+60Lines changed: 60 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1033,6 +1033,66 @@ For example, to replace a transitive dependency with a fork:
1033
1033
}
1034
1034
```
1035
1035
1036
+
### packageExtensions
1037
+
1038
+
`packageExtensions` lets a project apply small, declarative repairs to the manifests of third-party dependencies before npm resolves the dependency tree.
1039
+
Use it to add a missing `dependencies`, `optionalDependencies`, or `peerDependencies` entry, or to correct `peerDependencies` and `peerDependenciesMeta`, while you wait for the upstream package to publish a fix.
1040
+
1041
+
This is especially useful with [`install-strategy=linked`](/using-npm/config#install-strategy), where dependencies are fully isolated and a package only sees what it actually declared.
1042
+
A package that worked under a hoisted layout because a dependency happened to be hoisted above it can fail under `linked`; `packageExtensions` records the missing edge as explicit, reviewable, root-owned policy.
1043
+
1044
+
`packageExtensions` complements [`overrides`](#overrides): `overrides` changes what an existing dependency edge resolves to, while `packageExtensions` adds or corrects the dependency metadata that creates the edge in the first place.
1045
+
For changing the resolved version of a dependency that is already declared, use `overrides`.
1046
+
1047
+
Like `overrides`, `packageExtensions` is only honored in the root `package.json` of a project (the workspace root in a workspace).
1048
+
The field in installed dependencies and in non-root workspace packages is ignored.
1049
+
Because it is root-only project policy, npm refuses to publish a non-private package that contains `packageExtensions`; it remains available to private packages and unpublished local projects.
1050
+
1051
+
Each key is a package selector: a package name with an optional semver range.
Selectors match a candidate package's own `name` and `version`. They do not accept dist-tags, git, file, directory, URL, or `npm:`alias specs. For aliases, the selector matches the underlying package name. At most one selector may match a given package; overlapping selectors that both match the same package fail the install.
1085
+
1086
+
Only `dependencies`, `optionalDependencies`, `peerDependencies`, and `peerDependenciesMeta` may be extended. The merge rules are:
1087
+
1088
+
- `dependencies` and `optionalDependencies` entries add a missing dependency only. Adding a name that the package already declares in either field is an error; use `overrides` to change a version.
1089
+
- `peerDependencies` entries are merged by name, replacing an existing range.
1090
+
- `peerDependenciesMeta` entries are merged by name and then by key, so you can add `optional: true` without dropping other metadata. Every `peerDependenciesMeta` entry must correspond to a `peerDependencies` entry.
1091
+
1092
+
Deletion is not supported; a `null`, `false`, or `"-"` value is an error.
1093
+
1094
+
`packageExtensions` does not rewrite the installed package's `package.json` on disk and does not modify `bundleDependencies`. Affected packages are recorded in `package-lock.json` and surfaced by [`npm explain`](/commands/npm-explain) and [`npm ls`](/commands/npm-ls), so each repair is easy to audit and to remove once upstream is fixed.
1095
+
1036
1096
### engines
1037
1097
1038
1098
You can specify the version of node that your stuff works on:
Copy file name to clipboardExpand all lines: lib/commands/publish.js
+15Lines changed: 15 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -96,6 +96,9 @@ class Publish extends BaseCommand {
96
96
constspec=npa(args[0])
97
97
letmanifest=awaitthis.#getManifest(spec,opts)
98
98
99
+
// packageExtensions is root-only project policy and must never be published; fail fast so dry-run reports it too
100
+
this.#assertNoPackageExtensions(manifest)
101
+
99
102
// only run scripts for directory type publishes
100
103
if(spec.type==='directory'&&!ignoreScripts){
101
104
awaitrunScript({
@@ -122,6 +125,8 @@ class Publish extends BaseCommand {
122
125
123
126
// The purpose of re-reading the manifest is in case it changed, so that we send the latest and greatest thing to the registry note that publishConfig might have changed as well!
124
127
manifest=awaitthis.#getManifest(spec,opts,true)
128
+
// re-check the authoritative manifest in case a lifecycle script introduced packageExtensions
0 commit comments