From 8cd086991b90232a81596134e2b483e457577bac Mon Sep 17 00:00:00 2001 From: Kevin Deng Date: Mon, 20 Jul 2026 14:37:44 +0900 Subject: [PATCH] refactor: replace semver with verkit --- .../node-modules-inspector/build.config.ts | 1 - packages/node-modules-inspector/package.json | 5 +- .../components/display/VersionWithUpdates.vue | 6 +-- .../src/app/utils/prompts.ts | 4 +- .../src/app/utils/semver.test.ts | 37 +++++++++++++ .../src/app/utils/semver.ts | 6 +-- .../node-modules-inspector/src/nuxt.config.ts | 2 +- .../src/shared/reports/maintainers.test.ts | 54 +++++++++++++++++++ .../src/shared/reports/maintainers.ts | 10 ++-- .../src/shared/semver.test.ts | 17 ++++++ .../src/shared/semver.ts | 4 +- .../src/shared/vulnerable-info.ts | 2 +- packages/node-modules-tools/package.json | 4 +- .../src/utils/filter.test.ts | 2 + .../node-modules-tools/src/utils/filter.ts | 2 +- pnpm-lock.yaml | 32 ++++++----- pnpm-workspace.yaml | 2 +- 17 files changed, 148 insertions(+), 42 deletions(-) create mode 100644 packages/node-modules-inspector/src/app/utils/semver.test.ts create mode 100644 packages/node-modules-inspector/src/shared/semver.test.ts diff --git a/packages/node-modules-inspector/build.config.ts b/packages/node-modules-inspector/build.config.ts index 8d43f51d..11f2590b 100644 --- a/packages/node-modules-inspector/build.config.ts +++ b/packages/node-modules-inspector/build.config.ts @@ -23,7 +23,6 @@ export default defineBuildConfig({ rollup: { inlineDependencies: [ '@antfu/utils', - 'semver', ], }, }) diff --git a/packages/node-modules-inspector/package.json b/packages/node-modules-inspector/package.json index 01e3bdd8..516182ea 100644 --- a/packages/node-modules-inspector/package.json +++ b/packages/node-modules-inspector/package.json @@ -57,10 +57,10 @@ "tinyglobby": "catalog:deps", "unconfig": "catalog:deps", "unstorage": "catalog:deps", - "valibot": "catalog:deps" + "valibot": "catalog:deps", + "verkit": "catalog:deps" }, "devDependencies": { - "@types/semver": "catalog:types", "@unocss/nuxt": "catalog:bundling", "@valibot/to-json-schema": "catalog:testing", "@vueuse/nuxt": "catalog:bundling", @@ -76,7 +76,6 @@ "modern-screenshot": "catalog:frontend", "nanovis": "catalog:frontend", "rollup": "catalog:bundling", - "semver": "catalog:deps", "theme-vitesse": "catalog:frontend", "vite-hot-client": "catalog:frontend" } diff --git a/packages/node-modules-inspector/src/app/components/display/VersionWithUpdates.vue b/packages/node-modules-inspector/src/app/components/display/VersionWithUpdates.vue index a1f9b997..39fe2fe6 100644 --- a/packages/node-modules-inspector/src/app/components/display/VersionWithUpdates.vue +++ b/packages/node-modules-inspector/src/app/components/display/VersionWithUpdates.vue @@ -1,7 +1,7 @@ diff --git a/packages/node-modules-inspector/src/app/utils/prompts.ts b/packages/node-modules-inspector/src/app/utils/prompts.ts index e34f8c75..a6736719 100644 --- a/packages/node-modules-inspector/src/app/utils/prompts.ts +++ b/packages/node-modules-inspector/src/app/utils/prompts.ts @@ -1,14 +1,14 @@ import type { PackageNode } from 'node-modules-tools' import type { DepUpgradeAction, MaintainerActionItem, PublintAction } from '../state/maintainer-actions' import { formatMessage } from 'publint/utils' -import semver from 'semver' +import { getMajor } from 'verkit' import { parseSemverRange } from './semver' function safeMajor(version: string | undefined): number | undefined { if (!version) return undefined try { - return semver.major(version) + return getMajor(version) } catch { return undefined diff --git a/packages/node-modules-inspector/src/app/utils/semver.test.ts b/packages/node-modules-inspector/src/app/utils/semver.test.ts new file mode 100644 index 00000000..b0fd8fb3 --- /dev/null +++ b/packages/node-modules-inspector/src/app/utils/semver.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest' +import { compareSemverRange, parseSemverRange } from './semver' + +describe('parseSemverRange', () => { + it('parses unions and partial versions', () => { + expect(parseSemverRange('^20.19 || ^22.12 || >=24')).toEqual({ + valid: true, + raw: '^20.19 || ^22.12 || >=24', + highest: '24.0.0', + lowest: '20.19.0', + parts: ['^20.19', '^22.12', '>=24'], + bare: ['20.19.0', '22.12.0', '24.0.0'], + }) + }) + + it('preserves prerelease versions', () => { + expect(parseSemverRange('^1.0.0-beta.1')).toMatchObject({ + valid: true, + highest: '1.0.0-beta.1', + lowest: '1.0.0-beta.1', + }) + }) + + it('rejects invalid ranges', () => { + expect(parseSemverRange('not a range')).toEqual({ + valid: false, + raw: 'not a range', + }) + }) +}) + +describe('compareSemverRange', () => { + it('orders ranges by their lowest version', () => { + expect(compareSemverRange('^1.0.0', '^2.0.0')).toBe(1) + expect(compareSemverRange('^2.0.0', '^1.0.0')).toBe(-1) + }) +}) diff --git a/packages/node-modules-inspector/src/app/utils/semver.ts b/packages/node-modules-inspector/src/app/utils/semver.ts index 3f8c39b5..fbb6055d 100644 --- a/packages/node-modules-inspector/src/app/utils/semver.ts +++ b/packages/node-modules-inspector/src/app/utils/semver.ts @@ -1,4 +1,4 @@ -import semver from 'semver' +import { isValid, isValidRange } from 'verkit' import { compareSemver } from '../../shared/semver' export { compareSemver } @@ -24,7 +24,7 @@ export function parseSemverRange(range: string) { } SemverParseCache.set(range, result) - if (!semver.validRange(range)) { + if (!isValidRange(range)) { return result } @@ -46,7 +46,7 @@ export function parseSemverRange(range: string) { const highest = partsBare.at(-1)! const lowest = partsBare.at(0)! - if (!semver.valid(highest) || !semver.valid(lowest)) { + if (!isValid(highest) || !isValid(lowest)) { return result } diff --git a/packages/node-modules-inspector/src/nuxt.config.ts b/packages/node-modules-inspector/src/nuxt.config.ts index 9dbd322b..b03d26c5 100644 --- a/packages/node-modules-inspector/src/nuxt.config.ts +++ b/packages/node-modules-inspector/src/nuxt.config.ts @@ -142,7 +142,7 @@ export default defineNuxtConfig({ 'modern-screenshot', 'floating-vue', '@antfu/utils', - 'semver', + 'verkit', 'devframe/client', 'publint/utils', ], diff --git a/packages/node-modules-inspector/src/shared/reports/maintainers.test.ts b/packages/node-modules-inspector/src/shared/reports/maintainers.test.ts index 1ebbe404..13fae08a 100644 --- a/packages/node-modules-inspector/src/shared/reports/maintainers.test.ts +++ b/packages/node-modules-inspector/src/shared/reports/maintainers.test.ts @@ -81,6 +81,37 @@ describe('computeMaintainerActions', () => { expect(items).toEqual([]) }) + it('ignores prerelease versions when finding the highest installed version', () => { + const stable = pkg({ name: 'dep', version: '1.5.0' }) + const prerelease = pkg({ name: 'dep', version: '2.0.0-beta.1' }) + const consumer = pkg({ + name: 'consumer', + version: '1.0.0', + dependencies: { dep: '^1.0.0' }, + }) + const all = [consumer, stable, prerelease] + const items = computeMaintainerActions({ + packages: all, + versions: buildVersions(all), + }) + expect(items).toEqual([]) + }) + + it('ignores invalid semver ranges', () => { + const dep = pkg({ name: 'dep', version: '2.0.0' }) + const consumer = pkg({ + name: 'consumer', + version: '1.0.0', + dependencies: { dep: 'not a range' }, + }) + const all = [consumer, dep] + const items = computeMaintainerActions({ + packages: all, + versions: buildVersions(all), + }) + expect(items).toEqual([]) + }) + it('resolves catalog: ranges', () => { const dep1 = pkg({ name: 'dep', version: '1.0.0' }) const dep2 = pkg({ name: 'dep', version: '2.0.0' }) @@ -167,4 +198,27 @@ describe('groupMaintainerActions', () => { expect(groups).toHaveLength(1) expect(groups[0]!.consumer.version).toBe('2.0.0') }) + + it('filters consumers to the latest major version', () => { + const dep = pkg({ name: 'dep', version: '2.0.0' }) + const consumer = pkg({ + name: 'consumer', + version: '2.1.0', + dependencies: { dep: '^1.0.0' }, + }) + const all = [consumer, dep] + const items = computeMaintainerActions({ + packages: all, + versions: buildVersions(all), + }) + + expect(groupMaintainerActions(items, { + latestOnly: true, + latestVersionOf: () => '2.5.0', + })).toHaveLength(1) + expect(groupMaintainerActions(items, { + latestOnly: true, + latestVersionOf: () => '3.0.0', + })).toEqual([]) + }) }) diff --git a/packages/node-modules-inspector/src/shared/reports/maintainers.ts b/packages/node-modules-inspector/src/shared/reports/maintainers.ts index 27f666e0..8c13d2ee 100644 --- a/packages/node-modules-inspector/src/shared/reports/maintainers.ts +++ b/packages/node-modules-inspector/src/shared/reports/maintainers.ts @@ -1,6 +1,6 @@ import type { PackageNode, PublintMessage } from 'node-modules-tools' import type { ParsedAuthor } from 'node-modules-tools/utils' -import semver from 'semver' +import { getMajor, getPrerelease, isGreaterThanRange, satisfies } from 'verkit' import { compareSemver } from '../semver' export function authorKey(author: ParsedAuthor): string { @@ -86,7 +86,7 @@ function isPlainSemverRange(range: string | undefined): range is string { function safeSatisfies(version: string, range: string) { try { - return semver.satisfies(version, range) + return satisfies(version, range) } catch { return null @@ -95,7 +95,7 @@ function safeSatisfies(version: string, range: string) { function safeGtr(version: string, range: string) { try { - return semver.gtr(version, range) + return isGreaterThanRange(version, range) } catch { return null @@ -103,7 +103,7 @@ function safeGtr(version: string, range: string) { } function isStable(version: string) { - return semver.prerelease(version) === null + return getPrerelease(version) === null } function getPublintMessagesFor( @@ -337,7 +337,7 @@ export function groupMaintainerActions( if (!latest) return true try { - return semver.major(g.consumer.version) === semver.major(latest) + return getMajor(g.consumer.version) === getMajor(latest) } catch { return true diff --git a/packages/node-modules-inspector/src/shared/semver.test.ts b/packages/node-modules-inspector/src/shared/semver.test.ts new file mode 100644 index 00000000..8bad877b --- /dev/null +++ b/packages/node-modules-inspector/src/shared/semver.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it, vi } from 'vitest' +import { compareSemver } from './semver' + +describe('compareSemver', () => { + it('compares stable and prerelease versions', () => { + expect(compareSemver('1.0.0', '2.0.0')).toBe(-1) + expect(compareSemver('1.0.0', '1.0.0-beta.1')).toBe(1) + expect(compareSemver('1.0.0-beta.2', '1.0.0-beta.1')).toBe(1) + }) + + it('falls back for invalid versions', () => { + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + expect(compareSemver('invalid', '1.0.0')).toBe(0) + expect(error).toHaveBeenCalledOnce() + error.mockRestore() + }) +}) diff --git a/packages/node-modules-inspector/src/shared/semver.ts b/packages/node-modules-inspector/src/shared/semver.ts index 48a79fcb..6a11ca75 100644 --- a/packages/node-modules-inspector/src/shared/semver.ts +++ b/packages/node-modules-inspector/src/shared/semver.ts @@ -1,10 +1,10 @@ -import semver from 'semver' +import { compare } from 'verkit' export function compareSemver(a: string, b: string): number { if (a === b) return 0 try { - return semver.compare(a, b) + return compare(a, b) } catch (e) { console.error('Failed to compare semver ', e) diff --git a/packages/node-modules-inspector/src/shared/vulnerable-info.ts b/packages/node-modules-inspector/src/shared/vulnerable-info.ts index 6dd23706..aaefaac9 100644 --- a/packages/node-modules-inspector/src/shared/vulnerable-info.ts +++ b/packages/node-modules-inspector/src/shared/vulnerable-info.ts @@ -1,7 +1,7 @@ import type { AuditLevelString, NpmMeta } from 'node-modules-tools' import type { ListPackagesNpmMetaOptions } from './types' import pLimit from 'p-limit' -import { satisfies } from 'semver' +import { satisfies } from 'verkit' interface AuditReport { id: string diff --git a/packages/node-modules-tools/package.json b/packages/node-modules-tools/package.json index 2ec0c1e7..93a5a4af 100644 --- a/packages/node-modules-tools/package.json +++ b/packages/node-modules-tools/package.json @@ -40,8 +40,8 @@ "pathe": "catalog:deps", "pkg-types": "catalog:types", "publint": "catalog:deps", - "semver": "catalog:deps", - "tinyexec": "catalog:deps" + "tinyexec": "catalog:deps", + "verkit": "catalog:deps" }, "devDependencies": { "@pnpm/list": "catalog:types", diff --git a/packages/node-modules-tools/src/utils/filter.test.ts b/packages/node-modules-tools/src/utils/filter.test.ts index b937078b..f0d30db1 100644 --- a/packages/node-modules-tools/src/utils/filter.test.ts +++ b/packages/node-modules-tools/src/utils/filter.test.ts @@ -24,7 +24,9 @@ describe('constructPackageFilter', () => { const filter = constructPackageFilter('foo@^1.0.0') expect(filter({ name: 'foo', version: '1.0.0' })).toBe(true) expect(filter({ name: 'foo', version: '1.1.0' })).toBe(true) + expect(filter({ name: 'foo', version: '1.1.0-beta.1' })).toBe(false) expect(filter({ name: 'foo', version: '2.0.0' })).toBe(false) + expect(filter({ name: 'foo', version: 'invalid' })).toBe(false) }) it('prefix', () => { diff --git a/packages/node-modules-tools/src/utils/filter.ts b/packages/node-modules-tools/src/utils/filter.ts index 39d14a9e..3be1b55a 100644 --- a/packages/node-modules-tools/src/utils/filter.ts +++ b/packages/node-modules-tools/src/utils/filter.ts @@ -1,4 +1,4 @@ -import { satisfies } from 'semver' +import { satisfies } from 'verkit' export interface PackageNodeLike { name: string diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb7ff3eb..d1dd175c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,6 +106,9 @@ catalogs: valibot: specifier: ^1.4.1 version: 1.4.1 + verkit: + specifier: ^0.1.2 + version: 0.1.2 dev: '@antfu/ni': specifier: ^30.1.0 @@ -248,9 +251,6 @@ catalogs: '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 - '@types/semver': - specifier: ^7.7.1 - version: 7.7.1 '@types/stream-json': specifier: ^1.7.8 version: 1.7.8 @@ -465,10 +465,10 @@ importers: valibot: specifier: catalog:deps version: 1.4.1(typescript@6.0.3) + verkit: + specifier: catalog:deps + version: 0.1.2 devDependencies: - '@types/semver': - specifier: catalog:types - version: 7.7.1 '@unocss/nuxt': specifier: catalog:bundling version: 66.7.0(magicast@0.5.3)(vite@8.0.14(@types/node@22.13.9)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.39.0)(yaml@2.9.0))(webpack@5.98.0(esbuild@0.28.0)) @@ -514,9 +514,6 @@ importers: rollup: specifier: catalog:bundling version: 4.60.4 - semver: - specifier: 7.8.0 - version: 7.8.0 theme-vitesse: specifier: catalog:frontend version: 1.0.0 @@ -544,12 +541,12 @@ importers: publint: specifier: catalog:deps version: 0.3.21 - semver: - specifier: 7.8.0 - version: 7.8.0 tinyexec: specifier: catalog:deps version: 1.2.3 + verkit: + specifier: catalog:deps + version: 0.1.2 devDependencies: '@pnpm/list': specifier: catalog:types @@ -3159,9 +3156,6 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - '@types/ssri@7.1.5': resolution: {integrity: sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw==} @@ -7929,6 +7923,10 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + verkit@0.1.2: + resolution: {integrity: sha512-WqkT8n3hqizuCu71W3bUzf5fjBmkbXcudsehe/NbxA8PgqoKnSOY5K0Ba2ckg1qaRaSpSz7as/n9K1R9JXjQKg==} + engines: {node: '>=18.12.0'} + vite-dev-rpc@2.0.0: resolution: {integrity: sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==} peerDependencies: @@ -10981,8 +10979,6 @@ snapshots: '@types/resolve@1.20.2': {} - '@types/semver@7.7.1': {} - '@types/ssri@7.1.5': dependencies: '@types/node': 22.13.9 @@ -17337,6 +17333,8 @@ snapshots: vary@1.1.2: {} + verkit@0.1.2: {} + vite-dev-rpc@2.0.0(vite@8.0.14(@types/node@22.13.9)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.39.0)(yaml@2.9.0)): dependencies: birpc: 4.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 79c68044..0ecfe6c9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -58,6 +58,7 @@ catalogs: unconfig: ^7.5.0 unstorage: ^1.17.5 valibot: ^1.4.1 + verkit: ^0.1.2 dev: '@antfu/ni': ^30.1.0 '@nuxt/devtools': ^4.0.0-alpha.6 @@ -112,7 +113,6 @@ catalogs: '@types/d3': ^7.4.3 '@types/d3-hierarchy': ^3.1.7 '@types/js-yaml': ^4.0.9 - '@types/semver': ^7.7.1 '@types/stream-json': ^1.7.8 pkg-types: ^2.3.1 allowBuilds: