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

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 (42) hide show
  1. package/README.md +8 -38
  2. package/dist/{PluginDriver-BXibeQk-.cjs → KubbDriver-BXSnJ3qM.cjs} +719 -164
  3. package/dist/KubbDriver-BXSnJ3qM.cjs.map +1 -0
  4. package/dist/{PluginDriver-DV3p2Hky.js → KubbDriver-Cxii_rBp.js} +693 -162
  5. package/dist/KubbDriver-Cxii_rBp.js.map +1 -0
  6. package/dist/{types-CC09VtBt.d.ts → createKubb-Dcmtjqds.d.ts} +1395 -1238
  7. package/dist/index.cjs +556 -785
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +2 -185
  10. package/dist/index.js +551 -783
  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 +12 -0
  19. package/src/FileProcessor.ts +37 -38
  20. package/src/{PluginDriver.ts → KubbDriver.ts} +249 -86
  21. package/src/constants.ts +11 -6
  22. package/src/createAdapter.ts +84 -1
  23. package/src/createKubb.ts +1336 -297
  24. package/src/createRenderer.ts +23 -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 +268 -147
  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 +38 -1292
  36. package/dist/PluginDriver-BXibeQk-.cjs.map +0 -1
  37. package/dist/PluginDriver-DV3p2Hky.js.map +0 -1
  38. package/src/Kubb.ts +0 -300
  39. package/src/renderNode.ts +0 -35
  40. package/src/utils/diagnostics.ts +0 -18
  41. package/src/utils/isInputPath.ts +0 -10
  42. package/src/utils/packageJSON.ts +0 -99
package/src/createKubb.ts CHANGED
@@ -1,20 +1,840 @@
1
1
  import { resolve } from 'node:path'
2
- import { AsyncEventEmitter, BuildError, exists, formatMs, getElapsedMs, URLPath } from '@internals/utils'
3
- import type { FileNode, OperationNode } from '@kubb/ast'
4
- import { collectUsedSchemaNames, transform, walk } from '@kubb/ast'
5
- import { DEFAULT_BANNER, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL } from './constants.ts'
2
+ import { version as nodeVersion } from 'node:process'
3
+ import type { PossiblePromise } from '@internals/utils'
4
+ import { AsyncEventEmitter, BuildError, exists, forBatches, formatMs, getElapsedMs, URLPath, isPromise, withDrain } from '@internals/utils'
5
+ import type { FileNode, InputMeta, OperationNode, SchemaNode } from '@kubb/ast'
6
+ import { collectUsedSchemaNames, transform } from '@kubb/ast'
7
+ import { version as KubbVersion } from '../package.json'
8
+ import { DEFAULT_BANNER, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL, SCHEMA_PARALLEL, STREAM_FLUSH_EVERY } from './constants.ts'
9
+ import type { Adapter } from './createAdapter.ts'
6
10
  import type { RendererFactory } from './createRenderer.ts'
