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