@kubb/core 5.0.0-beta.82 → 5.0.0-beta.85

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.
package/dist/index.cjs CHANGED
@@ -1,14 +1,49 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_memoryStorage = require("./memoryStorage-DQ6qXJ8o.cjs");
3
- let node_crypto = require("node:crypto");
2
+ const require_usingCtx = require("./usingCtx-fxRpJw0P.cjs");
3
+ let node_async_hooks = require("node:async_hooks");
4
4
  let node_util = require("node:util");
5
+ let node_crypto = require("node:crypto");
5
6
  let node_fs_promises = require("node:fs/promises");
6
7
  let node_path = require("node:path");
7
- node_path = require_memoryStorage.__toESM(node_path, 1);
8
- let node_async_hooks = require("node:async_hooks");
8
+ node_path = require_usingCtx.__toESM(node_path, 1);
9
9
  let _kubb_ast = require("@kubb/ast");
10
10
  let node_process = require("node:process");
11
- node_process = require_memoryStorage.__toESM(node_process, 1);
11
+ node_process = require_usingCtx.__toESM(node_process, 1);
12
+ //#region src/createAdapter.ts
13
+ /**
14
+ * Defines a custom adapter that translates a spec format into Kubb's universal
15
+ * AST, for example GraphQL, gRPC, or AsyncAPI. The built-in `@kubb/adapter-oas`
16
+ * handles OpenAPI/Swagger documents.
17
+ *
18
+ * Adapters must return an `InputNode` from `parse`. That node is what every
19
+ * plugin in the build consumes.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import { createAdapter, type AdapterFactoryOptions } from '@kubb/core'
24
+ * import { ast } from '@kubb/ast'
25
+ *
26
+ * type MyAdapter = AdapterFactoryOptions<'my-adapter', { validate?: boolean }>
27
+ *
28
+ * export const myAdapter = createAdapter<MyAdapter>((options) => ({
29
+ * name: 'my-adapter',
30
+ * options,
31
+ * document: null,
32
+ * async parse(_source) {
33
+ * // Convert the source (path or inline data) into an InputNode.
34
+ * return ast.factory.createInput()
35
+ * },
36
+ * getImports: () => [],
37
+ * async validate() {
38
+ * // Throw here when the spec is invalid.
39
+ * },
40
+ * }))
41
+ * ```
42
+ */
43
+ function createAdapter(build) {
44
+ return (options) => build(options ?? {});
45
+ }
46
+ //#endregion
12
47
  //#region ../../internals/utils/src/time.ts
13
48
  /**
14
49
  * Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.
@@ -92,21 +127,11 @@ const randomColors = [
92
127
  */
93
128
  function randomCliColor(text) {
94
129
  if (!text) return "";
95
- return (0, node_util.styleText)(randomColors[(0, node_crypto.hash)("sha256", text, "buffer").readUInt32BE(0) % randomColors.length] ?? "white", text);
130
+ const index = (0, node_crypto.hash)("sha256", text, "buffer").readUInt32BE(0) % randomColors.length;
131
+ return (0, node_util.styleText)(randomColors[index] ?? "white", text);
96
132
  }
97
133
  //#endregion
98
134
  //#region ../../internals/utils/src/promise.ts
99
- /** Returns `true` when `result` is a thenable `Promise`.
100
- *
101
- * @example
102
- * ```ts
103
- * isPromise(Promise.resolve(1)) // true
104
- * isPromise(42) // false
105
- * ```
106
- */
107
- function isPromise(result) {
108
- return result !== null && result !== void 0 && typeof result["then"] === "function";
109
- }
110
135
  /**
111
136
  * Wraps `factory` with a keyed cache backed by the provided store.
112
137
  *
@@ -145,258 +170,116 @@ function memoize(store, factory) {
145
170
  return value;
146
171
  };
147
172
  }
148
- /**
149
- * Wraps a plain array in a reusable `AsyncIterable`.
150
- * Each `[Symbol.asyncIterator]()` call returns a fresh generator so the
151
- * iterable can be consumed multiple times (e.g. once per plugin pre-scan).
152
- *
153
- * @example
154
- * ```ts
155
- * const stream = arrayToAsyncIterable([1, 2, 3])
156
- * for await (const n of stream) console.log(n) // 1, 2, 3
157
- * ```
158
- */
159
- function arrayToAsyncIterable(arr) {
160
- return { [Symbol.asyncIterator]() {
161
- return (async function* () {
162
- yield* arr;
163
- })();
164
- } };
165
- }
166
173
  //#endregion
167
- //#region ../../internals/utils/src/reserved.ts
174
+ //#region package.json
175
+ var version = "5.0.0-beta.85";
176
+ //#endregion
177
+ //#region src/constants.ts
168
178
  /**
169
- * JavaScript and Java reserved words.
170
- * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
179
+ * Plugin `include` filter types that select operations directly. When one of these is set
180
+ * without a `schemaName` include, the generate phase pre-scans operations to compute the set
181
+ * of schemas they reach, so unreachable schemas can be pruned for that plugin.
171
182
  */
172
- const reservedWords = /* @__PURE__ */ new Set([
173
- "abstract",
174
- "arguments",
175
- "boolean",
176
- "break",
177
- "byte",
178
- "case",
179
- "catch",
180
- "char",
181
- "class",
182
- "const",
183
- "continue",
184
- "debugger",
185
- "default",
186
- "delete",
187
- "do",
188
- "double",
189
- "else",
190
- "enum",
191
- "eval",
192
- "export",
193
- "extends",
194
- "false",
195
- "final",
196
- "finally",
197
- "float",
198
- "for",
199
- "function",
200
- "goto",
201
- "if",
202
- "implements",
203
- "import",
204
- "in",
205
- "instanceof",
206
- "int",
207
- "interface",
208
- "let",
209
- "long",
210
- "native",
211
- "new",
212
- "null",
213
- "package",
214
- "private",
215
- "protected",
216
- "public",
217
- "return",
218
- "short",
219
- "static",
220
- "super",
221
- "switch",
222
- "synchronized",
223
- "this",
224
- "throw",
225
- "throws",
226
- "transient",
227
- "true",
228
- "try",
229
- "typeof",
230
- "var",
231
- "void",
232
- "volatile",
233
- "while",
234
- "with",
235
- "yield",
236
- "Array",
237
- "Date",
238
- "hasOwnProperty",
239
- "Infinity",
240
- "isFinite",
241
- "isNaN",
242
- "isPrototypeOf",
243
- "length",
244
- "Math",
245
- "name",
246
- "NaN",
247
- "Number",
248
- "Object",
249
- "prototype",
250
- "String",
251
- "toString",
252
- "undefined",
253
- "valueOf"
183
+ const OPERATION_FILTER_TYPES = /* @__PURE__ */ new Set([
184
+ "tag",
185
+ "operationId",
186
+ "path",
187
+ "method",
188
+ "contentType"
254
189
  ]);
255
190
  /**
256
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
257
- *
258
- * @example
259
- * ```ts
260
- * isValidVarName('status') // true
261
- * isValidVarName('class') // false (reserved word)
262
- * isValidVarName('42foo') // false (starts with digit)
263
- * ```
191
+ * Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode
192
+ * and stays stable so it can be referenced in tooling and (later) docs. Reference
193
+ * these instead of inlining the string at a throw site.
264
194
  */
265
- function isValidVarName(name) {
266
- if (!name || reservedWords.has(name)) return false;
267
- return isIdentifier(name);
268
- }
269
- /**
270
- * Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
271
- *
272
- * Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
273
- * even though they are not valid variable names, so use this (not {@link isValidVarName}) when
274
- * deciding whether an object key needs quoting.
275
- *
276
- * @example
277
- * ```ts
278
- * isIdentifier('name') // true
279
- * isIdentifier('x-total')// false
280
- * ```
281
- */
282
- function isIdentifier(name) {
283
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
284
- }
285
- //#endregion
286
- //#region ../../internals/utils/src/url.ts
287
- function transformParam(raw, casing) {
288
- const param = isValidVarName(raw) ? raw : require_memoryStorage.camelCase(raw);
289
- return casing === "camelcase" ? require_memoryStorage.camelCase(param) : param;
290
- }
291
- function toParamsObject(path, { replacer, casing } = {}) {
292
- const params = {};
293
- for (const match of path.matchAll(/\{([^}]+)\}/g)) {
294
- const param = transformParam(match[1], casing);
295
- const key = replacer ? replacer(param) : param;
296
- params[key] = key;
297
- }
298
- return Object.keys(params).length > 0 ? params : null;
299
- }
300
- /**
301
- * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
302
- */
303
- var Url = class Url {
195
+ const diagnosticCode = {
304
196
  /**
305
- * Converts an OpenAPI/Swagger path to Express-style colon syntax.
306
- *
307
- * @example
308
- * Url.toPath('/pet/{petId}') // '/pet/:petId'
197
+ * Fallback for an unstructured error with no specific code.
309
198
  */
310
- static toPath(path) {
311
- return path.replace(/\{([^}]+)\}/g, ":$1");
312
- }
199
+ unknown: "KUBB_UNKNOWN",
313
200
  /**
314
- * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
315
- * `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
316
- * and `casing` controls parameter identifier casing.
317
- *
318
- * @example
319
- * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
320
- *
321
- * @example
322
- * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
201
+ * The `input.path` file or URL could not be read.
323
202
  */
324
- static toTemplateString(path, { prefix, replacer, casing } = {}) {
325
- const result = path.split(/\{([^}]+)\}/).map((part, i) => {
326
- if (i % 2 === 0) return part;
327
- const param = transformParam(part, casing);
328
- return `\${${replacer ? replacer(param) : param}}`;
329
- }).join("");
330
- return `\`${prefix ?? ""}${result}\``;
331
- }
203
+ inputNotFound: "KUBB_INPUT_NOT_FOUND",
332
204
  /**
333
- * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
334
- * expression when `stringify` is set.
335
- *
336
- * @example
337
- * Url.toObject('/pet/{petId}')
338
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
339
- */
340
- static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
341
- const object = {
342
- url: type === "path" ? Url.toPath(path) : Url.toTemplateString(path, {
343
- replacer,
344
- casing
345
- }),
346
- params: toParamsObject(path, {
347
- replacer,
348
- casing
349
- })
350
- };
351
- if (stringify) {
352
- if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
353
- if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
354
- return `{ url: '${object.url}' }`;
355
- }
356
- return object;
357
- }
205
+ * An adapter was configured without an `input`.
206
+ */
207
+ inputRequired: "KUBB_INPUT_REQUIRED",
208
+ /**
209
+ * A `$ref` (or equivalent reference) could not be resolved in the source document.
210
+ */
211
+ refNotFound: "KUBB_REF_NOT_FOUND",
212
+ /**
213
+ * A server variable value is not allowed by its `enum`.
214
+ */
215
+ invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE",
216
+ /**
217
+ * A required plugin is missing from the config.
218
+ */
219
+ pluginNotFound: "KUBB_PLUGIN_NOT_FOUND",
220
+ /**
221
+ * A plugin threw while generating.
222
+ */
223
+ pluginFailed: "KUBB_PLUGIN_FAILED",
224
+ /**
225
+ * A plugin reported a non-fatal warning through `ctx.warn`.
226
+ */
227
+ pluginWarning: "KUBB_PLUGIN_WARNING",
228
+ /**
229
+ * A plugin reported an informational message through `ctx.info`.
230
+ */
231
+ pluginInfo: "KUBB_PLUGIN_INFO",
232
+ /**
233
+ * A schema uses a `format` Kubb does not map to a specific type. Reserved for
234
+ * adapters to emit as a `warning`.
235
+ */
236
+ unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT",
237
+ /**
238
+ * A referenced schema or operation is marked `deprecated`. Reserved for adapters
239
+ * to emit as an `info`.
240
+ */
241
+ deprecated: "KUBB_DEPRECATED",
242
+ /**
243
+ * An adapter is required but the config has none. The build cannot read the input
244
+ * without one.
245
+ */
246
+ adapterRequired: "KUBB_ADAPTER_REQUIRED",
247
+ /**
248
+ * A resolved output path escapes the output directory, which can stem from a path
249
+ * traversal in the spec or a misconfigured `group.name`.
250
+ */
251
+ pathTraversal: "KUBB_PATH_TRAVERSAL",
252
+ /**
253
+ * A plugin's options are invalid, for example `output.mode: 'file'` paired with a `group` option.
254
+ */
255
+ invalidPluginOptions: "KUBB_INVALID_PLUGIN_OPTIONS",
256
+ /**
257
+ * A post-generate shell hook (`hooks.done`) exited with a failure.
258
+ */
259
+ hookFailed: "KUBB_HOOK_FAILED",
260
+ /**
261
+ * The formatter pass over the generated files failed.
262
+ */
263
+ formatFailed: "KUBB_FORMAT_FAILED",
264
+ /**
265
+ * The linter pass over the generated files failed.
266
+ */
267
+ lintFailed: "KUBB_LINT_FAILED",
268
+ /**
269
+ * Not a failure. Carries a plugin's elapsed time, summed into the run total.
270
+ */
271
+ performance: "KUBB_PERFORMANCE",
272
+ /**
273
+ * Not a failure. A newer Kubb version is available on npm.
274
+ */
275
+ updateAvailable: "KUBB_UPDATE_AVAILABLE"
358
276
  };
