Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
251 changes: 251 additions & 0 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,198 @@ const sortedOptionalDependencies = computed(() => {
return Object.entries(props.optionalDependencies).sort(([a], [b]) => a.localeCompare(b))
})

// Fetch size information for dependencies that require it
const { data: serverSizes, pending: sizesLoading } = await useAsyncData(
`sizes:${props.packageName}:${props.version}`,
async (_app, { signal }) => {
const entries = sortedDependencies.value

const results = await Promise.all(
entries.map<
Promise<
{ kind: 'success'; packageSize: InstallSizeResult } | { kind: 'error'; error: NuxtError }
>
>(async ([name, version]) => {
try {
const { data: resolvedVersion, error } = await useResolvedVersion(name, version)

if (error.value || !resolvedVersion.value) return { kind: 'error', error: error.value! }

return {
kind: 'success',
packageSize: await $fetch<InstallSizeResult>(
`/api/registry/install-size/${name}/v/${encodeURIComponent(resolvedVersion.value)}`,
{ signal },
),
}
} catch (err) {
return { kind: 'error', error: (err as Ref<NuxtError>)?.value }
}
}),
)

return results.reduce(
(acc, curr) => {
if (curr.kind === 'error') return acc
acc[curr.packageSize.package] = curr
return acc
},
{} as Record<
string,
{ kind: 'success'; packageSize: InstallSizeResult } | { kind: 'error'; error: NuxtError }
>,
)
},
{
watch: [sortedDependencies],
},
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

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 total = props.packageSize.totalSize

// 1. Map everything first, preserving the 'bundled' flag from the source
const allMapped = props.packageSize.dependencies
.map(depSize => {
const bundled = !sortedDependencies.value.some(([name]) => name === depSize.name)
const percent = props.packageSize ? (depSize.size / props.packageSize.totalSize) * 100 : 0
const serverData = serverSizes.value?.[depSize.name]
const error = serverData?.kind === 'error' ? serverData.error : null
return {
info:
serverData?.kind === 'success'
? {
package: depSize.name,
totalSize: serverData.packageSize?.totalSize ?? depSize.size,
selfSize: serverData.packageSize?.selfSize ?? depSize.size,
}
: {
package: depSize.name,
totalSize: depSize.size,
selfSize: depSize.size,
},
error,
bundled,
percent,
} as Sizereq
})
.sort((a, b) => {
// Bundled first
if (a.bundled !== b.bundled) return a.bundled ? -1 : 1
return b.info.totalSize - a.info.totalSize
})

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

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

const totalOthersSize = others.reduce((acc, d) => acc + d.info.totalSize, 0)
const othersPercentage = (totalOthersSize / total) * 100
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

if (othersPercentage < THRESHOLD_PERCENT || others.length === 1) {
visible.push(others[0]!)
others.length = 0
visible.sort((a, b) => b.info.totalSize - a.info.totalSize)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

return { visible, others, totalOthersSize, 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, getDepSizeTooltipText(size), '']),
]

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

// Sum up everything we actually HAVE data for
const self = props.packageSize.selfSize || 0
const depsSum = [
...sortedSizereqDependecies.value.visible,
...sortedSizereqDependecies.value.others,
].reduce((acc, d) => acc + d.info.totalSize, 0)

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

// Get dependency size tooltip
function getDepSizeTooltip(dep: string): string | undefined {
const size = [
...sortedSizereqDependecies.value.visible,
...sortedSizereqDependecies.value.others,
].find(d => d.info.package === dep)
return size && getDepSizeTooltipText(size)
}

function getDepSizeTooltipText(size: Sizereq): string {
const packageSize = size?.error ? undefined : size?.info
const percent = size?.percent
return [
size?.error?.message,
percent && numberFormatter.value.format(percent),
packageSize &&
packageSize?.totalSize !== packageSize?.selfSize &&
t('package.stats.size_tooltip.unpacked', {
size: bytesFormatter.format(packageSize.selfSize!),
}),
packageSize?.totalSize &&
t('package.stats.size_tooltip.total', {
count: packageSize.dependencyCount,
size: bytesFormatter.format(packageSize.totalSize),
}),
]
.filter(Boolean)
.join('\n')
}

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

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

<template>
Expand All @@ -121,6 +317,41 @@ const numberFormatter = useNumberFormatter()
)
"
>
<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-blue-500"
: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-blue-500' : 'bg-fg'"
:style="{ width: dep.percent + '%' }"
/>
</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>
<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 Expand Up @@ -189,6 +420,26 @@ 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': sizesLoading && !serverSizes?.[dep],
}"
aria-hidden="true"
/>
</button>
</TooltipApp>
<span v-if="outdatedDeps[dep]" class="sr-only">
({{ getOutdatedTooltip(outdatedDeps[dep], $t) }})
</span>
Expand Down
4 changes: 4 additions & 0 deletions app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -983,10 +983,14 @@ const showSkeleton = shallowRef(false)
v-if="hasDependencies && resolvedVersion && displayVersion"
:package-name="pkg.name"
:version="resolvedVersion"
:package-size="installSize"
:dependencies="displayVersion.dependencies"
:peer-dependencies="displayVersion.peerDependencies"
:peer-dependencies-meta="displayVersion.peerDependenciesMeta"
:optional-dependencies="displayVersion.optionalDependencies"
:bundled-dependencies="
displayVersion.bundleDependencies || displayVersion.bundledDependencies
"
/>

<!-- Keywords -->
Expand Down
Loading