@kubb/core 5.0.0-beta.1 → 5.0.0-beta.11

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 (40) hide show
  1. package/README.md +9 -39
  2. package/dist/{PluginDriver-BXibeQk-.cjs → PluginDriver-C1OsqGBJ.cjs} +106 -56
  3. package/dist/PluginDriver-C1OsqGBJ.cjs.map +1 -0
  4. package/dist/{PluginDriver-DV3p2Hky.js → PluginDriver-CGypdXHg.js} +101 -57
  5. package/dist/PluginDriver-CGypdXHg.js.map +1 -0
  6. package/dist/{types-CuNocrbJ.d.ts → createKubb-BSfMDBwR.d.ts} +1533 -1505
  7. package/dist/index.cjs +249 -209
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +2 -185
  10. package/dist/index.js +249 -209
  11. package/dist/index.js.map +1 -1
  12. package/dist/mocks.cjs +1 -1
  13. package/dist/mocks.cjs.map +1 -1
  14. package/dist/mocks.d.ts +1 -1
  15. package/dist/mocks.js +1 -1
  16. package/dist/mocks.js.map +1 -1
  17. package/package.json +5 -12
  18. package/src/FileManager.ts +8 -0
  19. package/src/FileProcessor.ts +12 -7
  20. package/src/PluginDriver.ts +49 -7
  21. package/src/constants.ts +6 -2
  22. package/src/createAdapter.ts +77 -1
  23. package/src/createKubb.ts +973 -141
  24. package/src/defineGenerator.ts +92 -4
  25. package/src/defineLogger.ts +42 -3
  26. package/src/defineMiddleware.ts +1 -1
  27. package/src/definePlugin.ts +304 -8
  28. package/src/defineResolver.ts +185 -52
  29. package/src/devtools.ts +8 -1
  30. package/src/index.ts +1 -1
  31. package/src/mocks.ts +1 -2
  32. package/src/storages/fsStorage.ts +6 -31
  33. package/src/types.ts +38 -1292
  34. package/dist/PluginDriver-BXibeQk-.cjs.map +0 -1
  35. package/dist/PluginDriver-DV3p2Hky.js.map +0 -1
  36. package/src/Kubb.ts +0 -300
  37. package/src/renderNode.ts +0 -35
  38. package/src/utils/diagnostics.ts +0 -18
  39. package/src/utils/isInputPath.ts +0 -10
  40. package/src/utils/packageJSON.ts +0 -99
