Skip to content

Commit da17a10

Browse files
feat(completion): support 'npm:'-prefixed versions (#29)
* feat(utils): unify `parseVersion` * replace `version` with `semver` * fix: support `npm:` prefix semver * [autofix.ci] apply automated fixes * perf: early return for the unsupported protocol * fix: include URL prefixes in version parsing logic * hard code slice length --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent eb787bf commit da17a10

File tree

8 files changed

+152
-36
lines changed

8 files changed

+152
-36
lines changed

playground/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"dependencies": {
33
"@deno/doc": "jsr:^0.189.1",
4-
"@prismicio/client": "~7.21.0-canary.147e3f2"
4+
"@prismicio/client": "~7.21.0-canary.147e3f2",
5+
"nuxt": "npm:4.3.0"
56
},
67
"devDependencies": {
78
"array-includes": "",

src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export const PNPM_WORKSPACE_BASENAME = 'pnpm-workspace.yaml'
44
export const PACKAGE_JSON_PATTERN = `**/${PACKAGE_JSON_BASENAME}`
55
export const PNPM_WORKSPACE_PATTERN = `**/${PNPM_WORKSPACE_BASENAME}`
66

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

99
export const CACHE_TTL_ONE_DAY = 1000 * 60 * 60 * 24
1010

src/providers/completion-item/version.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Extractor } from '#types/extractor'
22
import type { CompletionItemProvider, Position, TextDocument } from 'vscode'
33
import { config } from '#state'
44
import { getPackageInfo } from '#utils/api/package'
5-
import { extractVersionPrefix } from '#utils/package'
5+
import { formatVersion, parseVersion } from '#utils/package'
66
import { CompletionItem, CompletionItemKind } from 'vscode'
77

88
export class VersionCompletionItemProvider<T extends Extractor> implements CompletionItemProvider {
@@ -28,27 +28,29 @@ export class VersionCompletionItemProvider<T extends Extractor> implements Compl
2828
version,
2929
} = info
3030

31+
const parsed = parseVersion(version)
32+
if (!parsed)
33+
return
34+
3135
const pkg = await getPackageInfo(name)
3236
if (!pkg)
3337
return
3438

35-
const prefix = extractVersionPrefix(version)
36-
3739
const items: CompletionItem[] = []
3840

39-
for (const version in pkg.versionsMeta) {
40-
const meta = pkg.versionsMeta[version]
41+
for (const semver in pkg.versionsMeta) {
42+
const meta = pkg.versionsMeta[semver]
4143

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

45-
const text = `${prefix}${version}`
47+
const text = formatVersion({ ...parsed, semver })
4648
const item = new CompletionItem(text, CompletionItemKind.Value)
4749

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

51-
const tag = pkg.versionToTag.get(version)
53+
const tag = pkg.versionToTag.get(semver)
5254
if (tag)
5355
item.detail = tag
5456

src/providers/diagnostics/rules/deprecation.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import type { DiagnosticRule } from '..'
22
import { npmxPackageUrl } from '#utils/links'
3-
import { extractVersion } from '#utils/package'
3+
import { parseVersion } from '#utils/package'
44
import { DiagnosticSeverity, DiagnosticTag, Uri } from 'vscode'
55

66
export const checkDeprecation: DiagnosticRule = (dep, pkg) => {
7-
const exactVersion = extractVersion(dep.version)
8-
const versionInfo = pkg.versionsMeta[exactVersion]
7+
const parsed = parseVersion(dep.version)
8+
if (!parsed)
9+
return
10+
11+
const { semver } = parsed
12+
const versionInfo = pkg.versionsMeta[semver]
913

1014
if (!versionInfo?.deprecated)
1115
return
1216

1317
return {
1418
node: dep.versionNode,
15-
message: `${dep.name} v${exactVersion} has been deprecated: ${versionInfo.deprecated}`,
19+
message: `${dep.name} v${semver} has been deprecated: ${versionInfo.deprecated}`,
1620
severity: DiagnosticSeverity.Error,
1721
code: {
1822
value: 'deprecation',
19-
target: Uri.parse(npmxPackageUrl(dep.name, exactVersion)),
23+
target: Uri.parse(npmxPackageUrl(dep.name, semver)),
2024
},
2125
tags: [DiagnosticTag.Deprecated],
2226
}

src/providers/diagnostics/rules/vulnerability.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { OsvSeverityLevel } from '#utils/api/vulnerability'
22
import type { DiagnosticRule } from '..'
33
import { getVulnerability, SEVERITY_LEVELS } from '#utils/api/vulnerability'
44
import { npmxPackageUrl } from '#utils/links'
5-
import { extractVersion } from '#utils/package'
5+
import { parseVersion } from '#utils/package'
66
import { DiagnosticSeverity, Uri } from 'vscode'
77

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

1515
export const checkVulnerability: DiagnosticRule = async (dep, pkg) => {
16-
const exactVersion = extractVersion(dep.version)
17-
const versionInfo = pkg.versionsMeta[exactVersion]
16+
const parsed = parseVersion(dep.version)
17+
if (!parsed)
18+
return
1819

20+
const { semver } = parsed
21+
const versionInfo = pkg.versionsMeta[semver]
1922
if (!versionInfo)
2023
return
2124

22-
const result = await getVulnerability({ name: dep.name, version: exactVersion })
25+
const result = await getVulnerability({ name: dep.name, version: semver })
2326
if (!result)
2427
return
2528

@@ -48,7 +51,7 @@ export const checkVulnerability: DiagnosticRule = async (dep, pkg) => {
4851
severity: DiagnosticSeverity.Error,
4952
code: {
5053
value: 'vulnerability',
51-
target: Uri.parse(npmxPackageUrl(dep.name, exactVersion)),
54+
target: Uri.parse(npmxPackageUrl(dep.name, semver)),
5255
},
5356
}
5457
}

src/providers/hover/npmx.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { HoverProvider, Position, TextDocument } from 'vscode'
33
import { SPACER } from '#constants'
44
import { getPackageInfo } from '#utils/api/package'
55
import { npmPacakgeUrl, npmxDocsUrl, npmxPackageUrl } from '#utils/links'
6-
import { extractVersion } from '#utils/package'
6+
import { parseVersion } from '#utils/package'
77
import { Hover, MarkdownString } from 'vscode'
88

99
export class NpmxHoverProvider<T extends Extractor> implements HoverProvider {
@@ -23,23 +23,29 @@ export class NpmxHoverProvider<T extends Extractor> implements HoverProvider {
2323
if (!dep)
2424
return
2525

26-
const { name, version } = dep
27-
const coercedVersion = extractVersion(version)
28-
const md = new MarkdownString('', true)
29-
md.isTrusted = true
26+
const parsed = parseVersion(dep.version)
27+
if (!parsed)
28+
return
29+
30+
const { name } = dep
3031

3132
const pkg = await getPackageInfo(name)
3233
if (!pkg)
3334
return
3435

35-
const currentVersion = pkg.versionsMeta[coercedVersion]
36+
const md = new MarkdownString('', true)
37+
md.isTrusted = true
38+
39+
const { semver } = parsed
40+
41+
const currentVersion = pkg.versionsMeta[semver]
3642
if (currentVersion) {
3743
if (currentVersion.provenance)
38-
md.appendMarkdown(`[$(verified)${SPACER}Verified provenance](${npmPacakgeUrl(name, coercedVersion)}#provenance)\n\n`)
44+
md.appendMarkdown(`[$(verified)${SPACER}Verified provenance](${npmPacakgeUrl(name, semver)}#provenance)\n\n`)
3945
}
4046

4147
const packageLink = `[$(package)${SPACER}View on npmx](${npmxPackageUrl(name)})`
42-
const docsLink = `[$(book)${SPACER}View docs on npmx](${npmxDocsUrl(name, coercedVersion)})`
48+
const docsLink = `[$(book)${SPACER}View docs on npmx](${npmxDocsUrl(name, semver)})`
4349

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

src/utils/package.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,51 @@ export function encodePackageName(name: string): string {
99
return encodeURIComponent(name)
1010
}
1111

12-
export function isValidPrefix(c: string) {
13-
return c === '^' || c === '~'
14-
}
12+
const WORKSPACE_PREFIX = 'workspace:'
13+
const CATALOG_PREFIX = 'catalog:'
14+
const NPM_PREFIX = 'npm:'
15+
const JSR_PREFIX = 'jsr:'
16+
const URL_PREFIXES = ['http://', 'https://', 'git://', 'git+']
17+
18+
export type VersionProtocol = 'npm' | null
1519

16-
export function extractVersionPrefix(v: string) {
17-
const firstChar = v[0]
18-
const valid = isValidPrefix(firstChar)
20+
export interface ParsedVersion {
21+
protocol: VersionProtocol
22+
prefix: '' | '^' | '~'
23+
semver: string
24+
}
1925

20-
return valid ? firstChar : ''
26+
export function formatVersion(parsed: ParsedVersion): string {
27+
const protocol = parsed.protocol ? `${parsed.protocol}:` : ''
28+
return `${protocol}${parsed.prefix}${parsed.semver}`
2129
}
2230

23-
export function extractVersion(versionRange: string): string {
24-
return versionRange.replace(/^[\^~]/, '')
31+
export function parseVersion(rawVersion: string): ParsedVersion | null {
32+
// Skip special protocols that aren't standard npm versions
33+
if (
34+
[
35+
WORKSPACE_PREFIX,
36+
CATALOG_PREFIX,
37+
JSR_PREFIX,
38+
...URL_PREFIXES,
39+
].some((p) => rawVersion.startsWith(p))
40+
) {
41+
return null
42+
}
43+
44+
let protocol: VersionProtocol = null
45+
let versionStr = rawVersion
46+
47+
// Handle npm: protocol (e.g., npm:^1.0.0)
48+
if (rawVersion.startsWith(NPM_PREFIX)) {
49+
protocol = 'npm'
50+
versionStr = rawVersion.slice(4 /* NPM_PREFIX.length */)
51+
}
52+
53+
const firstChar = versionStr[0]
54+
const hasPrefix = firstChar === '^' || firstChar === '~'
55+
const prefix = hasPrefix ? firstChar : ''
56+
const semver = hasPrefix ? versionStr.slice(1) : versionStr
57+
58+
return { protocol, prefix, semver }
2559
}

tests/package.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { encodePackageName, parseVersion } from '../src/utils/package'
3+
4+
describe('encodePackageName', () => {
5+
it('should encode regular package name', () => {
6+
expect(encodePackageName('lodash')).toBe('lodash')
7+
})
8+
9+
it('should encode scoped package name', () => {
10+
expect(encodePackageName('@vue/core')).toBe('@vue%2Fcore')
11+
})
12+
})
13+
14+
describe('parseVersion', () => {
15+
it('should parse plain version', () => {
16+
expect(parseVersion('1.0.0')).toEqual({
17+
protocol: null,
18+
prefix: '',
19+
semver: '1.0.0',
20+
})
21+
})
22+
23+
it('should parse version with ^ prefix', () => {
24+
expect(parseVersion('^1.2.3')).toEqual({
25+
protocol: null,
26+
prefix: '^',
27+
semver: '1.2.3',
28+
})
29+
})
30+
31+
it('should parse version with ~ prefix', () => {
32+
expect(parseVersion('~2.0.0')).toEqual({
33+
protocol: null,
34+
prefix: '~',
35+
semver: '2.0.0',
36+
})
37+
})
38+
39+
it('should parse npm: protocol', () => {
40+
expect(parseVersion('npm:1.0.0')).toEqual({
41+
protocol: 'npm',
42+
prefix: '',
43+
semver: '1.0.0',
44+
})
45+
})
46+
47+
it('should parse npm: protocol with prefix', () => {
48+
expect(parseVersion('npm:^1.0.0')).toEqual({
49+
protocol: 'npm',
50+
prefix: '^',
51+
semver: '1.0.0',
52+
})
53+
})
54+
55+
it('should return null for workspace:', () => {
56+
expect(parseVersion('workspace:*')).toBeNull()
57+
})
58+
59+
it('should return null for catalog:', () => {
60+
expect(parseVersion('catalog:default')).toBeNull()
61+
})
62+
63+
it('should return null for jsr:', () => {
64+
expect(parseVersion('jsr:@std/fs')).toBeNull()
65+
})
66+
})

0 commit comments

Comments
 (0)