Creates devtools integrations for Vite using @vitejs/devtools-kit. Use when building Vite plugins with devtools panels, RPC functions, dock entries, shared state, logs/notifications, or any devtools-related functionality. Applies to files importing from @vitejs/devtools-kit or containing devtools.setup hooks in Vite plugins.
Build custom developer tools that integrate with Vite DevTools using @vitejs/devtools-kit.
A DevTools plugin extends a Vite plugin with a devtools.setup(ctx) hook. The context provides:
| Property | Purpose |
|---|---|
ctx.docks | Register dock entries (iframe, action, custom-render, launcher, json-render) |
ctx.views | Host static files for UI |
ctx.rpc | Register RPC functions, broadcast to clients |
ctx.rpc.sharedState | Synchronized server-client state |
ctx.logs | Emit structured log entries and toast notifications |
ctx.terminals | Spawn and manage child processes with streaming terminal output |
ctx.createJsonRenderer |
| Create server-side JSON render specs for zero-client-code UIs |
ctx.commands | Register executable commands with keyboard shortcuts and palette visibility |
ctx.viteConfig | Resolved Vite configuration |
ctx.viteServer | Dev server instance (dev mode only) |
ctx.mode | 'dev' or 'build' |
/// <reference types="@vitejs/devtools-kit" />
import type { Plugin } from 'vite'
export default function myPlugin(): Plugin {
return {
name: 'my-plugin',
devtools: {
setup(ctx) {
ctx.docks.register({
id: 'my-plugin',
title: 'My Plugin',
icon: 'ph:puzzle-piece-duotone',
type: 'iframe',
url: 'https://example.com/devtools',
})
},
},
}
}
/// <reference types="@vitejs/devtools-kit" />
import type { Plugin } from 'vite'
import { fileURLToPath } from 'node:url'
import { defineRpcFunction } from '@vitejs/devtools-kit'
export default function myAnalyzer(): Plugin {
const data = new Map<string, { size: number }>()
return {
name: 'my-analyzer',
// Collect data in Vite hooks
transform(code, id) {
data.set(id, { size: code.length })
},
devtools: {
setup(ctx) {
// 1. Host static UI
const clientPath = fileURLToPath(
new URL('../dist/client', import.meta.url)
)
ctx.views.hostStatic('/.my-analyzer/', clientPath)
// 2. Register dock entry
ctx.docks.register({
id: 'my-analyzer',
title: 'Analyzer',
icon: 'ph:chart-bar-duotone',
type: 'iframe',
url: '/.my-analyzer/',
})
// 3. Register RPC function
ctx.rpc.register(
defineRpcFunction({
name: 'my-analyzer:get-data',
type: 'query',
setup: () => ({
handler: async () => Array.from(data.entries()),
}),
})
)
},
},
}
}
CRITICAL: Always prefix RPC functions, shared state keys, dock IDs, and command IDs with your plugin name:
// Good - namespaced
'my-plugin:get-modules'
'my-plugin:state'
'my-plugin:clear-cache' // command ID
// Bad - may conflict
'get-modules'
'state'
| Type | Use Case |
|---|---|
iframe | Full UI panels, dashboards (most common) |
json-render | Server-side JSON specs — zero client code needed |
action | Buttons that trigger client-side scripts (inspectors, toggles) |
custom-render | Direct DOM access in panel (framework mounting) |
launcher | Actionable setup cards for initialization tasks |
ctx.docks.register({
id: 'my-plugin',
title: 'My Plugin',
icon: 'ph:house-duotone',
type: 'iframe',
url: '/.my-plugin/',
})
ctx.docks.register({
id: 'my-inspector',
title: 'Inspector',
icon: 'ph:cursor-duotone',
type: 'action',
action: {
importFrom: 'my-plugin/devtools-action',
importName: 'default',
},
})
ctx.docks.register({
id: 'my-custom',
title: 'Custom View',
icon: 'ph:code-duotone',
type: 'custom-render',
renderer: {
importFrom: 'my-plugin/devtools-renderer',
importName: 'default',
},
})
Build UIs entirely from server-side TypeScript — no client code needed:
const ui = ctx.createJsonRenderer({
root: 'root',
elements: {
root: {
type: 'Stack',
props: { direction: 'vertical', gap: 12 },
children: ['heading', 'info'],
},
heading: {
type: 'Text',
props: { content: 'Hello from JSON!', variant: 'heading' },
},
info: {
type: 'KeyValueTable',
props: {
entries: [
{ key: 'Version', value: '1.0.0' },
{ key: 'Status', value: 'Running' },
],
},
},
},
})
ctx.docks.register({
id: 'my-panel',
title: 'My Panel',
icon: 'ph:chart-bar-duotone',
type: 'json-render',
ui,
})
const entry = ctx.docks.register({
id: 'my-setup',
title: 'My Setup',
icon: 'ph:rocket-launch-duotone',
type: 'launcher',
launcher: {
title: 'Initialize My Plugin',
description: 'Run initial setup before using the plugin',
buttonStart: 'Start Setup',
buttonLoading: 'Setting up...',
onLaunch: async () => {
// Run initialization logic
},
},
})
Spawn and manage child processes with streaming terminal output:
const session = await ctx.terminals.startChildProcess(
{
command: 'vite',
args: ['build', '--watch'],
cwd: process.cwd(),
},
{
id: 'my-plugin:build-watcher',
title: 'Build Watcher',
icon: 'ph:terminal-duotone',
},
)
// Lifecycle controls
await session.terminate()
await session.restart()
A common pattern is combining with launcher docks — see Terminals Patterns.
Register executable commands discoverable via Mod+K palette:
import { defineCommand } from '@vitejs/devtools-kit'
ctx.commands.register(defineCommand({
id: 'my-plugin:clear-cache',
title: 'Clear Build Cache',
icon: 'ph:trash-duotone',
keybindings: [{ key: 'Mod+Shift+C' }],
when: 'clientType == embedded',
handler: async () => { /* ... */ },
}))
Commands support sub-commands (two-level hierarchy), conditional visibility via when clauses, and user-customizable keyboard shortcuts.
See Commands Patterns and When Clauses for full details.
Plugins can emit structured log entries from both server and client contexts. Logs appear in the built-in Logs panel and can optionally show as toast notifications.
// No await needed
context.logs.add({
message: 'Plugin initialized',
level: 'info',
})
const handle = await context.logs.add({
id: 'my-build',
message: 'Building...',
level: 'info',
status: 'loading',
})
// Update later
await handle.update({
message: 'Build complete',
level: 'success',
status: 'idle',
})
// Or dismiss
await handle.dismiss()
| Field | Type | Description |
|---|---|---|
message | string | Short title (required) |
level | 'info' | 'warn' | 'error' | 'success' | 'debug' | Severity (required) |
description | string | Detailed description |
notify | boolean | Show as toast notification |
filePosition | { file, line?, column? } | Source file location (clickable) |
elementPosition | { selector?, boundingBox?, description? } | DOM element position |
id | string | Explicit id for deduplication |
status | 'loading' | 'idle' | Shows spinner when loading |
category | string | Grouping (e.g., 'a11y', 'lint') |
labels | string[] | Tags for filtering |
autoDismiss | number | Toast auto-dismiss time in ms (default: 5000) |
autoDelete | number | Auto-delete time in ms |
The from field is automatically set to 'server' or 'browser'.
Re-adding with the same id updates the existing entry instead of creating a duplicate:
context.logs.add({ id: 'my-scan', message: 'Scanning...', level: 'info', status: 'loading' })
context.logs.add({ id: 'my-scan', message: 'Scan complete', level: 'success', status: 'idle' })
import { defineRpcFunction } from '@vitejs/devtools-kit'
const getModules = defineRpcFunction({
name: 'my-plugin:get-modules',
type: 'query', // 'query' | 'action' | 'static'
setup: ctx => ({
handler: async (filter?: string) => {
// ctx has full DevToolsNodeContext
return modules.filter(m => !filter || m.includes(filter))
},
}),
})
// Register in setup
ctx.rpc.register(getModules)
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
const rpc = await getDevToolsRpcClient()
const modules = await rpc.call('my-plugin:get-modules', 'src/')
import type { DevToolsClientScriptContext } from '@vitejs/devtools-kit/client'
export default function setup(ctx: DevToolsClientScriptContext) {
ctx.current.events.on('entry:activated', async () => {
const data = await ctx.current.rpc.call('my-plugin:get-data')
})
}
The global client context (DevToolsClientContext) provides access to the RPC client and is set automatically when DevTools initializes (embedded or standalone). Use getDevToolsClientContext() to access it from anywhere on the client side:
import { getDevToolsClientContext } from '@vitejs/devtools-kit/client'
const ctx = getDevToolsClientContext()
if (ctx) {
const modules = await ctx.rpc.call('my-plugin:get-modules')
}
// Server broadcasts to all clients
ctx.rpc.broadcast({
method: 'my-plugin:on-update',
args: [{ changedFile: '/src/main.ts' }],
})
Extend the DevTools Kit interfaces for full type checking:
// src/types.ts
import '@vitejs/devtools-kit'
declare module '@vitejs/devtools-kit' {
interface DevToolsRpcServerFunctions {
'my-plugin:get-modules': (filter?: string) => Promise<Module[]>
}
interface DevToolsRpcClientFunctions {
'my-plugin:on-update': (data: { changedFile: string }) => void
}
interface DevToolsRpcSharedStates {
'my-plugin:state': MyPluginState
}
}
const state = await ctx.rpc.sharedState.get('my-plugin:state', {
initialValue: { count: 0, items: [] },
})
// Read
console.log(state.value())
// Mutate (auto-syncs to clients)
state.mutate((draft) => {
draft.count += 1
draft.items.push('new item')
})
const client = await getDevToolsRpcClient()
const state = await client.rpc.sharedState.get('my-plugin:state')
// Read
console.log(state.value())
// Subscribe to changes
state.on('updated', (newState) => {
console.log('State updated:', newState)
})
For action buttons and custom renderers:
// src/devtools-action.ts
import type { DevToolsClientScriptContext } from '@vitejs/devtools-kit/client'
export default function setup(ctx: DevToolsClientScriptContext) {
ctx.current.events.on('entry:activated', () => {
console.log('Action activated')
// Your inspector/tool logic here
})
ctx.current.events.on('entry:deactivated', () => {
console.log('Action deactivated')
// Cleanup
})
}
Export from package.json:
{
"exports": {
".": "./dist/index.mjs",
"./devtools-action": "./dist/devtools-action.mjs"
}
}
Use @vitejs/devtools-self-inspect to debug your DevTools plugin. It shows registered RPC functions, dock entries, client scripts, and plugins in a meta-introspection UI at /.devtools-self-inspect/.
import DevTools from '@vitejs/devtools'
import DevToolsSelfInspect from '@vitejs/devtools-self-inspect'
export default defineConfig({
plugins: [
DevTools(),
DevToolsSelfInspect(),
],
})
DevToolsRpcServerFunctions for type-safe RPCmutate() call for multiple changesctx.views.hostStatic() for your UI assetsph:* (Phosphor) icons: icon: 'ph:chart-bar-duotone'id for logs representing ongoing operations@vitejs/devtools-self-inspect during development to debug your pluginmy-plugin:action pattern for command IDs, same as RPC and statewhen clauses - Conditionally show commands/docks with when expressions instead of programmatic show/hideReal-world example plugins in the repo — reference their code structure and patterns when building new integrations:
examples/plugin-a11y-checker) — Action dock entry, client-side axe-core audits, logs with severity levels and element positions, log handle updatesexamples/plugin-file-explorer) — Iframe dock entry, RPC functions (static/query/action), hosted UI panel, RPC dump for static builds, backend mode detectionexamples/plugin-git-ui) — JSON render dock entry, server-side JSON specs, $bindState two-way binding, $state in action params, dynamic badge updates