-
-
Notifications
You must be signed in to change notification settings - Fork 424
Expand file tree
/
Copy pathversions.ts
More file actions
209 lines (189 loc) · 6.14 KB
/
versions.ts
File metadata and controls
209 lines (189 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import { compare, satisfies, validRange, valid } from 'semver'
/**
* Utilities for handling npm package versions and dist-tags
*/
/**
* Check if a version string is an exact semver version.
* Returns true for "1.2.3", "1.0.0-beta.1", etc.
* Returns false for ranges like "^1.2.3", ">=1.0.0", tags like "latest", etc.
* @param version - The version string to check
* @returns true if the version is an exact semver version
*/
export function isExactVersion(version: string): boolean {
return valid(version) !== null
}
/** Parsed semver version components */
export interface ParsedVersion {
major: number
minor: number
patch: number
prerelease: string
}
/**
* Parse a semver version string into its components
* @param version - The version string (e.g., "1.2.3" or "1.0.0-beta.1")
* @returns Parsed version object with major, minor, patch, and prerelease
*/
export function parseVersion(version: string): ParsedVersion {
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?/)
if (!match) return { major: 0, minor: 0, patch: 0, prerelease: '' }
return {
major: Number(match[1]),
minor: Number(match[2]),
patch: Number(match[3]),
prerelease: match[4] ?? '',
}
}
/**
* Extract the prerelease channel from a version string
* @param version - The version string (e.g., "1.0.0-beta.1")
* @returns The channel name (e.g., "beta") or empty string for stable versions
*/
export function getPrereleaseChannel(version: string): string {
const parsed = parseVersion(version)
if (!parsed.prerelease) return ''
const match = parsed.prerelease.match(/^([a-z]+)/i)
return match ? match[1]!.toLowerCase() : ''
}
/**
* Sort tags with 'latest' first, then alphabetically
* @param tags - Array of tag names
* @returns New sorted array
*/
export function sortTags(tags: string[]): string[] {
return [...tags].sort((a, b) => {
if (a === 'latest') return -1
if (b === 'latest') return 1
return a.localeCompare(b)
})
}
/**
* Build a map from version strings to their associated dist-tags
* Handles the case where multiple tags point to the same version
* @param distTags - Object mapping tag names to version strings
* @returns Map from version to sorted array of tags
*/
export function buildVersionToTagsMap(distTags: Record<string, string>): Map<string, string[]> {
const map = new Map<string, string[]>()
for (const [tag, version] of Object.entries(distTags)) {
const existing = map.get(version)
if (existing) {
existing.push(tag)
} else {
map.set(version, [tag])
}
}
// Sort tags within each version
for (const tags of map.values()) {
tags.sort((a, b) => {
if (a === 'latest') return -1
if (b === 'latest') return 1
return a.localeCompare(b)
})
}
return map
}
/** A tagged version row for display */
export interface TaggedVersionRow {
/** Unique identifier for the row */
id: string
/** Primary tag (first in sorted order, used for expand/collapse) */
primaryTag: string
/** All tags for this version */
tags: string[]
/** The version string */
version: string
}
/**
* Build deduplicated rows for tagged versions
* Each unique version appears once with all its tags
* @param distTags - Object mapping tag names to version strings
* @returns Array of rows sorted by version (descending)
*/
export function buildTaggedVersionRows(distTags: Record<string, string>): TaggedVersionRow[] {
const versionToTags = buildVersionToTagsMap(distTags)
return Array.from(versionToTags.entries())
.map(([version, tags]) => ({
id: `version:${version}`,
primaryTag: tags[0]!,
tags,
version,
}))
.sort((a, b) => compare(b.version, a.version))
}
/**
* Filter tags to exclude those already shown in a parent context
* Useful when showing nested versions that shouldn't repeat parent tags
* @param tags - Tags to filter
* @param excludeTags - Tags to exclude
* @returns Filtered array of tags
*/
export function filterExcludedTags(tags: string[], excludeTags: string[]): string[] {
const excludeSet = new Set(excludeTags)
return tags.filter(tag => !excludeSet.has(tag))
}
/**
* Get a grouping key for a version that handles 0.x versions specially.
*
* Per semver spec, versions below 1.0.0 can have breaking changes in minor bumps,
* so 0.9.x should be in a separate group from 0.10.x.
*
* @param version - The version string (e.g., "0.9.3", "1.2.3")
* @returns A grouping key string (e.g., "0.9", "1")
*/
export function getVersionGroupKey(version: string): string {
const parsed = parseVersion(version)
if (parsed.major === 0) {
// For 0.x versions, group by major.minor
return `0.${parsed.minor}`
}
// For 1.x+, group by major only
return String(parsed.major)
}
/**
* Get a display label for a version group key.
*
* @param groupKey - The group key from getVersionGroupKey()
* @returns A display label (e.g., "0.9.x", "1.x")
*/
export function getVersionGroupLabel(groupKey: string): string {
return `${groupKey}.x`
}
/**
* Check if two versions belong to the same version group.
*
* For versions >= 1.0.0, same major = same group.
* For versions < 1.0.0, same major.minor = same group.
*
* @param versionA - First version string
* @param versionB - Second version string
* @returns true if both versions are in the same group
*/
export function isSameVersionGroup(versionA: string, versionB: string): boolean {
return getVersionGroupKey(versionA) === getVersionGroupKey(versionB)
}
/**
* Filter versions by a semver range string.
*
* @param versions - Array of version strings to filter
* @param range - A semver range string (e.g., "^3.0.0", ">=2.0.0 <3.0.0")
* @returns Set of version strings that satisfy the range.
* Returns all versions if range is empty/whitespace.
* Returns empty set if range is invalid.
*/
export function filterVersions(versions: string[], range: string): Set<string> {
const trimmed = range.trim()
if (trimmed === '') {
return new Set(versions)
}
if (!validRange(trimmed)) {
return new Set()
}
const matched = new Set<string>()
for (const v of versions) {
if (satisfies(v, trimmed, { includePrerelease: true })) {
matched.add(v)
}
}
return matched
}