|
| 1 | +--- |
| 2 | +outline: deep |
| 3 | +--- |
| 4 | + |
| 5 | +# Structured Diagnostics |
| 6 | + |
| 7 | +`ctx.diagnostics` is a thin layer over [`logs-sdk`](https://github.com/vercel-labs/logs-sdk) that lets integrations register their own coded errors and warnings into a shared logger — without taking a direct dependency on `logs-sdk`. |
| 8 | + |
| 9 | +Use it for *author-defined coded diagnostics* (errors, warnings, deprecations) that have 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) instead. |
| 10 | + |
| 11 | +| Surface | Purpose | Example | |
| 12 | +|---------|---------|---------| |
| 13 | +| `ctx.diagnostics` | Coded errors and warnings emitted from node-side plugin code | `MYP0001: Plugin foo not configured` | |
| 14 | +| [`ctx.messages`](./messages) | Free-form, user-facing notifications shown in the Messages panel | `'Audit complete — 3 issues found'` | |
| 15 | + |
| 16 | +## Shape |
| 17 | + |
| 18 | +```ts |
| 19 | +interface DevToolsDiagnosticsHost { |
| 20 | + /** Combined logs-sdk Logger across all registered diagnostics. */ |
| 21 | + readonly logger: Logger |
| 22 | + |
| 23 | + /** Register additional diagnostic definitions. */ |
| 24 | + register: (definitions: DiagnosticsResult) => void |
| 25 | + |
| 26 | + /** Re-export of logs-sdk's `defineDiagnostics`. */ |
| 27 | + defineDiagnostics: typeof defineDiagnostics |
| 28 | + |
| 29 | + /** Re-export of logs-sdk's `createLogger`. */ |
| 30 | + createLogger: typeof createLogger |
| 31 | +} |
| 32 | +``` |
| 33 | + |
| 34 | +The host ships pre-seeded with devframe's own `DF*` codes, plus the host package's codes (`DTK*` for `@vitejs/devtools`, etc.). Call `register()` to add your own. |
| 35 | + |
| 36 | +## Register Your Own Codes |
| 37 | + |
| 38 | +```ts |
| 39 | +export function MyPlugin(): PluginWithDevTools { |
| 40 | + return { |
| 41 | + name: 'my-plugin', |
| 42 | + devtools: { |
| 43 | + setup(ctx) { |
| 44 | + const myDiagnostics = ctx.diagnostics.defineDiagnostics({ |
| 45 | + docsBase: 'https://example.com/errors', |
| 46 | + codes: { |
| 47 | + MYP0001: { |
| 48 | + message: (p: { name: string }) => `Plugin "${p.name}" is not configured`, |
| 49 | + hint: 'Add the plugin to your `vite.config.ts` and pass an options object.', |
| 50 | + }, |
| 51 | + MYP0002: { |
| 52 | + message: 'Cache directory missing — running cold.', |
| 53 | + level: 'warn', |
| 54 | + }, |
| 55 | + }, |
| 56 | + }) |
| 57 | + |
| 58 | + ctx.diagnostics.register(myDiagnostics) |
| 59 | + |
| 60 | + // Now you can emit codes through the shared logger: |
| 61 | + ctx.diagnostics.logger.MYP0002().log() |
| 62 | + }, |
| 63 | + }, |
| 64 | + } |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +## Code Conventions |
| 69 | + |
| 70 | +Use a **4-letter prefix + 4-digit number** for your codes (e.g. `MYP0001`). Pick a prefix that's specific to your plugin or tool — short enough to type, distinctive enough not to collide with other integrations. |
| 71 | + |
| 72 | +Prefixes already in use in this monorepo: |
| 73 | + |
| 74 | +| Prefix | Owner | |
| 75 | +|--------|-------| |
| 76 | +| `DF` | `devframe` | |
| 77 | +| `DTK` | `@vitejs/devtools` (Vite-specific) | |
| 78 | +| `RDDT` | `@vitejs/devtools-rolldown` | |
| 79 | +| `VDT` | `@vitejs/devtools-vite` (reserved) | |
| 80 | + |
| 81 | +Each definition supports a `message` (string or function), an optional `hint`, an optional `level` (`'error'` / `'warn'` / `'suggestion'` / `'deprecation'` — defaults to `'error'`), and a `docsBase` for generating documentation URLs. See [`logs-sdk`](https://github.com/vercel-labs/logs-sdk) for the full schema. |
| 82 | + |
| 83 | +## Emit a Diagnostic |
| 84 | + |
| 85 | +Each registered code becomes a callable factory on `ctx.diagnostics.logger`. The factory returns an object with `.throw()`, `.warn()`, `.error()`, `.log()`, and `.format()`. |
| 86 | + |
| 87 | +```ts |
| 88 | +// Throw — control flow stops here |
| 89 | +throw ctx.diagnostics.logger.MYP0001({ name: 'foo' }).throw() |
| 90 | + |
| 91 | +// Log without throwing |
| 92 | +ctx.diagnostics.logger.MYP0002().log() |
| 93 | + |
| 94 | +// Override level per call |
| 95 | +ctx.diagnostics.logger.MYP0002().warn() |
| 96 | + |
| 97 | +// Attach a `cause` |
| 98 | +ctx.diagnostics.logger.MYP0001({ name: 'foo' }, { cause: error }).log() |
| 99 | +``` |
| 100 | + |
| 101 | +`.throw()` is also typed `never` — TypeScript will treat the line after it as unreachable, so prefix it with `throw` for control-flow narrowing: |
| 102 | + |
| 103 | +```ts |
| 104 | +throw ctx.diagnostics.logger.MYP0001({ name }).throw() |
| 105 | +``` |
| 106 | + |
| 107 | +## Typed Logger Reference |
| 108 | + |
| 109 | +`ctx.diagnostics.logger` is loosely typed (it covers an unbounded set of registered codes). If you want full type narrowing — e.g. autocompletion for your plugin's specific codes — keep your own typed reference returned from `createLogger`: |
| 110 | + |
| 111 | +```ts |
| 112 | +const myDiagnostics = ctx.diagnostics.defineDiagnostics({ |
| 113 | + docsBase: 'https://example.com/errors', |
| 114 | + codes: { |
| 115 | + MYP0001: { message: (p: { name: string }) => `…${p.name}` }, |
| 116 | + }, |
| 117 | +}) |
| 118 | + |
| 119 | +// Register so the shared logger can also see it |
| 120 | +ctx.diagnostics.register(myDiagnostics) |
| 121 | + |
| 122 | +// Keep a typed reference for your own emit sites |
| 123 | +const logger = ctx.diagnostics.createLogger({ diagnostics: [myDiagnostics] }) |
| 124 | +logger.MYP0001({ name: 'foo' }).warn() |
| 125 | +``` |
| 126 | + |
| 127 | +Both loggers share the formatter and reporter defaults set by the host (ANSI console output). |
| 128 | + |
| 129 | +## Updating the Combined Logger |
| 130 | + |
| 131 | +`ctx.diagnostics.logger` is a *getter* — it always returns the freshest combined logger, rebuilt each time `register()` is called. Don't cache it: |
| 132 | + |
| 133 | +```ts |
| 134 | +// ❌ Stale after a later register() call |
| 135 | +const log = ctx.diagnostics.logger |
| 136 | +log.MYP0001({ name: 'foo' }).log() |
| 137 | + |
| 138 | +// ✅ Always fresh |
| 139 | +ctx.diagnostics.logger.MYP0001({ name: 'foo' }).log() |
| 140 | +``` |
| 141 | + |
| 142 | +If you want a stable reference, use `ctx.diagnostics.createLogger({ diagnostics: [myDiagnostics] })` — that one stays bound to *your* definitions. |
| 143 | + |
| 144 | +## Document Your Codes |
| 145 | + |
| 146 | +Pair each code with a documentation page. devframe and the published Vite DevTools packages follow this layout: |
| 147 | + |
| 148 | +``` |
| 149 | +docs/errors/ |
| 150 | + index.md # Table of all codes |
| 151 | + MYP0001.md # One page per code |
| 152 | + MYP0002.md |
| 153 | +``` |
| 154 | + |
| 155 | +Each page covers the message, cause, example, and fix — see any [DF code page](https://devtools.vite.dev/devframe/errors/) for the canonical template. Set `docsBase` on `defineDiagnostics({...})` so the URL is auto-attached to every emitted diagnostic. |
| 156 | + |
| 157 | +## When to Use What |
| 158 | + |
| 159 | +- **`ctx.diagnostics`** — Coded conditions you want users (or other tools) to be able to look up: misconfiguration, deprecations, validation failures, internal invariants. Always have a docs page. Often `.throw()`. |
| 160 | +- **`ctx.messages`** — User-facing activity surfaces in the DevTools UI: progress indicators, audit results, "URL copied" toasts. No code, no docs URL — just a message and a level. |
| 161 | + |
| 162 | +The two systems are intentionally separate: diagnostics are for tool authors and CI; messages are for the human in front of the DevTools panel. |
0 commit comments