Skip to content

Commit 97e094a

Browse files
committed
feat: add package size and dependency changes
1 parent 0bf2f53 commit 97e094a

3 files changed

Lines changed: 173 additions & 4 deletions

File tree

app/pages/package-timeline/[[org]]/[packageName].vue

Lines changed: 156 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,19 @@ const timelineEntries = computed(() => {
4141
const time = pkg.value.time
4242
const versions = Object.keys(pkg.value.versions)
4343
44+
const tagsByVersion = new Map<string, string[]>()
45+
for (const [tag, ver] of Object.entries(pkg.value['dist-tags'] ?? {})) {
46+
const list = tagsByVersion.get(ver)
47+
if (list) list.push(tag)
48+
else tagsByVersion.set(ver, [tag])
49+
}
50+
4451
return versions
4552
.filter(v => time[v])
4653
.map(v => ({
4754
version: v,
4855
time: time[v]!,
49-
tags: Object.entries(pkg.value!['dist-tags'] ?? {})
50-
.filter(([, ver]) => ver === v)
51-
.map(([tag]) => tag),
56+
tags: tagsByVersion.get(v) ?? [],
5257
}))
5358
.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime())
5459
})
@@ -62,6 +67,88 @@ function loadMore() {
6267
visibleCount.value += PAGE_SIZE
6368
}
6469
70+
const SIZE_INCREASE_THRESHOLD = 0.25
71+
const DEP_INCREASE_THRESHOLD = 5
72+
73+
const sizeCache = shallowReactive(new Map<string, InstallSizeResult>())
74+
const fetchingVersions = shallowReactive(new Set<string>())
75+
76+
async function fetchSize(ver: string) {
77+
if (sizeCache.has(ver) || fetchingVersions.has(ver)) return
78+
fetchingVersions.add(ver)
79+
try {
80+
const data = await $fetch<InstallSizeResult>(
81+
`/api/registry/install-size/${packageName.value}/v/${ver}`,
82+
)
83+
sizeCache.set(ver, data)
84+
} catch {
85+
// silently skip — size data is best-effort
86+
} finally {
87+
fetchingVersions.delete(ver)
88+
}
89+
}
90+
91+
// Fetch sizes for visible version pairs
92+
if (import.meta.client) {
93+
watch(
94+
visibleEntries,
95+
entries => {
96+
for (const entry of entries) {
97+
fetchSize(entry.version)
98+
}
99+
},
100+
{ immediate: true },
101+
)
102+
}
103+
104+
interface SizeEvent {
105+
direction: 'increase' | 'decrease'
106+
sizeRatio: number
107+
sizeDelta: number
108+
depDiff: number
109+
sizeThresholdExceeded: boolean
110+
depThresholdExceeded: boolean
111+
}
112+
113+
// Compute size events between consecutive visible versions
114+
const sizeEvents = computed(() => {
115+
const events = new Map<string, SizeEvent>()
116+
const entries = visibleEntries.value
117+
118+
for (let i = 0; i < entries.length - 1; i++) {
119+
const current = sizeCache.get(entries[i]!.version)
120+
const previous = sizeCache.get(entries[i + 1]!.version)
121+
if (!current || !previous) continue
122+
123+
const sizeRatio =
124+
previous.totalSize > 0 ? (current.totalSize - previous.totalSize) / previous.totalSize : 0
125+
const depDiff = current.dependencyCount - previous.dependencyCount
126+
127+
const sizeIncreased = sizeRatio > SIZE_INCREASE_THRESHOLD
128+
const sizeDecreased = sizeRatio < -SIZE_INCREASE_THRESHOLD
129+
const depsIncreased = depDiff > DEP_INCREASE_THRESHOLD
130+
const depsDecreased = depDiff < -DEP_INCREASE_THRESHOLD
131+
132+
if (!sizeIncreased && !sizeDecreased && !depsIncreased && !depsDecreased) continue
133+
134+
events.set(entries[i]!.version, {
135+
direction:
136+
(sizeDecreased || depsDecreased) && !sizeIncreased && !depsIncreased
137+
? 'decrease'
138+
: 'increase',
139+
sizeRatio,
140+
sizeDelta: current.totalSize - previous.totalSize,
141+
depDiff,
142+
sizeThresholdExceeded: sizeIncreased || sizeDecreased,
143+
depThresholdExceeded: depsIncreased || depsDecreased,
144+
})
145+
}
146+
147+
return events
148+
})
149+
150+
const bytesFormatter = useBytesFormatter()
151+
65152
useSeoMeta({
66153
title: () => `Timeline - ${packageName.value} - npmx`,
67154
description: () => `Version timeline for ${packageName.value}`,
@@ -83,6 +170,72 @@ useSeoMeta({
83170
<!-- Timeline -->
84171
<ol v-if="visibleEntries.length" class="relative border-s border-border ms-4">
85172
<li v-for="entry in visibleEntries" :key="entry.version" class="mb-6 ms-6">
173+
<!-- Size event -->
174+
<div v-if="sizeEvents.has(entry.version)" class="mb-4 -ms-6 ps-6 relative">
175+
<span
176+
class="absolute -start-2 flex items-center justify-center w-4 h-4 rounded-full border"
177+
:class="
178+
sizeEvents.get(entry.version)!.direction === 'decrease'
179+
? 'bg-green-500 border-green-600'
180+
: 'bg-amber-500 border-amber-600'
181+
"
182+
>
183+
<span
184+
class="w-2.5 h-2.5 text-white"
185+
:class="
186+
sizeEvents.get(entry.version)!.direction === 'decrease'
187+
? 'i-lucide:trending-down'
188+
: 'i-lucide:trending-up'
189+
"
190+
aria-hidden="true"
191+
/>
192+
</span>
193+
<div
194+
class="border rounded-lg px-3 py-2 text-sm"
195+
:class="
196+
sizeEvents.get(entry.version)!.direction === 'decrease'
197+
? 'border-green-600/40 bg-green-500/10 text-green-800 dark:text-green-400'
198+
: 'border-amber-600/40 bg-amber-500/10 text-amber-800 dark:text-amber-400'
199+
"
200+
>
201+
<template v-if="sizeEvents.get(entry.version)!.sizeThresholdExceeded">
202+
{{
203+
sizeEvents.get(entry.version)!.direction === 'decrease'
204+
? $t('package.timeline.size_decrease', {
205+
percent: Math.abs(
206+
Math.round(sizeEvents.get(entry.version)!.sizeRatio * 100),
207+
),
208+
size: bytesFormatter.format(
209+
Math.abs(sizeEvents.get(entry.version)!.sizeDelta),
210+
),
211+
})
212+
: $t('package.timeline.size_increase', {
213+
percent: Math.round(sizeEvents.get(entry.version)!.sizeRatio * 100),
214+
size: bytesFormatter.format(sizeEvents.get(entry.version)!.sizeDelta),
215+
})
216+
}}
217+
</template>
218+
<template
219+
v-if="
220+
sizeEvents.get(entry.version)!.sizeThresholdExceeded &&
221+
sizeEvents.get(entry.version)!.depThresholdExceeded
222+
"
223+
>
224+
&middot;
225+
</template>
226+
<template v-if="sizeEvents.get(entry.version)!.depThresholdExceeded">
227+
{{
228+
sizeEvents.get(entry.version)!.depDiff > 0
229+
? $t('package.timeline.dep_increase', {
230+
count: sizeEvents.get(entry.version)!.depDiff,
231+
})
232+
: $t('package.timeline.dep_decrease', {
233+
count: Math.abs(sizeEvents.get(entry.version)!.depDiff),
234+
})
235+
}}
236+
</template>
237+
</div>
238+
</div>
86239
<!-- Dot -->
87240
<span
88241
class="absolute -start-2 flex items-center justify-center w-4 h-4 rounded-full border border-border"

i18n/locales/en.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,11 @@
422422
"no_match_filter": "No versions match {filter}"
423423
},
424424
"timeline": {
425-
"load_more": "Load more"
425+
"load_more": "Load more",
426+
"size_increase": "Install size increased by {percent}% ({size})",
427+
"size_decrease": "Install size decreased by {percent}% ({size})",
428+
"dep_increase": "{count} dependencies added",
429+
"dep_decrease": "{count} dependencies removed"
426430
},
427431
"dependencies": {
428432
"title": "Dependency ({count}) | Dependencies ({count})",

i18n/schema.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,6 +1272,18 @@
12721272
"properties": {
12731273
"load_more": {
12741274
"type": "string"
1275+
},
1276+
"size_increase": {
1277+
"type": "string"
1278+
},
1279+
"size_decrease": {
1280+
"type": "string"
1281+
},
1282+
"dep_increase": {
1283+
"type": "string"
1284+
},
1285+
"dep_decrease": {
1286+
"type": "string"
12751287
}
12761288
},
12771289
"additionalProperties": false

0 commit comments

Comments
 (0)