Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
20 changes: 9 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ The kit is the integration hub. When adding to it, the question is "does this he

## Structured Diagnostics (Error Codes)

All node-side warnings and errors use structured diagnostics via [`logs-sdk`](https://github.com/vercel-labs/logs-sdk). Never use raw `console.warn`, `console.error`, or `throw new Error` with ad-hoc messages in node-side code — always define a coded diagnostic.
All node-side warnings and errors use structured diagnostics via [`nostics`](https://github.com/vercel-labs/nostics). Never use raw `console.warn`, `console.error`, or `throw new Error` with ad-hoc messages in node-side code — always define a coded diagnostic.

### Code prefixes

Expand All @@ -115,23 +115,21 @@ Codes are sequential 4-digit numbers per prefix (e.g. `DTK0033`, `RDDT0003`). Ch
```txt
// diagnostics.ts
DTK0033: {
message: (p: { name: string }) => `Something went wrong with "${p.name}"`,
hint: 'Optional hint for the user.',
level: 'warn', // defaults to 'error' if omitted
why: (p: { name: string }) => `Something went wrong with "${p.name}"`,
fix: 'Optional remediation hint for the user.',
},
```

2. **Use the logger** at the call site:
2. **Emit the diagnostic** at the call site:
```ts
import { logger } from './diagnostics'
import { diagnostics } from './diagnostics'

// For thrown errors — always prefix with `throw` for TypeScript control flow:
throw logger.DTK0033({ name }).throw()
throw diagnostics.DTK0033.throw({ name })

// For logged warnings/errors (not thrown):
logger.DTK0033({ name }).log() // uses definition level
logger.DTK0033({ name }).warn() // override to warn
logger.DTK0033({ name }, { cause: error }).log() // attach cause
// For reported (non-thrown) diagnostics:
diagnostics.DTK0033.report({ name })
diagnostics.DTK0033.report({ name, cause: error }) // attach cause via params
```

3. **Create a docs page** at `docs/errors/DTK0033.md`:
Expand Down
2 changes: 1 addition & 1 deletion devframe
Submodule devframe updated 51 files
+13 −13 AGENTS.md
+1 −0 alias.ts
+1 −1 docs/adapters/cli.md
+1 −1 docs/errors/index.md
+27 −47 docs/guide/diagnostics.md
+1 −1 docs/guide/index.md
+1 −1 examples/files-inspector/package.json
+1 −1 examples/streaming-chat/package.json
+2 −2 package.json
+3 −2 packages/devframe/package.json
+114 −0 packages/devframe/scripts/check-client-dist.ts
+3 −3 packages/devframe/src/adapters/mcp/build-server.ts
+2 −2 packages/devframe/src/helpers/vite.ts
+24 −33 packages/devframe/src/node/diagnostics.ts
+6 −6 packages/devframe/src/node/host-agent.ts
+19 −25 packages/devframe/src/node/host-diagnostics.ts
+3 −3 packages/devframe/src/node/host-functions.ts
+2 −2 packages/devframe/src/node/host-views.ts
+2 −2 packages/devframe/src/node/rpc-shared-state.ts
+5 −5 packages/devframe/src/node/rpc-streaming.ts
+2 −2 packages/devframe/src/node/storage.ts
+5 −5 packages/devframe/src/rpc/collector.ts
+17 −21 packages/devframe/src/rpc/diagnostics.ts
+278 −0 packages/devframe/src/rpc/dump/collect.ts
+3 −278 packages/devframe/src/rpc/dump/index.ts
+3 −2 packages/devframe/src/rpc/dump/static.ts
+2 −2 packages/devframe/src/rpc/handler.ts
+50 −3 packages/devframe/src/rpc/index.ts
+2 −2 packages/devframe/src/rpc/serialization.ts
+3 −3 packages/devframe/src/rpc/validation.ts
+2 −2 packages/devframe/src/types/context.ts
+46 −37 packages/devframe/src/types/diagnostics.ts
+12 −0 packages/devframe/src/utils/diagnostics-reporter.ts
+133 −92 packages/devframe/tsdown.config.ts
+10 −1 packages/nuxt/nuxt.d.ts
+6 −3 packages/nuxt/package.json
+9 −5 packages/nuxt/src/module.ts
+1 −2 packages/nuxt/src/runtime/plugin.client.ts
+20 −3 packages/nuxt/tsdown.config.ts
+146 −182 pnpm-lock.yaml
+2 −1 pnpm-workspace.yaml
+1 −1 skills/devframe/SKILL.md
+3 −16 tests/__snapshots__/tsnapi/@devframes/nuxt/index.snapshot.d.ts
+1 −0 tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts
+5 −8 tests/__snapshots__/tsnapi/devframe/node.snapshot.d.ts
+38 −8 tests/__snapshots__/tsnapi/devframe/rpc.snapshot.js
+18 −0 tests/__snapshots__/tsnapi/devframe/rpc/dump.snapshot.d.ts
+11 −0 tests/__snapshots__/tsnapi/devframe/rpc/dump.snapshot.js
+13 −3 tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-client.snapshot.d.ts
+1 −0 tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts
+3 −0 tsconfig.base.json
2 changes: 1 addition & 1 deletion docs/errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Vite DevTools uses structured diagnostics to surface actionable warnings and err
- Codes follow the pattern **prefix + 4-digit number** (e.g., `DF0001`, `DTK0008`, `RDDT0002`).
- Each prefix maps to a package: `DTK` for `@vitejs/devtools` (Vite-specific pieces), `RDDT` for `@vitejs/devtools-rolldown`. The framework-neutral `devframe` package documents its own `DF`-prefixed codes at the [Devframe docs site](https://devfra.me/errors/).
- Every error page includes the cause, recommended fix, and a reference to the source file that emits it.
- The diagnostics system is powered by [`logs-sdk`](https://github.com/vercel-labs/logs-sdk), which provides structured logging with docs URLs, ANSI-formatted console output, and level-based filtering.
- The diagnostics system is powered by [`nostics`](https://github.com/vercel-labs/nostics), which provides structured diagnostic codes with docs URLs and ANSI-formatted console output.

## DevTools Kit (DTK)

Expand Down
79 changes: 31 additions & 48 deletions docs/kit/diagnostics.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Structured Diagnostics

`ctx.diagnostics` is a thin layer over [`logs-sdk`](https://github.com/vercel-labs/logs-sdk) that lets DevTools plugins register coded errors and warnings into a shared logger without depending on `logs-sdk` directly. Use it for author-defined coded diagnostics — errors, warnings, deprecations — that carry a stable code, a documentation URL, and a structured payload. For free-form runtime output that should appear in the DevTools UI, use [`ctx.messages`](./messages).
`ctx.diagnostics` is a thin layer over [`nostics`](https://github.com/vercel-labs/nostics) that lets DevTools plugins register coded errors and warnings into a shared registry without depending on `nostics` directly. Use it for author-defined coded diagnostics — errors, warnings, deprecations — that carry a stable code, a documentation URL, and a structured payload. For free-form runtime output that should appear in the DevTools UI, use [`ctx.messages`](./messages).

| Surface | Purpose | Example |
|---------|---------|---------|
Expand All @@ -11,17 +11,20 @@

```ts
interface DevToolsDiagnosticsHost {
/** Combined logs-sdk Logger across all registered diagnostics. */
readonly logger: Logger
/**
* Proxy-backed lookup of every registered code by name. Each entry is a
* `nostics` handle with `.report()` and `.throw()` methods.
*/
readonly logger: Record<string, any>

/** Register additional diagnostic definitions. */
register: (definitions: DiagnosticsResult) => void
register: (definitions: Record<string, unknown>) => void

/** Re-export of logs-sdk's `defineDiagnostics`. */
/**
* Mirror of `nostics`'s `defineDiagnostics`, pre-wired with the host's
* ANSI console reporter — plugins typically omit `reporters`.
*/
defineDiagnostics: typeof defineDiagnostics

/** Re-export of logs-sdk's `createLogger`. */
createLogger: typeof createLogger
}
```

Expand All @@ -41,20 +44,19 @@ export function MyPlugin(): PluginWithDevTools {
docsBase: 'https://example.com/errors',
codes: {
MYP0001: {
message: (p: { name: string }) => `Plugin "${p.name}" is not configured`,
hint: 'Add the plugin to your `vite.config.ts` and pass an options object.',
why: (p: { name: string }) => `Plugin "${p.name}" is not configured`,
fix: 'Add the plugin to your `vite.config.ts` and pass an options object.',
},
MYP0002: {
message: 'Cache directory missing — running cold.',
level: 'warn',
why: 'Cache directory missing — running cold.',
},
},
})

ctx.diagnostics.register(diagnostics)

// Now you can emit codes through the shared logger:
ctx.diagnostics.logger.MYP0002().log()
// Emit codes through the shared lookup:
ctx.diagnostics.logger.MYP0002.report()
},
},
}
Expand All @@ -74,68 +76,49 @@ Prefixes used by the in-tree packages:
| `RDDT` | `@vitejs/devtools-rolldown` |
| `VDT` | `@vitejs/devtools-vite` (reserved) |

Each definition supports `message` (string or function), optional `hint`, optional `level` (`'error'` / `'warn'` / `'suggestion'` / `'deprecation'` — defaults to `'error'`), and a `docsBase` for generating documentation URLs.
Each definition supports `why` (string or function returning a string) and an optional `fix` (string or function). A `docsBase` on the definition group auto-attaches a per-code URL to every emitted diagnostic.

## Emit a diagnostic

Each registered code becomes a callable factory on `ctx.diagnostics.logger`. The factory returns an object with `.throw()`, `.warn()`, `.error()`, `.log()`, and `.format()`.
Each registered code is reachable as a property on `ctx.diagnostics.logger`. Every handle exposes `.throw(params)` and `.report(params)`.

```ts
// Throw — control flow stops here
throw ctx.diagnostics.logger.MYP0001({ name: 'foo' }).throw()

// Log without throwing
ctx.diagnostics.logger.MYP0002().log()
throw ctx.diagnostics.logger.MYP0001.throw({ name: 'foo' })

// Override level per call
ctx.diagnostics.logger.MYP0002().warn()
// Report without throwing (goes through the host's reporter)
ctx.diagnostics.logger.MYP0002.report()

// Attach a `cause`
ctx.diagnostics.logger.MYP0001({ name: 'foo' }, { cause: error }).log()
// Attach a `cause` via the params object
ctx.diagnostics.logger.MYP0001.report({ name: 'foo', cause: error })
```

`.throw()` is typed `never`. Prefix the call with `throw` so TypeScript narrows control flow correctly:

```ts
throw ctx.diagnostics.logger.MYP0001({ name }).throw()
throw ctx.diagnostics.logger.MYP0001.throw({ name })
```

## Typed logger reference
## Typed handle reference

`ctx.diagnostics.logger` is loosely typed — it covers an unbounded set of registered codes, beyond what TypeScript can narrow. For autocompletion on your plugin's specific codes, keep a typed reference returned from `createLogger`:
`ctx.diagnostics.logger` is a loosely typed proxy — it covers an unbounded set of registered codes, beyond what TypeScript can narrow. For autocompletion on your plugin's specific codes, keep a reference to the typed handle returned by `defineDiagnostics()`:

```ts
const myDiagnostics = ctx.diagnostics.defineDiagnostics({
docsBase: 'https://example.com/errors',
codes: {
MYP0001: { message: (p: { name: string }) => `…${p.name}` },
MYP0001: { why: (p: { name: string }) => `…${p.name}` },
},
})

// Register so the shared logger can also see it
// Register so the shared lookup can also see it
ctx.diagnostics.register(myDiagnostics)

// Keep a typed reference for your own emit sites
const logger = ctx.diagnostics.createLogger({ diagnostics: [myDiagnostics] })
logger.MYP0001({ name: 'foo' }).warn()
```

Both loggers share the formatter and reporter defaults set by the host (ANSI console output).

## Don't cache the combined logger

`ctx.diagnostics.logger` is a getter — it returns the freshest combined logger, rebuilt each time `register()` is called. Don't cache it across registrations:

```ts
// ❌ Stale after a later register() call
const log = ctx.diagnostics.logger
log.MYP0001({ name: 'foo' }).log()

// ✅ Always fresh
ctx.diagnostics.logger.MYP0001({ name: 'foo' }).log()
// Use the typed handle directly at emit sites
myDiagnostics.MYP0001.report({ name: 'foo' })
```

For a stable reference, use the typed `createLogger` form above.
Both paths share the formatter and reporter defaults set by the host (ANSI console output).

## Document your codes

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@
"chokidar": "catalog:devtools",
"esbuild": "catalog:build",
"eslint": "catalog:devtools",
"logs-sdk": "catalog:deps",
"magic-string": "catalog:build",
"nano-staged": "catalog:devtools",
"nostics": "catalog:deps",
"nuxt": "catalog:build",
"p-limit": "catalog:deps",
"pathe": "catalog:deps",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@
"cac": "catalog:deps",
"devframe": "catalog:deps",
"h3": "catalog:deps",
"logs-sdk": "catalog:deps",
"mlly": "catalog:deps",
"nostics": "catalog:deps",
"obug": "catalog:deps",
"pathe": "catalog:deps",
"perfect-debounce": "catalog:deps",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/node/cli-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { colors as c } from 'devframe/utils/colors'
import { open } from 'devframe/utils/open'
import { resolve } from 'pathe'
import { MARK_NODE } from './constants'
import { logger } from './diagnostics'
import { diagnostics } from './diagnostics'

export interface StartOptions {
root?: string
Expand Down Expand Up @@ -88,5 +88,5 @@ export async function build(options: BuildOptions) {
outDir,
})

logger.DTK0010().log()
diagnostics.DTK0010.report()
}
4 changes: 2 additions & 2 deletions packages/core/src/node/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ResolvedConfig, ViteDevServer } from 'vite'
import { createKitContext, createViteDevToolsHost } from '@vitejs/devtools-kit/node'
import { isObject } from 'devframe/node'
import { createDebug } from 'obug'
import { diagnostics, logger } from './diagnostics'
import { diagnostics } from './diagnostics'
import { builtinRpcDeclarations } from './rpc'

const debugSetup = createDebug('vite:devtools:context:setup')
Expand Down Expand Up @@ -78,7 +78,7 @@ export async function createDevToolsContext(
await plugin.devtools?.setup?.(context)
}
catch (error) {
throw logger.DTK0014({ name: plugin.name }, { cause: error }).throw()
throw diagnostics.DTK0014.throw({ name: plugin.name, cause: error })
}
}

