@kubb/core 5.0.0-beta.1 → 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.
@@ -1,9 +1,97 @@
1
- import type { PossiblePromise } from '@internals/utils'
2
- import type { FileNode, OperationNode, SchemaNode } from '@kubb/ast'
1
+ import type { AsyncEventEmitter, PossiblePromise } from '@internals/utils'
2
+ import type { FileNode, InputNode, OperationNode, SchemaNode, Visitor } from '@kubb/ast'
3
+ import type { Adapter } from './createAdapter.ts'
3
4
  import type { RendererFactory } from './createRenderer.ts'
4
- import type { GeneratorContext, PluginFactoryOptions } from './types.ts'
5
+ import type { KubbHooks } from './types.ts'
6
+ import type { PluginDriver } from './PluginDriver.ts'
7
+ import type { Plugin, PluginFactoryOptions } from './definePlugin.ts'
8
+ import type { Resolver } from './defineResolver.ts'
9
+ import type { Config, DevtoolsOptions } from './types.ts'
5
10
 
6
- export type { GeneratorContext } from './types.ts'
11
+ /**
12
+ * Context object passed to generator `schema`, `operation`, and `operations` methods.
13
+ *
14
+ * The adapter is always defined (guaranteed by `runPluginAstHooks`) so no runtime checks
15
+ * are needed. `ctx.options` carries resolved per-node options after exclude/include/override
16
+ * filtering for individual schema/operation calls, or plugin-level options for operations.
17
+ */
18
+ export type GeneratorContext<TOptions extends PluginFactoryOptions = PluginFactoryOptions> = {
19
+ config: Config
20
+ /**
21
+ * Absolute path to the current plugin's output directory.
22
+ */
23
+ root: string
24
+ /**
25
+ * Determine output mode based on the output config.
26
+ * Returns `'single'` when `output.path` is a file, `'split'` for a directory.
27
+ */
28
+ getMode: (output: { path: string }) => 'single' | 'split'
29
+ driver: PluginDriver
30
+ /**
31
+ * Get a plugin by name, typed via `Kubb.PluginRegistry` when registered.
32
+ */
33
+ getPlugin<TName extends keyof Kubb.PluginRegistry>(name: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined
34
+ getPlugin(name: string): Plugin | undefined
35
+ /**
36
+ * Get a plugin by name, throws an error if not found.
37
+ */
38
+ requirePlugin<TName extends keyof Kubb.PluginRegistry>(name: TName): Plugin<Kubb.PluginRegistry[TName]>
39
+ requirePlugin(name: string): Plugin
40
+ /**
41
+ * Get a resolver by plugin name, typed via `Kubb.PluginRegistry` when registered.
42
+ */
43
+ getResolver<TName extends keyof Kubb.PluginRegistry>(name: TName): Kubb.PluginRegistry[TName]['resolver']
44
+ getResolver(name: string): Resolver
45
+ /**
46
+ * Add files only if they don't exist.
47
+ */
48
+ addFile: (...file: Array<FileNode>) => Promise<void>
49
+ /**
50
+ * Merge sources into the same output file.
51
+ */
52
+ upsertFile: (...file: Array<FileNode>) => Promise<void>
53
+ hooks: AsyncEventEmitter<KubbHooks>
54
+ /**
55
+ * The current plugin instance.
56
+ */
57
+ plugin: Plugin<TOptions>
58
+ /**
59
+ * The current plugin's resolver.
60
+ */
61
+ resolver: TOptions['resolver']
62
+ /**
63
+ * The current plugin's transformer.
64
+ */
65
+ transformer: Visitor | undefined
66
+ /**
67
+ * Emit a warning.
68
+ */
69
+ warn: (message: string) => void
70
+ /**
71
+ * Emit an error.
72
+ */
73
+ error: (error: string | Error) => void
74
+ /**
75
+ * Emit an info message.
76
+ */
77
+ info: (message: string) => void
78
+ /**
79
+ * Open the current input node in Kubb Studio.
80
+ */
81
+ openInStudio: (options?: DevtoolsOptions) => Promise<void>
82
+ /**
83
+ * The configured adapter instance.
84
+ */
85
+ adapter: Adapter
86
+ /**
87
+ * The universal `InputNode` produced by the adapter.
88
+ */
89
+ inputNode: InputNode
90
+ /**
91
+ * Resolved options after exclude/include/override filtering.
92
+ */
93
+ options: TOptions['resolvedOptions']
94
+ }
7
95
 
8
96
  /**
9
97
  * Declares a named generator unit that walks the AST and emits files.
@@ -1,9 +1,35 @@
1
- import type { Logger, LoggerOptions, UserLogger } from './types.ts'
1
+ import type { AsyncEventEmitter } from '@internals/utils'
2
+ import type { logLevel } from './constants.ts'
3
+ import type { KubbHooks } from './types.ts'
4
+
5
+ export type LoggerOptions = {
6
+ /**
7
+ * Log level for output verbosity.
8
+ * @default 3
9
+ */
10
+ logLevel: (typeof logLevel)[keyof typeof logLevel]
11
+ }
12
+
13
+ /**
14
+ * Shared context passed to plugins, parsers, and other internals.
15
+ */
16
+ export type LoggerContext = AsyncEventEmitter<KubbHooks>
17
+
18
+ export type Logger<TOptions extends LoggerOptions = LoggerOptions, TInstallReturn = void> = {
19
+ name: string
20
+ install: (context: LoggerContext, options?: TOptions) => TInstallReturn | Promise<TInstallReturn>
21
+ }
22
+
23
+ export type UserLogger<TOptions extends LoggerOptions = LoggerOptions, TInstallReturn = void> = Logger<TOptions, TInstallReturn>
2
24
 
