From 4273492632341fcabc6b736726a901efdfe61c92 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Wed, 25 Feb 2026 17:32:07 +0800 Subject: [PATCH 01/21] feat: handle version ranges correctly --- src/providers/completion-item/version.ts | 12 ++--- .../diagnostics/rules/deprecation.ts | 15 ++++-- src/providers/diagnostics/rules/upgrade.ts | 7 ++- .../diagnostics/rules/vulnerability.ts | 14 ++--- src/utils/version.ts | 54 ++++++++++++++----- tests/utils/version.test.ts | 35 ++---------- 6 files changed, 70 insertions(+), 67 deletions(-) diff --git a/src/providers/completion-item/version.ts b/src/providers/completion-item/version.ts index c10a3a7..f597064 100644 --- a/src/providers/completion-item/version.ts +++ b/src/providers/completion-item/version.ts @@ -3,7 +3,7 @@ import type { CompletionItemProvider, Position, TextDocument } from 'vscode' import { PRERELEASE_PATTERN } from '#constants' import { config } from '#state' import { getPackageInfo } from '#utils/api/package' -import { formatVersion, isSupportedProtocol, parseVersion } from '#utils/version' +import { getUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' import { CompletionItem, CompletionItemKind } from 'vscode' export class VersionCompletionItemProvider implements CompletionItemProvider { @@ -39,25 +39,25 @@ export class VersionCompletionItemProvider implements Compl const items: CompletionItem[] = [] - for (const semver in pkg.versionsMeta) { - const meta = pkg.versionsMeta[semver] + for (const version in pkg.versionsMeta) { + const meta = pkg.versionsMeta[version] if (meta.deprecated != null) continue - if (config.completion.excludePrerelease && PRERELEASE_PATTERN.test(semver)) + if (config.completion.excludePrerelease && PRERELEASE_PATTERN.test(version)) continue if (config.completion.version === 'provenance-only' && !meta.provenance) continue - const text = formatVersion({ ...parsed, semver }) + const text = getUpgradeVersion(parsed, version) const item = new CompletionItem(text, CompletionItemKind.Value) item.range = this.extractor.getNodeRange(document, versionNode) item.insertText = text - const tag = pkg.versionToTag.get(semver) + const tag = pkg.versionToTag.get(version) if (tag) item.detail = tag diff --git a/src/providers/diagnostics/rules/deprecation.ts b/src/providers/diagnostics/rules/deprecation.ts index 4c7d1a3..5077db0 100644 --- a/src/providers/diagnostics/rules/deprecation.ts +++ b/src/providers/diagnostics/rules/deprecation.ts @@ -1,6 +1,7 @@ import type { DiagnosticRule } from '..' import { npmxPackageUrl } from '#utils/links' import { isSupportedProtocol, parseVersion } from '#utils/version' +import maxSatisfying from 'semver/ranges/max-satisfying' import { DiagnosticSeverity, DiagnosticTag, Uri } from 'vscode' export const checkDeprecation: DiagnosticRule = (dep, pkg) => { @@ -8,19 +9,23 @@ export const checkDeprecation: DiagnosticRule = (dep, pkg) => { if (!parsed || !isSupportedProtocol(parsed.protocol)) return - const { semver } = parsed - const versionInfo = pkg.versionsMeta[semver] + const version = maxSatisfying(Object.keys(pkg.versionsMeta), parsed.semver) - if (!versionInfo?.deprecated) + if (!version) + return + + const versionInfo = pkg.versionsMeta[version] + + if (!versionInfo.deprecated) return return { node: dep.versionNode, - message: `${dep.name} v${semver} has been deprecated: ${versionInfo.deprecated}`, + message: `${dep.name} ${version} has been deprecated: ${versionInfo.deprecated}`, severity: DiagnosticSeverity.Error, code: { value: 'deprecation', - target: Uri.parse(npmxPackageUrl(dep.name, semver)), + target: Uri.parse(npmxPackageUrl(dep.name, version)), }, tags: [DiagnosticTag.Deprecated], } diff --git a/src/providers/diagnostics/rules/upgrade.ts b/src/providers/diagnostics/rules/upgrade.ts index 5560fdf..d8d4330 100644 --- a/src/providers/diagnostics/rules/upgrade.ts +++ b/src/providers/diagnostics/rules/upgrade.ts @@ -1,18 +1,17 @@ import type { DependencyInfo } from '#types/extractor' import type { ParsedVersion } from '#utils/version' import type { DiagnosticRule, NodeDiagnosticInfo } from '..' -import { formatVersion, isSupportedProtocol, parseVersion } from '#utils/version' +import { getUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' import prerelease from 'semver/functions/prerelease' import gtr from 'semver/ranges/gtr' import ltr from 'semver/ranges/ltr' import { DiagnosticSeverity } from 'vscode' -function createUpgradeDiagnostic(dep: DependencyInfo, parsed: ParsedVersion, upgradeVersion: string): NodeDiagnosticInfo { - const target = formatVersion({ ...parsed, semver: upgradeVersion }) +function createUpgradeDiagnostic(dep: DependencyInfo, parsed: ParsedVersion, target: string): NodeDiagnosticInfo { return { node: dep.versionNode, severity: DiagnosticSeverity.Hint, - message: `New version available: ${target}`, + message: `New version available: ${getUpgradeVersion(parsed, target)}`, code: 'upgrade', } } diff --git a/src/providers/diagnostics/rules/vulnerability.ts b/src/providers/diagnostics/rules/vulnerability.ts index c48a669..6f2ffc1 100644 --- a/src/providers/diagnostics/rules/vulnerability.ts +++ b/src/providers/diagnostics/rules/vulnerability.ts @@ -2,8 +2,9 @@ import type { OsvSeverityLevel, PackageVulnerabilityInfo } from '#utils/api/vuln import type { DiagnosticRule } from '..' import { getVulnerability, SEVERITY_LEVELS } from '#utils/api/vulnerability' import { npmxPackageUrl } from '#utils/links' -import { formatVersion, isSupportedProtocol, parseVersion } from '#utils/version' +import { getUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' import lt from 'semver/functions/lt' +import maxSatisfying from 'semver/ranges/max-satisfying' import { DiagnosticSeverity, Uri } from 'vscode' const DIAGNOSTIC_MAPPING: Record, DiagnosticSeverity> = { @@ -31,12 +32,11 @@ export const checkVulnerability: DiagnosticRule = async (dep, pkg) => { if (!parsed || !isSupportedProtocol(parsed.protocol)) return - const { semver } = parsed - const versionInfo = pkg.versionsMeta[semver] - if (!versionInfo) + const version = maxSatisfying(Object.keys(pkg.versionsMeta), parsed.semver) + if (!version) return - const result = await getVulnerability({ name: dep.name, version: semver }) + const result = await getVulnerability({ name: dep.name, version }) if (!result) return @@ -61,7 +61,7 @@ export const checkVulnerability: DiagnosticRule = async (dep, pkg) => { const fixedInVersion = getBigestFixedInVersion(vulnerablePackages) const messageSuffix = fixedInVersion - ? ` Upgrade to ${formatVersion({ ...parsed, semver: fixedInVersion })} to fix.` + ? ` Upgrade to ${getUpgradeVersion(parsed, fixedInVersion)} to fix.` : '' return { @@ -70,7 +70,7 @@ export const checkVulnerability: DiagnosticRule = async (dep, pkg) => { severity: severity ?? DiagnosticSeverity.Error, code: { value: 'vulnerability', - target: Uri.parse(npmxPackageUrl(dep.name, semver)), + target: Uri.parse(npmxPackageUrl(dep.name, version)), }, } } diff --git a/src/utils/version.ts b/src/utils/version.ts index 5cd6688..b4c6c77 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -6,7 +6,6 @@ const KNOWN_PROTOCOLS = new Set([...UNSUPPORTED_PROTOCOLS, 'npm']) export interface ParsedVersion { protocol: VersionProtocol - prefix: '' | '^' | '~' semver: string } @@ -14,11 +13,6 @@ export function isSupportedProtocol(protocol: VersionProtocol): boolean { return !protocol || !UNSUPPORTED_PROTOCOLS.has(protocol) } -export function formatVersion(parsed: ParsedVersion): string { - const protocol = parsed.protocol ? `${parsed.protocol}:` : '' - return `${protocol}${parsed.prefix}${parsed.semver}` -} - function isKnownProtocol(protocol: string): protocol is NonNullable { return KNOWN_PROTOCOLS.has(protocol) } @@ -29,7 +23,7 @@ export function parseVersion(rawVersion: string): ParsedVersion | null { return null let protocol: string | null = null - let versionStr = rawVersion + let semver = rawVersion const colonIndex = rawVersion.indexOf(':') if (colonIndex !== -1) { @@ -38,13 +32,47 @@ export function parseVersion(rawVersion: string): ParsedVersion | null { if (!isKnownProtocol(protocol)) return null - versionStr = rawVersion.slice(colonIndex + 1) + semver = rawVersion.slice(colonIndex + 1) } - const firstChar = versionStr[0] - const hasPrefix = firstChar === '^' || firstChar === '~' - const prefix = hasPrefix ? firstChar : '' - const semver = hasPrefix ? versionStr.slice(1) : versionStr + return { protocol, semver } +} + +const RANGE_PREFIXES = ['>=', '<=', '=', '>', '<', '~', '^'] + +function getVersionRangePrefix(v: string) { + const ver = v.trim().toLowerCase() + + if (ver === '*' || ver === '') + return '*' + if (ver[0] === '~' || ver[0] === '^') + return ver[0] + for (const leading of RANGE_PREFIXES) { + if (ver.startsWith(leading)) + return leading + } + if (ver.includes('x')) { + const parts = ver.split('.') + if (parts[0] === 'x') + return '*' + if (parts[1] === 'x') + return '^' + if (parts[2] === 'x') + return '~' + } + + return '' +} + +export function getUpgradeVersion(current: ParsedVersion, target: string) { + const prefix = getVersionRangePrefix(current.semver) + + if (prefix === '*') + return '*' + + const result = `${prefix}${target}` + if (!current.protocol) + return result - return { protocol, prefix, semver } + return `${current.protocol}:${result}` } diff --git a/tests/utils/version.test.ts b/tests/utils/version.test.ts index d945ac1..c81ff18 100644 --- a/tests/utils/version.test.ts +++ b/tests/utils/version.test.ts @@ -5,47 +5,20 @@ describe('parseVersion', () => { it('should parse plain version', () => { expect(parseVersion('1.0.0')).toEqual({ protocol: null, - prefix: '', semver: '1.0.0', }) }) - it('should parse version with ^ prefix', () => { - expect(parseVersion('^1.2.3')).toEqual({ - protocol: null, - prefix: '^', - semver: '1.2.3', - }) - }) - - it('should parse version with ~ prefix', () => { - expect(parseVersion('~2.0.0')).toEqual({ - protocol: null, - prefix: '~', - semver: '2.0.0', - }) - }) - it('should parse npm: protocol', () => { - expect(parseVersion('npm:1.0.0')).toEqual({ + expect(parseVersion('npm:~1.0.0')).toEqual({ protocol: 'npm', - prefix: '', - semver: '1.0.0', - }) - }) - - it('should parse npm: protocol with prefix', () => { - expect(parseVersion('npm:^1.0.0')).toEqual({ - protocol: 'npm', - prefix: '^', - semver: '1.0.0', + semver: '~1.0.0', }) }) it('should parse workspace: protocol', () => { expect(parseVersion('workspace:*')).toEqual({ protocol: 'workspace', - prefix: '', semver: '*', }) }) @@ -53,7 +26,6 @@ describe('parseVersion', () => { it('should parse catalog: protocol', () => { expect(parseVersion('catalog:default')).toEqual({ protocol: 'catalog', - prefix: '', semver: 'default', }) }) @@ -61,8 +33,7 @@ describe('parseVersion', () => { it('should parse jsr: protocol', () => { expect(parseVersion('jsr:^1.1.4')).toEqual({ protocol: 'jsr', - prefix: '^', - semver: '1.1.4', + semver: '^1.1.4', }) }) From d8915198843c55e434dfec4f3d611480655144cd Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Thu, 26 Feb 2026 17:51:29 +0800 Subject: [PATCH 02/21] chore: update --- src/providers/completion-item/version.ts | 4 +- .../diagnostics/rules/deprecation.ts | 2 +- src/providers/diagnostics/rules/upgrade.ts | 4 +- .../diagnostics/rules/vulnerability.ts | 4 +- src/utils/version.ts | 6 +-- tests/utils/version.test.ts | 48 ++++++++++++++++++- 6 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/providers/completion-item/version.ts b/src/providers/completion-item/version.ts index f597064..31b8f78 100644 --- a/src/providers/completion-item/version.ts +++ b/src/providers/completion-item/version.ts @@ -3,7 +3,7 @@ import type { CompletionItemProvider, Position, TextDocument } from 'vscode' import { PRERELEASE_PATTERN } from '#constants' import { config } from '#state' import { getPackageInfo } from '#utils/api/package' -import { getUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' +import { formatUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' import { CompletionItem, CompletionItemKind } from 'vscode' export class VersionCompletionItemProvider implements CompletionItemProvider { @@ -51,7 +51,7 @@ export class VersionCompletionItemProvider implements Compl if (config.completion.version === 'provenance-only' && !meta.provenance) continue - const text = getUpgradeVersion(parsed, version) + const text = formatUpgradeVersion(parsed, version) const item = new CompletionItem(text, CompletionItemKind.Value) item.range = this.extractor.getNodeRange(document, versionNode) diff --git a/src/providers/diagnostics/rules/deprecation.ts b/src/providers/diagnostics/rules/deprecation.ts index 5077db0..7457173 100644 --- a/src/providers/diagnostics/rules/deprecation.ts +++ b/src/providers/diagnostics/rules/deprecation.ts @@ -21,7 +21,7 @@ export const checkDeprecation: DiagnosticRule = (dep, pkg) => { return { node: dep.versionNode, - message: `${dep.name} ${version} has been deprecated: ${versionInfo.deprecated}`, + message: `${dep.name} v${version} has been deprecated: ${versionInfo.deprecated}`, severity: DiagnosticSeverity.Error, code: { value: 'deprecation', diff --git a/src/providers/diagnostics/rules/upgrade.ts b/src/providers/diagnostics/rules/upgrade.ts index d8d4330..eebdf8f 100644 --- a/src/providers/diagnostics/rules/upgrade.ts +++ b/src/providers/diagnostics/rules/upgrade.ts @@ -1,7 +1,7 @@ import type { DependencyInfo } from '#types/extractor' import type { ParsedVersion } from '#utils/version' import type { DiagnosticRule, NodeDiagnosticInfo } from '..' -import { getUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' +import { formatUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' import prerelease from 'semver/functions/prerelease' import gtr from 'semver/ranges/gtr' import ltr from 'semver/ranges/ltr' @@ -11,7 +11,7 @@ function createUpgradeDiagnostic(dep: DependencyInfo, parsed: ParsedVersion, tar return { node: dep.versionNode, severity: DiagnosticSeverity.Hint, - message: `New version available: ${getUpgradeVersion(parsed, target)}`, + message: `New version available: ${formatUpgradeVersion(parsed, target)}`, code: 'upgrade', } } diff --git a/src/providers/diagnostics/rules/vulnerability.ts b/src/providers/diagnostics/rules/vulnerability.ts index 6f2ffc1..b73b737 100644 --- a/src/providers/diagnostics/rules/vulnerability.ts +++ b/src/providers/diagnostics/rules/vulnerability.ts @@ -2,7 +2,7 @@ import type { OsvSeverityLevel, PackageVulnerabilityInfo } from '#utils/api/vuln import type { DiagnosticRule } from '..' import { getVulnerability, SEVERITY_LEVELS } from '#utils/api/vulnerability' import { npmxPackageUrl } from '#utils/links' -import { getUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' +import { formatUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' import lt from 'semver/functions/lt' import maxSatisfying from 'semver/ranges/max-satisfying' import { DiagnosticSeverity, Uri } from 'vscode' @@ -61,7 +61,7 @@ export const checkVulnerability: DiagnosticRule = async (dep, pkg) => { const fixedInVersion = getBigestFixedInVersion(vulnerablePackages) const messageSuffix = fixedInVersion - ? ` Upgrade to ${getUpgradeVersion(parsed, fixedInVersion)} to fix.` + ? ` Upgrade to ${formatUpgradeVersion(parsed, fixedInVersion)} to fix.` : '' return { diff --git a/src/utils/version.ts b/src/utils/version.ts index b4c6c77..77928fc 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -38,9 +38,9 @@ export function parseVersion(rawVersion: string): ParsedVersion | null { return { protocol, semver } } -const RANGE_PREFIXES = ['>=', '<=', '=', '>', '<', '~', '^'] +const RANGE_PREFIXES = ['>=', '<=', '=', '>', '<'] -function getVersionRangePrefix(v: string) { +function getVersionRangePrefix(v: string): string { const ver = v.trim().toLowerCase() if (ver === '*' || ver === '') @@ -64,7 +64,7 @@ function getVersionRangePrefix(v: string) { return '' } -export function getUpgradeVersion(current: ParsedVersion, target: string) { +export function formatUpgradeVersion(current: ParsedVersion, target: string): string { const prefix = getVersionRangePrefix(current.semver) if (prefix === '*') diff --git a/tests/utils/version.test.ts b/tests/utils/version.test.ts index c81ff18..1a152e6 100644 --- a/tests/utils/version.test.ts +++ b/tests/utils/version.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { parseVersion } from '../../src/utils/version' +import { formatUpgradeVersion, parseVersion } from '../../src/utils/version' describe('parseVersion', () => { it('should parse plain version', () => { @@ -43,3 +43,49 @@ describe('parseVersion', () => { expect(parseVersion('git+https://github.com/user/repo')).toBeNull() }) }) + +describe('formatUpgradeVersion', () => { + it('should preserve ^ prefix', () => { + expect(formatUpgradeVersion({ protocol: null, semver: '^1.0.0' }, '2.0.0')).toBe('^2.0.0') + }) + + it('should preserve ~ prefix', () => { + expect(formatUpgradeVersion({ protocol: null, semver: '~1.0.0' }, '1.1.0')).toBe('~1.1.0') + }) + + it('should handle pinned version', () => { + expect(formatUpgradeVersion({ protocol: null, semver: '1.0.0' }, '2.0.0')).toBe('2.0.0') + }) + + it('should preserve >= prefix', () => { + expect(formatUpgradeVersion({ protocol: null, semver: '>=1.0.0' }, '2.0.0')).toBe('>=2.0.0') + }) + + it('should return * for wildcard', () => { + expect(formatUpgradeVersion({ protocol: null, semver: '*' }, '2.0.0')).toBe('*') + }) + + it('should return * for empty semver', () => { + expect(formatUpgradeVersion({ protocol: null, semver: '' }, '2.0.0')).toBe('*') + }) + + it('should handle x-range major wildcard', () => { + expect(formatUpgradeVersion({ protocol: null, semver: 'x' }, '2.0.0')).toBe('*') + }) + + it('should handle x-range minor wildcard as ^', () => { + expect(formatUpgradeVersion({ protocol: null, semver: '1.x' }, '2.0.0')).toBe('^2.0.0') + }) + + it('should handle x-range patch wildcard as ~', () => { + expect(formatUpgradeVersion({ protocol: null, semver: '1.0.x' }, '1.1.0')).toBe('~1.1.0') + }) + + it('should include protocol in result', () => { + expect(formatUpgradeVersion({ protocol: 'npm', semver: '^1.0.0' }, '2.0.0')).toBe('npm:^2.0.0') + }) + + it('should handle pinned version with protocol', () => { + expect(formatUpgradeVersion({ protocol: 'npm', semver: '1.0.0' }, '2.0.0')).toBe('npm:2.0.0') + }) +}) From 862f76db54e60363f07b3edddb1b5ac0f9c2aa1f Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Thu, 26 Feb 2026 20:37:47 +0800 Subject: [PATCH 03/21] refactor: use better name --- .../diagnostics/rules/deprecation.ts | 10 ++++----- src/providers/diagnostics/rules/dist-tag.ts | 2 +- src/providers/diagnostics/rules/upgrade.ts | 8 +++---- .../diagnostics/rules/vulnerability.ts | 8 +++---- src/providers/hover/npmx.ts | 10 ++++----- src/utils/version.ts | 10 ++++----- tests/utils/version.test.ts | 22 +++++++++---------- 7 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/providers/diagnostics/rules/deprecation.ts b/src/providers/diagnostics/rules/deprecation.ts index 7457173..aabd4c5 100644 --- a/src/providers/diagnostics/rules/deprecation.ts +++ b/src/providers/diagnostics/rules/deprecation.ts @@ -9,23 +9,23 @@ export const checkDeprecation: DiagnosticRule = (dep, pkg) => { if (!parsed || !isSupportedProtocol(parsed.protocol)) return - const version = maxSatisfying(Object.keys(pkg.versionsMeta), parsed.semver) + const maxSatisfyingVersion = maxSatisfying(Object.keys(pkg.versionsMeta), parsed.version) - if (!version) + if (!maxSatisfyingVersion) return - const versionInfo = pkg.versionsMeta[version] + const versionInfo = pkg.versionsMeta[maxSatisfyingVersion] if (!versionInfo.deprecated) return return { node: dep.versionNode, - message: `${dep.name} v${version} has been deprecated: ${versionInfo.deprecated}`, + message: `${dep.name} v${maxSatisfyingVersion} has been deprecated: ${versionInfo.deprecated}`, severity: DiagnosticSeverity.Error, code: { value: 'deprecation', - target: Uri.parse(npmxPackageUrl(dep.name, version)), + target: Uri.parse(npmxPackageUrl(dep.name, maxSatisfyingVersion)), }, tags: [DiagnosticTag.Deprecated], } diff --git a/src/providers/diagnostics/rules/dist-tag.ts b/src/providers/diagnostics/rules/dist-tag.ts index e374066..c8885f1 100644 --- a/src/providers/diagnostics/rules/dist-tag.ts +++ b/src/providers/diagnostics/rules/dist-tag.ts @@ -8,7 +8,7 @@ export const checkDistTag: DiagnosticRule = (dep, pkg) => { if (!parsed || !isSupportedProtocol(parsed.protocol)) return - const tag = parsed.semver + const tag = parsed.version if (!(tag in pkg.distTags)) return diff --git a/src/providers/diagnostics/rules/upgrade.ts b/src/providers/diagnostics/rules/upgrade.ts index eebdf8f..95d059b 100644 --- a/src/providers/diagnostics/rules/upgrade.ts +++ b/src/providers/diagnostics/rules/upgrade.ts @@ -21,13 +21,13 @@ export const checkUpgrade: DiagnosticRule = (dep, pkg) => { if (!parsed || !isSupportedProtocol(parsed.protocol)) return - const { semver } = parsed + const { version } = parsed const latest = pkg.distTags.latest - if (latest && gtr(latest, semver)) + if (latest && gtr(latest, version)) return createUpgradeDiagnostic(dep, parsed, latest) - const currentPreId = prerelease(semver)?.[0] + const currentPreId = prerelease(version)?.[0] if (currentPreId == null) return @@ -36,7 +36,7 @@ export const checkUpgrade: DiagnosticRule = (dep, pkg) => { continue if (prerelease(tagVersion)?.[0] !== currentPreId) continue - if (ltr(tagVersion, semver)) + if (ltr(tagVersion, version)) continue return createUpgradeDiagnostic(dep, parsed, tagVersion) diff --git a/src/providers/diagnostics/rules/vulnerability.ts b/src/providers/diagnostics/rules/vulnerability.ts index b73b737..9162599 100644 --- a/src/providers/diagnostics/rules/vulnerability.ts +++ b/src/providers/diagnostics/rules/vulnerability.ts @@ -32,11 +32,11 @@ export const checkVulnerability: DiagnosticRule = async (dep, pkg) => { if (!parsed || !isSupportedProtocol(parsed.protocol)) return - const version = maxSatisfying(Object.keys(pkg.versionsMeta), parsed.semver) - if (!version) + const maxSatisfyingVersion = maxSatisfying(Object.keys(pkg.versionsMeta), parsed.version) + if (!maxSatisfyingVersion) return - const result = await getVulnerability({ name: dep.name, version }) + const result = await getVulnerability({ name: dep.name, version: maxSatisfyingVersion }) if (!result) return @@ -70,7 +70,7 @@ export const checkVulnerability: DiagnosticRule = async (dep, pkg) => { severity: severity ?? DiagnosticSeverity.Error, code: { value: 'vulnerability', - target: Uri.parse(npmxPackageUrl(dep.name, version)), + target: Uri.parse(npmxPackageUrl(dep.name, maxSatisfyingVersion)), }, } } diff --git a/src/providers/hover/npmx.ts b/src/providers/hover/npmx.ts index 78f028c..3c0e50d 100644 --- a/src/providers/hover/npmx.ts +++ b/src/providers/hover/npmx.ts @@ -28,11 +28,11 @@ export class NpmxHoverProvider implements HoverProvider { return const { name } = dep - const { protocol, semver } = parsed + const { protocol, version } = parsed if (protocol === 'jsr') { const jsrMd = new MarkdownString('', true) - const jsrUrl = jsrPackageUrl(name, semver) + const jsrUrl = jsrPackageUrl(name, version) jsrMd.isTrusted = true @@ -59,14 +59,14 @@ export class NpmxHoverProvider implements HoverProvider { const md = new MarkdownString('', true) md.isTrusted = true - const currentVersion = pkg.versionsMeta[semver] + const currentVersion = pkg.versionsMeta[version] if (currentVersion) { if (currentVersion.provenance) - md.appendMarkdown(`[$(verified)${SPACER}Verified provenance](${npmxPackageUrl(name, semver)}#provenance)\n\n`) + md.appendMarkdown(`[$(verified)${SPACER}Verified provenance](${npmxPackageUrl(name, version)}#provenance)\n\n`) } const packageLink = `[$(package)${SPACER}View on npmx.dev](${npmxPackageUrl(name)})` - const docsLink = `[$(book)${SPACER}View docs on npmx.dev](${npmxDocsUrl(name, semver)})` + const docsLink = `[$(book)${SPACER}View docs on npmx.dev](${npmxDocsUrl(name, version)})` md.appendMarkdown(`${packageLink} | ${docsLink}`) diff --git a/src/utils/version.ts b/src/utils/version.ts index 77928fc..3feb127 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -6,7 +6,7 @@ const KNOWN_PROTOCOLS = new Set([...UNSUPPORTED_PROTOCOLS, 'npm']) export interface ParsedVersion { protocol: VersionProtocol - semver: string + version: string } export function isSupportedProtocol(protocol: VersionProtocol): boolean { @@ -23,7 +23,7 @@ export function parseVersion(rawVersion: string): ParsedVersion | null { return null let protocol: string | null = null - let semver = rawVersion + let version = rawVersion const colonIndex = rawVersion.indexOf(':') if (colonIndex !== -1) { @@ -32,10 +32,10 @@ export function parseVersion(rawVersion: string): ParsedVersion | null { if (!isKnownProtocol(protocol)) return null - semver = rawVersion.slice(colonIndex + 1) + version = rawVersion.slice(colonIndex + 1) } - return { protocol, semver } + return { protocol, version } } const RANGE_PREFIXES = ['>=', '<=', '=', '>', '<'] @@ -65,7 +65,7 @@ function getVersionRangePrefix(v: string): string { } export function formatUpgradeVersion(current: ParsedVersion, target: string): string { - const prefix = getVersionRangePrefix(current.semver) + const prefix = getVersionRangePrefix(current.version) if (prefix === '*') return '*' diff --git a/tests/utils/version.test.ts b/tests/utils/version.test.ts index 1a152e6..085e7aa 100644 --- a/tests/utils/version.test.ts +++ b/tests/utils/version.test.ts @@ -46,46 +46,46 @@ describe('parseVersion', () => { describe('formatUpgradeVersion', () => { it('should preserve ^ prefix', () => { - expect(formatUpgradeVersion({ protocol: null, semver: '^1.0.0' }, '2.0.0')).toBe('^2.0.0') + expect(formatUpgradeVersion({ protocol: null, version: '^1.0.0' }, '2.0.0')).toBe('^2.0.0') }) it('should preserve ~ prefix', () => { - expect(formatUpgradeVersion({ protocol: null, semver: '~1.0.0' }, '1.1.0')).toBe('~1.1.0') + expect(formatUpgradeVersion({ protocol: null, version: '~1.0.0' }, '1.1.0')).toBe('~1.1.0') }) it('should handle pinned version', () => { - expect(formatUpgradeVersion({ protocol: null, semver: '1.0.0' }, '2.0.0')).toBe('2.0.0') + expect(formatUpgradeVersion({ protocol: null, version: '1.0.0' }, '2.0.0')).toBe('2.0.0') }) it('should preserve >= prefix', () => { - expect(formatUpgradeVersion({ protocol: null, semver: '>=1.0.0' }, '2.0.0')).toBe('>=2.0.0') + expect(formatUpgradeVersion({ protocol: null, version: '>=1.0.0' }, '2.0.0')).toBe('>=2.0.0') }) it('should return * for wildcard', () => { - expect(formatUpgradeVersion({ protocol: null, semver: '*' }, '2.0.0')).toBe('*') + expect(formatUpgradeVersion({ protocol: null, version: '*' }, '2.0.0')).toBe('*') }) it('should return * for empty semver', () => { - expect(formatUpgradeVersion({ protocol: null, semver: '' }, '2.0.0')).toBe('*') + expect(formatUpgradeVersion({ protocol: null, version: '' }, '2.0.0')).toBe('*') }) it('should handle x-range major wildcard', () => { - expect(formatUpgradeVersion({ protocol: null, semver: 'x' }, '2.0.0')).toBe('*') + expect(formatUpgradeVersion({ protocol: null, version: 'x' }, '2.0.0')).toBe('*') }) it('should handle x-range minor wildcard as ^', () => { - expect(formatUpgradeVersion({ protocol: null, semver: '1.x' }, '2.0.0')).toBe('^2.0.0') + expect(formatUpgradeVersion({ protocol: null, version: '1.x' }, '2.0.0')).toBe('^2.0.0') }) it('should handle x-range patch wildcard as ~', () => { - expect(formatUpgradeVersion({ protocol: null, semver: '1.0.x' }, '1.1.0')).toBe('~1.1.0') + expect(formatUpgradeVersion({ protocol: null, version: '1.0.x' }, '1.1.0')).toBe('~1.1.0') }) it('should include protocol in result', () => { - expect(formatUpgradeVersion({ protocol: 'npm', semver: '^1.0.0' }, '2.0.0')).toBe('npm:^2.0.0') + expect(formatUpgradeVersion({ protocol: 'npm', version: '^1.0.0' }, '2.0.0')).toBe('npm:^2.0.0') }) it('should handle pinned version with protocol', () => { - expect(formatUpgradeVersion({ protocol: 'npm', semver: '1.0.0' }, '2.0.0')).toBe('npm:2.0.0') + expect(formatUpgradeVersion({ protocol: 'npm', version: '1.0.0' }, '2.0.0')).toBe('npm:2.0.0') }) }) From b9e4aa4f1283d9fef9333279e4648af1ac3860ac Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Thu, 26 Feb 2026 20:43:57 +0800 Subject: [PATCH 04/21] apply suggestions from coderabbit --- src/utils/version.ts | 5 +---- tests/utils/version.test.ts | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/utils/version.ts b/src/utils/version.ts index 3feb127..84b372a 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -67,10 +67,7 @@ function getVersionRangePrefix(v: string): string { export function formatUpgradeVersion(current: ParsedVersion, target: string): string { const prefix = getVersionRangePrefix(current.version) - if (prefix === '*') - return '*' - - const result = `${prefix}${target}` + const result = prefix === '*' ? '*' : `${prefix}${target}` if (!current.protocol) return result diff --git a/tests/utils/version.test.ts b/tests/utils/version.test.ts index 085e7aa..2e08d06 100644 --- a/tests/utils/version.test.ts +++ b/tests/utils/version.test.ts @@ -88,4 +88,8 @@ describe('formatUpgradeVersion', () => { it('should handle pinned version with protocol', () => { expect(formatUpgradeVersion({ protocol: 'npm', version: '1.0.0' }, '2.0.0')).toBe('npm:2.0.0') }) + + it('should preserve protocol for wildcard', () => { + expect(formatUpgradeVersion({ protocol: 'npm', version: '*' }, '2.0.0')).toBe('npm:*') + }) }) From f032c6a32c001f1dfa0b637cc2b70535a82fa7e9 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Thu, 26 Feb 2026 21:04:15 +0800 Subject: [PATCH 05/21] fix: catch each rule's error separately --- src/providers/diagnostics/index.ts | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/providers/diagnostics/index.ts b/src/providers/diagnostics/index.ts index 486ca2d..fa7a44a 100644 --- a/src/providers/diagnostics/index.ts +++ b/src/providers/diagnostics/index.ts @@ -87,19 +87,22 @@ export function useDiagnostics() { continue for (const rule of rules) { - const diagnostic = await rule(dep, pkg) - if (isDocumentChanged(document, targetUri, targetVersion)) - return - if (!diagnostic) - continue - - diagnostics.push({ - source: displayName, - range: extractor.getNodeRange(document, diagnostic.node), - ...diagnostic, - }) - - flush(document, targetUri, targetVersion, diagnostics) + try { + const diagnostic = await rule(dep, pkg) + if (isDocumentChanged(document, targetUri, targetVersion)) + return + if (!diagnostic) + continue + + diagnostics.push({ + source: displayName, + range: extractor.getNodeRange(document, diagnostic.node), + ...diagnostic, + }) + flush(document, targetUri, targetVersion, diagnostics) + } catch (err) { + logger.warn(`Fail to check ${dep.name} (${rule.name}): ${err}`) + } } } catch (err) { logger.warn(`Failed to check ${dep.name}: ${err}`) From 636b2833cdbeb216dd2cd8fa39f76ce842c09f1b Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Thu, 26 Feb 2026 21:06:59 +0800 Subject: [PATCH 06/21] fix: don't compare version when it's a tag --- src/providers/diagnostics/rules/upgrade.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/providers/diagnostics/rules/upgrade.ts b/src/providers/diagnostics/rules/upgrade.ts index 95d059b..791dc06 100644 --- a/src/providers/diagnostics/rules/upgrade.ts +++ b/src/providers/diagnostics/rules/upgrade.ts @@ -22,9 +22,11 @@ export const checkUpgrade: DiagnosticRule = (dep, pkg) => { return const { version } = parsed - const latest = pkg.distTags.latest + if (version in pkg.distTags) + return - if (latest && gtr(latest, version)) + const { latest } = pkg.distTags + if (gtr(latest, version)) return createUpgradeDiagnostic(dep, parsed, latest) const currentPreId = prerelease(version)?.[0] From ce97496e523aa3f16c646ed973e695c8ff59e787 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Thu, 26 Feb 2026 21:26:42 +0800 Subject: [PATCH 07/21] test: fix --- tests/utils/version.test.ts | 50 ++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/tests/utils/version.test.ts b/tests/utils/version.test.ts index 2e08d06..41c1c8a 100644 --- a/tests/utils/version.test.ts +++ b/tests/utils/version.test.ts @@ -3,38 +3,48 @@ import { formatUpgradeVersion, parseVersion } from '../../src/utils/version' describe('parseVersion', () => { it('should parse plain version', () => { - expect(parseVersion('1.0.0')).toEqual({ - protocol: null, - semver: '1.0.0', - }) + expect(parseVersion('1.0.0')).toMatchInlineSnapshot(` + { + "protocol": null, + "version": "1.0.0", + } + `) }) it('should parse npm: protocol', () => { - expect(parseVersion('npm:~1.0.0')).toEqual({ - protocol: 'npm', - semver: '~1.0.0', - }) + expect(parseVersion('npm:~1.0.0')).toMatchInlineSnapshot(` + { + "protocol": "npm", + "version": "~1.0.0", + } + `) }) it('should parse workspace: protocol', () => { - expect(parseVersion('workspace:*')).toEqual({ - protocol: 'workspace', - semver: '*', - }) + expect(parseVersion('workspace:*')).toMatchInlineSnapshot(` + { + "protocol": "workspace", + "version": "*", + } + `) }) it('should parse catalog: protocol', () => { - expect(parseVersion('catalog:default')).toEqual({ - protocol: 'catalog', - semver: 'default', - }) + expect(parseVersion('catalog:default')).toMatchInlineSnapshot(` + { + "protocol": "catalog", + "version": "default", + } + `) }) it('should parse jsr: protocol', () => { - expect(parseVersion('jsr:^1.1.4')).toEqual({ - protocol: 'jsr', - semver: '^1.1.4', - }) + expect(parseVersion('jsr:^1.1.4')).toMatchInlineSnapshot(` + { + "protocol": "jsr", + "version": "^1.1.4", + } + `) }) it('should return null for URL-based versions', () => { From 4d5968f63a5329afed5b3773ce3be8d9a8e1aac6 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Thu, 26 Feb 2026 22:13:35 +0800 Subject: [PATCH 08/21] fix: resolve version in hover provider --- src/providers/hover/npmx.ts | 14 ++++++++------ src/utils/links.ts | 6 ++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/providers/hover/npmx.ts b/src/providers/hover/npmx.ts index 3c0e50d..f57c108 100644 --- a/src/providers/hover/npmx.ts +++ b/src/providers/hover/npmx.ts @@ -4,6 +4,7 @@ import { SPACER } from '#constants' import { getPackageInfo } from '#utils/api/package' import { jsrPackageUrl, npmxDocsUrl, npmxPackageUrl } from '#utils/links' import { isSupportedProtocol, parseVersion } from '#utils/version' +import maxSatisfying from 'semver/ranges/max-satisfying' import { Hover, MarkdownString } from 'vscode' export class NpmxHoverProvider implements HoverProvider { @@ -59,13 +60,14 @@ export class NpmxHoverProvider implements HoverProvider { const md = new MarkdownString('', true) md.isTrusted = true - const currentVersion = pkg.versionsMeta[version] - if (currentVersion) { - if (currentVersion.provenance) - md.appendMarkdown(`[$(verified)${SPACER}Verified provenance](${npmxPackageUrl(name, version)}#provenance)\n\n`) - } + const resolvedVersion = version in pkg.distTags + ? pkg.distTags[version] + : maxSatisfying(Object.keys(pkg.versionsMeta), version) + + if (resolvedVersion && pkg.versionsMeta[resolvedVersion]?.provenance) + md.appendMarkdown(`[$(verified)${SPACER}Verified provenance](${npmxPackageUrl(name, version)}#provenance)\n\n`) - const packageLink = `[$(package)${SPACER}View on npmx.dev](${npmxPackageUrl(name)})` + const packageLink = `[$(package)${SPACER}View on npmx.dev](${npmxPackageUrl(name, version)})` const docsLink = `[$(book)${SPACER}View docs on npmx.dev](${npmxDocsUrl(name, version)})` md.appendMarkdown(`${packageLink} | ${docsLink}`) diff --git a/src/utils/links.ts b/src/utils/links.ts index ef4843c..d507711 100644 --- a/src/utils/links.ts +++ b/src/utils/links.ts @@ -22,6 +22,8 @@ export function npmxFileUrl(name: string, version: string, path: string, startLi return `${base}#L${startLine}` } -export function jsrPackageUrl(name: string, version: string): string { - return `https://jsr.io/${name}@${version}` +export function jsrPackageUrl(name: string, version?: string): string { + return version + ? `https://jsr.io/${name}@${version}` + : `https://jsr.io/${name}` } From 8498154010dd120f77879617d77bb76634a27ff3 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Thu, 26 Feb 2026 22:35:42 +0800 Subject: [PATCH 09/21] extract `resolveExactVersion` --- src/providers/diagnostics/rules/deprecation.ts | 12 ++++++------ src/providers/diagnostics/rules/vulnerability.ts | 10 +++++----- src/providers/hover/npmx.ts | 11 ++++------- src/utils/package.ts | 10 ++++++++++ 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/providers/diagnostics/rules/deprecation.ts b/src/providers/diagnostics/rules/deprecation.ts index aabd4c5..a84f1f2 100644 --- a/src/providers/diagnostics/rules/deprecation.ts +++ b/src/providers/diagnostics/rules/deprecation.ts @@ -1,7 +1,7 @@ import type { DiagnosticRule } from '..' import { npmxPackageUrl } from '#utils/links' +import { resolveExactVersion } from '#utils/package' import { isSupportedProtocol, parseVersion } from '#utils/version' -import maxSatisfying from 'semver/ranges/max-satisfying' import { DiagnosticSeverity, DiagnosticTag, Uri } from 'vscode' export const checkDeprecation: DiagnosticRule = (dep, pkg) => { @@ -9,23 +9,23 @@ export const checkDeprecation: DiagnosticRule = (dep, pkg) => { if (!parsed || !isSupportedProtocol(parsed.protocol)) return - const maxSatisfyingVersion = maxSatisfying(Object.keys(pkg.versionsMeta), parsed.version) + const exactVersion = resolveExactVersion(pkg, parsed.version) - if (!maxSatisfyingVersion) + if (!exactVersion) return - const versionInfo = pkg.versionsMeta[maxSatisfyingVersion] + const versionInfo = pkg.versionsMeta[exactVersion] if (!versionInfo.deprecated) return return { node: dep.versionNode, - message: `${dep.name} v${maxSatisfyingVersion} has been deprecated: ${versionInfo.deprecated}`, + message: `${dep.name} v${exactVersion} has been deprecated: ${versionInfo.deprecated}`, severity: DiagnosticSeverity.Error, code: { value: 'deprecation', - target: Uri.parse(npmxPackageUrl(dep.name, maxSatisfyingVersion)), + target: Uri.parse(npmxPackageUrl(dep.name, parsed.version)), }, tags: [DiagnosticTag.Deprecated], } diff --git a/src/providers/diagnostics/rules/vulnerability.ts b/src/providers/diagnostics/rules/vulnerability.ts index 9162599..67a0f4a 100644 --- a/src/providers/diagnostics/rules/vulnerability.ts +++ b/src/providers/diagnostics/rules/vulnerability.ts @@ -2,9 +2,9 @@ import type { OsvSeverityLevel, PackageVulnerabilityInfo } from '#utils/api/vuln import type { DiagnosticRule } from '..' import { getVulnerability, SEVERITY_LEVELS } from '#utils/api/vulnerability' import { npmxPackageUrl } from '#utils/links' +import { resolveExactVersion } from '#utils/package' import { formatUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' import lt from 'semver/functions/lt' -import maxSatisfying from 'semver/ranges/max-satisfying' import { DiagnosticSeverity, Uri } from 'vscode' const DIAGNOSTIC_MAPPING: Record, DiagnosticSeverity> = { @@ -32,11 +32,11 @@ export const checkVulnerability: DiagnosticRule = async (dep, pkg) => { if (!parsed || !isSupportedProtocol(parsed.protocol)) return - const maxSatisfyingVersion = maxSatisfying(Object.keys(pkg.versionsMeta), parsed.version) - if (!maxSatisfyingVersion) + const exactVersion = resolveExactVersion(pkg, parsed.version) + if (!exactVersion) return - const result = await getVulnerability({ name: dep.name, version: maxSatisfyingVersion }) + const result = await getVulnerability({ name: dep.name, version: exactVersion }) if (!result) return @@ -70,7 +70,7 @@ export const checkVulnerability: DiagnosticRule = async (dep, pkg) => { severity: severity ?? DiagnosticSeverity.Error, code: { value: 'vulnerability', - target: Uri.parse(npmxPackageUrl(dep.name, maxSatisfyingVersion)), + target: Uri.parse(npmxPackageUrl(dep.name, parsed.version)), }, } } diff --git a/src/providers/hover/npmx.ts b/src/providers/hover/npmx.ts index f57c108..4025b83 100644 --- a/src/providers/hover/npmx.ts +++ b/src/providers/hover/npmx.ts @@ -3,8 +3,8 @@ import type { HoverProvider, Position, TextDocument } from 'vscode' import { SPACER } from '#constants' import { getPackageInfo } from '#utils/api/package' import { jsrPackageUrl, npmxDocsUrl, npmxPackageUrl } from '#utils/links' +import { resolveExactVersion } from '#utils/package' import { isSupportedProtocol, parseVersion } from '#utils/version' -import maxSatisfying from 'semver/ranges/max-satisfying' import { Hover, MarkdownString } from 'vscode' export class NpmxHoverProvider implements HoverProvider { @@ -33,7 +33,7 @@ export class NpmxHoverProvider implements HoverProvider { if (protocol === 'jsr') { const jsrMd = new MarkdownString('', true) - const jsrUrl = jsrPackageUrl(name, version) + const jsrUrl = jsrPackageUrl(name) jsrMd.isTrusted = true @@ -60,11 +60,8 @@ export class NpmxHoverProvider implements HoverProvider { const md = new MarkdownString('', true) md.isTrusted = true - const resolvedVersion = version in pkg.distTags - ? pkg.distTags[version] - : maxSatisfying(Object.keys(pkg.versionsMeta), version) - - if (resolvedVersion && pkg.versionsMeta[resolvedVersion]?.provenance) + const exactVersion = resolveExactVersion(pkg, version) + if (exactVersion && pkg.versionsMeta[exactVersion]?.provenance) md.appendMarkdown(`[$(verified)${SPACER}Verified provenance](${npmxPackageUrl(name, version)}#provenance)\n\n`) const packageLink = `[$(package)${SPACER}View on npmx.dev](${npmxPackageUrl(name, version)})` diff --git a/src/utils/package.ts b/src/utils/package.ts index b5429bd..ec80e2b 100644 --- a/src/utils/package.ts +++ b/src/utils/package.ts @@ -1,3 +1,6 @@ +import type { PackageInfo } from './api/package' +import maxSatisfying from 'semver/ranges/max-satisfying' + /** * Encode a package name for use in npm registry URLs. * Handles scoped packages (e.g., @scope/name -> @scope%2Fname). @@ -8,3 +11,10 @@ export function encodePackageName(name: string): string { } return encodeURIComponent(name) } + +export function resolveExactVersion(pkg: PackageInfo, version: string) { + if (version in pkg.distTags) + return pkg.distTags[version] + + return maxSatisfying(Object.keys(pkg.versionsMeta), version) +} From f0836610a6c94796456582abff1f8a846adf1a09 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Thu, 26 Feb 2026 23:14:30 +0800 Subject: [PATCH 10/21] prefer `Object.hasOwn` --- src/providers/diagnostics/rules/dist-tag.ts | 2 +- src/providers/diagnostics/rules/upgrade.ts | 2 +- src/utils/package.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/providers/diagnostics/rules/dist-tag.ts b/src/providers/diagnostics/rules/dist-tag.ts index c8885f1..a0f7fe2 100644 --- a/src/providers/diagnostics/rules/dist-tag.ts +++ b/src/providers/diagnostics/rules/dist-tag.ts @@ -9,7 +9,7 @@ export const checkDistTag: DiagnosticRule = (dep, pkg) => { return const tag = parsed.version - if (!(tag in pkg.distTags)) + if (!Object.hasOwn(pkg.distTags, tag)) return return { diff --git a/src/providers/diagnostics/rules/upgrade.ts b/src/providers/diagnostics/rules/upgrade.ts index 791dc06..4fef0ef 100644 --- a/src/providers/diagnostics/rules/upgrade.ts +++ b/src/providers/diagnostics/rules/upgrade.ts @@ -22,7 +22,7 @@ export const checkUpgrade: DiagnosticRule = (dep, pkg) => { return const { version } = parsed - if (version in pkg.distTags) + if (Object.hasOwn(pkg.distTags, version)) return const { latest } = pkg.distTags diff --git a/src/utils/package.ts b/src/utils/package.ts index ec80e2b..424a8dc 100644 --- a/src/utils/package.ts +++ b/src/utils/package.ts @@ -13,7 +13,7 @@ export function encodePackageName(name: string): string { } export function resolveExactVersion(pkg: PackageInfo, version: string) { - if (version in pkg.distTags) + if (Object.hasOwn(pkg.distTags, version)) return pkg.distTags[version] return maxSatisfying(Object.keys(pkg.versionsMeta), version) From 66138eee717c16d3f4c58663327e6e6dfd4ef273 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Feb 2026 15:11:34 +0800 Subject: [PATCH 11/21] feat: check url package starts with `github` --- src/utils/version.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils/version.ts b/src/utils/version.ts index 84b372a..e7a3ef9 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -1,6 +1,10 @@ type VersionProtocol = 'workspace' | 'catalog' | 'npm' | 'jsr' | null -const URL_PREFIXES = ['http://', 'https://', 'git://', 'git+'] +const URL_PACKAGE_PATTERN = /^(?:https?:|git\+|github:)/ +function isUrlPackage(currentVersion: string) { + return URL_PACKAGE_PATTERN.test(currentVersion) +} + const UNSUPPORTED_PROTOCOLS = new Set(['workspace', 'catalog', 'jsr']) const KNOWN_PROTOCOLS = new Set([...UNSUPPORTED_PROTOCOLS, 'npm']) @@ -19,7 +23,7 @@ function isKnownProtocol(protocol: string): protocol is NonNullable rawVersion.startsWith(p))) + if (isUrlPackage(rawVersion)) return null let protocol: string | null = null From 7b3a4afc9461bcef05d9e57a7ae1d43b54a52335 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Feb 2026 15:32:33 +0800 Subject: [PATCH 12/21] compare upgrade with exactVersion --- playground/package.json | 1 + src/providers/diagnostics/rules/upgrade.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/playground/package.json b/playground/package.json index cc96b84..3cccad7 100644 --- a/playground/package.json +++ b/playground/package.json @@ -5,6 +5,7 @@ "nuxt": "npm:4.3.0" }, "devDependencies": { + "ofetch": "^2.0.0-alpha.1", "array-includes": "latest", "axios": "", "is-number": "", diff --git a/src/providers/diagnostics/rules/upgrade.ts b/src/providers/diagnostics/rules/upgrade.ts index 4fef0ef..ce68552 100644 --- a/src/providers/diagnostics/rules/upgrade.ts +++ b/src/providers/diagnostics/rules/upgrade.ts @@ -1,10 +1,11 @@ import type { DependencyInfo } from '#types/extractor' import type { ParsedVersion } from '#utils/version' import type { DiagnosticRule, NodeDiagnosticInfo } from '..' +import { resolveExactVersion } from '#utils/package' import { formatUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' +import lte from 'semver/functions/lte' import prerelease from 'semver/functions/prerelease' import gtr from 'semver/ranges/gtr' -import ltr from 'semver/ranges/ltr' import { DiagnosticSeverity } from 'vscode' function createUpgradeDiagnostic(dep: DependencyInfo, parsed: ParsedVersion, target: string): NodeDiagnosticInfo { @@ -29,7 +30,11 @@ export const checkUpgrade: DiagnosticRule = (dep, pkg) => { if (gtr(latest, version)) return createUpgradeDiagnostic(dep, parsed, latest) - const currentPreId = prerelease(version)?.[0] + const exactVersion = resolveExactVersion(pkg, version) + if (!exactVersion) + return + + const currentPreId = prerelease(exactVersion)?.[0] if (currentPreId == null) return @@ -38,7 +43,7 @@ export const checkUpgrade: DiagnosticRule = (dep, pkg) => { continue if (prerelease(tagVersion)?.[0] !== currentPreId) continue - if (ltr(tagVersion, version)) + if (lte(tagVersion, exactVersion)) continue return createUpgradeDiagnostic(dep, parsed, tagVersion) From 74ac4cefac494d00b23162b80d20c3705e4dfd0f Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Feb 2026 15:47:51 +0800 Subject: [PATCH 13/21] refactor(diagnostics): extract version resolution into DiagnosticContext --- src/providers/diagnostics/index.ts | 19 +++++++++++-- .../diagnostics/rules/deprecation.ts | 13 +++------ src/providers/diagnostics/rules/dist-tag.ts | 5 ++-- .../diagnostics/rules/replacement.ts | 2 +- src/providers/diagnostics/rules/upgrade.ts | 7 ++--- .../diagnostics/rules/vulnerability.ts | 12 +++------ tests/diagnostics/dist-tag.test.ts | 27 +++++++------------ 7 files changed, 38 insertions(+), 47 deletions(-) diff --git a/src/providers/diagnostics/index.ts b/src/providers/diagnostics/index.ts index fa7a44a..f8ed47c 100644 --- a/src/providers/diagnostics/index.ts +++ b/src/providers/diagnostics/index.ts @@ -1,10 +1,13 @@ import type { DependencyInfo, ValidNode } from '#types/extractor' import type { PackageInfo } from '#utils/api/package' +import type { ParsedVersion } from '#utils/version' import type { Awaitable } from 'reactive-vscode' import type { Diagnostic, TextDocument } from 'vscode' import { useActiveExtractor } from '#composables/active-extractor' import { config, logger } from '#state' import { getPackageInfo } from '#utils/api/package' +import { resolveExactVersion } from '#utils/package' +import { parseVersion } from '#utils/version' import { debounce } from 'perfect-debounce' import { computed, useActiveTextEditor, useDisposable, useDocumentText, watch } from 'reactive-vscode' import { languages } from 'vscode' @@ -15,10 +18,17 @@ import { checkReplacement } from './rules/replacement' import { checkUpgrade } from './rules/upgrade' import { checkVulnerability } from './rules/vulnerability' +export interface DiagnosticContext { + dep: DependencyInfo + pkg: PackageInfo + parsed: ParsedVersion | null + exactVersion: string | null +} + export interface NodeDiagnosticInfo extends Omit { node: ValidNode } -export type DiagnosticRule = (dep: DependencyInfo, pkg: PackageInfo) => Awaitable +export type DiagnosticRule = (ctx: DiagnosticContext) => Awaitable export function useDiagnostics() { const diagnosticCollection = useDisposable(languages.createDiagnosticCollection(displayName)) @@ -86,9 +96,14 @@ export function useDiagnostics() { if (!pkg) continue + const parsed = parseVersion(dep.version) + const exactVersion = parsed + ? resolveExactVersion(pkg, parsed.version) + : null + for (const rule of rules) { try { - const diagnostic = await rule(dep, pkg) + const diagnostic = await rule({ dep, pkg, parsed, exactVersion }) if (isDocumentChanged(document, targetUri, targetVersion)) return if (!diagnostic) diff --git a/src/providers/diagnostics/rules/deprecation.ts b/src/providers/diagnostics/rules/deprecation.ts index a84f1f2..2ab2933 100644 --- a/src/providers/diagnostics/rules/deprecation.ts +++ b/src/providers/diagnostics/rules/deprecation.ts @@ -1,17 +1,10 @@ import type { DiagnosticRule } from '..' import { npmxPackageUrl } from '#utils/links' -import { resolveExactVersion } from '#utils/package' -import { isSupportedProtocol, parseVersion } from '#utils/version' +import { isSupportedProtocol } from '#utils/version' import { DiagnosticSeverity, DiagnosticTag, Uri } from 'vscode' -export const checkDeprecation: DiagnosticRule = (dep, pkg) => { - const parsed = parseVersion(dep.version) - if (!parsed || !isSupportedProtocol(parsed.protocol)) - return - - const exactVersion = resolveExactVersion(pkg, parsed.version) - - if (!exactVersion) +export const checkDeprecation: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) => { + if (!parsed || !isSupportedProtocol(parsed.protocol) || !exactVersion) return const versionInfo = pkg.versionsMeta[exactVersion] diff --git a/src/providers/diagnostics/rules/dist-tag.ts b/src/providers/diagnostics/rules/dist-tag.ts index a0f7fe2..6edaa86 100644 --- a/src/providers/diagnostics/rules/dist-tag.ts +++ b/src/providers/diagnostics/rules/dist-tag.ts @@ -1,10 +1,9 @@ import type { DiagnosticRule } from '..' import { npmxPackageUrl } from '#utils/links' -import { isSupportedProtocol, parseVersion } from '#utils/version' +import { isSupportedProtocol } from '#utils/version' import { DiagnosticSeverity, Uri } from 'vscode' -export const checkDistTag: DiagnosticRule = (dep, pkg) => { - const parsed = parseVersion(dep.version) +export const checkDistTag: DiagnosticRule = ({ dep, pkg, parsed }) => { if (!parsed || !isSupportedProtocol(parsed.protocol)) return diff --git a/src/providers/diagnostics/rules/replacement.ts b/src/providers/diagnostics/rules/replacement.ts index f49d29d..be99efe 100644 --- a/src/providers/diagnostics/rules/replacement.ts +++ b/src/providers/diagnostics/rules/replacement.ts @@ -39,7 +39,7 @@ function getReplacementInfo(replacement: ModuleReplacement) { } } -export const checkReplacement: DiagnosticRule = async (dep) => { +export const checkReplacement: DiagnosticRule = async ({ dep }) => { const replacement = await getReplacement(dep.name) if (!replacement) return diff --git a/src/providers/diagnostics/rules/upgrade.ts b/src/providers/diagnostics/rules/upgrade.ts index ce68552..69ae6e9 100644 --- a/src/providers/diagnostics/rules/upgrade.ts +++ b/src/providers/diagnostics/rules/upgrade.ts @@ -1,8 +1,7 @@ import type { DependencyInfo } from '#types/extractor' import type { ParsedVersion } from '#utils/version' import type { DiagnosticRule, NodeDiagnosticInfo } from '..' -import { resolveExactVersion } from '#utils/package' -import { formatUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' +import { formatUpgradeVersion, isSupportedProtocol } from '#utils/version' import lte from 'semver/functions/lte' import prerelease from 'semver/functions/prerelease' import gtr from 'semver/ranges/gtr' @@ -17,8 +16,7 @@ function createUpgradeDiagnostic(dep: DependencyInfo, parsed: ParsedVersion, tar } } -export const checkUpgrade: DiagnosticRule = (dep, pkg) => { - const parsed = parseVersion(dep.version) +export const checkUpgrade: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) => { if (!parsed || !isSupportedProtocol(parsed.protocol)) return @@ -30,7 +28,6 @@ export const checkUpgrade: DiagnosticRule = (dep, pkg) => { if (gtr(latest, version)) return createUpgradeDiagnostic(dep, parsed, latest) - const exactVersion = resolveExactVersion(pkg, version) if (!exactVersion) return diff --git a/src/providers/diagnostics/rules/vulnerability.ts b/src/providers/diagnostics/rules/vulnerability.ts index 67a0f4a..5c35f1e 100644 --- a/src/providers/diagnostics/rules/vulnerability.ts +++ b/src/providers/diagnostics/rules/vulnerability.ts @@ -2,8 +2,7 @@ import type { OsvSeverityLevel, PackageVulnerabilityInfo } from '#utils/api/vuln import type { DiagnosticRule } from '..' import { getVulnerability, SEVERITY_LEVELS } from '#utils/api/vulnerability' import { npmxPackageUrl } from '#utils/links' -import { resolveExactVersion } from '#utils/package' -import { formatUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version' +import { formatUpgradeVersion, isSupportedProtocol } from '#utils/version' import lt from 'semver/functions/lt' import { DiagnosticSeverity, Uri } from 'vscode' @@ -27,13 +26,8 @@ function getBigestFixedInVersion(vulnerablePackages: PackageVulnerabilityInfo[]) return bigest } -export const checkVulnerability: DiagnosticRule = async (dep, pkg) => { - const parsed = parseVersion(dep.version) - if (!parsed || !isSupportedProtocol(parsed.protocol)) - return - - const exactVersion = resolveExactVersion(pkg, parsed.version) - if (!exactVersion) +export const checkVulnerability: DiagnosticRule = async ({ dep, parsed, exactVersion }) => { + if (!parsed || !isSupportedProtocol(parsed.protocol) || !exactVersion) return const result = await getVulnerability({ name: dep.name, version: exactVersion }) diff --git a/tests/diagnostics/dist-tag.test.ts b/tests/diagnostics/dist-tag.test.ts index 551df96..bc6736f 100644 --- a/tests/diagnostics/dist-tag.test.ts +++ b/tests/diagnostics/dist-tag.test.ts @@ -1,34 +1,27 @@ import type { DependencyInfo } from '#types/extractor' import type { PackageInfo } from '#utils/api/package' +import type { DiagnosticContext } from '../../src/providers/diagnostics' +import { parseVersion } from '#utils/version' import { describe, expect, it } from 'vitest' import { checkDistTag } from '../../src/providers/diagnostics/rules/dist-tag' -function createDependency(name: string, version: string): DependencyInfo { - return { - name, - version, - nameNode: {}, - versionNode: {}, - } -} - -function createPackageInfo(distTags: Record): PackageInfo { - return { distTags } as PackageInfo +function createContext(name: string, version: string, distTags: Record): DiagnosticContext { + const dep: DependencyInfo = { name, version, nameNode: {}, versionNode: {} } + const pkg = { distTags } as PackageInfo + return { dep, pkg, parsed: parseVersion(version), exactVersion: null } } describe('checkDistTag', () => { - const packageInfo = createPackageInfo({ latest: '2.0.0' }) - it('should flag when version matches a dist tag in metadata', async () => { - const dependency = createDependency('lodash', 'latest') - const result = await checkDistTag(dependency, packageInfo) + const ctx = createContext('lodash', 'latest', { latest: '2.0.0' }) + const result = await checkDistTag(ctx) expect(result).toBeDefined() }) it('should not flag when version does not match any dist tag in metadata', async () => { - const dependency = createDependency('lodash', 'next') - const result = await checkDistTag(dependency, packageInfo) + const ctx = createContext('lodash', 'next', { latest: '2.0.0' }) + const result = await checkDistTag(ctx) expect(result).toBeUndefined() }) From df365cd198bc0b0bab288549f19fc63db9dff1d2 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Feb 2026 17:00:03 +0800 Subject: [PATCH 14/21] refactor: extract isSupportedProtocol --- src/providers/diagnostics/index.ts | 4 ++-- src/providers/diagnostics/rules/deprecation.ts | 3 +-- src/providers/diagnostics/rules/dist-tag.ts | 5 ++--- src/providers/diagnostics/rules/upgrade.ts | 11 ++++------- src/providers/diagnostics/rules/vulnerability.ts | 4 ++-- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/providers/diagnostics/index.ts b/src/providers/diagnostics/index.ts index f8ed47c..9041c96 100644 --- a/src/providers/diagnostics/index.ts +++ b/src/providers/diagnostics/index.ts @@ -7,7 +7,7 @@ import { useActiveExtractor } from '#composables/active-extractor' import { config, logger } from '#state' import { getPackageInfo } from '#utils/api/package' import { resolveExactVersion } from '#utils/package' -import { parseVersion } from '#utils/version' +import { isSupportedProtocol, parseVersion } from '#utils/version' import { debounce } from 'perfect-debounce' import { computed, useActiveTextEditor, useDisposable, useDocumentText, watch } from 'reactive-vscode' import { languages } from 'vscode' @@ -97,7 +97,7 @@ export function useDiagnostics() { continue const parsed = parseVersion(dep.version) - const exactVersion = parsed + const exactVersion = parsed && isSupportedProtocol(parsed.protocol) ? resolveExactVersion(pkg, parsed.version) : null diff --git a/src/providers/diagnostics/rules/deprecation.ts b/src/providers/diagnostics/rules/deprecation.ts index 2ab2933..dcd0945 100644 --- a/src/providers/diagnostics/rules/deprecation.ts +++ b/src/providers/diagnostics/rules/deprecation.ts @@ -1,10 +1,9 @@ import type { DiagnosticRule } from '..' import { npmxPackageUrl } from '#utils/links' -import { isSupportedProtocol } from '#utils/version' import { DiagnosticSeverity, DiagnosticTag, Uri } from 'vscode' export const checkDeprecation: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) => { - if (!parsed || !isSupportedProtocol(parsed.protocol) || !exactVersion) + if (!parsed || !exactVersion) return const versionInfo = pkg.versionsMeta[exactVersion] diff --git a/src/providers/diagnostics/rules/dist-tag.ts b/src/providers/diagnostics/rules/dist-tag.ts index 6edaa86..7d4a399 100644 --- a/src/providers/diagnostics/rules/dist-tag.ts +++ b/src/providers/diagnostics/rules/dist-tag.ts @@ -1,10 +1,9 @@ import type { DiagnosticRule } from '..' import { npmxPackageUrl } from '#utils/links' -import { isSupportedProtocol } from '#utils/version' import { DiagnosticSeverity, Uri } from 'vscode' -export const checkDistTag: DiagnosticRule = ({ dep, pkg, parsed }) => { - if (!parsed || !isSupportedProtocol(parsed.protocol)) +export const checkDistTag: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) => { + if (!parsed || !exactVersion) return const tag = parsed.version diff --git a/src/providers/diagnostics/rules/upgrade.ts b/src/providers/diagnostics/rules/upgrade.ts index 69ae6e9..8775b21 100644 --- a/src/providers/diagnostics/rules/upgrade.ts +++ b/src/providers/diagnostics/rules/upgrade.ts @@ -1,10 +1,10 @@ import type { DependencyInfo } from '#types/extractor' import type { ParsedVersion } from '#utils/version' import type { DiagnosticRule, NodeDiagnosticInfo } from '..' -import { formatUpgradeVersion, isSupportedProtocol } from '#utils/version' +import { formatUpgradeVersion } from '#utils/version' +import gt from 'semver/functions/gt' import lte from 'semver/functions/lte' import prerelease from 'semver/functions/prerelease' -import gtr from 'semver/ranges/gtr' import { DiagnosticSeverity } from 'vscode' function createUpgradeDiagnostic(dep: DependencyInfo, parsed: ParsedVersion, target: string): NodeDiagnosticInfo { @@ -17,7 +17,7 @@ function createUpgradeDiagnostic(dep: DependencyInfo, parsed: ParsedVersion, tar } export const checkUpgrade: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) => { - if (!parsed || !isSupportedProtocol(parsed.protocol)) + if (!parsed || !exactVersion) return const { version } = parsed @@ -25,12 +25,9 @@ export const checkUpgrade: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) return const { latest } = pkg.distTags - if (gtr(latest, version)) + if (gt(latest, exactVersion)) return createUpgradeDiagnostic(dep, parsed, latest) - if (!exactVersion) - return - const currentPreId = prerelease(exactVersion)?.[0] if (currentPreId == null) return diff --git a/src/providers/diagnostics/rules/vulnerability.ts b/src/providers/diagnostics/rules/vulnerability.ts index 5c35f1e..63ca465 100644 --- a/src/providers/diagnostics/rules/vulnerability.ts +++ b/src/providers/diagnostics/rules/vulnerability.ts @@ -2,7 +2,7 @@ import type { OsvSeverityLevel, PackageVulnerabilityInfo } from '#utils/api/vuln import type { DiagnosticRule } from '..' import { getVulnerability, SEVERITY_LEVELS } from '#utils/api/vulnerability' import { npmxPackageUrl } from '#utils/links' -import { formatUpgradeVersion, isSupportedProtocol } from '#utils/version' +import { formatUpgradeVersion } from '#utils/version' import lt from 'semver/functions/lt' import { DiagnosticSeverity, Uri } from 'vscode' @@ -27,7 +27,7 @@ function getBigestFixedInVersion(vulnerablePackages: PackageVulnerabilityInfo[]) } export const checkVulnerability: DiagnosticRule = async ({ dep, parsed, exactVersion }) => { - if (!parsed || !isSupportedProtocol(parsed.protocol) || !exactVersion) + if (!parsed || !exactVersion) return const result = await getVulnerability({ name: dep.name, version: exactVersion }) From 44055fa608dcf9170210ce78fc0569dd8b871f7c Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Feb 2026 17:51:44 +0800 Subject: [PATCH 15/21] test: add all diagnostics tests --- package.json | 1 + pnpm-lock.yaml | 399 +++++++++++++++++++++++- pnpm-workspace.yaml | 1 + tests/__setup__/index.ts | 7 + tests/__setup__/msw.ts | 76 +++++ tests/diagnostics/context.ts | 23 ++ tests/diagnostics/deprecation.test.ts | 35 +++ tests/diagnostics/dist-tag.test.ts | 20 +- tests/diagnostics/replacement.test.ts | 44 +++ tests/diagnostics/upgrade.test.ts | 68 ++++ tests/diagnostics/vulnerability.test.ts | 72 +++++ vitest.config.ts | 1 + 12 files changed, 722 insertions(+), 25 deletions(-) create mode 100644 tests/__setup__/index.ts create mode 100644 tests/__setup__/msw.ts create mode 100644 tests/diagnostics/context.ts create mode 100644 tests/diagnostics/deprecation.test.ts create mode 100644 tests/diagnostics/replacement.test.ts create mode 100644 tests/diagnostics/upgrade.test.ts create mode 100644 tests/diagnostics/vulnerability.test.ts diff --git a/package.json b/package.json index bfa6781..858f28f 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,7 @@ "jest-mock-vscode": "catalog:test", "jsonc-parser": "catalog:inline", "module-replacements": "catalog:inline", + "msw": "catalog:test", "nano-staged": "catalog:dev", "ofetch": "catalog:inline", "perfect-debounce": "catalog:inline", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c48c2cb..872064a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ catalogs: jest-mock-vscode: specifier: ^4.11.0 version: 4.11.0 + msw: + specifier: ^2.12.10 + version: 2.12.10 vite-tsconfig-paths: specifier: ^6.1.1 version: 6.1.1 @@ -84,7 +87,7 @@ importers: version: 1.101.0 '@vida0905/eslint-config': specifier: catalog:dev - version: 2.10.1(@vue/compiler-sfc@3.5.14)(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)) + version: 2.10.1(@vue/compiler-sfc@3.5.14)(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2)) eslint: specifier: catalog:dev version: 10.0.1(jiti@2.6.1) @@ -103,6 +106,9 @@ importers: module-replacements: specifier: catalog:inline version: 2.11.0 + msw: + specifier: catalog:test + version: 2.12.10(@types/node@25.3.0)(typescript@5.9.3) nano-staged: specifier: catalog:dev version: 0.9.0 @@ -129,7 +135,7 @@ importers: version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)) vitest: specifier: catalog:test - version: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2) + version: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2) vscode-ext-gen: specifier: catalog:dev version: 1.6.0 @@ -515,6 +521,41 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -528,9 +569,22 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mswjs/interceptors@0.41.3': + resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==} + engines: {node: '>=18'} + '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@ota-meshi/ast-token-store@0.2.1': resolution: {integrity: sha512-+8oB1wcOSWJCR6vAm2ioSLas7SoPwp+8tZ1Tcy8DSVEHMip6jxxlGu6EsRzJLAYVCyzKQ38B5pAqSbon1l1rmA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -822,6 +876,9 @@ packages: '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -970,6 +1027,14 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + ansis@4.2.0: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} @@ -1047,6 +1112,21 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comment-parser@1.4.1: resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} engines: {node: '>= 12.0.0'} @@ -1061,6 +1141,10 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-js-compat@3.46.0: resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} @@ -1118,6 +1202,9 @@ packages: electron-to-chromium@1.5.237: resolution: {integrity: sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + empathic@2.0.0: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} @@ -1430,6 +1517,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} @@ -1461,6 +1552,13 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql@16.13.0: + resolution: {integrity: sha512-uSisMYERbaB9bkA9M4/4dnqyktaEkf1kMHNKq/7DHyxVeWqHQ2mBmVqm5u6/FVHwF3iCNalKcg82Zfl+tffWoA==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hookable@6.0.1: resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==} @@ -1500,10 +1598,17 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1710,6 +1815,20 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.12.10: + resolution: {integrity: sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + nano-staged@0.9.0: resolution: {integrity: sha512-0JfyX4i0Vp5HhC9RDtJ1kp7psz8CFuS3Gya3Z6WZv//QCwA9dPzi1S803VdR0c0P6R7sSvweZ5mSJmYQ/N+loQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1746,6 +1865,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1775,6 +1897,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1844,6 +1969,10 @@ packages: resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + reserved-identifiers@1.2.0: resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} engines: {node: '>=18'} @@ -1851,6 +1980,9 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + rettime@0.10.1: + resolution: {integrity: sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==} + rolldown-plugin-dts@0.22.1: resolution: {integrity: sha512-5E0AiM5RSQhU6cjtkDFWH6laW4IrMu0j1Mo8x04Xo1ALHmaRMs9/7zej7P3RrryVHW/DdZAp85MA7Be55p0iUw==} engines: {node: '>=20.19.0'} @@ -1900,6 +2032,10 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -1919,9 +2055,24 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-indent@4.1.1: resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} engines: {node: '>=12'} @@ -1930,6 +2081,10 @@ packages: resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==} engines: {node: ^14.18.0 || >=16.0.0} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tapable@2.2.2: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} engines: {node: '>=6'} @@ -1949,6 +2104,13 @@ packages: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} + tldts-core@7.0.23: + resolution: {integrity: sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==} + + tldts@7.0.23: + resolution: {integrity: sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==} + hasBin: true + to-valid-identifier@1.0.0: resolution: {integrity: sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==} engines: {node: '>=20'} @@ -1957,6 +2119,10 @@ packages: resolution: {integrity: sha512-A5F0cM6+mDleacLIEUkmfpkBbnHJFV1d2rprHU2MXNk7mlxHq2zGojA+SRvQD1RoMo9gqjZPWEaKG4v1BQ48lw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -2014,6 +2180,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@5.4.4: + resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} + engines: {node: '>=20'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -2050,6 +2220,9 @@ packages: synckit: optional: true + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -2168,10 +2341,22 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yaml-eslint-parser@2.0.0: resolution: {integrity: sha512-h0uDm97wvT2bokfwwTmY6kJ1hp6YDFL0nRHwNKz8s/VD1FH/vvZjAKoMUE+un0eaYBSG7/c6h+lJTP+31tjgTw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -2181,16 +2366,28 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} snapshots: - '@antfu/eslint-config@7.4.3(@vue/compiler-sfc@3.5.14)(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))': + '@antfu/eslint-config@7.4.3(@vue/compiler-sfc@3.5.14)(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2))': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 1.0.1 @@ -2199,7 +2396,7 @@ snapshots: '@stylistic/eslint-plugin': 5.9.0(eslint@10.0.1(jiti@2.6.1)) '@typescript-eslint/eslint-plugin': 8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3) - '@vitest/eslint-plugin': 1.6.9(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)) + '@vitest/eslint-plugin': 1.6.9(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2)) ansis: 4.2.0 cac: 6.7.14 eslint: 10.0.1(jiti@2.6.1) @@ -2481,6 +2678,34 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@inquirer/ansi@1.0.2': {} + + '@inquirer/confirm@5.1.21(@types/node@25.3.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@25.3.0) + '@inquirer/type': 3.0.10(@types/node@25.3.0) + optionalDependencies: + '@types/node': 25.3.0 + + '@inquirer/core@10.3.2(@types/node@25.3.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@25.3.0) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 25.3.0 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/type@3.0.10(@types/node@25.3.0)': + optionalDependencies: + '@types/node': 25.3.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2495,6 +2720,15 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mswjs/interceptors@0.41.3': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@napi-rs/wasm-runtime@1.1.1': dependencies: '@emnapi/core': 1.7.1 @@ -2502,6 +2736,15 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@ota-meshi/ast-token-store@0.2.1(@eslint/markdown@7.5.1)(eslint@10.0.1(jiti@2.6.1))': dependencies: '@eslint/markdown': 7.5.1 @@ -2685,6 +2928,8 @@ snapshots: '@types/semver@7.7.1': {} + '@types/statuses@2.0.6': {} + '@types/unist@3.0.3': {} '@types/vscode@1.101.0': {} @@ -2780,9 +3025,9 @@ snapshots: '@typescript-eslint/types': 8.56.0 eslint-visitor-keys: 5.0.1 - '@vida0905/eslint-config@2.10.1(@vue/compiler-sfc@3.5.14)(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))': + '@vida0905/eslint-config@2.10.1(@vue/compiler-sfc@3.5.14)(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2))': dependencies: - '@antfu/eslint-config': 7.4.3(@vue/compiler-sfc@3.5.14)(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)) + '@antfu/eslint-config': 7.4.3(@vue/compiler-sfc@3.5.14)(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2)) '@e18e/eslint-plugin': 0.2.0(eslint@10.0.1(jiti@2.6.1)) cac: 6.7.14 eslint: 10.0.1(jiti@2.6.1) @@ -2815,14 +3060,14 @@ snapshots: - typescript - vitest - '@vitest/eslint-plugin@1.6.9(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))': + '@vitest/eslint-plugin@1.6.9(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2))': dependencies: '@typescript-eslint/scope-manager': 8.56.0 '@typescript-eslint/utils': 8.56.0(eslint@10.0.1(jiti@2.6.1))(typescript@5.9.3) eslint: 10.0.1(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 - vitest: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2) + vitest: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -2835,12 +3080,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))': + '@vitest/mocker@4.0.18(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: + msw: 2.12.10(@types/node@25.3.0)(typescript@5.9.3) vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2) '@vitest/pretty-format@4.0.18': @@ -2910,6 +3156,12 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + ansis@4.2.0: {} are-docs-informative@0.0.2: {} @@ -2968,6 +3220,20 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + comment-parser@1.4.1: {} comment-parser@1.4.5: {} @@ -2976,6 +3242,8 @@ snapshots: confbox@0.2.2: {} + cookie@1.1.1: {} + core-js-compat@3.46.0: dependencies: browserslist: 4.26.3 @@ -3014,6 +3282,8 @@ snapshots: electron-to-chromium@1.5.237: {} + emoji-regex@8.0.0: {} + empathic@2.0.0: {} enhanced-resolve@5.18.1: @@ -3411,6 +3681,8 @@ snapshots: fsevents@2.3.3: optional: true + get-caller-file@2.0.5: {} + get-tsconfig@4.13.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -3433,6 +3705,10 @@ snapshots: graphemer@1.4.0: {} + graphql@16.13.0: {} + + headers-polyfill@4.0.3: {} + hookable@6.0.1: {} html-entities@2.6.0: {} @@ -3455,10 +3731,14 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-node-process@1.2.0: {} + isexe@2.0.0: {} jest-mock-vscode@4.11.0(@types/vscode@1.101.0): @@ -3847,6 +4127,33 @@ snapshots: ms@2.1.3: {} + msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.21(@types/node@25.3.0) + '@mswjs/interceptors': 0.41.3 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.13.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.10.1 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 5.4.4 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@2.0.0: {} + nano-staged@0.9.0: dependencies: picocolors: 1.1.1 @@ -3878,6 +4185,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + outvariant@1.4.3: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -3900,6 +4209,8 @@ snapshots: path-key@3.1.1: {} + path-to-regexp@6.3.0: {} + pathe@2.0.3: {} perfect-debounce@2.1.0: {} @@ -3965,10 +4276,14 @@ snapshots: dependencies: jsesc: 3.1.0 + require-directory@2.1.1: {} + reserved-identifiers@1.2.0: {} resolve-pkg-maps@1.0.0: {} + rettime@0.10.1: {} + rolldown-plugin-dts@0.22.1(rolldown@1.0.0-rc.3)(typescript@5.9.3): dependencies: '@babel/generator': 8.0.0-rc.1 @@ -4052,6 +4367,8 @@ snapshots: siginfo@2.0.0: {} + signal-exit@4.1.0: {} + sisteransi@1.0.5: {} source-map-js@1.2.1: {} @@ -4067,14 +4384,30 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} + strict-event-emitter@0.5.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-indent@4.1.1: {} synckit@0.11.8: dependencies: '@pkgr/core': 0.2.7 + tagged-tag@1.0.0: {} + tapable@2.2.2: {} tinybench@2.9.0: {} @@ -4088,6 +4421,12 @@ snapshots: tinyrainbow@3.0.3: {} + tldts-core@7.0.23: {} + + tldts@7.0.23: + dependencies: + tldts-core: 7.0.23 + to-valid-identifier@1.0.0: dependencies: '@sindresorhus/base62': 1.0.0 @@ -4097,6 +4436,10 @@ snapshots: dependencies: eslint-visitor-keys: 5.0.1 + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.23 + tree-kill@1.2.2: {} ts-api-utils@2.4.0(typescript@5.9.3): @@ -4146,6 +4489,10 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@5.4.4: + dependencies: + tagged-tag: 1.0.0 + typescript@5.9.3: {} ufo@1.6.1: {} @@ -4180,6 +4527,8 @@ snapshots: dependencies: rolldown: 1.0.0-rc.3 + until-async@3.0.2: {} + update-browserslist-db@1.1.3(browserslist@4.26.3): dependencies: browserslist: 4.26.3 @@ -4216,10 +4565,10 @@ snapshots: jiti: 2.6.1 yaml: 2.8.2 - vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2): + vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)) + '@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@25.3.0)(typescript@5.9.3))(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -4283,8 +4632,22 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + xml-name-validator@4.0.0: {} + y18n@5.0.8: {} + yaml-eslint-parser@2.0.0: dependencies: eslint-visitor-keys: 5.0.1 @@ -4292,6 +4655,20 @@ snapshots: yaml@2.8.2: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} + yoctocolors-cjs@2.1.3: {} + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3f7c2cc..571b67b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -23,5 +23,6 @@ catalogs: yaml: ^2.8.2 test: jest-mock-vscode: ^4.11.0 + msw: ^2.12.10 vite-tsconfig-paths: ^6.1.1 vitest: ^4.0.18 diff --git a/tests/__setup__/index.ts b/tests/__setup__/index.ts new file mode 100644 index 0000000..cecb13e --- /dev/null +++ b/tests/__setup__/index.ts @@ -0,0 +1,7 @@ +import { vi } from 'vitest' + +import './msw' + +vi.mock('#state', () => ({ + logger: { info: vi.fn(), warn: vi.fn() }, +})) diff --git a/tests/__setup__/msw.ts b/tests/__setup__/msw.ts new file mode 100644 index 0000000..f8e8fcb --- /dev/null +++ b/tests/__setup__/msw.ts @@ -0,0 +1,76 @@ +import { NPMX_DEV_API } from '#constants' +import { all } from 'module-replacements' +import { http, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' +import { afterAll, afterEach, beforeAll } from 'vitest' + +const replacementsByName = new Map( + all.moduleReplacements.map((r) => [r.moduleName, r]), +) + +const server = setupServer( + http.get(`${NPMX_DEV_API}/replacements/:name`, ({ params }) => { + const data = replacementsByName.get(params.name as string) + return data + ? HttpResponse.json(data) + : new HttpResponse(null, { status: 404 }) + }), + + http.get(`${NPMX_DEV_API}/registry/vulnerabilities/:name/v/:version`, ({ params }) => { + const key = `${params.name}@${params.version}` + const results: Record> = { + 'pkg-crit@1.0.0': { + package: 'pkg-crit', + version: '1.0.0', + vulnerablePackages: [], + deprecatedPackages: [], + totalPackages: 1, + failedQueries: 0, + totalCounts: { total: 1, critical: 1, high: 0, moderate: 0, low: 0 }, + }, + 'pkg-multi@1.0.0': { + package: 'pkg-multi', + version: '1.0.0', + vulnerablePackages: [], + deprecatedPackages: [], + totalPackages: 1, + failedQueries: 0, + totalCounts: { total: 3, critical: 1, high: 2, moderate: 0, low: 0 }, + }, + 'pkg-fix@1.0.0': { + package: 'pkg-fix', + version: '1.0.0', + vulnerablePackages: [{ + name: 'pkg-fix', + version: '1.0.0', + depth: 'root', + path: [], + vulnerabilities: [{ id: 'GHSA-1', summary: '', severity: 'high', aliases: [], url: '', fixedIn: '1.2.0' }], + counts: { total: 1, critical: 0, high: 1, moderate: 0, low: 0 }, + }], + deprecatedPackages: [], + totalPackages: 1, + failedQueries: 0, + totalCounts: { total: 1, critical: 0, high: 1, moderate: 0, low: 0 }, + }, + 'pkg-safe@1.0.0': { + package: 'pkg-safe', + version: '1.0.0', + vulnerablePackages: [], + deprecatedPackages: [], + totalPackages: 1, + failedQueries: 0, + totalCounts: { total: 0, critical: 0, high: 0, moderate: 0, low: 0 }, + }, + } + + const data = results[key] + return data + ? HttpResponse.json(data) + : new HttpResponse(null, { status: 404 }) + }), +) + +beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' })) +afterEach(() => server.resetHandlers()) +afterAll(() => server.close()) diff --git a/tests/diagnostics/context.ts b/tests/diagnostics/context.ts new file mode 100644 index 0000000..96cdf91 --- /dev/null +++ b/tests/diagnostics/context.ts @@ -0,0 +1,23 @@ +import type { DependencyInfo } from '#types/extractor' +import type { PackageInfo } from '#utils/api/package' +import type { DiagnosticContext } from '../../src/providers/diagnostics' +import { resolveExactVersion } from '#utils/package' +import { isSupportedProtocol, parseVersion } from '#utils/version' + +interface CreateContextOptions { + name: string + version: string + distTags?: Record + versionsMeta?: Record +} + +export function createContext(options: CreateContextOptions): DiagnosticContext { + const { name, version, distTags = {}, versionsMeta = {} } = options + const dep: DependencyInfo = { name, version, nameNode: {}, versionNode: {} } + const pkg = { distTags, versionsMeta, versionToTag: new Map() } as PackageInfo + const parsed = parseVersion(version) + const exactVersion = parsed && isSupportedProtocol(parsed.protocol) + ? resolveExactVersion(pkg, parsed.version) + : null + return { dep, pkg, parsed, exactVersion } +} diff --git a/tests/diagnostics/deprecation.test.ts b/tests/diagnostics/deprecation.test.ts new file mode 100644 index 0000000..9cab767 --- /dev/null +++ b/tests/diagnostics/deprecation.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' +import { DiagnosticTag } from 'vscode' +import { checkDeprecation } from '../../src/providers/diagnostics/rules/deprecation' +import { createContext } from './context' + +describe('checkDeprecation', () => { + it('should flag deprecated version', async () => { + const deprecated = 'Use undici instead' + const ctx = createContext({ + name: 'request', + version: '^1.0.0', + distTags: { latest: '1.0.0' }, + versionsMeta: { '1.0.0': { deprecated } }, + }) + const result = await checkDeprecation(ctx) + + expect(result).toBeDefined() + expect(result!.message).toContain('deprecated') + expect(result!.message).toContain(deprecated) + expect(result!.tags).toContain(DiagnosticTag.Deprecated) + expect(result!.code).toMatchObject({ value: 'deprecation' }) + }) + + it('should not flag non-deprecated version', async () => { + const ctx = createContext({ + name: 'lodash', + version: '^1.0.0', + distTags: { latest: '1.0.0' }, + versionsMeta: { '1.0.0': {} }, + }) + const result = await checkDeprecation(ctx) + + expect(result).toBeUndefined() + }) +}) diff --git a/tests/diagnostics/dist-tag.test.ts b/tests/diagnostics/dist-tag.test.ts index bc6736f..7f2136b 100644 --- a/tests/diagnostics/dist-tag.test.ts +++ b/tests/diagnostics/dist-tag.test.ts @@ -1,26 +1,18 @@ -import type { DependencyInfo } from '#types/extractor' -import type { PackageInfo } from '#utils/api/package' -import type { DiagnosticContext } from '../../src/providers/diagnostics' -import { parseVersion } from '#utils/version' import { describe, expect, it } from 'vitest' import { checkDistTag } from '../../src/providers/diagnostics/rules/dist-tag' - -function createContext(name: string, version: string, distTags: Record): DiagnosticContext { - const dep: DependencyInfo = { name, version, nameNode: {}, versionNode: {} } - const pkg = { distTags } as PackageInfo - return { dep, pkg, parsed: parseVersion(version), exactVersion: null } -} +import { createContext } from './context' describe('checkDistTag', () => { - it('should flag when version matches a dist tag in metadata', async () => { - const ctx = createContext('lodash', 'latest', { latest: '2.0.0' }) + it('should flag when version matches a dist tag', async () => { + const ctx = createContext({ name: 'lodash', version: 'latest', distTags: { latest: '2.0.0' } }) const result = await checkDistTag(ctx) expect(result).toBeDefined() + expect(result!.code).toMatchObject({ value: 'dist-tag' }) }) - it('should not flag when version does not match any dist tag in metadata', async () => { - const ctx = createContext('lodash', 'next', { latest: '2.0.0' }) + it('should not flag when version does not match any dist tag', async () => { + const ctx = createContext({ name: 'lodash', version: 'next', distTags: { latest: '2.0.0' } }) const result = await checkDistTag(ctx) expect(result).toBeUndefined() diff --git a/tests/diagnostics/replacement.test.ts b/tests/diagnostics/replacement.test.ts new file mode 100644 index 0000000..b470e3c --- /dev/null +++ b/tests/diagnostics/replacement.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { checkReplacement } from '../../src/providers/diagnostics/rules/replacement' +import { createContext } from './context' + +function createReplacementContext(name: string) { + return createContext({ + name, + version: '^1.0.0', + distTags: { latest: '1.0.0' }, + versionsMeta: { '1.0.0': {} }, + }) +} + +describe('checkReplacement', () => { + it('should flag native replacement', async () => { + const result = await checkReplacement(createReplacementContext('left-pad')) + + expect(result).toBeDefined() + expect(result!.message).toMatchInlineSnapshot('"This can be replaced with String.prototype.padStart, available since Node 8.0.0."') + }) + + it('should flag simple replacement', async () => { + const result = await checkReplacement(createReplacementContext('is-number')) + + expect(result).toBeDefined() + expect(result!.message).toMatchInlineSnapshot(` + "The community has flagged this package as redundant, with the advice: + Use typeof v === "number" || (typeof v === "string" && Number.isFinite(+v))." + `) + }) + + it('should flag documented replacement', async () => { + const result = await checkReplacement(createReplacementContext('@jsdevtools/ez-spawn')) + + expect(result).toBeDefined() + expect(result!.message).toMatchInlineSnapshot('"The community has flagged this package as having more performant alternatives."') + }) + + it('should not flag when no replacement found', async () => { + const result = await checkReplacement(createReplacementContext('vitest')) + + expect(result).toBeUndefined() + }) +}) diff --git a/tests/diagnostics/upgrade.test.ts b/tests/diagnostics/upgrade.test.ts new file mode 100644 index 0000000..aeaf6f2 --- /dev/null +++ b/tests/diagnostics/upgrade.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest' +import { checkUpgrade } from '../../src/providers/diagnostics/rules/upgrade' +import { createContext } from './context' + +function createUpgradeContext(version: string) { + return createContext({ + name: 'vite', + version, + distTags: { + latest: '2.7.0', + next: '3.0.0-alpha.5', + }, + versionsMeta: { + '1.0.0': {}, + '2.7.0': {}, + '3.0.0-alpha.1': {}, + '3.0.0-alpha.5': {}, + }, + }) +} + +describe('checkUpgrade', () => { + it('should flag when latest is greater than current version', async () => { + const ctx = createUpgradeContext('^1.0.0') + const result = await checkUpgrade(ctx) + + expect(result).toBeDefined() + expect(result!.code).toBe('upgrade') + expect(result!.message).toContain('2.7.0') + }) + + it('should not flag when already on latest', async () => { + const ctx = createUpgradeContext('^2.7.0') + const result = await checkUpgrade(ctx) + + expect(result).toBeUndefined() + }) + + it('should not flag when version is a dist tag', async () => { + const ctx = createUpgradeContext('latest') + const result = await checkUpgrade(ctx) + + expect(result).toBeUndefined() + }) + + it('should flag prerelease upgrade within same pre-id', async () => { + const ctx = createUpgradeContext('3.0.0-alpha.1') + const result = await checkUpgrade(ctx) + + expect(result).toBeDefined() + expect(result!.message).toContain('3.0.0-alpha.5') + }) + + it('should not flag prerelease when already on latest pre-id version', async () => { + const ctx = createUpgradeContext('3.0.0-alpha.5') + const result = await checkUpgrade(ctx) + + expect(result).toBeUndefined() + }) + + it('should preserve protocol prefix in message', async () => { + const ctx = createUpgradeContext('npm:^1.0.0') + const result = await checkUpgrade(ctx) + + expect(result).toBeDefined() + expect(result!.message).toContain('npm:^2.7.0') + }) +}) diff --git a/tests/diagnostics/vulnerability.test.ts b/tests/diagnostics/vulnerability.test.ts new file mode 100644 index 0000000..9ac3007 --- /dev/null +++ b/tests/diagnostics/vulnerability.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest' +import { checkVulnerability } from '../../src/providers/diagnostics/rules/vulnerability' +import { createContext } from './context' + +describe('checkVulnerability', () => { + it('should flag version with critical vulnerability', async () => { + const ctx = createContext({ + name: 'pkg-crit', + version: '^1.0.0', + distTags: { latest: '1.0.0' }, + versionsMeta: { '1.0.0': {} }, + }) + const result = await checkVulnerability(ctx) + + expect(result).toBeDefined() + expect(result!.message).toContain('1 critical') + expect(result!.message).toContain('vulnerability') + expect(result!.code).toMatchObject({ value: 'vulnerability' }) + }) + + it('should flag multiple severity levels', async () => { + const ctx = createContext({ + name: 'pkg-multi', + version: '^1.0.0', + distTags: { latest: '1.0.0' }, + versionsMeta: { '1.0.0': {} }, + }) + const result = await checkVulnerability(ctx) + + expect(result).toBeDefined() + expect(result!.message).toContain('1 critical') + expect(result!.message).toContain('2 high') + expect(result!.message).toContain('vulnerabilities') + }) + + it('should include fix suggestion when fixedIn is available', async () => { + const ctx = createContext({ + name: 'pkg-fix', + version: '^1.0.0', + distTags: { latest: '1.0.0' }, + versionsMeta: { '1.0.0': {} }, + }) + const result = await checkVulnerability(ctx) + + expect(result).toBeDefined() + expect(result!.message).toContain('Upgrade to ^1.2.0 to fix.') + }) + + it('should not flag when no vulnerabilities', async () => { + const ctx = createContext({ + name: 'pkg-safe', + version: '^1.0.0', + distTags: { latest: '1.0.0' }, + versionsMeta: { '1.0.0': {} }, + }) + const result = await checkVulnerability(ctx) + + expect(result).toBeUndefined() + }) + + it('should not flag when API returns 404', async () => { + const ctx = createContext({ + name: 'pkg-404', + version: '^1.0.0', + distTags: { latest: '1.0.0' }, + versionsMeta: { '1.0.0': {} }, + }) + const result = await checkVulnerability(ctx) + + expect(result).toBeUndefined() + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts index ba41226..391e618 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -12,5 +12,6 @@ export default defineConfig({ vscode: join(rootDir, '/tests/__mocks__/vscode.ts'), }, include: ['tests/**/*.test.ts'], + setupFiles: ['tests/__setup__/index.ts'], }, }) From e0b59e2d144f13e6bc694df5f6bb97e499107b62 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Feb 2026 22:31:24 +0800 Subject: [PATCH 16/21] test: update --- tests/diagnostics/deprecation.test.ts | 30 ++++++++++++++------------- tests/diagnostics/replacement.test.ts | 21 ++----------------- tests/diagnostics/upgrade.test.ts | 2 +- 3 files changed, 19 insertions(+), 34 deletions(-) diff --git a/tests/diagnostics/deprecation.test.ts b/tests/diagnostics/deprecation.test.ts index 9cab767..5c31afa 100644 --- a/tests/diagnostics/deprecation.test.ts +++ b/tests/diagnostics/deprecation.test.ts @@ -3,31 +3,33 @@ import { DiagnosticTag } from 'vscode' import { checkDeprecation } from '../../src/providers/diagnostics/rules/deprecation' import { createContext } from './context' +function createDeprecationContext(version: string) { + return createContext({ + name: 'lodash', + version, + distTags: { latest: '2.0.0' }, + versionsMeta: { + '1.0.0': { + deprecated: 'deprecated', + }, + '2.0.0': {}, + }, + }) +} + describe('checkDeprecation', () => { it('should flag deprecated version', async () => { - const deprecated = 'Use undici instead' - const ctx = createContext({ - name: 'request', - version: '^1.0.0', - distTags: { latest: '1.0.0' }, - versionsMeta: { '1.0.0': { deprecated } }, - }) + const ctx = createDeprecationContext('^1.0.0') const result = await checkDeprecation(ctx) expect(result).toBeDefined() expect(result!.message).toContain('deprecated') - expect(result!.message).toContain(deprecated) expect(result!.tags).toContain(DiagnosticTag.Deprecated) expect(result!.code).toMatchObject({ value: 'deprecation' }) }) it('should not flag non-deprecated version', async () => { - const ctx = createContext({ - name: 'lodash', - version: '^1.0.0', - distTags: { latest: '1.0.0' }, - versionsMeta: { '1.0.0': {} }, - }) + const ctx = createDeprecationContext('^2.0.0') const result = await checkDeprecation(ctx) expect(result).toBeUndefined() diff --git a/tests/diagnostics/replacement.test.ts b/tests/diagnostics/replacement.test.ts index b470e3c..f54638e 100644 --- a/tests/diagnostics/replacement.test.ts +++ b/tests/diagnostics/replacement.test.ts @@ -12,28 +12,11 @@ function createReplacementContext(name: string) { } describe('checkReplacement', () => { - it('should flag native replacement', async () => { + it('should flag when replacement found', async () => { const result = await checkReplacement(createReplacementContext('left-pad')) expect(result).toBeDefined() - expect(result!.message).toMatchInlineSnapshot('"This can be replaced with String.prototype.padStart, available since Node 8.0.0."') - }) - - it('should flag simple replacement', async () => { - const result = await checkReplacement(createReplacementContext('is-number')) - - expect(result).toBeDefined() - expect(result!.message).toMatchInlineSnapshot(` - "The community has flagged this package as redundant, with the advice: - Use typeof v === "number" || (typeof v === "string" && Number.isFinite(+v))." - `) - }) - - it('should flag documented replacement', async () => { - const result = await checkReplacement(createReplacementContext('@jsdevtools/ez-spawn')) - - expect(result).toBeDefined() - expect(result!.message).toMatchInlineSnapshot('"The community has flagged this package as having more performant alternatives."') + expect(result!.message).toBeDefined() }) it('should not flag when no replacement found', async () => { diff --git a/tests/diagnostics/upgrade.test.ts b/tests/diagnostics/upgrade.test.ts index aeaf6f2..527b7a7 100644 --- a/tests/diagnostics/upgrade.test.ts +++ b/tests/diagnostics/upgrade.test.ts @@ -25,7 +25,7 @@ describe('checkUpgrade', () => { const result = await checkUpgrade(ctx) expect(result).toBeDefined() - expect(result!.code).toBe('upgrade') + expect(result!.code).toMatchObject({ value: 'upgrade' }) expect(result!.message).toContain('2.7.0') }) From 99ea373a109ee88f394fffa97d78657e73c515d6 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Feb 2026 22:33:09 +0800 Subject: [PATCH 17/21] feat(upgrade): add diagnostics target --- src/providers/diagnostics/rules/upgrade.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/providers/diagnostics/rules/upgrade.ts b/src/providers/diagnostics/rules/upgrade.ts index 8775b21..ca6933a 100644 --- a/src/providers/diagnostics/rules/upgrade.ts +++ b/src/providers/diagnostics/rules/upgrade.ts @@ -1,18 +1,22 @@ import type { DependencyInfo } from '#types/extractor' import type { ParsedVersion } from '#utils/version' import type { DiagnosticRule, NodeDiagnosticInfo } from '..' +import { npmxPackageUrl } from '#utils/links' import { formatUpgradeVersion } from '#utils/version' import gt from 'semver/functions/gt' import lte from 'semver/functions/lte' import prerelease from 'semver/functions/prerelease' -import { DiagnosticSeverity } from 'vscode' +import { DiagnosticSeverity, Uri } from 'vscode' function createUpgradeDiagnostic(dep: DependencyInfo, parsed: ParsedVersion, target: string): NodeDiagnosticInfo { return { node: dep.versionNode, severity: DiagnosticSeverity.Hint, message: `New version available: ${formatUpgradeVersion(parsed, target)}`, - code: 'upgrade', + code: { + value: 'upgrade', + target: Uri.parse(npmxPackageUrl(dep.name, target)), + }, } } From 4083b5fcb85cc32b0d3de714d5609f095beabbe1 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Feb 2026 22:37:24 +0800 Subject: [PATCH 18/21] test: update --- tests/diagnostics/replacement.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/diagnostics/replacement.test.ts b/tests/diagnostics/replacement.test.ts index f54638e..8fe3808 100644 --- a/tests/diagnostics/replacement.test.ts +++ b/tests/diagnostics/replacement.test.ts @@ -17,6 +17,7 @@ describe('checkReplacement', () => { expect(result).toBeDefined() expect(result!.message).toBeDefined() + expect(result!.code).toMatchObject({ value: 'replacement' }) }) it('should not flag when no replacement found', async () => { From 357ef3c6a267ce3cd90767cc93086fcaecad6320 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Feb 2026 22:49:10 +0800 Subject: [PATCH 19/21] update --- src/providers/diagnostics/rules/upgrade.ts | 3 +-- src/providers/hover/npmx.ts | 2 +- tests/__setup__/msw.ts | 2 +- tests/diagnostics/upgrade.test.ts | 7 +++++++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/providers/diagnostics/rules/upgrade.ts b/src/providers/diagnostics/rules/upgrade.ts index ca6933a..17643b8 100644 --- a/src/providers/diagnostics/rules/upgrade.ts +++ b/src/providers/diagnostics/rules/upgrade.ts @@ -24,8 +24,7 @@ export const checkUpgrade: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) if (!parsed || !exactVersion) return - const { version } = parsed - if (Object.hasOwn(pkg.distTags, version)) + if (Object.hasOwn(pkg.distTags, exactVersion)) return const { latest } = pkg.distTags diff --git a/src/providers/hover/npmx.ts b/src/providers/hover/npmx.ts index 4025b83..8a65715 100644 --- a/src/providers/hover/npmx.ts +++ b/src/providers/hover/npmx.ts @@ -64,7 +64,7 @@ export class NpmxHoverProvider implements HoverProvider { if (exactVersion && pkg.versionsMeta[exactVersion]?.provenance) md.appendMarkdown(`[$(verified)${SPACER}Verified provenance](${npmxPackageUrl(name, version)}#provenance)\n\n`) - const packageLink = `[$(package)${SPACER}View on npmx.dev](${npmxPackageUrl(name, version)})` + const packageLink = `[$(package)${SPACER}View on npmx.dev](${npmxPackageUrl(name)})` const docsLink = `[$(book)${SPACER}View docs on npmx.dev](${npmxDocsUrl(name, version)})` md.appendMarkdown(`${packageLink} | ${docsLink}`) diff --git a/tests/__setup__/msw.ts b/tests/__setup__/msw.ts index f8e8fcb..0247a75 100644 --- a/tests/__setup__/msw.ts +++ b/tests/__setup__/msw.ts @@ -71,6 +71,6 @@ const server = setupServer( }), ) -beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' })) +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) afterEach(() => server.resetHandlers()) afterAll(() => server.close()) diff --git a/tests/diagnostics/upgrade.test.ts b/tests/diagnostics/upgrade.test.ts index 527b7a7..9a71e7c 100644 --- a/tests/diagnostics/upgrade.test.ts +++ b/tests/diagnostics/upgrade.test.ts @@ -43,6 +43,13 @@ describe('checkUpgrade', () => { expect(result).toBeUndefined() }) + it('should not flag when version is a dist tag with protocol', async () => { + const ctx = createUpgradeContext('npm:latest') + const result = await checkUpgrade(ctx) + + expect(result).toBeUndefined() + }) + it('should flag prerelease upgrade within same pre-id', async () => { const ctx = createUpgradeContext('3.0.0-alpha.1') const result = await checkUpgrade(ctx) From f678d9c8e78c8208b1b2d014e9ccd102fef7a47d Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Feb 2026 22:56:40 +0800 Subject: [PATCH 20/21] update --- tests/diagnostics/deprecation.test.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/diagnostics/deprecation.test.ts b/tests/diagnostics/deprecation.test.ts index 5c31afa..5860202 100644 --- a/tests/diagnostics/deprecation.test.ts +++ b/tests/diagnostics/deprecation.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest' -import { DiagnosticTag } from 'vscode' import { checkDeprecation } from '../../src/providers/diagnostics/rules/deprecation' import { createContext } from './context' @@ -10,7 +9,10 @@ function createDeprecationContext(version: string) { distTags: { latest: '2.0.0' }, versionsMeta: { '1.0.0': { - deprecated: 'deprecated', + deprecated: '1.0.0', + }, + '1.2.0': { + deprecated: '1.2.0', }, '2.0.0': {}, }, @@ -19,12 +21,20 @@ function createDeprecationContext(version: string) { describe('checkDeprecation', () => { it('should flag deprecated version', async () => { + const ctx = createDeprecationContext('1.0.0') + const result = await checkDeprecation(ctx) + + expect(result).toBeDefined() + expect(result!.message).toMatchInlineSnapshot('"lodash v1.0.0 has been deprecated: 1.0.0"') + expect(result!.code).toMatchObject({ value: 'deprecation' }) + }) + + it('resolve range to the highest matching deprecated version', async () => { const ctx = createDeprecationContext('^1.0.0') const result = await checkDeprecation(ctx) expect(result).toBeDefined() - expect(result!.message).toContain('deprecated') - expect(result!.tags).toContain(DiagnosticTag.Deprecated) + expect(result!.message).toMatchInlineSnapshot('"lodash v1.2.0 has been deprecated: 1.2.0"') expect(result!.code).toMatchObject({ value: 'deprecation' }) }) From 88bd4cd497ced77477177e75e176ce05ab6df717 Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Feb 2026 22:59:09 +0800 Subject: [PATCH 21/21] update --- tests/__setup__/msw.ts | 94 +++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/tests/__setup__/msw.ts b/tests/__setup__/msw.ts index 0247a75..5f24c5d 100644 --- a/tests/__setup__/msw.ts +++ b/tests/__setup__/msw.ts @@ -8,6 +8,52 @@ const replacementsByName = new Map( all.moduleReplacements.map((r) => [r.moduleName, r]), ) +const vulnerabilityResults: Record> = { + 'pkg-crit@1.0.0': { + package: 'pkg-crit', + version: '1.0.0', + vulnerablePackages: [], + deprecatedPackages: [], + totalPackages: 1, + failedQueries: 0, + totalCounts: { total: 1, critical: 1, high: 0, moderate: 0, low: 0 }, + }, + 'pkg-multi@1.0.0': { + package: 'pkg-multi', + version: '1.0.0', + vulnerablePackages: [], + deprecatedPackages: [], + totalPackages: 1, + failedQueries: 0, + totalCounts: { total: 3, critical: 1, high: 2, moderate: 0, low: 0 }, + }, + 'pkg-fix@1.0.0': { + package: 'pkg-fix', + version: '1.0.0', + vulnerablePackages: [{ + name: 'pkg-fix', + version: '1.0.0', + depth: 'root', + path: [], + vulnerabilities: [{ id: 'GHSA-1', summary: '', severity: 'high', aliases: [], url: '', fixedIn: '1.2.0' }], + counts: { total: 1, critical: 0, high: 1, moderate: 0, low: 0 }, + }], + deprecatedPackages: [], + totalPackages: 1, + failedQueries: 0, + totalCounts: { total: 1, critical: 0, high: 1, moderate: 0, low: 0 }, + }, + 'pkg-safe@1.0.0': { + package: 'pkg-safe', + version: '1.0.0', + vulnerablePackages: [], + deprecatedPackages: [], + totalPackages: 1, + failedQueries: 0, + totalCounts: { total: 0, critical: 0, high: 0, moderate: 0, low: 0 }, + }, +} + const server = setupServer( http.get(`${NPMX_DEV_API}/replacements/:name`, ({ params }) => { const data = replacementsByName.get(params.name as string) @@ -18,53 +64,7 @@ const server = setupServer( http.get(`${NPMX_DEV_API}/registry/vulnerabilities/:name/v/:version`, ({ params }) => { const key = `${params.name}@${params.version}` - const results: Record> = { - 'pkg-crit@1.0.0': { - package: 'pkg-crit', - version: '1.0.0', - vulnerablePackages: [], - deprecatedPackages: [], - totalPackages: 1, - failedQueries: 0, - totalCounts: { total: 1, critical: 1, high: 0, moderate: 0, low: 0 }, - }, - 'pkg-multi@1.0.0': { - package: 'pkg-multi', - version: '1.0.0', - vulnerablePackages: [], - deprecatedPackages: [], - totalPackages: 1, - failedQueries: 0, - totalCounts: { total: 3, critical: 1, high: 2, moderate: 0, low: 0 }, - }, - 'pkg-fix@1.0.0': { - package: 'pkg-fix', - version: '1.0.0', - vulnerablePackages: [{ - name: 'pkg-fix', - version: '1.0.0', - depth: 'root', - path: [], - vulnerabilities: [{ id: 'GHSA-1', summary: '', severity: 'high', aliases: [], url: '', fixedIn: '1.2.0' }], - counts: { total: 1, critical: 0, high: 1, moderate: 0, low: 0 }, - }], - deprecatedPackages: [], - totalPackages: 1, - failedQueries: 0, - totalCounts: { total: 1, critical: 0, high: 1, moderate: 0, low: 0 }, - }, - 'pkg-safe@1.0.0': { - package: 'pkg-safe', - version: '1.0.0', - vulnerablePackages: [], - deprecatedPackages: [], - totalPackages: 1, - failedQueries: 0, - totalCounts: { total: 0, critical: 0, high: 0, moderate: 0, low: 0 }, - }, - } - - const data = results[key] + const data = vulnerabilityResults[key] return data ? HttpResponse.json(data) : new HttpResponse(null, { status: 404 })