diff --git a/packages/rolldown/src/app/components/compare/SingleSideRow.vue b/packages/rolldown/src/app/components/compare/SingleSideRow.vue
index ad018de7f..7f26db27b 100644
--- a/packages/rolldown/src/app/components/compare/SingleSideRow.vue
+++ b/packages/rolldown/src/app/components/compare/SingleSideRow.vue
@@ -14,6 +14,7 @@ withDefaults(defineProps<{
delta: number
format: 'bytes' | 'duration' | 'number'
subtitle?: string
+ link?: string
badges?: string[]
stats?: SingleSideDiffStat[]
}>(), {
@@ -32,7 +33,10 @@ withDefaults(defineProps<{
{{ sessionLabel }}
- {{ title }}
+
+ {{ title }}
+
+ {{ title }}
({{ titleMeta }})
diff --git a/packages/rolldown/src/app/components/compare/SplitRow.vue b/packages/rolldown/src/app/components/compare/SplitRow.vue
index b22b61825..af92c306f 100644
--- a/packages/rolldown/src/app/components/compare/SplitRow.vue
+++ b/packages/rolldown/src/app/components/compare/SplitRow.vue
@@ -16,6 +16,8 @@ const props = withDefaults(defineProps<{
currentTitleMeta?: string
previousSubtitle?: string
currentSubtitle?: string
+ previousLink?: string
+ currentLink?: string
previousStats?: SplitDiffStat[]
currentStats?: SplitDiffStat[]
previousBadges?: string[]
@@ -62,7 +64,10 @@ const ratioClass = computed(() => {
Session A
- {{ previousTitle }}
+
+ {{ previousTitle }}
+
+ {{ previousTitle }}
({{ previousTitleMeta }})
@@ -92,7 +97,10 @@ const ratioClass = computed(() => {
Session B
- {{ currentTitle }}
+
+ {{ currentTitle }}
+
+ {{ currentTitle }}
({{ currentTitleMeta }})
diff --git a/packages/rolldown/src/app/components/compare/TabView.vue b/packages/rolldown/src/app/components/compare/TabView.vue
index d45ce450f..a36f80ee5 100644
--- a/packages/rolldown/src/app/components/compare/TabView.vue
+++ b/packages/rolldown/src/app/components/compare/TabView.vue
@@ -7,8 +7,9 @@ import type {
} from '~~/shared/types'
import { bytesToHumanSize } from '@vitejs/devtools-ui/utils/format'
import { computed, ref, watch } from 'vue'
+import { useRoute, useRouter } from '#app/composables/router'
-type CompareTab = 'overview' | 'plugins' | 'chunks' | 'assets' | 'packages'
+type CompareTab = 'overview' | 'plugins' | 'chunks' | 'modules' | 'assets' | 'packages'
type DiffMode = 'changed' | 'added' | 'removed'
type DiffFormat = 'bytes' | 'duration' | 'number'
type SummaryTone = 'increase' | 'decrease'
@@ -37,6 +38,8 @@ interface SplitDiffRow extends SessionCompareMetricValue {
currentTitleMeta?: string
previousSubtitle?: string
currentSubtitle?: string
+ previousLink?: string
+ currentLink?: string
previousStats?: SplitDiffStat[]
currentStats?: SplitDiffStat[]
previousBadges: string[]
@@ -54,7 +57,13 @@ const props = defineProps<{
const TABLE_LIMIT = 300
+const route = useRoute()
+const router = useRouter()
+
const activeDiffMode = ref
('changed')
+const moduleSearch = ref('')
+const moduleChunkFilter = computed(() => typeof route.query.chunk === 'string' ? route.query.chunk : undefined)
+const moduleChunkFilterName = computed(() => props.details?.chunks.find(chunk => chunk.key === moduleChunkFilter.value)?.name ?? moduleChunkFilter.value)
const activeTabMeta = computed(() => props.tabs.find(tab => tab.id === props.tab))
const comparisonMetrics = computed(() => {
@@ -167,6 +176,26 @@ const sessionSummaries = computed(() => {
tone: getSummaryTone(index, session.initial_js, sessionA?.initial_js),
},
])
+ case 'modules': {
+ return props.sessions.map((session, index) => {
+ const codeSize = sumCompareSide(props.details?.modules, index, item => item.previous, item => item.current)
+ const codeSizeA = sumCompareSide(props.details?.modules, 0, item => item.previous, item => item.current)
+ return [
+ {
+ label: 'Modules',
+ value: session.modules.toLocaleString(),
+ icon: 'i-ph-graph-duotone',
+ tone: getSummaryTone(index, session.modules, sessionA?.modules),
+ },
+ {
+ label: 'Code Size',
+ value: formatBytesSummary(codeSize),
+ icon: 'i-ph-file-code-duotone',
+ tone: getSummaryTone(index, codeSize, codeSizeA),
+ },
+ ]
+ })
+ }
case 'packages': {
const packageStatsA = getPackageSummaryStats(0)
return props.sessions.map((_session, index) => {
@@ -226,6 +255,17 @@ const sessionSummaries = computed(() => {
const assetDiffs = computed(() => changedEntries(props.details?.assets))
const chunkDiffs = computed(() => changedEntries(props.details?.chunks))
+const moduleDiffs = computed(() => {
+ const chunk = moduleChunkFilter.value
+ const search = moduleSearch.value.trim().toLowerCase()
+ return changedEntries(props.details?.modules).filter((item) => {
+ if (chunk && !item.previousChunkKeys.includes(chunk) && !item.currentChunkKeys.includes(chunk))
+ return false
+ if (search && !item.name.toLowerCase().includes(search) && !item.package?.toLowerCase().includes(search))
+ return false
+ return true
+ })
+})
const packageDiffs = computed(() => changedEntries(props.details?.packages))
const pluginDiffs = computed(() => changedEntries(props.details?.plugins))
@@ -250,6 +290,8 @@ const chunkRows = computed(() => chunkDiffs.value.map(item => cr
status: item.status,
previousTitle: item.previousChunkId == null ? undefined : item.name,
currentTitle: item.currentChunkId == null ? undefined : item.name,
+ previousLink: item.previousChunkId == null ? undefined : getChunkModulesLink(item.key),
+ currentLink: item.currentChunkId == null ? undefined : getChunkModulesLink(item.key),
previousStats: createChunkStats(item.previousModules, item.previousImports),
currentStats: createChunkStats(item.currentModules, item.currentImports, {
modules: item.previousModules,
@@ -270,6 +312,29 @@ const chunkRows = computed(() => chunkDiffs.value.map(item => cr
].filter(Boolean) as string[],
})))
+const moduleRows = computed(() => {
+ const [sessionA, sessionB] = props.sessions
+ return moduleDiffs.value.map(item => createSplitRow({
+ key: item.key,
+ status: item.status,
+ previousTitle: item.previousId == null ? undefined : item.name,
+ currentTitle: item.currentId == null ? undefined : item.name,
+ previousSubtitle: formatModuleChunks(item.previousChunks),
+ currentSubtitle: formatModuleChunks(item.currentChunks),
+ previousLink: item.previousId == null || !sessionA ? undefined : getModuleLink(sessionA.id, item.previousId),
+ currentLink: item.currentId == null || !sessionB ? undefined : getModuleLink(sessionB.id, item.currentId),
+ previous: item.previous,
+ current: item.current,
+ delta: item.delta,
+ deltaRatio: item.deltaRatio,
+ format: 'bytes',
+ previousBadges: item.package ? [item.package] : [],
+ currentBadges: item.moved
+ ? [item.package, 'moved'].filter(Boolean) as string[]
+ : item.package ? [item.package] : [],
+ }))
+})
+
const packageRows = computed(() => packageDiffs.value.map(item => createSplitRow({
key: item.key,
status: item.status,
@@ -322,6 +387,8 @@ const activeRows = computed(() => {
return assetRows.value
case 'chunks':
return chunkRows.value
+ case 'modules':
+ return moduleRows.value
case 'packages':
return packageRows.value
case 'plugins':
@@ -420,6 +487,24 @@ function formatAssetTitleMeta(chunkName: string | undefined) {
return chunkName?.replace(/[[\]]/g, '')
}
+function formatModuleChunks(chunks: string[]) {
+ if (!chunks.length)
+ return undefined
+ return `in ${chunks.map(chunk => chunk.replace(/[[\]]/g, '')).join(', ')}`
+}
+
+function getChunkModulesLink(chunkKey: string) {
+ return `/compare/${props.sessions.map(session => session.id).join(',')}/modules?chunk=${encodeURIComponent(chunkKey)}`
+}
+
+function clearModuleChunkFilter() {
+ router.replace({ query: { ...route.query, chunk: undefined } })
+}
+
+function getModuleLink(session: string, module: string) {
+ return `/session/${session}/graph?module=${encodeURIComponent(module)}`
+}
+
function formatPackageTitle(name: string, version: string | undefined) {
if (!version || version === '(unknown)')
return name
@@ -586,6 +671,28 @@ function sumDelta(rows: SplitDiffRow[]) {
+
+
+
+
+