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

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 +15 -148
  2. package/dist/diagnostics-DhfW8YpT.d.ts +2963 -0
  3. package/dist/index.cjs +775 -1193
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +165 -171
  6. package/dist/index.js +760 -1188
  7. package/dist/index.js.map +1 -1
  8. package/dist/memoryStorage-DTv1Kub1.js +2852 -0
  9. package/dist/memoryStorage-DTv1Kub1.js.map +1 -0
  10. package/dist/memoryStorage-Dkxnid2K.cjs +2985 -0
  11. package/dist/memoryStorage-Dkxnid2K.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 +897 -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 +921 -493
  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 +78 -7
  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 +59 -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,1001 @@
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> = {
71
+ /**
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
+ * ```
79
+ */
80
+ name?: string
33
81
  /**
34
- * Elapsed time in milliseconds for each plugin, keyed by plugin name.
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()`).
35
85
  */
36
- pluginTimings: Map<string, number>
37
- error?: Error
86
+ root: string
38
87
  /**
39
- * Raw generated source, keyed by absolute file path.
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
+ * ```
40
103
  */
41
- sources: Map<string, string>
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
- }
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 {}
101
435
  }
436
+ }
102
437
 
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
- }
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:end': [ctx: KubbHookEndContext]
468
+ 'kubb:info': [ctx: KubbInfoContext]
469
+ 'kubb:error': [ctx: KubbErrorContext]
470
+ 'kubb:success': [ctx: KubbSuccessContext]
471
+ 'kubb:warn': [ctx: KubbWarnContext]
472
+ 'kubb:diagnostic': [ctx: KubbDiagnosticContext]
473
+ 'kubb:files:processing:start': [ctx: KubbFilesProcessingStartContext]
474
+ 'kubb:files:processing:update': [ctx: KubbFilesProcessingUpdateContext]
475
+ 'kubb:files:processing:end': [ctx: KubbFilesProcessingEndContext]
476
+ 'kubb:plugin:start': [ctx: KubbPluginStartContext]
477
+ 'kubb:plugin:end': [ctx: KubbPluginEndContext]
478
+ 'kubb:plugin:setup': [ctx: KubbPluginSetupContext]
479
+ 'kubb:build:start': [ctx: KubbBuildStartContext]
480
+ 'kubb:plugins:end': [ctx: KubbPluginsEndContext]
481
+ 'kubb:build:end': [ctx: KubbBuildEndContext]
482
+ 'kubb:generate:schema': [node: SchemaNode, ctx: GeneratorContext]
483
+ 'kubb:generate:operation': [node: OperationNode, ctx: GeneratorContext]
484
+ 'kubb:generate:operations': [nodes: Array<OperationNode>, ctx: GeneratorContext]
485
+ }
124
486
 
