update-notifier.js 3.8 KB

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