* feat: add winget version badge * chore: accept dotted path instead of slashed * test: add test for winget-version * fix: remove debug code * chore: use winget-specific version compare algorithm * fix: support latest and unknown * fix(winget/version): trailing '.0' handling is incorrect * fix(winget/version): latest returns last newest version instead of the first newest version * fix(winget/version): confusing subpackage and version name * fix(winget/version): example for latest is incorrect * add a couple of extra test cases for latest() --------- Co-authored-by: chris48s <git@chris-shaw.dev>
173 lines
5.6 KiB
JavaScript
173 lines
5.6 KiB
JavaScript
/**
|
|
* Comparing versions with winget's version comparator.
|
|
*
|
|
* See https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp for original implementation.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
/**
|
|
* Compares two strings representing version numbers lexicographically and returns an integer value.
|
|
*
|
|
* @param {string} v1 - The first version to compare
|
|
* @param {string} v2 - The second version to compare
|
|
* @returns {number} -1 if v1 is smaller than v2, 1 if v1 is larger than v2, 0 if v1 and v2 are equal
|
|
* @example
|
|
* compareVersion('1.2.3', '1.2.4') // returns -1 because numeric part of first version is smaller than the numeric part of second version.
|
|
*/
|
|
function compareVersion(v1, v2) {
|
|
// https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L109-L173
|
|
// This implementation does not parse s_Approximate_Greater_Than
|
|
// and s_Approximate_Less_Than since they won't appear in directory name (package version parsed by shields.io)
|
|
const v1Trimmed = trimPrefix(v1)
|
|
const v2Trimmed = trimPrefix(v2)
|
|
|
|
const v1Latest = v1Trimmed.trim().toLowerCase() === 'latest'
|
|
const v2Latest = v2Trimmed.trim().toLowerCase() === 'latest'
|
|
|
|
if (v1Latest && v2Latest) {
|
|
return 0
|
|
} else if (v1Latest) {
|
|
return 1
|
|
} else if (v2Latest) {
|
|
return -1
|
|
}
|
|
|
|
const v1Unknown = v1Trimmed.trim().toLowerCase() === 'unknown'
|
|
const v2Unknown = v2Trimmed.trim().toLowerCase() === 'unknown'
|
|
|
|
if (v1Unknown && v2Unknown) {
|
|
return 0
|
|
} else if (v1Unknown) {
|
|
return -1
|
|
} else if (v2Unknown) {
|
|
return 1
|
|
}
|
|
|
|
const parts1 = v1Trimmed.split('.')
|
|
const parts2 = v2Trimmed.split('.')
|
|
|
|
trimLastZeros(parts1)
|
|
trimLastZeros(parts2)
|
|
|
|
for (let i = 0; i < Math.min(parts1.length, parts2.length); i++) {
|
|
const part1 = parts1[i]
|
|
const part2 = parts2[i]
|
|
|
|
const compare = compareVersionPart(part1, part2)
|
|
if (compare !== 0) {
|
|
return compare
|
|
}
|
|
}
|
|
|
|
if (parts1.length === parts2.length) {
|
|
return 0
|
|
}
|
|
|
|
if (parts1.length > parts2.length) {
|
|
return 1
|
|
} else if (parts1.length < parts2.length) {
|
|
return -1
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
/**
|
|
* Removes all leading non-digit characters from a version number string
|
|
* if there is a digit before the split character, or no split characters exist.
|
|
*
|
|
* @param {string} version The version number string to trim
|
|
* @returns {string} The version number string with all leading non-digit characters removed
|
|
*/
|
|
function trimPrefix(version) {
|
|
// https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L66
|
|
// If there is a digit before the split character, or no split characters exist, trim off all leading non-digit characters
|
|
|
|
const digitPos = version.match(/(\d.*)/)
|
|
const splitPos = version.match(/\./)
|
|
if (digitPos && (splitPos == null || digitPos.index < splitPos.index)) {
|
|
// there is digit before the split character so strip off all leading non-digit characters
|
|
return version.slice(digitPos.index)
|
|
}
|
|
return version
|
|
}
|
|
|
|
/**
|
|
* Removes all trailing zeros from a version number part array.
|
|
*
|
|
* @param {string[]} parts - parts
|
|
*/
|
|
function trimLastZeros(parts) {
|
|
while (parts.length > 1 && parts[parts.length - 1].trim() === '0') {
|
|
parts.pop()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compares two strings representing version number parts lexicographically and returns an integer value.
|
|
*
|
|
* @param {string} part1 - The first version part to compare
|
|
* @param {string} part2 - The second version part to compare
|
|
* @returns {number} -1 if part1 is smaller than part2, 1 if part1 is larger than part2, 0 if part1 and part2 are equal
|
|
* @example
|
|
* compareVersionPart('3', '4') // returns -1 because numeric part of first part is smaller than the numeric part of second part.
|
|
*/
|
|
function compareVersionPart(part1, part2) {
|
|
// https://github.com/microsoft/winget-cli/blob/ae566c7bf21cfcc75be7ec30e4036a30eede8396/src/AppInstallerSharedLib/Versions.cpp#L324-L352
|
|
const [, numericString1, other1] = part1.trim().match(/^(\d*)(.*)$/)
|
|
const [, numericString2, other2] = part2.trim().match(/^(\d*)(.*)$/)
|
|
const numeric1 = parseInt(numericString1 || '0', 10)
|
|
const numeric2 = parseInt(numericString2 || '0', 10)
|
|
|
|
if (numeric1 < numeric2) {
|
|
return -1
|
|
} else if (numeric1 > numeric2) {
|
|
return 1
|
|
}
|
|
// numeric1 === numeric2
|
|
|
|
const otherFolded1 = (other1 ?? '').toLowerCase()
|
|
const otherFolded2 = (other2 ?? '').toLowerCase()
|
|
|
|
if (otherFolded1.length !== 0 && otherFolded2.length === 0) {
|
|
return -1
|
|
} else if (otherFolded1.length === 0 && otherFolded2.length !== 0) {
|
|
return 1
|
|
}
|
|
|
|
if (otherFolded1 < otherFolded2) {
|
|
return -1
|
|
} else if (otherFolded1 > otherFolded2) {
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
/**
|
|
* Finds the largest version number lexicographically from an array of strings representing version numbers and returns it as a string.
|
|
*
|
|
* @param {string[]} versions - The array of version numbers to compare
|
|
* @returns {string|undefined} The largest version number as a string, or undefined if the array is empty
|
|
* @example
|
|
* latest(['1.2.3', '1.2.4', '1.3', '2.0']) // returns '2.0' because it is the largest version number.
|
|
* latest(['1.2.3', '1.2.4', '1.3-alpha', '2.0-beta']) // returns '2.0-beta'. there is no special handling for pre-release versions.
|
|
*/
|
|
function latest(versions) {
|
|
const len = versions.length
|
|
if (len === 0) {
|
|
return
|
|
}
|
|
|
|
let version = versions[0]
|
|
for (let i = 1; i < len; i++) {
|
|
if (compareVersion(version, versions[i]) <= 0) {
|
|
version = versions[i]
|
|
}
|
|
}
|
|
return version
|
|
}
|
|
|
|
export { latest, compareVersion }
|