-
-
Notifications
You must be signed in to change notification settings - Fork 77
feat: logs system #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: logs system #193
Changes from 5 commits
da86e0d
fd2b44d
9483bf8
e9b2be3
dbf92e1
36f1a1c
ddd655d
7b1b309
52ad731
1015746
9133ba8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | | ||
|
|
||
| ## 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() | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
|
||
| ``` | ||
|
|
||
| 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. | ||
| 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" | ||
| } | ||
| } |
| 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> |
| 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> | ||
| ) | ||
| } |
| 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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /// <reference types="vite/client" /> |
| 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, | ||
| }), | ||
| ], | ||
| }) |
There was a problem hiding this comment.
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.