package/src/createKubb.ts CHANGED
@@ -1,20 +1,684 @@
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 { 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
+ 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
+ /**
574
+ * Read-only view of the files Kubb wrote during this build.
575
+ *
576
+ * Keys are scoped to this run; files from earlier builds are not included.
577
+ * Reads go directly to `config.storage`, so nothing is buffered in memory.
578
+ *
579
+ * @example Read a generated file
580
+ * ```ts
581
+ * const code = await storage.getItem('/src/gen/pet.ts')
582
+ * ```
583
+ *
584
+ * @example Walk every generated file
585
+ * ```ts
586
+ * for (const path of await storage.getKeys()) {
587
+ * const code = await storage.getItem(path)
588
+ * }
589
+ * ```
590
+ */
591
+ storage: Storage
592
+ }
593
+
594
+ export type KubbGenerationSummaryContext = {
595
+ config: Config
596
+ failedPlugins: Set<{ plugin: Plugin; error: Error }>
597
+ status: 'success' | 'failed'
598
+ hrStart: [number, number]
599
+ filesCreated: number
600
+ pluginTimings?: Map<Plugin['name'], number>
601
+ }
602
+
603
+ export type KubbVersionNewContext = {
604
+ currentVersion: string
605
+ latestVersion: string
606
+ }
607
+
608
+ export type KubbInfoContext = {
609
+ message: string
610
+ info?: string
611
+ }
612
+
613
+ export type KubbErrorContext = {
614
+ error: Error
615
+ meta?: Record<string, unknown>
616
+ }
617
+
618
+ export type KubbSuccessContext = {
619
+ message: string
620
+ info?: string
621
+ }
622
+
623
+ export type KubbWarnContext = {
624
+ message: string
625
+ info?: string
626
+ }
627
+
628
+ export type KubbDebugContext = {
629
+ date: Date
630
+ logs: Array<string>
631
+ fileName?: string
632
+ }
633
+
634
+ export type KubbFilesProcessingStartContext = {
635
+ files: Array<FileNode>
636
+ }
637
+
638
+ export type KubbFileProcessingUpdateContext = {
639
+ processed: number
640
+ total: number
641
+ percentage: number
642
+ source?: string
643
+ file: FileNode
644
+ config: Config
645
+ }
646
+
647
+ export type KubbFilesProcessingEndContext = {
648
+ files: Array<FileNode>
649
+ }
650
+
651
+ export type KubbHookStartContext = {
652
+ id?: string
653
+ command: string
654
+ args?: readonly string[]
655
+ }
656
+
657
+ export type KubbHookEndContext = {
658
+ id?: string
659
+ command: string
660
+ args?: readonly string[]
661
+ success: boolean
662
+ error: Error | null
663
+ }
664
+
665
+ /**
666
+ * CLI options derived from command-line flags.
667
+ */
668
+ export type CLIOptions = {
669
+ config?: string
670
+ watch?: boolean
671
+ /** @default 'silent' */
672
+ logLevel?: 'silent' | 'info' | 'debug'
673
+ }
674
+
675
+ /**
676
+ * All accepted forms of a Kubb configuration.
677
+ * Accepts `Config`/`Config[]`/promise or a factory (optionally receiving `TCliOptions`).
678
+ */
679
+ export type PossibleConfig<TCliOptions = undefined> =
680
+ | PossiblePromise<Config | Config[]>
681
+ | ((...args: [TCliOptions] extends [undefined] ? [] : [TCliOptions]) => PossiblePromise<Config | Config[]>)
18
682
 
