@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.
Files changed (56) hide show
  1. package/LICENSE +17 -10
  2. package/README.md +25 -158
  3. package/dist/diagnostics-B-UZnFqP.d.ts +2906 -0
  4. package/dist/index.cjs +2497 -1071
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.ts +80 -273
  7. package/dist/index.js +2487 -1067
  8. package/dist/index.js.map +1 -1
  9. package/dist/memoryStorage-CUj1hrxa.cjs +823 -0
  10. package/dist/memoryStorage-CUj1hrxa.cjs.map +1 -0
  11. package/dist/memoryStorage-CWFzAz4o.js +714 -0
  12. package/dist/memoryStorage-CWFzAz4o.js.map +1 -0
  13. package/dist/mocks.cjs +79 -19
  14. package/dist/mocks.cjs.map +1 -1
  15. package/dist/mocks.d.ts +35 -9
  16. package/dist/mocks.js +80 -22
  17. package/dist/mocks.js.map +1 -1
  18. package/package.json +8 -28
  19. package/src/FileManager.ts +86 -64
  20. package/src/FileProcessor.ts +170 -44
  21. package/src/KubbDriver.ts +908 -0
  22. package/src/Transform.ts +75 -0
  23. package/src/constants.ts +111 -20
  24. package/src/createAdapter.ts +112 -17
  25. package/src/createKubb.ts +140 -517
  26. package/src/createRenderer.ts +43 -28
  27. package/src/createReporter.ts +134 -0
  28. package/src/createStorage.ts +36 -23
  29. package/src/defineGenerator.ts +147 -17
  30. package/src/defineParser.ts +30 -12
  31. package/src/definePlugin.ts +370 -21
  32. package/src/defineResolver.ts +402 -212
  33. package/src/diagnostics.ts +662 -0
  34. package/src/index.ts +8 -8
  35. package/src/mocks.ts +91 -20
  36. package/src/reporters/cliReporter.ts +89 -0
  37. package/src/reporters/fileReporter.ts +103 -0
  38. package/src/reporters/jsonReporter.ts +20 -0
  39. package/src/reporters/report.ts +85 -0
  40. package/src/storages/fsStorage.ts +23 -55
  41. package/src/types.ts +411 -887
  42. package/dist/PluginDriver-BkTRD2H2.js +0 -946
  43. package/dist/PluginDriver-BkTRD2H2.js.map +0 -1
  44. package/dist/PluginDriver-Cadu4ORh.cjs +0 -1037
  45. package/dist/PluginDriver-Cadu4ORh.cjs.map +0 -1
  46. package/dist/types-DVPKmzw_.d.ts +0 -2159
  47. package/src/Kubb.ts +0 -300
  48. package/src/PluginDriver.ts +0 -426
  49. package/src/defineLogger.ts +0 -19
  50. package/src/defineMiddleware.ts +0 -62
  51. package/src/devtools.ts +0 -59
  52. package/src/renderNode.ts +0 -35
  53. package/src/utils/diagnostics.ts +0 -18
  54. package/src/utils/isInputPath.ts +0 -10
  55. package/src/utils/packageJSON.ts +0 -99
  56. /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/src/createKubb.ts CHANGED
@@ -1,573 +1,196 @@
1
1
  import { resolve } from 'node:path'
2
- import { AsyncEventEmitter, BuildError, exists, formatMs, getElapsedMs, URLPath } from '@internals/utils'
3
- import type { FileNode, OperationNode } from '@kubb/ast'
4
- import { collectUsedSchemaNames, transform, walk } from '@kubb/ast'
5
- import { DEFAULT_BANNER, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL } from './constants.ts'
6
- import type { RendererFactory } from './createRenderer.ts'
7
- import type { Generator } from './defineGenerator.ts'
8
- import type { Parser } from './defineParser.ts'
9
- import type { Plugin } from './definePlugin.ts'
10
- import { FileProcessor } from './FileProcessor.ts'
11
- import type { Kubb } from './Kubb.ts'
12
- import { PluginDriver } from './PluginDriver.ts'
13
- import { applyHookResult } from './renderNode.ts'
2
+ import { AsyncEventEmitter, BuildError } from '@internals/utils'
3
+ import { HOOK_LISTENERS_PER_PLUGIN } from './constants.ts'
4
+ import { Diagnostics } from './diagnostics.ts'
5
+ import { createStorage, type Storage } from './createStorage.ts'
6
+ import { KubbDriver } from './KubbDriver.ts'
14
7
  import { fsStorage } from './storages/fsStorage.ts'