125
- const storage: Storage | null = config.output.write === false ? null : (config.storage ?? fsStorage())
487
+ export type KubbBuildStartContext = {
488
+ /**
489
+ * Resolved configuration for this build.
490
+ */
491
+ config: Config
492
+ /**
493
+ * Adapter that parsed the input into the universal AST.
494
+ */
495
+ adapter: Adapter
496
+ /**
497
+ * Metadata about the parsed document (title, version, base URL, circular schema names, enum names).
498
+ * To observe individual schemas and operations use the `kubb:generate:schema` / `kubb:generate:operation` hooks.
499
+ */
500
+ meta: InputMeta | undefined
501
+ /**
502
+ * Looks up a registered plugin by name, typed by the plugin registry.
503
+ */
504
+ getPlugin<TName extends keyof Kubb.PluginRegistry>(name: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined
505
+ getPlugin(name: string): Plugin | undefined
506
+ /**
507
+ * Snapshot of all files accumulated so far.
508
+ */
509
+ readonly files: ReadonlyArray<FileNode>
510
+ /**
511
+ * Adds or merges one or more files into the file manager.
512
+ */
513
+ upsertFile: (...files: Array<FileNode>) => void
514
+ }
126
515
 
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))
133
- }
516
+ export type KubbPluginsEndContext = {
517
+ /**
518
+ * Resolved configuration for this build.
519
+ */
520
+ config: Config
521
+ /**
522
+ * Snapshot of all files accumulated across all plugins.
523
+ */
524
+ readonly files: ReadonlyArray<FileNode>
525
+ /**
526
+ * Adds or merges one or more files into the file manager.
527
+ */
528
+ upsertFile: (...files: Array<FileNode>) => void
529
+ }
134
530
 
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
- }
531
+ export type KubbBuildEndContext = {
532
+ /**
533
+ * All files generated during this build.
534
+ */
535
+ files: Array<FileNode>
536
+ /**
537
+ * Resolved configuration for this build.
538
+ */
539
+ config: Config
540
+ /**
541
+ * Absolute path to the output directory.
542
+ */
543
+ outputDir: string
544
+ }
148
545
 
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
- }
546
+ export type KubbLifecycleStartContext = {
547
+ /**
548
+ * Current Kubb version string.
549
+ */
550
+ version: string
551
+ }
154
552
 
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
- }
553
+ export type KubbConfigEndContext = {
554
+ /**
555
+ * All resolved configs after defaults are applied.
556
+ */
557
+ configs: Array<Config>
558
+ }
175
559
 
176
- return {
177
- config,
178
- hooks,
179
- driver,
180
- sources,
181
- storage,
182
- }
560
+ export type KubbGenerationStartContext = {
561
+ /**
562
+ * Resolved configuration for this generation run.
563
+ */
564
+ config: Config
183
565
  }
184
566
 
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
567
+ export type KubbGenerationEndContext = {
568
+ /**
569
+ * Resolved configuration for this generation run.
570
+ */
571
+ config: Config
572
+ /**
573
+ * Read-only view of the files written during this build.
574
+ * Reads go directly to `config.storage`, nothing extra is held in memory.
575
+ *
576
+ * @example Read a generated file
577
+ * `const code = await storage.getItem('/src/gen/pet.ts')`
578
+ *
579
+ * @example Walk every generated file
580
+ * ```ts
581
+ * for (const path of await storage.getKeys()) {
582
+ * const code = await storage.getItem(path)
583
+ * }
584
+ * ```
585
+ */
586
+ storage: Storage
587
+ /**
588
+ * Diagnostics collected during the build: error/warning/info problems plus a
589
+ * `timing` diagnostic per plugin. The end-of-run summary derives its failure counts
590
+ * and per-plugin timings from these. Set by the CLI runner, omitted by other callers.
591
+ */
592
+ diagnostics?: Array<Diagnostic>
593
+ /**
594
+ * `'success'` when all plugins completed without errors, `'failed'` otherwise.
595
+ */
596
+ status?: 'success' | 'failed'
597
+ /**
598
+ * High-resolution start time from `process.hrtime()`, used to compute the elapsed time.
599
+ */
600
+ hrStart?: [number, number]
601
+ /**
602
+ * Total number of files created during this run.
603
+ */
604
+ filesCreated?: number
605
+ }
198
606
 
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
- }
607
+ export type KubbInfoContext = {
608
+ /**
609
+ * Human-readable info message.
610
+ */
611
+ message: string
612
+ /**
613
+ * Optional supplementary detail.
614
+ */
615
+ info?: string
616
+ }
202
617
 
203
- function resolveRenderer(gen: Generator): RendererFactory | undefined {
204
- return gen.renderer === null ? undefined : (gen.renderer ?? plugin.renderer ?? context.config.renderer)
205
- }
618
+ export type KubbErrorContext = {
619
+ /**
620
+ * The caught error.
621
+ */
622
+ error: Error
623
+ /**
624
+ * Optional structured metadata for additional context.
625
+ */
626
+ meta?: Record<string, unknown>
627
+ }
206
628
 
