Skip to content

Commit 1155b52

Browse files
authored
feat: folder view mode for module graph (#10)
1 parent 61ed00c commit 1155b52

File tree

9 files changed

+255
-9
lines changed

9 files changed

+255
-9
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@antfu/utils": "catalog:inlined",
2121
"@iconify-json/carbon": "catalog:icons",
2222
"@iconify-json/catppuccin": "catalog:icons",
23+
"@iconify-json/codicon": "catalog:icons",
2324
"@iconify-json/logos": "catalog:icons",
2425
"@iconify-json/ph": "catalog:icons",
2526
"@iconify-json/ri": "catalog:icons",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<script setup lang="ts">
2+
import type { ModuleDest, ModuleTreeNode } from '~~/shared/types'
3+
import { useRoute } from '#app/composables/router'
4+
import { NuxtLink } from '#components'
5+
6+
const props = withDefaults(defineProps<{
7+
node: ModuleTreeNode
8+
icon?: string
9+
link?: string | boolean
10+
}>(), {
11+
icon: 'i-carbon-folder',
12+
})
13+
14+
const emit = defineEmits<{
15+
(e: 'select', node: ModuleDest): void
16+
}>()
17+
const route = useRoute()
18+
const location = window.location
19+
function select(node: ModuleDest) {
20+
if (!props.link) {
21+
emit('select', node)
22+
}
23+
}
24+
</script>
25+
26+
<template>
27+
<details open>
28+
<summary
29+
cursor-default
30+
select-none
31+
text-sm
32+
truncate
33+
p="y1"
34+
>
35+
<div :class="icon" inline-block vertical-text-bottom />
36+
{{ node.name }}
37+
</summary>
38+
39+
<DisplayTreeNode v-for="e of Object.entries(node.children)" :key="e[0]" ml4 :node="e[1]" :link="link" />
40+
<div
41+
v-for="i of node.items"
42+
:key="i.full"
43+
ml4
44+
ws-nowrap
45+
>
46+
<component
47+
:is="link ? NuxtLink : 'div'"
48+
:to="link ? (typeof link === 'string' ? link : { path: route.path, query: { ...route.query, module: i.full }, hash: location.hash }) : undefined"
49+
block
50+
text-sm
51+
p="x2 y1"
52+
ml1
53+
rounded
54+
@click="select(i)"
55+
>
56+
<DisplayFileIcon :filename="i.full" inline-block vertical-text-bottom />
57+
<span ml-1>
58+
{{ i.path.split('/').pop() }}
59+
</span>
60+
</component>
61+
</div>
62+
</details>
63+
</template>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<script setup lang="ts">
2+
import type { ModuleDest, ModuleListItem, SessionContext } from '~~/shared/types'
3+
import { computed } from 'vue'
4+
import { toTree } from '../../utils/format'
5+
6+
const props = defineProps<{
7+
session: SessionContext
8+
modules: ModuleListItem[]
9+
}>()
10+
11+
const moduleTree = computed(() => {
12+
if (!props.session.modulesList.length) {
13+
return {
14+
workspace: {
15+
children: {},
16+
items: [],
17+
},
18+
nodeModules: {
19+
children: {},
20+
items: [],
21+
},
22+
virtual: {
23+
children: {},
24+
items: [],
25+
},
26+
}
27+
}
28+
const inWorkspace: ModuleDest[] = []
29+
const inNodeModules: ModuleDest[] = []
30+
const inVirtual: ModuleDest[] = []
31+
32+
props.modules.map(i => ({ full: i.id, path: i.path! })).forEach((i) => {
33+
if (i.full.startsWith(props.session.meta.cwd)) {
34+
if (!i.path.startsWith('../')) {
35+
i.path = i.full.slice(props.session.meta.cwd.length + 1)
36+
}
37+
38+
inWorkspace.push(i)
39+
}
40+
else if (i.full.includes('node_modules')) {
41+
inNodeModules.push({
42+
full: i.full,
43+
path: i.full,
44+
})
45+
}
46+
else if (i.full.startsWith('virtual:')) {
47+
inVirtual.push(i)
48+
}
49+
})
50+
51+
return {
52+
workspace: toTree(inWorkspace, 'Project Root'),
53+
nodeModules: toTree(inNodeModules, 'Node Modules'),
54+
virtual: toTree(inVirtual, 'Virtual Modules'),
55+
}
56+
})
57+
</script>
58+
59+
<template>
60+
<div of-scroll max-h-screen pt-40 relative>
61+
<div flex="~ col gap-2" p4>
62+
<DisplayTreeNode
63+
v-if="Object.keys(moduleTree.workspace.children).length"
64+
:node="moduleTree.workspace"
65+
p="l3"
66+
icon="i-carbon-portfolio"
67+
:link="true"
68+
/>
69+
<DisplayTreeNode
70+
v-if="Object.keys(moduleTree.nodeModules.children).length"
71+
:node="moduleTree.nodeModules"
72+
p="l3"
73+
icon="i-carbon-categories"
74+
:link="true"
75+
/>
76+
<DisplayTreeNode
77+
v-if="Object.keys(moduleTree.virtual.children).length"
78+
:node="moduleTree.virtual"
79+
p="l3"
80+
icon="i-codicon:file-symlink-directory"
81+
:link="true"
82+
/>
83+
</div>
84+
</div>
85+
</template>

packages/devtools/src/app/pages/session/[session]/graph/index.vue

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { SessionContext } from '~~/shared/types'
3+
import type { ClientSettings } from '~/state/settings'
34
import { useRoute, useRouter } from '#app/composables/router'
45
import { clearUndefined, toArray } from '@antfu/utils'
56
import { computedWithControl, debouncedWatch } from '@vueuse/core'
@@ -27,6 +28,23 @@ const filters = reactive<Filters>({
2728
file_types: (route.query.file_types ? toArray(route.query.file_types) : null) as string[] | null,
2829
node_modules: (route.query.node_modules ? toArray(route.query.node_modules) : null) as string[] | null,
2930
})
31+
const moduleViewTypes = [
32+
{
33+
label: 'List',
34+
value: 'list',
35+
icon: 'i-ph-list-duotone',
36+
},
37+
{
38+
label: 'Graph',
39+
value: 'graph',
40+
icon: 'i-ph-graph-duotone',
41+
},
42+
{
43+
label: 'Folder',
44+
value: 'folder',
45+
icon: 'i-ph-folder-duotone',
46+
},
47+
] as const
3048
3149
debouncedWatch(
3250
filters,
@@ -79,7 +97,7 @@ const filtered = computed(() => {
7997
if (filters.node_modules) {
8098
modules = modules.filter(mod => mod.path.moduleName && filters.node_modules!.includes(mod.path.moduleName))
8199
}
82-
return modules.map(mod => mod.mod)
100+
return modules.map(mod => ({ ...mod.mod, path: mod.path.path }))
83101
})
84102
85103
function isFileTypeSelected(type: string) {
@@ -119,11 +137,11 @@ const searched = computed(() => {
119137
.map(r => r.item)
120138
})
121139
122-
function toggleDisplay() {
140+
function toggleDisplay(type: ClientSettings['flowModuleGraphView']) {
123141
if (route.query.module) {
124142
router.replace({ query: { ...route.query, module: undefined } })
125143
}
126-
settings.value.flowModuleGraphView = settings.value.flowModuleGraphView === 'list' ? 'graph' : 'list'
144+
settings.value.flowModuleGraphView = type
127145
}
128146
</script>
129147

@@ -161,12 +179,14 @@ function toggleDisplay() {
161179
<div flex="~ gap-2 items-center" p2 border="t base">
162180
<span op50 pl2 text-sm>View as</span>
163181
<button
182+
v-for="viewType of moduleViewTypes"
183+
:key="viewType.value"
164184
btn-action
165-
@click="toggleDisplay"
185+
:class="settings.flowModuleGraphView === viewType.value ? 'bg-active' : 'grayscale op50'"
186+
@click="toggleDisplay(viewType.value)"
166187
>
167-
<div v-if="settings.flowModuleGraphView === 'graph'" i-ph-graph-duotone />
168-
<div v-else i-ph-list-duotone />
169-
{{ settings.flowModuleGraphView === 'list' ? 'List' : 'Graph' }}
188+
<div :class="viewType.icon" />
189+
{{ viewType.label }}
170190
</button>
171191
</div>
172192
<!-- TODO: should we add filters for node_modules? -->
@@ -184,11 +204,17 @@ function toggleDisplay() {
184204
</div>
185205
</div>
186206
</template>
187-
<template v-else>
207+
<template v-else-if="settings.flowModuleGraphView === 'graph'">
188208
<ModulesGraph
189209
:session="session"
190210
:modules="searched"
191211
/>
192212
</template>
213+
<template v-else>
214+
<ModulesFolder
215+
:session="session"
216+
:modules="searched"
217+
/>
218+
</template>
193219
</div>
194220
</template>

packages/devtools/src/app/state/settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { computed } from 'vue'
55
export interface ClientSettings {
66
codeviewerLineWrap: boolean
77
codeviewerDiffPanelSize: number
8-
flowModuleGraphView: 'list' | 'graph'
8+
flowModuleGraphView: 'list' | 'graph' | 'folder'
99
flowExpandResolveId: boolean
1010
flowExpandTransforms: boolean
1111
flowExpandLoads: boolean
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,53 @@
1+
import type { ModuleDest, ModuleTreeNode } from '~~/shared/types'
2+
13
export function bytesToHumanSize(bytes: number, digits = 2) {
24
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
35
const i = Math.floor(Math.log(bytes) / Math.log(1024))
46
if (i === 0)
57
return ['<1', 'K']
68
return [(+(bytes / 1024 ** i).toFixed(digits)).toLocaleString(), sizes[i]]
79
}
10+
11+
export function toTree(modules: ModuleDest[], name: string) {
12+
const node: ModuleTreeNode = { name, children: {}, items: [] }
13+
14+
function add(mod: ModuleDest, parts: string[], current = node) {
15+
if (!mod)
16+
return
17+
18+
if (parts.length <= 1) {
19+
current.items.push(mod)
20+
return
21+
}
22+
23+
const first = parts.shift()!
24+
if (!current.children[first])
25+
current.children[first] = { name: first, children: {}, items: [] }
26+
add(mod, parts, current.children[first])
27+
}
28+
29+
modules.forEach((m) => {
30+
const parts = m.path.split(/\//g).filter(Boolean)
31+
add(m, parts)
32+
})
33+
34+
function flat(node: ModuleTreeNode) {
35+
if (!node)
36+
return
37+
const children = Object.values(node.children)
38+
if (children.length === 1 && !node.items.length) {
39+
const child = children[0]
40+
node.name = node.name ? `${node.name}/${child.name}` : child.name
41+
node.items = child.items
42+
node.children = child.children
43+
flat(node)
44+
}
45+
else {
46+
children.forEach(flat)
47+
}
48+
}
49+
50+
Object.values(node.children).forEach(flat)
51+
52+
return node
53+
}

packages/devtools/src/shared/types/data.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type { ModuleImport }
44

55
export interface ModuleListItem {
66
id: string
7+
path?: string
78
fileType: string
89
imports: ModuleImport[]
910
importers: string[]
@@ -26,6 +27,16 @@ export interface ModuleInfo {
2627
assets: RolldownAssetInfo[]
2728
}
2829

30+
export interface ModuleDest {
31+
full: string
32+
path: string
33+
}
34+
export interface ModuleTreeNode {
35+
name?: string
36+
children: Record<string, ModuleTreeNode>
37+
items: ModuleDest[]
38+
}
39+
2940
export interface RolldownResolveInfo {
3041
type: 'resolve'
3142
id: string

pnpm-lock.yaml

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ catalogs:
7171
icons:
7272
'@iconify-json/carbon': ^1.2.10
7373
'@iconify-json/catppuccin': ^1.2.11
74+
'@iconify-json/codicon': ^1.2.24
7475
'@iconify-json/logos': ^1.2.4
7576
'@iconify-json/ph': ^1.2.2
7677
'@iconify-json/ri': ^1.2.5

0 commit comments

Comments
 (0)