@@ -510,7 +520,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
@@ -690,7 +715,7 @@ function getTagVersions(tag: string): VersionDisplay[] {
+
{{ v.version }}
diff --git a/app/components/PackageVulnerabilityTree.vue b/app/components/PackageVulnerabilityTree.vue
index f1e1f29280..6741ae6461 100644
--- a/app/components/PackageVulnerabilityTree.vue
+++ b/app/components/PackageVulnerabilityTree.vue
@@ -11,7 +11,7 @@ const {
data: vulnTree,
status,
fetch: fetchVulnTree,
-} = useVulnerabilityTree(
+} = useDependencyAnalysis(
() => props.packageName,
() => props.version,
)
diff --git a/app/composables/useVulnerabilityTree.ts b/app/composables/useDependencyAnalysis.ts
similarity index 77%
rename from app/composables/useVulnerabilityTree.ts
rename to app/composables/useDependencyAnalysis.ts
index 223be0a711..36f1baad1d 100644
--- a/app/composables/useVulnerabilityTree.ts
+++ b/app/composables/useDependencyAnalysis.ts
@@ -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
,
version: MaybeRefOrGetter,
) {
// 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(key, () => null)
@@ -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'
}
}
diff --git a/app/pages/[...package].vue b/app/pages/[...package].vue
index 350f548269..f968d00282 100644
--- a/app/pages/[...package].vue
+++ b/app/pages/[...package].vue
@@ -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) {
@@ -1191,6 +1191,12 @@ defineOgImageComponent('Package', {
:package-name="pkg.name"
:version="displayVersion.version"
/>
+
diff --git a/i18n/locales/de-DE.json b/i18n/locales/de-DE.json
index 6d6ffbf0f8..bf1390d3d2 100644
--- a/i18n/locales/de-DE.json
+++ b/i18n/locales/de-DE.json
@@ -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",
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index 363baa7dc4..da3b00ec56 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -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",
diff --git a/i18n/locales/es.json b/i18n/locales/es.json
index 38cf950298..b111d37115 100644
--- a/i18n/locales/es.json
+++ b/i18n/locales/es.json
@@ -233,6 +233,11 @@
"low": "baja"
}
},
+ "deprecated": {
+ "label": "Obsoleto",
+ "tree_found": "{count} dependencia obsoleta | {count} dependencias obsoletas",
+ "show_all": "mostrar los {count} paquetes obsoletos"
+ },
"access": {
"title": "Acceso de Equipos",
"refresh": "Actualizar acceso de equipos",
diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json
index 27eb97ff6a..971404afc0 100644
--- a/i18n/locales/fr-FR.json
+++ b/i18n/locales/fr-FR.json
@@ -225,6 +225,11 @@
"low": "faible"
}
},
+ "deprecated": {
+ "label": "Obsolète",
+ "tree_found": "{count} dépendance obsolète | {count} dépendances obsolètes",
+ "show_all": "afficher les {count} paquets obsolètes"
+ },
"access": {
"title": "Accès des équipes",
"refresh": "Actualiser l'accès des équipes",
diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json
index b9d1c6f485..c5bf4bb0b7 100644
--- a/i18n/locales/it-IT.json
+++ b/i18n/locales/it-IT.json
@@ -241,6 +241,11 @@
"low": "bassa"
}
},
+ "deprecated": {
+ "label": "Deprecato",
+ "tree_found": "{count} dipendenza deprecata | {count} dipendenze deprecate",
+ "show_all": "mostra tutti i {count} pacchetti deprecati"
+ },
"access": {
"title": "Accesso Team",
"refresh": "Aggiorna accesso team",
diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json
index 37ca2552ad..c4a2433fae 100644
--- a/i18n/locales/ja-JP.json
+++ b/i18n/locales/ja-JP.json
@@ -276,6 +276,11 @@
"low": "低"
}
},
+ "deprecated": {
+ "label": "非推奨",
+ "tree_found": "{count} 件の非推奨の依存関係",
+ "show_all": "{count} 件の非推奨パッケージをすべて表示"
+ },
"access": {
"title": "チームアクセス",
"refresh": "チームアクセスを更新",
diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json
index dcb4de3a08..bd2100baea 100644
--- a/i18n/locales/zh-CN.json
+++ b/i18n/locales/zh-CN.json
@@ -241,6 +241,11 @@
"low": "低"
}
},
+ "deprecated": {
+ "label": "已弃用",
+ "tree_found": "{count} 个已弃用的依赖",
+ "show_all": "显示全部 {count} 个已弃用的包"
+ },
"access": {
"title": "团队权限",
"refresh": "刷新团队权限",
diff --git a/lunaria/files/de-DE.json b/lunaria/files/de-DE.json
index 6d6ffbf0f8..bf1390d3d2 100644
--- a/lunaria/files/de-DE.json
+++ b/lunaria/files/de-DE.json
@@ -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",
diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json
index 363baa7dc4..da3b00ec56 100644
--- a/lunaria/files/en-US.json
+++ b/lunaria/files/en-US.json
@@ -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",
diff --git a/lunaria/files/es-419.json b/lunaria/files/es-419.json
index 38cf950298..b111d37115 100644
--- a/lunaria/files/es-419.json
+++ b/lunaria/files/es-419.json
@@ -233,6 +233,11 @@
"low": "baja"
}
},
+ "deprecated": {
+ "label": "Obsoleto",
+ "tree_found": "{count} dependencia obsoleta | {count} dependencias obsoletas",
+ "show_all": "mostrar los {count} paquetes obsoletos"
+ },
"access": {
"title": "Acceso de Equipos",
"refresh": "Actualizar acceso de equipos",
diff --git a/lunaria/files/es-ES.json b/lunaria/files/es-ES.json
index 38cf950298..b111d37115 100644
--- a/lunaria/files/es-ES.json
+++ b/lunaria/files/es-ES.json
@@ -233,6 +233,11 @@
"low": "baja"
}
},
+ "deprecated": {
+ "label": "Obsoleto",
+ "tree_found": "{count} dependencia obsoleta | {count} dependencias obsoletas",
+ "show_all": "mostrar los {count} paquetes obsoletos"
+ },
"access": {
"title": "Acceso de Equipos",
"refresh": "Actualizar acceso de equipos",
diff --git a/lunaria/files/fr-FR.json b/lunaria/files/fr-FR.json
index 27eb97ff6a..971404afc0 100644
--- a/lunaria/files/fr-FR.json
+++ b/lunaria/files/fr-FR.json
@@ -225,6 +225,11 @@
"low": "faible"
}
},
+ "deprecated": {
+ "label": "Obsolète",
+ "tree_found": "{count} dépendance obsolète | {count} dépendances obsolètes",
+ "show_all": "afficher les {count} paquets obsolètes"
+ },
"access": {
"title": "Accès des équipes",
"refresh": "Actualiser l'accès des équipes",
diff --git a/lunaria/files/it-IT.json b/lunaria/files/it-IT.json
index b9d1c6f485..c5bf4bb0b7 100644
--- a/lunaria/files/it-IT.json
+++ b/lunaria/files/it-IT.json
@@ -241,6 +241,11 @@
"low": "bassa"
}
},
+ "deprecated": {
+ "label": "Deprecato",
+ "tree_found": "{count} dipendenza deprecata | {count} dipendenze deprecate",
+ "show_all": "mostra tutti i {count} pacchetti deprecati"
+ },
"access": {
"title": "Accesso Team",
"refresh": "Aggiorna accesso team",
diff --git a/lunaria/files/ja-JP.json b/lunaria/files/ja-JP.json
index 37ca2552ad..c4a2433fae 100644
--- a/lunaria/files/ja-JP.json
+++ b/lunaria/files/ja-JP.json
@@ -276,6 +276,11 @@
"low": "低"
}
},
+ "deprecated": {
+ "label": "非推奨",
+ "tree_found": "{count} 件の非推奨の依存関係",
+ "show_all": "{count} 件の非推奨パッケージをすべて表示"
+ },
"access": {
"title": "チームアクセス",
"refresh": "チームアクセスを更新",
diff --git a/lunaria/files/zh-CN.json b/lunaria/files/zh-CN.json
index dcb4de3a08..bd2100baea 100644
--- a/lunaria/files/zh-CN.json
+++ b/lunaria/files/zh-CN.json
@@ -241,6 +241,11 @@
"low": "低"
}
},
+ "deprecated": {
+ "label": "已弃用",
+ "tree_found": "{count} 个已弃用的依赖",
+ "show_all": "显示全部 {count} 个已弃用的包"
+ },
"access": {
"title": "团队权限",
"refresh": "刷新团队权限",
diff --git a/server/api/registry/vulnerabilities/[...pkg].get.ts b/server/api/registry/vulnerabilities/[...pkg].get.ts
index d9c627deb6..54a6197e9c 100644
--- a/server/api/registry/vulnerabilities/[...pkg].get.ts
+++ b/server/api/registry/vulnerabilities/[...pkg].get.ts
@@ -5,7 +5,8 @@ import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants'
/**
* GET /api/registry/vulnerabilities/:name or /api/registry/vulnerabilities/:name/v/:version
*
- * Analyze entire dependency tree for vulnerabilities.
+ * Analyze entire dependency tree for vulnerabilities and deprecated dependencies.
+ * I does not rename this endpoint for backward compatibility.
*/
export default defineCachedEventHandler(
async event => {
@@ -31,7 +32,7 @@ export default defineCachedEventHandler(
}
}
- return await analyzeVulnerabilityTree(packageName, version)
+ return await analyzeDependencyTree(packageName, version)
} catch (error: unknown) {
handleApiError(error, {
statusCode: 502,
diff --git a/server/utils/vulnerability-tree.ts b/server/utils/dependency-analysis.ts
similarity index 84%
rename from server/utils/vulnerability-tree.ts
rename to server/utils/dependency-analysis.ts
index 2e326f437e..5937536b4a 100644
--- a/server/utils/vulnerability-tree.ts
+++ b/server/utils/dependency-analysis.ts
@@ -6,7 +6,8 @@ import type {
DependencyDepth,
PackageVulnerabilityInfo,
VulnerabilityTreeResult,
-} from '#shared/types'
+ DeprecatedPackageInfo,
+} from '#shared/types/dependency-analysis'
import { resolveDependencyTree } from './dependency-resolver'
/** Result of a single OSV query */
@@ -67,7 +68,7 @@ async function queryOsv(
return { status: 'ok', data: { name, version, depth, path, vulnerabilities, counts } }
} catch (error) {
// oxlint-disable-next-line no-console -- log OSV API failures for debugging
- console.warn(`[vuln-tree] OSV query failed for ${name}@${version}:`, error)
+ console.warn(`[dep-analysis] OSV query failed for ${name}@${version}:`, error)
return { status: 'error' }
}
}
@@ -108,10 +109,10 @@ function getSeverityLevel(vuln: OsvVulnerability): OsvSeverityLevel {
}
/**
- * Analyze entire dependency tree for vulnerabilities.
+ * Analyze entire dependency tree for vulnerabilities and deprecated packages.
* @public
*/
-export const analyzeVulnerabilityTree = defineCachedFunction(
+export const analyzeDependencyTree = defineCachedFunction(
async (name: string, version: string): Promise
=> {
// Resolve all packages in the tree with depth tracking
const resolved = await resolveDependencyTree(name, version, { trackDepth: true })
@@ -119,6 +120,22 @@ export const analyzeVulnerabilityTree = defineCachedFunction(
// Convert to array for OSV querying
const packages = [...resolved.values()]
+ // Collect deprecated packages (no API call needed - already in packument data)
+ const deprecatedPackages: DeprecatedPackageInfo[] = packages
+ .filter(pkg => pkg.deprecated)
+ .map(pkg => ({
+ name: pkg.name,
+ version: pkg.version,
+ depth: pkg.depth!,
+ path: pkg.path || [],
+ message: pkg.deprecated!,
+ }))
+ .sort((a, b) => {
+ // Sort by depth (root → direct → transitive)
+ const depthOrder: Record = { root: 0, direct: 1, transitive: 2 }
+ return depthOrder[a.depth] - depthOrder[b.depth]
+ })
+
// Query OSV for all packages in parallel batches
const vulnerablePackages: PackageVulnerabilityInfo[] = []
let failedQueries = 0
@@ -163,7 +180,7 @@ export const analyzeVulnerabilityTree = defineCachedFunction(
if (failedQueries > 0 && failedQueries > packages.length / 2) {
// oxlint-disable-next-line no-console -- critical error logging
console.error(
- `[vuln-tree] Critical: ${failedQueries}/${packages.length} OSV queries failed for ${name}@${version}`,
+ `[dep-analysis] Critical: ${failedQueries}/${packages.length} OSV queries failed for ${name}@${version}`,
)
}
@@ -171,6 +188,7 @@ export const analyzeVulnerabilityTree = defineCachedFunction(
package: name,
version,
vulnerablePackages,
+ deprecatedPackages,
totalPackages: packages.length,
failedQueries,
totalCounts,
@@ -179,7 +197,7 @@ export const analyzeVulnerabilityTree = defineCachedFunction(
{
maxAge: 60 * 60,
swr: true,
- name: 'vulnerability-tree',
+ name: 'dependency-analysis',
getKey: (name: string, version: string) => `v1:${name}@${version}`,
},
)
diff --git a/server/utils/dependency-resolver.ts b/server/utils/dependency-resolver.ts
index 4d3f200000..47eaa559bc 100644
--- a/server/utils/dependency-resolver.ts
+++ b/server/utils/dependency-resolver.ts
@@ -111,6 +111,8 @@ export interface ResolvedPackage {
depth?: DependencyDepth
/** Dependency path from root (only when trackDepth is enabled) */
path?: string[]
+ /** Deprecation message if the version is deprecated */
+ deprecated?: string
}
/**
@@ -171,6 +173,9 @@ export async function resolveDependencyTree(
pkg.depth = level === 0 ? 'root' : level === 1 ? 'direct' : 'transitive'
pkg.path = currentPath
}
+ if (versionData.deprecated) {
+ pkg.deprecated = versionData.deprecated
+ }
resolved.set(key, pkg)
}
diff --git a/shared/types/osv.ts b/shared/types/dependency-analysis.ts
similarity index 82%
rename from shared/types/osv.ts
rename to shared/types/dependency-analysis.ts
index 53ad78dce3..7df81e5b7e 100644
--- a/shared/types/osv.ts
+++ b/shared/types/dependency-analysis.ts
@@ -1,5 +1,7 @@
/**
- * OSV (Open Source Vulnerabilities) API types
+ * Dependency Analysis Types
+ * Types for vulnerability scanning (via OSV API) and deprecated package detection.
+ *
* @see https://google.github.io/osv.dev/api/
*/
@@ -108,7 +110,21 @@ export interface PackageVulnerabilityInfo {
}
/**
- * Result of vulnerability tree analysis
+ * Deprecated package info in the dependency tree
+ */
+export interface DeprecatedPackageInfo {
+ name: string
+ version: string
+ /** Depth in dependency tree: root (0), direct (1), transitive (2+) */
+ depth: DependencyDepth
+ /** Dependency path from root package */
+ path: string[]
+ /** Deprecation message */
+ message: string
+}
+
+/**
+ * Result of dependency tree analysis
*/
export interface VulnerabilityTreeResult {
/** Root package name */
@@ -117,6 +133,8 @@ export interface VulnerabilityTreeResult {
version: string
/** All packages with vulnerabilities in the tree */
vulnerablePackages: PackageVulnerabilityInfo[]
+ /** All deprecated packages in the tree */
+ deprecatedPackages: DeprecatedPackageInfo[]
/** Total packages analyzed */
totalPackages: number
/** Number of packages that could not be checked (OSV query failed) */
diff --git a/shared/types/index.ts b/shared/types/index.ts
index 228f6e8523..1bb0a8a132 100644
--- a/shared/types/index.ts
+++ b/shared/types/index.ts
@@ -1,6 +1,6 @@
export * from './npm-registry'
export * from './jsr'
-export * from './osv'
+export * from './dependency-analysis'
export * from './readme'
export * from './docs'
export * from './deno-doc'
diff --git a/test/nuxt/components.spec.ts b/test/nuxt/components.spec.ts
index a50dc54a20..7d47dd49c5 100644
--- a/test/nuxt/components.spec.ts
+++ b/test/nuxt/components.spec.ts
@@ -93,6 +93,7 @@ import PackageTableRow from '~/components/PackageTableRow.vue'
import PaginationControls from '~/components/PaginationControls.vue'
import ViewModeToggle from '~/components/ViewModeToggle.vue'
import PackageVulnerabilityTree from '~/components/PackageVulnerabilityTree.vue'
+import PackageDeprecatedTree from '~/components/PackageDeprecatedTree.vue'
import DependencyPathPopup from '~/components/DependencyPathPopup.vue'
describe('component accessibility audits', () => {
@@ -1258,6 +1259,19 @@ describe('component accessibility audits', () => {
})
})
+ describe('PackageDeprecatedTree', () => {
+ it('should have no accessibility violations in idle state', async () => {
+ const component = await mountSuspended(PackageDeprecatedTree, {
+ props: {
+ packageName: 'vue',
+ version: '3.5.0',
+ },
+ })
+ const results = await runAxe(component)
+ expect(results.violations).toEqual([])
+ })
+ })
+
describe('DependencyPathPopup', () => {
it('should have no accessibility violations with short path', async () => {
const component = await mountSuspended(DependencyPathPopup, {
diff --git a/test/unit/vulnerability-tree.spec.ts b/test/unit/dependency-analysis.spec.ts
similarity index 62%
rename from test/unit/vulnerability-tree.spec.ts
rename to test/unit/dependency-analysis.spec.ts
index 5090874c68..fd2a5818ee 100644
--- a/test/unit/vulnerability-tree.spec.ts
+++ b/test/unit/dependency-analysis.spec.ts
@@ -5,7 +5,7 @@ vi.stubGlobal('defineCachedFunction', (fn: Function) => fn)
vi.stubGlobal('$fetch', vi.fn())
// Import module under test
-const { analyzeVulnerabilityTree } = await import('../../server/utils/vulnerability-tree')
+const { analyzeDependencyTree } = await import('../../server/utils/dependency-analysis')
// Mock the dependency resolver
vi.mock('../../server/utils/dependency-resolver', () => ({
@@ -14,12 +14,12 @@ vi.mock('../../server/utils/dependency-resolver', () => ({
const { resolveDependencyTree } = await import('../../server/utils/dependency-resolver')
-describe('vulnerability-tree', () => {
+describe('dependency-analysis', () => {
beforeEach(() => {
vi.clearAllMocks()
})
- describe('analyzeVulnerabilityTree', () => {
+ describe('analyzeDependencyTree', () => {
it('returns empty result when no packages have vulnerabilities', async () => {
const mockResolved = new Map([
[
@@ -39,7 +39,7 @@ describe('vulnerability-tree', () => {
// Mock OSV API returning no vulnerabilities
vi.mocked($fetch).mockResolvedValue({ vulns: [] })
- const result = await analyzeVulnerabilityTree('test-pkg', '1.0.0')
+ const result = await analyzeDependencyTree('test-pkg', '1.0.0')
expect(result.package).toBe('test-pkg')
expect(result.version).toBe('1.0.0')
@@ -81,7 +81,7 @@ describe('vulnerability-tree', () => {
.mockResolvedValueOnce({ vulns: [] })
.mockRejectedValueOnce(new Error('OSV API error'))
- const result = await analyzeVulnerabilityTree('test-pkg', '1.0.0')
+ const result = await analyzeDependencyTree('test-pkg', '1.0.0')
expect(result.failedQueries).toBe(1)
expect(result.totalPackages).toBe(2)
@@ -113,7 +113,7 @@ describe('vulnerability-tree', () => {
],
})
- const result = await analyzeVulnerabilityTree('vuln-pkg', '1.0.0')
+ const result = await analyzeDependencyTree('vuln-pkg', '1.0.0')
expect(result.vulnerablePackages).toHaveLength(1)
expect(result.totalCounts).toEqual({ total: 4, critical: 1, high: 1, moderate: 1, low: 1 })
@@ -160,7 +160,7 @@ describe('vulnerability-tree', () => {
],
}) // vuln-dep has vuln
- const result = await analyzeVulnerabilityTree('root', '1.0.0')
+ const result = await analyzeDependencyTree('root', '1.0.0')
expect(result.vulnerablePackages).toHaveLength(1)
const vulnPkg = result.vulnerablePackages[0]
@@ -228,7 +228,7 @@ describe('vulnerability-tree', () => {
],
})
- const result = await analyzeVulnerabilityTree('root', '1.0.0')
+ const result = await analyzeDependencyTree('root', '1.0.0')
expect(result.vulnerablePackages).toHaveLength(3)
// Should be sorted: root first, then direct, then transitive
@@ -263,7 +263,7 @@ describe('vulnerability-tree', () => {
],
})
- const result = await analyzeVulnerabilityTree('pkg', '1.0.0')
+ const result = await analyzeDependencyTree('pkg', '1.0.0')
expect(result.vulnerablePackages[0].vulnerabilities[0].url).toBe(
'https://github.com/advisories/GHSA-xxxx-yyyy-zzzz',
@@ -297,7 +297,7 @@ describe('vulnerability-tree', () => {
],
})
- const result = await analyzeVulnerabilityTree('pkg', '1.0.0')
+ const result = await analyzeDependencyTree('pkg', '1.0.0')
expect(result.vulnerablePackages[0].vulnerabilities[0].url).toBe(
'https://nvd.nist.gov/vuln/detail/CVE-2024-12345',
@@ -326,7 +326,7 @@ describe('vulnerability-tree', () => {
],
})
- const result = await analyzeVulnerabilityTree('pkg', '1.0.0')
+ const result = await analyzeDependencyTree('pkg', '1.0.0')
expect(result.vulnerablePackages[0].vulnerabilities[0].url).toBe(
'https://osv.dev/vulnerability/PYSEC-2024-001',
@@ -362,12 +362,190 @@ describe('vulnerability-tree', () => {
],
})
- const result = await analyzeVulnerabilityTree('pkg', '1.0.0')
+ const result = await analyzeDependencyTree('pkg', '1.0.0')
expect(result.totalCounts.critical).toBe(1)
expect(result.totalCounts.high).toBe(1)
expect(result.totalCounts.moderate).toBe(1)
expect(result.totalCounts.low).toBe(1)
})
+
+ it('collects deprecated packages from the dependency tree', async () => {
+ const mockResolved = new Map([
+ [
+ 'root@1.0.0',
+ {
+ name: 'root',
+ version: '1.0.0',
+ size: 1000,
+ optional: false,
+ depth: 'root' as const,
+ path: ['root@1.0.0'],
+ },
+ ],
+ [
+ 'deprecated-pkg@2.0.0',
+ {
+ name: 'deprecated-pkg',
+ version: '2.0.0',
+ size: 500,
+ optional: false,
+ depth: 'direct' as const,
+ path: ['root@1.0.0', 'deprecated-pkg@2.0.0'],
+ deprecated: 'This package is deprecated. Use new-pkg instead.',
+ },
+ ],
+ ])
+ vi.mocked(resolveDependencyTree).mockResolvedValue(mockResolved)
+ vi.mocked($fetch).mockResolvedValue({ vulns: [] })
+
+ const result = await analyzeDependencyTree('root', '1.0.0')
+
+ expect(result.deprecatedPackages).toHaveLength(1)
+ expect(result.deprecatedPackages[0]).toEqual({
+ name: 'deprecated-pkg',
+ version: '2.0.0',
+ depth: 'direct',
+ path: ['root@1.0.0', 'deprecated-pkg@2.0.0'],
+ message: 'This package is deprecated. Use new-pkg instead.',
+ })
+ })
+
+ it('returns empty deprecatedPackages when none are deprecated', async () => {
+ const mockResolved = new Map([
+ [
+ 'root@1.0.0',
+ {
+ name: 'root',
+ version: '1.0.0',
+ size: 1000,
+ optional: false,
+ depth: 'root' as const,
+ path: ['root@1.0.0'],
+ },
+ ],
+ ])
+ vi.mocked(resolveDependencyTree).mockResolvedValue(mockResolved)
+ vi.mocked($fetch).mockResolvedValue({ vulns: [] })
+
+ const result = await analyzeDependencyTree('root', '1.0.0')
+
+ expect(result.deprecatedPackages).toHaveLength(0)
+ })
+
+ it('sorts deprecated packages by depth (root → direct → transitive)', async () => {
+ const mockResolved = new Map([
+ [
+ 'root@1.0.0',
+ {
+ name: 'root',
+ version: '1.0.0',
+ size: 1000,
+ optional: false,
+ depth: 'root' as const,
+ path: ['root@1.0.0'],
+ deprecated: 'Root is deprecated',
+ },
+ ],
+ [
+ 'transitive-dep@1.0.0',
+ {
+ name: 'transitive-dep',
+ version: '1.0.0',
+ size: 300,
+ optional: false,
+ depth: 'transitive' as const,
+ path: ['root@1.0.0', 'direct-dep@1.0.0', 'transitive-dep@1.0.0'],
+ deprecated: 'Transitive is deprecated',
+ },
+ ],
+ [
+ 'direct-dep@1.0.0',
+ {
+ name: 'direct-dep',
+ version: '1.0.0',
+ size: 500,
+ optional: false,
+ depth: 'direct' as const,
+ path: ['root@1.0.0', 'direct-dep@1.0.0'],
+ deprecated: 'Direct is deprecated',
+ },
+ ],
+ ])
+ vi.mocked(resolveDependencyTree).mockResolvedValue(mockResolved)
+ vi.mocked($fetch).mockResolvedValue({ vulns: [] })
+
+ const result = await analyzeDependencyTree('root', '1.0.0')
+
+ expect(result.deprecatedPackages).toHaveLength(3)
+ expect(result.deprecatedPackages[0].name).toBe('root')
+ expect(result.deprecatedPackages[0].depth).toBe('root')
+ expect(result.deprecatedPackages[1].name).toBe('direct-dep')
+ expect(result.deprecatedPackages[1].depth).toBe('direct')
+ expect(result.deprecatedPackages[2].name).toBe('transitive-dep')
+ expect(result.deprecatedPackages[2].depth).toBe('transitive')
+ })
+
+ it('returns both vulnerabilities and deprecated packages together', async () => {
+ const mockResolved = new Map([
+ [
+ 'root@1.0.0',
+ {
+ name: 'root',
+ version: '1.0.0',
+ size: 1000,
+ optional: false,
+ depth: 'root' as const,
+ path: ['root@1.0.0'],
+ },
+ ],
+ [
+ 'vuln-pkg@1.0.0',
+ {
+ name: 'vuln-pkg',
+ version: '1.0.0',
+ size: 500,
+ optional: false,
+ depth: 'direct' as const,
+ path: ['root@1.0.0', 'vuln-pkg@1.0.0'],
+ },
+ ],
+ [
+ 'deprecated-pkg@1.0.0',
+ {
+ name: 'deprecated-pkg',
+ version: '1.0.0',
+ size: 300,
+ optional: false,
+ depth: 'direct' as const,
+ path: ['root@1.0.0', 'deprecated-pkg@1.0.0'],
+ deprecated: 'Use something else',
+ },
+ ],
+ ])
+ vi.mocked(resolveDependencyTree).mockResolvedValue(mockResolved)
+
+ // root and deprecated-pkg have no vulns, vuln-pkg has one
+ vi.mocked($fetch)
+ .mockResolvedValueOnce({ vulns: [] }) // root
+ .mockResolvedValueOnce({
+ vulns: [
+ {
+ id: 'GHSA-vuln',
+ summary: 'A vulnerability',
+ database_specific: { severity: 'HIGH' },
+ },
+ ],
+ }) // vuln-pkg
+ .mockResolvedValueOnce({ vulns: [] }) // deprecated-pkg
+
+ const result = await analyzeDependencyTree('root', '1.0.0')
+
+ expect(result.vulnerablePackages).toHaveLength(1)
+ expect(result.vulnerablePackages[0].name).toBe('vuln-pkg')
+ expect(result.deprecatedPackages).toHaveLength(1)
+ expect(result.deprecatedPackages[0].name).toBe('deprecated-pkg')
+ expect(result.totalPackages).toBe(3)
+ })
})
})