From 0e30b942dc25f869dbbcd535e54925b5accc8610 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Wed, 18 Mar 2026 18:47:42 +0900 Subject: [PATCH 1/3] chore: migrate RPC layer to Vite DevTools Kit's RPC system Replace direct birpc usage with @vitejs/devtools-kit's RpcFunctionsHost for unified RPC transport. Maintain backward compatibility for existing Nuxt modules using extendServerRpc() through compatibility proxies. - Move server RPC registration into Vite DevTools plugin setup callback - Use RpcFunctionsHost.register() instead of createBirpcGroup - Replace client-side birpc+HMR with getDevToolsRpcClient() from devtools-kit - Add compatibility broadcast/functions proxies for seamless module integration - Remove unused WS_EVENT_NAME constant and direct birpc dependencies Co-Authored-By: Claude Haiku 4.5 --- .../devtools-kit/src/_types/client-api.ts | 7 +- .../devtools-kit/src/_types/server-ctx.ts | 24 ++- packages/devtools-kit/src/index.ts | 8 +- packages/devtools/client/composables/rpc.ts | 159 ++++++++------- packages/devtools/client/setup/client-rpc.ts | 5 +- packages/devtools/src/constant.ts | 2 - packages/devtools/src/module-main.ts | 11 +- packages/devtools/src/server-rpc/index.ts | 187 ++++++++++-------- 8 files changed, 238 insertions(+), 165 deletions(-) diff --git a/packages/devtools-kit/src/_types/client-api.ts b/packages/devtools-kit/src/_types/client-api.ts index 121696f979..d2a839e329 100644 --- a/packages/devtools-kit/src/_types/client-api.ts +++ b/packages/devtools-kit/src/_types/client-api.ts @@ -1,5 +1,4 @@ import type {} from '@nuxt/schema' -import type { BirpcReturn } from 'birpc' import type { Hookable } from 'hookable' import type { NuxtApp } from 'nuxt/app' import type { AppConfig } from 'nuxt/schema' @@ -7,7 +6,7 @@ import type { $Fetch } from 'ofetch' import type { BuiltinLanguage } from 'shiki' import type { Ref } from 'vue' import type { HookInfo, LoadingTimeMetric, PluginMetric } from './integrations' -import type { ClientFunctions, ServerFunctions } from './rpc' +import type { ServerFunctions } from './rpc' import type { TimelineMetrics } from './timeline-metrics' export interface DevToolsFrameState { @@ -107,7 +106,7 @@ export interface CodeHighlightOptions { } export interface NuxtDevtoolsClient { - rpc: BirpcReturn + rpc: ServerFunctions renderCodeHighlight: (code: string, lang?: BuiltinLanguage, options?: CodeHighlightOptions) => { code: string supported: boolean @@ -115,7 +114,7 @@ export interface NuxtDevtoolsClient { renderMarkdown: (markdown: string) => string colorMode: string - extendClientRpc: , ClientFunctions extends object = Record>(name: string, functions: ClientFunctions) => BirpcReturn + extendClientRpc: , ClientFunctions extends object = Record>(name: string, functions: ClientFunctions) => ServerFunctions } export interface NuxtDevtoolsIframeClient { diff --git a/packages/devtools-kit/src/_types/server-ctx.ts b/packages/devtools-kit/src/_types/server-ctx.ts index 8cbb2f70d9..c07acd56be 100644 --- a/packages/devtools-kit/src/_types/server-ctx.ts +++ b/packages/devtools-kit/src/_types/server-ctx.ts @@ -1,8 +1,26 @@ -import type { BirpcGroup } from 'birpc' import type { Nuxt, NuxtDebugModuleMutationRecord } from 'nuxt/schema' import type { ModuleOptions } from './options' import type { ClientFunctions, ServerFunctions } from './rpc' +/** + * Compatibility RPC interface that supports broadcast and function access. + * Backed by Vite DevTools Kit's RpcFunctionsHost internally. + */ +export interface NuxtDevtoolsRpc { + /** + * Broadcast proxy for calling client functions. + * Supports `rpc.broadcast.refresh.asEvent(event)` pattern for backward compatibility. + */ + broadcast: { + [K in keyof ClientFunctions]: ClientFunctions[K] & { asEvent: ClientFunctions[K] } + } + + /** + * Proxy for accessing server functions locally. + */ + functions: ServerFunctions +} + /** * @internal */ @@ -10,7 +28,7 @@ export interface NuxtDevtoolsServerContext { nuxt: Nuxt options: ModuleOptions - rpc: BirpcGroup + rpc: NuxtDevtoolsRpc /** * Hook to open file in editor @@ -27,7 +45,7 @@ export interface NuxtDevtoolsServerContext { */ ensureDevAuthToken: (token: string) => Promise - extendServerRpc: , ServerFunctions extends object = Record>(name: string, functions: ServerFunctions) => BirpcGroup + extendServerRpc: , ServerFunctions extends object = Record>(name: string, functions: ServerFunctions) => { broadcast: ClientFunctions } } export interface NuxtDevtoolsInfo { diff --git a/packages/devtools-kit/src/index.ts b/packages/devtools-kit/src/index.ts index 72904b2962..788f73bfeb 100644 --- a/packages/devtools-kit/src/index.ts +++ b/packages/devtools-kit/src/index.ts @@ -1,4 +1,3 @@ -import type { BirpcGroup } from 'birpc' import type { ChildProcess } from 'node:child_process' import type { Result } from 'tinyexec' import type { ModuleCustomTab, NuxtDevtoolsInfo, NuxtDevtoolsServerContext, SubprocessOptions, TerminalState } from './types' @@ -132,11 +131,16 @@ export function startSubprocess( } } +/** + * Extend server RPC with namespaced functions. + * + * Returns an object with a `broadcast` proxy for calling client functions. + */ export function extendServerRpc, ServerFunctions extends object = Record>( namespace: string, functions: ServerFunctions, nuxt = useNuxt(), -): BirpcGroup { +): { broadcast: ClientFunctions } { const ctx = _getContext(nuxt) if (!ctx) throw new Error('[Nuxt DevTools] Failed to get devtools context.') diff --git a/packages/devtools/client/composables/rpc.ts b/packages/devtools/client/composables/rpc.ts index 56f853f261..7e02d44f1c 100644 --- a/packages/devtools/client/composables/rpc.ts +++ b/packages/devtools/client/composables/rpc.ts @@ -1,98 +1,109 @@ +import type { DevToolsRpcClient } from '@vitejs/devtools-kit/client' import type { ClientFunctions, ServerFunctions } from '../../src/types' +import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client' import { useDebounce } from '@vueuse/core' -import { createBirpc } from 'birpc' -import { parse, stringify } from 'structured-clone-es' -import { tryCreateHotContext } from 'vite-hot-client' import { ref, shallowRef } from 'vue' -import { WS_EVENT_NAME } from '../../src/constant' -const LEADING_TRAILING_SLASH_RE = /^\/|\/$/g -const DEVTOOLS_CLIENT_PATH_RE = /\/__nuxt_devtools__\/client\/.*$/ - -export const wsConnecting = ref(false) +export const wsConnecting = ref(true) export const wsError = shallowRef() export const wsConnectingDebounced = useDebounce(wsConnecting, 2000) -const connectPromise = connectVite() -let onMessage: any = () => {} - export const clientFunctions = { - // will be added in app.vue + // will be added in setup/client-rpc.ts } as ClientFunctions export const extendedRpcMap = new Map() -export const rpc = createBirpc(clientFunctions, { - post: async (d) => { - (await connectPromise).send(WS_EVENT_NAME, d) - }, - on: (fn) => { - onMessage = fn - }, - serialize: stringify, - deserialize: parse, - resolver(name, fn) { - if (fn) - return fn - if (!name.includes(':')) - return - const [namespace, fnName] = name.split(':') as [string, string] - return extendedRpcMap.get(namespace)?.[fnName] - }, - onFunctionError(error, name) { - console.error(`[nuxt-devtools] RPC error on executing "${name}":`) - console.error(error) - return true - }, - onGeneralError(error) { - console.error(`[nuxt-devtools] RPC error:`) - console.error(error) - return true +let rpcClient: DevToolsRpcClient | undefined + +const connectPromise = connectDevToolsRpc() + +/** + * Proxy-based RPC object that provides backward-compatible `rpc.functionName()` interface. + * Server functions are called via Vite DevTools Kit's RPC client. + */ +export const rpc = new Proxy({} as ServerFunctions, { + get: (_, method: string) => { + return async (...args: any[]) => { + const client = rpcClient || await connectPromise + // Check extended RPC map first for namespaced functions + if (method.includes(':')) { + const [namespace, fnName] = method.split(':') as [string, string] + const extFn = extendedRpcMap.get(namespace)?.[fnName] + if (extFn) + return extFn(...args) + } + return client.call(method as any, ...args as any) + } }, - timeout: 120_000, }) -async function connectVite() { - const appConfig = window.parent?.__NUXT__?.config?.app - ?? window.parent?.useNuxtApp?.()?.payload?.config?.app // Nuxt 4 removes __NUXT__ - - let base = appConfig?.baseURL ?? '/' - const buildAssetsDir = appConfig?.buildAssetsDir?.replace(LEADING_TRAILING_SLASH_RE, '') ?? '_nuxt' - if (base && !base.endsWith('/')) - base += '/' - const current = window.location.href.replace(DEVTOOLS_CLIENT_PATH_RE, '/') - const hot = await tryCreateHotContext(undefined, [...new Set([ - `${base}${buildAssetsDir}/`, - `${base}_nuxt/`, - base, - `${current}${buildAssetsDir}/`, - `${current}_nuxt/`, - current, - ])]) +async function connectDevToolsRpc(): Promise { + try { + const client = await getDevToolsRpcClient() - if (!hot) { - wsConnecting.value = true - console.error('[Nuxt DevTools] Unable to find Vite HMR context') - throw new Error('[Nuxt DevTools] Unable to connect to devtools') - } + rpcClient = client - hot.on(WS_EVENT_NAME, (data) => { - wsConnecting.value = false - onMessage(data) - }) + // Register client functions so the server can call them + for (const [name, handler] of Object.entries(clientFunctions)) { + if (typeof handler === 'function') { + client.client.register({ + name, + type: 'event', + handler: handler as any, + }) + } + } - wsConnecting.value = true + // Register extended client RPC functions + for (const [namespace, fns] of extendedRpcMap) { + for (const [fnName, handler] of Object.entries(fns)) { + if (typeof handler === 'function') { + client.client.register({ + name: `${namespace}:${fnName}`, + type: 'event', + handler: handler as any, + }) + } + } + } - hot.on('vite:ws:connect', () => { // eslint-disable-next-line no-console - console.log('[nuxt-devtools] Connected to WebSocket') + console.log('[nuxt-devtools] Connected to Vite DevTools RPC') wsConnecting.value = false - }) - hot.on('vite:ws:disconnect', () => { - // eslint-disable-next-line no-console - console.log('[nuxt-devtools] Disconnected from WebSocket') + + return client + } + catch (e) { wsConnecting.value = true - }) + wsError.value = e + console.error('[Nuxt DevTools] Unable to connect to Vite DevTools RPC', e) + throw e + } +} - return hot +/** + * Register additional client functions after initial connection. + * Used by setup/client-rpc.ts to register functions that are set up later. + */ +export async function registerClientFunctions() { + const client = rpcClient || await connectPromise + for (const [name, handler] of Object.entries(clientFunctions)) { + if (typeof handler === 'function') { + try { + client.client.update({ + name, + type: 'event', + handler: handler as any, + }) + } + catch { + client.client.register({ + name, + type: 'event', + handler: handler as any, + }) + } + } + } } diff --git a/packages/devtools/client/setup/client-rpc.ts b/packages/devtools/client/setup/client-rpc.ts index 7483f8cbf4..05a7248b7b 100644 --- a/packages/devtools/client/setup/client-rpc.ts +++ b/packages/devtools/client/setup/client-rpc.ts @@ -2,7 +2,7 @@ import type { ClientFunctions } from '../../src/types' import { useNuxtApp, useRouter } from '#imports' import { useClient } from '../composables/client' import { devAuthToken, isDevAuthed } from '../composables/dev-auth' -import { clientFunctions, rpc } from '../composables/rpc' +import { clientFunctions, registerClientFunctions, rpc } from '../composables/rpc' import { processAnalyzeBuildInfo, processInstallingModules } from '../composables/state-subprocess' import { useDevToolsOptions } from '../composables/storage-options' import { telemetry } from '../composables/telemetry' @@ -43,6 +43,9 @@ export function setupClientRPC() { }, } satisfies ClientFunctions) + // Re-register client functions now that they're populated + registerClientFunctions() + rpc.getModuleOptions() .then((options) => { if (options.disableAuthorization) { diff --git a/packages/devtools/src/constant.ts b/packages/devtools/src/constant.ts index c7e009854b..83c21195ad 100644 --- a/packages/devtools/src/constant.ts +++ b/packages/devtools/src/constant.ts @@ -1,8 +1,6 @@ import type { ModuleOptions, NuxtDevToolsOptions } from './types' import { provider } from 'std-env' -export const WS_EVENT_NAME = 'nuxt:devtools:rpc' - const isSandboxed = provider === 'stackblitz' || provider === 'codesandbox' export const defaultOptions: ModuleOptions = { diff --git a/packages/devtools/src/module-main.ts b/packages/devtools/src/module-main.ts index ccfe102fb1..27b34fdf47 100644 --- a/packages/devtools/src/module-main.ts +++ b/packages/devtools/src/module-main.ts @@ -59,6 +59,10 @@ export async function enableModule(options: ModuleOptions, nuxt: Nuxt) { const DevTools = await import('@vitejs/devtools').then(r => r.DevTools()) addVitePlugin(DevTools) + + // Deferred: will be set when Vite DevTools plugin setup runs + let connectRpcHost: ((host: any) => void) | undefined + addVitePlugin(defineViteDevToolsPlugin({ name: 'nuxt:devtools', devtools: { @@ -70,6 +74,9 @@ export async function enableModule(options: ModuleOptions, nuxt: Nuxt) { title: 'Nuxt DevTools', url: '/__nuxt_devtools__/client/', }) + + // Connect Nuxt DevTools RPC to Vite DevTools Kit's RPC host + connectRpcHost?.(ctx.rpc) }, }, })) @@ -120,11 +127,11 @@ window.__NUXT_DEVTOOLS_TIME_METRIC__.appInit = Date.now() }) const { - vitePlugin, + connectRpcHost: _connectRpcHost, ...ctx } = setupRPC(nuxt, options) - addVitePlugin(vitePlugin) + connectRpcHost = _connectRpcHost const clientDirExists = existsSync(clientDir) diff --git a/packages/devtools/src/server-rpc/index.ts b/packages/devtools/src/server-rpc/index.ts index 5d16c3783a..dd784d0787 100644 --- a/packages/devtools/src/server-rpc/index.ts +++ b/packages/devtools/src/server-rpc/index.ts @@ -1,14 +1,9 @@ -import type { ChannelOptions } from 'birpc' +import type { RpcFunctionsHost } from '@vitejs/devtools-kit' import type { Nuxt } from 'nuxt/schema' -import type { Plugin } from 'vite' -import type { WebSocket } from 'ws' -import type { ClientFunctions, ModuleOptions, NuxtDevtoolsServerContext, ServerFunctions } from '../types' +import type { ModuleOptions, NuxtDevtoolsServerContext, ServerFunctions } from '../types' import { logger } from '@nuxt/kit' -import { createBirpcGroup } from 'birpc' import { colors } from 'consola/utils' -import { parse, stringify } from 'structured-clone-es' -import { WS_EVENT_NAME } from '../constant' import { getDevAuthToken } from '../dev-auth' import { setupAnalyzeBuildRPC } from './analyze-build' import { setupAssetsRPC } from './assets' @@ -26,53 +21,88 @@ import { setupTimelineRPC } from './timeline' export function setupRPC(nuxt: Nuxt, options: ModuleOptions) { const serverFunctions = {} as ServerFunctions - const extendedRpcMap = new Map() - const rpc = createBirpcGroup( - serverFunctions, - [], - { - resolver: (name, fn) => { - if (fn) - return fn - - if (!name.includes(':')) - return + const extendedRpcMap = new Map any>>() + let rpcHost: RpcFunctionsHost | undefined - const [namespace, fnName] = name.split(':') - return extendedRpcMap.get(namespace!)?.[fnName!] - }, - onFunctionError(error, name) { - logger.error( - colors.yellow(`[nuxt-devtools] RPC error on executing "${colors.bold(name)}":\n`) - + colors.red(error?.message || ''), - ) + function broadcast(method: string, ...args: any[]) { + if (!rpcHost) { + logger.warn(`[nuxt-devtools] RPC host not connected yet, cannot broadcast "${method}"`) + return + } + rpcHost.broadcast({ + method: method as any, + args: args as any, + event: true, + }) + } + + /** + * Compatibility broadcast proxy that supports the old birpc-style API: + * `rpc.broadcast.refresh.asEvent(event)` and `rpc.broadcast.onTerminalData.asEvent({ id, data })` + */ + function createBroadcastProxy(prefix = ''): any { + return new Proxy({}, { + get: (_, method) => { + if (typeof method !== 'string') + return + const fullMethod = prefix ? `${prefix}:${method}` : method + const fn = (...args: any[]) => broadcast(fullMethod, ...args) + fn.asEvent = (...args: any[]) => broadcast(fullMethod, ...args) + return fn }, - timeout: 120_000, + }) + } + + /** + * Compatibility proxy for `rpc.functions` that reads/writes to serverFunctions + * and also updates the RpcFunctionsHost when available. + */ + const functionsProxy = new Proxy(serverFunctions, { + set(target, prop, value) { + (target as any)[prop] = value + // Also update on RpcFunctionsHost if available + if (rpcHost && typeof prop === 'string') { + if (rpcHost.has(prop)) { + rpcHost.update({ name: prop, handler: value }) + } + else { + rpcHost.register({ name: prop, handler: value }) + } + } + return true }, - ) + }) + + const rpc = { + broadcast: createBroadcastProxy(), + functions: functionsProxy, + } function refresh(event: keyof ServerFunctions) { - rpc.broadcast.refresh.asEvent(event) + broadcast('refresh', event) } function extendServerRpc(namespace: string, functions: any): any { extendedRpcMap.set(namespace, functions) + // Register on RpcFunctionsHost if already available + if (rpcHost) { + for (const [fnName, handler] of Object.entries(functions)) { + if (typeof handler === 'function') { + rpcHost.register({ name: `${namespace}:${fnName}`, handler: handler as any }) + } + } + } + return { - broadcast: new Proxy({}, { - get: (_, key) => { - if (typeof key !== 'string') - return - return (rpc.broadcast as any)[`${namespace}:${key}`] - }, - }), + broadcast: createBroadcastProxy(namespace), } } const ctx: NuxtDevtoolsServerContext = { nuxt, options, - rpc, + rpc: rpc as any, refresh, extendServerRpc, openInEditorHooks: [], @@ -103,51 +133,54 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions) { ...setupServerDataRPC(ctx), } as ServerFunctions) - const wsClients = new Set() - - const vitePlugin: Plugin = { - name: 'nuxt:devtools:rpc', - configureServer(server) { - server.ws.on('connection', (ws) => { - wsClients.add(ws) - const channel: ChannelOptions = { - post: d => ws.send(JSON.stringify({ - type: 'custom', - event: WS_EVENT_NAME, - data: d, - })), - on: (fn) => { - ws.on('message', (e) => { - try { - const data = JSON.parse(String(e)) || {} - if (data.type === 'custom' && data.event === WS_EVENT_NAME) { - // console.log(data.data) - fn(data.data) - } - } - catch {} + /** + * Connect to Vite DevTools Kit's RPC host. + * Called from the Vite DevTools plugin setup callback. + */ + function connectRpcHost(host: RpcFunctionsHost) { + rpcHost = host + + // Register all collected server functions + for (const [name, handler] of Object.entries(serverFunctions)) { + if (typeof handler === 'function') { + try { + host.register({ + name, + handler: handler as any, + }) + } + catch (e) { + logger.warn( + colors.yellow(`[nuxt-devtools] Failed to register RPC function "${name}":\n`) + + colors.red((e as Error)?.message || ''), + ) + } + } + } + + // Register extended (namespaced) functions + for (const [namespace, fns] of extendedRpcMap) { + for (const [fnName, handler] of Object.entries(fns)) { + if (typeof handler === 'function') { + try { + host.register({ + name: `${namespace}:${fnName}`, + handler: handler as any, }) - }, - serialize: stringify, - deserialize: parse, + } + catch (e) { + logger.warn( + colors.yellow(`[nuxt-devtools] Failed to register RPC function "${namespace}:${fnName}":\n`) + + colors.red((e as Error)?.message || ''), + ) + } } - rpc.updateChannels((c) => { - c.push(channel) - }) - ws.on('close', () => { - wsClients.delete(ws) - rpc.updateChannels((c) => { - const index = c.indexOf(channel) - if (index >= 0) - c.splice(index, 1) - }) - }) - }) - }, + } + } } return { - vitePlugin, + connectRpcHost, ...ctx, } } From d435ec7a5dd09971192a24a7f37ecc613ddcebdb Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Thu, 19 Mar 2026 18:07:25 +0900 Subject: [PATCH 2/3] fix: queue pre-connection broadcasts, remove auth in favor of Vite DevTools - Queue RPC broadcasts before host connects instead of dropping them - Remove Nuxt DevTools auth system (now handled by Vite DevTools) - Keep auth APIs as deprecated noops with runtime warnings for backward compat - Fix RPC return types to use AsyncServerFunctions for correct type inference Co-Authored-By: Claude Opus 4.6 (1M context) --- .../devtools-kit/src/_types/client-api.ts | 6 +- packages/devtools-kit/src/_types/options.ts | 7 +- .../devtools-kit/src/_types/server-ctx.ts | 2 +- .../client/components/AuthConfirmDialog.vue | 16 +--- .../client/components/AuthRequiredPanel.vue | 76 +--------------- .../devtools/client/composables/dev-auth.ts | 86 +++---------------- packages/devtools/client/composables/rpc.ts | 4 +- packages/devtools/client/setup/client-rpc.ts | 11 +-- packages/devtools/src/module-main.ts | 29 +------ packages/devtools/src/server-rpc/general.ts | 43 ++-------- packages/devtools/src/server-rpc/index.ts | 18 ++-- 11 files changed, 47 insertions(+), 251 deletions(-) diff --git a/packages/devtools-kit/src/_types/client-api.ts b/packages/devtools-kit/src/_types/client-api.ts index d2a839e329..9ac2b14e04 100644 --- a/packages/devtools-kit/src/_types/client-api.ts +++ b/packages/devtools-kit/src/_types/client-api.ts @@ -105,8 +105,12 @@ export interface CodeHighlightOptions { grammarContextCode?: string } +export type AsyncServerFunctions = { + [K in keyof ServerFunctions]: (...args: Parameters) => Promise>> +} + export interface NuxtDevtoolsClient { - rpc: ServerFunctions + rpc: AsyncServerFunctions renderCodeHighlight: (code: string, lang?: BuiltinLanguage, options?: CodeHighlightOptions) => { code: string supported: boolean diff --git a/packages/devtools-kit/src/_types/options.ts b/packages/devtools-kit/src/_types/options.ts index 3e78dc757c..f637bfb7d1 100644 --- a/packages/devtools-kit/src/_types/options.ts +++ b/packages/devtools-kit/src/_types/options.ts @@ -44,12 +44,7 @@ export interface ModuleOptions { viteInspect?: boolean /** - * Disable dev time authorization check. - * - * **NOT RECOMMENDED**, only use this if you know what you are doing. - * - * @see https://github.com/nuxt/devtools/pull/257 - * @default false + * @deprecated Auth is now handled by Vite DevTools. This option is ignored. */ disableAuthorization?: boolean diff --git a/packages/devtools-kit/src/_types/server-ctx.ts b/packages/devtools-kit/src/_types/server-ctx.ts index c07acd56be..f3c194c29d 100644 --- a/packages/devtools-kit/src/_types/server-ctx.ts +++ b/packages/devtools-kit/src/_types/server-ctx.ts @@ -41,7 +41,7 @@ export interface NuxtDevtoolsServerContext { refresh: (event: keyof ServerFunctions) => void /** - * Ensure dev auth token is valid, throw if not + * @deprecated Auth is now handled by Vite DevTools. This is a noop. */ ensureDevAuthToken: (token: string) => Promise diff --git a/packages/devtools/client/components/AuthConfirmDialog.vue b/packages/devtools/client/components/AuthConfirmDialog.vue index b433b8ee4c..04d85e1bdb 100644 --- a/packages/devtools/client/components/AuthConfirmDialog.vue +++ b/packages/devtools/client/components/AuthConfirmDialog.vue @@ -1,17 +1,3 @@ - - diff --git a/packages/devtools/client/components/AuthRequiredPanel.vue b/packages/devtools/client/components/AuthRequiredPanel.vue index bbe104f43f..5535dd5e18 100644 --- a/packages/devtools/client/components/AuthRequiredPanel.vue +++ b/packages/devtools/client/components/AuthRequiredPanel.vue @@ -1,76 +1,4 @@ - - diff --git a/packages/devtools/client/composables/dev-auth.ts b/packages/devtools/client/composables/dev-auth.ts index 213454e597..44afac4ac1 100644 --- a/packages/devtools/client/composables/dev-auth.ts +++ b/packages/devtools/client/composables/dev-auth.ts @@ -1,88 +1,26 @@ -import { devtoolsUiShowNotification } from '#imports' -import { until } from '@vueuse/core' import { parseUA } from 'ua-parser-modern' import { ref } from 'vue' -import { AuthConfirm } from './dialog' -import { rpc } from './rpc' -export const devAuthToken = ref(localStorage.getItem('__nuxt_dev_token__')) +/** @deprecated Auth is now handled by Vite DevTools */ +export const devAuthToken = ref('disabled') -export const isDevAuthed = ref(false) +/** @deprecated Auth is now handled by Vite DevTools */ +export const isDevAuthed = ref(true) -const bc = new BroadcastChannel('__nuxt_dev_token__') - -bc.addEventListener('message', (e) => { - if (e.data.event === 'new-token') { - if (e.data.data === devAuthToken.value) - return - const token = e.data.data - rpc.verifyAuthToken(token) - .then((result) => { - devAuthToken.value = result ? token : null - isDevAuthed.value = result - }) - } -}) - -export function updateDevAuthToken(token: string) { - devAuthToken.value = token - isDevAuthed.value = true - localStorage.setItem('__nuxt_dev_token__', token) - bc.postMessage({ event: 'new-token', data: token }) +/** @deprecated Auth is now handled by Vite DevTools */ +export function updateDevAuthToken(_token: string) { + console.warn('[nuxt-devtools] `updateDevAuthToken` is deprecated. Auth is now handled by Vite DevTools.') } +/** @deprecated Auth is now handled by Vite DevTools */ export async function ensureDevAuthToken() { - if (isDevAuthed.value) - return devAuthToken.value! - - if (!devAuthToken.value) - await authConfirmAction() - - isDevAuthed.value = await rpc.verifyAuthToken(devAuthToken.value!) - if (!isDevAuthed.value) { - devAuthToken.value = null - devtoolsUiShowNotification({ - message: 'Invalid auth token, action canceled', - icon: 'i-carbon-warning-alt', - classes: 'text-red', - }) - await authConfirmAction() - throw new Error('[Nuxt DevTools] Invalid auth token') - } - - return devAuthToken.value! + console.warn('[nuxt-devtools] `ensureDevAuthToken` is deprecated. Auth is now handled by Vite DevTools.') + return '' } export const userAgentInfo = parseUA(navigator.userAgent) +/** @deprecated Auth is now handled by Vite DevTools */ export async function requestForAuth() { - const desc = [ - userAgentInfo.browser.name, - userAgentInfo.browser.version, - '|', - userAgentInfo.os.name, - userAgentInfo.os.version, - userAgentInfo.device.type, - ].filter(i => i).join(' ') - return await rpc.requestForAuth(desc, window.location.origin) -} - -async function authConfirmAction() { - if (!devAuthToken.value) - requestForAuth() - - const result = await Promise.race([ - AuthConfirm.start(), - until(devAuthToken.value).toBeTruthy(), - ]) - - if (result === false) { - // @unocss-include - devtoolsUiShowNotification({ - message: 'Action canceled', - icon: 'carbon-close', - classes: 'text-orange', - }) - throw new Error('[Nuxt DevTools] User canceled auth') - } + console.warn('[nuxt-devtools] `requestForAuth` is deprecated. Auth is now handled by Vite DevTools.') } diff --git a/packages/devtools/client/composables/rpc.ts b/packages/devtools/client/composables/rpc.ts index 7e02d44f1c..007e2dc73d 100644 --- a/packages/devtools/client/composables/rpc.ts +++ b/packages/devtools/client/composables/rpc.ts @@ -1,5 +1,5 @@ import type { DevToolsRpcClient } from '@vitejs/devtools-kit/client' -import type { ClientFunctions, ServerFunctions } from '../../src/types' +import type { AsyncServerFunctions, ClientFunctions } from '../../src/types' import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client' import { useDebounce } from '@vueuse/core' import { ref, shallowRef } from 'vue' @@ -22,7 +22,7 @@ const connectPromise = connectDevToolsRpc() * Proxy-based RPC object that provides backward-compatible `rpc.functionName()` interface. * Server functions are called via Vite DevTools Kit's RPC client. */ -export const rpc = new Proxy({} as ServerFunctions, { +export const rpc = new Proxy({} as AsyncServerFunctions, { get: (_, method: string) => { return async (...args: any[]) => { const client = rpcClient || await connectPromise diff --git a/packages/devtools/client/setup/client-rpc.ts b/packages/devtools/client/setup/client-rpc.ts index 05a7248b7b..dfef0e3fec 100644 --- a/packages/devtools/client/setup/client-rpc.ts +++ b/packages/devtools/client/setup/client-rpc.ts @@ -1,8 +1,7 @@ import type { ClientFunctions } from '../../src/types' import { useNuxtApp, useRouter } from '#imports' import { useClient } from '../composables/client' -import { devAuthToken, isDevAuthed } from '../composables/dev-auth' -import { clientFunctions, registerClientFunctions, rpc } from '../composables/rpc' +import { clientFunctions, registerClientFunctions } from '../composables/rpc' import { processAnalyzeBuildInfo, processInstallingModules } from '../composables/state-subprocess' import { useDevToolsOptions } from '../composables/storage-options' import { telemetry } from '../composables/telemetry' @@ -46,14 +45,6 @@ export function setupClientRPC() { // Re-register client functions now that they're populated registerClientFunctions() - rpc.getModuleOptions() - .then((options) => { - if (options.disableAuthorization) { - isDevAuthed.value = true - devAuthToken.value ||= 'disabled' - } - }) - const { hiddenTabs, pinnedTabs, diff --git a/packages/devtools/src/module-main.ts b/packages/devtools/src/module-main.ts index 27b34fdf47..3ef583effc 100644 --- a/packages/devtools/src/module-main.ts +++ b/packages/devtools/src/module-main.ts @@ -13,7 +13,6 @@ import sirv from 'sirv' import { searchForWorkspaceRoot } from 'vite' import { version } from '../package.json' import { defaultTabOptions } from './constant' -import { getDevAuthToken } from './dev-auth' import { clientDir, packageDir, runtimeDir } from './dirs' import { setupRPC } from './server-rpc' import { readLocalOptions } from './utils/local-options' @@ -39,6 +38,10 @@ export async function enableModule(options: ModuleOptions, nuxt: Nuxt) { await nuxt.callHook('devtools:before') + if (nuxt.options.devtools && typeof nuxt.options.devtools !== 'boolean' && 'disableAuthorization' in nuxt.options.devtools) { + logger.warn('[nuxt-devtools] `disableAuthorization` option is deprecated. Auth is now handled by Vite DevTools.') + } + if (options.iframeProps) { nuxt.options.runtimeConfig.app.devtools ||= {} nuxt.options.runtimeConfig.app.devtools.iframeProps = options.iframeProps @@ -158,8 +161,6 @@ window.__NUXT_DEVTOOLS_TIME_METRIC__.appInit = Date.now() const ROUTE_PATH = `${nuxt.options.app.baseURL || '/'}/__nuxt_devtools__`.replace(MULTIPLE_SLASHES_RE, '/') const ROUTE_CLIENT = `${ROUTE_PATH}/client` - const ROUTE_AUTH = `${ROUTE_PATH}/auth` - const ROUTE_AUTH_VERIFY = `${ROUTE_PATH}/auth-verify` const ROUTE_ANALYZE = `${ROUTE_PATH}/analyze` // TODO: Use WS from nitro server when possible @@ -189,28 +190,6 @@ window.__NUXT_DEVTOOLS_TIME_METRIC__.appInit = Date.now() return handleStatic(req, res, () => handleIndex(res)) }) } - server.middlewares.use(ROUTE_AUTH, sirv(join(runtimeDir, 'auth'), { single: true, dev: true })) - server.middlewares.use(ROUTE_AUTH_VERIFY, async (req, res) => { - const search = req.url?.split('?')[1] - if (!search) { - res.statusCode = 400 - res.end('No token provided') - } - const query = new URLSearchParams(search) - const token = query.get('token') - if (!token) { - res.statusCode = 400 - res.end('No token provided') - } - if (token === await getDevAuthToken()) { - res.statusCode = 200 - res.end('Valid token') - } - else { - res.statusCode = 403 - res.end('Invalid token') - } - }) }) await import('./integrations/plugin-metrics').then(({ setup }) => setup(ctx)) diff --git a/packages/devtools/src/server-rpc/general.ts b/packages/devtools/src/server-rpc/general.ts index fbe40d286f..b319d5ef37 100644 --- a/packages/devtools/src/server-rpc/general.ts +++ b/packages/devtools/src/server-rpc/general.ts @@ -5,13 +5,11 @@ import type { AutoImportsWithMetadata, HookInfo, NuxtDevtoolsServerContext, Serv import { existsSync } from 'node:fs' import fs from 'node:fs/promises' import { logger } from '@nuxt/kit' -import { colors } from 'consola/utils' import destr from 'destr' import { dirname, join, resolve } from 'pathe' import { snakeCase } from 'scule' import { resolveBuiltinPresets } from 'unimport' -import { getDevAuthToken } from '../dev-auth' import { setupHooksDebug } from '../runtime/shared/hooks' import { toJsLiteral } from '../utils/serialize-js-literal' import { getOptions } from './options' @@ -19,7 +17,6 @@ import { getOptions } from './options' const ABSOLUTE_PATH_RE = /^[a-z]:|^\//i // eslint-disable-next-line regexp/no-super-linear-backtracking const FILE_LINE_COL_RE = /^(.*?)(:[:\d]*)$/ -const MULTIPLE_SLASHES_RE = /\/+/g const NUXT_WELCOME_RE = // export function setupGeneralRPC({ @@ -270,40 +267,14 @@ export function setupGeneralRPC({ logger.info('Restarting Nuxt...') return nuxt.callHook('restart', { hard }) }, - async requestForAuth(info, origin?) { - if (options.disableAuthorization) - return - - const token = await getDevAuthToken() - - origin ||= `${nuxt.options.devServer.https ? 'https' : 'http'}://${nuxt.options.devServer.host === '::' ? 'localhost' : (nuxt.options.devServer.host || 'localhost')}:${nuxt.options.devServer.port}` - - const ROUTE_AUTH = `${nuxt.options.app.baseURL || '/'}/__nuxt_devtools__/auth`.replace(MULTIPLE_SLASHES_RE, '/') - - const message = [ - `A browser is requesting permissions of ${colors.bold(colors.yellow('writing files and running commands'))} from the DevTools UI.`, - colors.bold(info || 'Unknown'), - '', - 'Please open the following URL in the browser:', - colors.bold(colors.green(`${origin}${ROUTE_AUTH}?token=${token}`)), - '', - 'Or manually copy and paste the following token:', - colors.bold(colors.cyan(token)), - ] - - logger.box({ - message: message.join('\n'), - title: colors.bold(colors.yellow(' Permission Request ')), - style: { - borderColor: 'yellow', - borderStyle: 'rounded', - }, - }) + /** @deprecated Auth is now handled by Vite DevTools */ + async requestForAuth() { + logger.warn('[nuxt-devtools] `requestForAuth` is deprecated. Auth is now handled by Vite DevTools.') }, - async verifyAuthToken(token: string) { - if (options.disableAuthorization) - return true - return token === await getDevAuthToken() + /** @deprecated Auth is now handled by Vite DevTools */ + async verifyAuthToken() { + logger.warn('[nuxt-devtools] `verifyAuthToken` is deprecated. Auth is now handled by Vite DevTools.') + return true }, } satisfies Partial } diff --git a/packages/devtools/src/server-rpc/index.ts b/packages/devtools/src/server-rpc/index.ts index dd784d0787..db801c9da0 100644 --- a/packages/devtools/src/server-rpc/index.ts +++ b/packages/devtools/src/server-rpc/index.ts @@ -4,7 +4,6 @@ import type { Nuxt } from 'nuxt/schema' import type { ModuleOptions, NuxtDevtoolsServerContext, ServerFunctions } from '../types' import { logger } from '@nuxt/kit' import { colors } from 'consola/utils' -import { getDevAuthToken } from '../dev-auth' import { setupAnalyzeBuildRPC } from './analyze-build' import { setupAssetsRPC } from './assets' import { setupCustomTabRPC } from './custom-tabs' @@ -23,10 +22,11 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions) { const serverFunctions = {} as ServerFunctions const extendedRpcMap = new Map any>>() let rpcHost: RpcFunctionsHost | undefined + const pendingBroadcasts: { method: string, args: any[] }[] = [] function broadcast(method: string, ...args: any[]) { if (!rpcHost) { - logger.warn(`[nuxt-devtools] RPC host not connected yet, cannot broadcast "${method}"`) + pendingBroadcasts.push({ method, args }) return } rpcHost.broadcast({ @@ -106,11 +106,9 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions) { refresh, extendServerRpc, openInEditorHooks: [], - async ensureDevAuthToken(token: string) { - if (options.disableAuthorization) - return - if (token !== await getDevAuthToken()) - throw new Error('[Nuxt DevTools] Invalid dev auth token.') + /** @deprecated Auth is now handled by Vite DevTools */ + async ensureDevAuthToken(_token: string) { + logger.warn('[nuxt-devtools] `ensureDevAuthToken` is deprecated. Auth is now handled by Vite DevTools.') }, } @@ -140,6 +138,12 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions) { function connectRpcHost(host: RpcFunctionsHost) { rpcHost = host + // Flush any broadcasts that were queued before connection + for (const { method, args } of pendingBroadcasts) { + broadcast(method, ...args) + } + pendingBroadcasts.length = 0 + // Register all collected server functions for (const [name, handler] of Object.entries(serverFunctions)) { if (typeof handler === 'function') { From 7c781224e8eb58705d74032b3525c1387a14ec90 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 23 Mar 2026 11:03:52 +0900 Subject: [PATCH 3/3] fix: restore BirpcGroup-compatible return type for extendServerRpc Keep backward compatibility for modules that type the return value of extendServerRpc as BirpcGroup by providing stub properties (functions, clients, updateChannels) alongside the broadcast proxy. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/devtools-kit/src/_types/server-ctx.ts | 3 ++- packages/devtools-kit/src/index.ts | 3 ++- packages/devtools/src/server-rpc/index.ts | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/devtools-kit/src/_types/server-ctx.ts b/packages/devtools-kit/src/_types/server-ctx.ts index f3c194c29d..8088acc110 100644 --- a/packages/devtools-kit/src/_types/server-ctx.ts +++ b/packages/devtools-kit/src/_types/server-ctx.ts @@ -1,3 +1,4 @@ +import type { BirpcGroup } from 'birpc' import type { Nuxt, NuxtDebugModuleMutationRecord } from 'nuxt/schema' import type { ModuleOptions } from './options' import type { ClientFunctions, ServerFunctions } from './rpc' @@ -45,7 +46,7 @@ export interface NuxtDevtoolsServerContext { */ ensureDevAuthToken: (token: string) => Promise - extendServerRpc: , ServerFunctions extends object = Record>(name: string, functions: ServerFunctions) => { broadcast: ClientFunctions } + extendServerRpc: , ServerFunctions extends object = Record>(name: string, functions: ServerFunctions) => BirpcGroup } export interface NuxtDevtoolsInfo { diff --git a/packages/devtools-kit/src/index.ts b/packages/devtools-kit/src/index.ts index 788f73bfeb..65d2ebb0a5 100644 --- a/packages/devtools-kit/src/index.ts +++ b/packages/devtools-kit/src/index.ts @@ -1,3 +1,4 @@ +import type { BirpcGroup } from 'birpc' import type { ChildProcess } from 'node:child_process' import type { Result } from 'tinyexec' import type { ModuleCustomTab, NuxtDevtoolsInfo, NuxtDevtoolsServerContext, SubprocessOptions, TerminalState } from './types' @@ -140,7 +141,7 @@ export function extendServerRpc { const ctx = _getContext(nuxt) if (!ctx) throw new Error('[Nuxt DevTools] Failed to get devtools context.') diff --git a/packages/devtools/src/server-rpc/index.ts b/packages/devtools/src/server-rpc/index.ts index db801c9da0..97fb3eaf01 100644 --- a/packages/devtools/src/server-rpc/index.ts +++ b/packages/devtools/src/server-rpc/index.ts @@ -96,6 +96,9 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions) { return { broadcast: createBroadcastProxy(namespace), + functions, + clients: [], + updateChannels: () => [], } }