Skip to content

Commit 58f5462

Browse files
committed
feat: add dependents sorted by download
1 parent 2df63c6 commit 58f5462

3 files changed

Lines changed: 131 additions & 0 deletions

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<script setup lang="ts">
2+
import { usePackageDependents } from '~/composables/useNpmRegistry'
3+
import { formatCompactNumber } from '~/utils/formatters'
4+
5+
const props = defineProps<{
6+
packageName: string
7+
}>()
8+
9+
const { data, status } = usePackageDependents(() => props.packageName)
10+
11+
const dependents = computed(() => data.value?.objects ?? [])
12+
const total = computed(() => data.value?.total ?? 0)
13+
14+
// Expanded state for showing all dependents
15+
const expanded = ref(false)
16+
17+
// Show first 10 by default, all when expanded
18+
const visibleDependents = computed(() => {
19+
if (expanded.value) {
20+
return dependents.value
21+
}
22+
return dependents.value.slice(0, 10)
23+
})
24+
25+
// Show section only when we have dependents or are loading
26+
const showSection = computed(() => {
27+
return status.value === 'pending' || dependents.value.length > 0
28+
})
29+
</script>
30+
31+
<template>
32+
<section v-if="showSection" aria-labelledby="dependents-heading">
33+
<h2 id="dependents-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
34+
Dependents
35+
<span v-if="status === 'success' && total > 0">({{ total.toLocaleString() }})</span>
36+
</h2>
37+
38+
<!-- Loading state -->
39+
<div v-if="status === 'pending'" class="space-y-2">
40+
<div v-for="i in 5" :key="i" class="flex items-center justify-between py-1">
41+
<div class="skeleton h-4 rounded" :style="{ width: `${60 + (i % 3) * 20}px` }" />
42+
<div class="skeleton h-3 w-12 rounded" />
43+
</div>
44+
</div>
45+
46+
<!-- Dependents list -->
47+
<ul
48+
v-else-if="dependents.length > 0"
49+
class="space-y-1 list-none m-0 p-0"
50+
aria-label="Packages that depend on this package"
51+
>
52+
<li
53+
v-for="dependent in visibleDependents"
54+
:key="dependent.package.name"
55+
class="flex items-center justify-between py-1 text-sm gap-2"
56+
>
57+
<NuxtLink
58+
:to="{ name: 'package', params: { package: dependent.package.name.split('/') } }"
59+
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate min-w-0"
60+
>
61+
{{ dependent.package.name }}
62+
</NuxtLink>
63+
<span
64+
v-if="dependent.downloads?.weekly"
65+
class="font-mono text-xs text-fg-subtle shrink-0 flex items-center gap-1"
66+
:title="`${dependent.downloads.weekly.toLocaleString()} weekly downloads`"
67+
>
68+
<span class="i-carbon-download w-3 h-3" aria-hidden="true" />
69+
{{ formatCompactNumber(dependent.downloads.weekly, { decimals: 1 }) }}
70+
</span>
71+
</li>
72+
</ul>
73+
74+
<!-- Expand button -->
75+
<button
76+
v-if="dependents.length > 10 && !expanded"
77+
type="button"
78+
class="mt-2 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
79+
@click="expanded = true"
80+
>
81+
show top {{ dependents.length }}
82+
</button>
83+
</section>
84+
</template>

app/composables/useNpmRegistry.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,3 +555,47 @@ export function getOutdatedTooltip(info: OutdatedDependencyInfo): string {
555555
}
556556
return `Patch update available (latest: ${info.latest})`
557557
}
558+
559+
// ============================================================================
560+
// Package Dependents
561+
// ============================================================================
562+
563+
/**
564+
* Fetch packages that depend on a given package (dependents).
565+
* Results are sorted by weekly download count (most downloaded first)
566+
* to help with security triage when vulnerabilities are discovered.
567+
*/
568+
export function usePackageDependents(
569+
packageName: MaybeRefOrGetter<string>,
570+
options: MaybeRefOrGetter<{ size?: number }> = {},
571+
) {
572+
return useLazyAsyncData(
573+
() => `dependents:${toValue(packageName)}:${JSON.stringify(toValue(options))}`,
574+
async () => {
575+
const name = toValue(packageName)
576+
if (!name) return emptySearchResponse
577+
578+
const { size = 50 } = toValue(options)
579+
580+
// Use the existing searchNpmPackages with depends-on: query
581+
// This finds packages that have `name` as a dependency
582+
const response = await searchNpmPackages(`depends-on:${name}`, { size })
583+
584+
// Sort by weekly downloads (descending) for security triage
585+
const sortedObjects = [...response.objects].sort((a, b) => {
586+
const aDownloads = a.downloads?.weekly ?? 0
587+
const bDownloads = b.downloads?.weekly ?? 0
588+
return bDownloads - aDownloads
589+
})
590+
591+
return {
592+
...response,
593+
objects: sortedObjects,
594+
}
595+
},
596+
{
597+
server: false,
598+
default: () => emptySearchResponse,
599+
},
600+
)
601+
}

app/pages/[...package].vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,9 @@ defineOgImageComponent('Package', {
789789
:peer-dependencies-meta="displayVersion?.peerDependenciesMeta"
790790
:optional-dependencies="displayVersion?.optionalDependencies"
791791
/>
792+
793+
<!-- Dependents (packages that depend on this one) -->
794+
<PackageDependents :package-name="pkg.name" />
792795
</div>
793796
</div>
794797
</article>

0 commit comments

Comments
 (0)