@kubb/core 5.0.0-beta.10 → 5.0.0-beta.101

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