Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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: 2 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const DevToolsKitNav = [
{ text: 'Dock System', link: '/kit/dock-system' },
{ text: 'RPC', link: '/kit/rpc' },
{ text: 'Shared State', link: '/kit/shared-state' },
{ text: 'Logs', link: '/kit/logs' },
]

const SocialLinks = [
Expand Down Expand Up @@ -66,6 +67,7 @@ export default extendConfig(withMermaid(defineConfig({
{ text: 'Dock System', link: '/kit/dock-system' },
{ text: 'RPC', link: '/kit/rpc' },
{ text: 'Shared State', link: '/kit/shared-state' },
{ text: 'Logs', link: '/kit/logs' },
],
},
],
Expand Down
191 changes: 191 additions & 0 deletions docs/kit/logs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Logs

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.

## Use Cases

- **Accessibility audits** — Run axe or similar tools on the client side, report warnings with element positions and autofix suggestions
- **Runtime errors** — Capture and display errors with stack traces
- **Linting & testing** — Run ESLint or test runners alongside the dev server and surface results with file positions
- **Notifications** — Short-lived messages like "URL copied" that auto-dismiss

## Log Entry Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `message` | `string` | Yes | Short title or summary |
| `level` | `'info' \| 'warn' \| 'error' \| 'success' \| 'debug'` | Yes | Severity level, determines color and icon |
| `description` | `string` | No | Detailed description or explanation |
| `stacktrace` | `string` | No | Stack trace string |
| `filePosition` | `{ file, line?, column? }` | No | Source file location (clickable in the panel) |
| `elementPosition` | `{ selector?, boundingBox?, description? }` | No | DOM element position info |
| `autofix` | `{ type: 'rpc', name: string } \| Function` | No | Autofix action |
| `notify` | `boolean` | No | Show as a toast notification |
| `category` | `string` | No | Grouping category (e.g., `'a11y'`, `'lint'`) |
| `labels` | `string[]` | No | Tags for filtering |
| `autoDismiss` | `number` | No | Time in ms to auto-dismiss the toast (default: 5000) |
| `autoDelete` | `number` | No | Time in ms to auto-delete the log entry |
| `status` | `'loading' \| 'idle'` | No | Status indicator (shows spinner when `'loading'`) |
| `id` | `string` | No | Explicit id for deduplication — re-adding with the same id updates the existing entry |

Comment on lines +14 to +29
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The markdown table uses || at the start of each row, which won’t render as a table in VitePress/Markdown (it should be single | pipes). Please update the table syntax so the docs render correctly.

Copilot uses AI. Check for mistakes.
## Server-Side Usage

In your plugin's `devtools.setup`, use `context.logs` to emit log entries. The `add()` method returns a **handle** with `.update()` and `.dismiss()` helpers:

```ts
export function myPlugin() {
return {
name: 'my-plugin',
devtools: {
async setup(context) {
// Simple log
await context.logs.add({
message: 'Plugin initialized',
level: 'info',
})

// Log with loading state, then update
const log = await context.logs.add({
message: 'Building...',
level: 'info',
status: 'loading',
})

// Later, update via the handle
await log.update({
message: 'Build complete',
level: 'success',
status: 'idle',
})

// Or dismiss it
await log.dismiss()
},
},
}
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Server-Side Usage” section shows await context.logs.add(...) returning a handle with .update()/.dismiss(), but the current DevToolsLogsHost.add() API is synchronous and returns a DevToolsLogEntry. Either update the implementation/types to match the documented handle-based API, or adjust the docs/examples to reflect the actual host API to avoid misleading plugin authors.

Copilot uses AI. Check for mistakes.
```

The `source` field is automatically set to the plugin name.

## Client-Side Usage

In dock action scripts, use `context.logs` — the same API as on the server:

```ts
import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'

export default async function (context: DockClientScriptContext) {
const log1 = await context.logs.add({
message: 'Running audit...',
level: 'info',
status: 'loading',
notify: true,
category: 'a11y',
})

// ... do work ...

await log1.update({
message: 'Audit complete — 3 issues found',
level: 'warn',
status: 'idle',
})
}
```

The `source` is automatically set to the dock entry id.

## Log Handle

`context.logs.add()` returns a `DevToolsLogHandle` with:

| Property/Method | Description |
|-----------------|-------------|
| `handle.id` | The log entry id |
| `handle.entry` | The current `DevToolsLogEntry` data |
| `handle.update(patch)` | Partially update the log entry |
| `handle.dismiss()` | Remove the log entry |

## Deduplication

When you call `context.logs.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:

```ts
// First call creates the entry
await context.logs.add({ id: 'my-scan', message: 'Scanning...', level: 'info', status: 'loading' })

// Second call with same id updates it
await context.logs.add({ id: 'my-scan', message: 'Scan complete', level: 'success', status: 'idle' })
```

## Autofix

Autofix actions let users fix issues with a single click. There are two approaches:

### RPC-Based Autofix

Register an RPC function for the fix, then reference it by name:

```ts
context.rpc.register(defineRpcFunction({
name: 'my-plugin:fix-deprecated-api',
type: 'action',
setup: () => ({
async handler() {
// Perform the fix
},
}),
}))

await context.logs.add({
message: 'Deprecated API usage',
level: 'warn',
autofix: { type: 'rpc', name: 'my-plugin:fix-deprecated-api' },
})
```

### Function Autofix

For server-side plugins, you can pass a function directly:

```ts
await context.logs.add({
message: 'Missing configuration',
level: 'warn',
autofix: async () => {
// Write the config file
},
})
```

## Toast Notifications

Set `notify: true` to show the log entry as a toast notification overlay. Toasts appear regardless of whether the Logs panel is open.

```ts
await context.logs.add({
message: 'URL copied to clipboard',
level: 'success',
notify: true,
autoDismiss: 2000, // disappear after 2 seconds
})
```

The default auto-dismiss time for toasts is 5 seconds.

## Managing Logs

```ts
// Remove a specific log by id
await context.logs.remove(entryId)

// Clear all logs
await context.logs.clear()
```

Logs have a maximum capacity of 1000 entries. When the limit is reached, the oldest entries are automatically removed.

## Dock Badge

The Logs dock icon automatically shows a badge with the total log count. The icon is hidden when there are no logs.
34 changes: 34 additions & 0 deletions examples/plugin-a11y-checker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "example-plugin-a11y-checker",
"type": "module",
"version": "0.0.0-alpha.34",
"private": true,
"exports": {
".": "./dist/index.mjs",
"./package.json": "./package.json"
},
"types": "./dist/index.d.mts",
"files": [
"dist"
],
"scripts": {
"build:node": "tsdown --config-loader=tsx",
"build": "pnpm run build:node",
"play:dev": "pnpm run build && cd playground && DEBUG='vite:devtools:*' vite"
},
"peerDependencies": {
"vite": "*"
},
"dependencies": {
"@vitejs/devtools": "workspace:*",
"@vitejs/devtools-kit": "workspace:*",
"axe-core": "catalog:devtools"
},
"devDependencies": {
"solid-js": "catalog:devtools",
"tsdown": "catalog:build",
"unocss": "catalog:build",
"vite": "catalog:build",
"vite-plugin-solid": "catalog:devtools"
}
}
12 changes: 12 additions & 0 deletions examples/plugin-a11y-checker/playground/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>A11y Checker Playground</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
73 changes: 73 additions & 0 deletions examples/plugin-a11y-checker/playground/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// @unocss-include

/**
* This component has intentional accessibility issues
* for testing the A11y Checker plugin.
*/
export default function App() {
return (
<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">
<main class="mx-auto max-w-4xl px-6 py-10">
<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">
<h1 class="m-0 text-3xl font-semibold tracking-tight">A11y Checker Playground</h1>
<p class="mt-3 leading-7 text-slate-700 dark:text-slate-300">
Open Vite DevTools and click the
{' '}
<strong>A11y Checker</strong>
{' '}
icon
(wheelchair) to run an accessibility audit on this page.
The results will appear in the
{' '}
<strong>Logs</strong>
{' '}
panel.
</p>
</section>

{/* Intentional a11y issues below */}

<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">
<h2 class="text-xl font-semibold mb-4">Test Cases</h2>

{/* Issue: image without alt */}
<div class="mb-4">
<h3 class="text-sm font-medium mb-2 op50">Image without alt text</h3>
<img src="https://placehold.co/200x100" width="200" height="100" />
</div>

{/* Issue: button with no accessible name */}
<div class="mb-4">
<h3 class="text-sm font-medium mb-2 op50">Button without label</h3>
<button class="px-3 py-1 rounded bg-blue-500 text-white" />
</div>

{/* Issue: low contrast text */}
<div class="mb-4">
<h3 class="text-sm font-medium mb-2 op50">Low contrast text</h3>
<p style={{ 'color': '#ccc', 'background-color': '#fff' }}>
This text has very low contrast and is hard to read.
</p>
</div>

{/* Issue: form input without label */}
<div class="mb-4">
<h3 class="text-sm font-medium mb-2 op50">Input without label</h3>
<input type="text" placeholder="Enter something..." class="border rounded px-2 py-1" />
</div>

{/* Issue: clickable div without role */}
<div class="mb-4">
<h3 class="text-sm font-medium mb-2 op50">Clickable div without role</h3>
<div
onClick={() => {}}
class="cursor-pointer bg-purple-100 dark:bg-purple-900 rounded px-3 py-2 inline-block"
>
Click me (I'm a div, not a button)
</div>
</div>
</section>
</main>
</div>
)
}
11 changes: 11 additions & 0 deletions examples/plugin-a11y-checker/playground/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* @refresh reload */
import { render } from 'solid-js/web'
import App from './App'
import '@unocss/reset/tailwind.css'
import 'virtual:uno.css'

const root = document.getElementById('app')
if (!root)
throw new Error('Missing #app root')

render(() => <App />, root)
1 change: 1 addition & 0 deletions examples/plugin-a11y-checker/playground/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
21 changes: 21 additions & 0 deletions examples/plugin-a11y-checker/playground/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { fileURLToPath } from 'node:url'
import { DevTools } from '@vitejs/devtools'
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
import a11yChecker from '../src/node'

const unoConfig = fileURLToPath(new URL('../uno.config.ts', import.meta.url))

export default defineConfig({
plugins: [
DevTools({
builtinDevTools: false,
}),
solid(),
a11yChecker(),
UnoCSS({
configFile: unoConfig,
}),
],
})
Loading
Loading