207
- const generators = plugin.generators ?? []
208
- const collectedOperations: Array<OperationNode> = []
629
+ export type KubbSuccessContext = {
630
+ /**
631
+ * Human-readable success message.
632
+ */
633
+ message: string
634
+ /**
635
+ * Optional supplementary detail.
636
+ */
637
+ info?: string
638
+ }
209
639
 
210
- const generatorContext = {
211
- ...context,
212
- resolver: driver.getResolver(plugin.name),
213
- }
640
+ export type KubbWarnContext = {
641
+ /**
642
+ * Human-readable warning message.
643
+ */
644
+ message: string
645
+ /**
646
+ * Optional supplementary detail.
647
+ */
648
+ info?: string
649
+ }
214
650
 
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
- }
651
+ export type KubbDiagnosticContext = {
652
+ /**
653
+ * The structured diagnostic to render: a build problem or a version-update notice.
654
+ */
655
+ diagnostic: ProblemDiagnostic | UpdateDiagnostic
656
+ }
228
657
 
229
- await walk(inputNode, {
230
- depth: 'shallow',
231
- async schema(node) {
232
- const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
658
+ export type KubbFilesProcessingStartContext = {
659
+ /**
660
+ * Files about to be serialised and written.
661
+ */
662
+ files: Array<FileNode>
663
+ }
233
664
 
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
- }
665
+ export type KubbFileProcessingUpdate = {
666
+ /**
667
+ * Number of files processed so far in this batch.
668
+ */
669
+ processed: number
670
+ /**
671
+ * Total number of files in this batch.
672
+ */
673
+ total: number
674
+ /**
675
+ * Completion percentage, `0` to `100`.
676
+ */
677
+ percentage: number
678
+ /**
679
+ * Serialised file content, or `undefined` when the file produced no output.
680
+ */
681
+ source?: string
682
+ /**
683
+ * The file that was just processed.
684
+ */
685
+ file: FileNode
686
+ /**
687
+ * Resolved configuration for this build.
688
+ */
689
+ config: Config
690
+ }
238
691
 
239
- const options = resolver.resolveOptions(transformedNode, {
240
- options: plugin.options,
241
- exclude,
242
- include,
243
- override,
244
- })
245
- if (options === null) return
692
+ export type KubbFilesProcessingUpdateContext = {
693
+ /**
694
+ * All files processed in this flush chunk.
695
+ */
696
+ files: Array<KubbFileProcessingUpdate>
697
+ }
246
698
 
247
- const ctx = { ...generatorContext, options }
699
+ export type KubbFilesProcessingEndContext = {
700
+ /**
701
+ * All files that were serialised in this batch.
702
+ */
703
+ files: Array<FileNode>
704
+ }
248
705
 
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
- }
706
+ export type KubbHookStartContext = {
707
+ /**
708
+ * Optional identifier for correlating start/end events.
709
+ */
710
+ id?: string
711
+ /**
712
+ * The shell command that is about to run.
713
+ */
714
+ command: string
715
+ /**
716
+ * Parsed argument list, when available.
717
+ */
718
+ args?: ReadonlyArray<string>
719
+ }
254
720
 
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
- })
721
+ export type KubbHookEndContext = {
722
+ /**
723
+ * Optional identifier matching the corresponding `kubb:hook:start` event.
724
+ */
725
+ id?: string
726
+ /**
727
+ * The shell command that ran.
728
+ */
729
+ command: string
730
+ /**
731
+ * Parsed argument list, when available.
732
+ */
733
+ args?: ReadonlyArray<string>
734
+ /**
735
+ * `true` when the command exited with code `0`.
736
+ */
737
+ success: boolean
738
+ /**
739
+ * Error thrown by the command, or `null` on success.
740
+ */
741
+ error: Error | null
742
+ }
280
743
 
