Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
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
81 changes: 81 additions & 0 deletions app/composables/npm/useUserPackages.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
import type { NpmSearchResponse } from '#shared/types'

/** Default page size for incremental loading (npm registry path) */
const PAGE_SIZE = 50 as const

/** npm search API practical limit for maintainer queries */
const MAX_RESULTS = 250

type RawNpmSearchResponse = Omit<NpmSearchResponse, 'isStale'> &
Partial<Pick<NpmSearchResponse, 'isStale'>>

interface UserPackagesPrefetchGlobal {
__NPMX_USER_PACKAGES_PREFETCH__?: Record<string, Promise<RawNpmSearchResponse | null> | undefined>
}

type UserPackagesPrefetchWindow = Window & UserPackagesPrefetchGlobal

function normalizeNpmSearchResponse(response: RawNpmSearchResponse): NpmSearchResponse {
return {
...response,
isStale: response.isStale ?? false,
}
}

async function getPrehydratedNpmUserPackages(username: string): Promise<NpmSearchResponse | null> {
if (!import.meta.client) return null

const prefetches = (window as UserPackagesPrefetchWindow).__NPMX_USER_PACKAGES_PREFETCH__
const response = await prefetches?.[username.toLowerCase()]
if (!response) return null

delete prefetches?.[username.toLowerCase()]
return normalizeNpmSearchResponse(response)
}

/**
* Fetch packages for a given npm user/maintainer.
*
Expand All @@ -30,6 +59,44 @@ export function useUserPackages(username: MaybeRefOrGetter<string>) {
const { $npmRegistry } = useNuxtApp()
const { searchByMaintainer } = useAlgoliaSearch()

onPrehydrate(el => {
let settings
try {
settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
} catch {
settings = {}
}
if (settings.searchProvider !== 'npm') return

const params = new URLSearchParams(window.location.search)
if (params.get('p') === 'npm') return

const prehydrateUsername =
el?.getAttribute('data-user-packages-username') ||
decodeURIComponent(window.location.pathname.split('/')[1] || '').replace(/^~/, '')
if (!prehydrateUsername) return

const prefetchWindow = window as typeof window & UserPackagesPrefetchGlobal
const prefetches = (prefetchWindow.__NPMX_USER_PACKAGES_PREFETCH__ ||= {})
if (prefetches[prehydrateUsername]) return

const searchParams = new URLSearchParams()
searchParams.set('text', `maintainer:${prehydrateUsername}`)
searchParams.set('size', '50')

prefetches[prehydrateUsername] = fetch(
`https://registry.npmjs.org/-/v1/search?${searchParams}`,
{
cache: 'force-cache',
},
)
.then(response => {
if (!response.ok) return null
return response.json()
})
.catch(() => null)
})

// --- Incremental loading state (npm path) ---
const currentPage = shallowRef(1)

Expand Down Expand Up @@ -87,6 +154,20 @@ export function useUserPackages(username: MaybeRefOrGetter<string>) {
cache.value = null
currentPage.value = 1

const prehydrated = await getPrehydratedNpmUserPackages(user)
if (prehydrated) {
if (user !== toValue(username) || provider !== searchProviderValue.value) {
return emptySearchResponse()
}

cache.value = {
username: user,
objects: prehydrated.objects,
total: prehydrated.total,
}
return prehydrated
}

const params = new URLSearchParams()
params.set('text', `maintainer:${user}`)
params.set('size', String(PAGE_SIZE))
Expand Down
7 changes: 4 additions & 3 deletions app/composables/useSettings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { RemovableRef } from '@vueuse/core'
import { useLocalStorage } from '@vueuse/core'
import { ACCENT_COLORS, type AccentColorId } from '#shared/utils/constants'
import type { LocaleObject } from '@nuxtjs/i18n'
import { useLocalStorage, useMounted } from '@vueuse/core'
import { ACCENT_COLORS, type AccentColorId } from '#shared/utils/constants'
import { BACKGROUND_THEMES } from '#shared/utils/constants'

type BackgroundThemeId = keyof typeof BACKGROUND_THEMES
Expand Down Expand Up @@ -197,9 +197,10 @@ export function useAccentColor() {
*/
export function useSearchProvider() {
const { settings } = useSettings()
const isMounted = useMounted()

const searchProvider = computed({
get: () => settings.value.searchProvider,
get: () => (isMounted.value ? settings.value.searchProvider : DEFAULT_SETTINGS.searchProvider),
set: (value: SearchProvider) => {
settings.value.searchProvider = value
},
Expand Down
7 changes: 5 additions & 2 deletions app/pages/~[username]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,10 @@ defineOgImage(
</script>

<template>
<main class="container flex-1 flex flex-col py-8 sm:py-12 w-full">
<main
class="container flex-1 flex flex-col py-8 sm:py-12 w-full"
:data-user-packages-username="username"
>
<!-- Header -->
<header class="mb-8 pb-8 border-b border-border">
<div class="flex flex-wrap items-center gap-4">
Expand Down Expand Up @@ -180,7 +183,7 @@ defineOgImage(

<!-- Loading state (only on initial load, not when we already have data) -->
<LoadingSpinner
v-if="status === 'pending' && packages.length === 0 && !error"
v-if="(status === 'idle' || status === 'pending') && packages.length === 0 && !error"
:text="$t('common.loading_packages')"
/>

Expand Down
Loading