🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

nostics

Package Overview
Dependencies
Maintainers
2
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

nostics - npm Package Compare versions

Comparing version
1.0.0
to
1.1.0
+288
dist/diagnostic-BajIZ3Zi.d.mts
//#region src/utils.d.ts
/**
* A value of type T, or a function that resolves T from a single params object.
*
* @internal
*/
type ValueOrFn<T, P = any> = T | ((params: P) => T);
/**
* Extracts the param type from a single-arg function, or `never` for
* non-function inputs. Pairs with {@link ValueOrFn}.
*
* @internal
*/
type ExtractFnParam<T> = T extends ((params: infer P) => any) ? P : never;
/**
* Converts a union of types to their intersection.
*
* @internal
*/
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
/**
* `true` when `T` is the `any` type.
*
* @internal
*/
type IsAny<Type> = 0 extends 1 & Type ? true : false;
/**
* `true` when `T` is the `unknown` type (and not `any`).
*
* @internal
*/
type IsUnknown<Type> = IsAny<Type> extends true ? false : unknown extends Type ? true : false;
/**
* Expands a type to its property listing so editor hovers show the resolved
* shape instead of a chain of aliases / intersections.
*
* @internal
*/
type Prettify<Type> = { [Key in keyof Type]: Type[Key] };
//#endregion
//#region src/diagnostic.d.ts
/**
* Define-time shape of a diagnostic. Each field can be a static value or a
* function that resolves it from a shared `params` object passed at call
* time. Runtime-only fields (`cause`, `sources`) from {@link DiagnosticInit}
* are intentionally omitted: they're only meaningful at the call site.
*/
interface DiagnosticDefinition<P = any> {
/**
* The error message: why this failed. String, or a function of `params`.
*
* @example
* ```ts
* why: (p: { name: string }) => `module "${p.name}" failed to load`
* ```
*/
why: ValueOrFn<string, P>;
/**
* Actionable instructions on how to resolve the problem. String, or a
* function of `params`.
*
* @example
* ```ts
* fix: (p: { name: string }) => `run "npm install ${p.name}"`
* ```
*/
fix?: ValueOrFn<string, P>;
/**
* Per-code docs URL. A string overrides
* {@link DefineDiagnosticsOptions.docsBase} for this code; `false` opts this
* code out entirely, even when `docsBase` is set. When omitted, the URL is
* derived from `docsBase`.
*/
docs?: string | false;
}
/**
* Runtime-only fields that can be passed alongside the interpolation params
* at call time. Merged into the same object so callers pass everything in
* one place.
*/
interface DiagnosticCallParams {
/**
* Original error or exception that triggered this diagnostic. Pass it
* through when re-throwing so the original stack trace is preserved.
*/
cause?: unknown;
/**
* Locations in user code that contributed to this diagnostic, in
* `file:line:column` format. Useful for compilers and other tools where the
* JS stack trace doesn't reflect the user's source.
*/
sources?: string[];
}
/**
* Structured initializer for a {@link Diagnostic}. `why` is the only required
* field: it becomes the {@link Diagnostic.message}. The remaining fields are
* optional metadata that reporters and consumers can render or forward.
*/
interface DiagnosticInit extends DiagnosticCallParams {
/**
* The actual error message: why this failed.
* Mirrored to `Error.message`.
*/
why: string;
/**
* Optional actionable instructions on how to resolve the problem.
*/
fix?: string;
/**
* URL to extended documentation for this diagnostic.
*/
docs?: string;
}
/**
* Represents how to report a diagnostic. Could call `console.log()`, send the
* diagnostic to a server, or something else. Reporters declare the shape of
* options they need via `ReporterOpts`; `defineDiagnostics` intersects every
* reporter's options into a single object passed at the call site.
*/
type DiagnosticReporter<ReporterOpts extends object = {}> = (diagnostic: Diagnostic, options: ReporterOpts) => void;
/**
* Permissive reporter constraint used internally so reporters with 1 arg,
* required options, or optional options all satisfy the array constraint.
*
* @internal
*/
type AnyDiagnosticReporter = (diagnostic: Diagnostic, options: any) => void;
/**
* The `console` methods a log reporter can route to.
*/
type ConsoleMethod = "log" | "error" | "warn";
/**
* Options for {@link createConsoleReporter}.
*/
interface ConsoleReporterOptions {
/**
* `console` method used to print the diagnostic. Defaults to `'warn'`. The
* returned reporter still accepts a per-call `{ method }` override through
* the call-site reporter options.
*/
method?: ConsoleMethod;
/**
* Renders the diagnostic into the string handed to `console`. Defaults to
* {@link formatDiagnostic}, the plain unicode-decorated formatter.
*/
formatter?: (diagnostic: Diagnostic) => string;
}
/**
* Creates a console reporter that renders each diagnostic with `formatter` and
* prints the result via `console[method]`. Both default sensibly (`'warn'` and
* {@link formatDiagnostic}); `method` can also be overridden per call through
* the reporter options.
*/
declare function createConsoleReporter({
method: defaultMethod,
formatter
}?: ConsoleReporterOptions): DiagnosticReporter<{
method?: ConsoleMethod;
}>;
/**
* Resolves the `params` type a code expects from the intersection of params
* across all function-typed fields, falling back to `{}` when every field is
* static. Merged with {@link DiagnosticCallParams} at the call site.
*
* @internal
*/
type InferCodeParams<Def> = [ExtractFnParam<Def[keyof Def]>] extends [never] ? {} : UnionToIntersection<ExtractFnParam<Def[keyof Def]>>;
/**
* Options for {@link defineDiagnostics}.
*/
interface DefineDiagnosticsOptions<Codes extends Record<string, DiagnosticDefinition>, Reporters extends readonly AnyDiagnosticReporter[]> {
/**
* Base URL or resolver for documentation links. When a string, the code is
* appended as a lowercase path segment (e.g. `"https://docs.example.com"` →
* `"https://docs.example.com/math_e001"`). When a function, receives the
* code and returns a URL or `undefined`.
*/
docsBase?: string | ((code: keyof Codes) => string | undefined);
/**
* Map of diagnostic codes to their definitions.
*/
codes: Codes;
/**
* Reporters called every time a diagnostic is produced. Can be used to
* integrate with custom logging.
*/
reporters?: Reporters;
}
/**
* The first positional argument of a {@link DiagnosticHandle} call:
* interpolation params merged with the runtime-only call-site fields
* (`cause`, `sources`).
*
* @internal
*/
type CallSiteParams<Params> = Params & DiagnosticCallParams;
/**
* Resolves the full argument tuple for a {@link DiagnosticHandle} call.
* Branches on whether params and reporter options each have required fields.
* Required positions become required tuple elements, all-optional ones
* become `?`, and when no reporter declares any options the parameter is
* omitted entirely.
*
* @internal
*/
type ActionArgs<Params, ReporterOpts> = keyof ReporterOpts extends never ? {} extends Params ? [params?: CallSiteParams<Params>] : [params: CallSiteParams<Params>] : {} extends ReporterOpts ? {} extends Params ? [params?: CallSiteParams<Params>, reporterOptions?: ReporterOpts] : [params: CallSiteParams<Params>, reporterOptions?: ReporterOpts] : {} extends Params ? [params: CallSiteParams<Params> | undefined, reporterOptions: ReporterOpts] : [params: CallSiteParams<Params>, reporterOptions: ReporterOpts];
/**
* Per-code handle exposed by {@link defineDiagnostics}. Each code is a
* callable: invoke it to build the diagnostic and run every reporter, or
* prefix the call with `throw` to raise it.
*
* @example
* ```ts
* diagnostics.MATH_E001({ name: 'x' }) // report
* throw diagnostics.MATH_E001({ name: 'x' }) // throw
* ```
*/
interface DiagnosticHandle<Params, ReporterOpts> {
/**
* Builds the diagnostic, runs every reporter, and returns the diagnostic
* instance. The returned diagnostic can be inspected, attached as `cause`,
* or thrown with `throw`.
*/
(...args: ActionArgs<Params, ReporterOpts>): Diagnostic;
}
/**
* Return type of {@link defineDiagnostics}.
*
* @internal
*/
type Diagnostics<Codes extends Record<string, DiagnosticDefinition>, Reporters extends readonly AnyDiagnosticReporter[]> = { [Code in keyof Codes]: DiagnosticHandle<InferCodeParams<Codes[Code]>, Prettify<ExtractReportersOptions<Reporters>>> };
declare class Diagnostic extends Error {
name: string;
/**
* URL to extended documentation for this diagnostic code.
* Auto-generated from {@link DefineDiagnosticsOptions.docsBase}.
*/
docs?: string;
/**
* Optional actionable instructions on how to resolve the problem.
*/
fix?: string;
/**
* Locations in user code that contributed to this diagnostic, in
* `file:line:column` format. Relevant when the stack trace doesn't reflect
* the user's source (e.g. compilers, bundlers), otherwise redundant with the
* stack and should be omitted.
*/
sources?: string[];
/**
* Alias for {@link Error.message}: the reason this diagnostic was raised.
*/
get why(): string;
/**
* @param init structured initializer; `why` is required
* @param captureFrom V8 stack-cutoff frame. Defaults to {@link Diagnostic}
* so the top of the trace is the `new Diagnostic(...)` call site.
* `defineDiagnostics` passes its action method to strip its own frames too.
* Ignored on engines without `Error.captureStackTrace`.
*/
constructor(init: DiagnosticInit, captureFrom?: Function);
/**
* Converts the diagnostic into a serializable structured object.
*/
toJSON(): object;
}
/**
* Creates a typed diagnostics object from a set of code definitions. Each
* code becomes a callable {@link DiagnosticHandle}: invoke to report, or
* `throw` the result to raise. No `new` required, no proxy.
*/
declare function defineDiagnostics<const Codes extends Record<string, DiagnosticDefinition>, const Reporters extends readonly AnyDiagnosticReporter[]>(options: DefineDiagnosticsOptions<Codes, Reporters>): Diagnostics<Codes, Reporters>;
/**
* Extracts the options object a reporter accepts as its 2nd argument. Returns
* `{}` when the reporter has no 2nd arg (so it contributes nothing to the
* merged shape).
*/
type ExtractSingleReporterOptions<Reporter> = Reporter extends ((diagnostic: Diagnostic, options: infer ReporterOpts) => any) ? IsUnknown<ReporterOpts> extends true ? {} : Exclude<ReporterOpts, undefined> : {};
/**
* Intersects every reporter's options shape into a single object. If any
* reporter has a required field, the merged shape has a required field, and
* {@link ActionArgs} flips `reporterOptions` from optional to required via
* `{} extends Merged`.
*/
type ExtractReportersOptions<Reporters extends readonly any[]> = Reporters extends readonly [infer First, ...infer Rest] ? ExtractSingleReporterOptions<First> & ExtractReportersOptions<Rest> : {};
//#endregion
export { Diagnostic as a, DiagnosticHandle as c, Diagnostics as d, createConsoleReporter as f, DefineDiagnosticsOptions as i, DiagnosticInit as l, ValueOrFn as m, ConsoleMethod as n, DiagnosticCallParams as o, defineDiagnostics as p, ConsoleReporterOptions as r, DiagnosticDefinition as s, AnyDiagnosticReporter as t, DiagnosticReporter as u };
//# sourceMappingURL=diagnostic-BajIZ3Zi.d.mts.map
+1
-1

@@ -1,2 +0,2 @@

import { i as Diagnostic } from "../diagnostic-ChOW5Avm.mjs";
import { a as Diagnostic } from "../diagnostic-BajIZ3Zi.mjs";

@@ -3,0 +3,0 @@ //#region src/formatters/ansi.d.ts

@@ -1,2 +0,2 @@

import { i as Diagnostic } from "../diagnostic-ChOW5Avm.mjs";
import { a as Diagnostic } from "../diagnostic-BajIZ3Zi.mjs";

