From 59c52c12b754bb4b47e35cca358cc8428aeb7f62 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Tue, 17 Mar 2026 08:49:58 +0900 Subject: [PATCH] docs: update skills with json-render and terminals features Add comprehensive documentation for two new features: - JSON Render: server-side JSON specs for zero-client-code UI panels - Terminals: child process spawning with streaming xterm.js output Updates include new reference guides, Core Concepts table additions, dock entry type descriptions, and Git UI example reference. Co-Authored-By: Claude Haiku 4.5 --- skills/vite-devtools-kit/SKILL.md | 71 ++++- .../references/dock-entry-types.md | 87 ++++++ .../references/json-render-patterns.md | 284 ++++++++++++++++++ .../references/terminals-patterns.md | 123 ++++++++ 4 files changed, 564 insertions(+), 1 deletion(-) create mode 100644 skills/vite-devtools-kit/references/json-render-patterns.md create mode 100644 skills/vite-devtools-kit/references/terminals-patterns.md diff --git a/skills/vite-devtools-kit/SKILL.md b/skills/vite-devtools-kit/SKILL.md index 40fb88c8..3d856898 100644 --- a/skills/vite-devtools-kit/SKILL.md +++ b/skills/vite-devtools-kit/SKILL.md @@ -18,11 +18,13 @@ A DevTools plugin extends a Vite plugin with a `devtools.setup(ctx)` hook. The c | Property | Purpose | |----------|---------| -| `ctx.docks` | Register dock entries (iframe, action, custom-render, launcher) | +| `ctx.docks` | Register dock entries (iframe, action, custom-render, launcher, json-render) | | `ctx.views` | Host static files for UI | | `ctx.rpc` | Register RPC functions, broadcast to clients | | `ctx.rpc.sharedState` | Synchronized server-client state | | `ctx.logs` | Emit structured log entries and toast notifications | +| `ctx.terminals` | Spawn and manage child processes with streaming terminal output | +| `ctx.createJsonRenderer` | Create server-side JSON render specs for zero-client-code UIs | | `ctx.viteConfig` | Resolved Vite configuration | | `ctx.viteServer` | Dev server instance (dev mode only) | | `ctx.mode` | `'dev'` or `'build'` | @@ -122,6 +124,7 @@ export default function myAnalyzer(): Plugin { | Type | Use Case | |------|----------| | `iframe` | Full UI panels, dashboards (most common) | +| `json-render` | Server-side JSON specs — zero client code needed | | `action` | Buttons that trigger client-side scripts (inspectors, toggles) | | `custom-render` | Direct DOM access in panel (framework mounting) | | `launcher` | Actionable setup cards for initialization tasks | @@ -168,6 +171,44 @@ ctx.docks.register({ }) ``` +### JSON Render Entry + +Build UIs entirely from server-side TypeScript — no client code needed: + +```ts +const ui = ctx.createJsonRenderer({ + root: 'root', + elements: { + root: { + type: 'Stack', + props: { direction: 'vertical', gap: 12 }, + children: ['heading', 'info'], + }, + heading: { + type: 'Text', + props: { content: 'Hello from JSON!', variant: 'heading' }, + }, + info: { + type: 'KeyValueTable', + props: { + entries: [ + { key: 'Version', value: '1.0.0' }, + { key: 'Status', value: 'Running' }, + ], + }, + }, + }, +}) + +ctx.docks.register({ + id: 'my-panel', + title: 'My Panel', + icon: 'ph:chart-bar-duotone', + type: 'json-render', + ui, +}) +``` + ### Launcher Entry ```ts @@ -188,6 +229,31 @@ const entry = ctx.docks.register({ }) ``` +## Terminals & Subprocesses + +Spawn and manage child processes with streaming terminal output: + +```ts +const session = await ctx.terminals.startChildProcess( + { + command: 'vite', + args: ['build', '--watch'], + cwd: process.cwd(), + }, + { + id: 'my-plugin:build-watcher', + title: 'Build Watcher', + icon: 'ph:terminal-duotone', + }, +) + +// Lifecycle controls +await session.terminate() +await session.restart() +``` + +A common pattern is combining with launcher docks — see [Terminals Patterns](./references/terminals-patterns.md). + ## Logs & Notifications 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. @@ -426,6 +492,7 @@ Real-world example plugins in the repo — reference their code structure and pa - **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 - **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 +- **Git UI** ([`examples/plugin-git-ui`](https://github.com/vitejs/devtools/tree/main/examples/plugin-git-ui)) — JSON render dock entry, server-side JSON specs, `$bindState` two-way binding, `$state` in action params, dynamic badge updates ## Further Reading @@ -433,4 +500,6 @@ Real-world example plugins in the repo — reference their code structure and pa - [Dock Entry Types](./references/dock-entry-types.md) - Detailed dock configuration options - [Shared State Patterns](./references/shared-state-patterns.md) - Framework integration examples - [Project Structure](./references/project-structure.md) - Recommended file organization +- [JSON Render Patterns](./references/json-render-patterns.md) - Server-side JSON specs, components, state binding +- [Terminals Patterns](./references/terminals-patterns.md) - Child processes, custom streams, session lifecycle - [Logs Patterns](./references/logs-patterns.md) - Log entries, toast notifications, and handle patterns diff --git a/skills/vite-devtools-kit/references/dock-entry-types.md b/skills/vite-devtools-kit/references/dock-entry-types.md index 12e46d21..e9adfaeb 100644 --- a/skills/vite-devtools-kit/references/dock-entry-types.md +++ b/skills/vite-devtools-kit/references/dock-entry-types.md @@ -201,6 +201,93 @@ export default function setup(ctx: DevToolsClientScriptContext) { } ``` +## JSON Render Entries + +Server-side JSON specs rendered by the built-in component library. No client code needed. + +```ts +interface JsonRenderEntry extends DockEntryBase { + type: 'json-render' + ui: JsonRenderer // Handle from ctx.createJsonRenderer() +} + +// Registration +const ui = ctx.createJsonRenderer({ + root: 'root', + state: { query: '' }, + elements: { + root: { + type: 'Stack', + props: { direction: 'vertical', gap: 12 }, + children: ['heading', 'info'], + }, + heading: { + type: 'Text', + props: { content: 'My Panel', variant: 'heading' }, + }, + info: { + type: 'KeyValueTable', + props: { + entries: [ + { key: 'Status', value: 'Running' }, + ], + }, + }, + }, +}) + +ctx.docks.register({ + id: 'my-panel', + title: 'My Panel', + icon: 'ph:chart-bar-duotone', + type: 'json-render', + ui, +}) +``` + +### Dynamic Updates + +```ts +// Replace the entire spec +await ui.updateSpec(buildSpec(newData)) + +// Shallow-merge into spec.state +await ui.updateState({ query: 'vue' }) +``` + +### Action Handling + +Buttons trigger server-side RPC functions via `on.press.action`: + +```ts +// In spec element +{ + type: 'Button', + props: { label: 'Refresh', icon: 'ph:arrows-clockwise' }, + on: { press: { action: 'my-plugin:refresh' } }, +} + +// Matching RPC function +ctx.rpc.register(defineRpcFunction({ + name: 'my-plugin:refresh', + type: 'action', + setup: ctx => ({ + handler: async () => { + await ui.updateSpec(buildSpec(await fetchData())) + }, + }), +})) +``` + +### JSON Render Use Cases + +- **Build reports** — Display build stats, module lists, timing data +- **Configuration viewers** — Show resolved config with key-value tables +- **Status dashboards** — Progress bars, badges, real-time updates +- **Simple forms** — Text inputs with state binding + action buttons + +See [JSON Render Patterns](./json-render-patterns.md) for the full component library and state binding details. + ## Launcher Entries Actionable setup cards for running initialization tasks. Shows a card with title, description, and a launch button. diff --git a/skills/vite-devtools-kit/references/json-render-patterns.md b/skills/vite-devtools-kit/references/json-render-patterns.md new file mode 100644 index 00000000..84296192 --- /dev/null +++ b/skills/vite-devtools-kit/references/json-render-patterns.md @@ -0,0 +1,284 @@ +# JSON Render Patterns + +Build DevTools UIs entirely from server-side TypeScript — no client code needed. Describe your UI as a JSON spec, and the DevTools client renders it with the built-in component library. + +## Spec Structure + +A JSON render spec has three parts: a `root` element ID, an `elements` map, and an optional `state` object for two-way bindings. + +```ts +ctx.createJsonRenderer({ + root: 'root', + state: { + searchQuery: '', + }, + elements: { + root: { + type: 'Stack', + props: { direction: 'vertical', gap: 12 }, + children: ['title', 'content'], + }, + title: { + type: 'Text', + props: { content: 'My Panel', variant: 'heading' }, + }, + content: { + type: 'Text', + props: { content: 'Hello world' }, + }, + }, +}) +``` + +Every element has a `type` (component name), `props`, and optionally `children` (array of element IDs) or `on` (event handlers). + +## Registration + +Pass the renderer handle as `ui` when registering a `json-render` dock entry: + +```ts +const ui = ctx.createJsonRenderer(spec) + +ctx.docks.register({ + id: 'my-panel', + title: 'My Panel', + icon: 'ph:chart-bar-duotone', + type: 'json-render', + ui, +}) +``` + +## Dynamic Updates + +The `JsonRenderer` handle provides two methods for updating the UI reactively: + +```ts +const ui = ctx.createJsonRenderer(buildSpec(initialData)) + +// Replace the entire spec (e.g. after fetching new data) +await ui.updateSpec(buildSpec(newData)) + +// Shallow-merge into spec.state (updates client-side state values) +await ui.updateState({ searchQuery: 'vue' }) +``` + +Update the dock entry badge when data changes: + +```ts +ctx.docks.update({ + id: 'my-panel', + type: 'json-render', + title: 'My Panel', + icon: 'ph:chart-bar-duotone', + ui, + badge: hasWarnings ? '!' : undefined, +}) +``` + +## Handling Actions via RPC + +Buttons in the spec trigger RPC functions on the server via the `on` property: + +```ts +// In the spec — Button with an action +const ui = ctx.createJsonRenderer({ + root: 'refresh-btn', + elements: { + 'refresh-btn': { + type: 'Button', + props: { label: 'Refresh', icon: 'ph:arrows-clockwise' }, + on: { press: { action: 'my-plugin:refresh' } }, + }, + }, +}) + +// On the server — register the matching RPC function +ctx.rpc.register(defineRpcFunction({ + name: 'my-plugin:refresh', + type: 'action', + setup: ctx => ({ + handler: async () => { + const data = await fetchData() + await ui.updateSpec(buildSpec(data)) + }, + }), +})) +``` + +Pass parameters from the spec to the action handler: + +```ts +on: { + press: { + action: 'my-plugin:delete', + params: { id: 'some-id' }, + }, +} +``` + +## State and Two-Way Binding + +Use `$bindState` on TextInput `value` to create two-way binding with a state key. Use `$state` to read the bound value in action params: + +```ts +const ui = ctx.createJsonRenderer({ + root: 'root', + state: { message: '' }, + elements: { + root: { + type: 'Stack', + props: { direction: 'horizontal', gap: 8 }, + children: ['input', 'submit'], + }, + input: { + type: 'TextInput', + props: { + placeholder: 'Type here...', + value: { $bindState: '/message' }, + }, + }, + submit: { + type: 'Button', + props: { label: 'Submit', variant: 'primary' }, + on: { + press: { + action: 'my-plugin:submit', + params: { text: { $state: '/message' } }, + }, + }, + }, + }, +}) +``` + +The server-side handler receives the resolved state values: + +```ts +ctx.rpc.register(defineRpcFunction({ + name: 'my-plugin:submit', + type: 'action', + setup: ctx => ({ + handler: async (params: { text?: string }) => { + console.log('User submitted:', params.text) + }, + }), +})) +``` + +## Built-in Components + +### Layout + +| Component | Props | Description | +|-----------|-------|-------------| +| `Stack` | `direction`, `gap`, `align`, `justify`, `padding` | Flex layout container | +| `Card` | `title`, `collapsible` | Container with optional title, collapsible | +| `Divider` | `label` | Separator line with optional label | + +### Typography + +| Component | Props | Description | +|-----------|-------|-------------| +| `Text` | `content`, `variant` (`heading`/`body`/`caption`/`code`) | Display text | +| `Icon` | `name`, `size` | Iconify icon by name | +| `Badge` | `text`, `variant` (`info`/`success`/`warning`/`error`/`default`) | Status label | + +### Inputs + +| Component | Props | Description | +|-----------|-------|-------------| +| `Button` | `label`, `icon`, `variant` (`primary`/`secondary`/`ghost`/`danger`), `disabled` | Clickable button, fires `press` event | +| `TextInput` | `placeholder`, `value`, `label`, `disabled` | Text input, supports `$bindState` on `value` | + +### Data Display + +| Component | Props | Description | +|-----------|-------|-------------| +| `KeyValueTable` | `title`, `entries` (`Array<{ key, value }>`) | Two-column key-value table | +| `DataTable` | `columns`, `rows`, `maxHeight` | Tabular data with configurable columns | +| `CodeBlock` | `code`, `language`, `filename`, `maxHeight` | Code snippet with optional filename header | +| `Progress` | `value`, `max`, `label` | Progress bar with percentage | +| `Tree` | `data`, `expandLevel` | Expandable tree for nested objects | + +## Full Example + +```ts +import type { JsonRenderSpec, PluginWithDevTools } from '@vitejs/devtools-kit' +import { defineRpcFunction } from '@vitejs/devtools-kit' + +function buildSpec(data: { modules: number, time: string, size: string }): JsonRenderSpec { + return { + root: 'root', + state: { filter: '' }, + elements: { + 'root': { + type: 'Stack', + props: { direction: 'vertical', gap: 12, padding: 8 }, + children: ['header', 'divider', 'stats'], + }, + 'header': { + type: 'Stack', + props: { direction: 'horizontal', gap: 8, align: 'center', justify: 'space-between' }, + children: ['title', 'refresh-btn'], + }, + 'title': { + type: 'Text', + props: { content: 'Build Report', variant: 'heading' }, + }, + 'refresh-btn': { + type: 'Button', + props: { label: 'Refresh', icon: 'ph:arrows-clockwise' }, + on: { press: { action: 'build-report:refresh' } }, + }, + 'divider': { + type: 'Divider', + props: {}, + }, + 'stats': { + type: 'KeyValueTable', + props: { + title: 'Summary', + entries: [ + { key: 'Total Modules', value: String(data.modules) }, + { key: 'Build Time', value: data.time }, + { key: 'Output Size', value: data.size }, + ], + }, + }, + }, + } +} + +export function BuildReportPlugin(): PluginWithDevTools { + return { + name: 'build-report', + devtools: { + setup(ctx) { + const data = { modules: 142, time: '1.2s', size: '48 KB' } + const ui = ctx.createJsonRenderer(buildSpec(data)) + + ctx.docks.register({ + id: 'build-report', + title: 'Build Report', + icon: 'ph:chart-bar-duotone', + type: 'json-render', + ui, + }) + + ctx.rpc.register(defineRpcFunction({ + name: 'build-report:refresh', + type: 'action', + setup: ctx => ({ + handler: async () => { + const newData = { modules: 145, time: '1.1s', size: '47 KB' } + await ui.updateSpec(buildSpec(newData)) + }, + }), + })) + }, + }, + } +} +``` + +> See the [Git UI example](https://github.com/vitejs/devtools/tree/main/examples/plugin-git-ui) for a more advanced plugin using json-render with per-file actions, text input with state binding, and dynamic badge updates. diff --git a/skills/vite-devtools-kit/references/terminals-patterns.md b/skills/vite-devtools-kit/references/terminals-patterns.md new file mode 100644 index 00000000..0eed9d3c --- /dev/null +++ b/skills/vite-devtools-kit/references/terminals-patterns.md @@ -0,0 +1,123 @@ +# Terminals & Subprocesses + +Spawn and manage child processes from your plugin. Output streams in real-time to an xterm.js terminal inside DevTools. + +## Starting a Child Process + +```ts +const session = await ctx.terminals.startChildProcess( + { + command: 'vite', + args: ['build', '--watch'], + cwd: process.cwd(), + env: { NODE_ENV: 'development' }, + }, + { + id: 'my-plugin:build-watcher', + title: 'Build Watcher', + icon: 'ph:terminal-duotone', + }, +) +``` + +### Execute Options + +```ts +interface DevToolsChildProcessExecuteOptions { + command: string + args: string[] + cwd?: string + env?: Record +} +``` + +The second argument provides terminal metadata (`id`, `title`, and optional `description`/`icon`). + +> Color output is enabled automatically — `FORCE_COLOR` and `COLORS` environment variables are set to `'true'` by default. + +## Session Lifecycle + +`startChildProcess()` returns a `DevToolsChildProcessTerminalSession` with lifecycle controls: + +```ts +// Terminate the process +await session.terminate() + +// Restart (kill + re-spawn) +await session.restart() + +// Access the underlying Node.js ChildProcess +const cp = session.getChildProcess() +``` + +### Session Status + +| Status | Description | +|--------|-------------| +| `running` | Process is active and streaming output | +| `stopped` | Process exited normally | +| `error` | Process exited with an error | + +Update a session's metadata or status at any time: + +```ts +ctx.terminals.update({ + id: 'my-plugin:build-watcher', + status: 'stopped', + title: 'Build Watcher (done)', +}) +``` + +## Combining with Launcher Docks + +A common pattern is pairing a launcher dock entry with a terminal session. The launcher gives the user a button to start the process on demand: + +```ts +ctx.docks.register({ + id: 'my-plugin:launcher', + title: 'My App', + icon: 'ph:rocket-launch-duotone', + type: 'launcher', + launcher: { + title: 'Start My App', + description: 'Launch the dev server', + onLaunch: async () => { + await ctx.terminals.startChildProcess( + { + command: 'vite', + args: ['dev'], + cwd: process.cwd(), + }, + { + id: 'my-plugin:dev-server', + title: 'Dev Server', + }, + ) + }, + }, +}) +``` + +## Custom Terminal Sessions + +For scenarios that don't involve spawning a child process (e.g. streaming logs from an external source), register a session directly with a custom `ReadableStream`: + +```ts +let controller: ReadableStreamDefaultController + +const stream = new ReadableStream({ + start(c) { + controller = c + }, +}) + +ctx.terminals.register({ + id: 'my-plugin:custom-stream', + title: 'Custom Output', + status: 'running', + stream, +}) + +// Push data to the terminal +controller.enqueue('Hello from custom stream!\n') +```