@kubb/core 5.0.0-beta.75 → 5.0.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/createKubb.ts CHANGED
@@ -1,20 +1,667 @@
1
1
  import { resolve } from 'node:path'
2
+ import { version as nodeVersion } from 'node:process'
3
+ import type { PossiblePromise } from '@internals/utils'
2
4
  import { AsyncEventEmitter, BuildError, exists, formatMs, getElapsedMs, URLPath } from '@internals/utils'
3
- import type { FileNode, OperationNode } from '@kubb/ast'
4
- import { transform, walk } from '@kubb/ast'
5
+ import type { FileNode, InputNode, OperationNode, SchemaNode } from '@kubb/ast'
6
+ import { collectUsedSchemaNames, transform, walk } from '@kubb/ast'
7
+ import { version as KubbVersion } from '../package.json'
5
8
  import { DEFAULT_BANNER, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL } from './constants.ts'
9
+ import type { Adapter, AdapterSource } from './createAdapter.ts'
6
10
  import type { RendererFactory } from './createRenderer.ts'
7
- import type { Generator } from './defineGenerator.ts'
11
+ import type { Storage } from './createStorage.ts'
12
+ import type { GeneratorContext, Generator } from './defineGenerator.ts'
13
+ import type { Middleware } from './defineMiddleware.ts'
8
14
  import type { Parser } from './defineParser.ts'
9
- import type { Plugin } from './definePlugin.ts'
15
+ import type { KubbPluginEndContext, KubbPluginSetupContext, KubbPluginStartContext, NormalizedPlugin, Plugin } from './definePlugin.ts'
10
16
  import { FileProcessor } from './FileProcessor.ts'
11
- import type { Kubb } from './Kubb.ts'
12
- import { PluginDriver } from './PluginDriver.ts'
13
- import { applyHookResult } from './renderNode.ts'
17
+ import { applyHookResult, PluginDriver } from './PluginDriver.ts'
14
18
  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'
