update-notifier.js 4.3 KB

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