Skip to content

Commit f128ced

Browse files
committed
refactore(core): move dock to shared state
1 parent 6c645f5 commit f128ced

46 files changed

Lines changed: 434 additions & 131 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/guide/index.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,32 @@ export default defineConfig({
114114
})
115115
```
116116

117+
#### Dock defaults
118+
119+
`dock` sets host-wide dock defaults — category order, the float bar's inline
120+
capacity, and the initial window placement:
121+
122+
```ts [vite.config.ts] twoslash
123+
import { DevTools } from '@vitejs/devtools'
124+
import { defineConfig } from 'vite'
125+
126+
export default defineConfig({
127+
plugins: [
128+
DevTools({
129+
dock: {
130+
categoryOrder: { web: -60, advanced: -50, app: -40 },
131+
maxVisibleItems: 8,
132+
defaultMode: 'edge',
133+
defaultPosition: 'left',
134+
},
135+
}),
136+
],
137+
})
138+
```
139+
140+
A plugin may additionally declare its own `devtools.dock.categoryOrder` to
141+
weigh the categories it contributes — see [Dock config](/kit/devtools-plugin#dock-config).
142+
117143
#### Projects without an HTML entry
118144

119145
For apps where Vite doesn't serve the HTML (JS-only entries, backend integration, middleware mode), import the client injector from a browser entry instead. One entry per visibility mode — import whichever one you want:

‎docs/kit/devtools-plugin.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ const plugin: Plugin = {
8585
| Property | Type | Description |
8686
|----------|------|-------------|
8787
| `ctx.docks` | `DocksHost` | Register and manage [dock entries](./dock-system) |
88+
| `ctx.dockConfig` | `SharedState<DevToolsDockConfig>` | Merged dock config — see [Dock config](#dock-config) below |
8889
| `ctx.views` | `ViewsHost` | Host static files for your DevTools UI |
8990
| `ctx.rpc` | `RpcHost` | Register [RPC functions](./rpc) and broadcast to clients |
9091
| `ctx.viteConfig` | `ResolvedConfig` | The resolved Vite configuration |
@@ -93,6 +94,38 @@ const plugin: Plugin = {
9394
| `ctx.cwd` | `string` | Current working directory |
9495
| `ctx.workspaceRoot` | `string` | Workspace root directory |
9596

97+
### Dock config
98+
99+
A plugin can declare `devtools.dock.categoryOrder` alongside `setup()` to weigh
100+
the categories its own entries fall into — it shallow-merges with every other
101+
plugin's declaration:
102+
103+
```ts
104+
const plugin: Plugin = {
105+
devtools: {
106+
dock: {
107+
categoryOrder: { 'my-plugin': -40 },
108+
},
109+
setup(ctx) {
110+
// ...
111+
},
112+
},
113+
}
114+
```
115+
116+
`maxVisibleItems`, `defaultMode`, and `defaultPosition` are properties of the
117+
one shared dock bar, not of any single plugin, so they're set once by the host
118+
via `DevTools({ dock })` instead (see the [Getting Started guide](/guide/#the-devtools-plugin)).
119+
120+
The merged result is `ctx.dockConfig` — a `SharedState`, synced to every
121+
connected client, that a plugin can `mutate()` to reconfigure them live:
122+
123+
```ts
124+
ctx.dockConfig.mutate((config) => {
125+
config.maxVisibleItems = 8
126+
})
127+
```
128+
96129
### Example: accessing Vite config
97130

98131
```ts

‎packages/core/src/client/inject/runtime.test.ts‎

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import type { DevToolsDockConfig } from '@vitejs/devtools-kit'
2+
import type { DevToolsRpcClient, DockPanelStorage } from '@vitejs/devtools-kit/client'
3+
import type { Ref } from 'vue'
4+
import { createSharedState } from 'devframe/utils/shared-state'
15
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2-
import { startDevTools } from './runtime'
6+
import { ref } from 'vue'
7+
import { seedWindowDefaultsOnce, startDevTools } from './runtime'
38

