Skip to content

Commit c661887

Browse files
authored
feat: logs system (#193)
1 parent 79dd169 commit c661887

Some content is hidden

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

49 files changed

+2558
-42
lines changed

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 & Notifications', 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: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Logs & Notifications
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 a11y checks or similar tools on the client side, report warnings with element positions
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+
| `notify` | `boolean` | No | Show as a toast notification |
23+
| `category` | `string` | No | Grouping category (e.g., `'a11y'`, `'lint'`) |
24+
| `labels` | `string[]` | No | Tags for filtering |
25+
| `autoDismiss` | `number` | No | Time in ms to auto-dismiss the toast (default: 5000) |
26+
| `autoDelete` | `number` | No | Time in ms to auto-delete the log entry |
27+
| `status` | `'loading' \| 'idle'` | No | Status indicator (shows spinner when `'loading'`) |
28+
| `id` | `string` | No | Explicit id for deduplication — re-adding with the same id updates the existing entry |
29+
30+
The `source` field is automatically set to `'server'` or `'client'` depending on where the log was emitted.
31+
32+
## Usage
33+
34+
Both server-side and client-side share the same `context.logs` API. All methods return Promises, but you don't need to `await` them for fire-and-forget usage.
35+
36+
### Fire-and-Forget
37+
38+
```ts
39+
// No await needed — just emit the log
40+
context.logs.add({
41+
message: 'Plugin initialized',
42+
level: 'info',
43+
})
44+
```
45+
46+
### With Handle
47+
48+
`await` the `add()` call to get a `DevToolsLogHandle` for subsequent updates:
49+
50+
```ts
51+
// Await to get a handle for later updates
52+
const handle = await context.logs.add({
53+
id: 'my-build',
54+
message: 'Building...',
55+
level: 'info',
56+
status: 'loading',
57+
})
58+
59+
// Later, update via the handle
60+
await handle.update({
61+
message: 'Build complete',
62+
level: 'success',
63+
status: 'idle',
64+
})
65+
66+
// Or dismiss it
67+
await handle.dismiss()
68+
```
69+
70+
### Server-Side Example
71+
72+
```ts
73+
export function myPlugin() {
74+
return {
75+
name: 'my-plugin',
76+
devtools: {
77+
setup(context) {
78+
// Fire-and-forget
79+
context.logs.add({
80+
message: 'Plugin initialized',
81+
level: 'info',
82+
})
83+
},
84+
},
85+
}
86+
}
87+
```
88+
89+
### Client-Side Example
90+
91+
```ts
92+
import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'
93+
94+
export default async function (context: DockClientScriptContext) {
95+
// Await to get the handle
96+
const log = await context.logs.add({
97+
message: 'Running audit...',
98+
level: 'info',
99+
status: 'loading',
100+
notify: true,
101+
})
102+
103+
// ... do work ...
104+
105+
// Update via handle — can also be fire-and-forget
106+
log.update({
107+
message: 'Audit complete — 3 issues found',
108+
level: 'warn',
109+
status: 'idle',
110+
})
111+
}
112+
```
113+
114+
## Log Handle
115+
116+
`context.logs.add()` returns a `Promise<DevToolsLogHandle>` with:
117+
118+
| Property/Method | Description |
119+
|-----------------|-------------|
120+
| `handle.id` | The log entry id |
121+
| `handle.entry` | The current `DevToolsLogEntry` data |
122+
| `handle.update(patch)` | Partially update the log entry (returns `Promise`) |
123+
| `handle.dismiss()` | Remove the log entry (returns `Promise`) |
124+
125+
Both `handle.update()` and `handle.dismiss()` return Promises but can be used without `await` for fire-and-forget.
126+
127+
## Deduplication
128+
129+
When you call `add()` with an explicit `id` that already exists, the existing entry is **updated** instead of duplicated. This is useful for logs that represent ongoing operations:
130+
131+
```ts
132+
// First call creates the entry
133+
context.logs.add({ id: 'my-scan', message: 'Scanning...', level: 'info', status: 'loading' })
134+
135+
// Second call with same id updates it
136+
context.logs.add({ id: 'my-scan', message: 'Scan complete', level: 'success', status: 'idle' })
137+
```
138+
139+
## Toast Notifications
140+
141+
Set `notify: true` to show the log entry as a toast notification overlay. Toasts appear regardless of whether the Logs panel is open.
142+
143+
```ts
144+
context.logs.add({
145+
message: 'URL copied to clipboard',
146+
level: 'success',
147+
notify: true,
148+
autoDismiss: 2000, // disappear after 2 seconds
149+
})
150+
```
151+
152+
The default auto-dismiss time for toasts is 5 seconds.
153+
154+
## Managing Logs
155+
156+
```ts
157+
// Remove a specific log by id
158+
context.logs.remove(entryId)
159+
160+
// Clear all logs
161+
context.logs.clear()
162+
```
163+
164+
Logs have a maximum capacity of 1000 entries. When the limit is reached, the oldest entries are automatically removed.
165+
166+
## Dock Badge
167+
168+
The Logs dock icon automatically shows a badge with the total log count. The icon is hidden when there are no logs.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "example-plugin-a11y-checker",
3+
"type": "module",
4+
"version": "0.0.0-alpha.34",
5+
"private": true,
6+
"exports": {
7+
".": "./dist/index.mjs",
8+
"./package.json": "./package.json"
9+
},
10+
"types": "./dist/index.d.mts",
11+
"files": [
12+
"dist"
13+
],
14+
"scripts": {
15+
"build:node": "tsdown --config-loader=tsx",
16+
"build": "pnpm run build:node",
17+
"play:dev": "pnpm run build && cd playground && DEBUG='vite:devtools:*' vite"
18+
},
19+
"peerDependencies": {
20+
"vite": "*"
21+
},
22+
"dependencies": {
23+
"@vitejs/devtools": "workspace:*",
24+
"@vitejs/devtools-kit": "workspace:*",
25+
"axe-core": "catalog:devtools"
26+
},
27+
"devDependencies": {
28+
"solid-js": "catalog:devtools",
29+
"tsdown": "catalog:build",
30+
"unocss": "catalog:build",
31+
"vite": "catalog:build",
32+
"vite-plugin-solid": "catalog:devtools"
33+
}
34+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>A11y Checker Playground</title>
7+
</head>
8+
<body>
9+
<div id="app"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// @unocss-include
2+
3+
/**
4+
* This component has intentional accessibility issues
5+
* for testing the A11y Checker plugin.
6+
*/
7+
export default function App() {
8+
return (
9+
<div class="min-h-screen bg-gradient-to-br from-slate-100 via-blue-50 to-cyan-100 text-slate-800 dark:from-slate-950 dark:via-slate-900 dark:to-slate-800 dark:text-slate-100">
10+
<main class="mx-auto max-w-4xl px-6 py-10">
11+
<section class="rounded-2xl border border-slate-200 bg-white/80 p-6 shadow-xl shadow-slate-300/20 backdrop-blur dark:border-slate-700 dark:bg-slate-900/70 dark:shadow-black/25">
12+
<h1 class="m-0 text-3xl font-semibold tracking-tight">A11y Checker Playground</h1>
13+
<p class="mt-3 leading-7 text-slate-700 dark:text-slate-300">
14+
Open Vite DevTools and click the
15+
{' '}
16+
<strong>A11y Checker</strong>
17+
{' '}
18+
icon
19+
(wheelchair) to run an accessibility audit on this page.
20+
The results will appear in the
21+
{' '}
22+
<strong>Logs</strong>
23+
{' '}
24+
panel.
25+
</p>
26+
</section>
27+
28+
{/* Intentional a11y issues below */}
29+
30+
<section class="mt-6 rounded-2xl border border-slate-200 bg-white/80 p-6 shadow-xl dark:border-slate-700 dark:bg-slate-900/70">
31+
<h2 class="text-xl font-semibold mb-4">Test Cases</h2>
32+
33+
{/* Issue: image without alt */}
34+
<div class="mb-4">
35+
<h3 class="text-sm font-medium mb-2 op50">Image without alt text</h3>
36+
<img src="https://placehold.co/200x100" width="200" height="100" />
37+
</div>
38+
39+
{/* Issue: button with no accessible name */}
40+
<div class="mb-4">
41+
<h3 class="text-sm font-medium mb-2 op50">Button without label</h3>
42+
<button class="px-3 py-1 rounded bg-blue-500 text-white" />
43+
</div>
44+
45+
{/* Issue: low contrast text */}
46+
<div class="mb-4">
47+
<h3 class="text-sm font-medium mb-2 op50">Low contrast text</h3>
48+
<p style={{ 'color': '#ccc', 'background-color': '#fff' }}>
49+
This text has very low contrast and is hard to read.
50+
</p>
51+
</div>
52+
53+
{/* Issue: form input without label */}
54+
<div class="mb-4">
55+
<h3 class="text-sm font-medium mb-2 op50">Input without label</h3>
56+
<input type="text" placeholder="Enter something..." class="border rounded px-2 py-1" />
57+
</div>
58+
59+
{/* Issue: clickable div without role */}
60+
<div class="mb-4">
61+
<h3 class="text-sm font-medium mb-2 op50">Clickable div without role</h3>
62+
<div
63+
onClick={() => {}}
64+
class="cursor-pointer bg-purple-100 dark:bg-purple-900 rounded px-3 py-2 inline-block"
65+
>
66+
Click me (I'm a div, not a button)
67+
</div>
68+
</div>
69+
</section>
70+
</main>
71+
</div>
72+
)
73+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* @refresh reload */
2+
import { render } from 'solid-js/web'
3+
import App from './App'
4+
import '@unocss/reset/tailwind.css'
5+
import 'virtual:uno.css'
6+
7+
const root = document.getElementById('app')
8+
if (!root)
9+
throw new Error('Missing #app root')
10+
11+
render(() => <App />, root)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { fileURLToPath } from 'node:url'
2+
import { DevTools } from '@vitejs/devtools'
3+
import UnoCSS from 'unocss/vite'
4+
import { defineConfig } from 'vite'
5+
import solid from 'vite-plugin-solid'
6+
import { A11yCheckerPlugin } from '../src/node'
7+
8+
const unoConfig = fileURLToPath(new URL('../uno.config.ts', import.meta.url))
9+
10+
export default defineConfig({
11+
plugins: [
12+
DevTools({
13+
builtinDevTools: false,
14+
}),
15+
solid(),
16+
A11yCheckerPlugin(),
17+
UnoCSS({
18+
configFile: unoConfig,
19+
}),
20+
],
21+
})

0 commit comments

Comments
 (0)