@@ -3,7 +3,9 @@ import type {
33 PackumentVersion ,
44 SlimPackument ,
55 NpmSearchResponse ,
6+ NpmSearchResult ,
67 NpmDownloadCount ,
8+ NpmPerson ,
79} from '#shared/types'
810
911const NPM_REGISTRY = 'https://registry.npmjs.org'
@@ -216,3 +218,111 @@ export function useNpmSearch(
216218 { default : ( ) => lastSearch || emptySearchResponse } ,
217219 )
218220}
221+
222+ /**
223+ * Fetch all package names in an npm organization
224+ * Uses the /-/org/{org}/package endpoint
225+ */
226+ async function fetchOrgPackageNames ( orgName : string ) : Promise < string [ ] > {
227+ const data = await $fetch < Record < string , string > > (
228+ `${ NPM_REGISTRY } /-/org/${ encodeURIComponent ( orgName ) } /package` ,
229+ )
230+ return Object . keys ( data )
231+ }
232+
233+ /**
234+ * Minimal packument data needed for package cards
235+ */
236+ interface MinimalPackument {
237+ 'name' : string
238+ 'description' ?: string
239+ 'dist-tags' : Record < string , string >
240+ 'time' : Record < string , string >
241+ 'maintainers' ?: NpmPerson [ ]
242+ }
243+
244+ /**
245+ * Fetch minimal packument data for a single package
246+ */
247+ async function fetchMinimalPackument ( name : string ) : Promise < MinimalPackument | null > {
248+ try {
249+ const encoded = encodePackageName ( name )
250+ return await $fetch < MinimalPackument > ( `${ NPM_REGISTRY } /${ encoded } ` , {
251+ // Only fetch the fields we need using Accept header
252+ // Note: npm registry doesn't support field filtering, so we get full packument
253+ // but we only use what we need
254+ } )
255+ } catch {
256+ // Package might not exist or be private
257+ return null
258+ }
259+ }
260+
261+ /**
262+ * Convert packument to search result format for display
263+ */
264+ function packumentToSearchResult ( pkg : MinimalPackument ) : NpmSearchResult {
265+ const latestVersion = pkg [ 'dist-tags' ] . latest || Object . values ( pkg [ 'dist-tags' ] ) [ 0 ] || ''
266+ const modified = pkg . time . modified || pkg . time [ latestVersion ] || ''
267+
268+ return {
269+ package : {
270+ name : pkg . name ,
271+ version : latestVersion ,
272+ description : pkg . description ,
273+ date : pkg . time [ latestVersion ] || modified ,
274+ links : {
275+ npm : `https://www.npmjs.com/package/${ pkg . name } ` ,
276+ } ,
277+ maintainers : pkg . maintainers ,
278+ } ,
279+ score : { final : 0 , detail : { quality : 0 , popularity : 0 , maintenance : 0 } } ,
280+ searchScore : 0 ,
281+ updated : modified ,
282+ }
283+ }
284+
285+ /**
286+ * Fetch all packages for an npm organization
287+ * Returns search-result-like objects for compatibility with PackageList
288+ */
289+ export function useOrgPackages ( orgName : MaybeRefOrGetter < string > ) {
290+ return useLazyAsyncData (
291+ ( ) => `org-packages:${ toValue ( orgName ) } ` ,
292+ async ( ) => {
293+ const org = toValue ( orgName )
294+ if ( ! org ) {
295+ return emptySearchResponse
296+ }
297+
298+ // Get all package names in the org
299+ const packageNames = await fetchOrgPackageNames ( org )
300+
301+ if ( packageNames . length === 0 ) {
302+ return emptySearchResponse
303+ }
304+
305+ // Fetch packuments in parallel (with concurrency limit)
306+ const concurrency = 10
307+ const results : NpmSearchResult [ ] = [ ]
308+
309+ for ( let i = 0 ; i < packageNames . length ; i += concurrency ) {
310+ const batch = packageNames . slice ( i , i + concurrency )
311+ const packuments = await Promise . all ( batch . map ( name => fetchMinimalPackument ( name ) ) )
312+
313+ for ( const pkg of packuments ) {
314+ if ( pkg ) {
315+ results . push ( packumentToSearchResult ( pkg ) )
316+ }
317+ }
318+ }
319+
320+ return {
321+ objects : results ,
322+ total : results . length ,
323+ time : new Date ( ) . toISOString ( ) ,
324+ } satisfies NpmSearchResponse
325+ } ,
326+ { default : ( ) => emptySearchResponse } ,
327+ )
328+ }
0 commit comments