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
22 changes: 20 additions & 2 deletions app/components/PackageDependencies.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { useVulnerabilityTree } from '~/composables/useVulnerabilityTree'
import { useDependencyAnalysis } from '~/composables/useDependencyAnalysis'
import { SEVERITY_TEXT_COLORS, getHighestSeverity } from '#shared/utils/severity'

const props = defineProps<{
Expand All @@ -15,7 +15,7 @@ const props = defineProps<{
const outdatedDeps = useOutdatedDependencies(() => props.dependencies)

// Get vulnerability info from shared cache (already fetched by PackageVulnerabilityTree)
const { data: vulnTree } = useVulnerabilityTree(
const { data: vulnTree } = useDependencyAnalysis(
() => props.packageName,
() => props.version,
)
Expand All @@ -26,6 +26,12 @@ function getVulnerableDepInfo(depName: string) {
return vulnTree.value.vulnerablePackages.find(p => p.name === depName && p.depth === 'direct')
}

// Check if a dependency is deprecated (only direct deps)
function getDeprecatedDepInfo(depName: string) {
if (!vulnTree.value) return null
return vulnTree.value.deprecatedPackages.find(p => p.name === depName && p.depth === 'direct')
}

// Expanded state for each section
const depsExpanded = shallowRef(false)
const peerDepsExpanded = shallowRef(false)
Expand Down Expand Up @@ -120,6 +126,18 @@ const sortedOptionalDependencies = computed(() => {
<span class="i-carbon-security w-3 h-3 block" aria-hidden="true" />
<span class="sr-only">{{ $t('package.dependencies.view_vulnerabilities') }}</span>
</NuxtLink>
<NuxtLink
v-if="getDeprecatedDepInfo(dep)"
:to="{
name: 'package',
params: { package: [...dep.split('/'), 'v', getDeprecatedDepInfo(dep)!.version] },
}"
class="shrink-0 text-purple-500"
:title="getDeprecatedDepInfo(dep)!.message"
>
<span class="i-carbon-warning-hex w-3 h-3 block" aria-hidden="true" />
<span class="sr-only">{{ $t('package.deprecated.label') }}</span>
</NuxtLink>
<NuxtLink
:to="{ name: 'package', params: { package: [...dep.split('/'), 'v', version] } }"
class="font-mono text-xs text-right truncate"
Expand Down
120 changes: 120 additions & 0 deletions app/components/PackageDeprecatedTree.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<script setup lang="ts">
import type { DependencyDepth } from '#shared/types'

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

const { data: analysisData, status } = useDependencyAnalysis(
() => props.packageName,
() => props.version,
)

const isExpanded = shallowRef(false)
const showAll = shallowRef(false)

const hasDeprecated = computed(
() => analysisData.value && analysisData.value.deprecatedPackages.length > 0,
)

// Banner color - purple for deprecated
const bannerColor = 'border-purple-600/40 bg-purple-500/10 text-purple-700 dark:text-purple-400'

// Styling for each depth level
const depthStyles = {
root: {
bg: 'bg-purple-500/5 border-l-2 border-l-purple-600',
text: 'text-fg',
},
direct: {
bg: 'bg-purple-500/5 border-l-2 border-l-purple-500',
text: 'text-fg-muted',
},
transitive: {
bg: 'bg-purple-500/5 border-l-2 border-l-purple-400',
text: 'text-fg-muted',
},
} as const

function getDepthStyle(depth: DependencyDepth) {
return depthStyles[depth] || depthStyles.transitive
}
</script>

<template>
<section
v-if="status === 'success' && hasDeprecated"
aria-labelledby="deprecated-tree-heading"
class="relative"
>
<div class="rounded-lg border overflow-hidden" :class="bannerColor">
<!-- Header -->
<button
type="button"
class="w-full flex items-center justify-between gap-3 px-4 py-3 text-left transition-colors duration-200 hover:bg-white/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-fg/50"
:aria-expanded="isExpanded"
aria-controls="deprecated-tree-details"
@click="isExpanded = !isExpanded"
>
<div class="flex items-center gap-2 min-w-0">
<span class="i-carbon-warning-hex w-4 h-4 shrink-0" aria-hidden="true" />
<span class="font-mono text-sm font-medium truncate">
{{ $t('package.deprecated.tree_found', analysisData!.deprecatedPackages.length) }}
</span>
</div>
<span
class="i-carbon-chevron-down w-4 h-4 transition-transform duration-200 shrink-0"
:class="{ 'rotate-180': isExpanded }"
aria-hidden="true"
/>
</button>

<!-- Expandable details -->
<div
v-show="isExpanded"
id="deprecated-tree-details"
class="border-t border-border bg-bg-subtle"
>
<ul class="divide-y divide-border list-none m-0 p-0">
<li
v-for="pkg in analysisData!.deprecatedPackages.slice(0, showAll ? undefined : 5)"
:key="`${pkg.name}@${pkg.version}`"
class="px-4 py-3"
:class="getDepthStyle(pkg.depth).bg"
>
<div class="flex items-center gap-2 mb-1">
<!-- Path badge -->
<DependencyPathPopup v-if="pkg.path && pkg.path.length > 1" :path="pkg.path" />

<NuxtLink
:to="{
name: 'package',
params: { package: [...pkg.name.split('/'), 'v', pkg.version] },
}"
class="font-mono text-sm font-medium hover:underline truncate"
:class="getDepthStyle(pkg.depth).text"
>
{{ pkg.name }}@{{ pkg.version }}
</NuxtLink>
</div>
<p class="text-xs text-fg-muted m-0 line-clamp-2">
{{ pkg.message }}
</p>
</li>
</ul>

<button
v-if="analysisData!.deprecatedPackages.length > 5 && !showAll"
type="button"
class="w-full px-4 py-2 text-xs font-mono text-fg-muted hover:text-fg border-t border-border transition-colors duration-200"
@click="showAll = true"
>
{{
$t('package.deprecated.show_all', { count: analysisData!.deprecatedPackages.length })
}}
</button>
</div>
</div>
</section>
</template>
42 changes: 36 additions & 6 deletions app/components/PackageVersions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
<div class="flex items-center justify-between gap-2">
<NuxtLink
:to="versionRoute(row.primaryVersion.version)"
class="font-mono text-sm transition-colors duration-200 truncate"
class="font-mono text-sm transition-colors duration-200 truncate inline-flex items-center gap-1"
:class="
row.primaryVersion.deprecated
? 'text-red-400 hover:text-red-300'
Expand All @@ -377,6 +377,11 @@ function getTagVersions(tag: string): VersionDisplay[] {
: row.primaryVersion.version
"
>
<span
v-if="row.primaryVersion.deprecated"
class="i-carbon-warning-hex w-3.5 h-3.5 shrink-0"
aria-hidden="true"
/>
{{ row.primaryVersion.version }}
</NuxtLink>
<div class="flex items-center gap-2 shrink-0">
Expand Down Expand Up @@ -418,7 +423,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
<div class="flex items-center justify-between gap-2">
<NuxtLink
:to="versionRoute(v.version)"
class="font-mono text-xs transition-colors duration-200 truncate"
class="font-mono text-xs transition-colors duration-200 truncate inline-flex items-center gap-1"
:class="
v.deprecated
? 'text-red-400 hover:text-red-300'
Expand All @@ -430,6 +435,11 @@ function getTagVersions(tag: string): VersionDisplay[] {
: v.version
"
>
<span
v-if="v.deprecated"
class="i-carbon-warning-hex w-3 h-3 shrink-0"
aria-hidden="true"
/>
{{ v.version }}
</NuxtLink>
<div class="flex items-center gap-2 shrink-0">
Expand Down Expand Up @@ -510,7 +520,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
<div class="flex items-center justify-between gap-2">
<NuxtLink
:to="versionRoute(row.primaryVersion.version)"
class="font-mono text-xs transition-colors duration-200 truncate"
class="font-mono text-xs transition-colors duration-200 truncate inline-flex items-center gap-1"
:class="
row.primaryVersion.deprecated
? 'text-red-400 hover:text-red-300'
Expand All @@ -524,6 +534,11 @@ function getTagVersions(tag: string): VersionDisplay[] {
: row.primaryVersion.version
"
>
<span
v-if="row.primaryVersion.deprecated"
class="i-carbon-warning-hex w-3 h-3 shrink-0"
aria-hidden="true"
/>
{{ row.primaryVersion.version }}
</NuxtLink>
<div class="flex items-center gap-2 shrink-0">
Expand Down Expand Up @@ -580,7 +595,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
<NuxtLink
v-if="group.versions[0]?.version"
:to="versionRoute(group.versions[0]?.version)"
class="font-mono text-xs transition-colors duration-200 truncate"
class="font-mono text-xs transition-colors duration-200 truncate inline-flex items-center gap-1"
:class="
group.versions[0]?.deprecated
? 'text-red-400 hover:text-red-300'
Expand All @@ -594,6 +609,11 @@ function getTagVersions(tag: string): VersionDisplay[] {
: group.versions[0]?.version
"
>
<span
v-if="group.versions[0]?.deprecated"
class="i-carbon-warning-hex w-3 h-3 shrink-0"
aria-hidden="true"
/>
{{ group.versions[0]?.version }}
</NuxtLink>
</div>
Expand Down Expand Up @@ -636,7 +656,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
<NuxtLink
v-if="group.versions[0]?.version"
:to="versionRoute(group.versions[0]?.version)"
class="font-mono text-xs transition-colors duration-200 truncate"
class="font-mono text-xs transition-colors duration-200 truncate inline-flex items-center gap-1"
:class="
group.versions[0]?.deprecated
? 'text-red-400 hover:text-red-300'
Expand All @@ -650,6 +670,11 @@ function getTagVersions(tag: string): VersionDisplay[] {
: group.versions[0]?.version
"
>
<span
v-if="group.versions[0]?.deprecated"
class="i-carbon-warning-hex w-3 h-3 shrink-0"
aria-hidden="true"
/>
{{ group.versions[0]?.version }}
</NuxtLink>
</div>
Expand Down Expand Up @@ -690,7 +715,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
<div class="flex items-center justify-between gap-2">
<NuxtLink
:to="versionRoute(v.version)"
class="font-mono text-xs transition-colors duration-200 truncate"
class="font-mono text-xs transition-colors duration-200 truncate inline-flex items-center gap-1"
:class="
v.deprecated
? 'text-red-400 hover:text-red-300'
Expand All @@ -702,6 +727,11 @@ function getTagVersions(tag: string): VersionDisplay[] {
: v.version
"
>
<span
v-if="v.deprecated"
class="i-carbon-warning-hex w-3 h-3 shrink-0"
aria-hidden="true"
/>
{{ v.version }}
</NuxtLink>
<div class="flex items-center gap-2 shrink-0">
Expand Down
2 changes: 1 addition & 1 deletion app/components/PackageVulnerabilityTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const {
data: vulnTree,
status,
fetch: fetchVulnTree,
} = useVulnerabilityTree(
} = useDependencyAnalysis(
() => props.packageName,
() => props.version,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import type { VulnerabilityTreeResult } from '#shared/types/osv'
import type { VulnerabilityTreeResult } from '#shared/types/dependency-analysis'

/**
* Shared composable for vulnerability tree data.
* Shared composable for dependency analysis data (vulnerabilities, deprecated packages).
* Fetches once and caches the result so multiple components can use it.
* Before: useVulnerabilityTree - but now we use this for both vulnerabilities and deprecated packages.
*/
export function useVulnerabilityTree(
export function useDependencyAnalysis(
packageName: MaybeRefOrGetter<string>,
version: MaybeRefOrGetter<string>,
) {
// Build a stable key from the current values
const name = toValue(packageName)
const ver = toValue(version)
const key = `vuln-tree:v1:${name}@${ver}`
const key = `dep-analysis:v1:${name}@${ver}`

// Use useState for SSR-safe caching across components
const data = useState<VulnerabilityTreeResult | null>(key, () => null)
Expand All @@ -37,7 +38,7 @@ export function useVulnerabilityTree(
data.value = result
status.value = 'success'
} catch (e) {
error.value = e instanceof Error ? e : new Error('Failed to fetch vulnerabilities')
error.value = e instanceof Error ? e : new Error('Failed to fetch dependency analysis')
status.value = 'error'
}
}
Expand Down
12 changes: 9 additions & 3 deletions app/pages/[...package].vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,13 @@ const displayVersion = computed(() => {
return pkg.value.versions[latestTag] ?? null
})

// Fetch vulnerability tree (lazy, client-side)
// This is the same composable used by PackageVulnerabilityTree
// Fetch dependency analysis (lazy, client-side)
// This is the same composable used by PackageVulnerabilityTree and PackageDeprecatedTree
const {
data: vulnTree,
status: vulnTreeStatus,
fetch: fetchVulnTree,
} = useVulnerabilityTree(packageName, () => displayVersion.value?.version ?? '')
} = useDependencyAnalysis(packageName, () => displayVersion.value?.version ?? '')
onMounted(() => {
// Fetch vulnerability tree once displayVersion is available
if (displayVersion.value) {
Expand Down Expand Up @@ -1191,6 +1191,12 @@ defineOgImageComponent('Package', {
:package-name="pkg.name"
:version="displayVersion.version"
/>
<PackageDeprecatedTree
v-if="displayVersion"
:package-name="pkg.name"
:version="displayVersion.version"
class="mt-3"
/>
</ClientOnly>
</div>

Expand Down
5 changes: 5 additions & 0 deletions i18n/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,11 @@
"transitive": "Transitive Abhängigkeit"
}
},
"deprecated": {
"label": "Veraltet",
"tree_found": "{count} veraltete Abhängigkeit | {count} veraltete Abhängigkeiten",
"show_all": "alle {count} veralteten Pakete anzeigen"
},
"access": {
"title": "Team-Zugriff",
"refresh": "Team-Zugriff aktualisieren",
Expand Down
5 changes: 5 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,11 @@
"low": "low"
}
},
"deprecated": {
"label": "Deprecated",
"tree_found": "{count} deprecated dependency | {count} deprecated dependencies",
"show_all": "show all {count} deprecated packages"
},
"access": {
"title": "Team Access",
"refresh": "Refresh team access",
Expand Down
Loading