Skip to content

Commit 0a662d1

Browse files
authored
feat: display package deprecation diagnostics (#7)
* feat: show deprecation diagnostics * update * log * #utils/version * refactor: more extendable * rename
1 parent f08ea8c commit 0a662d1

File tree

11 files changed

+140
-23
lines changed

11 files changed

+140
-23
lines changed

src/constants.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
export const PACKAGE_JSON_PATTERN = '**/package.json'
2-
export const PNPM_WORKSPACE_PATTERN = '**/pnpm-workspace.yaml'
1+
export const PACKAGE_JSON_BASENAME = 'package.json'
2+
export const PNPM_WORKSPACE_BASENAME = 'pnpm-workspace.yaml'
3+
4+
export const PACKAGE_JSON_PATTERN = `**/${PACKAGE_JSON_BASENAME}`
5+
export const PNPM_WORKSPACE_PATTERN = `**/${PNPM_WORKSPACE_BASENAME}`
36

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

src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
import { PACKAGE_JSON_PATTERN, PNPM_WORKSPACE_PATTERN, VERSION_TRIGGER_CHARACTERS } from '#constants'
1+
import {
2+
PACKAGE_JSON_BASENAME,
3+
PACKAGE_JSON_PATTERN,
4+
PNPM_WORKSPACE_BASENAME,
5+
PNPM_WORKSPACE_PATTERN,
6+
VERSION_TRIGGER_CHARACTERS,
7+
} from '#constants'
28
import { defineExtension } from 'reactive-vscode'
39
import { languages } from 'vscode'
410
import { JsonExtractor } from './extractors/json'
511
import { YamlExtractor } from './extractors/yaml'
612
import { displayName, version } from './generated-meta'
713
import { VersionCompletionItemProvider } from './providers/completion-item/version'
14+
import { registerDiagnosticCollection } from './providers/diagnostics'
815
import { NpmxDocumentLinkProvider } from './providers/document-link/npmx'
916
import { config, logger } from './state'
1017

@@ -39,4 +46,9 @@ export const { activate, deactivate } = defineExtension((ctx) => {
3946
),
4047
)
4148
}
49+
50+
registerDiagnosticCollection({
51+
[PACKAGE_JSON_BASENAME]: jsonExtractor,
52+
[PNPM_WORKSPACE_BASENAME]: yamlExtractor,
53+
})
4254
})

src/providers/completion-item/version.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,10 @@ import type { Extractor } from '#types/extractor'
22
import type { CompletionItemProvider, Position, TextDocument } from 'vscode'
33
import { config } from '#state'
44
import { getPackageInfo } from '#utils/npm'
5+
import { extractVersionPrefix } from '#utils/version'
56
import { CompletionItem, CompletionItemKind } from 'vscode'
67

