From 7b4d3240783384da6c313d58bc3d5f892b55fa0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?X=C3=B9d=C5=8Dng=20Y=C3=A1ng?= Date: Tue, 23 Jul 2024 21:00:14 -0700 Subject: [PATCH] Implement proper version sorting Got bored and wanted to explore Typescript. Tested on https://typescriptlang.org/play: > console.log(sortVersions(['1.2.3', '1.2.3-d', '1.2.3.bcr.1', '1.2.3rc3', '1', 'k'])) ["k", "1.2.3rc3", "1.2.3.bcr.1", "1.2.3", "1.2.3-d", "1"] Addresses #54. Fixes #142. --- data/moduleStaticProps.ts | 85 +++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 17 deletions(-) diff --git a/data/moduleStaticProps.ts b/data/moduleStaticProps.ts index b40604ddd68c..3c1502db7a5d 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) }