Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions app/components/Package/Dependencies.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ const { t } = useI18n()
const props = defineProps<{
packageName: string
version: string
packageSize?: InstallSizeResult | undefined
dependencies?: Record<string, string>
peerDependencies?: Record<string, string>
peerDependenciesMeta?: Record<string, { optional?: boolean }>
optionalDependencies?: Record<string, string>
bundledDependencies?: boolean | string[]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}>()

// Fetch outdated info for dependencies
Expand Down Expand Up @@ -121,6 +123,20 @@ const numberFormatter = useNumberFormatter()
)
"
>
<div class="mb-4">
<div class="flex items-center justify-between mb-3">
<div class="text-xs text-fg-subtle uppercase tracking-wider font-medium">
{{ $t('package.stats.install_size') }}
</div>
</div>
<PackageSizeBar
:package-name="props.packageName"
:version="props.version"
:package-size="props.packageSize"
:dependencies="props.dependencies"
:bundled-dependencies="props.bundledDependencies"
/>
</div>
<ul class="space-y-1 list-none m-0" :aria-label="$t('package.dependencies.list_label')">
<li
v-for="[dep, version] in visibleDeps"
Expand Down
140 changes: 140 additions & 0 deletions app/components/Package/SizeBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<script setup lang="ts">
import type { InstallSizeResult } from '#shared/types/install-size'

const props = withDefaults(
defineProps<{
packageName: string
version: string
packageSize?: InstallSizeResult | undefined
dependencies?: Record<string, string>
bundledDependencies?: boolean | string[]
height?: string
}>(),
{
height: 'h-6',
},
)

const { data: sizereqData } = usePackageDependencySizes(
props.packageName,
props.version,
props.packageSize?.dependencies,
)

// Minimum percentage to be shown as an individual slice
const THRESHOLD_PERCENT = 2

type Sizereq = {
info: InstallSizeResult
bundled: boolean
percent: number
}

// Process dependencies for size visualization
const sortedSizereqDependecies = computed(() => {
if (!props.packageSize?.totalSize || !props.packageSize.dependencies) {
return { visible: [], others: [], totalOthersSize: 0, othersPercentage: 0 }
}

const allMapped = props.packageSize.dependencies.map(depSize => {
let bundled = false
switch (typeof props.bundledDependencies) {
case 'boolean':
bundled = props.bundledDependencies
break
case 'object':
bundled = props.bundledDependencies.some(name => name === depSize.name)
break
}
const percent = props.packageSize ? (depSize.size / props.packageSize.totalSize) * 100 : 0
const serverData = sizereqData.value?.[depSize.name]
return {
info:
serverData?.kind === 'success' && serverData.packageSize
? {
package: depSize.name,
version: depSize.version,
totalSize: serverData.packageSize.totalSize,
selfSize: serverData.packageSize.selfSize,
}
: {
package: depSize.name,
version: depSize.version,
totalSize: depSize.size,
selfSize: depSize.size,
},
bundled,
percent,
} as Sizereq
})

const visible: Sizereq[] = []
const others: Sizereq[] = []

for (const dep of allMapped) {
const percentage = (dep.info.selfSize / props.packageSize.totalSize) * 100
if (percentage >= THRESHOLD_PERCENT) {
visible.push({ ...dep, percent: percentage })
} else {
others.push(dep)
}
}

const othersSelfSize = others.reduce((acc, d) => acc + d.info.selfSize, 0)
const othersPercentage = (othersSelfSize / props.packageSize.totalSize) * 100

return { visible, others, totalOthersSize: othersSelfSize, othersPercentage }
})

const selfSizeWidth = computed(() => {
if (!props.packageSize?.selfSize || !props.packageSize?.totalSize) return 0
return (props.packageSize.selfSize / props.packageSize.totalSize) * 100
})

const remainingWidth = computed(() => {
const total = props.packageSize?.totalSize
if (!total) return 100

const self = props.packageSize.selfSize || 0
const depsSum = [
...sortedSizereqDependecies.value.visible,
...sortedSizereqDependecies.value.others,
].reduce((acc, d) => acc + d.info.selfSize, 0)

const width = ((total - (self + depsSum)) / total) * 100
return Math.max(0, width)
})
</script>

<template>
<div
:class="[
props.height,
'gap-0.5 flex flex-row w-full bg-fg-muted/10 overflow-hidden rounded-md',
]"
>
<div
v-if="selfSizeWidth > 0"
class="h-full bg-accent"
:style="{ width: selfSizeWidth + '%' }"
/>

