@kubb/core 5.0.0-alpha.30 → 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.30",
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.30"
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 { FabricFile } from '@kubb/fabric-core/types'
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<FabricFile.ResolvedFile>, sources: Map<FabricFile.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<FabricFile.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: FabricFile.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<FabricFile.ResolvedFile>]
201
+ 'files:processing:end': [files: Array<FileNode>]
202
202
 
203
203
  /**
204
204
  * Emitted when a plugin starts executing.
@@ -2,10 +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 { FabricFile, 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 { FileManager } from './FileManager.ts'
9
10
 
10
11
  import type {
11
12
  Adapter,
@@ -46,7 +47,6 @@ type SafeParseResult<H extends PluginLifecycleHooks, Result = ReturnType<ParseRe
46
47
  // inspired by: https://github.com/rollup/rollup/blob/master/src/utils/PluginDriver.ts#
47
48
 
48
49
  type Options = {
49
- fabric: FabricType
50
50
  events: AsyncEventEmitter<KubbEvents>
51
51
  /**
52
52
  * @default Number.POSITIVE_INFINITY
@@ -59,8 +59,8 @@ type Options = {
59
59
  */
60
60
  export type GetFileOptions<TOptions = object> = {
61
61
  name: string
62
- mode?: FabricFile.Mode
63
- extname: FabricFile.Extname
62
+ mode?: 'single' | 'split'
63
+ extname: FileNode['extname']
64
64
  pluginName: string
65
65
  options?: TOptions
66
66
  }
@@ -74,7 +74,7 @@ export type GetFileOptions<TOptions = object> = {
74
74
  * getMode('src/gen/types') // 'split'
75
75
  * ```
76
76
  */
77
- export function getMode(fileOrFolder: string | undefined | null): FabricFile.Mode {
77
+ export function getMode(fileOrFolder: string | undefined | null): 'single' | 'split' {
78
78
  if (!fileOrFolder) {
79
79
  return 'split'
80
80
  }
@@ -88,13 +88,20 @@ export class PluginDriver {
88
88
  readonly options: Options
89
89
 
90
90
  /**
91
- * The universal `@kubb/ast` `RootNode` produced by the adapter, set by
91
+ * The universal `@kubb/ast` `InputNode` produced by the adapter, set by
92
92
  * the build pipeline after the adapter's `parse()` resolves.
93
93
  */
94
- rootNode: RootNode | undefined = undefined
94
+ inputNode: InputNode | undefined = undefined
95
95
  adapter: Adapter | undefined = undefined
96
96
  #studioIsOpen = false
97
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
+
98
105
  readonly plugins = new Map<string, Plugin>()
99
106
 
100
107
  constructor(config: Config, options: Options) {
@@ -126,12 +133,11 @@ export class PluginDriver {
126
133
  const driver = this
127
134
 
128
135
  const baseContext = {
129
- fabric: driver.options.fabric,
130
136
  config: driver.config,
131
137
  get root(): string {
132
138
  return resolve(driver.config.root, driver.config.output.path)
133
139
  },
134
- getMode(output: { path: string }): FabricFile.Mode {
140
+ getMode(output: { path: string }): 'single' | 'split' {
135
141
  return getMode(resolve(driver.config.root, driver.config.output.path, output.path))
136
142
  },
137
143
  events: driver.options.events,
@@ -139,14 +145,14 @@ export class PluginDriver {
139
145
  getPlugin: driver.getPlugin.bind(driver),
140
146
  requirePlugin: driver.requirePlugin.bind(driver),
141
147
  driver: driver,
142
- addFile: async (...files: Array<FabricFile.File>) => {
143
- await this.options.fabric.addFile(...files)
148
+ addFile: async (...files: Array<FileNode>) => {
149
+ driver.fileManager.add(...files)
144
150
  },
145
- upsertFile: async (...files: Array<FabricFile.File>) => {
146
- await this.options.fabric.upsertFile(...files)
151
+ upsertFile: async (...files: Array<FileNode>) => {
152
+ driver.fileManager.upsert(...files)
147
153
  },
148
- get rootNode(): RootNode | undefined {
149
- return driver.rootNode
154
+ get inputNode(): InputNode | undefined {
155
+ return driver.inputNode
150
156
  },
151
157
  get adapter(): Adapter | undefined {
152
158
  return driver.adapter
@@ -175,7 +181,7 @@ export class PluginDriver {
175
181
  throw new Error('Devtools must be an object')
176
182
  }
177
183
 
178
- if (!driver.rootNode || !driver.adapter) {
184
+ if (!driver.inputNode || !driver.adapter) {
179
185
  throw new Error('adapter is not defined, make sure you have set the parser in kubb.config.ts')
180
186
  }
181
187
 
@@ -183,7 +189,7 @@ export class PluginDriver {
183
189
 
184
190
  const studioUrl = driver.config.devtools?.studioUrl ?? DEFAULT_STUDIO_URL
185
191
 
186
- return openInStudioFn(driver.rootNode, studioUrl, options)
192
+ return openInStudioFn(driver.inputNode, studioUrl, options)
187
193
  },
188
194
  } as unknown as PluginContext<TOptions>
189
195
 
@@ -206,7 +212,7 @@ export class PluginDriver {
206
212
  /**
207
213
  * @deprecated use resolvers context instead
208
214
  */
209
- getFile<TOptions = object>({ name, mode, extname, pluginName, options }: GetFileOptions<TOptions>): FabricFile.File<{ pluginName: string }> {
215
+ getFile<TOptions = object>({ name, mode, extname, pluginName, options }: GetFileOptions<TOptions>): FileNode<{ pluginName: string }> {
210
216
  const resolvedName = mode ? (mode === 'single' ? '' : this.resolveName({ name, pluginName, type: 'file' })) : name
211
217
 
212
218
  const path = this.resolvePath({
@@ -220,22 +226,22 @@ export class PluginDriver {
220
226
  throw new Error(`Filepath should be defined for resolvedName "${resolvedName}" and pluginName "${pluginName}"`)
221
227
  }
222
228
 
223
- return {
229
+ return createFile<{ pluginName: string }>({
224
230
  path,
225
- baseName: basename(path) as FabricFile.File['baseName'],
231
+ baseName: basename(path) as `${string}.${string}`,
226
232
  meta: {
227
233
  pluginName,
228
234
  },
229
235
  sources: [],
230
236
  imports: [],
231
237
  exports: [],
232
- }
238
+ })
233
239
  }
234
240
 
235
241
  /**
236
242
  * @deprecated use resolvers context instead
237
243
  */
238
- resolvePath = <TOptions = object>(params: ResolvePathParams<TOptions>): FabricFile.Path => {
244
+ resolvePath = <TOptions = object>(params: ResolvePathParams<TOptions>): string => {
239
245
  const root = resolve(this.config.root, this.config.output.path)
240
246
  const defaultPath = resolve(root, params.baseName)
241
247