Skip to content

Commit 837a721

Browse files
committed
feat(ui): improve compare types in FacetRow for CLI package
- fix tooltips in FacetRow - add 'muted' status option with text-fg-subtle styling - show types as "N/A" with muted style for binary-only packages - move binary detection utils to shared/utils for reuse - use Packument type for registry API responses
1 parent 8b6d617 commit 837a721

File tree

6 files changed

+73
-62
lines changed

6 files changed

+73
-62
lines changed

app/components/compare/FacetRow.vue

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ function getStatusClass(status?: FacetValue['status']): string {
4848
return 'text-amber-400'
4949
case 'bad':
5050
return 'text-red-400'
51+
case 'muted':
52+
return 'text-fg-subtle'
5153
default:
5254
return 'text-fg'
5355
}
@@ -62,16 +64,11 @@ function isCellLoading(index: number): boolean {
6264
<template>
6365
<div class="contents">
6466
<!-- Label cell -->
65-
<div
66-
class="comparison-label flex items-center gap-1.5 px-4 py-3 border-b border-border"
67-
:title="description"
68-
>
67+
<div class="comparison-label flex items-center gap-1.5 px-4 py-3 border-b border-border">
6968
<span class="text-xs text-fg-muted uppercase tracking-wider">{{ label }}</span>
70-
<span
71-
v-if="description"
72-
class="i-carbon:information w-3 h-3 text-fg-subtle"
73-
aria-hidden="true"
74-
/>
69+
<AppTooltip v-if="description" :text="description" position="top">
70+
<span class="i-carbon:information w-3 h-3 text-fg-subtle cursor-help" aria-hidden="true" />
71+
</AppTooltip>
7572
</div>
7673

7774
<!-- Value cells -->

app/composables/usePackageComparison.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import type { FacetValue, ComparisonFacet, ComparisonPackage } from '#shared/types'
1+
import type { FacetValue, ComparisonFacet, ComparisonPackage, Packument } from '#shared/types'
22
import { encodePackageName } from '#shared/utils/npm'
33
import type { PackageAnalysisResponse } from './usePackageAnalysis'
4+
import { isBinaryOnlyPackage } from '#shared/utils/binary-detection'
45

56
export interface PackageComparisonData {
67
package: ComparisonPackage
@@ -24,6 +25,8 @@ export interface PackageComparisonData {
2425
engines?: { node?: string; npm?: string }
2526
deprecated?: string
2627
}
28+
/** Whether this is a binary-only package (CLI without library entry points) */
29+
isBinaryOnly?: boolean
2730
}
2831

2932
/**
@@ -75,13 +78,9 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
7578
namesToFetch.map(async (name): Promise<PackageComparisonData | null> => {
7679
try {
7780
// Fetch basic package info first (required)
78-
const pkgData = await $fetch<{
79-
'name': string
80-
'dist-tags': Record<string, string>
81-
'time': Record<string, string>
82-
'license'?: string
83-
'versions': Record<string, { dist?: { unpackedSize?: number }; deprecated?: string }>
84-
}>(`https://registry.npmjs.org/${encodePackageName(name)}`)
81+
const pkgData = await $fetch<Packument>(
82+
`https://registry.npmjs.org/${encodePackageName(name)}`,
83+
)
8584

8685
const latestVersion = pkgData['dist-tags']?.latest
8786
if (!latestVersion) return null
@@ -100,6 +99,15 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
10099
const versionData = pkgData.versions[latestVersion]
101100
const packageSize = versionData?.dist?.unpackedSize
102101

102+
// Detect if package is binary-only
103+
const isBinary = isBinaryOnlyPackage({
104+
name: pkgData.name,
105+
bin: versionData?.bin,
106+
main: versionData?.main,
107+
module: versionData?.module,
108+
exports: versionData?.exports,
109+
})
110+
103111
// Count vulnerabilities by severity
104112
const vulnCounts = { critical: 0, high: 0, medium: 0, low: 0 }
105113
const vulnList = vulns?.vulnerabilities ?? []
@@ -128,6 +136,7 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
128136
engines: analysis?.engines,
129137
deprecated: versionData?.deprecated,
130138
},
139+
isBinaryOnly: isBinary,
131140
}
132141
} catch {
133142
return null
@@ -259,6 +268,13 @@ function computeFacetValue(facet: ComparisonFacet, data: PackageComparisonData):
259268
}
260269

261270
case 'types':
271+
if (data.isBinaryOnly) {
272+
return {
273+
raw: 'binary',
274+
display: 'N/A',
275+
status: 'muted',
276+
}
277+
}
262278
if (!data.analysis) return null
263279
const types = data.analysis.types
264280
return {

app/utils/run-command.ts

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,6 @@ import type { JsrPackageInfo } from '#shared/types/jsr'
22
import { getPackageSpecifier, packageManagers } from './install-command'
33
import type { PackageManagerId } from './install-command'
44

5-
/**
6-
* Metadata needed to determine if a package is binary-only.
7-
*/
8-
export interface PackageMetadata {
9-
name: string
10-
bin?: string | Record<string, string>
11-
main?: string
12-
module?: unknown
13-
exports?: unknown
14-
}
15-
16-
/**
17-
* Determine if a package is "binary-only" (executable without library entry points).
18-
* Binary-only packages should show execute commands without install commands.
19-
*
20-
* A package is binary-only if:
21-
* - Name starts with "create-" (e.g., create-vite)
22-
* - Scoped name contains "/create-" (e.g., @vue/create-app)
23-
* - Has bin field but no main, module, or exports fields
24-
*/
25-
export function isBinaryOnlyPackage(pkg: PackageMetadata): boolean {
26-
// Check create-* patterns
27-
if (isCreatePackage(pkg.name)) {
28-
return true
29-
}
30-
31-
// Has bin but no entry points
32-
const hasBin =
33-
pkg.bin !== undefined && (typeof pkg.bin === 'string' || Object.keys(pkg.bin).length > 0)
34-
const hasEntryPoint = !!pkg.main || !!pkg.module || !!pkg.exports
35-
36-
return hasBin && !hasEntryPoint
37-
}
38-
39-
/**
40-
* Check if a package uses the create-* naming convention.
41-
*/
42-
export function isCreatePackage(packageName: string): boolean {
43-
const baseName = packageName.startsWith('@') ? packageName.split('/')[1] : packageName
44-
return baseName?.startsWith('create-') || packageName.includes('/create-') || false
45-
}
46-
475
/**
486
* Information about executable commands provided by a package.
497
*/

shared/types/comparison.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export interface FacetValue<T = unknown> {
124124
/** Formatted display string (or ISO date string if type is 'date') */
125125
display: string
126126
/** Optional status indicator */
127-
status?: 'good' | 'info' | 'warning' | 'bad' | 'neutral'
127+
status?: 'good' | 'info' | 'warning' | 'bad' | 'neutral' | 'muted'
128128
/** Value type for special rendering (e.g., dates use DateTime component) */
129129
type?: 'date'
130130
}

shared/utils/binary-detection.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Metadata needed to determine if a package is binary-only.
3+
*/
4+
export interface PackageMetadata {
5+
name: string
6+
bin?: string | Record<string, string>
7+
main?: string
8+
module?: unknown
9+
exports?: unknown
10+
}
11+
12+
/**
13+
* Determine if a package is "binary-only" (executable without library entry points).
14+
* Binary-only packages should show execute commands without install commands.
15+
*
16+
* A package is binary-only if:
17+
* - Name starts with "create-" (e.g., create-vite)
18+
* - Scoped name contains "/create-" (e.g., @vue/create-app)
19+
* - Has bin field but no main, module, or exports fields
20+
*/
21+
export function isBinaryOnlyPackage(pkg: PackageMetadata): boolean {
22+
// Check create-* patterns
23+
if (isCreatePackage(pkg.name)) {
24+
return true
25+
}
26+
27+
// Has bin but no entry points
28+
const hasBin =
29+
pkg.bin !== undefined && (typeof pkg.bin === 'string' || Object.keys(pkg.bin).length > 0)
30+
const hasEntryPoint = !!pkg.main || !!pkg.module || !!pkg.exports
31+
32+
return hasBin && !hasEntryPoint
33+
}
34+
35+
/**
36+
* Check if a package uses the create-* naming convention.
37+
*/
38+
export function isCreatePackage(packageName: string): boolean {
39+
const baseName = packageName.startsWith('@') ? packageName.split('/')[1] : packageName
40+
return baseName?.startsWith('create-') || packageName.includes('/create-') || false
41+
}

test/unit/app/utils/run-command.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import {
33
getExecutableInfo,
44
getRunCommand,
55
getRunCommandParts,
6-
isBinaryOnlyPackage,
7-
isCreatePackage,
86
} from '../../../../app/utils/run-command'
7+
import { isBinaryOnlyPackage, isCreatePackage } from '../../../../shared/utils/binary-detection'
98
import type { JsrPackageInfo } from '../../../../shared/types/jsr'
109

1110
describe('executable detection and run commands', () => {

0 commit comments

Comments
 (0)