forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import type { PackageVersionsInfo } from 'fast-npm-meta'
2import { getVersionsBatch } from 'fast-npm-meta'
3import { maxSatisfying, prerelease, major, minor, diff, gt } from 'semver'
4import {
5 type OutdatedDependencyInfo,
6 isNonSemverConstraint,
7 constraintIncludesPrerelease,
8} from '~/utils/npm/outdated-dependencies'
9
10const BATCH_SIZE = 50
11
12function resolveOutdated(
13 versions: string[],
14 latestTag: string,
15 constraint: string,
16): OutdatedDependencyInfo | null {
17 if (constraint === 'latest') {
18 return {
19 resolved: latestTag,
20 latest: latestTag,
21 majorsBehind: 0,
22 minorsBehind: 0,
23 diffType: null,
24 }
25 }
26
27 let filteredVersions = versions
28 if (!constraintIncludesPrerelease(constraint)) {
29 filteredVersions = versions.filter(v => !prerelease(v))
30 }
31
32 const resolved = maxSatisfying(filteredVersions, constraint)
33 if (!resolved) return null
34
35 if (resolved === latestTag) return null
36
37 // Resolved is newer than latest (e.g. ^2.0.0-rc when latest is 1.x)
38 if (gt(resolved, latestTag)) {
39 return null
40 }
41
42 const diffType = diff(resolved, latestTag)
43 const majorsBehind = major(latestTag) - major(resolved)
44 const minorsBehind = majorsBehind === 0 ? minor(latestTag) - minor(resolved) : 0
45
46 return {
47 resolved,
48 latest: latestTag,
49 majorsBehind,
50 minorsBehind,
51 diffType,
52 }
53}
54
55/**
56 * Check for outdated dependencies via fast-npm-meta batch version lookups.
57 * Returns a reactive map of dependency name to outdated info.
58 */
59export function useOutdatedDependencies(
60 dependencies: MaybeRefOrGetter<Record<string, string> | undefined>,
61) {
62 const outdated = shallowRef<Record<string, OutdatedDependencyInfo>>({})
63
64 async function fetchOutdatedInfo(deps: Record<string, string> | undefined) {
65 if (!deps || Object.keys(deps).length === 0) {
66 outdated.value = {}
67 return
68 }
69
70 const semverEntries = Object.entries(deps).filter(
71 ([, constraint]) => !isNonSemverConstraint(constraint),
72 )
73
74 if (semverEntries.length === 0) {
75 outdated.value = {}
76 return
77 }
78
79 const packageNames = semverEntries.map(([name]) => name)
80
81 const chunks: string[][] = []
82 for (let i = 0; i < packageNames.length; i += BATCH_SIZE) {
83 chunks.push(packageNames.slice(i, i + BATCH_SIZE))
84 }
85 const batchResults = await Promise.all(
86 chunks.map(chunk => getVersionsBatch(chunk, { throw: false })),
87 )
88 const allVersionData = batchResults.flat()
89
90 // Build a lookup map from package name to version data
91 const versionMap = new Map<string, PackageVersionsInfo>()
92 for (const data of allVersionData) {
93 if ('error' in data) continue
94 versionMap.set(data.name, data)
95 }
96
97 const results: Record<string, OutdatedDependencyInfo> = {}
98 for (const [name, constraint] of semverEntries) {
99 const data = versionMap.get(name)
100 if (!data) continue
101
102 const latestTag = data.distTags.latest
103 if (!latestTag) continue
104
105 const info = resolveOutdated(data.versions, latestTag, constraint)
106 if (info) {
107 results[name] = info
108 }
109 }
110
111 outdated.value = results
112 }
113
114 watch(
115 () => toValue(dependencies),
116 deps => {
117 fetchOutdatedInfo(deps).catch(() => {
118 // Network failure or fast-npm-meta outage — leave stale results in place
119 })
120 },
121 { immediate: true },
122 )
123
124 return outdated
125}