Skip to content

Commit 5f73f46

Browse files
committed
feat: basic json-render implementation
1 parent c748c18 commit 5f73f46

25 files changed

Lines changed: 1703 additions & 34 deletions

File tree

docs/kit/dock-system.md

Lines changed: 164 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ Dock entries are the primary way for users to interact with your DevTools integr
88

99
## Entry Types
1010

11-
DevTools Kit supports four types of dock entries:
11+
DevTools Kit supports five types of dock entries:
1212

1313
| Type | Description | Use Case |
1414
|------|-------------|----------|
1515
| `iframe` | Displays your UI in an iframe panel | Full-featured UIs, dashboards, data visualization |
1616
| `action` | Button that triggers client-side scripts | Inspectors, toggles, one-time actions |
1717
| `custom-render` | Renders directly in the user's app DOM | When you need direct DOM access or framework integration |
1818
| `launcher` | Actionable setup card shown in panel | Run one-time setup tasks before showing other tools |
19+
| `json-render` | Renders UI from a JSON spec — no client code needed | Data panels, config viewers, simple interactive tools |
1920

2021
## Iframe Panels
2122

@@ -71,7 +72,7 @@ interface DockEntry {
7172
/** Icon URL, data URI, or Iconify icon name (e.g., 'ph:house-duotone') */
7273
icon: string
7374
/** Entry type */
74-
type: 'iframe' | 'action' | 'custom-render' | 'launcher'
75+
type: 'iframe' | 'action' | 'custom-render' | 'launcher' | 'json-render'
7576
/** URL to load in the iframe (for type: 'iframe') */
7677
url?: string
7778
/** Action configuration (for type: 'action') */
@@ -86,6 +87,10 @@ interface DockEntry {
8687
buttonStart?: string
8788
buttonLoading?: string
8889
}
90+
/** Inline JSON spec (for type: 'json-render') */
91+
spec?: JsonRenderSpec
92+
/** Shared state key holding the JSON spec (for type: 'json-render') */
93+
sharedStateKey?: string
8994
}
9095
```
9196

@@ -296,6 +301,163 @@ ctx.docks.register({
296301
> [!NOTE]
297302
> Built-in logs panel (`~logs`) is currently reserved and hidden while log UI is under development.
298303
304+
## JSON Render Panels
305+
306+
JSON render panels let you describe your UI as a JSON spec on the server side. The DevTools client renders it with built-in components powered by [json-render](https://github.com/vercel-labs/json-render). **No client code needed.**
307+
308+
This is the simplest way to add a DevTools panel — you only write server-side TypeScript.
309+
310+
### Basic Example
311+
312+
```ts
313+
import { defineJsonRenderSpec } from '@vitejs/devtools-kit'
314+
315+
ctx.docks.register({
316+
id: 'my-panel',
317+
title: 'My Panel',
318+
icon: 'ph:chart-bar-duotone',
319+
type: 'json-render',
320+
spec: defineJsonRenderSpec({
321+
root: 'root',
322+
elements: {
323+
root: {
324+
type: 'Stack',
325+
props: { direction: 'vertical', gap: 12 },
326+
children: ['heading', 'info'],
327+
},
328+
heading: {
329+
type: 'Text',
330+
props: { content: 'Hello from JSON!', variant: 'heading' },
331+
},
332+
info: {
333+
type: 'KeyValueTable',
334+
props: {
335+
entries: [
336+
{ key: 'Version', value: '1.0.0' },
337+
{ key: 'Status', value: 'Running' },
338+
],
339+
},
340+
},
341+
},
342+
}),
343+
})
344+
```
345+
346+
### Dynamic Data with Shared State
347+
348+
For dynamic UIs that update over time, store the spec in a [shared state](./shared-state) key:
349+
350+
```ts
351+
ctx.docks.register({
352+
id: 'my-panel',
353+
title: 'My Panel',
354+
icon: 'ph:chart-bar-duotone',
355+
type: 'json-render',
356+
sharedStateKey: 'my-plugin:ui',
357+
})
358+
359+
const ui = await ctx.rpc.sharedState.get('my-plugin:ui', {
360+
initialValue: defineJsonRenderSpec({ /* spec */ }),
361+
})
362+
363+
// Later, update the UI reactively:
364+
ui.mutate((draft) => {
365+
draft.elements.heading.props.content = 'Updated!'
366+
})
367+
```
368+
369+
### Handling Actions via RPC
370+
371+
Buttons in the spec can trigger RPC functions on the server:
372+
373+
```ts
374+
// In the spec:
375+
defineJsonRenderSpec({
376+
// ...
377+
elements: {
378+
'refresh-btn': {
379+
type: 'Button',
380+
props: { label: 'Refresh' },
381+
on: { press: { action: 'my-plugin:refresh' } },
382+
},
383+
},
384+
})
385+
```
386+
387+
```ts
388+
// On the server:
389+
ctx.rpc.register(defineRpcFunction({
390+
name: 'my-plugin:refresh',
391+
type: 'event',
392+
setup: ctx => ({
393+
handler: async () => {
394+
// Fetch new data, then update the spec
395+
ui.mutate((draft) => { /* ... */ })
396+
},
397+
}),
398+
}))
399+
```
400+
401+
### Text Input with Two-Way Binding
402+
403+
Use `$bindState` for two-way binding on text inputs, and `$state` to read the bound value in action params:
404+
405+
```ts
406+
defineJsonRenderSpec({
407+
// ...
408+
elements: {
409+
'my-input': {
410+
type: 'TextInput',
411+
props: {
412+
placeholder: 'Type here...',
413+
value: { $bindState: '/inputValue' },
414+
},
415+
},
416+
'submit-btn': {
417+
type: 'Button',
418+
props: { label: 'Submit' },
419+
on: {
420+
press: {
421+
action: 'my-plugin:submit',
422+
params: { text: { $state: '/inputValue' } },
423+
},
424+
},
425+
},
426+
}
427+
})
428+
```
429+
430+
The initial state can be set in the spec:
431+
432+
```ts
433+
defineJsonRenderSpec({
434+
root: 'root',
435+
state: { inputValue: '' },
436+
elements: { /* ... */ },
437+
})
438+
```
439+
440+
### Available Components
441+
442+
| Component | Description |
443+
|-----------|-------------|
444+
| `Stack` | Flex layout container (vertical/horizontal) |
445+
| `Card` | Container with optional title, collapsible |
446+
| `Text` | Text display (heading, body, caption, code variants) |
447+
| `Badge` | Status label (info, success, warning, error) |
448+
| `Button` | Clickable button — emits `press` event for actions |
449+
| `Icon` | Iconify icon by name |
450+
| `Divider` | Visual separator with optional label |
451+
| `TextInput` | Text input with `$bindState` two-way binding |
452+
| `KeyValueTable` | Key-value pairs display |
453+
| `DataTable` | Tabular data with columns and rows |
454+
| `CodeBlock` | Code display with optional filename |
455+
| `Progress` | Progress bar with label |
456+
| `Tree` | Expandable tree view for nested objects |
457+
458+
> [!TIP]
459+
> See the [Git UI example](/kit/examples#git-ui) for a complete interactive plugin using json-render with RPC actions, text input, and dynamic state.
460+
299461
## Communication with Server
300462

301463
All client scripts (actions and custom renderers) can communicate with the server using [RPC](./rpc):

docs/kit/examples.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,18 @@ A file explorer dock that lists, reads, and writes files through RPC.
3232
- Detecting backend mode (`websocket` vs `static`) on the client
3333

3434
**Source:** [`examples/plugin-file-explorer`](https://github.com/vitejs/devtools/tree/main/examples/plugin-file-explorer)
35+
36+
## Git UI
37+
38+
An interactive Git panel built entirely with server-side JSON specs — no client code at all.
39+
40+
**Features demonstrated:**
41+
42+
- Using the `json-render` dock type for zero-client-code panels
43+
- Building a `JsonRenderSpec` from server-side data (git branch, status, log)
44+
- Dynamic spec updates via shared state (`sharedStateKey`)
45+
- Button actions bridged to RPC functions (`git-ui:refresh`, `git-ui:commit`)
46+
- Text input with `$bindState` two-way binding and `$state` in action params
47+
- Updating dock badge text reactively
48+
49+
**Source:** [`examples/plugin-git-ui`](https://github.com/vitejs/devtools/tree/main/examples/plugin-git-ui)

examples/plugin-git-ui/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Example: DevTools Kit Git UI Plugin
2+
3+
This example shows how to build a fully interactive DevTools panel using `@vitejs/devtools-kit`'s **json-render** dock type — with zero client-side code.
4+
5+
It registers a `json-render` dock that displays the current git state: branch, staged/unstaged files, recent commits, and a commit form.
6+
7+
## How It Works
8+
9+
The entire UI is described as a JSON spec on the server side. The DevTools client renders it using built-in components powered by [@json-render/vue](https://github.com/vercel-labs/json-render).
10+
11+
1. Node plugin (`src/node/plugin.ts`)
12+
- collects git state by running `git` commands (`git branch`, `git log`, `git status`)
13+
- builds a `JsonRenderSpec` describing the UI layout (Stack, Card, DataTable, TextInput, Button, etc.)
14+
- stores the spec in a shared state key and registers a `json-render` dock entry
15+
- registers two RPC functions:
16+
- `git-ui:refresh` — re-reads git state and updates the spec
17+
- `git-ui:commit` — runs `git commit -m "..."` with the message from the text input
18+
19+
2. Client rendering (automatic)
20+
- `ViewJsonRender.vue` subscribes to the shared state key
21+
- renders the spec using the built-in devtools component registry
22+
- bridges button clicks → RPC calls via the json-render action system
23+
- text input uses `$bindState` for two-way binding; the commit action reads the message via `$state`
24+
25+
**Key point**: there is no client-side code in this plugin. No Vue components, no Nuxt app, no iframe. The plugin only writes TypeScript on the server side.
26+
27+
## Run The Example
28+
29+
From the `examples/plugin-git-ui` directory (`cd examples/plugin-git-ui`):
30+
31+
```bash
32+
pnpm play:dev
33+
```
34+
35+
Then open the app URL, open Vite DevTools, and click the **Git** dock entry.
36+
37+
You can also test it from the core playground:
38+
39+
```bash
40+
pnpm -C packages/core run play
41+
```
42+
43+
## Components Used
44+
45+
| Component | Purpose in this example |
46+
|-----------|------------------------|
47+
| `Stack` | Layout — vertical/horizontal flex containers |
48+
| `Card` | Collapsible sections for staged, unstaged, and commits |
49+
| `Text` | Headings, branch name, empty state messages |
50+
| `Badge` | Change count indicator (warning/success) |
51+
| `Button` | Refresh and Commit actions |
52+
| `TextInput` | Commit message input with `$bindState` binding |
53+
| `DataTable` | Staged files, unstaged files, and commit history |
54+
| `Divider` | Visual separator |
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "example-plugin-git-ui",
3+
"type": "module",
4+
"version": "0.1.0",
5+
"private": true,
6+
"exports": {
7+
".": "./src/node/index.ts",
8+
"./package.json": "./package.json"
9+
},
10+
"scripts": {
11+
"play:dev": "cd playground && DEBUG='vite:devtools:*' vite"
12+
},
13+
"peerDependencies": {
14+
"vite": "*"
15+
},
16+
"dependencies": {
17+
"@vitejs/devtools": "workspace:*",
18+
"@vitejs/devtools-kit": "workspace:*"
19+
},
20+
"devDependencies": {
21+
"vite": "catalog:build"
22+
}
23+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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>Git UI Playground</title>
7+
</head>
8+
<body>
9+
<div id="app"></div>
10+
<style>
11+
@media (prefers-color-scheme: dark) {
12+
body {
13+
color-scheme: dark;
14+
background-color: #111;
15+
color: #fff;
16+
}
17+
}
18+
</style>
19+
<script type="module" src="/src/main.ts"></script>
20+
</body>
21+
</html>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
document.getElementById('app')!.innerHTML = `
2+
<div style="padding: 2rem; font-family: system-ui, sans-serif;">
3+
<h1>Git UI Plugin Playground</h1>
4+
<p>Open Vite DevTools and click the <strong>Git</strong> dock entry to see the Git UI panel.</p>
5+
<p style="opacity: 0.6; font-size: 14px;">
6+
The panel shows your current branch, staged/unstaged files, recent commits, and lets you create commits — all rendered from a JSON spec with zero client-side code.
7+
</p>
8+
</div>
9+
`
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { DevTools } from '@vitejs/devtools'
2+
import { defineConfig } from 'vite'
3+
import { GitUIPlugin } from '../src/node'
4+
5+
export default defineConfig({
6+
plugins: [
7+
DevTools({
8+
builtinDevTools: false,
9+
}),
10+
GitUIPlugin(),
11+
],
12+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { GitUIPlugin } from './plugin'

0 commit comments

Comments
 (0)