@kubb/core 5.0.0-beta.4 → 5.0.0-beta.41

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