From 355a0e607ebc3bb674fc82c0c9b9e031421bc495 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Tue, 14 Jul 2026 12:11:07 +0000 Subject: [PATCH 1/4] fix(devtools): force-register client RPC functions and deprecate extendClientRpc The legacy client RPC proxy registered its event functions (refresh, callHook, onTerminalData, onTerminalExit, navigateTo) via a has()->update()/register() dance that could throw DF0022 ("not registered"). Force-register instead so it is an idempotent override, fixing the errors and making registration robust across (re)connect and integration overrides. `extendClientRpc` now overrides by default (via the forced upsert) and is deprecated in favour of registering on the devframe client context (`getDevToolsRpcClient()` / `getDevToolsClientContext()` from `@vitejs/devtools-kit/client`). Created with the help of an agent. --- packages/devtools-kit/src/_types/client-api.ts | 5 +++++ packages/devtools/client/composables/client.ts | 18 ++++++++++++++++++ packages/devtools/client/composables/rpc.ts | 11 +++++------ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/devtools-kit/src/_types/client-api.ts b/packages/devtools-kit/src/_types/client-api.ts index 9ac2b14e04..75bafcd9f3 100644 --- a/packages/devtools-kit/src/_types/client-api.ts +++ b/packages/devtools-kit/src/_types/client-api.ts @@ -118,6 +118,11 @@ export interface NuxtDevtoolsClient { renderMarkdown: (markdown: string) => string colorMode: string + /** + * @deprecated Register client RPC functions on the devframe client context + * instead, via `getDevToolsRpcClient()` / `getDevToolsClientContext()` from + * `@vitejs/devtools-kit/client`. + */ extendClientRpc: , ClientFunctions extends object = Record>(name: string, functions: ClientFunctions) => ServerFunctions } diff --git a/packages/devtools/client/composables/client.ts b/packages/devtools/client/composables/client.ts index 85b932288a..dfaab39f30 100644 --- a/packages/devtools/client/composables/client.ts +++ b/packages/devtools/client/composables/client.ts @@ -10,6 +10,22 @@ import { renderMarkdown } from './client-services/markdown' import { renderCodeHighlight } from './client-services/shiki' import { connectPromise, rpc, rpcClient, upsertClientFunction } from './rpc' +let warnedExtendClientRpc = false +/** + * @deprecated `extendClientRpc` is deprecated. Register client RPC functions on + * the devframe client context instead, via `getDevToolsRpcClient()` / + * `getDevToolsClientContext()` from `@vitejs/devtools-kit/client`. + */ +function warnExtendClientRpcDeprecated() { + if (warnedExtendClientRpc) + return + warnedExtendClientRpc = true + console.warn( + '[nuxt-devtools] `extendClientRpc` is deprecated. Register client RPC functions on the ' + + 'devframe client context instead (`getDevToolsRpcClient()` from `@vitejs/devtools-kit/client`).', + ) +} + export function useClient() { return useState('devtools-client') } @@ -60,9 +76,11 @@ export function useInjectionClient(): ComputedRef { return renderMarkdown(code) }, extendClientRpc(namespace, functions) { + warnExtendClientRpcDeprecated() const register = (client: DevToolsRpcClient) => { for (const [name, handler] of Object.entries(functions)) { if (typeof handler === 'function') + // force-registers (override by default) via upsertClientFunction upsertClientFunction(client, `${namespace}:${name}`, handler as any) } } diff --git a/packages/devtools/client/composables/rpc.ts b/packages/devtools/client/composables/rpc.ts index 5e58ef3898..27a6308533 100644 --- a/packages/devtools/client/composables/rpc.ts +++ b/packages/devtools/client/composables/rpc.ts @@ -77,13 +77,12 @@ export async function registerClientFunctions() { * Register (or replace) a client-side event function on the devframe RPC host. * * devframe 0.6 split registration into `register()` (throws DF0021 if the name - * already exists) and `update()` (throws DF0022 if it does not), so pick the - * right one based on the current definitions to keep this an idempotent upsert. + * already exists) and `update()` (throws DF0022 if it does not). We always + * force-register so this stays an idempotent override regardless of the current + * state — the client re-runs registration on (re)connect and integrations may + * override a function, so neither error should ever surface. */ export function upsertClientFunction(client: DevToolsRpcClient, name: string, handler: (...args: any[]) => any) { const definition = { name, type: 'event', handler } as const - if (client.client.definitions.has(name)) - client.client.update(definition as any) - else - client.client.register(definition as any) + ;(client.client.register as (fn: any, force?: boolean) => void)(definition, true) } From 93f4d58a2434b611f3899c55e81f4176c3e040f0 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Wed, 15 Jul 2026 00:26:26 +0000 Subject: [PATCH 2/4] feat(devtools): expose the Vite DevTools client on NuxtDevtoolsClient Mirror the server-side `nuxt.devtools.devtoolsKit` on the client: the injected `client.devtools` now carries `devtoolsKit`, the connected Vite DevTools RPC client, giving module authors full devframe-native access (register client RPC functions, call server functions, shared state, streaming, scoping) without importing from `@vitejs/devtools-kit/client` themselves. `extendClientRpc`'s deprecation now points at `client.devtools.devtoolsKit?.client.register(...)`. Created with the help of an agent. --- docs/content/2.module/1.utils-kit.md | 18 ++++++++++++++++++ packages/devtools-kit/src/_types/client-api.ts | 16 +++++++++++++--- packages/devtools/client/composables/client.ts | 7 ++++--- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/docs/content/2.module/1.utils-kit.md b/docs/content/2.module/1.utils-kit.md index d84ce740c3..aa78f8a34b 100644 --- a/docs/content/2.module/1.utils-kit.md +++ b/docs/content/2.module/1.utils-kit.md @@ -250,6 +250,24 @@ For example, you can get the router instance from the client app: const router = computed(() => devtoolsClient.value?.host?.nuxt.vueApp.config.globalProperties?.$router) ``` +The `devtools` object also exposes `devtoolsKit` — the connected Vite DevTools +RPC client (mirroring `nuxt.devtools.devtoolsKit` on the server) — for full +devframe-native access. Use it to register client RPC functions, call server +functions, and more: + +```ts +const kit = devtoolsClient.value?.devtools.devtoolsKit +kit?.client.register({ + name: 'my-module:on-update', + type: 'event', + handler: (payload) => { + // ... + }, +}) +``` + +This replaces the deprecated `devtools.extendClientRpc()`. + ### `onDevtoolsClientConnected()` Similiar to `useDevtoolsClient()` but as a callback style: diff --git a/packages/devtools-kit/src/_types/client-api.ts b/packages/devtools-kit/src/_types/client-api.ts index 75bafcd9f3..8f7486a0fd 100644 --- a/packages/devtools-kit/src/_types/client-api.ts +++ b/packages/devtools-kit/src/_types/client-api.ts @@ -1,4 +1,5 @@ import type {} from '@nuxt/schema' +import type { DevToolsRpcClient } from '@vitejs/devtools-kit/client' import type { Hookable } from 'hookable' import type { NuxtApp } from 'nuxt/app' import type { AppConfig } from 'nuxt/schema' @@ -119,9 +120,18 @@ export interface NuxtDevtoolsClient { colorMode: string /** - * @deprecated Register client RPC functions on the devframe client context - * instead, via `getDevToolsRpcClient()` / `getDevToolsClientContext()` from - * `@vitejs/devtools-kit/client`. + * The connected Vite DevTools RPC client, mirroring `nuxt.devtools.devtoolsKit` + * on the server. Use it for full devframe-native access — register client RPC + * functions (`devtoolsKit.client.register(...)`), call server functions + * (`devtoolsKit.call(...)`), shared state, streaming, scoping, etc. + * + * `undefined` until the DevTools client has connected. + */ + devtoolsKit: DevToolsRpcClient | undefined + + /** + * @deprecated Register client RPC functions on the Vite DevTools client + * instead: `client.devtools.devtoolsKit?.client.register({ name, type: 'event', handler })`. */ extendClientRpc: , ClientFunctions extends object = Record>(name: string, functions: ClientFunctions) => ServerFunctions } diff --git a/packages/devtools/client/composables/client.ts b/packages/devtools/client/composables/client.ts index dfaab39f30..dba15cad19 100644 --- a/packages/devtools/client/composables/client.ts +++ b/packages/devtools/client/composables/client.ts @@ -13,8 +13,8 @@ import { connectPromise, rpc, rpcClient, upsertClientFunction } from './rpc' let warnedExtendClientRpc = false /** * @deprecated `extendClientRpc` is deprecated. Register client RPC functions on - * the devframe client context instead, via `getDevToolsRpcClient()` / - * `getDevToolsClientContext()` from `@vitejs/devtools-kit/client`. + * the exposed Vite DevTools client instead: + * `client.devtools.devtoolsKit?.client.register(...)`. */ function warnExtendClientRpcDeprecated() { if (warnedExtendClientRpc) @@ -22,7 +22,7 @@ function warnExtendClientRpcDeprecated() { warnedExtendClientRpc = true console.warn( '[nuxt-devtools] `extendClientRpc` is deprecated. Register client RPC functions on the ' - + 'devframe client context instead (`getDevToolsRpcClient()` from `@vitejs/devtools-kit/client`).', + + 'exposed Vite DevTools client instead: `client.devtools.devtoolsKit?.client.register(...)`.', ) } @@ -68,6 +68,7 @@ export function useInjectionClient(): ComputedRef { host: client.value, devtools: { rpc, + devtoolsKit: rpcClient.value, colorMode: mode.value, renderCodeHighlight(code, lang) { return renderCodeHighlight(code, lang as any) From e6cf4bb0701663ad01f7a723585ec0be7b316bc3 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Wed, 15 Jul 2026 00:44:29 +0000 Subject: [PATCH 3/4] feat(devtools): add client-side onDevtoolsReady hook Mirror the server's `onDevtoolsReady` on the client: exported from `@nuxt/devtools-kit/iframe-client`, it runs once the Vite DevTools client is connected and hands back the `DevToolsRpcClient` (`client.devtools.devtoolsKit`), the recommended entry point for client-side integration (register client RPC, call server functions, shared state, streaming, ...). `extendClientRpc`'s deprecation now points at this hook. Created with the help of an agent. --- docs/content/2.module/1.utils-kit.md | 22 ++++++++++++++++ .../devtools-kit/src/_types/client-api.ts | 5 ++-- .../devtools-kit/src/runtime/iframe-client.ts | 25 +++++++++++++++++++ .../devtools/client/composables/client.ts | 9 +++---- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/docs/content/2.module/1.utils-kit.md b/docs/content/2.module/1.utils-kit.md index aa78f8a34b..b7902c9f68 100644 --- a/docs/content/2.module/1.utils-kit.md +++ b/docs/content/2.module/1.utils-kit.md @@ -233,6 +233,28 @@ export const devtoolsClient = useDevtoolsClient() When the iframe been served with the same origin (CORS limitation), devtools will automatically inject `__NUXT_DEVTOOLS__` to the iframe's window object. You can access it as a ref using `useDevtoolsClient()` utility. +### `onDevtoolsReady()` + +The client-side mirror of the server's [`onDevtoolsReady`](#ondevtoolsready). The +callback runs once the Vite DevTools client is connected and receives the +`DevToolsRpcClient` (the same object as `client.devtools.devtoolsKit`), giving +full devframe-native access. This is the recommended way to register client RPC +functions: + +```ts +import { onDevtoolsReady } from '@nuxt/devtools-kit/iframe-client' + +onDevtoolsReady((kit) => { + kit.client.register({ + name: 'my-module:on-update', + type: 'event', + handler: (payload) => { + // ... + }, + }) +}) +``` + ### `useDevtoolsClient()` It will return a ref of `NuxtDevtoolsIframeClient` object that are intially `null` and will be updated when the connection is ready. diff --git a/packages/devtools-kit/src/_types/client-api.ts b/packages/devtools-kit/src/_types/client-api.ts index 8f7486a0fd..c260cfa7f1 100644 --- a/packages/devtools-kit/src/_types/client-api.ts +++ b/packages/devtools-kit/src/_types/client-api.ts @@ -130,8 +130,9 @@ export interface NuxtDevtoolsClient { devtoolsKit: DevToolsRpcClient | undefined /** - * @deprecated Register client RPC functions on the Vite DevTools client - * instead: `client.devtools.devtoolsKit?.client.register({ name, type: 'event', handler })`. + * @deprecated Use `onDevtoolsReady((kit) => kit.client.register(...))` from + * `@nuxt/devtools-kit/iframe-client`, or the exposed `devtoolsKit` client + * directly (`client.devtools.devtoolsKit?.client.register(...)`). */ extendClientRpc: , ClientFunctions extends object = Record>(name: string, functions: ClientFunctions) => ServerFunctions } diff --git a/packages/devtools-kit/src/runtime/iframe-client.ts b/packages/devtools-kit/src/runtime/iframe-client.ts index ecc4183275..571647babf 100644 --- a/packages/devtools-kit/src/runtime/iframe-client.ts +++ b/packages/devtools-kit/src/runtime/iframe-client.ts @@ -1,3 +1,4 @@ +import type { DevToolsRpcClient } from '@vitejs/devtools-kit/client' import type { Ref } from 'vue' import type { NuxtDevtoolsIframeClient } from '../types' import { shallowRef, triggerRef } from 'vue' @@ -39,6 +40,30 @@ export function onDevtoolsClientConnected(fn: (client: NuxtDevtoolsIframeClient) } } +/** + * Run a callback once the Vite DevTools client is ready, receiving the connected + * `DevToolsRpcClient` — the client-side mirror of the server's + * `onDevtoolsReady((ctx) => …)`. + * + * This is the recommended way to do client-side DevTools integration (register + * client RPC functions, call server functions, shared state, streaming, …): + * + * ```ts + * import { onDevtoolsReady } from '@nuxt/devtools-kit/iframe-client' + * + * onDevtoolsReady((kit) => { + * kit.client.register({ name: 'my-module:on-update', type: 'event', handler }) + * }) + * ``` + */ +export function onDevtoolsReady(fn: (kit: DevToolsRpcClient) => void) { + return onDevtoolsClientConnected((client) => { + const kit = client.devtools.devtoolsKit + if (kit) + fn(kit) + }) +} + export function useDevtoolsClient() { if (!clientRef) { clientRef = shallowRef() diff --git a/packages/devtools/client/composables/client.ts b/packages/devtools/client/composables/client.ts index dba15cad19..be8a75f6be 100644 --- a/packages/devtools/client/composables/client.ts +++ b/packages/devtools/client/composables/client.ts @@ -12,17 +12,16 @@ import { connectPromise, rpc, rpcClient, upsertClientFunction } from './rpc' let warnedExtendClientRpc = false /** - * @deprecated `extendClientRpc` is deprecated. Register client RPC functions on - * the exposed Vite DevTools client instead: - * `client.devtools.devtoolsKit?.client.register(...)`. + * @deprecated `extendClientRpc` is deprecated. Use `onDevtoolsReady` from + * `@nuxt/devtools-kit/iframe-client`, or the exposed `devtoolsKit` client. */ function warnExtendClientRpcDeprecated() { if (warnedExtendClientRpc) return warnedExtendClientRpc = true console.warn( - '[nuxt-devtools] `extendClientRpc` is deprecated. Register client RPC functions on the ' - + 'exposed Vite DevTools client instead: `client.devtools.devtoolsKit?.client.register(...)`.', + '[nuxt-devtools] `extendClientRpc` is deprecated. Use `onDevtoolsReady((kit) => ' + + 'kit.client.register(...))` from `@nuxt/devtools-kit/iframe-client`.', ) } From 3c41be3c7eef17f864990bb80f35296dad917bb2 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Wed, 15 Jul 2026 00:54:57 +0000 Subject: [PATCH 4/4] docs(devtools): explain the devframe force-param type-gap cast --- packages/devtools/client/composables/rpc.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/devtools/client/composables/rpc.ts b/packages/devtools/client/composables/rpc.ts index 27a6308533..367caddda9 100644 --- a/packages/devtools/client/composables/rpc.ts +++ b/packages/devtools/client/composables/rpc.ts @@ -84,5 +84,9 @@ export async function registerClientFunctions() { */ export function upsertClientFunction(client: DevToolsRpcClient, name: string, handler: (...args: any[]) => any) { const definition = { name, type: 'event', handler } as const + // The cast is a workaround for a devframe type gap: its `RpcFunctionsCollector` + // interface (used for the client host) omits the `force` parameter that the + // concrete `RpcFunctionsCollectorBase` and the runtime actually accept. Drop + // the cast once devframe types `register(fn, force?)` on the interface. ;(client.client.register as (fn: any, force?: boolean) => void)(definition, true) }