@vitejs/devtools-kit 0.0.0-alpha.9 → 0.1.1

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/dist/index.js CHANGED
@@ -1,17 +1,10 @@
1
- //#region src/utils/rpc.ts
2
- function defineRpcFunction(definition) {
3
- return definition;
4
- }
5
- async function getRpcHandler(definition, context) {
6
- if (definition.handler) return definition.handler;
7
- if (definition.__resolved?.handler) return definition.__resolved.handler;
8
- definition.__promise ??= Promise.resolve(definition.setup(context)).then((r) => {
9
- definition.__resolved = r;
10
- definition.__promise = void 0;
11
- return r;
12
- });
13
- return (definition.__resolved ??= await definition.__promise).handler;
1
+ import { createDefineWrapperWithContext } from "@vitejs/devtools-rpc";
2
+ //#region src/utils/define.ts
3
+ const defineRpcFunction = createDefineWrapperWithContext();
4
+ //#endregion
5
+ //#region src/utils/json-render.ts
6
+ function defineJsonRenderSpec(spec) {
7
+ return spec;
14
8
  }
15
-
16
9
  //#endregion
17
- export { defineRpcFunction, getRpcHandler };
10
+ export { defineJsonRenderSpec, defineRpcFunction };
@@ -0,0 +1,53 @@
1
+ import { t as EventEmitter } from "./events-B41U-zeg.js";
2
+ import { Objectish, Patch, Patch as SharedStatePatch } from "immer";
3
+
4
+ //#region src/utils/shared-state.d.ts
5
+ type ImmutablePrimitive = undefined | null | boolean | string | number | Function;
6
+ type Immutable<T> = T extends ImmutablePrimitive ? T : T extends Array<infer U> ? ImmutableArray<U> : T extends Map<infer K, infer V> ? ImmutableMap<K, V> : T extends Set<infer M> ? ImmutableSet<M> : ImmutableObject<T>;
7
+ type ImmutableArray<T> = ReadonlyArray<Immutable<T>>;
8
+ type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>;
9
+ type ImmutableSet<T> = ReadonlySet<Immutable<T>>;
10
+ type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> };
11
+ /**
12
+ * State host that is immutable by default with explicit mutate.
13
+ */
14
+ interface SharedState<T> {
15
+ /**
16
+ * Get the current state. Immutable.
17
+ */
18
+ value: () => Immutable<T>;
19
+ /**
20
+ * Subscribe to state changes.
21
+ */
22
+ on: EventEmitter<SharedStateEvents<T>>['on'];
23
+ /**
24
+ * Mutate the state.
25
+ */
26
+ mutate: (fn: (state: T) => void, syncId?: string) => void;
27
+ /**
28
+ * Apply patches to the state.
29
+ */
30
+ patch: (patches: Patch[], syncId?: string) => void;
31
+ /**
32
+ * Sync IDs that have been applied to the state.
33
+ */
34
+ syncIds: Set<string>;
35
+ }
36
+ interface SharedStateEvents<T> {
37
+ updated: (fullState: T, patches: Patch[] | undefined, syncId: string) => void;
38
+ }
39
+ interface SharedStateOptions<T> {
40
+ /**
41
+ * Initial state.
42
+ */
43
+ initialValue: T;
44
+ /**
45
+ * Enable patches.
46
+ *
47
+ * @default false
48
+ */
49
+ enablePatches?: boolean;
50
+ }
51
+ declare function createSharedState<T extends Objectish>(options: SharedStateOptions<T>): SharedState<T>;
52
+ //#endregion
53
+ export { ImmutableSet as a, SharedStateOptions as c, ImmutableObject as i, SharedStatePatch as l, ImmutableArray as n, SharedState as o, ImmutableMap as r, SharedStateEvents as s, Immutable as t, createSharedState as u };
@@ -0,0 +1,9 @@
1
+ import { r as EventsMap, t as EventEmitter } from "../events-B41U-zeg.js";
2
+
3
+ //#region src/utils/events.d.ts
4
+ /**
5
+ * Create event emitter.
6
+ */
7
+ declare function createEventEmitter<Events extends EventsMap>(): EventEmitter<Events>;
8
+ //#endregion
9
+ export { createEventEmitter };
@@ -0,0 +1,40 @@
1
+ //#region src/utils/events.ts
2
+ /**
3
+ * Create event emitter.
4
+ */
5
+ function createEventEmitter() {
6
+ const _listeners = {};
7
+ function emit(event, ...args) {
8
+ const callbacks = _listeners[event] || [];
9
+ for (let i = 0, length = callbacks.length; i < length; i++) {
10
+ const callback = callbacks[i];
11
+ if (callback) callback(...args);
12
+ }
13
+ }
14
+ function emitOnce(event, ...args) {
15
+ emit(event, ...args);
16
+ delete _listeners[event];
17
+ }
18
+ function on(event, cb) {
19
+ (_listeners[event] ||= []).push(cb);
20
+ return () => {
21
+ _listeners[event] = _listeners[event]?.filter((i) => cb !== i);
22
+ };
23
+ }
24
+ function once(event, cb) {
25
+ const unsubscribe = on(event, ((...args) => {
26
+ unsubscribe();
27
+ return cb(...args);
28
+ }));
29
+ return unsubscribe;
30
+ }
31
+ return {
32
+ _listeners,
33
+ emit,
34
+ emitOnce,
35
+ on,
36
+ once
37
+ };
38
+ }
39
+ //#endregion
40
+ export { createEventEmitter };
@@ -0,0 +1,4 @@
1
+ //#region src/utils/nanoid.d.ts
2
+ declare function nanoid(size?: number): string;
3
+ //#endregion
4
+ export { nanoid };
@@ -0,0 +1,10 @@
1
+ //#region src/utils/nanoid.ts
2
+ const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
3
+ function nanoid(size = 21) {
4
+ let id = "";
5
+ let i = size;
6
+ while (i--) id += urlAlphabet[Math.random() * 64 | 0];
7
+ return id;
8
+ }
9
+ //#endregion
10
+ export { nanoid };
@@ -0,0 +1,2 @@
1
+ import { a as ImmutableSet, c as SharedStateOptions, i as ImmutableObject, l as SharedStatePatch, n as ImmutableArray, o as SharedState, r as ImmutableMap, s as SharedStateEvents, t as Immutable, u as createSharedState } from "../shared-state-BFKKxNt1.js";
2
+ export { Immutable, ImmutableArray, ImmutableMap, ImmutableObject, ImmutableSet, SharedState, SharedStateEvents, SharedStateOptions, SharedStatePatch, createSharedState };
@@ -0,0 +1,36 @@
1
+ import { createEventEmitter } from "./events.js";
2
+ import { nanoid } from "./nanoid.js";
3
+ import { applyPatches, enablePatches, produce, produceWithPatches } from "immer";
4
+ //#region src/utils/shared-state.ts
5
+ function createSharedState(options) {
6
+ const { enablePatches: enablePatches$1 = false } = options;
7
+ const events = createEventEmitter();
8
+ let state = options.initialValue;
9
+ const syncIds = /* @__PURE__ */ new Set();
10
+ return {
11
+ on: events.on,
12
+ value: () => state,
13
+ patch: (patches, syncId = nanoid()) => {
14
+ if (syncIds.has(syncId)) return;
15
+ enablePatches();
16
+ state = applyPatches(state, patches);
17
+ syncIds.add(syncId);
18
+ events.emit("updated", state, void 0, syncId);
19
+ },
20
+ mutate: (fn, syncId = nanoid()) => {
21
+ if (syncIds.has(syncId)) return;
22
+ syncIds.add(syncId);
23
+ if (enablePatches$1) {
24
+ const [newState, patches] = produceWithPatches(state, fn);
25
+ state = newState;
26
+ events.emit("updated", state, patches, syncId);
27
+ } else {
28
+ state = produce(state, fn);
29
+ events.emit("updated", state, void 0, syncId);
30
+ }
31
+ },
32
+ syncIds
33
+ };
34
+ }
35
+ //#endregion
36
+ export { createSharedState };
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.9",
4
+ "version": "0.1.1",
5
5
  "description": "Vite DevTools Kit",
