Skip to content

Commit cfa8db0

Browse files
feat: added npm package scores
1 parent d4584fe commit cfa8db0

7 files changed

Lines changed: 147 additions & 0 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<script setup lang="ts">
2+
const props = defineProps<{
3+
packageName: string
4+
}>()
5+
6+
const { data: score, status } = usePackageScore(() => props.packageName)
7+
8+
const scoreMetrics = computed(() => {
9+
if (!score.value) return []
10+
return [
11+
{
12+
key: 'quality',
13+
value: score.value.detail.quality * 100,
14+
label: $t('package.scores.quality'),
15+
},
16+
{
17+
key: 'popularity',
18+
value: score.value.detail.popularity * 100,
19+
label: $t('package.scores.popularity'),
20+
},
21+
{
22+
key: 'maintenance',
23+
value: score.value.detail.maintenance * 100,
24+
label: $t('package.scores.maintenance'),
25+
},
26+
]
27+
})
28+
29+
function getScoreColor(percentage: number): string {
30+
if (percentage < 40) return 'oklch(0.55 0.12 25)'
31+
if (percentage < 70) return 'oklch(0.6 0.1 85)'
32+
return 'oklch(0.55 0.1 145)'
33+
}
34+
</script>
35+
36+
<template>
37+
<CollapsibleSection id="scores" :title="$t('package.scores.title')">
38+
<template #actions>
39+
<TooltipApp :text="$t('package.scores.source')">
40+
<span class="i-carbon:information w-3.5 h-3.5 text-fg-subtle" aria-hidden="true" />
41+
</TooltipApp>
42+
</template>
43+
<div v-if="status === 'pending'" class="flex flex-col gap-2">
44+
<div v-for="i in 3" :key="i" class="flex items-center gap-3">
45+
<SkeletonInline class="w-24 h-3" />
46+
<SkeletonInline class="flex-1 h-3 rounded-full" />
47+
<SkeletonInline class="w-8 h-3" />
48+
</div>
49+
</div>
50+
<div v-else-if="score" class="flex flex-col gap-2">
51+
<div v-for="metric in scoreMetrics" :key="metric.key" class="flex items-center gap-3">
52+
<span class="w-24 text-xs text-fg-subtle">{{ metric.label }}</span>
53+
<div class="flex-1 h-1.5 bg-border-subtle rounded-full overflow-hidden">
54+
<div
55+
class="h-full rounded-full"
56+
:style="{ width: `${metric.value}%`, background: getScoreColor(metric.value) }"
57+
/>
58+
</div>
59+
<span class="w-8 text-xs font-mono text-fg-muted text-right">
60+
{{ Math.round(metric.value) }}%
61+
</span>
62+
</div>
63+
</div>
64+
<p v-else class="text-fg-subtle text-sm">{{ $t('package.scores.unavailable') }}</p>
65+
</CollapsibleSection>
66+
</template>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { NpmsScore } from '#server/api/registry/score/[...pkg].get'
2+
3+
export function usePackageScore(name: MaybeRefOrGetter<string>) {
4+
return useLazyFetch<NpmsScore>(() => `/api/registry/score/${toValue(name)}`)
5+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,9 @@ defineOgImageComponent('Package', {
11011101
<!-- Download stats -->
11021102
<PackageWeeklyDownloadStats :packageName :createdIso="pkg?.time?.created ?? null" />
11031103

1104+
<!-- Package scores -->
1105+
<PackageScoreGauges :packageName />
1106+
11041107
<!-- Playground links -->
11051108
<PackagePlaygrounds
11061109
v-if="readmeData?.playgroundLinks?.length"

i18n/locales/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,14 @@
287287
"download_file": "Download {fileType}",
288288
"toggle_annotator": "Toggle annotator"
289289
},
290+
"scores": {
291+
"title": "Scores",
292+
"quality": "Quality",
293+
"popularity": "Popularity",
294+
"maintenance": "Maintenance",
295+
"unavailable": "Score data unavailable",
296+
"source": "Scores from npms.io"
297+
},
290298
"install_scripts": {
291299
"title": "Install Scripts",
292300
"script_label": "(script)",

lunaria/files/en-GB.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,14 @@
287287
"download_file": "Download {fileType}",
288288
"toggle_annotator": "Toggle annotator"
289289
},
290+
"scores": {
291+
"title": "Scores",
292+
"quality": "Quality",
293+
"popularity": "Popularity",
294+
"maintenance": "Maintenance",
295+
"unavailable": "Score data unavailable",
296+
"source": "Scores from npms.io"
297+
},
290298
"install_scripts": {
291299
"title": "Install Scripts",
292300
"script_label": "(script)",

lunaria/files/en-US.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,14 @@
287287
"download_file": "Download {fileType}",
288288
"toggle_annotator": "Toggle annotator"
289289
},
290+
"scores": {
291+
"title": "Scores",
292+
"quality": "Quality",
293+
"popularity": "Popularity",
294+
"maintenance": "Maintenance",
295+
"unavailable": "Score data unavailable",
296+
"source": "Scores from npms.io"
297+
},
290298
"install_scripts": {
291299
"title": "Install Scripts",
292300
"script_label": "(script)",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 NPMS_API = 'https://api.npms.io/v2/package'
6+
7+
export interface NpmsScore {
8+
final: number
9+
detail: {
10+
quality: number
11+
popularity: number
12+
maintenance: number
13+
}
14+
}
15+
16+
export default defineCachedEventHandler(
17+
async event => {
18+
const pkgParamSegments = getRouterParam(event, 'pkg')?.split('/') ?? []
19+
const { rawPackageName } = parsePackageParams(pkgParamSegments)
20+
21+
try {
22+
const { packageName } = v.parse(PackageRouteParamsSchema, {
23+
packageName: rawPackageName,
24+
})
25+
26+
const response = await fetch(`${NPMS_API}/${encodeURIComponent(packageName)}`)
27+
28+
if (!response.ok) {
29+
throw createError({ statusCode: response.status, message: 'Failed to fetch npms score' })
30+
}
31+
32+
const data = await response.json()
33+
return data.score as NpmsScore
34+
} catch (error: unknown) {
35+
handleApiError(error, {
36+
statusCode: 502,
37+
message: 'Failed to fetch package score from npms.io',
38+
})
39+
}
40+
},
41+
{
42+
maxAge: CACHE_MAX_AGE_ONE_HOUR,
43+
swr: true,
44+
getKey: event => {
45+
const pkg = getRouterParam(event, 'pkg') ?? ''
46+
return `npms-score:${pkg}`
47+
},
48+
},
49+
)

0 commit comments

Comments
 (0)