fix(update): compare core versions using SemVer

Only show update indicators when the remote core version is newer.
Add coverage for prereleases, build metadata, invalid versions, and version boundaries.
This commit is contained in:
lzt404 committed 2026-09-06 01:28:58 +08:00
1 parent 355d8057e6
commit 45ecd37ce5
3 files changed
+105 -4

No files matched your search

+56 -3
View File
@@ -35,13 +35,66 @@ function normalizeVersion(version: string | null | undefined) {
return (version ?? '').trim().replace(/^v/i, '');
}
type SemanticVersion = {
core: [string, string, string];
prerelease: string[] | null;
};
const semanticVersionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
function parseSemanticVersion(version: string | null | undefined): SemanticVersion | null {
const match = semanticVersionPattern.exec(normalizeVersion(version));
if (!match) return null;
return {
core: [match[1], match[2], match[3]],
prerelease: match[4] ? match[4].split('.') : null,
};
}
function compareNumericIdentifier(left: string, right: string) {
if (left.length !== right.length) return left.length > right.length ? 1 : -1;
if (left === right) return 0;
return left > right ? 1 : -1;
}
function compareSemanticVersions(left: SemanticVersion, right: SemanticVersion) {
for (let index = 0; index < left.core.length; index += 1) {
const difference = compareNumericIdentifier(left.core[index], right.core[index]);
if (difference !== 0) return difference;
}
if (left.prerelease === null) return right.prerelease === null ? 0 : 1;
if (right.prerelease === null) return -1;
const count = Math.max(left.prerelease.length, right.prerelease.length);
for (let index = 0; index < count; index += 1) {
const leftIdentifier = left.prerelease[index];
const rightIdentifier = right.prerelease[index];
if (leftIdentifier === undefined) return -1;
if (rightIdentifier === undefined) return 1;
if (leftIdentifier === rightIdentifier) continue;
const leftIsNumeric = /^\d+$/.test(leftIdentifier);
const rightIsNumeric = /^\d+$/.test(rightIdentifier);
if (leftIsNumeric && rightIsNumeric) {
return compareNumericIdentifier(leftIdentifier, rightIdentifier);
}
if (leftIsNumeric) return -1;
if (rightIsNumeric) return 1;
return leftIdentifier > rightIdentifier ? 1 : -1;
}
return 0;
}
export function coreUpdateAvailable(
currentVersion: string | null | undefined,
latestVersion: string | null | undefined,
) {
const current = normalizeVersion(currentVersion);
const latest = normalizeVersion(latestVersion);
return Boolean(current && latest && current !== latest);
const current = parseSemanticVersion(currentVersion);
const latest = parseSemanticVersion(latestVersion);
return Boolean(current && latest && compareSemanticVersions(latest, current) > 0);
}
export function requestLatestCore(force = false) {
+1 -1
View File
@@ -742,7 +742,7 @@ export function VersionManagementPage() {
<button
type="button"
className={latestVersion && (!coreInstalled || currentVersion !== latestVersion) ? 'primary-button' : 'secondary-button'}
className={latestVersion && (!coreInstalled || coreHasUpdate) ? 'primary-button' : 'secondary-button'}
title={latestVersion ? t('kernel.versions.stopAndUpdateVersion', { version: latestVersion }) : t('kernel.versions.installLatest')}
disabled={!latestVersion || busy}
onClick={() => setConfirmUpdateOpen(true)}