Skip to content

Commit 35e96d4

Browse files
committed
fix: handle versions below 1.0.0
resolves #153
1 parent 3c7b8f9 commit 35e96d4

3 files changed

Lines changed: 173 additions & 34 deletions

File tree

app/components/PackageVersions.vue

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import {
66
buildVersionToTagsMap,
77
filterExcludedTags,
88
getPrereleaseChannel,
9+
getVersionGroupKey,
10+
getVersionGroupLabel,
11+
isSameVersionGroup,
912
parseVersion,
1013
} from '~/utils/versions'
1114
import { fetchAllPackageVersions } from '~/composables/useNpmRegistry'
@@ -120,8 +123,10 @@ const tagVersions = ref<Map<string, VersionDisplay[]>>(new Map())
120123
const loadingTags = ref<Set<string>>(new Set())
121124
122125
const otherVersionsExpanded = shallowRef(false)
123-
const expandedMajorGroups = ref<Set<number>>(new Set())
124-
const otherMajorGroups = shallowRef<Array<{ major: number; versions: VersionDisplay[] }>>([])
126+
const expandedMajorGroups = ref<Set<string>>(new Set())
127+
const otherMajorGroups = shallowRef<
128+
Array<{ groupKey: string; label: string; versions: VersionDisplay[] }>
129+
>([])
125130
const otherVersionsLoading = shallowRef(false)
126131
127132
// Cached full version list (local to component instance)
@@ -167,14 +172,14 @@ function processLoadedVersions(allVersions: PackageVersionInfo[]) {
167172
const tagVersion = distTags[row.tag]
168173
if (!tagVersion) continue
169174
170-
const tagParsed = parseVersion(tagVersion)
171175
const tagChannel = getPrereleaseChannel(tagVersion)
172176
177+
// Find all versions in the same version group + prerelease channel
178+
// For 0.x versions, this means same major.minor; for 1.x+, same major
173179
const channelVersions = allVersions
174180
.filter(v => {
175-
const vParsed = parseVersion(v.version)
176181
const vChannel = getPrereleaseChannel(v.version)
177-
return vParsed.major === tagParsed.major && vChannel === tagChannel
182+
return isSameVersionGroup(v.version, tagVersion) && vChannel === tagChannel
178183
})
179184
.sort((a, b) => compare(b.version, a.version))
180185
.map(v => ({
@@ -192,17 +197,19 @@ function processLoadedVersions(allVersions: PackageVersionInfo[]) {
192197
}
193198
}
194199
195-
// Group unclaimed versions by major
196-
const byMajor = new Map<number, VersionDisplay[]>()
200+
// Group unclaimed versions by version group key
201+
// For 0.x versions, group by major.minor (e.g., "0.9", "0.10")
202+
// For 1.x+, group by major (e.g., "1", "2")
203+
const byGroupKey = new Map<string, VersionDisplay[]>()
197204
198205
for (const v of allVersions) {
199206
if (claimedVersions.has(v.version)) continue
200207
201-
const major = parseVersion(v.version).major
202-
if (!byMajor.has(major)) {
203-
byMajor.set(major, [])
208+
const groupKey = getVersionGroupKey(v.version)
209+
if (!byGroupKey.has(groupKey)) {
210+
byGroupKey.set(groupKey, [])
204211
}
205-
byMajor.get(major)!.push({
212+
byGroupKey.get(groupKey)!.push({
206213
version: v.version,
207214
time: v.time,
208215
tags: versionToTags.value.get(v.version),
@@ -211,16 +218,23 @@ function processLoadedVersions(allVersions: PackageVersionInfo[]) {
211218
})
212219
}
213220
214-
// Sort within each major
215-
for (const versions of byMajor.values()) {
221+
// Sort within each group
222+
for (const versions of byGroupKey.values()) {
216223
versions.sort((a, b) => compare(b.version, a.version))
217224
}
218225
219-
// Build major groups sorted by major descending
220-
const sortedMajors = Array.from(byMajor.keys()).sort((a, b) => b - a)
221-
otherMajorGroups.value = sortedMajors.map(major => ({
222-
major,
223-
versions: byMajor.get(major)!,
226+
// Build groups sorted by group key descending
227+
// Sort: "2", "1", "0.10", "0.9" (numerically descending)
228+
const sortedGroupKeys = Array.from(byGroupKey.keys()).sort((a, b) => {
229+
const [aMajor, aMinor] = a.split('.').map(Number)
230+
const [bMajor, bMinor] = b.split('.').map(Number)
231+
if (aMajor !== bMajor) return (bMajor ?? 0) - (aMajor ?? 0)
232+
return (bMinor ?? -1) - (aMinor ?? -1)
233+
})
234+
otherMajorGroups.value = sortedGroupKeys.map(groupKey => ({
235+
groupKey,
236+
label: getVersionGroupLabel(groupKey),
237+
versions: byGroupKey.get(groupKey)!,
224238
}))
225239
expandedMajorGroups.value.clear()
226240
}
@@ -275,12 +289,12 @@ async function expandOtherVersions() {
275289
otherVersionsExpanded.value = true
276290
}
277291
278-
// Toggle a major group
279-
function toggleMajorGroup(major: number) {
280-
if (expandedMajorGroups.value.has(major)) {
281-
expandedMajorGroups.value.delete(major)
292+
// Toggle a version group
293+
function toggleMajorGroup(groupKey: string) {
294+
if (expandedMajorGroups.value.has(groupKey)) {
295+
expandedMajorGroups.value.delete(groupKey)
282296
} else {
283-
expandedMajorGroups.value.add(major)
297+
expandedMajorGroups.value.add(groupKey)
284298
}
285299
}
286300
@@ -521,28 +535,28 @@ function getTagVersions(tag: string): VersionDisplay[] {
521535
</div>
522536
</div>
523537

524-
<!-- Major version groups (untagged versions) -->
538+
<!-- Version groups (untagged versions) -->
525539
<template v-if="otherMajorGroups.length > 0">
526-
<div v-for="group in otherMajorGroups" :key="group.major">
527-
<!-- Major group header -->
540+
<div v-for="group in otherMajorGroups" :key="group.groupKey">
541+
<!-- Version group header -->
528542
<div v-if="group.versions.length > 1" class="py-1">
529543
<div class="flex items-center justify-between gap-2">
530544
<div class="flex items-center gap-2 min-w-0">
531545
<button
532546
type="button"
533547
class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg-muted focus-visible:ring-offset-1 focus-visible:ring-offset-bg rounded-sm"
534-
:aria-expanded="expandedMajorGroups.has(group.major)"
548+
:aria-expanded="expandedMajorGroups.has(group.groupKey)"
535549
:aria-label="
536-
expandedMajorGroups.has(group.major)
537-
? $t('package.versions.collapse_major', { major: group.major })
538-
: $t('package.versions.expand_major', { major: group.major })
550+
expandedMajorGroups.has(group.groupKey)
551+
? $t('package.versions.collapse_major', { major: group.label })
552+
: $t('package.versions.expand_major', { major: group.label })
539553
"
540-
@click="toggleMajorGroup(group.major)"
554+
@click="toggleMajorGroup(group.groupKey)"
541555
>
542556
<span
543557
class="w-3 h-3 transition-transform duration-200"
544558
:class="
545-
expandedMajorGroups.has(group.major)
559+
expandedMajorGroups.has(group.groupKey)
546560
? 'i-carbon-chevron-down'
547561
: 'i-carbon-chevron-right'
548562
"
@@ -653,9 +667,9 @@ function getTagVersions(tag: string): VersionDisplay[] {
653667
</div>
654668
</div>
655669

656-
<!-- Major group versions -->
670+
<!-- Version group versions -->
657671
<div
658-
v-if="expandedMajorGroups.has(group.major) && group.versions.length > 1"
672+
v-if="expandedMajorGroups.has(group.groupKey) && group.versions.length > 1"
659673
class="ml-6 space-y-0.5"
660674
>
661675
<div v-for="v in group.versions.slice(1)" :key="v.version" class="py-1">

app/utils/versions.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,46 @@ export function filterExcludedTags(tags: string[], excludeTags: string[]): strin
136136
const excludeSet = new Set(excludeTags)
137137
return tags.filter(tag => !excludeSet.has(tag))
138138
}
139+
140+
/**
141+
* Get a grouping key for a version that handles 0.x versions specially.
142+
*
143+
* Per semver spec, versions below 1.0.0 can have breaking changes in minor bumps,
144+
* so 0.9.x should be in a separate group from 0.10.x.
145+
*
146+
* @param version - The version string (e.g., "0.9.3", "1.2.3")
147+
* @returns A grouping key string (e.g., "0.9", "1")
148+
*/
149+
export function getVersionGroupKey(version: string): string {
150+
const parsed = parseVersion(version)
151+
if (parsed.major === 0) {
152+
// For 0.x versions, group by major.minor
153+
return `0.${parsed.minor}`
154+
}
155+
// For 1.x+, group by major only
156+
return String(parsed.major)
157+
}
158+
159+
/**
160+
* Get a display label for a version group key.
161+
*
162+
* @param groupKey - The group key from getVersionGroupKey()
163+
* @returns A display label (e.g., "0.9.x", "1.x")
164+
*/
165+
export function getVersionGroupLabel(groupKey: string): string {
166+
return `${groupKey}.x`
167+
}
168+
169+
/**
170+
* Check if two versions belong to the same version group.
171+
*
172+
* For versions >= 1.0.0, same major = same group.
173+
* For versions < 1.0.0, same major.minor = same group.
174+
*
175+
* @param versionA - First version string
176+
* @param versionB - Second version string
177+
* @returns true if both versions are in the same group
178+
*/
179+
export function isSameVersionGroup(versionA: string, versionB: string): boolean {
180+
return getVersionGroupKey(versionA) === getVersionGroupKey(versionB)
181+
}

test/unit/versions.spec.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import {
44
buildVersionToTagsMap,
55
filterExcludedTags,
66
getPrereleaseChannel,
7+
getVersionGroupKey,
8+
getVersionGroupLabel,
9+
isSameVersionGroup,
710
parseVersion,
811
sortTags,
912
} from '../../app/utils/versions'
@@ -305,3 +308,82 @@ describe('filterExcludedTags', () => {
305308
expect(filterExcludedTags(['beta', 'rc'], ['latest'])).toEqual(['beta', 'rc'])
306309
})
307310
})
311+
312+
describe('getVersionGroupKey', () => {
313+
it('groups 1.x+ versions by major only', () => {
314+
expect(getVersionGroupKey('1.0.0')).toBe('1')
315+
expect(getVersionGroupKey('1.5.3')).toBe('1')
316+
expect(getVersionGroupKey('2.0.0')).toBe('2')
317+
expect(getVersionGroupKey('10.5.2')).toBe('10')
318+
})
319+
320+
it('groups 0.x versions by major.minor', () => {
321+
expect(getVersionGroupKey('0.1.0')).toBe('0.1')
322+
expect(getVersionGroupKey('0.1.5')).toBe('0.1')
323+
expect(getVersionGroupKey('0.9.0')).toBe('0.9')
324+
expect(getVersionGroupKey('0.9.3')).toBe('0.9')
325+
expect(getVersionGroupKey('0.10.0')).toBe('0.10')
326+
expect(getVersionGroupKey('0.10.5')).toBe('0.10')
327+
})
328+
329+
it('handles prerelease versions', () => {
330+
expect(getVersionGroupKey('1.0.0-beta.1')).toBe('1')
331+
expect(getVersionGroupKey('0.5.0-alpha.1')).toBe('0.5')
332+
})
333+
})
334+
335+
describe('getVersionGroupLabel', () => {
336+
it('formats 1.x+ group keys', () => {
337+
expect(getVersionGroupLabel('1')).toBe('1.x')
338+
expect(getVersionGroupLabel('2')).toBe('2.x')
339+
expect(getVersionGroupLabel('10')).toBe('10.x')
340+
})
341+
342+
it('formats 0.x group keys', () => {
343+
expect(getVersionGroupLabel('0.1')).toBe('0.1.x')
344+
expect(getVersionGroupLabel('0.9')).toBe('0.9.x')
345+
expect(getVersionGroupLabel('0.10')).toBe('0.10.x')
346+
})
347+
})
348+
349+
describe('isSameVersionGroup', () => {
350+
it('groups 1.x+ versions by major', () => {
351+
expect(isSameVersionGroup('1.0.0', '1.5.3')).toBe(true)
352+
expect(isSameVersionGroup('1.0.0', '1.99.99')).toBe(true)
353+
expect(isSameVersionGroup('2.0.0', '2.1.0')).toBe(true)
354+
})
355+
356+
it('separates different major versions', () => {
357+
expect(isSameVersionGroup('1.0.0', '2.0.0')).toBe(false)
358+
expect(isSameVersionGroup('1.5.3', '2.0.0')).toBe(false)
359+
})
360+
361+
it('groups 0.x versions by major.minor', () => {
362+
expect(isSameVersionGroup('0.1.0', '0.1.5')).toBe(true)
363+
expect(isSameVersionGroup('0.9.0', '0.9.3')).toBe(true)
364+
expect(isSameVersionGroup('0.10.0', '0.10.5')).toBe(true)
365+
})
366+
367+
it('separates different 0.x minor versions', () => {
368+
// This is the key test: 0.9.x should NOT be grouped with 0.10.x
369+
expect(isSameVersionGroup('0.9.0', '0.10.0')).toBe(false)
370+
expect(isSameVersionGroup('0.9.3', '0.10.5')).toBe(false)
371+
expect(isSameVersionGroup('0.1.0', '0.2.0')).toBe(false)
372+
})
373+
374+
it('separates 0.x from 1.x', () => {
375+
expect(isSameVersionGroup('0.9.0', '1.0.0')).toBe(false)
376+
expect(isSameVersionGroup('0.99.99', '1.0.0')).toBe(false)
377+
})
378+
379+
it('handles prerelease versions in 1.x+', () => {
380+
expect(isSameVersionGroup('1.0.0-beta.1', '1.0.0')).toBe(true)
381+
expect(isSameVersionGroup('1.0.0-alpha.1', '1.5.0')).toBe(true)
382+
})
383+
384+
it('handles prerelease versions in 0.x', () => {
385+
expect(isSameVersionGroup('0.5.0-beta.1', '0.5.0')).toBe(true)
386+
expect(isSameVersionGroup('0.5.0-alpha.1', '0.5.3')).toBe(true)
387+
expect(isSameVersionGroup('0.5.0-beta.1', '0.6.0')).toBe(false)
388+
})
389+
})

0 commit comments

Comments
 (0)