119 lines
3.8 KiB
JavaScript
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
|
|
};
|