Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 129 additions & 12 deletions docs/kit/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,16 @@ const getModules = defineRpcFunction({
})
```

### Registering Functions
### Naming Convention

Register your RPC function in the `devtools.setup`:
Recommended RPC function naming:

```ts
const plugin: Plugin = {
devtools: {
setup(ctx) {
ctx.rpc.register(getModules)
}
}
}
```
1. Scope functions with your package prefix: `<package-name>:...`
2. Use kebab-case for the function part after `:`

Examples:
- `my-plugin:get-modules`
- `my-plugin:read-file`

### Function Types

Expand Down Expand Up @@ -108,16 +105,33 @@ setup: (ctx) => {
> [!IMPORTANT]
> For build mode compatibility, compute data in the setup function using the context rather than relying on runtime global state. This allows the dump feature to pre-compute results at build time.

### Registering Functions

Register your RPC function in the `devtools.setup`:

```ts
const plugin: Plugin = {
devtools: {
setup(ctx) {
ctx.rpc.register(getModules)
}
}
}
```

### Dump Feature for Build Mode

When using `vite devtools build` to create a static DevTools build, the server cannot execute functions at runtime. The **dump feature** solves this by pre-computing RPC results at build time.

#### How It Works

1. At build time, `dumpFunctions()` executes your RPC handlers with predefined arguments
2. Results are stored in `.vdt-rpc-dump.json` in the build output
2. Results are stored in `.rpc-dump/index.json` in the build output
3. The static client reads from this JSON file instead of making live RPC calls

Dump shard files are written to `.rpc-dump/*.json`. Function names in shard file keys replace `:` with `~` (for example `my-plugin:get-data` -> `my-plugin~get-data`).
Query record maps are embedded directly in `.rpc-dump/index.json`; no per-function index files are generated.

#### Static Functions (Recommended)

Functions with `type: 'static'` are **automatically dumped** with no arguments:
Expand Down Expand Up @@ -198,6 +212,109 @@ const getLiveMetrics = defineRpcFunction({
> [!TIP]
> If your data genuinely needs live server state, use `type: 'query'` without dumps. The function will work in dev mode but gracefully fail in build mode.

### Organization Convention

For plugin-scale RPC modules, we recommend this structure:

General guidelines:

1. Keep function definitions small and focused: one RPC function per file.
2. Use `src/node/rpc/index.ts` as the single composition point for registration and type augmentation.
3. Store plugin-specific runtime options in `src/node/rpc/context.ts` (instead of mutating the base DevTools context object).
4. Use `context.rpc.invokeLocal(...)` for server-side cross-function composition.

Rough file tree:

```text
src/node/rpc/
├─ index.ts # exports rpcFunctions + module augmentation
├─ context.ts # WeakMap-backed helpers (set/get shared rpc context)
└─ functions/
├─ get-info.ts # metadata-style query/static function
├─ list-files.ts # list operation, reusable by other functions
├─ read-file.ts # can invoke `list-files` via invokeLocal
└─ write-file.ts # mutation-oriented function
```

1. `src/node/rpc/index.ts`
Keep all RPC declarations in one exported list (for example `rpcFunctions`) and centralize type augmentation (`DevToolsRpcServerFunctions`) in the same file.

```ts
// src/node/rpc/index.ts
import type { RpcDefinitionsToFunctions } from '@vitejs/devtools-kit'
import { getInfo } from './functions/get-info'
import { listFiles } from './functions/list-files'
import { readFile } from './functions/read-file'
import '@vitejs/devtools-kit'

export const rpcFunctions = [
getInfo,
listFiles,
readFile,
] as const // use `as const` to allow type inference

export type ServerFunctions = RpcDefinitionsToFunctions<typeof rpcFunctions>

declare module '@vitejs/devtools-kit' {
export interface DevToolsRpcServerFunctions extends ServerFunctions {}
}
```

2. `src/node/rpc/context.ts`
Use a shared context helper (for example `WeakMap`-backed `set/get`) to provide plugin-specific options across RPC functions without mutating the base context shape.

```ts
// src/node/rpc/context.ts
import type { DevToolsNodeContext } from '@vitejs/devtools-kit'

const rpcContext = new WeakMap<DevToolsNodeContext, { targetDir: string }>()

export function setRpcContext(context: DevToolsNodeContext, options: { targetDir: string }) {
rpcContext.set(context, options)
}

export function getRpcContext(context: DevToolsNodeContext) {
const value = rpcContext.get(context)
if (!value)
throw new Error('Missing RPC context')
return value
}
```

```ts
// plugin setup
const plugin = {
devtools: {
setup(context) {
setRpcContext(context, { targetDir: 'src' })
rpcFunctions.forEach(fn => context.rpc.register(fn))
},
},
}
```

3. `src/node/rpc/functions/read-file.ts`
For cross-function calls on the server, use `context.rpc.invokeLocal('<package-name>:list-files')` rather than network-style calls.

```ts
// src/node/rpc/functions/read-file.ts
export const readFile = defineRpcFunction({
name: 'my-plugin:read-file',
type: 'query',
dump: async (context) => {
const files = await context.rpc.invokeLocal('my-plugin:list-files')
return {
inputs: files.map(file => [file.path] as [string]),
}
},
setup: () => ({
handler: async (path: string) => {
// ...
},
}),
})
```

## Schema Validation (Optional)

The RPC system has built-in support for runtime schema validation using [Valibot](https://valibot.dev). When you provide schemas, TypeScript types are automatically inferred and validation happens at runtime.
Expand Down
68 changes: 68 additions & 0 deletions examples/plugin-file-explorer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Example: DevTools Kit File Explorer Plugin

This example shows how to build a custom Vite DevTools panel with `@vitejs/devtools-kit`.

It provides a **File Explorer** dock that:
- lists files under a target directory
- reads file content on demand
- writes file content in dev (websocket) mode
- works in static mode via RPC dump files

## How It Works

The example has three main parts:

1. Node plugin (`src/node/plugin.ts`)
- creates RPC functions
- registers them with `context.rpc.register(...)`
- hosts the built UI with `context.views.hostStatic(...)`
- registers a dock entry (`type: 'iframe'`) for the panel

2. RPC functions (`src/node/rpc/functions/*`)
- `plugin-file-explorer:get-info` (`type: 'static'`)
- `plugin-file-explorer:list-files` (`type: 'query'`, dumped with empty args)
- `plugin-file-explorer:read-file` (`type: 'query'`, fallback `null`)
- `plugin-file-explorer:write-file` (`type: 'action'`, dev-only behavior)

3. UI app (`src/ui/main.tsx`)
- connects using `getDevToolsRpcClient()`
- detects backend mode (`websocket` vs `static`)
- hides write controls in static mode

## Run The Example

From the `examples/plugin-file-explorer` directory (`cd examples/plugin-file-explorer`):

```bash
pnpm play:dev
```

Then open the app URL, open Vite DevTools, and switch to the **File Explorer** dock.

## Static Build / Preview

Build static output:

```bash
pnpm play:build
```

Preview generated static files:

```bash
pnpm play:preview
```

Static artifacts are generated under:

- `playground/.vite-devtools/.devtools/.connection.json`
- `playground/.vite-devtools/.devtools/.rpc-dump/index.json`
- `playground/.vite-devtools/.devtools/.rpc-dump/*.json`

## Notes

- Default UI base: `/.plugin-file-explorer/`
- Default target directory: `src`
- You can override via options or env:
- `KIT_PLUGIN_FILE_EXPLORER_UI_BASE`
- `KIT_PLUGIN_FILE_EXPLORER_TARGET_DIR`
41 changes: 41 additions & 0 deletions examples/plugin-file-explorer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "example-plugin-file-explorer",
"type": "module",
"version": "0.0.0",
"private": true,
"exports": {
".": "./dist/index.mjs",
"./package.json": "./package.json"
},
"types": "./dist/index.d.mts",
"files": [
"dist"
],
"scripts": {
"build:node": "tsdown --config-loader=tsx",
"build:ui": "cd src/ui && vite build",
"build": "pnpm run build:node && pnpm run build:ui",
"play:dev": "pnpm run build && cd playground && DEBUG='vite:devtools:*' vite",
"play:build": "pnpm run build && cd playground && vite build && vite-devtools build",
"play:preview": "serve ./playground/.vite-devtools"
},
"peerDependencies": {
"vite": "*"
},
"dependencies": {
"@vitejs/devtools": "workspace:*",
"@vitejs/devtools-kit": "workspace:*",
"pathe": "catalog:deps",
"tinyglobby": "catalog:deps"
},
"devDependencies": {
"@types/react": "catalog:types",
"@types/react-dom": "catalog:types",
"react": "catalog:frontend",
"react-dom": "catalog:frontend",
"serve": "catalog:devtools",
"tsdown": "catalog:build",
"unocss": "catalog:build",
"vite": "catalog:build"
}
}
12 changes: 12 additions & 0 deletions examples/plugin-file-explorer/playground/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kit Plugin File Explorer Playground</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
34 changes: 34 additions & 0 deletions examples/plugin-file-explorer/playground/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import '@unocss/reset/tailwind.css'
import 'virtual:uno.css'

// @unocss-include

const app = document.querySelector<HTMLDivElement>('#app')
if (!app)
throw new Error('Missing #app root')

app.innerHTML = `
<div class="min-h-screen bg-gradient-to-br from-slate-100 via-blue-50 to-cyan-100 text-slate-800 dark:from-slate-950 dark:via-slate-900 dark:to-slate-800 dark:text-slate-100">
<main class="mx-auto max-w-4xl px-6 py-10">
<section class="rounded-2xl border border-slate-200 bg-white/80 p-6 shadow-xl shadow-slate-300/20 backdrop-blur dark:border-slate-700 dark:bg-slate-900/70 dark:shadow-black/25">
<h1 class="m-0 text-3xl font-semibold tracking-tight">Kit Plugin File Explorer Playground</h1>
<p class="mt-3 leading-7 text-slate-700 dark:text-slate-300">
Open Vite DevTools and switch to <strong>File Explorer</strong>.
The panel lists files under <code class="rounded bg-slate-200/70 px-1 py-0.5 font-mono text-xs dark:bg-slate-700/70">playground/src</code> and loads file contents on demand.
<strong>Save</strong> is available in websocket mode and hidden in static build mode.
</p>
<div class="mt-5 rounded-xl border border-slate-200 bg-slate-50 p-4 text-sm leading-6 dark:border-slate-700 dark:bg-slate-800/70">
<p class="m-0 font-medium">Try this:</p>
<ol class="my-2 pl-5">
<li>Select <code class="rounded bg-slate-200/70 px-1 py-0.5 font-mono text-xs dark:bg-slate-700/70">src/main.ts</code> in the File Explorer dock.</li>
<li>Edit a sentence and click <strong>Save</strong> in websocket mode.</li>
<li>Run static build and confirm write controls are hidden.</li>
</ol>
<p class="m-0 text-slate-600 dark:text-slate-400">
This playground keeps the source folder intentionally small so file operations are easy to inspect.
</p>
</div>
</section>
</main>
</div>
`
1 change: 1 addition & 0 deletions examples/plugin-file-explorer/playground/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
24 changes: 24 additions & 0 deletions examples/plugin-file-explorer/playground/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { fileURLToPath } from 'node:url'
import { DevTools } from '@vitejs/devtools'
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'
import kitPluginFileExplorer from '../src/node'

const unoConfig = fileURLToPath(new URL('../uno.config.ts', import.meta.url))

export default defineConfig({
plugins: [
DevTools({
builtinDevTools: false,
}),
kitPluginFileExplorer(),
UnoCSS({
configFile: unoConfig,
}),
],
build: {
rollupOptions: {
devtools: {},
},
},
})
2 changes: 2 additions & 0 deletions examples/plugin-file-explorer/src/node/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const DEFAULT_UI_BASE = '/.plugin-file-explorer/'
export const DEFAULT_TARGET_DIR = 'src'
3 changes: 3 additions & 0 deletions examples/plugin-file-explorer/src/node/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default } from './plugin'
export * from './plugin'
export * from './types'
Loading
Loading