Skip to content

Commit 2eb3f54

Browse files
committed
perf: use fast-npm-meta to check outdated deps
1 parent 1d9fb64 commit 2eb3f54

File tree

1 file changed

+46
-58
lines changed

1 file changed

+46
-58
lines changed

app/composables/npm/useOutdatedDependencies.ts

Lines changed: 46 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,19 @@
1-
import type { NuxtApp } from '#app'
1+
import type { PackageVersionsInfo } from 'fast-npm-meta'
2+
import { getVersionsBatch } from 'fast-npm-meta'
23
import { maxSatisfying, prerelease, major, minor, diff, gt } from 'semver'
3-
import type { Packument } from '#shared/types'
4-
import { mapWithConcurrency } from '#shared/utils/async'
5-
import type { CachedFetchFunction } from '#shared/utils/fetch-cache-config'
64
import {
75
type OutdatedDependencyInfo,
86
isNonSemverConstraint,
97
constraintIncludesPrerelease,
108
} from '~/utils/npm/outdated-dependencies'
119

12-
// Cache for packument fetches to avoid duplicate requests across components
13-
const packumentCache = new Map<string, Promise<Packument | null>>()
10+
const BATCH_SIZE = 50
1411

15-
/**
16-
* Check if a dependency is outdated.
17-
* Returns null if up-to-date or if we can't determine.
18-
*/
19-
async function checkDependencyOutdated(
20-
cachedFetch: CachedFetchFunction,
21-
$npmRegistry: NuxtApp['$npmRegistry'],
22-
packageName: string,
12+
function resolveOutdated(
13+
versions: string[],
14+
latestTag: string,
2315
constraint: string,
24-
): Promise<OutdatedDependencyInfo | null> {
25-
if (isNonSemverConstraint(constraint)) {
26-
return null
27-
}
28-
29-
// Check in-memory cache first
30-
let packument: Packument | null
31-
const cached = packumentCache.get(packageName)
32-
if (cached) {
33-
packument = await cached
34-
} else {
35-
const promise = $npmRegistry<Packument>(`/${encodePackageName(packageName)}`)
36-
.then(({ data }) => data)
37-
.catch(() => null)
38-
packumentCache.set(packageName, promise)
39-
packument = await promise
40-
}
41-
42-
if (!packument) return null
43-
44-
const latestTag = packument['dist-tags']?.latest
45-
if (!latestTag) return null
46-
47-
// Handle "latest" constraint specially - return info with current version
16+
): OutdatedDependencyInfo | null {
4817
if (constraint === 'latest') {
4918
return {
5019
resolved: latestTag,
@@ -55,20 +24,17 @@ async function checkDependencyOutdated(
5524
}
5625
}
5726

58-
let versions = Object.keys(packument.versions)
59-
const includesPrerelease = constraintIncludesPrerelease(constraint)
60-
61-
if (!includesPrerelease) {
62-
versions = versions.filter(v => !prerelease(v))
27+
let filteredVersions = versions
28+
if (!constraintIncludesPrerelease(constraint)) {
29+
filteredVersions = versions.filter(v => !prerelease(v))
6330
}
6431

65-
const resolved = maxSatisfying(versions, constraint)
32+
const resolved = maxSatisfying(filteredVersions, constraint)
6633
if (!resolved) return null
6734

6835
if (resolved === latestTag) return null
6936

70-
// If resolved version is newer than latest, not outdated
71-
// (e.g., using ^2.0.0-rc when latest is 1.x)
37+
// Resolved is newer than latest (e.g. ^2.0.0-rc when latest is 1.x)
7238
if (gt(resolved, latestTag)) {
7339
return null
7440
}
@@ -87,14 +53,12 @@ async function checkDependencyOutdated(
8753
}
8854

8955
/**
90-
* Composable to check for outdated dependencies.
56+
* Check for outdated dependencies via fast-npm-meta batch version lookups.
9157
* Returns a reactive map of dependency name to outdated info.
9258
*/
9359
export function useOutdatedDependencies(
9460
dependencies: MaybeRefOrGetter<Record<string, string> | undefined>,
9561
) {
96-
const { $npmRegistry } = useNuxtApp()
97-
const cachedFetch = useCachedFetch()
9862
const outdated = shallowRef<Record<string, OutdatedDependencyInfo>>({})
9963

10064
async function fetchOutdatedInfo(deps: Record<string, string> | undefined) {
@@ -103,18 +67,42 @@ export function useOutdatedDependencies(
10367
return
10468
}
10569

106-
const entries = Object.entries(deps)
107-
const batchResults = await mapWithConcurrency(
108-
entries,
109-
async ([name, constraint]) => {
110-
const info = await checkDependencyOutdated(cachedFetch, $npmRegistry, name, constraint)
111-
return [name, info] as const
112-
},
113-
5,
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 })),
11487
)
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+
}
11596

11697
const results: Record<string, OutdatedDependencyInfo> = {}
117-
for (const [name, info] of batchResults) {
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)
118106
if (info) {
119107
results[name] = info
120108
}

0 commit comments

Comments
 (0)