@kubb/core 5.0.0-beta.4 → 5.0.0-beta.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +15 -148
  2. package/dist/diagnostics-DhfW8YpT.d.ts +2963 -0
  3. package/dist/index.cjs +775 -1193
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +165 -171
  6. package/dist/index.js +760 -1188
  7. package/dist/index.js.map +1 -1
  8. package/dist/memoryStorage-DTv1Kub1.js +2852 -0
  9. package/dist/memoryStorage-DTv1Kub1.js.map +1 -0
  10. package/dist/memoryStorage-Dkxnid2K.cjs +2985 -0
  11. package/dist/memoryStorage-Dkxnid2K.cjs.map +1 -0
  12. package/dist/mocks.cjs +80 -18
  13. package/dist/mocks.cjs.map +1 -1
  14. package/dist/mocks.d.ts +35 -8
  15. package/dist/mocks.js +81 -21
  16. package/dist/mocks.js.map +1 -1
  17. package/package.json +8 -19
  18. package/src/FileManager.ts +85 -63
  19. package/src/FileProcessor.ts +171 -43
  20. package/src/HookRegistry.ts +45 -0
  21. package/src/KubbDriver.ts +897 -0
  22. package/src/Telemetry.ts +278 -0
  23. package/src/Transform.ts +58 -0
  24. package/src/constants.ts +111 -19
  25. package/src/createAdapter.ts +113 -17
  26. package/src/createKubb.ts +921 -493
  27. package/src/createRenderer.ts +58 -27
  28. package/src/createReporter.ts +147 -0
  29. package/src/createStorage.ts +36 -23
  30. package/src/defineGenerator.ts +129 -17
  31. package/src/defineLogger.ts +78 -7
  32. package/src/defineMiddleware.ts +19 -17
  33. package/src/defineParser.ts +30 -13
  34. package/src/definePlugin.ts +320 -17
  35. package/src/defineResolver.ts +381 -179
  36. package/src/diagnostics.ts +660 -0
  37. package/src/index.ts +8 -6
  38. package/src/mocks.ts +92 -19
  39. package/src/reporters/cliReporter.ts +90 -0
  40. package/src/reporters/fileReporter.ts +103 -0
  41. package/src/reporters/jsonReporter.ts +20 -0
  42. package/src/reporters/report.ts +85 -0
  43. package/src/storages/fsStorage.ts +13 -37
  44. package/src/types.ts +59 -1297
  45. package/dist/PluginDriver-Ds-Us-e4.cjs +0 -1036
  46. package/dist/PluginDriver-Ds-Us-e4.cjs.map +0 -1
  47. package/dist/PluginDriver-Wi34Pegx.js +0 -945
  48. package/dist/PluginDriver-Wi34Pegx.js.map +0 -1
  49. package/dist/types-Cd0jhNmx.d.ts +0 -2153
  50. package/src/Kubb.ts +0 -300
  51. package/src/PluginDriver.ts +0 -424
  52. package/src/devtools.ts +0 -59
  53. package/src/renderNode.ts +0 -35
  54. package/src/utils/diagnostics.ts +0 -18
  55. package/src/utils/isInputPath.ts +0 -10
  56. package/src/utils/packageJSON.ts +0 -99
  57. /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
