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
29 changes: 26 additions & 3 deletions packages/devtools/client/components/ModuleItem.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { InstalledModuleInfo } from '../../src/types'
import { computed } from 'vue'
import type { VersionScoreSlim } from '~/composables/state-module-scores'
import { computed, ref } from 'vue'
import { useCurrentTerminalId } from '~/composables/state-routes'

const props = defineProps<{
Expand All @@ -14,6 +15,8 @@ const data = computed(() => ({
...staticInfo.value,
}))
const terminalId = useCurrentTerminalId()
const healthDropdownShown = ref(false)
const moduleScoreRef = ref<{ versionScore: VersionScoreSlim | null } | null>(null)
</script>

<template>
Expand All @@ -24,9 +27,11 @@ const terminalId = useCurrentTerminalId()
<FilepathItem :filepath="mod.entryPath" text-sm op50 hover="text-primary op100" />
</div>

<!-- NPM Version bump -->
<!-- NPM Version + Health Score -->
<NpmVersionCheck v-if="data.npm" :key="data.npm" :package-name="data.npm" :options="{ dev: true }">
<template #default="{ info, update, state, id, restart }">
<!-- Health Score with installed version -->
<ModuleScoreItem ref="moduleScoreRef" :npm="data.npm" :version="info?.current" />
<NuxtLink
v-if="state === 'running'" flex="~ gap-2"
animate-pulse items-center
Expand All @@ -47,7 +52,7 @@ const terminalId = useCurrentTerminalId()
<code text-xs>Update installed, click to restart</code>
</button>
</div>
<div v-else-if="info?.needsUpdate" mx--2>
<div v-else-if="info?.needsUpdate" mx--2 flex="~ gap-1 items-center">
<button
flex="~ gap-2" title="Click to upgrade" items-center rounded px2 text-sm
hover="bg-active"
Expand All @@ -58,6 +63,24 @@ const terminalId = useCurrentTerminalId()
<div i-carbon-arrow-right op50 />
<code text-green>v{{ info.latest }}</code>
</button>
<VDropdown v-model:shown="healthDropdownShown">
<button
v-tooltip="'Health comparison'"
flex-none rounded p1.5 text-rose-500
hover="bg-rose-500/10"
>
<span i-carbon-health-cross text-lg />
</button>
<template #popper>
<ModuleUpgradePopup
:npm="data.npm"
:current-version="info.current"
:latest-version="info.latest"
:score-data="moduleScoreRef?.versionScore"
@upgrade="update(); healthDropdownShown = false"
/>
</template>
</VDropdown>
</div>
<div v-else-if="info?.latest" flex="~ gap-2" items-center title="NPM">
<span i-carbon-cube flex-none text-lg op50 />
Expand Down
3 changes: 3 additions & 0 deletions packages/devtools/client/components/ModuleItemInstall.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,8 @@ const anyObj = {} as any
</NButton>
</NDropdown>
</template>
<template #items>
<ModuleScoreItem :npm="item.npm" />
</template>
</ModuleItemBase>
</template>
30 changes: 30 additions & 0 deletions packages/devtools/client/components/ModuleScoreBadge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { computed } from 'vue'
import { statusColors, useModuleScores } from '~/composables/state-module-scores'

const props = defineProps<{
npm?: string
}>()

const { scores, loading } = useModuleScores()
const score = computed(() => props.npm ? scores.value.get(props.npm) : undefined)
const color = computed(() => score.value ? statusColors[score.value.status] : undefined)
</script>

<template>
<span v-if="loading && !score" class="n-badge" op50>
<span i-carbon-circle-dash animate-spin />
</span>
<a
v-else-if="score"
inline-flex items-center
:href="`https://nuxt.care/?search=npm:${npm}`"
target="_blank"
rel="noopener"
:title="`Nuxt Care Score: ${score.score}/100 (${score.status})`"
>
<span class="n-badge" :style="{ color, borderColor: color }">
{{ score.status }}
</span>
</a>
</template>
145 changes: 145 additions & 0 deletions packages/devtools/client/components/ModuleScoreItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<script setup lang="ts">
import type { VersionScoreSlim } from '~/composables/state-module-scores'
import { computed, ref, watch } from 'vue'
import { formatAge, getAgeColor, statusColors } from '~/composables/state-module-scores'

const props = defineProps<{
npm?: string
version?: string
}>()

const API_BASE = 'https://nuxt.care/api/v1'

const versionScore = ref<VersionScoreSlim | null>(null)
const loading = ref(false)
const error = ref(false)

// Fetch version-specific score (installed modules use their version, others use "latest")
watch(
() => [props.npm, props.version] as const,
async ([npm, version]) => {
if (!npm)
return

error.value = false
loading.value = true

try {
const params = new URLSearchParams({
package: npm,
version: version || 'latest',
slim: 'true',
})
const res = await fetch(`${API_BASE}/score?${params}`)
if (res.ok)
versionScore.value = await res.json()
else
error.value = true
}
catch (e) {
console.warn('[DevTools] Failed to fetch version score:', e)
error.value = true
}
finally {
loading.value = false
}
},
{ immediate: true },
)

const score = computed(() => versionScore.value)
const isLoading = computed(() => loading.value && !score.value)
const color = computed(() => score.value ? statusColors[score.value.status] : undefined)
const statusLabel = computed(() => score.value?.status ? score.value.status[0]?.toUpperCase() + score.value.status.slice(1) : '')

const showUpgrade = computed(() =>
versionScore.value?.recommendation === 'upgrade' && !versionScore.value?.isLatest,
)
const latestColor = computed(() =>
versionScore.value?.latestStatus ? statusColors[versionScore.value.latestStatus] : undefined,
)

defineExpose({ versionScore })
</script>

<template>
<div flex="~ gap-2 items-center">
<template v-if="isLoading">
<span i-carbon-circle-dash flex-none animate-spin text-lg op50 />
<span text-sm op50>Loading health...</span>
</template>

<template v-else-if="error">
<span i-carbon-warning-alt flex-none text-lg op50 />
<span text-sm op50>Health unavailable</span>
</template>

<template v-else-if="score">
<VDropdown :triggers="['hover']" :delay="{ show: 300, hide: 100 }">
<a
flex="~ gap-2 items-center"
:href="`https://nuxt.care/?search=npm:${npm}`"
target="_blank"
rel="noopener"
hover="op100"
>
<span i-carbon-health-cross flex-none text-lg op50 />
<span text-sm op50>Health</span>
<span text-sm font-medium :style="{ color }">{{ score.score }}/100</span>
<span rounded-full px1.5 py0.5 text-xs :style="{ backgroundColor: `${color}20`, color }">
{{ statusLabel }}
</span>
</a>
<template #popper>
<!-- Version-specific details (installed modules) -->
<div v-if="versionScore" min-w-48 p2 text-sm>
<div mb2 font-medium>
v{{ versionScore.version }}
</div>
<div flex="~ col gap-1" text-xs>
<div flex="~ justify-between">
<span op50>Vulnerabilities</span>
<span :class="versionScore.vulnCount > 0 ? 'text-red-500' : 'text-green-500'">
{{ versionScore.vulnCount }}
</span>
</div>
<div flex="~ justify-between items-center">
<span op50>Tests</span>
<span v-if="versionScore.hasTests" i-carbon-checkmark-filled text-green-500 />
<span v-else i-carbon-close-filled text-red-400 />
</div>
<div flex="~ justify-between items-center">
<span op50>TypeScript</span>
<span v-if="versionScore.hasTypes" i-carbon-checkmark-filled text-green-500 />
<span v-else i-carbon-close-filled text-red-400 />
</div>
<div flex="~ justify-between items-center">
<span op50>CI</span>
<span v-if="versionScore.ciPassing === true" i-carbon-checkmark-filled text-green-500 />
<span v-else-if="versionScore.ciPassing === false" i-carbon-close-filled text-red-400 />
<span v-else i-carbon-help op30 />
</div>
<div flex="~ justify-between">
<span op50>Published</span>
<span :class="getAgeColor(versionScore.daysSincePublish)">{{ formatAge(versionScore.daysSincePublish, true) }}</span>
</div>
<div v-if="versionScore.deprecated" mt1 flex="~ gap-1 items-center" text-red-500>
<span i-carbon-warning-alt />
<span>Deprecated</span>
</div>
</div>
<div mt2 border-t="~ base" pt2 text-xs op50>
Click to view on nuxt.care
</div>
</div>
</template>
</VDropdown>

<template v-if="showUpgrade && versionScore">
<span i-carbon-arrow-right flex-none op30 />
<span text-xs op50>v{{ versionScore.latestVersion }}</span>
<span text-xs font-medium :style="{ color: latestColor }">({{ versionScore.latestScore }})</span>
</template>
</template>
</div>
</template>
Loading