19
683
  type SetupOptions = {
20
684
  hooks?: AsyncEventEmitter<KubbHooks>
@@ -36,28 +700,154 @@ export type BuildOutput = {
36
700
  pluginTimings: Map<string, number>
37
701
  error?: Error
38
702
  /**
39
- * Raw generated source, keyed by absolute file path.
703
+ * Read-only view of every file written during this build.
704
+ *
705
+ * Keys are limited to this run. Reads go straight to `config.storage`,
706
+ * so nothing extra is held in memory.
707
+ *
708
+ * @example Read a generated file
709
+ * ```ts
710
+ * const code = await buildOutput.storage.getItem('/src/gen/pet.ts')
711
+ * ```
712
+ *
713
+ * @example List all generated file paths
714
+ * ```ts
715
+ * const paths = await buildOutput.storage.getKeys()
716
+ * ```
40
717
  */
41
- sources: Map<string, string>
718
+ storage: Storage
719
+ }
720
+
721
+ /**
722
+ * Kubb code generation instance returned by {@link createKubb}.
723
+ *
724
+ * Use this when orchestrating multiple builds, inspecting plugin timings, or integrating Kubb into a larger toolchain.
725
+ * For a single one-off build, chain directly: `await createKubb(config).build()`.
726
+ */
727
+ export type Kubb = {
728
+ /**
729
+ * Shared event emitter for lifecycle and status events. Attach listeners before calling `setup()` or `build()`.
730
+ */
731
+ readonly hooks: AsyncEventEmitter<KubbHooks>
732
+ /**
733
+ * Read-only view of the files from the most recent `build()` or `safeBuild()` call.
734
+ * Only populated after the build completes.
735
+ *
736
+ * Keys are scoped to the current run. Reads go straight to `config.storage`,
737
+ * so nothing extra is held in memory.
738
+ *
739
+ * @example Read a generated file
740
+ * ```ts
741
+ * const { storage } = await kubb.safeBuild()
742
+ * const code = await storage.getItem('/src/gen/pet.ts')
743
+ * ```
744
+ *
745
+ * @example Walk every generated file
746
+ * ```ts
747
+ * for (const path of await kubb.storage.getKeys()) {
748
+ * const code = await kubb.storage.getItem(path)
749
+ * }
750
+ * ```
751
+ */
752
+ readonly storage: Storage
753
+ /**
754
+ * Plugin driver managing all plugins. Available after `setup()` completes.
755
+ */
756
+ readonly driver: PluginDriver
757
+ /**
758
+ * Resolved configuration with defaults applied. Available after `setup()` completes.
759
+ */
760
+ readonly config: Config
761
+ /**
762
+ * Resolves config and initializes the driver. `build()` calls this automatically.
763
+ */
764
+ setup(): Promise<void>
765
+ /**
766
+ * Runs the full pipeline and throws on any plugin error. Automatically calls `setup()` if needed.
767
+ */
768
+ build(): Promise<BuildOutput>
769
+ /**
770
+ * Runs the full pipeline and captures errors in `BuildOutput` instead of throwing. Automatically calls `setup()` if needed.
771
+ */
772
+ safeBuild(): Promise<BuildOutput>
42
773
  }
43
774
 
44
775
  type SetupResult = {
45
776
  hooks: AsyncEventEmitter<KubbHooks>
46
777
  driver: PluginDriver
47
- sources: Map<string, string>
778
+ storage: Storage
48
779
  config: Config
49
- storage: Storage | null
780
+ }
781
+
782
+ /**
783
+ * Builds a `Storage` view scoped to the file paths produced by the current build.
784
+ *
785
+ * Reads delegate to the underlying `storage` (typically `fsStorage()`) so source bytes
786
+ * stay where they were written instead of being held in an extra in-memory map.
787
+ * Writing via `setItem` stores the content in the underlying storage and registers the
788
+ * key so subsequent reads and `getKeys` are scoped to this build's output.
789
+ */
790
+ function createSourcesView(storage: Storage): Storage {
791
+ const paths = new Set<string>()
792
+ return createStorage(() => ({
793
+ name: `${storage.name}:sources`,
794
+ async hasItem(key: string) {
795
+ return paths.has(key) && (await storage.hasItem(key))
796
+ },
797
+ async getItem(key: string) {
798
+ return paths.has(key) ? storage.getItem(key) : null
799
+ },
800
+ async setItem(key: string, value: string) {
801
+ paths.add(key)
802
+ await storage.setItem(key, value)
803
+ },
804
+ async removeItem(key: string) {
805
+ paths.delete(key)
806
+ await storage.removeItem(key)
807
+ },
808
+ async getKeys(base?: string) {
809
+ if (!base) return [...paths]
810
+ const result: Array<string> = []
811
+ for (const key of paths) {
812
+ if (key.startsWith(base)) result.push(key)
813
+ }
814
+ return result
815
+ },
816
+ async clear() {
817
+ paths.clear()
818
+ await storage.clear()
819
+ },
820
+ }))()
50
821
  }
51
822
 
52
823
  async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promise<SetupResult> {
53
824
  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' })
825
+ const config: Config = {
826
+ ...userConfig,
827
+ root: userConfig.root || process.cwd(),
828
+ parsers: userConfig.parsers ?? [],
829
+ adapter: userConfig.adapter,
830
+ output: {
831
+ format: false,
832
+ lint: false,
833
+ extension: DEFAULT_EXTENSION,
834
+ defaultBanner: DEFAULT_BANNER,
835
+ ...userConfig.output,
836
+ },
837
+ storage: userConfig.storage ?? fsStorage(),
838
+ devtools: userConfig.devtools
839
+ ? {
840
+ studioUrl: DEFAULT_STUDIO_URL,
841
+ ...(typeof userConfig.devtools === 'boolean' ? {} : userConfig.devtools),
842
+ }
843
+ : undefined,
844
+ plugins: (userConfig.plugins ?? []) as unknown as Config['plugins'],
60
845
  }
846
+ const driver = new PluginDriver(config, {
847
+ hooks,
848
+ })
849
+ const storage: Storage = createSourcesView(config.storage)
850
+ const diagnosticInfo = getDiagnosticInfo()
61
851
 
62
852
  await hooks.emit('kubb:debug', {
63
853
  date: new Date(),
@@ -68,7 +858,7 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
68
858
  ` • Output: ${userConfig.output?.path || 'not specified'}`,
69
859
  ` • Plugins: ${userConfig.plugins?.length || 0}`,
70
860
  'Output Settings:',
71
- ` • Storage: ${userConfig.storage ? `custom(${userConfig.storage.name})` : userConfig.output?.write === false ? 'disabled' : 'filesystem (default)'}`,
861
+ ` • Storage: ${config.storage.name}`,
72
862
  ` • Formatter: ${userConfig.output?.format || 'none'}`,
73
863
  ` • Linter: ${userConfig.output?.lint || 'none'}`,
74
864
  'Environment:',
@@ -100,46 +890,14 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
100
890
  }
101
891
  }
102
892
 
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
893
  if (config.output.clean) {
132
894
  await hooks.emit('kubb:debug', {
133
895
  date: new Date(),
134
896
  logs: ['Cleaning output directories', ` • Output: ${config.output.path}`],
135
897
  })
136
- await storage?.clear(resolve(config.root, config.output.path))
898
+ await config.storage.clear(resolve(config.root, config.output.path))
137
899
  }
138
900
 
139
- const driver = new PluginDriver(config, {
140
- hooks,
141
- })
142
-
143
901
  // Register middleware hooks after all plugin hooks are registered.
144
902
  // Because AsyncEventEmitter calls listeners in registration order,
145
903
  // middleware hooks for any event fire after all plugin hooks for that event.
@@ -156,34 +914,31 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
156
914
  }
157
915
  }
