@kubb/core 5.0.0-alpha.34 → 5.0.0-alpha.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/PluginDriver-B_65W4fv.js +1677 -0
  2. package/dist/PluginDriver-B_65W4fv.js.map +1 -0
  3. package/dist/{PluginDriver-BBi_41VF.d.ts → PluginDriver-C9iBgYbk.d.ts} +743 -376
  4. package/dist/PluginDriver-CCdkwR14.cjs +1806 -0
  5. package/dist/PluginDriver-CCdkwR14.cjs.map +1 -0
  6. package/dist/hooks.d.ts +1 -1
  7. package/dist/index.cjs +272 -1666
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +62 -141
  10. package/dist/index.js +231 -1623
  11. package/dist/index.js.map +1 -1
  12. package/dist/mocks.cjs +165 -0
  13. package/dist/mocks.cjs.map +1 -0
  14. package/dist/mocks.d.ts +74 -0
  15. package/dist/mocks.js +159 -0
  16. package/dist/mocks.js.map +1 -0
  17. package/package.json +11 -5
  18. package/src/FileManager.ts +1 -1
  19. package/src/FileProcessor.ts +1 -1
  20. package/src/Kubb.ts +145 -38
  21. package/src/PluginDriver.ts +318 -40
  22. package/src/constants.ts +1 -1
  23. package/src/{build.ts → createKubb.ts} +180 -122
  24. package/src/createPlugin.ts +1 -0
  25. package/src/createRenderer.ts +57 -0
  26. package/src/defineGenerator.ts +57 -84
  27. package/src/defineLogger.ts +2 -2
  28. package/src/defineParser.ts +3 -2
  29. package/src/definePlugin.ts +95 -0
  30. package/src/defineResolver.ts +1 -1
  31. package/src/devtools.ts +1 -1
  32. package/src/index.ts +7 -6
  33. package/src/mocks.ts +234 -0
  34. package/src/renderNode.ts +35 -0
  35. package/src/types.ts +275 -210
  36. package/src/utils/TreeNode.ts +1 -1
  37. package/src/utils/getBarrelFiles.ts +3 -3
  38. package/src/utils/getFunctionParams.ts +14 -7
  39. package/src/utils/isInputPath.ts +2 -2
  40. package/src/utils/packageJSON.ts +2 -3
  41. package/src/defineConfig.ts +0 -51
  42. package/src/definePresets.ts +0 -16
  43. package/src/renderNode.tsx +0 -28
  44. package/src/utils/getConfigs.ts +0 -16
  45. package/src/utils/getPreset.ts +0 -78
@@ -1,121 +1,94 @@
1
1
  import type { PossiblePromise } from '@internals/utils'
2
- import type { FileNode, OperationNode, SchemaNode } from '@kubb/ast/types'
3
- import type { KubbReactNode } from '@kubb/renderer-jsx/types'
4
- import { applyHookResult } from './renderNode.tsx'
2
+ import type { FileNode, OperationNode, SchemaNode } from '@kubb/ast'
3
+ import type { RendererFactory } from './createRenderer.ts'
5
4
  import type { GeneratorContext, PluginFactoryOptions } from './types.ts'
6
5
 
7
6
  export type { GeneratorContext } from './types.ts'
8
7
 
