Skip to content

Commit a0c4e64

Browse files
authored
fix: handle registries missing 'time' metadata during version resolution (#1763)
* fix: improve version resolution when registry time is missing - for 'newest' target return greatest when time is missing - don't skip by cooldown versions with no time - print [missing time] next to the upgraded version * Add cli test do not warn about empty results when every dep is already at the highest version for non-latest --target * safer semver.minVersion calls
1 parent 4a83d68 commit a0c4e64

5 files changed

Lines changed: 428 additions & 92 deletions

File tree

src/lib/logging.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,17 @@ export async function toDependencyTable({
242242
const diffUrl = format?.includes('diff')
243243
? `${process.env.NCU_DIFF || 'https://npmdiff.dev'}/${encodeURIComponent(dep)}/${from.replace(/^\W+/, '')}/${to.replace(/^\W+/, '')}`
244244
: ''
245-
const timestamp = format?.includes('time') && time?.[dep] ? time[dep] : null
246-
const publishTime = timestamp ? timeAgoFormat(timestamp, 'en_US') : ''
245+
246+
const showCoolDown = format?.includes('cooldown')
247+
const showTime = format?.includes('time')
248+
// show '[missing time]' in publishTime column or cooldown column
249+
const missingTime = time?.[dep] ? '' : '[missing time]'
250+
const timestamp = showTime && time?.[dep] ? time[dep] : null
251+
const publishTime = timestamp
252+
? timeAgoFormat(timestamp, 'en_US')
253+
: showTime || !showCooldownCol
254+
? missingTime
255+
: ''
247256

248257
const cooldownVersion = skippedByCooldown?.[dep]?.version
249258
let cooldown = ''
@@ -256,6 +265,8 @@ export async function toDependencyTable({
256265
coerced && !cooldownVersion.endsWith(coerced.version) ? `${coerced.version}-+` : cooldownVersion
257266
const skippedColorized = colorizeDiff(to, wildcard + getVersion(shortended))
258267
cooldown = `[cooldown] ${skippedColorized.replace(wildcard, '')}`
268+
} else if (showCoolDown && !showTime) {
269+
cooldown = missingTime
259270
}
260271

261272
return [
@@ -302,7 +313,7 @@ async function printSkippedByCooldownTable({
302313

303314
for (const params of Object.values(skippedByCooldown)) {
304315
const { name, version, currentVersion, fallbackVersion, time: versionTime } = params
305-
if (!isFetchable(currentVersion)) continue
316+
if (!isFetchable(currentVersion) || !version) continue
306317

307318
const wildcard = WILDCARDS.includes(currentVersion[0]) ? currentVersion[0] : ''
308319
const caf = wildcard + stripRange(fallbackVersion ?? currentVersion)

src/package-managers/filters.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ export const satisfiesCooldownPeriod = (
7575
if (!version) return false
7676

7777
if (!cooldownDaysOrPredicateFn) return true
78-
if (!versionTimeData) return false
78+
// when there is no time to check wh can not check it for cooldown, always return true
79+
if (!versionTimeData) return true
7980

8081
const versionReleaseDate = new Date(versionTimeData)
8182
const DAY_AS_MS = 86400000 // milliseconds in a day

src/package-managers/npm.ts

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,16 @@ const findTargetAndFallback = ({
162162
compare?: (v1: string, v2: string) => number
163163
}): GreatestWithFallbackResult => {
164164
const isValidVersion = filterPredicate(options)
165-
const cur = nodeSemver.minVersion(currentVersion)?.version
165+
// nodeSemver.minVersion throws on non-semver specs (e.g. catalog: or workspace:)
166+
const cur = nodeSemver.validRange(currentVersion) ? nodeSemver.minVersion(currentVersion)?.version : null
166167
if (!cur) {
167168
return {
168169
targetVersion: null,
169170
fallbackVersion: null,
170171
targetBlockedByCooldown: false,
171172
}
172173
}
174+
const currentInCooldown = !satisfiesCooldownPeriod(packageName, cur, time?.[cur], options.cooldown)
173175

174176
const result = versions.reduce(
175177
(acc, versionData) => {
@@ -204,15 +206,15 @@ const findTargetAndFallback = ({
204206
{
205207
targetVersion: cur,
206208
fallbackVersion: cur,
207-
targetBlockedByCooldown: false,
209+
targetBlockedByCooldown: currentInCooldown,
208210
} as {
209211
targetVersion: string
210212
fallbackVersion: string
211213
targetBlockedByCooldown: boolean
212214
},
213215
)
214216

215-
let targetVersion: string | null = result.targetVersion
217+
const targetVersion: string | null = result.targetVersion
216218
let fallbackVersion: string | null = result.fallbackVersion
217219

218220
if (fallbackVersion === result.targetVersion) {
@@ -223,8 +225,9 @@ const findTargetAndFallback = ({
223225
fallbackVersion = null
224226
}
225227

226-
if (!nodeSemver.gt(targetVersion, cur)) {
227-
targetVersion = null
228+
// don't "Block" current version
229+
if (currentInCooldown && targetVersion === cur) {
230+
result.targetBlockedByCooldown = false
228231
}
229232

230233
return {
@@ -263,7 +266,7 @@ const toVersionResult = ({
263266

264267
// only check fallback if target is in cooldown period.
265268
if (options.cooldown && targetVersion && targetBlockedByCooldown) {
266-
const current = nodeSemver.minVersion(currentVersion)?.version
269+
const current = nodeSemver.validRange(currentVersion) ? nodeSemver.minVersion(currentVersion)?.version : null
267270
const cooldownInfo: CooldownInfo = {
268271
name: packageName,
269272
currentVersion,
@@ -562,7 +565,10 @@ npmApi.mockFetchUpgradedPackument =
562565
)
563566
}
564567

565-
const time = (isPackument(partialPackument) && partialPackument.time?.[version]) || new Date().toISOString()
568+
const time =
569+
isPackument(partialPackument) && partialPackument.time
570+
? partialPackument.time?.[version]
571+
: new Date().toISOString()
566572
const packument: Packument = {
567573
name,
568574
'dist-tags': {
@@ -1005,9 +1011,11 @@ export const distTag: GetVersion = async (
10051011

10061012
const publishTime = packument?.time?.[version!]
10071013
const maybeTime = publishTime ? { time: publishTime } : null
1014+
const current = (nodeSemver.validRange(currentVersion) && nodeSemver.minVersion(currentVersion)?.version) || '0.0.0'
10081015

10091016
const isSatisfiesCooldown =
1010-
tagPackument && satisfiesCooldownPeriod(packageName, tagPackument.version, publishTime, options.cooldown)
1017+
tagPackument.version === current ||
1018+
(tagPackument && satisfiesCooldownPeriod(packageName, tagPackument.version, publishTime, options.cooldown))
10111019

10121020
// latest should not be deprecated
10131021
// if latest exists and latest is not a prerelease version, return it
@@ -1031,20 +1039,15 @@ export const distTag: GetVersion = async (
10311039
)
10321040
}
10331041

1034-
const current = nodeSemver.minVersion(currentVersion)?.version ?? '0.0.0'
1035-
if (nodeSemver.gt(tagPackument.version, current)) {
1036-
return {
1037-
cooldownInfo: {
1038-
name: packageName,
1039-
currentVersion,
1040-
currentVersionTime: packument?.time?.[current],
1041-
version: tagPackument.version,
1042-
...maybeTime,
1043-
},
1044-
}
1042+
return {
1043+
cooldownInfo: {
1044+
name: packageName,
1045+
currentVersion,
1046+
currentVersionTime: packument?.time?.[current],
1047+
version: tagPackument.version,
1048+
...maybeTime,
1049+
},
10451050
}
1046-
1047-
return {}
10481051
}
10491052

10501053
// If we use a custom dist-tag, we do not want to get other 'pre' versions, just the ones from this dist-tag
@@ -1119,16 +1122,24 @@ export const newest: GetVersion = async (
11191122
)
11201123

11211124
const versions = Object.values(packument?.versions ?? {})
1122-
const packageInfo = { packageName, currentVersion, options, versions, time: packument?.time }
1123-
1124-
const versionResult = findTargetAndFallback({
1125-
...packageInfo,
1126-
compare: (v1, v2) => {
1127-
const t1 = packument?.time?.[v1] || ''
1128-
const t2 = packument?.time?.[v2] || ''
1129-
return t1 > t2 ? 1 : t1 < t2 ? -1 : 0
1130-
},
1131-
})
1125+
const time = packument?.time
1126+
const isTimeMissing = !time || Object.keys(time).length === 0
1127+
const packageInfo = { packageName, currentVersion, options, versions, time }
1128+
1129+
const versionResult = isTimeMissing
1130+
? {
1131+
targetVersion: currentVersion,
1132+
fallbackVersion: null,
1133+
targetBlockedByCooldown: false,
1134+
}
1135+
: findTargetAndFallback({
1136+
...packageInfo,
1137+
compare: (v1, v2) => {
1138+
const t1 = packument?.time?.[v1] || ''
1139+
const t2 = packument?.time?.[v2] || ''
1140+
return t1 > t2 ? 1 : t1 < t2 ? -1 : 0
1141+
},
1142+
})
11321143

11331144
return toVersionResult({ ...packageInfo, ...versionResult })
11341145
}

test/bin.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,17 @@ describe('bin', async function () {
237237
stub.restore()
238238
})
239239

240+
it('do not warn about empty results when every dep is already at the highest version for non-latest --target', async () => {
241+
const stub = stubVersions({ 'ncu-test-v2': '1.0.0' }, { spawn: true })
242+
const { stdout } = await spawn('node', [bin, '--stdin', '--target', 'minor'], {
243+
stdin: JSON.stringify({ dependencies: { 'ncu-test-v2': '1.0.0' } }),
244+
})
245+
const out = stripAnsi(stdout)!
246+
out.should.not.include('No package versions were returned.')
247+
out.should.include('All dependencies match the minor package versions')
248+
stub.restore()
249+
})
250+
240251
it('combine boolean flags with arguments', async () => {
241252
const stub = stubVersions('99.9.9', { spawn: true })
242253
const { stdout } = await spawn('node', [bin, '--stdin', '--jsonUpgraded', 'ncu-test-v2'], {
@@ -287,7 +298,8 @@ describe('bin', async function () {
287298
const { stdout } = await spawn('node', [bin, '--stdin'], { stdin: JSON.stringify({ dependencies }) })
288299
stripAnsi(stdout)
289300
.trim()
290-
.should.equal('ncu-test-v2 https://github.com/raineorshine/ncu-test-v2.git#v1.0.0 → v2.0.0')
301+
.replace(/\s+/g, ' ') // Replace all whitespace sequences with a single space
302+
.should.equal('ncu-test-v2 https://github.com/raineorshine/ncu-test-v2.git#v1.0.0 → v2.0.0 [missing time]')
291303
})
292304

293305
it('strip prefix from npm alias in "to" output', async () => {

0 commit comments

Comments
 (0)