@kubb/core 5.0.0-beta.3 → 5.0.0-beta.31

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 (46) hide show
  1. package/README.md +8 -38
  2. package/dist/KubbDriver-CFx2DdhF.js +2131 -0
  3. package/dist/KubbDriver-CFx2DdhF.js.map +1 -0
  4. package/dist/KubbDriver-vyD7F0Ip.cjs +2252 -0
  5. package/dist/KubbDriver-vyD7F0Ip.cjs.map +1 -0
  6. package/dist/{types-CC09VtBt.d.ts → createKubb-6zii1jo-.d.ts} +1610 -1257
  7. package/dist/index.cjs +351 -1125
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +3 -186
  10. package/dist/index.js +341 -1119
  11. package/dist/index.js.map +1 -1
  12. package/dist/mocks.cjs +30 -21
  13. package/dist/mocks.cjs.map +1 -1
  14. package/dist/mocks.d.ts +5 -5
  15. package/dist/mocks.js +29 -20
  16. package/dist/mocks.js.map +1 -1
  17. package/package.json +6 -18
  18. package/src/FileManager.ts +78 -61
  19. package/src/FileProcessor.ts +48 -38
  20. package/src/KubbDriver.ts +930 -0
  21. package/src/constants.ts +11 -6
  22. package/src/createAdapter.ts +113 -17
  23. package/src/createKubb.ts +1039 -478
  24. package/src/createRenderer.ts +58 -27
  25. package/src/createStorage.ts +36 -23
  26. package/src/defineGenerator.ts +127 -15
  27. package/src/defineLogger.ts +66 -7
  28. package/src/defineMiddleware.ts +19 -17
  29. package/src/defineParser.ts +30 -13
  30. package/src/definePlugin.ts +329 -14
  31. package/src/defineResolver.ts +365 -167
  32. package/src/devtools.ts +8 -1
  33. package/src/index.ts +2 -2
  34. package/src/mocks.ts +11 -14
  35. package/src/storages/fsStorage.ts +13 -37
  36. package/src/types.ts +48 -1292
  37. package/dist/PluginDriver-BXibeQk-.cjs +0 -1036
  38. package/dist/PluginDriver-BXibeQk-.cjs.map +0 -1
  39. package/dist/PluginDriver-DV3p2Hky.js +0 -945
  40. package/dist/PluginDriver-DV3p2Hky.js.map +0 -1
  41. package/src/Kubb.ts +0 -300
  42. package/src/PluginDriver.ts +0 -424
  43. package/src/renderNode.ts +0 -35
  44. package/src/utils/diagnostics.ts +0 -18
  45. package/src/utils/isInputPath.ts +0 -10
  46. package/src/utils/packageJSON.ts +0 -99
package/src/createKubb.ts CHANGED
@@ -1,574 +1,1135 @@
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'
2
+ import { version as nodeVersion } from 'node:process'
3
+ import type { PossiblePromise } from '@internals/utils'
4
+ import { AsyncEventEmitter, BuildError, exists, URLPath } from '@internals/utils'
5
+ import type { FileNode, InputMeta, OperationNode, SchemaNode } from '@kubb/ast'
6
+ import { version as KubbVersion } from '../package.json'
5
7
  import { DEFAULT_BANNER, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL } from './constants.ts'
8
+ import type { Adapter } from './createAdapter.ts'
6
9
  import type { RendererFactory } from './createRenderer.ts'
7
- import type { Generator } from './defineGenerator.ts'
10
+ import { createStorage, type Storage } from './createStorage.ts'
11
+ import type { GeneratorContext } from './defineGenerator.ts'
12
+ import type { Middleware } from './defineMiddleware.ts'
8
13
  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'
14
+ import type { KubbPluginEndContext, KubbPluginSetupContext, KubbPluginStartContext, Plugin } from './definePlugin.ts'
15
+
16
+ import { KubbDriver } from './KubbDriver.ts'
14
17
  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
18
 
