Skip to content

Commit e7e612d

Browse files
committed
feat: show a warning when significant size increase
When viewing a package, this shows a warning box making the user aware that the number of dependencies or the install size significantly increased. It sets the thresholds as: - Significant size increase = 25% - Significant dependency count increase = >5
1 parent 1e1fe41 commit e7e612d

File tree

11 files changed

+443
-33
lines changed

11 files changed

+443
-33
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script setup lang="ts">
2+
import type { InstallSizeDiff } from '~/composables/useInstallSizeDiff'
3+
4+
const props = defineProps<{
5+
diff: InstallSizeDiff
6+
}>()
7+
8+
const bytesFormatter = useBytesFormatter()
9+
const numberFormatter = useNumberFormatter()
10+
11+
const sizePercent = computed(() => Math.round(props.diff.sizeRatio * 100))
12+
</script>
13+
14+
<template>
15+
<div
16+
class="border border-amber-600/40 bg-amber-500/10 rounded-lg px-3 py-2 text-base text-amber-800 dark:text-amber-400"
17+
>
18+
<h2 class="font-medium mb-1 flex items-center gap-2">
19+
<span class="i-lucide:trending-up w-4 h-4" aria-hidden="true" />
20+
{{
21+
diff.sizeThresholdExceeded && diff.depThresholdExceeded
22+
? $t('package.size_increase.title_both', { version: diff.comparisonVersion })
23+
: diff.sizeThresholdExceeded
24+
? $t('package.size_increase.title_size', { version: diff.comparisonVersion })
25+
: $t('package.size_increase.title_deps', { version: diff.comparisonVersion })
26+
}}
27+
</h2>
28+
<p class="text-sm m-0 mt-1">
29+
<i18n-t v-if="diff.sizeThresholdExceeded" keypath="package.size_increase.size" scope="global">
30+
<template #percent
31+
><strong>{{ sizePercent }}%</strong></template
32+
>
33+
<template #size
34+
><strong>{{ bytesFormatter.format(diff.sizeIncrease) }}</strong></template
35+
>
36+
</i18n-t>
37+
<template v-if="diff.sizeThresholdExceeded && diff.depThresholdExceeded"> · </template>
38+
<i18n-t v-if="diff.depThresholdExceeded" keypath="package.size_increase.deps" scope="global">
39+
<template #count
40+
><strong>+{{ numberFormatter.format(diff.depDiff) }}</strong></template
41+
>
42+
</i18n-t>
43+
</p>
44+
</div>
45+
</template>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { compare, prerelease, valid } from 'semver'
2+
import type { InstallSizeResult, SlimPackument } from '#shared/types'
3+
4+
export interface InstallSizeDiff {
5+
comparisonVersion: string
6+
sizeRatio: number
7+
sizeIncrease: number
8+
currentSize: number
9+
previousSize: number
10+
depDiff: number
11+
currentDeps: number
12+
previousDeps: number
13+
sizeThresholdExceeded: boolean
14+
depThresholdExceeded: boolean
15+
}
16+
17+
const SIZE_INCREASE_THRESHOLD = 0.25
18+
const DEP_INCREASE_THRESHOLD = 5
19+
20+
function getComparisonVersion(pkg: SlimPackument, resolvedVersion: string): string | null {
21+
const isCurrentPrerelease = prerelease(resolvedVersion) !== null
22+
23+
if (isCurrentPrerelease) {
24+
const latest = pkg['dist-tags']?.latest
25+
if (!latest || latest === resolvedVersion) return null
26+
return latest
27+
}
28+
29+
// Find the previous version in time that was stable
30+
const stableVersions = Object.keys(pkg.time)
31+
.filter(v => v !== 'modified' && v !== 'created' && valid(v) !== null && prerelease(v) === null)
32+
.sort((a, b) => compare(a, b))
33+
34+
const currentIdx = stableVersions.indexOf(resolvedVersion)
35+
if (currentIdx <= 0) return null
36+
37+
return stableVersions[currentIdx - 1]!
38+
}
39+
40+
export function useInstallSizeDiff(
41+
packageName: MaybeRefOrGetter<string>,
42+
resolvedVersion: MaybeRefOrGetter<string | null | undefined>,
43+
pkg: MaybeRefOrGetter<SlimPackument | null | undefined>,
44+
currentInstallSize: MaybeRefOrGetter<InstallSizeResult | null | undefined>,
45+
) {
46+
const comparisonVersion = computed<string | null>(() => {
47+
const pkgVal = toValue(pkg)
48+
const version = toValue(resolvedVersion)
49+
if (!pkgVal || !version) return null
50+
return getComparisonVersion(pkgVal, version)
51+
})
52+
53+
const {
54+
data: comparisonInstallSize,
55+
status: comparisonStatus,
56+
execute: fetchComparisonSize,
57+
} = useLazyFetch<InstallSizeResult | null>(
58+
() => {
59+
const v = comparisonVersion.value
60+
if (!v) return ''
61+
return `/api/registry/install-size/${toValue(packageName)}/v/${v}`
62+
},
63+
{
64+
server: false,
65+
immediate: false,
66+
default: () => null,
67+
},
68+
)
69+
70+
if (import.meta.client) {
71+
watch(
72+
comparisonVersion,
73+
v => {
74+
if (v) fetchComparisonSize()
75+
},
76+
{ immediate: true },
77+
)
78+
}
79+
80+
const diff = computed<InstallSizeDiff | null>(() => {
81+
const current = toValue(currentInstallSize)
82+
const previous = comparisonInstallSize.value
83+
const cv = comparisonVersion.value
84+
85+
if (!current || !previous || !cv) return null
86+
87+
const sizeRatio =
88+
previous.totalSize > 0 ? (current.totalSize - previous.totalSize) / previous.totalSize : 0
89+
const depDiff = current.dependencyCount - previous.dependencyCount
90+
91+
const sizeThresholdExceeded = sizeRatio > SIZE_INCREASE_THRESHOLD
92+
const depThresholdExceeded = depDiff > DEP_INCREASE_THRESHOLD
93+
94+
if (!sizeThresholdExceeded && !depThresholdExceeded) return null
95+
96+
return {
97+
comparisonVersion: cv,
98+
sizeRatio,
99+
sizeIncrease: current.totalSize - previous.totalSize,
100+
currentSize: current.totalSize,
101+
previousSize: previous.totalSize,
102+
depDiff,
103+
currentDeps: current.dependencyCount,
104+
previousDeps: previous.dependencyCount,
105+
sizeThresholdExceeded,
106+
depThresholdExceeded,
107+
}
108+
})
109+
110+
return { diff, comparisonVersion, comparisonStatus }
111+
}