@@ -3,0 +3,0 @@ //#region src/formatters/json.d.ts

@@ -1,2 +0,2 @@

import { a as DiagnosticCallParams, c as DiagnosticInit, d as defineDiagnostics, f as ValueOrFn, i as Diagnostic, l as DiagnosticReporter, n as ConsoleReporterOptions, o as DiagnosticDefinition, r as DefineDiagnosticsOptions, s as DiagnosticHandle, t as ConsoleMethod, u as createConsoleReporter } from "./diagnostic-ChOW5Avm.mjs";
import { a as Diagnostic, c as DiagnosticHandle, d as Diagnostics, f as createConsoleReporter, i as DefineDiagnosticsOptions, l as DiagnosticInit, m as ValueOrFn, n as ConsoleMethod, o as DiagnosticCallParams, p as defineDiagnostics, r as ConsoleReporterOptions, s as DiagnosticDefinition, t as AnyDiagnosticReporter, u as DiagnosticReporter } from "./diagnostic-BajIZ3Zi.mjs";

@@ -11,3 +11,42 @@ //#region src/formatters/plain.d.ts

//#endregion
export { type ConsoleMethod, type ConsoleReporterOptions, type DefineDiagnosticsOptions, Diagnostic, type DiagnosticCallParams, type DiagnosticDefinition, type DiagnosticHandle, type DiagnosticInit, type DiagnosticReporter, type ValueOrFn as _ValueOrFn, createConsoleReporter, defineDiagnostics, formatDiagnostic };
//#region src/prod-diagnostics.d.ts
/**
* Options for {@link defineProdDiagnostics}. A lean subset of
* {@link DefineDiagnosticsOptions}: no `codes` map (the proxy serves any code),
* only what is needed to keep behaviour correct in production.
*/
interface DefineProdDiagnosticsOptions<Reporters extends readonly AnyDiagnosticReporter[] = readonly AnyDiagnosticReporter[]> {
/**
* Base URL or resolver for documentation links, identical to
* {@link DefineDiagnosticsOptions.docsBase}. The docs URL is derived from the
* accessed code at call time, so links survive even without the catalog.
*/
docsBase?: string | ((code: string) => string | undefined);
/**
* Reporters called every time a diagnostic is produced. Omitted by default in
* production builds; the strip plugin can copy them into the prod branch when
* prod-time reporting (e.g. telemetry) is desired.
*/
reporters?: Reporters;
}
/**
* Production counterpart to {@link defineDiagnostics}. Returns a `Proxy` that
* builds a minimal {@link Diagnostic} for any accessed code: the code becomes
* the `message` (`why`), `name` stays the default `'Diagnostic'`, and `docs` is
* derived from `docsBase`. It carries no catalog text, so it stays tiny in a
* bundle.
*
* The strip plugin (`@nostics/unplugin`) can rewrite a `defineDiagnostics()`
* call into a `process.env.NODE_ENV === 'production'` ternary that selects this
* factory in production, dropping every `why`/`fix` string from the bundle.
*
* @example
* ```ts
* const diagnostics = defineProdDiagnostics({ docsBase: 'https://docs.example.com' })
* throw diagnostics.NUXT_B2011() // Error: NUXT_B2011, docs derived from docsBase
* ```
*/
declare function defineProdDiagnostics<const Codes extends Record<string, DiagnosticDefinition> = Record<string, DiagnosticDefinition>, const Reporters extends readonly AnyDiagnosticReporter[] = readonly AnyDiagnosticReporter[]>(options?: DefineProdDiagnosticsOptions<Reporters>): Diagnostics<Codes, Reporters>;
//#endregion
export { type ConsoleMethod, type ConsoleReporterOptions, type DefineDiagnosticsOptions, type DefineProdDiagnosticsOptions, Diagnostic, type DiagnosticCallParams, type DiagnosticDefinition, type DiagnosticHandle, type DiagnosticInit, type DiagnosticReporter, type ValueOrFn as _ValueOrFn, createConsoleReporter, defineDiagnostics, defineProdDiagnostics, formatDiagnostic };
//# sourceMappingURL=index.d.mts.map

@@ -59,3 +59,5 @@ //#region src/formatters/plain.ts

* Locations in user code that contributed to this diagnostic, in
* `file:line:column` format.
* `file:line:column` format. Relevant when the stack trace doesn't reflect
* the user's source (e.g. compilers, bundlers), otherwise redundant with the
* stack and should be omitted.
*/

@@ -99,2 +101,13 @@ sources;

/**
* Resolves the docs URL for a code from a `docsBase` (string template or
* resolver function). Shared by {@link defineDiagnostics} and
* {@link defineProdDiagnostics}. Per-code `docs` overrides are handled by the
* caller; this only covers the `docsBase`-derived case.
*
* @internal
*/
function deriveDocs(docsBase, code) {
return typeof docsBase === "string" ? `${docsBase}/${code.toLowerCase()}` : docsBase?.(code);
}
/**
* Creates a typed diagnostics object from a set of code definitions. Each

@@ -111,3 +124,3 @@ * code becomes a callable {@link DiagnosticHandle}: invoke to report, or

const def = options.codes[code];
const docs = def.docs === false ? void 0 : def.docs || (typeof docsBase === "string" ? `${docsBase}/${code.toLowerCase()}` : docsBase?.(code));
const docs = def.docs === false ? void 0 : def.docs || deriveDocs(docsBase, code);
const handle = (params = {}, reporterOptions = {}) => {

@@ -130,4 +143,41 @@ const diagnostic = new Diagnostic({

//#endregion
export { Diagnostic, createConsoleReporter, defineDiagnostics, formatDiagnostic };
//#region src/prod-diagnostics.ts
/**
* Production counterpart to {@link defineDiagnostics}. Returns a `Proxy` that
* builds a minimal {@link Diagnostic} for any accessed code: the code becomes
* the `message` (`why`), `name` stays the default `'Diagnostic'`, and `docs` is
* derived from `docsBase`. It carries no catalog text, so it stays tiny in a
* bundle.
*
* The strip plugin (`@nostics/unplugin`) can rewrite a `defineDiagnostics()`
* call into a `process.env.NODE_ENV === 'production'` ternary that selects this
* factory in production, dropping every `why`/`fix` string from the bundle.
*
* @example
* ```ts
* const diagnostics = defineProdDiagnostics({ docsBase: 'https://docs.example.com' })
* throw diagnostics.NUXT_B2011() // Error: NUXT_B2011, docs derived from docsBase
* ```
*/
/* @__NO_SIDE_EFFECTS__ */
function defineProdDiagnostics(options = {}) {
const { docsBase, reporters = [] } = options;
return new Proxy({}, { get(_target, code) {
if (typeof code !== "string") return void 0;
const handle = (params = {}, reporterOptions = {}) => {
const diagnostic = new Diagnostic({
why: code,
docs: deriveDocs(docsBase, code),
cause: params.cause,
sources: params.sources
}, handle);
for (const reporter of reporters) reporter(diagnostic, reporterOptions);
return diagnostic;
};
return handle;
} });
}
//#endregion
export { Diagnostic, createConsoleReporter, defineDiagnostics, defineProdDiagnostics, formatDiagnostic };
//# sourceMappingURL=index.mjs.map

@@ -1,1 +0,1 @@

