From nostics
Structured diagnostic code library for JavaScript/TypeScript. Turns errors into typed `Diagnostic` instances with stable codes, docs URLs, and actionable fields. Covers `defineDiagnostics`, reporters, formatters, Vite plugins, and migration from `console.warn`/`Error`.
How this skill is triggered — by the user, by Claude, or both
Slash command
/nostics:nosticsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Every error condition becomes a typed `Diagnostic` (extends `Error`) with a stable code, docs URL, and actionable `fix`. Serializable via `toJSON()`.
Every error condition becomes a typed Diagnostic (extends Error) with a stable code, docs URL, and actionable fix. Serializable via toJSON().
Diagnostic: name (the code), message/why (interpolated text), fix?, docs?, sources? ('file:line:column'), cause?, toJSON(). Throw it, catch it with instanceof Diagnostic, send toJSON() across process boundaries.
Returns one callable handle per code. Calling a handle builds a fresh Diagnostic, fires every reporter in order, and returns it. throw the return value to raise (reporters still run, so a thrown diagnostic also reports).
import { createConsoleReporter, defineDiagnostics } from 'nostics'
const diagnostics = /*#__PURE__*/ defineDiagnostics({
docsBase: (code) => `https://nuxt.com/e/${code.replace('NUXT_', '').toLowerCase()}`,
reporters: [/*#__PURE__*/ createConsoleReporter()],
codes: {
NUXT_B1001: {
why: 'Could not compile template.',
fix: 'Check the template for syntax errors.',
},
NUXT_B2011: {
why: (p: { src: string }) => `Invalid plugin "${p.src}". src option is required.`,
fix: 'Pass a string path or an object with a `src` to `addPlugin()`.',
},
NUXT_W9001: { why: 'message', docs: false }, // per-code: string overrides docsBase, false opts out
},
})
docsBase string | (code) => string | undefined: string appends /${code.toLowerCase()}; function returns the full URL (or undefined to omit).codes: each definition needs why (string | (params) => string, the only required field, becomes Error.message); optional fix (string | (params) => string) and docs (string | false).reporters: fired on every call; optional. Their options types are intersected; required reporter options become required at the call site. Omit it (or pass []) for a catalog whose codes are only ever thrown: the thrown Diagnostic already carries the message, so a console reporter would print it once and surface it again from the uncaught error, a visible duplicate. Keep report-only warnings and fatal throws in separate catalogs when one needs a reporter and the other does not.why and fix are intersected and required at the call site. If why needs { src } and fix needs { date }, the call requires { src, date }.diagnostics.NUXT_B1001() // no params: report only
diagnostics.NUXT_B2011({ src: '/plugins/bad.ts' }) // params first
diagnostics.NUXT_B2011({
src,
cause: originalError,
sources: ['nuxt.config.ts:42:3'],
}) // runtime fields merge in
diagnostics.NUXT_B2011({ src }, { method: 'error' }) // reporter options second
throw diagnostics.NUXT_B2011({ src }) // raise
cause/sources go in the params object; sources matters most for build/config diagnostics where the JS stack points inside the library. Catch with if (err instanceof Diagnostic) then read .name, .message, .fix, .docs.
(diagnostic: Diagnostic, options?: Opts) => void. Declaring a required options type makes the second call-site argument required and typed.
| Reporter | Import | Description |
|---|---|---|
createConsoleReporter(options?) | nostics | console[method](formatter(d)). method defaults 'warn' ('log'|'warn'|'error'), formatter defaults formatDiagnostic; both via options, method also overridable per call. |
createFetchReporter(url) | nostics/reporters/fetch | POSTs diagnostic JSON to the URL; failures swallowed. |
createFileReporter(options?) | nostics/reporters/node | Appends NDJSON to a local file (default .nostics.log). |
createDevReporter() | nostics/reporters/dev | Sends toJSON() to the Vite dev server via import.meta.hot.send(). |
import type { DiagnosticReporter } from 'nostics'
const sentryReporter: DiagnosticReporter = (d) =>
sentry.captureMessage(d.message, { tags: { code: d.name } })
const audited: DiagnosticReporter<{ priority: number }> = (d, o) =>
audit.log({ name: d.name, priority: o.priority })
// → audited makes diagnostics.X({...}, { priority: 1 }) required and type-checked.
| Formatter | Import | Description |
|---|---|---|
formatDiagnostic | nostics | Plain unicode-decorated string (built-in reporters use it). |
ansiFormatter(colors) | nostics/formatters/ansi | Colorized; accepts a Colors interface (red/yellow/cyan/gray/bold/dim, each (s) => string). |
jsonFormatter | nostics/formatters/json | JSON.stringify(diagnostic) via toJSON(). |
formatDiagnostic output, detail order fixed fix → sources → see, missing fields omitted:
[NUXT_B2011] Invalid plugin `/plugins/bad.ts`. src option is required.
├▶ fix: Pass a string path or an object with a `src` to `addPlugin()`.
├▶ sources: nuxt.config.ts:42:3
╰▶ see: https://nuxt.com/e/b2011
@nostics/unplugin, dev dependency)@nostics/unplugin/strip-transform (library authors, build optimization) and @nostics/unplugin/dev-server-collector (app developers, dev-time collection). Both unplugin-based: .vite(), .webpack(), .rollup(), etc.
nosticsStrip marks defineDiagnostics() /*#__PURE__*/ and wraps bare diagnostic expression statements with a NODE_ENV guard so they tree-shake out of production. Option packageName? (default 'nostics'). Throws/returns/assignments stay (they are behavior). For tracking: relative imports, export the catalog directly, no factory wrappers or deep barrels.
/*#__PURE__*/ before defineDiagnostics( and before each reporter factory call inside it (as in every example here), and dev-guard each report-only call site (process.env.NODE_ENV !== 'production' && diagnostics.CODE(p)). Always write the annotations in source; reach for the plugin when report-only call sites are unguarded and you want stripping without touching them.nosticsCollector listens for createDevReporter() diagnostics over the Vite WebSocket and writes them as NDJSON via createFileReporter. Vite-only. Options logFile? (default .nostics.log), debug? (default !!process.env.DEBUG).// vite.config.ts
import { nosticsStrip } from '@nostics/unplugin/strip-transform'
import { nosticsCollector } from '@nostics/unplugin/dev-server-collector'
export default defineConfig({
plugins: [nosticsStrip.vite(), nosticsCollector.vite()],
})
// src/diagnostics.ts — pair the collector with createDevReporter()
import { createConsoleReporter, defineDiagnostics } from 'nostics'
import { createDevReporter } from 'nostics/reporters/dev'
export const diagnostics = /*#__PURE__*/ defineDiagnostics({
reporters: [/*#__PURE__*/ createConsoleReporter(), /*#__PURE__*/ createDevReporter()],
codes: {
/* ... */
},
})
diagnostics.X()) should disappear: nosticsStrip or hand annotations drop them, then the unused catalog tree-shakes.throw/return/assigned/argument) stay, and each keeps the whole catalog reachable, so every why/fix ships. Not every library throws in production: if yours only reports, stripping is enough, stop here.When a library does throw in production, pick defineProdDiagnostics at definition time with a NODE_ENV ternary, so a consumer bundler drops the dev branch (all catalog text):
import { defineDiagnostics, defineProdDiagnostics } from 'nostics'
export const diagnostics =
process.env.NODE_ENV === 'production'
? /*#__PURE__*/ defineProdDiagnostics({ docsBase })
: /*#__PURE__*/ defineDiagnostics({
docsBase,
reporters: [
/* ... */
],
codes: {
/* text */
},
})
The accessed code becomes the message, docs still derives from docsBase, no why/fix text ships. No reporters by default (so a surviving throw doesn't also log and then resurface as the uncaught error); pass reporters to keep prod telemetry. nosticsStrip tracks this ternary like a direct catalog export.
PREFIX_XNNNN (B build, R runtime, C config, D deprecation). Never reuse or reassign a published code.why; provide fix whenever the solution is known (the most actionable field for humans and agents). Use parameterized templates for runtime values, not string concatenation outside the factory.why is the diagnosis, fix is the remedy — split them, don't overlap them. why states only what is wrong; fix states only what to do. The reporter prints both, so any wording that appears in both is dead weight. When a single source sentence carries both ("A hash must start with '#'. Prefix it with '#'."), cut it in two — diagnosis to why, remedy to fix — rather than pasting the whole thing into why and echoing it in fix. fix accepts a param function too ((p) => ...), so move value-bearing remedies (use "#${p.hash}") into it instead of leaving them in why.cause when re-raising; pass sources when the JS stack doesn't reflect the user's source.diagnostics/build.ts, runtime.ts, config.ts, re-exported from index.ts), each defineDiagnostics() sharing docsBase with its own code range.console.warn/console.error/warn()/thrown Errors with diagnostic codes, without changing runtime behavior): follow references/migration.md start to finish.references/documentation-site.md.npx claudepluginhub vercel-labs/nostics --plugin nosticsWalks through adding a new diagnostic code into a `nostics` catalog: selecting prefix, area letter, next free number, and writing the entry.
TypeScript deep-dive skill covering type-level programming, performance optimization, monorepo patterns, and migration strategies. Also redirects to specialized subagents for bundler/module/type issues.
Classifies errors into 7 taxonomy categories (syntax, logic, design, performance, etc.) and routes to deterministic resolution strategies. Use when root cause is unclear or bug fix attempts fail.