Skip to content

Commit dbf92e1

Browse files
committed
feat: logs api
1 parent e9b2be3 commit dbf92e1

File tree

7 files changed

+142
-66
lines changed

7 files changed

+142
-66
lines changed

docs/kit/logs.md

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -25,48 +25,41 @@ The Logs system allows plugins to emit structured log entries from both the serv
2525
| `labels` | `string[]` | No | Tags for filtering |
2626
| `autoDismiss` | `number` | No | Time in ms to auto-dismiss the toast (default: 5000) |
2727
| `autoDelete` | `number` | No | Time in ms to auto-delete the log entry |
28+
| `status` | `'loading' \| 'idle'` | No | Status indicator (shows spinner when `'loading'`) |
29+
| `id` | `string` | No | Explicit id for deduplication — re-adding with the same id updates the existing entry |
2830

2931
## Server-Side Usage
3032

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

3335
```ts
3436
export function myPlugin() {
3537
return {
3638
name: 'my-plugin',
3739
devtools: {
38-
setup(context) {
40+
async setup(context) {
3941
// Simple log
40-
context.logs.add({
42+
await context.logs.add({
4143
message: 'Plugin initialized',
4244
level: 'info',
4345
})
4446

45-
// Warning with file position
46-
context.logs.add({
47-
message: 'Deprecated API usage detected',
48-
level: 'warn',
49-
description: 'The `foo()` API is deprecated. Use `bar()` instead.',
50-
filePosition: { file: 'src/app.ts', line: 42, column: 5 },
51-
category: 'lint',
52-
})
53-
54-
// Error with stack trace
55-
context.logs.add({
56-
message: 'Build failed',
57-
level: 'error',
58-
stacktrace: error.stack,
59-
category: 'build',
47+
// Log with loading state, then update
48+
const log = await context.logs.add({
49+
message: 'Building...',
50+
level: 'info',
51+
status: 'loading',
6052
})
6153

62-
// Short notification
63-
context.logs.add({
64-
message: 'Configuration reloaded',
54+
// Later, update via the handle
55+
await log.update({
56+
message: 'Build complete',
6557
level: 'success',
66-
notify: true,
67-
autoDismiss: 3000,
68-
autoDelete: 10000,
58+
status: 'idle',
6959
})
60+
61+
// Or dismiss it
62+
await log.dismiss()
7063
},
7164
},
7265
}
@@ -77,27 +70,54 @@ The `source` field is automatically set to the plugin name.
7770

7871
## Client-Side Usage
7972

80-
From client-side code (running in the user's browser), emit logs via the RPC client:
73+
In dock action scripts, use `context.logs` — the same API as on the server:
8174

8275
```ts
83-
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
76+
import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'
77+
78+
export default async function (context: DockClientScriptContext) {
79+
const log1 = await context.logs.add({
80+
message: 'Running audit...',
81+
level: 'info',
82+
status: 'loading',
83+
notify: true,
84+
category: 'a11y',
85+
})
86+
87+
// ... do work ...
88+
89+
await log1.update({
90+
message: 'Audit complete — 3 issues found',
91+
level: 'warn',
92+
status: 'idle',
93+
})
94+
}
95+
```
8496

85-
const client = await getDevToolsRpcClient()
97+
The `source` is automatically set to the dock entry id.
8698

87-
await client.call('devtoolskit:internal:logs:add', {
88-
message: 'Accessibility issue: missing alt text',
89-
level: 'warn',
90-
description: 'Images should have alt attributes for screen readers.',
91-
elementPosition: {
92-
selector: 'img.hero-image',
93-
description: 'Hero image in the header',
94-
},
95-
category: 'a11y',
96-
labels: ['wcag-2.1', 'images'],
97-
}, 'axe-plugin')
98-
```
99+
## Log Handle
100+
101+
`context.logs.add()` returns a `DevToolsLogHandle` with:
102+
103+
| Property/Method | Description |
104+
|-----------------|-------------|
105+
| `handle.id` | The log entry id |
106+
| `handle.entry` | The current `DevToolsLogEntry` data |
107+
| `handle.update(patch)` | Partially update the log entry |
108+
| `handle.dismiss()` | Remove the log entry |
109+
110+
## Deduplication
99111

100-
The second argument to `logs:add` is the `source` identifier.
112+
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:
113+
114+
```ts
115+
// First call creates the entry
116+
await context.logs.add({ id: 'my-scan', message: 'Scanning...', level: 'info', status: 'loading' })
117+
118+
// Second call with same id updates it
119+
await context.logs.add({ id: 'my-scan', message: 'Scan complete', level: 'success', status: 'idle' })
120+
```
101121

