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/errors/DTK0034.md b/docs/errors/DTK0034.md
new file mode 100644
index 000000000..8d9f19b37
--- /dev/null
+++ b/docs/errors/DTK0034.md
@@ -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.
diff --git a/docs/errors/index.md b/docs/errors/index.md
index df0e2699d..8674fb36f 100644
--- a/docs/errors/index.md
+++ b/docs/errors/index.md
@@ -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 |
diff --git a/docs/guide/index.md b/docs/guide/index.md
index f2f2ab55c..de0610a3c 100644
--- a/docs/guide/index.md
+++ b/docs/guide/index.md
@@ -70,22 +70,17 @@ 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',
+ apply: 'serve',
+ embeddedVisibility: 'passive',
},
})
```
@@ -93,20 +88,15 @@ 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',
+ apply: 'serve',
+ dockPreferences: {
+ defaultMode: 'edge',
+ defaultPosition: 'bottom',
+ },
},
})
```
@@ -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
+ },
+ },
})
```
diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts
index 7aac62feb..292156d41 100644
--- a/packages/core/src/integration.ts
+++ b/packages/core/src/integration.ts
@@ -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[0])
+ return _DevToolsIntegration(options)
}
export function runDevTools(builder: unknown): Promise {
diff --git a/packages/core/src/node/__tests__/auth-handler.test.ts b/packages/core/src/node/__tests__/auth-handler.test.ts
index e80bce6ad..6ebd77f58 100644
--- a/packages/core/src/node/__tests__/auth-handler.test.ts
+++ b/packages/core/src/node/__tests__/auth-handler.test.ts
@@ -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, 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()
@@ -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()
@@ -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()
@@ -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)
})
})
diff --git a/packages/core/src/node/__tests__/context-auth.test.ts b/packages/core/src/node/__tests__/context-auth.test.ts
index 568d0c4b3..1d0cacb5b 100644
--- a/packages/core/src/node/__tests__/context-auth.test.ts
+++ b/packages/core/src/node/__tests__/context-auth.test.ts
@@ -1,36 +1,46 @@
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`.
@@ -38,13 +48,21 @@ describe('createDevToolsContext auth registration', () => {
})
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)
})
@@ -52,7 +70,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..cf6a79a0b 100644
--- a/packages/core/src/node/__tests__/integration.test.ts
+++ b/packages/core/src/node/__tests__/integration.test.ts
@@ -1,24 +1,58 @@
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 { normalizeDevToolsConfig } from '../config'
+import { DevToolsIntegration, runDevTools } from '../plugins/integration'
+import { startDevTools } from '../start'
-function createConfig(command: 'serve' | 'build', apply: 'serve' | 'build' | 'all' = command): ResolvedConfig {
+vi.mock('../start', () => ({
+ startDevTools: vi.fn(),
+}))
+
+function createResolvedConfig(
+ command: 'serve' | 'build',
+ environments: ResolvedConfig['environments'] = {},
+): ResolvedConfig {
return {
command,
+ devtools: false,
root: '/vite-devtools-test-project',
- devtools: {
- apply,
- config: {},
- enabled: true,
- },
+ environments,
+ plugins: [],
+ server: { host: 'localhost' },
} 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',
+ options: { apply },
+ } as const
+}
+
describe('devToolsIntegration', () => {
+ beforeEach(() => {
+ vi.mocked(startDevTools).mockClear()
+ })
+
it('returns the existing DevTools plugins for serve', async () => {
- const plugins = await DevToolsIntegration({ config: createConfig('serve') })
+ const plugins = await DevToolsIntegration({
+ ...createIntegrationOptions('serve'),
+ devtools: createDevToolsConfig('serve'),
+ })
expect((plugins as Plugin[]).map(plugin => plugin.name)).toEqual([
+ 'vite:devtools',
'vite:devtools:builtin',
'vite:devtools:injection',
'vite:devtools:server',
@@ -26,7 +60,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',
@@ -34,33 +72,113 @@ describe('devToolsIntegration', () => {
})
})
+ it('creates the static build plugin from the core config', async () => {
+ const plugins = await DevToolsIntegration({
+ ...createIntegrationOptions('build'),
+ devtools: {
+ host: 'localhost',
+ options: { build: { withApp: true } },
+ },
+ })
+
+ expect(plugins.map(plugin => plugin.name)).toContain('vite:devtools:build')
+ expect(plugins.filter(plugin => plugin.name === 'vite:devtools')).toHaveLength(1)
+ })
+
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({
+ ...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', 'build') })
+ const plugins = await DevToolsIntegration({
+ ...createIntegrationOptions('serve'),
+ devtools: createDevToolsConfig('build'),
+ })
expect(plugins).toEqual([])
})
+ it('passes the resolved config to standalone DevTools', async () => {
+ const config = createResolvedConfig('build', { client: {} as never })
+ const plugins = await DevToolsIntegration({
+ command: 'build',
+ devtools: {
+ host: 'dev.example.com',
+ options: {
+ allowedOrigins: ['https://dev.example.com'],
+ builtinDevTools: false,
+ clientAuthTokens: ['trusted-token'],
+ },
+ },
+ root: config.root,
+ })
+ Object.assign(config, {
+ plugins,
+ server: { host: 'dev.example.com' },
+ })
+ const configPlugin = plugins.find(plugin => plugin.name === 'vite:devtools')
+ const configResolved = configPlugin?.configResolved
+ if (typeof configResolved !== 'object')
+ throw new TypeError('Expected an object configResolved hook')
+ await configResolved.handler.call({} as never, config)
+
+ await runDevTools({ config })
+
+ 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('does not run standalone DevTools for a manual plugin config', async () => {
+ const config = createResolvedConfig('build', { client: {} as never })
+ Object.assign(config, {
+ devtools: normalizeDevToolsConfig(true, 'localhost'),
+ plugins: [{ name: 'vite:devtools' }],
+ })
+
+ await runDevTools({ config })
+
+ expect(startDevTools).not.toHaveBeenCalled()
+ })
+
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 = {
- devtools: {
- config: { environments: ['client'] },
- enabled: true,
- },
environments: { client, ssr },
} as unknown as ResolvedConfig
@@ -72,4 +190,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')
+
+ expect(configPlugin).toMatchObject({
+ apply: 'serve',
+ enforce: 'pre',
+ configResolved: { order: 'pre' },
+ })
+ })
})
diff --git a/packages/core/src/node/auth-handler.ts b/packages/core/src/node/auth-handler.ts
index 272143db4..c7548ed21 100644
--- a/packages/core/src/node/auth-handler.ts
+++ b/packages/core/src/node/auth-handler.ts
@@ -1,8 +1,8 @@
import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
-import type { DevToolsConfig } from './config'
import { randomBytes } from 'node:crypto'
import process from 'node:process'
import { createInteractiveAuth } from 'devframe/recipes/interactive-auth'
+import { getResolvedDevToolsConfig } from './resolved-config'
export type DevToolsAuthHandler = ReturnType
@@ -44,16 +44,16 @@ export function getBuildCapabilityToken(context: ViteDevToolsNodeContext): strin
export function getAuthHandler(context: ViteDevToolsNodeContext): DevToolsAuthHandler {
let handler = handlers.get(context)
if (!handler) {
- const config = context.viteConfig.devtools?.config as DevToolsConfig | undefined
+ const config = getResolvedDevToolsConfig(context).config
const buildCapability = isBuildCapabilityAuth(context)
- const clientAuthTokens = config?.clientAuthTokens ? [...config.clientAuthTokens] : []
+ const clientAuthTokens = config.clientAuthTokens ? [...config.clientAuthTokens] : []
if (buildCapability)
clientAuthTokens.push(getBuildCapabilityToken(context))
handler = createInteractiveAuth(context, {
clientAuthTokens,
// Build mode trusts purely via the per-process capability token baked
// into the served connection meta, so silence the OTP console banner.
- banner: buildCapability ? () => {} : config?.banner,
+ banner: buildCapability ? () => {} : config.banner,
})
handlers.set(context, handler)
}
@@ -76,7 +76,7 @@ export function getAuthHandler(context: ViteDevToolsNodeContext): DevToolsAuthHa
* {@link isBuildCapabilityAuth}.
*/
export function isClientAuthDisabled(context: ViteDevToolsNodeContext): boolean {
- return context.viteConfig.devtools?.config?.clientAuth === false
+ return getResolvedDevToolsConfig(context).config.clientAuth === false
|| process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH === 'true'
}
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/config.ts b/packages/core/src/node/config.ts
index bcde84845..be946b36b 100644
--- a/packages/core/src/node/config.ts
+++ b/packages/core/src/node/config.ts
@@ -1,9 +1,9 @@
-import type { CreateInteractiveAuthOptions } from 'devframe/recipes/interactive-auth'
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.
*
@@ -42,7 +42,7 @@ export interface DevToolsConfig extends Partial {
* The default banner is a boxed `console.log` from inside the dev server.
* Supply this to surface the code in the host's own chrome instead.
*/
- banner?: CreateInteractiveAuthOptions['banner']
+ banner?: (info: { code: string, url: string }) => void
/**
* Origins allowed to open the DevTools WebSocket connection, in addition to the built-in
* loopback allowlist (`localhost`, `127.0.0.1`, etc).
diff --git a/packages/core/src/node/context.ts b/packages/core/src/node/context.ts
index 0b0bcaf09..cb69d12c5 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/diagnostics.ts b/packages/core/src/node/diagnostics.ts
index 6a7f4da87..ea0ba52f5 100644
--- a/packages/core/src/node/diagnostics.ts
+++ b/packages/core/src/node/diagnostics.ts
@@ -44,5 +44,9 @@ export const diagnostics = /* #__PURE__ */ defineDiagnostics({
why: (p: { file: string }) => `Failed to persist the DevTools mode flag at "${p.file}"`,
fix: 'Ensure the project\'s node_modules directory is writable.',
},
+ DTK0034: {
+ why: 'Vite DevTools has been registered multiple times.',
+ fix: 'Remove the duplicate Vite DevTools registration.',
+ },
},
})
diff --git a/packages/core/src/node/plugin-options.ts b/packages/core/src/node/plugin-options.ts
new file mode 100644
index 000000000..e72e75e15
--- /dev/null
+++ b/packages/core/src/node/plugin-options.ts
@@ -0,0 +1,66 @@
+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 interface DevToolsDockRendererRegistration {
+ type: string
+ file: string
+ importName?: string
+}
+
+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
+ /** Dock renderer modules, replacing built-ins with the same type and appending new types. */
+ renderers?: readonly DevToolsDockRendererRegistration[]
+ /** 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/__tests__/index.test.ts b/packages/core/src/node/plugins/__tests__/index.test.ts
index 9c29df92e..784287256 100644
--- a/packages/core/src/node/plugins/__tests__/index.test.ts
+++ b/packages/core/src/node/plugins/__tests__/index.test.ts
@@ -1,13 +1,23 @@
import { isPackageExists } from 'local-pkg'
import { resolve } from 'pathe'
+import { resolveConfig } from 'vite'
import { describe, expect, it, vi } from 'vitest'
-import { DevTools } from '../index'
+import { createDevToolsPlugins, DevTools } from '../index'
+import { DevToolsIntegration } from '../integration'
vi.mock('local-pkg', () => ({
isPackageExists: vi.fn(() => false),
}))
describe('devTools', () => {
+ it('marks 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)
@@ -22,4 +32,76 @@ describe('devTools', () => {
['@vitejs/devtools-oxc', { paths: [resolvedCwd] }],
])
})
+
+ it('populates the resolved config for the manual plugin', async () => {
+ let observedDevTools: unknown
+ const plugins = await DevTools({
+ builtinDevTools: false,
+ embeddedVisibility: 'passive',
+ })
+ const config = await resolveConfig({
+ configFile: false,
+ plugins: [
+ plugins,
+ {
+ name: 'observe-devtools-config',
+ configResolved(config) {
+ observedDevTools = config.devtools
+ },
+ },
+ ],
+ }, 'serve')
+
+ expect(config.devtools).toMatchObject({
+ apply: 'all',
+ config: {
+ builtinDevTools: false,
+ clientAuth: true,
+ clientAuthTokens: [],
+ embeddedVisibility: 'passive',
+ host: 'localhost',
+ },
+ enabled: true,
+ })
+ expect(observedDevTools).toBe(config.devtools)
+ })
+
+ it('uses the explicitly registered manual plugin when auto integration is disabled', async () => {
+ const config = await resolveConfig({
+ configFile: false,
+ devtools: false,
+ plugins: [DevTools({ builtinDevTools: false })],
+ }, 'serve')
+
+ expect(config.devtools).toMatchObject({
+ config: { builtinDevTools: false },
+ enabled: true,
+ })
+ })
+
+ it('rejects duplicate manual plugin instances', async () => {
+ await expect(resolveConfig({
+ configFile: false,
+ plugins: [
+ DevTools({ builtinDevTools: false }),
+ DevTools({ builtinDevTools: false }),
+ ],
+ }, 'serve')).rejects.toThrow('Vite DevTools has been registered multiple times.')
+ })
+
+ it('rejects automatic and manual plugin instances together', async () => {
+ const integrationPlugins = await DevToolsIntegration({
+ command: 'serve',
+ devtools: { host: 'localhost', options: true },
+ root: process.cwd(),
+ })
+
+ await expect(resolveConfig({
+ configFile: false,
+ plugins: [
+ integrationPlugins,
+ DevTools({ builtinDevTools: false }),
+ ],
+ }, 'serve')).rejects.toThrow('Vite DevTools has been registered multiple times.')
+ })
})
diff --git a/packages/core/src/node/plugins/build.ts b/packages/core/src/node/plugins/build.ts
index 02669938e..dd00ef7b0 100644
--- a/packages/core/src/node/plugins/build.ts
+++ b/packages/core/src/node/plugins/build.ts
@@ -2,12 +2,14 @@
import type { DockRendererRegistration, 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
renderers?: readonly DockRendererRegistration[]
/** Reference-UI options forwarded to the static snapshot's `createUi`. */
@@ -28,7 +30,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/config.ts b/packages/core/src/node/plugins/config.ts
new file mode 100644
index 000000000..b4c9ee720
--- /dev/null
+++ b/packages/core/src/node/plugins/config.ts
@@ -0,0 +1,29 @@
+import type { Plugin } from 'vite'
+import type { DevToolsConfig, ResolvedDevToolsConfig } from '../config'
+import { normalizeDevToolsConfig } from '../config'
+import { diagnostics } from '../diagnostics'
+
+export function DevToolsConfigPlugin(
+ config: boolean | DevToolsConfig | undefined,
+ resolvedConfig: ResolvedDevToolsConfig,
+ command?: 'serve' | 'build',
+): Plugin {
+ return {
+ name: 'vite:devtools',
+ enforce: 'pre',
+ apply: command,
+ configResolved: {
+ order: 'pre',
+ handler(viteConfig) {
+ if (viteConfig.plugins.filter(plugin => plugin.name === 'vite:devtools').length > 1)
+ throw diagnostics.DTK0034()
+
+ const host = viteConfig.server.host === true
+ ? '0.0.0.0'
+ : viteConfig.server.host || 'localhost'
+ Object.assign(resolvedConfig, normalizeDevToolsConfig(config, host))
+ Object.assign(viteConfig, { devtools: resolvedConfig })
+ },
+ },
+ }
+}
diff --git a/packages/core/src/node/plugins/index.ts b/packages/core/src/node/plugins/index.ts
index e2ded6f0c..b1b46f0c6 100644
--- a/packages/core/src/node/plugins/index.ts
+++ b/packages/core/src/node/plugins/index.ts
@@ -1,99 +1,76 @@
-import type { DockRendererRegistration } from '@vitejs/devtools-kit'
import type { Plugin } from 'vite'
-import type { ViteDevToolsUiOptions } from '../ui'
+import type { DevToolsConfig, ResolvedDevToolsConfig } from '../config'
+import type { DevToolsOptions } from '../plugin-options'
+import { normalizeDevToolsConfig } from '../config'
import { DevToolsBuild } from './build'
import { DevToolsBuiltin } from './builtin'
+import { DevToolsConfigPlugin } from './config'
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'
- /** Dock renderer modules, replacing built-ins with the same type and appending new types. */
- renderers?: readonly DockRendererRegistration[]
-
- /**
- * 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']
+export function resolveDevToolsPluginOptions(
+ config: ResolvedDevToolsConfig,
+ cwd: string,
+): DevToolsOptions {
+ const {
+ branding,
+ build,
+ builtinDevTools,
+ dockPreferences,
+ embeddedVisibility,
+ renderers,
+ } = config.config
- /**
- * 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
+ return {
+ branding,
+ build,
+ builtinDevTools,
+ cwd,
+ dockPreferences,
+ embeddedVisibility,
+ renderers,
}
}
export async function DevTools(options: DevToolsOptions = {}): Promise {
+ const { cwd: _cwd, ...configOptions } = options
+ const config: DevToolsConfig = { ...configOptions, enabled: true }
+ const resolvedConfig = normalizeDevToolsConfig(config, 'localhost')
+ return [
+ DevToolsConfigPlugin(config, resolvedConfig),
+ ...await createDevToolsPlugins(options, resolvedConfig),
+ ]
+}
+
+export async function createDevToolsPlugins(
+ options: DevToolsOptions = {},
+ resolvedConfig?: ResolvedDevToolsConfig,
+): Promise {
const {
builtinDevTools = true,
build,
branding,
embeddedVisibility = 'normal',
dockPreferences,
+ renderers,
} = options
const ui = { branding, embeddedVisibility, dockPreferences }
const plugins = [
DevToolsInjection(),
- DevToolsServer(ui, options.renderers),
+ DevToolsServer(ui, resolvedConfig, renderers),
]
if (build?.withApp) {
- plugins.push(DevToolsBuild({ outDir: build.outDir, ui, renderers: options.renderers }))
+ plugins.push(DevToolsBuild({
+ outDir: build.outDir,
+ renderers,
+ resolvedConfig,
+ ui,
+ }))
}
plugins.unshift(
diff --git a/packages/core/src/node/plugins/integration.ts b/packages/core/src/node/plugins/integration.ts
index 332ca20bc..60a64683e 100644
--- a/packages/core/src/node/plugins/integration.ts
+++ b/packages/core/src/node/plugins/integration.ts
@@ -1,16 +1,27 @@
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 { isDevToolsEnabled, normalizeDevToolsConfig } from '../config'
+import { DevToolsConfigPlugin } from './config'
+import { createDevToolsPlugins, resolveDevToolsPluginOptions } from './index'
type DevToolsEnvironment = ResolvedConfig['environments'][string]
+const DEVTOOLS_BUILD_INTEGRATION_NAME = 'vite:devtools:integration'
export interface DevToolsIntegrationOptions {
- config: ResolvedConfig
+ command: 'serve' | 'build'
+ root: string
+ 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 +35,26 @@ function getDevToolsEnvironments(config: ResolvedConfig): DevToolsEnvironment[]
return environments
}
-export async function runDevTools(builder: unknown) {
+export async function runDevTools(
+ builder: unknown,
+) {
const config = (builder as ViteBuilder).config
- if (!isDevToolsEnabled(config.devtools as ResolvedDevToolsConfig, config.command))
+ if (!config.plugins.some(plugin => plugin.name === DEVTOOLS_BUILD_INTEGRATION_NAME))
+ return
+
+ const devtoolsConfig = config.devtools as unknown as ResolvedDevToolsConfig | false
+ if (!devtoolsConfig)
+ return
+
+ 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)
+ const { startDevTools } = await import('../start')
+ await startDevTools({
+ ...devtoolsConfig.config,
+ root: devtoolsConfig.config.root ?? config.root,
+ }, devtoolsConfig)
}
catch (error: any) {
config.logger.error(
@@ -42,15 +65,15 @@ export async function runDevTools(builder: unknown) {
}
}
-function DevToolsBuildIntegration(): Plugin {
+function DevToolsBuildIntegration(devtoolsConfig: ResolvedDevToolsConfig): Plugin {
return {
- name: 'vite:devtools:integration',
+ name: DEVTOOLS_BUILD_INTEGRATION_NAME,
apply: 'build',
configResolved: {
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 +82,22 @@ function DevToolsBuildIntegration(): Plugin {
}
export async function DevToolsIntegration(options: DevToolsIntegrationOptions): Promise {
- const config = options.config
- if (!isDevToolsEnabled(config.devtools as ResolvedDevToolsConfig, config.command))
+ const { command, devtools, root } = options
+ const devtoolsConfig = normalizeDevToolsConfig(devtools.options, devtools.host)
+ const enabled = isDevToolsEnabled(devtoolsConfig, command)
+ if (!enabled) {
return []
- return options.config.command === 'serve'
- ? DevTools({ cwd: options.config.root })
- : [DevToolsBuildIntegration()]
+ }
+
+ const pluginOptions = resolveDevToolsPluginOptions(devtoolsConfig, root)
+ const configPlugin = DevToolsConfigPlugin(devtools.options, devtoolsConfig, command)
+ if (command === 'serve') {
+ return [configPlugin, ...await createDevToolsPlugins(pluginOptions, devtoolsConfig)]
+ }
+
+ const plugins = [configPlugin, 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 6586062ae..6d2ee492c 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, DockRendererRegistration, 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,
@@ -39,6 +40,7 @@ export function renderDockImportsMap(docks: Iterable): string
export function DevToolsServer(
options: ViteDevToolsUiOptions = {},
+ devtoolsConfig?: ResolvedDevToolsConfig,
renderers?: readonly DockRendererRegistration[],
): Plugin {
let context: ViteDevToolsNodeContext
@@ -48,7 +50,11 @@ export function DevToolsServer(
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 03d9a82f9..133353b61 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, DockRendererRegistration, 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 { DEVTOOLS_CONNECTION_META_FILENAME, DEVTOOLS_MOUNT_PATH } from '@vitejs/devtools-kit/constants'
import { getAuthHandler, getBuildCapabilityToken, isBuildCapabilityAuth, isClientAuthDisabled } from './auth-handler'
import { resolveDockRendererRegistrations } from './renderers'
+import { getResolvedDevToolsConfig } from './resolved-config'
import { createViteDevToolsUi } from './ui'
export interface CreateDevToolsHubOptions {
@@ -70,9 +70,7 @@ export async function createDevToolsHub(options: CreateDevToolsHubOptions): Prom
? getBuildCapabilityToken(context)
: undefined
- // 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/standalone.ts b/packages/core/src/node/standalone.ts
index aaa7017fc..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 { DevTools } 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: [
- DevTools({ 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..8dac12e9a
--- /dev/null
+++ b/packages/core/src/node/start.ts
@@ -0,0 +1,69 @@
+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,
+ renderers: resolvedConfig?.config.renderers,
+ 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)
+ })
+}
diff --git a/packages/core/src/node/ui.ts b/packages/core/src/node/ui.ts
index f7dd7f597..d4e2fbaf1 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,12 +44,12 @@ 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
}
- for (const key of Object.keys(overrides) as (keyof DevframeBranding)[]) {
+ for (const key of Object.keys(overrides) as (keyof DevToolsBranding)[]) {
const value = overrides[key]
if (value !== undefined) {
branding[key] = value as never
diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/cli-commands.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools/cli-commands.snapshot.d.ts
index 5fc89c50b..221b6d9df 100644
--- a/test/__snapshots__/tsnapi/@vitejs/devtools/cli-commands.snapshot.d.ts
+++ b/test/__snapshots__/tsnapi/@vitejs/devtools/cli-commands.snapshot.d.ts
@@ -1,23 +1,9 @@
/**
* Generated by tsnapi — public API snapshot of `@vitejs/devtools/cli-commands`
*/
-// #region Interfaces
-export interface BuildOptions {
- root: string;
- config?: string;
- outDir: string;
- base: string;
-}
-export interface StartOptions {
- root?: string;
- config?: string;
- host: string;
- port?: string | number;
- open?: boolean;
-}
-// #endregion
-
-// #region Functions
-export declare function build(_: BuildOptions): Promise;
-export declare function start(_: StartOptions): Promise;
+// #region Other
+export { build }
+export { BuildOptions }
+export { start }
+export { StartOptions }
// #endregion
\ No newline at end of file
diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/config.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools/config.snapshot.d.ts
index 58e68b9c3..17d54e781 100644
--- a/test/__snapshots__/tsnapi/@vitejs/devtools/config.snapshot.d.ts
+++ b/test/__snapshots__/tsnapi/@vitejs/devtools/config.snapshot.d.ts
@@ -1,30 +1,10 @@
/**
* Generated by tsnapi — public API snapshot of `@vitejs/devtools/config`
*/
-// #region Interfaces
-export interface DevToolsConfig extends Partial {
- enabled?: boolean;
- apply?: DevToolsApply;
- environments?: string[];
- clientAuth?: boolean;
- clientAuthTokens?: string[];
- banner?: CreateInteractiveAuthOptions['banner'];
- allowedOrigins?: string[];
-}
-export interface ResolvedDevToolsConfig {
- 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;
+// #region Other
+export { DevToolsApply }
+export { DevToolsConfig }
+export { isDevToolsEnabled }
+export { normalizeDevToolsConfig }
+export { ResolvedDevToolsConfig }
// #endregion
\ No newline at end of file
diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.d.ts
index 47292c348..c0b4b2cae 100644
--- a/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.d.ts
+++ b/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.d.ts
@@ -23,31 +23,11 @@ export type BuiltinServerFunctions = RpcDefinitionsToFunctions;
+export declare function createDevToolsContext(_: ResolvedConfig, _?: ViteDevServer, _?: ResolvedDevToolsConfig): Promise;
export declare function createDevToolsHub(_: CreateDevToolsHubOptions): Promise;
export declare function DevTools(_?: DevToolsOptions): Promise;
// #endregion
-// #region Referenced (internal)
-interface DevToolsOptions {
- cwd?: string;
- builtinDevTools?: boolean;
- renderers?: readonly DockRendererRegistration[];
- branding?: ViteDevToolsUiOptions['branding'];
- embeddedVisibility?: ViteDevToolsUiOptions['embeddedVisibility'];
- dockPreferences?: ViteDevToolsUiOptions['dockPreferences'];
- build?: {
- withApp?: boolean;
- outDir?: string;
- };
-}
-interface ViteDevToolsUiOptions {
- branding?: DevframeBranding;
- embeddedVisibility?: EmbeddedVisibility;
- dockPreferences?: DevframeDockPreferences;
-}
-// #endregion
-
// #region Other
export { DevToolsInternalContext }
export { InternalAnonymousAuthStorage }
diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.js b/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.js
index 56da853b6..b0c91366a 100644
--- a/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.js
+++ b/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.js
@@ -2,7 +2,7 @@
* Generated by tsnapi — public API snapshot of `@vitejs/devtools`
*/
// #region Functions
-export async function createDevToolsContext(_, _) {}
+export async function createDevToolsContext(_, _, _) {}
export async function createDevToolsHub(_) {}
export async function DevTools(_) {}
// #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 2c342bf84..018987078 100644
--- a/test/__snapshots__/tsnapi/@vitejs/devtools/integration.snapshot.d.ts
+++ b/test/__snapshots__/tsnapi/@vitejs/devtools/integration.snapshot.d.ts
@@ -2,8 +2,14 @@
* Generated by tsnapi — public API snapshot of `@vitejs/devtools/integration`
*/
// #region Interfaces
+export interface DevToolsIntegrationConfig {
+ host: string;
+ options: boolean | DevToolsConfig | undefined;
+}
export interface DevToolsIntegrationOptions {
- config: unknown;
+ command: 'serve' | 'build';
+ root: string;
+ devtools: DevToolsIntegrationConfig;
}
// #endregion