19
- type SetupOptions = {
20
- hooks?: AsyncEventEmitter<KubbHooks>
19
+ /**
20
+ * Safely extracts a type from a registry, returning `{}` if the key doesn't exist.
21
+ * Enables optional interface augmentation for `Kubb.ConfigOptionsRegistry` and `Kubb.PluginOptionsRegistry`
22
+ * without requiring changes to core.
23
+ *
24
+ * @internal
25
+ */
26
+ type ExtractRegistryKey<T, K extends PropertyKey> = K extends keyof T ? T[K] : {}
27
+
28
+ /**
29
+ * Reference to an input file to generate code from.
30
+ *
31
+ * Specify an absolute path or a path relative to the config file location.
32
+ * The adapter will parse this file (e.g., OpenAPI YAML or JSON) into the universal AST.
33
+ */
34
+ export type InputPath = {
35
+ /**
36
+ * Path to your Swagger/OpenAPI file, absolute or relative to the config file location.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * { path: './petstore.yaml' }
41
+ * { path: '/absolute/path/to/openapi.json' }
42
+ * ```
43
+ */
44
+ path: string
21
45
  }
22
46
 
23
47
  /**
24
- * Full output produced by a successful or failed build.
48
+ * Inline input data to generate code from.
49
+ *
50
+ * Useful when you want to pass the specification directly instead of from a file.
51
+ * Can be a string (YAML/JSON) or a parsed object.
25
52
  */
26
- export type BuildOutput = {
53
+ export type InputData = {
27
54
  /**
28
- * Plugins that threw during installation, paired with the caught error.
55
+ * Swagger/OpenAPI data as a string (YAML/JSON) or a parsed object.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * { data: fs.readFileSync('./openapi.yaml', 'utf8') }
60
+ * { data: { openapi: '3.1.0', info: { ... } } }
61
+ * ```
62
+ */
63
+ data: string | unknown
64
+ }
65
+
66
+ type Input = InputPath | InputData
67
+
68
+ /**
69
+ * Build configuration for Kubb code generation.
70
+ *
71
+ * The Config is the main entry point for customizing how Kubb generates code. It specifies:
72
+ * - What to generate from (adapter + input)
73
+ * - Where to output generated code (output)
74
+ * - How to generate (plugins + middleware)
75
+ * - Runtime details (parsers, storage, renderer)
76
+ *
77
+ * See `UserConfig` for a relaxed version with sensible defaults.
78
+ *
79
+ * @private
80
+ */
81
+ export type Config<TInput = Input> = {
82
+ /**
83
+ * Display name for this configuration in CLI output and logs.
84
+ * Useful when running multiple builds with `defineConfig` arrays.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * name: 'api-client'
89
+ * ```
90
+ */
91
+ name?: string
92
+ /**
93
+ * Project root directory, absolute or relative to the config file. Already
94
+ * resolved on the `Config` instance — see `UserConfig` for the optional
95
+ * form that defaults to `process.cwd()`.
96
+ */
97
+ root: string
98
+ /**
99
+ * Parsers that convert generated files into strings. Each parser handles a
100
+ * set of file extensions; a fallback parser handles anything else.
101
+ *
102
+ * Already resolved on the `Config` instance — see `UserConfig` for the
103
+ * optional form that defaults to `[parserTs, parserTsx]`.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * import { defineConfig } from 'kubb'
108
+ * import { parserTs, parserTsx } from '@kubb/parser-ts'
109
+ *
110
+ * export default defineConfig({
111
+ * parsers: [parserTs, parserTsx],
112
+ * })
113
+ * ```
114
+ */
115
+ parsers: Array<Parser>
116
+ /**
117
+ * Adapter that parses input files into the universal AST representation.
118
+ * Use `@kubb/adapter-oas` for OpenAPI/Swagger or `@kubb/adapter-asyncapi` for other formats.
119
+ *
120
+ * When omitted, Kubb runs in plugin-only mode: `kubb:plugin:setup` fires and files
121
+ * injected via `injectFile` are written, but no AST walk occurs and generator hooks
122
+ * (`kubb:generate:schema`, `kubb:generate:operation`) are never emitted.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * import { adapterOas } from '@kubb/adapter-oas'
127
+ * export default defineConfig({
128
+ * adapter: adapterOas(),
129
+ * input: { path: './petstore.yaml' },
130
+ * })
131
+ * ```
132
+ */
133
+ adapter?: Adapter
134
+ /**
135
+ * Source file or data to generate code from.
136
+ * Use `input.path` for a file path or `input.data` for inline data.
137
+ * Required when an adapter is configured; omit when running in plugin-only mode.
138
+ */
139
+ input?: TInput
140
+ output: {
141
+ /**
142
+ * Output directory for generated files, absolute or relative to `root`.
143
+ *
144
+ * All generated files will be written under this directory. Subdirectories can be created
145
+ * by plugins based on grouping strategy (by tag, path, etc.).
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * output: {
150
+ * path: './src/gen', // generates ./src/gen/api.ts, ./src/gen/types.ts, etc.
151
+ * }
152
+ * ```
153
+ */
154
+ path: string
155
+ /**
156
+ * Remove all files from the output directory before starting the build.
157
+ *
158
+ * Useful to ensure old generated files aren't mixed with new ones.
159
+ * Set to `true` for fresh builds, `false` to preserve manual edits in output dir.
160
+ *
161
+ * @default false
162
+ * @example
163
+ * ```ts
164
+ * clean: true // wipes ./src/gen/* before generating
165
+ * ```
166
+ */
167
+ clean?: boolean
168
+ /**
169
+ * Auto-format generated files after code generation completes.
170
+ *
171
+ * Applies a code formatter to all generated files. Use `'auto'` to detect which formatter
172
+ * is available on your system. Pass `false` to skip formatting (useful for CI or specific workflows).
173
+ *
174
+ * @default false
175
+ * @example
176
+ * ```ts
177
+ * format: 'auto' // auto-detect prettier, biome, or oxfmt
178
+ * format: 'prettier' // force prettier
179
+ * format: false // skip formatting
180
+ * ```
181
+ */
182
+ format?: 'auto' | 'prettier' | 'biome' | 'oxfmt' | false
183
+ /**
184
+ * Auto-lint generated files after code generation completes.
185
+ *
186
+ * Analyzes all generated files for style/correctness issues. Use `'auto'` to detect which linter
187
+ * is available on your system. Pass `false` to skip linting.
188
+ *
189
+ * @default false
190
+ * @example
191
+ * ```ts
192
+ * lint: 'auto' // auto-detect oxlint, biome, or eslint
193
+ * lint: 'eslint' // force eslint
194
+ * lint: false // skip linting
195
+ * ```
196
+ */
197
+ lint?: 'auto' | 'eslint' | 'biome' | 'oxlint' | false
198
+ /**
199
+ * Map file extensions to different output extensions.
200
+ *
201
+ * Useful when you want generated `.ts` imports to reference `.js` files or vice versa (e.g., for ESM dual packages).
202
+ * Keys are the original extension, values are the output extension. Use empty string `''` to omit extension.
203
+ *
204
+ * @default { '.ts': '.ts' }
205
+ * @example
206
+ * ```ts
207
+ * extension: { '.ts': '.js' } // generates import './api.js' instead of './api.ts'
208
+ * extension: { '.ts': '', '.tsx': '.jsx' }
209
+ * ```
210
+ */
211
+ extension?: Record<FileNode['extname'], FileNode['extname'] | ''>
212
+ /**
213
+ * Banner text prepended to every generated file.
214
+ *
215
+ * Useful for auto-generation notices or license headers. Choose a preset or write custom text.
216
+ * Use `'simple'` for a basic Kubb banner, `'full'` for detailed metadata, or `false` to omit.
217
+ *
218
+ * @default 'simple'
219
+ * @example
220
+ * ```ts
221
+ * defaultBanner: 'simple' // "This file was autogenerated by Kubb"
222
+ * defaultBanner: 'full' // adds source, title, description, API version
223
+ * defaultBanner: false // no banner
224
+ * ```
225
+ */
226
+ defaultBanner?: 'simple' | 'full' | false
227
+ /**
228
+ * When `true`, overwrites existing files. When `false`, skips generated files that already exist.
229
+ *
230
+ * Individual plugins can override this setting. This is useful for preventing accidental data loss
231
+ * when re-generating while you have local edits in the output folder.
232
+ *
233
+ * @default false
234
+ * @example
235
+ * ```ts
236
+ * override: true // regenerate everything, even existing files
237
+ * override: false // skip files that already exist
238
+ * ```
239
+ */
240
+ override?: boolean
241
+ } & ExtractRegistryKey<Kubb.ConfigOptionsRegistry, 'output'>
242
+ /**
243
+ * Storage backend that controls where and how generated files are persisted.
244
+ *
245
+ * Defaults to `fsStorage()` which writes to the file system. Pass `memoryStorage()` to keep files in RAM,
246
+ * or implement a custom `Storage` interface to write to cloud storage, databases, or other backends.
247
+ *
248
+ * @default fsStorage()
249
+ * @example
250
+ * ```ts
251
+ * import { memoryStorage } from '@kubb/core'
252
+ *
253
+ * // Keep generated files in memory (useful for testing, CI pipelines)
254
+ * storage: memoryStorage()
255
+ *
256
+ * // Use custom S3 storage
257
+ * storage: myS3Storage()
258
+ * ```
259
+ *
260
+ * @see {@link Storage} interface for implementing custom backends.
261
+ */
262
+ storage: Storage
263
+ /**
264
+ * Plugins that execute during the build to generate code and transform the AST.
265
+ *
266
+ * Each plugin processes the AST produced by the adapter and can emit files for different
267
+ * programming languages or formats (TypeScript, Zod schemas, Faker data, etc.).
268
+ * Dependencies are enforced — an error is thrown if a plugin requires another plugin that isn't registered.
269
+ *
270
+ * Plugins can declare their own options via `PluginFactoryOptions`. See plugin documentation for details.
271
+ *
272
+ * @example
273
+ * ```ts
274
+ * import { pluginTs } from '@kubb/plugin-ts'
275
+ * import { pluginZod } from '@kubb/plugin-zod'
276
+ *
277
+ * plugins: [
278
+ * pluginTs({ output: { path: './src/gen' } }),
279
+ * pluginZod({ output: { path: './src/gen' } }),
280
+ * ]
281
+ * ```
282
+ */
283
+ plugins: Array<Plugin>
284
+ /**
285
+ * Middleware instances that observe build events and post-process generated code.
286
+ *
287
+ * Middleware fires AFTER all plugins for each event. Perfect for tasks like:
288
+ * - Auditing what was generated
289
+ * - Adding barrel/index files
290
+ * - Validating output
291
+ * - Running custom transformations
292
+ *
293
+ * @example
294
+ * ```ts
295
+ * import { middlewareBarrel } from '@kubb/middleware-barrel'
296
+ *
297
+ * middleware: [middlewareBarrel()]
298
+ * ```
299
+ *
300
+ * @see {@link defineMiddleware} to create custom middleware.
301
+ */
302
+ middleware?: Array<Middleware>
303
+ /**
304
+ * Renderer that converts generated AST nodes to code strings.
305
+ *
306
+ * By default, Kubb uses the JSX renderer (`rendererJsx`). Pass a custom renderer to support
307
+ * different output formats (template engines, code generation DSLs, etc.).
308
+ *
309
+ * @default rendererJsx() // from @kubb/renderer-jsx
310
+ * @example
311
+ * ```ts
312
+ * import { rendererJsx } from '@kubb/renderer-jsx'
313
+ * renderer: rendererJsx()
314
+ * ```
315
+ *
316
+ * @see {@link Renderer} to implement a custom renderer.
317
+ */
318
+ renderer?: RendererFactory
319
+ /**
320
+ * Kubb Studio cloud integration settings.
321
+ *
322
+ * Kubb Studio (https://kubb.studio) is a web-based IDE for managing API specs and generated code.
323
+ * Set to `true` to enable with default settings, or pass an object to customize the Studio URL.
324
+ *
325
+ * @default false // disabled by default
326
+ * @example
327
+ * ```ts
328
+ * devtools: true // use default Kubb Studio
329
+ * devtools: { studioUrl: 'https://my-studio.dev' } // custom Studio instance
330
+ * ```
331
+ */
332
+ devtools?:
333
+ | true
334
+ | {
335
+ /**
336
+ * Override the Kubb Studio base URL.
337
+ * @default 'https://kubb.studio'
338
+ */
339
+ studioUrl?: typeof DEFAULT_STUDIO_URL | (string & {})
340
+ }
341
+ /**
342
+ * Lifecycle hooks that execute during or after the build process.
343
+ *
344
+ * Hooks allow you to run external tools (prettier, eslint, custom scripts) based on build events.
345
+ * Currently supports the `done` hook which fires after all plugins and middleware complete.
346
+ *
347
+ * @example
348
+ * ```ts
349
+ * hooks: {
350
+ * done: 'prettier --write "./src/gen"', // auto-format generated files
351
+ * // or multiple commands:
352
+ * done: ['prettier --write "./src/gen"', 'eslint --fix "./src/gen"']
353
+ * }
354
+ * ```
355
+ */
356
+ hooks?: {
357
+ /**
358
+ * Command(s) to run after all plugins and middleware complete generation.
359
+ *
360
+ * Useful for post-processing: formatting, linting, copying files, or custom validation.
361
+ * Pass a single command string or array of command strings to run sequentially.
362
+ * Commands are executed relative to the `root` directory.
363
+ *
364
+ * @example
365
+ * ```ts
366
+ * done: 'prettier --write "./src/gen"'
367
+ * done: ['prettier --write "./src/gen"', 'eslint --fix "./src/gen"']
368
+ * ```
369
+ */
370
+ done?: string | Array<string>
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Partial `Config` for user-facing entry points with sensible defaults.
376
+ *
377
+ * `UserConfig` is what you pass to `defineConfig()`. It has optional `root`, `plugins`, `parsers`, and `adapter`
378
+ * fields (which fall back to sensible defaults). All other Config options are available, including `output`, `input`,
379
+ * `storage`, `middleware`, `renderer`, `devtools`, and `hooks`.
380
+ *
381
+ * @example
382
+ * ```ts
383
+ * export default defineConfig({
384
+ * input: { path: './petstore.yaml' },
385
+ * output: { path: './src/gen' },
386
+ * plugins: [pluginTs(), pluginZod()],
387
+ * })
388
+ * ```
389
+ */
390
+ export type UserConfig<TInput = Input> = Omit<Config<TInput>, 'root' | 'plugins' | 'parsers' | 'adapter' | 'storage'> & {
391
+ /**
392
+ * Project root directory, absolute or relative to the config file location.
393
+ * @default process.cwd()
394
+ */
395
+ root?: string
396
+ /**
397
+ * Custom parsers that convert generated AST nodes to strings (TypeScript, JSON, markdown, etc.).
398
+ * @default [parserTs] // from `@kubb/parser-ts`
399
+ */
400
+ parsers?: Array<Parser>
401
+ /**
402
+ * Adapter that parses your API specification into Kubb's universal AST.
403
+ * When omitted, Kubb runs in plugin-only mode.
404
+ */
405
+ adapter?: Adapter
406
+ /**
407
+ * Plugins that execute during the build to generate code and transform the AST.
408
+ * @default []
409
+ */
410
+ plugins?: Array<Plugin>
411
+ /**
412
+ * Storage backend that controls where and how generated files are persisted.
413
+ * @default fsStorage()
414
+ */
415
+ storage?: Storage
416
+ }
417
+
418
+ declare global {
419
+ namespace Kubb {
420
+ /**
421
+ * Registry that maps plugin names to their `PluginFactoryOptions`.
422
+ * Augment this interface in each plugin's `types.ts` to enable automatic
423
+ * typing for `getPlugin` and `requirePlugin`.
424
+ *
425
+ * @example
426
+ * ```ts
427
+ * // packages/plugin-ts/src/types.ts
428
+ * declare global {
429
+ * namespace Kubb {
430
+ * interface PluginRegistry {
431
+ * 'plugin-ts': PluginTs
432
+ * }
433
+ * }
434
+ * }
435
+ * ```
436
+ */
437
+ interface PluginRegistry {}
438
+
439
+ /**
440
+ * Extension point for root `Config['output']` options.
441
+ * Augment the `output` key in middleware or plugin packages to add extra fields
442
+ * to the global output configuration without touching core types.
443
+ *
444
+ * @example
445
+ * ```ts
446
+ * // packages/middleware-barrel/src/types.ts
447
+ * declare global {
448
+ * namespace Kubb {
449
+ * interface ConfigOptionsRegistry {
450
+ * output: {
451
+ * barrel?: import('./types.ts').BarrelConfig | false
452
+ * }
453
+ * }
454
+ * }
455
+ * }
456
+ * ```
457
+ */
458
+ interface ConfigOptionsRegistry {}
459
+
460
+ /**
461
+ * Extension point for per-plugin `Output` options.
462
+ * Augment the `output` key in middleware or plugin packages to add extra fields
463
+ * to the per-plugin output configuration without touching core types.
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * // packages/middleware-barrel/src/types.ts
468
+ * declare global {
469
+ * namespace Kubb {
470
+ * interface PluginOptionsRegistry {
471
+ * output: {
472
+ * barrel?: import('./types.ts').PluginBarrelConfig | false
473
+ * }
474
+ * }
475
+ * }
476
+ * }
477
+ * ```
478
+ */
479
+ interface PluginOptionsRegistry {}
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Lifecycle events emitted during Kubb code generation.
485
+ * Attach listeners before calling `setup()` or `build()` to observe and react to build progress.
486
+ *
487
+ * @example
488
+ * ```ts
489
+ * kubb.hooks.on('kubb:lifecycle:start', () => {
490
+ * console.log('Starting Kubb generation')
491
+ * })
492
+ *
493
+ * kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => {
494
+ * console.log(`${plugin.name} completed in ${duration}ms`)
495
+ * })
496
+ * ```
497
+ */
498
+ export interface KubbHooks {
499
+ 'kubb:lifecycle:start': [ctx: KubbLifecycleStartContext]
500
+ 'kubb:lifecycle:end': []
501
+ 'kubb:config:start': []
502
+ 'kubb:config:end': [ctx: KubbConfigEndContext]
503
+ 'kubb:generation:start': [ctx: KubbGenerationStartContext]
504
+ 'kubb:generation:end': [ctx: KubbGenerationEndContext]
505
+ 'kubb:generation:summary': [ctx: KubbGenerationSummaryContext]
506
+ 'kubb:format:start': []
507
+ 'kubb:format:end': []
508
+ 'kubb:lint:start': []
509
+ 'kubb:lint:end': []
510
+ 'kubb:hooks:start': []
511
+ 'kubb:hooks:end': []
512
+ 'kubb:hook:start': [ctx: KubbHookStartContext]
513
+ 'kubb:hook:end': [ctx: KubbHookEndContext]
514
+ 'kubb:version:new': [ctx: KubbVersionNewContext]
515
+ 'kubb:info': [ctx: KubbInfoContext]
516
+ 'kubb:error': [ctx: KubbErrorContext]
517
+ 'kubb:success': [ctx: KubbSuccessContext]
518
+ 'kubb:warn': [ctx: KubbWarnContext]
519
+ 'kubb:debug': [ctx: KubbDebugContext]
520
+ 'kubb:files:processing:start': [ctx: KubbFilesProcessingStartContext]
521
+ 'kubb:files:processing:update': [ctx: KubbFilesProcessingUpdateContext]
522
+ 'kubb:files:processing:end': [ctx: KubbFilesProcessingEndContext]
523
+ 'kubb:plugin:start': [ctx: KubbPluginStartContext]
524
+ 'kubb:plugin:end': [ctx: KubbPluginEndContext]
525
+ 'kubb:plugin:setup': [ctx: KubbPluginSetupContext]
526
+ 'kubb:build:start': [ctx: KubbBuildStartContext]
527
+ 'kubb:plugins:end': [ctx: KubbPluginsEndContext]
528
+ 'kubb:build:end': [ctx: KubbBuildEndContext]
529
+ 'kubb:generate:schema': [node: SchemaNode, ctx: GeneratorContext]
530
+ 'kubb:generate:operation': [node: OperationNode, ctx: GeneratorContext]
531
+ 'kubb:generate:operations': [nodes: Array<OperationNode>, ctx: GeneratorContext]
532
+ }
533
+
534
+ export type KubbBuildStartContext = {
535
+ /**
536
+ * Resolved configuration for this build.
537
+ */
538
+ config: Config
539
+ /**
540
+ * Adapter that parsed the input into the universal AST.
541
+ */
542
+ adapter: Adapter
543
+ /**
544
+ * Metadata about the parsed document (title, version, base URL, circular schema names, enum names).
545
+ * To observe individual schemas and operations use the `kubb:generate:schema` / `kubb:generate:operation` hooks.
546
+ */
547
+ meta: InputMeta | undefined
548
+ /**
549
+ * Looks up a registered plugin by name, typed by the plugin registry.
550
+ */
551
+ getPlugin<TName extends keyof Kubb.PluginRegistry>(name: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined
552
+ getPlugin(name: string): Plugin | undefined
553
+ /**
554
+ * Snapshot of all files accumulated so far.
555
+ */
556
+ readonly files: ReadonlyArray<FileNode>
557
+ /**
558
+ * Adds or merges one or more files into the file manager.
559
+ */
560
+ upsertFile: (...files: Array<FileNode>) => void
561
+ }
562
+
563
+ export type KubbPluginsEndContext = {
564
+ /**
565
+ * Resolved configuration for this build.
566
+ */
567
+ config: Config
568
+ /**
569
+ * Snapshot of all files accumulated across all plugins.
570
+ */
571
+ readonly files: ReadonlyArray<FileNode>
572
+ /**
573
+ * Adds or merges one or more files into the file manager.
574
+ */
575
+ upsertFile: (...files: Array<FileNode>) => void
576
+ }
577
+
578
+ export type KubbBuildEndContext = {
579
+ /**
580
+ * All files generated during this build.
29
581
  */
30
- failedPlugins: Set<{ plugin: Plugin; error: Error }>
31
582
  files: Array<FileNode>
32
- driver: PluginDriver
33
583
  /**
34
- * Elapsed time in milliseconds for each plugin, keyed by plugin name.
584
+ * Resolved configuration for this build.
35
585
  */
36
- pluginTimings: Map<string, number>
37
- error?: Error
586
+ config: Config
587
+ /**
588
+ * Absolute path to the output directory.
589
+ */
590
+ outputDir: string
591
+ }
592
+
593
+ export type KubbLifecycleStartContext = {
38
594
  /**
39
- * Raw generated source, keyed by absolute file path.
595
+ * Current Kubb version string.
40
596
  */
41
- sources: Map<string, string>
597
+ version: string
42
598
  }
43
599
 
44
- type SetupResult = {
45
- hooks: AsyncEventEmitter<KubbHooks>
46
- driver: PluginDriver
47
- sources: Map<string, string>
600
+ export type KubbConfigEndContext = {
601
+ /**
602
+ * All resolved configs after defaults are applied.
603
+ */
604
+ configs: Array<Config>
605
+ }
606
+
607
+ export type KubbGenerationStartContext = {
608
+ /**
609
+ * Resolved configuration for this generation run.
610
+ */
48
611
  config: Config
49
- storage: Storage | null
50
612
  }
51
613
 
52
- async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promise<SetupResult> {
53
- const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
614
+ export type KubbGenerationEndContext = {
615
+ /**
616
+ * Resolved configuration for this generation run.
617
+ */
618
+ config: Config
619
+ /**
620
+ * Read-only view of the files written during this build.
621
+ * Reads go directly to `config.storage` — nothing extra is held in memory.
622
+ *
623
+ * @example Read a generated file
624
+ * `const code = await storage.getItem('/src/gen/pet.ts')`
625
+ *
626
+ * @example Walk every generated file
627
+ * ```ts
628
+ * for (const path of await storage.getKeys()) {
629
+ * const code = await storage.getItem(path)
630
+ * }
631
+ * ```
632
+ */
633
+ storage: Storage
634
+ }
54
635
 
55
- const sources: Map<string, string> = new Map<string, string>()
56
- const diagnosticInfo = getDiagnosticInfo()
636
+ export type KubbGenerationSummaryContext = {
637
+ /**
638
+ * Resolved configuration for this generation run.
639
+ */
640
+ config: Config
641
+ /**
642
+ * Plugins that threw during generation, paired with their errors.
643
+ */
644
+ failedPlugins: Set<{ plugin: Plugin; error: Error }>
645
+ /**
646
+ * `'success'` when all plugins completed without errors, `'failed'` otherwise.
647
+ */
648
+ status: 'success' | 'failed'
649
+ /**
650
+ * High-resolution start time from `process.hrtime()`.
651
+ */
652
+ hrStart: [number, number]
653
+ /**
654
+ * Total number of files created during this run.
655
+ */
656
+ filesCreated: number
657
+ /**
658
+ * Elapsed milliseconds per plugin, keyed by plugin name.
659
+ */
660
+ pluginTimings?: Map<Plugin['name'], number>
661
+ }
57
662
 
58
- if (Array.isArray(userConfig.input)) {
59
- await hooks.emit('kubb:warn', { message: 'This feature is still under development — use with caution' })
60
- }
663
+ export type KubbVersionNewContext = {
664
+ /**
665
+ * The installed Kubb version.
666
+ */
667
+ currentVersion: string
668
+ /**
669
+ * The newest available version on npm.
670
+ */
671
+ latestVersion: string
672
+ }
61
673
 
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
- })
674
+ export type KubbInfoContext = {
675
+ /**
676
+ * Human-readable info message.
677
+ */
678
+ message: string
679
+ /**
680
+ * Optional supplementary detail.
681
+ */
682
+ info?: string
683
+ }
80
684
 
81
- try {
82
- if (isInputPath(userConfig) && !new URLPath(userConfig.input.path).isURL) {
83
- await exists(userConfig.input.path)
685
+ export type KubbErrorContext = {
686
+ /**
687
+ * The caught error.
688
+ */
689
+ error: Error
690
+ /**
691
+ * Optional structured metadata for additional context.
692
+ */
693
+ meta?: Record<string, unknown>
694
+ }
84
695
 
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
- }
696
+ export type KubbSuccessContext = {
697
+ /**
698
+ * Human-readable success message.
699
+ */
700
+ message: string
701
+ /**
702
+ * Optional supplementary detail.
703
+ */
704
+ info?: string
705
+ }
102
706
 
103
- if (!userConfig.adapter) {
104
- throw new Error('Adapter should be defined')
105
- }
707
+ export type KubbWarnContext = {
708
+ /**
709
+ * Human-readable warning message.
710
+ */
711
+ message: string
712
+ /**
713
+ * Optional supplementary detail.
714
+ */
715
+ info?: string
716
+ }
717
+
718
+ export type KubbDebugContext = {
719
+ /**
720
+ * Timestamp when the debug entry was created.
721
+ */
722
+ date: Date
723
+ /**
724
+ * One or more log lines to emit.
725
+ */
726
+ logs: Array<string>
727
+ /**
728
+ * Optional source file name associated with this entry.
729
+ */
730
+ fileName?: string
731
+ }
732
+
733
+ export type KubbFilesProcessingStartContext = {
734
+ /**
735
+ * Files about to be serialised and written.
736
+ */
737
+ files: Array<FileNode>
738
+ }
739
+
740
+ export type KubbFileProcessingUpdate = {
741
+ /**
742
+ * Number of files processed so far in this batch.
743
+ */
744
+ processed: number
745
+ /**
746
+ * Total number of files in this batch.
747
+ */
748
+ total: number
749
+ /**
750
+ * Completion percentage (`0`–`100`).
751
+ */
752
+ percentage: number
753
+ /**
754
+ * Serialised file content, or `undefined` when the file produced no output.
755
+ */
756
+ source?: string
757
+ /**
758
+ * The file that was just processed.
759
+ */
760
+ file: FileNode
761
+ /**
762
+ * Resolved configuration for this build.
763
+ */
764
+ config: Config
765
+ }
766
+
767
+ export type KubbFilesProcessingUpdateContext = {
768
+ /**
769
+ * All files processed in this flush chunk.
770
+ */
771
+ files: Array<KubbFileProcessingUpdate>
772
+ }
773
+
774
+ export type KubbFilesProcessingEndContext = {
775
+ /**
776
+ * All files that were serialised in this batch.
777
+ */
778
+ files: Array<FileNode>
779
+ }
780
+
781
+ export type KubbHookStartContext = {
782
+ /**
783
+ * Optional identifier for correlating start/end events.
784
+ */
785
+ id?: string
786
+ /**
787
+ * The shell command that is about to run.
788
+ */
789
+ command: string
790
+ /**
791
+ * Parsed argument list, when available.
792
+ */
793
+ args?: ReadonlyArray<string>
794
+ }
106
795
 
107
- const config: Config = {
796
+ export type KubbHookEndContext = {
797
+ /**
798
+ * Optional identifier matching the corresponding `kubb:hook:start` event.
799
+ */
800
+ id?: string
801
+ /**
802
+ * The shell command that ran.
803
+ */
804
+ command: string
805
+ /**
806
+ * Parsed argument list, when available.
807
+ */
808
+ args?: ReadonlyArray<string>
809
+ /**
810
+ * `true` when the command exited with code `0`.
811
+ */
812
+ success: boolean
813
+ /**
814
+ * Error thrown by the command, or `null` on success.
815
+ */
816
+ error: Error | null
817
+ }
818
+
819
+ /**
820
+ * CLI options derived from command-line flags.
821
+ */
822
+ export type CLIOptions = {
823
+ /**
824
+ * Path to the Kubb config file.
825
+ */
826
+ config?: string
827
+ /**
828
+ * OpenAPI input path passed as the positional argument to `kubb generate`.
829
+ * Overrides `config.input.path` when set.
830
+ */
831
+ input?: string
832
+ /**
833
+ * Re-run generation whenever input files change.
834
+ */
835
+ watch?: boolean
836
+ /**
837
+ * Controls how much output the CLI prints.
838
+ *
839
+ * @default 'info'
840
+ */
841
+ logLevel?: 'silent' | 'info' | 'verbose' | 'debug'
842
+ }
843
+
844
+ /**
845
+ * All accepted forms of a Kubb configuration.
846
+ * Accepts `Config`/`Config[]`/promise or a factory (optionally receiving `TCliOptions`.
847
+ */
848
+ export type PossibleConfig<TCliOptions = undefined> =
849
+ | PossiblePromise<Config | Array<Config>>
850
+ | ((...args: [TCliOptions] extends [undefined] ? [] : [TCliOptions]) => PossiblePromise<Config | Array<Config>>)
851
+
852
+ /**
853
+ * Full output produced by a successful or failed build.
854
+ */
855
+ export type BuildOutput = {
856
+ /**
857
+ * Plugins that threw during generation, paired with their errors.
858
+ */
859
+ failedPlugins: Set<{ plugin: Plugin; error: Error }>
860
+ /**
861
+ * All files generated during this build.
862
+ */
863
+ files: Array<FileNode>
864
+ /**
865
+ * The plugin driver that orchestrated this build.
866
+ */
867
+ driver: KubbDriver
868
+ /**
869
+ * Elapsed milliseconds per plugin, keyed by plugin name.
870
+ */
871
+ pluginTimings: Map<string, number>
872
+ /**
873
+ * Top-level error when the build threw before completing, otherwise `undefined`.
874
+ */
875
+ error?: Error
876
+ /**
877
+ * Read-only view of every file written during this build.
878
+ * Reads go straight to `config.storage` — nothing extra is held in memory.
879
+ *
880
+ * @example Read a generated file
881
+ * `const code = await buildOutput.storage.getItem('/src/gen/pet.ts')`
882
+ *
883
+ * @example List all generated file paths
884
+ * `const paths = await buildOutput.storage.getKeys()`
885
+ */
886
+ storage: Storage
887
+ }
888
+
889
+ /**
890
+ * Builds a `Storage` view scoped to the file paths produced by the current build.
891
+ * Reads delegate to the underlying `storage` so source bytes stay where they were
892
+ * written; writes register the key so subsequent reads and `getKeys` are scoped
893
+ * to this build's output.
894
+ */
895
+ function createSourcesView(storage: Storage): Storage {
896
+ const paths = new Set<string>()
897
+
898
+ return createStorage(() => ({
899
+ name: `${storage.name}:sources`,
900
+ async hasItem(key: string) {
901
+ return paths.has(key) && (await storage.hasItem(key))
902
+ },
903
+ async getItem(key: string) {
904
+ return paths.has(key) ? storage.getItem(key) : null
905
+ },
906
+ async setItem(key: string, value: string) {
907
+ paths.add(key)
908
+ await storage.setItem(key, value)
909
+ },
910
+ async removeItem(key: string) {
911
+ paths.delete(key)
912
+ await storage.removeItem(key)
913
+ },
914
+ async getKeys(base?: string) {
915
+ if (!base) return [...paths]
916
+ const result: Array<string> = []
917
+ for (const key of paths) {
918
+ if (key.startsWith(base)) result.push(key)
919
+ }
920
+ return result
921
+ },
922
+ async clear() {
923
+ paths.clear()
924
+ await storage.clear()
925
+ },
926
+ }))()
927
+ }
928
+
929
+ function resolveConfig(userConfig: UserConfig): Config {
930
+ return {
108
931
  ...userConfig,
109
932
  root: userConfig.root || process.cwd(),
110
933
  parsers: userConfig.parsers ?? [],
111
- adapter: userConfig.adapter,
112
934
  output: {
113
935
  format: false,
114
936
  lint: false,
115
- write: true,
116
937
  extension: DEFAULT_EXTENSION,
117
938
  defaultBanner: DEFAULT_BANNER,
118
939
  ...userConfig.output,
119
940
  },
941
+ storage: userConfig.storage ?? fsStorage(),
120
942
  devtools: userConfig.devtools
121
943
  ? {
122
944
  studioUrl: DEFAULT_STUDIO_URL,
123
945
  ...(typeof userConfig.devtools === 'boolean' ? {} : userConfig.devtools),
124
946
  }
125
947
  : undefined,
126
- plugins: userConfig.plugins as unknown as Config['plugins'],
127
- }
128
-
129
- const storage: Storage | null = config.output.write === false ? null : (config.storage ?? fsStorage())
130
-
131
- if (config.output.clean) {
132
- await hooks.emit('kubb:debug', {
133
- date: new Date(),
134
- logs: ['Cleaning output directories', ` • Output: ${config.output.path}`],
135
- })
136
- await storage?.clear(resolve(config.root, config.output.path))
137
- }
138
-
139
- const driver = new PluginDriver(config, {
140
- hooks,
141
- })
142
-
143
- // Register middleware hooks after all plugin hooks are registered.
144
- // Because AsyncEventEmitter calls listeners in registration order,
145
- // middleware hooks for any event fire after all plugin hooks for that event.
146
- function registerMiddlewareHook<K extends keyof KubbHooks & string>(event: K, middlewareHooks: Middleware['hooks']) {
147
- const handler = middlewareHooks[event]
148
- if (handler) {
149
- hooks.on(event, handler)
150
- }
151
- }
152
-
153
- for (const middleware of config.middleware ?? []) {
154
- for (const event of Object.keys(middleware.hooks) as Array<keyof KubbHooks & string>) {
155
- registerMiddlewareHook(event, middleware.hooks)
156
- }
948
+ plugins: (userConfig.plugins ?? []) as unknown as Config['plugins'],
157
949
  }
950
+ }
158
951
 
159
- const adapter = config.adapter
160
- if (!adapter) {
161
- throw new Error('No adapter configured. Please provide an adapter in your kubb.config.ts.')
162
- }
163
- const source = inputToAdapterSource(config)
164
-
165
- await hooks.emit('kubb:debug', {
166
- date: new Date(),
167
- logs: [`Running adapter: ${adapter.name}`],
168
- })
169
-
170
- driver.adapter = adapter
171
- driver.inputNode = await adapter.parse(source)
952
+ /**
953
+ * Returns a snapshot of the current runtime environment.
954
+ *
955
+ * Useful for attaching context to debug logs and error reports so that
956
+ * issues can be reproduced without manual information gathering.
957
+ */
958
+ export function getDiagnosticInfo() {
959
+ return {
960
+ nodeVersion,
961
+ KubbVersion,
962
+ platform: process.platform,
963
+ arch: process.arch,
964
+ cwd: process.cwd(),
965
+ } as const
966
+ }
172
967
 
173
- await hooks.emit('kubb:debug', {
174
- date: new Date(),
175
- logs: [
176
- `✓ Adapter '${adapter.name}' resolved InputNode`,
177
- ` • Schemas: ${driver.inputNode.schemas.length}`,
178
- ` • Operations: ${driver.inputNode.operations.length}`,
179
- ],
180
- })
968
+ /**
969
+ * Type guard to check if a given config has an `input.path`.
970
+ */
971
+ export function isInputPath(config: UserConfig | undefined): config is UserConfig<InputPath> & { input: InputPath }
972
+ export function isInputPath(config: Config | undefined): config is Config<InputPath> & { input: InputPath }
973
+ export function isInputPath(config: Config | UserConfig | undefined): config is (Config<InputPath> | UserConfig<InputPath>) & { input: InputPath } {
974
+ return typeof config?.input === 'object' && config.input !== null && 'path' in config.input
975
+ }
181
976
 
182
- return {
183
- config,
184
- hooks,
185
- driver,
186
- sources,
187
- storage,
188
- }
977
+ type CreateKubbOptions = {
978
+ hooks?: AsyncEventEmitter<KubbHooks>
189
979
  }
190
980
 
191
981
  /**
192
- * Walks the AST and dispatches nodes to a plugin's direct AST hooks
193
- * (`schema`, `operation`, `operations`).
982
+ * Kubb code-generation instance bound to a single config entry. Resolves the user
983
+ * config during `setup()` and shares `hooks`, `storage`, `driver`, and `config` across
984
+ * the `setup → build` lifecycle.
194
985
  *
195
- * When `include` contains only operation-scoped filters (`tag`, `operationId`, `path`,
196
- * `method`, `contentType`) and no `schemaName` filter, the function pre-computes the set
197
- * of top-level schema names transitively reachable from the included operations and skips
198
- * schemas that fall outside that set. This ensures that component schemas referenced
199
- * exclusively by excluded operations are not generated.
986
+ * Attach event listeners to `.hooks` before calling `setup()` or `build()`.
987
+ *
988
+ * @example
989
+ * ```ts
990
+ * const kubb = createKubb(userConfig)
991
+ * kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => console.log(plugin.name, duration))
992
+ * const { files, failedPlugins } = await kubb.safeBuild()
993
+ * ```
200
994
  */
201
- async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorContext): Promise<void> {
202
- const { adapter, inputNode, resolver, driver } = context
203
- const { exclude, include, override } = plugin.options
204
-
205
- if (!adapter || !inputNode) {
206
- throw new Error(`[${plugin.name}] No adapter found. Add an OAS adapter (e.g. pluginOas()) before this plugin in your Kubb config.`)
995
+ export class Kubb {
996
+ readonly hooks: AsyncEventEmitter<KubbHooks>
997
+ readonly #userConfig: UserConfig
998
+ #config: Config | null = null
999
+ #driver: KubbDriver | null = null
1000
+ #storage: Storage | null = null
1001
+
1002
+ constructor(userConfig: UserConfig, options: CreateKubbOptions = {}) {
1003
+ this.#userConfig = userConfig
1004
+ this.hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
207
1005
  }
208
1006
 
209
- function resolveRenderer(gen: Generator): RendererFactory | undefined {
210
- return gen.renderer === null ? undefined : (gen.renderer ?? plugin.renderer ?? context.config.renderer)
1007
+ get storage(): Storage {
1008
+ if (!this.#storage) throw new Error('[kubb] setup() must be called before accessing storage')
1009
+ return this.#storage
211
1010
  }
212
1011
 
213
- const generators = plugin.generators ?? []
214
- const collectedOperations: Array<OperationNode> = []
215
-
216
- const generatorContext = {
217
- ...context,
218
- resolver: driver.getResolver(plugin.name),
1012
+ get driver(): KubbDriver {
1013
+ if (!this.#driver) throw new Error('[kubb] setup() must be called before accessing driver')
1014
+ return this.#driver
219
1015
  }
220
1016
 
221
- // When `include` has operation-based filters (tag, operationId, path, method, contentType)
222
- // but no schema-level filters (schemaName), pre-compute the set of top-level schema names
223
- // that are transitively referenced by the included operations. Schemas outside that set are
224
- // skipped so that types belonging exclusively to excluded operations are not generated.
225
- const operationFilterTypes = new Set(['tag', 'operationId', 'path', 'method', 'contentType'])
226
- const hasOperationBasedIncludes = include?.some(({ type }) => operationFilterTypes.has(type)) ?? false
227
- const hasSchemaNameIncludes = include?.some(({ type }) => type === 'schemaName') ?? false
228
-
229
- let allowedSchemaNames: Set<string> | undefined
230
- if (hasOperationBasedIncludes && !hasSchemaNameIncludes) {
231
- const includedOps = inputNode.operations.filter((op) => resolver.resolveOptions(op, { options: plugin.options, exclude, include, override }) !== null)
232
- allowedSchemaNames = collectUsedSchemaNames(includedOps, inputNode.schemas)
1017
+ get config(): Config {
1018
+ if (!this.#config) throw new Error('[kubb] setup() must be called before accessing config')
1019
+ return this.#config
233
1020
  }
234
1021
 
235
- await walk(inputNode, {
236
- depth: 'shallow',
237
- async schema(node) {
238
- const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
239
-
240
- // Skip named top-level schemas that are not reachable from any included operation.
241
- if (allowedSchemaNames !== undefined && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) {
242
- return
243
- }
244
-
245
- const options = resolver.resolveOptions(transformedNode, {
246
- options: plugin.options,
247
- exclude,
248
- include,
249
- override,
250
- })
251
- if (options === null) return
252
-
253
- const ctx = { ...generatorContext, options }
254
-
255
- for (const gen of generators) {
256
- if (!gen.schema) continue
257
- const result = await gen.schema(transformedNode, ctx)
258
- await applyHookResult(result, driver, resolveRenderer(gen))
259
- }
260
-
261
- await driver.hooks.emit('kubb:generate:schema', transformedNode, ctx)
262
- },
263
- async operation(node) {
264
- const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
265
- const options = resolver.resolveOptions(transformedNode, {
266
- options: plugin.options,
267
- exclude,
268
- include,
269
- override,
270
- })
271
- if (options !== null) {
272
- collectedOperations.push(transformedNode)
273
-
274
- const ctx = { ...generatorContext, options }
275
-
276
- for (const gen of generators) {
277
- if (!gen.operation) continue
278
- const result = await gen.operation(transformedNode, ctx)
279
- await applyHookResult(result, driver, resolveRenderer(gen))
280
- }
281
-
282
- await driver.hooks.emit('kubb:generate:operation', transformedNode, ctx)
283
- }
284
- },
285
- })
286
-
287
- if (collectedOperations.length > 0) {
288
- const ctx = { ...generatorContext, options: plugin.options }
289
-
290
- for (const gen of generators) {
291
- if (!gen.operations) continue
292
- const result = await gen.operations(collectedOperations, ctx)
293
- await applyHookResult(result, driver, resolveRenderer(gen))
294
- }
295
-
296
- await driver.hooks.emit('kubb:generate:operations', collectedOperations, ctx)
297
- }
298
- }
299
-
300
- async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
301
- const { driver, hooks, sources, storage } = setupResult
302
-
303
- const failedPlugins = new Set<{ plugin: Plugin; error: Error }>()
304
- const pluginTimings = new Map<string, number>()
305
- const config = driver.config
306
-
307
- try {
308
- await driver.emitSetupHooks()
309
-
310
- if (driver.adapter && driver.inputNode) {
311
- await hooks.emit('kubb:build:start', {
312
- config,
313
- adapter: driver.adapter,
314
- inputNode: driver.inputNode,
315
- getPlugin: driver.getPlugin.bind(driver),
316
- get files() {
317
- return driver.fileManager.files
318
- },
319
- upsertFile: (...files) => driver.fileManager.upsert(...files),
320
- })
321
- }
1022
+ /**
1023
+ * Resolves config and initializes the driver. `build()` calls this automatically.
1024
+ */
1025
+ async setup(): Promise<void> {
1026
+ const config = resolveConfig(this.#userConfig)
1027
+ const driver = new KubbDriver(config, { hooks: this.hooks })
1028
+ const storage = createSourcesView(config.storage)
322
1029
 
323
- for (const plugin of driver.plugins.values()) {
324
- const context = driver.getContext(plugin)
325
- const hrStart = process.hrtime()
1030
+ await this.hooks.emit('kubb:debug', { date: new Date(), logs: this.#configLogs(config) })
326
1031
 
1032
+ if (isInputPath(this.#userConfig) && !new URLPath(this.#userConfig.input.path).isURL) {
327
1033
  try {
328
- const timestamp = new Date()
329
-
330
- await hooks.emit('kubb:plugin:start', { plugin })
331
-
332
- await hooks.emit('kubb:debug', {
333
- date: timestamp,
334
- logs: ['Starting plugin...', ` • Plugin Name: ${plugin.name}`],
335
- })
336
-
337
- if (plugin.generators?.length || driver.hasRegisteredGenerators(plugin.name)) {
338
- await runPluginAstHooks(plugin, context)
339
- }
340
-
341
- const duration = getElapsedMs(hrStart)
342
- pluginTimings.set(plugin.name, duration)
343
-
344
- await hooks.emit('kubb:plugin:end', {
345
- plugin,
346
- duration,
347
- success: true,
348
- config,
349
- get files() {
350
- return driver.fileManager.files
351
- },
352
- upsertFile: (...files) => driver.fileManager.upsert(...files),
353
- })
354
-
355
- await hooks.emit('kubb:debug', {
356
- date: new Date(),
357
- logs: [`✓ Plugin started successfully (${formatMs(duration)})`],
358
- })
1034
+ await exists(this.#userConfig.input.path)
1035
+ await this.hooks.emit('kubb:debug', { date: new Date(), logs: [`✓ Input file validated: ${this.#userConfig.input.path}`] })
359
1036
  } catch (caughtError) {
360
- const error = caughtError as Error
361
- const errorTimestamp = new Date()
362
- const duration = getElapsedMs(hrStart)
363
-
364
- await hooks.emit('kubb:plugin:end', {
365
- plugin,
366
- duration,
367
- success: false,
368
- error,
369
- config,
370
- get files() {
371
- return driver.fileManager.files
372
- },
373
- upsertFile: (...files) => driver.fileManager.upsert(...files),
374
- })
375
-
376
- await hooks.emit('kubb:debug', {
377
- date: errorTimestamp,
378
- logs: [
379
- '✗ Plugin start failed',
380
- ` • Plugin Name: ${plugin.name}`,
381
- ` • Error: ${error.constructor.name} - ${error.message}`,
382
- ' • Stack Trace:',
383
- error.stack || 'No stack trace available',
384
- ],
385
- })
386
-
387
- failedPlugins.add({ plugin, error })
1037
+ throw new Error(
1038
+ `Cannot read file/URL defined in \`input.path\` or set with \`kubb generate PATH\` in the CLI of your Kubb config ${this.#userConfig.input.path}`,
1039
+ { cause: caughtError as Error },
1040
+ )
388
1041
  }
389
1042
  }
390
1043
 
391
- await hooks.emit('kubb:plugins:end', {
392
- config,
393
- get files() {
394
- return driver.fileManager.files
395
- },
396
- upsertFile: (...files) => driver.fileManager.upsert(...files),
397
- })
398
-
399
- const files = driver.fileManager.files
400
-
401
- const parsersMap = new Map<FileNode['extname'], Parser>()
402
- for (const parser of config.parsers) {
403
- if (parser.extNames) {
404
- for (const extname of parser.extNames) {
405
- parsersMap.set(extname, parser)
406
- }
407
- }
1044
+ if (config.output.clean) {
1045
+ await this.hooks.emit('kubb:debug', { date: new Date(), logs: ['Cleaning output directories', ` • Output: ${config.output.path}`] })
1046
+ await config.storage.clear(resolve(config.root, config.output.path))
408
1047
  }
409
1048
 
410
- const fileProcessor = new FileProcessor()
411
-
412
- await hooks.emit('kubb:debug', {
413
- date: new Date(),
414
- logs: [`Writing ${files.length} files...`],
415
- })
416
-
417
- await fileProcessor.run(files, {
418
- parsers: parsersMap,
419
- extension: config.output.extension,
420
- onStart: async (processingFiles) => {
421
- await hooks.emit('kubb:files:processing:start', { files: processingFiles })
422
- },
423
- onUpdate: async ({ file, source, processed, total, percentage }) => {
424
- await hooks.emit('kubb:file:processing:update', {
425
- file,
426
- source,
427
- processed,
428
- total,
429
- percentage,
430
- config,
431
- })
432
- if (source) {
433
- await storage?.setItem(file.path, source)
434
- sources.set(file.path, source)
435
- }
436
- },
437
- onEnd: async (processedFiles) => {
438
- await hooks.emit('kubb:files:processing:end', { files: processedFiles })
439
- await hooks.emit('kubb:debug', {
440
- date: new Date(),
441
- logs: [`✓ File write process completed for ${processedFiles.length} files`],
442
- })
443
- },
444
- })
445
-
446
- await hooks.emit('kubb:build:end', {
447
- files,
448
- config,
449
- outputDir: resolve(config.root, config.output.path),
450
- })
451
-
452
- return {
453
- failedPlugins,
454
- files,
455
- driver,
456
- pluginTimings,
457
- sources,
458
- }
459
- } catch (error) {
460
- return {
461
- failedPlugins,
462
- files: [],
463
- driver,
464
- pluginTimings,
465
- error: error as Error,
466
- sources,
467
- }
468
- } finally {
469
- driver.dispose()
470
- }
471
- }
472
-
473
- async function build(setupResult: SetupResult): Promise<BuildOutput> {
474
- const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(setupResult)
1049
+ await driver.setup()
475
1050
 
476
- if (error) {
477
- throw error
1051
+ this.#config = config
1052
+ this.#driver = driver
1053
+ this.#storage = storage
478
1054
  }
479
1055
 
480
- if (failedPlugins.size > 0) {
481
- const errors = [...failedPlugins].map(({ error }) => error)
482
-
483
- throw new BuildError(`Build Error with ${failedPlugins.size} failed plugins`, { errors })
1056
+ /**
1057
+ * Runs the full pipeline and throws on any plugin error.
1058
+ * Automatically calls `setup()` if needed.
1059
+ */
1060
+ async build(): Promise<BuildOutput> {
1061
+ const out = await this.safeBuild()
1062
+ if (out.error) throw out.error
1063
+ if (out.failedPlugins.size > 0) {
1064
+ const errors = [...out.failedPlugins].map(({ error }) => error)
1065
+ throw new BuildError(`Build Error with ${out.failedPlugins.size} failed plugins`, { errors })
1066
+ }
1067
+ return out
484
1068
  }
485
1069
 
486
- return {
487
- failedPlugins,
488
- files,
489
- driver,
490
- pluginTimings,
491
- error: undefined,
492
- sources,
1070
+ /**
1071
+ * Runs the full pipeline and captures errors in `BuildOutput` instead of throwing.
1072
+ * Automatically calls `setup()` if needed.
1073
+ */
1074
+ async safeBuild(): Promise<BuildOutput> {
1075
+ if (!this.#driver) await this.setup()
1076
+ using cleanup = this
1077
+ const driver = cleanup.driver
1078
+ const storage = cleanup.storage
1079
+ const { failedPlugins, pluginTimings, error } = await driver.run({ storage })
1080
+ return { failedPlugins, files: driver.fileManager.files, driver, pluginTimings, storage, ...(error ? { error } : {}) }
493
1081
  }
494
- }
495
1082
 
496
- function inputToAdapterSource(config: Config): AdapterSource {
497
- if (Array.isArray(config.input)) {
498
- return {
499
- type: 'paths',
500
- paths: config.input.map((i) => (new URLPath(i.path).isURL ? i.path : resolve(config.root, i.path))),
501
- }
1083
+ dispose(): void {
1084
+ this.#driver?.dispose()
502
1085
  }
503
1086
 
504
- if ('data' in config.input) {
505
- return { type: 'data', data: config.input.data }
1087
+ [Symbol.dispose](): void {
1088
+ this.dispose()
506
1089
  }
507
1090
 
508
- if (new URLPath(config.input.path).isURL) {
509
- return { type: 'path', path: config.input.path }
1091
+ #configLogs(config: Config): Array<string> {
1092
+ const u = this.#userConfig
1093
+ const diag = getDiagnosticInfo()
1094
+ return [
1095
+ 'Configuration:',
1096
+ ` • Name: ${u.name || 'unnamed'}`,
1097
+ ` • Root: ${u.root || process.cwd()}`,
1098
+ ` • Output: ${u.output?.path || 'not specified'}`,
1099
+ ` • Plugins: ${u.plugins?.length || 0}`,
1100
+ 'Output Settings:',
1101
+ ` • Storage: ${config.storage.name}`,
1102
+ ` • Formatter: ${u.output?.format || 'none'}`,
1103
+ ` • Linter: ${u.output?.lint || 'none'}`,
1104
+ `Running adapter: ${config.adapter?.name || 'none'}`,
1105
+ 'Environment:',
1106
+ Object.entries(diag)
1107
+ .map(([key, value]) => ` • ${key}: ${value}`)
1108
+ .join('\n'),
1109
+ ]
510
1110
  }
511
-
512
- const resolved = resolve(config.root, config.input.path)
513
- return { type: 'path', path: resolved }
514
- }
515
-
516
- type CreateKubbOptions = {
517
- hooks?: AsyncEventEmitter<KubbHooks>
518
1111
  }
519
1112
 
520
1113
  /**
521
- * Creates a Kubb instance bound to a single config entry.
522
- *
523
- * Accepts a user-facing config shape and resolves it to a full {@link Config} during
524
- * `setup()`. The instance then holds shared state (`hooks`, `sources`, `driver`, `config`)
525
- * across the `setup → build` lifecycle. Attach event listeners to `kubb.hooks` before
526
- * calling `setup()` or `build()`.
1114
+ * Constructs a {@link Kubb} build orchestrator from a user config. Equivalent
1115
+ * to `new Kubb(userConfig, options)` and the canonical public entry point.
527
1116
  *
528
1117
  * @example
529
1118
  * ```ts
530
- * const kubb = createKubb(userConfig)
1119
+ * import { createKubb } from '@kubb/core'
1120
+ * import { adapterOas } from '@kubb/adapter-oas'
1121
+ * import { pluginTs } from '@kubb/plugin-ts'
531
1122
  *
532
- * kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => {
533
- * console.log(`${plugin.name} completed in ${duration}ms`)
1123
+ * const kubb = createKubb({
1124
+ * input: { path: './petStore.yaml' },
1125
+ * output: { path: './src/gen' },
1126
+ * adapter: adapterOas(),
1127
+ * plugins: [pluginTs()],
534
1128
  * })
535
1129
  *
536
- * const { files, failedPlugins } = await kubb.safeBuild()
1130
+ * await kubb.build()
537
1131
  * ```
538
1132
  */
539
1133
  export function createKubb(userConfig: UserConfig, options: CreateKubbOptions = {}): Kubb {
540
- const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
541
- let setupResult: SetupResult | undefined
542
-
543
- const instance: Kubb = {
544
- get hooks() {
545
- return hooks
546
- },
547
- get sources() {
548
- return setupResult?.sources ?? new Map()
549
- },
550
- get driver() {
551
- return setupResult?.driver
552
- },
553
- get config() {
554
- return setupResult?.config
555
- },
556
- async setup() {
557
- setupResult = await setup(userConfig, { hooks })
558
- },
559
- async build() {
560
- if (!setupResult) {
561
- await instance.setup()
562
- }
563
- return build(setupResult!)
564
- },
565
- async safeBuild() {
566
- if (!setupResult) {
567
- await instance.setup()
568
- }
569
- return safeBuild(setupResult!)
570
- },
571
- }
572
-
573
- return instance
1134
+ return new Kubb(userConfig, options)
574
1135
  }