Skip to content

Commit 3226764

Browse files
fix: show correct dependencies count (#787)
Co-authored-by: shamilkotta <shamilkotta99@gmail.com>
1 parent d54ac07 commit 3226764

File tree

6 files changed

+86
-64
lines changed

6 files changed

+86
-64
lines changed

app/composables/usePackageComparison.ts

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@ import type {
88
import { encodePackageName } from '#shared/utils/npm'
99
import type { PackageAnalysisResponse } from './usePackageAnalysis'
1010
import { isBinaryOnlyPackage } from '#shared/utils/binary-detection'
11+
import { formatBytes } from '~/utils/formatters'
12+
import { getDependencyCount } from '~/utils/npm/dependency-count'
1113

1214
export interface PackageComparisonData {
1315
package: ComparisonPackage
1416
downloads?: number
1517
/** Package's own unpacked size (from dist.unpackedSize) */
1618
packageSize?: number
19+
/** Number of direct dependencies */
20+
directDeps: number | null
1721
/** Install size data (fetched lazily) */
1822
installSize?: {
1923
selfSize: number
2024
totalSize: number
25+
/** Total dependency count */
2126
dependencyCount: number
2227
}
2328
analysis?: PackageAnalysisResponse
@@ -139,6 +144,7 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
139144
},
140145
downloads: downloads?.downloads,
141146
packageSize,
147+
directDeps: versionData ? getDependencyCount(versionData) : null,
142148
installSize: undefined, // Will be filled in second pass
143149
analysis: analysis ?? undefined,
144150
vulnerabilities: {
@@ -230,7 +236,7 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
230236
function isFacetLoading(facet: ComparisonFacet): boolean {
231237
if (!installSizeLoading.value) return false
232238
// These facets depend on install-size API
233-
return facet === 'installSize' || facet === 'dependencies'
239+
return facet === 'installSize' || facet === 'totalDependencies'
234240
}
235241

236242
// Check if a specific column (package) is loading
@@ -255,40 +261,40 @@ function computeFacetValue(
255261
t: (key: string, params?: Record<string, unknown>) => string,
256262
): FacetValue | null {
257263
switch (facet) {
258-
case 'downloads':
264+
case 'downloads': {
259265
if (data.downloads === undefined) return null
260266
return {
261267
raw: data.downloads,
262268
display: formatCompactNumber(data.downloads),
263269
status: 'neutral',
264270
}
265-
266-
case 'packageSize':
271+
}
272+
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
}
273-
274-
case 'installSize':
279+
}
280+
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
}
281-
282-
case 'moduleFormat':
287+
}
288+
case 'moduleFormat': {
283289
if (!data.analysis) return null
284290
const format = data.analysis.moduleFormat
285291
return {
286292
raw: format,
287293
display: format === 'dual' ? 'ESM + CJS' : format.toUpperCase(),
288294
status: format === 'esm' || format === 'dual' ? 'good' : 'neutral',
289295
}
290-
291-
case 'types':
296+
}
297+
case 'types': {
292298
if (data.isBinaryOnly) {
293299
return {
294300
raw: 'binary',
@@ -309,8 +315,8 @@ function computeFacetValue(
309315
: t('compare.facets.values.types_none'),
310316
status: types.kind === 'included' ? 'good' : types.kind === '@types' ? 'info' : 'bad',
311317
}
312-
313-
case 'engines':
318+
}
319+
case 'engines': {
314320
const engines = data.metadata?.engines
315321
if (!engines?.node) {
316322
return { raw: null, display: t('compare.facets.values.any'), status: 'neutral' }
@@ -320,8 +326,8 @@ function computeFacetValue(
320326
display: `Node ${engines.node}`,
321327
status: 'neutral',
322328
}
323-
324-
case 'vulnerabilities':
329+
}
330+
case 'vulnerabilities': {
325331
if (!data.vulnerabilities) return null
326332
const count = data.vulnerabilities.count
327333
const sev = data.vulnerabilities.severity
@@ -337,8 +343,8 @@ function computeFacetValue(
337343
}),
338344
status: count === 0 ? 'good' : sev.critical > 0 || sev.high > 0 ? 'bad' : 'warning',
339345
}
340-
341-
case 'lastUpdated':
346+
}
347+
case 'lastUpdated': {
342348
if (!data.metadata?.lastUpdated) return null
343349
const date = new Date(data.metadata.lastUpdated)
344350
return {
@@ -347,8 +353,8 @@ function computeFacetValue(
347353
status: isStale(date) ? 'warning' : 'neutral',
348354
type: 'date',
349355
}
350-
351-
case 'license':
356+
}
357+
case 'license': {
352358
const license = data.metadata?.license
353359
if (!license) {
354360
return { raw: null, display: t('compare.facets.values.unknown'), status: 'warning' }
@@ -358,17 +364,17 @@ function computeFacetValue(
358364
display: license,
359365
status: 'neutral',
360366
}
361-
362-
case 'dependencies':
363-
if (!data.installSize) return null
364-
const depCount = data.installSize.dependencyCount
367+
}
368+
case 'dependencies': {
369+
const depCount = data.directDeps
370+
if (depCount === null) return null
365371
return {
366372
raw: depCount,
367373
display: String(depCount),
368-
status: depCount > 50 ? 'warning' : 'neutral',
374+
status: depCount > 10 ? 'warning' : 'neutral',
369375
}
370-
371-
case 'deprecated':
376+
}
377+
case 'deprecated': {
372378
const isDeprecated = !!data.metadata?.deprecated
373379
return {
374380
raw: isDeprecated,
@@ -377,22 +383,23 @@ function computeFacetValue(
377383
: t('compare.facets.values.not_deprecated'),
378384
status: isDeprecated ? 'bad' : 'good',
379385
}
380-
386+
}
381387
// Coming soon facets
382-
case 'totalDependencies':
383-
return null
384-
385-
default:
388+
case 'totalDependencies': {
389+
if (!data.installSize) return null
390+
const totalDepCount = data.installSize.dependencyCount
391+
return {
392+
raw: totalDepCount,
393+
display: String(totalDepCount),
394+
status: totalDepCount > 50 ? 'warning' : 'neutral',
395+
}
396+
}
397+
default: {
386398
return null
399+
}
387400
}
388401
}
389402

390-
function formatBytes(bytes: number): string {
391-
if (bytes < 1024) return `${bytes} B`
392-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`
393-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
394-
}
395-
396403
function isStale(date: Date): boolean {
397404
const now = new Date()
398405
const diffMs = now.getTime() - date.getTime()

app/pages/package/[...package].vue

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { joinURL } from 'ufo'
1111
import { areUrlsEquivalent } from '#shared/utils/url'
1212
import { isEditableElement } from '~/utils/input'
1313
import { formatBytes } from '~/utils/formatters'
14+
import { getDependencyCount } from '~/utils/npm/dependency-count'
1415
import { NuxtLink } from '#components'
1516
import { useModal } from '~/composables/useModal'
1617
import { useAtproto } from '~/composables/atproto/useAtproto'
@@ -300,11 +301,6 @@ function normalizeGitUrl(url: string): string {
300301
.replace(/^git@github\.com:/, 'https://github.com/')
301302
}
302303
303-
function getDependencyCount(version: PackumentVersion | null): number {
304-
if (!version?.dependencies) return 0
305-
return Object.keys(version.dependencies).length
306-
}
307-
308304
// Check if a version has provenance/attestations
309305
// The dist object may have attestations that aren't in the base type
310306
function hasProvenance(version: PackumentVersion | null): boolean {

app/utils/npm/dependency-count.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { PackumentVersion } from '#shared/types'
2+
3+
export function getDependencyCount(version: PackumentVersion | null): number {
4+
if (!version?.dependencies) return 0
5+
return Object.keys(version.dependencies).length
6+
}

shared/types/comparison.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ export const FACET_INFO: Record<ComparisonFacet, Omit<FacetInfo, 'id'>> = {
4141
},
4242
totalDependencies: {
4343
category: 'performance',
44-
comingSoon: true,
4544
},
4645
// Health
4746
downloads: {
@@ -111,3 +110,9 @@ export interface ComparisonPackage {
111110
version: string
112111
description?: string
113112
}
113+
114+
// ComingSoon tests run only when FACET_INFO has at least one comingSoon facet
115+
export const comingSoonFacets = (Object.keys(FACET_INFO) as ComparisonFacet[]).filter(
116+
f => FACET_INFO[f].comingSoon,
117+
)
118+
export const hasComingSoonFacets = comingSoonFacets.length > 0

test/nuxt/components/compare/FacetSelector.spec.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { ComparisonFacet } from '#shared/types/comparison'
2-
import { CATEGORY_ORDER, FACET_INFO, FACETS_BY_CATEGORY } from '#shared/types/comparison'
2+
import {
3+
CATEGORY_ORDER,
4+
FACET_INFO,
5+
FACETS_BY_CATEGORY,
6+
comingSoonFacets,
7+
hasComingSoonFacets,
8+
} from '#shared/types/comparison'
39
import FacetSelector from '~/components/Compare/FacetSelector.vue'
410
import { beforeEach, describe, expect, it, vi } from 'vitest'
511
import { computed, ref } from 'vue'
@@ -21,7 +27,7 @@ const facetLabels: Record<ComparisonFacet, { label: string; description: string
2127
license: { label: 'License', description: 'Package license' },
2228
dependencies: { label: 'Direct Deps', description: 'Number of direct dependencies' },
2329
totalDependencies: {
24-
label: 'Total Deps',
30+
label: 'Total deps',
2531
description: 'Total number of dependencies including transitive',
2632
},
2733
deprecated: { label: 'Deprecated?', description: 'Whether the package is deprecated' },
@@ -34,6 +40,11 @@ const categoryLabels: Record<string, string> = {
3440
security: 'Security & Compliance',
3541
}
3642

43+
const comingSoonFacetId = comingSoonFacets[0]
44+
const comingSoonFacetLabel = hasComingSoonFacets
45+
? (facetLabels[comingSoonFacetId!]?.label ?? comingSoonFacetId)
46+
: ''
47+
3748
// Helper to build facet info with labels
3849
function buildFacetInfo(facet: ComparisonFacet) {
3950
return {
@@ -174,13 +185,13 @@ describe('FacetSelector', () => {
174185
})
175186
})
176187

177-
describe('comingSoon facets', () => {
188+
describe.runIf(hasComingSoonFacets)('comingSoon facets', () => {
178189
it('disables comingSoon facets', async () => {
179190
const component = await mountSuspended(FacetSelector)
180191

181192
// totalDependencies is marked as comingSoon
182193
const buttons = component.findAll('button')
183-
const comingSoonButton = buttons.find(b => b.text().includes('Total Deps'))
194+
const comingSoonButton = buttons.find(b => b.text().includes(comingSoonFacetLabel))
184195

185196
expect(comingSoonButton?.attributes('disabled')).toBeDefined()
186197
})
@@ -196,7 +207,7 @@ describe('FacetSelector', () => {
196207

197208
// Find the comingSoon button
198209
const buttons = component.findAll('button')
199-
const comingSoonButton = buttons.find(b => b.text().includes('Total Deps'))
210+
const comingSoonButton = buttons.find(b => b.text().includes(comingSoonFacetLabel))
200211

201212
// Should not have checkmark or add icon
202213
expect(comingSoonButton?.find('.i-carbon\\:checkmark').exists()).toBe(false)
@@ -207,11 +218,11 @@ describe('FacetSelector', () => {
207218
const component = await mountSuspended(FacetSelector)
208219

209220
const buttons = component.findAll('button')
210-
const comingSoonButton = buttons.find(b => b.text().includes('Total Deps'))
221+
const comingSoonButton = buttons.find(b => b.text().includes(comingSoonFacetLabel))
211222
await comingSoonButton?.trigger('click')
212223

213224
// toggleFacet should not have been called with totalDependencies
214-
expect(mockToggleFacet).not.toHaveBeenCalledWith('totalDependencies')
225+
expect(mockToggleFacet).not.toHaveBeenCalledWith(comingSoonFacetId)
215226
})
216227
})
217228

@@ -242,13 +253,11 @@ describe('FacetSelector', () => {
242253

243254
it('disables all button when all facets in category are selected', async () => {
244255
// Select all performance facets
245-
const performanceFacets = FACETS_BY_CATEGORY.performance.filter(
256+
const performanceFacets: (string | ComparisonFacet)[] = FACETS_BY_CATEGORY.performance.filter(
246257
f => !FACET_INFO[f].comingSoon,
247258
)
248259
mockSelectedFacets.value = performanceFacets
249-
mockIsFacetSelected.mockImplementation((f: string) =>
250-
performanceFacets.includes(f as ComparisonFacet),
251-
)
260+
mockIsFacetSelected.mockImplementation((f: string) => performanceFacets.includes(f))
252261

253262
const component = await mountSuspended(FacetSelector)
254263

@@ -281,7 +290,7 @@ describe('FacetSelector', () => {
281290
expect(component.find('.bg-bg-muted').exists()).toBe(true)
282291
})
283292

284-
it('applies cursor-not-allowed to comingSoon facets', async () => {
293+
it.runIf(hasComingSoonFacets)('applies cursor-not-allowed to comingSoon facets', async () => {
285294
const component = await mountSuspended(FacetSelector)
286295

287296
expect(component.find('.cursor-not-allowed').exists()).toBe(true)

test/nuxt/composables/use-facet-selection.spec.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
22
import { mountSuspended } from '@nuxt/test-utils/runtime'
33
import { ref } from 'vue'
44
import type { ComparisonFacet } from '#shared/types/comparison'
5-
import { DEFAULT_FACETS, FACETS_BY_CATEGORY } from '#shared/types/comparison'
5+
import {
6+
DEFAULT_FACETS,
7+
FACET_INFO,
8+
FACETS_BY_CATEGORY,
9+
hasComingSoonFacets,
10+
} from '#shared/types/comparison'
611
import type { FacetInfoWithLabels } from '~/composables/useFacetSelection'
712

813
// Mock useRouteQuery - needs to be outside of the helper to persist across calls
@@ -113,7 +118,7 @@ describe('useFacetSelection', () => {
113118
expect(isFacetSelected('types')).toBe(true)
114119
})
115120

116-
it('filters out comingSoon facets from query', async () => {
121+
it.runIf(hasComingSoonFacets)('filters out comingSoon facets from query', async () => {
117122
mockRouteQuery.value = 'downloads,totalDependencies,types'
118123

119124
const { isFacetSelected } = await useFacetSelectionInComponent()
@@ -225,7 +230,7 @@ describe('useFacetSelection', () => {
225230
selectCategory('performance')
226231

227232
const performanceFacets = FACETS_BY_CATEGORY.performance.filter(
228-
f => f !== 'totalDependencies', // comingSoon facet
233+
f => !FACET_INFO[f].comingSoon, // comingSoon facet
229234
)
230235
for (const facet of performanceFacets) {
231236
expect(isFacetSelected(facet)).toBe(true)
@@ -252,7 +257,7 @@ describe('useFacetSelection', () => {
252257
deselectCategory('performance')
253258

254259
const nonComingSoonPerformanceFacets = FACETS_BY_CATEGORY.performance.filter(
255-
f => f !== 'totalDependencies',
260+
f => !FACET_INFO[f].comingSoon,
256261
)
257262
for (const facet of nonComingSoonPerformanceFacets) {
258263
expect(isFacetSelected(facet)).toBe(false)
@@ -369,12 +374,6 @@ describe('useFacetSelection', () => {
369374
expect(Array.isArray(allFacets)).toBe(true)
370375
expect(allFacets.length).toBeGreaterThan(0)
371376
})
372-
373-
it('allFacets includes all facets including comingSoon', async () => {
374-
const { allFacets } = await useFacetSelectionInComponent()
375-
376-
expect(allFacets).toContain('totalDependencies')
377-
})
378377
})
379378

380379
describe('whitespace handling', () => {

0 commit comments

Comments
 (0)