diff --git a/lib/cli/update-notifier.js b/lib/cli/update-notifier.js index 32cac18350be9..ffe51af1feea6 100644 --- a/lib/cli/update-notifier.js +++ b/lib/cli/update-notifier.js @@ -40,7 +40,7 @@ const updateCheck = async (npm, spec, version, current) => { // and should get the updates from that release train. // Note that this isn't another http request over the network, because // the packument will be cached by pacote from previous request. - if (gt(version, latest) && spec === 'latest') { + if (gt(version, latest) && spec === '*') { return updateNotifier(npm, `^${version}`) } @@ -71,7 +71,7 @@ const updateCheck = async (npm, spec, version, current) => { return message } -const updateNotifier = async (npm, spec = 'latest') => { +const updateNotifier = async (npm, spec = '*') => { // if we're on a prerelease train, then updates are coming fast // check for a new one daily. otherwise, weekly. const { version } = npm @@ -83,7 +83,7 @@ const updateNotifier = async (npm, spec = 'latest') => { } // while on a beta train, get updates daily - const duration = spec !== 'latest' ? DAILY : WEEKLY + const duration = current.prerelease.length ? DAILY : WEEKLY const t = new Date(Date.now() - duration) // if we don't have a file, then definitely check it. diff --git a/tap-snapshots/test/lib/cli/update-notifier.js.test.cjs b/tap-snapshots/test/lib/cli/update-notifier.js.test.cjs index 244d5216340f8..8736ee4623cd4 100644 --- a/tap-snapshots/test/lib/cli/update-notifier.js.test.cjs +++ b/tap-snapshots/test/lib/cli/update-notifier.js.test.cjs @@ -5,6 +5,14 @@ * Make sure to inspect the output below. Do not ignore changes! */ 'use strict' +exports[`test/lib/cli/update-notifier.js TAP notification situation with engine compatibility > must match snapshot 1`] = ` + +New minor version of npm available! 123.420.70 -> 123.421.60 +Changelog: https://github.com/npm/cli/releases/tag/v123.421.60 +To update run: npm install -g npm@123.421.60 + +` + exports[`test/lib/cli/update-notifier.js TAP notification situations 122.420.69 - color=always > must match snapshot 1`] = ` New major version of npm available! 122.420.69 -> 123.420.69 diff --git a/test/lib/cli/update-notifier.js b/test/lib/cli/update-notifier.js index 929e088bd4fa5..8e6761e5b7b44 100644 --- a/test/lib/cli/update-notifier.js +++ b/test/lib/cli/update-notifier.js @@ -2,21 +2,60 @@ const t = require('tap') const { basename } = require('node:path') const tmock = require('../../fixtures/tmock') const mockNpm = require('../../fixtures/mock-npm') +const MockRegistry = require('@npmcli/mock-registry') +const pacote = require('pacote') +const mockGlobals = require('@npmcli/mock-globals') const CURRENT_VERSION = '123.420.69' const CURRENT_MAJOR = '122.420.69' const CURRENT_MINOR = '123.419.69' const CURRENT_PATCH = '123.420.68' const NEXT_VERSION = '123.421.70' +const NEXT_VERSION_ENGINE_COMPATIBLE = '123.421.60' +const NEXT_VERSION_ENGINE_COMPATIBLE_MINOR = `123.420.70` +const NEXT_VERSION_ENGINE_COMPATIBLE_PATCH = `123.421.58` const NEXT_MINOR = '123.420.70' const NEXT_PATCH = '123.421.69' const CURRENT_BETA = '124.0.0-beta.99999' const HAVE_BETA = '124.0.0-beta.0' +const pacumentResponse = { + _id: 'npm', + name: 'npm', + 'dist-tags': { + latest: CURRENT_VERSION, + }, + access: 'public', + versions: { + [CURRENT_VERSION]: { version: CURRENT_VERSION, engines: { node: '>1' } }, + [CURRENT_MAJOR]: { version: CURRENT_MAJOR, engines: { node: '>1' } }, + [CURRENT_MINOR]: { version: CURRENT_MINOR, engines: { node: '>1' } }, + [CURRENT_PATCH]: { version: CURRENT_PATCH, engines: { node: '>1' } }, + [NEXT_VERSION]: { version: NEXT_VERSION, engines: { node: '>1' } }, + [NEXT_MINOR]: { version: NEXT_MINOR, engines: { node: '>1' } }, + [NEXT_PATCH]: { version: NEXT_PATCH, engines: { node: '>1' } }, + [CURRENT_BETA]: { version: CURRENT_BETA, engines: { node: '>1' } }, + [HAVE_BETA]: { version: HAVE_BETA, engines: { node: '>1' } }, + [NEXT_VERSION_ENGINE_COMPATIBLE]: { + version: NEXT_VERSION_ENGINE_COMPATIBLE, + engiges: { node: '<=1' }, + }, + [NEXT_VERSION_ENGINE_COMPATIBLE_MINOR]: { + version: NEXT_VERSION_ENGINE_COMPATIBLE_MINOR, + engines: { node: '<=1' }, + }, + [NEXT_VERSION_ENGINE_COMPATIBLE_PATCH]: { + version: NEXT_VERSION_ENGINE_COMPATIBLE_PATCH, + engines: { node: '<=1' }, + }, + }, +} + const runUpdateNotifier = async (t, { STAT_ERROR, WRITE_ERROR, PACOTE_ERROR, + PACOTE_MOCK_REQ_COUNT = 1, STAT_MTIME = 0, mocks: _mocks = {}, command = 'help', @@ -53,7 +92,7 @@ const runUpdateNotifier = async (t, { const MANIFEST_REQUEST = [] const mockPacote = { - manifest: async (spec) => { + manifest: async (spec, ...opts) => { if (!spec.match(/^npm@/)) { t.fail('no pacote manifest allowed for non npm packages') } @@ -61,9 +100,7 @@ const runUpdateNotifier = async (t, { if (PACOTE_ERROR) { throw PACOTE_ERROR } - const manifestV = spec === 'npm@latest' ? CURRENT_VERSION - : /-/.test(spec) ? CURRENT_BETA : NEXT_VERSION - return { version: manifestV } + return pacote.manifest(spec, ...opts) }, } @@ -83,6 +120,15 @@ const runUpdateNotifier = async (t, { prefixDir, argv, }) + const registry = new MockRegistry({ + tap: t, + registry: mock.npm.config.get('registry'), + }) + + if (PACOTE_MOCK_REQ_COUNT > 0) { + registry.nock.get('/npm').times(PACOTE_MOCK_REQ_COUNT).reply(200, pacumentResponse) + } + const updateNotifier = tmock(t, '{LIB}/cli/update-notifier.js', mocks) const result = await updateNotifier(mock.npm) @@ -104,6 +150,7 @@ t.test('duration has elapsed, no updates', async t => { t.test('situations in which we do not notify', t => { t.test('nothing to do if notifier disabled', async t => { const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { + PACOTE_MOCK_REQ_COUNT: 0, 'update-notifier': false, }) t.equal(wroteFile, false) @@ -113,6 +160,7 @@ t.test('situations in which we do not notify', t => { t.test('do not suggest update if already updating', async t => { const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { + PACOTE_MOCK_REQ_COUNT: 0, command: 'install', prefixDir: { 'package.json': `{"name":"${t.testName}"}` }, argv: ['npm'], @@ -125,6 +173,7 @@ t.test('situations in which we do not notify', t => { t.test('do not suggest update if already updating with spec', async t => { const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { + PACOTE_MOCK_REQ_COUNT: 0, command: 'install', prefixDir: { 'package.json': `{"name":"${t.testName}"}` }, argv: ['npm@latest'], @@ -139,28 +188,30 @@ t.test('situations in which we do not notify', t => { const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t) t.equal(wroteFile, true) t.equal(result, null) - t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') + t.strictSame(MANIFEST_REQUEST, ['npm@*'], 'requested latest version') }) t.test('check if stat errors (here for coverage)', async t => { const STAT_ERROR = new Error('blorg') const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { STAT_ERROR }) t.equal(wroteFile, true) t.equal(result, null) - t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') + t.strictSame(MANIFEST_REQUEST, ['npm@*'], 'requested latest version') }) t.test('ok if write errors (here for coverage)', async t => { const WRITE_ERROR = new Error('grolb') const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { WRITE_ERROR }) t.equal(wroteFile, true) t.equal(result, null) - t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') + t.strictSame(MANIFEST_REQUEST, ['npm@*'], 'requested latest version') }) t.test('ignore pacote failures (here for coverage)', async t => { const PACOTE_ERROR = new Error('pah-KO-tchay') - const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { PACOTE_ERROR }) + const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { + PACOTE_ERROR, PACOTE_MOCK_REQ_COUNT: 0, + }) t.equal(result, null) t.equal(wroteFile, true) - t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') + t.strictSame(MANIFEST_REQUEST, ['npm@*'], 'requested latest version') }) t.test('do not update if newer than latest, but same as next', async t => { const { @@ -170,7 +221,7 @@ t.test('situations in which we do not notify', t => { } = await runUpdateNotifier(t, { version: NEXT_VERSION }) t.equal(result, null) t.equal(wroteFile, true) - const reqs = ['npm@latest', `npm@^${NEXT_VERSION}`] + const reqs = ['npm@*', `npm@^${NEXT_VERSION}`] t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions') }) t.test('do not update if on the latest beta', async t => { @@ -188,7 +239,8 @@ t.test('situations in which we do not notify', t => { t.test('do not update in CI', async t => { const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { mocks: { 'ci-info': { isCI: true, name: 'something' }, - } }) + }, + PACOTE_MOCK_REQ_COUNT: 0 }) t.equal(wroteFile, false) t.equal(result, null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') @@ -197,7 +249,10 @@ t.test('situations in which we do not notify', t => { t.test('only check weekly for GA releases', async t => { // One week (plus five minutes to account for test environment fuzziness) const STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 * 7 + 1000 * 60 * 5 - const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { STAT_MTIME }) + const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { + STAT_MTIME, + PACOTE_MOCK_REQ_COUNT: 0, + }) t.equal(wroteFile, false, 'duration was not reset') t.equal(result, null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') @@ -210,7 +265,7 @@ t.test('situations in which we do not notify', t => { wroteFile, result, MANIFEST_REQUEST, - } = await runUpdateNotifier(t, { STAT_MTIME, version: HAVE_BETA }) + } = await runUpdateNotifier(t, { STAT_MTIME, version: HAVE_BETA, PACOTE_MOCK_REQ_COUNT: 0 }) t.equal(wroteFile, false, 'duration was not reset') t.equal(result, null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') @@ -222,11 +277,11 @@ t.test('situations in which we do not notify', t => { t.test('notification situations', async t => { const cases = { [HAVE_BETA]: [`^{V}`], - [NEXT_PATCH]: [`latest`, `^{V}`], - [NEXT_MINOR]: [`latest`, `^{V}`], - [CURRENT_PATCH]: ['latest'], - [CURRENT_MINOR]: ['latest'], - [CURRENT_MAJOR]: ['latest'], + [NEXT_PATCH]: [`*`, `^{V}`], + [NEXT_MINOR]: [`*`, `^{V}`], + [CURRENT_PATCH]: ['*'], + [CURRENT_MINOR]: ['*'], + [CURRENT_MAJOR]: ['*'], } for (const [version, reqs] of Object.entries(cases)) { @@ -236,7 +291,7 @@ t.test('notification situations', async t => { wroteFile, result, MANIFEST_REQUEST, - } = await runUpdateNotifier(t, { version, color }) + } = await runUpdateNotifier(t, { version, color, PACOTE_MOCK_REQ_COUNT: reqs.length }) t.matchSnapshot(result) t.equal(wroteFile, true) t.strictSame(MANIFEST_REQUEST, reqs.map(r => `npm@${r.replace('{V}', version)}`)) @@ -244,3 +299,19 @@ t.test('notification situations', async t => { } } }) + +t.test('notification situation with engine compatibility', async t => { + mockGlobals(t, { 'process.version': 'v1.0.0' }, { replace: true }) + + const { + wroteFile, + result, + MANIFEST_REQUEST, + } = await runUpdateNotifier(t, { + version: NEXT_VERSION_ENGINE_COMPATIBLE_MINOR, + PACOTE_MOCK_REQ_COUNT: 1 }) + + t.matchSnapshot(result) + t.equal(wroteFile, true) + t.strictSame(MANIFEST_REQUEST, [`npm@*`]) +})