7
- import type { Generator } from './defineGenerator.ts'
11
+ import { createStorage, 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
+
18
+ import { applyHookResult, KubbDriver } from './KubbDriver.ts'
14
19
  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'
20
+
21
+ /**
22
+ * Safely extracts a type from a registry, returning `{}` if the key doesn't exist.
23
+ * Enables optional interface augmentation for `Kubb.ConfigOptionsRegistry` and `Kubb.PluginOptionsRegistry`
24
+ * without requiring changes to core.
25
+ *
26
+ * @internal
27
+ */
28
+ type ExtractRegistryKey<T, K extends PropertyKey> = K extends keyof T ? T[K] : {}
29
+
30
+ /**
31
+ * Reference to an input file to generate code from.
32
+ *
33
+ * Specify an absolute path or a path relative to the config file location.
34
+ * The adapter will parse this file (e.g., OpenAPI YAML or JSON) into the universal AST.
35
+ */
36
+ export type InputPath = {
37
+ /**
38
+ * Path to your Swagger/OpenAPI file, absolute or relative to the config file location.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * { path: './petstore.yaml' }
43
+ * { path: '/absolute/path/to/openapi.json' }
44
+ * ```
45
+ */
46
+ path: string
47
+ }
48
+
49
+ /**
50
+ * Inline input data to generate code from.
51
+ *
52
+ * Useful when you want to pass the specification directly instead of from a file.
53
+ * Can be a string (YAML/JSON) or a parsed object.
54
+ */
55
+ export type InputData = {
56
+ /**
57
+ * Swagger/OpenAPI data as a string (YAML/JSON) or a parsed object.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * { data: fs.readFileSync('./openapi.yaml', 'utf8') }
62
+ * { data: { openapi: '3.1.0', info: { ... } } }
63
+ * ```
64
+ */
65
+ data: string | unknown
66
+ }
67
+
68
+ type Input = InputPath | InputData
69
+
70
+ /**
71
+ * Build configuration for Kubb code generation.
72
+ *
73
+ * The Config is the main entry point for customizing how Kubb generates code. It specifies:
74
+ * - What to generate from (adapter + input)
75
+ * - Where to output generated code (output)
76
+ * - How to generate (plugins + middleware)
77
+ * - Runtime details (parsers, storage, renderer)
78
+ *
79
+ * See `UserConfig` for a relaxed version with sensible defaults.
80
+ *
81
+ * @private
82
+ */
83
+ export type Config<TInput = Input> = {
84
+ /**
85
+ * Display name for this configuration in CLI output and logs.
86
+ * Useful when running multiple builds with `defineConfig` arrays.
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * name: 'api-client'
91
+ * ```
92
+ */
93
+ name?: string
94
+ /**
95
+ * Project root directory, absolute or relative to the config file.
96
+ * @default process.cwd()
97
+ */
98
+ root: string
99
+ /**
100
+ * Parsers that convert generated files to strings.
101
+ * Each parser handles specific extensions (e.g. `.ts`, `.tsx`).
102
+ * A fallback parser is appended for unhandled extensions.
103
+ * When omitted, defaults to `parserTs` from `@kubb/parser-ts`.
104
+ *
105
+ * @default [parserTs] from `@kubb/parser-ts`
106
+ * @example
107
+ * ```ts
108
+ * import { parserTs, tsxParser } from '@kubb/parser-ts'
109
+ * export default defineConfig({
110
+ * parsers: [parserTs, tsxParser],
111
+ * })
112
+ * ```
113
+ */
114
+ parsers: Array<Parser>
115
+ /**
116
+ * Adapter that parses input files into the universal AST representation.
117
+ * Use `@kubb/adapter-oas` for OpenAPI/Swagger or `@kubb/adapter-asyncapi` for other formats.
118
+ *
119
+ * When omitted, Kubb runs in plugin-only mode: `kubb:plugin:setup` fires and files
120
+ * injected via `injectFile` are written, but no AST walk occurs and generator hooks
121
+ * (`kubb:generate:schema`, `kubb:generate:operation`) are never emitted.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * import { adapterOas } from '@kubb/adapter-oas'
126
+ * export default defineConfig({
127
+ * adapter: adapterOas(),
128
+ * input: { path: './petstore.yaml' },
129
+ * })
130
+ * ```
131
+ */
132
+ adapter?: Adapter
133
+ /**
134
+ * Source file or data to generate code from.
135
+ * Use `input.path` for a file path or `input.data` for inline data.
136
+ * Required when an adapter is configured; omit when running in plugin-only mode.
137
+ */
138
+ input?: TInput
139
+ output: {
140
+ /**
141
+ * Output directory for generated files, absolute or relative to `root`.
142
+ *
143
+ * All generated files will be written under this directory. Subdirectories can be created
144
+ * by plugins based on grouping strategy (by tag, path, etc.).
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * output: {
149
+ * path: './src/gen', // generates ./src/gen/api.ts, ./src/gen/types.ts, etc.
150
+ * }
151
+ * ```
152
+ */
153
+ path: string
154
+ /**
155
+ * Remove all files from the output directory before starting the build.
156
+ *
157
+ * Useful to ensure old generated files aren't mixed with new ones.
158
+ * Set to `true` for fresh builds, `false` to preserve manual edits in output dir.
159
+ *
160
+ * @default false
161
+ * @example
162
+ * ```ts
163
+ * clean: true // wipes ./src/gen/* before generating
164
+ * ```
165
+ */
166
+ clean?: boolean
167
+ /**
168
+ * Auto-format generated files after code generation completes.
169
+ *
170
+ * Applies a code formatter to all generated files. Use `'auto'` to detect which formatter
171
+ * is available on your system. Pass `false` to skip formatting (useful for CI or specific workflows).
172
+ *
173
+ * @default false
174
+ * @example
175
+ * ```ts
176
+ * format: 'auto' // auto-detect prettier, biome, or oxfmt
177
+ * format: 'prettier' // force prettier
178
+ * format: false // skip formatting
179
+ * ```
180
+ */
181
+ format?: 'auto' | 'prettier' | 'biome' | 'oxfmt' | false
182
+ /**
183
+ * Auto-lint generated files after code generation completes.
184
+ *
185
+ * Analyzes all generated files for style/correctness issues. Use `'auto'` to detect which linter
186
+ * is available on your system. Pass `false` to skip linting.
187
+ *
188
+ * @default false
189
+ * @example
190
+ * ```ts
191
+ * lint: 'auto' // auto-detect oxlint, biome, or eslint
192
+ * lint: 'eslint' // force eslint
193
+ * lint: false // skip linting
194
+ * ```
195
+ */
196
+ lint?: 'auto' | 'eslint' | 'biome' | 'oxlint' | false
197
+ /**
198
+ * Map file extensions to different output extensions.
199
+ *
200
+ * Useful when you want generated `.ts` imports to reference `.js` files or vice versa (e.g., for ESM dual packages).
201
+ * Keys are the original extension, values are the output extension. Use empty string `''` to omit extension.
202
+ *
203
+ * @default { '.ts': '.ts' }
204
+ * @example
205
+ * ```ts
206
+ * extension: { '.ts': '.js' } // generates import './api.js' instead of './api.ts'
207
+ * extension: { '.ts': '', '.tsx': '.jsx' }
208
+ * ```
209
+ */
210
+ extension?: Record<FileNode['extname'], FileNode['extname'] | ''>
211
+ /**
212
+ * Banner text prepended to every generated file.
213
+ *
214
+ * Useful for auto-generation notices or license headers. Choose a preset or write custom text.
215
+ * Use `'simple'` for a basic Kubb banner, `'full'` for detailed metadata, or `false` to omit.
216
+ *
217
+ * @default 'simple'
218
+ * @example
219
+ * ```ts
220
+ * defaultBanner: 'simple' // "This file was autogenerated by Kubb"
221
+ * defaultBanner: 'full' // adds source, title, description, API version
222
+ * defaultBanner: false // no banner
223
+ * ```
224
+ */
225
+ defaultBanner?: 'simple' | 'full' | false
226
+ /**
227
+ * When `true`, overwrites existing files. When `false`, skips generated files that already exist.
228
+ *
229
+ * Individual plugins can override this setting. This is useful for preventing accidental data loss
230
+ * when re-generating while you have local edits in the output folder.
231
+ *
232
+ * @default false
233
+ * @example
234
+ * ```ts
235
+ * override: true // regenerate everything, even existing files
236
+ * override: false // skip files that already exist
237
+ * ```
238
+ */
239
+ override?: boolean
240
+ } & ExtractRegistryKey<Kubb.ConfigOptionsRegistry, 'output'>
241
+ /**
242
+ * Storage backend that controls where and how generated files are persisted.
243
+ *
244
+ * Defaults to `fsStorage()` which writes to the file system. Pass `memoryStorage()` to keep files in RAM,
245
+ * or implement a custom `Storage` interface to write to cloud storage, databases, or other backends.
246
+ *
247
+ * @default fsStorage()
248
+ * @example
249
+ * ```ts
250
+ * import { memoryStorage } from '@kubb/core'
251
+ *
252
+ * // Keep generated files in memory (useful for testing, CI pipelines)
253
+ * storage: memoryStorage()
254
+ *
255
+ * // Use custom S3 storage
256
+ * storage: myS3Storage()
257
+ * ```
258
+ *
259
+ * @see {@link Storage} interface for implementing custom backends.
260
+ */
261
+ storage: Storage
262
+ /**
263
+ * Plugins that execute during the build to generate code and transform the AST.
264
+ *
265
+ * Each plugin processes the AST produced by the adapter and can emit files for different
266
+ * programming languages or formats (TypeScript, Zod schemas, Faker data, etc.).
267
+ * Dependencies are enforced — an error is thrown if a plugin requires another plugin that isn't registered.
268
+ *
269
+ * Plugins can declare their own options via `PluginFactoryOptions`. See plugin documentation for details.
270
+ *
271
+ * @example
272
+ * ```ts
273
+ * import { pluginTs } from '@kubb/plugin-ts'
274
+ * import { pluginZod } from '@kubb/plugin-zod'
275
+ *
276
+ * plugins: [
277
+ * pluginTs({ output: { path: './src/gen' } }),
278
+ * pluginZod({ output: { path: './src/gen' } }),
279
+ * ]
280
+ * ```
281
+ */
282
+ plugins: Array<Plugin>
283
+ /**
284
+ * Middleware instances that observe build events and post-process generated code.
285
+ *
286
+ * Middleware fires AFTER all plugins for each event. Perfect for tasks like:
287
+ * - Auditing what was generated
288
+ * - Adding barrel/index files
289
+ * - Validating output
290
+ * - Running custom transformations
291
+ *
292
+ * @example
293
+ * ```ts
294
+ * import { middlewareBarrel } from '@kubb/middleware-barrel'
295
+ *
296
+ * middleware: [middlewareBarrel()]
297
+ * ```
298
+ *
299
+ * @see {@link defineMiddleware} to create custom middleware.
300
+ */
301
+ middleware?: Array<Middleware>
302
+ /**
303
+ * Renderer that converts generated AST nodes to code strings.
304
+ *
305
+ * By default, Kubb uses the JSX renderer (`rendererJsx`). Pass a custom renderer to support
306
+ * different output formats (template engines, code generation DSLs, etc.).
307
+ *
308
+ * @default rendererJsx() // from @kubb/renderer-jsx
309
+ * @example
310
+ * ```ts
311
+ * import { rendererJsx } from '@kubb/renderer-jsx'
312
+ * renderer: rendererJsx()
313
+ * ```
314
+ *
315
+ * @see {@link Renderer} to implement a custom renderer.
316
+ */
317
+ renderer?: RendererFactory
318
+ /**
319
+ * Kubb Studio cloud integration settings.
320
+ *
321
+ * Kubb Studio (https://kubb.studio) is a web-based IDE for managing API specs and generated code.
322
+ * Set to `true` to enable with default settings, or pass an object to customize the Studio URL.
323
+ *
324
+ * @default false // disabled by default
325
+ * @example
326
+ * ```ts
327
+ * devtools: true // use default Kubb Studio
328
+ * devtools: { studioUrl: 'https://my-studio.dev' } // custom Studio instance
329
+ * ```
330
+ */
331
+ devtools?:
332
+ | true
333
+ | {
334
+ /**
335
+ * Override the Kubb Studio base URL.
336
+ * @default 'https://kubb.studio'
337
+ */
338
+ studioUrl?: typeof DEFAULT_STUDIO_URL | (string & {})
339
+ }
340
+ /**
341
+ * Lifecycle hooks that execute during or after the build process.
342
+ *
343
+ * Hooks allow you to run external tools (prettier, eslint, custom scripts) based on build events.
344
+ * Currently supports the `done` hook which fires after all plugins and middleware complete.
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * hooks: {
349
+ * done: 'prettier --write "./src/gen"', // auto-format generated files
350
+ * // or multiple commands:
351
+ * done: ['prettier --write "./src/gen"', 'eslint --fix "./src/gen"']
352
+ * }
353
+ * ```
354
+ */
355
+ hooks?: {
356
+ /**
357
+ * Command(s) to run after all plugins and middleware complete generation.
358
+ *
359
+ * Useful for post-processing: formatting, linting, copying files, or custom validation.
360
+ * Pass a single command string or array of command strings to run sequentially.
361
+ * Commands are executed relative to the `root` directory.
362
+ *
363
+ * @example
364
+ * ```ts
365
+ * done: 'prettier --write "./src/gen"'
366
+ * done: ['prettier --write "./src/gen"', 'eslint --fix "./src/gen"']
367
+ * ```
368
+ */
369
+ done?: string | Array<string>
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Partial `Config` for user-facing entry points with sensible defaults.
375
+ *
376
+ * `UserConfig` is what you pass to `defineConfig()`. It has optional `root`, `plugins`, `parsers`, and `adapter`
377
+ * fields (which fall back to sensible defaults). All other Config options are available, including `output`, `input`,
378
+ * `storage`, `middleware`, `renderer`, `devtools`, and `hooks`.
379
+ *
380
+ * @example
381
+ * ```ts
382
+ * export default defineConfig({
383
+ * input: { path: './petstore.yaml' },
384
+ * output: { path: './src/gen' },
385
+ * plugins: [pluginTs(), pluginZod()],
386
+ * })
387
+ * ```
388
+ */
389
+ export type UserConfig<TInput = Input> = Omit<Config<TInput>, 'root' | 'plugins' | 'parsers' | 'adapter' | 'storage'> & {
390
+ /**
391
+ * Project root directory, absolute or relative to the config file location.
392
+ * @default process.cwd()
393
+ */
394
+ root?: string
395
+ /**
396
+ * Custom parsers that convert generated AST nodes to strings (TypeScript, JSON, markdown, etc.).
397
+ * @default [parserTs] // from `@kubb/parser-ts`
398
+ */
399
+ parsers?: Array<Parser>
400
+ /**
401
+ * Adapter that parses your API specification into Kubb's universal AST.
402
+ * When omitted, Kubb runs in plugin-only mode.
403
+ */
404
+ adapter?: Adapter
405
+ /**
406
+ * Plugins that execute during the build to generate code and transform the AST.
407
+ * @default []
408
+ */
409
+ plugins?: Array<Plugin>
410
+ /**
411
+ * Storage backend that controls where and how generated files are persisted.
412
+ * @default fsStorage()
413
+ */
414
+ storage?: Storage
415
+ }
416
+
417
+ declare global {
418
+ namespace Kubb {
419
+ /**
420
+ * Registry that maps plugin names to their `PluginFactoryOptions`.
421
+ * Augment this interface in each plugin's `types.ts` to enable automatic
422
+ * typing for `getPlugin` and `requirePlugin`.
423
+ *
424
+ * @example
425
+ * ```ts
426
+ * // packages/plugin-ts/src/types.ts
427
+ * declare global {
428
+ * namespace Kubb {
429
+ * interface PluginRegistry {
430
+ * 'plugin-ts': PluginTs
431
+ * }
432
+ * }
433
+ * }
434
+ * ```
435
+ */
436
+ interface PluginRegistry {}
437
+
438
+ /**
439
+ * Extension point for root `Config['output']` options.
440
+ * Augment the `output` key in middleware or plugin packages to add extra fields
441
+ * to the global output configuration without touching core types.
442
+ *
443
+ * @example
444
+ * ```ts
445
+ * // packages/middleware-barrel/src/types.ts
446
+ * declare global {
447
+ * namespace Kubb {
448
+ * interface ConfigOptionsRegistry {
449
+ * output: {
450
+ * barrel?: import('./types.ts').BarrelConfig | false
451
+ * }
452
+ * }
453
+ * }
454
+ * }
455
+ * ```
456
+ */
457
+ interface ConfigOptionsRegistry {}
458
+
459
+ /**
460
+ * Extension point for per-plugin `Output` options.
461
+ * Augment the `output` key in middleware or plugin packages to add extra fields
462
+ * to the per-plugin output configuration without touching core types.
463
+ *
464
+ * @example
465
+ * ```ts
466
+ * // packages/middleware-barrel/src/types.ts
467
+ * declare global {
468
+ * namespace Kubb {
469
+ * interface PluginOptionsRegistry {
470
+ * output: {
471
+ * barrel?: import('./types.ts').PluginBarrelConfig | false
472
+ * }
473
+ * }
474
+ * }
475
+ * }
476
+ * ```
477
+ */
478
+ interface PluginOptionsRegistry {}
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Lifecycle events emitted during Kubb code generation.
484
+ * Attach listeners before calling `setup()` or `build()` to observe and react to build progress.
485
+ *
486
+ * @example
487
+ * ```ts
488
+ * kubb.hooks.on('kubb:lifecycle:start', () => {
489
+ * console.log('Starting Kubb generation')
490
+ * })
491
+ *
492
+ * kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => {
493
+ * console.log(`${plugin.name} completed in ${duration}ms`)
494
+ * })
495
+ * ```
496
+ */
497
+ export interface KubbHooks {
498
+ 'kubb:lifecycle:start': [ctx: KubbLifecycleStartContext]
499
+ 'kubb:lifecycle:end': []
500
+ 'kubb:config:start': []
501
+ 'kubb:config:end': [ctx: KubbConfigEndContext]
502
+ 'kubb:generation:start': [ctx: KubbGenerationStartContext]
503
+ 'kubb:generation:end': [ctx: KubbGenerationEndContext]
504
+ 'kubb:generation:summary': [ctx: KubbGenerationSummaryContext]
505
+ 'kubb:format:start': []
506
+ 'kubb:format:end': []
507
+ 'kubb:lint:start': []
508
+ 'kubb:lint:end': []
509
+ 'kubb:hooks:start': []
510
+ 'kubb:hooks:end': []
511
+ 'kubb:hook:start': [ctx: KubbHookStartContext]
512
+ 'kubb:hook:end': [ctx: KubbHookEndContext]
513
+ 'kubb:version:new': [ctx: KubbVersionNewContext]
514
+ 'kubb:info': [ctx: KubbInfoContext]
515
+ 'kubb:error': [ctx: KubbErrorContext]
516
+ 'kubb:success': [ctx: KubbSuccessContext]
517
+ 'kubb:warn': [ctx: KubbWarnContext]
518
+ 'kubb:debug': [ctx: KubbDebugContext]
519
+ 'kubb:files:processing:start': [ctx: KubbFilesProcessingStartContext]
520
+ 'kubb:file:processing:update': [ctx: KubbFileProcessingUpdateContext]
521
+ 'kubb:files:processing:end': [ctx: KubbFilesProcessingEndContext]
522
+ 'kubb:plugin:start': [ctx: KubbPluginStartContext]
523
+ 'kubb:plugin:end': [ctx: KubbPluginEndContext]
524
+ 'kubb:plugin:setup': [ctx: KubbPluginSetupContext]
525
+ 'kubb:build:start': [ctx: KubbBuildStartContext]
526
+ 'kubb:plugins:end': [ctx: KubbPluginsEndContext]
527
+ 'kubb:build:end': [ctx: KubbBuildEndContext]
528
+ 'kubb:generate:schema': [node: SchemaNode, ctx: GeneratorContext]
529
+ 'kubb:generate:operation': [node: OperationNode, ctx: GeneratorContext]
530
+ 'kubb:generate:operations': [nodes: Array<OperationNode>, ctx: GeneratorContext]
531
+ }
532
+
533
+ export type KubbBuildStartContext = {
534
+ /**
535
+ * Resolved configuration for this build.
536
+ */
537
+ config: Config
538
+ /**
539
+ * Adapter that parsed the input into the universal AST.
540
+ */
541
+ adapter: Adapter
542
+ /**
543
+ * Metadata about the parsed document (title, version, base URL, circular schema names, enum names).
544
+ * To observe individual schemas and operations use the `kubb:generate:schema` / `kubb:generate:operation` hooks.
545
+ */
546
+ meta: InputMeta | undefined
547
+ /**
548
+ * Looks up a registered plugin by name, typed by the plugin registry.
549
+ */
550
+ getPlugin<TName extends keyof Kubb.PluginRegistry>(name: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined
551
+ getPlugin(name: string): Plugin | undefined
552
+ /**
553
+ * Snapshot of all files accumulated so far.
554
+ */
555
+ readonly files: ReadonlyArray<FileNode>
556
+ /**
557
+ * Adds or merges one or more files into the file manager.
558
+ */
559
+ upsertFile: (...files: Array<FileNode>) => void
560
+ }
561
+
562
+ export type KubbPluginsEndContext = {
563
+ /**
564
+ * Resolved configuration for this build.
565
+ */
566
+ config: Config
567
+ /**
568
+ * Snapshot of all files accumulated across all plugins.
569
+ */
570
+ readonly files: ReadonlyArray<FileNode>
571
+ /**
572
+ * Adds or merges one or more files into the file manager.
573
+ */
574
+ upsertFile: (...files: Array<FileNode>) => void
575
+ }
576
+
577
+ export type KubbBuildEndContext = {
578
+ /**
579
+ * All files generated during this build.
580
+ */
581
+ files: Array<FileNode>
582
+ /**
583
+ * Resolved configuration for this build.
584
+ */
585
+ config: Config
586
+ /**
587
+ * Absolute path to the output directory.
588
+ */
589
+ outputDir: string
590
+ }
591
+
592
+ export type KubbLifecycleStartContext = {
593
+ /**
594
+ * Current Kubb version string.
595
+ */
596
+ version: string
597
+ }
598
+
599
+ export type KubbConfigEndContext = {
600
+ /**
601
+ * All resolved configs after defaults are applied.
602
+ */
603
+ configs: Array<Config>
604
+ }
605
+
606
+ export type KubbGenerationStartContext = {
607
+ /**
608
+ * Resolved configuration for this generation run.
609
+ */
610
+ config: Config
611
+ }
612
+
613
+ export type KubbGenerationEndContext = {
614
+ /**
615
+ * Resolved configuration for this generation run.
616
+ */
617
+ config: Config
618
+ /**
619
+ * Read-only view of the files written during this build.
620
+ * Reads go directly to `config.storage` — nothing extra is held in memory.
621
+ *
622
+ * @example Read a generated file
623
+ * `const code = await storage.getItem('/src/gen/pet.ts')`
624
+ *
625
+ * @example Walk every generated file
626
+ * ```ts
627
+ * for (const path of await storage.getKeys()) {
628
+ * const code = await storage.getItem(path)
629
+ * }
630
+ * ```
631
+ */
632
+ storage: Storage
633
+ }
634
+
635
+ export type KubbGenerationSummaryContext = {
636
+ /**
637
+ * Resolved configuration for this generation run.
638
+ */
639
+ config: Config
640
+ /**
641
+ * Plugins that threw during generation, paired with their errors.
642
+ */
643
+ failedPlugins: Set<{ plugin: Plugin; error: Error }>
644
+ /**
645
+ * `'success'` when all plugins completed without errors, `'failed'` otherwise.
646
+ */
647
+ status: 'success' | 'failed'
648
+ /**
649
+ * High-resolution start time from `process.hrtime()`.
650
+ */
651
+ hrStart: [number, number]
652
+ /**
653
+ * Total number of files created during this run.
654
+ */
655
+ filesCreated: number
656
+ /**
657
+ * Elapsed milliseconds per plugin, keyed by plugin name.
658
+ */
659
+ pluginTimings?: Map<Plugin['name'], number>
660
+ }
661
+
662
+ export type KubbVersionNewContext = {
663
+ /**
664
+ * The installed Kubb version.
665
+ */
666
+ currentVersion: string
667
+ /**
668
+ * The newest available version on npm.
669
+ */
670
+ latestVersion: string
671
+ }
672
+
673
+ export type KubbInfoContext = {
674
+ /**
675
+ * Human-readable info message.
676
+ */
677
+ message: string
678
+ /**
679
+ * Optional supplementary detail.
680
+ */
681
+ info?: string
682
+ }
683
+
684
+ export type KubbErrorContext = {
685
+ /**
686
+ * The caught error.
687
+ */
688
+ error: Error
689
+ /**
690
+ * Optional structured metadata for additional context.
691
+ */
692
+ meta?: Record<string, unknown>
693
+ }
694
+
695
+ export type KubbSuccessContext = {
696
+ /**
697
+ * Human-readable success message.
698
+ */
699
+ message: string
700
+ /**
701
+ * Optional supplementary detail.
702
+ */
703
+ info?: string
704
+ }
705
+
706
+ export type KubbWarnContext = {
707
+ /**
708
+ * Human-readable warning message.
709
+ */
710
+ message: string
711
+ /**
712
+ * Optional supplementary detail.
713
+ */
714
+ info?: string
715
+ }
716
+
717
+ export type KubbDebugContext = {
718
+ /**
719
+ * Timestamp when the debug entry was created.
720
+ */
721
+ date: Date
722
+ /**
723
+ * One or more log lines to emit.
724
+ */
725
+ logs: Array<string>
726
+ /**
727
+ * Optional source file name associated with this entry.
728
+ */
729
+ fileName?: string
730
+ }
731
+
732
+ export type KubbFilesProcessingStartContext = {
733
+ /**
734
+ * Files about to be serialised and written.
735
+ */
736
+ files: Array<FileNode>
737
+ }
738
+
739
+ export type KubbFileProcessingUpdateContext = {
740
+ /**
741
+ * Number of files processed so far in this batch.
742
+ */
743
+ processed: number
744
+ /**
745
+ * Total number of files in this batch.
746
+ */
747
+ total: number
748
+ /**
749
+ * Completion percentage (`0`–`100`).
750
+ */
751
+ percentage: number
752
+ /**
753
+ * Serialised file content, or `undefined` when the file produced no output.
754
+ */
755
+ source?: string
756
+ /**
757
+ * The file that was just processed.
758
+ */
759
+ file: FileNode
760
+ /**
761
+ * Resolved configuration for this build.
762
+ */
763
+ config: Config
764
+ }
765
+
766
+ export type KubbFilesProcessingEndContext = {
767
+ /**
768
+ * All files that were serialised in this batch.
769
+ */
770
+ files: Array<FileNode>
771
+ }
772
+
773
+ export type KubbHookStartContext = {
774
+ /**
775
+ * Optional identifier for correlating start/end events.
776
+ */
777
+ id?: string
778
+ /**
779
+ * The shell command that is about to run.
780
+ */
781
+ command: string
782
+ /**
783
+ * Parsed argument list, when available.
784
+ */
785
+ args?: readonly string[]
786
+ }
787
+
788
+ export type KubbHookEndContext = {
789
+ /**
790
+ * Optional identifier matching the corresponding `kubb:hook:start` event.
791
+ */
792
+ id?: string
793
+ /**
794
+ * The shell command that ran.
795
+ */
796
+ command: string
797
+ /**
798
+ * Parsed argument list, when available.
799
+ */
800
+ args?: readonly string[]
801
+ /**
802
+ * `true` when the command exited with code `0`.
803
+ */
804
+ success: boolean
805
+ /**
806
+ * Error thrown by the command, or `null` on success.
807
+ */
808
+ error: Error | null
809
+ }
810
+
811
+ /**
812
+ * CLI options derived from command-line flags.
813
+ */
814
+ export type CLIOptions = {
815
+ /**
816
+ * Path to the Kubb config file.
817
+ */
818
+ config?: string
819
+ /**
820
+ * Re-run generation whenever input files change.
821
+ */
822
+ watch?: boolean
823
+ /**
824
+ * Controls how much output the CLI prints.
825
+ *
826
+ * @default 'silent'
827
+ */
828
+ logLevel?: 'silent' | 'info' | 'debug'
829
+ }
830
+
831
+ /**
832
+ * All accepted forms of a Kubb configuration.
833
+ * Accepts `Config`/`Config[]`/promise or a factory (optionally receiving `TCliOptions`.
834
+ */
835
+ export type PossibleConfig<TCliOptions = undefined> =
836
+ | PossiblePromise<Config | Config[]>
837
+ | ((...args: [TCliOptions] extends [undefined] ? [] : [TCliOptions]) => PossiblePromise<Config | Config[]>)
18
838
 
19
839
  type SetupOptions = {
20
840
  hooks?: AsyncEventEmitter<KubbHooks>
@@ -25,39 +845,170 @@ type SetupOptions = {
25
845
  */
26
846
  export type BuildOutput = {
27
847
  /**
28
- * Plugins that threw during installation, paired with the caught error.
848
+ * Plugins that threw during generation, paired with their errors.
29
849
  */
30
850
  failedPlugins: Set<{ plugin: Plugin; error: Error }>
851
+ /**
852
+ * All files generated during this build.
853
+ */
31
854
  files: Array<FileNode>
32
- driver: PluginDriver
33
855
  /**
34
- * Elapsed time in milliseconds for each plugin, keyed by plugin name.
856
+ * The plugin driver that orchestrated this build.
857
+ */
858
+ driver: KubbDriver
859
+ /**
860
+ * Elapsed milliseconds per plugin, keyed by plugin name.
35
861
  */
36
862
  pluginTimings: Map<string, number>
863
+ /**
864
+ * Top-level error when the build threw before completing, otherwise `undefined`.
865
+ */
37
866
  error?: Error
38
867
  /**
39
- * Raw generated source, keyed by absolute file path.
868
+ * Read-only view of every file written during this build.
869
+ * Reads go straight to `config.storage` — nothing extra is held in memory.
870
+ *
871
+ * @example Read a generated file
872
+ * `const code = await buildOutput.storage.getItem('/src/gen/pet.ts')`
873
+ *
874
+ * @example List all generated file paths
875
+ * `const paths = await buildOutput.storage.getKeys()`
40
876
  */
41
- sources: Map<string, string>
877
+ storage: Storage
878
+ }
879
+
880
+ /**
881
+ * Kubb code generation instance returned by {@link createKubb}.
882
+ *
883
+ * Use this when orchestrating multiple builds, inspecting plugin timings, or integrating Kubb into a larger toolchain.
884
+ * For a single one-off build, chain directly: `await createKubb(config).build()`.
885
+ */
886
+ export type Kubb = {
887
+ /**
888
+ * Shared event emitter for lifecycle and status events. Attach listeners before calling `setup()` or `build()`.
889
+ */
890
+ readonly hooks: AsyncEventEmitter<KubbHooks>
891
+ /**
892
+ * Read-only view of the files from the most recent `build()` or `safeBuild()` call.
893
+ * Only populated after the build completes.
894
+ *
895
+ * Keys are scoped to the current run. Reads go straight to `config.storage`,
896
+ * so nothing extra is held in memory.
897
+ *
898
+ * @example Read a generated file
899
+ * ```ts
900
+ * const { storage } = await kubb.safeBuild()
901
+ * const code = await storage.getItem('/src/gen/pet.ts')
902
+ * ```
903
+ *
904
+ * @example Walk every generated file
905
+ * ```ts
906
+ * for (const path of await kubb.storage.getKeys()) {
907
+ * const code = await kubb.storage.getItem(path)
908
+ * }
909
+ * ```
910
+ */
911
+ readonly storage: Storage
912
+ /**
913
+ * Plugin driver managing all plugins. Available after `setup()` completes.
914
+ */
915
+ readonly driver: KubbDriver
916
+ /**
917
+ * Resolved configuration with defaults applied. Available after `setup()` completes.
918
+ */
919
+ readonly config: Config
920
+ /**
921
+ * Resolves config and initializes the driver. `build()` calls this automatically.
922
+ */
923
+ setup(): Promise<void>
924
+ /**
925
+ * Runs the full pipeline and throws on any plugin error. Automatically calls `setup()` if needed.
926
+ */
927
+ build(): Promise<BuildOutput>
928
+ /**
929
+ * Runs the full pipeline and captures errors in `BuildOutput` instead of throwing. Automatically calls `setup()` if needed.
930
+ */
931
+ safeBuild(): Promise<BuildOutput>
42
932
  }
43
933
 
44
934
  type SetupResult = {
45
935
  hooks: AsyncEventEmitter<KubbHooks>
46
- driver: PluginDriver
47
- sources: Map<string, string>
936
+ driver: KubbDriver
937
+ storage: Storage
48
938
  config: Config
49
- storage: Storage | null
939
+ dispose: () => void
940
+ [Symbol.dispose](): void
941
+ }
942
+
943
+ /**
944
+ * Builds a `Storage` view scoped to the file paths produced by the current build.
945
+ *
946
+ * Reads delegate to the underlying `storage` (typically `fsStorage()`) so source bytes
947
+ * stay where they were written instead of being held in an extra in-memory map.
948
+ * Writing via `setItem` stores the content in the underlying storage and registers the
949
+ * key so subsequent reads and `getKeys` are scoped to this build's output.
950
+ */
951
+ function createSourcesView(storage: Storage): Storage {
952
+ const paths = new Set<string>()
953
+
954
+ return createStorage(() => ({
955
+ name: `${storage.name}:sources`,
956
+ async hasItem(key: string) {
957
+ return paths.has(key) && (await storage.hasItem(key))
958
+ },
959
+ async getItem(key: string) {
960
+ return paths.has(key) ? storage.getItem(key) : null
961
+ },
962
+ async setItem(key: string, value: string) {
963
+ paths.add(key)
964
+ await storage.setItem(key, value)
965
+ },
966
+ async removeItem(key: string) {
967
+ paths.delete(key)
968
+ await storage.removeItem(key)
969
+ },
970
+ async getKeys(base?: string) {
971
+ if (!base) return [...paths]
972
+ const result: Array<string> = []
973
+ for (const key of paths) {
974
+ if (key.startsWith(base)) result.push(key)
975
+ }
976
+ return result
977
+ },
978
+ async clear() {
979
+ paths.clear()
980
+ await storage.clear()
981
+ },
982
+ }))()
50
983
  }
51
984
 
52
985
  async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promise<SetupResult> {
53
986
  const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
54
-
55
- const sources: Map<string, string> = new Map<string, string>()
56
- const diagnosticInfo = getDiagnosticInfo()
57
-
58
- if (Array.isArray(userConfig.input)) {
59
- await hooks.emit('kubb:warn', { message: 'This feature is still under development — use with caution' })
987
+ const config: Config = {
988
+ ...userConfig,
989
+ root: userConfig.root || process.cwd(),
990
+ parsers: userConfig.parsers ?? [],
991
+ output: {
992
+ format: false,
993
+ lint: false,
994
+ extension: DEFAULT_EXTENSION,
995
+ defaultBanner: DEFAULT_BANNER,
996
+ ...userConfig.output,
997
+ },
998
+ storage: userConfig.storage ?? fsStorage(),
999
+ devtools: userConfig.devtools
1000
+ ? {
1001
+ studioUrl: DEFAULT_STUDIO_URL,
1002
+ ...(typeof userConfig.devtools === 'boolean' ? {} : userConfig.devtools),
1003
+ }
1004
+ : undefined,
1005
+ plugins: (userConfig.plugins ?? []) as unknown as Config['plugins'],
60
1006
  }
1007
+ const driver = new KubbDriver(config, {
1008
+ hooks,
1009
+ })
1010
+ const storage = createSourcesView(config.storage)
1011
+ const diagnosticInfo = getDiagnosticInfo()
61
1012
 
62
1013
  await hooks.emit('kubb:debug', {
63
1014
  date: new Date(),
@@ -68,9 +1019,10 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
68
1019
  ` • Output: ${userConfig.output?.path || 'not specified'}`,
69
1020
  ` • Plugins: ${userConfig.plugins?.length || 0}`,
70
1021
  'Output Settings:',
71
- ` • Storage: ${userConfig.storage ? `custom(${userConfig.storage.name})` : userConfig.output?.write === false ? 'disabled' : 'filesystem (default)'}`,
1022
+ ` • Storage: ${config.storage.name}`,
72
1023
  ` • Formatter: ${userConfig.output?.format || 'none'}`,
73
1024
  ` • Linter: ${userConfig.output?.lint || 'none'}`,
1025
+ `Running adapter: ${config.adapter?.name || 'none'}`,
74
1026
  'Environment:',
75
1027
  Object.entries(diagnosticInfo)
76
1028
  .map(([key, value]) => ` • ${key}: ${value}`)
@@ -100,209 +1052,314 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
100
1052
  }
101
1053
  }
102
1054
 
103
- if (!userConfig.adapter) {
104
- throw new Error('Adapter should be defined')
105
- }
106
-
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
-
131
1055
  if (config.output.clean) {
132
1056
  await hooks.emit('kubb:debug', {
133
1057
  date: new Date(),
134
1058
  logs: ['Cleaning output directories', ` • Output: ${config.output.path}`],
135
1059
  })
136
- await storage?.clear(resolve(config.root, config.output.path))
137
- }
138
-
139
- const driver = new PluginDriver(config, {
140
- hooks,
141
- })
142
1060
 
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
- }
1061
+ await config.storage.clear(resolve(config.root, config.output.path))
157
1062
  }
158
1063
 
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
- })
1064
+ await driver.setup()
181
1065
 
182
1066
  return {
183
1067
  config,
184
1068
  hooks,
185
1069
  driver,
186
- sources,
187
1070
  storage,
1071
+ dispose,
1072
+ [Symbol.dispose]: dispose,
1073
+ }
1074
+
1075
+ function dispose() {
1076
+ driver.dispose()
188
1077
  }
189
1078
  }
190
1079
 
191
- /**
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.
200
- */
201
- async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorContext): Promise<void> {
202
- const { adapter, inputNode, resolver, driver } = context
203
- const { exclude, include, override } = plugin.options
1080
+ type GeneratorEntry = { plugin: NormalizedPlugin; context: GeneratorContext; hrStart: ReturnType<typeof process.hrtime> }
204
1081
 
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
- }
1082
+ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1083
+ using _cleanup = setupResult
1084
+ const { driver, hooks, storage } = setupResult
208
1085
 
209
- function resolveRenderer(gen: Generator): RendererFactory | undefined {
210
- return gen.renderer === null ? undefined : (gen.renderer ?? plugin.renderer ?? context.config.renderer)
1086
+ const failedPlugins = new Set<{ plugin: Plugin; error: Error }>()
1087
+ const pluginTimings = new Map<string, number>()
1088
+ const config = driver.config
1089
+ const writtenPaths = new Set<string>()
1090
+ const parsersMap = new Map<FileNode['extname'], Parser>()
1091
+ const fileProcessor = new FileProcessor()
1092
+
1093
+ for (const parser of config.parsers) {
1094
+ if (parser.extNames) {
1095
+ for (const extname of parser.extNames) {
1096
+ parsersMap.set(extname, parser)
1097
+ }
1098
+ }
211
1099
  }
212
1100
 
213
- const generators = plugin.generators ?? []
214
- const collectedOperations: Array<OperationNode> = []
1101
+ async function flushPendingFiles(): Promise<void> {
1102
+ const files = driver.fileManager.files.filter((f) => !writtenPaths.has(f.path))
1103
+ if (files.length === 0) {
1104
+ return
1105
+ }
1106
+
1107
+ await hooks.emit('kubb:debug', {
1108
+ date: new Date(),
1109
+ logs: [`Writing ${files.length} files...`],
1110
+ })
1111
+
1112
+ await hooks.emit('kubb:files:processing:start', { files })
1113
+
1114
+ const stream = fileProcessor.stream(files, { parsers: parsersMap, extension: config.output.extension })
1115
+
1116
+ const queue: Array<Promise<void>> = []
1117
+ for (const { file, source, processed, total, percentage } of stream) {
1118
+ writtenPaths.add(file.path)
1119
+ queue.push(
1120
+ (async () => {
1121
+ await hooks.emit('kubb:file:processing:update', { file, source, processed, total, percentage, config })
1122
+ if (source) {
1123
+ await storage.setItem(file.path, source)
1124
+ }
1125
+ })(),
1126
+ )
1127
+ if (queue.length >= STREAM_FLUSH_EVERY) {
1128
+ await Promise.all(queue.splice(0))
1129
+ }
1130
+ }
1131
+ await Promise.all(queue)
215
1132
 
216
- const generatorContext = {
217
- ...context,
218
- resolver: driver.getResolver(plugin.name),
1133
+ await hooks.emit('kubb:files:processing:end', { files })
1134
+ await hooks.emit('kubb:debug', {
1135
+ date: new Date(),
1136
+ logs: [`✓ File write process completed for ${files.length} files`],
1137
+ })
219
1138
  }
220
1139
 
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)
1140
+ async function dispatchOperationsToGenerators(
1141
+ generators: Generator[],
1142
+ collectedOperations: OperationNode[],
1143
+ ctx: GeneratorContext,
1144
+ rendererFor: (gen: Generator) => RendererFactory | undefined,
1145
+ ): Promise<void> {
1146
+ for (const gen of generators) {
1147
+ if (!gen.operations) continue
1148
+ const result = await gen.operations(collectedOperations, ctx)
1149
+ await applyHookResult({ result, driver, rendererFactory: rendererFor(gen) })
1150
+ }
1151
+ await driver.hooks.emit('kubb:generate:operations', collectedOperations, ctx)
233
1152
  }
234
1153
 
235
- await walk(inputNode, {
236
- depth: 'shallow',
237
- async schema(node) {
238
- const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
1154
+ /**
1155
+ * Single-pass fan-out: iterates all schemas and operations once, distributing each node
1156
+ * to every generator-plugin in parallel. This replaces the N-pass-per-plugin pattern
1157
+ * (each plugin getting its own iterator) with one parse pass fanned to all plugins,
1158
+ * eliminating the N×parse-time overhead for multi-plugin builds.
1159
+ */
1160
+ async function runPlugins(entries: Array<GeneratorEntry>): Promise<void> {
1161
+ type PluginState = {
1162
+ plugin: NormalizedPlugin
1163
+ generatorContext: GeneratorContext
1164
+ generators: Generator[]
1165
+ hrStart: ReturnType<typeof process.hrtime>
1166
+ failed: boolean
1167
+ error: Error | undefined
1168
+ /**
1169
+ * `true` when the plugin's options have no `include`, `exclude`, or `override`
1170
+ * filters. The per-node `resolveOptions` call always returns the same `options`
1171
+ * reference in that case, so the inner loop can skip it entirely.
1172
+ */
1173
+ optionsAreStatic: boolean
1174
+ /**
1175
+ * Set when the plugin has operation-based includes (tag, operationId, path, method, contentType)
1176
+ * but no schemaName includes. Schema nodes whose name is not in this set are skipped,
1177
+ * matching the pruning behavior of the eager path.
1178
+ */
1179
+ allowedSchemaNames: Set<string> | undefined
1180
+ }
239
1181
 
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
1182
+ const { schemas, operations } = driver.inputNode!
1183
+ const operationFilterTypes = new Set(['tag', 'operationId', 'path', 'method', 'contentType'])
1184
+ const states: PluginState[] = entries.map(({ plugin, context, hrStart }) => {
1185
+ const { exclude, include, override } = plugin.options
1186
+ const hasExclude = Array.isArray(exclude) && exclude.length > 0
1187
+ const hasInclude = Array.isArray(include) && include.length > 0
1188
+ const hasOverride = Array.isArray(override) && override.length > 0
1189
+ return {
1190
+ plugin,
1191
+ generatorContext: { ...context, resolver: driver.getResolver(plugin.name) },
1192
+ generators: plugin.generators ?? [],
1193
+ hrStart,
1194
+ failed: false,
1195
+ error: undefined,
1196
+ optionsAreStatic: !hasExclude && !hasInclude && !hasOverride,
1197
+ allowedSchemaNames: undefined,
243
1198
  }
1199
+ })
244
1200
 
245
- const options = resolver.resolveOptions(transformedNode, {
246
- options: plugin.options,
247
- exclude,
248
- include,
249
- override,
250
- })
251
- if (options === null) return
1201
+ // Pre-scan: compute allowedSchemaNames for plugins that use operation-based includes
1202
+ // without schemaName filters. Each AsyncIterable yields a fresh iterator on every call,
1203
+ // so consuming them here does not affect the main dispatch passes below.
1204
+ const pruningStates = states.filter(({ plugin }) => {
1205
+ const { include } = plugin.options
1206
+ return (include?.some(({ type }) => operationFilterTypes.has(type)) ?? false) && !(include?.some(({ type }) => type === 'schemaName') ?? false)
1207
+ })
1208
+
1209
+ if (pruningStates.length > 0) {
1210
+ // Known trade-off: computing the reachable-schema set for operation-based includes
1211
+ // requires the full schema graph in memory at once — there is no way to determine
1212
+ // transitive reachability from a single schema node in isolation.
1213
+ // `allSchemas` is released as soon as this block exits; it is never held past
1214
+ // the pruning pre-scan. The main dispatch passes below each get their own
1215
+ // fresh iterator from the AsyncIterable, so this consumption does not affect them.
1216
+ const allSchemas: SchemaNode[] = []
1217
+ for await (const schema of schemas) {
1218
+ allSchemas.push(schema)
1219
+ }
252
1220
 
253
- const ctx = { ...generatorContext, options }
1221
+ // Collect the included operations for each pruning plugin in one shared pass.
1222
+ const includedOpsByState = new Map<PluginState, OperationNode[]>(pruningStates.map((s) => [s, []]))
1223
+ for await (const operation of operations) {
1224
+ for (const state of pruningStates) {
1225
+ const { exclude, include, override } = state.plugin.options
1226
+ const options = state.generatorContext.resolver.resolveOptions(operation, { options: state.plugin.options, exclude, include, override })
1227
+ if (options !== null) includedOpsByState.get(state)?.push(operation)
1228
+ }
1229
+ }
254
1230
 
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))
1231
+ // Derive the allowed schema name set per pruning plugin.
1232
+ for (const state of pruningStates) {
1233
+ state.allowedSchemaNames = collectUsedSchemaNames(includedOpsByState.get(state) ?? [], allSchemas)
259
1234
  }
1235
+ }
260
1236
 
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)
1237
+ function resolveRendererFor(gen: Generator, state: PluginState): RendererFactory | undefined {
1238
+ return gen.renderer === null ? undefined : (gen.renderer ?? state.plugin.renderer ?? state.generatorContext.config.renderer)
1239
+ }
1240
+
1241
+ async function dispatchSchema(state: PluginState, node: SchemaNode): Promise<void> {
1242
+ if (state.failed) return
1243
+
1244
+ try {
1245
+ const { plugin, generatorContext, generators } = state
1246
+
1247
+ const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
1248
+
1249
+ // Skip named top-level schemas not reachable from any included operation.
1250
+ if (state.allowedSchemaNames !== undefined && transformedNode.name && !state.allowedSchemaNames.has(transformedNode.name)) {
1251
+ return
1252
+ }
1253
+
1254
+ const { exclude, include, override } = plugin.options
1255
+ const options = state.optionsAreStatic
1256
+ ? plugin.options
1257
+ : generatorContext.resolver.resolveOptions(transformedNode, { options: plugin.options, exclude, include, override })
1258
+ if (options === null) return
1259
+
1260
+ const ctx = { ...generatorContext, options }
1261
+
1262
+ for (const gen of generators) {
1263
+ if (!gen.schema) continue
1264
+ const raw = gen.schema(transformedNode, ctx)
1265
+ const result = isPromise(raw) ? await raw : raw
1266
+ const applied = applyHookResult({ result, driver, rendererFactory: resolveRendererFor(gen, state) })
1267
+ if (isPromise(applied)) await applied
1268
+ }
1269
+
1270
+ await driver.hooks.emit('kubb:generate:schema', transformedNode, ctx)
1271
+ } catch (caughtError) {
1272
+ state.failed = true
1273
+ state.error = caughtError as Error
1274
+ }
1275
+ }
1276
+
1277
+ async function dispatchOperation(state: PluginState, node: OperationNode): Promise<void> {
1278
+ if (state.failed) return
1279
+
1280
+ try {
1281
+ const { plugin, generatorContext, generators } = state
1282
+
1283
+ const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
1284
+ const { exclude, include, override } = plugin.options
1285
+ const options = state.optionsAreStatic
1286
+ ? plugin.options
1287
+ : generatorContext.resolver.resolveOptions(transformedNode, { options: plugin.options, exclude, include, override })
1288
+ if (options === null) return
273
1289
 
274
1290
  const ctx = { ...generatorContext, options }
275
1291
 
276
1292
  for (const gen of generators) {
277
1293
  if (!gen.operation) continue
278
- const result = await gen.operation(transformedNode, ctx)
279
- await applyHookResult(result, driver, resolveRenderer(gen))
1294
+ const raw = gen.operation(transformedNode, ctx)
1295
+ const result = isPromise(raw) ? await raw : raw
1296
+ const applied = applyHookResult({ result, driver, rendererFactory: resolveRendererFor(gen, state) })
1297
+ if (isPromise(applied)) await applied
280
1298
  }
281
1299
 
282
1300
  await driver.hooks.emit('kubb:generate:operation', transformedNode, ctx)
1301
+ } catch (caughtError) {
1302
+ state.failed = true
1303
+ state.error = caughtError as Error
283
1304
  }
284
- },
285
- })
1305
+ }
286
1306
 
287
- if (collectedOperations.length > 0) {
288
- const ctx = { ...generatorContext, options: plugin.options }
1307
+ // Batch schemas: SCHEMA_PARALLEL nodes dispatched across all plugins concurrently.
1308
+ // Per-plugin work inside dispatchSchema stays sequential so FileManager.upsert
1309
+ // ordering for any single plugin chain remains deterministic.
1310
+ await forBatches(schemas, (nodes) => Promise.all(nodes.flatMap((n) => states.map((state) => dispatchSchema(state, n)))), {
1311
+ concurrency: SCHEMA_PARALLEL,
1312
+ flush: flushPendingFiles,
1313
+ })
289
1314
 
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
- }
1315
+ const collectedOperations: OperationNode[] = []
295
1316
 
296
- await driver.hooks.emit('kubb:generate:operations', collectedOperations, ctx)
297
- }
298
- }
1317
+ await forBatches(
1318
+ operations,
1319
+ (nodes) => {
1320
+ collectedOperations.push(...nodes)
1321
+ return Promise.all(nodes.flatMap((n) => states.map((state) => dispatchOperation(state, n))))
1322
+ },
1323
+ { concurrency: SCHEMA_PARALLEL, flush: flushPendingFiles },
1324
+ )
1325
+
1326
+ for (const state of states) {
1327
+ if (!state.failed) {
1328
+ try {
1329
+ const { plugin, generatorContext, generators } = state
1330
+ const ctx = { ...generatorContext, options: plugin.options }
1331
+ await dispatchOperationsToGenerators(generators, collectedOperations, ctx, (gen) => resolveRendererFor(gen, state))
1332
+ } catch (caughtError) {
1333
+ state.failed = true
1334
+ state.error = caughtError as Error
1335
+ }
1336
+ }
299
1337
 
300
- async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
301
- const { driver, hooks, sources, storage } = setupResult
1338
+ const duration = getElapsedMs(state.hrStart)
1339
+ pluginTimings.set(state.plugin.name, duration)
302
1340
 
303
- const failedPlugins = new Set<{ plugin: Plugin; error: Error }>()
304
- const pluginTimings = new Map<string, number>()
305
- const config = driver.config
1341
+ await driver.hooks.emit('kubb:plugin:end', {
1342
+ plugin: state.plugin,
1343
+ duration,
1344
+ success: !state.failed,
1345
+ ...(state.failed && state.error ? { error: state.error } : {}),
1346
+ config: driver.config,
1347
+ get files() {
1348
+ return driver.fileManager.files
1349
+ },
1350
+ upsertFile: (...files) => driver.fileManager.upsert(...files),
1351
+ })
1352
+
1353
+ if (state.failed && state.error) {
1354
+ failedPlugins.add({ plugin: state.plugin, error: state.error })
1355
+ }
1356
+
1357
+ await driver.hooks.emit('kubb:debug', {
1358
+ date: new Date(),
1359
+ logs: [state.failed ? '✗ Plugin start failed' : `✓ Plugin started successfully (${formatMs(duration)})`],
1360
+ })
1361
+ }
1362
+ }
306
1363
 
