[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: tidy up and disable `extractAsyncDataHandlers`

+67 -128
+53 -65
app/components/PackageVersions.vue
··· 16 16 hasProvenance: boolean 17 17 } 18 18 19 - /** A dist-tag row */ 20 - interface TagRow { 21 - id: string 22 - tag: string 23 - primaryVersion: VersionDisplay 24 - /** Versions in this tag's channel (same major + same prerelease type) */ 25 - allVersions: VersionDisplay[] 26 - loading: boolean 27 - expanded: boolean 28 - } 29 - 30 - /** Unclaimed major version group */ 31 - interface MajorGroup { 32 - major: number 33 - versions: VersionDisplay[] 34 - expanded: boolean 35 - } 36 - 37 19 // Check if a version has provenance/attestations 38 20 function hasProvenance(version: PackumentVersion | undefined): boolean { 39 21 if (!version?.dist) return false ··· 77 59 return match ? match[1]!.toLowerCase() : '' 78 60 } 79 61 80 - // Cached full version list 81 - const allVersionsCache = ref<PackageVersionInfo[] | null>(null) 82 - const loadingVersions = ref(false) 83 - const hasLoadedAll = ref(false) 84 - 85 62 // Version to tag lookup 86 63 const versionToTag = computed(() => { 87 64 const map = new Map<string, string>() ··· 94 71 return map 95 72 }) 96 73 97 - // Dist-tag rows (stable structure) 98 - const tagRows = ref<TagRow[]>([]) 99 - 100 - // Unclaimed versions section 101 - const otherVersionsExpanded = ref(false) 102 - const otherMajorGroups = ref<MajorGroup[]>([]) 103 - 104 - // Initialize tag rows from props 105 - watchEffect(() => { 106 - const rows: TagRow[] = Object.entries(props.distTags) 74 + // Initial tag rows derived from props (SSR-safe) 75 + const initialTagRows = computed(() => { 76 + return Object.entries(props.distTags) 107 77 .map(([tag, version]) => { 108 78 const versionData = props.versions[version] 109 79 return { ··· 114 84 time: props.time[version], 115 85 tag, 116 86 hasProvenance: hasProvenance(versionData), 117 - }, 118 - allVersions: [], 119 - loading: false, 120 - expanded: false, 87 + } as VersionDisplay, 121 88 } 122 89 }) 123 90 .sort((a, b) => compareVersions(b.primaryVersion.version, a.primaryVersion.version)) 91 + }) 124 92 125 - tagRows.value = rows 126 - }) 93 + // Client-side state for expansion and loaded versions 94 + const expandedTags = ref<Set<string>>(new Set()) 95 + const tagVersions = ref<Map<string, VersionDisplay[]>>(new Map()) 96 + const loadingTags = ref<Set<string>>(new Set()) 97 + 98 + const otherVersionsExpanded = ref(false) 99 + const otherMajorGroups = ref<Array<{ major: number, versions: VersionDisplay[], expanded: boolean }>>([]) 100 + const otherVersionsLoading = ref(false) 101 + 102 + // Cached full version list 103 + const allVersionsCache = ref<PackageVersionInfo[] | null>(null) 104 + const loadingVersions = ref(false) 105 + const hasLoadedAll = ref(false) 127 106 128 107 // npm registry packument type (simplified) 129 108 interface NpmPackument { ··· 162 141 .map(version => ({ 163 142 version, 164 143 time: data.time[version], 165 - hasProvenance: false, // We don't have this info from the basic packument 144 + hasProvenance: false, 166 145 })) 167 146 .sort((a, b) => compareVersions(b.version, a.version)) 168 147 169 148 allVersionsCache.value = versions 149 + hasLoadedAll.value = true 170 150 return versions 171 151 } 172 152 finally { ··· 174 154 } 175 155 } 176 156 177 - // Process loaded versions - populate tag rows and find unclaimed versions 157 + // Process loaded versions 178 158 function processLoadedVersions(allVersions: PackageVersionInfo[]) { 179 159 const distTags = props.distTags 180 160 181 161 // For each tag, find versions in its channel (same major + same prerelease channel) 182 162 const claimedVersions = new Set<string>() 183 163 184 - for (const row of tagRows.value) { 164 + for (const row of initialTagRows.value) { 185 165 const tagVersion = distTags[row.tag] 186 166 if (!tagVersion) continue 187 167 ··· 202 182 hasProvenance: v.hasProvenance, 203 183 })) 204 184 205 - row.allVersions = channelVersions 185 + tagVersions.value.set(row.tag, channelVersions) 206 186 207 187 for (const v of channelVersions) { 208 188 claimedVersions.add(v.version) ··· 239 219 versions: byMajor.get(major)!, 240 220 expanded: false, 241 221 })) 242 - 243 - hasLoadedAll.value = true 244 222 } 245 223 246 224 // Expand a tag row 247 - async function expandTagRow(index: number) { 248 - const row = tagRows.value[index] 249 - if (!row) return 250 - 251 - if (row.expanded) { 252 - row.expanded = false 225 + async function expandTagRow(tag: string) { 226 + if (expandedTags.value.has(tag)) { 227 + expandedTags.value.delete(tag) 228 + expandedTags.value = new Set(expandedTags.value) 253 229 return 254 230 } 255 231 256 232 if (!hasLoadedAll.value) { 257 - row.loading = true 233 + loadingTags.value.add(tag) 234 + loadingTags.value = new Set(loadingTags.value) 258 235 try { 259 236 const allVersions = await loadAllVersions() 260 237 processLoadedVersions(allVersions) ··· 263 240 console.error('Failed to load versions:', error) 264 241 } 265 242 finally { 266 - row.loading = false 243 + loadingTags.value.delete(tag) 244 + loadingTags.value = new Set(loadingTags.value) 267 245 } 268 246 } 269 247 270 - row.expanded = true 248 + expandedTags.value.add(tag) 249 + expandedTags.value = new Set(expandedTags.value) 271 250 } 272 251 273 252 // Expand "Other versions" section ··· 278 257 } 279 258 280 259 if (!hasLoadedAll.value) { 260 + otherVersionsLoading.value = true 281 261 try { 282 262 const allVersions = await loadAllVersions() 283 263 processLoadedVersions(allVersions) ··· 285 265 catch (error) { 286 266 console.error('Failed to load versions:', error) 287 267 } 268 + finally { 269 + otherVersionsLoading.value = false 270 + } 288 271 } 289 272 290 273 otherVersionsExpanded.value = true ··· 298 281 } 299 282 } 300 283 284 + // Get versions for a tag (from loaded data or empty) 285 + function getTagVersions(tag: string): VersionDisplay[] { 286 + return tagVersions.value.get(tag) ?? [] 287 + } 288 + 301 289 function formatDate(dateStr: string): string { 302 290 return new Date(dateStr).toLocaleDateString('en-US', { 303 291 year: 'numeric', ··· 309 297 310 298 <template> 311 299 <section 312 - v-if="tagRows.length > 0" 300 + v-if="initialTagRows.length > 0" 313 301 aria-labelledby="versions-heading" 314 302 > 315 303 <h2 ··· 322 310 <div class="space-y-0.5"> 323 311 <!-- Dist-tag rows --> 324 312 <div 325 - v-for="(row, index) in tagRows" 313 + v-for="row in initialTagRows" 326 314 :key="row.id" 327 315 > 328 316 <div class="flex items-center gap-2"> 329 317 <!-- Expand button (only if there are more versions to show) --> 330 318 <button 331 - v-if="row.allVersions.length > 1 || !hasLoadedAll" 319 + v-if="getTagVersions(row.tag).length > 1 || !hasLoadedAll" 332 320 type="button" 333 321 class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors" 334 - :aria-expanded="row.expanded" 335 - :aria-label="row.expanded ? `Collapse ${row.tag}` : `Expand ${row.tag}`" 336 - @click="expandTagRow(index)" 322 + :aria-expanded="expandedTags.has(row.tag)" 323 + :aria-label="expandedTags.has(row.tag) ? `Collapse ${row.tag}` : `Expand ${row.tag}`" 324 + @click="expandTagRow(row.tag)" 337 325 > 338 326 <span 339 - v-if="row.loading" 327 + v-if="loadingTags.has(row.tag)" 340 328 class="i-carbon-rotate w-3 h-3 animate-spin" 341 329 /> 342 330 <span 343 331 v-else 344 332 class="w-3 h-3 transition-transform duration-200" 345 - :class="row.expanded ? 'i-carbon-chevron-down' : 'i-carbon-chevron-right'" 333 + :class="expandedTags.has(row.tag) ? 'i-carbon-chevron-down' : 'i-carbon-chevron-right'" 346 334 /> 347 335 </button> 348 336 <span ··· 383 371 384 372 <!-- Expanded versions --> 385 373 <div 386 - v-if="row.expanded && row.allVersions.length > 1" 374 + v-if="expandedTags.has(row.tag) && getTagVersions(row.tag).length > 1" 387 375 class="ml-4 pl-2 border-l border-border space-y-0.5" 388 376 > 389 377 <div 390 - v-for="v in row.allVersions.slice(1)" 378 + v-for="v in getTagVersions(row.tag).slice(1)" 391 379 :key="v.version" 392 380 class="flex items-center justify-between py-1 text-sm gap-2" 393 381 > ··· 434 422 > 435 423 <span class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors"> 436 424 <span 437 - v-if="loadingVersions && !hasLoadedAll" 425 + v-if="otherVersionsLoading" 438 426 class="i-carbon-rotate w-3 h-3 animate-spin" 439 427 /> 440 428 <span
+12 -55
app/composables/useNpmRegistry.ts
··· 4 4 SlimPackument, 5 5 NpmSearchResponse, 6 6 NpmDownloadCount, 7 - NpmDownloadRange, 8 7 } from '#shared/types' 9 8 10 9 const NPM_REGISTRY = 'https://registry.npmjs.org' 11 10 const NPM_API = 'https://api.npmjs.org' 12 11 13 - export function useNpmRegistry() { 14 - async function fetchPackage(name: string): Promise<Packument> { 15 - const encodedName = encodePackageName(name) 16 - return await $fetch<Packument>(`${NPM_REGISTRY}/${encodedName}`) 17 - } 12 + async function fetchNpmPackage(name: string): Promise<Packument> { 13 + const encodedName = encodePackageName(name) 14 + return await $fetch<Packument>(`${NPM_REGISTRY}/${encodedName}`) 15 + } 18 16 19 - async function searchPackages( 20 - query: string, 21 - options: { 22 - size?: number 23 - from?: number 24 - quality?: number 25 - popularity?: number 26 - maintenance?: number 27 - } = {}, 28 - ): Promise<NpmSearchResponse> { 29 - const params = new URLSearchParams() 30 - params.set('text', query) 31 - if (options.size) params.set('size', String(options.size)) 32 - if (options.from) params.set('from', String(options.from)) 33 - if (options.quality !== undefined) params.set('quality', String(options.quality)) 34 - if (options.popularity !== undefined) params.set('popularity', String(options.popularity)) 35 - if (options.maintenance !== undefined) params.set('maintenance', String(options.maintenance)) 36 - 37 - return await $fetch<NpmSearchResponse>(`${NPM_REGISTRY}/-/v1/search?${params.toString()}`) 38 - } 39 - 40 - async function fetchDownloads( 41 - packageName: string, 42 - period: 'last-day' | 'last-week' | 'last-month' | 'last-year' = 'last-week', 43 - ): Promise<NpmDownloadCount> { 44 - const encodedName = encodePackageName(packageName) 45 - return await $fetch<NpmDownloadCount>(`${NPM_API}/downloads/point/${period}/${encodedName}`) 46 - } 47 - 48 - async function fetchDownloadRange( 49 - packageName: string, 50 - period: 'last-week' | 'last-month' | 'last-year' = 'last-month', 51 - ): Promise<NpmDownloadRange> { 52 - const encodedName = encodePackageName(packageName) 53 - return await $fetch<NpmDownloadRange>(`${NPM_API}/downloads/range/${period}/${encodedName}`) 54 - } 55 - 56 - return { 57 - fetchPackage, 58 - searchPackages, 59 - fetchDownloads, 60 - fetchDownloadRange, 61 - } 17 + async function fetchNpmDownloads( 18 + packageName: string, 19 + period: 'last-day' | 'last-week' | 'last-month' | 'last-year' = 'last-week', 20 + ): Promise<NpmDownloadCount> { 21 + const encodedName = encodePackageName(packageName) 22 + return await $fetch<NpmDownloadCount>(`${NPM_API}/downloads/point/${period}/${encodedName}`) 62 23 } 63 24 64 25 function encodePackageName(name: string): string { ··· 134 95 } 135 96 136 97 export function usePackage(name: MaybeRefOrGetter<string>) { 137 - const registry = useNpmRegistry() 138 - 139 98 return useLazyAsyncData( 140 99 () => `package:${toValue(name)}`, 141 - () => registry.fetchPackage(toValue(name)).then(r => transformPackument(r)), 100 + () => fetchNpmPackage(toValue(name)).then(r => transformPackument(r)), 142 101 ) 143 102 } 144 103 ··· 146 105 name: MaybeRefOrGetter<string>, 147 106 period: MaybeRefOrGetter<'last-day' | 'last-week' | 'last-month' | 'last-year'> = 'last-week', 148 107 ) { 149 - const registry = useNpmRegistry() 150 - 151 108 return useLazyAsyncData( 152 109 () => `downloads:${toValue(name)}:${toValue(period)}`, 153 - () => registry.fetchDownloads(toValue(name), toValue(period)), 110 + () => fetchNpmDownloads(toValue(name), toValue(period)), 154 111 ) 155 112 } 156 113
+2 -7
app/pages/package/[...name].vue
··· 45 45 const { data: downloads } = usePackageDownloads(packageName, 'last-week') 46 46 47 47 // Fetch README for specific version if requested, otherwise latest 48 - const readmeUrl = computed(() => { 48 + const { data: readmeData } = useLazyFetch(() => { 49 49 const base = `/api/registry/readme/${packageName.value}` 50 50 const version = requestedVersion.value 51 51 return version ? `${base}/v/${version}` : base 52 - }) 53 - 54 - const { data: readmeData } = useLazyFetch(readmeUrl, { 55 - key: () => `readme:${packageName.value}:${requestedVersion.value ?? 'latest'}`, 56 - default: () => ({ html: '' }), 57 - }) 52 + }, { default: () => ({ html: '' }) }) 58 53 59 54 // Get the version to display (requested or latest) 60 55 const displayVersion = computed(() => {
-1
nuxt.config.ts
··· 49 49 }, 50 50 51 51 experimental: { 52 - extractAsyncDataHandlers: true, 53 52 viewTransition: true, 54 53 typedPages: true, 55 54 defaults: {