19
+
20
+ /**
21
+ * Safely extracts a type from a registry, returning `{}` if the key doesn't exist.
22
+ * Enables optional interface augmentation for `Kubb.ConfigOptionsRegistry` and `Kubb.PluginOptionsRegistry`
23
+ * without requiring changes to core.
24
+ *
25
+ * @internal
26
+ */
27
+ type ExtractRegistryKey<T, K extends PropertyKey> = K extends keyof T ? T[K] : {}
28
+
29
+ /**
30
+ * Reference to an input file to generate code from.
31
+ *
32
+ * Specify an absolute path or a path relative to the config file location.
33
+ * The adapter will parse this file (e.g., OpenAPI YAML or JSON) into the universal AST.
34
+ */
35
+ export type InputPath = {
36
+ /**
37
+ * Path to your Swagger/OpenAPI file, absolute or relative to the config file location.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * { path: './petstore.yaml' }
42
+ * { path: '/absolute/path/to/openapi.json' }
43
+ * ```
44
+ */
45
+ path: string
46
+ }
47
+
48
+ /**
49
+ * Inline input data to generate code from.
50
+ *
51
+ * Useful when you want to pass the specification directly instead of from a file.
52
+ * Can be a string (YAML/JSON) or a parsed object.
53
+ */
54
+ export type InputData = {
55
+ /**
56
+ * Swagger/OpenAPI data as a string (YAML/JSON) or a parsed object.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * { data: fs.readFileSync('./openapi.yaml', 'utf8') }
61
+ * { data: { openapi: '3.1.0', info: { ... } } }
62
+ * ```
63
+ */
64
+ data: string | unknown
65
+ }
66
+
67
+ type Input = InputPath | InputData
68
+
69
+ /**
70
+ * Build configuration for Kubb code generation.
71
+ *
72
+ * The Config is the main entry point for customizing how Kubb generates code. It specifies:
73
+ * - What to generate from (adapter + input)
74
+ * - Where to output generated code (output)
75
+ * - How to generate (plugins + middleware)
76
+ * - Runtime details (parsers, storage, renderer)
77
+ *
78
+ * See `UserConfig` for a relaxed version with sensible defaults.
79
+ *
80
+ * @private
81
+ */
82
+ export type Config<TInput = Input> = {
83
+ /**
84
+ * Display name for this configuration in CLI output and logs.
85
+ * Useful when running multiple builds with `defineConfig` arrays.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * name: 'api-client'
90
+ * ```
91
+ */
92
+ name?: string
93
+ /**
94
+ * Project root directory, absolute or relative to the config file.
95
+ * @default process.cwd()
96
+ */
97
+ root: string
98
+ /**
99
+ * Parsers that convert generated files to strings.
100
+ * Each parser handles specific extensions (e.g. `.ts`, `.tsx`).
101
+ * A fallback parser is appended for unhandled extensions.
102
+ * When omitted, defaults to `parserTs` from `@kubb/parser-ts`.
103
+ *
104
+ * @default [parserTs] from `@kubb/parser-ts`
105
+ * @example
106
+ * ```ts
107
+ * import { parserTs, tsxParser } from '@kubb/parser-ts'
108
+ * export default defineConfig({
109
+ * parsers: [parserTs, tsxParser],
110
+ * })
111
+ * ```
112
+ */
113
+ parsers: Array<Parser>
114
+ /**
115
+ * Adapter that parses input files into the universal `InputNode` representation.
116
+ * Use `@kubb/adapter-oas` for OpenAPI/Swagger or `@kubb/adapter-asyncapi` for other formats.
117
+ *
118
+ * When omitted, Kubb runs in plugin-only mode: `kubb:plugin:setup` fires and files
119
+ * injected via `injectFile` are written, but no AST walk occurs and generator hooks
120
+ * (`kubb:generate:schema`, `kubb:generate:operation`) are never emitted.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * import { adapterOas } from '@kubb/adapter-oas'
125
+ * export default defineConfig({
126
+ * adapter: adapterOas(),
127
+ * input: { path: './petstore.yaml' },
128
+ * })
129
+ * ```
130
+ */
131
+ adapter?: Adapter
132
+ /**
133
+ * Source file or data to generate code from.
134
+ * Use `input.path` for a file path or `input.data` for inline data.
135
+ * Required when an adapter is configured; omit when running in plugin-only mode.
136
+ */
137
+ input?: TInput
138
+ output: {
139
+ /**
140
+ * Output directory for generated files, absolute or relative to `root`.
141
+ *
142
+ * All generated files will be written under this directory. Subdirectories can be created
143
+ * by plugins based on grouping strategy (by tag, path, etc.).
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * output: {
148
+ * path: './src/gen', // generates ./src/gen/api.ts, ./src/gen/types.ts, etc.
149
+ * }
150
+ * ```
151
+ */
152
+ path: string
153
+ /**
154
+ * Remove all files from the output directory before starting the build.
155
+ *
156
+ * Useful to ensure old generated files aren't mixed with new ones.
157
+ * Set to `true` for fresh builds, `false` to preserve manual edits in output dir.
158
+ *
159
+ * @default false
160
+ * @example
161
+ * ```ts
162
+ * clean: true // wipes ./src/gen/* before generating
163
+ * ```
164
+ */
165
+ clean?: boolean
166
+ /**
167
+ * Auto-format generated files after code generation completes.
168
+ *
169
+ * Applies a code formatter to all generated files. Use `'auto'` to detect which formatter
170
+ * is available on your system. Pass `false` to skip formatting (useful for CI or specific workflows).
171
+ *
172
+ * @default false
173
+ * @example
174
+ * ```ts
175
+ * format: 'auto' // auto-detect prettier, biome, or oxfmt
176
+ * format: 'prettier' // force prettier
177
+ * format: false // skip formatting
178
+ * ```
179
+ */
180
+ format?: 'auto' | 'prettier' | 'biome' | 'oxfmt' | false
181
+ /**
182
+ * Auto-lint generated files after code generation completes.
183
+ *
184
+ * Analyzes all generated files for style/correctness issues. Use `'auto'` to detect which linter
185
+ * is available on your system. Pass `false` to skip linting.
186
+ *
187
+ * @default false
188
+ * @example
189
+ * ```ts
190
+ * lint: 'auto' // auto-detect oxlint, biome, or eslint
191
+ * lint: 'eslint' // force eslint
192
+ * lint: false // skip linting
193
+ * ```
194
+ */
195
+ lint?: 'auto' | 'eslint' | 'biome' | 'oxlint' | false
196
+ /**
197
+ * Map file extensions to different output extensions.
198
+ *
199
+ * Useful when you want generated `.ts` imports to reference `.js` files or vice versa (e.g., for ESM dual packages).
200
+ * Keys are the original extension, values are the output extension. Use empty string `''` to omit extension.
201
+ *
202
+ * @default { '.ts': '.ts' }
203
+ * @example
204
+ * ```ts
205
+ * extension: { '.ts': '.js' } // generates import './api.js' instead of './api.ts'
206
+ * extension: { '.ts': '', '.tsx': '.jsx' }
207
+ * ```
208
+ */
209
+ extension?: Record<FileNode['extname'], FileNode['extname'] | ''>
210
+ /**
211
+ * Banner text prepended to every generated file.
212
+ *
213
+ * Useful for auto-generation notices or license headers. Choose a preset or write custom text.
214
+ * Use `'simple'` for a basic Kubb banner, `'full'` for detailed metadata, or `false` to omit.
215
+ *
216
+ * @default 'simple'
217
+ * @example
218
+ * ```ts
219
+ * defaultBanner: 'simple' // "This file was autogenerated by Kubb"
220
+ * defaultBanner: 'full' // adds source, title, description, API version
221
+ * defaultBanner: false // no banner
222
+ * ```
223
+ */
224
+ defaultBanner?: 'simple' | 'full' | false
225
+ /**
226
+ * When `true`, overwrites existing files. When `false`, skips generated files that already exist.
227
+ *
228
+ * Individual plugins can override this setting. This is useful for preventing accidental data loss
229
+ * when re-generating while you have local edits in the output folder.
230
+ *
231
+ * @default false
232
+ * @example
233
+ * ```ts
234
+ * override: true // regenerate everything, even existing files
235
+ * override: false // skip files that already exist
236
+ * ```
237
+ */
238
+ override?: boolean
239
+ } & ExtractRegistryKey<Kubb.ConfigOptionsRegistry, 'output'>
240
+ /**
241
+ * Storage backend that controls where and how generated files are persisted.
242
+ *
243
+ * Defaults to `fsStorage()` which writes to the file system. Pass `memoryStorage()` to keep files in RAM,
244
+ * or implement a custom `Storage` interface to write to cloud storage, databases, or other backends.
245
+ *
246
+ * @default fsStorage()
247
+ * @example
248
+ * ```ts
249
+ * import { memoryStorage } from '@kubb/core'
250
+ *
251
+ * // Keep generated files in memory (useful for testing, CI pipelines)
252
+ * storage: memoryStorage()
253
+ *
254
+ * // Use custom S3 storage
255
+ * storage: myS3Storage()
256
+ * ```
257
+ *
258
+ * @see {@link Storage} interface for implementing custom backends.
259
+ */
260
+ storage: Storage
261
+ /**
262
+ * Plugins that execute during the build to generate code and transform the AST.
263
+ *
264
+ * Each plugin processes the AST produced by the adapter and can emit files for different
265
+ * programming languages or formats (TypeScript, Zod schemas, Faker data, etc.).
266
+ * Dependencies are enforced — an error is thrown if a plugin requires another plugin that isn't registered.
267
+ *
268
+ * Plugins can declare their own options via `PluginFactoryOptions`. See plugin documentation for details.
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * import { pluginTs } from '@kubb/plugin-ts'
273
+ * import { pluginZod } from '@kubb/plugin-zod'
274
+ *
275
+ * plugins: [
276
+ * pluginTs({ output: { path: './src/gen' } }),
277
+ * pluginZod({ output: { path: './src/gen' } }),
278
+ * ]
279
+ * ```
280
+ */
281
+ plugins: Array<Plugin>
282
+ /**
283
+ * Middleware instances that observe build events and post-process generated code.
284
+ *
285
+ * Middleware fires AFTER all plugins for each event. Perfect for tasks like:
286
+ * - Auditing what was generated
287
+ * - Adding barrel/index files
288
+ * - Validating output
289
+ * - Running custom transformations
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * import { middlewareBarrel } from '@kubb/middleware-barrel'
294
+ *
295
+ * middleware: [middlewareBarrel()]
296
+ * ```
297
+ *
298
+ * @see {@link defineMiddleware} to create custom middleware.
299
+ */
300
+ middleware?: Array<Middleware>
301
+ /**
302
+ * Renderer that converts generated AST nodes to code strings.
303
+ *
304
+ * By default, Kubb uses the JSX renderer (`rendererJsx`). Pass a custom renderer to support
305
+ * different output formats (template engines, code generation DSLs, etc.).
306
+ *
307
+ * @default rendererJsx() // from @kubb/renderer-jsx
308
+ * @example
309
+ * ```ts
310
+ * import { rendererJsx } from '@kubb/renderer-jsx'
311
+ * renderer: rendererJsx()
312
+ * ```
313
+ *
314
+ * @see {@link Renderer} to implement a custom renderer.
315
+ */
316
+ renderer?: RendererFactory
317
+ /**
318
+ * Kubb Studio cloud integration settings.
319
+ *
320
+ * Kubb Studio (https://kubb.studio) is a web-based IDE for managing API specs and generated code.
321
+ * Set to `true` to enable with default settings, or pass an object to customize the Studio URL.
322
+ *
323
+ * @default false // disabled by default
324
+ * @example
325
+ * ```ts
326
+ * devtools: true // use default Kubb Studio
327
+ * devtools: { studioUrl: 'https://my-studio.dev' } // custom Studio instance
328
+ * ```
329
+ */
330
+ devtools?:
331
+ | true
332
+ | {
333
+ /**
334
+ * Override the Kubb Studio base URL.
335
+ * @default 'https://kubb.studio'
336
+ */
337
+ studioUrl?: typeof DEFAULT_STUDIO_URL | (string & {})
338
+ }
339
+ /**
340
+ * Lifecycle hooks that execute during or after the build process.
341
+ *
342
+ * Hooks allow you to run external tools (prettier, eslint, custom scripts) based on build events.
343
+ * Currently supports the `done` hook which fires after all plugins and middleware complete.
344
+ *
345
+ * @example
346
+ * ```ts
347
+ * hooks: {
348
+ * done: 'prettier --write "./src/gen"', // auto-format generated files
349
+ * // or multiple commands:
350
+ * done: ['prettier --write "./src/gen"', 'eslint --fix "./src/gen"']
351
+ * }
352
+ * ```
353
+ */
354
+ hooks?: {
355
+ /**
356
+ * Command(s) to run after all plugins and middleware complete generation.
357
+ *
358
+ * Useful for post-processing: formatting, linting, copying files, or custom validation.
359
+ * Pass a single command string or array of command strings to run sequentially.
360
+ * Commands are executed relative to the `root` directory.
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * done: 'prettier --write "./src/gen"'
365
+ * done: ['prettier --write "./src/gen"', 'eslint --fix "./src/gen"']
366
+ * ```
367
+ */
368
+ done?: string | Array<string>
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Partial `Config` for user-facing entry points with sensible defaults.
374
+ *
375
+ * `UserConfig` is what you pass to `defineConfig()`. It has optional `root`, `plugins`, `parsers`, and `adapter`
376
+ * fields (which fall back to sensible defaults). All other Config options are available, including `output`, `input`,
377
+ * `storage`, `middleware`, `renderer`, `devtools`, and `hooks`.
378
+ *
379
+ * @example
380
+ * ```ts
381
+ * export default defineConfig({
382
+ * input: { path: './petstore.yaml' },
383
+ * output: { path: './src/gen' },
384
+ * plugins: [pluginTs(), pluginZod()],
385
+ * })
386
+ * ```
387
+ */
388
+ export type UserConfig<TInput = Input> = Omit<Config<TInput>, 'root' | 'plugins' | 'parsers' | 'adapter'| 'storage'> & {
389
+ /**
390
+ * Project root directory, absolute or relative to the config file location.
391
+ * @default process.cwd()
392
+ */
393
+ root?: string
394
+ /**
395
+ * Custom parsers that convert generated AST nodes to strings (TypeScript, JSON, markdown, etc.).
396
+ * @default [parserTs] // from `@kubb/parser-ts`
397
+ */
398
+ parsers?: Array<Parser>
399
+ /**
400
+ * Adapter that parses your API specification into Kubb's universal AST.
401
+ * When omitted, Kubb runs in plugin-only mode.
402
+ */
403
+ adapter?: Adapter
404
+ /**
405
+ * Plugins that execute during the build to generate code and transform the AST.
406
+ * @default []
407
+ */
408
+ plugins?: Array<Plugin>
409
+ /**
410
+ * Storage backend that controls where and how generated files are persisted.
411
+ * @default fsStorage()
412
+ */
413
+ storage?: Storage
414
+ }
415
+
416
+ declare global {
417
+ namespace Kubb {
418
+ /**
419
+ * Registry that maps plugin names to their `PluginFactoryOptions`.
420
+ * Augment this interface in each plugin's `types.ts` to enable automatic
421
+ * typing for `getPlugin` and `requirePlugin`.
422
+ *
423
+ * @example
424
+ * ```ts
425
+ * // packages/plugin-ts/src/types.ts
426
+ * declare global {
427
+ * namespace Kubb {
428
+ * interface PluginRegistry {
429
+ * 'plugin-ts': PluginTs
430
+ * }
431
+ * }
432
+ * }
433
+ * ```
434
+ */
435
+ interface PluginRegistry {}
436
+
437
+ /**
438
+ * Extension point for root `Config['output']` options.
439
+ * Augment the `output` key in middleware or plugin packages to add extra fields
440
+ * to the global output configuration without touching core types.
441
+ *
442
+ * @example
443
+ * ```ts
444
+ * // packages/middleware-barrel/src/types.ts
445
+ * declare global {
446
+ * namespace Kubb {
447
+ * interface ConfigOptionsRegistry {
448
+ * output: {
449
+ * barrel?: import('./types.ts').BarrelConfig | false
450
+ * }
451
+ * }
452
+ * }
453
+ * }
454
+ * ```
455
+ */
456
+ interface ConfigOptionsRegistry {}
457
+
458
+ /**
459
+ * Extension point for per-plugin `Output` options.
460
+ * Augment the `output` key in middleware or plugin packages to add extra fields
461
+ * to the per-plugin output configuration without touching core types.
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * // packages/middleware-barrel/src/types.ts
466
+ * declare global {
467
+ * namespace Kubb {
468
+ * interface PluginOptionsRegistry {
469
+ * output: {
470
+ * barrel?: import('./types.ts').PluginBarrelConfig | false
471
+ * }
472
+ * }
473
+ * }
474
+ * }
475
+ * ```
476
+ */
477
+ interface PluginOptionsRegistry {}
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Lifecycle events emitted during Kubb code generation.
483
+ * Use these for logging, progress tracking, and custom integrations.
484
+ *
485
+ * @example
486
+ * ```typescript
487
+ * import type { AsyncEventEmitter } from '@internals/utils'
488
+ * import type { KubbHooks } from '@kubb/core'
489
+ *
490
+ * const hooks: AsyncEventEmitter<KubbHooks> = new AsyncEventEmitter()
491
+ *
492
+ * hooks.on('kubb:lifecycle:start', () => {
493
+ * console.log('Starting Kubb generation')
494
+ * })
495
+ *
496
+ * hooks.on('kubb:plugin:end', ({ plugin, duration }) => {
497
+ * console.log(`Plugin ${plugin.name} completed in ${duration}ms`)
498
+ * })
499
+ * ```
500
+ */
501
+ export interface KubbHooks {
502
+ 'kubb:lifecycle:start': [ctx: KubbLifecycleStartContext]
503
+ 'kubb:lifecycle:end': []
504
+ 'kubb:config:start': []
505
+ 'kubb:config:end': [ctx: KubbConfigEndContext]
506
+ 'kubb:generation:start': [ctx: KubbGenerationStartContext]
507
+ 'kubb:generation:end': [ctx: KubbGenerationEndContext]
508
+ 'kubb:generation:summary': [ctx: KubbGenerationSummaryContext]
509
+ 'kubb:format:start': []
510
+ 'kubb:format:end': []
511
+ 'kubb:lint:start': []
512
+ 'kubb:lint:end': []
513
+ 'kubb:hooks:start': []
514
+ 'kubb:hooks:end': []
515
+ 'kubb:hook:start': [ctx: KubbHookStartContext]
516
+ 'kubb:hook:end': [ctx: KubbHookEndContext]
517
+ 'kubb:version:new': [ctx: KubbVersionNewContext]
518
+ 'kubb:info': [ctx: KubbInfoContext]
519
+ 'kubb:error': [ctx: KubbErrorContext]
520
+ 'kubb:success': [ctx: KubbSuccessContext]
521
+ 'kubb:warn': [ctx: KubbWarnContext]
522
+ 'kubb:debug': [ctx: KubbDebugContext]
523
+ 'kubb:files:processing:start': [ctx: KubbFilesProcessingStartContext]
524
+ 'kubb:file:processing:update': [ctx: KubbFileProcessingUpdateContext]
525
+ 'kubb:files:processing:end': [ctx: KubbFilesProcessingEndContext]
526
+ 'kubb:plugin:start': [ctx: KubbPluginStartContext]
527
+ 'kubb:plugin:end': [ctx: KubbPluginEndContext]
528
+ 'kubb:plugin:setup': [ctx: KubbPluginSetupContext]
529
+ 'kubb:build:start': [ctx: KubbBuildStartContext]
530
+ 'kubb:plugins:end': [ctx: KubbPluginsEndContext]
531
+ 'kubb:build:end': [ctx: KubbBuildEndContext]
532
+ 'kubb:generate:schema': [node: SchemaNode, ctx: GeneratorContext]
533
+ 'kubb:generate:operation': [node: OperationNode, ctx: GeneratorContext]
534
+ 'kubb:generate:operations': [nodes: Array<OperationNode>, ctx: GeneratorContext]
535
+ }
536
+
537
+ export type KubbBuildStartContext = {
538
+ config: Config
539
+ adapter: Adapter
540
+ inputNode: InputNode
541
+ getPlugin<TName extends keyof Kubb.PluginRegistry>(name: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined
542
+ getPlugin(name: string): Plugin | undefined
543
+ readonly files: ReadonlyArray<FileNode>
544
+ upsertFile: (...files: Array<FileNode>) => void
545
+ }
546
+
547
+ export type KubbPluginsEndContext = {
548
+ config: Config
549
+ readonly files: ReadonlyArray<FileNode>
550
+ upsertFile: (...files: Array<FileNode>) => void
551
+ }
552
+
553
+ export type KubbBuildEndContext = {
554
+ files: Array<FileNode>
555
+ config: Config
556
+ outputDir: string
557
+ }
558
+
559
+ export type KubbLifecycleStartContext = {
560
+ version: string
561
+ }
562
+
563
+ export type KubbConfigEndContext = {
564
+ configs: Array<Config>
565
+ }
566
+
567
+ export type KubbGenerationStartContext = {
568
+ config: Config
569
+ }
570
+
571
+ export type KubbGenerationEndContext = {
572
+ config: Config
573
+ files: Array<FileNode>
574
+ sources: Map<string, string>
575
+ }
576
+
577
+ export type KubbGenerationSummaryContext = {
578
+ config: Config
579
+ failedPlugins: Set<{ plugin: Plugin; error: Error }>
580
+ status: 'success' | 'failed'
581
+ hrStart: [number, number]
582
+ filesCreated: number
583
+ pluginTimings?: Map<Plugin['name'], number>
584
+ }
585
+
586
+ export type KubbVersionNewContext = {
587
+ currentVersion: string
588
+ latestVersion: string
589
+ }
590
+
591
+ export type KubbInfoContext = {
592
+ message: string
593
+ info?: string
594
+ }
595
+
596
+ export type KubbErrorContext = {
597
+ error: Error
598
+ meta?: Record<string, unknown>
599
+ }
600
+
601
+ export type KubbSuccessContext = {
602
+ message: string
603
+ info?: string
604
+ }
605
+
606
+ export type KubbWarnContext = {
607
+ message: string
608
+ info?: string
609
+ }
610
+
611
+ export type KubbDebugContext = {
612
+ date: Date
613
+ logs: Array<string>
614
+ fileName?: string
615
+ }
616
+
617
+ export type KubbFilesProcessingStartContext = {
618
+ files: Array<FileNode>
619
+ }
620
+
621
+ export type KubbFileProcessingUpdateContext = {
622
+ processed: number
623
+ total: number
624
+ percentage: number
625
+ source?: string
626
+ file: FileNode
627
+ config: Config
628
+ }
629
+
630
+ export type KubbFilesProcessingEndContext = {
631
+ files: Array<FileNode>
632
+ }
633
+
634
+ export type KubbHookStartContext = {
635
+ id?: string
636
+ command: string
637
+ args?: readonly string[]
638
+ }
639
+
640
+ export type KubbHookEndContext = {
641
+ id?: string
642
+ command: string
643
+ args?: readonly string[]
644
+ success: boolean
645
+ error: Error | null
646
+ }
647
+
648
+ /**
649
+ * CLI options derived from command-line flags.
650
+ */
651
+ export type CLIOptions = {
652
+ config?: string
653
+ watch?: boolean
654
+ /** @default 'silent' */
655
+ logLevel?: 'silent' | 'info' | 'debug'
656
+ }
657
+
658
+ /**
659
+ * All accepted forms of a Kubb configuration.
660
+ * Accepts `Config`/`Config[]`/promise or a factory (optionally receiving `TCliOptions`).
661
+ */
662
+ export type PossibleConfig<TCliOptions = undefined> =
663
+ | PossiblePromise<Config | Config[]>
664
+ | ((...args: [TCliOptions] extends [undefined] ? [] : [TCliOptions]) => PossiblePromise<Config | Config[]>)
18
665
 
19
666
  type SetupOptions = {
20
667
  hooks?: AsyncEventEmitter<KubbHooks>
@@ -41,24 +688,79 @@ export type BuildOutput = {
41
688
  sources: Map<string, string>
42
689
  }
43
690
 
691
+ /**
692
+ * Kubb code generation instance returned by {@link createKubb}.
693
+ *
694
+ * Use this when orchestrating multiple builds, inspecting plugin timings, or integrating Kubb into a larger toolchain.
695
+ * For a single one-off build, chain directly: `await createKubb(config).build()`.
696
+ */
697
+ export type Kubb = {
698
+ /**
699
+ * Shared event emitter for lifecycle and status events. Attach listeners before calling `setup()` or `build()`.
700
+ */
701
+ readonly hooks: AsyncEventEmitter<KubbHooks>
702
+ /**
703
+ * Generated source code keyed by absolute file path. Available after `build()` or `safeBuild()` completes.
704
+ */
705
+ readonly sources: Map<string, string>
706
+ /**
707
+ * Plugin driver managing all plugins. Available after `setup()` completes.
708
+ */
709
+ readonly driver: PluginDriver | undefined
710
+ /**
711
+ * Resolved configuration with defaults applied. Available after `setup()` completes.
712
+ */
713
+ readonly config: Config | undefined
714
+ /**
715
+ * Resolves config and initializes the driver. `build()` calls this automatically.
716
+ */
717
+ setup(): Promise<void>
718
+ /**
719
+ * Runs the full pipeline and throws on any plugin error. Automatically calls `setup()` if needed.
720
+ */
721
+ build(): Promise<BuildOutput>
722
+ /**
723
+ * Runs the full pipeline and captures errors in `BuildOutput` instead of throwing. Automatically calls `setup()` if needed.
724
+ */
725
+ safeBuild(): Promise<BuildOutput>
726
+ }
727
+
44
728
  type SetupResult = {
45
729
  hooks: AsyncEventEmitter<KubbHooks>
46
730
  driver: PluginDriver
47
731
  sources: Map<string, string>
48
732
  config: Config
49
- storage: Storage | null
50
733
  }
51
734
 
52
735
  async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promise<SetupResult> {
53
736
  const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
54
-
737
+ const config: Config = {
738
+ ...userConfig,
739
+ root: userConfig.root || process.cwd(),
740
+ parsers: userConfig.parsers ?? [],
741
+ adapter: userConfig.adapter,
742
+ output: {
743
+ format: false,
744
+ lint: false,
745
+ extension: DEFAULT_EXTENSION,
746
+ defaultBanner: DEFAULT_BANNER,
747
+ ...userConfig.output,
748
+ },
749
+ storage: userConfig.storage ?? fsStorage(),
750
+ devtools: userConfig.devtools
751
+ ? {
752
+ studioUrl: DEFAULT_STUDIO_URL,
753
+ ...(typeof userConfig.devtools === 'boolean' ? {} : userConfig.devtools),
754
+ }
755
+ : undefined,
756
+ plugins: (userConfig.plugins ?? []) as unknown as Config['plugins'],
757
+ }
758
+ const driver = new PluginDriver(config, {
759
+ hooks,
760
+ })
55
761
  const sources: Map<string, string> = new Map<string, string>()
56
762
  const diagnosticInfo = getDiagnosticInfo()
57
763
 
58
- if (Array.isArray(userConfig.input)) {
59
- await hooks.emit('kubb:warn', { message: 'This feature is still under development — use with caution' })
60
- }
61
-
62
764
  await hooks.emit('kubb:debug', {
63
765
  date: new Date(),
64
766
  logs: [
@@ -68,7 +770,7 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
68
770
  ` • Output: ${userConfig.output?.path || 'not specified'}`,
69
771
  ` • Plugins: ${userConfig.plugins?.length || 0}`,
70
772
  'Output Settings:',
71
- ` • Storage: ${userConfig.storage ? `custom(${userConfig.storage.name})` : userConfig.output?.write === false ? 'disabled' : 'filesystem (default)'}`,
773
+ ` • Storage: ${config.storage.name}`,
72
774
  ` • Formatter: ${userConfig.output?.format || 'none'}`,
73
775
  ` • Linter: ${userConfig.output?.lint || 'none'}`,
74
776
  'Environment:',
@@ -100,46 +802,16 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
100
802
  }
101
803
  }
102
804
 
103
- if (!userConfig.adapter) {
104
- throw new Error('Adapter should be defined')
105
- }
106
805
 
107
- const config: Config = {
108
- ...userConfig,
109
- root: userConfig.root || process.cwd(),
110
- parsers: userConfig.parsers ?? [],
111
- adapter: userConfig.adapter,
112
- output: {
113
- format: false,
114
- lint: false,
115
- write: true,
116
- extension: DEFAULT_EXTENSION,
117
- defaultBanner: DEFAULT_BANNER,
118
- ...userConfig.output,
119
- },
120
- devtools: userConfig.devtools
121
- ? {
122
- studioUrl: DEFAULT_STUDIO_URL,
123
- ...(typeof userConfig.devtools === 'boolean' ? {} : userConfig.devtools),
124
- }
125
- : 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
806
 
131
807
  if (config.output.clean) {
132
808
  await hooks.emit('kubb:debug', {
133
809
  date: new Date(),
134
810
  logs: ['Cleaning output directories', ` • Output: ${config.output.path}`],
135
811
  })
136
- await storage?.clear(resolve(config.root, config.output.path))
812
+ await config.storage.clear(resolve(config.root, config.output.path))
137
813
  }
138
814
 
139
- const driver = new PluginDriver(config, {
140
- hooks,
141
- })
142
-
143
815
  // Register middleware hooks after all plugin hooks are registered.
144
816
  // Because AsyncEventEmitter calls listeners in registration order,
145
817
  // middleware hooks for any event fire after all plugin hooks for that event.
@@ -156,48 +828,51 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
156
828
  }
157
829
  }
158
830
 
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)
831
+ if (config.adapter) {
832
+ const source = inputToAdapterSource(config)
164
833
 
165
- await hooks.emit('kubb:debug', {
166
- date: new Date(),
167
- logs: [`Running adapter: ${adapter.name}`],
168
- })
834
+ await hooks.emit('kubb:debug', {
835
+ date: new Date(),
836
+ logs: [`Running adapter: ${config.adapter.name}`],
837
+ })
169
838
 
170
- driver.adapter = adapter
171
- driver.inputNode = await adapter.parse(source)
839
+ driver.adapter = config.adapter
840
+ driver.inputNode = await config.adapter.parse(source)
172
841
 
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
- })
842
+ await hooks.emit('kubb:debug', {
843
+ date: new Date(),
844
+ logs: [
845
+ `✓ Adapter '${config.adapter.name}' resolved InputNode`,
846
+ ` • Schemas: ${driver.inputNode.schemas.length}`,
847
+ ` • Operations: ${driver.inputNode.operations.length}`,
848
+ ],
849
+ })
850
+ }
181
851
 
182
852
  return {
183
853
  config,
184
854
  hooks,
185
855
  driver,
186
856
  sources,
187
- storage,
188
857
  }
189
858
  }
190
859
 
191
860
  /**
192
861
  * Walks the AST and dispatches nodes to a plugin's direct AST hooks
193
862
  * (`schema`, `operation`, `operations`).
863
+ *
864
+ * When `include` contains only operation-scoped filters (`tag`, `operationId`, `path`,
865
+ * `method`, `contentType`) and no `schemaName` filter, the function pre-computes the set
866
+ * of top-level schema names transitively reachable from the included operations and skips
867
+ * schemas that fall outside that set. This ensures that component schemas referenced
868
+ * exclusively by excluded operations are not generated.
194
869
  */
195
870
  async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorContext): Promise<void> {
196
871
  const { adapter, inputNode, resolver, driver } = context
197
872
  const { exclude, include, override } = plugin.options
198
873
 
199
874
  if (!adapter || !inputNode) {
200
- throw new Error(`[${plugin.name}] No adapter found. Add an OAS adapter (e.g. pluginOas()) before this plugin in your Kubb config.`)
875
+ throw new Error(`[${plugin.name}] No adapter found. Add an OAS adapter (e.g. adapterOas()) before this plugin in your Kubb config.`)
201
876
  }
202
877
 
203
878
  function resolveRenderer(gen: Generator): RendererFactory | undefined {
@@ -212,10 +887,30 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
212
887
  resolver: driver.getResolver(plugin.name),
213
888
  }
214
889
 
890
+ // When `include` has operation-based filters (tag, operationId, path, method, contentType)
891
+ // but no schema-level filters (schemaName), pre-compute the set of top-level schema names
892
+ // that are transitively referenced by the included operations. Schemas outside that set are
893
+ // skipped so that types belonging exclusively to excluded operations are not generated.
894
+ const operationFilterTypes = new Set(['tag', 'operationId', 'path', 'method', 'contentType'])
895
+ const hasOperationBasedIncludes = include?.some(({ type }) => operationFilterTypes.has(type)) ?? false
896
+ const hasSchemaNameIncludes = include?.some(({ type }) => type === 'schemaName') ?? false
897
+
898
+ let allowedSchemaNames: Set<string> | undefined
899
+ if (hasOperationBasedIncludes && !hasSchemaNameIncludes) {
900
+ const includedOps = inputNode.operations.filter((op) => resolver.resolveOptions(op, { options: plugin.options, exclude, include, override }) !== null)
901
+ allowedSchemaNames = collectUsedSchemaNames(includedOps, inputNode.schemas)
902
+ }
903
+
215
904
  await walk(inputNode, {
216
905
  depth: 'shallow',
217
906
  async schema(node) {
218
907
  const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
908
+
909
+ // Skip named top-level schemas that are not reachable from any included operation.
910
+ if (allowedSchemaNames !== undefined && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) {
911
+ return
912
+ }
913
+
219
914
  const options = resolver.resolveOptions(transformedNode, {
220
915
  options: plugin.options,
221
916
  exclude,
@@ -272,7 +967,7 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
272
967
  }
273
968
 
274
969
  async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
275
- const { driver, hooks, sources, storage } = setupResult
970
+ const { driver, hooks, sources } = setupResult
276
971
 
277
972
  const failedPlugins = new Set<{ plugin: Plugin; error: Error }>()
278
973
  const pluginTimings = new Map<string, number>()
@@ -302,7 +997,6 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
302
997
  const timestamp = new Date()
303
998
 
304
999
  await hooks.emit('kubb:plugin:start', { plugin })
305
-
306
1000
  await hooks.emit('kubb:debug', {
307
1001
  date: timestamp,
308
1002
  logs: ['Starting plugin...', ` • Plugin Name: ${plugin.name}`],
@@ -390,6 +1084,7 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
390
1084
 
391
1085
  await fileProcessor.run(files, {
392
1086
  parsers: parsersMap,
1087
+ mode: 'parallel',
393
1088
  extension: config.output.extension,
394
1089
  onStart: async (processingFiles) => {
395
1090
  await hooks.emit('kubb:files:processing:start', { files: processingFiles })
@@ -404,7 +1099,8 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
404
1099
  config,
405
1100
  })
406
1101
  if (source) {
407
- await storage?.setItem(file.path, source)
1102
+ await config.storage.setItem(file.path, source)
1103
+
408
1104
  sources.set(file.path, source)
409
1105
  }
410
1106
  },
@@ -467,23 +1163,47 @@ async function build(setupResult: SetupResult): Promise<BuildOutput> {
467
1163
  }
468
1164
  }
469
1165
 
1166
+ /**
1167
+ * Returns a snapshot of the current runtime environment.
1168
+ *
1169
+ * Useful for attaching context to debug logs and error reports so that
1170
+ * issues can be reproduced without manual information gathering.
1171
+ */
1172
+ export function getDiagnosticInfo() {
1173
+ return {
1174
+ nodeVersion,
1175
+ KubbVersion,
1176
+ platform: process.platform,
1177
+ arch: process.arch,
1178
+ cwd: process.cwd(),
1179
+ } as const
1180
+ }
1181
+
1182
+ /**
1183
+ * Type guard to check if a given config has an `input.path`.
1184
+ */
1185
+ export function isInputPath(config: UserConfig | undefined): config is UserConfig<InputPath> & { input: InputPath }
1186
+ export function isInputPath(config: Config | undefined): config is Config<InputPath> & { input: InputPath }
1187
+ export function isInputPath(config: Config | UserConfig | undefined): config is (Config<InputPath> | UserConfig<InputPath>) & { input: InputPath } {
1188
+ return typeof config?.input === 'object' && config.input !== null && 'path' in config.input
1189
+ }
1190
+
470
1191
  function inputToAdapterSource(config: Config): AdapterSource {
471
- if (Array.isArray(config.input)) {
472
- return {
473
- type: 'paths',
474
- paths: config.input.map((i) => (new URLPath(i.path).isURL ? i.path : resolve(config.root, i.path))),
475
- }
1192
+ const input = config.input
1193
+ if (!input) {
1194
+ throw new Error('[kubb] input is required when using an adapter. Provide input.path or input.data in your config.')
476
1195
  }
477
1196
 
478
- if ('data' in config.input) {
479
- return { type: 'data', data: config.input.data }
1197
+ if ('data' in input) {
1198
+ return { type: 'data', data: input.data }
480
1199
  }
481
1200
 
482
- if (new URLPath(config.input.path).isURL) {
483
- return { type: 'path', path: config.input.path }
1201
+ if (new URLPath(input.path).isURL) {
1202
+ return { type: 'path', path: input.path }
484
1203
  }
485
1204
 
486
- const resolved = resolve(config.root, config.input.path)
1205
+ const resolved = resolve(config.root, input.path)
1206
+
487
1207
  return { type: 'path', path: resolved }
488
1208
  }
489
1209