writing-vite-devtools-integrations
Vite DevTools Kit
Build custom developer tools that integrate with Vite DevTools using @vitejs/devtools-kit.
Core Concepts
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) |
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.viteConfig |
Resolved Vite configuration |
ctx.viteServer |
Dev server instance (dev mode only) |
ctx.mode |
'dev' or 'build' |
Quick Start: Minimal Plugin
/// <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',
})
},
},
}
}
Quick Start: Full Integration
/// <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()),
}),
})
)
},
},
}
}
Namespacing Convention
CRITICAL: Always prefix RPC functions, shared state keys, and dock IDs with your plugin name:
// Good - namespaced
'my-plugin:get-modules'
'my-plugin:state'
// Bad - may conflict
'get-modules'
'state'
Dock Entry Types
| Type | Use Case |
|---|---|
iframe |
Full UI panels, dashboards (most common) |
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 |
Iframe Entry
ctx.docks.register({
id: 'my-plugin',
title: 'My Plugin',
icon: 'ph:house-duotone',
type: 'iframe',
url: '/.my-plugin/',
})
Action Entry
ctx.docks.register({
id: 'my-inspector',
title: 'Inspector',
icon: 'ph:cursor-duotone',
type: 'action',
action: {
importFrom: 'my-plugin/devtools-action',
importName: 'default',
},
})
Custom Render Entry
ctx.docks.register({
id: 'my-custom',
title: 'Custom View',
icon: 'ph:code-duotone',
type: 'custom-render',
renderer: {
importFrom: 'my-plugin/devtools-renderer',
importName: 'default',
},
})
Launcher Entry
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
},
},
})
Logs & Notifications
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.
Fire-and-Forget
// No await needed
context.logs.add({
message: 'Plugin initialized',
level: 'info',
})
With Handle
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()
Key Fields
| 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'.
Deduplication
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' })
RPC Functions
Server-Side Definition
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)
Client-Side Call (iframe)
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
const rpc = await getDevToolsRpcClient()
const modules = await rpc.call('my-plugin:get-modules', 'src/')
Client-Side Call (action/renderer script)
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')
})
}
Broadcasting to Clients
// Server broadcasts to all clients
ctx.rpc.broadcast({
method: 'my-plugin:on-update',
args: [{ changedFile: '/src/main.ts' }],
})
Type Safety
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
}
}
Shared State
Server-Side
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')
})
Client-Side
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)
})
Client Scripts
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"
}
}
Debugging with Self-Inspect
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(),
],
})
Best Practices
- Always namespace - Prefix all identifiers with your plugin name
- Use type augmentation - Extend
DevToolsRpcServerFunctionsfor type-safe RPC - Keep state serializable - No functions or circular references in shared state
- Batch mutations - Use single
mutate()call for multiple changes - Host static files - Use
ctx.views.hostStatic()for your UI assets - Use Iconify icons - Prefer
ph:*(Phosphor) icons:icon: 'ph:chart-bar-duotone' - Deduplicate logs - Use explicit
idfor logs representing ongoing operations - Use Self-Inspect - Add
@vitejs/devtools-self-inspectduring development to debug your plugin
Example Plugins
Real-world example plugins in the repo — reference their code structure and patterns when building new integrations:
- A11y Checker (
examples/plugin-a11y-checker) — Action dock entry, client-side axe-core audits, logs with severity levels and element positions, log handle updates - File Explorer (
examples/plugin-file-explorer) — Iframe dock entry, RPC functions (static/query/action), hosted UI panel, RPC dump for static builds, backend mode detection
Further Reading
- RPC Patterns - Advanced RPC patterns and type utilities
- Dock Entry Types - Detailed dock configuration options
- Shared State Patterns - Framework integration examples
- Project Structure - Recommended file organization
- Logs Patterns - Log entries, toast notifications, and handle patterns