307
1364
  try {
308
1365
  await driver.emitSetupHooks()
@@ -311,7 +1368,7 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
311
1368
  await hooks.emit('kubb:build:start', {
312
1369
  config,
313
1370
  adapter: driver.adapter,
314
- inputNode: driver.inputNode,
1371
+ meta: driver.inputNode.meta,
315
1372
  getPlugin: driver.getPlugin.bind(driver),
316
1373
  get files() {
317
1374
  return driver.fileManager.files
@@ -320,47 +1377,25 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
320
1377
  })
321
1378
  }
322
1379
 
1380
+ // Always run the plugin lifecycle so middleware hooks (kubb:plugin:start,
1381
+ // kubb:plugin:end) fire even when no adapter is configured.
1382
+ // Generator-plugins are collected for the stream fan-out pass below.
1383
+ const generatorPlugins: Array<GeneratorEntry> = []
1384
+
323
1385
  for (const plugin of driver.plugins.values()) {
324
1386
  const context = driver.getContext(plugin)
325
1387
  const hrStart = process.hrtime()
326
1388
 
327
1389
  try {
328
- const timestamp = new Date()
329
-
330
1390
  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
1391
  await hooks.emit('kubb:debug', {
356
1392
  date: new Date(),
357
- logs: [`✓ Plugin started successfully (${formatMs(duration)})`],
1393
+ logs: ['Starting plugin...', ` • Plugin Name: ${plugin.name}`],
358
1394
  })
359
1395
  } catch (caughtError) {
360
1396
  const error = caughtError as Error
361
- const errorTimestamp = new Date()
362
1397
  const duration = getElapsedMs(hrStart)
363
-
1398
+ pluginTimings.set(plugin.name, duration)
364
1399
  await hooks.emit('kubb:plugin:end', {
365
1400
  plugin,
366
1401
  duration,
@@ -372,19 +1407,54 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
372
1407
  },
373
1408
  upsertFile: (...files) => driver.fileManager.upsert(...files),
374
1409
  })
1410
+ failedPlugins.add({ plugin, error })
1411
+ continue
1412
+ }
375
1413
 
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
- })
1414
+ if (plugin.generators?.length || driver.hasEventGenerators(plugin.name)) {
1415
+ generatorPlugins.push({ plugin, context, hrStart })
1416
+ continue
1417
+ }
1418
+ // No generators: plugin ran via setup hooks; finish it now.
1419
+ const duration = getElapsedMs(hrStart)
1420
+ pluginTimings.set(plugin.name, duration)
1421
+ await hooks.emit('kubb:plugin:end', {
1422
+ plugin,
1423
+ duration,
1424
+ success: true,
1425
+ config,
1426
+ get files() {
1427
+ return driver.fileManager.files
1428
+ },
1429
+ upsertFile: (...files) => driver.fileManager.upsert(...files),
1430
+ })
1431
+ await hooks.emit('kubb:debug', {
1432
+ date: new Date(),
1433
+ logs: [`✓ Plugin started successfully (${formatMs(duration)})`],
1434
+ })
1435
+ }
386
1436
 
