@kubb/core 5.0.0-beta.18 → 5.0.0-beta.19

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/src/createKubb.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { resolve } from 'node:path'
2
2
  import { version as nodeVersion } from 'node:process'
3
3
  import type { PossiblePromise } from '@internals/utils'
4
- import { AsyncEventEmitter, BuildError, exists, formatMs, getElapsedMs, URLPath } from '@internals/utils'
5
- import type { FileNode, InputNode, InputStreamNode, OperationNode, SchemaNode } from '@kubb/ast'
4
+ import { AsyncEventEmitter, BuildError, exists, formatMs, getElapsedMs, URLPath, isPromise } from '@internals/utils'
5
+ import type { FileNode, InputNode, OperationNode, SchemaNode } from '@kubb/ast'
6
6
  import { collectUsedSchemaNames, transform, walk } from '@kubb/ast'
7
7
  import { version as KubbVersion } from '../package.json'
8
- import { DEFAULT_BANNER, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL, STREAM_FLUSH_EVERY, STREAM_SCHEMA_THRESHOLD } from './constants.ts'
8
+ import { DEFAULT_BANNER, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL, STREAM_SCHEMA_THRESHOLD } from './constants.ts'
9
9
  import type { Adapter, AdapterSource } from './createAdapter.ts'
10
10
  import type { RendererFactory } from './createRenderer.ts'
11
11
  import { createStorage, type Storage } from './createStorage.ts'
@@ -14,6 +14,7 @@ import type { Middleware } from './defineMiddleware.ts'
14
14
  import type { Parser } from './defineParser.ts'
15
15
  import type { KubbPluginEndContext, KubbPluginSetupContext, KubbPluginStartContext, NormalizedPlugin, Plugin } from './definePlugin.ts'
16
16
  import { FileProcessor } from './FileProcessor.ts'
17
+
17
18
  import { applyHookResult, PluginDriver } from './PluginDriver.ts'
18
19
  import { fsStorage } from './storages/fsStorage.ts'
19
20
 