<template v-for="dep in sortedSizereqDependecies.visible" :key="dep.info.package">
<div
class="h-full"
:class="dep.bundled ? 'bg-accent' : 'bg-fg'"
:style="{ width: dep.percent + '%' }"
/>
</template>

<div
v-if="sortedSizereqDependecies.others.length > 0"
class="h-full bg-fg flex items-center justify-center"
:style="{ width: sortedSizereqDependecies.othersPercentage + '%' }"
>
<span class="i-lucide:boxes w-3 h-3 text-bg" aria-hidden="true" />
</div>

<div v-if="remainingWidth > 0" class="h-full bg-bg-elevated animate-skeleton-pulse flex-1" />
</div>
</template>
127 changes: 127 additions & 0 deletions app/components/Package/SizeCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<script setup lang="ts">
import type { SizeEntry } from '~/types/size'
import { getSizeRoute, packageRoute } from '~/utils/router'

const props = defineProps<{
entry: SizeEntry
}>()

const { t } = useI18n()
const numberFormatter = useNumberFormatter()
const bytesFormatter = useBytesFormatter()

const target = useTemplateRef('target')
const targetIsVisible = shallowRef(false)

const { stop } = useIntersectionObserver(target, ([entry]) => {
if ((targetIsVisible.value = entry?.isIntersecting || false)) stop()
})

const isSizeUnknown = computed(() => Number.isNaN(props.entry.totalSize))

const { data: fetchedSize, execute } = usePackageSize(
() => props.entry.name,
() => props.entry.version,
{ immediate: false },
)

watch(targetIsVisible, visible => {
if (visible && isSizeUnknown.value) {
execute()
}
})

const displayTotalSize = computed(() => {
if (!isSizeUnknown.value) return props.entry.totalSize
return fetchedSize.value?.totalSize ?? NaN
})

const displayDepCount = computed(() => {
if (!isSizeUnknown.value) return props.entry.depCount
return fetchedSize.value?.dependencies?.length ?? props.entry.depCount
})
</script>

<template>
<div ref="target" class="p-4 border border-border rounded-lg bg-bg-elevated transition-colors">
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-3 flex-wrap">
<div class="flex-1 min-w-0">
<LinkBase
:to="packageRoute(entry.name, entry.version)"
class="block truncate hover:text-accent transition-colors font-mono font-medium text-base"
:title="entry.name"
>
{{ entry.name }}
</LinkBase>
</div>
<LinkBase
variant="button-secondary"
size="sm"
:to="getSizeRoute(entry.name, entry.version)"
classicon="i-lucide:list-tree"
class="whitespace-nowrap gap-2 px-3 py-1.5"
>
<span class="text-xs font-medium">{{ t('package.stats.view_all_sizes') }}</span>
</LinkBase>
</div>
<div class="text-xs text-fg-subtle font-mono mt-1">
{{ entry.version }}
</div>
</div>
<div class="force-text-right">
<div class="font-mono text-sm">{{ bytesFormatter.format(entry.selfSize) }}</div>
<div class="text-xs text-fg-muted font-mono mt-1">
<template v-if="!Number.isNaN(displayTotalSize)">
{{
t('package.stats.size.total', {
size: bytesFormatter.format(displayTotalSize),
})
}}
</template>

<template v-else-if="targetIsVisible">
<div
class="w-3 h-3 border-2 border-fg-muted/20 border-t-accent rounded-full animate-spin"
aria-hidden="true"
/>
</template>
</div>
</div>
</div>

<div class="mt-4 flex items-center gap-4 text-sm">
<div class="flex items-center gap-1.5 text-fg-muted">
<span class="i-lucide:boxes w-4 h-4" aria-hidden="true" />
<template v-if="!Number.isNaN(displayTotalSize)">
<span>{{ numberFormatter.format(displayDepCount) }}</span>
</template>
<template v-else-if="targetIsVisible">
<div
class="w-3 h-3 border-2 border-fg-muted/20 border-t-accent rounded-full animate-spin"
aria-hidden="true"
/>
</template>
</div>

<div class="flex-1">
<PackageSizeBar
v-if="!Number.isNaN(displayTotalSize)"
:package-name="entry.name"
:version="entry.version"
:package-size="
fetchedSize || {
package: entry.name,
version: entry.version,
selfSize: entry.selfSize,
totalSize: displayTotalSize,
dependencyCount: displayDepCount,
dependencies: [],
}
"
/>
</div>
</div>
</div>
</template>
Loading
Loading