{"version":3,"file":"index.mjs","names":[],"sources":["../src/formatters/plain.ts","../src/utils.ts","../src/diagnostic.ts"],"sourcesContent":["import type { Diagnostic } from '../diagnostic'\n\n/**\n * Renders a diagnostic into a multi-line, unicode-decorated string suitable\n * for terminal output. The first line is `[<name>] <message>`; optional\n * details (`fix`, `sources`, `docs`) follow with `├▶`/`╰▶` connectors.\n */\nexport function formatDiagnostic(diagnostic: Diagnostic): string {\n const header = `[${diagnostic.name}] ${diagnostic.message}`\n\n const details: string[] = []\n if (diagnostic.fix) {\n details.push(`fix: ${diagnostic.fix}`)\n }\n if (diagnostic.sources?.length) {\n details.push(`sources: ${diagnostic.sources.join(', ')}`)\n }\n if (diagnostic.docs) {\n details.push(`see: ${diagnostic.docs}`)\n }\n\n if (details.length === 0) {\n return header\n }\n\n const lines = details.map((detail, i) => {\n const connector = i < details.length - 1 ? '├▶' : '╰▶'\n return `${connector} ${detail}`\n })\n\n return [header, ...lines].join('\\n')\n}\n","/**\n * Transforms a value or a function that returns a value to a value.\n *\n * @param valFn either a value or a function that returns a value\n * @param args arguments to pass to the function if `valFn` is a function\n *\n * @internal\n */\nexport function toValueWithArgs<T, Args extends any[]>(\n valFn: T | ((...args: Args) => T),\n ...args: Args\n): T {\n return typeof valFn === 'function' ? (valFn as (...args: Args) => T)(...args) : valFn\n}\n\n/**\n * A value of type T, or a function that resolves T from a single params object.\n *\n * @internal\n */\nexport type ValueOrFn<T, P = any> = T | ((params: P) => T)\n\n/**\n * Extracts the param type from a single-arg function, or `never` for\n * non-function inputs. Pairs with {@link ValueOrFn}.\n *\n * @internal\n */\nexport type ExtractFnParam<T> = T extends (params: infer P) => any ? P : never\n\n/**\n * Converts a union of types to their intersection.\n *\n * @internal\n */\nexport type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (\n k: infer I,\n) => void\n ? I\n : never\n\n/**\n * `true` when `T` is the `any` type.\n *\n * @internal\n */\nexport type IsAny<Type> = 0 extends 1 & Type ? true : false\n\n/**\n * `true` when `T` is the `unknown` type (and not `any`).\n *\n * @internal\n */\nexport type IsUnknown<Type> = IsAny<Type> extends true ? false : unknown extends Type ? true : false\n\n/**\n * Expands a type to its property listing so editor hovers show the resolved\n * shape instead of a chain of aliases / intersections.\n *\n * @internal\n */\nexport type Prettify<Type> = {\n [Key in keyof Type]: Type[Key]\n}\n","/* eslint-disable ts/no-empty-object-type -- `{}` is used as the neutral element when intersecting reporter option shapes */\n/* eslint-disable ts/no-unsafe-function-type -- used by captureStackTrace */\n\nimport type { ExtractFnParam, IsUnknown, Prettify, UnionToIntersection, ValueOrFn } from './utils'\nimport { formatDiagnostic } from './formatters/plain'\nimport { toValueWithArgs } from './utils'\n\n/**\n * Define-time shape of a diagnostic. Each field can be a static value or a\n * function that resolves it from a shared `params` object passed at call\n * time. Runtime-only fields (`cause`, `sources`) from {@link DiagnosticInit}\n * are intentionally omitted: they're only meaningful at the call site.\n */\nexport interface DiagnosticDefinition<P = any> {\n /**\n * The error message: why this failed. String, or a function of `params`.\n *\n * @example\n * ```ts\n * why: (p: { name: string }) => `module \"${p.name}\" failed to load`\n * ```\n */\n why: ValueOrFn<string, P>\n\n /**\n * Actionable instructions on how to resolve the problem. String, or a\n * function of `params`.\n *\n * @example\n * ```ts\n * fix: (p: { name: string }) => `run \"npm install ${p.name}\"`\n * ```\n */\n fix?: ValueOrFn<string, P>\n\n /**\n * Per-code docs URL. A string overrides\n * {@link DefineDiagnosticsOptions.docsBase} for this code; `false` opts this\n * code out entirely, even when `docsBase` is set. When omitted, the URL is\n * derived from `docsBase`.\n */\n docs?: string | false\n}\n\n/**\n * Runtime-only fields that can be passed alongside the interpolation params\n * at call time. Merged into the same object so callers pass everything in\n * one place.\n */\nexport interface DiagnosticCallParams {\n /**\n * Original error or exception that triggered this diagnostic. Pass it\n * through when re-throwing so the original stack trace is preserved.\n */\n cause?: unknown\n\n /**\n * Locations in user code that contributed to this diagnostic, in\n * `file:line:column` format. Useful for compilers and other tools where the\n * JS stack trace doesn't reflect the user's source.\n */\n sources?: string[]\n}\n\n/**\n * Structured initializer for a {@link Diagnostic}. `why` is the only required\n * field: it becomes the {@link Diagnostic.message}. The remaining fields are\n * optional metadata that reporters and consumers can render or forward.\n */\nexport interface DiagnosticInit extends DiagnosticCallParams {\n /**\n * The actual error message: why this failed.\n * Mirrored to `Error.message`.\n */\n why: string\n\n /**\n * Optional actionable instructions on how to resolve the problem.\n */\n fix?: string\n\n /**\n * URL to extended documentation for this diagnostic.\n */\n docs?: string\n}\n\n/**\n * Represents how to report a diagnostic. Could call `console.log()`, send the\n * diagnostic to a server, or something else. Reporters declare the shape of\n * options they need via `ReporterOpts`; `defineDiagnostics` intersects every\n * reporter's options into a single object passed at the call site.\n */\nexport type DiagnosticReporter<ReporterOpts extends object = {}> = (\n diagnostic: Diagnostic,\n options: ReporterOpts,\n) => void\n\n/**\n * Permissive reporter constraint used internally so reporters with 1 arg,\n * required options, or optional options all satisfy the array constraint.\n *\n * @internal\n */\ntype AnyDiagnosticReporter = (diagnostic: Diagnostic, options: any) => void\n\n/**\n * The `console` methods a log reporter can route to.\n */\nexport type ConsoleMethod = 'log' | 'error' | 'warn'\n\n/**\n * Options for {@link createConsoleReporter}.\n */\nexport interface ConsoleReporterOptions {\n /**\n * `console` method used to print the diagnostic. Defaults to `'warn'`. The\n * returned reporter still accepts a per-call `{ method }` override through\n * the call-site reporter options.\n */\n method?: ConsoleMethod\n\n /**\n * Renders the diagnostic into the string handed to `console`. Defaults to\n * {@link formatDiagnostic}, the plain unicode-decorated formatter.\n */\n formatter?: (diagnostic: Diagnostic) => string\n}\n\n/**\n * Creates a console reporter that renders each diagnostic with `formatter` and\n * prints the result via `console[method]`. Both default sensibly (`'warn'` and\n * {@link formatDiagnostic}); `method` can also be overridden per call through\n * the reporter options.\n */\n/* @__NO_SIDE_EFFECTS__ */\nexport function createConsoleReporter({\n method: defaultMethod = 'warn',\n formatter = formatDiagnostic,\n}: ConsoleReporterOptions = {}): DiagnosticReporter<{ method?: ConsoleMethod }> {\n return (diagnostic, { method = defaultMethod } = {}) => {\n // eslint-disable-next-line no-console\n console[method](formatter(diagnostic))\n }\n}\n\n/**\n * Resolves the `params` type a code expects from the intersection of params\n * across all function-typed fields, falling back to `{}` when every field is\n * static. Merged with {@link DiagnosticCallParams} at the call site.\n *\n * @internal\n */\ntype InferCodeParams<Def> = [ExtractFnParam<Def[keyof Def]>] extends [never]\n ? {}\n : UnionToIntersection<ExtractFnParam<Def[keyof Def]>>\n\n/**\n * Options for {@link defineDiagnostics}.\n */\nexport interface DefineDiagnosticsOptions<\n Codes extends Record<string, DiagnosticDefinition>,\n Reporters extends readonly AnyDiagnosticReporter[],\n> {\n /**\n * Base URL or resolver for documentation links. When a string, the code is\n * appended as a lowercase path segment (e.g. `\"https://docs.example.com\"` →\n * `\"https://docs.example.com/math_e001\"`). When a function, receives the\n * code and returns a URL or `undefined`.\n */\n docsBase?: string | ((code: keyof Codes) => string | undefined)\n\n /**\n * Map of diagnostic codes to their definitions.\n */\n codes: Codes\n\n /**\n * Reporters called every time a diagnostic is produced. Can be used to\n * integrate with custom logging.\n */\n reporters?: Reporters\n}\n\n/**\n * The first positional argument of a {@link DiagnosticHandle} call:\n * interpolation params merged with the runtime-only call-site fields\n * (`cause`, `sources`).\n *\n * @internal\n */\ntype CallSiteParams<Params> = Params & DiagnosticCallParams\n\n/**\n * Resolves the full argument tuple for a {@link DiagnosticHandle} call.\n * Branches on whether params and reporter options each have required fields.\n * Required positions become required tuple elements, all-optional ones\n * become `?`, and when no reporter declares any options the parameter is\n * omitted entirely.\n *\n * @internal\n */\ntype ActionArgs<Params, ReporterOpts> = keyof ReporterOpts extends never\n ? {} extends Params\n ? [params?: CallSiteParams<Params>]\n : [params: CallSiteParams<Params>]\n : {} extends ReporterOpts\n ? {} extends Params\n ? [params?: CallSiteParams<Params>, reporterOptions?: ReporterOpts]\n : [params: CallSiteParams<Params>, reporterOptions?: ReporterOpts]\n : {} extends Params\n ? [params: CallSiteParams<Params> | undefined, reporterOptions: ReporterOpts]\n : [params: CallSiteParams<Params>, reporterOptions: ReporterOpts]\n\n/**\n * Per-code handle exposed by {@link defineDiagnostics}. Each code is a\n * callable: invoke it to build the diagnostic and run every reporter, or\n * prefix the call with `throw` to raise it.\n *\n * @example\n * ```ts\n * diagnostics.MATH_E001({ name: 'x' }) // report\n * throw diagnostics.MATH_E001({ name: 'x' }) // throw\n * ```\n */\nexport interface DiagnosticHandle<Params, ReporterOpts> {\n /**\n * Builds the diagnostic, runs every reporter, and returns the diagnostic\n * instance. The returned diagnostic can be inspected, attached as `cause`,\n * or thrown with `throw`.\n */\n (...args: ActionArgs<Params, ReporterOpts>): Diagnostic\n}\n\n/**\n * Return type of {@link defineDiagnostics}.\n */\ntype Diagnostics<\n Codes extends Record<string, DiagnosticDefinition>,\n Reporters extends readonly AnyDiagnosticReporter[],\n> = {\n [Code in keyof Codes]: DiagnosticHandle<\n InferCodeParams<Codes[Code]>,\n Prettify<ExtractReportersOptions<Reporters>>\n >\n}\n\nconst captureStackTrace = (\n Error as { captureStackTrace?: (target: object, frame: Function) => void }\n).captureStackTrace\n\nexport class Diagnostic extends Error {\n name: string = 'Diagnostic'\n\n /**\n * URL to extended documentation for this diagnostic code.\n * Auto-generated from {@link DefineDiagnosticsOptions.docsBase}.\n */\n docs?: string\n\n /**\n * Optional actionable instructions on how to resolve the problem.\n */\n fix?: string\n\n /**\n * Locations in user code that contributed to this diagnostic, in\n * `file:line:column` format.\n */\n sources?: string[]\n\n /**\n * Alias for {@link Error.message}: the reason this diagnostic was raised.\n */\n get why(): string {\n return this.message\n }\n\n /**\n * @param init structured initializer; `why` is required\n * @param captureFrom V8 stack-cutoff frame. Defaults to {@link Diagnostic}\n * so the top of the trace is the `new Diagnostic(...)` call site.\n * `defineDiagnostics` passes its action method to strip its own frames too.\n * Ignored on engines without `Error.captureStackTrace`.\n */\n constructor(init: DiagnosticInit, captureFrom: Function = Diagnostic) {\n super(init.why, { cause: init.cause })\n this.fix = init.fix\n this.docs = init.docs\n this.sources = init.sources\n // V8-only API, but also implemented pretty much everywhere. Worst case\n // scenario, we fall back to the stack `Error` captures by default, which\n // includes a couple of extra internal frames but is still usable.\n captureStackTrace?.(this, captureFrom)\n }\n\n /**\n * Converts the diagnostic into a serializable structured object.\n */\n toJSON(): object {\n return {\n name: this.name,\n why: this.why,\n fix: this.fix,\n docs: this.docs,\n sources: this.sources,\n cause: this.cause,\n stack: this.stack,\n }\n }\n}\n\n/**\n * Creates a typed diagnostics object from a set of code definitions. Each\n * code becomes a callable {@link DiagnosticHandle}: invoke to report, or\n * `throw` the result to raise. No `new` required, no proxy.\n */\n/* @__NO_SIDE_EFFECTS__ */\nexport function defineDiagnostics<\n const Codes extends Record<string, DiagnosticDefinition>,\n const Reporters extends readonly AnyDiagnosticReporter[],\n>(options: DefineDiagnosticsOptions<Codes, Reporters>): Diagnostics<Codes, Reporters> {\n const reporters = options.reporters ?? []\n const result = {} as Diagnostics<Codes, Reporters>\n\n const { docsBase } = options\n\n for (const code of Object.keys(options.codes) as Extract<keyof Codes, string>[]) {\n const def = options.codes[code]\n // skip docs if set to false, otherwise use it or derive it from docsBase\n const docs\n = def.docs === false\n ? undefined\n : def.docs\n || (typeof docsBase === 'string' ? `${docsBase}/${code.toLowerCase()}` : docsBase?.(code))\n\n const handle = (\n params: DiagnosticCallParams & Record<string, unknown> = {},\n reporterOptions: any = {},\n ): Diagnostic => {\n const diagnostic = new Diagnostic(\n {\n why: toValueWithArgs(def.why, params),\n fix: toValueWithArgs(def.fix, params),\n docs,\n cause: params.cause,\n sources: params.sources,\n },\n handle,\n )\n diagnostic.name = code\n for (const reporter of reporters) reporter(diagnostic, reporterOptions)\n return diagnostic\n }\n\n result[code] = handle as unknown as Diagnostics<Codes, Reporters>[typeof code]\n }\n\n return result\n}\n\n/**\n * Extracts the options object a reporter accepts as its 2nd argument. Returns\n * `{}` when the reporter has no 2nd arg (so it contributes nothing to the\n * merged shape).\n */\ntype ExtractSingleReporterOptions<Reporter> = Reporter extends (\n diagnostic: Diagnostic,\n options: infer ReporterOpts,\n) => any\n ? IsUnknown<ReporterOpts> extends true\n ? {}\n : Exclude<ReporterOpts, undefined>\n : {}\n\n/**\n * Intersects every reporter's options shape into a single object. If any\n * reporter has a required field, the merged shape has a required field, and\n * {@link ActionArgs} flips `reporterOptions` from optional to required via\n * `{} extends Merged`.\n */\ntype ExtractReportersOptions<Reporters extends readonly any[]> = Reporters extends readonly [\n infer First,\n ...infer Rest,\n]\n ? ExtractSingleReporterOptions<First> & ExtractReportersOptions<Rest>\n : {}\n"],"mappings":";;;;;;AAOA,SAAgB,iBAAiB,YAAgC;CAC/D,MAAM,SAAS,IAAI,WAAW,KAAK,IAAI,WAAW;CAElD,MAAM,UAAoB,CAAC;CAC3B,IAAI,WAAW,KACb,QAAQ,KAAK,QAAQ,WAAW,KAAK;CAEvC,IAAI,WAAW,SAAS,QACtB,QAAQ,KAAK,YAAY,WAAW,QAAQ,KAAK,IAAI,GAAG;CAE1D,IAAI,WAAW,MACb,QAAQ,KAAK,QAAQ,WAAW,MAAM;CAGxC,IAAI,QAAQ,WAAW,GACrB,OAAO;CAQT,OAAO,CAAC,QAAQ,GALF,QAAQ,KAAK,QAAQ,MAAM;EAEvC,OAAO,GADW,IAAI,QAAQ,SAAS,IAAI,OAAO,KAC9B,GAAG;CACzB,CAEuB,CAAC,CAAC,CAAC,KAAK,IAAI;AACrC;;;;;;;;;;;ACvBA,SAAgB,gBACd,OACA,GAAG,MACA;CACH,OAAO,OAAO,UAAU,aAAc,MAA+B,GAAG,IAAI,IAAI;AAClF;;;;;;;;;;AC2HA,SAAgB,sBAAsB,EACpC,QAAQ,gBAAgB,QACxB,YAAY,qBACc,CAAC,GAAmD;CAC9E,QAAQ,YAAY,EAAE,SAAS,kBAAkB,CAAC,MAAM;EAEtD,QAAQ,OAAO,CAAC,UAAU,UAAU,CAAC;CACvC;AACF;AAuGA,MAAM,oBACJ,MACA;AAEF,IAAa,aAAb,MAAa,mBAAmB,MAAM;CACpC,OAAe;;;;;CAMf;;;;CAKA;;;;;CAMA;;;;CAKA,IAAI,MAAc;EAChB,OAAO,KAAK;CACd;;;;;;;;CASA,YAAY,MAAsB,cAAwB,YAAY;EACpE,MAAM,KAAK,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;EACrC,KAAK,MAAM,KAAK;EAChB,KAAK,OAAO,KAAK;EACjB,KAAK,UAAU,KAAK;EAIpB,oBAAoB,MAAM,WAAW;CACvC;;;;CAKA,SAAiB;EACf,OAAO;GACL,MAAM,KAAK;GACX,KAAK,KAAK;GACV,KAAK,KAAK;GACV,MAAM,KAAK;GACX,SAAS,KAAK;GACd,OAAO,KAAK;GACZ,OAAO,KAAK;EACd;CACF;AACF;;;;;;;AAQA,SAAgB,kBAGd,SAAoF;CACpF,MAAM,YAAY,QAAQ,aAAa,CAAC;CACxC,MAAM,SAAS,CAAC;CAEhB,MAAM,EAAE,aAAa;CAErB,KAAK,MAAM,QAAQ,OAAO,KAAK,QAAQ,KAAK,GAAqC;EAC/E,MAAM,MAAM,QAAQ,MAAM;EAE1B,MAAM,OACF,IAAI,SAAS,QACX,KAAA,IACA,IAAI,SACA,OAAO,aAAa,WAAW,GAAG,SAAS,GAAG,KAAK,YAAY,MAAM,WAAW,IAAI;EAE9F,MAAM,UACJ,SAAyD,CAAC,GAC1D,kBAAuB,CAAC,MACT;GACf,MAAM,aAAa,IAAI,WACrB;IACE,KAAK,gBAAgB,IAAI,KAAK,MAAM;IACpC,KAAK,gBAAgB,IAAI,KAAK,MAAM;IACpC;IACA,OAAO,OAAO;IACd,SAAS,OAAO;GAClB,GACA,MACF;GACA,WAAW,OAAO;GAClB,KAAK,MAAM,YAAY,WAAW,SAAS,YAAY,eAAe;GACtE,OAAO;EACT;EAEA,OAAO,QAAQ;CACjB;CAEA,OAAO;AACT"}
{"version":3,"file":"index.mjs","names":[],"sources":["../src/formatters/plain.ts","../src/utils.ts","../src/diagnostic.ts","../src/prod-diagnostics.ts"],"sourcesContent":["import type { Diagnostic } from '../diagnostic'\n\n/**\n * Renders a diagnostic into a multi-line, unicode-decorated string suitable\n * for terminal output. The first line is `[<name>] <message>`; optional\n * details (`fix`, `sources`, `docs`) follow with `├▶`/`╰▶` connectors.\n */\nexport function formatDiagnostic(diagnostic: Diagnostic): string {\n const header = `[${diagnostic.name}] ${diagnostic.message}`\n\n const details: string[] = []\n if (diagnostic.fix) {\n details.push(`fix: ${diagnostic.fix}`)\n }\n if (diagnostic.sources?.length) {\n details.push(`sources: ${diagnostic.sources.join(', ')}`)\n }\n if (diagnostic.docs) {\n details.push(`see: ${diagnostic.docs}`)\n }\n\n if (details.length === 0) {\n return header\n }\n\n const lines = details.map((detail, i) => {\n const connector = i < details.length - 1 ? '├▶' : '╰▶'\n return `${connector} ${detail}`\n })\n\n return [header, ...lines].join('\\n')\n}\n","/**\n * Transforms a value or a function that returns a value to a value.\n *\n * @param valFn either a value or a function that returns a value\n * @param args arguments to pass to the function if `valFn` is a function\n *\n * @internal\n */\nexport function toValueWithArgs<T, Args extends any[]>(\n valFn: T | ((...args: Args) => T),\n ...args: Args\n): T {\n return typeof valFn === 'function' ? (valFn as (...args: Args) => T)(...args) : valFn\n}\n\n/**\n * A value of type T, or a function that resolves T from a single params object.\n *\n * @internal\n */\nexport type ValueOrFn<T, P = any> = T | ((params: P) => T)\n\n/**\n * Extracts the param type from a single-arg function, or `never` for\n * non-function inputs. Pairs with {@link ValueOrFn}.\n *\n * @internal\n */\nexport type ExtractFnParam<T> = T extends (params: infer P) => any ? P : never\n\n/**\n * Converts a union of types to their intersection.\n *\n * @internal\n */\nexport type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (\n k: infer I,\n) => void\n ? I\n : never\n\n/**\n * `true` when `T` is the `any` type.\n *\n * @internal\n */\nexport type IsAny<Type> = 0 extends 1 & Type ? true : false\n\n/**\n * `true` when `T` is the `unknown` type (and not `any`).\n *\n * @internal\n */\nexport type IsUnknown<Type> = IsAny<Type> extends true ? false : unknown extends Type ? true : false\n\n/**\n * Expands a type to its property listing so editor hovers show the resolved\n * shape instead of a chain of aliases / intersections.\n *\n * @internal\n */\nexport type Prettify<Type> = {\n [Key in keyof Type]: Type[Key]\n}\n","/* eslint-disable ts/no-empty-object-type -- `{}` is used as the neutral element when intersecting reporter option shapes */\n/* eslint-disable ts/no-unsafe-function-type -- used by captureStackTrace */\n\nimport type { ExtractFnParam, IsUnknown, Prettify, UnionToIntersection, ValueOrFn } from './utils'\nimport { formatDiagnostic } from './formatters/plain'\nimport { toValueWithArgs } from './utils'\n\n/**\n * Define-time shape of a diagnostic. Each field can be a static value or a\n * function that resolves it from a shared `params` object passed at call\n * time. Runtime-only fields (`cause`, `sources`) from {@link DiagnosticInit}\n * are intentionally omitted: they're only meaningful at the call site.\n */\nexport interface DiagnosticDefinition<P = any> {\n /**\n * The error message: why this failed. String, or a function of `params`.\n *\n * @example\n * ```ts\n * why: (p: { name: string }) => `module \"${p.name}\" failed to load`\n * ```\n */\n why: ValueOrFn<string, P>\n\n /**\n * Actionable instructions on how to resolve the problem. String, or a\n * function of `params`.\n *\n * @example\n * ```ts\n * fix: (p: { name: string }) => `run \"npm install ${p.name}\"`\n * ```\n */\n fix?: ValueOrFn<string, P>\n\n /**\n * Per-code docs URL. A string overrides\n * {@link DefineDiagnosticsOptions.docsBase} for this code; `false` opts this\n * code out entirely, even when `docsBase` is set. When omitted, the URL is\n * derived from `docsBase`.\n */\n docs?: string | false\n}\n\n/**\n * Runtime-only fields that can be passed alongside the interpolation params\n * at call time. Merged into the same object so callers pass everything in\n * one place.\n */\nexport interface DiagnosticCallParams {\n /**\n * Original error or exception that triggered this diagnostic. Pass it\n * through when re-throwing so the original stack trace is preserved.\n */\n cause?: unknown\n\n /**\n * Locations in user code that contributed to this diagnostic, in\n * `file:line:column` format. Useful for compilers and other tools where the\n * JS stack trace doesn't reflect the user's source.\n */\n sources?: string[]\n}\n\n/**\n * Structured initializer for a {@link Diagnostic}. `why` is the only required\n * field: it becomes the {@link Diagnostic.message}. The remaining fields are\n * optional metadata that reporters and consumers can render or forward.\n */\nexport interface DiagnosticInit extends DiagnosticCallParams {\n /**\n * The actual error message: why this failed.\n * Mirrored to `Error.message`.\n */\n why: string\n\n /**\n * Optional actionable instructions on how to resolve the problem.\n */\n fix?: string\n\n /**\n * URL to extended documentation for this diagnostic.\n */\n docs?: string\n}\n\n/**\n * Represents how to report a diagnostic. Could call `console.log()`, send the\n * diagnostic to a server, or something else. Reporters declare the shape of\n * options they need via `ReporterOpts`; `defineDiagnostics` intersects every\n * reporter's options into a single object passed at the call site.\n */\nexport type DiagnosticReporter<ReporterOpts extends object = {}> = (\n diagnostic: Diagnostic,\n options: ReporterOpts,\n) => void\n\n/**\n * Permissive reporter constraint used internally so reporters with 1 arg,\n * required options, or optional options all satisfy the array constraint.\n *\n * @internal\n */\nexport type AnyDiagnosticReporter = (diagnostic: Diagnostic, options: any) => void\n\n/**\n * The `console` methods a log reporter can route to.\n */\nexport type ConsoleMethod = 'log' | 'error' | 'warn'\n\n/**\n * Options for {@link createConsoleReporter}.\n */\nexport interface ConsoleReporterOptions {\n /**\n * `console` method used to print the diagnostic. Defaults to `'warn'`. The\n * returned reporter still accepts a per-call `{ method }` override through\n * the call-site reporter options.\n */\n method?: ConsoleMethod\n\n /**\n * Renders the diagnostic into the string handed to `console`. Defaults to\n * {@link formatDiagnostic}, the plain unicode-decorated formatter.\n */\n formatter?: (diagnostic: Diagnostic) => string\n}\n\n/**\n * Creates a console reporter that renders each diagnostic with `formatter` and\n * prints the result via `console[method]`. Both default sensibly (`'warn'` and\n * {@link formatDiagnostic}); `method` can also be overridden per call through\n * the reporter options.\n */\n/* @__NO_SIDE_EFFECTS__ */\nexport function createConsoleReporter({\n method: defaultMethod = 'warn',\n formatter = formatDiagnostic,\n}: ConsoleReporterOptions = {}): DiagnosticReporter<{ method?: ConsoleMethod }> {\n return (diagnostic, { method = defaultMethod } = {}) => {\n // eslint-disable-next-line no-console\n console[method](formatter(diagnostic))\n }\n}\n\n/**\n * Resolves the `params` type a code expects from the intersection of params\n * across all function-typed fields, falling back to `{}` when every field is\n * static. Merged with {@link DiagnosticCallParams} at the call site.\n *\n * @internal\n */\ntype InferCodeParams<Def> = [ExtractFnParam<Def[keyof Def]>] extends [never]\n ? {}\n : UnionToIntersection<ExtractFnParam<Def[keyof Def]>>\n\n/**\n * Options for {@link defineDiagnostics}.\n */\nexport interface DefineDiagnosticsOptions<\n Codes extends Record<string, DiagnosticDefinition>,\n Reporters extends readonly AnyDiagnosticReporter[],\n> {\n /**\n * Base URL or resolver for documentation links. When a string, the code is\n * appended as a lowercase path segment (e.g. `\"https://docs.example.com\"` →\n * `\"https://docs.example.com/math_e001\"`). When a function, receives the\n * code and returns a URL or `undefined`.\n */\n docsBase?: string | ((code: keyof Codes) => string | undefined)\n\n /**\n * Map of diagnostic codes to their definitions.\n */\n codes: Codes\n\n /**\n * Reporters called every time a diagnostic is produced. Can be used to\n * integrate with custom logging.\n */\n reporters?: Reporters\n}\n\n/**\n * The first positional argument of a {@link DiagnosticHandle} call:\n * interpolation params merged with the runtime-only call-site fields\n * (`cause`, `sources`).\n *\n * @internal\n */\ntype CallSiteParams<Params> = Params & DiagnosticCallParams\n\n/**\n * Resolves the full argument tuple for a {@link DiagnosticHandle} call.\n * Branches on whether params and reporter options each have required fields.\n * Required positions become required tuple elements, all-optional ones\n * become `?`, and when no reporter declares any options the parameter is\n * omitted entirely.\n *\n * @internal\n */\ntype ActionArgs<Params, ReporterOpts> = keyof ReporterOpts extends never\n ? {} extends Params\n ? [params?: CallSiteParams<Params>]\n : [params: CallSiteParams<Params>]\n : {} extends ReporterOpts\n ? {} extends Params\n ? [params?: CallSiteParams<Params>, reporterOptions?: ReporterOpts]\n : [params: CallSiteParams<Params>, reporterOptions?: ReporterOpts]\n : {} extends Params\n ? [params: CallSiteParams<Params> | undefined, reporterOptions: ReporterOpts]\n : [params: CallSiteParams<Params>, reporterOptions: ReporterOpts]\n\n/**\n * Per-code handle exposed by {@link defineDiagnostics}. Each code is a\n * callable: invoke it to build the diagnostic and run every reporter, or\n * prefix the call with `throw` to raise it.\n *\n * @example\n * ```ts\n * diagnostics.MATH_E001({ name: 'x' }) // report\n * throw diagnostics.MATH_E001({ name: 'x' }) // throw\n * ```\n */\nexport interface DiagnosticHandle<Params, ReporterOpts> {\n /**\n * Builds the diagnostic, runs every reporter, and returns the diagnostic\n * instance. The returned diagnostic can be inspected, attached as `cause`,\n * or thrown with `throw`.\n */\n (...args: ActionArgs<Params, ReporterOpts>): Diagnostic\n}\n\n/**\n * Return type of {@link defineDiagnostics}.\n *\n * @internal\n */\nexport type Diagnostics<\n Codes extends Record<string, DiagnosticDefinition>,\n Reporters extends readonly AnyDiagnosticReporter[],\n> = {\n [Code in keyof Codes]: DiagnosticHandle<\n InferCodeParams<Codes[Code]>,\n Prettify<ExtractReportersOptions<Reporters>>\n >\n}\n\nconst captureStackTrace = (\n Error as { captureStackTrace?: (target: object, frame: Function) => void }\n).captureStackTrace\n\nexport class Diagnostic extends Error {\n name: string = 'Diagnostic'\n\n /**\n * URL to extended documentation for this diagnostic code.\n * Auto-generated from {@link DefineDiagnosticsOptions.docsBase}.\n */\n docs?: string\n\n /**\n * Optional actionable instructions on how to resolve the problem.\n */\n fix?: string\n\n /**\n * Locations in user code that contributed to this diagnostic, in\n * `file:line:column` format. Relevant when the stack trace doesn't reflect\n * the user's source (e.g. compilers, bundlers), otherwise redundant with the\n * stack and should be omitted.\n */\n sources?: string[]\n\n /**\n * Alias for {@link Error.message}: the reason this diagnostic was raised.\n */\n get why(): string {\n return this.message\n }\n\n /**\n * @param init structured initializer; `why` is required\n * @param captureFrom V8 stack-cutoff frame. Defaults to {@link Diagnostic}\n * so the top of the trace is the `new Diagnostic(...)` call site.\n * `defineDiagnostics` passes its action method to strip its own frames too.\n * Ignored on engines without `Error.captureStackTrace`.\n */\n constructor(init: DiagnosticInit, captureFrom: Function = Diagnostic) {\n super(init.why, { cause: init.cause })\n this.fix = init.fix\n this.docs = init.docs\n this.sources = init.sources\n // V8-only API, but also implemented pretty much everywhere. Worst case\n // scenario, we fall back to the stack `Error` captures by default, which\n // includes a couple of extra internal frames but is still usable.\n captureStackTrace?.(this, captureFrom)\n }\n\n /**\n * Converts the diagnostic into a serializable structured object.\n */\n toJSON(): object {\n return {\n name: this.name,\n why: this.why,\n fix: this.fix,\n docs: this.docs,\n sources: this.sources,\n cause: this.cause,\n stack: this.stack,\n }\n }\n}\n\n/**\n * Resolves the docs URL for a code from a `docsBase` (string template or\n * resolver function). Shared by {@link defineDiagnostics} and\n * {@link defineProdDiagnostics}. Per-code `docs` overrides are handled by the\n * caller; this only covers the `docsBase`-derived case.\n *\n * @internal\n */\nexport function deriveDocs(\n docsBase: string | ((code: any) => string | undefined) | undefined,\n code: string,\n): string | undefined {\n return typeof docsBase === 'string' ? `${docsBase}/${code.toLowerCase()}` : docsBase?.(code)\n}\n\n/**\n * Creates a typed diagnostics object from a set of code definitions. Each\n * code becomes a callable {@link DiagnosticHandle}: invoke to report, or\n * `throw` the result to raise. No `new` required, no proxy.\n */\n/* @__NO_SIDE_EFFECTS__ */\nexport function defineDiagnostics<\n const Codes extends Record<string, DiagnosticDefinition>,\n const Reporters extends readonly AnyDiagnosticReporter[],\n>(options: DefineDiagnosticsOptions<Codes, Reporters>): Diagnostics<Codes, Reporters> {\n const reporters = options.reporters ?? []\n const result = {} as Diagnostics<Codes, Reporters>\n\n const { docsBase } = options\n\n for (const code of Object.keys(options.codes) as Extract<keyof Codes, string>[]) {\n const def = options.codes[code]\n // skip docs if set to false, otherwise use it or derive it from docsBase\n const docs = def.docs === false ? undefined : def.docs || deriveDocs(docsBase, code)\n\n const handle = (\n params: DiagnosticCallParams & Record<string, unknown> = {},\n reporterOptions: any = {},\n ): Diagnostic => {\n const diagnostic = new Diagnostic(\n {\n why: toValueWithArgs(def.why, params),\n fix: toValueWithArgs(def.fix, params),\n docs,\n cause: params.cause,\n sources: params.sources,\n },\n handle,\n )\n diagnostic.name = code\n for (const reporter of reporters) reporter(diagnostic, reporterOptions)\n return diagnostic\n }\n\n result[code] = handle as unknown as Diagnostics<Codes, Reporters>[typeof code]\n }\n\n return result\n}\n\n/**\n * Extracts the options object a reporter accepts as its 2nd argument. Returns\n * `{}` when the reporter has no 2nd arg (so it contributes nothing to the\n * merged shape).\n */\ntype ExtractSingleReporterOptions<Reporter> = Reporter extends (\n diagnostic: Diagnostic,\n options: infer ReporterOpts,\n) => any\n ? IsUnknown<ReporterOpts> extends true\n ? {}\n : Exclude<ReporterOpts, undefined>\n : {}\n\n/**\n * Intersects every reporter's options shape into a single object. If any\n * reporter has a required field, the merged shape has a required field, and\n * {@link ActionArgs} flips `reporterOptions` from optional to required via\n * `{} extends Merged`.\n */\ntype ExtractReportersOptions<Reporters extends readonly any[]> = Reporters extends readonly [\n infer First,\n ...infer Rest,\n]\n ? ExtractSingleReporterOptions<First> & ExtractReportersOptions<Rest>\n : {}\n","import type {\n AnyDiagnosticReporter,\n DiagnosticCallParams,\n DiagnosticDefinition,\n Diagnostics,\n} from './diagnostic'\nimport { deriveDocs, Diagnostic } from './diagnostic'\n\n/**\n * Options for {@link defineProdDiagnostics}. A lean subset of\n * {@link DefineDiagnosticsOptions}: no `codes` map (the proxy serves any code),\n * only what is needed to keep behaviour correct in production.\n */\nexport interface DefineProdDiagnosticsOptions<\n Reporters extends readonly AnyDiagnosticReporter[] = readonly AnyDiagnosticReporter[],\n> {\n /**\n * Base URL or resolver for documentation links, identical to\n * {@link DefineDiagnosticsOptions.docsBase}. The docs URL is derived from the\n * accessed code at call time, so links survive even without the catalog.\n */\n docsBase?: string | ((code: string) => string | undefined)\n\n /**\n * Reporters called every time a diagnostic is produced. Omitted by default in\n * production builds; the strip plugin can copy them into the prod branch when\n * prod-time reporting (e.g. telemetry) is desired.\n */\n reporters?: Reporters\n}\n\n/**\n * Production counterpart to {@link defineDiagnostics}. Returns a `Proxy` that\n * builds a minimal {@link Diagnostic} for any accessed code: the code becomes\n * the `message` (`why`), `name` stays the default `'Diagnostic'`, and `docs` is\n * derived from `docsBase`. It carries no catalog text, so it stays tiny in a\n * bundle.\n *\n * The strip plugin (`@nostics/unplugin`) can rewrite a `defineDiagnostics()`\n * call into a `process.env.NODE_ENV === 'production'` ternary that selects this\n * factory in production, dropping every `why`/`fix` string from the bundle.\n *\n * @example\n * ```ts\n * const diagnostics = defineProdDiagnostics({ docsBase: 'https://docs.example.com' })\n * throw diagnostics.NUXT_B2011() // Error: NUXT_B2011, docs derived from docsBase\n * ```\n */\n/* @__NO_SIDE_EFFECTS__ */\nexport function defineProdDiagnostics<\n const Codes extends Record<string, DiagnosticDefinition> = Record<string, DiagnosticDefinition>,\n const Reporters extends readonly AnyDiagnosticReporter[] = readonly AnyDiagnosticReporter[],\n>(options: DefineProdDiagnosticsOptions<Reporters> = {}): Diagnostics<Codes, Reporters> {\n const { docsBase, reporters = [] } = options\n\n return new Proxy({} as Diagnostics<Codes, Reporters>, {\n get(_target, code) {\n // ignore symbol / non-string probes (e.g. `then`, `Symbol.toPrimitive`)\n if (typeof code !== 'string')\n return undefined\n\n const handle = (\n params: DiagnosticCallParams & Record<string, unknown> = {},\n reporterOptions: any = {},\n ): Diagnostic => {\n const diagnostic = new Diagnostic(\n {\n why: code,\n docs: deriveDocs(docsBase, code),\n cause: params.cause,\n sources: params.sources,\n },\n handle,\n )\n for (const reporter of reporters) reporter(diagnostic, reporterOptions)\n return diagnostic\n }\n\n return handle\n },\n })\n}\n"],"mappings":";;;;;;AAOA,SAAgB,iBAAiB,YAAgC;CAC/D,MAAM,SAAS,IAAI,WAAW,KAAK,IAAI,WAAW;CAElD,MAAM,UAAoB,CAAC;CAC3B,IAAI,WAAW,KACb,QAAQ,KAAK,QAAQ,WAAW,KAAK;CAEvC,IAAI,WAAW,SAAS,QACtB,QAAQ,KAAK,YAAY,WAAW,QAAQ,KAAK,IAAI,GAAG;CAE1D,IAAI,WAAW,MACb,QAAQ,KAAK,QAAQ,WAAW,MAAM;CAGxC,IAAI,QAAQ,WAAW,GACrB,OAAO;CAQT,OAAO,CAAC,QAAQ,GALF,QAAQ,KAAK,QAAQ,MAAM;EAEvC,OAAO,GADW,IAAI,QAAQ,SAAS,IAAI,OAAO,KAC9B,GAAG;CACzB,CAEuB,CAAC,CAAC,CAAC,KAAK,IAAI;AACrC;;;;;;;;;;;ACvBA,SAAgB,gBACd,OACA,GAAG,MACA;CACH,OAAO,OAAO,UAAU,aAAc,MAA+B,GAAG,IAAI,IAAI;AAClF;;;;;;;;;;AC2HA,SAAgB,sBAAsB,EACpC,QAAQ,gBAAgB,QACxB,YAAY,qBACc,CAAC,GAAmD;CAC9E,QAAQ,YAAY,EAAE,SAAS,kBAAkB,CAAC,MAAM;EAEtD,QAAQ,OAAO,CAAC,UAAU,UAAU,CAAC;CACvC;AACF;AAyGA,MAAM,oBACJ,MACA;AAEF,IAAa,aAAb,MAAa,mBAAmB,MAAM;CACpC,OAAe;;;;;CAMf;;;;CAKA;;;;;;;CAQA;;;;CAKA,IAAI,MAAc;EAChB,OAAO,KAAK;CACd;;;;;;;;CASA,YAAY,MAAsB,cAAwB,YAAY;EACpE,MAAM,KAAK,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;EACrC,KAAK,MAAM,KAAK;EAChB,KAAK,OAAO,KAAK;EACjB,KAAK,UAAU,KAAK;EAIpB,oBAAoB,MAAM,WAAW;CACvC;;;;CAKA,SAAiB;EACf,OAAO;GACL,MAAM,KAAK;GACX,KAAK,KAAK;GACV,KAAK,KAAK;GACV,MAAM,KAAK;GACX,SAAS,KAAK;GACd,OAAO,KAAK;GACZ,OAAO,KAAK;EACd;CACF;AACF;;;;;;;;;AAUA,SAAgB,WACd,UACA,MACoB;CACpB,OAAO,OAAO,aAAa,WAAW,GAAG,SAAS,GAAG,KAAK,YAAY,MAAM,WAAW,IAAI;AAC7F;;;;;;;AAQA,SAAgB,kBAGd,SAAoF;CACpF,MAAM,YAAY,QAAQ,aAAa,CAAC;CACxC,MAAM,SAAS,CAAC;CAEhB,MAAM,EAAE,aAAa;CAErB,KAAK,MAAM,QAAQ,OAAO,KAAK,QAAQ,KAAK,GAAqC;EAC/E,MAAM,MAAM,QAAQ,MAAM;EAE1B,MAAM,OAAO,IAAI,SAAS,QAAQ,KAAA,IAAY,IAAI,QAAQ,WAAW,UAAU,IAAI;EAEnF,MAAM,UACJ,SAAyD,CAAC,GAC1D,kBAAuB,CAAC,MACT;GACf,MAAM,aAAa,IAAI,WACrB;IACE,KAAK,gBAAgB,IAAI,KAAK,MAAM;IACpC,KAAK,gBAAgB,IAAI,KAAK,MAAM;IACpC;IACA,OAAO,OAAO;IACd,SAAS,OAAO;GAClB,GACA,MACF;GACA,WAAW,OAAO;GAClB,KAAK,MAAM,YAAY,WAAW,SAAS,YAAY,eAAe;GACtE,OAAO;EACT;EAEA,OAAO,QAAQ;CACjB;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;ACrUA,SAAgB,sBAGd,UAAmD,CAAC,GAAkC;CACtF,MAAM,EAAE,UAAU,YAAY,CAAC,MAAM;CAErC,OAAO,IAAI,MAAM,CAAC,GAAoC,EACpD,IAAI,SAAS,MAAM;EAEjB,IAAI,OAAO,SAAS,UAClB,OAAO,KAAA;EAET,MAAM,UACJ,SAAyD,CAAC,GAC1D,kBAAuB,CAAC,MACT;GACf,MAAM,aAAa,IAAI,WACrB;IACE,KAAK;IACL,MAAM,WAAW,UAAU,IAAI;IAC/B,OAAO,OAAO;IACd,SAAS,OAAO;GAClB,GACA,MACF;GACA,KAAK,MAAM,YAAY,WAAW,SAAS,YAAY,eAAe;GACtE,OAAO;EACT;EAEA,OAAO;CACT,EACF,CAAC;AACH"}

