Skip to content

Commit d9b7ebb

Browse files
authored
feat(devframe)!: migrate from logs-sdk to nostics (#19)
1 parent 17c1c74 commit d9b7ebb

31 files changed

Lines changed: 359 additions & 414 deletions

AGENTS.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co
3939
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".
4040

4141
- **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.
42-
- **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.
42+
- **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.
4343
- **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.
4444
- **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.
4545
- **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`.
4646

4747
## Structured Diagnostics (Error Codes)
4848

49-
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.
49+
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.
5050

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

@@ -56,23 +56,23 @@ Prefix: **`DF`**. Codes are sequential 4-digit numbers (e.g. `DF0033`). Check th
5656
<!-- eslint-skip -->
5757
```ts
5858
DF0033: {
59-
message: (p: { name: string }) => `Something went wrong with "${p.name}"`,
60-
hint: 'Optional hint for the user.',
61-
level: 'warn', // defaults to 'error' if omitted
59+
why: (p: { name: string }) => `Something went wrong with "${p.name}"`,
60+
fix: 'Optional resolution hint for the user.',
6261
},
6362
```
6463

65-
2. **Use the logger** at the call site:
64+
2. **Use the diagnostics** at the call site:
6665
```ts
67-
import { logger } from './diagnostics'
66+
import { diagnostics } from './diagnostics'
6867

6968
// For thrown errors — always prefix with `throw` for TypeScript control flow:
70-
throw logger.DF0033({ name }).throw()
69+
throw diagnostics.DF0033.throw({ name })
7170

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

7878
3. **Create a docs page** at `docs/errors/DF0033.md` (when `docs/` lands):

docs/adapters/cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ await createCli(devframe, {
9494
}).parse()
9595
```
9696

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

9999
## Use your own CLI framework
100100

docs/errors/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Devframe uses structured diagnostics to surface actionable warnings and errors a
1010

1111
- Codes follow the pattern **`DF` + 4-digit number** (e.g., `DF0001`).
1212
- Every error page includes the cause, recommended fix, and a reference to the source file that emits it.
13-
- 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.
13+
- 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.
1414

1515
## Devframe (DF)
1616

docs/guide/diagnostics.md

Lines changed: 27 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ outline: deep
44

55
# Structured Diagnostics
66

7-
`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).
7+
`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).
88

99
| Surface | Purpose | Example |
1010
|---------|---------|---------|
@@ -15,17 +15,14 @@ outline: deep
1515

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

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

24-
/** Re-export of logs-sdk's `defineDiagnostics`. */
24+
/** Build a typed diagnostics object with the host's ANSI reporter pre-wired. */
2525
defineDiagnostics: typeof defineDiagnostics
26-
27-
/** Re-export of logs-sdk's `createLogger`. */
28-
createLogger: typeof createLogger
2926
}
3027
```
3128

@@ -43,20 +40,19 @@ export function MyPlugin(): PluginWithDevTools {
4340
docsBase: 'https://example.com/errors',
4441
codes: {
4542
MYP0001: {
46-
message: (p: { name: string }) => `Plugin "${p.name}" is not configured`,
47-
hint: 'Add the plugin to your `vite.config.ts` and pass an options object.',
43+
why: (p: { name: string }) => `Plugin "${p.name}" is not configured`,
44+
fix: 'Add the plugin to your `vite.config.ts` and pass an options object.',
4845
},
4946
MYP0002: {
50-
message: 'Cache directory missing — running cold.',
51-
level: 'warn',
47+
why: 'Cache directory missing — running cold.',
5248
},
5349
},
5450
})
5551

5652
ctx.diagnostics.register(myDiagnostics)
5753

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

79-
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.
75+
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.
8076

8177
## Emit a diagnostic
8278

83-
Each registered code becomes a callable factory on `ctx.diagnostics.logger`. The factory returns an object with `.throw()`, `.warn()`, `.error()`, `.log()`, and `.format()`.
79+
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()`.
8480

8581
```ts
8682
// Throw — control flow stops here
87-
throw ctx.diagnostics.logger.MYP0001({ name: 'foo' }).throw()
83+
throw myDiagnostics.MYP0001.throw({ name: 'foo' })
8884

89-
// Log without throwing
90-
ctx.diagnostics.logger.MYP0002().log()
85+
// Report without throwing (default console method: `warn`)
86+
myDiagnostics.MYP0002.report()
9187

92-
// Override level per call
93-
ctx.diagnostics.logger.MYP0002().warn()
88+
// Override the console method per call
89+
myDiagnostics.MYP0002.report({}, { method: 'error' })
9490

95-
// Attach a `cause`
96-
ctx.diagnostics.logger.MYP0001({ name: 'foo' }, { cause: error }).log()
91+
// Attach a `cause` — merged into the params object
92+
myDiagnostics.MYP0001.throw({ name: 'foo', cause: error })
9793
```
9894

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

10197
```ts
102-
throw ctx.diagnostics.logger.MYP0001({ name }).throw()
98+
throw myDiagnostics.MYP0001.throw({ name })
10399
```
104100

105-
## Typed logger reference
101+
## Typed handle reference
106102

107-
`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`:
103+
`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()`:
108104

109105
```ts
110106
const myDiagnostics = ctx.diagnostics.defineDiagnostics({
111107
docsBase: 'https://example.com/errors',
112108
codes: {
113-
MYP0001: { message: (p: { name: string }) => `…${p.name}` },
109+
MYP0001: { why: (p: { name: string }) => `…${p.name}` },
114110
},
115111
})
116112

