Skip to content

Commit 663203b

Browse files
authored
feat: version upgrade diagnostics and quick fix (#36)
* wip * wip * wip: simplify upgrade * fix: correct target string * feat: add upgrade diagnostic for prerelease and outdated versions * refactor: remove `semver`, use custom version parse, compare * test: add basic test cases * chore: revert playground * refactor: reduce type assertion * chore: improve readability of pre-release comparison logic * chore: remove unused inlineOnly
1 parent 7b9d7fd commit 663203b

16 files changed

Lines changed: 343 additions & 145 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
| `npmx.hover.enabled` | Enable hover information for packages | `boolean` | `true` |
3838
| `npmx.completion.version` | Version completion behavior | `string` | `"provenance-only"` |
3939
| `npmx.completion.excludePrerelease` | Exclude prerelease versions (alpha, beta, rc, canary, etc.) from completion suggestions | `boolean` | `true` |
40+
| `npmx.diagnostics.upgrade` | Show hints when a newer version of a package is available | `boolean` | `true` |
4041
| `npmx.diagnostics.deprecation` | Show warnings for deprecated packages | `boolean` | `true` |
4142
| `npmx.diagnostics.replacement` | Show suggestions for package replacements | `boolean` | `true` |
4243
| `npmx.diagnostics.vulnerability` | Show warnings for packages with known vulnerabilities | `boolean` | `true` |

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@
6969
"default": true,
7070
"description": "Exclude prerelease versions (alpha, beta, rc, canary, etc.) from completion suggestions"
7171
},
72+
"npmx.diagnostics.upgrade": {
73+
"type": "boolean",
74+
"default": true,
75+
"description": "Show hints when a newer version of a package is available"
76+
},
7277
"npmx.diagnostics.deprecation": {
7378
"type": "boolean",
7479
"default": true,

pnpm-lock.yaml

Lines changed: 18 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ export const NPMX_DEV = 'https://npmx.dev'
1313
export const NPMX_DEV_API = `${NPMX_DEV}/api`
1414

1515
export const SPACER = ' '
16+
17+
export const UPGRADE_MESSAGE_PREFIX = 'New version available: '

src/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import {
66
VERSION_TRIGGER_CHARACTERS,
77
} from '#constants'
88
import { defineExtension, useCommands, watchEffect } from 'reactive-vscode'
9-
import { Disposable, languages } from 'vscode'
9+
import { CodeActionKind, Disposable, languages } from 'vscode'
1010
import { openFileInNpmx } from './commands/open-file-in-npmx'
1111
import { openInBrowser } from './commands/open-in-browser'
1212
import { PackageJsonExtractor } from './extractors/package-json'
1313
import { PnpmWorkspaceYamlExtractor } from './extractors/pnpm-workspace-yaml'
1414
import { commands, displayName, version } from './generated-meta'
15+
import { UpgradeProvider } from './providers/code-actions/upgrade'
1516
import { VersionCompletionItemProvider } from './providers/completion-item/version'
1617
import { registerDiagnosticCollection } from './providers/diagnostics'
1718
import { NpmxHoverProvider } from './providers/hover/npmx'
@@ -61,6 +62,20 @@ export const { activate, deactivate } = defineExtension(() => {
6162
onCleanup(() => Disposable.from(...disposables).dispose())
6263
})
6364

65+
watchEffect((onCleanup) => {
66+
if (!config.diagnostics.upgrade)
67+
return
68+
69+
const provider = new UpgradeProvider()
70+
const options = { providedCodeActionKinds: [CodeActionKind.QuickFix] }
71+
const disposable = Disposable.from(
72+
languages.registerCodeActionsProvider({ pattern: PACKAGE_JSON_PATTERN }, provider, options),
73+
languages.registerCodeActionsProvider({ pattern: PNPM_WORKSPACE_PATTERN }, provider, options),
74+
)
75+
76+
onCleanup(() => disposable.dispose())
77+
})
78+
6479
registerDiagnosticCollection({
6580
[PACKAGE_JSON_BASENAME]: packageJsonExtractor,
6681
[PNPM_WORKSPACE_BASENAME]: pnpmWorkspaceYamlExtractor,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { CodeActionContext, CodeActionProvider, Command, ProviderResult, Range, Selection, TextDocument } from 'vscode'
2+
import { UPGRADE_MESSAGE_PREFIX } from '#constants'
3+
import { CodeAction, CodeActionKind, WorkspaceEdit } from 'vscode'
4+
5+
export class UpgradeProvider implements CodeActionProvider {
6+
provideCodeActions(document: TextDocument, _range: Range | Selection, context: CodeActionContext): ProviderResult<(CodeAction | Command)[]> {
7+
return context.diagnostics.flatMap((d) => {
8+
if (!d.message.startsWith(UPGRADE_MESSAGE_PREFIX))
9+
return []
10+
11+
const target = d.message.slice(UPGRADE_MESSAGE_PREFIX.length)
12+
const fix = new CodeAction(`Update to ${target}`, CodeActionKind.QuickFix)
13+
fix.edit = new WorkspaceEdit()
14+
fix.edit.replace(document.uri, d.range, `${target}`)
15+
fix.diagnostics = [d]
16+
return [fix]
17+
})
18+
}
19+
}

src/providers/completion-item/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { CompletionItemProvider, Position, TextDocument } from 'vscode'
33
import { PRERELEASE_PATTERN } from '#constants'
44
import { config } from '#state'
55
import { getPackageInfo } from '#utils/api/package'
6-
import { formatVersion, isSupportedProtocol, parseVersion } from '#utils/package'
6+
import { formatVersion, isSupportedProtocol, parseVersion } from '#utils/version'
77
import { CompletionItem, CompletionItemKind } from 'vscode'
88

99
export class VersionCompletionItemProvider<T extends Extractor> implements CompletionItemProvider {

src/providers/diagnostics/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { languages } from 'vscode'
1111
import { displayName } from '../../generated-meta'
1212
import { checkDeprecation } from './rules/deprecation'
1313
import { checkReplacement } from './rules/replacement'
14+
import { checkUpgrade } from './rules/upgrade'
1415
import { checkVulnerability } from './rules/vulnerability'
1516

1617
export interface NodeDiagnosticInfo extends Omit<Diagnostic, 'range' | 'source'> {
@@ -20,6 +21,8 @@ export type DiagnosticRule = (dep: DependencyInfo, pkg: PackageInfo) => Awaitabl
2021

2122
const enabledRules = computed<DiagnosticRule[]>(() => {
2223
const rules: DiagnosticRule[] = []
24+
if (config.diagnostics.upgrade)
25+
rules.push(checkUpgrade)
2326
if (config.diagnostics.deprecation)
2427
rules.push(checkDeprecation)
2528
if (config.diagnostics.replacement)

src/providers/diagnostics/rules/deprecation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { DiagnosticRule } from '..'
22
import { npmxPackageUrl } from '#utils/links'
3-
import { isSupportedProtocol, parseVersion } from '#utils/package'
3+
import { isSupportedProtocol, parseVersion } from '#utils/version'
44
import { DiagnosticSeverity, DiagnosticTag, Uri } from 'vscode'
55

66
export const checkDeprecation: DiagnosticRule = (dep, pkg) => {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { DependencyInfo } from '#types/extractor'
2+
import type { ParsedVersion } from '#utils/version'
3+
import type { DiagnosticRule, NodeDiagnosticInfo } from '..'
4+
import { UPGRADE_MESSAGE_PREFIX } from '#constants'
5+
import { formatVersion, getPrereleaseId, isSupportedProtocol, lt, parseVersion } from '#utils/version'
6+
import { DiagnosticSeverity } from 'vscode'
7+
8+
function createUpgradeDiagnostic(dep: DependencyInfo, parsed: ParsedVersion, upgradeVersion: string): NodeDiagnosticInfo {
9+
const target = formatVersion({ ...parsed, semver: upgradeVersion })
10+
return {
11+
node: dep.versionNode,
12+
severity: DiagnosticSeverity.Hint,
13+
message: `${UPGRADE_MESSAGE_PREFIX}${target}`,
14+
}
15+
}
16+
17+
export const checkUpgrade: DiagnosticRule = (dep, pkg) => {
18+
const parsed = parseVersion(dep.version)
19+
if (!parsed || !isSupportedProtocol(parsed.protocol))
20+
return
21+
22+
const { semver } = parsed
23+
const latest = pkg.distTags.latest
24+
25+
if (latest && lt(semver, latest))
26+
return createUpgradeDiagnostic(dep, parsed, latest)
27+
28+
const currentPreId = getPrereleaseId(semver)
29+
if (!currentPreId)
30+
return
31+
32+
for (const [tag, tagVersion] of Object.entries(pkg.distTags)) {
33+
if (tag === 'latest')
34+
continue
35+
if (getPrereleaseId(tagVersion) !== currentPreId)
36+
continue
37+
if (!lt(semver, tagVersion))
38+
continue
39+
40+
return createUpgradeDiagnostic(dep, parsed, tagVersion)
41+
}
42+
}

0 commit comments

Comments
 (0)