@kubb/core 5.0.0-alpha.9 → 5.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -40
- package/dist/PluginDriver-Cu1Kj9S-.cjs +1075 -0
- package/dist/PluginDriver-Cu1Kj9S-.cjs.map +1 -0
- package/dist/PluginDriver-D8Z0Htid.js +978 -0
- package/dist/PluginDriver-D8Z0Htid.js.map +1 -0
- package/dist/createKubb-ALdb8lmq.d.ts +2082 -0
- package/dist/index.cjs +747 -1667
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +175 -269
- package/dist/index.js +734 -1638
- package/dist/index.js.map +1 -1
- package/dist/mocks.cjs +145 -0
- package/dist/mocks.cjs.map +1 -0
- package/dist/mocks.d.ts +80 -0
- package/dist/mocks.js +140 -0
- package/dist/mocks.js.map +1 -0
- package/package.json +47 -60
- package/src/FileManager.ts +115 -0
- package/src/FileProcessor.ts +86 -0
- package/src/PluginDriver.ts +355 -561
- package/src/constants.ts +21 -48
- package/src/createAdapter.ts +88 -5
- package/src/createKubb.ts +1266 -0
- package/src/createRenderer.ts +57 -0
- package/src/createStorage.ts +13 -1
- package/src/defineGenerator.ts +160 -119
- package/src/defineLogger.ts +46 -5
- package/src/defineMiddleware.ts +62 -0
- package/src/defineParser.ts +44 -0
- package/src/definePlugin.ts +379 -0
- package/src/defineResolver.ts +548 -25
- package/src/devtools.ts +22 -15
- package/src/index.ts +13 -15
- package/src/mocks.ts +177 -0
- package/src/storages/fsStorage.ts +13 -8
- package/src/storages/memoryStorage.ts +4 -2
- package/src/types.ts +40 -547
- package/dist/PluginDriver-BkFepPdm.d.ts +0 -1054
- package/dist/chunk-ByKO4r7w.cjs +0 -38
- package/dist/hooks.cjs +0 -103
- package/dist/hooks.cjs.map +0 -1
- package/dist/hooks.d.ts +0 -77
- package/dist/hooks.js +0 -98
- package/dist/hooks.js.map +0 -1
- package/src/Kubb.ts +0 -224
- package/src/build.ts +0 -418
- package/src/config.ts +0 -56
- package/src/createPlugin.ts +0 -28
- package/src/hooks/index.ts +0 -4
- package/src/hooks/useKubb.ts +0 -143
- package/src/hooks/useMode.ts +0 -11
- package/src/hooks/usePlugin.ts +0 -11
- package/src/hooks/usePluginDriver.ts +0 -11
- package/src/utils/FunctionParams.ts +0 -155
- package/src/utils/TreeNode.ts +0 -215
- package/src/utils/diagnostics.ts +0 -15
- package/src/utils/executeStrategies.ts +0 -81
- package/src/utils/formatters.ts +0 -56
- package/src/utils/getBarrelFiles.ts +0 -141
- package/src/utils/getConfigs.ts +0 -12
- package/src/utils/linters.ts +0 -25
- package/src/utils/packageJSON.ts +0 -61
|
@@ -0,0 +1,1266 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { version as nodeVersion } from 'node:process'
|
|
3
|
+
import type { PossiblePromise } from '@internals/utils'
|
|
4
|
+
import { AsyncEventEmitter, BuildError, exists, formatMs, getElapsedMs, URLPath } from '@internals/utils'
|
|
5
|
+
import type { FileNode, InputNode, OperationNode, SchemaNode } from '@kubb/ast'
|
|
6
|
+
import { collectUsedSchemaNames, transform, walk } from '@kubb/ast'
|
|
7
|
+
import { version as KubbVersion } from '../package.json'
|
|
8
|
+
import { DEFAULT_BANNER, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL } from './constants.ts'
|
|
9
|
+
import type { Adapter, AdapterSource } from './createAdapter.ts'
|
|
10
|
+
import type { RendererFactory } from './createRenderer.ts'
|
|
11
|
+
import type { Storage } from './createStorage.ts'
|
|
12
|
+
import type { GeneratorContext, Generator } from './defineGenerator.ts'
|
|
13
|
+
import type { Middleware } from './defineMiddleware.ts'
|
|
14
|
+
import type { Parser } from './defineParser.ts'
|
|
15
|
+
import type { KubbPluginEndContext, KubbPluginSetupContext, KubbPluginStartContext, NormalizedPlugin, Plugin } from './definePlugin.ts'
|
|
16
|
+
import { FileProcessor } from './FileProcessor.ts'
|
|
17
|
+
import { applyHookResult, PluginDriver } from './PluginDriver.ts'
|
|
18
|
+
import { fsStorage } from './storages/fsStorage.ts'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Safely extracts a type from a registry, returning `{}` if the key doesn't exist.
|
|
22
|
+
* Enables optional interface augmentation for `Kubb.ConfigOptionsRegistry` and `Kubb.PluginOptionsRegistry`
|
|
23
|
+
* without requiring changes to core.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
type ExtractRegistryKey<T, K extends PropertyKey> = K extends keyof T ? T[K] : {}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reference to an input file to generate code from.
|
|
31
|
+
*
|
|
32
|
+
* Specify an absolute path or a path relative to the config file location.
|
|
33
|
+
* The adapter will parse this file (e.g., OpenAPI YAML or JSON) into the universal AST.
|
|
34
|
+
*/
|
|
35
|
+
export type InputPath = {
|
|
36
|
+
/**
|
|
37
|
+
* Path to your Swagger/OpenAPI file, absolute or relative to the config file location.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* { path: './petstore.yaml' }
|
|
42
|
+
* { path: '/absolute/path/to/openapi.json' }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
path: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Inline input data to generate code from.
|
|
50
|
+
*
|
|
51
|
+
* Useful when you want to pass the specification directly instead of from a file.
|
|
52
|
+
* Can be a string (YAML/JSON) or a parsed object.
|
|
53
|
+
*/
|
|
54
|
+
export type InputData = {
|
|
55
|
+
/**
|
|
56
|
+
* Swagger/OpenAPI data as a string (YAML/JSON) or a parsed object.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* { data: fs.readFileSync('./openapi.yaml', 'utf8') }
|
|
61
|
+
* { data: { openapi: '3.1.0', info: { ... } } }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
data: string | unknown
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type Input = InputPath | InputData
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build configuration for Kubb code generation.
|
|
71
|
+
*
|
|
72
|
+
* The Config is the main entry point for customizing how Kubb generates code. It specifies:
|
|
73
|
+
* - What to generate from (adapter + input)
|
|
74
|
+
* - Where to output generated code (output)
|
|
75
|
+
* - How to generate (plugins + middleware)
|
|
76
|
+
* - Runtime details (parsers, storage, renderer)
|
|
77
|
+
*
|
|
78
|
+
* See `UserConfig` for a relaxed version with sensible defaults.
|
|
79
|
+
*
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
export type Config<TInput = Input> = {
|
|
83
|
+
/**
|
|
84
|
+
* Display name for this configuration in CLI output and logs.
|
|
85
|
+
* Useful when running multiple builds with `defineConfig` arrays.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* name: 'api-client'
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
name?: string
|
|
93
|
+
/**
|
|
94
|
+
* Project root directory, absolute or relative to the config file.
|
|
95
|
+
* @default process.cwd()
|
|
96
|
+
*/
|
|
97
|
+
root: string
|
|
98
|
+
/**
|
|
99
|
+
* Parsers that convert generated files to strings.
|
|
100
|
+
* Each parser handles specific extensions (e.g. `.ts`, `.tsx`).
|
|
101
|
+
* A fallback parser is appended for unhandled extensions.
|
|
102
|
+
* When omitted, defaults to `parserTs` from `@kubb/parser-ts`.
|
|
103
|
+
*
|
|
104
|
+
* @default [parserTs] from `@kubb/parser-ts`
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* import { parserTs, tsxParser } from '@kubb/parser-ts'
|
|
108
|
+
* export default defineConfig({
|
|
109
|
+
* parsers: [parserTs, tsxParser],
|
|
110
|
+
* })
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
parsers: Array<Parser>
|
|
114
|
+
/**
|
|
115
|
+
* Adapter that parses input files into the universal `InputNode` representation.
|
|
116
|
+
* Use `@kubb/adapter-oas` for OpenAPI/Swagger or `@kubb/adapter-asyncapi` for other formats.
|
|
117
|
+
*
|
|
118
|
+
* When omitted, Kubb runs in plugin-only mode: `kubb:plugin:setup` fires and files
|
|
119
|
+
* injected via `injectFile` are written, but no AST walk occurs and generator hooks
|
|
120
|
+
* (`kubb:generate:schema`, `kubb:generate:operation`) are never emitted.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* import { adapterOas } from '@kubb/adapter-oas'
|
|
125
|
+
* export default defineConfig({
|
|
126
|
+
* adapter: adapterOas(),
|
|
127
|
+
* input: { path: './petstore.yaml' },
|
|
128
|
+
* })
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
adapter?: Adapter
|
|
132
|
+
/**
|
|
133
|
+
* Source file or data to generate code from.
|
|
134
|
+
* Use `input.path` for a file path or `input.data` for inline data.
|
|
135
|
+
* Required when an adapter is configured; omit when running in plugin-only mode.
|
|
136
|
+
*/
|
|
137
|
+
input?: TInput
|
|
138
|
+
output: {
|
|
139
|
+
/**
|
|
140
|
+
* Output directory for generated files, absolute or relative to `root`.
|
|
141
|
+
*
|
|
142
|
+
* All generated files will be written under this directory. Subdirectories can be created
|
|
143
|
+
* by plugins based on grouping strategy (by tag, path, etc.).
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* output: {
|
|
148
|
+
* path: './src/gen', // generates ./src/gen/api.ts, ./src/gen/types.ts, etc.
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
path: string
|
|
153
|
+
/**
|
|
154
|
+
* Remove all files from the output directory before starting the build.
|
|
155
|
+
*
|
|
156
|
+
* Useful to ensure old generated files aren't mixed with new ones.
|
|
157
|
+
* Set to `true` for fresh builds, `false` to preserve manual edits in output dir.
|
|
158
|
+
*
|
|
159
|
+
* @default false
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* clean: true // wipes ./src/gen/* before generating
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
clean?: boolean
|
|
166
|
+
/**
|
|
167
|
+
* Auto-format generated files after code generation completes.
|
|
168
|
+
*
|
|
169
|
+
* Applies a code formatter to all generated files. Use `'auto'` to detect which formatter
|
|
170
|
+
* is available on your system. Pass `false` to skip formatting (useful for CI or specific workflows).
|
|
171
|
+
*
|
|
172
|
+
* @default false
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* format: 'auto' // auto-detect prettier, biome, or oxfmt
|
|
176
|
+
* format: 'prettier' // force prettier
|
|
177
|
+
* format: false // skip formatting
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
format?: 'auto' | 'prettier' | 'biome' | 'oxfmt' | false
|
|
181
|
+
/**
|
|
182
|
+
* Auto-lint generated files after code generation completes.
|
|
183
|
+
*
|
|
184
|
+
* Analyzes all generated files for style/correctness issues. Use `'auto'` to detect which linter
|
|
185
|
+
* is available on your system. Pass `false` to skip linting.
|
|
186
|
+
*
|
|
187
|
+
* @default false
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* lint: 'auto' // auto-detect oxlint, biome, or eslint
|
|
191
|
+
* lint: 'eslint' // force eslint
|
|
192
|
+
* lint: false // skip linting
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
lint?: 'auto' | 'eslint' | 'biome' | 'oxlint' | false
|
|
196
|
+
/**
|
|
197
|
+
* Map file extensions to different output extensions.
|
|
198
|
+
*
|
|
199
|
+
* Useful when you want generated `.ts` imports to reference `.js` files or vice versa (e.g., for ESM dual packages).
|
|
200
|
+
* Keys are the original extension, values are the output extension. Use empty string `''` to omit extension.
|
|
201
|
+
*
|
|
202
|
+
* @default { '.ts': '.ts' }
|
|
203
|
+
* @example
|
|
204
|
+
* ```ts
|
|
205
|
+
* extension: { '.ts': '.js' } // generates import './api.js' instead of './api.ts'
|
|
206
|
+
* extension: { '.ts': '', '.tsx': '.jsx' }
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
extension?: Record<FileNode['extname'], FileNode['extname'] | ''>
|
|
210
|
+
/**
|
|
211
|
+
* Banner text prepended to every generated file.
|
|
212
|
+
*
|
|
213
|
+
* Useful for auto-generation notices or license headers. Choose a preset or write custom text.
|
|
214
|
+
* Use `'simple'` for a basic Kubb banner, `'full'` for detailed metadata, or `false` to omit.
|
|
215
|
+
*
|
|
216
|
+
* @default 'simple'
|
|
217
|
+
* @example
|
|
218
|
+
* ```ts
|
|
219
|
+
* defaultBanner: 'simple' // "This file was autogenerated by Kubb"
|
|
220
|
+
* defaultBanner: 'full' // adds source, title, description, API version
|
|
221
|
+
* defaultBanner: false // no banner
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
defaultBanner?: 'simple' | 'full' | false
|
|
225
|
+
/**
|
|
226
|
+
* When `true`, overwrites existing files. When `false`, skips generated files that already exist.
|
|
227
|
+
*
|
|
228
|
+
* Individual plugins can override this setting. This is useful for preventing accidental data loss
|
|
229
|
+
* when re-generating while you have local edits in the output folder.
|
|
230
|
+
*
|
|
231
|
+
* @default false
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* override: true // regenerate everything, even existing files
|
|
235
|
+
* override: false // skip files that already exist
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
override?: boolean
|
|
239
|
+
} & ExtractRegistryKey<Kubb.ConfigOptionsRegistry, 'output'>
|
|
240
|
+
/**
|
|
241
|
+
* Storage backend that controls where and how generated files are persisted.
|
|
242
|
+
*
|
|
243
|
+
* Defaults to `fsStorage()` which writes to the file system. Pass `memoryStorage()` to keep files in RAM,
|
|
244
|
+
* or implement a custom `Storage` interface to write to cloud storage, databases, or other backends.
|
|
245
|
+
*
|
|
246
|
+
* @default fsStorage()
|
|
247
|
+
* @example
|
|
248
|
+
* ```ts
|
|
249
|
+
* import { memoryStorage } from '@kubb/core'
|
|
250
|
+
*
|
|
251
|
+
* // Keep generated files in memory (useful for testing, CI pipelines)
|
|
252
|
+
* storage: memoryStorage()
|
|
253
|
+
*
|
|
254
|
+
* // Use custom S3 storage
|
|
255
|
+
* storage: myS3Storage()
|
|
256
|
+
* ```
|
|
257
|
+
*
|
|
258
|
+
* @see {@link Storage} interface for implementing custom backends.
|
|
259
|
+
*/
|
|
260
|
+
storage: Storage
|
|
261
|
+
/**
|
|
262
|
+
* Plugins that execute during the build to generate code and transform the AST.
|
|
263
|
+
*
|
|
264
|
+
* Each plugin processes the AST produced by the adapter and can emit files for different
|
|
265
|
+
* programming languages or formats (TypeScript, Zod schemas, Faker data, etc.).
|
|
266
|
+
* Dependencies are enforced — an error is thrown if a plugin requires another plugin that isn't registered.
|
|
267
|
+
*
|
|
268
|
+
* Plugins can declare their own options via `PluginFactoryOptions`. See plugin documentation for details.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
273
|
+
* import { pluginZod } from '@kubb/plugin-zod'
|
|
274
|
+
*
|
|
275
|
+
* plugins: [
|
|
276
|
+
* pluginTs({ output: { path: './src/gen' } }),
|
|
277
|
+
* pluginZod({ output: { path: './src/gen' } }),
|
|
278
|
+
* ]
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
plugins: Array<Plugin>
|
|
282
|
+
/**
|
|
283
|
+
* Middleware instances that observe build events and post-process generated code.
|
|
284
|
+
*
|
|
285
|
+
* Middleware fires AFTER all plugins for each event. Perfect for tasks like:
|
|
286
|
+
* - Auditing what was generated
|
|
287
|
+
* - Adding barrel/index files
|
|
288
|
+
* - Validating output
|
|
289
|
+
* - Running custom transformations
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```ts
|
|
293
|
+
* import { middlewareBarrel } from '@kubb/middleware-barrel'
|
|
294
|
+
*
|
|
295
|
+
* middleware: [middlewareBarrel()]
|
|
296
|
+
* ```
|
|
297
|
+
*
|
|
298
|
+
* @see {@link defineMiddleware} to create custom middleware.
|
|
299
|
+
*/
|
|
300
|
+
middleware?: Array<Middleware>
|
|
301
|
+
/**
|
|
302
|
+
* Renderer that converts generated AST nodes to code strings.
|
|
303
|
+
*
|
|
304
|
+
* By default, Kubb uses the JSX renderer (`rendererJsx`). Pass a custom renderer to support
|
|
305
|
+
* different output formats (template engines, code generation DSLs, etc.).
|
|
306
|
+
*
|
|
307
|
+
* @default rendererJsx() // from @kubb/renderer-jsx
|
|
308
|
+
* @example
|
|
309
|
+
* ```ts
|
|
310
|
+
* import { rendererJsx } from '@kubb/renderer-jsx'
|
|
311
|
+
* renderer: rendererJsx()
|
|
312
|
+
* ```
|
|
313
|
+
*
|
|
314
|
+
* @see {@link Renderer} to implement a custom renderer.
|
|
315
|
+
*/
|
|
316
|
+
renderer?: RendererFactory
|
|
317
|
+
/**
|
|
318
|
+
* Kubb Studio cloud integration settings.
|
|
319
|
+
*
|
|
320
|
+
* Kubb Studio (https://kubb.studio) is a web-based IDE for managing API specs and generated code.
|
|
321
|
+
* Set to `true` to enable with default settings, or pass an object to customize the Studio URL.
|
|
322
|
+
*
|
|
323
|
+
* @default false // disabled by default
|
|
324
|
+
* @example
|
|
325
|
+
* ```ts
|
|
326
|
+
* devtools: true // use default Kubb Studio
|
|
327
|
+
* devtools: { studioUrl: 'https://my-studio.dev' } // custom Studio instance
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
devtools?:
|
|
331
|
+
| true
|
|
332
|
+
| {
|
|
333
|
+
/**
|
|
334
|
+
* Override the Kubb Studio base URL.
|
|
335
|
+
* @default 'https://kubb.studio'
|
|
336
|
+
*/
|
|
337
|
+
studioUrl?: typeof DEFAULT_STUDIO_URL | (string & {})
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Lifecycle hooks that execute during or after the build process.
|
|
341
|
+
*
|
|
342
|
+
* Hooks allow you to run external tools (prettier, eslint, custom scripts) based on build events.
|
|
343
|
+
* Currently supports the `done` hook which fires after all plugins and middleware complete.
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* ```ts
|
|
347
|
+
* hooks: {
|
|
348
|
+
* done: 'prettier --write "./src/gen"', // auto-format generated files
|
|
349
|
+
* // or multiple commands:
|
|
350
|
+
* done: ['prettier --write "./src/gen"', 'eslint --fix "./src/gen"']
|
|
351
|
+
* }
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
hooks?: {
|
|
355
|
+
/**
|
|
356
|
+
* Command(s) to run after all plugins and middleware complete generation.
|
|
357
|
+
*
|
|
358
|
+
* Useful for post-processing: formatting, linting, copying files, or custom validation.
|
|
359
|
+
* Pass a single command string or array of command strings to run sequentially.
|
|
360
|
+
* Commands are executed relative to the `root` directory.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* ```ts
|
|
364
|
+
* done: 'prettier --write "./src/gen"'
|
|
365
|
+
* done: ['prettier --write "./src/gen"', 'eslint --fix "./src/gen"']
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
368
|
+
done?: string | Array<string>
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Partial `Config` for user-facing entry points with sensible defaults.
|
|
374
|
+
*
|
|
375
|
+
* `UserConfig` is what you pass to `defineConfig()`. It has optional `root`, `plugins`, `parsers`, and `adapter`
|
|
376
|
+
* fields (which fall back to sensible defaults). All other Config options are available, including `output`, `input`,
|
|
377
|
+
* `storage`, `middleware`, `renderer`, `devtools`, and `hooks`.
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```ts
|
|
381
|
+
* export default defineConfig({
|
|
382
|
+
* input: { path: './petstore.yaml' },
|
|
383
|
+
* output: { path: './src/gen' },
|
|
384
|
+
* plugins: [pluginTs(), pluginZod()],
|
|
385
|
+
* })
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
export type UserConfig<TInput = Input> = Omit<Config<TInput>, 'root' | 'plugins' | 'parsers' | 'adapter' | 'storage'> & {
|
|
389
|
+
/**
|
|
390
|
+
* Project root directory, absolute or relative to the config file location.
|
|
391
|
+
* @default process.cwd()
|
|
392
|
+
*/
|
|
393
|
+
root?: string
|
|
394
|
+
/**
|
|
395
|
+
* Custom parsers that convert generated AST nodes to strings (TypeScript, JSON, markdown, etc.).
|
|
396
|
+
* @default [parserTs] // from `@kubb/parser-ts`
|
|
397
|
+
*/
|
|
398
|
+
parsers?: Array<Parser>
|
|
399
|
+
/**
|
|
400
|
+
* Adapter that parses your API specification into Kubb's universal AST.
|
|
401
|
+
* When omitted, Kubb runs in plugin-only mode.
|
|
402
|
+
*/
|
|
403
|
+
adapter?: Adapter
|
|
404
|
+
/**
|
|
405
|
+
* Plugins that execute during the build to generate code and transform the AST.
|
|
406
|
+
* @default []
|
|
407
|
+
*/
|
|
408
|
+
plugins?: Array<Plugin>
|
|
409
|
+
/**
|
|
410
|
+
* Storage backend that controls where and how generated files are persisted.
|
|
411
|
+
* @default fsStorage()
|
|
412
|
+
*/
|
|
413
|
+
storage?: Storage
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
declare global {
|
|
417
|
+
namespace Kubb {
|
|
418
|
+
/**
|
|
419
|
+
* Registry that maps plugin names to their `PluginFactoryOptions`.
|
|
420
|
+
* Augment this interface in each plugin's `types.ts` to enable automatic
|
|
421
|
+
* typing for `getPlugin` and `requirePlugin`.
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* ```ts
|
|
425
|
+
* // packages/plugin-ts/src/types.ts
|
|
426
|
+
* declare global {
|
|
427
|
+
* namespace Kubb {
|
|
428
|
+
* interface PluginRegistry {
|
|
429
|
+
* 'plugin-ts': PluginTs
|
|
430
|
+
* }
|
|
431
|
+
* }
|
|
432
|
+
* }
|
|
433
|
+
* ```
|
|
434
|
+
*/
|
|
435
|
+
interface PluginRegistry {}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Extension point for root `Config['output']` options.
|
|
439
|
+
* Augment the `output` key in middleware or plugin packages to add extra fields
|
|
440
|
+
* to the global output configuration without touching core types.
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```ts
|
|
444
|
+
* // packages/middleware-barrel/src/types.ts
|
|
445
|
+
* declare global {
|
|
446
|
+
* namespace Kubb {
|
|
447
|
+
* interface ConfigOptionsRegistry {
|
|
448
|
+
* output: {
|
|
449
|
+
* barrel?: import('./types.ts').BarrelConfig | false
|
|
450
|
+
* }
|
|
451
|
+
* }
|
|
452
|
+
* }
|
|
453
|
+
* }
|
|
454
|
+
* ```
|
|
455
|
+
*/
|
|
456
|
+
interface ConfigOptionsRegistry {}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Extension point for per-plugin `Output` options.
|
|
460
|
+
* Augment the `output` key in middleware or plugin packages to add extra fields
|
|
461
|
+
* to the per-plugin output configuration without touching core types.
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* ```ts
|
|
465
|
+
* // packages/middleware-barrel/src/types.ts
|
|
466
|
+
* declare global {
|
|
467
|
+
* namespace Kubb {
|
|
468
|
+
* interface PluginOptionsRegistry {
|
|
469
|
+
* output: {
|
|
470
|
+
* barrel?: import('./types.ts').PluginBarrelConfig | false
|
|
471
|
+
* }
|
|
472
|
+
* }
|
|
473
|
+
* }
|
|
474
|
+
* }
|
|
475
|
+
* ```
|
|
476
|
+
*/
|
|
477
|
+
interface PluginOptionsRegistry {}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Lifecycle events emitted during Kubb code generation.
|
|
483
|
+
* Use these for logging, progress tracking, and custom integrations.
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```typescript
|
|
487
|
+
* import type { AsyncEventEmitter } from '@internals/utils'
|
|
488
|
+
* import type { KubbHooks } from '@kubb/core'
|
|
489
|
+
*
|
|
490
|
+
* const hooks: AsyncEventEmitter<KubbHooks> = new AsyncEventEmitter()
|
|
491
|
+
*
|
|
492
|
+
* hooks.on('kubb:lifecycle:start', () => {
|
|
493
|
+
* console.log('Starting Kubb generation')
|
|
494
|
+
* })
|
|
495
|
+
*
|
|
496
|
+
* hooks.on('kubb:plugin:end', ({ plugin, duration }) => {
|
|
497
|
+
* console.log(`Plugin ${plugin.name} completed in ${duration}ms`)
|
|
498
|
+
* })
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
export interface KubbHooks {
|
|
502
|
+
'kubb:lifecycle:start': [ctx: KubbLifecycleStartContext]
|
|
503
|
+
'kubb:lifecycle:end': []
|
|
504
|
+
'kubb:config:start': []
|
|
505
|
+
'kubb:config:end': [ctx: KubbConfigEndContext]
|
|
506
|
+
'kubb:generation:start': [ctx: KubbGenerationStartContext]
|
|
507
|
+
'kubb:generation:end': [ctx: KubbGenerationEndContext]
|
|
508
|
+
'kubb:generation:summary': [ctx: KubbGenerationSummaryContext]
|
|
509
|
+
'kubb:format:start': []
|
|
510
|
+
'kubb:format:end': []
|
|
511
|
+
'kubb:lint:start': []
|
|
512
|
+
'kubb:lint:end': []
|
|
513
|
+
'kubb:hooks:start': []
|
|
514
|
+
'kubb:hooks:end': []
|
|
515
|
+
'kubb:hook:start': [ctx: KubbHookStartContext]
|
|
516
|
+
'kubb:hook:end': [ctx: KubbHookEndContext]
|
|
517
|
+
'kubb:version:new': [ctx: KubbVersionNewContext]
|
|
518
|
+
'kubb:info': [ctx: KubbInfoContext]
|
|
519
|
+
'kubb:error': [ctx: KubbErrorContext]
|
|
520
|
+
'kubb:success': [ctx: KubbSuccessContext]
|
|
521
|
+
'kubb:warn': [ctx: KubbWarnContext]
|
|
522
|
+
'kubb:debug': [ctx: KubbDebugContext]
|
|
523
|
+
'kubb:files:processing:start': [ctx: KubbFilesProcessingStartContext]
|
|
524
|
+
'kubb:file:processing:update': [ctx: KubbFileProcessingUpdateContext]
|
|
525
|
+
'kubb:files:processing:end': [ctx: KubbFilesProcessingEndContext]
|
|
526
|
+
'kubb:plugin:start': [ctx: KubbPluginStartContext]
|
|
527
|
+
'kubb:plugin:end': [ctx: KubbPluginEndContext]
|
|
528
|
+
'kubb:plugin:setup': [ctx: KubbPluginSetupContext]
|
|
529
|
+
'kubb:build:start': [ctx: KubbBuildStartContext]
|
|
530
|
+
'kubb:plugins:end': [ctx: KubbPluginsEndContext]
|
|
531
|
+
'kubb:build:end': [ctx: KubbBuildEndContext]
|
|
532
|
+
'kubb:generate:schema': [node: SchemaNode, ctx: GeneratorContext]
|
|
533
|
+
'kubb:generate:operation': [node: OperationNode, ctx: GeneratorContext]
|
|
534
|
+
'kubb:generate:operations': [nodes: Array<OperationNode>, ctx: GeneratorContext]
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export type KubbBuildStartContext = {
|
|
538
|
+
config: Config
|
|
539
|
+
adapter: Adapter
|
|
540
|
+
inputNode: InputNode
|
|
541
|
+
getPlugin<TName extends keyof Kubb.PluginRegistry>(name: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined
|
|
542
|
+
getPlugin(name: string): Plugin | undefined
|
|
543
|
+
readonly files: ReadonlyArray<FileNode>
|
|
544
|
+
upsertFile: (...files: Array<FileNode>) => void
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export type KubbPluginsEndContext = {
|
|
548
|
+
config: Config
|
|
549
|
+
readonly files: ReadonlyArray<FileNode>
|
|
550
|
+
upsertFile: (...files: Array<FileNode>) => void
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export type KubbBuildEndContext = {
|
|
554
|
+
files: Array<FileNode>
|
|
555
|
+
config: Config
|
|
556
|
+
outputDir: string
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export type KubbLifecycleStartContext = {
|
|
560
|
+
version: string
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export type KubbConfigEndContext = {
|
|
564
|
+
configs: Array<Config>
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export type KubbGenerationStartContext = {
|
|
568
|
+
config: Config
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export type KubbGenerationEndContext = {
|
|
572
|
+
config: Config
|
|
573
|
+
files: Array<FileNode>
|
|
574
|
+
sources: Map<string, string>
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export type KubbGenerationSummaryContext = {
|
|
578
|
+
config: Config
|
|
579
|
+
failedPlugins: Set<{ plugin: Plugin; error: Error }>
|
|
580
|
+
status: 'success' | 'failed'
|
|
581
|
+
hrStart: [number, number]
|
|
582
|
+
filesCreated: number
|
|
583
|
+
pluginTimings?: Map<Plugin['name'], number>
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export type KubbVersionNewContext = {
|
|
587
|
+
currentVersion: string
|
|
588
|
+
latestVersion: string
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export type KubbInfoContext = {
|
|
592
|
+
message: string
|
|
593
|
+
info?: string
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export type KubbErrorContext = {
|
|
597
|
+
error: Error
|
|
598
|
+
meta?: Record<string, unknown>
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export type KubbSuccessContext = {
|
|
602
|
+
message: string
|
|
603
|
+
info?: string
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export type KubbWarnContext = {
|
|
607
|
+
message: string
|
|
608
|
+
info?: string
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export type KubbDebugContext = {
|
|
612
|
+
date: Date
|
|
613
|
+
logs: Array<string>
|
|
614
|
+
fileName?: string
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export type KubbFilesProcessingStartContext = {
|
|
618
|
+
files: Array<FileNode>
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export type KubbFileProcessingUpdateContext = {
|
|
622
|
+
processed: number
|
|
623
|
+
total: number
|
|
624
|
+
percentage: number
|
|
625
|
+
source?: string
|
|
626
|
+
file: FileNode
|
|
627
|
+
config: Config
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export type KubbFilesProcessingEndContext = {
|
|
631
|
+
files: Array<FileNode>
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export type KubbHookStartContext = {
|
|
635
|
+
id?: string
|
|
636
|
+
command: string
|
|
637
|
+
args?: readonly string[]
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export type KubbHookEndContext = {
|
|
641
|
+
id?: string
|
|
642
|
+
command: string
|
|
643
|
+
args?: readonly string[]
|
|
644
|
+
success: boolean
|
|
645
|
+
error: Error | null
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* CLI options derived from command-line flags.
|
|
650
|
+
*/
|
|
651
|
+
export type CLIOptions = {
|
|
652
|
+
config?: string
|
|
653
|
+
watch?: boolean
|
|
654
|
+
/** @default 'silent' */
|
|
655
|
+
logLevel?: 'silent' | 'info' | 'debug'
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* All accepted forms of a Kubb configuration.
|
|
660
|
+
* Accepts `Config`/`Config[]`/promise or a factory (optionally receiving `TCliOptions`).
|
|
661
|
+
*/
|
|
662
|
+
export type PossibleConfig<TCliOptions = undefined> =
|
|
663
|
+
| PossiblePromise<Config | Config[]>
|
|
664
|
+
| ((...args: [TCliOptions] extends [undefined] ? [] : [TCliOptions]) => PossiblePromise<Config | Config[]>)
|
|
665
|
+
|
|
666
|
+
type SetupOptions = {
|
|
667
|
+
hooks?: AsyncEventEmitter<KubbHooks>
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Full output produced by a successful or failed build.
|
|
672
|
+
*/
|
|
673
|
+
export type BuildOutput = {
|
|
674
|
+
/**
|
|
675
|
+
* Plugins that threw during installation, paired with the caught error.
|
|
676
|
+
*/
|
|
677
|
+
failedPlugins: Set<{ plugin: Plugin; error: Error }>
|
|
678
|
+
files: Array<FileNode>
|
|
679
|
+
driver: PluginDriver
|
|
680
|
+
/**
|
|
681
|
+
* Elapsed time in milliseconds for each plugin, keyed by plugin name.
|
|
682
|
+
*/
|
|
683
|
+
pluginTimings: Map<string, number>
|
|
684
|
+
error?: Error
|
|
685
|
+
/**
|
|
686
|
+
* Raw generated source, keyed by absolute file path.
|
|
687
|
+
*/
|
|
688
|
+
sources: Map<string, string>
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Kubb code generation instance returned by {@link createKubb}.
|
|
693
|
+
*
|
|
694
|
+
* Use this when orchestrating multiple builds, inspecting plugin timings, or integrating Kubb into a larger toolchain.
|
|
695
|
+
* For a single one-off build, chain directly: `await createKubb(config).build()`.
|
|
696
|
+
*/
|
|
697
|
+
export type Kubb = {
|
|
698
|
+
/**
|
|
699
|
+
* Shared event emitter for lifecycle and status events. Attach listeners before calling `setup()` or `build()`.
|
|
700
|
+
*/
|
|
701
|
+
readonly hooks: AsyncEventEmitter<KubbHooks>
|
|
702
|
+
/**
|
|
703
|
+
* Generated source code keyed by absolute file path. Available after `build()` or `safeBuild()` completes.
|
|
704
|
+
*/
|
|
705
|
+
readonly sources: Map<string, string>
|
|
706
|
+
/**
|
|
707
|
+
* Plugin driver managing all plugins. Available after `setup()` completes.
|
|
708
|
+
*/
|
|
709
|
+
readonly driver: PluginDriver | undefined
|
|
710
|
+
/**
|
|
711
|
+
* Resolved configuration with defaults applied. Available after `setup()` completes.
|
|
712
|
+
*/
|
|
713
|
+
readonly config: Config | undefined
|
|
714
|
+
/**
|
|
715
|
+
* Resolves config and initializes the driver. `build()` calls this automatically.
|
|
716
|
+
*/
|
|
717
|
+
setup(): Promise<void>
|
|
718
|
+
/**
|
|
719
|
+
* Runs the full pipeline and throws on any plugin error. Automatically calls `setup()` if needed.
|
|
720
|
+
*/
|
|
721
|
+
build(): Promise<BuildOutput>
|
|
722
|
+
/**
|
|
723
|
+
* Runs the full pipeline and captures errors in `BuildOutput` instead of throwing. Automatically calls `setup()` if needed.
|
|
724
|
+
*/
|
|
725
|
+
safeBuild(): Promise<BuildOutput>
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
type SetupResult = {
|
|
729
|
+
hooks: AsyncEventEmitter<KubbHooks>
|
|
730
|
+
driver: PluginDriver
|
|
731
|
+
sources: Map<string, string>
|
|
732
|
+
config: Config
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promise<SetupResult> {
|
|
736
|
+
const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
|
|
737
|
+
const config: Config = {
|
|
738
|
+
...userConfig,
|
|
739
|
+
root: userConfig.root || process.cwd(),
|
|
740
|
+
parsers: userConfig.parsers ?? [],
|
|
741
|
+
adapter: userConfig.adapter,
|
|
742
|
+
output: {
|
|
743
|
+
format: false,
|
|
744
|
+
lint: false,
|
|
745
|
+
extension: DEFAULT_EXTENSION,
|
|
746
|
+
defaultBanner: DEFAULT_BANNER,
|
|
747
|
+
...userConfig.output,
|
|
748
|
+
},
|
|
749
|
+
storage: userConfig.storage ?? fsStorage(),
|
|
750
|
+
devtools: userConfig.devtools
|
|
751
|
+
? {
|
|
752
|
+
studioUrl: DEFAULT_STUDIO_URL,
|
|
753
|
+
...(typeof userConfig.devtools === 'boolean' ? {} : userConfig.devtools),
|
|
754
|
+
}
|
|
755
|
+
: undefined,
|
|
756
|
+
plugins: (userConfig.plugins ?? []) as unknown as Config['plugins'],
|
|
757
|
+
}
|
|
758
|
+
const driver = new PluginDriver(config, {
|
|
759
|
+
hooks,
|
|
760
|
+
})
|
|
761
|
+
const sources: Map<string, string> = new Map<string, string>()
|
|
762
|
+
const diagnosticInfo = getDiagnosticInfo()
|
|
763
|
+
|
|
764
|
+
await hooks.emit('kubb:debug', {
|
|
765
|
+
date: new Date(),
|
|
766
|
+
logs: [
|
|
767
|
+
'Configuration:',
|
|
768
|
+
` • Name: ${userConfig.name || 'unnamed'}`,
|
|
769
|
+
` • Root: ${userConfig.root || process.cwd()}`,
|
|
770
|
+
` • Output: ${userConfig.output?.path || 'not specified'}`,
|
|
771
|
+
` • Plugins: ${userConfig.plugins?.length || 0}`,
|
|
772
|
+
'Output Settings:',
|
|
773
|
+
` • Storage: ${config.storage.name}`,
|
|
774
|
+
` • Formatter: ${userConfig.output?.format || 'none'}`,
|
|
775
|
+
` • Linter: ${userConfig.output?.lint || 'none'}`,
|
|
776
|
+
'Environment:',
|
|
777
|
+
Object.entries(diagnosticInfo)
|
|
778
|
+
.map(([key, value]) => ` • ${key}: ${value}`)
|
|
779
|
+
.join('\n'),
|
|
780
|
+
],
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
try {
|
|
784
|
+
if (isInputPath(userConfig) && !new URLPath(userConfig.input.path).isURL) {
|
|
785
|
+
await exists(userConfig.input.path)
|
|
786
|
+
|
|
787
|
+
await hooks.emit('kubb:debug', {
|
|
788
|
+
date: new Date(),
|
|
789
|
+
logs: [`✓ Input file validated: ${userConfig.input.path}`],
|
|
790
|
+
})
|
|
791
|
+
}
|
|
792
|
+
} catch (caughtError) {
|
|
793
|
+
if (isInputPath(userConfig)) {
|
|
794
|
+
const error = caughtError as Error
|
|
795
|
+
|
|
796
|
+
throw new Error(
|
|
797
|
+
`Cannot read file/URL defined in \`input.path\` or set with \`kubb generate PATH\` in the CLI of your Kubb config ${userConfig.input.path}`,
|
|
798
|
+
{
|
|
799
|
+
cause: error,
|
|
800
|
+
},
|
|
801
|
+
)
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (config.output.clean) {
|
|
806
|
+
await hooks.emit('kubb:debug', {
|
|
807
|
+
date: new Date(),
|
|
808
|
+
logs: ['Cleaning output directories', ` • Output: ${config.output.path}`],
|
|
809
|
+
})
|
|
810
|
+
await config.storage.clear(resolve(config.root, config.output.path))
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Register middleware hooks after all plugin hooks are registered.
|
|
814
|
+
// Because AsyncEventEmitter calls listeners in registration order,
|
|
815
|
+
// middleware hooks for any event fire after all plugin hooks for that event.
|
|
816
|
+
function registerMiddlewareHook<K extends keyof KubbHooks & string>(event: K, middlewareHooks: Middleware['hooks']) {
|
|
817
|
+
const handler = middlewareHooks[event]
|
|
818
|
+
if (handler) {
|
|
819
|
+
hooks.on(event, handler)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
for (const middleware of config.middleware ?? []) {
|
|
824
|
+
for (const event of Object.keys(middleware.hooks) as Array<keyof KubbHooks & string>) {
|
|
825
|
+
registerMiddlewareHook(event, middleware.hooks)
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (config.adapter) {
|
|
830
|
+
const source = inputToAdapterSource(config)
|
|
831
|
+
|
|
832
|
+
await hooks.emit('kubb:debug', {
|
|
833
|
+
date: new Date(),
|
|
834
|
+
logs: [`Running adapter: ${config.adapter.name}`],
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
driver.adapter = config.adapter
|
|
838
|
+
driver.inputNode = await config.adapter.parse(source)
|
|
839
|
+
|
|
840
|
+
await hooks.emit('kubb:debug', {
|
|
841
|
+
date: new Date(),
|
|
842
|
+
logs: [
|
|
843
|
+
`✓ Adapter '${config.adapter.name}' resolved InputNode`,
|
|
844
|
+
` • Schemas: ${driver.inputNode.schemas.length}`,
|
|
845
|
+
` • Operations: ${driver.inputNode.operations.length}`,
|
|
846
|
+
],
|
|
847
|
+
})
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return {
|
|
851
|
+
config,
|
|
852
|
+
hooks,
|
|
853
|
+
driver,
|
|
854
|
+
sources,
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Walks the AST and dispatches nodes to a plugin's direct AST hooks
|
|
860
|
+
* (`schema`, `operation`, `operations`).
|
|
861
|
+
*
|
|
862
|
+
* When `include` contains only operation-scoped filters (`tag`, `operationId`, `path`,
|
|
863
|
+
* `method`, `contentType`) and no `schemaName` filter, the function pre-computes the set
|
|
864
|
+
* of top-level schema names transitively reachable from the included operations and skips
|
|
865
|
+
* schemas that fall outside that set. This ensures that component schemas referenced
|
|
866
|
+
* exclusively by excluded operations are not generated.
|
|
867
|
+
*/
|
|
868
|
+
async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorContext): Promise<void> {
|
|
869
|
+
const { adapter, inputNode, resolver, driver } = context
|
|
870
|
+
const { exclude, include, override } = plugin.options
|
|
871
|
+
|
|
872
|
+
if (!adapter || !inputNode) {
|
|
873
|
+
throw new Error(`[${plugin.name}] No adapter found. Add an OAS adapter (e.g. adapterOas()) before this plugin in your Kubb config.`)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function resolveRenderer(gen: Generator): RendererFactory | undefined {
|
|
877
|
+
return gen.renderer === null ? undefined : (gen.renderer ?? plugin.renderer ?? context.config.renderer)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const generators = plugin.generators ?? []
|
|
881
|
+
const collectedOperations: Array<OperationNode> = []
|
|
882
|
+
|
|
883
|
+
const generatorContext = {
|
|
884
|
+
...context,
|
|
885
|
+
resolver: driver.getResolver(plugin.name),
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// When `include` has operation-based filters (tag, operationId, path, method, contentType)
|
|
889
|
+
// but no schema-level filters (schemaName), pre-compute the set of top-level schema names
|
|
890
|
+
// that are transitively referenced by the included operations. Schemas outside that set are
|
|
891
|
+
// skipped so that types belonging exclusively to excluded operations are not generated.
|
|
892
|
+
const operationFilterTypes = new Set(['tag', 'operationId', 'path', 'method', 'contentType'])
|
|
893
|
+
const hasOperationBasedIncludes = include?.some(({ type }) => operationFilterTypes.has(type)) ?? false
|
|
894
|
+
const hasSchemaNameIncludes = include?.some(({ type }) => type === 'schemaName') ?? false
|
|
895
|
+
|
|
896
|
+
let allowedSchemaNames: Set<string> | undefined
|
|
897
|
+
if (hasOperationBasedIncludes && !hasSchemaNameIncludes) {
|
|
898
|
+
const includedOps = inputNode.operations.filter((op) => resolver.resolveOptions(op, { options: plugin.options, exclude, include, override }) !== null)
|
|
899
|
+
allowedSchemaNames = collectUsedSchemaNames(includedOps, inputNode.schemas)
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
await walk(inputNode, {
|
|
903
|
+
depth: 'shallow',
|
|
904
|
+
async schema(node) {
|
|
905
|
+
const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
|
|
906
|
+
|
|
907
|
+
// Skip named top-level schemas that are not reachable from any included operation.
|
|
908
|
+
if (allowedSchemaNames !== undefined && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) {
|
|
909
|
+
return
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const options = resolver.resolveOptions(transformedNode, {
|
|
913
|
+
options: plugin.options,
|
|
914
|
+
exclude,
|
|
915
|
+
include,
|
|
916
|
+
override,
|
|
917
|
+
})
|
|
918
|
+
if (options === null) return
|
|
919
|
+
|
|
920
|
+
const ctx = { ...generatorContext, options }
|
|
921
|
+
|
|
922
|
+
for (const gen of generators) {
|
|
923
|
+
if (!gen.schema) continue
|
|
924
|
+
const result = await gen.schema(transformedNode, ctx)
|
|
925
|
+
await applyHookResult(result, driver, resolveRenderer(gen))
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
await driver.hooks.emit('kubb:generate:schema', transformedNode, ctx)
|
|
929
|
+
},
|
|
930
|
+
async operation(node) {
|
|
931
|
+
const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
|
|
932
|
+
const options = resolver.resolveOptions(transformedNode, {
|
|
933
|
+
options: plugin.options,
|
|
934
|
+
exclude,
|
|
935
|
+
include,
|
|
936
|
+
override,
|
|
937
|
+
})
|
|
938
|
+
if (options !== null) {
|
|
939
|
+
collectedOperations.push(transformedNode)
|
|
940
|
+
|
|
941
|
+
const ctx = { ...generatorContext, options }
|
|
942
|
+
|
|
943
|
+
for (const gen of generators) {
|
|
944
|
+
if (!gen.operation) continue
|
|
945
|
+
const result = await gen.operation(transformedNode, ctx)
|
|
946
|
+
await applyHookResult(result, driver, resolveRenderer(gen))
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
await driver.hooks.emit('kubb:generate:operation', transformedNode, ctx)
|
|
950
|
+
}
|
|
951
|
+
},
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
if (collectedOperations.length > 0) {
|
|
955
|
+
const ctx = { ...generatorContext, options: plugin.options }
|
|
956
|
+
|
|
957
|
+
for (const gen of generators) {
|
|
958
|
+
if (!gen.operations) continue
|
|
959
|
+
const result = await gen.operations(collectedOperations, ctx)
|
|
960
|
+
await applyHookResult(result, driver, resolveRenderer(gen))
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
await driver.hooks.emit('kubb:generate:operations', collectedOperations, ctx)
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
|
|
968
|
+
const { driver, hooks, sources } = setupResult
|
|
969
|
+
|
|
970
|
+
const failedPlugins = new Set<{ plugin: Plugin; error: Error }>()
|
|
971
|
+
const pluginTimings = new Map<string, number>()
|
|
972
|
+
const config = driver.config
|
|
973
|
+
|
|
974
|
+
try {
|
|
975
|
+
await driver.emitSetupHooks()
|
|
976
|
+
|
|
977
|
+
if (driver.adapter && driver.inputNode) {
|
|
978
|
+
await hooks.emit('kubb:build:start', {
|
|
979
|
+
config,
|
|
980
|
+
adapter: driver.adapter,
|
|
981
|
+
inputNode: driver.inputNode,
|
|
982
|
+
getPlugin: driver.getPlugin.bind(driver),
|
|
983
|
+
get files() {
|
|
984
|
+
return driver.fileManager.files
|
|
985
|
+
},
|
|
986
|
+
upsertFile: (...files) => driver.fileManager.upsert(...files),
|
|
987
|
+
})
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
for (const plugin of driver.plugins.values()) {
|
|
991
|
+
const context = driver.getContext(plugin)
|
|
992
|
+
const hrStart = process.hrtime()
|
|
993
|
+
|
|
994
|
+
try {
|
|
995
|
+
const timestamp = new Date()
|
|
996
|
+
|
|
997
|
+
await hooks.emit('kubb:plugin:start', { plugin })
|
|
998
|
+
await hooks.emit('kubb:debug', {
|
|
999
|
+
date: timestamp,
|
|
1000
|
+
logs: ['Starting plugin...', ` • Plugin Name: ${plugin.name}`],
|
|
1001
|
+
})
|
|
1002
|
+
|
|
1003
|
+
if (plugin.generators?.length || driver.hasRegisteredGenerators(plugin.name)) {
|
|
1004
|
+
await runPluginAstHooks(plugin, context)
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const duration = getElapsedMs(hrStart)
|
|
1008
|
+
pluginTimings.set(plugin.name, duration)
|
|
1009
|
+
|
|
1010
|
+
await hooks.emit('kubb:plugin:end', {
|
|
1011
|
+
plugin,
|
|
1012
|
+
duration,
|
|
1013
|
+
success: true,
|
|
1014
|
+
config,
|
|
1015
|
+
get files() {
|
|
1016
|
+
return driver.fileManager.files
|
|
1017
|
+
},
|
|
1018
|
+
upsertFile: (...files) => driver.fileManager.upsert(...files),
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
await hooks.emit('kubb:debug', {
|
|
1022
|
+
date: new Date(),
|
|
1023
|
+
logs: [`✓ Plugin started successfully (${formatMs(duration)})`],
|
|
1024
|
+
})
|
|
1025
|
+
} catch (caughtError) {
|
|
1026
|
+
const error = caughtError as Error
|
|
1027
|
+
const errorTimestamp = new Date()
|
|
1028
|
+
const duration = getElapsedMs(hrStart)
|
|
1029
|
+
|
|
1030
|
+
await hooks.emit('kubb:plugin:end', {
|
|
1031
|
+
plugin,
|
|
1032
|
+
duration,
|
|
1033
|
+
success: false,
|
|
1034
|
+
error,
|
|
1035
|
+
config,
|
|
1036
|
+
get files() {
|
|
1037
|
+
return driver.fileManager.files
|
|
1038
|
+
},
|
|
1039
|
+
upsertFile: (...files) => driver.fileManager.upsert(...files),
|
|
1040
|
+
})
|
|
1041
|
+
|
|
1042
|
+
await hooks.emit('kubb:debug', {
|
|
1043
|
+
date: errorTimestamp,
|
|
1044
|
+
logs: [
|
|
1045
|
+
'✗ Plugin start failed',
|
|
1046
|
+
` • Plugin Name: ${plugin.name}`,
|
|
1047
|
+
` • Error: ${error.constructor.name} - ${error.message}`,
|
|
1048
|
+
' • Stack Trace:',
|
|
1049
|
+
error.stack || 'No stack trace available',
|
|
1050
|
+
],
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
failedPlugins.add({ plugin, error })
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
await hooks.emit('kubb:plugins:end', {
|
|
1058
|
+
config,
|
|
1059
|
+
get files() {
|
|
1060
|
+
return driver.fileManager.files
|
|
1061
|
+
},
|
|
1062
|
+
upsertFile: (...files) => driver.fileManager.upsert(...files),
|
|
1063
|
+
})
|
|
1064
|
+
|
|
1065
|
+
const files = driver.fileManager.files
|
|
1066
|
+
|
|
1067
|
+
const parsersMap = new Map<FileNode['extname'], Parser>()
|
|
1068
|
+
for (const parser of config.parsers) {
|
|
1069
|
+
if (parser.extNames) {
|
|
1070
|
+
for (const extname of parser.extNames) {
|
|
1071
|
+
parsersMap.set(extname, parser)
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const fileProcessor = new FileProcessor()
|
|
1077
|
+
|
|
1078
|
+
await hooks.emit('kubb:debug', {
|
|
1079
|
+
date: new Date(),
|
|
1080
|
+
logs: [`Writing ${files.length} files...`],
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
await fileProcessor.run(files, {
|
|
1084
|
+
parsers: parsersMap,
|
|
1085
|
+
mode: 'parallel',
|
|
1086
|
+
extension: config.output.extension,
|
|
1087
|
+
onStart: async (processingFiles) => {
|
|
1088
|
+
await hooks.emit('kubb:files:processing:start', { files: processingFiles })
|
|
1089
|
+
},
|
|
1090
|
+
onUpdate: async ({ file, source, processed, total, percentage }) => {
|
|
1091
|
+
await hooks.emit('kubb:file:processing:update', {
|
|
1092
|
+
file,
|
|
1093
|
+
source,
|
|
1094
|
+
processed,
|
|
1095
|
+
total,
|
|
1096
|
+
percentage,
|
|
1097
|
+
config,
|
|
1098
|
+
})
|
|
1099
|
+
if (source) {
|
|
1100
|
+
await config.storage.setItem(file.path, source)
|
|
1101
|
+
|
|
1102
|
+
sources.set(file.path, source)
|
|
1103
|
+
}
|
|
1104
|
+
},
|
|
1105
|
+
onEnd: async (processedFiles) => {
|
|
1106
|
+
await hooks.emit('kubb:files:processing:end', { files: processedFiles })
|
|
1107
|
+
await hooks.emit('kubb:debug', {
|
|
1108
|
+
date: new Date(),
|
|
1109
|
+
logs: [`✓ File write process completed for ${processedFiles.length} files`],
|
|
1110
|
+
})
|
|
1111
|
+
},
|
|
1112
|
+
})
|
|
1113
|
+
|
|
1114
|
+
await hooks.emit('kubb:build:end', {
|
|
1115
|
+
files,
|
|
1116
|
+
config,
|
|
1117
|
+
outputDir: resolve(config.root, config.output.path),
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1120
|
+
return {
|
|
1121
|
+
failedPlugins,
|
|
1122
|
+
files,
|
|
1123
|
+
driver,
|
|
1124
|
+
pluginTimings,
|
|
1125
|
+
sources,
|
|
1126
|
+
}
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
return {
|
|
1129
|
+
failedPlugins,
|
|
1130
|
+
files: [],
|
|
1131
|
+
driver,
|
|
1132
|
+
pluginTimings,
|
|
1133
|
+
error: error as Error,
|
|
1134
|
+
sources,
|
|
1135
|
+
}
|
|
1136
|
+
} finally {
|
|
1137
|
+
driver.dispose()
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
async function build(setupResult: SetupResult): Promise<BuildOutput> {
|
|
1142
|
+
const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(setupResult)
|
|
1143
|
+
|
|
1144
|
+
if (error) {
|
|
1145
|
+
throw error
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (failedPlugins.size > 0) {
|
|
1149
|
+
const errors = [...failedPlugins].map(({ error }) => error)
|
|
1150
|
+
|
|
1151
|
+
throw new BuildError(`Build Error with ${failedPlugins.size} failed plugins`, { errors })
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
return {
|
|
1155
|
+
failedPlugins,
|
|
1156
|
+
files,
|
|
1157
|
+
driver,
|
|
1158
|
+
pluginTimings,
|
|
1159
|
+
error: undefined,
|
|
1160
|
+
sources,
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Returns a snapshot of the current runtime environment.
|
|
1166
|
+
*
|
|
1167
|
+
* Useful for attaching context to debug logs and error reports so that
|
|
1168
|
+
* issues can be reproduced without manual information gathering.
|
|
1169
|
+
*/
|
|
1170
|
+
export function getDiagnosticInfo() {
|
|
1171
|
+
return {
|
|
1172
|
+
nodeVersion,
|
|
1173
|
+
KubbVersion,
|
|
1174
|
+
platform: process.platform,
|
|
1175
|
+
arch: process.arch,
|
|
1176
|
+
cwd: process.cwd(),
|
|
1177
|
+
} as const
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Type guard to check if a given config has an `input.path`.
|
|
1182
|
+
*/
|
|
1183
|
+
export function isInputPath(config: UserConfig | undefined): config is UserConfig<InputPath> & { input: InputPath }
|
|
1184
|
+
export function isInputPath(config: Config | undefined): config is Config<InputPath> & { input: InputPath }
|
|
1185
|
+
export function isInputPath(config: Config | UserConfig | undefined): config is (Config<InputPath> | UserConfig<InputPath>) & { input: InputPath } {
|
|
1186
|
+
return typeof config?.input === 'object' && config.input !== null && 'path' in config.input
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function inputToAdapterSource(config: Config): AdapterSource {
|
|
1190
|
+
const input = config.input
|
|
1191
|
+
if (!input) {
|
|
1192
|
+
throw new Error('[kubb] input is required when using an adapter. Provide input.path or input.data in your config.')
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
if ('data' in input) {
|
|
1196
|
+
return { type: 'data', data: input.data }
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
if (new URLPath(input.path).isURL) {
|
|
1200
|
+
return { type: 'path', path: input.path }
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const resolved = resolve(config.root, input.path)
|
|
1204
|
+
|
|
1205
|
+
return { type: 'path', path: resolved }
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
type CreateKubbOptions = {
|
|
1209
|
+
hooks?: AsyncEventEmitter<KubbHooks>
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Creates a Kubb instance bound to a single config entry.
|
|
1214
|
+
*
|
|
1215
|
+
* Accepts a user-facing config shape and resolves it to a full {@link Config} during
|
|
1216
|
+
* `setup()`. The instance then holds shared state (`hooks`, `sources`, `driver`, `config`)
|
|
1217
|
+
* across the `setup → build` lifecycle. Attach event listeners to `kubb.hooks` before
|
|
1218
|
+
* calling `setup()` or `build()`.
|
|
1219
|
+
*
|
|
1220
|
+
* @example
|
|
1221
|
+
* ```ts
|
|
1222
|
+
* const kubb = createKubb(userConfig)
|
|
1223
|
+
*
|
|
1224
|
+
* kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => {
|
|
1225
|
+
* console.log(`${plugin.name} completed in ${duration}ms`)
|
|
1226
|
+
* })
|
|
1227
|
+
*
|
|
1228
|
+
* const { files, failedPlugins } = await kubb.safeBuild()
|
|
1229
|
+
* ```
|
|
1230
|
+
*/
|
|
1231
|
+
export function createKubb(userConfig: UserConfig, options: CreateKubbOptions = {}): Kubb {
|
|
1232
|
+
const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
|
|
1233
|
+
let setupResult: SetupResult | undefined
|
|
1234
|
+
|
|
1235
|
+
const instance: Kubb = {
|
|
1236
|
+
get hooks() {
|
|
1237
|
+
return hooks
|
|
1238
|
+
},
|
|
1239
|
+
get sources() {
|
|
1240
|
+
return setupResult?.sources ?? new Map()
|
|
1241
|
+
},
|
|
1242
|
+
get driver() {
|
|
1243
|
+
return setupResult?.driver
|
|
1244
|
+
},
|
|
1245
|
+
get config() {
|
|
1246
|
+
return setupResult?.config
|
|
1247
|
+
},
|
|
1248
|
+
async setup() {
|
|
1249
|
+
setupResult = await setup(userConfig, { hooks })
|
|
1250
|
+
},
|
|
1251
|
+
async build() {
|
|
1252
|
+
if (!setupResult) {
|
|
1253
|
+
await instance.setup()
|
|
1254
|
+
}
|
|
1255
|
+
return build(setupResult!)
|
|
1256
|
+
},
|
|
1257
|
+
async safeBuild() {
|
|
1258
|
+
if (!setupResult) {
|
|
1259
|
+
await instance.setup()
|
|
1260
|
+
}
|
|
1261
|
+
return safeBuild(setupResult!)
|
|
1262
|
+
},
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return instance
|
|
1266
|
+
}
|