From e756fca12641633e12bd94906bd4337bbf104d04 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Thu, 2 Jul 2026 08:07:53 +0000 Subject: [PATCH 1/3] docs(kit): add client script & client context page --- docs/.vitepress/config.ts | 2 + docs/kit/client-context.md | 111 +++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 docs/kit/client-context.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 3cee248a..258795b3 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -25,6 +25,7 @@ const DevToolsKitNav = [ { 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: 'Remote Client', link: '/kit/remote-client' }, { text: 'RPC', link: '/kit/rpc' }, { text: 'Shared State', link: '/kit/shared-state' }, @@ -121,6 +122,7 @@ export default extendConfig(withMermaid(defineConfig({ { 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: 'Remote Client', link: '/kit/remote-client' }, { text: 'RPC', link: '/kit/rpc' }, { text: 'Shared State', link: '/kit/shared-state' }, diff --git a/docs/kit/client-context.md b/docs/kit/client-context.md new file mode 100644 index 00000000..9b51a636 --- /dev/null +++ b/docs/kit/client-context.md @@ -0,0 +1,111 @@ +--- +outline: deep +--- + +# Client Script & Client 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. + +## 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: + +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`. + +### 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 `virtual:vite-devtools-injection` module, which in turn loads the client entry: + +```mermaid +sequenceDiagram + participant Vite as Vite Dev Server + participant Page as Host Page + participant Server as DevTools Server + + Vite->>Page: transformIndexHtml appends
import "virtual:vite-devtools-injection" + Page->>Page: loads @vitejs/devtools/client/inject + Page->>Server: RPC connect (/__devtools/) + Page->>Page: publish client context, mount dock +``` + +Injection is scoped to where the embedded client makes sense: + +- **Dev server only** — `vite build` uses the [standalone client](/guide/#standalone-mode) instead, which hosts the same context in its own page. +- **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. + +## 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: + +```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`) + }) +} +``` + +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 + +### The client script isn't 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 + +- **Plugin registered?** The `DevTools()` plugin from `@vitejs/devtools` must be in your Vite config's `plugins` for injection to run. +- **Dev mode?** The embedded client is a dev-server feature. For `vite build`, use the [standalone client](/guide/#standalone-mode) (`devtools: { enabled: true }`). +- **Dock appears but asks for authorization?** That's client trust, a separate layer from injection — see [DTK0008](/errors/DTK0008) and the `devtools.clientAuth` option. From 6ed97e4704faabddf75d62ef965ecbd5a5727627 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Thu, 2 Jul 2026 08:10:01 +0000 Subject: [PATCH 2/3] docs: move Remote Client nav entry above Examples --- docs/.vitepress/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 258795b3..8fc75f2c 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -26,7 +26,6 @@ const DevToolsKitNav = [ { text: 'DevTools Plugin', link: '/kit/devtools-plugin' }, { text: 'Dock System', link: '/kit/dock-system' }, { text: 'Client Script & Context', link: '/kit/client-context' }, - { text: 'Remote Client', link: '/kit/remote-client' }, { text: 'RPC', link: '/kit/rpc' }, { text: 'Shared State', link: '/kit/shared-state' }, { text: 'Streaming', link: '/kit/streaming' }, @@ -35,6 +34,7 @@ const DevToolsKitNav = [ { 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' }, ] @@ -123,7 +123,6 @@ export default extendConfig(withMermaid(defineConfig({ { text: 'DevTools Plugin', link: '/kit/devtools-plugin' }, { text: 'Dock System', link: '/kit/dock-system' }, { text: 'Client Script & Context', link: '/kit/client-context' }, - { text: 'Remote Client', link: '/kit/remote-client' }, { text: 'RPC', link: '/kit/rpc' }, { text: 'Shared State', link: '/kit/shared-state' }, { text: 'Streaming', link: '/kit/streaming' }, @@ -133,6 +132,7 @@ export default extendConfig(withMermaid(defineConfig({ { 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' }, ], }, From 390c1eb7e3f9266e2d18d50e9bb0d22df84a9f40 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Thu, 2 Jul 2026 08:12:47 +0000 Subject: [PATCH 3/3] docs: deduplicate client script/context coverage into the new page --- docs/guide/index.md | 4 ++-- docs/kit/client-context.md | 2 +- docs/kit/dock-system.md | 2 +- docs/kit/rpc.md | 11 +---------- docs/kit/shared-state.md | 11 +---------- 5 files changed, 6 insertions(+), 24 deletions(-) diff --git a/docs/guide/index.md b/docs/guide/index.md index 8e8a151f..c15461ff 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -101,13 +101,13 @@ Open your app in the browser; the DevTools panel appears in the corner. #### Projects without an HTML entry -The embedded client is normally injected through Vite's `transformIndexHtml` hook. For apps that start from a JS entry instead, import the client injector from a browser entry (`main.ts`, `entry.client.ts`) and skip server-only or shared SSR files: +For apps where Vite doesn't serve the HTML (JS-only entries, backend integration, middleware mode), import the client injector from a browser entry instead: ```ts twoslash import '@vitejs/devtools/client/inject' ``` -Use this only when there's no HTML entry — combining it with HTML injection mounts the client twice. +See [Client Script & Context](/kit/client-context#client-script-not-injected) for how injection works and the full troubleshooting checklist. #### Building with the app diff --git a/docs/kit/client-context.md b/docs/kit/client-context.md index 9b51a636..96b1cdf2 100644 --- a/docs/kit/client-context.md +++ b/docs/kit/client-context.md @@ -81,7 +81,7 @@ Iframe panels run in their own document, so they create their own RPC client wit ## Troubleshooting -### The client script isn't injected +### Client script not injected Symptoms: the dock never appears, `getDevToolsClientContext()` always returns `undefined`, and the browser console has no `[VITE DEVTOOLS] Client injected` log. diff --git a/docs/kit/dock-system.md b/docs/kit/dock-system.md index 538dd764..26655c67 100644 --- a/docs/kit/dock-system.md +++ b/docs/kit/dock-system.md @@ -149,7 +149,7 @@ ctx.docks.register({ ### Client script -The action script runs in the user's browser: +The action script runs in the user's browser. It receives the [client context](/kit/client-context), extended with the dock-scoped `current` (entry state and events) and `messages`: ```ts // src/devtools-action.ts diff --git a/docs/kit/rpc.md b/docs/kit/rpc.md index 7c78e748..3b2c6c53 100644 --- a/docs/kit/rpc.md +++ b/docs/kit/rpc.md @@ -449,16 +449,7 @@ export const getData = defineRpcFunction({ ### Global client context -`getDevToolsClientContext()` returns the `DevToolsClientContext` from anywhere on the client side. DevTools sets it automatically in embedded or standalone mode, and the function returns `undefined` until initialization completes. - -```ts -import { getDevToolsClientContext } from '@vitejs/devtools-kit/client' - -const ctx = getDevToolsClientContext() -if (ctx) { - const modules = await ctx.rpc.call('my-plugin:get-modules') -} -``` +Beyond RPC, the full client context — docks, commands, panel state — is available anywhere in the host page via `getDevToolsClientContext()`. See [Client Script & Context](/kit/client-context). ## Client-side functions diff --git a/docs/kit/shared-state.md b/docs/kit/shared-state.md index e18bb760..5630ce3a 100644 --- a/docs/kit/shared-state.md +++ b/docs/kit/shared-state.md @@ -117,16 +117,7 @@ const state = await client.sharedState.get('my-plugin:state') console.log(state.value()) ``` -The global client context exposes shared state too: - -```ts -import { getDevToolsClientContext } from '@vitejs/devtools-kit/client' - -const ctx = getDevToolsClientContext() -if (ctx) { - const state = await ctx.rpc.sharedState.get('my-plugin:state') -} -``` +The [global client context](/kit/client-context) exposes the same API via `ctx.rpc.sharedState`. ### Subscribing to changes