Skip to content
Merged
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
39 changes: 2 additions & 37 deletions app/components/PackageVulnerabilityTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -188,44 +188,9 @@ function getDepthStyle(depth: string | undefined) {
</div>
</section>

<!-- Loading state - muted -->
<section
v-else-if="status === 'pending' || status === 'idle'"
aria-labelledby="vuln-tree-loading"
>
<div class="rounded-lg border border-border bg-bg-subtle px-4 py-3">
<div class="flex items-center gap-2">
<span
class="i-carbon-circle-dash w-4 h-4 animate-spin motion-reduce:animate-none text-fg-subtle"
aria-hidden="true"
/>
<span class="text-sm text-fg-muted">{{ $t('package.vulnerabilities.scanning_tree') }}</span>
</div>
</div>
</section>
<!-- Loading state - hidden (loading indicator shown in stats banner) -->

<!-- No vulnerabilities found - muted, not attention-grabbing -->
<section
v-else-if="status === 'success' && !hasVulnerabilities"
aria-labelledby="vuln-tree-success"
>
<div class="rounded-lg border border-border bg-bg-subtle px-4 py-3">
<div class="flex items-center gap-2">
<span class="i-carbon-checkmark w-4 h-4 text-fg-subtle" aria-hidden="true" />
<span class="text-sm text-fg-muted">
{{ $t('package.vulnerabilities.no_known', { count: vulnTree?.totalPackages ?? 0 }) }}
</span>
</div>
<!-- Warning if some queries failed -->
<div
v-if="vulnTree?.failedQueries"
class="flex items-center gap-2 mt-2 text-xs text-fg-subtle"
>
<span class="i-carbon-warning w-3 h-3" aria-hidden="true" />
<span>{{ $t('package.vulnerabilities.packages_failed', vulnTree.failedQueries) }}</span>
</div>
</div>
</section>
<!-- No vulnerabilities found - don't show anything (count is shown in stats banner) -->

<!-- Error state - subtle, not alarming -->
<section v-else-if="status === 'error'" aria-labelledby="vuln-tree-error">
Expand Down
102 changes: 100 additions & 2 deletions app/pages/[...package].vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ const displayVersion = computed(() => {
return pkg.value.versions[latestTag] ?? null
})

// Fetch vulnerability tree (lazy, client-side)
// This is the same composable used by PackageVulnerabilityTree
const {
data: vulnTree,
status: vulnTreeStatus,
fetch: fetchVulnTree,
} = useVulnerabilityTree(packageName, () => displayVersion.value?.version ?? '')
onMounted(() => {
// Fetch vulnerability tree once displayVersion is available
if (displayVersion.value) {
fetchVulnTree()
}
})
watch(
() => displayVersion.value?.version,
() => {
if (displayVersion.value) {
fetchVulnTree()
}
},
)

// Keep latestVersion for comparison (to show "(latest)" badge)
const latestVersion = computed(() => {
if (!pkg.value) return null
Expand Down Expand Up @@ -129,6 +151,22 @@ const hasDependencies = computed(() => {
)
})

// Vulnerability count for the stats banner
const vulnCount = computed(() => vulnTree.value?.totalCounts.total ?? 0)
const hasVulnerabilities = computed(() => vulnCount.value > 0)

// Total transitive dependencies count (from either vuln tree or install size)
// Subtract 1 to exclude the root package itself
const totalDepsCount = computed(() => {
if (vulnTree.value) {
return vulnTree.value.totalPackages - 1
}
if (installSize.value) {
return installSize.value.dependencyCount
}
return null
})

const repositoryUrl = computed(() => {
const repo = displayVersion.value?.repository
if (!repo?.url) return null
Expand Down Expand Up @@ -726,7 +764,7 @@ defineOgImageComponent('Package', {

<!-- Stats grid -->
<dl
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 sm:gap-4 py-4 sm:py-6 mt-4 sm:mt-6 border-t border-border"
class="grid grid-cols-2 sm:grid-cols-5 gap-3 sm:gap-4 py-4 sm:py-6 mt-4 sm:mt-6 border-t border-border"
>
<div v-if="pkg.license" class="space-y-1">
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
Expand All @@ -742,7 +780,31 @@ defineOgImageComponent('Package', {
{{ $t('package.stats.deps') }}
</dt>
<dd class="font-mono text-sm text-fg flex items-center justify-start gap-2">
{{ getDependencyCount(displayVersion) }}
<!-- Direct deps (muted) -->
<span class="text-fg-muted">{{ getDependencyCount(displayVersion) }}</span>

<!-- Separator and total transitive deps -->
<span class="text-fg-subtle mx-1">/</span>

<ClientOnly>
<span
v-if="
vulnTreeStatus === 'pending' || (installSizeStatus === 'pending' && !vulnTree)
"
class="inline-flex items-center gap-1 text-fg-subtle"
>
<span
class="i-carbon-circle-dash w-3 h-3 motion-safe:animate-spin"
aria-hidden="true"
/>
</span>
<span v-else-if="totalDepsCount !== null">{{ totalDepsCount }}</span>
<span v-else class="text-fg-subtle">-</span>
<template #fallback>
<span class="text-fg-subtle">-</span>
</template>
</ClientOnly>

<a
v-if="getDependencyCount(displayVersion) > 0"
:href="`https://npmgraph.js.org/?q=${pkg.name}`"
Expand Down Expand Up @@ -809,6 +871,42 @@ defineOgImageComponent('Package', {
</dd>
</div>

<!-- Vulnerabilities count -->
<ClientOnly>
<div class="space-y-1">
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
{{ $t('package.stats.vulns') }}
</dt>
<dd class="font-mono text-sm text-fg">
<span
v-if="vulnTreeStatus === 'pending' || vulnTreeStatus === 'idle'"
class="inline-flex items-center gap-1 text-fg-subtle"
>
<span
class="i-carbon-circle-dash w-3 h-3 motion-safe:animate-spin"
aria-hidden="true"
/>
</span>
<span v-else-if="vulnTreeStatus === 'success'">
<span v-if="hasVulnerabilities" class="text-amber-500">{{ vulnCount }}</span>
<span v-else class="inline-flex items-center gap-1 text-fg-muted">
<span class="i-carbon-checkmark w-3 h-3" aria-hidden="true" />
0
</span>
</span>
<span v-else class="text-fg-subtle">-</span>
</dd>
</div>
<template #fallback>
<div class="space-y-1">
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
{{ $t('package.stats.vulns') }}
</dt>
<dd class="font-mono text-sm text-fg-subtle">-</dd>
</div>
</template>
</ClientOnly>

<div v-if="pkg.time?.modified" class="space-y-1">
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
{{ $t('package.stats.updated') }}
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"license": "License",
"deps": "Deps",
"install_size": "Install Size",
"vulns": "Vulns",
"updated": "Updated",
"view_dependency_graph": "View dependency graph",
"inspect_dependency_tree": "Inspect dependency tree"
Expand Down
1 change: 1 addition & 0 deletions lunaria/files/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"license": "License",
"deps": "Deps",
"install_size": "Install Size",
"vulns": "Vulns",
"updated": "Updated",
"view_dependency_graph": "View dependency graph",
"inspect_dependency_tree": "Inspect dependency tree"
Expand Down