# Quick start `nostics` is for library authors who want errors and warnings to be easy to recognize, document, and fix. Instead of throwing anonymous strings from different places, you define a small catalog of diagnostic codes. Each code has a stable name, a message, an optional fix, and an optional docs URL. ## Install ::code-group ```bash [pnpm] pnpm add nostics ``` ```bash [npm] npm install nostics ``` ```bash [yarn] yarn add nostics ``` :: The package is ESM only. ## Define codes Create one or multiple diagnostics file(s) in your library. ```ts [src/diagnostics.ts] import { createConsoleReporter, defineDiagnostics } from 'nostics' export const diagnostics = defineDiagnostics({ docsBase: code => `https://nuxt.com/e/${code.replace('NUXT_', '').toLowerCase()}`, reporters: [createConsoleReporter()], codes: { NUXT_B2011: { why: (p: { src: string, mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Plugin "${p.src}" is ${expected}-only but was registered with mode "${p.mode}".` }, fix: (p: { mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Rename the file or register it with mode "${expected}".` }, }, NUXT_B5001: { why: (p: { value: string, configPath: string }) => `Invalid compatibilityDate "${p.value}" in ${p.configPath}.`, fix: (p: { example: string }) => `Use an ISO date like "${p.example}", or "latest".`, }, }, }) ``` The returned `diagnostics` object has one function per code. TypeScript checks the params at the call site. ## Use them ```ts [src/module.ts] import { diagnostics } from './diagnostics' type PluginMode = 'all' | 'client' | 'server' export function addRuntimePlugin(plugin: { src: string, mode?: PluginMode }): void { const suffix = plugin.src.match(/\.(client|server)\./)?.[1] as 'client' | 'server' | undefined if (suffix && plugin.mode && plugin.mode !== 'all' && plugin.mode !== suffix) { throw diagnostics.NUXT_B2011({ src: plugin.src, mode: plugin.mode, sources: [plugin.src], }) } // register plugin } export function validateConfig(config: { compatibilityDate?: string, filepath: string }): void { if (config.compatibilityDate && !/^(\d{4}-\d{2}-\d{2}|latest)$/.test(config.compatibilityDate)) { diagnostics.NUXT_B5001({ configPath: config.filepath, value: config.compatibilityDate, example: '2024-04-03', }) } } ``` Calling a diagnostic reports it and returns a `Diagnostic`. Throwing the return value raises it. ## Output ::diagnostic --- log: > [NUXT_B2011] Plugin `./runtime/analytics.server.ts` is server-only but was registered with mode `client`. ├▶ fix: Rename the file or register it with mode `server`. ├▶ sources: modules/analytics.ts:18:5 ╰▶ see: https://nuxt.com/e/b2011 --- :: The code is stable, the message is readable, and the fix is next to the error. ## Catch it `Diagnostic` extends `Error`. ```ts import { Diagnostic } from 'nostics' try { const plugin = resolvePlugin() addRuntimePlugin(plugin) } catch (error) { if (error instanceof Diagnostic) { console.log(error.name) console.log(error.fix) console.log(error.docs) } } ``` ## Claude Code plugin Install the nostics plugin to give Claude agent skills for your diagnostic catalog: ```bash claude plugin add https://github.com/vercel-labs/nostics ``` Claude will automatically pick up the `nostics` API reference and an `add-diagnostic` skill that finds the right catalog, chooses the next free code, and wires the call site. ## Agent skills Prefer just the skills, without the plugin? Install them with [`npx skills`](https://github.com/vercel-labs/skills){rel=""nofollow""} into any supported agent (Claude Code, Codex, Cursor, opencode, and more): ```bash npx skills add vercel-labs/nostics ``` ## Next ::card-group :::card --- icon: i-lucide-list-tree title: Define diagnostics to: https://nostics.dev/guide/defining-diagnostics --- Parameters, docs URLs, sources, causes, and code naming. ::: :::card --- icon: i-lucide-scissors title: Production builds to: https://nostics.dev/guide/production-builds --- Strip report-only diagnostics from production bundles. ::: :::card --- icon: i-lucide-file-code title: API reference to: https://nostics.dev/api/reference --- Imports, types, options, and built-ins. ::: :: # Defining diagnostics Diagnostics work best when you treat them as a catalog. Keep the definitions close together, give each one a **permanent** code, and make the fix specific. ## Shape ```ts import { createConsoleReporter, defineDiagnostics } from 'nostics' export const diagnostics = defineDiagnostics({ docsBase: code => `https://nuxt.com/e/${code.replace('NUXT_', '').toLowerCase()}`, reporters: [createConsoleReporter()], codes: { NUXT_B2011: { why: (p: { src: string, mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Plugin "${p.src}" is ${expected}-only but was registered with mode "${p.mode}".` }, fix: (p: { mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Rename the file or register it with mode "${expected}".` }, data: (p: { src: string, mode: 'client' | 'server' }) => ({ src: p.src, mode: p.mode, }), }, }, }) ``` Each key becomes a callable handle: ```ts diagnostics.NUXT_B2011({ src: pluginSrc, mode: 'client', }) throw diagnostics.NUXT_B2011({ src: pluginSrc, mode: 'client', }) ``` Calling it creates a fresh `Diagnostic`, runs every reporter, and returns the instance. ## Params `why`, `fix`, and `data` can use functions. Function params are inferred and required at the call site. ```ts export const diagnostics = defineDiagnostics({ codes: { NUXT_B5001: { why: (p: { value: string, configPath: string }) => `Invalid compatibilityDate "${p.value}" in ${p.configPath}.`, fix: (p: { example: string }) => `Use an ISO date like "${p.example}", or "latest".`, }, }, }) // somewhere in config validation diagnostics.NUXT_B5001({ configPath: config.filepath, value: config.compatibilityDate, example: '2024-04-03', }) ``` Params from `why`, `fix`, and `data` are combined. In the example above, `value`, `configPath`, and `example` are required. ## Structured data Use `data` for machine-readable information that depends on the diagnostic code. It can be a static object or a function of the shared params. ```ts const diagnostic = defineDiagnostics({ codes: { NUXT_B5001: { why: (p: { value: string }) => `Invalid value "${p.value}".`, data: (p: { value: string }) => ({ received: p.value, expected: 'ISO date' as const, }), }, }, }).NUXT_B5001({ value: 'today' }) diagnostic.data.received // string diagnostic.data.expected // "ISO date" ``` Each code keeps its own `data` type. `toJSON()` includes the resolved object, so JSON reporters keep it too. Use JSON-compatible values when a reporter serializes the diagnostic. ## Call-site fields Two fields belong at the place where you report the diagnostic: ```ts try { await loadConfig() } catch (cause) { throw diagnostics.NUXT_B2011({ src: pluginSrc, mode: 'client', cause, sources: [ pluginSrc + ':' + line + ':' + column, // ./path/to/file:10:5 ], }) } ``` `cause` preserves the original error. `sources` points at user code when the JavaScript stack would point inside your library. The `sources` is especially important for build and config diagnostics, where the problem is often in user files but the error is thrown from your library, so the stack trace is not helpful. Reporters can use `cause` and `sources` to give better output. ## Docs URLs Use `docsBase` to create the docs URL for each code. A function gives you full control. ```ts defineDiagnostics({ docsBase: code => `https://nuxt.com/e/${code.replace('NUXT_', '').toLowerCase()}`, codes: { NUXT_B2011: { why: 'Plugin mode conflicts with file suffix.' }, }, }) ``` `NUXT_B2011` gets `https://nuxt.com/e/b2011` printed in the diagnostic. Use a string when your URL structure is regular and the lowercased code can be appended. ```ts defineDiagnostics({ docsBase: 'https://nuxt.com/errors', codes: { NUXT_B2011: { why: 'Plugin mode conflicts with file suffix.' }, }, }) ``` `NUXT_B2011` gets `https://nuxt.com/errors/nuxt_b2011`. Set `docs` on a code to override the URL, or `false` to omit it. ```ts defineDiagnostics({ docsBase: 'https://nuxt.com/e', codes: { NUXT_B2011: { why: 'Plugin mode conflicts with file suffix.', docs: 'https://nuxt.com/docs/guide/directory-structure/plugins', }, NUXT_I001: { why: 'Using default Nuxt configuration.', docs: false, }, }, }) ``` ## Codes Pick a format and keep it stable. We recommend a structured shape that scales across many codes: ```txt _ ``` Examples: ```txt NUXT_B2011 NUXT_R1001 NUXT_D0042 NUXT_C0100 ``` Common category letters are `C` for config, `R` for runtime, `B` for build, `D` for deprecation, `W` for warning, and `E` for error. The exact letters matter less than keeping them consistent. ::tip Once a code is published, do not rename it or reuse it for a different problem. This is crucial for users to find the right docs and for the production strip plugin to work correctly. :: This shape is only a convention, not a requirement. A code is any string, so an explicit name like `invalidDate` works just as well and reads clearer when you only have a few: ```ts [src/diagnostics.ts] export const diagnostics = defineDiagnostics({ codes: { invalidDate: { why: (p: { value: string }) => `...`, fix: (p: { example: string }) => `...`, }, missingConfig: { why: (p: { path: string }) => `...`, fix: () => `...`, }, }, }) diagnostics.invalidDate({ value, example }) diagnostics.missingConfig({ path }) ``` ## Split files For small libraries, one `src/diagnostics.ts` file is enough. For larger ones, split by area and export the handles directly. ```txt src/diagnostics/ build.ts config.ts runtime.ts ``` ```ts [src/diagnostics/config.ts] export const configDiagnostics = defineDiagnostics({ docsBase: code => `https://nuxt.com/e/${code.replace('NUXT_', '').toLowerCase()}`, codes: { NUXT_C001: { why: 'Could not load Nuxt configuration.' }, }, }) ``` Direct exports are easier for the production strip plugin to track than factory wrappers or deep barrels. ## Write good fixes Good diagnostics say what happened and what to do next. ```ts NUXT_B2011: { why: (p: { src: string, mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Plugin "${p.src}" is ${expected}-only but was registered with mode "${p.mode}".` }, fix: (p: { mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Rename the file or register it with mode "${expected}".` }, } ``` Avoid fixes that only restate the problem or are too vague to be helpful. ```ts NUXT_B2011: { why: 'Plugin mode is wrong.', fix: 'Fix the plugin.', } ``` # Reporters and formatters A reporter runs every time a diagnostic is created. A formatter turns a diagnostic into text. ::warning A diagnostic only reaches the console (or anywhere else) through a reporter. With no `reporters` configured, `diagnostics.NUXT_B2011()` just builds and returns a `Diagnostic` , it prints nothing. Add a reporter such as `createConsoleReporter()` to report it, or `throw` the returned diagnostic to surface it as an error. :: ## Reporters ```ts import { createConsoleReporter, defineDiagnostics } from 'nostics' export const diagnostics = defineDiagnostics({ reporters: [createConsoleReporter()], codes: { NUXT_B2011: { why: (p: { src: string, mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Plugin "${p.src}" is ${expected}-only but was registered with mode "${p.mode}".` }, fix: (p: { mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Rename the file or register it with mode "${expected}".` }, }, }, }) ``` Reporters run in order. They receive the `Diagnostic` and optional reporter options. ```ts import type { DiagnosticReporter } from 'nostics' const sentryReporter: DiagnosticReporter = (diagnostic) => { Sentry.captureMessage(diagnostic.message, { tags: { code: diagnostic.name }, extra: { fix: diagnostic.fix, docs: diagnostic.docs, sources: diagnostic.sources, }, }) } ``` Built-ins: | Reporter | Import | Use | | --------------------------------- | ------------------------- | ----------------------------------------------------------------- | | `createConsoleReporter(options?)` | `nostics` | Console reporter; defaults to `console.warn` + `formatDiagnostic` | | `createFileReporter()` | `nostics/reporters/node` | Append NDJSON to a local file | | `createFetchReporter(url)` | `nostics/reporters/fetch` | POST JSON to a URL | | `createDevReporter()` | `nostics/reporters/dev` | Send browser diagnostics to Vite dev server | ## Console reporter `createConsoleReporter()` uses `console.warn` by default. Pass a method when you call the diagnostic if you need a different console method. ```ts const plugin = resolvePlugin() diagnostics.NUXT_B2011({ src: plugin.src, mode: plugin.mode, }, { method: 'error' }) ``` If you always want the same method, bake it into the reporter: ```ts const reporterAlwaysError = createConsoleReporter({ method: 'error' }) ``` ### Custom formatter The reporter renders each diagnostic with a `formatter`, defaulting to [`formatDiagnostic`](https://nostics.dev/#formatters). Pass your own to change how the diagnostic is printed, for example to add color in a terminal with `ansiFormatter`: ```ts import { createConsoleReporter } from 'nostics' import { ansiFormatter } from 'nostics/formatters/ansi' import colors from 'picocolors' const reporter = createConsoleReporter({ formatter: ansiFormatter(colors), }) ``` Any function with the `(diagnostic: Diagnostic) => string` shape works, so you can also reuse `jsonFormatter` or write your own. ## File reporter Use the Node reporter when your code runs in Node and you want a local log. ```ts import { createFileReporter } from 'nostics/reporters/node' export const diagnostics = defineDiagnostics({ reporters: [ createFileReporter({ logFile: '.nuxt-diagnostics.log', excludeStackFrames: [/\/node_modules\//, /\(node:/], }), ], codes: { NUXT_B2011: { why: 'Plugin mode conflicts with file suffix.' }, }, }) ``` Each diagnostic is written as one JSON line. `excludeStackFrames` defaults to `[/\/node_modules\//i]`, so dependency frames are stripped automatically. Pass your own patterns to strip more, or `[]` to keep every frame. ```bash tail -f .nuxt-diagnostics.log | jq . ``` ## Fetch reporter Use the fetch reporter for fire-and-forget HTTP logging. ```ts import { createFetchReporter } from 'nostics/reporters/fetch' export const diagnostics = defineDiagnostics({ reporters: [createFetchReporter('https://telemetry.example.com/diagnostics')], codes: { NUXT_B2011: { why: 'Plugin mode conflicts with file suffix.' }, }, }) ``` Fetch failures are swallowed so reporting does not break user code. ## Reporter options Reporter options are typed and merged into the diagnostic call signature. ```ts const audited: DiagnosticReporter<{ priority: number }> = (diagnostic, options) => { audit.record({ code: diagnostic.name, priority: options.priority, }) } const diagnostics = defineDiagnostics({ reporters: [audited], codes: { NUXT_B2011: { why: (p: { src: string, mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Plugin "${p.src}" is ${expected}-only but was registered with mode "${p.mode}".` }, }, }, }) const plugin = resolvePlugin() diagnostics.NUXT_B2011( { src: plugin.src, mode: plugin.mode, }, // the second argument is required now { priority: 1 }, ) ``` Use required reporter options sparingly. They make every call site pass the second argument. ## Formatters The default formatter is `formatDiagnostic`. ```ts import { formatDiagnostic } from 'nostics' console.log(formatDiagnostic(diagnostic)) ``` ::diagnostic --- log: > [NUXT_B2011] Plugin `./runtime/analytics.server.ts` is server-only but was registered with mode `client`. ├▶ fix: Rename the file or register it with mode `server`. ├▶ sources: modules/analytics.ts:18:5 ╰▶ see: https://nuxt.com/e/b2011 --- :: `formatDiagnostic` emits this layout as plain text. Swap in `ansiFormatter` and the same diagnostic gains color when printed to a terminal, rendered above as it would appear there. Detail lines are rendered in this order: `fix`, `sources`, `see`. Other formatters: | Formatter | Import | Output | | ----------------------- | ------------------------- | ---------------------- | | `formatDiagnostic` | `nostics` | Plain multiline text | | `ansiFormatter(colors)` | `nostics/formatters/ansi` | Colored multiline text | | `jsonFormatter` | `nostics/formatters/json` | One JSON string | `ansiFormatter` accepts any color helper with this shape: ```ts interface Colors { red: (s: string) => string yellow: (s: string) => string cyan: (s: string) => string gray: (s: string) => string bold: (s: string) => string dim: (s: string) => string } ``` You can also write a formatter as a plain function: ```ts import type { Diagnostic } from 'nostics' function compact(diagnostic: Diagnostic): string { return `${diagnostic.name}: ${diagnostic.message}` } ``` # Production builds A diagnostic catalog carries user-facing `why`/`fix` text. It's invaluable in development, but mostly dead weight in production, where you usually want two things: - the codes you `throw` to keep working, but **without** shipping all their catalog text; - **report-only** diagnostics (bare `diagnostics.X()` calls) to disappear entirely. Both are plain source changes. The first needs no build tooling and matters most for a library. The second you can do by hand too, or automate with a plugin. ## Keep surviving diagnostics lean Diagnostics you `throw`, `return`, assign, or pass to a function are runtime behavior, so they stay in the bundle. Each one keeps the whole `defineDiagnostics()` catalog reachable, so every code's `why`/`fix` text ships too, even codes you never throw. `defineProdDiagnostics` removes that text with no build tooling. Pick a lean catalog at definition time with a `process.env.NODE_ENV` ternary: ```ts [diagnostics.ts] import { createConsoleReporter, defineDiagnostics, defineProdDiagnostics } from 'nostics' const docsBase = (code: string) => `https://mylib.dev/e/${code.toLowerCase()}` export const diagnostics = process.env.NODE_ENV === 'production' ? /*#__PURE__*/ defineProdDiagnostics({ docsBase }) : /*#__PURE__*/ defineDiagnostics({ docsBase, reporters: [/*#__PURE__*/ createConsoleReporter()], codes: { /* all your why/fix text */ }, }) ``` This is plain JavaScript. Any bundler that replaces `process.env.NODE_ENV` (Vite, Rollup, webpack, esbuild) drops the `defineDiagnostics({ codes })` branch (all the text) in a production build and tree-shakes the `defineDiagnostics` import out. Your consumers' build needs no setup, which makes this a good fit for a published library: their bundler does the work. The `/*#__PURE__*/` markers tell the bundler it can drop the whole `diagnostics` binding (and the reporter factory) when nothing references it. See [Remove report-only diagnostics](https://nostics.dev/#remove-report-only-diagnostics) for the details. [`defineProdDiagnostics`](https://nostics.dev/api/reference#defineproddiagnosticsoptions) returns a `Proxy` that answers for any code you access. The code becomes the instance `name`, `docs` is still derived from `docsBase`, and the `message` (`why`) points to the docs URL (empty when there is no `docsBase`, so the thrown header is just the code): ```ts throw diagnostics.NUXT_B2011() // NUXT_B2011: https://docs.example.com/nuxt_b2011 ``` So a thrown diagnostic keeps its code (as the `name`) and a working docs link, but no catalog text reaches the bundle. It does not keep definition data, so `diagnostic.data` is `undefined` in this branch. In a development and production ternary, TypeScript exposes the development data type as optional. By default the prod branch has no `reporters`, so a surviving `throw` won't also log through a reporter and resurface as an uncaught error. To keep reporting in production (say a `createFetchReporter` that phones home), pass `reporters` to `defineProdDiagnostics` too. ## Remove report-only diagnostics Report-only diagnostics are bare statements. They don't affect control flow, so production can drop them entirely: ```ts diagnostics.NUXT_B2011() condition && diagnostics.NUXT_B5001() ``` These stay, because they are behavior, not reports: ```ts throw diagnostics.NUXT_B2011() return diagnostics.NUXT_B2011() const diagnostic = diagnostics.NUXT_B2011() fn(diagnostics.NUXT_B2011()) ``` To strip the report-only ones by hand, add two annotations in your source: 1. Mark the catalog pure so an unused one tree-shakes: put `/*#__PURE__*/` before `defineDiagnostics(` and before each reporter factory call inside it. 2. Dev-guard every report-only call site. ```ts const diagnostics = /*#__PURE__*/ defineDiagnostics({ reporters: [/*#__PURE__*/ createConsoleReporter()], codes: { /* ... */ }, }) process.env.NODE_ENV !== 'production' && diagnostics.NUXT_B2011() ``` In a production build the guarded calls drop. Once they're gone, the catalog is unused and tree-shakes away. If you already applied the ternary above, the full `defineDiagnostics()` only lives in the dev branch, so the pure annotation doesn't matter there, but the dev guard still removes the bare call in production. ## Automate stripping with `nosticsStrip` Rather not annotate call sites by hand? The `nosticsStrip` build plugin adds the `/*#__PURE__*/` annotations and `NODE_ENV` guards for you. It ships in the separate `@nostics/unplugin` package: ```bash pnpm add -D @nostics/unplugin ``` A library bundled with `tsdown` uses the `.rolldown()` adapter. Keep `nostics` external so your published code keeps the conditional import for your consumers' bundler to resolve: ::code-group ```ts [vite.config.ts] import { nosticsStrip } from '@nostics/unplugin/strip-transform' import { defineConfig } from 'vite' export default defineConfig({ plugins: [nosticsStrip.vite()] }) ``` ```ts [tsdown.config.ts] import { nosticsStrip } from '@nostics/unplugin/strip-transform' import { defineConfig } from 'tsdown' export default defineConfig({ entry: ['src/index.ts'], deps: { neverBundle: ['nostics'] }, plugins: [nosticsStrip.rolldown()], }) ``` :: It is built with unplugin, so beyond `.vite()` and `.rolldown()` it also exposes `.rollup()`, `.webpack()`, `.rspack()`, `.esbuild()`, and `.farm()`. It can follow a diagnostics object across one relative import. For that to work, use relative imports, export the `defineDiagnostics()` result (or the prod/dev ternary above) directly, and avoid factory wrappers or deep barrel re-exports. | Option | Default | Use | | ------------- | ----------- | ---------------------------------------- | | `packageName` | `'nostics'` | Detect imports from another package name | The plugin only automates report-only stripping. Pair it with `defineProdDiagnostics` when you also want surviving diagnostics lean. ## Verify the output After a production build, search the bundle: ```bash rg 'NUXT_B2011|Plugin mode conflicts' dist/ ``` Report-only diagnostics should be gone, and surviving diagnostics should show only their code, not the `why`/`fix` text. # Dev collector Browser diagnostics normally live in the browser console. The dev collector copies them to a local file during Vite dev, which makes them easy to tail, grep, and share. There are two pieces: - `createDevReporter()` runs in browser code and sends diagnostics over Vite HMR. - `nosticsCollector` runs in the Vite dev server and writes them to a file. It ships in the separate `@nostics/unplugin` package. ## Browser side Add the dev reporter next to your normal reporter. ```ts [src/diagnostics.ts] import { createConsoleReporter, defineDiagnostics } from 'nostics' import { createDevReporter } from 'nostics/reporters/dev' export const diagnostics = defineDiagnostics({ reporters: [createConsoleReporter(), createDevReporter()], codes: { NUXT_B2011: { why: (p: { src: string, mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Plugin "${p.src}" is ${expected}-only but was registered with mode "${p.mode}".` }, fix: (p: { mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Rename the file or register it with mode "${expected}".` }, }, }, }) ``` The console reporter still prints in the browser console. The dev reporter adds the Vite dev-server copy. ## Vite side Add the collector to the app consuming the browser code. ```bash pnpm add -D @nostics/unplugin ``` ```ts [vite.config.ts] import { nosticsCollector } from '@nostics/unplugin/dev-server-collector' import { defineConfig } from 'vite' export default defineConfig({ plugins: [nosticsCollector.vite()], }) ``` Start Vite and tail the log: ```bash tail -f .nostics.log | jq . ``` Each diagnostic is appended as one JSON line. ## Options ```ts nosticsCollector.vite({ logFile: '.app-diagnostics.log', debug: true, excludeStackFrames: [/\/node_modules\//, /\(node:/], }) ``` | Option | Default | Use | | -------------------- | ----------------------- | -------------------------------------------------------------------------- | | `logFile` | `'.nostics.log'` | File to append diagnostics to | | `debug` | `!!process.env.DEBUG` | Log collector activity in the Vite server | | `excludeStackFrames` | `[/\/node_modules\//i]` | Remove matching stack frames before writing. Pass `[]` to keep every frame | ## Vite only `nosticsCollector` uses Vite `configureServer` and `server.ws`. Other unplugin adapters exist because of the shared plugin shape, but they do nothing for this collector. Use `createFileReporter()` directly for server-side Node code. Use `createFetchReporter()` or your own reporter for production telemetry. # Diagnostic registry A diagnostic code is most useful when it points to a page that explains the problem and the fix. The page does not need to be long. It needs to be stable, readable without JavaScript, and consistent across codes. ## Pick the URL shape Use `docsBase` to decide where code pages live. A function gives you full control over the URL. ```ts defineDiagnostics({ docsBase: code => `https://nuxt.com/e/${code.replace('NUXT_', '').toLowerCase()}`, codes: { NUXT_B2011: { why: 'Plugin mode conflicts with file suffix.' }, }, }) ``` This creates `https://nuxt.com/e/b2011`. Once published, keep the URL working. Logs and search results can outlive the release that emitted them. ## Page template ````markdown # NUXT_B2011: Plugin mode conflicts with file suffix Code: `NUXT_B2011` Level: error ## What happened Nuxt tried to register a plugin whose filename says it should run on one side, but whose `mode` says it should run on the other side. ## How to fix it Remove `mode` and let the filename suffix decide: ```ts [app/plugins/analytics.server.ts] export default defineNuxtPlugin(() => { // plugin code }) ``` ## Common causes - A plugin file was renamed from `.client.ts` to `.server.ts` without updating `mode`. - A module option switches the plugin mode but not the resolved file path. - A shared helper applies a default `mode` after the plugin path was already selected. ## Example output ```txt [NUXT_B2011] Plugin `./runtime/analytics.server.ts` is server-only but was registered with mode `client`. ├▶ fix: Rename the file or register it with mode `server`. ╰▶ see: https://nuxt.com/e/b2011 ``` ```` Keep the fix near the top. Most people arrive from an error message and want the next action. ## Keep it in sync The useful rule is simple: a new published code should ship with its docs page. Common ways to enforce that: - Keep the markdown in the same repo as the diagnostic catalog. - Generate an index of all codes from the catalog. - Add a CI check that every code has a matching page. Example check: ```ts import { existsSync } from 'node:fs' import { diagnostics } from '../src/diagnostics' const missing = Object.keys(diagnostics).filter((code) => { return !existsSync(`docs/errors/${code.toLowerCase()}.md`) }) if (missing.length > 0) { console.error('Missing diagnostic docs:', missing.join(', ')) process.exit(1) } ``` ## Make pages easy to consume - Render the content in the initial HTML. - Include the code in the title and body. - Use the same section names on every page. - Keep examples self-contained. - Return `404` for unknown codes. This helps humans, search engines, and tools that fetch the URL from a log. # Reference ## `nostics` ```ts import { createConsoleReporter, defineDiagnostics, defineProdDiagnostics, Diagnostic, formatDiagnostic, } from 'nostics' ``` ### `defineDiagnostics(options)` ```ts const diagnostics = defineDiagnostics({ docsBase: code => `https://nuxt.com/e/${code.replace('NUXT_', '').toLowerCase()}`, reporters: [createConsoleReporter()], codes: { NUXT_B2011: { why: (p: { src: string, mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Plugin "${p.src}" is ${expected}-only but was registered with mode "${p.mode}".` }, fix: (p: { mode: 'client' | 'server' }) => { const expected = p.mode === 'client' ? 'server' : 'client' return `Rename the file or register it with mode "${expected}".` }, }, }, }) ``` Options: | Field | Type | Required | Description | | ----------- | ----------------------------------------- | -------- | ------------------------------------ | | `codes` | `Record` | yes | Code definitions | | `docsBase` | `string | ((code) => string | undefined)` | no | Base docs URL or URL resolver | | `reporters` | `DiagnosticReporter[]` | no | Reporters called on every diagnostic | Definition: ```ts interface DiagnosticDefinition< P = any, Data extends Record = Record, > { why: string | ((params: P) => string) fix?: string | ((params: P) => string) docs?: string | false data?: Data | ((params: P) => Data) } ``` Call-site fields: ```ts interface DiagnosticCallParams { cause?: unknown sources?: string[] } ``` ### `defineProdDiagnostics(options?)` Lean production counterpart to `defineDiagnostics`. Returns a `Proxy` that builds a minimal `Diagnostic` for any accessed code: the code becomes the instance `name`, `docs` is derived from `docsBase`, and the `message` (`why`) points to the docs URL when one exists (empty otherwise, so the thrown header is just the code). It carries no catalog text or data, so it stays tiny in a bundle. Its `data` property is `undefined`. ```ts const diagnostics = defineProdDiagnostics({ docsBase: code => `https://nuxt.com/e/${code.replace('NUXT_', '').toLowerCase()}`, }) throw diagnostics.NUXT_B2011() // Error: NUXT_B2011, docs derived from docsBase ``` Options: | Field | Type | Required | Description | | ----------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------- | | `docsBase` | `string | ((code) => string | undefined)` | no | Base docs URL or URL resolver, identical to `defineDiagnostics` | | `reporters` | `DiagnosticReporter[]` | no | Reporters called on every diagnostic. Omitted by default in production builds | Select it in production with a `process.env.NODE_ENV` ternary so bundlers drop the full catalog. See [Production builds](https://nostics.dev/guide/production-builds). ### `Diagnostic` ```ts class Diagnostic extends Error { name: string message: string get why(): string fix?: string docs?: string sources?: string[] cause?: unknown data: Data toJSON(): DiagnosticJSON } ``` `name` is the code. `message` and `why` are the resolved explanation. `data` is inferred for each code from its static object or resolver return type. `toJSON()` returns `name`, `why`, `fix`, `docs`, `sources`, `cause`, `data`, and `stack`. When you call `JSON.stringify(diagnostic)`, fields whose value is `undefined` are omitted from the JSON output. ### `DiagnosticReporter` ```ts type DiagnosticReporter = ( diagnostic: Diagnostic, options: Options, ) => void ``` Use the second generic when a reporter handles a known data shape. Reporters shared by unrelated codes can leave it at the default. ### `formatDiagnostic(diagnostic)` Renders plain multiline text. ```txt [NUXT_B2011] Plugin `./runtime/analytics.server.ts` is server-only but was registered with mode `client`. ├▶ fix: Rename the file or register it with mode `server`. ├▶ sources: modules/analytics.ts:18:5 ╰▶ see: https://nuxt.com/e/b2011 ``` ### `createConsoleReporter` ```ts function createConsoleReporter(options?: { method?: 'log' | 'error' | 'warn' // default 'warn' formatter?: (diagnostic: Diagnostic) => string // default formatDiagnostic }): DiagnosticReporter<{ method?: 'log' | 'error' | 'warn' }> ``` Builds a console reporter. `createConsoleReporter()` uses `console.warn` and `formatDiagnostic`; `method` is overridable per call. ## Reporters ### `nostics/reporters/node` ```ts import { createFileReporter } from 'nostics/reporters/node' ``` ```ts function createFileReporter(options?: { logFile?: string excludeStackFrames?: readonly RegExp[] }): DiagnosticReporter ``` Appends one JSON line per diagnostic. Default file: `.nostics.log`. `excludeStackFrames` defaults to `[/\/node_modules\//i]`; pass `[]` to keep every frame. ### `nostics/reporters/fetch` ```ts import { createFetchReporter } from 'nostics/reporters/fetch' ``` ```ts function createFetchReporter(url: string): DiagnosticReporter ``` POSTs `JSON.stringify(diagnostic)` to `url` with `Content-Type: application/json`. Fetch failures are swallowed. ### `nostics/reporters/dev` ```ts import { createDevReporter } from 'nostics/reporters/dev' ``` `createDevReporter()` builds a reporter that sends `diagnostic.toJSON()` over `import.meta.hot.send('nostics:report', payload)`. Outside Vite, it warns and does nothing else. ## Formatters ### `nostics/formatters/ansi` ```ts import { ansiFormatter } from 'nostics/formatters/ansi' ``` ```ts function ansiFormatter(colors: Colors): (diagnostic: Diagnostic) => string ``` ```ts interface Colors { red: (s: string) => string yellow: (s: string) => string cyan: (s: string) => string gray: (s: string) => string bold: (s: string) => string dim: (s: string) => string } ``` ### `nostics/formatters/json` ```ts import { jsonFormatter } from 'nostics/formatters/json' ``` ```ts const jsonFormatter: (diagnostic: Diagnostic) => string ``` Calls `JSON.stringify(diagnostic)`. ## Plugins The build-time plugins ship in the separate `@nostics/unplugin` package: ```bash pnpm add -D @nostics/unplugin ``` ### `@nostics/unplugin/strip-transform` ```ts import { nosticsStrip } from '@nostics/unplugin/strip-transform' ``` Build-time plugin that marks `defineDiagnostics()` calls as pure and guards report-only diagnostic expression statements with `process.env.NODE_ENV !== "production"`. ```ts nosticsStrip.vite({ packageName: 'nostics', }) ``` Options: | Field | Default | Description | | ------------- | ----------- | ----------------------------------- | | `packageName` | `'nostics'` | Package name to detect imports from | ### `@nostics/unplugin/dev-server-collector` ```ts import { nosticsCollector } from '@nostics/unplugin/dev-server-collector' ``` Vite dev-server plugin that listens for `nostics:report` events and appends diagnostics to a local file. ```ts nosticsCollector.vite({ logFile: '.nostics.log', debug: false, excludeStackFrames: [/\/node_modules\//], }) ``` Options: | Field | Default | Description | | -------------------- | ----------------------- | -------------------------------------------------------------------------- | | `logFile` | `'.nostics.log'` | Output file | | `debug` | `!!process.env.DEBUG` | Log collector activity | | `excludeStackFrames` | `[/\/node_modules\//i]` | Remove matching stack frames before writing. Pass `[]` to keep every frame | Only the Vite adapter is meaningful for `nosticsCollector`. # Errors worth reading ::u-page-hero #headline :logo{.mx-auto.h-20.mb-6} #title Errors worth reading #description [nostics]{.font-mono} helps library authors replace ad hoc error strings with stable codes, actionable fixes, docs links, and structured `Diagnostic` objects. :::div{.diag-hero} ::::diagnostic --- log: | [NUXT_B2011] Plugin `./runtime/analytics.server.ts` has mode `client`. ├▶ fix: Rename the file or register it with mode `server`. ├▶ sources: modules/analytics.ts:18:5 ╰▶ see: https://nuxt.com/e/b2011 --- :::: ::: #links :::u-button --- color: primary size: xl to: https://nostics.dev/getting-started/quick-start trailing-icon: i-lucide-arrow-right --- Quick start ::: :::u-button --- color: neutral icon: simple-icons-github size: xl to: https://github.com/vercel-labs/nostics variant: outline --- GitHub ::: :: ::u-page-section #headline [What it gives you]{.section-eyebrow} #features :::u-page-feature --- class: dev-card icon: i-lucide-hash --- #title Stable codes #description Give every known problem a permanent code like `NUXT_B2011`. Users can search it, docs can link to it, and your wording can improve without breaking the identifier. ::: :::u-page-feature --- class: dev-card icon: i-lucide-check-circle --- #title Actionable fixes #description Put the next step next to the error. The default formatter renders the message, fix, source locations, and docs URL in one compact block. ::: :::u-page-feature --- class: dev-card icon: i-lucide-braces --- #title Typed params #description Use functions for dynamic messages and fixes. TypeScript requires the right params wherever you report the diagnostic. ::: :::u-page-feature --- class: dev-card icon: i-lucide-share-2 --- #title Reporters #description Log to the console, append NDJSON to a file, POST to an endpoint, or write your own reporter. ::: :::u-page-feature --- class: dev-card icon: i-lucide-scissors --- #title Production stripping #description Use the build plugin to remove report-only diagnostics and their catalog text from production bundles. ::: :::u-page-feature --- class: dev-card icon: i-lucide-terminal --- #title Vite dev collection #description Forward browser diagnostics to a local log file during Vite dev when the browser console is not enough. ::: :: ::u-page-section #headline [Start here]{.section-eyebrow} #features :::u-page-feature --- icon: i-lucide-rocket to: https://nostics.dev/getting-started/quick-start --- #title Quick start #description Install the package, define two codes, and report your first diagnostic. ::: :::u-page-feature --- icon: i-lucide-list-tree to: https://nostics.dev/guide/defining-diagnostics --- #title Guide #description Learn the catalog shape, params, docs URLs, sources, causes, reporters, and build plugins. ::: :::u-page-feature --- icon: i-lucide-file-code to: https://nostics.dev/api/reference --- #title API reference #description Check imports, option names, reporter signatures, formatter shapes, and plugin options. ::: ::