158
916
 
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)
917
+ if (config.adapter) {
918
+ const source = inputToAdapterSource(config)
164
919
 
165
- await hooks.emit('kubb:debug', {
166
- date: new Date(),
167
- logs: [`Running adapter: ${adapter.name}`],
168
- })
920
+ await hooks.emit('kubb:debug', {
921
+ date: new Date(),
922
+ logs: [`Running adapter: ${config.adapter.name}`],
923
+ })
169
924
 
170
- driver.adapter = adapter
171
- driver.inputNode = await adapter.parse(source)
925
+ driver.adapter = config.adapter
926
+ driver.inputNode = await config.adapter.parse(source)
172
927
 
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
- })
928
+ await hooks.emit('kubb:debug', {
929
+ date: new Date(),
930
+ logs: [
931
+ `✓ Adapter '${config.adapter.name}' resolved InputNode`,
932
+ ` • Schemas: ${driver.inputNode.schemas.length}`,
933
+ ` • Operations: ${driver.inputNode.operations.length}`,
934
+ ],
935
+ })
936
+ }
181
937
 
182
938
  return {
183
939
  config,
184
940
  hooks,
185
941
  driver,
186
- sources,
187
942
  storage,
188
943
  }
189
944
  }
@@ -191,13 +946,19 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
191
946
  /**
192
947
  * Walks the AST and dispatches nodes to a plugin's direct AST hooks
193
948
  * (`schema`, `operation`, `operations`).
949
+ *
950
+ * When `include` contains only operation-scoped filters (`tag`, `operationId`, `path`,
951
+ * `method`, `contentType`) and no `schemaName` filter, the function pre-computes the set
952
+ * of top-level schema names transitively reachable from the included operations and skips
953
+ * schemas that fall outside that set. This ensures that component schemas referenced
954
+ * exclusively by excluded operations are not generated.
194
955
  */
