From 2d7386c6a304a35da8785a07c6d4252b32d35c1c Mon Sep 17 00:00:00 2001 From: arlo Date: Mon, 24 Aug 2026 18:30:53 +0800 Subject: [PATCH 1/5] feat(core): resolve integration options within devtools --- docs/errors/DTK0013.md | 4 - docs/guide/index.md | 39 +++------ packages/core/src/integration.ts | 11 ++- .../src/node/__tests__/context-auth.test.ts | 35 ++++++-- .../src/node/__tests__/integration.test.ts | 61 ++++++++++---- packages/core/src/node/auth-handler.ts | 5 +- packages/core/src/node/config.ts | 3 +- packages/core/src/node/context.ts | 11 +++ packages/core/src/node/plugin-options.ts | 58 +++++++++++++ packages/core/src/node/plugins/build.ts | 8 +- packages/core/src/node/plugins/index.ts | 84 ++++--------------- packages/core/src/node/plugins/integration.ts | 70 ++++++++++++---- packages/core/src/node/plugins/server.ts | 12 ++- packages/core/src/node/resolved-config.ts | 30 +++++++ packages/core/src/node/server.ts | 6 +- packages/core/src/node/ui.ts | 35 +------- 16 files changed, 291 insertions(+), 181 deletions(-) create mode 100644 packages/core/src/node/plugin-options.ts create mode 100644 packages/core/src/node/resolved-config.ts diff --git a/docs/errors/DTK0013.md b/docs/errors/DTK0013.md index 7c9b08be9..9721d1d5a 100644 --- a/docs/errors/DTK0013.md +++ b/docs/errors/DTK0013.md @@ -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'], diff --git a/docs/guide/index.md b/docs/guide/index.md index f2f2ab55c..250bf6fab 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -70,22 +70,16 @@ 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 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' import { defineConfig } from 'vite' export default defineConfig({ - plugins: [ - DevTools({ - embeddedVisibility: 'passive', - }), - ], devtools: { - apply: 'build', + embeddedVisibility: 'passive', }, }) ``` @@ -93,20 +87,14 @@ export default defineConfig({ 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', + dockPreferences: { + defaultMode: 'edge', + defaultPosition: 'bottom', + }, }, }) ``` @@ -137,21 +125,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 + }, + }, }) ``` diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 7aac62feb..8f2cc75a3 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,3 +1,4 @@ +import type { DevToolsIntegrationConfig } from './node/plugins/integration' import { DevToolsIntegration as _DevToolsIntegration, runDevTools as _runDevTools, @@ -5,12 +6,18 @@ import { export interface DevToolsIntegrationOptions { config: unknown + devtools: DevToolsIntegrationConfig } export function DevToolsIntegration(options: DevToolsIntegrationOptions): Promise<{ name: string }[]> { return _DevToolsIntegration(options as Parameters[0]) } -export function runDevTools(builder: unknown): Promise { - return _runDevTools(builder) +export function runDevTools( + builder: unknown, + devtools: DevToolsIntegrationConfig, +): Promise { + return _runDevTools(builder, devtools) } + +export type { DevToolsIntegrationConfig } diff --git a/packages/core/src/node/__tests__/context-auth.test.ts b/packages/core/src/node/__tests__/context-auth.test.ts index d2882a14e..f9ec39b6a 100644 --- a/packages/core/src/node/__tests__/context-auth.test.ts +++ b/packages/core/src/node/__tests__/context-auth.test.ts @@ -1,6 +1,7 @@ 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' @@ -12,25 +13,37 @@ function createConfig(options: { root: process.cwd(), command: options.command ?? 'serve', 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('skips the interactive-auth handshake in build mode (regression #539)', async () => { - const ctx = await createDevToolsContext(createConfig({ command: 'build' })) + const ctx = await createDevToolsContext( + createConfig({ command: 'build' }), + undefined, + createDevToolsConfig(), + ) // Left unregistered so devframe's `auth: false` auto-trust shim (armed // by `createDevToolsHub`) can install its own noop handler and mark the @@ -39,7 +52,11 @@ describe('createDevToolsContext auth registration', () => { }) 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({ clientAuth: false }), + undefined, + createDevToolsConfig(false), + ) expect(ctx.rpc.definitions.has('anonymous:devframe:auth')).toBe(false) }) @@ -47,7 +64,11 @@ describe('createDevToolsContext auth registration', () => { 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) }) diff --git a/packages/core/src/node/__tests__/integration.test.ts b/packages/core/src/node/__tests__/integration.test.ts index cc5ceed8c..7b8dcc7da 100644 --- a/packages/core/src/node/__tests__/integration.test.ts +++ b/packages/core/src/node/__tests__/integration.test.ts @@ -2,31 +2,42 @@ 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 { +function createConfig(command: 'serve' | 'build'): ResolvedConfig { return { command, root: '/vite-devtools-test-project', - devtools: { - apply, - config: {}, - enabled: true, - }, + environments: {}, + plugins: [], } as unknown as ResolvedConfig } +function createDevToolsConfig(apply: 'serve' | 'build' | 'all') { + return { + host: 'localhost', + options: { apply }, + } as const +} + describe('devToolsIntegration', () => { it('returns the existing DevTools plugins for serve', async () => { - const plugins = await DevToolsIntegration({ config: createConfig('serve') }) + const plugins = await DevToolsIntegration({ + config: createConfig('serve'), + devtools: createDevToolsConfig('serve'), + }) expect((plugins as Plugin[]).map(plugin => plugin.name)).toEqual([ 'vite:devtools:builtin', + 'vite:devtools', 'vite:devtools:injection', 'vite:devtools:server', ]) }) it('returns the build integration plugin for build', async () => { - const [plugin] = await DevToolsIntegration({ config: createConfig('build') }) + const [plugin] = await DevToolsIntegration({ + config: createConfig('build'), + devtools: createDevToolsConfig('build'), + }) expect(plugin).toMatchObject({ name: 'vite:devtools:integration', @@ -34,11 +45,26 @@ describe('devToolsIntegration', () => { }) }) + it('creates the static build plugin from the core config', async () => { + const plugins = await DevToolsIntegration({ + config: createConfig('build'), + devtools: { + host: 'localhost', + options: { build: { withApp: true } }, + }, + }) + + expect(plugins.map(plugin => plugin.name)).toContain('vite:devtools: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 plugins = await DevToolsIntegration({ + config: createConfig(command), + devtools: createDevToolsConfig('all'), + }) const plugin = command === 'serve' ? plugins.find(plugin => plugin.name === 'vite:devtools:server') : plugins[0] @@ -47,20 +73,25 @@ describe('devToolsIntegration', () => { }) it('returns no plugins when apply excludes the current command', async () => { - const plugins = await DevToolsIntegration({ config: createConfig('serve', 'build') }) + const plugins = await DevToolsIntegration({ + config: createConfig('serve'), + devtools: createDevToolsConfig('build'), + }) expect(plugins).toEqual([]) }) it('enables Rolldown DevTools for selected build environments', async () => { - const [plugin] = await DevToolsIntegration({ config: createConfig('build') }) + const [plugin] = await DevToolsIntegration({ + config: createConfig('build'), + devtools: { + host: 'localhost', + options: { environments: ['client'] }, + }, + }) 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 diff --git a/packages/core/src/node/auth-handler.ts b/packages/core/src/node/auth-handler.ts index 42d8eaee7..1c6e9eeae 100644 --- a/packages/core/src/node/auth-handler.ts +++ b/packages/core/src/node/auth-handler.ts @@ -1,6 +1,7 @@ import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit' import process from 'node:process' import { createInteractiveAuth } from 'devframe/recipes/interactive-auth' +import { getResolvedDevToolsConfig } from './resolved-config' export type DevToolsAuthHandler = ReturnType @@ -18,7 +19,7 @@ export function getAuthHandler(context: ViteDevToolsNodeContext): DevToolsAuthHa let handler = handlers.get(context) if (!handler) { handler = createInteractiveAuth(context, { - clientAuthTokens: context.viteConfig.devtools?.config?.clientAuthTokens, + clientAuthTokens: getResolvedDevToolsConfig(context).config.clientAuthTokens, }) handlers.set(context, handler) } @@ -38,6 +39,6 @@ export function getAuthHandler(context: ViteDevToolsNodeContext): DevToolsAuthHa */ export function isClientAuthDisabled(context: ViteDevToolsNodeContext): boolean { return context.mode === 'build' - || context.viteConfig.devtools?.config?.clientAuth === false + || getResolvedDevToolsConfig(context).config.clientAuth === false || process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH === 'true' } diff --git a/packages/core/src/node/config.ts b/packages/core/src/node/config.ts index 721c72eac..81d30d96c 100644 --- a/packages/core/src/node/config.ts +++ b/packages/core/src/node/config.ts @@ -1,8 +1,9 @@ import type { StartOptions } from './cli-commands' +import type { DevToolsUserOptions } from './plugin-options' export type DevToolsApply = 'serve' | 'build' | 'all' -export interface DevToolsConfig extends Partial { +export interface DevToolsConfig extends Partial, DevToolsUserOptions { /** * Enable Vite DevTools. * diff --git a/packages/core/src/node/context.ts b/packages/core/src/node/context.ts index 44e17928f..a7d6c3b72 100644 --- a/packages/core/src/node/context.ts +++ b/packages/core/src/node/context.ts @@ -1,11 +1,16 @@ import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit' import type { RpcFunctionsHost } from 'devframe/node' import type { ResolvedConfig, ViteDevServer } from 'vite' +import type { ResolvedDevToolsConfig } from './config' import { createKitContext, createViteDevToolsHost } from '@vitejs/devtools-kit/node' import { createDebug } from 'obug' import { DEVTOOLS_ASSETS_BASE, dirAssets } from '../dirs' import { getAuthHandler, isClientAuthDisabled } from './auth-handler' import { diagnostics } from './diagnostics' +import { + defaultResolvedDevToolsConfig, + setResolvedDevToolsConfig, +} from './resolved-config' import { builtinRpcDeclarations } from './rpc' const debugSetup = createDebug('vite:devtools:context:setup') @@ -29,6 +34,7 @@ function shouldSkipSetupByCapabilities( export async function createDevToolsContext( viteConfig: ResolvedConfig, viteServer?: ViteDevServer, + devtoolsConfig?: ResolvedDevToolsConfig, ): Promise { const cwd = viteConfig.root @@ -46,6 +52,11 @@ export async function createDevToolsContext( viteServer, })) as ViteDevToolsNodeContext + setResolvedDevToolsConfig( + context, + devtoolsConfig ?? defaultResolvedDevToolsConfig, + ) + // Fold the core (Vite) diagnostics into the shared host logger so plugin // setup() hooks can reference DTK codes via `ctx.diagnostics.logger`. context.diagnostics.register(diagnostics) diff --git a/packages/core/src/node/plugin-options.ts b/packages/core/src/node/plugin-options.ts new file mode 100644 index 000000000..cb21f8106 --- /dev/null +++ b/packages/core/src/node/plugin-options.ts @@ -0,0 +1,58 @@ +export type DevToolsBrandingLogo + = | string + | { light: string, dark: string } + +export interface DevToolsBranding { + productName?: string + logo?: DevToolsBrandingLogo + wordmark?: DevToolsBrandingLogo + primaryColor?: string + tagline?: string + favicon?: string + windowTitle?: string +} + +export interface DevToolsDockPreferences { + categoryOrder?: Record + maxVisibleItems?: number + defaultMode?: 'float' | 'edge' + defaultPosition?: 'left' | 'right' | 'top' | 'bottom' +} + +export type DevToolsEmbeddedVisibility = 'normal' | 'passive' | 'hidden' + +export interface ViteDevToolsUiOptions { + branding?: DevToolsBranding + embeddedVisibility?: DevToolsEmbeddedVisibility + dockPreferences?: DevToolsDockPreferences +} + +export interface DevToolsUserOptions { + /** + * Include the Vite builtin devtools UI. + * + * @default true + */ + builtinDevTools?: boolean + /** Override the branding handed to the DevTools client. */ + branding?: ViteDevToolsUiOptions['branding'] + /** Control how the embedded floating dock reveals itself. */ + embeddedVisibility?: ViteDevToolsUiOptions['embeddedVisibility'] + /** Configure the initial dock layout. */ + dockPreferences?: ViteDevToolsUiOptions['dockPreferences'] + /** Options for building static DevTools output alongside `vite build`. */ + build?: { + /** + * Automatically build DevTools when running `vite build`. + * @default false + */ + withApp?: boolean + /** Output directory relative to root. Defaults to Vite's `build.outDir`. */ + outDir?: string + } +} + +export interface DevToolsOptions extends DevToolsUserOptions { + /** Directory to search for installed integrations. */ + cwd?: string +} diff --git a/packages/core/src/node/plugins/build.ts b/packages/core/src/node/plugins/build.ts index 80bd851eb..552717924 100644 --- a/packages/core/src/node/plugins/build.ts +++ b/packages/core/src/node/plugins/build.ts @@ -2,12 +2,14 @@ import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit' import type { Plugin, ResolvedConfig } from 'vite' +import type { ResolvedDevToolsConfig } from '../config' import type { ViteDevToolsUiOptions } from '../ui' import { colors as c } from 'devframe/utils/colors' import { resolve } from 'pathe' import { MARK_NODE } from '../constants' export interface DevToolsBuildOptions { + resolvedConfig?: ResolvedDevToolsConfig outDir?: string /** Reference-UI options forwarded to the static snapshot's `createUi`. */ ui?: ViteDevToolsUiOptions @@ -27,7 +29,11 @@ export function DevToolsBuild(options: DevToolsBuildOptions = {}): Plugin { async buildStart() { const { createDevToolsContext } = await import('../context') - context = await createDevToolsContext(resolvedConfig) + context = await createDevToolsContext( + resolvedConfig, + undefined, + options.resolvedConfig, + ) }, async closeBundle() { diff --git a/packages/core/src/node/plugins/index.ts b/packages/core/src/node/plugins/index.ts index 044827638..e1b1006bf 100644 --- a/packages/core/src/node/plugins/index.ts +++ b/packages/core/src/node/plugins/index.ts @@ -1,78 +1,21 @@ import type { Plugin } from 'vite' -import type { ViteDevToolsUiOptions } from '../ui' +import type { ResolvedDevToolsConfig } from '../config' +import type { DevToolsOptions } from '../plugin-options' import { DevToolsBuild } from './build' import { DevToolsBuiltin } from './builtin' import { DevToolsInjection } from './injection' import { DevToolsServer } from './server' -export interface DevToolsOptions { - /** Directory to search for installed integrations. */ - cwd?: string - /** - * Include the Vite builtin devtools UI. - * - * @default true - */ - builtinDevTools?: boolean +export type { DevToolsOptions } from '../plugin-options' - /** - * Override the branding handed to the DevTools client (`@devframes/hub-ui`) - * — product name, logo, wordmark, primary color, tagline, favicon, and - * window title. - * - * Each field is merged over the built-in Vite DevTools defaults, so a host - * embedding Vite DevTools (e.g. Nuxt DevTools) can re-skin the client while - * inheriting any field it leaves unset. Asset fields - * (`logo`/`wordmark`/`favicon`) take URL strings the host is responsible for - * serving. - */ - branding?: ViteDevToolsUiOptions['branding'] - - /** - * How the embedded floating dock reveals itself on a fresh page. - * - * - `'normal'` — show the docks immediately. - * - `'passive'` — the floating docks stay hidden and a console hint invites - * the developer to reveal them with a keyboard shortcut. Revealing once - * persists per-origin, so later dev sessions on this browser start shown; - * the "Hide DevTools" command returns to passive mode. - * - `'hidden'` — always keep the docks hidden; the shortcut reveals them for - * the current session only, without remembering the choice. - * - * Seeds a user-overridable preference published as - * `ConnectionMeta.configs.ui.embeddedVisibility`. - * - * @default 'normal' - */ - embeddedVisibility?: ViteDevToolsUiOptions['embeddedVisibility'] - - /** - * Dock-bar rendering preferences — category ordering, floating-dock - * inline-item capacity, and the first-run float/edge mode and position. - * Each seeds a user-overridable preference published as - * `ConnectionMeta.configs.ui.dockPreferences`. - */ - dockPreferences?: ViteDevToolsUiOptions['dockPreferences'] - - /** - * Options for building static DevTools output alongside `vite build`. - */ - build?: { - /** - * Automatically build DevTools when running `vite build`. - * - * @default false - */ - withApp?: boolean - /** - * Output directory for the DevTools build (relative to root). - * Defaults to Vite's `build.outDir`. - */ - outDir?: string - } +export function DevTools(options: DevToolsOptions = {}): Promise { + return createDevToolsPlugins(options) } -export async function DevTools(options: DevToolsOptions = {}): Promise { +export async function createDevToolsPlugins( + options: DevToolsOptions = {}, + resolvedConfig?: ResolvedDevToolsConfig, +): Promise { const { builtinDevTools = true, build, @@ -84,12 +27,17 @@ export async function DevTools(options: DevToolsOptions = {}): Promise const ui = { branding, embeddedVisibility, dockPreferences } const plugins = [ + { name: 'vite:devtools' }, DevToolsInjection(), - DevToolsServer(ui), + DevToolsServer(ui, resolvedConfig), ] if (build?.withApp) { - plugins.push(DevToolsBuild({ outDir: build.outDir, ui })) + plugins.push(DevToolsBuild({ + outDir: build.outDir, + resolvedConfig, + ui, + })) } plugins.unshift( diff --git a/packages/core/src/node/plugins/integration.ts b/packages/core/src/node/plugins/integration.ts index 332ca20bc..0f5bf2077 100644 --- a/packages/core/src/node/plugins/integration.ts +++ b/packages/core/src/node/plugins/integration.ts @@ -1,16 +1,25 @@ import type { Plugin, ResolvedConfig, ViteBuilder } from 'vite' -import type { ResolvedDevToolsConfig } from '../config' -import { isDevToolsEnabled } from '../config' -import { DevTools } from './index' +import type { DevToolsConfig, ResolvedDevToolsConfig } from '../config' +import type { DevToolsOptions } from '../plugin-options' +import { isDevToolsEnabled, normalizeDevToolsConfig } from '../config' +import { createDevToolsPlugins } from './index' type DevToolsEnvironment = ResolvedConfig['environments'][string] export interface DevToolsIntegrationOptions { config: ResolvedConfig + devtools: DevToolsIntegrationConfig } -function getDevToolsEnvironments(config: ResolvedConfig): DevToolsEnvironment[] { - const devToolsConfig = config.devtools as ResolvedDevToolsConfig +export interface DevToolsIntegrationConfig { + host: string + options: boolean | DevToolsConfig | undefined +} + +function getDevToolsEnvironments( + config: ResolvedConfig, + devToolsConfig: ResolvedDevToolsConfig, +): DevToolsEnvironment[] { const environmentNames = devToolsConfig.config.environments ?? Object.keys(config.environments) const environments: DevToolsEnvironment[] = [] @@ -24,14 +33,18 @@ function getDevToolsEnvironments(config: ResolvedConfig): DevToolsEnvironment[] return environments } -export async function runDevTools(builder: unknown) { +export async function runDevTools( + builder: unknown, + devtools: DevToolsIntegrationConfig, +) { const config = (builder as ViteBuilder).config - if (!isDevToolsEnabled(config.devtools as ResolvedDevToolsConfig, config.command)) + const devtoolsConfig = normalizeDevToolsConfig(devtools.options, devtools.host) + if (!isDevToolsEnabled(devtoolsConfig, config.command)) return - for (const _environment of getDevToolsEnvironments(config)) { + for (const _environment of getDevToolsEnvironments(config, devtoolsConfig)) { try { const { start } = await import('../cli-commands') - await start(config.devtools.config) + await start(devtoolsConfig.config) } catch (error: any) { config.logger.error( @@ -42,7 +55,7 @@ export async function runDevTools(builder: unknown) { } } -function DevToolsBuildIntegration(): Plugin { +function DevToolsBuildIntegration(devtoolsConfig: ResolvedDevToolsConfig): Plugin { return { name: 'vite:devtools:integration', apply: 'build', @@ -50,7 +63,7 @@ function DevToolsBuildIntegration(): Plugin { order: 'post', handler(config) { // Enable `rolldownOptions.devtools` if the environment is selected, or for all environments by default. - for (const environment of getDevToolsEnvironments(config)) { + for (const environment of getDevToolsEnvironments(config, devtoolsConfig)) { environment.build.rolldownOptions.devtools ??= {} } }, @@ -59,10 +72,35 @@ function DevToolsBuildIntegration(): Plugin { } export async function DevToolsIntegration(options: DevToolsIntegrationOptions): Promise { - const config = options.config - if (!isDevToolsEnabled(config.devtools as ResolvedDevToolsConfig, config.command)) + const { config, devtools } = options + const devtoolsConfig = normalizeDevToolsConfig(devtools.options, devtools.host) + const enabled = isDevToolsEnabled(devtoolsConfig, config.command) + if (!enabled) { return [] - return options.config.command === 'serve' - ? DevTools({ cwd: options.config.root }) - : [DevToolsBuildIntegration()] + } + + const { + branding, + build, + builtinDevTools, + dockPreferences, + embeddedVisibility, + } = devtoolsConfig.config + const pluginOptions: DevToolsOptions = { + branding, + build, + builtinDevTools, + cwd: config.root, + dockPreferences, + embeddedVisibility, + } + if (config.command === 'serve') { + return createDevToolsPlugins(pluginOptions, devtoolsConfig) + } + + const plugins = [DevToolsBuildIntegration(devtoolsConfig)] + if (devtoolsConfig.config.build?.withApp) { + plugins.push(...await createDevToolsPlugins(pluginOptions, devtoolsConfig)) + } + return plugins } diff --git a/packages/core/src/node/plugins/server.ts b/packages/core/src/node/plugins/server.ts index 048804b71..498615d7f 100644 --- a/packages/core/src/node/plugins/server.ts +++ b/packages/core/src/node/plugins/server.ts @@ -1,6 +1,7 @@ import type { ClientScriptEntry, DevToolsDockEntry, ViteDevToolsNodeContext } from '@vitejs/devtools-kit' import type { Server as NodeHttpServer } from 'node:http' import type { Plugin } from 'vite' +import type { ResolvedDevToolsConfig } from '../config' import type { ViteDevToolsUiOptions } from '../ui' import { DEVTOOLS_DOCK_IMPORTS_VIRTUAL_ID, @@ -37,7 +38,10 @@ export function renderDockImportsMap(docks: Iterable): string ].join('\n') } -export function DevToolsServer(options: ViteDevToolsUiOptions = {}): Plugin { +export function DevToolsServer( + options: ViteDevToolsUiOptions = {}, + devtoolsConfig?: ResolvedDevToolsConfig, +): Plugin { let context: ViteDevToolsNodeContext let close: (() => Promise) | undefined return { @@ -45,7 +49,11 @@ export function DevToolsServer(options: ViteDevToolsUiOptions = {}): Plugin { enforce: 'post', apply: 'serve', async configureServer(viteDevServer) { - context = await createDevToolsContext(viteDevServer.config, viteDevServer) + context = await createDevToolsContext( + viteDevServer.config, + viteDevServer, + devtoolsConfig, + ) const host = viteDevServer.config.server.host === true ? '0.0.0.0' diff --git a/packages/core/src/node/resolved-config.ts b/packages/core/src/node/resolved-config.ts new file mode 100644 index 000000000..862fc1f86 --- /dev/null +++ b/packages/core/src/node/resolved-config.ts @@ -0,0 +1,30 @@ +import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit' +import type { ResolvedDevToolsConfig } from './config' + +const resolvedDevToolsConfigs = new WeakMap< + ViteDevToolsNodeContext, + ResolvedDevToolsConfig +>() + +export const defaultResolvedDevToolsConfig: ResolvedDevToolsConfig = { + apply: 'all', + config: { + clientAuth: true, + clientAuthTokens: [], + host: 'localhost', + }, + enabled: true, +} + +export function setResolvedDevToolsConfig( + context: ViteDevToolsNodeContext, + config: ResolvedDevToolsConfig, +): void { + resolvedDevToolsConfigs.set(context, config) +} + +export function getResolvedDevToolsConfig( + context: ViteDevToolsNodeContext, +): ResolvedDevToolsConfig { + return resolvedDevToolsConfigs.get(context) ?? defaultResolvedDevToolsConfig +} diff --git a/packages/core/src/node/server.ts b/packages/core/src/node/server.ts index 1c4158ded..dbc9f3474 100644 --- a/packages/core/src/node/server.ts +++ b/packages/core/src/node/server.ts @@ -2,12 +2,12 @@ import type { HubInstance } from '@devframes/hub/initiate' import type { ConnectionMeta, ViteDevToolsNodeContext } from '@vitejs/devtools-kit' import type { ViteDevToolsHost } from '@vitejs/devtools-kit/node' import type { Server as NodeHttpServer } from 'node:http' -import type { DevToolsConfig } from './config' import type { ViteDevToolsUiOptions } from './ui' import { initHub } from '@devframes/hub/initiate' import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub' import { DEVTOOLS_MOUNT_PATH } from '@vitejs/devtools-kit/constants' import { getAuthHandler, isClientAuthDisabled } from './auth-handler' +import { getResolvedDevToolsConfig } from './resolved-config' import { createViteDevToolsUi } from './ui' export interface CreateDevToolsHubOptions { @@ -56,9 +56,7 @@ export async function createDevToolsHub(options: CreateDevToolsHubOptions): Prom // helper) — see `isClientAuthDisabled` for why. const authDisabled = isClientAuthDisabled(context) - // Vite's published types bundle a frozen `DevToolsConfig` snapshot, so a - // field added here isn't visible through `config` until Vite re-vendors it. - const allowedOrigins = (context.viteConfig.devtools?.config as DevToolsConfig | undefined)?.allowedOrigins + const allowedOrigins = getResolvedDevToolsConfig(context).config.allowedOrigins const hub = initHub({ base: DEVTOOLS_MOUNT_PATH, diff --git a/packages/core/src/node/ui.ts b/packages/core/src/node/ui.ts index f7dd7f597..805c0e30c 100644 --- a/packages/core/src/node/ui.ts +++ b/packages/core/src/node/ui.ts @@ -1,37 +1,10 @@ -import type { DevframeBranding, DevframeDockPreferences, EmbeddedVisibility } from '@devframes/hub-ui' +import type { DevframeBranding } from '@devframes/hub-ui' import type { DevframeHubUi } from '@devframes/hub/initiate' +import type { DevToolsBranding, ViteDevToolsUiOptions } from './plugin-options' import { createUi } from '@devframes/hub-ui' import { DEVTOOLS_ASSETS_BASE } from '../dirs' -export interface ViteDevToolsUiOptions { - /** - * Override the Vite DevTools branding handed to `@devframes/hub-ui` - * (`ConnectionMeta.configs.ui.branding`) — product name, logo, wordmark, - * primary color, tagline, favicon, and window title. - * - * Each field is merged over the built-in Vite DevTools defaults - * ({@link viteDevToolsBranding}), so a host such as Nuxt DevTools can - * re-skin the client while inheriting any field it leaves unset. Asset - * fields (`logo`/`wordmark`/`favicon`) take URL strings; a host serving its - * own marks is responsible for hosting them. - */ - branding?: DevframeBranding - /** - * How the embedded floating dock reveals itself on a fresh page. Seeds a - * user-overridable preference published as - * `ConnectionMeta.configs.ui.embeddedVisibility`. - * - * @default 'normal' - */ - embeddedVisibility?: EmbeddedVisibility - /** - * Dock-bar rendering preferences — category ordering, floating-dock - * inline-item capacity, and the first-run float/edge mode and position. - * Each seeds a user-overridable preference published as - * `ConnectionMeta.configs.ui.dockPreferences`. - */ - dockPreferences?: DevframeDockPreferences -} +export type { ViteDevToolsUiOptions } from './plugin-options' export function viteDevToolsBranding(): DevframeBranding { return { @@ -71,7 +44,7 @@ export function createViteDevToolsUi(options: ViteDevToolsUiOptions = {}): Devfr * the host actually sets win; an explicit `undefined` is ignored so a partial * override never clobbers a default with a hole. */ -function resolveBranding(overrides?: DevframeBranding): DevframeBranding { +function resolveBranding(overrides?: DevToolsBranding): DevframeBranding { const branding = viteDevToolsBranding() if (!overrides) { return branding From 3d1ea949c01bb226f95066a357ef67658540001b Mon Sep 17 00:00:00 2001 From: arlo Date: Mon, 24 Aug 2026 22:30:55 +0800 Subject: [PATCH 2/5] fix(core): exclude internal plugins from duplicate detection --- packages/core/src/node/__tests__/integration.test.ts | 2 +- packages/core/src/node/plugins/__tests__/index.test.ts | 10 +++++++++- packages/core/src/node/plugins/index.ts | 8 +++++--- packages/core/src/node/standalone.ts | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/core/src/node/__tests__/integration.test.ts b/packages/core/src/node/__tests__/integration.test.ts index 7b8dcc7da..9bbbac6a6 100644 --- a/packages/core/src/node/__tests__/integration.test.ts +++ b/packages/core/src/node/__tests__/integration.test.ts @@ -27,7 +27,6 @@ describe('devToolsIntegration', () => { expect((plugins as Plugin[]).map(plugin => plugin.name)).toEqual([ 'vite:devtools:builtin', - 'vite:devtools', 'vite:devtools:injection', 'vite:devtools:server', ]) @@ -55,6 +54,7 @@ describe('devToolsIntegration', () => { }) expect(plugins.map(plugin => plugin.name)).toContain('vite:devtools:build') + expect(plugins.map(plugin => plugin.name)).not.toContain('vite:devtools') }) it.each([ diff --git a/packages/core/src/node/plugins/__tests__/index.test.ts b/packages/core/src/node/plugins/__tests__/index.test.ts index 9c29df92e..48a0aaa05 100644 --- a/packages/core/src/node/plugins/__tests__/index.test.ts +++ b/packages/core/src/node/plugins/__tests__/index.test.ts @@ -1,13 +1,21 @@ import { isPackageExists } from 'local-pkg' import { resolve } from 'pathe' import { describe, expect, it, vi } from 'vitest' -import { DevTools } from '../index' +import { createDevToolsPlugins, DevTools } from '../index' vi.mock('local-pkg', () => ({ isPackageExists: vi.fn(() => false), })) describe('devTools', () => { + it('marks only the public manual plugin entry', async () => { + const manualPlugins = await DevTools({ builtinDevTools: false }) + const internalPlugins = await createDevToolsPlugins({ builtinDevTools: false }) + + expect(manualPlugins.map(plugin => plugin.name)).toContain('vite:devtools') + expect(internalPlugins.map(plugin => plugin.name)).not.toContain('vite:devtools') + }) + it('resolves optional integrations from the configured project directory', async () => { const cwd = 'project/root' const resolvedCwd = resolve(cwd) diff --git a/packages/core/src/node/plugins/index.ts b/packages/core/src/node/plugins/index.ts index e1b1006bf..4b3e28804 100644 --- a/packages/core/src/node/plugins/index.ts +++ b/packages/core/src/node/plugins/index.ts @@ -8,8 +8,11 @@ import { DevToolsServer } from './server' export type { DevToolsOptions } from '../plugin-options' -export function DevTools(options: DevToolsOptions = {}): Promise { - return createDevToolsPlugins(options) +export async function DevTools(options: DevToolsOptions = {}): Promise { + return [ + { name: 'vite:devtools' }, + ...await createDevToolsPlugins(options), + ] } export async function createDevToolsPlugins( @@ -27,7 +30,6 @@ export async function createDevToolsPlugins( const ui = { branding, embeddedVisibility, dockPreferences } const plugins = [ - { name: 'vite:devtools' }, DevToolsInjection(), DevToolsServer(ui, resolvedConfig), ] diff --git a/packages/core/src/node/standalone.ts b/packages/core/src/node/standalone.ts index aaa7017fc..3e6e8c8f6 100644 --- a/packages/core/src/node/standalone.ts +++ b/packages/core/src/node/standalone.ts @@ -2,7 +2,7 @@ import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit' import type { Plugin, ResolvedConfig } from 'vite' import process from 'node:process' import { createDevToolsContext } from './context' -import { DevTools } from './plugins' +import { createDevToolsPlugins } from './plugins' export interface StandaloneDevToolsOptions { cwd?: string @@ -28,7 +28,7 @@ export async function startStandaloneDevTools(options: StandaloneDevToolsOptions configFile: options.config, root: cwd, plugins: [ - DevTools({ cwd }), + createDevToolsPlugins({ cwd }), ], }, command, From 0c6bf0617b2e5610c4f05ce67de56708ed8cf502 Mon Sep 17 00:00:00 2001 From: arlo Date: Mon, 24 Aug 2026 22:59:13 +0800 Subject: [PATCH 3/5] fix(core): forward resolved config to standalone devtools --- .../src/node/__tests__/integration.test.ts | 52 ++++++++++++-- packages/core/src/node/cli-commands.ts | 56 +-------------- packages/core/src/node/plugins/index.ts | 22 ++++++ packages/core/src/node/plugins/integration.ts | 26 ++----- packages/core/src/node/standalone.ts | 15 +++- packages/core/src/node/start.ts | 68 +++++++++++++++++++ 6 files changed, 159 insertions(+), 80 deletions(-) create mode 100644 packages/core/src/node/start.ts diff --git a/packages/core/src/node/__tests__/integration.test.ts b/packages/core/src/node/__tests__/integration.test.ts index 9bbbac6a6..449267ef4 100644 --- a/packages/core/src/node/__tests__/integration.test.ts +++ b/packages/core/src/node/__tests__/integration.test.ts @@ -1,12 +1,20 @@ import type { Plugin, ResolvedConfig } from 'vite' -import { describe, expect, it } from 'vitest' -import { DevToolsIntegration } from '../plugins/integration' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { DevToolsIntegration, runDevTools } from '../plugins/integration' +import { startDevTools } from '../start' -function createConfig(command: 'serve' | 'build'): ResolvedConfig { +vi.mock('../start', () => ({ + startDevTools: vi.fn(), +})) + +function createConfig( + command: 'serve' | 'build', + environments: ResolvedConfig['environments'] = {}, +): ResolvedConfig { return { command, root: '/vite-devtools-test-project', - environments: {}, + environments, plugins: [], } as unknown as ResolvedConfig } @@ -19,6 +27,10 @@ function createDevToolsConfig(apply: 'serve' | 'build' | 'all') { } describe('devToolsIntegration', () => { + beforeEach(() => { + vi.mocked(startDevTools).mockClear() + }) + it('returns the existing DevTools plugins for serve', async () => { const plugins = await DevToolsIntegration({ config: createConfig('serve'), @@ -81,6 +93,38 @@ describe('devToolsIntegration', () => { expect(plugins).toEqual([]) }) + it('passes the resolved config to standalone DevTools', async () => { + const config = createConfig('build', { client: {} as never }) + + await runDevTools({ config }, { + host: 'dev.example.com', + options: { + allowedOrigins: ['https://dev.example.com'], + builtinDevTools: false, + clientAuthTokens: ['trusted-token'], + }, + }) + + const resolvedConfig = { + apply: 'all', + config: expect.objectContaining({ + allowedOrigins: ['https://dev.example.com'], + builtinDevTools: false, + clientAuth: true, + clientAuthTokens: ['trusted-token'], + host: 'dev.example.com', + }), + enabled: true, + } + expect(startDevTools).toHaveBeenCalledWith( + expect.objectContaining({ + host: 'dev.example.com', + root: '/vite-devtools-test-project', + }), + resolvedConfig, + ) + }) + it('enables Rolldown DevTools for selected build environments', async () => { const [plugin] = await DevToolsIntegration({ config: createConfig('build'), diff --git a/packages/core/src/node/cli-commands.ts b/packages/core/src/node/cli-commands.ts index 47fef5e87..50b5528ef 100644 --- a/packages/core/src/node/cli-commands.ts +++ b/packages/core/src/node/cli-commands.ts @@ -1,9 +1,6 @@ /* eslint-disable no-console */ -import { DEVTOOLS_MOUNT_PATH } from '@vitejs/devtools-kit/constants' -import { normalizeHttpServerUrl } from 'devframe/internal' import { colors as c } from 'devframe/utils/colors' -import { open } from 'devframe/utils/open' import { resolve } from 'pathe' import { MARK_NODE } from './constants' import { diagnostics } from './diagnostics' @@ -17,57 +14,8 @@ export interface StartOptions { } export async function start(options: StartOptions) { - const { host } = options - const { getPort } = await import('devframe/utils/get-port') - const port = await getPort({ - host, - port: options.port == null ? undefined : +options.port, - portRange: [9999, 15000], - }) - - const { startStandaloneDevTools } = await import('./standalone') - const { createDevToolsHub } = await import('./server') - - const devtools = await startStandaloneDevTools({ - cwd: options.root, - }) - - // Standalone has no shared HTTP server for the WS upgrade, so the hub opens - // a side-car WS server (advertised in `__connection.json`). Its middleware - // answers the whole `/__devtools/` surface — the branded hub-ui viewer, the - // connection meta, and the client bundles. - const { middleware } = await createDevToolsHub({ - context: devtools.context, - host, - }) - - const { createServer } = await import('node:http') - const { defineHandler, H3, sendRedirect } = await import('h3') - const { toNodeHandler } = await import('h3/node') - const { mountStaticHandler } = await import('devframe/utils/serve-static') - const { resolveStaticAssetsSource } = await import('devframe/utils/remote-assets') - - const app = new H3() - - const projectStorageDir = devtools.context.host.getStorageDir('project') - for (const { baseUrl, source } of devtools.context.views.buildStaticDirs) - mountStaticHandler(app, baseUrl, resolveStaticAssetsSource(source, projectStorageDir)) - - app.use('/', defineHandler(event => sendRedirect(event, DEVTOOLS_MOUNT_PATH, 302))) - - const appHandler = toNodeHandler(app) - // Hub first (owns `/__devtools/*`); anything outside its base falls through - // to the sub-frame statics + the root redirect. - const server = createServer((req, res) => { - middleware(req, res, () => appHandler(req, res)) - }) - - server.listen(port, host, async () => { - const url = normalizeHttpServerUrl(host, port) - console.log(c.green`${MARK_NODE} Vite DevTools started at`, c.green(url), '\n') - if (options.open) - await open(url) - }) + const { startDevTools } = await import('./start') + return startDevTools(options) } export interface BuildOptions { diff --git a/packages/core/src/node/plugins/index.ts b/packages/core/src/node/plugins/index.ts index 4b3e28804..c3c59acee 100644 --- a/packages/core/src/node/plugins/index.ts +++ b/packages/core/src/node/plugins/index.ts @@ -8,6 +8,28 @@ import { DevToolsServer } from './server' export type { DevToolsOptions } from '../plugin-options' +export function resolveDevToolsPluginOptions( + config: ResolvedDevToolsConfig, + cwd: string, +): DevToolsOptions { + const { + branding, + build, + builtinDevTools, + dockPreferences, + embeddedVisibility, + } = config.config + + return { + branding, + build, + builtinDevTools, + cwd, + dockPreferences, + embeddedVisibility, + } +} + export async function DevTools(options: DevToolsOptions = {}): Promise { return [ { name: 'vite:devtools' }, diff --git a/packages/core/src/node/plugins/integration.ts b/packages/core/src/node/plugins/integration.ts index 0f5bf2077..c83bcbb7a 100644 --- a/packages/core/src/node/plugins/integration.ts +++ b/packages/core/src/node/plugins/integration.ts @@ -1,8 +1,7 @@ import type { Plugin, ResolvedConfig, ViteBuilder } from 'vite' import type { DevToolsConfig, ResolvedDevToolsConfig } from '../config' -import type { DevToolsOptions } from '../plugin-options' import { isDevToolsEnabled, normalizeDevToolsConfig } from '../config' -import { createDevToolsPlugins } from './index' +import { createDevToolsPlugins, resolveDevToolsPluginOptions } from './index' type DevToolsEnvironment = ResolvedConfig['environments'][string] @@ -43,8 +42,11 @@ export async function runDevTools( return for (const _environment of getDevToolsEnvironments(config, devtoolsConfig)) { try { - const { start } = await import('../cli-commands') - await start(devtoolsConfig.config) + const { startDevTools } = await import('../start') + await startDevTools({ + ...devtoolsConfig.config, + root: devtoolsConfig.config.root ?? config.root, + }, devtoolsConfig) } catch (error: any) { config.logger.error( @@ -79,21 +81,7 @@ export async function DevToolsIntegration(options: DevToolsIntegrationOptions): return [] } - const { - branding, - build, - builtinDevTools, - dockPreferences, - embeddedVisibility, - } = devtoolsConfig.config - const pluginOptions: DevToolsOptions = { - branding, - build, - builtinDevTools, - cwd: config.root, - dockPreferences, - embeddedVisibility, - } + const pluginOptions = resolveDevToolsPluginOptions(devtoolsConfig, config.root) if (config.command === 'serve') { return createDevToolsPlugins(pluginOptions, devtoolsConfig) } diff --git a/packages/core/src/node/standalone.ts b/packages/core/src/node/standalone.ts index 3e6e8c8f6..3d14b3a79 100644 --- a/packages/core/src/node/standalone.ts +++ b/packages/core/src/node/standalone.ts @@ -1,8 +1,9 @@ import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit' import type { Plugin, ResolvedConfig } from 'vite' +import type { ResolvedDevToolsConfig } from './config' import process from 'node:process' import { createDevToolsContext } from './context' -import { createDevToolsPlugins } from './plugins' +import { createDevToolsPlugins, resolveDevToolsPluginOptions } from './plugins' export interface StandaloneDevToolsOptions { cwd?: string @@ -10,6 +11,7 @@ export interface StandaloneDevToolsOptions { config?: string command?: 'build' | 'serve' mode?: 'development' | 'production' + resolvedConfig?: ResolvedDevToolsConfig } export async function startStandaloneDevTools(options: StandaloneDevToolsOptions = {}): Promise<{ @@ -23,12 +25,15 @@ export async function startStandaloneDevTools(options: StandaloneDevToolsOptions } = options const { resolveConfig } = await import('vite') + const pluginOptions = options.resolvedConfig + ? resolveDevToolsPluginOptions(options.resolvedConfig, cwd) + : { cwd } const resolved = await resolveConfig( { configFile: options.config, root: cwd, plugins: [ - createDevToolsPlugins({ cwd }), + createDevToolsPlugins(pluginOptions, options.resolvedConfig), ], }, command, @@ -40,7 +45,11 @@ export async function startStandaloneDevTools(options: StandaloneDevToolsOptions plugin => plugin.name?.startsWith('vite:devtools'), ) - const context = await createDevToolsContext(resolved) + const context = await createDevToolsContext( + resolved, + undefined, + options.resolvedConfig, + ) return { config: resolved, diff --git a/packages/core/src/node/start.ts b/packages/core/src/node/start.ts new file mode 100644 index 000000000..4688be560 --- /dev/null +++ b/packages/core/src/node/start.ts @@ -0,0 +1,68 @@ +import type { StartOptions } from './cli-commands' +import type { ResolvedDevToolsConfig } from './config' +import { DEVTOOLS_MOUNT_PATH } from '@vitejs/devtools-kit/constants' +import { normalizeHttpServerUrl } from 'devframe/internal' +import { colors as c } from 'devframe/utils/colors' +import { open } from 'devframe/utils/open' +import { MARK_NODE } from './constants' + +export async function startDevTools( + options: StartOptions, + resolvedConfig?: ResolvedDevToolsConfig, +) { + const { host } = options + const { getPort } = await import('devframe/utils/get-port') + const port = await getPort({ + host, + port: options.port == null ? undefined : +options.port, + portRange: [9999, 15000], + }) + + const { startStandaloneDevTools } = await import('./standalone') + const { createDevToolsHub } = await import('./server') + + const devtools = await startStandaloneDevTools({ + config: options.config, + cwd: options.root, + resolvedConfig, + }) + + // Standalone has no shared HTTP server for the WS upgrade, so the hub opens + // a side-car WS server (advertised in `__connection.json`). Its middleware + // answers the whole `/__devtools/` surface — the branded hub-ui viewer, the + // connection meta, and the client bundles. + const { middleware } = await createDevToolsHub({ + context: devtools.context, + host, + ui: resolvedConfig?.config, + }) + + const { createServer } = await import('node:http') + const { defineHandler, H3, sendRedirect } = await import('h3') + const { toNodeHandler } = await import('h3/node') + const { mountStaticHandler } = await import('devframe/utils/serve-static') + const { resolveStaticAssetsSource } = await import('devframe/utils/remote-assets') + + const app = new H3() + + const projectStorageDir = devtools.context.host.getStorageDir('project') + for (const { baseUrl, source } of devtools.context.views.buildStaticDirs) + mountStaticHandler(app, baseUrl, resolveStaticAssetsSource(source, projectStorageDir)) + + app.use('/', defineHandler(event => sendRedirect(event, DEVTOOLS_MOUNT_PATH, 302))) + + const appHandler = toNodeHandler(app) + // Hub first (owns `/__devtools/*`); anything outside its base falls through + // to the sub-frame statics + the root redirect. + const server = createServer((req, res) => { + middleware(req, res, () => appHandler(req, res)) + }) + + server.listen(port, host, async () => { + const url = normalizeHttpServerUrl(host, port) + // eslint-disable-next-line no-console + console.log(c.green`${MARK_NODE} Vite DevTools started at`, c.green(url), '\n') + if (options.open) + await open(url) + }) +} From 67a4fd8f2e5d55be228bca9f27f7e1581e283007 Mon Sep 17 00:00:00 2001 From: arlo Date: Thu, 27 Aug 2026 17:01:44 +0800 Subject: [PATCH 4/5] chore: update --- packages/core/src/integration.ts | 3 +- .../src/node/__tests__/integration.test.ts | 50 +++++++++++++++---- packages/core/src/node/plugins/integration.ts | 37 +++++++++++--- .../devtools/integration.snapshot.d.ts | 3 +- 4 files changed, 73 insertions(+), 20 deletions(-) diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 8f2cc75a3..1e1e887d4 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -5,7 +5,8 @@ import { } from './node/plugins/integration' export interface DevToolsIntegrationOptions { - config: unknown + command: 'serve' | 'build' + root: string devtools: DevToolsIntegrationConfig } diff --git a/packages/core/src/node/__tests__/integration.test.ts b/packages/core/src/node/__tests__/integration.test.ts index 449267ef4..5d3a12a93 100644 --- a/packages/core/src/node/__tests__/integration.test.ts +++ b/packages/core/src/node/__tests__/integration.test.ts @@ -7,7 +7,7 @@ vi.mock('../start', () => ({ startDevTools: vi.fn(), })) -function createConfig( +function createResolvedConfig( command: 'serve' | 'build', environments: ResolvedConfig['environments'] = {}, ): ResolvedConfig { @@ -19,6 +19,17 @@ function createConfig( } as unknown as ResolvedConfig } +function createIntegrationOptions( + command: 'serve' | 'build', + devtools = createDevToolsConfig(command), +) { + return { + command, + devtools, + root: '/vite-devtools-test-project', + } as const +} + function createDevToolsConfig(apply: 'serve' | 'build' | 'all') { return { host: 'localhost', @@ -33,11 +44,12 @@ describe('devToolsIntegration', () => { it('returns the existing DevTools plugins for serve', async () => { const plugins = await DevToolsIntegration({ - config: createConfig('serve'), + ...createIntegrationOptions('serve'), devtools: createDevToolsConfig('serve'), }) expect((plugins as Plugin[]).map(plugin => plugin.name)).toEqual([ + 'vite:devtools:config', 'vite:devtools:builtin', 'vite:devtools:injection', 'vite:devtools:server', @@ -45,10 +57,11 @@ describe('devToolsIntegration', () => { }) it('returns the build integration plugin for build', async () => { - const [plugin] = await DevToolsIntegration({ - config: createConfig('build'), + const plugins = await DevToolsIntegration({ + ...createIntegrationOptions('build'), devtools: createDevToolsConfig('build'), }) + const plugin = plugins.find(plugin => plugin.name === 'vite:devtools:integration') expect(plugin).toMatchObject({ name: 'vite:devtools:integration', @@ -58,7 +71,7 @@ describe('devToolsIntegration', () => { it('creates the static build plugin from the core config', async () => { const plugins = await DevToolsIntegration({ - config: createConfig('build'), + ...createIntegrationOptions('build'), devtools: { host: 'localhost', options: { build: { withApp: true } }, @@ -74,19 +87,19 @@ describe('devToolsIntegration', () => { { 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), + ...createIntegrationOptions(command), devtools: createDevToolsConfig('all'), }) const plugin = command === 'serve' ? plugins.find(plugin => plugin.name === 'vite:devtools:server') - : plugins[0] + : plugins.find(plugin => plugin.name === 'vite:devtools:integration') expect(plugin?.enforce).toBe(expected) }) it('returns no plugins when apply excludes the current command', async () => { const plugins = await DevToolsIntegration({ - config: createConfig('serve'), + ...createIntegrationOptions('serve'), devtools: createDevToolsConfig('build'), }) @@ -94,7 +107,7 @@ describe('devToolsIntegration', () => { }) it('passes the resolved config to standalone DevTools', async () => { - const config = createConfig('build', { client: {} as never }) + const config = createResolvedConfig('build', { client: {} as never }) await runDevTools({ config }, { host: 'dev.example.com', @@ -126,13 +139,14 @@ describe('devToolsIntegration', () => { }) it('enables Rolldown DevTools for selected build environments', async () => { - const [plugin] = await DevToolsIntegration({ - config: createConfig('build'), + const plugins = await DevToolsIntegration({ + ...createIntegrationOptions('build'), devtools: { host: 'localhost', options: { environments: ['client'] }, }, }) + const plugin = plugins.find(plugin => plugin.name === 'vite:devtools:integration') const client: { build: { rolldownOptions: { devtools?: object } } } = { build: { rolldownOptions: {} } } const ssr: { build: { rolldownOptions: { devtools?: object } } } = { build: { rolldownOptions: {} } } const config = { @@ -147,4 +161,18 @@ describe('devToolsIntegration', () => { expect(client.build.rolldownOptions.devtools).toEqual({}) expect(ssr.build.rolldownOptions.devtools).toBeUndefined() }) + + it('returns a pre plugin for refreshing the resolved integration config', async () => { + const plugins = await DevToolsIntegration({ + ...createIntegrationOptions('serve'), + devtools: createDevToolsConfig('serve'), + }) + const configPlugin = plugins.find(plugin => plugin.name === 'vite:devtools:config') + + expect(configPlugin).toMatchObject({ + apply: 'serve', + enforce: 'pre', + configResolved: { order: 'pre' }, + }) + }) }) diff --git a/packages/core/src/node/plugins/integration.ts b/packages/core/src/node/plugins/integration.ts index c83bcbb7a..86faa3de7 100644 --- a/packages/core/src/node/plugins/integration.ts +++ b/packages/core/src/node/plugins/integration.ts @@ -6,7 +6,8 @@ import { createDevToolsPlugins, resolveDevToolsPluginOptions } from './index' type DevToolsEnvironment = ResolvedConfig['environments'][string] export interface DevToolsIntegrationOptions { - config: ResolvedConfig + command: 'serve' | 'build' + root: string devtools: DevToolsIntegrationConfig } @@ -73,20 +74,42 @@ function DevToolsBuildIntegration(devtoolsConfig: ResolvedDevToolsConfig): Plugi } } +function DevToolsConfigIntegration( + devtools: DevToolsIntegrationConfig, + devtoolsConfig: ResolvedDevToolsConfig, + command: 'serve' | 'build', +): Plugin { + return { + name: 'vite:devtools:config', + enforce: 'pre', + apply: command, + configResolved: { + order: 'pre', + handler() { + const resolved = normalizeDevToolsConfig(devtools.options, devtools.host) + devtoolsConfig.apply = resolved.apply + devtoolsConfig.config = resolved.config + devtoolsConfig.enabled = resolved.enabled + }, + }, + } +} + export async function DevToolsIntegration(options: DevToolsIntegrationOptions): Promise { - const { config, devtools } = options + const { command, devtools, root } = options const devtoolsConfig = normalizeDevToolsConfig(devtools.options, devtools.host) - const enabled = isDevToolsEnabled(devtoolsConfig, config.command) + const enabled = isDevToolsEnabled(devtoolsConfig, command) if (!enabled) { return [] } - const pluginOptions = resolveDevToolsPluginOptions(devtoolsConfig, config.root) - if (config.command === 'serve') { - return createDevToolsPlugins(pluginOptions, devtoolsConfig) + const pluginOptions = resolveDevToolsPluginOptions(devtoolsConfig, root) + const configPlugin = DevToolsConfigIntegration(devtools, devtoolsConfig, command) + if (command === 'serve') { + return [configPlugin, ...await createDevToolsPlugins(pluginOptions, devtoolsConfig)] } - const plugins = [DevToolsBuildIntegration(devtoolsConfig)] + const plugins = [configPlugin, DevToolsBuildIntegration(devtoolsConfig)] if (devtoolsConfig.config.build?.withApp) { plugins.push(...await createDevToolsPlugins(pluginOptions, devtoolsConfig)) } diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/integration.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools/integration.snapshot.d.ts index 514f0e173..f1d588974 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools/integration.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools/integration.snapshot.d.ts @@ -7,7 +7,8 @@ export interface DevToolsIntegrationConfig { options: boolean | DevToolsConfig | undefined; } export interface DevToolsIntegrationOptions { - config: unknown; + command: 'serve' | 'build'; + root: string; devtools: DevToolsIntegrationConfig; } // #endregion From 0638c37ff01fd6fd3e041efb58b0bb35f88d6d72 Mon Sep 17 00:00:00 2001 From: arlo Date: Thu, 27 Aug 2026 18:11:28 +0800 Subject: [PATCH 5/5] chore: update --- packages/core/src/integration.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 1e1e887d4..0ab1d2305 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,9 +1,14 @@ -import type { DevToolsIntegrationConfig } from './node/plugins/integration' +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 { command: 'serve' | 'build' root: string @@ -11,7 +16,7 @@ export interface DevToolsIntegrationOptions { } export function DevToolsIntegration(options: DevToolsIntegrationOptions): Promise<{ name: string }[]> { - return _DevToolsIntegration(options as Parameters[0]) + return _DevToolsIntegration(options) } export function runDevTools( @@ -20,5 +25,3 @@ export function runDevTools( ): Promise { return _runDevTools(builder, devtools) } - -export type { DevToolsIntegrationConfig }