@kubb/core 5.0.0-beta.6 → 5.0.0-beta.61

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