diff --git a/app/pages/package/[[org]]/[name]/versions.vue b/app/pages/package/[[org]]/[name]/versions.vue index 8a3b9c5070..014433b9cb 100644 --- a/app/pages/package/[[org]]/[name]/versions.vue +++ b/app/pages/package/[[org]]/[name]/versions.vue @@ -17,6 +17,15 @@ definePageMeta({ name: 'package-versions', }) +interface NpmWebsiteVersionDownload { + version: string + downloads: number +} + +interface NpmWebsiteVersionsResponse { + versions: NpmWebsiteVersionDownload[] +} + /** Number of flat items (headers + version rows) to render statically during SSR */ const SSR_COUNT = 20 @@ -49,6 +58,52 @@ const distTags = computed(() => versionSummary.value?.distTags ?? {}) const versionStrings = computed(() => versionSummary.value?.versions ?? []) const versionTimes = computed(() => versionSummary.value?.time ?? {}) +const { data: npmWebsiteVersions } = useLazyFetch( + () => `/api/registry/npmjs-versions/${encodeURIComponent(packageName.value)}`, + { + key: () => `npmjs-versions:${packageName.value}`, + deep: false, + default: () => ({ versions: [] }), + getCachedData(key, nuxtApp) { + return nuxtApp.static.data[key] ?? nuxtApp.payload.data[key] + }, + }, +) + +const numberFormatter = useNumberFormatter() +const { t } = useI18n() +const versionDownloadsMap = computed( + () => + new Map( + (npmWebsiteVersions.value?.versions ?? []).map(({ version, downloads }) => [ + version, + downloads, + ]), + ), +) + +function getVersionDownloads(version: string): number | undefined { + return versionDownloadsMap.value.get(version) +} + +function getGroupDownloads(versions: string[]): number | undefined { + let total = 0 + let hasValue = false + + for (const version of versions) { + const downloads = getVersionDownloads(version) + if (downloads === undefined) continue + total += downloads + hasValue = true + } + + return hasValue ? total : undefined +} + +function getDownloadsAriaLabel(downloads: number): string { + return `${numberFormatter.value.format(downloads)} ${t('package.downloads.title')}` +} + // ─── Phase 2: full metadata (loaded on first group expand) ──────────────────── // Fetches deprecated status, provenance, and exact times needed for version rows. @@ -237,10 +292,20 @@ const flatItems = computed(() => { :to="packageRoute(packageName, latestTagRow!.version)" class="text-2xl font-semibold tracking-tight after:absolute after:inset-0 after:content-['']" dir="ltr" - >{{ latestTagRow!.version }}v{{ latestTagRow!.version }} +
+ {{ numberFormatter.format(getVersionDownloads(latestTagRow!.version)!) }} + +
(() => { (() => { class="text-sm flex-1 min-w-0 after:absolute after:inset-0 after:content-['']" dir="ltr" > - {{ row.version }} + v{{ row.version }} + + {{ numberFormatter.format(getVersionDownloads(row.version)!) }} + +