Skip to content

Commit f327d68

Browse files
authored
feat: build sessions compare (#95)
1 parent f0db95f commit f327d68

9 files changed

Lines changed: 399 additions & 31 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import { bytesToHumanSize } from '~/utils/format'
4+
5+
const props = defineProps<{
6+
name: string
7+
description: string
8+
icon: string
9+
current: number
10+
previous: number
11+
format?: string
12+
}>()
13+
14+
const formattedCurrent = computed(() => {
15+
if (props.format === 'bytes')
16+
return bytesToHumanSize(props.current)
17+
return props.current
18+
})
19+
</script>
20+
21+
<template>
22+
<div font-500 op50 text-4 flex="~ items-center gap-2" :title="description">
23+
<div :class="icon" class="text-xl" />
24+
{{ name }}
25+
</div>
26+
<div flex="~ gap-2" items-center>
27+
<div v-if="format === 'bytes'">
28+
<span font-semibold text-5 font-mono>{{ (formattedCurrent as Array<number | string>)[0] }}</span>
29+
<span font-semibold text-4 font-mono>{{ (formattedCurrent as Array<number | string>)[1] }}</span>
30+
</div>
31+
<div v-else>
32+
<span font-semibold text-5 font-mono>{{ formattedCurrent }}</span>
33+
</div>
34+
<DisplayComparisonMetric :current="current" :previous="previous" />
35+
</div>
36+
</template>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
sessions: Array<{ id: string, createdAt: Date, title: string }>
4+
}>()
5+
</script>
6+
7+
<template>
8+
<div flex="~ gap5" w-full border="b base" pb3>
9+
<div v-for="(item) of sessions" :key="item.id" flex-1 border="~ base rounded" p4 grid="~ cols-[max-content_140px_2fr] max-lg:cols-[max-content_80px_2fr] gap-2 items-center">
10+
<!-- session meta -->
11+
<div class="i-ph-hash-duotone" />
12+
<div>
13+
{{ item.title }}
14+
</div>
15+
<div font-mono>
16+
<span>{{ item.id }}</span>
17+
</div>
18+
<!-- created at meta -->
19+
<div class="i-ph-clock-duotone" />
20+
<div>
21+
Created At
22+
</div>
23+
<div font-mono>
24+
<time :datetime="item.createdAt.toISOString()">{{ item.createdAt.toLocaleString() }}</time>
25+
</div>
26+
</div>
27+
</div>
28+
</template>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
4+
const props = defineProps<{
5+
current: number
6+
previous: number
7+
}>()
8+
9+
const isNotChanged = computed(() => props.previous === props.current)
10+
const normalizedPercent = computed(() => Math.abs((props.current - props.previous) / props.previous * 100).toFixed(isNotChanged.value ? 0 : 2))
11+
const trendSymbol = computed(() => isNotChanged.value ? '' : (props.current > props.previous ? '+' : '-'))
12+
const comparisonColorClass = computed(() => isNotChanged.value ? 'text-gray-500' : (props.current > props.previous ? 'text-green-500' : 'text-red-500'))
13+
</script>
14+
15+
<template>
16+
<span
17+
text-4 mt-1 font-mono
18+
:class="comparisonColorClass"
19+
>
20+
{{ trendSymbol }}{{ normalizedPercent }}%
21+
</span>
22+
</template>
Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,67 @@
11
<script setup lang="ts">
2-
import { useRpc } from '#imports'
2+
import type { BuildInfo } from '~~/node/rolldown/logs-manager'
3+
import { NuxtLink } from '#components'
4+
import { computed } from 'vue'
5+
import { parseReadablePath } from '~/utils/filepath'
36
4-
const rpc = useRpc()
5-
const sessions = await rpc.value!['vite:rolldown:list-sessions']()
7+
const props = defineProps<{
8+
sessionMode: 'list' | 'compare'
9+
sessions: BuildInfo[]
10+
selectedSessionIds: string[]
11+
selectedSessions: BuildInfo[]
12+
}>()
13+
const emit = defineEmits<{
14+
(e: 'select', session: BuildInfo): void
15+
}>()
16+
17+
function parseEntryPath(session: BuildInfo) {
18+
return parseReadablePath(session.meta.inputs[0]?.filename ?? '', session.meta.cwd).path
19+
}
20+
21+
const selectedSessionEntry = computed(() => {
22+
const session = props.selectedSessions?.[0]
23+
return session ? parseEntryPath(session) : ''
24+
})
25+
26+
function checkIsDifferentEntry(session: BuildInfo) {
27+
return selectedSessionEntry.value && selectedSessionEntry.value !== parseEntryPath(session)
28+
}
29+
30+
function select(session: BuildInfo) {
31+
if (props.sessionMode === 'compare' && !checkIsDifferentEntry(session)) {
32+
emit('select', session)
33+
}
34+
}
635
</script>
736