9
8
  /**
10
9
  * A generator is a named object with optional `schema`, `operation`, and `operations`
11
- * methods. Each method is called with `this = PluginContext` of the parent plugin,
12
- * giving full access to `this.config`, `this.resolver`, `this.adapter`,
13
- * `this.driver`, etc.
10
+ * methods. Each method receives the AST node as the first argument and a typed
11
+ * `ctx` object as the second, giving access to `ctx.config`, `ctx.resolver`,
12
+ * `ctx.adapter`, `ctx.options`, `ctx.upsertFile`, etc.
14
13
  *
15
- * Return a React element, an array of `FileNode`, or `void` to handle file
16
- * writing manually via `this.upsertFile`. Both React and core (non-React) generators
17
- * use the same method signatures — the return type determines how output is handled.
14
+ * Generators that return renderer elements (e.g. JSX) must declare a `renderer`
15
+ * factory so that core knows how to process the output without coupling core
16
+ * to any specific renderer package.
17
+ *
18
+ * Return a renderer element, an array of `FileNode`, or `void` to handle file
19
+ * writing manually via `ctx.upsertFile`.
18
20
  *
19
21
  * @example
20
22
  * ```ts
23
+ * import { jsxRenderer } from '@kubb/renderer-jsx'
24
+ *
21
25
  * export const typeGenerator = defineGenerator<PluginTs>({
22
26
  * name: 'typescript',
23
- * schema(node, options) {
24
- * const { adapter, resolver, root } = this
27
+ * renderer: jsxRenderer,
28
+ * schema(node, ctx) {
29
+ * const { adapter, resolver, root, options } = ctx
25
30
  * return <File ...><Type node={node} resolver={resolver} /></File>
26
31
  * },
27
- * operation(node, options) {
32
+ * operation(node, ctx) {
33
+ * const { options } = ctx
28
34
  * return <File ...><OperationType node={node} /></File>
29
35
  * },
30
36
  * })
31
37
  * ```
32
38
  */
