diff --git a/data/moduleStaticProps.ts b/data/moduleStaticProps.ts index b40604ddd68c..3410228650ca 100644 --- a/data/moduleStaticProps.ts +++ b/data/moduleStaticProps.ts @@ -1,4 +1,3 @@ -import { compareVersions, validate as validateVersion } from 'compare-versions' import { extractModuleInfo, getModuleMetadata, @@ -53,23 +52,75 @@ export const getStaticPropsModulePage = async ( } /** - * Sort versions, by splitting them into sortable and unsortable ones. + * Parse and sort versions according to https://bazel.build/external/module#version_format. * - * The sortable versions will form the start of the list and are sorted, while the unsortable ones will - * form the end of it. - * - * This is mostly a placeholder until we have proper version parsing and comparison - * (see discussion in https://github.com/bazel-contrib/bcr-ui/issues/54). + * This is a placeholder until we switch to a common "Bzlmod semver" library + * (see https://github.com/bazel-contrib/bcr-ui/issues/54#issuecomment-1521844089). */ -const sortVersions = (versions: string[]): string[] => { - const sortableVersions = versions.filter((version) => - validateVersion(version) - ) - const unsortableVersions = versions.filter( - (version) => !validateVersion(version) - ) - sortableVersions.sort(compareVersions) - sortableVersions.reverse() - return [...sortableVersions, ...unsortableVersions] +type Version = { + release: string[] + prerelease: string[] + original: string +} + +const parseVersion = (v: string): Version => { + const firstDash = v.indexOf('-') + if (firstDash === -1) { + return { release: v.split('.'), prerelease: [], original: v } + } + return { + release: v.slice(0, firstDash).split('.'), + prerelease: v.slice(firstDash + 1).split('.'), + original: v, + } +} + +type Cmp = (a: T, b: T) => number + +function natural(a: T, b: T): number { + return a < b ? -1 : a > b ? 1 : 0 +} + +function comparing(f: (t: T) => U, innerCmp: Cmp = natural): Cmp { + return (a: T, b: T): number => innerCmp(f(a), f(b)) +} + +function lexicographically(elementCmp: Cmp = natural): Cmp { + return (as: T[], bs: T[]): number => { + for (let i = 0; i < as.length && i < bs.length; i++) { + const result = elementCmp(as[i], bs[i]) + if (result !== 0) return result + } + return as.length - bs.length + } +} + +function composeCmps(...cmps: Cmp[]): Cmp { + return (a: T, b: T): number => { + for (const cmp of cmps) { + const result = cmp(a, b) + if (result !== 0) return result + } + return 0 + } +} + +const compareIdentifiers: Cmp = composeCmps( + comparing((id) => !/^\d+$/.test(id)), // pure numbers compare less than non-numbers + comparing((id) => (/^\d+$/.test(id) ? parseInt(id) : 0)), + natural +) + +const compareVersions: Cmp = composeCmps( + comparing((v) => v.release, lexicographically(compareIdentifiers)), + comparing((v) => v.prerelease.length === 0), // nonempty prerelease compares less than empty prerelease + comparing((v) => v.prerelease, lexicographically(compareIdentifiers)) +) + +const sortVersions = (versions: string[]): string[] => { + const parsed = versions.map(parseVersion) + parsed.sort(compareVersions) + parsed.reverse() + return parsed.map((v) => v.original) }