6
6
  "author": "VoidZero Inc.",
7
7
  "license": "MIT",
@@ -21,24 +21,33 @@
21
21
  "exports": {
22
22
  ".": "./dist/index.js",
23
23
  "./client": "./dist/client.js",
24
+ "./constants": "./dist/constants.js",
25
+ "./utils/events": "./dist/utils/events.js",
26
+ "./utils/nanoid": "./dist/utils/nanoid.js",
27
+ "./utils/shared-state": "./dist/utils/shared-state.js",
24
28
  "./package.json": "./package.json"
25
29
  },
26
- "main": "./dist/index.js",
27
- "module": "./dist/index.js",
28
- "types": "./dist/index.d.ts",
30
+ "types": "./dist/index.d.mts",
29
31
  "files": [
30
- "dist"
32
+ "dist",
33
+ "skills"
31
34
  ],
32
35
  "peerDependencies": {
33
- "vite": "npm:rolldown-vite@^7.1.20"
36
+ "vite": "*"
34
37
  },
35
38
  "dependencies": {
36
- "birpc": "^2.6.1",
37
- "@vitejs/devtools-rpc": "0.0.0-alpha.9"
39
+ "birpc": "^4.0.0",
40
+ "immer": "^11.1.4",
41
+ "@vitejs/devtools-rpc": "0.1.1"
38
42
  },