3
25
  /**
4
26
  * Wraps a logger definition into a typed {@link Logger}.
5
27
  *
6
- * @example
28
+ * The optional second type parameter `TInstallReturn` allows loggers to return
29
+ * a value from `install` — for example, a sink factory that the caller can
30
+ * forward to hook execution.
31
+ *
32
+ * @example Basic logger
7
33
  * ```ts
8
34
  * export const myLogger = defineLogger({
9
35
  * name: 'my-logger',
@@ -13,7 +39,20 @@ import type { Logger, LoggerOptions, UserLogger } from './types.ts'
13
39
  * },
14
40
  * })
15
41
  * ```
42
+ *
43
+ * @example Logger that returns a hook sink factory
44
+ * ```ts
45
+ * export const myLogger = defineLogger<LoggerOptions, HookSinkFactory>({
46
+ * name: 'my-logger',
47
+ * install(context, options) {
48
+ * // … register event handlers …
49
+ * return (commandWithArgs) => ({ onStdout: console.log })
50
+ * },
51
+ * })
52
+ * ```
16
53
  */
17
- export function defineLogger<Options extends LoggerOptions = LoggerOptions>(logger: UserLogger<Options>): Logger<Options> {
54
+ export function defineLogger<Options extends LoggerOptions = LoggerOptions, TInstallReturn = void>(
55
+ logger: UserLogger<Options, TInstallReturn>,
56
+ ): Logger<Options, TInstallReturn> {
18
57
  return logger
19
58
  }
@@ -1,4 +1,4 @@
1
- import type { KubbHooks } from './Kubb.ts'
1
+ import type { KubbHooks } from './types.ts'
2
2
 
3
3
  /**
4
4
  * A middleware instance produced by calling a factory created with `defineMiddleware`.
@@ -1,5 +1,260 @@
1
- import type { KubbHooks } from './Kubb.ts'
2
- import type { KubbPluginSetupContext, PluginFactoryOptions } from './types.ts'
1
+ import { extname } from 'node:path'
2
+ import type { FileNode, HttpMethod, InputNode, UserFileNode, Visitor } from '@kubb/ast'
3
+ import type { RendererFactory } from './createRenderer.ts'
4
+ import type { Generator } from './defineGenerator.ts'
5
+ import type { Resolver } from './defineResolver.ts'
6
+ import type { Config, KubbHooks } from './types.ts'
7
+
8
+ /**
9
+ * Safely extracts a type from a registry, returning `{}` if the key doesn't exist.
10
+ * Enables optional interface augmentation for `Kubb.ConfigOptionsRegistry` and `Kubb.PluginOptionsRegistry`
11
+ * without requiring changes to core.
12
+ *
13
+ * @internal
14
+ */
15
+ type ExtractRegistryKey<T, K extends PropertyKey> = K extends keyof T ? T[K] : {}
16
+
17
+ /**
18
+ * Output configuration for generated files.
19
+ */
20
+ export type Output<_TOptions = unknown> = {
21
+ /**
22
+ * Output folder or file path for generated code.
23
+ */
24
+ path: string
25
+ /**
26
+ * Text or function prepended to every generated file.
27
+ * When a function, receives the current `InputNode` and returns a string.
28
+ */
29
+ banner?: string | ((node?: InputNode) => string)
30
+ /**
31
+ * Text or function appended to every generated file.
32
+ * When a function, receives the current `InputNode` and returns a string.
33
+ */
34
+ footer?: string | ((node?: InputNode) => string)
35
+ /**
36
+ * Whether to override existing external files if they already exist.
37
+ * @default false
38
+ */
39
+ override?: boolean
40
+ } & ExtractRegistryKey<Kubb.PluginOptionsRegistry, 'output'>
41
+
42
+ export type Group = {
43
+ /**
44
+ * How to group files into subdirectories.
45
+ * - `'tag'` — group by OpenAPI tags
46
+ * - `'path'` — group by OpenAPI paths
47
+ */
48
+ type: 'tag' | 'path'
49
+ /**
50
+ * Function that returns the subdirectory name for a group value.
51
+ * Defaults to `${camelCase(group)}Controller` for tags, first path segment for paths.
52
+ */
53
+ name?: (context: { group: string }) => string
54
+ }
55
+
56
+ type ByTag = {
57
+ /**
58
+ * Filter by OpenAPI `tags` field. Matches one or more tags assigned to operations.
59
+ */
60
+ type: 'tag'
61
+ /**
62
+ * Tag name to match (case-sensitive). Can be a literal string or regex pattern.
63
+ */
64
+ pattern: string | RegExp
65
+ }
66
+
67
+ type ByOperationId = {
68
+ /**
69
+ * Filter by OpenAPI `operationId` field. Each operation (GET, POST, etc.) has a unique identifier.
70
+ */
71
+ type: 'operationId'
72
+ /**
73
+ * Operation ID to match (case-sensitive). Can be a literal string or regex pattern.
74
+ */
75
+ pattern: string | RegExp
76
+ }
77
+
78
+ type ByPath = {
79
+ /**
80
+ * Filter by OpenAPI `path` (URL endpoint). Useful to group or filter by service segments like `/pets`, `/users`, etc.
81
+ */
82
+ type: 'path'
83
+ /**
84
+ * URL path to match (case-sensitive). Can be a literal string or regex pattern. Matches against the full path.
85
+ */
86
+ pattern: string | RegExp
87
+ }
88
+
89
+ type ByMethod = {
90
+ /**
91
+ * Filter by HTTP method: `'get'`, `'post'`, `'put'`, `'delete'`, `'patch'`, `'head'`, `'options'`.
92
+ */
93
+ type: 'method'
94
+ /**
95
+ * HTTP method to match (case-insensitive when using string, or regex for dynamic matching).
96
+ */
97
+ pattern: HttpMethod | RegExp
98
+ }
99
+ // TODO implement as alternative for ByMethod
100
+ // type ByMethods = {
101
+ // type: 'methods'
102
+ // pattern: Array<HttpMethod>
103
+ // }
104
+
105
+ type BySchemaName = {
106
+ /**
107
+ * Filter by schema component name (TypeScript or JSON schema). Matches schemas in `#/components/schemas`.
108
+ */
109
+ type: 'schemaName'
110
+ /**
111
+ * Schema name to match (case-sensitive). Can be a literal string or regex pattern.
112
+ */
113
+ pattern: string | RegExp
114
+ }
115
+
116
+ type ByContentType = {
117
+ /**
118
+ * Filter by response or request content type: `'application/json'`, `'application/xml'`, etc.
119
+ */
120
+ type: 'contentType'
121
+ /**
122
+ * Content type to match (case-sensitive). Can be a literal string or regex pattern.
123
+ */
124
+ pattern: string | RegExp
125
+ }
126
+
127
+ /**
128
+ * A pattern filter that prevents matching nodes from being generated.
129
+ *
130
+ * Use to skip code generation for specific operations or schemas. For example, exclude deprecated endpoints
131
+ * or internal-only schemas. Can filter by tag, operationId, path, HTTP method, content type, or schema name.
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * exclude: [
136
+ * { type: 'tag', pattern: 'internal' }, // skip "internal" tag
137
+ * { type: 'path', pattern: /^\/admin/ }, // skip all /admin endpoints
138
+ * { type: 'operationId', pattern: 'deprecated_*' } // skip operationIds matching pattern
139
+ * ]
140
+ * ```
141
+ */
142
+ export type Exclude = ByTag | ByOperationId | ByPath | ByMethod | ByContentType | BySchemaName
143
+
144
+ /**
145
+ * A pattern filter that restricts generation to only matching nodes.
146
+ *
147
+ * Use to generate code for a subset of operations or schemas. For example, only generate for a specific service
148
+ * tag or only for "production" endpoints. Can filter by tag, operationId, path, HTTP method, content type, or schema name.
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * include: [
153
+ * { type: 'tag', pattern: 'public' }, // generate only "public" tag
154
+ * { type: 'path', pattern: /^\/api\/v1/ }, // generate only v1 endpoints
155
+ * ]
156
+ * ```
157
+ */
158
+ export type Include = ByTag | ByOperationId | ByPath | ByMethod | ByContentType | BySchemaName
159
+
160
+ /**
161
+ * A pattern filter paired with partial option overrides applied when the pattern matches.
162
+ *
163
+ * Use to customize generation for specific operations or schemas. For example, apply different output paths
164
+ * for different tags, or use custom resolver functions per operation. Can filter by tag, operationId, path,
165
+ * HTTP method, schema name, or content type.
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * override: [
170
+ * {
171
+ * type: 'tag',
172
+ * pattern: 'admin',
173
+ * options: { output: { path: './src/gen/admin' } } // admin APIs go to separate folder
174
+ * },
175
+ * {
176
+ * type: 'operationId',
177
+ * pattern: 'listPets',
178
+ * options: { exclude: true } // skip this specific operation
179
+ * }
180
+ * ]
181
+ * ```
182
+ */
183
+ export type Override<TOptions> = (ByTag | ByOperationId | ByPath | ByMethod | BySchemaName | ByContentType) & {
184
+ //TODO should be options: Omit<Partial<TOptions>, 'override'>
185
+ options: Partial<TOptions>
186
+ }
187
+
188
+ export type PluginFactoryOptions<
189
+ /**
190
+ * Unique plugin name.
191
+ */
192
+ TName extends string = string,
193
+ /**
194
+ * User-facing plugin options.
195
+ */
196
+ TOptions extends object = object,
197
+ /**
198
+ * Plugin options after defaults are applied.
199
+ */
200
+ TResolvedOptions extends object = TOptions,
201
+ /**
202
+ * Resolver that encapsulates naming and path-resolution helpers.
203
+ * Define with `defineResolver` and export alongside the plugin.
204
+ */
205
+ TResolver extends Resolver = Resolver,
206
+ > = {
207
+ name: TName
208
+ options: TOptions
209
+ resolvedOptions: TResolvedOptions
210
+ resolver: TResolver
211
+ }
212
+
213
+ /**
214
+ * Context for hook-style plugin `kubb:plugin:setup` handler.
215
+ * Provides methods to register generators, configure resolvers, transformers, and renderers.
216
+ */
217
+ export type KubbPluginSetupContext<TFactory extends PluginFactoryOptions = PluginFactoryOptions> = {
218
+ /**
219
+ * Register a generator dynamically. Generators fire during the AST walk (schema/operation/operations)
220
+ * just like generators declared statically on `createPlugin`.
221
+ */
222
+ addGenerator<TElement = unknown>(generator: Generator<TFactory, TElement>): void
223
+ /**
224
+ * Set or override the resolver for this plugin.
225
+ * The resolver controls file naming and path resolution.
226
+ */
227
+ setResolver(resolver: Partial<TFactory['resolver']>): void
228
+ /**
229
+ * Set the AST transformer to pre-process nodes before they reach generators.
230
+ */
231
+ setTransformer(visitor: Visitor): void
232
+ /**
233
+ * Set the renderer factory to process JSX elements from generators.
234
+ */
235
+ setRenderer(renderer: RendererFactory): void
236
+ /**
237
+ * Set resolved options merged into the normalized plugin's `options`.
238
+ * Call this in `kubb:plugin:setup` to provide options generators need.
239
+ */
240
+ setOptions(options: TFactory['resolvedOptions']): void
241
+ /**
242
+ * Inject a raw file into the build output, bypassing the generation pipeline.
243
+ */
244
+ injectFile(userFileNode: UserFileNode): void
245
+ /**
246
+ * Merge a partial config update into the current build configuration.
247
+ */
248
+ updateConfig(config: Partial<Config>): void
249
+ /**
250
+ * The resolved build configuration at setup time.
251
+ */
252
+ config: Config
253
+ /**
254
+ * The plugin's user-provided options.
255
+ */
256
+ options: TFactory['options']
257
+ }
3
258
 
4
259
  /**
5
260
  * A plugin object produced by `definePlugin`.
@@ -37,20 +292,52 @@ export type Plugin<TFactory extends PluginFactoryOptions = PluginFactoryOptions>
37
292
  * Any event from the global `KubbHooks` map can be subscribed to here.
38
293
  */
39
294
  hooks: {
40
- [K in Exclude<keyof KubbHooks, 'kubb:plugin:setup'>]?: (...args: KubbHooks[K]) => void | Promise<void>
295
+ [K in keyof KubbHooks as K extends 'kubb:plugin:setup' ? never : K]?: (...args: KubbHooks[K]) => void | Promise<void>
41
296
  } & {
42
297
  'kubb:plugin:setup'?(ctx: KubbPluginSetupContext<TFactory>): void | Promise<void>
43
298
  }
44
299
  }
