@kubb/core 5.0.0-beta.1 → 5.0.0-beta.100

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