Skip to content

Commit d062b2c

Browse files
committed
new npm-specific update-notifier implementation
This drops our usage of the update-notifier module, in favor of checking ourselves, using the modules and UX patterns that npm already has in place. - While on a prerelease version, updates are checked for every day, instead of every week, and always checks for a new beta in the current release family. Ie, ^7.0.0-beta.2 instead of latest. - Latest version is suggested if newer than current. - If current version is newer than latest, then we check again for an update in the current version family. Ie, ^7.0.0 instead of latest, if current is 7.0.0 and latest is 6.x. - Output is printed using log.notice, at the end of all other log output, so that it's both less visually disruptive, and less likely to be missed among other warnings and notices. This has the side effect of requiring that we set npm.flatOptions as soon as config is loaded, rather than waiting for a command to be run. Since the cli runs a command immediately after loading anyway, this is not a relevant change for our purposes, but worth mentioning here.
1 parent cf28192 commit d062b2c

File tree

7 files changed

+388
-199
lines changed

7 files changed

+388
-199
lines changed

lib/cli.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ module.exports = (process) => {
5252
// this is how to use npm programmatically:
5353
conf._exit = true
5454
const updateNotifier = require('../lib/utils/update-notifier.js')
55-
npm.load(conf, er => {
55+
npm.load(conf, async er => {
5656
if (er) return errorHandler(er)
5757

58-
updateNotifier(npm)
58+
npm.updateNotification = await updateNotifier(npm)
5959

6060
const cmd = npm.argv.shift()
6161
const impl = npm.commands[cmd]

lib/npm.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ const npm = module.exports = new class extends EventEmitter {
6666
constructor () {
6767
super()
6868
require('./utils/perf.js')
69+
this.modes = {
70+
exec: 0o755,
71+
file: 0o644,
72+
umask: 0o22
73+
}
6974
this.started = Date.now()
7075
this.command = null
7176
this.commands = proxyCmds(this)
@@ -75,6 +80,7 @@ const npm = module.exports = new class extends EventEmitter {
7580
this.config = notYetLoadedConfig
7681
this.loading = false
7782
this.loaded = false
83+
this.updateNotification = null
7884
}
7985

8086
deref (c) {
@@ -93,11 +99,6 @@ const npm = module.exports = new class extends EventEmitter {
9399
process.emit('time', `command:${cmd}`)
94100
this.command = cmd
95101

96-
if (!this[_flatOptions]) {
97-
this[_flatOptions] = require('./config/flat-options.js')(this)
98-
require('./config/set-envs.js')(this)
99-
}
100-
101102
// Options are prefixed by a hyphen-minus (-, \u2d).
102103
// Other dash-type chars look similar but are invalid.
103104
if (!warnedNonDashArg) {
@@ -144,6 +145,10 @@ const npm = module.exports = new class extends EventEmitter {
144145
if (!er && this.config.get('force')) {
145146
this.log.warn('using --force', 'Recommended protections disabled.')
146147
}
148+
if (!er && !this[_flatOptions]) {
149+
this[_flatOptions] = require('./config/flat-options.js')(this)
150+
require('./config/set-envs.js')(this)
151+
}
147152
process.emit('timeEnd', 'npm:load')
148153
this.emit('load', er)
149154
})

lib/utils/error-handler.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ const errorHandler = (er) => {
130130
er = er || new Error('Callback called more than once.')
131131
}
132132

133+
if (npm.updateNotification) {
134+
const { level } = log
135+
log.level = log.levels.notice
136+
log.notice('', npm.updateNotification)
137+
log.level = level
138+
}
139+
133140
cbCalled = true
134141
if (!er) return exit(0)
135142
if (typeof er === 'string') {

lib/utils/update-notifier.js

Lines changed: 108 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,122 @@
11
// print a banner telling the user to upgrade npm to latest
22
// but not in CI, and not if we're doing that already.
3+
// Check daily for betas, and weekly otherwise.
4+
5+
const pacote = require('pacote')
6+
const ciDetect = require('@npmcli/ci-detect')
7+
const semver = require('semver')
8+
const chalk = require('chalk')
9+
const { promisify } = require('util')
10+
const stat = promisify(require('fs').stat)
11+
const writeFile = promisify(require('fs').writeFile)
12+
const { resolve } = require('path')
313

414
const isGlobalNpmUpdate = npm => {
5-
return npm.config.get('global') &&
15+
return npm.flatOptions.global &&
616
['install', 'update'].includes(npm.command) &&
717
npm.argv.includes('npm')
818
}
919

10-
const { checkVersion } = require('./unsupported.js')
20+
// update check frequency
21+
const DAILY = 1000 * 60 * 60 * 24
22+
const WEEKLY = DAILY * 7
23+
24+
const updateTimeout = async (npm, duration) => {
25+
const t = new Date(Date.now() - duration)
26+
// don't put it in the _cacache folder, just in npm's cache
27+
const f = resolve(npm.flatOptions.cache, '../_update-notifier-last-checked')
28+
// if we don't have a file, then definitely check it.
29+
const st = await stat(f).catch(() => ({ mtime: t - 1 }))
30+
31+
if (t > st.mtime) {
32+
// best effort, if this fails, it's ok.
33+
// might be using /dev/null as the cache or something weird like that.
34+
await writeFile(f, '').catch(() => {})
35+
return true
36+
} else {
37+
return false
38+
}
39+
}
1140

12-
module.exports = (npm) => {
41+
const updateNotifier = module.exports = async (npm, spec = 'latest') => {
42+
// never check for updates in CI, when updating npm already, or opted out
1343
if (!npm.config.get('update-notifier') ||
14-
isGlobalNpmUpdate(npm) ||
15-
checkVersion(process.version).unsupported) {
16-
return
44+
isGlobalNpmUpdate(npm) ||
45+
ciDetect()) {
46+
return null
47+
}
48+
49+
// if we're on a prerelease train, then updates are coming fast
50+
// check for a new one daily. otherwise, weekly.
51+
const { version } = npm
52+
const current = semver.parse(version)
53+
54+
// if we're on a beta train, always get the next beta
55+
if (current.prerelease.length) {
56+
spec = `^${version}`
57+
}
58+
59+
// while on a beta train, get updates daily
60+
const duration = spec !== 'latest' ? DAILY : WEEKLY
61+
62+
// if we've already checked within the specified duration, don't check again
63+
if (!(await updateTimeout(npm, duration))) {
64+
return null
65+
}
66+
67+
// if they're currently using a prerelease, nudge to the next prerelease
68+
// otherwise, nudge to latest.
69+
const useColor = npm.log.useColor()
70+
71+
const mani = await pacote.manifest(`npm@${spec}`, {
72+
// always prefer latest, even if doing --tag=whatever on the cmd
73+
defaultTag: 'latest',
74+
...npm.flatOptions
75+
}).catch(() => null)
76+
77+
// if pacote failed, give up
78+
if (!mani) {
79+
return null
1780
}
1881

19-
const pkg = require('../../package.json')
20-
const notifier = require('update-notifier')({ pkg })
21-
const ciDetect = require('@npmcli/ci-detect')
22-
if (
23-
notifier.update &&
24-
notifier.update.latest !== pkg.version &&
25-
!ciDetect()
26-
) {
27-
const chalk = require('chalk')
28-
const useColor = npm.color
29-
const useUnicode = npm.config.get('unicode')
30-
const old = notifier.update.current
31-
const latest = notifier.update.latest
32-
const type = notifier.update.type
33-
const typec = !useColor ? type
34-
: type === 'major' ? chalk.red(type)
35-
: type === 'minor' ? chalk.yellow(type)
36-
: chalk.green(type)
37-
38-
const changelog = `https://github.com/npm/cli/releases/tag/v${latest}`
39-
notifier.notify({
40-
message: `New ${typec} version of ${pkg.name} available! ${
41-
useColor ? chalk.red(old) : old
42-
} ${useUnicode ? '→' : '->'} ${
43-
useColor ? chalk.green(latest) : latest
44-
}\n` +
45-
`${
46-
useColor ? chalk.yellow('Changelog:') : 'Changelog:'
47-
} ${
48-
useColor ? chalk.cyan(changelog) : changelog
49-
}\n` +
50-
`Run ${
51-
useColor
52-
? chalk.green(`npm install -g ${pkg.name}`)
53-
: `npm i -g ${pkg.name}`
54-
} to update!`
55-
})
82+
const latest = mani.version
83+
84+
// if the current version is *greater* than latest, we're on a 'next'
85+
// and should get the updates from that release train.
86+
// Note that this isn't another http request over the network, because
87+
// the packument will be cached by pacote from previous request.
88+
if (semver.gt(version, latest) && spec === 'latest') {
89+
return updateNotifier(npm, `^${version}`)
90+
}
91+
92+
// if we already have something >= the desired spec, then we're done
93+
if (semver.gte(version, latest)) {
94+
return null
5695
}
96+
97+
// ok! notify the user about this update they should get.
98+
// The message is saved for printing at process exit so it will not get
99+
// lost in any other messages being printed as part of the command.
100+
const update = semver.parse(mani.version)
101+
const type = update.major !== current.major ? 'major'
102+
: update.minor !== current.minor ? 'minor'
103+
: update.patch !== current.patch ? 'patch'
104+
: 'prerelease'
105+
const typec = !useColor ? type
106+
: type === 'major' ? chalk.red(type)
107+
: type === 'minor' ? chalk.yellow(type)
108+
: chalk.green(type)
109+
const oldc = !useColor ? current : chalk.red(current)
110+
const latestc = !useColor ? latest : chalk.green(latest)
111+
const changelog = `https://github.com/npm/cli/releases/tag/v${latest}`
112+
const changelogc = !useColor ? `<${changelog}>` : chalk.cyan(changelog)
113+
const cmd = `npm install -g npm@${latest}`
114+
const cmdc = !useColor ? `\`${cmd}\`` : chalk.green(cmd)
115+
const message = `\nNew ${typec} version of npm available! ` +
116+
`${oldc} -> ${latestc}\n` +
117+
`Changelog: ${changelogc}\n` +
118+
`Run ${cmdc} to update!\n`
119+
const messagec = !useColor ? message : chalk.bgBlack.white(message)
120+
121+
return messagec
57122
}

tap-snapshots/test-lib-utils-update-notifier.js-TAP.test.js

Lines changed: 72 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,74 +5,98 @@
55
* Make sure to inspect the output below. Do not ignore changes!
66
*/
77
'use strict'
8-
exports[`test/lib/utils/update-notifier.js TAP notification situations color and unicode major > must match snapshot 1`] = `
9-
New major version of npm available! <<major>>-beta.1 → 7.0.0
10-
Changelog: https://github.com/npm/cli/releases/tag/v7.0.0
11-
Run npm install -g npm to update!
8+
exports[`test/lib/utils/update-notifier.js TAP notification situations major to current > color 1`] = `
9+

10+
New major version of npm available! 122.420.69 -> 123.420.69
11+
Changelog: https://github.com/npm/cli/releases/tag/v123.420.69
12+
Run npm install -g [email protected] to update!
13+

1214
`
1315

14-
exports[`test/lib/utils/update-notifier.js TAP notification situations color and unicode minor > must match snapshot 1`] = `
15-
New minor version of npm available! <<minor>>-beta.1 → 7.0.0
16-
Changelog: https://github.com/npm/cli/releases/tag/v7.0.0
17-
Run npm install -g npm to update!
16+
exports[`test/lib/utils/update-notifier.js TAP notification situations major to current > no color 1`] = `
17+
18+
New major version of npm available! 122.420.69 -> 123.420.69
19+
Changelog: <https://github.com/npm/cli/releases/tag/v123.420.69>
20+
Run \`npm install -g [email protected]\` to update!
21+
1822
`
1923

20-
exports[`test/lib/utils/update-notifier.js TAP notification situations color and unicode minor > must match snapshot 2`] = `
21-
New patch version of npm available! <<patch>>-beta.1 → 7.0.0
22-
Changelog: https://github.com/npm/cli/releases/tag/v7.0.0
23-
Run npm install -g npm to update!
24+
exports[`test/lib/utils/update-notifier.js TAP notification situations minor to current > color 1`] = `
25+

26+
New minor version of npm available! 123.419.69 -> 123.420.69
27+
Changelog: https://github.com/npm/cli/releases/tag/v123.420.69
28+
Run npm install -g [email protected] to update!
29+

2430
`
2531

26-
exports[`test/lib/utils/update-notifier.js TAP notification situations color, no unicode major > must match snapshot 1`] = `
27-
New major version of npm available! <<major>>-beta.1 -> 7.0.0
28-
Changelog: https://github.com/npm/cli/releases/tag/v7.0.0
29-
Run npm install -g npm to update!
32+
exports[`test/lib/utils/update-notifier.js TAP notification situations minor to current > no color 1`] = `
33+
34+
New minor version of npm available! 123.419.69 -> 123.420.69
35+
Changelog: <https://github.com/npm/cli/releases/tag/v123.420.69>
36+
Run \`npm install -g [email protected]\` to update!
37+
3038
`
3139

32-
exports[`test/lib/utils/update-notifier.js TAP notification situations color, no unicode minor > must match snapshot 1`] = `
33-
New minor version of npm available! <<minor>>-beta.1 -> 7.0.0
34-
Changelog: https://github.com/npm/cli/releases/tag/v7.0.0
35-
Run npm install -g npm to update!
40+
exports[`test/lib/utils/update-notifier.js TAP notification situations minor to next version > color 1`] = `
41+

42+
New minor version of npm available! 123.420.70 -> 123.421.70
43+
Changelog: https://github.com/npm/cli/releases/tag/v123.421.70
44+
Run npm install -g [email protected] to update!
45+

3646
`
3747

38-
exports[`test/lib/utils/update-notifier.js TAP notification situations color, no unicode minor > must match snapshot 2`] = `
39-
New patch version of npm available! <<patch>>-beta.1 -> 7.0.0
40-
Changelog: https://github.com/npm/cli/releases/tag/v7.0.0
41-
Run npm install -g npm to update!
48+
exports[`test/lib/utils/update-notifier.js TAP notification situations minor to next version > no color 1`] = `
49+
50+
New minor version of npm available! 123.420.70 -> 123.421.70
51+
Changelog: <https://github.com/npm/cli/releases/tag/v123.421.70>
52+
Run \`npm install -g [email protected]\` to update!
53+
4254
`
4355

44-
exports[`test/lib/utils/update-notifier.js TAP notification situations no color, no unicode major > must match snapshot 1`] = `
45-
New major version of npm available! <<major>>-beta.1 -> 7.0.0
46-
Changelog: https://github.com/npm/cli/releases/tag/v7.0.0
47-
Run npm i -g npm to update!
56+
exports[`test/lib/utils/update-notifier.js TAP notification situations new beta available > color 1`] = `
57+

58+
New prerelease version of npm available! 124.0.0-beta.0 -> 124.0.0-beta.99999
59+
Changelog: https://github.com/npm/cli/releases/tag/v124.0.0-beta.99999
60+
Run npm install -g [email protected] to update!
61+

4862
`
4963

50-
exports[`test/lib/utils/update-notifier.js TAP notification situations no color, no unicode minor > must match snapshot 1`] = `
51-
New minor version of npm available! <<minor>>-beta.1 -> 7.0.0
52-
Changelog: https://github.com/npm/cli/releases/tag/v7.0.0
53-
Run npm i -g npm to update!
64+
exports[`test/lib/utils/update-notifier.js TAP notification situations new beta available > no color 1`] = `
65+
66+
New prerelease version of npm available! 124.0.0-beta.0 -> 124.0.0-beta.99999
67+
Changelog: <https://github.com/npm/cli/releases/tag/v124.0.0-beta.99999>
68+
Run \`npm install -g [email protected]\` to update!
69+
5470
`
5571

56-
exports[`test/lib/utils/update-notifier.js TAP notification situations no color, no unicode minor > must match snapshot 2`] = `
57-
New patch version of npm available! <<patch>>-beta.1 -> 7.0.0
58-
Changelog: https://github.com/npm/cli/releases/tag/v7.0.0
59-
Run npm i -g npm to update!
72+
exports[`test/lib/utils/update-notifier.js TAP notification situations patch to current > color 1`] = `
73+

74+
New patch version of npm available! 123.420.68 -> 123.420.69
75+
Changelog: https://github.com/npm/cli/releases/tag/v123.420.69
76+
Run npm install -g [email protected] to update!
77+

6078
`
6179

62-
exports[`test/lib/utils/update-notifier.js TAP notification situations unicode, no color major > must match snapshot 1`] = `
63-
New major version of npm available! <<major>>-beta.1 → 7.0.0
64-
Changelog: https://github.com/npm/cli/releases/tag/v7.0.0
65-
Run npm i -g npm to update!
80+
exports[`test/lib/utils/update-notifier.js TAP notification situations patch to current > no color 1`] = `
81+
82+
New patch version of npm available! 123.420.68 -> 123.420.69
83+
Changelog: <https://github.com/npm/cli/releases/tag/v123.420.69>
84+
Run \`npm install -g [email protected]\` to update!
85+
6686
`
6787

68-
exports[`test/lib/utils/update-notifier.js TAP notification situations unicode, no color minor > must match snapshot 1`] = `
69-
New minor version of npm available! <<minor>>-beta.1 → 7.0.0
70-
Changelog: https://github.com/npm/cli/releases/tag/v7.0.0
71-
Run npm i -g npm to update!
88+
exports[`test/lib/utils/update-notifier.js TAP notification situations patch to next version > color 1`] = `
89+

90+
New patch version of npm available! 123.421.69 -> 123.421.70
91+
Changelog: https://github.com/npm/cli/releases/tag/v123.421.70
92+
Run npm install -g [email protected] to update!
93+

7294
`
7395

74-
exports[`test/lib/utils/update-notifier.js TAP notification situations unicode, no color minor > must match snapshot 2`] = `
75-
New patch version of npm available! <<patch>>-beta.1 → 7.0.0
76-
Changelog: https://github.com/npm/cli/releases/tag/v7.0.0
77-
Run npm i -g npm to update!
96+
exports[`test/lib/utils/update-notifier.js TAP notification situations patch to next version > no color 1`] = `
97+
98+
New patch version of npm available! 123.421.69 -> 123.421.70
99+
Changelog: <https://github.com/npm/cli/releases/tag/v123.421.70>
100+
Run \`npm install -g [email protected]\` to update!
101+
78102
`

test/lib/npm.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,7 @@ t.test('npm.load', t => {
115115
t.match(npm, {
116116
loaded: true,
117117
loading: false,
118-
// flatOptions only loaded when we run an actual command
119-
flatOptions: null
118+
flatOptions: {}
120119
})
121120
t.equal(firstCalled, true, 'first callback got called')
122121
t.equal(secondCalled, true, 'second callback got called')

0 commit comments

Comments
 (0)