export type SemVer = { major: number; minor: number; patch: number; rc?: number; parsedFrom: string; }; export namespace SemVer { const bumpTypes = ["major", "minor", "patch", "rc", "no bump"] as const; export type BumpType = (typeof bumpTypes)[number]; export function parse(versionStr: string): SemVer { const match = versionStr.match(/^v?([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(?:-rc.([0-9]+))?$/); if (!match) { throw new Error(`${versionStr} is not a valid semantic version`); } const semVer: Omit = { "major": parseInt(match[1]), "minor": parseInt(match[2]), "patch": (() => { const str = match[3]; return str === undefined ? 0 : parseInt(str); })(), ...(() => { const str = match[4]; return str === undefined ? {} : { "rc": parseInt(str) }; })() }; const initialStr = stringify(semVer); Object.defineProperty(semVer, "parsedFrom", { "enumerable": true, "get": function () { const currentStr = stringify(this); if (currentStr !== initialStr) { throw new Error(`SemVer.parsedFrom can't be read anymore, the version have been modified from ${initialStr} to ${currentStr}`); } return versionStr; } }); return semVer as any; } export function stringify(v: Omit): string { return `${v.major}.${v.minor}.${v.patch}${v.rc === undefined ? "" : `-rc.${v.rc}`}`; } /** * * v1 < v2 => -1 * v1 === v2 => 0 * v1 > v2 => 1 * */ export function compare(v1: SemVer, v2: SemVer): -1 | 0 | 1 { const sign = (diff: number): -1 | 0 | 1 => (diff === 0 ? 0 : diff < 0 ? -1 : 1); const noUndefined = (n: number | undefined) => n ?? Infinity; for (const level of ["major", "minor", "patch", "rc"] as const) { if (noUndefined(v1[level]) !== noUndefined(v2[level])) { return sign(noUndefined(v1[level]) - noUndefined(v2[level])); } } return 0; } /* console.log(compare(parse("3.0.0-rc.3"), parse("3.0.0")) === -1 ) console.log(compare(parse("3.0.0-rc.3"), parse("3.0.0-rc.4")) === -1 ) console.log(compare(parse("3.0.0-rc.3"), parse("4.0.0")) === -1 ) */ export function bumpType(params: { versionBehind: string | SemVer; versionAhead: string | SemVer }): BumpType | "no bump" { const versionAhead = typeof params.versionAhead === "string" ? parse(params.versionAhead) : params.versionAhead; const versionBehind = typeof params.versionBehind === "string" ? parse(params.versionBehind) : params.versionBehind; if (compare(versionBehind, versionAhead) === 1) { throw new Error(`Version regression ${stringify(versionBehind)} -> ${stringify(versionAhead)}`); } for (const level of ["major", "minor", "patch", "rc"] as const) { if (versionBehind[level] !== versionAhead[level]) { return level; } } return "no bump"; } }