359
277
  //#endregion
360
- //#region src/createAdapter.ts
361
- /**
362
- * Defines a custom adapter that translates a spec format into Kubb's universal
363
- * AST, for example GraphQL, gRPC, or AsyncAPI. The built-in `@kubb/adapter-oas`
364
- * handles OpenAPI/Swagger documents.
365
- *
366
- * Adapters must return an `InputNode` from `parse`. That node is what every
367
- * plugin in the build consumes.
368
- *
369
- * @example
370
- * ```ts
371
- * import { createAdapter, type AdapterFactoryOptions } from '@kubb/core'
372
- * import { ast } from '@kubb/ast'
373
- *
374
- * type MyAdapter = AdapterFactoryOptions<'my-adapter', { validate?: boolean }>
375
- *
376
- * export const myAdapter = createAdapter<MyAdapter>((options) => ({
377
- * name: 'my-adapter',
378
- * options,
379
- * document: null,
380
- * async parse(_source) {
381
- * // Convert the source (path or inline data) into an InputNode.
382
- * return ast.factory.createInput()
383
- * },
384
- * getImports: () => [],
385
- * async validate() {
386
- * // Throw here when the spec is invalid.
387
- * },
388
- * }))
389
- * ```
390
- */
391
- function createAdapter(build) {
392
- return (options) => build(options ?? {});
393
- }
394
- //#endregion
395
- //#region src/diagnostics.ts
278
+ //#region src/Diagnostics.ts
396
279
  /**
397
280
  * Docs major version, derived from the package version so the link tracks the published major.
398
281
  */
399
- const docsMajor = "5.0.0-beta.82".split(".")[0] ?? "5";
282
+ const docsMajor = version.split(".")[0] ?? "5";
400
283
  /**
401
284
  * Narrows a {@link Diagnostic} to the variant for `code`, or `null` when it does not match.
402
285
  *
@@ -454,106 +337,106 @@ const isUpdate = isKind("update");
454
337
  * blue info).
455
338
  */
456
339
  const severityStyle = {
457
- error: { color: "red" },
458
- warning: { color: "yellow" },
459
- info: { color: "blue" }
340
+ error: "red",
341
+ warning: "yellow",
342
+ info: "blue"
460
343
  };
461
344
  /**
462
345
  * Explanation for every {@link diagnosticCode}. Use {@link Diagnostics.explain} to look one up
463
346
  * and `Diagnostics.docsUrl` for the matching kubb.dev page.
464
347
  */
