Skip to content

Commit c4aeba9

Browse files
committed
feat: module detailed list
1 parent 5d791a4 commit c4aeba9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3193
-1479
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"lint": "pnpm -C packages/devtools run dev:prepare && eslint .",
1414
"test": "vitest",
1515
"release": "bumpp -r && pnpm publish -r",
16-
"typecheck": "vue-tsc --noEmit"
16+
"typecheck": "vue-tsc -b --noEmit"
1717
},
1818
"devDependencies": {
1919
"@antfu/eslint-config": "catalog:devtools",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<script setup lang="ts">
2+
import type { Asset as AssetInfo } from '@rolldown/debug'
3+
import type { ModuleDest, SessionContext } from '~~/shared/types'
4+
import { computed } from 'vue'
5+
import { toTree } from '../../utils/format'
6+
7+
const props = defineProps<{
8+
assets: AssetInfo[]
9+
session: SessionContext
10+
}>()
11+
const assetTree = computed(() => {
12+
const nodes: ModuleDest[] = []
13+
props.assets.forEach((i) => {
14+
nodes.push({
15+
full: i.filename,
16+
path: i.filename,
17+
})
18+
})
19+
return toTree(nodes, 'Project')
20+
})
21+
</script>
22+
23+
<template>
24+
<div flex="~ gap-2">
25+
<DisplayTreeNode
26+
v-if="assets?.length"
27+
flex-1
28+
:node="assetTree"
29+
icon="i-catppuccin:folder-dist catppuccin"
30+
icon-open="i-catppuccin:folder-dist-open catppuccin"
31+
:link="true"
32+
link-query-key="asset"
33+
/>
34+
</div>
35+
</template>
36+
37+
<style scoped>
38+
39+
</style>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script setup lang="ts">
2+
import type { Asset as AssetInfo } from '@rolldown/debug'
3+
4+
defineProps<{
5+
assets: AssetInfo[]
6+
}>()
7+
</script>
8+
9+
<template>
10+
<div p4>
11+
<div flex="~ col gap-1">
12+
<template v-for="asset in assets" :key="asset.id">
13+
<NuxtLink
14+
:to="{ query: { asset: asset.filename } }"
15+
font-mono border="~ rounded base" px2 py1 text-sm hover="bg-active"
16+
>
17+
<div flex="~ gap-1">
18+
<DisplayFileIcon :filename="asset.filename" />
19+
{{ asset.filename }}
20+
</div>
21+
</NuxtLink>
22+
</template>
23+
</div>
24+
</div>
25+
</template>
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
<script setup lang="ts">
2+
import type { Asset as AssetInfo } from '@rolldown/debug'
3+
import type { GraphBase, GraphBaseOptions } from 'nanovis'
4+
import type { SessionContext } from '~~/shared/types'
5+
import type { AssetChartInfo, AssetChartNode } from '~/types/assets'
6+
import { useRouter } from '#app/composables/router'
7+
import { useMouse } from '@vueuse/core'
8+
import { createColorGetterSpectrum, Treemap } from 'nanovis'
9+
import { computed, nextTick, onUnmounted, reactive, ref, shallowRef, watch } from 'vue'
10+
import { isDark } from '~/composables/dark'
11+
import { settings } from '~/state/settings'
12+
import { bytesToHumanSize } from '~/utils/format'
13+
14+
const props = defineProps<{
15+
assets: AssetInfo[]
16+
session: SessionContext
17+
}>()
18+
19+
const router = useRouter()
20+
const mouse = reactive(useMouse())
21+
const graph = shallowRef<GraphBase<AssetChartInfo | undefined, GraphBaseOptions<AssetChartInfo | undefined>> | undefined>(undefined)
22+
const nodeHover = shallowRef<AssetChartNode | undefined>(undefined)
23+
const nodeSelected = shallowRef<AssetChartNode | undefined>(undefined)
24+
const selectedNode = ref<AssetChartInfo | undefined>(undefined)
25+
let dispose: () => void | undefined
26+
27+
const tree = computed(() => {
28+
const assets = props.assets
29+
const map = new Map<string, AssetChartNode>()
30+
let maxDepth = 0
31+
32+
const root: AssetChartNode = {
33+
id: '~root',
34+
text: 'Project',
35+
size: 0,
36+
sizeSelf: 0,
37+
children: [],
38+
}
39+
40+
const macrosTasks: (() => void)[] = []
41+
42+
macrosTasks.unshift(() => {
43+
root.size += root.children.reduce((acc, i) => acc + i.size, 0)
44+
root.subtext = bytesToHumanSize(root.size).join(' ')
45+
root.children.sort((a, b) => b.size - a.size || a.id.localeCompare(b.id))
46+
})
47+
48+
function assetToNode(asset: AssetInfo, path: string, name: string, parent: AssetChartNode, depth: number): AssetChartNode {
49+
if (map.has(path)) {
50+
return map.get(path)!
51+
}
52+
53+
if (depth > maxDepth) {
54+
maxDepth = depth
55+
}
56+
57+
const node: AssetChartNode = {
58+
id: path,
59+
text: name,
60+
size: 0,
61+
sizeSelf: 0,
62+
children: [],
63+
meta: {
64+
chunk_id: 0,
65+
content: '',
66+
filename: '',
67+
size: 0,
68+
path: name,
69+
type: 'folder',
70+
},
71+
parent,
72+
}
73+
74+
map.set(path, node)
75+
parent.children.push(node)
76+
77+
macrosTasks.unshift(() => {
78+
const selfSize = node.sizeSelf
79+
node.size += node.children.reduce((acc, i) => acc + i.size, 0)
80+
node.subtext = bytesToHumanSize(node.size).join(' ')
81+
82+
if (node.children.length && selfSize / node.size > 0.1) {
83+
node.children.push({
84+
id: `${node.id}-self`,
85+
text: '',
86+
size: selfSize,
87+
sizeSelf: selfSize,
88+
subtext: bytesToHumanSize(selfSize).join(' '),
89+
children: [],
90+
meta: {
91+
...asset,
92+
path: '',
93+
type: 'file',
94+
},
95+
parent: node,
96+
})
97+
}
98+
99+
node.children.sort((a, b) => b.size - a.size || a.id.localeCompare(b.id))
100+
})
101+
102+
return node
103+
}
104+
105+
function processAsset(asset: AssetInfo) {
106+
const parts = asset.filename.split('/').filter(Boolean)
107+
let current = root
108+
let currentPath = ''
109+
let depth = 0
110+
111+
parts.forEach((part, index) => {
112+
currentPath += (currentPath ? '/' : '') + part
113+
depth++
114+
115+
if (index === parts.length - 1) {
116+
const fileNode: AssetChartNode = {
117+
id: asset.filename,
118+
text: part,
119+
size: asset.size,
120+
sizeSelf: asset.size,
121+
subtext: bytesToHumanSize(asset.size).join(' '),
122+
children: [],
123+
meta: {
124+
...asset,
125+
path: part,
126+
type: 'file',
127+
},
128+
}
129+
130+
current.children.push(fileNode)
131+
map.set(asset.filename, fileNode)
132+
}
133+
else {
134+
current = assetToNode(asset, currentPath, part, current, depth)
135+
}
136+
})
137+
}
138+
139+
assets.forEach(processAsset)
140+
141+
macrosTasks.forEach(fn => fn())
142+
143+
return {
144+
map,
145+
root,
146+
maxDepth,
147+
}
148+
})
149+
150+
const options = computed<GraphBaseOptions<AssetChartInfo | undefined>>(() => {
151+
return {
152+
onClick(node) {
153+
if (node)
154+
nodeHover.value = node
155+
if (node.meta?.type === 'file') {
156+
selectedNode.value = node.meta
157+
router.replace({ query: { asset: node.meta.filename } })
158+
}
159+
},
160+
onHover(node) {
161+
if (node)
162+
nodeHover.value = node
163+
},
164+
onLeave() {
165+
nodeHover.value = undefined
166+
},
167+
onSelect(node) {
168+
nodeSelected.value = node || tree.value.root
169+
selectedNode.value = node?.meta
170+
},
171+
animate: settings.value.chartAnimation,
172+
palette: {
173+
stroke: isDark.value ? '#222' : '#555',
174+
fg: isDark.value ? '#fff' : '#000',
175+
bg: isDark.value ? '#111' : '#fff',
176+
},
177+
getColor: createColorGetterSpectrum(
178+
tree.value.root,
179+
isDark.value ? 0.8 : 0.9,
180+
isDark.value ? 1 : 1.1,
181+
),
182+
getSubtext: (node) => {
183+
return node.subtext
184+
},
185+
}
186+
})
187+
188+
function selectNode(node: AssetChartNode | null, animate?: boolean) {
189+
selectedNode.value = node?.meta
190+
if (!node?.children.length)
191+
node = node?.parent || null
192+
graph.value?.select(node, animate)
193+
}
194+
195+
watch(() => [tree.value, options.value], () => {
196+
dispose?.()
197+
198+
nodeSelected.value = tree.value.root
199+
200+
if (tree.value?.root) {
201+
graph.value = new Treemap(tree.value.root, {
202+
...options.value,
203+
selectedPaddingRatio: 0,
204+
})
205+
}
206+
nextTick(() => {
207+
const selected = selectedNode.value ? tree.value.map.get(selectedNode.value.filename) || null : null
208+
if (selected)
209+
selectNode(selected, false)
210+
})
211+
212+
dispose = () => {
213+
graph.value?.dispose()
214+
graph.value = undefined
215+
}
216+
}, {
217+
deep: true,
218+
immediate: true,
219+
})
220+
221+
onUnmounted(() => {
222+
dispose?.()
223+
})
224+
</script>
225+
226+
<template>
227+
<ChartAssetTreemap
228+
v-if="graph"
229+
:graph="graph"
230+
:selected="nodeSelected"
231+
@select="x => selectNode(x)"
232+
/>
233+
<div
234+
v-if="nodeHover?.meta"
235+
bg-glass fixed z-panel-nav border="~ base rounded" p2 text-sm
236+
flex="~ col gap-2"
237+
:style="{
238+
left: `${mouse.x + 10}px`,
239+
top: `${mouse.y + 10}px`,
240+
}"
241+
>
242+
<div flex="~ gap-1 items-center">
243+
{{ nodeHover.text }}
244+
</div>
245+
<div flex="~ gap-1 items-center">
246+
<DisplayFileSizeBadge :bytes="nodeHover.size" :percent="false" />
247+
</div>
248+
</div>
249+
</template>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script setup lang="ts">
2+
import type { GraphBaseOptions } from 'nanovis'
3+
import type { AssetChartInfo, AssetChartNode } from '~/types/assets'
4+
import { computed } from 'vue'
5+
6+
const props = defineProps<{
7+
selected?: AssetChartNode
8+
options: GraphBaseOptions<AssetChartInfo | undefined>
9+
}>()
10+
11+
const emit = defineEmits<{
12+
(e: 'select', node: AssetChartNode | null): void
13+
}>()
14+
15+
const parentStack = computed(() => {
16+
const stack: AssetChartNode[] = []
17+
let current = props.selected
18+
while (current) {
19+
stack.unshift(current)
20+
current = current.parent
21+
}
22+
return stack
23+
})
24+
</script>
25+
26+
<template>
27+
<div flex="~ gap-1 items-center wrap">
28+
<template v-for="node, idx of parentStack" :key="node.id">
29+
<div v-if="idx > 0" i-ph-arrow-right-bold text-sm op-fade />
30+
<button
31+
hover="bg-active" rounded px1
32+
@click="emit('select', node)"
33+
>
34+
<span>{{ node.text || node.id }}</span>
35+
</button>
36+
</template>
37+
</div>
38+
</template>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script setup lang="ts">
2+
import type { GraphBase, GraphBaseOptions } from 'nanovis'
3+
import type { AssetChartInfo, AssetChartNode } from '~/types/assets'
4+
import { useTemplateRef, watchEffect } from 'vue'
5+
6+
const props = defineProps<{
7+
graph: GraphBase<AssetChartInfo | undefined, GraphBaseOptions<AssetChartInfo | undefined>>
8+
selected?: AssetChartNode | undefined
9+
}>()
10+
11+
const emit = defineEmits<{
12+
(e: 'select', node: AssetChartNode | null): void
13+
}>()
14+
15+
const el = useTemplateRef<HTMLDivElement>('el')
16+
watchEffect(() => el.value?.append(props.graph.el))
17+
</script>
18+
19+
<template>
20+
<ChartAssetNavBreadcrumb
21+
border="b base" py2 min-h-10
22+
:selected="selected"
23+
:options="graph.options"
24+
@select="emit('select', $event)"
25+
/>
26+
<div ref="el" />
27+
</template>

0 commit comments

Comments
 (0)