@vitejs/devtools-kit 0.0.0-alpha.27 → 0.0.0-alpha.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vitejs/devtools-kit",
3
3
  "type": "module",
4
- "version": "0.0.0-alpha.27",
4
+ "version": "0.0.0-alpha.29",
5
5
  "description": "Vite DevTools Kit",
6
6
  "author": "VoidZero Inc.",
7
7
  "license": "MIT",
@@ -29,7 +29,8 @@
29
29
  },
30
30
  "types": "./dist/index.d.mts",
31
31
  "files": [
32
- "dist"
32
+ "dist",
33
+ "skills"
33
34
  ],
34
35
  "peerDependencies": {
35
36
  "vite": "*"
@@ -37,7 +38,7 @@
37
38
  "dependencies": {
38
39
  "birpc": "^4.0.0",
39
40
  "immer": "^11.1.3",
40
- "@vitejs/devtools-rpc": "0.0.0-alpha.27"
41
+ "@vitejs/devtools-rpc": "0.0.0-alpha.29"
41
42
  },
42
43
  "devDependencies": {
43
44
  "my-ua-parser": "^2.0.4",
@@ -0,0 +1,325 @@
1
+ ---
2
+ name: writing-vite-devtools-integrations
3
+ description: >
4
+ Creates devtools integrations for Vite using @vitejs/devtools-kit.
5
+ Use when building Vite plugins with devtools panels, RPC functions,
6
+ dock entries, shared state, or any devtools-related functionality.
7
+ Applies to files importing from @vitejs/devtools-kit or containing
8
+ devtools.setup hooks in Vite plugins.
9
+ ---
10
+
11
+ # Vite DevTools Kit
12
+
13
+ Build custom developer tools that integrate with Vite DevTools using `@vitejs/devtools-kit`.
14
+
15
+ ## Core Concepts
16
+
17
+ A DevTools plugin extends a Vite plugin with a `devtools.setup(ctx)` hook. The context provides:
18
+
19
+ | Property | Purpose |
20
+ |----------|---------|
21
+ | `ctx.docks` | Register dock entries (iframe, action, custom-render) |
22
+ | `ctx.views` | Host static files for UI |
23
+ | `ctx.rpc` | Register RPC functions, broadcast to clients |
24
+ | `ctx.rpc.sharedState` | Synchronized server-client state |
25
+ | `ctx.viteConfig` | Resolved Vite configuration |
26
+ | `ctx.viteServer` | Dev server instance (dev mode only) |
27
+ | `ctx.mode` | `'dev'` or `'build'` |
28
+
29
+ ## Quick Start: Minimal Plugin
30
+
31
+ ```ts
32
+ /// <reference types="@vitejs/devtools-kit" />
33
+ import type { Plugin } from 'vite'
34
+
35
+ export default function myPlugin(): Plugin {
36
+ return {
37
+ name: 'my-plugin',
38
+ devtools: {
39
+ setup(ctx) {
40
+ ctx.docks.register({
41
+ id: 'my-plugin',
42
+ title: 'My Plugin',
43
+ icon: 'ph:puzzle-piece-duotone',
44
+ type: 'iframe',
45
+ url: 'https://example.com/devtools',
46
+ })
47
+ },
48
+ },
49
+ }
50
+ }
51
+ ```
52
+
53
+ ## Quick Start: Full Integration
54
+
55
+ ```ts
56
+ /// <reference types="@vitejs/devtools-kit" />
57
+ import type { Plugin } from 'vite'
58
+ import { fileURLToPath } from 'node:url'
59
+ import { defineRpcFunction } from '@vitejs/devtools-kit'
60
+
61
+ export default function myAnalyzer(): Plugin {
62
+ const data = new Map<string, { size: number }>()
63
+
64
+ return {
65
+ name: 'my-analyzer',
66
+
67
+ // Collect data in Vite hooks
68
+ transform(code, id) {
69
+ data.set(id, { size: code.length })
70
+ },
71
+
72
+ devtools: {
73
+ setup(ctx) {
74
+ // 1. Host static UI
75
+ const clientPath = fileURLToPath(
76
+ new URL('../dist/client', import.meta.url)
77
+ )
78
+ ctx.views.hostStatic('/.my-analyzer/', clientPath)
79
+
80
+ // 2. Register dock entry
81
+ ctx.docks.register({
82
+ id: 'my-analyzer',
83
+ title: 'Analyzer',
84
+ icon: 'ph:chart-bar-duotone',
85
+ type: 'iframe',
86
+ url: '/.my-analyzer/',
87
+ })
88
+
89
+ // 3. Register RPC function
90
+ ctx.rpc.register(
91
+ defineRpcFunction({
92
+ name: 'my-analyzer:get-data',
93
+ type: 'query',
94
+ setup: () => ({
95
+ handler: async () => Array.from(data.entries()),
96
+ }),
97
+ })
98
+ )
99
+ },
100
+ },
101
+ }
102
+ }
103
+ ```
104
+
105
+ ## Namespacing Convention
106
+
107
+ **CRITICAL**: Always prefix RPC functions, shared state keys, and dock IDs with your plugin name:
108
+
109
+ ```ts
110
+ // Good - namespaced
111
+ 'my-plugin:get-modules'
112
+ 'my-plugin:state'
113
+
114
+ // Bad - may conflict
115
+ 'get-modules'
116
+ 'state'
117
+ ```
118
+
119
+ ## Dock Entry Types
120
+
121
+ | Type | Use Case |
122
+ |------|----------|
123
+ | `iframe` | Full UI panels, dashboards (most common) |
124
+ | `action` | Buttons that trigger client-side scripts (inspectors, toggles) |
125
+ | `custom-render` | Direct DOM access in panel (framework mounting) |
126
+
127
+ ### Iframe Entry
128
+
129
+ ```ts
130
+ ctx.docks.register({
131
+ id: 'my-plugin',
132
+ title: 'My Plugin',
133
+ icon: 'ph:house-duotone',
134
+ type: 'iframe',
135
+ url: '/.my-plugin/',
136
+ })
137
+ ```
138
+
139
+ ### Action Entry
140
+
141
+ ```ts
142
+ ctx.docks.register({
143
+ id: 'my-inspector',
144
+ title: 'Inspector',
145
+ icon: 'ph:cursor-duotone',
146
+ type: 'action',
147
+ action: {
148
+ importFrom: 'my-plugin/devtools-action',
149
+ importName: 'default',
150
+ },
151
+ })
152
+ ```
153
+
154
+ ### Custom Render Entry
155
+
156
+ ```ts
157
+ ctx.docks.register({
158
+ id: 'my-custom',
159
+ title: 'Custom View',
160
+ icon: 'ph:code-duotone',
161
+ type: 'custom-render',
162
+ renderer: {
163
+ importFrom: 'my-plugin/devtools-renderer',
164
+ importName: 'default',
165
+ },
166
+ })
167
+ ```
168
+
169
+ ## RPC Functions
170
+
171
+ ### Server-Side Definition
172
+
173
+ ```ts
174
+ import { defineRpcFunction } from '@vitejs/devtools-kit'
175
+
176
+ const getModules = defineRpcFunction({
177
+ name: 'my-plugin:get-modules',
178
+ type: 'query', // 'query' | 'action' | 'static'
179
+ setup: ctx => ({
180
+ handler: async (filter?: string) => {
181
+ // ctx has full DevToolsNodeContext
182
+ return modules.filter(m => !filter || m.includes(filter))
183
+ },
184
+ }),
185
+ })
186
+
187
+ // Register in setup
188
+ ctx.rpc.register(getModules)
189
+ ```
190
+
191
+ ### Client-Side Call (iframe)
192
+
193
+ ```ts
194
+ import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
195
+
196
+ const rpc = await getDevToolsRpcClient()
197
+ const modules = await rpc.call('my-plugin:get-modules', 'src/')
198
+ ```
199
+
200
+ ### Client-Side Call (action/renderer script)
201
+
202
+ ```ts
203
+ import type { DevToolsClientScriptContext } from '@vitejs/devtools-kit/client'
204
+
205
+ export default function setup(ctx: DevToolsClientScriptContext) {
206
+ ctx.current.events.on('entry:activated', async () => {
207
+ const data = await ctx.current.rpc.call('my-plugin:get-data')
208
+ })
209
+ }
210
+ ```
211
+
212
+ ### Broadcasting to Clients
213
+
214
+ ```ts
215
+ // Server broadcasts to all clients
216
+ ctx.rpc.broadcast({
217
+ method: 'my-plugin:on-update',
218
+ args: [{ changedFile: '/src/main.ts' }],
219
+ })
220
+ ```
221
+
222
+ ## Type Safety
223
+
224
+ Extend the DevTools Kit interfaces for full type checking:
225
+
226
+ ```ts
227
+ // src/types.ts
228
+ import '@vitejs/devtools-kit'
229
+
230
+ declare module '@vitejs/devtools-kit' {
231
+ interface DevToolsRpcServerFunctions {
232
+ 'my-plugin:get-modules': (filter?: string) => Promise<Module[]>
233
+ }
234
+
235
+ interface DevToolsRpcClientFunctions {
236
+ 'my-plugin:on-update': (data: { changedFile: string }) => void
237
+ }
238
+
239
+ interface DevToolsRpcSharedStates {
240
+ 'my-plugin:state': MyPluginState
241
+ }
242
+ }
243
+ ```
244
+
245
+ ## Shared State
246
+
247
+ ### Server-Side
248
+
249
+ ```ts
250
+ const state = await ctx.rpc.sharedState.get('my-plugin:state', {
251
+ initialValue: { count: 0, items: [] },
252
+ })
253
+
254
+ // Read
255
+ console.log(state.value())
256
+
257
+ // Mutate (auto-syncs to clients)
258
+ state.mutate((draft) => {
259
+ draft.count += 1
260
+ draft.items.push('new item')
261
+ })
262
+ ```
263
+
264
+ ### Client-Side
265
+
266
+ ```ts
267
+ const client = await getDevToolsRpcClient()
268
+ const state = await client.rpc.sharedState.get('my-plugin:state')
269
+
270
+ // Read
271
+ console.log(state.value())
272
+
273
+ // Subscribe to changes
274
+ state.on('updated', (newState) => {
275
+ console.log('State updated:', newState)
276
+ })
277
+ ```
278
+
279
+ ## Client Scripts
280
+
281
+ For action buttons and custom renderers:
282
+
283
+ ```ts
284
+ // src/devtools-action.ts
285
+ import type { DevToolsClientScriptContext } from '@vitejs/devtools-kit/client'
286
+
287
+ export default function setup(ctx: DevToolsClientScriptContext) {
288
+ ctx.current.events.on('entry:activated', () => {
289
+ console.log('Action activated')
290
+ // Your inspector/tool logic here
291
+ })
292
+
293
+ ctx.current.events.on('entry:deactivated', () => {
294
+ console.log('Action deactivated')
295
+ // Cleanup
296
+ })
297
+ }
298
+ ```
299
+
300
+ Export from package.json:
301
+
302
+ ```json
303
+ {
304
+ "exports": {
305
+ ".": "./dist/index.mjs",
306
+ "./devtools-action": "./dist/devtools-action.mjs"
307
+ }
308
+ }
309
+ ```
310
+
311
+ ## Best Practices
312
+
313
+ 1. **Always namespace** - Prefix all identifiers with your plugin name
314
+ 2. **Use type augmentation** - Extend `DevToolsRpcServerFunctions` for type-safe RPC
315
+ 3. **Keep state serializable** - No functions or circular references in shared state
316
+ 4. **Batch mutations** - Use single `mutate()` call for multiple changes
317
+ 5. **Host static files** - Use `ctx.views.hostStatic()` for your UI assets
318
+ 6. **Use Iconify icons** - Prefer `ph:*` (Phosphor) icons: `icon: 'ph:chart-bar-duotone'`
319
+
320
+ ## Further Reading
321
+
322
+ - [RPC Patterns](./references/rpc-patterns.md) - Advanced RPC patterns and type utilities
323
+ - [Dock Entry Types](./references/dock-entry-types.md) - Detailed dock configuration options
324
+ - [Shared State Patterns](./references/shared-state-patterns.md) - Framework integration examples
325
+ - [Project Structure](./references/project-structure.md) - Recommended file organization
@@ -0,0 +1,240 @@
1
+ # Dock Entry Types
2
+
3
+ Detailed configuration for each dock entry type.
4
+
5
+ ## Common Properties
6
+
7
+ All dock entries share these properties:
8
+
9
+ ```ts
10
+ interface DockEntryBase {
11
+ id: string // Unique identifier
12
+ title: string // Display title
13
+ icon: string // URL, data URI, or Iconify name
14
+ category?: string // Grouping category
15
+ defaultOrder?: number // Sort order (higher = earlier)
16
+ isHidden?: boolean // Hide from dock
17
+ }
18
+ ```
19
+
20
+ ## Icons
21
+
22
+ <!-- eslint-skip -->
23
+
24
+ ```ts
25
+ // Iconify (recommended)
26
+ icon: 'ph:chart-bar-duotone' // Phosphor Icons
27
+ icon: 'carbon:analytics' // Carbon Icons
28
+ icon: 'mdi:view-dashboard' // Material Design
29
+
30
+ // URL
31
+ icon: 'https://example.com/logo.svg'
32
+
33
+ // Data URI
34
+ icon: 'data:image/svg+xml,<svg>...</svg>'
35
+
36
+ // Light/dark variants
37
+ icon: {
38
+ light: 'https://example.com/logo-light.svg',
39
+ dark: 'https://example.com/logo-dark.svg',
40
+ }
41
+ ```
42
+
43
+ Browse icons at [Iconify](https://icon-sets.iconify.design/).
44
+
45
+ ## Iframe Entries
46
+
47
+ Most common type. Displays your UI in an isolated iframe.
48
+
49
+ ```ts
50
+ interface IframeEntry extends DockEntryBase {
51
+ type: 'iframe'
52
+ url: string // URL to load
53
+ frameId?: string // Share iframe between entries
54
+ clientScript?: ClientScriptEntry // Optional client script
55
+ }
56
+
57
+ // Example
58
+ ctx.docks.register({
59
+ id: 'my-plugin',
60
+ title: 'My Plugin',
61
+ icon: 'ph:house-duotone',
62
+ type: 'iframe',
63
+ url: '/.my-plugin/',
64
+ })
65
+ ```
66
+
67
+ ### Hosting Your Own UI
68
+
69
+ ```ts
70
+ import { fileURLToPath } from 'node:url'
71
+
72
+ const clientDist = fileURLToPath(
73
+ new URL('../dist/client', import.meta.url)
74
+ )
75
+
76
+ ctx.views.hostStatic('/.my-plugin/', clientDist)
77
+
78
+ ctx.docks.register({
79
+ id: 'my-plugin',
80
+ title: 'My Plugin',
81
+ icon: 'ph:house-duotone',
82
+ type: 'iframe',
83
+ url: '/.my-plugin/',
84
+ })
85
+ ```
86
+
87
+ ## Action Entries
88
+
89
+ Buttons that trigger client-side scripts. Perfect for inspectors and toggles.
90
+
91
+ ```ts
92
+ interface ActionEntry extends DockEntryBase {
93
+ type: 'action'
94
+ action: {
95
+ importFrom: string // Package export path
96
+ importName?: string // Export name (default: 'default')
97
+ }
98
+ }
99
+
100
+ // Registration
101
+ ctx.docks.register({
102
+ id: 'my-inspector',
103
+ title: 'Inspector',
104
+ icon: 'ph:cursor-duotone',
105
+ type: 'action',
106
+ action: {
107
+ importFrom: 'my-plugin/devtools-action',
108
+ importName: 'default',
109
+ },
110
+ })
111
+ ```
112
+
113
+ ### Client Script Implementation
114
+
115
+ ```ts
116
+ // src/devtools-action.ts
117
+ import type { DevToolsClientScriptContext } from '@vitejs/devtools-kit/client'
118
+
119
+ export default function setup(ctx: DevToolsClientScriptContext) {
120
+ let overlay: HTMLElement | null = null
121
+
122
+ ctx.current.events.on('entry:activated', () => {
123
+ overlay = document.createElement('div')
124
+ overlay.style.cssText = `
125
+ position: fixed;
126
+ inset: 0;
127
+ cursor: crosshair;
128
+ z-index: 99999;
129
+ `
130
+ overlay.onclick = (e) => {
131
+ const target = document.elementFromPoint(e.clientX, e.clientY)
132
+ console.log('Selected:', target)
133
+ }
134
+ document.body.appendChild(overlay)
135
+ })
136
+
137
+ ctx.current.events.on('entry:deactivated', () => {
138
+ overlay?.remove()
139
+ overlay = null
140
+ })
141
+ }
142
+ ```
143
+
144
+ ### Package Export
145
+
146
+ ```json
147
+ {
148
+ "exports": {
149
+ ".": "./dist/index.mjs",
150
+ "./devtools-action": "./dist/devtools-action.mjs"
151
+ }
152
+ }
153
+ ```
154
+
155
+ ## Custom Render Entries
156
+
157
+ Render directly into the DevTools panel DOM. Use when you need direct DOM access or framework mounting.
158
+
159
+ ```ts
160
+ interface CustomRenderEntry extends DockEntryBase {
161
+ type: 'custom-render'
162
+ renderer: {
163
+ importFrom: string
164
+ importName?: string
165
+ }
166
+ }
167
+
168
+ ctx.docks.register({
169
+ id: 'my-custom',
170
+ title: 'Custom View',
171
+ icon: 'ph:code-duotone',
172
+ type: 'custom-render',
173
+ renderer: {
174
+ importFrom: 'my-plugin/devtools-renderer',
175
+ importName: 'default',
176
+ },
177
+ })
178
+ ```
179
+
180
+ ### Renderer Implementation
181
+
182
+ ```ts
183
+ // src/devtools-renderer.ts
184
+ import type { DevToolsClientScriptContext } from '@vitejs/devtools-kit/client'
185
+
186
+ export default function setup(ctx: DevToolsClientScriptContext) {
187
+ ctx.current.events.on('dom:panel:mounted', (panel) => {
188
+ // Vanilla JS
189
+ panel.innerHTML = `<div style="padding: 16px;">Hello</div>`
190
+
191
+ // Or mount Vue
192
+ // import { createApp } from 'vue'
193
+ // import App from './App.vue'
194
+ // createApp(App).mount(panel)
195
+
196
+ // Or mount React
197
+ // import { createRoot } from 'react-dom/client'
198
+ // createRoot(panel).render(<App />)
199
+ })
200
+ }
201
+ ```
202
+
203
+ ## Client Script Events
204
+
205
+ | Event | Payload | Description |
206
+ |-------|---------|-------------|
207
+ | `entry:activated` | - | Entry was selected |
208
+ | `entry:deactivated` | - | Entry was deselected |
209
+ | `entry:updated` | `DevToolsDockUserEntry` | Entry metadata changed |
210
+ | `dom:panel:mounted` | `HTMLDivElement` | Panel DOM ready (custom-render only) |
211
+ | `dom:iframe:mounted` | `HTMLIFrameElement` | Iframe mounted (iframe only) |
212
+
213
+ ## Category Order
214
+
215
+ Default category ordering:
216
+
217
+ ```ts
218
+ DEFAULT_CATEGORIES_ORDER = {
219
+ '~viteplus': -1000, // First
220
+ 'default': 0,
221
+ 'app': 100,
222
+ 'framework': 200,
223
+ 'web': 300,
224
+ 'advanced': 400,
225
+ '~builtin': 1000, // Last
226
+ }
227
+ ```
228
+
229
+ Use `category` to group related entries:
230
+
231
+ ```ts
232
+ ctx.docks.register({
233
+ id: 'my-plugin',
234
+ title: 'My Plugin',
235
+ icon: 'ph:house-duotone',
236
+ type: 'iframe',
237
+ url: '/.my-plugin/',
238
+ category: 'framework',
239
+ })
240
+ ```
@@ -0,0 +1,247 @@
1
+ # Project Structure
2
+
3
+ Recommended file organization for DevTools integrations.
4
+
5
+ ## Basic Structure
6
+
7
+ ```
8
+ my-devtools-plugin/
9
+ ├── src/
10
+ │ ├── node/
11
+ │ │ ├── index.ts # Plugin entry (exports main plugin)
12
+ │ │ ├── rpc/
13
+ │ │ │ ├── index.ts # RPC function exports
14
+ │ │ │ └── functions/ # Individual RPC functions
15
+ │ │ │ ├── get-modules.ts
16
+ │ │ │ └── get-stats.ts
17
+ │ │ └── utils.ts # Server-side utilities
18
+ │ ├── client/
19
+ │ │ ├── main.ts # Client app entry
20
+ │ │ ├── App.vue # Root component
21
+ │ │ └── composables/
22
+ │ │ └── rpc.ts # RPC composables
23
+ │ ├── types.ts # Type augmentations
24
+ │ └── shared/
25
+ │ └── constants.ts # Shared constants
26
+ ├── dist/
27
+ │ ├── index.mjs # Node plugin bundle
28
+ │ └── client/ # Built client assets
29
+ ├── package.json
30
+ └── tsconfig.json
31
+ ```
32
+
33
+ ## Package.json Configuration
34
+
35
+ ```json
36
+ {
37
+ "name": "my-devtools-plugin",
38
+ "type": "module",
39
+ "exports": {
40
+ ".": {
41
+ "import": "./dist/index.mjs",
42
+ "types": "./dist/index.d.ts"
43
+ },
44
+ "./devtools-action": {
45
+ "import": "./dist/devtools-action.mjs"
46
+ }
47
+ },
48
+ "files": ["dist"],
49
+ "scripts": {
50
+ "build": "tsdown src/node/index.ts && vite build src/client",
51
+ "dev": "vite src/client"
52
+ },
53
+ "dependencies": {},
54
+ "devDependencies": {
55
+ "@vitejs/devtools-kit": "^0.x.x",
56
+ "vite": "^6.x.x"
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## Plugin Entry (src/node/index.ts)
62
+
63
+ ```ts
64
+ /// <reference types="@vitejs/devtools-kit" />
65
+ import type { Plugin } from 'vite'
66
+ import { fileURLToPath } from 'node:url'
67
+ import { rpcFunctions } from './rpc'
68
+ import '../types'
69
+
70
+ const clientDist = fileURLToPath(
71
+ new URL('../../dist/client', import.meta.url)
72
+ )
73
+
74
+ export default function myPlugin(): Plugin {
75
+ return {
76
+ name: 'my-plugin',
77
+
78
+ devtools: {
79
+ setup(ctx) {
80
+ // Register all RPC functions
81
+ for (const fn of rpcFunctions) {
82
+ ctx.rpc.register(fn)
83
+ }
84
+
85
+ // Host static UI
86
+ ctx.views.hostStatic('/.my-plugin/', clientDist)
87
+
88
+ // Register dock entry
89
+ ctx.docks.register({
90
+ id: 'my-plugin',
91
+ title: 'My Plugin',
92
+ icon: 'ph:puzzle-piece-duotone',
93
+ type: 'iframe',
94
+ url: '/.my-plugin/',
95
+ })
96
+ },
97
+ },
98
+ }
99
+ }
100
+ ```
101
+
102
+ ## RPC Index (src/node/rpc/index.ts)
103
+
104
+ ```ts
105
+ import type { RpcDefinitionsToFunctions } from '@vitejs/devtools-kit'
106
+ import { getModules } from './functions/get-modules'
107
+ import { getStats } from './functions/get-stats'
108
+ import '@vitejs/devtools-kit'
109
+
110
+ export const rpcFunctions = [
111
+ getModules,
112
+ getStats,
113
+ ] as const
114
+
115
+ export type ServerFunctions = RpcDefinitionsToFunctions<typeof rpcFunctions>
116
+
117
+ declare module '@vitejs/devtools-kit' {
118
+ export interface DevToolsRpcServerFunctions extends ServerFunctions {}
119
+ }
120
+ ```
121
+
122
+ ## RPC Function (src/node/rpc/functions/get-modules.ts)
123
+
124
+ ```ts
125
+ import type { Module } from '../../../types'
126
+ import { defineRpcFunction } from '@vitejs/devtools-kit'
127
+
128
+ export const getModules = defineRpcFunction({
129
+ name: 'my-plugin:get-modules',
130
+ type: 'query',
131
+ setup: (ctx) => {
132
+ return {
133
+ handler: async (): Promise<Module[]> => {
134
+ // Access vite config, server, etc. from ctx
135
+ const root = ctx.viteConfig.root
136
+ return []
137
+ },
138
+ }
139
+ },
140
+ })
141
+ ```
142
+
143
+ ## Type Augmentations (src/types.ts)
144
+
145
+ ```ts
146
+ import '@vitejs/devtools-kit'
147
+
148
+ export interface Module {
149
+ id: string
150
+ size: number
151
+ imports: string[]
152
+ }
153
+
154
+ export interface MyPluginState {
155
+ modules: Module[]
156
+ selectedId: string | null
157
+ }
158
+
159
+ declare module '@vitejs/devtools-kit' {
160
+ interface DevToolsRpcSharedStates {
161
+ 'my-plugin:state': MyPluginState
162
+ }
163
+ }
164
+ ```
165
+
166
+ ## Client Entry (src/client/main.ts)
167
+
168
+ ```ts
169
+ import { createApp } from 'vue'
170
+ import App from './App.vue'
171
+
172
+ createApp(App).mount('#app')
173
+ ```
174
+
175
+ ## Client RPC Composable (src/client/composables/rpc.ts)
176
+
177
+ ```ts
178
+ import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
179
+
180
+ let clientPromise: Promise<Awaited<ReturnType<typeof getDevToolsRpcClient>>>
181
+
182
+ export function useRpc() {
183
+ if (!clientPromise) {
184
+ clientPromise = getDevToolsRpcClient()
185
+ }
186
+ return clientPromise
187
+ }
188
+ ```
189
+
190
+ ## Client App Component (src/client/App.vue)
191
+
192
+ ```vue
193
+ <script setup lang="ts">
194
+ import type { Module } from '../types'
195
+ import { onMounted, shallowRef } from 'vue'
196
+ import { useRpc } from './composables/rpc'
197
+
198
+ const modules = shallowRef<Module[]>([])
199
+ const loading = shallowRef(true)
200
+
201
+ onMounted(async () => {
202
+ const rpc = await useRpc()
203
+ modules.value = await rpc.call('my-plugin:get-modules')
204
+ loading.value = false
205
+ })
206
+ </script>
207
+
208
+ <template>
209
+ <div class="p-4">
210
+ <h1 class="text-xl font-bold mb-4">
211
+ My Plugin
212
+ </h1>
213
+ <div v-if="loading">
214
+ Loading...
215
+ </div>
216
+ <ul v-else>
217
+ <li v-for="mod in modules" :key="mod.id">
218
+ {{ mod.id }} ({{ mod.size }} bytes)
219
+ </li>
220
+ </ul>
221
+ </div>
222
+ </template>
223
+ ```
224
+
225
+ ## Vite Config for Client (src/client/vite.config.ts)
226
+
227
+ ```ts
228
+ import vue from '@vitejs/plugin-vue'
229
+ import { defineConfig } from 'vite'
230
+
231
+ export default defineConfig({
232
+ plugins: [vue()],
233
+ build: {
234
+ outDir: '../../dist/client',
235
+ emptyOutDir: true,
236
+ },
237
+ })
238
+ ```
239
+
240
+ ## Real-World Reference
241
+
242
+ See [packages/vite](https://github.com/user/vite-devtools/tree/main/packages/vite) for a complete implementation example with:
243
+
244
+ - Multiple RPC functions organized by feature
245
+ - Nuxt-based client UI
246
+ - Complex data visualization
247
+ - Build session management
@@ -0,0 +1,185 @@
1
+ # RPC Patterns
2
+
3
+ Advanced patterns for server-client communication in DevTools integrations.
4
+
5
+ ## Function Types
6
+
7
+ | Type | Caching | Use Case |
8
+ |------|---------|----------|
9
+ | `query` | Can be cached | Read operations, data fetching |
10
+ | `action` | Never cached | Mutations, side effects |
11
+ | `static` | Cached indefinitely | Constants, configuration |
12
+
13
+ ## Type-Safe RPC Setup
14
+
15
+ ### Step 1: Define Types
16
+
17
+ ```ts
18
+ // src/types.ts
19
+ import '@vitejs/devtools-kit'
20
+
21
+ interface Module {
22
+ id: string
23
+ size: number
24
+ imports: string[]
25
+ }
26
+
27
+ declare module '@vitejs/devtools-kit' {
28
+ interface DevToolsRpcServerFunctions {
29
+ 'my-plugin:list-modules': () => Promise<Module[]>
30
+ 'my-plugin:get-module': (id: string) => Promise<Module | null>
31
+ 'my-plugin:analyze': (options: { deep: boolean }) => Promise<void>
32
+ }
33
+
34
+ interface DevToolsRpcClientFunctions {
35
+ 'my-plugin:highlight': (selector: string) => void
36
+ 'my-plugin:refresh': () => void
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### Step 2: Import Types File
42
+
43
+ ```ts
44
+ // src/node/plugin.ts
45
+ import '../types' // Side-effect import for type augmentation
46
+ ```
47
+
48
+ ### Step 3: Register Functions
49
+
50
+ ```ts
51
+ import { defineRpcFunction } from '@vitejs/devtools-kit'
52
+
53
+ const listModules = defineRpcFunction({
54
+ name: 'my-plugin:list-modules',
55
+ type: 'query',
56
+ setup: () => ({
57
+ handler: async (): Promise<Module[]> => {
58
+ return Array.from(moduleMap.values())
59
+ },
60
+ }),
61
+ })
62
+ ```
63
+
64
+ ## Context Access in Setup
65
+
66
+ The `setup` function receives the full `DevToolsNodeContext`:
67
+
68
+ ```ts
69
+ defineRpcFunction({
70
+ name: 'my-plugin:get-config',
71
+ type: 'static',
72
+ setup: (ctx) => {
73
+ // Access at setup time (runs once)
74
+ const root = ctx.viteConfig.root
75
+ const isDev = ctx.mode === 'dev'
76
+
77
+ return {
78
+ handler: async () => ({
79
+ root,
80
+ isDev,
81
+ plugins: ctx.viteConfig.plugins.map(p => p.name),
82
+ }),
83
+ }
84
+ },
85
+ })
86
+ ```
87
+
88
+ ## Broadcasting Patterns
89
+
90
+ ### Basic Broadcast
91
+
92
+ ```ts
93
+ // Notify all clients
94
+ ctx.rpc.broadcast({
95
+ method: 'my-plugin:refresh',
96
+ args: [],
97
+ })
98
+ ```
99
+
100
+ ### Broadcast with Data
101
+
102
+ ```ts
103
+ ctx.viteServer?.watcher.on('change', (file) => {
104
+ ctx.rpc.broadcast({
105
+ method: 'my-plugin:file-changed',
106
+ args: [{ path: file, timestamp: Date.now() }],
107
+ })
108
+ })
109
+ ```
110
+
111
+ ### Optional Broadcast
112
+
113
+ ```ts
114
+ // Won't error if no clients have registered the function
115
+ ctx.rpc.broadcast({
116
+ method: 'my-plugin:optional-update',
117
+ args: [data],
118
+ optional: true,
119
+ })
120
+ ```
121
+
122
+ ## Client Function Registration
123
+
124
+ ```ts
125
+ // Client-side (action/renderer script)
126
+ export default function setup(ctx: DevToolsClientScriptContext) {
127
+ ctx.current.rpc.client.register({
128
+ name: 'my-plugin:highlight',
129
+ type: 'action',
130
+ handler: (selector: string) => {
131
+ const el = document.querySelector(selector)
132
+ if (el) {
133
+ el.style.outline = '3px solid red'
134
+ setTimeout(() => {
135
+ el.style.outline = ''
136
+ }, 2000)
137
+ }
138
+ },
139
+ })
140
+ }
141
+ ```
142
+
143
+ ## Collecting RPC Functions
144
+
145
+ Organize RPC functions in a registry pattern:
146
+
147
+ ```ts
148
+ import { analyzeBundle } from './functions/analyze-bundle'
149
+ // src/node/rpc/index.ts
150
+ import { getModules } from './functions/get-modules'
151
+ import { getStats } from './functions/get-stats'
152
+
153
+ export const rpcFunctions = [
154
+ getModules,
155
+ getStats,
156
+ analyzeBundle,
157
+ ] as const
158
+
159
+ // Register all in setup
160
+ for (const fn of rpcFunctions) {
161
+ ctx.rpc.register(fn)
162
+ }
163
+ ```
164
+
165
+ ## Type Extraction Utilities
166
+
167
+ ```ts
168
+ import type {
169
+ RpcDefinitionsFilter,
170
+ RpcDefinitionsToFunctions,
171
+ } from '@vitejs/devtools-kit'
172
+
173
+ // Extract all function types
174
+ export type ServerFunctions = RpcDefinitionsToFunctions<typeof rpcFunctions>
175
+
176
+ // Extract only static functions
177
+ export type StaticFunctions = RpcDefinitionsToFunctions<
178
+ RpcDefinitionsFilter<typeof rpcFunctions, 'static'>
179
+ >
180
+
181
+ // Augment the global interface
182
+ declare module '@vitejs/devtools-kit' {
183
+ export interface DevToolsRpcServerFunctions extends ServerFunctions {}
184
+ }
185
+ ```
@@ -0,0 +1,290 @@
1
+ # Shared State Patterns
2
+
3
+ Synchronized state between server and all clients.
4
+
5
+ ## Basic Usage
6
+
7
+ ### Server-Side
8
+
9
+ ```ts
10
+ const state = await ctx.rpc.sharedState.get('my-plugin:state', {
11
+ initialValue: {
12
+ count: 0,
13
+ items: [],
14
+ settings: { theme: 'dark' },
15
+ },
16
+ })
17
+
18
+ // Read
19
+ const current = state.value()
20
+
21
+ // Mutate (syncs to all clients)
22
+ state.mutate((draft) => {
23
+ draft.count += 1
24
+ draft.items.push({ id: Date.now(), name: 'New' })
25
+ })
26
+ ```
27
+
28
+ ### Client-Side
29
+
30
+ ```ts
31
+ import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
32
+
33
+ const client = await getDevToolsRpcClient()
34
+ const state = await client.rpc.sharedState.get('my-plugin:state')
35
+
36
+ // Read
37
+ console.log(state.value())
38
+
39
+ // Subscribe
40
+ state.on('updated', (newState) => {
41
+ console.log('Updated:', newState)
42
+ })
43
+ ```
44
+
45
+ ## Type-Safe Shared State
46
+
47
+ ```ts
48
+ // src/types.ts
49
+ interface MyPluginState {
50
+ count: number
51
+ items: Array<{ id: string, name: string }>
52
+ settings: {
53
+ theme: 'light' | 'dark'
54
+ notifications: boolean
55
+ }
56
+ }
57
+
58
+ declare module '@vitejs/devtools-kit' {
59
+ interface DevToolsRpcSharedStates {
60
+ 'my-plugin:state': MyPluginState
61
+ }
62
+ }
63
+ ```
64
+
65
+ ## Vue Integration
66
+
67
+ ```ts
68
+ // composables/useSharedState.ts
69
+ import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
70
+ import { onMounted, shallowRef } from 'vue'
71
+
72
+ export function useSharedState<T>(name: string) {
73
+ const state = shallowRef<T | null>(null)
74
+ const loading = shallowRef(true)
75
+
76
+ onMounted(async () => {
77
+ const client = await getDevToolsRpcClient()
78
+ const shared = await client.rpc.sharedState.get<T>(name)
79
+
80
+ state.value = shared.value()
81
+ loading.value = false
82
+
83
+ shared.on('updated', (newState) => {
84
+ state.value = newState
85
+ })
86
+ })
87
+
88
+ return { state, loading }
89
+ }
90
+
91
+ // Usage in component
92
+ const { state, loading } = useSharedState<MyPluginState>('my-plugin:state')
93
+ ```
94
+
95
+ ### Full Vue Component Example
96
+
97
+ ```vue
98
+ <script setup lang="ts">
99
+ import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
100
+ import { onMounted, shallowRef } from 'vue'
101
+
102
+ interface PluginState {
103
+ count: number
104
+ items: string[]
105
+ }
106
+
107
+ const state = shallowRef<PluginState | null>(null)
108
+
109
+ onMounted(async () => {
110
+ const client = await getDevToolsRpcClient()
111
+ const shared = await client.rpc.sharedState.get<PluginState>('my-plugin:state')
112
+
113
+ state.value = shared.value()
114
+
115
+ shared.on('updated', (newState) => {
116
+ state.value = newState
117
+ })
118
+ })
119
+ </script>
120
+
121
+ <template>
122
+ <div v-if="state">
123
+ <p>Count: {{ state.count }}</p>
124
+ <ul>
125
+ <li v-for="item in state.items" :key="item">
126
+ {{ item }}
127
+ </li>
128
+ </ul>
129
+ </div>
130
+ </template>
131
+ ```
132
+
133
+ ## React Integration
134
+
135
+ ```tsx
136
+ import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
137
+ import { useEffect, useState } from 'react'
138
+
139
+ function useSharedState<T>(name: string, fallback: T) {
140
+ const [state, setState] = useState<T>(fallback)
141
+ const [loading, setLoading] = useState(true)
142
+
143
+ useEffect(() => {
144
+ let mounted = true
145
+
146
+ async function init() {
147
+ const client = await getDevToolsRpcClient()
148
+ const shared = await client.rpc.sharedState.get<T>(name)
149
+
150
+ if (mounted) {
151
+ setState(shared.value() ?? fallback)
152
+ setLoading(false)
153
+
154
+ shared.on('updated', (newState) => {
155
+ if (mounted)
156
+ setState(newState)
157
+ })
158
+ }
159
+ }
160
+
161
+ init()
162
+ return () => {
163
+ mounted = false
164
+ }
165
+ }, [name])
166
+
167
+ return { state, loading }
168
+ }
169
+
170
+ // Usage
171
+ function MyComponent() {
172
+ const { state, loading } = useSharedState('my-plugin:state', { count: 0 })
173
+
174
+ if (loading)
175
+ return <div>Loading...</div>
176
+
177
+ return (
178
+ <div>
179
+ Count:
180
+ {state.count}
181
+ </div>
182
+ )
183
+ }
184
+ ```
185
+
186
+ ## Svelte Integration
187
+
188
+ ```svelte
189
+ <script lang="ts">
190
+ import { onMount } from 'svelte'
191
+ import { writable } from 'svelte/store'
192
+ import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
193
+
194
+ interface PluginState {
195
+ count: number
196
+ }
197
+
198
+ const state = writable<PluginState>({ count: 0 })
199
+
200
+ onMount(async () => {
201
+ const client = await getDevToolsRpcClient()
202
+ const shared = await client.rpc.sharedState.get<PluginState>('my-plugin:state')
203
+
204
+ state.set(shared.value())
205
+
206
+ shared.on('updated', (newState) => {
207
+ state.set(newState)
208
+ })
209
+ })
210
+ </script>
211
+
212
+ <p>Count: {$state.count}</p>
213
+ ```
214
+
215
+ ## Best Practices
216
+
217
+ ### Batch Mutations
218
+
219
+ ```ts
220
+ // Good - single sync event
221
+ state.mutate((draft) => {
222
+ draft.count += 1
223
+ draft.lastUpdate = Date.now()
224
+ draft.items.push(newItem)
225
+ })
226
+
227
+ // Bad - multiple sync events
228
+ state.mutate((d) => {
229
+ d.count += 1
230
+ })
231
+ state.mutate((d) => {
232
+ d.lastUpdate = Date.now()
233
+ })
234
+ state.mutate((d) => {
235
+ d.items.push(newItem)
236
+ })
237
+ ```
238
+
239
+ ### Keep State Small
240
+
241
+ For large datasets, store IDs in shared state and fetch details via RPC:
242
+
243
+ <!-- eslint-skip -->
244
+ ```ts
245
+ // Shared state (small)
246
+ {
247
+ moduleIds: ['a', 'b', 'c'],
248
+ selectedId: 'a'
249
+ }
250
+
251
+ // Fetch full data via RPC when needed
252
+ const module = await rpc.call('my-plugin:get-module', state.selectedId)
253
+ ```
254
+
255
+ ### Serializable Data Only
256
+
257
+ <!-- eslint-skip -->
258
+ ```ts
259
+ // Bad - functions can't be serialized
260
+ { count: 0, increment: () => {} }
261
+
262
+ // Bad - circular references
263
+ const obj = { child: null }
264
+ obj.child = obj
265
+
266
+ // Good - plain data
267
+ { count: 0, items: [{ id: 1 }] }
268
+ ```
269
+
270
+ ### Real-Time Updates Example
271
+
272
+ ```ts
273
+ const plugin: Plugin = {
274
+ devtools: {
275
+ async setup(ctx) {
276
+ const state = await ctx.rpc.sharedState.get('my-plugin:state', {
277
+ initialValue: { modules: [], lastUpdate: 0 },
278
+ })
279
+
280
+ // Update state when Vite processes modules
281
+ ctx.viteServer?.watcher.on('change', (file) => {
282
+ state.mutate((draft) => {
283
+ draft.modules.push(file)
284
+ draft.lastUpdate = Date.now()
285
+ })
286
+ })
287
+ }
288
+ }
289
+ }
290
+ ```