@kubb/core 5.0.0-beta.2 → 5.0.0-beta.20

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 (42) hide show
  1. package/README.md +8 -38
  2. package/dist/{PluginDriver-BXibeQk-.cjs → KubbDriver-BXSnJ3qM.cjs} +719 -164
  3. package/dist/KubbDriver-BXSnJ3qM.cjs.map +1 -0
  4. package/dist/{PluginDriver-DV3p2Hky.js → KubbDriver-Cxii_rBp.js} +693 -162
  5. package/dist/KubbDriver-Cxii_rBp.js.map +1 -0
  6. package/dist/{types-CC09VtBt.d.ts → createKubb-Dcmtjqds.d.ts} +1395 -1238
  7. package/dist/index.cjs +556 -785
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +2 -185
  10. package/dist/index.js +551 -783
  11. package/dist/index.js.map +1 -1
  12. package/dist/mocks.cjs +30 -21
  13. package/dist/mocks.cjs.map +1 -1
  14. package/dist/mocks.d.ts +5 -5
  15. package/dist/mocks.js +29 -20
  16. package/dist/mocks.js.map +1 -1
  17. package/package.json +6 -18
  18. package/src/FileManager.ts +12 -0
  19. package/src/FileProcessor.ts +37 -38
  20. package/src/{PluginDriver.ts → KubbDriver.ts} +249 -86
  21. package/src/constants.ts +11 -6
  22. package/src/createAdapter.ts +84 -1
  23. package/src/createKubb.ts +1336 -297
  24. package/src/createRenderer.ts +23 -22
  25. package/src/defineGenerator.ts +96 -7
  26. package/src/defineLogger.ts +42 -3
  27. package/src/defineMiddleware.ts +1 -1
  28. package/src/defineParser.ts +1 -1
  29. package/src/definePlugin.ts +304 -8
  30. package/src/defineResolver.ts +268 -147
  31. package/src/devtools.ts +8 -1
  32. package/src/index.ts +2 -2
  33. package/src/mocks.ts +11 -14
  34. package/src/storages/fsStorage.ts +13 -37
  35. package/src/types.ts +38 -1292
  36. package/dist/PluginDriver-BXibeQk-.cjs.map +0 -1
  37. package/dist/PluginDriver-DV3p2Hky.js.map +0 -1
  38. package/src/Kubb.ts +0 -300
  39. package/src/renderNode.ts +0 -35
  40. package/src/utils/diagnostics.ts +0 -18
  41. package/src/utils/isInputPath.ts +0 -10
  42. package/src/utils/packageJSON.ts +0 -99
@@ -1,29 +1,30 @@
1
- import { extname, resolve } from 'node:path'
2
- import type { AsyncEventEmitter } from '@internals/utils'
3
- import type { FileNode, InputNode, OperationNode, SchemaNode } from '@kubb/ast'
4
- import { createFile } from '@kubb/ast'
1
+ import { resolve } from 'node:path'
2
+ import { arrayToAsyncIterable, type AsyncEventEmitter, memoize, URLPath } from '@internals/utils'
3
+ import { createFile, createStreamInput } from '@kubb/ast'
4
+ import type { FileNode, InputMeta, InputNode, InputStreamNode, OperationNode, SchemaNode } from '@kubb/ast'
5
5
  import { DEFAULT_STUDIO_URL } from './constants.ts'
6
6
  import type { Generator } from './defineGenerator.ts'
7
7
  import type { Plugin } from './definePlugin.ts'
8
+ import { getMode } from './definePlugin.ts'
8
9
  import { defineResolver } from './defineResolver.ts'
9
10
  import { openInStudio as openInStudioFn } from './devtools.ts'
10
11
  import { FileManager } from './FileManager.ts'
11
- import { applyHookResult } from './renderNode.ts'
12
+ import type { RendererFactory } from './createRenderer.ts'
12
13
 
