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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 8 additions & 0 deletions
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 npm@123.421.60
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

Lines changed: 91 additions & 58 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy