Skip to content
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@
"*": "eslint --fix"
},
"devDependencies": {
"@npm/types": "^2.1.0",
"@types/node": "^25.1.0",
"@types/vscode": "1.101.0",
"@vida0905/eslint-config": "^2.9.0",
"@vscode/vsce": "^3.7.1",
"eslint": "^9.39.2",
"fast-npm-meta": "^1.2.0",
"husky": "^9.1.7",
"jsonc-parser": "^3.3.1",
"module-replacements": "^2.11.0",
Expand Down
48 changes: 19 additions & 29 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export const VERSION_TRIGGER_CHARACTERS = ['.', '^', '~', ...Array.from({ length

export const CACHE_TTL_ONE_DAY = 1000 * 60 * 60 * 24

export const NPMJS_COM = 'https://npmjs.com'
export const NPMX_DEV = 'https://npmx.dev'

export const NPM_REGISTRY = 'https://registry.npmjs.org'
export const NPMX_DEV_API = `${NPMX_DEV}/api`
18 changes: 12 additions & 6 deletions src/providers/completion-item/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,27 @@ export class VersionCompletionItemProvider<T extends Extractor> implements Compl

const prefix = extractVersionPrefix(version)

let versionsKV = Object.values(pkg.versions)
const items: CompletionItem[] = []

if (config.completion.version === 'provenance-only')
versionsKV = versionsKV.filter(({ hasProvenance }) => hasProvenance)
for (const version in pkg.versionsMeta) {
const meta = pkg.versionsMeta[version]

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

return versionsKV.map(({ version, tag }) => {
const text = `${prefix}${version}`
const item = new CompletionItem(text, CompletionItemKind.Value)

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

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

return item
})
items.push(item)
}

return items
}
}
4 changes: 2 additions & 2 deletions src/providers/diagnostics/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DependencyInfo, Extractor, ValidNode } from '#types/extractor'
import type { ResolvedPackument } from '#utils/api/package'
import type { PackageInfo } from '#utils/api/package'
import type { Awaitable } from 'reactive-vscode'
import type { Diagnostic, TextDocument } from 'vscode'
import { basename } from 'node:path'
Expand All @@ -15,7 +15,7 @@ import { checkVulnerability } from './rules/vulnerability'
export interface NodeDiagnosticInfo extends Pick<Diagnostic, 'message' | 'severity' | 'code'> {
node: ValidNode
}
export type DiagnosticRule = (dep: DependencyInfo, pkg: ResolvedPackument) => Awaitable<NodeDiagnosticInfo | undefined>
export type DiagnosticRule = (dep: DependencyInfo, pkg: PackageInfo) => Awaitable<NodeDiagnosticInfo | undefined>

const enabledRules = computed<DiagnosticRule[]>(() => {
const rules: DiagnosticRule[] = []
Expand Down
2 changes: 1 addition & 1 deletion src/providers/diagnostics/rules/deprecation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DiagnosticSeverity, Uri } from 'vscode'

export const checkDeprecation: DiagnosticRule = (dep, pkg) => {
const exactVersion = extractVersion(dep.version)
const versionInfo = pkg.versions[exactVersion]
const versionInfo = pkg.versionsMeta[exactVersion]

if (!versionInfo?.deprecated)
return
Expand Down
8 changes: 6 additions & 2 deletions src/providers/diagnostics/rules/replacement.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ModuleReplacement } from '#utils/api/replacement'
import type { ModuleReplacement } from 'module-replacements'
import type { DiagnosticRule } from '..'
import { getReplacement } from '#utils/api/replacement'
import { DiagnosticSeverity, Uri } from 'vscode'
Expand All @@ -11,7 +11,11 @@ function getReplacementsDocUrl(path: string): string {
return `https://github.com/es-tooling/module-replacements/blob/main/docs/modules/${path}.md`
}

// https://github.com/npmx-dev/npmx.dev/blob/main/app/components/PackageReplacement.vue#L8-L30
/**
* Keep messages in sync with npmx.dev wording.
*
* https://github.com/npmx-dev/npmx.dev/blob/main/app/components/PackageReplacement.vue#L8-L30
*/
function getReplacementInfo(replacement: ModuleReplacement) {
switch (replacement.type) {
case 'native':
Expand Down
2 changes: 1 addition & 1 deletion src/providers/diagnostics/rules/vulnerability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const DIAGNOSTIC_MAPPING: Record<Exclude<OsvSeverityLevel, 'unknown'>, Diagnosti

export const checkVulnerability: DiagnosticRule = async (dep, pkg) => {
const exactVersion = extractVersion(dep.version)
const versionInfo = pkg.versions[exactVersion]
const versionInfo = pkg.versionsMeta[exactVersion]

if (!versionInfo)
return
Expand Down
11 changes: 6 additions & 5 deletions src/providers/hover/npmx.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Extractor } from '#types/extractor'
import type { HoverProvider, Position, TextDocument } from 'vscode'
import { getPackageInfo } from '#utils/api/package'
import { npmPacakgeUrl, npmxDocsUrl, npmxPackageUrl } from '#utils/links'
import { extractVersion } from '#utils/package'
import { Hover, MarkdownString } from 'vscode'

Expand Down Expand Up @@ -30,15 +31,15 @@ export class NpmxHoverProvider<T extends Extractor> implements HoverProvider {
if (!pkg)
return

const currentVersion = pkg.versions[coercedVersion]
const currentVersion = pkg.versionsMeta[coercedVersion]
if (currentVersion) {
if (currentVersion.hasProvenance)
md.appendMarkdown(`[$(verified) Verified provenance](https://www.npmjs.com/package/${name}/v/${currentVersion.version}#provenance)\n\n`)
if (currentVersion.provenance)
md.appendMarkdown(`[$(verified) Verified provenance](${npmPacakgeUrl(name, coercedVersion)}#provenance)\n\n`)
}

const footer = [
`**[View on npmx](https://npmx.dev/package/${name})**`,
`**[View docs on npmx](https://npmx.dev/docs/${name}/v/${coercedVersion})**`,
`[View on npmx](${npmxPackageUrl(name)})`,
`[View docs on npmx](${npmxDocsUrl(name, coercedVersion)})`,
]

md.appendMarkdown(`${footer.join(' | ')}\n`)
Expand Down
67 changes: 32 additions & 35 deletions src/utils/api/package.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,43 @@
import type { Packument, PackumentVersion } from '@npm/types'
import { NPM_REGISTRY } from '#constants'
import type { PackageVersionsInfoWithMetadata } from 'fast-npm-meta'
import { logger } from '#state'
import { encodePackageName } from '#utils/package'
import { ofetch } from 'ofetch'
import { getVersions } from 'fast-npm-meta'
import { memoize } from '../memoize'

interface ResolvedPackumentVersion extends Pick<PackumentVersion, 'version'> {
tag?: string
hasProvenance: boolean
deprecated?: string
export interface PackageInfo extends PackageVersionsInfoWithMetadata {
versionToTag: Map<string, string>
}

export interface ResolvedPackument {
versions: Record<string, ResolvedPackumentVersion>
}

export const getPackageInfo = memoize<string, Promise<ResolvedPackument>>(async (name) => {
/**
* Fetch npm package versions and build a version-to-tag lookup map.
*
* @see https://github.com/antfu/fast-npm-meta
*/
export const getPackageInfo = memoize<string, Promise<PackageInfo | null>>(async (name) => {
logger.info(`Fetching package info for ${name}`)
const encodedName = encodePackageName(name)

const pkg = await ofetch<Packument>(`${NPM_REGISTRY}/${encodedName}`)
logger.info(`Fetched package info for ${name}`)

const resolvedVersions = Object.fromEntries(
Object.keys(pkg.versions)
.filter((v) => pkg.time[v])
.map<[string, ResolvedPackumentVersion]>((v) => [
v,
{
version: v,
// @ts-expect-error present if published with provenance
hasProvenance: !!pkg.versions[v].dist.attestations,
deprecated: pkg.versions[v].deprecated,
},
]),
)

Object.entries(pkg['dist-tags']).forEach(([tag, version]) => {
resolvedVersions[version].tag = tag
const pkg = await getVersions(name, {
metadata: true,
throw: false,
})

return {
versions: resolvedVersions,
if ('error' in pkg) {
logger.warn(`Fetching package info for ${name} error: ${JSON.stringify(pkg)}`)

// Return null to trigger a cache hit
if (pkg.status === 404)
return null

throw pkg
}

logger.info(`Fetched package info for ${name}`)

const versionToTag = new Map<string, string>()
if (pkg.distTags) {
for (const [tag, ver] of Object.entries(pkg.distTags)) {
versionToTag.set(ver, tag)
}
}

return { ...pkg, versionToTag }
})
4 changes: 0 additions & 4 deletions src/utils/api/replacement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ import { ofetch } from 'ofetch'
import { memoize } from '../memoize'
import { encodePackageName } from '../package'

export type {
ModuleReplacement,
}

export const getReplacement = memoize<string, Promise<ModuleReplacement | null>>(async (name) => {
logger.info(`Fetching replacements for ${name}`)
const encodedName = encodePackageName(name)
Expand Down
Loading