Skip to content
Merged
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
24 changes: 12 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co
These reinforce devframe's positioning as "the container for one devtool integration, portable to multiple viewers". When in doubt, err on the side of "devframe provides primitives, the hub provides UX".

- **Single-integration scope.** Devframe describes one tool. If a feature only makes sense when multiple tools share a UI — docking, a unified command palette, cross-tool toasts, terminal aggregation — it belongs in a hub package, not here.
- **Headless by default.** No default startup banners, no opinionated logging to stdout, no default styling. Provide hooks (`onReady`, `cli.configure`, etc.); let the application print its own branding. Structured diagnostics via `logs-sdk` are fine — ad-hoc `console.log`s baked into adapters are not.
- **Headless by default.** No default startup banners, no opinionated logging to stdout, no default styling. Provide hooks (`onReady`, `cli.configure`, etc.); let the application print its own branding. Structured diagnostics via `nostics` are fine — ad-hoc `console.log`s baked into adapters are not.
- **Mount path depends on adapter context.** Given `id: 'foo'`, the default mount path is `/__foo/` for *hosted* adapters (`vite`, `embedded`) and `/` for *standalone* adapters (`cli`, `spa`, `build`). Authors override via `DevframeDefinition.basePath`. Don't hardcode mount paths in adapter code paths that may run standalone.
- **SPAs own their basePath at runtime.** Build SPAs with relative asset paths (`vite.base: './'`); discover the effective base in the browser from the executing script's location / `document.baseURI`. `createBuild` / `createSpa` copy SPA output verbatim — no HTML rewriting, no build-time `--base` injection. The client (`connectDevframe`) resolves `.connection.json` relative to the runtime base automatically.
- **CLI flags compose from both sides.** The `cac` instance backing `createCli` is exposed both to the `DevframeDefinition` (`cli.configure(cli)`) — for capabilities contributed by the tool itself — and to the `createCli` caller — for flags added at the final assembly stage. Parsed flag values are forwarded to `setup(ctx, { flags })`. Never hardcode domain-specific flags into `createCli`.

## 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://www.npmjs.com/package/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.

Prefix: **`DF`**. Codes are sequential 4-digit numbers (e.g. `DF0033`). Check the existing diagnostics file to find the next available number.

Expand All @@ -56,23 +56,23 @@ Prefix: **`DF`**. Codes are sequential 4-digit numbers (e.g. `DF0033`). Check th
<!-- eslint-skip -->
```ts
DF0033: {
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 resolution hint for the user.',
},
```

2. **Use the logger** at the call site:
2. **Use the diagnostics** 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.DF0033({ name }).throw()
throw diagnostics.DF0033.throw({ name })

// For logged warnings/errors (not thrown):
logger.DF0033({ name }).log() // uses definition level
logger.DF0033({ name }).warn() // override to warn
logger.DF0033({ name }, { cause: error }).log() // attach cause
// For reported warnings/errors (not thrown). The default console method is `warn`;
// override with the 2nd-arg reporter options when needed:
diagnostics.DF0033.report({ name }) // console.warn
diagnostics.DF0033.report({ name }, { method: 'error' }) // console.error
diagnostics.DF0033.report({ name, cause: error }, { method: 'warn' }) // attach cause
```

3. **Create a docs page** at `docs/errors/DF0033.md` (when `docs/` lands):
Expand Down
2 changes: 1 addition & 1 deletion docs/adapters/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ await createCli(devframe, {
}).parse()
```

Structured diagnostics (via `logs-sdk`) continue to surface through their normal reporters.
Structured diagnostics (via `nostics`) continue to surface through their normal reporters.

## Use your own CLI framework

Expand Down
2 changes: 1 addition & 1 deletion docs/errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Devframe uses structured diagnostics to surface actionable warnings and errors a

- Codes follow the pattern **`DF` + 4-digit number** (e.g., `DF0001`).
- 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://www.npmjs.com/package/nostics), which provides structured diagnostic codes with docs URLs, ANSI-formatted console output, and pluggable reporters.

## Devframe (DF)

Expand Down
74 changes: 27 additions & 47 deletions docs/guide/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ outline: deep

# Structured Diagnostics

