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