Skip to content
Draft
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
4 changes: 0 additions & 4 deletions docs/errors/DTK0013.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,10 @@ Authorize the browser. When an untrusted client connects, the dev-server termina
For automated setups (CI, shared machines), configure static trusted tokens instead — a client presenting one via the `devframe_auth_token` connection parameter is trusted without the interactive step:

```ts
import { DevTools } from '@vitejs/devtools'
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
plugins: [
DevTools(),
],
devtools: {
enabled: true,
clientAuthTokens: ['your-trusted-token'],
Expand Down
21 changes: 21 additions & 0 deletions docs/errors/DTK0034.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
outline: deep
---

# DTK0034: Duplicate DevTools Plugin

## Message

> Vite DevTools has been registered multiple times.

## Cause

More than one Vite DevTools plugin instance was added to the same Vite configuration. This can happen when both the user and a framework register DevTools, or when the plugin is listed more than once.

## Fix

Remove the duplicate Vite DevTools registration.

## Source

- [`packages/core/src/node/plugins/config.ts`](https://github.com/vitejs/devtools/blob/main/packages/core/src/node/plugins/config.ts) — `DevToolsConfigPlugin()` detects duplicate plugin instances during config resolution.
1 change: 1 addition & 0 deletions docs/errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Emitted by `@vitejs/devtools` and `@vitejs/devtools-kit`.
| [DTK0031](./DTK0031) | error | Dock Entry Not a Launcher |
| [DTK0032](./DTK0032) | error | Dock Launch Error |
| [DTK0033](./DTK0033) | warn | DevTools Mode Persist Failed |
| [DTK0034](./DTK0034) | error | Duplicate DevTools Plugin |
| [DTK0050](./DTK0050) | error | Integration Install Failed |
| [DTK0051](./DTK0051) | warn | Connection Meta Serve Failed |
| [DTK0052](./DTK0052) | error | Launcher Process Exited Before Ready |
Expand Down
41 changes: 13 additions & 28 deletions docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,43 +70,33 @@ export default defineConfig({

### Customize the embedded UI

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.
Vite adds the embedded dock automatically during `vite dev`. Configure its UI through the core `devtools` option.

`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'
import { defineConfig } from 'vite'

