Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"dependencies": {
"@deno/doc": "jsr:^0.189.1",
"@prismicio/client": "~7.21.0-canary.147e3f2"
"@prismicio/client": "~7.21.0-canary.147e3f2",
"nuxt": "npm:4.3.0"
},
"devDependencies": {
"array-includes": "",
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const PNPM_WORKSPACE_BASENAME = 'pnpm-workspace.yaml'
export const PACKAGE_JSON_PATTERN = `**/${PACKAGE_JSON_BASENAME}`
export const PNPM_WORKSPACE_PATTERN = `**/${PNPM_WORKSPACE_BASENAME}`

export const VERSION_TRIGGER_CHARACTERS = ['.', '^', '~', ...Array.from({ length: 10 }).map((_, i) => `${i}`)]
export const VERSION_TRIGGER_CHARACTERS = [':', '^', '~', '.', ...Array.from({ length: 10 }).map((_, i) => `${i}`)]

export const CACHE_TTL_ONE_DAY = 1000 * 60 * 60 * 24

Expand Down
16 changes: 9 additions & 7 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 { formatVersion, parseVersion } from '#utils/package'
import { CompletionItem, CompletionItemKind } from 'vscode'

export class VersionCompletionItemProvider<T extends Extractor> implements CompletionItemProvider {
Expand All @@ -28,27 +28,29 @@ export class VersionCompletionItemProvider<T extends Extractor> implements Compl
version,
} = info

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

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

const prefix = extractVersionPrefix(version)

const items: CompletionItem[] = []

for (const version in pkg.versionsMeta) {
const meta = pkg.versionsMeta[version]
for (const semver in pkg.versionsMeta) {
const meta = pkg.versionsMeta[semver]

if (config.completion.version === 'provenance-only' && !meta.provenance)
continue

const text = `${prefix}${version}`
const text = formatVersion({ ...parsed, semver })
const item = new CompletionItem(text, CompletionItemKind.Value)

item.range = this.extractor.getNodeRange(document, versionNode)
item.insertText = text

const tag = pkg.versionToTag.get(version)
const tag = pkg.versionToTag.get(semver)
if (tag)
item.detail = tag

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 { semver } = parsed
const versionInfo = pkg.versionsMeta[semver]

if (!versionInfo?.deprecated)
return

return {
node: dep.versionNode,
message: `${dep.name} v${exactVersion} has been deprecated: ${versionInfo.deprecated}`,
message: `${dep.name} v${semver} has been deprecated: ${versionInfo.deprecated}`,
severity: DiagnosticSeverity.Error,
code: {
value: 'deprecation',
target: Uri.parse(npmxPackageUrl(dep.name, exactVersion)),
target: Uri.parse(npmxPackageUrl(dep.name, semver)),
},
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 { semver } = parsed
const versionInfo = pkg.versionsMeta[semver]
if (!versionInfo)
return

const result = await getVulnerability({ name: dep.name, version: exactVersion })
const result = await getVulnerability({ name: dep.name, version: semver })
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, semver)),
},
}
}
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 { semver } = parsed

const currentVersion = pkg.versionsMeta[semver]
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, semver)}#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, semver)})`

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

Expand Down
52 changes: 43 additions & 9 deletions src/utils/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,51 @@ 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:'
const URL_PREFIXES = ['http://', 'https://', 'git://', 'git+']

export type VersionProtocol = 'npm' | null

export function extractVersionPrefix(v: string) {
const firstChar = v[0]
const valid = isValidPrefix(firstChar)
export interface ParsedVersion {
protocol: VersionProtocol
prefix: '' | '^' | '~'
semver: string
}

return valid ? firstChar : ''
export function formatVersion(parsed: ParsedVersion): string {
const protocol = parsed.protocol ? `${parsed.protocol}:` : ''
return `${protocol}${parsed.prefix}${parsed.semver}`
}

export function extractVersion(versionRange: string): string {
return versionRange.replace(/^[\^~]/, '')
export function parseVersion(rawVersion: string): ParsedVersion | null {
// Skip special protocols that aren't standard npm versions
if (
[
WORKSPACE_PREFIX,
CATALOG_PREFIX,
JSR_PREFIX,
...URL_PREFIXES,
].some((p) => rawVersion.startsWith(p))
) {
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(4 /* NPM_PREFIX.length */)
}

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

return { protocol, prefix, semver }
}
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({
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({
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',
})
})

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()
})
})