• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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