Conversation
@vitejs/devtools
@vitejs/devtools-kit
@vitejs/devtools-rolldown
@vitejs/devtools-rpc
@vitejs/devtools-self-inspect
commit: |
There was a problem hiding this comment.
Pull request overview
Adds a new structured Logs system to Vite DevTools Kit/Core, including a built-in Logs dock panel, toast notifications, internal RPC endpoints for log management, and an example accessibility checker plugin that emits logs from a client action script.
Changes:
- Introduce
DevToolsLogsHost+ kit log types and wire logs into the node context and dock model (including dock badge + auto-hide). - Add internal RPC methods + client-side reactive state/UI for logs (Logs panel + toast overlay).
- Add
examples/plugin-a11y-checker(Solid + axe-core) demonstrating client-side audits that report results into Logs.
Reviewed changes
Copilot reviewed 40 out of 42 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-workspace.yaml | Adds catalog entries for axe-core, solid-js, vite-plugin-solid. |
| pnpm-lock.yaml | Locks new dependencies and adds importer for examples/plugin-a11y-checker. |
| packages/kit/src/types/vite-plugin.ts | Extends DevToolsNodeContext with logs. |
| packages/kit/src/types/logs.ts | Adds log entry/types + host/client interfaces. |
| packages/kit/src/types/index.ts | Exports new logs types. |
| packages/kit/src/types/docks.ts | Adds optional badge field to dock entries. |
| packages/kit/src/client/client-script.ts | Exposes logs on DockClientScriptContext. |
| packages/core/src/node/rpc/internal/logs-add.ts | Internal RPC for adding logs. |
| packages/core/src/node/rpc/internal/logs-update.ts | Internal RPC for updating logs. |
| packages/core/src/node/rpc/internal/logs-remove.ts | Internal RPC for removing a log. |
| packages/core/src/node/rpc/internal/logs-list.ts | Internal RPC for listing logs. |
| packages/core/src/node/rpc/internal/logs-clear.ts | Internal RPC for clearing logs. |
| packages/core/src/node/rpc/internal/logs-autofix.ts | Internal RPC for running a log’s autofix. |
| packages/core/src/node/rpc/index.ts | Registers new internal RPCs + client broadcast type. |
| packages/core/src/node/host-logs.ts | Implements the in-memory logs host with eviction + auto-delete. |
| packages/core/src/node/host-docks.ts | Adds Logs built-in dock visibility + badge logic. |
| packages/core/src/node/context.ts | Wires logs host into context and broadcasts updates. |
| packages/core/src/client/webcomponents/state/toasts.ts | Adds toast state + timers. |
| packages/core/src/client/webcomponents/state/setup-script.ts | Ensures action dock scripts re-run per click (no caching). |
| packages/core/src/client/webcomponents/state/logs.ts | Adds reactive logs state + toast triggering + unread count. |
| packages/core/src/client/webcomponents/state/logs-client.ts | Client logs API that uses RPC and returns update/dismiss handles. |
| packages/core/src/client/webcomponents/state/context.ts | Injects context.logs into dock client script context. |
| packages/core/src/client/webcomponents/components/ViewBuiltinLogs.vue | Implements the Logs panel UI (filters/search/detail/actions). |
| packages/core/src/client/webcomponents/components/ToastOverlay.vue | Adds toast overlay UI and initializes logs early. |
| packages/core/src/client/webcomponents/components/DockStandalone.vue | Mounts toast overlay in standalone dock. |
| packages/core/src/client/webcomponents/components/DockEntries.vue | Passes dock badge through to entry component. |
| packages/core/src/client/webcomponents/components/DockEmbedded.vue | Mounts toast overlay in embedded dock. |
| packages/core/src/client/webcomponents/.generated/css.ts | Updates generated CSS for new UI classes. |
| examples/plugin-a11y-checker/uno.config.ts | UnoCSS config for the example playground. |
| examples/plugin-a11y-checker/tsdown.config.ts | Build config for node plugin + client action script bundle. |
| examples/plugin-a11y-checker/tsconfig.json | TS config for Solid JSX + bundler resolution. |
| examples/plugin-a11y-checker/src/node/plugin.ts | Example devtools plugin registering an action + emitting a startup log. |
| examples/plugin-a11y-checker/src/node/index.ts | Exports example plugin entrypoint. |
| examples/plugin-a11y-checker/src/client/run-axe.ts | Client action script running axe and emitting logs/toasts. |
| examples/plugin-a11y-checker/playground/vite.config.ts | Playground config wiring DevTools + Solid + example plugin + UnoCSS. |
| examples/plugin-a11y-checker/playground/src/vite-env.d.ts | Vite client typings for playground. |
| examples/plugin-a11y-checker/playground/src/main.tsx | Solid playground bootstrap. |
| examples/plugin-a11y-checker/playground/src/App.tsx | Playground page with intentional accessibility issues. |
| examples/plugin-a11y-checker/playground/index.html | Playground HTML entry. |
| examples/plugin-a11y-checker/package.json | Example package definition + deps/scripts. |
| docs/kit/logs.md | New documentation for the Logs system. |
| docs/.vitepress/config.ts | Adds Logs page to kit navigation. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
packages/core/src/node/rpc/index.ts:76
- The
DevToolsRpcClientFunctionssection is marked// @keep-sorted, but there are extra blank lines around the newly addeddevtoolskit:internal:logs:updatedentry. If you have an auto-sort/lint rule for these blocks, consider removing the blank lines to keep the formatting consistent.
// @keep-sorted
export interface DevToolsRpcClientFunctions {
'devtoolskit:internal:logs:updated': () => Promise<void>
'devtoolskit:internal:rpc:client-state:patch': (key: string, patches: SharedStatePatch[], syncId: string) => Promise<void>
'devtoolskit:internal:rpc:client-state:updated': (key: string, fullState: any, syncId: string) => Promise<void>
'devtoolskit:internal:terminals:stream-chunk': (data: DevToolsTerminalSessionStreamChunkEvent) => Promise<void>
'devtoolskit:internal:terminals:updated': () => Promise<void>
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return { | ||
| async handler(): Promise<DevToolsLogEntry[]> { | ||
| return Array.from(context.logs.entries.values()) | ||
| }, |
There was a problem hiding this comment.
logs:list returns DevToolsLogEntry objects directly from the host map. Since DevToolsLogEntry.autofix can be a function (per kit types/docs), this RPC will fail at runtime because the WS RPC layer uses structured cloning (functions are not cloneable). Consider mapping entries to a serializable shape (e.g., strip function autofix and replace with a boolean/descriptor) before returning.
packages/kit/src/types/logs.ts
Outdated
|
|
||
| export type DevToolsLogAutofix = DevToolsLogAutofixRpc | (() => void | Promise<void>) | ||
|
|
There was a problem hiding this comment.
DevToolsLogAutofix includes a function type, but DevToolsLogEntry is used as an RPC payload (e.g. devtoolskit:internal:logs:list) over the WS transport which uses structured cloning; functions will cause DataCloneError. Suggest splitting server-only autofix callbacks from the serializable log entry shape (e.g., store callbacks in the host and expose only an autofix descriptor/flag to clients).
packages/core/src/node/host-logs.ts
Outdated
| source: (input as any).source ?? 'unknown', | ||
| } | ||
|
|
There was a problem hiding this comment.
source is defaulting to 'unknown' unless callers sneak it into DevToolsLogEntryInput via any. Since createDevToolsContext passes the raw context into plugin.devtools.setup, plugin authors calling context.logs.add(...) will produce logs with source: unknown (and the new a11y example does this). Consider making add accept an explicit source (or creating a plugin-scoped proxy context that auto-injects plugin.name/dock entry id) to avoid relying on any and to match the docs.
| source: (input as any).source ?? 'unknown', | |
| } | |
| } | |
| if (!entry.source) | |
| entry.source = 'unknown' |
| setup: (context) => { | ||
| return { | ||
| async handler(input: DevToolsLogEntryInput, source: string): Promise<DevToolsLogEntry> { | ||
| return context.logs.add(Object.assign(input, { source })) |
There was a problem hiding this comment.
This mutates the RPC input object via Object.assign(input, { source }). That can create surprising side-effects if the caller reuses input (and it also bypasses the DevToolsLogEntryInput type by injecting source). Prefer constructing a new object (and/or updating the host API to accept source as a separate parameter) so the handler is side-effect free.
| return context.logs.add(Object.assign(input, { source })) | |
| return context.logs.add({ ...(input as DevToolsLogEntry), source } as DevToolsLogEntry) |
| const prevEntryMap = new Map<string, DevToolsLogEntry>() | ||
|
|
||
| async function updateLogs() { | ||
| const logs = await context.rpc.call('devtoolskit:internal:logs:list') | ||
| let newCount = 0 | ||
|
|
||
| for (const entry of logs) { | ||
| const prev = prevEntryMap.get(entry.id) | ||
| if (!prev) { | ||
| // New entry | ||
| newCount++ | ||
| if (entry.notify) | ||
| addToast(entry) | ||
| } | ||
| else if (entry.notify && entry !== prev && JSON.stringify(entry) !== JSON.stringify(prev)) { | ||
| // Updated entry with notify flag — update the toast | ||
| addToast(entry) | ||
| } | ||
| } | ||
|
|
||
| state.entries = logs | ||
| state.unreadCount += newCount | ||
|
|
||
| prevEntryMap.clear() | ||
| for (const entry of logs) | ||
| prevEntryMap.set(entry.id, entry) |
There was a problem hiding this comment.
Using JSON.stringify to detect changes can get expensive with many log entries and frequent updates (and it re-serializes both objects every time). Consider tracking a cheap revision key per entry (e.g., add an updatedAt/revision field on the server) or caching a serialized string/hash in prevEntryMap so comparisons are O(1) without repeated full serialization.
| const prevEntryMap = new Map<string, DevToolsLogEntry>() | |
| async function updateLogs() { | |
| const logs = await context.rpc.call('devtoolskit:internal:logs:list') | |
| let newCount = 0 | |
| for (const entry of logs) { | |
| const prev = prevEntryMap.get(entry.id) | |
| if (!prev) { | |
| // New entry | |
| newCount++ | |
| if (entry.notify) | |
| addToast(entry) | |
| } | |
| else if (entry.notify && entry !== prev && JSON.stringify(entry) !== JSON.stringify(prev)) { | |
| // Updated entry with notify flag — update the toast | |
| addToast(entry) | |
| } | |
| } | |
| state.entries = logs | |
| state.unreadCount += newCount | |
| prevEntryMap.clear() | |
| for (const entry of logs) | |
| prevEntryMap.set(entry.id, entry) | |
| const prevEntryMap = new Map<string, { entry: DevToolsLogEntry; serialized: string }>() | |
| async function updateLogs() { | |
| const logs = await context.rpc.call('devtoolskit:internal:logs:list') | |
| let newCount = 0 | |
| const nextPrevEntries: { id: string; entry: DevToolsLogEntry; serialized: string }[] = [] | |
| for (const entry of logs) { | |
| const prev = prevEntryMap.get(entry.id) | |
| let serialized: string | undefined | |
| if (!prev) { | |
| // New entry | |
| newCount++ | |
| if (entry.notify) | |
| addToast(entry) | |
| } | |
| else if (entry.notify && entry !== prev.entry) { | |
| // Updated entry with notify flag — compare against cached serialized value | |
| serialized = JSON.stringify(entry) | |
| if (serialized !== prev.serialized) | |
| addToast(entry) | |
| } | |
| if (!serialized) | |
| serialized = JSON.stringify(entry) | |
| nextPrevEntries.push({ | |
| id: entry.id, | |
| entry, | |
| serialized, | |
| }) | |
| } | |
| state.entries = logs | |
| state.unreadCount += newCount | |
| prevEntryMap.clear() | |
| for (const { id, entry, serialized } of nextPrevEntries) | |
| prevEntryMap.set(id, { entry, serialized }) |
| | 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 | | ||
|
|
There was a problem hiding this comment.
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.
docs/kit/logs.md
Outdated
| 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() | ||
| }, | ||
| }, | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
No description provided.