@vitejs/devtools-kit 0.0.0-alpha.28 → 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 +4 -3
- package/skills/vite-devtools-kit/SKILL.md +325 -0
- package/skills/vite-devtools-kit/references/dock-entry-types.md +240 -0
- package/skills/vite-devtools-kit/references/project-structure.md +247 -0
- package/skills/vite-devtools-kit/references/rpc-patterns.md +185 -0
- package/skills/vite-devtools-kit/references/shared-state-patterns.md +290 -0
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.
|
|
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.
|
|
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
|
+
```
|