387
- failedPlugins.add({ plugin, error })
1437
+ if (generatorPlugins.length > 0) {
1438
+ if (driver.inputNode) {
1439
+ // Normal path: fan-out schemas and operations to all generator-plugins in one pass.
1440
+ await withDrain(() => runPlugins(generatorPlugins), flushPendingFiles)
1441
+ } else {
1442
+ // No adapter configured — generator-plugins have nothing to process.
1443
+ // Still emit plugin:end so middleware hooks (e.g. barrel) complete their lifecycle.
1444
+ for (const { plugin, hrStart } of generatorPlugins) {
1445
+ const duration = getElapsedMs(hrStart)
1446
+ pluginTimings.set(plugin.name, duration)
1447
+ await hooks.emit('kubb:plugin:end', {
1448
+ plugin,
1449
+ duration,
1450
+ success: true,
1451
+ config,
1452
+ get files() {
1453
+ return driver.fileManager.files
1454
+ },
1455
+ upsertFile: (...files) => driver.fileManager.upsert(...files),
1456
+ })
1457
+ }
388
1458
  }
389
1459
  }
390
1460
 
@@ -396,52 +1466,9 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
396
1466
  upsertFile: (...files) => driver.fileManager.upsert(...files),
397
1467
  })
398
1468
 
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
- }
408
- }
409
-
410
- const fileProcessor = new FileProcessor()
411
-
412
- await hooks.emit('kubb:debug', {
413
- date: new Date(),
414
- logs: [`Writing ${files.length} files...`],
415
- })
1469
+ await flushPendingFiles()
416
1470
 
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
- })
1471
+ const files = driver.fileManager.files
445
1472
 
