@kubb/core 5.0.0-beta.4 → 5.0.0-beta.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -148
- package/dist/diagnostics-DhfW8YpT.d.ts +2963 -0
- package/dist/index.cjs +775 -1193
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +165 -171
- package/dist/index.js +760 -1188
- package/dist/index.js.map +1 -1
- package/dist/memoryStorage-DTv1Kub1.js +2852 -0
- package/dist/memoryStorage-DTv1Kub1.js.map +1 -0
- package/dist/memoryStorage-Dkxnid2K.cjs +2985 -0
- package/dist/memoryStorage-Dkxnid2K.cjs.map +1 -0
- package/dist/mocks.cjs +80 -18
- package/dist/mocks.cjs.map +1 -1
- package/dist/mocks.d.ts +35 -8
- package/dist/mocks.js +81 -21
- package/dist/mocks.js.map +1 -1
- package/package.json +8 -19
- package/src/FileManager.ts +85 -63
- package/src/FileProcessor.ts +171 -43
- package/src/HookRegistry.ts +45 -0
- package/src/KubbDriver.ts +897 -0
- package/src/Telemetry.ts +278 -0
- package/src/Transform.ts +58 -0
- package/src/constants.ts +111 -19
- package/src/createAdapter.ts +113 -17
- package/src/createKubb.ts +921 -493
- package/src/createRenderer.ts +58 -27
- package/src/createReporter.ts +147 -0
- package/src/createStorage.ts +36 -23
- package/src/defineGenerator.ts +129 -17
- package/src/defineLogger.ts +78 -7
- package/src/defineMiddleware.ts +19 -17
- package/src/defineParser.ts +30 -13
- package/src/definePlugin.ts +320 -17
- package/src/defineResolver.ts +381 -179
- package/src/diagnostics.ts +660 -0
- package/src/index.ts +8 -6
- package/src/mocks.ts +92 -19
- package/src/reporters/cliReporter.ts +90 -0
- package/src/reporters/fileReporter.ts +103 -0
- package/src/reporters/jsonReporter.ts +20 -0
- package/src/reporters/report.ts +85 -0
- package/src/storages/fsStorage.ts +13 -37
- package/src/types.ts +59 -1297
- package/dist/PluginDriver-Ds-Us-e4.cjs +0 -1036
- package/dist/PluginDriver-Ds-Us-e4.cjs.map +0 -1
- package/dist/PluginDriver-Wi34Pegx.js +0 -945
- package/dist/PluginDriver-Wi34Pegx.js.map +0 -1
- package/dist/types-Cd0jhNmx.d.ts +0 -2153
- package/src/Kubb.ts +0 -300
- package/src/PluginDriver.ts +0 -424
- package/src/devtools.ts +0 -59
- package/src/renderNode.ts +0 -35
- package/src/utils/diagnostics.ts +0 -18
- package/src/utils/isInputPath.ts +0 -10
- package/src/utils/packageJSON.ts +0 -99
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
|
@@ -0,0 +1,2985 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __name = (target, value) => __defProp(target, "name", {
|
|
5
|
+
value,
|
|
6
|
+
configurable: true
|
|
7
|
+
});
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
14
|
+
key = keys[i];
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
16
|
+
get: ((k) => from[k]).bind(null, key),
|
|
17
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
//#endregion
|
|
27
|
+
let node_events = require("node:events");
|
|
28
|
+
let node_util = require("node:util");
|
|
29
|
+
let node_path = require("node:path");
|
|
30
|
+
node_path = __toESM(node_path, 1);
|
|
31
|
+
let _kubb_ast = require("@kubb/ast");
|
|
32
|
+
let node_async_hooks = require("node:async_hooks");
|
|
33
|
+
//#region ../../internals/utils/src/errors.ts
|
|
34
|
+
/**
|
|
35
|
+
* Thrown when one or more errors occur during a Kubb build.
|
|
36
|
+
* Carries the full list of underlying errors on `errors`.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* throw new BuildError('Build failed', { errors: [err1, err2] })
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
var BuildError = class extends Error {
|
|
44
|
+
errors;
|
|
45
|
+
constructor(message, options) {
|
|
46
|
+
super(message, { cause: options.cause });
|
|
47
|
+
this.name = "BuildError";
|
|
48
|
+
this.errors = options.errors;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Coerces an unknown thrown value to an `Error` instance.
|
|
53
|
+
* Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* try { ... } catch(err) {
|
|
58
|
+
* throw new BuildError('Build failed', { cause: toError(err), errors: [] })
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
function toError(value) {
|
|
63
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Extracts a human-readable message from any thrown value.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* getErrorMessage(new Error('oops')) // 'oops'
|
|
71
|
+
* getErrorMessage('plain string') // 'plain string'
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
function getErrorMessage(value) {
|
|
75
|
+
return value instanceof Error ? value.message : String(value);
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region ../../internals/utils/src/asyncEventEmitter.ts
|
|
79
|
+
/**
|
|
80
|
+
* Typed `EventEmitter` that awaits all async listeners before resolving.
|
|
81
|
+
* Wraps Node's `EventEmitter` with full TypeScript event-map inference.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const emitter = new AsyncEventEmitter<{ build: [name: string] }>()
|
|
86
|
+
* emitter.on('build', async (name) => { console.log(name) })
|
|
87
|
+
* await emitter.emit('build', 'petstore') // all listeners awaited
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
var AsyncEventEmitter = class {
|
|
91
|
+
/**
|
|
92
|
+
* Maximum number of listeners per event before Node emits a memory-leak warning.
|
|
93
|
+
* @default 10
|
|
94
|
+
*/
|
|
95
|
+
constructor(maxListener = 10) {
|
|
96
|
+
this.#emitter.setMaxListeners(maxListener);
|
|
97
|
+
}
|
|
98
|
+
#emitter = new node_events.EventEmitter();
|
|
99
|
+
/**
|
|
100
|
+
* Emits `eventName` and awaits all registered listeners sequentially.
|
|
101
|
+
* Throws if any listener rejects, wrapping the cause with the event name and serialized arguments.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* await emitter.emit('build', 'petstore')
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
emit(eventName, ...eventArgs) {
|
|
109
|
+
const listeners = this.#emitter.listeners(eventName);
|
|
110
|
+
if (listeners.length === 0) return;
|
|
111
|
+
return this.#emitAll(eventName, listeners, eventArgs);
|
|
112
|
+
}
|
|
113
|
+
async #emitAll(eventName, listeners, eventArgs) {
|
|
114
|
+
for (const listener of listeners) try {
|
|
115
|
+
await listener(...eventArgs);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
let serializedArgs;
|
|
118
|
+
try {
|
|
119
|
+
serializedArgs = JSON.stringify(eventArgs);
|
|
120
|
+
} catch {
|
|
121
|
+
serializedArgs = String(eventArgs);
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`Error in async listener for "${eventName}" with eventArgs ${serializedArgs}`, { cause: toError(err) });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Registers a persistent listener for `eventName`.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* emitter.on('build', async (name) => { console.log(name) })
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
on(eventName, handler) {
|
|
135
|
+
this.#emitter.on(eventName, handler);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Registers a one-shot listener that removes itself after the first invocation.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* emitter.onOnce('build', async (name) => { console.log(name) })
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
onOnce(eventName, handler) {
|
|
146
|
+
const wrapper = (...args) => {
|
|
147
|
+
this.off(eventName, wrapper);
|
|
148
|
+
return handler(...args);
|
|
149
|
+
};
|
|
150
|
+
this.on(eventName, wrapper);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Removes a previously registered listener.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* emitter.off('build', handler)
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
off(eventName, handler) {
|
|
161
|
+
this.#emitter.off(eventName, handler);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Returns the number of listeners registered for `eventName`.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```ts
|
|
168
|
+
* emitter.on('build', handler)
|
|
169
|
+
* emitter.listenerCount('build') // 1
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
listenerCount(eventName) {
|
|
173
|
+
return this.#emitter.listenerCount(eventName);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Raises or lowers the per-event listener ceiling before Node warns about a memory leak.
|
|
177
|
+
* Set this above the expected listener count when many listeners attach by design.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* emitter.setMaxListeners(40)
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
setMaxListeners(max) {
|
|
185
|
+
this.#emitter.setMaxListeners(max);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Returns the current per-event listener ceiling.
|
|
189
|
+
*/
|
|
190
|
+
getMaxListeners() {
|
|
191
|
+
return this.#emitter.getMaxListeners();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Removes all listeners from every event channel.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* emitter.removeAll()
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
removeAll() {
|
|
202
|
+
this.#emitter.removeAllListeners();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region ../../internals/utils/src/casing.ts
|
|
207
|
+
/**
|
|
208
|
+
* Shared implementation for camelCase and PascalCase conversion.
|
|
209
|
+
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
210
|
+
* and capitalizes each word according to `pascal`.
|
|
211
|
+
*
|
|
212
|
+
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
213
|
+
*/
|
|
214
|
+
function toCamelOrPascal(text, pascal) {
|
|
215
|
+
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
216
|
+
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
217
|
+
if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
|
|
218
|
+
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
219
|
+
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Splits `text` on `.` and applies `transformPart` to each segment.
|
|
223
|
+
* The last segment receives `isLast = true`, all earlier segments receive `false`.
|
|
224
|
+
* Segments are joined with `/` to form a file path.
|
|
225
|
+
*
|
|
226
|
+
* Only splits on dots followed by a letter so that version numbers
|
|
227
|
+
* embedded in operationIds (e.g. `v2025.0`) are kept intact.
|
|
228
|
+
*
|
|
229
|
+
* Empty segments are filtered before joining. They arise when the text starts with
|
|
230
|
+
* a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']`
|
|
231
|
+
* and `'..'` transforms to an empty string). Without this filter the join would produce
|
|
232
|
+
* a leading `/`, which `path.resolve` would interpret as an absolute path, allowing
|
|
233
|
+
* generated files to escape the configured output directory.
|
|
234
|
+
*/
|
|
235
|
+
function applyToFileParts(text, transformPart) {
|
|
236
|
+
const parts = text.split(/\.(?=[a-zA-Z])/);
|
|
237
|
+
return parts.map((part, i) => transformPart(part, i === parts.length - 1)).filter(Boolean).join("/");
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Converts `text` to camelCase.
|
|
241
|
+
* When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* camelCase('hello-world') // 'helloWorld'
|
|
245
|
+
* camelCase('pet.petId', { isFile: true }) // 'pet/petId'
|
|
246
|
+
*/
|
|
247
|
+
function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
248
|
+
if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
|
|
249
|
+
prefix,
|
|
250
|
+
suffix
|
|
251
|
+
} : {}));
|
|
252
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Converts `text` to PascalCase.
|
|
256
|
+
* When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* pascalCase('hello-world') // 'HelloWorld'
|
|
260
|
+
* pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
|
|
261
|
+
*/
|
|
262
|
+
function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
263
|
+
if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
|
|
264
|
+
prefix,
|
|
265
|
+
suffix
|
|
266
|
+
}) : camelCase(part));
|
|
267
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region ../../internals/utils/src/time.ts
|
|
271
|
+
/**
|
|
272
|
+
* Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.
|
|
273
|
+
* Rounds to 2 decimal places for sub-millisecond precision without noise.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```ts
|
|
277
|
+
* const start = process.hrtime()
|
|
278
|
+
* doWork()
|
|
279
|
+
* getElapsedMs(start) // 42.35
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
function getElapsedMs(hrStart) {
|
|
283
|
+
const [seconds, nanoseconds] = process.hrtime(hrStart);
|
|
284
|
+
const ms = seconds * 1e3 + nanoseconds / 1e6;
|
|
285
|
+
return Math.round(ms * 100) / 100;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Converts a millisecond duration into a human-readable string (`ms`, `s`, or `m s`).
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```ts
|
|
292
|
+
* formatMs(250) // '250ms'
|
|
293
|
+
* formatMs(1500) // '1.50s'
|
|
294
|
+
* formatMs(90000) // '1m 30.0s'
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
function formatMs(ms) {
|
|
298
|
+
if (ms >= 6e4) return `${Math.floor(ms / 6e4)}m ${(ms % 6e4 / 1e3).toFixed(1)}s`;
|
|
299
|
+
if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
|
|
300
|
+
return `${Math.round(ms)}ms`;
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region ../../internals/utils/src/promise.ts
|
|
304
|
+
function* chunks(arr, size) {
|
|
305
|
+
for (let i = 0; i < arr.length; i += size) yield arr.slice(i, i + size);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Slices `source` into batches of `concurrency` items and awaits `process` for each batch.
|
|
309
|
+
* Accepts both plain arrays (sync) and `AsyncIterable` (streaming).
|
|
310
|
+
*
|
|
311
|
+
* `process` controls whether items inside a batch run in parallel; this helper only
|
|
312
|
+
* controls batch size and per-batch flushing.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* ```ts
|
|
316
|
+
* // parallel dispatch inside each batch
|
|
317
|
+
* await forBatches(schemas, (batch) => Promise.all(batch.map(process)), { concurrency: 8 })
|
|
318
|
+
*
|
|
319
|
+
* // async iterable with a flush after every batch
|
|
320
|
+
* await forBatches(stream.schemas, (batch) => dispatch(batch), { concurrency: 8, flush })
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
async function forBatches(source, process, options) {
|
|
324
|
+
const { concurrency, flush } = options;
|
|
325
|
+
if (Array.isArray(source)) {
|
|
326
|
+
for (const batch of chunks(source, concurrency)) {
|
|
327
|
+
await process(batch);
|
|
328
|
+
if (flush) await flush();
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const batch = [];
|
|
333
|
+
for await (const item of source) {
|
|
334
|
+
batch.push(item);
|
|
335
|
+
if (batch.length >= concurrency) {
|
|
336
|
+
await process(batch.splice(0));
|
|
337
|
+
if (flush) await flush();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (batch.length > 0) {
|
|
341
|
+
await process(batch.splice(0));
|
|
342
|
+
if (flush) await flush();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/** Returns `true` when `result` is a thenable `Promise`.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```ts
|
|
349
|
+
* isPromise(Promise.resolve(1)) // true
|
|
350
|
+
* isPromise(42) // false
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
function isPromise(result) {
|
|
354
|
+
return result !== null && result !== void 0 && typeof result["then"] === "function";
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Wraps `factory` with a keyed cache backed by the provided store.
|
|
358
|
+
*
|
|
359
|
+
* Pass a `WeakMap` for object keys (results are GC-eligible when the key is
|
|
360
|
+
* collected) or a `Map` for primitive keys. For multi-argument functions,
|
|
361
|
+
* nest two `memoize` calls — the outer keyed by the first argument, the
|
|
362
|
+
* inner (created once per outer miss) keyed by the second.
|
|
363
|
+
*
|
|
364
|
+
* Because the cache is owned by the caller, it can be shared, inspected, or
|
|
365
|
+
* cleared independently of the memoized function.
|
|
366
|
+
*
|
|
367
|
+
* @example Single WeakMap key
|
|
368
|
+
* ```ts
|
|
369
|
+
* const cache = new WeakMap<SchemaNode, Set<string>>()
|
|
370
|
+
* const getRefs = memoize(cache, (node) => collectRefs(node))
|
|
371
|
+
* ```
|
|
372
|
+
*
|
|
373
|
+
* @example Single Map key (primitive)
|
|
374
|
+
* ```ts
|
|
375
|
+
* const cache = new Map<string, Resolver>()
|
|
376
|
+
* const getResolver = memoize(cache, (name) => buildResolver(name))
|
|
377
|
+
* ```
|
|
378
|
+
*
|
|
379
|
+
* @example Two-level (object + primitive)
|
|
380
|
+
* ```ts
|
|
381
|
+
* const outer = new WeakMap<Params[], Map<string, Params[]>>()
|
|
382
|
+
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
383
|
+
* fn(params)('camelcase')
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
function memoize(store, factory) {
|
|
387
|
+
return (key) => {
|
|
388
|
+
if (store.has(key)) return store.get(key);
|
|
389
|
+
const value = factory(key);
|
|
390
|
+
store.set(key, value);
|
|
391
|
+
return value;
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Wraps a plain array in a reusable `AsyncIterable`.
|
|
396
|
+
* Each `[Symbol.asyncIterator]()` call returns a fresh generator so the
|
|
397
|
+
* iterable can be consumed multiple times (e.g. once per plugin pre-scan).
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```ts
|
|
401
|
+
* const stream = arrayToAsyncIterable([1, 2, 3])
|
|
402
|
+
* for await (const n of stream) console.log(n) // 1, 2, 3
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
function arrayToAsyncIterable(arr) {
|
|
406
|
+
return { [Symbol.asyncIterator]() {
|
|
407
|
+
return (async function* () {
|
|
408
|
+
yield* arr;
|
|
409
|
+
})();
|
|
410
|
+
} };
|
|
411
|
+
}
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region ../../internals/utils/src/reserved.ts
|
|
414
|
+
/**
|
|
415
|
+
* JavaScript and Java reserved words.
|
|
416
|
+
* @link https://github.com/jonschlinkert/reserved/blob/master/index.js
|
|
417
|
+
*/
|
|
418
|
+
const reservedWords = new Set([
|
|
419
|
+
"abstract",
|
|
420
|
+
"arguments",
|
|
421
|
+
"boolean",
|
|
422
|
+
"break",
|
|
423
|
+
"byte",
|
|
424
|
+
"case",
|
|
425
|
+
"catch",
|
|
426
|
+
"char",
|
|
427
|
+
"class",
|
|
428
|
+
"const",
|
|
429
|
+
"continue",
|
|
430
|
+
"debugger",
|
|
431
|
+
"default",
|
|
432
|
+
"delete",
|
|
433
|
+
"do",
|
|
434
|
+
"double",
|
|
435
|
+
"else",
|
|
436
|
+
"enum",
|
|
437
|
+
"eval",
|
|
438
|
+
"export",
|
|
439
|
+
"extends",
|
|
440
|
+
"false",
|
|
441
|
+
"final",
|
|
442
|
+
"finally",
|
|
443
|
+
"float",
|
|
444
|
+
"for",
|
|
445
|
+
"function",
|
|
446
|
+
"goto",
|
|
447
|
+
"if",
|
|
448
|
+
"implements",
|
|
449
|
+
"import",
|
|
450
|
+
"in",
|
|
451
|
+
"instanceof",
|
|
452
|
+
"int",
|
|
453
|
+
"interface",
|
|
454
|
+
"let",
|
|
455
|
+
"long",
|
|
456
|
+
"native",
|
|
457
|
+
"new",
|
|
458
|
+
"null",
|
|
459
|
+
"package",
|
|
460
|
+
"private",
|
|
461
|
+
"protected",
|
|
462
|
+
"public",
|
|
463
|
+
"return",
|
|
464
|
+
"short",
|
|
465
|
+
"static",
|
|
466
|
+
"super",
|
|
467
|
+
"switch",
|
|
468
|
+
"synchronized",
|
|
469
|
+
"this",
|
|
470
|
+
"throw",
|
|
471
|
+
"throws",
|
|
472
|
+
"transient",
|
|
473
|
+
"true",
|
|
474
|
+
"try",
|
|
475
|
+
"typeof",
|
|
476
|
+
"var",
|
|
477
|
+
"void",
|
|
478
|
+
"volatile",
|
|
479
|
+
"while",
|
|
480
|
+
"with",
|
|
481
|
+
"yield",
|
|
482
|
+
"Array",
|
|
483
|
+
"Date",
|
|
484
|
+
"hasOwnProperty",
|
|
485
|
+
"Infinity",
|
|
486
|
+
"isFinite",
|
|
487
|
+
"isNaN",
|
|
488
|
+
"isPrototypeOf",
|
|
489
|
+
"length",
|
|
490
|
+
"Math",
|
|
491
|
+
"name",
|
|
492
|
+
"NaN",
|
|
493
|
+
"Number",
|
|
494
|
+
"Object",
|
|
495
|
+
"prototype",
|
|
496
|
+
"String",
|
|
497
|
+
"toString",
|
|
498
|
+
"undefined",
|
|
499
|
+
"valueOf"
|
|
500
|
+
]);
|
|
501
|
+
/**
|
|
502
|
+
* Returns `true` when `name` is a syntactically valid JavaScript variable name.
|
|
503
|
+
*
|
|
504
|
+
* @example
|
|
505
|
+
* ```ts
|
|
506
|
+
* isValidVarName('status') // true
|
|
507
|
+
* isValidVarName('class') // false (reserved word)
|
|
508
|
+
* isValidVarName('42foo') // false (starts with digit)
|
|
509
|
+
* ```
|
|
510
|
+
*/
|
|
511
|
+
function isValidVarName(name) {
|
|
512
|
+
if (!name || reservedWords.has(name)) return false;
|
|
513
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
514
|
+
}
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region ../../internals/utils/src/urlPath.ts
|
|
517
|
+
/**
|
|
518
|
+
* Parses and transforms an OpenAPI/Swagger path string into various URL formats.
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* const p = new URLPath('/pet/{petId}')
|
|
522
|
+
* p.URL // '/pet/:petId'
|
|
523
|
+
* p.template // '`/pet/${petId}`'
|
|
524
|
+
*/
|
|
525
|
+
var URLPath = class {
|
|
526
|
+
/**
|
|
527
|
+
* The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
|
|
528
|
+
*/
|
|
529
|
+
path;
|
|
530
|
+
#options;
|
|
531
|
+
constructor(path, options = {}) {
|
|
532
|
+
this.path = path;
|
|
533
|
+
this.#options = options;
|
|
534
|
+
}
|
|
535
|
+
/** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* ```ts
|
|
539
|
+
* new URLPath('/pet/{petId}').URL // '/pet/:petId'
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
542
|
+
get URL() {
|
|
543
|
+
return this.toURLPath();
|
|
544
|
+
}
|
|
545
|
+
/** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```ts
|
|
549
|
+
* new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
|
|
550
|
+
* new URLPath('/pet/{petId}').isURL // false
|
|
551
|
+
* ```
|
|
552
|
+
*/
|
|
553
|
+
get isURL() {
|
|
554
|
+
try {
|
|
555
|
+
return !!new URL(this.path).href;
|
|
556
|
+
} catch {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Converts the OpenAPI path to a TypeScript template literal string.
|
|
562
|
+
*
|
|
563
|
+
* @example
|
|
564
|
+
* new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
|
|
565
|
+
* new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
|
|
566
|
+
*/
|
|
567
|
+
get template() {
|
|
568
|
+
return this.toTemplateString();
|
|
569
|
+
}
|
|
570
|
+
/** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* ```ts
|
|
574
|
+
* new URLPath('/pet/{petId}').object
|
|
575
|
+
* // { url: '/pet/:petId', params: { petId: 'petId' } }
|
|
576
|
+
* ```
|
|
577
|
+
*/
|
|
578
|
+
get object() {
|
|
579
|
+
return this.toObject();
|
|
580
|
+
}
|
|
581
|
+
/** Returns a map of path parameter names, or `undefined` when the path has no parameters.
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* ```ts
|
|
585
|
+
* new URLPath('/pet/{petId}').params // { petId: 'petId' }
|
|
586
|
+
* new URLPath('/pet').params // undefined
|
|
587
|
+
* ```
|
|
588
|
+
*/
|
|
589
|
+
get params() {
|
|
590
|
+
return this.getParams();
|
|
591
|
+
}
|
|
592
|
+
#transformParam(raw) {
|
|
593
|
+
const param = isValidVarName(raw) ? raw : camelCase(raw);
|
|
594
|
+
return this.#options.casing === "camelcase" ? camelCase(param) : param;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
|
|
598
|
+
*/
|
|
599
|
+
#eachParam(fn) {
|
|
600
|
+
for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
|
|
601
|
+
const raw = match[1];
|
|
602
|
+
fn(raw, this.#transformParam(raw));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
toObject({ type = "path", replacer, stringify } = {}) {
|
|
606
|
+
const object = {
|
|
607
|
+
url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
|
|
608
|
+
params: this.getParams()
|
|
609
|
+
};
|
|
610
|
+
if (stringify) {
|
|
611
|
+
if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
|
|
612
|
+
if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
|
|
613
|
+
return `{ url: '${object.url}' }`;
|
|
614
|
+
}
|
|
615
|
+
return object;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Converts the OpenAPI path to a TypeScript template literal string.
|
|
619
|
+
* An optional `replacer` can transform each extracted parameter name before interpolation.
|
|
620
|
+
*
|
|
621
|
+
* @example
|
|
622
|
+
* new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
|
|
623
|
+
*/
|
|
624
|
+
toTemplateString({ prefix, replacer } = {}) {
|
|
625
|
+
const result = this.path.split(/\{([^}]+)\}/).map((part, i) => {
|
|
626
|
+
if (i % 2 === 0) return part;
|
|
627
|
+
const param = this.#transformParam(part);
|
|
628
|
+
return `\${${replacer ? replacer(param) : param}}`;
|
|
629
|
+
}).join("");
|
|
630
|
+
return `\`${prefix ?? ""}${result}\``;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Extracts all `{param}` segments from the path and returns them as a key-value map.
|
|
634
|
+
* An optional `replacer` transforms each parameter name in both key and value positions.
|
|
635
|
+
* Returns `undefined` when no path parameters are found.
|
|
636
|
+
*
|
|
637
|
+
* @example
|
|
638
|
+
* ```ts
|
|
639
|
+
* new URLPath('/pet/{petId}/tag/{tagId}').getParams()
|
|
640
|
+
* // { petId: 'petId', tagId: 'tagId' }
|
|
641
|
+
* ```
|
|
642
|
+
*/
|
|
643
|
+
getParams(replacer) {
|
|
644
|
+
const params = {};
|
|
645
|
+
this.#eachParam((_raw, param) => {
|
|
646
|
+
const key = replacer ? replacer(param) : param;
|
|
647
|
+
params[key] = key;
|
|
648
|
+
});
|
|
649
|
+
return Object.keys(params).length > 0 ? params : void 0;
|
|
650
|
+
}
|
|
651
|
+
/** Converts the OpenAPI path to Express-style colon syntax.
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```ts
|
|
655
|
+
* new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
|
|
656
|
+
* ```
|
|
657
|
+
*/
|
|
658
|
+
toURLPath() {
|
|
659
|
+
return this.path.replace(/\{([^}]+)\}/g, ":$1");
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region package.json
|
|
664
|
+
var version = "5.0.0-beta.40";
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region src/constants.ts
|
|
667
|
+
/**
|
|
668
|
+
* OpenTelemetry ingestion endpoint for anonymous usage telemetry.
|
|
669
|
+
*/
|
|
670
|
+
const OTLP_ENDPOINT = "https://otlp.kubb.dev";
|
|
671
|
+
/**
|
|
672
|
+
* Plugin `include` filter types that select operations directly. When one of these is set
|
|
673
|
+
* without a `schemaName` include, the generate phase pre-scans operations to compute the set
|
|
674
|
+
* of schemas they reach, so unreachable schemas can be pruned for that plugin.
|
|
675
|
+
*/
|
|
676
|
+
const OPERATION_FILTER_TYPES = new Set([
|
|
677
|
+
"tag",
|
|
678
|
+
"operationId",
|
|
679
|
+
"path",
|
|
680
|
+
"method",
|
|
681
|
+
"contentType"
|
|
682
|
+
]);
|
|
683
|
+
/**
|
|
684
|
+
* Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode
|
|
685
|
+
* and stays stable so it can be referenced in tooling and (later) docs. Reference
|
|
686
|
+
* these instead of inlining the string at a throw site.
|
|
687
|
+
*/
|
|
688
|
+
const diagnosticCode = {
|
|
689
|
+
/**
|
|
690
|
+
* Fallback for an unstructured error with no specific code.
|
|
691
|
+
*/
|
|
692
|
+
unknown: "KUBB_UNKNOWN",
|
|
693
|
+
/**
|
|
694
|
+
* The `input.path` file or URL could not be read.
|
|
695
|
+
*/
|
|
696
|
+
inputNotFound: "KUBB_INPUT_NOT_FOUND",
|
|
697
|
+
/**
|
|
698
|
+
* An adapter was configured without an `input`.
|
|
699
|
+
*/
|
|
700
|
+
inputRequired: "KUBB_INPUT_REQUIRED",
|
|
701
|
+
/**
|
|
702
|
+
* A `$ref` (or equivalent reference) could not be resolved in the source document.
|
|
703
|
+
*/
|
|
704
|
+
refNotFound: "KUBB_REF_NOT_FOUND",
|
|
705
|
+
/**
|
|
706
|
+
* A server variable value is not allowed by its `enum`.
|
|
707
|
+
*/
|
|
708
|
+
invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE",
|
|
709
|
+
/**
|
|
710
|
+
* A required plugin is missing from the config.
|
|
711
|
+
*/
|
|
712
|
+
pluginNotFound: "KUBB_PLUGIN_NOT_FOUND",
|
|
713
|
+
/**
|
|
714
|
+
* A plugin threw while generating.
|
|
715
|
+
*/
|
|
716
|
+
pluginFailed: "KUBB_PLUGIN_FAILED",
|
|
717
|
+
/**
|
|
718
|
+
* A plugin reported a non-fatal warning through `ctx.warn`.
|
|
719
|
+
*/
|
|
720
|
+
pluginWarning: "KUBB_PLUGIN_WARNING",
|
|
721
|
+
/**
|
|
722
|
+
* A plugin reported an informational message through `ctx.info`.
|
|
723
|
+
*/
|
|
724
|
+
pluginInfo: "KUBB_PLUGIN_INFO",
|
|
725
|
+
/**
|
|
726
|
+
* A schema uses a `format` Kubb does not map to a specific type. Reserved for
|
|
727
|
+
* adapters to emit as a `warning`.
|
|
728
|
+
*/
|
|
729
|
+
unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT",
|
|
730
|
+
/**
|
|
731
|
+
* A referenced schema or operation is marked `deprecated`. Reserved for adapters
|
|
732
|
+
* to emit as an `info`.
|
|
733
|
+
*/
|
|
734
|
+
deprecated: "KUBB_DEPRECATED",
|
|
735
|
+
/**
|
|
736
|
+
* An adapter is required but the config has none. The build cannot read the input
|
|
737
|
+
* without one.
|
|
738
|
+
*/
|
|
739
|
+
adapterRequired: "KUBB_ADAPTER_REQUIRED",
|
|
740
|
+
/**
|
|
741
|
+
* A resolved output path escapes the output directory, which can stem from a path
|
|
742
|
+
* traversal in the spec or a misconfigured `group.name`.
|
|
743
|
+
*/
|
|
744
|
+
pathTraversal: "KUBB_PATH_TRAVERSAL",
|
|
745
|
+
/**
|
|
746
|
+
* A post-generate shell hook (`hooks.done`) exited with a failure.
|
|
747
|
+
*/
|
|
748
|
+
hookFailed: "KUBB_HOOK_FAILED",
|
|
749
|
+
/**
|
|
750
|
+
* The formatter pass over the generated files failed.
|
|
751
|
+
*/
|
|
752
|
+
formatFailed: "KUBB_FORMAT_FAILED",
|
|
753
|
+
/**
|
|
754
|
+
* The linter pass over the generated files failed.
|
|
755
|
+
*/
|
|
756
|
+
lintFailed: "KUBB_LINT_FAILED",
|
|
757
|
+
/**
|
|
758
|
+
* Not a failure. Carries a plugin's elapsed time, summed into the run total.
|
|
759
|
+
*/
|
|
760
|
+
performance: "KUBB_PERFORMANCE",
|
|
761
|
+
/**
|
|
762
|
+
* Not a failure. A newer Kubb version is available on npm.
|
|
763
|
+
*/
|
|
764
|
+
updateAvailable: "KUBB_UPDATE_AVAILABLE"
|
|
765
|
+
};
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region src/diagnostics.ts
|
|
768
|
+
/**
|
|
769
|
+
* Docs major, derived from the package version so the link tracks the published major.
|
|
770
|
+
*/
|
|
771
|
+
const docsMajor = version.split(".")[0] ?? "5";
|
|
772
|
+
/**
|
|
773
|
+
* Narrows a {@link Diagnostic} to the variant for `code`, or `null` when it does not match.
|
|
774
|
+
*
|
|
775
|
+
* @example
|
|
776
|
+
* ```ts
|
|
777
|
+
* const update = narrow(diagnostic, diagnosticCode.updateAvailable)
|
|
778
|
+
* if (update) {
|
|
779
|
+
* console.log(update.latestVersion)
|
|
780
|
+
* }
|
|
781
|
+
* ```
|
|
782
|
+
*/
|
|
783
|
+
function narrow(diagnostic, code) {
|
|
784
|
+
return diagnostic.code === code ? diagnostic : null;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Builds a type guard that narrows a {@link Diagnostic} to the variant for `kind`. A diagnostic
|
|
788
|
+
* with no `kind` is treated as a `problem`.
|
|
789
|
+
*/
|
|
790
|
+
function isKind(kind) {
|
|
791
|
+
return (diagnostic) => (diagnostic.kind ?? "problem") === kind;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Returns `true` when the diagnostic is a build {@link ProblemDiagnostic}.
|
|
795
|
+
*
|
|
796
|
+
* @example
|
|
797
|
+
* ```ts
|
|
798
|
+
* if (isProblem(diagnostic)) {
|
|
799
|
+
* console.log(diagnostic.location)
|
|
800
|
+
* }
|
|
801
|
+
* ```
|
|
802
|
+
*/
|
|
803
|
+
const isProblem = isKind("problem");
|
|
804
|
+
/**
|
|
805
|
+
* Returns `true` when the diagnostic is a per-plugin {@link PerformanceDiagnostic}.
|
|
806
|
+
*
|
|
807
|
+
* @example
|
|
808
|
+
* ```ts
|
|
809
|
+
* const timings = diagnostics.filter(isPerformance)
|
|
810
|
+
* ```
|
|
811
|
+
*/
|
|
812
|
+
const isPerformance = isKind("performance");
|
|
813
|
+
/**
|
|
814
|
+
* Returns `true` when the diagnostic is a version-update {@link UpdateDiagnostic}.
|
|
815
|
+
*
|
|
816
|
+
* @example
|
|
817
|
+
* ```ts
|
|
818
|
+
* if (isUpdate(diagnostic)) {
|
|
819
|
+
* console.log(diagnostic.latestVersion)
|
|
820
|
+
* }
|
|
821
|
+
* ```
|
|
822
|
+
*/
|
|
823
|
+
const isUpdate = isKind("update");
|
|
824
|
+
/**
|
|
825
|
+
* Glyph and accent color per severity, matching the miette/oxlint convention
|
|
826
|
+
* (`×` error, `⚠` warning, `ℹ` advice).
|
|
827
|
+
*/
|
|
828
|
+
const severityStyle = {
|
|
829
|
+
error: {
|
|
830
|
+
glyph: "×",
|
|
831
|
+
color: "red"
|
|
832
|
+
},
|
|
833
|
+
warning: {
|
|
834
|
+
glyph: "⚠",
|
|
835
|
+
color: "yellow"
|
|
836
|
+
},
|
|
837
|
+
info: {
|
|
838
|
+
glyph: "ℹ",
|
|
839
|
+
color: "blue"
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
/**
|
|
843
|
+
* Explanation for every {@link diagnosticCode}. Use {@link Diagnostics.explain} to look one up
|
|
844
|
+
* and `Diagnostics.docsUrl` for the matching kubb.dev page.
|
|
845
|
+
*/
|
|
846
|
+
const diagnosticCatalog = {
|
|
847
|
+
[diagnosticCode.unknown]: {
|
|
848
|
+
title: "Unknown error",
|
|
849
|
+
cause: "An error was thrown without a stable Kubb code, so it is reported as-is.",
|
|
850
|
+
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."
|
|
851
|
+
},
|
|
852
|
+
[diagnosticCode.inputNotFound]: {
|
|
853
|
+
title: "Input not found",
|
|
854
|
+
cause: "The file or URL set in `input.path` (or passed as `kubb generate PATH`) could not be read.",
|
|
855
|
+
fix: "Check that the path or URL exists and is readable, then set it in `input.path` or pass it on the CLI."
|
|
856
|
+
},
|
|
857
|
+
[diagnosticCode.inputRequired]: {
|
|
858
|
+
title: "Input required",
|
|
859
|
+
cause: "An adapter is configured but no `input` was provided.",
|
|
860
|
+
fix: "Set `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config."
|
|
861
|
+
},
|
|
862
|
+
[diagnosticCode.refNotFound]: {
|
|
863
|
+
title: "Reference not found",
|
|
864
|
+
cause: "A `$ref` could not be resolved in the source document.",
|
|
865
|
+
fix: "Add the missing definition (for example under `components.schemas`) or fix the `$ref`. Run `kubb validate` to check the spec."
|
|
866
|
+
},
|
|
867
|
+
[diagnosticCode.invalidServerVariable]: {
|
|
868
|
+
title: "Invalid server variable",
|
|
869
|
+
cause: "A server variable value is not allowed by its `enum`.",
|
|
870
|
+
fix: "Use one of the values listed in the server variable `enum`, or update the spec."
|
|
871
|
+
},
|
|
872
|
+
[diagnosticCode.pluginNotFound]: {
|
|
873
|
+
title: "Plugin not found",
|
|
874
|
+
cause: "A plugin that another plugin depends on is missing from the config.",
|
|
875
|
+
fix: "Add the required plugin to the `plugins` array in kubb.config.ts, or remove the dependency on it."
|
|
876
|
+
},
|
|
877
|
+
[diagnosticCode.pluginFailed]: {
|
|
878
|
+
title: "Plugin failed",
|
|
879
|
+
cause: "A plugin threw while generating, or reported an error through `ctx.error`.",
|
|
880
|
+
fix: "Read the underlying error and check the plugin options and the schema or operation it failed on."
|
|
881
|
+
},
|
|
882
|
+
[diagnosticCode.pluginWarning]: {
|
|
883
|
+
title: "Plugin warning",
|
|
884
|
+
cause: "A plugin reported a non-fatal warning through `ctx.warn`.",
|
|
885
|
+
fix: "Review the message. It does not fail the build; adjust the plugin options or input if the warning is unwanted."
|
|
886
|
+
},
|
|
887
|
+
[diagnosticCode.pluginInfo]: {
|
|
888
|
+
title: "Plugin info",
|
|
889
|
+
cause: "A plugin reported an informational message through `ctx.info`.",
|
|
890
|
+
fix: "Informational only. No action is required."
|
|
891
|
+
},
|
|
892
|
+
[diagnosticCode.unsupportedFormat]: {
|
|
893
|
+
title: "Unsupported format",
|
|
894
|
+
cause: "A schema uses a `format` Kubb does not map to a specific type, so it falls back to the base type.",
|
|
895
|
+
fix: "Use a format Kubb supports, or handle the custom format with a parser or plugin."
|
|
896
|
+
},
|
|
897
|
+
[diagnosticCode.deprecated]: {
|
|
898
|
+
title: "Deprecated",
|
|
899
|
+
cause: "A referenced schema or operation is marked `deprecated`.",
|
|
900
|
+
fix: "Migrate off the deprecated definition if the warning is unwanted."
|
|
901
|
+
},
|
|
902
|
+
[diagnosticCode.adapterRequired]: {
|
|
903
|
+
title: "Adapter required",
|
|
904
|
+
cause: "An action needs an adapter but none is configured.",
|
|
905
|
+
fix: "Set `adapter` in kubb.config.ts, for example `adapterOas()`."
|
|
906
|
+
},
|
|
907
|
+
[diagnosticCode.pathTraversal]: {
|
|
908
|
+
title: "Path traversal",
|
|
909
|
+
cause: "A resolved output path escaped the output directory, which can stem from a path traversal in the spec or a misconfigured `group.name`.",
|
|
910
|
+
fix: "Keep generated paths within the output directory. Review the `group.name` function and the names coming from the spec."
|
|
911
|
+
},
|
|
912
|
+
[diagnosticCode.hookFailed]: {
|
|
913
|
+
title: "Hook failed",
|
|
914
|
+
cause: "A post-generate shell hook (`hooks.done`) exited with a non-zero status.",
|
|
915
|
+
fix: "Check the command is installed and correct, and run it manually to see the error."
|
|
916
|
+
},
|
|
917
|
+
[diagnosticCode.formatFailed]: {
|
|
918
|
+
title: "Format failed",
|
|
919
|
+
cause: "The formatter pass over the generated files failed.",
|
|
920
|
+
fix: "Check the formatter (oxfmt, biome, or prettier) is installed and its config is valid, then run it manually on the output."
|
|
921
|
+
},
|
|
922
|
+
[diagnosticCode.lintFailed]: {
|
|
923
|
+
title: "Lint failed",
|
|
924
|
+
cause: "The linter pass over the generated files failed.",
|
|
925
|
+
fix: "Check the linter (oxlint, biome, or eslint) is installed and its config is valid, then run it manually on the output."
|
|
926
|
+
},
|
|
927
|
+
[diagnosticCode.performance]: {
|
|
928
|
+
title: "Performance",
|
|
929
|
+
cause: "Not a failure. Records a plugin’s elapsed time, summed into the run total.",
|
|
930
|
+
fix: "No action. This is an informational metric."
|
|
931
|
+
},
|
|
932
|
+
[diagnosticCode.updateAvailable]: {
|
|
933
|
+
title: "Update available",
|
|
934
|
+
cause: "A newer Kubb version is published on npm than the one running.",
|
|
935
|
+
fix: "Update the `@kubb/*` packages, for example `npm install -g @kubb/cli`, to get the latest fixes."
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
/**
|
|
939
|
+
* Static helpers for working with {@link Diagnostic}s, plus the run-scoped sink
|
|
940
|
+
* that lets deep code report a diagnostic without threading a callback.
|
|
941
|
+
*
|
|
942
|
+
* The sink lives in a single `AsyncLocalStorage` in the `@kubb/core` bundle.
|
|
943
|
+
* `Diagnostics.scope` activates it for a run, so anything inside that run (the
|
|
944
|
+
* adapter parse, a lazily consumed stream, a generator) reports through
|
|
945
|
+
* `Diagnostics.report` and lands in the same run.
|
|
946
|
+
*/
|
|
947
|
+
var Diagnostics = class Diagnostics {
|
|
948
|
+
static #reporterStorage = new node_async_hooks.AsyncLocalStorage();
|
|
949
|
+
/**
|
|
950
|
+
* The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
|
|
951
|
+
*/
|
|
952
|
+
static code = diagnosticCode;
|
|
953
|
+
/**
|
|
954
|
+
* Type guard for a build {@link ProblemDiagnostic}.
|
|
955
|
+
*/
|
|
956
|
+
static isProblem = isProblem;
|
|
957
|
+
/**
|
|
958
|
+
* Type guard for a version-update {@link UpdateDiagnostic}.
|
|
959
|
+
*/
|
|
960
|
+
static isUpdate = isUpdate;
|
|
961
|
+
/**
|
|
962
|
+
* Type guard for a per-plugin {@link PerformanceDiagnostic}.
|
|
963
|
+
*/
|
|
964
|
+
static isPerformance = isPerformance;
|
|
965
|
+
/**
|
|
966
|
+
* Narrows a {@link Diagnostic} to the variant for `code`, or `null` when it does not match.
|
|
967
|
+
*/
|
|
968
|
+
static narrow = narrow;
|
|
969
|
+
/**
|
|
970
|
+
* An `Error` that carries a {@link Diagnostic}, so structured problems can flow
|
|
971
|
+
* through the existing throw/catch paths while keeping their code and location.
|
|
972
|
+
*
|
|
973
|
+
* @example
|
|
974
|
+
* ```ts
|
|
975
|
+
* throw new Diagnostics.Error({ code: diagnosticCode.refNotFound, severity: 'error', message: `Could not find ${ref}`, location: { kind: 'schema', pointer: ref, ref } })
|
|
976
|
+
* ```
|
|
977
|
+
*/
|
|
978
|
+
static Error = class DiagnosticError extends Error {
|
|
979
|
+
diagnostic;
|
|
980
|
+
constructor(diagnostic) {
|
|
981
|
+
super(diagnostic.message, { cause: diagnostic.cause });
|
|
982
|
+
this.name = "DiagnosticError";
|
|
983
|
+
this.diagnostic = diagnostic;
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
/**
|
|
987
|
+
* Structural check for a {@link Diagnostics.Error}, including one thrown from a duplicated
|
|
988
|
+
* `@kubb/core` copy where `instanceof` fails. Matches on the `name` and a `diagnostic`
|
|
989
|
+
* that carries a `code`.
|
|
990
|
+
*/
|
|
991
|
+
static isError(error) {
|
|
992
|
+
if (error instanceof Diagnostics.Error) return true;
|
|
993
|
+
return error instanceof Error && error.name === "DiagnosticError" && "diagnostic" in error && typeof error.diagnostic === "object" && error.diagnostic !== null && typeof error.diagnostic?.code === "string";
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Runs `fn` with `sink` as the active diagnostic sink for the whole async
|
|
997
|
+
* subtree, so {@link Diagnostics.report} reaches it from anywhere inside.
|
|
998
|
+
*/
|
|
999
|
+
static scope(sink, fn) {
|
|
1000
|
+
return Diagnostics.#reporterStorage.run(sink, fn);
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Collects a diagnostic into the active build via the run-scoped sink, without throwing.
|
|
1004
|
+
* Returns `true` when a run consumed it, `false` when called outside a {@link Diagnostics.scope}
|
|
1005
|
+
* (so callers can fall back to throwing). Use a `warning`/`info` severity for non-fatal issues.
|
|
1006
|
+
* For rendering a diagnostic live on the hook bus, use {@link Diagnostics.emit} instead.
|
|
1007
|
+
*/
|
|
1008
|
+
static report(diagnostic) {
|
|
1009
|
+
const sink = Diagnostics.#reporterStorage.getStore();
|
|
1010
|
+
if (!sink) return false;
|
|
1011
|
+
sink(diagnostic);
|
|
1012
|
+
return true;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Emits a diagnostic on the run's `kubb:diagnostic` event so the loggers render it live.
|
|
1016
|
+
* Use it instead of calling `hooks.emit('kubb:diagnostic', ...)` directly. To collect a
|
|
1017
|
+
* diagnostic into the build result from deep in a run, use {@link Diagnostics.report} instead.
|
|
1018
|
+
*/
|
|
1019
|
+
static async emit(hooks, diagnostic) {
|
|
1020
|
+
await hooks.emit("kubb:diagnostic", { diagnostic });
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Coerces any thrown value into a {@link ProblemDiagnostic}. A {@link Diagnostics.Error}
|
|
1024
|
+
* keeps its structured data, and anything else becomes a `KUBB_UNKNOWN` error.
|
|
1025
|
+
*/
|
|
1026
|
+
static from(error) {
|
|
1027
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1028
|
+
let current = error;
|
|
1029
|
+
let root;
|
|
1030
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
1031
|
+
if (Diagnostics.isError(current)) return current.diagnostic;
|
|
1032
|
+
seen.add(current);
|
|
1033
|
+
root = current;
|
|
1034
|
+
current = current.cause;
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
code: diagnosticCode.unknown,
|
|
1038
|
+
severity: "error",
|
|
1039
|
+
message: root ? root.message : getErrorMessage(error),
|
|
1040
|
+
cause: root
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Builds a per-plugin performance record. Reporters sum these into the run total.
|
|
1045
|
+
*/
|
|
1046
|
+
static performance({ plugin, duration }) {
|
|
1047
|
+
return {
|
|
1048
|
+
kind: "performance",
|
|
1049
|
+
code: diagnosticCode.performance,
|
|
1050
|
+
severity: "info",
|
|
1051
|
+
message: `${plugin} generated in ${Math.round(duration)}ms`,
|
|
1052
|
+
plugin,
|
|
1053
|
+
duration
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Builds the version-update notice shown when a newer Kubb is published on npm.
|
|
1058
|
+
*/
|
|
1059
|
+
static update({ currentVersion, latestVersion }) {
|
|
1060
|
+
return {
|
|
1061
|
+
kind: "update",
|
|
1062
|
+
code: diagnosticCode.updateAvailable,
|
|
1063
|
+
severity: "info",
|
|
1064
|
+
message: `Update available: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`,
|
|
1065
|
+
currentVersion,
|
|
1066
|
+
latestVersion
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* True when any diagnostic is an error, the severity that fails a build. Non-error
|
|
1071
|
+
* diagnostics are ignored.
|
|
1072
|
+
*/
|
|
1073
|
+
static hasError(diagnostics) {
|
|
1074
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Names of the plugins that failed, deduped, derived from the error diagnostics
|
|
1078
|
+
* that carry a `plugin`.
|
|
1079
|
+
*/
|
|
1080
|
+
static failedPlugins(diagnostics) {
|
|
1081
|
+
const names = /* @__PURE__ */ new Set();
|
|
1082
|
+
for (const diagnostic of diagnostics) if (diagnostic.severity === "error" && diagnostic.plugin) names.add(diagnostic.plugin);
|
|
1083
|
+
return [...names];
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Counts `problem` diagnostics by severity for the run summary. `timing`
|
|
1087
|
+
* diagnostics are ignored.
|
|
1088
|
+
*/
|
|
1089
|
+
static count(diagnostics) {
|
|
1090
|
+
let errors = 0;
|
|
1091
|
+
let warnings = 0;
|
|
1092
|
+
let infos = 0;
|
|
1093
|
+
for (const diagnostic of diagnostics) {
|
|
1094
|
+
if (!isProblem(diagnostic)) continue;
|
|
1095
|
+
if (diagnostic.severity === "error") errors += 1;
|
|
1096
|
+
else if (diagnostic.severity === "warning") warnings += 1;
|
|
1097
|
+
else infos += 1;
|
|
1098
|
+
}
|
|
1099
|
+
return {
|
|
1100
|
+
errors,
|
|
1101
|
+
warnings,
|
|
1102
|
+
infos
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Drops duplicate `problem` diagnostics that share a code, location pointer, and
|
|
1107
|
+
* plugin, so the same issue reported across several passes is shown once. Non-problem
|
|
1108
|
+
* diagnostics are always kept.
|
|
1109
|
+
*/
|
|
1110
|
+
static dedupe(diagnostics) {
|
|
1111
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1112
|
+
const result = [];
|
|
1113
|
+
for (const diagnostic of diagnostics) {
|
|
1114
|
+
if (!isProblem(diagnostic)) {
|
|
1115
|
+
result.push(diagnostic);
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
const pointer = diagnostic.location && "pointer" in diagnostic.location ? diagnostic.location.pointer : "";
|
|
1119
|
+
const key = `${diagnostic.code} ${pointer} ${diagnostic.plugin ?? ""}`;
|
|
1120
|
+
if (seen.has(key)) continue;
|
|
1121
|
+
seen.add(key);
|
|
1122
|
+
result.push(diagnostic);
|
|
1123
|
+
}
|
|
1124
|
+
return result;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Builds the kubb.dev docs URL for a diagnostic code, e.g.
|
|
1128
|
+
* `KUBB_REF_NOT_FOUND` → `https://kubb.dev/docs/5.x/reference/diagnostics/kubb-ref-not-found`.
|
|
1129
|
+
*/
|
|
1130
|
+
static docsUrl(code) {
|
|
1131
|
+
return `https://kubb.dev/docs/${docsMajor}.x/reference/diagnostics/${code.toLowerCase().replaceAll("_", "-")}`;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* The catalog entry for a code: its title, cause, and fix. Mirrors the kubb.dev
|
|
1135
|
+
* `/diagnostics/<slug>` page.
|
|
1136
|
+
*/
|
|
1137
|
+
static explain(code) {
|
|
1138
|
+
return diagnosticCatalog[code];
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Reduces a diagnostic to its JSON-safe fields plus a `docsUrl`, for machine-readable
|
|
1142
|
+
* consumers. The `cause`, `kind`, and `duration` are dropped, and absent optional
|
|
1143
|
+
* fields are omitted rather than set to `undefined`.
|
|
1144
|
+
*/
|
|
1145
|
+
static serialize(diagnostic) {
|
|
1146
|
+
const problem = isProblem(diagnostic) ? diagnostic : void 0;
|
|
1147
|
+
return {
|
|
1148
|
+
code: diagnostic.code,
|
|
1149
|
+
severity: diagnostic.severity,
|
|
1150
|
+
message: diagnostic.message,
|
|
1151
|
+
...problem?.location ? { location: problem.location } : {},
|
|
1152
|
+
...problem?.help ? { help: problem.help } : {},
|
|
1153
|
+
...problem?.plugin ? { plugin: problem.plugin } : {},
|
|
1154
|
+
...diagnostic.code === diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Renders a {@link Diagnostic} for terminal output as its parts: the colored severity `symbol`
|
|
1159
|
+
* (the gutter glyph), the `plugin(CODE): message` `headline`, and the `details` lines (optional
|
|
1160
|
+
* `at <pointer>`, `help:`, and `docs:`).
|
|
1161
|
+
*
|
|
1162
|
+
* Hosts compose these to fit their gutter: a clack logger passes `symbol` as its own gutter and
|
|
1163
|
+
* `[headline, ...details]` as the message, while plain text outputs use {@link Diagnostics.formatLines}.
|
|
1164
|
+
*/
|
|
1165
|
+
static format(diagnostic) {
|
|
1166
|
+
const { code, severity, message } = diagnostic;
|
|
1167
|
+
const { glyph, color } = severityStyle[severity];
|
|
1168
|
+
const problem = isProblem(diagnostic) ? diagnostic : void 0;
|
|
1169
|
+
const rule = (0, node_util.styleText)(color, (0, node_util.styleText)("bold", problem?.plugin ? `${problem.plugin}(${code})` : code));
|
|
1170
|
+
const details = [];
|
|
1171
|
+
if (problem?.location && "pointer" in problem.location) details.push(` ${(0, node_util.styleText)("dim", "at")} ${(0, node_util.styleText)("cyan", problem.location.pointer)}`);
|
|
1172
|
+
if (problem?.help) details.push(` ${(0, node_util.styleText)("cyan", "help:")} ${problem.help}`);
|
|
1173
|
+
if (code !== diagnosticCode.unknown) details.push(` ${(0, node_util.styleText)("dim", "docs:")} ${(0, node_util.styleText)("cyan", Diagnostics.docsUrl(code))}`);
|
|
1174
|
+
return {
|
|
1175
|
+
symbol: (0, node_util.styleText)(color, (0, node_util.styleText)("bold", glyph)),
|
|
1176
|
+
headline: `${rule}: ${message}`,
|
|
1177
|
+
details
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* The self-contained block form of {@link Diagnostics.format}: `${symbol} ${headline}` followed by
|
|
1182
|
+
* the detail lines. Used where there is no gutter to own the symbol (plain and file output).
|
|
1183
|
+
*/
|
|
1184
|
+
static formatLines(diagnostic) {
|
|
1185
|
+
const { symbol, headline, details } = Diagnostics.format(diagnostic);
|
|
1186
|
+
return [`${symbol} ${headline}`, ...details];
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
//#endregion
|
|
1190
|
+
//#region src/createStorage.ts
|
|
1191
|
+
/**
|
|
1192
|
+
* Defines a custom storage backend. The builder receives user options and
|
|
1193
|
+
* returns a `Storage` implementation. Kubb ships with filesystem and
|
|
1194
|
+
* in-memory storages, reach for this when you need to write generated files
|
|
1195
|
+
* elsewhere (cloud storage, a database, a remote API).
|
|
1196
|
+
*
|
|
1197
|
+
* @example In-memory storage (the built-in implementation)
|
|
1198
|
+
* ```ts
|
|
1199
|
+
* import { createStorage } from '@kubb/core'
|
|
1200
|
+
*
|
|
1201
|
+
* export const memoryStorage = createStorage(() => {
|
|
1202
|
+
* const store = new Map<string, string>()
|
|
1203
|
+
*
|
|
1204
|
+
* return {
|
|
1205
|
+
* name: 'memory',
|
|
1206
|
+
* async hasItem(key) {
|
|
1207
|
+
* return store.has(key)
|
|
1208
|
+
* },
|
|
1209
|
+
* async getItem(key) {
|
|
1210
|
+
* return store.get(key) ?? null
|
|
1211
|
+
* },
|
|
1212
|
+
* async setItem(key, value) {
|
|
1213
|
+
* store.set(key, value)
|
|
1214
|
+
* },
|
|
1215
|
+
* async removeItem(key) {
|
|
1216
|
+
* store.delete(key)
|
|
1217
|
+
* },
|
|
1218
|
+
* async getKeys(base) {
|
|
1219
|
+
* const keys = [...store.keys()]
|
|
1220
|
+
* return base ? keys.filter((k) => k.startsWith(base)) : keys
|
|
1221
|
+
* },
|
|
1222
|
+
* async clear(base) {
|
|
1223
|
+
* if (!base) store.clear()
|
|
1224
|
+
* },
|
|
1225
|
+
* }
|
|
1226
|
+
* })
|
|
1227
|
+
* ```
|
|
1228
|
+
*/
|
|
1229
|
+
function createStorage(build) {
|
|
1230
|
+
return (options) => build(options ?? {});
|
|
1231
|
+
}
|
|
1232
|
+
//#endregion
|
|
1233
|
+
//#region src/definePlugin.ts
|
|
1234
|
+
/**
|
|
1235
|
+
* Wraps a plugin factory and returns a function that accepts user options and
|
|
1236
|
+
* yields a fully typed `Plugin`. Lifecycle handlers go inside a single
|
|
1237
|
+
* `hooks` object (inspired by Astro integrations).
|
|
1238
|
+
*
|
|
1239
|
+
* Pass a `PluginFactoryOptions` type parameter to get a typed `ctx` inside
|
|
1240
|
+
* `kubb:plugin:setup`. Plugin names should follow the `plugin-<feature>`
|
|
1241
|
+
* convention (`plugin-react-query`, `plugin-zod`, ...).
|
|
1242
|
+
*
|
|
1243
|
+
* @example
|
|
1244
|
+
* ```ts
|
|
1245
|
+
* import { definePlugin } from '@kubb/core'
|
|
1246
|
+
*
|
|
1247
|
+
* export const pluginTs = definePlugin((options: { prefix?: string } = {}) => ({
|
|
1248
|
+
* name: 'plugin-ts',
|
|
1249
|
+
* hooks: {
|
|
1250
|
+
* 'kubb:plugin:setup'(ctx) {
|
|
1251
|
+
* ctx.setResolver(resolverTs)
|
|
1252
|
+
* },
|
|
1253
|
+
* },
|
|
1254
|
+
* }))
|
|
1255
|
+
* ```
|
|
1256
|
+
*/
|
|
1257
|
+
function definePlugin(factory) {
|
|
1258
|
+
return (options) => factory(options ?? {});
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Detects whether an output path points at a single file (`'single'`) or a
|
|
1262
|
+
* directory (`'split'`). Decided purely from the presence of a file extension.
|
|
1263
|
+
*
|
|
1264
|
+
* @example Directory
|
|
1265
|
+
* `getMode('./types') // 'split'`
|
|
1266
|
+
*
|
|
1267
|
+
* @example Single file
|
|
1268
|
+
* `getMode('./api.ts') // 'single'`
|
|
1269
|
+
*/
|
|
1270
|
+
function getMode(fileOrFolder) {
|
|
1271
|
+
if (!fileOrFolder) return "split";
|
|
1272
|
+
return (0, node_path.extname)(fileOrFolder) ? "single" : "split";
|
|
1273
|
+
}
|
|
1274
|
+
//#endregion
|
|
1275
|
+
//#region src/defineResolver.ts
|
|
1276
|
+
/**
|
|
1277
|
+
* Merges document `meta` with per-file `file` context into the `BannerMeta` passed to a
|
|
1278
|
+
* `banner`/`footer` function. Missing fields default to empty/`false` so the object shape
|
|
1279
|
+
* is stable even when a caller (e.g. the barrel middleware) has no document metadata.
|
|
1280
|
+
*/
|
|
1281
|
+
function buildBannerMeta({ meta, file }) {
|
|
1282
|
+
return {
|
|
1283
|
+
title: meta?.title,
|
|
1284
|
+
description: meta?.description,
|
|
1285
|
+
version: meta?.version,
|
|
1286
|
+
baseURL: meta?.baseURL,
|
|
1287
|
+
circularNames: meta?.circularNames ?? [],
|
|
1288
|
+
enumNames: meta?.enumNames ?? [],
|
|
1289
|
+
filePath: file?.path ?? "",
|
|
1290
|
+
baseName: file?.baseName ?? "",
|
|
1291
|
+
isBarrel: file?.isBarrel ?? false,
|
|
1292
|
+
isAggregation: file?.isAggregation ?? false
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
const stringPatternCache = /* @__PURE__ */ new Map();
|
|
1296
|
+
function testPattern(value, pattern) {
|
|
1297
|
+
if (typeof pattern === "string") {
|
|
1298
|
+
let regex = stringPatternCache.get(pattern);
|
|
1299
|
+
if (!regex) {
|
|
1300
|
+
regex = new RegExp(pattern);
|
|
1301
|
+
stringPatternCache.set(pattern, regex);
|
|
1302
|
+
}
|
|
1303
|
+
return regex.test(value);
|
|
1304
|
+
}
|
|
1305
|
+
return value.match(pattern) !== null;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).
|
|
1309
|
+
*/
|
|
1310
|
+
function matchesOperationPattern(node, type, pattern) {
|
|
1311
|
+
if (type === "tag") return node.tags.some((tag) => testPattern(tag, pattern));
|
|
1312
|
+
if (type === "operationId") return testPattern(node.operationId, pattern);
|
|
1313
|
+
if (type === "path") return node.path !== void 0 && testPattern(node.path, pattern);
|
|
1314
|
+
if (type === "method") return node.method !== void 0 && testPattern(node.method.toLowerCase(), pattern);
|
|
1315
|
+
if (type === "contentType") return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false;
|
|
1316
|
+
return false;
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Checks if a schema matches a pattern for a given filter type (`schemaName`).
|
|
1320
|
+
*
|
|
1321
|
+
* Returns `null` when the filter type doesn't apply to schemas.
|
|
1322
|
+
*/
|
|
1323
|
+
function matchesSchemaPattern(node, type, pattern) {
|
|
1324
|
+
if (type === "schemaName") return node.name ? testPattern(node.name, pattern) : false;
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Default name resolver used by `defineResolver`.
|
|
1329
|
+
*
|
|
1330
|
+
* - `camelCase` for `function` and `file` types.
|
|
1331
|
+
* - `PascalCase` for `type`.
|
|
1332
|
+
* - `camelCase` for everything else.
|
|
1333
|
+
*/
|
|
1334
|
+
function defaultResolver(name, type) {
|
|
1335
|
+
if (type === "file" || type === "function") return camelCase(name, { isFile: type === "file" });
|
|
1336
|
+
if (type === "type") return pascalCase(name);
|
|
1337
|
+
return camelCase(name);
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Default option resolver. Applies include/exclude filters and merges matching override options.
|
|
1341
|
+
*
|
|
1342
|
+
* Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule.
|
|
1343
|
+
*
|
|
1344
|
+
* @example Include/exclude filtering
|
|
1345
|
+
* ```ts
|
|
1346
|
+
* const options = defaultResolveOptions(operationNode, {
|
|
1347
|
+
* options: { output: 'types' },
|
|
1348
|
+
* exclude: [{ type: 'tag', pattern: 'internal' }],
|
|
1349
|
+
* })
|
|
1350
|
+
* // → null when node has tag 'internal'
|
|
1351
|
+
* ```
|
|
1352
|
+
*
|
|
1353
|
+
* @example Override merging
|
|
1354
|
+
* ```ts
|
|
1355
|
+
* const options = defaultResolveOptions(operationNode, {
|
|
1356
|
+
* options: { enumType: 'asConst' },
|
|
1357
|
+
* override: [{ type: 'operationId', pattern: 'listPets', options: { enumType: 'enum' } }],
|
|
1358
|
+
* })
|
|
1359
|
+
* // → { enumType: 'enum' } when operationId matches
|
|
1360
|
+
* ```
|
|
1361
|
+
*/
|
|
1362
|
+
const resolveOptionsCache = /* @__PURE__ */ new WeakMap();
|
|
1363
|
+
function computeOptions(node, options, exclude, include, override) {
|
|
1364
|
+
if ((0, _kubb_ast.isOperationNode)(node)) {
|
|
1365
|
+
if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
|
|
1366
|
+
if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
|
|
1367
|
+
const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options;
|
|
1368
|
+
return {
|
|
1369
|
+
...options,
|
|
1370
|
+
...overrideOptions
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
if ((0, _kubb_ast.isSchemaNode)(node)) {
|
|
1374
|
+
if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null;
|
|
1375
|
+
if (include) {
|
|
1376
|
+
const applicable = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)).filter((result) => result !== null);
|
|
1377
|
+
if (applicable.length > 0 && !applicable.includes(true)) return null;
|
|
1378
|
+
}
|
|
1379
|
+
const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options;
|
|
1380
|
+
return {
|
|
1381
|
+
...options,
|
|
1382
|
+
...overrideOptions
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
return options;
|
|
1386
|
+
}
|
|
1387
|
+
function defaultResolveOptions(node, { options, exclude = [], include, override = [] }) {
|
|
1388
|
+
const optionsKey = options;
|
|
1389
|
+
let byOptions = resolveOptionsCache.get(optionsKey);
|
|
1390
|
+
if (!byOptions) {
|
|
1391
|
+
byOptions = /* @__PURE__ */ new WeakMap();
|
|
1392
|
+
resolveOptionsCache.set(optionsKey, byOptions);
|
|
1393
|
+
}
|
|
1394
|
+
const cached = byOptions.get(node);
|
|
1395
|
+
if (cached !== void 0) return cached.value;
|
|
1396
|
+
const result = computeOptions(node, options, exclude, include, override);
|
|
1397
|
+
byOptions.set(node, { value: result });
|
|
1398
|
+
return result;
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Default path resolver used by `defineResolver`.
|
|
1402
|
+
*
|
|
1403
|
+
* - Returns the output directory in `single` mode.
|
|
1404
|
+
* - Resolves into a tag- or path-based subdirectory when `group` and a `tag`/`path` value are provided.
|
|
1405
|
+
* - Falls back to a flat `output/baseName` path otherwise.
|
|
1406
|
+
*
|
|
1407
|
+
* A custom `group.name` function overrides the default subdirectory naming.
|
|
1408
|
+
* For `tag` groups the default is `${camelCase(tag)}Controller`.
|
|
1409
|
+
* For `path` groups the default is the first path segment after `/`.
|
|
1410
|
+
*
|
|
1411
|
+
* @example Flat output
|
|
1412
|
+
* ```ts
|
|
1413
|
+
* defaultResolvePath({ baseName: 'petTypes.ts' }, { root: '/src', output: { path: 'types' } })
|
|
1414
|
+
* // → '/src/types/petTypes.ts'
|
|
1415
|
+
* ```
|
|
1416
|
+
*
|
|
1417
|
+
* @example Tag-based grouping
|
|
1418
|
+
* ```ts
|
|
1419
|
+
* defaultResolvePath(
|
|
1420
|
+
* { baseName: 'petTypes.ts', tag: 'pets' },
|
|
1421
|
+
* { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
|
|
1422
|
+
* )
|
|
1423
|
+
* // → '/src/types/petsController/petTypes.ts'
|
|
1424
|
+
* ```
|
|
1425
|
+
*
|
|
1426
|
+
* @example Path-based grouping
|
|
1427
|
+
* ```ts
|
|
1428
|
+
* defaultResolvePath(
|
|
1429
|
+
* { baseName: 'petTypes.ts', path: '/pets/list' },
|
|
1430
|
+
* { root: '/src', output: { path: 'types' }, group: { type: 'path' } },
|
|
1431
|
+
* )
|
|
1432
|
+
* // → '/src/types/pets/petTypes.ts'
|
|
1433
|
+
* ```
|
|
1434
|
+
*
|
|
1435
|
+
* @example Single-file mode
|
|
1436
|
+
* ```ts
|
|
1437
|
+
* defaultResolvePath(
|
|
1438
|
+
* { baseName: 'petTypes.ts', pathMode: 'single' },
|
|
1439
|
+
* { root: '/src', output: { path: 'types' } },
|
|
1440
|
+
* )
|
|
1441
|
+
* // → '/src/types'
|
|
1442
|
+
* ```
|
|
1443
|
+
*/
|
|
1444
|
+
function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root, output, group }) {
|
|
1445
|
+
if ((pathMode ?? getMode(node_path.default.resolve(root, output.path))) === "single") return node_path.default.resolve(root, output.path);
|
|
1446
|
+
const result = (() => {
|
|
1447
|
+
if (group && (groupPath || tag)) {
|
|
1448
|
+
const groupValue = group.type === "path" ? groupPath : tag;
|
|
1449
|
+
const defaultName = group.type === "tag" ? ({ group: groupName }) => `${camelCase(groupName)}Controller` : ({ group: groupName }) => {
|
|
1450
|
+
const segment = groupName.split("/").filter((part) => part !== "" && part !== "." && part !== "..")[0];
|
|
1451
|
+
return segment ? camelCase(segment) : "";
|
|
1452
|
+
};
|
|
1453
|
+
const resolveName = group.name ?? defaultName;
|
|
1454
|
+
return node_path.default.resolve(root, output.path, resolveName({ group: groupValue }), baseName);
|
|
1455
|
+
}
|
|
1456
|
+
return node_path.default.resolve(root, output.path, baseName);
|
|
1457
|
+
})();
|
|
1458
|
+
const outputDir = node_path.default.resolve(root, output.path);
|
|
1459
|
+
const outputDirWithSep = outputDir.endsWith(node_path.default.sep) ? outputDir : `${outputDir}${node_path.default.sep}`;
|
|
1460
|
+
if (result !== outputDir && !result.startsWith(outputDirWithSep)) throw new Diagnostics.Error({
|
|
1461
|
+
code: Diagnostics.code.pathTraversal,
|
|
1462
|
+
severity: "error",
|
|
1463
|
+
message: `Resolved path "${result}" is outside the output directory "${outputDir}".`,
|
|
1464
|
+
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.",
|
|
1465
|
+
location: { kind: "config" }
|
|
1466
|
+
});
|
|
1467
|
+
return result;
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Default file resolver used by `defineResolver`.
|
|
1471
|
+
*
|
|
1472
|
+
* Resolves a `FileNode` by combining name resolution (`resolver.default`) with
|
|
1473
|
+
* path resolution (`resolver.resolvePath`). The resolved file always has empty
|
|
1474
|
+
* `sources`, `imports`, and `exports` arrays, which consumers populate separately.
|
|
1475
|
+
*
|
|
1476
|
+
* In `single` mode the name is omitted and the file sits directly in the output directory.
|
|
1477
|
+
*
|
|
1478
|
+
* @example Resolve a schema file
|
|
1479
|
+
* ```ts
|
|
1480
|
+
* const file = defaultResolveFile.call(
|
|
1481
|
+
* resolver,
|
|
1482
|
+
* { name: 'pet', extname: '.ts' },
|
|
1483
|
+
* { root: '/src', output: { path: 'types' } },
|
|
1484
|
+
* )
|
|
1485
|
+
* // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
|
|
1486
|
+
* ```
|
|
1487
|
+
*
|
|
1488
|
+
* @example Resolve an operation file with tag grouping
|
|
1489
|
+
* ```ts
|
|
1490
|
+
* const file = defaultResolveFile.call(
|
|
1491
|
+
* resolver,
|
|
1492
|
+
* { name: 'listPets', extname: '.ts', tag: 'pets' },
|
|
1493
|
+
* { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
|
|
1494
|
+
* )
|
|
1495
|
+
* // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
|
|
1496
|
+
* ```
|
|
1497
|
+
*/
|
|
1498
|
+
function defaultResolveFile({ name, extname, tag, path: groupPath }, context) {
|
|
1499
|
+
const pathMode = getMode(node_path.default.resolve(context.root, context.output.path));
|
|
1500
|
+
const baseName = `${pathMode === "single" ? "" : this.default(name, "file")}${extname}`;
|
|
1501
|
+
const filePath = this.resolvePath({
|
|
1502
|
+
baseName,
|
|
1503
|
+
pathMode,
|
|
1504
|
+
tag,
|
|
1505
|
+
path: groupPath
|
|
1506
|
+
}, context);
|
|
1507
|
+
return (0, _kubb_ast.createFile)({
|
|
1508
|
+
path: filePath,
|
|
1509
|
+
baseName: node_path.default.basename(filePath),
|
|
1510
|
+
meta: { pluginName: this.pluginName },
|
|
1511
|
+
sources: [],
|
|
1512
|
+
imports: [],
|
|
1513
|
+
exports: []
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Generates the default "Generated by Kubb" banner from config and optional node metadata.
|
|
1518
|
+
*/
|
|
1519
|
+
function buildDefaultBanner({ title, description, version, config }) {
|
|
1520
|
+
try {
|
|
1521
|
+
const source = (() => {
|
|
1522
|
+
if (Array.isArray(config.input)) {
|
|
1523
|
+
const first = config.input[0];
|
|
1524
|
+
if (first && "path" in first) return node_path.default.basename(first.path);
|
|
1525
|
+
return "";
|
|
1526
|
+
}
|
|
1527
|
+
if (config.input && "path" in config.input) return node_path.default.basename(config.input.path);
|
|
1528
|
+
if (config.input && "data" in config.input) return "text content";
|
|
1529
|
+
return "";
|
|
1530
|
+
})();
|
|
1531
|
+
let banner = "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n";
|
|
1532
|
+
if (config.output.defaultBanner === "simple") {
|
|
1533
|
+
banner += "*/\n";
|
|
1534
|
+
return banner;
|
|
1535
|
+
}
|
|
1536
|
+
if (source) banner += `* Source: ${source}\n`;
|
|
1537
|
+
if (title) banner += `* Title: ${title}\n`;
|
|
1538
|
+
if (description) {
|
|
1539
|
+
const formattedDescription = description.replace(/\n/gm, "\n* ");
|
|
1540
|
+
banner += `* Description: ${formattedDescription}\n`;
|
|
1541
|
+
}
|
|
1542
|
+
if (version) banner += `* OpenAPI spec version: ${version}\n`;
|
|
1543
|
+
banner += "*/\n";
|
|
1544
|
+
return banner;
|
|
1545
|
+
} catch (_error) {
|
|
1546
|
+
return "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n*/";
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Default banner resolver. Returns the banner string for a generated file.
|
|
1551
|
+
*
|
|
1552
|
+
* A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
|
|
1553
|
+
* When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
|
|
1554
|
+
* from the document metadata when `meta` is provided).
|
|
1555
|
+
*
|
|
1556
|
+
* - When `output.banner` is a function, calls it with the file's `BannerMeta` and returns the result.
|
|
1557
|
+
* - When `output.banner` is a string, returns it directly.
|
|
1558
|
+
* - When `config.output.defaultBanner` is `false`, returns `undefined`.
|
|
1559
|
+
* - Otherwise returns the Kubb "Generated by Kubb" notice.
|
|
1560
|
+
*
|
|
1561
|
+
* @example String banner overrides default
|
|
1562
|
+
* ```ts
|
|
1563
|
+
* defaultResolveBanner(undefined, { output: { banner: '// my banner' }, config })
|
|
1564
|
+
* // → '// my banner'
|
|
1565
|
+
* ```
|
|
1566
|
+
*
|
|
1567
|
+
* @example Function banner with metadata
|
|
1568
|
+
* ```ts
|
|
1569
|
+
* defaultResolveBanner(meta, { output: { banner: (m) => `// v${m.version}` }, config })
|
|
1570
|
+
* // → '// v3.0.0'
|
|
1571
|
+
* ```
|
|
1572
|
+
*
|
|
1573
|
+
* @example Function banner skips re-export files
|
|
1574
|
+
* ```ts
|
|
1575
|
+
* defaultResolveBanner(meta, { output: { banner: (m) => (m.isBarrel ? '' : "'use server'") }, config, file: { path, baseName, isBarrel: true } })
|
|
1576
|
+
* // → ''
|
|
1577
|
+
* ```
|
|
1578
|
+
*
|
|
1579
|
+
* @example No user banner, Kubb notice with OAS metadata
|
|
1580
|
+
* ```ts
|
|
1581
|
+
* defaultResolveBanner(meta, { config })
|
|
1582
|
+
* // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
|
|
1583
|
+
* ```
|
|
1584
|
+
*
|
|
1585
|
+
* @example Disabled default banner
|
|
1586
|
+
* ```ts
|
|
1587
|
+
* defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })
|
|
1588
|
+
* // → null
|
|
1589
|
+
* ```
|
|
1590
|
+
*/
|
|
1591
|
+
function defaultResolveBanner(meta, { output, config, file }) {
|
|
1592
|
+
if (typeof output?.banner === "function") return output.banner(buildBannerMeta({
|
|
1593
|
+
meta,
|
|
1594
|
+
file
|
|
1595
|
+
}));
|
|
1596
|
+
if (typeof output?.banner === "string") return output.banner;
|
|
1597
|
+
if (config.output.defaultBanner === false) return null;
|
|
1598
|
+
return buildDefaultBanner({
|
|
1599
|
+
title: meta?.title,
|
|
1600
|
+
version: meta?.version,
|
|
1601
|
+
config
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Default footer resolver. Returns the footer string for a generated file.
|
|
1606
|
+
*
|
|
1607
|
+
* - When `output.footer` is a function, calls it with the file's `BannerMeta` and returns the result.
|
|
1608
|
+
* - When `output.footer` is a string, returns it directly.
|
|
1609
|
+
* - Otherwise returns `undefined`.
|
|
1610
|
+
*
|
|
1611
|
+
* @example String footer
|
|
1612
|
+
* ```ts
|
|
1613
|
+
* defaultResolveFooter(undefined, { output: { footer: '// end of file' }, config })
|
|
1614
|
+
* // → '// end of file'
|
|
1615
|
+
* ```
|
|
1616
|
+
*
|
|
1617
|
+
* @example Function footer with metadata
|
|
1618
|
+
* ```ts
|
|
1619
|
+
* defaultResolveFooter(meta, { output: { footer: (m) => `// ${m.title}` }, config })
|
|
1620
|
+
* // → '// Pet Store'
|
|
1621
|
+
* ```
|
|
1622
|
+
*/
|
|
1623
|
+
function defaultResolveFooter(meta, { output, file }) {
|
|
1624
|
+
if (typeof output?.footer === "function") return output.footer(buildBannerMeta({
|
|
1625
|
+
meta,
|
|
1626
|
+
file
|
|
1627
|
+
}));
|
|
1628
|
+
if (typeof output?.footer === "string") return output.footer;
|
|
1629
|
+
return null;
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Defines a plugin resolver. The resolver is the object that decides what
|
|
1633
|
+
* every generated symbol and file path is called. Built-in defaults handle
|
|
1634
|
+
* name casing, include/exclude/override filtering, output path computation,
|
|
1635
|
+
* and file construction. Supply your own to override any of them:
|
|
1636
|
+
*
|
|
1637
|
+
* - `default` sets the name casing strategy (camelCase or PascalCase).
|
|
1638
|
+
* - `resolveOptions` does include/exclude/override filtering.
|
|
1639
|
+
* - `resolvePath` computes the output path.
|
|
1640
|
+
* - `resolveFile` builds the full `FileNode`.
|
|
1641
|
+
* - `resolveBanner` and `resolveFooter` produce the top and bottom of file text.
|
|
1642
|
+
*
|
|
1643
|
+
* Methods in the returned object can call sibling resolver methods via `this`,
|
|
1644
|
+
* which keeps custom rules small (`this.default(name, 'type')` to delegate).
|
|
1645
|
+
*
|
|
1646
|
+
* @example Basic resolver with naming helpers
|
|
1647
|
+
* ```ts
|
|
1648
|
+
* export const resolverTs = defineResolver<PluginTs>(() => ({
|
|
1649
|
+
* name: 'default',
|
|
1650
|
+
* resolveName(name) {
|
|
1651
|
+
* return this.default(name, 'function')
|
|
1652
|
+
* },
|
|
1653
|
+
* resolveTypeName(name) {
|
|
1654
|
+
* return this.default(name, 'type')
|
|
1655
|
+
* },
|
|
1656
|
+
* }))
|
|
1657
|
+
* ```
|
|
1658
|
+
*
|
|
1659
|
+
* @example Custom output path
|
|
1660
|
+
* ```ts
|
|
1661
|
+
* import path from 'node:path'
|
|
1662
|
+
*
|
|
1663
|
+
* export const resolverTs = defineResolver<PluginTs>(() => ({
|
|
1664
|
+
* name: 'custom',
|
|
1665
|
+
* resolvePath({ baseName }, { root, output }) {
|
|
1666
|
+
* return path.resolve(root, output.path, 'generated', baseName)
|
|
1667
|
+
* },
|
|
1668
|
+
* }))
|
|
1669
|
+
* ```
|
|
1670
|
+
*/
|
|
1671
|
+
function defineResolver(build) {
|
|
1672
|
+
let resolver;
|
|
1673
|
+
resolver = {
|
|
1674
|
+
default: defaultResolver,
|
|
1675
|
+
resolveOptions: defaultResolveOptions,
|
|
1676
|
+
resolvePath: defaultResolvePath,
|
|
1677
|
+
resolveFile: (params, context) => defaultResolveFile.call(resolver, params, context),
|
|
1678
|
+
resolveBanner: defaultResolveBanner,
|
|
1679
|
+
resolveFooter: defaultResolveFooter,
|
|
1680
|
+
...build()
|
|
1681
|
+
};
|
|
1682
|
+
return resolver;
|
|
1683
|
+
}
|
|
1684
|
+
//#endregion
|
|
1685
|
+
//#region src/FileManager.ts
|
|
1686
|
+
function mergeFile(a, b) {
|
|
1687
|
+
return {
|
|
1688
|
+
...a,
|
|
1689
|
+
banner: b.banner,
|
|
1690
|
+
footer: b.footer,
|
|
1691
|
+
sources: a.sources.length ? b.sources.length ? [...a.sources, ...b.sources] : a.sources : b.sources,
|
|
1692
|
+
imports: a.imports.length ? b.imports.length ? [...a.imports, ...b.imports] : a.imports : b.imports,
|
|
1693
|
+
exports: a.exports.length ? b.exports.length ? [...a.exports, ...b.exports] : a.exports : b.exports
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
function isIndexPath(path) {
|
|
1697
|
+
return path.endsWith("/index.ts") || path === "index.ts";
|
|
1698
|
+
}
|
|
1699
|
+
function compareFiles(a, b) {
|
|
1700
|
+
const lenDiff = a.path.length - b.path.length;
|
|
1701
|
+
if (lenDiff !== 0) return lenDiff;
|
|
1702
|
+
const aIsIndex = isIndexPath(a.path);
|
|
1703
|
+
const bIsIndex = isIndexPath(b.path);
|
|
1704
|
+
if (aIsIndex && !bIsIndex) return 1;
|
|
1705
|
+
if (!aIsIndex && bIsIndex) return -1;
|
|
1706
|
+
return 0;
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* In-memory file store for generated files. Files sharing a `path` are merged
|
|
1710
|
+
* (sources/imports/exports concatenated). The `files` getter is sorted by
|
|
1711
|
+
* path length (barrel `index.ts` last within a bucket).
|
|
1712
|
+
*
|
|
1713
|
+
* @example
|
|
1714
|
+
* ```ts
|
|
1715
|
+
* const manager = new FileManager()
|
|
1716
|
+
* manager.upsert(myFile)
|
|
1717
|
+
* manager.files // sorted view
|
|
1718
|
+
* ```
|
|
1719
|
+
*/
|
|
1720
|
+
var FileManager = class {
|
|
1721
|
+
/**
|
|
1722
|
+
* Subscribe to file-store changes. Listeners on `upsert` see each resolved file as it lands
|
|
1723
|
+
* through `add` or `upsert`.
|
|
1724
|
+
*/
|
|
1725
|
+
hooks = new AsyncEventEmitter();
|
|
1726
|
+
#cache = /* @__PURE__ */ new Map();
|
|
1727
|
+
#sorted = null;
|
|
1728
|
+
add(...files) {
|
|
1729
|
+
return this.#store(files, false);
|
|
1730
|
+
}
|
|
1731
|
+
upsert(...files) {
|
|
1732
|
+
return this.#store(files, true);
|
|
1733
|
+
}
|
|
1734
|
+
#store(files, mergeExisting) {
|
|
1735
|
+
const batch = files.length > 1 ? this.#dedupe(files) : files;
|
|
1736
|
+
const resolved = [];
|
|
1737
|
+
for (const file of batch) {
|
|
1738
|
+
const existing = this.#cache.get(file.path);
|
|
1739
|
+
const merged = existing && mergeExisting ? (0, _kubb_ast.createFile)(mergeFile(existing, file)) : (0, _kubb_ast.createFile)(file);
|
|
1740
|
+
this.#cache.set(merged.path, merged);
|
|
1741
|
+
resolved.push(merged);
|
|
1742
|
+
this.hooks.emit("upsert", merged);
|
|
1743
|
+
}
|
|
1744
|
+
if (resolved.length > 0) this.#sorted = null;
|
|
1745
|
+
return resolved;
|
|
1746
|
+
}
|
|
1747
|
+
#dedupe(files) {
|
|
1748
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1749
|
+
for (const file of files) {
|
|
1750
|
+
const prev = seen.get(file.path);
|
|
1751
|
+
seen.set(file.path, prev ? mergeFile(prev, file) : file);
|
|
1752
|
+
}
|
|
1753
|
+
return [...seen.values()];
|
|
1754
|
+
}
|
|
1755
|
+
getByPath(path) {
|
|
1756
|
+
return this.#cache.get(path) ?? null;
|
|
1757
|
+
}
|
|
1758
|
+
deleteByPath(path) {
|
|
1759
|
+
if (!this.#cache.delete(path)) return;
|
|
1760
|
+
this.#sorted = null;
|
|
1761
|
+
}
|
|
1762
|
+
clear() {
|
|
1763
|
+
this.#cache.clear();
|
|
1764
|
+
this.#sorted = null;
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Releases all stored files and clears every `hooks` listener. Called by the core after
|
|
1768
|
+
* `kubb:build:end`.
|
|
1769
|
+
*/
|
|
1770
|
+
dispose() {
|
|
1771
|
+
this.clear();
|
|
1772
|
+
this.hooks.removeAll();
|
|
1773
|
+
}
|
|
1774
|
+
[Symbol.dispose]() {
|
|
1775
|
+
this.dispose();
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* All stored files in stable sort order (shortest path first, barrel files
|
|
1779
|
+
* last within a length bucket). Returns a cached view, do not mutate.
|
|
1780
|
+
*/
|
|
1781
|
+
get files() {
|
|
1782
|
+
return this.#sorted ??= [...this.#cache.values()].sort(compareFiles);
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
//#endregion
|
|
1786
|
+
//#region src/FileProcessor.ts
|
|
1787
|
+
function joinSources(file) {
|
|
1788
|
+
const sources = file.sources;
|
|
1789
|
+
if (sources.length === 0) return "";
|
|
1790
|
+
const parts = [];
|
|
1791
|
+
for (const source of sources) {
|
|
1792
|
+
const text = (0, _kubb_ast.extractStringsFromNodes)(source.nodes);
|
|
1793
|
+
if (text) parts.push(text);
|
|
1794
|
+
}
|
|
1795
|
+
return parts.join("\n\n");
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Turns `FileNode`s into source strings and writes them to storage.
|
|
1799
|
+
*
|
|
1800
|
+
* Two modes share the same instance. Stateless mode (`parse`, `stream`, `run`) just runs the
|
|
1801
|
+
* conversion. Queue mode (`enqueue`, `flush`, `drain`) buffers files deduped by path and
|
|
1802
|
+
* writes each batch through storage with up to `STREAM_FLUSH_EVERY` requests in flight.
|
|
1803
|
+
*
|
|
1804
|
+
* `flush` does not wait for its batch to finish, so dispatch can overlap with IO. The next
|
|
1805
|
+
* `flush` or `drain` picks the in-flight batch up. `drain` blocks until everything has been
|
|
1806
|
+
* written and is meant for the end of a build.
|
|
1807
|
+
*
|
|
1808
|
+
* To surface build-level hook signals (`kubb:files:processing:*` and friends) subscribe to
|
|
1809
|
+
* `hooks` and re-emit on the kubb bus.
|
|
1810
|
+
*/
|
|
1811
|
+
var FileProcessor = class {
|
|
1812
|
+
hooks = new AsyncEventEmitter();
|
|
1813
|
+
#parsers;
|
|
1814
|
+
#storage;
|
|
1815
|
+
#extension;
|
|
1816
|
+
#pending = /* @__PURE__ */ new Map();
|
|
1817
|
+
#runningFlush = null;
|
|
1818
|
+
constructor(options) {
|
|
1819
|
+
this.#parsers = options.parsers ?? null;
|
|
1820
|
+
this.#storage = options.storage;
|
|
1821
|
+
this.#extension = options.extension ?? null;
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Files waiting in the queue.
|
|
1825
|
+
*/
|
|
1826
|
+
get size() {
|
|
1827
|
+
return this.#pending.size;
|
|
1828
|
+
}
|
|
1829
|
+
parse(file) {
|
|
1830
|
+
const parsers = this.#parsers;
|
|
1831
|
+
const parseExtName = this.#extension?.[file.extname] || void 0;
|
|
1832
|
+
if (!parsers || !file.extname) return joinSources(file);
|
|
1833
|
+
const parser = parsers.get(file.extname);
|
|
1834
|
+
if (!parser) return joinSources(file);
|
|
1835
|
+
return parser.parse(file, { extname: parseExtName });
|
|
1836
|
+
}
|
|
1837
|
+
*stream(files) {
|
|
1838
|
+
const total = files.length;
|
|
1839
|
+
if (total === 0) return;
|
|
1840
|
+
let processed = 0;
|
|
1841
|
+
for (const file of files) {
|
|
1842
|
+
const source = this.parse(file);
|
|
1843
|
+
processed++;
|
|
1844
|
+
yield {
|
|
1845
|
+
file,
|
|
1846
|
+
source,
|
|
1847
|
+
processed,
|
|
1848
|
+
total,
|
|
1849
|
+
percentage: processed / total * 100
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
async run(files) {
|
|
1854
|
+
await this.hooks.emit("start", files);
|
|
1855
|
+
for (const { file, source, processed, total, percentage } of this.stream(files)) await this.hooks.emit("update", {
|
|
1856
|
+
file,
|
|
1857
|
+
source,
|
|
1858
|
+
processed,
|
|
1859
|
+
percentage,
|
|
1860
|
+
total
|
|
1861
|
+
});
|
|
1862
|
+
await this.hooks.emit("end", files);
|
|
1863
|
+
return files;
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Adds a file to the next flush. A later `enqueue` for the same path replaces the previous
|
|
1867
|
+
* entry, matching `FileManager.upsert`. Fires the `enqueue` event.
|
|
1868
|
+
*/
|
|
1869
|
+
enqueue(file) {
|
|
1870
|
+
this.#pending.set(file.path, file);
|
|
1871
|
+
this.hooks.emit("enqueue", file);
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Starts processing the queued files. Waits for any previous flush to finish (so two
|
|
1875
|
+
* batches never run together) and then returns without waiting for the new one. The next
|
|
1876
|
+
* `flush` or `drain` picks up the in-flight task.
|
|
1877
|
+
*/
|
|
1878
|
+
async flush() {
|
|
1879
|
+
if (this.#runningFlush) await this.#runningFlush;
|
|
1880
|
+
if (this.#pending.size === 0) return;
|
|
1881
|
+
const batch = [...this.#pending.values()];
|
|
1882
|
+
this.#pending.clear();
|
|
1883
|
+
this.#runningFlush = this.#processAndWrite(batch).finally(() => {
|
|
1884
|
+
this.#runningFlush = null;
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Waits for the in-flight flush and writes any files still queued. Fires the `drain` event
|
|
1889
|
+
* when both are done.
|
|
1890
|
+
*/
|
|
1891
|
+
async drain() {
|
|
1892
|
+
if (this.#runningFlush) await this.#runningFlush;
|
|
1893
|
+
if (this.#pending.size > 0) {
|
|
1894
|
+
const batch = [...this.#pending.values()];
|
|
1895
|
+
this.#pending.clear();
|
|
1896
|
+
await this.#processAndWrite(batch);
|
|
1897
|
+
}
|
|
1898
|
+
await this.hooks.emit("drain");
|
|
1899
|
+
}
|
|
1900
|
+
async #processAndWrite(files) {
|
|
1901
|
+
const storage = this.#storage;
|
|
1902
|
+
await this.hooks.emit("start", files);
|
|
1903
|
+
const items = [...this.stream(files)];
|
|
1904
|
+
for (const item of items) await this.hooks.emit("update", item);
|
|
1905
|
+
const queue = [];
|
|
1906
|
+
for (const { file, source } of items) if (source) {
|
|
1907
|
+
queue.push(storage.setItem(file.path, source));
|
|
1908
|
+
if (queue.length >= 50) await Promise.all(queue.splice(0));
|
|
1909
|
+
}
|
|
1910
|
+
await Promise.all(queue);
|
|
1911
|
+
await this.hooks.emit("end", files);
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Clears every listener and the pending queue.
|
|
1915
|
+
*/
|
|
1916
|
+
dispose() {
|
|
1917
|
+
this.hooks.removeAll();
|
|
1918
|
+
this.#pending.clear();
|
|
1919
|
+
}
|
|
1920
|
+
[Symbol.dispose]() {
|
|
1921
|
+
this.dispose();
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1924
|
+
//#endregion
|
|
1925
|
+
//#region src/HookRegistry.ts
|
|
1926
|
+
/**
|
|
1927
|
+
* Listener bookkeeping around an `AsyncEventEmitter`. Listeners attached through `register`
|
|
1928
|
+
* stay on the emitter but are tracked so `dispose()` removes only them, listeners attached
|
|
1929
|
+
* directly via `emitter.on(...)` survive.
|
|
1930
|
+
*/
|
|
1931
|
+
var HookRegistry = class {
|
|
1932
|
+
#emitter;
|
|
1933
|
+
#entries = /* @__PURE__ */ new Set();
|
|
1934
|
+
constructor(options) {
|
|
1935
|
+
this.#emitter = options.emitter;
|
|
1936
|
+
}
|
|
1937
|
+
get emitter() {
|
|
1938
|
+
return this.#emitter;
|
|
1939
|
+
}
|
|
1940
|
+
get size() {
|
|
1941
|
+
return this.#entries.size;
|
|
1942
|
+
}
|
|
1943
|
+
register(options) {
|
|
1944
|
+
this.#emitter.on(options.event, options.handler);
|
|
1945
|
+
this.#entries.add(options);
|
|
1946
|
+
}
|
|
1947
|
+
dispose() {
|
|
1948
|
+
for (const entry of this.#entries) this.#emitter.off(entry.event, entry.handler);
|
|
1949
|
+
this.#entries.clear();
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
//#endregion
|
|
1953
|
+
//#region src/Transform.ts
|
|
1954
|
+
/**
|
|
1955
|
+
* Holds one `Visitor` per plugin, keyed by plugin name. Each plugin's transformer runs in
|
|
1956
|
+
* isolation on the original adapter node. `applyTo` is a lookup, not a chain, so plugin A's
|
|
1957
|
+
* visitor never sees plugin B's output. When no transformer is registered, `applyTo` returns
|
|
1958
|
+
* the original node reference, and the `@kubb/ast` `transform` primitive does the same when
|
|
1959
|
+
* its visitor leaves the tree untouched. Callers can compare by identity to detect a no-op.
|
|
1960
|
+
*
|
|
1961
|
+
* Registration order matches the order setup hooks fire, which the driver has already sorted
|
|
1962
|
+
* by `enforce` and dependency edges. The registry does not re-order anything.
|
|
1963
|
+
*/
|
|
1964
|
+
var Transform = class {
|
|
1965
|
+
#visitors = /* @__PURE__ */ new Map();
|
|
1966
|
+
/**
|
|
1967
|
+
* Number of plugins with a registered transformer.
|
|
1968
|
+
*/
|
|
1969
|
+
get size() {
|
|
1970
|
+
return this.#visitors.size;
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Records `visitor` as the transformer for `pluginName`. A second call for the same plugin
|
|
1974
|
+
* replaces the first.
|
|
1975
|
+
*/
|
|
1976
|
+
register(pluginName, visitor) {
|
|
1977
|
+
this.#visitors.set(pluginName, visitor);
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Looks up the transformer for `pluginName`. The generator context uses this so plugins can
|
|
1981
|
+
* read their own visitor through `ctx.transformer`.
|
|
1982
|
+
*/
|
|
1983
|
+
get(pluginName) {
|
|
1984
|
+
return this.#visitors.get(pluginName);
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Runs the plugin's transformer on `node`. Returns the original node reference when the
|
|
1988
|
+
* plugin has no transformer, so callers can compare by identity to detect a no-op.
|
|
1989
|
+
*/
|
|
1990
|
+
applyTo(pluginName, node) {
|
|
1991
|
+
const visitor = this.#visitors.get(pluginName);
|
|
1992
|
+
if (!visitor) return node;
|
|
1993
|
+
return (0, _kubb_ast.transform)(node, visitor);
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Clears every registration. Called from the driver's `dispose()` so visitors do not leak
|
|
1997
|
+
* across builds.
|
|
1998
|
+
*/
|
|
1999
|
+
dispose() {
|
|
2000
|
+
this.#visitors.clear();
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
//#endregion
|
|
2004
|
+
//#region \0@oxc-project+runtime@0.133.0/helpers/esm/usingCtx.js
|
|
2005
|
+
function _usingCtx() {
|
|
2006
|
+
var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
|
|
2007
|
+
var n = Error();
|
|
2008
|
+
return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
|
|
2009
|
+
};
|
|
2010
|
+
var e = {};
|
|
2011
|
+
var n = [];
|
|
2012
|
+
function using(r, e) {
|
|
2013
|
+
if (null != e) {
|
|
2014
|
+
if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
|
|
2015
|
+
if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
|
|
2016
|
+
if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
|
|
2017
|
+
if ("function" != typeof o) throw new TypeError("Object is not disposable.");
|
|
2018
|
+
t && (o = function o() {
|
|
2019
|
+
try {
|
|
2020
|
+
t.call(e);
|
|
2021
|
+
} catch (r) {
|
|
2022
|
+
return Promise.reject(r);
|
|
2023
|
+
}
|
|
2024
|
+
}), n.push({
|
|
2025
|
+
v: e,
|
|
2026
|
+
d: o,
|
|
2027
|
+
a: r
|
|
2028
|
+
});
|
|
2029
|
+
} else r && n.push({
|
|
2030
|
+
d: e,
|
|
2031
|
+
a: r
|
|
2032
|
+
});
|
|
2033
|
+
return e;
|
|
2034
|
+
}
|
|
2035
|
+
return {
|
|
2036
|
+
e,
|
|
2037
|
+
u: using.bind(null, !1),
|
|
2038
|
+
a: using.bind(null, !0),
|
|
2039
|
+
d: function d() {
|
|
2040
|
+
var o;
|
|
2041
|
+
var t = this.e;
|
|
2042
|
+
var s = 0;
|
|
2043
|
+
function next() {
|
|
2044
|
+
for (; o = n.pop();) try {
|
|
2045
|
+
if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
|
|
2046
|
+
if (o.d) {
|
|
2047
|
+
var r = o.d.call(o.v);
|
|
2048
|
+
if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
|
|
2049
|
+
} else s |= 1;
|
|
2050
|
+
} catch (r) {
|
|
2051
|
+
return err(r);
|
|
2052
|
+
}
|
|
2053
|
+
if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
|
|
2054
|
+
if (t !== e) throw t;
|
|
2055
|
+
}
|
|
2056
|
+
function err(n) {
|
|
2057
|
+
return t = t !== e ? new r(n, t) : n, next();
|
|
2058
|
+
}
|
|
2059
|
+
return next();
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
//#endregion
|
|
2064
|
+
//#region src/KubbDriver.ts
|
|
2065
|
+
function enforceOrder(enforce) {
|
|
2066
|
+
return enforce === "pre" ? -1 : enforce === "post" ? 1 : 0;
|
|
2067
|
+
}
|
|
2068
|
+
var KubbDriver = class KubbDriver {
|
|
2069
|
+
config;
|
|
2070
|
+
options;
|
|
2071
|
+
/**
|
|
2072
|
+
* Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise.
|
|
2073
|
+
*
|
|
2074
|
+
* @example
|
|
2075
|
+
* ```ts
|
|
2076
|
+
* KubbDriver.getMode('src/gen/types.ts') // 'single'
|
|
2077
|
+
* KubbDriver.getMode('src/gen/types') // 'split'
|
|
2078
|
+
* ```
|
|
2079
|
+
*/
|
|
2080
|
+
static getMode(fileOrFolder) {
|
|
2081
|
+
return getMode(fileOrFolder);
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* The streaming `InputStreamNode` produced by the adapter.
|
|
2085
|
+
* Always set after adapter setup, parse-only adapters are wrapped automatically.
|
|
2086
|
+
*/
|
|
2087
|
+
inputNode = null;
|
|
2088
|
+
adapter = null;
|
|
2089
|
+
/**
|
|
2090
|
+
* Raw adapter source so `adapter.parse()` / `adapter.stream()` can run lazily.
|
|
2091
|
+
* Intentionally outlives the build, cleared by `dispose()`.
|
|
2092
|
+
*/
|
|
2093
|
+
#adapterSource = null;
|
|
2094
|
+
/**
|
|
2095
|
+
* Central file store for all generated files.
|
|
2096
|
+
* Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
|
|
2097
|
+
* add files. This property gives direct read/write access when needed.
|
|
2098
|
+
*/
|
|
2099
|
+
fileManager = new FileManager();
|
|
2100
|
+
plugins = /* @__PURE__ */ new Map();
|
|
2101
|
+
/**
|
|
2102
|
+
* Tracks which plugins have generators registered via `addGenerator()` (event-based path).
|
|
2103
|
+
* Used by the build loop to decide whether to emit generator events for a given plugin.
|
|
2104
|
+
*/
|
|
2105
|
+
#eventGeneratorPlugins = /* @__PURE__ */ new Set();
|
|
2106
|
+
#resolvers = /* @__PURE__ */ new Map();
|
|
2107
|
+
#defaultResolvers = /* @__PURE__ */ new Map();
|
|
2108
|
+
/**
|
|
2109
|
+
* Tracks every listener the driver added (plugin, middleware, generator) so `dispose()` can
|
|
2110
|
+
* remove them in one pass. Middleware registers after plugins, so it fires last via `Set`
|
|
2111
|
+
* insertion order. External `hooks.on(...)` listeners are not tracked.
|
|
2112
|
+
*/
|
|
2113
|
+
#registry;
|
|
2114
|
+
/**
|
|
2115
|
+
* Transform registry. Plugins populate it during `kubb:plugin:setup` via `setTransformer`,
|
|
2116
|
+
* and `#runGenerators` reads it once per `(plugin, node)` pair through `applyTo`.
|
|
2117
|
+
*/
|
|
2118
|
+
#transforms = new Transform();
|
|
2119
|
+
constructor(config, options) {
|
|
2120
|
+
this.config = config;
|
|
2121
|
+
this.options = options;
|
|
2122
|
+
this.adapter = config.adapter ?? null;
|
|
2123
|
+
this.#registry = new HookRegistry({ emitter: options.hooks });
|
|
2124
|
+
}
|
|
2125
|
+
async setup() {
|
|
2126
|
+
const normalized = this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin));
|
|
2127
|
+
normalized.sort((a, b) => {
|
|
2128
|
+
if (b.dependencies?.includes(a.name)) return -1;
|
|
2129
|
+
if (a.dependencies?.includes(b.name)) return 1;
|
|
2130
|
+
return enforceOrder(a.enforce) - enforceOrder(b.enforce);
|
|
2131
|
+
});
|
|
2132
|
+
for (const plugin of normalized) {
|
|
2133
|
+
if (plugin.apply) plugin.apply(this.config);
|
|
2134
|
+
this.#registerPlugin(plugin);
|
|
2135
|
+
this.plugins.set(plugin.name, plugin);
|
|
2136
|
+
}
|
|
2137
|
+
if (this.config.middleware) for (const middleware of this.config.middleware) for (const event of Object.keys(middleware.hooks)) this.#registerMiddleware(event, middleware.hooks);
|
|
2138
|
+
if (this.config.adapter) this.#adapterSource = inputToAdapterSource(this.config);
|
|
2139
|
+
}
|
|
2140
|
+
get hooks() {
|
|
2141
|
+
return this.options.hooks;
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Creates an `NormalizedPlugin` from a hook-style plugin and registers
|
|
2145
|
+
* its lifecycle handlers on the `AsyncEventEmitter`.
|
|
2146
|
+
*/
|
|
2147
|
+
#normalizePlugin(plugin) {
|
|
2148
|
+
const normalized = {
|
|
2149
|
+
name: plugin.name,
|
|
2150
|
+
dependencies: plugin.dependencies,
|
|
2151
|
+
enforce: plugin.enforce,
|
|
2152
|
+
hooks: plugin.hooks,
|
|
2153
|
+
options: plugin.options ?? {
|
|
2154
|
+
output: { path: "." },
|
|
2155
|
+
exclude: [],
|
|
2156
|
+
override: []
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
if ("apply" in plugin && typeof plugin.apply === "function") normalized.apply = plugin.apply;
|
|
2160
|
+
return normalized;
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Parses the adapter source into `this.inputNode`. Idempotent, so repeated calls from
|
|
2164
|
+
* `run` do not re-parse. Adapters with `stream()` are used directly.
|
|
2165
|
+
* Adapters with only `parse()` are wrapped via `createStreamInput` so the dispatch loop
|
|
2166
|
+
* stays stream-only.
|
|
2167
|
+
*/
|
|
2168
|
+
async #parseInput() {
|
|
2169
|
+
if (this.inputNode || !this.adapter || !this.#adapterSource) return;
|
|
2170
|
+
const adapter = this.adapter;
|
|
2171
|
+
const source = this.#adapterSource;
|
|
2172
|
+
if (adapter.stream) {
|
|
2173
|
+
this.inputNode = await adapter.stream(source);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
const parsed = await adapter.parse(source);
|
|
2177
|
+
this.inputNode = (0, _kubb_ast.createStreamInput)(arrayToAsyncIterable(parsed.schemas), arrayToAsyncIterable(parsed.operations), parsed.meta);
|
|
2178
|
+
}
|
|
2179
|
+
#registerMiddleware(event, middlewareHooks) {
|
|
2180
|
+
const handler = middlewareHooks[event];
|
|
2181
|
+
if (!handler) return;
|
|
2182
|
+
this.#registry.register({
|
|
2183
|
+
event,
|
|
2184
|
+
handler,
|
|
2185
|
+
source: "middleware"
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.
|
|
2190
|
+
*
|
|
2191
|
+
* The `kubb:plugin:setup` listener wraps the global context in a plugin-specific one so
|
|
2192
|
+
* `addGenerator`, `setResolver`, and `setTransformer` target the right `normalizedPlugin`.
|
|
2193
|
+
* Every other `KubbHooks` event registers as a pass-through listener that external tooling
|
|
2194
|
+
* can observe via `hooks.on(...)`.
|
|
2195
|
+
*
|
|
2196
|
+
* @internal
|
|
2197
|
+
*/
|
|
2198
|
+
#registerPlugin(plugin) {
|
|
2199
|
+
const { hooks } = plugin;
|
|
2200
|
+
if (!hooks) return;
|
|
2201
|
+
if (hooks["kubb:plugin:setup"]) {
|
|
2202
|
+
const setupHandler = (globalCtx) => {
|
|
2203
|
+
const pluginCtx = {
|
|
2204
|
+
...globalCtx,
|
|
2205
|
+
options: plugin.options ?? {},
|
|
2206
|
+
addGenerator: (gen) => {
|
|
2207
|
+
this.registerGenerator(plugin.name, gen);
|
|
2208
|
+
},
|
|
2209
|
+
setResolver: (resolver) => {
|
|
2210
|
+
this.setPluginResolver(plugin.name, resolver);
|
|
2211
|
+
},
|
|
2212
|
+
setTransformer: (visitor) => {
|
|
2213
|
+
this.#transforms.register(plugin.name, visitor);
|
|
2214
|
+
},
|
|
2215
|
+
setOptions: (opts) => {
|
|
2216
|
+
plugin.options = {
|
|
2217
|
+
...plugin.options,
|
|
2218
|
+
...opts
|
|
2219
|
+
};
|
|
2220
|
+
},
|
|
2221
|
+
injectFile: (userFileNode) => {
|
|
2222
|
+
this.fileManager.add((0, _kubb_ast.createFile)(userFileNode));
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
return hooks["kubb:plugin:setup"](pluginCtx);
|
|
2226
|
+
};
|
|
2227
|
+
this.#registry.register({
|
|
2228
|
+
event: "kubb:plugin:setup",
|
|
2229
|
+
handler: setupHandler,
|
|
2230
|
+
source: "plugin"
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
for (const event of Object.keys(hooks)) {
|
|
2234
|
+
if (event === "kubb:plugin:setup") continue;
|
|
2235
|
+
const handler = hooks[event];
|
|
2236
|
+
if (!handler) continue;
|
|
2237
|
+
this.#registry.register({
|
|
2238
|
+
event,
|
|
2239
|
+
handler,
|
|
2240
|
+
source: "plugin"
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Emits the `kubb:plugin:setup` event so that all registered hook-style plugin listeners
|
|
2246
|
+
* can configure generators, resolvers, transformers and renderers before `buildStart` runs.
|
|
2247
|
+
*
|
|
2248
|
+
* Call this once from `safeBuild` before the plugin execution loop begins.
|
|
2249
|
+
*/
|
|
2250
|
+
async emitSetupHooks() {
|
|
2251
|
+
const noop = () => {};
|
|
2252
|
+
await this.hooks.emit("kubb:plugin:setup", {
|
|
2253
|
+
config: this.config,
|
|
2254
|
+
options: {},
|
|
2255
|
+
addGenerator: noop,
|
|
2256
|
+
setResolver: noop,
|
|
2257
|
+
setTransformer: noop,
|
|
2258
|
+
setOptions: noop,
|
|
2259
|
+
injectFile: noop,
|
|
2260
|
+
updateConfig: noop
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Registers a generator for the given plugin on the shared event emitter.
|
|
2265
|
+
*
|
|
2266
|
+
* The generator's `schema`, `operation`, and `operations` methods are registered as
|
|
2267
|
+
* listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations`
|
|
2268
|
+
* respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check
|
|
2269
|
+
* so that generators from different plugins do not cross-fire.
|
|
2270
|
+
*
|
|
2271
|
+
* The renderer comes from `generator.renderer`. Set `generator.renderer = null` (or leave it
|
|
2272
|
+
* unset) to opt out of rendering.
|
|
2273
|
+
*
|
|
2274
|
+
* Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
|
|
2275
|
+
*/
|
|
2276
|
+
registerGenerator(pluginName, generator) {
|
|
2277
|
+
if (generator.schema) {
|
|
2278
|
+
const schemaHandler = async (node, ctx) => {
|
|
2279
|
+
if (ctx.plugin.name !== pluginName) return;
|
|
2280
|
+
const result = await generator.schema(node, ctx);
|
|
2281
|
+
await this.dispatch({
|
|
2282
|
+
result,
|
|
2283
|
+
renderer: generator.renderer
|
|
2284
|
+
});
|
|
2285
|
+
};
|
|
2286
|
+
this.#registry.register({
|
|
2287
|
+
event: "kubb:generate:schema",
|
|
2288
|
+
handler: schemaHandler,
|
|
2289
|
+
source: "driver"
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
if (generator.operation) {
|
|
2293
|
+
const operationHandler = async (node, ctx) => {
|
|
2294
|
+
if (ctx.plugin.name !== pluginName) return;
|
|
2295
|
+
const result = await generator.operation(node, ctx);
|
|
2296
|
+
await this.dispatch({
|
|
2297
|
+
result,
|
|
2298
|
+
renderer: generator.renderer
|
|
2299
|
+
});
|
|
2300
|
+
};
|
|
2301
|
+
this.#registry.register({
|
|
2302
|
+
event: "kubb:generate:operation",
|
|
2303
|
+
handler: operationHandler,
|
|
2304
|
+
source: "driver"
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
if (generator.operations) {
|
|
2308
|
+
const operationsHandler = async (nodes, ctx) => {
|
|
2309
|
+
if (ctx.plugin.name !== pluginName) return;
|
|
2310
|
+
const result = await generator.operations(nodes, ctx);
|
|
2311
|
+
await this.dispatch({
|
|
2312
|
+
result,
|
|
2313
|
+
renderer: generator.renderer
|
|
2314
|
+
});
|
|
2315
|
+
};
|
|
2316
|
+
this.#registry.register({
|
|
2317
|
+
event: "kubb:generate:operations",
|
|
2318
|
+
handler: operationsHandler,
|
|
2319
|
+
source: "driver"
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
this.#eventGeneratorPlugins.add(pluginName);
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Returns `true` when at least one generator was registered for the given plugin
|
|
2326
|
+
* via `addGenerator()` in `kubb:plugin:setup` (event-based path).
|
|
2327
|
+
*
|
|
2328
|
+
* Used by the build loop to decide whether to walk the AST and emit generator events
|
|
2329
|
+
* for a plugin that has no static `plugin.generators`.
|
|
2330
|
+
*/
|
|
2331
|
+
hasEventGenerators(pluginName) {
|
|
2332
|
+
return this.#eventGeneratorPlugins.has(pluginName);
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* Runs the full plugin pipeline. Returns the diagnostics collected so far even
|
|
2336
|
+
* when an outer hook throws, since the orchestrator preserves partial state by capturing
|
|
2337
|
+
* the failure as a {@link Diagnostic} instead of propagating. Each plugin also
|
|
2338
|
+
* contributes a `timing` diagnostic for the run summary.
|
|
2339
|
+
*/
|
|
2340
|
+
async run({ storage }) {
|
|
2341
|
+
const { hooks, config } = this;
|
|
2342
|
+
const diagnostics = [];
|
|
2343
|
+
const parsersMap = /* @__PURE__ */ new Map();
|
|
2344
|
+
for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
|
|
2345
|
+
const processor = new FileProcessor({
|
|
2346
|
+
parsers: parsersMap,
|
|
2347
|
+
storage,
|
|
2348
|
+
extension: config.output.extension
|
|
2349
|
+
});
|
|
2350
|
+
processor.hooks.on("start", async (files) => {
|
|
2351
|
+
await hooks.emit("kubb:files:processing:start", { files });
|
|
2352
|
+
});
|
|
2353
|
+
const updateBuffer = [];
|
|
2354
|
+
processor.hooks.on("update", (item) => {
|
|
2355
|
+
updateBuffer.push(item);
|
|
2356
|
+
});
|
|
2357
|
+
processor.hooks.on("end", async (files) => {
|
|
2358
|
+
await hooks.emit("kubb:files:processing:update", { files: updateBuffer.map((item) => ({
|
|
2359
|
+
...item,
|
|
2360
|
+
config
|
|
2361
|
+
})) });
|
|
2362
|
+
updateBuffer.length = 0;
|
|
2363
|
+
await hooks.emit("kubb:files:processing:end", { files });
|
|
2364
|
+
});
|
|
2365
|
+
const onFileUpsert = (file) => {
|
|
2366
|
+
processor.enqueue(file);
|
|
2367
|
+
};
|
|
2368
|
+
this.fileManager.hooks.on("upsert", onFileUpsert);
|
|
2369
|
+
return Diagnostics.scope((diagnostic) => diagnostics.push(diagnostic), async () => {
|
|
2370
|
+
try {
|
|
2371
|
+
await this.#parseInput();
|
|
2372
|
+
await this.emitSetupHooks();
|
|
2373
|
+
if (this.adapter && this.inputNode) await hooks.emit("kubb:build:start", Object.assign({
|
|
2374
|
+
config,
|
|
2375
|
+
adapter: this.adapter,
|
|
2376
|
+
meta: this.inputNode.meta,
|
|
2377
|
+
getPlugin: this.getPlugin.bind(this)
|
|
2378
|
+
}, this.#filesPayload()));
|
|
2379
|
+
const generatorPlugins = [];
|
|
2380
|
+
for (const plugin of this.plugins.values()) {
|
|
2381
|
+
const context = this.getContext(plugin);
|
|
2382
|
+
const hrStart = process.hrtime();
|
|
2383
|
+
try {
|
|
2384
|
+
await hooks.emit("kubb:plugin:start", { plugin });
|
|
2385
|
+
} catch (caughtError) {
|
|
2386
|
+
const error = caughtError;
|
|
2387
|
+
const duration = getElapsedMs(hrStart);
|
|
2388
|
+
await this.#emitPluginEnd({
|
|
2389
|
+
plugin,
|
|
2390
|
+
duration,
|
|
2391
|
+
success: false,
|
|
2392
|
+
error
|
|
2393
|
+
});
|
|
2394
|
+
diagnostics.push({
|
|
2395
|
+
...Diagnostics.from(error),
|
|
2396
|
+
plugin: plugin.name
|
|
2397
|
+
}, Diagnostics.performance({
|
|
2398
|
+
plugin: plugin.name,
|
|
2399
|
+
duration
|
|
2400
|
+
}));
|
|
2401
|
+
continue;
|
|
2402
|
+
}
|
|
2403
|
+
if (this.hasEventGenerators(plugin.name)) {
|
|
2404
|
+
generatorPlugins.push({
|
|
2405
|
+
plugin,
|
|
2406
|
+
context,
|
|
2407
|
+
hrStart
|
|
2408
|
+
});
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
const duration = getElapsedMs(hrStart);
|
|
2412
|
+
diagnostics.push(Diagnostics.performance({
|
|
2413
|
+
plugin: plugin.name,
|
|
2414
|
+
duration
|
|
2415
|
+
}));
|
|
2416
|
+
await this.#emitPluginEnd({
|
|
2417
|
+
plugin,
|
|
2418
|
+
duration,
|
|
2419
|
+
success: true
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
diagnostics.push(...await this.#runGenerators(generatorPlugins, () => processor.flush()));
|
|
2423
|
+
await processor.drain();
|
|
2424
|
+
await hooks.emit("kubb:plugins:end", Object.assign({ config }, this.#filesPayload()));
|
|
2425
|
+
await processor.drain();
|
|
2426
|
+
await hooks.emit("kubb:build:end", {
|
|
2427
|
+
files: this.fileManager.files,
|
|
2428
|
+
config,
|
|
2429
|
+
outputDir: (0, node_path.resolve)(config.root, config.output.path)
|
|
2430
|
+
});
|
|
2431
|
+
return { diagnostics: Diagnostics.dedupe(diagnostics) };
|
|
2432
|
+
} catch (caughtError) {
|
|
2433
|
+
diagnostics.push(Diagnostics.from(caughtError));
|
|
2434
|
+
return { diagnostics: Diagnostics.dedupe(diagnostics) };
|
|
2435
|
+
} finally {
|
|
2436
|
+
this.fileManager.hooks.off("upsert", onFileUpsert);
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
#filesPayload() {
|
|
2441
|
+
const driver = this;
|
|
2442
|
+
return {
|
|
2443
|
+
get files() {
|
|
2444
|
+
return driver.fileManager.files;
|
|
2445
|
+
},
|
|
2446
|
+
upsertFile: (...files) => driver.fileManager.upsert(...files)
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
#emitPluginEnd({ plugin, duration, success, error }) {
|
|
2450
|
+
return this.hooks.emit("kubb:plugin:end", Object.assign({
|
|
2451
|
+
plugin,
|
|
2452
|
+
duration,
|
|
2453
|
+
success,
|
|
2454
|
+
...error ? { error } : {},
|
|
2455
|
+
config: this.config
|
|
2456
|
+
}, this.#filesPayload()));
|
|
2457
|
+
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Streams schemas and operations through every plugin's generators. Each node is run
|
|
2460
|
+
* through the plugin's transformer (from `this.#transforms`) before the generator sees it,
|
|
2461
|
+
* so plugins stay isolated and the hot path stays per-node. Schemas run before operations
|
|
2462
|
+
* because the two passes share `flushPending` and the FileProcessor's event emitter.
|
|
2463
|
+
* A failing plugin contributes an error diagnostic so the rest of the build continues.
|
|
2464
|
+
* Every plugin also contributes a `timing` diagnostic.
|
|
2465
|
+
*
|
|
2466
|
+
* When `entries` is empty or `this.inputNode` is `null`, every entry still gets a
|
|
2467
|
+
* `kubb:plugin:end` so middleware listeners (the barrel writer and friends) complete.
|
|
2468
|
+
*/
|
|
2469
|
+
async #runGenerators(entries, flushPending) {
|
|
2470
|
+
const diagnostics = [];
|
|
2471
|
+
if (entries.length === 0) return diagnostics;
|
|
2472
|
+
if (!this.inputNode) {
|
|
2473
|
+
for (const { plugin, hrStart } of entries) {
|
|
2474
|
+
const duration = getElapsedMs(hrStart);
|
|
2475
|
+
diagnostics.push(Diagnostics.performance({
|
|
2476
|
+
plugin: plugin.name,
|
|
2477
|
+
duration
|
|
2478
|
+
}));
|
|
2479
|
+
await this.#emitPluginEnd({
|
|
2480
|
+
plugin,
|
|
2481
|
+
duration,
|
|
2482
|
+
success: true
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
return diagnostics;
|
|
2486
|
+
}
|
|
2487
|
+
const transforms = this.#transforms;
|
|
2488
|
+
const { schemas, operations } = this.inputNode;
|
|
2489
|
+
const states = entries.map(({ plugin, context, hrStart }) => {
|
|
2490
|
+
const { exclude, include, override } = plugin.options;
|
|
2491
|
+
const hasExclude = Array.isArray(exclude) && exclude.length > 0;
|
|
2492
|
+
const hasInclude = Array.isArray(include) && include.length > 0;
|
|
2493
|
+
const hasOverride = Array.isArray(override) && override.length > 0;
|
|
2494
|
+
return {
|
|
2495
|
+
plugin,
|
|
2496
|
+
generatorContext: {
|
|
2497
|
+
...context,
|
|
2498
|
+
resolver: this.getResolver(plugin.name)
|
|
2499
|
+
},
|
|
2500
|
+
generators: plugin.generators ?? [],
|
|
2501
|
+
hrStart,
|
|
2502
|
+
failed: false,
|
|
2503
|
+
error: null,
|
|
2504
|
+
optionsAreStatic: !hasExclude && !hasInclude && !hasOverride,
|
|
2505
|
+
allowedSchemaNames: null
|
|
2506
|
+
};
|
|
2507
|
+
});
|
|
2508
|
+
const emitsSchemaHook = this.hooks.listenerCount("kubb:generate:schema") > 0;
|
|
2509
|
+
const emitsOperationHook = this.hooks.listenerCount("kubb:generate:operation") > 0;
|
|
2510
|
+
const pruningStates = states.filter(({ plugin }) => {
|
|
2511
|
+
const { include } = plugin.options;
|
|
2512
|
+
return (include?.some(({ type }) => OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false);
|
|
2513
|
+
});
|
|
2514
|
+
if (pruningStates.length > 0) {
|
|
2515
|
+
const allSchemas = [];
|
|
2516
|
+
for await (const schema of schemas) allSchemas.push(schema);
|
|
2517
|
+
const includedOpsByState = new Map(pruningStates.map((state) => [state, []]));
|
|
2518
|
+
for await (const operation of operations) for (const state of pruningStates) {
|
|
2519
|
+
const { exclude, include, override } = state.plugin.options;
|
|
2520
|
+
if (state.generatorContext.resolver.resolveOptions(operation, {
|
|
2521
|
+
options: state.plugin.options,
|
|
2522
|
+
exclude,
|
|
2523
|
+
include,
|
|
2524
|
+
override
|
|
2525
|
+
}) !== null) includedOpsByState.get(state)?.push(operation);
|
|
2526
|
+
}
|
|
2527
|
+
for (const state of pruningStates) {
|
|
2528
|
+
state.allowedSchemaNames = (0, _kubb_ast.collectUsedSchemaNames)(includedOpsByState.get(state) ?? [], allSchemas);
|
|
2529
|
+
includedOpsByState.delete(state);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
const resolveForPlugin = (state, node) => {
|
|
2533
|
+
const { plugin, generatorContext } = state;
|
|
2534
|
+
const transformedNode = transforms.applyTo(plugin.name, node);
|
|
2535
|
+
if (state.optionsAreStatic) return {
|
|
2536
|
+
transformedNode,
|
|
2537
|
+
options: plugin.options
|
|
2538
|
+
};
|
|
2539
|
+
const { exclude, include, override } = plugin.options;
|
|
2540
|
+
const options = generatorContext.resolver.resolveOptions(transformedNode, {
|
|
2541
|
+
options: plugin.options,
|
|
2542
|
+
exclude,
|
|
2543
|
+
include,
|
|
2544
|
+
override
|
|
2545
|
+
});
|
|
2546
|
+
if (options === null) return null;
|
|
2547
|
+
return {
|
|
2548
|
+
transformedNode,
|
|
2549
|
+
options
|
|
2550
|
+
};
|
|
2551
|
+
};
|
|
2552
|
+
const dispatchNode = async (state, node, dispatch) => {
|
|
2553
|
+
if (state.failed) return;
|
|
2554
|
+
try {
|
|
2555
|
+
const resolved = resolveForPlugin(state, node);
|
|
2556
|
+
if (!resolved) return;
|
|
2557
|
+
const { transformedNode, options } = resolved;
|
|
2558
|
+
if (dispatch.checkAllowedNames && state.allowedSchemaNames !== null && "name" in transformedNode && transformedNode.name && !state.allowedSchemaNames.has(transformedNode.name)) return;
|
|
2559
|
+
const ctx = {
|
|
2560
|
+
...state.generatorContext,
|
|
2561
|
+
options
|
|
2562
|
+
};
|
|
2563
|
+
for (const gen of state.generators) {
|
|
2564
|
+
const run = gen[dispatch.method];
|
|
2565
|
+
if (!run) continue;
|
|
2566
|
+
const raw = run(transformedNode, ctx);
|
|
2567
|
+
const result = isPromise(raw) ? await raw : raw;
|
|
2568
|
+
const applied = this.dispatch({
|
|
2569
|
+
result,
|
|
2570
|
+
renderer: gen.renderer
|
|
2571
|
+
});
|
|
2572
|
+
if (isPromise(applied)) await applied;
|
|
2573
|
+
}
|
|
2574
|
+
if (dispatch.emit) await dispatch.emit(transformedNode, ctx);
|
|
2575
|
+
} catch (caughtError) {
|
|
2576
|
+
state.failed = true;
|
|
2577
|
+
state.error = caughtError;
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2580
|
+
const schemaDispatch = {
|
|
2581
|
+
method: "schema",
|
|
2582
|
+
checkAllowedNames: true,
|
|
2583
|
+
emit: emitsSchemaHook ? (node, ctx) => this.hooks.emit("kubb:generate:schema", node, ctx) : null
|
|
2584
|
+
};
|
|
2585
|
+
const operationDispatch = {
|
|
2586
|
+
method: "operation",
|
|
2587
|
+
checkAllowedNames: false,
|
|
2588
|
+
emit: emitsOperationHook ? (node, ctx) => this.hooks.emit("kubb:generate:operation", node, ctx) : null
|
|
2589
|
+
};
|
|
2590
|
+
const needsCollectedOperations = this.hooks.listenerCount("kubb:generate:operations") > 0 || states.some((state) => state.generators.some((gen) => !!gen.operations));
|
|
2591
|
+
const collectedOperations = needsCollectedOperations ? [] : void 0;
|
|
2592
|
+
await forBatches(schemas, (nodes) => Promise.all(nodes.flatMap((node) => states.map((state) => dispatchNode(state, node, schemaDispatch)))), {
|
|
2593
|
+
concurrency: 8,
|
|
2594
|
+
flush: flushPending
|
|
2595
|
+
});
|
|
2596
|
+
await forBatches(operations, (nodes) => {
|
|
2597
|
+
if (needsCollectedOperations) collectedOperations?.push(...nodes);
|
|
2598
|
+
return Promise.all(nodes.flatMap((node) => states.map((state) => dispatchNode(state, node, operationDispatch))));
|
|
2599
|
+
}, {
|
|
2600
|
+
concurrency: 8,
|
|
2601
|
+
flush: flushPending
|
|
2602
|
+
});
|
|
2603
|
+
for (const state of states) {
|
|
2604
|
+
if (!state.failed && needsCollectedOperations) try {
|
|
2605
|
+
const { plugin, generatorContext, generators } = state;
|
|
2606
|
+
const ctx = {
|
|
2607
|
+
...generatorContext,
|
|
2608
|
+
options: plugin.options
|
|
2609
|
+
};
|
|
2610
|
+
const pluginOperations = (collectedOperations ?? []).reduce((acc, node) => {
|
|
2611
|
+
const resolved = resolveForPlugin(state, node);
|
|
2612
|
+
if (resolved) acc.push(resolved.transformedNode);
|
|
2613
|
+
return acc;
|
|
2614
|
+
}, []);
|
|
2615
|
+
for (const gen of generators) {
|
|
2616
|
+
if (!gen.operations) continue;
|
|
2617
|
+
const result = await gen.operations(pluginOperations, ctx);
|
|
2618
|
+
await this.dispatch({
|
|
2619
|
+
result,
|
|
2620
|
+
renderer: gen.renderer
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
await this.hooks.emit("kubb:generate:operations", pluginOperations, ctx);
|
|
2624
|
+
} catch (caughtError) {
|
|
2625
|
+
state.failed = true;
|
|
2626
|
+
state.error = caughtError;
|
|
2627
|
+
}
|
|
2628
|
+
const duration = getElapsedMs(state.hrStart);
|
|
2629
|
+
await this.#emitPluginEnd({
|
|
2630
|
+
plugin: state.plugin,
|
|
2631
|
+
duration,
|
|
2632
|
+
success: !state.failed,
|
|
2633
|
+
error: state.failed && state.error ? state.error : void 0
|
|
2634
|
+
});
|
|
2635
|
+
if (state.failed && state.error) diagnostics.push({
|
|
2636
|
+
...Diagnostics.from(state.error),
|
|
2637
|
+
plugin: state.plugin.name
|
|
2638
|
+
});
|
|
2639
|
+
diagnostics.push(Diagnostics.performance({
|
|
2640
|
+
plugin: state.plugin.name,
|
|
2641
|
+
duration
|
|
2642
|
+
}));
|
|
2643
|
+
}
|
|
2644
|
+
return diagnostics;
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Stores whatever a generator method or `kubb:generate:*` hook returned.
|
|
2648
|
+
*
|
|
2649
|
+
* - An `Array<FileNode>` goes straight into `fileManager` via `upsert`.
|
|
2650
|
+
* - A renderer element runs through `renderer` (the renderer factory, e.g. JSX) and the
|
|
2651
|
+
* produced files go to `fileManager.upsert`.
|
|
2652
|
+
* - A falsy result is treated as a no-op. The generator wrote files itself via
|
|
2653
|
+
* `ctx.upsertFile`.
|
|
2654
|
+
*
|
|
2655
|
+
* Pass `renderer` when the result may be a renderer element. Generators that only return
|
|
2656
|
+
* `Array<FileNode>` do not need one.
|
|
2657
|
+
*/
|
|
2658
|
+
async dispatch({ result, renderer }) {
|
|
2659
|
+
try {
|
|
2660
|
+
var _usingCtx$1 = _usingCtx();
|
|
2661
|
+
if (!result) return;
|
|
2662
|
+
if (Array.isArray(result)) {
|
|
2663
|
+
this.fileManager.upsert(...result);
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
if (!renderer) return;
|
|
2667
|
+
const instance = _usingCtx$1.u(renderer());
|
|
2668
|
+
if (instance.stream) {
|
|
2669
|
+
for (const file of instance.stream(result)) this.fileManager.upsert(file);
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
await instance.render(result);
|
|
2673
|
+
this.fileManager.upsert(...instance.files);
|
|
2674
|
+
} catch (_) {
|
|
2675
|
+
_usingCtx$1.e = _;
|
|
2676
|
+
} finally {
|
|
2677
|
+
_usingCtx$1.d();
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Removes every listener the driver added. Listeners attached directly to `hooks` from outside
|
|
2682
|
+
* the driver survive. Called at the end of a build to prevent leaks across repeated builds.
|
|
2683
|
+
*
|
|
2684
|
+
* @internal
|
|
2685
|
+
*/
|
|
2686
|
+
dispose() {
|
|
2687
|
+
this.#registry.dispose();
|
|
2688
|
+
this.#eventGeneratorPlugins.clear();
|
|
2689
|
+
this.#transforms.dispose();
|
|
2690
|
+
this.#resolvers.clear();
|
|
2691
|
+
this.#defaultResolvers.clear();
|
|
2692
|
+
this.fileManager.dispose();
|
|
2693
|
+
this.inputNode = null;
|
|
2694
|
+
this.#adapterSource = null;
|
|
2695
|
+
}
|
|
2696
|
+
[Symbol.dispose]() {
|
|
2697
|
+
this.dispose();
|
|
2698
|
+
}
|
|
2699
|
+
#getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => defineResolver(() => ({
|
|
2700
|
+
name: "default",
|
|
2701
|
+
pluginName
|
|
2702
|
+
})));
|
|
2703
|
+
/**
|
|
2704
|
+
* Merges `partial` with the plugin's default resolver and stores the result.
|
|
2705
|
+
* Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver`
|
|
2706
|
+
* get the up-to-date resolver without going through `getResolver()`.
|
|
2707
|
+
*/
|
|
2708
|
+
setPluginResolver(pluginName, partial) {
|
|
2709
|
+
const merged = {
|
|
2710
|
+
...this.#getDefaultResolver(pluginName),
|
|
2711
|
+
...partial
|
|
2712
|
+
};
|
|
2713
|
+
this.#resolvers.set(pluginName, merged);
|
|
2714
|
+
const plugin = this.plugins.get(pluginName);
|
|
2715
|
+
if (plugin) plugin.resolver = merged;
|
|
2716
|
+
}
|
|
2717
|
+
getResolver(pluginName) {
|
|
2718
|
+
return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#getDefaultResolver(pluginName);
|
|
2719
|
+
}
|
|
2720
|
+
getContext(plugin) {
|
|
2721
|
+
const driver = this;
|
|
2722
|
+
const report = (diagnostic) => {
|
|
2723
|
+
Diagnostics.report({
|
|
2724
|
+
...diagnostic,
|
|
2725
|
+
plugin: plugin.name
|
|
2726
|
+
});
|
|
2727
|
+
};
|
|
2728
|
+
return {
|
|
2729
|
+
config: driver.config,
|
|
2730
|
+
get root() {
|
|
2731
|
+
return (0, node_path.resolve)(driver.config.root, driver.config.output.path);
|
|
2732
|
+
},
|
|
2733
|
+
getMode(output) {
|
|
2734
|
+
return KubbDriver.getMode((0, node_path.resolve)(driver.config.root, driver.config.output.path, output.path));
|
|
2735
|
+
},
|
|
2736
|
+
hooks: driver.hooks,
|
|
2737
|
+
plugin,
|
|
2738
|
+
getPlugin: driver.getPlugin.bind(driver),
|
|
2739
|
+
requirePlugin: driver.requirePlugin.bind(driver),
|
|
2740
|
+
getResolver: driver.getResolver.bind(driver),
|
|
2741
|
+
driver,
|
|
2742
|
+
addFile: async (...files) => {
|
|
2743
|
+
driver.fileManager.add(...files);
|
|
2744
|
+
},
|
|
2745
|
+
upsertFile: async (...files) => {
|
|
2746
|
+
driver.fileManager.upsert(...files);
|
|
2747
|
+
},
|
|
2748
|
+
get meta() {
|
|
2749
|
+
return driver.inputNode?.meta ?? {
|
|
2750
|
+
circularNames: [],
|
|
2751
|
+
enumNames: []
|
|
2752
|
+
};
|
|
2753
|
+
},
|
|
2754
|
+
get adapter() {
|
|
2755
|
+
return driver.adapter;
|
|
2756
|
+
},
|
|
2757
|
+
get resolver() {
|
|
2758
|
+
return driver.getResolver(plugin.name);
|
|
2759
|
+
},
|
|
2760
|
+
get transformer() {
|
|
2761
|
+
return driver.#transforms.get(plugin.name);
|
|
2762
|
+
},
|
|
2763
|
+
warn(message) {
|
|
2764
|
+
report({
|
|
2765
|
+
code: Diagnostics.code.pluginWarning,
|
|
2766
|
+
severity: "warning",
|
|
2767
|
+
message
|
|
2768
|
+
});
|
|
2769
|
+
},
|
|
2770
|
+
error(error) {
|
|
2771
|
+
const cause = typeof error === "string" ? void 0 : error;
|
|
2772
|
+
report({
|
|
2773
|
+
code: Diagnostics.code.pluginFailed,
|
|
2774
|
+
severity: "error",
|
|
2775
|
+
message: typeof error === "string" ? error : error.message,
|
|
2776
|
+
cause
|
|
2777
|
+
});
|
|
2778
|
+
},
|
|
2779
|
+
info(message) {
|
|
2780
|
+
report({
|
|
2781
|
+
code: Diagnostics.code.pluginInfo,
|
|
2782
|
+
severity: "info",
|
|
2783
|
+
message
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
getPlugin(pluginName) {
|
|
2789
|
+
return this.plugins.get(pluginName);
|
|
2790
|
+
}
|
|
2791
|
+
requirePlugin(pluginName) {
|
|
2792
|
+
const plugin = this.plugins.get(pluginName);
|
|
2793
|
+
if (!plugin) throw new Diagnostics.Error({
|
|
2794
|
+
code: Diagnostics.code.pluginNotFound,
|
|
2795
|
+
severity: "error",
|
|
2796
|
+
message: `Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`,
|
|
2797
|
+
help: `Add "${pluginName}" to the \`plugins\` array in kubb.config.ts, or remove the dependency on it.`,
|
|
2798
|
+
location: { kind: "config" }
|
|
2799
|
+
});
|
|
2800
|
+
return plugin;
|
|
2801
|
+
}
|
|
2802
|
+
};
|
|
2803
|
+
function inputToAdapterSource(config) {
|
|
2804
|
+
const input = config.input;
|
|
2805
|
+
if (!input) throw new Diagnostics.Error({
|
|
2806
|
+
code: Diagnostics.code.inputRequired,
|
|
2807
|
+
severity: "error",
|
|
2808
|
+
message: "An adapter is configured without an input.",
|
|
2809
|
+
help: "Provide `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config.",
|
|
2810
|
+
location: { kind: "config" }
|
|
2811
|
+
});
|
|
2812
|
+
if ("data" in input) return {
|
|
2813
|
+
type: "data",
|
|
2814
|
+
data: input.data
|
|
2815
|
+
};
|
|
2816
|
+
if (new URLPath(input.path).isURL) return {
|
|
2817
|
+
type: "path",
|
|
2818
|
+
path: input.path
|
|
2819
|
+
};
|
|
2820
|
+
return {
|
|
2821
|
+
type: "path",
|
|
2822
|
+
path: (0, node_path.resolve)(config.root, input.path)
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
//#endregion
|
|
2826
|
+
//#region src/storages/memoryStorage.ts
|
|
2827
|
+
/**
|
|
2828
|
+
* In-memory storage driver. Useful for testing and dry-run scenarios where
|
|
2829
|
+
* generated output should be captured without touching the filesystem.
|
|
2830
|
+
*
|
|
2831
|
+
* All data lives in a `Map` scoped to the storage instance and is discarded
|
|
2832
|
+
* when the instance is garbage-collected.
|
|
2833
|
+
*
|
|
2834
|
+
* @example
|
|
2835
|
+
* ```ts
|
|
2836
|
+
* import { memoryStorage } from '@kubb/core'
|
|
2837
|
+
* import { defineConfig } from 'kubb'
|
|
2838
|
+
*
|
|
2839
|
+
* export default defineConfig({
|
|
2840
|
+
* input: { path: './petStore.yaml' },
|
|
2841
|
+
* output: { path: './src/gen' },
|
|
2842
|
+
* storage: memoryStorage(),
|
|
2843
|
+
* })
|
|
2844
|
+
* ```
|
|
2845
|
+
*/
|
|
2846
|
+
const memoryStorage = createStorage(() => {
|
|
2847
|
+
const store = /* @__PURE__ */ new Map();
|
|
2848
|
+
return {
|
|
2849
|
+
name: "memory",
|
|
2850
|
+
async hasItem(key) {
|
|
2851
|
+
return store.has(key);
|
|
2852
|
+
},
|
|
2853
|
+
async getItem(key) {
|
|
2854
|
+
return store.get(key) ?? null;
|
|
2855
|
+
},
|
|
2856
|
+
async setItem(key, value) {
|
|
2857
|
+
store.set(key, value);
|
|
2858
|
+
},
|
|
2859
|
+
async removeItem(key) {
|
|
2860
|
+
store.delete(key);
|
|
2861
|
+
},
|
|
2862
|
+
async getKeys(base) {
|
|
2863
|
+
const keys = [...store.keys()];
|
|
2864
|
+
return base ? keys.filter((k) => k.startsWith(base)) : keys;
|
|
2865
|
+
},
|
|
2866
|
+
async clear(base) {
|
|
2867
|
+
if (!base) {
|
|
2868
|
+
store.clear();
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
for (const key of store.keys()) if (key.startsWith(base)) store.delete(key);
|
|
2872
|
+
}
|
|
2873
|
+
};
|
|
2874
|
+
});
|
|
2875
|
+
//#endregion
|
|
2876
|
+
Object.defineProperty(exports, "AsyncEventEmitter", {
|
|
2877
|
+
enumerable: true,
|
|
2878
|
+
get: function() {
|
|
2879
|
+
return AsyncEventEmitter;
|
|
2880
|
+
}
|
|
2881
|
+
});
|
|
2882
|
+
Object.defineProperty(exports, "BuildError", {
|
|
2883
|
+
enumerable: true,
|
|
2884
|
+
get: function() {
|
|
2885
|
+
return BuildError;
|
|
2886
|
+
}
|
|
2887
|
+
});
|
|
2888
|
+
Object.defineProperty(exports, "Diagnostics", {
|
|
2889
|
+
enumerable: true,
|
|
2890
|
+
get: function() {
|
|
2891
|
+
return Diagnostics;
|
|
2892
|
+
}
|
|
2893
|
+
});
|
|
2894
|
+
Object.defineProperty(exports, "FileManager", {
|
|
2895
|
+
enumerable: true,
|
|
2896
|
+
get: function() {
|
|
2897
|
+
return FileManager;
|
|
2898
|
+
}
|
|
2899
|
+
});
|
|
2900
|
+
Object.defineProperty(exports, "FileProcessor", {
|
|
2901
|
+
enumerable: true,
|
|
2902
|
+
get: function() {
|
|
2903
|
+
return FileProcessor;
|
|
2904
|
+
}
|
|
2905
|
+
});
|
|
2906
|
+
Object.defineProperty(exports, "KubbDriver", {
|
|
2907
|
+
enumerable: true,
|
|
2908
|
+
get: function() {
|
|
2909
|
+
return KubbDriver;
|
|
2910
|
+
}
|
|
2911
|
+
});
|
|
2912
|
+
Object.defineProperty(exports, "OTLP_ENDPOINT", {
|
|
2913
|
+
enumerable: true,
|
|
2914
|
+
get: function() {
|
|
2915
|
+
return OTLP_ENDPOINT;
|
|
2916
|
+
}
|
|
2917
|
+
});
|
|
2918
|
+
Object.defineProperty(exports, "URLPath", {
|
|
2919
|
+
enumerable: true,
|
|
2920
|
+
get: function() {
|
|
2921
|
+
return URLPath;
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
Object.defineProperty(exports, "__name", {
|
|
2925
|
+
enumerable: true,
|
|
2926
|
+
get: function() {
|
|
2927
|
+
return __name;
|
|
2928
|
+
}
|
|
2929
|
+
});
|
|
2930
|
+
Object.defineProperty(exports, "__toESM", {
|
|
2931
|
+
enumerable: true,
|
|
2932
|
+
get: function() {
|
|
2933
|
+
return __toESM;
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
2936
|
+
Object.defineProperty(exports, "_usingCtx", {
|
|
2937
|
+
enumerable: true,
|
|
2938
|
+
get: function() {
|
|
2939
|
+
return _usingCtx;
|
|
2940
|
+
}
|
|
2941
|
+
});
|
|
2942
|
+
Object.defineProperty(exports, "camelCase", {
|
|
2943
|
+
enumerable: true,
|
|
2944
|
+
get: function() {
|
|
2945
|
+
return camelCase;
|
|
2946
|
+
}
|
|
2947
|
+
});
|
|
2948
|
+
Object.defineProperty(exports, "createStorage", {
|
|
2949
|
+
enumerable: true,
|
|
2950
|
+
get: function() {
|
|
2951
|
+
return createStorage;
|
|
2952
|
+
}
|
|
2953
|
+
});
|
|
2954
|
+
Object.defineProperty(exports, "definePlugin", {
|
|
2955
|
+
enumerable: true,
|
|
2956
|
+
get: function() {
|
|
2957
|
+
return definePlugin;
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2960
|
+
Object.defineProperty(exports, "defineResolver", {
|
|
2961
|
+
enumerable: true,
|
|
2962
|
+
get: function() {
|
|
2963
|
+
return defineResolver;
|
|
2964
|
+
}
|
|
2965
|
+
});
|
|
2966
|
+
Object.defineProperty(exports, "formatMs", {
|
|
2967
|
+
enumerable: true,
|
|
2968
|
+
get: function() {
|
|
2969
|
+
return formatMs;
|
|
2970
|
+
}
|
|
2971
|
+
});
|
|
2972
|
+
Object.defineProperty(exports, "getElapsedMs", {
|
|
2973
|
+
enumerable: true,
|
|
2974
|
+
get: function() {
|
|
2975
|
+
return getElapsedMs;
|
|
2976
|
+
}
|
|
2977
|
+
});
|
|
2978
|
+
Object.defineProperty(exports, "memoryStorage", {
|
|
2979
|
+
enumerable: true,
|
|
2980
|
+
get: function() {
|
|
2981
|
+
return memoryStorage;
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2984
|
+
|
|
2985
|
+
//# sourceMappingURL=memoryStorage-Dkxnid2K.cjs.map
|