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
Copy file name to clipboardExpand all lines: accepted/0000-package-manifest-extensions.md
+52-25Lines changed: 52 additions & 25 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -111,22 +111,25 @@ When a selector matches a package manifest, npm applies the extension to a per-i
111
111
For each supported object field:
112
112
113
113
1. If the field is missing from the package manifest, npm creates it.
114
-
2. For `dependencies`, `optionalDependencies`, and `peerDependencies`, npm shallow-merges entries by dependency name.
115
-
3. If the package already declares the same dependency or peer name in the same field, the extension value replaces the package's value in memory.
116
-
4. For `peerDependenciesMeta`, npm merges by peer name and then shallow-merges each peer metadata object, so an extension can add `optional: true` without replacing unrelated metadata keys for that peer.
114
+
2. For `dependencies` and `optionalDependencies`, npm adds entries by dependency name only when that package name is not already declared in either normal dependency field.
115
+
3. If a package already declares a package name in `dependencies` or `optionalDependencies`, an extension that provides that name in either normal dependency field is an error. Users should use `overrides` for normal dependency version changes.
116
+
4. For `peerDependencies`, npm shallow-merges entries by peer name, and the extension value replaces the package's peer range in memory when that peer name already exists.
117
+
5. For `peerDependenciesMeta`, npm merges by peer name and then shallow-merges each peer metadata object, so an extension can add `optional: true` without replacing unrelated metadata keys for that peer.
117
118
118
-
After extension application, npm validates and normalizes the manifest using the same rules it uses for published package manifests. An extension must not create a new duplicate package name across `dependencies` and `optionalDependencies`, and it must not try to move a package name between those two fields. If the package already declares `bar` in `optionalDependencies`, then an extension may replace `optionalDependencies.bar`but may not add `dependencies.bar`; if the package already declares `bar` in `dependencies`, then an extension may replace `dependencies.bar`but may not add `optionalDependencies.bar`. This keeps v1 from implicitly converting optional dependencies into required dependencies or required dependencies into optional dependencies without an explicit deletion feature.
119
+
After extension application, npm validates and normalizes the manifest using the same rules it uses for published package manifests. An extension must not create a new duplicate package name across `dependencies` and `optionalDependencies`, and it must not try to move a package name between those two fields. If the package already declares `bar` in `optionalDependencies`, then an extension may not provide either `dependencies.bar`or `optionalDependencies.bar`; if the package already declares `bar` in `dependencies`, then an extension may not provide either `optionalDependencies.bar`or `dependencies.bar`. This keeps v1 from implicitly converting optional dependencies into required dependencies or required dependencies into optional dependencies without an explicit deletion feature, while leaving normal dependency version changes to `overrides`.
119
120
120
-
`peerDependencies` may overlap with `dependencies` or `optionalDependencies`, because packages commonly provide a fallback implementation while also declaring a peer contract. An extension may add or correct a `peerDependencies` entry for a package that already lists the same name in `dependencies` or `optionalDependencies`, and it may add or correct a `dependencies` or `optionalDependencies` entry for a package that already lists the same name in `peerDependencies`.
121
+
`peerDependencies` may overlap with `dependencies` or `optionalDependencies`, because packages commonly provide a fallback implementation while also declaring a peer contract. An extension may add or correct a `peerDependencies` entry for a package that already lists the same name in `dependencies` or `optionalDependencies`, and it may add a `dependencies` or `optionalDependencies` entry for a package that already lists the same name in `peerDependencies` when that name is not already declared in either normal dependency field.
121
122
122
123
Every `peerDependenciesMeta` entry present after extension application must correspond to a `peerDependencies` entry present after extension application. An extension may add `peerDependenciesMeta.<name>.optional` only if the package already declares `peerDependencies.<name>` or the same extension also adds `peerDependencies.<name>`. Orphaned `peerDependenciesMeta` entries are an error in v1.
123
124
124
-
This permits both of the common manifest-repair cases:
125
+
This permits the common manifest-repair cases that are in scope for v1:
125
126
126
-
- add a missing dependency edge
127
-
- correct a dependency or peer range that is known to be wrong
127
+
- add a missing `dependencies` or `optionalDependencies` edge
128
+
- add a missing peer dependency edge
129
+
- correct a peer dependency range that is known to be wrong
130
+
- add or correct peer dependency metadata
128
131
129
-
Deletion is not supported in v1. A `null`, `false`, or `"-"` value is an error. Removing dependencies or install behavior has different security and compatibility consequences and should be handled by`overrides`, lifecycle-script policy, native patching, or a follow-up RFC.
132
+
Deletion is not supported in v1. A `null`, `false`, or `"-"` value is an error. Changing normal dependency selection should be handled by `overrides`; removing dependencies or changing install behavior has different security and compatibility consequences and should be handled by lifecycle-script policy, native patching, or a follow-up RFC.
130
133
131
134
### Root-only behavior
132
135
@@ -210,13 +213,27 @@ The preferred v1 shape is to store only the canonical extension hash on the root
210
213
}
211
214
```
212
215
213
-
The canonical hash input is the normalized root `packageExtensions` object from `package.json`. The normalized form should be key-order independent and should ignore insignificant JSON formatting. The root manifest remains authoritative for the extension rules; the lockfile hash proves that the locked graph was generated from the same canonical rule set. The install should reject multiple selectors that match the same candidate package before writing lockfile state. Package entries continue to store their normal effective `dependencies`, `optionalDependencies`, `peerDependencies`, and `peerDependenciesMeta` fields after extension application.
216
+
The canonical hash input is the root `packageExtensions` field after `package.json` parsing and extension-schema validation, not the lockfile's effective dependency metadata or per-entry provenance.
217
+
218
+
The canonicalization rules are:
219
+
220
+
- If the root manifest does not contain `packageExtensions`, npm records no extension hash and no applied-extension provenance, and `npm ci` fails when the lockfile records either one.
221
+
- If the root manifest contains `packageExtensions`, including `{}`, npm hashes the canonical form of that object.
222
+
- Unsupported fields, invalid value types, invalid selectors, and selector conflicts are rejected before npm writes lockfile state.
223
+
- Object keys are sorted lexicographically at every object level before serialization.
224
+
- Selector strings, package names, field names, metadata keys, specifier strings, and metadata values are preserved exactly after JSON parsing.
225
+
- npm must not normalize semver ranges, registry specs, whitespace inside string values, or boolean metadata values for the hash.
226
+
- Serialization uses deterministic JSON with no insignificant whitespace.
227
+
- The digest is written using npm's existing lockfile digest encoding, with `sha512` as the expected v1 algorithm unless npm standardizes another lockfile hash format before implementation.
228
+
- The hash covers only the canonical root extension rules, while package entries store effective dependency metadata and minimal provenance separately.
229
+
230
+
The root manifest remains authoritative for the extension rules; the lockfile hash proves that the locked graph was generated from the same canonical rule set. The install should reject multiple selectors that match the same candidate package before writing lockfile state. Package entries continue to store their normal effective `dependencies`, `optionalDependencies`, `peerDependencies`, and `peerDependenciesMeta` fields after extension application.
214
231
215
232
The required behavior is:
216
233
217
234
-`npm install` updates `package-lock.json` when `packageExtensions` changes.
218
-
-`npm ci` fails if the root `packageExtensions` field is present but the lockfile does not contain extension state.
219
-
-`npm ci` fails if the lockfile contains non-empty extension state but the root `packageExtensions` field is absent.
235
+
-`npm ci` fails if the root `packageExtensions` field is present but the lockfile does not contain a matching extension hash.
236
+
-`npm ci` fails if the lockfile contains an extension hash or applied-extension provenance but the root `packageExtensions` field is absent.
220
237
-`npm ci` fails if the root `packageExtensions` state does not match the canonical state represented in the lockfile.
221
238
-`npm ci` fails if any package identity recorded in the lockfile matches more than one root selector.
222
239
-`npm ci` fails if a lockfile package entry records extension provenance that no longer corresponds to exactly one selector in the canonical root extension state.
@@ -345,6 +362,8 @@ The primary implementation work is in `npm/cli`, especially Arborist.
345
362
- Avoid mutating shared pacote, packument, manifest, registry metadata, or cache objects in place.
346
363
- Skip workspace package manifests as extension targets and warn when a selector would match a workspace member.
347
364
- Normalize cross-field dependency metadata after extension application using the same rules as normal package manifests.
365
+
- Reject `dependencies` and `optionalDependencies` extension entries that attempt to provide a name already declared in either normal dependency field.
366
+
- Allow `peerDependencies` extension entries to replace an existing peer range before peer resolution.
348
367
- Reject extensions that create or attempt to move duplicate names across `dependencies` and `optionalDependencies`.
349
368
- Reject `peerDependenciesMeta` entries that do not correspond to a `peerDependencies` entry after extension application.
350
369
- Preserve extended manifest data in the ideal tree, effective dependency metadata in the lockfile, and minimal extension provenance for affected lockfile entries.
@@ -383,9 +402,14 @@ Required test coverage:
383
402
- Add `peerDependenciesMeta.<peer>.optional`.
384
403
- Verify Arborist's peer resolution sees the extended metadata.
385
404
386
-
- Existing dependency correction:
387
-
- A package declares `bar: "^1"` and the extension changes it to `^2`.
388
-
- The resulting lockfile resolves from the corrected edge.
405
+
- Normal dependency repair:
406
+
- A package missing `dependencies.bar` can receive an extension-created `bar` edge.
407
+
- An extension that attempts to replace existing `dependencies.bar` or `optionalDependencies.bar` fails with a clear error.
408
+
- Normal dependency version changes are handled by `overrides`, not `packageExtensions`.
409
+
410
+
- Peer dependency correction:
411
+
- A package declares `peerDependencies.bar: "^1"` and the extension changes it to `^2`.
412
+
- Arborist's peer resolution and the resulting lockfile use the corrected peer contract.
389
413
390
414
-`overrides` composition:
391
415
- Extension adds `bar: "^1"`.
@@ -414,9 +438,11 @@ Required test coverage:
414
438
- Two installs using the same cached manifest metadata do not observe each other's package extensions.
415
439
416
440
- Merge behavior:
417
-
-`dependencies`, `optionalDependencies`, and `peerDependencies` merge by dependency name.
441
+
-`dependencies` and `optionalDependencies` add missing names only.
442
+
- Extensions that provide a name already declared in `dependencies` or `optionalDependencies` fail.
443
+
-`peerDependencies` merges by peer name.
444
+
- Extension values replace existing peer ranges in `peerDependencies`.
418
445
-`peerDependenciesMeta` merges by peer name and then by metadata key.
419
-
- Extension values replace existing values in the same field.
420
446
- Extensions that create duplicate names across `dependencies` and `optionalDependencies` fail.
421
447
- Extensions that try to move a name between `dependencies` and `optionalDependencies` fail.
422
448
- Extensions may create or preserve overlap between `peerDependencies` and `dependencies` or `optionalDependencies`.
@@ -426,8 +452,11 @@ Required test coverage:
426
452
- Lockfile determinism:
427
453
-`npm install` records a canonical extension hash, effective dependency metadata, and minimal provenance.
428
454
-`npm install` records minimal provenance for affected package entries without duplicating the full extension object on every affected entry.
455
+
- Key order and insignificant JSON formatting changes do not change the canonical extension hash.
456
+
- Selector, package name, field name, metadata key, specifier string, or metadata value changes do change the canonical extension hash.
457
+
- Unsupported fields, invalid value types, invalid selectors, and selector conflicts fail before npm writes extension lockfile state.
429
458
-`npm ci` succeeds with matching extension state.
430
-
-`npm ci` fails when the root manifest has `packageExtensions` but the lockfile lacks extension state.
459
+
-`npm ci` fails when the root manifest has `packageExtensions` but the lockfile lacks a matching extension hash.
431
460
-`npm ci` fails after the root `packageExtensions` entry changes without updating the lockfile.
432
461
-`npm ci` fails after a lockfile package entry records extension provenance that no longer corresponds to exactly one canonical root extension rule.
433
462
-`npm ci` validates extension hash, selector conflicts, and provenance before trusting locked effective dependency metadata.
1.**Lockfile placement and versioning.** Should the canonical root extension hash live on `packages[""]` or in a top-level lockfile section? Does this require a lockfile version bump, or is an additive field enough?
518
547
519
-
2.**Overwrite semantics.** This RFC allows extension values to replace existing dependency and peer ranges. Should v1 instead be add-only, with range correction left entirely to `overrides`?
520
-
521
-
3.**Deletion.** Should v1 support removing dependency entries with a sentinel such as `"-"`? This would match some package-manager prior art, but it also increases the risk of deleting dependencies that are needed at runtime.
548
+
2.**Deletion.** Should v1 support removing dependency entries with a sentinel such as `"-"`? This would match some package-manager prior art, but it also increases the risk of deleting dependencies that are needed at runtime.
522
549
523
-
4.**Where should the field live?**`package.json` is consistent with npm's current lack of a workspace config file. If npm later introduces an `npm-workspace.yaml` or similar file, `packageExtensions` may be a good candidate for migration.
550
+
3.**Where should the field live?**`package.json` is consistent with npm's current lack of a workspace config file. If npm later introduces an `npm-workspace.yaml` or similar file, `packageExtensions` may be a good candidate for migration.
524
551
525
-
5.**Should npm provide management commands?** A future command such as `npm package-extensions ls` or `npm explain --extensions` could identify extension rules that no longer match any package, rules made redundant by upstream fixes, or extension-created edges.
552
+
4.**Should npm provide management commands?** A future command such as `npm package-extensions ls` or `npm explain --extensions` could identify extension rules that no longer match any package, rules made redundant by upstream fixes, or extension-created edges.
526
553
527
-
6.**Shared ecosystem database.** Should npm participate in a shared package-extension database, similar to `@yarnpkg/extensions`, or should all extensions remain project-local?
554
+
5.**Shared ecosystem database.** Should npm participate in a shared package-extension database, similar to `@yarnpkg/extensions`, or should all extensions remain project-local?
528
555
529
-
7.**Imperative hooks.** If the declarative field is not sufficient, should npm add `.npmfile.mjs` / `.npmfile.cjs` with a `readPackage` hook? If so, how should hook output be represented in the lockfile, and what restrictions are needed to keep `npm ci` deterministic?
556
+
6.**Imperative hooks.** If the declarative field is not sufficient, should npm add `.npmfile.mjs` / `.npmfile.cjs` with a `readPackage` hook? If so, how should hook output be represented in the lockfile, and what restrictions are needed to keep `npm ci` deterministic?
0 commit comments