45
300
 
46
301
  /**
47
- * Returns `true` when `plugin` is a hook-style plugin created with `definePlugin`.
302
+ * Normalized plugin after setup, with runtime fields populated.
303
+ * For internal use only — plugins use the public `Plugin` type externally.
48
304
  *
49
- * Used by `PluginDriver` to distinguish hook-style plugins from legacy `createPlugin` plugins
50
- * so it can normalize them and register their handlers on the `AsyncEventEmitter`.
305
+ * @internal
51
306
  */
52
- export function isPlugin(plugin: unknown): plugin is Plugin {
53
- return typeof plugin === 'object' && plugin !== null && 'hooks' in plugin
307
+ export type NormalizedPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions> = Plugin<TOptions> & {
308
+ options: TOptions['resolvedOptions'] & {
309
+ output: Output
310
+ include?: Array<Include>
311
+ exclude: Array<Exclude>
312
+ override: Array<Override<TOptions['resolvedOptions']>>
313
+ }
314
+ resolver: TOptions['resolver']
315
+ transformer?: Visitor
316
+ renderer?: RendererFactory
317
+ generators?: Array<Generator>
318
+ apply?: (config: Config) => boolean
319
+ version?: string
320
+ }
321
+
322
+ export type KubbPluginStartContext = {
323
+ plugin: NormalizedPlugin
324
+ }
325
+
326
+ export type KubbPluginEndContext = {
327
+ plugin: NormalizedPlugin
328
+ duration: number
329
+ success: boolean
330
+ error?: Error
331
+ config: Config
332
+ /**
333
+ * Returns all files currently in the file manager (lazy snapshot).
334
+ * Includes files added by plugins that have already run.
335
+ */
336
+ readonly files: ReadonlyArray<FileNode>
337
+ /**
338
+ * Upsert one or more files into the file manager.
339
+ */
340
+ upsertFile: (...files: Array<FileNode>) => void
54
341
  }
55
342
 
56
343
  /**
@@ -81,3 +368,12 @@ export function definePlugin<TFactory extends PluginFactoryOptions = PluginFacto
81
368
  ): (options?: TFactory['options']) => Plugin<TFactory> {
82
369
  return (options) => factory(options ?? ({} as TFactory['options']))
83
370
  }
371
+
372
+ /**
373
+ * Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise.
374
+ * Used to determine whether an output path targets a single file or a directory.
375
+ */
376
+ export function getMode(fileOrFolder: string | undefined | null): 'single' | 'split' {
377
+ if (!fileOrFolder) return 'split'
378
+ return extname(fileOrFolder) ? 'single' : 'split'
379
+ }