@kubb/core 5.0.0-beta.10 → 5.0.0-beta.100
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/LICENSE +17 -10
- package/README.md +20 -123
- package/dist/index.cjs +2272 -1132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +131 -179
- package/dist/index.js +2256 -1117
- package/dist/index.js.map +1 -1
- package/dist/mocks.cjs +81 -32
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +37 -14
- package/dist/mocks.js +83 -36
- package/dist/mocks.js.map +1 -1
- package/dist/types-DM7-MGjZ.d.ts +2838 -0
- package/dist/usingCtx-BNggxUEL.js +597 -0
- package/dist/usingCtx-BNggxUEL.js.map +1 -0
- package/dist/usingCtx-CZyLSqds.cjs +705 -0
- package/dist/usingCtx-CZyLSqds.cjs.map +1 -0
- package/package.json +7 -28
- package/dist/PluginDriver-Cu1Kj9S-.cjs +0 -1075
- package/dist/PluginDriver-Cu1Kj9S-.cjs.map +0 -1
- package/dist/PluginDriver-D8Z0Htid.js +0 -978
- package/dist/PluginDriver-D8Z0Htid.js.map +0 -1
- package/dist/createKubb-ALdb8lmq.d.ts +0 -2082
- package/src/FileManager.ts +0 -115
- package/src/FileProcessor.ts +0 -86
- package/src/PluginDriver.ts +0 -457
- package/src/constants.ts +0 -35
- package/src/createAdapter.ts +0 -108
- package/src/createKubb.ts +0 -1266
- package/src/createRenderer.ts +0 -57
- package/src/createStorage.ts +0 -70
- package/src/defineGenerator.ts +0 -175
- package/src/defineLogger.ts +0 -58
- package/src/defineMiddleware.ts +0 -62
- package/src/defineParser.ts +0 -44
- package/src/definePlugin.ts +0 -379
- package/src/defineResolver.ts +0 -654
- package/src/devtools.ts +0 -66
- package/src/index.ts +0 -20
- package/src/mocks.ts +0 -177
- package/src/storages/fsStorage.ts +0 -89
- package/src/storages/memoryStorage.ts +0 -55
- package/src/types.ts +0 -41
- /package/dist/{chunk--u3MIqq1.js → rolldown-runtime-C0LytTxp.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,150 +1,65 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
1
|
+
import "./rolldown-runtime-C0LytTxp.js";
|
|
2
|
+
import { a as isPathInside, c as write, d as toError, f as camelCase, i as clean, l as BuildError, n as FileManager, o as toFilePath, r as Hookable, s as toPosixPath, t as _usingCtx, u as getErrorMessage } from "./usingCtx-BNggxUEL.js";
|
|
3
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
+
import { stripVTControlCharacters, styleText } from "node:util";
|
|
5
|
+
import { hash } from "node:crypto";
|
|
6
|
+
import { access, glob, readFile, rm } from "node:fs/promises";
|
|
7
|
+
import path, { join, relative, resolve } from "node:path";
|
|
8
|
+
import { ast, collectSync, collectUsedSchemaNames, composeMacros, narrowSchema, operationDef, resolveRefName, schemaDef, transform } from "@kubb/ast";
|
|
9
|
+
import process$1 from "node:process";
|
|
10
|
+
//#region src/createAdapter.ts
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* Defines a custom adapter that translates a spec format into Kubb's universal
|
|
13
|
+
* AST, for example GraphQL, gRPC, or AsyncAPI. The built-in `@kubb/adapter-oas`
|
|
14
|
+
* handles OpenAPI/Swagger documents.
|
|
13
15
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* throw new BuildError('Build failed', { errors: [err1, err2] })
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
var BuildError = class extends Error {
|
|
20
|
-
errors;
|
|
21
|
-
constructor(message, options) {
|
|
22
|
-
super(message, { cause: options.cause });
|
|
23
|
-
this.name = "BuildError";
|
|
24
|
-
this.errors = options.errors;
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* Coerces an unknown thrown value to an `Error` instance.
|
|
29
|
-
* Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`.
|
|
16
|
+
* Adapters must return an `InputNode` from `parse`. That node is what every
|
|
17
|
+
* plugin in the build consumes.
|
|
30
18
|
*
|
|
31
19
|
* @example
|
|
32
20
|
* ```ts
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
21
|
+
* import { createAdapter, type AdapterFactoryOptions } from '@kubb/core'
|
|
22
|
+
* import { ast } from '@kubb/ast'
|
|
23
|
+
*
|
|
24
|
+
* type MyAdapter = AdapterFactoryOptions<'my-adapter', { validate?: boolean }>
|
|
25
|
+
*
|
|
26
|
+
* export const myAdapter = createAdapter<MyAdapter>((options) => ({
|
|
27
|
+
* name: 'my-adapter',
|
|
28
|
+
* options,
|
|
29
|
+
* document: null,
|
|
30
|
+
* async parse(_source) {
|
|
31
|
+
* // Convert the source (path or inline data) into an InputNode.
|
|
32
|
+
* return ast.factory.createInput()
|
|
33
|
+
* },
|
|
34
|
+
* async validate() {
|
|
35
|
+
* // Throw here when the spec is invalid.
|
|
36
|
+
* },
|
|
37
|
+
* }))
|
|
36
38
|
* ```
|
|
37
39
|
*/
|
|
38
|
-
function
|
|
39
|
-
return
|
|
40
|
+
function createAdapter(build) {
|
|
41
|
+
return (options) => build(options ?? {});
|
|
40
42
|
}
|
|
41
43
|
//#endregion
|
|
42
|
-
//#region
|
|
44
|
+
//#region src/applyConfigDefaults.ts
|
|
43
45
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* const emitter = new AsyncEventEmitter<{ build: [name: string] }>()
|
|
50
|
-
* emitter.on('build', async (name) => { console.log(name) })
|
|
51
|
-
* await emitter.emit('build', 'petstore') // all listeners awaited
|
|
52
|
-
* ```
|
|
46
|
+
* Fills in the config defaults shared by `defineConfig` and the unplugin factory: the fallback
|
|
47
|
+
* adapter, `defaultOutput`'s fields, and appending the barrel plugin when it's not already
|
|
48
|
+
* registered. Both entry points construct their own adapter, barrel plugin, and output defaults
|
|
49
|
+
* (`barrel` is a `@kubb/plugin-barrel` extension field core doesn't know about) and pass them in,
|
|
50
|
+
* so `@kubb/core` doesn't need to depend on `@kubb/adapter-oas` or `@kubb/plugin-barrel`.
|
|
53
51
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
#emitter = new EventEmitter();
|
|
63
|
-
/**
|
|
64
|
-
* Emits `eventName` and awaits all registered listeners sequentially.
|
|
65
|
-
* Throws if any listener rejects, wrapping the cause with the event name and serialized arguments.
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* ```ts
|
|
69
|
-
* await emitter.emit('build', 'petstore')
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
async emit(eventName, ...eventArgs) {
|
|
73
|
-
const listeners = this.#emitter.listeners(eventName);
|
|
74
|
-
if (listeners.length === 0) return;
|
|
75
|
-
for (const listener of listeners) try {
|
|
76
|
-
await listener(...eventArgs);
|
|
77
|
-
} catch (err) {
|
|
78
|
-
let serializedArgs;
|
|
79
|
-
try {
|
|
80
|
-
serializedArgs = JSON.stringify(eventArgs);
|
|
81
|
-
} catch {
|
|
82
|
-
serializedArgs = String(eventArgs);
|
|
83
|
-
}
|
|
84
|
-
throw new Error(`Error in async listener for "${eventName}" with eventArgs ${serializedArgs}`, { cause: toError(err) });
|
|
52
|
+
function applyConfigDefaults(config, { defaultAdapter, barrelPlugin, barrelPluginName, defaultOutput }) {
|
|
53
|
+
const plugins = config.plugins?.some((plugin) => plugin.name === barrelPluginName) ? config.plugins ?? [] : [...config.plugins ?? [], barrelPlugin];
|
|
54
|
+
return {
|
|
55
|
+
adapter: config.adapter ?? defaultAdapter,
|
|
56
|
+
plugins,
|
|
57
|
+
output: {
|
|
58
|
+
...defaultOutput,
|
|
59
|
+
...config.output
|
|
85
60
|
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
* Registers a persistent listener for `eventName`.
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* ```ts
|
|
92
|
-
* emitter.on('build', async (name) => { console.log(name) })
|
|
93
|
-
* ```
|
|
94
|
-
*/
|
|
95
|
-
on(eventName, handler) {
|
|
96
|
-
this.#emitter.on(eventName, handler);
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Registers a one-shot listener that removes itself after the first invocation.
|
|
100
|
-
*
|
|
101
|
-
* @example
|
|
102
|
-
* ```ts
|
|
103
|
-
* emitter.onOnce('build', async (name) => { console.log(name) })
|
|
104
|
-
* ```
|
|
105
|
-
*/
|
|
106
|
-
onOnce(eventName, handler) {
|
|
107
|
-
const wrapper = (...args) => {
|
|
108
|
-
this.off(eventName, wrapper);
|
|
109
|
-
return handler(...args);
|
|
110
|
-
};
|
|
111
|
-
this.on(eventName, wrapper);
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Removes a previously registered listener.
|
|
115
|
-
*
|
|
116
|
-
* @example
|
|
117
|
-
* ```ts
|
|
118
|
-
* emitter.off('build', handler)
|
|
119
|
-
* ```
|
|
120
|
-
*/
|
|
121
|
-
off(eventName, handler) {
|
|
122
|
-
this.#emitter.off(eventName, handler);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Returns the number of listeners registered for `eventName`.
|
|
126
|
-
*
|
|
127
|
-
* @example
|
|
128
|
-
* ```ts
|
|
129
|
-
* emitter.on('build', handler)
|
|
130
|
-
* emitter.listenerCount('build') // 1
|
|
131
|
-
* ```
|
|
132
|
-
*/
|
|
133
|
-
listenerCount(eventName) {
|
|
134
|
-
return this.#emitter.listenerCount(eventName);
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Removes all listeners from every event channel.
|
|
138
|
-
*
|
|
139
|
-
* @example
|
|
140
|
-
* ```ts
|
|
141
|
-
* emitter.removeAll()
|
|
142
|
-
* ```
|
|
143
|
-
*/
|
|
144
|
-
removeAll() {
|
|
145
|
-
this.#emitter.removeAllListeners();
|
|
146
|
-
}
|
|
147
|
-
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
148
63
|
//#endregion
|
|
149
64
|
//#region ../../internals/utils/src/time.ts
|
|
150
65
|
/**
|
|
@@ -179,560 +94,1853 @@ function formatMs(ms) {
|
|
|
179
94
|
return `${Math.round(ms)}ms`;
|
|
180
95
|
}
|
|
181
96
|
//#endregion
|
|
182
|
-
//#region ../../internals/utils/src/
|
|
97
|
+
//#region ../../internals/utils/src/colors.ts
|
|
183
98
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
99
|
+
* Parses a CSS hex color string (`#RGB`) into its RGB channels.
|
|
100
|
+
* Falls back to `255` for any channel that cannot be parsed.
|
|
101
|
+
*/
|
|
102
|
+
function parseHex(color) {
|
|
103
|
+
const int = Number.parseInt(color.replace("#", ""), 16);
|
|
104
|
+
return Number.isNaN(int) ? {
|
|
105
|
+
r: 255,
|
|
106
|
+
g: 255,
|
|
107
|
+
b: 255
|
|
108
|
+
} : {
|
|
109
|
+
r: int >> 16 & 255,
|
|
110
|
+
g: int >> 8 & 255,
|
|
111
|
+
b: int & 255
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Returns a function that wraps a string in a 24-bit ANSI true-color escape sequence
|
|
116
|
+
* for the given hex color.
|
|
193
117
|
*/
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return
|
|
118
|
+
function hex(color) {
|
|
119
|
+
const { r, g, b } = parseHex(color);
|
|
120
|
+
return (text) => `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
197
121
|
}
|
|
122
|
+
hex("#F55A17"), hex("#F5A217"), hex("#F58517"), hex("#B45309"), hex("#FFFFFF"), hex("#adadc6"), hex("#FDA4AF");
|
|
123
|
+
/**
|
|
124
|
+
* ANSI color names used by {@link randomCliColor} for deterministic terminal coloring.
|
|
125
|
+
*/
|
|
126
|
+
const randomColors = [
|
|
127
|
+
"black",
|
|
128
|
+
"red",
|
|
129
|
+
"green",
|
|
130
|
+
"yellow",
|
|
131
|
+
"blue",
|
|
132
|
+
"white",
|
|
133
|
+
"magenta",
|
|
134
|
+
"cyan",
|
|
135
|
+
"gray"
|
|
136
|
+
];
|
|
198
137
|
/**
|
|
199
|
-
*
|
|
200
|
-
* Skips the write when the trimmed content is empty or identical to what is already on disk.
|
|
201
|
-
* Creates any missing parent directories automatically.
|
|
202
|
-
* When `sanity` is `true`, re-reads the file after writing and throws if the content does not match.
|
|
138
|
+
* Wraps `text` in a deterministic ANSI color derived from the text's SHA-256 hash.
|
|
203
139
|
*
|
|
204
140
|
* @example
|
|
205
141
|
* ```ts
|
|
206
|
-
*
|
|
207
|
-
* await write('./src/Pet.ts', source) // null — file unchanged
|
|
208
|
-
* await write('./src/Pet.ts', ' ') // null — empty content skipped
|
|
142
|
+
* randomCliColor('petstore') // '\x1b[33m' + 'petstore' + '\x1b[39m' (always the same color for 'petstore')
|
|
209
143
|
* ```
|
|
210
144
|
*/
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (typeof Bun !== "undefined") {
|
|
216
|
-
const file = Bun.file(resolved);
|
|
217
|
-
if ((await file.exists() ? await file.text() : null) === trimmed) return null;
|
|
218
|
-
await Bun.write(resolved, trimmed);
|
|
219
|
-
return trimmed;
|
|
220
|
-
}
|
|
221
|
-
try {
|
|
222
|
-
if (await readFile(resolved, { encoding: "utf-8" }) === trimmed) return null;
|
|
223
|
-
} catch {}
|
|
224
|
-
await mkdir(dirname(resolved), { recursive: true });
|
|
225
|
-
await writeFile(resolved, trimmed, { encoding: "utf-8" });
|
|
226
|
-
if (options.sanity) {
|
|
227
|
-
const savedData = await readFile(resolved, { encoding: "utf-8" });
|
|
228
|
-
if (savedData !== trimmed) throw new Error(`Sanity check failed for ${path}\n\nData[${data.length}]:\n${data}\n\nSaved[${savedData.length}]:\n${savedData}\n`);
|
|
229
|
-
return savedData;
|
|
230
|
-
}
|
|
231
|
-
return trimmed;
|
|
145
|
+
function randomCliColor(text) {
|
|
146
|
+
if (!text) return "";
|
|
147
|
+
const index = hash("sha256", text, "buffer").readUInt32BE(0) % randomColors.length;
|
|
148
|
+
return styleText(randomColors[index] ?? "white", text);
|
|
232
149
|
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region ../../internals/utils/src/promise.ts
|
|
233
152
|
/**
|
|
234
|
-
*
|
|
153
|
+
* Wraps `factory` with a keyed cache backed by the provided store.
|
|
235
154
|
*
|
|
236
|
-
*
|
|
155
|
+
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
156
|
+
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
157
|
+
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
158
|
+
* inner (created once per outer miss) keyed by the second.
|
|
159
|
+
*
|
|
160
|
+
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
161
|
+
* cleared independently of the memoized function.
|
|
162
|
+
*
|
|
163
|
+
* @example Single WeakMap key
|
|
237
164
|
* ```ts
|
|
238
|
-
*
|
|
165
|
+
* const cache = new WeakMap<SchemaNode, Set<string>>()
|
|
166
|
+
* const getRefs = memoize(cache, (node) => collectRefs(node))
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @example Single Map key (primitive)
|
|
170
|
+
* ```ts
|
|
171
|
+
* const cache = new Map<string, Resolver>()
|
|
172
|
+
* const getResolver = memoize(cache, (name) => buildResolver(name))
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* @example Two-level (object + primitive)
|
|
176
|
+
* ```ts
|
|
177
|
+
* const outer = new WeakMap<Params[], Map<string, Params[]>>()
|
|
178
|
+
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
179
|
+
* fn(params)('camelcase')
|
|
239
180
|
* ```
|
|
240
181
|
*/
|
|
241
|
-
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
182
|
+
function memoize(store, factory) {
|
|
183
|
+
return (key) => {
|
|
184
|
+
if (store.has(key)) return store.get(key);
|
|
185
|
+
const value = factory(key);
|
|
186
|
+
store.set(key, value);
|
|
187
|
+
return value;
|
|
188
|
+
};
|
|
246
189
|
}
|
|
247
190
|
//#endregion
|
|
248
|
-
//#region
|
|
191
|
+
//#region package.json
|
|
192
|
+
var version = "5.0.0-beta.100";
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/constants.ts
|
|
249
195
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
196
|
+
* Plugin `include` filter types that select operations directly. When one of these is set
|
|
197
|
+
* without a `schemaName` include, the generate phase pre-scans operations to compute the set
|
|
198
|
+
* of schemas they reach, so unreachable schemas can be pruned for that plugin.
|
|
252
199
|
*/
|
|
253
|
-
const
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"case",
|
|
260
|
-
"catch",
|
|
261
|
-
"char",
|
|
262
|
-
"class",
|
|
263
|
-
"const",
|
|
264
|
-
"continue",
|
|
265
|
-
"debugger",
|
|
266
|
-
"default",
|
|
267
|
-
"delete",
|
|
268
|
-
"do",
|
|
269
|
-
"double",
|
|
270
|
-
"else",
|
|
271
|
-
"enum",
|
|
272
|
-
"eval",
|
|
273
|
-
"export",
|
|
274
|
-
"extends",
|
|
275
|
-
"false",
|
|
276
|
-
"final",
|
|
277
|
-
"finally",
|
|
278
|
-
"float",
|
|
279
|
-
"for",
|
|
280
|
-
"function",
|
|
281
|
-
"goto",
|
|
282
|
-
"if",
|
|
283
|
-
"implements",
|
|
284
|
-
"import",
|
|
285
|
-
"in",
|
|
286
|
-
"instanceof",
|
|
287
|
-
"int",
|
|
288
|
-
"interface",
|
|
289
|
-
"let",
|
|
290
|
-
"long",
|
|
291
|
-
"native",
|
|
292
|
-
"new",
|
|
293
|
-
"null",
|
|
294
|
-
"package",
|
|
295
|
-
"private",
|
|
296
|
-
"protected",
|
|
297
|
-
"public",
|
|
298
|
-
"return",
|
|
299
|
-
"short",
|
|
300
|
-
"static",
|
|
301
|
-
"super",
|
|
302
|
-
"switch",
|
|
303
|
-
"synchronized",
|
|
304
|
-
"this",
|
|
305
|
-
"throw",
|
|
306
|
-
"throws",
|
|
307
|
-
"transient",
|
|
308
|
-
"true",
|
|
309
|
-
"try",
|
|
310
|
-
"typeof",
|
|
311
|
-
"var",
|
|
312
|
-
"void",
|
|
313
|
-
"volatile",
|
|
314
|
-
"while",
|
|
315
|
-
"with",
|
|
316
|
-
"yield",
|
|
317
|
-
"Array",
|
|
318
|
-
"Date",
|
|
319
|
-
"hasOwnProperty",
|
|
320
|
-
"Infinity",
|
|
321
|
-
"isFinite",
|
|
322
|
-
"isNaN",
|
|
323
|
-
"isPrototypeOf",
|
|
324
|
-
"length",
|
|
325
|
-
"Math",
|
|
326
|
-
"name",
|
|
327
|
-
"NaN",
|
|
328
|
-
"Number",
|
|
329
|
-
"Object",
|
|
330
|
-
"prototype",
|
|
331
|
-
"String",
|
|
332
|
-
"toString",
|
|
333
|
-
"undefined",
|
|
334
|
-
"valueOf"
|
|
200
|
+
const OPERATION_FILTER_TYPES = /* @__PURE__ */ new Set([
|
|
201
|
+
"tag",
|
|
202
|
+
"operationId",
|
|
203
|
+
"path",
|
|
204
|
+
"method",
|
|
205
|
+
"contentType"
|
|
335
206
|
]);
|
|
336
207
|
/**
|
|
337
|
-
*
|
|
208
|
+
* Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode
|
|
209
|
+
* and stays stable so it can be referenced in tooling and (later) docs. Reference
|
|
210
|
+
* these instead of inlining the string at a throw site.
|
|
211
|
+
*/
|
|
212
|
+
const diagnosticCode = {
|
|
213
|
+
/**
|
|
214
|
+
* Fallback for an unstructured error with no specific code.
|
|
215
|
+
*/
|
|
216
|
+
unknown: "KUBB_UNKNOWN",
|
|
217
|
+
/**
|
|
218
|
+
* The file or URL set as `input` could not be read.
|
|
219
|
+
*/
|
|
220
|
+
inputNotFound: "KUBB_INPUT_NOT_FOUND",
|
|
221
|
+
/**
|
|
222
|
+
* An adapter was configured without an `input`.
|
|
223
|
+
*/
|
|
224
|
+
inputRequired: "KUBB_INPUT_REQUIRED",
|
|
225
|
+
/**
|
|
226
|
+
* A `$ref` (or equivalent reference) could not be resolved in the source document.
|
|
227
|
+
*/
|
|
228
|
+
refNotFound: "KUBB_REF_NOT_FOUND",
|
|
229
|
+
/**
|
|
230
|
+
* A server variable value is not allowed by its `enum`.
|
|
231
|
+
*/
|
|
232
|
+
invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE",
|
|
233
|
+
/**
|
|
234
|
+
* A required plugin is missing from the config.
|
|
235
|
+
*/
|
|
236
|
+
pluginNotFound: "KUBB_PLUGIN_NOT_FOUND",
|
|
237
|
+
/**
|
|
238
|
+
* A plugin threw while generating.
|
|
239
|
+
*/
|
|
240
|
+
pluginFailed: "KUBB_PLUGIN_FAILED",
|
|
241
|
+
/**
|
|
242
|
+
* A plugin reported a non-fatal warning through `ctx.warn`.
|
|
243
|
+
*/
|
|
244
|
+
pluginWarning: "KUBB_PLUGIN_WARNING",
|
|
245
|
+
/**
|
|
246
|
+
* A plugin reported an informational message through `ctx.info`.
|
|
247
|
+
*/
|
|
248
|
+
pluginInfo: "KUBB_PLUGIN_INFO",
|
|
249
|
+
/**
|
|
250
|
+
* A schema uses a `format` Kubb does not map to a specific type. Reserved for
|
|
251
|
+
* adapters to emit as a `warning`.
|
|
252
|
+
*/
|
|
253
|
+
unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT",
|
|
254
|
+
/**
|
|
255
|
+
* A referenced schema or operation is marked `deprecated`. Reserved for adapters
|
|
256
|
+
* to emit as an `info`.
|
|
257
|
+
*/
|
|
258
|
+
deprecated: "KUBB_DEPRECATED",
|
|
259
|
+
/**
|
|
260
|
+
* An adapter is required but the config has none. The build cannot read the input
|
|
261
|
+
* without one.
|
|
262
|
+
*/
|
|
263
|
+
adapterRequired: "KUBB_ADAPTER_REQUIRED",
|
|
264
|
+
/**
|
|
265
|
+
* A resolved output path escapes the output directory, which can stem from a path
|
|
266
|
+
* traversal in the spec or a misconfigured `group.name`.
|
|
267
|
+
*/
|
|
268
|
+
pathTraversal: "KUBB_PATH_TRAVERSAL",
|
|
269
|
+
/**
|
|
270
|
+
* `output.clean` is enabled but `output.path` resolves to the project root or a parent of it,
|
|
271
|
+
* so cleaning would delete kubb.config and every source file.
|
|
272
|
+
*/
|
|
273
|
+
cleanRoot: "KUBB_CLEAN_ROOT",
|
|
274
|
+
/**
|
|
275
|
+
* A plugin's options are invalid, for example `output.mode: 'file'` paired with a `group` option.
|
|
276
|
+
*/
|
|
277
|
+
invalidPluginOptions: "KUBB_INVALID_PLUGIN_OPTIONS",
|
|
278
|
+
/**
|
|
279
|
+
* A post-generate command (`output.postGenerate`) exited with a failure.
|
|
280
|
+
*/
|
|
281
|
+
postGenerateFailed: "KUBB_POST_GENERATE_FAILED",
|
|
282
|
+
/**
|
|
283
|
+
* The formatter pass over the generated files failed.
|
|
284
|
+
*/
|
|
285
|
+
formatFailed: "KUBB_FORMAT_FAILED",
|
|
286
|
+
/**
|
|
287
|
+
* The linter pass over the generated files failed.
|
|
288
|
+
*/
|
|
289
|
+
lintFailed: "KUBB_LINT_FAILED",
|
|
290
|
+
/**
|
|
291
|
+
* Not a failure. Carries a plugin's elapsed time, summed into the run total.
|
|
292
|
+
*/
|
|
293
|
+
performance: "KUBB_PERFORMANCE",
|
|
294
|
+
/**
|
|
295
|
+
* Not a failure. A newer Kubb version is available on npm.
|
|
296
|
+
*/
|
|
297
|
+
updateAvailable: "KUBB_UPDATE_AVAILABLE"
|
|
298
|
+
};
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/Diagnostics.ts
|
|
301
|
+
/**
|
|
302
|
+
* Docs major version, derived from the package version so the link tracks the published major.
|
|
303
|
+
*/
|
|
304
|
+
const docsMajor = version.split(".")[0] ?? "5";
|
|
305
|
+
/**
|
|
306
|
+
* Builds a type guard that narrows a {@link Diagnostic} to the variant for `kind`. A diagnostic
|
|
307
|
+
* with no `kind` is treated as a `problem`.
|
|
308
|
+
*/
|
|
309
|
+
function isKind(kind) {
|
|
310
|
+
return (diagnostic) => (diagnostic.kind ?? "problem") === kind;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Returns `true` when the diagnostic is a build {@link ProblemDiagnostic}.
|
|
338
314
|
*
|
|
339
315
|
* @example
|
|
340
316
|
* ```ts
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
317
|
+
* if (isProblem(diagnostic)) {
|
|
318
|
+
* console.log(diagnostic.location)
|
|
319
|
+
* }
|
|
344
320
|
* ```
|
|
345
321
|
*/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
322
|
+
const isProblem = isKind("problem");
|
|
323
|
+
/**
|
|
324
|
+
* Returns `true` when the diagnostic is a per-plugin {@link PerformanceDiagnostic}.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* const timings = diagnostics.filter(isPerformance)
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
const isPerformance = isKind("performance");
|
|
352
332
|
/**
|
|
353
|
-
*
|
|
333
|
+
* Returns `true` when the diagnostic is a version-update {@link UpdateDiagnostic}.
|
|
354
334
|
*
|
|
355
335
|
* @example
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
336
|
+
* ```ts
|
|
337
|
+
* if (isUpdate(diagnostic)) {
|
|
338
|
+
* console.log(diagnostic.latestVersion)
|
|
339
|
+
* }
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
const isUpdate = isKind("update");
|
|
343
|
+
/**
|
|
344
|
+
* Accent color per severity. The color tints the `[CODE]` tag (red error, yellow warning,
|
|
345
|
+
* blue info).
|
|
346
|
+
*/
|
|
347
|
+
const severityStyle = {
|
|
348
|
+
error: "red",
|
|
349
|
+
warning: "yellow",
|
|
350
|
+
info: "blue"
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* Explanation for every {@link diagnosticCode}. Use {@link Diagnostics.explain} to look one up
|
|
354
|
+
* and `Diagnostics.docsUrl` for the matching kubb.dev page.
|
|
355
|
+
*/
|
|
356
|
+
const diagnosticCatalog = {
|
|
357
|
+
[diagnosticCode.unknown]: {
|
|
358
|
+
title: "Unknown error",
|
|
359
|
+
cause: "An error was thrown without a stable Kubb code, so it is reported as-is.",
|
|
360
|
+
fix: "Read the underlying message and stack. If it comes from a plugin or adapter, check its configuration; otherwise report it as a possible Kubb bug."
|
|
361
|
+
},
|
|
362
|
+
[diagnosticCode.inputNotFound]: {
|
|
363
|
+
title: "Input not found",
|
|
364
|
+
cause: "The file or URL set as `input` (or passed as `kubb generate PATH`) could not be read.",
|
|
365
|
+
fix: "Check that the path or URL exists and is readable, then set it as `input` or pass it on the CLI."
|
|
366
|
+
},
|
|
367
|
+
[diagnosticCode.inputRequired]: {
|
|
368
|
+
title: "Input required",
|
|
369
|
+
cause: "An adapter is configured but no `input` was provided.",
|
|
370
|
+
fix: "Set `input` to a file path, a URL, an inline spec (JSON/YAML string), or a parsed object in your Kubb config."
|
|
371
|
+
},
|
|
372
|
+
[diagnosticCode.refNotFound]: {
|
|
373
|
+
title: "Reference not found",
|
|
374
|
+
cause: "A `$ref` could not be resolved in the source document.",
|
|
375
|
+
fix: "Add the missing definition (for example under `components.schemas`) or fix the `$ref`. Run `kubb validate` to check the spec."
|
|
376
|
+
},
|
|
377
|
+
[diagnosticCode.invalidServerVariable]: {
|
|
378
|
+
title: "Invalid server variable",
|
|
379
|
+
cause: "A server variable value is not allowed by its `enum`.",
|
|
380
|
+
fix: "Use one of the values listed in the server variable `enum`, or update the spec."
|
|
381
|
+
},
|
|
382
|
+
[diagnosticCode.pluginNotFound]: {
|
|
383
|
+
title: "Plugin not found",
|
|
384
|
+
cause: "A plugin that another plugin depends on is missing from the config.",
|
|
385
|
+
fix: "Add the required plugin to the `plugins` array in kubb.config.ts, or remove the dependency on it."
|
|
386
|
+
},
|
|
387
|
+
[diagnosticCode.pluginFailed]: {
|
|
388
|
+
title: "Plugin failed",
|
|
389
|
+
cause: "A plugin threw while generating, or reported an error through `ctx.error`.",
|
|
390
|
+
fix: "Read the underlying error and check the plugin options and the schema or operation it failed on."
|
|
391
|
+
},
|
|
392
|
+
[diagnosticCode.pluginWarning]: {
|
|
393
|
+
title: "Plugin warning",
|
|
394
|
+
cause: "A plugin reported a non-fatal warning through `ctx.warn`.",
|
|
395
|
+
fix: "Review the message. It does not fail the build; adjust the plugin options or input if the warning is unwanted."
|
|
396
|
+
},
|
|
397
|
+
[diagnosticCode.pluginInfo]: {
|
|
398
|
+
title: "Plugin info",
|
|
399
|
+
cause: "A plugin reported an informational message through `ctx.info`.",
|
|
400
|
+
fix: "Informational only. No action is required."
|
|
401
|
+
},
|
|
402
|
+
[diagnosticCode.unsupportedFormat]: {
|
|
403
|
+
title: "Unsupported format",
|
|
404
|
+
cause: "A schema uses a `format` Kubb does not map to a specific type, so it falls back to the base type.",
|
|
405
|
+
fix: "Use a format Kubb supports, or handle the custom format with a parser or plugin."
|
|
406
|
+
},
|
|
407
|
+
[diagnosticCode.deprecated]: {
|
|
408
|
+
title: "Deprecated",
|
|
409
|
+
cause: "A referenced schema or operation is marked `deprecated`.",
|
|
410
|
+
fix: "Migrate off the deprecated definition if the warning is unwanted."
|
|
411
|
+
},
|
|
412
|
+
[diagnosticCode.adapterRequired]: {
|
|
413
|
+
title: "Adapter required",
|
|
414
|
+
cause: "An action needs an adapter but none is configured.",
|
|
415
|
+
fix: "Set `adapter` in kubb.config.ts, for example `adapterOas()`."
|
|
416
|
+
},
|
|
417
|
+
[diagnosticCode.pathTraversal]: {
|
|
418
|
+
title: "Path traversal",
|
|
419
|
+
cause: "A resolved output path escaped the output directory, which can stem from a path traversal in the spec or a misconfigured `group.name`.",
|
|
420
|
+
fix: "Keep generated paths within the output directory. Review the `group.name` function and the names coming from the spec."
|
|
421
|
+
},
|
|
422
|
+
[diagnosticCode.cleanRoot]: {
|
|
423
|
+
title: "Clean targets the project root",
|
|
424
|
+
cause: "`output.clean` is enabled and `output.path` resolves to the project root or a parent of it, so cleaning would delete `kubb.config` and every source file.",
|
|
425
|
+
fix: "Point `output.path` at a subdirectory such as `./src/gen` so clean only removes generated code, or disable `output.clean`."
|
|
426
|
+
},
|
|
427
|
+
[diagnosticCode.invalidPluginOptions]: {
|
|
428
|
+
title: "Invalid plugin options",
|
|
429
|
+
cause: "A plugin was configured with options that cannot be honored, for example `output.mode: 'file'` paired with a `group` option.",
|
|
430
|
+
fix: "Fix the plugin options. A single-file output has nothing to group, so remove the `group` option or use `output.mode: 'directory'`."
|
|
431
|
+
},
|
|
432
|
+
[diagnosticCode.postGenerateFailed]: {
|
|
433
|
+
title: "Post-generate command failed",
|
|
434
|
+
cause: "A post-generate command (`output.postGenerate`) exited with a non-zero status.",
|
|
435
|
+
fix: "Check the command is installed and correct, and run it manually to see the error."
|
|
436
|
+
},
|
|
437
|
+
[diagnosticCode.formatFailed]: {
|
|
438
|
+
title: "Format failed",
|
|
439
|
+
cause: "The formatter pass over the generated files failed.",
|
|
440
|
+
fix: "Check the formatter (oxfmt, biome, or prettier) is installed and its config is valid, then run it manually on the output."
|
|
441
|
+
},
|
|
442
|
+
[diagnosticCode.lintFailed]: {
|
|
443
|
+
title: "Lint failed",
|
|
444
|
+
cause: "The linter pass over the generated files failed.",
|
|
445
|
+
fix: "Check the linter (oxlint, biome, or eslint) is installed and its config is valid, then run it manually on the output."
|
|
446
|
+
},
|
|
447
|
+
[diagnosticCode.performance]: {
|
|
448
|
+
title: "Performance",
|
|
449
|
+
cause: "Not a failure. Records a plugin’s elapsed time, summed into the run total.",
|
|
450
|
+
fix: "No action. This is an informational metric."
|
|
451
|
+
},
|
|
452
|
+
[diagnosticCode.updateAvailable]: {
|
|
453
|
+
title: "Update available",
|
|
454
|
+
cause: "A newer Kubb version is published on npm than the one running.",
|
|
455
|
+
fix: "Update the `@kubb/*` packages, for example `npm install -g @kubb/cli`, to get the latest fixes."
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
/**
|
|
459
|
+
* Static helpers for working with {@link Diagnostic}s, plus the run-scoped sink
|
|
460
|
+
* that lets deep code report a diagnostic without threading a callback.
|
|
461
|
+
*
|
|
462
|
+
* The sink lives in a single `AsyncLocalStorage` in the `@kubb/core` bundle.
|
|
463
|
+
* `Diagnostics.scope` activates it for a run, so anything inside that run (the
|
|
464
|
+
* adapter parse, a generator) reports through `Diagnostics.report` and lands
|
|
465
|
+
* in the same run.
|
|
359
466
|
*/
|
|
360
|
-
var
|
|
467
|
+
var Diagnostics = class Diagnostics {
|
|
468
|
+
static #reporterStorage = new AsyncLocalStorage();
|
|
361
469
|
/**
|
|
362
|
-
* The
|
|
470
|
+
* The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
|
|
363
471
|
*/
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
this.path = path;
|
|
368
|
-
this.#options = options;
|
|
369
|
-
}
|
|
370
|
-
/** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
|
|
371
|
-
*
|
|
372
|
-
* @example
|
|
373
|
-
* ```ts
|
|
374
|
-
* new URLPath('/pet/{petId}').URL // '/pet/:petId'
|
|
375
|
-
* ```
|
|
472
|
+
static code = diagnosticCode;
|
|
473
|
+
/**
|
|
474
|
+
* Type guard for a build {@link ProblemDiagnostic}.
|
|
376
475
|
*/
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
476
|
+
static isProblem = isProblem;
|
|
477
|
+
/**
|
|
478
|
+
* Type guard for a version-update {@link UpdateDiagnostic}.
|
|
479
|
+
*/
|
|
480
|
+
static isUpdate = isUpdate;
|
|
481
|
+
/**
|
|
482
|
+
* Type guard for a per-plugin {@link PerformanceDiagnostic}.
|
|
483
|
+
*/
|
|
484
|
+
static isPerformance = isPerformance;
|
|
485
|
+
/**
|
|
486
|
+
* An `Error` that carries a {@link Diagnostic}, so structured problems can flow
|
|
487
|
+
* through the existing throw/catch paths while keeping their code and location.
|
|
381
488
|
*
|
|
382
489
|
* @example
|
|
383
490
|
* ```ts
|
|
384
|
-
* new
|
|
385
|
-
* new URLPath('/pet/{petId}').isURL // false
|
|
491
|
+
* throw new Diagnostics.Error({ code: diagnosticCode.refNotFound, severity: 'error', message: `Could not find ${ref}`, location: { kind: 'schema', pointer: ref, ref } })
|
|
386
492
|
* ```
|
|
387
493
|
*/
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
494
|
+
static Error = class DiagnosticError extends Error {
|
|
495
|
+
diagnostic;
|
|
496
|
+
constructor(diagnostic) {
|
|
497
|
+
super(diagnostic.message, { cause: diagnostic.cause });
|
|
498
|
+
this.name = "DiagnosticError";
|
|
499
|
+
this.diagnostic = diagnostic;
|
|
393
500
|
}
|
|
394
|
-
}
|
|
501
|
+
};
|
|
395
502
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
* new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
|
|
400
|
-
* new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
|
|
503
|
+
* Structural check for a {@link Diagnostics.Error}, including one thrown from a duplicated
|
|
504
|
+
* `@kubb/core` copy where `instanceof` fails. Matches on the `name` and a `diagnostic`
|
|
505
|
+
* that carries a `code`.
|
|
401
506
|
*/
|
|
402
|
-
|
|
403
|
-
|
|
507
|
+
static isError(error) {
|
|
508
|
+
if (error instanceof Diagnostics.Error) return true;
|
|
509
|
+
return error instanceof Error && error.name === "DiagnosticError" && "diagnostic" in error && typeof error.diagnostic === "object" && error.diagnostic !== null && typeof error.diagnostic?.code === "string";
|
|
404
510
|
}
|
|
405
|
-
/**
|
|
406
|
-
*
|
|
407
|
-
* @
|
|
408
|
-
* ```ts
|
|
409
|
-
* new URLPath('/pet/{petId}').object
|
|
410
|
-
* // { url: '/pet/:petId', params: { petId: 'petId' } }
|
|
411
|
-
* ```
|
|
511
|
+
/**
|
|
512
|
+
* Runs `fn` with `sink` as the active diagnostic sink for the whole async
|
|
513
|
+
* subtree, so {@link Diagnostics.report} reaches it from anywhere inside.
|
|
412
514
|
*/
|
|
413
|
-
|
|
414
|
-
return
|
|
515
|
+
static scope(sink, fn) {
|
|
516
|
+
return Diagnostics.#reporterStorage.run(sink, fn);
|
|
415
517
|
}
|
|
416
|
-
/**
|
|
417
|
-
*
|
|
418
|
-
* @
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
* new URLPath('/pet').params // undefined
|
|
422
|
-
* ```
|
|
518
|
+
/**
|
|
519
|
+
* Collects a diagnostic into the active build via the run-scoped sink, without throwing.
|
|
520
|
+
* Returns `true` when a run consumed it, `false` when called outside a {@link Diagnostics.scope}
|
|
521
|
+
* (so callers can fall back to throwing). Use a `warning`/`info` severity for non-fatal issues.
|
|
522
|
+
* For rendering a diagnostic live on the hook bus, use {@link Diagnostics.emit} instead.
|
|
423
523
|
*/
|
|
424
|
-
|
|
425
|
-
|
|
524
|
+
static report(diagnostic) {
|
|
525
|
+
const sink = Diagnostics.#reporterStorage.getStore();
|
|
526
|
+
if (!sink) return false;
|
|
527
|
+
sink(diagnostic);
|
|
528
|
+
return true;
|
|
426
529
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
530
|
+
/**
|
|
531
|
+
* Emits a diagnostic on the run's `kubb:diagnostic` hook so the loggers render it live.
|
|
532
|
+
* Use it instead of calling `hooks.callHook('kubb:diagnostic', ...)` directly. To collect a
|
|
533
|
+
* diagnostic into the build result from deep in a run, use {@link Diagnostics.report} instead.
|
|
534
|
+
*/
|
|
535
|
+
static async emit(hooks, diagnostic) {
|
|
536
|
+
await hooks.callHook("kubb:diagnostic", { diagnostic });
|
|
430
537
|
}
|
|
431
538
|
/**
|
|
432
|
-
*
|
|
539
|
+
* Coerces any thrown value into a {@link ProblemDiagnostic}. A {@link Diagnostics.Error}
|
|
540
|
+
* keeps its structured data, and anything else becomes a `KUBB_UNKNOWN` error.
|
|
433
541
|
*/
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
542
|
+
static from(error) {
|
|
543
|
+
const seen = /* @__PURE__ */ new Set();
|
|
544
|
+
let current = error;
|
|
545
|
+
let root;
|
|
546
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
547
|
+
if (Diagnostics.isError(current)) return current.diagnostic;
|
|
548
|
+
seen.add(current);
|
|
549
|
+
root = current;
|
|
550
|
+
current = current.cause;
|
|
438
551
|
}
|
|
552
|
+
return {
|
|
553
|
+
code: diagnosticCode.unknown,
|
|
554
|
+
severity: "error",
|
|
555
|
+
message: root ? root.message : getErrorMessage(error),
|
|
556
|
+
cause: root
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Builds a per-plugin performance record. Reporters sum these into the run total.
|
|
561
|
+
*/
|
|
562
|
+
static performance({ plugin, duration }) {
|
|
563
|
+
return {
|
|
564
|
+
kind: "performance",
|
|
565
|
+
code: diagnosticCode.performance,
|
|
566
|
+
severity: "info",
|
|
567
|
+
message: `${plugin} generated in ${Math.round(duration)}ms`,
|
|
568
|
+
plugin,
|
|
569
|
+
duration
|
|
570
|
+
};
|
|
439
571
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
572
|
+
/**
|
|
573
|
+
* Builds the version-update notice shown when a newer Kubb is published on npm.
|
|
574
|
+
*/
|
|
575
|
+
static update({ currentVersion, latestVersion }) {
|
|
576
|
+
return {
|
|
577
|
+
kind: "update",
|
|
578
|
+
code: diagnosticCode.updateAvailable,
|
|
579
|
+
severity: "info",
|
|
580
|
+
message: `Update available: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`,
|
|
581
|
+
currentVersion,
|
|
582
|
+
latestVersion
|
|
444
583
|
};
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* True when any diagnostic is an error, the severity that fails a build. Non-error
|
|
587
|
+
* diagnostics are ignored.
|
|
588
|
+
*/
|
|
589
|
+
static hasError(diagnostics) {
|
|
590
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Names of the plugins that failed, deduped, derived from the error diagnostics
|
|
594
|
+
* that carry a `plugin`.
|
|
595
|
+
*/
|
|
596
|
+
static failedPlugins(diagnostics) {
|
|
597
|
+
const names = /* @__PURE__ */ new Set();
|
|
598
|
+
for (const diagnostic of diagnostics) if (diagnostic.severity === "error" && diagnostic.plugin) names.add(diagnostic.plugin);
|
|
599
|
+
return [...names];
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Counts `problem` diagnostics by severity for the run summary. `performance` and
|
|
603
|
+
* `update` diagnostics are ignored.
|
|
604
|
+
*/
|
|
605
|
+
static count(diagnostics) {
|
|
606
|
+
let errors = 0;
|
|
607
|
+
let warnings = 0;
|
|
608
|
+
let infos = 0;
|
|
609
|
+
for (const diagnostic of diagnostics) {
|
|
610
|
+
if (!isProblem(diagnostic)) continue;
|
|
611
|
+
if (diagnostic.severity === "error") errors += 1;
|
|
612
|
+
else if (diagnostic.severity === "warning") warnings += 1;
|
|
613
|
+
else infos += 1;
|
|
449
614
|
}
|
|
450
|
-
return
|
|
615
|
+
return {
|
|
616
|
+
errors,
|
|
617
|
+
warnings,
|
|
618
|
+
infos
|
|
619
|
+
};
|
|
451
620
|
}
|
|
452
621
|
/**
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
* @example
|
|
457
|
-
* new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
|
|
622
|
+
* Drops duplicate `problem` diagnostics that share a code, location pointer, and
|
|
623
|
+
* plugin, so the same issue reported across several passes is shown once. Non-problem
|
|
624
|
+
* diagnostics are always kept.
|
|
458
625
|
*/
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
626
|
+
static dedupe(diagnostics) {
|
|
627
|
+
const seen = /* @__PURE__ */ new Set();
|
|
628
|
+
const result = [];
|
|
629
|
+
for (const diagnostic of diagnostics) {
|
|
630
|
+
if (!isProblem(diagnostic)) {
|
|
631
|
+
result.push(diagnostic);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const pointer = diagnostic.location && "pointer" in diagnostic.location ? diagnostic.location.pointer : "";
|
|
635
|
+
const key = `${diagnostic.code} ${pointer} ${diagnostic.plugin ?? ""}`;
|
|
636
|
+
if (seen.has(key)) continue;
|
|
637
|
+
seen.add(key);
|
|
638
|
+
result.push(diagnostic);
|
|
639
|
+
}
|
|
640
|
+
return result;
|
|
465
641
|
}
|
|
466
642
|
/**
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
* Returns `undefined` when no path parameters are found.
|
|
470
|
-
*
|
|
471
|
-
* @example
|
|
472
|
-
* ```ts
|
|
473
|
-
* new URLPath('/pet/{petId}/tag/{tagId}').getParams()
|
|
474
|
-
* // { petId: 'petId', tagId: 'tagId' }
|
|
475
|
-
* ```
|
|
643
|
+
* Builds the kubb.dev docs URL for a diagnostic code, e.g.
|
|
644
|
+
* `KUBB_REF_NOT_FOUND` → `https://kubb.dev/docs/5.x/reference/diagnostics/kubb-ref-not-found`.
|
|
476
645
|
*/
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
const key = replacer ? replacer(param) : param;
|
|
481
|
-
params[key] = key;
|
|
482
|
-
});
|
|
483
|
-
return Object.keys(params).length > 0 ? params : void 0;
|
|
646
|
+
static docsUrl(code) {
|
|
647
|
+
const slug = code.toLowerCase().replaceAll("_", "-");
|
|
648
|
+
return `https://kubb.dev/docs/${docsMajor}.x/reference/diagnostics/${slug}`;
|
|
484
649
|
}
|
|
485
|
-
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
* ```ts
|
|
489
|
-
* new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
|
|
490
|
-
* ```
|
|
650
|
+
/**
|
|
651
|
+
* The catalog entry for a code: its title, cause, and fix. Mirrors the kubb.dev
|
|
652
|
+
* `/diagnostics/<slug>` page.
|
|
491
653
|
*/
|
|
492
|
-
|
|
493
|
-
return
|
|
654
|
+
static explain(code) {
|
|
655
|
+
return diagnosticCatalog[code];
|
|
494
656
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
657
|
+
/**
|
|
658
|
+
* Reduces a diagnostic to its JSON-safe fields plus a `docsUrl`, for machine-readable
|
|
659
|
+
* consumers. The `cause`, `kind`, and `duration` are dropped, and absent optional
|
|
660
|
+
* fields are omitted rather than set to `undefined`.
|
|
661
|
+
*/
|
|
662
|
+
static serialize(diagnostic) {
|
|
663
|
+
const problem = isProblem(diagnostic) ? diagnostic : void 0;
|
|
664
|
+
return {
|
|
665
|
+
code: diagnostic.code,
|
|
666
|
+
severity: diagnostic.severity,
|
|
667
|
+
message: diagnostic.message,
|
|
668
|
+
...problem?.location ? { location: problem.location } : {},
|
|
669
|
+
...problem?.help ? { help: problem.help } : {},
|
|
670
|
+
...problem?.plugin ? { plugin: problem.plugin } : {},
|
|
671
|
+
...diagnostic.code === diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Renders a {@link Diagnostic} for terminal output as its parts: the `headline`
|
|
676
|
+
* (`[CODE] plugin: message`, with the code in the severity color) and the indented `details`
|
|
677
|
+
* rows (`at:` pointer, `fix:` help, `see:` docs link).
|
|
678
|
+
*
|
|
679
|
+
* Hosts compose these to fit their gutter: a clack logger passes `[headline, ...details]` as the
|
|
680
|
+
* message with no gutter symbol, while plain text outputs use {@link Diagnostics.formatLines}.
|
|
681
|
+
*/
|
|
682
|
+
static format(diagnostic) {
|
|
683
|
+
const { code, severity, message } = diagnostic;
|
|
684
|
+
const color = severityStyle[severity];
|
|
685
|
+
const problem = isProblem(diagnostic) ? diagnostic : void 0;
|
|
686
|
+
const tag = styleText(color, styleText("bold", `[${code}]`));
|
|
687
|
+
const headline = problem?.plugin ? `${tag} ${problem.plugin}: ${message}` : `${tag}: ${message}`;
|
|
688
|
+
const details = [];
|
|
689
|
+
if (problem?.location && "pointer" in problem.location) details.push(` ${styleText("dim", "at:")} ${styleText("cyan", problem.location.pointer)}`);
|
|
690
|
+
if (problem?.help) details.push(` ${styleText("cyan", "fix:")} ${problem.help}`);
|
|
691
|
+
if (code !== diagnosticCode.unknown) details.push(` ${styleText("dim", "see:")} ${styleText("cyan", Diagnostics.docsUrl(code))}`);
|
|
692
|
+
return {
|
|
693
|
+
headline,
|
|
694
|
+
details
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* The self-contained block form of {@link Diagnostics.format}: the `headline` followed by the
|
|
699
|
+
* indented detail rows. Used where there is no gutter (plain and file output).
|
|
700
|
+
*/
|
|
701
|
+
static formatLines(diagnostic) {
|
|
702
|
+
const { headline, details } = Diagnostics.format(diagnostic);
|
|
703
|
+
return [headline, ...details];
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/definePlugin.ts
|
|
498
708
|
/**
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
|
|
709
|
+
* Merges the `output.mode` default into the output config and validates the combination.
|
|
710
|
+
* Throws `KUBB_INVALID_PLUGIN_OPTIONS` when `mode: 'file'` is paired with a `group` option,
|
|
711
|
+
* since a single-file output has nothing to group.
|
|
712
|
+
*/
|
|
713
|
+
function normalizeOutput({ output, group, pluginName }) {
|
|
714
|
+
const mode = output.mode ?? "file";
|
|
715
|
+
if (mode === "file" && group) throw new Diagnostics.Error({
|
|
716
|
+
code: diagnosticCode.invalidPluginOptions,
|
|
717
|
+
severity: "error",
|
|
718
|
+
message: `Plugin "${pluginName}" sets \`output.mode: 'file'\` but also configures a \`group\` option.`,
|
|
719
|
+
help: "A single-file output has nothing to group. Remove the `group` option, or use `output.mode: 'directory'` to organize files into subdirectories.",
|
|
720
|
+
location: { kind: "config" },
|
|
721
|
+
plugin: pluginName
|
|
722
|
+
});
|
|
723
|
+
return {
|
|
724
|
+
...output,
|
|
725
|
+
mode
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Wraps a plugin factory and returns a function that accepts user options and
|
|
730
|
+
* yields a typed `Plugin`. Lifecycle handlers go inside a single `hooks` object.
|
|
503
731
|
*
|
|
504
|
-
*
|
|
732
|
+
* Pass a `PluginFactoryOptions` type parameter to get a typed `ctx` inside
|
|
733
|
+
* `kubb:plugin:setup`. Plugin names should follow the `plugin-<feature>`
|
|
734
|
+
* convention (`plugin-react-query`, `plugin-zod`, ...).
|
|
505
735
|
*
|
|
506
736
|
* @example
|
|
507
737
|
* ```ts
|
|
508
|
-
*
|
|
509
|
-
* return {
|
|
510
|
-
* name: 'my-adapter',
|
|
511
|
-
* options,
|
|
512
|
-
* async parse(source) {
|
|
513
|
-
* // Transform source format to InputNode
|
|
514
|
-
* return { ... }
|
|
515
|
-
* },
|
|
516
|
-
* }
|
|
517
|
-
* })
|
|
738
|
+
* import { definePlugin } from '@kubb/core'
|
|
518
739
|
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
740
|
+
* export const pluginTs = definePlugin((options: { prefix?: string } = {}) => ({
|
|
741
|
+
* name: 'plugin-ts',
|
|
742
|
+
* hooks: {
|
|
743
|
+
* 'kubb:plugin:setup'(ctx) {
|
|
744
|
+
* ctx.setResolver(resolverTs)
|
|
745
|
+
* },
|
|
746
|
+
* },
|
|
747
|
+
* }))
|
|
521
748
|
* ```
|
|
522
749
|
*/
|
|
523
|
-
function
|
|
524
|
-
return (options) =>
|
|
750
|
+
function definePlugin(factory) {
|
|
751
|
+
return (options) => factory(options ?? {});
|
|
525
752
|
}
|
|
526
753
|
//#endregion
|
|
527
|
-
//#region
|
|
528
|
-
|
|
754
|
+
//#region src/input.ts
|
|
755
|
+
/**
|
|
756
|
+
* Classifies an `input` value so callers branch on it once instead of repeating the checks.
|
|
757
|
+
*
|
|
758
|
+
* A non-string is a parsed spec (`object`). A string is `inline` when it holds OpenAPI content,
|
|
759
|
+
* meaning it starts with `{` or `[`, spans multiple lines, or opens with a YAML `openapi:` or
|
|
760
|
+
* `swagger:` key. Otherwise a string is a `url` when it parses as one, or a `file` path.
|
|
761
|
+
*/
|
|
762
|
+
function getInputKind(input) {
|
|
763
|
+
if (typeof input !== "string") return "object";
|
|
764
|
+
const trimmed = input.trimStart();
|
|
765
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[") || input.includes("\n") || /^(openapi|swagger)\s*:/i.test(trimmed)) return "inline";
|
|
766
|
+
if (URL.canParse(input)) return "url";
|
|
767
|
+
return "file";
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Normalizes `config.input` into an `AdapterSource` the adapter can parse.
|
|
771
|
+
*
|
|
772
|
+
* A parsed object and inline content become `{ type: 'data' }`; a URL is kept verbatim and a
|
|
773
|
+
* local path is resolved against `config.root`, both as `{ type: 'path' }`.
|
|
774
|
+
*/
|
|
775
|
+
function inputToAdapterSource(config) {
|
|
776
|
+
const input = config.input;
|
|
777
|
+
if (!input) throw new Diagnostics.Error({
|
|
778
|
+
code: Diagnostics.code.inputRequired,
|
|
779
|
+
severity: "error",
|
|
780
|
+
message: "An adapter is configured without an input.",
|
|
781
|
+
help: "Set `input` to a file path, a URL, an inline spec (JSON/YAML string), or a parsed object in your Kubb config.",
|
|
782
|
+
location: { kind: "config" }
|
|
783
|
+
});
|
|
784
|
+
if (typeof input !== "string") return {
|
|
785
|
+
type: "data",
|
|
786
|
+
data: input
|
|
787
|
+
};
|
|
788
|
+
const kind = getInputKind(input);
|
|
789
|
+
if (kind === "inline") return {
|
|
790
|
+
type: "data",
|
|
791
|
+
data: input
|
|
792
|
+
};
|
|
793
|
+
if (kind === "url") return {
|
|
794
|
+
type: "path",
|
|
795
|
+
path: input
|
|
796
|
+
};
|
|
797
|
+
return {
|
|
798
|
+
type: "path",
|
|
799
|
+
path: resolve(config.root, input)
|
|
800
|
+
};
|
|
801
|
+
}
|
|
529
802
|
//#endregion
|
|
530
|
-
//#region
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
803
|
+
//#region src/Resolver.ts
|
|
804
|
+
function isNamespace(value) {
|
|
805
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Shared brand for reaching a resolver's build options. `Resolver.merge` reads this instead of
|
|
809
|
+
* relying on `instanceof`, which fails when a CommonJS config and the ESM CLI each load their own
|
|
810
|
+
* copy of `@kubb/core`. `Symbol.for` resolves to one key across those copies, so the options stay
|
|
811
|
+
* reachable and a `file` override is never dropped.
|
|
812
|
+
*/
|
|
813
|
+
const resolverOptions = Symbol.for("@kubb/core/resolver/options");
|
|
814
|
+
/**
|
|
815
|
+
* Built-in `file.baseName`: casts the identifier with `toFilePath` and appends the extension.
|
|
816
|
+
*/
|
|
817
|
+
function toBaseName({ name, extname }) {
|
|
818
|
+
return `${toFilePath(name)}${extname}`;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Base constraint for all plugin resolver objects.
|
|
822
|
+
*
|
|
823
|
+
* The built-in machinery lives under `default`. Generators call the top-level `name`, `file`,
|
|
824
|
+
* and `imports`, and a plugin overrides `name` and `file` to set its conventions. Extend with
|
|
825
|
+
* top-level helpers (`typeName`, …) and/or grouped namespaces (`query`, `schema`, …).
|
|
826
|
+
*
|
|
827
|
+
* @example Top-level helper
|
|
828
|
+
* ```ts
|
|
829
|
+
* type MyResolver = Resolver & {
|
|
830
|
+
* typeName(name: string): string
|
|
831
|
+
* }
|
|
832
|
+
* ```
|
|
833
|
+
*
|
|
834
|
+
* @example Grouped namespace
|
|
835
|
+
* ```ts
|
|
836
|
+
* type MyResolver = Resolver & {
|
|
837
|
+
* query: {
|
|
838
|
+
* name(node: OperationNode): string
|
|
839
|
+
* keyName(node: OperationNode): string
|
|
840
|
+
* }
|
|
841
|
+
* }
|
|
842
|
+
* ```
|
|
843
|
+
*/
|
|
844
|
+
var Resolver = class Resolver {
|
|
845
|
+
static #patternCache = /* @__PURE__ */ new Map();
|
|
846
|
+
static #optionsCache = /* @__PURE__ */ new WeakMap();
|
|
847
|
+
pluginName;
|
|
848
|
+
#options;
|
|
849
|
+
#baseName;
|
|
850
|
+
#filePath;
|
|
851
|
+
constructor(options) {
|
|
852
|
+
this.pluginName = options.pluginName;
|
|
853
|
+
this.#options = options;
|
|
854
|
+
this.#baseName = options.file?.baseName ? options.file.baseName.bind(this) : toBaseName;
|
|
855
|
+
this.#filePath = options.file?.path ? options.file.path.bind(this) : void 0;
|
|
856
|
+
this.#apply(options);
|
|
534
857
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
this.value = value;
|
|
858
|
+
/** Exposes the raw build options so `Resolver.merge` can read them across `@kubb/core` copies. */
|
|
859
|
+
get [resolverOptions]() {
|
|
860
|
+
return this.#options;
|
|
539
861
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
this.#
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
862
|
+
/**
|
|
863
|
+
* The built-in resolution machinery. Always reaches the untouched defaults, even when a
|
|
864
|
+
* plugin overrides the top-level `name` or `file`.
|
|
865
|
+
*/
|
|
866
|
+
get default() {
|
|
867
|
+
return {
|
|
868
|
+
name: camelCase,
|
|
869
|
+
options: this.#resolveOptions.bind(this),
|
|
870
|
+
path: this.#resolvePath.bind(this),
|
|
871
|
+
file: this.#resolveFile.bind(this),
|
|
872
|
+
banner: this.#resolveBanner.bind(this),
|
|
873
|
+
footer: this.#resolveFooter.bind(this)
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
name(name) {
|
|
877
|
+
return this.default.name(name);
|
|
878
|
+
}
|
|
879
|
+
file(options) {
|
|
880
|
+
return this.#resolveFile(options);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Builds one `ImportNode` per unique schema referenced in the tree, in first-occurrence
|
|
884
|
+
* order. Each ref's target resolves through `resolveRefName`, so collision- or macro-renamed
|
|
885
|
+
* schemas (`targetName`) import the emitted name. Names and paths go through the top-level
|
|
886
|
+
* `name` and `file`, so import entries follow the plugin's conventions, and a per-call
|
|
887
|
+
* `name` override wins over both.
|
|
888
|
+
*/
|
|
889
|
+
imports(options) {
|
|
890
|
+
const { node, root, output, group, extname = ".ts", name } = options;
|
|
891
|
+
const resolveName = name ?? ((schemaName) => this.name(schemaName));
|
|
892
|
+
const seen = /* @__PURE__ */ new Set();
|
|
893
|
+
return collectSync(node, { schema: (schemaNode) => {
|
|
894
|
+
const schemaRef = narrowSchema(schemaNode, "ref");
|
|
895
|
+
if (!schemaRef?.ref) return null;
|
|
896
|
+
const schemaName = resolveRefName(schemaRef);
|
|
897
|
+
if (!schemaName || seen.has(schemaName)) return null;
|
|
898
|
+
seen.add(schemaName);
|
|
899
|
+
return ast.factory.createImport({
|
|
900
|
+
name: [resolveName(schemaName)],
|
|
901
|
+
path: this.file({
|
|
902
|
+
name: schemaName,
|
|
903
|
+
extname,
|
|
904
|
+
root,
|
|
905
|
+
output,
|
|
906
|
+
group
|
|
907
|
+
}).path
|
|
908
|
+
});
|
|
909
|
+
} });
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Folds each `override` over `base`, left to right, and returns a new resolver with helpers
|
|
913
|
+
* re-bound. Top-level keys replace, and a namespace (or `file`) merges per method, so overriding
|
|
914
|
+
* `query.name` keeps the base `query.keyName`. The last override wins per key. Used when applying
|
|
915
|
+
* `setResolver` partial overrides, and to compose shared resolver fragments without spreading each
|
|
916
|
+
* namespace by hand. Reads a resolver's options through the shared brand rather than `instanceof`,
|
|
917
|
+
* so a `file` override survives even when `base` and `override` come from different `@kubb/core`
|
|
918
|
+
* copies.
|
|
919
|
+
*
|
|
920
|
+
* @example Fold several partial overrides onto a resolver
|
|
921
|
+
* ```ts
|
|
922
|
+
* const resolver = Resolver.merge(defaultResolver, sharedNamingPatch, { name: (name) => name.toUpperCase() })
|
|
923
|
+
* ```
|
|
924
|
+
*/
|
|
925
|
+
static merge(base, ...overrides) {
|
|
926
|
+
const merged = overrides.reduce((acc, override) => {
|
|
927
|
+
const patch = resolverOptions in override ? override[resolverOptions] : override;
|
|
928
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
929
|
+
if (value === void 0) continue;
|
|
930
|
+
const current = acc[key];
|
|
931
|
+
acc[key] = isNamespace(value) && isNamespace(current) ? {
|
|
932
|
+
...current,
|
|
933
|
+
...value
|
|
934
|
+
} : value;
|
|
935
|
+
}
|
|
936
|
+
return acc;
|
|
937
|
+
}, { ...base[resolverOptions] });
|
|
938
|
+
return new Resolver(merged);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Binds each entry of `options` onto the resolver, so `this.name`, `this.default`, and
|
|
942
|
+
* `this.file` resolve there for top-level helpers and namespace methods alike. `default`
|
|
943
|
+
* is skipped so it can't be shadowed.
|
|
944
|
+
*/
|
|
945
|
+
#apply(options) {
|
|
946
|
+
const root = this;
|
|
947
|
+
const bind = (value) => typeof value === "function" ? value.bind(root) : value;
|
|
948
|
+
for (const [key, value] of Object.entries(options)) {
|
|
949
|
+
if (key === "pluginName" || key === "default" || key === "file" || value === void 0) continue;
|
|
950
|
+
root[key] = isNamespace(value) ? Object.fromEntries(Object.entries(value).map(([method, member]) => [method, bind(member)])) : bind(value);
|
|
556
951
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (!this.#head) this.#tail = void 0;
|
|
565
|
-
return current.value;
|
|
566
|
-
}
|
|
567
|
-
peek() {
|
|
568
|
-
if (!this.#head) return;
|
|
569
|
-
return this.#head.value;
|
|
570
|
-
}
|
|
571
|
-
clear() {
|
|
572
|
-
this.#head = void 0;
|
|
573
|
-
this.#tail = void 0;
|
|
574
|
-
this.#size = 0;
|
|
575
|
-
}
|
|
576
|
-
get size() {
|
|
577
|
-
return this.#size;
|
|
578
|
-
}
|
|
579
|
-
*[Symbol.iterator]() {
|
|
580
|
-
let current = this.#head;
|
|
581
|
-
while (current) {
|
|
582
|
-
yield current.value;
|
|
583
|
-
current = current.next;
|
|
952
|
+
}
|
|
953
|
+
static #testPattern(value, pattern) {
|
|
954
|
+
if (typeof pattern === "string") {
|
|
955
|
+
let regex = Resolver.#patternCache.get(pattern);
|
|
956
|
+
regex ??= new RegExp(pattern);
|
|
957
|
+
Resolver.#patternCache.set(pattern, regex);
|
|
958
|
+
return regex.test(value);
|
|
584
959
|
}
|
|
960
|
+
return value.match(pattern) !== null;
|
|
585
961
|
}
|
|
586
|
-
|
|
587
|
-
|
|
962
|
+
static #matchesOperation(node, { type, pattern }) {
|
|
963
|
+
if (type === "tag") return node.tags.some((tag) => Resolver.#testPattern(tag, pattern));
|
|
964
|
+
if (type === "operationId") return Resolver.#testPattern(node.operationId, pattern);
|
|
965
|
+
if (type === "path") return node.path !== void 0 && Resolver.#testPattern(node.path, pattern);
|
|
966
|
+
if (type === "method") return node.method !== void 0 && Resolver.#testPattern(node.method.toLowerCase(), pattern);
|
|
967
|
+
if (type === "contentType") return node.requestBody?.content?.some((c) => Resolver.#testPattern(c.contentType, pattern)) ?? false;
|
|
968
|
+
return false;
|
|
588
969
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
970
|
+
/**
|
|
971
|
+
* Returns `null` when the filter type doesn't apply to schemas, so include rules built
|
|
972
|
+
* from operation filters (e.g. `tag`) don't exclude every schema.
|
|
973
|
+
*/
|
|
974
|
+
static #matchesSchema(node, { type, pattern }) {
|
|
975
|
+
if (type === "schemaName") return node.name ? Resolver.#testPattern(node.name, pattern) : false;
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
static #computeOptions(node, { options, exclude = [], include, override = [] }) {
|
|
979
|
+
if (operationDef.is(node)) {
|
|
980
|
+
if (exclude.some((filter) => Resolver.#matchesOperation(node, filter))) return null;
|
|
981
|
+
if (include && !include.some((filter) => Resolver.#matchesOperation(node, filter))) return null;
|
|
982
|
+
return {
|
|
983
|
+
...options,
|
|
984
|
+
...override.find((filter) => Resolver.#matchesOperation(node, filter))?.options
|
|
985
|
+
};
|
|
603
986
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const run = async (function_, resolve, arguments_) => {
|
|
610
|
-
const result = (async () => function_(...arguments_))();
|
|
611
|
-
resolve(result);
|
|
612
|
-
try {
|
|
613
|
-
await result;
|
|
614
|
-
} catch {}
|
|
615
|
-
next();
|
|
616
|
-
};
|
|
617
|
-
const enqueue = (function_, resolve, reject, arguments_) => {
|
|
618
|
-
const queueItem = { reject };
|
|
619
|
-
new Promise((internalResolve) => {
|
|
620
|
-
queueItem.run = internalResolve;
|
|
621
|
-
queue.enqueue(queueItem);
|
|
622
|
-
}).then(run.bind(void 0, function_, resolve, arguments_));
|
|
623
|
-
if (activeCount < concurrency) resumeNext();
|
|
624
|
-
};
|
|
625
|
-
const generator = (function_, ...arguments_) => new Promise((resolve, reject) => {
|
|
626
|
-
enqueue(function_, resolve, reject, arguments_);
|
|
627
|
-
});
|
|
628
|
-
Object.defineProperties(generator, {
|
|
629
|
-
activeCount: { get: () => activeCount },
|
|
630
|
-
pendingCount: { get: () => queue.size },
|
|
631
|
-
clearQueue: { value() {
|
|
632
|
-
if (!rejectOnClear) {
|
|
633
|
-
queue.clear();
|
|
634
|
-
return;
|
|
987
|
+
if (schemaDef.is(node)) {
|
|
988
|
+
if (exclude.some((filter) => Resolver.#matchesSchema(node, filter) === true)) return null;
|
|
989
|
+
if (include) {
|
|
990
|
+
const applicable = include.map((filter) => Resolver.#matchesSchema(node, filter)).filter((result) => result !== null);
|
|
991
|
+
if (applicable.length > 0 && !applicable.includes(true)) return null;
|
|
635
992
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
993
|
+
return {
|
|
994
|
+
...options,
|
|
995
|
+
...override.find((filter) => Resolver.#matchesSchema(node, filter) === true)?.options
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
return options;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Applies include/exclude filters and merges matching override options, caching the result
|
|
1002
|
+
* per `(options, node)` pair. Returns `null` when the node is filtered out.
|
|
1003
|
+
*/
|
|
1004
|
+
#resolveOptions(node, context) {
|
|
1005
|
+
const { options } = context;
|
|
1006
|
+
if (typeof options !== "object" || options === null) return Resolver.#computeOptions(node, context);
|
|
1007
|
+
let byOptions = Resolver.#optionsCache.get(options);
|
|
1008
|
+
if (!byOptions) {
|
|
1009
|
+
byOptions = /* @__PURE__ */ new WeakMap();
|
|
1010
|
+
Resolver.#optionsCache.set(options, byOptions);
|
|
1011
|
+
}
|
|
1012
|
+
const cached = byOptions.get(node);
|
|
1013
|
+
if (cached) return cached.value;
|
|
1014
|
+
const result = Resolver.#computeOptions(node, context);
|
|
1015
|
+
byOptions.set(node, { value: result });
|
|
1016
|
+
return result;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* A custom `group.name` wins; otherwise `tag` groups use the camelCased tag and `path`
|
|
1020
|
+
* groups use the first non-traversal segment (`''` when none remain, placing the file in
|
|
1021
|
+
* the output root, kept safe by the caller's boundary check).
|
|
1022
|
+
*/
|
|
1023
|
+
static #resolveGroupDir(group, groupValue) {
|
|
1024
|
+
if (group.name) return group.name({ group: groupValue });
|
|
1025
|
+
if (group.type === "tag") return camelCase(groupValue);
|
|
1026
|
+
const segment = groupValue.split("/").filter((part) => part !== "" && part !== "." && part !== "..")[0];
|
|
1027
|
+
return segment ? camelCase(segment) : "";
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* `mode: 'file'` (default) resolves directly to `output.path`. `mode: 'directory'` resolves
|
|
1031
|
+
* to `output.path/{baseName}`, or into a subdirectory when `group` and a `tag`/`path` value
|
|
1032
|
+
* are provided.
|
|
1033
|
+
*/
|
|
1034
|
+
#resolvePath({ baseName, tag, path: groupPath, root, output, group }) {
|
|
1035
|
+
if (output.mode !== "directory") return path.resolve(root, output.path);
|
|
1036
|
+
const outputDir = path.resolve(root, output.path);
|
|
1037
|
+
const result = group && (groupPath || tag) ? path.resolve(outputDir, Resolver.#resolveGroupDir(group, group.type === "path" ? groupPath : tag), baseName) : path.resolve(outputDir, baseName);
|
|
1038
|
+
const outputDirWithSep = outputDir.endsWith(path.sep) ? outputDir : `${outputDir}${path.sep}`;
|
|
1039
|
+
if (result !== outputDir && !result.startsWith(outputDirWithSep)) throw new Diagnostics.Error({
|
|
1040
|
+
code: Diagnostics.code.pathTraversal,
|
|
1041
|
+
severity: "error",
|
|
1042
|
+
message: `Resolved path "${result}" is outside the output directory "${outputDir}".`,
|
|
1043
|
+
help: "This can stem from a path traversal in the OpenAPI specification or a misconfigured `group.name` function. Keep generated paths within the output directory.",
|
|
1044
|
+
location: { kind: "config" }
|
|
1045
|
+
});
|
|
1046
|
+
return result;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Resolves a resolver-supplied full path (`file.path`) against `root`, bypassing `output.path`
|
|
1050
|
+
* and `group`. The path may not escape `root`, which keeps a `file.path` that interpolates
|
|
1051
|
+
* spec-derived values from writing outside the project.
|
|
1052
|
+
*/
|
|
1053
|
+
#resolveOverridePath(filePath, root) {
|
|
1054
|
+
const resolved = path.resolve(root, filePath);
|
|
1055
|
+
const rootWithSep = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
1056
|
+
if (resolved !== root && !resolved.startsWith(rootWithSep)) throw new Diagnostics.Error({
|
|
1057
|
+
code: Diagnostics.code.pathTraversal,
|
|
1058
|
+
severity: "error",
|
|
1059
|
+
message: `Resolved path "${resolved}" is outside the project root "${root}".`,
|
|
1060
|
+
help: "A resolver `file.path` must return a path inside the project root.",
|
|
1061
|
+
location: { kind: "config" }
|
|
1062
|
+
});
|
|
1063
|
+
return resolved;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Builds a `FileNode`. When `#filePath` (the resolver's `file.path`) is set it owns the whole
|
|
1067
|
+
* path; otherwise the base name (from `#baseName`, the resolver's `file.baseName` or the
|
|
1068
|
+
* built-in `toBaseName`) is placed by the `output.path`/`group` layout. The resolved file starts
|
|
1069
|
+
* with empty `sources`, `imports`, and `exports`, which consumers populate separately.
|
|
1070
|
+
*/
|
|
1071
|
+
#resolveFile(options) {
|
|
1072
|
+
const { name, extname, tag, path: groupPath, root, output, group } = options;
|
|
1073
|
+
const baseName = this.#baseName({
|
|
1074
|
+
name,
|
|
1075
|
+
extname
|
|
1076
|
+
});
|
|
1077
|
+
const filePath = this.#filePath ? this.#resolveOverridePath(this.#filePath({
|
|
1078
|
+
baseName,
|
|
1079
|
+
output
|
|
1080
|
+
}), root) : this.#resolvePath({
|
|
1081
|
+
baseName,
|
|
1082
|
+
tag,
|
|
1083
|
+
path: groupPath,
|
|
1084
|
+
root,
|
|
1085
|
+
output,
|
|
1086
|
+
group
|
|
1087
|
+
});
|
|
1088
|
+
return ast.factory.createFile({
|
|
1089
|
+
path: filePath,
|
|
1090
|
+
baseName: path.basename(filePath),
|
|
1091
|
+
meta: { pluginName: this.pluginName },
|
|
1092
|
+
sources: [],
|
|
1093
|
+
imports: [],
|
|
1094
|
+
exports: []
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Missing fields default to empty/`false` so the `BannerMeta` shape stays stable even when
|
|
1099
|
+
* a caller (e.g. the barrel plugin) has no document metadata.
|
|
1100
|
+
*/
|
|
1101
|
+
static #buildBannerMeta(meta, file) {
|
|
1102
|
+
return {
|
|
1103
|
+
title: meta?.title,
|
|
1104
|
+
description: meta?.description,
|
|
1105
|
+
version: meta?.version,
|
|
1106
|
+
baseURL: meta?.baseURL,
|
|
1107
|
+
circularNames: meta?.circularNames ?? [],
|
|
1108
|
+
enumNames: meta?.enumNames ?? [],
|
|
1109
|
+
filePath: file?.path ?? "",
|
|
1110
|
+
baseName: file?.baseName ?? "",
|
|
1111
|
+
isBarrel: file?.isBarrel ?? false,
|
|
1112
|
+
isAggregation: file?.isAggregation ?? false
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Resolves a user-configured banner/footer value. `undefined` means not configured.
|
|
1117
|
+
*/
|
|
1118
|
+
static #resolveUserText(value, meta, file) {
|
|
1119
|
+
if (typeof value === "function") return value(Resolver.#buildBannerMeta(meta, file));
|
|
1120
|
+
if (typeof value === "string") return value;
|
|
1121
|
+
}
|
|
1122
|
+
static #buildDefaultBanner({ title, version, config }) {
|
|
1123
|
+
const lines = [
|
|
1124
|
+
"/**",
|
|
1125
|
+
"* Generated by Kubb (https://kubb.dev/).",
|
|
1126
|
+
"* Do not edit manually."
|
|
1127
|
+
];
|
|
1128
|
+
if (config.output.defaultBanner !== "simple") {
|
|
1129
|
+
const input = config.input;
|
|
1130
|
+
let source = "";
|
|
1131
|
+
if (typeof input === "string") source = getInputKind(input) === "inline" ? "text content" : path.basename(input);
|
|
1132
|
+
else if (input) source = "text content";
|
|
1133
|
+
if (source) lines.push(`* Source: ${source}`);
|
|
1134
|
+
if (title) lines.push(`* Title: ${title}`);
|
|
1135
|
+
if (version) lines.push(`* OpenAPI spec version: ${version}`);
|
|
1136
|
+
}
|
|
1137
|
+
return `${lines.join("\n")}\n*/\n`;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* A user-supplied `output.banner` overrides the default Kubb notice. When
|
|
1141
|
+
* `config.output.defaultBanner` is `false` and no user banner is set, returns `null`.
|
|
1142
|
+
*/
|
|
1143
|
+
#resolveBanner(meta, { output, config, file }) {
|
|
1144
|
+
const userBanner = Resolver.#resolveUserText(output?.banner, meta, file);
|
|
1145
|
+
if (userBanner !== void 0) return userBanner;
|
|
1146
|
+
if (config.output.defaultBanner === false) return null;
|
|
1147
|
+
return Resolver.#buildDefaultBanner({
|
|
1148
|
+
title: meta?.title,
|
|
1149
|
+
version: meta?.version,
|
|
1150
|
+
config
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
#resolveFooter(meta, { output, file }) {
|
|
1154
|
+
return Resolver.#resolveUserText(output?.footer, meta, file) ?? null;
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
659
1157
|
//#endregion
|
|
660
|
-
//#region src/
|
|
661
|
-
|
|
662
|
-
|
|
1158
|
+
//#region src/createResolver.ts
|
|
1159
|
+
/**
|
|
1160
|
+
* Defines a plugin resolver, the object that decides what every generated symbol and file
|
|
1161
|
+
* path is called. Override the top-level `name` and `file` to set the plugin's conventions,
|
|
1162
|
+
* and add your own naming helpers, top-level (`typeName`, …) or grouped in namespaces
|
|
1163
|
+
* (`query`, `schema`, …). Every method reaches sibling helpers and the built-in machinery
|
|
1164
|
+
* through `this.name`, `this.file`, and `this.default`.
|
|
1165
|
+
*
|
|
1166
|
+
* @example Custom identifier casing
|
|
1167
|
+
* ```ts
|
|
1168
|
+
* export const resolverTs = createResolver<PluginTs>({
|
|
1169
|
+
* pluginName: 'plugin-ts',
|
|
1170
|
+
* name(name) {
|
|
1171
|
+
* return ensureValidVarName(pascalCase(name))
|
|
1172
|
+
* },
|
|
1173
|
+
* })
|
|
1174
|
+
* ```
|
|
1175
|
+
*
|
|
1176
|
+
* @example Rename generated files with `file.baseName`
|
|
1177
|
+
* ```ts
|
|
1178
|
+
* export const resolverFaker = createResolver<PluginFaker>({
|
|
1179
|
+
* pluginName: 'plugin-faker',
|
|
1180
|
+
* name(name) {
|
|
1181
|
+
* return camelCase(name, { prefix: 'create' })
|
|
1182
|
+
* },
|
|
1183
|
+
* file: {
|
|
1184
|
+
* baseName({ name, extname }) {
|
|
1185
|
+
* return `${camelCase(name, { prefix: 'create' })}${extname}`
|
|
1186
|
+
* },
|
|
1187
|
+
* },
|
|
1188
|
+
* })
|
|
1189
|
+
* ```
|
|
1190
|
+
*
|
|
1191
|
+
* @example Own the full path with `file.path`
|
|
1192
|
+
* ```ts
|
|
1193
|
+
* export const resolverFaker = createResolver<PluginFaker>({
|
|
1194
|
+
* pluginName: 'plugin-faker',
|
|
1195
|
+
* file: {
|
|
1196
|
+
* path({ baseName, output }) {
|
|
1197
|
+
* return `${output.path}/mocks/${baseName}`
|
|
1198
|
+
* },
|
|
1199
|
+
* },
|
|
1200
|
+
* })
|
|
1201
|
+
* ```
|
|
1202
|
+
*/
|
|
1203
|
+
function createResolver(options) {
|
|
1204
|
+
return new Resolver(options);
|
|
663
1205
|
}
|
|
1206
|
+
//#endregion
|
|
1207
|
+
//#region src/Transform.ts
|
|
664
1208
|
/**
|
|
665
|
-
*
|
|
666
|
-
*
|
|
1209
|
+
* Holds an ordered list of macros per plugin, keyed by plugin name. Each plugin's macros run in
|
|
1210
|
+
* isolation on the original adapter node and are composed into a single `Visitor` that the
|
|
1211
|
+
* `@kubb/ast` `transform` primitive applies. `applyTo` is a per-plugin lookup, not a cross-plugin
|
|
1212
|
+
* chain, so plugin A's macros never see plugin B's output. When a plugin has no macros, `applyTo`
|
|
1213
|
+
* returns the original node reference, and `transform` does the same when the composed visitor
|
|
1214
|
+
* leaves the tree untouched, so callers can detect a no-op by identity.
|
|
667
1215
|
*
|
|
668
|
-
*
|
|
1216
|
+
* Registration order matches the order setup hooks fire, which the driver has already sorted by
|
|
1217
|
+
* `enforce` and dependency edges. The registry preserves that order. Macro `enforce` only reorders
|
|
1218
|
+
* within a single plugin's list.
|
|
669
1219
|
*/
|
|
670
|
-
var
|
|
671
|
-
#
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1220
|
+
var Transform = class {
|
|
1221
|
+
#macros = /* @__PURE__ */ new Map();
|
|
1222
|
+
#composed = /* @__PURE__ */ new Map();
|
|
1223
|
+
#memo = /* @__PURE__ */ new Map();
|
|
1224
|
+
/**
|
|
1225
|
+
* Appends `macro` to the plugin's list, after any macros already registered.
|
|
1226
|
+
*/
|
|
1227
|
+
add(pluginName, macro) {
|
|
1228
|
+
const list = this.#macros.get(pluginName);
|
|
1229
|
+
if (list) list.push(macro);
|
|
1230
|
+
else this.#macros.set(pluginName, [macro]);
|
|
1231
|
+
this.#invalidate(pluginName);
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Replaces the plugin's macro list with `macros`.
|
|
1235
|
+
*/
|
|
1236
|
+
set(pluginName, macros) {
|
|
1237
|
+
this.#macros.set(pluginName, [...macros]);
|
|
1238
|
+
this.#invalidate(pluginName);
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Runs the plugin's macros on `node`. Returns the original node reference when the plugin has no
|
|
1242
|
+
* macros, so callers can compare by identity to detect a no-op.
|
|
1243
|
+
*/
|
|
1244
|
+
applyTo(pluginName, node) {
|
|
1245
|
+
const visitor = this.#visitorFor(pluginName);
|
|
1246
|
+
if (!visitor) return node;
|
|
1247
|
+
let memo = this.#memo.get(pluginName);
|
|
1248
|
+
if (!memo) {
|
|
1249
|
+
memo = /* @__PURE__ */ new WeakMap();
|
|
1250
|
+
this.#memo.set(pluginName, memo);
|
|
1251
|
+
}
|
|
1252
|
+
const cached = memo.get(node);
|
|
1253
|
+
if (cached) return cached;
|
|
1254
|
+
const result = transform(node, visitor);
|
|
1255
|
+
memo.set(node, result);
|
|
1256
|
+
return result;
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Clears every registration. Called from the driver's `dispose()` so macros do not leak across
|
|
1260
|
+
* builds.
|
|
1261
|
+
*/
|
|
1262
|
+
dispose() {
|
|
1263
|
+
this.#macros.clear();
|
|
1264
|
+
this.#composed.clear();
|
|
1265
|
+
this.#memo.clear();
|
|
1266
|
+
}
|
|
1267
|
+
#invalidate(pluginName) {
|
|
1268
|
+
this.#composed.delete(pluginName);
|
|
1269
|
+
this.#memo.delete(pluginName);
|
|
1270
|
+
}
|
|
1271
|
+
#visitorFor(pluginName) {
|
|
1272
|
+
const macros = this.#macros.get(pluginName);
|
|
1273
|
+
if (!macros || macros.length === 0) return void 0;
|
|
1274
|
+
let composed = this.#composed.get(pluginName);
|
|
1275
|
+
if (!composed) {
|
|
1276
|
+
composed = composeMacros(macros);
|
|
1277
|
+
this.#composed.set(pluginName, composed);
|
|
1278
|
+
}
|
|
1279
|
+
return composed;
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
//#endregion
|
|
1283
|
+
//#region src/KubbDriver.ts
|
|
1284
|
+
const ENFORCE_ORDER = {
|
|
1285
|
+
pre: -1,
|
|
1286
|
+
post: 1
|
|
1287
|
+
};
|
|
1288
|
+
const enforceWeight = (plugin) => plugin.enforce ? ENFORCE_ORDER[plugin.enforce] : 0;
|
|
1289
|
+
var KubbDriver = class {
|
|
1290
|
+
config;
|
|
1291
|
+
options;
|
|
1292
|
+
/**
|
|
1293
|
+
* The `InputNode` produced by the adapter. Set after adapter setup.
|
|
1294
|
+
*/
|
|
1295
|
+
inputNode = null;
|
|
1296
|
+
adapter = null;
|
|
1297
|
+
/**
|
|
1298
|
+
* Raw adapter source so `adapter.parse()` can run lazily.
|
|
1299
|
+
* Intentionally outlives the build, cleared by `dispose()`.
|
|
1300
|
+
*/
|
|
1301
|
+
#adapterSource = null;
|
|
1302
|
+
/**
|
|
1303
|
+
* Central file store for all generated files.
|
|
1304
|
+
* Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
|
|
1305
|
+
* add files. This property gives direct read/write access when needed.
|
|
1306
|
+
*/
|
|
1307
|
+
fileManager = new FileManager();
|
|
1308
|
+
plugins = /* @__PURE__ */ new Map();
|
|
1309
|
+
/**
|
|
1310
|
+
* Tracks which plugins have generators registered via `addGenerator()` (hook-based path).
|
|
1311
|
+
* Used by the build loop to decide whether to emit generator hooks for a given plugin.
|
|
1312
|
+
*/
|
|
1313
|
+
#hookGeneratorPlugins = /* @__PURE__ */ new Set();
|
|
1314
|
+
#resolvers = /* @__PURE__ */ new Map();
|
|
1315
|
+
#defaultResolvers = /* @__PURE__ */ new Map();
|
|
1316
|
+
/**
|
|
1317
|
+
* Removers for every listener the driver added (plugin, generator) so `dispose()` can detach
|
|
1318
|
+
* them in one pass. External `hooks.hook(...)` listeners are not tracked.
|
|
1319
|
+
*/
|
|
1320
|
+
#unhooks = [];
|
|
1321
|
+
/**
|
|
1322
|
+
* Transform registry. Plugins populate it during `kubb:plugin:setup` via `addMacro`/`setMacros`,
|
|
1323
|
+
* and `#runGenerators` reads it once per `(plugin, node)` pair through `applyTo`.
|
|
1324
|
+
*/
|
|
1325
|
+
#transforms = new Transform();
|
|
1326
|
+
constructor(config, options) {
|
|
1327
|
+
this.config = config;
|
|
1328
|
+
this.options = options;
|
|
1329
|
+
this.adapter = config.adapter ?? null;
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Normalizes every configured plugin, orders them, and registers their lifecycle handlers.
|
|
1333
|
+
* A plugin that another lists as a dependency runs first, then `enforce: 'pre'` before
|
|
1334
|
+
* `'post'`. When the config has an adapter, the adapter source is resolved from the input
|
|
1335
|
+
* so `run` can parse it later.
|
|
1336
|
+
*/
|
|
1337
|
+
async setup() {
|
|
1338
|
+
const normalized = this.#sortPlugins(this.config.plugins.map((rawPlugin) => {
|
|
1339
|
+
return {
|
|
1340
|
+
name: rawPlugin.name,
|
|
1341
|
+
dependencies: rawPlugin.dependencies,
|
|
1342
|
+
enforce: rawPlugin.enforce,
|
|
1343
|
+
hooks: rawPlugin.hooks,
|
|
1344
|
+
options: rawPlugin.options ?? {
|
|
1345
|
+
output: {
|
|
1346
|
+
path: ".",
|
|
1347
|
+
mode: "directory"
|
|
1348
|
+
},
|
|
1349
|
+
exclude: [],
|
|
1350
|
+
override: []
|
|
1351
|
+
},
|
|
1352
|
+
resolver: this.#getDefaultResolver(rawPlugin.name)
|
|
1353
|
+
};
|
|
1354
|
+
}));
|
|
1355
|
+
for (const plugin of normalized) {
|
|
1356
|
+
this.#registerPlugin(plugin);
|
|
1357
|
+
this.plugins.set(plugin.name, plugin);
|
|
1358
|
+
}
|
|
1359
|
+
if (this.config.adapter) this.#adapterSource = inputToAdapterSource(this.config);
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Orders plugins so every dependency runs before its dependents (Kahn's algorithm), with
|
|
1363
|
+
* `enforce` (`'pre'` before normal before `'post'`) and declaration order as tiebreaks.
|
|
1364
|
+
* A pairwise `Array.sort` comparator cannot do this: dependency relations are not transitive
|
|
1365
|
+
* at the comparator level, so a chain where A depends on B and B depends on C could come out
|
|
1366
|
+
* wrong when A and C are never compared directly. Dependencies on plugins missing from the
|
|
1367
|
+
* config are ignored here and surface later through `requirePlugin`.
|
|
1368
|
+
*/
|
|
1369
|
+
#sortPlugins(plugins) {
|
|
1370
|
+
const queue = [...plugins].sort((a, b) => enforceWeight(a) - enforceWeight(b));
|
|
1371
|
+
const names = new Set(queue.map((plugin) => plugin.name));
|
|
1372
|
+
const blockedBy = new Map(queue.map((plugin) => [plugin.name, new Set(plugin.dependencies?.filter((name) => names.has(name) && name !== plugin.name))]));
|
|
1373
|
+
const sorted = [];
|
|
1374
|
+
for (const _ of plugins) {
|
|
1375
|
+
const index = queue.findIndex((plugin) => blockedBy.get(plugin.name)?.size === 0);
|
|
1376
|
+
if (index === -1) throw new Diagnostics.Error({
|
|
1377
|
+
code: Diagnostics.code.invalidPluginOptions,
|
|
1378
|
+
severity: "error",
|
|
1379
|
+
message: `Plugin dependencies form a cycle: ${queue.map((plugin) => plugin.name).join(" → ")}.`,
|
|
1380
|
+
help: "Remove one of the `dependencies` entries so the plugins can be ordered.",
|
|
1381
|
+
location: { kind: "config" }
|
|
687
1382
|
});
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1383
|
+
const [plugin] = queue.splice(index, 1);
|
|
1384
|
+
if (!plugin) break;
|
|
1385
|
+
sorted.push(plugin);
|
|
1386
|
+
for (const blockers of blockedBy.values()) blockers.delete(plugin.name);
|
|
1387
|
+
}
|
|
1388
|
+
return sorted;
|
|
1389
|
+
}
|
|
1390
|
+
get hooks() {
|
|
1391
|
+
return this.options.hooks;
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Parses the adapter source into `this.inputNode`. Idempotent, so repeated calls from
|
|
1395
|
+
* `run` do not re-parse.
|
|
1396
|
+
*/
|
|
1397
|
+
async #parseInput() {
|
|
1398
|
+
if (this.inputNode || !this.adapter || !this.#adapterSource) return;
|
|
1399
|
+
this.inputNode = await this.adapter.parse(this.#adapterSource);
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Registers a plugin's lifecycle hooks on the shared `Hookable` as pass-through listeners that
|
|
1403
|
+
* external tooling can observe via `hooks.hook(...)`. The returned remover is tracked for
|
|
1404
|
+
* `dispose`. `kubb:plugin:setup` is skipped here; `setupHooks` invokes it directly with a
|
|
1405
|
+
* plugin-scoped context.
|
|
1406
|
+
*
|
|
1407
|
+
* @internal
|
|
1408
|
+
*/
|
|
1409
|
+
#registerPlugin(plugin) {
|
|
1410
|
+
const { hooks } = plugin;
|
|
1411
|
+
if (!hooks) return;
|
|
1412
|
+
const { "kubb:plugin:setup": _setup, ...configHooks } = hooks;
|
|
1413
|
+
this.#unhooks.push(this.hooks.addHooks(configHooks));
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Runs each plugin's `kubb:plugin:setup` handler, in plugin order, with a context scoped to that
|
|
1417
|
+
* plugin so `addGenerator`, `setResolver`, `addMacro`, `setMacros`, and `setOptions` target its
|
|
1418
|
+
* `NormalizedPlugin` entry. Called once from `run` before the plugin execution loop begins, so
|
|
1419
|
+
* plugins can configure generators, resolvers, macros, and options before `buildStart`.
|
|
1420
|
+
*/
|
|
1421
|
+
async setupHooks() {
|
|
1422
|
+
for (const plugin of this.plugins.values()) {
|
|
1423
|
+
const setup = plugin.hooks?.["kubb:plugin:setup"];
|
|
1424
|
+
if (!setup) continue;
|
|
1425
|
+
await setup({
|
|
1426
|
+
config: this.config,
|
|
1427
|
+
options: plugin.options ?? {},
|
|
1428
|
+
addGenerator: (...generators) => {
|
|
1429
|
+
for (const generator of generators) this.registerGenerator(plugin.name, generator);
|
|
1430
|
+
},
|
|
1431
|
+
setResolver: (resolver) => {
|
|
1432
|
+
this.setPluginResolver(plugin.name, resolver);
|
|
1433
|
+
},
|
|
1434
|
+
addMacro: (macro) => {
|
|
1435
|
+
this.#transforms.add(plugin.name, macro);
|
|
1436
|
+
},
|
|
1437
|
+
setMacros: (macros) => {
|
|
1438
|
+
this.#transforms.set(plugin.name, macros);
|
|
1439
|
+
},
|
|
1440
|
+
setOptions: (opts) => {
|
|
1441
|
+
plugin.options = {
|
|
1442
|
+
...plugin.options,
|
|
1443
|
+
...opts
|
|
1444
|
+
};
|
|
1445
|
+
if (plugin.options.output) {
|
|
1446
|
+
const group = "group" in plugin.options ? plugin.options.group : void 0;
|
|
1447
|
+
plugin.options.output = normalizeOutput({
|
|
1448
|
+
output: plugin.options.output,
|
|
1449
|
+
group,
|
|
1450
|
+
pluginName: plugin.name
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
},
|
|
1454
|
+
injectFile: (userFileNode) => {
|
|
1455
|
+
this.fileManager.add(ast.factory.createFile(userFileNode));
|
|
1456
|
+
}
|
|
696
1457
|
});
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Registers a generator for the given plugin on the shared hook emitter.
|
|
1462
|
+
*
|
|
1463
|
+
* The generator's `schema`, `operation`, and `operations` methods are registered as
|
|
1464
|
+
* listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations`
|
|
1465
|
+
* respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check
|
|
1466
|
+
* so that generators from different plugins do not cross-fire.
|
|
1467
|
+
*
|
|
1468
|
+
* The renderer comes from `generator.renderer`. Set `generator.renderer = null` (or leave it
|
|
1469
|
+
* unset) to opt out of rendering.
|
|
1470
|
+
*
|
|
1471
|
+
* Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
|
|
1472
|
+
*/
|
|
1473
|
+
registerGenerator(pluginName, generator) {
|
|
1474
|
+
const wrap = (method) => {
|
|
1475
|
+
if (!method) return void 0;
|
|
1476
|
+
return async (node, ctx) => {
|
|
1477
|
+
if (ctx.plugin.name !== pluginName) return;
|
|
1478
|
+
const result = await method(node, ctx);
|
|
1479
|
+
await this.dispatch({
|
|
1480
|
+
result,
|
|
1481
|
+
renderer: generator.renderer
|
|
1482
|
+
});
|
|
1483
|
+
};
|
|
697
1484
|
};
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
1485
|
+
this.#unhooks.push(this.hooks.addHooks({
|
|
1486
|
+
"kubb:generate:schema": wrap(generator.schema),
|
|
1487
|
+
"kubb:generate:operation": wrap(generator.operation),
|
|
1488
|
+
"kubb:generate:operations": wrap(generator.operations)
|
|
1489
|
+
}));
|
|
1490
|
+
this.#hookGeneratorPlugins.add(pluginName);
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Returns `true` when at least one generator was registered for the given plugin
|
|
1494
|
+
* via `addGenerator()` in `kubb:plugin:setup`.
|
|
1495
|
+
*
|
|
1496
|
+
* Used by the build loop to decide whether to walk the AST and emit generator hooks
|
|
1497
|
+
* for a plugin.
|
|
1498
|
+
*/
|
|
1499
|
+
hasHookGenerators(pluginName) {
|
|
1500
|
+
return this.#hookGeneratorPlugins.has(pluginName);
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Runs the full plugin pipeline. Returns the diagnostics collected so far even
|
|
1504
|
+
* when an outer hook throws, since the orchestrator preserves partial state by capturing
|
|
1505
|
+
* the failure as a {@link Diagnostic} instead of propagating. Each plugin also
|
|
1506
|
+
* contributes a `timing` diagnostic for the run summary.
|
|
1507
|
+
*/
|
|
1508
|
+
async run() {
|
|
1509
|
+
const { hooks, config, fileManager } = this;
|
|
1510
|
+
const diagnostics = [];
|
|
1511
|
+
const updateBuffer = [];
|
|
1512
|
+
const parsersMap = /* @__PURE__ */ new Map();
|
|
1513
|
+
for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
|
|
1514
|
+
const unhookWrites = fileManager.hooks.addHooks({
|
|
1515
|
+
start: async (files) => {
|
|
1516
|
+
await hooks.callHook("kubb:files:processing:start", { files });
|
|
1517
|
+
},
|
|
1518
|
+
update: (item) => {
|
|
1519
|
+
updateBuffer.push(item);
|
|
1520
|
+
},
|
|
1521
|
+
end: async (files) => {
|
|
1522
|
+
await hooks.callHook("kubb:files:processing:update", { files: updateBuffer.map((item) => ({
|
|
1523
|
+
...item,
|
|
1524
|
+
config
|
|
1525
|
+
})) });
|
|
1526
|
+
updateBuffer.length = 0;
|
|
1527
|
+
await hooks.callHook("kubb:files:processing:end", { files });
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
return Diagnostics.scope((diagnostic) => diagnostics.push(diagnostic), async () => {
|
|
1531
|
+
try {
|
|
1532
|
+
const outputRoot = resolve(config.root, config.output.path);
|
|
1533
|
+
await this.#parseInput();
|
|
1534
|
+
await this.setupHooks();
|
|
1535
|
+
if (this.adapter && this.inputNode) await hooks.callHook("kubb:build:start", Object.assign({
|
|
1536
|
+
config,
|
|
1537
|
+
adapter: this.adapter,
|
|
1538
|
+
meta: this.inputNode.meta,
|
|
1539
|
+
getPlugin: this.getPlugin.bind(this)
|
|
1540
|
+
}, this.#filesPayload()));
|
|
1541
|
+
const generatorPlugins = [];
|
|
1542
|
+
for (const plugin of this.plugins.values()) {
|
|
1543
|
+
const context = this.getContext(plugin);
|
|
1544
|
+
const hrStart = process.hrtime();
|
|
1545
|
+
try {
|
|
1546
|
+
await hooks.callHook("kubb:plugin:start", { plugin });
|
|
1547
|
+
} catch (caughtError) {
|
|
1548
|
+
const error = toError(caughtError);
|
|
1549
|
+
const duration = getElapsedMs(hrStart);
|
|
1550
|
+
await this.#emitPluginEnd({
|
|
1551
|
+
plugin,
|
|
1552
|
+
duration,
|
|
1553
|
+
success: false,
|
|
1554
|
+
error
|
|
1555
|
+
});
|
|
1556
|
+
diagnostics.push({
|
|
1557
|
+
...Diagnostics.from(error),
|
|
1558
|
+
plugin: plugin.name
|
|
1559
|
+
}, Diagnostics.performance({
|
|
1560
|
+
plugin: plugin.name,
|
|
1561
|
+
duration
|
|
1562
|
+
}));
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
if (this.hasHookGenerators(plugin.name)) {
|
|
1566
|
+
generatorPlugins.push({
|
|
1567
|
+
plugin,
|
|
1568
|
+
context,
|
|
1569
|
+
hrStart
|
|
1570
|
+
});
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
const duration = getElapsedMs(hrStart);
|
|
1574
|
+
diagnostics.push(Diagnostics.performance({
|
|
1575
|
+
plugin: plugin.name,
|
|
1576
|
+
duration
|
|
1577
|
+
}));
|
|
1578
|
+
await this.#emitPluginEnd({
|
|
1579
|
+
plugin,
|
|
1580
|
+
duration,
|
|
1581
|
+
success: true
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
diagnostics.push(...await this.#runGenerators(generatorPlugins));
|
|
1585
|
+
await hooks.callHook("kubb:plugins:end", Object.assign({ config }, this.#filesPayload()));
|
|
1586
|
+
await fileManager.write(fileManager.files, {
|
|
1587
|
+
storage: config.storage,
|
|
1588
|
+
parsers: parsersMap
|
|
1589
|
+
});
|
|
1590
|
+
await hooks.callHook("kubb:build:end", {
|
|
1591
|
+
files: this.fileManager.files,
|
|
1592
|
+
config,
|
|
1593
|
+
outputDir: outputRoot
|
|
1594
|
+
});
|
|
1595
|
+
return { diagnostics: Diagnostics.dedupe(diagnostics) };
|
|
1596
|
+
} catch (caughtError) {
|
|
1597
|
+
diagnostics.push(Diagnostics.from(caughtError));
|
|
1598
|
+
return { diagnostics: Diagnostics.dedupe(diagnostics) };
|
|
1599
|
+
} finally {
|
|
1600
|
+
unhookWrites();
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
#filesPayload() {
|
|
1605
|
+
const driver = this;
|
|
1606
|
+
return {
|
|
1607
|
+
get files() {
|
|
1608
|
+
return driver.fileManager.files;
|
|
1609
|
+
},
|
|
1610
|
+
upsertFile: (...files) => driver.fileManager.upsert(...files)
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
#emitPluginEnd({ plugin, duration, success, error }) {
|
|
1614
|
+
return this.hooks.callHook("kubb:plugin:end", Object.assign({
|
|
1615
|
+
plugin,
|
|
1616
|
+
duration,
|
|
1617
|
+
success,
|
|
1618
|
+
...error ? { error } : {},
|
|
1619
|
+
config: this.config
|
|
1620
|
+
}, this.#filesPayload()));
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Runs schemas and operations through every plugin's generators. Each node is run
|
|
1624
|
+
* through the plugin's macros (from `this.#transforms`) before the generator sees it,
|
|
1625
|
+
* so plugins stay isolated and the hot path stays per-node. Schemas run before operations
|
|
1626
|
+
* so file output stays deterministic across runs.
|
|
1627
|
+
* A failing plugin contributes an error diagnostic so the rest of the build continues.
|
|
1628
|
+
* Every plugin also contributes a `timing` diagnostic.
|
|
1629
|
+
*
|
|
1630
|
+
* Plugins are processed one at a time, in full, so `kubb:plugin:end` fires as each one
|
|
1631
|
+
* completes rather than all at once at the end. That ordering drives the CLI's
|
|
1632
|
+
* `Plugins N/M` counter.
|
|
1633
|
+
*
|
|
1634
|
+
* When `this.inputNode` is `null`, every entry still gets a `kubb:plugin:end` so
|
|
1635
|
+
* post-plugin listeners (the barrel writer and friends) complete.
|
|
1636
|
+
*/
|
|
1637
|
+
async #runGenerators(entries) {
|
|
1638
|
+
const diagnostics = [];
|
|
1639
|
+
if (entries.length === 0) return diagnostics;
|
|
1640
|
+
if (!this.inputNode) {
|
|
1641
|
+
for (const { plugin, hrStart } of entries) {
|
|
1642
|
+
const duration = getElapsedMs(hrStart);
|
|
1643
|
+
diagnostics.push(Diagnostics.performance({
|
|
1644
|
+
plugin: plugin.name,
|
|
1645
|
+
duration
|
|
1646
|
+
}));
|
|
1647
|
+
await this.#emitPluginEnd({
|
|
1648
|
+
plugin,
|
|
1649
|
+
duration,
|
|
1650
|
+
success: true
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
return diagnostics;
|
|
1654
|
+
}
|
|
1655
|
+
const transforms = this.#transforms;
|
|
1656
|
+
const { schemas, operations } = this.inputNode;
|
|
1657
|
+
const emitsSchemaHook = this.hooks.listenerCount("kubb:generate:schema") > 0;
|
|
1658
|
+
const emitsOperationHook = this.hooks.listenerCount("kubb:generate:operation") > 0;
|
|
1659
|
+
const emitsOperationsHook = this.hooks.listenerCount("kubb:generate:operations") > 0;
|
|
1660
|
+
const allowedSchemaNamesByPlugin = /* @__PURE__ */ new Map();
|
|
1661
|
+
for (const { plugin } of entries) {
|
|
1662
|
+
const { exclude, include, override } = plugin.options;
|
|
1663
|
+
if (!((include?.some(({ type }) => OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false))) continue;
|
|
1664
|
+
const resolver = this.getResolver(plugin.name);
|
|
1665
|
+
const includedOps = operations.filter((operation) => resolver.default.options(operation, {
|
|
1666
|
+
options: plugin.options,
|
|
1667
|
+
exclude,
|
|
1668
|
+
include,
|
|
1669
|
+
override
|
|
1670
|
+
}) !== null);
|
|
1671
|
+
allowedSchemaNamesByPlugin.set(plugin.name, collectUsedSchemaNames(includedOps, schemas));
|
|
1672
|
+
}
|
|
1673
|
+
for (const { plugin, context, hrStart } of entries) {
|
|
1674
|
+
const generatorContext = {
|
|
1675
|
+
...context,
|
|
1676
|
+
resolver: this.getResolver(plugin.name)
|
|
1677
|
+
};
|
|
1678
|
+
const { exclude, include, override } = plugin.options;
|
|
1679
|
+
const optionsAreStatic = !exclude?.length && !include?.length && !override?.length;
|
|
1680
|
+
const allowedSchemaNames = allowedSchemaNamesByPlugin.get(plugin.name) ?? null;
|
|
1681
|
+
let error = null;
|
|
1682
|
+
const resolveForPlugin = (node) => {
|
|
1683
|
+
const transformedNode = transforms.applyTo(plugin.name, node);
|
|
1684
|
+
if (optionsAreStatic) return {
|
|
1685
|
+
transformedNode,
|
|
1686
|
+
options: plugin.options
|
|
1687
|
+
};
|
|
1688
|
+
const options = generatorContext.resolver.default.options(transformedNode, {
|
|
1689
|
+
options: plugin.options,
|
|
1690
|
+
exclude,
|
|
1691
|
+
include,
|
|
1692
|
+
override
|
|
1693
|
+
});
|
|
1694
|
+
if (options === null) return null;
|
|
1695
|
+
return {
|
|
1696
|
+
transformedNode,
|
|
1697
|
+
options
|
|
1698
|
+
};
|
|
1699
|
+
};
|
|
1700
|
+
if (emitsSchemaHook) for (const node of schemas) {
|
|
1701
|
+
if (error) break;
|
|
1702
|
+
try {
|
|
1703
|
+
const resolved = resolveForPlugin(node);
|
|
1704
|
+
if (!resolved) continue;
|
|
1705
|
+
const { transformedNode, options } = resolved;
|
|
1706
|
+
if (allowedSchemaNames !== null && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) continue;
|
|
1707
|
+
await this.hooks.callHook("kubb:generate:schema", transformedNode, {
|
|
1708
|
+
...generatorContext,
|
|
1709
|
+
options
|
|
1710
|
+
});
|
|
1711
|
+
} catch (caughtError) {
|
|
1712
|
+
error = toError(caughtError);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
if (emitsOperationHook) for (const node of operations) {
|
|
1716
|
+
if (error) break;
|
|
1717
|
+
try {
|
|
1718
|
+
const resolved = resolveForPlugin(node);
|
|
1719
|
+
if (!resolved) continue;
|
|
1720
|
+
await this.hooks.callHook("kubb:generate:operation", resolved.transformedNode, {
|
|
1721
|
+
...generatorContext,
|
|
1722
|
+
options: resolved.options
|
|
1723
|
+
});
|
|
1724
|
+
} catch (caughtError) {
|
|
1725
|
+
error = toError(caughtError);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (!error && emitsOperationsHook) try {
|
|
1729
|
+
const ctx = {
|
|
1730
|
+
...generatorContext,
|
|
1731
|
+
options: plugin.options
|
|
1732
|
+
};
|
|
1733
|
+
const pluginOperations = operations.reduce((acc, node) => {
|
|
1734
|
+
const resolved = resolveForPlugin(node);
|
|
1735
|
+
if (resolved) acc.push(resolved.transformedNode);
|
|
1736
|
+
return acc;
|
|
1737
|
+
}, []);
|
|
1738
|
+
await this.hooks.callHook("kubb:generate:operations", pluginOperations, ctx);
|
|
1739
|
+
} catch (caughtError) {
|
|
1740
|
+
error = toError(caughtError);
|
|
1741
|
+
}
|
|
1742
|
+
const duration = getElapsedMs(hrStart);
|
|
1743
|
+
await this.#emitPluginEnd({
|
|
1744
|
+
plugin,
|
|
1745
|
+
duration,
|
|
1746
|
+
success: !error,
|
|
1747
|
+
error: error ?? void 0
|
|
1748
|
+
});
|
|
1749
|
+
if (error) diagnostics.push({
|
|
1750
|
+
...Diagnostics.from(error),
|
|
1751
|
+
plugin: plugin.name
|
|
1752
|
+
});
|
|
1753
|
+
diagnostics.push(Diagnostics.performance({
|
|
1754
|
+
plugin: plugin.name,
|
|
1755
|
+
duration
|
|
1756
|
+
}));
|
|
1757
|
+
}
|
|
1758
|
+
return diagnostics;
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Stores whatever a generator method or `kubb:generate:*` hook returned.
|
|
1762
|
+
*
|
|
1763
|
+
* - An `Array<FileNode>` goes straight into `fileManager` via `upsert`.
|
|
1764
|
+
* - A renderer element runs through `renderer` (the renderer factory, e.g. JSX) and the
|
|
1765
|
+
* produced files go to `fileManager.upsert`.
|
|
1766
|
+
* - A falsy result is treated as a no-op. The generator wrote files itself via
|
|
1767
|
+
* `ctx.upsertFile`.
|
|
1768
|
+
*
|
|
1769
|
+
* Pass `renderer` when the result may be a renderer element. Generators that only return
|
|
1770
|
+
* `Array<FileNode>` do not need one.
|
|
1771
|
+
*/
|
|
1772
|
+
async dispatch({ result, renderer }) {
|
|
1773
|
+
try {
|
|
1774
|
+
var _usingCtx$2 = _usingCtx();
|
|
1775
|
+
if (!result) return;
|
|
1776
|
+
if (Array.isArray(result)) {
|
|
1777
|
+
this.fileManager.upsert(...result);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
if (!renderer) return;
|
|
1781
|
+
const instance = _usingCtx$2.u(renderer());
|
|
1782
|
+
await instance.render(result);
|
|
1783
|
+
this.fileManager.upsert(...instance.files);
|
|
1784
|
+
} catch (_) {
|
|
1785
|
+
_usingCtx$2.e = _;
|
|
1786
|
+
} finally {
|
|
1787
|
+
_usingCtx$2.d();
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Removes every listener the driver added. Listeners attached directly to `hooks` from outside
|
|
1792
|
+
* the driver survive. Called at the end of a build to prevent leaks across repeated builds.
|
|
1793
|
+
*
|
|
1794
|
+
* @internal
|
|
1795
|
+
*/
|
|
1796
|
+
dispose() {
|
|
1797
|
+
for (const unhook of this.#unhooks) unhook();
|
|
1798
|
+
this.#unhooks.length = 0;
|
|
1799
|
+
this.#hookGeneratorPlugins.clear();
|
|
1800
|
+
this.#transforms.dispose();
|
|
1801
|
+
this.#resolvers.clear();
|
|
1802
|
+
this.#defaultResolvers.clear();
|
|
1803
|
+
this.fileManager.dispose();
|
|
1804
|
+
this.inputNode = null;
|
|
1805
|
+
this.#adapterSource = null;
|
|
1806
|
+
}
|
|
1807
|
+
[Symbol.dispose]() {
|
|
1808
|
+
this.dispose();
|
|
1809
|
+
}
|
|
1810
|
+
#getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => createResolver({ pluginName }));
|
|
1811
|
+
/**
|
|
1812
|
+
* Merges `partial` with the plugin's default resolver and stores the result.
|
|
1813
|
+
* Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver`
|
|
1814
|
+
* get the up-to-date resolver without going through `getResolver()`.
|
|
1815
|
+
*/
|
|
1816
|
+
setPluginResolver(pluginName, partial) {
|
|
1817
|
+
const defaultResolver = this.#getDefaultResolver(pluginName);
|
|
1818
|
+
const merged = Resolver.merge(defaultResolver, partial);
|
|
1819
|
+
this.#resolvers.set(pluginName, merged);
|
|
1820
|
+
const plugin = this.plugins.get(pluginName);
|
|
1821
|
+
if (plugin) plugin.resolver = merged;
|
|
1822
|
+
}
|
|
1823
|
+
getResolver(pluginName) {
|
|
1824
|
+
return this.#resolvers.get(pluginName) ?? this.#getDefaultResolver(pluginName);
|
|
1825
|
+
}
|
|
1826
|
+
getContext(plugin) {
|
|
1827
|
+
const driver = this;
|
|
1828
|
+
const report = (diagnostic) => {
|
|
1829
|
+
Diagnostics.report({
|
|
1830
|
+
...diagnostic,
|
|
1831
|
+
plugin: plugin.name
|
|
1832
|
+
});
|
|
1833
|
+
};
|
|
1834
|
+
return {
|
|
1835
|
+
config: driver.config,
|
|
1836
|
+
get root() {
|
|
1837
|
+
return resolve(driver.config.root, driver.config.output.path);
|
|
1838
|
+
},
|
|
1839
|
+
hooks: driver.hooks,
|
|
1840
|
+
plugin,
|
|
1841
|
+
getPlugin: driver.getPlugin.bind(driver),
|
|
1842
|
+
requirePlugin: ((name) => driver.requirePlugin(name, { requiredBy: plugin.name })),
|
|
1843
|
+
getResolver: driver.getResolver.bind(driver),
|
|
1844
|
+
driver,
|
|
1845
|
+
addFile: async (...files) => {
|
|
1846
|
+
driver.fileManager.add(...files);
|
|
1847
|
+
},
|
|
1848
|
+
upsertFile: async (...files) => {
|
|
1849
|
+
driver.fileManager.upsert(...files);
|
|
1850
|
+
},
|
|
1851
|
+
get meta() {
|
|
1852
|
+
return driver.inputNode?.meta ?? {
|
|
1853
|
+
circularNames: [],
|
|
1854
|
+
enumNames: []
|
|
1855
|
+
};
|
|
1856
|
+
},
|
|
1857
|
+
get adapter() {
|
|
1858
|
+
return driver.adapter;
|
|
1859
|
+
},
|
|
1860
|
+
get resolver() {
|
|
1861
|
+
return driver.getResolver(plugin.name);
|
|
1862
|
+
},
|
|
1863
|
+
warn(message) {
|
|
1864
|
+
report({
|
|
1865
|
+
code: Diagnostics.code.pluginWarning,
|
|
1866
|
+
severity: "warning",
|
|
1867
|
+
message
|
|
1868
|
+
});
|
|
1869
|
+
},
|
|
1870
|
+
error(error) {
|
|
1871
|
+
const cause = typeof error === "string" ? void 0 : error;
|
|
1872
|
+
report({
|
|
1873
|
+
code: Diagnostics.code.pluginFailed,
|
|
1874
|
+
severity: "error",
|
|
1875
|
+
message: typeof error === "string" ? error : error.message,
|
|
1876
|
+
cause
|
|
1877
|
+
});
|
|
1878
|
+
},
|
|
1879
|
+
info(message) {
|
|
1880
|
+
report({
|
|
1881
|
+
code: Diagnostics.code.pluginInfo,
|
|
1882
|
+
severity: "info",
|
|
1883
|
+
message
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
getPlugin(pluginName) {
|
|
1889
|
+
return this.plugins.get(pluginName);
|
|
1890
|
+
}
|
|
1891
|
+
requirePlugin(pluginName, context) {
|
|
1892
|
+
const plugin = this.getPlugin(pluginName);
|
|
1893
|
+
if (plugin) return plugin;
|
|
1894
|
+
const requiredBy = context?.requiredBy;
|
|
1895
|
+
const by = requiredBy ? ` by "${requiredBy}"` : "";
|
|
1896
|
+
const help = requiredBy ? ` (required by "${requiredBy}")` : "";
|
|
1897
|
+
throw new Diagnostics.Error({
|
|
1898
|
+
code: Diagnostics.code.pluginNotFound,
|
|
1899
|
+
severity: "error",
|
|
1900
|
+
message: `Plugin "${pluginName}" is required${by} but not found. Make sure it is included in your Kubb config.`,
|
|
1901
|
+
help: `Add "${pluginName}" to the \`plugins\` array in kubb.config.ts${help}, or remove the dependency on it.`,
|
|
1902
|
+
location: { kind: "config" }
|
|
1903
|
+
});
|
|
702
1904
|
}
|
|
703
1905
|
};
|
|
704
1906
|
//#endregion
|
|
705
1907
|
//#region src/createStorage.ts
|
|
706
1908
|
/**
|
|
707
|
-
*
|
|
1909
|
+
* Defines a custom storage backend. The builder receives user options and
|
|
1910
|
+
* returns a `Storage` implementation. Kubb ships with filesystem and in-memory
|
|
1911
|
+
* storages. A custom backend writes generated files elsewhere, such as cloud
|
|
1912
|
+
* storage or a database.
|
|
708
1913
|
*
|
|
709
|
-
*
|
|
710
|
-
* Kubb provides filesystem and in-memory implementations out of the box.
|
|
711
|
-
*
|
|
712
|
-
* @note Call the returned factory with optional options to instantiate the storage adapter.
|
|
713
|
-
*
|
|
714
|
-
* @example
|
|
1914
|
+
* @example In-memory storage (the built-in implementation)
|
|
715
1915
|
* ```ts
|
|
716
1916
|
* import { createStorage } from '@kubb/core'
|
|
717
1917
|
*
|
|
718
1918
|
* export const memoryStorage = createStorage(() => {
|
|
719
1919
|
* const store = new Map<string, string>()
|
|
1920
|
+
*
|
|
720
1921
|
* return {
|
|
721
1922
|
* name: 'memory',
|
|
722
|
-
* async hasItem(key) {
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* async
|
|
1923
|
+
* async hasItem(key) {
|
|
1924
|
+
* return store.has(key)
|
|
1925
|
+
* },
|
|
1926
|
+
* async getItem(key) {
|
|
1927
|
+
* return store.get(key) ?? null
|
|
1928
|
+
* },
|
|
1929
|
+
* async setItem(key, value) {
|
|
1930
|
+
* store.set(key, value)
|
|
1931
|
+
* },
|
|
1932
|
+
* async removeItem(key) {
|
|
1933
|
+
* store.delete(key)
|
|
1934
|
+
* },
|
|
726
1935
|
* async getKeys(base) {
|
|
727
1936
|
* const keys = [...store.keys()]
|
|
728
1937
|
* return base ? keys.filter((k) => k.startsWith(base)) : keys
|
|
729
1938
|
* },
|
|
730
|
-
* async clear(base) {
|
|
1939
|
+
* async clear(base) {
|
|
1940
|
+
* if (!base) store.clear()
|
|
1941
|
+
* },
|
|
731
1942
|
* }
|
|
732
1943
|
* })
|
|
733
|
-
*
|
|
734
|
-
* // Instantiate:
|
|
735
|
-
* const storage = memoryStorage()
|
|
736
1944
|
* ```
|
|
737
1945
|
*/
|
|
738
1946
|
function createStorage(build) {
|
|
@@ -740,6 +1948,29 @@ function createStorage(build) {
|
|
|
740
1948
|
}
|
|
741
1949
|
//#endregion
|
|
742
1950
|
//#region src/storages/fsStorage.ts
|
|
1951
|
+
const WRITE_CONCURRENCY = 50;
|
|
1952
|
+
function createLimiter(concurrency) {
|
|
1953
|
+
let active = 0;
|
|
1954
|
+
const queue = [];
|
|
1955
|
+
function next() {
|
|
1956
|
+
if (active >= concurrency) return;
|
|
1957
|
+
const run = queue.shift();
|
|
1958
|
+
if (!run) return;
|
|
1959
|
+
active++;
|
|
1960
|
+
run();
|
|
1961
|
+
}
|
|
1962
|
+
return function limit(task) {
|
|
1963
|
+
return new Promise((resolve, reject) => {
|
|
1964
|
+
queue.push(() => {
|
|
1965
|
+
task().then(resolve, reject).finally(() => {
|
|
1966
|
+
active--;
|
|
1967
|
+
next();
|
|
1968
|
+
});
|
|
1969
|
+
});
|
|
1970
|
+
next();
|
|
1971
|
+
});
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
743
1974
|
/**
|
|
744
1975
|
* Built-in filesystem storage driver.
|
|
745
1976
|
*
|
|
@@ -747,11 +1978,13 @@ function createStorage(build) {
|
|
|
747
1978
|
* Keys are resolved against `process.cwd()`, so root-relative paths such as
|
|
748
1979
|
* `src/gen/api/getPets.ts` are written to the correct location without extra configuration.
|
|
749
1980
|
*
|
|
750
|
-
*
|
|
751
|
-
* -
|
|
752
|
-
* -
|
|
753
|
-
* -
|
|
754
|
-
* -
|
|
1981
|
+
* Writes are deduplicated and directory-safe:
|
|
1982
|
+
* - leading and trailing whitespace is trimmed before writing
|
|
1983
|
+
* - the write is skipped when the file content is already identical
|
|
1984
|
+
* - missing parent directories are created automatically
|
|
1985
|
+
* - Bun's native file API is used when running under Bun
|
|
1986
|
+
* - concurrent `setItem` calls are capped at {@link WRITE_CONCURRENCY} in flight, so a caller
|
|
1987
|
+
* can fire every file's write without pacing itself
|
|
755
1988
|
*
|
|
756
1989
|
* @example
|
|
757
1990
|
* ```ts
|
|
@@ -759,630 +1992,536 @@ function createStorage(build) {
|
|
|
759
1992
|
* import { defineConfig } from 'kubb'
|
|
760
1993
|
*
|
|
761
1994
|
* export default defineConfig({
|
|
762
|
-
* input:
|
|
1995
|
+
* input: './petStore.yaml',
|
|
763
1996
|
* output: { path: './src/gen' },
|
|
764
1997
|
* storage: fsStorage(),
|
|
765
1998
|
* })
|
|
766
1999
|
* ```
|
|
767
2000
|
*/
|
|
768
|
-
const fsStorage = createStorage(() =>
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
return true;
|
|
774
|
-
} catch (_error) {
|
|
775
|
-
return false;
|
|
776
|
-
}
|
|
777
|
-
},
|
|
778
|
-
async getItem(key) {
|
|
779
|
-
try {
|
|
780
|
-
return await readFile(resolve(key), "utf8");
|
|
781
|
-
} catch (_error) {
|
|
782
|
-
return null;
|
|
783
|
-
}
|
|
784
|
-
},
|
|
785
|
-
async setItem(key, value) {
|
|
786
|
-
await write(resolve(key), value, { sanity: false });
|
|
787
|
-
},
|
|
788
|
-
async removeItem(key) {
|
|
789
|
-
await rm(resolve(key), { force: true });
|
|
790
|
-
},
|
|
791
|
-
async getKeys(base) {
|
|
792
|
-
const keys = [];
|
|
793
|
-
const resolvedBase = resolve(base ?? process.cwd());
|
|
794
|
-
async function walk(dir, prefix) {
|
|
795
|
-
let entries;
|
|
2001
|
+
const fsStorage = createStorage(() => {
|
|
2002
|
+
const limit = createLimiter(WRITE_CONCURRENCY);
|
|
2003
|
+
return {
|
|
2004
|
+
name: "fs",
|
|
2005
|
+
async hasItem(key) {
|
|
796
2006
|
try {
|
|
797
|
-
|
|
2007
|
+
await access(resolve(key));
|
|
2008
|
+
return true;
|
|
798
2009
|
} catch (_error) {
|
|
799
|
-
return;
|
|
2010
|
+
return false;
|
|
800
2011
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
2012
|
+
},
|
|
2013
|
+
async getItem(key) {
|
|
2014
|
+
try {
|
|
2015
|
+
return await readFile(resolve(key), "utf8");
|
|
2016
|
+
} catch (_error) {
|
|
2017
|
+
return null;
|
|
805
2018
|
}
|
|
2019
|
+
},
|
|
2020
|
+
async setItem(key, value) {
|
|
2021
|
+
await limit(() => write(resolve(key), value, { sanity: false }));
|
|
2022
|
+
},
|
|
2023
|
+
async removeItem(key) {
|
|
2024
|
+
await rm(resolve(key), { force: true });
|
|
2025
|
+
},
|
|
2026
|
+
async getKeys(base) {
|
|
2027
|
+
const resolvedBase = resolve(base ?? process.cwd());
|
|
2028
|
+
const keys = [];
|
|
2029
|
+
try {
|
|
2030
|
+
for await (const entry of glob("**/*", {
|
|
2031
|
+
cwd: resolvedBase,
|
|
2032
|
+
withFileTypes: true
|
|
2033
|
+
})) if (entry.isFile()) keys.push(toPosixPath(relative(resolvedBase, join(entry.parentPath, entry.name))));
|
|
2034
|
+
} catch (_error) {}
|
|
2035
|
+
return keys;
|
|
2036
|
+
},
|
|
2037
|
+
async clear(base) {
|
|
2038
|
+
if (!base) return;
|
|
2039
|
+
await clean(resolve(base));
|
|
806
2040
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
},
|
|
810
|
-
async clear(base) {
|
|
811
|
-
if (!base) return;
|
|
812
|
-
await clean(resolve(base));
|
|
813
|
-
}
|
|
814
|
-
}));
|
|
2041
|
+
};
|
|
2042
|
+
});
|
|
815
2043
|
//#endregion
|
|
816
2044
|
//#region src/createKubb.ts
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const config = {
|
|
2045
|
+
function resolveConfig(userConfig) {
|
|
2046
|
+
return {
|
|
820
2047
|
...userConfig,
|
|
821
2048
|
root: userConfig.root || process.cwd(),
|
|
822
2049
|
parsers: userConfig.parsers ?? [],
|
|
823
|
-
adapter: userConfig.adapter,
|
|
824
2050
|
output: {
|
|
825
2051
|
format: false,
|
|
826
2052
|
lint: false,
|
|
827
|
-
|
|
828
|
-
defaultBanner: DEFAULT_BANNER,
|
|
2053
|
+
defaultBanner: "simple",
|
|
829
2054
|
...userConfig.output
|
|
830
2055
|
},
|
|
831
2056
|
storage: userConfig.storage ?? fsStorage(),
|
|
832
|
-
|
|
833
|
-
studioUrl: DEFAULT_STUDIO_URL,
|
|
834
|
-
...typeof userConfig.devtools === "boolean" ? {} : userConfig.devtools
|
|
835
|
-
} : void 0,
|
|
2057
|
+
reporters: userConfig.reporters ?? [],
|
|
836
2058
|
plugins: userConfig.plugins ?? []
|
|
837
2059
|
};
|
|
838
|
-
const driver = new PluginDriver(config, { hooks });
|
|
839
|
-
const sources = /* @__PURE__ */ new Map();
|
|
840
|
-
const diagnosticInfo = getDiagnosticInfo();
|
|
841
|
-
await hooks.emit("kubb:debug", {
|
|
842
|
-
date: /* @__PURE__ */ new Date(),
|
|
843
|
-
logs: [
|
|
844
|
-
"Configuration:",
|
|
845
|
-
` • Name: ${userConfig.name || "unnamed"}`,
|
|
846
|
-
` • Root: ${userConfig.root || process.cwd()}`,
|
|
847
|
-
` • Output: ${userConfig.output?.path || "not specified"}`,
|
|
848
|
-
` • Plugins: ${userConfig.plugins?.length || 0}`,
|
|
849
|
-
"Output Settings:",
|
|
850
|
-
` • Storage: ${config.storage.name}`,
|
|
851
|
-
` • Formatter: ${userConfig.output?.format || "none"}`,
|
|
852
|
-
` • Linter: ${userConfig.output?.lint || "none"}`,
|
|
853
|
-
"Environment:",
|
|
854
|
-
Object.entries(diagnosticInfo).map(([key, value]) => ` • ${key}: ${value}`).join("\n")
|
|
855
|
-
]
|
|
856
|
-
});
|
|
857
|
-
try {
|
|
858
|
-
if (isInputPath(userConfig) && !new URLPath(userConfig.input.path).isURL) {
|
|
859
|
-
await exists(userConfig.input.path);
|
|
860
|
-
await hooks.emit("kubb:debug", {
|
|
861
|
-
date: /* @__PURE__ */ new Date(),
|
|
862
|
-
logs: [`✓ Input file validated: ${userConfig.input.path}`]
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
} catch (caughtError) {
|
|
866
|
-
if (isInputPath(userConfig)) {
|
|
867
|
-
const error = caughtError;
|
|
868
|
-
throw new Error(`Cannot read file/URL defined in \`input.path\` or set with \`kubb generate PATH\` in the CLI of your Kubb config ${userConfig.input.path}`, { cause: error });
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
if (config.output.clean) {
|
|
872
|
-
await hooks.emit("kubb:debug", {
|
|
873
|
-
date: /* @__PURE__ */ new Date(),
|
|
874
|
-
logs: ["Cleaning output directories", ` • Output: ${config.output.path}`]
|
|
875
|
-
});
|
|
876
|
-
await config.storage.clear(resolve(config.root, config.output.path));
|
|
877
|
-
}
|
|
878
|
-
function registerMiddlewareHook(event, middlewareHooks) {
|
|
879
|
-
const handler = middlewareHooks[event];
|
|
880
|
-
if (handler) hooks.on(event, handler);
|
|
881
|
-
}
|
|
882
|
-
for (const middleware of config.middleware ?? []) for (const event of Object.keys(middleware.hooks)) registerMiddlewareHook(event, middleware.hooks);
|
|
883
|
-
if (config.adapter) {
|
|
884
|
-
const source = inputToAdapterSource(config);
|
|
885
|
-
await hooks.emit("kubb:debug", {
|
|
886
|
-
date: /* @__PURE__ */ new Date(),
|
|
887
|
-
logs: [`Running adapter: ${config.adapter.name}`]
|
|
888
|
-
});
|
|
889
|
-
driver.adapter = config.adapter;
|
|
890
|
-
driver.inputNode = await config.adapter.parse(source);
|
|
891
|
-
await hooks.emit("kubb:debug", {
|
|
892
|
-
date: /* @__PURE__ */ new Date(),
|
|
893
|
-
logs: [
|
|
894
|
-
`✓ Adapter '${config.adapter.name}' resolved InputNode`,
|
|
895
|
-
` • Schemas: ${driver.inputNode.schemas.length}`,
|
|
896
|
-
` • Operations: ${driver.inputNode.operations.length}`
|
|
897
|
-
]
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
return {
|
|
901
|
-
config,
|
|
902
|
-
hooks,
|
|
903
|
-
driver,
|
|
904
|
-
sources
|
|
905
|
-
};
|
|
906
2060
|
}
|
|
907
2061
|
/**
|
|
908
|
-
*
|
|
909
|
-
*
|
|
2062
|
+
* Kubb code-generation instance bound to a single config entry. Resolves the user
|
|
2063
|
+
* config in the constructor, so `config` is available right away, and shares `hooks`,
|
|
2064
|
+
* `storage`, and `driver` across the `setup → build` lifecycle.
|
|
910
2065
|
*
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
2066
|
+
* `createKubb` takes a plain config object (the shape `defineConfig` produces),
|
|
2067
|
+
* not a fluent builder.
|
|
2068
|
+
*
|
|
2069
|
+
* Attach hook listeners to `.hooks` before calling `setup()` or `build()`.
|
|
2070
|
+
*
|
|
2071
|
+
* @example
|
|
2072
|
+
* ```ts
|
|
2073
|
+
* const kubb = createKubb(userConfig)
|
|
2074
|
+
* kubb.hooks.hook('kubb:plugin:end', ({ plugin, duration }) => console.log(plugin.name, duration))
|
|
2075
|
+
* const { files, diagnostics } = await kubb.safeBuild()
|
|
2076
|
+
* ```
|
|
916
2077
|
*/
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
"
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node;
|
|
950
|
-
if (allowedSchemaNames !== void 0 && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) return;
|
|
951
|
-
const options = resolver.resolveOptions(transformedNode, {
|
|
952
|
-
options: plugin.options,
|
|
953
|
-
exclude,
|
|
954
|
-
include,
|
|
955
|
-
override
|
|
956
|
-
});
|
|
957
|
-
if (options === null) return;
|
|
958
|
-
const ctx = {
|
|
959
|
-
...generatorContext,
|
|
960
|
-
options
|
|
961
|
-
};
|
|
962
|
-
for (const gen of generators) {
|
|
963
|
-
if (!gen.schema) continue;
|
|
964
|
-
await applyHookResult(await gen.schema(transformedNode, ctx), driver, resolveRenderer(gen));
|
|
965
|
-
}
|
|
966
|
-
await driver.hooks.emit("kubb:generate:schema", transformedNode, ctx);
|
|
967
|
-
},
|
|
968
|
-
async operation(node) {
|
|
969
|
-
const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node;
|
|
970
|
-
const options = resolver.resolveOptions(transformedNode, {
|
|
971
|
-
options: plugin.options,
|
|
972
|
-
exclude,
|
|
973
|
-
include,
|
|
974
|
-
override
|
|
2078
|
+
var Kubb = class {
|
|
2079
|
+
hooks;
|
|
2080
|
+
config;
|
|
2081
|
+
#driver = null;
|
|
2082
|
+
#storage = null;
|
|
2083
|
+
constructor(userConfig, options = {}) {
|
|
2084
|
+
this.config = resolveConfig(userConfig);
|
|
2085
|
+
this.hooks = options.hooks ?? new Hookable();
|
|
2086
|
+
}
|
|
2087
|
+
get storage() {
|
|
2088
|
+
if (!this.#storage) throw new Error("[kubb] setup() must be called before accessing storage");
|
|
2089
|
+
return this.#storage;
|
|
2090
|
+
}
|
|
2091
|
+
get driver() {
|
|
2092
|
+
if (!this.#driver) throw new Error("[kubb] setup() must be called before accessing driver");
|
|
2093
|
+
return this.#driver;
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Initializes the driver and storage. `build()` calls this automatically.
|
|
2097
|
+
*/
|
|
2098
|
+
async setup() {
|
|
2099
|
+
const config = this.config;
|
|
2100
|
+
const driver = new KubbDriver(config, { hooks: this.hooks });
|
|
2101
|
+
this.hooks.setMaxListeners(Math.max(10, config.plugins.length * 4));
|
|
2102
|
+
if (config.output.clean) {
|
|
2103
|
+
const cleanPath = resolve(config.root, config.output.path);
|
|
2104
|
+
if (isPathInside(config.root, cleanPath)) throw new Diagnostics.Error({
|
|
2105
|
+
code: Diagnostics.code.cleanRoot,
|
|
2106
|
+
severity: "error",
|
|
2107
|
+
message: `output.clean cannot delete "${cleanPath}" because it is the project root or a parent of it.`,
|
|
2108
|
+
help: "Point `output.path` at a subdirectory such as `./src/gen` so clean only removes generated code.",
|
|
2109
|
+
location: { kind: "config" }
|
|
975
2110
|
});
|
|
976
|
-
|
|
977
|
-
collectedOperations.push(transformedNode);
|
|
978
|
-
const ctx = {
|
|
979
|
-
...generatorContext,
|
|
980
|
-
options
|
|
981
|
-
};
|
|
982
|
-
for (const gen of generators) {
|
|
983
|
-
if (!gen.operation) continue;
|
|
984
|
-
await applyHookResult(await gen.operation(transformedNode, ctx), driver, resolveRenderer(gen));
|
|
985
|
-
}
|
|
986
|
-
await driver.hooks.emit("kubb:generate:operation", transformedNode, ctx);
|
|
987
|
-
}
|
|
2111
|
+
await config.storage.clear(cleanPath);
|
|
988
2112
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
2113
|
+
await driver.setup();
|
|
2114
|
+
this.#driver = driver;
|
|
2115
|
+
this.#storage = config.storage;
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Runs the full pipeline and throws on any plugin error.
|
|
2119
|
+
* Automatically calls `setup()` if needed.
|
|
2120
|
+
*/
|
|
2121
|
+
async build() {
|
|
2122
|
+
const out = await this.safeBuild();
|
|
2123
|
+
if (Diagnostics.hasError(out.diagnostics)) {
|
|
2124
|
+
const errors = out.diagnostics.filter(Diagnostics.isProblem).filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.cause ?? new Diagnostics.Error(diagnostic));
|
|
2125
|
+
throw new BuildError(`Build failed with ${errors.length} ${errors.length === 1 ? "error" : "errors"}`, { errors });
|
|
998
2126
|
}
|
|
999
|
-
|
|
2127
|
+
return out;
|
|
1000
2128
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
await hooks.emit("kubb:plugin:start", { plugin });
|
|
1025
|
-
await hooks.emit("kubb:debug", {
|
|
1026
|
-
date: timestamp,
|
|
1027
|
-
logs: ["Starting plugin...", ` • Plugin Name: ${plugin.name}`]
|
|
1028
|
-
});
|
|
1029
|
-
if (plugin.generators?.length || driver.hasRegisteredGenerators(plugin.name)) await runPluginAstHooks(plugin, context);
|
|
1030
|
-
const duration = getElapsedMs(hrStart);
|
|
1031
|
-
pluginTimings.set(plugin.name, duration);
|
|
1032
|
-
await hooks.emit("kubb:plugin:end", {
|
|
1033
|
-
plugin,
|
|
1034
|
-
duration,
|
|
1035
|
-
success: true,
|
|
1036
|
-
config,
|
|
1037
|
-
get files() {
|
|
1038
|
-
return driver.fileManager.files;
|
|
1039
|
-
},
|
|
1040
|
-
upsertFile: (...files) => driver.fileManager.upsert(...files)
|
|
1041
|
-
});
|
|
1042
|
-
await hooks.emit("kubb:debug", {
|
|
1043
|
-
date: /* @__PURE__ */ new Date(),
|
|
1044
|
-
logs: [`✓ Plugin started successfully (${formatMs(duration)})`]
|
|
1045
|
-
});
|
|
1046
|
-
} catch (caughtError) {
|
|
1047
|
-
const error = caughtError;
|
|
1048
|
-
const errorTimestamp = /* @__PURE__ */ new Date();
|
|
1049
|
-
const duration = getElapsedMs(hrStart);
|
|
1050
|
-
await hooks.emit("kubb:plugin:end", {
|
|
1051
|
-
plugin,
|
|
1052
|
-
duration,
|
|
1053
|
-
success: false,
|
|
1054
|
-
error,
|
|
1055
|
-
config,
|
|
1056
|
-
get files() {
|
|
1057
|
-
return driver.fileManager.files;
|
|
1058
|
-
},
|
|
1059
|
-
upsertFile: (...files) => driver.fileManager.upsert(...files)
|
|
1060
|
-
});
|
|
1061
|
-
await hooks.emit("kubb:debug", {
|
|
1062
|
-
date: errorTimestamp,
|
|
1063
|
-
logs: [
|
|
1064
|
-
"✗ Plugin start failed",
|
|
1065
|
-
` • Plugin Name: ${plugin.name}`,
|
|
1066
|
-
` • Error: ${error.constructor.name} - ${error.message}`,
|
|
1067
|
-
" • Stack Trace:",
|
|
1068
|
-
error.stack || "No stack trace available"
|
|
1069
|
-
]
|
|
1070
|
-
});
|
|
1071
|
-
failedPlugins.add({
|
|
1072
|
-
plugin,
|
|
1073
|
-
error
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Runs the full pipeline and captures errors in `BuildOutput` instead of throwing.
|
|
2131
|
+
* Automatically calls `setup()` if needed. This is the canonical call: it never throws on
|
|
2132
|
+
* plugin errors, so callers stay in control of how failures surface.
|
|
2133
|
+
*/
|
|
2134
|
+
async safeBuild() {
|
|
2135
|
+
try {
|
|
2136
|
+
var _usingCtx$1 = _usingCtx();
|
|
2137
|
+
if (!this.#driver) await this.setup();
|
|
2138
|
+
const self = _usingCtx$1.u(this);
|
|
2139
|
+
const driver = self.driver;
|
|
2140
|
+
const storage = self.storage;
|
|
2141
|
+
const { diagnostics } = await driver.run();
|
|
2142
|
+
return {
|
|
2143
|
+
diagnostics,
|
|
2144
|
+
files: driver.fileManager.files,
|
|
2145
|
+
driver,
|
|
2146
|
+
storage
|
|
2147
|
+
};
|
|
2148
|
+
} catch (_) {
|
|
2149
|
+
_usingCtx$1.e = _;
|
|
2150
|
+
} finally {
|
|
2151
|
+
_usingCtx$1.d();
|
|
1076
2152
|
}
|
|
1077
|
-
await hooks.emit("kubb:plugins:end", {
|
|
1078
|
-
config,
|
|
1079
|
-
get files() {
|
|
1080
|
-
return driver.fileManager.files;
|
|
1081
|
-
},
|
|
1082
|
-
upsertFile: (...files) => driver.fileManager.upsert(...files)
|
|
1083
|
-
});
|
|
1084
|
-
const files = driver.fileManager.files;
|
|
1085
|
-
const parsersMap = /* @__PURE__ */ new Map();
|
|
1086
|
-
for (const parser of config.parsers) if (parser.extNames) for (const extname of parser.extNames) parsersMap.set(extname, parser);
|
|
1087
|
-
const fileProcessor = new FileProcessor();
|
|
1088
|
-
await hooks.emit("kubb:debug", {
|
|
1089
|
-
date: /* @__PURE__ */ new Date(),
|
|
1090
|
-
logs: [`Writing ${files.length} files...`]
|
|
1091
|
-
});
|
|
1092
|
-
await fileProcessor.run(files, {
|
|
1093
|
-
parsers: parsersMap,
|
|
1094
|
-
mode: "parallel",
|
|
1095
|
-
extension: config.output.extension,
|
|
1096
|
-
onStart: async (processingFiles) => {
|
|
1097
|
-
await hooks.emit("kubb:files:processing:start", { files: processingFiles });
|
|
1098
|
-
},
|
|
1099
|
-
onUpdate: async ({ file, source, processed, total, percentage }) => {
|
|
1100
|
-
await hooks.emit("kubb:file:processing:update", {
|
|
1101
|
-
file,
|
|
1102
|
-
source,
|
|
1103
|
-
processed,
|
|
1104
|
-
total,
|
|
1105
|
-
percentage,
|
|
1106
|
-
config
|
|
1107
|
-
});
|
|
1108
|
-
if (source) {
|
|
1109
|
-
await config.storage.setItem(file.path, source);
|
|
1110
|
-
sources.set(file.path, source);
|
|
1111
|
-
}
|
|
1112
|
-
},
|
|
1113
|
-
onEnd: async (processedFiles) => {
|
|
1114
|
-
await hooks.emit("kubb:files:processing:end", { files: processedFiles });
|
|
1115
|
-
await hooks.emit("kubb:debug", {
|
|
1116
|
-
date: /* @__PURE__ */ new Date(),
|
|
1117
|
-
logs: [`✓ File write process completed for ${processedFiles.length} files`]
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
});
|
|
1121
|
-
await hooks.emit("kubb:build:end", {
|
|
1122
|
-
files,
|
|
1123
|
-
config,
|
|
1124
|
-
outputDir: resolve(config.root, config.output.path)
|
|
1125
|
-
});
|
|
1126
|
-
return {
|
|
1127
|
-
failedPlugins,
|
|
1128
|
-
files,
|
|
1129
|
-
driver,
|
|
1130
|
-
pluginTimings,
|
|
1131
|
-
sources
|
|
1132
|
-
};
|
|
1133
|
-
} catch (error) {
|
|
1134
|
-
return {
|
|
1135
|
-
failedPlugins,
|
|
1136
|
-
files: [],
|
|
1137
|
-
driver,
|
|
1138
|
-
pluginTimings,
|
|
1139
|
-
error,
|
|
1140
|
-
sources
|
|
1141
|
-
};
|
|
1142
|
-
} finally {
|
|
1143
|
-
driver.dispose();
|
|
1144
2153
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(setupResult);
|
|
1148
|
-
if (error) throw error;
|
|
1149
|
-
if (failedPlugins.size > 0) {
|
|
1150
|
-
const errors = [...failedPlugins].map(({ error }) => error);
|
|
1151
|
-
throw new BuildError(`Build Error with ${failedPlugins.size} failed plugins`, { errors });
|
|
2154
|
+
dispose() {
|
|
2155
|
+
this.#driver?.dispose();
|
|
1152
2156
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
pluginTimings,
|
|
1158
|
-
error: void 0,
|
|
1159
|
-
sources
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
/**
|
|
1163
|
-
* Returns a snapshot of the current runtime environment.
|
|
1164
|
-
*
|
|
1165
|
-
* Useful for attaching context to debug logs and error reports so that
|
|
1166
|
-
* issues can be reproduced without manual information gathering.
|
|
1167
|
-
*/
|
|
1168
|
-
function getDiagnosticInfo() {
|
|
1169
|
-
return {
|
|
1170
|
-
nodeVersion: version,
|
|
1171
|
-
KubbVersion: version$1,
|
|
1172
|
-
platform: process.platform,
|
|
1173
|
-
arch: process.arch,
|
|
1174
|
-
cwd: process.cwd()
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
function isInputPath(config) {
|
|
1178
|
-
return typeof config?.input === "object" && config.input !== null && "path" in config.input;
|
|
1179
|
-
}
|
|
1180
|
-
function inputToAdapterSource(config) {
|
|
1181
|
-
const input = config.input;
|
|
1182
|
-
if (!input) throw new Error("[kubb] input is required when using an adapter. Provide input.path or input.data in your config.");
|
|
1183
|
-
if ("data" in input) return {
|
|
1184
|
-
type: "data",
|
|
1185
|
-
data: input.data
|
|
1186
|
-
};
|
|
1187
|
-
if (new URLPath(input.path).isURL) return {
|
|
1188
|
-
type: "path",
|
|
1189
|
-
path: input.path
|
|
1190
|
-
};
|
|
1191
|
-
return {
|
|
1192
|
-
type: "path",
|
|
1193
|
-
path: resolve(config.root, input.path)
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
2157
|
+
[Symbol.dispose]() {
|
|
2158
|
+
this.dispose();
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
1196
2161
|
/**
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
* Accepts a user-facing config shape and resolves it to a full {@link Config} during
|
|
1200
|
-
* `setup()`. The instance then holds shared state (`hooks`, `sources`, `driver`, `config`)
|
|
1201
|
-
* across the `setup → build` lifecycle. Attach event listeners to `kubb.hooks` before
|
|
1202
|
-
* calling `setup()` or `build()`.
|
|
2162
|
+
* Constructs a {@link Kubb} build orchestrator from a user config. Equivalent
|
|
2163
|
+
* to `new Kubb(userConfig, options)` and the canonical public entry point.
|
|
1203
2164
|
*
|
|
1204
2165
|
* @example
|
|
1205
2166
|
* ```ts
|
|
1206
|
-
*
|
|
2167
|
+
* import { createKubb } from '@kubb/core'
|
|
2168
|
+
* import { adapterOas } from '@kubb/adapter-oas'
|
|
2169
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
1207
2170
|
*
|
|
1208
|
-
* kubb
|
|
1209
|
-
*
|
|
2171
|
+
* const kubb = createKubb({
|
|
2172
|
+
* input: './petStore.yaml',
|
|
2173
|
+
* output: { path: './src/gen' },
|
|
2174
|
+
* adapter: adapterOas(),
|
|
2175
|
+
* plugins: [pluginTs()],
|
|
1210
2176
|
* })
|
|
1211
2177
|
*
|
|
1212
|
-
*
|
|
2178
|
+
* await kubb.build()
|
|
1213
2179
|
* ```
|
|
1214
2180
|
*/
|
|
1215
2181
|
function createKubb(userConfig, options = {}) {
|
|
1216
|
-
|
|
1217
|
-
let setupResult;
|
|
1218
|
-
const instance = {
|
|
1219
|
-
get hooks() {
|
|
1220
|
-
return hooks;
|
|
1221
|
-
},
|
|
1222
|
-
get sources() {
|
|
1223
|
-
return setupResult?.sources ?? /* @__PURE__ */ new Map();
|
|
1224
|
-
},
|
|
1225
|
-
get driver() {
|
|
1226
|
-
return setupResult?.driver;
|
|
1227
|
-
},
|
|
1228
|
-
get config() {
|
|
1229
|
-
return setupResult?.config;
|
|
1230
|
-
},
|
|
1231
|
-
async setup() {
|
|
1232
|
-
setupResult = await setup(userConfig, { hooks });
|
|
1233
|
-
},
|
|
1234
|
-
async build() {
|
|
1235
|
-
if (!setupResult) await instance.setup();
|
|
1236
|
-
return build(setupResult);
|
|
1237
|
-
},
|
|
1238
|
-
async safeBuild() {
|
|
1239
|
-
if (!setupResult) await instance.setup();
|
|
1240
|
-
return safeBuild(setupResult);
|
|
1241
|
-
}
|
|
1242
|
-
};
|
|
1243
|
-
return instance;
|
|
2182
|
+
return new Kubb(userConfig, options);
|
|
1244
2183
|
}
|
|
1245
2184
|
//#endregion
|
|
1246
|
-
//#region src/
|
|
2185
|
+
//#region src/createReporter.ts
|
|
1247
2186
|
/**
|
|
1248
|
-
*
|
|
2187
|
+
* Numeric log-level thresholds used internally to compare verbosity.
|
|
1249
2188
|
*
|
|
1250
|
-
*
|
|
1251
|
-
|
|
1252
|
-
|
|
2189
|
+
* Higher numbers are more verbose.
|
|
2190
|
+
*/
|
|
2191
|
+
const logLevel = {
|
|
2192
|
+
silent: Number.NEGATIVE_INFINITY,
|
|
2193
|
+
error: 0,
|
|
2194
|
+
warn: 1,
|
|
2195
|
+
info: 3,
|
|
2196
|
+
verbose: 4
|
|
2197
|
+
};
|
|
2198
|
+
/**
|
|
2199
|
+
* Defines a reporter. The returned reporter buffers each value `report` returns in order and, when
|
|
2200
|
+
* the definition has a `drain`, hands the array to `drain` once and then clears it. Wiring the
|
|
2201
|
+
* reporter onto the run's hooks is the host's job, so the reporter only ever deals with a
|
|
2202
|
+
* {@link GenerationResult}.
|
|
1253
2203
|
*
|
|
1254
2204
|
* @example
|
|
1255
2205
|
* ```ts
|
|
1256
|
-
*
|
|
1257
|
-
* export const jsxRenderer = createRenderer(() => {
|
|
1258
|
-
* const runtime = new Runtime()
|
|
1259
|
-
* return {
|
|
1260
|
-
* async render(element) { await runtime.render(element) },
|
|
1261
|
-
* get files() { return runtime.nodes },
|
|
1262
|
-
* unmount(error) { runtime.unmount(error) },
|
|
1263
|
-
* }
|
|
1264
|
-
* })
|
|
2206
|
+
* import { createReporter, Diagnostics } from '@kubb/core'
|
|
1265
2207
|
*
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
2208
|
+
* export const jsonReporter = createReporter({
|
|
2209
|
+
* name: 'json',
|
|
2210
|
+
* report(result) {
|
|
2211
|
+
* return { status: Diagnostics.hasError(result.diagnostics) ? 'failed' : 'success', diagnostics: result.diagnostics }
|
|
2212
|
+
* },
|
|
2213
|
+
* drain(context, reports) {
|
|
2214
|
+
* process.stdout.write(`${JSON.stringify(reports, null, 2)}\n`)
|
|
2215
|
+
* },
|
|
1272
2216
|
* })
|
|
1273
2217
|
* ```
|
|
1274
2218
|
*/
|
|
1275
|
-
function
|
|
1276
|
-
|
|
2219
|
+
function createReporter(reporter) {
|
|
2220
|
+
const reports = [];
|
|
2221
|
+
return {
|
|
2222
|
+
name: reporter.name,
|
|
2223
|
+
async report(result, context) {
|
|
2224
|
+
const report = await reporter.report(result, context);
|
|
2225
|
+
if (reporter.drain) reports.push(report);
|
|
2226
|
+
},
|
|
2227
|
+
async drain(context) {
|
|
2228
|
+
await reporter.drain?.(context, [...reports]);
|
|
2229
|
+
reports.length = 0;
|
|
2230
|
+
},
|
|
2231
|
+
[Symbol.dispose]() {
|
|
2232
|
+
reports.length = 0;
|
|
2233
|
+
}
|
|
2234
|
+
};
|
|
1277
2235
|
}
|
|
1278
2236
|
//#endregion
|
|
1279
|
-
//#region src/
|
|
2237
|
+
//#region src/reporters/report.ts
|
|
1280
2238
|
/**
|
|
1281
|
-
*
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
2239
|
+
* Builds the normalized {@link Report} for one config from its {@link GenerationResult}. Splits the
|
|
2240
|
+
* diagnostics into problems and per-plugin timings (slowest first) and derives the plugin and issue
|
|
2241
|
+
* counts, so every reporter renders the same data.
|
|
1284
2242
|
*/
|
|
1285
|
-
function
|
|
1286
|
-
|
|
2243
|
+
function buildReport(result) {
|
|
2244
|
+
const { config, diagnostics, filesCreated, status, hrStart } = result;
|
|
2245
|
+
const failed = Diagnostics.failedPlugins(diagnostics);
|
|
2246
|
+
const total = config.plugins?.length ?? 0;
|
|
2247
|
+
const counts = Diagnostics.count(diagnostics);
|
|
2248
|
+
const problems = diagnostics.filter(Diagnostics.isProblem);
|
|
2249
|
+
const timings = diagnostics.filter(Diagnostics.isPerformance).sort((a, b) => b.duration - a.duration).map((diagnostic) => ({
|
|
2250
|
+
plugin: diagnostic.plugin,
|
|
2251
|
+
durationMs: diagnostic.duration
|
|
2252
|
+
}));
|
|
2253
|
+
return {
|
|
2254
|
+
name: config.name ?? "",
|
|
2255
|
+
status,
|
|
2256
|
+
plugins: {
|
|
2257
|
+
passed: total - failed.length,
|
|
2258
|
+
failed,
|
|
2259
|
+
total
|
|
2260
|
+
},
|
|
2261
|
+
counts,
|
|
2262
|
+
filesCreated,
|
|
2263
|
+
durationMs: getElapsedMs(hrStart),
|
|
2264
|
+
output: resolve(config.root, config.output.path),
|
|
2265
|
+
timings,
|
|
2266
|
+
diagnostics: problems.map((diagnostic) => Diagnostics.serialize(diagnostic))
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
//#endregion
|
|
2270
|
+
//#region src/reporters/cliReporter.ts
|
|
2271
|
+
/**
|
|
2272
|
+
* Builds the vitest/jest-style summary for one {@link Report}: right-aligned dim labels with
|
|
2273
|
+
* `N passed (total)` counts, and a per-plugin `Timings` section when `showTimings`.
|
|
2274
|
+
*/
|
|
2275
|
+
function buildSummaryLines(report, { showTimings }) {
|
|
2276
|
+
const { status, plugins, counts, filesCreated, durationMs, output, timings } = report;
|
|
2277
|
+
const rows = [];
|
|
2278
|
+
rows.push(["Plugins", status === "success" ? `${styleText("green", `${plugins.passed} passed`)} (${plugins.total})` : `${styleText("green", `${plugins.passed} passed`)} | ${styleText("red", `${plugins.failed.length} failed`)} (${plugins.total})`]);
|
|
2279
|
+
if (status === "failed" && plugins.failed.length > 0) rows.push(["Failed", plugins.failed.map((name) => randomCliColor(name)).join(", ")]);
|
|
2280
|
+
if (counts.errors > 0 || counts.warnings > 0) {
|
|
2281
|
+
const issues = [counts.errors > 0 ? styleText("red", `${counts.errors} ${counts.errors === 1 ? "error" : "errors"}`) : void 0, counts.warnings > 0 ? styleText("yellow", `${counts.warnings} ${counts.warnings === 1 ? "warning" : "warnings"}`) : void 0].filter(Boolean).join(" | ");
|
|
2282
|
+
rows.push(["Issues", issues]);
|
|
2283
|
+
}
|
|
2284
|
+
rows.push(["Files", `${styleText("green", String(filesCreated))} generated`]);
|
|
2285
|
+
rows.push(["Duration", styleText("green", formatMs(durationMs))]);
|
|
2286
|
+
rows.push(["Output", output]);
|
|
2287
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length), timings.length > 0 ? 7 : 0);
|
|
2288
|
+
const lines = rows.map(([label, value]) => `${styleText("dim", label.padStart(labelWidth))} ${value}`);
|
|
2289
|
+
if (showTimings && timings.length > 0) {
|
|
2290
|
+
const nameWidth = Math.max(0, ...timings.map((timing) => timing.plugin.length));
|
|
2291
|
+
const indent = " ".repeat(labelWidth + 2);
|
|
2292
|
+
lines.push(styleText("dim", "Timings".padStart(labelWidth)));
|
|
2293
|
+
for (const timing of timings) {
|
|
2294
|
+
const timeStr = formatMs(timing.durationMs);
|
|
2295
|
+
const barLength = Math.min(Math.ceil(timing.durationMs / 100), 10);
|
|
2296
|
+
const bar = styleText("dim", "█".repeat(barLength));
|
|
2297
|
+
lines.push(`${indent}${styleText("dim", "•")} ${timing.plugin.padEnd(nameWidth)} ${bar} ${timeStr}`);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
return lines;
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Renders the summary as plain `console.log` lines so it works in every CLI (no clack/TTY
|
|
2304
|
+
* dependency): a blank line, the config name colored by status, then the summary rows.
|
|
2305
|
+
*/
|
|
2306
|
+
function renderSummary(lines, { title, status }) {
|
|
2307
|
+
console.log("");
|
|
2308
|
+
if (title) console.log(styleText(status === "failed" ? "red" : "green", title));
|
|
2309
|
+
for (const line of lines) console.log(line);
|
|
1287
2310
|
}
|
|
2311
|
+
/**
|
|
2312
|
+
* The default `cli` reporter. Renders the {@link Report} for each config as it finishes, independent
|
|
2313
|
+
* of the live logger view. Suppressed at `silent`. The `verbose` level adds the per-plugin timings.
|
|
2314
|
+
*/
|
|
2315
|
+
const cliReporter = createReporter({
|
|
2316
|
+
name: "cli",
|
|
2317
|
+
report(result, { logLevel: logLevel$1 }) {
|
|
2318
|
+
if (logLevel$1 <= logLevel.silent) return;
|
|
2319
|
+
const report = buildReport(result);
|
|
2320
|
+
renderSummary(buildSummaryLines(report, { showTimings: logLevel$1 >= logLevel.verbose }), {
|
|
2321
|
+
title: report.name,
|
|
2322
|
+
status: report.status
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
});
|
|
1288
2326
|
//#endregion
|
|
1289
|
-
//#region src/
|
|
2327
|
+
//#region src/reporters/fileReporter.ts
|
|
2328
|
+
/**
|
|
2329
|
+
* Builds the `## Summary` section: the same counts the cli and json reporters expose, as a list of
|
|
2330
|
+
* `label value` rows with the labels padded to a common width.
|
|
2331
|
+
*/
|
|
2332
|
+
function buildSummarySection(report) {
|
|
2333
|
+
const { status, plugins, counts, filesCreated, durationMs, output } = report;
|
|
2334
|
+
const rows = [["Status", status], ["Plugins", status === "success" ? `${plugins.passed} passed (${plugins.total})` : `${plugins.passed} passed | ${plugins.failed.length} failed (${plugins.total})`]];
|
|
2335
|
+
if (plugins.failed.length > 0) rows.push(["Failed", plugins.failed.join(", ")]);
|
|
2336
|
+
rows.push(["Issues", `${counts.errors} errors | ${counts.warnings} warnings | ${counts.infos} infos`]);
|
|
2337
|
+
rows.push(["Files", `${filesCreated} generated`]);
|
|
2338
|
+
rows.push(["Duration", formatMs(durationMs)]);
|
|
2339
|
+
rows.push(["Output", output]);
|
|
2340
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
2341
|
+
return [
|
|
2342
|
+
"## Summary",
|
|
2343
|
+
"",
|
|
2344
|
+
...rows.map(([label, value]) => ` ${label.padEnd(labelWidth)} ${value}`)
|
|
2345
|
+
];
|
|
2346
|
+
}
|
|
2347
|
+
/**
|
|
2348
|
+
* Builds the `## Problems` section: each problem rendered in the miette block format, blocks
|
|
2349
|
+
* separated by a blank line. Returns an empty array when there are no problems, so the caller
|
|
2350
|
+
* can drop the heading.
|
|
2351
|
+
*/
|
|
2352
|
+
function buildProblemSection(diagnostics) {
|
|
2353
|
+
const problems = diagnostics.filter(Diagnostics.isProblem);
|
|
2354
|
+
if (problems.length === 0) return [];
|
|
2355
|
+
return [
|
|
2356
|
+
"## Problems",
|
|
2357
|
+
"",
|
|
2358
|
+
problems.map((diagnostic) => Diagnostics.formatLines(diagnostic).join("\n")).join("\n\n")
|
|
2359
|
+
];
|
|
2360
|
+
}
|
|
1290
2361
|
/**
|
|
1291
|
-
*
|
|
2362
|
+
* Builds the `## Timings` section from a {@link Report}: one `plugin duration` row per record,
|
|
2363
|
+
* slowest first with the plugin names left-aligned and the durations right-aligned. Returns an
|
|
2364
|
+
* empty array when there are no timings.
|
|
2365
|
+
*/
|
|
2366
|
+
function buildTimingSection(report) {
|
|
2367
|
+
const { timings } = report;
|
|
2368
|
+
if (timings.length === 0) return [];
|
|
2369
|
+
const nameWidth = Math.max(...timings.map((timing) => timing.plugin.length));
|
|
2370
|
+
const durations = timings.map((timing) => formatMs(timing.durationMs));
|
|
2371
|
+
const durationWidth = Math.max(...durations.map((duration) => duration.length));
|
|
2372
|
+
return [
|
|
2373
|
+
"## Timings",
|
|
2374
|
+
"",
|
|
2375
|
+
...timings.map((timing, index) => ` ${timing.plugin.padEnd(nameWidth)} ${durations[index].padStart(durationWidth)}`)
|
|
2376
|
+
];
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* The `file` reporter. Writes a config's {@link Report} to `.kubb/kubb-<name>-<timestamp>.log` as a
|
|
2380
|
+
* plain-text document: a `# <name> — <timestamp>` header, a `## Summary` with the same counts the
|
|
2381
|
+
* cli and json reporters expose, a `## Problems` section in the miette block format, and a
|
|
2382
|
+
* `## Timings` section. Selected with `--reporter file` (or `reporters: ['file']`).
|
|
1292
2383
|
*
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
2384
|
+
* @note It captures the collected diagnostics once a config finishes, not the live
|
|
2385
|
+
* `kubb:info`/`kubb:plugin` hook stream. Color is stripped so the file stays plain text even when
|
|
2386
|
+
* the run is attached to a TTY.
|
|
2387
|
+
*/
|
|
2388
|
+
const fileReporter = createReporter({
|
|
2389
|
+
name: "file",
|
|
2390
|
+
async report(result) {
|
|
2391
|
+
const { diagnostics, config } = result;
|
|
2392
|
+
if (diagnostics.length === 0) return;
|
|
2393
|
+
const report = buildReport(result);
|
|
2394
|
+
const content = stripVTControlCharacters([config.name ? `# ${config.name} — ${(/* @__PURE__ */ new Date()).toISOString()}` : `# ${(/* @__PURE__ */ new Date()).toISOString()}`, ...[
|
|
2395
|
+
buildSummarySection(report),
|
|
2396
|
+
buildProblemSection(diagnostics),
|
|
2397
|
+
buildTimingSection(report)
|
|
2398
|
+
].filter((section) => section.length > 0).map((section) => section.join("\n"))].join("\n\n"));
|
|
2399
|
+
const baseName = `${[
|
|
2400
|
+
"kubb",
|
|
2401
|
+
config.name,
|
|
2402
|
+
Date.now()
|
|
2403
|
+
].filter(Boolean).join("-")}.log`;
|
|
2404
|
+
const pathName = resolve(process$1.cwd(), ".kubb", baseName);
|
|
2405
|
+
await write(pathName, `${content}\n`);
|
|
2406
|
+
console.error(`Debug log written to ${relative(process$1.cwd(), pathName)}`);
|
|
2407
|
+
}
|
|
2408
|
+
});
|
|
2409
|
+
//#endregion
|
|
2410
|
+
//#region src/reporters/jsonReporter.ts
|
|
2411
|
+
/**
|
|
2412
|
+
* The `json` reporter. `report` returns one config's {@link Report}, which {@link createReporter}
|
|
2413
|
+
* buffers, and `drain` writes them as a single pretty-printed JSON array on `kubb:lifecycle:end`.
|
|
2414
|
+
* Buffering keeps a multi-config run one valid JSON document on stdout instead of concatenated
|
|
2415
|
+
* objects that would break `jq .`. The terminal reporter is suppressed while `json` is active so
|
|
2416
|
+
* stdout stays valid JSON.
|
|
2417
|
+
*/
|
|
2418
|
+
const jsonReporter = createReporter({
|
|
2419
|
+
name: "json",
|
|
2420
|
+
report(result) {
|
|
2421
|
+
return buildReport(result);
|
|
2422
|
+
},
|
|
2423
|
+
drain(_context, reports) {
|
|
2424
|
+
process$1.stdout.write(`${JSON.stringify(reports, null, 2)}\n`);
|
|
2425
|
+
}
|
|
2426
|
+
});
|
|
2427
|
+
//#endregion
|
|
2428
|
+
//#region src/createRenderer.ts
|
|
2429
|
+
/**
|
|
2430
|
+
* Defines a renderer factory. Renderers turn the generator's return value
|
|
2431
|
+
* (JSX, a template string, a tree of any shape) into `FileNode`s that get
|
|
2432
|
+
* written to disk.
|
|
1296
2433
|
*
|
|
1297
|
-
*
|
|
1298
|
-
*
|
|
1299
|
-
*
|
|
1300
|
-
* name: 'my-logger',
|
|
1301
|
-
* install(context, options) {
|
|
1302
|
-
* context.on('kubb:info', (message) => console.log('ℹ', message))
|
|
1303
|
-
* context.on('kubb:error', (error) => console.error('✗', error.message))
|
|
1304
|
-
* },
|
|
1305
|
-
* })
|
|
1306
|
-
* ```
|
|
2434
|
+
* A renderer can target output formats beyond JSX, for instance a Handlebars
|
|
2435
|
+
* renderer or one that writes binary files. Plugins and generators pick the
|
|
2436
|
+
* renderer to use via the `renderer` field on `defineGenerator`.
|
|
1307
2437
|
*
|
|
1308
|
-
* @example
|
|
2438
|
+
* @example A minimal renderer that wraps a custom runtime
|
|
1309
2439
|
* ```ts
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
*
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
2440
|
+
* import { createRenderer } from '@kubb/core'
|
|
2441
|
+
*
|
|
2442
|
+
* export const myRenderer = createRenderer(() => {
|
|
2443
|
+
* const runtime = new MyRuntime()
|
|
2444
|
+
* return {
|
|
2445
|
+
* async render(element) {
|
|
2446
|
+
* await runtime.render(element)
|
|
2447
|
+
* },
|
|
2448
|
+
* get files() {
|
|
2449
|
+
* return runtime.files
|
|
2450
|
+
* },
|
|
2451
|
+
* [Symbol.dispose]() {
|
|
2452
|
+
* runtime.dispose()
|
|
2453
|
+
* },
|
|
2454
|
+
* }
|
|
1316
2455
|
* })
|
|
1317
2456
|
* ```
|
|
1318
2457
|
*/
|
|
1319
|
-
function
|
|
1320
|
-
return
|
|
2458
|
+
function createRenderer(factory) {
|
|
2459
|
+
return factory;
|
|
1321
2460
|
}
|
|
1322
2461
|
//#endregion
|
|
1323
|
-
//#region src/
|
|
2462
|
+
//#region src/defineGenerator.ts
|
|
1324
2463
|
/**
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1328
|
-
* Per-build state (such as accumulators) belongs inside the factory closure so each `createKubb` invocation gets its own isolated instance.
|
|
2464
|
+
* Defines a generator: a unit of work that runs during the plugin's AST walk
|
|
2465
|
+
* and produces files. Plugins register generators via `ctx.addGenerator()`
|
|
2466
|
+
* inside `kubb:plugin:setup`.
|
|
1329
2467
|
*
|
|
1330
|
-
*
|
|
2468
|
+
* The returned object is the input as-is, but with `this` types preserved so
|
|
2469
|
+
* `schema`/`operation`/`operations` methods are correctly typed against the
|
|
2470
|
+
* plugin's `PluginFactoryOptions`. Renderer elements and `FileNode[]` returns
|
|
2471
|
+
* are both handled by the runtime, so pick whichever style fits.
|
|
1331
2472
|
*
|
|
1332
|
-
* @example
|
|
1333
|
-
* ```
|
|
1334
|
-
* import {
|
|
2473
|
+
* @example JSX-based schema generator
|
|
2474
|
+
* ```tsx
|
|
2475
|
+
* import { defineGenerator } from '@kubb/core'
|
|
2476
|
+
* import { jsxRenderer } from '@kubb/renderer-jsx'
|
|
1335
2477
|
*
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
2478
|
+
* export const typeGenerator = defineGenerator({
|
|
2479
|
+
* name: 'typescript',
|
|
2480
|
+
* renderer: jsxRenderer,
|
|
2481
|
+
* schema(node, ctx) {
|
|
2482
|
+
* return (
|
|
2483
|
+
* <File path={`${ctx.root}/${node.name}.ts`}>
|
|
2484
|
+
* <Type node={node} resolver={ctx.resolver} />
|
|
2485
|
+
* </File>
|
|
2486
|
+
* )
|
|
1343
2487
|
* },
|
|
1344
|
-
* }))
|
|
1345
|
-
*
|
|
1346
|
-
* // Middleware with options and per-build state
|
|
1347
|
-
* export const prefixMiddleware = defineMiddleware((options: { prefix: string } = { prefix: '' }) => {
|
|
1348
|
-
* const seen = new Set<string>()
|
|
1349
|
-
* return {
|
|
1350
|
-
* name: 'prefix-middleware',
|
|
1351
|
-
* hooks: {
|
|
1352
|
-
* 'kubb:plugin:end'({ plugin }) {
|
|
1353
|
-
* seen.add(`${options.prefix}${plugin.name}`)
|
|
1354
|
-
* },
|
|
1355
|
-
* },
|
|
1356
|
-
* }
|
|
1357
2488
|
* })
|
|
1358
2489
|
* ```
|
|
1359
2490
|
*/
|
|
1360
|
-
function
|
|
1361
|
-
return
|
|
2491
|
+
function defineGenerator(generator) {
|
|
2492
|
+
return generator;
|
|
1362
2493
|
}
|
|
1363
2494
|
//#endregion
|
|
1364
2495
|
//#region src/defineParser.ts
|
|
1365
2496
|
/**
|
|
1366
|
-
*
|
|
2497
|
+
* Wraps a parser factory and returns a function that accepts user options and
|
|
2498
|
+
* yields a typed {@link Parser}. Mirrors {@link definePlugin}: the factory
|
|
2499
|
+
* receives the caller's options, and calling the returned function without
|
|
2500
|
+
* options passes an empty object.
|
|
1367
2501
|
*
|
|
1368
|
-
*
|
|
2502
|
+
* Register the result in the `parsers` array on `defineConfig`, calling it to
|
|
2503
|
+
* apply options (`parserTs({ extension: { '.ts': '.js' } })`).
|
|
1369
2504
|
*
|
|
1370
2505
|
* @example
|
|
1371
2506
|
* ```ts
|
|
1372
2507
|
* import { defineParser } from '@kubb/core'
|
|
2508
|
+
* import { extractStringsFromNodes } from '@kubb/ast'
|
|
1373
2509
|
*
|
|
1374
|
-
* export const
|
|
2510
|
+
* export const parserJson = defineParser((options: { pretty?: boolean } = {}) => ({
|
|
1375
2511
|
* name: 'json',
|
|
1376
2512
|
* extNames: ['.json'],
|
|
1377
2513
|
* parse(file) {
|
|
1378
|
-
* const
|
|
1379
|
-
* return
|
|
2514
|
+
* const source = file.sources.map((source) => extractStringsFromNodes(source.nodes ?? [])).join('\n')
|
|
2515
|
+
* return options.pretty ? JSON.stringify(JSON.parse(source), null, 2) : source
|
|
1380
2516
|
* },
|
|
1381
|
-
*
|
|
2517
|
+
* print(...nodes) {
|
|
2518
|
+
* return nodes.map(String).join('\n')
|
|
2519
|
+
* },
|
|
2520
|
+
* }))
|
|
1382
2521
|
* ```
|
|
1383
2522
|
*/
|
|
1384
|
-
function defineParser(
|
|
1385
|
-
return
|
|
2523
|
+
function defineParser(factory) {
|
|
2524
|
+
return (options) => factory(options ?? {});
|
|
1386
2525
|
}
|
|
1387
2526
|
//#endregion
|
|
1388
2527
|
//#region src/storages/memoryStorage.ts
|
|
@@ -1399,7 +2538,7 @@ function defineParser(parser) {
|
|
|
1399
2538
|
* import { defineConfig } from 'kubb'
|
|
1400
2539
|
*
|
|
1401
2540
|
* export default defineConfig({
|
|
1402
|
-
* input:
|
|
2541
|
+
* input: './petStore.yaml',
|
|
1403
2542
|
* output: { path: './src/gen' },
|
|
1404
2543
|
* storage: memoryStorage(),
|
|
1405
2544
|
* })
|
|
@@ -1435,6 +2574,6 @@ const memoryStorage = createStorage(() => {
|
|
|
1435
2574
|
};
|
|
1436
2575
|
});
|
|
1437
2576
|
//#endregion
|
|
1438
|
-
export {
|
|
2577
|
+
export { Diagnostics, Hookable, KubbDriver, Resolver, applyConfigDefaults, cliReporter, createAdapter, createKubb, createRenderer, createReporter, createResolver, createStorage, defineGenerator, defineParser, definePlugin, fileReporter, fsStorage, getInputKind, jsonReporter, logLevel, memoryStorage };
|
|
1439
2578
|
|
|
1440
2579
|
//# sourceMappingURL=index.js.map
|