Skip to content

Commit bbbe883

Browse files
committed
feat(vite): modules page
1 parent 49be80f commit bbbe883

77 files changed

Lines changed: 7143 additions & 60 deletions

Some content is hidden

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

docs/errors/VDT0001.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# VDT0001: Inspect Context Unavailable
6+
7+
## Message
8+
9+
> Vite inspect context is not available for this DevTools context.
10+
11+
## Cause
12+
13+
A `vite:inspect:*` RPC function was called in a DevTools context that did not register the Vite inspect collector.
14+
15+
## Example
16+
17+
Calling `vite:inspect:get-modules-list` while only the base Vite metadata RPC functions are registered triggers this error.
18+
19+
## Fix
20+
21+
Register the Vite UI plugin with inspect collection enabled before calling `vite:inspect:*` RPC functions.
22+
23+
## Source
24+
25+
- [`packages/vite/src/node/rpc/inspect-context.ts`](https://github.com/vitejs/devtools/blob/main/packages/vite/src/node/rpc/inspect-context.ts)`getViteInspectContext()` throws this when no inspect context was registered for the current DevTools context.

docs/errors/VDT0002.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# VDT0002: Inspect Target Not Found
6+
7+
## Message
8+
9+
> Vite inspect target "`{id}`" was not found in `{target}`.
10+
11+
## Cause
12+
13+
An inspect RPC query referenced a Vite instance or environment that the collector has not recorded.
14+
15+
## Example
16+
17+
Calling `vite:inspect:get-module-transform-info` with an unknown `{ vite, env }` query triggers this error.
18+
19+
## Fix
20+
21+
Read `vite:inspect:get-metadata` first and pass one of the returned Vite instance IDs and environment names to follow-up inspect RPC calls.
22+
23+
## Source
24+
25+
- [`packages/vite/src/node/inspect/context.ts`](https://github.com/vitejs/devtools/blob/main/packages/vite/src/node/inspect/context.ts)`getViteContext()` and `getEnvContext()` throw this when a query references an unknown inspect target.

docs/errors/index.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Vite DevTools uses structured diagnostics to surface actionable warnings and err
99
## How error codes work
1010

1111
- Codes follow the pattern **prefix + 4-digit number** (e.g., `DF0001`, `DTK0008`, `RDDT0002`).
12-
- Each prefix maps to a package: `DTK` for `@vitejs/devtools` (Vite-specific pieces), `RDDT` for `@vitejs/devtools-rolldown`. The framework-neutral `devframe` package documents its own `DF`-prefixed codes at the [Devframe docs site](https://devfra.me/errors/).
12+
- Each prefix maps to a package: `DTK` for `@vitejs/devtools` (Vite-specific pieces), `RDDT` for `@vitejs/devtools-rolldown`, `VDT` for `@vitejs/devtools-vite`. The framework-neutral `devframe` package documents its own `DF`-prefixed codes at the [Devframe docs site](https://devfra.me/errors/).
1313
- Every error page includes the cause, recommended fix, and a reference to the source file that emits it.
1414
- The diagnostics system is powered by [`nostics`](https://github.com/vercel-labs/nostics), which provides structured diagnostic codes with docs URLs and ANSI-formatted console output.
1515

@@ -42,3 +42,12 @@ Emitted by `@vitejs/devtools-rolldown`.
4242
|------|-------|-------|
4343
| [RDDT0001](./RDDT0001) | warn | Rolldown Logs Directory Not Found |
4444
| [RDDT0002](./RDDT0002) | warn | Rolldown Log Reader Bad Line |
45+
46+
## Vite DevTools (VDT)
47+
48+
Emitted by `@vitejs/devtools-vite`.
49+
50+
| Code | Level | Title |
51+
|------|-------|-------|
52+
| [VDT0001](./VDT0001) | error | Inspect Context Unavailable |
53+
| [VDT0002](./VDT0002) | error | Inspect Target Not Found |

docs/kit/diagnostics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Prefixes used by the in-tree packages:
7575
| `DF` | `devframe` |
7676
| `DTK` | `@vitejs/devtools` |
7777
| `RDDT` | `@vitejs/devtools-rolldown` |
78-
| `VDT` | `@vitejs/devtools-vite` (reserved) |
78+
| `VDT` | `@vitejs/devtools-vite` |
7979

8080
Each definition supports `why` (string or function returning a string) and an optional `fix` (string or function). A `docsBase` on the definition group auto-attaches a per-code URL to every emitted diagnostic.
8181

packages/rolldown/src/nuxt.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export default defineNuxtConfig({
142142
},
143143
// Temporary disable type check for nuxt, rely on CI for now
144144
// typeCheck: true,
145-
includeWorkspace: true,
145+
includeWorkspace: false,
146146
},
147147

148148
workspaceDir: '../../',

packages/vite/package.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,40 @@
3434
"dev:prepare": "nuxi prepare src",
3535
"prepack": "pnpm build"
3636
},
37+
"peerDependencies": {
38+
"vite": "*"
39+
},
3740
"dependencies": {
3841
"@floating-ui/dom": "catalog:frontend",
3942
"@vitejs/devtools-kit": "workspace:*",
4043
"birpc": "catalog:deps",
44+
"d3-shape": "catalog:frontend",
4145
"devframe": "catalog:deps",
4246
"envinfo": "catalog:deps",
4347
"get-port-please": "catalog:deps",
4448
"h3": "catalog:deps",
49+
"nostics": "catalog:deps",
4550
"pathe": "catalog:deps",
4651
"ws": "catalog:deps"
4752
},
4853
"devDependencies": {
54+
"@types/d3-hierarchy": "catalog:types",
4955
"@types/envinfo": "catalog:types",
56+
"@types/splitpanes": "catalog:types",
5057
"@unocss/nuxt": "catalog:build",
58+
"@vueuse/components": "catalog:frontend",
5159
"@vueuse/core": "catalog:frontend",
5260
"@vueuse/nuxt": "catalog:build",
61+
"comlink": "catalog:frontend",
62+
"d3-hierarchy": "catalog:frontend",
63+
"diff-match-patch-es": "catalog:frontend",
5364
"floating-vue": "catalog:frontend",
65+
"fuse.js": "catalog:frontend",
66+
"modern-monaco": "catalog:frontend",
67+
"nanovis": "catalog:frontend",
68+
"splitpanes": "catalog:frontend",
5469
"tsdown": "catalog:build",
55-
"unocss": "catalog:build"
70+
"unocss": "catalog:build",
71+
"vue-virtual-scroller": "catalog:frontend"
5672
}
5773
}

packages/vite/src/app/app.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import PanelSideNav from '@vitejs/devtools-ui/components/PanelSideNav.vue'
33
import { useSideNav } from '@vitejs/devtools-ui/composables/nav'
44
import { useHead } from '#app/composables/head'
55
import { connect, rpcConnectionState } from './composables/rpc'
6+
import 'floating-vue/dist/style.css'
7+
import './styles/cm.css'
8+
import './styles/splitpanes.css'
69
import './styles/global.css'
710
import '@vitejs/devtools-ui/composables/dark'
8-
import 'floating-vue/dist/style.css'
911
1012
useHead({
1113
title: 'Vite DevTools',
@@ -20,6 +22,11 @@ useSideNav(() => {
2022
icon: 'i-ph-house-duotone',
2123
to: '/home',
2224
},
25+
{
26+
title: 'Modules',
27+
icon: 'i-ph-graph-duotone',
28+
to: '/graph',
29+
},
2330
]
2431
})
2532
</script>
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
<script setup lang="ts">
2+
import type { TreeNodeInput } from 'nanovis'
3+
import type { ViteModuleListItem } from '~/types/modules'
4+
import DisplayDuration from '@vitejs/devtools-ui/components/DisplayDuration.vue'
5+
import { Flamegraph, normalizeTreeNode } from 'nanovis'
6+
import { computed, nextTick, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue'
7+
import { normalizeTimestamp } from '~/utils/format'
8+
9+
interface FlowTransform {
10+
name: string
11+
result?: string | null
12+
start: number
13+
end: number
14+
}
15+
16+
interface ViteModuleFlamegraphItem {
17+
plugin_name: string
18+
duration: number
19+
timestamp_start: number
20+
timestamp_end: number
21+
}
22+
23+
interface ViteModuleFlamegraphInfo {
24+
resolve_ids: ViteModuleFlamegraphItem[]
25+
loads: ViteModuleFlamegraphItem[]
26+
transforms: ViteModuleFlamegraphItem[]
27+
}
28+
29+
const props = withDefaults(defineProps<{
30+
module: ViteModuleListItem
31+
transforms: FlowTransform[]
32+
flowNodeSelected?: boolean
33+
}>(), {
34+
flowNodeSelected: false,
35+
})
36+
37+
const n = (node: TreeNodeInput<any>) => normalizeTreeNode(node, undefined, false)
38+
39+
const info = computed<ViteModuleFlamegraphInfo>(() => {
40+
const first = props.transforms[0]
41+
const load = first && first.name !== '__load__'
42+
? [{
43+
plugin_name: first.name,
44+
duration: Math.max(0, first.end - first.start),
45+
timestamp_start: first.start,
46+
timestamp_end: first.end,
47+
}]
48+
: []
49+
50+
return {
51+
resolve_ids: props.module.plugins
52+
.filter(plugin => plugin.resolveId != null)
53+
.map((plugin, index) => ({
54+
plugin_name: plugin.name,
55+
duration: plugin.resolveId ?? 0,
56+
timestamp_start: index,
57+
timestamp_end: index + (plugin.resolveId ?? 0),
58+
})),
59+
loads: load,
60+
transforms: props.transforms
61+
.slice(1)
62+
.filter(transform => transform.name !== '__load__')
63+
.map(transform => ({
64+
plugin_name: transform.name,
65+
duration: Math.max(0, transform.end - transform.start),
66+
timestamp_start: transform.start,
67+
timestamp_end: transform.end,
68+
})),
69+
}
70+
})
71+
72+
const tree = computed(() => {
73+
const resolveIds = info.value.resolve_ids.map((id, idx) => n({
74+
id: `resolveId-${idx}`,
75+
text: id.plugin_name,
76+
size: id.duration,
77+
meta: id,
78+
}))
79+
const loads = info.value.loads.map((load, idx) => n({
80+
id: `load-${idx}`,
81+
text: load.plugin_name,
82+
size: load.duration,
83+
meta: load,
84+
}))
85+
const transforms = info.value.transforms.map((transform, idx) => n({
86+
id: `transform-${idx}`,
87+
text: transform.plugin_name,
88+
size: transform.duration,
89+
meta: transform,
90+
}))
91+
const children = [
92+
n({
93+
id: '~resolves',
94+
text: 'Resolve Id',
95+
children: resolveIds,
96+
}),
97+
n({
98+
id: '~loads',
99+
text: 'Load',
100+
children: loads,
101+
}),
102+
n({
103+
id: '~transforms',
104+
text: 'Transform',
105+
children: transforms,
106+
}),
107+
]
108+
109+
return n({
110+
id: '~root',
111+
text: 'Module Flamegraph',
112+
children,
113+
})
114+
})
115+
116+
const hoverNode = ref<{
117+
plugin_name: string
118+
duration: number
119+
meta: ViteModuleFlamegraphItem | undefined
120+
} | null>(null)
121+
const hoverX = ref<number>(0)
122+
const hoverY = ref<number>(0)
123+
const el = useTemplateRef<HTMLDivElement>('el')
124+
const flamegraph = shallowRef<Flamegraph | null>(null)
125+
126+
function buildFlamegraph() {
127+
flamegraph.value = new Flamegraph(tree.value, {
128+
animate: true,
129+
palette: {
130+
fg: '#888',
131+
},
132+
getSubtext: (node) => {
133+
const p = node.size / tree.value.size * 100
134+
if (p > 15 && p !== 100) {
135+
return `${p.toFixed(1)}%`
136+
}
137+
return undefined
138+
},
139+
onHover(node, e) {
140+
if (!node) {
141+
hoverNode.value = null
142+
return
143+
}
144+
if (e) {
145+
hoverX.value = e.clientX
146+
hoverY.value = e.clientY
147+
}
148+
hoverNode.value = {
149+
plugin_name: node.text!,
150+
duration: node.size,
151+
meta: node.meta,
152+
}
153+
},
154+
onLeave() {
155+
hoverNode.value = null
156+
},
157+
})
158+
el.value!.appendChild(flamegraph.value!.el)
159+
}
160+
161+
function disposeFlamegraph() {
162+
flamegraph.value?.dispose()
163+
}
164+
165+
onMounted(() => {
166+
buildFlamegraph()
167+
})
168+
169+
onUnmounted(() => {
170+
disposeFlamegraph()
171+
})
172+
173+
watch(tree, async () => {
174+
disposeFlamegraph()
175+
buildFlamegraph()
176+
}, {
177+
deep: true,
178+
})
179+
180+
watch(() => props.flowNodeSelected, async () => {
181+
await nextTick()
182+
flamegraph.value?.resize()
183+
})
184+
</script>
185+
186+
<template>
187+
<div relative border="t base" pb10 py1 mt4>
188+
<DisplayGraphHoverView :hover-x="hoverX" :hover-y="hoverY">
189+
<div
190+
v-if="hoverNode"
191+
border="~ base" rounded-lg shadow-lg px3 py2
192+
bg-glass pointer-events-none text-sm max-w-80
193+
>
194+
<div font-semibold font-mono text-base mb2>
195+
{{ hoverNode.plugin_name }}
196+
</div>
197+
<div v-if="hoverNode.meta" border="t base" pt2 flex="~ col gap-1.5" min-w-48>
198+
<div flex="~ justify-between items-center" py1>
199+
<label text-xs opacity-70>Start Time</label>
200+
<time
201+
:datetime="new Date(hoverNode.meta.timestamp_start).toISOString()"
202+
font-mono text="xs"
203+
bg="base/10"
204+
px1.5 py0.5 rounded
205+
>
206+
{{ normalizeTimestamp(hoverNode.meta.timestamp_start) }}
207+
</time>
208+
</div>
209+
<div flex="~ justify-between items-center" py1>
210+
<label text-xs opacity-70>End Time</label>
211+
<time
212+
:datetime="new Date(hoverNode.meta.timestamp_end).toISOString()"
213+
font-mono text="xs"
214+
bg="base/10"
215+
px1.5 py0.5 rounded
216+
>
217+
{{ normalizeTimestamp(hoverNode.meta.timestamp_end) }}
218+
</time>
219+
</div>
220+
<div flex="~ justify-between items-center" py1 border="t base dashed" pt2>
221+
<label text="xs" op70>Duration</label>
222+
<DisplayDuration :duration="hoverNode.duration" />
223+
</div>
224+
</div>
225+
<div v-else>
226+
<DisplayDuration :duration="hoverNode.duration" />
227+
</div>
228+
</div>
229+
</DisplayGraphHoverView>
230+
<div ref="el" min-h-30 />
231+
</div>
232+
</template>

0 commit comments

Comments
 (0)