@kubb/core 5.0.0-beta.38 → 5.0.0-beta.39
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.
- package/dist/{diagnostics-CYfKPtbU.d.ts → diagnostics-DhfW8YpT.d.ts} +289 -347
- package/dist/index.cjs +553 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +164 -119
- package/dist/index.js +532 -149
- package/dist/index.js.map +1 -1
- package/dist/{KubbDriver-CyNF-NIb.js → memoryStorage-CNQTs-YG.js} +240 -157
- package/dist/memoryStorage-CNQTs-YG.js.map +1 -0
- package/dist/{KubbDriver-BYBUfOZ8.cjs → memoryStorage-DHi1d0To.cjs} +255 -196
- package/dist/memoryStorage-DHi1d0To.cjs.map +1 -0
- package/dist/mocks.cjs +36 -6
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +30 -3
- package/dist/mocks.js +33 -5
- package/dist/mocks.js.map +1 -1
- package/package.json +4 -4
- package/src/KubbDriver.ts +21 -65
- package/src/Telemetry.ts +278 -0
- package/src/constants.ts +14 -21
- package/src/createKubb.ts +22 -50
- package/src/createReporter.ts +43 -13
- package/src/defineGenerator.ts +2 -6
- package/src/defineLogger.ts +13 -1
- package/src/defineResolver.ts +3 -4
- package/src/diagnostics.ts +130 -61
- package/src/index.ts +8 -16
- package/src/mocks.ts +57 -2
- package/src/reporters/cliReporter.ts +90 -0
- package/src/reporters/fileReporter.ts +103 -0
- package/src/reporters/jsonReporter.ts +20 -0
- package/src/reporters/report.ts +85 -0
- package/src/types.ts +0 -1
- package/dist/KubbDriver-BYBUfOZ8.cjs.map +0 -1
- package/dist/KubbDriver-CyNF-NIb.js.map +0 -1
- package/src/devtools.ts +0 -53
package/src/diagnostics.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
import { styleText } from 'node:util'
|
|
2
3
|
import type { AsyncEventEmitter } from '@internals/utils'
|
|
3
4
|
import { getErrorMessage } from '@internals/utils'
|
|
4
5
|
import { version } from '../package.json'
|
|
@@ -159,7 +160,7 @@ export type UpdateDiagnostic = {
|
|
|
159
160
|
export type Diagnostic = ProblemDiagnostic | PerformanceDiagnostic | UpdateDiagnostic
|
|
160
161
|
|
|
161
162
|
/**
|
|
162
|
-
* Maps each {@link DiagnosticCode} to the variant it selects, for {@link
|
|
163
|
+
* Maps each {@link DiagnosticCode} to the variant it selects, for {@link narrow}.
|
|
163
164
|
* Every {@link ProblemCode} selects a {@link ProblemDiagnostic}, and the two bookkeeping codes
|
|
164
165
|
* select their own variant.
|
|
165
166
|
*/
|
|
@@ -172,13 +173,13 @@ export type DiagnosticByCode = Record<ProblemCode, ProblemDiagnostic> &
|
|
|
172
173
|
*
|
|
173
174
|
* @example
|
|
174
175
|
* ```ts
|
|
175
|
-
* const update =
|
|
176
|
+
* const update = narrow(diagnostic, diagnosticCode.updateAvailable)
|
|
176
177
|
* if (update) {
|
|
177
178
|
* console.log(update.latestVersion)
|
|
178
179
|
* }
|
|
179
180
|
* ```
|
|
180
181
|
*/
|
|
181
|
-
|
|
182
|
+
function narrow<C extends DiagnosticCode>(diagnostic: Diagnostic, code: C): DiagnosticByCode[C] | null {
|
|
182
183
|
return diagnostic.code === code ? (diagnostic as DiagnosticByCode[C]) : null
|
|
183
184
|
}
|
|
184
185
|
|
|
@@ -195,34 +196,44 @@ function isKind<T extends Diagnostic>(kind: DiagnosticKind) {
|
|
|
195
196
|
*
|
|
196
197
|
* @example
|
|
197
198
|
* ```ts
|
|
198
|
-
* if (
|
|
199
|
+
* if (isProblem(diagnostic)) {
|
|
199
200
|
* console.log(diagnostic.location)
|
|
200
201
|
* }
|
|
201
202
|
* ```
|
|
202
203
|
*/
|
|
203
|
-
|
|
204
|
+
const isProblem = isKind<ProblemDiagnostic>('problem')
|
|
204
205
|
|
|
205
206
|
/**
|
|
206
207
|
* Returns `true` when the diagnostic is a per-plugin {@link PerformanceDiagnostic}.
|
|
207
208
|
*
|
|
208
209
|
* @example
|
|
209
210
|
* ```ts
|
|
210
|
-
* const timings = diagnostics.filter(
|
|
211
|
+
* const timings = diagnostics.filter(isPerformance)
|
|
211
212
|
* ```
|
|
212
213
|
*/
|
|
213
|
-
|
|
214
|
+
const isPerformance = isKind<PerformanceDiagnostic>('performance')
|
|
214
215
|
|
|
215
216
|
/**
|
|
216
217
|
* Returns `true` when the diagnostic is a version-update {@link UpdateDiagnostic}.
|
|
217
218
|
*
|
|
218
219
|
* @example
|
|
219
220
|
* ```ts
|
|
220
|
-
* if (
|
|
221
|
+
* if (isUpdate(diagnostic)) {
|
|
221
222
|
* console.log(diagnostic.latestVersion)
|
|
222
223
|
* }
|
|
223
224
|
* ```
|
|
224
225
|
*/
|
|
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
|
+
}
|
|
226
237
|
|
|
227
238
|
/**
|
|
228
239
|
* A {@link Diagnostic} reduced to its JSON-safe fields plus a `docsUrl`, for
|
|
@@ -242,49 +253,11 @@ export type SerializedDiagnostic = {
|
|
|
242
253
|
docsUrl?: string
|
|
243
254
|
}
|
|
244
255
|
|
|
245
|
-
/**
|
|
246
|
-
* An `Error` that carries a {@link Diagnostic}, so structured problems can flow
|
|
247
|
-
* through the existing throw/catch paths while keeping their code and location.
|
|
248
|
-
*
|
|
249
|
-
* @example
|
|
250
|
-
* ```ts
|
|
251
|
-
* throw new DiagnosticError({ code: diagnosticCode.refNotFound, severity: 'error', message: `Could not find ${ref}`, location: { kind: 'schema', pointer: ref, ref } })
|
|
252
|
-
* ```
|
|
253
|
-
*/
|
|
254
|
-
export class DiagnosticError extends Error {
|
|
255
|
-
diagnostic: ProblemDiagnostic
|
|
256
|
-
|
|
257
|
-
constructor(diagnostic: ProblemDiagnostic) {
|
|
258
|
-
super(diagnostic.message, { cause: diagnostic.cause })
|
|
259
|
-
this.name = 'DiagnosticError'
|
|
260
|
-
this.diagnostic = diagnostic
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Structural check for a {@link DiagnosticError}, including one thrown from a duplicated
|
|
266
|
-
* `@kubb/core` copy where `instanceof` fails. Matches on the `name` and a `diagnostic`
|
|
267
|
-
* that carries a `code`.
|
|
268
|
-
*/
|
|
269
|
-
function isDiagnosticError(error: unknown): error is DiagnosticError {
|
|
270
|
-
if (error instanceof DiagnosticError) {
|
|
271
|
-
return true
|
|
272
|
-
}
|
|
273
|
-
return (
|
|
274
|
-
error instanceof Error &&
|
|
275
|
-
error.name === 'DiagnosticError' &&
|
|
276
|
-
'diagnostic' in error &&
|
|
277
|
-
typeof (error as { diagnostic?: unknown }).diagnostic === 'object' &&
|
|
278
|
-
(error as { diagnostic?: Diagnostic }).diagnostic !== null &&
|
|
279
|
-
typeof (error as { diagnostic?: { code?: unknown } }).diagnostic?.code === 'string'
|
|
280
|
-
)
|
|
281
|
-
}
|
|
282
|
-
|
|
283
256
|
/**
|
|
284
257
|
* Explanation for every {@link diagnosticCode}. Use {@link Diagnostics.explain} to look one up
|
|
285
258
|
* and `Diagnostics.docsUrl` for the matching kubb.dev page.
|
|
286
259
|
*/
|
|
287
|
-
|
|
260
|
+
const diagnosticCatalog: Record<DiagnosticCode, DiagnosticDoc> = {
|
|
288
261
|
[diagnosticCode.unknown]: {
|
|
289
262
|
title: 'Unknown error',
|
|
290
263
|
cause: 'An error was thrown without a stable Kubb code, so it is reported as-is.',
|
|
@@ -342,14 +315,9 @@ export const diagnosticCatalog: Record<DiagnosticCode, DiagnosticDoc> = {
|
|
|
342
315
|
},
|
|
343
316
|
[diagnosticCode.adapterRequired]: {
|
|
344
317
|
title: 'Adapter required',
|
|
345
|
-
cause: 'An action needs an adapter
|
|
318
|
+
cause: 'An action needs an adapter but none is configured.',
|
|
346
319
|
fix: 'Set `adapter` in kubb.config.ts, for example `adapterOas()`.',
|
|
347
320
|
},
|
|
348
|
-
[diagnosticCode.devtoolsInvalid]: {
|
|
349
|
-
title: 'Invalid devtools config',
|
|
350
|
-
cause: 'The `devtools` config is set to something other than an object.',
|
|
351
|
-
fix: 'Set `devtools` to an options object, or remove it to disable Kubb Studio.',
|
|
352
|
-
},
|
|
353
321
|
[diagnosticCode.pathTraversal]: {
|
|
354
322
|
title: 'Path traversal',
|
|
355
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`.',
|
|
@@ -394,6 +362,69 @@ export const diagnosticCatalog: Record<DiagnosticCode, DiagnosticDoc> = {
|
|
|
394
362
|
export class Diagnostics {
|
|
395
363
|
static #reporterStorage = new AsyncLocalStorage<(diagnostic: Diagnostic) => void>()
|
|
396
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
|
+
|
|
397
428
|
/**
|
|
398
429
|
* Runs `fn` with `sink` as the active diagnostic sink for the whole async
|
|
399
430
|
* subtree, so {@link Diagnostics.report} reaches it from anywhere inside.
|
|
@@ -427,21 +458,21 @@ export class Diagnostics {
|
|
|
427
458
|
}
|
|
428
459
|
|
|
429
460
|
/**
|
|
430
|
-
* Coerces any thrown value into a {@link ProblemDiagnostic}. A {@link
|
|
461
|
+
* Coerces any thrown value into a {@link ProblemDiagnostic}. A {@link Diagnostics.Error}
|
|
431
462
|
* keeps its structured data, and anything else becomes a `KUBB_UNKNOWN` error.
|
|
432
463
|
*/
|
|
433
464
|
static from(error: unknown): ProblemDiagnostic {
|
|
434
465
|
// The event emitter and BuildError wrap the original, so walk the cause chain to
|
|
435
|
-
// recover a
|
|
466
|
+
// recover a Diagnostics.Error thrown deeper down. `root` tracks the deepest error so
|
|
436
467
|
// the unknown diagnostic reports the original message and stack, not the wrapper's.
|
|
437
468
|
const seen = new Set<unknown>()
|
|
438
469
|
let current: unknown = error
|
|
439
470
|
let root: Error | undefined
|
|
440
471
|
while (current instanceof Error && !seen.has(current)) {
|
|
441
|
-
// Match structurally, not just by `instanceof`: a `
|
|
472
|
+
// Match structurally, not just by `instanceof`: a `Diagnostics.Error` thrown from a
|
|
442
473
|
// duplicated `@kubb/core` copy (bundled into an adapter or plugin) is a different
|
|
443
474
|
// class, but still carries the same `diagnostic`, so its code must survive.
|
|
444
|
-
if (
|
|
475
|
+
if (Diagnostics.isError(current)) {
|
|
445
476
|
return current.diagnostic
|
|
446
477
|
}
|
|
447
478
|
seen.add(current)
|
|
@@ -516,7 +547,7 @@ export class Diagnostics {
|
|
|
516
547
|
let warnings = 0
|
|
517
548
|
let infos = 0
|
|
518
549
|
for (const diagnostic of diagnostics) {
|
|
519
|
-
if (!
|
|
550
|
+
if (!isProblem(diagnostic)) {
|
|
520
551
|
continue
|
|
521
552
|
}
|
|
522
553
|
if (diagnostic.severity === 'error') {
|
|
@@ -539,7 +570,7 @@ export class Diagnostics {
|
|
|
539
570
|
const seen = new Set<string>()
|
|
540
571
|
const result: Array<Diagnostic> = []
|
|
541
572
|
for (const diagnostic of diagnostics) {
|
|
542
|
-
if (!
|
|
573
|
+
if (!isProblem(diagnostic)) {
|
|
543
574
|
result.push(diagnostic)
|
|
544
575
|
continue
|
|
545
576
|
}
|
|
@@ -577,7 +608,7 @@ export class Diagnostics {
|
|
|
577
608
|
* fields are omitted rather than set to `undefined`.
|
|
578
609
|
*/
|
|
579
610
|
static serialize(diagnostic: Diagnostic): SerializedDiagnostic {
|
|
580
|
-
const problem =
|
|
611
|
+
const problem = isProblem(diagnostic) ? diagnostic : undefined
|
|
581
612
|
return {
|
|
582
613
|
code: diagnostic.code,
|
|
583
614
|
severity: diagnostic.severity,
|
|
@@ -588,4 +619,42 @@ export class Diagnostics {
|
|
|
588
619
|
...(diagnostic.code === diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }),
|
|
589
620
|
}
|
|
590
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
|
+
}
|
|
591
660
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
|
-
export { AsyncEventEmitter
|
|
1
|
+
export { AsyncEventEmitter } from '@internals/utils'
|
|
2
2
|
export * as ast from '@kubb/ast'
|
|
3
|
-
export { diagnosticCode, logLevel } from './constants.ts'
|
|
4
3
|
export { createAdapter } from './createAdapter.ts'
|
|
5
|
-
export {
|
|
6
|
-
diagnosticCatalog,
|
|
7
|
-
DiagnosticError,
|
|
8
|
-
Diagnostics,
|
|
9
|
-
isPerformanceDiagnostic,
|
|
10
|
-
isProblemDiagnostic,
|
|
11
|
-
isUpdateDiagnostic,
|
|
12
|
-
narrowDiagnostic,
|
|
13
|
-
} from './diagnostics.ts'
|
|
4
|
+
export { Diagnostics } from './diagnostics.ts'
|
|
14
5
|
export { createKubb } from './createKubb.ts'
|
|
15
|
-
export { createReporter } from './createReporter.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'
|
|
16
11
|
export { createRenderer } from './createRenderer.ts'
|
|
17
12
|
export { createStorage } from './createStorage.ts'
|
|
18
13
|
export { defineGenerator } from './defineGenerator.ts'
|
|
19
|
-
export { defineLogger } from './defineLogger.ts'
|
|
14
|
+
export { defineLogger, logLevel } from './defineLogger.ts'
|
|
20
15
|
export { defineMiddleware } from './defineMiddleware.ts'
|
|
21
16
|
export { defineParser } from './defineParser.ts'
|
|
22
17
|
export { definePlugin } from './definePlugin.ts'
|
|
23
18
|
export { defineResolver } from './defineResolver.ts'
|
|
24
|
-
export { FileManager } from './FileManager.ts'
|
|
25
|
-
export { FileProcessor } from './FileProcessor.ts'
|
|
26
19
|
export { KubbDriver } from './KubbDriver.ts'
|
|
27
20
|
export { fsStorage } from './storages/fsStorage.ts'
|
|
28
21
|
export { memoryStorage } from './storages/memoryStorage.ts'
|
|
29
22
|
export * from './types.ts'
|
|
30
|
-
export { isInputPath } from './createKubb.ts'
|
package/src/mocks.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
1
|
+
import path, { resolve } from 'node:path'
|
|
2
|
+
import { camelCase } from '@internals/utils'
|
|
2
3
|
import type { FileNode, InputMeta, OperationNode, SchemaNode, Visitor } from '@kubb/ast'
|
|
3
4
|
import { transform } from '@kubb/ast'
|
|
5
|
+
import { expect } from 'vitest'
|
|
6
|
+
import type { Parser } from './defineParser.ts'
|
|
4
7
|
import { FileManager } from './FileManager.ts'
|
|
8
|
+
import { FileProcessor } from './FileProcessor.ts'
|
|
5
9
|
import { KubbDriver } from './KubbDriver.ts'
|
|
10
|
+
import { memoryStorage } from './storages/memoryStorage.ts'
|
|
6
11
|
import type { Adapter, AdapterFactoryOptions, Config, Generator, GeneratorContext, NormalizedPlugin, PluginFactoryOptions, RendererFactory } from './types.ts'
|
|
7
12
|
|
|
8
13
|
/**
|
|
@@ -119,7 +124,6 @@ function createMockedPluginContext<TOptions extends PluginFactoryOptions>(opts:
|
|
|
119
124
|
warn: (msg: string) => console.warn(msg),
|
|
120
125
|
error: (msg: string) => console.error(msg),
|
|
121
126
|
info: (msg: string) => console.info(msg),
|
|
122
|
-
openInStudio: async () => {},
|
|
123
127
|
} as unknown as Omit<GeneratorContext<TOptions>, 'options'>
|
|
124
128
|
}
|
|
125
129
|
|
|
@@ -194,3 +198,54 @@ export async function renderGeneratorOperations<TOptions extends PluginFactoryOp
|
|
|
194
198
|
})
|
|
195
199
|
await opts.driver.dispatch({ result, renderer: generator.renderer })
|
|
196
200
|
}
|
|
201
|
+
|
|
202
|
+
type MatchFilesOptions = {
|
|
203
|
+
/**
|
|
204
|
+
* Parsers indexed by file extension, used to render each `FileNode` to source.
|
|
205
|
+
* Without a matching parser the file's raw content is used.
|
|
206
|
+
*/
|
|
207
|
+
parsers?: Map<FileNode['extname'], Parser>
|
|
208
|
+
/**
|
|
209
|
+
* Formatter applied to non-JSON output before snapshotting, e.g. prettier. When
|
|
210
|
+
* omitted the parsed source is snapshotted as-is.
|
|
211
|
+
*/
|
|
212
|
+
format?: (source?: string) => string | Promise<string>
|
|
213
|
+
/**
|
|
214
|
+
* Subfolder under `__snapshots__`, camelCased. Useful to keep variant snapshots apart.
|
|
215
|
+
*/
|
|
216
|
+
pre?: string
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Renders the driver's collected `FileNode`s to source and asserts each against a file snapshot.
|
|
221
|
+
* Pair it with the `renderGenerator*` helpers to snapshot a generator's output.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })
|
|
226
|
+
* await matchFiles(driver.fileManager.files, { parsers, format })
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
export async function matchFiles(files: Array<FileNode> | undefined, options: MatchFilesOptions = {}): Promise<Map<string, string> | undefined> {
|
|
230
|
+
if (!files?.length) return
|
|
231
|
+
|
|
232
|
+
const { parsers = new Map(), format, pre } = options
|
|
233
|
+
const fileProcessor = new FileProcessor({ storage: memoryStorage(), parsers })
|
|
234
|
+
const processed = new Map<string, string>()
|
|
235
|
+
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
if (!file?.path || processed.has(file.path)) {
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const parsed = fileProcessor.parse(file)
|
|
242
|
+
const code = file.baseName.endsWith('.json') || !format ? parsed : await format(parsed)
|
|
243
|
+
|
|
244
|
+
processed.set(file.path, code)
|
|
245
|
+
|
|
246
|
+
const snapshotPath = path.join('__snapshots__', ...(pre ? [camelCase(pre)] : []), file.baseName)
|
|
247
|
+
await expect(code).toMatchFileSnapshot(snapshotPath)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return processed
|
|
251
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { styleText } from 'node:util'
|
|
2
|
+
import { formatMs, randomCliColor } from '@internals/utils'
|
|
3
|
+
import { SUMMARY_MAX_BAR_LENGTH, SUMMARY_TIME_SCALE_DIVISOR } from '../constants.ts'
|
|
4
|
+
import { logLevel as logLevelMap } from '../defineLogger.ts'
|
|
5
|
+
import { createReporter } from '../createReporter.ts'
|
|
6
|
+
import { buildReport, type Report } from './report.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds the vitest/jest-style summary for one {@link Report}: right-aligned dim labels with
|
|
10
|
+
* `N passed (total)` counts, and a per-plugin `Timings` section when `showTimings`.
|
|
11
|
+
*/
|
|
12
|
+
function buildSummaryLines(report: Report, { showTimings }: { showTimings: boolean }): Array<string> {
|
|
13
|
+
const { status, plugins, counts, filesCreated, durationMs, output, timings } = report
|
|
14
|
+
|
|
15
|
+
const rows: Array<[label: string, value: string]> = []
|
|
16
|
+
|
|
17
|
+
rows.push([
|
|
18
|
+
'Plugins',
|
|
19
|
+
status === 'success'
|
|
20
|
+
? `${styleText('green', `${plugins.passed} passed`)} (${plugins.total})`
|
|
21
|
+
: `${styleText('green', `${plugins.passed} passed`)} | ${styleText('red', `${plugins.failed.length} failed`)} (${plugins.total})`,
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
if (status === 'failed' && plugins.failed.length > 0) {
|
|
25
|
+
rows.push(['Failed', plugins.failed.map((name) => randomCliColor(name)).join(', ')])
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (counts.errors > 0 || counts.warnings > 0) {
|
|
29
|
+
const issues = [
|
|
30
|
+
counts.errors > 0 ? styleText('red', `${counts.errors} ${counts.errors === 1 ? 'error' : 'errors'}`) : undefined,
|
|
31
|
+
counts.warnings > 0 ? styleText('yellow', `${counts.warnings} ${counts.warnings === 1 ? 'warning' : 'warnings'}`) : undefined,
|
|
32
|
+
]
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.join(' | ')
|
|
35
|
+
rows.push(['Issues', issues])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
rows.push(['Files', `${styleText('green', String(filesCreated))} generated`])
|
|
39
|
+
rows.push(['Duration', styleText('green', formatMs(durationMs))])
|
|
40
|
+
rows.push(['Output', output])
|
|
41
|
+
|
|
42
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length), timings.length > 0 ? 'Timings'.length : 0)
|
|
43
|
+
const lines = rows.map(([label, value]) => `${styleText('dim', label.padStart(labelWidth))} ${value}`)
|
|
44
|
+
|
|
45
|
+
if (showTimings && timings.length > 0) {
|
|
46
|
+
const nameWidth = Math.max(0, ...timings.map((timing) => timing.plugin.length))
|
|
47
|
+
const indent = ' '.repeat(labelWidth + 2)
|
|
48
|
+
|
|
49
|
+
lines.push(styleText('dim', 'Timings'.padStart(labelWidth)))
|
|
50
|
+
for (const timing of timings) {
|
|
51
|
+
const timeStr = formatMs(timing.durationMs)
|
|
52
|
+
const barLength = Math.min(Math.ceil(timing.durationMs / SUMMARY_TIME_SCALE_DIVISOR), SUMMARY_MAX_BAR_LENGTH)
|
|
53
|
+
const bar = styleText('dim', '█'.repeat(barLength))
|
|
54
|
+
lines.push(`${indent}${styleText('dim', '•')} ${timing.plugin.padEnd(nameWidth)} ${bar} ${timeStr}`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return lines
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Renders the summary as plain `console.log` lines so it works in every CLI (no clack/TTY
|
|
63
|
+
* dependency): a blank line, the config name colored by status, then the summary rows.
|
|
64
|
+
*/
|
|
65
|
+
function renderSummary(lines: ReadonlyArray<string>, { title, status }: { title: string; status: 'success' | 'failed' }): void {
|
|
66
|
+
console.log('')
|
|
67
|
+
if (title) {
|
|
68
|
+
console.log(styleText(status === 'failed' ? 'red' : 'green', title))
|
|
69
|
+
}
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
console.log(line)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The default `cli` reporter. Renders the {@link Report} for each config as it finishes, independent
|
|
77
|
+
* of the live logger view. Suppressed at `silent`; the `verbose` level adds the per-plugin timings.
|
|
78
|
+
*/
|
|
79
|
+
export const cliReporter = createReporter({
|
|
80
|
+
name: 'cli',
|
|
81
|
+
report(result, { logLevel }) {
|
|
82
|
+
if (logLevel <= logLevelMap.silent) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const report = buildReport(result)
|
|
87
|
+
const lines = buildSummaryLines(report, { showTimings: logLevel >= logLevelMap.verbose })
|
|
88
|
+
renderSummary(lines, { title: report.name, status: report.status })
|
|
89
|
+
},
|
|
90
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { relative, resolve } from 'node:path'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import { stripVTControlCharacters } from 'node:util'
|
|
4
|
+
import { formatMs, write } from '@internals/utils'
|
|
5
|
+
import { createReporter } from '../createReporter.ts'
|
|
6
|
+
import { type Diagnostic, Diagnostics } from '../diagnostics.ts'
|
|
7
|
+
import { buildReport, type Report } from './report.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds the `## Summary` section: the same counts the cli and json reporters expose, as a list of
|
|
11
|
+
* `label value` rows with the labels padded to a common width.
|
|
12
|
+
*/
|
|
13
|
+
function buildSummarySection(report: Report): Array<string> {
|
|
14
|
+
const { status, plugins, counts, filesCreated, durationMs, output } = report
|
|
15
|
+
|
|
16
|
+
const rows: Array<[label: string, value: string]> = [
|
|
17
|
+
['Status', status],
|
|
18
|
+
[
|
|
19
|
+
'Plugins',
|
|
20
|
+
status === 'success' ? `${plugins.passed} passed (${plugins.total})` : `${plugins.passed} passed | ${plugins.failed.length} failed (${plugins.total})`,
|
|
21
|
+
],
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
if (plugins.failed.length > 0) {
|
|
25
|
+
rows.push(['Failed', plugins.failed.join(', ')])
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
rows.push(['Issues', `${counts.errors} errors | ${counts.warnings} warnings | ${counts.infos} infos`])
|
|
29
|
+
rows.push(['Files', `${filesCreated} generated`])
|
|
30
|
+
rows.push(['Duration', formatMs(durationMs)])
|
|
31
|
+
rows.push(['Output', output])
|
|
32
|
+
|
|
33
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length))
|
|
34
|
+
const lines = rows.map(([label, value]) => ` ${label.padEnd(labelWidth)} ${value}`)
|
|
35
|
+
|
|
36
|
+
return ['## Summary', '', ...lines]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Builds the `## Problems` section: each problem rendered in the miette block format, blocks
|
|
41
|
+
* separated by a blank line. Returns an empty array when there are no problems, so the caller
|
|
42
|
+
* can drop the heading.
|
|
43
|
+
*/
|
|
44
|
+
function buildProblemSection(diagnostics: ReadonlyArray<Diagnostic>): Array<string> {
|
|
45
|
+
const problems = diagnostics.filter(Diagnostics.isProblem)
|
|
46
|
+
if (problems.length === 0) {
|
|
47
|
+
return []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const blocks = problems.map((diagnostic) => Diagnostics.formatLines(diagnostic).join('\n'))
|
|
51
|
+
return ['## Problems', '', blocks.join('\n\n')]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Builds the `## Timings` section from a {@link Report}: one `plugin duration` row per record,
|
|
56
|
+
* slowest first with the plugin names left-aligned and the durations right-aligned. Returns an
|
|
57
|
+
* empty array when there are no timings.
|
|
58
|
+
*/
|
|
59
|
+
function buildTimingSection(report: Report): Array<string> {
|
|
60
|
+
const { timings } = report
|
|
61
|
+
if (timings.length === 0) {
|
|
62
|
+
return []
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const nameWidth = Math.max(...timings.map((timing) => timing.plugin.length))
|
|
66
|
+
const durations = timings.map((timing) => formatMs(timing.durationMs))
|
|
67
|
+
const durationWidth = Math.max(...durations.map((duration) => duration.length))
|
|
68
|
+
const rows = timings.map((timing, index) => ` ${timing.plugin.padEnd(nameWidth)} ${durations[index]!.padStart(durationWidth)}`)
|
|
69
|
+
|
|
70
|
+
return ['## Timings', '', ...rows]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The `file` reporter. Writes a config's {@link Report} to `.kubb/kubb-<name>-<timestamp>.log` as a
|
|
75
|
+
* plain-text document: a `# <name> — <timestamp>` header, a `## Summary` with the same counts the
|
|
76
|
+
* cli and json reporters expose, a `## Problems` section in the miette block format, and a
|
|
77
|
+
* `## Timings` section. Selected with `--reporter file` (or `reporters: ['file']`), replacing the
|
|
78
|
+
* old `--debug` flag.
|
|
79
|
+
*
|
|
80
|
+
* @note Unlike the streaming logger it replaced, it captures the collected diagnostics once a
|
|
81
|
+
* config finishes, not the live `kubb:info`/`kubb:plugin` event stream. Color is stripped so the
|
|
82
|
+
* file stays plain text even when the run is attached to a TTY.
|
|
83
|
+
*/
|
|
84
|
+
export const fileReporter = createReporter({
|
|
85
|
+
name: 'file',
|
|
86
|
+
async report(result) {
|
|
87
|
+
const { diagnostics, config } = result
|
|
88
|
+
if (diagnostics.length === 0) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const report = buildReport(result)
|
|
93
|
+
const header = config.name ? `# ${config.name} — ${new Date().toISOString()}` : `# ${new Date().toISOString()}`
|
|
94
|
+
const sections = [buildSummarySection(report), buildProblemSection(diagnostics), buildTimingSection(report)].filter((section) => section.length > 0)
|
|
95
|
+
const content = stripVTControlCharacters([header, ...sections.map((section) => section.join('\n'))].join('\n\n'))
|
|
96
|
+
|
|
97
|
+
const baseName = `${['kubb', config.name, Date.now()].filter(Boolean).join('-')}.log`
|
|
98
|
+
const pathName = resolve(process.cwd(), '.kubb', baseName)
|
|
99
|
+
|
|
100
|
+
await write(pathName, `${content}\n`)
|
|
101
|
+
console.error(`Debug log written to ${relative(process.cwd(), pathName)}`)
|
|
102
|
+
},
|
|
103
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
import { createReporter } from '../createReporter.ts'
|
|
3
|
+
import { buildReport } from './report.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `json` reporter. `report` returns one config's {@link Report}, which {@link createReporter}
|
|
7
|
+
* buffers, and `drain` writes them as a single pretty-printed JSON array on `kubb:lifecycle:end`.
|
|
8
|
+
* Buffering keeps a multi-config run one valid JSON document on stdout instead of concatenated
|
|
9
|
+
* objects that would break `jq .`. The terminal reporter is suppressed while `json` is active so
|
|
10
|
+
* stdout stays valid JSON.
|
|
11
|
+
*/
|
|
12
|
+
export const jsonReporter = createReporter({
|
|
13
|
+
name: 'json',
|
|
14
|
+
report(result) {
|
|
15
|
+
return buildReport(result)
|
|
16
|
+
},
|
|
17
|
+
drain(_context, reports) {
|
|
18
|
+
process.stdout.write(`${JSON.stringify(reports, null, 2)}\n`)
|
|
19
|
+
},
|
|
20
|
+
})
|