Skip to content

Commit 6c12500

Browse files
committed
feat(install): very strict global npm engines
This will do an engines check when installing npm globally and fail if the new npm is known not to work in the current node version. It will not work for older npm versions because they don't have an engines field (it wasn't added till [email protected]). It will at least prevent npm@7 from being installed in node@8. PR-URL: #3731 Credit: @wraithgar Close: #3731 Reviewed-by: @nlf
1 parent 1ad0938 commit 6c12500

File tree

4 files changed

+162
-2
lines changed

4 files changed

+162
-2
lines changed

lib/install.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const log = require('npmlog')
88
const { resolve, join } = require('path')
99
const Arborist = require('@npmcli/arborist')
1010
const runScript = require('@npmcli/run-script')
11+
const pacote = require('pacote')
12+
const checks = require('npm-install-checks')
1113

1214
const ArboristWorkspaceCmd = require('./workspaces/arborist-cmd.js')
1315
class Install extends ArboristWorkspaceCmd {
@@ -126,6 +128,23 @@ class Install extends ArboristWorkspaceCmd {
126128
const ignoreScripts = this.npm.config.get('ignore-scripts')
127129
const isGlobalInstall = this.npm.config.get('global')
128130
const where = isGlobalInstall ? globalTop : this.npm.prefix
131+
const forced = this.npm.config.get('force')
132+
const isDev = this.npm.config.get('dev')
133+
const scriptShell = this.npm.config.get('script-shell') || undefined
134+
135+
// be very strict about engines when trying to update npm itself
136+
const npmInstall = args.find(arg => arg.startsWith('npm@') || arg === 'npm')
137+
if (isGlobalInstall && npmInstall) {
138+
const npmManifest = await pacote.manifest(npmInstall)
139+
try {
140+
checks.checkEngine(npmManifest, npmManifest.version, process.version)
141+
} catch (e) {
142+
if (forced)
143+
this.npm.log.warn('install', `Forcing global npm install with incompatible version ${npmManifest.version} into node ${process.version}`)
144+
else
145+
throw e
146+
}
147+
}
129148

130149
// don't try to install the prefix into itself
131150
args = args.filter(a => resolve(a) !== this.npm.prefix)
@@ -135,7 +154,7 @@ class Install extends ArboristWorkspaceCmd {
135154
args = ['.']
136155

137156
// TODO: Add warnings for other deprecated flags? or remove this one?
138-
if (this.npm.config.get('dev'))
157+
if (isDev)
139158
log.warn('install', 'Usage of the `--dev` option is deprecated. Use `--include=dev` instead.')
140159

141160
const opts = {
@@ -150,7 +169,6 @@ class Install extends ArboristWorkspaceCmd {
150169
await arb.reify(opts)
151170

152171
if (!args.length && !isGlobalInstall && !ignoreScripts) {
153-
const scriptShell = this.npm.config.get('script-shell') || undefined
154172
const scripts = [
155173
'preinstall',
156174
'install',

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"node-gyp": "^7.1.2",
9898
"nopt": "^5.0.0",
9999
"npm-audit-report": "^2.1.5",
100+
"npm-install-checks": "^4.0.0",
100101
"npm-package-arg": "^8.1.5",
101102
"npm-pick-manifest": "^6.1.1",
102103
"npm-profile": "^5.0.3",

test/lib/install.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,146 @@ t.test('should install globally using Arborist', (t) => {
126126
})
127127
})
128128

129+
t.test('npm i -g npm engines check success', (t) => {
130+
const Install = t.mock('../../lib/install.js', {
131+
'../../lib/utils/reify-finish.js': async () => {},
132+
'@npmcli/arborist': function () {
133+
this.reify = () => {}
134+
},
135+
pacote: {
136+
manifest: () => {
137+
return {
138+
version: '100.100.100',
139+
engines: {
140+
node: '>1',
141+
},
142+
}
143+
},
144+
},
145+
})
146+
const npm = mockNpm({
147+
globalDir: 'path/to/node_modules/',
148+
config: {
149+
global: true,
150+
},
151+
})
152+
const install = new Install(npm)
153+
install.exec(['npm'], er => {
154+
if (er)
155+
throw er
156+
t.end()
157+
})
158+
})
159+
160+
t.test('npm i -g npm engines check failure', (t) => {
161+
const Install = t.mock('../../lib/install.js', {
162+
pacote: {
163+
manifest: () => {
164+
return {
165+
166+
version: '100.100.100',
167+
engines: {
168+
node: '>1000',
169+
},
170+
}
171+
},
172+
},
173+
})
174+
const npm = mockNpm({
175+
globalDir: 'path/to/node_modules/',
176+
config: {
177+
global: true,
178+
},
179+
})
180+
const install = new Install(npm)
181+
install.exec(['npm'], er => {
182+
t.match(er, {
183+
message: 'Unsupported engine',
184+
185+
current: {
186+
node: process.version,
187+
npm: '100.100.100',
188+
},
189+
required: {
190+
node: '>1000',
191+
},
192+
code: 'EBADENGINE',
193+
})
194+
t.end()
195+
})
196+
})
197+
198+
t.test('npm i -g npm engines check failure forced override', (t) => {
199+
const Install = t.mock('../../lib/install.js', {
200+
'../../lib/utils/reify-finish.js': async () => {},
201+
'@npmcli/arborist': function () {
202+
this.reify = () => {}
203+
},
204+
pacote: {
205+
manifest: () => {
206+
return {
207+
208+
version: '100.100.100',
209+
engines: {
210+
node: '>1000',
211+
},
212+
}
213+
},
214+
},
215+
})
216+
const npm = mockNpm({
217+
globalDir: 'path/to/node_modules/',
218+
config: {
219+
force: true,
220+
global: true,
221+
},
222+
})
223+
const install = new Install(npm)
224+
install.exec(['npm'], er => {
225+
if (er)
226+
throw er
227+
t.end()
228+
})
229+
})
230+
231+
t.test('npm i -g npm@version engines check failure', (t) => {
232+
const Install = t.mock('../../lib/install.js', {
233+
pacote: {
234+
manifest: () => {
235+
return {
236+
237+
version: '100.100.100',
238+
engines: {
239+
node: '>1000',
240+
},
241+
}
242+
},
243+
},
244+
})
245+
const npm = mockNpm({
246+
globalDir: 'path/to/node_modules/',
247+
config: {
248+
global: true,
249+
},
250+
})
251+
const install = new Install(npm)
252+
install.exec(['npm@100'], er => {
253+
t.match(er, {
254+
message: 'Unsupported engine',
255+
256+
current: {
257+
node: process.version,
258+
npm: '100.100.100',
259+
},
260+
required: {
261+
node: '>1000',
262+
},
263+
code: 'EBADENGINE',
264+
})
265+
t.end()
266+
})
267+
})
268+
129269
t.test('completion to folder', async t => {
130270
const Install = t.mock('../../lib/install.js', {
131271
'../../lib/utils/reify-finish.js': async () => {},

0 commit comments

Comments
 (0)