@kubb/core 5.0.0-beta.17 → 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
  }
@@ -778,6 +937,7 @@ type SetupResult = {
778
937
  storage: Storage
779
938
  config: Config
780
939
  dispose: () => void
940
+ [Symbol.dispose](): void
781
941
  }
782
942
 
783
943
  /**
@@ -944,19 +1104,10 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
944
1104
  ` • Operations: ${operationCount}`,
945
1105
  ],
946
1106
  })
947
- } else {
948
- driver.inputNode = await config.adapter.parse(source)
949
-
950
- await hooks.emit('kubb:debug', {
951
- date: new Date(),
952
- logs: [
953
- `✓ Adapter '${config.adapter.name}' resolved InputNode`,
954
- ` • Schemas: ${driver.inputNode.schemas.length}`,
955
- ` • Operations: ${driver.inputNode.operations.length}`,
956
- ],
957
- })
958
1107
  }
959
- } else {
1108
+ }
1109
+
1110
+ if (!driver.inputStreamNode) {
960
1111
  driver.inputNode = await config.adapter.parse(source)
961
1112
 
962
1113
  await hooks.emit('kubb:debug', {
@@ -975,31 +1126,39 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
975
1126
  hooks,
976
1127
  driver,
977
1128
  storage,
978
- dispose: () => {
979
- driver.dispose()
980
- for (const [event, handler] of middlewareListeners) {
981
- hooks.off(event, handler as never)
982
- }
983
- },
1129
+ dispose,
1130
+ [Symbol.dispose]: dispose,
1131
+ }
1132
+
1133
+ function dispose() {
1134
+ driver.dispose()
1135
+ for (const [event, handler] of middlewareListeners) {
1136
+ hooks.off(event, handler as never)
1137
+ }
984
1138
  }
985
1139
  }
986
1140
 
987
- /**
988
- * Walks the AST and dispatches nodes to a plugin's direct AST hooks
989
- * (`schema`, `operation`, `operations`).
990
- *
991
- * When `include` contains only operation-scoped filters (`tag`, `operationId`, `path`,
992
- * `method`, `contentType`) and no `schemaName` filter, the function pre-computes the set
993
- * of top-level schema names transitively reachable from the included operations and skips
994
- * schemas that fall outside that set. This ensures that component schemas referenced
995
- * exclusively by excluded operations are not generated.
996
- */
997
1141
  type PluginStreamEntry = {
998
1142
  plugin: NormalizedPlugin
999
1143
  context: GeneratorContext
1000
1144
  hrStart: ReturnType<typeof process.hrtime>
1001
1145
  }
1002
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
+
1003
1162
  /**
1004
1163
  * Single-pass fan-out for streaming mode.
1005
1164
  *
@@ -1011,91 +1170,111 @@ type PluginStreamEntry = {
1011
1170
  * Each plugin still gets independent `plugin:start` / `plugin:end` events and its own
1012
1171
  * timing, but the schema and operation nodes are parsed only once total.
1013
1172
  */
1014
- async function runPluginStreamHooks(
1015
- inputStreamNode: InputStreamNode,
1016
- entries: PluginStreamEntry[],
1017
- driver: PluginDriver,
1018
- hooks: AsyncEventEmitter<KubbHooks>,
1019
- config: Config,
1020
- pluginTimings: Map<string, number>,
1021
- failedPlugins: Set<{ plugin: Plugin; error: Error }>,
1022
- flushPendingFiles: () => Promise<void>,
1023
- ): Promise<void> {
1024
- type PluginState = {
1025
- plugin: NormalizedPlugin
1026
- generatorContext: GeneratorContext
1027
- generators: Generator[]
1028
- hrStart: ReturnType<typeof process.hrtime>
1029
- failed: boolean
1030
- error: Error | undefined
1031
- }
1032
-
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!
1033
1185
  function resolveRendererFor(gen: Generator, state: PluginState): RendererFactory | undefined {
1034
1186
  return gen.renderer === null ? undefined : (gen.renderer ?? state.plugin.renderer ?? state.generatorContext.config.renderer)
1035
1187
  }
1036
1188
 
1037
- const states: PluginState[] = entries.map(({ plugin, context, hrStart }) => ({
1038
- plugin,
1039
- generatorContext: { ...context, resolver: driver.getResolver(plugin.name) },
1040
- generators: plugin.generators ?? [],
1041
- hrStart,
1042
- failed: false,
1043
- error: undefined,
1044
- }))
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
+ })
1045
1204
 
