@@ -49,6 +49,8 @@ export interface PackageComparisonData {
4949 }
5050 /** Whether this is a binary-only package (CLI without library entry points) */
5151 isBinaryOnly ?: boolean
52+ /** Computed health score (0-100) */
53+ healthScore ?: number
5254 /** Marks this as the "no dependency" column for special display */
5355 isNoDependency ?: boolean
5456}
@@ -191,11 +193,12 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
191193 } ) ,
192194 )
193195
194- // Add results to cache
196+ // Add results to cache (with computed health score)
195197 const newCache = new Map ( cache . value )
196198 for ( const [ i , name ] of namesToFetch . entries ( ) ) {
197199 const data = results [ i ]
198200 if ( data ) {
201+ data . healthScore = computeHealthScore ( data )
199202 newCache . set ( name , data )
200203 }
201204 }
@@ -338,6 +341,60 @@ function resolveNoDependencyDisplay(
338341 }
339342}
340343
344+ /**
345+ * Compute a health score (0-100) from already-fetched package data.
346+ * Dimensions: Maintenance (35%), Quality (30%), Security (20%), Popularity (15%)
347+ */
348+ export function computeHealthScore ( data : PackageComparisonData ) : number {
349+ // Deprecated packages get score 0 regardless
350+ if ( data . metadata ?. deprecated ) return 0
351+
352+ // MAINTENANCE (35%) — based on lastUpdated
353+ let maintenance = 0
354+ if ( data . metadata ?. lastUpdated ) {
355+ const daysSince = Math . floor (
356+ ( Date . now ( ) - new Date ( data . metadata . lastUpdated ) . getTime ( ) ) / 86400000 ,
357+ )
358+ if ( daysSince < 30 ) maintenance = 100
359+ else if ( daysSince < 90 ) maintenance = 80
360+ else if ( daysSince < 180 ) maintenance = 60
361+ else if ( daysSince < 365 ) maintenance = 40
362+ else maintenance = 10
363+ }
364+
365+ // QUALITY (30%) — based on analysis data
366+ let quality = 0
367+ if ( data . analysis ) {
368+ if ( data . analysis . types ?. kind === 'included' || data . analysis . types ?. kind === '@types' )
369+ quality += 40
370+ if ( data . analysis . moduleFormat === 'esm' || data . analysis . moduleFormat === 'dual' )
371+ quality += 30
372+ if ( data . metadata ?. license ) quality += 20
373+ if ( data . package . description ) quality += 10
374+ }
375+
376+ // SECURITY (20%) — based on vulnerability severity
377+ let security = 100
378+ if ( data . vulnerabilities ) {
379+ if ( data . vulnerabilities . severity . critical > 0 ) security = 0
380+ else if ( data . vulnerabilities . severity . high > 0 ) security = 25
381+ else if ( data . vulnerabilities . severity . moderate > 0 ) security = 50
382+ else if ( data . vulnerabilities . count > 0 ) security = 75
383+ }
384+
385+ // POPULARITY (15%) — based on weekly downloads
386+ let popularity = 0
387+ const dl = data . downloads ?? 0
388+ if ( dl > 1_000_000 ) popularity = 100
389+ else if ( dl > 100_000 ) popularity = 80
390+ else if ( dl > 10_000 ) popularity = 60
391+ else if ( dl > 1_000 ) popularity = 40
392+ else if ( dl > 100 ) popularity = 20
393+ else popularity = 5
394+
395+ return Math . round ( maintenance * 0.35 + quality * 0.3 + security * 0.2 + popularity * 0.15 )
396+ }
397+
341398function computeFacetValue (
342399 facet : ComparisonFacet ,
343400 data : PackageComparisonData ,
@@ -538,6 +595,16 @@ function computeFacetValue(
538595 status : totalDepCount > 50 ? 'warning' : 'neutral' ,
539596 }
540597 }
598+ case 'healthScore' : {
599+ const score = data . healthScore
600+ if ( score === undefined ) return null
601+ return {
602+ raw : score ,
603+ display : `${ score } /100` ,
604+ status : score >= 80 ? 'good' : score >= 60 ? 'warning' : 'bad' ,
605+ tooltip : t ( 'compare.facets.items.healthScore.tooltip' ) ,
606+ }
607+ }
541608 default : {
542609 return null
543610 }
0 commit comments