Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions scripts/build-version-index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,36 @@ import { fileURLToPath } from 'node:url'

const SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/

function comparePrerelease(a, b) {
if (a === b) return 0
if (!a) return 1
if (!b) return -1
const la = a.split('.')
const lb = b.split('.')
const length = Math.max(la.length, lb.length)
for (let i = 0; i < length; i += 1) {
const x = la[i]
const y = lb[i]
if (x === undefined) return -1
if (y === undefined) return 1
if (x === y) continue
const xn = /^\d+$/.test(x)
const yn = /^\d+$/.test(y)
if (xn && yn) {
const nx = x.replace(/^0+/, '') || '0'
const ny = y.replace(/^0+/, '') || '0'
if (nx.length !== ny.length) return nx.length < ny.length ? -1 : 1
if (nx !== ny) return nx < ny ? -1 : 1
continue
}
if (xn) return -1
if (yn) return 1
if (x < y) return -1
if (x > y) return 1
}
return 0
}

function compare(a, b) {
const pa = a.split('-')[0].split('.').map(Number)
const pb = b.split('-')[0].split('.').map(Number)
Expand All @@ -11,10 +41,7 @@ function compare(a, b) {
}
const preA = a.includes('-') ? a.slice(a.indexOf('-') + 1) : ''
const preB = b.includes('-') ? b.slice(b.indexOf('-') + 1) : ''
if (preA === preB) return 0
if (!preA) return 1
if (!preB) return -1
return preA < preB ? -1 : 1
return comparePrerelease(preA, preB)
}

/**
Expand Down
46 changes: 42 additions & 4 deletions src/main/update/version-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,48 @@ export function compareVersions(a: string, b: string): -1 | 0 | 1 {
const diff = (left.nums[i] ?? 0) - (right.nums[i] ?? 0)
if (diff !== 0) return diff < 0 ? -1 : 1
}
if (left.pre === right.pre) return 0
if (!left.pre) return 1 // release > prerelease
if (!right.pre) return -1
return left.pre < right.pre ? -1 : 1
return comparePrerelease(left.pre, right.pre)
}

/**
* Semver-style prerelease precedence once the numeric core is equal. A release
* (no prerelease) sorts above any prerelease; dot-separated identifiers
* compare with numeric identifiers numerically and below alphanumeric ones,
* and fewer identifiers sort below more ("alpha" < "alpha.1"). Plain string
* comparison would order "rc.10" below "rc.9", mis-sorting the archive index
* (and the picker/downgrade split in the preload) once a prerelease counter
* reaches two digits.
*/
function comparePrerelease(left: string, right: string): -1 | 0 | 1 {
if (left === right) return 0
if (!left) return 1 // release > prerelease
if (!right) return -1
const l = left.split('.')
const r = right.split('.')
const length = Math.max(l.length, r.length)
for (let i = 0; i < length; i += 1) {
const x = l[i]
const y = r[i]
if (x === undefined) return -1 // fewer identifiers sorts below
if (y === undefined) return 1
if (x === y) continue
const xn = /^\d+$/.test(x)
const yn = /^\d+$/.test(y)
if (xn && yn) {
// Compare without Number() so leading-zero forms and large counters do
// not lose precision.
const nx = x.replace(/^0+/, '') || '0'
const ny = y.replace(/^0+/, '') || '0'
if (nx.length !== ny.length) return nx.length < ny.length ? -1 : 1
if (nx !== ny) return nx < ny ? -1 : 1
continue
}
if (xn) return -1 // numeric identifiers sort below alphanumeric ones
if (yn) return 1
if (x < y) return -1
if (x > y) return 1
}
return 0
}

