Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ const root = fileURLToPath(new URL('.', import.meta.url))
const r = (path: string) => fileURLToPath(new URL(`./packages/${path}`, import.meta.url))

export const alias = {
'@vitejs/devtools-rpc': r('rpc/src'),
'@vitejs/devtools-rpc/presets/ws/server': r('rpc/src/presets/ws/server.ts'),
'@vitejs/devtools-rpc/presets/ws/client': r('rpc/src/presets/ws/client.ts'),
'@vitejs/devtools-rpc': r('rpc/src'),
'@vitejs/devtools-kit/client': r('kit/src/client/index.ts'),
'@vitejs/devtools-kit/utils/events': r('kit/src/utils/events.ts'),
'@vitejs/devtools-kit/utils/nanoid': r('kit/src/utils/nanoid.ts'),
Expand Down
85 changes: 85 additions & 0 deletions docs/kit/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ DevTools Kit offers a complete toolkit for building DevTools integrations:

- **🔌 [Built-in RPC Layer](#remote-procedure-calls-rpc)**: Type-safe bidirectional communication between your Node.js server and browser clients, eliminating the need to set up WebSocket connections or message passing manually

- **🔗 [Shared State](#shared-state)**: Share data between server and client with automatic synchronization

- **🌐 Isomorphic Views Hosting**: Write your UI once and deploy it anywhere—as embedded floating panels, browser extension panels, standalone webpages, or even deployable SPAs for sharing build snapshots (work in progress).

## Why DevTools Kit?
Expand Down Expand Up @@ -418,6 +420,89 @@ rpc.client.register({
})
```

### Shared State

The DevTools Kit provides a built-in shared state system that enables you to share data between the server and client with automatic synchronization.

On the server side, you can get the shared state using `ctx.rpc.sharedState.get(name, options)`:

```ts {6-10}
export default function myPlugin(): Plugin {
return {
name: 'my-plugin',
devtools: {
async setup(ctx) {
// Get the shared state
const state = await ctx.rpc.sharedState.get('my-plugin:state', {
initialValue: {
count: 0,
name: 'John Doe',
},
})

// Use .value() to get the current state
console.log(state.value()) // { count: 0, name: 'John Doe' }

setTimeout(() => {
// Mutate the shared state, changes will be automatically synchronized to all the connected clients
state.mutate((state) => {
state.count += 1
})
}, 1000)
},
},
}
}
```

<details>
<summary>Type-safe shared state</summary>

The shared state is type-safe, you can get the state with the type of the initial value. To do so, you need to extend the `DevToolsRpcSharedStates` interface in your plugin's type definitions.

```ts [src/types.ts]
import '@vitejs/devtools-kit'

declare module '@vitejs/devtools-kit' {
interface DevToolsRpcSharedStates {
'my-plugin:state': { count: number, name: string }
}
}
```

</details>

On the client side, you can get the shared state using `client.rpc.sharedState.get(name)`:

```ts {6-10}
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'

const client = await getDevToolsRpcClient()

const state = await client.rpc.sharedState.get('my-plugin:state')

console.log(state.value()) // { count: 0, name: 'John Doe' }

// Use .on('updated') to subscribe to changes
state.on('updated', (newState) => {
console.log(newState) // { count: 1, name: 'John Doe' }
})
```

For example, if you use Vue, you can wrap it into a reactive ref:

```ts {6-10}
import { shallowRef } from 'vue'

const sharedState = await client.rpc.sharedState.get('my-plugin:state')
const state = shallowRef(sharedState.value())
sharedState.on('updated', (newState) => {
state.value = newState
})

// Now the `state` ref will be updated automatically when the shared state changes
```

## References

The docs might not cover all the details, please help us to improve it by submitting PRs. And in the meantime, you can refer to the following existing DevTools integrations for reference (but note they might not always be up to date with the latest API changes):
Expand Down
2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@
"tsdown": "catalog:build",
"typescript": "catalog:devtools",
"unplugin-vue": "catalog:build",
"unplugin-vue-router": "catalog:playground",
"vite": "catalog:build",
"vue": "catalog:frontend",
"vue-router": "catalog:playground",
"vue-tsc": "catalog:devtools"
}
}
34 changes: 6 additions & 28 deletions packages/core/playground/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,30 +1,8 @@
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
<div>
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo">
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo">
</a>
</div>
<HelloWorld msg="Vite + Vue" />
<Suspense>
<RouterView />
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>

<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
12 changes: 11 additions & 1 deletion packages/core/playground/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from 'vue-router/auto-routes'

import App from './App.vue'
import './style.css'

createApp(App).mount('#app')
const router = createRouter({
history: createWebHistory(),
routes,
})

createApp(App)
.use(router)
.mount('#app')
42 changes: 42 additions & 0 deletions packages/core/playground/src/pages/devtools.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script setup lang="ts">
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
import { onMounted, shallowRef } from 'vue'

const stateRef = shallowRef<any>(undefined)
const isTrustedRef = shallowRef<boolean | null>(null)

let increment = () => {}

onMounted(async () => {
const client = await getDevToolsRpcClient()

isTrustedRef.value = client.isTrusted
client.events.on('rpc:is-trusted:updated', (isTrusted) => {
isTrustedRef.value = isTrusted
})

const state = await client.sharedState.get('counter')

increment = () => {
state.mutate((state) => {
state.count++
})
}

stateRef.value = state.value()
state.on('updated', (newState) => {
stateRef.value = newState
})
})
</script>

<template>
<div>
<h1>DevTools </h1>
<div>{{ isTrustedRef }}</div>
<pre>{{ stateRef }}</pre>
<button @click="increment">
Increment
</button>
</div>
</template>
30 changes: 30 additions & 0 deletions packages/core/playground/src/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">
import HelloWorld from '../components/HelloWorld.vue'
</script>

<template>
<div>
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo">
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="../assets/vue.svg" class="logo vue" alt="Vue logo">
</a>
</div>
<HelloWorld msg="Vite + Vue" />
</template>

<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
26 changes: 26 additions & 0 deletions packages/core/playground/typed-router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ declare module 'vue-router/auto-routes' {
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
'/': RouteRecordInfo<
'/',
'/',
Record<never, never>,
Record<never, never>,
| never
>,
'/devtools': RouteRecordInfo<
'/devtools',
'/devtools',
Record<never, never>,
Record<never, never>,
| never
>,
}

/**
Expand All @@ -36,6 +50,18 @@ declare module 'vue-router/auto-routes' {
* @internal
*/
export interface _RouteFileInfoMap {
'src/pages/index.vue': {
routes:
| '/'
views:
| never
}
'src/pages/devtools.vue': {
routes:
| '/devtools'
views:
| never
}
}

/**
Expand Down
32 changes: 29 additions & 3 deletions packages/core/playground/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import process from 'node:process'
import Vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
import VueRouter from 'unplugin-vue-router/vite'
import { defineConfig } from 'vite'
import Tracer from 'vite-plugin-vue-tracer'
import { alias } from '../../../alias'
// eslint-disable-next-line ts/ban-ts-comment
// @ts-ignore ignore the type error
import { DevToolsViteUI } from '../../vite/src/node'
import { DevTools } from '../src'
import { buildCSS } from '../src/client/webcomponents/scripts/build-css'

declare module '@vitejs/devtools-kit' {
interface DevToolsRpcSharedStates {
counter: { count: number }
}
}

// https://vite.dev/config/
export default defineConfig({
define: {
'import.meta.env.VITE_DEVTOOLS_LOCAL_DEV': JSON.stringify(process.env.VITE_DEVTOOLS_LOCAL_DEV),
},
base: './',
resolve: {
alias,
},
plugins: [
VueRouter(),
Vue(),
{
name: 'build-css',
Expand All @@ -38,7 +50,7 @@ export default defineConfig({
{
name: 'local',
devtools: {
setup(ctx) {
async setup(ctx) {
ctx.docks.register({
title: 'Local',
icon: 'logos:vue',
Expand Down Expand Up @@ -94,6 +106,14 @@ export default defineConfig({
action: ctx.utils.createSimpleClientScript(() => {}),
})

ctx.docks.register({
id: 'devtools-tab',
type: 'iframe',
url: '/devtools/',
title: 'DevTools',
icon: 'ph:gear-duotone',
})

ctx.docks.register({
id: 'launcher',
type: 'launcher',
Expand Down Expand Up @@ -123,10 +143,16 @@ export default defineConfig({
},
})

let count = 1
const counterState = await ctx.rpc.sharedState.get('counter', {
initialValue: { count: 1 },
})

// eslint-disable-next-line unimport/auto-insert
setInterval(() => {
count = (count + 1) % 5
counterState.mutate((current) => {
current.count = (current.count + 1) % 5
})
const count = counterState.value().count
ctx.docks.update({
id: 'counter',
type: 'action',
Expand Down
20 changes: 16 additions & 4 deletions packages/core/src/node/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,27 @@ export async function createDevToolsContext(

// Register hosts side effects
docksHost.events.on('dock:entry:updated', debounce(() => {
rpcHost.broadcast('vite:internal:docks:updated')
rpcHost.broadcast({
method: 'vite:internal:docks:updated',
args: [],
})
}, 10))
terminalsHost.events.on('terminal:session:updated', debounce(() => {
rpcHost.broadcast('vite:internal:terminals:updated')
rpcHost.broadcast({
method: 'vite:internal:terminals:updated',
args: [],
})
// New terminals might affect the visibility of the terminals dock entry, we trigger it here as well
rpcHost.broadcast('vite:internal:docks:updated')
rpcHost.broadcast({
method: 'vite:internal:docks:updated',
args: [],
})
}, 10))
terminalsHost.events.on('terminal:session:stream-chunk', (data) => {
rpcHost.broadcast('vite:internal:terminals:stream-chunk', data)
rpcHost.broadcast({
method: 'vite:internal:terminals:stream-chunk',
args: [data],
})
})

// Register plugins
Expand Down
Loading
Loading