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: docs/lib/content/using-npm/scripts.md
+10-5Lines changed: 10 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -111,33 +111,38 @@ It is run AFTER the changes have been applied and the `package.json` and `packag
111
111
112
112
#### [`npm ci`](/commands/npm-ci)
113
113
114
-
*`preinstall`
114
+
*`preinstall` (before dependencies are installed)
115
115
*`install`
116
116
*`postinstall`
117
117
*`prepublish`
118
118
*`preprepare`
119
119
*`prepare`
120
120
*`postprepare`
121
121
122
-
These all run after the actual installation of modules into
123
-
`node_modules`, in order, with no internal actions happening in between
122
+
`preinstall` runs before any dependencies are fetched or unpacked into `node_modules`, so scripts can prepare the environment (for example, setting up authentication for a private registry) before tarballs are fetched. For `npm ci`, `preinstall` fires *after* the lockfile has been validated against `package.json`, so it cannot influence dependency resolution — that remains locked to `package-lock.json`. The remaining scripts run after the installation of modules into `node_modules`, in order, with no internal actions happening in between.
123
+
124
+
Because `preinstall` runs before reify, scripts cannot rely on packages from `node_modules`. `npm ci` wipes `node_modules` before `preinstall` fires, so `require()` of a dependency will always fail. Use `install` or `postinstall` for setup that depends on installed packages.
124
125
125
126
#### [`npm diff`](/commands/npm-diff)
126
127
127
128
*`prepare`
128
129
129
130
#### [`npm install`](/commands/npm-install)
130
131
131
-
These also run when you run`npm install -g <pkg-name>`
132
+
These run on a bare`npm install` in a local project (no package arguments).
132
133
133
-
*`preinstall`
134
+
*`preinstall` (before dependencies are installed)
134
135
*`install`
135
136
*`postinstall`
136
137
*`prepublish`
137
138
*`preprepare`
138
139
*`prepare`
139
140
*`postprepare`
140
141
142
+
`preinstall` runs before any dependencies are fetched or unpacked into `node_modules`, so scripts can prepare the environment (for example, setting up authentication for a private registry) before resolution begins. The remaining scripts run after installation has completed.
143
+
144
+
Because `preinstall` runs before reify, scripts cannot rely on packages from `node_modules`. On a fresh checkout, `require()` of a dependency will fail. On a repeat `npm install` against an existing `node_modules/`, it may incidentally succeed because the previously-installed tree is still on disk, but the version available is whatever was previously installed and may be removed or replaced by the upcoming install. Use `install` or `postinstall` for setup that depends on installed packages.
145
+
141
146
If there is a `binding.gyp` file in the root of your package and you haven't defined your own `install` or `preinstall` scripts, npm will default the `install` command to compile using node-gyp via `node-gyp rebuild`
Copy file name to clipboardExpand all lines: lib/commands/ci.js
+17-12Lines changed: 17 additions & 12 deletions
Original file line number
Diff line number
Diff line change
@@ -99,28 +99,33 @@ class CI extends ArboristWorkspaceCmd {
99
99
})
100
100
}
101
101
102
+
// Root lifecycle scripts for `npm ci` mirror those run by `npm install`. `preinstall` runs *before* reify so that scripts can bootstrap the environment (e.g. private-registry auth) before any dependency is fetched or unpacked. The remaining scripts run after reify as they did before.
Copy file name to clipboardExpand all lines: lib/commands/install.js
+19-11Lines changed: 19 additions & 11 deletions
Original file line number
Diff line number
Diff line change
@@ -145,27 +145,35 @@ class Install extends ArboristWorkspaceCmd {
145
145
add: args,
146
146
workspaces: this.workspaceNames,
147
147
}
148
+
149
+
// Root lifecycle scripts only run for a bare `npm install` in a local project. `preinstall` runs *before* Arborist touches the filesystem so that scripts can bootstrap the environment (e.g. set up private-registry auth, generate files consumed during resolution) before dependencies are fetched or unpacked. The remaining scripts run after reify as they did before.
t.equal(pre.depInstalled,false,'preinstall runs before dependencies are installed')
222
+
t.equal(post.depInstalled,true,'postinstall runs after dependencies are installed')
223
+
})
224
+
225
+
// Regression test: --ignore-scripts must suppress the new pre-reify `preinstall` path in `npm ci`, matching the symmetric guarantee in `npm install`.
226
+
t.test('--ignore-scripts skips preinstall entirely for npm ci',asynct=>{
227
+
constevents=[]
228
+
const{ npm, registry }=awaitloadMockNpm(t,{
229
+
config: {'ignore-scripts': true,audit: false},
230
+
prefixDir: {
231
+
abbrev: abbrev,
232
+
'package.json': JSON.stringify({
233
+
...packageJson,
234
+
scripts: {
235
+
preinstall: 'echo preinstall',
236
+
postinstall: 'echo postinstall',
237
+
},
238
+
}),
239
+
'package-lock.json': JSON.stringify(packageLock),
240
+
},
241
+
mocks: {
242
+
'@npmcli/run-script': async(opts)=>{
243
+
if(opts.path===npm.prefix){
244
+
events.push(opts.event)
245
+
}
246
+
},
247
+
},
248
+
})
249
+
constmanifest=registry.manifest({name: 'abbrev'})
250
+
awaitregistry.tarball({
251
+
manifest: manifest.versions['1.0.0'],
252
+
tarball: path.join(npm.prefix,'abbrev'),
253
+
})
254
+
awaitnpm.exec('ci',[])
255
+
t.strictSame(events,[],'no root lifecycle scripts run when --ignore-scripts is set')
256
+
})
257
+
258
+
// Regression test: symmetric to the install-side guarantee — a failing root `preinstall` must short-circuit before reify runs in `npm ci`, so dependencies never reach disk on failure.
259
+
t.test('a failing preinstall prevents reify for npm ci',asynct=>{
Copy file name to clipboardExpand all lines: test/lib/commands/install.js
+112Lines changed: 112 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -101,6 +101,118 @@ t.test('exec commands', async t => {
101
101
t.strictSame(lifecycleScripts,runOrder,'all script ran in the correct order')
102
102
})
103
103
104
+
// Regression test: root `preinstall` must run before any dependency is fetched/unpacked, while `install` and `postinstall` run after reify has populated node_modules.
105
+
awaitt.test('preinstall runs before reify, post-reify scripts run after',asynct=>{
106
+
constevents=[]
107
+
const{ npm, registry }=awaitloadMockNpm(t,{
108
+
config: {audit: false},
109
+
prefixDir: {
110
+
'package.json': JSON.stringify({
111
+
...packageJson,
112
+
scripts: {
113
+
preinstall: 'echo preinstall',
114
+
install: 'echo install',
115
+
postinstall: 'echo postinstall',
116
+
},
117
+
}),
118
+
abbrev,
119
+
},
120
+
mocks: {
121
+
'@npmcli/run-script': async(opts)=>{
122
+
// Only record scripts targeted at the project root, not any that arborist may run for dependencies during reify.
t.strictSame(events,[],'no root lifecycle scripts run when --ignore-scripts is set')
178
+
})
179
+
180
+
// Regression test: a failing root `preinstall` must short-circuit before reify runs, so dependencies never reach disk on failure. This is the cleaner failure mode the PR was motivated by; future refactors that swallow the rejection and still call reify must fail here.
0 commit comments