Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ useHeadSafe({
<div min-h-0 flex flex-1 flex-col lg:flex-row>
<InputContainer min-w-0 flex-1 />
<div class="panel-divider" />
<OutputContainer min-w-0 flex-1 />
<OutputView min-w-0 flex-1 />
</div>
</main>

Expand Down
223 changes: 223 additions & 0 deletions app/components/GraphContainer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
<script setup lang="ts">
import {
generateChunkGraphMermaid,
generateModuleGraphMermaid,
} from '~/composables/graph-mermaid'
import type { ChunkNode, ModuleNode } from '~/composables/bundler'

const props = defineProps<{
moduleGraph?: ModuleNode[]
chunkGraph?: ChunkNode[]
isLoading?: boolean
}>()

const activeTab = ref<'module' | 'chunk'>('module')
const { mermaidLoaded, mermaidError, renderDiagram } = useMermaid()
const moduleGraphSvg = ref<string>('')
const chunkGraphSvg = ref<string>('')
const renderError = ref<string | null>(null)

// Compute graph statistics
const stats = computed(() => {
return {
modules: props.moduleGraph?.length || 0,
chunks: props.chunkGraph?.length || 0,
entries: props.moduleGraph?.filter((m) => m.isEntry).length || 0,
}
})

// Render module graph when data changes
watch(
() => props.moduleGraph,
async (modules) => {
if (!modules || modules.length === 0) {
moduleGraphSvg.value = ''
return
}

const definition = generateModuleGraphMermaid(modules)
const svg = await renderDiagram(definition, 'module-graph')
if (svg) {
moduleGraphSvg.value = svg
renderError.value = null
} else {
renderError.value = 'Failed to render module graph'
}
},
{ immediate: true },
)

// Render chunk graph when data changes
watch(
() => props.chunkGraph,
async (chunks) => {
if (!chunks || chunks.length === 0) {
chunkGraphSvg.value = ''
return
}

const definition = generateChunkGraphMermaid(chunks)
const svg = await renderDiagram(definition, 'chunk-graph')
if (svg) {
chunkGraphSvg.value = svg
renderError.value = null
} else {
renderError.value = 'Failed to render chunk graph'
}
},
{ immediate: true },
)
</script>

<template>
<div h-full flex flex-col>
<div flex items-center border-b border-base>
<button
class="graph-tab"
:class="activeTab === 'module' && 'tab-active'"
@click="activeTab = 'module'"
>
Module Graph
</button>
<button
class="graph-tab"
:class="activeTab === 'chunk' && 'tab-active'"
@click="activeTab = 'chunk'"
>
Chunk Graph
</button>
</div>

<div min-h-0 w-full flex-1 overflow-auto p4>
<Loading v-if="props.isLoading" text="Loading graph..." />

<!-- Error Message -->
<div v-else-if="mermaidError || renderError" class="error-state">
<div i-ph:warning text-6xl text-red op60 />
<div mt4 text-secondary>
{{ mermaidError || renderError }}
</div>
</div>

<!-- Module Graph View -->
<div
v-else-if="activeTab === 'module'"
class="graph-container"
flex="~ col"
items-center
>
<div v-if="stats.modules > 0" mb4 w-full text-sm text-secondary>
<div>{{ stats.modules }} modules, {{ stats.entries }} entries</div>
</div>

<div
v-if="moduleGraphSvg"
class="mermaid-diagram"
v-html="moduleGraphSvg"
/>
<div v-else-if="!mermaidLoaded" class="empty-state">
<div i-ph:graph text-6xl text-secondary op40 />
<div mt4 text-secondary>Loading Mermaid...</div>
</div>
<div v-else class="empty-state">
<div i-ph:graph text-6xl text-secondary op40 />
<div mt4 text-secondary>No module graph data available</div>
</div>
</div>

<!-- Chunk Graph View -->
<div
v-else-if="activeTab === 'chunk'"
class="graph-container"
flex="~ col"
items-center
>
<div v-if="stats.chunks > 0" mb4 w-full text-sm text-secondary>
<div>{{ stats.chunks }} chunks</div>
</div>

<div
v-if="chunkGraphSvg"
class="mermaid-diagram"
v-html="chunkGraphSvg"
/>
<div v-else-if="!mermaidLoaded" class="empty-state">
<div i-ph:graph text-6xl text-secondary op40 />
<div mt4 text-secondary>Loading Mermaid...</div>
</div>
<div v-else class="empty-state">
<div i-ph:graph text-6xl text-secondary op40 />
<div mt4 text-secondary>No chunk graph data available</div>
</div>
</div>
</div>
</div>
</template>

<style scoped>
.graph-tab {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
cursor: pointer;
white-space: nowrap;
font-size: 13px;
color: var(--c-text-secondary);
border-bottom: 2px solid transparent;
transition:
color var(--transition-fast),
background var(--transition-fast),
border-color var(--transition-fast);
}

.graph-tab:hover {
color: var(--c-text-base);
background: var(--c-bg-mute);
}

.graph-tab.tab-active {
color: var(--c-accent);
border-bottom-color: var(--c-accent);
}

.graph-container {
width: 100%;
max-width: 100%;
}

.mermaid-diagram {
width: 100%;
display: flex;
justify-content: center;
overflow: auto;
}

.mermaid-diagram :deep(svg) {
max-width: 100%;
height: auto;
}

.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 200px;
}

.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 200px;
color: #dc2626;
}

