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