1// print a banner telling the user to upgrade npm to latest 2// but not in CI, and not if we're doing that already. 3// Check daily for betas, and weekly otherwise. 4 5const ciInfo = require('ci-info') 6const semver = require('semver') 7const { stat, writeFile } = require('fs/promises') 8const { resolve } = require('path') 9 10// update check frequency 11const DAILY = 1000 * 60 * 60 * 24 12const WEEKLY = DAILY * 7 13 14// don't put it in the _cacache folder, just in npm's cache 15const lastCheckedFile = npm => 16 resolve(npm.flatOptions.cache, '../_update-notifier-last-checked') 17 18// Actual check for updates. This is a separate function so that we only load 19// this if we are doing the actual update 20const updateCheck = async (npm, spec, version, current) => { 21 const pacote = require('pacote') 22 23 const mani = await pacote.manifest(`npm@${spec}`, { 24 // always prefer latest, even if doing --tag=whatever on the cmd 25 defaultTag: 'latest', 26 ...npm.flatOptions, 27 }).catch(() => null) 28 29 // if pacote failed, give up 30 if (!mani) { 31 return null 32 } 33 34 const latest = mani.version 35 36 // if the current version is *greater* than latest, we're on a 'next' 37 // and should get the updates from that release train. 38 // Note that this isn't another http request over the network, because 39 // the packument will be cached by pacote from previous request. 40 if (semver.gt(version, latest) && spec === 'latest') { 41 return updateNotifier(npm, `^${version}`) 42 } 43 44 // if we already have something >= the desired spec, then we're done 45 if (semver.gte(version, latest)) { 46 return null 47 } 48 49 const useColor = npm.logColor 50 const chalk = npm.logChalk 51 52 // ok! notify the user about this update they should get. 53 // The message is saved for printing at process exit so it will not get 54 // lost in any other messages being printed as part of the command. 55 const update = semver.parse(mani.version) 56 const type = update.major !== current.major ? 'major' 57 : update.minor !== current.minor ? 'minor' 58 : update.patch !== current.patch ? 'patch' 59 : 'prerelease' 60 const typec = type === 'major' ? chalk.red(type) 61 : type === 'minor' ? chalk.yellow(type) 62 : chalk.green(type) 63 const oldc = chalk.red(current) 64 const latestc = chalk.green(latest) 65 const changelog = `https://github.com/npm/cli/releases/tag/v${latest}` 66 const changelogc = !useColor ? `<${changelog}>` : chalk.cyan(changelog) 67 const cmd = `npm install -g npm@${latest}` 68 const cmdc = !useColor ? `\`${cmd}\`` : chalk.green(cmd) 69 const message = `\nNew ${typec} version of npm available! ` + 70 `${oldc} -> ${latestc}\n` + 71 `Changelog: ${changelogc}\n` + 72 `Run ${cmdc} to update!\n` 73 74 return message 75} 76 77const updateNotifier = async (npm, spec = 'latest') => { 78 // if we're on a prerelease train, then updates are coming fast 79 // check for a new one daily. otherwise, weekly. 80 const { version } = npm 81 const current = semver.parse(version) 82 83 // if we're on a beta train, always get the next beta 84 if (current.prerelease.length) { 85 spec = `^${version}` 86 } 87 88 // while on a beta train, get updates daily 89 const duration = spec !== 'latest' ? DAILY : WEEKLY 90 91 const t = new Date(Date.now() - duration) 92 // if we don't have a file, then definitely check it. 93 const st = await stat(lastCheckedFile(npm)).catch(() => ({ mtime: t - 1 })) 94 95 // if we've already checked within the specified duration, don't check again 96 if (!(t > st.mtime)) { 97 return null 98 } 99 100 return updateCheck(npm, spec, version, current) 101} 102 103// only update the notification timeout if we actually finished checking 104module.exports = async npm => { 105 if ( 106 // opted out 107 !npm.config.get('update-notifier') 108 // global npm update 109 || (npm.flatOptions.global && 110 ['install', 'update'].includes(npm.command) && 111 npm.argv.some(arg => /^npm(@|$)/.test(arg))) 112 // CI 113 || ciInfo.isCI 114 ) { 115 return null 116 } 117 118 const notification = await updateNotifier(npm) 119 120 // intentional. do not await this. it's a best-effort update. if this 121 // fails, it's ok. might be using /dev/null as the cache or something weird 122 // like that. 123 writeFile(lastCheckedFile(npm), '').catch(() => {}) 124 return notification 125} 126