diff --git a/docs/content/2.module/1.utils-kit.md b/docs/content/2.module/1.utils-kit.md index d84ce740c3..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. @@ -250,6 +272,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 9ac2b14e04..c260cfa7f1 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' @@ -118,6 +119,21 @@ export interface NuxtDevtoolsClient { renderMarkdown: (markdown: string) => string colorMode: string + /** + * 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 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 85b932288a..be8a75f6be 100644 --- a/packages/devtools/client/composables/client.ts +++ b/packages/devtools/client/composables/client.ts @@ -10,6 +10,21 @@ 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. 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. Use `onDevtoolsReady((kit) => ' + + 'kit.client.register(...))` from `@nuxt/devtools-kit/iframe-client`.', + ) +} + export function useClient() { return useState('devtools-client') } @@ -52,6 +67,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) @@ -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..367caddda9 100644 --- a/packages/devtools/client/composables/rpc.ts +++ b/packages/devtools/client/composables/rpc.ts @@ -77,13 +77,16 @@ 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) + // 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) }