feat: packageExtensions for root-owned dependency manifest repairs#9496
Open
manzoorwanijk wants to merge 10 commits into
Open
feat: packageExtensions for root-owned dependency manifest repairs#9496manzoorwanijk wants to merge 10 commits into
manzoorwanijk wants to merge 10 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements package manifest extensions per RFC #889: a root-only
packageExtensionsfield inpackage.jsonthat applies declarative repairs to third-party dependency manifests before Arborist finalizes the ideal tree. It lets a project add missingdependencies/optionalDependencies, add or correctpeerDependencies, and mark peers optional viapeerDependenciesMeta, without forking and republishing a package.{ "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=linkedgives 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 tooverrides(which needs an existing edge to retarget) and to native dependency patching #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 manifestname/version(the underlying name for aliases) and reject dist-tag, git, file, URL, andnpm:specs. At most one selector may match a candidate. Honored only in the rootpackage.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 ofoverrides.Merge semantics
Only the four resolution-affecting fields may be extended.
dependencies/optionalDependenciesadd a missing name only; providing a name already declared in either field is an error (useoverridesto change a version), which also forbids moving a name between the two.peerDependenciesshallow-merges by name, replacing an existing range.peerDependenciesMetamerges by name then key (e.g. addoptional: true); every meta entry must have a correspondingpeerDependenciesentry.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.jsonis not rewritten, andbundleDependenciesis unchanged.overridesstill 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 forceslockfileVersion: 4so older npm clients abort rather than silently dropping the repaired graph.npm installre-resolves affected packages when the rule set changes;npm civalidates the hash, selector conflicts, and stale provenance before trusting the locked metadata.Visibility
npm explainappends(added by packageExtensions["foo@1"].dependencies.bar)to the edge;npm lsannotates the node andnpm ls --jsonincludespackageExtensionsApplied. Publishing a non-private package containing the field warns that it does not affect consumers.Notes
lockfileVersion: 4is shared with native dependency patching (#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 samemaxLockfileVersion/bump constants rather than introduce a competing version.References
Implements npm/rfcs#889