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
60 changes: 52 additions & 8 deletions app/components/Package/Dependencies.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { NuxtError } from '#app'
import { SEVERITY_TEXT_COLORS, getHighestSeverity } from '#shared/utils/severity'
import { getOutdatedTooltip, getVersionClass } from '~/utils/npm/outdated-dependencies'

Expand All @@ -7,10 +8,12 @@ const { t } = useI18n()
const props = defineProps<{
packageName: string
version: string
packageSize?: InstallSizeResult | null
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 @@ -66,6 +69,19 @@ const sortedOptionalDependencies = computed(() => {
return Object.entries(props.optionalDependencies).sort(([a], [b]) => a.localeCompare(b))
})

// Fetch size information for dependencies that require it
const { data: sizereqData, pending: sizereqLoading } = usePackageDependencySizes(
() => props.packageName,
() => props.version,
() => props.dependencies,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const { getTooltipText: getDepSizeTooltip } = usePackageDependencySizeTooltip(
sizereqData,
() => props.packageSize,
t,
)

// Get version tooltip
function getDepVersionTooltip(dep: string, version: string) {
const outdated = outdatedDeps.value[dep]
Expand Down Expand Up @@ -103,6 +119,7 @@ const {
} = useVisibleItems(sortedOptionalDependencies, 10)

const numberFormatter = useNumberFormatter()
const bytesFormatter = useBytesFormatter()
</script>

<template>
Expand All @@ -112,7 +129,7 @@ const numberFormatter = useNumberFormatter()
v-if="sortedDependencies.length > 0"
id="dependencies"
:title="
$t(
t(
'package.dependencies.title',
{
count: numberFormatter.format(sortedDependencies.length),
Expand All @@ -121,6 +138,13 @@ const numberFormatter = useNumberFormatter()
)
"
>
<PackageSizeBar
:package-name="props.packageName"
:version="props.version"
:package-size="props.packageSize"
:dependencies="props.dependencies"
:bundled-dependencies="props.bundledDependencies"
/>
<ul class="space-y-1 list-none m-0" :aria-label="$t('package.dependencies.list_label')">
<li
v-for="[dep, version] in visibleDeps"
Expand All @@ -135,12 +159,12 @@ const numberFormatter = useNumberFormatter()
v-if="outdatedDeps[dep]"
class="shrink-0"
:class="getVersionClass(outdatedDeps[dep])"
:text="getOutdatedTooltip(outdatedDeps[dep], $t)"
:text="getOutdatedTooltip(outdatedDeps[dep], t)"
>
<button
type="button"
class="inline-flex items-center justify-center p-2 -m-2"
:aria-label="getOutdatedTooltip(outdatedDeps[dep], $t)"
:aria-label="getOutdatedTooltip(outdatedDeps[dep], t)"
>
<span class="i-lucide:circle-alert w-3 h-3" aria-hidden="true" />
</button>
Expand Down Expand Up @@ -189,8 +213,28 @@ const numberFormatter = useNumberFormatter()
>
{{ version }}
</LinkBase>
<TooltipApp
v-if="getDepSizeTooltip(dep)"
class="shrink-0"
:class="getVersionClass(undefined)"
:text="getDepSizeTooltip(dep)"
>
<button
type="button"
class="inline-flex items-center justify-center p-2 -m-2 outline-none"
:aria-label="getDepSizeTooltip(dep)"
>
<span
class="i-lucide:info w-3 h-3 opacity-50 transition-opacity hover:opacity-100"
:class="{
'i-svg-spinners:ring-resize': sizereqLoading && !sizereqData?.[dep],
}"
aria-hidden="true"
/>
</button>
</TooltipApp>
<span v-if="outdatedDeps[dep]" class="sr-only">
({{ getOutdatedTooltip(outdatedDeps[dep], $t) }})
({{ getOutdatedTooltip(outdatedDeps[dep], t) }})
</span>
<span v-if="getVulnerableDepInfo(dep)" class="sr-only">
({{
Expand All @@ -209,7 +253,7 @@ const numberFormatter = useNumberFormatter()
@click="expandDeps"
>
{{
$t(
t(
'package.dependencies.show_all',
{
count: numberFormatter.format(sortedDependencies.length),
Expand Down Expand Up @@ -264,7 +308,7 @@ const numberFormatter = useNumberFormatter()
@click="expandPeerDeps"
>
{{
$t(
t(
'package.peer_dependencies.show_all',
{
count: numberFormatter.format(sortedPeerDependencies.length),
Expand All @@ -280,7 +324,7 @@ const numberFormatter = useNumberFormatter()
v-if="sortedOptionalDependencies.length > 0"
id="optional-dependencies"
:title="
$t(
t(
'package.optional_dependencies.title',
{
count: numberFormatter.format(sortedOptionalDependencies.length),
Expand Down Expand Up @@ -318,7 +362,7 @@ const numberFormatter = useNumberFormatter()
@click="expandOptionalDeps"
>
{{
$t(
t(
'package.optional_dependencies.show_all',
{
count: numberFormatter.format(sortedOptionalDependencies.length),
Expand Down
178 changes: 178 additions & 0 deletions app/components/Package/SizeBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<script setup lang="ts">
import type { NuxtError } from '#app'
import type { InstallSizeResult } from '#shared/types/install-size'

const { t } = useI18n()

const props = defineProps<{
packageName: string
version: string
packageSize?: InstallSizeResult | null
dependencies?: Record<string, string>
bundledDependencies?: boolean | string[]
}>()

const { data: sizereqData, pending: sizereqLoading } = usePackageDependencySizes(
() => props.packageName,
() => props.version,
() => props.dependencies,
)

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

type Sizereq = {
info: InstallSizeResult
bundled: boolean
percent: number
error: NuxtError | null
}

// 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]
const error = serverData?.kind === 'error' ? serverData.error : null
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,
},
error,
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 othersTooltip = computed(() => {
const others = sortedSizereqDependecies.value.others
if (others.length === 0) return ''

const MAX_VISIBLE_IN_TOOLTIP = 0
const visiblePart = others.slice(0, MAX_VISIBLE_IN_TOOLTIP)
const remainingCount = others.length - MAX_VISIBLE_IN_TOOLTIP

const lines = [
bytesFormatter.format(sortedSizereqDependecies.value.totalOthersSize),
numberFormatter.value.format(sortedSizereqDependecies.value.othersPercentage),
'',
...visiblePart.flatMap(size => [size.info.package, getDepSizeTooltip(size.info.package), '']),
]

if (remainingCount > 0) {
lines.push(t('package.size_increase.deps', { count: remainingCount }))
}

return lines.join('\n')
})

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)
})

const { getTooltipText: getDepSizeTooltip } = usePackageDependencySizeTooltip(
sizereqData,
() => props.packageSize,
t,
)

const numberFormatter = useNumberFormatter()
const bytesFormatter = useBytesFormatter()
</script>

<template>
<div class="gap-0.5 flex flex-row h-6 w-full bg-fg-muted/10 overflow-hidden rounded-md">
<TooltipApp
v-if="selfSizeWidth > 0"
:text="
t('package.stats.size_tooltip.unpacked', {
size: bytesFormatter.format(props.packageSize?.selfSize || 0),
})
"
class="h-full bg-accent"
:style="{ width: selfSizeWidth + '%' }"
/>

<template v-for="dep in sortedSizereqDependecies.visible" :key="dep.info.package">
<TooltipApp
:text="`${dep.info.package}\n${getDepSizeTooltip(dep.info.package)}`"
class="h-full"
:class="dep.bundled ? 'bg-accent' : 'bg-fg'"
:style="{ width: dep.percent + '%' }"
>
<RouterLink
:to="packageRoute(dep.info.package, dep.info.version)"
class="block w-full h-full"
/>
</TooltipApp>
</template>

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

<div v-if="remainingWidth > 0" class="h-full bg-bg-elevated animate-skeleton-pulse flex-1" />
</div>
</template>
Loading
Loading