13
14
  import type {
14
15
  Adapter,
16
+ AdapterSource,
15
17
  Config,
16
18
  DevtoolsOptions,
17
19
  GeneratorContext,
18
20
  KubbHooks,
19
21
  KubbPluginSetupContext,
22
+ Middleware,
20
23
  NormalizedPlugin,
21
24
  PluginFactoryOptions,
22
25
  Resolver,
23
26
  } from './types.ts'
24
27
 
25
- // inspired by: https://github.com/rollup/rollup/blob/master/src/utils/PluginDriver.ts#
26
-
27
28
  type Options = {
28
29
  hooks: AsyncEventEmitter<KubbHooks>
29
30
  }
@@ -32,7 +33,7 @@ function enforceOrder(enforce: 'pre' | 'post' | undefined): number {
32
33
  return enforce === 'pre' ? -1 : enforce === 'post' ? 1 : 0
33
34
  }
34
35
 
35
- export class PluginDriver {
36
+ export class KubbDriver {
36
37
  readonly config: Config
37
38
  readonly options: Options
38
39
 
@@ -41,24 +42,41 @@ export class PluginDriver {
41
42
  *
42
43
  * @example
43
44
  * ```ts
44
- * PluginDriver.getMode('src/gen/types.ts') // 'single'
45
- * PluginDriver.getMode('src/gen/types') // 'split'
45
+ * KubbDriver.getMode('src/gen/types.ts') // 'single'
46
+ * KubbDriver.getMode('src/gen/types') // 'split'
46
47
  * ```
47
48
  */
48
49
  static getMode(fileOrFolder: string | undefined | null): 'single' | 'split' {
49
- if (!fileOrFolder) {
50
- return 'split'
51
- }
52
- return extname(fileOrFolder) ? 'single' : 'split'
50
+ return getMode(fileOrFolder)
53
51
  }
54
52
 
55
53
  /**
56
- * The universal `@kubb/ast` `InputNode` produced by the adapter, set by
57
- * the build pipeline after the adapter's `parse()` resolves.
54
+ * The streaming `InputStreamNode` produced by the adapter.
55
+ * Always set after adapter setup — parse-only adapters are wrapped automatically.
58
56
  */
59
- inputNode: InputNode | undefined = undefined
57
+ inputNode: InputStreamNode | undefined = undefined
60
58
  adapter: Adapter | undefined = undefined
61
- #studioIsOpen = false
59
+ /**
60
+ * Studio session state, kept together so `dispose()` can reset it atomically.
61
+ *
62
+ * - `source` holds the raw adapter source so `adapter.parse()` can be called lazily.
63
+ * Intentionally outlives the build; cleared by `dispose()`.
64
+ * - `isOpen` prevents opening the studio more than once per build.
65
+ * - `inputNode` caches the parse promise so `adapter.parse()` is called at most once
66
+ * per studio session, even when `openInStudio()` is called multiple times.
67
+ */
68
+ #studio: { source: AdapterSource | undefined; isOpen: boolean; inputNode: Promise<InputNode> | undefined } = {
69
+ source: undefined,
70
+ isOpen: false,
71
+ inputNode: undefined,
72
+ }
73
+
74
+ // Register middleware hooks after all plugin hooks are registered.
75
+ // Because AsyncEventEmitter calls listeners in registration order,
76
+ // middleware hooks for any event fire after all plugin hooks for that event.
77
+ // Handlers are tracked so they can be removed after each build (disposeMiddleware),
78
+ // preventing accumulation when multiple configs share the same hooks instance.
79
+ #middlewareListeners: Array<[keyof KubbHooks & string, (...args: never[]) => void | Promise<void>]> = []
62
80
 
63
81
  /**
64
82
  * Central file store for all generated files.
@@ -73,7 +91,7 @@ export class PluginDriver {
73
91
  * Tracks which plugins have generators registered via `addGenerator()` (event-based path).
74
92
  * Used by the build loop to decide whether to emit generator events for a given plugin.
75
93
  */
76
- readonly #pluginsWithEventGenerators = new Set<string>()
94
+ readonly #eventGeneratorPlugins = new Set<string>()
77
95
  readonly #resolvers = new Map<string, Resolver>()
78
96
  readonly #defaultResolvers = new Map<string, Resolver>()
79
97
  readonly #hookListeners = new Map<keyof KubbHooks, Set<(...args: never[]) => void | Promise<void>>>()
@@ -81,23 +99,38 @@ export class PluginDriver {
81
99
  constructor(config: Config, options: Options) {
82
100
  this.config = config
83
101
  this.options = options
84
- config.plugins
85
- .map((rawPlugin) => this.#normalizePlugin(rawPlugin as Plugin))
86
- .filter((plugin) => {
87
- if (typeof plugin.apply === 'function') {
88
- return plugin.apply(config)
102
+ this.adapter = config.adapter
103
+ }
104
+
105
+ async setup() {
106
+ const normalized: NormalizedPlugin[] = this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin as Plugin))
107
+
108
+ normalized.sort((a, b) => {
109
+ if (b.dependencies?.includes(a.name)) return -1
110
+ if (a.dependencies?.includes(b.name)) return 1
111
+
112
+ return enforceOrder(a.enforce) - enforceOrder(b.enforce)
113
+ })
114
+
115
+ for (const plugin of normalized) {
116
+ if (plugin.apply) {
117
+ plugin.apply(this.config)
118
+ }
119
+
120
+ this.#registerPlugin(plugin)
121
+ this.plugins.set(plugin.name, plugin)
122
+ }
123
+
124
+ if (this.config.middleware) {
125
+ for (const middleware of this.config.middleware) {
126
+ for (const event of Object.keys(middleware.hooks) as Array<keyof KubbHooks & string>) {
127
+ this.#registerMiddleware(event, middleware.hooks)
89
128
  }
90
- return true
91
- })
92
- .sort((a, b) => {
93
- if (b.dependencies?.includes(a.name)) return -1
94
- if (a.dependencies?.includes(b.name)) return 1
95
- // enforce: 'pre' plugins run first, 'post' plugins run last
96
- return enforceOrder(a.enforce) - enforceOrder(b.enforce)
97
- })
98
- .forEach((plugin) => {
99
- this.plugins.set(plugin.name, plugin)
100
- })
129
+ }
130
+ }
131
+ if (this.config.adapter) {
132
+ await this.#registerAdapter(this.config.adapter)
133
+ }
101
134
  }
102
135
 
103
136
  get hooks() {
@@ -108,16 +141,59 @@ export class PluginDriver {
108
141
  * Creates an `NormalizedPlugin` from a hook-style plugin and registers
109
142
  * its lifecycle handlers on the `AsyncEventEmitter`.
110
143
  */
111
- #normalizePlugin(hookPlugin: Plugin): NormalizedPlugin {
112
- const normalizedPlugin = {
113
- name: hookPlugin.name,
114
- dependencies: hookPlugin.dependencies,
115
- enforce: hookPlugin.enforce,
116
- options: { output: { path: '.' }, exclude: [], override: [] },
117
- } as unknown as NormalizedPlugin
118
-
119
- this.registerPluginHooks(hookPlugin, normalizedPlugin)
120
- return normalizedPlugin
144
+ #normalizePlugin(plugin: Plugin): NormalizedPlugin {
145
+ const normalized: NormalizedPlugin = {
146
+ name: plugin.name,
147
+ dependencies: plugin.dependencies,
148
+ enforce: plugin.enforce,
149
+ hooks: plugin.hooks,
150
+ options: plugin.options ?? { output: { path: '.' }, exclude: [], override: [] },
151
+ } as NormalizedPlugin
152
+
153
+ if ('apply' in plugin && typeof plugin.apply === 'function') {
154
+ normalized.apply = plugin.apply as (config: Config) => boolean
155
+ }
156
+
157
+ return normalized
158
+ }
159
+
160
+ async #registerAdapter(adapter: Adapter) {
161
+ const source = inputToAdapterSource(this.config)
162
+ this.#studio.source = source
163
+
164
+ if (adapter.stream) {
165
+ this.inputNode = await adapter.stream(source)
166
+
167
+ await this.hooks.emit('kubb:debug', {
168
+ date: new Date(),
169
+ logs: [`✓ Adapter '${adapter.name}' producing input stream`],
170
+ })
171
+ } else {
172
+ // Adapter does not implement stream() — eagerly parse and wrap in a
173
+ // reusable AsyncIterable so the rest of the pipeline stays stream-only.
174
+ const inputNode = await adapter.parse(source)
175
+ this.inputNode = createStreamInput(arrayToAsyncIterable(inputNode.schemas), arrayToAsyncIterable(inputNode.operations), inputNode.meta)
176
+
177
+ await this.hooks.emit('kubb:debug', {
178
+ date: new Date(),
179
+ logs: [
180
+ `✓ Adapter '${adapter.name}' resolved InputNode (wrapped as stream)`,
181
+ ` • Schemas: ${inputNode.schemas.length}`,
182
+ ` • Operations: ${inputNode.operations.length}`,
183
+ ],
184
+ })
185
+ }
186
+ }
187
+
188
+ #registerMiddleware<K extends keyof KubbHooks & string>(event: K, middlewareHooks: Middleware['hooks']) {
189
+ const handler = middlewareHooks[event]
190
+
191
+ if (!handler) {
192
+ return
193
+ }
194
+
195
+ this.hooks.on(event, handler)
196
+ this.#middlewareListeners.push([event, handler as (...args: never[]) => void | Promise<void>])
121
197
  }
122
198
 
123
199
  /**
@@ -135,8 +211,10 @@ export class PluginDriver {
135
211
  *
136
212
  * @internal
137
213
  */
138
- registerPluginHooks(hookPlugin: Plugin, normalizedPlugin: NormalizedPlugin): void {
139
- const { hooks } = hookPlugin
214
+ #registerPlugin(plugin: NormalizedPlugin): void {
215
+ const { hooks } = plugin
216
+
217
+ if (!hooks) return
140
218
 
141
219
  // kubb:plugin:setup gets special treatment: the globally emitted context is wrapped with
142
220
  // plugin-specific implementations so that addGenerator / setResolver / etc. target
@@ -145,21 +223,21 @@ export class PluginDriver {
145
223
  const setupHandler = (globalCtx: KubbPluginSetupContext) => {
146
224
  const pluginCtx: KubbPluginSetupContext = {
147
225
  ...globalCtx,
148
- options: hookPlugin.options ?? {},
226
+ options: plugin.options ?? {},
149
227
  addGenerator: (gen) => {
150
- this.registerGenerator(normalizedPlugin.name, gen)
228
+ this.registerGenerator(plugin.name, gen)
151
229
  },
152
230
  setResolver: (resolver) => {
153
- this.setPluginResolver(normalizedPlugin.name, resolver)
231
+ this.setPluginResolver(plugin.name, resolver)
154
232
  },
155
233
  setTransformer: (visitor) => {
156
- normalizedPlugin.transformer = visitor
234
+ plugin.transformer = visitor
157
235
  },
158
236
  setRenderer: (renderer) => {
159
- normalizedPlugin.renderer = renderer
237
+ plugin.renderer = renderer
160
238
  },
161
239
  setOptions: (opts) => {
162
- normalizedPlugin.options = { ...normalizedPlugin.options, ...opts }
240
+ plugin.options = { ...plugin.options, ...opts }
163
241
  },
164
242
  injectFile: (userFileNode) => {
165
243
  this.fileManager.add(createFile(userFileNode))
@@ -189,6 +267,7 @@ export class PluginDriver {
189
267
  */
190
268
  async emitSetupHooks(): Promise<void> {
191
269
  const noop = () => {}
270
+
192
271
  await this.hooks.emit('kubb:plugin:setup', {
193
272
  config: this.config,
194
273
  options: {},
@@ -226,7 +305,7 @@ export class PluginDriver {
226
305
  const schemaHandler = async (node: SchemaNode, ctx: GeneratorContext) => {
227
306
  if (ctx.plugin.name !== pluginName) return
228
307
  const result = await gen.schema!(node, ctx)
229
- await applyHookResult(result, this, resolveRenderer())
308
+ await applyHookResult({ result, driver: this, rendererFactory: resolveRenderer() })
230
309
  }
231
310
 
232
311
  this.hooks.on('kubb:generate:schema', schemaHandler)
@@ -237,7 +316,7 @@ export class PluginDriver {
237
316
  const operationHandler = async (node: OperationNode, ctx: GeneratorContext) => {
238
317
  if (ctx.plugin.name !== pluginName) return
239
318
  const result = await gen.operation!(node, ctx)
240
- await applyHookResult(result, this, resolveRenderer())
319
+ await applyHookResult({ result, driver: this, rendererFactory: resolveRenderer() })
241
320
  }
242
321
 
243
322
  this.hooks.on('kubb:generate:operation', operationHandler)
@@ -248,14 +327,14 @@ export class PluginDriver {
248
327
  const operationsHandler = async (nodes: Array<OperationNode>, ctx: GeneratorContext) => {
249
328
  if (ctx.plugin.name !== pluginName) return
250
329
  const result = await gen.operations!(nodes, ctx)
251
- await applyHookResult(result, this, resolveRenderer())
330
+ await applyHookResult({ result, driver: this, rendererFactory: resolveRenderer() })
252
331
  }
253
332
 
254
333
  this.hooks.on('kubb:generate:operations', operationsHandler)
255
334
  this.#trackHookListener('kubb:generate:operations', operationsHandler as (...args: never[]) => void | Promise<void>)
256
335
  }
257
336
 
258
- this.#pluginsWithEventGenerators.add(pluginName)
337
+ this.#eventGeneratorPlugins.add(pluginName)
259
338
  }
260
339
 
261
340
  /**
@@ -265,8 +344,8 @@ export class PluginDriver {
265
344
  * Used by the build loop to decide whether to walk the AST and emit generator events
266
345
  * for a plugin that has no static `plugin.generators`.
267
346
  */
268
- hasRegisteredGenerators(pluginName: string): boolean {
269
- return this.#pluginsWithEventGenerators.has(pluginName)
347
+ hasEventGenerators(pluginName: string): boolean {
348
+ return this.#eventGeneratorPlugins.has(pluginName)
270
349
  }
271
350
 
272
351
  /**
@@ -281,8 +360,27 @@ export class PluginDriver {
281
360
  this.hooks.off(event, handler as never)
282
361
  }
283
362
  }
363
+
284
364
  this.#hookListeners.clear()
285
- this.#pluginsWithEventGenerators.clear()
365
+ this.#eventGeneratorPlugins.clear()
366
+ // Release resolver closures — the driver is rebuilt for each build() call
367
+ // so there is no value in retaining these maps after disposal.
368
+ this.#resolvers.clear()
369
+ this.#defaultResolvers.clear()
370
+ // Release the FileNode cache, parsed adapter graph, and studio state so
371
+ // memory is reclaimed between builds. The returned `BuildOutput.files`
372
+ // array still references any FileNodes the caller needs to inspect.
373
+ this.fileManager.dispose()
374
+ this.inputNode = undefined
375
+ this.#studio = { source: undefined, isOpen: false, inputNode: undefined }
376
+
377
+ for (const [event, handler] of this.#middlewareListeners) {
378
+ this.hooks.off(event, handler as never)
379
+ }
380
+ }
381
+
382
+ [Symbol.dispose](): void {
383
+ this.dispose()
286
384
  }
287
385
 
288
386
  #trackHookListener(event: keyof KubbHooks, handler: (...args: never[]) => void | Promise<void>): void {
@@ -294,19 +392,10 @@ export class PluginDriver {
294
392
  handlers.add(handler)
295
393
  }
296
394
 
297
- #createDefaultResolver(pluginName: string): Resolver {
298
- const existingResolver = this.#defaultResolvers.get(pluginName)
299
- if (existingResolver) {
300
- return existingResolver
301
- }
302
-
303
- const resolver = defineResolver<PluginFactoryOptions>((_ctx) => ({
304
- name: 'default',
305
- pluginName,
306
- }))
307
- this.#defaultResolvers.set(pluginName, resolver)
308
- return resolver
309
- }
395
+ #getDefaultResolver = memoize(
396
+ this.#defaultResolvers,
397
+ (pluginName: string): Resolver => defineResolver<PluginFactoryOptions>(() => ({ name: 'default', pluginName })),
398
+ )
310
399
 
311
400
  /**
312
401
  * Merges `partial` with the plugin's default resolver and stores the result.
@@ -314,7 +403,7 @@ export class PluginDriver {
314
403
  * get the up-to-date resolver without going through `getResolver()`.
315
404
  */
316
405
  setPluginResolver(pluginName: string, partial: Partial<Resolver>): void {
317
- const defaultResolver = this.#createDefaultResolver(pluginName)
406
+ const defaultResolver = this.#getDefaultResolver(pluginName)
318
407
  const merged = { ...defaultResolver, ...partial }
319
408
  this.#resolvers.set(pluginName, merged)
320
409
  const plugin = this.plugins.get(pluginName)
@@ -332,19 +421,19 @@ export class PluginDriver {
332
421
  getResolver<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Kubb.PluginRegistry[TName]['resolver']
333
422
  getResolver<TResolver extends Resolver = Resolver>(pluginName: string): TResolver
334
423
  getResolver(pluginName: string): Resolver {
335
- return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#createDefaultResolver(pluginName)
424
+ return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#getDefaultResolver(pluginName)
336
425
  }
337
426
 
338
427
  getContext<TOptions extends PluginFactoryOptions>(plugin: NormalizedPlugin<TOptions>): GeneratorContext<TOptions> & Record<string, unknown> {
339
428
  const driver = this
340
429
 
341
- const baseContext = {
430
+ return {
342
431
  config: driver.config,
343
432
  get root(): string {
344
433
  return resolve(driver.config.root, driver.config.output.path)
345
434
  },
346
435
  getMode(output: { path: string }): 'single' | 'split' {
347
- return PluginDriver.getMode(resolve(driver.config.root, driver.config.output.path, output.path))
436
+ return KubbDriver.getMode(resolve(driver.config.root, driver.config.output.path, output.path))
348
437
  },
349
438
  hooks: driver.hooks,
350
439
  plugin,
@@ -358,8 +447,8 @@ export class PluginDriver {
358
447
  upsertFile: async (...files: Array<FileNode>) => {
359
448
  driver.fileManager.upsert(...files)
360
449
  },
361
- get inputNode(): InputNode | undefined {
362
- return driver.inputNode
450
+ get meta(): InputMeta {
451
+ return driver.inputNode?.meta ?? { circularNames: [], enumNames: [] }
363
452
  },
364
453
  get adapter(): Adapter | undefined {
365
454
  return driver.adapter
@@ -379,8 +468,8 @@ export class PluginDriver {
379
468
  info(message: string) {
380
469
  driver.hooks.emit('kubb:info', { message })
381
470
  },
382
- openInStudio(options?: DevtoolsOptions) {
383
- if (!driver.config.devtools || driver.#studioIsOpen) {
471
+ async openInStudio(options?: DevtoolsOptions) {
472
+ if (!driver.config.devtools || driver.#studio.isOpen) {
384
473
  return
385
474
  }
386
475
 
@@ -388,19 +477,19 @@ export class PluginDriver {
388
477
  throw new Error('Devtools must be an object')
389
478
  }
390
479
 
391
- if (!driver.inputNode || !driver.adapter) {
480
+ if (!driver.adapter || !driver.#studio.source) {
392
481
  throw new Error('adapter is not defined, make sure you have set the parser in kubb.config.ts')
393
482
  }
394
483
 
395
- driver.#studioIsOpen = true
484
+ driver.#studio.isOpen = true
396
485
 
397
486
  const studioUrl = driver.config.devtools?.studioUrl ?? DEFAULT_STUDIO_URL
487
+ driver.#studio.inputNode ??= Promise.resolve(driver.adapter.parse(driver.#studio.source))
488
+ const inputNode = await driver.#studio.inputNode
398
489
 
399
- return openInStudioFn(driver.inputNode, studioUrl, options)
490
+ return openInStudioFn(inputNode, studioUrl, options)
400
491
  },
401
492
  } as unknown as GeneratorContext<TOptions>
402
-
403
- return baseContext
404
493
  }
405
494
 
406
495
  getPlugin<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined
@@ -422,3 +511,77 @@ export class PluginDriver {
422
511
  return plugin
423
512
  }
424
513
  }
514
+
515
+ /**
516
+ * Handles the return value of a plugin AST hook or generator method.
517
+ *
518
+ * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`
519
+ * - `Array<FileNode>` → added directly into `driver.fileManager`
520
+ * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
521
+ *
522
+ * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result
523
+ * may be a renderer element. Generators that only return `Array<FileNode>` do not need one.
524
+ */
525
+ export function applyHookResult<TElement = unknown>({
526
+ result,
527
+ driver,
528
+ rendererFactory,
529
+ }: {
530
+ result: TElement | Array<FileNode> | void
531
+ driver: KubbDriver
532
+ rendererFactory?: RendererFactory<TElement>
533
+ }): void | Promise<void> {
534
+ if (!result) return
535
+
536
+ if (Array.isArray(result)) {
537
+ driver.fileManager.upsert(...(result as Array<FileNode>))
538
+ return
539
+ }
540
+
541
+ if (!rendererFactory) {
542
+ return
543
+ }
544
+
545
+ const renderer = rendererFactory()
546
+ if (renderer.stream) {
547
+ for (const file of renderer.stream(result)) {
548
+ driver.fileManager.upsert(file)
549
+ }
550
+ renderer.unmount()
551
+ return
552
+ }
553
+ return applyAsyncRender({ renderer, result, driver })
554
+ }
555
+
556
+ async function applyAsyncRender<TElement>({
557
+ renderer,
558
+ result,
559
+ driver,
560
+ }: {
561
+ renderer: { render(el: TElement): Promise<void>; files: ReadonlyArray<FileNode>; unmount(): void }
562
+ result: TElement
563
+ driver: KubbDriver
564
+ }): Promise<void> {
565
+ await renderer.render(result)
566
+ driver.fileManager.upsert(...renderer.files)
567
+ renderer.unmount()
568
+ }
569
+
570
+ function inputToAdapterSource(config: Config): AdapterSource {
571
+ const input = config.input
572
+ if (!input) {
573
+ throw new Error('[kubb] input is required when using an adapter. Provide input.path or input.data in your config.')
574
+ }
575
+
576
+ if ('data' in input) {
577
+ return { type: 'data', data: input.data }
578
+ }
579
+
580
+ if (new URLPath(input.path).isURL) {
581
+ return { type: 'path', path: input.path }
582
+ }
583
+
584
+ const resolved = resolve(config.root, input.path)
585
+
586
+ return { type: 'path', path: resolved }
587
+ }
package/src/constants.ts CHANGED
@@ -3,12 +3,7 @@ import type { FileNode } from '@kubb/ast'
3
3
  /**
4
4
  * Base URL for the Kubb Studio web app.
5
5
  */
6
- export const DEFAULT_STUDIO_URL = 'https://studio.kubb.dev' as const
7
-
8
- /**
9
- * Maximum number of files processed in parallel by FileProcessor.
10
- */
11
- export const PARALLEL_CONCURRENCY_LIMIT = 100
6
+ export const DEFAULT_STUDIO_URL = 'https://kubb.studio' as const
12
7
 
13
8
  /**
14
9
  * Default banner style written at the top of every generated file.
@@ -20,6 +15,16 @@ export const DEFAULT_BANNER = 'simple' as const
20
15
  */
21
16
  export const DEFAULT_EXTENSION: Record<FileNode['extname'], FileNode['extname'] | ''> = { '.ts': '.ts' }
22
17
 
18
+ /**
19
+ * Number of file writes to batch in parallel during `flushPendingFiles`.
20
+ */
21
+ export const STREAM_FLUSH_EVERY = 50
22
+
23
+ /**
24
+ * Number of schema/operation nodes to dispatch concurrently during generation.
25
+ */
26
+ export const SCHEMA_PARALLEL = 8
27
+
23
28
  /**
24
29
  * Numeric log-level thresholds used internally to compare verbosity.
25
30
  *
@@ -1,4 +1,87 @@
1
- import type { Adapter, AdapterFactoryOptions } from './types.ts'
1
+ import type { PossiblePromise } from '@internals/utils'
2
+ import type { ImportNode, InputNode, InputStreamNode, SchemaNode } from '@kubb/ast'
3
+
4
+ /**
5
+ * Source data passed to an adapter's `parse` function.
6
+ * Mirrors the config input shape with paths resolved to absolute.
7
+ */
8
+ export type AdapterSource = { type: 'path'; path: string } | { type: 'data'; data: string | unknown } | { type: 'paths'; paths: Array<string> }
9
+
10
+ /**
11
+ * Generic type parameters for an adapter definition.
12
+ *
13
+ * - `TName` — unique identifier (e.g. `'oas'`, `'asyncapi'`)
14
+ * - `TOptions` — user-facing options passed to the adapter factory
15
+ * - `TResolvedOptions` — options after defaults applied
16
+ * - `TDocument` — type of the parsed source document
17
+ */
18
+ export type AdapterFactoryOptions<
19
+ TName extends string = string,
20
+ TOptions extends object = object,
21
+ TResolvedOptions extends object = TOptions,
22
+ TDocument = unknown,
23
+ > = {
24
+ name: TName
25
+ options: TOptions
26
+ resolvedOptions: TResolvedOptions
27
+ document: TDocument
28
+ }
29
+
30
+ /**
31
+ * Adapter that converts input files or data into an `InputNode`.
32
+ *
33
+ * Adapters parse different schema formats (OpenAPI, AsyncAPI, Drizzle, etc.) into Kubb's
34
+ * universal intermediate representation that all plugins consume.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { adapterOas } from '@kubb/adapter-oas'
39
+ *
40
+ * export default defineConfig({
41
+ * adapter: adapterOas(),
42
+ * input: { path: './openapi.yaml' },
43
+ * plugins: [pluginTs(), pluginZod()],
44
+ * })
45
+ * ```
46
+ */
47
+ export type Adapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions> = {
48
+ /**
49
+ * Human-readable adapter identifier (e.g. `'oas'`, `'asyncapi'`).
50
+ */
51
+ name: TOptions['name']
52
+ /**
53
+ * Resolved adapter options after defaults have been applied.
54
+ */
55
+ options: TOptions['resolvedOptions']
56
+ /**
57
+ * Parsed source document after the first `parse()` call. `null` before parsing.
58
+ */
59
+ document: TOptions['document'] | null
60
+ /**
61
+ * Parse the source into a universal `InputNode`.
62
+ */
63
+ parse: (source: AdapterSource) => PossiblePromise<InputNode>
64
+ /**
65
+ * Extract `ImportNode` entries for a schema tree.
66
+ * Returns an empty array before the first `parse()` call.
67
+ *
68
+ * The `resolve` callback receives the collision-corrected schema name and must
69
+ * return `{ name, path }` for the import, or `undefined` to skip it.
70
+ */
71
+ getImports: (node: SchemaNode, resolve: (schemaName: string) => { name: string; path: string }) => Array<ImportNode>
72
+ /**
73
+ * Validate the document at the given path or URL.
74
+ */
75
+ validate: (input: string, options?: { throwOnError?: boolean }) => Promise<void>
76
+ /**
77
+ * Memory-efficient streaming variant of `parse()`.
78
+ *
79
+ * Returns an `InputStreamNode` whose `schemas` and `operations` are `AsyncIterable`.
80
+ * Each `for await` loop creates a fresh parse pass over the cached in-memory document —
81
+ * no pre-built arrays are held in memory.
82
+ */
83
+ stream?: (source: AdapterSource) => Promise<InputStreamNode>
84
+ }
2
85
 
3
86
  type AdapterBuilder<T extends AdapterFactoryOptions> = (options: T['options']) => Adapter<T>
4
87