From 6c12500ae14a6f8b78e3ab091ee6cc8e2ea9fd23 Mon Sep 17 00:00:00 2001 From: Gar Date: Thu, 9 Sep 2021 08:21:56 -0700 Subject: [PATCH] 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 npm@6.14.0). It will at least prevent npm@7 from being installed in node@8. PR-URL: https://github.com/npm/cli/pull/3731 Credit: @wraithgar Close: #3731 Reviewed-by: @nlf --- lib/install.js | 22 ++++++- package-lock.json | 1 + package.json | 1 + test/lib/install.js | 140 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 2 deletions(-) diff --git a/lib/install.js b/lib/install.js index 6611763978e61..1589ff589c38e 100644 --- a/lib/install.js +++ b/lib/install.js @@ -8,6 +8,8 @@ const log = require('npmlog') const { resolve, join } = require('path') const Arborist = require('@npmcli/arborist') const runScript = require('@npmcli/run-script') +const pacote = require('pacote') +const checks = require('npm-install-checks') const ArboristWorkspaceCmd = require('./workspaces/arborist-cmd.js') class Install extends ArboristWorkspaceCmd { @@ -126,6 +128,23 @@ class Install extends ArboristWorkspaceCmd { const ignoreScripts = this.npm.config.get('ignore-scripts') const isGlobalInstall = this.npm.config.get('global') const where = isGlobalInstall ? globalTop : this.npm.prefix + const forced = this.npm.config.get('force') + const isDev = this.npm.config.get('dev') + const scriptShell = this.npm.config.get('script-shell') || undefined + + // be very strict about engines when trying to update npm itself + const npmInstall = args.find(arg => arg.startsWith('npm@') || arg === 'npm') + if (isGlobalInstall && npmInstall) { + const npmManifest = await pacote.manifest(npmInstall) + try { + checks.checkEngine(npmManifest, npmManifest.version, process.version) + } catch (e) { + if (forced) + this.npm.log.warn('install', `Forcing global npm install with incompatible version ${npmManifest.version} into node ${process.version}`) + else + throw e + } + } // don't try to install the prefix into itself args = args.filter(a => resolve(a) !== this.npm.prefix) @@ -135,7 +154,7 @@ class Install extends ArboristWorkspaceCmd { args = ['.'] // TODO: Add warnings for other deprecated flags? or remove this one? - if (this.npm.config.get('dev')) + if (isDev) log.warn('install', 'Usage of the `--dev` option is deprecated. Use `--include=dev` instead.') const opts = { @@ -150,7 +169,6 @@ class Install extends ArboristWorkspaceCmd { await arb.reify(opts) if (!args.length && !isGlobalInstall && !ignoreScripts) { - const scriptShell = this.npm.config.get('script-shell') || undefined const scripts = [ 'preinstall', 'install', diff --git a/package-lock.json b/package-lock.json index 9abe13a85d668..8546bd714d204 100644 --- a/package-lock.json +++ b/package-lock.json @@ -127,6 +127,7 @@ "node-gyp": "^7.1.2", "nopt": "^5.0.0", "npm-audit-report": "^2.1.5", + "npm-install-checks": "^4.0.0", "npm-package-arg": "^8.1.5", "npm-pick-manifest": "^6.1.1", "npm-profile": "^5.0.3", diff --git a/package.json b/package.json index e0363a49bf52e..4531d1508fbe0 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "node-gyp": "^7.1.2", "nopt": "^5.0.0", "npm-audit-report": "^2.1.5", + "npm-install-checks": "^4.0.0", "npm-package-arg": "^8.1.5", "npm-pick-manifest": "^6.1.1", "npm-profile": "^5.0.3", diff --git a/test/lib/install.js b/test/lib/install.js index 6412b34c16f25..2cbee02e67b28 100644 --- a/test/lib/install.js +++ b/test/lib/install.js @@ -126,6 +126,146 @@ t.test('should install globally using Arborist', (t) => { }) }) +t.test('npm i -g npm engines check success', (t) => { + const Install = t.mock('../../lib/install.js', { + '../../lib/utils/reify-finish.js': async () => {}, + '@npmcli/arborist': function () { + this.reify = () => {} + }, + pacote: { + manifest: () => { + return { + version: '100.100.100', + engines: { + node: '>1', + }, + } + }, + }, + }) + const npm = mockNpm({ + globalDir: 'path/to/node_modules/', + config: { + global: true, + }, + }) + const install = new Install(npm) + install.exec(['npm'], er => { + if (er) + throw er + t.end() + }) +}) + +t.test('npm i -g npm engines check failure', (t) => { + const Install = t.mock('../../lib/install.js', { + pacote: { + manifest: () => { + return { + _id: 'npm@1.2.3', + version: '100.100.100', + engines: { + node: '>1000', + }, + } + }, + }, + }) + const npm = mockNpm({ + globalDir: 'path/to/node_modules/', + config: { + global: true, + }, + }) + const install = new Install(npm) + install.exec(['npm'], er => { + t.match(er, { + message: 'Unsupported engine', + pkgid: 'npm@1.2.3', + current: { + node: process.version, + npm: '100.100.100', + }, + required: { + node: '>1000', + }, + code: 'EBADENGINE', + }) + t.end() + }) +}) + +t.test('npm i -g npm engines check failure forced override', (t) => { + const Install = t.mock('../../lib/install.js', { + '../../lib/utils/reify-finish.js': async () => {}, + '@npmcli/arborist': function () { + this.reify = () => {} + }, + pacote: { + manifest: () => { + return { + _id: 'npm@1.2.3', + version: '100.100.100', + engines: { + node: '>1000', + }, + } + }, + }, + }) + const npm = mockNpm({ + globalDir: 'path/to/node_modules/', + config: { + force: true, + global: true, + }, + }) + const install = new Install(npm) + install.exec(['npm'], er => { + if (er) + throw er + t.end() + }) +}) + +t.test('npm i -g npm@version engines check failure', (t) => { + const Install = t.mock('../../lib/install.js', { + pacote: { + manifest: () => { + return { + _id: 'npm@1.2.3', + version: '100.100.100', + engines: { + node: '>1000', + }, + } + }, + }, + }) + const npm = mockNpm({ + globalDir: 'path/to/node_modules/', + config: { + global: true, + }, + }) + const install = new Install(npm) + install.exec(['npm@100'], er => { + t.match(er, { + message: 'Unsupported engine', + pkgid: 'npm@1.2.3', + current: { + node: process.version, + npm: '100.100.100', + }, + required: { + node: '>1000', + }, + code: 'EBADENGINE', + }) + t.end() + }) +}) + t.test('completion to folder', async t => { const Install = t.mock('../../lib/install.js', { '../../lib/utils/reify-finish.js': async () => {},