@@ -480,21 +481,16 @@ declare global {
480
481
 
481
482
  /**
482
483
  * Lifecycle events emitted during Kubb code generation.
483
- * Use these for logging, progress tracking, and custom integrations.
484
+ * Attach listeners before calling `setup()` or `build()` to observe and react to build progress.
484
485
  *
485
486
  * @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', () => {
487
+ * ```ts
488
+ * kubb.hooks.on('kubb:lifecycle:start', () => {
493
489
  * console.log('Starting Kubb generation')
494
490
  * })
495
491
  *
496
- * hooks.on('kubb:plugin:end', ({ plugin, duration }) => {
497
- * console.log(`Plugin ${plugin.name} completed in ${duration}ms`)
492
+ * kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => {
493
+ * console.log(`${plugin.name} completed in ${duration}ms`)
498
494
  * })
499
495
  * ```
500
496
  */
@@ -535,51 +531,96 @@ export interface KubbHooks {
535
531
  }
536
532
 
537
533
  export type KubbBuildStartContext = {
534
+ /**
535
+ * Resolved configuration for this build.
536
+ */
538
537
  config: Config
538
+ /**
539
+ * Adapter that parsed the input into the universal AST.
540
+ */
539
541
  adapter: Adapter
542
+ /**
543
+ * Parsed input node. For streaming builds the node is a synthetic empty shell
544
+ * with only `meta` populated — use `kubb:generate:schema` / `kubb:generate:operation` to observe individual nodes.
545
+ */
540
546
  inputNode: InputNode
547
+ /**
548
+ * Looks up a registered plugin by name, typed by the plugin registry.
549
+ */
541
550
  getPlugin<TName extends keyof Kubb.PluginRegistry>(name: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined
542
551
  getPlugin(name: string): Plugin | undefined
552
+ /**
553
+ * Snapshot of all files accumulated so far.
554
+ */
543
555
  readonly files: ReadonlyArray<FileNode>
556
+ /**
557
+ * Adds or merges one or more files into the file manager.
558
+ */
544
559
  upsertFile: (...files: Array<FileNode>) => void
545
560
  }
546
561
 
547
562
  export type KubbPluginsEndContext = {
563
+ /**
564
+ * Resolved configuration for this build.
565
+ */
548
566
  config: Config
567
+ /**
568
+ * Snapshot of all files accumulated across all plugins.
569
+ */
549
570
  readonly files: ReadonlyArray<FileNode>
571
+ /**
572
+ * Adds or merges one or more files into the file manager.
573
+ */
550
574
  upsertFile: (...files: Array<FileNode>) => void
551
575
  }
552
576
 
553
577
  export type KubbBuildEndContext = {
578
+ /**
579
+ * All files generated during this build.
580
+ */
554
581
  files: Array<FileNode>
582
+ /**
583
+ * Resolved configuration for this build.
584
+ */
555
585
  config: Config
586
+ /**
587
+ * Absolute path to the output directory.
588
+ */
556
589
  outputDir: string
557
590
  }
558
591
 
559
592
  export type KubbLifecycleStartContext = {
593
+ /**
594
+ * Current Kubb version string.
595
+ */
560
596
  version: string
561
597
  }
562
598
 
563
599
  export type KubbConfigEndContext = {
600
+ /**
601
+ * All resolved configs after defaults are applied.
602
+ */
564
603
  configs: Array<Config>
565
604
  }
566
605
 
567
606
  export type KubbGenerationStartContext = {
607
+ /**
608
+ * Resolved configuration for this generation run.
609
+ */
568
610
  config: Config
569
611
  }
570
612
 
571
613
  export type KubbGenerationEndContext = {
614
+ /**
615
+ * Resolved configuration for this generation run.
616
+ */
572
617
  config: Config
573
618
  /**
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.
619
+ * Read-only view of the files written during this build.
620
+ * Reads go directly to `config.storage` — nothing extra is held in memory.
578
621
  *
579
622
  * @example Read a generated file
580
- * ```ts
581
- * const code = await storage.getItem('/src/gen/pet.ts')
582
- * ```
623
+ * `const code = await storage.getItem('/src/gen/pet.ts')`
583
624
  *
584
625
  * @example Walk every generated file
585
626
  * ```ts
@@ -592,73 +633,178 @@ export type KubbGenerationEndContext = {
592
633
  }
593
634
 
594
635
  export type KubbGenerationSummaryContext = {
636
+ /**
637
+ * Resolved configuration for this generation run.
638
+ */
595
639
  config: Config
640
+ /**
641
+ * Plugins that threw during generation, paired with their errors.
642
+ */
596
643
  failedPlugins: Set<{ plugin: Plugin; error: Error }>
644
+ /**
645
+ * `'success'` when all plugins completed without errors, `'failed'` otherwise.
646
+ */
597
647
  status: 'success' | 'failed'
648
+ /**
649
+ * High-resolution start time from `process.hrtime()`.
650
+ */
598
651
  hrStart: [number, number]
652
+ /**
653
+ * Total number of files created during this run.
654
+ */
599
655
  filesCreated: number
656
+ /**
657
+ * Elapsed milliseconds per plugin, keyed by plugin name.
658
+ */
600
659
  pluginTimings?: Map<Plugin['name'], number>
601
660
  }
602
661
 
603
662
  export type KubbVersionNewContext = {
663
+ /**
664
+ * The installed Kubb version.
665
+ */
604
666
  currentVersion: string
667
+ /**
668
+ * The newest available version on npm.
669
+ */
605
670
  latestVersion: string
606
671
  }
607
672
 
608
673
  export type KubbInfoContext = {
674
+ /**
675
+ * Human-readable info message.
676
+ */
609
677
  message: string
678
+ /**
679
+ * Optional supplementary detail.
680
+ */
610
681
  info?: string
611
682
  }
612
683
 
613
684
  export type KubbErrorContext = {
685
+ /**
686
+ * The caught error.
687
+ */
614
688
  error: Error
689
+ /**
690
+ * Optional structured metadata for additional context.
691
+ */
615
692
  meta?: Record<string, unknown>
616
693
  }
617
694
 
618
695
  export type KubbSuccessContext = {
696
+ /**
697
+ * Human-readable success message.
698
+ */
619
699
  message: string
700
+ /**
701
+ * Optional supplementary detail.
702
+ */
620
703
  info?: string
621
704
  }
622
705
 
623
706
  export type KubbWarnContext = {
707
+ /**
708
+ * Human-readable warning message.
709
+ */
624
710
  message: string
711
+ /**
712
+ * Optional supplementary detail.
713
+ */
625
714
  info?: string
626
715
  }
627
716
 
628
717
  export type KubbDebugContext = {
718
+ /**
719
+ * Timestamp when the debug entry was created.
720
+ */
629
721
  date: Date
722
+ /**
723
+ * One or more log lines to emit.
724
+ */
630
725
  logs: Array<string>
726
+ /**
727
+ * Optional source file name associated with this entry.
728
+ */
631
729
  fileName?: string
632
730
  }
633
731
 
634
732
  export type KubbFilesProcessingStartContext = {
733
+ /**
734
+ * Files about to be serialised and written.
735
+ */
635
736
  files: Array<FileNode>
636
737
  }
637
738
 
638
739
  export type KubbFileProcessingUpdateContext = {
740
+ /**
741
+ * Number of files processed so far in this batch.
742
+ */
639
743
  processed: number
744
+ /**
745
+ * Total number of files in this batch.
746
+ */
640
747
  total: number
748
+ /**
749
+ * Completion percentage (`0`–`100`).
750
+ */
641
751
  percentage: number
752
+ /**
753
+ * Serialised file content, or `undefined` when the file produced no output.
754
+ */
642
755
  source?: string
756
+ /**
757
+ * The file that was just processed.
758
+ */
643
759
  file: FileNode
760
+ /**
761
+ * Resolved configuration for this build.
762
+ */
644
763
  config: Config
645
764
  }
646
765
 
647
766
  export type KubbFilesProcessingEndContext = {
767
+ /**
768
+ * All files that were serialised in this batch.
769
+ */
648
770
  files: Array<FileNode>
649
771
  }
650
772
 
651
773
  export type KubbHookStartContext = {
774
+ /**
775
+ * Optional identifier for correlating start/end events.
776
+ */
652
777
  id?: string
778
+ /**
779
+ * The shell command that is about to run.
780
+ */
653
781
  command: string
782
+ /**
783
+ * Parsed argument list, when available.
784
+ */
654
785
  args?: readonly string[]
655
786
  }
656
787
 
657
788
  export type KubbHookEndContext = {
789
+ /**
790
+ * Optional identifier matching the corresponding `kubb:hook:start` event.
791
+ */
658
792
  id?: string
793
+ /**
794
+ * The shell command that ran.
795
+ */
659
796
  command: string
797
+ /**
798
+ * Parsed argument list, when available.
799
+ */
660
800
  args?: readonly string[]
801
+ /**
802
+ * `true` when the command exited with code `0`.
803
+ */
661
804
  success: boolean
805
+ /**
806
+ * Error thrown by the command, or `null` on success.
807
+ */
662
808
  error: Error | null
663
809
  }
664
810
 
@@ -666,9 +812,19 @@ export type KubbHookEndContext = {
666
812
  * CLI options derived from command-line flags.
667
813
  */
668
814
  export type CLIOptions = {
815
+ /**
816
+ * Path to the Kubb config file.
817
+ */
669
818
  config?: string
819
+ /**
820
+ * Re-run generation whenever input files change.
821
+ */
670
822
  watch?: boolean
671
- /** @default 'silent' */
823
+ /**
824
+ * Controls how much output the CLI prints.
825
+ *
826
+ * @default 'silent'
827
+ */
672
828
  logLevel?: 'silent' | 'info' | 'debug'
673
829
  }
674
830
 
@@ -689,31 +845,34 @@ type SetupOptions = {
689
845
  */
690
846
  export type BuildOutput = {
691
847
  /**
692
- * Plugins that threw during installation, paired with the caught error.
848
+ * Plugins that threw during generation, paired with their errors.
693
849
  */
694
850
  failedPlugins: Set<{ plugin: Plugin; error: Error }>
851
+ /**
852
+ * All files generated during this build.
853
+ */
695
854
  files: Array<FileNode>
855
+ /**
856
+ * The plugin driver that orchestrated this build.
857
+ */
696
858
  driver: PluginDriver
697
859
  /**
698
- * Elapsed time in milliseconds for each plugin, keyed by plugin name.
860
+ * Elapsed milliseconds per plugin, keyed by plugin name.
699
861
  */
700
862
  pluginTimings: Map<string, number>
863
+ /**
864
+ * Top-level error when the build threw before completing, otherwise `undefined`.
865
+ */
701
866
  error?: Error
702
867
  /**
703
868
  * 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.
869
+ * Reads go straight to `config.storage` — nothing extra is held in memory.
707
870
  *
708
871
  * @example Read a generated file
709
- * ```ts
710
- * const code = await buildOutput.storage.getItem('/src/gen/pet.ts')
711
- * ```
872
+ * `const code = await buildOutput.storage.getItem('/src/gen/pet.ts')`
712
873
  *
713
874
  * @example List all generated file paths
714
- * ```ts
715
- * const paths = await buildOutput.storage.getKeys()
716
- * ```
875
+ * `const paths = await buildOutput.storage.getKeys()`
717
876
  */
718
877
  storage: Storage
719
878
  }
@@ -945,19 +1104,10 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
945
1104
  ` • Operations: ${operationCount}`,
946
1105
  ],
947
1106
  })
948
- } else {
949
- driver.inputNode = await config.adapter.parse(source)
950
-
951
- await hooks.emit('kubb:debug', {
952
- date: new Date(),
953
- logs: [
954
- `✓ Adapter '${config.adapter.name}' resolved InputNode`,
955
- ` • Schemas: ${driver.inputNode.schemas.length}`,
956
- ` • Operations: ${driver.inputNode.operations.length}`,
957
- ],
958
- })
959
1107
  }
