187 lines
6.2 KiB
JavaScript
187 lines
6.2 KiB
JavaScript
//#region src/formatters/plain.ts
|
|
/**
|
|
* Renders a diagnostic into a multi-line, unicode-decorated string suitable
|
|
* for terminal output. The first line is `[<name>] <message>`; optional
|
|
* details (`fix`, `sources`, `docs`) follow with `├▶`/`╰▶` connectors.
|
|
*/
|
|
function formatDiagnostic(diagnostic) {
|
|
const header = `[${diagnostic.name}] ${diagnostic.message}`;
|
|
const details = [];
|
|
if (diagnostic.fix) details.push(`fix: ${diagnostic.fix}`);
|
|
if (diagnostic.sources?.length) details.push(`sources: ${diagnostic.sources.join(", ")}`);
|
|
if (diagnostic.docs) details.push(`see: ${diagnostic.docs}`);
|
|
if (details.length === 0) return header;
|
|
return [header, ...details.map((detail, i) => {
|
|
return `${i < details.length - 1 ? "├▶" : "╰▶"} ${detail}`;
|
|
})].join("\n");
|
|
}
|
|
//#endregion
|
|
//#region src/utils.ts
|
|
/**
|
|
* Transforms a value or a function that returns a value to a value.
|
|
*
|
|
* @param valFn either a value or a function that returns a value
|
|
* @param args arguments to pass to the function if `valFn` is a function
|
|
*
|
|
* @internal
|
|
*/
|
|
function toValueWithArgs(valFn, ...args) {
|
|
return typeof valFn === "function" ? valFn(...args) : valFn;
|
|
}
|
|
//#endregion
|
|
//#region src/diagnostic.ts
|
|
/**
|
|
* 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.
|
|
*/
|
|
/* @__NO_SIDE_EFFECTS__ */
|
|
function createConsoleReporter({ method: defaultMethod = "warn", formatter = formatDiagnostic } = {}) {
|
|
return (diagnostic, { method = defaultMethod } = {}) => {
|
|
console[method](formatter(diagnostic));
|
|
};
|
|
}
|
|
const captureStackTrace = Error.captureStackTrace;
|
|
var Diagnostic = class Diagnostic extends Error {
|
|
name;
|
|
/**
|
|
* The diagnostic code, e.g. `MATH_E001`.
|
|
* Also appears as the `name` property.
|
|
*/
|
|
code;
|
|
/**
|
|
* URL to extended documentation for this diagnostic code.
|
|
* Auto-generated from {@link DefineDiagnosticsOptions.docsBase}.
|
|
*/
|
|
docs;
|
|
/**
|
|
* Optional actionable instructions on how to resolve the problem.
|
|
*/
|
|
fix;
|
|
/**
|
|
* 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;
|
|
/**
|
|
* Alias for {@link Error.message}: the reason this diagnostic was raised.
|
|
*/
|
|
get why() {
|
|
return this.message;
|
|
}
|
|
/**
|
|
* @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, captureFrom = Diagnostic) {
|
|
super(init.why, { cause: init.cause });
|
|
this.code = this.name = init.code;
|
|
this.fix = init.fix;
|
|
this.docs = init.docs;
|
|
this.sources = init.sources;
|
|
captureStackTrace?.(this, captureFrom);
|
|
}
|
|
/**
|
|
* Converts the diagnostic into a serializable structured object.
|
|
*/
|
|
toJSON() {
|
|
return {
|
|
name: this.name,
|
|
why: this.why,
|
|
fix: this.fix,
|
|
docs: this.docs,
|
|
sources: this.sources,
|
|
cause: this.cause,
|
|
stack: this.stack
|
|
};
|
|
}
|
|
};
|
|
/**
|
|
* 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
|
|
* code becomes a callable {@link DiagnosticHandle}: invoke to report, or
|
|
* `throw` the result to raise. No `new` required, no proxy.
|
|
*/
|
|
/* @__NO_SIDE_EFFECTS__ */
|
|
function defineDiagnostics(options) {
|
|
const reporters = options.reporters ?? [];
|
|
const result = {};
|
|
const { docsBase } = options;
|
|
for (const code of Object.keys(options.codes)) {
|
|
const def = options.codes[code];
|
|
const docs = def.docs === false ? void 0 : def.docs || deriveDocs(docsBase, code);
|
|
const handle = (params = {}, reporterOptions = {}) => {
|
|
const diagnostic = new Diagnostic({
|
|
code,
|
|
why: toValueWithArgs(def.why, params),
|
|
fix: toValueWithArgs(def.fix, params),
|
|
docs,
|
|
cause: params.cause,
|
|
sources: params.sources
|
|
}, handle);
|
|
for (const reporter of reporters) reporter(diagnostic, reporterOptions);
|
|
return diagnostic;
|
|
};
|
|
result[code] = handle;
|
|
}
|
|
return result;
|
|
}
|
|
//#endregion
|
|
//#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 instance `name`, `docs` is derived from `docsBase`, and `why` points to
|
|
* the docs URL when one exists (empty otherwise, so the thrown header is just
|
|
* the code). 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() // NUXT_B2011: https://docs.example.com/nuxt_b2011
|
|
* ```
|
|
*/
|
|
/* @__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 docs = deriveDocs(docsBase, code);
|
|
const diagnostic = new Diagnostic({
|
|
code,
|
|
why: docs ?? "",
|
|
docs,
|
|
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
|