15
- import type { AdapterSource, Config, GeneratorContext, KubbHooks, Middleware, NormalizedPlugin, Storage, UserConfig } from './types.ts'
16
- import { getDiagnosticInfo } from './utils/diagnostics.ts'
17
- import { isInputPath } from './utils/isInputPath.ts'
18
-
19
- type SetupOptions = {
20
- hooks?: AsyncEventEmitter<KubbHooks>
21
- }
8
+ import type { BuildOutput, Config, KubbHooks, UserConfig } from './types.ts'
22
9
 
23
10
  /**
24
- * Full output produced by a successful or failed build.
11
+ * Builds a `Storage` view scoped to the file paths produced by the current build.
12
+ * Reads delegate to the underlying `storage` so source bytes stay where they were
13
+ * written. Writes register the key so subsequent reads and `getKeys` are scoped
14
+ * to this build's output.
25
15
  */
26
- export type BuildOutput = {
27
- /**
28
- * Plugins that threw during installation, paired with the caught error.
29
- */
30
- failedPlugins: Set<{ plugin: Plugin; error: Error }>
31
- files: Array<FileNode>
32
- driver: PluginDriver
33
- /**
34
- * Elapsed time in milliseconds for each plugin, keyed by plugin name.
35
- */
36
- pluginTimings: Map<string, number>
37
- error?: Error
38
- /**
39
- * Raw generated source, keyed by absolute file path.
40
- */
41
- sources: Map<string, string>
42
- }
16
+ function createSourcesView(storage: Storage): Storage {
17
+ const paths = new Set<string>()
43
18
 
44
- type SetupResult = {
45
- hooks: AsyncEventEmitter<KubbHooks>
46
- driver: PluginDriver
47
- sources: Map<string, string>
48
- config: Config
49
- storage: Storage | null
19
+ return createStorage(() => ({
20
+ name: `${storage.name}:sources`,
21
+ async hasItem(key: string) {
22
+ return paths.has(key) && (await storage.hasItem(key))
23
+ },
24
+ async getItem(key: string) {
25
+ return paths.has(key) ? storage.getItem(key) : null
26
+ },
27
+ async setItem(key: string, value: string) {
28
+ paths.add(key)
29
+ await storage.setItem(key, value)
30
+ },
31
+ async removeItem(key: string) {
32
+ paths.delete(key)
33
+ await storage.removeItem(key)
34
+ },
35
+ async getKeys(base?: string) {
36
+ if (!base) return [...paths]
37
+ const result: Array<string> = []
38
+ for (const key of paths) {
39
+ if (key.startsWith(base)) result.push(key)
40
+ }
41
+ return result
42
+ },
43
+ async clear() {
44
+ paths.clear()
45
+ await storage.clear()
46
+ },
47
+ }))()
50
48
  }
51
49
 
52
- async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promise<SetupResult> {
53
- const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
54
-
55
- const sources: Map<string, string> = new Map<string, string>()
56
- const diagnosticInfo = getDiagnosticInfo()
57
-
58
- if (Array.isArray(userConfig.input)) {
59
- await hooks.emit('kubb:warn', { message: 'This feature is still under development — use with caution' })
60
- }
61
-
62
- await hooks.emit('kubb:debug', {
63
- date: new Date(),
64
- logs: [
65
- 'Configuration:',
66
- ` • Name: ${userConfig.name || 'unnamed'}`,
67
- ` • Root: ${userConfig.root || process.cwd()}`,
68
- ` • Output: ${userConfig.output?.path || 'not specified'}`,
69
- ` • Plugins: ${userConfig.plugins?.length || 0}`,
70
- 'Output Settings:',
71
- ` • Storage: ${userConfig.storage ? `custom(${userConfig.storage.name})` : userConfig.output?.write === false ? 'disabled' : 'filesystem (default)'}`,
72
- ` • Formatter: ${userConfig.output?.format || 'none'}`,
73
- ` • Linter: ${userConfig.output?.lint || 'none'}`,
74
- 'Environment:',
75
- Object.entries(diagnosticInfo)
76
- .map(([key, value]) => ` • ${key}: ${value}`)
77
- .join('\n'),
78
- ],
79
- })
80
-
81
- try {
82
- if (isInputPath(userConfig) && !new URLPath(userConfig.input.path).isURL) {
83
- await exists(userConfig.input.path)
84
-
85
- await hooks.emit('kubb:debug', {
86
- date: new Date(),
87
- logs: [`✓ Input file validated: ${userConfig.input.path}`],
88
- })
89
- }
90
- } catch (caughtError) {
91
- if (isInputPath(userConfig)) {
92
- const error = caughtError as Error
93
-
94
- throw new Error(
95
- `Cannot read file/URL defined in \`input.path\` or set with \`kubb generate PATH\` in the CLI of your Kubb config ${userConfig.input.path}`,
96
- {
97
- cause: error,
98
- },
99
- )
100
- }
101
- }
102
-
103
- const config: Config = {
50
+ function resolveConfig(userConfig: UserConfig): Config {
51
+ return {
104
52
  ...userConfig,
105
53
  root: userConfig.root || process.cwd(),
106
54
  parsers: userConfig.parsers ?? [],
107
- adapter: userConfig.adapter,
108
55
  output: {
109
56
  format: false,
110
57
  lint: false,
111
- write: true,
112
- extension: DEFAULT_EXTENSION,
113
- defaultBanner: DEFAULT_BANNER,
58
+ extension: { '.ts': '.ts' },
59
+ defaultBanner: 'simple',
114
60
  ...userConfig.output,
115
61
  },
116
- devtools: userConfig.devtools
117
- ? {
118
- studioUrl: DEFAULT_STUDIO_URL,
119
- ...(typeof userConfig.devtools === 'boolean' ? {} : userConfig.devtools),
120
- }
121
- : undefined,
122
- plugins: (userConfig.plugins ?? []) as unknown as Config['plugins'],
123
- }
124
-
125
- const storage: Storage | null = config.output.write === false ? null : (config.storage ?? fsStorage())
126
-
127
- if (config.output.clean) {
128
- await hooks.emit('kubb:debug', {
129
- date: new Date(),
130
- logs: ['Cleaning output directories', ` • Output: ${config.output.path}`],
131
- })
132
- await storage?.clear(resolve(config.root, config.output.path))
133
- }
134
-
135
- const driver = new PluginDriver(config, {
136
- hooks,
137
- })
138
-
139
- // Register middleware hooks after all plugin hooks are registered.
140
- // Because AsyncEventEmitter calls listeners in registration order,
141
- // middleware hooks for any event fire after all plugin hooks for that event.
142
- function registerMiddlewareHook<K extends keyof KubbHooks & string>(event: K, middlewareHooks: Middleware['hooks']) {
143
- const handler = middlewareHooks[event]
144
- if (handler) {
145
- hooks.on(event, handler)
146
- }
147
- }
148
-
149
- for (const middleware of config.middleware ?? []) {
150
- for (const event of Object.keys(middleware.hooks) as Array<keyof KubbHooks & string>) {
151
- registerMiddlewareHook(event, middleware.hooks)
152
- }
153
- }
154
-
155
- if (config.adapter) {
156
- const source = inputToAdapterSource(config)
157
-
158
- await hooks.emit('kubb:debug', {
159
- date: new Date(),
160
- logs: [`Running adapter: ${config.adapter.name}`],
161
- })
162
-
163
- driver.adapter = config.adapter
164
- driver.inputNode = await config.adapter.parse(source)
165
-
166
- await hooks.emit('kubb:debug', {
167
- date: new Date(),
168
- logs: [
169
- `✓ Adapter '${config.adapter.name}' resolved InputNode`,
170
- ` • Schemas: ${driver.inputNode.schemas.length}`,
171
- ` • Operations: ${driver.inputNode.operations.length}`,
172
- ],
173
- })
62
+ storage: userConfig.storage ?? fsStorage(),
63
+ reporters: userConfig.reporters ?? [],
64
+ plugins: userConfig.plugins ?? [],
174
65
  }
66
+ }
175
67
 
176
- return {
177
- config,
178
- hooks,
179
- driver,
180
- sources,
181
- storage,
182
- }
68
+ type CreateKubbOptions = {
69
+ hooks?: AsyncEventEmitter<KubbHooks>
183
70
  }
184
71
 
185
72
  /**
186
- * Walks the AST and dispatches nodes to a plugin's direct AST hooks
187
- * (`schema`, `operation`, `operations`).
73
+ * Kubb code-generation instance bound to a single config entry. Resolves the user
74
+ * config in the constructor, so `config` is available right away, and shares `hooks`,
75
+ * `storage`, and `driver` across the `setup → build` lifecycle.
76
+ *
77
+ * `createKubb` takes a plain, serializable config object (the shape `defineConfig`
78
+ * produces), not a fluent builder. Config stays plain data so it can be cache
79
+ * fingerprinted and validated against the shipped JSON schema.
188
80
  *
189
- * When `include` contains only operation-scoped filters (`tag`, `operationId`, `path`,
190
- * `method`, `contentType`) and no `schemaName` filter, the function pre-computes the set
191
- * of top-level schema names transitively reachable from the included operations and skips
192
- * schemas that fall outside that set. This ensures that component schemas referenced
193
- * exclusively by excluded operations are not generated.
81
+ * Attach event listeners to `.hooks` before calling `setup()` or `build()`.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const kubb = createKubb(userConfig)
86
+ * kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => console.log(plugin.name, duration))
87
+ * const { files, diagnostics } = await kubb.safeBuild()
88
+ * ```
194
89
  */
195
- async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorContext): Promise<void> {
196
- const { adapter, inputNode, resolver, driver } = context
197
- const { exclude, include, override } = plugin.options
90
+ export class Kubb {
91
+ readonly hooks: AsyncEventEmitter<KubbHooks>
92
+ readonly config: Config
93
+ #driver: KubbDriver | null = null
94
+ #storage: Storage | null = null
198
95
 
199
- if (!adapter || !inputNode) {
200
- throw new Error(`[${plugin.name}] No adapter found. Add an OAS adapter (e.g. pluginOas()) before this plugin in your Kubb config.`)
96
+ constructor(userConfig: UserConfig, options: CreateKubbOptions = {}) {
97
+ this.config = resolveConfig(userConfig)
98
+ this.hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
201
99
  }
202
100
 
203
- function resolveRenderer(gen: Generator): RendererFactory | undefined {
204
- return gen.renderer === null ? undefined : (gen.renderer ?? plugin.renderer ?? context.config.renderer)
101
+ get storage(): Storage {
102
+ if (!this.#storage) throw new Error('[kubb] setup() must be called before accessing storage')
103
+ return this.#storage
205
104
  }
206
105
 
207
- const generators = plugin.generators ?? []
208
- const collectedOperations: Array<OperationNode> = []
209
-
210
- const generatorContext = {
211
- ...context,
212
- resolver: driver.getResolver(plugin.name),
213
- }
214
-
215
- // When `include` has operation-based filters (tag, operationId, path, method, contentType)
216
- // but no schema-level filters (schemaName), pre-compute the set of top-level schema names
217
- // that are transitively referenced by the included operations. Schemas outside that set are
218
- // skipped so that types belonging exclusively to excluded operations are not generated.
219
- const operationFilterTypes = new Set(['tag', 'operationId', 'path', 'method', 'contentType'])
220
- const hasOperationBasedIncludes = include?.some(({ type }) => operationFilterTypes.has(type)) ?? false
221
- const hasSchemaNameIncludes = include?.some(({ type }) => type === 'schemaName') ?? false
222
-
223
- let allowedSchemaNames: Set<string> | undefined
224
- if (hasOperationBasedIncludes && !hasSchemaNameIncludes) {
225
- const includedOps = inputNode.operations.filter((op) => resolver.resolveOptions(op, { options: plugin.options, exclude, include, override }) !== null)
226
- allowedSchemaNames = collectUsedSchemaNames(includedOps, inputNode.schemas)
227
- }
228
-
229
- await walk(inputNode, {
230
- depth: 'shallow',
231
- async schema(node) {
232
- const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
233
-
234
- // Skip named top-level schemas that are not reachable from any included operation.
235
- if (allowedSchemaNames !== undefined && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) {
236
- return
237
- }
238
-
239
- const options = resolver.resolveOptions(transformedNode, {
240
- options: plugin.options,
241
- exclude,
242
- include,
243
- override,
244
- })
245
- if (options === null) return
246
-
247
- const ctx = { ...generatorContext, options }
248
-
249
- for (const gen of generators) {
250
- if (!gen.schema) continue
251
- const result = await gen.schema(transformedNode, ctx)
252
- await applyHookResult(result, driver, resolveRenderer(gen))
253
- }
254
-
255
- await driver.hooks.emit('kubb:generate:schema', transformedNode, ctx)
256
- },
257
- async operation(node) {
258
- const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
259
- const options = resolver.resolveOptions(transformedNode, {
260
- options: plugin.options,
261
- exclude,
262
- include,
263
- override,
264
- })
265
- if (options !== null) {
266
- collectedOperations.push(transformedNode)
267
-
268
- const ctx = { ...generatorContext, options }
269
-
270
- for (const gen of generators) {
271
- if (!gen.operation) continue
272
- const result = await gen.operation(transformedNode, ctx)
273
- await applyHookResult(result, driver, resolveRenderer(gen))
274
- }
275
-
276
- await driver.hooks.emit('kubb:generate:operation', transformedNode, ctx)
277
- }
278
- },
279
- })
280
-
281
- if (collectedOperations.length > 0) {
282
- const ctx = { ...generatorContext, options: plugin.options }
283
-
284
- for (const gen of generators) {
285
- if (!gen.operations) continue
286
- const result = await gen.operations(collectedOperations, ctx)
287
- await applyHookResult(result, driver, resolveRenderer(gen))
288
- }
289
-
290
- await driver.hooks.emit('kubb:generate:operations', collectedOperations, ctx)
106
+ get driver(): KubbDriver {
107
+ if (!this.#driver) throw new Error('[kubb] setup() must be called before accessing driver')
108
+ return this.#driver
291
109
  }
292
- }
293
-
294
- async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
295
- const { driver, hooks, sources, storage } = setupResult
296
-
297
- const failedPlugins = new Set<{ plugin: Plugin; error: Error }>()
298
- const pluginTimings = new Map<string, number>()
299
- const config = driver.config
300
-
301
- try {
302
- await driver.emitSetupHooks()
303
-
304
- if (driver.adapter && driver.inputNode) {
305
- await hooks.emit('kubb:build:start', {
306
- config,
307
- adapter: driver.adapter,
308
- inputNode: driver.inputNode,
309
- getPlugin: driver.getPlugin.bind(driver),
310
- get files() {
311
- return driver.fileManager.files
312
- },
313
- upsertFile: (...files) => driver.fileManager.upsert(...files),
314
- })
315
- }
316
-
317
- for (const plugin of driver.plugins.values()) {
318
- const context = driver.getContext(plugin)
319
- const hrStart = process.hrtime()
320
-
321
- try {
322
- const timestamp = new Date()
323
-
324
- await hooks.emit('kubb:plugin:start', { plugin })
325
-
326
- await hooks.emit('kubb:debug', {
327
- date: timestamp,
328
- logs: ['Starting plugin...', ` • Plugin Name: ${plugin.name}`],
329
- })
330
-
331
- if (plugin.generators?.length || driver.hasRegisteredGenerators(plugin.name)) {
332
- await runPluginAstHooks(plugin, context)
333
- }
334
110
 
335
- const duration = getElapsedMs(hrStart)
336
- pluginTimings.set(plugin.name, duration)
337
-
338
- await hooks.emit('kubb:plugin:end', {
339
- plugin,
340
- duration,
341
- success: true,
342
- config,
343
- get files() {
344
- return driver.fileManager.files
345
- },
346
- upsertFile: (...files) => driver.fileManager.upsert(...files),
347
- })
348
-
349
- await hooks.emit('kubb:debug', {
350
- date: new Date(),
351
- logs: [`✓ Plugin started successfully (${formatMs(duration)})`],
352
- })
353
- } catch (caughtError) {
354
- const error = caughtError as Error
355
- const errorTimestamp = new Date()
356
- const duration = getElapsedMs(hrStart)
357
-
358
- await hooks.emit('kubb:plugin:end', {
359
- plugin,
360
- duration,
361
- success: false,
362
- error,
363
- config,
364
- get files() {
365
- return driver.fileManager.files
366
- },
367
- upsertFile: (...files) => driver.fileManager.upsert(...files),
368
- })
369
-
370
- await hooks.emit('kubb:debug', {
371
- date: errorTimestamp,
372
- logs: [
373
- '✗ Plugin start failed',
374
- ` • Plugin Name: ${plugin.name}`,
375
- ` • Error: ${error.constructor.name} - ${error.message}`,
376
- ' • Stack Trace:',
377
- error.stack || 'No stack trace available',
378
- ],
379
- })
380
-
381
- failedPlugins.add({ plugin, error })
382
- }
383
- }
384
-
385
- await hooks.emit('kubb:plugins:end', {
386
- config,
387
- get files() {
388
- return driver.fileManager.files
389
- },
390
- upsertFile: (...files) => driver.fileManager.upsert(...files),
391
- })
392
-
393
- const files = driver.fileManager.files
394
-
395
- const parsersMap = new Map<FileNode['extname'], Parser>()
396
- for (const parser of config.parsers) {
397
- if (parser.extNames) {
398
- for (const extname of parser.extNames) {
399
- parsersMap.set(extname, parser)
400
- }
401
- }
402
- }
403
-
404
- const fileProcessor = new FileProcessor()
405
-
406
- await hooks.emit('kubb:debug', {
407
- date: new Date(),
408
- logs: [`Writing ${files.length} files...`],
409
- })
410
-
411
- await fileProcessor.run(files, {
412
- parsers: parsersMap,
413
- extension: config.output.extension,
414
- onStart: async (processingFiles) => {
415
- await hooks.emit('kubb:files:processing:start', { files: processingFiles })
416
- },
417
- onUpdate: async ({ file, source, processed, total, percentage }) => {
418
- await hooks.emit('kubb:file:processing:update', {
419
- file,
420
- source,
421
- processed,
422
- total,
423
- percentage,
424
- config,
425
- })
426
- if (source) {
427
- await storage?.setItem(file.path, source)
428
- sources.set(file.path, source)
429
- }
430
- },
431
- onEnd: async (processedFiles) => {
432
- await hooks.emit('kubb:files:processing:end', { files: processedFiles })
433
- await hooks.emit('kubb:debug', {
434
- date: new Date(),
435
- logs: [`✓ File write process completed for ${processedFiles.length} files`],
436
- })
437
- },
438
- })
439
-
440
- await hooks.emit('kubb:build:end', {
441
- files,
442
- config,
443
- outputDir: resolve(config.root, config.output.path),
444
- })
445
-
446
- return {
447
- failedPlugins,
448
- files,
449
- driver,
450
- pluginTimings,
451
- sources,
452
- }
453
- } catch (error) {
454
- return {
455
- failedPlugins,
456
- files: [],
457
- driver,
458
- pluginTimings,
459
- error: error as Error,
460
- sources,
111
+ /**
112
+ * Initializes the driver and storage. `build()` calls this automatically.
113
+ */
114
+ async setup(): Promise<void> {
115
+ const config = this.config
116
+ const driver = new KubbDriver(config, { hooks: this.hooks })
117
+ const storage = createSourcesView(config.storage)
118
+
119
+ // Each generator a plugin registers adds a listener to the shared hooks emitter, so size the
120
+ // ceiling to the plugin count. Without this, a multi-generator plugin set trips Node's
121
+ // EventEmitter leak warning at the default 10.
122
+ this.hooks.setMaxListeners(Math.max(10, config.plugins.length * HOOK_LISTENERS_PER_PLUGIN))
123
+
124
+ if (config.output.clean) {
125
+ await config.storage.clear(resolve(config.root, config.output.path))
461
126
  }
462
- } finally {
463
- driver.dispose()
464
- }
465
- }
466
127
 
467
- async function build(setupResult: SetupResult): Promise<BuildOutput> {
468
- const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(setupResult)
128
+ await driver.setup()
469
129
 
470
- if (error) {
471
- throw error
130
+ this.#driver = driver
131
+ this.#storage = storage
472
132
  }
473
133
 
474
- if (failedPlugins.size > 0) {
475
- const errors = [...failedPlugins].map(({ error }) => error)
476
-
477
- throw new BuildError(`Build Error with ${failedPlugins.size} failed plugins`, { errors })
478
- }
479
-
480
- return {
481
- failedPlugins,
482
- files,
483
- driver,
484
- pluginTimings,
485
- error: undefined,
486
- sources,
134
+ /**
135
+ * Runs the full pipeline and throws on any plugin error.
136
+ * Automatically calls `setup()` if needed.
137
+ */
138
+ async build(): Promise<BuildOutput> {
139
+ const out = await this.safeBuild()
140
+ if (Diagnostics.hasError(out.diagnostics)) {
141
+ const errors = out.diagnostics
142
+ .filter(Diagnostics.isProblem)
143
+ .filter((diagnostic) => diagnostic.severity === 'error')
144
+ .map((diagnostic) => diagnostic.cause ?? new Diagnostics.Error(diagnostic))
145
+ throw new BuildError(`Build failed with ${errors.length} ${errors.length === 1 ? 'error' : 'errors'}`, { errors })
146
+ }
147
+ return out
487
148
  }
488
- }
489
149
 
490
- function inputToAdapterSource(config: Config): AdapterSource {
491
- const input = config.input
492
- if (!input) {
493
- throw new Error('[kubb] input is required when using an adapter. Provide input.path or input.data in your config.')
494
- }
150
+ /**
151
+ * Runs the full pipeline and captures errors in `BuildOutput` instead of throwing.
152
+ * Automatically calls `setup()` if needed. This is the canonical call: it never throws on
153
+ * plugin errors, so callers stay in control of how failures surface.
154
+ */
155
+ async safeBuild(): Promise<BuildOutput> {
156
+ if (!this.#driver) await this.setup()
157
+ using cleanup = this
158
+ const driver = cleanup.driver
159
+ const storage = cleanup.storage
160
+ const { diagnostics } = await driver.run({ storage })
495
161
 
496
- if (Array.isArray(input)) {
497
- return {
498
- type: 'paths',
499
- paths: input.map((i) => (new URLPath(i.path).isURL ? i.path : resolve(config.root, i.path))),
500
- }
162
+ return { diagnostics, files: driver.fileManager.files, driver, storage }
501
163
  }
502
164
 
503
- if ('data' in input) {
504
- return { type: 'data', data: input.data }
165
+ dispose(): void {
166
+ this.#driver?.dispose()
505
167
  }
506
168
 
507
- if (new URLPath(input.path).isURL) {
508
- return { type: 'path', path: input.path }
169
+ [Symbol.dispose](): void {
170
+ this.dispose()
509
171
  }
510
-
511
- const resolved = resolve(config.root, input.path)
512
- return { type: 'path', path: resolved }
513
- }
514
-
515
- type CreateKubbOptions = {
516
- hooks?: AsyncEventEmitter<KubbHooks>
517
172
  }
518
173
 
519
174
  /**
520
- * Creates a Kubb instance bound to a single config entry.
521
- *
522
- * Accepts a user-facing config shape and resolves it to a full {@link Config} during
523
- * `setup()`. The instance then holds shared state (`hooks`, `sources`, `driver`, `config`)
524
- * across the `setup → build` lifecycle. Attach event listeners to `kubb.hooks` before
525
- * calling `setup()` or `build()`.
175
+ * Constructs a {@link Kubb} build orchestrator from a user config. Equivalent
176
+ * to `new Kubb(userConfig, options)` and the canonical public entry point.
526
177
  *
527
178
  * @example
528
179
  * ```ts
529
- * const kubb = createKubb(userConfig)
180
+ * import { createKubb } from '@kubb/core'
181
+ * import { adapterOas } from '@kubb/adapter-oas'
182
+ * import { pluginTs } from '@kubb/plugin-ts'
530
183
  *
531
- * kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => {
532
- * console.log(`${plugin.name} completed in ${duration}ms`)
184
+ * const kubb = createKubb({
185
+ * input: { path: './petStore.yaml' },
186
+ * output: { path: './src/gen' },
187
+ * adapter: adapterOas(),
188
+ * plugins: [pluginTs()],
533
189
  * })
534
190
  *
535
- * const { files, failedPlugins } = await kubb.safeBuild()
191
+ * await kubb.build()
536
192
  * ```
537
193
  */
538
194
  export function createKubb(userConfig: UserConfig, options: CreateKubbOptions = {}): Kubb {
539
- const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
540
- let setupResult: SetupResult | undefined
541
-
542
- const instance: Kubb = {
543
- get hooks() {
544
- return hooks
545
- },
546
- get sources() {
547
- return setupResult?.sources ?? new Map()
548
- },
549
- get driver() {
550
- return setupResult?.driver
551
- },
552
- get config() {
553
- return setupResult?.config
554
- },
555
- async setup() {
556
- setupResult = await setup(userConfig, { hooks })
557
- },
558
- async build() {
559
- if (!setupResult) {
560
- await instance.setup()
561
- }
562
- return build(setupResult!)
563
- },
564
- async safeBuild() {
565
- if (!setupResult) {
566
- await instance.setup()
567
- }
568
- return safeBuild(setupResult!)
569
- },
570
- }
571
-
572
- return instance
195
+ return new Kubb(userConfig, options)
573
196
  }