diff --git a/app/pages/package/[[org]]/[name]/versions.vue b/app/pages/package/[[org]]/[name]/versions.vue index b07f8aac92..2f91330670 100644 --- a/app/pages/package/[[org]]/[name]/versions.vue +++ b/app/pages/package/[[org]]/[name]/versions.vue @@ -17,6 +17,18 @@ definePageMeta({ name: 'package-versions', }) +interface NpmWebsiteVersionDownload { + version: string + downloads: number +} + +interface NpmWebsiteVersionsResponse { + packages: Array<{ + packageName: string + versions: NpmWebsiteVersionDownload[] + }> +} + /** Number of flat items (headers + version rows) to render statically during SSR */ const SSR_COUNT = 20 @@ -26,6 +38,9 @@ const packageName = computed(() => { const { org, name } = route.params return org ? `${org}/${name}` : name }) +const packageNameQueryParam = computed(() => { + return packageName.value ? { packages: packageName.value } : {} +}) const orgName = computed(() => route.params.org?.replace('@', '') ?? null) // ─── Phase 1: lightweight fetch (page load) ─────────────────────────────────── @@ -49,6 +64,65 @@ const distTags = computed(() => versionSummary.value?.distTags ?? {}) const versionStrings = computed(() => versionSummary.value?.versions ?? []) const versionTimes = computed(() => versionSummary.value?.time ?? {}) +const { data: npmWebsiteVersions } = useLazyFetch( + () => '/api/registry/downloads/versions', + { + key: () => `downloads-versions:${packageName.value}`, + query: packageNameQueryParam, + deep: false, + default: () => ({ packages: [] }), + getCachedData(key, nuxtApp) { + return nuxtApp.static.data[key] ?? nuxtApp.payload.data[key] + }, + }, +) + +const packageVersions = computed(() => { + return ( + npmWebsiteVersions.value?.packages.find(pkg => pkg.packageName === packageName.value) + ?.versions ?? [] + ) +}) + +const numberFormatter = useNumberFormatter() +const { t } = useI18n() +const versionDownloadsMap = computed( + () => new Map(packageVersions.value.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 +} + +const groupDownloadsMap = computed(() => { + const map = new Map() + for (const group of versionGroups.value) { + const downloads = getGroupDownloads(group.versions) + if (downloads !== undefined) { + map.set(group.groupKey, downloads) + } + } + return map +}) + +function getDownloadsAriaLabel(downloads: number): string { + return `${numberFormatter.value.format(downloads)} ${t('package.downloads.title')}` +} + // ─── Phase 2: full metadata (fired automatically after phase 1 completes) ──── // Fetches deprecated status, provenance, and exact times needed for version rows. @@ -260,9 +334,9 @@ const flatItems = computed(() => {
- +
latest @@ -273,34 +347,47 @@ const flatItems = computed(() => { :title="tag" >{{ tag }} + deprecated +
+
+ v{{ latestTagRow!.version }} +
- {{ latestTagRow!.version }}
- -
+ +
deprecated - + {{ numberFormatter.format(getVersionDownloads(latestTagRow!.version)!) }} + + (() => { >
- - - {{ row.version }} - - - -
+ +
+ + v{{ row.version }} + + deprecated +
+ + + + {{ numberFormatter.format(getVersionDownloads(row.version)!) }} + + + + + +
@@ -427,14 +530,27 @@ const flatItems = computed(() => { >deprecated ({{ item.versions.length }}) - - {{ - item.versions[0] + v{{ item.versions[0] }} + + {{ + numberFormatter.format(groupDownloadsMap.get(item.groupKey)!) }} + + + +
@@ -539,12 +670,25 @@ const flatItems = computed(() => { {{ item.label }} ({{ item.versions.length }}) - - {{ item.versions[0] }} + v{{ item.versions[0] }} + + {{ numberFormatter.format(groupDownloadsMap.get(item.groupKey)!) }} + + + +