Lumi/src/services/versioning.js
2026-06-16 09:44:16 +02:00

119 lines
3.8 KiB
JavaScript

function parseSemver(value) {
const match = String(value || "").trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
if (!match) return null;
return {
major: Number(match[1]),
minor: Number(match[2]),
patch: Number(match[3]),
raw: `${Number(match[1])}.${Number(match[2])}.${Number(match[3])}`
};
}
function compareSemver(left, right) {
const a = parseSemver(left);
const b = parseSemver(right);
if (!a && !b) return 0;
if (!a) return -1;
if (!b) return 1;
for (const key of ["major", "minor", "patch"]) {
if (a[key] > b[key]) return 1;
if (a[key] < b[key]) return -1;
}
return 0;
}
function isMajorJump(current, target) {
const from = parseSemver(current);
const to = parseSemver(target);
return Boolean(from && to && from.major !== to.major);
}
function versionInRange(version, from, to) {
return compareSemver(version, from) > 0 && compareSemver(version, to) <= 0;
}
function normalizeVersionEntry(entry, fallback = {}) {
if (typeof entry === "string") return { version: entry, ...fallback };
if (!entry || typeof entry !== "object") return null;
const version = entry.version || entry.name || entry.tag;
if (!parseSemver(version)) return null;
return { ...fallback, ...entry, version: parseSemver(version).raw };
}
function normalizeVersions(entries, fallback = {}) {
return (Array.isArray(entries) ? entries : [])
.map((entry) => normalizeVersionEntry(entry, fallback))
.filter(Boolean)
.sort((a, b) => compareSemver(a.version, b.version));
}
function compatibleFromAllows(entry, current) {
if (!entry?.compatible_from) return false;
return compareSemver(current, entry.compatible_from) >= 0;
}
function isBridgeTarget(entry, current) {
const from = parseSemver(current);
const to = parseSemver(entry?.version);
if (!from || !to || from.major === to.major) return false;
if (entry.compatibility_bridge === true || entry.migration_kind === "compatibility_bridge") {
return compatibleFromAllows(entry, current) || !entry.compatible_from;
}
if (to.major === from.major + 1 && to.minor === 0 && to.patch === 0) {
return compatibleFromAllows(entry, current) || entry.compatible_from === undefined;
}
return false;
}
function findSafeTarget(currentVersion, versionEntries) {
const current = parseSemver(currentVersion);
const versions = normalizeVersions(versionEntries);
if (!current) {
return {
target: null,
blocked: true,
reason: "Current version is not valid semver."
};
}
const newer = versions.filter((entry) => compareSemver(entry.version, current.raw) > 0);
if (!newer.length) {
return { target: null, blocked: false, reason: "Already up to date." };
}
const latest = newer[newer.length - 1];
const sameMajor = newer.filter((entry) => parseSemver(entry.version)?.major === current.major);
if (sameMajor.length) {
return { target: sameMajor[sameMajor.length - 1], latest, blocked: false };
}
const bridge = newer.find((entry) => isBridgeTarget(entry, current.raw));
if (bridge) {
return {
target: bridge,
latest,
blocked: false,
warning: `Latest ${latest.version} crosses a major boundary; targeting compatibility bridge ${bridge.version}.`
};
}
return {
target: null,
latest,
blocked: true,
reason: `Latest ${latest.version} crosses a major boundary and no compatible bridge target was found.`
};
}
function collectChangelogRange(currentVersion, targetVersion, changelogEntries) {
if (!targetVersion) return [];
return normalizeVersions(changelogEntries)
.filter((entry) => versionInRange(entry.version, currentVersion, targetVersion))
.sort((a, b) => compareSemver(b.version, a.version));
}
module.exports = {
parseSemver,
compareSemver,
isMajorJump,
normalizeVersions,
findSafeTarget,
collectChangelogRange
};