function isRelease(value: unknown): value is AvailableRelease {
Expand Down
39 changes: 34 additions & 5 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,9 @@ function skipButton(status: UpdateStatus): HTMLButtonElement {

/** Compare two dotted versions; prerelease sorts below its release. Mirrors
* `version-catalog.compareVersions` — a small duplication across the
* main/preload boundary, kept local so the preload bundle stays standalone. */
* main/preload boundary, kept local so the preload bundle stays standalone.
* Keep the two implementations in lockstep, including the semver-style
* numeric prerelease comparison below ("rc.10" > "rc.9"). */
function comparePreloadVersions(a: string, b: string): number {
const parse = (value: string): [number[], string] => {
const [core = '', ...pre] = value.trim().split('-')
Expand All @@ -557,10 +559,37 @@ function comparePreloadVersions(a: string, b: string): number {
for (let i = 0; i < 3; i += 1) {
if ((an[i] ?? 0) !== (bn[i] ?? 0)) return (an[i] ?? 0) - (bn[i] ?? 0)
}
if (ap === bp) return 0
if (!ap) return 1
if (!bp) return -1
return ap < bp ? -1 : 1
return comparePrerelease(ap, bp)
}

function comparePrerelease(left: string, right: string): number {
if (left === right) return 0
if (!left) return 1
if (!right) return -1
const l = left.split('.')
const r = right.split('.')
const length = Math.max(l.length, r.length)
for (let i = 0; i < length; i += 1) {
const x = l[i]
const y = r[i]
if (x === undefined) return -1
if (y === undefined) return 1
if (x === y) continue
const xn = /^\d+$/.test(x)
const yn = /^\d+$/.test(y)
if (xn && yn) {
const nx = x.replace(/^0+/, '') || '0'
const ny = y.replace(/^0+/, '') || '0'
if (nx.length !== ny.length) return nx.length < ny.length ? -1 : 1
if (nx !== ny) return nx < ny ? -1 : 1
continue
}
if (xn) return -1
if (yn) return 1
if (x < y) return -1
if (x > y) return 1
}
return 0
}

function loadVersionList(onDone?: () => void): void {
Expand Down
15 changes: 15 additions & 0 deletions test/build-version-index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ describe('buildVersionIndex', () => {
])
})

it('orders prerelease counters numerically so rc.10 is newer than rc.9', () => {
const index = buildVersionIndex([
'1.2.3-rc.1',
'1.2.3-rc.10',
'1.2.3-rc.9',
'1.2.3-rc.2'
])
expect(index.versions.map((v: { version: string }) => v.version)).toEqual([
'1.2.3-rc.10',
'1.2.3-rc.9',
'1.2.3-rc.2',
'1.2.3-rc.1'
])
})

it('derives tag and archiveUrl for each entry', () => {
const [entry] = buildVersionIndex(['3.4.5']).versions
expect(entry).toEqual({
Expand Down
17 changes: 17 additions & 0 deletions test/version-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ describe('compareVersions', () => {
expect(compareVersions('1.2.3', '1.2.3-rc.1')).toBe(1)
expect(compareVersions('1.2.3-rc.1', '1.2.3-rc.2')).toBe(-1)
})

it('compares prerelease counters numerically, not lexicographically', () => {
// "rc.10" < "rc.9" under string comparison; semver says the reverse.
expect(compareVersions('1.2.3-rc.10', '1.2.3-rc.9')).toBe(1)
expect(compareVersions('1.2.3-rc.9', '1.2.3-rc.10')).toBe(-1)
expect(compareVersions('1.2.3-alpha.10', '1.2.3-alpha.9')).toBe(1)
expect(compareVersions('1.2.3-rc.10', '1.2.3-rc.1')).toBe(1)
})

it('follows semver identifier precedence', () => {
// fewer identifiers < more ("alpha" < "alpha.1")
expect(compareVersions('1.2.3-alpha', '1.2.3-alpha.1')).toBe(-1)
// numeric identifiers < alphanumeric ones ("1" < "alpha")
expect(compareVersions('1.2.3-1', '1.2.3-alpha')).toBe(-1)
expect(compareVersions('1.2.3-alpha', '1.2.3-beta')).toBe(-1)
expect(compareVersions('1.2.3-rc.10', '1.2.3-rc.10')).toBe(0)
})
})

describe('parseVersionIndex', () => {
Expand Down