Skip to content

Commit 4b6228e

Browse files
committed
perf: init loading for basic data, and lazy full loading when expanding
1 parent c9a8036 commit 4b6228e

1 file changed

Lines changed: 148 additions & 108 deletions

File tree

app/pages/package/[[org]]/[name]/versions.vue

Lines changed: 148 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<script setup lang="ts">
2+
import { getVersions } from 'fast-npm-meta'
23
import {
34
buildVersionToTagsMap,
45
buildTaggedVersionRows,
56
getVersionGroupKey,
67
getVersionGroupLabel,
78
} from '~/utils/versions'
9+
import { fetchAllPackageVersions } from '~/utils/npm/api'
810
911
definePageMeta({
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))
5173
const tagRows = computed(() => buildTaggedVersionRows(distTags.value))
5274
53-
const versionByKey = computed(() => new Map(versionHistory.value.map(v => [v.version, v])))
54-
5575
function getVersionTime(version: string): string | undefined {
56-
return versionByKey.value.get(version)?.time
76+
return versionTimes.value[version]
5777
}
5878
5979
// ─── Version groups ───────────────────────────────────────────────────────────
6080
6181
const expandedGroups = ref(new Set<string>())
82+
const renderedGroups = ref(new Set<string>())
83+
const loadingGroup = ref<string | null>(null)
6284
6385
const 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('')
111142
function 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

Comments
 (0)