11<script setup lang="ts">
2+ import { getVersions } from ' fast-npm-meta'
23import {
34 buildVersionToTagsMap ,
45 buildTaggedVersionRows ,
56 getVersionGroupKey ,
67 getVersionGroupLabel ,
78} from ' ~/utils/versions'
9+ import { fetchAllPackageVersions } from ' ~/utils/npm/api'
810
911definePageMeta ({
1012 name: ' package-versions' ,
@@ -25,45 +27,65 @@ const orgName = computed(() => {
2527 return match ? match [1 ] : null
2628})
2729
28- // ─── Data ─────────────────────────────────────────────────────────────────────
30+ // ─── Phase 1: lightweight fetch (page load) ───────────────────────────────────
31+ // Fetches only version strings, dist-tags, and publish times — no deprecated/provenance metadata.
32+ // Enough to render the "Current Tags" section and all group headers immediately.
33+
34+ const { data : versionSummary } = useLazyAsyncData (
35+ () => ` package-version-summary:${packageName .value } ` ,
36+ async () => {
37+ const data = await getVersions (packageName .value )
38+ return {
39+ distTags: data .distTags as Record <string , string >,
40+ versions: data .versions ,
41+ time: data .time as Record <string , string >,
42+ }
43+ },
44+ )
2945
30- const { data : versionHistoryData } = usePackageVersionHistory (packageName )
46+ const distTags = computed (() => versionSummary .value ?.distTags ?? {})
47+ const versionStrings = computed (() => versionSummary .value ?.versions ?? [])
48+ const versionTimes = computed (() => versionSummary .value ?.time ?? {})
3149
32- // TODO: Replace mockChangelogs with pre-rendered HTML from the server
33- // (GitHub releases body or CHANGELOG.md, parsed server-side like README)
34- const mockChangelogs: Record <string , string > = {}
50+ // ─── Phase 2: full metadata (loaded on first group expand) ────────────────────
51+ // Fetches deprecated status, provenance, and exact times needed for version rows.
3552
36- // ─── Derived data ─────────────────────────────────────────────────────────────
53+ const fullVersionMap = ref < Map <
54+ string ,
55+ { time ?: string ; deprecated ?: string ; hasProvenance : boolean }
56+ > | null > (null )
57+ const hasLoadedFull = ref (false )
3758
38- const distTags = computed (() => versionHistoryData .value ?.distTags ?? {})
39- const versionHistory = computed (() => versionHistoryData .value ?.versions ?? [])
59+ async function ensureFullDataLoaded() {
60+ if (hasLoadedFull .value ) return
61+ const versions = await fetchAllPackageVersions (packageName .value )
62+ fullVersionMap .value = new Map (versions .map (v => [v .version , v ]))
63+ hasLoadedFull .value = true
64+ }
4065
41- const versionToTagsMap = computed (() => buildVersionToTagsMap ( distTags . value ))
66+ // ─── Derived data ─────────────────────────────────────────────────────────────
4267
43- const sortedVersions = computed (() =>
44- versionHistory .value .map (v => ({
45- ... v ,
46- tags: versionToTagsMap .value .get (v .version ),
47- hasChangelog: v .version in mockChangelogs ,
48- })),
49- )
68+ // TODO: Replace mockChangelogs with pre-rendered HTML from the server
69+ // (GitHub releases body or CHANGELOG.md, parsed server-side like README)
70+ const mockChangelogs: Record <string , string > = {}
5071
72+ const versionToTagsMap = computed (() => buildVersionToTagsMap (distTags .value ))
5173const tagRows = computed (() => buildTaggedVersionRows (distTags .value ))
5274
53- const versionByKey = computed (() => new Map (versionHistory .value .map (v => [v .version , v ])))
54-
5575function getVersionTime(version : string ): string | undefined {
56- return versionByKey .value . get ( version )?. time
76+ return versionTimes .value [ version ]
5777}
5878
5979// ─── Version groups ───────────────────────────────────────────────────────────
6080
6181const expandedGroups = ref (new Set <string >())
82+ const renderedGroups = ref (new Set <string >())
83+ const loadingGroup = ref <string | null >(null )
6284
6385const versionGroups = computed (() => {
64- const byKey = new Map <string , typeof sortedVersions . value >()
65- for (const v of sortedVersions .value ) {
66- const key = getVersionGroupKey (v . version )
86+ const byKey = new Map <string , string [] >()
87+ for (const v of versionStrings .value ) {
88+ const key = getVersionGroupKey (v )
6789 if (! byKey .has (key )) byKey .set (key , [])
6890 byKey .get (key )! .push (v )
6991 }
@@ -82,12 +104,21 @@ const versionGroups = computed(() => {
82104 }))
83105})
84106
85- function toggleGroup(groupKey : string ) {
107+ async function toggleGroup(groupKey : string ) {
86108 if (expandedGroups .value .has (groupKey )) {
87109 expandedGroups .value .delete (groupKey )
88- } else {
89- expandedGroups .value .add (groupKey )
110+ return
90111 }
112+ if (! hasLoadedFull .value ) {
113+ loadingGroup .value = groupKey
114+ try {
115+ await ensureFullDataLoaded ()
116+ } finally {
117+ loadingGroup .value = null
118+ }
119+ }
120+ renderedGroups .value .add (groupKey )
121+ expandedGroups .value .add (groupKey )
91122}
92123
93124// ─── Changelog side panel ─────────────────────────────────────────────────────
@@ -111,7 +142,7 @@ const jumpError = ref('')
111142function navigateToVersion() {
112143 const v = jumpVersion .value .trim ()
113144 if (! v ) return
114- if (! versionHistory .value .some ( entry => entry . version === v )) {
145+ if (! versionStrings .value .includes ( v )) {
115146 jumpError .value = ` "${v }" not found `
116147 return
117148 }
@@ -208,7 +239,7 @@ watch(jumpVersion, () => {
208239 <!-- Right: date + provenance -->
209240 <div class =" flex flex-col items-end gap-1.5 shrink-0 relative z-10" >
210241 <ProvenanceBadge
211- v-if =" versionByKey .get(tagRows[0].version)?.hasProvenance"
242+ v-if =" fullVersionMap? .get(tagRows[0].version)?.hasProvenance"
212243 :package-name =" packageName"
213244 :version =" tagRows[0].version"
214245 compact
@@ -266,7 +297,7 @@ watch(jumpVersion, () => {
266297
267298 <!-- Provenance -->
268299 <ProvenanceBadge
269- v-if =" versionByKey .get(row.version)?.hasProvenance"
300+ v-if =" fullVersionMap? .get(row.version)?.hasProvenance"
270301 :package-name =" packageName"
271302 :version =" row.version"
272303 compact
@@ -282,7 +313,7 @@ watch(jumpVersion, () => {
282313 <h2 class =" text-xs text-fg-subtle uppercase tracking-wider mb-3 px-4 sm:px-6 ps-1" >
283314 Version History
284315 <span class =" ms-1 normal-case font-normal tracking-normal" >
285- ({{ sortedVersions .length }})
316+ ({{ versionStrings .length }})
286317 </span >
287318 </h2 >
288319
@@ -307,6 +338,12 @@ watch(jumpVersion, () => {
307338 >
308339 <span class =" w-4 h-4 flex items-center justify-center text-fg-subtle shrink-0" >
309340 <span
341+ v-if =" loadingGroup === group.groupKey"
342+ class =" i-svg-spinners:ring-resize w-3 h-3"
343+ aria-hidden =" true"
344+ />
345+ <span
346+ v-else
310347 class =" i-lucide:chevron-right w-3 h-3 transition-transform duration-200 rtl-flip"
311348 :class =" expandedGroups.has(group.groupKey) ? 'rotate-90' : ''"
312349 aria-hidden =" true"
@@ -316,11 +353,11 @@ watch(jumpVersion, () => {
316353 <span class =" text-xs text-fg-subtle" >({{ group.versions.length }})</span >
317354 <span class =" ms-auto flex items-center gap-3 shrink-0" >
318355 <span class =" font-mono text-xs text-fg-muted" dir =" ltr" >{{
319- group.versions[0]?.version
356+ group.versions[0]
320357 }}</span >
321358 <DateTime
322- v-if =" group.versions[0]?.time "
323- :datetime =" group.versions[0].time "
359+ v-if =" getVersionTime( group.versions[0]) "
360+ :datetime =" getVersionTime( group.versions[0])! "
324361 class =" text-xs text-fg-subtle hidden sm:block"
325362 year =" numeric"
326363 month =" short"
@@ -331,94 +368,97 @@ watch(jumpVersion, () => {
331368
332369 <!-- Expanded versions -->
333370 <div v-show =" expandedGroups.has(group.groupKey)" class =" border-t border-border" >
334- <div
335- v-for =" v in group.versions"
336- :key =" v.version"
337- class =" border-b border-border last:border-0 transition-colors"
338- :class =" selectedChangelogVersion === v.version ? 'bg-bg-subtle' : ''"
339- >
371+ <template v-if =" renderedGroups .has (group .groupKey )" >
340372 <div
341- class =" flex items-center gap-3 px-4 ps-11 py-2.5 group relative"
342- :class =" selectedChangelogVersion === v.version ? '' : 'hover:bg-bg-subtle'"
373+ v-for =" v in group.versions"
374+ :key =" v"
375+ class =" border-b border-border last:border-0 transition-colors"
376+ :class =" selectedChangelogVersion === v ? 'bg-bg-subtle' : ''"
343377 >
344- <!-- Version + badges -->
345- <div class =" flex-1 min-w-0 flex items-center gap-2 flex-wrap" >
346- <LinkBase
347- :to =" packageRoute(packageName, v.version)"
348- class =" font-mono text-sm after:absolute after:inset-0 after:content-['']"
349- :class =" v.deprecated ? 'text-red-700 dark:text-red-400' : ''"
350- :classicon =" v.deprecated ? 'i-lucide:octagon-alert' : undefined"
351- dir =" ltr"
352- >
353- {{ v.version }}
354- </LinkBase >
355- <div
356- v-if =" v.tags?.length"
357- class =" flex items-center gap-1 flex-wrap relative z-10"
358- >
378+ <div
379+ class =" flex items-center gap-3 px-4 ps-11 py-2.5 group relative"
380+ :class =" selectedChangelogVersion === v ? '' : 'hover:bg-bg-subtle'"
381+ >
382+ <!-- Version + badges -->
383+ <div class =" flex-1 min-w-0 flex items-center gap-2 flex-wrap" >
384+ <LinkBase
385+ :to =" packageRoute(packageName, v)"
386+ class =" font-mono text-sm after:absolute after:inset-0 after:content-['']"
387+ :class ="
388+ fullVersionMap?.get(v)?.deprecated
389+ ? 'text-red-700 dark:text-red-400'
390+ : ''
391+ "
392+ :classicon ="
393+ fullVersionMap?.get(v)?.deprecated
394+ ? 'i-lucide:octagon-alert'
395+ : undefined
396+ "
397+ dir =" ltr"
398+ >
399+ {{ v }}
400+ </LinkBase >
401+ <div
402+ v-if =" versionToTagsMap.get(v)?.length"
403+ class =" flex items-center gap-1 flex-wrap relative z-10"
404+ >
405+ <span
406+ v-for =" tag in versionToTagsMap.get(v)"
407+ :key =" tag"
408+ class =" text-4xs font-semibold uppercase tracking-wide"
409+ :class =" tag === 'latest' ? 'text-accent' : 'text-fg-subtle'"
410+ >
411+ {{ tag }}
412+ </span >
413+ </div >
359414 <span
360- v-for =" tag in v.tags"
361- :key =" tag"
362- class =" text-4xs font-semibold uppercase tracking-wide"
363- :class =" tag === 'latest' ? 'text-accent' : 'text-fg-subtle'"
415+ v-if =" fullVersionMap?.get(v)?.deprecated"
416+ 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"
417+ :title =" fullVersionMap.get(v)!.deprecated"
364418 >
365- {{ tag }}
419+ deprecated
366420 </span >
367421 </div >
368- <span
369- v-if =" v.deprecated"
370- 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"
371- :title =" v.deprecated"
372- >
373- deprecated
374- </span >
375- </div >
376422
377- <!-- Right side -->
378- <div class =" flex items-center gap-2 shrink-0 relative z-10" >
379- <!-- TODO(atriiy): changelog would be implemented later -->
380-
381- <!-- Divider -->
382- <span
383- v-if =" v.hasChangelog"
384- class =" w-px h-3.5 bg-border shrink-0 hidden sm:block"
385- aria-hidden =" true"
386- />
387-
388- <!-- Metadata: date + provenance -->
389- <DateTime
390- v-if =" v.time"
391- :datetime =" v.time"
392- class =" text-xs text-fg-subtle hidden sm:block"
393- year =" numeric"
394- month =" short"
395- day =" numeric"
396- />
397- <ProvenanceBadge
398- v-if =" v.hasProvenance"
399- :package-name =" packageName"
400- :version =" v.version"
401- compact
402- :linked =" false"
403- />
423+ <!-- Right side -->
424+ <div class =" flex items-center gap-2 shrink-0 relative z-10" >
425+ <!-- TODO(atriiy): changelog would be implemented later -->
426+
427+ <!-- Metadata: date + provenance -->
428+ <DateTime
429+ v-if =" getVersionTime(v)"
430+ :datetime =" getVersionTime(v)!"
431+ class =" text-xs text-fg-subtle hidden sm:block"
432+ year =" numeric"
433+ month =" short"
434+ day =" numeric"
435+ />
436+ <ProvenanceBadge
437+ v-if =" fullVersionMap?.get(v)?.hasProvenance"
438+ :package-name =" packageName"
439+ :version =" v"
440+ compact
441+ :linked =" false"
442+ />
443+ </div >
404444 </div >
405- </div >
406445
407- <!-- Mobile inline changelog (below the row, sm and up uses side panel) -->
408- <div
409- v-if =" v.hasChangelog"
410- class =" grid sm:hidden transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none"
411- :class ="
412- selectedChangelogVersion === v.version ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
413- "
414- >
415- <div class =" overflow-hidden" >
416- <div class =" changelog-body border-t border-border px-4 py-3 text-sm" >
417- {{ selectedChangelogVersion === v.version ? selectedChangelogContent : '' }}
446+ <!-- Mobile inline changelog (below the row, sm and up uses side panel) -->
447+ <div
448+ v-if =" v in mockChangelogs"
449+ class =" grid sm:hidden transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none"
450+ :class ="
451+ selectedChangelogVersion === v ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
452+ "
453+ >
454+ <div class =" overflow-hidden" >
455+ <div class =" changelog-body border-t border-border px-4 py-3 text-sm" >
456+ {{ selectedChangelogVersion === v ? selectedChangelogContent : '' }}
457+ </div >
418458 </div >
419459 </div >
420460 </div >
421- </div >
461+ </template >
422462 </div >
423463 </div >
424464 </div >
0 commit comments