195
956
  async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorContext): Promise<void> {
196
957
  const { adapter, inputNode, resolver, driver } = context
197
958
  const { exclude, include, override } = plugin.options
198
959
 
199
960
  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.`)
961
+ throw new Error(`[${plugin.name}] No adapter found. Add an OAS adapter (e.g. adapterOas()) before this plugin in your Kubb config.`)
201
962
  }
202
963
 
203
964
  function resolveRenderer(gen: Generator): RendererFactory | undefined {
@@ -212,10 +973,30 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
212
973
  resolver: driver.getResolver(plugin.name),
213
974
  }
214
975
 
976
+ // When `include` has operation-based filters (tag, operationId, path, method, contentType)
977
+ // but no schema-level filters (schemaName), pre-compute the set of top-level schema names
978
+ // that are transitively referenced by the included operations. Schemas outside that set are
979
+ // skipped so that types belonging exclusively to excluded operations are not generated.
980
+ const operationFilterTypes = new Set(['tag', 'operationId', 'path', 'method', 'contentType'])
981
+ const hasOperationBasedIncludes = include?.some(({ type }) => operationFilterTypes.has(type)) ?? false
982
+ const hasSchemaNameIncludes = include?.some(({ type }) => type === 'schemaName') ?? false
983
+
984
+ let allowedSchemaNames: Set<string> | undefined
985
+ if (hasOperationBasedIncludes && !hasSchemaNameIncludes) {
986
+ const includedOps = inputNode.operations.filter((op) => resolver.resolveOptions(op, { options: plugin.options, exclude, include, override }) !== null)
987
+ allowedSchemaNames = collectUsedSchemaNames(includedOps, inputNode.schemas)
988
+ }
989
+
215
990
  await walk(inputNode, {
216
991
  depth: 'shallow',
217
992
  async schema(node) {
218
993
  const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
994
+
995
+ // Skip named top-level schemas that are not reachable from any included operation.
996
+ if (allowedSchemaNames !== undefined && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) {
997
+ return
998
+ }
999
+
219
1000
  const options = resolver.resolveOptions(transformedNode, {
220
1001
  options: plugin.options,
221
1002
  exclude,
@@ -272,11 +1053,69 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
272
1053
  }
273
1054
 
274
1055
  async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
275
- const { driver, hooks, sources, storage } = setupResult
1056
+ const { driver, hooks, storage } = setupResult
276
1057
 
277
1058
  const failedPlugins = new Set<{ plugin: Plugin; error: Error }>()
278
1059
  const pluginTimings = new Map<string, number>()
279
1060
  const config = driver.config
1061
+ const writtenPaths = new Set<string>()
1062
+ const parsersMap = new Map<FileNode['extname'], Parser>()
1063
+ for (const parser of config.parsers) {
1064
+ if (parser.extNames) {
1065
+ for (const extname of parser.extNames) {
1066
+ parsersMap.set(extname, parser)
1067
+ }
1068
+ }
1069
+ }
1070
+ const fileProcessor = new FileProcessor()
1071
+
1072
+ fileProcessor.events.on('start', async (processingFiles) => {
1073
+ await hooks.emit('kubb:files:processing:start', { files: processingFiles })
1074
+ })
1075
+
1076
+ fileProcessor.events.on('update', async ({ file, source, processed, total, percentage }) => {
1077
+ await hooks.emit('kubb:file:processing:update', {
1078
+ file,
1079
+ source,
1080
+ processed,
1081
+ total,
1082
+ percentage,
1083
+ config,
1084
+ })
1085
+ if (source) {
1086
+ await storage.setItem(file.path, source)
1087
+ }
1088
+ })
1089
+
1090
+ fileProcessor.events.on('end', async (processed) => {
1091
+ await hooks.emit('kubb:files:processing:end', { files: processed })
1092
+ await hooks.emit('kubb:debug', {
1093
+ date: new Date(),
1094
+ logs: [`✓ File write process completed for ${processed.length} files`],
1095
+ })
1096
+ })
1097
+
1098
+ async function flushPendingFiles(): Promise<void> {
1099
+ const files = driver.fileManager.files.filter((f) => !writtenPaths.has(f.path))
1100
+ if (files.length === 0) {
1101
+ return
1102
+ }
1103
+
1104
+ await hooks.emit('kubb:debug', {
1105
+ date: new Date(),
1106
+ logs: [`Writing ${files.length} files...`],
1107
+ })
1108
+
1109
+ await fileProcessor.run(files, {
1110
+ parsers: parsersMap,
1111
+ mode: 'parallel',
1112
+ extension: config.output.extension,
1113
+ })
1114
+
1115
+ for (const file of files) {
1116
+ writtenPaths.add(file.path)
1117
+ }
1118
+ }
280
1119
 
281
1120
  try {
282
1121
  await driver.emitSetupHooks()
@@ -302,7 +1141,6 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
302
1141
  const timestamp = new Date()
303
1142
 
304
1143
  await hooks.emit('kubb:plugin:start', { plugin })
305
-
306
1144
  await hooks.emit('kubb:debug', {
307
1145
  date: timestamp,
308
1146
  logs: ['Starting plugin...', ` • Plugin Name: ${plugin.name}`],
@@ -326,6 +1164,8 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
326
1164
  upsertFile: (...files) => driver.fileManager.upsert(...files),
327
1165
  })
328
1166
 
1167
+ await flushPendingFiles()
1168
+
329
1169
  await hooks.emit('kubb:debug', {
330
1170
  date: new Date(),
331
1171
  logs: [`✓ Plugin started successfully (${formatMs(duration)})`],
@@ -347,6 +1187,8 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
347
1187
  upsertFile: (...files) => driver.fileManager.upsert(...files),
348
1188
  })
349
1189
 
1190
+ await flushPendingFiles()
1191
+
350
1192
  await hooks.emit('kubb:debug', {
351
1193
  date: errorTimestamp,
352
1194
  logs: [
@@ -370,52 +1212,9 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
370
1212
  upsertFile: (...files) => driver.fileManager.upsert(...files),
371
1213
  })
372
1214
 
373
- const files = driver.fileManager.files
374
-
375
- const parsersMap = new Map<FileNode['extname'], Parser>()
376
- for (const parser of config.parsers) {
377
- if (parser.extNames) {
378
- for (const extname of parser.extNames) {
379
- parsersMap.set(extname, parser)
380
- }
381
- }
382
- }
1215
+ await flushPendingFiles()
383
1216
 
384
- const fileProcessor = new FileProcessor()
385
-
386
- await hooks.emit('kubb:debug', {
387
- date: new Date(),
388
- logs: [`Writing ${files.length} files...`],
389
- })
390
-
391
- await fileProcessor.run(files, {
392
- parsers: parsersMap,
393
- extension: config.output.extension,
394
- onStart: async (processingFiles) => {
395
- await hooks.emit('kubb:files:processing:start', { files: processingFiles })
396
- },
397
- onUpdate: async ({ file, source, processed, total, percentage }) => {
398
- await hooks.emit('kubb:file:processing:update', {
399
- file,
400
- source,
401
- processed,
402
- total,
403
- percentage,
404
- config,
405
- })
406
- if (source) {
407
- await storage?.setItem(file.path, source)
408
- sources.set(file.path, source)
409
- }
410
- },
411
- onEnd: async (processedFiles) => {
412
- await hooks.emit('kubb:files:processing:end', { files: processedFiles })
413
- await hooks.emit('kubb:debug', {
414
- date: new Date(),
415
- logs: [`✓ File write process completed for ${processedFiles.length} files`],
416
- })
417
- },
418
- })
1217
+ const files = driver.fileManager.files
419
1218
 
420
1219
  await hooks.emit('kubb:build:end', {
421
1220
  files,
@@ -428,7 +1227,7 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
428
1227
  files,
429
1228
  driver,
430
1229
  pluginTimings,
431
- sources,
1230
+ storage,
432
1231
  }
433
1232
  } catch (error) {
434
1233
  return {
@@ -437,7 +1236,7 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
437
1236
  driver,
438
1237
  pluginTimings,
439
1238
  error: error as Error,
440
- sources,
1239
+ storage,
441
1240
  }
442
1241
  } finally {
443
1242
  driver.dispose()
@@ -445,7 +1244,7 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
445
1244
  }
446
1245
 
447
1246
  async function build(setupResult: SetupResult): Promise<BuildOutput> {
448
- const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(setupResult)
1247
+ const { files, driver, failedPlugins, pluginTimings, error, storage } = await safeBuild(setupResult)
449
1248
 
450
1249
  if (error) {
451
1250
  throw error
@@ -463,27 +1262,51 @@ async function build(setupResult: SetupResult): Promise<BuildOutput> {
463
1262
  driver,
464
1263
  pluginTimings,
465
1264
  error: undefined,
466
- sources,
1265
+ storage,
467
1266
  }
468
1267
  }
469
1268
 
1269
+ /**
1270
+ * Returns a snapshot of the current runtime environment.
1271
+ *
1272
+ * Useful for attaching context to debug logs and error reports so that
1273
+ * issues can be reproduced without manual information gathering.
1274
+ */
1275
+ export function getDiagnosticInfo() {
1276
+ return {
1277
+ nodeVersion,
1278
+ KubbVersion,
1279
+ platform: process.platform,
1280
+ arch: process.arch,
1281
+ cwd: process.cwd(),
1282
+ } as const
1283
+ }
1284
+
1285
+ /**
1286
+ * Type guard to check if a given config has an `input.path`.
1287
+ */
1288
+ export function isInputPath(config: UserConfig | undefined): config is UserConfig<InputPath> & { input: InputPath }
1289
+ export function isInputPath(config: Config | undefined): config is Config<InputPath> & { input: InputPath }
1290
+ export function isInputPath(config: Config | UserConfig | undefined): config is (Config<InputPath> | UserConfig<InputPath>) & { input: InputPath } {
1291
+ return typeof config?.input === 'object' && config.input !== null && 'path' in config.input
1292
+ }
1293
+
470
1294
  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
- }
1295
+ const input = config.input
1296
+ if (!input) {
1297
+ throw new Error('[kubb] input is required when using an adapter. Provide input.path or input.data in your config.')
476
1298
  }
477
1299
 
478
- if ('data' in config.input) {
479
- return { type: 'data', data: config.input.data }
1300
+ if ('data' in input) {
1301
+ return { type: 'data', data: input.data }
480
1302
  }
481
1303
 
482
- if (new URLPath(config.input.path).isURL) {
483
- return { type: 'path', path: config.input.path }
1304
+ if (new URLPath(input.path).isURL) {
1305
+ return { type: 'path', path: input.path }
484
1306
  }
485
1307
 
486
- const resolved = resolve(config.root, config.input.path)
1308
+ const resolved = resolve(config.root, input.path)
1309
+
487
1310
  return { type: 'path', path: resolved }
488
1311
  }
489
1312
 
@@ -495,7 +1318,7 @@ type CreateKubbOptions = {
495
1318
  * Creates a Kubb instance bound to a single config entry.
496
1319
  *
497
1320
  * Accepts a user-facing config shape and resolves it to a full {@link Config} during
498
- * `setup()`. The instance then holds shared state (`hooks`, `sources`, `driver`, `config`)
1321
+ * `setup()`. The instance then holds shared state (`hooks`, `storage`, `driver`, `config`)
499
1322
  * across the `setup → build` lifecycle. Attach event listeners to `kubb.hooks` before
500
1323
  * calling `setup()` or `build()`.
501
1324
  *
@@ -518,14 +1341,23 @@ export function createKubb(userConfig: UserConfig, options: CreateKubbOptions =
518
1341
  get hooks() {
519
1342
  return hooks
520
1343
  },
521
- get sources() {
522
- return setupResult?.sources ?? new Map()
1344
+ get storage() {
1345
+ if (!setupResult) {
1346
+ throw new Error('[kubb] setup() must be called before accessing storage')
1347
+ }
1348
+ return setupResult.storage
523
1349
  },
524
1350
  get driver() {
525
- return setupResult?.driver
1351
+ if (!setupResult) {
1352
+ throw new Error('[kubb] setup() must be called before accessing driver')
1353
+ }
1354
+ return setupResult.driver
526
1355
  },
527
1356
  get config() {
528
- return setupResult?.config
1357
+ if (!setupResult) {
1358
+ throw new Error('[kubb] setup() must be called before accessing config')
1359
+ }
1360
+ return setupResult.config
529
1361
  },
530
1362
  async setup() {
531
1363
  setupResult = await setup(userConfig, { hooks })