281
- if (collectedOperations.length > 0) {
282
- const ctx = { ...generatorContext, options: plugin.options }
744
+ /**
745
+ * CLI options derived from command-line flags.
746
+ */
747
+ export type CLIOptions = {
748
+ /**
749
+ * Path to the Kubb config file.
750
+ */
751
+ config?: string
752
+ /**
753
+ * OpenAPI input path passed as the positional argument to `kubb generate`.
754
+ * Overrides `config.input.path` when set.
755
+ */
756
+ input?: string
757
+ /**
758
+ * Re-run generation whenever input files change.
759
+ */
760
+ watch?: boolean
761
+ /**
762
+ * Controls how much output the CLI prints.
763
+ *
764
+ * @default 'info'
765
+ */
766
+ logLevel?: 'silent' | 'info' | 'verbose'
767
+ /**
768
+ * Reporters selected on the CLI via `--reporter`, overriding `config.reporters`.
769
+ */
770
+ reporters?: Array<ReporterName>
771
+ }
283
772
 
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
- }
773
+ /**
774
+ * All accepted forms of a Kubb configuration.
775
+ * Accepts `Config`/`Config[]`/promise or a factory (optionally receiving `TCliOptions`.
776
+ */
777
+ export type PossibleConfig<TCliOptions = undefined> =
778
+ | PossiblePromise<Config | Array<Config>>
779
+ | ((...args: [TCliOptions] extends [undefined] ? [] : [TCliOptions]) => PossiblePromise<Config | Array<Config>>)
289
780
 
290
- await driver.hooks.emit('kubb:generate:operations', collectedOperations, ctx)
291
- }
781
+ /**
782
+ * Full output produced by a successful or failed build.
783
+ */
784
+ export type BuildOutput = {
785
+ /**
786
+ * Structured diagnostics collected during the build: error/warning/info problems
787
+ * (each with a code, severity, and where known a JSON-pointer location) plus a
788
+ * `timing` diagnostic per plugin. Includes a top-level diagnostic when the build
789
+ * threw before completing. Use {@link Diagnostics.hasError} to test for failure.
790
+ */
791
+ diagnostics: Array<Diagnostic>
792
+ /**
793
+ * All files generated during this build.
794
+ */
795
+ files: Array<FileNode>
796
+ /**
797
+ * The plugin driver that orchestrated this build.
798
+ */
799
+ driver: KubbDriver
800
+ /**
801
+ * Read-only view of every file written during this build.
802
+ * Reads go straight to `config.storage`, nothing extra is held in memory.
803
+ *
804
+ * @example Read a generated file
805
+ * `const code = await buildOutput.storage.getItem('/src/gen/pet.ts')`
806
+ *
807
+ * @example List all generated file paths
808
+ * `const paths = await buildOutput.storage.getKeys()`
809
+ */
810
+ storage: Storage
292
811
  }
293
812
 