960
- } else {
1108
+ }
1109
+
1110
+ if (!driver.inputStreamNode) {
961
1111
  driver.inputNode = await config.adapter.parse(source)
962
1112
 
963
1113
  await hooks.emit('kubb:debug', {
@@ -988,22 +1138,27 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
988
1138
  }
989
1139
  }
990
1140
 
991
- /**
992
- * Walks the AST and dispatches nodes to a plugin's direct AST hooks
993
- * (`schema`, `operation`, `operations`).
994
- *
995
- * When `include` contains only operation-scoped filters (`tag`, `operationId`, `path`,
996
- * `method`, `contentType`) and no `schemaName` filter, the function pre-computes the set
997
- * of top-level schema names transitively reachable from the included operations and skips
998
- * schemas that fall outside that set. This ensures that component schemas referenced
999
- * exclusively by excluded operations are not generated.
1000
- */
1001
1141
  type PluginStreamEntry = {
1002
1142
  plugin: NormalizedPlugin
1003
1143
  context: GeneratorContext
1004
1144
  hrStart: ReturnType<typeof process.hrtime>
1005
1145
  }
1006
1146
 
1147
+ type PluginState = {
1148
+ plugin: NormalizedPlugin
1149
+ generatorContext: GeneratorContext
1150
+ generators: Generator[]
1151
+ hrStart: ReturnType<typeof process.hrtime>
1152
+ failed: boolean
1153
+ error: Error | undefined
1154
+ /**
1155
+ * `true` when the plugin's options have no `include`, `exclude`, or `override`
1156
+ * filters. The per-node `resolveOptions` call always returns the same `options`
1157
+ * reference in that case, so the inner loop can skip it entirely.
1158
+ */
1159
+ optionsAreStatic: boolean
1160
+ }
1161
+
1007
1162
  /**
1008
1163
  * Single-pass fan-out for streaming mode.
1009
1164
  *
@@ -1015,91 +1170,111 @@ type PluginStreamEntry = {
1015
1170
  * Each plugin still gets independent `plugin:start` / `plugin:end` events and its own
1016
1171
  * timing, but the schema and operation nodes are parsed only once total.
1017
1172
  */
1018
- async function runPluginStreamHooks(
1019
- inputStreamNode: InputStreamNode,
1020
- entries: PluginStreamEntry[],
1021
- driver: PluginDriver,
1022
- hooks: AsyncEventEmitter<KubbHooks>,
1023
- config: Config,
1024
- pluginTimings: Map<string, number>,
1025
- failedPlugins: Set<{ plugin: Plugin; error: Error }>,
1026
- flushPendingFiles: () => Promise<void>,
1027
- ): Promise<void> {
1028
- type PluginState = {
1029
- plugin: NormalizedPlugin
1030
- generatorContext: GeneratorContext
1031
- generators: Generator[]
1032
- hrStart: ReturnType<typeof process.hrtime>
1033
- failed: boolean
1034
- error: Error | undefined
1035
- }
1036
-
1173
+ async function runPluginStreamHooks({
1174
+ entries,
1175
+ driver,
1176
+ pluginTimings,
1177
+ failedPlugins,
1178
+ }: {
1179
+ entries: PluginStreamEntry[]
1180
+ driver: PluginDriver
1181
+ pluginTimings: Map<string, number>
1182
+ failedPlugins: Set<{ plugin: Plugin; error: Error }>
1183
+ }): Promise<void> {
1184
+ const inputStreamNode = driver.inputStreamNode!
1037
1185
  function resolveRendererFor(gen: Generator, state: PluginState): RendererFactory | undefined {
1038
1186
  return gen.renderer === null ? undefined : (gen.renderer ?? state.plugin.renderer ?? state.generatorContext.config.renderer)
1039
1187
  }
1040
1188
 
1041
- const states: PluginState[] = entries.map(({ plugin, context, hrStart }) => ({
1042
- plugin,
1043
- generatorContext: { ...context, resolver: driver.getResolver(plugin.name) },
1044
- generators: plugin.generators ?? [],
1045
- hrStart,
1046
- failed: false,
1047
- error: undefined,
1048
- }))
1189
+ const states: PluginState[] = entries.map(({ plugin, context, hrStart }) => {
1190
+ const { exclude, include, override } = plugin.options
1191
+ const hasExclude = Array.isArray(exclude) && exclude.length > 0
1192
+ const hasInclude = Array.isArray(include) && include.length > 0
1193
+ const hasOverride = Array.isArray(override) && override.length > 0
1194
+ return {
1195
+ plugin,
1196
+ generatorContext: { ...context, resolver: driver.getResolver(plugin.name) },
1197
+ generators: plugin.generators ?? [],
1198
+ hrStart,
1199
+ failed: false,
1200
+ error: undefined,
1201
+ optionsAreStatic: !hasExclude && !hasInclude && !hasOverride,
1202
+ }
1203
+ })
1049
1204
 
1050
- let schemasProcessed = 0
1051
- for await (const node of inputStreamNode.schemas) {
1052
- for (const state of states) {
1053
- if (state.failed) continue
1054
- try {
1055
- const { plugin, generatorContext, generators } = state
1056
- const { exclude, include, override } = plugin.options
1057
- const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
1058
- const options = generatorContext.resolver.resolveOptions(transformedNode, { options: plugin.options, exclude, include, override })
1059
- if (options === null) continue
1205
+ async function dispatchSchema(state: PluginState, node: SchemaNode): Promise<void> {
1206
+ if (state.failed) return
1060
1207
 
1061
- const ctx = { ...generatorContext, options }
1062
- for (const gen of generators) {
1063
- if (!gen.schema) continue
1064
- const result = await gen.schema(transformedNode, ctx)
1065
- await applyHookResult(result, driver, resolveRendererFor(gen, state))
1066
- }
1067
- await driver.hooks.emit('kubb:generate:schema', transformedNode, ctx)
1068
- } catch (caughtError) {
1069
- state.failed = true
1070
- state.error = caughtError as Error
1208
+ try {
1209
+ const { plugin, generatorContext, generators } = state
1210
+ const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
1211
+ const { exclude, include, override } = plugin.options
1212
+ const options: typeof plugin.options | null = state.optionsAreStatic
1213
+ ? plugin.options
1214
+ : generatorContext.resolver.resolveOptions(transformedNode, { options: plugin.options, exclude, include, override })
1215
+
1216
+ if (options === null) return
1217
+
1218
+ const ctx = { ...generatorContext, options }
1219
+ for (const gen of generators) {
1220
+ if (!gen.schema) continue
1221
+
1222
+ const raw = gen.schema(transformedNode, ctx)
1223
+ const result = isPromise(raw) ? await raw : raw
1224
+ const applied = applyHookResult({ result, driver, rendererFactory: resolveRendererFor(gen, state) })
1225
+
1226
+ if (isPromise(applied)) await applied
1071
1227
  }
1228
+ await driver.hooks.emit('kubb:generate:schema', transformedNode, ctx)
1229
+ } catch (caughtError) {
1230
+ state.failed = true
1231
+ state.error = caughtError as Error
1072
1232
  }
1073
- schemasProcessed++
1074
- if (schemasProcessed % STREAM_FLUSH_EVERY === 0) {
1075
- await flushPendingFiles()
1233
+ }
1234
+
1235
+ async function dispatchOperation(state: PluginState, node: OperationNode): Promise<void> {
1236
+ if (state.failed) return
1237
+ try {
1238
+ const { plugin, generatorContext, generators } = state
1239
+ const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
1240
+ const { exclude, include, override } = plugin.options
1241
+ const options: typeof plugin.options | null = state.optionsAreStatic
1242
+ ? plugin.options
1243
+ : generatorContext.resolver.resolveOptions(transformedNode, { options: plugin.options, exclude, include, override })
1244
+
1245
+ if (options === null) return
1246
+
1247
+ const ctx = { ...generatorContext, options }
1248
+
1249
+ for (const gen of generators) {
1250
+ if (!gen.operation) continue
1251
+
1252
+ const raw = gen.operation(transformedNode, ctx)
1253
+ const result = isPromise(raw) ? await raw : raw
1254
+ const applied = applyHookResult({ result, driver, rendererFactory: resolveRendererFor(gen, state) })
1255
+
1256
+ if (isPromise(applied)) await applied
1257
+ }
1258
+ await driver.hooks.emit('kubb:generate:operation', transformedNode, ctx)
1259
+ } catch (caughtError) {
1260
+ state.failed = true
1261
+ state.error = caughtError as Error
1076
1262
  }
1077
1263
  }
1078
1264
 
1265
+ for await (const node of inputStreamNode.schemas) {
1266
+ // Plugins are dispatched concurrently; per-plugin work (the inner generator
1267
+ // loop) stays sequential so `FileManager.upsert` ordering for any single
1268
+ // plugin chain remains deterministic.
1269
+ await Promise.all(states.map((state) => dispatchSchema(state, node)))
1270
+ }
1271
+
1079
1272
  const collectedOperations: OperationNode[] = []
1273
+
1080
1274
  for await (const node of inputStreamNode.operations) {
1081
1275
  collectedOperations.push(node)
1082
- for (const state of states) {
1083
- if (state.failed) continue
1084
- try {
1085
- const { plugin, generatorContext, generators } = state
1086
- const { exclude, include, override } = plugin.options
1087
- const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
1088
- const options = generatorContext.resolver.resolveOptions(transformedNode, { options: plugin.options, exclude, include, override })
1089
- if (options === null) continue
1090
1276
 
1091
- const ctx = { ...generatorContext, options }
1092
- for (const gen of generators) {
1093
- if (!gen.operation) continue
1094
- const result = await gen.operation(transformedNode, ctx)
1095
- await applyHookResult(result, driver, resolveRendererFor(gen, state))
1096
- }
1097
- await driver.hooks.emit('kubb:generate:operation', transformedNode, ctx)
1098
- } catch (caughtError) {
1099
- state.failed = true
1100
- state.error = caughtError as Error
1101
- }
1102
- }
1277
+ await Promise.all(states.map((state) => dispatchOperation(state, node)))
1103
1278
  }
1104
1279
 
1105
1280
  // After stream: gen.operations for each plugin, then emit plugin:end
@@ -1108,11 +1283,13 @@ async function runPluginStreamHooks(
1108
1283
  try {
1109
1284
  const { plugin, generatorContext, generators } = state
1110
1285
  const ctx = { ...generatorContext, options: plugin.options }
1286
+
1111
1287
  for (const gen of generators) {
1112
1288
  if (!gen.operations) continue
1113
1289
  const result = await gen.operations(collectedOperations, ctx)
1114
- await applyHookResult(result, driver, resolveRendererFor(gen, state))
1290
+ await applyHookResult({ result, driver, rendererFactory: resolveRendererFor(gen, state) })
1115
1291
  }
1292
+
1116
1293
  await driver.hooks.emit('kubb:generate:operations', collectedOperations, ctx)
1117
1294
  } catch (caughtError) {
1118
1295
  state.failed = true
@@ -1123,12 +1300,12 @@ async function runPluginStreamHooks(
1123
1300
  const duration = getElapsedMs(state.hrStart)
1124
1301
  pluginTimings.set(state.plugin.name, duration)
1125
1302
 
1126
- await hooks.emit('kubb:plugin:end', {
1303
+ await driver.hooks.emit('kubb:plugin:end', {
1127
1304
  plugin: state.plugin,
1128
1305
  duration,
1129
1306
  success: !state.failed,
1130
1307
  ...(state.failed && state.error ? { error: state.error } : {}),
1131
- config,
1308
+ config: driver.config,
1132
1309
  get files() {
1133
1310
  return driver.fileManager.files
1134
1311
  },
@@ -1139,15 +1316,23 @@ async function runPluginStreamHooks(
1139
1316
  failedPlugins.add({ plugin: state.plugin, error: state.error })
1140
1317
  }
1141
1318
 
1142
- await hooks.emit('kubb:debug', {
1319
+ await driver.hooks.emit('kubb:debug', {
1143
1320
  date: new Date(),
1144
1321
  logs: [state.failed ? '✗ Plugin start failed' : `✓ Plugin started successfully (${formatMs(duration)})`],
1145
1322
  })
1146
1323
  }
1147
-
1148
- await flushPendingFiles()
1149
1324
  }
1150
1325
 
1326
+ /**
1327
+ * Walks the AST and dispatches nodes to a plugin's direct AST hooks
1328
+ * (`schema`, `operation`, `operations`).
1329
+ *
1330
+ * When `include` contains only operation-scoped filters (`tag`, `operationId`, `path`,
1331
+ * `method`, `contentType`) and no `schemaName` filter, the function pre-computes the set
1332
+ * of top-level schema names transitively reachable from the included operations and skips
1333
+ * schemas that fall outside that set. This ensures that component schemas referenced
1334
+ * exclusively by excluded operations are not generated.
1335
+ */
1151
1336
  async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorContext): Promise<void> {
1152
1337
  const { adapter, inputNode, resolver, driver } = context
1153
1338
  const { exclude, include, override } = plugin.options
@@ -1168,7 +1353,6 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
1168
1353
  resolver: driver.getResolver(plugin.name),
1169
1354
  }
1170
1355
 
1171
- // ── BATCH PATH ────────────────────────────────────────────────────────────
1172
1356
  // When `include` has operation-based filters (tag, operationId, path, method, contentType)
1173
1357
  // but no schema-level filters (schemaName), pre-compute the set of top-level schema names
1174
1358
  // that are transitively referenced by the included operations. Schemas outside that set are
@@ -1177,11 +1361,11 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
1177
1361
  const hasOperationBasedIncludes = include?.some(({ type }) => operationFilterTypes.has(type)) ?? false
1178
1362
  const hasSchemaNameIncludes = include?.some(({ type }) => type === 'schemaName') ?? false
1179
1363
 
1180
- let allowedSchemaNames: Set<string> | undefined
1181
- if (hasOperationBasedIncludes && !hasSchemaNameIncludes) {
1364
+ const allowedSchemaNames: Set<string> | undefined = (() => {
1365
+ if (!hasOperationBasedIncludes || hasSchemaNameIncludes) return undefined
1182
1366
  const includedOps = inputNode!.operations.filter((op) => resolver.resolveOptions(op, { options: plugin.options, exclude, include, override }) !== null)
1183
- allowedSchemaNames = collectUsedSchemaNames(includedOps, inputNode!.schemas)
1184
- }
1367
+ return collectUsedSchemaNames(includedOps, inputNode!.schemas)
1368
+ })()
1185
1369
 
1186
1370
  await walk(inputNode!, {
1187
1371
  depth: 'shallow',
@@ -1208,7 +1392,7 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
1208
1392
  .filter((gen) => gen.schema)
1209
1393
  .map(async (gen) => {
1210
1394
  const result = await gen.schema!(transformedNode, ctx)
1211
- return applyHookResult(result, driver, resolveRenderer(gen))
1395
+ return applyHookResult({ result, driver, rendererFactory: resolveRenderer(gen) })
1212
1396
  }),
1213
1397
  )
1214
1398
 
@@ -1222,22 +1406,22 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
1222
1406
  include,
1223
1407
  override,
1224
1408
  })
1225
- if (options !== null) {
1226
- collectedOperations.push(transformedNode)
1409
+ if (options === null) return
1227
1410
 
1228
- const ctx = { ...generatorContext, options }
1411
+ collectedOperations.push(transformedNode)
1229
1412
 
1230
- await Promise.all(
1231
- generators
1232
- .filter((gen) => gen.operation)
1233
- .map(async (gen) => {
1234
- const result = await gen.operation!(transformedNode, ctx)
1235
- return applyHookResult(result, driver, resolveRenderer(gen))
1236
- }),
1237
- )
1413
+ const ctx = { ...generatorContext, options }
1238
1414
 
1239
- await driver.hooks.emit('kubb:generate:operation', transformedNode, ctx)
1240
- }
1415
+ await Promise.all(
1416
+ generators
1417
+ .filter((gen) => gen.operation)
1418
+ .map(async (gen) => {
1419
+ const result = await gen.operation!(transformedNode, ctx)
1420
+ return applyHookResult({ result, driver, rendererFactory: resolveRenderer(gen) })
1421
+ }),
1422
+ )
1423
+
1424
+ await driver.hooks.emit('kubb:generate:operation', transformedNode, ctx)
1241
1425
  },
1242
1426
  })
1243
1427
 
@@ -1247,7 +1431,7 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
1247
1431
  for (const gen of generators) {
1248
1432
  if (!gen.operations) continue
1249
1433
  const result = await gen.operations(collectedOperations, ctx)
1250
- await applyHookResult(result, driver, resolveRenderer(gen))
1434
+ await applyHookResult({ result, driver, rendererFactory: resolveRenderer(gen) })
1251
1435
  }
1252
1436
 
1253
1437
  await driver.hooks.emit('kubb:generate:operations', collectedOperations, ctx)
@@ -1272,8 +1456,8 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1272
1456
  }
1273
1457
  const fileProcessor = new FileProcessor()
1274
1458
 
1275
- async function flushPendingFiles(snapshot?: ReadonlySet<string>): Promise<void> {
1276
- const files = driver.fileManager.files.filter((f) => !writtenPaths.has(f.path) && (!snapshot || !snapshot.has(f.path)))
1459
+ async function flushPendingFiles(): Promise<void> {
1460
+ const files = driver.fileManager.files.filter((f) => !writtenPaths.has(f.path))
1277
1461
  if (files.length === 0) {
1278
1462
  return
1279
1463
  }
@@ -1287,7 +1471,7 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1287
1471
 
1288
1472
  const stream = fileProcessor.stream(files, { parsers: parsersMap, extension: config.output.extension })
1289
1473
 
1290
- for await (const { file, source, processed, total, percentage } of stream) {
1474
+ for (const { file, source, processed, total, percentage } of stream) {
1291
1475
  await hooks.emit('kubb:file:processing:update', { file, source, processed, total, percentage, config })
1292
1476
  if (source) {
1293
1477
  await storage.setItem(file.path, source)
@@ -1320,7 +1504,6 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1320
1504
 
1321
1505
  const inputStreamNode = driver.inputStreamNode
1322
1506
  if (inputStreamNode) {
1323
- // ── STREAMING: fan-out single-pass ────────────────────────────────────
1324
1507
  // Emit plugin:start for all plugins up front, collect generator-plugins
1325
1508
  // for the fan-out pass, then handle non-generator plugins immediately.
1326
1509
  const streamPluginEntries: PluginStreamEntry[] = []
@@ -1337,32 +1520,32 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1337
1520
 
1338
1521
  if (plugin.generators?.length || driver.hasRegisteredGenerators(plugin.name)) {
1339
1522
  streamPluginEntries.push({ plugin, context, hrStart })
1340
- } else {
1341
- // No generators: plugin ran via setup hooks; finish it now.
1342
- const duration = getElapsedMs(hrStart)
1343
- pluginTimings.set(plugin.name, duration)
1344
- await hooks.emit('kubb:plugin:end', {
1345
- plugin,
1346
- duration,
1347
- success: true,
1348
- config,
1349
- get files() {
1350
- return driver.fileManager.files
1351
- },
1352
- upsertFile: (...files) => driver.fileManager.upsert(...files),
1353
- })
1354
- await hooks.emit('kubb:debug', {
1355
- date: new Date(),
1356
- logs: [`✓ Plugin started successfully (${formatMs(duration)})`],
1357
- })
1523
+ continue
1358
1524
  }
1525
+ // No generators: plugin ran via setup hooks; finish it now.
1526
+ const duration = getElapsedMs(hrStart)
1527
+ pluginTimings.set(plugin.name, duration)
1528
+ await hooks.emit('kubb:plugin:end', {
1529
+ plugin,
1530
+ duration,
1531
+ success: true,
1532
+ config,
1533
+ get files() {
1534
+ return driver.fileManager.files
1535
+ },
1536
+ upsertFile: (...files) => driver.fileManager.upsert(...files),
1537
+ })
1538
+ await hooks.emit('kubb:debug', {
1539
+ date: new Date(),
1540
+ logs: [`✓ Plugin started successfully (${formatMs(duration)})`],
1541
+ })
1359
1542
  }
1360
1543
 
1361
1544
  if (streamPluginEntries.length > 0) {
1362
- await runPluginStreamHooks(inputStreamNode, streamPluginEntries, driver, hooks, config, pluginTimings, failedPlugins, flushPendingFiles)
1545
+ await runPluginStreamHooks({ entries: streamPluginEntries, driver, pluginTimings, failedPlugins })
1546
+ await flushPendingFiles()
1363
1547
  }
1364
1548
  } else {
1365
- // ── BATCH: existing per-plugin sequential loop ────────────────────────
1366
1549
  for (const plugin of driver.plugins.values()) {
1367
1550
  const context = driver.getContext(plugin)
1368
1551
  const hrStart = process.hrtime()