···11+/**
22+ * Utilities for handling npm package versions and dist-tags
33+ */
44+55+/** Parsed semver version components */
66+export interface ParsedVersion {
77+ major: number
88+ minor: number
99+ patch: number
1010+ prerelease: string
1111+}
1212+1313+/**
1414+ * Parse a semver version string into its components
1515+ * @param version - The version string (e.g., "1.2.3" or "1.0.0-beta.1")
1616+ * @returns Parsed version object with major, minor, patch, and prerelease
1717+ */
1818+export function parseVersion(version: string): ParsedVersion {
1919+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?/)
2020+ if (!match) return { major: 0, minor: 0, patch: 0, prerelease: '' }
2121+ return {
2222+ major: Number(match[1]),
2323+ minor: Number(match[2]),
2424+ patch: Number(match[3]),
2525+ prerelease: match[4] ?? '',
2626+ }
2727+}
2828+2929+/**
3030+ * Compare two semver versions for sorting
3131+ * Returns positive if a > b, negative if a < b, 0 if equal
3232+ * @param a - First version string
3333+ * @param b - Second version string
3434+ * @returns Comparison result for sorting
3535+ */
3636+export function compareVersions(a: string, b: string): number {
3737+ const va = parseVersion(a)
3838+ const vb = parseVersion(b)
3939+4040+ if (va.major !== vb.major) return va.major - vb.major
4141+ if (va.minor !== vb.minor) return va.minor - vb.minor
4242+ if (va.patch !== vb.patch) return va.patch - vb.patch
4343+4444+ // Stable versions (no prerelease) are greater than prereleases
4545+ if (va.prerelease && vb.prerelease) return va.prerelease.localeCompare(vb.prerelease)
4646+ if (va.prerelease) return -1
4747+ if (vb.prerelease) return 1
4848+4949+ return 0
5050+}
5151+5252+/**
5353+ * Extract the prerelease channel from a version string
5454+ * @param version - The version string (e.g., "1.0.0-beta.1")
5555+ * @returns The channel name (e.g., "beta") or empty string for stable versions
5656+ */
5757+export function getPrereleaseChannel(version: string): string {
5858+ const parsed = parseVersion(version)
5959+ if (!parsed.prerelease) return ''
6060+ const match = parsed.prerelease.match(/^([a-z]+)/i)
6161+ return match ? match[1]!.toLowerCase() : ''
6262+}
6363+6464+/**
6565+ * Sort tags with 'latest' first, then alphabetically
6666+ * @param tags - Array of tag names
6767+ * @returns New sorted array
6868+ */
6969+export function sortTags(tags: string[]): string[] {
7070+ return [...tags].sort((a, b) => {
7171+ if (a === 'latest') return -1
7272+ if (b === 'latest') return 1
7373+ return a.localeCompare(b)
7474+ })
7575+}
7676+7777+/**
7878+ * Build a map from version strings to their associated dist-tags
7979+ * Handles the case where multiple tags point to the same version
8080+ * @param distTags - Object mapping tag names to version strings
8181+ * @returns Map from version to sorted array of tags
8282+ */
8383+export function buildVersionToTagsMap(distTags: Record<string, string>): Map<string, string[]> {
8484+ const map = new Map<string, string[]>()
8585+8686+ for (const [tag, version] of Object.entries(distTags)) {
8787+ const existing = map.get(version)
8888+ if (existing) {
8989+ existing.push(tag)
9090+ } else {
9191+ map.set(version, [tag])
9292+ }
9393+ }
9494+9595+ // Sort tags within each version
9696+ for (const tags of map.values()) {
9797+ tags.sort((a, b) => {
9898+ if (a === 'latest') return -1
9999+ if (b === 'latest') return 1
100100+ return a.localeCompare(b)
101101+ })
102102+ }
103103+104104+ return map
105105+}
106106+107107+/** A tagged version row for display */
108108+export interface TaggedVersionRow {
109109+ /** Unique identifier for the row */
110110+ id: string
111111+ /** Primary tag (first in sorted order, used for expand/collapse) */
112112+ primaryTag: string
113113+ /** All tags for this version */
114114+ tags: string[]
115115+ /** The version string */
116116+ version: string
117117+}
118118+119119+/**
120120+ * Build deduplicated rows for tagged versions
121121+ * Each unique version appears once with all its tags
122122+ * @param distTags - Object mapping tag names to version strings
123123+ * @returns Array of rows sorted by version (descending)
124124+ */
125125+export function buildTaggedVersionRows(distTags: Record<string, string>): TaggedVersionRow[] {
126126+ const versionToTags = buildVersionToTagsMap(distTags)
127127+128128+ return Array.from(versionToTags.entries())
129129+ .map(([version, tags]) => ({
130130+ id: `version:${version}`,
131131+ primaryTag: tags[0]!,
132132+ tags,
133133+ version,
134134+ }))
135135+ .sort((a, b) => compareVersions(b.version, a.version))
136136+}
137137+138138+/**
139139+ * Filter tags to exclude those already shown in a parent context
140140+ * Useful when showing nested versions that shouldn't repeat parent tags
141141+ * @param tags - Tags to filter
142142+ * @param excludeTags - Tags to exclude
143143+ * @returns Filtered array of tags
144144+ */
145145+export function filterExcludedTags(tags: string[], excludeTags: string[]): string[] {
146146+ const excludeSet = new Set(excludeTags)
147147+ return tags.filter(tag => !excludeSet.has(tag))
148148+}