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
2 changes: 1 addition & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"dependencies": {
"@deno/doc": "jsr:^0.189.1",
"@prismicio/client": "~7.21.0-canary.147e3f2",
"nuxt": "npm:4.3.0"
"nuxt": "npm:nuxt@4.3.0"
},
"devDependencies": {
"ofetch": "^2.0.0-alpha.1",
Expand Down
7 changes: 6 additions & 1 deletion src/providers/completion-item/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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 { resolvePackageName } from '#utils/package'
import { formatUpgradeVersion, isSupportedProtocol, parseVersion } from '#utils/version'
import { CompletionItem, CompletionItemKind } from 'vscode'

Expand Down Expand Up @@ -33,7 +34,11 @@ export class VersionCompletionItemProvider<T extends Extractor> implements Compl
if (!parsed || !isSupportedProtocol(parsed.protocol))
return

const pkg = await getPackageInfo(name)
const packageName = resolvePackageName(name, parsed)
if (!packageName)
return

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

Expand Down
13 changes: 9 additions & 4 deletions src/providers/diagnostics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { Diagnostic, TextDocument, Uri } from 'vscode'
import { extractorEntries } from '#extractors'
import { config, logger } from '#state'
import { getPackageInfo } from '#utils/api/package'
import { resolveExactVersion } from '#utils/package'
import { resolveExactVersion, resolvePackageName } from '#utils/package'
import { isSupportedProtocol, parseVersion } from '#utils/version'
import { debounce } from 'perfect-debounce'
import { computed, useActiveTextEditor, useDisposable, useDocumentText, useFileSystemWatcher, watch } from 'reactive-vscode'
Expand All @@ -22,6 +22,7 @@ import { checkVulnerability } from './rules/vulnerability'

export interface DiagnosticContext {
dep: DependencyInfo
name: string
pkg: PackageInfo
parsed: ParsedVersion | null
exactVersion: string | null
Expand Down Expand Up @@ -108,17 +109,21 @@ export function useDiagnostics() {

const collect = async (dep: DependencyInfo) => {
try {
const pkg = await getPackageInfo(dep.name)
const parsed = parseVersion(dep.version)
const name = resolvePackageName(dep.name, parsed)
if (!name)
return

const pkg = await getPackageInfo(name)
if (!pkg || isStale(document, targetVersion))
return

const parsed = parseVersion(dep.version)
const exactVersion = parsed && isSupportedProtocol(parsed.protocol)
? resolveExactVersion(pkg, parsed.version)
: null

for (const rule of rules) {
runRule(rule, { dep, pkg, parsed, exactVersion, engines })
runRule(rule, { dep, name, pkg, parsed, exactVersion, engines })
}
} catch (err) {
logger.warn(`[diagnostics] fail to check ${dep.name}: ${err}`)
Expand Down
8 changes: 4 additions & 4 deletions src/providers/diagnostics/rules/deprecation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { npmxPackageUrl } from '#utils/links'
import { formatPackageId } from '#utils/package'
import { DiagnosticSeverity, DiagnosticTag, Uri } from 'vscode'

export const checkDeprecation: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) => {
export const checkDeprecation: DiagnosticRule = ({ dep, name, pkg, parsed, exactVersion }) => {
if (!parsed || !exactVersion)
return

Expand All @@ -14,16 +14,16 @@ export const checkDeprecation: DiagnosticRule = ({ dep, pkg, parsed, exactVersio
if (!versionInfo.deprecated)
return

if (checkIgnored({ ignoreList: config.ignore.deprecation, name: dep.name, version: exactVersion }))
if (checkIgnored({ ignoreList: config.ignore.deprecation, name, version: exactVersion }))
return

return {
node: dep.versionNode,
message: `"${formatPackageId(dep.name, exactVersion)}" has been deprecated: ${versionInfo.deprecated}`,
message: `"${formatPackageId(name, exactVersion)}" has been deprecated: ${versionInfo.deprecated}`,
severity: DiagnosticSeverity.Error,
code: {
value: 'deprecation',
target: Uri.parse(npmxPackageUrl(dep.name, parsed.version)),
target: Uri.parse(npmxPackageUrl(name, parsed.version)),
},
tags: [DiagnosticTag.Deprecated],
}
Expand Down
6 changes: 3 additions & 3 deletions src/providers/diagnostics/rules/dist-tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { DiagnosticRule } from '..'
import { npmxPackageUrl } from '#utils/links'
import { DiagnosticSeverity, Uri } from 'vscode'

export const checkDistTag: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) => {
export const checkDistTag: DiagnosticRule = ({ dep, name, pkg, parsed, exactVersion }) => {
if (!parsed || !exactVersion)
return

Expand All @@ -12,11 +12,11 @@ export const checkDistTag: DiagnosticRule = ({ dep, pkg, parsed, exactVersion })

return {
node: dep.versionNode,
message: `"${dep.name}" uses the "${tag}" version tag. This may lead to unexpected breaking changes. Consider pinning to a specific version.`,
message: `"${name}" uses the "${tag}" version tag. This may lead to unexpected breaking changes. Consider pinning to a specific version.`,
severity: DiagnosticSeverity.Warning,
code: {
value: 'dist-tag',
target: Uri.parse(npmxPackageUrl(dep.name)),
target: Uri.parse(npmxPackageUrl(name)),
},
}
}
6 changes: 3 additions & 3 deletions src/providers/diagnostics/rules/engine-mismatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function resolveEngineMismatches(
return mismatches
}

export const checkEngineMismatch: DiagnosticRule = ({ dep, pkg, parsed, exactVersion, engines }) => {
export const checkEngineMismatch: DiagnosticRule = ({ dep, name, pkg, parsed, exactVersion, engines }) => {
if (!parsed || !exactVersion || !engines)
return

Expand All @@ -64,11 +64,11 @@ export const checkEngineMismatch: DiagnosticRule = ({ dep, pkg, parsed, exactVer

return {
node: dep.versionNode,
message: `Engines mismatch for "${formatPackageId(dep.name, exactVersion)}": ${mismatchDetails}.`,
message: `Engines mismatch for "${formatPackageId(name, exactVersion)}": ${mismatchDetails}.`,
severity: DiagnosticSeverity.Warning,
code: {
value: 'engine-mismatch',
target: Uri.parse(npmxPackageUrl(dep.name, parsed.version)),
target: Uri.parse(npmxPackageUrl(name, parsed.version)),
},
}
}
8 changes: 4 additions & 4 deletions src/providers/diagnostics/rules/replacement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,19 @@ function getReplacementInfo(replacement: ModuleReplacement) {
}
}

export const checkReplacement: DiagnosticRule = async ({ dep }) => {
if (checkIgnored({ ignoreList: config.ignore.replacement, name: dep.name }))
export const checkReplacement: DiagnosticRule = async ({ dep, name }) => {
if (checkIgnored({ ignoreList: config.ignore.replacement, name }))
return

const replacement = await getReplacement(dep.name)
const replacement = await getReplacement(name)
if (!replacement)
return

const { message, link } = getReplacementInfo(replacement)

return {
node: dep.nameNode,
message: `"${dep.name}" ${message}`,
message: `"${name}" ${message}`,
severity: DiagnosticSeverity.Warning,
code: link ? { value: 'replacement', target: Uri.parse(link) } : 'replacement',
}
Expand Down
22 changes: 11 additions & 11 deletions src/providers/diagnostics/rules/upgrade.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DependencyInfo } from '#types/extractor'
import type { ValidNode } from '#types/extractor'
import type { DiagnosticRule, NodeDiagnosticInfo } from '..'
import { config } from '#state'
import { checkIgnored } from '#utils/ignore'
Expand All @@ -9,31 +9,31 @@ import lte from 'semver/functions/lte'
import prerelease from 'semver/functions/prerelease'
import { DiagnosticSeverity, Uri } from 'vscode'

function createUpgradeDiagnostic(dep: DependencyInfo, targetVersion: string): NodeDiagnosticInfo {
function createUpgradeDiagnostic(node: ValidNode, name: string, targetVersion: string): NodeDiagnosticInfo {
return {
node: dep.versionNode,
node,
severity: DiagnosticSeverity.Hint,
message: `"${dep.name}" can be upgraded to ${targetVersion}.`,
message: `"${name}" can be upgraded to ${targetVersion}.`,
code: {
value: 'upgrade',
target: Uri.parse(npmxPackageUrl(dep.name, targetVersion)),
target: Uri.parse(npmxPackageUrl(name, targetVersion)),
},
}
}

export const checkUpgrade: DiagnosticRule = ({ dep, pkg, parsed, exactVersion }) => {
export const checkUpgrade: DiagnosticRule = ({ dep, name, pkg, parsed, exactVersion }) => {
if (!parsed || !exactVersion)
return

if (Object.hasOwn(pkg.distTags, exactVersion))
if (Object.hasOwn(pkg.distTags, dep.version))
return
Comment on lines +28 to 29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Alias dist-tags are no longer filtered out.

Line 28 now checks pkg.distTags against dep.version, but DependencyInfo.version is the raw manifest spec (src/types/extractor.ts:8-13). For alias declarations like npm:react@next or npm-compatible JSR aliases, that raw value will never equal a dist-tag key such as next, so this early return is skipped and we emit a bogus upgrade hint. Please normalise the declared spec down to its tag component before calling hasOwn, or read the tag from parsed instead.


const { latest } = pkg.distTags
if (gt(latest, exactVersion)) {
const targetVersion = formatUpgradeVersion(parsed, latest)
if (checkIgnored({ ignoreList: config.ignore.upgrade, name: dep.name, version: targetVersion }))
if (checkIgnored({ ignoreList: config.ignore.upgrade, name, version: targetVersion }))
return
return createUpgradeDiagnostic(dep, targetVersion)
return createUpgradeDiagnostic(dep.versionNode, name, targetVersion)
}

const currentPreId = prerelease(exactVersion)?.[0]
Expand All @@ -48,9 +48,9 @@ export const checkUpgrade: DiagnosticRule = ({ dep, pkg, parsed, exactVersion })
if (lte(tagVersion, exactVersion))
continue
const targetVersion = formatUpgradeVersion(parsed, tagVersion)
if (checkIgnored({ ignoreList: config.ignore.upgrade, name: dep.name, version: targetVersion }))
if (checkIgnored({ ignoreList: config.ignore.upgrade, name, version: targetVersion }))
continue

return createUpgradeDiagnostic(dep, targetVersion)
return createUpgradeDiagnostic(dep.versionNode, name, targetVersion)
}
}
10 changes: 5 additions & 5 deletions src/providers/diagnostics/rules/vulnerability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ function getBigestFixedInVersion(vulnerablePackages: PackageVulnerabilityInfo[])
return bigest
}

export const checkVulnerability: DiagnosticRule = async ({ dep, parsed, exactVersion }) => {
export const checkVulnerability: DiagnosticRule = async ({ dep, name, parsed, exactVersion }) => {
if (!parsed || !exactVersion)
return

if (checkIgnored({ ignoreList: config.ignore.vulnerability, name: dep.name, version: exactVersion }))
if (checkIgnored({ ignoreList: config.ignore.vulnerability, name, version: exactVersion }))
return

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

Expand Down Expand Up @@ -66,11 +66,11 @@ export const checkVulnerability: DiagnosticRule = async ({ dep, parsed, exactVer

return {
node: dep.versionNode,
message: `"${formatPackageId(dep.name, exactVersion)}" has ${message.join(', ')} ${message.length === 1 ? 'vulnerability' : 'vulnerabilities'}.${messageSuffix}`,
message: `"${formatPackageId(name, exactVersion)}" has ${message.join(', ')} ${message.length === 1 ? 'vulnerability' : 'vulnerabilities'}.${messageSuffix}`,
severity: severity ?? DiagnosticSeverity.Error,
code: {
value: 'vulnerability',
target: Uri.parse(npmxPackageUrl(dep.name, parsed.version)),
target: Uri.parse(npmxPackageUrl(name, parsed.version)),
},
}
}
25 changes: 12 additions & 13 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 { jsrPackageUrl, npmxDocsUrl, npmxPackageUrl } from '#utils/links'
import { resolveExactVersion } from '#utils/package'
import { isJsrNpmPackage, jsrNpmToJsrName, resolveExactVersion, resolvePackageName } from '#utils/package'
import { isSupportedProtocol, parseVersion } from '#utils/version'
import { Hover, MarkdownString } from 'vscode'

Expand All @@ -28,26 +28,25 @@ export class NpmxHoverProvider<T extends Extractor> implements HoverProvider {
if (!parsed)
return

const { name } = dep
const { protocol, version } = parsed
const packageName = resolvePackageName(dep.name, parsed)
if (!packageName)
return

if (protocol === 'jsr') {
if (protocol === 'jsr' || isJsrNpmPackage(packageName)) {
const jsrMd = new MarkdownString('', true)
const jsrUrl = jsrPackageUrl(name)

jsrMd.isTrusted = true

const jsrPackageLink = `[$(package)${SPACER}View on jsr.io](${jsrUrl})`
const npmxWarning = '$(warning) Not on npmx'
jsrMd.appendMarkdown(`${jsrPackageLink} | ${npmxWarning}`)

const jsrName = jsrNpmToJsrName(packageName)
const jsrPackageLink = `[$(package)${SPACER}View on jsr.io](${jsrPackageUrl(jsrName)})`
jsrMd.appendMarkdown(`${jsrPackageLink} | $(warning) Not on npmx`)
return new Hover(jsrMd)
}

if (!isSupportedProtocol(protocol))
return

const pkg = await getPackageInfo(name)
const pkg = await getPackageInfo(packageName)
if (!pkg) {
const errorMd = new MarkdownString('', true)

Expand All @@ -62,10 +61,10 @@ export class NpmxHoverProvider<T extends Extractor> implements HoverProvider {

const exactVersion = resolveExactVersion(pkg, version)
if (exactVersion && pkg.versionsMeta[exactVersion]?.provenance)
md.appendMarkdown(`[$(verified)${SPACER}Verified provenance](${npmxPackageUrl(name, version)}#provenance)\n\n`)
md.appendMarkdown(`[$(verified)${SPACER}Verified provenance](${npmxPackageUrl(packageName, 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, version)})`
const packageLink = `[$(package)${SPACER}View on npmx.dev](${npmxPackageUrl(packageName)})`
const docsLink = `[$(book)${SPACER}View docs on npmx.dev](${npmxDocsUrl(packageName, version)})`

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

Expand Down
22 changes: 22 additions & 0 deletions src/utils/package.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { PackageInfo } from './api/package'
import type { ParsedVersion } from './version'
import Range from 'semver/classes/range'
import gt from 'semver/functions/gt'
import lte from 'semver/functions/lte'
Expand All @@ -15,6 +16,27 @@ export function encodePackageName(name: string): string {
return encodeURIComponent(name)
}

export function resolvePackageName(depName: string, parsed: ParsedVersion | null): string {
return parsed?.aliasName ?? depName
}

const JSR_NPM_SCOPE = '@jsr/'

export function isJsrNpmPackage(name: string): boolean {
return name.startsWith(JSR_NPM_SCOPE)
}

export function jsrNpmToJsrName(name: string): string {
if (!isJsrNpmPackage(name))
return name

const bare = name.slice(JSR_NPM_SCOPE.length)
const separatorIndex = bare.indexOf('__')
if (separatorIndex === -1)
return bare
return `@${bare.slice(0, separatorIndex)}/${bare.slice(separatorIndex + 2)}`
}

export function formatPackageId(name: string, version: string): string {
return `${name}@${version}`
}
Expand Down
Loading