From ab61f5ab3346dee7f065550d0e5ee6b439612ecd Mon Sep 17 00:00:00 2001 From: MatteoGabriele Date: Mon, 2 Feb 2026 11:52:00 +0100 Subject: [PATCH 01/14] feat(search): show package link for invalid query length --- app/pages/search.vue | 31 ++++++++++++++++++++++++++++++- i18n/locales/en.json | 3 +++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/pages/search.vue b/app/pages/search.vue index 352d137090..980416eff2 100644 --- a/app/pages/search.vue +++ b/app/pages/search.vue @@ -72,11 +72,21 @@ const { isLoadingMore, hasMore, fetchMore, + error, } = useNpmSearch(query, () => ({ size: requestedSize.value, incremental: true, })) +interface SearchError { + code?: string +} + +const hasTextLengthError = computed(() => { + const errorData = error.value?.data as SearchError | undefined + return errorData?.code === 'ERR_TEXT_LENGTH' +}) + // Results to display (directly from incremental search) const rawVisibleResults = computed(() => results.value) @@ -594,6 +604,25 @@ defineOgImageComponent('Default', {

{{ $t('search.title') }}

+
+
+

+ {{ $t('search.not_invalid') }} +

+

+ {{ $t('search.not_invalid_description') }} +

+
+ + {{ $t('search.not_invalid_button_label', { name: query }) }} + +
@@ -698,7 +727,7 @@ defineOgImageComponent('Default', { -
+

{{ $t('search.no_results', { query }) }}

diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 9330ad174f..dc12e513d9 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -34,6 +34,9 @@ "want_to_claim": "Want to claim this package name?", "start_typing": "Start typing to search packages", "exact_match": "exact", + "not_invalid": "Search query invalid", + "not_invalid_description": "Your search query doesn't meet the requirements. View the package directly instead.", + "not_invalid_button_label": "View \"{name}\"", "suggestion": { "user": "user", "org": "org", From 656233812c348c7bececa89c0b735668523ba4b7 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:55:45 +0000 Subject: [PATCH 02/14] [autofix.ci] apply automated fixes --- lunaria/files/en-US.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json index 9330ad174f..dc12e513d9 100644 --- a/lunaria/files/en-US.json +++ b/lunaria/files/en-US.json @@ -34,6 +34,9 @@ "want_to_claim": "Want to claim this package name?", "start_typing": "Start typing to search packages", "exact_match": "exact", + "not_invalid": "Search query invalid", + "not_invalid_description": "Your search query doesn't meet the requirements. View the package directly instead.", + "not_invalid_button_label": "View \"{name}\"", "suggestion": { "user": "user", "org": "org", From ba044c960dc96ad71bf903069f7fdf1ddd8aa599 Mon Sep 17 00:00:00 2001 From: MatteoGabriele Date: Mon, 2 Feb 2026 11:57:44 +0100 Subject: [PATCH 03/14] refactor: move block next to no results found --- app/pages/search.vue | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/app/pages/search.vue b/app/pages/search.vue index 980416eff2..d22b5cfb50 100644 --- a/app/pages/search.vue +++ b/app/pages/search.vue @@ -604,25 +604,6 @@ defineOgImageComponent('Default', {

{{ $t('search.title') }}

-
-
-

- {{ $t('search.not_invalid') }} -

-

- {{ $t('search.not_invalid_description') }} -

-
- - {{ $t('search.not_invalid_button_label', { name: query }) }} - -
@@ -727,8 +708,27 @@ defineOgImageComponent('Default', {
-
-

+

+
+
+

+ {{ $t('search.not_invalid') }} +

+

+ {{ $t('search.not_invalid_description') }} +

+
+ + {{ $t('search.not_invalid_button_label', { name: query }) }} + +
+

{{ $t('search.no_results', { query }) }}

From d266f98f849164d59c8580c8291a9e5ff1c80a98 Mon Sep 17 00:00:00 2001 From: MatteoGabriele Date: Mon, 2 Feb 2026 11:59:20 +0100 Subject: [PATCH 04/14] refactor: remove margin --- app/pages/search.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pages/search.vue b/app/pages/search.vue index d22b5cfb50..9018891b99 100644 --- a/app/pages/search.vue +++ b/app/pages/search.vue @@ -711,7 +711,7 @@ defineOgImageComponent('Default', {

From e4e478e2f1133f07c3da7d9ab99f5c2d67f455ac Mon Sep 17 00:00:00 2001 From: MatteoGabriele Date: Mon, 2 Feb 2026 12:07:36 +0100 Subject: [PATCH 05/14] refactor: use different hover state --- app/pages/search.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pages/search.vue b/app/pages/search.vue index 9018891b99..c6ddacb73c 100644 --- a/app/pages/search.vue +++ b/app/pages/search.vue @@ -723,7 +723,7 @@ defineOgImageComponent('Default', {

{{ $t('search.not_invalid_button_label', { name: query }) }} From 64031f830918c2470561a3cfecaaa37c732f267d Mon Sep 17 00:00:00 2001 From: MatteoGabriele Date: Mon, 2 Feb 2026 14:09:49 +0100 Subject: [PATCH 06/14] refactor: display package as a single search result --- app/components/Package/CardExact.vue | 14 ++++++++++++++ app/composables/useNpmRegistry.ts | 23 +++++++++++++++++++++++ app/pages/search.vue | 20 +------------------- 3 files changed, 38 insertions(+), 19 deletions(-) create mode 100644 app/components/Package/CardExact.vue diff --git a/app/components/Package/CardExact.vue b/app/components/Package/CardExact.vue new file mode 100644 index 0000000000..8ac1638ad9 --- /dev/null +++ b/app/components/Package/CardExact.vue @@ -0,0 +1,14 @@ + + + diff --git a/app/composables/useNpmRegistry.ts b/app/composables/useNpmRegistry.ts index 8b5dfba8d8..b91d40bb67 100644 --- a/app/composables/useNpmRegistry.ts +++ b/app/composables/useNpmRegistry.ts @@ -586,6 +586,29 @@ export function useOrgPackages(orgName: MaybeRefOrGetter) { return asyncData } +/** + * Returns a single package as an NpmSearchResult + * Similar to useNpmSearch but for exact package lookups + */ +export function useExactPackage(query: MaybeRefOrGetter) { + const { data: pkg, status, error } = usePackage(query) + const { data: downloads } = usePackageDownloads(query) + + const data = computed(() => { + if (!pkg.value) { + return undefined + } + + return packumentToSearchResult(pkg.value, downloads.value?.downloads) + }) + + return { + data, + status, + error, + } +} + // ============================================================================ // Package Versions // ============================================================================ diff --git a/app/pages/search.vue b/app/pages/search.vue index c6ddacb73c..d8a59f1774 100644 --- a/app/pages/search.vue +++ b/app/pages/search.vue @@ -709,25 +709,7 @@ defineOgImageComponent('Default', {
-
-
-

- {{ $t('search.not_invalid') }} -

-

- {{ $t('search.not_invalid_description') }} -

-
- - {{ $t('search.not_invalid_button_label', { name: query }) }} - -
+

{{ $t('search.no_results', { query }) }}

From 74f156c288b8be13b5d690b92f7177eae067589d Mon Sep 17 00:00:00 2001 From: MatteoGabriele Date: Mon, 2 Feb 2026 14:14:18 +0100 Subject: [PATCH 07/14] refactor: remove unused copy --- i18n/locales/en.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/i18n/locales/en.json b/i18n/locales/en.json index dc12e513d9..9330ad174f 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -34,9 +34,6 @@ "want_to_claim": "Want to claim this package name?", "start_typing": "Start typing to search packages", "exact_match": "exact", - "not_invalid": "Search query invalid", - "not_invalid_description": "Your search query doesn't meet the requirements. View the package directly instead.", - "not_invalid_button_label": "View \"{name}\"", "suggestion": { "user": "user", "org": "org", From 2285e6876cf9285a7b2f21a5d3030788281c6a42 Mon Sep 17 00:00:00 2001 From: MatteoGabriele Date: Mon, 2 Feb 2026 14:40:07 +0100 Subject: [PATCH 08/14] refactor: move check to useNpmSearch --- app/composables/useNpmRegistry.ts | 67 ++++++++++++++++++++++++++----- app/pages/search.vue | 13 +----- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/app/composables/useNpmRegistry.ts b/app/composables/useNpmRegistry.ts index b91d40bb67..8081c4feb8 100644 --- a/app/composables/useNpmRegistry.ts +++ b/app/composables/useNpmRegistry.ts @@ -18,6 +18,12 @@ import type { CachedFetchFunction } from '#shared/utils/fetch-cache-config' const NPM_REGISTRY = 'https://registry.npmjs.org' const NPM_API = 'https://api.npmjs.org' +interface NpmSearchError { + data: { + code: string + } +} + // Cache for packument fetches to avoid duplicate requests across components const packumentCache = new Map>() @@ -306,6 +312,7 @@ export function useNpmSearch( () => `search:incremental:${toValue(query)}`, async (_nuxtApp, { signal }) => { const q = toValue(query) + if (!q.trim()) { return emptySearchResponse } @@ -321,19 +328,57 @@ export function useNpmSearch( // Use requested size for initial fetch params.set('size', String(opts.size ?? 25)) - const { data: response, isStale } = await cachedFetch( - `${NPM_REGISTRY}/-/v1/search?${params.toString()}`, - { signal }, - 60, - ) + try { + const { data: response, isStale } = await cachedFetch( + `${NPM_REGISTRY}/-/v1/search?${params.toString()}`, + { signal }, + 60, + ) - cache.value = { - query: q, - objects: response.objects, - total: response.total, - } + cache.value = { + query: q, + objects: response.objects, + total: response.total, + } - return { ...response, isStale } + return { ...response, isStale } + } catch (error) { + if ((error as NpmSearchError)?.data?.code === 'ERR_TEXT_LENGTH') { + try { + const encodedName = encodePackageName(q) + const [{ data: pkg }, { data: downloads }] = await Promise.all([ + cachedFetch(`${NPM_REGISTRY}/${encodedName}`, { signal }), + cachedFetch(`${NPM_API}/downloads/point/last-week/${encodedName}`, { + signal, + }), + ]) + + if (!pkg) { + throw error + } + + const result = packumentToSearchResult(pkg, downloads?.downloads) + + cache.value = { + query: q, + objects: [result], + total: 1, + } + + return { + objects: [result], + total: 1, + isStale: false, + time: new Date().toISOString(), + } + } catch { + // If exact lookup also fails, throw original error + throw error + } + } + + throw error + } }, { default: () => lastSearch || emptySearchResponse }, ) diff --git a/app/pages/search.vue b/app/pages/search.vue index d8a59f1774..352d137090 100644 --- a/app/pages/search.vue +++ b/app/pages/search.vue @@ -72,21 +72,11 @@ const { isLoadingMore, hasMore, fetchMore, - error, } = useNpmSearch(query, () => ({ size: requestedSize.value, incremental: true, })) -interface SearchError { - code?: string -} - -const hasTextLengthError = computed(() => { - const errorData = error.value?.data as SearchError | undefined - return errorData?.code === 'ERR_TEXT_LENGTH' -}) - // Results to display (directly from incremental search) const rawVisibleResults = computed(() => results.value) @@ -709,8 +699,7 @@ defineOgImageComponent('Default', {
- -

+

{{ $t('search.no_results', { query }) }}

From df5b466442a847373ec446a597c0df9035e39b91 Mon Sep 17 00:00:00 2001 From: MatteoGabriele Date: Mon, 2 Feb 2026 14:42:58 +0100 Subject: [PATCH 09/14] chore: delete unused component --- app/components/Package/CardExact.vue | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 app/components/Package/CardExact.vue diff --git a/app/components/Package/CardExact.vue b/app/components/Package/CardExact.vue deleted file mode 100644 index 8ac1638ad9..0000000000 --- a/app/components/Package/CardExact.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - From c768c3367c242d28e0311112de15883aa0c9f1d5 Mon Sep 17 00:00:00 2001 From: MatteoGabriele Date: Mon, 2 Feb 2026 14:45:58 +0100 Subject: [PATCH 10/14] chore: delete unused composable --- app/composables/useNpmRegistry.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/app/composables/useNpmRegistry.ts b/app/composables/useNpmRegistry.ts index 8081c4feb8..6decf97c48 100644 --- a/app/composables/useNpmRegistry.ts +++ b/app/composables/useNpmRegistry.ts @@ -631,29 +631,6 @@ export function useOrgPackages(orgName: MaybeRefOrGetter) { return asyncData } -/** - * Returns a single package as an NpmSearchResult - * Similar to useNpmSearch but for exact package lookups - */ -export function useExactPackage(query: MaybeRefOrGetter) { - const { data: pkg, status, error } = usePackage(query) - const { data: downloads } = usePackageDownloads(query) - - const data = computed(() => { - if (!pkg.value) { - return undefined - } - - return packumentToSearchResult(pkg.value, downloads.value?.downloads) - }) - - return { - data, - status, - error, - } -} - // ============================================================================ // Package Versions // ============================================================================ From aafdc8f9dbf5cdf7830ad2cabcc164813951c7c9 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:53:37 +0000 Subject: [PATCH 11/14] [autofix.ci] apply automated fixes --- lunaria/files/en-US.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json index d5820417d4..c2781bc8df 100644 --- a/lunaria/files/en-US.json +++ b/lunaria/files/en-US.json @@ -34,9 +34,6 @@ "want_to_claim": "Want to claim this package name?", "start_typing": "Start typing to search packages", "exact_match": "exact", - "not_invalid": "Search query invalid", - "not_invalid_description": "Your search query doesn't meet the requirements. View the package directly instead.", - "not_invalid_button_label": "View \"{name}\"", "suggestion": { "user": "user", "org": "org", From d3381679802ceea12a3ba7bb657cbb1029f9c9e3 Mon Sep 17 00:00:00 2001 From: MatteoGabriele Date: Mon, 2 Feb 2026 15:06:15 +0100 Subject: [PATCH 12/14] refactor: return package response --- app/composables/useNpmRegistry.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/composables/useNpmRegistry.ts b/app/composables/useNpmRegistry.ts index 6decf97c48..bffa16e73d 100644 --- a/app/composables/useNpmRegistry.ts +++ b/app/composables/useNpmRegistry.ts @@ -346,7 +346,7 @@ export function useNpmSearch( if ((error as NpmSearchError)?.data?.code === 'ERR_TEXT_LENGTH') { try { const encodedName = encodePackageName(q) - const [{ data: pkg }, { data: downloads }] = await Promise.all([ + const [{ data: pkg, isStale }, { data: downloads }] = await Promise.all([ cachedFetch(`${NPM_REGISTRY}/${encodedName}`, { signal }), cachedFetch(`${NPM_API}/downloads/point/last-week/${encodedName}`, { signal, @@ -365,12 +365,7 @@ export function useNpmSearch( total: 1, } - return { - objects: [result], - total: 1, - isStale: false, - time: new Date().toISOString(), - } + return { ...pkg, isStale } } catch { // If exact lookup also fails, throw original error throw error From 8232384a722bcd388fcea69a6e5eb18be6351602 Mon Sep 17 00:00:00 2001 From: MatteoGabriele Date: Mon, 2 Feb 2026 15:26:03 +0100 Subject: [PATCH 13/14] fix: return search response --- app/composables/useNpmRegistry.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/composables/useNpmRegistry.ts b/app/composables/useNpmRegistry.ts index bffa16e73d..763986d4f0 100644 --- a/app/composables/useNpmRegistry.ts +++ b/app/composables/useNpmRegistry.ts @@ -365,7 +365,12 @@ export function useNpmSearch( total: 1, } - return { ...pkg, isStale } + return { + objects: [result], + total: 1, + isStale, + time: new Date().toISOString(), + } } catch { // If exact lookup also fails, throw original error throw error From bf908d3141ff92483675baf5441407f2ea41edf4 Mon Sep 17 00:00:00 2001 From: MatteoGabriele Date: Mon, 2 Feb 2026 17:47:23 +0100 Subject: [PATCH 14/14] refactor: avoid search call with 1 letter --- app/composables/useNpmRegistry.ts | 83 +++++++++++++------------------ 1 file changed, 34 insertions(+), 49 deletions(-) diff --git a/app/composables/useNpmRegistry.ts b/app/composables/useNpmRegistry.ts index 763986d4f0..7190858390 100644 --- a/app/composables/useNpmRegistry.ts +++ b/app/composables/useNpmRegistry.ts @@ -18,12 +18,6 @@ import type { CachedFetchFunction } from '#shared/utils/fetch-cache-config' const NPM_REGISTRY = 'https://registry.npmjs.org' const NPM_API = 'https://api.npmjs.org' -interface NpmSearchError { - data: { - code: string - } -} - // Cache for packument fetches to avoid duplicate requests across components const packumentCache = new Map>() @@ -328,57 +322,48 @@ export function useNpmSearch( // Use requested size for initial fetch params.set('size', String(opts.size ?? 25)) - try { - const { data: response, isStale } = await cachedFetch( - `${NPM_REGISTRY}/-/v1/search?${params.toString()}`, - { signal }, - 60, - ) + if (q.length === 1) { + const encodedName = encodePackageName(q) + const [{ data: pkg, isStale }, { data: downloads }] = await Promise.all([ + cachedFetch(`${NPM_REGISTRY}/${encodedName}`, { signal }), + cachedFetch(`${NPM_API}/downloads/point/last-week/${encodedName}`, { + signal, + }), + ]) - cache.value = { - query: q, - objects: response.objects, - total: response.total, + if (!pkg) { + return emptySearchResponse } - return { ...response, isStale } - } catch (error) { - if ((error as NpmSearchError)?.data?.code === 'ERR_TEXT_LENGTH') { - try { - const encodedName = encodePackageName(q) - const [{ data: pkg, isStale }, { data: downloads }] = await Promise.all([ - cachedFetch(`${NPM_REGISTRY}/${encodedName}`, { signal }), - cachedFetch(`${NPM_API}/downloads/point/last-week/${encodedName}`, { - signal, - }), - ]) - - if (!pkg) { - throw error - } - - const result = packumentToSearchResult(pkg, downloads?.downloads) + const result = packumentToSearchResult(pkg, downloads?.downloads) - cache.value = { - query: q, - objects: [result], - total: 1, - } + cache.value = { + query: q, + objects: [result], + total: 1, + } - return { - objects: [result], - total: 1, - isStale, - time: new Date().toISOString(), - } - } catch { - // If exact lookup also fails, throw original error - throw error - } + return { + objects: [result], + total: 1, + isStale, + time: new Date().toISOString(), } + } - throw error + const { data: response, isStale } = await cachedFetch( + `${NPM_REGISTRY}/-/v1/search?${params.toString()}`, + { signal }, + 60, + ) + + cache.value = { + query: q, + objects: response.objects, + total: response.total, } + + return { ...response, isStale } }, { default: () => lastSearch || emptySearchResponse }, )