@kubb/core 5.0.0-beta.11 → 5.0.0-beta.110

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