294
- async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
295
- const { driver, hooks, sources, storage } = setupResult
296
-
297
- const failedPlugins = new Set<{ plugin: Plugin; error: Error }>()
298
- const pluginTimings = new Map<string, number>()
299
- const config = driver.config
300
-
301
- try {
302
- await driver.emitSetupHooks()
303
-
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
- }
316
-
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
- }
813
+ /**
814
+ * Builds a `Storage` view scoped to the file paths produced by the current build.
815
+ * Reads delegate to the underlying `storage` so source bytes stay where they were
816
+ * written. Writes register the key so subsequent reads and `getKeys` are scoped
817
+ * to this build's output.
818
+ */
819
+ function createSourcesView(storage: Storage): Storage {
820
+ const paths = new Set<string>()
384
821
 
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
- }
822
+ return createStorage(() => ({
823
+ name: `${storage.name}:sources`,
824
+ async hasItem(key: string) {
825
+ return paths.has(key) && (await storage.hasItem(key))
826
+ },
827
+ async getItem(key: string) {
828
+ return paths.has(key) ? storage.getItem(key) : null
829
+ },
830
+ async setItem(key: string, value: string) {
831
+ paths.add(key)
832
+ await storage.setItem(key, value)
833
+ },
834
+ async removeItem(key: string) {
835
+ paths.delete(key)
836
+ await storage.removeItem(key)
837
+ },
838
+ async getKeys(base?: string) {
839
+ if (!base) return [...paths]
840
+ const result: Array<string> = []
841
+ for (const key of paths) {
842
+ if (key.startsWith(base)) result.push(key)
401
843
  }
402
- }
844
+ return result
845
+ },
846
+ async clear() {
847
+ paths.clear()
848
+ await storage.clear()
849
+ },
850
+ }))()
851
+ }
403
852
 
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()
853
+ function resolveConfig(userConfig: UserConfig): Config {
854
+ return {
855
+ ...userConfig,
856
+ root: userConfig.root || process.cwd(),
857
+ parsers: userConfig.parsers ?? [],
858
+ output: {
859
+ format: false,
860
+ lint: false,
861
+ extension: { '.ts': '.ts' },
862
+ defaultBanner: 'simple',
863
+ ...userConfig.output,
864
+ },
865
+ storage: userConfig.storage ?? fsStorage(),
866
+ reporters: userConfig.reporters ?? [],
867
+ plugins: userConfig.plugins ?? [],
464
868
  }
465
869
  }
466
870
 
