Skip to content

Commit 6ee85b2

Browse files
committed
fix: use fast-npm-meta to get latest versions
1 parent 6c62d94 commit 6ee85b2

7 files changed

Lines changed: 109 additions & 18 deletions

File tree

app/pages/docs/[...path].vue

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,11 @@ const latestVersion = computed(() => pkg.value?.['dist-tags']?.latest ?? null)
4141
4242
if (import.meta.server && !requestedVersion.value) {
4343
const app = useNuxtApp()
44-
const { data: pkg } = await usePackage(packageName)
45-
const latest = pkg.value?.['dist-tags']?.latest
46-
if (latest) {
44+
const version = await fetchLatestVersion(packageName.value)
45+
if (version) {
4746
setResponseHeader(useRequestEvent()!, 'Cache-Control', 'no-cache')
4847
app.runWithContext(() =>
49-
navigateTo('/docs/' + packageName.value + '/v/' + latest, { redirectCode: 302 }),
48+
navigateTo('/docs/' + packageName.value + '/v/' + version, { redirectCode: 302 }),
5049
)
5150
}
5251
}

server/api/registry/badge/[...pkg].get.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as v from 'valibot'
22
import { createError, getRouterParam, setHeader } from 'h3'
33
import { PackageRouteParamsSchema } from '#shared/schemas/package'
44
import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants'
5-
import { fetchNpmPackage } from '#server/utils/npm'
5+
import { fetchLatestVersion } from '#server/utils/npm'
66
import { assertValidPackageName } from '#shared/utils/npm'
77
import { handleApiError } from '#server/utils/error-handler'
88

@@ -30,8 +30,7 @@ export default defineCachedEventHandler(
3030

3131
const label = `./ ${packageName}`
3232

33-
const value =
34-
requestedVersion ?? (await fetchNpmPackage(packageName))['dist-tags']?.latest ?? 'unknown'
33+
const value = requestedVersion ?? (await fetchLatestVersion(packageName)) ?? 'unknown'
3534

3635
const leftWidth = measureTextWidth(label)
3736
const rightWidth = measureTextWidth(value)

server/api/registry/install-size/[...pkg].get.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,17 @@ export default defineCachedEventHandler(
2222
version: rawVersion,
2323
})
2424

25-
// If no version specified, resolve to latest
26-
let version = requestedVersion
25+
// If no version specified, resolve to latest using fast-npm-meta (lightweight)
26+
let version: string | undefined = requestedVersion
2727
if (!version) {
28-
const packument = await fetchNpmPackage(packageName)
29-
version = packument['dist-tags']?.latest
30-
if (!version) {
28+
const latestVersion = await fetchLatestVersion(packageName)
29+
if (!latestVersion) {
3130
throw createError({
3231
statusCode: 404,
3332
message: 'No latest version found',
3433
})
3534
}
35+
version = latestVersion
3636
}
3737

3838
return await calculateInstallSize(packageName, version)

server/api/registry/vulnerabilities/[...pkg].get.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,17 @@ export default defineCachedEventHandler(
1919
version: rawVersion,
2020
})
2121

22-
// If no version specified, resolve to latest
23-
let version = requestedVersion
22+
// If no version specified, resolve to latest using fast-npm-meta (lightweight)
23+
let version: string | undefined = requestedVersion
2424
if (!version) {
25-
const packument = await fetchNpmPackage(packageName)
26-
version = packument['dist-tags']?.latest
27-
if (!version) {
25+
const latestVersion = await fetchLatestVersion(packageName)
26+
if (!latestVersion) {
2827
throw createError({
2928
statusCode: 404,
3029
message: 'No latest version found',
3130
})
3231
}
32+
version = latestVersion
3333
}
3434

3535
return await analyzeDependencyTree(packageName, version)

server/utils/npm.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import type { Packument } from '#shared/types'
1+
import type { FastNpmMetaResponse, Packument } from '#shared/types'
22
import { maxSatisfying, prerelease } from 'semver'
33

44
const NPM_REGISTRY = 'https://registry.npmjs.org'
5+
const FAST_NPM_META_API = 'https://npm.antfu.dev'
56

67
function encodePackageName(name: string): string {
78
if (name.startsWith('@')) {
@@ -23,6 +24,53 @@ export const fetchNpmPackage = defineCachedFunction(
2324
},
2425
)
2526

27+
/**
28+
* Fetch lightweight package metadata from fast-npm-meta API.
29+
* Much smaller payload than full packument - ideal for just getting latest version.
30+
*
31+
* @param name Package name
32+
* @param specifier Optional version specifier (tag like "alpha", or range like "^2.1.0")
33+
* @returns Resolved version info
34+
* @see https://github.com/antfu/fast-npm-meta
35+
*/
36+
export const fetchFastNpmMeta = defineCachedFunction(
37+
async (name: string, specifier?: string): Promise<FastNpmMetaResponse> => {
38+
const encodedName = encodePackageName(name)
39+
const url = specifier
40+
? `${FAST_NPM_META_API}/${encodedName}@${encodeURIComponent(specifier)}`
41+
: `${FAST_NPM_META_API}/${encodedName}`
42+
return await $fetch<FastNpmMetaResponse>(url)
43+
},
44+
{
45+
maxAge: 60 * 5,
46+
swr: true,
47+
name: 'fast-npm-meta',
48+
getKey: (name: string, specifier?: string) => (specifier ? `${name}@${specifier}` : name),
49+
},
50+
)
51+
52+
/**
53+
* Get the latest version of a package using fast-npm-meta API.
54+
* Falls back to full packument if fast-npm-meta fails.
55+
*
56+
* @param name Package name
57+
* @returns Latest version string or null if not found
58+
*/
59+
export async function fetchLatestVersion(name: string): Promise<string | null> {
60+
try {
61+
const meta = await fetchFastNpmMeta(name)
62+
return meta.version
63+
} catch {
64+
// Fallback to full packument
65+
try {
66+
const packument = await fetchNpmPackage(name)
67+
return packument['dist-tags']?.latest ?? null
68+
} catch {
69+
return null
70+
}
71+
}
72+
}
73+
2674
/**
2775
* Check if a version constraint explicitly includes a prerelease tag.
2876
* e.g., "^1.0.0-alpha" or ">=2.0.0-beta.1" include prereleases

shared/types/fast-npm-meta.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Types for fast-npm-meta API responses
3+
* @see https://github.com/antfu/fast-npm-meta
4+
*/
5+
6+
/**
7+
* Response from GET /:pkg endpoint
8+
* Returns resolved version info for a single package
9+
*/
10+
export interface FastNpmMetaResponse {
11+
/** Package name */
12+
name: string
13+
/** The specifier used (e.g., "latest", "^2.1.0", "alpha") */
14+
specifier: string
15+
/** Resolved version */
16+
version: string
17+
/** When this version was published (ISO 8601) */
18+
publishedAt: string
19+
/** When the cache was last synced (timestamp) */
20+
lastSynced: number
21+
/** Error message if resolution failed (when throw=false) */
22+
error?: string
23+
}
24+
25+
/**
26+
* Response from GET /versions/:pkg endpoint
27+
* Returns all versions and dist-tags for a package
28+
*/
29+
export interface FastNpmMetaVersionsResponse {
30+
/** Package name */
31+
name: string
32+
/** The specifier used */
33+
specifier: string
34+
/** Dist tags (latest, alpha, beta, etc.) */
35+
distTags: Record<string, string>
36+
/** When the cache was last synced (timestamp) */
37+
lastSynced: number
38+
/** All versions (filtered if specifier provided) */
39+
versions: string[]
40+
/** Version publish times (ISO 8601) */
41+
time: Record<string, string>
42+
/** Error message if resolution failed (when throw=false) */
43+
error?: string
44+
}

shared/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './deno-doc'
88
export * from './i18n-status'
99
export * from './comparison'
1010
export * from './skills'
11+
export * from './fast-npm-meta'

0 commit comments

Comments
 (0)