@kubb/core 5.0.0-beta.6 → 5.0.0-beta.60
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/LICENSE +17 -10
- package/README.md +25 -158
- package/dist/diagnostics-B-UZnFqP.d.ts +2906 -0
- package/dist/index.cjs +2497 -1071
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +80 -273
- package/dist/index.js +2487 -1067
- package/dist/index.js.map +1 -1
- package/dist/memoryStorage-CUj1hrxa.cjs +823 -0
- package/dist/memoryStorage-CUj1hrxa.cjs.map +1 -0
- package/dist/memoryStorage-CWFzAz4o.js +714 -0
- package/dist/memoryStorage-CWFzAz4o.js.map +1 -0
- package/dist/mocks.cjs +79 -19
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +35 -9
- package/dist/mocks.js +80 -22
- package/dist/mocks.js.map +1 -1
- package/package.json +8 -28
- package/src/FileManager.ts +86 -64
- package/src/FileProcessor.ts +170 -44
- package/src/KubbDriver.ts +908 -0
- package/src/Transform.ts +75 -0
- package/src/constants.ts +111 -20
- package/src/createAdapter.ts +112 -17
- package/src/createKubb.ts +140 -517
- package/src/createRenderer.ts +43 -28
- package/src/createReporter.ts +134 -0
- package/src/createStorage.ts +36 -23
- package/src/defineGenerator.ts +147 -17
- package/src/defineParser.ts +30 -12
- package/src/definePlugin.ts +370 -21
- package/src/defineResolver.ts +402 -212
- package/src/diagnostics.ts +662 -0
- package/src/index.ts +8 -8
- package/src/mocks.ts +91 -20
- package/src/reporters/cliReporter.ts +89 -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 +23 -55
- package/src/types.ts +411 -887
- package/dist/PluginDriver-BkTRD2H2.js +0 -946
- package/dist/PluginDriver-BkTRD2H2.js.map +0 -1
- package/dist/PluginDriver-Cadu4ORh.cjs +0 -1037
- package/dist/PluginDriver-Cadu4ORh.cjs.map +0 -1
- package/dist/types-DVPKmzw_.d.ts +0 -2159
- package/src/Kubb.ts +0 -300
- package/src/PluginDriver.ts +0 -426
- package/src/defineLogger.ts +0 -19
- package/src/defineMiddleware.ts +0 -62
- 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,21 @@
|
|
|
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 type { 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
|
-
|
|
11
14
|
* Creates a minimal `PluginDriver` mock for unit tests.
|
|
12
15
|
*/
|
|
13
|
-
export function createMockedPluginDriver(options: { name?: string; plugin?: NormalizedPlugin; config?: Config } = {}):
|
|
16
|
+
export function createMockedPluginDriver(options: { name?: string; plugin?: NormalizedPlugin; config?: Config } = {}): KubbDriver {
|
|
17
|
+
const fileManager = new FileManager()
|
|
18
|
+
|
|
14
19
|
return {
|
|
15
20
|
config: options?.config ?? {
|
|
16
21
|
root: '.',
|
|
@@ -22,29 +27,45 @@ export function createMockedPluginDriver(options: { name?: string; plugin?: Norm
|
|
|
22
27
|
return options?.plugin
|
|
23
28
|
},
|
|
24
29
|
getResolver: (_pluginName: string) => options?.plugin?.resolver,
|
|
25
|
-
fileManager
|
|
26
|
-
|
|
30
|
+
fileManager,
|
|
31
|
+
async dispatch({ result, renderer }: { result: unknown; renderer?: RendererFactory | null }): Promise<void> {
|
|
32
|
+
if (!result) return
|
|
33
|
+
|
|
34
|
+
if (Array.isArray(result)) {
|
|
35
|
+
fileManager.upsert(...(result as Array<FileNode>))
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!renderer) return
|
|
40
|
+
|
|
41
|
+
using instance = renderer()
|
|
42
|
+
if (instance.stream) {
|
|
43
|
+
for (const file of instance.stream(result)) fileManager.upsert(file)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await instance.render(result)
|
|
48
|
+
fileManager.upsert(...instance.files)
|
|
49
|
+
},
|
|
50
|
+
} as unknown as KubbDriver
|
|
27
51
|
}
|
|
28
52
|
|
|
29
53
|
/**
|
|
30
54
|
* Creates a minimal `Adapter` mock for unit tests.
|
|
31
|
-
* `parse` returns an empty `InputNode` by default
|
|
55
|
+
* `parse` returns an empty `InputNode` by default. Override via `options.parse`.
|
|
32
56
|
* `getImports` returns `[]` by default.
|
|
33
57
|
*/
|
|
34
58
|
export function createMockedAdapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions>(
|
|
35
59
|
options: {
|
|
36
60
|
name?: TOptions['name']
|
|
37
61
|
resolvedOptions?: TOptions['resolvedOptions']
|
|
38
|
-
inputNode?: Adapter<TOptions>['inputNode']
|
|
39
62
|
parse?: Adapter<TOptions>['parse']
|
|
40
63
|
getImports?: Adapter<TOptions>['getImports']
|
|
41
64
|
} = {},
|
|
42
65
|
): Adapter<TOptions> {
|
|
43
|
-
const inputNode = options.inputNode ?? null
|
|
44
66
|
return {
|
|
45
67
|
name: (options.name ?? 'oas') as TOptions['name'],
|
|
46
68
|
options: (options.resolvedOptions ?? {}) as TOptions['resolvedOptions'],
|
|
47
|
-
inputNode,
|
|
48
69
|
parse: options.parse ?? (async () => ({ kind: 'Input' as const, schemas: [], operations: [] })),
|
|
49
70
|
getImports: options.getImports ?? ((_node: SchemaNode, _resolve: (schemaName: string) => { name: string; path: string }) => []),
|
|
50
71
|
} as Adapter<TOptions>
|
|
@@ -76,7 +97,8 @@ export function createMockedPlugin<TOptions extends PluginFactoryOptions = Plugi
|
|
|
76
97
|
type RenderGeneratorOptions<TOptions extends PluginFactoryOptions> = {
|
|
77
98
|
config: Config
|
|
78
99
|
adapter: Adapter
|
|
79
|
-
|
|
100
|
+
meta?: InputMeta
|
|
101
|
+
driver: KubbDriver
|
|
80
102
|
plugin: NormalizedPlugin<TOptions>
|
|
81
103
|
options: TOptions['resolvedOptions']
|
|
82
104
|
resolver: TOptions['resolver']
|
|
@@ -88,20 +110,18 @@ function createMockedPluginContext<TOptions extends PluginFactoryOptions>(opts:
|
|
|
88
110
|
return {
|
|
89
111
|
config: opts.config,
|
|
90
112
|
root,
|
|
91
|
-
getMode: (output: { path: string }) => PluginDriver.getMode(resolve(root, output.path)),
|
|
92
113
|
adapter: opts.adapter,
|
|
93
114
|
resolver: opts.resolver,
|
|
94
115
|
plugin: opts.plugin,
|
|
95
116
|
driver: opts.driver,
|
|
96
117
|
getResolver: (name: string) => opts.driver.getResolver(name),
|
|
97
|
-
|
|
118
|
+
meta: opts.meta ?? { circularNames: [], enumNames: [] },
|
|
98
119
|
addFile: async (...files: Array<FileNode>) => opts.driver.fileManager.add(...files),
|
|
99
120
|
upsertFile: async (...files: Array<FileNode>) => opts.driver.fileManager.upsert(...files),
|
|
100
121
|
hooks: opts.driver.hooks ?? ({} as never),
|
|
101
122
|
warn: (msg: string) => console.warn(msg),
|
|
102
123
|
error: (msg: string) => console.error(msg),
|
|
103
124
|
info: (msg: string) => console.info(msg),
|
|
104
|
-
openInStudio: async () => {},
|
|
105
125
|
} as unknown as Omit<GeneratorContext<TOptions>, 'options'>
|
|
106
126
|
}
|
|
107
127
|
|
|
@@ -126,7 +146,7 @@ export async function renderGeneratorSchema<TOptions extends PluginFactoryOption
|
|
|
126
146
|
...context,
|
|
127
147
|
options: opts.options,
|
|
128
148
|
})
|
|
129
|
-
await
|
|
149
|
+
await opts.driver.dispatch({ result, renderer: generator.renderer })
|
|
130
150
|
}
|
|
131
151
|
|
|
132
152
|
/**
|
|
@@ -150,7 +170,7 @@ export async function renderGeneratorOperation<TOptions extends PluginFactoryOpt
|
|
|
150
170
|
...context,
|
|
151
171
|
options: opts.options,
|
|
152
172
|
})
|
|
153
|
-
await
|
|
173
|
+
await opts.driver.dispatch({ result, renderer: generator.renderer })
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
/**
|
|
@@ -174,5 +194,56 @@ export async function renderGeneratorOperations<TOptions extends PluginFactoryOp
|
|
|
174
194
|
...context,
|
|
175
195
|
options: opts.options,
|
|
176
196
|
})
|
|
177
|
-
await
|
|
197
|
+
await opts.driver.dispatch({ result, renderer: generator.renderer })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
type MatchFilesOptions = {
|
|
201
|
+
/**
|
|
202
|
+
* Parsers indexed by file extension, used to render each `FileNode` to source.
|
|
203
|
+
* Without a matching parser the file's raw content is used.
|
|
204
|
+
*/
|
|
205
|
+
parsers?: Map<FileNode['extname'], Parser>
|
|
206
|
+
/**
|
|
207
|
+
* Formatter applied to non-JSON output before snapshotting, e.g. prettier. When
|
|
208
|
+
* omitted the parsed source is snapshotted as-is.
|
|
209
|
+
*/
|
|
210
|
+
format?: (source?: string) => string | Promise<string>
|
|
211
|
+
/**
|
|
212
|
+
* Subfolder under `__snapshots__`, camelCased. Useful to keep variant snapshots apart.
|
|
213
|
+
*/
|
|
214
|
+
pre?: string
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Renders the driver's collected `FileNode`s to source and asserts each against a file snapshot.
|
|
219
|
+
* Pair it with the `renderGenerator*` helpers to snapshot a generator's output.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })
|
|
224
|
+
* await matchFiles(driver.fileManager.files, { parsers, format })
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
export async function matchFiles(files: Array<FileNode> | undefined, options: MatchFilesOptions = {}): Promise<Map<string, string> | undefined> {
|
|
228
|
+
if (!files?.length) return
|
|
229
|
+
|
|
230
|
+
const { parsers = new Map(), format, pre } = options
|
|
231
|
+
const fileProcessor = new FileProcessor({ storage: memoryStorage(), parsers })
|
|
232
|
+
const processed = new Map<string, string>()
|
|
233
|
+
|
|
234
|
+
for (const file of files) {
|
|
235
|
+
if (!file?.path || processed.has(file.path)) {
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const parsed = fileProcessor.parse(file)
|
|
240
|
+
const code = file.baseName.endsWith('.json') || !format ? parsed : await format(parsed)
|
|
241
|
+
|
|
242
|
+
processed.set(file.path, code)
|
|
243
|
+
|
|
244
|
+
const snapshotPath = path.join('__snapshots__', ...(pre ? [camelCase(pre)] : []), file.baseName)
|
|
245
|
+
await expect(code).toMatchFileSnapshot(snapshotPath)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return processed
|
|
178
249
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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 { createReporter, logLevel as logLevelMap } from '../createReporter.ts'
|
|
5
|
+
import { buildReport, type Report } from './report.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds the vitest/jest-style summary for one {@link Report}: right-aligned dim labels with
|
|
9
|
+
* `N passed (total)` counts, and a per-plugin `Timings` section when `showTimings`.
|
|
10
|
+
*/
|
|
11
|
+
function buildSummaryLines(report: Report, { showTimings }: { showTimings: boolean }): Array<string> {
|
|
12
|
+
const { status, plugins, counts, filesCreated, durationMs, output, timings } = report
|
|
13
|
+
|
|
14
|
+
const rows: Array<[label: string, value: string]> = []
|
|
15
|
+
|
|
16
|
+
rows.push([
|
|
17
|
+
'Plugins',
|
|
18
|
+
status === 'success'
|
|
19
|
+
? `${styleText('green', `${plugins.passed} passed`)} (${plugins.total})`
|
|
20
|
+
: `${styleText('green', `${plugins.passed} passed`)} | ${styleText('red', `${plugins.failed.length} failed`)} (${plugins.total})`,
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
if (status === 'failed' && plugins.failed.length > 0) {
|
|
24
|
+
rows.push(['Failed', plugins.failed.map((name) => randomCliColor(name)).join(', ')])
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (counts.errors > 0 || counts.warnings > 0) {
|
|
28
|
+
const issues = [
|
|
29
|
+
counts.errors > 0 ? styleText('red', `${counts.errors} ${counts.errors === 1 ? 'error' : 'errors'}`) : undefined,
|
|
30
|
+
counts.warnings > 0 ? styleText('yellow', `${counts.warnings} ${counts.warnings === 1 ? 'warning' : 'warnings'}`) : undefined,
|
|
31
|
+
]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(' | ')
|
|
34
|
+
rows.push(['Issues', issues])
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
rows.push(['Files', `${styleText('green', String(filesCreated))} generated`])
|
|
38
|
+
rows.push(['Duration', styleText('green', formatMs(durationMs))])
|
|
39
|
+
rows.push(['Output', output])
|
|
40
|
+
|
|
41
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length), timings.length > 0 ? 'Timings'.length : 0)
|
|
42
|
+
const lines = rows.map(([label, value]) => `${styleText('dim', label.padStart(labelWidth))} ${value}`)
|
|
43
|
+
|
|
44
|
+
if (showTimings && timings.length > 0) {
|
|
45
|
+
const nameWidth = Math.max(0, ...timings.map((timing) => timing.plugin.length))
|
|
46
|
+
const indent = ' '.repeat(labelWidth + 2)
|
|
47
|
+
|
|
48
|
+
lines.push(styleText('dim', 'Timings'.padStart(labelWidth)))
|
|
49
|
+
for (const timing of timings) {
|
|
50
|
+
const timeStr = formatMs(timing.durationMs)
|
|
51
|
+
const barLength = Math.min(Math.ceil(timing.durationMs / SUMMARY_TIME_SCALE_DIVISOR), SUMMARY_MAX_BAR_LENGTH)
|
|
52
|
+
const bar = styleText('dim', '█'.repeat(barLength))
|
|
53
|
+
lines.push(`${indent}${styleText('dim', '•')} ${timing.plugin.padEnd(nameWidth)} ${bar} ${timeStr}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return lines
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Renders the summary as plain `console.log` lines so it works in every CLI (no clack/TTY
|
|
62
|
+
* dependency): a blank line, the config name colored by status, then the summary rows.
|
|
63
|
+
*/
|
|
64
|
+
function renderSummary(lines: ReadonlyArray<string>, { title, status }: { title: string; status: 'success' | 'failed' }): void {
|
|
65
|
+
console.log('')
|
|
66
|
+
if (title) {
|
|
67
|
+
console.log(styleText(status === 'failed' ? 'red' : 'green', title))
|
|
68
|
+
}
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
console.log(line)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The default `cli` reporter. Renders the {@link Report} for each config as it finishes, independent
|
|
76
|
+
* of the live logger view. Suppressed at `silent`. The `verbose` level adds the per-plugin timings.
|
|
77
|
+
*/
|
|
78
|
+
export const cliReporter = createReporter({
|
|
79
|
+
name: 'cli',
|
|
80
|
+
report(result, { logLevel }) {
|
|
81
|
+
if (logLevel <= logLevelMap.silent) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const report = buildReport(result)
|
|
86
|
+
const lines = buildSummaryLines(report, { showTimings: logLevel >= logLevelMap.verbose })
|
|
87
|
+
renderSummary(lines, { title: report.name, status: report.status })
|
|
88
|
+
},
|
|
89
|
+
})
|
|
@@ -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
|
+
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
|
+
}
|
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { clean, write } from '@internals/utils'
|
|
1
|
+
import { access, glob, readFile, rm } from 'node:fs/promises'
|
|
2
|
+
import { join, relative, resolve } from 'node:path'
|
|
3
|
+
import { clean, runtime, toPosixPath, write } from '@internals/utils'
|
|
5
4
|
import { createStorage } from '../createStorage.ts'
|
|
6
5
|
|
|
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
6
|
/**
|
|
15
7
|
* Built-in filesystem storage driver.
|
|
16
8
|
*
|
|
@@ -18,11 +10,11 @@ function isMissingPathError(error: unknown): error is NodeJS.ErrnoException {
|
|
|
18
10
|
* Keys are resolved against `process.cwd()`, so root-relative paths such as
|
|
19
11
|
* `src/gen/api/getPets.ts` are written to the correct location without extra configuration.
|
|
20
12
|
*
|
|
21
|
-
*
|
|
22
|
-
* -
|
|
23
|
-
* -
|
|
24
|
-
* -
|
|
25
|
-
* -
|
|
13
|
+
* Writes are deduplicated and directory-safe:
|
|
14
|
+
* - leading and trailing whitespace is trimmed before writing
|
|
15
|
+
* - the write is skipped when the file content is already identical
|
|
16
|
+
* - missing parent directories are created automatically
|
|
17
|
+
* - Bun's native file API is used when running under Bun
|
|
26
18
|
*
|
|
27
19
|
* @example
|
|
28
20
|
* ```ts
|
|
@@ -42,27 +34,15 @@ export const fsStorage = createStorage(() => ({
|
|
|
42
34
|
try {
|
|
43
35
|
await access(resolve(key))
|
|
44
36
|
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
|
-
})
|
|
37
|
+
} catch (_error) {
|
|
38
|
+
return false
|
|
53
39
|
}
|
|
54
40
|
},
|
|
55
41
|
async getItem(key: string) {
|
|
56
42
|
try {
|
|
57
43
|
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
|
-
})
|
|
44
|
+
} catch (_error) {
|
|
45
|
+
return null
|
|
66
46
|
}
|
|
67
47
|
},
|
|
68
48
|
async setItem(key: string, value: string) {
|
|
@@ -72,36 +52,24 @@ export const fsStorage = createStorage(() => ({
|
|
|
72
52
|
await rm(resolve(key), { force: true })
|
|
73
53
|
},
|
|
74
54
|
async getKeys(base?: string) {
|
|
75
|
-
const keys: Array<string> = []
|
|
76
55
|
const resolvedBase = resolve(base ?? process.cwd())
|
|
77
56
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
withFileTypes: true,
|
|
83
|
-
})) as Array<Dirent>
|
|
84
|
-
} catch (error) {
|
|
85
|
-
if (isMissingPathError(error)) {
|
|
86
|
-
return
|
|
87
|
-
}
|
|
57
|
+
if (runtime.isBun) {
|
|
58
|
+
const bunGlob = new Bun.Glob('**/*')
|
|
59
|
+
return Array.fromAsync(bunGlob.scan({ cwd: resolvedBase, onlyFiles: true, dot: true }))
|
|
60
|
+
}
|
|
88
61
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name
|
|
95
|
-
if (entry.isDirectory()) {
|
|
96
|
-
await walk(join(dir, entry.name), rel)
|
|
97
|
-
} else {
|
|
98
|
-
keys.push(rel)
|
|
62
|
+
const keys: Array<string> = []
|
|
63
|
+
try {
|
|
64
|
+
for await (const entry of glob('**/*', { cwd: resolvedBase, withFileTypes: true })) {
|
|
65
|
+
if (entry.isFile()) {
|
|
66
|
+
keys.push(toPosixPath(relative(resolvedBase, join(entry.parentPath, entry.name))))
|
|
99
67
|
}
|
|
100
68
|
}
|
|
69
|
+
} catch (_error) {
|
|
70
|
+
// base directory does not exist yet
|
|
101
71
|
}
|
|
102
72
|
|
|
103
|
-
await walk(resolvedBase, '')
|
|
104
|
-
|
|
105
73
|
return keys
|
|
106
74
|
},
|
|
107
75
|
async clear(base?: string) {
|