diff --git a/README.md b/README.md index 2740c30..7288844 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ | `npmx.diagnostics.deprecation` | Show warnings for deprecated packages | `boolean` | `true` | | `npmx.diagnostics.replacement` | Show suggestions for package replacements | `boolean` | `true` | | `npmx.diagnostics.vulnerability` | Show warnings for packages with known vulnerabilities | `boolean` | `true` | +| `npmx.diagnostics.distTag` | Show warnings when a dependency uses a dist tag | `boolean` | `true` | diff --git a/package.json b/package.json index a87a764..c9d1936 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,11 @@ "type": "boolean", "default": true, "description": "Show warnings for packages with known vulnerabilities" + }, + "npmx.diagnostics.distTag": { + "type": "boolean", + "default": true, + "description": "Show warnings when a dependency uses a dist tag" } } }, diff --git a/playground/package.json b/playground/package.json index 9d95ff8..cc96b84 100644 --- a/playground/package.json +++ b/playground/package.json @@ -5,7 +5,7 @@ "nuxt": "npm:4.3.0" }, "devDependencies": { - "array-includes": "", + "array-includes": "latest", "axios": "", "is-number": "", "lodash": "catalog:" diff --git a/src/providers/diagnostics/index.ts b/src/providers/diagnostics/index.ts index d7a27c2..486ca2d 100644 --- a/src/providers/diagnostics/index.ts +++ b/src/providers/diagnostics/index.ts @@ -10,6 +10,7 @@ import { computed, useActiveTextEditor, useDisposable, useDocumentText, watch } import { languages } from 'vscode' import { displayName } from '../../generated-meta' import { checkDeprecation } from './rules/deprecation' +import { checkDistTag } from './rules/dist-tag' import { checkReplacement } from './rules/replacement' import { checkUpgrade } from './rules/upgrade' import { checkVulnerability } from './rules/vulnerability' @@ -32,6 +33,8 @@ export function useDiagnostics() { rules.push(checkUpgrade) if (config.diagnostics.deprecation) rules.push(checkDeprecation) + if (config.diagnostics.distTag) + rules.push(checkDistTag) if (config.diagnostics.replacement) rules.push(checkReplacement) if (config.diagnostics.vulnerability) diff --git a/src/providers/diagnostics/rules/dist-tag.ts b/src/providers/diagnostics/rules/dist-tag.ts new file mode 100644 index 0000000..e374066 --- /dev/null +++ b/src/providers/diagnostics/rules/dist-tag.ts @@ -0,0 +1,24 @@ +import type { DiagnosticRule } from '..' +import { npmxPackageUrl } from '#utils/links' +import { isSupportedProtocol, parseVersion } from '#utils/version' +import { DiagnosticSeverity, Uri } from 'vscode' + +export const checkDistTag: DiagnosticRule = (dep, pkg) => { + const parsed = parseVersion(dep.version) + if (!parsed || !isSupportedProtocol(parsed.protocol)) + return + + const tag = parsed.semver + if (!(tag in pkg.distTags)) + return + + 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.`, + severity: DiagnosticSeverity.Warning, + code: { + value: 'dist-tag', + target: Uri.parse(npmxPackageUrl(dep.name)), + }, + } +} diff --git a/tests/diagnostics/dist-tag.test.ts b/tests/diagnostics/dist-tag.test.ts new file mode 100644 index 0000000..551df96 --- /dev/null +++ b/tests/diagnostics/dist-tag.test.ts @@ -0,0 +1,35 @@ +import type { DependencyInfo } from '#types/extractor' +import type { PackageInfo } from '#utils/api/package' +import { describe, expect, it } from 'vitest' +import { checkDistTag } from '../../src/providers/diagnostics/rules/dist-tag' + +function createDependency(name: string, version: string): DependencyInfo { + return { + name, + version, + nameNode: {}, + versionNode: {}, + } +} + +function createPackageInfo(distTags: Record): PackageInfo { + return { distTags } as PackageInfo +} + +describe('checkDistTag', () => { + const packageInfo = createPackageInfo({ latest: '2.0.0' }) + + it('should flag when version matches a dist tag in metadata', async () => { + const dependency = createDependency('lodash', 'latest') + const result = await checkDistTag(dependency, packageInfo) + + expect(result).toBeDefined() + }) + + it('should not flag when version does not match any dist tag in metadata', async () => { + const dependency = createDependency('lodash', 'next') + const result = await checkDistTag(dependency, packageInfo) + + expect(result).toBeUndefined() + }) +})