Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Welcome to the Nuxt website repository available on [nuxt.com](https://nuxt.com).

[![Nuxt UI](https://img.shields.io/badge/Made%20with-Nuxt%20UI-00DC82?logo=nuxt.js&labelColor=020420)](https://ui.nuxt.com)
[![nuxt.care](https://img.shields.io/badge/Health%20by-nuxt.care-84cc16?labelColor=020420)](https://nuxt.care)

## Setup

Expand Down
2 changes: 1 addition & 1 deletion app/components/AdminDashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const items = computed<DropdownMenuItem[][]>(() => [
]
])

const { data: rawFeedback, refresh: refreshFeedback } = await useFetch('/api/feedback')
const { data: rawFeedback, refresh: refreshFeedback } = await useFetch<FeedbackItem[]>('/api/feedback')
const { deleteFeedback } = useFeedbackDelete()
const { exportFeedbackData, exportPageAnalytics } = useFeedbackExport()

Expand Down
17 changes: 16 additions & 1 deletion app/components/module/ModuleItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const items = computed(() => [
container: 'flex flex-col',
wrapper: 'flex flex-col min-h-0 items-start',
body: 'flex-none',
footer: 'w-full mt-auto pointer-events-auto pt-4 z-[1]'
footer: 'w-full mt-auto pointer-events-auto pt-4 z-1'
}"
@click="handleCardClick"
>
Expand Down Expand Up @@ -166,6 +166,21 @@ const items = computed(() => [
</NuxtLink>
</UTooltip>

<template v-if="module.health">
<UTooltip :text="`Health: ${module.health.status} - ${module.health.score}/100`">
<NuxtLink
:to="`https://nuxt.care/?search=npm:${module.npm}`"
class="flex items-center gap-1 hover:text-highlighted"
target="_blank"
>
<UIcon name="i-lucide-heart-pulse" class="size-4 shrink-0" :style="{ color: module.health.color }" />
<span class="text-sm font-medium whitespace-normal">
{{ module.health.score }}
</span>
</NuxtLink>
</UTooltip>
</template>

<UTooltip v-if="selectedSort.key === 'publishedAt'" :text="`Updated ${formatDateByLocale('en', module.stats.publishedAt)}`">
<NuxtLink
class="flex items-center gap-1 hover:text-highlighted"
Expand Down
16 changes: 15 additions & 1 deletion app/pages/modules/[slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ defineOgImageComponent('Module', {
:icon="moduleIcon(module.category)"
:alt="module.name"
size="xl"
class="-m-[4px] rounded-none bg-transparent"
class="-m-1 rounded-none bg-transparent"
/>

<div>
Expand Down Expand Up @@ -151,6 +151,20 @@ defineOgImageComponent('Module', {
</NuxtLink>
</UTooltip>

<template v-if="module.health">
<span class="hidden lg:block text-muted">&bull;</span>
<UTooltip :text="`Health: ${module.health.status} - ${module.health.score}/100`">
<NuxtLink
:to="`https://nuxt.care/?search=npm:${module.npm}`"
class="flex items-center gap-1.5"
target="_blank"
>
<UIcon name="i-lucide-heart-pulse" class="size-5 shrink-0" :style="{ color: module.health.color }" />
<span class="text-sm font-medium">{{ module.health.score }}</span>
</NuxtLink>
</UTooltip>
</template>

<div class="mx-3 h-6 border-l border-gray-200 dark:border-gray-800 w-px hidden lg:block" />

<div v-for="(maintainer, index) in module.maintainers" :key="maintainer.github" class="flex items-center gap-3">
Expand Down
8 changes: 5 additions & 3 deletions server/api/v1/modules/[name].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ export default defineCachedEventHandler(async (event) => {
})
}

const [stats, contributors, readme] = await Promise.all([
const [stats, contributors, readme, bulkHealth] = await Promise.all([
fetchModuleStats(event, module),
fetchModuleContributors(event, module),
fetchModuleReadme(event, module)
fetchModuleReadme(event, module),
fetchBulkModuleHealth(event, [module])
])
return {
...module,
generatedAt: new Date().toISOString(),
contributors,
stats,
readme
readme,
health: bulkHealth[module.name] || null
} satisfies Module
}, {
name: 'modules:v1',
Expand Down
6 changes: 5 additions & 1 deletion server/api/v1/modules/index.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export default defineCachedEventHandler(async (event) => {
modules: string[]
}

const bulkNpmStats = await npm.fetchBulkPackageStats(modules.map(m => m.npm), 'last-month')
const [bulkNpmStats, bulkHealth] = await Promise.all([
npm.fetchBulkPackageStats(modules.map(m => m.npm), 'last-month'),
fetchBulkModuleHealth(event, modules)
])

const maintainers: Record<string, MaintainerWithModules> = {}
const contributors: Record<string, ContributorWithModules> = {}
Expand All @@ -66,6 +69,7 @@ export default defineCachedEventHandler(async (event) => {
])
module.stats = mStats
module.contributors = mContributors
module.health = bulkHealth[module.name] || null

if (module.maintainers) {
for (const maintainer of module.maintainers) {
Expand Down
69 changes: 68 additions & 1 deletion server/utils/module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { parseMarkdown } from '@nuxtjs/mdc/runtime'

import type { H3Event } from 'h3'
import type { BaseModule, Module, ModuleContributor, ModuleStats } from '#shared/types'
import type { BaseModule, Module, ModuleContributor, ModuleHealth, ModuleStats } from '#shared/types'
import type { NpmDownloadStats } from '../types/npm'

export function isBot(username: string) {
Expand Down Expand Up @@ -97,6 +97,73 @@ export async function fetchModuleContributors(_event: H3Event, module: BaseModul
}
}

interface NuxtCareModuleSlim {
name: string
npm: string
score: number
status: string
lastUpdated: string | null
}

export async function fetchBulkModuleHealth(_event: H3Event, modules: BaseModule[]): Promise<Record<string, ModuleHealth>> {
const result: Record<string, ModuleHealth> = {}
const uncached: BaseModule[] = []

// Check KV cache first
for (const module of modules) {
const cached = await kv.get<ModuleHealth>(`module:health:${module.name}`)
if (cached) {
result[module.name] = cached
} else {
uncached.push(module)
}
}

if (!uncached.length) return result

const CHUNK_SIZE = 50
const statusColorMap: Record<string, string> = {
optimal: '#22c55e',
stable: '#84cc16',
degraded: '#eab308',
critical: '#ef4444',
unknown: '#6b7280'
}
const npmToModule = new Map(uncached.map(m => [m.npm, m]))

console.info(`Fetching health for ${uncached.length} modules from nuxt.care (${Math.ceil(uncached.length / CHUNK_SIZE)} chunks)...`)
for (let i = 0; i < uncached.length; i += CHUNK_SIZE) {
const chunk = uncached.slice(i, i + CHUNK_SIZE)
try {
const query = new URLSearchParams()
query.set('slim', 'true')
for (const m of chunk) {
query.append('package', m.npm)
}
const data = await $fetch<NuxtCareModuleSlim[]>(`https://nuxt.care/api/v1/modules?${query.toString()}`, {
timeout: 10_000,
retry: 2,
retryDelay: 1000
})
for (const item of data) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const module = npmToModule.get(item.npm)
if (!module) continue
const health: ModuleHealth = {
score: item.score,
color: statusColorMap[item.status] || '#6b7280',
status: item.status
}
result[module.name] = health
await kv.set(`module:health:${module.name}`, health, { ttl: 60 * 60 * 24 })
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
} catch (err) {
console.error(`Cannot fetch bulk health from nuxt.care (chunk ${Math.floor(i / CHUNK_SIZE) + 1}): ${err}`)
}
}

return result
}

export async function fetchModuleReadme(_event: H3Event, module: BaseModule) {
console.info(`Fetching module ${module.name} readme ...`)
const readme = await $fetch(`https://unpkg.com/${module.npm}/README.md`).catch(() => {
Expand Down
7 changes: 7 additions & 0 deletions shared/types/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,15 @@ export interface ModuleStats {
createdAt: number
}

export interface ModuleHealth {
score: number
color: string
status: string
}

export interface Module extends BaseModule {
stats?: ModuleStats
health?: ModuleHealth | null
contributors?: ModuleContributor[]
maintainers?: ModuleMaintainer[]
readme?: MDCParserResult
Expand Down