nostics

repository·main·Indexed 41 days ago

https://github.com/vercel-labs/nostics

A structured diagnostic code library and framework that replaces ad hoc error strings with stable diagnostic codes, actionable fixes, source locations, and documentation links. Version 1.2.0 includes the core library, @nostics/unplugin for build-time transformations and Vite dev-server collection, and integration skills for AI agents like Claude Code and Cursor.

Tokens
25.6K
Snippets
81
Records
107
Agent score
52%

What's inside nostics

  1. Overview of nostics

    main

    nostics is a library designed for library authors to replace ad hoc error strings with structured, actionable error reporting. It allows you to use stable error codes (e.g., NUXT_B2011), provide actionable fixes, link to documentation, and include source locations within a Diagnostic object.

    Key capabilities include:

    • Stable codes: Permanent identifiers for known problems that remain constant even if the wording changes.
    • Actionable fixes: Integrated instructions on how to resolve the error.
    • Typed params: Using functions to ensure dynamic error messages and fixes are type-safe via TypeScript.
    • Reporters: Support for logging to the console, appending NDJSON to files, POSTing to endpoints, or implementing custom reporters.
    • Production stripping: A build plugin to remove report-only diagnostics and their catalog text from production bundles.
    • Vite dev collection: Ability to forward browser diagnostics to a local log file during Vite development.
  2. How reporters and formatters work together

    main

    In nostics, a Reporter is responsible for sending a diagnostic to a destination (like the console, a file, or an HTTP endpoint), while a Formatter is responsible for turning a Diagnostic object into a string representation.

    Crucial Note: A diagnostic will not be visible anywhere (including the console) unless a reporter is configured. If you call a diagnostic code like diagnostics.NUXT_B2011() without any reporters in your defineDiagnostics configuration, it will simply return a Diagnostic object and do nothing else. To surface it, you must either:

    1. Add a reporter to your defineDiagnostics configuration.
    2. Manually throw the returned diagnostic object.
    import { createConsoleReporter, defineDiagnostics } from 'nostics'
    
    export const diagnostics = defineDiagnostics({
      reporters: [createConsoleReporter()], // Without this, diagnostics are silent
      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}".`
          },
        },
      },
    })
  3. Configure documentation URLs with `docsBase`

    main

    The docsBase option in defineDiagnostics controls how documentation links are generated for your codes. You have three ways to configure it:

    1. Function: Provides full control. The function receives the code as an argument. This is useful for complex URL patterns (e.g., stripping prefixes or changing case).
    2. String: If the URL structure is regular, providing a string will append the lowercased code to the end of the base URL.
    3. Code-level override: You can override the global docsBase by setting a docs property directly on a specific code. Set docs: false to omit the link entirely for that code.

    Note: If docsBase is a string, the code is appended as base/code_name.

    defineDiagnostics({
      docsBase: 'https://example.com/errors',
      codes: {
        NUXT_B2011: { why: 'Plugin mode conflict.' }, // URL: https://example.com/errors/nuxt_b2011
        NUXT_I001: {
          why: 'Default config.',
          docs: 'https://example.com/guide',
        }, // URL: https://example.com/guide
        NUXT_X999: {
          why: 'No docs.',
          docs: false,
        }, // No URL
      },
    })
  4. Format diagnostic codes and messages

    main

    Code Naming

    Use the pattern PREFIX_XNNNN. The letter represents the area, not severity:

    • B: Build
    • R: Runtime
    • C: Config
    • D: Deprecation

    Message Composition

    Split original warnings: Do not copy a full warning string into why. Instead, put the diagnosis in why and the remedy in fix. If the remedy requires data, make fix a parameter function.

    Example of splitting a warning:

    • Bad: { why: (p) => "A hash should start with '#'. Replace ${p.hash} with #${p.hash}.", fix: "Prefix with #" } (Redundant)
    • Good: { why: (p) => "A hash should start with "#" but received "${p.hash}".", fix: (p) => "Prepend "#": use "#${p.hash}"" }

    Parameters

    • cause (the original error) and sources (array of 'file:line:column' strings) must be passed inside the first argument (the params object).
    • The second argument is for reporter options (e.g., { method: 'error' }).
  5. Strip diagnostics from production with nosticsStrip

    main

    To remove report-only diagnostics from production bundles, you need two components:

    1. Pure Annotations: The catalog must use /*#__PURE__*/ on defineDiagnostics and all reporter factory calls so they can be tree-shaken.
    2. Dev Guards or Plugin:
      • Manual: Wrap report-only calls in a dev guard: DEV && diagnostics.CODE(params).
      • Automatic: Use the nosticsStrip plugin from @nostics/unplugin/strip-transform. This plugin automatically adds the necessary guards and annotations at build time.

    Warning: throw, return, or assigned diagnostics are considered runtime behavior and will not be stripped. Only bare expression statements (report-only calls) are eligible for stripping.

  6. Choose the right reporter for your environment

    main

    The nosticsCollector is specifically designed for Vite development using HMR. For other environments, use the following reporters:

    • Server-side Node code: Use createFileReporter() directly.
    • Production telemetry: Use createFetchReporter() or implement your own custom reporter.
    • Browser development: Use createDevReporter() (as part of the Dev collector setup).
  7. Nostics coding conventions

    main

    To ensure diagnostics are effective for both humans and AI agents, follow these conventions:

    • Stable Codes: Use fully-qualified, stable prefixes: B (build), R (runtime), C (config), D (deprecation). Never reuse or reassign a published code.
    • Separation of Concerns: why is the diagnosis (what is wrong); fix is the remedy (what to do). Do not overlap them. If a sentence contains both, split it.
    • Parameterized Templates: Use parameterized templates for runtime values in why and fix instead of external string concatenation.
    • Contextual Metadata: Always pass cause when re-raising an error and sources when the JavaScript stack trace does not accurately reflect the user's source code.
    • Catalog Organization: For large projects, split catalogs by domain (e.g., build.ts, runtime.ts, config.ts) and re-export them from a central index.ts.
  8. Preserve runtime behavior during migration

    main

    When migrating to nostics, you must ensure the library's runtime behavior remains identical to its original state.

    • Keep existing dev guards: If a report was guarded by process.env.NODE_ENV !== 'production', keep that guard. nostics stripping is additive; it does not replace existing guards.
    • Do not add new guards: If a report was intended to fire in production, do not add a guard. If you migrate an unguarded report-only call, it becomes strippable by nosticsStrip in production builds.
    • Maintain execution flow: Keep the original throw vs reject logic, timing, recovery code, and returned fallbacks.
    • Maintain error shapes: If the library uses structured error objects (specific fields or symbols), do not replace them. If you migrate a throw, the diagnostic's why replaces the thrown message; update tests accordingly.
  9. Design stable diagnostic codes

    main

    Diagnostic codes should be treated as a permanent catalog. Once a code is published, do not rename it or reuse it for a different problem. This ensures documentation links remain valid and production stripping tools work correctly.

    While any string works, a recommended structured format is: <PREFIX>_<CATEGORY><NUMBER>

    Example categories:

    • C: Config
    • R: Runtime
    • B: Build
    • D: Deprecation
    • W: Warning
    • E: Error

    Example: NUXT_B2011.

  10. Automate stripping report-only diagnostics with @nostics/unplugin

    main

    Instead of manual annotations, use the nosticsStrip plugin from @nostics/unplugin to automatically add /*#__PURE__*/ annotations and NODE_ENV guards.

    Requirements for automation:

    • Use relative imports.
    • Export the defineDiagnostics() result (or the prod/dev ternary) directly.
    • Avoid factory wrappers or deep barrel re-exports.

    Installation:

    pnpm add -D @nostics/unplugin
  11. Develop the nostics documentation site

    main

    To run the documentation site in development mode, use the pnpm command targeting the docs directory. This is useful for previewing changes to the documentation content located in docs/content.

    pnpm -C docs run dev