diff options
-rwxr-xr-x | bin/npm-cli.js | 66 | ||||
-rw-r--r-- | lib/utils/update-notifier.js | 57 | ||||
-rw-r--r-- | tap-snapshots/test-lib-utils-update-notifier.js-TAP.test.js | 78 | ||||
-rw-r--r-- | test/lib/utils/update-notifier.js | 121 |
4 files changed, 260 insertions, 62 deletions
diff --git a/bin/npm-cli.js b/bin/npm-cli.js index d760e8cdb..7507ef40a 100755 --- a/bin/npm-cli.js +++ b/bin/npm-cli.js @@ -4,8 +4,7 @@ process.title = 'npm' const { checkForBrokenNode, - checkForUnsupportedNode, - checkVersion + checkForUnsupportedNode } = require('../lib/utils/unsupported.js') checkForBrokenNode() @@ -49,71 +48,14 @@ log.info('using', 'node@%s', process.version) process.on('uncaughtException', errorHandler) process.on('unhandledRejection', errorHandler) -const isGlobalNpmUpdate = npm => { - return npm.config.get('global') && - ['install', 'update'].includes(npm.command) && - npm.argv.includes('npm') -} - // now actually fire up npm and run the command. // this is how to use npm programmatically: conf._exit = true +const updateNotifier = require('../lib/utils/update-notifier.js') npm.load(conf, function (er) { if (er) return errorHandler(er) - if ( - !isGlobalNpmUpdate(npm) && - npm.config.get('update-notifier') && - !checkVersion(process.version).unsupported - ) { - // XXX move update notifier stuff into separate module - const pkg = require('../package.json') - let notifier = require('update-notifier')({pkg}) - // XXX should use @npmcli/ci-detect - const isCI = require('ci-info').isCI - if ( - notifier.update && - notifier.update.latest !== pkg.version && - !isCI - ) { - const color = require('ansicolors') - const useColor = npm.color - const useUnicode = npm.config.get('unicode') - const old = notifier.update.current - const latest = notifier.update.latest - let type = notifier.update.type - if (useColor) { - switch (type) { - case 'major': - type = color.red(type) - break - case 'minor': - type = color.yellow(type) - break - case 'patch': - type = color.green(type) - break - } - } - const changelog = `https://github.com/npm/cli/releases/tag/v${latest}` - notifier.notify({ - message: `New ${type} version of ${pkg.name} available! ${ - useColor ? color.red(old) : old - } ${useUnicode ? '→' : '->'} ${ - useColor ? color.green(latest) : latest - }\n` + - `${ - useColor ? color.yellow('Changelog:') : 'Changelog:' - } ${ - useColor ? color.cyan(changelog) : changelog - }\n` + - `Run ${ - useColor - ? color.green(`npm install -g ${pkg.name}`) - : `npm i -g ${pkg.name}` - } to update!` - }) - } - } + + updateNotifier(npm) const cmd = npm.argv.shift() const impl = npm.commands[cmd] diff --git a/lib/utils/update-notifier.js b/lib/utils/update-notifier.js new file mode 100644 index 000000000..a6abe948b --- /dev/null +++ b/lib/utils/update-notifier.js @@ -0,0 +1,57 @@ +// print a banner telling the user to upgrade npm to latest +// but not in CI, and not if we're doing that already. + +const isGlobalNpmUpdate = npm => { + return npm.config.get('global') && + ['install', 'update'].includes(npm.command) && + npm.argv.includes('npm') +} + +const { checkVersion } = require('./unsupported.js') + +module.exports = (npm) => { + if (!npm.config.get('update-notifier') || + isGlobalNpmUpdate(npm) || + checkVersion(process.version).unsupported) { + return + } + + const pkg = require('../../package.json') + const notifier = require('update-notifier')({pkg}) + const ciDetect = require('@npmcli/ci-detect') + if ( + notifier.update && + notifier.update.latest !== pkg.version && + !ciDetect() + ) { + const chalk = require('chalk') + const useColor = npm.color + const useUnicode = npm.config.get('unicode') + const old = notifier.update.current + const latest = notifier.update.latest + const type = notifier.update.type + const typec = !useColor ? type + : type === 'major' ? chalk.red(type) + : type === 'minor' ? chalk.yellow(type) + : chalk.green(type) + + const changelog = `https://github.com/npm/cli/releases/tag/v${latest}` + notifier.notify({ + message: `New ${typec} version of ${pkg.name} available! ${ + useColor ? chalk.red(old) : old + } ${useUnicode ? '→' : '->'} ${ + useColor ? chalk.green(latest) : latest + }\n` + + `${ + useColor ? chalk.yellow('Changelog:') : 'Changelog:' + } ${ + useColor ? chalk.cyan(changelog) : changelog + }\n` + + `Run ${ + useColor + ? chalk.green(`npm install -g ${pkg.name}`) + : `npm i -g ${pkg.name}` + } to update!` + }) + } +} diff --git a/tap-snapshots/test-lib-utils-update-notifier.js-TAP.test.js b/tap-snapshots/test-lib-utils-update-notifier.js-TAP.test.js new file mode 100644 index 000000000..2bc52e1fe --- /dev/null +++ b/tap-snapshots/test-lib-utils-update-notifier.js-TAP.test.js @@ -0,0 +1,78 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/lib/utils/update-notifier.js TAP notification situations color and unicode major > must match snapshot 1`] = ` +New [31mmajor[39m version of npm available! [31m<<major>>-beta[39m → [32m7.0.0[39m +[33mChangelog:[39m [36mhttps://github.com/npm/cli/releases/tag/v7.0.0[39m +Run [32mnpm install -g npm[39m to update! +` + +exports[`test/lib/utils/update-notifier.js TAP notification situations color and unicode minor > must match snapshot 1`] = ` +New [33mminor[39m version of npm available! [31m<<minor>>-beta[39m → [32m7.0.0[39m +[33mChangelog:[39m [36mhttps://github.com/npm/cli/releases/tag/v7.0.0[39m +Run [32mnpm install -g npm[39m to update! +` + +exports[`test/lib/utils/update-notifier.js TAP notification situations color and unicode minor > must match snapshot 2`] = ` +New [32mpatch[39m version of npm available! [31m<<patch>>-beta[39m → [32m7.0.0[39m +[33mChangelog:[39m [36mhttps://github.com/npm/cli/releases/tag/v7.0.0[39m +Run [32mnpm install -g npm[39m to update! +` + +exports[`test/lib/utils/update-notifier.js TAP notification situations color, no unicode major > must match snapshot 1`] = ` +New [31mmajor[39m version of npm available! [31m<<major>>-beta[39m -> [32m7.0.0[39m +[33mChangelog:[39m [36mhttps://github.com/npm/cli/releases/tag/v7.0.0[39m +Run [32mnpm install -g npm[39m to update! +` + +exports[`test/lib/utils/update-notifier.js TAP notification situations color, no unicode minor > must match snapshot 1`] = ` +New [33mminor[39m version of npm available! [31m<<minor>>-beta[39m -> [32m7.0.0[39m +[33mChangelog:[39m [36mhttps://github.com/npm/cli/releases/tag/v7.0.0[39m +Run [32mnpm install -g npm[39m to update! +` + +exports[`test/lib/utils/update-notifier.js TAP notification situations color, no unicode minor > must match snapshot 2`] = ` +New [32mpatch[39m version of npm available! [31m<<patch>>-beta[39m -> [32m7.0.0[39m +[33mChangelog:[39m [36mhttps://github.com/npm/cli/releases/tag/v7.0.0[39m +Run [32mnpm install -g npm[39m to update! +` + +exports[`test/lib/utils/update-notifier.js TAP notification situations no color, no unicode major > must match snapshot 1`] = ` +New major version of npm available! <<major>>-beta -> 7.0.0 +Changelog: https://github.com/npm/cli/releases/tag/v7.0.0 +Run npm i -g npm to update! +` + +exports[`test/lib/utils/update-notifier.js TAP notification situations no color, no unicode minor > must match snapshot 1`] = ` +New minor version of npm available! <<minor>>-beta -> 7.0.0 +Changelog: https://github.com/npm/cli/releases/tag/v7.0.0 +Run npm i -g npm to update! +` + +exports[`test/lib/utils/update-notifier.js TAP notification situations no color, no unicode minor > must match snapshot 2`] = ` +New patch version of npm available! <<patch>>-beta -> 7.0.0 +Changelog: https://github.com/npm/cli/releases/tag/v7.0.0 +Run npm i -g npm to update! +` + +exports[`test/lib/utils/update-notifier.js TAP notification situations unicode, no color major > must match snapshot 1`] = ` +New major version of npm available! <<major>>-beta → 7.0.0 +Changelog: https://github.com/npm/cli/releases/tag/v7.0.0 +Run npm i -g npm to update! +` + +exports[`test/lib/utils/update-notifier.js TAP notification situations unicode, no color minor > must match snapshot 1`] = ` +New minor version of npm available! <<minor>>-beta → 7.0.0 +Changelog: https://github.com/npm/cli/releases/tag/v7.0.0 +Run npm i -g npm to update! +` + +exports[`test/lib/utils/update-notifier.js TAP notification situations unicode, no color minor > must match snapshot 2`] = ` +New patch version of npm available! <<patch>>-beta → 7.0.0 +Changelog: https://github.com/npm/cli/releases/tag/v7.0.0 +Run npm i -g npm to update! +` diff --git a/test/lib/utils/update-notifier.js b/test/lib/utils/update-notifier.js new file mode 100644 index 000000000..c55a2c1d4 --- /dev/null +++ b/test/lib/utils/update-notifier.js @@ -0,0 +1,121 @@ +const t = require('tap') +const requireInject = require('require-inject') +const notifierMock = {} +const unsupportedMock = {} +const unsupported = () => unsupportedMock +let ciMock = null +const updateNotifier = requireInject.installGlobally('../../../lib/utils/update-notifier.js', { + 'update-notifier': () => notifierMock, + '../../../lib/utils/unsupported.js': { checkVersion: unsupported }, + '@npmcli/ci-detect': () => ciMock +}) +const { version } = require('../../../package.json') +const semver = require('semver') + +t.test('situations in which we do not notify', t => { + t.test('nothing to do if notifier disabled', t => { + Object.assign(notifierMock, { + notify: () => { throw new Error('should not notify!') }, + update: { latest: '99.99.99' }, + }) + updateNotifier({ + config: { get: (k) => k === 'update-notifier' ? false : true }, + }) + t.end() + }) + + t.test('do not suggest update if already updating', t => { + Object.assign(notifierMock, { + notify: () => { throw new Error('should not notify!') }, + update: { latest: '99.99.99' }, + }) + updateNotifier({ + config: { get: (k) => true }, + command: 'install', + argv: ['npm'] + }) + t.end() + }) + + t.test('do not suggest update if version if unsupported', t => { + t.teardown(() => { delete unsupportedMock.unsupported }) + unsupportedMock.unsupported = true + Object.assign(notifierMock, { + notify: () => { throw new Error('should not notify!') }, + update: { latest: '99.99.99' }, + }) + updateNotifier({ + config: { get: (k) => k !== 'global' }, + command: 'view', + argv: ['npm'] + }) + t.end() + }) + + t.test('do not update if nothing to update', t => { + Object.assign(notifierMock, { + notify: () => { throw new Error('should not notify!') }, + update: { latest: version }, + }) + updateNotifier({ + config: { get: (k) => k !== 'global' }, + command: 'view', + argv: ['npm'] + }) + t.end() + }) + + t.test('do not update in CI', t => { + t.teardown(() => { ciMock = null }) + ciMock = 'something' + Object.assign(notifierMock, { + notify: () => { throw new Error('should not notify!') }, + update: { latest: '99.99.99' }, + }) + updateNotifier({ + config: { get: (k) => k !== 'global' }, + command: 'view', + argv: ['npm'] + }) + t.end() + }) + + t.end() +}) + +t.test('notification situations', t => { + const runTests = (config, t) => { + t.plan(3) + t.test('major', t => runTest(config, 'major', t)) + t.test('minor', t => runTest(config, 'minor', t)) + t.test('minor', t => runTest(config, 'patch', t)) + } + + const runTest = (config, type, t) => { + t.plan(1) + const latest = type === 'major' ? semver.inc(version, 'major') + : type === 'minor' ? semver.inc(version, 'minor') + : type === 'patch' ? semver.inc(version, 'patch') + : null + + Object.assign(notifierMock, { + notify: ({ message }) => { + // swap this out so we don't have to regen snapshots each version bump + t.matchSnapshot(message.replace(latest, `<<${type}>>`)) + }, + update: { latest, type, current: version } + }) + updateNotifier({ + color: config.color, + config: { get: (k) => k === 'update-notifier' ? true : !!config[k] }, + command: 'view', + argv: ['npm'] + }) + } + + t.plan(4) + t.test('color and unicode', t => runTests({ unicode: true, color: true }, t)) + t.test('color, no unicode', t => runTests({ color: true }, t)) + t.test('unicode, no color', t => runTests({ unicode: true }, t)) + t.test('no color, no unicode', t => runTests({}, t)) +}) |