diff --git a/app/components/Package/Versions.vue b/app/components/Package/Versions.vue
index c6df34a7e4..4d03bd56d7 100644
--- a/app/components/Package/Versions.vue
+++ b/app/components/Package/Versions.vue
@@ -95,6 +95,14 @@ function versionRoute(version: string): RouteLocationRaw {
return packageRoute(props.packageName, version)
}
+// Route to the full versions history page
+const versionsPageRoute = computed((): RouteLocationRaw => {
+ const [org, name = ''] = props.packageName.startsWith('@')
+ ? props.packageName.split('/')
+ : ['', props.packageName]
+ return { name: 'package-versions', params: { org, name } }
+})
+
// Version to tags lookup (supports multiple tags per version)
const versionToTags = computed(() => buildVersionToTagsMap(props.distTags))
@@ -521,15 +529,27 @@ function majorGroupContainsCurrent(group: (typeof otherMajorGroups.value)[0]): b
id="versions"
>
-
- {{ $t('package.downloads.community_distribution') }}
-
+
+
+ {{ $t('package.versions.view_all_versions') }}
+
+
+ {{ $t('package.downloads.community_distribution') }}
+
+
diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue
index 3af5415e5f..af2852a582 100644
--- a/app/pages/package/[[org]]/[name].vue
+++ b/app/pages/package/[[org]]/[name].vue
@@ -207,6 +207,8 @@ const { diff: sizeDiff } = useInstallSizeDiff(packageName, resolvedVersion, pkg,
// → Preserve the server-rendered DOM, don't flash to skeleton.
const nuxtApp = useNuxtApp()
const route = useRoute()
+// Gates template rendering only — data fetches intentionally still run.
+const isVersionsRoute = computed(() => route.name === 'package-versions')
const hasEmptyPayload =
import.meta.client &&
nuxtApp.payload.serverRendered &&
@@ -516,7 +518,8 @@ const showSkeleton = shallowRef(false)
-
+
+
Skeleton
-
+
+import { WindowVirtualizer } from 'virtua/vue'
+import { getVersions } from 'fast-npm-meta'
+import { compare, validRange } from 'semver'
+import {
+ buildVersionToTagsMap,
+ buildTaggedVersionRows,
+ filterVersions,
+ getVersionGroupKey,
+ getVersionGroupLabel,
+} from '~/utils/versions'
+import { fetchAllPackageVersions } from '~/utils/npm/api'
+
+definePageMeta({
+ name: 'package-versions',
+})
+
+/** Number of flat items (headers + version rows) to render statically during SSR */
+const SSR_COUNT = 20
+
+const route = useRoute('package-versions')
+
+const packageName = computed(() => {
+ const { org, name } = route.params
+ return org ? `${org}/${name}` : name
+})
+const orgName = computed(() => route.params.org?.replace('@', '') ?? null)
+
+// ─── Phase 1: lightweight fetch (page load) ───────────────────────────────────
+// Fetches only version strings, dist-tags, and publish times — no deprecated/provenance metadata.
+// Enough to render the "Current Tags" section and all group headers immediately.
+
+const { data: versionSummary } = useLazyAsyncData(
+ () => `package-version-summary:${packageName.value}`,
+ async () => {
+ const data = await getVersions(packageName.value)
+ return {
+ distTags: data.distTags as Record,
+ versions: data.versions,
+ time: data.time as Record,
+ }
+ },
+ { deep: false },
+)
+
+const distTags = computed(() => versionSummary.value?.distTags ?? {})
+const versionStrings = computed(() => versionSummary.value?.versions ?? [])
+const versionTimes = computed(() => versionSummary.value?.time ?? {})
+
+// ─── Phase 2: full metadata (loaded on first group expand) ────────────────────
+// Fetches deprecated status, provenance, and exact times needed for version rows.
+
+const fullVersionMap = shallowRef