@@ -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 */
2133const 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+ })
2944const 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 ?? {})
4964const versionStrings = computed (() => versionSummary .value ?.versions ?? [])
5065const 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"
0 commit comments