Expand Down
37 changes: 14 additions & 23 deletions packages/core/src/node/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,44 @@
import { colors as c } from 'devframe/utils/colors'
import { consoleReporter, createLogger, defineDiagnostics } from 'logs-sdk'
import { ansiFormatter } from 'logs-sdk/formatters/ansi'
import { defineDiagnostics, reporterLog } from 'nostics'

export const diagnostics = defineDiagnostics({
docsBase: 'https://devtools.vite.dev/errors',
reporters: [reporterLog],
codes: {
DTK0008: {
message: 'Client authentication is disabled. Any browser can connect to the devtools and access your server and filesystem.',
level: 'warn',
why: 'Client authentication is disabled. Any browser can connect to the devtools and access your server and filesystem.',
},
DTK0010: {
message: 'Static build is still experimental and not yet complete. Generated output may be missing features and can change without notice.',
level: 'warn',
why: 'Static build is still experimental and not yet complete. Generated output may be missing features and can change without notice.',
},
DTK0011: {
message: (p: { name: string }) => `RPC error on executing "${p.name}"`,
why: (p: { name: string }) => `RPC error on executing "${p.name}"`,
},
DTK0012: {
message: 'RPC error on executing rpc',
why: 'RPC error on executing rpc',
},
DTK0013: {
message: (p: { name: string, clientId: string }) => `Unauthorized access to method ${JSON.stringify(p.name)} from client [${p.clientId}]`,
why: (p: { name: string, clientId: string }) => `Unauthorized access to method ${JSON.stringify(p.name)} from client [${p.clientId}]`,
},
DTK0014: {
message: (p: { name: string }) => `Error setting up plugin ${p.name}`,
why: (p: { name: string }) => `Error setting up plugin ${p.name}`,
},
DTK0023: {
message: 'viteServer is required in dev mode',
why: 'viteServer is required in dev mode',
},
DTK0028: {
message: 'Path is outside the workspace root',
why: 'Path is outside the workspace root',
},
DTK0029: {
message: 'Path is outside the workspace root',
why: 'Path is outside the workspace root',
},
DTK0030: {
message: (p: { id: string }) => `Dock entry with id "${p.id}" not found`,
why: (p: { id: string }) => `Dock entry with id "${p.id}" not found`,
},
DTK0031: {
message: (p: { id: string }) => `Dock entry with id "${p.id}" is not a launcher`,
why: (p: { id: string }) => `Dock entry with id "${p.id}" is not a launcher`,
},
DTK0032: {
message: (p: { id: string }) => `Error launching dock entry "${p.id}"`,
why: (p: { id: string }) => `Error launching dock entry "${p.id}"`,
},
},
})