117-
// Register so the shared logger can also see it
113+
// Register so the shared lookup can also see it
118114
ctx.diagnostics.register(myDiagnostics)
119115

120-
// Keep a typed reference for your own emit sites
121-
const logger = ctx.diagnostics.createLogger({ diagnostics: [myDiagnostics] })
122-
logger.MYP0001({ name: 'foo' }).warn()
123-
```
124-
125-
Both loggers share the formatter and reporter defaults set by the host (ANSI console output).
126-
127-
## Updating the combined logger
128-
129-
`ctx.diagnostics.logger` is a getter — it returns the freshest combined logger, rebuilt each time `register()` is called. Don't cache it:
130-
131-
```ts
132-
// ❌ Stale after a later register() call
133-
const log = ctx.diagnostics.logger
134-
log.MYP0001({ name: 'foo' }).log()
135-
136-
// ✅ Always fresh
137-
ctx.diagnostics.logger.MYP0001({ name: 'foo' }).log()
116+
// Use the typed handle directly for autocompletion
117+
myDiagnostics.MYP0001.report({ name: 'foo' })
138118
```
139119

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

142122
## Document your codes
143123

docs/guide/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Devframe keeps its surface focused on one tool, so the same definition stays por
2929
| **[Devframe Definition](./devframe-definition)** | One `defineDevframe` call describes your tool once; the adapters deploy it anywhere. |
3030
| **[RPC](./rpc)** | Type-safe bidirectional calls built on birpc + valibot. Supports `query`, `static`, `action`, and `event` types. |
3131
| **[Shared State](./shared-state)** | Observable, patch-synced state that survives reconnects and bridges server ↔ browser. |
32-
| **[Diagnostics](./diagnostics)** | Coded warnings/errors via `logs-sdk` — registered into the host logger so adapters and consumers share the same surface. |
32+
| **[Diagnostics](./diagnostics)** | Coded warnings/errors via `nostics` — registered into the host's shared lookup so adapters and consumers share the same surface. |
3333
| **[Streaming](./streaming)** | One-way (RPC streaming) and two-way (uploads) channel primitives for long-running data. |
3434
| **[When Clauses](./when-clauses)** | VS Code-style conditional expressions for docks, commands, and custom UI. |
3535
| **[Utilities](/helpers/utilities)** | Bundled helpers under `devframe/utils/*` — terminal colors, hashing, editor launch, structured-clone serialization, and more. |

packages/devframe/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@
7777
"birpc": "catalog:deps",
7878
"cac": "catalog:deps",
7979
"h3": "catalog:deps",
80-
"logs-sdk": "catalog:deps",
8180
"mrmime": "catalog:deps",
81+
"nostics": "catalog:deps",
8282
"pathe": "catalog:deps",
8383
"valibot": "catalog:deps",
8484
"ws": "catalog:deps"

packages/devframe/src/adapters/mcp/build-server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '@modelcontextprotocol/sdk/types.js'
1313
import { createHostContext } from 'devframe/node'
1414
import { join } from 'pathe'
15-
import { logger } from '../../node/diagnostics'
15+
import { diagnostics } from '../../node/diagnostics'
1616
import { formatMcpError, stringifyForMcp } from './stringify'
1717
import { valibotArgsToJsonSchema, valibotReturnToJsonSchema } from './to-json-schema'
1818

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

107107
const host: DevToolsHost = {
108108
mountStatic: () => { /* MCP has no static surface */ },
@@ -132,7 +132,7 @@ export async function createMcpServer(
132132
}
133133
catch (error) {
134134
const reason = error instanceof Error ? error.message : String(error)
135-
throw logger.DF0017({ transport, reason }, { cause: error }).throw()
135+
throw diagnostics.DF0017.throw({ transport, reason, cause: error })
136136
}
137137

138138
options.onReady?.({ transport: 'stdio' })

packages/devframe/src/helpers/vite.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { resolve } from 'pathe'
44
import { resolveBasePath } from '../adapters/_shared'
55
import { createDevServer, resolveDevServerPort } from '../adapters/dev'
66
import { DEVTOOLS_CONNECTION_META_FILENAME } from '../constants'
7-
import { logger } from '../node/diagnostics'
7+
import { diagnostics } from '../node/diagnostics'
88

99
export interface ViteDevBridgeOptions {
1010
/**
@@ -106,7 +106,7 @@ export function viteDevBridge(d: DevframeDefinition, options: ViteDevBridgeOptio
106106
})
107107
}
108108
catch (e) {
109-
logger.DF0033({ id: d.id, reason: String(e) }, { cause: e as Error }).log()
109+
diagnostics.DF0033.report({ id: d.id, reason: String(e), cause: e as Error }, { method: 'warn' })
110110
return
111111
}
112112

0 commit comments

Comments
 (0)