446
1473
  await hooks.emit('kubb:build:end', {
447
1474
  files,
@@ -454,7 +1481,7 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
454
1481
  files,
455
1482
  driver,
456
1483
  pluginTimings,
457
- sources,
1484
+ storage,
458
1485
  }
459
1486
  } catch (error) {
460
1487
  return {
@@ -463,15 +1490,13 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
463
1490
  driver,
464
1491
  pluginTimings,
465
1492
  error: error as Error,
466
- sources,
1493
+ storage,
467
1494
  }
468
- } finally {
469
- driver.dispose()
470
1495
  }
471
1496
  }
472
1497
 
473
1498
  async function build(setupResult: SetupResult): Promise<BuildOutput> {
474
- const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(setupResult)
1499
+ const { files, driver, failedPlugins, pluginTimings, error, storage } = await safeBuild(setupResult)
475
1500
 
476
1501
  if (error) {
477
1502
  throw error
@@ -489,28 +1514,33 @@ async function build(setupResult: SetupResult): Promise<BuildOutput> {
489
1514
  driver,
490
1515
  pluginTimings,
491
1516
  error: undefined,
492
- sources,
1517
+ storage,
493
1518
  }
494
1519
  }
495
1520
 
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
- }
502
- }
503
-
504
- if ('data' in config.input) {
505
- return { type: 'data', data: config.input.data }
506
- }
507
-
508
- if (new URLPath(config.input.path).isURL) {
509
- return { type: 'path', path: config.input.path }
510
- }
1521
+ /**
1522
+ * Returns a snapshot of the current runtime environment.
1523
+ *
1524
+ * Useful for attaching context to debug logs and error reports so that
1525
+ * issues can be reproduced without manual information gathering.
1526
+ */
1527
+ export function getDiagnosticInfo() {
1528
+ return {
1529
+ nodeVersion,
1530
+ KubbVersion,
1531
+ platform: process.platform,
1532
+ arch: process.arch,
1533
+ cwd: process.cwd(),
1534
+ } as const
1535
+ }
511
1536
 