102122
## Autofix
103123

@@ -118,7 +138,7 @@ context.rpc.register(defineRpcFunction({
118138
}),
119139
}))
120140

121-
context.logs.add({
141+
await context.logs.add({
122142
message: 'Deprecated API usage',
123143
level: 'warn',
124144
autofix: { type: 'rpc', name: 'my-plugin:fix-deprecated-api' },
@@ -130,7 +150,7 @@ context.logs.add({
130150
For server-side plugins, you can pass a function directly:
131151

132152
```ts
133-
context.logs.add({
153+
await context.logs.add({
134154
message: 'Missing configuration',
135155
level: 'warn',
136156
autofix: async () => {
@@ -144,7 +164,7 @@ context.logs.add({
144164
Set `notify: true` to show the log entry as a toast notification overlay. Toasts appear regardless of whether the Logs panel is open.
145165

146166
```ts
147-
context.logs.add({
167+
await context.logs.add({
148168
message: 'URL copied to clipboard',
149169
level: 'success',
150170
notify: true,
@@ -157,11 +177,11 @@ The default auto-dismiss time for toasts is 5 seconds.
157177
## Managing Logs
158178

159179
```ts
160-
// Remove a specific log
161-
context.logs.remove(entry.id)
180+
// Remove a specific log by id
181+
await context.logs.remove(entryId)
162182

163183
// Clear all logs
164-
context.logs.clear()
184+
await context.logs.clear()
165185
```
166186

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

examples/plugin-a11y-checker/src/client/run-axe.ts

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { DevToolsLogLevel } from '@vitejs/devtools-kit'
22
import type { DockClientScriptContext } from '@vitejs/devtools-kit/client'
33
import axe from 'axe-core'
44

5-
const SOURCE = 'a11y-checker'
65
const SUMMARY_LOG_ID = 'a11y-checker-summary'
76

87
function impactToLevel(impact: string | undefined | null): DevToolsLogLevel {
@@ -19,17 +18,17 @@ function impactToLevel(impact: string | undefined | null): DevToolsLogLevel {
1918
}
2019

2120
export default async function runA11yCheck(context: DockClientScriptContext): Promise<void> {
22-
const { rpc } = context
21+
const { logs } = context
2322

2423
// Show loading state
25-
await rpc.call('devtoolskit:internal:logs:add', {
24+
const summary = await logs.add({
2625
id: SUMMARY_LOG_ID,
2726
message: 'Running accessibility audit...',
28-
level: 'info' as DevToolsLogLevel,
27+
level: 'info',
2928
category: 'a11y',
3029
status: 'loading',
3130
notify: true,
32-
}, SOURCE)
31+
})
3332

3433
try {
3534
const results = await axe.run(document)
@@ -38,7 +37,7 @@ export default async function runA11yCheck(context: DockClientScriptContext): Pr
3837
const level = impactToLevel(violation.impact)
3938
const firstNode = violation.nodes[0]
4039

41-
await rpc.call('devtoolskit:internal:logs:add', {
40+
await logs.add({
4241
id: `a11y-violation-${violation.id}`,
4342
message: violation.description,
4443
level,
@@ -51,35 +50,31 @@ export default async function runA11yCheck(context: DockClientScriptContext): Pr
5150
description: firstNode.html,
5251
}
5352
: undefined,
54-
}, SOURCE)
53+
})
5554
}
5655

5756
const violationCount = results.violations.length
5857
const passCount = results.passes.length
5958

60-
// Update the summary log (dedup by id)
61-
await rpc.call('devtoolskit:internal:logs:add', {
62-
id: SUMMARY_LOG_ID,
59+
// Update the summary log via handle
60+
await summary.update({
6361
message: violationCount > 0
6462
? `Found ${violationCount} violation${violationCount > 1 ? 's' : ''}, ${passCount} passed`
6563
: `All ${passCount} checks passed`,
66-
level: (violationCount > 0 ? 'warn' : 'success') as DevToolsLogLevel,
67-
category: 'a11y',
64+
level: violationCount > 0 ? 'warn' : 'success',
6865
status: 'idle',
6966
notify: true,
7067
autoDismiss: 4000,
71-
}, SOURCE)
68+
})
7269
}
7370
catch (err) {
74-
// Update the summary log with error
75-
await rpc.call('devtoolskit:internal:logs:add', {
76-
id: SUMMARY_LOG_ID,
71+
// Update the summary log with error via handle
72+
await summary.update({
7773
message: 'A11y audit failed',
78-
level: 'error' as DevToolsLogLevel,
74+
level: 'error',
7975
description: String(err),
80-
category: 'a11y',
8176
status: 'idle',
8277
notify: true,
83-
}, SOURCE)
78+
})
8479
}
8580
}

packages/core/src/client/webcomponents/state/context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { computed, markRaw, reactive, ref, toRefs, watchEffect } from 'vue'
77
import { BUILTIN_ENTRIES } from '../constants'
88
import { docksGroupByCategories } from './dock-settings'
99
import { createDockEntryState, DEFAULT_DOCK_PANEL_STORE, sharedStateToRef, useDocksEntries } from './docks'
10+
import { createClientLogsClient } from './logs-client'
1011
import { registerMainFrameDockActionHandler, requestDockPopupOpen, triggerMainFrameDockAction } from './popup'
1112
import { executeSetupScript } from './setup-script'
1213

@@ -80,6 +81,7 @@ export async function createDocksContext(
8081
const scriptContext: DockClientScriptContext = reactive({
8182
...toRefs(docksContext) as any,
8283
current,
84+
logs: createClientLogsClient(rpc, entry.id),
8385
})
8486
await executeSetupScript(entry, scriptContext)
8587
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { DevToolsLogEntry, DevToolsLogEntryInput, DevToolsLogHandle, DevToolsLogsClient } from '@vitejs/devtools-kit'
2+
import type { DevToolsRpcClient } from '@vitejs/devtools-kit/client'
3+
4+
function createRpcHandle(rpc: DevToolsRpcClient, initialEntry: DevToolsLogEntry): DevToolsLogHandle {
5+
let entry = initialEntry
6+
return {
7+
get entry() { return entry },
8+
get id() { return entry.id },
9+
async update(patch: Partial<DevToolsLogEntryInput>) {
10+
const updated = await rpc.call('devtoolskit:internal:logs:update', entry.id, patch)
11+
if (updated)
12+
entry = updated
13+
return updated ?? undefined
14+
},
15+
async dismiss() {
16+
await rpc.call('devtoolskit:internal:logs:remove', entry.id)
17+
},
18+
}
19+
}
20+
21+
export function createClientLogsClient(rpc: DevToolsRpcClient, source: string): DevToolsLogsClient {
22+
return {
23+
async add(input: DevToolsLogEntryInput): Promise<DevToolsLogHandle> {
24+
const entry = await rpc.call('devtoolskit:internal:logs:add', input, source)
25+
return createRpcHandle(rpc, entry)
26+
},
27+
async remove(id: string): Promise<void> {
28+
await rpc.call('devtoolskit:internal:logs:remove', id)
29+
},
30+
async clear(): Promise<void> {
31+
await rpc.call('devtoolskit:internal:logs:clear')
32+
},
33+
}
34+
}

packages/kit/src/client/client-script.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { DevToolsLogsClient } from '../types/logs'
12
import type { DockEntryState, DocksContext } from './docks'
23

34
/**
@@ -8,4 +9,8 @@ export interface DockClientScriptContext extends DocksContext {
89
* The state of the current dock entry
910
*/
1011
current: DockEntryState
12+
/**
13+
* Logs client scoped to this dock entry's source
14+
*/
15+
logs: DevToolsLogsClient
1116
}

packages/kit/src/types/logs.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ export type DevToolsLogEntryInput = Omit<DevToolsLogEntry, 'id' | 'timestamp' |
106106
timestamp?: number
107107
}
108108

109+
export interface DevToolsLogHandle {
110+
/** The underlying log entry data */
111+
readonly entry: DevToolsLogEntry
112+
/** Shortcut to entry.id */
113+
readonly id: string
114+
/** Partial update of this log entry */
115+
update: (patch: Partial<DevToolsLogEntryInput>) => Promise<DevToolsLogEntry | undefined>
116+
/** Remove this log entry */
117+
dismiss: () => Promise<void>
118+
}
119+
120+
export interface DevToolsLogsClient {
121+
/** Add a log entry, returns a handle for subsequent updates/dismissal */
122+
add: (input: DevToolsLogEntryInput) => Promise<DevToolsLogHandle>
123+
/** Remove a log entry by id */
124+
remove: (id: string) => Promise<void>
125+
/** Clear all log entries */
126+
clear: () => Promise<void>
127+
}
128+
109129
export interface DevToolsLogsHost {
110130
readonly entries: Map<string, DevToolsLogEntry>
111131
readonly events: EventEmitter<{

packages/kit/src/types/vite-plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export interface DevToolsNodeContext {
6363
*/
6464
terminals: DevToolsTerminalHost
6565
/**
66-
* Logs host, for emitting structured log entries
66+
* Logs host, for emitting and managing structured log entries
6767
*/
6868
logs: DevToolsLogsHost
6969
}

0 commit comments

Comments
 (0)