@kubb/core 5.0.0-beta.75 → 5.0.0-beta.76

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