Skip to content

Commit e345cc5

Browse files
authored
fix: don't suggest npm update outside of valid engine range (#8050)
When doing manifest call for fetching npm manifest it was using latest every-time, however fetching`npm@*` will still give `latest` if it's satisfies the engine range otherwise it will give highest matching version for the current engine range. Pacote.manifest calls internally uses pick-manifest logic to pick the appropriate version of the manifest for the engine range.
1 parent 9dc40e6 commit e345cc5

File tree

3 files changed

+102
-61
lines changed

3 files changed

+102
-61
lines changed

lib/cli/update-notifier.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const updateCheck = async (npm, spec, version, current) => {
4040
// and should get the updates from that release train.
4141
// Note that this isn't another http request over the network, because
4242
// the packument will be cached by pacote from previous request.
43-
if (gt(version, latest) && spec === 'latest') {
43+
if (gt(version, latest) && spec === '*') {
4444
return updateNotifier(npm, `^${version}`)
4545
}
4646

@@ -71,7 +71,7 @@ const updateCheck = async (npm, spec, version, current) => {
7171
return message
7272
}
7373

74-
const updateNotifier = async (npm, spec = 'latest') => {
74+
const updateNotifier = async (npm, spec = '*') => {
7575
// if we're on a prerelease train, then updates are coming fast
7676
// check for a new one daily. otherwise, weekly.
7777
const { version } = npm
@@ -83,7 +83,7 @@ const updateNotifier = async (npm, spec = 'latest') => {
8383
}
8484

8585
// while on a beta train, get updates daily
86-
const duration = spec !== 'latest' ? DAILY : WEEKLY
86+
const duration = current.prerelease.length ? DAILY : WEEKLY
8787

8888
const t = new Date(Date.now() - duration)
8989
// if we don't have a file, then definitely check it.

tap-snapshots/test/lib/cli/update-notifier.js.test.cjs

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55
* Make sure to inspect the output below. Do not ignore changes!
66
*/
77
'use strict'
8+
exports[`test/lib/cli/update-notifier.js TAP notification situation with engine compatibility > must match snapshot 1`] = `
9+
10+
New minor version of npm available! 123.420.70 -> 123.421.60
11+
Changelog: https://github.com/npm/cli/releases/tag/v123.421.60
12+
To update run: npm install -g [email protected]
13+
14+
`
15+
816
exports[`test/lib/cli/update-notifier.js TAP notification situations 122.420.69 - color=always > must match snapshot 1`] = `
917
1018
New major version of npm available! 122.420.69 -> 123.420.69

test/lib/cli/update-notifier.js

+91-58
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,59 @@ const t = require('tap')
22
const { basename } = require('node:path')
33
const tmock = require('../../fixtures/tmock')
44
const mockNpm = require('../../fixtures/mock-npm')
5+
const MockRegistry = require('@npmcli/mock-registry')
6+
const mockGlobals = require('@npmcli/mock-globals')
57

68
const CURRENT_VERSION = '123.420.69'
79
const CURRENT_MAJOR = '122.420.69'
810
const CURRENT_MINOR = '123.419.69'
911
const CURRENT_PATCH = '123.420.68'
1012
const NEXT_VERSION = '123.421.70'
13+
const NEXT_VERSION_ENGINE_COMPATIBLE = '123.421.60'
14+
const NEXT_VERSION_ENGINE_COMPATIBLE_MINOR = `123.420.70`
15+
const NEXT_VERSION_ENGINE_COMPATIBLE_PATCH = `123.421.58`
1116
const NEXT_MINOR = '123.420.70'
1217
const NEXT_PATCH = '123.421.69'
1318
const CURRENT_BETA = '124.0.0-beta.99999'
1419
const HAVE_BETA = '124.0.0-beta.0'
1520

21+
const packumentResponse = {
22+
_id: 'npm',
23+
name: 'npm',
24+
'dist-tags': {
25+
latest: CURRENT_VERSION,
26+
},
27+
access: 'public',
28+
versions: {
29+
[CURRENT_VERSION]: { version: CURRENT_VERSION, engines: { node: '>1' } },
30+
[CURRENT_MAJOR]: { version: CURRENT_MAJOR, engines: { node: '>1' } },
31+
[CURRENT_MINOR]: { version: CURRENT_MINOR, engines: { node: '>1' } },
32+
[CURRENT_PATCH]: { version: CURRENT_PATCH, engines: { node: '>1' } },
33+
[NEXT_VERSION]: { version: NEXT_VERSION, engines: { node: '>1' } },
34+
[NEXT_MINOR]: { version: NEXT_MINOR, engines: { node: '>1' } },
35+
[NEXT_PATCH]: { version: NEXT_PATCH, engines: { node: '>1' } },
36+
[CURRENT_BETA]: { version: CURRENT_BETA, engines: { node: '>1' } },
37+
[HAVE_BETA]: { version: HAVE_BETA, engines: { node: '>1' } },
38+
[NEXT_VERSION_ENGINE_COMPATIBLE]: {
39+
version: NEXT_VERSION_ENGINE_COMPATIBLE,
40+
engiges: { node: '<=1' },
41+
},
42+
[NEXT_VERSION_ENGINE_COMPATIBLE_MINOR]: {
43+
version: NEXT_VERSION_ENGINE_COMPATIBLE_MINOR,
44+
engines: { node: '<=1' },
45+
},
46+
[NEXT_VERSION_ENGINE_COMPATIBLE_PATCH]: {
47+
version: NEXT_VERSION_ENGINE_COMPATIBLE_PATCH,
48+
engines: { node: '<=1' },
49+
},
50+
},
51+
}
52+
1653
const runUpdateNotifier = async (t, {
1754
STAT_ERROR,
1855
WRITE_ERROR,
1956
PACOTE_ERROR,
57+
PACOTE_MOCK_REQ_COUNT = 1,
2058
STAT_MTIME = 0,
2159
mocks: _mocks = {},
2260
command = 'help',
@@ -51,24 +89,7 @@ const runUpdateNotifier = async (t, {
5189
},
5290
}
5391

54-
const MANIFEST_REQUEST = []
55-
const mockPacote = {
56-
manifest: async (spec) => {
57-
if (!spec.match(/^npm@/)) {
58-
t.fail('no pacote manifest allowed for non npm packages')
59-
}
60-
MANIFEST_REQUEST.push(spec)
61-
if (PACOTE_ERROR) {
62-
throw PACOTE_ERROR
63-
}
64-
const manifestV = spec === 'npm@latest' ? CURRENT_VERSION
65-
: /-/.test(spec) ? CURRENT_BETA : NEXT_VERSION
66-
return { version: manifestV }
67-
},
68-
}
69-
7092
const mocks = {
71-
pacote: mockPacote,
7293
'node:fs/promises': mockFs,
7394
'{ROOT}/package.json': { version },
7495
'ci-info': { isCI: false, name: null },
@@ -83,124 +104,125 @@ const runUpdateNotifier = async (t, {
83104
prefixDir,
84105
argv,
85106
})
107+
const registry = new MockRegistry({
108+
tap: t,
109+
registry: mock.npm.config.get('registry'),
110+
})
111+
112+
if (PACOTE_MOCK_REQ_COUNT > 0) {
113+
registry.nock.get('/npm').times(PACOTE_MOCK_REQ_COUNT).reply(200, packumentResponse)
114+
}
115+
86116
const updateNotifier = tmock(t, '{LIB}/cli/update-notifier.js', mocks)
87117

88118
const result = await updateNotifier(mock.npm)
89119

90120
return {
91121
wroteFile,
92122
result,
93-
MANIFEST_REQUEST,
94123
}
95124
}
96125

97126
t.test('duration has elapsed, no updates', async t => {
98-
const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t)
127+
const { wroteFile, result } = await runUpdateNotifier(t)
99128
t.equal(wroteFile, true)
100129
t.not(result)
101-
t.equal(MANIFEST_REQUEST.length, 1)
102130
})
103131

104132
t.test('situations in which we do not notify', t => {
105133
t.test('nothing to do if notifier disabled', async t => {
106-
const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, {
134+
const { wroteFile, result } = await runUpdateNotifier(t, {
135+
PACOTE_MOCK_REQ_COUNT: 0,
107136
'update-notifier': false,
108137
})
109138
t.equal(wroteFile, false)
110139
t.equal(result, null)
111-
t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests')
112140
})
113141

114142
t.test('do not suggest update if already updating', async t => {
115-
const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, {
143+
const { wroteFile, result } = await runUpdateNotifier(t, {
144+
PACOTE_MOCK_REQ_COUNT: 0,
116145
command: 'install',
117146
prefixDir: { 'package.json': `{"name":"${t.testName}"}` },
118147
argv: ['npm'],
119148
global: true,
120149
})
121150
t.equal(wroteFile, false)
122151
t.equal(result, null)
123-
t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests')
124152
})
125153

126154
t.test('do not suggest update if already updating with spec', async t => {
127-
const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, {
155+
const { wroteFile, result } = await runUpdateNotifier(t, {
156+
PACOTE_MOCK_REQ_COUNT: 0,
128157
command: 'install',
129158
prefixDir: { 'package.json': `{"name":"${t.testName}"}` },
130159
argv: ['npm@latest'],
131160
global: true,
132161
})
133162
t.equal(wroteFile, false)
134163
t.equal(result, null)
135-
t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests')
136164
})
137165

138166
t.test('do not update if same as latest', async t => {
139-
const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t)
167+
const { wroteFile, result } = await runUpdateNotifier(t)
140168
t.equal(wroteFile, true)
141169
t.equal(result, null)
142-
t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version')
143170
})
144171
t.test('check if stat errors (here for coverage)', async t => {
145172
const STAT_ERROR = new Error('blorg')
146-
const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { STAT_ERROR })
173+
const { wroteFile, result } = await runUpdateNotifier(t, { STAT_ERROR })
147174
t.equal(wroteFile, true)
148175
t.equal(result, null)
149-
t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version')
150176
})
151177
t.test('ok if write errors (here for coverage)', async t => {
152178
const WRITE_ERROR = new Error('grolb')
153-
const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { WRITE_ERROR })
179+
const { wroteFile, result } = await runUpdateNotifier(t, { WRITE_ERROR })
154180
t.equal(wroteFile, true)
155181
t.equal(result, null)
156-
t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version')
157182
})
158183
t.test('ignore pacote failures (here for coverage)', async t => {
159184
const PACOTE_ERROR = new Error('pah-KO-tchay')
160-
const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { PACOTE_ERROR })
185+
const { wroteFile, result } = await runUpdateNotifier(t, {
186+
PACOTE_ERROR, PACOTE_MOCK_REQ_COUNT: 0,
187+
})
161188
t.equal(result, null)
162189
t.equal(wroteFile, true)
163-
t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version')
164190
})
165191
t.test('do not update if newer than latest, but same as next', async t => {
166192
const {
167193
wroteFile,
168194
result,
169-
MANIFEST_REQUEST,
170195
} = await runUpdateNotifier(t, { version: NEXT_VERSION })
171196
t.equal(result, null)
172197
t.equal(wroteFile, true)
173-
const reqs = ['npm@latest', `npm@^${NEXT_VERSION}`]
174-
t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions')
175198
})
176199
t.test('do not update if on the latest beta', async t => {
177200
const {
178201
wroteFile,
179202
result,
180-
MANIFEST_REQUEST,
181203
} = await runUpdateNotifier(t, { version: CURRENT_BETA })
182204
t.equal(result, null)
183205
t.equal(wroteFile, true)
184-
const reqs = [`npm@^${CURRENT_BETA}`]
185-
t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions')
186206
})
187207

188208
t.test('do not update in CI', async t => {
189-
const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { mocks: {
209+
const { wroteFile, result } = await runUpdateNotifier(t, { mocks: {
190210
'ci-info': { isCI: true, name: 'something' },
191-
} })
211+
},
212+
PACOTE_MOCK_REQ_COUNT: 0 })
192213
t.equal(wroteFile, false)
193214
t.equal(result, null)
194-
t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests')
195215
})
196216

197217
t.test('only check weekly for GA releases', async t => {
198218
// One week (plus five minutes to account for test environment fuzziness)
199219
const STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 * 7 + 1000 * 60 * 5
200-
const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { STAT_MTIME })
220+
const { wroteFile, result } = await runUpdateNotifier(t, {
221+
STAT_MTIME,
222+
PACOTE_MOCK_REQ_COUNT: 0,
223+
})
201224
t.equal(wroteFile, false, 'duration was not reset')
202225
t.equal(result, null)
203-
t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests')
204226
})
205227

206228
t.test('only check daily for betas', async t => {
@@ -209,37 +231,48 @@ t.test('situations in which we do not notify', t => {
209231
const {
210232
wroteFile,
211233
result,
212-
MANIFEST_REQUEST,
213-
} = await runUpdateNotifier(t, { STAT_MTIME, version: HAVE_BETA })
234+
} = await runUpdateNotifier(t, { STAT_MTIME, version: HAVE_BETA, PACOTE_MOCK_REQ_COUNT: 0 })
214235
t.equal(wroteFile, false, 'duration was not reset')
215236
t.equal(result, null)
216-
t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests')
217237
})
218238

219239
t.end()
220240
})
221241

242+
t.test('notification situation with engine compatibility', async t => {
243+
// no version which are greater than node 1.0.0 should be selected.
244+
mockGlobals(t, { 'process.version': 'v1.0.0' }, { replace: true })
245+
246+
const {
247+
wroteFile,
248+
result,
249+
} = await runUpdateNotifier(t, {
250+
version: NEXT_VERSION_ENGINE_COMPATIBLE_MINOR,
251+
PACOTE_MOCK_REQ_COUNT: 1 })
252+
253+
t.matchSnapshot(result)
254+
t.equal(wroteFile, true)
255+
})
256+
222257
t.test('notification situations', async t => {
223258
const cases = {
224-
[HAVE_BETA]: [`^{V}`],
225-
[NEXT_PATCH]: [`latest`, `^{V}`],
226-
[NEXT_MINOR]: [`latest`, `^{V}`],
227-
[CURRENT_PATCH]: ['latest'],
228-
[CURRENT_MINOR]: ['latest'],
229-
[CURRENT_MAJOR]: ['latest'],
259+
[HAVE_BETA]: 1,
260+
[NEXT_PATCH]: 2,
261+
[NEXT_MINOR]: 2,
262+
[CURRENT_PATCH]: 1,
263+
[CURRENT_MINOR]: 1,
264+
[CURRENT_MAJOR]: 1,
230265
}
231266

232-
for (const [version, reqs] of Object.entries(cases)) {
267+
for (const [version, requestCount] of Object.entries(cases)) {
233268
for (const color of [false, 'always']) {
234269
await t.test(`${version} - color=${color}`, async t => {
235270
const {
236271
wroteFile,
237272
result,
238-
MANIFEST_REQUEST,
239-
} = await runUpdateNotifier(t, { version, color })
273+
} = await runUpdateNotifier(t, { version, color, PACOTE_MOCK_REQ_COUNT: requestCount })
240274
t.matchSnapshot(result)
241275
t.equal(wroteFile, true)
242-
t.strictSame(MANIFEST_REQUEST, reqs.map(r => `npm@${r.replace('{V}', version)}`))
243276
})
244277
}
245278
}

0 commit comments

Comments
 (0)