512
- const resolved = resolve(config.root, config.input.path)
513
- return { type: 'path', path: resolved }
1537
+ /**
1538
+ * Type guard to check if a given config has an `input.path`.
1539
+ */
1540
+ export function isInputPath(config: UserConfig | undefined): config is UserConfig<InputPath> & { input: InputPath }
1541
+ export function isInputPath(config: Config | undefined): config is Config<InputPath> & { input: InputPath }
1542
+ export function isInputPath(config: Config | UserConfig | undefined): config is (Config<InputPath> | UserConfig<InputPath>) & { input: InputPath } {
1543
+ return typeof config?.input === 'object' && config.input !== null && 'path' in config.input
514
1544
  }
515
1545
 
516
1546
  type CreateKubbOptions = {
@@ -521,7 +1551,7 @@ type CreateKubbOptions = {
521
1551
  * Creates a Kubb instance bound to a single config entry.
522
1552
  *
523
1553
  * 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`)
1554
+ * `setup()`. The instance then holds shared state (`hooks`, `storage`, `driver`, `config`)
525
1555
  * across the `setup → build` lifecycle. Attach event listeners to `kubb.hooks` before
526
1556
  * calling `setup()` or `build()`.
527
1557
  *
@@ -544,14 +1574,23 @@ export function createKubb(userConfig: UserConfig, options: CreateKubbOptions =
544
1574
  get hooks() {
545
1575
  return hooks
546
1576
  },
547
- get sources() {
548
- return setupResult?.sources ?? new Map()
1577
+ get storage() {
1578
+ if (!setupResult) {
1579
+ throw new Error('[kubb] setup() must be called before accessing storage')
1580
+ }
1581
+ return setupResult.storage
549
1582
  },
550
1583
  get driver() {
551
- return setupResult?.driver
1584
+ if (!setupResult) {
1585
+ throw new Error('[kubb] setup() must be called before accessing driver')
1586
+ }
1587
+ return setupResult.driver
552
1588
  },
553
1589
  get config() {
554
- return setupResult?.config
1590
+ if (!setupResult) {
1591
+ throw new Error('[kubb] setup() must be called before accessing config')
1592
+ }
1593
+ return setupResult.config
555
1594
  },
556
1595
  async setup() {
557
1596
  setupResult = await setup(userConfig, { hooks })