Skip to content

Commit 977d8d0

Browse files
authored
feat: integrate module-replacements, display replacement info (#11)
* feat: use `module-replacements` * done
1 parent 485bf92 commit 977d8d0

File tree

7 files changed

+74
-4
lines changed

7 files changed

+74
-4
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"husky": "^9.1.7",
8383
"jsonc-parser": "^3.3.1",
8484
"lru-cache": "^11.2.5",
85+
"module-replacements": "^2.11.0",
8586
"nano-staged": "^0.9.0",
8687
"ofetch": "^2.0.0-alpha.3",
8788
"reactive-vscode": "^0.4.1",

pnpm-lock.yaml

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

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export const PNPM_WORKSPACE_PATTERN = `**/${PNPM_WORKSPACE_BASENAME}`
77
export const VERSION_TRIGGER_CHARACTERS = ['.', '^', '~', ...Array.from({ length: 10 }).map((_, i) => `${i}`)]
88

99
export const NPM_REGISTRY = 'https://registry.npmjs.org'
10+
export const NPMX_DEV_API = 'https://npmx.dev/api'

src/providers/diagnostics/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { DependencyInfo, Extractor, ValidNode } from '#types/extractor'
22
import type { ResolvedPackument } from '#utils/npm'
3+
import type { Awaitable } from 'reactive-vscode'
34
import type { Diagnostic, TextDocument } from 'vscode'
45
import { basename } from 'node:path'
56
import { logger } from '#state'
@@ -8,14 +9,16 @@ import { useActiveTextEditor, useDocumentText, watch } from 'reactive-vscode'
89
import { languages } from 'vscode'
910
import { displayName } from '../../generated-meta'
1011
import { checkDeprecation } from './rules/deprecation'
12+
import { checkReplacement } from './rules/replacement'
1113

1214
export interface NodeDiagnosticInfo extends Pick<Diagnostic, 'message' | 'severity'> {
1315
node: ValidNode
1416
}
15-
export type DiagnosticRule = (dep: DependencyInfo, pkg: ResolvedPackument) => NodeDiagnosticInfo | undefined
17+
export type DiagnosticRule = (dep: DependencyInfo, pkg: ResolvedPackument) => Awaitable<NodeDiagnosticInfo | undefined>
1618

1719
const rules: DiagnosticRule[] = [
1820
checkDeprecation,
21+
checkReplacement,
1922
]
2023

2124
export function registerDiagnosticCollection(mapping: Record<string, Extractor | undefined>) {
@@ -38,7 +41,7 @@ export function registerDiagnosticCollection(mapping: Record<string, Extractor |
3841
const pkg = await getPackageInfo(dep.name)
3942

4043
for (const rule of rules) {
41-
const diagnostic = rule(dep, pkg)
44+
const diagnostic = await rule(dep, pkg)
4245

4346
if (diagnostic) {
4447
diagnostics.push({
@@ -55,6 +58,8 @@ export function registerDiagnosticCollection(mapping: Record<string, Extractor |
5558
}),
5659
)
5760

61+
if (diagnostics.length > 0)
62+
logger.info(`${diagnostics.length} diagnostic collected in ${document.fileName}.`)
5863
diagnosticCollection.set(document.uri, diagnostics)
5964
}
6065

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { ModuleReplacement } from 'module-replacements'
2+
import type { DiagnosticRule } from '..'
3+
import { getReplacement } from '#utils/replacement'
4+
import { DiagnosticSeverity } from 'vscode'
5+
6+
// https://github.com/npmx-dev/npmx.dev/blob/main/app/components/PackageReplacement.vue#L8-L30
7+
function generateMessage(replacement: ModuleReplacement) {
8+
switch (replacement.type) {
9+
case 'native':
10+
return `This can be replaced with ${replacement.replacement}, available since Node ${replacement.nodeVersion}.`
11+
case 'simple':
12+
return `The community has flagged this package as redundant, with the advice: ${replacement.replacement}.`
13+
case 'documented':
14+
return 'The community has flagged this package as having more performant alternatives.'
15+
case 'none':
16+
return 'This package has been flagged as no longer needed, and its functionality is likely available natively in all engines.'
17+
}
18+
}
19+
20+
export const checkReplacement: DiagnosticRule = async (dep) => {
21+
const replacement = await getReplacement(dep.name)
22+
// Fallback for cache compatibility (LRUCache rejects null/undefined)
23+
if (!replacement || !('type' in replacement))
24+
return
25+
26+
return {
27+
node: dep.nameNode,
28+
message: generateMessage(replacement),
29+
severity: DiagnosticSeverity.Warning,
30+
}
31+
}

src/utils/npm.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface ResolvedPackument {
1818
* Encode a package name for use in npm registry URLs.
1919
* Handles scoped packages (e.g., @scope/name -> @scope%2Fname).
2020
*/
21-
function encodePackageName(name: string): string {
21+
export function encodePackageName(name: string): string {
2222
if (name.startsWith('@')) {
2323
return `@${encodeURIComponent(name.slice(1))}`
2424
}
@@ -33,7 +33,7 @@ const cache = new LRUCache<string, ResolvedPackument>({
3333
fetchMethod: async (name, staleValue, { signal }) => {
3434
const encodedName = encodePackageName(name)
3535

36-
logger.info(`fetching ${name}...`)
36+
logger.info(`fetching package info for ${name}...`)
3737
const pkg = await ofetch<Packument>(`${NPM_REGISTRY}/${encodedName}`, { signal })
3838

3939
const resolvedVersions = Object.fromEntries(

src/utils/replacement.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { ModuleReplacement } from 'module-replacements'
2+
import { NPMX_DEV_API } from '#constants'
3+
import { logger } from '#state'
4+
import { LRUCache } from 'lru-cache'
5+
import { ofetch } from 'ofetch'
6+
import { encodePackageName } from './npm'
7+
8+
const cache = new LRUCache<string, ModuleReplacement>({
9+
max: 500,
10+
ttl: 60 * 60 * 1000,
11+
updateAgeOnGet: true,
12+
allowStale: true,
13+
fetchMethod: async (name, staleValue, { signal }) => {
14+
const encodedName = encodePackageName(name)
15+
16+
logger.info(`fetching replacement for ${name}...`)
17+
try {
18+
return await ofetch<ModuleReplacement>(`${NPMX_DEV_API}/replacements/${encodedName}`, { signal })
19+
// Fallback for cache compatibility (LRUCache rejects null/undefined)
20+
?? {}
21+
} catch (err) {
22+
logger.warn('fetching replacement error: ', err)
23+
}
24+
},
25+
})
26+
27+
export async function getReplacement(name: string) {
28+
return (await cache.fetch(name))!
29+
}

0 commit comments

Comments
 (0)