- {{ $t('search.title') }} -
++ {{ $t('search.title') }} +
+{
+ {{ $t('settings.sections.search') }} +
++ {{ $t('settings.search_provider_description') }} +
+ ++ {{ + settings.searchProvider === 'algolia' + ? $t('settings.search_provider_algolia_description') + : $t('settings.search_provider_npm_description') + }} +
+ + + + {{ $t('search.algolia_disclaimer') }} + + +
{{ $t('settings.sections.language') }}
diff --git a/app/pages/~[username]/index.vue b/app/pages/~[username]/index.vue
index c4cab4920..f5e4c26e6 100644
--- a/app/pages/~[username]/index.vue
+++ b/app/pages/~[username]/index.vue
@@ -7,17 +7,6 @@ const router = useRouter()
const username = computed(() => route.params.username)
-// Infinite scroll state
-const pageSize = 50
-const maxResults = 250 // npm API hard limit
-const currentPage = shallowRef(1)
-
-// Get initial page from URL (for scroll restoration on reload)
-const initialPage = computed(() => {
- const p = Number.parseInt(normalizeSearchParam(route.query.page), 10)
- return Number.isNaN(p) ? 1 : Math.max(1, p)
-})
-
// Debounced URL update for page and filter/sort
const updateUrl = debounce((updates: { page?: number; filter?: string; sort?: string }) => {
router.replace({
@@ -38,48 +27,35 @@ const sortOption = shallowRef(
(normalizeSearchParam(route.query.sort) as SortOption) || 'downloads',
)
-// Track if we've loaded all results (one-way flag, doesn't reset)
-// Initialize to true if URL already has filter/sort params
-const hasLoadedAll = shallowRef(
- Boolean(route.query.q) ||
- (route.query.sort && normalizeSearchParam(route.query.sort) !== 'downloads'),
-)
-
// Update URL when filter/sort changes (debounced)
const debouncedUpdateUrl = debounce((filter: string, sort: string) => {
updateUrl({ filter, sort })
}, 300)
+// Load all results when user starts filtering/sorting (so client-side filter works on full set)
watch([filterText, sortOption], ([filter, sort]) => {
- // Once user interacts with filter/sort, load all results
- if (!hasLoadedAll.value && (filter !== '' || sort !== 'downloads')) {
- hasLoadedAll.value = true
+ if (filter !== '' || sort !== 'downloads') {
+ loadAll()
}
debouncedUpdateUrl(filter, sort)
})
-// Search for packages by this maintainer
-const searchQuery = computed(() => `maintainer:${username.value}`)
-
-// Request size: load all if user has interacted with filter/sort, otherwise paginate
-const requestSize = computed(() => (hasLoadedAll.value ? maxResults : pageSize * currentPage.value))
-
+// Fetch packages (composable manages pagination & provider dispatch internally)
const {
data: results,
status,
error,
isLoadingMore,
- hasMore: apiHasMore,
- fetchMore,
-} = useNpmSearch(searchQuery, () => ({
- size: requestSize.value,
-}))
+ hasMore,
+ loadMore,
+ loadAll,
+ pageSize,
+} = useUserPackages(username)
-// Initialize current page from URL on mount
-onMounted(() => {
- if (initialPage.value > 1) {
- currentPage.value = initialPage.value
- }
+// Get initial page from URL (for scroll restoration on reload)
+const initialPage = computed(() => {
+ const p = Number.parseInt(normalizeSearchParam(route.query.page), 10)
+ return Number.isNaN(p) ? 1 : Math.max(1, p)
})
// Get the base packages list
@@ -132,22 +108,6 @@ const totalWeeklyDownloads = computed(() =>
filteredAndSortedPackages.value.reduce((sum, pkg) => sum + (pkg.downloads?.weekly ?? 0), 0),
)
-// Check if there are potentially more results
-const hasMore = computed(() => {
- if (!results.value) return false
- // Don't show "load more" when we've already loaded all
- if (hasLoadedAll.value) return false
- // Use API's hasMore, but cap at maxResults
- if (!apiHasMore.value) return false
- return results.value.objects.length < maxResults
-})
-
-async function loadMore() {
- if (isLoadingMore.value || !hasMore.value) return
- currentPage.value++
- await fetchMore(requestSize.value)
-}
-
// Update URL when page changes from scrolling
function handlePageChange(page: number) {
updateUrl({ page, filter: filterText.value, sort: sortOption.value })
@@ -155,10 +115,8 @@ function handlePageChange(page: number) {
// Reset state when username changes
watch(username, () => {
- currentPage.value = 1
filterText.value = ''
sortOption.value = 'downloads'
- hasLoadedAll.value = false
})
useSeoMeta({
@@ -219,7 +177,7 @@ defineOgImageComponent('Default', {
@@ -260,7 +218,7 @@ defineOgImageComponent('Default', {
v-else
:results="filteredAndSortedPackages"
:has-more="hasMore"
- :is-loading="isLoadingMore || (status === 'pending' && currentPage > 1)"
+ :is-loading="isLoadingMore"
:page-size="pageSize"
:initial-page="initialPage"
@load-more="loadMore"
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index efbf68e70..6c917caa3 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -35,7 +35,7 @@
"claim_button": "Claim \"{name}\"",
"want_to_claim": "Want to claim this package name?",
"start_typing": "Start typing to search packages",
- "algolia_disclaimer": "Search by Algolia",
+ "algolia_disclaimer": "Powered by Algolia",
"exact_match": "exact",
"suggestion": {
"user": "user",
@@ -63,8 +63,15 @@
"sections": {
"appearance": "Appearance",
"display": "Display",
+ "search": "Data source",
"language": "Language"
},
+ "search_provider": "Data source",
+ "search_provider_description": "Choose where npmx gets package data for search results, org pages, and user pages. Individual package pages always use the npm registry directly.",
+ "search_provider_npm": "npm Registry",
+ "search_provider_npm_description": "Fetches package listings directly from the official npm registry. Authoritative, but can be slower for large result sets.",
+ "search_provider_algolia": "Algolia",
+ "search_provider_algolia_description": "Uses npm's Algolia index for faster package listings and search. Org and user pages load in a single request instead of many.",
"relative_dates": "Relative dates",
"include_types": "Include {'@'}types in install",
"include_types_description": "Add {'@'}types package to install commands for untyped packages",
diff --git a/lunaria/files/en-GB.json b/lunaria/files/en-GB.json
index f445873c6..d7ae467e3 100644
--- a/lunaria/files/en-GB.json
+++ b/lunaria/files/en-GB.json
@@ -35,7 +35,7 @@
"claim_button": "Claim \"{name}\"",
"want_to_claim": "Want to claim this package name?",
"start_typing": "Start typing to search packages",
- "algolia_disclaimer": "Search by Algolia",
+ "algolia_disclaimer": "Powered by Algolia",
"exact_match": "exact",
"suggestion": {
"user": "user",
@@ -63,8 +63,15 @@
"sections": {
"appearance": "Appearance",
"display": "Display",
+ "search": "Data source",
"language": "Language"
},
+ "search_provider": "Data source",
+ "search_provider_description": "Choose where npmx gets package data for search results, org pages, and user pages. Individual package pages always use the npm registry directly.",
+ "search_provider_npm": "npm Registry",
+ "search_provider_npm_description": "Fetches package listings directly from the official npm registry. Authoritative, but can be slower for large result sets.",
+ "search_provider_algolia": "Algolia",
+ "search_provider_algolia_description": "Uses npm's Algolia index for faster package listings and search. Org and user pages load in a single request instead of many.",
"relative_dates": "Relative dates",
"include_types": "Include {'@'}types in install",
"include_types_description": "Add {'@'}types package to install commands for untyped packages",
diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json
index efbf68e70..6c917caa3 100644
--- a/lunaria/files/en-US.json
+++ b/lunaria/files/en-US.json
@@ -35,7 +35,7 @@
"claim_button": "Claim \"{name}\"",
"want_to_claim": "Want to claim this package name?",
"start_typing": "Start typing to search packages",
- "algolia_disclaimer": "Search by Algolia",
+ "algolia_disclaimer": "Powered by Algolia",
"exact_match": "exact",
"suggestion": {
"user": "user",
@@ -63,8 +63,15 @@
"sections": {
"appearance": "Appearance",
"display": "Display",
+ "search": "Data source",
"language": "Language"
},
+ "search_provider": "Data source",
+ "search_provider_description": "Choose where npmx gets package data for search results, org pages, and user pages. Individual package pages always use the npm registry directly.",
+ "search_provider_npm": "npm Registry",
+ "search_provider_npm_description": "Fetches package listings directly from the official npm registry. Authoritative, but can be slower for large result sets.",
+ "search_provider_algolia": "Algolia",
+ "search_provider_algolia_description": "Uses npm's Algolia index for faster package listings and search. Org and user pages load in a single request instead of many.",
"relative_dates": "Relative dates",
"include_types": "Include {'@'}types in install",
"include_types_description": "Add {'@'}types package to install commands for untyped packages",
diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts
index cc270bd51..17774db29 100644
--- a/test/nuxt/a11y.spec.ts
+++ b/test/nuxt/a11y.spec.ts
@@ -138,6 +138,7 @@ import {
ProvenanceBadge,
Readme,
ReadmeTocDropdown,
+ SearchProviderToggle,
SearchSuggestionCard,
SettingsAccentColorPicker,
SettingsBgThemePicker,
@@ -159,6 +160,7 @@ import {
import HeaderAccountMenuServer from '~/components/Header/AccountMenu.server.vue'
import ToggleServer from '~/components/Settings/Toggle.server.vue'
import PackageDownloadAnalytics from '~/components/Package/DownloadAnalytics.vue'
+import SearchProviderToggleServer from '~/components/SearchProviderToggle.server.vue'
describe('component accessibility audits', () => {
describe('DateTime', () => {
@@ -2165,6 +2167,22 @@ describe('component accessibility audits', () => {
})
})
+ describe('SearchProviderToggle', () => {
+ it('should have no accessibility violations', async () => {
+ const component = await mountSuspended(SearchProviderToggle)
+ const results = await runAxe(component)
+ expect(results.violations).toEqual([])
+ })
+ })
+
+ describe('SearchProviderToggle.server', () => {
+ it('should have no accessibility violations', async () => {
+ const component = await mountSuspended(SearchProviderToggleServer)
+ const results = await runAxe(component)
+ expect(results.violations).toEqual([])
+ })
+ })
+
describe('Toggle.server', () => {
it('should have no accessibility violations', async () => {
const component = await mountSuspended(ToggleServer, {
From 192ef68e7fee61c33913dcc29e852a8b9ca7e788 Mon Sep 17 00:00:00 2001
From: Daniel Roe
Date: Sun, 8 Feb 2026 10:39:36 +0000
Subject: [PATCH 04/21] refactor: extract creds to runtime config
---
app/composables/npm/useAlgoliaSearch.ts | 11 +++++++----
nuxt.config.ts | 8 ++++++++
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/app/composables/npm/useAlgoliaSearch.ts b/app/composables/npm/useAlgoliaSearch.ts
index 418496f73..28383840b 100644
--- a/app/composables/npm/useAlgoliaSearch.ts
+++ b/app/composables/npm/useAlgoliaSearch.ts
@@ -7,14 +7,17 @@ import {
/**
* Algolia search client for npm packages.
- * Uses npm's public Algolia index (same as npmjs.com).
+ * Credentials and index name come from runtimeConfig.public.algolia.
*/
let _searchClient: LiteClient | null = null
+let _configuredAppId: string | null = null
function getAlgoliaClient(): LiteClient {
- if (!_searchClient) {
- // npm's public search-only Algolia credentials (same as npmjs.com uses)
- _searchClient = algoliasearch('OFCNCOG2CU', 'f54e21fa3a2a0160595bb058179bfb1e')
+ const { algolia } = useRuntimeConfig().public
+ // Re-create client if app ID changed (shouldn't happen, but be safe)
+ if (!_searchClient || _configuredAppId !== algolia.appId) {
+ _searchClient = algoliasearch(algolia.appId, algolia.apiKey)
+ _configuredAppId = algolia.appId
}
return _searchClient
}
diff --git a/nuxt.config.ts b/nuxt.config.ts
index a4baaf79d..7d512685e 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -33,6 +33,14 @@ export default defineNuxtConfig({
redisRestUrl: process.env.UPSTASH_KV_REST_API_URL || process.env.KV_REST_API_URL || '',
redisRestToken: process.env.UPSTASH_KV_REST_API_TOKEN || process.env.KV_REST_API_TOKEN || '',
},
+ public: {
+ // Algolia npm-search index (maintained by Algolia & jsDelivr, used by yarnpkg.com et al.)
+ algolia: {
+ appId: 'OFCNCOG2CU',
+ apiKey: 'f54e21fa3a2a0160595bb058179bfb1e',
+ indexName: 'npm-search',
+ },
+ },
},
devtools: { enabled: true },
From 8cd2aa02e259386f6248fa98f799f090af87b0fd Mon Sep 17 00:00:00 2001
From: Daniel Roe
Date: Sun, 8 Feb 2026 10:44:17 +0000
Subject: [PATCH 05/21] chore: bump algoliasearch version
---
package.json | 2 +-
pnpm-lock.yaml | 321 +++++++++++++++++++++++++++----------------------
2 files changed, 180 insertions(+), 143 deletions(-)
diff --git a/package.json b/package.json
index b4e5188dc..0b64e0c91 100644
--- a/package.json
+++ b/package.json
@@ -78,7 +78,7 @@
"@vueuse/nuxt": "14.2.0",
"@vueuse/router": "^14.2.0",
"@vueuse/shared": "14.2.0",
- "algoliasearch": "5.47.0",
+ "algoliasearch": "5.48.0",
"defu": "6.1.4",
"fast-npm-meta": "1.0.0",
"focus-trap": "^7.8.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4a3951925..4903d2079 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -123,8 +123,8 @@ importers:
specifier: 14.2.0
version: 14.2.0(vue@3.5.27(typescript@5.9.3))
algoliasearch:
- specifier: 5.47.0
- version: 5.47.0
+ specifier: 5.48.0
+ version: 5.48.0
defu:
specifier: 6.1.4
version: 6.1.4
@@ -343,60 +343,60 @@ importers:
packages:
- '@algolia/abtesting@1.13.0':
- resolution: {integrity: sha512-Zrqam12iorp3FjiKMXSTpedGYznZ3hTEOAr2oCxI8tbF8bS1kQHClyDYNq/eV0ewMNLyFkgZVWjaS+8spsOYiQ==}
+ '@algolia/abtesting@1.14.0':
+ resolution: {integrity: sha512-cZfj+1Z1dgrk3YPtNQNt0H9Rr67P8b4M79JjUKGS0d7/EbFbGxGgSu6zby5f22KXo3LT0LZa4O2c6VVbupJuDg==}
engines: {node: '>= 14.0.0'}
- '@algolia/client-abtesting@5.47.0':
- resolution: {integrity: sha512-aOpsdlgS9xTEvz47+nXmw8m0NtUiQbvGWNuSEb7fA46iPL5FxOmOUZkh8PREBJpZ0/H8fclSc7BMJCVr+Dn72w==}
+ '@algolia/client-abtesting@5.48.0':
+ resolution: {integrity: sha512-n17WSJ7vazmM6yDkWBAjY12J8ERkW9toOqNgQ1GEZu/Kc4dJDJod1iy+QP5T/UlR3WICgZDi/7a/VX5TY5LAPQ==}
engines: {node: '>= 14.0.0'}
- '@algolia/client-analytics@5.47.0':
- resolution: {integrity: sha512-EcF4w7IvIk1sowrO7Pdy4Ako7x/S8+nuCgdk6En+u5jsaNQM4rTT09zjBPA+WQphXkA2mLrsMwge96rf6i7Mow==}
+ '@algolia/client-analytics@5.48.0':
+ resolution: {integrity: sha512-v5bMZMEqW9U2l40/tTAaRyn4AKrYLio7KcRuHmLaJtxuJAhvZiE7Y62XIsF070juz4MN3eyvfQmI+y5+OVbZuA==}
engines: {node: '>= 14.0.0'}
- '@algolia/client-common@5.47.0':
- resolution: {integrity: sha512-Wzg5Me2FqgRDj0lFuPWFK05UOWccSMsIBL2YqmTmaOzxVlLZ+oUqvKbsUSOE5ud8Fo1JU7JyiLmEXBtgDKzTwg==}
+ '@algolia/client-common@5.48.0':
+ resolution: {integrity: sha512-7H3DgRyi7UByScc0wz7EMrhgNl7fKPDjKX9OcWixLwCj7yrRXDSIzwunykuYUUO7V7HD4s319e15FlJ9CQIIFQ==}
engines: {node: '>= 14.0.0'}
- '@algolia/client-insights@5.47.0':
- resolution: {integrity: sha512-Ci+cn/FDIsDxSKMRBEiyKrqybblbk8xugo6ujDN1GSTv9RIZxwxqZYuHfdLnLEwLlX7GB8pqVyqrUSlRnR+sJA==}
+ '@algolia/client-insights@5.48.0':
+ resolution: {integrity: sha512-tXmkB6qrIGAXrtRYHQNpfW0ekru/qymV02bjT0w5QGaGw0W91yT+53WB6dTtRRsIrgS30Al6efBvyaEosjZ5uw==}
engines: {node: '>= 14.0.0'}
- '@algolia/client-personalization@5.47.0':
- resolution: {integrity: sha512-gsLnHPZmWcX0T3IigkDL2imCNtsQ7dR5xfnwiFsb+uTHCuYQt+IwSNjsd8tok6HLGLzZrliSaXtB5mfGBtYZvQ==}
+ '@algolia/client-personalization@5.48.0':
+ resolution: {integrity: sha512-4tXEsrdtcBZbDF73u14Kb3otN+xUdTVGop1tBjict+Rc/FhsJQVIwJIcTrOJqmvhtBfc56Bu65FiVOnpAZCxcw==}
engines: {node: '>= 14.0.0'}
- '@algolia/client-query-suggestions@5.47.0':
- resolution: {integrity: sha512-PDOw0s8WSlR2fWFjPQldEpmm/gAoUgLigvC3k/jCSi/DzigdGX6RdC0Gh1RR1P8Cbk5KOWYDuL3TNzdYwkfDyA==}
+ '@algolia/client-query-suggestions@5.48.0':
+ resolution: {integrity: sha512-unzSUwWFpsDrO8935RhMAlyK0Ttua/5XveVIwzfjs5w+GVBsHgIkbOe8VbBJccMU/z1LCwvu1AY3kffuSLAR5Q==}
engines: {node: '>= 14.0.0'}
- '@algolia/client-search@5.47.0':
- resolution: {integrity: sha512-b5hlU69CuhnS2Rqgsz7uSW0t4VqrLMLTPbUpEl0QVz56rsSwr1Sugyogrjb493sWDA+XU1FU5m9eB8uH7MoI0g==}
+ '@algolia/client-search@5.48.0':
+ resolution: {integrity: sha512-RB9bKgYTVUiOcEb5bOcZ169jiiVW811dCsJoLT19DcbbFmU4QaK0ghSTssij35QBQ3SCOitXOUrHcGgNVwS7sQ==}
engines: {node: '>= 14.0.0'}
- '@algolia/ingestion@1.47.0':
- resolution: {integrity: sha512-WvwwXp5+LqIGISK3zHRApLT1xkuEk320/EGeD7uYy+K8WwDd5OjXnhjuXRhYr1685KnkvWkq1rQ/ihCJjOfHpQ==}
+ '@algolia/ingestion@1.48.0':
+ resolution: {integrity: sha512-rhoSoPu+TDzDpvpk3cY/pYgbeWXr23DxnAIH/AkN0dUC+GCnVIeNSQkLaJ+CL4NZ51cjLIjksrzb4KC5Xu+ktw==}
engines: {node: '>= 14.0.0'}
- '@algolia/monitoring@1.47.0':
- resolution: {integrity: sha512-j2EUFKAlzM0TE4GRfkDE3IDfkVeJdcbBANWzK16Tb3RHz87WuDfQ9oeEW6XiRE1/bEkq2xf4MvZesvSeQrZRDA==}
+ '@algolia/monitoring@1.48.0':
+ resolution: {integrity: sha512-aSe6jKvWt+8VdjOaq2ERtsXp9+qMXNJ3mTyTc1VMhNfgPl7ArOhRMRSQ8QBnY8ZL4yV5Xpezb7lAg8pdGrrulg==}
engines: {node: '>= 14.0.0'}
- '@algolia/recommend@5.47.0':
- resolution: {integrity: sha512-+kTSE4aQ1ARj2feXyN+DMq0CIDHJwZw1kpxIunedkmpWUg8k3TzFwWsMCzJVkF2nu1UcFbl7xsIURz3Q3XwOXA==}
+ '@algolia/recommend@5.48.0':
+ resolution: {integrity: sha512-p9tfI1bimAaZrdiVExL/dDyGUZ8gyiSHsktP1ZWGzt5hXpM3nhv4tSjyHtXjEKtA0UvsaHKwSfFE8aAAm1eIQA==}
engines: {node: '>= 14.0.0'}
- '@algolia/requester-browser-xhr@5.47.0':
- resolution: {integrity: sha512-Ja+zPoeSA2SDowPwCNRbm5Q2mzDvVV8oqxCQ4m6SNmbKmPlCfe30zPfrt9ho3kBHnsg37pGucwOedRIOIklCHw==}
+ '@algolia/requester-browser-xhr@5.48.0':
+ resolution: {integrity: sha512-XshyfpsQB7BLnHseMinp3fVHOGlTv6uEHOzNK/3XrEF9mjxoZAcdVfY1OCXObfwRWX5qXZOq8FnrndFd44iVsQ==}
engines: {node: '>= 14.0.0'}
- '@algolia/requester-fetch@5.47.0':
- resolution: {integrity: sha512-N6nOvLbaR4Ge+oVm7T4W/ea1PqcSbsHR4O58FJ31XtZjFPtOyxmnhgCmGCzP9hsJI6+x0yxJjkW5BMK/XI8OvA==}
+ '@algolia/requester-fetch@5.48.0':
+ resolution: {integrity: sha512-Q4XNSVQU89bKNAPuvzSYqTH9AcbOOiIo6AeYMQTxgSJ2+uvT78CLPMG89RIIloYuAtSfE07s40OLV50++l1Bbw==}
engines: {node: '>= 14.0.0'}
- '@algolia/requester-node-http@5.47.0':
- resolution: {integrity: sha512-z1oyLq5/UVkohVXNDEY70mJbT/sv/t6HYtCvCwNrOri6pxBJDomP9R83KOlwcat+xqBQEdJHjbrPh36f1avmZA==}
+ '@algolia/requester-node-http@5.48.0':
+ resolution: {integrity: sha512-ZgxV2+5qt3NLeUYBTsi6PLyHcENQWC0iFppFZekHSEDA2wcLdTUjnaJzimTEULHIvJuLRCkUs4JABdhuJktEag==}
engines: {node: '>= 14.0.0'}
'@alloc/quick-lru@5.2.0':
@@ -542,6 +542,10 @@ packages:
resolution: {integrity: sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==}
engines: {node: '>=6.9.0'}
+ '@babel/compat-data@7.29.0':
+ resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
+ engines: {node: '>=6.9.0'}
+
'@babel/core@7.29.0':
resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==}
engines: {node: '>=6.9.0'}
@@ -736,8 +740,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-async-generator-functions@7.28.6':
- resolution: {integrity: sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA==}
+ '@babel/plugin-transform-async-generator-functions@7.29.0':
+ resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -802,8 +806,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.28.6':
- resolution: {integrity: sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==}
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0':
+ resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
@@ -880,8 +884,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-modules-systemjs@7.28.5':
- resolution: {integrity: sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==}
+ '@babel/plugin-transform-modules-systemjs@7.29.0':
+ resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -892,8 +896,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-named-capturing-groups-regex@7.27.1':
- resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==}
+ '@babel/plugin-transform-named-capturing-groups-regex@7.29.0':
+ resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
@@ -964,8 +968,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
- '@babel/plugin-transform-regenerator@7.28.6':
- resolution: {integrity: sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==}
+ '@babel/plugin-transform-regenerator@7.29.0':
+ resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -1042,8 +1046,8 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
- '@babel/preset-env@7.28.6':
- resolution: {integrity: sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==}
+ '@babel/preset-env@7.29.0':
+ resolution: {integrity: sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
@@ -1806,10 +1810,18 @@ packages:
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
engines: {node: 20 || >=22}
+ '@isaacs/brace-expansion@5.0.1':
+ resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==}
+ engines: {node: 20 || >=22}
+
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
+ '@isaacs/cliui@9.0.0':
+ resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
+ engines: {node: '>=18'}
+
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
@@ -4634,8 +4646,8 @@ packages:
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
- algoliasearch@5.47.0:
- resolution: {integrity: sha512-AGtz2U7zOV4DlsuYV84tLp2tBbA7RPtLA44jbVH4TTpDcc1dIWmULjHSsunlhscbzDydnjuFlNhflR3nV4VJaQ==}
+ algoliasearch@5.48.0:
+ resolution: {integrity: sha512-aD8EQC6KEman6/S79FtPdQmB7D4af/etcRL/KwiKFKgAE62iU8c5PeEQvpvIcBPurC3O/4Lj78nOl7ZcoazqSw==}
engines: {node: '>= 14.0.0'}
alien-signals@3.1.2:
@@ -4770,8 +4782,8 @@ packages:
peerDependencies:
'@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
- babel-plugin-polyfill-corejs3@0.13.0:
- resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==}
+ babel-plugin-polyfill-corejs3@0.14.0:
+ resolution: {integrity: sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==}
peerDependencies:
'@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
@@ -5500,6 +5512,10 @@ packages:
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
engines: {node: '>=10.13.0'}
+ enhanced-resolve@5.19.0:
+ resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==}
+ engines: {node: '>=10.13.0'}
+
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@@ -6562,8 +6578,8 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
- jackspeak@4.1.1:
- resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
+ jackspeak@4.2.3:
+ resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==}
engines: {node: 20 || >=22}
jake@10.9.4:
@@ -7184,6 +7200,10 @@ packages:
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
engines: {node: 20 || >=22}
+ minimatch@10.1.2:
+ resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
+ engines: {node: 20 || >=22}
+
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -9709,89 +9729,89 @@ packages:
snapshots:
- '@algolia/abtesting@1.13.0':
+ '@algolia/abtesting@1.14.0':
dependencies:
- '@algolia/client-common': 5.47.0
- '@algolia/requester-browser-xhr': 5.47.0
- '@algolia/requester-fetch': 5.47.0
- '@algolia/requester-node-http': 5.47.0
+ '@algolia/client-common': 5.48.0
+ '@algolia/requester-browser-xhr': 5.48.0
+ '@algolia/requester-fetch': 5.48.0
+ '@algolia/requester-node-http': 5.48.0
- '@algolia/client-abtesting@5.47.0':
+ '@algolia/client-abtesting@5.48.0':
dependencies:
- '@algolia/client-common': 5.47.0
- '@algolia/requester-browser-xhr': 5.47.0
- '@algolia/requester-fetch': 5.47.0
- '@algolia/requester-node-http': 5.47.0
+ '@algolia/client-common': 5.48.0
+ '@algolia/requester-browser-xhr': 5.48.0
+ '@algolia/requester-fetch': 5.48.0
+ '@algolia/requester-node-http': 5.48.0
- '@algolia/client-analytics@5.47.0':
+ '@algolia/client-analytics@5.48.0':
dependencies:
- '@algolia/client-common': 5.47.0
- '@algolia/requester-browser-xhr': 5.47.0
- '@algolia/requester-fetch': 5.47.0
- '@algolia/requester-node-http': 5.47.0
+ '@algolia/client-common': 5.48.0
+ '@algolia/requester-browser-xhr': 5.48.0
+ '@algolia/requester-fetch': 5.48.0
+ '@algolia/requester-node-http': 5.48.0
- '@algolia/client-common@5.47.0': {}
+ '@algolia/client-common@5.48.0': {}
- '@algolia/client-insights@5.47.0':
+ '@algolia/client-insights@5.48.0':
dependencies:
- '@algolia/client-common': 5.47.0
- '@algolia/requester-browser-xhr': 5.47.0
- '@algolia/requester-fetch': 5.47.0
- '@algolia/requester-node-http': 5.47.0
+ '@algolia/client-common': 5.48.0
+ '@algolia/requester-browser-xhr': 5.48.0
+ '@algolia/requester-fetch': 5.48.0
+ '@algolia/requester-node-http': 5.48.0
- '@algolia/client-personalization@5.47.0':
+ '@algolia/client-personalization@5.48.0':
dependencies:
- '@algolia/client-common': 5.47.0
- '@algolia/requester-browser-xhr': 5.47.0
- '@algolia/requester-fetch': 5.47.0
- '@algolia/requester-node-http': 5.47.0
+ '@algolia/client-common': 5.48.0
+ '@algolia/requester-browser-xhr': 5.48.0
+ '@algolia/requester-fetch': 5.48.0
+ '@algolia/requester-node-http': 5.48.0
- '@algolia/client-query-suggestions@5.47.0':
+ '@algolia/client-query-suggestions@5.48.0':
dependencies:
- '@algolia/client-common': 5.47.0
- '@algolia/requester-browser-xhr': 5.47.0
- '@algolia/requester-fetch': 5.47.0
- '@algolia/requester-node-http': 5.47.0
+ '@algolia/client-common': 5.48.0
+ '@algolia/requester-browser-xhr': 5.48.0
+ '@algolia/requester-fetch': 5.48.0
+ '@algolia/requester-node-http': 5.48.0
- '@algolia/client-search@5.47.0':
+ '@algolia/client-search@5.48.0':
dependencies:
- '@algolia/client-common': 5.47.0
- '@algolia/requester-browser-xhr': 5.47.0
- '@algolia/requester-fetch': 5.47.0
- '@algolia/requester-node-http': 5.47.0
+ '@algolia/client-common': 5.48.0
+ '@algolia/requester-browser-xhr': 5.48.0
+ '@algolia/requester-fetch': 5.48.0
+ '@algolia/requester-node-http': 5.48.0
- '@algolia/ingestion@1.47.0':
+ '@algolia/ingestion@1.48.0':
dependencies:
- '@algolia/client-common': 5.47.0
- '@algolia/requester-browser-xhr': 5.47.0
- '@algolia/requester-fetch': 5.47.0
- '@algolia/requester-node-http': 5.47.0
+ '@algolia/client-common': 5.48.0
+ '@algolia/requester-browser-xhr': 5.48.0
+ '@algolia/requester-fetch': 5.48.0
+ '@algolia/requester-node-http': 5.48.0
- '@algolia/monitoring@1.47.0':
+ '@algolia/monitoring@1.48.0':
dependencies:
- '@algolia/client-common': 5.47.0
- '@algolia/requester-browser-xhr': 5.47.0
- '@algolia/requester-fetch': 5.47.0
- '@algolia/requester-node-http': 5.47.0
+ '@algolia/client-common': 5.48.0
+ '@algolia/requester-browser-xhr': 5.48.0
+ '@algolia/requester-fetch': 5.48.0
+ '@algolia/requester-node-http': 5.48.0
- '@algolia/recommend@5.47.0':
+ '@algolia/recommend@5.48.0':
dependencies:
- '@algolia/client-common': 5.47.0
- '@algolia/requester-browser-xhr': 5.47.0
- '@algolia/requester-fetch': 5.47.0
- '@algolia/requester-node-http': 5.47.0
+ '@algolia/client-common': 5.48.0
+ '@algolia/requester-browser-xhr': 5.48.0
+ '@algolia/requester-fetch': 5.48.0
+ '@algolia/requester-node-http': 5.48.0
- '@algolia/requester-browser-xhr@5.47.0':
+ '@algolia/requester-browser-xhr@5.48.0':
dependencies:
- '@algolia/client-common': 5.47.0
+ '@algolia/client-common': 5.48.0
- '@algolia/requester-fetch@5.47.0':
+ '@algolia/requester-fetch@5.48.0':
dependencies:
- '@algolia/client-common': 5.47.0
+ '@algolia/client-common': 5.48.0
- '@algolia/requester-node-http@5.47.0':
+ '@algolia/requester-node-http@5.48.0':
dependencies:
- '@algolia/client-common': 5.47.0
+ '@algolia/client-common': 5.48.0
'@alloc/quick-lru@5.2.0': {}
@@ -10079,6 +10099,8 @@ snapshots:
'@babel/compat-data@7.28.6': {}
+ '@babel/compat-data@7.29.0': {}
+
'@babel/core@7.29.0':
dependencies:
'@babel/code-frame': 7.29.0
@@ -10320,7 +10342,7 @@ snapshots:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-async-generator-functions@7.28.6(@babel/core@7.29.0)':
+ '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
@@ -10401,7 +10423,7 @@ snapshots:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.28.6(@babel/core@7.29.0)':
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
@@ -10483,7 +10505,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-modules-systemjs@7.28.5(@babel/core@7.29.0)':
+ '@babel/plugin-transform-modules-systemjs@7.29.0(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
@@ -10501,7 +10523,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.29.0)':
+ '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
@@ -10581,7 +10603,7 @@ snapshots:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
- '@babel/plugin-transform-regenerator@7.28.6(@babel/core@7.29.0)':
+ '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
@@ -10659,9 +10681,9 @@ snapshots:
'@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0)
'@babel/helper-plugin-utils': 7.28.6
- '@babel/preset-env@7.28.6(@babel/core@7.29.0)':
+ '@babel/preset-env@7.29.0(@babel/core@7.29.0)':
dependencies:
- '@babel/compat-data': 7.28.6
+ '@babel/compat-data': 7.29.0
'@babel/core': 7.29.0
'@babel/helper-compilation-targets': 7.28.6
'@babel/helper-plugin-utils': 7.28.6
@@ -10676,7 +10698,7 @@ snapshots:
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0)
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
- '@babel/plugin-transform-async-generator-functions': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0)
'@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0)
@@ -10687,7 +10709,7 @@ snapshots:
'@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0)
'@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0)
- '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0)
'@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0)
@@ -10700,9 +10722,9 @@ snapshots:
'@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0)
- '@babel/plugin-transform-modules-systemjs': 7.28.5(@babel/core@7.29.0)
+ '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0)
'@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0)
- '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.29.0)
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0)
'@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0)
@@ -10714,7 +10736,7 @@ snapshots:
'@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0)
- '@babel/plugin-transform-regenerator': 7.28.6(@babel/core@7.29.0)
+ '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0)
'@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0)
@@ -10728,7 +10750,7 @@ snapshots:
'@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0)
'@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0)
babel-plugin-polyfill-corejs2: 0.4.15(@babel/core@7.29.0)
- babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0)
+ babel-plugin-polyfill-corejs3: 0.14.0(@babel/core@7.29.0)
babel-plugin-polyfill-regenerator: 0.6.6(@babel/core@7.29.0)
core-js-compat: 3.48.0
semver: 6.3.1
@@ -11329,6 +11351,10 @@ snapshots:
dependencies:
'@isaacs/balanced-match': 4.0.1
+ '@isaacs/brace-expansion@5.0.1':
+ dependencies:
+ '@isaacs/balanced-match': 4.0.1
+
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -11338,6 +11364,8 @@ snapshots:
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
+ '@isaacs/cliui@9.0.0': {}
+
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.2
@@ -14686,22 +14714,22 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
- algoliasearch@5.47.0:
- dependencies:
- '@algolia/abtesting': 1.13.0
- '@algolia/client-abtesting': 5.47.0
- '@algolia/client-analytics': 5.47.0
- '@algolia/client-common': 5.47.0
- '@algolia/client-insights': 5.47.0
- '@algolia/client-personalization': 5.47.0
- '@algolia/client-query-suggestions': 5.47.0
- '@algolia/client-search': 5.47.0
- '@algolia/ingestion': 1.47.0
- '@algolia/monitoring': 1.47.0
- '@algolia/recommend': 5.47.0
- '@algolia/requester-browser-xhr': 5.47.0
- '@algolia/requester-fetch': 5.47.0
- '@algolia/requester-node-http': 5.47.0
+ algoliasearch@5.48.0:
+ dependencies:
+ '@algolia/abtesting': 1.14.0
+ '@algolia/client-abtesting': 5.48.0
+ '@algolia/client-analytics': 5.48.0
+ '@algolia/client-common': 5.48.0
+ '@algolia/client-insights': 5.48.0
+ '@algolia/client-personalization': 5.48.0
+ '@algolia/client-query-suggestions': 5.48.0
+ '@algolia/client-search': 5.48.0
+ '@algolia/ingestion': 1.48.0
+ '@algolia/monitoring': 1.48.0
+ '@algolia/recommend': 5.48.0
+ '@algolia/requester-browser-xhr': 5.48.0
+ '@algolia/requester-fetch': 5.48.0
+ '@algolia/requester-node-http': 5.48.0
alien-signals@3.1.2: {}
@@ -14853,14 +14881,14 @@ snapshots:
babel-plugin-polyfill-corejs2@0.4.15(@babel/core@7.29.0):
dependencies:
- '@babel/compat-data': 7.28.6
+ '@babel/compat-data': 7.29.0
'@babel/core': 7.29.0
'@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0)
semver: 6.3.1
transitivePeerDependencies:
- supports-color
- babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0):
+ babel-plugin-polyfill-corejs3@0.14.0(@babel/core@7.29.0):
dependencies:
'@babel/core': 7.29.0
'@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0)
@@ -15643,6 +15671,11 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
+ enhanced-resolve@5.19.0:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.0
+
entities@4.5.0: {}
entities@6.0.1: {}
@@ -16370,8 +16403,8 @@ snapshots:
glob@11.1.0:
dependencies:
foreground-child: 3.3.1
- jackspeak: 4.1.1
- minimatch: 10.1.1
+ jackspeak: 4.2.3
+ minimatch: 10.1.2
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 2.0.1
@@ -17042,9 +17075,9 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
- jackspeak@4.1.1:
+ jackspeak@4.2.3:
dependencies:
- '@isaacs/cliui': 8.0.2
+ '@isaacs/cliui': 9.0.0
jake@10.9.4:
dependencies:
@@ -17800,6 +17833,10 @@ snapshots:
dependencies:
'@isaacs/brace-expansion': 5.0.0
+ minimatch@10.1.2:
+ dependencies:
+ '@isaacs/brace-expansion': 5.0.1
+
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
@@ -21067,7 +21104,7 @@ snapshots:
acorn-import-phases: 1.0.4(acorn@8.15.0)
browserslist: 4.28.1
chrome-trace-event: 1.0.4
- enhanced-resolve: 5.18.4
+ enhanced-resolve: 5.19.0
es-module-lexer: 2.0.0
eslint-scope: 5.1.1
events: 3.3.0
@@ -21167,7 +21204,7 @@ snapshots:
dependencies:
'@apideck/better-ajv-errors': 0.3.6(ajv@8.17.1)
'@babel/core': 7.29.0
- '@babel/preset-env': 7.28.6(@babel/core@7.29.0)
+ '@babel/preset-env': 7.29.0(@babel/core@7.29.0)
'@babel/runtime': 7.28.6
'@rollup/plugin-babel': 5.3.1(@babel/core@7.29.0)(rollup@2.79.2)
'@rollup/plugin-node-resolve': 15.3.1(rollup@2.79.2)
From 8296a24ed594c59efb600be003bebc1ebfe202f7 Mon Sep 17 00:00:00 2001
From: Daniel Roe
Date: Sun, 8 Feb 2026 10:49:19 +0000
Subject: [PATCH 06/21] fix: refine and improve
---
.../SearchProviderToggle.client.vue | 11 +-
app/composables/npm/useAlgoliaSearch.ts | 170 ++++++++++--------
app/composables/npm/useNpmSearch.ts | 2 +-
app/composables/npm/useOrgPackages.ts | 4 +-
app/composables/npm/useUserPackages.ts | 21 ++-
app/pages/search.vue | 12 --
6 files changed, 119 insertions(+), 101 deletions(-)
diff --git a/app/components/SearchProviderToggle.client.vue b/app/components/SearchProviderToggle.client.vue
index 77aa878dc..4cac0e975 100644
--- a/app/components/SearchProviderToggle.client.vue
+++ b/app/components/SearchProviderToggle.client.vue
@@ -17,16 +17,15 @@ useEventListener('keydown', event => {
-
+ />
{
- const client = getAlgoliaClient()
-
- const { results } = await client.search([
- {
- indexName: 'npm-search',
- params: {
- query,
- offset: options.from,
- length: options.size,
- filters: options.filters || '',
- analyticsTags: ['npmx.dev'],
- attributesToRetrieve: ATTRIBUTES_TO_RETRIEVE,
- attributesToHighlight: [],
- },
- },
- ])
-
- const response = results[0] as SearchResponse
-
- return {
- isStale: false,
- objects: response.hits.map(hitToSearchResult),
- total: response.nbHits!,
- time: new Date().toISOString(),
- }
-}
-
-/**
- * Fetch all packages in an Algolia scope (org or user).
- * Uses facet filters for efficient server-side filtering.
+ * Composable that provides Algolia search functions for npm packages.
*
- * For orgs: filters by `owner.name:orgname` which matches scoped packages.
- * For users: filters by `owner.name:username` which matches maintainer.
+ * Must be called during component setup (or inside another composable)
+ * because it reads from `useRuntimeConfig()`. The returned functions
+ * are safe to call at any time (event handlers, async callbacks, etc.).
*/
-export async function searchAlgoliaByOwner(
- ownerName: string,
- options: { maxResults?: number } = {},
-): Promise {
- const client = getAlgoliaClient()
- const max = options.maxResults ?? 1000
-
- const allHits: AlgoliaHit[] = []
- let offset = 0
- const batchSize = 200
-
- // Algolia supports up to 1000 results per query with offset/length pagination
- while (offset < max) {
- const length = Math.min(batchSize, max - offset)
-
+export function useAlgoliaSearch() {
+ const { algolia } = useRuntimeConfig().public
+ const client = getOrCreateClient(algolia.appId, algolia.apiKey)
+ const indexName = algolia.indexName
+
+ /**
+ * Search npm packages via Algolia.
+ * Returns results in the same NpmSearchResponse format as the npm registry API.
+ */
+ async function search(
+ query: string,
+ options: AlgoliaSearchOptions = {},
+ ): Promise {
const { results } = await client.search([
{
- indexName: 'npm-search',
+ indexName,
params: {
- query: '',
- offset,
- length,
- filters: `owner.name:${ownerName}`,
+ query,
+ offset: options.from,
+ length: options.size,
+ filters: options.filters || '',
analyticsTags: ['npmx.dev'],
attributesToRetrieve: ATTRIBUTES_TO_RETRIEVE,
attributesToHighlight: [],
@@ -196,21 +156,77 @@ export async function searchAlgoliaByOwner(
},
])
- const response = results[0] as SearchResponse
- allHits.push(...response.hits)
+ const response = results[0] as SearchResponse | undefined
+ if (!response) {
+ return { isStale: false, objects: [], total: 0, time: new Date().toISOString() }
+ }
+
+ return {
+ isStale: false,
+ objects: response.hits.map(hitToSearchResult),
+ total: response.nbHits ?? 0,
+ time: new Date().toISOString(),
+ }
+ }
+
+ /**
+ * Fetch all packages for an Algolia owner (org or user).
+ * Uses `owner.name` filter for efficient server-side filtering.
+ */
+ async function searchByOwner(
+ ownerName: string,
+ options: { maxResults?: number } = {},
+ ): Promise {
+ const max = options.maxResults ?? 1000
+
+ const allHits: AlgoliaHit[] = []
+ let offset = 0
+ const batchSize = 200
+
+ // Algolia supports up to 1000 results per query with offset/length pagination
+ while (offset < max) {
+ const length = Math.min(batchSize, max - offset)
+
+ const { results } = await client.search([
+ {
+ indexName,
+ params: {
+ query: '',
+ offset,
+ length,
+ filters: `owner.name:${ownerName}`,
+ analyticsTags: ['npmx.dev'],
+ attributesToRetrieve: ATTRIBUTES_TO_RETRIEVE,
+ attributesToHighlight: [],
+ },
+ },
+ ])
+
+ const response = results[0] as SearchResponse | undefined
+ if (!response) break
- // If we got fewer than requested, we've exhausted all results
- if (response.hits.length < length || allHits.length >= response.nbHits!) {
- break
+ allHits.push(...response.hits)
+
+ // If we got fewer than requested, we've exhausted all results
+ if (response.hits.length < length || allHits.length >= (response.nbHits ?? 0)) {
+ break
+ }
+
+ offset += length
}
- offset += length
+ return {
+ isStale: false,
+ objects: allHits.map(hitToSearchResult),
+ total: allHits.length,
+ time: new Date().toISOString(),
+ }
}
return {
- isStale: false,
- objects: allHits.map(hitToSearchResult),
- total: allHits.length,
- time: new Date().toISOString(),
+ /** Search packages by text query */
+ search,
+ /** Fetch all packages for an owner (org or user) */
+ searchByOwner,
}
}
diff --git a/app/composables/npm/useNpmSearch.ts b/app/composables/npm/useNpmSearch.ts
index 6cd1067d9..2a0076183 100644
--- a/app/composables/npm/useNpmSearch.ts
+++ b/app/composables/npm/useNpmSearch.ts
@@ -6,7 +6,6 @@ import type {
MinimalPackument,
} from '#shared/types'
import type { SearchProvider } from '~/composables/useSettings'
-import { searchAlgolia } from './useAlgoliaSearch'
/**
* Convert packument to search result format for display
@@ -58,6 +57,7 @@ export function useNpmSearch(
) {
const { $npmRegistry } = useNuxtApp()
const { searchProvider } = useSearchProvider()
+ const { search: searchAlgolia } = useAlgoliaSearch()
// Client-side cache
const cache = shallowRef<{
diff --git a/app/composables/npm/useOrgPackages.ts b/app/composables/npm/useOrgPackages.ts
index 4849de4f5..23da7732d 100644
--- a/app/composables/npm/useOrgPackages.ts
+++ b/app/composables/npm/useOrgPackages.ts
@@ -1,7 +1,6 @@
import type { NuxtApp } from '#app'
import type { NpmSearchResponse, NpmSearchResult, MinimalPackument } from '#shared/types'
import { emptySearchResponse, packumentToSearchResult } from './useNpmSearch'
-import { searchAlgoliaByOwner } from './useAlgoliaSearch'
import { mapWithConcurrency } from '#shared/utils/async'
/**
@@ -86,6 +85,7 @@ async function fetchBulkDownloads(
*/
export function useOrgPackages(orgName: MaybeRefOrGetter) {
const { searchProvider } = useSearchProvider()
+ const { searchByOwner } = useAlgoliaSearch()
const asyncData = useLazyAsyncData(
() => `org-packages:${searchProvider.value}:${toValue(orgName)}`,
@@ -98,7 +98,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter) {
// --- Algolia fast path ---
if (searchProvider.value === 'algolia') {
try {
- return await searchAlgoliaByOwner(org)
+ return await searchByOwner(org)
} catch {
// Fall through to npm registry path on Algolia failure
}
diff --git a/app/composables/npm/useUserPackages.ts b/app/composables/npm/useUserPackages.ts
index 0015369f4..9477b3904 100644
--- a/app/composables/npm/useUserPackages.ts
+++ b/app/composables/npm/useUserPackages.ts
@@ -1,6 +1,5 @@
import type { NpmSearchResponse, NpmSearchResult } from '#shared/types'
import { emptySearchResponse } from './useNpmSearch'
-import { searchAlgoliaByOwner } from './useAlgoliaSearch'
/** Default page size for incremental loading (npm registry path) */
const PAGE_SIZE = 50 as const
@@ -25,6 +24,7 @@ const MAX_RESULTS = 250
export function useUserPackages(username: MaybeRefOrGetter) {
const { searchProvider } = useSearchProvider()
const { $npmRegistry } = useNuxtApp()
+ const { searchByOwner } = useAlgoliaSearch()
// --- Incremental loading state (npm path) ---
const currentPage = shallowRef(1)
@@ -50,7 +50,13 @@ export function useUserPackages(username: MaybeRefOrGetter) {
// --- Algolia: fetch all at once ---
if (provider === 'algolia') {
try {
- const response = await searchAlgoliaByOwner(user)
+ const response = await searchByOwner(user)
+
+ // Guard against stale response (user/provider changed during await)
+ if (user !== toValue(username) || provider !== searchProvider.value) {
+ return emptySearchResponse
+ }
+
cache.value = {
username: user,
objects: response.objects,
@@ -76,6 +82,11 @@ export function useUserPackages(username: MaybeRefOrGetter) {
60,
)
+ // Guard against stale response (user/provider changed during await)
+ if (user !== toValue(username) || provider !== searchProvider.value) {
+ return emptySearchResponse
+ }
+
cache.value = {
username: user,
objects: response.objects,
@@ -89,7 +100,8 @@ export function useUserPackages(username: MaybeRefOrGetter) {
// --- Fetch more (npm path only) ---
async function fetchMore(): Promise {
const user = toValue(username)
- if (!user || searchProvider.value !== 'npm') return
+ const provider = searchProvider.value
+ if (!user || provider !== 'npm') return
if (cache.value && cache.value.username !== user) {
cache.value = null
@@ -119,6 +131,9 @@ export function useUserPackages(username: MaybeRefOrGetter) {
60,
)
+ // Guard against stale response
+ if (user !== toValue(username) || provider !== searchProvider.value) return
+
if (cache.value && cache.value.username === user) {
const existingNames = new Set(cache.value.objects.map(obj => obj.package.name))
const newObjects = response.objects.filter(obj => !existingNames.has(obj.package.name))
diff --git a/app/pages/search.vue b/app/pages/search.vue
index 8edc77caf..37c5e6ebb 100644
--- a/app/pages/search.vue
+++ b/app/pages/search.vue
@@ -9,9 +9,6 @@ import { normalizeSearchParam } from '#shared/utils/url'
const route = useRoute()
const router = useRouter()
-// Search provider
-const { isAlgolia } = useSearchProvider()
-
// Preferences (persisted to localStorage)
const {
viewMode,
@@ -760,15 +757,6 @@ defineOgImageComponent('Default', {
{{
$t('search.updating')
}}
-
- {{ $t('search.algolia_disclaimer') }}
-
Date: Sun, 8 Feb 2026 10:59:23 +0000
Subject: [PATCH 07/21] fix: speed up loading of org/user suggestions
---
app/pages/search.vue | 66 ++++++++++++++++++++++++++++++++------------
1 file changed, 49 insertions(+), 17 deletions(-)
diff --git a/app/pages/search.vue b/app/pages/search.vue
index 37c5e6ebb..c18611b1e 100644
--- a/app/pages/search.vue
+++ b/app/pages/search.vue
@@ -316,14 +316,13 @@ interface ValidatedSuggestion {
/** Cache for existence checks to avoid repeated API calls */
const existenceCache = ref>({})
-interface NpmSearchResponse {
- total: number
- objects: Array<{ package: { name: string } }>
-}
+const { search: algoliaSearch } = useAlgoliaSearch()
+const { isAlgolia } = useSearchProvider()
/**
- * Check if an org exists by searching for packages with @orgname scope
- * Uses the search API which has CORS enabled
+ * Check if an org exists by searching for scoped packages (@orgname/...).
+ * When Algolia is active, searches for `@name/` scoped packages via text query.
+ * Falls back to npm registry search API otherwise.
*/
async function checkOrgExists(name: string): Promise {
const cacheKey = `org:${name.toLowerCase()}`
@@ -333,12 +332,24 @@ async function checkOrgExists(name: string): Promise {
}
existenceCache.value[cacheKey] = 'pending'
try {
- // Search for packages in the @org scope
- const response = await $fetch(`${NPM_REGISTRY}/-/v1/search`, {
- query: { text: `@${name}`, size: 5 },
- })
- // Verify at least one result actually starts with @orgname/
const scopePrefix = `@${name.toLowerCase()}/`
+
+ if (isAlgolia.value) {
+ // Algolia: search for scoped packages — use the scope as a text query
+ // and verify a result actually starts with @name/
+ const response = await algoliaSearch(`@${name}`, { size: 5 })
+ const exists = response.objects.some(obj =>
+ obj.package.name.toLowerCase().startsWith(scopePrefix),
+ )
+ existenceCache.value[cacheKey] = exists
+ return exists
+ }
+
+ // npm registry: search for packages in the @org scope
+ const response = await $fetch<{ total: number; objects: Array<{ package: { name: string } }> }>(
+ `${NPM_REGISTRY}/-/v1/search`,
+ { query: { text: `@${name}`, size: 5 } },
+ )
const exists = response.objects.some(obj =>
obj.package.name.toLowerCase().startsWith(scopePrefix),
)
@@ -351,8 +362,10 @@ async function checkOrgExists(name: string): Promise {
}
/**
- * Check if a user exists by searching for packages they maintain
- * Uses the search API which has CORS enabled
+ * Check if a user exists by searching for packages they maintain.
+ * Always uses the npm registry `maintainer:` search because Algolia's
+ * `owner.name` field represents the org/account, not individual maintainers,
+ * and cannot reliably distinguish users from orgs.
*/
async function checkUserExists(name: string): Promise {
const cacheKey = `user:${name.toLowerCase()}`
@@ -418,8 +431,8 @@ const parsedQuery = computed(() => {
const validatedSuggestions = ref([])
const suggestionsLoading = shallowRef(false)
-/** Debounced function to validate suggestions */
-const validateSuggestions = debounce(async (parsed: ParsedQuery) => {
+/** Validate suggestions (check org/user existence) */
+async function validateSuggestionsImpl(parsed: ParsedQuery) {
if (!parsed.type || !parsed.name) {
validatedSuggestions.value = []
return
@@ -458,17 +471,36 @@ const validateSuggestions = debounce(async (parsed: ParsedQuery) => {
}
validatedSuggestions.value = suggestions
-}, 200)
+}
+
+// Debounce lightly for npm (extra API calls are slower), skip debounce for Algolia (fast)
+const validateSuggestionsDebounced = debounce(validateSuggestionsImpl, 100)
// Validate suggestions when query changes
watch(
parsedQuery,
parsed => {
- validateSuggestions(parsed)
+ if (isAlgolia.value) {
+ // Algolia existence checks are fast - fire immediately
+ validateSuggestionsImpl(parsed)
+ } else {
+ validateSuggestionsDebounced(parsed)
+ }
},
{ immediate: true },
)
+// Re-validate suggestions and clear caches when provider changes
+watch(isAlgolia, () => {
+ // Clear existence cache since results may differ between providers
+ existenceCache.value = {}
+ // Re-validate with current query
+ const parsed = parsedQuery.value
+ if (parsed.type) {
+ validateSuggestionsImpl(parsed)
+ }
+})
+
/** Check if there's an exact package match in results */
const hasExactPackageMatch = computed(() => {
const q = query.value.trim().toLowerCase()
From 4074e9b7e41528e7cfd977dc80c209fc50c1c919 Mon Sep 17 00:00:00 2001
From: Daniel Roe
Date: Sun, 8 Feb 2026 11:21:24 +0000
Subject: [PATCH 08/21] fix: more fixes
---
app/composables/npm/useAlgoliaSearch.ts | 7 +++++--
app/composables/npm/useOrgPackages.ts | 7 ++++++-
app/composables/npm/useUserPackages.ts | 15 ++++++++++-----
app/pages/search.vue | 2 ++
4 files changed, 23 insertions(+), 8 deletions(-)
diff --git a/app/composables/npm/useAlgoliaSearch.ts b/app/composables/npm/useAlgoliaSearch.ts
index d32640508..b9351a73f 100644
--- a/app/composables/npm/useAlgoliaSearch.ts
+++ b/app/composables/npm/useAlgoliaSearch.ts
@@ -181,6 +181,7 @@ export function useAlgoliaSearch() {
const allHits: AlgoliaHit[] = []
let offset = 0
+ let serverTotal = 0
const batchSize = 200
// Algolia supports up to 1000 results per query with offset/length pagination
@@ -205,10 +206,11 @@ export function useAlgoliaSearch() {
const response = results[0] as SearchResponse | undefined
if (!response) break
+ serverTotal = response.nbHits ?? 0
allHits.push(...response.hits)
// If we got fewer than requested, we've exhausted all results
- if (response.hits.length < length || allHits.length >= (response.nbHits ?? 0)) {
+ if (response.hits.length < length || allHits.length >= serverTotal) {
break
}
@@ -218,7 +220,8 @@ export function useAlgoliaSearch() {
return {
isStale: false,
objects: allHits.map(hitToSearchResult),
- total: allHits.length,
+ // Use server total so callers can detect truncation (allHits.length < total)
+ total: serverTotal,
time: new Date().toISOString(),
}
}
diff --git a/app/composables/npm/useOrgPackages.ts b/app/composables/npm/useOrgPackages.ts
index 23da7732d..281ee6395 100644
--- a/app/composables/npm/useOrgPackages.ts
+++ b/app/composables/npm/useOrgPackages.ts
@@ -98,7 +98,12 @@ export function useOrgPackages(orgName: MaybeRefOrGetter) {
// --- Algolia fast path ---
if (searchProvider.value === 'algolia') {
try {
- return await searchByOwner(org)
+ const response = await searchByOwner(org)
+ // If Algolia returns no results, the org may not exist — fall through
+ // to npm registry path which can properly detect a 404
+ if (response.objects.length > 0) {
+ return response
+ }
} catch {
// Fall through to npm registry path on Algolia failure
}
diff --git a/app/composables/npm/useUserPackages.ts b/app/composables/npm/useUserPackages.ts
index 9477b3904..fcb7867e5 100644
--- a/app/composables/npm/useUserPackages.ts
+++ b/app/composables/npm/useUserPackages.ts
@@ -57,12 +57,17 @@ export function useUserPackages(username: MaybeRefOrGetter) {
return emptySearchResponse
}
- cache.value = {
- username: user,
- objects: response.objects,
- total: response.total,
+ // If Algolia returns results, use them. If empty, fall through to npm
+ // registry which uses `maintainer:` search (matches all maintainers,
+ // not just the primary owner that Algolia's owner.name indexes).
+ if (response.objects.length > 0) {
+ cache.value = {
+ username: user,
+ objects: response.objects,
+ total: response.total,
+ }
+ return response
}
- return response
} catch {
// Fall through to npm registry path on Algolia failure
}
diff --git a/app/pages/search.vue b/app/pages/search.vue
index c18611b1e..e8c7433ab 100644
--- a/app/pages/search.vue
+++ b/app/pages/search.vue
@@ -492,6 +492,8 @@ watch(
// Re-validate suggestions and clear caches when provider changes
watch(isAlgolia, () => {
+ // Cancel any pending debounced validation from the previous provider
+ validateSuggestionsDebounced.cancel?.()
// Clear existence cache since results may differ between providers
existenceCache.value = {}
// Re-validate with current query
From 0495cc506fd7e4847a18acda2dfe5419ed81db3a Mon Sep 17 00:00:00 2001
From: Daniel Roe
Date: Sun, 8 Feb 2026 11:25:54 +0000
Subject: [PATCH 09/21] fix: ssr render 404 page for orgs
---
app/composables/npm/useOrgPackages.ts | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/app/composables/npm/useOrgPackages.ts b/app/composables/npm/useOrgPackages.ts
index 281ee6395..3b2d58239 100644
--- a/app/composables/npm/useOrgPackages.ts
+++ b/app/composables/npm/useOrgPackages.ts
@@ -89,7 +89,7 @@ export function useOrgPackages(orgName: MaybeRefOrGetter) {
const asyncData = useLazyAsyncData(
() => `org-packages:${searchProvider.value}:${toValue(orgName)}`,
- async ({ $npmRegistry, $npmApi }, { signal }) => {
+ async ({ $npmRegistry, $npmApi, ssrContext }, { signal }) => {
const org = toValue(orgName)
if (!org) {
return emptySearchResponse
@@ -120,11 +120,15 @@ export function useOrgPackages(orgName: MaybeRefOrGetter) {
} catch (err) {
// Check if this is a 404 (org not found)
if (err && typeof err === 'object' && 'statusCode' in err && err.statusCode === 404) {
- throw createError({
+ const error = createError({
statusCode: 404,
statusMessage: 'Organization not found',
message: `The organization "@${org}" does not exist on npm`,
})
+ if (import.meta.server) {
+ ssrContext!.payload.error = error
+ }
+ throw error
}
// For other errors (network, etc.), return empty array to be safe
packageNames = []
From 114abf316d731bc404212f81926af4202df88d26 Mon Sep 17 00:00:00 2001
From: Daniel Roe
Date: Sun, 8 Feb 2026 11:55:04 +0000
Subject: [PATCH 10/21] chore: tweak language
---
i18n/locales/en.json | 6 +++---
lunaria/files/en-GB.json | 6 +++---
lunaria/files/en-US.json | 6 +++---
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index 2b9e83f3d..465d26ad2 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -68,11 +68,11 @@
"language": "Language"
},
"search_provider": "Data source",
- "search_provider_description": "Choose where npmx gets package data for search results, org pages, and user pages. Individual package pages always use the npm registry directly.",
+ "search_provider_description": "Choose where npmx gets search data. Individual package pages always use the npm registry directly.",
"search_provider_npm": "npm Registry",
- "search_provider_npm_description": "Fetches package listings directly from the official npm registry. Authoritative, but can be slower for large result sets.",
+ "search_provider_npm_description": "Fetches search, org and user listings directly from the official npm registry. Authoritative, but can be slower.",
"search_provider_algolia": "Algolia",
- "search_provider_algolia_description": "Uses npm's Algolia index for faster package listings and search. Org and user pages load in a single request instead of many.",
+ "search_provider_algolia_description": "Uses Algolia for faster search, org and user pages.",
"relative_dates": "Relative dates",
"include_types": "Include {'@'}types in install",
"include_types_description": "Add {'@'}types package to install commands for untyped packages",
diff --git a/lunaria/files/en-GB.json b/lunaria/files/en-GB.json
index 2f4d5b35f..18f73c8a1 100644
--- a/lunaria/files/en-GB.json
+++ b/lunaria/files/en-GB.json
@@ -68,11 +68,11 @@
"language": "Language"
},
"search_provider": "Data source",
- "search_provider_description": "Choose where npmx gets package data for search results, org pages, and user pages. Individual package pages always use the npm registry directly.",
+ "search_provider_description": "Choose where npmx gets search data. Individual package pages always use the npm registry directly.",
"search_provider_npm": "npm Registry",
- "search_provider_npm_description": "Fetches package listings directly from the official npm registry. Authoritative, but can be slower for large result sets.",
+ "search_provider_npm_description": "Fetches search, org and user listings directly from the official npm registry. Authoritative, but can be slower.",
"search_provider_algolia": "Algolia",
- "search_provider_algolia_description": "Uses npm's Algolia index for faster package listings and search. Org and user pages load in a single request instead of many.",
+ "search_provider_algolia_description": "Uses Algolia for faster search, org and user pages.",
"relative_dates": "Relative dates",
"include_types": "Include {'@'}types in install",
"include_types_description": "Add {'@'}types package to install commands for untyped packages",
diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json
index 2b9e83f3d..465d26ad2 100644
--- a/lunaria/files/en-US.json
+++ b/lunaria/files/en-US.json
@@ -68,11 +68,11 @@
"language": "Language"
},
"search_provider": "Data source",
- "search_provider_description": "Choose where npmx gets package data for search results, org pages, and user pages. Individual package pages always use the npm registry directly.",
+ "search_provider_description": "Choose where npmx gets search data. Individual package pages always use the npm registry directly.",
"search_provider_npm": "npm Registry",
- "search_provider_npm_description": "Fetches package listings directly from the official npm registry. Authoritative, but can be slower for large result sets.",
+ "search_provider_npm_description": "Fetches search, org and user listings directly from the official npm registry. Authoritative, but can be slower.",
"search_provider_algolia": "Algolia",
- "search_provider_algolia_description": "Uses npm's Algolia index for faster package listings and search. Org and user pages load in a single request instead of many.",
+ "search_provider_algolia_description": "Uses Algolia for faster search, org and user pages.",
"relative_dates": "Relative dates",
"include_types": "Include {'@'}types in install",
"include_types_description": "Add {'@'}types package to install commands for untyped packages",
From e5022a5a7f80eb4e3fa3fa2fd86caf3815307730 Mon Sep 17 00:00:00 2001
From: Daniel Roe
Date: Sun, 8 Feb 2026 11:59:25 +0000
Subject: [PATCH 11/21] chore: update i18n key and reduce flakiness on
suggestion
---
app/components/SearchProviderToggle.client.vue | 12 ++++++------
app/pages/search.vue | 18 ++++++++++++++++--
app/pages/settings.vue | 12 ++++++------
i18n/locales/en.json | 14 ++++++++------
lunaria/files/en-GB.json | 14 ++++++++------
lunaria/files/en-US.json | 14 ++++++++------
6 files changed, 52 insertions(+), 32 deletions(-)
diff --git a/app/components/SearchProviderToggle.client.vue b/app/components/SearchProviderToggle.client.vue
index 4cac0e975..598a01a42 100644
--- a/app/components/SearchProviderToggle.client.vue
+++ b/app/components/SearchProviderToggle.client.vue
@@ -18,7 +18,7 @@ useEventListener('keydown', event => {
{
v-if="isOpen"
class="absolute inset-ie-0 top-full pt-2 w-72 z-50"
role="menu"
- :aria-label="$t('settings.search_provider')"
+ :aria-label="$t('settings.data_source.label')"
>
{
/>
- {{ $t('settings.search_provider_npm') }}
+ {{ $t('settings.data_source.npm') }}
- {{ $t('settings.search_provider_npm_description') }}
+ {{ $t('settings.data_source.npm_description') }}
@@ -90,10 +90,10 @@ useEventListener('keydown', event => {
/>
- {{ $t('settings.search_provider_algolia') }}
+ {{ $t('settings.data_source.algolia') }}
- {{ $t('settings.search_provider_algolia_description') }}
+ {{ $t('settings.data_source.algolia_description') }}
diff --git a/app/pages/search.vue b/app/pages/search.vue
index e8c7433ab..a11090c29 100644
--- a/app/pages/search.vue
+++ b/app/pages/search.vue
@@ -431,10 +431,16 @@ const parsedQuery = computed(() => {
const validatedSuggestions = ref([])
const suggestionsLoading = shallowRef(false)
+/** Counter to discard stale async results when query changes rapidly */
+let suggestionRequestId = 0
+
/** Validate suggestions (check org/user existence) */
async function validateSuggestionsImpl(parsed: ParsedQuery) {
+ const requestId = ++suggestionRequestId
+
if (!parsed.type || !parsed.name) {
validatedSuggestions.value = []
+ suggestionsLoading.value = false
return
}
@@ -444,11 +450,13 @@ async function validateSuggestionsImpl(parsed: ParsedQuery) {
try {
if (parsed.type === 'user') {
const exists = await checkUserExists(parsed.name)
+ if (requestId !== suggestionRequestId) return
if (exists) {
suggestions.push({ type: 'user', name: parsed.name, exists: true })
}
} else if (parsed.type === 'org') {
const exists = await checkOrgExists(parsed.name)
+ if (requestId !== suggestionRequestId) return
if (exists) {
suggestions.push({ type: 'org', name: parsed.name, exists: true })
}
@@ -458,6 +466,7 @@ async function validateSuggestionsImpl(parsed: ParsedQuery) {
checkOrgExists(parsed.name),
checkUserExists(parsed.name),
])
+ if (requestId !== suggestionRequestId) return
// Org first (more common)
if (orgExists) {
suggestions.push({ type: 'org', name: parsed.name, exists: true })
@@ -467,10 +476,15 @@ async function validateSuggestionsImpl(parsed: ParsedQuery) {
}
}
} finally {
- suggestionsLoading.value = false
+ // Only clear loading if this is still the active request
+ if (requestId === suggestionRequestId) {
+ suggestionsLoading.value = false
+ }
}
- validatedSuggestions.value = suggestions
+ if (requestId === suggestionRequestId) {
+ validatedSuggestions.value = suggestions
+ }
}
// Debounce lightly for npm (extra API calls are slower), skip debounce for Algolia (fast)
diff --git a/app/pages/settings.vue b/app/pages/settings.vue
index 92f879f64..96928df95 100644
--- a/app/pages/settings.vue
+++ b/app/pages/settings.vue
@@ -156,10 +156,10 @@ const setLocale: typeof setNuxti18nLocale = locale => {
- {{ $t('settings.search_provider_description') }}
+ {{ $t('settings.data_source.description') }}
@@ -173,10 +173,10 @@ const setLocale: typeof setNuxti18nLocale = locale => {
"
>
@@ -194,8 +194,8 @@ const setLocale: typeof setNuxti18nLocale = locale => {
{{
settings.searchProvider === 'algolia'
- ? $t('settings.search_provider_algolia_description')
- : $t('settings.search_provider_npm_description')
+ ? $t('settings.data_source.algolia_description')
+ : $t('settings.data_source.npm_description')
}}
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index 465d26ad2..87a4d4875 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -67,12 +67,14 @@
"search": "Data source",
"language": "Language"
},
- "search_provider": "Data source",
- "search_provider_description": "Choose where npmx gets search data. Individual package pages always use the npm registry directly.",
- "search_provider_npm": "npm Registry",
- "search_provider_npm_description": "Fetches search, org and user listings directly from the official npm registry. Authoritative, but can be slower.",
- "search_provider_algolia": "Algolia",
- "search_provider_algolia_description": "Uses Algolia for faster search, org and user pages.",
+ "data_source": {
+ "label": "Data source",
+ "description": "Choose where npmx gets search data. Individual package pages always use the npm registry directly.",
+ "npm": "npm Registry",
+ "npm_description": "Fetches search, org and user listings directly from the official npm registry. Authoritative, but can be slower.",
+ "algolia": "Algolia",
+ "algolia_description": "Uses Algolia for faster search, org and user pages."
+ },
"relative_dates": "Relative dates",
"include_types": "Include {'@'}types in install",
"include_types_description": "Add {'@'}types package to install commands for untyped packages",
diff --git a/lunaria/files/en-GB.json b/lunaria/files/en-GB.json
index 18f73c8a1..0de2502d7 100644
--- a/lunaria/files/en-GB.json
+++ b/lunaria/files/en-GB.json
@@ -67,12 +67,14 @@
"search": "Data source",
"language": "Language"
},
- "search_provider": "Data source",
- "search_provider_description": "Choose where npmx gets search data. Individual package pages always use the npm registry directly.",
- "search_provider_npm": "npm Registry",
- "search_provider_npm_description": "Fetches search, org and user listings directly from the official npm registry. Authoritative, but can be slower.",
- "search_provider_algolia": "Algolia",
- "search_provider_algolia_description": "Uses Algolia for faster search, org and user pages.",
+ "data_source": {
+ "label": "Data source",
+ "description": "Choose where npmx gets search data. Individual package pages always use the npm registry directly.",
+ "npm": "npm Registry",
+ "npm_description": "Fetches search, org and user listings directly from the official npm registry. Authoritative, but can be slower.",
+ "algolia": "Algolia",
+ "algolia_description": "Uses Algolia for faster search, org and user pages."
+ },
"relative_dates": "Relative dates",
"include_types": "Include {'@'}types in install",
"include_types_description": "Add {'@'}types package to install commands for untyped packages",
diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json
index 465d26ad2..87a4d4875 100644
--- a/lunaria/files/en-US.json
+++ b/lunaria/files/en-US.json
@@ -67,12 +67,14 @@
"search": "Data source",
"language": "Language"
},
- "search_provider": "Data source",
- "search_provider_description": "Choose where npmx gets search data. Individual package pages always use the npm registry directly.",
- "search_provider_npm": "npm Registry",
- "search_provider_npm_description": "Fetches search, org and user listings directly from the official npm registry. Authoritative, but can be slower.",
- "search_provider_algolia": "Algolia",
- "search_provider_algolia_description": "Uses Algolia for faster search, org and user pages.",
+ "data_source": {
+ "label": "Data source",
+ "description": "Choose where npmx gets search data. Individual package pages always use the npm registry directly.",
+ "npm": "npm Registry",
+ "npm_description": "Fetches search, org and user listings directly from the official npm registry. Authoritative, but can be slower.",
+ "algolia": "Algolia",
+ "algolia_description": "Uses Algolia for faster search, org and user pages."
+ },
"relative_dates": "Relative dates",
"include_types": "Include {'@'}types in install",
"include_types_description": "Add {'@'}types package to install commands for untyped packages",
From 54e95b5e9c17632f26976ea68fac75a039dd3eff Mon Sep 17 00:00:00 2001
From: Daniel Roe
Date: Sun, 8 Feb 2026 13:03:19 +0000
Subject: [PATCH 12/21] fix: address code review comments
---
app/components/SearchProviderToggle.client.vue | 2 +-
app/composables/npm/useAlgoliaSearch.ts | 7 +++++--
app/composables/npm/useUserPackages.ts | 9 ++++++---
app/pages/~[username]/index.vue | 6 +++---
4 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/app/components/SearchProviderToggle.client.vue b/app/components/SearchProviderToggle.client.vue
index 598a01a42..bb1dff5d1 100644
--- a/app/components/SearchProviderToggle.client.vue
+++ b/app/components/SearchProviderToggle.client.vue
@@ -74,7 +74,7 @@ useEventListener('keydown', event => {
-
+
-
+
{{ error?.message ?? $t('user.page.failed_to_load') }}
From 2641a99d6821a0100ce58bf28fe2371ffe6c3caf Mon Sep 17 00:00:00 2001
From: Daniel Roe
Date: Sun, 8 Feb 2026 13:56:19 +0000
Subject: [PATCH 13/21] fix: address coderabbit reviews
---
app/composables/npm/useUserPackages.ts | 14 +++++++++-----
app/pages/settings.vue | 2 +-
2 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/app/composables/npm/useUserPackages.ts b/app/composables/npm/useUserPackages.ts
index 79e768213..87eed6d61 100644
--- a/app/composables/npm/useUserPackages.ts
+++ b/app/composables/npm/useUserPackages.ts
@@ -23,6 +23,7 @@ const MAX_RESULTS = 250
*/
export function useUserPackages(username: MaybeRefOrGetter) {
const { searchProvider } = useSearchProvider()
+ // this is only used in npm path, but we need to extract it when the composable runs
const { $npmRegistry } = useNuxtApp()
const { searchByOwner } = useAlgoliaSearch()
@@ -103,7 +104,11 @@ export function useUserPackages(username: MaybeRefOrGetter) {
{ default: () => emptySearchResponse },
)
// --- Fetch more (npm path only) ---
- async function fetchMore(): Promise {
+ /**
+ * Fetch the next page of results from npm registry.
+ * @param manageLoadingState - When false, caller manages isLoadingMore (used by loadAll to prevent flicker)
+ */
+ async function fetchMore(manageLoadingState = true): Promise {
const user = toValue(username)
const provider = searchProvider.value
if (!user || provider !== 'npm') return
@@ -119,7 +124,7 @@ export function useUserPackages(username: MaybeRefOrGetter) {
if (currentCount >= total) return
- isLoadingMore.value = true
+ if (manageLoadingState) isLoadingMore.value = true
try {
const from = currentCount
@@ -155,7 +160,7 @@ export function useUserPackages(username: MaybeRefOrGetter) {
}
}
} finally {
- isLoadingMore.value = false
+ if (manageLoadingState) isLoadingMore.value = false
}
}
@@ -173,7 +178,7 @@ export function useUserPackages(username: MaybeRefOrGetter) {
isLoadingMore.value = true
try {
while (hasMore.value) {
- await fetchMore()
+ await fetchMore(false)
}
} finally {
isLoadingMore.value = false
@@ -184,7 +189,6 @@ export function useUserPackages(username: MaybeRefOrGetter) {
watch(searchProvider, () => {
cache.value = null
currentPage.value = 1
- asyncData.refresh()
})
// Computed data that uses cache
diff --git a/app/pages/settings.vue b/app/pages/settings.vue
index 96928df95..d4a2b045a 100644
--- a/app/pages/settings.vue
+++ b/app/pages/settings.vue
@@ -166,7 +166,7 @@ const setLocale: typeof setNuxti18nLocale = locale => {
Date: Sun, 8 Feb 2026 10:59:23 +0000
Subject: [PATCH 07/21] fix: speed up loading of org/user suggestions
---
app/pages/search.vue | 66 ++++++++++++++++++++++++++++++++------------
1 file changed, 49 insertions(+), 17 deletions(-)
diff --git a/app/pages/search.vue b/app/pages/search.vue
index 37c5e6ebb..c18611b1e 100644
--- a/app/pages/search.vue
+++ b/app/pages/search.vue
@@ -316,14 +316,13 @@ interface ValidatedSuggestion {
/** Cache for existence checks to avoid repeated API calls */
const existenceCache = ref
- {{ $t('settings.search_provider_npm_description') }}
+ {{ $t('settings.data_source.npm_description') }}
- {{ $t('settings.search_provider_algolia_description') }}
+ {{ $t('settings.data_source.algolia_description') }}
- {{ $t('settings.search_provider_description') }}
+ {{ $t('settings.data_source.description') }}
{{
settings.searchProvider === 'algolia'
- ? $t('settings.search_provider_algolia_description')
- : $t('settings.search_provider_npm_description')
+ ? $t('settings.data_source.algolia_description')
+ : $t('settings.data_source.npm_description')
}}
{{ error?.message ?? $t('user.page.failed_to_load') }}