1046
- let schemasProcessed = 0
1047
- for await (const node of inputStreamNode.schemas) {
1048
- for (const state of states) {
1049
- if (state.failed) continue
1050
- try {
1051
- const { plugin, generatorContext, generators } = state
1052
- const { exclude, include, override } = plugin.options
1053
- const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
1054
- const options = generatorContext.resolver.resolveOptions(transformedNode, { options: plugin.options, exclude, include, override })
1055
- if (options === null) continue
1205
+ async function dispatchSchema(state: PluginState, node: SchemaNode): Promise<void> {
1206
+ if (state.failed) return
1056
1207
 
1057
- const ctx = { ...generatorContext, options }
1058
- for (const gen of generators) {
1059
- if (!gen.schema) continue
1060
- const result = await gen.schema(transformedNode, ctx)
1061
- await applyHookResult(result, driver, resolveRendererFor(gen, state))
1062
- }
1063
- await driver.hooks.emit('kubb:generate:schema', transformedNode, ctx)
1064
- } catch (caughtError) {
1065
- state.failed = true
1066
- 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
1067
1227
  }
1228
+ await driver.hooks.emit('kubb:generate:schema', transformedNode, ctx)
1229
+ } catch (caughtError) {
1230
+ state.failed = true
1231
+ state.error = caughtError as Error
1068
1232
  }
1069
- schemasProcessed++
1070
- if (schemasProcessed % STREAM_FLUSH_EVERY === 0) {
1071
- 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
1072
1262
  }
1073
1263
  }
1074
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
+
1075
1272
  const collectedOperations: OperationNode[] = []
1273
+
1076
1274
  for await (const node of inputStreamNode.operations) {
1077
1275
  collectedOperations.push(node)
1078
- for (const state of states) {
1079
- if (state.failed) continue
1080
- try {
1081
- const { plugin, generatorContext, generators } = state
1082
- const { exclude, include, override } = plugin.options
1083
- const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
1084
- const options = generatorContext.resolver.resolveOptions(transformedNode, { options: plugin.options, exclude, include, override })
1085
- if (options === null) continue
1086
1276
 
1087
- const ctx = { ...generatorContext, options }
1088
- for (const gen of generators) {
1089
- if (!gen.operation) continue
1090
- const result = await gen.operation(transformedNode, ctx)
1091
- await applyHookResult(result, driver, resolveRendererFor(gen, state))
1092
- }
1093
- await driver.hooks.emit('kubb:generate:operation', transformedNode, ctx)
1094
- } catch (caughtError) {
1095
- state.failed = true
1096
- state.error = caughtError as Error
1097
- }
1098
- }
1277
+ await Promise.all(states.map((state) => dispatchOperation(state, node)))
1099
1278
  }
1100
1279
 
1101
1280
  // After stream: gen.operations for each plugin, then emit plugin:end
@@ -1104,11 +1283,13 @@ async function runPluginStreamHooks(
1104
1283
  try {
1105
1284
  const { plugin, generatorContext, generators } = state
1106
1285
  const ctx = { ...generatorContext, options: plugin.options }
1286
+
1107
1287
  for (const gen of generators) {
1108
1288
  if (!gen.operations) continue
1109
1289
  const result = await gen.operations(collectedOperations, ctx)
1110
- await applyHookResult(result, driver, resolveRendererFor(gen, state))
1290
+ await applyHookResult({ result, driver, rendererFactory: resolveRendererFor(gen, state) })
1111
1291
  }
1292
+
1112
1293
  await driver.hooks.emit('kubb:generate:operations', collectedOperations, ctx)
1113
1294
  } catch (caughtError) {
1114
1295
  state.failed = true
@@ -1119,12 +1300,12 @@ async function runPluginStreamHooks(
1119
1300
  const duration = getElapsedMs(state.hrStart)
1120
1301
  pluginTimings.set(state.plugin.name, duration)
1121
1302
 
1122
- await hooks.emit('kubb:plugin:end', {
1303
+ await driver.hooks.emit('kubb:plugin:end', {
1123
1304
  plugin: state.plugin,
1124
1305
  duration,
1125
1306
  success: !state.failed,
1126
1307
  ...(state.failed && state.error ? { error: state.error } : {}),
1127
- config,
1308
+ config: driver.config,
1128
1309
  get files() {
1129
1310
  return driver.fileManager.files
1130
1311
  },
@@ -1135,15 +1316,23 @@ async function runPluginStreamHooks(
1135
1316
  failedPlugins.add({ plugin: state.plugin, error: state.error })
1136
1317
  }
1137
1318
 
1138
- await hooks.emit('kubb:debug', {
1319
+ await driver.hooks.emit('kubb:debug', {
1139
1320
  date: new Date(),
1140
1321
  logs: [state.failed ? '✗ Plugin start failed' : `✓ Plugin started successfully (${formatMs(duration)})`],
1141
1322
  })
1142
1323
  }
1143
-
1144
- await flushPendingFiles()
1145
1324
  }
1146
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
+ */
1147
1336
  async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorContext): Promise<void> {
1148
1337
  const { adapter, inputNode, resolver, driver } = context
1149
1338
  const { exclude, include, override } = plugin.options
@@ -1164,7 +1353,6 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
1164
1353
  resolver: driver.getResolver(plugin.name),
1165
1354
  }
1166
1355
 
1167
- // ── BATCH PATH ────────────────────────────────────────────────────────────
1168
1356
  // When `include` has operation-based filters (tag, operationId, path, method, contentType)
1169
1357
  // but no schema-level filters (schemaName), pre-compute the set of top-level schema names
1170
1358
  // that are transitively referenced by the included operations. Schemas outside that set are
@@ -1173,11 +1361,11 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
1173
1361
  const hasOperationBasedIncludes = include?.some(({ type }) => operationFilterTypes.has(type)) ?? false
1174
1362
  const hasSchemaNameIncludes = include?.some(({ type }) => type === 'schemaName') ?? false
1175
1363
 
1176
- let allowedSchemaNames: Set<string> | undefined
1177
- if (hasOperationBasedIncludes && !hasSchemaNameIncludes) {
1364
+ const allowedSchemaNames: Set<string> | undefined = (() => {
1365
+ if (!hasOperationBasedIncludes || hasSchemaNameIncludes) return undefined
1178
1366
  const includedOps = inputNode!.operations.filter((op) => resolver.resolveOptions(op, { options: plugin.options, exclude, include, override }) !== null)
1179
- allowedSchemaNames = collectUsedSchemaNames(includedOps, inputNode!.schemas)
1180
- }
1367
+ return collectUsedSchemaNames(includedOps, inputNode!.schemas)
1368
+ })()
1181
1369
 
1182
1370
  await walk(inputNode!, {
1183
1371
  depth: 'shallow',
@@ -1202,7 +1390,10 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
1202
1390
  await Promise.all(
1203
1391
  generators
1204
1392
  .filter((gen) => gen.schema)
1205
- .map((gen) => Promise.resolve(gen.schema!(transformedNode, ctx)).then((result) => applyHookResult(result, driver, resolveRenderer(gen)))),
1393
+ .map(async (gen) => {
1394
+ const result = await gen.schema!(transformedNode, ctx)
1395
+ return applyHookResult({ result, driver, rendererFactory: resolveRenderer(gen) })
1396
+ }),
1206
1397
  )
1207
1398
 
1208
1399
  await driver.hooks.emit('kubb:generate:schema', transformedNode, ctx)
@@ -1215,19 +1406,22 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
1215
1406
  include,
1216
1407
  override,
1217
1408
  })
1218
- if (options !== null) {
1219
- collectedOperations.push(transformedNode)
1409
+ if (options === null) return
1220
1410
 
1221
- const ctx = { ...generatorContext, options }
1411
+ collectedOperations.push(transformedNode)
1222
1412
 
1223
- await Promise.all(
1224
- generators
1225
- .filter((gen) => gen.operation)
1226
- .map((gen) => Promise.resolve(gen.operation!(transformedNode, ctx)).then((result) => applyHookResult(result, driver, resolveRenderer(gen)))),
1227
- )
1413
+ const ctx = { ...generatorContext, options }
1228
1414
 
1229
- await driver.hooks.emit('kubb:generate:operation', transformedNode, ctx)
1230
- }
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)
1231
1425
  },
1232
1426
  })
1233
1427
 
@@ -1237,7 +1431,7 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
1237
1431
  for (const gen of generators) {
1238
1432
  if (!gen.operations) continue
1239
1433
  const result = await gen.operations(collectedOperations, ctx)
1240
- await applyHookResult(result, driver, resolveRenderer(gen))
1434
+ await applyHookResult({ result, driver, rendererFactory: resolveRenderer(gen) })
1241
1435
  }
1242
1436
 
1243
1437
  await driver.hooks.emit('kubb:generate:operations', collectedOperations, ctx)
@@ -1245,6 +1439,7 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
1245
1439
  }
1246
1440
 
1247
1441
  async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1442
+ using _cleanup = setupResult
1248
1443
  const { driver, hooks, storage } = setupResult
1249
1444
 
1250
1445
  const failedPlugins = new Set<{ plugin: Plugin; error: Error }>()
@@ -1261,8 +1456,8 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1261
1456
  }
1262
1457
  const fileProcessor = new FileProcessor()
1263
1458
 
1264
- async function flushPendingFiles(snapshot?: ReadonlySet<string>): Promise<void> {
1265
- 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))
1266
1461
  if (files.length === 0) {
1267
1462
  return
1268
1463
  }