export const logger = createLogger({
diagnostics: [diagnostics],
formatter: ansiFormatter(c),
reporters: consoleReporter,
})
8 changes: 4 additions & 4 deletions packages/core/src/node/rpc/internal/docks-on-launch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineRpcFunction } from '@vitejs/devtools-kit'
import { logger } from '../../diagnostics'
import { diagnostics } from '../../diagnostics'

export const docksOnLaunch = defineRpcFunction({
name: 'devtoolskit:internal:docks:on-launch',
Expand All @@ -14,10 +14,10 @@ export const docksOnLaunch = defineRpcFunction({

const entry = context.docks.values().find(entry => entry.id === entryId)
if (!entry) {
throw logger.DTK0030({ id: entryId }).throw()
throw diagnostics.DTK0030.throw({ id: entryId })
}
if (entry.type !== 'launcher') {
throw logger.DTK0031({ id: entryId }).throw()
throw diagnostics.DTK0031.throw({ id: entryId })
}
try {
context.docks.update({
Expand All @@ -43,7 +43,7 @@ export const docksOnLaunch = defineRpcFunction({
return result
}
catch (error) {
logger.DTK0032({ id: entryId }, { cause: error }).log()
diagnostics.DTK0032.report({ id: entryId, cause: error })
context.docks.update({
...entry,
launcher: {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/node/rpc/public/open-in-editor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { relative, resolve } from 'node:path'
import { defineRpcFunction } from '@vitejs/devtools-kit'
import { launchEditor } from 'devframe/utils/launch-editor'
import { logger } from '../../diagnostics'
import { diagnostics } from '../../diagnostics'

export const openInEditor = defineRpcFunction({
name: 'vite:core:open-in-editor',
Expand All @@ -15,7 +15,7 @@ export const openInEditor = defineRpcFunction({

// Prevent escaping the workspace root
if (rel.startsWith('..') || rel.includes('\0')) {
throw logger.DTK0028().throw()
throw diagnostics.DTK0028.throw()
}

launchEditor(resolved)
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/node/rpc/public/open-in-finder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { relative, resolve } from 'node:path'
import { defineRpcFunction } from '@vitejs/devtools-kit'
import { open } from 'devframe/utils/open'
import { logger } from '../../diagnostics'
import { diagnostics } from '../../diagnostics'

export const openInFinder = defineRpcFunction({
name: 'vite:core:open-in-finder',
Expand All @@ -15,7 +15,7 @@ export const openInFinder = defineRpcFunction({

// Ensure the path stays within workspace root
if (rel.startsWith('..') || rel.includes('\0')) {
throw logger.DTK0029().throw()
throw diagnostics.DTK0029.throw()
}

await open(resolved)
Expand Down
Loading
Loading