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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/providers/completion-item/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Extractor } from '#types/extractor'
import type { CompletionItemProvider, Position, TextDocument } from 'vscode'
import { config } from '#state'
import { getPackageInfo } from '#utils/api/package'
import { extractVersionPrefix } from '#utils/package'
import { parseVersion } from '#utils/package'
import { CompletionItem, CompletionItemKind } from 'vscode'

export class VersionCompletionItemProvider<T extends Extractor> implements CompletionItemProvider {
Expand Down Expand Up @@ -32,7 +32,11 @@ export class VersionCompletionItemProvider<T extends Extractor> implements Compl
if (!pkg)
return

const prefix = extractVersionPrefix(version)
const parsed = parseVersion(version)
if (!parsed)
return

const { prefix } = parsed

const items: CompletionItem[] = []

Expand Down
14 changes: 9 additions & 5 deletions src/providers/diagnostics/rules/deprecation.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import type { DiagnosticRule } from '..'
import { npmxPackageUrl } from '#utils/links'
import { extractVersion } from '#utils/package'
import { parseVersion } from '#utils/package'
import { DiagnosticSeverity, DiagnosticTag, Uri } from 'vscode'

export const checkDeprecation: DiagnosticRule = (dep, pkg) => {
const exactVersion = extractVersion(dep.version)
const versionInfo = pkg.versionsMeta[exactVersion]
const parsed = parseVersion(dep.version)
if (!parsed)
return

const { version } = parsed
const versionInfo = pkg.versionsMeta[version]

if (!versionInfo?.deprecated)
return

return {
node: dep.versionNode,
message: `${dep.name} v${exactVersion} has been deprecated: ${versionInfo.deprecated}`,
message: `${dep.name} v${version} has been deprecated: ${versionInfo.deprecated}`,
severity: DiagnosticSeverity.Error,
code: {
value: 'deprecation',
target: Uri.parse(npmxPackageUrl(dep.name, exactVersion)),
target: Uri.parse(npmxPackageUrl(dep.name, version)),
},
tags: [DiagnosticTag.Deprecated],
}
Expand Down
13 changes: 8 additions & 5 deletions src/providers/diagnostics/rules/vulnerability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { OsvSeverityLevel } from '#utils/api/vulnerability'
import type { DiagnosticRule } from '..'
import { getVulnerability, SEVERITY_LEVELS } from '#utils/api/vulnerability'
import { npmxPackageUrl } from '#utils/links'
import { extractVersion } from '#utils/package'
import { parseVersion } from '#utils/package'
import { DiagnosticSeverity, Uri } from 'vscode'

const DIAGNOSTIC_MAPPING: Record<Exclude<OsvSeverityLevel, 'unknown'>, DiagnosticSeverity> = {
Expand All @@ -13,13 +13,16 @@ const DIAGNOSTIC_MAPPING: Record<Exclude<OsvSeverityLevel, 'unknown'>, Diagnosti
}

export const checkVulnerability: DiagnosticRule = async (dep, pkg) => {
const exactVersion = extractVersion(dep.version)
const versionInfo = pkg.versionsMeta[exactVersion]
const parsed = parseVersion(dep.version)
if (!parsed)
return

const { version } = parsed
const versionInfo = pkg.versionsMeta[version]
if (!versionInfo)
return

const result = await getVulnerability({ name: dep.name, version: exactVersion })
const result = await getVulnerability({ name: dep.name, version })
if (!result)
return

Expand Down Expand Up @@ -48,7 +51,7 @@ export const checkVulnerability: DiagnosticRule = async (dep, pkg) => {
severity: DiagnosticSeverity.Error,
code: {
value: 'vulnerability',
target: Uri.parse(npmxPackageUrl(dep.name, exactVersion)),
target: Uri.parse(npmxPackageUrl(dep.name, version)),
},
}
}
22 changes: 14 additions & 8 deletions src/providers/hover/npmx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { HoverProvider, Position, TextDocument } from 'vscode'
import { SPACER } from '#constants'
import { getPackageInfo } from '#utils/api/package'
import { npmPacakgeUrl, npmxDocsUrl, npmxPackageUrl } from '#utils/links'
import { extractVersion } from '#utils/package'
import { parseVersion } from '#utils/package'
import { Hover, MarkdownString } from 'vscode'

export class NpmxHoverProvider<T extends Extractor> implements HoverProvider {
Expand All @@ -23,23 +23,29 @@ export class NpmxHoverProvider<T extends Extractor> implements HoverProvider {
if (!dep)
return

const { name, version } = dep
const coercedVersion = extractVersion(version)
const md = new MarkdownString('', true)
md.isTrusted = true
const parsed = parseVersion(dep.version)
if (!parsed)
return

const { name } = dep

const pkg = await getPackageInfo(name)
if (!pkg)
return

const currentVersion = pkg.versionsMeta[coercedVersion]
const md = new MarkdownString('', true)
md.isTrusted = true

const { version } = parsed

const currentVersion = pkg.versionsMeta[version]
if (currentVersion) {
if (currentVersion.provenance)
md.appendMarkdown(`[$(verified)${SPACER}Verified provenance](${npmPacakgeUrl(name, coercedVersion)}#provenance)\n\n`)
md.appendMarkdown(`[$(verified)${SPACER}Verified provenance](${npmPacakgeUrl(name, version)}#provenance)\n\n`)
}

const packageLink = `[$(package)${SPACER}View on npmx](${npmxPackageUrl(name)})`
const docsLink = `[$(book)${SPACER}View docs on npmx](${npmxDocsUrl(name, coercedVersion)})`
const docsLink = `[$(book)${SPACER}View docs on npmx](${npmxDocsUrl(name, version)})`

md.appendMarkdown(`${packageLink} | ${docsLink}`)

Expand Down
39 changes: 29 additions & 10 deletions src/utils/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,36 @@ export function encodePackageName(name: string): string {
return encodeURIComponent(name)
}

export function isValidPrefix(c: string) {
return c === '^' || c === '~'
}
const WORKSPACE_PREFIX = 'workspace:'
const CATALOG_PREFIX = 'catalog:'
const NPM_PREFIX = 'npm:'
const JSR_PREFIX = 'jsr:'

export function extractVersionPrefix(v: string) {
const firstChar = v[0]
const valid = isValidPrefix(firstChar)
export type VersionProtocol = 'npm' | null

return valid ? firstChar : ''
}
export function parseVersion(rawVersion: string): { prefix: '' | '^' | '~', version: string, protocol: VersionProtocol } | null {
// Skip special protocols that aren't standard npm versions
if (
rawVersion.startsWith(WORKSPACE_PREFIX)
|| rawVersion.startsWith(CATALOG_PREFIX)
|| rawVersion.startsWith(JSR_PREFIX)
) {
return null
}

let protocol: VersionProtocol = null
let versionStr = rawVersion

// Handle npm: protocol (e.g., npm:^1.0.0)
if (rawVersion.startsWith(NPM_PREFIX)) {
protocol = 'npm'
versionStr = rawVersion.slice(NPM_PREFIX.length)
}

const firstChar = versionStr[0]
const hasPrefix = firstChar === '^' || firstChar === '~'
const prefix = hasPrefix ? firstChar : ''
const version = hasPrefix ? versionStr.slice(1) : versionStr

export function extractVersion(versionRange: string): string {
return versionRange.replace(/^[\^~]/, '')
return { prefix, version, protocol }
}
66 changes: 66 additions & 0 deletions tests/package.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest'
import { encodePackageName, parseVersion } from '../src/utils/package'

describe('encodePackageName', () => {
it('should encode regular package name', () => {
expect(encodePackageName('lodash')).toBe('lodash')
})

it('should encode scoped package name', () => {
expect(encodePackageName('@vue/core')).toBe('@vue%2Fcore')
})
})

describe('parseVersion', () => {
it('should parse plain version', () => {
expect(parseVersion('1.0.0')).toEqual({
prefix: '',
version: '1.0.0',
protocol: null,
})
})

it('should parse version with ^ prefix', () => {
expect(parseVersion('^1.2.3')).toEqual({
prefix: '^',
version: '1.2.3',
protocol: null,
})
})

it('should parse version with ~ prefix', () => {
expect(parseVersion('~2.0.0')).toEqual({
prefix: '~',
version: '2.0.0',
protocol: null,
})
})

it('should parse npm: protocol', () => {
expect(parseVersion('npm:1.0.0')).toEqual({
prefix: '',
version: '1.0.0',
protocol: 'npm',
})
})

it('should parse npm: protocol with prefix', () => {
expect(parseVersion('npm:^1.0.0')).toEqual({
prefix: '^',
version: '1.0.0',
protocol: 'npm',
})
})

it('should return null for workspace:', () => {
expect(parseVersion('workspace:*')).toBeNull()
})

it('should return null for catalog:', () => {
expect(parseVersion('catalog:default')).toBeNull()
})

it('should return null for jsr:', () => {
expect(parseVersion('jsr:@std/fs')).toBeNull()
})
})