Skip to content
Merged
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
71 changes: 24 additions & 47 deletions docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <kbd>Shift</kbd> + <kbd>Alt</kbd> + <kbd>D</kbd> (<kbd>⇧</kbd> <kbd>⌥</kbd> <kbd>D</kbd> 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 <kbd>Shift</kbd> + <kbd>Alt</kbd> + <kbd>D</kbd> (<kbd>⇧</kbd> <kbd>⌥</kbd> <kbd>D</kbd> 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'
Expand All @@ -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'
Expand All @@ -129,6 +105,9 @@ export default defineConfig({
},
}),
],
devtools: {
apply: 'build',
},
})
```

Expand All @@ -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'
Expand All @@ -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).

Expand Down
8 changes: 4 additions & 4 deletions docs/kit/client-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion packages/core/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof _DevToolsIntegration>[0])
}

Expand Down
51 changes: 51 additions & 0 deletions packages/core/src/node/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
75 changes: 75 additions & 0 deletions packages/core/src/node/__tests__/integration.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
31 changes: 27 additions & 4 deletions packages/core/src/node/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import type { StartOptions } from './cli-commands'

export type DevToolsApply = 'serve' | 'build' | 'all'

export interface DevToolsConfig extends Partial<StartOptions> {
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.
*/
Expand Down Expand Up @@ -36,22 +48,33 @@ export interface DevToolsConfig extends Partial<StartOptions> {
}

export interface ResolvedDevToolsConfig {
config: Omit<DevToolsConfig, 'enabled'> & { host: string }
config: Omit<DevToolsConfig, 'enabled' | 'apply'> & { host: string }
enabled: boolean
apply: DevToolsApply
}

export function normalizeDevToolsConfig(
config: DevToolsConfig | boolean | undefined,
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)
}
15 changes: 14 additions & 1 deletion packages/core/src/node/plugins/integration.ts
Original file line number Diff line number Diff line change
@@ -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]

Expand All @@ -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')
Expand All @@ -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',
Expand All @@ -53,3 +57,12 @@ export function DevToolsIntegration(_options: DevToolsIntegrationOptions): Plugi
},
}
}

export async function DevToolsIntegration(options: DevToolsIntegrationOptions): Promise<Plugin[]> {
const config = options.config
if (!isDevToolsEnabled(config.devtools as ResolvedDevToolsConfig, config.command))
return []
return options.config.command === 'serve'
? DevTools({ cwd: options.config.root })
: [DevToolsBuildIntegration()]
}
Loading
Loading