diff --git a/AGENTS.md b/AGENTS.md index 19209674..f3fb049b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,6 @@ Monorepo (pnpm workspaces) for VS Code extension around [npmx.dev](https://npmx. ## Code Style - ESM, strict TypeScript — never use `any` or type-cast with `as`; validate rather than assert - Imports: type imports first, then `#` aliases (`#state`, `#utils/`, `#core/`), then external packages, then relative — no blank lines between groups -- No `node:` built-in imports in `src/` (browser-compat constraint); use `semver` subpath imports (not bare `semver`) +- No `node:` built-in imports in `src/` (browser-compat constraint) - Naming: files/folders `kebab-case`, tests `*.test.ts`, functions `camelCase`, constants `SCREAMING_SNAKE_CASE`, types `PascalCase` - Commits: [Conventional Commits](https://www.conventionalcommits.org/) — `type(scope): description` (lowercase subject) diff --git a/package.json b/package.json index a8f60ef5..e6d1ee9b 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "devDependencies": { "@types/node": "catalog:dev", "@types/path-browserify": "catalog:dev", - "@types/semver": "catalog:dev", "@vida0905/eslint-config": "catalog:dev", "eslint": "catalog:dev", "husky": "catalog:dev", diff --git a/packages/language-core/package.json b/packages/language-core/package.json index ee1e193e..e5f0fdf6 100644 --- a/packages/language-core/package.json +++ b/packages/language-core/package.json @@ -30,7 +30,7 @@ }, "dependencies": { "ocache": "catalog:inline", - "semver": "catalog:inline" + "verkit": "catalog:inline" }, "devDependencies": { "fast-npm-meta": "catalog:inline", diff --git a/packages/language-core/src/utils/package.test.ts b/packages/language-core/src/utils/package.test.ts index 8f2c9880..2ac7dae5 100644 --- a/packages/language-core/src/utils/package.test.ts +++ b/packages/language-core/src/utils/package.test.ts @@ -64,6 +64,7 @@ describe('resolveExactVersion', () => { ['latest', '4.10.0'], ['next', '4.11.0-beta.1'], ['beta', null], + ['not a range', null], ])('should resolve $0 to $1', (spec, version) => { const pkg = { distTags: { diff --git a/packages/language-core/src/utils/package.ts b/packages/language-core/src/utils/package.ts index af97487f..51c73de9 100644 --- a/packages/language-core/src/utils/package.ts +++ b/packages/language-core/src/utils/package.ts @@ -1,8 +1,5 @@ import type { PackageInfo } from '../api/package' -import Range from 'semver/classes/range' -import gt from 'semver/functions/gt' -import lte from 'semver/functions/lte' -import satisfies from 'semver/functions/satisfies' +import { isGreater, isLessOrEqual, parseRange, satisfies } from 'verkit' export function formatPackageId(name: string, version: string): string { return `${name}@${version}` @@ -67,7 +64,7 @@ function getMaxSatisfying(versions: string[], current: string, tags: PackageInfo let version: string | null = null try { - const range = new Range(current) + const range = parseRange(current) let maxVersion: string | null = tags.latest if (!satisfies(maxVersion, range)) @@ -77,8 +74,8 @@ function getMaxSatisfying(versions: string[], current: string, tags: PackageInfo if (!satisfies(ver, range)) continue - if (!maxVersion || lte(ver, maxVersion)) { - if (!version || gt(ver, version)) { + if (!maxVersion || isLessOrEqual(ver, maxVersion)) { + if (!version || isGreater(ver, version)) { version = ver } } diff --git a/packages/language-server/package.json b/packages/language-server/package.json index db7eaa53..95b1699a 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -45,7 +45,7 @@ "package-manager-detector": "1.7.0", "path-browserify": "1.0.1", "request-light": "0.7.0", - "semver": "7.8.5", + "verkit": "0.3.2", "vscode-jsonrpc": "9.0.1", "vscode-languageserver": "9.0.1", "vscode-languageserver-protocol": "3.17.5", diff --git a/packages/language-server/tsdown.config.ts b/packages/language-server/tsdown.config.ts index d96f0813..6c04bb7a 100644 --- a/packages/language-server/tsdown.config.ts +++ b/packages/language-server/tsdown.config.ts @@ -21,7 +21,7 @@ export default defineConfig({ 'package-manager-detector', 'request-light', 'path-browserify', - 'semver', + 'verkit', 'ohash', 'ocache', ], diff --git a/packages/language-service/package.json b/packages/language-service/package.json index 0bfc66c9..31f5b227 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -25,7 +25,7 @@ "@volar/language-service": "catalog:lsp", "npmx-language-core": "workspace:*", "npmx-shared": "workspace:*", - "semver": "catalog:inline", + "verkit": "catalog:inline", "vscode-languageserver-textdocument": "catalog:inline", "vscode-languageserver-types": "catalog:lsp", "vscode-uri": "catalog:lsp" diff --git a/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.test.ts b/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.test.ts index 58532619..1e39e35b 100644 --- a/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.test.ts +++ b/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.test.ts @@ -55,5 +55,10 @@ describe('resolveEngineMismatches', () => { { node: '>=18' }, { node: 'lts' }, )).toEqual([]) + + expect(resolveEngineMismatches( + { node: 'lts' }, + { node: '>=18' }, + )).toEqual([]) }) }) diff --git a/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.ts b/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.ts index 2fee6c1a..f3bdc7bf 100644 --- a/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.ts +++ b/packages/language-service/src/plugins/diagnostics/rules/engine-mismatch.ts @@ -3,9 +3,7 @@ import type { Engines } from 'npmx-language-core/types' import type { DiagnosticRule } from '../types' import { npmxPackageUrl } from 'npmx-language-core/links' import { formatPackageId, isPackageManifest } from 'npmx-language-core/utils' -import SemverRange from 'semver/classes/range' -import intersects from 'semver/ranges/intersects' -import subset from 'semver/ranges/subset' +import { isRangeSubset, parseRange, rangesIntersect } from 'verkit' import { URI } from 'vscode-uri' interface EngineMismatch { @@ -27,17 +25,17 @@ export function resolveEngineMismatches( continue try { - const pkgRange = new SemverRange(packageRangeStr) - const depRange = new SemverRange(dependencyRangeStr) + const pkgRange = parseRange(packageRangeStr) + const depRange = parseRange(dependencyRangeStr) - if (subset(pkgRange, depRange)) + if (isRangeSubset(pkgRange, depRange)) continue mismatches.push({ engine, packageRange: packageRangeStr, dependencyRange: dependencyRangeStr, - hasIntersection: intersects(pkgRange, depRange), + hasIntersection: rangesIntersect(pkgRange, depRange), }) } catch { continue diff --git a/packages/language-service/src/plugins/diagnostics/rules/upgrade.test.ts b/packages/language-service/src/plugins/diagnostics/rules/upgrade.test.ts index 8c64e7f6..d0d6bd42 100644 --- a/packages/language-service/src/plugins/diagnostics/rules/upgrade.test.ts +++ b/packages/language-service/src/plugins/diagnostics/rules/upgrade.test.ts @@ -43,6 +43,16 @@ describe('resolveUpgrade', () => { expect(resolveUpgrade(...await createOptions('3.0.0-alpha.1'), [])).toBe('3.0.0-alpha.5') }) + it('should ignore build metadata when comparing versions', async () => { + const [dep, pkg] = await createOptions('^2.7.0') + expect(resolveUpgrade(dep, pkg, '2.7.0+build.1', [])).toBeUndefined() + }) + + it('should throw for an invalid resolved version', async () => { + const [dep, pkg] = await createOptions('^1.0.0') + expect(() => resolveUpgrade(dep, pkg, 'not a version', [])).toThrow(TypeError) + }) + it('should not flag when target upgrade version is ignored', async () => { expect(resolveUpgrade(...await createOptions('^1.0.0'), ['vite@^2.7.0'])).toBeUndefined() }) diff --git a/packages/language-service/src/plugins/diagnostics/rules/upgrade.ts b/packages/language-service/src/plugins/diagnostics/rules/upgrade.ts index 062c1fdc..9022e55d 100644 --- a/packages/language-service/src/plugins/diagnostics/rules/upgrade.ts +++ b/packages/language-service/src/plugins/diagnostics/rules/upgrade.ts @@ -4,9 +4,7 @@ import type { DependencyInfo } from 'npmx-language-core/workspace' import type { DiagnosticRule } from '../types' import { npmxPackageUrl } from 'npmx-language-core/links' import { checkIgnored } from 'npmx-language-core/utils' -import gt from 'semver/functions/gt' -import lte from 'semver/functions/lte' -import prerelease from 'semver/functions/prerelease' +import { getPrerelease, isGreater, isLessOrEqual } from 'verkit' import { formatUpgradeVersion } from '../../../utils/version' export function resolveUpgrade(dep: DependencyInfo, pkg: PackageInfo, resolvedVersion: string, ignoreList: string[]) { @@ -17,7 +15,7 @@ export function resolveUpgrade(dep: DependencyInfo, pkg: PackageInfo, resolvedVe const { latest } = distTags const { resolvedName } = dep - if (gt(latest, resolvedVersion)) { + if (isGreater(latest, resolvedVersion)) { const targetVersion = formatUpgradeVersion(dep, latest) if (checkIgnored({ ignoreList, name: resolvedName, version: targetVersion })) return @@ -25,16 +23,16 @@ export function resolveUpgrade(dep: DependencyInfo, pkg: PackageInfo, resolvedVe return targetVersion } - const currentPreId = prerelease(resolvedVersion)?.[0] + const currentPreId = getPrerelease(resolvedVersion)?.[0] if (currentPreId == null) return for (const [tag, tagVersion] of Object.entries(distTags)) { if (tag === 'latest') continue - if (prerelease(tagVersion)?.[0] !== currentPreId) + if (getPrerelease(tagVersion)?.[0] !== currentPreId) continue - if (lte(tagVersion, resolvedVersion)) + if (isLessOrEqual(tagVersion, resolvedVersion)) continue const targetVersion = formatUpgradeVersion(dep, tagVersion) if (checkIgnored({ ignoreList, name: resolvedName, version: targetVersion })) diff --git a/packages/language-service/src/plugins/diagnostics/rules/vulnerability.test.ts b/packages/language-service/src/plugins/diagnostics/rules/vulnerability.test.ts index 1dbdf264..fed70f20 100644 --- a/packages/language-service/src/plugins/diagnostics/rules/vulnerability.test.ts +++ b/packages/language-service/src/plugins/diagnostics/rules/vulnerability.test.ts @@ -27,6 +27,16 @@ describe('checkVulnerability', () => { }) }) + it('should suggest the greatest fixed version', async () => { + expect(await checkVulnerability(createVulnerabilityContext('pkg-fix-multi'), [])).toMatchObject({ + message: expect.stringContaining('Upgrade to 2.0.0 to fix.'), + }) + }) + + it('should throw when fixed versions contain invalid semver', async () => { + await expect(checkVulnerability(createVulnerabilityContext('pkg-fix-invalid'), [])).rejects.toThrow(TypeError) + }) + it('should not flag when no vulnerabilities', async () => { expect(await checkVulnerability(createVulnerabilityContext('pkg-safe'), [])).toBeUndefined() }) diff --git a/packages/language-service/src/plugins/diagnostics/rules/vulnerability.ts b/packages/language-service/src/plugins/diagnostics/rules/vulnerability.ts index 6195409f..8f770e6f 100644 --- a/packages/language-service/src/plugins/diagnostics/rules/vulnerability.ts +++ b/packages/language-service/src/plugins/diagnostics/rules/vulnerability.ts @@ -4,7 +4,7 @@ import type { DiagnosticRule } from '../types' import { getVulnerability, SEVERITY_LEVELS } from 'npmx-language-core/api/vulnerability' import { npmxPackageUrl } from 'npmx-language-core/links' import { checkIgnored, formatPackageId } from 'npmx-language-core/utils' -import lt from 'semver/functions/lt' +import { isLess } from 'verkit' import { formatUpgradeVersion } from '../../../utils/version' const DIAGNOSTIC_SEVERITY_MAPPING: Record, DiagnosticSeverity> = { @@ -20,7 +20,7 @@ function getBiggestFixedInVersion(vulnerablePackages: PackageVulnerabilityInfo[] if (depth !== 'root') continue for (const { fixedIn } of vulnerabilities) { - if (fixedIn && (!biggest || lt(biggest, fixedIn))) + if (fixedIn && (!biggest || isLess(biggest, fixedIn))) biggest = fixedIn } } diff --git a/packages/language-service/tests/__setup__/msw.ts b/packages/language-service/tests/__setup__/msw.ts index 16806f15..d7c13006 100644 --- a/packages/language-service/tests/__setup__/msw.ts +++ b/packages/language-service/tests/__setup__/msw.ts @@ -39,6 +39,44 @@ const vulnerabilityResults: Record> = { failedQueries: 0, totalCounts: { total: 1, critical: 0, high: 1, moderate: 0, low: 0 }, }, + 'pkg-fix-multi@1.0.0': { + package: 'pkg-fix-multi', + version: '1.0.0', + vulnerablePackages: [{ + name: 'pkg-fix-multi', + version: '1.0.0', + depth: 'root', + path: [], + vulnerabilities: [ + { id: 'GHSA-1', summary: '', severity: 'high', aliases: [], url: '', fixedIn: '2.0.0' }, + { id: 'GHSA-2', summary: '', severity: 'high', aliases: [], url: '', fixedIn: '1.5.0' }, + ], + counts: { total: 2, critical: 0, high: 2, moderate: 0, low: 0 }, + }], + deprecatedPackages: [], + totalPackages: 1, + failedQueries: 0, + totalCounts: { total: 2, critical: 0, high: 2, moderate: 0, low: 0 }, + }, + 'pkg-fix-invalid@1.0.0': { + package: 'pkg-fix-invalid', + version: '1.0.0', + vulnerablePackages: [{ + name: 'pkg-fix-invalid', + version: '1.0.0', + depth: 'root', + path: [], + vulnerabilities: [ + { id: 'GHSA-1', summary: '', severity: 'high', aliases: [], url: '', fixedIn: 'not a version' }, + { id: 'GHSA-2', summary: '', severity: 'high', aliases: [], url: '', fixedIn: '1.2.0' }, + ], + counts: { total: 2, critical: 0, high: 2, moderate: 0, low: 0 }, + }], + deprecatedPackages: [], + totalPackages: 1, + failedQueries: 0, + totalCounts: { total: 2, critical: 0, high: 2, moderate: 0, low: 0 }, + }, 'pkg-safe@1.0.0': { package: 'pkg-safe', version: '1.0.0', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4c4dd82..4e635d16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,9 +12,6 @@ catalogs: '@types/path-browserify': specifier: ^1.0.3 version: 1.0.3 - '@types/semver': - specifier: ^7.7.1 - version: 7.7.1 '@vida0905/eslint-config': specifier: ^2.13.0 version: 2.13.0 @@ -67,9 +64,9 @@ catalogs: reactive-vscode: specifier: ^1.0.2 version: 1.0.2 - semver: - specifier: ^7.8.5 - version: 7.8.5 + verkit: + specifier: ^0.3.2 + version: 0.3.2 vscode-find-up: specifier: ^0.1.1 version: 0.1.1 @@ -121,9 +118,6 @@ importers: '@types/path-browserify': specifier: catalog:dev version: 1.0.3 - '@types/semver': - specifier: catalog:dev - version: 7.7.1 '@vida0905/eslint-config': specifier: catalog:dev version: 2.13.0(@typescript-eslint/typescript-estree@8.64.0(@typescript/typescript6@6.0.2))(@typescript-eslint/utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0))(@typescript/typescript6@6.0.2)(@vue/compiler-sfc@3.5.40)(eslint@10.7.0)(vitest@4.1.10(@types/node@26.1.1)(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.5(@types/node@26.1.1)(yaml@2.9.0))) @@ -196,9 +190,9 @@ importers: ocache: specifier: catalog:inline version: 0.2.0 - semver: + verkit: specifier: catalog:inline - version: 7.8.5 + version: 0.3.2 devDependencies: fast-npm-meta: specifier: catalog:inline @@ -254,9 +248,9 @@ importers: npmx-shared: specifier: workspace:* version: link:../shared - semver: + verkit: specifier: catalog:inline - version: 7.8.5 + version: 0.3.2 vscode-languageserver-textdocument: specifier: catalog:inline version: 1.0.12 @@ -796,9 +790,6 @@ packages: '@types/path-browserify@1.0.3': resolution: {integrity: sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw==} - '@types/semver@7.7.1': - resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - '@types/set-cookie-parser@2.4.10': resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} @@ -2515,6 +2506,10 @@ packages: resolution: {integrity: sha512-WqkT8n3hqizuCu71W3bUzf5fjBmkbXcudsehe/NbxA8PgqoKnSOY5K0Ba2ckg1qaRaSpSz7as/n9K1R9JXjQKg==} engines: {node: '>=18.12.0'} + verkit@0.3.2: + resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==} + engines: {node: '>=18.12.0'} + vite-tsconfig-paths@6.1.1: resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} peerDependencies: @@ -3141,8 +3136,6 @@ snapshots: '@types/path-browserify@1.0.3': {} - '@types/semver@7.7.1': {} - '@types/set-cookie-parser@2.4.10': dependencies: '@types/node': 26.1.1 @@ -4964,6 +4957,8 @@ snapshots: verkit@0.1.2: {} + verkit@0.3.2: {} + vite-tsconfig-paths@6.1.1(@typescript/typescript6@6.0.2)(vite@8.1.5(@types/node@26.1.1)(yaml@2.9.0)): dependencies: debug: 4.4.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ad9f2bb3..940bd480 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -13,7 +13,6 @@ catalogs: dev: '@types/node': ^26.1.1 '@types/path-browserify': ^1.0.3 - '@types/semver': ^7.7.1 '@vida0905/eslint-config': ^2.13.0 eslint: ^10.7.0 husky: ^9.1.7 @@ -32,7 +31,7 @@ catalogs: package-manager-detector: ^1.7.0 path-browserify: ^1.0.1 reactive-vscode: ^1.0.2 - semver: ^7.8.5 + verkit: ^0.3.2 vscode-find-up: ^0.1.1 vscode-languageserver-textdocument: ^1.0.12 yaml: ^2.9.0