@kubb/core 5.0.0-alpha.31 → 5.0.0-alpha.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/core",
3
- "version": "5.0.0-alpha.31",
3
+ "version": "5.0.0-alpha.32",
4
4
  "description": "Core functionality for Kubb's plugin-based code generation system, providing the foundation for transforming OpenAPI specifications.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -64,14 +64,13 @@
64
64
  }
65
65
  ],
66
66
  "dependencies": {
67
- "@kubb/fabric-core": "0.15.1",
68
67
  "@kubb/react-fabric": "0.15.1",
69
68
  "empathic": "^2.0.0",
70
69
  "fflate": "^0.8.2",
71
70
  "remeda": "^2.33.7",
72
71
  "semver": "^7.7.4",
73
72
  "tinyexec": "^1.0.4",
74
- "@kubb/ast": "5.0.0-alpha.31"
73
+ "@kubb/ast": "5.0.0-alpha.32"
75
74
  },
76
75
  "devDependencies": {
77
76
  "@types/semver": "^7.7.1",
@@ -79,7 +78,6 @@
79
78
  "@internals/utils": "0.0.0"
80
79
  },
81
80
  "peerDependencies": {
82
- "@kubb/fabric-core": "0.14.0",
83
81
  "@kubb/react-fabric": "0.14.0"
84
82
  },
85
83
  "engines": {
@@ -0,0 +1,131 @@
1
+ import { trimExtName } from '@internals/utils'
2
+ import { createFile } from '@kubb/ast'
3
+ import type { FileNode } from '@kubb/ast/types'
4
+
5
+ function mergeFile<TMeta extends object = object>(a: FileNode<TMeta>, b: FileNode<TMeta>): FileNode<TMeta> {
6
+ return {
7
+ ...a,
8
+ sources: [...(a.sources || []), ...(b.sources || [])],
9
+ imports: [...(a.imports || []), ...(b.imports || [])],
10
+ exports: [...(a.exports || []), ...(b.exports || [])],
11
+ }
12
+ }
13
+
14
+ /**
15
+ * In-memory file store for generated files.
16
+ *
17
+ * Files with the same `path` are merged — sources, imports, and exports are concatenated.
18
+ * The `files` getter returns all stored files sorted by path length (shortest first).
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { FileManager } from '@kubb/core'
23
+ *
24
+ * const manager = new FileManager()
25
+ * manager.upsert(myFile)
26
+ * console.log(manager.files) // all stored files
27
+ * ```
28
+ */
29
+ export class FileManager {
30
+ readonly #cache = new Map<string, FileNode>()
31
+ #filesCache: Array<FileNode> | null = null
32
+
33
+ /**
34
+ * Adds one or more files. Files with the same path are merged — sources, imports,
35
+ * and exports from all calls with the same path are concatenated together.
36
+ */
37
+ add(...files: Array<FileNode>): Array<FileNode> {
38
+ const resolvedFiles: Array<FileNode> = []
39
+ const mergedFiles = new Map<string, FileNode>()
40
+
41
+ files.forEach((file) => {
42
+ const existing = mergedFiles.get(file.path)
43
+ if (existing) {
44
+ mergedFiles.set(file.path, mergeFile(existing, file))
45
+ } else {
46
+ mergedFiles.set(file.path, file)
47
+ }
48
+ })
49
+
50
+ for (const file of mergedFiles.values()) {
51
+ const resolvedFile = createFile(file)
52
+ this.#cache.set(resolvedFile.path, resolvedFile)
53
+ this.#filesCache = null
54
+ resolvedFiles.push(resolvedFile)
55
+ }
56
+
57
+ return resolvedFiles
58
+ }
59
+
60
+ /**
61
+ * Adds or merges one or more files.
62
+ * If a file with the same path already exists, its sources/imports/exports are merged together.
63
+ */
64
+ upsert(...files: Array<FileNode>): Array<FileNode> {
65
+ const resolvedFiles: Array<FileNode> = []
66
+ const mergedFiles = new Map<string, FileNode>()
67
+
68
+ files.forEach((file) => {
69
+ const existing = mergedFiles.get(file.path)
70
+ if (existing) {
71
+ mergedFiles.set(file.path, mergeFile(existing, file))
72
+ } else {
73
+ mergedFiles.set(file.path, file)
74
+ }
75
+ })
76
+
77
+ for (const file of mergedFiles.values()) {
78
+ const existing = this.#cache.get(file.path)
79
+ const merged = existing ? mergeFile(existing, file) : file
80
+ const resolvedFile = createFile(merged)
81
+ this.#cache.set(resolvedFile.path, resolvedFile)
82
+ this.#filesCache = null
83
+ resolvedFiles.push(resolvedFile)
84
+ }
85
+
86
+ return resolvedFiles
87
+ }
88
+
89
+ getByPath(path: string): FileNode | null {
90
+ return this.#cache.get(path) ?? null
91
+ }
92
+
93
+ deleteByPath(path: string): void {
94
+ this.#cache.delete(path)
95
+ this.#filesCache = null
96
+ }
97
+
98
+ clear(): void {
99
+ this.#cache.clear()
100
+ this.#filesCache = null
101
+ }
102
+
103
+ /**
104
+ * All stored files, sorted by path length (shorter paths first).
105
+ * Barrel/index files (e.g. index.ts) are sorted last within each length bucket.
106
+ */
107
+ get files(): Array<FileNode> {
108
+ if (this.#filesCache) {
109
+ return this.#filesCache
110
+ }
111
+
112
+ const keys = [...this.#cache.keys()].sort((a, b) => {
113
+ if (a.length !== b.length) return a.length - b.length
114
+ const aIsIndex = trimExtName(a).endsWith('index')
115
+ const bIsIndex = trimExtName(b).endsWith('index')
116
+ if (aIsIndex !== bIsIndex) return aIsIndex ? 1 : -1
117
+ return 0
118
+ })
119
+
120
+ const files: Array<FileNode> = []
121
+ for (const key of keys) {
122
+ const file = this.#cache.get(key)
123
+ if (file) {
124
+ files.push(file)
125
+ }
126
+ }
127
+
128
+ this.#filesCache = files
129
+ return files
130
+ }
131
+ }
@@ -0,0 +1,83 @@
1
+ import type { FileNode } from '@kubb/ast/types'
2
+ import pLimit from 'p-limit'
3
+ import { PARALLEL_CONCURRENCY_LIMIT } from './constants.ts'
4
+ import type { Parser } from './defineParser.ts'
5
+
6
+ type ParseOptions = {
7
+ parsers?: Map<FileNode['extname'], Parser>
8
+ extension?: Record<FileNode['extname'], FileNode['extname'] | ''>
9
+ }
10
+
11
+ type RunOptions = ParseOptions & {
12
+ /**
13
+ * @default 'sequential'
14
+ */
15
+ mode?: 'sequential' | 'parallel'
16
+ onStart?: (files: Array<FileNode>) => Promise<void> | void
17
+ onEnd?: (files: Array<FileNode>) => Promise<void> | void
18
+ onUpdate?: (params: { file: FileNode; source?: string; processed: number; total: number; percentage: number }) => Promise<void> | void
19
+ }
20
+
21
+ function joinSources(file: FileNode): string {
22
+ return file.sources
23
+ .map((item) => item.value)
24
+ .filter((value): value is string => value != null)
25
+ .join('\n\n')
26
+ }
27
+
28
+ /**
29
+ * Converts a single file to a string using the registered parsers.
30
+ * Falls back to joining source values when no matching parser is found.
31
+ */
32
+ export class FileProcessor {
33
+ readonly #limit = pLimit(PARALLEL_CONCURRENCY_LIMIT)
34
+
35
+ async parse(file: FileNode, { parsers, extension }: ParseOptions = {}): Promise<string> {
36
+ const parseExtName = extension?.[file.extname] || undefined
37
+
38
+ if (!parsers || !file.extname) {
39
+ return joinSources(file)
40
+ }
41
+
42
+ const parser = parsers.get(file.extname)
43
+
44
+ if (!parser) {
45
+ return joinSources(file)
46
+ }
47
+
48
+ return parser.parse(file, { extname: parseExtName })
49
+ }
50
+
51
+ async run(files: Array<FileNode>, { parsers, mode = 'sequential', extension, onStart, onEnd, onUpdate }: RunOptions = {}): Promise<Array<FileNode>> {
52
+ await onStart?.(files)
53
+
54
+ const total = files.length
55
+ let processed = 0
56
+
57
+ const processOne = async (file: FileNode) => {
58
+ const source = await this.parse(file, { extension, parsers })
59
+ const currentProcessed = ++processed
60
+ const percentage = (currentProcessed / total) * 100
61
+
62
+ await onUpdate?.({
63
+ file,
64
+ source,
65
+ processed: currentProcessed,
66
+ percentage,
67
+ total,
68
+ })
69
+ }
70
+
71
+ if (mode === 'sequential') {
72
+ for (const file of files) {
73
+ await processOne(file)
74
+ }
75
+ } else {
76
+ await Promise.all(files.map((file) => this.#limit(() => processOne(file))))
77
+ }
78
+
79
+ await onEnd?.(files)
80
+
81
+ return files
82
+ }
83
+ }
package/src/Kubb.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type * as KubbFile from './KubbFile.ts'
1
+ import type { FileNode } from '@kubb/ast/types'
2
2
  import type { Strategy } from './PluginDriver.ts'
3
3
  import type { Config, Plugin, PluginLifecycleHooks } from './types'
4
4
 
@@ -76,7 +76,7 @@ export interface KubbEvents {
76
76
  /**
77
77
  * Emitted when code generation phase completes.
78
78
  */
79
- 'generation:end': [config: Config, files: Array<KubbFile.ResolvedFile>, sources: Map<KubbFile.Path, string>]
79
+ 'generation:end': [config: Config, files: Array<FileNode>, sources: Map<string, string>]
80
80
  /**
81
81
  * Emitted with a summary of the generation results.
82
82
  * Contains summary lines, title, and success status.
@@ -160,7 +160,7 @@ export interface KubbEvents {
160
160
  * Emitted when file processing starts.
161
161
  * Contains the list of files to be processed.
162
162
  */
163
- 'files:processing:start': [files: Array<KubbFile.ResolvedFile>]
163
+ 'files:processing:start': [files: Array<FileNode>]
164
164
  /**
165
165
  * Emitted for each file being processed, providing progress updates.
166
166
  * Contains processed count, total count, percentage, and file details.
@@ -186,7 +186,7 @@ export interface KubbEvents {
186
186
  /**
187
187
  * The file being processed.
188
188
  */
189
- file: KubbFile.ResolvedFile
189
+ file: FileNode
190
190
  /**
191
191
  * Kubb configuration (not present in Fabric).
192
192
  * Provides access to the current config during file processing.
@@ -198,7 +198,7 @@ export interface KubbEvents {
198
198
  * Emitted when file processing completes.
199
199
  * Contains the list of processed files.
200
200
  */
201
- 'files:processing:end': [files: Array<KubbFile.ResolvedFile>]
201
+ 'files:processing:end': [files: Array<FileNode>]
202
202
 
203
203
  /**
204
204
  * Emitted when a plugin starts executing.
@@ -2,11 +2,11 @@ import { basename, extname, resolve } from 'node:path'
2
2
  import { performance } from 'node:perf_hooks'
3
3
  import type { AsyncEventEmitter } from '@internals/utils'
4
4
  import { isPromiseRejectedResult, transformReservedWord } from '@internals/utils'
5
- import type { RootNode } from '@kubb/ast/types'
6
- import type { Fabric as FabricType } from '@kubb/fabric-core/types'
5
+ import { createFile } from '@kubb/ast'
6
+ import type { FileNode, InputNode } from '@kubb/ast/types'
7
7
  import { DEFAULT_STUDIO_URL } from './constants.ts'
8
8
  import { openInStudio as openInStudioFn } from './devtools.ts'
9
- import type * as KubbFile from './KubbFile.ts'
9
+ import { FileManager } from './FileManager.ts'
10
10
 
11
11
  import type {
12
12
  Adapter,
@@ -47,7 +47,6 @@ type SafeParseResult<H extends PluginLifecycleHooks, Result = ReturnType<ParseRe
47
47
  // inspired by: https://github.com/rollup/rollup/blob/master/src/utils/PluginDriver.ts#
48
48
 
49
49
  type Options = {
50
- fabric: FabricType
51
50
  events: AsyncEventEmitter<KubbEvents>
52
51
  /**
53
52
  * @default Number.POSITIVE_INFINITY
@@ -60,8 +59,8 @@ type Options = {
60
59
  */
61
60
  export type GetFileOptions<TOptions = object> = {
62
61
  name: string
63
- mode?: KubbFile.Mode
64
- extname: KubbFile.Extname
62
+ mode?: 'single' | 'split'
63
+ extname: FileNode['extname']
65
64
  pluginName: string
66
65
  options?: TOptions
67
66
  }
@@ -75,7 +74,7 @@ export type GetFileOptions<TOptions = object> = {
75
74
  * getMode('src/gen/types') // 'split'
76
75
  * ```
77
76
  */
78
- export function getMode(fileOrFolder: string | undefined | null): KubbFile.Mode {
77
+ export function getMode(fileOrFolder: string | undefined | null): 'single' | 'split' {
79
78
  if (!fileOrFolder) {
80
79
  return 'split'
81
80
  }
@@ -89,13 +88,20 @@ export class PluginDriver {
89
88
  readonly options: Options
90
89
 
91
90
  /**
92
- * The universal `@kubb/ast` `RootNode` produced by the adapter, set by
91
+ * The universal `@kubb/ast` `InputNode` produced by the adapter, set by
93
92
  * the build pipeline after the adapter's `parse()` resolves.
94
93
  */
95
- rootNode: RootNode | undefined = undefined
94
+ inputNode: InputNode | undefined = undefined
96
95
  adapter: Adapter | undefined = undefined
97
96
  #studioIsOpen = false
98
97
 
98
+ /**
99
+ * Central file store for all generated files.
100
+ * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
101
+ * add files; this property gives direct read/write access when needed.
102
+ */
103
+ readonly fileManager = new FileManager()
104
+
99
105
  readonly plugins = new Map<string, Plugin>()
100
106
 
101
107
  constructor(config: Config, options: Options) {
@@ -127,12 +133,11 @@ export class PluginDriver {
127
133
  const driver = this
128
134
 
129
135
  const baseContext = {
130
- fabric: driver.options.fabric,
131
136
  config: driver.config,
132
137
  get root(): string {
133
138
  return resolve(driver.config.root, driver.config.output.path)
134
139
  },
135
- getMode(output: { path: string }): KubbFile.Mode {
140
+ getMode(output: { path: string }): 'single' | 'split' {
136
141
  return getMode(resolve(driver.config.root, driver.config.output.path, output.path))
137
142
  },
138
143
  events: driver.options.events,
@@ -140,14 +145,14 @@ export class PluginDriver {
140
145
  getPlugin: driver.getPlugin.bind(driver),
141
146
  requirePlugin: driver.requirePlugin.bind(driver),
142
147
  driver: driver,
143
- addFile: async (...files: Array<KubbFile.File>) => {
144
- await this.options.fabric.addFile(...files)
148
+ addFile: async (...files: Array<FileNode>) => {
149
+ driver.fileManager.add(...files)
145
150
  },
146
- upsertFile: async (...files: Array<KubbFile.File>) => {
147
- await this.options.fabric.upsertFile(...files)
151
+ upsertFile: async (...files: Array<FileNode>) => {
152
+ driver.fileManager.upsert(...files)
148
153
  },
149
- get rootNode(): RootNode | undefined {
150
- return driver.rootNode
154
+ get inputNode(): InputNode | undefined {
155
+ return driver.inputNode
151
156
  },
152
157
  get adapter(): Adapter | undefined {
153
158
  return driver.adapter
@@ -176,7 +181,7 @@ export class PluginDriver {
176
181
  throw new Error('Devtools must be an object')
177
182
  }
178
183
 
179
- if (!driver.rootNode || !driver.adapter) {
184
+ if (!driver.inputNode || !driver.adapter) {
180
185
  throw new Error('adapter is not defined, make sure you have set the parser in kubb.config.ts')
181
186
  }
182
187
 
@@ -184,7 +189,7 @@ export class PluginDriver {
184
189
 
185
190
  const studioUrl = driver.config.devtools?.studioUrl ?? DEFAULT_STUDIO_URL
186
191
 
187
- return openInStudioFn(driver.rootNode, studioUrl, options)
192
+ return openInStudioFn(driver.inputNode, studioUrl, options)
188
193
  },
189
194
  } as unknown as PluginContext<TOptions>
190
195
 
@@ -207,7 +212,7 @@ export class PluginDriver {
207
212
  /**
208
213
  * @deprecated use resolvers context instead
209
214
  */
210
- getFile<TOptions = object>({ name, mode, extname, pluginName, options }: GetFileOptions<TOptions>): KubbFile.File<{ pluginName: string }> {
215
+ getFile<TOptions = object>({ name, mode, extname, pluginName, options }: GetFileOptions<TOptions>): FileNode<{ pluginName: string }> {
211
216
  const resolvedName = mode ? (mode === 'single' ? '' : this.resolveName({ name, pluginName, type: 'file' })) : name
212
217
 
213
218
  const path = this.resolvePath({
@@ -221,22 +226,22 @@ export class PluginDriver {
221
226
  throw new Error(`Filepath should be defined for resolvedName "${resolvedName}" and pluginName "${pluginName}"`)
222
227
  }
223
228
 
224
- return {
229
+ return createFile<{ pluginName: string }>({
225
230
  path,
226
- baseName: basename(path) as KubbFile.File['baseName'],
231
+ baseName: basename(path) as `${string}.${string}`,
227
232
  meta: {
228
233
  pluginName,
229
234
  },
230
235
  sources: [],
231
236
  imports: [],
232
237
  exports: [],
233
- }
238
+ })
234
239
  }
235
240
 
236
241
  /**
237
242
  * @deprecated use resolvers context instead
238
243
  */
239
- resolvePath = <TOptions = object>(params: ResolvePathParams<TOptions>): KubbFile.Path => {
244
+ resolvePath = <TOptions = object>(params: ResolvePathParams<TOptions>): string => {
240
245
  const root = resolve(this.config.root, this.config.output.path)
241
246
  const defaultPath = resolve(root, params.baseName)
242
247