@kubb/core 5.0.0-beta.36 → 5.0.0-beta.38
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/{KubbDriver-Dil5m3NF.cjs → KubbDriver-BYBUfOZ8.cjs} +1081 -412
- package/dist/KubbDriver-BYBUfOZ8.cjs.map +1 -0
- package/dist/{KubbDriver-DrG5-FFe.js → KubbDriver-CyNF-NIb.js} +1040 -401
- package/dist/KubbDriver-CyNF-NIb.js.map +1 -0
- package/dist/{createKubb-BKpcUB6g.d.ts → diagnostics-CYfKPtbU.d.ts} +805 -285
- package/dist/index.cjs +66 -89
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +58 -90
- package/dist/index.js.map +1 -1
- package/dist/mocks.cjs +47 -24
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +2 -2
- package/dist/mocks.js +47 -24
- package/dist/mocks.js.map +1 -1
- package/package.json +4 -4
- package/src/FileManager.ts +23 -18
- package/src/FileProcessor.ts +142 -24
- package/src/HookRegistry.ts +45 -0
- package/src/KubbDriver.ts +327 -319
- package/src/Transform.ts +58 -0
- package/src/constants.ts +105 -11
- package/src/createKubb.ts +95 -201
- package/src/createRenderer.ts +2 -2
- package/src/createReporter.ts +117 -0
- package/src/createStorage.ts +1 -1
- package/src/defineGenerator.ts +11 -7
- package/src/defineParser.ts +1 -1
- package/src/definePlugin.ts +10 -22
- package/src/defineResolver.ts +29 -24
- package/src/devtools.ts +2 -3
- package/src/diagnostics.ts +591 -0
- package/src/index.ts +11 -1
- package/src/mocks.ts +28 -7
- package/src/types.ts +16 -4
- package/dist/KubbDriver-Dil5m3NF.cjs.map +0 -1
- package/dist/KubbDriver-DrG5-FFe.js.map +0 -1
package/src/KubbDriver.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
|
-
import { arrayToAsyncIterable, type AsyncEventEmitter, forBatches,
|
|
3
|
-
import { collectUsedSchemaNames, createFile, createStreamInput
|
|
2
|
+
import { arrayToAsyncIterable, type AsyncEventEmitter, forBatches, getElapsedMs, isPromise, memoize, URLPath } from '@internals/utils'
|
|
3
|
+
import { collectUsedSchemaNames, createFile, createStreamInput } from '@kubb/ast'
|
|
4
4
|
import type { FileNode, InputMeta, InputNode, InputStreamNode, OperationNode, SchemaNode } from '@kubb/ast'
|
|
5
|
-
import { DEFAULT_STUDIO_URL,
|
|
5
|
+
import { DEFAULT_STUDIO_URL, diagnosticCode, OPERATION_FILTER_TYPES, SCHEMA_PARALLEL } from './constants.ts'
|
|
6
|
+
import { type Diagnostic, DiagnosticError, Diagnostics, type ProblemDiagnostic } from './diagnostics.ts'
|
|
7
|
+
import type { RendererFactory } from './createRenderer.ts'
|
|
6
8
|
import type { Storage } from './createStorage.ts'
|
|
7
9
|
import type { Generator } from './defineGenerator.ts'
|
|
8
10
|
import type { Parser } from './defineParser.ts'
|
|
@@ -12,7 +14,8 @@ import { defineResolver } from './defineResolver.ts'
|
|
|
12
14
|
import { openInStudio as openInStudioFn } from './devtools.ts'
|
|
13
15
|
import { FileManager } from './FileManager.ts'
|
|
14
16
|
import { FileProcessor } from './FileProcessor.ts'
|
|
15
|
-
import type
|
|
17
|
+
import { type HookListener, HookRegistry } from './HookRegistry.ts'
|
|
18
|
+
import { Transform } from './Transform.ts'
|
|
16
19
|
|
|
17
20
|
import type {
|
|
18
21
|
Adapter,
|
|
@@ -36,8 +39,6 @@ function enforceOrder(enforce: 'pre' | 'post' | undefined): number {
|
|
|
36
39
|
return enforce === 'pre' ? -1 : enforce === 'post' ? 1 : 0
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
const OPERATION_FILTER_TYPES = new Set(['tag', 'operationId', 'path', 'method', 'contentType'])
|
|
40
|
-
|
|
41
42
|
export class KubbDriver {
|
|
42
43
|
readonly config: Config
|
|
43
44
|
readonly options: Options
|
|
@@ -57,7 +58,7 @@ export class KubbDriver {
|
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
60
|
* The streaming `InputStreamNode` produced by the adapter.
|
|
60
|
-
* Always set after adapter setup
|
|
61
|
+
* Always set after adapter setup, parse-only adapters are wrapped automatically.
|
|
61
62
|
*/
|
|
62
63
|
inputNode: InputStreamNode | null = null
|
|
63
64
|
adapter: Adapter | null = null
|
|
@@ -65,7 +66,7 @@ export class KubbDriver {
|
|
|
65
66
|
* Studio session state, kept together so `dispose()` can reset it atomically.
|
|
66
67
|
*
|
|
67
68
|
* - `source` holds the raw adapter source so `adapter.parse()` can be called lazily.
|
|
68
|
-
* Intentionally outlives the build
|
|
69
|
+
* Intentionally outlives the build, cleared by `dispose()`.
|
|
69
70
|
* - `isOpen` prevents opening the studio more than once per build.
|
|
70
71
|
* - `inputNode` caches the parse promise so `adapter.parse()` is called at most once
|
|
71
72
|
* per studio session, even when `openInStudio()` is called multiple times.
|
|
@@ -76,21 +77,12 @@ export class KubbDriver {
|
|
|
76
77
|
inputNode: null,
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
// Register middleware hooks after all plugin hooks are registered.
|
|
80
|
-
// Because AsyncEventEmitter calls listeners in registration order,
|
|
81
|
-
// middleware hooks for any event fire after all plugin hooks for that event.
|
|
82
|
-
// Handlers are tracked so they can be removed after each build (disposeMiddleware),
|
|
83
|
-
// preventing accumulation when multiple configs share the same hooks instance.
|
|
84
|
-
#middlewareListeners: Array<[keyof KubbHooks & string, (...args: Array<never>) => void | Promise<void>]> = []
|
|
85
|
-
|
|
86
80
|
/**
|
|
87
81
|
* Central file store for all generated files.
|
|
88
82
|
* Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
|
|
89
|
-
* add files
|
|
83
|
+
* add files. This property gives direct read/write access when needed.
|
|
90
84
|
*/
|
|
91
85
|
readonly fileManager = new FileManager()
|
|
92
|
-
readonly #fileProcessor = new FileProcessor()
|
|
93
|
-
|
|
94
86
|
readonly plugins = new Map<string, NormalizedPlugin>()
|
|
95
87
|
|
|
96
88
|
/**
|
|
@@ -100,12 +92,25 @@ export class KubbDriver {
|
|
|
100
92
|
readonly #eventGeneratorPlugins = new Set<string>()
|
|
101
93
|
readonly #resolvers = new Map<string, Resolver>()
|
|
102
94
|
readonly #defaultResolvers = new Map<string, Resolver>()
|
|
103
|
-
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Tracks every listener the driver added (plugin, middleware, generator) so `dispose()` can
|
|
98
|
+
* remove them in one pass. Middleware registers after plugins, so it fires last via `Set`
|
|
99
|
+
* insertion order. External `hooks.on(...)` listeners are not tracked.
|
|
100
|
+
*/
|
|
101
|
+
readonly #registry: HookRegistry<KubbHooks>
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Transform registry. Plugins populate it during `kubb:plugin:setup` via `setTransformer`,
|
|
105
|
+
* and `#runGenerators` reads it once per `(plugin, node)` pair through `applyTo`.
|
|
106
|
+
*/
|
|
107
|
+
readonly #transforms = new Transform()
|
|
104
108
|
|
|
105
109
|
constructor(config: Config, options: Options) {
|
|
106
110
|
this.config = config
|
|
107
111
|
this.options = options
|
|
108
112
|
this.adapter = config.adapter ?? null
|
|
113
|
+
this.#registry = new HookRegistry({ emitter: options.hooks })
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
async setup() {
|
|
@@ -135,7 +140,7 @@ export class KubbDriver {
|
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
142
|
if (this.config.adapter) {
|
|
138
|
-
|
|
143
|
+
this.#studio.source = inputToAdapterSource(this.config)
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
|
|
@@ -163,32 +168,25 @@ export class KubbDriver {
|
|
|
163
168
|
return normalized
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Parses the adapter source into `this.inputNode`. Idempotent, so repeated calls from
|
|
173
|
+
* `run` or the studio path do not re-parse. Adapters with `stream()` are used directly.
|
|
174
|
+
* Adapters with only `parse()` are wrapped via `createStreamInput` so the dispatch loop
|
|
175
|
+
* stays stream-only.
|
|
176
|
+
*/
|
|
177
|
+
async #parseInput(): Promise<void> {
|
|
178
|
+
if (this.inputNode || !this.adapter || !this.#studio.source) return
|
|
179
|
+
|
|
180
|
+
const adapter = this.adapter
|
|
181
|
+
const source = this.#studio.source
|
|
169
182
|
|
|
170
183
|
if (adapter.stream) {
|
|
171
184
|
this.inputNode = await adapter.stream(source)
|
|
172
|
-
|
|
173
|
-
await this.hooks.emit('kubb:debug', {
|
|
174
|
-
date: new Date(),
|
|
175
|
-
logs: [`✓ Adapter '${adapter.name}' producing input stream`],
|
|
176
|
-
})
|
|
177
|
-
} else {
|
|
178
|
-
// Adapter does not implement stream() — eagerly parse and wrap in a
|
|
179
|
-
// reusable AsyncIterable so the rest of the pipeline stays stream-only.
|
|
180
|
-
const inputNode = await adapter.parse(source)
|
|
181
|
-
this.inputNode = createStreamInput(arrayToAsyncIterable(inputNode.schemas), arrayToAsyncIterable(inputNode.operations), inputNode.meta)
|
|
182
|
-
|
|
183
|
-
await this.hooks.emit('kubb:debug', {
|
|
184
|
-
date: new Date(),
|
|
185
|
-
logs: [
|
|
186
|
-
`✓ Adapter '${adapter.name}' resolved InputNode (wrapped as stream)`,
|
|
187
|
-
` • Schemas: ${inputNode.schemas.length}`,
|
|
188
|
-
` • Operations: ${inputNode.operations.length}`,
|
|
189
|
-
],
|
|
190
|
-
})
|
|
185
|
+
return
|
|
191
186
|
}
|
|
187
|
+
|
|
188
|
+
const parsed = await adapter.parse(source)
|
|
189
|
+
this.inputNode = createStreamInput(arrayToAsyncIterable(parsed.schemas), arrayToAsyncIterable(parsed.operations), parsed.meta)
|
|
192
190
|
}
|
|
193
191
|
|
|
194
192
|
#registerMiddleware<K extends keyof KubbHooks & string>(event: K, middlewareHooks: Middleware['hooks']) {
|
|
@@ -198,22 +196,16 @@ export class KubbDriver {
|
|
|
198
196
|
return
|
|
199
197
|
}
|
|
200
198
|
|
|
201
|
-
this.
|
|
202
|
-
this.#middlewareListeners.push([event, handler as (...args: Array<never>) => void | Promise<void>])
|
|
199
|
+
this.#registry.register({ event, handler, source: 'middleware' })
|
|
203
200
|
}
|
|
204
201
|
|
|
205
202
|
/**
|
|
206
203
|
* Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.
|
|
207
204
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* `
|
|
211
|
-
*
|
|
212
|
-
* All other hooks are iterated and registered directly as pass-through listeners.
|
|
213
|
-
* Any event key present in the global `KubbHooks` interface can be subscribed to.
|
|
214
|
-
*
|
|
215
|
-
* External tooling can subscribe to any of these events via `hooks.on(...)` to observe
|
|
216
|
-
* the plugin lifecycle without modifying plugin behavior.
|
|
205
|
+
* The `kubb:plugin:setup` listener wraps the global context in a plugin-specific one so
|
|
206
|
+
* `addGenerator`, `setResolver`, and `setTransformer` target the right `normalizedPlugin`.
|
|
207
|
+
* Every other `KubbHooks` event registers as a pass-through listener that external tooling
|
|
208
|
+
* can observe via `hooks.on(...)`.
|
|
217
209
|
*
|
|
218
210
|
* @internal
|
|
219
211
|
*/
|
|
@@ -237,10 +229,7 @@ export class KubbDriver {
|
|
|
237
229
|
this.setPluginResolver(plugin.name, resolver)
|
|
238
230
|
},
|
|
239
231
|
setTransformer: (visitor) => {
|
|
240
|
-
plugin.
|
|
241
|
-
},
|
|
242
|
-
setRenderer: (renderer) => {
|
|
243
|
-
plugin.renderer = renderer
|
|
232
|
+
this.#transforms.register(plugin.name, visitor)
|
|
244
233
|
},
|
|
245
234
|
setOptions: (opts) => {
|
|
246
235
|
plugin.options = { ...plugin.options, ...opts }
|
|
@@ -252,16 +241,20 @@ export class KubbDriver {
|
|
|
252
241
|
return hooks['kubb:plugin:setup']!(pluginCtx)
|
|
253
242
|
}
|
|
254
243
|
|
|
255
|
-
this.
|
|
256
|
-
this.#trackHookListener('kubb:plugin:setup', setupHandler as (...args: Array<never>) => void | Promise<void>)
|
|
244
|
+
this.#registry.register({ event: 'kubb:plugin:setup', handler: setupHandler, source: 'plugin' })
|
|
257
245
|
}
|
|
258
246
|
|
|
259
247
|
// All other hooks are registered as direct pass-through listeners on the shared emitter.
|
|
260
|
-
for (const
|
|
261
|
-
if (event === 'kubb:plugin:setup'
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
248
|
+
for (const event of Object.keys(hooks) as Array<keyof KubbHooks & string>) {
|
|
249
|
+
if (event === 'kubb:plugin:setup') continue
|
|
250
|
+
const handler = hooks[event]
|
|
251
|
+
if (!handler) continue
|
|
252
|
+
|
|
253
|
+
this.#registry.register({
|
|
254
|
+
event,
|
|
255
|
+
handler: handler as HookListener<KubbHooks[typeof event], unknown>,
|
|
256
|
+
source: 'plugin',
|
|
257
|
+
})
|
|
265
258
|
}
|
|
266
259
|
}
|
|
267
260
|
|
|
@@ -280,7 +273,6 @@ export class KubbDriver {
|
|
|
280
273
|
addGenerator: noop,
|
|
281
274
|
setResolver: noop,
|
|
282
275
|
setTransformer: noop,
|
|
283
|
-
setRenderer: noop,
|
|
284
276
|
setOptions: noop,
|
|
285
277
|
injectFile: noop,
|
|
286
278
|
updateConfig: noop,
|
|
@@ -295,49 +287,42 @@ export class KubbDriver {
|
|
|
295
287
|
* respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check
|
|
296
288
|
* so that generators from different plugins do not cross-fire.
|
|
297
289
|
*
|
|
298
|
-
* The renderer
|
|
299
|
-
*
|
|
300
|
-
* declares a renderer.
|
|
290
|
+
* The renderer comes from `generator.renderer`. Set `generator.renderer = null` (or leave it
|
|
291
|
+
* unset) to opt out of rendering.
|
|
301
292
|
*
|
|
302
293
|
* Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
|
|
303
294
|
*/
|
|
304
|
-
registerGenerator(pluginName: string,
|
|
305
|
-
|
|
306
|
-
const plugin = this.plugins.get(pluginName)
|
|
307
|
-
return gen.renderer === null ? undefined : (gen.renderer ?? plugin?.renderer ?? this.config.renderer)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (gen.schema) {
|
|
295
|
+
registerGenerator(pluginName: string, generator: Generator): void {
|
|
296
|
+
if (generator.schema) {
|
|
311
297
|
const schemaHandler = async (node: SchemaNode, ctx: GeneratorContext) => {
|
|
312
298
|
if (ctx.plugin.name !== pluginName) return
|
|
313
|
-
const result = await
|
|
314
|
-
|
|
299
|
+
const result = await generator.schema!(node, ctx)
|
|
300
|
+
|
|
301
|
+
await this.dispatch({ result, renderer: generator.renderer })
|
|
315
302
|
}
|
|
316
303
|
|
|
317
|
-
this.
|
|
318
|
-
this.#trackHookListener('kubb:generate:schema', schemaHandler as (...args: Array<never>) => void | Promise<void>)
|
|
304
|
+
this.#registry.register({ event: 'kubb:generate:schema', handler: schemaHandler, source: 'driver' })
|
|
319
305
|
}
|
|
320
306
|
|
|
321
|
-
if (
|
|
307
|
+
if (generator.operation) {
|
|
322
308
|
const operationHandler = async (node: OperationNode, ctx: GeneratorContext) => {
|
|
323
309
|
if (ctx.plugin.name !== pluginName) return
|
|
324
|
-
|
|
325
|
-
|
|
310
|
+
|
|
311
|
+
const result = await generator.operation!(node, ctx)
|
|
312
|
+
await this.dispatch({ result, renderer: generator.renderer })
|
|
326
313
|
}
|
|
327
314
|
|
|
328
|
-
this.
|
|
329
|
-
this.#trackHookListener('kubb:generate:operation', operationHandler as (...args: Array<never>) => void | Promise<void>)
|
|
315
|
+
this.#registry.register({ event: 'kubb:generate:operation', handler: operationHandler, source: 'driver' })
|
|
330
316
|
}
|
|
331
317
|
|
|
332
|
-
if (
|
|
318
|
+
if (generator.operations) {
|
|
333
319
|
const operationsHandler = async (nodes: Array<OperationNode>, ctx: GeneratorContext) => {
|
|
334
320
|
if (ctx.plugin.name !== pluginName) return
|
|
335
|
-
const result = await
|
|
336
|
-
await
|
|
321
|
+
const result = await generator.operations!(nodes, ctx)
|
|
322
|
+
await this.dispatch({ result, renderer: generator.renderer })
|
|
337
323
|
}
|
|
338
324
|
|
|
339
|
-
this.
|
|
340
|
-
this.#trackHookListener('kubb:generate:operations', operationsHandler as (...args: Array<never>) => void | Promise<void>)
|
|
325
|
+
this.#registry.register({ event: 'kubb:generate:operations', handler: operationsHandler, source: 'driver' })
|
|
341
326
|
}
|
|
342
327
|
|
|
343
328
|
this.#eventGeneratorPlugins.add(pluginName)
|
|
@@ -355,19 +340,14 @@ export class KubbDriver {
|
|
|
355
340
|
}
|
|
356
341
|
|
|
357
342
|
/**
|
|
358
|
-
* Runs the full plugin pipeline. Returns
|
|
359
|
-
* when an outer hook throws
|
|
360
|
-
* the
|
|
343
|
+
* Runs the full plugin pipeline. Returns the diagnostics collected so far even
|
|
344
|
+
* when an outer hook throws, since the orchestrator preserves partial state by capturing
|
|
345
|
+
* the failure as a {@link Diagnostic} instead of propagating. Each plugin also
|
|
346
|
+
* contributes a `timing` diagnostic for the run summary.
|
|
361
347
|
*/
|
|
362
|
-
async run({ storage }: { storage: Storage }): Promise<{
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
error?: Error
|
|
366
|
-
}> {
|
|
367
|
-
const hooks = this.hooks
|
|
368
|
-
const config = this.config
|
|
369
|
-
const failedPlugins = new Set<{ plugin: Plugin; error: Error }>()
|
|
370
|
-
const pluginTimings = new Map<string, number>()
|
|
348
|
+
async run({ storage }: { storage: Storage }): Promise<{ diagnostics: Array<Diagnostic> }> {
|
|
349
|
+
const { hooks, config } = this
|
|
350
|
+
const diagnostics: Array<Diagnostic> = []
|
|
371
351
|
const parsersMap = new Map<FileNode['extname'], Parser>()
|
|
372
352
|
|
|
373
353
|
for (const parser of config.parsers) {
|
|
@@ -376,116 +356,111 @@ export class KubbDriver {
|
|
|
376
356
|
}
|
|
377
357
|
}
|
|
378
358
|
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
359
|
+
const processor = new FileProcessor({ parsers: parsersMap, storage, extension: config.output.extension })
|
|
360
|
+
// Bridge processor lifecycle to the user-facing kubb hooks so existing listeners on
|
|
361
|
+
// kubb:files:processing:* keep firing.
|
|
362
|
+
processor.hooks.on('start', async (files) => {
|
|
363
|
+
await hooks.emit('kubb:files:processing:start', { files })
|
|
382
364
|
})
|
|
365
|
+
const updateBuffer: Array<{ file: FileNode; source?: string; processed: number; total: number; percentage: number }> = []
|
|
366
|
+
processor.hooks.on('update', (item) => {
|
|
367
|
+
updateBuffer.push(item)
|
|
368
|
+
})
|
|
369
|
+
processor.hooks.on('end', async (files) => {
|
|
370
|
+
await hooks.emit('kubb:files:processing:update', {
|
|
371
|
+
files: updateBuffer.map((item) => ({ ...item, config })),
|
|
372
|
+
})
|
|
373
|
+
updateBuffer.length = 0
|
|
374
|
+
await hooks.emit('kubb:files:processing:end', { files })
|
|
375
|
+
})
|
|
376
|
+
const onFileUpsert = (file: FileNode): void => {
|
|
377
|
+
processor.enqueue(file)
|
|
378
|
+
}
|
|
379
|
+
this.fileManager.hooks.on('upsert', onFileUpsert)
|
|
383
380
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
queue.push(storage.setItem(file.path, source))
|
|
403
|
-
if (queue.length >= STREAM_FLUSH_EVERY) await Promise.all(queue.splice(0))
|
|
381
|
+
// Make `diagnostics` the active sink so deep code (adapter parse, lazily consumed
|
|
382
|
+
// streams, generators) can report into this run via `Diagnostics.report`.
|
|
383
|
+
return Diagnostics.scope(
|
|
384
|
+
(diagnostic) => diagnostics.push(diagnostic),
|
|
385
|
+
async () => {
|
|
386
|
+
try {
|
|
387
|
+
// Parse the adapter source into the streaming `InputNode`.
|
|
388
|
+
await this.#parseInput()
|
|
389
|
+
// Emit `kubb:plugin:setup` so plugins can register transformers via `setTransformer`.
|
|
390
|
+
// Each call writes into `this.#transforms`, which `#runGenerators` later reads through
|
|
391
|
+
// `transforms.applyTo`.
|
|
392
|
+
await this.emitSetupHooks()
|
|
393
|
+
|
|
394
|
+
if (this.adapter && this.inputNode) {
|
|
395
|
+
await hooks.emit(
|
|
396
|
+
'kubb:build:start',
|
|
397
|
+
Object.assign({ config, adapter: this.adapter, meta: this.inputNode.meta, getPlugin: this.getPlugin.bind(this) }, this.#filesPayload()),
|
|
398
|
+
)
|
|
404
399
|
}
|
|
405
|
-
}
|
|
406
|
-
await Promise.all(queue)
|
|
407
400
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
401
|
+
const generatorPlugins: Array<{ plugin: NormalizedPlugin; context: Omit<GeneratorContext, 'options'>; hrStart: ReturnType<typeof process.hrtime> }> =
|
|
402
|
+
[]
|
|
411
403
|
|
|
412
|
-
|
|
404
|
+
for (const plugin of this.plugins.values()) {
|
|
405
|
+
const context = this.getContext(plugin)
|
|
406
|
+
const hrStart = process.hrtime()
|
|
413
407
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
408
|
+
try {
|
|
409
|
+
await hooks.emit('kubb:plugin:start', { plugin })
|
|
410
|
+
} catch (caughtError) {
|
|
411
|
+
const error = caughtError as Error
|
|
412
|
+
const duration = getElapsedMs(hrStart)
|
|
420
413
|
|
|
421
|
-
|
|
414
|
+
await this.#emitPluginEnd({ plugin, duration, success: false, error })
|
|
422
415
|
|
|
423
|
-
|
|
424
|
-
const context = this.getContext(plugin)
|
|
425
|
-
const hrStart = process.hrtime()
|
|
416
|
+
diagnostics.push({ ...Diagnostics.from(error), plugin: plugin.name }, Diagnostics.performance({ plugin: plugin.name, duration }))
|
|
426
417
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
await hooks.emit('kubb:debug', { date: new Date(), logs: ['Starting plugin...', ` • Plugin Name: ${plugin.name}`] })
|
|
430
|
-
} catch (caughtError) {
|
|
431
|
-
const error = caughtError as Error
|
|
432
|
-
const duration = getElapsedMs(hrStart)
|
|
433
|
-
pluginTimings.set(plugin.name, duration)
|
|
434
|
-
await this.#emitPluginEnd({ plugin, duration, success: false, error })
|
|
435
|
-
failedPlugins.add({ plugin, error })
|
|
436
|
-
continue
|
|
437
|
-
}
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
438
420
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
continue
|
|
442
|
-
}
|
|
421
|
+
if (this.hasEventGenerators(plugin.name)) {
|
|
422
|
+
generatorPlugins.push({ plugin, context, hrStart })
|
|
443
423
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
await this.#emitPluginEnd({ plugin, duration, success: true })
|
|
447
|
-
await hooks.emit('kubb:debug', { date: new Date(), logs: [`✓ Plugin started successfully (${formatMs(duration)})`] })
|
|
448
|
-
}
|
|
424
|
+
continue
|
|
425
|
+
}
|
|
449
426
|
|
|
450
|
-
if (generatorPlugins.length > 0) {
|
|
451
|
-
if (this.inputNode) {
|
|
452
|
-
const { timings, failed } = await this.#runGenerators(generatorPlugins, flushPending)
|
|
453
|
-
// Drain any files written after the last batch's flush.
|
|
454
|
-
await flushPending()
|
|
455
|
-
for (const [name, duration] of timings) pluginTimings.set(name, duration)
|
|
456
|
-
for (const entry of failed) failedPlugins.add(entry)
|
|
457
|
-
} else {
|
|
458
|
-
// No adapter input: generator-plugins have nothing to dispatch, but still
|
|
459
|
-
// need their `kubb:plugin:end` so middleware (e.g. barrel) completes.
|
|
460
|
-
for (const { plugin, hrStart } of generatorPlugins) {
|
|
461
427
|
const duration = getElapsedMs(hrStart)
|
|
462
|
-
|
|
428
|
+
diagnostics.push(Diagnostics.performance({ plugin: plugin.name, duration }))
|
|
429
|
+
|
|
463
430
|
await this.#emitPluginEnd({ plugin, duration, success: true })
|
|
464
431
|
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
432
|
|
|
468
|
-
|
|
433
|
+
// Stream every node through the transform registry and into each plugin's generators.
|
|
434
|
+
// Handles the empty-entries and missing-`inputNode` cases by closing out each entry's
|
|
435
|
+
// `kubb:plugin:end` directly.
|
|
436
|
+
diagnostics.push(...(await this.#runGenerators(generatorPlugins, () => processor.flush())))
|
|
437
|
+
// Wait for the last in-flight batch and write anything still pending.
|
|
438
|
+
await processor.drain()
|
|
469
439
|
|
|
470
|
-
|
|
440
|
+
await hooks.emit('kubb:plugins:end', Object.assign({ config }, this.#filesPayload()))
|
|
471
441
|
|
|
472
|
-
|
|
442
|
+
// Plugins-end listeners (barrel middleware etc.) may have queued more files.
|
|
443
|
+
await processor.drain()
|
|
473
444
|
|
|
474
|
-
|
|
445
|
+
await hooks.emit('kubb:build:end', { files: this.fileManager.files, config, outputDir: resolve(config.root, config.output.path) })
|
|
475
446
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
447
|
+
return { diagnostics: Diagnostics.dedupe(diagnostics) }
|
|
448
|
+
} catch (caughtError) {
|
|
449
|
+
diagnostics.push(Diagnostics.from(caughtError))
|
|
450
|
+
return { diagnostics: Diagnostics.dedupe(diagnostics) }
|
|
451
|
+
} finally {
|
|
452
|
+
this.fileManager.hooks.off('upsert', onFileUpsert)
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
)
|
|
482
456
|
}
|
|
483
457
|
|
|
484
458
|
// Returns a fresh object with a lazy `files` getter and a bound `upsertFile`.
|
|
485
|
-
// Caller must use `Object.assign(extra, this.#filesPayload())`, not object spread
|
|
486
|
-
//
|
|
459
|
+
// Caller must use `Object.assign(extra, this.#filesPayload())`, not object spread.
|
|
460
|
+
// Spread would eagerly invoke the getter and freeze a stale snapshot into the payload.
|
|
487
461
|
#filesPayload(): { readonly files: Array<FileNode>; upsertFile: (...files: Array<FileNode>) => Array<FileNode> } {
|
|
488
462
|
const driver = this
|
|
463
|
+
|
|
489
464
|
return {
|
|
490
465
|
get files() {
|
|
491
466
|
return driver.fileManager.files
|
|
@@ -501,12 +476,37 @@ export class KubbDriver {
|
|
|
501
476
|
)
|
|
502
477
|
}
|
|
503
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Streams schemas and operations through every plugin's generators. Each node is run
|
|
481
|
+
* through the plugin's transformer (from `this.#transforms`) before the generator sees it,
|
|
482
|
+
* so plugins stay isolated and the hot path stays per-node. Schemas run before operations
|
|
483
|
+
* because the two passes share `flushPending` and the FileProcessor's event emitter.
|
|
484
|
+
* A failing plugin contributes an error diagnostic so the rest of the build continues.
|
|
485
|
+
* Every plugin also contributes a `timing` diagnostic.
|
|
486
|
+
*
|
|
487
|
+
* When `entries` is empty or `this.inputNode` is `null`, every entry still gets a
|
|
488
|
+
* `kubb:plugin:end` so middleware listeners (the barrel writer and friends) complete.
|
|
489
|
+
*/
|
|
504
490
|
async #runGenerators(
|
|
505
491
|
entries: Array<{ plugin: NormalizedPlugin; context: Omit<GeneratorContext, 'options'>; hrStart: ReturnType<typeof process.hrtime> }>,
|
|
506
492
|
flushPending: () => Promise<void>,
|
|
507
|
-
): Promise<
|
|
508
|
-
const
|
|
509
|
-
|
|
493
|
+
): Promise<Array<Diagnostic>> {
|
|
494
|
+
const diagnostics: Array<Diagnostic> = []
|
|
495
|
+
|
|
496
|
+
if (entries.length === 0) return diagnostics
|
|
497
|
+
|
|
498
|
+
if (!this.inputNode) {
|
|
499
|
+
for (const { plugin, hrStart } of entries) {
|
|
500
|
+
const duration = getElapsedMs(hrStart)
|
|
501
|
+
diagnostics.push(Diagnostics.performance({ plugin: plugin.name, duration }))
|
|
502
|
+
await this.#emitPluginEnd({ plugin, duration, success: true })
|
|
503
|
+
}
|
|
504
|
+
return diagnostics
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const transforms = this.#transforms
|
|
508
|
+
const { schemas, operations } = this.inputNode
|
|
509
|
+
|
|
510
510
|
type PluginState = {
|
|
511
511
|
plugin: NormalizedPlugin
|
|
512
512
|
generatorContext: Omit<GeneratorContext, 'options'>
|
|
@@ -518,8 +518,6 @@ export class KubbDriver {
|
|
|
518
518
|
allowedSchemaNames: Set<string> | null
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
-
const driver = this
|
|
522
|
-
const { schemas, operations } = this.inputNode!
|
|
523
521
|
const states: Array<PluginState> = entries.map(({ plugin, context, hrStart }) => {
|
|
524
522
|
const { exclude, include, override } = plugin.options
|
|
525
523
|
const hasExclude = Array.isArray(exclude) && exclude.length > 0
|
|
@@ -541,9 +539,9 @@ export class KubbDriver {
|
|
|
541
539
|
const emitsOperationHook = this.hooks.listenerCount('kubb:generate:operation') > 0
|
|
542
540
|
|
|
543
541
|
// Pre-scan: plugins with operation-based includes (but no schemaName include) need
|
|
544
|
-
// the reachable schema set. This requires the full schema graph in memory at once
|
|
545
|
-
// transitive reachability can't be derived from a single node. `allSchemas` is
|
|
546
|
-
// released as soon as the pre-scan returns
|
|
542
|
+
// the reachable schema set. This requires the full schema graph in memory at once,
|
|
543
|
+
// since transitive reachability can't be derived from a single node. `allSchemas` is
|
|
544
|
+
// released as soon as the pre-scan returns, so the main passes get fresh iterators.
|
|
547
545
|
const pruningStates = states.filter(({ plugin }) => {
|
|
548
546
|
const { include } = plugin.options
|
|
549
547
|
return (include?.some(({ type }) => OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === 'schemaName') ?? false)
|
|
@@ -553,7 +551,7 @@ export class KubbDriver {
|
|
|
553
551
|
const allSchemas: Array<SchemaNode> = []
|
|
554
552
|
for await (const schema of schemas) allSchemas.push(schema)
|
|
555
553
|
|
|
556
|
-
const includedOpsByState = new Map<PluginState, Array<OperationNode>>(pruningStates.map((
|
|
554
|
+
const includedOpsByState = new Map<PluginState, Array<OperationNode>>(pruningStates.map((state) => [state, []]))
|
|
557
555
|
for await (const operation of operations) {
|
|
558
556
|
for (const state of pruningStates) {
|
|
559
557
|
const { exclude, include, override } = state.plugin.options
|
|
@@ -568,12 +566,27 @@ export class KubbDriver {
|
|
|
568
566
|
}
|
|
569
567
|
}
|
|
570
568
|
|
|
571
|
-
|
|
572
|
-
|
|
569
|
+
// Apply the plugin's transformer, then resolve options (skipping the resolver when
|
|
570
|
+
// optionsAreStatic). Returns null when include/exclude/override rules out the node.
|
|
571
|
+
// The per-node dispatch and the collected-operations tail both go through this so
|
|
572
|
+
// they agree on what a plugin sees.
|
|
573
|
+
const resolveForPlugin = <TNode extends SchemaNode | OperationNode>(
|
|
574
|
+
state: PluginState,
|
|
575
|
+
node: TNode,
|
|
576
|
+
): { transformedNode: TNode; options: NormalizedPlugin['options'] } | null => {
|
|
577
|
+
const { plugin, generatorContext } = state
|
|
578
|
+
const transformedNode = transforms.applyTo(plugin.name, node)
|
|
579
|
+
if (state.optionsAreStatic) return { transformedNode, options: plugin.options }
|
|
580
|
+
|
|
581
|
+
const { exclude, include, override } = plugin.options
|
|
582
|
+
const options = generatorContext.resolver.resolveOptions(transformedNode, { options: plugin.options, exclude, include, override })
|
|
583
|
+
if (options === null) return null
|
|
584
|
+
return { transformedNode, options }
|
|
585
|
+
}
|
|
573
586
|
|
|
574
|
-
// Schema and operation passes share this body. They differ only in which
|
|
575
|
-
//
|
|
576
|
-
//
|
|
587
|
+
// Schema and operation passes share this body. They differ only in which generator
|
|
588
|
+
// method runs, which hook is emitted, and the schema-only `allowedSchemaNames` prune
|
|
589
|
+
// (operations don't carry that constraint).
|
|
577
590
|
const dispatchNode = async <TNode extends SchemaNode | OperationNode>(
|
|
578
591
|
state: PluginState,
|
|
579
592
|
node: TNode,
|
|
@@ -585,9 +598,10 @@ export class KubbDriver {
|
|
|
585
598
|
): Promise<void> => {
|
|
586
599
|
if (state.failed) return
|
|
587
600
|
try {
|
|
588
|
-
const
|
|
589
|
-
|
|
601
|
+
const resolved = resolveForPlugin(state, node)
|
|
602
|
+
if (!resolved) return
|
|
590
603
|
|
|
604
|
+
const { transformedNode, options } = resolved
|
|
591
605
|
if (
|
|
592
606
|
dispatch.checkAllowedNames &&
|
|
593
607
|
state.allowedSchemaNames !== null &&
|
|
@@ -598,19 +612,13 @@ export class KubbDriver {
|
|
|
598
612
|
return
|
|
599
613
|
}
|
|
600
614
|
|
|
601
|
-
const {
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const ctx = { ...generatorContext, options }
|
|
608
|
-
for (const gen of generators) {
|
|
609
|
-
const generate = gen[dispatch.method] as ((node: TNode, ctx: GeneratorContext) => unknown) | undefined
|
|
610
|
-
if (!generate) continue
|
|
611
|
-
const raw = generate(transformedNode, ctx)
|
|
615
|
+
const ctx = { ...state.generatorContext, options }
|
|
616
|
+
for (const gen of state.generators) {
|
|
617
|
+
const run = gen[dispatch.method] as ((node: TNode, ctx: GeneratorContext) => unknown) | undefined
|
|
618
|
+
if (!run) continue
|
|
619
|
+
const raw = run(transformedNode, ctx)
|
|
612
620
|
const result = isPromise(raw) ? await raw : raw
|
|
613
|
-
const applied =
|
|
621
|
+
const applied = this.dispatch({ result, renderer: gen.renderer })
|
|
614
622
|
if (isPromise(applied)) await applied
|
|
615
623
|
}
|
|
616
624
|
if (dispatch.emit) await dispatch.emit(transformedNode, ctx)
|
|
@@ -631,16 +639,16 @@ export class KubbDriver {
|
|
|
631
639
|
emit: emitsOperationHook ? (node: OperationNode, ctx: GeneratorContext) => this.hooks.emit('kubb:generate:operation', node, ctx) : null,
|
|
632
640
|
} as const
|
|
633
641
|
|
|
634
|
-
// Skip building the aggregated operations array when nothing consumes it.
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
|
|
642
|
+
// Skip building the aggregated operations array when nothing consumes it. Saves an
|
|
643
|
+
// N-sized allocation on the common path where plugins only define per-node `gen.operation`.
|
|
644
|
+
const needsCollectedOperations =
|
|
645
|
+
this.hooks.listenerCount('kubb:generate:operations') > 0 || states.some((state) => state.generators.some((gen) => !!gen.operations))
|
|
638
646
|
const collectedOperations: Array<OperationNode> | undefined = needsCollectedOperations ? [] : undefined
|
|
639
647
|
|
|
640
648
|
// Run schemas before operations: the two passes share `flushPending` and the
|
|
641
649
|
// FileProcessor's event emitter, so running them concurrently would interleave
|
|
642
650
|
// `kubb:files:processing:start|end` events and race on the shared dirty list.
|
|
643
|
-
await forBatches(schemas, (nodes) => Promise.all(nodes.flatMap((
|
|
651
|
+
await forBatches(schemas, (nodes) => Promise.all(nodes.flatMap((node) => states.map((state) => dispatchNode(state, node, schemaDispatch)))), {
|
|
644
652
|
concurrency: SCHEMA_PARALLEL,
|
|
645
653
|
flush: flushPending,
|
|
646
654
|
})
|
|
@@ -649,7 +657,7 @@ export class KubbDriver {
|
|
|
649
657
|
operations,
|
|
650
658
|
(nodes) => {
|
|
651
659
|
if (needsCollectedOperations) collectedOperations?.push(...nodes)
|
|
652
|
-
return Promise.all(nodes.flatMap((
|
|
660
|
+
return Promise.all(nodes.flatMap((node) => states.map((state) => dispatchNode(state, node, operationDispatch))))
|
|
653
661
|
},
|
|
654
662
|
{ concurrency: SCHEMA_PARALLEL, flush: flushPending },
|
|
655
663
|
)
|
|
@@ -659,23 +667,18 @@ export class KubbDriver {
|
|
|
659
667
|
try {
|
|
660
668
|
const { plugin, generatorContext, generators } = state
|
|
661
669
|
const ctx = { ...generatorContext, options: plugin.options }
|
|
662
|
-
//
|
|
663
|
-
// excludes/includes/overrides
|
|
664
|
-
// be hidden from the batched gen.operations() hook, otherwise grouped/barrel
|
|
665
|
-
// generators emit references to operation files that the per-op hook intentionally skipped.
|
|
670
|
+
// Match what the per-node dispatch passes to gen.operation(): the transformed node,
|
|
671
|
+
// already filtered by excludes/includes/overrides.
|
|
666
672
|
const ops = collectedOperations ?? []
|
|
667
|
-
const pluginOperations =
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
return generatorContext.resolver.resolveOptions(transformed, { options: plugin.options, exclude, include, override }) !== null
|
|
674
|
-
})
|
|
673
|
+
const pluginOperations = ops.reduce<Array<OperationNode>>((acc, node) => {
|
|
674
|
+
const resolved = resolveForPlugin(state, node)
|
|
675
|
+
if (resolved) acc.push(resolved.transformedNode)
|
|
676
|
+
return acc
|
|
677
|
+
}, [])
|
|
675
678
|
for (const gen of generators) {
|
|
676
679
|
if (!gen.operations) continue
|
|
677
680
|
const result = await gen.operations(pluginOperations, ctx)
|
|
678
|
-
await
|
|
681
|
+
await this.dispatch({ result, renderer: gen.renderer })
|
|
679
682
|
}
|
|
680
683
|
await this.hooks.emit('kubb:generate:operations', pluginOperations, ctx)
|
|
681
684
|
} catch (caughtError) {
|
|
@@ -685,36 +688,70 @@ export class KubbDriver {
|
|
|
685
688
|
}
|
|
686
689
|
|
|
687
690
|
const duration = getElapsedMs(state.hrStart)
|
|
688
|
-
timings.set(state.plugin.name, duration)
|
|
689
691
|
await this.#emitPluginEnd({ plugin: state.plugin, duration, success: !state.failed, error: state.failed && state.error ? state.error : undefined })
|
|
690
692
|
|
|
691
|
-
if (state.failed && state.error)
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
logs: [state.failed ? '✗ Plugin start failed' : `✓ Plugin started successfully (${formatMs(duration)})`],
|
|
696
|
-
})
|
|
693
|
+
if (state.failed && state.error) {
|
|
694
|
+
diagnostics.push({ ...Diagnostics.from(state.error), plugin: state.plugin.name })
|
|
695
|
+
}
|
|
696
|
+
diagnostics.push(Diagnostics.performance({ plugin: state.plugin.name, duration }))
|
|
697
697
|
}
|
|
698
698
|
|
|
699
|
-
return
|
|
699
|
+
return diagnostics
|
|
700
700
|
}
|
|
701
701
|
|
|
702
702
|
/**
|
|
703
|
-
*
|
|
704
|
-
* Called at the end of a build to prevent listener leaks across repeated builds.
|
|
703
|
+
* Stores whatever a generator method or `kubb:generate:*` hook returned.
|
|
705
704
|
*
|
|
706
|
-
*
|
|
705
|
+
* - An `Array<FileNode>` goes straight into `fileManager` via `upsert`.
|
|
706
|
+
* - A renderer element runs through `renderer` (the renderer factory, e.g. JSX) and the
|
|
707
|
+
* produced files go to `fileManager.upsert`.
|
|
708
|
+
* - A falsy result is treated as a no-op. The generator wrote files itself via
|
|
709
|
+
* `ctx.upsertFile`.
|
|
710
|
+
*
|
|
711
|
+
* Pass `renderer` when the result may be a renderer element. Generators that only return
|
|
712
|
+
* `Array<FileNode>` do not need one.
|
|
707
713
|
*/
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
714
|
+
async dispatch<TElement = unknown>({
|
|
715
|
+
result,
|
|
716
|
+
renderer,
|
|
717
|
+
}: {
|
|
718
|
+
result: TElement | Array<FileNode> | undefined | null
|
|
719
|
+
renderer?: RendererFactory<TElement> | null
|
|
720
|
+
}): Promise<void> {
|
|
721
|
+
if (!result) return
|
|
722
|
+
|
|
723
|
+
if (Array.isArray(result)) {
|
|
724
|
+
this.fileManager.upsert(...(result as Array<FileNode>))
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (!renderer) {
|
|
729
|
+
return
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
using instance = renderer()
|
|
733
|
+
if (instance.stream) {
|
|
734
|
+
for (const file of instance.stream(result)) {
|
|
735
|
+
this.fileManager.upsert(file)
|
|
712
736
|
}
|
|
737
|
+
return
|
|
713
738
|
}
|
|
714
739
|
|
|
715
|
-
|
|
740
|
+
await instance.render(result)
|
|
741
|
+
this.fileManager.upsert(...instance.files)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Removes every listener the driver added. Listeners attached directly to `hooks` from outside
|
|
746
|
+
* the driver survive. Called at the end of a build to prevent leaks across repeated builds.
|
|
747
|
+
*
|
|
748
|
+
* @internal
|
|
749
|
+
*/
|
|
750
|
+
dispose(): void {
|
|
751
|
+
this.#registry.dispose()
|
|
716
752
|
this.#eventGeneratorPlugins.clear()
|
|
717
|
-
|
|
753
|
+
this.#transforms.dispose()
|
|
754
|
+
// Release resolver closures. The driver is rebuilt for each build() call
|
|
718
755
|
// so there is no value in retaining these maps after disposal.
|
|
719
756
|
this.#resolvers.clear()
|
|
720
757
|
this.#defaultResolvers.clear()
|
|
@@ -722,28 +759,14 @@ export class KubbDriver {
|
|
|
722
759
|
// memory is reclaimed between builds. The returned `BuildOutput.files`
|
|
723
760
|
// array still references any FileNodes the caller needs to inspect.
|
|
724
761
|
this.fileManager.dispose()
|
|
725
|
-
this.#fileProcessor.dispose()
|
|
726
762
|
this.inputNode = null
|
|
727
763
|
this.#studio = { source: null, isOpen: false, inputNode: null }
|
|
728
|
-
|
|
729
|
-
for (const [event, handler] of this.#middlewareListeners) {
|
|
730
|
-
this.hooks.off(event, handler as never)
|
|
731
|
-
}
|
|
732
764
|
}
|
|
733
765
|
|
|
734
766
|
[Symbol.dispose](): void {
|
|
735
767
|
this.dispose()
|
|
736
768
|
}
|
|
737
769
|
|
|
738
|
-
#trackHookListener(event: keyof KubbHooks, handler: (...args: Array<never>) => void | Promise<void>): void {
|
|
739
|
-
let handlers = this.#hookListeners.get(event)
|
|
740
|
-
if (!handlers) {
|
|
741
|
-
handlers = new Set()
|
|
742
|
-
this.#hookListeners.set(event, handlers)
|
|
743
|
-
}
|
|
744
|
-
handlers.add(handler)
|
|
745
|
-
}
|
|
746
|
-
|
|
747
770
|
#getDefaultResolver = memoize(
|
|
748
771
|
this.#defaultResolvers,
|
|
749
772
|
(pluginName: string): Resolver => defineResolver<PluginFactoryOptions>(() => ({ name: 'default', pluginName })),
|
|
@@ -779,6 +802,13 @@ export class KubbDriver {
|
|
|
779
802
|
getContext<TOptions extends PluginFactoryOptions>(plugin: NormalizedPlugin<TOptions>): Omit<GeneratorContext<TOptions>, 'options'> {
|
|
780
803
|
const driver = this
|
|
781
804
|
|
|
805
|
+
// Collect into the active build only. The host renders each collected diagnostic once after the
|
|
806
|
+
// build (the CLI via `Diagnostics.emit`, the agent via its post-build loop), so emitting a live
|
|
807
|
+
// `kubb:error`/`kubb:warn`/`kubb:info` here would render it twice.
|
|
808
|
+
const report = (diagnostic: Omit<ProblemDiagnostic, 'plugin'>): void => {
|
|
809
|
+
Diagnostics.report({ ...diagnostic, plugin: plugin.name })
|
|
810
|
+
}
|
|
811
|
+
|
|
782
812
|
return {
|
|
783
813
|
config: driver.config,
|
|
784
814
|
get root(): string {
|
|
@@ -811,16 +841,17 @@ export class KubbDriver {
|
|
|
811
841
|
return driver.getResolver(plugin.name)
|
|
812
842
|
},
|
|
813
843
|
get transformer() {
|
|
814
|
-
return plugin.
|
|
844
|
+
return driver.#transforms.get(plugin.name)
|
|
815
845
|
},
|
|
816
846
|
warn(message: string) {
|
|
817
|
-
|
|
847
|
+
report({ code: diagnosticCode.pluginWarning, severity: 'warning', message })
|
|
818
848
|
},
|
|
819
849
|
error(error: string | Error) {
|
|
820
|
-
|
|
850
|
+
const cause = typeof error === 'string' ? undefined : error
|
|
851
|
+
report({ code: diagnosticCode.pluginFailed, severity: 'error', message: typeof error === 'string' ? error : error.message, cause })
|
|
821
852
|
},
|
|
822
853
|
info(message: string) {
|
|
823
|
-
|
|
854
|
+
report({ code: diagnosticCode.pluginInfo, severity: 'info', message })
|
|
824
855
|
},
|
|
825
856
|
async openInStudio(options?: DevtoolsOptions) {
|
|
826
857
|
if (!driver.config.devtools || driver.#studio.isOpen) {
|
|
@@ -828,11 +859,23 @@ export class KubbDriver {
|
|
|
828
859
|
}
|
|
829
860
|
|
|
830
861
|
if (typeof driver.config.devtools !== 'object') {
|
|
831
|
-
throw new
|
|
862
|
+
throw new DiagnosticError({
|
|
863
|
+
code: diagnosticCode.devtoolsInvalid,
|
|
864
|
+
severity: 'error',
|
|
865
|
+
message: 'The `devtools` config must be an object.',
|
|
866
|
+
help: 'Set `devtools` to an options object, or remove it to disable Kubb Studio.',
|
|
867
|
+
location: { kind: 'config' },
|
|
868
|
+
})
|
|
832
869
|
}
|
|
833
870
|
|
|
834
871
|
if (!driver.adapter || !driver.#studio.source) {
|
|
835
|
-
throw new
|
|
872
|
+
throw new DiagnosticError({
|
|
873
|
+
code: diagnosticCode.adapterRequired,
|
|
874
|
+
severity: 'error',
|
|
875
|
+
message: 'An adapter is required to open Kubb Studio, but none is configured.',
|
|
876
|
+
help: 'Set `adapter` in kubb.config.ts (for example `adapterOas()`).',
|
|
877
|
+
location: { kind: 'config' },
|
|
878
|
+
})
|
|
836
879
|
}
|
|
837
880
|
|
|
838
881
|
driver.#studio.isOpen = true
|
|
@@ -860,63 +903,28 @@ export class KubbDriver {
|
|
|
860
903
|
requirePlugin(pluginName: string): Plugin {
|
|
861
904
|
const plugin = this.plugins.get(pluginName)
|
|
862
905
|
if (!plugin) {
|
|
863
|
-
throw new
|
|
906
|
+
throw new DiagnosticError({
|
|
907
|
+
code: diagnosticCode.pluginNotFound,
|
|
908
|
+
severity: 'error',
|
|
909
|
+
message: `Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`,
|
|
910
|
+
help: `Add "${pluginName}" to the \`plugins\` array in kubb.config.ts, or remove the dependency on it.`,
|
|
911
|
+
location: { kind: 'config' },
|
|
912
|
+
})
|
|
864
913
|
}
|
|
865
914
|
return plugin
|
|
866
915
|
}
|
|
867
916
|
}
|
|
868
917
|
|
|
869
|
-
/**
|
|
870
|
-
* Handles the return value of a plugin AST hook or generator method.
|
|
871
|
-
*
|
|
872
|
-
* - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`
|
|
873
|
-
* - `Array<FileNode>` → added directly into `driver.fileManager`
|
|
874
|
-
* - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
|
|
875
|
-
*
|
|
876
|
-
* Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result
|
|
877
|
-
* may be a renderer element. Generators that only return `Array<FileNode>` do not need one.
|
|
878
|
-
*/
|
|
879
|
-
export function applyHookResult<TElement = unknown>({
|
|
880
|
-
result,
|
|
881
|
-
driver,
|
|
882
|
-
rendererFactory,
|
|
883
|
-
}: {
|
|
884
|
-
result: TElement | Array<FileNode> | undefined | null
|
|
885
|
-
driver: KubbDriver
|
|
886
|
-
rendererFactory?: RendererFactory<TElement> | null
|
|
887
|
-
}): void | Promise<void> {
|
|
888
|
-
if (!result) return
|
|
889
|
-
|
|
890
|
-
if (Array.isArray(result)) {
|
|
891
|
-
driver.fileManager.upsert(...(result as Array<FileNode>))
|
|
892
|
-
return
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
if (!rendererFactory) {
|
|
896
|
-
return
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
const renderer = rendererFactory()
|
|
900
|
-
if (renderer.stream) {
|
|
901
|
-
using r = renderer
|
|
902
|
-
for (const file of r.stream!(result)) {
|
|
903
|
-
driver.fileManager.upsert(file)
|
|
904
|
-
}
|
|
905
|
-
return
|
|
906
|
-
}
|
|
907
|
-
return applyAsyncRender({ renderer, result, driver })
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
async function applyAsyncRender<TElement>({ renderer, result, driver }: { renderer: Renderer<TElement>; result: TElement; driver: KubbDriver }): Promise<void> {
|
|
911
|
-
using r = renderer
|
|
912
|
-
await r.render(result)
|
|
913
|
-
driver.fileManager.upsert(...r.files)
|
|
914
|
-
}
|
|
915
|
-
|
|
916
918
|
function inputToAdapterSource(config: Config): AdapterSource {
|
|
917
919
|
const input = config.input
|
|
918
920
|
if (!input) {
|
|
919
|
-
throw new
|
|
921
|
+
throw new DiagnosticError({
|
|
922
|
+
code: diagnosticCode.inputRequired,
|
|
923
|
+
severity: 'error',
|
|
924
|
+
message: 'An adapter is configured without an input.',
|
|
925
|
+
help: 'Provide `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config.',
|
|
926
|
+
location: { kind: 'config' },
|
|
927
|
+
})
|
|
920
928
|
}
|
|
921
929
|
|
|
922
930
|
if ('data' in input) {
|