Skip to content

Commit 814e27c

Browse files
authored
feat: implement static build support (#187)
1 parent 5f2f79f commit 814e27c

57 files changed

Lines changed: 2471 additions & 175 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/kit/rpc.md

Lines changed: 129 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,16 @@ const getModules = defineRpcFunction({
4444
})
4545
```
4646

47-
### Registering Functions
47+
### Naming Convention
4848

49-
Register your RPC function in the `devtools.setup`:
49+
Recommended RPC function naming:
5050

51-
```ts
52-
const plugin: Plugin = {
53-
devtools: {
54-
setup(ctx) {
55-
ctx.rpc.register(getModules)
56-
}
57-
}
58-
}
59-
```
51+
1. Scope functions with your package prefix: `<package-name>:...`
52+
2. Use kebab-case for the function part after `:`
53+
54+
Examples:
55+
- `my-plugin:get-modules`
56+
- `my-plugin:read-file`
6057

6158
### Function Types
6259

@@ -108,16 +105,33 @@ setup: (ctx) => {
108105
> [!IMPORTANT]
109106
> 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.
110107
108+
### Registering Functions
109+
110+
Register your RPC function in the `devtools.setup`:
111+
112+
```ts
113+
const plugin: Plugin = {
114+
devtools: {
115+
setup(ctx) {
116+
ctx.rpc.register(getModules)
117+
}
118+
}
119+
}
120+
```
121+
111122
### Dump Feature for Build Mode
112123

113124
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.
114125

115126
#### How It Works
116127

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

132+
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`).
133+
Query record maps are embedded directly in `.rpc-dump/index.json`; no per-function index files are generated.
134+
121135
#### Static Functions (Recommended)
122136

