Skip to content

Commit 6d6a3c7

Browse files
committed
Configure devminer for dependents download counts
Note: The data from /registry is stale currently, but is being refreshed. Work to add download counts to /live_registry is in progress.
1 parent 9ca57f5 commit 6d6a3c7

3 files changed

Lines changed: 146 additions & 33 deletions

File tree

app/components/PackageDependents.vue

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const props = defineProps<{
88
99
const { data, status } = usePackageDependents(() => props.packageName)
1010
11-
const dependents = computed(() => data.value?.objects ?? [])
11+
const dependents = computed(() => data.value?.dependents ?? [])
1212
const total = computed(() => data.value?.total ?? 0)
1313
1414
// Expanded state for showing all dependents
@@ -32,7 +32,7 @@ const showSection = computed(() => {
3232
<section v-if="showSection" aria-labelledby="dependents-heading">
3333
<h2 id="dependents-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
3434
Dependents
35-
<span v-if="status === 'success' && total > 0">({{ total.toLocaleString() }})</span>
35+
<span v-if="status === 'success' && total > 0">(top {{ total.toLocaleString() }})</span>
3636
</h2>
3737

3838
<!-- Loading state -->
@@ -51,22 +51,22 @@ const showSection = computed(() => {
5151
>
5252
<li
5353
v-for="dependent in visibleDependents"
54-
:key="dependent.package.name"
54+
:key="dependent.name"
5555
class="flex items-center justify-between py-1 text-sm gap-2"
5656
>
5757
<NuxtLink
58-
:to="{ name: 'package', params: { package: dependent.package.name.split('/') } }"
58+
:to="{ name: 'package', params: { package: dependent.name.split('/') } }"
5959
class="font-mono text-fg-muted hover:text-fg transition-colors duration-200 truncate min-w-0"
6060
>
61-
{{ dependent.package.name }}
61+
{{ dependent.name }}
6262
</NuxtLink>
6363
<span
64-
v-if="dependent.downloads?.weekly"
64+
v-if="dependent.downloads"
6565
class="font-mono text-xs text-fg-subtle shrink-0 flex items-center gap-1"
66-
:title="`${dependent.downloads.weekly.toLocaleString()} weekly downloads`"
66+
:title="`${dependent.downloads.toLocaleString()} downloads`"
6767
>
6868
<span class="i-carbon-download w-3 h-3" aria-hidden="true" />
69-
{{ formatCompactNumber(dependent.downloads.weekly, { decimals: 1 }) }}
69+
{{ formatCompactNumber(dependent.downloads, { decimals: 1 }) }}
7070
</span>
7171
</li>
7272
</ul>

app/composables/useNpmRegistry.ts

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -612,42 +612,43 @@ export function getOutdatedTooltip(info: OutdatedDependencyInfo): string {
612612
// Package Dependents
613613
// ============================================================================
614614

615+
export interface DependentPackage {
616+
name: string
617+
downloads: number
618+
description?: string
619+
version?: string
620+
}
621+
622+
export interface DependentsResponse {
623+
dependents: DependentPackage[]
624+
total: number
625+
}
626+
627+
const emptyDependentsResponse: DependentsResponse = {
628+
dependents: [],
629+
total: 0,
630+
}
631+
615632
/**
616633
* Fetch packages that depend on a given package (dependents).
617-
* Results are sorted by weekly download count (most downloaded first)
634+
* Uses the e18e CouchDB mirror to get accurate dependency data.
635+
* Results are sorted by download count (most downloaded first)
618636
* to help with security triage when vulnerabilities are discovered.
619637
*/
620-
export function usePackageDependents(
621-
packageName: MaybeRefOrGetter<string>,
622-
options: MaybeRefOrGetter<{ size?: number }> = {},
623-
) {
638+
export function usePackageDependents(packageName: MaybeRefOrGetter<string>) {
624639
return useLazyAsyncData(
625-
() => `dependents:${toValue(packageName)}:${JSON.stringify(toValue(options))}`,
640+
() => `dependents:${toValue(packageName)}`,
626641
async () => {
627642
const name = toValue(packageName)
628-
if (!name) return emptySearchResponse
629-
630-
const { size = 50 } = toValue(options)
631-
632-
// Use the existing searchNpmPackages with depends-on: query
633-
// This finds packages that have `name` as a dependency
634-
const response = await searchNpmPackages(`depends-on:${name}`, { size })
635-
636-
// Sort by weekly downloads (descending) for security triage
637-
const sortedObjects = [...response.objects].sort((a, b) => {
638-
const aDownloads = a.downloads?.weekly ?? 0
639-
const bDownloads = b.downloads?.weekly ?? 0
640-
return bDownloads - aDownloads
641-
})
643+
if (!name) return emptyDependentsResponse
642644

643-
return {
644-
...response,
645-
objects: sortedObjects,
646-
}
645+
return await $fetch<DependentsResponse>(
646+
`/api/registry/dependents/${encodeURIComponent(name)}`,
647+
)
647648
},
648649
{
649650
server: false,
650-
default: () => emptySearchResponse,
651+
default: () => emptyDependentsResponse,
651652
},
652653
)
653654
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import * as v from 'valibot'
2+
import { PackageRouteParamsSchema } from '#shared/schemas/package'
3+
import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants'
4+
5+
const E18E_LIVE_REGISTRY_URL = 'https://npm.devminer.xyz/live_registry'
6+
const E18E_REGISTRY_URL = 'https://npm.devminer.xyz/registry'
7+
8+
interface DependentsViewResponse {
9+
total_rows: number
10+
offset: number
11+
rows: {
12+
id: string
13+
key: string
14+
value: { name: string; version: string }
15+
}[]
16+
}
17+
18+
interface DownloadsViewResponse {
19+
total_rows: number
20+
offset: number
21+
rows: {
22+
id: string
23+
key: string
24+
value: number
25+
}[]
26+
}
27+
28+
export interface DependentPackage {
29+
name: string
30+
downloads: number
31+
version?: string
32+
}
33+
34+
export interface DependentsResponse {
35+
dependents: DependentPackage[]
36+
total: number
37+
}
38+
39+
/**
40+
* GET /api/registry/dependents/:name
41+
*
42+
* Fetch packages that depend on the given package using the e18e CouchDB mirror.
43+
* Uses CouchDB views for efficient lookups, then fetches download stats separately.
44+
* Results are sorted by download count (most downloaded first) for security triage.
45+
*/
46+
export default defineCachedEventHandler(
47+
async (event): Promise<DependentsResponse> => {
48+
const pkgParamSegments = getRouterParam(event, 'pkg')?.split('/') ?? []
49+
const { rawPackageName } = parsePackageParams(pkgParamSegments)
50+
51+
try {
52+
const { packageName } = v.parse(PackageRouteParamsSchema, {
53+
packageName: rawPackageName,
54+
})
55+
56+
const dependentsResponse = await $fetch<DependentsViewResponse>(
57+
`${E18E_LIVE_REGISTRY_URL}/_design/dependents/_view/dependents2?key=${encodeURIComponent(JSON.stringify(packageName))}&limit=250`,
58+
)
59+
60+
if (dependentsResponse.rows.length === 0) {
61+
return { dependents: [], total: 0 }
62+
}
63+
64+
const packageNames = dependentsResponse.rows.map(row => row.value.name)
65+
66+
const downloadsResponse = await $fetch<DownloadsViewResponse>(
67+
`${E18E_REGISTRY_URL}/_design/downloads/_view/downloads`,
68+
{
69+
method: 'POST',
70+
headers: { 'Content-Type': 'application/json' },
71+
body: { keys: packageNames },
72+
},
73+
)
74+
75+
const downloadsMap = new Map<string, number>()
76+
for (const row of downloadsResponse.rows) {
77+
downloadsMap.set(row.key, row.value)
78+
}
79+
80+
const versionMap = new Map<string, string>()
81+
for (const row of dependentsResponse.rows) {
82+
versionMap.set(row.value.name, row.value.version)
83+
}
84+
85+
const dependents: DependentPackage[] = packageNames
86+
.map(name => ({
87+
name,
88+
downloads: downloadsMap.get(name) ?? 0,
89+
version: versionMap.get(name),
90+
}))
91+
.sort((a, b) => b.downloads - a.downloads)
92+
93+
return {
94+
dependents,
95+
total: dependents.length,
96+
}
97+
} catch {
98+
return {
99+
dependents: [],
100+
total: 0,
101+
}
102+
}
103+
},
104+
{
105+
maxAge: CACHE_MAX_AGE_ONE_HOUR,
106+
swr: true,
107+
getKey: event => {
108+
const pkg = getRouterParam(event, 'pkg') ?? ''
109+
return `dependents:v2:${pkg.replace(/\/+$/, '').trim()}`
110+
},
111+
},
112+
)

0 commit comments

Comments
 (0)