7-
function isVersionPrefix(c: string) {
8-
return c === '^' || c === '~'
9-
}
10-
11-
function extractVersionPrefix(v: string) {
12-
const firstChar = v[0]
13-
const valid = isVersionPrefix(firstChar)
14-
15-
return valid ? firstChar : ''
16-
}
17-
18-
export class VersionCompletionItemProvider<T extends Extractor<any>> implements CompletionItemProvider {
8+
export class VersionCompletionItemProvider<T extends Extractor> implements CompletionItemProvider {
199
extractor: T
2010

2111
constructor(extractor: T) {

src/providers/diagnostics/index.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { DependencyInfo, Extractor, ValidNode } from '#types/extractor'
2+
import type { ResolvedPackument } from '#utils/npm'
3+
import type { Diagnostic, TextDocument } from 'vscode'
4+
import { basename } from 'node:path'
5+
import { logger } from '#state'
6+
import { getPackageInfo } from '#utils/npm'
7+
import { useActiveTextEditor, useDocumentText, watch } from 'reactive-vscode'
8+
import { languages } from 'vscode'
9+
import { displayName } from '../../generated-meta'
10+
import { checkDeprecations } from './rules/deprecation'
11+
12+
export interface NodeDiagnosticInfo extends Pick<Diagnostic, 'message' | 'severity'> {
13+
node: ValidNode
14+
}
15+
export type DiagnosticRule = (dep: DependencyInfo, pkg: ResolvedPackument) => NodeDiagnosticInfo | undefined
16+
17+
const rules: DiagnosticRule[] = [
18+
checkDeprecations,
19+
]
20+
21+
export function registerDiagnosticCollection(mapping: Record<string, Extractor | undefined>) {
22+
const diagnosticCollection = languages.createDiagnosticCollection(displayName)
23+
24+
const activeEditor = useActiveTextEditor()
25+
const activeDocumentText = useDocumentText(() => activeEditor.value?.document)
26+
27+
async function collectDiagnostics(document: TextDocument, extractor: Extractor) {
28+
const root = extractor.parse(document)
29+
if (!root)
30+
return
31+
32+
const dependencies = extractor.getDependenciesInfo(root)
33+
const diagnostics: Diagnostic[] = []
34+
35+
await Promise.all(
36+
dependencies.map(async (dep) => {
37+
try {
38+
const pkg = await getPackageInfo(dep.name)
39+
40+
for (const rule of rules) {
41+
const diagnostic = rule(dep, pkg)
42+
43+
if (diagnostic) {
44+
diagnostics.push({
45+
source: displayName,
46+
message: diagnostic.message,
47+
severity: diagnostic.severity,
48+
range: extractor.getNodeRange(document, diagnostic.node),
49+
})
50+
}
51+
}
52+
} catch (err) {
53+
logger.warn(`Failed to check ${dep.name}: ${err}`)
54+
}
55+
}),
56+
)
57+
58+
diagnosticCollection.set(document.uri, diagnostics)
59+
}
60+
61+
watch(activeDocumentText, async () => {
62+
const editor = activeEditor.value
63+
if (!editor)
64+
return
65+
66+
const document = editor.document
67+
const filename = basename(document.fileName)
68+
const extractor = mapping[filename]
69+
70+
if (extractor)
71+
await collectDiagnostics(document, extractor)
72+
}, { immediate: true })
73+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { DiagnosticRule } from '..'
2+
import { extractVersion } from '#utils/version'
3+
import { DiagnosticSeverity } from 'vscode'
4+
5+
export const checkDeprecations: DiagnosticRule = (dep, pkg) => {
6+
const exactVersion = extractVersion(dep.version)
7+
const versionInfo = pkg.versions[exactVersion]
8+
9+
if (!versionInfo?.deprecated)
10+
return
11+
12+
return {
13+
node: dep.versionNode,
14+
message: `${dep.name} v${exactVersion} has been deprecated: ${versionInfo.deprecated}`,
15+
severity: DiagnosticSeverity.Error,
16+
}
17+
}

src/providers/document-link/npmx.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Extractor } from '#types/extractor'
22
import type { DocumentLinkProvider, TextDocument } from 'vscode'
33
import { DocumentLink, Uri } from 'vscode'
44

5-
export class NpmxDocumentLinkProvider<T extends Extractor<any>> implements DocumentLinkProvider {
5+
export class NpmxDocumentLinkProvider<T extends Extractor> implements DocumentLinkProvider {
66
extractor: T
77

88
constructor(extractor: T) {

src/state.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { NestedScopedConfigs } from './generated-meta'
22
import { defineConfigObject, defineLogger } from 'reactive-vscode'
3-
import { scopedConfigs } from './generated-meta'
3+
import { displayName, scopedConfigs } from './generated-meta'
44

55
export const config = defineConfigObject<NestedScopedConfigs>(
66
scopedConfigs.scope,
77
scopedConfigs.defaults,
88
)
99

10-
export const logger = defineLogger('npmx')
10+
export const logger = defineLogger(displayName)

src/types/extractor.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
import type { Node as JsonNode } from 'jsonc-parser'
12
import type { Range, TextDocument } from 'vscode'
3+
import type { Node as YamlNode } from 'yaml'
24

3-
export interface DependencyInfo<T> {
5+
export type ValidNode = JsonNode | YamlNode
6+
7+
export interface DependencyInfo<T extends ValidNode = any> {
48
nameNode: T
59
versionNode: T
610
name: string
711
version: string
812
}
913

10-
export interface Extractor<T> {
14+
export interface Extractor<T extends ValidNode = any> {
1115
parse: (document: TextDocument) => T | null | undefined
1216

1317
getNodeRange: (document: TextDocument, node: T) => Range

src/utils/data.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Extractor } from '#types/extractor'
1+
import type { Extractor, ValidNode } from '#types/extractor'
22
import type { TextDocument } from 'vscode'
33
import { createHash } from 'node:crypto'
44
import { logger } from '#state'
@@ -17,7 +17,7 @@ function computeHash(text: string) {
1717
return createHash('sha1').update(text).digest('hex')
1818
}
1919

20-
export function createCachedParse<T>(
20+
export function createCachedParse<T extends ValidNode>(
2121
parse: (text: string) => ReturnType<Extractor<T>['parse']>,
2222
): Extractor<T>['parse'] {
2323
return function (doc: TextDocument) {

src/utils/npm.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import type { Packument, PackumentVersion } from '@npm/types'
22
import { NPM_REGISTRY } from '#constants'
3+
import { logger } from '#state'
34
import { LRUCache } from 'lru-cache'
45
import { ofetch } from 'ofetch'
56

67
interface ResolvedPackumentVersion extends Pick<PackumentVersion, 'version'> {
78
tag?: string
89
hasProvenance: boolean
10+
deprecated?: string
911
}
1012

11-
interface ResolvedPackument {
13+
export interface ResolvedPackument {
1214
versions: Record<string, ResolvedPackumentVersion>
1315
}
1416

@@ -31,6 +33,7 @@ const cache = new LRUCache<string, ResolvedPackument>({
3133
fetchMethod: async (name, staleValue, { signal }) => {
3234
const encodedName = encodePackageName(name)
3335

36+
logger.info(`fetching ${name}...`)
3437
const pkg = await ofetch<Packument>(`${NPM_REGISTRY}/${encodedName}`, { signal })
3538

3639
const resolvedVersions = Object.fromEntries(
@@ -42,6 +45,7 @@ const cache = new LRUCache<string, ResolvedPackument>({
4245
version: v,
4346
// @ts-expect-error present if published with provenance
4447
hasProvenance: !!pkg.versions[v].dist.attestations,
48+
deprecated: pkg.versions[v].deprecated,
4549
},
4650
]),
4751
)

0 commit comments

Comments
 (0)