@@ -0,0 +1,660 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { styleText } from 'node:util'
3
+ import type { AsyncEventEmitter } from '@internals/utils'
4
+ import { getErrorMessage } from '@internals/utils'
5
+ import { version } from '../package.json'
6
+ import { type DiagnosticCode, diagnosticCode } from './constants.ts'
7
+ import type { KubbHooks } from './createKubb.ts'
8
+
9
+ /**
10
+ * Docs major, derived from the package version so the link tracks the published major.
11
+ */
12
+ const docsMajor = version.split('.')[0] ?? '5'
13
+
14
+ /**
15
+ * How serious a diagnostic is. `error` fails the build, `warning` and `info`
16
+ * are reported but do not.
17
+ */
18
+ export type DiagnosticSeverity = 'error' | 'warning' | 'info'
19
+
20
+ /**
21
+ * A human-readable explanation of a diagnostic code: a short title, what triggers it, and how
22
+ * to resolve it. This is the single source of truth the kubb.dev `/diagnostics/<slug>` pages
23
+ * mirror, so every code stays documented in one place. Typed as a total record over
24
+ * {@link DiagnosticCode}, so adding a code without documenting it fails the build.
25
+ */
26
+ export type DiagnosticDoc = {
27
+ /**
28
+ * Short title shown as the docs heading.
29
+ */
30
+ title: string
31
+ /**
32
+ * What triggers the diagnostic.
33
+ */
34
+ cause: string
35
+ /**
36
+ * The action that resolves it.
37
+ */
38
+ fix: string
39
+ }
40
+
41
+ /**
42
+ * Points a diagnostic back into the source document. Inputs are parsed into an
43
+ * object model with no line/column, so locations carry a JSON pointer the adapter
44
+ * builds (the OAS adapter emits `#/components/schemas/Pet`). A `config` diagnostic
45
+ * points at the Kubb config itself and so has no pointer.
46
+ */
47
+ export type DiagnosticLocation =
48
+ | {
49
+ kind: 'schema'
50
+ /**
51
+ * RFC 6901 JSON pointer into the source document.
52
+ */
53
+ pointer: string
54
+ /**
55
+ * The original reference when the diagnostic stems from an unresolved one.
56
+ */
57
+ ref?: string
58
+ }
59
+ | {
60
+ kind: 'operation' | 'document'
61
+ /**
62
+ * RFC 6901 JSON pointer into the source document.
63
+ */
64
+ pointer: string
65
+ }
66
+ | {
67
+ kind: 'config'
68
+ }
69
+
70
+ /**
71
+ * What a diagnostic carries.
72
+ * - `problem` is a build issue shown to the user, and the only kind rendered as a problem.
73
+ * - `performance` records a plugin's elapsed time.
74
+ * - `update` is a version notice.
75
+ */
76
+ export type DiagnosticKind = 'problem' | 'performance' | 'update'
77
+
78
+ /**
79
+ * Codes that describe a build problem: every {@link DiagnosticCode} except the
80
+ * `performance` and `updateAvailable` codes, which ride on their own variants.
81
+ */
82
+ export type ProblemCode = Exclude<DiagnosticCode, typeof diagnosticCode.performance | typeof diagnosticCode.updateAvailable>
83
+
84
+ /**
85
+ * A build problem collected during a run, gathered into the result instead of
86
+ * aborting on the first failure. It carries a stable {@link ProblemCode}, a
87
+ * `severity`, a `message`, and optionally a `location` into the source document,
88
+ * a `help`, the `plugin` that produced it, and the `cause` it wraps.
89
+ */
90
+ export type ProblemDiagnostic = {
91
+ /**
92
+ * @default 'problem'
93
+ */
94
+ kind?: 'problem'
95
+ /**
96
+ * Stable identifier for the problem, from the {@link diagnosticCode} catalog.
97
+ */
98
+ code: ProblemCode
99
+ severity: DiagnosticSeverity
100
+ message: string
101
+ location?: DiagnosticLocation
102
+ /**
103
+ * A suggested fix, phrased as an action the user can take.
104
+ */
105
+ help?: string
106
+ /**
107
+ * Name of the plugin or subsystem that produced the diagnostic.
108
+ */
109
+ plugin?: string
110
+ /**
111
+ * The underlying error, when the diagnostic wraps a thrown one.
112
+ */
113
+ cause?: Error
114
+ }
115
+
116
+ /**
117
+ * A per-plugin performance record, built with {@link Diagnostics.performance}. It carries the
118
+ * `plugin` and its elapsed `duration`, and the `performance` kind keeps it out of the problem
119
+ * list. It feeds the per-plugin timing bars, and reporters sum these into the run total.
120
+ */
121
+ export type PerformanceDiagnostic = {
122
+ kind: 'performance'
123
+ code: typeof diagnosticCode.performance
124
+ severity: 'info'
125
+ message: string
126
+ /**
127
+ * The plugin this measurement belongs to.
128
+ */
129
+ plugin: string
130
+ /**
131
+ * Elapsed milliseconds.
132
+ */
133
+ duration: number
134
+ }
135
+
136
+ /**
137
+ * A notice that a newer Kubb version is available on npm, built with {@link Diagnostics.update}.
138
+ * It carries the running and latest versions and renders like any info diagnostic.
139
+ */
140
+ export type UpdateDiagnostic = {
141
+ kind: 'update'
142
+ code: typeof diagnosticCode.updateAvailable
143
+ severity: 'info'
144
+ message: string
145
+ /**
146
+ * The running Kubb version.
147
+ */
148
+ currentVersion: string
149
+ /**
150
+ * The newest version published on npm.
151
+ */
152
+ latestVersion: string
153
+ }
154
+
155
+ /**
156
+ * A structured record collected during a build, discriminated on `kind`: a
157
+ * {@link ProblemDiagnostic} for an issue, a {@link PerformanceDiagnostic} for a per-plugin
158
+ * timing, or an {@link UpdateDiagnostic} for a version notice.
159
+ */
160
+ export type Diagnostic = ProblemDiagnostic | PerformanceDiagnostic | UpdateDiagnostic
161
+
162
+ /**
163
+ * Maps each {@link DiagnosticCode} to the variant it selects, for {@link narrow}.
164
+ * Every {@link ProblemCode} selects a {@link ProblemDiagnostic}, and the two bookkeeping codes
165
+ * select their own variant.
166
+ */
167
+ export type DiagnosticByCode = Record<ProblemCode, ProblemDiagnostic> &
168
+ Record<typeof diagnosticCode.performance, PerformanceDiagnostic> &
169
+ Record<typeof diagnosticCode.updateAvailable, UpdateDiagnostic>
170
+
171
+ /**
172
+ * Narrows a {@link Diagnostic} to the variant for `code`, or `null` when it does not match.
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * const update = narrow(diagnostic, diagnosticCode.updateAvailable)
177
+ * if (update) {
178
+ * console.log(update.latestVersion)
179
+ * }
180
+ * ```
181
+ */
182
+ function narrow<C extends DiagnosticCode>(diagnostic: Diagnostic, code: C): DiagnosticByCode[C] | null {
183
+ return diagnostic.code === code ? (diagnostic as DiagnosticByCode[C]) : null
184
+ }
185
+
186
+ /**
187
+ * Builds a type guard that narrows a {@link Diagnostic} to the variant for `kind`. A diagnostic
188
+ * with no `kind` is treated as a `problem`.
189
+ */
190
+ function isKind<T extends Diagnostic>(kind: DiagnosticKind) {
191
+ return (diagnostic: Diagnostic): diagnostic is T => (diagnostic.kind ?? 'problem') === kind
192
+ }
193
+
194
+ /**
195
+ * Returns `true` when the diagnostic is a build {@link ProblemDiagnostic}.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * if (isProblem(diagnostic)) {
200
+ * console.log(diagnostic.location)
201
+ * }
202
+ * ```
203
+ */
204
+ const isProblem = isKind<ProblemDiagnostic>('problem')
205
+
206
+ /**
207
+ * Returns `true` when the diagnostic is a per-plugin {@link PerformanceDiagnostic}.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * const timings = diagnostics.filter(isPerformance)
212
+ * ```
213
+ */
214
+ const isPerformance = isKind<PerformanceDiagnostic>('performance')
215
+
216
+ /**
217
+ * Returns `true` when the diagnostic is a version-update {@link UpdateDiagnostic}.
218
+ *
219
+ * @example
220
+ * ```ts
221
+ * if (isUpdate(diagnostic)) {
222
+ * console.log(diagnostic.latestVersion)
223
+ * }
224
+ * ```
225
+ */
226
+ const isUpdate = isKind<UpdateDiagnostic>('update')
227
+
228
+ /**
229
+ * Glyph and accent color per severity, matching the miette/oxlint convention
230
+ * (`×` error, `⚠` warning, `ℹ` advice).
231
+ */
232
+ const severityStyle: Record<DiagnosticSeverity, { glyph: string; color: 'red' | 'yellow' | 'blue' }> = {
233
+ error: { glyph: '×', color: 'red' },
234
+ warning: { glyph: '⚠', color: 'yellow' },
235
+ info: { glyph: 'ℹ', color: 'blue' },
236
+ }
237
+
238
+ /**
239
+ * A {@link Diagnostic} reduced to its JSON-safe fields plus a `docsUrl`, for
240
+ * machine-readable output (the `--reporter json` report, the MCP tools). Drops the
241
+ * non-serializable `cause` and the `timing`/`duration` bookkeeping.
242
+ */
243
+ export type SerializedDiagnostic = {
244
+ code: DiagnosticCode
245
+ severity: DiagnosticSeverity
246
+ message: string
247
+ location?: DiagnosticLocation
248
+ help?: string
249
+ plugin?: string
250
+ /**
251
+ * The kubb.dev docs link for the code, omitted for the unknown fallback.
252
+ */
253
+ docsUrl?: string
254
+ }
255
+
256
+ /**
257
+ * Explanation for every {@link diagnosticCode}. Use {@link Diagnostics.explain} to look one up
258
+ * and `Diagnostics.docsUrl` for the matching kubb.dev page.
259
+ */
260
+ const diagnosticCatalog: Record<DiagnosticCode, DiagnosticDoc> = {
261
+ [diagnosticCode.unknown]: {
262
+ title: 'Unknown error',
263
+ cause: 'An error was thrown without a stable Kubb code, so it is reported as-is.',
264
+ fix: 'Read the underlying message and stack. If it comes from a plugin or adapter, check its configuration; otherwise report it as a possible Kubb bug.',
265
+ },
266
+ [diagnosticCode.inputNotFound]: {
267
+ title: 'Input not found',
268
+ cause: 'The file or URL set in `input.path` (or passed as `kubb generate PATH`) could not be read.',
269
+ fix: 'Check that the path or URL exists and is readable, then set it in `input.path` or pass it on the CLI.',
270
+ },
271
+ [diagnosticCode.inputRequired]: {
272
+ title: 'Input required',
273
+ cause: 'An adapter is configured but no `input` was provided.',
274
+ fix: 'Set `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config.',
275
+ },
276
+ [diagnosticCode.refNotFound]: {
277
+ title: 'Reference not found',
278
+ cause: 'A `$ref` could not be resolved in the source document.',
279
+ fix: 'Add the missing definition (for example under `components.schemas`) or fix the `$ref`. Run `kubb validate` to check the spec.',
280
+ },
281
+ [diagnosticCode.invalidServerVariable]: {
282
+ title: 'Invalid server variable',
283
+ cause: 'A server variable value is not allowed by its `enum`.',
284
+ fix: 'Use one of the values listed in the server variable `enum`, or update the spec.',
285
+ },
286
+ [diagnosticCode.pluginNotFound]: {
287
+ title: 'Plugin not found',
288
+ cause: 'A plugin that another plugin depends on is missing from the config.',
289
+ fix: 'Add the required plugin to the `plugins` array in kubb.config.ts, or remove the dependency on it.',
290
+ },
291
+ [diagnosticCode.pluginFailed]: {
292
+ title: 'Plugin failed',
293
+ cause: 'A plugin threw while generating, or reported an error through `ctx.error`.',
294
+ fix: 'Read the underlying error and check the plugin options and the schema or operation it failed on.',
295
+ },
296
+ [diagnosticCode.pluginWarning]: {
297
+ title: 'Plugin warning',
298
+ cause: 'A plugin reported a non-fatal warning through `ctx.warn`.',
299
+ fix: 'Review the message. It does not fail the build; adjust the plugin options or input if the warning is unwanted.',
300
+ },
301
+ [diagnosticCode.pluginInfo]: {
302
+ title: 'Plugin info',
303
+ cause: 'A plugin reported an informational message through `ctx.info`.',
304
+ fix: 'Informational only. No action is required.',
305
+ },
306
+ [diagnosticCode.unsupportedFormat]: {
307
+ title: 'Unsupported format',
308
+ cause: 'A schema uses a `format` Kubb does not map to a specific type, so it falls back to the base type.',
309
+ fix: 'Use a format Kubb supports, or handle the custom format with a parser or plugin.',
310
+ },
311
+ [diagnosticCode.deprecated]: {
312
+ title: 'Deprecated',
313
+ cause: 'A referenced schema or operation is marked `deprecated`.',
314
+ fix: 'Migrate off the deprecated definition if the warning is unwanted.',
315
+ },
316
+ [diagnosticCode.adapterRequired]: {
317
+ title: 'Adapter required',
318
+ cause: 'An action needs an adapter but none is configured.',
319
+ fix: 'Set `adapter` in kubb.config.ts, for example `adapterOas()`.',
320
+ },
321
+ [diagnosticCode.pathTraversal]: {
322
+ title: 'Path traversal',
323
+ cause: 'A resolved output path escaped the output directory, which can stem from a path traversal in the spec or a misconfigured `group.name`.',
324
+ fix: 'Keep generated paths within the output directory. Review the `group.name` function and the names coming from the spec.',
325
+ },
326
+ [diagnosticCode.hookFailed]: {
327
+ title: 'Hook failed',
328
+ cause: 'A post-generate shell hook (`hooks.done`) exited with a non-zero status.',
329
+ fix: 'Check the command is installed and correct, and run it manually to see the error.',
330
+ },
331
+ [diagnosticCode.formatFailed]: {
332
+ title: 'Format failed',
333
+ cause: 'The formatter pass over the generated files failed.',
334
+ fix: 'Check the formatter (oxfmt, biome, or prettier) is installed and its config is valid, then run it manually on the output.',
335
+ },
336
+ [diagnosticCode.lintFailed]: {
337
+ title: 'Lint failed',
338
+ cause: 'The linter pass over the generated files failed.',
339
+ fix: 'Check the linter (oxlint, biome, or eslint) is installed and its config is valid, then run it manually on the output.',
340
+ },
341
+ [diagnosticCode.performance]: {
342
+ title: 'Performance',
343
+ cause: 'Not a failure. Records a plugin’s elapsed time, summed into the run total.',
344
+ fix: 'No action. This is an informational metric.',
345
+ },
346
+ [diagnosticCode.updateAvailable]: {
347
+ title: 'Update available',
348
+ cause: 'A newer Kubb version is published on npm than the one running.',
349
+ fix: 'Update the `@kubb/*` packages, for example `npm install -g @kubb/cli`, to get the latest fixes.',
350
+ },
351
+ }
352
+
353
+ /**
354
+ * Static helpers for working with {@link Diagnostic}s, plus the run-scoped sink
355
+ * that lets deep code report a diagnostic without threading a callback.
356
+ *
357
+ * The sink lives in a single `AsyncLocalStorage` in the `@kubb/core` bundle.
358
+ * `Diagnostics.scope` activates it for a run, so anything inside that run (the
359
+ * adapter parse, a lazily consumed stream, a generator) reports through
360
+ * `Diagnostics.report` and lands in the same run.
361
+ */
362
+ export class Diagnostics {
363
+ static #reporterStorage = new AsyncLocalStorage<(diagnostic: Diagnostic) => void>()
364
+
365
+ /**
366
+ * The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
367
+ */
368
+ static code = diagnosticCode
369
+
370
+ /**
371
+ * Type guard for a build {@link ProblemDiagnostic}.
372
+ */
373
+ static isProblem = isProblem
374
+
375
+ /**
376
+ * Type guard for a version-update {@link UpdateDiagnostic}.
377
+ */
378
+ static isUpdate = isUpdate
379
+
380
+ /**
381
+ * Type guard for a per-plugin {@link PerformanceDiagnostic}.
382
+ */
383
+ static isPerformance = isPerformance
384
+
385
+ /**
386
+ * Narrows a {@link Diagnostic} to the variant for `code`, or `null` when it does not match.
387
+ */
388
+ static narrow = narrow
389
+
390
+ /**
391
+ * An `Error` that carries a {@link Diagnostic}, so structured problems can flow
392
+ * through the existing throw/catch paths while keeping their code and location.
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * throw new Diagnostics.Error({ code: diagnosticCode.refNotFound, severity: 'error', message: `Could not find ${ref}`, location: { kind: 'schema', pointer: ref, ref } })
397
+ * ```
398
+ */
399
+ static Error = class DiagnosticError extends Error {
400
+ diagnostic: ProblemDiagnostic
401
+
402
+ constructor(diagnostic: ProblemDiagnostic) {
403
+ super(diagnostic.message, { cause: diagnostic.cause })
404
+ this.name = 'DiagnosticError'
405
+ this.diagnostic = diagnostic
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Structural check for a {@link Diagnostics.Error}, including one thrown from a duplicated
411
+ * `@kubb/core` copy where `instanceof` fails. Matches on the `name` and a `diagnostic`
412
+ * that carries a `code`.
413
+ */
414
+ static isError(error: unknown): error is InstanceType<typeof Diagnostics.Error> {
415
+ if (error instanceof Diagnostics.Error) {
416
+ return true
417
+ }
418
+ return (
419
+ error instanceof Error &&
420
+ error.name === 'DiagnosticError' &&
421
+ 'diagnostic' in error &&
422
+ typeof (error as { diagnostic?: unknown }).diagnostic === 'object' &&
423
+ (error as { diagnostic?: Diagnostic }).diagnostic !== null &&
424
+ typeof (error as { diagnostic?: { code?: unknown } }).diagnostic?.code === 'string'
425
+ )
426
+ }
427
+
428
+ /**
429
+ * Runs `fn` with `sink` as the active diagnostic sink for the whole async
430
+ * subtree, so {@link Diagnostics.report} reaches it from anywhere inside.
431
+ */
432
+ static scope<T>(sink: (diagnostic: Diagnostic) => void, fn: () => T): T {
433
+ return Diagnostics.#reporterStorage.run(sink, fn)
434
+ }
435
+
436
+ /**
437
+ * Collects a diagnostic into the active build via the run-scoped sink, without throwing.
438
+ * Returns `true` when a run consumed it, `false` when called outside a {@link Diagnostics.scope}
439
+ * (so callers can fall back to throwing). Use a `warning`/`info` severity for non-fatal issues.
440
+ * For rendering a diagnostic live on the hook bus, use {@link Diagnostics.emit} instead.
441
+ */
442
+ static report(diagnostic: Diagnostic): boolean {
443
+ const sink = Diagnostics.#reporterStorage.getStore()
444
+ if (!sink) {
445
+ return false
446
+ }
447
+ sink(diagnostic)
448
+ return true
449
+ }
450
+
451
+ /**
452
+ * Emits a diagnostic on the run's `kubb:diagnostic` event so the loggers render it live.
453
+ * Use it instead of calling `hooks.emit('kubb:diagnostic', ...)` directly. To collect a
454
+ * diagnostic into the build result from deep in a run, use {@link Diagnostics.report} instead.
455
+ */
456
+ static async emit(hooks: AsyncEventEmitter<KubbHooks>, diagnostic: ProblemDiagnostic | UpdateDiagnostic): Promise<void> {
457
+ await hooks.emit('kubb:diagnostic', { diagnostic })
458
+ }
459
+
460
+ /**
461
+ * Coerces any thrown value into a {@link ProblemDiagnostic}. A {@link Diagnostics.Error}
462
+ * keeps its structured data, and anything else becomes a `KUBB_UNKNOWN` error.
463
+ */
464
+ static from(error: unknown): ProblemDiagnostic {
465
+ // The event emitter and BuildError wrap the original, so walk the cause chain to
466
+ // recover a Diagnostics.Error thrown deeper down. `root` tracks the deepest error so
467
+ // the unknown diagnostic reports the original message and stack, not the wrapper's.
468
+ const seen = new Set<unknown>()
469
+ let current: unknown = error
470
+ let root: Error | undefined
471
+ while (current instanceof Error && !seen.has(current)) {
472
+ // Match structurally, not just by `instanceof`: a `Diagnostics.Error` thrown from a
473
+ // duplicated `@kubb/core` copy (bundled into an adapter or plugin) is a different
474
+ // class, but still carries the same `diagnostic`, so its code must survive.
475
+ if (Diagnostics.isError(current)) {
476
+ return current.diagnostic
477
+ }
478
+ seen.add(current)
479
+ root = current
480
+ current = current.cause
481
+ }
482
+
483
+ return {
484
+ code: diagnosticCode.unknown,
485
+ severity: 'error',
486
+ message: root ? root.message : getErrorMessage(error),
487
+ cause: root,
488
+ }
489
+ }
490
+
491
+ /**
492
+ * Builds a per-plugin performance record. Reporters sum these into the run total.
493
+ */
494
+ static performance({ plugin, duration }: { plugin: string; duration: number }): PerformanceDiagnostic {
495
+ return {
496
+ kind: 'performance',
497
+ code: diagnosticCode.performance,
498
+ severity: 'info',
499
+ message: `${plugin} generated in ${Math.round(duration)}ms`,
500
+ plugin,
501
+ duration,
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Builds the version-update notice shown when a newer Kubb is published on npm.
507
+ */
508
+ static update({ currentVersion, latestVersion }: { currentVersion: string; latestVersion: string }): UpdateDiagnostic {
509
+ return {
510
+ kind: 'update',
511
+ code: diagnosticCode.updateAvailable,
512
+ severity: 'info',
513
+ message: `Update available: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`,
514
+ currentVersion,
515
+ latestVersion,
516
+ }
517
+ }
518
+
519
+ /**
520
+ * True when any diagnostic is an error, the severity that fails a build. Non-error
521
+ * diagnostics are ignored.
522
+ */
523
+ static hasError(diagnostics: ReadonlyArray<Diagnostic>): boolean {
524
+ return diagnostics.some((diagnostic) => diagnostic.severity === 'error')
525
+ }
526
+
527
+ /**
528
+ * Names of the plugins that failed, deduped, derived from the error diagnostics
529
+ * that carry a `plugin`.
530
+ */
531
+ static failedPlugins(diagnostics: ReadonlyArray<Diagnostic>): Array<string> {
532
+ const names = new Set<string>()
533
+ for (const diagnostic of diagnostics) {
534
+ if (diagnostic.severity === 'error' && diagnostic.plugin) {
535
+ names.add(diagnostic.plugin)
536
+ }
537
+ }
538
+ return [...names]
539
+ }
540
+
541
+ /**
542
+ * Counts `problem` diagnostics by severity for the run summary. `timing`
543
+ * diagnostics are ignored.
544
+ */
545
+ static count(diagnostics: ReadonlyArray<Diagnostic>): { errors: number; warnings: number; infos: number } {
546
+ let errors = 0
547
+ let warnings = 0
548
+ let infos = 0
549
+ for (const diagnostic of diagnostics) {
550
+ if (!isProblem(diagnostic)) {
551
+ continue
552
+ }
553
+ if (diagnostic.severity === 'error') {
554
+ errors += 1
555
+ } else if (diagnostic.severity === 'warning') {
556
+ warnings += 1
557
+ } else {
558
+ infos += 1
559
+ }
560
+ }
561
+ return { errors, warnings, infos }
562
+ }
563
+
564
+ /**
565
+ * Drops duplicate `problem` diagnostics that share a code, location pointer, and
566
+ * plugin, so the same issue reported across several passes is shown once. Non-problem
567
+ * diagnostics are always kept.
568
+ */
569
+ static dedupe(diagnostics: ReadonlyArray<Diagnostic>): Array<Diagnostic> {
570
+ const seen = new Set<string>()
571
+ const result: Array<Diagnostic> = []
572
+ for (const diagnostic of diagnostics) {
573
+ if (!isProblem(diagnostic)) {
574
+ result.push(diagnostic)
575
+ continue
576
+ }
577
+ const pointer = diagnostic.location && 'pointer' in diagnostic.location ? diagnostic.location.pointer : ''
578
+ const key = `${diagnostic.code} ${pointer} ${diagnostic.plugin ?? ''}`
579
+ if (seen.has(key)) {
580
+ continue
581
+ }
582
+ seen.add(key)
583
+ result.push(diagnostic)
584
+ }
585
+ return result
586
+ }
587
+
588
+ /**
589
+ * Builds the kubb.dev docs URL for a diagnostic code, e.g.
590
+ * `KUBB_REF_NOT_FOUND` → `https://kubb.dev/docs/5.x/reference/diagnostics/kubb-ref-not-found`.
591
+ */
592
+ static docsUrl(code: string): string {
593
+ const slug = code.toLowerCase().replaceAll('_', '-')
594
+ return `https://kubb.dev/docs/${docsMajor}.x/reference/diagnostics/${slug}`
595
+ }
596
+
597
+ /**
598
+ * The catalog entry for a code: its title, cause, and fix. Mirrors the kubb.dev
599
+ * `/diagnostics/<slug>` page.
600
+ */
601
+ static explain(code: DiagnosticCode): DiagnosticDoc {
602
+ return diagnosticCatalog[code]
603
+ }
604
+
605
+ /**
606
+ * Reduces a diagnostic to its JSON-safe fields plus a `docsUrl`, for machine-readable
607
+ * consumers. The `cause`, `kind`, and `duration` are dropped, and absent optional
608
+ * fields are omitted rather than set to `undefined`.
609
+ */
610
+ static serialize(diagnostic: Diagnostic): SerializedDiagnostic {
611
+ const problem = isProblem(diagnostic) ? diagnostic : undefined
612
+ return {
613
+ code: diagnostic.code,
614
+ severity: diagnostic.severity,
615
+ message: diagnostic.message,
616
+ ...(problem?.location ? { location: problem.location } : {}),
617
+ ...(problem?.help ? { help: problem.help } : {}),
618
+ ...(problem?.plugin ? { plugin: problem.plugin } : {}),
619
+ ...(diagnostic.code === diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }),
620
+ }
621
+ }
622
+
623
+ /**
624
+ * Renders a {@link Diagnostic} for terminal output as its parts: the colored severity `symbol`
625
+ * (the gutter glyph), the `plugin(CODE): message` `headline`, and the `details` lines (optional
626
+ * `at <pointer>`, `help:`, and `docs:`).
627
+ *
628
+ * Hosts compose these to fit their gutter: a clack logger passes `symbol` as its own gutter and
629
+ * `[headline, ...details]` as the message, while plain text outputs use {@link Diagnostics.formatLines}.
630
+ */
631
+ static format(diagnostic: Diagnostic): { symbol: string; headline: string; details: Array<string> } {
632
+ const { code, severity, message } = diagnostic
633
+ const { glyph, color } = severityStyle[severity]
634
+ const problem = isProblem(diagnostic) ? diagnostic : undefined
635
+
636
+ const rule = styleText(color, styleText('bold', problem?.plugin ? `${problem.plugin}(${code})` : code))
637
+ const details: Array<string> = []
638
+
639
+ if (problem?.location && 'pointer' in problem.location) {
640
+ details.push(` ${styleText('dim', 'at')} ${styleText('cyan', problem.location.pointer)}`)
641
+ }
642
+ if (problem?.help) {
643
+ details.push(` ${styleText('cyan', 'help:')} ${problem.help}`)
644
+ }
645
+ if (code !== diagnosticCode.unknown) {
646
+ details.push(` ${styleText('dim', 'docs:')} ${styleText('cyan', Diagnostics.docsUrl(code))}`)
647
+ }
648
+
649
+ return { symbol: styleText(color, styleText('bold', glyph)), headline: `${rule}: ${message}`, details }
650
+ }
651
+
652
+ /**
653
+ * The self-contained block form of {@link Diagnostics.format}: `${symbol} ${headline}` followed by
654
+ * the detail lines. Used where there is no gutter to own the symbol (plain and file output).
655
+ */
656
+ static formatLines(diagnostic: Diagnostic): Array<string> {
657
+ const { symbol, headline, details } = Diagnostics.format(diagnostic)
658
+ return [`${symbol} ${headline}`, ...details]
659
+ }
660
+ }
package/src/index.ts CHANGED
@@ -1,20 +1,22 @@
1
1
  export { AsyncEventEmitter, URLPath } from '@internals/utils'
2
2
  export * as ast from '@kubb/ast'
3
- export { logLevel } from './constants.ts'
4
3
  export { createAdapter } from './createAdapter.ts'
4
+ export { Diagnostics } from './diagnostics.ts'
5
5
  export { createKubb } from './createKubb.ts'
6
+ export { createReporter, selectReporters } from './createReporter.ts'
7
+ export { cliReporter } from './reporters/cliReporter.ts'
8
+ export { fileReporter } from './reporters/fileReporter.ts'
9
+ export { jsonReporter } from './reporters/jsonReporter.ts'
10
+ export { Telemetry } from './Telemetry.ts'
6
11
  export { createRenderer } from './createRenderer.ts'
7
12
  export { createStorage } from './createStorage.ts'
8
13
  export { defineGenerator } from './defineGenerator.ts'
9
- export { defineLogger } from './defineLogger.ts'
14
+ export { defineLogger, logLevel } from './defineLogger.ts'
10
15
  export { defineMiddleware } from './defineMiddleware.ts'
11
16
  export { defineParser } from './defineParser.ts'
12
17
  export { definePlugin } from './definePlugin.ts'
13
18
  export { defineResolver } from './defineResolver.ts'
14
- export { FileManager } from './FileManager.ts'
15
- export { FileProcessor } from './FileProcessor.ts'
16
- export { PluginDriver } from './PluginDriver.ts'
19
+ export { KubbDriver } from './KubbDriver.ts'
17
20
  export { fsStorage } from './storages/fsStorage.ts'
18
21
  export { memoryStorage } from './storages/memoryStorage.ts'
19
22
  export * from './types.ts'
20
- export { isInputPath } from './utils/isInputPath.ts'