Skip to content

feat: logs system#193

Merged
antfu merged 11 commits intomainfrom
antfu/logs-system
Mar 12, 2026
Merged

feat: logs system#193
antfu merged 11 commits intomainfrom
antfu/logs-system

Conversation

@antfu
Copy link
Member

@antfu antfu commented Mar 11, 2026

No description provided.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 11, 2026

Open in StackBlitz

@vitejs/devtools

npm i https://pkg.pr.new/@vitejs/devtools@193

@vitejs/devtools-kit

npm i https://pkg.pr.new/@vitejs/devtools-kit@193

@vitejs/devtools-rolldown

npm i https://pkg.pr.new/@vitejs/devtools-rolldown@193

@vitejs/devtools-rpc

npm i https://pkg.pr.new/@vitejs/devtools-rpc@193

@vitejs/devtools-self-inspect

npm i https://pkg.pr.new/@vitejs/devtools-self-inspect@193

commit: f24699d

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 DevToolsRpcClientFunctions section is marked // @keep-sorted, but there are extra blank lines around the newly added devtoolskit:internal:logs:updated entry. 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.

Comment on lines +8 to +11
return {
async handler(): Promise<DevToolsLogEntry[]> {
return Array.from(context.logs.entries.values())
},
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.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +31

export type DevToolsLogAutofix = DevToolsLogAutofixRpc | (() => void | Promise<void>)

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.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +29
source: (input as any).source ?? 'unknown',
}

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.

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.

Suggested change
source: (input as any).source ?? 'unknown',
}
}
if (!entry.source)
entry.source = 'unknown'

Copilot uses AI. Check for mistakes.
setup: (context) => {
return {
async handler(input: DevToolsLogEntryInput, source: string): Promise<DevToolsLogEntry> {
return context.logs.add(Object.assign(input, { source }))
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.

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.

Suggested change
return context.logs.add(Object.assign(input, { source }))
return context.logs.add({ ...(input as DevToolsLogEntry), source } as DevToolsLogEntry)

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +48
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)
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.

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.

Suggested change
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 })

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +30
| 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 |

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.
docs/kit/logs.md Outdated
Comment on lines +33 to +66
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.
@antfu antfu marked this pull request as ready for review March 12, 2026 08:14
@antfu antfu merged commit c661887 into main Mar 12, 2026
9 checks passed
@antfu antfu deleted the antfu/logs-system branch March 12, 2026 08:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants