@kubb/core 5.0.0-beta.10 → 5.0.0-beta.11

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/dist/mocks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./chunk--u3MIqq1.js";
2
- import { n as applyHookResult, r as FileManager, t as PluginDriver } from "./PluginDriver-D8Z0Htid.js";
2
+ import { n as applyHookResult, r as FileManager, t as PluginDriver } from "./PluginDriver-CGypdXHg.js";
3
3
  import { resolve } from "node:path";
4
4
  import { transform } from "@kubb/ast";
5
5
  //#region src/mocks.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/core",
3
- "version": "5.0.0-beta.10",
3
+ "version": "5.0.0-beta.11",
4
4
  "description": "Core engine for Kubb's plugin-based code generation system. Provides the plugin driver, file manager, defineConfig, and build orchestration used by every Kubb plugin.",
5
5
  "keywords": [
6
6
  "code-generator",
@@ -58,15 +58,15 @@
58
58
  "dependencies": {
59
59
  "fflate": "^0.8.2",
60
60
  "tinyexec": "^1.1.2",
61
- "@kubb/ast": "5.0.0-beta.10"
61
+ "@kubb/ast": "5.0.0-beta.11"
62
62
  },
63
63
  "devDependencies": {
64
64
  "p-limit": "^7.3.0",
65
65
  "@internals/utils": "0.0.0",
66
- "@kubb/renderer-jsx": "5.0.0-beta.10"
66
+ "@kubb/renderer-jsx": "5.0.0-beta.11"
67
67
  },
68
68
  "peerDependencies": {
69
- "@kubb/renderer-jsx": "5.0.0-beta.10"
69
+ "@kubb/renderer-jsx": "5.0.0-beta.11"
70
70
  },
71
71
  "size-limit": [
72
72
  {
@@ -91,6 +91,14 @@ export class FileManager {
91
91
  this.#filesCache = null
92
92
  }
93
93
 
94
+ /**
95
+ * Releases all stored files. Called by the core after `kubb:build:end` to
96
+ * free the per-plugin FileNode caches for the rest of the process lifetime.
97
+ */
98
+ dispose(): void {
99
+ this.clear()
100
+ }
101
+
94
102
  /**
95
103
  * All stored files, sorted by path length (shorter paths first).
96
104
  */
@@ -1,5 +1,6 @@
1
1
  import type { CodeNode, FileNode } from '@kubb/ast'
2
2
  import { extractStringsFromNodes } from '@kubb/ast'
3
+ import { AsyncEventEmitter } from '@internals/utils'
3
4
  import pLimit from 'p-limit'
4
5
  import { PARALLEL_CONCURRENCY_LIMIT } from './constants.ts'
5
6
  import type { Parser } from './defineParser.ts'
@@ -14,9 +15,12 @@ type RunOptions = ParseOptions & {
14
15
  * @default 'sequential'
15
16
  */
16
17
  mode?: 'sequential' | 'parallel'
17
- onStart?: (files: Array<FileNode>) => Promise<void> | void
18
- onEnd?: (files: Array<FileNode>) => Promise<void> | void
19
- onUpdate?: (params: { file: FileNode; source?: string; processed: number; total: number; percentage: number }) => Promise<void> | void
18
+ }
19
+
20
+ export type FileProcessorEvents = {
21
+ start: [files: Array<FileNode>]
22
+ update: [params: { file: FileNode; source?: string; processed: number; total: number; percentage: number }]
23
+ end: [files: Array<FileNode>]
20
24
  }
21
25
 
22
26
  function joinSources(file: FileNode): string {
@@ -33,6 +37,7 @@ function joinSources(file: FileNode): string {
33
37
  * @internal
34
38
  */
35
39
  export class FileProcessor {
40
+ readonly events = new AsyncEventEmitter<FileProcessorEvents>()
36
41
  readonly #limit = pLimit(PARALLEL_CONCURRENCY_LIMIT)
37
42
 
38
43
  async parse(file: FileNode, { parsers, extension }: ParseOptions = {}): Promise<string> {
@@ -51,8 +56,8 @@ export class FileProcessor {
51
56
  return parser.parse(file, { extname: parseExtName })
52
57
  }
53
58
 
54
- async run(files: Array<FileNode>, { parsers, mode = 'sequential', extension, onStart, onEnd, onUpdate }: RunOptions = {}): Promise<Array<FileNode>> {
55
- await onStart?.(files)
59
+ async run(files: Array<FileNode>, { parsers, mode = 'sequential', extension }: RunOptions = {}): Promise<Array<FileNode>> {
60
+ await this.events.emit('start', files)
56
61
 
57
62
  const total = files.length
58
63
  let processed = 0
@@ -62,7 +67,7 @@ export class FileProcessor {
62
67
  const currentProcessed = ++processed
63
68
  const percentage = (currentProcessed / total) * 100
64
69
 
65
- await onUpdate?.({
70
+ await this.events.emit('update', {
66
71
  file,
67
72
  source,
68
73
  processed: currentProcessed,
@@ -79,7 +84,7 @@ export class FileProcessor {
79
84
  await Promise.all(files.map((file) => this.#limit(() => processOne(file))))
80
85
  }
81
86
 
82
- await onEnd?.(files)
87
+ await this.events.emit('end', files)
83
88
 
84
89
  return files
85
90
  }
@@ -284,6 +284,15 @@ export class PluginDriver {
284
284
  }
285
285
  this.#hookListeners.clear()
286
286
  this.#pluginsWithEventGenerators.clear()
287
+ // Release resolver closures — the driver is rebuilt for each build() call
288
+ // so there is no value in retaining these maps after disposal.
289
+ this.#resolvers.clear()
290
+ this.#defaultResolvers.clear()
291
+ // Release the parsed adapter graph and the FileNode cache once the build
292
+ // has finished; the returned `BuildOutput.files` array still references
293
+ // any FileNodes the caller needs to inspect.
294
+ this.fileManager.dispose()
295
+ this.inputNode = undefined
287
296
  }
288
297
 
289
298
  #trackHookListener(event: keyof KubbHooks, handler: (...args: never[]) => void | Promise<void>): void {
package/src/constants.ts CHANGED
@@ -7,8 +7,12 @@ export const DEFAULT_STUDIO_URL = 'https://kubb.studio' as const
7
7
 
8
8
  /**
9
9
  * Maximum number of files processed in parallel by FileProcessor.
10
+ *
11
+ * Capped at 16 to bound the number of CodeNode trees that are alive simultaneously
12
+ * during rendering; I/O latency is the real bottleneck so higher values offer no
13
+ * meaningful throughput improvement.
10
14
  */
11
- export const PARALLEL_CONCURRENCY_LIMIT = 100
15
+ export const PARALLEL_CONCURRENCY_LIMIT = 16
12
16
 
13
17
  /**
14
18
  * Default banner style written at the top of every generated file.
package/src/createKubb.ts CHANGED
@@ -8,7 +8,7 @@ import { version as KubbVersion } from '../package.json'
8
8
  import { DEFAULT_BANNER, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL } from './constants.ts'
9
9
  import type { Adapter, AdapterSource } from './createAdapter.ts'
10
10
  import type { RendererFactory } from './createRenderer.ts'
11
- import type { Storage } from './createStorage.ts'
11
+ import { createStorage, type Storage } from './createStorage.ts'
12
12
  import type { GeneratorContext, Generator } from './defineGenerator.ts'
13
13
  import type { Middleware } from './defineMiddleware.ts'
14
14
  import type { Parser } from './defineParser.ts'
@@ -570,8 +570,25 @@ export type KubbGenerationStartContext = {
570
570
 
571
571
  export type KubbGenerationEndContext = {
572
572
  config: Config
573
- files: Array<FileNode>
574
- sources: Map<string, string>
573
+ /**
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.
578
+ *
579
+ * @example Read a generated file
580
+ * ```ts
581
+ * const code = await storage.getItem('/src/gen/pet.ts')
582
+ * ```
583
+ *
584
+ * @example Walk every generated file
585
+ * ```ts
586
+ * for (const path of await storage.getKeys()) {
587
+ * const code = await storage.getItem(path)
588
+ * }
589
+ * ```
590
+ */
591
+ storage: Storage
575
592
  }
576
593
 
577
594
  export type KubbGenerationSummaryContext = {
@@ -683,9 +700,22 @@ export type BuildOutput = {
683
700
  pluginTimings: Map<string, number>
684
701
  error?: Error
685
702
  /**
686
- * Raw generated source, keyed by absolute file path.
703
+ * 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.
707
+ *
708
+ * @example Read a generated file
709
+ * ```ts
710
+ * const code = await buildOutput.storage.getItem('/src/gen/pet.ts')
711
+ * ```
712
+ *
713
+ * @example List all generated file paths
714
+ * ```ts
715
+ * const paths = await buildOutput.storage.getKeys()
716
+ * ```
687
717
  */
688
- sources: Map<string, string>
718
+ storage: Storage
689
719
  }
690
720
 
691
721
  /**
@@ -700,17 +730,34 @@ export type Kubb = {
700
730
  */
701
731
  readonly hooks: AsyncEventEmitter<KubbHooks>
702
732
  /**
703
- * Generated source code keyed by absolute file path. Available after `build()` or `safeBuild()` completes.
733
+ * Read-only view of the files from the most recent `build()` or `safeBuild()` call.
734
+ * Only populated after the build completes.
735
+ *
736
+ * Keys are scoped to the current run. Reads go straight to `config.storage`,
737
+ * so nothing extra is held in memory.
738
+ *
739
+ * @example Read a generated file
740
+ * ```ts
741
+ * const { storage } = await kubb.safeBuild()
742
+ * const code = await storage.getItem('/src/gen/pet.ts')
743
+ * ```
744
+ *
745
+ * @example Walk every generated file
746
+ * ```ts
747
+ * for (const path of await kubb.storage.getKeys()) {
748
+ * const code = await kubb.storage.getItem(path)
749
+ * }
750
+ * ```
704
751
  */
705
- readonly sources: Map<string, string>
752
+ readonly storage: Storage
706
753
  /**
707
754
  * Plugin driver managing all plugins. Available after `setup()` completes.
708
755
  */
709
- readonly driver: PluginDriver | undefined
756
+ readonly driver: PluginDriver
710
757
  /**
711
758
  * Resolved configuration with defaults applied. Available after `setup()` completes.
712
759
  */
713
- readonly config: Config | undefined
760
+ readonly config: Config
714
761
  /**
715
762
  * Resolves config and initializes the driver. `build()` calls this automatically.
716
763
  */
@@ -728,10 +775,51 @@ export type Kubb = {
728
775
  type SetupResult = {
729
776
  hooks: AsyncEventEmitter<KubbHooks>
730
777
  driver: PluginDriver
731
- sources: Map<string, string>
778
+ storage: Storage
732
779
  config: Config
733
780
  }
734
781
 
782
+ /**
783
+ * Builds a `Storage` view scoped to the file paths produced by the current build.
784
+ *
785
+ * Reads delegate to the underlying `storage` (typically `fsStorage()`) so source bytes
786
+ * stay where they were written instead of being held in an extra in-memory map.
787
+ * Writing via `setItem` stores the content in the underlying storage and registers the
788
+ * key so subsequent reads and `getKeys` are scoped to this build's output.
789
+ */
790
+ function createSourcesView(storage: Storage): Storage {
791
+ const paths = new Set<string>()
792
+ return createStorage(() => ({
793
+ name: `${storage.name}:sources`,
794
+ async hasItem(key: string) {
795
+ return paths.has(key) && (await storage.hasItem(key))
796
+ },
797
+ async getItem(key: string) {
798
+ return paths.has(key) ? storage.getItem(key) : null
799
+ },
800
+ async setItem(key: string, value: string) {
801
+ paths.add(key)
802
+ await storage.setItem(key, value)
803
+ },
804
+ async removeItem(key: string) {
805
+ paths.delete(key)
806
+ await storage.removeItem(key)
807
+ },
808
+ async getKeys(base?: string) {
809
+ if (!base) return [...paths]
810
+ const result: Array<string> = []
811
+ for (const key of paths) {
812
+ if (key.startsWith(base)) result.push(key)
813
+ }
814
+ return result
815
+ },
816
+ async clear() {
817
+ paths.clear()
818
+ await storage.clear()
819
+ },
820
+ }))()
821
+ }
822
+
735
823
  async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promise<SetupResult> {
736
824
  const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
737
825
  const config: Config = {
@@ -758,7 +846,7 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
758
846
  const driver = new PluginDriver(config, {
759
847
  hooks,
760
848
  })
761
- const sources: Map<string, string> = new Map<string, string>()
849
+ const storage: Storage = createSourcesView(config.storage)
762
850
  const diagnosticInfo = getDiagnosticInfo()
763
851
 
764
852
  await hooks.emit('kubb:debug', {
@@ -851,7 +939,7 @@ async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promis
851
939
  config,
852
940
  hooks,
853
941
  driver,
854
- sources,
942
+ storage,
855
943
  }
856
944
  }
857
945
 
@@ -965,11 +1053,69 @@ async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorCon
965
1053
  }
966
1054
 
967
1055
  async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
968
- const { driver, hooks, sources } = setupResult
1056
+ const { driver, hooks, storage } = setupResult
969
1057
 
970
1058
  const failedPlugins = new Set<{ plugin: Plugin; error: Error }>()
971
1059
  const pluginTimings = new Map<string, number>()
972
1060
  const config = driver.config
1061
+ const writtenPaths = new Set<string>()
1062
+ const parsersMap = new Map<FileNode['extname'], Parser>()
1063
+ for (const parser of config.parsers) {
1064
+ if (parser.extNames) {
1065
+ for (const extname of parser.extNames) {
1066
+ parsersMap.set(extname, parser)
1067
+ }
1068
+ }
1069
+ }
1070
+ const fileProcessor = new FileProcessor()
1071
+
1072
+ fileProcessor.events.on('start', async (processingFiles) => {
1073
+ await hooks.emit('kubb:files:processing:start', { files: processingFiles })
1074
+ })
1075
+
1076
+ fileProcessor.events.on('update', async ({ file, source, processed, total, percentage }) => {
1077
+ await hooks.emit('kubb:file:processing:update', {
1078
+ file,
1079
+ source,
1080
+ processed,
1081
+ total,
1082
+ percentage,
1083
+ config,
1084
+ })
1085
+ if (source) {
1086
+ await storage.setItem(file.path, source)
1087
+ }
1088
+ })
1089
+
1090
+ fileProcessor.events.on('end', async (processed) => {
1091
+ await hooks.emit('kubb:files:processing:end', { files: processed })
1092
+ await hooks.emit('kubb:debug', {
1093
+ date: new Date(),
1094
+ logs: [`✓ File write process completed for ${processed.length} files`],
1095
+ })
1096
+ })
1097
+
1098
+ async function flushPendingFiles(): Promise<void> {
1099
+ const files = driver.fileManager.files.filter((f) => !writtenPaths.has(f.path))
1100
+ if (files.length === 0) {
1101
+ return
1102
+ }
1103
+
1104
+ await hooks.emit('kubb:debug', {
1105
+ date: new Date(),
1106
+ logs: [`Writing ${files.length} files...`],
1107
+ })
1108
+
1109
+ await fileProcessor.run(files, {
1110
+ parsers: parsersMap,
1111
+ mode: 'parallel',
1112
+ extension: config.output.extension,
1113
+ })
1114
+
1115
+ for (const file of files) {
1116
+ writtenPaths.add(file.path)
1117
+ }
1118
+ }
973
1119
 
974
1120
  try {
975
1121
  await driver.emitSetupHooks()
@@ -1018,6 +1164,8 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1018
1164
  upsertFile: (...files) => driver.fileManager.upsert(...files),
1019
1165
  })
1020
1166
 
1167
+ await flushPendingFiles()
1168
+
1021
1169
  await hooks.emit('kubb:debug', {
1022
1170
  date: new Date(),
1023
1171
  logs: [`✓ Plugin started successfully (${formatMs(duration)})`],
@@ -1039,6 +1187,8 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1039
1187
  upsertFile: (...files) => driver.fileManager.upsert(...files),
1040
1188
  })
1041
1189
 
1190
+ await flushPendingFiles()
1191
+
1042
1192
  await hooks.emit('kubb:debug', {
1043
1193
  date: errorTimestamp,
1044
1194
  logs: [
@@ -1062,54 +1212,9 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1062
1212
  upsertFile: (...files) => driver.fileManager.upsert(...files),
1063
1213
  })
1064
1214
 
1065
- const files = driver.fileManager.files
1215
+ await flushPendingFiles()
1066
1216
 
1067
- const parsersMap = new Map<FileNode['extname'], Parser>()
1068
- for (const parser of config.parsers) {
1069
- if (parser.extNames) {
1070
- for (const extname of parser.extNames) {
1071
- parsersMap.set(extname, parser)
1072
- }
1073
- }
1074
- }
1075
-
1076
- const fileProcessor = new FileProcessor()
1077
-
1078
- await hooks.emit('kubb:debug', {
1079
- date: new Date(),
1080
- logs: [`Writing ${files.length} files...`],
1081
- })
1082
-
1083
- await fileProcessor.run(files, {
1084
- parsers: parsersMap,
1085
- mode: 'parallel',
1086
- extension: config.output.extension,
1087
- onStart: async (processingFiles) => {
1088
- await hooks.emit('kubb:files:processing:start', { files: processingFiles })
1089
- },
1090
- onUpdate: async ({ file, source, processed, total, percentage }) => {
1091
- await hooks.emit('kubb:file:processing:update', {
1092
- file,
1093
- source,
1094
- processed,
1095
- total,
1096
- percentage,
1097
- config,
1098
- })
1099
- if (source) {
1100
- await config.storage.setItem(file.path, source)
1101
-
1102
- sources.set(file.path, source)
1103
- }
1104
- },
1105
- onEnd: async (processedFiles) => {
1106
- await hooks.emit('kubb:files:processing:end', { files: processedFiles })
1107
- await hooks.emit('kubb:debug', {
1108
- date: new Date(),
1109
- logs: [`✓ File write process completed for ${processedFiles.length} files`],
1110
- })
1111
- },
1112
- })
1217
+ const files = driver.fileManager.files
1113
1218
 
1114
1219
  await hooks.emit('kubb:build:end', {
1115
1220
  files,
@@ -1122,7 +1227,7 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1122
1227
  files,
1123
1228
  driver,
1124
1229
  pluginTimings,
1125
- sources,
1230
+ storage,
1126
1231
  }
1127
1232
  } catch (error) {
1128
1233
  return {
@@ -1131,7 +1236,7 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1131
1236
  driver,
1132
1237
  pluginTimings,
1133
1238
  error: error as Error,
1134
- sources,
1239
+ storage,
1135
1240
  }
1136
1241
  } finally {
1137
1242
  driver.dispose()
@@ -1139,7 +1244,7 @@ async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
1139
1244
  }
1140
1245
 
1141
1246
  async function build(setupResult: SetupResult): Promise<BuildOutput> {
1142
- const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(setupResult)
1247
+ const { files, driver, failedPlugins, pluginTimings, error, storage } = await safeBuild(setupResult)
1143
1248
 
1144
1249
  if (error) {
1145
1250
  throw error
@@ -1157,7 +1262,7 @@ async function build(setupResult: SetupResult): Promise<BuildOutput> {
1157
1262
  driver,
1158
1263
  pluginTimings,
1159
1264
  error: undefined,
1160
- sources,
1265
+ storage,
1161
1266
  }
1162
1267
  }
1163
1268
 
@@ -1213,7 +1318,7 @@ type CreateKubbOptions = {
1213
1318
  * Creates a Kubb instance bound to a single config entry.
1214
1319
  *
1215
1320
  * Accepts a user-facing config shape and resolves it to a full {@link Config} during
1216
- * `setup()`. The instance then holds shared state (`hooks`, `sources`, `driver`, `config`)
1321
+ * `setup()`. The instance then holds shared state (`hooks`, `storage`, `driver`, `config`)
1217
1322
  * across the `setup → build` lifecycle. Attach event listeners to `kubb.hooks` before
1218
1323
  * calling `setup()` or `build()`.
1219
1324
  *
@@ -1236,14 +1341,23 @@ export function createKubb(userConfig: UserConfig, options: CreateKubbOptions =
1236
1341
  get hooks() {
1237
1342
  return hooks
1238
1343
  },
1239
- get sources() {
1240
- return setupResult?.sources ?? new Map()
1344
+ get storage() {
1345
+ if (!setupResult) {
1346
+ throw new Error('[kubb] setup() must be called before accessing storage')
1347
+ }
1348
+ return setupResult.storage
1241
1349
  },
1242
1350
  get driver() {
1243
- return setupResult?.driver
1351
+ if (!setupResult) {
1352
+ throw new Error('[kubb] setup() must be called before accessing driver')
1353
+ }
1354
+ return setupResult.driver
1244
1355
  },
1245
1356
  get config() {
1246
- return setupResult?.config
1357
+ if (!setupResult) {
1358
+ throw new Error('[kubb] setup() must be called before accessing config')
1359
+ }
1360
+ return setupResult.config
1247
1361
  },
1248
1362
  async setup() {
1249
1363
  setupResult = await setup(userConfig, { hooks })
package/src/types.ts CHANGED
@@ -32,6 +32,7 @@ export type {
32
32
  } from './createKubb.ts'
33
33
  export type { Renderer, RendererFactory } from './createRenderer.ts'
34
34
  export type { Storage } from './createStorage.ts'
35
+ export type { FileProcessorEvents } from './FileProcessor.ts'
35
36
  export type { Generator, GeneratorContext } from './defineGenerator.ts'
36
37
  export type { Logger, LoggerContext, LoggerOptions, UserLogger } from './defineLogger.ts'
37
38
  export type { Middleware } from './defineMiddleware.ts'