interface ParsedCliSemver { readonly major: number; readonly minor: number; readonly patch: number; readonly prerelease: ReadonlyArray<string>;} const CLI_VERSION_NUMBER_SEGMENT = /^\d+$/; export function normalizeCliVersion(version: string): string { const [main, prerelease] = version.trim().split("-", 2); const segments = (main ?? "") .split(".") .map((segment) => segment.trim()) .filter((segment) => segment.length > 0); if (segments.length === 2) { segments.push("0"); } return prerelease ? `${segments.join(".")}-${prerelease}` : segments.join(".");} function parseCliSemver(version: string): ParsedCliSemver | null { const normalized = normalizeCliVersion(version); const [main = "", prerelease] = normalized.split("-", 2); const segments = main.split("."); if (segments.length !== 3) { return null; } const [majorSegment, minorSegment, patchSegment] = segments; if (majorSegment === undefined || minorSegment === undefined || patchSegment === undefined) { return null; } if ( !CLI_VERSION_NUMBER_SEGMENT.test(majorSegment) || !CLI_VERSION_NUMBER_SEGMENT.test(minorSegment) || !CLI_VERSION_NUMBER_SEGMENT.test(patchSegment) ) { return null; } const major = Number.parseInt(majorSegment, 10); const minor = Number.parseInt(minorSegment, 10); const patch = Number.parseInt(patchSegment, 10); if (![major, minor, patch].every(Number.isInteger)) { return null; } return { major, minor, patch, prerelease: prerelease ?.split(".") .map((segment) => segment.trim()) .filter((segment) => segment.length > 0) ?? [], };} function comparePrereleaseIdentifier(left: string, right: string): number { const leftNumeric = /^\d+$/.test(left); const rightNumeric = /^\d+$/.test(right); if (leftNumeric && rightNumeric) { return Number.parseInt(left, 10) - Number.parseInt(right, 10); } if (leftNumeric) { return -1; } if (rightNumeric) { return 1; } return left.localeCompare(right);} export function compareCliVersions(left: string, right: string): number { const parsedLeft = parseCliSemver(left); const parsedRight = parseCliSemver(right); if (!parsedLeft || !parsedRight) { return left.localeCompare(right); } if (parsedLeft.major !== parsedRight.major) { return parsedLeft.major - parsedRight.major; } if (parsedLeft.minor !== parsedRight.minor) { return parsedLeft.minor - parsedRight.minor; } if (parsedLeft.patch !== parsedRight.patch) { return parsedLeft.patch - parsedRight.patch; } if (parsedLeft.prerelease.length === 0 && parsedRight.prerelease.length === 0) { return 0; } if (parsedLeft.prerelease.length === 0) { return 1; } if (parsedRight.prerelease.length === 0) { return -1; } const length = Math.max(parsedLeft.prerelease.length, parsedRight.prerelease.length); for (let index = 0; index < length; index += 1) { const leftIdentifier = parsedLeft.prerelease[index]; const rightIdentifier = parsedRight.prerelease[index]; if (leftIdentifier === undefined) { return -1; } if (rightIdentifier === undefined) { return 1; } const comparison = comparePrereleaseIdentifier(leftIdentifier, rightIdentifier); if (comparison !== 0) { return comparison; } } return 0;}
interface ParsedCliSemver {
readonly major: number;
readonly minor: number;
readonly patch: number;
readonly prerelease: ReadonlyArray<string>;
}
const CLI_VERSION_NUMBER_SEGMENT = /^\d+$/;
export function normalizeCliVersion(version: string): string {
const [main, prerelease] = version.trim().split("-", 2);
const segments = (main ?? "")
.split(".")
.map((segment) => segment.trim())
.filter((segment) => segment.length > 0);
if (segments.length === 2) {
segments.push("0");
}
return prerelease ? `${segments.join(".")}-${prerelease}` : segments.join(".");
}
function parseCliSemver(version: string): ParsedCliSemver | null {
const normalized = normalizeCliVersion(version);
const [main = "", prerelease] = normalized.split("-", 2);
const segments = main.split(".");
if (segments.length !== 3) {
return null;
}
const [majorSegment, minorSegment, patchSegment] = segments;
if (majorSegment === undefined || minorSegment === undefined || patchSegment === undefined) {
return null;
}
if (
!CLI_VERSION_NUMBER_SEGMENT.test(majorSegment) ||
!CLI_VERSION_NUMBER_SEGMENT.test(minorSegment) ||
!CLI_VERSION_NUMBER_SEGMENT.test(patchSegment)
) {
return null;
}
const major = Number.parseInt(majorSegment, 10);
const minor = Number.parseInt(minorSegment, 10);
const patch = Number.parseInt(patchSegment, 10);
if (![major, minor, patch].every(Number.isInteger)) {
return null;
}
return {
major,
minor,
patch,
prerelease:
prerelease
?.split(".")
.map((segment) => segment.trim())
.filter((segment) => segment.length > 0) ?? [],
};
}
function comparePrereleaseIdentifier(left: string, right: string): number {
const leftNumeric = /^\d+$/.test(left);
const rightNumeric = /^\d+$/.test(right);
if (leftNumeric && rightNumeric) {
return Number.parseInt(left, 10) - Number.parseInt(right, 10);
}
if (leftNumeric) {
return -1;
}
if (rightNumeric) {
return 1;
}
return left.localeCompare(right);
}
export function compareCliVersions(left: string, right: string): number {
const parsedLeft = parseCliSemver(left);
const parsedRight = parseCliSemver(right);
if (!parsedLeft || !parsedRight) {
return left.localeCompare(right);
}
if (parsedLeft.major !== parsedRight.major) {
return parsedLeft.major - parsedRight.major;
}
if (parsedLeft.minor !== parsedRight.minor) {
return parsedLeft.minor - parsedRight.minor;
}
if (parsedLeft.patch !== parsedRight.patch) {
return parsedLeft.patch - parsedRight.patch;
}
if (parsedLeft.prerelease.length === 0 && parsedRight.prerelease.length === 0) {
return 0;
}
if (parsedLeft.prerelease.length === 0) {
return 1;
}
if (parsedRight.prerelease.length === 0) {
return -1;
}
const length = Math.max(parsedLeft.prerelease.length, parsedRight.prerelease.length);
for (let index = 0; index < length; index += 1) {
const leftIdentifier = parsedLeft.prerelease[index];
const rightIdentifier = parsedRight.prerelease[index];
if (leftIdentifier === undefined) {
return -1;
}
if (rightIdentifier === undefined) {
return 1;
}
const comparison = comparePrereleaseIdentifier(leftIdentifier, rightIdentifier);
if (comparison !== 0) {
return comparison;
}
}
return 0;
}