@kubb/core 5.0.0-beta.2 → 5.0.0-beta.21

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