From 367f8f274acb1c02fc687619126589040b1417df Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Tue, 28 Jul 2026 05:21:30 +0000 Subject: [PATCH 1/2] feat(devtools): unify Data Inspector Vite instance and refresh builtin queries Nuxt 5 merged the separate client and SSR Vite dev servers into a single instance, so the Data Inspector's live source now captures and surfaces one Vite config (the former per-environment configs live under `vite.environments`) instead of a `vite: { client, ssr }` split. Refresh the source's suggested queries to target the surfaces authors care about: Nuxt modules, Vite plugins, Vite config, Vite environments, and Nitro routes. The deprecated `getServerData()` shim keeps its legacy `vite: { server, client }` contract independently by projecting the one captured config onto both fields. --- .../src/integrations/data-inspector.ts | 54 +++++++++---------- packages/devtools/test/data-inspector.test.ts | 48 +++++++++-------- 2 files changed, 50 insertions(+), 52 deletions(-) diff --git a/packages/devtools/src/integrations/data-inspector.ts b/packages/devtools/src/integrations/data-inspector.ts index 5f2c560d9f..6933d8c389 100644 --- a/packages/devtools/src/integrations/data-inspector.ts +++ b/packages/devtools/src/integrations/data-inspector.ts @@ -19,8 +19,7 @@ import { mountDevframe } from '@vitejs/devtools-kit/node' */ interface CapturedServerData { nitro?: Nitro - viteClient?: ResolvedConfig - viteSsr?: ResolvedConfig + vite?: ResolvedConfig } const captured: CapturedServerData = {} @@ -70,18 +69,14 @@ export function setup(ctx: NuxtDevtoolsServerContext): void { captured.nitro = nitro }) - // Capture the raw resolved Vite config for each environment. Nuxt fires - // `vite:configResolved` once with `isClient` and once with `isServer`, for - // both serial Vite servers and Environment API projections, so the source - // contract stays exactly `vite: { client, ssr }`. Nuxt marks this hook - // deprecated, but it is the only current host API that reports both configs - // semantically; the returned-environment-plugin path is unreliable in Vite 8 - // (Vite resolves those plugins after top-level `configResolved`). - nuxt.hook('vite:configResolved', (config, env) => { - if (env.isClient) - captured.viteClient = config as unknown as ResolvedConfig - if (env.isServer) - captured.viteSsr = config as unknown as ResolvedConfig + // Capture the raw resolved Vite config. Nuxt 5 unified the former client and + // SSR Vite servers into a single dev server instance, so there is exactly one + // resolved config to capture; the per-environment (client/ssr) configs live + // under `config.environments` and are surfaced by the `Vite environments` + // query. Nuxt marks this hook deprecated, but it is the current host API that + // reports the resolved config semantically. + nuxt.hook('vite:configResolved', (config) => { + captured.vite = config as unknown as ResolvedConfig }) // Register the single live source early with a non-static factory, so Nuxt @@ -97,16 +92,15 @@ export function setup(ctx: NuxtDevtoolsServerContext): void { data: () => ({ nuxt: nuxt.options, nitro: captured.nitro?.options, - vite: { - client: captured.viteClient, - ssr: captured.viteSsr, - }, + vite: captured.vite, }), queries: [ { title: 'Overview', query: '', excludeFunctions: true }, - { title: 'Nuxt options', query: 'nuxt', excludeFunctions: true }, - { title: 'Nitro options', query: 'nitro', excludeFunctions: true }, - { title: 'Vite configs', query: 'vite', excludeFunctions: true }, + { title: 'Nuxt modules', query: 'nuxt.modules', excludeFunctions: true }, + { title: 'Vite plugins', query: 'vite.plugins', excludeFunctions: true }, + { title: 'Vite config', query: 'vite', excludeFunctions: true }, + { title: 'Vite environments', query: 'vite.environments', excludeFunctions: true }, + { title: 'Nitro routes', query: 'nitro.handlers', excludeFunctions: true }, ], }) @@ -114,8 +108,7 @@ export function setup(ctx: NuxtDevtoolsServerContext): void { nuxt.hook('close', () => { unregister() captured.nitro = undefined - captured.viteClient = undefined - captured.viteSsr = undefined + captured.vite = undefined }) // Mount the Data Inspector's bundled SPA as a member of the Nuxt group. The @@ -141,8 +134,12 @@ export function setup(ctx: NuxtDevtoolsServerContext): void { * use; it will be removed in a future major. * * Unlike the live source, this returns the legacy `NuxtServerData` shape - * (`vite: { server, client }`) with the Vite configs normalized for RPC - * transport. + * (`vite: { server, client }`) with the Vite config normalized for RPC + * transport. Nuxt 5 unified the former client and SSR Vite servers into a + * single instance, but the legacy shape predates that; this shim keeps the + * old contract alive independently by projecting the one captured config onto + * both `server` and `client` (each normalized separately, so consumers that + * mutate one field don't affect the other). */ export function getServerData(nuxt: Nuxt): NuxtServerData { deprecate(nuxt, 'NDT_DEP_0009', { @@ -153,8 +150,8 @@ export function getServerData(nuxt: Nuxt): NuxtServerData { nuxt: nuxt.options, nitro: captured.nitro?.options, vite: { - server: captured.viteSsr ? normalizeViteConfig(captured.viteSsr) : undefined, - client: captured.viteClient ? normalizeViteConfig(captured.viteClient) : undefined, + server: captured.vite ? normalizeViteConfig(captured.vite) : undefined, + client: captured.vite ? normalizeViteConfig(captured.vite) : undefined, }, // `nuxt.options` is `@nuxt/schema`'s `NuxtOptions` while `NuxtServerData` // pins the structurally-identical `nuxt/schema` one; bridge the two. @@ -166,6 +163,5 @@ export function getServerData(nuxt: Nuxt): NuxtServerData { */ export function resetCapturedServerData(): void { captured.nitro = undefined - captured.viteClient = undefined - captured.viteSsr = undefined + captured.vite = undefined } diff --git a/packages/devtools/test/data-inspector.test.ts b/packages/devtools/test/data-inspector.test.ts index b731cd368d..130e5b2278 100644 --- a/packages/devtools/test/data-inspector.test.ts +++ b/packages/devtools/test/data-inspector.test.ts @@ -50,12 +50,12 @@ function fakeViteConfig(overrides: Partial = {}): ResolvedConfig } as unknown as ResolvedConfig } -/** Drive Nuxt's `vite:configResolved` hook for one environment. */ -async function captureViteEnv(fake: FakeNuxt, envName: 'client' | 'ssr', config: ResolvedConfig) { - await fake.callHook('vite:configResolved', config, { - isClient: envName === 'client', - isServer: envName === 'ssr', - }) +/** + * Drive Nuxt's `vite:configResolved` hook. Nuxt 5 unified the client and SSR + * Vite servers into a single instance, so this fires once with one config. + */ +async function captureVite(fake: FakeNuxt, config: ResolvedConfig) { + await fake.callHook('vite:configResolved', config) } beforeEach(() => { @@ -77,16 +77,18 @@ describe('data-inspector source registration', () => { expect(typeof entry!.data).toBe('function') }) - it('exposes the four read-only suggested queries with function exclusion', () => { + it('exposes the read-only suggested queries with function exclusion', () => { const { nuxt } = fakeNuxt() setup({ nuxt } as any) const queries = getDataSource('nuxt:application')!.queries! expect(queries).toEqual([ { title: 'Overview', query: '', excludeFunctions: true }, - { title: 'Nuxt options', query: 'nuxt', excludeFunctions: true }, - { title: 'Nitro options', query: 'nitro', excludeFunctions: true }, - { title: 'Vite configs', query: 'vite', excludeFunctions: true }, + { title: 'Nuxt modules', query: 'nuxt.modules', excludeFunctions: true }, + { title: 'Vite plugins', query: 'vite.plugins', excludeFunctions: true }, + { title: 'Vite config', query: 'vite', excludeFunctions: true }, + { title: 'Vite environments', query: 'vite.environments', excludeFunctions: true }, + { title: 'Nitro routes', query: 'nitro.handlers', excludeFunctions: true }, ]) }) @@ -98,26 +100,23 @@ describe('data-inspector source registration', () => { const data = await resolveSourceData(getDataSource('nuxt:application')!) as any expect(data.nuxt).toBe(options) expect(data.nitro).toBeUndefined() - expect(data.vite).toEqual({ client: undefined, ssr: undefined }) + expect(data.vite).toBeUndefined() }) - it('populates Nitro and raw Vite configs after the hooks fire', async () => { + it('populates Nitro and the raw unified Vite config after the hooks fire', async () => { const fake = fakeNuxt() setup({ nuxt: fake.nuxt } as any) const nitro = { options: { preset: 'node' } } await fake.callHook('nitro:build:before', nitro) - const client = fakeViteConfig({ marker: 'client' } as any) - const ssr = fakeViteConfig({ marker: 'ssr' } as any) - await captureViteEnv(fake, 'client', client) - await captureViteEnv(fake, 'ssr', ssr) + const vite = fakeViteConfig({ marker: 'vite' } as any) + await captureVite(fake, vite) const data = await resolveSourceData(getDataSource('nuxt:application')!) as any expect(data.nitro).toBe(nitro.options) - // The live source hands the RAW config objects to the Data Inspector engine. - expect(data.vite.client).toBe(client) - expect(data.vite.ssr).toBe(ssr) + // The live source hands the RAW config object to the Data Inspector engine. + expect(data.vite).toBe(vite) }) it('unregisters the source on the Nuxt `close` hook', async () => { @@ -131,15 +130,14 @@ describe('data-inspector source registration', () => { }) describe('getServerData deprecated shim', () => { - it('returns the legacy `vite: { server, client }` shape with normalized configs', async () => { + it('projects the unified config onto the legacy `vite: { server, client }` shape, normalized', async () => { const options = { appId: 'app' } const fake = fakeNuxt(options) setup({ nuxt: fake.nuxt } as any) const nitro = { options: { preset: 'node' } } await fake.callHook('nitro:build:before', nitro) - await captureViteEnv(fake, 'client', fakeViteConfig()) - await captureViteEnv(fake, 'ssr', fakeViteConfig()) + await captureVite(fake, fakeViteConfig()) const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) const data = getServerData(fake.nuxt) @@ -147,12 +145,16 @@ describe('getServerData deprecated shim', () => { expect(data.nuxt).toBe(options) expect(data.nitro).toBe(nitro.options) - // Legacy field naming: `ssr` capture maps to `vite.server`. + // Nuxt 5 has one Vite instance; the shim projects it onto both fields. expect(data.vite).toHaveProperty('server') expect(data.vite).toHaveProperty('client') + // Each field is normalized independently (separate object identities). + expect(data.vite.server).not.toBe(data.vite.client) // Normalization strips the live Vite branches for RPC transport. expect(data.vite.server!.inlineConfig).toBeNull() expect((data.vite.server!.plugins[0] as any).api).toBeUndefined() + expect(data.vite.client!.inlineConfig).toBeNull() + expect((data.vite.client!.plugins[0] as any).api).toBeUndefined() }) it('emits the NDT_DEP_0009 deprecation once per process', () => { From 9bc781bbe15d7c058093890faf308822f1f9c5b3 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Tue, 28 Jul 2026 05:42:54 +0000 Subject: [PATCH 2/2] refactor(devtools): surface the Vite dev server instance, not just its config The Data Inspector's `vite` object is now the live Vite dev server instance (captured from `vite:serverCreated`), which carries the runtime environments, module graph, and websocket in addition to the resolved config at `vite.config`. Suggested queries and the deprecated `getServerData()` shim read the config through `vite.config` accordingly. --- .../src/integrations/data-inspector.ts | 40 +++++++++--------- packages/devtools/test/data-inspector.test.ts | 42 ++++++++++++------- 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/packages/devtools/src/integrations/data-inspector.ts b/packages/devtools/src/integrations/data-inspector.ts index 6933d8c389..0bcdc80364 100644 --- a/packages/devtools/src/integrations/data-inspector.ts +++ b/packages/devtools/src/integrations/data-inspector.ts @@ -1,6 +1,6 @@ import type { Nitro } from 'nitropack' import type { Nuxt } from 'nuxt/schema' -import type { ResolvedConfig } from 'vite' +import type { ResolvedConfig, ViteDevServer } from 'vite' import type { NuxtDevtoolsServerContext, NuxtServerData } from '../types' import { createDataInspectorDevframe, registerDataSource } from '@devframes/plugin-data-inspector' import { deprecate, NUXT_DEVTOOLS_GROUP_ID, onDevtoolsReady } from '@nuxt/devtools-kit' @@ -19,7 +19,7 @@ import { mountDevframe } from '@vitejs/devtools-kit/node' */ interface CapturedServerData { nitro?: Nitro - vite?: ResolvedConfig + vite?: ViteDevServer } const captured: CapturedServerData = {} @@ -69,14 +69,14 @@ export function setup(ctx: NuxtDevtoolsServerContext): void { captured.nitro = nitro }) - // Capture the raw resolved Vite config. Nuxt 5 unified the former client and - // SSR Vite servers into a single dev server instance, so there is exactly one - // resolved config to capture; the per-environment (client/ssr) configs live - // under `config.environments` and are surfaced by the `Vite environments` - // query. Nuxt marks this hook deprecated, but it is the current host API that - // reports the resolved config semantically. - nuxt.hook('vite:configResolved', (config) => { - captured.vite = config as unknown as ResolvedConfig + // Capture the live Vite dev server instance. Nuxt 5 unified the former client + // and SSR Vite servers into a single dev server, so there is exactly one + // instance to capture. The server carries far more than the resolved config + // (which lives at `server.config`): its runtime `environments` (client/ssr), + // module graph, plugin containers, watcher, and websocket. The Data Inspector + // engine handles the deep/circular/function-valued branches. + nuxt.hook('vite:serverCreated', (server) => { + captured.vite = server as unknown as ViteDevServer }) // Register the single live source early with a non-static factory, so Nuxt @@ -97,8 +97,8 @@ export function setup(ctx: NuxtDevtoolsServerContext): void { queries: [ { title: 'Overview', query: '', excludeFunctions: true }, { title: 'Nuxt modules', query: 'nuxt.modules', excludeFunctions: true }, - { title: 'Vite plugins', query: 'vite.plugins', excludeFunctions: true }, - { title: 'Vite config', query: 'vite', excludeFunctions: true }, + { title: 'Vite plugins', query: 'vite.config.plugins', excludeFunctions: true }, + { title: 'Vite config', query: 'vite.config', excludeFunctions: true }, { title: 'Vite environments', query: 'vite.environments', excludeFunctions: true }, { title: 'Nitro routes', query: 'nitro.handlers', excludeFunctions: true }, ], @@ -134,24 +134,26 @@ export function setup(ctx: NuxtDevtoolsServerContext): void { * use; it will be removed in a future major. * * Unlike the live source, this returns the legacy `NuxtServerData` shape - * (`vite: { server, client }`) with the Vite config normalized for RPC + * (`vite: { server, client }`) with the resolved Vite config normalized for RPC * transport. Nuxt 5 unified the former client and SSR Vite servers into a - * single instance, but the legacy shape predates that; this shim keeps the - * old contract alive independently by projecting the one captured config onto - * both `server` and `client` (each normalized separately, so consumers that - * mutate one field don't affect the other). + * single dev server instance, but the legacy shape predates that; this shim + * keeps the old contract alive independently by reading the one dev server's + * resolved config (`server.config`) and projecting it onto both `server` and + * `client` (each normalized separately, so consumers that mutate one field + * don't affect the other). */ export function getServerData(nuxt: Nuxt): NuxtServerData { deprecate(nuxt, 'NDT_DEP_0009', { api: 'getServerData()', replacement: 'the Nuxt Application source in the Data Inspector panel', }) + const config = captured.vite?.config return { nuxt: nuxt.options, nitro: captured.nitro?.options, vite: { - server: captured.vite ? normalizeViteConfig(captured.vite) : undefined, - client: captured.vite ? normalizeViteConfig(captured.vite) : undefined, + server: config ? normalizeViteConfig(config) : undefined, + client: config ? normalizeViteConfig(config) : undefined, }, // `nuxt.options` is `@nuxt/schema`'s `NuxtOptions` while `NuxtServerData` // pins the structurally-identical `nuxt/schema` one; bridge the two. diff --git a/packages/devtools/test/data-inspector.test.ts b/packages/devtools/test/data-inspector.test.ts index 130e5b2278..71ab643d78 100644 --- a/packages/devtools/test/data-inspector.test.ts +++ b/packages/devtools/test/data-inspector.test.ts @@ -1,5 +1,5 @@ import type { Nuxt } from 'nuxt/schema' -import type { ResolvedConfig } from 'vite' +import type { ResolvedConfig, ViteDevServer } from 'vite' import { getDataSource, resetDataSources, @@ -51,11 +51,22 @@ function fakeViteConfig(overrides: Partial = {}): ResolvedConfig } /** - * Drive Nuxt's `vite:configResolved` hook. Nuxt 5 unified the client and SSR - * Vite servers into a single instance, so this fires once with one config. + * Minimal Vite dev server stub. The resolved config lives at `.config`, and the + * runtime environments at `.environments`, mirroring `ViteDevServer`. */ -async function captureVite(fake: FakeNuxt, config: ResolvedConfig) { - await fake.callHook('vite:configResolved', config) +function fakeViteServer(config: ResolvedConfig = fakeViteConfig()): ViteDevServer { + return { + config, + environments: { client: { name: 'client' }, ssr: { name: 'ssr' } }, + } as unknown as ViteDevServer +} + +/** + * Drive Nuxt's `vite:serverCreated` hook. Nuxt 5 unified the client and SSR + * Vite servers into a single instance, so this fires once with one dev server. + */ +async function captureVite(fake: FakeNuxt, server: ViteDevServer) { + await fake.callHook('vite:serverCreated', server) } beforeEach(() => { @@ -85,8 +96,8 @@ describe('data-inspector source registration', () => { expect(queries).toEqual([ { title: 'Overview', query: '', excludeFunctions: true }, { title: 'Nuxt modules', query: 'nuxt.modules', excludeFunctions: true }, - { title: 'Vite plugins', query: 'vite.plugins', excludeFunctions: true }, - { title: 'Vite config', query: 'vite', excludeFunctions: true }, + { title: 'Vite plugins', query: 'vite.config.plugins', excludeFunctions: true }, + { title: 'Vite config', query: 'vite.config', excludeFunctions: true }, { title: 'Vite environments', query: 'vite.environments', excludeFunctions: true }, { title: 'Nitro routes', query: 'nitro.handlers', excludeFunctions: true }, ]) @@ -103,20 +114,22 @@ describe('data-inspector source registration', () => { expect(data.vite).toBeUndefined() }) - it('populates Nitro and the raw unified Vite config after the hooks fire', async () => { + it('populates Nitro and the raw unified Vite dev server after the hooks fire', async () => { const fake = fakeNuxt() setup({ nuxt: fake.nuxt } as any) const nitro = { options: { preset: 'node' } } await fake.callHook('nitro:build:before', nitro) - const vite = fakeViteConfig({ marker: 'vite' } as any) - await captureVite(fake, vite) + const server = fakeViteServer(fakeViteConfig({ marker: 'vite' } as any)) + await captureVite(fake, server) const data = await resolveSourceData(getDataSource('nuxt:application')!) as any expect(data.nitro).toBe(nitro.options) - // The live source hands the RAW config object to the Data Inspector engine. - expect(data.vite).toBe(vite) + // The live source hands the RAW dev server instance to the Data Inspector + // engine; the resolved config is reachable at `vite.config`. + expect(data.vite).toBe(server) + expect(data.vite.config).toBe(server.config) }) it('unregisters the source on the Nuxt `close` hook', async () => { @@ -137,7 +150,7 @@ describe('getServerData deprecated shim', () => { const nitro = { options: { preset: 'node' } } await fake.callHook('nitro:build:before', nitro) - await captureVite(fake, fakeViteConfig()) + await captureVite(fake, fakeViteServer()) const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) const data = getServerData(fake.nuxt) @@ -145,7 +158,8 @@ describe('getServerData deprecated shim', () => { expect(data.nuxt).toBe(options) expect(data.nitro).toBe(nitro.options) - // Nuxt 5 has one Vite instance; the shim projects it onto both fields. + // Nuxt 5 has one Vite dev server; the shim reads its resolved config + // (`server.config`) and projects it onto both fields. expect(data.vite).toHaveProperty('server') expect(data.vite).toHaveProperty('client') // Each field is normalized independently (separate object identities).