@kubb/core 5.0.0-beta.4 → 5.0.0-beta.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -148
- package/dist/diagnostics-DhfW8YpT.d.ts +2963 -0
- package/dist/index.cjs +775 -1193
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +165 -171
- package/dist/index.js +760 -1188
- package/dist/index.js.map +1 -1
- package/dist/memoryStorage-DTv1Kub1.js +2852 -0
- package/dist/memoryStorage-DTv1Kub1.js.map +1 -0
- package/dist/memoryStorage-Dkxnid2K.cjs +2985 -0
- package/dist/memoryStorage-Dkxnid2K.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 +897 -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 +921 -493
- 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 +78 -7
- 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 +59 -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
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { arrayToAsyncIterable, type AsyncEventEmitter, forBatches, getElapsedMs, isPromise, memoize, URLPath } from '@internals/utils'
|
|
3
|
+
import { collectUsedSchemaNames, createFile, createStreamInput } from '@kubb/ast'
|
|
4
|
+
import type { FileNode, InputMeta, InputStreamNode, OperationNode, SchemaNode } from '@kubb/ast'
|
|
5
|
+
import { OPERATION_FILTER_TYPES, SCHEMA_PARALLEL } from './constants.ts'
|
|
6
|
+
import { type Diagnostic, Diagnostics, type ProblemDiagnostic } from './diagnostics.ts'
|
|
7
|
+
import type { RendererFactory } from './createRenderer.ts'
|
|
8
|
+
import type { Storage } from './createStorage.ts'
|
|
9
|
+
import type { Generator } from './defineGenerator.ts'
|
|
10
|
+
import type { Parser } from './defineParser.ts'
|
|
11
|
+
import type { Plugin } from './definePlugin.ts'
|
|
12
|
+
import { getMode } from './definePlugin.ts'
|
|
13
|
+
import { defineResolver } from './defineResolver.ts'
|
|
14
|
+
import { FileManager } from './FileManager.ts'
|
|
15
|
+
import { FileProcessor } from './FileProcessor.ts'
|
|
16
|
+
import { type HookListener, HookRegistry } from './HookRegistry.ts'
|
|
17
|
+
import { Transform } from './Transform.ts'
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
Adapter,
|
|
21
|
+
AdapterSource,
|
|
22
|
+
Config,
|
|
23
|
+
GeneratorContext,
|
|
24
|
+
KubbHooks,
|
|
25
|
+
KubbPluginSetupContext,
|
|
26
|
+
Middleware,
|
|
27
|
+
NormalizedPlugin,
|
|
28
|
+
PluginFactoryOptions,
|
|
29
|
+
Resolver,
|
|
30
|
+
} from './types.ts'
|
|
31
|
+
|
|
32
|
+
type Options = {
|
|
33
|
+
hooks: AsyncEventEmitter<KubbHooks>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function enforceOrder(enforce: 'pre' | 'post' | undefined): number {
|
|
37
|
+
return enforce === 'pre' ? -1 : enforce === 'post' ? 1 : 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class KubbDriver {
|
|
41
|
+
readonly config: Config
|
|
42
|
+
readonly options: Options
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* KubbDriver.getMode('src/gen/types.ts') // 'single'
|
|
50
|
+
* KubbDriver.getMode('src/gen/types') // 'split'
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
static getMode(fileOrFolder: string | undefined | null): 'single' | 'split' {
|
|
54
|
+
return getMode(fileOrFolder)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The streaming `InputStreamNode` produced by the adapter.
|
|
59
|
+
* Always set after adapter setup, parse-only adapters are wrapped automatically.
|
|
60
|
+
*/
|
|
61
|
+
inputNode: InputStreamNode | null = null
|
|
62
|
+
adapter: Adapter | null = null
|
|
63
|
+
/**
|
|
64
|
+
* Raw adapter source so `adapter.parse()` / `adapter.stream()` can run lazily.
|
|
65
|
+
* Intentionally outlives the build, cleared by `dispose()`.
|
|
66
|
+
*/
|
|
67
|
+
#adapterSource: AdapterSource | null = null
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Central file store for all generated files.
|
|
71
|
+
* Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
|
|
72
|
+
* add files. This property gives direct read/write access when needed.
|
|
73
|
+
*/
|
|
74
|
+
readonly fileManager = new FileManager()
|
|
75
|
+
readonly plugins = new Map<string, NormalizedPlugin>()
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Tracks which plugins have generators registered via `addGenerator()` (event-based path).
|
|
79
|
+
* Used by the build loop to decide whether to emit generator events for a given plugin.
|
|
80
|
+
*/
|
|
81
|
+
readonly #eventGeneratorPlugins = new Set<string>()
|
|
82
|
+
readonly #resolvers = new Map<string, Resolver>()
|
|
83
|
+
readonly #defaultResolvers = new Map<string, Resolver>()
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Tracks every listener the driver added (plugin, middleware, generator) so `dispose()` can
|
|
87
|
+
* remove them in one pass. Middleware registers after plugins, so it fires last via `Set`
|
|
88
|
+
* insertion order. External `hooks.on(...)` listeners are not tracked.
|
|
89
|
+
*/
|
|
90
|
+
readonly #registry: HookRegistry<KubbHooks>
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Transform registry. Plugins populate it during `kubb:plugin:setup` via `setTransformer`,
|
|
94
|
+
* and `#runGenerators` reads it once per `(plugin, node)` pair through `applyTo`.
|
|
95
|
+
*/
|
|
96
|
+
readonly #transforms = new Transform()
|
|
97
|
+
|
|
98
|
+
constructor(config: Config, options: Options) {
|
|
99
|
+
this.config = config
|
|
100
|
+
this.options = options
|
|
101
|
+
this.adapter = config.adapter ?? null
|
|
102
|
+
this.#registry = new HookRegistry({ emitter: options.hooks })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async setup() {
|
|
106
|
+
const normalized: Array<NormalizedPlugin> = this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin as Plugin))
|
|
107
|
+
|
|
108
|
+
normalized.sort((a, b) => {
|
|
109
|
+
if (b.dependencies?.includes(a.name)) return -1
|
|
110
|
+
if (a.dependencies?.includes(b.name)) return 1
|
|
111
|
+
|
|
112
|
+
return enforceOrder(a.enforce) - enforceOrder(b.enforce)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
for (const plugin of normalized) {
|
|
116
|
+
if (plugin.apply) {
|
|
117
|
+
plugin.apply(this.config)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.#registerPlugin(plugin)
|
|
121
|
+
this.plugins.set(plugin.name, plugin)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.config.middleware) {
|
|
125
|
+
for (const middleware of this.config.middleware) {
|
|
126
|
+
for (const event of Object.keys(middleware.hooks) as Array<keyof KubbHooks & string>) {
|
|
127
|
+
this.#registerMiddleware(event, middleware.hooks)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (this.config.adapter) {
|
|
132
|
+
this.#adapterSource = inputToAdapterSource(this.config)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get hooks() {
|
|
137
|
+
return this.options.hooks
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Creates an `NormalizedPlugin` from a hook-style plugin and registers
|
|
142
|
+
* its lifecycle handlers on the `AsyncEventEmitter`.
|
|
143
|
+
*/
|
|
144
|
+
#normalizePlugin(plugin: Plugin): NormalizedPlugin {
|
|
145
|
+
const normalized: NormalizedPlugin = {
|
|
146
|
+
name: plugin.name,
|
|
147
|
+
dependencies: plugin.dependencies,
|
|
148
|
+
enforce: plugin.enforce,
|
|
149
|
+
hooks: plugin.hooks,
|
|
150
|
+
options: plugin.options ?? { output: { path: '.' }, exclude: [], override: [] },
|
|
151
|
+
} as NormalizedPlugin
|
|
152
|
+
|
|
153
|
+
if ('apply' in plugin && typeof plugin.apply === 'function') {
|
|
154
|
+
normalized.apply = plugin.apply as (config: Config) => boolean
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return normalized
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Parses the adapter source into `this.inputNode`. Idempotent, so repeated calls from
|
|
162
|
+
* `run` do not re-parse. Adapters with `stream()` are used directly.
|
|
163
|
+
* Adapters with only `parse()` are wrapped via `createStreamInput` so the dispatch loop
|
|
164
|
+
* stays stream-only.
|
|
165
|
+
*/
|
|
166
|
+
async #parseInput(): Promise<void> {
|
|
167
|
+
if (this.inputNode || !this.adapter || !this.#adapterSource) return
|
|
168
|
+
|
|
169
|
+
const adapter = this.adapter
|
|
170
|
+
const source = this.#adapterSource
|
|
171
|
+
|
|
172
|
+
if (adapter.stream) {
|
|
173
|
+
this.inputNode = await adapter.stream(source)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const parsed = await adapter.parse(source)
|
|
178
|
+
this.inputNode = createStreamInput(arrayToAsyncIterable(parsed.schemas), arrayToAsyncIterable(parsed.operations), parsed.meta)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#registerMiddleware<K extends keyof KubbHooks & string>(event: K, middlewareHooks: Middleware['hooks']) {
|
|
182
|
+
const handler = middlewareHooks[event]
|
|
183
|
+
|
|
184
|
+
if (!handler) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.#registry.register({ event, handler, source: 'middleware' })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.
|
|
193
|
+
*
|
|
194
|
+
* The `kubb:plugin:setup` listener wraps the global context in a plugin-specific one so
|
|
195
|
+
* `addGenerator`, `setResolver`, and `setTransformer` target the right `normalizedPlugin`.
|
|
196
|
+
* Every other `KubbHooks` event registers as a pass-through listener that external tooling
|
|
197
|
+
* can observe via `hooks.on(...)`.
|
|
198
|
+
*
|
|
199
|
+
* @internal
|
|
200
|
+
*/
|
|
201
|
+
#registerPlugin(plugin: NormalizedPlugin): void {
|
|
202
|
+
const { hooks } = plugin
|
|
203
|
+
|
|
204
|
+
if (!hooks) return
|
|
205
|
+
|
|
206
|
+
// kubb:plugin:setup gets special treatment: the globally emitted context is wrapped with
|
|
207
|
+
// plugin-specific implementations so that addGenerator / setResolver / etc. target
|
|
208
|
+
// this plugin's normalizedPlugin entry rather than being no-ops.
|
|
209
|
+
if (hooks['kubb:plugin:setup']) {
|
|
210
|
+
const setupHandler = (globalCtx: KubbPluginSetupContext) => {
|
|
211
|
+
const pluginCtx: KubbPluginSetupContext = {
|
|
212
|
+
...globalCtx,
|
|
213
|
+
options: plugin.options ?? {},
|
|
214
|
+
addGenerator: (gen) => {
|
|
215
|
+
this.registerGenerator(plugin.name, gen)
|
|
216
|
+
},
|
|
217
|
+
setResolver: (resolver) => {
|
|
218
|
+
this.setPluginResolver(plugin.name, resolver)
|
|
219
|
+
},
|
|
220
|
+
setTransformer: (visitor) => {
|
|
221
|
+
this.#transforms.register(plugin.name, visitor)
|
|
222
|
+
},
|
|
223
|
+
setOptions: (opts) => {
|
|
224
|
+
plugin.options = { ...plugin.options, ...opts }
|
|
225
|
+
},
|
|
226
|
+
injectFile: (userFileNode) => {
|
|
227
|
+
this.fileManager.add(createFile(userFileNode))
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
return hooks['kubb:plugin:setup']!(pluginCtx)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this.#registry.register({ event: 'kubb:plugin:setup', handler: setupHandler, source: 'plugin' })
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// All other hooks are registered as direct pass-through listeners on the shared emitter.
|
|
237
|
+
for (const event of Object.keys(hooks) as Array<keyof KubbHooks & string>) {
|
|
238
|
+
if (event === 'kubb:plugin:setup') continue
|
|
239
|
+
const handler = hooks[event]
|
|
240
|
+
if (!handler) continue
|
|
241
|
+
|
|
242
|
+
this.#registry.register({
|
|
243
|
+
event,
|
|
244
|
+
handler: handler as HookListener<KubbHooks[typeof event], unknown>,
|
|
245
|
+
source: 'plugin',
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Emits the `kubb:plugin:setup` event so that all registered hook-style plugin listeners
|
|
252
|
+
* can configure generators, resolvers, transformers and renderers before `buildStart` runs.
|
|
253
|
+
*
|
|
254
|
+
* Call this once from `safeBuild` before the plugin execution loop begins.
|
|
255
|
+
*/
|
|
256
|
+
async emitSetupHooks(): Promise<void> {
|
|
257
|
+
const noop = () => {}
|
|
258
|
+
|
|
259
|
+
await this.hooks.emit('kubb:plugin:setup', {
|
|
260
|
+
config: this.config,
|
|
261
|
+
options: {},
|
|
262
|
+
addGenerator: noop,
|
|
263
|
+
setResolver: noop,
|
|
264
|
+
setTransformer: noop,
|
|
265
|
+
setOptions: noop,
|
|
266
|
+
injectFile: noop,
|
|
267
|
+
updateConfig: noop,
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Registers a generator for the given plugin on the shared event emitter.
|
|
273
|
+
*
|
|
274
|
+
* The generator's `schema`, `operation`, and `operations` methods are registered as
|
|
275
|
+
* listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations`
|
|
276
|
+
* respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check
|
|
277
|
+
* so that generators from different plugins do not cross-fire.
|
|
278
|
+
*
|
|
279
|
+
* The renderer comes from `generator.renderer`. Set `generator.renderer = null` (or leave it
|
|
280
|
+
* unset) to opt out of rendering.
|
|
281
|
+
*
|
|
282
|
+
* Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
|
|
283
|
+
*/
|
|
284
|
+
registerGenerator(pluginName: string, generator: Generator): void {
|
|
285
|
+
if (generator.schema) {
|
|
286
|
+
const schemaHandler = async (node: SchemaNode, ctx: GeneratorContext) => {
|
|
287
|
+
if (ctx.plugin.name !== pluginName) return
|
|
288
|
+
const result = await generator.schema!(node, ctx)
|
|
289
|
+
|
|
290
|
+
await this.dispatch({ result, renderer: generator.renderer })
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.#registry.register({ event: 'kubb:generate:schema', handler: schemaHandler, source: 'driver' })
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (generator.operation) {
|
|
297
|
+
const operationHandler = async (node: OperationNode, ctx: GeneratorContext) => {
|
|
298
|
+
if (ctx.plugin.name !== pluginName) return
|
|
299
|
+
|
|
300
|
+
const result = await generator.operation!(node, ctx)
|
|
301
|
+
await this.dispatch({ result, renderer: generator.renderer })
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.#registry.register({ event: 'kubb:generate:operation', handler: operationHandler, source: 'driver' })
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (generator.operations) {
|
|
308
|
+
const operationsHandler = async (nodes: Array<OperationNode>, ctx: GeneratorContext) => {
|
|
309
|
+
if (ctx.plugin.name !== pluginName) return
|
|
310
|
+
const result = await generator.operations!(nodes, ctx)
|
|
311
|
+
await this.dispatch({ result, renderer: generator.renderer })
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.#registry.register({ event: 'kubb:generate:operations', handler: operationsHandler, source: 'driver' })
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.#eventGeneratorPlugins.add(pluginName)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Returns `true` when at least one generator was registered for the given plugin
|
|
322
|
+
* via `addGenerator()` in `kubb:plugin:setup` (event-based path).
|
|
323
|
+
*
|
|
324
|
+
* Used by the build loop to decide whether to walk the AST and emit generator events
|
|
325
|
+
* for a plugin that has no static `plugin.generators`.
|
|
326
|
+
*/
|
|
327
|
+
hasEventGenerators(pluginName: string): boolean {
|
|
328
|
+
return this.#eventGeneratorPlugins.has(pluginName)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Runs the full plugin pipeline. Returns the diagnostics collected so far even
|
|
333
|
+
* when an outer hook throws, since the orchestrator preserves partial state by capturing
|
|
334
|
+
* the failure as a {@link Diagnostic} instead of propagating. Each plugin also
|
|
335
|
+
* contributes a `timing` diagnostic for the run summary.
|
|
336
|
+
*/
|
|
337
|
+
async run({ storage }: { storage: Storage }): Promise<{ diagnostics: Array<Diagnostic> }> {
|
|
338
|
+
const { hooks, config } = this
|
|
339
|
+
const diagnostics: Array<Diagnostic> = []
|
|
340
|
+
const parsersMap = new Map<FileNode['extname'], Parser>()
|
|
341
|
+
|
|
342
|
+
for (const parser of config.parsers) {
|
|
343
|
+
if (parser.extNames) {
|
|
344
|
+
for (const ext of parser.extNames) parsersMap.set(ext, parser)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const processor = new FileProcessor({ parsers: parsersMap, storage, extension: config.output.extension })
|
|
349
|
+
// Bridge processor lifecycle to the user-facing kubb hooks so existing listeners on
|
|
350
|
+
// kubb:files:processing:* keep firing.
|
|
351
|
+
processor.hooks.on('start', async (files) => {
|
|
352
|
+
await hooks.emit('kubb:files:processing:start', { files })
|
|
353
|
+
})
|
|
354
|
+
const updateBuffer: Array<{ file: FileNode; source?: string; processed: number; total: number; percentage: number }> = []
|
|
355
|
+
processor.hooks.on('update', (item) => {
|
|
356
|
+
updateBuffer.push(item)
|
|
357
|
+
})
|
|
358
|
+
processor.hooks.on('end', async (files) => {
|
|
359
|
+
await hooks.emit('kubb:files:processing:update', {
|
|
360
|
+
files: updateBuffer.map((item) => ({ ...item, config })),
|
|
361
|
+
})
|
|
362
|
+
updateBuffer.length = 0
|
|
363
|
+
await hooks.emit('kubb:files:processing:end', { files })
|
|
364
|
+
})
|
|
365
|
+
const onFileUpsert = (file: FileNode): void => {
|
|
366
|
+
processor.enqueue(file)
|
|
367
|
+
}
|
|
368
|
+
this.fileManager.hooks.on('upsert', onFileUpsert)
|
|
369
|
+
|
|
370
|
+
// Make `diagnostics` the active sink so deep code (adapter parse, lazily consumed
|
|
371
|
+
// streams, generators) can report into this run via `Diagnostics.report`.
|
|
372
|
+
return Diagnostics.scope(
|
|
373
|
+
(diagnostic) => diagnostics.push(diagnostic),
|
|
374
|
+
async () => {
|
|
375
|
+
try {
|
|
376
|
+
// Parse the adapter source into the streaming `InputNode`.
|
|
377
|
+
await this.#parseInput()
|
|
378
|
+
// Emit `kubb:plugin:setup` so plugins can register transformers via `setTransformer`.
|
|
379
|
+
// Each call writes into `this.#transforms`, which `#runGenerators` later reads through
|
|
380
|
+
// `transforms.applyTo`.
|
|
381
|
+
await this.emitSetupHooks()
|
|
382
|
+
|
|
383
|
+
if (this.adapter && this.inputNode) {
|
|
384
|
+
await hooks.emit(
|
|
385
|
+
'kubb:build:start',
|
|
386
|
+
Object.assign({ config, adapter: this.adapter, meta: this.inputNode.meta, getPlugin: this.getPlugin.bind(this) }, this.#filesPayload()),
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const generatorPlugins: Array<{ plugin: NormalizedPlugin; context: Omit<GeneratorContext, 'options'>; hrStart: ReturnType<typeof process.hrtime> }> =
|
|
391
|
+
[]
|
|
392
|
+
|
|
393
|
+
for (const plugin of this.plugins.values()) {
|
|
394
|
+
const context = this.getContext(plugin)
|
|
395
|
+
const hrStart = process.hrtime()
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
await hooks.emit('kubb:plugin:start', { plugin })
|
|
399
|
+
} catch (caughtError) {
|
|
400
|
+
const error = caughtError as Error
|
|
401
|
+
const duration = getElapsedMs(hrStart)
|
|
402
|
+
|
|
403
|
+
await this.#emitPluginEnd({ plugin, duration, success: false, error })
|
|
404
|
+
|
|
405
|
+
diagnostics.push({ ...Diagnostics.from(error), plugin: plugin.name }, Diagnostics.performance({ plugin: plugin.name, duration }))
|
|
406
|
+
|
|
407
|
+
continue
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (this.hasEventGenerators(plugin.name)) {
|
|
411
|
+
generatorPlugins.push({ plugin, context, hrStart })
|
|
412
|
+
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const duration = getElapsedMs(hrStart)
|
|
417
|
+
diagnostics.push(Diagnostics.performance({ plugin: plugin.name, duration }))
|
|
418
|
+
|
|
419
|
+
await this.#emitPluginEnd({ plugin, duration, success: true })
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Stream every node through the transform registry and into each plugin's generators.
|
|
423
|
+
// Handles the empty-entries and missing-`inputNode` cases by closing out each entry's
|
|
424
|
+
// `kubb:plugin:end` directly.
|
|
425
|
+
diagnostics.push(...(await this.#runGenerators(generatorPlugins, () => processor.flush())))
|
|
426
|
+
// Wait for the last in-flight batch and write anything still pending.
|
|
427
|
+
await processor.drain()
|
|
428
|
+
|
|
429
|
+
await hooks.emit('kubb:plugins:end', Object.assign({ config }, this.#filesPayload()))
|
|
430
|
+
|
|
431
|
+
// Plugins-end listeners (barrel middleware etc.) may have queued more files.
|
|
432
|
+
await processor.drain()
|
|
433
|
+
|
|
434
|
+
await hooks.emit('kubb:build:end', { files: this.fileManager.files, config, outputDir: resolve(config.root, config.output.path) })
|
|
435
|
+
|
|
436
|
+
return { diagnostics: Diagnostics.dedupe(diagnostics) }
|
|
437
|
+
} catch (caughtError) {
|
|
438
|
+
diagnostics.push(Diagnostics.from(caughtError))
|
|
439
|
+
return { diagnostics: Diagnostics.dedupe(diagnostics) }
|
|
440
|
+
} finally {
|
|
441
|
+
this.fileManager.hooks.off('upsert', onFileUpsert)
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Returns a fresh object with a lazy `files` getter and a bound `upsertFile`.
|
|
448
|
+
// Caller must use `Object.assign(extra, this.#filesPayload())`, not object spread.
|
|
449
|
+
// Spread would eagerly invoke the getter and freeze a stale snapshot into the payload.
|
|
450
|
+
#filesPayload(): { readonly files: Array<FileNode>; upsertFile: (...files: Array<FileNode>) => Array<FileNode> } {
|
|
451
|
+
const driver = this
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
get files() {
|
|
455
|
+
return driver.fileManager.files
|
|
456
|
+
},
|
|
457
|
+
upsertFile: (...files: Array<FileNode>) => driver.fileManager.upsert(...files),
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
#emitPluginEnd({ plugin, duration, success, error }: { plugin: NormalizedPlugin; duration: number; success: boolean; error?: Error }): Promise<void> | void {
|
|
462
|
+
return this.hooks.emit(
|
|
463
|
+
'kubb:plugin:end',
|
|
464
|
+
Object.assign({ plugin, duration, success, ...(error ? { error } : {}), config: this.config }, this.#filesPayload()),
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Streams schemas and operations through every plugin's generators. Each node is run
|
|
470
|
+
* through the plugin's transformer (from `this.#transforms`) before the generator sees it,
|
|
471
|
+
* so plugins stay isolated and the hot path stays per-node. Schemas run before operations
|
|
472
|
+
* because the two passes share `flushPending` and the FileProcessor's event emitter.
|
|
473
|
+
* A failing plugin contributes an error diagnostic so the rest of the build continues.
|
|
474
|
+
* Every plugin also contributes a `timing` diagnostic.
|
|
475
|
+
*
|
|
476
|
+
* When `entries` is empty or `this.inputNode` is `null`, every entry still gets a
|
|
477
|
+
* `kubb:plugin:end` so middleware listeners (the barrel writer and friends) complete.
|
|
478
|
+
*/
|
|
479
|
+
async #runGenerators(
|
|
480
|
+
entries: Array<{ plugin: NormalizedPlugin; context: Omit<GeneratorContext, 'options'>; hrStart: ReturnType<typeof process.hrtime> }>,
|
|
481
|
+
flushPending: () => Promise<void>,
|
|
482
|
+
): Promise<Array<Diagnostic>> {
|
|
483
|
+
const diagnostics: Array<Diagnostic> = []
|
|
484
|
+
|
|
485
|
+
if (entries.length === 0) return diagnostics
|
|
486
|
+
|
|
487
|
+
if (!this.inputNode) {
|
|
488
|
+
for (const { plugin, hrStart } of entries) {
|
|
489
|
+
const duration = getElapsedMs(hrStart)
|
|
490
|
+
diagnostics.push(Diagnostics.performance({ plugin: plugin.name, duration }))
|
|
491
|
+
await this.#emitPluginEnd({ plugin, duration, success: true })
|
|
492
|
+
}
|
|
493
|
+
return diagnostics
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const transforms = this.#transforms
|
|
497
|
+
const { schemas, operations } = this.inputNode
|
|
498
|
+
|
|
499
|
+
type PluginState = {
|
|
500
|
+
plugin: NormalizedPlugin
|
|
501
|
+
generatorContext: Omit<GeneratorContext, 'options'>
|
|
502
|
+
generators: Array<Generator>
|
|
503
|
+
hrStart: ReturnType<typeof process.hrtime>
|
|
504
|
+
failed: boolean
|
|
505
|
+
error: Error | null
|
|
506
|
+
optionsAreStatic: boolean
|
|
507
|
+
allowedSchemaNames: Set<string> | null
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const states: Array<PluginState> = entries.map(({ plugin, context, hrStart }) => {
|
|
511
|
+
const { exclude, include, override } = plugin.options
|
|
512
|
+
const hasExclude = Array.isArray(exclude) && exclude.length > 0
|
|
513
|
+
const hasInclude = Array.isArray(include) && include.length > 0
|
|
514
|
+
const hasOverride = Array.isArray(override) && override.length > 0
|
|
515
|
+
return {
|
|
516
|
+
plugin,
|
|
517
|
+
generatorContext: { ...context, resolver: this.getResolver(plugin.name) },
|
|
518
|
+
generators: plugin.generators ?? [],
|
|
519
|
+
hrStart,
|
|
520
|
+
failed: false,
|
|
521
|
+
error: null,
|
|
522
|
+
optionsAreStatic: !hasExclude && !hasInclude && !hasOverride,
|
|
523
|
+
allowedSchemaNames: null,
|
|
524
|
+
}
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
const emitsSchemaHook = this.hooks.listenerCount('kubb:generate:schema') > 0
|
|
528
|
+
const emitsOperationHook = this.hooks.listenerCount('kubb:generate:operation') > 0
|
|
529
|
+
|
|
530
|
+
// Pre-scan: plugins with operation-based includes (but no schemaName include) need
|
|
531
|
+
// the reachable schema set. This requires the full schema graph in memory at once,
|
|
532
|
+
// since transitive reachability can't be derived from a single node. `allSchemas` is
|
|
533
|
+
// released as soon as the pre-scan returns, so the main passes get fresh iterators.
|
|
534
|
+
const pruningStates = states.filter(({ plugin }) => {
|
|
535
|
+
const { include } = plugin.options
|
|
536
|
+
return (include?.some(({ type }) => OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === 'schemaName') ?? false)
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
if (pruningStates.length > 0) {
|
|
540
|
+
const allSchemas: Array<SchemaNode> = []
|
|
541
|
+
for await (const schema of schemas) allSchemas.push(schema)
|
|
542
|
+
|
|
543
|
+
const includedOpsByState = new Map<PluginState, Array<OperationNode>>(pruningStates.map((state) => [state, []]))
|
|
544
|
+
for await (const operation of operations) {
|
|
545
|
+
for (const state of pruningStates) {
|
|
546
|
+
const { exclude, include, override } = state.plugin.options
|
|
547
|
+
const options = state.generatorContext.resolver.resolveOptions(operation, { options: state.plugin.options, exclude, include, override })
|
|
548
|
+
if (options !== null) includedOpsByState.get(state)?.push(operation)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
for (const state of pruningStates) {
|
|
553
|
+
state.allowedSchemaNames = collectUsedSchemaNames(includedOpsByState.get(state) ?? [], allSchemas)
|
|
554
|
+
includedOpsByState.delete(state)
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Apply the plugin's transformer, then resolve options (skipping the resolver when
|
|
559
|
+
// optionsAreStatic). Returns null when include/exclude/override rules out the node.
|
|
560
|
+
// The per-node dispatch and the collected-operations tail both go through this so
|
|
561
|
+
// they agree on what a plugin sees.
|
|
562
|
+
const resolveForPlugin = <TNode extends SchemaNode | OperationNode>(
|
|
563
|
+
state: PluginState,
|
|
564
|
+
node: TNode,
|
|
565
|
+
): { transformedNode: TNode; options: NormalizedPlugin['options'] } | null => {
|
|
566
|
+
const { plugin, generatorContext } = state
|
|
567
|
+
const transformedNode = transforms.applyTo(plugin.name, node)
|
|
568
|
+
if (state.optionsAreStatic) return { transformedNode, options: plugin.options }
|
|
569
|
+
|
|
570
|
+
const { exclude, include, override } = plugin.options
|
|
571
|
+
const options = generatorContext.resolver.resolveOptions(transformedNode, { options: plugin.options, exclude, include, override })
|
|
572
|
+
if (options === null) return null
|
|
573
|
+
return { transformedNode, options }
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Schema and operation passes share this body. They differ only in which generator
|
|
577
|
+
// method runs, which hook is emitted, and the schema-only `allowedSchemaNames` prune
|
|
578
|
+
// (operations don't carry that constraint).
|
|
579
|
+
const dispatchNode = async <TNode extends SchemaNode | OperationNode>(
|
|
580
|
+
state: PluginState,
|
|
581
|
+
node: TNode,
|
|
582
|
+
dispatch: {
|
|
583
|
+
method: 'schema' | 'operation'
|
|
584
|
+
checkAllowedNames: boolean
|
|
585
|
+
emit: ((node: TNode, ctx: GeneratorContext) => Promise<void> | void) | null
|
|
586
|
+
},
|
|
587
|
+
): Promise<void> => {
|
|
588
|
+
if (state.failed) return
|
|
589
|
+
try {
|
|
590
|
+
const resolved = resolveForPlugin(state, node)
|
|
591
|
+
if (!resolved) return
|
|
592
|
+
|
|
593
|
+
const { transformedNode, options } = resolved
|
|
594
|
+
if (
|
|
595
|
+
dispatch.checkAllowedNames &&
|
|
596
|
+
state.allowedSchemaNames !== null &&
|
|
597
|
+
'name' in transformedNode &&
|
|
598
|
+
transformedNode.name &&
|
|
599
|
+
!state.allowedSchemaNames.has(transformedNode.name)
|
|
600
|
+
) {
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const ctx = { ...state.generatorContext, options }
|
|
605
|
+
for (const gen of state.generators) {
|
|
606
|
+
const run = gen[dispatch.method] as ((node: TNode, ctx: GeneratorContext) => unknown) | undefined
|
|
607
|
+
if (!run) continue
|
|
608
|
+
const raw = run(transformedNode, ctx)
|
|
609
|
+
const result = isPromise(raw) ? await raw : raw
|
|
610
|
+
const applied = this.dispatch({ result, renderer: gen.renderer })
|
|
611
|
+
if (isPromise(applied)) await applied
|
|
612
|
+
}
|
|
613
|
+
if (dispatch.emit) await dispatch.emit(transformedNode, ctx)
|
|
614
|
+
} catch (caughtError) {
|
|
615
|
+
state.failed = true
|
|
616
|
+
state.error = caughtError as Error
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const schemaDispatch = {
|
|
621
|
+
method: 'schema',
|
|
622
|
+
checkAllowedNames: true,
|
|
623
|
+
emit: emitsSchemaHook ? (node: SchemaNode, ctx: GeneratorContext) => this.hooks.emit('kubb:generate:schema', node, ctx) : null,
|
|
624
|
+
} as const
|
|
625
|
+
const operationDispatch = {
|
|
626
|
+
method: 'operation',
|
|
627
|
+
checkAllowedNames: false,
|
|
628
|
+
emit: emitsOperationHook ? (node: OperationNode, ctx: GeneratorContext) => this.hooks.emit('kubb:generate:operation', node, ctx) : null,
|
|
629
|
+
} as const
|
|
630
|
+
|
|
631
|
+
// Skip building the aggregated operations array when nothing consumes it. Saves an
|
|
632
|
+
// N-sized allocation on the common path where plugins only define per-node `gen.operation`.
|
|
633
|
+
const needsCollectedOperations =
|
|
634
|
+
this.hooks.listenerCount('kubb:generate:operations') > 0 || states.some((state) => state.generators.some((gen) => !!gen.operations))
|
|
635
|
+
const collectedOperations: Array<OperationNode> | undefined = needsCollectedOperations ? [] : undefined
|
|
636
|
+
|
|
637
|
+
// Run schemas before operations: the two passes share `flushPending` and the
|
|
638
|
+
// FileProcessor's event emitter, so running them concurrently would interleave
|
|
639
|
+
// `kubb:files:processing:start|end` events and race on the shared dirty list.
|
|
640
|
+
await forBatches(schemas, (nodes) => Promise.all(nodes.flatMap((node) => states.map((state) => dispatchNode(state, node, schemaDispatch)))), {
|
|
641
|
+
concurrency: SCHEMA_PARALLEL,
|
|
642
|
+
flush: flushPending,
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
await forBatches(
|
|
646
|
+
operations,
|
|
647
|
+
(nodes) => {
|
|
648
|
+
if (needsCollectedOperations) collectedOperations?.push(...nodes)
|
|
649
|
+
return Promise.all(nodes.flatMap((node) => states.map((state) => dispatchNode(state, node, operationDispatch))))
|
|
650
|
+
},
|
|
651
|
+
{ concurrency: SCHEMA_PARALLEL, flush: flushPending },
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
for (const state of states) {
|
|
655
|
+
if (!state.failed && needsCollectedOperations) {
|
|
656
|
+
try {
|
|
657
|
+
const { plugin, generatorContext, generators } = state
|
|
658
|
+
const ctx = { ...generatorContext, options: plugin.options }
|
|
659
|
+
// Match what the per-node dispatch passes to gen.operation(): the transformed node,
|
|
660
|
+
// already filtered by excludes/includes/overrides.
|
|
661
|
+
const ops = collectedOperations ?? []
|
|
662
|
+
const pluginOperations = ops.reduce<Array<OperationNode>>((acc, node) => {
|
|
663
|
+
const resolved = resolveForPlugin(state, node)
|
|
664
|
+
if (resolved) acc.push(resolved.transformedNode)
|
|
665
|
+
return acc
|
|
666
|
+
}, [])
|
|
667
|
+
for (const gen of generators) {
|
|
668
|
+
if (!gen.operations) continue
|
|
669
|
+
const result = await gen.operations(pluginOperations, ctx)
|
|
670
|
+
await this.dispatch({ result, renderer: gen.renderer })
|
|
671
|
+
}
|
|
672
|
+
await this.hooks.emit('kubb:generate:operations', pluginOperations, ctx)
|
|
673
|
+
} catch (caughtError) {
|
|
674
|
+
state.failed = true
|
|
675
|
+
state.error = caughtError as Error
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const duration = getElapsedMs(state.hrStart)
|
|
680
|
+
await this.#emitPluginEnd({ plugin: state.plugin, duration, success: !state.failed, error: state.failed && state.error ? state.error : undefined })
|
|
681
|
+
|
|
682
|
+
if (state.failed && state.error) {
|
|
683
|
+
diagnostics.push({ ...Diagnostics.from(state.error), plugin: state.plugin.name })
|
|
684
|
+
}
|
|
685
|
+
diagnostics.push(Diagnostics.performance({ plugin: state.plugin.name, duration }))
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return diagnostics
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Stores whatever a generator method or `kubb:generate:*` hook returned.
|
|
693
|
+
*
|
|
694
|
+
* - An `Array<FileNode>` goes straight into `fileManager` via `upsert`.
|
|
695
|
+
* - A renderer element runs through `renderer` (the renderer factory, e.g. JSX) and the
|
|
696
|
+
* produced files go to `fileManager.upsert`.
|
|
697
|
+
* - A falsy result is treated as a no-op. The generator wrote files itself via
|
|
698
|
+
* `ctx.upsertFile`.
|
|
699
|
+
*
|
|
700
|
+
* Pass `renderer` when the result may be a renderer element. Generators that only return
|
|
701
|
+
* `Array<FileNode>` do not need one.
|
|
702
|
+
*/
|
|
703
|
+
async dispatch<TElement = unknown>({
|
|
704
|
+
result,
|
|
705
|
+
renderer,
|
|
706
|
+
}: {
|
|
707
|
+
result: TElement | Array<FileNode> | undefined | null
|
|
708
|
+
renderer?: RendererFactory<TElement> | null
|
|
709
|
+
}): Promise<void> {
|
|
710
|
+
if (!result) return
|
|
711
|
+
|
|
712
|
+
if (Array.isArray(result)) {
|
|
713
|
+
this.fileManager.upsert(...(result as Array<FileNode>))
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (!renderer) {
|
|
718
|
+
return
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
using instance = renderer()
|
|
722
|
+
if (instance.stream) {
|
|
723
|
+
for (const file of instance.stream(result)) {
|
|
724
|
+
this.fileManager.upsert(file)
|
|
725
|
+
}
|
|
726
|
+
return
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
await instance.render(result)
|
|
730
|
+
this.fileManager.upsert(...instance.files)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Removes every listener the driver added. Listeners attached directly to `hooks` from outside
|
|
735
|
+
* the driver survive. Called at the end of a build to prevent leaks across repeated builds.
|
|
736
|
+
*
|
|
737
|
+
* @internal
|
|
738
|
+
*/
|
|
739
|
+
dispose(): void {
|
|
740
|
+
this.#registry.dispose()
|
|
741
|
+
this.#eventGeneratorPlugins.clear()
|
|
742
|
+
this.#transforms.dispose()
|
|
743
|
+
// Release resolver closures. The driver is rebuilt for each build() call
|
|
744
|
+
// so there is no value in retaining these maps after disposal.
|
|
745
|
+
this.#resolvers.clear()
|
|
746
|
+
this.#defaultResolvers.clear()
|
|
747
|
+
// Release the FileNode cache and parsed adapter graph so memory is reclaimed
|
|
748
|
+
// between builds. The returned `BuildOutput.files` array still references any
|
|
749
|
+
// FileNodes the caller needs to inspect.
|
|
750
|
+
this.fileManager.dispose()
|
|
751
|
+
this.inputNode = null
|
|
752
|
+
this.#adapterSource = null
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
[Symbol.dispose](): void {
|
|
756
|
+
this.dispose()
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
#getDefaultResolver = memoize(
|
|
760
|
+
this.#defaultResolvers,
|
|
761
|
+
(pluginName: string): Resolver => defineResolver<PluginFactoryOptions>(() => ({ name: 'default', pluginName })),
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Merges `partial` with the plugin's default resolver and stores the result.
|
|
766
|
+
* Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver`
|
|
767
|
+
* get the up-to-date resolver without going through `getResolver()`.
|
|
768
|
+
*/
|
|
769
|
+
setPluginResolver(pluginName: string, partial: Partial<Resolver>): void {
|
|
770
|
+
const defaultResolver = this.#getDefaultResolver(pluginName)
|
|
771
|
+
const merged = { ...defaultResolver, ...partial }
|
|
772
|
+
this.#resolvers.set(pluginName, merged)
|
|
773
|
+
const plugin = this.plugins.get(pluginName)
|
|
774
|
+
if (plugin) {
|
|
775
|
+
plugin.resolver = merged
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Returns the resolver for the given plugin.
|
|
781
|
+
*
|
|
782
|
+
* Resolution order: dynamic resolver set via `setPluginResolver` → static resolver on the
|
|
783
|
+
* plugin → lazily created default resolver (identity name, no path transforms).
|
|
784
|
+
*/
|
|
785
|
+
getResolver<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Kubb.PluginRegistry[TName]['resolver']
|
|
786
|
+
getResolver<TResolver extends Resolver = Resolver>(pluginName: string): TResolver
|
|
787
|
+
getResolver(pluginName: string): Resolver {
|
|
788
|
+
return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#getDefaultResolver(pluginName)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
getContext<TOptions extends PluginFactoryOptions>(plugin: NormalizedPlugin<TOptions>): Omit<GeneratorContext<TOptions>, 'options'> {
|
|
792
|
+
const driver = this
|
|
793
|
+
|
|
794
|
+
// Collect into the active build only. The host renders each collected diagnostic once after the
|
|
795
|
+
// build (the CLI via `Diagnostics.emit`, the agent via its post-build loop), so emitting a live
|
|
796
|
+
// `kubb:error`/`kubb:warn`/`kubb:info` here would render it twice.
|
|
797
|
+
const report = (diagnostic: Omit<ProblemDiagnostic, 'plugin'>): void => {
|
|
798
|
+
Diagnostics.report({ ...diagnostic, plugin: plugin.name })
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
config: driver.config,
|
|
803
|
+
get root(): string {
|
|
804
|
+
return resolve(driver.config.root, driver.config.output.path)
|
|
805
|
+
},
|
|
806
|
+
getMode(output: { path: string }): 'single' | 'split' {
|
|
807
|
+
return KubbDriver.getMode(resolve(driver.config.root, driver.config.output.path, output.path))
|
|
808
|
+
},
|
|
809
|
+
hooks: driver.hooks,
|
|
810
|
+
plugin,
|
|
811
|
+
getPlugin: driver.getPlugin.bind(driver),
|
|
812
|
+
requirePlugin: driver.requirePlugin.bind(driver),
|
|
813
|
+
getResolver: driver.getResolver.bind(driver),
|
|
814
|
+
driver,
|
|
815
|
+
addFile: async (...files: Array<FileNode>) => {
|
|
816
|
+
driver.fileManager.add(...files)
|
|
817
|
+
},
|
|
818
|
+
upsertFile: async (...files: Array<FileNode>) => {
|
|
819
|
+
driver.fileManager.upsert(...files)
|
|
820
|
+
},
|
|
821
|
+
get meta(): InputMeta {
|
|
822
|
+
return driver.inputNode?.meta ?? { circularNames: [], enumNames: [] }
|
|
823
|
+
},
|
|
824
|
+
get adapter(): Adapter {
|
|
825
|
+
// Generators only read `adapter` during AST hooks, which run after the
|
|
826
|
+
// adapter is set, so it is guaranteed defined at read time.
|
|
827
|
+
return driver.adapter!
|
|
828
|
+
},
|
|
829
|
+
get resolver() {
|
|
830
|
+
return driver.getResolver(plugin.name)
|
|
831
|
+
},
|
|
832
|
+
get transformer() {
|
|
833
|
+
return driver.#transforms.get(plugin.name)
|
|
834
|
+
},
|
|
835
|
+
warn(message: string) {
|
|
836
|
+
report({ code: Diagnostics.code.pluginWarning, severity: 'warning', message })
|
|
837
|
+
},
|
|
838
|
+
error(error: string | Error) {
|
|
839
|
+
const cause = typeof error === 'string' ? undefined : error
|
|
840
|
+
report({ code: Diagnostics.code.pluginFailed, severity: 'error', message: typeof error === 'string' ? error : error.message, cause })
|
|
841
|
+
},
|
|
842
|
+
info(message: string) {
|
|
843
|
+
report({ code: Diagnostics.code.pluginInfo, severity: 'info', message })
|
|
844
|
+
},
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
getPlugin<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined
|
|
849
|
+
getPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(pluginName: string): Plugin<TOptions> | undefined
|
|
850
|
+
getPlugin(pluginName: string): Plugin | undefined {
|
|
851
|
+
return this.plugins.get(pluginName)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Like `getPlugin` but throws a descriptive error when the plugin is not found.
|
|
856
|
+
*/
|
|
857
|
+
requirePlugin<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Plugin<Kubb.PluginRegistry[TName]>
|
|
858
|
+
requirePlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(pluginName: string): Plugin<TOptions>
|
|
859
|
+
requirePlugin(pluginName: string): Plugin {
|
|
860
|
+
const plugin = this.plugins.get(pluginName)
|
|
861
|
+
if (!plugin) {
|
|
862
|
+
throw new Diagnostics.Error({
|
|
863
|
+
code: Diagnostics.code.pluginNotFound,
|
|
864
|
+
severity: 'error',
|
|
865
|
+
message: `Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`,
|
|
866
|
+
help: `Add "${pluginName}" to the \`plugins\` array in kubb.config.ts, or remove the dependency on it.`,
|
|
867
|
+
location: { kind: 'config' },
|
|
868
|
+
})
|
|
869
|
+
}
|
|
870
|
+
return plugin
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function inputToAdapterSource(config: Config): AdapterSource {
|
|
875
|
+
const input = config.input
|
|
876
|
+
if (!input) {
|
|
877
|
+
throw new Diagnostics.Error({
|
|
878
|
+
code: Diagnostics.code.inputRequired,
|
|
879
|
+
severity: 'error',
|
|
880
|
+
message: 'An adapter is configured without an input.',
|
|
881
|
+
help: 'Provide `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config.',
|
|
882
|
+
location: { kind: 'config' },
|
|
883
|
+
})
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if ('data' in input) {
|
|
887
|
+
return { type: 'data', data: input.data }
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (new URLPath(input.path).isURL) {
|
|
891
|
+
return { type: 'path', path: input.path }
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const resolved = resolve(config.root, input.path)
|
|
895
|
+
|
|
896
|
+
return { type: 'path', path: resolved }
|
|
897
|
+
}
|