Skip to content

Commit da86e0d

Browse files
committed
feat: logs system
1 parent 98f9207 commit da86e0d

23 files changed

Lines changed: 901 additions & 5 deletions

File tree

docs/.vitepress/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const DevToolsKitNav = [
1414
{ text: 'Dock System', link: '/kit/dock-system' },
1515
{ text: 'RPC', link: '/kit/rpc' },
1616
{ text: 'Shared State', link: '/kit/shared-state' },
17+
{ text: 'Logs', link: '/kit/logs' },
1718
]
1819

1920
const SocialLinks = [
@@ -66,6 +67,7 @@ export default extendConfig(withMermaid(defineConfig({
6667
{ text: 'Dock System', link: '/kit/dock-system' },
6768
{ text: 'RPC', link: '/kit/rpc' },
6869
{ text: 'Shared State', link: '/kit/shared-state' },
70+
{ text: 'Logs', link: '/kit/logs' },
6971
],
7072
},
7173
],

docs/kit/logs.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Logs
2+
3+
The Logs system allows plugins to emit structured log entries from both the server (Node.js) and client (browser) contexts. Logs are displayed in the built-in **Logs** panel in the DevTools dock, and can optionally appear as toast notifications.
4+
5+
## Use Cases
6+
7+
- **Accessibility audits** — Run axe or similar tools on the client side, report warnings with element positions and autofix suggestions
8+
- **Runtime errors** — Capture and display errors with stack traces
9+
- **Linting & testing** — Run ESLint or test runners alongside the dev server and surface results with file positions
10+
- **Notifications** — Short-lived messages like "URL copied" that auto-dismiss
11+
12+
## Log Entry Fields
13+
14+
| Field | Type | Required | Description |
15+
|-------|------|----------|-------------|
16+
| `message` | `string` | Yes | Short title or summary |
17+
| `level` | `'info' \| 'warn' \| 'error' \| 'success' \| 'debug'` | Yes | Severity level, determines color and icon |
18+
| `description` | `string` | No | Detailed description or explanation |
19+
| `stacktrace` | `string` | No | Stack trace string |
20+
| `filePosition` | `{ file, line?, column? }` | No | Source file location (clickable in the panel) |
21+
| `elementPosition` | `{ selector?, boundingBox?, description? }` | No | DOM element position info |
22+
| `autofix` | `{ type: 'rpc', name: string } \| Function` | No | Autofix action |
23+
| `notify` | `boolean` | No | Show as a toast notification |
24+
| `category` | `string` | No | Grouping category (e.g., `'a11y'`, `'lint'`) |
25+
| `labels` | `string[]` | No | Tags for filtering |
26+
| `autoDismiss` | `number` | No | Time in ms to auto-dismiss the toast (default: 5000) |
27+
| `autoDelete` | `number` | No | Time in ms to auto-delete the log entry |
28+
29+
## Server-Side Usage
30+
31+
In your plugin's `devtools.setup`, use `context.logs` to emit log entries:
32+
33+
```ts
34+
export function myPlugin() {
35+
return {
36+
name: 'my-plugin',
37+
devtools: {
38+
setup(context) {
39+
// Simple log
40+
context.logs.add({
41+
message: 'Plugin initialized',
42+
level: 'info',
43+
})
44+
45+
// Warning with file position
46+
context.logs.add({
47+
message: 'Deprecated API usage detected',
48+
level: 'warn',
49+
description: 'The `foo()` API is deprecated. Use `bar()` instead.',
50+
filePosition: { file: 'src/app.ts', line: 42, column: 5 },
51+
category: 'lint',
52+
})
53+
54+
// Error with stack trace
55+
context.logs.add({
56+
message: 'Build failed',
57+
level: 'error',
58+
stacktrace: error.stack,
59+
category: 'build',
60+
})
61+
62+
// Short notification
63+
context.logs.add({
64+
message: 'Configuration reloaded',
65+
level: 'success',
66+
notify: true,
67+
autoDismiss: 3000,
68+
autoDelete: 10000,
69+
})
70+
},
71+
},
72+
}
73+
}
74+
```
75+
76+
The `source` field is automatically set to the plugin name.
77+
78+
## Client-Side Usage
79+
80+
From client-side code (running in the user's browser), emit logs via the RPC client:
81+
82+
```ts
83+
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
84+
85+
const client = await getDevToolsRpcClient()
86+
87+
await client.call('devtoolskit:internal:logs:add', {
88+
message: 'Accessibility issue: missing alt text',
89+
level: 'warn',
90+
description: 'Images should have alt attributes for screen readers.',
91+
elementPosition: {
92+
selector: 'img.hero-image',
93+
description: 'Hero image in the header',
94+
},
95+
category: 'a11y',
96+
labels: ['wcag-2.1', 'images'],
97+
}, 'axe-plugin')
98+
```
99+
100+
The second argument to `logs:add` is the `source` identifier.
101+
102+
## Autofix
103+
104+
Autofix actions let users fix issues with a single click. There are two approaches:
105+
106+
### RPC-Based Autofix
107+
108+
Register an RPC function for the fix, then reference it by name:
109+
110+
```ts
111+
context.rpc.register(defineRpcFunction({
112+
name: 'my-plugin:fix-deprecated-api',
113+
type: 'action',
114+
setup: () => ({
115+
async handler() {
116+
// Perform the fix
117+
},
118+
}),
119+
}))
120+
121+
context.logs.add({
122+
message: 'Deprecated API usage',
123+
level: 'warn',
124+
autofix: { type: 'rpc', name: 'my-plugin:fix-deprecated-api' },
125+
})
126+
```
127+
128+
### Function Autofix
129+
130+
For server-side plugins, you can pass a function directly:
131+
132+
```ts
133+
context.logs.add({
134+
message: 'Missing configuration',
135+
level: 'warn',
136+
autofix: async () => {
137+
// Write the config file
138+
},
139+
})
140+
```
141+
142+
## Toast Notifications
143+
144+
Set `notify: true` to show the log entry as a toast notification overlay. Toasts appear regardless of whether the Logs panel is open.
145+
146+
```ts
147+
context.logs.add({
148+
message: 'URL copied to clipboard',
149+
level: 'success',
150+
notify: true,
151+
autoDismiss: 2000, // disappear after 2 seconds
152+
})
153+
```
154+
155+
The default auto-dismiss time for toasts is 5 seconds.
156+
157+
## Managing Logs
158+
159+
```ts
160+
// Remove a specific log
161+
context.logs.remove(entry.id)
162+
163+
// Clear all logs
164+
context.logs.clear()
165+
```
166+
167+
Logs have a maximum capacity of 1000 entries. When the limit is reached, the oldest entries are automatically removed.
168+
169+
## Dock Badge
170+
171+
The Logs dock icon automatically shows a badge with the total log count. The icon is hidden when there are no logs.

packages/core/src/client/webcomponents/.generated/css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/core/src/client/webcomponents/components/DockEmbedded.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { closeDockPopup, useIsDockPopupOpen } from '../state/popup'
55
import Dock from './Dock.vue'
66
import DockPanel from './DockPanel.vue'
77
import FloatingElements from './FloatingElements.vue'
8+
import ToastOverlay from './ToastOverlay.vue'
89
910
defineProps<{
1011
context: DocksContext
@@ -29,4 +30,5 @@ onUnmounted(() => {
2930
</template>
3031
</Dock>
3132
<FloatingElements v-if="!isDockPopupOpen" />
33+
<ToastOverlay />
3234
</template>

packages/core/src/client/webcomponents/components/DockEntries.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function toggleDockEntry(dock: DevToolsDockEntry) {
3434
:is-selected="selected?.id === dock.id"
3535
:is-dimmed="selected ? (selected.id !== dock.id) : false"
3636
:is-vertical="isVertical"
37+
:badge="dock.badge"
3738
@click="toggleDockEntry(dock)"
3839
/>
3940
</template>

packages/core/src/client/webcomponents/components/DockStandalone.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { PersistedDomViewsManager } from '../utils/PersistedDomViewsManager'
66
import DockEntriesWithCategories from './DockEntriesWithCategories.vue'
77
import FloatingElements from './FloatingElements.vue'
88
import VitePlus from './icons/VitePlus.vue'
9+
import ToastOverlay from './ToastOverlay.vue'
910
import ViewBuiltinClientAuthNotice from './ViewBuiltinClientAuthNotice.vue'
1011
import ViewEntry from './ViewEntry.vue'
1112
@@ -79,4 +80,5 @@ function switchEntry(id: string | undefined) {
7980
</div>
8081
</div>
8182
<FloatingElements />
83+
<ToastOverlay />
8284
</template>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<script setup lang="ts">
2+
import { dismissToast, useToasts } from '../state/toasts'
3+
import DockIcon from './DockIcon.vue'
4+
5+
const toasts = useToasts()
6+
7+
const levelColors: Record<string, string> = {
8+
info: 'border-blue',
9+
warn: 'border-amber',
10+
error: 'border-red',
11+
success: 'border-green',
12+
debug: 'border-gray',
13+
}
14+
15+
const levelIcons: Record<string, string> = {
16+
info: 'ph:info-duotone',
17+
warn: 'ph:warning-duotone',
18+
error: 'ph:x-circle-duotone',
19+
success: 'ph:check-circle-duotone',
20+
debug: 'ph:bug-duotone',
21+
}
22+
</script>
23+
24+
<template>
25+
<Teleport to="body">
26+
<div
27+
v-if="toasts.length > 0"
28+
class="fixed bottom-4 right-4 z-2147483647 flex flex-col gap-2 pointer-events-auto max-w-80"
29+
>
30+
<TransitionGroup
31+
enter-active-class="transition-all duration-300 ease-out"
32+
leave-active-class="transition-all duration-200 ease-in"
33+
enter-from-class="opacity-0 translate-x-4"
34+
leave-to-class="opacity-0 translate-x-4"
35+
>
36+
<div
37+
v-for="toast of toasts"
38+
:key="toast.id"
39+
class="bg-base border border-base rounded-lg shadow-lg flex items-start gap-2 px-3 py-2 border-l-3"
40+
:class="levelColors[toast.entry.level] || 'border-gray'"
41+
>
42+
<DockIcon
43+
:icon="levelIcons[toast.entry.level] || 'ph:info-duotone'"
44+
class="w-4 h-4 flex-none mt-0.5"
45+
/>
46+
<div class="flex-1 min-w-0">
47+
<div class="text-sm font-medium truncate">
48+
{{ toast.entry.message }}
49+
</div>
50+
<div v-if="toast.entry.source" class="text-xs op50 truncate">
51+
{{ toast.entry.source }}
52+
</div>
53+
</div>
54+
<button
55+
class="flex-none op50 hover:op100 p-0.5"
56+
@click="dismissToast(toast.id)"
57+
>
58+
<DockIcon icon="ph:x" class="w-3 h-3" />
59+
</button>
60+
</div>
61+
</TransitionGroup>
62+
</div>
63+
</Teleport>
64+
</template>

0 commit comments

Comments
 (0)