@kubb/core 5.0.0-alpha.9 → 5.0.0-beta.2
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 +24 -21
- package/dist/PluginDriver-BXibeQk-.cjs +1036 -0
- package/dist/PluginDriver-BXibeQk-.cjs.map +1 -0
- package/dist/PluginDriver-DV3p2Hky.js +945 -0
- package/dist/PluginDriver-DV3p2Hky.js.map +1 -0
- package/dist/index.cjs +752 -1641
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +271 -225
- package/dist/index.js +736 -1609
- package/dist/index.js.map +1 -1
- package/dist/mocks.cjs +145 -0
- package/dist/mocks.cjs.map +1 -0
- package/dist/mocks.d.ts +80 -0
- package/dist/mocks.js +140 -0
- package/dist/mocks.js.map +1 -0
- package/dist/types-CC09VtBt.d.ts +2148 -0
- package/package.json +51 -57
- package/src/FileManager.ts +115 -0
- package/src/FileProcessor.ts +86 -0
- package/src/Kubb.ts +207 -131
- package/src/PluginDriver.ts +325 -564
- package/src/constants.ts +20 -47
- package/src/createAdapter.ts +13 -6
- package/src/createKubb.ts +574 -0
- package/src/createRenderer.ts +57 -0
- package/src/createStorage.ts +13 -1
- package/src/defineGenerator.ts +77 -124
- package/src/defineLogger.ts +4 -2
- package/src/defineMiddleware.ts +62 -0
- package/src/defineParser.ts +44 -0
- package/src/definePlugin.ts +83 -0
- package/src/defineResolver.ts +418 -28
- package/src/devtools.ts +14 -14
- package/src/index.ts +13 -15
- package/src/mocks.ts +178 -0
- package/src/renderNode.ts +35 -0
- package/src/storages/fsStorage.ts +41 -11
- package/src/storages/memoryStorage.ts +4 -2
- package/src/types.ts +1031 -283
- package/src/utils/diagnostics.ts +4 -1
- package/src/utils/isInputPath.ts +10 -0
- package/src/utils/packageJSON.ts +50 -12
- package/dist/PluginDriver-BkFepPdm.d.ts +0 -1054
- package/dist/chunk-ByKO4r7w.cjs +0 -38
- package/dist/hooks.cjs +0 -103
- package/dist/hooks.cjs.map +0 -1
- package/dist/hooks.d.ts +0 -77
- package/dist/hooks.js +0 -98
- package/dist/hooks.js.map +0 -1
- package/src/build.ts +0 -418
- package/src/config.ts +0 -56
- package/src/createPlugin.ts +0 -28
- package/src/hooks/index.ts +0 -4
- package/src/hooks/useKubb.ts +0 -143
- package/src/hooks/useMode.ts +0 -11
- package/src/hooks/usePlugin.ts +0 -11
- package/src/hooks/usePluginDriver.ts +0 -11
- package/src/utils/FunctionParams.ts +0 -155
- package/src/utils/TreeNode.ts +0 -215
- package/src/utils/executeStrategies.ts +0 -81
- package/src/utils/formatters.ts +0 -56
- package/src/utils/getBarrelFiles.ts +0 -141
- package/src/utils/getConfigs.ts +0 -12
- package/src/utils/linters.ts +0 -25
package/src/constants.ts
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FileNode } from '@kubb/ast'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Base URL for the Kubb Studio web app.
|
|
5
|
+
*/
|
|
3
6
|
export const DEFAULT_STUDIO_URL = 'https://studio.kubb.dev' as const
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export const DEFAULT_CONCURRENCY = 15
|
|
10
|
-
|
|
11
|
-
export const BARREL_FILENAME = 'index.ts' as const
|
|
8
|
+
/**
|
|
9
|
+
* Maximum number of files processed in parallel by FileProcessor.
|
|
10
|
+
*/
|
|
11
|
+
export const PARALLEL_CONCURRENCY_LIMIT = 100
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Default banner style written at the top of every generated file.
|
|
15
|
+
*/
|
|
13
16
|
export const DEFAULT_BANNER = 'simple' as const
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Default file-extension mapping used when no explicit mapping is configured.
|
|
20
|
+
*/
|
|
21
|
+
export const DEFAULT_EXTENSION: Record<FileNode['extname'], FileNode['extname'] | ''> = { '.ts': '.ts' }
|
|
18
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Numeric log-level thresholds used internally to compare verbosity.
|
|
25
|
+
*
|
|
26
|
+
* Higher numbers are more verbose.
|
|
27
|
+
*/
|
|
19
28
|
export const logLevel = {
|
|
20
29
|
silent: Number.NEGATIVE_INFINITY,
|
|
21
30
|
error: 0,
|
|
@@ -24,39 +33,3 @@ export const logLevel = {
|
|
|
24
33
|
verbose: 4,
|
|
25
34
|
debug: 5,
|
|
26
35
|
} as const
|
|
27
|
-
|
|
28
|
-
export const linters = {
|
|
29
|
-
eslint: {
|
|
30
|
-
command: 'eslint',
|
|
31
|
-
args: (outputPath: string) => [outputPath, '--fix'],
|
|
32
|
-
errorMessage: 'Eslint not found',
|
|
33
|
-
},
|
|
34
|
-
biome: {
|
|
35
|
-
command: 'biome',
|
|
36
|
-
args: (outputPath: string) => ['lint', '--fix', outputPath],
|
|
37
|
-
errorMessage: 'Biome not found',
|
|
38
|
-
},
|
|
39
|
-
oxlint: {
|
|
40
|
-
command: 'oxlint',
|
|
41
|
-
args: (outputPath: string) => ['--fix', outputPath],
|
|
42
|
-
errorMessage: 'Oxlint not found',
|
|
43
|
-
},
|
|
44
|
-
} as const
|
|
45
|
-
|
|
46
|
-
export const formatters = {
|
|
47
|
-
prettier: {
|
|
48
|
-
command: 'prettier',
|
|
49
|
-
args: (outputPath: string) => ['--ignore-unknown', '--write', outputPath],
|
|
50
|
-
errorMessage: 'Prettier not found',
|
|
51
|
-
},
|
|
52
|
-
biome: {
|
|
53
|
-
command: 'biome',
|
|
54
|
-
args: (outputPath: string) => ['format', '--write', outputPath],
|
|
55
|
-
errorMessage: 'Biome not found',
|
|
56
|
-
},
|
|
57
|
-
oxfmt: {
|
|
58
|
-
command: 'oxfmt',
|
|
59
|
-
args: (outputPath: string) => [outputPath],
|
|
60
|
-
errorMessage: 'Oxfmt not found',
|
|
61
|
-
},
|
|
62
|
-
} as const
|
package/src/createAdapter.ts
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
import type { Adapter, AdapterFactoryOptions } from './types.ts'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Builder type for an {@link Adapter} — takes options and returns the adapter instance.
|
|
5
|
-
*/
|
|
6
3
|
type AdapterBuilder<T extends AdapterFactoryOptions> = (options: T['options']) => Adapter<T>
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
|
-
*
|
|
6
|
+
* Factory for implementing custom adapters that translate non-OpenAPI specs into Kubb's AST.
|
|
7
|
+
*
|
|
8
|
+
* Use this to support GraphQL schemas, gRPC definitions, AsyncAPI, or custom domain-specific languages.
|
|
9
|
+
* Built-in adapters include `@kubb/adapter-oas` for OpenAPI and Swagger documents.
|
|
10
|
+
*
|
|
11
|
+
* @note Adapters must parse their input format to Kubb's `InputNode` structure.
|
|
10
12
|
*
|
|
11
13
|
* @example
|
|
14
|
+
* ```ts
|
|
12
15
|
* export const myAdapter = createAdapter<MyAdapter>((options) => {
|
|
13
16
|
* return {
|
|
14
17
|
* name: 'my-adapter',
|
|
15
18
|
* options,
|
|
16
|
-
* async parse(source) {
|
|
19
|
+
* async parse(source) {
|
|
20
|
+
* // Transform source format to InputNode
|
|
21
|
+
* return { ... }
|
|
22
|
+
* },
|
|
17
23
|
* }
|
|
18
24
|
* })
|
|
19
25
|
*
|
|
20
|
-
* //
|
|
26
|
+
* // Instantiate:
|
|
21
27
|
* const adapter = myAdapter({ validate: true })
|
|
28
|
+
* ```
|
|
22
29
|
*/
|
|
23
30
|
export function createAdapter<T extends AdapterFactoryOptions = AdapterFactoryOptions>(build: AdapterBuilder<T>): (options?: T['options']) => Adapter<T> {
|
|
24
31
|
return (options) => build(options ?? ({} as T['options']))
|
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { AsyncEventEmitter, BuildError, exists, formatMs, getElapsedMs, URLPath } from '@internals/utils'
|
|
3
|
+
import type { FileNode, OperationNode } from '@kubb/ast'
|
|
4
|
+
import { collectUsedSchemaNames, transform, walk } from '@kubb/ast'
|
|
5
|
+
import { DEFAULT_BANNER, DEFAULT_EXTENSION, DEFAULT_STUDIO_URL } from './constants.ts'
|
|
6
|
+
import type { RendererFactory } from './createRenderer.ts'
|
|
7
|
+
import type { Generator } from './defineGenerator.ts'
|
|
8
|
+
import type { Parser } from './defineParser.ts'
|
|
9
|
+
import type { Plugin } from './definePlugin.ts'
|
|
10
|
+
import { FileProcessor } from './FileProcessor.ts'
|
|
11
|
+
import type { Kubb } from './Kubb.ts'
|
|
12
|
+
import { PluginDriver } from './PluginDriver.ts'
|
|
13
|
+
import { applyHookResult } from './renderNode.ts'
|
|
14
|
+
import { fsStorage } from './storages/fsStorage.ts'
|
|
15
|
+
import type { AdapterSource, Config, GeneratorContext, KubbHooks, Middleware, NormalizedPlugin, Storage, UserConfig } from './types.ts'
|
|
16
|
+
import { getDiagnosticInfo } from './utils/diagnostics.ts'
|
|
17
|
+
import { isInputPath } from './utils/isInputPath.ts'
|
|
18
|
+
|
|
19
|
+
type SetupOptions = {
|
|
20
|
+
hooks?: AsyncEventEmitter<KubbHooks>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Full output produced by a successful or failed build.
|
|
25
|
+
*/
|
|
26
|
+
export type BuildOutput = {
|
|
27
|
+
/**
|
|
28
|
+
* Plugins that threw during installation, paired with the caught error.
|
|
29
|
+
*/
|
|
30
|
+
failedPlugins: Set<{ plugin: Plugin; error: Error }>
|
|
31
|
+
files: Array<FileNode>
|
|
32
|
+
driver: PluginDriver
|
|
33
|
+
/**
|
|
34
|
+
* Elapsed time in milliseconds for each plugin, keyed by plugin name.
|
|
35
|
+
*/
|
|
36
|
+
pluginTimings: Map<string, number>
|
|
37
|
+
error?: Error
|
|
38
|
+
/**
|
|
39
|
+
* Raw generated source, keyed by absolute file path.
|
|
40
|
+
*/
|
|
41
|
+
sources: Map<string, string>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type SetupResult = {
|
|
45
|
+
hooks: AsyncEventEmitter<KubbHooks>
|
|
46
|
+
driver: PluginDriver
|
|
47
|
+
sources: Map<string, string>
|
|
48
|
+
config: Config
|
|
49
|
+
storage: Storage | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function setup(userConfig: UserConfig, options: SetupOptions = {}): Promise<SetupResult> {
|
|
53
|
+
const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
|
|
54
|
+
|
|
55
|
+
const sources: Map<string, string> = new Map<string, string>()
|
|
56
|
+
const diagnosticInfo = getDiagnosticInfo()
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(userConfig.input)) {
|
|
59
|
+
await hooks.emit('kubb:warn', { message: 'This feature is still under development — use with caution' })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await hooks.emit('kubb:debug', {
|
|
63
|
+
date: new Date(),
|
|
64
|
+
logs: [
|
|
65
|
+
'Configuration:',
|
|
66
|
+
` • Name: ${userConfig.name || 'unnamed'}`,
|
|
67
|
+
` • Root: ${userConfig.root || process.cwd()}`,
|
|
68
|
+
` • Output: ${userConfig.output?.path || 'not specified'}`,
|
|
69
|
+
` • Plugins: ${userConfig.plugins?.length || 0}`,
|
|
70
|
+
'Output Settings:',
|
|
71
|
+
` • Storage: ${userConfig.storage ? `custom(${userConfig.storage.name})` : userConfig.output?.write === false ? 'disabled' : 'filesystem (default)'}`,
|
|
72
|
+
` • Formatter: ${userConfig.output?.format || 'none'}`,
|
|
73
|
+
` • Linter: ${userConfig.output?.lint || 'none'}`,
|
|
74
|
+
'Environment:',
|
|
75
|
+
Object.entries(diagnosticInfo)
|
|
76
|
+
.map(([key, value]) => ` • ${key}: ${value}`)
|
|
77
|
+
.join('\n'),
|
|
78
|
+
],
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
if (isInputPath(userConfig) && !new URLPath(userConfig.input.path).isURL) {
|
|
83
|
+
await exists(userConfig.input.path)
|
|
84
|
+
|
|
85
|
+
await hooks.emit('kubb:debug', {
|
|
86
|
+
date: new Date(),
|
|
87
|
+
logs: [`✓ Input file validated: ${userConfig.input.path}`],
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
} catch (caughtError) {
|
|
91
|
+
if (isInputPath(userConfig)) {
|
|
92
|
+
const error = caughtError as Error
|
|
93
|
+
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Cannot read file/URL defined in \`input.path\` or set with \`kubb generate PATH\` in the CLI of your Kubb config ${userConfig.input.path}`,
|
|
96
|
+
{
|
|
97
|
+
cause: error,
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!userConfig.adapter) {
|
|
104
|
+
throw new Error('Adapter should be defined')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const config: Config = {
|
|
108
|
+
...userConfig,
|
|
109
|
+
root: userConfig.root || process.cwd(),
|
|
110
|
+
parsers: userConfig.parsers ?? [],
|
|
111
|
+
adapter: userConfig.adapter,
|
|
112
|
+
output: {
|
|
113
|
+
format: false,
|
|
114
|
+
lint: false,
|
|
115
|
+
write: true,
|
|
116
|
+
extension: DEFAULT_EXTENSION,
|
|
117
|
+
defaultBanner: DEFAULT_BANNER,
|
|
118
|
+
...userConfig.output,
|
|
119
|
+
},
|
|
120
|
+
devtools: userConfig.devtools
|
|
121
|
+
? {
|
|
122
|
+
studioUrl: DEFAULT_STUDIO_URL,
|
|
123
|
+
...(typeof userConfig.devtools === 'boolean' ? {} : userConfig.devtools),
|
|
124
|
+
}
|
|
125
|
+
: undefined,
|
|
126
|
+
plugins: userConfig.plugins as unknown as Config['plugins'],
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const storage: Storage | null = config.output.write === false ? null : (config.storage ?? fsStorage())
|
|
130
|
+
|
|
131
|
+
if (config.output.clean) {
|
|
132
|
+
await hooks.emit('kubb:debug', {
|
|
133
|
+
date: new Date(),
|
|
134
|
+
logs: ['Cleaning output directories', ` • Output: ${config.output.path}`],
|
|
135
|
+
})
|
|
136
|
+
await storage?.clear(resolve(config.root, config.output.path))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const driver = new PluginDriver(config, {
|
|
140
|
+
hooks,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// Register middleware hooks after all plugin hooks are registered.
|
|
144
|
+
// Because AsyncEventEmitter calls listeners in registration order,
|
|
145
|
+
// middleware hooks for any event fire after all plugin hooks for that event.
|
|
146
|
+
function registerMiddlewareHook<K extends keyof KubbHooks & string>(event: K, middlewareHooks: Middleware['hooks']) {
|
|
147
|
+
const handler = middlewareHooks[event]
|
|
148
|
+
if (handler) {
|
|
149
|
+
hooks.on(event, handler)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const middleware of config.middleware ?? []) {
|
|
154
|
+
for (const event of Object.keys(middleware.hooks) as Array<keyof KubbHooks & string>) {
|
|
155
|
+
registerMiddlewareHook(event, middleware.hooks)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const adapter = config.adapter
|
|
160
|
+
if (!adapter) {
|
|
161
|
+
throw new Error('No adapter configured. Please provide an adapter in your kubb.config.ts.')
|
|
162
|
+
}
|
|
163
|
+
const source = inputToAdapterSource(config)
|
|
164
|
+
|
|
165
|
+
await hooks.emit('kubb:debug', {
|
|
166
|
+
date: new Date(),
|
|
167
|
+
logs: [`Running adapter: ${adapter.name}`],
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
driver.adapter = adapter
|
|
171
|
+
driver.inputNode = await adapter.parse(source)
|
|
172
|
+
|
|
173
|
+
await hooks.emit('kubb:debug', {
|
|
174
|
+
date: new Date(),
|
|
175
|
+
logs: [
|
|
176
|
+
`✓ Adapter '${adapter.name}' resolved InputNode`,
|
|
177
|
+
` • Schemas: ${driver.inputNode.schemas.length}`,
|
|
178
|
+
` • Operations: ${driver.inputNode.operations.length}`,
|
|
179
|
+
],
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
config,
|
|
184
|
+
hooks,
|
|
185
|
+
driver,
|
|
186
|
+
sources,
|
|
187
|
+
storage,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Walks the AST and dispatches nodes to a plugin's direct AST hooks
|
|
193
|
+
* (`schema`, `operation`, `operations`).
|
|
194
|
+
*
|
|
195
|
+
* When `include` contains only operation-scoped filters (`tag`, `operationId`, `path`,
|
|
196
|
+
* `method`, `contentType`) and no `schemaName` filter, the function pre-computes the set
|
|
197
|
+
* of top-level schema names transitively reachable from the included operations and skips
|
|
198
|
+
* schemas that fall outside that set. This ensures that component schemas referenced
|
|
199
|
+
* exclusively by excluded operations are not generated.
|
|
200
|
+
*/
|
|
201
|
+
async function runPluginAstHooks(plugin: NormalizedPlugin, context: GeneratorContext): Promise<void> {
|
|
202
|
+
const { adapter, inputNode, resolver, driver } = context
|
|
203
|
+
const { exclude, include, override } = plugin.options
|
|
204
|
+
|
|
205
|
+
if (!adapter || !inputNode) {
|
|
206
|
+
throw new Error(`[${plugin.name}] No adapter found. Add an OAS adapter (e.g. pluginOas()) before this plugin in your Kubb config.`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveRenderer(gen: Generator): RendererFactory | undefined {
|
|
210
|
+
return gen.renderer === null ? undefined : (gen.renderer ?? plugin.renderer ?? context.config.renderer)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const generators = plugin.generators ?? []
|
|
214
|
+
const collectedOperations: Array<OperationNode> = []
|
|
215
|
+
|
|
216
|
+
const generatorContext = {
|
|
217
|
+
...context,
|
|
218
|
+
resolver: driver.getResolver(plugin.name),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// When `include` has operation-based filters (tag, operationId, path, method, contentType)
|
|
222
|
+
// but no schema-level filters (schemaName), pre-compute the set of top-level schema names
|
|
223
|
+
// that are transitively referenced by the included operations. Schemas outside that set are
|
|
224
|
+
// skipped so that types belonging exclusively to excluded operations are not generated.
|
|
225
|
+
const operationFilterTypes = new Set(['tag', 'operationId', 'path', 'method', 'contentType'])
|
|
226
|
+
const hasOperationBasedIncludes = include?.some(({ type }) => operationFilterTypes.has(type)) ?? false
|
|
227
|
+
const hasSchemaNameIncludes = include?.some(({ type }) => type === 'schemaName') ?? false
|
|
228
|
+
|
|
229
|
+
let allowedSchemaNames: Set<string> | undefined
|
|
230
|
+
if (hasOperationBasedIncludes && !hasSchemaNameIncludes) {
|
|
231
|
+
const includedOps = inputNode.operations.filter((op) => resolver.resolveOptions(op, { options: plugin.options, exclude, include, override }) !== null)
|
|
232
|
+
allowedSchemaNames = collectUsedSchemaNames(includedOps, inputNode.schemas)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await walk(inputNode, {
|
|
236
|
+
depth: 'shallow',
|
|
237
|
+
async schema(node) {
|
|
238
|
+
const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
|
|
239
|
+
|
|
240
|
+
// Skip named top-level schemas that are not reachable from any included operation.
|
|
241
|
+
if (allowedSchemaNames !== undefined && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) {
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const options = resolver.resolveOptions(transformedNode, {
|
|
246
|
+
options: plugin.options,
|
|
247
|
+
exclude,
|
|
248
|
+
include,
|
|
249
|
+
override,
|
|
250
|
+
})
|
|
251
|
+
if (options === null) return
|
|
252
|
+
|
|
253
|
+
const ctx = { ...generatorContext, options }
|
|
254
|
+
|
|
255
|
+
for (const gen of generators) {
|
|
256
|
+
if (!gen.schema) continue
|
|
257
|
+
const result = await gen.schema(transformedNode, ctx)
|
|
258
|
+
await applyHookResult(result, driver, resolveRenderer(gen))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await driver.hooks.emit('kubb:generate:schema', transformedNode, ctx)
|
|
262
|
+
},
|
|
263
|
+
async operation(node) {
|
|
264
|
+
const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
|
|
265
|
+
const options = resolver.resolveOptions(transformedNode, {
|
|
266
|
+
options: plugin.options,
|
|
267
|
+
exclude,
|
|
268
|
+
include,
|
|
269
|
+
override,
|
|
270
|
+
})
|
|
271
|
+
if (options !== null) {
|
|
272
|
+
collectedOperations.push(transformedNode)
|
|
273
|
+
|
|
274
|
+
const ctx = { ...generatorContext, options }
|
|
275
|
+
|
|
276
|
+
for (const gen of generators) {
|
|
277
|
+
if (!gen.operation) continue
|
|
278
|
+
const result = await gen.operation(transformedNode, ctx)
|
|
279
|
+
await applyHookResult(result, driver, resolveRenderer(gen))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await driver.hooks.emit('kubb:generate:operation', transformedNode, ctx)
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
if (collectedOperations.length > 0) {
|
|
288
|
+
const ctx = { ...generatorContext, options: plugin.options }
|
|
289
|
+
|
|
290
|
+
for (const gen of generators) {
|
|
291
|
+
if (!gen.operations) continue
|
|
292
|
+
const result = await gen.operations(collectedOperations, ctx)
|
|
293
|
+
await applyHookResult(result, driver, resolveRenderer(gen))
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await driver.hooks.emit('kubb:generate:operations', collectedOperations, ctx)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function safeBuild(setupResult: SetupResult): Promise<BuildOutput> {
|
|
301
|
+
const { driver, hooks, sources, storage } = setupResult
|
|
302
|
+
|
|
303
|
+
const failedPlugins = new Set<{ plugin: Plugin; error: Error }>()
|
|
304
|
+
const pluginTimings = new Map<string, number>()
|
|
305
|
+
const config = driver.config
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
await driver.emitSetupHooks()
|
|
309
|
+
|
|
310
|
+
if (driver.adapter && driver.inputNode) {
|
|
311
|
+
await hooks.emit('kubb:build:start', {
|
|
312
|
+
config,
|
|
313
|
+
adapter: driver.adapter,
|
|
314
|
+
inputNode: driver.inputNode,
|
|
315
|
+
getPlugin: driver.getPlugin.bind(driver),
|
|
316
|
+
get files() {
|
|
317
|
+
return driver.fileManager.files
|
|
318
|
+
},
|
|
319
|
+
upsertFile: (...files) => driver.fileManager.upsert(...files),
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
for (const plugin of driver.plugins.values()) {
|
|
324
|
+
const context = driver.getContext(plugin)
|
|
325
|
+
const hrStart = process.hrtime()
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
const timestamp = new Date()
|
|
329
|
+
|
|
330
|
+
await hooks.emit('kubb:plugin:start', { plugin })
|
|
331
|
+
|
|
332
|
+
await hooks.emit('kubb:debug', {
|
|
333
|
+
date: timestamp,
|
|
334
|
+
logs: ['Starting plugin...', ` • Plugin Name: ${plugin.name}`],
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
if (plugin.generators?.length || driver.hasRegisteredGenerators(plugin.name)) {
|
|
338
|
+
await runPluginAstHooks(plugin, context)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const duration = getElapsedMs(hrStart)
|
|
342
|
+
pluginTimings.set(plugin.name, duration)
|
|
343
|
+
|
|
344
|
+
await hooks.emit('kubb:plugin:end', {
|
|
345
|
+
plugin,
|
|
346
|
+
duration,
|
|
347
|
+
success: true,
|
|
348
|
+
config,
|
|
349
|
+
get files() {
|
|
350
|
+
return driver.fileManager.files
|
|
351
|
+
},
|
|
352
|
+
upsertFile: (...files) => driver.fileManager.upsert(...files),
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
await hooks.emit('kubb:debug', {
|
|
356
|
+
date: new Date(),
|
|
357
|
+
logs: [`✓ Plugin started successfully (${formatMs(duration)})`],
|
|
358
|
+
})
|
|
359
|
+
} catch (caughtError) {
|
|
360
|
+
const error = caughtError as Error
|
|
361
|
+
const errorTimestamp = new Date()
|
|
362
|
+
const duration = getElapsedMs(hrStart)
|
|
363
|
+
|
|
364
|
+
await hooks.emit('kubb:plugin:end', {
|
|
365
|
+
plugin,
|
|
366
|
+
duration,
|
|
367
|
+
success: false,
|
|
368
|
+
error,
|
|
369
|
+
config,
|
|
370
|
+
get files() {
|
|
371
|
+
return driver.fileManager.files
|
|
372
|
+
},
|
|
373
|
+
upsertFile: (...files) => driver.fileManager.upsert(...files),
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
await hooks.emit('kubb:debug', {
|
|
377
|
+
date: errorTimestamp,
|
|
378
|
+
logs: [
|
|
379
|
+
'✗ Plugin start failed',
|
|
380
|
+
` • Plugin Name: ${plugin.name}`,
|
|
381
|
+
` • Error: ${error.constructor.name} - ${error.message}`,
|
|
382
|
+
' • Stack Trace:',
|
|
383
|
+
error.stack || 'No stack trace available',
|
|
384
|
+
],
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
failedPlugins.add({ plugin, error })
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
await hooks.emit('kubb:plugins:end', {
|
|
392
|
+
config,
|
|
393
|
+
get files() {
|
|
394
|
+
return driver.fileManager.files
|
|
395
|
+
},
|
|
396
|
+
upsertFile: (...files) => driver.fileManager.upsert(...files),
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
const files = driver.fileManager.files
|
|
400
|
+
|
|
401
|
+
const parsersMap = new Map<FileNode['extname'], Parser>()
|
|
402
|
+
for (const parser of config.parsers) {
|
|
403
|
+
if (parser.extNames) {
|
|
404
|
+
for (const extname of parser.extNames) {
|
|
405
|
+
parsersMap.set(extname, parser)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const fileProcessor = new FileProcessor()
|
|
411
|
+
|
|
412
|
+
await hooks.emit('kubb:debug', {
|
|
413
|
+
date: new Date(),
|
|
414
|
+
logs: [`Writing ${files.length} files...`],
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
await fileProcessor.run(files, {
|
|
418
|
+
parsers: parsersMap,
|
|
419
|
+
extension: config.output.extension,
|
|
420
|
+
onStart: async (processingFiles) => {
|
|
421
|
+
await hooks.emit('kubb:files:processing:start', { files: processingFiles })
|
|
422
|
+
},
|
|
423
|
+
onUpdate: async ({ file, source, processed, total, percentage }) => {
|
|
424
|
+
await hooks.emit('kubb:file:processing:update', {
|
|
425
|
+
file,
|
|
426
|
+
source,
|
|
427
|
+
processed,
|
|
428
|
+
total,
|
|
429
|
+
percentage,
|
|
430
|
+
config,
|
|
431
|
+
})
|
|
432
|
+
if (source) {
|
|
433
|
+
await storage?.setItem(file.path, source)
|
|
434
|
+
sources.set(file.path, source)
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
onEnd: async (processedFiles) => {
|
|
438
|
+
await hooks.emit('kubb:files:processing:end', { files: processedFiles })
|
|
439
|
+
await hooks.emit('kubb:debug', {
|
|
440
|
+
date: new Date(),
|
|
441
|
+
logs: [`✓ File write process completed for ${processedFiles.length} files`],
|
|
442
|
+
})
|
|
443
|
+
},
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
await hooks.emit('kubb:build:end', {
|
|
447
|
+
files,
|
|
448
|
+
config,
|
|
449
|
+
outputDir: resolve(config.root, config.output.path),
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
failedPlugins,
|
|
454
|
+
files,
|
|
455
|
+
driver,
|
|
456
|
+
pluginTimings,
|
|
457
|
+
sources,
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return {
|
|
461
|
+
failedPlugins,
|
|
462
|
+
files: [],
|
|
463
|
+
driver,
|
|
464
|
+
pluginTimings,
|
|
465
|
+
error: error as Error,
|
|
466
|
+
sources,
|
|
467
|
+
}
|
|
468
|
+
} finally {
|
|
469
|
+
driver.dispose()
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function build(setupResult: SetupResult): Promise<BuildOutput> {
|
|
474
|
+
const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(setupResult)
|
|
475
|
+
|
|
476
|
+
if (error) {
|
|
477
|
+
throw error
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (failedPlugins.size > 0) {
|
|
481
|
+
const errors = [...failedPlugins].map(({ error }) => error)
|
|
482
|
+
|
|
483
|
+
throw new BuildError(`Build Error with ${failedPlugins.size} failed plugins`, { errors })
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
failedPlugins,
|
|
488
|
+
files,
|
|
489
|
+
driver,
|
|
490
|
+
pluginTimings,
|
|
491
|
+
error: undefined,
|
|
492
|
+
sources,
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function inputToAdapterSource(config: Config): AdapterSource {
|
|
497
|
+
if (Array.isArray(config.input)) {
|
|
498
|
+
return {
|
|
499
|
+
type: 'paths',
|
|
500
|
+
paths: config.input.map((i) => (new URLPath(i.path).isURL ? i.path : resolve(config.root, i.path))),
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if ('data' in config.input) {
|
|
505
|
+
return { type: 'data', data: config.input.data }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (new URLPath(config.input.path).isURL) {
|
|
509
|
+
return { type: 'path', path: config.input.path }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const resolved = resolve(config.root, config.input.path)
|
|
513
|
+
return { type: 'path', path: resolved }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
type CreateKubbOptions = {
|
|
517
|
+
hooks?: AsyncEventEmitter<KubbHooks>
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Creates a Kubb instance bound to a single config entry.
|
|
522
|
+
*
|
|
523
|
+
* Accepts a user-facing config shape and resolves it to a full {@link Config} during
|
|
524
|
+
* `setup()`. The instance then holds shared state (`hooks`, `sources`, `driver`, `config`)
|
|
525
|
+
* across the `setup → build` lifecycle. Attach event listeners to `kubb.hooks` before
|
|
526
|
+
* calling `setup()` or `build()`.
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```ts
|
|
530
|
+
* const kubb = createKubb(userConfig)
|
|
531
|
+
*
|
|
532
|
+
* kubb.hooks.on('kubb:plugin:end', ({ plugin, duration }) => {
|
|
533
|
+
* console.log(`${plugin.name} completed in ${duration}ms`)
|
|
534
|
+
* })
|
|
535
|
+
*
|
|
536
|
+
* const { files, failedPlugins } = await kubb.safeBuild()
|
|
537
|
+
* ```
|
|
538
|
+
*/
|
|
539
|
+
export function createKubb(userConfig: UserConfig, options: CreateKubbOptions = {}): Kubb {
|
|
540
|
+
const hooks = options.hooks ?? new AsyncEventEmitter<KubbHooks>()
|
|
541
|
+
let setupResult: SetupResult | undefined
|
|
542
|
+
|
|
543
|
+
const instance: Kubb = {
|
|
544
|
+
get hooks() {
|
|
545
|
+
return hooks
|
|
546
|
+
},
|
|
547
|
+
get sources() {
|
|
548
|
+
return setupResult?.sources ?? new Map()
|
|
549
|
+
},
|
|
550
|
+
get driver() {
|
|
551
|
+
return setupResult?.driver
|
|
552
|
+
},
|
|
553
|
+
get config() {
|
|
554
|
+
return setupResult?.config
|
|
555
|
+
},
|
|
556
|
+
async setup() {
|
|
557
|
+
setupResult = await setup(userConfig, { hooks })
|
|
558
|
+
},
|
|
559
|
+
async build() {
|
|
560
|
+
if (!setupResult) {
|
|
561
|
+
await instance.setup()
|
|
562
|
+
}
|
|
563
|
+
return build(setupResult!)
|
|
564
|
+
},
|
|
565
|
+
async safeBuild() {
|
|
566
|
+
if (!setupResult) {
|
|
567
|
+
await instance.setup()
|
|
568
|
+
}
|
|
569
|
+
return safeBuild(setupResult!)
|
|
570
|
+
},
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return instance
|
|
574
|
+
}
|