Skip to content

Commit 4e4daf7

Browse files
committed
Address packageExtensions review feedback
1 parent 65db3e5 commit 4e4daf7

1 file changed

Lines changed: 52 additions & 25 deletions

File tree

accepted/0000-package-manifest-extensions.md

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -111,22 +111,25 @@ When a selector matches a package manifest, npm applies the extension to a per-i
111111
For each supported object field:
112112

113113
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.
117118

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`.
119120

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.
121122

122123
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.
123124

124-
This permits both of the common manifest-repair cases:
125+
This permits the common manifest-repair cases that are in scope for v1:
125126

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
128131

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.
130133

131134
### Root-only behavior
132135

@@ -210,13 +213,27 @@ The preferred v1 shape is to store only the canonical extension hash on the root
210213
}
211214
```
212215

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.
214231

215232
The required behavior is:
216233

217234
- `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.
220237
- `npm ci` fails if the root `packageExtensions` state does not match the canonical state represented in the lockfile.
221238
- `npm ci` fails if any package identity recorded in the lockfile matches more than one root selector.
222239
- `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.
345362
- Avoid mutating shared pacote, packument, manifest, registry metadata, or cache objects in place.
346363
- Skip workspace package manifests as extension targets and warn when a selector would match a workspace member.
347364
- 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.
348367
- Reject extensions that create or attempt to move duplicate names across `dependencies` and `optionalDependencies`.
349368
- Reject `peerDependenciesMeta` entries that do not correspond to a `peerDependencies` entry after extension application.
350369
- 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:
383402
- Add `peerDependenciesMeta.<peer>.optional`.
384403
- Verify Arborist's peer resolution sees the extended metadata.
385404

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.
389413

390414
- `overrides` composition:
391415
- Extension adds `bar: "^1"`.
@@ -414,9 +438,11 @@ Required test coverage:
414438
- Two installs using the same cached manifest metadata do not observe each other's package extensions.
415439

416440
- 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`.
418445
- `peerDependenciesMeta` merges by peer name and then by metadata key.
419-
- Extension values replace existing values in the same field.
420446
- Extensions that create duplicate names across `dependencies` and `optionalDependencies` fail.
421447
- Extensions that try to move a name between `dependencies` and `optionalDependencies` fail.
422448
- Extensions may create or preserve overlap between `peerDependencies` and `dependencies` or `optionalDependencies`.
@@ -426,8 +452,11 @@ Required test coverage:
426452
- Lockfile determinism:
427453
- `npm install` records a canonical extension hash, effective dependency metadata, and minimal provenance.
428454
- `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.
429458
- `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.
431460
- `npm ci` fails after the root `packageExtensions` entry changes without updating the lockfile.
432461
- `npm ci` fails after a lockfile package entry records extension provenance that no longer corresponds to exactly one canonical root extension rule.
433462
- `npm ci` validates extension hash, selector conflicts, and provenance before trusting locked effective dependency metadata.
@@ -516,14 +545,12 @@ The [Make Install Scripts Opt-In RFC](https://github.com/npm/rfcs/pull/868) prop
516545

517546
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?
518547

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.
522549

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.
524551

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.
526553

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?
528555

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

Comments
 (0)