diff --git a/app/components/Package/Dependencies.vue b/app/components/Package/Dependencies.vue
index 2bddda193..6036a0d5e 100644
--- a/app/components/Package/Dependencies.vue
+++ b/app/components/Package/Dependencies.vue
@@ -1,4 +1,5 @@
@@ -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),
@@ -121,6 +138,13 @@ const numberFormatter = useNumberFormatter()
)
"
>
+
-
@@ -189,8 +213,28 @@ const numberFormatter = useNumberFormatter()
>
{{ version }}
+
+
+
- ({{ getOutdatedTooltip(outdatedDeps[dep], $t) }})
+ ({{ getOutdatedTooltip(outdatedDeps[dep], t) }})
({{
@@ -209,7 +253,7 @@ const numberFormatter = useNumberFormatter()
@click="expandDeps"
>
{{
- $t(
+ t(
'package.dependencies.show_all',
{
count: numberFormatter.format(sortedDependencies.length),
@@ -264,7 +308,7 @@ const numberFormatter = useNumberFormatter()
@click="expandPeerDeps"
>
{{
- $t(
+ t(
'package.peer_dependencies.show_all',
{
count: numberFormatter.format(sortedPeerDependencies.length),
@@ -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),
@@ -318,7 +362,7 @@ const numberFormatter = useNumberFormatter()
@click="expandOptionalDeps"
>
{{
- $t(
+ t(
'package.optional_dependencies.show_all',
{
count: numberFormatter.format(sortedOptionalDependencies.length),
diff --git a/app/components/Package/SizeBar.vue b/app/components/Package/SizeBar.vue
new file mode 100644
index 000000000..45b5a51af
--- /dev/null
+++ b/app/components/Package/SizeBar.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/composables/npm/usePackageDependencySizes.ts b/app/composables/npm/usePackageDependencySizes.ts
new file mode 100644
index 000000000..65d531374
--- /dev/null
+++ b/app/composables/npm/usePackageDependencySizes.ts
@@ -0,0 +1,115 @@
+import type { NuxtError } from '#app'
+import type { InstallSizeResult } from '#shared/types/install-size'
+import type { MaybeRefOrGetter, Ref } from 'vue'
+import { computed, toValue } from 'vue'
+import { useAsyncData, useNumberFormatter, useBytesFormatter } from '#imports'
+
+/**
+ * Fetches size information for all dependencies of a package.
+ */
+export function usePackageDependencySizes(
+ packageName: MaybeRefOrGetter,
+ version: MaybeRefOrGetter,
+ dependencies: MaybeRefOrGetter | undefined>,
+) {
+ const sortedDependencies = computed(() => {
+ const deps = toValue(dependencies)
+ if (!deps) return []
+ return Object.entries(deps).sort(([a], [b]) => a.localeCompare(b))
+ })
+
+ return useAsyncData(
+ `sizes:${toValue(packageName)}:${toValue(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, depVersion]) => {
+ try {
+ const { data: resolvedVersion, error } = await useResolvedVersion(name, depVersion)
+
+ if (error.value || !resolvedVersion.value) return { kind: 'error', error: error.value! }
+
+ return {
+ kind: 'success',
+ packageSize: await $fetch(
+ `/api/registry/install-size/${name}/v/${encodeURIComponent(resolvedVersion.value)}`,
+ { signal },
+ ),
+ }
+ } catch (err) {
+ return { kind: 'error', error: (err as Ref)?.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],
+ server: false,
+ },
+ )
+}
+
+/**
+ * Helper to generate dependency size tooltips.
+ */
+export function usePackageDependencySizeTooltip(
+ sizereqData: Ref | null>,
+ packageSize: MaybeRefOrGetter,
+ t: (key: string, params?: Record, count?: number) => string,
+) {
+ const numberFormatter = useNumberFormatter()
+ const bytesFormatter = useBytesFormatter()
+
+ function getTooltipText(dep: string): string | undefined {
+ const data = sizereqData.value?.[dep]
+ const total = toValue(packageSize)?.totalSize
+
+ if (data?.kind === 'error') return data.error.message
+
+ const info = data?.kind === 'success' ? data.packageSize : undefined
+ if (!info && !total) return undefined
+
+ const percent = total && info ? (info.selfSize / total) * 100 : undefined
+
+ return [
+ percent && numberFormatter.value.format(percent),
+ info &&
+ info?.totalSize !== info?.selfSize &&
+ t('package.stats.size_tooltip.unpacked', {
+ size: bytesFormatter.format(info.selfSize!),
+ }),
+ info?.totalSize &&
+ t('package.stats.size_tooltip.total', {
+ count: info.dependencyCount,
+ size: bytesFormatter.format(info.totalSize),
+ }),
+ ]
+ .filter(Boolean)
+ .join('\n')
+ }
+
+ return {
+ getTooltipText,
+ }
+}
diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue
index 1610b8aa1..86a4db210 100644
--- a/app/pages/package/[[org]]/[name].vue
+++ b/app/pages/package/[[org]]/[name].vue
@@ -994,10 +994,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
+ "
/>