···88import { encodePackageName } from '#shared/utils/npm'
99import type { PackageAnalysisResponse } from './usePackageAnalysis'
1010import { isBinaryOnlyPackage } from '#shared/utils/binary-detection'
1111+import { formatBytes } from '~/utils/formatters'
1212+import { getDependencyCount } from '~/utils/npm/dependency-count'
11131214export interface PackageComparisonData {
1315 package: ComparisonPackage
1416 downloads?: number
1517 /** Package's own unpacked size (from dist.unpackedSize) */
1618 packageSize?: number
1919+ /** Number of direct dependencies */
2020+ directDeps: number | null
1721 /** Install size data (fetched lazily) */
1822 installSize?: {
1923 selfSize: number
2024 totalSize: number
2525+ /** Total dependency count */
2126 dependencyCount: number
2227 }
2328 analysis?: PackageAnalysisResponse
···139144 },
140145 downloads: downloads?.downloads,
141146 packageSize,
147147+ directDeps: versionData ? getDependencyCount(versionData) : null,
142148 installSize: undefined, // Will be filled in second pass
143149 analysis: analysis ?? undefined,
144150 vulnerabilities: {
···230236 function isFacetLoading(facet: ComparisonFacet): boolean {
231237 if (!installSizeLoading.value) return false
232238 // These facets depend on install-size API
233233- return facet === 'installSize' || facet === 'dependencies'
239239+ return facet === 'installSize' || facet === 'totalDependencies'
234240 }
235241236242 // Check if a specific column (package) is loading
···255261 t: (key: string, params?: Record<string, unknown>) => string,
256262): FacetValue | null {
257263 switch (facet) {
258258- case 'downloads':
264264+ case 'downloads': {
259265 if (data.downloads === undefined) return null
260266 return {
261267 raw: data.downloads,
262268 display: formatCompactNumber(data.downloads),
263269 status: 'neutral',
264270 }
265265-266266- case 'packageSize':
271271+ }
272272+ case 'packageSize': {
267273 if (!data.packageSize) return null
268274 return {
269275 raw: data.packageSize,
270276 display: formatBytes(data.packageSize),
271277 status: data.packageSize > 5 * 1024 * 1024 ? 'warning' : 'neutral',
272278 }
273273-274274- case 'installSize':
279279+ }
280280+ case 'installSize': {
275281 if (!data.installSize) return null
276282 return {
277283 raw: data.installSize.totalSize,
278284 display: formatBytes(data.installSize.totalSize),
279285 status: data.installSize.totalSize > 50 * 1024 * 1024 ? 'warning' : 'neutral',
280286 }
281281-282282- case 'moduleFormat':
287287+ }
288288+ case 'moduleFormat': {
283289 if (!data.analysis) return null
284290 const format = data.analysis.moduleFormat
285291 return {
···287293 display: format === 'dual' ? 'ESM + CJS' : format.toUpperCase(),
288294 status: format === 'esm' || format === 'dual' ? 'good' : 'neutral',
289295 }
290290-291291- case 'types':
296296+ }
297297+ case 'types': {
292298 if (data.isBinaryOnly) {
293299 return {
294300 raw: 'binary',
···309315 : t('compare.facets.values.types_none'),
310316 status: types.kind === 'included' ? 'good' : types.kind === '@types' ? 'info' : 'bad',
311317 }
312312-313313- case 'engines':
318318+ }
319319+ case 'engines': {
314320 const engines = data.metadata?.engines
315321 if (!engines?.node) {
316322 return { raw: null, display: t('compare.facets.values.any'), status: 'neutral' }
···320326 display: `Node ${engines.node}`,
321327 status: 'neutral',
322328 }
323323-324324- case 'vulnerabilities':
329329+ }
330330+ case 'vulnerabilities': {
325331 if (!data.vulnerabilities) return null
326332 const count = data.vulnerabilities.count
327333 const sev = data.vulnerabilities.severity
···337343 }),
338344 status: count === 0 ? 'good' : sev.critical > 0 || sev.high > 0 ? 'bad' : 'warning',
339345 }
340340-341341- case 'lastUpdated':
346346+ }
347347+ case 'lastUpdated': {
342348 if (!data.metadata?.lastUpdated) return null
343349 const date = new Date(data.metadata.lastUpdated)
344350 return {
···347353 status: isStale(date) ? 'warning' : 'neutral',
348354 type: 'date',
349355 }
350350-351351- case 'license':
356356+ }
357357+ case 'license': {
352358 const license = data.metadata?.license
353359 if (!license) {
354360 return { raw: null, display: t('compare.facets.values.unknown'), status: 'warning' }
···358364 display: license,
359365 status: 'neutral',
360366 }
361361-362362- case 'dependencies':
363363- if (!data.installSize) return null
364364- const depCount = data.installSize.dependencyCount
367367+ }
368368+ case 'dependencies': {
369369+ const depCount = data.directDeps
370370+ if (depCount === null) return null
365371 return {
366372 raw: depCount,
367373 display: String(depCount),
368368- status: depCount > 50 ? 'warning' : 'neutral',
374374+ status: depCount > 10 ? 'warning' : 'neutral',
369375 }
370370-371371- case 'deprecated':
376376+ }
377377+ case 'deprecated': {
372378 const isDeprecated = !!data.metadata?.deprecated
373379 return {
374380 raw: isDeprecated,
···377383 : t('compare.facets.values.not_deprecated'),
378384 status: isDeprecated ? 'bad' : 'good',
379385 }
380380-386386+ }
381387 // Coming soon facets
382382- case 'totalDependencies':
388388+ case 'totalDependencies': {
389389+ if (!data.installSize) return null
390390+ const totalDepCount = data.installSize.dependencyCount
391391+ return {
392392+ raw: totalDepCount,
393393+ display: String(totalDepCount),
394394+ status: totalDepCount > 50 ? 'warning' : 'neutral',
395395+ }
396396+ }
397397+ default: {
383398 return null
384384-385385- default:
386386- return null
399399+ }
387400 }
388388-}
389389-390390-function formatBytes(bytes: number): string {
391391- if (bytes < 1024) return `${bytes} B`
392392- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`
393393- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
394401}
395402396403function isStale(date: Date): boolean {
+1-5
app/pages/package/[...package].vue
···1111import { areUrlsEquivalent } from '#shared/utils/url'
1212import { isEditableElement } from '~/utils/input'
1313import { formatBytes } from '~/utils/formatters'
1414+import { getDependencyCount } from '~/utils/npm/dependency-count'
1415import { NuxtLink } from '#components'
1516import { useModal } from '~/composables/useModal'
1617import { useAtproto } from '~/composables/atproto/useAtproto'
···298299 .replace(/\.git$/, '')
299300 .replace(/^ssh:\/\/git@github\.com/, 'https://github.com')
300301 .replace(/^git@github\.com:/, 'https://github.com/')
301301-}
302302-303303-function getDependencyCount(version: PackumentVersion | null): number {
304304- if (!version?.dependencies) return 0
305305- return Object.keys(version.dependencies).length
306302}
307303308304// Check if a version has provenance/attestations
+6
app/utils/npm/dependency-count.ts
···11+import type { PackumentVersion } from '#shared/types'
22+33+export function getDependencyCount(version: PackumentVersion | null): number {
44+ if (!version?.dependencies) return 0
55+ return Object.keys(version.dependencies).length
66+}
+6-1
shared/types/comparison.ts
···4141 },
4242 totalDependencies: {
4343 category: 'performance',
4444- comingSoon: true,
4544 },
4645 // Health
4746 downloads: {
···111110 version: string
112111 description?: string
113112}
113113+114114+// ComingSoon tests run only when FACET_INFO has at least one comingSoon facet
115115+export const comingSoonFacets = (Object.keys(FACET_INFO) as ComparisonFacet[]).filter(
116116+ f => FACET_INFO[f].comingSoon,
117117+)
118118+export const hasComingSoonFacets = comingSoonFacets.length > 0