diff --git a/docs/guide/index.md b/docs/guide/index.md
index 4546ecac4..f2f2ab55c 100644
--- a/docs/guide/index.md
+++ b/docs/guide/index.md
@@ -42,64 +42,37 @@ Install the required DevTools package:
pnpm add -D @vitejs/devtools
```
-Vite DevTools has two client modes. Pick one.
-
-### Standalone mode
-
-The DevTools client runs in a standalone window.
-
-Configure `vite.config.ts`:
+Enable Vite DevTools in `vite.config.ts`:
```ts [vite.config.ts] twoslash
import { defineConfig } from 'vite'
export default defineConfig({
- devtools: {
- enabled: true,
- },
+ devtools: true,
})
```
-Run:
-
-```bash
-pnpm build
-```
-
-After the build completes, open the DevTools URL printed in the terminal.
+`devtools: true` enables DevTools for both `vite dev` and `vite build`.
-### Embedded mode
+### Limit DevTools to dev or build
-The DevTools client runs as a floating panel inside the user app.
-
-Configure `vite.config.ts`:
+The default `apply` value is `'all'`. Set it to `'serve'` or `'build'` to enable DevTools for only that command:
```ts [vite.config.ts] twoslash
-import { DevTools } from '@vitejs/devtools'
import { defineConfig } from 'vite'
export default defineConfig({
- plugins: [
- DevTools(),
- ],
- build: {
- rolldownOptions: {
- devtools: {}, // enable devtools mode
- },
- }
+ devtools: {
+ apply: 'serve',
+ },
})
```
-Run:
-
-```bash
-pnpm build
-pnpm dev
-```
+### Customize the embedded UI
-Open your app in the browser; the floating docks appear in the corner.
+Vite adds the embedded dock automatically during `vite dev`. To customize it, add the `DevTools()` plugin manually. The examples keep the automatic integration enabled only for build to avoid mounting the dock twice.
-The `embeddedVisibility` option sets the starting mode. The default `'normal'` shows the docks immediately. `'passive'` keeps them out of the way and prints a console hint to reveal them with Shift + Alt + D (⇧ ⌥ D on macOS); revealing once persists per-origin in the browser, so later sessions on this browser open straight into the docks, and the "Hide DevTools" command returns to passive mode. `'hidden'` also starts hidden but never remembers — the shortcut reveals the docks for the current session only.
+`embeddedVisibility` controls when the dock appears. The default `'normal'` shows it immediately. `'passive'` hides it until Shift + Alt + D (⇧ ⌥ D on macOS) and remembers when it has been revealed. `'hidden'` uses the same shortcut without remembering the choice.
```ts [vite.config.ts] twoslash
import { DevTools } from '@vitejs/devtools'
@@ -111,10 +84,13 @@ export default defineConfig({
embeddedVisibility: 'passive',
}),
],
+ devtools: {
+ apply: 'build',
+ },
})
```
-The `dockPreferences` option seeds the dock bar's first-run layout — category ordering, the floating dock's inline-item capacity, and the default float/edge mode and position. Each is a user-overridable preference, so the visitor's own choice wins from then on.
+Use `dockPreferences` to set the initial dock layout. Users can still change these settings in DevTools.
```ts [vite.config.ts] twoslash
import { DevTools } from '@vitejs/devtools'
@@ -129,6 +105,9 @@ export default defineConfig({
},
}),
],
+ devtools: {
+ apply: 'build',
+ },
})
```
@@ -153,9 +132,9 @@ 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.
-#### Building with the app
+### Building with the app
-Generate a static DevTools build alongside the app build by enabling `build.withApp`:
+Set `build.withApp` to write the static DevTools files alongside the app build:
```ts [vite.config.ts] twoslash
import { DevTools } from '@vitejs/devtools'
@@ -170,19 +149,17 @@ export default defineConfig({
},
}),
],
- build: {
- rolldownOptions: {
- devtools: {},
- },
+ devtools: {
+ apply: 'build',
}
})
```
-`build.withApp` writes the DevTools static output into the build directory using the same build context, so the analysis panels reflect the real build with no separate command.
+Open `/__devtools/` for the full-page UI, or load `/__devtools/embedded.js` to embed the dock in the built app.
## What's next
-- **Explore the built-in tools** — open the [DevTools for Rolldown](/rolldown/) panels.
+- **Explore the built-in tools** — inspect Vite development with [Vite DevTools](/vite/) and production builds with [DevTools for Rolldown](/rolldown/).
- **Build custom integrations** — extend DevTools with the [Vite DevTools Kit](/kit/).
- **Contribute** — see the [contributing guide](https://github.com/antfu/contribute).
diff --git a/docs/kit/client-context.md b/docs/kit/client-context.md
index 870acf1e4..12e61bcc5 100644
--- a/docs/kit/client-context.md
+++ b/docs/kit/client-context.md
@@ -31,9 +31,9 @@ sequenceDiagram
Page->>Page: publish client context, mount dock
```
-Injection is scoped to where the embedded client makes sense:
+Automatic 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.
+- **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.
@@ -106,6 +106,6 @@ if (import.meta.env.DEV)
### 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 }`).
+- **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.
diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts
index ca480bd9b..7aac62feb 100644
--- a/packages/core/src/integration.ts
+++ b/packages/core/src/integration.ts
@@ -7,7 +7,7 @@ export interface DevToolsIntegrationOptions {
config: unknown
}
-export function DevToolsIntegration(options: DevToolsIntegrationOptions): { name: string } {
+export function DevToolsIntegration(options: DevToolsIntegrationOptions): Promise<{ name: string }[]> {
return _DevToolsIntegration(options as Parameters[0])
}
diff --git a/packages/core/src/node/__tests__/config.test.ts b/packages/core/src/node/__tests__/config.test.ts
new file mode 100644
index 000000000..9f05d09f2
--- /dev/null
+++ b/packages/core/src/node/__tests__/config.test.ts
@@ -0,0 +1,51 @@
+import { describe, expect, it } from 'vitest'
+import { isDevToolsEnabled, normalizeDevToolsConfig } from '../config'
+
+describe('normalizeDevToolsConfig', () => {
+ it.each([
+ { raw: undefined, enabled: false },
+ { raw: false, enabled: false },
+ { raw: true, enabled: true },
+ { raw: {}, enabled: true },
+ { raw: { enabled: false }, enabled: false },
+ { raw: { apply: 'serve' }, enabled: true },
+ { raw: { apply: 'build' }, enabled: true },
+ { raw: { apply: 'all' }, enabled: true },
+ ] as const)('normalizes $raw', ({ raw, enabled }) => {
+ expect(normalizeDevToolsConfig(raw, 'localhost').enabled).toBe(enabled)
+ })
+
+ it('keeps apply separate from runtime options', () => {
+ expect(normalizeDevToolsConfig({ apply: 'serve' }, 'localhost')).toEqual({
+ apply: 'serve',
+ enabled: true,
+ config: {
+ clientAuth: true,
+ clientAuthTokens: [],
+ host: 'localhost',
+ },
+ })
+ })
+
+ it('normalizes an omitted apply option to all', () => {
+ expect(normalizeDevToolsConfig(true, 'localhost').apply).toBe('all')
+ })
+
+ it.each([
+ { apply: 'all', command: 'serve', enabled: true },
+ { apply: 'all', command: 'build', enabled: true },
+ { apply: 'serve', command: 'serve', enabled: true },
+ { apply: 'serve', command: 'build', enabled: false },
+ { apply: 'build', command: 'serve', enabled: false },
+ { apply: 'build', command: 'build', enabled: true },
+ ] as const)('checks $apply against $command', ({ apply, command, enabled }) => {
+ const config = normalizeDevToolsConfig({ apply }, 'localhost')
+ expect(isDevToolsEnabled(config, command)).toBe(enabled)
+ })
+
+ it('keeps every command disabled when enabled is false', () => {
+ const config = normalizeDevToolsConfig({ enabled: false, apply: 'all' }, 'localhost')
+ expect(isDevToolsEnabled(config, 'serve')).toBe(false)
+ expect(isDevToolsEnabled(config, 'build')).toBe(false)
+ })
+})
diff --git a/packages/core/src/node/__tests__/integration.test.ts b/packages/core/src/node/__tests__/integration.test.ts
new file mode 100644
index 000000000..cc5ceed8c
--- /dev/null
+++ b/packages/core/src/node/__tests__/integration.test.ts
@@ -0,0 +1,75 @@
+import type { Plugin, ResolvedConfig } from 'vite'
+import { describe, expect, it } from 'vitest'
+import { DevToolsIntegration } from '../plugins/integration'
+
+function createConfig(command: 'serve' | 'build', apply: 'serve' | 'build' | 'all' = command): ResolvedConfig {
+ return {
+ command,
+ root: '/vite-devtools-test-project',
+ devtools: {
+ apply,
+ config: {},
+ enabled: true,
+ },
+ } as unknown as ResolvedConfig
+}
+
+describe('devToolsIntegration', () => {
+ it('returns the existing DevTools plugins for serve', async () => {
+ const plugins = await DevToolsIntegration({ config: createConfig('serve') })
+
+ expect((plugins as Plugin[]).map(plugin => plugin.name)).toEqual([
+ 'vite:devtools:builtin',
+ 'vite:devtools:injection',
+ 'vite:devtools:server',
+ ])
+ })
+
+ it('returns the build integration plugin for build', async () => {
+ const [plugin] = await DevToolsIntegration({ config: createConfig('build') })
+
+ expect(plugin).toMatchObject({
+ name: 'vite:devtools:integration',
+ apply: 'build',
+ })
+ })
+
+ it.each([
+ { command: 'serve', expected: 'post' },
+ { command: 'build', expected: undefined },
+ ] as const)('uses the current $command integration when apply is all', async ({ command, expected }) => {
+ const plugins = await DevToolsIntegration({ config: createConfig(command, 'all') })
+ const plugin = command === 'serve'
+ ? plugins.find(plugin => plugin.name === 'vite:devtools:server')
+ : plugins[0]
+
+ expect(plugin?.enforce).toBe(expected)
+ })
+
+ it('returns no plugins when apply excludes the current command', async () => {
+ const plugins = await DevToolsIntegration({ config: createConfig('serve', 'build') })
+
+ expect(plugins).toEqual([])
+ })
+
+ it('enables Rolldown DevTools for selected build environments', async () => {
+ const [plugin] = await DevToolsIntegration({ config: createConfig('build') })
+ const client: { build: { rolldownOptions: { devtools?: object } } } = { build: { rolldownOptions: {} } }
+ const ssr: { build: { rolldownOptions: { devtools?: object } } } = { build: { rolldownOptions: {} } }
+ const config = {
+ devtools: {
+ config: { environments: ['client'] },
+ enabled: true,
+ },
+ environments: { client, ssr },
+ } as unknown as ResolvedConfig
+
+ const configResolved = plugin?.configResolved
+ if (typeof configResolved !== 'object')
+ throw new TypeError('Expected an object configResolved hook')
+ await configResolved.handler.call({} as never, config)
+
+ expect(client.build.rolldownOptions.devtools).toEqual({})
+ expect(ssr.build.rolldownOptions.devtools).toBeUndefined()
+ })
+})
diff --git a/packages/core/src/node/config.ts b/packages/core/src/node/config.ts
index 3306cdab8..721c72eac 100644
--- a/packages/core/src/node/config.ts
+++ b/packages/core/src/node/config.ts
@@ -1,7 +1,19 @@
import type { StartOptions } from './cli-commands'
+export type DevToolsApply = 'serve' | 'build' | 'all'
+
export interface DevToolsConfig extends Partial {
- enabled: boolean
+ /**
+ * Enable Vite DevTools.
+ *
+ * @default true
+ */
+ enabled?: boolean
+ /**
+ * Limit Vite DevTools to a specific Vite command.
+ * By default, Vite DevTools applies to both serve and build.
+ */
+ apply?: DevToolsApply
/**
* Vite environments to enable DevTools for. Defaults to all environments.
*/
@@ -36,8 +48,9 @@ export interface DevToolsConfig extends Partial {
}
export interface ResolvedDevToolsConfig {
- config: Omit & { host: string }
+ config: Omit & { host: string }
enabled: boolean
+ apply: DevToolsApply
}
export function normalizeDevToolsConfig(
@@ -45,13 +58,23 @@ export function normalizeDevToolsConfig(
host: string,
): ResolvedDevToolsConfig {
const resolved = typeof config === 'object' && config !== null ? config : undefined
+ const enabled = config === true || (resolved != null && (resolved.enabled ?? true))
+ const { enabled: _enabled, apply = 'all', ...options } = resolved ?? {}
return {
- enabled: config === true || !!(config && config.enabled),
+ enabled,
+ apply,
config: {
- ...(resolved ?? {}),
+ ...options,
clientAuth: resolved?.clientAuth ?? true,
clientAuthTokens: resolved?.clientAuthTokens ?? [],
host: resolved?.host ?? host,
},
}
}
+
+export function isDevToolsEnabled(
+ config: ResolvedDevToolsConfig,
+ command: 'serve' | 'build',
+): boolean {
+ return config.enabled && (config.apply === 'all' || config.apply === command)
+}
diff --git a/packages/core/src/node/plugins/integration.ts b/packages/core/src/node/plugins/integration.ts
index 456aae6ea..332ca20bc 100644
--- a/packages/core/src/node/plugins/integration.ts
+++ b/packages/core/src/node/plugins/integration.ts
@@ -1,5 +1,7 @@
import type { Plugin, ResolvedConfig, ViteBuilder } from 'vite'
import type { ResolvedDevToolsConfig } from '../config'
+import { isDevToolsEnabled } from '../config'
+import { DevTools } from './index'
type DevToolsEnvironment = ResolvedConfig['environments'][string]
@@ -24,6 +26,8 @@ function getDevToolsEnvironments(config: ResolvedConfig): DevToolsEnvironment[]
export async function runDevTools(builder: unknown) {
const config = (builder as ViteBuilder).config
+ if (!isDevToolsEnabled(config.devtools as ResolvedDevToolsConfig, config.command))
+ return
for (const _environment of getDevToolsEnvironments(config)) {
try {
const { start } = await import('../cli-commands')
@@ -38,7 +42,7 @@ export async function runDevTools(builder: unknown) {
}
}
-export function DevToolsIntegration(_options: DevToolsIntegrationOptions): Plugin {
+function DevToolsBuildIntegration(): Plugin {
return {
name: 'vite:devtools:integration',
apply: 'build',
@@ -53,3 +57,12 @@ export function DevToolsIntegration(_options: DevToolsIntegrationOptions): Plugi
},
}
}
+
+export async function DevToolsIntegration(options: DevToolsIntegrationOptions): Promise {
+ const config = options.config
+ if (!isDevToolsEnabled(config.devtools as ResolvedDevToolsConfig, config.command))
+ return []
+ return options.config.command === 'serve'
+ ? DevTools({ cwd: options.config.root })
+ : [DevToolsBuildIntegration()]
+}
diff --git a/packages/vite/src/node/__tests__/inspect-context.test.ts b/packages/vite/src/node/__tests__/inspect-context.test.ts
index 913ed7700..9c863e8dd 100644
--- a/packages/vite/src/node/__tests__/inspect-context.test.ts
+++ b/packages/vite/src/node/__tests__/inspect-context.test.ts
@@ -54,6 +54,43 @@ async function waitFor(condition: () => boolean): Promise {
}
describe('vite inspect context', () => {
+ it('only exposes and records configured environments', async () => {
+ const config = {
+ root: '/project',
+ plugins: [],
+ devtools: {
+ config: {
+ environments: ['client'],
+ },
+ },
+ } as unknown as ResolvedConfig
+ const client = {
+ name: 'client',
+ mode: 'dev',
+ getTopLevelConfig: () => config,
+ } as Environment
+ const ssr = {
+ name: 'ssr',
+ mode: 'dev',
+ getTopLevelConfig: () => config,
+ } as Environment
+ const ctx = await ViteInspectContext.create()
+ contexts.push(ctx)
+ const vite = ctx.getViteContext(config)
+
+ vite.registerEnvironmentNames(['client', 'ssr'])
+
+ expect(vite.getEnvContext(client)).toBeDefined()
+ expect(vite.isEnvironmentEnabled(ssr.name)).toBe(false)
+ expect(ctx.getEnvContext(ssr)).toBeUndefined()
+ expect(ctx.getMetadata().instances[0]).toMatchObject({
+ environments: ['client'],
+ environmentPlugins: {
+ client: [],
+ },
+ })
+ })
+
it('records module transforms and normalizes version query by default', async () => {
const { envCtx } = await createFixture()
diff --git a/packages/vite/src/node/inspect/context.ts b/packages/vite/src/node/inspect/context.ts
index ed554eb3a..2110a7634 100644
--- a/packages/vite/src/node/inspect/context.ts
+++ b/packages/vite/src/node/inspect/context.ts
@@ -107,7 +107,10 @@ export class ViteInspectContext {
getEnvContext(env: Environment | undefined): ViteInspectEnvironmentContext | undefined {
if (!env)
return undefined
- return this.getViteContext(env.getTopLevelConfig()).getEnvContext(env)
+ const vite = this.getViteContext(env.getTopLevelConfig())
+ if (!vite.isEnvironmentEnabled(env.name))
+ return undefined
+ return vite.getEnvContext(env)
}
queryEnv(query: ViteInspectQuery): ViteInspectEnvironmentContext {
@@ -118,6 +121,7 @@ export class ViteInspectContext {
export class ViteInspectViteContext {
readonly environmentNames = new Set()
readonly environments = new Map()
+ readonly enabledEnvironmentNames: ReadonlySet | undefined
readonly data: {
serverMetrics: ViteInspectServerMetrics
} = {
@@ -130,11 +134,24 @@ export class ViteInspectViteContext {
readonly id: string,
readonly context: ViteInspectContext,
readonly config: ResolvedConfig,
- ) {}
+ ) {
+ const environmentNames = (config.devtools as {
+ config?: { environments?: string[] }
+ } | undefined)?.config?.environments
+ this.enabledEnvironmentNames = environmentNames
+ ? new Set(environmentNames)
+ : undefined
+ }
+
+ isEnvironmentEnabled(name: string): boolean {
+ return this.enabledEnvironmentNames?.has(name) ?? true
+ }
registerEnvironmentNames(names: Iterable): void {
- for (const name of names)
- this.environmentNames.add(name)
+ for (const name of names) {
+ if (this.isEnvironmentEnabled(name))
+ this.environmentNames.add(name)
+ }
}
getEnvContext(env: Environment | string): ViteInspectEnvironmentContext {
diff --git a/packages/vite/src/node/inspect/plugin.ts b/packages/vite/src/node/inspect/plugin.ts
index b4a4b8ae9..32ba60965 100644
--- a/packages/vite/src/node/inspect/plugin.ts
+++ b/packages/vite/src/node/inspect/plugin.ts
@@ -170,7 +170,10 @@ export function DevToolsViteInspect(): PluginWithDevTools {
}
const vite = ctx.getViteContext(server.config)
- Object.values(server.environments).forEach(env => vite.getEnvContext(env))
+ Object.values(server.environments).forEach((env) => {
+ if (vite.isEnvironmentEnabled(env.name))
+ vite.getEnvContext(env)
+ })
setupEnvironmentInvalidation(server, vite)
return () => {
@@ -181,6 +184,8 @@ export function DevToolsViteInspect(): PluginWithDevTools {
hotUpdate({ modules }) {
if (!inspectContext)
return
+ if (!inspectContext.getEnvContext(this.environment))
+ return
notifyInspectModuleUpdated(modules.map(module => module.id).filter(id => id != null))
},
diff --git a/packages/vite/src/node/inspect/server.ts b/packages/vite/src/node/inspect/server.ts
index be2367c2e..a777945c5 100644
--- a/packages/vite/src/node/inspect/server.ts
+++ b/packages/vite/src/node/inspect/server.ts
@@ -39,6 +39,8 @@ export function isTransformRequestStale(request?: ViteInspectTransformRequestSta
export function setupEnvironmentInvalidation(server: ViteDevServer, vite: ViteInspectViteContext): void {
Object.values(server.environments).forEach((env) => {
+ if (!vite.isEnvironmentEnabled(env.name))
+ return
const envContext = vite.getEnvContext(env)
const existingState = environmentInvalidationStates.get(env)
if (existingState) {
diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/config.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools/config.snapshot.d.ts
index b4b5ecb87..07844e34c 100644
--- a/test/__snapshots__/tsnapi/@vitejs/devtools/config.snapshot.d.ts
+++ b/test/__snapshots__/tsnapi/@vitejs/devtools/config.snapshot.d.ts
@@ -3,20 +3,27 @@
*/
// #region Interfaces
export interface DevToolsConfig extends Partial {
- enabled: boolean;
+ enabled?: boolean;
+ apply?: DevToolsApply;
environments?: string[];
clientAuth?: boolean;
clientAuthTokens?: string[];
allowedOrigins?: string[];
}
export interface ResolvedDevToolsConfig {
- config: Omit & {
+ config: Omit & {
host: string;
};
enabled: boolean;
+ apply: DevToolsApply;
}
// #endregion
+// #region Types
+export type DevToolsApply = 'serve' | 'build' | 'all';
+// #endregion
+
// #region Functions
+export declare function isDevToolsEnabled(_: ResolvedDevToolsConfig, _: 'serve' | 'build'): boolean;
export declare function normalizeDevToolsConfig(_: DevToolsConfig | boolean | undefined, _: string): ResolvedDevToolsConfig;
// #endregion
\ No newline at end of file
diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/config.snapshot.js b/test/__snapshots__/tsnapi/@vitejs/devtools/config.snapshot.js
index 51ce21202..9572f48db 100644
--- a/test/__snapshots__/tsnapi/@vitejs/devtools/config.snapshot.js
+++ b/test/__snapshots__/tsnapi/@vitejs/devtools/config.snapshot.js
@@ -2,5 +2,6 @@
* Generated by tsnapi — public API snapshot of `@vitejs/devtools/config`
*/
// #region Functions
+export function isDevToolsEnabled(_, _) {}
export function normalizeDevToolsConfig(_, _) {}
// #endregion
\ No newline at end of file
diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/integration.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools/integration.snapshot.d.ts
index 814a16d19..2c342bf84 100644
--- a/test/__snapshots__/tsnapi/@vitejs/devtools/integration.snapshot.d.ts
+++ b/test/__snapshots__/tsnapi/@vitejs/devtools/integration.snapshot.d.ts
@@ -8,8 +8,8 @@ export interface DevToolsIntegrationOptions {
// #endregion
// #region Functions
-export declare function DevToolsIntegration(_: DevToolsIntegrationOptions): {
+export declare function DevToolsIntegration(_: DevToolsIntegrationOptions): Promise<{
name: string;
-};
+}[]>;
export declare function runDevTools(_: unknown): Promise;
// #endregion
\ No newline at end of file