fix: don't downgrade dist-tags to prerelease versions when minimumReleaseAge is set (#9988)

close #9979
This commit is contained in:
Zoltan Kochan
2025-09-17 14:34:42 +02:00
committed by GitHub
parent 121b44e246
commit 02f8b690ad
3 changed files with 151 additions and 9 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/npm-resolver": patch
"pnpm": patch
---
When `minimumReleaseAge` is set and the active version under a dist-tag is not mature enough, do not downgrade to a prerelease version in case the original version wasn't a prerelease one [#9979](https://github.com/pnpm/pnpm/issues/9979).

View File

@@ -240,6 +240,19 @@ function filterMetaByPublishedDate (meta: PackageMetaWithTime, publishedBy: Date
const distTagsWithinDate: PackageMeta['dist-tags'] = {}
const allDistTags = meta['dist-tags'] ?? {}
const parsedSemverCache = new Map<string, semver.SemVer>()
function tryParseSemver (semverStr: string): semver.SemVer | null {
let parsedSemver = parsedSemverCache.get(semverStr)
if (!parsedSemver) {
try {
parsedSemver = new semver.SemVer(semverStr, true)
} catch {
return null
}
parsedSemverCache.set(semverStr, parsedSemver)
}
return parsedSemver
}
for (const tag in allDistTags) {
if (!Object.hasOwn(allDistTags, tag)) continue
const distTagVersion = allDistTags[tag]
@@ -248,17 +261,18 @@ function filterMetaByPublishedDate (meta: PackageMetaWithTime, publishedBy: Date
continue
}
// Repopulate the tag to the highest version available within date that has the same major as the original tag's version
let originalSemVer: semver.SemVer | null = null
try {
originalSemVer = new semver.SemVer(distTagVersion, true)
} catch {
continue
}
const originalMajor = originalSemVer.major
const originalSemVer = tryParseSemver(distTagVersion)
if (!originalSemVer) continue
const originalIsPrerelease = (originalSemVer.prerelease.length > 0)
let bestVersion: string | undefined
const originalMajorPrefix = `${originalMajor}.`
for (const candidate in versionsWithinDate) {
if (!Object.hasOwn(versionsWithinDate, candidate) || !candidate.startsWith(originalMajorPrefix)) continue
if (!Object.hasOwn(versionsWithinDate, candidate)) continue
const candidateParsed = tryParseSemver(candidate)
if (
!candidateParsed ||
candidateParsed.major !== originalSemVer.major ||
(candidateParsed.prerelease.length > 0) !== originalIsPrerelease
) continue
if (!bestVersion) {
bestVersion = candidate
} else {

View File

@@ -79,6 +79,128 @@ test('repopulate dist-tag to highest same-major version within the date cutoff',
expect(res!.id).toBe(`${name}@3.1.0`)
})
test('repopulate dist-tag to highest non-prerelease same-major version within the date cutoff', async () => {
const name = 'dist-tag-date'
const meta = {
name,
versions: {
'3.0.0': {
name,
version: '3.0.0',
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-3.0.0.tgz` },
},
'3.1.0-alpha.0': {
name,
version: '3.1.0-alpha.0',
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-3.1.0-alpha.0.tgz` },
},
'3.2.0': {
name,
version: '3.2.0',
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-3.2.0.tgz` },
},
'2.9.9': {
name,
version: '2.9.9',
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-2.9.9.tgz` },
},
},
'dist-tags': {
latest: '3.2.0',
},
time: {
'2.9.9': '2020-01-01T00:00:00.000Z',
'3.0.0': '2020-02-01T00:00:00.000Z',
'3.1.0-alpha.0': '2020-03-01T00:00:00.000Z',
'3.2.0': '2020-05-01T00:00:00.000Z',
},
}
// Cutoff before 3.2.0, so latest must be remapped to 3.1.0 (same major 3)
const cutoff = new Date('2020-04-01T00:00:00.000Z')
nock(registries.default)
.get(`/${name}`)
.reply(200, meta)
const cacheDir = tempy.directory()
const { resolveFromNpm } = createResolveFromNpm({
cacheDir,
fullMetadata: true,
registries,
})
const res = await resolveFromNpm({ alias: name, bareSpecifier: 'latest' }, {
publishedBy: cutoff,
})
expect(res!.id).toBe(`${name}@3.0.0`)
})
test('repopulate dist-tag to highest prerelease same-major version within the date cutoff', async () => {
const name = 'dist-tag-date'
const meta = {
name,
versions: {
'3.0.0-alpha.0': {
name,
version: '3.0.0-alpha.0',
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-3.0.0-alpha.0.tgz` },
},
'3.0.0-alpha.1': {
name,
version: '3.0.0-alpha.1',
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-3.0.0-alpha.1.tgz` },
},
'3.0.0-alpha.2': {
name,
version: '3.0.0-alpha.2',
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-3.0.0-alpha.2.tgz` },
},
'3.2.0': {
name,
version: '3.2.0',
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-3.2.0.tgz` },
},
'2.9.9': {
name,
version: '2.9.9',
dist: { tarball: `https://registry.npmjs.org/${name}/-/${name}-2.9.9.tgz` },
},
},
'dist-tags': {
latest: '3.0.0-alpha.2',
},
time: {
'2.9.9': '2020-01-01T00:00:00.000Z',
'3.0.0-alpha.0': '2020-02-01T00:00:00.000Z',
'3.0.0-alpha.1': '2020-03-01T00:00:00.000Z',
'3.0.0-alpha.2': '2020-05-01T00:00:00.000Z',
'3.2.0': '2020-05-01T00:00:00.000Z',
},
}
// Cutoff before 3.2.0 and 3.0.0-alpha.2, so latest must be remapped to 3.0.0-alpha.1 (the highest prerelease version within the cutoff)
const cutoff = new Date('2020-04-01T00:00:00.000Z')
nock(registries.default)
.get(`/${name}`)
.reply(200, meta)
const cacheDir = tempy.directory()
const { resolveFromNpm } = createResolveFromNpm({
cacheDir,
fullMetadata: true,
registries,
})
const res = await resolveFromNpm({ alias: name, bareSpecifier: 'latest' }, {
publishedBy: cutoff,
})
expect(res!.id).toBe(`${name}@3.0.0-alpha.1`)
})
test('keep dist-tag if original version is within the date cutoff', async () => {
const name = 'dist-tag-date-keep'
const meta = {