465
348
  const diagnosticCatalog = {
466
- [require_memoryStorage.diagnosticCode.unknown]: {
349
+ [diagnosticCode.unknown]: {
467
350
  title: "Unknown error",
468
351
  cause: "An error was thrown without a stable Kubb code, so it is reported as-is.",
469
352
  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."
470
353
  },
471
- [require_memoryStorage.diagnosticCode.inputNotFound]: {
354
+ [diagnosticCode.inputNotFound]: {
472
355
  title: "Input not found",
473
356
  cause: "The file or URL set in `input.path` (or passed as `kubb generate PATH`) could not be read.",
474
357
  fix: "Check that the path or URL exists and is readable, then set it in `input.path` or pass it on the CLI."
475
358
  },
476
- [require_memoryStorage.diagnosticCode.inputRequired]: {
359
+ [diagnosticCode.inputRequired]: {
477
360
  title: "Input required",
478
361
  cause: "An adapter is configured but no `input` was provided.",
479
362
  fix: "Set `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config."
480
363
  },
481
- [require_memoryStorage.diagnosticCode.refNotFound]: {
364
+ [diagnosticCode.refNotFound]: {
482
365
  title: "Reference not found",
483
366
  cause: "A `$ref` could not be resolved in the source document.",
484
367
  fix: "Add the missing definition (for example under `components.schemas`) or fix the `$ref`. Run `kubb validate` to check the spec."
485
368
  },
486
- [require_memoryStorage.diagnosticCode.invalidServerVariable]: {
369
+ [diagnosticCode.invalidServerVariable]: {
487
370
  title: "Invalid server variable",
488
371
  cause: "A server variable value is not allowed by its `enum`.",
489
372
  fix: "Use one of the values listed in the server variable `enum`, or update the spec."
490
373
  },
491
- [require_memoryStorage.diagnosticCode.pluginNotFound]: {
374
+ [diagnosticCode.pluginNotFound]: {
492
375
  title: "Plugin not found",
493
376
  cause: "A plugin that another plugin depends on is missing from the config.",
494
377
  fix: "Add the required plugin to the `plugins` array in kubb.config.ts, or remove the dependency on it."
495
378
  },
496
- [require_memoryStorage.diagnosticCode.pluginFailed]: {
379
+ [diagnosticCode.pluginFailed]: {
497
380
  title: "Plugin failed",
498
381
  cause: "A plugin threw while generating, or reported an error through `ctx.error`.",
499
382
  fix: "Read the underlying error and check the plugin options and the schema or operation it failed on."
500
383
  },
501
- [require_memoryStorage.diagnosticCode.pluginWarning]: {
384
+ [diagnosticCode.pluginWarning]: {
502
385
  title: "Plugin warning",
503
386
  cause: "A plugin reported a non-fatal warning through `ctx.warn`.",
504
387
  fix: "Review the message. It does not fail the build; adjust the plugin options or input if the warning is unwanted."
505
388
  },
506
- [require_memoryStorage.diagnosticCode.pluginInfo]: {
389
+ [diagnosticCode.pluginInfo]: {
507
390
  title: "Plugin info",
508
391
  cause: "A plugin reported an informational message through `ctx.info`.",
509
392
  fix: "Informational only. No action is required."
510
393
  },
511
- [require_memoryStorage.diagnosticCode.unsupportedFormat]: {
394
+ [diagnosticCode.unsupportedFormat]: {
512
395
  title: "Unsupported format",
513
396
  cause: "A schema uses a `format` Kubb does not map to a specific type, so it falls back to the base type.",
514
397
  fix: "Use a format Kubb supports, or handle the custom format with a parser or plugin."
515
398
  },
516
- [require_memoryStorage.diagnosticCode.deprecated]: {
399
+ [diagnosticCode.deprecated]: {
517
400
  title: "Deprecated",
518
401
  cause: "A referenced schema or operation is marked `deprecated`.",
519
402
  fix: "Migrate off the deprecated definition if the warning is unwanted."
520
403
  },
521
- [require_memoryStorage.diagnosticCode.adapterRequired]: {
404
+ [diagnosticCode.adapterRequired]: {
522
405
  title: "Adapter required",
523
406
  cause: "An action needs an adapter but none is configured.",
524
407
  fix: "Set `adapter` in kubb.config.ts, for example `adapterOas()`."
525
408
  },
526
- [require_memoryStorage.diagnosticCode.pathTraversal]: {
409
+ [diagnosticCode.pathTraversal]: {
527
410
  title: "Path traversal",
528
411
  cause: "A resolved output path escaped the output directory, which can stem from a path traversal in the spec or a misconfigured `group.name`.",
529
412
  fix: "Keep generated paths within the output directory. Review the `group.name` function and the names coming from the spec."
530
413
  },
531
- [require_memoryStorage.diagnosticCode.invalidPluginOptions]: {
414
+ [diagnosticCode.invalidPluginOptions]: {
532
415
  title: "Invalid plugin options",
533
416
  cause: "A plugin was configured with options that cannot be honored, for example `output.mode: 'file'` paired with a `group` option.",
534
417
  fix: "Fix the plugin options. A single-file output has nothing to group, so remove the `group` option or use `output.mode: 'directory'`."
535
418
  },
536
- [require_memoryStorage.diagnosticCode.hookFailed]: {
419
+ [diagnosticCode.hookFailed]: {
537
420
  title: "Hook failed",
538
421
  cause: "A post-generate shell hook (`hooks.done`) exited with a non-zero status.",
539
422
  fix: "Check the command is installed and correct, and run it manually to see the error."
540
423
  },
541
- [require_memoryStorage.diagnosticCode.formatFailed]: {
424
+ [diagnosticCode.formatFailed]: {
542
425
  title: "Format failed",
543
426
  cause: "The formatter pass over the generated files failed.",
544
427
  fix: "Check the formatter (oxfmt, biome, or prettier) is installed and its config is valid, then run it manually on the output."
545
428
  },
546
- [require_memoryStorage.diagnosticCode.lintFailed]: {
429
+ [diagnosticCode.lintFailed]: {
547
430
  title: "Lint failed",
548
431
  cause: "The linter pass over the generated files failed.",
549
432
  fix: "Check the linter (oxlint, biome, or eslint) is installed and its config is valid, then run it manually on the output."
550
433
  },
551
- [require_memoryStorage.diagnosticCode.performance]: {
434
+ [diagnosticCode.performance]: {
552
435
  title: "Performance",
553
436
  cause: "Not a failure. Records a plugin’s elapsed time, summed into the run total.",
554
437
  fix: "No action. This is an informational metric."
555
438
  },
556
- [require_memoryStorage.diagnosticCode.updateAvailable]: {
439
+ [diagnosticCode.updateAvailable]: {
557
440
  title: "Update available",
558
441
  cause: "A newer Kubb version is published on npm than the one running.",
559
442
  fix: "Update the `@kubb/*` packages, for example `npm install -g @kubb/cli`, to get the latest fixes."
@@ -565,15 +448,15 @@ const diagnosticCatalog = {
565
448
  *
566
449
  * The sink lives in a single `AsyncLocalStorage` in the `@kubb/core` bundle.
567
450
  * `Diagnostics.scope` activates it for a run, so anything inside that run (the
568
- * adapter parse, a lazily consumed stream, a generator) reports through
569
- * `Diagnostics.report` and lands in the same run.
451
+ * adapter parse, a generator) reports through `Diagnostics.report` and lands
452
+ * in the same run.
570
453
  */
571
454
  var Diagnostics = class Diagnostics {
572
455
  static #reporterStorage = new node_async_hooks.AsyncLocalStorage();
573
456
  /**
574
457
  * The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
575
458
  */
576
- static code = require_memoryStorage.diagnosticCode;
459
+ static code = diagnosticCode;
577
460
  /**
578
461
  * Type guard for a build {@link ProblemDiagnostic}.
579
462
  */
@@ -658,9 +541,9 @@ var Diagnostics = class Diagnostics {
658
541
  current = current.cause;
659
542
  }
660
543
  return {
661
- code: require_memoryStorage.diagnosticCode.unknown,
544
+ code: diagnosticCode.unknown,
662
545
  severity: "error",
663
- message: root ? root.message : require_memoryStorage.getErrorMessage(error),
546
+ message: root ? root.message : require_usingCtx.getErrorMessage(error),
664
547
  cause: root
665
548
  };
666
549
  }
@@ -670,7 +553,7 @@ var Diagnostics = class Diagnostics {
670
553
  static performance({ plugin, duration }) {
671
554
  return {
672
555
  kind: "performance",
673
- code: require_memoryStorage.diagnosticCode.performance,
556
+ code: diagnosticCode.performance,
674
557
  severity: "info",
675
558
  message: `${plugin} generated in ${Math.round(duration)}ms`,
676
559
  plugin,
@@ -683,7 +566,7 @@ var Diagnostics = class Diagnostics {
683
566
  static update({ currentVersion, latestVersion }) {
684
567
  return {
685
568
  kind: "update",
686
- code: require_memoryStorage.diagnosticCode.updateAvailable,
569
+ code: diagnosticCode.updateAvailable,
687
570
  severity: "info",
688
571
  message: `Update available: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`,
689
572
  currentVersion,
@@ -752,7 +635,8 @@ var Diagnostics = class Diagnostics {
752
635
  * `KUBB_REF_NOT_FOUND` → `https://kubb.dev/docs/5.x/reference/diagnostics/kubb-ref-not-found`.
753
636
  */
754
637
  static docsUrl(code) {
755
- return `https://kubb.dev/docs/${docsMajor}.x/reference/diagnostics/${code.toLowerCase().replaceAll("_", "-")}`;
638
+ const slug = code.toLowerCase().replaceAll("_", "-");
639
+ return `https://kubb.dev/docs/${docsMajor}.x/reference/diagnostics/${slug}`;
756
640
  }
757
641
  /**
758
642
  * The catalog entry for a code: its title, cause, and fix. Mirrors the kubb.dev
@@ -775,7 +659,7 @@ var Diagnostics = class Diagnostics {
775
659
  ...problem?.location ? { location: problem.location } : {},
776
660
  ...problem?.help ? { help: problem.help } : {},
777
661
  ...problem?.plugin ? { plugin: problem.plugin } : {},
778
- ...diagnostic.code === require_memoryStorage.diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
662
+ ...diagnostic.code === diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
779
663
  };
780
664
  }
781
665
  /**
@@ -788,14 +672,14 @@ var Diagnostics = class Diagnostics {
788
672
  */
789
673
  static format(diagnostic) {
790
674
  const { code, severity, message } = diagnostic;
791
- const { color } = severityStyle[severity];
675
+ const color = severityStyle[severity];
792
676
  const problem = isProblem(diagnostic) ? diagnostic : void 0;
793
677
  const tag = (0, node_util.styleText)(color, (0, node_util.styleText)("bold", `[${code}]`));
794
678
  const headline = problem?.plugin ? `${tag} ${problem.plugin}: ${message}` : `${tag}: ${message}`;
795
679
  const details = [];
796
680
  if (problem?.location && "pointer" in problem.location) details.push(` ${(0, node_util.styleText)("dim", "at:")} ${(0, node_util.styleText)("cyan", problem.location.pointer)}`);
797
681
  if (problem?.help) details.push(` ${(0, node_util.styleText)("cyan", "fix:")} ${problem.help}`);
798
- if (code !== require_memoryStorage.diagnosticCode.unknown) details.push(` ${(0, node_util.styleText)("dim", "see:")} ${(0, node_util.styleText)("cyan", Diagnostics.docsUrl(code))}`);
682
+ if (code !== diagnosticCode.unknown) details.push(` ${(0, node_util.styleText)("dim", "see:")} ${(0, node_util.styleText)("cyan", Diagnostics.docsUrl(code))}`);
799
683
  return {
800
684
  headline,
801
685
  details
@@ -820,7 +704,7 @@ var Diagnostics = class Diagnostics {
820
704
  function normalizeOutput({ output, group, pluginName }) {
821
705
  const mode = output.mode ?? "directory";
822
706
  if (mode === "file" && group) throw new Diagnostics.Error({
823
- code: require_memoryStorage.diagnosticCode.invalidPluginOptions,
707
+ code: diagnosticCode.invalidPluginOptions,
824
708
  severity: "error",
825
709
  message: `Plugin "${pluginName}" sets \`output.mode: 'file'\` but also configures a \`group\` option.`,
826
710
  help: "A single-file output has nothing to group. Remove the `group` option, or use `output.mode: 'directory'` to organize files into subdirectories.",
@@ -858,412 +742,284 @@ function definePlugin(factory) {
858
742
  return (options) => factory(options ?? {});
859
743
  }
860
744
  //#endregion
861
- //#region src/defineResolver.ts
862
- /**
863
- * Merges document `meta` with per-file `file` context into the `BannerMeta` passed to a
864
- * `banner`/`footer` function. Missing fields default to empty/`false` so the object shape
865
- * is stable even when a caller (e.g. the barrel plugin) has no document metadata.
866
- */
867
- function buildBannerMeta({ meta, file }) {
868
- return {
869
- title: meta?.title,
870
- description: meta?.description,
871
- version: meta?.version,
872
- baseURL: meta?.baseURL,
873
- circularNames: meta?.circularNames ?? [],
874
- enumNames: meta?.enumNames ?? [],
875
- filePath: file?.path ?? "",
876
- baseName: file?.baseName ?? "",
877
- isBarrel: file?.isBarrel ?? false,
878
- isAggregation: file?.isAggregation ?? false
879
- };
880
- }
881
- const stringPatternCache = /* @__PURE__ */ new Map();
882
- function testPattern(value, pattern) {
883
- if (typeof pattern === "string") {
884
- let regex = stringPatternCache.get(pattern);
885
- if (!regex) {
886
- regex = new RegExp(pattern);
887
- stringPatternCache.set(pattern, regex);
888
- }
889
- return regex.test(value);
890
- }
891
- return value.match(pattern) !== null;
745
+ //#region src/createResolver.ts
746
+ function isNamespace(value) {
747
+ return typeof value === "object" && value !== null && !Array.isArray(value);
892
748
  }
893
749
  /**
894
- * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).
895
- */
896
- function matchesOperationPattern(node, type, pattern) {
897
- if (type === "tag") return node.tags.some((tag) => testPattern(tag, pattern));
898
- if (type === "operationId") return testPattern(node.operationId, pattern);
899
- if (type === "path") return node.path !== void 0 && testPattern(node.path, pattern);
900
- if (type === "method") return node.method !== void 0 && testPattern(node.method.toLowerCase(), pattern);
901
- if (type === "contentType") return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false;
902
- return false;
903
- }
904
- /**
905
- * Checks if a schema matches a pattern for a given filter type (`schemaName`).
906
- *
907
- * Returns `null` when the filter type doesn't apply to schemas.
908
- */
909
- function matchesSchemaPattern(node, type, pattern) {
910
- if (type === "schemaName") return node.name ? testPattern(node.name, pattern) : false;
911
- return null;
912
- }
913
- /**
914
- * Default name resolver used by `defineResolver`.
750
+ * Base constraint for all plugin resolver objects.
915
751
  *
916
- * - `camelCase` for `file`, with dotted names split into `/`-joined nested paths.
917
- * - `PascalCase` for `type`.
918
- * - `camelCase` for `function` and everything else.
919
- */
920
- function defaultResolver(name, type) {
921
- if (type === "file") return require_memoryStorage.toFilePath(name);
922
- if (type === "type") return require_memoryStorage.pascalCase(name);
923
- return require_memoryStorage.camelCase(name);
924
- }
925
- /**
926
- * Default option resolver. Applies include/exclude filters and merges matching override options.
927
- *
928
- * Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule.
752
+ * The built-in machinery lives under `default`. Generators call the top-level `name` and
753
+ * `file`, and a plugin overrides them to set its conventions. Extend with top-level helpers
754
+ * (`typeName`, …) and/or grouped namespaces (`query`, `schema`, …).
929
755
  *
930
- * @example Include/exclude filtering
756
+ * @example Top-level helper
931
757
  * ```ts
932
- * const options = defaultResolveOptions(operationNode, {
933
- * options: { output: 'types' },
934
- * exclude: [{ type: 'tag', pattern: 'internal' }],
935
- * })
936
- * // → null when node has tag 'internal'
758
+ * type MyResolver = Resolver & {
759
+ * typeName(name: string): string
760
+ * }
937
761
  * ```
938
762
  *
939
- * @example Override merging
763
+ * @example Grouped namespace
940
764
  * ```ts
941
- * const options = defaultResolveOptions(operationNode, {
942
- * options: { enumType: 'asConst' },
943
- * override: [{ type: 'operationId', pattern: 'listPets', options: { enumType: 'enum' } }],
944
- * })
945
- * // → { enumType: 'enum' } when operationId matches
765
+ * type MyResolver = Resolver & {
766
+ * query: {
767
+ * name(node: OperationNode): string
768
+ * keyName(node: OperationNode): string
769
+ * }
770
+ * }
946
771
  * ```
947
772
  */
948
- const resolveOptionsCache = /* @__PURE__ */ new WeakMap();
949
- function computeOptions(node, options, exclude, include, override) {
950
- if (_kubb_ast.operationDef.is(node)) {
951
- if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
952
- if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
953
- const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options;
773
+ var Resolver = class Resolver {
774
+ static #patternCache = /* @__PURE__ */ new Map();
775
+ static #optionsCache = /* @__PURE__ */ new WeakMap();
776
+ pluginName;
777
+ #options;
778
+ constructor(options) {
779
+ this.pluginName = options.pluginName;
780
+ this.#options = options;
781
+ this.#apply(options);
782
+ }
783
+ /**
784
+ * The built-in resolution machinery. Always reaches the untouched defaults, even when a
785
+ * plugin overrides the top-level `name` or `file`.
786
+ */
787
+ get default() {
954
788
  return {
955
- ...options,
956
- ...overrideOptions
789
+ name: require_usingCtx.camelCase,
790
+ options: this.#resolveOptions.bind(this),
791
+ path: this.#resolvePath.bind(this),
792
+ file: this.#resolveFile.bind(this),
793
+ banner: this.#resolveBanner.bind(this),
794
+ footer: this.#resolveFooter.bind(this)
957
795
  };
958
796
  }
959
- if (_kubb_ast.schemaDef.is(node)) {
960
- if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null;
961
- if (include) {
962
- const applicable = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)).filter((result) => result !== null);
963
- if (applicable.length > 0 && !applicable.includes(true)) return null;
797
+ name(name) {
798
+ return this.default.name(name);
799
+ }
800
+ file(params, context) {
801
+ return this.default.file(params, context);
802
+ }
803
+ /**
804
+ * Merges `override` over `base` and returns a new resolver with helpers re-bound.
805
+ * Each key is replaced wholesale. Used when applying `setResolver` partial overrides.
806
+ */
807
+ static merge(base, override) {
808
+ const patch = override instanceof Resolver ? override.#options : override;
809
+ return createResolver({
810
+ ...base.#options,
811
+ ...patch
812
+ });
813
+ }
814
+ /**
815
+ * Binds each entry of `options` onto the resolver, so `this.name`, `this.default`, and
816
+ * `this.file` resolve there for top-level helpers and namespace methods alike. `default`
817
+ * is skipped so it can't be shadowed.
818
+ */
819
+ #apply(options) {
820
+ const root = this;
821
+ const bind = (value) => typeof value === "function" ? value.bind(root) : value;
822
+ for (const [key, value] of Object.entries(options)) {
823
+ if (key === "pluginName" || key === "default" || value === void 0) continue;
824
+ root[key] = isNamespace(value) ? Object.fromEntries(Object.entries(value).map(([method, member]) => [method, bind(member)])) : bind(value);
964
825
  }
965
- const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options;
966
- return {
967
- ...options,
968
- ...overrideOptions
969
- };
970
826
  }
971
- return options;
972
- }
973
- function defaultResolveOptions(node, { options, exclude = [], include, override = [] }) {
974
- if (typeof options !== "object" || options === null) return computeOptions(node, options, exclude, include, override);
975
- let byOptions = resolveOptionsCache.get(options);
976
- if (!byOptions) {
977
- byOptions = /* @__PURE__ */ new WeakMap();
978
- resolveOptionsCache.set(options, byOptions);
979
- }
980
- const cached = byOptions.get(node);
981
- if (cached !== void 0) return cached.value;
982
- const result = computeOptions(node, options, exclude, include, override);
983
- byOptions.set(node, { value: result });
984
- return result;
985
- }
986
- /**
987
- * Default path resolver used by `defineResolver`.
988
- *
989
- * - `mode: 'file'` resolves directly to `output.path` (the full file path, extension included).
990
- * - `mode: 'directory'` (default) resolves to `output.path/{baseName}`, or into a
991
- * subdirectory when `group` and a `tag`/`path` value are provided.
992
- *
993
- * A custom `group.name` function overrides the default subdirectory naming.
994
- * For `tag` groups the default is the camelCased tag.
995
- * For `path` groups the default is the first path segment after `/`.
996
- *
997
- * @example Flat output
998
- * ```ts
999
- * defaultResolvePath({ baseName: 'petTypes.ts' }, { root: '/src', output: { path: 'types' } })
1000
- * // → '/src/types/petTypes.ts'
1001
- * ```
1002
- *
1003
- * @example Tag-based grouping
1004
- * ```ts
1005
- * defaultResolvePath(
1006
- * { baseName: 'petTypes.ts', tag: 'pets' },
1007
- * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
1008
- * )
1009
- * // → '/src/types/pets/petTypes.ts'
1010
- * ```
1011
- *
1012
- * @example Path-based grouping
1013
- * ```ts
1014
- * defaultResolvePath(
1015
- * { baseName: 'petTypes.ts', path: '/pets/list' },
1016
- * { root: '/src', output: { path: 'types' }, group: { type: 'path' } },
1017
- * )
1018
- * // → '/src/types/pets/petTypes.ts'
1019
- * ```
1020
- *
1021
- * @example Single file (`mode: 'file'`)
1022
- * ```ts
1023
- * defaultResolvePath(
1024
- * { baseName: 'petTypes.ts' },
1025
- * { root: '/src', output: { path: 'types.ts', mode: 'file' } },
1026
- * )
1027
- * // → '/src/types.ts'
1028
- * ```
1029
- */
1030
- function defaultResolvePath({ baseName, tag, path: groupPath }, { root, output, group }) {
1031
- if ((output.mode ?? "directory") === "file") return node_path.default.resolve(root, output.path);
1032
- const result = (() => {
1033
- if (group && (groupPath || tag)) {
1034
- const groupValue = group.type === "path" ? groupPath : tag;
1035
- const defaultName = group.type === "tag" ? ({ group: groupName }) => require_memoryStorage.camelCase(groupName) : ({ group: groupName }) => {
1036
- const segment = groupName.split("/").filter((part) => part !== "" && part !== "." && part !== "..")[0];
1037
- return segment ? require_memoryStorage.camelCase(segment) : "";
827
+ static #testPattern(value, pattern) {
828
+ if (typeof pattern === "string") {
829
+ let regex = Resolver.#patternCache.get(pattern);
830
+ regex ??= new RegExp(pattern);
831
+ Resolver.#patternCache.set(pattern, regex);
832
+ return regex.test(value);
833
+ }
834
+ return value.match(pattern) !== null;
835
+ }
836
+ static #matchesOperation(node, { type, pattern }) {
837
+ if (type === "tag") return node.tags.some((tag) => Resolver.#testPattern(tag, pattern));
838
+ if (type === "operationId") return Resolver.#testPattern(node.operationId, pattern);
839
+ if (type === "path") return node.path !== void 0 && Resolver.#testPattern(node.path, pattern);
840
+ if (type === "method") return node.method !== void 0 && Resolver.#testPattern(node.method.toLowerCase(), pattern);
841
+ if (type === "contentType") return node.requestBody?.content?.some((c) => Resolver.#testPattern(c.contentType, pattern)) ?? false;
842
+ return false;
843
+ }
844
+ /**
845
+ * Returns `null` when the filter type doesn't apply to schemas, so include rules built
846
+ * from operation filters (e.g. `tag`) don't exclude every schema.
847
+ */
848
+ static #matchesSchema(node, { type, pattern }) {
849
+ if (type === "schemaName") return node.name ? Resolver.#testPattern(node.name, pattern) : false;
850
+ return null;
851
+ }
852
+ static #computeOptions(node, { options, exclude = [], include, override = [] }) {
853
+ if (_kubb_ast.operationDef.is(node)) {
854
+ if (exclude.some((filter) => Resolver.#matchesOperation(node, filter))) return null;
855
+ if (include && !include.some((filter) => Resolver.#matchesOperation(node, filter))) return null;
856
+ return {
857
+ ...options,
858
+ ...override.find((filter) => Resolver.#matchesOperation(node, filter))?.options
1038
859
  };
1039
- const groupName = (group.name ?? defaultName)({ group: groupValue });
1040
- return node_path.default.resolve(root, output.path, groupName, baseName);
1041
860
  }
1042
- return node_path.default.resolve(root, output.path, baseName);
1043
- })();
1044
- const outputDir = node_path.default.resolve(root, output.path);
1045
- const outputDirWithSep = outputDir.endsWith(node_path.default.sep) ? outputDir : `${outputDir}${node_path.default.sep}`;
1046
- if (result !== outputDir && !result.startsWith(outputDirWithSep)) throw new Diagnostics.Error({
1047
- code: Diagnostics.code.pathTraversal,
1048
- severity: "error",
1049
- message: `Resolved path "${result}" is outside the output directory "${outputDir}".`,
1050
- help: "This can stem from a path traversal in the OpenAPI specification or a misconfigured `group.name` function. Keep generated paths within the output directory.",
1051
- location: { kind: "config" }
1052
- });
1053
- return result;
1054
- }
1055
- /**
1056
- * Default file resolver used by `defineResolver`.
1057
- *
1058
- * Resolves a `FileNode` by combining name resolution (`resolver.default`) with
1059
- * path resolution (`resolver.resolvePath`). The resolved file always has empty
1060
- * `sources`, `imports`, and `exports` arrays, which consumers populate separately.
1061
- *
1062
- * In `mode: 'file'` the name is omitted and the file sits directly at the output path.
1063
- *
1064
- * @example Resolve a schema file
1065
- * ```ts
1066
- * const file = defaultResolveFile.call(
1067
- * resolver,
1068
- * { name: 'pet', extname: '.ts' },
1069
- * { root: '/src', output: { path: 'types' } },
1070
- * )
1071
- * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
1072
- * ```
1073
- *
1074
- * @example Resolve an operation file with tag grouping
1075
- * ```ts
1076
- * const file = defaultResolveFile.call(
1077
- * resolver,
1078
- * { name: 'listPets', extname: '.ts', tag: 'pets' },
1079
- * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
1080
- * )
1081
- * // → { baseName: 'listPets.ts', path: '/src/types/pets/listPets.ts', ... }
1082
- * ```
1083
- */
1084
- function defaultResolveFile({ name, extname, tag, path: groupPath }, context) {
1085
- const baseName = `${(context.output.mode ?? "directory") === "file" ? "" : this.default(name, "file")}${extname}`;
1086
- const filePath = this.resolvePath({
1087
- baseName,
1088
- tag,
1089
- path: groupPath
1090
- }, context);
1091
- return _kubb_ast.ast.factory.createFile({
1092
- path: filePath,
1093
- baseName: node_path.default.basename(filePath),
1094
- meta: { pluginName: this.pluginName },
1095
- sources: [],
1096
- imports: [],
1097
- exports: []
1098
- });
1099
- }
1100
- /**
1101
- * Generates the default "Generated by Kubb" banner from config and optional node metadata.
1102
- */
1103
- function buildDefaultBanner({ title, description, version, config }) {
1104
- try {
1105
- const source = (() => {
1106
- if (Array.isArray(config.input)) {
1107
- const first = config.input[0];
1108
- if (first && "path" in first) return node_path.default.basename(first.path);
1109
- return "";
861
+ if (_kubb_ast.schemaDef.is(node)) {
862
+ if (exclude.some((filter) => Resolver.#matchesSchema(node, filter) === true)) return null;
863
+ if (include) {
864
+ const applicable = include.map((filter) => Resolver.#matchesSchema(node, filter)).filter((result) => result !== null);
865
+ if (applicable.length > 0 && !applicable.includes(true)) return null;
1110
866
  }
1111
- if (config.input && "path" in config.input) return node_path.default.basename(config.input.path);
1112
- if (config.input && "data" in config.input) return "text content";
1113
- return "";
1114
- })();
1115
- let banner = "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n";
1116
- if (config.output.defaultBanner === "simple") {
1117
- banner += "*/\n";
1118
- return banner;
867
+ return {
868
+ ...options,
869
+ ...override.find((filter) => Resolver.#matchesSchema(node, filter) === true)?.options
870
+ };
1119
871
  }
1120
- if (source) banner += `* Source: ${source}\n`;
1121
- if (title) banner += `* Title: ${title}\n`;
1122
- if (description) {
1123
- const formattedDescription = description.replace(/\n/gm, "\n* ");
1124
- banner += `* Description: ${formattedDescription}\n`;
872
+ return options;
873
+ }
874
+ /**
875
+ * Applies include/exclude filters and merges matching override options, caching the result
876
+ * per `(options, node)` pair. Returns `null` when the node is filtered out.
877
+ */
878
+ #resolveOptions(node, context) {
879
+ const { options } = context;
880
+ if (typeof options !== "object" || options === null) return Resolver.#computeOptions(node, context);
881
+ let byOptions = Resolver.#optionsCache.get(options);
882
+ if (!byOptions) {
883
+ byOptions = /* @__PURE__ */ new WeakMap();
884
+ Resolver.#optionsCache.set(options, byOptions);
1125
885
  }
1126
- if (version) banner += `* OpenAPI spec version: ${version}\n`;
1127
- banner += "*/\n";
1128
- return banner;
1129
- } catch (_error) {
1130
- return "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n*/";
886
+ const cached = byOptions.get(node);
887
+ if (cached) return cached.value;
888
+ const result = Resolver.#computeOptions(node, context);
889
+ byOptions.set(node, { value: result });
890
+ return result;
1131
891
  }
1132
- }
1133
- /**
1134
- * Default banner resolver. Returns the banner string for a generated file.
1135
- *
1136
- * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
1137
- * When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
1138
- * from the document metadata when `meta` is provided).
1139
- *
1140
- * - When `output.banner` is a function, calls it with the file's `BannerMeta` and returns the result.
1141
- * - When `output.banner` is a string, returns it directly.
1142
- * - When `config.output.defaultBanner` is `false`, returns `undefined`.
1143
- * - Otherwise returns the Kubb "Generated by Kubb" notice.
1144
- *
1145
- * @example String banner overrides default
1146
- * ```ts
1147
- * defaultResolveBanner(undefined, { output: { banner: '// my banner' }, config })
1148
- * // '// my banner'
1149
- * ```
1150
- *
1151
- * @example Function banner with metadata
1152
- * ```ts
1153
- * defaultResolveBanner(meta, { output: { banner: (m) => `// v${m.version}` }, config })
1154
- * // → '// v3.0.0'
1155
- * ```
1156
- *
1157
- * @example Function banner skips re-export files
1158
- * ```ts
1159
- * defaultResolveBanner(meta, { output: { banner: (m) => (m.isBarrel ? '' : "'use server'") }, config, file: { path, baseName, isBarrel: true } })
1160
- * // → ''
1161
- * ```
1162
- *
1163
- * @example No user banner, Kubb notice with OAS metadata
1164
- * ```ts
1165
- * defaultResolveBanner(meta, { config })
1166
- * // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
1167
- * ```
1168
- *
1169
- * @example Disabled default banner
1170
- * ```ts
1171
- * defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })
1172
- * // → null
1173
- * ```
1174
- */
1175
- function defaultResolveBanner(meta, { output, config, file }) {
1176
- if (typeof output?.banner === "function") return output.banner(buildBannerMeta({
1177
- meta,
1178
- file
1179
- }));
1180
- if (typeof output?.banner === "string") return output.banner;
1181
- if (config.output.defaultBanner === false) return null;
1182
- return buildDefaultBanner({
1183
- title: meta?.title,
1184
- version: meta?.version,
1185
- config
1186
- });
1187
- }
1188
- /**
1189
- * Default footer resolver. Returns the footer string for a generated file.
1190
- *
1191
- * - When `output.footer` is a function, calls it with the file's `BannerMeta` and returns the result.
1192
- * - When `output.footer` is a string, returns it directly.
1193
- * - Otherwise returns `undefined`.
1194
- *
1195
- * @example String footer
1196
- * ```ts
1197
- * defaultResolveFooter(undefined, { output: { footer: '// end of file' }, config })
1198
- * // '// end of file'
1199
- * ```
1200
- *
1201
- * @example Function footer with metadata
1202
- * ```ts
1203
- * defaultResolveFooter(meta, { output: { footer: (m) => `// ${m.title}` }, config })
1204
- * // '// Pet Store'
1205
- * ```
1206
- */
1207
- function defaultResolveFooter(meta, { output, file }) {
1208
- if (typeof output?.footer === "function") return output.footer(buildBannerMeta({
1209
- meta,
1210
- file
1211
- }));
1212
- if (typeof output?.footer === "string") return output.footer;
1213
- return null;
1214
- }
892
+ /**
893
+ * A custom `group.name` wins; otherwise `tag` groups use the camelCased tag and `path`
894
+ * groups use the first non-traversal segment (`''` when none remain, placing the file in
895
+ * the output root, kept safe by the caller's boundary check).
896
+ */
897
+ static #resolveGroupDir(group, groupValue) {
898
+ if (group.name) return group.name({ group: groupValue });
899
+ if (group.type === "tag") return require_usingCtx.camelCase(groupValue);
900
+ const segment = groupValue.split("/").filter((part) => part !== "" && part !== "." && part !== "..")[0];
901
+ return segment ? require_usingCtx.camelCase(segment) : "";
902
+ }
903
+ /**
904
+ * `mode: 'file'` resolves directly to `output.path`. `mode: 'directory'` (default) resolves
905
+ * to `output.path/{baseName}`, or into a subdirectory when `group` and a `tag`/`path` value
906
+ * are provided.
907
+ */
908
+ #resolvePath({ baseName, tag, path: groupPath }, { root, output, group }) {
909
+ if (output.mode === "file") return node_path.default.resolve(root, output.path);
910
+ const outputDir = node_path.default.resolve(root, output.path);
911
+ 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);
912
+ const outputDirWithSep = outputDir.endsWith(node_path.default.sep) ? outputDir : `${outputDir}${node_path.default.sep}`;
913
+ if (result !== outputDir && !result.startsWith(outputDirWithSep)) throw new Diagnostics.Error({
914
+ code: Diagnostics.code.pathTraversal,
915
+ severity: "error",
916
+ message: `Resolved path "${result}" is outside the output directory "${outputDir}".`,
917
+ 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.",
918
+ location: { kind: "config" }
919
+ });
920
+ return result;
921
+ }
922
+ /**
923
+ * Builds a `FileNode` by combining file-name casing (`params.resolveName`) with path
924
+ * resolution. The resolved file starts with empty `sources`, `imports`, and `exports`,
925
+ * which consumers populate separately.
926
+ */
927
+ #resolveFile({ name, extname, tag, path: groupPath, resolveName = require_usingCtx.toFilePath }, context) {
928
+ const resolvedName = context.output.mode === "file" ? "" : resolveName(name);
929
+ const filePath = this.#resolvePath({
930
+ baseName: `${resolvedName}${extname}`,
931
+ tag,
932
+ path: groupPath
933
+ }, context);
934
+ return _kubb_ast.ast.factory.createFile({
935
+ path: filePath,
936
+ baseName: node_path.default.basename(filePath),
937
+ meta: { pluginName: this.pluginName },
938
+ sources: [],
939
+ imports: [],
940
+ exports: []
941
+ });
942
+ }
943
+ /**
944
+ * Missing fields default to empty/`false` so the `BannerMeta` shape stays stable even when
945
+ * a caller (e.g. the barrel plugin) has no document metadata.
946
+ */
947
+ static #buildBannerMeta(meta, file) {
948
+ return {
949
+ title: meta?.title,
950
+ description: meta?.description,
951
+ version: meta?.version,
952
+ baseURL: meta?.baseURL,
953
+ circularNames: meta?.circularNames ?? [],
954
+ enumNames: meta?.enumNames ?? [],
955
+ filePath: file?.path ?? "",
956
+ baseName: file?.baseName ?? "",
957
+ isBarrel: file?.isBarrel ?? false,
958
+ isAggregation: file?.isAggregation ?? false
959
+ };
960
+ }
961
+ /**
962
+ * Resolves a user-configured banner/footer value. `undefined` means not configured.
963
+ */
964
+ static #resolveUserText(value, meta, file) {
965
+ if (typeof value === "function") return value(Resolver.#buildBannerMeta(meta, file));
966
+ if (typeof value === "string") return value;
967
+ }
968
+ static #buildDefaultBanner({ title, version, config }) {
969
+ const lines = [
970
+ "/**",
971
+ "* Generated by Kubb (https://kubb.dev/).",
972
+ "* Do not edit manually."
973
+ ];
974
+ if (config.output.defaultBanner !== "simple") {
975
+ const input = Array.isArray(config.input) ? config.input[0] : config.input;
976
+ const source = input && "path" in input ? node_path.default.basename(input.path) : input && "data" in input ? "text content" : "";
977
+ if (source) lines.push(`* Source: ${source}`);
978
+ if (title) lines.push(`* Title: ${title}`);
979
+ if (version) lines.push(`* OpenAPI spec version: ${version}`);
980
+ }
981
+ return `${lines.join("\n")}\n*/\n`;
982
+ }
983
+ /**
984
+ * A user-supplied `output.banner` overrides the default Kubb notice. When
985
+ * `config.output.defaultBanner` is `false` and no user banner is set, returns `null`.
986
+ */
987
+ #resolveBanner(meta, { output, config, file }) {
988
+ const userBanner = Resolver.#resolveUserText(output?.banner, meta, file);
989
+ if (userBanner !== void 0) return userBanner;
990
+ if (config.output.defaultBanner === false) return null;
991
+ return Resolver.#buildDefaultBanner({
992
+ title: meta?.title,
993
+ version: meta?.version,
994
+ config
995
+ });
996
+ }
997
+ #resolveFooter(meta, { output, file }) {
998
+ return Resolver.#resolveUserText(output?.footer, meta, file) ?? null;
999
+ }
1000
+ };
1215
1001
  /**
1216
- * Defines a plugin resolver. The resolver is the object that decides what
1217
- * every generated symbol and file path is called. Built-in defaults handle
1218
- * name casing, include/exclude/override filtering, output path computation,
1219
- * and file construction. Supply your own to override any of them:
1220
- *
1221
- * - `default` sets the name casing strategy (camelCase or PascalCase).
1222
- * - `resolveOptions` does include/exclude/override filtering.
1223
- * - `resolvePath` computes the output path.
1224
- * - `resolveFile` builds the full `FileNode`.
1225
- * - `resolveBanner` and `resolveFooter` produce the top and bottom of file text.
1226
- *
1227
- * Methods in the returned object can call sibling resolver methods via `this`.
1228
- * A custom rule can delegate to a default, for example `this.default(name, 'type')`.
1002
+ * Defines a plugin resolver, the object that decides what every generated symbol and file
1003
+ * path is called. Override the top-level `name` and `file` to set the plugin's conventions,
1004
+ * and add your own naming helpers, top-level (`typeName`, …) or grouped in namespaces
1005
+ * (`query`, `schema`, …). Every method reaches sibling helpers and the built-in machinery
1006
+ * through `this.name`, `this.file`, and `this.default`.
1229
1007
  *
1230
- * @example Basic resolver with naming helpers
1008
+ * @example Custom identifier and file casing
1231
1009
  * ```ts
1232
- * export const resolverTs = defineResolver<PluginTs>(() => ({
1233
- * name: 'default',
1234
- * resolveName(name) {
1235
- * return this.default(name, 'function')
1010
+ * export const resolverTs = createResolver<PluginTs>({
1011
+ * pluginName: 'plugin-ts',
1012
+ * name(name) {
1013
+ * return ensureValidVarName(pascalCase(name))
1236
1014
  * },
1237
- * resolveTypeName(name) {
1238
- * return this.default(name, 'type')
1015
+ * file(params, context) {
1016
+ * return this.default.file({ ...params, resolveName: (name) => toFilePath(name, pascalCase) }, context)
1239
1017
  * },
1240
- * }))
1241
- * ```
1242
- *
1243
- * @example Custom output path
1244
- * ```ts
1245
- * import path from 'node:path'
1246
- *
1247
- * export const resolverTs = defineResolver<PluginTs>(() => ({
1248
- * name: 'custom',
1249
- * resolvePath({ baseName }, { root, output }) {
1250
- * return path.resolve(root, output.path, 'generated', baseName)
1251
- * },
1252
- * }))
1018
+ * })
1253
1019
  * ```
1254
1020
  */
1255
- function defineResolver(build) {
1256
- let resolver;
1257
- resolver = {
1258
- default: defaultResolver,
1259
- resolveOptions: defaultResolveOptions,
1260
- resolvePath: defaultResolvePath,
1261
- resolveFile: (params, context) => defaultResolveFile.call(resolver, params, context),
1262
- resolveBanner: defaultResolveBanner,
1263
- resolveFooter: defaultResolveFooter,
1264
- ...build()
1265
- };
1266
- return resolver;
1021
+ function createResolver(options) {
1022
+ return new Resolver(options);
1267
1023
  }
1268
1024
  //#endregion
1269
1025
  //#region src/Transform.ts
@@ -1284,12 +1040,6 @@ var Transform = class {
1284
1040
  #composed = /* @__PURE__ */ new Map();
1285
1041
  #memo = /* @__PURE__ */ new Map();
1286
1042
  /**
1287
- * Number of plugins with at least one registered macro.
1288
- */
1289
- get size() {
1290
- return this.#macros.size;
1291
- }
1292
- /**
1293
1043
  * Appends `macro` to the plugin's list, after any macros already registered.
1294
1044
  */
1295
1045
  add(pluginName, macro) {
@@ -1306,12 +1056,6 @@ var Transform = class {
1306
1056
  this.#invalidate(pluginName);
1307
1057
  }
1308
1058
  /**
1309
- * Looks up the composed visitor for `pluginName`, or `undefined` when the plugin has no macros.
1310
- */
1311
- get(pluginName) {
1312
- return this.#visitorFor(pluginName);
1313
- }
1314
- /**
1315
1059
  * Runs the plugin's macros on `node`. Returns the original node reference when the plugin has no
1316
1060
  * macros, so callers can compare by identity to detect a no-op.
1317
1061
  */
@@ -1355,20 +1099,49 @@ var Transform = class {
1355
1099
  };
1356
1100
  //#endregion
1357
1101
  //#region src/KubbDriver.ts
1358
- function enforceOrder(enforce) {
1359
- return enforce === "pre" ? -1 : enforce === "post" ? 1 : 0;
1102
+ const ENFORCE_ORDER = {
1103
+ pre: -1,
1104
+ post: 1
1105
+ };
1106
+ /**
1107
+ * Orders plugins so every dependency runs before its dependents (Kahn's algorithm), with
1108
+ * `enforce` (`'pre'` before normal before `'post'`) and declaration order as tiebreaks.
1109
+ * A pairwise `Array.sort` comparator cannot do this: dependency relations are not transitive
1110
+ * at the comparator level, so a chain where A depends on B and B depends on C could come out
1111
+ * wrong when A and C are never compared directly. Dependencies on plugins missing from the
1112
+ * config are ignored here and surface later through `requirePlugin`.
1113
+ */
1114
+ function sortPlugins(plugins) {
1115
+ const queue = [...plugins].sort((a, b) => (a.enforce ? ENFORCE_ORDER[a.enforce] : 0) - (b.enforce ? ENFORCE_ORDER[b.enforce] : 0));
1116
+ const names = new Set(queue.map((plugin) => plugin.name));
1117
+ const blockedBy = new Map(queue.map((plugin) => [plugin.name, new Set(plugin.dependencies?.filter((name) => names.has(name) && name !== plugin.name))]));
1118
+ const sorted = [];
1119
+ while (queue.length > 0) {
1120
+ const index = queue.findIndex((plugin) => blockedBy.get(plugin.name)?.size === 0);
1121
+ if (index === -1) throw new Diagnostics.Error({
1122
+ code: Diagnostics.code.invalidPluginOptions,
1123
+ severity: "error",
1124
+ message: `Plugin dependencies form a cycle: ${queue.map((plugin) => plugin.name).join(" → ")}.`,
1125
+ help: "Remove one of the `dependencies` entries so the plugins can be ordered.",
1126
+ location: { kind: "config" }
1127
+ });
1128
+ const [plugin] = queue.splice(index, 1);
1129
+ if (!plugin) break;
1130
+ sorted.push(plugin);
1131
+ for (const blockers of blockedBy.values()) blockers.delete(plugin.name);
1132
+ }
1133
+ return sorted;
1360
1134
  }
1361
1135
  var KubbDriver = class {
1362
1136
  config;
1363
1137
  options;
1364
1138
  /**
1365
- * The streaming `InputNode<true>` produced by the adapter. Set after adapter setup.
1366
- * Parse-only adapters are wrapped automatically.
1139
+ * The `InputNode` produced by the adapter. Set after adapter setup.
1367
1140
  */
1368
1141
  inputNode = null;
1369
1142
  adapter = null;
1370
1143
  /**
1371
- * Raw adapter source so `adapter.parse()` / `adapter.stream()` can run lazily.
1144
+ * Raw adapter source so `adapter.parse()` can run lazily.
1372
1145
  * Intentionally outlives the build, cleared by `dispose()`.
1373
1146
  */
1374
1147
  #adapterSource = null;
@@ -1377,7 +1150,7 @@ var KubbDriver = class {
1377
1150
  * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
1378
1151
  * add files. This property gives direct read/write access when needed.
1379
1152
  */
1380
- fileManager = new require_memoryStorage.FileManager();
1153
+ fileManager = new require_usingCtx.FileManager();
1381
1154
  plugins = /* @__PURE__ */ new Map();
1382
1155
  /**
1383
1156
  * Tracks which plugins have generators registered via `addGenerator()` (event-based path).
@@ -1416,15 +1189,8 @@ var KubbDriver = class {
1416
1189
  * so `run` can parse it later.
1417
1190
  */
1418
1191
  async setup() {
1419
- const normalized = this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin));
1420
- const dependenciesByName = new Map(normalized.map((plugin) => [plugin.name, new Set(plugin.dependencies ?? [])]));
1421
- normalized.sort((a, b) => {
1422
- if (dependenciesByName.get(b.name)?.has(a.name)) return -1;
1423
- if (dependenciesByName.get(a.name)?.has(b.name)) return 1;
1424
- return enforceOrder(a.enforce) - enforceOrder(b.enforce);
1425
- });
1192
+ const normalized = sortPlugins(this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin)));
1426
1193
  for (const plugin of normalized) {
1427
- if (plugin.apply) plugin.apply(this.config);
1428
1194
  this.#registerPlugin(plugin);
1429
1195
  this.plugins.set(plugin.name, plugin);
1430
1196
  }
@@ -1435,11 +1201,11 @@ var KubbDriver = class {
1435
1201
  }
1436
1202
  /**
1437
1203
  * Builds a `NormalizedPlugin` from a hook-style plugin, filling in default
1438
- * options and copying `apply` when present. Registering its lifecycle handlers
1439
- * on the `AsyncEventEmitter` is done separately by `#registerPlugin`.
1204
+ * options. Registering its lifecycle handlers on the `AsyncEventEmitter` is
1205
+ * done separately by `#registerPlugin`.
1440
1206
  */
1441
1207
  #normalizePlugin(plugin) {
1442
- const normalized = {
1208
+ return {
1443
1209
  name: plugin.name,
1444
1210
  dependencies: plugin.dependencies,
1445
1211
  enforce: plugin.enforce,
@@ -1453,30 +1219,14 @@ var KubbDriver = class {
1453
1219
  override: []
1454
1220
  }
1455
1221
  };
1456
- if ("apply" in plugin && typeof plugin.apply === "function") normalized.apply = plugin.apply;
1457
- return normalized;
1458
1222
  }
1459
1223
  /**
1460
1224
  * Parses the adapter source into `this.inputNode`. Idempotent, so repeated calls from
1461
- * `run` do not re-parse. Adapters with `stream()` are used directly.
1462
- * Adapters with only `parse()` are wrapped via `ast.factory.createInput({ stream: true })` so the dispatch loop
1463
- * stays stream-only.
1225
+ * `run` do not re-parse.
1464
1226
  */
1465
1227
  async #parseInput() {
1466
1228
  if (this.inputNode || !this.adapter || !this.#adapterSource) return;
1467
- const adapter = this.adapter;
1468
- const source = this.#adapterSource;
1469
- if (adapter.stream) {
1470
- this.inputNode = await adapter.stream(source);
1471
- return;
1472
- }
1473
- const parsed = await adapter.parse(source);
1474
- this.inputNode = _kubb_ast.ast.factory.createInput({
1475
- stream: true,
1476
- schemas: arrayToAsyncIterable(parsed.schemas),
1477
- operations: arrayToAsyncIterable(parsed.operations),
1478
- meta: parsed.meta
1479
- });
1229
+ this.inputNode = await this.adapter.parse(this.#adapterSource);
1480
1230
  }
1481
1231
  /**
1482
1232
  * Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.
@@ -1590,10 +1340,10 @@ var KubbDriver = class {
1590
1340
  }
1591
1341
  /**
1592
1342
  * Returns `true` when at least one generator was registered for the given plugin
1593
- * via `addGenerator()` in `kubb:plugin:setup` (event-based path).
1343
+ * via `addGenerator()` in `kubb:plugin:setup`.
1594
1344
  *
1595
1345
  * Used by the build loop to decide whether to walk the AST and emit generator events
1596
- * for a plugin that has no static `plugin.generators`.
1346
+ * for a plugin.
1597
1347
  */
1598
1348
  hasEventGenerators(pluginName) {
1599
1349
  return this.#eventGeneratorPlugins.has(pluginName);
@@ -1605,34 +1355,28 @@ var KubbDriver = class {
1605
1355
  * contributes a `timing` diagnostic for the run summary.
1606
1356
  */
1607
1357
  async run({ storage }) {
1608
- const { hooks, config } = this;
1358
+ const { hooks, config, fileManager } = this;
1609
1359
  const diagnostics = [];
1610
1360
  const parsersMap = /* @__PURE__ */ new Map();
1611
1361
  for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
1612
- const processor = new require_memoryStorage.FileProcessor({
1613
- parsers: parsersMap,
1614
- storage,
1615
- extension: config.output.extension
1616
- });
1617
- processor.hooks.on("start", async (files) => {
1362
+ const onWriteStart = async (files) => {
1618
1363
  await hooks.emit("kubb:files:processing:start", { files });
1619
- });
1364
+ };
1620
1365
  const updateBuffer = [];
1621
- processor.hooks.on("update", (item) => {
1366
+ const onWriteUpdate = (item) => {
1622
1367
  updateBuffer.push(item);
1623
- });
1624
- processor.hooks.on("end", async (files) => {
1368
+ };
1369
+ const onWriteEnd = async (files) => {
1625
1370
  await hooks.emit("kubb:files:processing:update", { files: updateBuffer.map((item) => ({
1626
1371
  ...item,
1627
1372
  config
1628
1373
  })) });
1629
1374
  updateBuffer.length = 0;
1630
1375
  await hooks.emit("kubb:files:processing:end", { files });
1631
- });
1632
- const onFileUpsert = (file) => {
1633
- processor.enqueue(file);
1634
1376
  };
1635
- this.fileManager.hooks.on("upsert", onFileUpsert);
1377
+ fileManager.hooks.on("start", onWriteStart);
1378
+ fileManager.hooks.on("update", onWriteUpdate);
1379
+ fileManager.hooks.on("end", onWriteEnd);
1636
1380
  return Diagnostics.scope((diagnostic) => diagnostics.push(diagnostic), async () => {
1637
1381
  try {
1638
1382
  const outputRoot = (0, node_path.resolve)(config.root, config.output.path);
@@ -1687,10 +1431,13 @@ var KubbDriver = class {
1687
1431
  success: true
1688
1432
  });
1689
1433
  }
1690
- diagnostics.push(...await this.#runGenerators(generatorPlugins, () => processor.flush()));
1691
- await processor.drain();
1434
+ diagnostics.push(...await this.#runGenerators(generatorPlugins));
1692
1435
  await hooks.emit("kubb:plugins:end", Object.assign({ config }, this.#filesPayload()));
1693
- await processor.drain();
1436
+ await fileManager.write(fileManager.files, {
1437
+ storage,
1438
+ parsers: parsersMap,
1439
+ extension: config.output.extension
1440
+ });
1694
1441
  await hooks.emit("kubb:build:end", {
1695
1442
  files: this.fileManager.files,
1696
1443
  config,
@@ -1701,7 +1448,9 @@ var KubbDriver = class {
1701
1448
  diagnostics.push(Diagnostics.from(caughtError));
1702
1449
  return { diagnostics: Diagnostics.dedupe(diagnostics) };
1703
1450
  } finally {
1704
- this.fileManager.hooks.off("upsert", onFileUpsert);
1451
+ fileManager.hooks.off("start", onWriteStart);
1452
+ fileManager.hooks.off("update", onWriteUpdate);
1453
+ fileManager.hooks.off("end", onWriteEnd);
1705
1454
  }
1706
1455
  });
1707
1456
  }
@@ -1724,22 +1473,21 @@ var KubbDriver = class {
1724
1473
  }, this.#filesPayload()));
1725
1474
  }
1726
1475
  /**
1727
- * Streams schemas and operations through every plugin's generators. Each node is run
1476
+ * Runs schemas and operations through every plugin's generators. Each node is run
1728
1477
  * through the plugin's macros (from `this.#transforms`) before the generator sees it,
1729
1478
  * so plugins stay isolated and the hot path stays per-node. Schemas run before operations
1730
- * because the two passes share `flushPending` and the FileProcessor's event emitter.
1479
+ * so file output stays deterministic across runs.
1731
1480
  * A failing plugin contributes an error diagnostic so the rest of the build continues.
1732
1481
  * Every plugin also contributes a `timing` diagnostic.
1733
1482
  *
1734
- * Plugins run sequentially so `kubb:plugin:end` fires as each plugin completes, instead
1735
- * of all at once after every plugin has marched through the parallel batches together.
1736
- * That ordering is what drives the CLI's `Plugins N/M` counter. Without it the bar would
1737
- * sit at the initial value until the very end of the run.
1483
+ * Plugins are processed one at a time, in full, so `kubb:plugin:end` fires as each one
1484
+ * completes rather than all at once at the end. That ordering drives the CLI's
1485
+ * `Plugins N/M` counter.
1738
1486
  *
1739
1487
  * When `this.inputNode` is `null`, every entry still gets a `kubb:plugin:end` so
1740
1488
  * post-plugin listeners (the barrel writer and friends) complete.
1741
1489
  */
1742
- async #runGenerators(entries, flushPending) {
1490
+ async #runGenerators(entries) {
1743
1491
  const diagnostics = [];
1744
1492
  if (entries.length === 0) return diagnostics;
1745
1493
  if (!this.inputNode) {
@@ -1759,160 +1507,104 @@ var KubbDriver = class {
1759
1507
  }
1760
1508
  const transforms = this.#transforms;
1761
1509
  const { schemas, operations } = this.inputNode;
1762
- const states = entries.map(({ plugin, context, hrStart }) => {
1763
- const { exclude, include, override } = plugin.options;
1764
- const hasExclude = Array.isArray(exclude) && exclude.length > 0;
1765
- const hasInclude = Array.isArray(include) && include.length > 0;
1766
- const hasOverride = Array.isArray(override) && override.length > 0;
1767
- return {
1768
- plugin,
1769
- generatorContext: {
1770
- ...context,
1771
- resolver: this.getResolver(plugin.name)
1772
- },
1773
- generators: plugin.generators ?? [],
1774
- hrStart,
1775
- failed: false,
1776
- error: null,
1777
- optionsAreStatic: !hasExclude && !hasInclude && !hasOverride,
1778
- allowedSchemaNames: null
1779
- };
1780
- });
1781
1510
  const emitsSchemaHook = this.hooks.listenerCount("kubb:generate:schema") > 0;
1782
1511
  const emitsOperationHook = this.hooks.listenerCount("kubb:generate:operation") > 0;
1783
1512
  const emitsOperationsHook = this.hooks.listenerCount("kubb:generate:operations") > 0;
1784
- const schemasBuffer = await Array.fromAsync(schemas);
1785
- const operationsBuffer = await Array.fromAsync(operations);
1786
- const pruningStates = states.filter(({ plugin }) => {
1787
- const { include } = plugin.options;
1788
- return (include?.some(({ type }) => require_memoryStorage.OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false);
1789
- });
1790
- if (pruningStates.length > 0) {
1791
- const includedOpsByState = new Map(pruningStates.map((state) => [state, []]));
1792
- for (const operation of operationsBuffer) for (const state of pruningStates) {
1793
- const { exclude, include, override } = state.plugin.options;
1794
- if (state.generatorContext.resolver.resolveOptions(operation, {
1795
- options: state.plugin.options,
1796
- exclude,
1797
- include,
1798
- override
1799
- }) !== null) includedOpsByState.get(state)?.push(operation);
1800
- }
1801
- for (const state of pruningStates) {
1802
- state.allowedSchemaNames = (0, _kubb_ast.collectUsedSchemaNames)(includedOpsByState.get(state) ?? [], schemasBuffer);
1803
- includedOpsByState.delete(state);
1804
- }
1805
- }
1806
- const resolveForPlugin = (state, node) => {
1807
- const { plugin, generatorContext } = state;
1808
- const transformedNode = transforms.applyTo(plugin.name, node);
1809
- if (state.optionsAreStatic) return {
1810
- transformedNode,
1811
- options: plugin.options
1812
- };
1513
+ const allowedSchemaNamesByPlugin = /* @__PURE__ */ new Map();
1514
+ for (const { plugin } of entries) {
1813
1515
  const { exclude, include, override } = plugin.options;
1814
- const options = generatorContext.resolver.resolveOptions(transformedNode, {
1516
+ if (!((include?.some(({ type }) => OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false))) continue;
1517
+ const resolver = this.getResolver(plugin.name);
1518
+ const includedOps = operations.filter((operation) => resolver.default.options(operation, {
1815
1519
  options: plugin.options,
1816
1520
  exclude,
1817
1521
  include,
1818
1522
  override
1819
- });
1820
- if (options === null) return null;
1821
- return {
1822
- transformedNode,
1823
- options
1523
+ }) !== null);
1524
+ allowedSchemaNamesByPlugin.set(plugin.name, (0, _kubb_ast.collectUsedSchemaNames)(includedOps, schemas));
1525
+ }
1526
+ for (const { plugin, context, hrStart } of entries) {
1527
+ const generatorContext = {
1528
+ ...context,
1529
+ resolver: this.getResolver(plugin.name)
1824
1530
  };
1825
- };
1826
- const dispatchNode = async (state, node, dispatch) => {
1827
- if (state.failed) return;
1828
- try {
1829
- const resolved = resolveForPlugin(state, node);
1830
- if (!resolved) return;
1831
- const { transformedNode, options } = resolved;
1832
- if (dispatch.checkAllowedNames && state.allowedSchemaNames !== null && "name" in transformedNode && transformedNode.name && !state.allowedSchemaNames.has(transformedNode.name)) return;
1833
- const ctx = {
1834
- ...state.generatorContext,
1531
+ const { exclude, include, override } = plugin.options;
1532
+ const optionsAreStatic = !exclude?.length && !include?.length && !override?.length;
1533
+ const allowedSchemaNames = allowedSchemaNamesByPlugin.get(plugin.name) ?? null;
1534
+ let error = null;
1535
+ const resolveForPlugin = (node) => {
1536
+ const transformedNode = transforms.applyTo(plugin.name, node);
1537
+ if (optionsAreStatic) return {
1538
+ transformedNode,
1539
+ options: plugin.options
1540
+ };
1541
+ const options = generatorContext.resolver.default.options(transformedNode, {
1542
+ options: plugin.options,
1543
+ exclude,
1544
+ include,
1545
+ override
1546
+ });
1547
+ if (options === null) return null;
1548
+ return {
1549
+ transformedNode,
1835
1550
  options
1836
1551
  };
1837
- for (const gen of state.generators) {
1838
- const run = gen[dispatch.method];
1839
- if (!run) continue;
1840
- const raw = run(transformedNode, ctx);
1841
- const result = isPromise(raw) ? await raw : raw;
1842
- const applied = this.dispatch({
1843
- result,
1844
- renderer: gen.renderer
1552
+ };
1553
+ if (emitsSchemaHook) for (const node of schemas) {
1554
+ if (error) break;
1555
+ try {
1556
+ const resolved = resolveForPlugin(node);
1557
+ if (!resolved) continue;
1558
+ const { transformedNode, options } = resolved;
1559
+ if (allowedSchemaNames !== null && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) continue;
1560
+ await this.hooks.emit("kubb:generate:schema", transformedNode, {
1561
+ ...generatorContext,
1562
+ options
1845
1563
  });
1846
- if (isPromise(applied)) await applied;
1564
+ } catch (caughtError) {
1565
+ error = caughtError;
1847
1566
  }
1848
- if (dispatch.emit) await dispatch.emit(transformedNode, ctx);
1849
- } catch (caughtError) {
1850
- state.failed = true;
1851
- state.error = caughtError;
1852
1567
  }
1853
- };
1854
- const schemaDispatch = {
1855
- method: "schema",
1856
- checkAllowedNames: true,
1857
- emit: emitsSchemaHook ? (node, ctx) => this.hooks.emit("kubb:generate:schema", node, ctx) : null
1858
- };
1859
- const operationDispatch = {
1860
- method: "operation",
1861
- checkAllowedNames: false,
1862
- emit: emitsOperationHook ? (node, ctx) => this.hooks.emit("kubb:generate:operation", node, ctx) : null
1863
- };
1864
- const dispatchPass = async (state, nodes, dispatch) => {
1865
- let sinceFlush = 0;
1866
- for (const node of nodes) {
1867
- await dispatchNode(state, node, dispatch);
1868
- if (++sinceFlush >= 8) {
1869
- sinceFlush = 0;
1870
- await flushPending();
1568
+ if (emitsOperationHook) for (const node of operations) {
1569
+ if (error) break;
1570
+ try {
1571
+ const resolved = resolveForPlugin(node);
1572
+ if (!resolved) continue;
1573
+ await this.hooks.emit("kubb:generate:operation", resolved.transformedNode, {
1574
+ ...generatorContext,
1575
+ options: resolved.options
1576
+ });
1577
+ } catch (caughtError) {
1578
+ error = caughtError;
1871
1579
  }
1872
1580
  }
1873
- if (sinceFlush > 0) await flushPending();
1874
- };
1875
- for (const state of states) {
1876
- const needsOperationsAggregate = emitsOperationsHook || state.generators.some((gen) => !!gen.operations);
1877
- await dispatchPass(state, schemasBuffer, schemaDispatch);
1878
- await dispatchPass(state, operationsBuffer, operationDispatch);
1879
- if (!state.failed && needsOperationsAggregate) try {
1880
- const { plugin, generatorContext, generators } = state;
1581
+ if (!error && emitsOperationsHook) try {
1881
1582
  const ctx = {
1882
1583
  ...generatorContext,
1883
1584
  options: plugin.options
1884
1585
  };
1885
- const pluginOperations = operationsBuffer.reduce((acc, node) => {
1886
- const resolved = resolveForPlugin(state, node);
1586
+ const pluginOperations = operations.reduce((acc, node) => {
1587
+ const resolved = resolveForPlugin(node);
1887
1588
  if (resolved) acc.push(resolved.transformedNode);
1888
1589
  return acc;
1889
1590
  }, []);
1890
- for (const gen of generators) {
1891
- if (!gen.operations) continue;
1892
- const result = await gen.operations(pluginOperations, ctx);
1893
- await this.dispatch({
1894
- result,
1895
- renderer: gen.renderer
1896
- });
1897
- }
1898
1591
  await this.hooks.emit("kubb:generate:operations", pluginOperations, ctx);
1899
1592
  } catch (caughtError) {
1900
- state.failed = true;
1901
- state.error = caughtError;
1593
+ error = caughtError;
1902
1594
  }
1903
- const duration = getElapsedMs(state.hrStart);
1595
+ const duration = getElapsedMs(hrStart);
1904
1596
  await this.#emitPluginEnd({
1905
- plugin: state.plugin,
1597
+ plugin,
1906
1598
  duration,
1907
- success: !state.failed,
1908
- error: state.failed && state.error ? state.error : void 0
1599
+ success: !error,
1600
+ error: error ?? void 0
1909
1601
  });
1910
- if (state.failed && state.error) diagnostics.push({
1911
- ...Diagnostics.from(state.error),
1912
- plugin: state.plugin.name
1602
+ if (error) diagnostics.push({
1603
+ ...Diagnostics.from(error),
1604
+ plugin: plugin.name
1913
1605
  });
1914
1606
  diagnostics.push(Diagnostics.performance({
1915
- plugin: state.plugin.name,
1607
+ plugin: plugin.name,
1916
1608
  duration
1917
1609
  }));
1918
1610
  }
@@ -1932,7 +1624,7 @@ var KubbDriver = class {
1932
1624
  */
1933
1625
  async dispatch({ result, renderer }) {
1934
1626
  try {
1935
- var _usingCtx$2 = require_memoryStorage._usingCtx();
1627
+ var _usingCtx$2 = require_usingCtx._usingCtx();
1936
1628
  if (!result) return;
1937
1629
  if (Array.isArray(result)) {
1938
1630
  this.fileManager.upsert(...result);
@@ -1940,10 +1632,6 @@ var KubbDriver = class {
1940
1632
  }
1941
1633
  if (!renderer) return;
1942
1634
  const instance = _usingCtx$2.u(renderer());
1943
- if (instance.stream) {
1944
- for (const file of instance.stream(result)) this.fileManager.upsert(file);
1945
- return;
1946
- }
1947
1635
  await instance.render(result);
1948
1636
  this.fileManager.upsert(...instance.files);
1949
1637
  } catch (_) {
@@ -1972,26 +1660,21 @@ var KubbDriver = class {
1972
1660
  [Symbol.dispose]() {
1973
1661
  this.dispose();
1974
1662
  }
1975
- #getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => defineResolver(() => ({
1976
- name: "default",
1977
- pluginName
1978
- })));
1663
+ #getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => createResolver({ pluginName }));
1979
1664
  /**
1980
1665
  * Merges `partial` with the plugin's default resolver and stores the result.
1981
1666
  * Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver`
1982
1667
  * get the up-to-date resolver without going through `getResolver()`.
1983
1668
  */
1984
1669
  setPluginResolver(pluginName, partial) {
1985
- const merged = {
1986
- ...this.#getDefaultResolver(pluginName),
1987
- ...partial
1988
- };
1670
+ const defaultResolver = this.#getDefaultResolver(pluginName);
1671
+ const merged = Resolver.merge(defaultResolver, partial);
1989
1672
  this.#resolvers.set(pluginName, merged);
1990
1673
  const plugin = this.plugins.get(pluginName);
1991
1674
  if (plugin) plugin.resolver = merged;
1992
1675
  }
1993
1676
  getResolver(pluginName) {
1994
- return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#getDefaultResolver(pluginName);
1677
+ return this.#resolvers.get(pluginName) ?? this.#getDefaultResolver(pluginName);
1995
1678
  }
1996
1679
  getContext(plugin) {
1997
1680
  const driver = this;
@@ -2096,7 +1779,73 @@ function inputToAdapterSource(config) {
2096
1779
  };
2097
1780
  }
2098
1781
  //#endregion
1782
+ //#region src/createStorage.ts
1783
+ /**
1784
+ * Defines a custom storage backend. The builder receives user options and
1785
+ * returns a `Storage` implementation. Kubb ships with filesystem and in-memory
1786
+ * storages. A custom backend writes generated files elsewhere, such as cloud
1787
+ * storage or a database.
1788
+ *
1789
+ * @example In-memory storage (the built-in implementation)
1790
+ * ```ts
1791
+ * import { createStorage } from '@kubb/core'
1792
+ *
1793
+ * export const memoryStorage = createStorage(() => {
1794
+ * const store = new Map<string, string>()
1795
+ *
1796
+ * return {
1797
+ * name: 'memory',
1798
+ * async hasItem(key) {
1799
+ * return store.has(key)
1800
+ * },
1801
+ * async getItem(key) {
1802
+ * return store.get(key) ?? null
1803
+ * },
1804
+ * async setItem(key, value) {
1805
+ * store.set(key, value)
1806
+ * },
1807
+ * async removeItem(key) {
1808
+ * store.delete(key)
1809
+ * },
1810
+ * async getKeys(base) {
1811
+ * const keys = [...store.keys()]
1812
+ * return base ? keys.filter((k) => k.startsWith(base)) : keys
1813
+ * },
1814
+ * async clear(base) {
1815
+ * if (!base) store.clear()
1816
+ * },
1817
+ * }
1818
+ * })
1819
+ * ```
1820
+ */
1821
+ function createStorage(build) {
1822
+ return (options) => build(options ?? {});
1823
+ }
1824
+ //#endregion
2099
1825
  //#region src/storages/fsStorage.ts
1826
+ const WRITE_CONCURRENCY = 50;
1827
+ function createLimiter(concurrency) {
1828
+ let active = 0;
1829
+ const queue = [];
1830
+ function next() {
1831
+ if (active >= concurrency) return;
1832
+ const run = queue.shift();
1833
+ if (!run) return;
1834
+ active++;
1835
+ run();
1836
+ }
1837
+ return function limit(task) {
1838
+ return new Promise((resolve, reject) => {
1839
+ queue.push(() => {
1840
+ task().then(resolve, reject).finally(() => {
1841
+ active--;
1842
+ next();
1843
+ });
1844
+ });
1845
+ next();
1846
+ });
1847
+ };
1848
+ }
2100
1849
  /**
2101
1850
  * Built-in filesystem storage driver.
2102
1851
  *
@@ -2109,6 +1858,8 @@ function inputToAdapterSource(config) {
2109
1858
  * - the write is skipped when the file content is already identical
2110
1859
  * - missing parent directories are created automatically
2111
1860
  * - Bun's native file API is used when running under Bun
1861
+ * - concurrent `setItem` calls are capped at {@link WRITE_CONCURRENCY} in flight, so a caller
1862
+ * can fire every file's write without pacing itself
2112
1863
  *
2113
1864
  * @example
2114
1865
  * ```ts
@@ -2122,91 +1873,50 @@ function inputToAdapterSource(config) {
2122
1873
  * })
2123
1874
  * ```
2124
1875
  */
2125
- const fsStorage = require_memoryStorage.createStorage(() => ({
2126
- name: "fs",
2127
- async hasItem(key) {
2128
- try {
2129
- await (0, node_fs_promises.access)((0, node_path.resolve)(key));
2130
- return true;
2131
- } catch (_error) {
2132
- return false;
2133
- }
2134
- },
2135
- async getItem(key) {
2136
- try {
2137
- return await (0, node_fs_promises.readFile)((0, node_path.resolve)(key), "utf8");
2138
- } catch (_error) {
2139
- return null;
2140
- }
2141
- },
2142
- async setItem(key, value) {
2143
- await require_memoryStorage.write((0, node_path.resolve)(key), value, { sanity: false });
2144
- },
2145
- async removeItem(key) {
2146
- await (0, node_fs_promises.rm)((0, node_path.resolve)(key), { force: true });
2147
- },
2148
- async getKeys(base) {
2149
- const resolvedBase = (0, node_path.resolve)(base ?? process.cwd());
2150
- if (require_memoryStorage.runtime.isBun) {
2151
- const bunGlob = new Bun.Glob("**/*");
2152
- return Array.fromAsync(bunGlob.scan({
2153
- cwd: resolvedBase,
2154
- onlyFiles: true,
2155
- dot: true
2156
- }));
2157
- }
2158
- const keys = [];
2159
- try {
2160
- for await (const entry of (0, node_fs_promises.glob)("**/*", {
2161
- cwd: resolvedBase,
2162
- withFileTypes: true
2163
- })) if (entry.isFile()) keys.push(require_memoryStorage.toPosixPath((0, node_path.relative)(resolvedBase, (0, node_path.join)(entry.parentPath, entry.name))));
2164
- } catch (_error) {}
2165
- return keys;
2166
- },
2167
- async clear(base) {
2168
- if (!base) return;
2169
- await require_memoryStorage.clean((0, node_path.resolve)(base));
2170
- }
2171
- }));
2172
- //#endregion
2173
- //#region src/createKubb.ts
2174
- /**
2175
- * Builds a `Storage` view scoped to the file paths produced by the current build.
2176
- * Reads delegate to the underlying `storage` so source bytes stay where they were
2177
- * written. Writes register the key so subsequent reads and `getKeys` are scoped
2178
- * to this build's output.
2179
- */
2180
- function createSourcesView(storage) {
2181
- const paths = /* @__PURE__ */ new Set();
2182
- return require_memoryStorage.createStorage(() => ({
2183
- name: `${storage.name}:sources`,
1876
+ const fsStorage = createStorage(() => {
1877
+ const limit = createLimiter(WRITE_CONCURRENCY);
1878
+ return {
1879
+ name: "fs",
2184
1880
  async hasItem(key) {
2185
- return paths.has(key) && await storage.hasItem(key);
1881
+ try {
1882
+ await (0, node_fs_promises.access)((0, node_path.resolve)(key));
1883
+ return true;
1884
+ } catch (_error) {
1885
+ return false;
1886
+ }
2186
1887
  },
2187
1888
  async getItem(key) {
2188
- return paths.has(key) ? storage.getItem(key) : null;
1889
+ try {
1890
+ return await (0, node_fs_promises.readFile)((0, node_path.resolve)(key), "utf8");
1891
+ } catch (_error) {
1892
+ return null;
1893
+ }
2189
1894
  },
2190
1895
  async setItem(key, value) {
2191
- paths.add(key);
2192
- await storage.setItem(key, value);
1896
+ await limit(() => require_usingCtx.write((0, node_path.resolve)(key), value, { sanity: false }));
2193
1897
  },
2194
1898
  async removeItem(key) {
2195
- paths.delete(key);
2196
- await storage.removeItem(key);
1899
+ await (0, node_fs_promises.rm)((0, node_path.resolve)(key), { force: true });
2197
1900
  },
2198
1901
  async getKeys(base) {
2199
- if (!base) return [...paths];
2200
- const result = [];
2201
- for (const key of paths) if (key.startsWith(base)) result.push(key);
2202
- return result;
1902
+ const resolvedBase = (0, node_path.resolve)(base ?? process.cwd());
1903
+ const keys = [];
1904
+ try {
1905
+ for await (const entry of (0, node_fs_promises.glob)("**/*", {
1906
+ cwd: resolvedBase,
1907
+ withFileTypes: true
1908
+ })) if (entry.isFile()) keys.push(require_usingCtx.toPosixPath((0, node_path.relative)(resolvedBase, (0, node_path.join)(entry.parentPath, entry.name))));
1909
+ } catch (_error) {}
1910
+ return keys;
2203
1911
  },
2204
- async clear() {
2205
- paths.clear();
2206
- await storage.clear();
1912
+ async clear(base) {
1913
+ if (!base) return;
1914
+ await require_usingCtx.clean((0, node_path.resolve)(base));
2207
1915
  }
2208
- }))();
2209
- }
1916
+ };
1917
+ });
1918
+ //#endregion
1919
+ //#region src/createKubb.ts
2210
1920
  function resolveConfig(userConfig) {
2211
1921
  return {
2212
1922
  ...userConfig,
@@ -2248,7 +1958,7 @@ var Kubb = class {
2248
1958
  #storage = null;
2249
1959
  constructor(userConfig, options = {}) {
2250
1960
  this.config = resolveConfig(userConfig);
2251
- this.hooks = options.hooks ?? new require_memoryStorage.AsyncEventEmitter();
1961
+ this.hooks = options.hooks ?? new require_usingCtx.AsyncEventEmitter();
2252
1962
  }
2253
1963
  get storage() {
2254
1964
  if (!this.#storage) throw new Error("[kubb] setup() must be called before accessing storage");
@@ -2264,12 +1974,11 @@ var Kubb = class {
2264
1974
  async setup() {
2265
1975
  const config = this.config;
2266
1976
  const driver = new KubbDriver(config, { hooks: this.hooks });
2267
- const storage = createSourcesView(config.storage);
2268
1977
  this.hooks.setMaxListeners(Math.max(10, config.plugins.length * 4));
2269
1978
  if (config.output.clean) await config.storage.clear((0, node_path.resolve)(config.root, config.output.path));
2270
1979
  await driver.setup();
2271
1980
  this.#driver = driver;
2272
- this.#storage = storage;
1981
+ this.#storage = config.storage;
2273
1982
  }
2274
1983
  /**
2275
1984
  * Runs the full pipeline and throws on any plugin error.
@@ -2279,7 +1988,7 @@ var Kubb = class {
2279
1988
  const out = await this.safeBuild();
2280
1989
  if (Diagnostics.hasError(out.diagnostics)) {
2281
1990
  const errors = out.diagnostics.filter(Diagnostics.isProblem).filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.cause ?? new Diagnostics.Error(diagnostic));
2282
- throw new require_memoryStorage.BuildError(`Build failed with ${errors.length} ${errors.length === 1 ? "error" : "errors"}`, { errors });
1991
+ throw new require_usingCtx.BuildError(`Build failed with ${errors.length} ${errors.length === 1 ? "error" : "errors"}`, { errors });
2283
1992
  }
2284
1993
  return out;
2285
1994
  }
@@ -2290,7 +1999,7 @@ var Kubb = class {
2290
1999
  */
2291
2000
  async safeBuild() {
2292
2001
  try {
2293
- var _usingCtx$1 = require_memoryStorage._usingCtx();
2002
+ var _usingCtx$1 = require_usingCtx._usingCtx();
2294
2003
  if (!this.#driver) await this.setup();
2295
2004
  const cleanup = _usingCtx$1.u(this);
2296
2005
  const driver = cleanup.driver;
@@ -2374,22 +2083,19 @@ const logLevel = {
2374
2083
  * ```
2375
2084
  */
2376
2085
  function createReporter(reporter) {
2377
- const drain = reporter.drain;
2378
- if (!drain) return {
2379
- name: reporter.name,
2380
- async report(result, context) {
2381
- await reporter.report(result, context);
2382
- }
2383
- };
2384
- const reports = [];
2086
+ const reports = /* @__PURE__ */ new Set();
2385
2087
  return {
2386
2088
  name: reporter.name,
2387
2089
  async report(result, context) {
2388
- reports.push(await reporter.report(result, context));
2090
+ const report = await reporter.report(result, context);
2091
+ reports.add(report);
2389
2092
  },
2390
2093
  async drain(context) {
2391
- await drain(context, reports);
2392
- reports.length = 0;
2094
+ await reporter.drain?.(context, Array.from(reports));
2095
+ reports.clear();
2096
+ },
2097
+ [Symbol.dispose]() {
2098
+ reports.clear();
2393
2099
  }
2394
2100
  };
2395
2101
  }
@@ -2562,7 +2268,7 @@ const fileReporter = createReporter({
2562
2268
  Date.now()
2563
2269
  ].filter(Boolean).join("-")}.log`;
2564
2270
  const pathName = (0, node_path.resolve)(node_process.default.cwd(), ".kubb", baseName);
2565
- await require_memoryStorage.write(pathName, `${content}\n`);
2271
+ await require_usingCtx.write(pathName, `${content}\n`);
2566
2272
  console.error(`Debug log written to ${(0, node_path.relative)(node_process.default.cwd(), pathName)}`);
2567
2273
  }
2568
2274
  });
@@ -2680,24 +2386,74 @@ function defineParser(parser) {
2680
2386
  return parser;
2681
2387
  }
2682
2388
  //#endregion
2683
- exports.AsyncEventEmitter = require_memoryStorage.AsyncEventEmitter;
2389
+ //#region src/storages/memoryStorage.ts
2390
+ /**
2391
+ * In-memory storage driver. Useful for testing and dry-run scenarios where
2392
+ * generated output should be captured without touching the filesystem.
2393
+ *
2394
+ * All data lives in a `Map` scoped to the storage instance and is discarded
2395
+ * when the instance is garbage-collected.
2396
+ *
2397
+ * @example
2398
+ * ```ts
2399
+ * import { memoryStorage } from '@kubb/core'
2400
+ * import { defineConfig } from 'kubb'
2401
+ *
2402
+ * export default defineConfig({
2403
+ * input: { path: './petStore.yaml' },
2404
+ * output: { path: './src/gen' },
2405
+ * storage: memoryStorage(),
2406
+ * })
2407
+ * ```
2408
+ */
2409
+ const memoryStorage = createStorage(() => {
2410
+ const store = /* @__PURE__ */ new Map();
2411
+ return {
2412
+ name: "memory",
2413
+ async hasItem(key) {
2414
+ return store.has(key);
2415
+ },
2416
+ async getItem(key) {
2417
+ return store.get(key) ?? null;
2418
+ },
2419
+ async setItem(key, value) {
2420
+ store.set(key, value);
2421
+ },
2422
+ async removeItem(key) {
2423
+ store.delete(key);
2424
+ },
2425
+ async getKeys(base) {
2426
+ const keys = [...store.keys()];
2427
+ return base ? keys.filter((k) => k.startsWith(base)) : keys;
2428
+ },
2429
+ async clear(base) {
2430
+ if (!base) {
2431
+ store.clear();
2432
+ return;
2433
+ }
2434
+ for (const key of store.keys()) if (key.startsWith(base)) store.delete(key);
2435
+ }
2436
+ };
2437
+ });
2438
+ //#endregion
2439
+ exports.AsyncEventEmitter = require_usingCtx.AsyncEventEmitter;
2684
2440
  exports.Diagnostics = Diagnostics;
2685
2441
  exports.KubbDriver = KubbDriver;
2686
- exports.Url = Url;
2442
+ exports.Resolver = Resolver;
2687
2443
  exports.cliReporter = cliReporter;
2688
2444
  exports.createAdapter = createAdapter;
2689
2445
  exports.createKubb = createKubb;
2690
2446
  exports.createRenderer = createRenderer;
2691
2447
  exports.createReporter = createReporter;
2692
- exports.createStorage = require_memoryStorage.createStorage;
2448
+ exports.createResolver = createResolver;
2449
+ exports.createStorage = createStorage;
2693
2450
  exports.defineGenerator = defineGenerator;
2694
2451
  exports.defineParser = defineParser;
2695
2452
  exports.definePlugin = definePlugin;
2696
- exports.defineResolver = defineResolver;
2697
2453
  exports.fileReporter = fileReporter;
2698
2454
  exports.fsStorage = fsStorage;
2699
2455
  exports.jsonReporter = jsonReporter;
2700
2456
  exports.logLevel = logLevel;
2701
- exports.memoryStorage = require_memoryStorage.memoryStorage;
2457
+ exports.memoryStorage = memoryStorage;
2702
2458
 
2703
2459
  //# sourceMappingURL=index.cjs.map