@kubb/core 5.0.0-beta.2 → 5.0.0-beta.20
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/README.md +8 -38
- package/dist/{PluginDriver-BXibeQk-.cjs → KubbDriver-BXSnJ3qM.cjs} +719 -164
- package/dist/KubbDriver-BXSnJ3qM.cjs.map +1 -0
- package/dist/{PluginDriver-DV3p2Hky.js → KubbDriver-Cxii_rBp.js} +693 -162
- package/dist/KubbDriver-Cxii_rBp.js.map +1 -0
- package/dist/{types-CC09VtBt.d.ts → createKubb-Dcmtjqds.d.ts} +1395 -1238
- package/dist/index.cjs +556 -785
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -185
- package/dist/index.js +551 -783
- package/dist/index.js.map +1 -1
- package/dist/mocks.cjs +30 -21
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +5 -5
- package/dist/mocks.js +29 -20
- package/dist/mocks.js.map +1 -1
- package/package.json +6 -18
- package/src/FileManager.ts +12 -0
- package/src/FileProcessor.ts +37 -38
- package/src/{PluginDriver.ts → KubbDriver.ts} +249 -86
- package/src/constants.ts +11 -6
- package/src/createAdapter.ts +84 -1
- package/src/createKubb.ts +1336 -297
- package/src/createRenderer.ts +23 -22
- package/src/defineGenerator.ts +96 -7
- package/src/defineLogger.ts +42 -3
- package/src/defineMiddleware.ts +1 -1
- package/src/defineParser.ts +1 -1
- package/src/definePlugin.ts +304 -8
- package/src/defineResolver.ts +268 -147
- package/src/devtools.ts +8 -1
- package/src/index.ts +2 -2
- package/src/mocks.ts +11 -14
- package/src/storages/fsStorage.ts +13 -37
- package/src/types.ts +38 -1292
- package/dist/PluginDriver-BXibeQk-.cjs.map +0 -1
- package/dist/PluginDriver-DV3p2Hky.js.map +0 -1
- package/src/Kubb.ts +0 -300
- package/src/renderNode.ts +0 -35
- package/src/utils/diagnostics.ts +0 -18
- package/src/utils/isInputPath.ts +0 -10
- package/src/utils/packageJSON.ts +0 -99
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { arrayToAsyncIterable, type AsyncEventEmitter, memoize, URLPath } from '@internals/utils'
|
|
3
|
+
import { createFile, createStreamInput } from '@kubb/ast'
|
|
4
|
+
import type { FileNode, InputMeta, InputNode, InputStreamNode, OperationNode, SchemaNode } from '@kubb/ast'
|
|
5
5
|
import { DEFAULT_STUDIO_URL } from './constants.ts'
|
|
6
6
|
import type { Generator } from './defineGenerator.ts'
|
|
7
7
|
import type { Plugin } from './definePlugin.ts'
|
|
8
|
+
import { getMode } from './definePlugin.ts'
|
|
8
9
|
import { defineResolver } from './defineResolver.ts'
|
|
9
10
|
import { openInStudio as openInStudioFn } from './devtools.ts'
|
|
10
11
|
import { FileManager } from './FileManager.ts'
|
|
11
|
-
import {
|
|
12
|
+
import type { RendererFactory } from './createRenderer.ts'
|
|
12
13
|
|
|
13
14
|
import type {
|
|
14
15
|
Adapter,
|
|
16
|
+
AdapterSource,
|
|
15
17
|
Config,
|
|
16
18
|
DevtoolsOptions,
|
|
17
19
|
GeneratorContext,
|
|
18
20
|
KubbHooks,
|
|
19
21
|
KubbPluginSetupContext,
|
|
22
|
+
Middleware,
|
|
20
23
|
NormalizedPlugin,
|
|
21
24
|
PluginFactoryOptions,
|
|
22
25
|
Resolver,
|
|
23
26
|
} from './types.ts'
|
|
24
27
|
|
|
25
|
-
// inspired by: https://github.com/rollup/rollup/blob/master/src/utils/PluginDriver.ts#
|
|
26
|
-
|
|
27
28
|
type Options = {
|
|
28
29
|
hooks: AsyncEventEmitter<KubbHooks>
|
|
29
30
|
}
|
|
@@ -32,7 +33,7 @@ function enforceOrder(enforce: 'pre' | 'post' | undefined): number {
|
|
|
32
33
|
return enforce === 'pre' ? -1 : enforce === 'post' ? 1 : 0
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
export class
|
|
36
|
+
export class KubbDriver {
|
|
36
37
|
readonly config: Config
|
|
37
38
|
readonly options: Options
|
|
38
39
|
|
|
@@ -41,24 +42,41 @@ export class PluginDriver {
|
|
|
41
42
|
*
|
|
42
43
|
* @example
|
|
43
44
|
* ```ts
|
|
44
|
-
*
|
|
45
|
-
*
|
|
45
|
+
* KubbDriver.getMode('src/gen/types.ts') // 'single'
|
|
46
|
+
* KubbDriver.getMode('src/gen/types') // 'split'
|
|
46
47
|
* ```
|
|
47
48
|
*/
|
|
48
49
|
static getMode(fileOrFolder: string | undefined | null): 'single' | 'split' {
|
|
49
|
-
|
|
50
|
-
return 'split'
|
|
51
|
-
}
|
|
52
|
-
return extname(fileOrFolder) ? 'single' : 'split'
|
|
50
|
+
return getMode(fileOrFolder)
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
/**
|
|
56
|
-
* The
|
|
57
|
-
*
|
|
54
|
+
* The streaming `InputStreamNode` produced by the adapter.
|
|
55
|
+
* Always set after adapter setup — parse-only adapters are wrapped automatically.
|
|
58
56
|
*/
|
|
59
|
-
inputNode:
|
|
57
|
+
inputNode: InputStreamNode | undefined = undefined
|
|
60
58
|
adapter: Adapter | undefined = undefined
|
|
61
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Studio session state, kept together so `dispose()` can reset it atomically.
|
|
61
|
+
*
|
|
62
|
+
* - `source` holds the raw adapter source so `adapter.parse()` can be called lazily.
|
|
63
|
+
* Intentionally outlives the build; cleared by `dispose()`.
|
|
64
|
+
* - `isOpen` prevents opening the studio more than once per build.
|
|
65
|
+
* - `inputNode` caches the parse promise so `adapter.parse()` is called at most once
|
|
66
|
+
* per studio session, even when `openInStudio()` is called multiple times.
|
|
67
|
+
*/
|
|
68
|
+
#studio: { source: AdapterSource | undefined; isOpen: boolean; inputNode: Promise<InputNode> | undefined } = {
|
|
69
|
+
source: undefined,
|
|
70
|
+
isOpen: false,
|
|
71
|
+
inputNode: undefined,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Register middleware hooks after all plugin hooks are registered.
|
|
75
|
+
// Because AsyncEventEmitter calls listeners in registration order,
|
|
76
|
+
// middleware hooks for any event fire after all plugin hooks for that event.
|
|
77
|
+
// Handlers are tracked so they can be removed after each build (disposeMiddleware),
|
|
78
|
+
// preventing accumulation when multiple configs share the same hooks instance.
|
|
79
|
+
#middlewareListeners: Array<[keyof KubbHooks & string, (...args: never[]) => void | Promise<void>]> = []
|
|
62
80
|
|
|
63
81
|
/**
|
|
64
82
|
* Central file store for all generated files.
|
|
@@ -73,7 +91,7 @@ export class PluginDriver {
|
|
|
73
91
|
* Tracks which plugins have generators registered via `addGenerator()` (event-based path).
|
|
74
92
|
* Used by the build loop to decide whether to emit generator events for a given plugin.
|
|
75
93
|
*/
|
|
76
|
-
readonly #
|
|
94
|
+
readonly #eventGeneratorPlugins = new Set<string>()
|
|
77
95
|
readonly #resolvers = new Map<string, Resolver>()
|
|
78
96
|
readonly #defaultResolvers = new Map<string, Resolver>()
|
|
79
97
|
readonly #hookListeners = new Map<keyof KubbHooks, Set<(...args: never[]) => void | Promise<void>>>()
|
|
@@ -81,23 +99,38 @@ export class PluginDriver {
|
|
|
81
99
|
constructor(config: Config, options: Options) {
|
|
82
100
|
this.config = config
|
|
83
101
|
this.options = options
|
|
84
|
-
config.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
this.adapter = config.adapter
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async setup() {
|
|
106
|
+
const normalized: NormalizedPlugin[] = this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin as Plugin))
|
|
107
|
+
|
|
108
|
+
normalized.sort((a, b) => {
|
|
109
|
+
if (b.dependencies?.includes(a.name)) return -1
|
|
110
|
+
if (a.dependencies?.includes(b.name)) return 1
|
|
111
|
+
|
|
112
|
+
return enforceOrder(a.enforce) - enforceOrder(b.enforce)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
for (const plugin of normalized) {
|
|
116
|
+
if (plugin.apply) {
|
|
117
|
+
plugin.apply(this.config)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.#registerPlugin(plugin)
|
|
121
|
+
this.plugins.set(plugin.name, plugin)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.config.middleware) {
|
|
125
|
+
for (const middleware of this.config.middleware) {
|
|
126
|
+
for (const event of Object.keys(middleware.hooks) as Array<keyof KubbHooks & string>) {
|
|
127
|
+
this.#registerMiddleware(event, middleware.hooks)
|
|
89
128
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// enforce: 'pre' plugins run first, 'post' plugins run last
|
|
96
|
-
return enforceOrder(a.enforce) - enforceOrder(b.enforce)
|
|
97
|
-
})
|
|
98
|
-
.forEach((plugin) => {
|
|
99
|
-
this.plugins.set(plugin.name, plugin)
|
|
100
|
-
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (this.config.adapter) {
|
|
132
|
+
await this.#registerAdapter(this.config.adapter)
|
|
133
|
+
}
|
|
101
134
|
}
|
|
102
135
|
|
|
103
136
|
get hooks() {
|
|
@@ -108,16 +141,59 @@ export class PluginDriver {
|
|
|
108
141
|
* Creates an `NormalizedPlugin` from a hook-style plugin and registers
|
|
109
142
|
* its lifecycle handlers on the `AsyncEventEmitter`.
|
|
110
143
|
*/
|
|
111
|
-
#normalizePlugin(
|
|
112
|
-
const
|
|
113
|
-
name:
|
|
114
|
-
dependencies:
|
|
115
|
-
enforce:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
#normalizePlugin(plugin: Plugin): NormalizedPlugin {
|
|
145
|
+
const normalized: NormalizedPlugin = {
|
|
146
|
+
name: plugin.name,
|
|
147
|
+
dependencies: plugin.dependencies,
|
|
148
|
+
enforce: plugin.enforce,
|
|
149
|
+
hooks: plugin.hooks,
|
|
150
|
+
options: plugin.options ?? { output: { path: '.' }, exclude: [], override: [] },
|
|
151
|
+
} as NormalizedPlugin
|
|
152
|
+
|
|
153
|
+
if ('apply' in plugin && typeof plugin.apply === 'function') {
|
|
154
|
+
normalized.apply = plugin.apply as (config: Config) => boolean
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return normalized
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async #registerAdapter(adapter: Adapter) {
|
|
161
|
+
const source = inputToAdapterSource(this.config)
|
|
162
|
+
this.#studio.source = source
|
|
163
|
+
|
|
164
|
+
if (adapter.stream) {
|
|
165
|
+
this.inputNode = await adapter.stream(source)
|
|
166
|
+
|
|
167
|
+
await this.hooks.emit('kubb:debug', {
|
|
168
|
+
date: new Date(),
|
|
169
|
+
logs: [`✓ Adapter '${adapter.name}' producing input stream`],
|
|
170
|
+
})
|
|
171
|
+
} else {
|
|
172
|
+
// Adapter does not implement stream() — eagerly parse and wrap in a
|
|
173
|
+
// reusable AsyncIterable so the rest of the pipeline stays stream-only.
|
|
174
|
+
const inputNode = await adapter.parse(source)
|
|
175
|
+
this.inputNode = createStreamInput(arrayToAsyncIterable(inputNode.schemas), arrayToAsyncIterable(inputNode.operations), inputNode.meta)
|
|
176
|
+
|
|
177
|
+
await this.hooks.emit('kubb:debug', {
|
|
178
|
+
date: new Date(),
|
|
179
|
+
logs: [
|
|
180
|
+
`✓ Adapter '${adapter.name}' resolved InputNode (wrapped as stream)`,
|
|
181
|
+
` • Schemas: ${inputNode.schemas.length}`,
|
|
182
|
+
` • Operations: ${inputNode.operations.length}`,
|
|
183
|
+
],
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#registerMiddleware<K extends keyof KubbHooks & string>(event: K, middlewareHooks: Middleware['hooks']) {
|
|
189
|
+
const handler = middlewareHooks[event]
|
|
190
|
+
|
|
191
|
+
if (!handler) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.hooks.on(event, handler)
|
|
196
|
+
this.#middlewareListeners.push([event, handler as (...args: never[]) => void | Promise<void>])
|
|
121
197
|
}
|
|
122
198
|
|
|
123
199
|
/**
|
|
@@ -135,8 +211,10 @@ export class PluginDriver {
|
|
|
135
211
|
*
|
|
136
212
|
* @internal
|
|
137
213
|
*/
|
|
138
|
-
|
|
139
|
-
const { hooks } =
|
|
214
|
+
#registerPlugin(plugin: NormalizedPlugin): void {
|
|
215
|
+
const { hooks } = plugin
|
|
216
|
+
|
|
217
|
+
if (!hooks) return
|
|
140
218
|
|
|
141
219
|
// kubb:plugin:setup gets special treatment: the globally emitted context is wrapped with
|
|
142
220
|
// plugin-specific implementations so that addGenerator / setResolver / etc. target
|
|
@@ -145,21 +223,21 @@ export class PluginDriver {
|
|
|
145
223
|
const setupHandler = (globalCtx: KubbPluginSetupContext) => {
|
|
146
224
|
const pluginCtx: KubbPluginSetupContext = {
|
|
147
225
|
...globalCtx,
|
|
148
|
-
options:
|
|
226
|
+
options: plugin.options ?? {},
|
|
149
227
|
addGenerator: (gen) => {
|
|
150
|
-
this.registerGenerator(
|
|
228
|
+
this.registerGenerator(plugin.name, gen)
|
|
151
229
|
},
|
|
152
230
|
setResolver: (resolver) => {
|
|
153
|
-
this.setPluginResolver(
|
|
231
|
+
this.setPluginResolver(plugin.name, resolver)
|
|
154
232
|
},
|
|
155
233
|
setTransformer: (visitor) => {
|
|
156
|
-
|
|
234
|
+
plugin.transformer = visitor
|
|
157
235
|
},
|
|
158
236
|
setRenderer: (renderer) => {
|
|
159
|
-
|
|
237
|
+
plugin.renderer = renderer
|
|
160
238
|
},
|
|
161
239
|
setOptions: (opts) => {
|
|
162
|
-
|
|
240
|
+
plugin.options = { ...plugin.options, ...opts }
|
|
163
241
|
},
|
|
164
242
|
injectFile: (userFileNode) => {
|
|
165
243
|
this.fileManager.add(createFile(userFileNode))
|
|
@@ -189,6 +267,7 @@ export class PluginDriver {
|
|
|
189
267
|
*/
|
|
190
268
|
async emitSetupHooks(): Promise<void> {
|
|
191
269
|
const noop = () => {}
|
|
270
|
+
|
|
192
271
|
await this.hooks.emit('kubb:plugin:setup', {
|
|
193
272
|
config: this.config,
|
|
194
273
|
options: {},
|
|
@@ -226,7 +305,7 @@ export class PluginDriver {
|
|
|
226
305
|
const schemaHandler = async (node: SchemaNode, ctx: GeneratorContext) => {
|
|
227
306
|
if (ctx.plugin.name !== pluginName) return
|
|
228
307
|
const result = await gen.schema!(node, ctx)
|
|
229
|
-
await applyHookResult(result, this, resolveRenderer())
|
|
308
|
+
await applyHookResult({ result, driver: this, rendererFactory: resolveRenderer() })
|
|
230
309
|
}
|
|
231
310
|
|
|
232
311
|
this.hooks.on('kubb:generate:schema', schemaHandler)
|
|
@@ -237,7 +316,7 @@ export class PluginDriver {
|
|
|
237
316
|
const operationHandler = async (node: OperationNode, ctx: GeneratorContext) => {
|
|
238
317
|
if (ctx.plugin.name !== pluginName) return
|
|
239
318
|
const result = await gen.operation!(node, ctx)
|
|
240
|
-
await applyHookResult(result, this, resolveRenderer())
|
|
319
|
+
await applyHookResult({ result, driver: this, rendererFactory: resolveRenderer() })
|
|
241
320
|
}
|
|
242
321
|
|
|
243
322
|
this.hooks.on('kubb:generate:operation', operationHandler)
|
|
@@ -248,14 +327,14 @@ export class PluginDriver {
|
|
|
248
327
|
const operationsHandler = async (nodes: Array<OperationNode>, ctx: GeneratorContext) => {
|
|
249
328
|
if (ctx.plugin.name !== pluginName) return
|
|
250
329
|
const result = await gen.operations!(nodes, ctx)
|
|
251
|
-
await applyHookResult(result, this, resolveRenderer())
|
|
330
|
+
await applyHookResult({ result, driver: this, rendererFactory: resolveRenderer() })
|
|
252
331
|
}
|
|
253
332
|
|
|
254
333
|
this.hooks.on('kubb:generate:operations', operationsHandler)
|
|
255
334
|
this.#trackHookListener('kubb:generate:operations', operationsHandler as (...args: never[]) => void | Promise<void>)
|
|
256
335
|
}
|
|
257
336
|
|
|
258
|
-
this.#
|
|
337
|
+
this.#eventGeneratorPlugins.add(pluginName)
|
|
259
338
|
}
|
|
260
339
|
|
|
261
340
|
/**
|
|
@@ -265,8 +344,8 @@ export class PluginDriver {
|
|
|
265
344
|
* Used by the build loop to decide whether to walk the AST and emit generator events
|
|
266
345
|
* for a plugin that has no static `plugin.generators`.
|
|
267
346
|
*/
|
|
268
|
-
|
|
269
|
-
return this.#
|
|
347
|
+
hasEventGenerators(pluginName: string): boolean {
|
|
348
|
+
return this.#eventGeneratorPlugins.has(pluginName)
|
|
270
349
|
}
|
|
271
350
|
|
|
272
351
|
/**
|
|
@@ -281,8 +360,27 @@ export class PluginDriver {
|
|
|
281
360
|
this.hooks.off(event, handler as never)
|
|
282
361
|
}
|
|
283
362
|
}
|
|
363
|
+
|
|
284
364
|
this.#hookListeners.clear()
|
|
285
|
-
this.#
|
|
365
|
+
this.#eventGeneratorPlugins.clear()
|
|
366
|
+
// Release resolver closures — the driver is rebuilt for each build() call
|
|
367
|
+
// so there is no value in retaining these maps after disposal.
|
|
368
|
+
this.#resolvers.clear()
|
|
369
|
+
this.#defaultResolvers.clear()
|
|
370
|
+
// Release the FileNode cache, parsed adapter graph, and studio state so
|
|
371
|
+
// memory is reclaimed between builds. The returned `BuildOutput.files`
|
|
372
|
+
// array still references any FileNodes the caller needs to inspect.
|
|
373
|
+
this.fileManager.dispose()
|
|
374
|
+
this.inputNode = undefined
|
|
375
|
+
this.#studio = { source: undefined, isOpen: false, inputNode: undefined }
|
|
376
|
+
|
|
377
|
+
for (const [event, handler] of this.#middlewareListeners) {
|
|
378
|
+
this.hooks.off(event, handler as never)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
[Symbol.dispose](): void {
|
|
383
|
+
this.dispose()
|
|
286
384
|
}
|
|
287
385
|
|
|
288
386
|
#trackHookListener(event: keyof KubbHooks, handler: (...args: never[]) => void | Promise<void>): void {
|
|
@@ -294,19 +392,10 @@ export class PluginDriver {
|
|
|
294
392
|
handlers.add(handler)
|
|
295
393
|
}
|
|
296
394
|
|
|
297
|
-
#
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const resolver = defineResolver<PluginFactoryOptions>((_ctx) => ({
|
|
304
|
-
name: 'default',
|
|
305
|
-
pluginName,
|
|
306
|
-
}))
|
|
307
|
-
this.#defaultResolvers.set(pluginName, resolver)
|
|
308
|
-
return resolver
|
|
309
|
-
}
|
|
395
|
+
#getDefaultResolver = memoize(
|
|
396
|
+
this.#defaultResolvers,
|
|
397
|
+
(pluginName: string): Resolver => defineResolver<PluginFactoryOptions>(() => ({ name: 'default', pluginName })),
|
|
398
|
+
)
|
|
310
399
|
|
|
311
400
|
/**
|
|
312
401
|
* Merges `partial` with the plugin's default resolver and stores the result.
|
|
@@ -314,7 +403,7 @@ export class PluginDriver {
|
|
|
314
403
|
* get the up-to-date resolver without going through `getResolver()`.
|
|
315
404
|
*/
|
|
316
405
|
setPluginResolver(pluginName: string, partial: Partial<Resolver>): void {
|
|
317
|
-
const defaultResolver = this.#
|
|
406
|
+
const defaultResolver = this.#getDefaultResolver(pluginName)
|
|
318
407
|
const merged = { ...defaultResolver, ...partial }
|
|
319
408
|
this.#resolvers.set(pluginName, merged)
|
|
320
409
|
const plugin = this.plugins.get(pluginName)
|
|
@@ -332,19 +421,19 @@ export class PluginDriver {
|
|
|
332
421
|
getResolver<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Kubb.PluginRegistry[TName]['resolver']
|
|
333
422
|
getResolver<TResolver extends Resolver = Resolver>(pluginName: string): TResolver
|
|
334
423
|
getResolver(pluginName: string): Resolver {
|
|
335
|
-
return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#
|
|
424
|
+
return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#getDefaultResolver(pluginName)
|
|
336
425
|
}
|
|
337
426
|
|
|
338
427
|
getContext<TOptions extends PluginFactoryOptions>(plugin: NormalizedPlugin<TOptions>): GeneratorContext<TOptions> & Record<string, unknown> {
|
|
339
428
|
const driver = this
|
|
340
429
|
|
|
341
|
-
|
|
430
|
+
return {
|
|
342
431
|
config: driver.config,
|
|
343
432
|
get root(): string {
|
|
344
433
|
return resolve(driver.config.root, driver.config.output.path)
|
|
345
434
|
},
|
|
346
435
|
getMode(output: { path: string }): 'single' | 'split' {
|
|
347
|
-
return
|
|
436
|
+
return KubbDriver.getMode(resolve(driver.config.root, driver.config.output.path, output.path))
|
|
348
437
|
},
|
|
349
438
|
hooks: driver.hooks,
|
|
350
439
|
plugin,
|
|
@@ -358,8 +447,8 @@ export class PluginDriver {
|
|
|
358
447
|
upsertFile: async (...files: Array<FileNode>) => {
|
|
359
448
|
driver.fileManager.upsert(...files)
|
|
360
449
|
},
|
|
361
|
-
get
|
|
362
|
-
return driver.inputNode
|
|
450
|
+
get meta(): InputMeta {
|
|
451
|
+
return driver.inputNode?.meta ?? { circularNames: [], enumNames: [] }
|
|
363
452
|
},
|
|
364
453
|
get adapter(): Adapter | undefined {
|
|
365
454
|
return driver.adapter
|
|
@@ -379,8 +468,8 @@ export class PluginDriver {
|
|
|
379
468
|
info(message: string) {
|
|
380
469
|
driver.hooks.emit('kubb:info', { message })
|
|
381
470
|
},
|
|
382
|
-
openInStudio(options?: DevtoolsOptions) {
|
|
383
|
-
if (!driver.config.devtools || driver.#
|
|
471
|
+
async openInStudio(options?: DevtoolsOptions) {
|
|
472
|
+
if (!driver.config.devtools || driver.#studio.isOpen) {
|
|
384
473
|
return
|
|
385
474
|
}
|
|
386
475
|
|
|
@@ -388,19 +477,19 @@ export class PluginDriver {
|
|
|
388
477
|
throw new Error('Devtools must be an object')
|
|
389
478
|
}
|
|
390
479
|
|
|
391
|
-
if (!driver.
|
|
480
|
+
if (!driver.adapter || !driver.#studio.source) {
|
|
392
481
|
throw new Error('adapter is not defined, make sure you have set the parser in kubb.config.ts')
|
|
393
482
|
}
|
|
394
483
|
|
|
395
|
-
driver.#
|
|
484
|
+
driver.#studio.isOpen = true
|
|
396
485
|
|
|
397
486
|
const studioUrl = driver.config.devtools?.studioUrl ?? DEFAULT_STUDIO_URL
|
|
487
|
+
driver.#studio.inputNode ??= Promise.resolve(driver.adapter.parse(driver.#studio.source))
|
|
488
|
+
const inputNode = await driver.#studio.inputNode
|
|
398
489
|
|
|
399
|
-
return openInStudioFn(
|
|
490
|
+
return openInStudioFn(inputNode, studioUrl, options)
|
|
400
491
|
},
|
|
401
492
|
} as unknown as GeneratorContext<TOptions>
|
|
402
|
-
|
|
403
|
-
return baseContext
|
|
404
493
|
}
|
|
405
494
|
|
|
406
495
|
getPlugin<TName extends keyof Kubb.PluginRegistry>(pluginName: TName): Plugin<Kubb.PluginRegistry[TName]> | undefined
|
|
@@ -422,3 +511,77 @@ export class PluginDriver {
|
|
|
422
511
|
return plugin
|
|
423
512
|
}
|
|
424
513
|
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Handles the return value of a plugin AST hook or generator method.
|
|
517
|
+
*
|
|
518
|
+
* - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`
|
|
519
|
+
* - `Array<FileNode>` → added directly into `driver.fileManager`
|
|
520
|
+
* - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
|
|
521
|
+
*
|
|
522
|
+
* Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result
|
|
523
|
+
* may be a renderer element. Generators that only return `Array<FileNode>` do not need one.
|
|
524
|
+
*/
|
|
525
|
+
export function applyHookResult<TElement = unknown>({
|
|
526
|
+
result,
|
|
527
|
+
driver,
|
|
528
|
+
rendererFactory,
|
|
529
|
+
}: {
|
|
530
|
+
result: TElement | Array<FileNode> | void
|
|
531
|
+
driver: KubbDriver
|
|
532
|
+
rendererFactory?: RendererFactory<TElement>
|
|
533
|
+
}): void | Promise<void> {
|
|
534
|
+
if (!result) return
|
|
535
|
+
|
|
536
|
+
if (Array.isArray(result)) {
|
|
537
|
+
driver.fileManager.upsert(...(result as Array<FileNode>))
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!rendererFactory) {
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const renderer = rendererFactory()
|
|
546
|
+
if (renderer.stream) {
|
|
547
|
+
for (const file of renderer.stream(result)) {
|
|
548
|
+
driver.fileManager.upsert(file)
|
|
549
|
+
}
|
|
550
|
+
renderer.unmount()
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
return applyAsyncRender({ renderer, result, driver })
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function applyAsyncRender<TElement>({
|
|
557
|
+
renderer,
|
|
558
|
+
result,
|
|
559
|
+
driver,
|
|
560
|
+
}: {
|
|
561
|
+
renderer: { render(el: TElement): Promise<void>; files: ReadonlyArray<FileNode>; unmount(): void }
|
|
562
|
+
result: TElement
|
|
563
|
+
driver: KubbDriver
|
|
564
|
+
}): Promise<void> {
|
|
565
|
+
await renderer.render(result)
|
|
566
|
+
driver.fileManager.upsert(...renderer.files)
|
|
567
|
+
renderer.unmount()
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function inputToAdapterSource(config: Config): AdapterSource {
|
|
571
|
+
const input = config.input
|
|
572
|
+
if (!input) {
|
|
573
|
+
throw new Error('[kubb] input is required when using an adapter. Provide input.path or input.data in your config.')
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if ('data' in input) {
|
|
577
|
+
return { type: 'data', data: input.data }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (new URLPath(input.path).isURL) {
|
|
581
|
+
return { type: 'path', path: input.path }
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const resolved = resolve(config.root, input.path)
|
|
585
|
+
|
|
586
|
+
return { type: 'path', path: resolved }
|
|
587
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -3,12 +3,7 @@ import type { FileNode } from '@kubb/ast'
|
|
|
3
3
|
/**
|
|
4
4
|
* Base URL for the Kubb Studio web app.
|
|
5
5
|
*/
|
|
6
|
-
export const DEFAULT_STUDIO_URL = 'https://
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Maximum number of files processed in parallel by FileProcessor.
|
|
10
|
-
*/
|
|
11
|
-
export const PARALLEL_CONCURRENCY_LIMIT = 100
|
|
6
|
+
export const DEFAULT_STUDIO_URL = 'https://kubb.studio' as const
|
|
12
7
|
|
|
13
8
|
/**
|
|
14
9
|
* Default banner style written at the top of every generated file.
|
|
@@ -20,6 +15,16 @@ export const DEFAULT_BANNER = 'simple' as const
|
|
|
20
15
|
*/
|
|
21
16
|
export const DEFAULT_EXTENSION: Record<FileNode['extname'], FileNode['extname'] | ''> = { '.ts': '.ts' }
|
|
22
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Number of file writes to batch in parallel during `flushPendingFiles`.
|
|
20
|
+
*/
|
|
21
|
+
export const STREAM_FLUSH_EVERY = 50
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Number of schema/operation nodes to dispatch concurrently during generation.
|
|
25
|
+
*/
|
|
26
|
+
export const SCHEMA_PARALLEL = 8
|
|
27
|
+
|
|
23
28
|
/**
|
|
24
29
|
* Numeric log-level thresholds used internally to compare verbosity.
|
|
25
30
|
*
|
package/src/createAdapter.ts
CHANGED
|
@@ -1,4 +1,87 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PossiblePromise } from '@internals/utils'
|
|
2
|
+
import type { ImportNode, InputNode, InputStreamNode, SchemaNode } from '@kubb/ast'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Source data passed to an adapter's `parse` function.
|
|
6
|
+
* Mirrors the config input shape with paths resolved to absolute.
|
|
7
|
+
*/
|
|
8
|
+
export type AdapterSource = { type: 'path'; path: string } | { type: 'data'; data: string | unknown } | { type: 'paths'; paths: Array<string> }
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generic type parameters for an adapter definition.
|
|
12
|
+
*
|
|
13
|
+
* - `TName` — unique identifier (e.g. `'oas'`, `'asyncapi'`)
|
|
14
|
+
* - `TOptions` — user-facing options passed to the adapter factory
|
|
15
|
+
* - `TResolvedOptions` — options after defaults applied
|
|
16
|
+
* - `TDocument` — type of the parsed source document
|
|
17
|
+
*/
|
|
18
|
+
export type AdapterFactoryOptions<
|
|
19
|
+
TName extends string = string,
|
|
20
|
+
TOptions extends object = object,
|
|
21
|
+
TResolvedOptions extends object = TOptions,
|
|
22
|
+
TDocument = unknown,
|
|
23
|
+
> = {
|
|
24
|
+
name: TName
|
|
25
|
+
options: TOptions
|
|
26
|
+
resolvedOptions: TResolvedOptions
|
|
27
|
+
document: TDocument
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Adapter that converts input files or data into an `InputNode`.
|
|
32
|
+
*
|
|
33
|
+
* Adapters parse different schema formats (OpenAPI, AsyncAPI, Drizzle, etc.) into Kubb's
|
|
34
|
+
* universal intermediate representation that all plugins consume.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { adapterOas } from '@kubb/adapter-oas'
|
|
39
|
+
*
|
|
40
|
+
* export default defineConfig({
|
|
41
|
+
* adapter: adapterOas(),
|
|
42
|
+
* input: { path: './openapi.yaml' },
|
|
43
|
+
* plugins: [pluginTs(), pluginZod()],
|
|
44
|
+
* })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export type Adapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions> = {
|
|
48
|
+
/**
|
|
49
|
+
* Human-readable adapter identifier (e.g. `'oas'`, `'asyncapi'`).
|
|
50
|
+
*/
|
|
51
|
+
name: TOptions['name']
|
|
52
|
+
/**
|
|
53
|
+
* Resolved adapter options after defaults have been applied.
|
|
54
|
+
*/
|
|
55
|
+
options: TOptions['resolvedOptions']
|
|
56
|
+
/**
|
|
57
|
+
* Parsed source document after the first `parse()` call. `null` before parsing.
|
|
58
|
+
*/
|
|
59
|
+
document: TOptions['document'] | null
|
|
60
|
+
/**
|
|
61
|
+
* Parse the source into a universal `InputNode`.
|
|
62
|
+
*/
|
|
63
|
+
parse: (source: AdapterSource) => PossiblePromise<InputNode>
|
|
64
|
+
/**
|
|
65
|
+
* Extract `ImportNode` entries for a schema tree.
|
|
66
|
+
* Returns an empty array before the first `parse()` call.
|
|
67
|
+
*
|
|
68
|
+
* The `resolve` callback receives the collision-corrected schema name and must
|
|
69
|
+
* return `{ name, path }` for the import, or `undefined` to skip it.
|
|
70
|
+
*/
|
|
71
|
+
getImports: (node: SchemaNode, resolve: (schemaName: string) => { name: string; path: string }) => Array<ImportNode>
|
|
72
|
+
/**
|
|
73
|
+
* Validate the document at the given path or URL.
|
|
74
|
+
*/
|
|
75
|
+
validate: (input: string, options?: { throwOnError?: boolean }) => Promise<void>
|
|
76
|
+
/**
|
|
77
|
+
* Memory-efficient streaming variant of `parse()`.
|
|
78
|
+
*
|
|
79
|
+
* Returns an `InputStreamNode` whose `schemas` and `operations` are `AsyncIterable`.
|
|
80
|
+
* Each `for await` loop creates a fresh parse pass over the cached in-memory document —
|
|
81
|
+
* no pre-built arrays are held in memory.
|
|
82
|
+
*/
|
|
83
|
+
stream?: (source: AdapterSource) => Promise<InputStreamNode>
|
|
84
|
+
}
|
|
2
85
|
|
|
3
86
|
type AdapterBuilder<T extends AdapterFactoryOptions> = (options: T['options']) => Adapter<T>
|
|
4
87
|
|