`ctx.diagnostics` is a thin layer over [`logs-sdk`](https://github.com/vercel-labs/logs-sdk) that lets integrations 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 — with 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`](https://devtools.vite.dev/kit/messages).
`ctx.diagnostics` is a thin layer over [`nostics`](https://www.npmjs.com/package/nostics) that lets integrations register coded errors and warnings into a shared lookup without depending on `nostics` directly. Use it for author-defined coded diagnostics — errors, warnings, deprecations — with 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`](https://devtools.vite.dev/kit/messages).

| Surface | Purpose | Example |
|---------|---------|---------|
Expand All @@ -15,17 +15,14 @@ outline: deep

```ts
interface DevToolsDiagnosticsHost {
/** Combined logs-sdk Logger across all registered diagnostics. */
readonly logger: Logger
/** Proxy-backed lookup over every registered code. */
readonly logger: Record<string, DiagnosticHandle>

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

/** Re-export of logs-sdk's `defineDiagnostics`. */
/** Build a typed diagnostics object with the host's ANSI reporter pre-wired. */
defineDiagnostics: typeof defineDiagnostics

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

Expand All @@ -43,20 +40,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(myDiagnostics)

// Now you can emit codes through the shared logger:
ctx.diagnostics.logger.MYP0002().log()
// Emit through the host's shared reporter:
myDiagnostics.MYP0002.report()
},
},
}
Expand All @@ -76,68 +72,52 @@ Prefixes already in use in this monorepo:
| `RDDT` | `@vitejs/devtools-rolldown` |
| `VDT` | `@vitejs/devtools-vite` (reserved) |

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.
Each definition supports a `why` (string or function — the message) and an optional `fix` (string or function — the suggested resolution). The `docsBase` on `defineDiagnostics({...})` auto-attaches the URL to every emitted diagnostic. See [`nostics`](https://www.npmjs.com/package/nostics) for the full schema.

## 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 becomes a `DiagnosticHandle` on the typed result of `defineDiagnostics()` (and through the shared `ctx.diagnostics.logger` lookup). Handles expose `.report()` and `.throw()`.

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

// Log without throwing
ctx.diagnostics.logger.MYP0002().log()
// Report without throwing (default console method: `warn`)
myDiagnostics.MYP0002.report()

// Override level per call
ctx.diagnostics.logger.MYP0002().warn()
// Override the console method per call
myDiagnostics.MYP0002.report({}, { method: 'error' })

// Attach a `cause`
ctx.diagnostics.logger.MYP0001({ name: 'foo' }, { cause: error }).log()
// Attach a `cause` — merged into the params object
myDiagnostics.MYP0001.throw({ name: 'foo', cause: error })
```

`.throw()` is typed `never`, so TypeScript treats the line after as unreachable. Prefix the call with `throw` for control-flow narrowing:

```ts
throw ctx.diagnostics.logger.MYP0001({ name }).throw()
throw myDiagnostics.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 loosely typed — it covers an unbounded set of registered codes, beyond what TypeScript can narrow. For autocompletion on your plugin's specific codes, keep the typed result of `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).

## Updating 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:

```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 for autocompletion
myDiagnostics.MYP0001.report({ name: 'foo' })
```

For a stable reference, use `ctx.diagnostics.createLogger({ diagnostics: [myDiagnostics] })` — that one stays bound to your definitions.
The host's `defineDiagnostics()` pre-wires its ANSI console reporter, so both the typed handle and the shared lookup produce the same output.

## Document your codes

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Devframe keeps its surface focused on one tool, so the same definition stays por
| **[Devframe Definition](./devframe-definition)** | One `defineDevframe` call describes your tool once; the adapters deploy it anywhere. |
| **[RPC](./rpc)** | Type-safe bidirectional calls built on birpc + valibot. Supports `query`, `static`, `action`, and `event` types. |
| **[Shared State](./shared-state)** | Observable, patch-synced state that survives reconnects and bridges server ↔ browser. |
| **[Diagnostics](./diagnostics)** | Coded warnings/errors via `logs-sdk` — registered into the host logger so adapters and consumers share the same surface. |
| **[Diagnostics](./diagnostics)** | Coded warnings/errors via `nostics` — registered into the host's shared lookup so adapters and consumers share the same surface. |
| **[Streaming](./streaming)** | One-way (RPC streaming) and two-way (uploads) channel primitives for long-running data. |
| **[When Clauses](./when-clauses)** | VS Code-style conditional expressions for docks, commands, and custom UI. |
| **[Utilities](/helpers/utilities)** | Bundled helpers under `devframe/utils/*` — terminal colors, hashing, editor launch, structured-clone serialization, and more. |
Expand Down
2 changes: 1 addition & 1 deletion packages/devframe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@
"birpc": "catalog:deps",
"cac": "catalog:deps",
"h3": "catalog:deps",
"logs-sdk": "catalog:deps",
"mrmime": "catalog:deps",
"nostics": "catalog:deps",
"pathe": "catalog:deps",
"valibot": "catalog:deps",
"ws": "catalog:deps"
Expand Down
6 changes: 3 additions & 3 deletions packages/devframe/src/adapters/mcp/build-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '@modelcontextprotocol/sdk/types.js'
import { createHostContext } from 'devframe/node'
import { join } from 'pathe'
import { logger } from '../../node/diagnostics'
import { diagnostics } from '../../node/diagnostics'
import { formatMcpError, stringifyForMcp } from './stringify'
import { valibotArgsToJsonSchema, valibotReturnToJsonSchema } from './to-json-schema'

Expand Down Expand Up @@ -102,7 +102,7 @@ export async function createMcpServer(
): Promise<McpServerHandle> {
const transport = options.transport ?? 'stdio'
if (transport !== 'stdio')
throw logger.DF0017({ transport, reason: 'Only stdio transport is supported in this release.' }).throw()
throw diagnostics.DF0017.throw({ transport, reason: 'Only stdio transport is supported in this release.' })

const host: DevToolsHost = {
mountStatic: () => { /* MCP has no static surface */ },
Expand Down Expand Up @@ -132,7 +132,7 @@ export async function createMcpServer(
}
catch (error) {
const reason = error instanceof Error ? error.message : String(error)
throw logger.DF0017({ transport, reason }, { cause: error }).throw()
throw diagnostics.DF0017.throw({ transport, reason, cause: error })
}

options.onReady?.({ transport: 'stdio' })
Expand Down
4 changes: 2 additions & 2 deletions packages/devframe/src/helpers/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { resolve } from 'pathe'
import { resolveBasePath } from '../adapters/_shared'
import { createDevServer, resolveDevServerPort } from '../adapters/dev'
import { DEVTOOLS_CONNECTION_META_FILENAME } from '../constants'
import { logger } from '../node/diagnostics'
import { diagnostics } from '../node/diagnostics'

export interface ViteDevBridgeOptions {
/**
Expand Down Expand Up @@ -106,7 +106,7 @@ export function viteDevBridge(d: DevframeDefinition, options: ViteDevBridgeOptio
})
}
catch (e) {
logger.DF0033({ id: d.id, reason: String(e) }, { cause: e as Error }).log()
diagnostics.DF0033.report({ id: d.id, reason: String(e), cause: e as Error }, { method: 'warn' })
return
}

Expand Down
Loading
Loading