interface ParsedSemanticVersion { major: number; minor: number; patch: number; prerelease: string[]; } function parseNumericIdentifier(value: string): number | null { return /^\d+$/.test(value) ? Number.parseInt(value, 10) : null; } function parseSemanticVersion(rawValue: string | null | undefined): ParsedSemanticVersion | null { const normalized = normalizeSemanticVersion(rawValue); if (!normalized) { return null; } const match = normalized.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?$/); if (!match) { return null; } const prerelease = match[4] ? match[4] .split('.') .map((part) => part.trim()) .filter(Boolean) : []; return { major: Number.parseInt(match[1], 10), minor: Number.parseInt(match[2] ?? '0', 10), patch: Number.parseInt(match[3] ?? '0', 10), prerelease }; } function comparePrereleaseIdentifiers(left: string, right: string): number { const leftNumeric = parseNumericIdentifier(left); const rightNumeric = parseNumericIdentifier(right); if (leftNumeric !== null && rightNumeric !== null) { return leftNumeric - rightNumeric; } if (leftNumeric !== null) { return -1; } if (rightNumeric !== null) { return 1; } return left.localeCompare(right); } function comparePrerelease(left: string[], right: string[]): number { if (left.length === 0 && right.length === 0) { return 0; } if (left.length === 0) { return 1; } if (right.length === 0) { return -1; } const maxLength = Math.max(left.length, right.length); for (let index = 0; index < maxLength; index += 1) { const leftValue = left[index]; const rightValue = right[index]; if (!leftValue) { return -1; } if (!rightValue) { return 1; } const comparison = comparePrereleaseIdentifiers(leftValue, rightValue); if (comparison !== 0) { return comparison; } } return 0; } export function normalizeSemanticVersion(rawValue: string | null | undefined): string | null { if (typeof rawValue !== 'string') { return null; } const trimmedValue = rawValue.trim(); if (!trimmedValue) { return null; } return trimmedValue.replace(/^v/i, '').split('+')[0] ?? null; } export function compareSemanticVersions( leftVersion: string | null | undefined, rightVersion: string | null | undefined ): number { const left = parseSemanticVersion(leftVersion); const right = parseSemanticVersion(rightVersion); if (!left && !right) { return 0; } if (!left) { return -1; } if (!right) { return 1; } if (left.major !== right.major) { return left.major - right.major; } if (left.minor !== right.minor) { return left.minor - right.minor; } if (left.patch !== right.patch) { return left.patch - right.patch; } return comparePrerelease(left.prerelease, right.prerelease); } export function sortSemanticVersionsDescending(versions: string[]): string[] { const normalizedVersions = versions .map((version) => normalizeSemanticVersion(version)) .filter((version): version is string => !!version); return [...new Set(normalizedVersions)].sort((left, right) => compareSemanticVersions(right, left)); }