39
43
  "devDependencies": {
40
- "tsdown": "^0.15.12",
41
- "vite": "npm:rolldown-vite@^7.1.20"
44
+ "tsdown": "^0.21.2",
45
+ "ua-parser-modern": "^0.1.1",
46
+ "vite": "^8.0.0"
47
+ },
48
+ "inlinedDependencies": {
49
+ "ohash": "2.0.11",
50
+ "ua-parser-modern": "0.1.1"
42
51
  },
43
52
  "scripts": {
44
53
  "build": "tsdown",
@@ -0,0 +1,436 @@
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, logs/notifications, or any devtools-related
7
+ functionality. Applies to files importing from @vitejs/devtools-kit or
8
+ containing 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, launcher) |
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.logs` | Emit structured log entries and toast notifications |
26
+ | `ctx.viteConfig` | Resolved Vite configuration |
27
+ | `ctx.viteServer` | Dev server instance (dev mode only) |
28
+ | `ctx.mode` | `'dev'` or `'build'` |
29
+
30
+ ## Quick Start: Minimal Plugin
31
+
32
+ ```ts
33
+ /// <reference types="@vitejs/devtools-kit" />
34
+ import type { Plugin } from 'vite'
35
+
36
+ export default function myPlugin(): Plugin {
37
+ return {
38
+ name: 'my-plugin',
39
+ devtools: {
40
+ setup(ctx) {
41
+ ctx.docks.register({
42
+ id: 'my-plugin',
43
+ title: 'My Plugin',
44
+ icon: 'ph:puzzle-piece-duotone',
45
+ type: 'iframe',
46
+ url: 'https://example.com/devtools',
47
+ })
48
+ },
49
+ },
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Quick Start: Full Integration
55
+
56
+ ```ts
57
+ /// <reference types="@vitejs/devtools-kit" />
58
+ import type { Plugin } from 'vite'
59
+ import { fileURLToPath } from 'node:url'
60
+ import { defineRpcFunction } from '@vitejs/devtools-kit'
61
+
62
+ export default function myAnalyzer(): Plugin {
63
+ const data = new Map<string, { size: number }>()
64
+
65
+ return {
66
+ name: 'my-analyzer',
67
+
68
+ // Collect data in Vite hooks
69
+ transform(code, id) {
70
+ data.set(id, { size: code.length })
71
+ },
72
+
73
+ devtools: {
74
+ setup(ctx) {
75
+ // 1. Host static UI
76
+ const clientPath = fileURLToPath(
77
+ new URL('../dist/client', import.meta.url)
78
+ )
79
+ ctx.views.hostStatic('/.my-analyzer/', clientPath)
80
+
81
+ // 2. Register dock entry
82
+ ctx.docks.register({
83
+ id: 'my-analyzer',
84
+ title: 'Analyzer',
85
+ icon: 'ph:chart-bar-duotone',
86
+ type: 'iframe',
87
+ url: '/.my-analyzer/',
88
+ })
89
+
90
+ // 3. Register RPC function
91
+ ctx.rpc.register(
92
+ defineRpcFunction({
93
+ name: 'my-analyzer:get-data',
94
+ type: 'query',
95
+ setup: () => ({
96
+ handler: async () => Array.from(data.entries()),
97
+ }),
98
+ })
99
+ )
100
+ },
101
+ },
102
+ }
103
+ }
104
+ ```
105
+
106
+ ## Namespacing Convention
107
+
108
+ **CRITICAL**: Always prefix RPC functions, shared state keys, and dock IDs with your plugin name:
109
+
110
+ ```ts
111
+ // Good - namespaced
112
+ 'my-plugin:get-modules'
113
+ 'my-plugin:state'
114
+
115
+ // Bad - may conflict
116
+ 'get-modules'
117
+ 'state'
118
+ ```
119
+
120
+ ## Dock Entry Types
121
+
122
+ | Type | Use Case |
123
+ |------|----------|
124
+ | `iframe` | Full UI panels, dashboards (most common) |
125
+ | `action` | Buttons that trigger client-side scripts (inspectors, toggles) |
126
+ | `custom-render` | Direct DOM access in panel (framework mounting) |
127
+ | `launcher` | Actionable setup cards for initialization tasks |
128
+
129
+ ### Iframe Entry
130
+
131
+ ```ts
132
+ ctx.docks.register({
133
+ id: 'my-plugin',
134
+ title: 'My Plugin',
135
+ icon: 'ph:house-duotone',
136
+ type: 'iframe',
137
+ url: '/.my-plugin/',
138
+ })
139
+ ```
140
+
141
+ ### Action Entry
142
+
143
+ ```ts
144
+ ctx.docks.register({
145
+ id: 'my-inspector',
146
+ title: 'Inspector',
147
+ icon: 'ph:cursor-duotone',
148
+ type: 'action',
149
+ action: {
150
+ importFrom: 'my-plugin/devtools-action',
151
+ importName: 'default',
152
+ },
153
+ })
154
+ ```
155
+
156
+ ### Custom Render Entry
157
+
158
+ ```ts
159
+ ctx.docks.register({
160
+ id: 'my-custom',
161
+ title: 'Custom View',
162
+ icon: 'ph:code-duotone',
163
+ type: 'custom-render',
164
+ renderer: {
165
+ importFrom: 'my-plugin/devtools-renderer',
166
+ importName: 'default',
167
+ },
168
+ })
169
+ ```
170
+
171
+ ### Launcher Entry
172
+
173
+ ```ts
174
+ const entry = ctx.docks.register({
175
+ id: 'my-setup',
176
+ title: 'My Setup',
177
+ icon: 'ph:rocket-launch-duotone',
178
+ type: 'launcher',
179
+ launcher: {
180
+ title: 'Initialize My Plugin',
181
+ description: 'Run initial setup before using the plugin',
182
+ buttonStart: 'Start Setup',
183
+ buttonLoading: 'Setting up...',
184
+ onLaunch: async () => {
185
+ // Run initialization logic
186
+ },
187
+ },
188
+ })
189
+ ```
190
+
191
+ ## Logs & Notifications
192
+
193
+ Plugins can emit structured log entries from both server and client contexts. Logs appear in the built-in **Logs** panel and can optionally show as toast notifications.
194
+
195
+ ### Fire-and-Forget
196
+
197
+ ```ts
198
+ // No await needed
199
+ context.logs.add({
200
+ message: 'Plugin initialized',
201
+ level: 'info',
202
+ })
203
+ ```
204
+
205
+ ### With Handle
206
+
207
+ ```ts
208
+ const handle = await context.logs.add({
209
+ id: 'my-build',
210
+ message: 'Building...',
211
+ level: 'info',
212
+ status: 'loading',
213
+ })
214
+
215
+ // Update later
216
+ await handle.update({
217
+ message: 'Build complete',
218
+ level: 'success',
219
+ status: 'idle',
220
+ })
221
+
222
+ // Or dismiss
223
+ await handle.dismiss()
224
+ ```
225
+
226
+ ### Key Fields
227
+
228
+ | Field | Type | Description |
229
+ |-------|------|-------------|
230
+ | `message` | `string` | Short title (required) |
231
+ | `level` | `'info' \| 'warn' \| 'error' \| 'success' \| 'debug'` | Severity (required) |
232
+ | `description` | `string` | Detailed description |
233
+ | `notify` | `boolean` | Show as toast notification |
234
+ | `filePosition` | `{ file, line?, column? }` | Source file location (clickable) |
235
+ | `elementPosition` | `{ selector?, boundingBox?, description? }` | DOM element position |
236
+ | `id` | `string` | Explicit id for deduplication |
237
+ | `status` | `'loading' \| 'idle'` | Shows spinner when loading |
238
+ | `category` | `string` | Grouping (e.g., `'a11y'`, `'lint'`) |
239
+ | `labels` | `string[]` | Tags for filtering |
240
+ | `autoDismiss` | `number` | Toast auto-dismiss time in ms (default: 5000) |
241
+ | `autoDelete` | `number` | Auto-delete time in ms |
242
+
243
+ The `from` field is automatically set to `'server'` or `'browser'`.
244
+
245
+ ### Deduplication
246
+
247
+ Re-adding with the same `id` updates the existing entry instead of creating a duplicate:
248
+
249
+ ```ts
250
+ context.logs.add({ id: 'my-scan', message: 'Scanning...', level: 'info', status: 'loading' })
251
+ context.logs.add({ id: 'my-scan', message: 'Scan complete', level: 'success', status: 'idle' })
252
+ ```
253
+
254
+ ## RPC Functions
255
+
256
+ ### Server-Side Definition
257
+
258
+ ```ts
259
+ import { defineRpcFunction } from '@vitejs/devtools-kit'
260
+
261
+ const getModules = defineRpcFunction({
262
+ name: 'my-plugin:get-modules',
263
+ type: 'query', // 'query' | 'action' | 'static'
264
+ setup: ctx => ({
265
+ handler: async (filter?: string) => {
266
+ // ctx has full DevToolsNodeContext
267
+ return modules.filter(m => !filter || m.includes(filter))
268
+ },
269
+ }),
270
+ })
271
+
272
+ // Register in setup
273
+ ctx.rpc.register(getModules)
274
+ ```
275
+
276
+ ### Client-Side Call (iframe)
277
+
278
+ ```ts
279
+ import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
280
+
281
+ const rpc = await getDevToolsRpcClient()
282
+ const modules = await rpc.call('my-plugin:get-modules', 'src/')
283
+ ```
284
+
285
+ ### Client-Side Call (action/renderer script)
286
+
287
+ ```ts
288
+ import type { DevToolsClientScriptContext } from '@vitejs/devtools-kit/client'
289
+
290
+ export default function setup(ctx: DevToolsClientScriptContext) {
291
+ ctx.current.events.on('entry:activated', async () => {
292
+ const data = await ctx.current.rpc.call('my-plugin:get-data')
293
+ })
294
+ }
295
+ ```
296
+
297
+ ### Broadcasting to Clients
298
+
299
+ ```ts
300
+ // Server broadcasts to all clients
301
+ ctx.rpc.broadcast({
302
+ method: 'my-plugin:on-update',
303
+ args: [{ changedFile: '/src/main.ts' }],
304
+ })
305
+ ```
306
+
307
+ ## Type Safety
308
+
309
+ Extend the DevTools Kit interfaces for full type checking:
310
+
311
+ ```ts
312
+ // src/types.ts
313
+ import '@vitejs/devtools-kit'
314
+
315
+ declare module '@vitejs/devtools-kit' {
316
+ interface DevToolsRpcServerFunctions {
317
+ 'my-plugin:get-modules': (filter?: string) => Promise<Module[]>
318
+ }
319
+
320
+ interface DevToolsRpcClientFunctions {
321
+ 'my-plugin:on-update': (data: { changedFile: string }) => void
322
+ }
323
+
324
+ interface DevToolsRpcSharedStates {
325
+ 'my-plugin:state': MyPluginState
326
+ }
327
+ }
328
+ ```
329
+
330
+ ## Shared State
331
+
332
+ ### Server-Side
333
+
334
+ ```ts
335
+ const state = await ctx.rpc.sharedState.get('my-plugin:state', {
336
+ initialValue: { count: 0, items: [] },
337
+ })
338
+
339
+ // Read
340
+ console.log(state.value())
341
+
342
+ // Mutate (auto-syncs to clients)
343
+ state.mutate((draft) => {
344
+ draft.count += 1
345
+ draft.items.push('new item')
346
+ })
347
+ ```
348
+
349
+ ### Client-Side
350
+
351
+ ```ts
352
+ const client = await getDevToolsRpcClient()
353
+ const state = await client.rpc.sharedState.get('my-plugin:state')
354
+
355
+ // Read
356
+ console.log(state.value())
357
+
358
+ // Subscribe to changes
359
+ state.on('updated', (newState) => {
360
+ console.log('State updated:', newState)
361
+ })
362
+ ```
363
+
364
+ ## Client Scripts
365
+
366
+ For action buttons and custom renderers:
367
+
368
+ ```ts
369
+ // src/devtools-action.ts
370
+ import type { DevToolsClientScriptContext } from '@vitejs/devtools-kit/client'
371
+
372
+ export default function setup(ctx: DevToolsClientScriptContext) {
373
+ ctx.current.events.on('entry:activated', () => {
374
+ console.log('Action activated')
375
+ // Your inspector/tool logic here
376
+ })
377
+
378
+ ctx.current.events.on('entry:deactivated', () => {
379
+ console.log('Action deactivated')
380
+ // Cleanup
381
+ })
382
+ }
383
+ ```
384
+
385
+ Export from package.json:
386
+
387
+ ```json
388
+ {
389
+ "exports": {
390
+ ".": "./dist/index.mjs",
391
+ "./devtools-action": "./dist/devtools-action.mjs"
392
+ }
393
+ }
394
+ ```
395
+
396
+ ## Debugging with Self-Inspect
397
+
398
+ Use `@vitejs/devtools-self-inspect` to debug your DevTools plugin. It shows registered RPC functions, dock entries, client scripts, and plugins in a meta-introspection UI at `/.devtools-self-inspect/`.
399
+
400
+ ```ts
401
+ import DevTools from '@vitejs/devtools'
402
+ import DevToolsSelfInspect from '@vitejs/devtools-self-inspect'
403
+
404
+ export default defineConfig({
405
+ plugins: [
406
+ DevTools(),
407
+ DevToolsSelfInspect(),
408
+ ],
409
+ })
410
+ ```
411
+
412
+ ## Best Practices
413
+
414
+ 1. **Always namespace** - Prefix all identifiers with your plugin name
415
+ 2. **Use type augmentation** - Extend `DevToolsRpcServerFunctions` for type-safe RPC
416
+ 3. **Keep state serializable** - No functions or circular references in shared state
417
+ 4. **Batch mutations** - Use single `mutate()` call for multiple changes
418
+ 5. **Host static files** - Use `ctx.views.hostStatic()` for your UI assets
419
+ 6. **Use Iconify icons** - Prefer `ph:*` (Phosphor) icons: `icon: 'ph:chart-bar-duotone'`
420
+ 7. **Deduplicate logs** - Use explicit `id` for logs representing ongoing operations
421
+ 8. **Use Self-Inspect** - Add `@vitejs/devtools-self-inspect` during development to debug your plugin
422
+
423
+ ## Example Plugins
424
+
425
+ Real-world example plugins in the repo — reference their code structure and patterns when building new integrations:
426
+
427
+ - **A11y Checker** ([`examples/plugin-a11y-checker`](https://github.com/vitejs/devtools/tree/main/examples/plugin-a11y-checker)) — Action dock entry, client-side axe-core audits, logs with severity levels and element positions, log handle updates
428
+ - **File Explorer** ([`examples/plugin-file-explorer`](https://github.com/vitejs/devtools/tree/main/examples/plugin-file-explorer)) — Iframe dock entry, RPC functions (static/query/action), hosted UI panel, RPC dump for static builds, backend mode detection
429
+
430
+ ## Further Reading
431
+
432
+ - [RPC Patterns](./references/rpc-patterns.md) - Advanced RPC patterns and type utilities
433
+ - [Dock Entry Types](./references/dock-entry-types.md) - Detailed dock configuration options
434
+ - [Shared State Patterns](./references/shared-state-patterns.md) - Framework integration examples
435
+ - [Project Structure](./references/project-structure.md) - Recommended file organization
436
+ - [Logs Patterns](./references/logs-patterns.md) - Log entries, toast notifications, and handle patterns