49
const mocks = vi.hoisted(() => ({
510
getDevToolsRpcClient: vi.fn(
@@ -51,3 +56,79 @@ describe('injected DevTools runtime', () => {
5156
)
5257
})
5358
})
59+
60+
describe('seedWindowDefaultsOnce', () => {
61+
function mockRpc(initialValue: DevToolsDockConfig) {
62+
const state = createSharedState({ initialValue, enablePatches: false })
63+
return {
64+
sharedState: {
65+
get: async () => state,
66+
},
67+
} as unknown as DevToolsRpcClient
68+
}
69+
70+
function panelState(): Ref<DockPanelStorage> {
71+
return ref<DockPanelStorage>({
72+
mode: 'float',
73+
width: 80,
74+
height: 80,
75+
top: 0,
76+
left: 0,
77+
position: 'left',
78+
open: false,
79+
inactiveTimeout: 3_000,
80+
})
81+
}
82+
83+
it('applies an already-available default mode and position', async () => {
84+
const state = panelState()
85+
await seedWindowDefaultsOnce(mockRpc({ defaultMode: 'edge', defaultPosition: 'right' }), state)
86+
87+
expect(state.value.mode).toBe('edge')
88+
expect(state.value.position).toBe('right')
89+
})
90+
91+
it('leaves the fallback untouched when nothing is declared', async () => {
92+
const state = panelState()
93+
await seedWindowDefaultsOnce(mockRpc({}), state)
94+
95+
expect(state.value.mode).toBe('float')
96+
expect(state.value.position).toBe('left')
97+
})
98+
99+
it('applies only the declared field, leaving the other at its fallback', async () => {
100+
const state = panelState()
101+
await seedWindowDefaultsOnce(mockRpc({ defaultMode: 'edge' }), state)
102+
103+
expect(state.value.mode).toBe('edge')
104+
expect(state.value.position).toBe('left')
105+
})
106+
107+
it('applies the default once it arrives later, and only once', async () => {
108+
const sharedState = createSharedState<DevToolsDockConfig>({ initialValue: {}, enablePatches: false })
109+
const rpc = {
110+
sharedState: {
111+
get: async () => sharedState,
112+
},
113+
} as unknown as DevToolsRpcClient
114+
const state = panelState()
115+
116+
const seeded = seedWindowDefaultsOnce(rpc, state)
117+
sharedState.mutate((config) => {
118+
config.defaultMode = 'edge'
119+
config.defaultPosition = 'right'
120+
})
121+
await seeded
122+
123+
expect(state.value.mode).toBe('edge')
124+
expect(state.value.position).toBe('right')
125+
126+
// A later reconfiguration (e.g. `ctx.dockConfig.mutate()`) must not keep
127+
// repositioning a dock the developer has since moved.
128+
state.value.mode = 'float'
129+
sharedState.mutate((config) => {
130+
config.defaultMode = 'edge'
131+
})
132+
expect(state.value.mode).toBe('float')
133+
})
134+
})

‎packages/core/src/client/inject/runtime.ts‎

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
/// <reference types="vite/client" />
22
/// <reference lib="dom" />
33

4-
import type { DockPanelStorage } from '@vitejs/devtools-kit/client'
4+
import type { DevToolsDockConfig } from '@vitejs/devtools-kit'
5+
import type { DevToolsRpcClient, DockPanelStorage } from '@vitejs/devtools-kit/client'
6+
import type { Ref } from 'vue'
57
import { CLIENT_CONTEXT_KEY, getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
68
import { DEVTOOLS_MOUNT_PATH } from '@vitejs/devtools-kit/constants'
79
import { useLocalStorage } from '@vueuse/core'
810
import { DEVTOOLS_HIDE_EVENT, DEVTOOLS_MODE_FILENAME } from '../../constants'
911
import { createDocksContext } from '../webcomponents/state/context'
1012

13+
const DOCK_STATE_STORAGE_KEY = 'vite-devtools-dock-state'
14+
1115
export type InjectMode = 'passive' | 'normal' | 'hidden'
1216

1317
// Persistence endpoint the node middleware serves next to `__connection.json`.
@@ -33,6 +37,40 @@ function matchesActivation(event: KeyboardEvent): boolean {
3337
&& (event.code === 'KeyD' || event.key === 'd' || event.key === 'D')
3438
}
3539

40+
/**
41+
* Seeds `state.mode`/`.position` from the host's declared `defaultMode`/
42+
* `defaultPosition`, once, for a developer with no stored preference yet.
43+
*
44+
* The client's `sharedState.get()` resolves immediately with just
45+
* `initialValue` while the connection is still untrusted — the normal
46+
* first-load case — so the declared config usually lands after `state` (and
47+
* the dock) already exist. Applying it retroactively here, exactly once,
48+
* avoids depending on `useLocalStorage`'s `mergeDefaults`, which would have
49+
* already persisted the fallback `float`/`left` and made the seed a no-op on
50+
* every later load too.
51+
*/
52+
export async function seedWindowDefaultsOnce(rpc: DevToolsRpcClient, state: Ref<DockPanelStorage>): Promise<void> {
53+
const configState = await rpc.sharedState.get<DevToolsDockConfig>('devtools:dock-config', { initialValue: {} })
54+
55+
const applyOnce = (config: DevToolsDockConfig): boolean => {
56+
if (config.defaultMode == null && config.defaultPosition == null)
57+
return false
58+
if (config.defaultMode != null)
59+
state.value.mode = config.defaultMode
60+
if (config.defaultPosition != null)
61+
state.value.position = config.defaultPosition
62+
return true
63+
}
64+
65+
if (applyOnce(configState.value()))
66+
return
67+
68+
const unsubscribe = configState.on('updated', (config) => {
69+
if (applyOnce(config))
70+
unsubscribe()
71+
})
72+
}
73+
3674
async function persistNormalMode(enabled: boolean): Promise<void> {
3775
try {
3876
await fetch(MODE_URL, {
@@ -60,23 +98,26 @@ async function mountDock(): Promise<void> {
6098
],
6199
})
62100

63-
// Plugin-declared mode/position only seed the defaults — `mergeDefaults` never
64-
// overwrites a developer's stored preference.
65-
const dockConfig = rpc.connectionMeta.dockConfig
101+
// Read before `useLocalStorage` creates/touches the entry below — the write
102+
// it does on every load would otherwise erase the "nothing stored yet"
103+
// signal `seedWindowDefaultsOnce` needs.
104+
const hasStoredDockState = localStorage.getItem(DOCK_STATE_STORAGE_KEY) !== null
66105
const state = useLocalStorage<DockPanelStorage>(
67-
'vite-devtools-dock-state',
106+
DOCK_STATE_STORAGE_KEY,
68107
{
69-
mode: dockConfig?.defaultMode ?? 'float',
108+
mode: 'float',
70109
width: 80,
71110
height: 80,
72111
top: 0,
73112
left: 0,
74-
position: dockConfig?.defaultPosition ?? 'left',
113+
position: 'left',
75114
open: false,
76115
inactiveTimeout: 3_000,
77116
},
78117
{ mergeDefaults: true },
79118
)
119+
if (!hasStoredDockState)
120+
void seedWindowDefaultsOnce(rpc, state)
80121

81122
const context = await createDocksContext(
82123
'embedded',

‎packages/core/src/client/standalone/App.vue‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import type { DocksContext } from '@vitejs/devtools-kit/client'
2+
import type { DevToolsDocksContext } from '../webcomponents/state/context'
33
import { CLIENT_CONTEXT_KEY, getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
44
import { watchEffect } from 'vue'
55
import DockStandalone from '../webcomponents/components/dock/DockStandalone.vue'
@@ -21,7 +21,7 @@ const rpc = await getDevToolsRpcClient()
2121
// eslint-disable-next-line no-console
2222
console.log('[VITE DEVTOOLS] RPC', rpc)
2323
24-
const context: DocksContext = await createDocksContext(
24+
const context: DevToolsDocksContext = await createDocksContext(
2525
'standalone',
2626
rpc,
2727
)

‎packages/core/src/client/webcomponents/components/DockEmbedded.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { DocksContext } from '@vitejs/devtools-kit/client'
21
import type { VueElementConstructor } from 'vue'
2+
import type { DevToolsDocksContext } from '../state/context'
33
import { defineCustomElement } from 'vue'
44
import css from '../.generated/css'
55
import Component from './dock/DockEmbedded.vue'
@@ -11,7 +11,7 @@ export const DockEmbedded = defineCustomElement(
1111
styles: [css],
1212
},
1313
) as VueElementConstructor<{
14-
context: DocksContext
14+
context: DevToolsDocksContext
1515
}>
1616

1717
customElements.define('vite-devtools-dock-embedded', DockEmbedded)

‎packages/core/src/client/webcomponents/components/DockStandalone.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { DocksContext } from '@vitejs/devtools-kit/client'
21
import type { VueElementConstructor } from 'vue'
2+
import type { DevToolsDocksContext } from '../state/context'
33
import { defineCustomElement } from 'vue'
44
import css from '../.generated/css'
55
import Component from './dock/DockStandalone.vue'
@@ -11,7 +11,7 @@ export const DockStandalone = defineCustomElement(
1111
styles: [css],
1212
},
1313
) as VueElementConstructor<{
14-
context: DocksContext
14+
context: DevToolsDocksContext
1515
}>
1616

1717
if (!customElements.get('vite-devtools-dock-standalone'))

‎packages/core/src/client/webcomponents/components/dock/Dock.vue‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
2-
import type { DocksContext } from '@vitejs/devtools-kit/client'
32
import type { CSSProperties } from 'vue'
3+
import type { DevToolsDocksContext } from '../../state/context'
44
import type { DockLayout } from './dock-layout'
55
import { useEventListener, useScreenSafeArea, whenever } from '@vueuse/core'
66
import { computed, onMounted, reactive, ref, useTemplateRef } from 'vue'
@@ -23,7 +23,7 @@ import DockEntriesWithCategories from './DockEntriesWithCategories.vue'
2323
import DockOverflowButton from './DockOverflowButton.vue'
2424
2525
const props = defineProps<{
26-
context: DocksContext
26+
context: DevToolsDocksContext
2727
/**
2828
* Override individual dock layout tunables (bar height, item capacity,
2929
* viewport margin, snapping, ...). Merged over `DEFAULT_DOCK_LAYOUT`.
@@ -34,9 +34,9 @@ const props = defineProps<{
3434
// Here we directly destructure is as we don't expect context to be changed
3535
const context = props.context
3636
37-
// A plugin-declared `maxVisibleItems` is the default; an explicit `layout` prop wins.
37+
// A plugin/host-declared `maxVisibleItems` is the default; an explicit `layout` prop wins.
3838
const layout = computed(() => resolveDockLayout({
39-
maxVisibleItems: context.rpc.connectionMeta.dockConfig?.maxVisibleItems,
39+
maxVisibleItems: context.dockConfig.maxVisibleItems,
4040
...props.layout,
4141
}))
4242

‎packages/core/src/client/webcomponents/components/dock/DockContextMenu.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DevToolsDockEntry } from '@vitejs/devtools-kit'
2-
import type { DocksContext } from '@vitejs/devtools-kit/client'
2+
import type { DevToolsDocksContext } from '../../state/context'
33
import { h } from 'vue'
44
import { setDockContextMenu } from '../../state/floating-tooltip'
55
import { isDockPopupSupported, requestDockPopupOpen, useIsDockPopupOpen } from '../../state/popup'
@@ -23,7 +23,7 @@ function renderMenuItem(item: DockMenuItem) {
2323
])
2424
}
2525

26-
function hideDock(context: DocksContext, entry: DevToolsDockEntry) {
26+
function hideDock(context: DevToolsDocksContext, entry: DevToolsDockEntry) {
2727
const settingsStore = context.docks.settings
2828
const id = entry.id
2929
settingsStore.mutate((state) => {
@@ -35,7 +35,7 @@ function hideDock(context: DocksContext, entry: DevToolsDockEntry) {
3535
setDockContextMenu(null)
3636
}
3737

38-
function refreshDock(context: DocksContext, entry: DevToolsDockEntry) {
38+
function refreshDock(context: DevToolsDocksContext, entry: DevToolsDockEntry) {
3939
const state = context.docks.getStateById(entry.id)
4040
const iframe = state?.domElements.iframe
4141
if (!iframe) {
@@ -48,7 +48,7 @@ function refreshDock(context: DocksContext, entry: DevToolsDockEntry) {
4848
setDockContextMenu(null)
4949
}
5050

51-
function canHide(context: DocksContext, entry: DevToolsDockEntry) {
51+
function canHide(context: DevToolsDocksContext, entry: DevToolsDockEntry) {
5252
if (entry.id === '~settings')
5353
return false
5454
return context.docks.entries.some(item => item.id === entry.id)
@@ -59,7 +59,7 @@ function canRefresh(entry: DevToolsDockEntry) {
5959
}
6060

6161
export function openDockContextMenu(options: {
62-
context: DocksContext
62+
context: DevToolsDocksContext
6363
entry: DevToolsDockEntry
6464
el: HTMLElement
6565
gap?: number

0 commit comments

Comments
 (0)