RFC: npm Extension for Manifest Repairs#903
Conversation
owlstronaut
left a comment
There was a problem hiding this comment.
Like the direction, and the security/lockfile framing is solid. My main concern is that a few things framed as inherited from the packageExtensions phase are actually new architecture:
npm ci"no execution":npm cialways builds an ideal tree and the apply hook lives inside that build, so as written it would executetransformManifestduringci. Needs an explicit path that skips the hook on a hash match (or fails before any manifest is re-fetched).- Non-registry sources: the current phase only runs in the manifest-fetch path, so
file:/directory/symlinked deps take the Link branch and aren't transformed today. Covering them plus the linked actual-tree handling from #9568/#9569 is real new wiring. - Deep isolation: since the input is arbitrary JS that can touch any field, the "deeply isolated copy" is a hard prerequisite, not a given (the existing apply only clones the four allowlisted fields and otherwise shares references with pacote's cache).
None of these block the concept, but I think the RFC should call them out as new work and spell out the npm ci design before implementation.
Thank you for flagging that. Updating... |
|
@owlstronaut thank you for the review. I have updated the document. One open item I can spell out further if useful or if you'd rather prefer it be left to implementation: For directory/symlinked dependency sources, npm currently reads the linked target's live |
|
@manzoorwanijk Yeah, worth a sentence in the doc since linked deps are normally read live, so having no-execution |
|
Updated with some more clarity. |
|
|
|
CLI implementation PR - npm/cli#9586 |
Summary
Adds an RFC for a root-owned
.npm-extension.mjs/.npm-extension.cjsfile with a top-leveltransformManifest(pkg, context)extension point.The proposal lets a project imperatively repair third-party package manifests before Arborist reads dependency and peer edges. It builds on the accepted Package Manifest Extensions proposal and its npm CLI implementation, which established the pre-resolution manifest repair phase, root-only authority model, lockfile visibility model, and publish isolation for local dependency metadata repairs.
packageExtensionsremains the safer declarative default for common metadata repairs..npm-extensioncovers the cases where projects need comments, upstream issue links, repeated transformations, conditional logic, reading existing manifest values, deletion, dependency-range replacement, or a local policy file that does not live in publishablepackage.json.Motivation
install-strategy=linkedmakes dependency boundaries stricter by avoiding accidental hoisting. That is useful for correctness, but it exposes packages that import dependencies or type packages they did not declare. The acceptedpackageExtensionsRFC handles the most common form of this problem: small deterministic additions to dependency and peer metadata.Some repairs are harder to keep clear as declarative JSON. During the Package Manifest Extensions proposal discussion, a Gutenberg migration example repeated the same optional
@types/reactpeer repair across many React-related dependencies. The declarative form worked, but the underlying policy was really a named list or predicate: "these packages import React types but do not declare an optional@types/reactpeer."Other local repairs need conditional logic, such as adding a type package only when a matching runtime peer exists, copying an existing peer range into a type dependency, narrowing a known bad peer range, or throwing when upstream has fixed metadata and a local repair should be removed.
packageExtensionsintentionally does not support that kind of code.This RFC proposes an explicit advanced escape hatch for those cases while preserving npm's root-owned authority model, lockfile visibility, and publish isolation.
Why a separate extension file
The RFC intentionally keeps executable policy out of
package.json. A public package may need local dependency repairs for its own tests, build, or linked-install migration, but it should not publish root-only install policy to the registry manifest or packument..npm-extensionis also deliberately not named.npmfileor shaped like pnpm'shooks.readPackage. The proposal borrows the useful manifest-transform idea from pnpm, but defines npm-specific semantics for trust, lockfile hashing,npm ci, publish exclusion, disable behavior, supported mutations, and future extension points.Notable semantics
.npm-extension..npm-extensionfiles warn and are ignored..npm-extensionfiles are ignored.transformManifest(pkg, context).dependencies,optionalDependencies,peerDependencies, andpeerDependenciesMeta.packageExtensions, the imperative form can delete supported dependency entries and replace existing normal dependency ranges.scripts,bin,exports,types, andbundleDependenciesare rejected.transformManifestruns for non-root, non-workspace dependency manifests from registry, git, remote tarball, local file, local directory, and symlinked dependency sources.npmExtensionAppliedprovenance inpackage-lock.json.npm civerifies matching extension state without importing or executing.npm-extension.npm installre-runtransformManifestacross candidate manifests rather than relying on selector-based selective re-resolution..npm-extension.mjsand.npm-extension.cjsare excluded fromnpm packandnpm publish, even when listed infiles.ignore-extension=truedisables extension execution, andignore-scripts=trueimpliesignore-extension=truefor commands that would otherwise execute it.Relationship to
packageExtensionsThis RFC is not trying to replace
packageExtensions. The declarative feature should remain the first choice for small, reviewable manifest repairs. The imperative extension file is for cases where the declarative shape becomes repetitive, cannot express the needed local policy, or cannot live in a publishable package manifest.The RFC keeps the two features ordered and auditable:
transformManifestruns beforepackageExtensions, then npm reads dependency and peer edges from the resulting effective manifest. That lets imperative repairs inspect the upstream manifest before declarative repairs are applied, while preserving the acceptedpackageExtensionsvalidation and provenance model.References
Follow up of #889