app/pages/package/[[org]]/[name].vue

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type {
3+
InstallSizeResult,
34
NpmVersionDist,
45
PackageVersionInfo,
56
PackumentVersion,
@@ -19,6 +20,7 @@ import { detectPublishSecurityDowngradeForVersion } from '~/utils/publish-securi
1920
import { useModal } from '~/composables/useModal'
2021
import { useAtproto } from '~/composables/atproto/useAtproto'
2122
import { togglePackageLike } from '~/utils/atproto/likes'
23+
import { useInstallSizeDiff } from '~/composables/useInstallSizeDiff'
2224
import type { RouteLocationRaw } from 'vue-router'
2325
2426
defineOgImageComponent('Package', {
@@ -182,13 +184,6 @@ const { data: jsrInfo } = useLazyFetch<JsrPackageInfo>(() => `/api/jsr/${package
182184
})
183185
184186
// Fetch total install size (lazy, can be slow for large dependency trees)
185-
interface InstallSizeResult {
186-
package: string
187-
version: string
188-
selfSize: number
189-
totalSize: number
190-
dependencyCount: number
191-
}
192187
const {
193188
data: installSize,
194189
status: installSizeStatus,
@@ -255,6 +250,8 @@ const {
255250
error,
256251
} = usePackage(packageName, () => resolvedVersion.value ?? requestedVersion.value)
257252
253+
const { diff: sizeDiff } = useInstallSizeDiff(packageName, resolvedVersion, pkg, installSize)
254+
258255
// Detect two hydration scenarios where the external _payload.json is missing:
259256
//
260257
// 1. SPA fallback (200.html): No real content was server-rendered.
@@ -1333,6 +1330,8 @@ const showSkeleton = shallowRef(false)
13331330
<div class="space-y-6" :class="$style.areaVulns">
13341331
<!-- Bad package warning -->
13351332
<PackageReplacement v-if="moduleReplacement" :replacement="moduleReplacement" />
1333+
<!-- Size / dependency increase notice -->
1334+
<PackageSizeIncrease v-if="sizeDiff" :diff="sizeDiff" />
13361335
<!-- Vulnerability scan -->
13371336
<ClientOnly>
13381337
<PackageVulnerabilityTree

i18n/locales/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,13 @@
157157
"version": "This version has been deprecated.",
158158
"no_reason": "No reason provided"
159159
},
160+
"size_increase": {
161+
"title_size": "Significant size increase since v{version}",
162+
"title_deps": "Significant dependency count increase since v{version}",
163+
"title_both": "Singificant size and dependency increase since v{version}",
164+
"size": "Install size increased by {percent} ({size} larger)",
165+
"deps": "{count} more dependencies"
166+
},
160167
"replacement": {
161168
"title": "You might not need this dependency.",
162169
"native": "This can be replaced with {replacement}, available since Node {nodeVersion}.",

i18n/schema.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,27 @@
475475
},
476476
"additionalProperties": false
477477
},
478+
"size_increase": {
479+
"type": "object",
480+
"properties": {
481+
"title_size": {
482+
"type": "string"
483+
},
484+
"title_deps": {
485+
"type": "string"
486+
},
487+
"title_both": {
488+
"type": "string"
489+
},
490+
"size": {
491+
"type": "string"
492+
},
493+
"deps": {
494+
"type": "string"
495+
}
496+
},
497+
"additionalProperties": false
498+
},
478499
"replacement": {
479500
"type": "object",
480501
"properties": {

lunaria/files/en-GB.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@
156156
"version": "This version has been deprecated.",
157157
"no_reason": "No reason provided"
158158
},
159+
"size_increase": {
160+
"title_size": "Significant size increase since v{version}",
161+
"title_deps": "Significant dependency count increase since v{version}",
162+
"title_both": "Singificant size and dependency increase since v{version}",
163+
"size": "Install size increased by {percent} ({size} larger)",
164+
"deps": "{count} more dependencies"
165+
},
159166
"replacement": {
160167
"title": "You might not need this dependency.",
161168
"native": "This can be replaced with {replacement}, available since Node {nodeVersion}.",

lunaria/files/en-US.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@
156156
"version": "This version has been deprecated.",
157157
"no_reason": "No reason provided"
158158
},
159+
"size_increase": {
160+
"title_size": "Significant size increase since v{version}",
161+
"title_deps": "Significant dependency count increase since v{version}",
162+
"title_both": "Singificant size and dependency increase since v{version}",
163+
"size": "Install size increased by {percent} ({size} larger)",
164+
"deps": "{count} more dependencies"
165+
},
159166
"replacement": {
160167
"title": "You might not need this dependency.",
161168
"native": "This can be replaced with {replacement}, available since Node {nodeVersion}.",

server/utils/install-size.ts

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,3 @@
1-
/**
2-
* Result of install size calculation
3-
*/
4-
export interface InstallSizeResult {
5-
/** Package name */
6-
package: string
7-
/** Package version */
8-
version: string
9-
/** Unpacked size of the package itself (bytes) */
10-
selfSize: number
11-
/** Total unpacked size including all dependencies (bytes) */
12-
totalSize: number
13-
/** Number of dependencies (including transitive) */
14-
dependencyCount: number
15-
/** Breakdown of dependency sizes */
16-
dependencies: DependencySize[]
17-
}
18-
19-
export interface DependencySize {
20-
name: string
21-
version: string
22-
size: number
23-
/** True if this is an optional dependency */
24-
optional?: boolean
25-
}
26-
271
/**
282
* Calculate the total install size for a package.
293
*

shared/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './i18n-status'
99
export * from './comparison'
1010
export * from './skills'
1111
export * from './version-downloads'
12+
export * from './install-size'

shared/types/install-size.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export interface DependencySize {
2+
name: string
3+
version: string
4+
size: number
5+
/** True if this is an optional dependency */
6+
optional?: boolean
7+
}
8+
9+
export interface InstallSizeResult {
10+
/** Package name */
11+
package: string
12+
/** Package version */
13+
version: string
14+
/** Unpacked size of the package itself (bytes) */
15+
selfSize: number
16+
/** Total unpacked size including all dependencies (bytes) */
17+
totalSize: number
18+
/** Number of dependencies (including transitive) */
19+
dependencyCount: number
20+
/** Breakdown of dependency sizes */
21+
dependencies: DependencySize[]
22+
}

0 commit comments

Comments
 (0)