467
- async function build(setupResult: SetupResult): Promise<BuildOutput> {
468
- const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(setupResult)
871
+ type CreateKubbOptions = {
872
+ hooks?: AsyncEventEmitter<KubbHooks>
873
+ }
469
874
 
470
- if (error) {
471
- throw error
875
+ /**
876
+ * Kubb code-generation instance bound to a single config entry. Resolves the user
877
+ * config during `setup()` and shares `hooks`, `storage`, `driver`, and `config` across
878
+ * the `setup → build` lifecycle.
879
+ *
880
+ * Attach event listeners to `.hooks` before calling `setup()` or `build()`.
881
+ *
882
+ * @example
883
+ * ```ts
884
+ * const kubb = createKubb(userConfig)
885
+ * kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => console.log(plugin.name, duration))
886
+ * const { files, diagnostics } = await kubb.safeBuild()
887
+ * ```
888
+ */
889
+ export class Kubb {
890
+ readonly hooks: AsyncEventEmitter<KubbHooks>
891
+ readonly #userConfig: UserConfig
892
+ #config: Config | null = null
893
+ #driver: KubbDriver | null = null
894
+ #storage: Storage | null = null
895
+
896
+ constructor(userConfig: UserConfig, options: CreateKubbOptions = {}) {
897
+ this.#userConfig = userConfig
898
+ this.hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
472
899
  }
473
900
 
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 })
901
+ get storage(): Storage {
902
+ if (!this.#storage) throw new Error('[kubb] setup() must be called before accessing storage')
903
+ return this.#storage
478
904
  }
479
905
 
480
- return {
481
- failedPlugins,
482
- files,
483
- driver,
484
- pluginTimings,
485
- error: undefined,
486
- sources,
906
+ get driver(): KubbDriver {
907
+ if (!this.#driver) throw new Error('[kubb] setup() must be called before accessing driver')
908
+ return this.#driver
487
909
  }
488
- }
489
910
 
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.')
911
+ get config(): Config {
912
+ if (!this.#config) throw new Error('[kubb] setup() must be called before accessing config')
913
+ return this.#config
494
914
  }
495
915
 
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))),
916
+ /**
917
+ * Resolves config and initializes the driver. `build()` calls this automatically.
918
+ */
919
+ async setup(): Promise<void> {
920
+ const config = resolveConfig(this.#userConfig)
921
+ const driver = new KubbDriver(config, { hooks: this.hooks })
922
+ const storage = createSourcesView(config.storage)
923
+
924
+ // Each generator a plugin registers adds a listener to the shared hooks emitter, so size the
925
+ // ceiling to the plugin count. Without this, a multi-generator plugin set trips Node's
926
+ // EventEmitter leak warning at the default 10.
927
+ this.hooks.setMaxListeners(Math.max(10, config.plugins.length * HOOK_LISTENERS_PER_PLUGIN))
928
+
929
+ if (config.output.clean) {
930
+ await config.storage.clear(resolve(config.root, config.output.path))
500
931
  }
932
+
933
+ await driver.setup()
934
+
935
+ this.#config = config
936
+ this.#driver = driver
937
+ this.#storage = storage
501
938
  }
502
939
 
503
- if ('data' in input) {
504
- return { type: 'data', data: input.data }
940
+ /**
941
+ * Runs the full pipeline and throws on any plugin error.
942
+ * Automatically calls `setup()` if needed.
943
+ */
944
+ async build(): Promise<BuildOutput> {
945
+ const out = await this.safeBuild()
946
+ if (Diagnostics.hasError(out.diagnostics)) {
947
+ const errors = out.diagnostics
948
+ .filter(Diagnostics.isProblem)
949
+ .filter((diagnostic) => diagnostic.severity === 'error')
950
+ .map((diagnostic) => diagnostic.cause ?? new Diagnostics.Error(diagnostic))
951
+ throw new BuildError(`Build failed with ${errors.length} ${errors.length === 1 ? 'error' : 'errors'}`, { errors })
952
+ }
953
+ return out
505
954
  }
506
955
 
507
- if (new URLPath(input.path).isURL) {
508
- return { type: 'path', path: input.path }
956
+ /**
957
+ * Runs the full pipeline and captures errors in `BuildOutput` instead of throwing.
958
+ * Automatically calls `setup()` if needed.
959
+ */
960
+ async safeBuild(): Promise<BuildOutput> {
961
+ if (!this.#driver) await this.setup()
962
+ using cleanup = this
963
+ const driver = cleanup.driver
964
+ const storage = cleanup.storage
965
+ const { diagnostics } = await driver.run({ storage })
966
+
967
+ return { diagnostics, files: driver.fileManager.files, driver, storage }
509
968
  }
510
969
 
511
- const resolved = resolve(config.root, input.path)
512
- return { type: 'path', path: resolved }
513
- }
970
+ dispose(): void {
971
+ this.#driver?.dispose()
972
+ }
514
973
 
515
- type CreateKubbOptions = {
516
- hooks?: AsyncEventEmitter<KubbHooks>
974
+ [Symbol.dispose](): void {
975
+ this.dispose()
976
+ }
517
977
  }
518
978
 
519
979
  /**
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()`.
980
+ * Constructs a {@link Kubb} build orchestrator from a user config. Equivalent
981
+ * to `new Kubb(userConfig, options)` and the canonical public entry point.
526
982
  *
527
983
  * @example
528
984
  * ```ts
529
- * const kubb = createKubb(userConfig)
985
+ * import { createKubb } from '@kubb/core'
986
+ * import { adapterOas } from '@kubb/adapter-oas'
987
+ * import { pluginTs } from '@kubb/plugin-ts'
530
988
  *
531
- * kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => {
532
- * console.log(`${plugin.name} completed in ${duration}ms`)
989
+ * const kubb = createKubb({
990
+ * input: { path: './petStore.yaml' },
991
+ * output: { path: './src/gen' },
992
+ * adapter: adapterOas(),
993
+ * plugins: [pluginTs()],
533
994
  * })
534
995
  *
535
- * const { files, failedPlugins } = await kubb.safeBuild()
996
+ * await kubb.build()
536
997
  * ```
537
998
  */
538
999
  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
1000
+ return new Kubb(userConfig, options)
573
1001
  }