:global(.dark) .error-state {
color: #f87171;
}
</style>
12 changes: 12 additions & 0 deletions app/components/OutputContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
timeCost,
} from '~/state/bundler'
import { npmVfsFiles, userDependencies } from '~/state/npm'
import { bundlerError, bundlerOutput, bundlerStatus } from '~/state/output'

const { data: rolldownVersions } = await useRolldownVersions()

Expand Down Expand Up @@ -145,6 +146,17 @@ watch(

watch([files, currentVersion], () => refresh(), { deep: true })

// Sync with shared state
watch(data, (newData) => {
bundlerOutput.value = newData
})
watch(status, (newStatus) => {
bundlerStatus.value = newStatus
})
watch(error, (newError) => {
bundlerError.value = newError
})

const isLoading = computed(() => status.value === 'pending')
const isLoadingDebounced = useDebounce(isLoading, 100)

Expand Down
67 changes: 67 additions & 0 deletions app/components/OutputView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script setup lang="ts">
import { bundlerOutput, bundlerStatus } from '~/state/output'

const activeTab = ref<'output' | 'graph'>('output')

const isLoading = computed(() => bundlerStatus.value === 'pending')
</script>

<template>
<div h-full flex flex-col>
<div flex items-center border-b border-base>
<button
class="view-tab"
:class="activeTab === 'output' && 'tab-active'"
@click="activeTab = 'output'"
>
Output
</button>
<button
class="view-tab"
:class="activeTab === 'graph' && 'tab-active'"
@click="activeTab = 'graph'"
>
Graph
</button>
</div>

<div min-h-0 flex-1>
<OutputContainer v-show="activeTab === 'output'" h-full />
<GraphContainer
v-show="activeTab === 'graph'"
:module-graph="bundlerOutput?.moduleGraph"
:chunk-graph="bundlerOutput?.chunkGraph"
:is-loading="isLoading"
h-full
/>
</div>
</div>
</template>

<style scoped>
.view-tab {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
cursor: pointer;
white-space: nowrap;
font-size: 13px;
color: var(--c-text-secondary);
border-bottom: 2px solid transparent;
transition:
color var(--transition-fast),
background var(--transition-fast),
border-color var(--transition-fast);
}

.view-tab:hover {
color: var(--c-text-base);
background: var(--c-bg-mute);
}

.view-tab.tab-active {
color: var(--c-accent);
border-bottom-color: var(--c-accent);
}
</style>
Loading