export default defineConfig({
plugins: [
DevTools({
embeddedVisibility: 'passive',
}),
],
devtools: {
apply: 'build',
apply: 'serve',
embeddedVisibility: 'passive',
},
})
Comment thread
webfansplz marked this conversation as resolved.
```

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'
import { defineConfig } from 'vite'

export default defineConfig({
plugins: [
DevTools({
dockPreferences: {
defaultMode: 'edge',
defaultPosition: 'bottom',
},
}),
],
devtools: {
apply: 'build',
apply: 'serve',
dockPreferences: {
defaultMode: 'edge',
defaultPosition: 'bottom',
},
},
})
```
Expand Down Expand Up @@ -137,21 +127,16 @@ See [Client Script & Context](/kit/client-context#client-script-not-injected) fo
Set `build.withApp` to write the static DevTools files alongside the app build:

```ts [vite.config.ts] twoslash
import { DevTools } from '@vitejs/devtools'
import { defineConfig } from 'vite'

export default defineConfig({
plugins: [
DevTools({
build: {
withApp: true, // generate DevTools output during `vite build`
// outDir: 'custom-dir', // optional, defaults to Vite's build.outDir
},
}),
],
devtools: {
apply: 'build',
}
build: {
withApp: true, // generate DevTools output during `vite build`
// outDir: 'custom-dir', // optional, defaults to Vite's build.outDir
},
},
})
```

Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/integration.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import type { DevToolsConfig } from './node/config'
import {
DevToolsIntegration as _DevToolsIntegration,
runDevTools as _runDevTools,
} from './node/plugins/integration'

export interface DevToolsIntegrationConfig {
host: string
options: boolean | DevToolsConfig | undefined
}

export interface DevToolsIntegrationOptions {
config: unknown
command: 'serve' | 'build'
root: string
devtools: DevToolsIntegrationConfig
}

export function DevToolsIntegration(options: DevToolsIntegrationOptions): Promise<{ name: string }[]> {
return _DevToolsIntegration(options as Parameters<typeof _DevToolsIntegration>[0])
return _DevToolsIntegration(options)
}

export function runDevTools(builder: unknown): Promise<void> {
Expand Down
53 changes: 42 additions & 11 deletions packages/core/src/node/__tests__/auth-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import type { ResolvedConfig } from 'vite'
import type { DevToolsConfig } from '../config'
import process from 'node:process'
import { describe, expect, it, vi } from 'vitest'
import { getAuthHandler, getBuildCapabilityToken, isBuildCapabilityAuth, isClientAuthDisabled } from '../auth-handler'
import { normalizeDevToolsConfig } from '../config'
import { createDevToolsContext } from '../context'
import '@vitejs/devtools-kit'

function createConfig(config?: Partial<DevToolsConfig>, command: 'serve' | 'build' = 'serve'): ResolvedConfig {
function createConfig(command: 'serve' | 'build' = 'serve'): ResolvedConfig {
return {
root: process.cwd(),
command,
plugins: [],
server: { port: 5173 },
devtools: config === undefined ? undefined : { config },
} as unknown as ResolvedConfig
}

describe('getAuthHandler banner', () => {
it('forwards a configured banner to the interactive auth handler', async () => {
const banner = vi.fn()
const ctx = await createDevToolsContext(createConfig({ banner }))
const ctx = await createDevToolsContext(
createConfig(),
undefined,
normalizeDevToolsConfig({ banner }, 'localhost'),
)

getAuthHandler(ctx).printBanner()

Expand All @@ -31,7 +34,11 @@ describe('getAuthHandler banner', () => {

it('falls back to the default stdout banner when unset', async () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
const ctx = await createDevToolsContext(createConfig())
const ctx = await createDevToolsContext(
createConfig(),
undefined,
normalizeDevToolsConfig(true, 'localhost'),
)

try {
getAuthHandler(ctx).printBanner()
Expand All @@ -44,7 +51,11 @@ describe('getAuthHandler banner', () => {

it('suppresses the OTP banner in implicit build mode (trust is token-based)', async () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
const ctx = await createDevToolsContext(createConfig(undefined, 'build'))
const ctx = await createDevToolsContext(
createConfig('build'),
undefined,
normalizeDevToolsConfig(true, 'localhost'),
)

try {
getAuthHandler(ctx).printBanner()
Expand All @@ -58,34 +69,54 @@ describe('getAuthHandler banner', () => {

describe('build-mode capability token', () => {
it('flags implicit build mode as capability-token auth, not disabled', async () => {
const ctx = await createDevToolsContext(createConfig(undefined, 'build'))
const ctx = await createDevToolsContext(
createConfig('build'),
undefined,
normalizeDevToolsConfig(true, 'localhost'),
)

expect(isBuildCapabilityAuth(ctx)).toBe(true)
expect(isClientAuthDisabled(ctx)).toBe(false)
})

it('is not capability-token auth in dev mode', async () => {
const ctx = await createDevToolsContext(createConfig())
const ctx = await createDevToolsContext(
createConfig(),
undefined,
normalizeDevToolsConfig(true, 'localhost'),
)

expect(isBuildCapabilityAuth(ctx)).toBe(false)
})

it('leaves an explicit clientAuth:false opt-out fully disabled in build mode', async () => {
const ctx = await createDevToolsContext(createConfig({ clientAuth: false }, 'build'))
const ctx = await createDevToolsContext(
createConfig('build'),
undefined,
normalizeDevToolsConfig({ clientAuth: false }, 'localhost'),
)

expect(isClientAuthDisabled(ctx)).toBe(true)
expect(isBuildCapabilityAuth(ctx)).toBe(false)
})

it('mints a stable, unguessable token per context', async () => {
const ctx = await createDevToolsContext(createConfig(undefined, 'build'))
const ctx = await createDevToolsContext(
createConfig('build'),
undefined,
normalizeDevToolsConfig(true, 'localhost'),
)

const token = getBuildCapabilityToken(ctx)
expect(token).toMatch(/^[\w-]{20,}$/)
// Memoized: the same context always yields the same token.
expect(getBuildCapabilityToken(ctx)).toBe(token)

const other = await createDevToolsContext(createConfig(undefined, 'build'))
const other = await createDevToolsContext(
createConfig('build'),
undefined,
normalizeDevToolsConfig(true, 'localhost'),
)
expect(getBuildCapabilityToken(other)).not.toBe(token)
})
})
48 changes: 35 additions & 13 deletions packages/core/src/node/__tests__/context-auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,80 @@
import type { ResolvedConfig } from 'vite'
import process from 'node:process'
import { afterEach, describe, expect, it } from 'vitest'
import { normalizeDevToolsConfig } from '../config'
import { createDevToolsContext } from '../context'
import '@vitejs/devtools-kit'

function createConfig(options: {
command?: 'serve' | 'build'
clientAuth?: boolean
} = {}): ResolvedConfig {
function createConfig(command: 'serve' | 'build' = 'serve'): ResolvedConfig {
return {
root: process.cwd(),
command: options.command ?? 'serve',
command,
plugins: [],
devtools: options.clientAuth === undefined
? undefined
: { config: { clientAuth: options.clientAuth } },
} as unknown as ResolvedConfig
}

function createDevToolsConfig(clientAuth?: boolean) {
return normalizeDevToolsConfig(
clientAuth === undefined ? true : { clientAuth },
'localhost',
)
}

describe('createDevToolsContext auth registration', () => {
afterEach(() => {
delete process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH
})

it('registers the interactive-auth handshake when client auth is enabled', async () => {
const ctx = await createDevToolsContext(createConfig())
const ctx = await createDevToolsContext(
createConfig(),
undefined,
createDevToolsConfig(),
)

expect(ctx.rpc.definitions.has('anonymous:devframe:auth')).toBe(true)
})

it('registers the interactive-auth handshake in build mode for capability-token trust (#552)', async () => {
const ctx = await createDevToolsContext(createConfig({ command: 'build' }))
const ctx = await createDevToolsContext(
createConfig('build'),
undefined,
createDevToolsConfig(),
)

// Build mode keeps the gate installed and trusts via a per-process
// capability token rather than a prompt — see `isBuildCapabilityAuth`.
expect(ctx.rpc.definitions.has('anonymous:devframe:auth')).toBe(true)
})

it('skips the interactive-auth handshake in build mode when clientAuth is explicitly false', async () => {
const ctx = await createDevToolsContext(createConfig({ command: 'build', clientAuth: false }))
const ctx = await createDevToolsContext(
createConfig('build'),
undefined,
createDevToolsConfig(false),
)

expect(ctx.rpc.definitions.has('anonymous:devframe:auth')).toBe(false)
})

it('skips the interactive-auth handshake when `devtools.clientAuth` is false (regression #539)', async () => {
const ctx = await createDevToolsContext(createConfig({ clientAuth: false }))
const ctx = await createDevToolsContext(
createConfig(),
undefined,
createDevToolsConfig(false),
)

expect(ctx.rpc.definitions.has('anonymous:devframe:auth')).toBe(false)
})

it('skips the interactive-auth handshake when VITE_DEVTOOLS_DISABLE_CLIENT_AUTH=true (regression #539)', async () => {
process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH = 'true'

const ctx = await createDevToolsContext(createConfig())
const ctx = await createDevToolsContext(
createConfig(),
undefined,
createDevToolsConfig(),
)

expect(ctx.rpc.definitions.has('anonymous:devframe:auth')).toBe(false)
})
Expand Down
Loading
Loading