33
- export type Generator<TOptions extends PluginFactoryOptions = PluginFactoryOptions> = {
34
- /** Used in diagnostic messages and debug output. */
39
+ export type Generator<TOptions extends PluginFactoryOptions = PluginFactoryOptions, TElement = unknown> = {
40
+ /**
41
+ * Used in diagnostic messages and debug output.
42
+ */
35
43
  name: string
44
+ /**
45
+ * Optional renderer factory that produces a {@link Renderer} for each render cycle.
46
+ *
47
+ * Generators that return renderer elements (e.g. JSX via `@kubb/renderer-jsx`) must set this
48
+ * to the matching renderer factory (e.g. `jsxRenderer` from `@kubb/renderer-jsx`).
49
+ *
50
+ * Generators that only return `Array<FileNode>` or `void` do not need to set this.
51
+ *
52
+ * Set `renderer: null` to explicitly opt out of rendering even when the parent plugin
53
+ * declares a `renderer` (overrides the plugin-level fallback).
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * import { jsxRenderer } from '@kubb/renderer-jsx'
58
+ * export const myGenerator = defineGenerator<PluginTs>({
59
+ * renderer: jsxRenderer,
60
+ * schema(node, ctx) { return <File ...>...</File> },
61
+ * })
62
+ * ```
63
+ */
64
+ renderer?: RendererFactory<TElement> | null
36
65
  /**
37
66
  * Called for each schema node in the AST walk.
38
- * `this` is the parent plugin's context with `adapter` and `inputNode` guaranteed present.
39
- * `options` contains the per-node resolved options (after exclude/include/override).
67
+ * `ctx` carries the plugin context with `adapter` and `inputNode` guaranteed present,
68
+ * plus `ctx.options` with the per-node resolved options (after exclude/include/override).
40
69
  */
41
- schema?: (this: GeneratorContext<TOptions>, node: SchemaNode, options: TOptions['resolvedOptions']) => PossiblePromise<KubbReactNode | Array<FileNode> | void>
70
+ schema?: (node: SchemaNode, ctx: GeneratorContext<TOptions>) => PossiblePromise<TElement | Array<FileNode> | void>
42
71
  /**
43
72
  * Called for each operation node in the AST walk.
44
- * `this` is the parent plugin's context with `adapter` and `inputNode` guaranteed present.
73
+ * `ctx` carries the plugin context with `adapter` and `inputNode` guaranteed present,
74
+ * plus `ctx.options` with the per-node resolved options (after exclude/include/override).
45
75
  */
46
- operation?: (
47
- this: GeneratorContext<TOptions>,
48
- node: OperationNode,
49
- options: TOptions['resolvedOptions'],
50
- ) => PossiblePromise<KubbReactNode | Array<FileNode> | void>
76
+ operation?: (node: OperationNode, ctx: GeneratorContext<TOptions>) => PossiblePromise<TElement | Array<FileNode> | void>
51
77
  /**
52
78
  * Called once after all operations have been walked.
53
- * `this` is the parent plugin's context with `adapter` and `inputNode` guaranteed present.
79
+ * `ctx` carries the plugin context with `adapter` and `inputNode` guaranteed present,
80
+ * plus `ctx.options` with the plugin-level options for the batch call.
54
81
  */
55
- operations?: (
56
- this: GeneratorContext<TOptions>,
57
- nodes: Array<OperationNode>,
58
- options: TOptions['resolvedOptions'],
59
- ) => PossiblePromise<KubbReactNode | Array<FileNode> | void>
82
+ operations?: (nodes: Array<OperationNode>, ctx: GeneratorContext<TOptions>) => PossiblePromise<TElement | Array<FileNode> | void>
60
83
  }
61
84
 
62
85
  /**
63
86
  * Defines a generator. Returns the object as-is with correct `this` typings.
64
- * No type discrimination (`type: 'react' | 'core'`) needed — `applyHookResult`
65
- * handles React elements and `File[]` uniformly.
87
+ * `applyHookResult` handles renderer elements and `File[]` uniformly using
88
+ * the generator's declared `renderer` factory.
66
89
  */
67
- export function defineGenerator<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(generator: Generator<TOptions>): Generator<TOptions> {
90
+ export function defineGenerator<TOptions extends PluginFactoryOptions = PluginFactoryOptions, TElement = unknown>(
91
+ generator: Generator<TOptions, TElement>,
92
+ ): Generator<TOptions, TElement> {
68
93
  return generator
69
94
  }
70
-
71
- /**
72
- * Merges an array of generators into a single generator.
73
- *
74
- * The merged generator's `schema`, `operation`, and `operations` methods run
75
- * the corresponding method from each input generator in sequence, applying each
76
- * result via `applyHookResult`. This eliminates the need to write the loop
77
- * manually in each plugin.
78
- *
79
- * @param generators - Array of generators to merge into a single generator.
80
- *
81
- * @example
82
- * ```ts
83
- * const merged = mergeGenerators(generators)
84
- *
85
- * return {
86
- * name: pluginName,
87
- * schema: merged.schema,
88
- * operation: merged.operation,
89
- * operations: merged.operations,
90
- * }
91
- * ```
92
- */
93
- export function mergeGenerators<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(generators: Array<Generator<TOptions>>): Generator<TOptions> {
94
- return {
95
- name: generators.length > 0 ? generators.map((g) => g.name).join('+') : 'merged',
96
- async schema(node, options) {
97
- for (const gen of generators) {
98
- if (!gen.schema) continue
99
- const result = await gen.schema.call(this, node, options)
100
-
101
- await applyHookResult(result, this.driver)
102
- }
103
- },
104
- async operation(node, options) {
105
- for (const gen of generators) {
106
- if (!gen.operation) continue
107
- const result = await gen.operation.call(this, node, options)
108
-
109
- await applyHookResult(result, this.driver)
110
- }
111
- },
112
- async operations(nodes, options) {
113
- for (const gen of generators) {
114
- if (!gen.operations) continue
115
- const result = await gen.operations.call(this, nodes, options)
116
-
117
- await applyHookResult(result, this.driver)
118
- }
119
- },
120
- }
121
- }
@@ -7,8 +7,8 @@ import type { Logger, LoggerOptions, UserLogger } from './types.ts'
7
7
  * export const myLogger = defineLogger({
8
8
  * name: 'my-logger',
9
9
  * install(context, options) {
10
- * context.on('info', (message) => console.log('ℹ', message))
11
- * context.on('error', (error) => console.error('✗', error.message))
10
+ * context.on('kubb:info', (message) => console.log('ℹ', message))
11
+ * context.on('kubb:error', (error) => console.error('✗', error.message))
12
12
  * },
13
13
  * })
14
14
  */
@@ -1,4 +1,4 @@
1
- import type { FileNode } from '@kubb/ast/types'
1
+ import type { FileNode } from '@kubb/ast'
2
2
 
3
3
  type PrintOptions = {
4
4
  extname?: FileNode['extname']
@@ -10,7 +10,8 @@ export type Parser<TMeta extends object = any> = {
10
10
  * File extensions this parser handles.
11
11
  * Use `undefined` to create a catch-all fallback parser.
12
12
  *
13
- * @example ['.ts', '.js']
13
+ * @example Handled extensions
14
+ * `['.ts', '.js']`
14
15
  */
15
16
  extNames: Array<FileNode['extname']> | undefined
16
17
  /**
@@ -0,0 +1,95 @@
1
+ import type { KubbHooks } from './Kubb.ts'
2
+ import type { KubbPluginSetupContext, PluginFactoryOptions } from './types.ts'
3
+
4
+ /**
5
+ * Base hook handlers for all events except `kubb:plugin:setup`.
6
+ * These handlers have identical signatures regardless of the plugin's
7
+ * `PluginFactoryOptions` generic — they are split out so that the
8
+ * interface below only needs to override the one event that depends on
9
+ * the plugin type.
10
+ */
11
+ type PluginHooksBase = {
12
+ [K in Exclude<keyof KubbHooks, 'kubb:plugin:setup'>]?: (...args: KubbHooks[K]) => void | Promise<void>
13
+ }
14
+
15
+ /**
16
+ * Plugin hook handlers.
17
+ *
18
+ * `kubb:plugin:setup` is typed with the plugin's own `PluginFactoryOptions` so
19
+ * `ctx.setResolver`, `ctx.setOptions`, `ctx.options` etc. use the correct types.
20
+ *
21
+ * Uses interface + method shorthand for `kubb:plugin:setup`
22
+ * checking, allowing `PluginHooks<PluginTs>` to be assignable to `PluginHooks`.
23
+ *
24
+ * @template TFactory - The plugin's `PluginFactoryOptions` type.
25
+ */
26
+ export interface PluginHooks<TFactory extends PluginFactoryOptions = PluginFactoryOptions> extends PluginHooksBase {
27
+ 'kubb:plugin:setup'?(ctx: KubbPluginSetupContext<TFactory>): void | Promise<void>
28
+ }
29
+
30
+ /**
31
+ * A hook-style plugin object produced by `definePlugin`.
32
+ * Instead of flat lifecycle methods, it groups all handlers under a `hooks:` property
33
+ * (matching Astro's integration naming convention).
34
+ *
35
+ * @template TFactory - The plugin's `PluginFactoryOptions` type.
36
+ */
37
+ export type HookStylePlugin<TFactory extends PluginFactoryOptions = PluginFactoryOptions> = {
38
+ /**
39
+ * Unique name for the plugin, following the same naming convention as `createPlugin`.
40
+ */
41
+ name: string
42
+ /**
43
+ * Plugins that must be registered before this plugin executes.
44
+ * An error is thrown at startup when any listed dependency is missing.
45
+ */
46
+ dependencies?: Array<string>
47
+ /**
48
+ * The options passed by the user when calling the plugin factory.
49
+ */
50
+ options?: TFactory['options']
51
+ /**
52
+ * Lifecycle event handlers for this plugin.
53
+ * Any event from the global `KubbHooks` map can be subscribed to here.
54
+ */
55
+ hooks: PluginHooks<TFactory>
56
+ }
57
+
58
+ /**
59
+ * Returns `true` when `plugin` is a hook-style plugin created with `definePlugin`.
60
+ *
61
+ * Used by `PluginDriver` to distinguish hook-style plugins from legacy `createPlugin` plugins
62
+ * so it can normalize them and register their handlers on the `AsyncEventEmitter`.
63
+ */
64
+ export function isHookStylePlugin(plugin: unknown): plugin is HookStylePlugin {
65
+ return typeof plugin === 'object' && plugin !== null && 'hooks' in plugin
66
+ }
67
+
68
+ /**
69
+ * Creates a plugin factory using the new hook-style (`hooks:`) API.
70
+ *
71
+ * The returned factory is called with optional options and produces a `HookStylePlugin`
72
+ * that coexists with plugins created via the legacy `createPlugin` API in the same
73
+ * `kubb.config.ts`.
74
+ *
75
+ * Lifecycle handlers are registered on the `PluginDriver`'s `AsyncEventEmitter`, enabling
76
+ * both the plugin's own handlers and external tooling (CLI, devtools) to observe every event.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * // With PluginFactoryOptions (recommended for real plugins)
81
+ * export const pluginTs = definePlugin<PluginTs>((options) => ({
82
+ * name: 'plugin-ts',
83
+ * hooks: {
84
+ * 'kubb:plugin:setup'(ctx) {
85
+ * ctx.setResolver(resolverTs) // typed as Partial<ResolverTs>
86
+ * },
87
+ * },
88
+ * }))
89
+ * ```
90
+ */
91
+ export function definePlugin<TFactory extends PluginFactoryOptions = PluginFactoryOptions>(
92
+ factory: (options: TFactory['options']) => HookStylePlugin<TFactory>,
93
+ ): (options?: TFactory['options']) => HookStylePlugin<TFactory> {
94
+ return (options) => factory(options ?? ({} as TFactory['options']))
95
+ }
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path'
2
2
  import { camelCase, pascalCase } from '@internals/utils'
3
+ import type { FileNode, InputNode, Node, OperationNode, SchemaNode } from '@kubb/ast'
3
4
  import { createFile, isOperationNode, isSchemaNode } from '@kubb/ast'
4
- import type { FileNode, InputNode, Node, OperationNode, SchemaNode } from '@kubb/ast/types'
5
5
  import { getMode } from './PluginDriver.ts'
6
6
  import type {
7
7
  Config,
package/src/devtools.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { InputNode } from '@kubb/ast/types'
1
+ import type { InputNode } from '@kubb/ast'
2
2
  import { deflateSync, inflateSync } from 'fflate'
3
3
  import { x } from 'tinyexec'
4
4
  import type { DevtoolsOptions } from './types.ts'
package/src/index.ts CHANGED
@@ -1,15 +1,16 @@
1
1
  export { AsyncEventEmitter, URLPath } from '@internals/utils'
2
+ export * as ast from '@kubb/ast'
2
3
  export { composeTransformers, definePrinter } from '@kubb/ast'
3
- export { build, build as default, safeBuild, setup } from './build.ts'
4
4
  export { formatters, linters, logLevel } from './constants.ts'
5
5
  export { createAdapter } from './createAdapter.ts'
6
+ export { createKubb } from './createKubb.ts'
6
7
  export { createPlugin } from './createPlugin.ts'
8
+ export { createRenderer } from './createRenderer.ts'
7
9
  export { createStorage } from './createStorage.ts'
8
- export { defineConfig } from './defineConfig.ts'
9
- export { defineGenerator, mergeGenerators } from './defineGenerator.ts'
10
+ export { defineGenerator } from './defineGenerator.ts'
10
11
  export { defineLogger } from './defineLogger.ts'
11
12
  export { defineParser } from './defineParser.ts'
12
- export { definePresets } from './definePresets.ts'
13
+ export { definePlugin } from './definePlugin.ts'
13
14
  export {
14
15
  buildDefaultBanner,
15
16
  defaultResolveBanner,
@@ -19,6 +20,8 @@ export {
19
20
  defaultResolvePath,
20
21
  defineResolver,
21
22
  } from './defineResolver.ts'
23
+ export { FileManager } from './FileManager.ts'
24
+ export { FileProcessor } from './FileProcessor.ts'
22
25
  export { getMode, PluginDriver } from './PluginDriver.ts'
23
26
  export { fsStorage } from './storages/fsStorage.ts'
24
27
  export { memoryStorage } from './storages/memoryStorage.ts'
@@ -26,10 +29,8 @@ export * from './types.ts'
26
29
  export type { FunctionParamsAST } from './utils/FunctionParams.ts'
27
30
  export { detectFormatter } from './utils/formatters.ts'
28
31
  export { getBarrelFiles } from './utils/getBarrelFiles.ts'
29
- export { getConfigs } from './utils/getConfigs.ts'
30
32
  export type { Param, Params } from './utils/getFunctionParams.ts'
31
33
  export { createFunctionParams, FunctionParams, getFunctionParams } from './utils/getFunctionParams.ts'
32
- export { getPreset } from './utils/getPreset.ts'
33
34
  export { isInputPath } from './utils/isInputPath.ts'
34
35
  export { detectLinter } from './utils/linters.ts'
35
36
  export { satisfiesDependency } from './utils/packageJSON.ts'
package/src/mocks.ts ADDED
@@ -0,0 +1,234 @@
1
+ import { resolve } from 'node:path'
2
+ import type { FileNode, OperationNode, SchemaNode, Visitor } from '@kubb/ast'
3
+ import { transform } from '@kubb/ast'
4
+ import { FileManager } from './FileManager.ts'
5
+ import { getMode, type PluginDriver } from './PluginDriver.ts'
6
+ import { applyHookResult } from './renderNode.ts'
7
+ import type {
8
+ Adapter,
9
+ AdapterFactoryOptions,
10
+ Config,
11
+ Generator,
12
+ GeneratorContext,
13
+ Plugin,
14
+ PluginFactoryOptions,
15
+ ResolveNameParams,
16
+ ResolvePathParams,
17
+ } from './types.ts'
18
+
19
+ function toCamelOrPascal(text: string, pascal: boolean): string {
20
+ const normalized = text
21
+ .trim()
22
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
23
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
24
+ .replace(/(\d)([a-z])/g, '$1 $2')
25
+
26
+ const words = normalized.split(/[\s\-_./\\:]+/).filter(Boolean)
27
+
28
+ return words
29
+ .map((word, i) => {
30
+ const allUpper = word.length > 1 && word === word.toUpperCase()
31
+ if (allUpper) return word
32
+ if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1)
33
+ return word.charAt(0).toUpperCase() + word.slice(1)
34
+ })
35
+ .join('')
36
+ .replace(/[^a-zA-Z0-9]/g, '')
37
+ }
38
+
39
+ function camelCase(text: string): string {
40
+ return toCamelOrPascal(text, false)
41
+ }
42
+
43
+ function pascalCase(text: string): string {
44
+ return toCamelOrPascal(text, true)
45
+ }
46
+
47
+ /**
48
+ * Creates a minimal `PluginDriver` mock suitable for unit tests.
49
+ */
50
+ export function createMockedPluginDriver(options: { name?: string; plugin?: Plugin<any>; config?: Config } = {}): PluginDriver {
51
+ return {
52
+ resolveName: (result: ResolveNameParams) => {
53
+ if (result.type === 'file') {
54
+ return camelCase(options?.name || result.name)
55
+ }
56
+
57
+ if (result.type === 'type') {
58
+ return pascalCase(result.name)
59
+ }
60
+
61
+ if (result.type === 'function') {
62
+ return camelCase(result.name)
63
+ }
64
+
65
+ return camelCase(result.name)
66
+ },
67
+ config: options?.config ?? {
68
+ root: '.',
69
+ output: {
70
+ path: './path',
71
+ },
72
+ },
73
+ resolvePath: ({ baseName }: ResolvePathParams) => baseName,
74
+ getFile: ({
75
+ name,
76
+ extname,
77
+ pluginName,
78
+ options: fileOptions,
79
+ }: {
80
+ name: string
81
+ extname: `.${string}`
82
+ pluginName: string
83
+ options?: { group?: { tag?: string; path?: string } }
84
+ }) => {
85
+ const baseName = `${name}${extname}`
86
+ const groupDir = fileOptions?.group?.tag ?? fileOptions?.group?.path?.split('/').filter(Boolean)[0]
87
+ const filePath = groupDir ? `${groupDir}/${baseName}` : baseName
88
+
89
+ return {
90
+ path: filePath,
91
+ baseName,
92
+ meta: { pluginName },
93
+ }
94
+ },
95
+ getPlugin(_pluginName: Plugin['name']): Plugin | undefined {
96
+ return options?.plugin
97
+ },
98
+ fileManager: new FileManager(),
99
+ } as unknown as PluginDriver
100
+ }
101
+
102
+ /**
103
+ * Creates a minimal `Adapter` mock suitable for unit tests.
104
+ *
105
+ * - `parse` returns an empty `InputNode` by default; override via `options.parse`.
106
+ * - `getImports` returns `[]` by default (single-file mode, no cross-file imports).
107
+ */
108
+ export function createMockedAdapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions>(
109
+ options: {
110
+ name?: TOptions['name']
111
+ resolvedOptions?: TOptions['resolvedOptions']
112
+ inputNode?: Adapter<TOptions>['inputNode']
113
+ parse?: Adapter<TOptions>['parse']
114
+ getImports?: Adapter<TOptions>['getImports']
115
+ } = {},
116
+ ): Adapter<TOptions> {
117
+ return {
118
+ name: (options.name ?? 'oas') as TOptions['name'],
119
+ options: (options.resolvedOptions ?? {}) as TOptions['resolvedOptions'],
120
+ inputNode: options.inputNode ?? null,
121
+ parse: options.parse ?? (async () => ({ kind: 'Input' as const, schemas: [], operations: [] })),
122
+ getImports: options.getImports ?? ((_node: SchemaNode, _resolve: (schemaName: string) => { name: string; path: string }) => []),
123
+ } as Adapter<TOptions>
124
+ }
125
+
126
+ /**
127
+ * Creates a minimal `Plugin` mock suitable for unit tests.
128
+ *
129
+ * @example
130
+ * const plugin = createMockedPlugin<PluginTs>({ name: '@kubb/plugin-ts', options })
131
+ */
132
+ export function createMockedPlugin<TOptions extends PluginFactoryOptions = PluginFactoryOptions>(params: {
133
+ name: TOptions['name']
134
+ options: TOptions['resolvedOptions']
135
+ resolver?: TOptions['resolver']
136
+ transformer?: Visitor
137
+ dependencies?: Array<string>
138
+ }): Plugin<TOptions> {
139
+ return {
140
+ name: params.name,
141
+ options: params.options,
142
+ resolver: params.resolver,
143
+ transformer: params.transformer,
144
+ dependencies: params.dependencies,
145
+ install: () => {},
146
+ inject: () => undefined as TOptions['context'],
147
+ } as unknown as Plugin<TOptions>
148
+ }
149
+
150
+ type RenderGeneratorOptions<TOptions extends PluginFactoryOptions> = {
151
+ config: Config
152
+ adapter: Adapter
153
+ driver: PluginDriver
154
+ plugin: Plugin<TOptions>
155
+ options: TOptions['resolvedOptions']
156
+ resolver: TOptions['resolver']
157
+ }
158
+
159
+ function createMockedPluginContext<TOptions extends PluginFactoryOptions>(opts: RenderGeneratorOptions<TOptions>): Omit<GeneratorContext<TOptions>, 'options'> {
160
+ const root = resolve(opts.config.root, opts.config.output.path)
161
+
162
+ return {
163
+ config: opts.config,
164
+ root,
165
+ getMode: (output: { path: string }) => getMode(resolve(root, output.path)),
166
+ adapter: opts.adapter,
167
+ resolver: opts.resolver,
168
+ plugin: opts.plugin,
169
+ driver: opts.driver,
170
+ inputNode: { kind: 'Input', schemas: [], operations: [] },
171
+ upsertFile: async (...files: Array<FileNode>) => opts.driver.fileManager.upsert(...files),
172
+ warn: (msg: string) => console.warn(msg),
173
+ error: (msg: string) => console.error(msg),
174
+ info: (msg: string) => console.info(msg),
175
+ openInStudio: async () => {},
176
+ } as unknown as Omit<GeneratorContext<TOptions>, 'options'>
177
+ }
178
+
179
+ /**
180
+ * Renders a generator's `schema` method in a test context.
181
+ *
182
+ * @example
183
+ * await renderGeneratorSchema(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })
184
+ * await matchFiles(driver.fileManager.files)
185
+ */
186
+ export async function renderGeneratorSchema<TOptions extends PluginFactoryOptions>(
187
+ generator: Generator<TOptions>,
188
+ node: SchemaNode,
189
+ opts: RenderGeneratorOptions<TOptions>,
190
+ ): Promise<void> {
191
+ if (!generator.schema) return
192
+ const context = createMockedPluginContext(opts)
193
+ const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node
194
+ const result = await generator.schema(transformedNode, { ...context, options: opts.options })
195
+ await applyHookResult(result, opts.driver, generator.renderer ?? undefined)
196
+ }
197
+
198
+ /**
199
+ * Renders a generator's `operation` method in a test context.
200
+ *
201
+ * @example
202
+ * await renderGeneratorOperation(typeGenerator, node, { config, adapter, driver, plugin, options, resolver })
203
+ * await matchFiles(driver.fileManager.files)
204
+ */
205
+ export async function renderGeneratorOperation<TOptions extends PluginFactoryOptions>(
206
+ generator: Generator<TOptions>,
207
+ node: OperationNode,
208
+ opts: RenderGeneratorOptions<TOptions>,
209
+ ): Promise<void> {
210
+ if (!generator.operation) return
211
+ const context = createMockedPluginContext(opts)
212
+ const transformedNode = opts.plugin.transformer ? transform(node, opts.plugin.transformer) : node
213
+ const result = await generator.operation(transformedNode, { ...context, options: opts.options })
214
+ await applyHookResult(result, opts.driver, generator.renderer ?? undefined)
215
+ }
216
+
217
+ /**
218
+ * Renders a generator's `operations` method in a test context.
219
+ *
220
+ * @example
221
+ * await renderGeneratorOperations(classClientGenerator, nodes, { config, adapter, driver, plugin, options, resolver })
222
+ * await matchFiles(driver.fileManager.files)
223
+ */
224
+ export async function renderGeneratorOperations<TOptions extends PluginFactoryOptions>(
225
+ generator: Generator<TOptions>,
226
+ nodes: Array<OperationNode>,
227
+ opts: RenderGeneratorOptions<TOptions>,
228
+ ): Promise<void> {
229
+ if (!generator.operations) return
230
+ const context = createMockedPluginContext(opts)
231
+ const transformedNodes = opts.plugin.transformer ? nodes.map((n) => transform(n, opts.plugin.transformer!)) : nodes
232
+ const result = await generator.operations(transformedNodes, { ...context, options: opts.options })
233
+ await applyHookResult(result, opts.driver, generator.renderer ?? undefined)
234
+ }
@@ -0,0 +1,35 @@
1
+ import type { FileNode } from '@kubb/ast'
2
+ import type { RendererFactory } from './createRenderer.ts'
3
+ import type { PluginDriver } from './PluginDriver.ts'
4
+
5
+ /**
6
+ * Handles the return value of a plugin AST hook or generator method.
7
+ *
8
+ * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`
9
+ * - `Array<FileNode>` → added directly into `driver.fileManager`
10
+ * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
11
+ *
12
+ * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result
13
+ * may be a renderer element. Generators that only return `Array<FileNode>` do not need one.
14
+ */
15
+ export async function applyHookResult<TElement = unknown>(
16
+ result: TElement | Array<FileNode> | void,
17
+ driver: PluginDriver,
18
+ rendererFactory?: RendererFactory<TElement>,
19
+ ): Promise<void> {
20
+ if (!result) return
21
+
22
+ if (Array.isArray(result)) {
23
+ driver.fileManager.upsert(...(result as Array<FileNode>))
24
+ return
25
+ }
26
+
27
+ if (!rendererFactory) {
28
+ return
29
+ }
30
+
31
+ const renderer = rendererFactory()
32
+ await renderer.render(result)
33
+ driver.fileManager.upsert(...renderer.files)
34
+ renderer.unmount()
35
+ }