Skip to content

Commit 5a62f31

Browse files
committed
docs: document the .npm-extension file and transformManifest
1 parent d72c9ed commit 5a62f31

3 files changed

Lines changed: 94 additions & 0 deletions

File tree

docs/lib/build.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ const generateNav = async (contentPath, navPath) => {
107107
'/configuring-npm/npmrc',
108108
'/configuring-npm/package-json',
109109
'/configuring-npm/package-lock-json',
110+
'/configuring-npm/npm-extension',
110111
]
111112

112113
// Hardcoded order for using-npm section (only urls - title/description come from frontmatter)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
title: .npm-extension
3+
section: 5
4+
description: Imperative, root-owned manifest repairs
5+
---
6+
7+
### Description
8+
9+
A root-owned `.npm-extension.mjs` or `.npm-extension.cjs` file lets a project imperatively repair the manifests of third-party dependencies before npm resolves the dependency tree. It exports a `transformManifest(pkg, context)` function that receives a candidate dependency manifest and returns the effective manifest npm should use.
10+
11+
`.npm-extension` is the imperative counterpart to the declarative [`packageExtensions`](/configuring-npm/package-json#packageextensions) field, and runs in the same pre-resolution phase, **before** `packageExtensions`. Prefer `packageExtensions` for simple, data-only repairs; reach for `.npm-extension` when you need comments and links explaining a repair, conditional logic, repeated repairs expressed as code, deletion or range rewrites, stale-repair guards, or a policy location outside `package.json`.
12+
13+
### Example
14+
15+
```js
16+
// .npm-extension.mjs
17+
export function transformManifest (pkg, context) {
18+
if (pkg.name === 'foo' && pkg.version.startsWith('1.')) {
19+
pkg.dependencies = { ...pkg.dependencies, bar: '^2.0.0' }
20+
context.log(`added bar to ${pkg.name}@${pkg.version}`)
21+
}
22+
return pkg
23+
}
24+
```
25+
26+
The `.cjs` form uses CommonJS exports instead:
27+
28+
```js
29+
// .npm-extension.cjs
30+
module.exports = {
31+
transformManifest (pkg, context) {
32+
return pkg
33+
},
34+
}
35+
```
36+
37+
### The `transformManifest` function
38+
39+
`transformManifest(pkg, context)` receives a deeply isolated copy of a candidate dependency manifest. It may mutate and return that copy, or return a new manifest object. It **must** return a manifest object synchronously; returning `null`, `undefined`, a primitive, an array, or a promise fails the install.
40+
41+
The `context` argument is intentionally small:
42+
43+
* `context.log(message)` writes an npm debug log message.
44+
* `context.root` is the absolute path to the project root.
45+
* `context.extensionPoint` is the string `"transformManifest"`.
46+
47+
npm provides no registry, fetch, lockfile, or extraction helpers. Keep the extension file self-contained or limited to Node builtins; npm does not guarantee that project dependencies are available when the file is loaded.
48+
49+
### Supported mutations
50+
51+
Only the four resolution-affecting fields may change:
52+
53+
* `dependencies`
54+
* `optionalDependencies`
55+
* `peerDependencies`
56+
* `peerDependenciesMeta`
57+
58+
Within those fields you may add, replace, or delete entries. Changing any other field (such as `scripts`, `bin`, `engines`, `os`, `cpu`, `exports`, or `main`) is rejected, and the install fails with an error naming `.npm-extension` and the package being processed. The package tarball and the installed `node_modules/<pkg>/package.json` are never rewritten.
59+
60+
### Discovery and `extension-file`
61+
62+
npm looks for a single `.npm-extension.mjs` or `.npm-extension.cjs` at the project root (the workspace root in a workspace project). Having both files present is an error. A `.npm-extension` file in a dependency or in a non-root workspace is ignored; a non-root workspace file produces a warning.
63+
64+
The [`extension-file`](/using-npm/config#extension-file) config selects a different project-local file. It must resolve inside the project root and use a `.mjs` or `.cjs` extension, and it is honored only from project config or the command line — never from user, global, or builtin config.
65+
66+
### Interaction with `packageExtensions` and `overrides`
67+
68+
When both are present, `transformManifest` runs first and `packageExtensions` is applied to its output. Avoid targeting the same package with both unless you intend to rely on that ordering. `overrides` still controls the final resolution target of any edge, including edges created by `transformManifest`.
69+
70+
### Lockfile and `npm ci`
71+
72+
A lockfile influenced by `.npm-extension` records an `npmExtensionHash` (a digest of the selected file's bytes and module format) on its root entry, and minimal `npmExtensionApplied` provenance on each affected package entry. Extension state requires `lockfileVersion: 4`.
73+
74+
Changing the file's contents makes `npm install` re-resolve the affected packages. `npm ci` does **not** import or execute `.npm-extension`; it verifies the recorded hash against the file and reifies the locked graph, failing if the file and lockfile disagree (or if one has extension state and the other does not).
75+
76+
The hash proves only that the install uses the same extension file bytes that generated the lockfile. It does not make arbitrary JavaScript deterministic: extension output that depends on environment variables, the network, the clock, or files imported by the extension can still produce non-reproducible installs. Treat `.npm-extension` as trusted, deterministic project code, and only enable it in repositories you trust.
77+
78+
### Disabling
79+
80+
Set [`ignore-extension`](/using-npm/config#ignore-extension) to skip importing and executing `.npm-extension`. [`ignore-scripts`](/using-npm/config#ignore-scripts) implies `ignore-extension`, since both disable root-owned install-time code. `npm ci` still verifies the file hash even when execution is disabled.
81+
82+
### Publishing
83+
84+
`.npm-extension.mjs` and `.npm-extension.cjs` are project configuration, not package contents. npm excludes the root file from the package tarball produced by `npm pack` and `npm publish`, even when the package's `files` list would include it, so a public package can keep `.npm-extension` in its repository for local use without publishing it.
85+
86+
### See also
87+
88+
* [package.json `packageExtensions`](/configuring-npm/package-json#packageextensions)
89+
* [package-lock.json](/configuring-npm/package-lock-json)
90+
* [config](/using-npm/config)

docs/lib/content/nav.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,9 @@
232232
- title: package-lock.json
233233
url: /configuring-npm/package-lock-json
234234
description: A manifestation of the manifest
235+
- title: .npm-extension
236+
url: /configuring-npm/npm-extension
237+
description: Imperative, root-owned manifest repairs
235238
- title: Using npm
236239
shortName: Using
237240
url: /using-npm

0 commit comments

Comments
 (0)