@@ -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+
65152useSeoMeta ({
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+ · ;
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"
0 commit comments