837
<template>
938
<div flex="~ col gap-2">
10-
<NuxtLink
11-
v-for="session of sessions"
12-
:key="session.id"
13-
:to="`/session/${session.id}`"
14-
hover="bg-active"
15-
border="~ base rounded-md"
16-
flex="~ col gap-1"
17-
px4 py3
18-
>
19-
<div flex="~ gap-1 items-center" font-mono op50 text-sm>
20-
<div i-ph-hash-duotone />
21-
{{ session.id }}
22-
</div>
23-
<div font-mono font-sm>
24-
{{ session.meta.cwd }}
25-
</div>
26-
<div v-if="session.meta.inputs[0]" flex="~ gap-1 items-center">
27-
<DisplayModuleId :id="session.meta.inputs[0].filename" :cwd="session.meta.cwd" />
28-
<DisplayBadge :text="session.meta.inputs[0].name || 'entry'" />
29-
<span v-if="session.meta.inputs.length > 1" op50 text-xs border="~ base rounded-md" px1 font-mono>
30-
+{{ session.meta.inputs.length - 1 }}
31-
</span>
32-
</div>
33-
<DisplayTimestamp :timestamp="session.timestamp" pt2 text-sm op50 />
34-
</NuxtLink>
39+
<div v-for="session of sessions" :key="session.id" flex="~ row gap-2" relative>
40+
<component
41+
:is="sessionMode === 'list' ? NuxtLink : 'div'"
42+
:to="`/session/${session.id}`"
43+
border="~ rounded-md"
44+
:class="sessionMode === 'list' ? ['hover:bg-active', 'border-base'] : [selectedSessionIds.includes(session.id) ? 'border-active' : 'border-base', checkIsDifferentEntry(session) || (selectedSessions.length === 2 && !selectedSessionIds.includes(session.id)) ? 'op50' : 'hover:bg-active']"
45+
flex="~ col gap-1"
46+
px4 py3
47+
@click="select(session)"
48+
>
49+
<div flex="~ gap-1 items-center" font-mono op50 text-sm>
50+
<div i-ph-hash-duotone />
51+
{{ session.id }}
52+
</div>
53+
<div font-mono font-sm>
54+
{{ session.meta.cwd }}
55+
</div>
56+
<div v-if="session.meta.inputs[0]" flex="~ gap-1 items-center">
57+
<DisplayModuleId :id="session.meta.inputs[0].filename" :cwd="session.meta.cwd" />
58+
<DisplayBadge :text="session.meta.inputs[0].name || 'entry'" />
59+
<span v-if="session.meta.inputs.length > 1" op50 text-xs border="~ base rounded-md" px1 font-mono>
60+
+{{ session.meta.inputs.length - 1 }}
61+
</span>
62+
</div>
63+
<DisplayTimestamp :timestamp="session.timestamp" pt2 text-sm op50 />
64+
</component>
65+
</div>
3566
</div>
3667
</template>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<script setup lang="ts">
2+
import type { SessionCompareContext } from '~~/shared/types'
3+
import { useRoute } from '#app/composables/router'
4+
import { useRpc } from '#imports'
5+
import { computed, onMounted, ref } from 'vue'
6+
7+
const isLoading = ref(false)
8+
const params = useRoute().params as {
9+
sessions: string
10+
}
11+
12+
const rpc = useRpc()
13+
const sessions = ref<SessionCompareContext[]>([])
14+
15+
onMounted(async () => {
16+
isLoading.value = true
17+
18+
const summary = await rpc.value!['vite:rolldown:get-session-compare-summary']!({
19+
sessions: params.sessions.split(','),
20+
})
21+
22+
sessions.value = summary
23+
24+
isLoading.value = false
25+
})
26+
27+
const normalizedSessions = computed<Array<SessionCompareContext & { createdAt: Date, title: string }>>(() => {
28+
return sessions.value.map((session, index) => ({
29+
...session,
30+
// @ts-expect-error missing type
31+
createdAt: new Date(session.meta?.timestamp ?? 0),
32+
title: index === 0 ? 'Session A' : 'Session B',
33+
}))
34+
})
35+
36+
const comparisonMetrics = computed(() => {
37+
const [sessionA, sessionB] = normalizedSessions.value
38+
return [
39+
{
40+
name: 'Bundle Size',
41+
description: 'Total file size of the assets',
42+
icon: 'i-ph-package-duotone',
43+
current: sessionB?.bundle_size ?? 0,
44+
previous: sessionA?.bundle_size ?? 0,
45+
format: 'bytes',
46+
},
47+
{
48+
name: 'Initial JS',
49+
description: 'Total file size of the initial JS chunks',
50+
icon: 'i-ph:file-js-duotone',
51+
current: sessionB?.initial_js ?? 0,
52+
previous: sessionA?.initial_js ?? 0,
53+
format: 'bytes',
54+
},
55+
{
56+
name: 'Modules',
57+
description: 'Total number of modules',
58+
icon: 'i-ph-graph-duotone',
59+
current: sessionB?.modules ?? 0,
60+
previous: sessionA?.modules ?? 0,
61+
},
62+
{
63+
name: 'Plugins',
64+
description: 'Total number of plugins',
65+
icon: 'i-ph-plugs-duotone',
66+
current: sessionB?.meta?.plugins.length ?? 0,
67+
previous: sessionA?.meta?.plugins.length ?? 0,
68+
},
69+
{
70+
name: 'Chunks',
71+
description: 'Total number of chunks',
72+
icon: 'i-ph-shapes-duotone',
73+
current: sessionB?.chunks ?? 0,
74+
previous: sessionA?.chunks ?? 0,
75+
},
76+
{
77+
name: 'Assets',
78+
description: 'Total number of assets',
79+
icon: 'i-ph-package-duotone',
80+
current: sessionB?.assets ?? 0,
81+
previous: sessionA?.assets ?? 0,
82+
},
83+
]
84+
})
85+
</script>
86+
87+
<template>
88+
<VisualLoading v-if="isLoading" />
89+
<div v-else h-screen w-screen max-w-screen max-h-screen of-hidden p6 flex="~ col gap-2">
90+
<div flex="~ gap-2">
91+
<NuxtLink btn-action :to="{ path: `/` }">
92+
<div i-ph-arrow-bend-up-left-duotone />
93+
Re-select Session
94+
</NuxtLink>
95+
</div>
96+
<div flex="~ col" border="~ base rounded-lg" p3 mt10>
97+
<div py3 indent-2>
98+
Compare Overview
99+
</div>
100+
<!-- meta info -->
101+
<CompareSessionMeta :sessions="normalizedSessions" />
102+
<div grid="~ cols-4 gap5" w-full pt3>
103+
<div v-for="(item, index) of comparisonMetrics" :key="item.name" :class="index < 2 ? 'col-span-2' : 'col-span-1'" border="~ base rounded" p4 flex="~ col" gap2>
104+
<CompareMetricCard v-bind="item" />
105+
</div>
106+
</div>
107+
</div>
108+
</div>
109+
</template>
Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,81 @@
1+
<script setup lang="ts">
2+
import type { BuildInfo } from '~~/node/rolldown/logs-manager'
3+
import { useRpc } from '#imports'
4+
import { computed, ref } from 'vue'
5+
6+
const sessionMode = ref<'list' | 'compare'>('list')
7+
8+
const modeList = [
9+
{
10+
label: 'Session List',
11+
icon: 'i-carbon-list',
12+
value: 'list',
13+
},
14+
{
15+
label: 'Session Compare',
16+
icon: 'i-carbon-compare',
17+
value: 'compare',
18+
},
19+
] as const
20+
21+
const selectedSessions = ref<BuildInfo[]>([])
22+
const selectedSessionIds = computed(() => {
23+
return selectedSessions.value.map(session => session.id).sort()
24+
})
25+
const normalizedSelectedSessions = computed(() => {
26+
const sortedSessions = [...selectedSessions.value].sort((a, b) => a.timestamp - b.timestamp)
27+
return sortedSessions.map((session, index) => ({
28+
...session,
29+
createdAt: new Date(session.timestamp),
30+
title: index === 0 ? 'Session A' : 'Session B',
31+
}))
32+
})
33+
34+
const rpc = useRpc()
35+
const sessions = await rpc.value!['vite:rolldown:list-sessions']()
36+
37+
function selectSession(session: BuildInfo) {
38+
if (selectedSessionIds.value.includes(session.id)) {
39+
selectedSessions.value = selectedSessions.value.filter(s => s.id !== session.id)
40+
}
41+
else {
42+
selectedSessions.value = [...selectedSessions.value, session]
43+
}
44+
}
45+
</script>
46+
147
<template>
2-
<div p4 flex="~ col gap-4" items-center justify-center>
48+
<div p4 flex="~ col gap-4" items-center justify-center relative>
349
<VisualLogoBanner />
450
<p op50>
5-
Select a build session to get started:
51+
{{ sessionMode === 'list' ? 'Select a build session to get started:' : 'Select 2 build sessions to compare:' }}
652
</p>
7-
<PanelSessionSelector />
53+
<div relative flex="~ col gap3 items-center">
54+
<PanelSessionSelector
55+
:session-mode="sessionMode"
56+
:sessions="sessions"
57+
:selected-session-ids="selectedSessionIds"
58+
:selected-sessions="selectedSessions"
59+
@select="selectSession"
60+
/>
61+
</div>
62+
<div fixed top-5 right-5 flex="~ col gap2">
63+
<div flex="~ row justify-around" w20 h8 border="~ base rounded-8" of-hidden>
64+
<button v-for="mode in modeList" :key="mode.value" :title="mode.label" flex-1 op50 flex="~ items-center justify-center" :class="{ 'bg-active text-base op100!': sessionMode === mode.value }" hover="bg-active text-base op100!" @click="sessionMode = mode.value">
65+
<span :class="mode.icon" class="text-sm" />
66+
</button>
67+
</div>
68+
</div>
69+
<div v-if="selectedSessions.length > 0 && sessionMode === 'compare'" fixed bottom-5 right-5 border="~ base rounded-2" w100 max-lg:w85 bg-glass z-panel-content>
70+
<CompareSessionMeta :sessions="normalizedSelectedSessions" class="flex-col gap0 [&>div]:border-none! [&>first-child]:border-b!" />
71+
<div flex="~ justify-center" p2>
72+
<NuxtLink v-if="selectedSessions.length === 2" tag="button" :to="`/compare/${selectedSessionIds.join(',')}`" btn-action rounded-8 text-3 flex="~ justify-center" w30 h8>
73+
Compare
74+
</NuxtLink>
75+
<div v-else op80 text-sm>
76+
Select one more session to compare.
77+
</div>
78+
</div>
79+
</div>
880
</div>
981
</template>

0 commit comments

Comments
 (0)