123137
Functions with `type: 'static'` are **automatically dumped** with no arguments:
@@ -198,6 +212,109 @@ const getLiveMetrics = defineRpcFunction({
198212
> [!TIP]
199213
> 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.
200214
215+
### Organization Convention
216+
217+
For plugin-scale RPC modules, we recommend this structure:
218+
219+
General guidelines:
220+
221+
1. Keep function definitions small and focused: one RPC function per file.
222+
2. Use `src/node/rpc/index.ts` as the single composition point for registration and type augmentation.
223+
3. Store plugin-specific runtime options in `src/node/rpc/context.ts` (instead of mutating the base DevTools context object).
224+
4. Use `context.rpc.invokeLocal(...)` for server-side cross-function composition.
225+
226+
Rough file tree:
227+
228+
```text
229+
src/node/rpc/
230+
├─ index.ts # exports rpcFunctions + module augmentation
231+
├─ context.ts # WeakMap-backed helpers (set/get shared rpc context)
232+
└─ functions/
233+
├─ get-info.ts # metadata-style query/static function
234+
├─ list-files.ts # list operation, reusable by other functions
235+
├─ read-file.ts # can invoke `list-files` via invokeLocal
236+
└─ write-file.ts # mutation-oriented function
237+
```
238+
239+
1. `src/node/rpc/index.ts`
240+
Keep all RPC declarations in one exported list (for example `rpcFunctions`) and centralize type augmentation (`DevToolsRpcServerFunctions`) in the same file.
241+
242+
```ts
243+
// src/node/rpc/index.ts
244+
import type { RpcDefinitionsToFunctions } from '@vitejs/devtools-kit'
245+
import { getInfo } from './functions/get-info'
246+
import { listFiles } from './functions/list-files'
247+
import { readFile } from './functions/read-file'
248+
import '@vitejs/devtools-kit'
249+
250+
export const rpcFunctions = [
251+
getInfo,
252+
listFiles,
253+
readFile,
254+
] as const // use `as const` to allow type inference
255+
256+
export type ServerFunctions = RpcDefinitionsToFunctions<typeof rpcFunctions>
257+
258+
declare module '@vitejs/devtools-kit' {
259+
export interface DevToolsRpcServerFunctions extends ServerFunctions {}
260+
}
261+
```
262+
263+
2. `src/node/rpc/context.ts`
264+
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.
265+
266+
```ts
267+
// src/node/rpc/context.ts
268+
import type { DevToolsNodeContext } from '@vitejs/devtools-kit'
269+
270+
const rpcContext = new WeakMap<DevToolsNodeContext, { targetDir: string }>()
271+
272+
export function setRpcContext(context: DevToolsNodeContext, options: { targetDir: string }) {
273+
rpcContext.set(context, options)
274+
}
275+
276+
export function getRpcContext(context: DevToolsNodeContext) {
277+
const value = rpcContext.get(context)
278+
if (!value)
279+
throw new Error('Missing RPC context')
280+
return value
281+
}
282+
```
283+
284+
```ts
285+
// plugin setup
286+
const plugin = {
287+
devtools: {
288+
setup(context) {
289+
setRpcContext(context, { targetDir: 'src' })
290+
rpcFunctions.forEach(fn => context.rpc.register(fn))
291+
},
292+
},
293+
}
294+
```
295+
296+
3. `src/node/rpc/functions/read-file.ts`
297+
For cross-function calls on the server, use `context.rpc.invokeLocal('<package-name>:list-files')` rather than network-style calls.
298+
299+
```ts
300+
// src/node/rpc/functions/read-file.ts
301+
export const readFile = defineRpcFunction({
302+
name: 'my-plugin:read-file',
303+
type: 'query',
304+
dump: async (context) => {
305+
const files = await context.rpc.invokeLocal('my-plugin:list-files')
306+
return {
307+
inputs: files.map(file => [file.path] as [string]),
308+
}
309+
},
310+
setup: () => ({
311+
handler: async (path: string) => {
312+
// ...
313+
},
314+
}),
315+
})
316+
```
317+
201318
## Schema Validation (Optional)
202319

203320
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.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Example: DevTools Kit File Explorer Plugin
2+
3+
This example shows how to build a custom Vite DevTools panel with `@vitejs/devtools-kit`.
4+
5+
It provides a **File Explorer** dock that:
6+
- lists files under a target directory
7+
- reads file content on demand
8+
- writes file content in dev (websocket) mode
9+
- works in static mode via RPC dump files
10+
11+
## How It Works
12+
13+
The example has three main parts:
14+
15+
1. Node plugin (`src/node/plugin.ts`)
16+
- creates RPC functions
17+
- registers them with `context.rpc.register(...)`
18+
- hosts the built UI with `context.views.hostStatic(...)`
19+
- registers a dock entry (`type: 'iframe'`) for the panel
20+
21+
2. RPC functions (`src/node/rpc/functions/*`)
22+
- `plugin-file-explorer:get-info` (`type: 'static'`)
23+
- `plugin-file-explorer:list-files` (`type: 'query'`, dumped with empty args)
24+
- `plugin-file-explorer:read-file` (`type: 'query'`, fallback `null`)
25+
- `plugin-file-explorer:write-file` (`type: 'action'`, dev-only behavior)
26+
27+
3. UI app (`src/ui/main.tsx`)
28+
- connects using `getDevToolsRpcClient()`
29+
- detects backend mode (`websocket` vs `static`)
30+
- hides write controls in static mode
31+
32+
## Run The Example
33+
34+
From the `examples/plugin-file-explorer` directory (`cd examples/plugin-file-explorer`):
35+
36+
```bash
37+
pnpm play:dev
38+
```
39+
40+
Then open the app URL, open Vite DevTools, and switch to the **File Explorer** dock.
41+
42+
## Static Build / Preview
43+
44+
Build static output:
45+
46+
```bash
47+
pnpm play:build
48+
```
49+
50+
Preview generated static files:
51+
52+
```bash
53+
pnpm play:preview
54+
```
55+
56+
Static artifacts are generated under:
57+
58+
- `playground/.vite-devtools/.devtools/.connection.json`
59+
- `playground/.vite-devtools/.devtools/.rpc-dump/index.json`
60+
- `playground/.vite-devtools/.devtools/.rpc-dump/*.json`
61+
62+
## Notes
63+
64+
- Default UI base: `/.plugin-file-explorer/`
65+
- Default target directory: `src`
66+
- You can override via options or env:
67+
- `KIT_PLUGIN_FILE_EXPLORER_UI_BASE`
68+
- `KIT_PLUGIN_FILE_EXPLORER_TARGET_DIR`
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "example-plugin-file-explorer",
3+
"type": "module",
4+
"version": "0.0.0",
5+
"private": true,
6+
"exports": {
7+
".": "./dist/index.mjs",
8+
"./package.json": "./package.json"
9+
},
10+
"types": "./dist/index.d.mts",
11+
"files": [
12+
"dist"
13+
],
14+
"scripts": {
15+
"build:node": "tsdown --config-loader=tsx",
16+
"build:ui": "cd src/ui && vite build",
17+
"build": "pnpm run build:node && pnpm run build:ui",
18+
"play:dev": "pnpm run build && cd playground && DEBUG='vite:devtools:*' vite",
19+
"play:build": "pnpm run build && cd playground && vite build && vite-devtools build",
20+
"play:preview": "serve ./playground/.vite-devtools"
21+
},
22+
"peerDependencies": {
23+
"vite": "*"
24+
},
25+
"dependencies": {
26+
"@vitejs/devtools": "workspace:*",
27+
"@vitejs/devtools-kit": "workspace:*",
28+
"pathe": "catalog:deps",
29+
"tinyglobby": "catalog:deps"
30+
},
31+
"devDependencies": {
32+
"@types/react": "catalog:types",
33+
"@types/react-dom": "catalog:types",
34+
"react": "catalog:frontend",
35+
"react-dom": "catalog:frontend",
36+
"serve": "catalog:devtools",
37+
"tsdown": "catalog:build",
38+
"unocss": "catalog:build",
39+
"vite": "catalog:build"
40+
}
41+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Kit Plugin File Explorer Playground</title>
7+
</head>
8+
<body>
9+
<div id="app"></div>
10+
<script type="module" src="/src/main.ts"></script>
11+
</body>
12+
</html>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import '@unocss/reset/tailwind.css'
2+
import 'virtual:uno.css'
3+
4+
// @unocss-include
5+
6+
const app = document.querySelector<HTMLDivElement>('#app')
7+
if (!app)
8+
throw new Error('Missing #app root')
9+
10+
app.innerHTML = `
11+
<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">
12+
<main class="mx-auto max-w-4xl px-6 py-10">
13+
<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">
14+
<h1 class="m-0 text-3xl font-semibold tracking-tight">Kit Plugin File Explorer Playground</h1>
15+
<p class="mt-3 leading-7 text-slate-700 dark:text-slate-300">
16+
Open Vite DevTools and switch to <strong>File Explorer</strong>.
17+
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.
18+
<strong>Save</strong> is available in websocket mode and hidden in static build mode.
19+
</p>
20+
<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">
21+
<p class="m-0 font-medium">Try this:</p>
22+
<ol class="my-2 pl-5">
23+
<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>
24+
<li>Edit a sentence and click <strong>Save</strong> in websocket mode.</li>
25+
<li>Run static build and confirm write controls are hidden.</li>
26+
</ol>
27+
<p class="m-0 text-slate-600 dark:text-slate-400">
28+
This playground keeps the source folder intentionally small so file operations are easy to inspect.
29+
</p>
30+
</div>
31+
</section>
32+
</main>
33+
</div>
34+
`
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { fileURLToPath } from 'node:url'
2+
import { DevTools } from '@vitejs/devtools'
3+
import UnoCSS from 'unocss/vite'
4+
import { defineConfig } from 'vite'
5+
import kitPluginFileExplorer from '../src/node'
6+
7+
const unoConfig = fileURLToPath(new URL('../uno.config.ts', import.meta.url))
8+
9+
export default defineConfig({
10+
plugins: [
11+
DevTools({
12+
builtinDevTools: false,
13+
}),
14+
kitPluginFileExplorer(),
15+
UnoCSS({
16+
configFile: unoConfig,
17+
}),
18+
],
19+
build: {
20+
rollupOptions: {
21+
devtools: {},
22+
},
23+
},
24+
})
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const DEFAULT_UI_BASE = '/.plugin-file-explorer/'
2+
export const DEFAULT_TARGET_DIR = 'src'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default } from './plugin'
2+
export * from './plugin'
3+
export * from './types'

0 commit comments

Comments
 (0)