@@ -1276,7 +1471,7 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1276
1471
 
1277
1472
  const stream = fileProcessor.stream(files, { parsers: parsersMap, extension: config.output.extension })
1278
1473
 
1279
- for await (const { file, source, processed, total, percentage } of stream) {
1474
+ for (const { file, source, processed, total, percentage } of stream) {
1280
1475
  await hooks.emit('kubb:file:processing:update', { file, source, processed, total, percentage, config })
1281
1476
  if (source) {
1282
1477
  await storage.setItem(file.path, source)
@@ -1309,7 +1504,6 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1309
1504
 
1310
1505
  const inputStreamNode = driver.inputStreamNode
1311
1506
  if (inputStreamNode) {
1312
- // ── STREAMING: fan-out single-pass ────────────────────────────────────
1313
1507
  // Emit plugin:start for all plugins up front, collect generator-plugins
1314
1508
  // for the fan-out pass, then handle non-generator plugins immediately.
1315
1509
  const streamPluginEntries: PluginStreamEntry[] = []
@@ -1326,32 +1520,32 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1326
1520
 
1327
1521
  if (plugin.generators?.length || driver.hasRegisteredGenerators(plugin.name)) {
1328
1522
  streamPluginEntries.push({ plugin, context, hrStart })
1329
- } else {
1330
- // No generators: plugin ran via setup hooks; finish it now.
1331
- const duration = getElapsedMs(hrStart)
1332
- pluginTimings.set(plugin.name, duration)
1333
- await hooks.emit('kubb:plugin:end', {
1334
- plugin,
1335
- duration,
1336
- success: true,
1337
- config,
1338
- get files() {
1339
- return driver.fileManager.files
1340
- },
1341
- upsertFile: (...files) => driver.fileManager.upsert(...files),
1342
- })
1343
- await hooks.emit('kubb:debug', {
1344
- date: new Date(),
1345
- logs: [`✓ Plugin started successfully (${formatMs(duration)})`],
1346
- })
1523
+ continue
1347
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
+ })
1348
1542
  }
1349
1543
 
1350
1544
  if (streamPluginEntries.length > 0) {
1351
- await runPluginStreamHooks(inputStreamNode, streamPluginEntries, driver, hooks, config, pluginTimings, failedPlugins, flushPendingFiles)
1545
+ await runPluginStreamHooks({ entries: streamPluginEntries, driver, pluginTimings, failedPlugins })
1546
+ await flushPendingFiles()
1352
1547
  }
1353
1548
  } else {
1354
- // ── BATCH: existing per-plugin sequential loop ────────────────────────
1355
1549
  for (const plugin of driver.plugins.values()) {
1356
1550
  const context = driver.getContext(plugin)
1357
1551
  const hrStart = process.hrtime()
@@ -1455,8 +1649,6 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1455
1649
  error: error as Error,
1456
1650
  storage,
1457
1651
  }
1458
- } finally {
1459
- setupResult.dispose()
1460
1652
  }
1461
1653
  }
1462
1654