Skip to content

Commit 8ebefec

Browse files
committed
feat: version history page display download count
1 parent 8cd4074 commit 8ebefec

File tree

3 files changed

+197
-3
lines changed

3 files changed

+197
-3
lines changed

app/pages/package/[[org]]/[name]/versions.vue

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ definePageMeta({
1717
name: 'package-versions',
1818
})
1919
20+
interface NpmWebsiteVersionDownload {
21+
version: string
22+
downloads: number
23+
}
24+
25+
interface NpmWebsiteVersionsResponse {
26+
weeklyDownloads?: number
27+
versions: NpmWebsiteVersionDownload[]
28+
}
29+
2030
/** Number of flat items (headers + version rows) to render statically during SSR */
2131
const SSR_COUNT = 20
2232
@@ -49,6 +59,47 @@ const distTags = computed(() => versionSummary.value?.distTags ?? {})
4959
const versionStrings = computed(() => versionSummary.value?.versions ?? [])
5060
const versionTimes = computed(() => versionSummary.value?.time ?? {})
5161
62+
const { data: npmWebsiteVersions } = useLazyFetch<NpmWebsiteVersionsResponse>(
63+
() => `/api/registry/npmjs-versions/${encodeURIComponent(packageName.value)}`,
64+
{
65+
key: () => `npmjs-versions:${packageName.value}`,
66+
deep: false,
67+
default: () => ({ versions: [] }),
68+
getCachedData(key, nuxtApp) {
69+
return nuxtApp.static.data[key] ?? nuxtApp.payload.data[key]
70+
},
71+
},
72+
)
73+
74+
const numberFormatter = useNumberFormatter()
75+
const versionDownloadsMap = computed(
76+
() =>
77+
new Map(
78+
(npmWebsiteVersions.value?.versions ?? []).map(({ version, downloads }) => [
79+
version,
80+
downloads,
81+
]),
82+
),
83+
)
84+
85+
function getVersionDownloads(version: string): number | undefined {
86+
return versionDownloadsMap.value.get(version)
87+
}
88+
89+
function getGroupDownloads(versions: string[]): number | undefined {
90+
let total = 0
91+
let hasValue = false
92+
93+
for (const version of versions) {
94+
const downloads = getVersionDownloads(version)
95+
if (downloads === undefined) continue
96+
total += downloads
97+
hasValue = true
98+
}
99+
100+
return hasValue ? total : undefined
101+
}
102+
52103
// ─── Phase 2: full metadata (loaded on first group expand) ────────────────────
53104
// Fetches deprecated status, provenance, and exact times needed for version rows.
54105
@@ -241,6 +292,14 @@ const flatItems = computed<FlatItem[]>(() => {
241292
>
242293
</div>
243294
<!-- Right: date + provenance -->
295+
<div
296+
v-if="getVersionDownloads(latestTagRow!.version) !== undefined"
297+
class="text-sm font-medium text-fg tabular-nums shrink-0"
298+
:aria-label="$t('package.downloads.title')"
299+
dir="ltr"
300+
>
301+
{{ numberFormatter.format(getVersionDownloads(latestTagRow!.version)!) }}
302+
</div>
244303
<div class="flex flex-col items-end gap-1.5 shrink-0 relative z-10">
245304
<ProvenanceBadge
246305
v-if="fullVersionMap?.get(latestTagRow!.version)?.hasProvenance"
@@ -290,6 +349,14 @@ const flatItems = computed<FlatItem[]>(() => {
290349
</LinkBase>
291350

292351
<!-- Date -->
352+
<span
353+
v-if="getVersionDownloads(row.version) !== undefined"
354+
class="text-xs text-fg-muted shrink-0 tabular-nums w-24 text-end"
355+
:aria-label="$t('package.downloads.title')"
356+
dir="ltr"
357+
>
358+
{{ numberFormatter.format(getVersionDownloads(row.version)!) }}
359+
</span>
293360
<DateTime
294361
v-if="getVersionTime(row.version)"
295362
:datetime="getVersionTime(row.version)!"
@@ -373,7 +440,15 @@ const flatItems = computed<FlatItem[]>(() => {
373440
</span>
374441
<span class="text-sm font-medium">{{ item.label }}</span>
375442
<span class="text-xs text-fg-subtle">({{ item.versions.length }})</span>
376-
<span class="ms-auto flex items-center gap-3 shrink-0">
443+
<span
444+
v-if="getGroupDownloads(item.versions) !== undefined"
445+
class="ms-auto text-xs text-fg-muted tabular-nums w-24 text-end"
446+
:aria-label="$t('package.downloads.title')"
447+
dir="ltr"
448+
>
449+
{{ numberFormatter.format(getGroupDownloads(item.versions)!) }}
450+
</span>
451+
<span class="flex items-center gap-3 shrink-0">
377452
<span class="text-xs text-fg-muted" dir="ltr">{{ item.versions[0] }}</span>
378453
<DateTime
379454
v-if="getVersionTime(item.versions[0])"
@@ -437,8 +512,16 @@ const flatItems = computed<FlatItem[]>(() => {
437512
</span>
438513
</div>
439514

515+
<span
516+
v-if="getVersionDownloads(item.version) !== undefined"
517+
class="text-xs text-fg-muted tabular-nums w-24 text-end shrink-0"
518+
:aria-label="$t('package.downloads.title')"
519+
dir="ltr"
520+
>
521+
{{ numberFormatter.format(getVersionDownloads(item.version)!) }}
522+
</span>
440523
<!-- Right side -->
441-
<div class="flex items-center gap-2 shrink-0 relative z-10">
524+
<div class="flex items-center gap-2 shrink-0 relative z-10 w-36 justify-end">
442525
<!-- Metadata: date + provenance -->
443526
<DateTime
444527
v-if="getVersionTime(item.version)"
@@ -477,7 +560,15 @@ const flatItems = computed<FlatItem[]>(() => {
477560
</span>
478561
<span class="text-sm font-medium">{{ item.label }}</span>
479562
<span class="text-xs text-fg-subtle">({{ item.versions.length }})</span>
480-
<span class="ms-auto flex items-center gap-3 shrink-0">
563+
<span
564+
v-if="getGroupDownloads(item.versions) !== undefined"
565+
class="ms-auto text-xs text-fg-muted tabular-nums w-24 text-end"
566+
:aria-label="$t('package.downloads.title')"
567+
dir="ltr"
568+
>
569+
{{ numberFormatter.format(getGroupDownloads(item.versions)!) }}
570+
</span>
571+
<span class="flex items-center gap-3 shrink-0">
481572
<span class="text-xs text-fg-muted" dir="ltr">{{ item.versions[0] }}</span>
482573
<DateTime
483574
v-if="getVersionTime(item.versions[0] ?? '')"
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants'
2+
import {
3+
fetchNpmVersionDownloadsFromApi,
4+
} from '#server/utils/npm-website-versions'
5+
6+
export default defineCachedEventHandler(
7+
async event => {
8+
const pkgParam = getRouterParam(event, 'pkg')
9+
if (!pkgParam) {
10+
throw createError({ statusCode: 404, message: 'Package name is required' })
11+
}
12+
13+
const packageName = decodeURIComponent(pkgParam)
14+
15+
try {
16+
const parsed = await fetchNpmVersionDownloadsFromApi(packageName)
17+
18+
if (parsed.versions.length === 0) {
19+
throw createError({
20+
statusCode: 502,
21+
message: 'Failed to fetch version download data',
22+
})
23+
}
24+
25+
return {
26+
packageName,
27+
source: 'npm-api',
28+
sourceUrl: `https://api.npmjs.org/versions/${encodePackageName(packageName)}/last-week`,
29+
fetchedAt: new Date().toISOString(),
30+
weeklyDownloads: parsed.weeklyDownloads,
31+
versions: parsed.versions,
32+
}
33+
} catch (error: unknown) {
34+
handleApiError(error, {
35+
statusCode: 502,
36+
message: 'Failed to fetch version download data from npm API',
37+
})
38+
}
39+
},
40+
{
41+
maxAge: CACHE_MAX_AGE_ONE_HOUR,
42+
swr: true,
43+
getKey: event => {
44+
const pkg = getRouterParam(event, 'pkg') ?? ''
45+
return `npmjs-versions:v2:${pkg}`
46+
},
47+
},
48+
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
export interface NpmWebsiteVersionDownload {
2+
version: string
3+
downloads: number
4+
}
5+
6+
export interface NpmWebsiteVersionsData {
7+
weeklyDownloads?: number
8+
versions: NpmWebsiteVersionDownload[]
9+
}
10+
11+
interface NpmApiVersionDownloadsResponse {
12+
downloads: Record<string, number>
13+
}
14+
15+
interface NpmApiWeeklyDownloadsResponse {
16+
downloads: number
17+
}
18+
19+
export async function fetchNpmVersionDownloadsFromApi(
20+
packageName: string,
21+
): Promise<NpmWebsiteVersionsData> {
22+
const encodedName = encodePackageName(packageName)
23+
24+
const [versionsResponse, weeklyDownloadsResponse] = await Promise.all([
25+
fetch(`https://api.npmjs.org/versions/${encodedName}/last-week`),
26+
fetch(`https://api.npmjs.org/downloads/point/last-week/${encodedName}`),
27+
])
28+
29+
if (!versionsResponse.ok) {
30+
if (versionsResponse.status === 404) {
31+
throw createError({
32+
statusCode: 404,
33+
message: 'Package not found',
34+
})
35+
}
36+
37+
throw createError({
38+
statusCode: 502,
39+
message: 'Failed to fetch version download data from npm API',
40+
})
41+
}
42+
43+
const versionsData = (await versionsResponse.json()) as NpmApiVersionDownloadsResponse
44+
const weeklyDownloadsData = weeklyDownloadsResponse.ok
45+
? ((await weeklyDownloadsResponse.json()) as NpmApiWeeklyDownloadsResponse)
46+
: null
47+
48+
return {
49+
weeklyDownloads: weeklyDownloadsData?.downloads,
50+
versions: Object.entries(versionsData.downloads).map(([version, downloads]) => ({
51+
version,
52+
downloads,
53+
})),
54+
}
55+
}

0 commit comments

Comments
 (0)