diff --git a/packages/core/src/client/inject/runtime.test.ts b/packages/core/src/client/inject/runtime.test.ts
index d653aaf5..11599e51 100644
--- a/packages/core/src/client/inject/runtime.test.ts
+++ b/packages/core/src/client/inject/runtime.test.ts
@@ -1,10 +1,12 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-import { startDevTools } from './runtime'
+import { nextTick, ref } from 'vue'
+import { startDevTools, useLocalStorageSharedState } from './runtime'
const mocks = vi.hoisted(() => ({
getDevToolsRpcClient: vi.fn(
(_options: { baseURL: string[] }) => new Promise(() => {}),
),
+ useLocalStorage: vi.fn(),
}))
vi.mock('@vitejs/devtools-kit/client', () => ({
@@ -13,7 +15,7 @@ vi.mock('@vitejs/devtools-kit/client', () => ({
}))
vi.mock('@vueuse/core', () => ({
- useLocalStorage: vi.fn(),
+ useLocalStorage: mocks.useLocalStorage,
}))
vi.mock('../webcomponents/state/context', () => ({
@@ -51,3 +53,49 @@ describe('injected DevTools runtime', () => {
)
})
})
+
+describe('useLocalStorageSharedState', () => {
+ afterEach(() => {
+ mocks.useLocalStorage.mockReset()
+ })
+
+ it('returns the exact ref useLocalStorage produces, untouched', () => {
+ const state = ref({ open: false })
+ mocks.useLocalStorage.mockReturnValue(state)
+ const rpc = { sharedState: { get: vi.fn(() => new Promise(() => {})) } } as any
+
+ expect(useLocalStorageSharedState(rpc, 'k', { open: false })).toBe(state)
+ })
+
+ it('creates the shared-state slot under the same key, seeded from the current local value', () => {
+ const state = ref({ open: true, mode: 'float' })
+ mocks.useLocalStorage.mockReturnValue(state)
+ const get = vi.fn(() => new Promise(() => {}))
+ const rpc = { sharedState: { get } } as any
+
+ useLocalStorageSharedState(rpc, 'vite-devtools-dock-state', { open: false, mode: 'float' })
+
+ expect(get).toHaveBeenCalledWith('vite-devtools-dock-state', { initialValue: state.value })
+ })
+
+ it('mirrors every local change into the shared state once the slot resolves', async () => {
+ const state = ref<{ open: boolean }>({ open: false })
+ mocks.useLocalStorage.mockReturnValue(state)
+ const mutate = vi.fn()
+ const sharedStatePromise = Promise.resolve({ mutate })
+ const rpc = { sharedState: { get: vi.fn(() => sharedStatePromise) } } as any
+
+ useLocalStorageSharedState(rpc, 'k', { open: false })
+ await sharedStatePromise // by then, the watchEffect this attaches has already run once, synchronously
+
+ expect(mutate).toHaveBeenCalledTimes(1)
+ expect(mutate.mock.calls[0]![0]()).toStrictEqual({ open: false })
+
+ mutate.mockClear()
+ state.value = { open: true }
+ await nextTick() // let the watchEffect's reactive dependency flush
+
+ expect(mutate).toHaveBeenCalledTimes(1)
+ expect(mutate.mock.calls[0]![0]()).toStrictEqual({ open: true })
+ })
+})
diff --git a/packages/core/src/client/inject/runtime.ts b/packages/core/src/client/inject/runtime.ts
index 0a5d2c52..23c7ac71 100644
--- a/packages/core/src/client/inject/runtime.ts
+++ b/packages/core/src/client/inject/runtime.ts
@@ -1,13 +1,39 @@
///
///
-import type { DockPanelStorage } from '@vitejs/devtools-kit/client'
+import type { DevToolsRpcClient, DockPanelStorage } from '@vitejs/devtools-kit/client'
+import type { UseStorageOptions } from '@vueuse/core'
import { CLIENT_CONTEXT_KEY, getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
import { DEVTOOLS_MOUNT_PATH } from '@vitejs/devtools-kit/constants'
import { useLocalStorage } from '@vueuse/core'
+import { watchEffect } from 'vue'
import { DEVTOOLS_HIDE_EVENT, DEVTOOLS_MODE_FILENAME } from '../../constants'
import { createDocksContext } from '../webcomponents/state/context'
+/**
+ * `useLocalStorage`, plus mirroring the whole value into shared state under
+ * the same key so a Node-side plugin can observe it too — a plain
+ * `useLocalStorage` ref never reaches the server on its own. Fire-and-forget:
+ * the local ref (returned unchanged) stays the source of truth for every
+ * existing reader/writer.
+ */
+export function useLocalStorageSharedState(
+ rpc: DevToolsRpcClient,
+ key: string,
+ initialValue: T,
+ options?: UseStorageOptions,
+) {
+ const state = useLocalStorage(key, initialValue, options)
+ void rpc.sharedState.get(key, { initialValue: state.value }).then((shared) => {
+ watchEffect(() => {
+ /** Read synchronously so this effect tracks `state.value` as its dependency — capturing it inside `mutate`'s lazy recipe wouldn't. */
+ const snapshot = { ...state.value }
+ shared.mutate(() => snapshot)
+ })
+ })
+ return state
+}
+
export type InjectMode = 'passive' | 'normal' | 'hidden'
// Persistence endpoint the node middleware serves next to `__connection.json`.
@@ -60,7 +86,8 @@ async function mountDock(): Promise {
],
})
- const state = useLocalStorage(
+ const state = useLocalStorageSharedState(
+ rpc,
'vite-devtools-dock-state',
{
mode: 'float',