Skip to content

Commit 213462e

Browse files
bteaautofix-ci[bot]ghostdevv
authored
feat: version history page display download count (#2178)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Willow (GHOST) <git@willow.sh>
1 parent fecb36c commit 213462e

File tree

5 files changed

+353
-64
lines changed

5 files changed

+353
-64
lines changed

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

Lines changed: 206 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ definePageMeta({
1717
name: 'package-versions',
1818
})
1919
20+
interface NpmWebsiteVersionDownload {
21+
version: string
22+
downloads: number
23+
}
24+
25+
interface NpmWebsiteVersionsResponse {
26+
packages: Array<{
27+
packageName: string
28+
versions: NpmWebsiteVersionDownload[]
29+
}>
30+
}
31+
2032
/** Number of flat items (headers + version rows) to render statically during SSR */
2133
const SSR_COUNT = 20
2234
@@ -26,6 +38,9 @@ const packageName = computed(() => {
2638
const { org, name } = route.params
2739
return org ? `${org}/${name}` : name
2840
})
41+
const packageNameQueryParam = computed(() => {
42+
return packageName.value ? { packages: packageName.value } : {}
43+
})
2944
const orgName = computed(() => route.params.org?.replace('@', '') ?? null)
3045
3146
// ─── Phase 1: lightweight fetch (page load) ───────────────────────────────────
@@ -49,6 +64,65 @@ const distTags = computed(() => versionSummary.value?.distTags ?? {})
4964
const versionStrings = computed(() => versionSummary.value?.versions ?? [])
5065
const versionTimes = computed(() => versionSummary.value?.time ?? {})
5166
67+
const { data: npmWebsiteVersions } = useLazyFetch<NpmWebsiteVersionsResponse>(
68+
() => '/api/registry/downloads/versions',
69+
{
70+
key: () => `downloads-versions:${packageName.value}`,
71+
query: packageNameQueryParam,
72+
deep: false,
73+
default: () => ({ packages: [] }),
74+
getCachedData(key, nuxtApp) {
75+
return nuxtApp.static.data[key] ?? nuxtApp.payload.data[key]
76+
},
77+
},
78+
)
79+
80+
const packageVersions = computed(() => {
81+
return (
82+
npmWebsiteVersions.value?.packages.find(pkg => pkg.packageName === packageName.value)
83+
?.versions ?? []
84+
)
85+
})
86+
87+
const numberFormatter = useNumberFormatter()
88+
const { t } = useI18n()
89+
const versionDownloadsMap = computed(
90+
() => new Map(packageVersions.value.map(({ version, downloads }) => [version, downloads])),
91+
)
92+
93+
function getVersionDownloads(version: string): number | undefined {
94+
return versionDownloadsMap.value.get(version)
95+
}
96+
97+
function getGroupDownloads(versions: string[]): number | undefined {
98+
let total = 0
99+
let hasValue = false
100+
101+
for (const version of versions) {
102+
const downloads = getVersionDownloads(version)
103+
if (downloads === undefined) continue
104+
total += downloads
105+
hasValue = true
106+
}
107+
108+
return hasValue ? total : undefined
109+
}
110+
111+
const groupDownloadsMap = computed(() => {
112+
const map = new Map<string, number>()
113+
for (const group of versionGroups.value) {
114+
const downloads = getGroupDownloads(group.versions)
115+
if (downloads !== undefined) {
116+
map.set(group.groupKey, downloads)
117+
}
118+
}
119+
return map
120+
})
121+
122+
function getDownloadsAriaLabel(downloads: number): string {
123+
return `${numberFormatter.value.format(downloads)} ${t('package.downloads.title')}`
124+
}
125+
52126
// ─── Phase 2: full metadata (fired automatically after phase 1 completes) ────
53127
// Fetches deprecated status, provenance, and exact times needed for version rows.
54128
@@ -260,9 +334,9 @@ const flatItems = computed<FlatItem[]>(() => {
260334
<!-- Latest — featured card -->
261335
<div
262336
v-if="latestTagRow"
263-
class="border-y sm:rounded-lg sm:border border-accent/40 bg-accent/5 px-5 py-4 relative flex items-center justify-between gap-4 hover:bg-accent/8 transition-colors"
337+
class="border-y sm:rounded-lg sm:border border-accent/40 bg-accent/5 px-4 py-4 relative flex items-center justify-between gap-4 hover:bg-accent/8 transition-colors"
264338
>
265-
<!-- Left: tags + version -->
339+
<!-- Left: tags + version + deprecated -->
266340
<div>
267341
<div class="flex items-center gap-2 mb-1.5 flex-wrap">
268342
<span class="text-3xs font-bold uppercase tracking-widest text-accent">latest</span>
@@ -273,34 +347,47 @@ const flatItems = computed<FlatItem[]>(() => {
273347
:title="tag"
274348
>{{ tag }}</span
275349
>
350+
<span
351+
v-if="fullVersionMap?.get(latestTagRow!.version)?.deprecated"
352+
class="text-3xs font-medium text-red-700 dark:text-red-400 bg-red-100 dark:bg-red-900/30 px-1.5 py-0.5 rounded"
353+
:title="fullVersionMap!.get(latestTagRow!.version)!.deprecated"
354+
>deprecated</span
355+
>
356+
</div>
357+
<div class="flex items-center gap-2">
358+
<LinkBase
359+
:to="packageRoute(packageName, latestTagRow!.version)"
360+
class="text-2xl font-semibold tracking-tight after:absolute after:inset-0 after:content-['']"
361+
:title="latestTagRow!.version"
362+
dir="ltr"
363+
>v{{ latestTagRow!.version }}</LinkBase
364+
>
365+
<ProvenanceBadge
366+
v-if="fullVersionMap?.get(latestTagRow!.version)?.hasProvenance"
367+
:package-name="packageName"
368+
:version="latestTagRow!.version"
369+
compact
370+
:linked="false"
371+
class="relative z-10"
372+
/>
276373
</div>
277-
<LinkBase
278-
:to="packageRoute(packageName, latestTagRow!.version)"
279-
class="text-2xl font-semibold tracking-tight after:absolute after:inset-0 after:content-['']"
280-
:title="latestTagRow!.version"
281-
dir="ltr"
282-
>{{ latestTagRow!.version }}</LinkBase
283-
>
284374
</div>
285-
<!-- Right: deprecated + date + provenance -->
286-
<div class="flex flex-col items-end gap-1.5 shrink-0 relative z-10">
375+
<!-- Right: downloads + date -->
376+
<div class="flex items-center gap-4 shrink-0 relative z-10">
287377
<span
288-
v-if="fullVersionMap?.get(latestTagRow!.version)?.deprecated"
289-
class="text-3xs font-medium text-red-700 dark:text-red-400 bg-red-100 dark:bg-red-900/30 px-1.5 py-0.5 rounded"
290-
:title="fullVersionMap!.get(latestTagRow!.version)!.deprecated"
291-
>deprecated</span
378+
v-if="getVersionDownloads(latestTagRow!.version)"
379+
class="w-28 grid grid-flow-col auto-cols-max items-center gap-1 text-xs text-fg-muted tabular-nums justify-end"
380+
:aria-label="getDownloadsAriaLabel(getVersionDownloads(latestTagRow!.version)!)"
381+
dir="ltr"
382+
:title="getDownloadsAriaLabel(getVersionDownloads(latestTagRow!.version)!)"
292383
>
293-
<ProvenanceBadge
294-
v-if="fullVersionMap?.get(latestTagRow!.version)?.hasProvenance"
295-
:package-name="packageName"
296-
:version="latestTagRow!.version"
297-
compact
298-
:linked="false"
299-
/>
384+
<span>{{ numberFormatter.format(getVersionDownloads(latestTagRow!.version)!) }}</span>
385+
<span class="i-lucide:chart-line" aria-hidden="true"></span>
386+
</span>
300387
<DateTime
301388
v-if="getVersionTime(latestTagRow!.version)"
302389
:datetime="getVersionTime(latestTagRow!.version)!"
303-
class="text-xs text-fg-subtle"
390+
class="text-xs text-fg-subtle whitespace-nowrap w-24 text-end"
304391
year="numeric"
305392
month="short"
306393
day="numeric"
@@ -329,39 +416,55 @@ const flatItems = computed<FlatItem[]>(() => {
329416
>
330417
</div>
331418

332-
<!-- Version -->
333-
<LinkBase
334-
:to="packageRoute(packageName, row.version)"
335-
class="text-sm flex-1 min-w-0 after:absolute after:inset-0 after:content-['']"
336-
:title="row.version"
337-
dir="ltr"
338-
>
339-
{{ row.version }}
340-
</LinkBase>
341-
342-
<!-- Deprecated + Date + Provenance -->
343-
<div class="flex items-center gap-2 shrink-0 relative z-10">
419+
<!-- Version + Provenance + Deprecated -->
420+
<div class="flex-1 min-w-0 flex items-center gap-2">
421+
<LinkBase
422+
:to="packageRoute(packageName, row.version)"
423+
class="text-sm after:absolute after:inset-0 after:content-['']"
424+
:title="row.version"
425+
dir="ltr"
426+
>
427+
v{{ row.version }}
428+
</LinkBase>
429+
<ProvenanceBadge
430+
v-if="fullVersionMap?.get(row.version)?.hasProvenance"
431+
:package-name="packageName"
432+
:version="row.version"
433+
compact
434+
:linked="false"
435+
class="relative z-10"
436+
/>
344437
<span
345438
v-if="fullVersionMap?.get(row.version)?.deprecated"
346-
class="text-3xs font-medium text-red-700 dark:text-red-400 bg-red-100 dark:bg-red-900/30 px-1.5 py-0.5 rounded"
439+
class="text-3xs font-medium text-red-700 dark:text-red-400 bg-red-100 dark:bg-red-900/30 px-1.5 py-0.5 rounded relative z-10"
347440
:title="fullVersionMap!.get(row.version)!.deprecated"
348441
>deprecated</span
349442
>
443+
</div>
444+
445+
<!-- Downloads -->
446+
<span
447+
v-if="getVersionDownloads(row.version)"
448+
class="w-28 grid grid-flow-col auto-cols-max items-center justify-end gap-1 text-xs text-fg-muted tabular-nums shrink-0 relative z-10"
449+
:aria-label="getDownloadsAriaLabel(getVersionDownloads(row.version)!)"
450+
dir="ltr"
451+
:title="getDownloadsAriaLabel(getVersionDownloads(row.version)!)"
452+
>
453+
<span>{{ numberFormatter.format(getVersionDownloads(row.version)!) }}</span>
454+
<span class="i-lucide:chart-line" aria-hidden="true"></span>
455+
</span>
456+
<span v-else class="w-28 shrink-0" />
457+
458+
<!-- Date -->
459+
<div class="flex items-center gap-2 shrink-0 relative z-10">
350460
<DateTime
351461
v-if="getVersionTime(row.version)"
352462
:datetime="getVersionTime(row.version)!"
353-
class="text-xs text-fg-subtle hidden sm:block"
463+
class="text-xs text-fg-subtle hidden sm:block w-24 text-end"
354464
year="numeric"
355465
month="short"
356466
day="numeric"
357467
/>
358-
<ProvenanceBadge
359-
v-if="fullVersionMap?.get(row.version)?.hasProvenance"
360-
:package-name="packageName"
361-
:version="row.version"
362-
compact
363-
:linked="false"
364-
/>
365468
</div>
366469
</div>
367470
</div>
@@ -427,14 +530,27 @@ const flatItems = computed<FlatItem[]>(() => {
427530
>deprecated</span
428531
>
429532
<span class="text-xs text-fg-subtle">({{ item.versions.length }})</span>
430-
<span class="ms-auto flex items-center gap-3 shrink-0">
431-
<span class="text-xs text-fg-muted" :title="item.versions[0]" dir="ltr">{{
432-
item.versions[0]
533+
<span class="text-xs text-fg-muted" :title="item.versions[0]" dir="ltr"
534+
>v{{ item.versions[0] }}</span
535+
>
536+
<span
537+
v-if="groupDownloadsMap.has(item.groupKey)"
538+
class="ms-auto w-28 grid grid-flow-col auto-cols-max items-center justify-end gap-1 text-xs text-fg-muted tabular-nums shrink-0"
539+
:aria-label="getDownloadsAriaLabel(groupDownloadsMap.get(item.groupKey)!)"
540+
dir="ltr"
541+
:title="getDownloadsAriaLabel(groupDownloadsMap.get(item.groupKey)!)"
542+
>
543+
<span>{{
544+
numberFormatter.format(groupDownloadsMap.get(item.groupKey)!)
433545
}}</span>
546+
<span class="i-lucide:chart-line" aria-hidden="true"></span>
547+
</span>
548+
<span v-else class="ms-auto w-28 shrink-0" />
549+
<span class="flex items-center gap-3 shrink-0">
434550
<DateTime
435551
v-if="getVersionTime(item.versions[0])"
436552
:datetime="getVersionTime(item.versions[0])!"
437-
class="text-xs text-fg-subtle hidden sm:block"
553+
class="text-xs text-fg-subtle hidden sm:block whitespace-nowrap w-24 text-end"
438554
year="numeric"
439555
month="short"
440556
day="numeric"
@@ -474,8 +590,16 @@ const flatItems = computed<FlatItem[]>(() => {
474590
"
475591
dir="ltr"
476592
>
477-
{{ item.version }}
593+
v{{ item.version }}
478594
</LinkBase>
595+
<ProvenanceBadge
596+
v-if="fullVersionMap?.get(item.version)?.hasProvenance"
597+
:package-name="packageName"
598+
:version="item.version"
599+
compact
600+
:linked="false"
601+
class="relative z-10"
602+
/>
479603
<div
480604
v-if="versionToTagsMap.get(item.version)?.length"
481605
class="flex items-center gap-1 flex-wrap relative z-10"
@@ -499,24 +623,31 @@ const flatItems = computed<FlatItem[]>(() => {
499623
</span>
500624
</div>
501625

502-
<!-- Right side -->
626+
<!-- Downloads -->
627+
<span
628+
v-if="getVersionDownloads(item.version)"
629+
class="w-28 grid grid-flow-col auto-cols-max items-center justify-end gap-1 text-xs text-fg-muted tabular-nums shrink-0 relative z-10"
630+
:aria-label="getDownloadsAriaLabel(getVersionDownloads(item.version)!)"
631+
:title="getDownloadsAriaLabel(getVersionDownloads(item.version)!)"
632+
dir="ltr"
633+
>
634+
<span>{{
635+
numberFormatter.format(getVersionDownloads(item.version)!)
636+
}}</span>
637+
<span class="i-lucide:chart-line" aria-hidden="true"></span>
638+
</span>
639+
<span v-else class="w-28 shrink-0" />
640+
641+
<!-- Date -->
503642
<div class="flex items-center gap-2 shrink-0 relative z-10">
504-
<!-- Metadata: date + provenance -->
505643
<DateTime
506644
v-if="getVersionTime(item.version)"
507645
:datetime="getVersionTime(item.version)!"
508-
class="text-xs text-fg-subtle hidden sm:block"
646+
class="text-xs text-fg-subtle hidden sm:block whitespace-nowrap w-24 text-end"
509647
year="numeric"
510648
month="short"
511649
day="numeric"
512650
/>
513-
<ProvenanceBadge
514-
v-if="fullVersionMap?.get(item.version)?.hasProvenance"
515-
:package-name="packageName"
516-
:version="item.version"
517-
compact
518-
:linked="false"
519-
/>
520651
</div>
521652
</div>
522653
</div>
@@ -539,12 +670,25 @@ const flatItems = computed<FlatItem[]>(() => {
539670
</span>
540671
<span class="text-sm font-medium">{{ item.label }}</span>
541672
<span class="text-xs text-fg-subtle">({{ item.versions.length }})</span>
542-
<span class="ms-auto flex items-center gap-3 shrink-0">
543-
<span class="text-xs text-fg-muted" dir="ltr">{{ item.versions[0] }}</span>
673+
<span v-if="item.versions[0]" class="text-xs text-fg-muted" dir="ltr"
674+
>v{{ item.versions[0] }}</span
675+
>
676+
<span
677+
v-if="groupDownloadsMap.has(item.groupKey)"
678+
class="ms-auto w-28 grid grid-flow-col auto-cols-max items-center justify-end gap-1 text-xs text-fg-muted tabular-nums shrink-0"
679+
:aria-label="getDownloadsAriaLabel(groupDownloadsMap.get(item.groupKey)!)"
680+
dir="ltr"
681+
:title="getDownloadsAriaLabel(groupDownloadsMap.get(item.groupKey)!)"
682+
>
683+
<span>{{ numberFormatter.format(groupDownloadsMap.get(item.groupKey)!) }}</span>
684+
<span class="i-lucide:chart-line" aria-hidden="true"></span>
685+
</span>
686+
<span v-else class="ms-auto w-28 shrink-0" />
687+
<span class="flex items-center gap-3 shrink-0">
544688
<DateTime
545689
v-if="getVersionTime(item.versions[0] ?? '')"
546690
:datetime="getVersionTime(item.versions[0] ?? '')!"
547-
class="text-xs text-fg-subtle hidden sm:block"
691+
class="text-xs text-fg-subtle hidden sm:block whitespace-nowrap w-24 text-end"
548692
year="numeric"
549693
month="short"
550694
day="numeric"

nuxt.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export default defineNuxtConfig({
118118
isr: {
119119
expiration: 60 * 60 /* one hour */,
120120
passQuery: true,
121-
allowQuery: ['mode', 'filterOldVersions', 'filterThreshold'],
121+
allowQuery: ['mode', 'filterOldVersions', 'filterThreshold', 'packages'],
122122
},
123123
},
124124
'/api/registry/timeline/**': {

0 commit comments

Comments
 (0)