Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
257 changes: 194 additions & 63 deletions app/pages/package/[[org]]/[name]/versions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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) ───────────────────────────────────
Expand All @@ -49,6 +64,54 @@ const distTags = computed(() => versionSummary.value?.distTags ?? {})
const versionStrings = computed(() => versionSummary.value?.versions ?? [])
const versionTimes = computed(() => versionSummary.value?.time ?? {})

const { data: npmWebsiteVersions } = useLazyFetch<NpmWebsiteVersionsResponse>(
() => '/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
}

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.

Expand Down Expand Up @@ -262,7 +325,7 @@ const flatItems = computed<FlatItem[]>(() => {
v-if="latestTagRow"
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"
>
<!-- Left: tags + version -->
<!-- Left: tags + version + deprecated -->
<div>
<div class="flex items-center gap-2 mb-1.5 flex-wrap">
<span class="text-3xs font-bold uppercase tracking-widest text-accent">latest</span>
Expand All @@ -273,34 +336,47 @@ const flatItems = computed<FlatItem[]>(() => {
:title="tag"
>{{ tag }}</span
>
<span
v-if="fullVersionMap?.get(latestTagRow!.version)?.deprecated"
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"
:title="fullVersionMap!.get(latestTagRow!.version)!.deprecated"
>deprecated</span
>
</div>
<div class="flex items-center gap-2">
<LinkBase
:to="packageRoute(packageName, latestTagRow!.version)"
class="text-2xl font-semibold tracking-tight after:absolute after:inset-0 after:content-['']"
:title="latestTagRow!.version"
dir="ltr"
>v{{ latestTagRow!.version }}</LinkBase
>
<ProvenanceBadge
v-if="fullVersionMap?.get(latestTagRow!.version)?.hasProvenance"
:package-name="packageName"
:version="latestTagRow!.version"
compact
:linked="false"
class="relative z-10"
/>
</div>
<LinkBase
:to="packageRoute(packageName, latestTagRow!.version)"
class="text-2xl font-semibold tracking-tight after:absolute after:inset-0 after:content-['']"
:title="latestTagRow!.version"
dir="ltr"
>{{ latestTagRow!.version }}</LinkBase
>
</div>
<!-- Right: deprecated + date + provenance -->
<!-- Right: downloads + date + provenance -->
<div
v-if="getVersionDownloads(latestTagRow!.version)"
class="grid grid-flow-col auto-cols-max items-center gap-1 text-sm font-medium text-fg tabular-nums shrink-0"
:aria-label="getDownloadsAriaLabel(getVersionDownloads(latestTagRow!.version)!)"
dir="ltr"
:title="getDownloadsAriaLabel(getVersionDownloads(latestTagRow!.version)!)"
>
<span>{{ numberFormatter.format(getVersionDownloads(latestTagRow!.version)!) }}</span>
<span class="i-lucide:chart-line" aria-hidden="true"></span>
</div>
<div class="flex flex-col items-end gap-1.5 shrink-0 relative z-10">
<span
v-if="fullVersionMap?.get(latestTagRow!.version)?.deprecated"
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"
:title="fullVersionMap!.get(latestTagRow!.version)!.deprecated"
>deprecated</span
>
<ProvenanceBadge
v-if="fullVersionMap?.get(latestTagRow!.version)?.hasProvenance"
:package-name="packageName"
:version="latestTagRow!.version"
compact
:linked="false"
/>
<DateTime
v-if="getVersionTime(latestTagRow!.version)"
:datetime="getVersionTime(latestTagRow!.version)!"
class="text-xs text-fg-subtle"
class="text-xs text-fg-subtle whitespace-nowrap"
year="numeric"
month="short"
day="numeric"
Expand Down Expand Up @@ -329,39 +405,55 @@ const flatItems = computed<FlatItem[]>(() => {
>
</div>

<!-- Version -->
<LinkBase
:to="packageRoute(packageName, row.version)"
class="text-sm flex-1 min-w-0 after:absolute after:inset-0 after:content-['']"
:title="row.version"
dir="ltr"
>
{{ row.version }}
</LinkBase>

<!-- Deprecated + Date + Provenance -->
<div class="flex items-center gap-2 shrink-0 relative z-10">
<!-- Version + Provenance + Deprecated -->
<div class="flex-1 min-w-0 flex items-center gap-2">
<LinkBase
:to="packageRoute(packageName, row.version)"
class="text-sm after:absolute after:inset-0 after:content-['']"
:title="row.version"
dir="ltr"
>
v{{ row.version }}
</LinkBase>
<ProvenanceBadge
v-if="fullVersionMap?.get(row.version)?.hasProvenance"
:package-name="packageName"
:version="row.version"
compact
:linked="false"
class="relative z-10"
/>
<span
v-if="fullVersionMap?.get(row.version)?.deprecated"
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"
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"
:title="fullVersionMap!.get(row.version)!.deprecated"
>deprecated</span
>
</div>

<!-- Downloads -->
<span
v-if="getVersionDownloads(row.version)"
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"
:aria-label="getDownloadsAriaLabel(getVersionDownloads(row.version)!)"
dir="ltr"
:title="getDownloadsAriaLabel(getVersionDownloads(row.version)!)"
>
<span>{{ numberFormatter.format(getVersionDownloads(row.version)!) }}</span>
<span class="i-lucide:chart-line" aria-hidden="true"></span>
</span>
<span v-else class="w-28 shrink-0" />

<!-- Date -->
<div class="flex items-center gap-2 shrink-0 relative z-10">
<DateTime
v-if="getVersionTime(row.version)"
:datetime="getVersionTime(row.version)!"
class="text-xs text-fg-subtle hidden sm:block"
class="text-xs text-fg-subtle hidden sm:block w-24 text-end"
year="numeric"
month="short"
day="numeric"
/>
<ProvenanceBadge
v-if="fullVersionMap?.get(row.version)?.hasProvenance"
:package-name="packageName"
:version="row.version"
compact
:linked="false"
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -427,14 +519,25 @@ const flatItems = computed<FlatItem[]>(() => {
>deprecated</span
>
<span class="text-xs text-fg-subtle">({{ item.versions.length }})</span>
<span class="ms-auto flex items-center gap-3 shrink-0">
<span class="text-xs text-fg-muted" :title="item.versions[0]" dir="ltr">{{
item.versions[0]
}}</span>
<span class="text-xs text-fg-muted" :title="item.versions[0]" dir="ltr"
>v{{ item.versions[0] }}</span
>
<span
v-if="getGroupDownloads(item.versions)"
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"
:aria-label="getDownloadsAriaLabel(getGroupDownloads(item.versions)!)"
dir="ltr"
:title="getDownloadsAriaLabel(getGroupDownloads(item.versions)!)"
>
<span>{{ numberFormatter.format(getGroupDownloads(item.versions)!) }}</span>
<span class="i-lucide:chart-line" aria-hidden="true"></span>
</span>
<span v-else class="ms-auto w-28 shrink-0" />
<span class="flex items-center gap-3 shrink-0">
<DateTime
v-if="getVersionTime(item.versions[0])"
:datetime="getVersionTime(item.versions[0])!"
class="text-xs text-fg-subtle hidden sm:block"
class="text-xs text-fg-subtle hidden sm:block whitespace-nowrap w-24 text-end"
year="numeric"
month="short"
day="numeric"
Expand Down Expand Up @@ -474,8 +577,16 @@ const flatItems = computed<FlatItem[]>(() => {
"
dir="ltr"
>
{{ item.version }}
v{{ item.version }}
</LinkBase>
<ProvenanceBadge
v-if="fullVersionMap?.get(item.version)?.hasProvenance"
:package-name="packageName"
:version="item.version"
compact
:linked="false"
class="relative z-10"
/>
<div
v-if="versionToTagsMap.get(item.version)?.length"
class="flex items-center gap-1 flex-wrap relative z-10"
Expand All @@ -499,24 +610,31 @@ const flatItems = computed<FlatItem[]>(() => {
</span>
</div>

<!-- Right side -->
<!-- Downloads -->
<span
v-if="getVersionDownloads(item.version)"
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"
:aria-label="getDownloadsAriaLabel(getVersionDownloads(item.version)!)"
:title="getDownloadsAriaLabel(getVersionDownloads(item.version)!)"
dir="ltr"
>
<span>{{
numberFormatter.format(getVersionDownloads(item.version)!)
}}</span>
<span class="i-lucide:chart-line" aria-hidden="true"></span>
</span>
<span v-else class="w-28 shrink-0" />

<!-- Date -->
<div class="flex items-center gap-2 shrink-0 relative z-10">
<!-- Metadata: date + provenance -->
<DateTime
v-if="getVersionTime(item.version)"
:datetime="getVersionTime(item.version)!"
class="text-xs text-fg-subtle hidden sm:block"
class="text-xs text-fg-subtle hidden sm:block whitespace-nowrap w-24 text-end"
year="numeric"
month="short"
day="numeric"
/>
<ProvenanceBadge
v-if="fullVersionMap?.get(item.version)?.hasProvenance"
:package-name="packageName"
:version="item.version"
compact
:linked="false"
/>
</div>
</div>
</div>
Expand All @@ -539,12 +657,25 @@ const flatItems = computed<FlatItem[]>(() => {
</span>
<span class="text-sm font-medium">{{ item.label }}</span>
<span class="text-xs text-fg-subtle">({{ item.versions.length }})</span>
<span class="ms-auto flex items-center gap-3 shrink-0">
<span class="text-xs text-fg-muted" dir="ltr">{{ item.versions[0] }}</span>
<span v-if="item.versions[0]" class="text-xs text-fg-muted" dir="ltr"
>v{{ item.versions[0] }}</span
>
<span
v-if="getGroupDownloads(item.versions)"
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"
:aria-label="getDownloadsAriaLabel(getGroupDownloads(item.versions)!)"
dir="ltr"
:title="getDownloadsAriaLabel(getGroupDownloads(item.versions)!)"
>
<span>{{ numberFormatter.format(getGroupDownloads(item.versions)!) }}</span>
<span class="i-lucide:chart-line" aria-hidden="true"></span>
</span>
<span v-else class="ms-auto w-28 shrink-0" />
<span class="flex items-center gap-3 shrink-0">
<DateTime
v-if="getVersionTime(item.versions[0] ?? '')"
:datetime="getVersionTime(item.versions[0] ?? '')!"
class="text-xs text-fg-subtle hidden sm:block"
class="text-xs text-fg-subtle hidden sm:block whitespace-nowrap w-24 text-end"
year="numeric"
month="short"
day="numeric"
Expand Down
2 changes: 1 addition & 1 deletion nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export default defineNuxtConfig({
isr: {
expiration: 60 * 60 /* one hour */,
passQuery: true,
allowQuery: ['mode', 'filterOldVersions', 'filterThreshold'],
allowQuery: ['mode', 'filterOldVersions', 'filterThreshold', 'packages'],
},
},
'/api/registry/docs/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
Expand Down
Loading
Loading