Skip to content

Commit 3778859

Browse files
authored
refactor: use semver instead of manual implementations (#48)
* refactor: use `semver` instead of manual implementations * fix: compare prerelease correctly
1 parent 31ac4b6 commit 3778859

8 files changed

Lines changed: 34 additions & 116 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
},
156156
"devDependencies": {
157157
"@types/node": "catalog:dev",
158+
"@types/semver": "catalog:dev",
158159
"@types/vscode": "1.101.0",
159160
"@vida0905/eslint-config": "catalog:dev",
160161
"eslint": "catalog:dev",
@@ -167,6 +168,7 @@
167168
"ofetch": "catalog:inline",
168169
"perfect-debounce": "catalog:inline",
169170
"reactive-vscode": "catalog:inline",
171+
"semver": "catalog:inline",
170172
"tsdown": "catalog:dev",
171173
"typescript": "catalog:dev",
172174
"vite-tsconfig-paths": "catalog:test",

pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ trustPolicy: no-downgrade
44
catalogs:
55
dev:
66
'@types/node': ^25.3.0
7+
'@types/semver': ^7.7.1
78
'@vida0905/eslint-config': ^2.10.1
89
eslint: ^10.0.1
910
husky: ^9.1.7
@@ -18,6 +19,7 @@ catalogs:
1819
ofetch: ^2.0.0-alpha.3
1920
perfect-debounce: ^2.1.0
2021
reactive-vscode: ^1.0.0-beta.2
22+
semver: ^7.7.4
2123
yaml: ^2.8.2
2224
test:
2325
jest-mock-vscode: ^4.11.0

src/providers/diagnostics/rules/upgrade.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { DependencyInfo } from '#types/extractor'
22
import type { ParsedVersion } from '#utils/version'
33
import type { DiagnosticRule, NodeDiagnosticInfo } from '..'
4-
import { formatVersion, getPrereleaseId, isSupportedProtocol, lt, parseVersion } from '#utils/version'
4+
import { formatVersion, isSupportedProtocol, parseVersion } from '#utils/version'
5+
import prerelease from 'semver/functions/prerelease'
6+
import gtr from 'semver/ranges/gtr'
7+
import ltr from 'semver/ranges/ltr'
58
import { DiagnosticSeverity } from 'vscode'
69

710
function createUpgradeDiagnostic(dep: DependencyInfo, parsed: ParsedVersion, upgradeVersion: string): NodeDiagnosticInfo {
@@ -22,19 +25,19 @@ export const checkUpgrade: DiagnosticRule = (dep, pkg) => {
2225
const { semver } = parsed
2326
const latest = pkg.distTags.latest
2427

25-
if (latest && lt(semver, latest))
28+
if (latest && gtr(latest, semver))
2629
return createUpgradeDiagnostic(dep, parsed, latest)
2730

28-
const currentPreId = getPrereleaseId(semver)
29-
if (!currentPreId)
31+
const currentPreId = prerelease(semver)?.[0]
32+
if (currentPreId == null)
3033
return
3134

3235
for (const [tag, tagVersion] of Object.entries(pkg.distTags)) {
3336
if (tag === 'latest')
3437
continue
35-
if (getPrereleaseId(tagVersion) !== currentPreId)
38+
if (prerelease(tagVersion)?.[0] !== currentPreId)
3639
continue
37-
if (!lt(semver, tagVersion))
40+
if (ltr(tagVersion, semver))
3841
continue
3942

4043
return createUpgradeDiagnostic(dep, parsed, tagVersion)

src/providers/diagnostics/rules/vulnerability.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { OsvSeverityLevel, PackageVulnerabilityInfo } from '#utils/api/vuln
22
import type { DiagnosticRule } from '..'
33
import { getVulnerability, SEVERITY_LEVELS } from '#utils/api/vulnerability'
44
import { npmxPackageUrl } from '#utils/links'
5-
import { formatVersion, isSupportedProtocol, lt, parseVersion } from '#utils/version'
5+
import { formatVersion, isSupportedProtocol, parseVersion } from '#utils/version'
6+
import lt from 'semver/functions/lt'
67
import { DiagnosticSeverity, Uri } from 'vscode'
78

89
const DIAGNOSTIC_MAPPING: Record<Exclude<OsvSeverityLevel, 'unknown'>, DiagnosticSeverity> = {

src/utils/version.ts

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -48,56 +48,3 @@ export function parseVersion(rawVersion: string): ParsedVersion | null {
4848

4949
return { protocol, prefix, semver }
5050
}
51-
52-
export function getPrereleaseId(version: string): string | null {
53-
const idx = version.indexOf('-')
54-
if (idx === -1)
55-
return null
56-
const pre = version.slice(idx + 1).split('.')[0]
57-
return pre || null
58-
}
59-
60-
/**
61-
* Compare two pre-release strings part by part following SemVer precedence rules.
62-
*
63-
* Numeric parts are compared as numbers, string parts are compared lexicographically.
64-
* A version with fewer parts is less than one with more parts when all preceding parts are equal.
65-
*/
66-
function comparePrereleasePrecedence(a: string, b: string): number {
67-
const partsA = a.split('.')
68-
const partsB = b.split('.')
69-
70-
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
71-
if (i >= partsA.length)
72-
return -1
73-
if (i >= partsB.length)
74-
return 1
75-
76-
const numA = Number(partsA[i])
77-
const numB = Number(partsB[i])
78-
if (!Number.isNaN(numA) && !Number.isNaN(numB)) {
79-
return numA - numB
80-
} else if (partsA[i] !== partsB[i]) {
81-
return partsA[i] < partsB[i] ? -1 : 1
82-
}
83-
}
84-
85-
return 0
86-
}
87-
88-
export function lt(a: string, b: string): boolean {
89-
const [coreA, preA] = a.split('-', 2)
90-
const [coreB, preB] = b.split('-', 2)
91-
const partsA = coreA.split('.').map(Number)
92-
const partsB = coreB.split('.').map(Number)
93-
for (let i = 0; i < 3; i++) {
94-
const diff = (partsA[i] || 0) - (partsB[i] || 0)
95-
if (diff !== 0)
96-
return diff < 0
97-
}
98-
if (preA && !preB)
99-
return true
100-
if (!preA || !preB)
101-
return false
102-
return comparePrereleasePrecedence(preA, preB) < 0
103-
}

tests/utils/version.test.ts

Lines changed: 1 addition & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from 'vitest'
2-
import { getPrereleaseId, lt, parseVersion } from '../../src/utils/version'
2+
import { parseVersion } from '../../src/utils/version'
33

44
describe('parseVersion', () => {
55
it('should parse plain version', () => {
@@ -72,58 +72,3 @@ describe('parseVersion', () => {
7272
expect(parseVersion('git+https://github.com/user/repo')).toBeNull()
7373
})
7474
})
75-
76-
describe('getPrereleaseId', () => {
77-
it('should return null for stable versions', () => {
78-
expect(getPrereleaseId('1.0.0')).toBeNull()
79-
})
80-
81-
it('should extract identifier', () => {
82-
expect(getPrereleaseId('2.0.0-beta.1')).toBe('beta')
83-
})
84-
85-
it('should handle prerelease without dots', () => {
86-
expect(getPrereleaseId('1.0.0-canary')).toBe('canary')
87-
})
88-
})
89-
90-
describe('lt', () => {
91-
it('should compare major versions', () => {
92-
expect(lt('1.0.0', '2.0.0')).toBe(true)
93-
expect(lt('2.0.0', '1.0.0')).toBe(false)
94-
})
95-
96-
it('should compare minor versions', () => {
97-
expect(lt('1.0.0', '1.1.0')).toBe(true)
98-
expect(lt('1.1.0', '1.0.0')).toBe(false)
99-
})
100-
101-
it('should compare patch versions', () => {
102-
expect(lt('1.0.0', '1.0.1')).toBe(true)
103-
expect(lt('1.0.1', '1.0.0')).toBe(false)
104-
})
105-
106-
it('should return false for equal versions', () => {
107-
expect(lt('1.0.0', '1.0.0')).toBe(false)
108-
})
109-
110-
it('should treat prerelease as less than release', () => {
111-
expect(lt('1.0.0-beta.1', '1.0.0')).toBe(true)
112-
expect(lt('1.0.0', '1.0.0-beta.1')).toBe(false)
113-
})
114-
115-
it('should compare prerelease versions numerically', () => {
116-
expect(lt('1.0.0-beta.1', '1.0.0-beta.2')).toBe(true)
117-
expect(lt('1.0.0-beta.2', '1.0.0-beta.1')).toBe(false)
118-
})
119-
120-
it('should compare different prerelease identifiers', () => {
121-
expect(lt('1.0.0-alpha.1', '1.0.0-beta.1')).toBe(true)
122-
expect(lt('1.0.0-beta.1', '1.0.0-alpha.1')).toBe(false)
123-
})
124-
125-
it('should handle prerelease with fewer segments', () => {
126-
expect(lt('1.0.0-beta', '1.0.0-beta.1')).toBe(true)
127-
expect(lt('1.0.0-beta.1', '1.0.0-beta')).toBe(false)
128-
})
129-
})

tsdown.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default defineConfig({
1717
'ofetch',
1818
'perfect-debounce',
1919
'reactive-vscode',
20+
'semver',
2021
'yaml',
2122
],
2223
minify: 'dce-only',

0 commit comments

Comments
 (0)