Skip to content
Closed
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
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ jobs:
run: nr knip
continue-on-error: true

docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Install pnpm
uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b
with:
install: false

- name: Set node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: lts/*

- name: Setup
run: npm i -g @antfu/ni

- name: Install
run: nci

- name: Build docs
run: nr docs:build

test:
runs-on: ${{ matrix.os }}
strategy:
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/src/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,33 @@ jobs:
run: nr knip
continue-on-error: true

docs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v5

- name: Install pnpm
uses: pnpm/setup@703c52620218391530e48b9e8870d5c0082e1b9b
with:
install: false

- name: Set node
uses: actions/setup-node@v5
with:
node-version: lts/*

- name: Setup
run: npm i -g @antfu/ni

- name: Install
run: nci

# Mirrors the Netlify build command so a broken docs build is caught in
# CI instead of silently leaving the published site on a stale deploy.
- name: Build docs
run: nr docs:build

test:
runs-on: ${{ matrix.os }}

Expand Down
22 changes: 2 additions & 20 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,14 @@ const DevToolsKitNav = [
{ text: 'Introduction', link: '/kit/' },
{ text: 'DevTools Plugin', link: '/kit/devtools-plugin' },
{ text: 'Dock System', link: '/kit/dock-system' },
{ text: 'Create Plugin from Devframe', link: '/kit/create-plugin-from-devframe' },
{ text: 'Client Script & Context', link: '/kit/client-context' },
{ text: 'RPC', link: '/kit/rpc' },
{ text: 'Shared State', link: '/kit/shared-state' },
{ text: 'Streaming', link: '/kit/streaming' },
{ text: 'Commands', link: '/kit/commands' },
{ text: 'When Clauses', link: '/kit/when-clauses' },
{ text: 'Messages & Notifications', link: '/kit/messages' },
{ text: 'Structured Diagnostics', link: '/kit/diagnostics' },
{ text: 'Terminals & Processes', link: '/kit/terminals' },
{ text: 'Remote Client', link: '/kit/remote-client' },
{ text: 'Examples', link: '/kit/examples' },
]

Expand Down Expand Up @@ -125,23 +123,7 @@ export default extendConfig(withMermaid(defineConfig({
},
{
text: 'DevTools Kit',
items: [
{ text: 'Introduction', link: '/kit/' },
{ text: 'DevTools Plugin', link: '/kit/devtools-plugin' },
{ text: 'Dock System', link: '/kit/dock-system' },
{ text: 'Client Script & Context', link: '/kit/client-context' },
{ text: 'RPC', link: '/kit/rpc' },
{ text: 'Shared State', link: '/kit/shared-state' },
{ text: 'Streaming', link: '/kit/streaming' },
{ text: 'Commands', link: '/kit/commands' },
{ text: 'When Clauses', link: '/kit/when-clauses' },
{ text: 'Messages', link: '/kit/messages' },
{ text: 'Diagnostics', link: '/kit/diagnostics' },
{ text: 'JSON Render', link: '/kit/json-render' },
{ text: 'Terminals', link: '/kit/terminals' },
{ text: 'Remote Client', link: '/kit/remote-client' },
{ text: 'Examples', link: '/kit/examples' },
],
items: DevToolsKitNav,
},
{
text: 'Error Reference',
Expand Down
2 changes: 1 addition & 1 deletion docs/errors/DTK0033.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Running the dev server against a project whose `node_modules` directory is mount

## Fix

Ensure the project's `node_modules` directory is writable by the user running the dev server. If `node_modules` must stay read-only, use `visibility: 'normal'` (the default) in the DevTools plugin options to always show the docks instead of relying on the persisted flag.
Ensure the project's `node_modules` directory is writable by the user running the dev server. If `node_modules` must stay read-only, use `embeddedVisibility: 'normal'` (the default) in the DevTools plugin options to always show the docks instead of relying on the persisted flag.

## Source

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ import '@vitejs/devtools/client/inject-passive'
import '@vitejs/devtools/client/inject-hidden'
```

See [Client Script & Context](/kit/client-context#client-script-not-injected) for how injection works and the full troubleshooting checklist.
See the [Devframe client guide](https://devfra.me/guide/client) for how injection and the browser RPC client work.

### Building with the app

Expand Down
105 changes: 17 additions & 88 deletions docs/kit/client-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,110 +2,39 @@
outline: deep
---

# Client Script & Client Context
# Client Script & Context

In embedded mode, Vite DevTools injects a small **client script** into your app's page. The script boots the dock and publishes the **client context** — the object that every client-side surface (dock client scripts, action buttons, your own app code) uses to talk to DevTools.
In embedded mode, Vite DevTools injects a small **client script** into your app's page. It boots the dock and publishes the **client context** — the object every client-side surface (dock action scripts, custom renderers, your own app code) uses to talk to DevTools.

## The client script

The client script is the browser entry of Vite DevTools (published as `@vitejs/devtools/client/inject`). When it runs in the host page it:
The client script (`@vitejs/devtools/client/inject`) connects an RPC client to the DevTools server at `/__devtools/`, builds the client context on top of it, and mounts the embedded dock. The `DevTools()` plugin injects it automatically:

1. Connects an RPC client to the DevTools server at `/__devtools/` (WebSocket in dev mode).
2. Builds the `DevToolsClientContext` — dock entries, panel state, commands, when-clauses — on top of that RPC client.
3. Publishes the context to a global slot, so `getDevToolsClientContext()` can read it from anywhere in the page.
4. Mounts the embedded dock web component into `document.body`.
- **During `vite dev`** via `transformIndexHtml`. A production build ships the same bootstrap through [`build.withApp`](/guide/#building-with-the-app).
- **In client environments only** — SSR builds and server code stay untouched.
- **In top-level windows only** — inside an iframe the script exits, so a page never mounts a second dock.

### How injection works

The `DevTools()` plugin injects the script through Vite's `transformIndexHtml` hook. During `vite dev`, every HTML page served by Vite receives a module script that imports the injection virtual module, which in turn loads the client entry. The plugin picks the entry from the project's resolved visibility: `@vitejs/devtools/client/inject` (docks shown immediately) by default, `@vitejs/devtools/client/inject-passive` (docks hidden until <kbd>Shift</kbd>+<kbd>Alt</kbd>+<kbd>D</kbd>, then remembered) for passive mode, or `@vitejs/devtools/client/inject-hidden` (revealed per session, never remembered) for hidden mode:

```mermaid
sequenceDiagram
participant Vite as Vite Dev Server
participant Page as Host Page
participant Server as DevTools Server

Vite->>Page: transformIndexHtml appends<br/>import "virtual:vite-devtools-injection"
Page->>Page: loads @vitejs/devtools/client/inject
Page->>Server: RPC connect (/__devtools/)
Page->>Page: publish client context, mount dock
```

Automatic injection is scoped to where the embedded client makes sense:

- **Automatic HTML injection during development** — `transformIndexHtml` mounts the embedded client in the dev server. A production build can ship the same embedded bootstrap through [`build.withApp`](/guide/#building-with-the-app).
- **Client environments only** — SSR builds and server code stay untouched.
- **Top-level windows only** — inside an iframe (including DevTools' own iframe panels) the script logs `[VITE DEVTOOLS] Skipping in iframe` and exits, so a page never mounts a second dock.
Which entry is injected follows the resolved `embeddedVisibility`: `inject` (shown immediately), `inject-passive` (hidden until <kbd>Shift</kbd>+<kbd>Alt</kbd>+<kbd>D</kbd>, then remembered), or `inject-hidden` (revealed per session). For projects without an HTML entry, import one of these entries yourself — see [Getting Started](/guide/#projects-without-an-html-entry).

## The client context

`DevToolsClientContext` is the client-side counterpart of the [node context](./devtools-plugin): one object carrying everything a client surface needs.

| Property | Description |
|----------|-------------|
| `rpc` | The RPC client — `call()` server functions, register [client-side functions](/kit/rpc#client-side-functions), access shared state and streaming. |
| `clientType` | `'embedded'` (dock inside your app) or `'standalone'` (independent DevTools page). |
| `docks` | Dock entries and selection — `entries`, `selected`, `switchEntry()`, `toggleEntry()`. |
| `panel` | Dock panel state: position, size, drag/resize flags. |
| `commands` | The [command palette](./commands): `register()`, `execute()`, keybindings. |
| `when` | The [when-clause](./when-clauses) evaluation context. |

### Accessing the context

From anywhere in the host page, use `getDevToolsClientContext()`. It returns `undefined` until the client script finishes initializing:
`DevToolsClientContext` is the client-side counterpart of the [node context](./devtools-plugin). Read it anywhere in the host page with `getDevToolsClientContext()`, which returns `undefined` until the script finishes initializing:

```ts
import { getDevToolsClientContext } from '@vitejs/devtools-kit/client'

const ctx = getDevToolsClientContext()
if (ctx) {
const modules = await ctx.rpc.call('my-plugin:get-modules')
ctx.docks.switchEntry('my-plugin')
}
```

[Dock client scripts](/kit/dock-system#client-script) — action buttons and custom renderers — receive the context directly as their argument, extended with two dock-scoped extras: `current` (this entry's state, DOM elements, and events) and `messages` (a [messages client](./messages) scoped to the entry):

```ts
import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'

export default function setup(ctx: DockClientScriptContext) {
ctx.current.events.on('entry:activated', async () => {
const data = await ctx.rpc.call('my-plugin:get-modules')
ctx.messages.info(`Loaded ${data.length} modules`)
})
const data = await ctx.rpc.call('my-plugin:get-modules')
}
```

Iframe panels run in their own document, so they create their own RPC client with [`getDevToolsRpcClient()`](/kit/rpc#in-iframe-pages) instead — the connection details are discovered automatically from the parent window.

## Troubleshooting

### Client script not injected

Symptoms: the dock never appears, `getDevToolsClientContext()` always returns `undefined`, and the browser console has no `[VITE DEVTOOLS] Client injected` log.

Injection rides on Vite's `transformIndexHtml` hook, so it requires an HTML page that Vite itself serves and transforms. Setups where the HTML comes from elsewhere skip it:

- **Backend integration** — Rails, Laravel, Django, or any server rendering its own HTML while Vite only serves assets.
- **Middleware mode** — an app framework embedding Vite's dev server without serving `index.html` through it.
- **JS-only entries** — projects whose entry point is a script rather than an HTML file.

The fix is to import the client injector manually from a browser entry (`main.ts`, `entry.client.ts`):

```ts
import '@vitejs/devtools/client/inject'
```

Keep the import out of server-only and shared SSR files, and use it only when HTML injection doesn't happen — combining both mounts the client twice. To keep the client out of production bundles, guard it as a dev-only dynamic import:

```ts
if (import.meta.env.DEV)
import('@vitejs/devtools/client/inject')
```

### Other checks
| Property | Description |
|----------|-------------|
| `rpc` | The [RPC](./rpc) client — call server functions, register client functions, reach shared state |
| `clientType` | `'embedded'` (dock inside your app) or `'standalone'` (independent DevTools page) |
| `docks` | Dock entries and selection — `entries`, `selected`, `switchEntry()` |
| `panel` | Dock panel state: position, size, drag/resize flags |
| `commands` | The [command palette](./commands): `register()`, `execute()`, keybindings |

- **Integration enabled?** Use Vite's `devtools` config, or register the `DevTools()` plugin from `@vitejs/devtools` manually.
- **Build output?** Enable build-time collection with `devtools: { apply: 'build' }`; use [`build.withApp`](/guide/#building-with-the-app) when the generated app should include the embedded client.
- **Dock appears but asks for authorization?** That's client trust, a separate layer from injection — see [DTK0008](/errors/DTK0008) and the `devtools.clientAuth` option.
The browser RPC client and connection modes are documented in the [Devframe client guide](https://devfra.me/guide/client).
Loading
Loading