forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import type { NpmSearchResponse, NpmSearchResult, PackageMetaResponse } from '#shared/types'
2import { emptySearchResponse, metaToSearchResult } from './search-utils'
3import { mapWithConcurrency } from '#shared/utils/async'
4
5/**
6 * Fetch all packages for an npm organization.
7 *
8 * 1. Gets the authoritative package list from the npm registry (single request)
9 * 2. Fetches metadata from Algolia by exact name (single request)
10 * 3. Falls back to lightweight server-side package-meta lookups
11 */
12export function useOrgPackages(orgName: MaybeRefOrGetter<string>) {
13 const route = useRoute()
14 const { searchProvider } = useSearchProvider()
15 const searchProviderValue = computed(() => {
16 const p = normalizeSearchParam(route.query.p)
17 if (p === 'npm' || searchProvider.value === 'npm') return 'npm'
18 return 'algolia'
19 })
20 const { getPackagesByName } = useAlgoliaSearch()
21
22 const asyncData = useLazyAsyncData(
23 () => `org-packages:${searchProviderValue.value}:${toValue(orgName)}`,
24 async ({ ssrContext }, { signal }) => {
25 const org = toValue(orgName)
26 if (!org) {
27 return emptySearchResponse()
28 }
29
30 // Get the authoritative package list from the npm registry (single request)
31 let packageNames: string[]
32 try {
33 const { packages } = await $fetch<{ packages: string[]; count: number }>(
34 `/api/registry/org/${encodeURIComponent(org)}/packages`,
35 { signal },
36 )
37 packageNames = packages
38 } catch (err) {
39 // Check if this is a 404 (org not found)
40 if (err && typeof err === 'object' && 'statusCode' in err && err.statusCode === 404) {
41 const error = createError({
42 statusCode: 404,
43 statusMessage: 'Organization not found',
44 message: `The organization "@${org}" does not exist on npm`,
45 })
46 if (import.meta.server) {
47 ssrContext!.payload.error = error
48 }
49 throw error
50 }
51 // For other errors (network, etc.), return empty array to be safe
52 packageNames = []
53 }
54
55 if (packageNames.length === 0) {
56 return emptySearchResponse()
57 }
58
59 // Fetch metadata + downloads from Algolia (single request via getObjects)
60 if (searchProviderValue.value === 'algolia') {
61 try {
62 const response = await getPackagesByName(packageNames)
63 if (response.objects.length > 0) {
64 return response
65 }
66 } catch {
67 // Fall through to npm registry path
68 }
69 }
70
71 // npm fallback: fetch lightweight metadata via server proxy
72 const metaResults = await mapWithConcurrency(
73 packageNames,
74 async name => {
75 try {
76 return await $fetch<PackageMetaResponse>(
77 `/api/registry/package-meta/${encodePackageName(name)}`,
78 { signal },
79 )
80 } catch {
81 return null
82 }
83 },
84 10,
85 )
86
87 const results: NpmSearchResult[] = metaResults
88 .filter((meta): meta is PackageMetaResponse => meta !== null)
89 .map(metaToSearchResult)
90
91 return {
92 isStale: false,
93 objects: results,
94 total: results.length,
95 time: new Date().toISOString(),
96 } satisfies NpmSearchResponse
97 },
98 { default: emptySearchResponse },
99 )
100
101 return asyncData
102}