161 lines
4.4 KiB
JavaScript
161 lines
4.4 KiB
JavaScript
// utils/versioning.mjs
|
|
|
|
const SUFFIX_PRIORITY = {
|
|
// 预发布 (越小越靠前)
|
|
'canary': 1, 'nightly': 1, 'snapshot': 1, 'dev': 1,
|
|
'pre-alpha': 2, 'prealpha': 2, 'preview': 2, 'eap': 2, 'milestone': 2,
|
|
'alpha': 3,
|
|
'beta': 4,
|
|
'rc': 5,
|
|
// 正式/稳定
|
|
'': 10, 'stable': 10, 'ga': 10, 'final': 10, 'release': 10, 'lts': 10,
|
|
};
|
|
|
|
/**
|
|
* 统一后缀别名到规范键.
|
|
*
|
|
* @param {string} nameRaw
|
|
* @return {string}
|
|
*/
|
|
function normalizeSuffixName(nameRaw) {
|
|
const n = String(nameRaw || '').toLowerCase();
|
|
switch (n) {
|
|
case 'a':
|
|
return 'alpha';
|
|
case 'b':
|
|
return 'beta';
|
|
case 'cr':
|
|
return 'rc';
|
|
case 'm':
|
|
return 'milestone';
|
|
case 'pre':
|
|
return 'preview';
|
|
case 'canary':
|
|
case 'nightly':
|
|
case 'snapshot':
|
|
case 'dev':
|
|
case 'pre-alpha':
|
|
case 'prealpha':
|
|
case 'preview':
|
|
case 'eap':
|
|
case 'milestone':
|
|
case 'alpha':
|
|
case 'beta':
|
|
case 'rc':
|
|
case 'stable':
|
|
case 'ga':
|
|
case 'final':
|
|
case 'release':
|
|
case 'lts':
|
|
return n;
|
|
default:
|
|
return n; // 未知后缀维持原样, 优先级将落在默认分支
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} version
|
|
* @return { [number[], [string, number]]}
|
|
*/
|
|
export function toVersionParts(version) {
|
|
const parts = version.split(/[\s+-]/);
|
|
const numberParts = parts[0].split('.').map(part => {
|
|
const num = parseInt(String(part), 10);
|
|
if (Number.isNaN(num)) {
|
|
throw new Error(`Invalid version part: '${part}' in version: '${version}'`);
|
|
}
|
|
return num;
|
|
});
|
|
|
|
// 解析后缀, 支持 rc1 / rc 1 / rc.1 / RC1 等; 默认数字为 1
|
|
const suffixPattern = /([A-Za-z]+)[\s._-]*(\d*)|([A-Za-z]*)[\s._-]*(\d+)/;
|
|
const suffixStr = parts[1] || '';
|
|
const m = suffixStr.match(suffixPattern);
|
|
if (!m) return [ numberParts, [ '', 0 ] ];
|
|
|
|
const rawName = (m[1] ?? m[3] ?? '');
|
|
const rawNum = (m[2] ?? m[4] ?? '');
|
|
const suffixName = normalizeSuffixName(rawName);
|
|
const suffixNumberParsed = parseInt(rawNum || '1', 10);
|
|
const suffixNumber = Number.isNaN(suffixNumberParsed) ? 1 : suffixNumberParsed;
|
|
return [ numberParts, [ suffixName, suffixNumber ] ];
|
|
}
|
|
|
|
/**
|
|
* @param {number[]} a
|
|
* @param {number[]} b
|
|
* @return {number}
|
|
*/
|
|
export function compareVersionParts(a, b) {
|
|
const max = Math.max(a.length, b.length);
|
|
for (let i = 0; i < max; i++) {
|
|
const x = a[i] ?? 0;
|
|
const y = b[i] ?? 0;
|
|
if (x !== y) return x > y ? 1 : -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @param {[string, number]} s1
|
|
* @param {[string, number]} s2
|
|
* @return {number}
|
|
*/
|
|
export function compareVersionSuffix(s1, s2) {
|
|
const [ name1Raw, num1 ] = s1;
|
|
const [ name2Raw, num2 ] = s2;
|
|
const name1 = normalizeSuffixName(name1Raw);
|
|
const name2 = normalizeSuffixName(name2Raw);
|
|
const p1 = SUFFIX_PRIORITY[name1] ?? Number.MAX_SAFE_INTEGER;
|
|
const p2 = SUFFIX_PRIORITY[name2] ?? Number.MAX_SAFE_INTEGER;
|
|
if (p1 !== p2) return p1 > p2 ? 1 : -1;
|
|
if (num1 !== num2) return num1 > num2 ? 1 : -1;
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @param {string} v1
|
|
* @param {string} v2
|
|
* @return {number}
|
|
*/
|
|
export function compareVersionStrings(v1, v2) {
|
|
const [ n1, s1 ] = toVersionParts(v1);
|
|
const [ n2, s2 ] = toVersionParts(v2);
|
|
const cmp = compareVersionParts(n1, n2);
|
|
return cmp !== 0 ? cmp : compareVersionSuffix(s1, s2);
|
|
}
|
|
|
|
/**
|
|
* @param {string} v1
|
|
* @param {string} v2
|
|
* @return {number}
|
|
*/
|
|
export function compareVersionStringsDescending(v1, v2) {
|
|
const [ n1, s1 ] = toVersionParts(v1);
|
|
const [ n2, s2 ] = toVersionParts(v2);
|
|
const cmp = compareVersionParts(n2, n1);
|
|
return cmp !== 0 ? cmp : compareVersionSuffix(s2, s1);
|
|
}
|
|
|
|
/**
|
|
* @param {string} v
|
|
* @return {boolean}
|
|
*/
|
|
export function isVersionStable(v) {
|
|
const [ _, [ suffixName ] ] = toVersionParts(v);
|
|
const normalizedName = normalizeSuffixName(suffixName);
|
|
return (SUFFIX_PRIORITY[normalizedName] ?? Number.MAX_SAFE_INTEGER) === 10;
|
|
}
|
|
|
|
/**
|
|
* @param {string} v
|
|
* @param {Object} options
|
|
* @param {string} [options.min]
|
|
* @param {string} [options.max]
|
|
* @return {boolean}
|
|
*/
|
|
export function isVersionInRange(v, { min, max } = {}) {
|
|
return (min == null || compareVersionStrings(v, min) >= 0)
|
|
&& (max == null || compareVersionStrings(v, max) <= 0);
|
|
}
|