@vitejs/devtools-kit
Advanced tools
| # Dock Entry Types | ||
| Detailed configuration for each dock entry type. | ||
| ## Common Properties | ||
| All dock entries share these properties: | ||
| ```ts | ||
| interface DockEntryBase { | ||
| id: string // Unique identifier | ||
| title: string // Display title | ||
| icon: string // URL, data URI, or Iconify name | ||
| category?: string // Grouping category | ||
| defaultOrder?: number // Sort order (higher = earlier) | ||
| isHidden?: boolean // Hide from dock | ||
| } | ||
| ``` | ||
| ## Icons | ||
| <!-- eslint-skip --> | ||
| ```ts | ||
| // Iconify (recommended) | ||
| icon: 'ph:chart-bar-duotone' // Phosphor Icons | ||
| icon: 'carbon:analytics' // Carbon Icons | ||
| icon: 'mdi:view-dashboard' // Material Design | ||
| // URL | ||
| icon: 'https://example.com/logo.svg' | ||
| // Data URI | ||
| icon: 'data:image/svg+xml,<svg>...</svg>' | ||
| // Light/dark variants | ||
| icon: { | ||
| light: 'https://example.com/logo-light.svg', | ||
| dark: 'https://example.com/logo-dark.svg', | ||
| } | ||
| ``` | ||
| Browse icons at [Iconify](https://icon-sets.iconify.design/). | ||
| ## Iframe Entries | ||
| Most common type. Displays your UI in an isolated iframe. | ||
| ```ts | ||
| interface IframeEntry extends DockEntryBase { | ||
| type: 'iframe' | ||
| url: string // URL to load | ||
| frameId?: string // Share iframe between entries | ||
| clientScript?: ClientScriptEntry // Optional client script | ||
| } | ||
| // Example | ||
| ctx.docks.register({ | ||
| id: 'my-plugin', | ||
| title: 'My Plugin', | ||
| icon: 'ph:house-duotone', | ||
| type: 'iframe', | ||
| url: '/.my-plugin/', | ||
| }) | ||
| ``` | ||
| ### Hosting Your Own UI | ||
| ```ts | ||
| import { fileURLToPath } from 'node:url' | ||
| const clientDist = fileURLToPath( | ||
| new URL('../dist/client', import.meta.url) | ||
| ) | ||
| ctx.views.hostStatic('/.my-plugin/', clientDist) | ||
| ctx.docks.register({ | ||
| id: 'my-plugin', | ||
| title: 'My Plugin', | ||
| icon: 'ph:house-duotone', | ||
| type: 'iframe', | ||
| url: '/.my-plugin/', | ||
| }) | ||
| ``` | ||
| ## Action Entries | ||
| Buttons that trigger client-side scripts. Perfect for inspectors and toggles. | ||
| ```ts | ||
| interface ActionEntry extends DockEntryBase { | ||
| type: 'action' | ||
| action: { | ||
| importFrom: string // Package export path | ||
| importName?: string // Export name (default: 'default') | ||
| } | ||
| } | ||
| // Registration | ||
| ctx.docks.register({ | ||
| id: 'my-inspector', | ||
| title: 'Inspector', | ||
| icon: 'ph:cursor-duotone', | ||
| type: 'action', | ||
| action: { | ||
| importFrom: 'my-plugin/devtools-action', | ||
| importName: 'default', | ||
| }, | ||
| }) | ||
| ``` | ||
| ### Client Script Implementation | ||
| ```ts | ||
| // src/devtools-action.ts | ||
| import type { DevToolsClientScriptContext } from '@vitejs/devtools-kit/client' | ||
| export default function setup(ctx: DevToolsClientScriptContext) { | ||
| let overlay: HTMLElement | null = null | ||
| ctx.current.events.on('entry:activated', () => { | ||
| overlay = document.createElement('div') | ||
| overlay.style.cssText = ` | ||
| position: fixed; | ||
| inset: 0; | ||
| cursor: crosshair; | ||
| z-index: 99999; | ||
| ` | ||
| overlay.onclick = (e) => { | ||
| const target = document.elementFromPoint(e.clientX, e.clientY) | ||
| console.log('Selected:', target) | ||
| } | ||
| document.body.appendChild(overlay) | ||
| }) | ||
| ctx.current.events.on('entry:deactivated', () => { | ||
| overlay?.remove() | ||
| overlay = null | ||
| }) | ||
| } | ||
| ``` | ||
| ### Package Export | ||
| ```json | ||
| { | ||
| "exports": { | ||
| ".": "./dist/index.mjs", | ||
| "./devtools-action": "./dist/devtools-action.mjs" | ||
| } | ||
| } | ||
| ``` | ||
| ## Custom Render Entries | ||
| Render directly into the DevTools panel DOM. Use when you need direct DOM access or framework mounting. | ||
| ```ts | ||
| interface CustomRenderEntry extends DockEntryBase { | ||
| type: 'custom-render' | ||
| renderer: { | ||
| importFrom: string | ||
| importName?: string | ||
| } | ||
| } | ||
| ctx.docks.register({ | ||
| id: 'my-custom', | ||
| title: 'Custom View', | ||
| icon: 'ph:code-duotone', | ||
| type: 'custom-render', | ||
| renderer: { | ||
| importFrom: 'my-plugin/devtools-renderer', | ||
| importName: 'default', | ||
| }, | ||
| }) | ||
| ``` | ||
| ### Renderer Implementation | ||
| ```ts | ||
| // src/devtools-renderer.ts | ||
| import type { DevToolsClientScriptContext } from '@vitejs/devtools-kit/client' | ||
| export default function setup(ctx: DevToolsClientScriptContext) { | ||
| ctx.current.events.on('dom:panel:mounted', (panel) => { | ||
| // Vanilla JS | ||
| panel.innerHTML = `<div style="padding: 16px;">Hello</div>` | ||
| // Or mount Vue | ||
| // import { createApp } from 'vue' | ||
| // import App from './App.vue' | ||
| // createApp(App).mount(panel) | ||
| // Or mount React | ||
| // import { createRoot } from 'react-dom/client' | ||
| // createRoot(panel).render(<App />) | ||
| }) | ||
| } | ||
| ``` | ||
| ## Client Script Events | ||
| | Event | Payload | Description | | ||
| |-------|---------|-------------| | ||
| | `entry:activated` | - | Entry was selected | | ||
| | `entry:deactivated` | - | Entry was deselected | | ||
| | `entry:updated` | `DevToolsDockUserEntry` | Entry metadata changed | | ||
| | `dom:panel:mounted` | `HTMLDivElement` | Panel DOM ready (custom-render only) | | ||
| | `dom:iframe:mounted` | `HTMLIFrameElement` | Iframe mounted (iframe only) | | ||
| ## Category Order | ||
| Default category ordering: | ||
| ```ts | ||
| DEFAULT_CATEGORIES_ORDER = { | ||
| '~viteplus': -1000, // First | ||
| 'default': 0, | ||
| 'app': 100, | ||
| 'framework': 200, | ||
| 'web': 300, | ||
| 'advanced': 400, | ||
| '~builtin': 1000, // Last | ||
| } | ||
| ``` | ||
| Use `category` to group related entries: | ||
| ```ts | ||
| ctx.docks.register({ | ||
| id: 'my-plugin', | ||
| title: 'My Plugin', | ||
| icon: 'ph:house-duotone', | ||
| type: 'iframe', | ||
| url: '/.my-plugin/', | ||
| category: 'framework', | ||
| }) | ||
| ``` |
| # Project Structure | ||
| Recommended file organization for DevTools integrations. | ||
| ## Basic Structure | ||
| ``` | ||
| my-devtools-plugin/ | ||
| ├── src/ | ||
| │ ├── node/ | ||
| │ │ ├── index.ts # Plugin entry (exports main plugin) | ||
| │ │ ├── rpc/ | ||
| │ │ │ ├── index.ts # RPC function exports | ||
| │ │ │ └── functions/ # Individual RPC functions | ||
| │ │ │ ├── get-modules.ts | ||
| │ │ │ └── get-stats.ts | ||
| │ │ └── utils.ts # Server-side utilities | ||
| │ ├── client/ | ||
| │ │ ├── main.ts # Client app entry | ||
| │ │ ├── App.vue # Root component | ||
| │ │ └── composables/ | ||
| │ │ └── rpc.ts # RPC composables | ||
| │ ├── types.ts # Type augmentations | ||
| │ └── shared/ | ||
| │ └── constants.ts # Shared constants | ||
| ├── dist/ | ||
| │ ├── index.mjs # Node plugin bundle | ||
| │ └── client/ # Built client assets | ||
| ├── package.json | ||
| └── tsconfig.json | ||
| ``` | ||
| ## Package.json Configuration | ||
| ```json | ||
| { | ||
| "name": "my-devtools-plugin", | ||
| "type": "module", | ||
| "exports": { | ||
| ".": { | ||
| "import": "./dist/index.mjs", | ||
| "types": "./dist/index.d.ts" | ||
| }, | ||
| "./devtools-action": { | ||
| "import": "./dist/devtools-action.mjs" | ||
| } | ||
| }, | ||
| "files": ["dist"], | ||
| "scripts": { | ||
| "build": "tsdown src/node/index.ts && vite build src/client", | ||
| "dev": "vite src/client" | ||
| }, | ||
| "dependencies": {}, | ||
| "devDependencies": { | ||
| "@vitejs/devtools-kit": "^0.x.x", | ||
| "vite": "^6.x.x" | ||
| } | ||
| } | ||
| ``` | ||
| ## Plugin Entry (src/node/index.ts) | ||
| ```ts | ||
| /// <reference types="@vitejs/devtools-kit" /> | ||
| import type { Plugin } from 'vite' | ||
| import { fileURLToPath } from 'node:url' | ||
| import { rpcFunctions } from './rpc' | ||
| import '../types' | ||
| const clientDist = fileURLToPath( | ||
| new URL('../../dist/client', import.meta.url) | ||
| ) | ||
| export default function myPlugin(): Plugin { | ||
| return { | ||
| name: 'my-plugin', | ||
| devtools: { | ||
| setup(ctx) { | ||
| // Register all RPC functions | ||
| for (const fn of rpcFunctions) { | ||
| ctx.rpc.register(fn) | ||
| } | ||
| // Host static UI | ||
| ctx.views.hostStatic('/.my-plugin/', clientDist) | ||
| // Register dock entry | ||
| ctx.docks.register({ | ||
| id: 'my-plugin', | ||
| title: 'My Plugin', | ||
| icon: 'ph:puzzle-piece-duotone', | ||
| type: 'iframe', | ||
| url: '/.my-plugin/', | ||
| }) | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
| ``` | ||
| ## RPC Index (src/node/rpc/index.ts) | ||
| ```ts | ||
| import type { RpcDefinitionsToFunctions } from '@vitejs/devtools-kit' | ||
| import { getModules } from './functions/get-modules' | ||
| import { getStats } from './functions/get-stats' | ||
| import '@vitejs/devtools-kit' | ||
| export const rpcFunctions = [ | ||
| getModules, | ||
| getStats, | ||
| ] as const | ||
| export type ServerFunctions = RpcDefinitionsToFunctions<typeof rpcFunctions> | ||
| declare module '@vitejs/devtools-kit' { | ||
| export interface DevToolsRpcServerFunctions extends ServerFunctions {} | ||
| } | ||
| ``` | ||
| ## RPC Function (src/node/rpc/functions/get-modules.ts) | ||
| ```ts | ||
| import type { Module } from '../../../types' | ||
| import { defineRpcFunction } from '@vitejs/devtools-kit' | ||
| export const getModules = defineRpcFunction({ | ||
| name: 'my-plugin:get-modules', | ||
| type: 'query', | ||
| setup: (ctx) => { | ||
| return { | ||
| handler: async (): Promise<Module[]> => { | ||
| // Access vite config, server, etc. from ctx | ||
| const root = ctx.viteConfig.root | ||
| return [] | ||
| }, | ||
| } | ||
| }, | ||
| }) | ||
| ``` | ||
| ## Type Augmentations (src/types.ts) | ||
| ```ts | ||
| import '@vitejs/devtools-kit' | ||
| export interface Module { | ||
| id: string | ||
| size: number | ||
| imports: string[] | ||
| } | ||
| export interface MyPluginState { | ||
| modules: Module[] | ||
| selectedId: string | null | ||
| } | ||
| declare module '@vitejs/devtools-kit' { | ||
| interface DevToolsRpcSharedStates { | ||
| 'my-plugin:state': MyPluginState | ||
| } | ||
| } | ||
| ``` | ||
| ## Client Entry (src/client/main.ts) | ||
| ```ts | ||
| import { createApp } from 'vue' | ||
| import App from './App.vue' | ||
| createApp(App).mount('#app') | ||
| ``` | ||
| ## Client RPC Composable (src/client/composables/rpc.ts) | ||
| ```ts | ||
| import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client' | ||
| let clientPromise: Promise<Awaited<ReturnType<typeof getDevToolsRpcClient>>> | ||
| export function useRpc() { | ||
| if (!clientPromise) { | ||
| clientPromise = getDevToolsRpcClient() | ||
| } | ||
| return clientPromise | ||
| } | ||
| ``` | ||
| ## Client App Component (src/client/App.vue) | ||
| ```vue | ||
| <script setup lang="ts"> | ||
| import type { Module } from '../types' | ||
| import { onMounted, shallowRef } from 'vue' | ||
| import { useRpc } from './composables/rpc' | ||
| const modules = shallowRef<Module[]>([]) | ||
| const loading = shallowRef(true) | ||
| onMounted(async () => { | ||
| const rpc = await useRpc() | ||
| modules.value = await rpc.call('my-plugin:get-modules') | ||
| loading.value = false | ||
| }) | ||
| </script> | ||
| <template> | ||
| <div class="p-4"> | ||
| <h1 class="text-xl font-bold mb-4"> | ||
| My Plugin | ||
| </h1> | ||
| <div v-if="loading"> | ||
| Loading... | ||
| </div> | ||
| <ul v-else> | ||
| <li v-for="mod in modules" :key="mod.id"> | ||
| {{ mod.id }} ({{ mod.size }} bytes) | ||
| </li> | ||
| </ul> | ||
| </div> | ||
| </template> | ||
| ``` | ||
| ## Vite Config for Client (src/client/vite.config.ts) | ||
| ```ts | ||
| import vue from '@vitejs/plugin-vue' | ||
| import { defineConfig } from 'vite' | ||
| export default defineConfig({ | ||
| plugins: [vue()], | ||
| build: { | ||
| outDir: '../../dist/client', | ||
| emptyOutDir: true, | ||
| }, | ||
| }) | ||
| ``` | ||
| ## Real-World Reference | ||
| See [packages/vite](https://github.com/user/vite-devtools/tree/main/packages/vite) for a complete implementation example with: | ||
| - Multiple RPC functions organized by feature | ||
| - Nuxt-based client UI | ||
| - Complex data visualization | ||
| - Build session management |
| # RPC Patterns | ||
| Advanced patterns for server-client communication in DevTools integrations. | ||
| ## Function Types | ||
| | Type | Caching | Use Case | | ||
| |------|---------|----------| | ||
| | `query` | Can be cached | Read operations, data fetching | | ||
| | `action` | Never cached | Mutations, side effects | | ||
| | `static` | Cached indefinitely | Constants, configuration | | ||
| ## Type-Safe RPC Setup | ||
| ### Step 1: Define Types | ||
| ```ts | ||
| // src/types.ts | ||
| import '@vitejs/devtools-kit' | ||
| interface Module { | ||
| id: string | ||
| size: number | ||
| imports: string[] | ||
| } | ||
| declare module '@vitejs/devtools-kit' { | ||
| interface DevToolsRpcServerFunctions { | ||
| 'my-plugin:list-modules': () => Promise<Module[]> | ||
| 'my-plugin:get-module': (id: string) => Promise<Module | null> | ||
| 'my-plugin:analyze': (options: { deep: boolean }) => Promise<void> | ||
| } | ||
| interface DevToolsRpcClientFunctions { | ||
| 'my-plugin:highlight': (selector: string) => void | ||
| 'my-plugin:refresh': () => void | ||
| } | ||
| } | ||
| ``` | ||
| ### Step 2: Import Types File | ||
| ```ts | ||
| // src/node/plugin.ts | ||
| import '../types' // Side-effect import for type augmentation | ||
| ``` | ||
| ### Step 3: Register Functions | ||
| ```ts | ||
| import { defineRpcFunction } from '@vitejs/devtools-kit' | ||
| const listModules = defineRpcFunction({ | ||
| name: 'my-plugin:list-modules', | ||
| type: 'query', | ||
| setup: () => ({ | ||
| handler: async (): Promise<Module[]> => { | ||
| return Array.from(moduleMap.values()) | ||
| }, | ||
| }), | ||
| }) | ||
| ``` | ||
| ## Context Access in Setup | ||
| The `setup` function receives the full `DevToolsNodeContext`: | ||
| ```ts | ||
| defineRpcFunction({ | ||
| name: 'my-plugin:get-config', | ||
| type: 'static', | ||
| setup: (ctx) => { | ||
| // Access at setup time (runs once) | ||
| const root = ctx.viteConfig.root | ||
| const isDev = ctx.mode === 'dev' | ||
| return { | ||
| handler: async () => ({ | ||
| root, | ||
| isDev, | ||
| plugins: ctx.viteConfig.plugins.map(p => p.name), | ||
| }), | ||
| } | ||
| }, | ||
| }) | ||
| ``` | ||
| ## Broadcasting Patterns | ||
| ### Basic Broadcast | ||
| ```ts | ||
| // Notify all clients | ||
| ctx.rpc.broadcast({ | ||
| method: 'my-plugin:refresh', | ||
| args: [], | ||
| }) | ||
| ``` | ||
| ### Broadcast with Data | ||
| ```ts | ||
| ctx.viteServer?.watcher.on('change', (file) => { | ||
| ctx.rpc.broadcast({ | ||
| method: 'my-plugin:file-changed', | ||
| args: [{ path: file, timestamp: Date.now() }], | ||
| }) | ||
| }) | ||
| ``` | ||
| ### Optional Broadcast | ||
| ```ts | ||
| // Won't error if no clients have registered the function | ||
| ctx.rpc.broadcast({ | ||
| method: 'my-plugin:optional-update', | ||
| args: [data], | ||
| optional: true, | ||
| }) | ||
| ``` | ||
| ## Client Function Registration | ||
| ```ts | ||
| // Client-side (action/renderer script) | ||
| export default function setup(ctx: DevToolsClientScriptContext) { | ||
| ctx.current.rpc.client.register({ | ||
| name: 'my-plugin:highlight', | ||
| type: 'action', | ||
| handler: (selector: string) => { | ||
| const el = document.querySelector(selector) | ||
| if (el) { | ||
| el.style.outline = '3px solid red' | ||
| setTimeout(() => { | ||
| el.style.outline = '' | ||
| }, 2000) | ||
| } | ||
| }, | ||
| }) | ||
| } | ||
| ``` | ||
| ## Collecting RPC Functions | ||
| Organize RPC functions in a registry pattern: | ||
| ```ts | ||
| import { analyzeBundle } from './functions/analyze-bundle' | ||
| // src/node/rpc/index.ts | ||
| import { getModules } from './functions/get-modules' | ||
| import { getStats } from './functions/get-stats' | ||
| export const rpcFunctions = [ | ||
| getModules, | ||
| getStats, | ||
| analyzeBundle, | ||
| ] as const | ||
| // Register all in setup | ||
| for (const fn of rpcFunctions) { | ||
| ctx.rpc.register(fn) | ||
| } | ||
| ``` | ||
| ## Type Extraction Utilities | ||
| ```ts | ||
| import type { | ||
| RpcDefinitionsFilter, | ||
| RpcDefinitionsToFunctions, | ||
| } from '@vitejs/devtools-kit' | ||
| // Extract all function types | ||
| export type ServerFunctions = RpcDefinitionsToFunctions<typeof rpcFunctions> | ||
| // Extract only static functions | ||
| export type StaticFunctions = RpcDefinitionsToFunctions< | ||
| RpcDefinitionsFilter<typeof rpcFunctions, 'static'> | ||
| > | ||
| // Augment the global interface | ||
| declare module '@vitejs/devtools-kit' { | ||
| export interface DevToolsRpcServerFunctions extends ServerFunctions {} | ||
| } | ||
| ``` |
| # Shared State Patterns | ||
| Synchronized state between server and all clients. | ||
| ## Basic Usage | ||
| ### Server-Side | ||
| ```ts | ||
| const state = await ctx.rpc.sharedState.get('my-plugin:state', { | ||
| initialValue: { | ||
| count: 0, | ||
| items: [], | ||
| settings: { theme: 'dark' }, | ||
| }, | ||
| }) | ||
| // Read | ||
| const current = state.value() | ||
| // Mutate (syncs to all clients) | ||
| state.mutate((draft) => { | ||
| draft.count += 1 | ||
| draft.items.push({ id: Date.now(), name: 'New' }) | ||
| }) | ||
| ``` | ||
| ### Client-Side | ||
| ```ts | ||
| import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client' | ||
| const client = await getDevToolsRpcClient() | ||
| const state = await client.rpc.sharedState.get('my-plugin:state') | ||
| // Read | ||
| console.log(state.value()) | ||
| // Subscribe | ||
| state.on('updated', (newState) => { | ||
| console.log('Updated:', newState) | ||
| }) | ||
| ``` | ||
| ## Type-Safe Shared State | ||
| ```ts | ||
| // src/types.ts | ||
| interface MyPluginState { | ||
| count: number | ||
| items: Array<{ id: string, name: string }> | ||
| settings: { | ||
| theme: 'light' | 'dark' | ||
| notifications: boolean | ||
| } | ||
| } | ||
| declare module '@vitejs/devtools-kit' { | ||
| interface DevToolsRpcSharedStates { | ||
| 'my-plugin:state': MyPluginState | ||
| } | ||
| } | ||
| ``` | ||
| ## Vue Integration | ||
| ```ts | ||
| // composables/useSharedState.ts | ||
| import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client' | ||
| import { onMounted, shallowRef } from 'vue' | ||
| export function useSharedState<T>(name: string) { | ||
| const state = shallowRef<T | null>(null) | ||
| const loading = shallowRef(true) | ||
| onMounted(async () => { | ||
| const client = await getDevToolsRpcClient() | ||
| const shared = await client.rpc.sharedState.get<T>(name) | ||
| state.value = shared.value() | ||
| loading.value = false | ||
| shared.on('updated', (newState) => { | ||
| state.value = newState | ||
| }) | ||
| }) | ||
| return { state, loading } | ||
| } | ||
| // Usage in component | ||
| const { state, loading } = useSharedState<MyPluginState>('my-plugin:state') | ||
| ``` | ||
| ### Full Vue Component Example | ||
| ```vue | ||
| <script setup lang="ts"> | ||
| import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client' | ||
| import { onMounted, shallowRef } from 'vue' | ||
| interface PluginState { | ||
| count: number | ||
| items: string[] | ||
| } | ||
| const state = shallowRef<PluginState | null>(null) | ||
| onMounted(async () => { | ||
| const client = await getDevToolsRpcClient() | ||
| const shared = await client.rpc.sharedState.get<PluginState>('my-plugin:state') | ||
| state.value = shared.value() | ||
| shared.on('updated', (newState) => { | ||
| state.value = newState | ||
| }) | ||
| }) | ||
| </script> | ||
| <template> | ||
| <div v-if="state"> | ||
| <p>Count: {{ state.count }}</p> | ||
| <ul> | ||
| <li v-for="item in state.items" :key="item"> | ||
| {{ item }} | ||
| </li> | ||
| </ul> | ||
| </div> | ||
| </template> | ||
| ``` | ||
| ## React Integration | ||
| ```tsx | ||
| import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client' | ||
| import { useEffect, useState } from 'react' | ||
| function useSharedState<T>(name: string, fallback: T) { | ||
| const [state, setState] = useState<T>(fallback) | ||
| const [loading, setLoading] = useState(true) | ||
| useEffect(() => { | ||
| let mounted = true | ||
| async function init() { | ||
| const client = await getDevToolsRpcClient() | ||
| const shared = await client.rpc.sharedState.get<T>(name) | ||
| if (mounted) { | ||
| setState(shared.value() ?? fallback) | ||
| setLoading(false) | ||
| shared.on('updated', (newState) => { | ||
| if (mounted) | ||
| setState(newState) | ||
| }) | ||
| } | ||
| } | ||
| init() | ||
| return () => { | ||
| mounted = false | ||
| } | ||
| }, [name]) | ||
| return { state, loading } | ||
| } | ||
| // Usage | ||
| function MyComponent() { | ||
| const { state, loading } = useSharedState('my-plugin:state', { count: 0 }) | ||
| if (loading) | ||
| return <div>Loading...</div> | ||
| return ( | ||
| <div> | ||
| Count: | ||
| {state.count} | ||
| </div> | ||
| ) | ||
| } | ||
| ``` | ||
| ## Svelte Integration | ||
| ```svelte | ||
| <script lang="ts"> | ||
| import { onMount } from 'svelte' | ||
| import { writable } from 'svelte/store' | ||
| import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client' | ||
| interface PluginState { | ||
| count: number | ||
| } | ||
| const state = writable<PluginState>({ count: 0 }) | ||
| onMount(async () => { | ||
| const client = await getDevToolsRpcClient() | ||
| const shared = await client.rpc.sharedState.get<PluginState>('my-plugin:state') | ||
| state.set(shared.value()) | ||
| shared.on('updated', (newState) => { | ||
| state.set(newState) | ||
| }) | ||
| }) | ||
| </script> | ||
| <p>Count: {$state.count}</p> | ||
| ``` | ||
| ## Best Practices | ||
| ### Batch Mutations | ||
| ```ts | ||
| // Good - single sync event | ||
| state.mutate((draft) => { | ||
| draft.count += 1 | ||
| draft.lastUpdate = Date.now() | ||
| draft.items.push(newItem) | ||
| }) | ||
| // Bad - multiple sync events | ||
| state.mutate((d) => { | ||
| d.count += 1 | ||
| }) | ||
| state.mutate((d) => { | ||
| d.lastUpdate = Date.now() | ||
| }) | ||
| state.mutate((d) => { | ||
| d.items.push(newItem) | ||
| }) | ||
| ``` | ||
| ### Keep State Small | ||
| For large datasets, store IDs in shared state and fetch details via RPC: | ||
| <!-- eslint-skip --> | ||
| ```ts | ||
| // Shared state (small) | ||
| { | ||
| moduleIds: ['a', 'b', 'c'], | ||
| selectedId: 'a' | ||
| } | ||
| // Fetch full data via RPC when needed | ||
| const module = await rpc.call('my-plugin:get-module', state.selectedId) | ||
| ``` | ||
| ### Serializable Data Only | ||
| <!-- eslint-skip --> | ||
| ```ts | ||
| // Bad - functions can't be serialized | ||
| { count: 0, increment: () => {} } | ||
| // Bad - circular references | ||
| const obj = { child: null } | ||
| obj.child = obj | ||
| // Good - plain data | ||
| { count: 0, items: [{ id: 1 }] } | ||
| ``` | ||
| ### Real-Time Updates Example | ||
| ```ts | ||
| const plugin: Plugin = { | ||
| devtools: { | ||
| async setup(ctx) { | ||
| const state = await ctx.rpc.sharedState.get('my-plugin:state', { | ||
| initialValue: { modules: [], lastUpdate: 0 }, | ||
| }) | ||
| // Update state when Vite processes modules | ||
| ctx.viteServer?.watcher.on('change', (file) => { | ||
| state.mutate((draft) => { | ||
| draft.modules.push(file) | ||
| draft.lastUpdate = Date.now() | ||
| }) | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| ``` |
| --- | ||
| name: writing-vite-devtools-integrations | ||
| description: > | ||
| Creates devtools integrations for Vite using @vitejs/devtools-kit. | ||
| Use when building Vite plugins with devtools panels, RPC functions, | ||
| dock entries, shared state, or any devtools-related functionality. | ||
| Applies to files importing from @vitejs/devtools-kit or containing | ||
| devtools.setup hooks in Vite plugins. | ||
| --- | ||
| # 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) | | ||
| | `ctx.views` | Host static files for UI | | ||
| | `ctx.rpc` | Register RPC functions, broadcast to clients | | ||
| | `ctx.rpc.sharedState` | Synchronized server-client state | | ||
| | `ctx.viteConfig` | Resolved Vite configuration | | ||
| | `ctx.viteServer` | Dev server instance (dev mode only) | | ||
| | `ctx.mode` | `'dev'` or `'build'` | | ||
| ## Quick Start: Minimal Plugin | ||
| ```ts | ||
| /// <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 | ||
| ```ts | ||
| /// <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: | ||
| ```ts | ||
| // 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) | | ||
| ### Iframe Entry | ||
| ```ts | ||
| ctx.docks.register({ | ||
| id: 'my-plugin', | ||
| title: 'My Plugin', | ||
| icon: 'ph:house-duotone', | ||
| type: 'iframe', | ||
| url: '/.my-plugin/', | ||
| }) | ||
| ``` | ||
| ### Action Entry | ||
| ```ts | ||
| 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 | ||
| ```ts | ||
| ctx.docks.register({ | ||
| id: 'my-custom', | ||
| title: 'Custom View', | ||
| icon: 'ph:code-duotone', | ||
| type: 'custom-render', | ||
| renderer: { | ||
| importFrom: 'my-plugin/devtools-renderer', | ||
| importName: 'default', | ||
| }, | ||
| }) | ||
| ``` | ||
| ## RPC Functions | ||
| ### Server-Side Definition | ||
| ```ts | ||
| 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) | ||
| ```ts | ||
| 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) | ||
| ```ts | ||
| 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 | ||
| ```ts | ||
| // 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: | ||
| ```ts | ||
| // 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 | ||
| ```ts | ||
| 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 | ||
| ```ts | ||
| 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: | ||
| ```ts | ||
| // 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: | ||
| ```json | ||
| { | ||
| "exports": { | ||
| ".": "./dist/index.mjs", | ||
| "./devtools-action": "./dist/devtools-action.mjs" | ||
| } | ||
| } | ||
| ``` | ||
| ## Best Practices | ||
| 1. **Always namespace** - Prefix all identifiers with your plugin name | ||
| 2. **Use type augmentation** - Extend `DevToolsRpcServerFunctions` for type-safe RPC | ||
| 3. **Keep state serializable** - No functions or circular references in shared state | ||
| 4. **Batch mutations** - Use single `mutate()` call for multiple changes | ||
| 5. **Host static files** - Use `ctx.views.hostStatic()` for your UI assets | ||
| 6. **Use Iconify icons** - Prefer `ph:*` (Phosphor) icons: `icon: 'ph:chart-bar-duotone'` | ||
| ## Further Reading | ||
| - [RPC Patterns](./references/rpc-patterns.md) - Advanced RPC patterns and type utilities | ||
| - [Dock Entry Types](./references/dock-entry-types.md) - Detailed dock configuration options | ||
| - [Shared State Patterns](./references/shared-state-patterns.md) - Framework integration examples | ||
| - [Project Structure](./references/project-structure.md) - Recommended file organization |
+4
-3
| { | ||
| "name": "@vitejs/devtools-kit", | ||
| "type": "module", | ||
| "version": "0.0.0-alpha.28", | ||
| "version": "0.0.0-alpha.29", | ||
| "description": "Vite DevTools Kit", | ||
@@ -32,3 +32,4 @@ "author": "VoidZero Inc.", | ||
| "files": [ | ||
| "dist" | ||
| "dist", | ||
| "skills" | ||
| ], | ||
@@ -41,3 +42,3 @@ "peerDependencies": { | ||
| "immer": "^11.1.3", | ||
| "@vitejs/devtools-rpc": "0.0.0-alpha.28" | ||
| "@vitejs/devtools-rpc": "0.0.0-alpha.29" | ||
| }, | ||
@@ -44,0 +45,0 @@ "devDependencies": { |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
156148
21.82%21
31.25%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed