Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
19 changes: 7 additions & 12 deletions app/components/SearchProviderToggle.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
const route = useRoute()
const router = useRouter()
const { searchProvider } = useSearchProvider()
const searchProviderValue = computed(() => {
const p = normalizeSearchParam(route.query.p)
if (p === 'npm' || searchProvider.value === 'npm') return 'npm'
return 'algolia'
})

const isOpen = shallowRef(false)
const toggleRef = useTemplateRef('toggleRef')
Expand Down Expand Up @@ -54,7 +49,7 @@ useEventListener('keydown', event => {
type="button"
role="menuitem"
class="w-full flex items-start gap-3 px-3 py-2.5 rounded-md text-start transition-colors hover:bg-bg-muted"
:class="[searchProviderValue !== 'algolia' ? 'bg-bg-muted' : '']"
:class="[searchProvider !== 'algolia' ? 'bg-bg-muted' : '']"
@click="
() => {
searchProvider = 'npm'
Expand All @@ -65,13 +60,13 @@ useEventListener('keydown', event => {
>
<span
class="i-simple-icons:npm w-4 h-4 mt-0.5 shrink-0"
:class="searchProviderValue !== 'algolia' ? 'text-accent' : 'text-fg-muted'"
:class="searchProvider !== 'algolia' ? 'text-accent' : 'text-fg-muted'"
aria-hidden="true"
/>
<div class="min-w-0 flex-1">
<div
class="text-sm font-medium"
:class="searchProviderValue !== 'algolia' ? 'text-fg' : 'text-fg-muted'"
:class="searchProvider !== 'algolia' ? 'text-fg' : 'text-fg-muted'"
>
{{ $t('settings.data_source.npm') }}
</div>
Expand All @@ -86,7 +81,7 @@ useEventListener('keydown', event => {
type="button"
role="menuitem"
class="w-full flex items-start gap-3 px-3 py-2.5 rounded-md text-start transition-colors hover:bg-bg-muted mt-1"
:class="[searchProviderValue === 'algolia' ? 'bg-bg-muted' : '']"
:class="[searchProvider === 'algolia' ? 'bg-bg-muted' : '']"
@click="
() => {
searchProvider = 'algolia'
Expand All @@ -97,13 +92,13 @@ useEventListener('keydown', event => {
>
<span
class="i-simple-icons:algolia w-4 h-4 mt-0.5 shrink-0"
:class="searchProviderValue === 'algolia' ? 'text-accent' : 'text-fg-muted'"
:class="searchProvider === 'algolia' ? 'text-accent' : 'text-fg-muted'"
aria-hidden="true"
/>
<div class="min-w-0 flex-1">
<div
class="text-sm font-medium"
:class="searchProviderValue === 'algolia' ? 'text-fg' : 'text-fg-muted'"
:class="searchProvider === 'algolia' ? 'text-fg' : 'text-fg-muted'"
>
{{ $t('settings.data_source.algolia') }}
</div>
Expand All @@ -115,7 +110,7 @@ useEventListener('keydown', event => {

<!-- Algolia attribution -->
<div
v-if="searchProviderValue === 'algolia'"
v-if="searchProvider === 'algolia'"
class="border-t border-border mx-1 mt-1 pt-2 pb-1"
>
<a
Expand Down
27 changes: 27 additions & 0 deletions app/composables/npm/search-utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
/**
* Bridge SSR payload when the resolved search provider on the client differs
* from the server default ('algolia'). Copies the SSR-cached data to the
* client's cache key so `useLazyAsyncData` hydrates without a refetch.
*
* Must be called at composable setup time (not inside an async callback).
*/
export function bridgeSearchSSRPayload(
prefix: string,
identifier: MaybeRefOrGetter<string>,
provider: MaybeRefOrGetter<string>,
): void {
if (import.meta.client) {
const nuxtApp = useNuxtApp()
const id = toValue(identifier)
const p = toValue(provider)

if (nuxtApp.isHydrating && id && p !== 'algolia') {
const ssrKey = `${prefix}:algolia:${id}`
const clientKey = `${prefix}:${p}:${id}`
if (nuxtApp.payload.data[ssrKey] && !nuxtApp.payload.data[clientKey]) {
nuxtApp.payload.data[clientKey] = nuxtApp.payload.data[ssrKey]
}
}
}
}

export function metaToSearchResult(meta: PackageMetaResponse): NpmSearchResult {
return {
package: {
Expand Down
14 changes: 6 additions & 8 deletions app/composables/npm/useOrgPackages.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { bridgeSearchSSRPayload } from './search-utils'

/**
* Fetch all packages for an npm organization.
*
Expand All @@ -6,17 +8,13 @@
* 3. Falls back to lightweight server-side package-meta lookups
*/
export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
const route = useRoute()
const { searchProvider } = useSearchProvider()
const searchProviderValue = computed(() => {
const p = normalizeSearchParam(route.query.p)
if (p === 'npm' || searchProvider.value === 'npm') return 'npm'
return 'algolia'
})
const { getPackagesByName } = useAlgoliaSearch()

bridgeSearchSSRPayload('org-packages', orgName, searchProvider)

const asyncData = useLazyAsyncData(
() => `org-packages:${searchProviderValue.value}:${toValue(orgName)}`,
() => `org-packages:${searchProvider.value}:${toValue(orgName)}`,
async ({ ssrContext }, { signal }) => {
const org = toValue(orgName)
if (!org) {
Expand Down Expand Up @@ -53,7 +51,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
}

// Fetch metadata + downloads from Algolia (single request via getObjects)
if (searchProviderValue.value === 'algolia') {
if (searchProvider.value === 'algolia') {
try {
const response = await getPackagesByName(packageNames)
if (response.objects.length > 0) {
Expand Down
10 changes: 8 additions & 2 deletions app/composables/npm/useSearch.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { bridgeSearchSSRPayload } from './search-utils'

function emptySearchPayload() {
return {
searchResponse: emptySearchResponse(),
Expand Down Expand Up @@ -136,6 +138,8 @@ export function useSearch(
suggestionsLoading.value = false
}

bridgeSearchSSRPayload('search', query, searchProvider)

const asyncData = useLazyAsyncData(
() => `search:${toValue(searchProvider)}:${toValue(query)}`,
async (_nuxtApp, { signal }) => {
Expand Down Expand Up @@ -462,12 +466,14 @@ export function useSearch(

if (import.meta.client && asyncData.data.value?.searchResponse.isStale) {
onMounted(() => {
asyncData.refresh()
void asyncData.refresh()
})
}

const { data: _data, ...rest } = asyncData

return {
...asyncData,
...rest,
data,
isLoadingMore,
hasMore,
Expand Down
26 changes: 13 additions & 13 deletions app/composables/npm/useUserPackages.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { bridgeSearchSSRPayload } from './search-utils'

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

Expand All @@ -19,13 +21,7 @@ const MAX_RESULTS = 250
* ```
*/
export function useUserPackages(username: MaybeRefOrGetter<string>) {
const route = useRoute()
const { searchProvider } = useSearchProvider()
const searchProviderValue = computed(() => {
const p = normalizeSearchParam(route.query.p)
if (p === 'npm' || searchProvider.value === 'npm') return 'npm'
return 'algolia'
})
// this is only used in npm path, but we need to extract it when the composable runs
const { $npmRegistry } = useNuxtApp()
const { searchByOwner } = useAlgoliaSearch()
Expand All @@ -35,7 +31,9 @@ export function useUserPackages(username: MaybeRefOrGetter<string>) {

/** Tracks which provider actually served the current data (may differ from
* searchProvider when Algolia returns empty and we fall through to npm) */
const activeProvider = shallowRef<'npm' | 'algolia'>(searchProviderValue.value)
const activeProvider = shallowRef<'npm' | 'algolia'>(searchProvider.value)

bridgeSearchSSRPayload('user-packages', username, searchProvider)

const cache = shallowRef<{
username: string
Expand All @@ -46,22 +44,22 @@ export function useUserPackages(username: MaybeRefOrGetter<string>) {
const isLoadingMore = shallowRef(false)

const asyncData = useLazyAsyncData(
() => `user-packages:${searchProviderValue.value}:${toValue(username)}`,
() => `user-packages:${searchProvider.value}:${toValue(username)}`,
async (_nuxtApp, { signal }) => {
const user = toValue(username)
if (!user) {
return emptySearchResponse()
}

const provider = searchProviderValue.value
const provider = searchProvider.value

// --- Algolia: fetch all at once ---
if (provider === 'algolia') {
try {
const response = await searchByOwner(user)

// Guard against stale response (user/provider changed during await)
if (user !== toValue(username) || provider !== searchProviderValue.value) {
if (user !== toValue(username) || provider !== searchProvider.value) {
return emptySearchResponse()
}

Expand Down Expand Up @@ -98,7 +96,7 @@ export function useUserPackages(username: MaybeRefOrGetter<string>) {
)

// Guard against stale response (user/provider changed during await)
if (user !== toValue(username) || provider !== searchProviderValue.value) {
if (user !== toValue(username) || provider !== searchProvider.value) {
return emptySearchResponse()
}

Expand Down Expand Up @@ -197,7 +195,7 @@ export function useUserPackages(username: MaybeRefOrGetter<string>) {
// asyncdata will automatically rerun due to key, but we need to reset cache/page
// when provider changes
watch(
() => searchProviderValue.value,
() => searchProvider.value,
newProvider => {
cache.value = null
currentPage.value = 1
Expand Down Expand Up @@ -231,8 +229,10 @@ export function useUserPackages(username: MaybeRefOrGetter<string>) {
return fetched < available && fetched < MAX_RESULTS
})

const { data: _data, ...rest } = asyncData

return {
...asyncData,
...rest,
/** Reactive package results */
data,
/** Whether currently loading more results */
Expand Down
18 changes: 6 additions & 12 deletions app/composables/useGlobalSearch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { normalizeSearchParam } from '#shared/utils/url'
import { debounce } from 'perfect-debounce'

// Pages that have their own local filter using ?q
Expand All @@ -9,11 +8,6 @@ const SEARCH_DEBOUNCE_MS = 100
export function useGlobalSearch(place: 'header' | 'content' = 'content') {
const { settings } = useSettings()
const { searchProvider } = useSearchProvider()
const searchProviderValue = computed(() => {
const p = normalizeSearchParam(route.query.p)
if (p === 'npm' || searchProvider.value === 'npm') return 'npm'
return 'algolia'
})

const router = useRouter()
const route = useRoute()
Expand All @@ -36,7 +30,7 @@ export function useGlobalSearch(place: 'header' | 'content' = 'content') {
// This is basically doing instant search as user types
watch(searchQuery, val => {
if (settings.value.instantSearch) {
commitSearchQuery(val)
void commitSearchQuery(val)
}
})

Expand All @@ -59,7 +53,7 @@ export function useGlobalSearch(place: 'header' | 'content' = 'content') {
}

if (route.name === 'search') {
router.replace({
void router.replace({
query: {
...route.query,
q: value || undefined,
Expand All @@ -68,7 +62,7 @@ export function useGlobalSearch(place: 'header' | 'content' = 'content') {
})
return
}
router.push({
void router.push({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these voids necessary?

name: 'search',
query: {
q: value,
Expand All @@ -87,7 +81,7 @@ export function useGlobalSearch(place: 'header' | 'content' = 'content') {
if (!settings.value.instantSearch) {
updateUrlQueryImpl(searchQuery.value, searchProvider.value)
} else {
updateUrlQuery.flush()
void updateUrlQuery.flush()
}
}

Expand All @@ -104,14 +98,14 @@ export function useGlobalSearch(place: 'header' | 'content' = 'content') {
if (!updateUrlQuery.isPending()) {
updateUrlQueryImpl(value, searchProvider.value)
}
updateUrlQuery(value, searchProvider.value)
void updateUrlQuery(value, searchProvider.value)
},
})

return {
model: searchQueryValue,
committedModel: committedSearchQuery,
provider: searchProviderValue,
provider: searchProvider,
startSearch: flushUpdateUrlQuery,
}
}
8 changes: 7 additions & 1 deletion app/composables/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useLocalStorage } from '@vueuse/core'
import { ACCENT_COLORS, type AccentColorId } from '#shared/utils/constants'
import type { LocaleObject } from '@nuxtjs/i18n'
import { BACKGROUND_THEMES } from '#shared/utils/constants'
import { normalizeSearchParam } from '#shared/utils/url'

type BackgroundThemeId = keyof typeof BACKGROUND_THEMES

Expand Down Expand Up @@ -181,9 +182,14 @@ export function useAccentColor() {
*/
export function useSearchProvider() {
const { settings } = useSettings()
const route = useRoute()

const searchProvider = computed({
get: () => settings.value.searchProvider,
get: () => {
const p = normalizeSearchParam(route.query.p)
if (p === 'npm' || p === 'algolia') return p
return settings.value.searchProvider
},
set: (value: SearchProvider) => {
settings.value.searchProvider = value
},
Expand Down
Loading
Loading