@@ -1,2 +0,2 @@

import { l as DiagnosticReporter } from "../diagnostic-ChOW5Avm.mjs";
import { u as DiagnosticReporter } from "../diagnostic-BajIZ3Zi.mjs";

@@ -3,0 +3,0 @@ //#region src/reporters/dev.d.ts

@@ -1,2 +0,2 @@

import { l as DiagnosticReporter } from "../diagnostic-ChOW5Avm.mjs";
import { u as DiagnosticReporter } from "../diagnostic-BajIZ3Zi.mjs";

@@ -3,0 +3,0 @@ //#region src/reporters/fetch.d.ts

@@ -1,2 +0,2 @@

import { l as DiagnosticReporter } from "../diagnostic-ChOW5Avm.mjs";
import { u as DiagnosticReporter } from "../diagnostic-BajIZ3Zi.mjs";

@@ -15,2 +15,5 @@ //#region src/reporters/node.d.ts

* always preserved.
*
* Pass an empty array to keep every frame.
* @default [/\/node_modules\//i]
*/

@@ -17,0 +20,0 @@ excludeStackFrames?: readonly RegExp[];

import { appendFileSync } from "node:fs";
//#region src/reporters/node.ts
const DEFAULT_EXCLUDE_STACK_FRAMES = [/\/node_modules\//i];
function applyExcludeStackFrames(raw, exclude) {

@@ -29,3 +30,3 @@ const [header, ...frames] = raw.split("\n");

const logFile = options?.logFile ?? ".nostics.log";
const excludeStackFrames = options?.excludeStackFrames;
const excludeStackFrames = options?.excludeStackFrames ?? DEFAULT_EXCLUDE_STACK_FRAMES;
return (diagnostic) => {

@@ -32,0 +33,0 @@ try {

@@ -1,1 +0,1 @@

{"version":3,"file":"node.mjs","names":[],"sources":["../../src/reporters/node.ts"],"sourcesContent":["import type { Diagnostic, DiagnosticReporter } from '../diagnostic'\nimport { appendFileSync } from 'node:fs'\n\nexport interface FileReporterOptions {\n /**\n * Path to the log file.\n * @default '.nostics.log'\n */\n logFile?: string\n\n /**\n * Stack frames matching ANY of these patterns are removed from\n * `diagnostic.stack` before it is written to the log file. Useful to strip\n * `node_modules` and Node internals. The `Error: ...` header line is\n * always preserved.\n */\n excludeStackFrames?: readonly RegExp[]\n}\n\nfunction applyExcludeStackFrames(raw: string, exclude: readonly RegExp[]): string {\n const [header, ...frames] = raw.split('\\n')\n return [header, ...frames.filter(frame => !exclude.some(re => re.test(frame)))].join('\\n')\n}\n\n/**\n * Creates a reporter that appends diagnostics as NDJSON to a local file.\n * Each diagnostic is written as a single JSON line. The diagnostic's `stack`\n * (if present) is included in the payload; noisy frames can be removed via\n * {@link FileReporterOptions.excludeStackFrames}.\n *\n * @example\n * ```ts\n * import { defineDiagnostics } from 'nostics'\n * import { createFileReporter } from 'nostics/reporters/node'\n *\n * const diagnostics = defineDiagnostics({\n * codes: { ... },\n * reporters: [createFileReporter({\n * excludeStackFrames: [/\\/node_modules\\//, /\\(node:/],\n * })],\n * })\n * ```\n */\n/* @__NO_SIDE_EFFECTS__ */\nexport function createFileReporter(options?: FileReporterOptions): DiagnosticReporter {\n const logFile = options?.logFile ?? '.nostics.log'\n const excludeStackFrames = options?.excludeStackFrames\n return (diagnostic) => {\n try {\n const d = diagnostic as Diagnostic & Record<string, unknown>\n const base: Record<string, unknown>\n = typeof d.toJSON === 'function' ? (d.toJSON() as Record<string, unknown>) : { ...d }\n if (d.stack) {\n base.stack = excludeStackFrames?.length\n ? applyExcludeStackFrames(d.stack, excludeStackFrames)\n : d.stack\n }\n appendFileSync(logFile, `${JSON.stringify(base)}\\n`)\n }\n catch (err: unknown) {\n console.error(`[nostics]: Failed to write log to \"${logFile}\":`, err)\n }\n }\n}\n"],"mappings":";;AAmBA,SAAS,wBAAwB,KAAa,SAAoC;CAChF,MAAM,CAAC,QAAQ,GAAG,UAAU,IAAI,MAAM,IAAI;CAC1C,OAAO,CAAC,QAAQ,GAAG,OAAO,QAAO,UAAS,CAAC,QAAQ,MAAK,OAAM,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;AAC3F;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,mBAAmB,SAAmD;CACpF,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,qBAAqB,SAAS;CACpC,QAAQ,eAAe;EACrB,IAAI;GACF,MAAM,IAAI;GACV,MAAM,OACF,OAAO,EAAE,WAAW,aAAc,EAAE,OAAO,IAAgC,EAAE,GAAG,EAAE;GACtF,IAAI,EAAE,OACJ,KAAK,QAAQ,oBAAoB,SAC7B,wBAAwB,EAAE,OAAO,kBAAkB,IACnD,EAAE;GAER,eAAe,SAAS,GAAG,KAAK,UAAU,IAAI,EAAE,GAAG;EACrD,SACO,KAAc;GACnB,QAAQ,MAAM,sCAAsC,QAAQ,KAAK,GAAG;EACtE;CACF;AACF"}
{"version":3,"file":"node.mjs","names":[],"sources":["../../src/reporters/node.ts"],"sourcesContent":["import type { Diagnostic, DiagnosticReporter } from '../diagnostic'\nimport { appendFileSync } from 'node:fs'\n\nexport interface FileReporterOptions {\n /**\n * Path to the log file.\n * @default '.nostics.log'\n */\n logFile?: string\n\n /**\n * Stack frames matching ANY of these patterns are removed from\n * `diagnostic.stack` before it is written to the log file. Useful to strip\n * `node_modules` and Node internals. The `Error: ...` header line is\n * always preserved.\n *\n * Pass an empty array to keep every frame.\n * @default [/\\/node_modules\\//i]\n */\n excludeStackFrames?: readonly RegExp[]\n}\n\nconst DEFAULT_EXCLUDE_STACK_FRAMES: readonly RegExp[] = [/\\/node_modules\\//i]\n\nfunction applyExcludeStackFrames(raw: string, exclude: readonly RegExp[]): string {\n const [header, ...frames] = raw.split('\\n')\n return [header, ...frames.filter(frame => !exclude.some(re => re.test(frame)))].join('\\n')\n}\n\n/**\n * Creates a reporter that appends diagnostics as NDJSON to a local file.\n * Each diagnostic is written as a single JSON line. The diagnostic's `stack`\n * (if present) is included in the payload; noisy frames can be removed via\n * {@link FileReporterOptions.excludeStackFrames}.\n *\n * @example\n * ```ts\n * import { defineDiagnostics } from 'nostics'\n * import { createFileReporter } from 'nostics/reporters/node'\n *\n * const diagnostics = defineDiagnostics({\n * codes: { ... },\n * reporters: [createFileReporter({\n * excludeStackFrames: [/\\/node_modules\\//, /\\(node:/],\n * })],\n * })\n * ```\n */\n/* @__NO_SIDE_EFFECTS__ */\nexport function createFileReporter(options?: FileReporterOptions): DiagnosticReporter {\n const logFile = options?.logFile ?? '.nostics.log'\n const excludeStackFrames = options?.excludeStackFrames ?? DEFAULT_EXCLUDE_STACK_FRAMES\n return (diagnostic) => {\n try {\n const d = diagnostic as Diagnostic & Record<string, unknown>\n const base: Record<string, unknown>\n = typeof d.toJSON === 'function' ? (d.toJSON() as Record<string, unknown>) : { ...d }\n if (d.stack) {\n base.stack = excludeStackFrames?.length\n ? applyExcludeStackFrames(d.stack, excludeStackFrames)\n : d.stack\n }\n appendFileSync(logFile, `${JSON.stringify(base)}\\n`)\n }\n catch (err: unknown) {\n console.error(`[nostics]: Failed to write log to \"${logFile}\":`, err)\n }\n }\n}\n"],"mappings":";;AAsBA,MAAM,+BAAkD,CAAC,mBAAmB;AAE5E,SAAS,wBAAwB,KAAa,SAAoC;CAChF,MAAM,CAAC,QAAQ,GAAG,UAAU,IAAI,MAAM,IAAI;CAC1C,OAAO,CAAC,QAAQ,GAAG,OAAO,QAAO,UAAS,CAAC,QAAQ,MAAK,OAAM,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;AAC3F;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,mBAAmB,SAAmD;CACpF,MAAM,UAAU,SAAS,WAAW;CACpC,MAAM,qBAAqB,SAAS,sBAAsB;CAC1D,QAAQ,eAAe;EACrB,IAAI;GACF,MAAM,IAAI;GACV,MAAM,OACF,OAAO,EAAE,WAAW,aAAc,EAAE,OAAO,IAAgC,EAAE,GAAG,EAAE;GACtF,IAAI,EAAE,OACJ,KAAK,QAAQ,oBAAoB,SAC7B,wBAAwB,EAAE,OAAO,kBAAkB,IACnD,EAAE;GAER,eAAe,SAAS,GAAG,KAAK,UAAU,IAAI,EAAE,GAAG;EACrD,SACO,KAAc;GACnB,QAAQ,MAAM,sCAAsC,QAAQ,KAAK,GAAG;EACtE;CACF;AACF"}
{
"name": "nostics",
"type": "module",
"version": "1.0.0",
"version": "1.1.0",
"description": "Structured diagnostic code library",

@@ -38,2 +38,4 @@ "author": "Anthony Fu <anthonyfu117@hotmail.com>",

"@posva/prompts": "^2.4.4",
"@size-limit/esbuild-why": "^12.1.0",
"@size-limit/preset-small-lib": "^12.1.0",
"@types/node": "^25.9.3",

@@ -51,2 +53,3 @@ "@types/semver": "^7.7.1",

"simple-git-hooks": "^2.13.1",
"size-limit": "^12.1.0",
"tsdown": "^0.22.2",

@@ -57,3 +60,3 @@ "tsnapi": "^0.3.3",

"vitest": "^4.1.8",
"nostics": "1.0.0"
"nostics": "1.1.0"
},

@@ -66,2 +69,44 @@ "simple-git-hooks": {

},
"size-limit": [
{
"name": "defineDiagnostics from source",
"path": "src/index.ts",
"import": "{ defineDiagnostics }"
},
{
"name": "defineDiagnostics",
"path": "dist/index.mjs",
"import": "{ defineDiagnostics }"
},
{
"name": "defineProdDiagnostics",
"path": "dist/index.mjs",
"import": "{ defineProdDiagnostics }"
},
{
"name": "defineDiagnostics + createConsoleReporter",
"path": "dist/index.mjs",
"import": "{ defineDiagnostics, createConsoleReporter }"
},
{
"name": "defineProdDiagnostics + createConsoleReporter",
"path": "dist/index.mjs",
"import": "{ defineProdDiagnostics, createConsoleReporter }"
},
{
"name": "formatDiagnostic",
"path": "dist/index.mjs",
"import": "{ formatDiagnostic }"
},
{
"name": "full root",
"path": "dist/index.mjs",
"import": "*"
},
{
"name": "ansi formatter",
"path": "dist/formatters/ansi.mjs",
"import": "*"
}
],
"scripts": {

@@ -75,2 +120,3 @@ "build": "tsdown && pnpm --filter \"./packages/*\" run build",

"release": "node scripts/release.ts",
"size": "size-limit",
"test": "pnpm run build && vitest run --coverage && pnpm -C demo-lib run build && pnpm -C playground run build && pnpm run typecheck && pnpm run lint",

@@ -77,0 +123,0 @@ "typecheck": "tsc && pnpm --filter \"./packages/*\" run typecheck"

@@ -9,2 +9,3 @@ <p align="center">

[![CI](https://github.com/vercel-labs/nostics/actions/workflows/ci.yml/badge.svg)](https://github.com/vercel-labs/nostics/actions/workflows/ci.yml)
[![bundle size](https://deno.bundlejs.com/badge?q=nostics&treeshake=[{+defineDiagnostics+}])](https://bundlejs.com/?q=nostics&treeshake=%5B%7B+defineDiagnostics+%7D%5D)

@@ -78,2 +79,20 @@ Errors worth reading.

## Claude Code plugin
Install the plugin to give Claude 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) into any supported agent (Claude Code, Codex, Cursor, opencode, and more):
```bash
npx skills add vercel-labs/nostics
```
## Why use it

@@ -80,0 +99,0 @@

@@ -53,5 +53,19 @@ # Migrate errors and warnings to nostics

- `why` says what happened with runtime values interpolated through typed param functions (both `why` and `fix` accept them; their params are merged and required at the call site). `fix` is the concrete next action, never a restatement of the problem.
- **Split the original warning; never copy it whole into `why`.** Most existing warnings bundle the diagnosis and the remedy in one string (`"A hash must start with '#'. Prefix it with '#'."`). The reporter prints `why` **and** `fix`, so pasting the full sentence into `why` and then writing a `fix` duplicates the remedy on screen. Cut the sentence in two: diagnosis to `why`, remedy to `fix`. If the remedy needs the offending value, make `fix` a param function — its params merge with `why`'s.
```ts
// before: warn(`A \`hash\` should start with "#". Replace "${hash}" with "#${hash}".`)
// ❌ remedy lives in why and is echoed by fix
{ why: (p) => `A \`hash\` should start with "#". Replace "${p.hash}" with "#${p.hash}".`,
fix: 'Prefix the hash with "#".' }
// ✅ diagnosis in why, remedy in fix (a function, because it needs the value)
{ why: (p) => `A \`hash\` should start with "#" but received "${p.hash}".`,
fix: (p) => `Prepend "#": use "#${p.hash}".` }
```
- Extra `console.warn`/`console.error` arguments must not be lost: an error value becomes `cause`; data values are interpolated into `why` (e.g. `JSON.stringify(p.value)`).
- `cause` and `sources` (`'file:line:column'` strings pointing at user code) go **inside the params object** (the first argument), merged with the message params. The second argument is reporter options only, e.g. `{ method: 'error' }`.
- `docsBase` is optional. If the project has no documented error-page URL scheme, propose one and surface it to the maintainer rather than inventing pages that do not exist.
- `docsBase` is optional. If the project has no documented error-page URL scheme, propose one and surface it to the maintainer rather than inventing pages that do not exist. When pointing per-code `docs` at existing documentation with `#hash` anchors, verify each anchor against the built or published HTML (grep the `id=`) instead of guessing it. A custom slugify can keep heading casing and leave a trailing hyphen, so the real anchor may look like `#Using-the-store-outside-of-setup-`.

@@ -70,3 +84,3 @@ ## Call-site patterns

Calling a handle always runs the reporters, so `throw diagnostics.CODE(params)` reports **and** throws. For a warn-then-throw site that is the same double output it already had; for a bare `throw new Error(...)` it adds a console report before the throw, which is normally fine but worth mentioning if the library is strict about console output.
Calling a handle always runs the reporters, so `throw diagnostics.CODE(params)` reports **and** throws. For a warn-then-throw site that is the same double output it already had. For a bare `throw new Error(...)` it adds a console report before the throw, which duplicates the message the uncaught error already shows. When a catalog's codes are only ever thrown (config validators, fatal asserts), give that catalog **no reporters** (`reporters` is optional) so the throw is the only output, and keep warnings in a separate reporter-backed catalog. Dropping the reporter also keeps a strict test harness happy when its `afterEach` fails on any unasserted `console.warn`/`console.error`.

@@ -80,3 +94,4 @@ Report-only calls must stay bare expression statements (`DEV && diagnostics.LIB_R0001(p)` included) so `nosticsStrip` can remove them in production. `throw`/`return`/assigned diagnostics are behavior and stay.

- Tests for warnings, throws, guards, and error shapes still pass; tests asserting exact message text are updated consciously, not accidentally.
- Watch substring assertions when splitting a warning. `toHaveBeenWarned('...')` / `toContain` pin a **fragment**, not the whole message, and a fragment may sit in the remedy half you just moved to `fix`. Before splitting, grep the tests for substrings of each warning: keep pinned **diagnosis** fragments in `why`; when a test pins a **remedy** fragment, update that assertion to the surviving `why` text. The same warning is often pinned by a shared constant duplicated across several spec files — fix every copy.
- Dev-only gates are still present everywhere the source had them, and no new gates were added.
- Report-only diagnostics remain strippable expression statements. Thrown/returned diagnostics keep their message text in production by design: they are behavior, not reports.
---
name: nostics
description: "Structured diagnostic code library for JavaScript/TypeScript. Turns errors and other conditions into typed, machine-readable `Diagnostic` instances with stable codes, docs URLs, and actionable fields. Use this skill whenever the project imports `nostics`, or works with `defineDiagnostics`, the `Diagnostic` class, diagnostic code registries, or structured error handling. Also covers reporters (`createConsoleReporter`, `createFetchReporter` from nostics/reporters/fetch, `createFileReporter` from nostics/reporters/node, `createDevReporter` from nostics/reporters/dev), formatters (`formatDiagnostic`, `ansiFormatter`, `jsonFormatter`), and Vite plugins (`nosticsStrip` from @nostics/unplugin/strip-transform, `nosticsCollector` from @nostics/unplugin/dev-server-collector). Also use when migrating a library's existing `console.warn`/`console.error`/`warn()` helpers or thrown `Error`s to diagnostics: follow `references/migration.md`."
description: "Structured diagnostic code library for JavaScript/TypeScript. Turns errors and other conditions into typed, machine-readable `Diagnostic` instances with stable codes, docs URLs, and actionable fields. Use this skill whenever the project imports `nostics`, or works with `defineDiagnostics`/`defineProdDiagnostics`, the `Diagnostic` class, diagnostic code registries, or structured error handling. Also covers reporters (`createConsoleReporter`, `createFetchReporter` from nostics/reporters/fetch, `createFileReporter` from nostics/reporters/node, `createDevReporter` from nostics/reporters/dev), formatters (`formatDiagnostic`, `ansiFormatter`, `jsonFormatter`), and Vite plugins (`nosticsStrip` from @nostics/unplugin/strip-transform, `nosticsCollector` from @nostics/unplugin/dev-server-collector). Also use when migrating a library's existing `console.warn`/`console.error`/`warn()` helpers or thrown `Error`s to diagnostics: follow `references/migration.md`."
license: MIT

@@ -39,3 +39,3 @@ ---

- **`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. Their `options` types are intersected; required reporter options become required at the call site.
- **`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 `throw`n: 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.
- **Param inference**: params from `why` and `fix` are intersected and required at the call site. If `why` needs `{ src }` and `fix` needs `{ date }`, the call requires `{ src, date }`.

@@ -48,3 +48,7 @@

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,
cause: originalError,
sources: ['nuxt.config.ts:42:3'],
}) // runtime fields merge in
diagnostics.NUXT_B2011({ src }, { method: 'error' }) // reporter options second

@@ -105,3 +109,5 @@ throw diagnostics.NUXT_B2011({ src }) // raise

import { nosticsCollector } from '@nostics/unplugin/dev-server-collector'
export default defineConfig({ plugins: [nosticsStrip.vite(), nosticsCollector.vite()] })
export default defineConfig({
plugins: [nosticsStrip.vite(), nosticsCollector.vite()],
})

@@ -119,2 +125,27 @@ // src/diagnostics.ts — pair the collector with createDevReporter()

## Production builds
- **Report-only** diagnostics (bare `diagnostics.X()`) should disappear: `nosticsStrip` or hand annotations drop them, then the unused catalog tree-shakes.
- **Surviving** diagnostics (`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):
```ts
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.
## Conventions

@@ -124,2 +155,3 @@

- Always provide `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`.
- Pass `cause` when re-raising; pass `sources` when the JS stack doesn't reflect the user's source.

@@ -126,0 +158,0 @@ - Split large catalogs by domain (`diagnostics/build.ts`, `runtime.ts`, `config.ts`, re-exported from `index.ts`), each `defineDiagnostics()` sharing `docsBase` with its own code range.

//#region src/utils.d.ts
/**
* A value of type T, or a function that resolves T from a single params object.
*
* @internal
*/
type ValueOrFn<T, P = any> = T | ((params: P) => T);
/**
* Extracts the param type from a single-arg function, or `never` for
* non-function inputs. Pairs with {@link ValueOrFn}.
*
* @internal
*/
type ExtractFnParam<T> = T extends ((params: infer P) => any) ? P : never;
/**
* Converts a union of types to their intersection.
*
* @internal
*/
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
/**
* `true` when `T` is the `any` type.
*
* @internal
*/
type IsAny<Type> = 0 extends 1 & Type ? true : false;
/**
* `true` when `T` is the `unknown` type (and not `any`).
*
* @internal
*/
type IsUnknown<Type> = IsAny<Type> extends true ? false : unknown extends Type ? true : false;
/**
* Expands a type to its property listing so editor hovers show the resolved
* shape instead of a chain of aliases / intersections.
*
* @internal
*/
type Prettify<Type> = { [Key in keyof Type]: Type[Key] };
//#endregion
//#region src/diagnostic.d.ts
/**
* Define-time shape of a diagnostic. Each field can be a static value or a
* function that resolves it from a shared `params` object passed at call
* time. Runtime-only fields (`cause`, `sources`) from {@link DiagnosticInit}
* are intentionally omitted: they're only meaningful at the call site.
*/
interface DiagnosticDefinition<P = any> {
/**
* The error message: why this failed. String, or a function of `params`.
*
* @example
* ```ts
* why: (p: { name: string }) => `module "${p.name}" failed to load`
* ```
*/
why: ValueOrFn<string, P>;
/**
* Actionable instructions on how to resolve the problem. String, or a
* function of `params`.
*
* @example
* ```ts
* fix: (p: { name: string }) => `run "npm install ${p.name}"`
* ```
*/
fix?: ValueOrFn<string, P>;
/**
* Per-code docs URL. A string overrides
* {@link DefineDiagnosticsOptions.docsBase} for this code; `false` opts this
* code out entirely, even when `docsBase` is set. When omitted, the URL is
* derived from `docsBase`.
*/
docs?: string | false;
}
/**
* Runtime-only fields that can be passed alongside the interpolation params
* at call time. Merged into the same object so callers pass everything in
* one place.
*/
interface DiagnosticCallParams {
/**
* Original error or exception that triggered this diagnostic. Pass it
* through when re-throwing so the original stack trace is preserved.
*/
cause?: unknown;
/**
* Locations in user code that contributed to this diagnostic, in
* `file:line:column` format. Useful for compilers and other tools where the
* JS stack trace doesn't reflect the user's source.
*/
sources?: string[];
}
/**
* Structured initializer for a {@link Diagnostic}. `why` is the only required
* field: it becomes the {@link Diagnostic.message}. The remaining fields are
* optional metadata that reporters and consumers can render or forward.
*/
interface DiagnosticInit extends DiagnosticCallParams {
/**
* The actual error message: why this failed.
* Mirrored to `Error.message`.
*/
why: string;
/**
* Optional actionable instructions on how to resolve the problem.
*/
fix?: string;
/**
* URL to extended documentation for this diagnostic.
*/
docs?: string;
}
/**
* Represents how to report a diagnostic. Could call `console.log()`, send the
* diagnostic to a server, or something else. Reporters declare the shape of
* options they need via `ReporterOpts`; `defineDiagnostics` intersects every
* reporter's options into a single object passed at the call site.
*/
type DiagnosticReporter<ReporterOpts extends object = {}> = (diagnostic: Diagnostic, options: ReporterOpts) => void;
/**
* Permissive reporter constraint used internally so reporters with 1 arg,
* required options, or optional options all satisfy the array constraint.
*
* @internal
*/
type AnyDiagnosticReporter = (diagnostic: Diagnostic, options: any) => void;
/**
* The `console` methods a log reporter can route to.
*/
type ConsoleMethod = "log" | "error" | "warn";
/**
* Options for {@link createConsoleReporter}.
*/
interface ConsoleReporterOptions {
/**
* `console` method used to print the diagnostic. Defaults to `'warn'`. The
* returned reporter still accepts a per-call `{ method }` override through
* the call-site reporter options.
*/
method?: ConsoleMethod;
/**
* Renders the diagnostic into the string handed to `console`. Defaults to
* {@link formatDiagnostic}, the plain unicode-decorated formatter.
*/
formatter?: (diagnostic: Diagnostic) => string;
}
/**
* Creates a console reporter that renders each diagnostic with `formatter` and
* prints the result via `console[method]`. Both default sensibly (`'warn'` and
* {@link formatDiagnostic}); `method` can also be overridden per call through
* the reporter options.
*/
declare function createConsoleReporter({
method: defaultMethod,
formatter
}?: ConsoleReporterOptions): DiagnosticReporter<{
method?: ConsoleMethod;
}>;
/**
* Resolves the `params` type a code expects from the intersection of params
* across all function-typed fields, falling back to `{}` when every field is
* static. Merged with {@link DiagnosticCallParams} at the call site.
*
* @internal
*/
type InferCodeParams<Def> = [ExtractFnParam<Def[keyof Def]>] extends [never] ? {} : UnionToIntersection<ExtractFnParam<Def[keyof Def]>>;
/**
* Options for {@link defineDiagnostics}.
*/
interface DefineDiagnosticsOptions<Codes extends Record<string, DiagnosticDefinition>, Reporters extends readonly AnyDiagnosticReporter[]> {
/**
* Base URL or resolver for documentation links. When a string, the code is
* appended as a lowercase path segment (e.g. `"https://docs.example.com"` →
* `"https://docs.example.com/math_e001"`). When a function, receives the
* code and returns a URL or `undefined`.
*/
docsBase?: string | ((code: keyof Codes) => string | undefined);
/**
* Map of diagnostic codes to their definitions.
*/
codes: Codes;
/**
* Reporters called every time a diagnostic is produced. Can be used to
* integrate with custom logging.
*/
reporters?: Reporters;
}
/**
* The first positional argument of a {@link DiagnosticHandle} call:
* interpolation params merged with the runtime-only call-site fields
* (`cause`, `sources`).
*
* @internal
*/
type CallSiteParams<Params> = Params & DiagnosticCallParams;
/**
* Resolves the full argument tuple for a {@link DiagnosticHandle} call.
* Branches on whether params and reporter options each have required fields.
* Required positions become required tuple elements, all-optional ones
* become `?`, and when no reporter declares any options the parameter is
* omitted entirely.
*
* @internal
*/
type ActionArgs<Params, ReporterOpts> = keyof ReporterOpts extends never ? {} extends Params ? [params?: CallSiteParams<Params>] : [params: CallSiteParams<Params>] : {} extends ReporterOpts ? {} extends Params ? [params?: CallSiteParams<Params>, reporterOptions?: ReporterOpts] : [params: CallSiteParams<Params>, reporterOptions?: ReporterOpts] : {} extends Params ? [params: CallSiteParams<Params> | undefined, reporterOptions: ReporterOpts] : [params: CallSiteParams<Params>, reporterOptions: ReporterOpts];
/**
* Per-code handle exposed by {@link defineDiagnostics}. Each code is a
* callable: invoke it to build the diagnostic and run every reporter, or
* prefix the call with `throw` to raise it.
*
* @example
* ```ts
* diagnostics.MATH_E001({ name: 'x' }) // report
* throw diagnostics.MATH_E001({ name: 'x' }) // throw
* ```
*/
interface DiagnosticHandle<Params, ReporterOpts> {
/**
* Builds the diagnostic, runs every reporter, and returns the diagnostic
* instance. The returned diagnostic can be inspected, attached as `cause`,
* or thrown with `throw`.
*/
(...args: ActionArgs<Params, ReporterOpts>): Diagnostic;
}
/**
* Return type of {@link defineDiagnostics}.
*/
type Diagnostics<Codes extends Record<string, DiagnosticDefinition>, Reporters extends readonly AnyDiagnosticReporter[]> = { [Code in keyof Codes]: DiagnosticHandle<InferCodeParams<Codes[Code]>, Prettify<ExtractReportersOptions<Reporters>>> };
declare class Diagnostic extends Error {
name: string;
/**
* URL to extended documentation for this diagnostic code.
* Auto-generated from {@link DefineDiagnosticsOptions.docsBase}.
*/
docs?: string;
/**
* Optional actionable instructions on how to resolve the problem.
*/
fix?: string;
/**
* Locations in user code that contributed to this diagnostic, in
* `file:line:column` format.
*/
sources?: string[];
/**
* Alias for {@link Error.message}: the reason this diagnostic was raised.
*/
get why(): string;
/**
* @param init structured initializer; `why` is required
* @param captureFrom V8 stack-cutoff frame. Defaults to {@link Diagnostic}
* so the top of the trace is the `new Diagnostic(...)` call site.
* `defineDiagnostics` passes its action method to strip its own frames too.
* Ignored on engines without `Error.captureStackTrace`.
*/
constructor(init: DiagnosticInit, captureFrom?: Function);
/**
* Converts the diagnostic into a serializable structured object.
*/
toJSON(): object;
}
/**
* Creates a typed diagnostics object from a set of code definitions. Each
* code becomes a callable {@link DiagnosticHandle}: invoke to report, or
* `throw` the result to raise. No `new` required, no proxy.
*/
declare function defineDiagnostics<const Codes extends Record<string, DiagnosticDefinition>, const Reporters extends readonly AnyDiagnosticReporter[]>(options: DefineDiagnosticsOptions<Codes, Reporters>): Diagnostics<Codes, Reporters>;
/**
* Extracts the options object a reporter accepts as its 2nd argument. Returns
* `{}` when the reporter has no 2nd arg (so it contributes nothing to the
* merged shape).
*/
type ExtractSingleReporterOptions<Reporter> = Reporter extends ((diagnostic: Diagnostic, options: infer ReporterOpts) => any) ? IsUnknown<ReporterOpts> extends true ? {} : Exclude<ReporterOpts, undefined> : {};
/**
* Intersects every reporter's options shape into a single object. If any
* reporter has a required field, the merged shape has a required field, and
* {@link ActionArgs} flips `reporterOptions` from optional to required via
* `{} extends Merged`.
*/
type ExtractReportersOptions<Reporters extends readonly any[]> = Reporters extends readonly [infer First, ...infer Rest] ? ExtractSingleReporterOptions<First> & ExtractReportersOptions<Rest> : {};
//#endregion
export { DiagnosticCallParams as a, DiagnosticInit as c, defineDiagnostics as d, ValueOrFn as f, Diagnostic as i, DiagnosticReporter as l, ConsoleReporterOptions as n, DiagnosticDefinition as o, DefineDiagnosticsOptions as r, DiagnosticHandle as s, ConsoleMethod as t, createConsoleReporter as u };
//# sourceMappingURL=diagnostic-ChOW5Avm.d.mts.map