@kubb/core 5.0.0-beta.4 → 5.0.0-beta.41
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/README.md +25 -158
- package/dist/diagnostics-Ba-FcsPo.d.ts +2970 -0
- package/dist/index.cjs +760 -1193
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +165 -171
- package/dist/index.js +745 -1188
- package/dist/index.js.map +1 -1
- package/dist/memoryStorage-DA1bnMte.js +2860 -0
- package/dist/memoryStorage-DA1bnMte.js.map +1 -0
- package/dist/memoryStorage-DZqlEW7H.cjs +2993 -0
- package/dist/memoryStorage-DZqlEW7H.cjs.map +1 -0
- package/dist/mocks.cjs +80 -18
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +35 -8
- package/dist/mocks.js +81 -21
- package/dist/mocks.js.map +1 -1
- package/package.json +8 -19
- package/src/FileManager.ts +85 -63
- package/src/FileProcessor.ts +171 -43
- package/src/HookRegistry.ts +45 -0
- package/src/KubbDriver.ts +906 -0
- package/src/Telemetry.ts +278 -0
- package/src/Transform.ts +58 -0
- package/src/constants.ts +111 -19
- package/src/createAdapter.ts +113 -17
- package/src/createKubb.ts +944 -492
- package/src/createRenderer.ts +58 -27
- package/src/createReporter.ts +147 -0
- package/src/createStorage.ts +36 -23
- package/src/defineGenerator.ts +129 -17
- package/src/defineLogger.ts +58 -5
- package/src/defineMiddleware.ts +19 -17
- package/src/defineParser.ts +30 -13
- package/src/definePlugin.ts +320 -17
- package/src/defineResolver.ts +381 -179
- package/src/diagnostics.ts +660 -0
- package/src/index.ts +8 -6
- package/src/mocks.ts +92 -19
- 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/storages/fsStorage.ts +13 -37
- package/src/types.ts +60 -1297
- package/dist/PluginDriver-Ds-Us-e4.cjs +0 -1036
- package/dist/PluginDriver-Ds-Us-e4.cjs.map +0 -1
- package/dist/PluginDriver-Wi34Pegx.js +0 -945
- package/dist/PluginDriver-Wi34Pegx.js.map +0 -1
- package/dist/types-Cd0jhNmx.d.ts +0 -2153
- package/src/Kubb.ts +0 -300
- package/src/PluginDriver.ts +0 -424
- package/src/devtools.ts +0 -59
- package/src/renderNode.ts +0 -35
- package/src/utils/diagnostics.ts +0 -18
- package/src/utils/isInputPath.ts +0 -10
- package/src/utils/packageJSON.ts +0 -99
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/src/mocks.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import { resolve } from 'node:path'
|
|
2
|
-
import
|
|
1
|
+
import path, { resolve } from 'node:path'
|
|
2
|
+
import { camelCase } from '@internals/utils'
|
|
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'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
+
import { FileProcessor } from './FileProcessor.ts'
|
|
9
|
+
import { KubbDriver } from './KubbDriver.ts'
|
|
10
|
+
import { memoryStorage } from './storages/memoryStorage.ts'
|
|
11
|
+
import type { Adapter, AdapterFactoryOptions, Config, Generator, GeneratorContext, NormalizedPlugin, PluginFactoryOptions, RendererFactory } from './types.ts'
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
|
|
11
15
|
* Creates a minimal `PluginDriver` mock for unit tests.
|
|
12
16
|
*/
|
|
13
|
-
export function createMockedPluginDriver(options: { name?: string; plugin?: NormalizedPlugin; config?: Config } = {}):
|
|
17
|
+
export function createMockedPluginDriver(options: { name?: string; plugin?: NormalizedPlugin; config?: Config } = {}): KubbDriver {
|
|
18
|
+
const fileManager = new FileManager()
|
|
19
|
+
|
|
14
20
|
return {
|
|
15
21
|
config: options?.config ?? {
|
|
16
22
|
root: '.',
|
|
@@ -22,29 +28,45 @@ export function createMockedPluginDriver(options: { name?: string; plugin?: Norm
|
|
|
22
28
|
return options?.plugin
|
|
23
29
|
},
|
|
24
30
|
getResolver: (_pluginName: string) => options?.plugin?.resolver,
|
|
25
|
-
fileManager
|
|
26
|
-
|
|
31
|
+
fileManager,
|
|
32
|
+
async dispatch({ result, renderer }: { result: unknown; renderer?: RendererFactory | null }): Promise<void> {
|
|
33
|
+
if (!result) return
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(result)) {
|
|
36
|
+
fileManager.upsert(...(result as Array<FileNode>))
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!renderer) return
|
|
41
|
+
|
|
42
|
+
using instance = renderer()
|
|
43
|
+
if (instance.stream) {
|
|
44
|
+
for (const file of instance.stream(result)) fileManager.upsert(file)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await instance.render(result)
|
|
49
|
+
fileManager.upsert(...instance.files)
|
|
50
|
+
},
|
|
51
|
+
} as unknown as KubbDriver
|
|
27
52
|
}
|
|
28
53
|
|
|
29
54
|
/**
|
|
30
55
|
* Creates a minimal `Adapter` mock for unit tests.
|
|
31
|
-
* `parse` returns an empty `InputNode` by default
|
|
56
|
+
* `parse` returns an empty `InputNode` by default. Override via `options.parse`.
|
|
32
57
|
* `getImports` returns `[]` by default.
|
|
33
58
|
*/
|
|
34
59
|
export function createMockedAdapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions>(
|
|
35
60
|
options: {
|
|
36
61
|
name?: TOptions['name']
|
|
37
62
|
resolvedOptions?: TOptions['resolvedOptions']
|
|
38
|
-
inputNode?: Adapter<TOptions>['inputNode']
|
|
39
63
|
parse?: Adapter<TOptions>['parse']
|
|
40
64
|
getImports?: Adapter<TOptions>['getImports']
|
|
41
65
|
} = {},
|
|
42
66
|
): Adapter<TOptions> {
|
|
43
|
-
const inputNode = options.inputNode ?? null
|
|
44
67
|
return {
|
|
45
68
|
name: (options.name ?? 'oas') as TOptions['name'],
|
|
46
69
|
options: (options.resolvedOptions ?? {}) as TOptions['resolvedOptions'],
|
|
47
|
-
inputNode,
|
|
48
70
|
parse: options.parse ?? (async () => ({ kind: 'Input' as const, schemas: [], operations: [] })),
|
|
49
71
|
getImports: options.getImports ?? ((_node: SchemaNode, _resolve: (schemaName: string) => { name: string; path: string }) => []),
|
|
50
72
|
} as Adapter<TOptions>
|
|
@@ -76,7 +98,8 @@ export function createMockedPlugin<TOptions extends PluginFactoryOptions = Plugi
|
|
|
76
98
|
type RenderGeneratorOptions<TOptions extends PluginFactoryOptions> = {
|
|
77
99
|
config: Config
|
|
78
100
|
adapter: Adapter
|
|
79
|
-
|
|
101
|
+
meta?: InputMeta
|
|
102
|
+
driver: KubbDriver
|
|
80
103
|
plugin: NormalizedPlugin<TOptions>
|
|
81
104
|
options: TOptions['resolvedOptions']
|
|
82
105
|
resolver: TOptions['resolver']
|
|
@@ -88,20 +111,19 @@ function createMockedPluginContext<TOptions extends PluginFactoryOptions>(opts:
|
|
|
88
111
|
return {
|
|
89
112
|
config: opts.config,
|
|
90
113
|
root,
|
|
91
|
-
getMode: (output: { path: string }) =>
|
|
114
|
+
getMode: (output: { path: string }) => KubbDriver.getMode(resolve(root, output.path)),
|
|
92
115
|
adapter: opts.adapter,
|
|
93
116
|
resolver: opts.resolver,
|
|
94
117
|
plugin: opts.plugin,
|
|
95
118
|
driver: opts.driver,
|
|
96
119
|
getResolver: (name: string) => opts.driver.getResolver(name),
|
|
97
|
-
|
|
120
|
+
meta: opts.meta ?? { circularNames: [], enumNames: [] },
|
|
98
121
|
addFile: async (...files: Array<FileNode>) => opts.driver.fileManager.add(...files),
|
|
99
122
|
upsertFile: async (...files: Array<FileNode>) => opts.driver.fileManager.upsert(...files),
|
|
100
123
|
hooks: opts.driver.hooks ?? ({} as never),
|
|
101
124
|
warn: (msg: string) => console.warn(msg),
|
|
102
125
|
error: (msg: string) => console.error(msg),
|
|
103
126
|
info: (msg: string) => console.info(msg),
|
|
104
|
-
openInStudio: async () => {},
|
|
105
127
|
} as unknown as Omit<GeneratorContext<TOptions>, 'options'>
|
|
106
128
|
}
|
|
107
129
|
|
|
@@ -126,7 +148,7 @@ export async function renderGeneratorSchema<TOptions extends PluginFactoryOption
|
|
|
126
148
|
...context,
|
|
127
149
|
options: opts.options,
|
|
128
150
|
})
|
|
129
|
-
await
|
|
151
|
+
await opts.driver.dispatch({ result, renderer: generator.renderer })
|
|
130
152
|
}
|
|
131
153
|
|
|
132
154
|
/**
|
|
@@ -150,7 +172,7 @@ export async function renderGeneratorOperation<TOptions extends PluginFactoryOpt
|
|
|
150
172
|
...context,
|
|
151
173
|
options: opts.options,
|
|
152
174
|
})
|
|
153
|
-
await
|
|
175
|
+
await opts.driver.dispatch({ result, renderer: generator.renderer })
|
|
154
176
|
}
|
|
155
177
|
|
|
156
178
|
/**
|
|
@@ -174,5 +196,56 @@ export async function renderGeneratorOperations<TOptions extends PluginFactoryOp
|
|
|
174
196
|
...context,
|
|
175
197
|
options: opts.options,
|
|
176
198
|
})
|
|
177
|
-
await
|
|
199
|
+
await opts.driver.dispatch({ result, renderer: generator.renderer })
|
|
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
|
|
178
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
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { getElapsedMs } from '@internals/utils'
|
|
3
|
+
import type { GenerationResult } from '../createReporter.ts'
|
|
4
|
+
import { Diagnostics, type SerializedDiagnostic } from '../diagnostics.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One plugin's elapsed time, derived from a `performance` diagnostic.
|
|
8
|
+
*/
|
|
9
|
+
export type ReportTiming = {
|
|
10
|
+
plugin: string
|
|
11
|
+
durationMs: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The normalized result of generating one config, shared by every reporter. Each reporter renders
|
|
16
|
+
* the same {@link Report} in its own format (the `cli` summary, the `json` document, the `file`
|
|
17
|
+
* log), so they always agree on the numbers. Build it with {@link buildReport}.
|
|
18
|
+
*/
|
|
19
|
+
export type Report = {
|
|
20
|
+
/**
|
|
21
|
+
* The config name, or an empty string when it is unnamed.
|
|
22
|
+
*/
|
|
23
|
+
name: string
|
|
24
|
+
status: 'success' | 'failed'
|
|
25
|
+
plugins: {
|
|
26
|
+
passed: number
|
|
27
|
+
/**
|
|
28
|
+
* Names of the plugins that failed.
|
|
29
|
+
*/
|
|
30
|
+
failed: Array<string>
|
|
31
|
+
total: number
|
|
32
|
+
}
|
|
33
|
+
counts: {
|
|
34
|
+
errors: number
|
|
35
|
+
warnings: number
|
|
36
|
+
infos: number
|
|
37
|
+
}
|
|
38
|
+
filesCreated: number
|
|
39
|
+
/**
|
|
40
|
+
* Wall-clock time spent generating this config, in milliseconds.
|
|
41
|
+
*/
|
|
42
|
+
durationMs: number
|
|
43
|
+
/**
|
|
44
|
+
* Absolute output directory the files were written to.
|
|
45
|
+
*/
|
|
46
|
+
output: string
|
|
47
|
+
/**
|
|
48
|
+
* Per-plugin durations, slowest first.
|
|
49
|
+
*/
|
|
50
|
+
timings: Array<ReportTiming>
|
|
51
|
+
/**
|
|
52
|
+
* The build problems, serialized to their JSON-safe fields plus a `docsUrl`.
|
|
53
|
+
*/
|
|
54
|
+
diagnostics: Array<SerializedDiagnostic>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Builds the normalized {@link Report} for one config from its {@link GenerationResult}. Splits the
|
|
59
|
+
* diagnostics into problems and per-plugin timings (slowest first) and derives the plugin and issue
|
|
60
|
+
* counts, so every reporter renders the same data.
|
|
61
|
+
*/
|
|
62
|
+
export function buildReport(result: GenerationResult): Report {
|
|
63
|
+
const { config, diagnostics, filesCreated, status, hrStart } = result
|
|
64
|
+
|
|
65
|
+
const failed = Diagnostics.failedPlugins(diagnostics)
|
|
66
|
+
const total = config.plugins?.length ?? 0
|
|
67
|
+
const counts = Diagnostics.count(diagnostics)
|
|
68
|
+
const problems = diagnostics.filter(Diagnostics.isProblem)
|
|
69
|
+
const timings = diagnostics
|
|
70
|
+
.filter(Diagnostics.isPerformance)
|
|
71
|
+
.sort((a, b) => b.duration - a.duration)
|
|
72
|
+
.map((diagnostic) => ({ plugin: diagnostic.plugin, durationMs: diagnostic.duration }))
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
name: config.name ?? '',
|
|
76
|
+
status,
|
|
77
|
+
plugins: { passed: total - failed.length, failed, total },
|
|
78
|
+
counts,
|
|
79
|
+
filesCreated,
|
|
80
|
+
durationMs: getElapsedMs(hrStart),
|
|
81
|
+
output: resolve(config.root, config.output.path),
|
|
82
|
+
timings,
|
|
83
|
+
diagnostics: problems.map((diagnostic) => Diagnostics.serialize(diagnostic)),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -4,13 +4,6 @@ import { join, resolve } from 'node:path'
|
|
|
4
4
|
import { clean, write } from '@internals/utils'
|
|
5
5
|
import { createStorage } from '../createStorage.ts'
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Detects the filesystem error used to indicate that a path does not exist.
|
|
9
|
-
*/
|
|
10
|
-
function isMissingPathError(error: unknown): error is NodeJS.ErrnoException {
|
|
11
|
-
return typeof error === 'object' && error !== null && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT'
|
|
12
|
-
}
|
|
13
|
-
|
|
14
7
|
/**
|
|
15
8
|
* Built-in filesystem storage driver.
|
|
16
9
|
*
|
|
@@ -42,27 +35,15 @@ export const fsStorage = createStorage(() => ({
|
|
|
42
35
|
try {
|
|
43
36
|
await access(resolve(key))
|
|
44
37
|
return true
|
|
45
|
-
} catch (
|
|
46
|
-
|
|
47
|
-
return false
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
throw new Error(`Failed to access storage item "${key}"`, {
|
|
51
|
-
cause: error as Error,
|
|
52
|
-
})
|
|
38
|
+
} catch (_error) {
|
|
39
|
+
return false
|
|
53
40
|
}
|
|
54
41
|
},
|
|
55
42
|
async getItem(key: string) {
|
|
56
43
|
try {
|
|
57
44
|
return await readFile(resolve(key), 'utf8')
|
|
58
|
-
} catch (
|
|
59
|
-
|
|
60
|
-
return null
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
throw new Error(`Failed to read storage item "${key}"`, {
|
|
64
|
-
cause: error as Error,
|
|
65
|
-
})
|
|
45
|
+
} catch (_error) {
|
|
46
|
+
return null
|
|
66
47
|
}
|
|
67
48
|
},
|
|
68
49
|
async setItem(key: string, value: string) {
|
|
@@ -72,36 +53,31 @@ export const fsStorage = createStorage(() => ({
|
|
|
72
53
|
await rm(resolve(key), { force: true })
|
|
73
54
|
},
|
|
74
55
|
async getKeys(base?: string) {
|
|
75
|
-
const keys: Array<string> = []
|
|
76
56
|
const resolvedBase = resolve(base ?? process.cwd())
|
|
77
57
|
|
|
78
|
-
async function walk(dir: string, prefix: string):
|
|
58
|
+
async function* walk(dir: string, prefix: string): AsyncGenerator<string, void, undefined> {
|
|
79
59
|
let entries: Array<Dirent>
|
|
80
60
|
try {
|
|
81
61
|
entries = (await readdir(dir, {
|
|
82
62
|
withFileTypes: true,
|
|
83
63
|
})) as Array<Dirent>
|
|
84
|
-
} catch (
|
|
85
|
-
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
throw new Error(`Failed to list storage keys under "${resolvedBase}"`, {
|
|
90
|
-
cause: error as Error,
|
|
91
|
-
})
|
|
64
|
+
} catch (_error) {
|
|
65
|
+
return
|
|
92
66
|
}
|
|
93
67
|
for (const entry of entries) {
|
|
94
68
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name
|
|
95
69
|
if (entry.isDirectory()) {
|
|
96
|
-
|
|
70
|
+
yield* walk(join(dir, entry.name), rel)
|
|
97
71
|
} else {
|
|
98
|
-
|
|
72
|
+
yield rel
|
|
99
73
|
}
|
|
100
74
|
}
|
|
101
75
|
}
|
|
102
76
|
|
|
103
|
-
|
|
104
|
-
|
|
77
|
+
const keys: Array<string> = []
|
|
78
|
+
for await (const key of walk(resolvedBase, '')) {
|
|
79
|
+
keys.push(key)
|
|
80
|
+
}
|
|
105
81
|
return keys
|
|
106
82
|
},
|
|
107
83
|
async clear(base?: string) {
|