Skip to content

Commit 41481f8

Browse files
committed
fix: attempt more graceful failure in older node versions
1 parent 457d388 commit 41481f8

File tree

1 file changed

+87
-32
lines changed

1 file changed

+87
-32
lines changed

lib/cli.js

+87-32
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,112 @@
1+
// This is separate to indicate that it should contain code we expect to work in
2+
// all conceivably runnable versions of node. This is a best effort to catch
3+
// syntax errors to give users a good error message if they are using a node
4+
// version that doesn't allow syntax we are using such as private properties, etc
5+
const createEnginesValidation = () => {
6+
const node = process.version.replace(/-.*$/, '')
7+
const pkg = require('../package.json')
8+
const engines = pkg.engines.node
9+
const npm = `v${pkg.version}`
10+
11+
const cols = Math.min(Math.max(20, process.stdout.columns) || 80, 80)
12+
const wrap = (lines) => lines
13+
.join(' ')
14+
.split(/[ \n]+/)
15+
.reduce((left, right) => {
16+
const last = left.split('\n').pop()
17+
const join = last.length && last.length + right.length > cols ? '\n' : ' '
18+
return left + join + right
19+
})
20+
.trim()
21+
22+
const unsupportedMessage = wrap([
23+
`npm ${npm} does not support Node.js ${node}.`,
24+
`You should probably upgrade to a newer version of node as we can't make any`,
25+
`promises that npm will work with this version.`,
26+
`This version of npm supports the following node versions: \`${engines}\`.`,
27+
'You can find the latest version at https://nodejs.org/.',
28+
])
29+
30+
const brokenMessage = wrap([
31+
`ERROR: npm ${npm} is known not to run on Node.js ${node}.`,
32+
`You'll need to upgrade to a newer Node.js version in order to use this version of npm.`,
33+
`This version of npm supports the following node versions: \`${engines}\`.`,
34+
'You can find the latest version at https://nodejs.org/.',
35+
])
36+
37+
// coverage ignored because this is only hit in very unsupported node versions
38+
// and it's a best effort attempt to show something nice in those cases
39+
/* istanbul ignore next */
40+
const syntaxErrorHandler = (err) => {
41+
if (err instanceof SyntaxError) {
42+
// eslint-disable-next-line no-console
43+
console.error(`${brokenMessage}\n\nERROR:`)
44+
// eslint-disable-next-line no-console
45+
console.error(err)
46+
return process.exit(1)
47+
}
48+
throw err
49+
}
50+
51+
process.on('uncaughtException', syntaxErrorHandler)
52+
process.on('unhandledRejection', syntaxErrorHandler)
53+
54+
return {
55+
node,
56+
engines,
57+
unsupportedMessage,
58+
off: () => {
59+
process.off('uncaughtException', syntaxErrorHandler)
60+
process.off('unhandledRejection', syntaxErrorHandler)
61+
},
62+
}
63+
}
64+
165
// Separated out for easier unit testing
266
module.exports = async process => {
367
// set it here so that regardless of what happens later, we don't
468
// leak any private CLI configs to other programs
569
process.title = 'npm'
670

7-
// We used to differentiate between known broken and unsupported
8-
// versions of node and attempt to only log unsupported but still run.
9-
// After we dropped node 10 support, we can use new features
10-
// (like static, private, etc) which will only give vague syntax errors,
11-
// so now both broken and unsupported use console, but only broken
12-
// will process.exit. It is important to now perform *both* of these
13-
// checks as early as possible so the user gets the error message.
14-
const semver = require('semver')
15-
const supported = require('../package.json').engines.node
16-
const knownBroken = '<12.5.0'
17-
18-
const nodejsVersion = process.version.replace(/-.*$/, '')
19-
/* eslint-disable no-console */
20-
if (semver.satisfies(nodejsVersion, knownBroken)) {
21-
console.error('ERROR: npm is known not to run on Node.js ' + process.version)
22-
console.error("You'll need to upgrade to a newer Node.js version in order to use this")
23-
console.error('version of npm. You can find the latest version at https://nodejs.org/')
24-
process.exit(1)
25-
}
26-
if (!semver.satisfies(nodejsVersion, supported)) {
27-
console.error('npm does not support Node.js ' + process.version)
28-
console.error('You should probably upgrade to a newer version of node as we')
29-
console.error("can't make any promises that npm will work with this version.")
30-
console.error('You can find the latest version at https://nodejs.org/')
31-
}
32-
/* eslint-enable no-console */
71+
// Nothing should happen before this line if we can't guarantee it will
72+
// not have syntax errors in some version of node
73+
const validateEngines = createEnginesValidation()
3374

75+
const satisfies = require('semver/functions/satisfies')
3476
const exitHandler = require('./utils/exit-handler.js')
35-
process.on('uncaughtException', exitHandler)
36-
process.on('unhandledRejection', exitHandler)
37-
3877
const Npm = require('./npm.js')
3978
const npm = new Npm()
4079
exitHandler.setNpm(npm)
4180

42-
// if npm is called as "npmg" or "npm_g", then
43-
// run in global mode.
81+
// if npm is called as "npmg" or "npm_g", then run in global mode.
4482
if (process.argv[1][process.argv[1].length - 1] === 'g') {
4583
process.argv.splice(1, 1, 'npm', '-g')
4684
}
4785

48-
const log = require('./utils/log-shim.js')
4986
// only log node and npm paths in argv initially since argv can contain
5087
// sensitive info. a cleaned version will be logged later
88+
const log = require('./utils/log-shim.js')
5189
log.verbose('cli', process.argv.slice(0, 2).join(' '))
5290
log.info('using', 'npm@%s', npm.version)
5391
log.info('using', 'node@%s', process.version)
5492

93+
// At this point we've required a few files and can be pretty sure
94+
// we dont contain invalid syntax for this version of node. It's
95+
// possible a lazy require would, but that's unlikely enough that
96+
// it's not worth catching anymore and we attach the more important
97+
// exit handlers.
98+
validateEngines.off()
99+
process.on('uncaughtException', exitHandler)
100+
process.on('unhandledRejection', exitHandler)
101+
102+
// It is now safe to log a warning if they are using a version of node
103+
// that is not going to fail on syntax errors but is still unsupported
104+
// and untested and might not work reliably. This is safe to use the logger
105+
// now which we want since this will show up in the error log too.
106+
if (!satisfies(validateEngines.node, validateEngines.engines)) {
107+
log.warn('cli', validateEngines.unsupportedMessage)
108+
}
109+
55110
let cmd
56111
// now actually fire up npm and run the command.
57112
// this is how to use npm programmatically:

0 commit comments

Comments
 (0)