@kubb/core 5.0.0-beta.2 → 5.0.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +8 -38
  2. package/dist/{PluginDriver-BXibeQk-.cjs → KubbDriver-BXSnJ3qM.cjs} +719 -164
  3. package/dist/KubbDriver-BXSnJ3qM.cjs.map +1 -0
  4. package/dist/{PluginDriver-DV3p2Hky.js → KubbDriver-Cxii_rBp.js} +693 -162
  5. package/dist/KubbDriver-Cxii_rBp.js.map +1 -0
  6. package/dist/{types-CC09VtBt.d.ts → createKubb-Dcmtjqds.d.ts} +1395 -1238
  7. package/dist/index.cjs +556 -785
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +2 -185
  10. package/dist/index.js +551 -783
  11. package/dist/index.js.map +1 -1
  12. package/dist/mocks.cjs +30 -21
  13. package/dist/mocks.cjs.map +1 -1
  14. package/dist/mocks.d.ts +5 -5
  15. package/dist/mocks.js +29 -20
  16. package/dist/mocks.js.map +1 -1
  17. package/package.json +6 -18
  18. package/src/FileManager.ts +12 -0
  19. package/src/FileProcessor.ts +37 -38
  20. package/src/{PluginDriver.ts → KubbDriver.ts} +249 -86
  21. package/src/constants.ts +11 -6
  22. package/src/createAdapter.ts +84 -1
  23. package/src/createKubb.ts +1336 -297
  24. package/src/createRenderer.ts +23 -22
  25. package/src/defineGenerator.ts +96 -7
  26. package/src/defineLogger.ts +42 -3
  27. package/src/defineMiddleware.ts +1 -1
  28. package/src/defineParser.ts +1 -1
  29. package/src/definePlugin.ts +304 -8
  30. package/src/defineResolver.ts +268 -147
  31. package/src/devtools.ts +8 -1
  32. package/src/index.ts +2 -2
  33. package/src/mocks.ts +11 -14
  34. package/src/storages/fsStorage.ts +13 -37
  35. package/src/types.ts +38 -1292
  36. package/dist/PluginDriver-BXibeQk-.cjs.map +0 -1
  37. package/dist/PluginDriver-DV3p2Hky.js.map +0 -1
  38. package/src/Kubb.ts +0 -300
  39. package/src/renderNode.ts +0 -35
  40. package/src/utils/diagnostics.ts +0 -18
  41. package/src/utils/isInputPath.ts +0 -10
  42. package/src/utils/packageJSON.ts +0 -99
@@ -93,11 +93,386 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
93
93
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
94
94
  }
95
95
  //#endregion
96
+ //#region ../../internals/utils/src/promise.ts
97
+ function* chunks(arr, size) {
98
+ for (let i = 0; i < arr.length; i += size) yield arr.slice(i, i + size);
99
+ }
100
+ /**
101
+ * Slices `source` into batches of `concurrency` items and awaits `process` for each batch.
102
+ * Accepts both plain arrays (sync) and `AsyncIterable` (streaming).
103
+ *
104
+ * `process` controls whether items inside a batch run in parallel; this helper only
105
+ * controls batch size and per-batch flushing.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * // parallel dispatch inside each batch
110
+ * await forBatches(schemas, (batch) => Promise.all(batch.map(process)), { concurrency: 8 })
111
+ *
112
+ * // async iterable with a flush after every batch
113
+ * await forBatches(stream.schemas, (batch) => dispatch(batch), { concurrency: 8, flush })
114
+ * ```
115
+ */
116
+ async function forBatches(source, process, options) {
117
+ const { concurrency, flush } = options;
118
+ if (Array.isArray(source)) {
119
+ for (const batch of chunks(source, concurrency)) {
120
+ await process(batch);
121
+ if (flush) await flush();
122
+ }
123
+ return;
124
+ }
125
+ const batch = [];
126
+ for await (const item of source) {
127
+ batch.push(item);
128
+ if (batch.length >= concurrency) {
129
+ await process(batch.splice(0));
130
+ if (flush) await flush();
131
+ }
132
+ }
133
+ if (batch.length > 0) {
134
+ await process(batch.splice(0));
135
+ if (flush) await flush();
136
+ }
137
+ }
138
+ /**
139
+ * Runs `work`, passing `flush` as its periodic-flush callback, then calls
140
+ * `flush` once more to drain any items that did not cross a flush boundary.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * await withDrain(
145
+ * (flush) => processItems(items, { flush }),
146
+ * () => writeRemainingFiles(),
147
+ * )
148
+ * ```
149
+ */
150
+ async function withDrain(work, flush) {
151
+ await work(flush);
152
+ await flush();
153
+ }
154
+ /** Returns `true` when `result` is a thenable `Promise`.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * isPromise(Promise.resolve(1)) // true
159
+ * isPromise(42) // false
160
+ * ```
161
+ */
162
+ function isPromise(result) {
163
+ return result !== null && result !== void 0 && typeof result["then"] === "function";
164
+ }
165
+ /**
166
+ * Wraps `factory` with a keyed cache backed by the provided store.
167
+ *
168
+ * Pass a `WeakMap` for object keys (results are GC-eligible when the key is
169
+ * collected) or a `Map` for primitive keys. For multi-argument functions,
170
+ * nest two `memoize` calls — the outer keyed by the first argument, the
171
+ * inner (created once per outer miss) keyed by the second.
172
+ *
173
+ * Because the cache is owned by the caller, it can be shared, inspected, or
174
+ * cleared independently of the memoized function.
175
+ *
176
+ * @example Single WeakMap key
177
+ * ```ts
178
+ * const cache = new WeakMap<SchemaNode, Set<string>>()
179
+ * const getRefs = memoize(cache, (node) => collectRefs(node))
180
+ * ```
181
+ *
182
+ * @example Single Map key (primitive)
183
+ * ```ts
184
+ * const cache = new Map<string, Resolver>()
185
+ * const getResolver = memoize(cache, (name) => buildResolver(name))
186
+ * ```
187
+ *
188
+ * @example Two-level (object + primitive)
189
+ * ```ts
190
+ * const outer = new WeakMap<Params[], Map<string, Params[]>>()
191
+ * const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
192
+ * fn(params)('camelcase')
193
+ * ```
194
+ */
195
+ function memoize(store, factory) {
196
+ return (key) => {
197
+ if (store.has(key)) return store.get(key);
198
+ const value = factory(key);
199
+ store.set(key, value);
200
+ return value;
201
+ };
202
+ }
203
+ /**
204
+ * Wraps a plain array in a reusable `AsyncIterable`.
205
+ * Each `[Symbol.asyncIterator]()` call returns a fresh generator so the
206
+ * iterable can be consumed multiple times (e.g. once per plugin pre-scan).
207
+ *
208
+ * @example
209
+ * ```ts
210
+ * const stream = arrayToAsyncIterable([1, 2, 3])
211
+ * for await (const n of stream) console.log(n) // 1, 2, 3
212
+ * ```
213
+ */
214
+ function arrayToAsyncIterable(arr) {
215
+ return { [Symbol.asyncIterator]() {
216
+ return (async function* () {
217
+ yield* arr;
218
+ })();
219
+ } };
220
+ }
221
+ //#endregion
222
+ //#region ../../internals/utils/src/reserved.ts
223
+ /**
224
+ * JavaScript and Java reserved words.
225
+ * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
226
+ */
227
+ const reservedWords = new Set([
228
+ "abstract",
229
+ "arguments",
230
+ "boolean",
231
+ "break",
232
+ "byte",
233
+ "case",
234
+ "catch",
235
+ "char",
236
+ "class",
237
+ "const",
238
+ "continue",
239
+ "debugger",
240
+ "default",
241
+ "delete",
242
+ "do",
243
+ "double",
244
+ "else",
245
+ "enum",
246
+ "eval",
247
+ "export",
248
+ "extends",
249
+ "false",
250
+ "final",
251
+ "finally",
252
+ "float",
253
+ "for",
254
+ "function",
255
+ "goto",
256
+ "if",
257
+ "implements",
258
+ "import",
259
+ "in",
260
+ "instanceof",
261
+ "int",
262
+ "interface",
263
+ "let",
264
+ "long",
265
+ "native",
266
+ "new",
267
+ "null",
268
+ "package",
269
+ "private",
270
+ "protected",
271
+ "public",
272
+ "return",
273
+ "short",
274
+ "static",
275
+ "super",
276
+ "switch",
277
+ "synchronized",
278
+ "this",
279
+ "throw",
280
+ "throws",
281
+ "transient",
282
+ "true",
283
+ "try",
284
+ "typeof",
285
+ "var",
286
+ "void",
287
+ "volatile",
288
+ "while",
289
+ "with",
290
+ "yield",
291
+ "Array",
292
+ "Date",
293
+ "hasOwnProperty",
294
+ "Infinity",
295
+ "isFinite",
296
+ "isNaN",
297
+ "isPrototypeOf",
298
+ "length",
299
+ "Math",
300
+ "name",
301
+ "NaN",
302
+ "Number",
303
+ "Object",
304
+ "prototype",
305
+ "String",
306
+ "toString",
307
+ "undefined",
308
+ "valueOf"
309
+ ]);
310
+ /**
311
+ * Returns `true` when `name` is a syntactically valid JavaScript variable name.
312
+ *
313
+ * @example
314
+ * ```ts
315
+ * isValidVarName('status') // true
316
+ * isValidVarName('class') // false (reserved word)
317
+ * isValidVarName('42foo') // false (starts with digit)
318
+ * ```
319
+ */
320
+ function isValidVarName(name) {
321
+ if (!name || reservedWords.has(name)) return false;
322
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
323
+ }
324
+ //#endregion
325
+ //#region ../../internals/utils/src/urlPath.ts
326
+ /**
327
+ * Parses and transforms an OpenAPI/Swagger path string into various URL formats.
328
+ *
329
+ * @example
330
+ * const p = new URLPath('/pet/{petId}')
331
+ * p.URL // '/pet/:petId'
332
+ * p.template // '`/pet/${petId}`'
333
+ */
334
+ var URLPath = class {
335
+ /**
336
+ * The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
337
+ */
338
+ path;
339
+ #options;
340
+ constructor(path, options = {}) {
341
+ this.path = path;
342
+ this.#options = options;
343
+ }
344
+ /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * new URLPath('/pet/{petId}').URL // '/pet/:petId'
349
+ * ```
350
+ */
351
+ get URL() {
352
+ return this.toURLPath();
353
+ }
354
+ /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
355
+ *
356
+ * @example
357
+ * ```ts
358
+ * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
359
+ * new URLPath('/pet/{petId}').isURL // false
360
+ * ```
361
+ */
362
+ get isURL() {
363
+ try {
364
+ return !!new URL(this.path).href;
365
+ } catch {
366
+ return false;
367
+ }
368
+ }
369
+ /**
370
+ * Converts the OpenAPI path to a TypeScript template literal string.
371
+ *
372
+ * @example
373
+ * new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
374
+ * new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
375
+ */
376
+ get template() {
377
+ return this.toTemplateString();
378
+ }
379
+ /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
380
+ *
381
+ * @example
382
+ * ```ts
383
+ * new URLPath('/pet/{petId}').object
384
+ * // { url: '/pet/:petId', params: { petId: 'petId' } }
385
+ * ```
386
+ */
387
+ get object() {
388
+ return this.toObject();
389
+ }
390
+ /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
391
+ *
392
+ * @example
393
+ * ```ts
394
+ * new URLPath('/pet/{petId}').params // { petId: 'petId' }
395
+ * new URLPath('/pet').params // undefined
396
+ * ```
397
+ */
398
+ get params() {
399
+ return this.getParams();
400
+ }
401
+ #transformParam(raw) {
402
+ const param = isValidVarName(raw) ? raw : camelCase(raw);
403
+ return this.#options.casing === "camelcase" ? camelCase(param) : param;
404
+ }
405
+ /**
406
+ * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
407
+ */
408
+ #eachParam(fn) {
409
+ for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
410
+ const raw = match[1];
411
+ fn(raw, this.#transformParam(raw));
412
+ }
413
+ }
414
+ toObject({ type = "path", replacer, stringify } = {}) {
415
+ const object = {
416
+ url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
417
+ params: this.getParams()
418
+ };
419
+ if (stringify) {
420
+ if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
421
+ if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
422
+ return `{ url: '${object.url}' }`;
423
+ }
424
+ return object;
425
+ }
426
+ /**
427
+ * Converts the OpenAPI path to a TypeScript template literal string.
428
+ * An optional `replacer` can transform each extracted parameter name before interpolation.
429
+ *
430
+ * @example
431
+ * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
432
+ */
433
+ toTemplateString({ prefix = "", replacer } = {}) {
434
+ return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
435
+ if (i % 2 === 0) return part;
436
+ const param = this.#transformParam(part);
437
+ return `\${${replacer ? replacer(param) : param}}`;
438
+ }).join("")}\``;
439
+ }
440
+ /**
441
+ * Extracts all `{param}` segments from the path and returns them as a key-value map.
442
+ * An optional `replacer` transforms each parameter name in both key and value positions.
443
+ * Returns `undefined` when no path parameters are found.
444
+ *
445
+ * @example
446
+ * ```ts
447
+ * new URLPath('/pet/{petId}/tag/{tagId}').getParams()
448
+ * // { petId: 'petId', tagId: 'tagId' }
449
+ * ```
450
+ */
451
+ getParams(replacer) {
452
+ const params = {};
453
+ this.#eachParam((_raw, param) => {
454
+ const key = replacer ? replacer(param) : param;
455
+ params[key] = key;
456
+ });
457
+ return Object.keys(params).length > 0 ? params : void 0;
458
+ }
459
+ /** Converts the OpenAPI path to Express-style colon syntax.
460
+ *
461
+ * @example
462
+ * ```ts
463
+ * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
464
+ * ```
465
+ */
466
+ toURLPath() {
467
+ return this.path.replace(/\{([^}]+)\}/g, ":$1");
468
+ }
469
+ };
470
+ //#endregion
96
471
  //#region src/constants.ts
97
472
  /**
98
473
  * Base URL for the Kubb Studio web app.
99
474
  */
100
- const DEFAULT_STUDIO_URL = "https://studio.kubb.dev";
475
+ const DEFAULT_STUDIO_URL = "https://kubb.studio";
101
476
  /**
102
477
  * Default banner style written at the top of every generated file.
103
478
  */
@@ -120,6 +495,42 @@ const logLevel = {
120
495
  debug: 5
121
496
  };
122
497
  //#endregion
498
+ //#region src/definePlugin.ts
499
+ /**
500
+ * Wraps a factory function and returns a typed `Plugin` with lifecycle handlers grouped under `hooks`.
501
+ *
502
+ * Handlers live in a single `hooks` object (inspired by Astro integrations).
503
+ * All lifecycle events from `KubbHooks` are available for subscription.
504
+ *
505
+ * @note For real plugins, use a `PluginFactoryOptions` type parameter to get type-safe context in `kubb:plugin:setup`.
506
+ * Plugin names should follow the convention `plugin-<feature>` (e.g., `plugin-react-query`, `plugin-zod`).
507
+ *
508
+ * @example
509
+ * ```ts
510
+ * import { definePlugin } from '@kubb/core'
511
+ *
512
+ * export const pluginTs = definePlugin((options: { prefix?: string } = {}) => ({
513
+ * name: 'plugin-ts',
514
+ * hooks: {
515
+ * 'kubb:plugin:setup'(ctx) {
516
+ * ctx.setResolver(resolverTs)
517
+ * },
518
+ * },
519
+ * }))
520
+ * ```
521
+ */
522
+ function definePlugin(factory) {
523
+ return (options) => factory(options ?? {});
524
+ }
525
+ /**
526
+ * Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise.
527
+ * Used to determine whether an output path targets a single file or a directory.
528
+ */
529
+ function getMode(fileOrFolder) {
530
+ if (!fileOrFolder) return "split";
531
+ return (0, node_path.extname)(fileOrFolder) ? "single" : "split";
532
+ }
533
+ //#endregion
123
534
  //#region src/defineResolver.ts
124
535
  const stringPatternCache = /* @__PURE__ */ new Map();
125
536
  function testPattern(value, pattern) {
@@ -137,14 +548,12 @@ function testPattern(value, pattern) {
137
548
  * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).
138
549
  */
139
550
  function matchesOperationPattern(node, type, pattern) {
140
- switch (type) {
141
- case "tag": return node.tags.some((tag) => testPattern(tag, pattern));
142
- case "operationId": return testPattern(node.operationId, pattern);
143
- case "path": return testPattern(node.path, pattern);
144
- case "method": return testPattern(node.method.toLowerCase(), pattern);
145
- case "contentType": return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false;
146
- default: return false;
147
- }
551
+ if (type === "tag") return node.tags.some((tag) => testPattern(tag, pattern));
552
+ if (type === "operationId") return testPattern(node.operationId, pattern);
553
+ if (type === "path") return testPattern(node.path, pattern);
554
+ if (type === "method") return testPattern(node.method.toLowerCase(), pattern);
555
+ if (type === "contentType") return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false;
556
+ return false;
148
557
  }
149
558
  /**
150
559
  * Checks if a schema matches a pattern for a given filter type (`schemaName`).
@@ -152,10 +561,8 @@ function matchesOperationPattern(node, type, pattern) {
152
561
  * Returns `null` when the filter type doesn't apply to schemas.
153
562
  */
154
563
  function matchesSchemaPattern(node, type, pattern) {
155
- switch (type) {
156
- case "schemaName": return node.name ? testPattern(node.name, pattern) : false;
157
- default: return null;
158
- }
564
+ if (type === "schemaName") return node.name ? testPattern(node.name, pattern) : false;
565
+ return null;
159
566
  }
160
567
  /**
161
568
  * Default name resolver used by `defineResolver`.
@@ -165,10 +572,9 @@ function matchesSchemaPattern(node, type, pattern) {
165
572
  * - `camelCase` for everything else.
166
573
  */
167
574
  function defaultResolver(name, type) {
168
- let resolvedName = camelCase(name);
169
- if (type === "file" || type === "function") resolvedName = camelCase(name, { isFile: type === "file" });
170
- if (type === "type") resolvedName = pascalCase(name);
171
- return resolvedName;
575
+ if (type === "file" || type === "function") return camelCase(name, { isFile: type === "file" });
576
+ if (type === "type") return pascalCase(name);
577
+ return camelCase(name);
172
578
  }
173
579
  /**
174
580
  * Default option resolver — applies include/exclude filters and merges matching override options.
@@ -193,7 +599,8 @@ function defaultResolver(name, type) {
193
599
  * // → { enumType: 'enum' } when operationId matches
194
600
  * ```
195
601
  */
196
- function defaultResolveOptions(node, { options, exclude = [], include, override = [] }) {
602
+ const resolveOptionsCache = /* @__PURE__ */ new WeakMap();
603
+ function computeOptions(node, options, exclude, include, override) {
197
604
  if ((0, _kubb_ast.isOperationNode)(node)) {
198
605
  if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
199
606
  if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
@@ -217,6 +624,19 @@ function defaultResolveOptions(node, { options, exclude = [], include, override
217
624
  }
218
625
  return options;
219
626
  }
627
+ function defaultResolveOptions(node, { options, exclude = [], include, override = [] }) {
628
+ const optionsKey = options;
629
+ let byOptions = resolveOptionsCache.get(optionsKey);
630
+ if (!byOptions) {
631
+ byOptions = /* @__PURE__ */ new WeakMap();
632
+ resolveOptionsCache.set(optionsKey, byOptions);
633
+ }
634
+ const cached = byOptions.get(node);
635
+ if (cached !== void 0) return cached.value;
636
+ const result = computeOptions(node, options, exclude, include, override);
637
+ byOptions.set(node, { value: result });
638
+ return result;
639
+ }
220
640
  /**
221
641
  * Default path resolver used by `defineResolver`.
222
642
  *
@@ -262,17 +682,19 @@ function defaultResolveOptions(node, { options, exclude = [], include, override
262
682
  * ```
263
683
  */
264
684
  function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root, output, group }) {
265
- if ((pathMode ?? PluginDriver.getMode(node_path.default.resolve(root, output.path))) === "single") return node_path.default.resolve(root, output.path);
266
- let result;
267
- if (group && (groupPath || tag)) {
268
- const groupValue = group.type === "path" ? groupPath : tag;
269
- const defaultName = group.type === "tag" ? ({ group: g }) => `${camelCase(g)}Controller` : ({ group: g }) => {
270
- const segment = g.split("/").filter((s) => s !== "" && s !== "." && s !== "..")[0];
271
- return segment ? camelCase(segment) : "";
272
- };
273
- const resolveName = group.name ?? defaultName;
274
- result = node_path.default.resolve(root, output.path, resolveName({ group: groupValue }), baseName);
275
- } else result = node_path.default.resolve(root, output.path, baseName);
685
+ if ((pathMode ?? getMode(node_path.default.resolve(root, output.path))) === "single") return node_path.default.resolve(root, output.path);
686
+ const result = (() => {
687
+ if (group && (groupPath || tag)) {
688
+ const groupValue = group.type === "path" ? groupPath : tag;
689
+ const defaultName = group.type === "tag" ? ({ group: g }) => `${camelCase(g)}Controller` : ({ group: g }) => {
690
+ const segment = g.split("/").filter((s) => s !== "" && s !== "." && s !== "..")[0];
691
+ return segment ? camelCase(segment) : "";
692
+ };
693
+ const resolveName = group.name ?? defaultName;
694
+ return node_path.default.resolve(root, output.path, resolveName({ group: groupValue }), baseName);
695
+ }
696
+ return node_path.default.resolve(root, output.path, baseName);
697
+ })();
276
698
  const outputDir = node_path.default.resolve(root, output.path);
277
699
  const outputDirWithSep = outputDir.endsWith(node_path.default.sep) ? outputDir : `${outputDir}${node_path.default.sep}`;
278
700
  if (result !== outputDir && !result.startsWith(outputDirWithSep)) throw new Error(`[Kubb] Resolved path "${result}" is outside the output directory "${outputDir}". This may indicate a path traversal attempt in the OpenAPI specification or a misconfigured group.name function.`);
@@ -289,28 +711,28 @@ function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root
289
711
  *
290
712
  * @example Resolve a schema file
291
713
  * ```ts
292
- * const file = defaultResolveFile(
714
+ * const file = defaultResolveFile.call(
715
+ * resolver,
293
716
  * { name: 'pet', extname: '.ts' },
294
717
  * { root: '/src', output: { path: 'types' } },
295
- * resolver,
296
718
  * )
297
719
  * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
298
720
  * ```
299
721
  *
300
722
  * @example Resolve an operation file with tag grouping
301
723
  * ```ts
302
- * const file = defaultResolveFile(
724
+ * const file = defaultResolveFile.call(
725
+ * resolver,
303
726
  * { name: 'listPets', extname: '.ts', tag: 'pets' },
304
727
  * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
305
- * resolver,
306
728
  * )
307
729
  * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
308
730
  * ```
309
731
  */
310
- function defaultResolveFile({ name, extname, tag, path: groupPath }, context, ctx) {
311
- const pathMode = PluginDriver.getMode(node_path.default.resolve(context.root, context.output.path));
312
- const baseName = `${pathMode === "single" ? "" : ctx.default(name, "file")}${extname}`;
313
- const filePath = ctx.resolvePath({
732
+ function defaultResolveFile({ name, extname, tag, path: groupPath }, context) {
733
+ const pathMode = getMode(node_path.default.resolve(context.root, context.output.path));
734
+ const baseName = `${pathMode === "single" ? "" : this.default(name, "file")}${extname}`;
735
+ const filePath = this.resolvePath({
314
736
  baseName,
315
737
  pathMode,
316
738
  tag,
@@ -319,7 +741,7 @@ function defaultResolveFile({ name, extname, tag, path: groupPath }, context, ct
319
741
  return (0, _kubb_ast.createFile)({
320
742
  path: filePath,
321
743
  baseName: node_path.default.basename(filePath),
322
- meta: { pluginName: ctx.pluginName },
744
+ meta: { pluginName: this.pluginName },
323
745
  sources: [],
324
746
  imports: [],
325
747
  exports: []
@@ -330,12 +752,16 @@ function defaultResolveFile({ name, extname, tag, path: groupPath }, context, ct
330
752
  */
331
753
  function buildDefaultBanner({ title, description, version, config }) {
332
754
  try {
333
- let source = "";
334
- if (Array.isArray(config.input)) {
335
- const first = config.input[0];
336
- if (first && "path" in first) source = node_path.default.basename(first.path);
337
- } else if ("path" in config.input) source = node_path.default.basename(config.input.path);
338
- else if ("data" in config.input) source = "text content";
755
+ const source = (() => {
756
+ if (Array.isArray(config.input)) {
757
+ const first = config.input[0];
758
+ if (first && "path" in first) return node_path.default.basename(first.path);
759
+ return "";
760
+ }
761
+ if (config.input && "path" in config.input) return node_path.default.basename(config.input.path);
762
+ if (config.input && "data" in config.input) return "text content";
763
+ return "";
764
+ })();
339
765
  let banner = "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n";
340
766
  if (config.output.defaultBanner === "simple") {
341
767
  banner += "*/\n";
@@ -359,10 +785,9 @@ function buildDefaultBanner({ title, description, version, config }) {
359
785
  *
360
786
  * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
361
787
  * When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
362
- * from the OAS spec when a `node` is provided).
788
+ * from the document metadata when `meta` is provided).
363
789
  *
364
- * - When `output.banner` is a function and `node` is provided, returns `output.banner(node)`.
365
- * - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice.
790
+ * - When `output.banner` is a function, calls it with `meta` and returns the result.
366
791
  * - When `output.banner` is a string, returns it directly.
367
792
  * - When `config.output.defaultBanner` is `false`, returns `undefined`.
368
793
  * - Otherwise returns the Kubb "Generated by Kubb" notice.
@@ -373,15 +798,15 @@ function buildDefaultBanner({ title, description, version, config }) {
373
798
  * // → '// my banner'
374
799
  * ```
375
800
  *
376
- * @example Function banner with node
801
+ * @example Function banner with metadata
377
802
  * ```ts
378
- * defaultResolveBanner(inputNode, { output: { banner: (node) => `// v${node.version}` }, config })
803
+ * defaultResolveBanner(meta, { output: { banner: (m) => `// v${m?.version}` }, config })
379
804
  * // → '// v3.0.0'
380
805
  * ```
381
806
  *
382
807
  * @example No user banner — Kubb notice with OAS metadata
383
808
  * ```ts
384
- * defaultResolveBanner(inputNode, { config })
809
+ * defaultResolveBanner(meta, { config })
385
810
  * // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
386
811
  * ```
387
812
  *
@@ -391,21 +816,20 @@ function buildDefaultBanner({ title, description, version, config }) {
391
816
  * // → undefined
392
817
  * ```
393
818
  */
394
- function defaultResolveBanner(node, { output, config }) {
395
- if (typeof output?.banner === "function") return output.banner(node);
819
+ function defaultResolveBanner(meta, { output, config }) {
820
+ if (typeof output?.banner === "function") return output.banner(meta);
396
821
  if (typeof output?.banner === "string") return output.banner;
397
822
  if (config.output.defaultBanner === false) return;
398
823
  return buildDefaultBanner({
399
- title: node?.meta?.title,
400
- version: node?.meta?.version,
824
+ title: meta?.title,
825
+ version: meta?.version,
401
826
  config
402
827
  });
403
828
  }
404
829
  /**
405
830
  * Default footer resolver — returns the footer string for a generated file.
406
831
  *
407
- * - When `output.footer` is a function and `node` is provided, calls it with the node.
408
- * - When `output.footer` is a function and `node` is absent, returns `undefined`.
832
+ * - When `output.footer` is a function, calls it with `meta` and returns the result.
409
833
  * - When `output.footer` is a string, returns it directly.
410
834
  * - Otherwise returns `undefined`.
411
835
  *
@@ -415,14 +839,14 @@ function defaultResolveBanner(node, { output, config }) {
415
839
  * // → '// end of file'
416
840
  * ```
417
841
  *
418
- * @example Function footer with node
842
+ * @example Function footer with metadata
419
843
  * ```ts
420
- * defaultResolveFooter(inputNode, { output: { footer: (node) => `// ${node.title}` }, config })
844
+ * defaultResolveFooter(meta, { output: { footer: (m) => `// ${m?.title}` }, config })
421
845
  * // → '// Pet Store'
422
846
  * ```
423
847
  */
424
- function defaultResolveFooter(node, { output }) {
425
- if (typeof output?.footer === "function") return node ? output.footer(node) : void 0;
848
+ function defaultResolveFooter(meta, { output }) {
849
+ if (typeof output?.footer === "function") return output.footer(meta);
426
850
  if (typeof output?.footer === "string") return output.footer;
427
851
  }
428
852
  /**
@@ -435,25 +859,24 @@ function defaultResolveFooter(node, { output }) {
435
859
  * - `resolvePath` — output path computation
436
860
  * - `resolveFile` — full `FileNode` construction
437
861
  *
438
- * The builder receives `ctx` a reference to the assembled resolver so methods can
439
- * call sibling resolver methods using `ctx` instead of `this`.
862
+ * Methods in the returned object can call sibling resolver methods via `this`.
440
863
  *
441
864
  * @example Basic resolver with naming helpers
442
865
  * ```ts
443
- * export const resolver = defineResolver<PluginTs>((ctx) => ({
866
+ * export const resolver = defineResolver<PluginTs>(() => ({
444
867
  * name: 'default',
445
868
  * resolveName(node) {
446
- * return ctx.default(node.name, 'function')
869
+ * return this.default(node.name, 'function')
447
870
  * },
448
871
  * resolveTypedName(node) {
449
- * return ctx.default(node.name, 'type')
872
+ * return this.default(node.name, 'type')
450
873
  * },
451
874
  * }))
452
875
  * ```
453
876
  *
454
877
  * @example Override resolvePath for a custom output structure
455
878
  * ```ts
456
- * export const resolver = defineResolver<PluginTs>((_ctx) => ({
879
+ * export const resolver = defineResolver<PluginTs>(() => ({
457
880
  * name: 'custom',
458
881
  * resolvePath({ baseName }, { root, output }) {
459
882
  * return path.resolve(root, output.path, 'generated', baseName)
@@ -461,27 +884,27 @@ function defaultResolveFooter(node, { output }) {
461
884
  * }))
462
885
  * ```
463
886
  *
464
- * @example Use ctx.default inside a helper
887
+ * @example Use this.default inside a helper
465
888
  * ```ts
466
- * export const resolver = defineResolver<PluginTs>((ctx) => ({
889
+ * export const resolver = defineResolver<PluginTs>(() => ({
467
890
  * name: 'default',
468
891
  * resolveParamName(node, param) {
469
- * return ctx.default(`${node.operationId} ${param.in} ${param.name}`, 'type')
892
+ * return this.default(`${node.operationId} ${param.in} ${param.name}`, 'type')
470
893
  * },
471
894
  * }))
472
895
  * ```
473
896
  */
474
897
  function defineResolver(build) {
475
- const resolver = {};
476
- Object.assign(resolver, {
898
+ let resolver;
899
+ resolver = {
477
900
  default: defaultResolver,
478
901
  resolveOptions: defaultResolveOptions,
479
902
  resolvePath: defaultResolvePath,
480
- resolveFile: (params, context) => defaultResolveFile(params, context, resolver),
903
+ resolveFile: (params, context) => defaultResolveFile.call(resolver, params, context),
481
904
  resolveBanner: defaultResolveBanner,
482
905
  resolveFooter: defaultResolveFooter,
483
- ...build(resolver)
484
- });
906
+ ...build()
907
+ };
485
908
  return resolver;
486
909
  }
487
910
  //#endregion
@@ -609,6 +1032,16 @@ var FileManager = class {
609
1032
  this.#filesCache = null;
610
1033
  }
611
1034
  /**
1035
+ * Releases all stored files. Called by the core after `kubb:build:end` to
1036
+ * free the per-plugin FileNode caches for the rest of the process lifetime.
1037
+ */
1038
+ dispose() {
1039
+ this.clear();
1040
+ }
1041
+ [Symbol.dispose]() {
1042
+ this.dispose();
1043
+ }
1044
+ /**
612
1045
  * All stored files, sorted by path length (shorter paths first).
613
1046
  */
614
1047
  get files() {
@@ -626,35 +1059,11 @@ var FileManager = class {
626
1059
  }
627
1060
  };
628
1061
  //#endregion
629
- //#region src/renderNode.ts
630
- /**
631
- * Handles the return value of a plugin AST hook or generator method.
632
- *
633
- * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`
634
- * - `Array<FileNode>` → added directly into `driver.fileManager`
635
- * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
636
- *
637
- * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result
638
- * may be a renderer element. Generators that only return `Array<FileNode>` do not need one.
639
- */
640
- async function applyHookResult(result, driver, rendererFactory) {
641
- if (!result) return;
642
- if (Array.isArray(result)) {
643
- driver.fileManager.upsert(...result);
644
- return;
645
- }
646
- if (!rendererFactory) return;
647
- const renderer = rendererFactory();
648
- await renderer.render(result);
649
- driver.fileManager.upsert(...renderer.files);
650
- renderer.unmount();
651
- }
652
- //#endregion
653
- //#region src/PluginDriver.ts
1062
+ //#region src/KubbDriver.ts
654
1063
  function enforceOrder(enforce) {
655
1064
  return enforce === "pre" ? -1 : enforce === "post" ? 1 : 0;
656
1065
  }
657
- var PluginDriver = class PluginDriver {
1066
+ var KubbDriver = class KubbDriver {
658
1067
  config;
659
1068
  options;
660
1069
  /**
@@ -662,21 +1071,34 @@ var PluginDriver = class PluginDriver {
662
1071
  *
663
1072
  * @example
664
1073
  * ```ts
665
- * PluginDriver.getMode('src/gen/types.ts') // 'single'
666
- * PluginDriver.getMode('src/gen/types') // 'split'
1074
+ * KubbDriver.getMode('src/gen/types.ts') // 'single'
1075
+ * KubbDriver.getMode('src/gen/types') // 'split'
667
1076
  * ```
668
1077
  */
669
1078
  static getMode(fileOrFolder) {
670
- if (!fileOrFolder) return "split";
671
- return (0, node_path.extname)(fileOrFolder) ? "single" : "split";
1079
+ return getMode(fileOrFolder);
672
1080
  }
673
1081
  /**
674
- * The universal `@kubb/ast` `InputNode` produced by the adapter, set by
675
- * the build pipeline after the adapter's `parse()` resolves.
1082
+ * The streaming `InputStreamNode` produced by the adapter.
1083
+ * Always set after adapter setup — parse-only adapters are wrapped automatically.
676
1084
  */
677
1085
  inputNode = void 0;
678
1086
  adapter = void 0;
679
- #studioIsOpen = false;
1087
+ /**
1088
+ * Studio session state, kept together so `dispose()` can reset it atomically.
1089
+ *
1090
+ * - `source` holds the raw adapter source so `adapter.parse()` can be called lazily.
1091
+ * Intentionally outlives the build; cleared by `dispose()`.
1092
+ * - `isOpen` prevents opening the studio more than once per build.
1093
+ * - `inputNode` caches the parse promise so `adapter.parse()` is called at most once
1094
+ * per studio session, even when `openInStudio()` is called multiple times.
1095
+ */
1096
+ #studio = {
1097
+ source: void 0,
1098
+ isOpen: false,
1099
+ inputNode: void 0
1100
+ };
1101
+ #middlewareListeners = [];
680
1102
  /**
681
1103
  * Central file store for all generated files.
682
1104
  * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
@@ -688,23 +1110,29 @@ var PluginDriver = class PluginDriver {
688
1110
  * Tracks which plugins have generators registered via `addGenerator()` (event-based path).
689
1111
  * Used by the build loop to decide whether to emit generator events for a given plugin.
690
1112
  */
691
- #pluginsWithEventGenerators = /* @__PURE__ */ new Set();
1113
+ #eventGeneratorPlugins = /* @__PURE__ */ new Set();
692
1114
  #resolvers = /* @__PURE__ */ new Map();
693
1115
  #defaultResolvers = /* @__PURE__ */ new Map();
694
1116
  #hookListeners = /* @__PURE__ */ new Map();
695
1117
  constructor(config, options) {
696
1118
  this.config = config;
697
1119
  this.options = options;
698
- config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin)).filter((plugin) => {
699
- if (typeof plugin.apply === "function") return plugin.apply(config);
700
- return true;
701
- }).sort((a, b) => {
1120
+ this.adapter = config.adapter;
1121
+ }
1122
+ async setup() {
1123
+ const normalized = this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin));
1124
+ normalized.sort((a, b) => {
702
1125
  if (b.dependencies?.includes(a.name)) return -1;
703
1126
  if (a.dependencies?.includes(b.name)) return 1;
704
1127
  return enforceOrder(a.enforce) - enforceOrder(b.enforce);
705
- }).forEach((plugin) => {
706
- this.plugins.set(plugin.name, plugin);
707
1128
  });
1129
+ for (const plugin of normalized) {
1130
+ if (plugin.apply) plugin.apply(this.config);
1131
+ this.#registerPlugin(plugin);
1132
+ this.plugins.set(plugin.name, plugin);
1133
+ }
1134
+ if (this.config.middleware) for (const middleware of this.config.middleware) for (const event of Object.keys(middleware.hooks)) this.#registerMiddleware(event, middleware.hooks);
1135
+ if (this.config.adapter) await this.#registerAdapter(this.config.adapter);
708
1136
  }
709
1137
  get hooks() {
710
1138
  return this.options.hooks;
@@ -713,19 +1141,48 @@ var PluginDriver = class PluginDriver {
713
1141
  * Creates an `NormalizedPlugin` from a hook-style plugin and registers
714
1142
  * its lifecycle handlers on the `AsyncEventEmitter`.
715
1143
  */
716
- #normalizePlugin(hookPlugin) {
717
- const normalizedPlugin = {
718
- name: hookPlugin.name,
719
- dependencies: hookPlugin.dependencies,
720
- enforce: hookPlugin.enforce,
721
- options: {
1144
+ #normalizePlugin(plugin) {
1145
+ const normalized = {
1146
+ name: plugin.name,
1147
+ dependencies: plugin.dependencies,
1148
+ enforce: plugin.enforce,
1149
+ hooks: plugin.hooks,
1150
+ options: plugin.options ?? {
722
1151
  output: { path: "." },
723
1152
  exclude: [],
724
1153
  override: []
725
1154
  }
726
1155
  };
727
- this.registerPluginHooks(hookPlugin, normalizedPlugin);
728
- return normalizedPlugin;
1156
+ if ("apply" in plugin && typeof plugin.apply === "function") normalized.apply = plugin.apply;
1157
+ return normalized;
1158
+ }
1159
+ async #registerAdapter(adapter) {
1160
+ const source = inputToAdapterSource(this.config);
1161
+ this.#studio.source = source;
1162
+ if (adapter.stream) {
1163
+ this.inputNode = await adapter.stream(source);
1164
+ await this.hooks.emit("kubb:debug", {
1165
+ date: /* @__PURE__ */ new Date(),
1166
+ logs: [`✓ Adapter '${adapter.name}' producing input stream`]
1167
+ });
1168
+ } else {
1169
+ const inputNode = await adapter.parse(source);
1170
+ this.inputNode = (0, _kubb_ast.createStreamInput)(arrayToAsyncIterable(inputNode.schemas), arrayToAsyncIterable(inputNode.operations), inputNode.meta);
1171
+ await this.hooks.emit("kubb:debug", {
1172
+ date: /* @__PURE__ */ new Date(),
1173
+ logs: [
1174
+ `✓ Adapter '${adapter.name}' resolved InputNode (wrapped as stream)`,
1175
+ ` • Schemas: ${inputNode.schemas.length}`,
1176
+ ` • Operations: ${inputNode.operations.length}`
1177
+ ]
1178
+ });
1179
+ }
1180
+ }
1181
+ #registerMiddleware(event, middlewareHooks) {
1182
+ const handler = middlewareHooks[event];
1183
+ if (!handler) return;
1184
+ this.hooks.on(event, handler);
1185
+ this.#middlewareListeners.push([event, handler]);
729
1186
  }
730
1187
  /**
731
1188
  * Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.
@@ -742,28 +1199,29 @@ var PluginDriver = class PluginDriver {
742
1199
  *
743
1200
  * @internal
744
1201
  */
745
- registerPluginHooks(hookPlugin, normalizedPlugin) {
746
- const { hooks } = hookPlugin;
1202
+ #registerPlugin(plugin) {
1203
+ const { hooks } = plugin;
1204
+ if (!hooks) return;
747
1205
  if (hooks["kubb:plugin:setup"]) {
748
1206
  const setupHandler = (globalCtx) => {
749
1207
  const pluginCtx = {
750
1208
  ...globalCtx,
751
- options: hookPlugin.options ?? {},
1209
+ options: plugin.options ?? {},
752
1210
  addGenerator: (gen) => {
753
- this.registerGenerator(normalizedPlugin.name, gen);
1211
+ this.registerGenerator(plugin.name, gen);
754
1212
  },
755
1213
  setResolver: (resolver) => {
756
- this.setPluginResolver(normalizedPlugin.name, resolver);
1214
+ this.setPluginResolver(plugin.name, resolver);
757
1215
  },
758
1216
  setTransformer: (visitor) => {
759
- normalizedPlugin.transformer = visitor;
1217
+ plugin.transformer = visitor;
760
1218
  },
761
1219
  setRenderer: (renderer) => {
762
- normalizedPlugin.renderer = renderer;
1220
+ plugin.renderer = renderer;
763
1221
  },
764
1222
  setOptions: (opts) => {
765
- normalizedPlugin.options = {
766
- ...normalizedPlugin.options,
1223
+ plugin.options = {
1224
+ ...plugin.options,
767
1225
  ...opts
768
1226
  };
769
1227
  },
@@ -824,7 +1282,11 @@ var PluginDriver = class PluginDriver {
824
1282
  if (gen.schema) {
825
1283
  const schemaHandler = async (node, ctx) => {
826
1284
  if (ctx.plugin.name !== pluginName) return;
827
- await applyHookResult(await gen.schema(node, ctx), this, resolveRenderer());
1285
+ await applyHookResult({
1286
+ result: await gen.schema(node, ctx),
1287
+ driver: this,
1288
+ rendererFactory: resolveRenderer()
1289
+ });
828
1290
  };
829
1291
  this.hooks.on("kubb:generate:schema", schemaHandler);
830
1292
  this.#trackHookListener("kubb:generate:schema", schemaHandler);
@@ -832,7 +1294,11 @@ var PluginDriver = class PluginDriver {
832
1294
  if (gen.operation) {
833
1295
  const operationHandler = async (node, ctx) => {
834
1296
  if (ctx.plugin.name !== pluginName) return;
835
- await applyHookResult(await gen.operation(node, ctx), this, resolveRenderer());
1297
+ await applyHookResult({
1298
+ result: await gen.operation(node, ctx),
1299
+ driver: this,
1300
+ rendererFactory: resolveRenderer()
1301
+ });
836
1302
  };
837
1303
  this.hooks.on("kubb:generate:operation", operationHandler);
838
1304
  this.#trackHookListener("kubb:generate:operation", operationHandler);
@@ -840,12 +1306,16 @@ var PluginDriver = class PluginDriver {
840
1306
  if (gen.operations) {
841
1307
  const operationsHandler = async (nodes, ctx) => {
842
1308
  if (ctx.plugin.name !== pluginName) return;
843
- await applyHookResult(await gen.operations(nodes, ctx), this, resolveRenderer());
1309
+ await applyHookResult({
1310
+ result: await gen.operations(nodes, ctx),
1311
+ driver: this,
1312
+ rendererFactory: resolveRenderer()
1313
+ });
844
1314
  };
845
1315
  this.hooks.on("kubb:generate:operations", operationsHandler);
846
1316
  this.#trackHookListener("kubb:generate:operations", operationsHandler);
847
1317
  }
848
- this.#pluginsWithEventGenerators.add(pluginName);
1318
+ this.#eventGeneratorPlugins.add(pluginName);
849
1319
  }
850
1320
  /**
851
1321
  * Returns `true` when at least one generator was registered for the given plugin
@@ -854,8 +1324,8 @@ var PluginDriver = class PluginDriver {
854
1324
  * Used by the build loop to decide whether to walk the AST and emit generator events
855
1325
  * for a plugin that has no static `plugin.generators`.
856
1326
  */
857
- hasRegisteredGenerators(pluginName) {
858
- return this.#pluginsWithEventGenerators.has(pluginName);
1327
+ hasEventGenerators(pluginName) {
1328
+ return this.#eventGeneratorPlugins.has(pluginName);
859
1329
  }
860
1330
  /**
861
1331
  * Unregisters all plugin lifecycle listeners from the shared event emitter.
@@ -866,7 +1336,20 @@ var PluginDriver = class PluginDriver {
866
1336
  dispose() {
867
1337
  for (const [event, handlers] of this.#hookListeners) for (const handler of handlers) this.hooks.off(event, handler);
868
1338
  this.#hookListeners.clear();
869
- this.#pluginsWithEventGenerators.clear();
1339
+ this.#eventGeneratorPlugins.clear();
1340
+ this.#resolvers.clear();
1341
+ this.#defaultResolvers.clear();
1342
+ this.fileManager.dispose();
1343
+ this.inputNode = void 0;
1344
+ this.#studio = {
1345
+ source: void 0,
1346
+ isOpen: false,
1347
+ inputNode: void 0
1348
+ };
1349
+ for (const [event, handler] of this.#middlewareListeners) this.hooks.off(event, handler);
1350
+ }
1351
+ [Symbol.dispose]() {
1352
+ this.dispose();
870
1353
  }
871
1354
  #trackHookListener(event, handler) {
872
1355
  let handlers = this.#hookListeners.get(event);
@@ -876,16 +1359,10 @@ var PluginDriver = class PluginDriver {
876
1359
  }
877
1360
  handlers.add(handler);
878
1361
  }
879
- #createDefaultResolver(pluginName) {
880
- const existingResolver = this.#defaultResolvers.get(pluginName);
881
- if (existingResolver) return existingResolver;
882
- const resolver = defineResolver((_ctx) => ({
883
- name: "default",
884
- pluginName
885
- }));
886
- this.#defaultResolvers.set(pluginName, resolver);
887
- return resolver;
888
- }
1362
+ #getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => defineResolver(() => ({
1363
+ name: "default",
1364
+ pluginName
1365
+ })));
889
1366
  /**
890
1367
  * Merges `partial` with the plugin's default resolver and stores the result.
891
1368
  * Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver`
@@ -893,7 +1370,7 @@ var PluginDriver = class PluginDriver {
893
1370
  */
894
1371
  setPluginResolver(pluginName, partial) {
895
1372
  const merged = {
896
- ...this.#createDefaultResolver(pluginName),
1373
+ ...this.#getDefaultResolver(pluginName),
897
1374
  ...partial
898
1375
  };
899
1376
  this.#resolvers.set(pluginName, merged);
@@ -901,7 +1378,7 @@ var PluginDriver = class PluginDriver {
901
1378
  if (plugin) plugin.resolver = merged;
902
1379
  }
903
1380
  getResolver(pluginName) {
904
- return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#createDefaultResolver(pluginName);
1381
+ return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#getDefaultResolver(pluginName);
905
1382
  }
906
1383
  getContext(plugin) {
907
1384
  const driver = this;
@@ -911,7 +1388,7 @@ var PluginDriver = class PluginDriver {
911
1388
  return (0, node_path.resolve)(driver.config.root, driver.config.output.path);
912
1389
  },
913
1390
  getMode(output) {
914
- return PluginDriver.getMode((0, node_path.resolve)(driver.config.root, driver.config.output.path, output.path));
1391
+ return KubbDriver.getMode((0, node_path.resolve)(driver.config.root, driver.config.output.path, output.path));
915
1392
  },
916
1393
  hooks: driver.hooks,
917
1394
  plugin,
@@ -925,8 +1402,11 @@ var PluginDriver = class PluginDriver {
925
1402
  upsertFile: async (...files) => {
926
1403
  driver.fileManager.upsert(...files);
927
1404
  },
928
- get inputNode() {
929
- return driver.inputNode;
1405
+ get meta() {
1406
+ return driver.inputNode?.meta ?? {
1407
+ circularNames: [],
1408
+ enumNames: []
1409
+ };
930
1410
  },
931
1411
  get adapter() {
932
1412
  return driver.adapter;
@@ -946,13 +1426,14 @@ var PluginDriver = class PluginDriver {
946
1426
  info(message) {
947
1427
  driver.hooks.emit("kubb:info", { message });
948
1428
  },
949
- openInStudio(options) {
950
- if (!driver.config.devtools || driver.#studioIsOpen) return;
1429
+ async openInStudio(options) {
1430
+ if (!driver.config.devtools || driver.#studio.isOpen) return;
951
1431
  if (typeof driver.config.devtools !== "object") throw new Error("Devtools must be an object");
952
- if (!driver.inputNode || !driver.adapter) throw new Error("adapter is not defined, make sure you have set the parser in kubb.config.ts");
953
- driver.#studioIsOpen = true;
954
- const studioUrl = driver.config.devtools?.studioUrl ?? "https://studio.kubb.dev";
955
- return openInStudio(driver.inputNode, studioUrl, options);
1432
+ if (!driver.adapter || !driver.#studio.source) throw new Error("adapter is not defined, make sure you have set the parser in kubb.config.ts");
1433
+ driver.#studio.isOpen = true;
1434
+ const studioUrl = driver.config.devtools?.studioUrl ?? "https://kubb.studio";
1435
+ driver.#studio.inputNode ??= Promise.resolve(driver.adapter.parse(driver.#studio.source));
1436
+ return openInStudio(await driver.#studio.inputNode, studioUrl, options);
956
1437
  }
957
1438
  };
958
1439
  }
@@ -965,6 +1446,56 @@ var PluginDriver = class PluginDriver {
965
1446
  return plugin;
966
1447
  }
967
1448
  };
1449
+ /**
1450
+ * Handles the return value of a plugin AST hook or generator method.
1451
+ *
1452
+ * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`
1453
+ * - `Array<FileNode>` → added directly into `driver.fileManager`
1454
+ * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
1455
+ *
1456
+ * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result
1457
+ * may be a renderer element. Generators that only return `Array<FileNode>` do not need one.
1458
+ */
1459
+ function applyHookResult({ result, driver, rendererFactory }) {
1460
+ if (!result) return;
1461
+ if (Array.isArray(result)) {
1462
+ driver.fileManager.upsert(...result);
1463
+ return;
1464
+ }
1465
+ if (!rendererFactory) return;
1466
+ const renderer = rendererFactory();
1467
+ if (renderer.stream) {
1468
+ for (const file of renderer.stream(result)) driver.fileManager.upsert(file);
1469
+ renderer.unmount();
1470
+ return;
1471
+ }
1472
+ return applyAsyncRender({
1473
+ renderer,
1474
+ result,
1475
+ driver
1476
+ });
1477
+ }
1478
+ async function applyAsyncRender({ renderer, result, driver }) {
1479
+ await renderer.render(result);
1480
+ driver.fileManager.upsert(...renderer.files);
1481
+ renderer.unmount();
1482
+ }
1483
+ function inputToAdapterSource(config) {
1484
+ const input = config.input;
1485
+ if (!input) throw new Error("[kubb] input is required when using an adapter. Provide input.path or input.data in your config.");
1486
+ if ("data" in input) return {
1487
+ type: "data",
1488
+ data: input.data
1489
+ };
1490
+ if (new URLPath(input.path).isURL) return {
1491
+ type: "path",
1492
+ path: input.path
1493
+ };
1494
+ return {
1495
+ type: "path",
1496
+ path: (0, node_path.resolve)(config.root, input.path)
1497
+ };
1498
+ }
968
1499
  //#endregion
969
1500
  Object.defineProperty(exports, "DEFAULT_BANNER", {
970
1501
  enumerable: true,
@@ -990,10 +1521,16 @@ Object.defineProperty(exports, "FileManager", {
990
1521
  return FileManager;
991
1522
  }
992
1523
  });
993
- Object.defineProperty(exports, "PluginDriver", {
1524
+ Object.defineProperty(exports, "KubbDriver", {
994
1525
  enumerable: true,
995
1526
  get: function() {
996
- return PluginDriver;
1527
+ return KubbDriver;
1528
+ }
1529
+ });
1530
+ Object.defineProperty(exports, "URLPath", {
1531
+ enumerable: true,
1532
+ get: function() {
1533
+ return URLPath;
997
1534
  }
998
1535
  });
999
1536
  Object.defineProperty(exports, "__name", {
@@ -1014,10 +1551,10 @@ Object.defineProperty(exports, "applyHookResult", {
1014
1551
  return applyHookResult;
1015
1552
  }
1016
1553
  });
1017
- Object.defineProperty(exports, "camelCase", {
1554
+ Object.defineProperty(exports, "definePlugin", {
1018
1555
  enumerable: true,
1019
1556
  get: function() {
1020
- return camelCase;
1557
+ return definePlugin;
1021
1558
  }
1022
1559
  });
1023
1560
  Object.defineProperty(exports, "defineResolver", {
@@ -1026,11 +1563,29 @@ Object.defineProperty(exports, "defineResolver", {
1026
1563
  return defineResolver;
1027
1564
  }
1028
1565
  });
1566
+ Object.defineProperty(exports, "forBatches", {
1567
+ enumerable: true,
1568
+ get: function() {
1569
+ return forBatches;
1570
+ }
1571
+ });
1572
+ Object.defineProperty(exports, "isPromise", {
1573
+ enumerable: true,
1574
+ get: function() {
1575
+ return isPromise;
1576
+ }
1577
+ });
1029
1578
  Object.defineProperty(exports, "logLevel", {
1030
1579
  enumerable: true,
1031
1580
  get: function() {
1032
1581
  return logLevel;
1033
1582
  }
1034
1583
  });
1584
+ Object.defineProperty(exports, "withDrain", {
1585
+ enumerable: true,
1586
+ get: function() {
1587
+ return withDrain;
1588
+ }
1589
+ });
1035
1590
 
1036
- //# sourceMappingURL=PluginDriver-BXibeQk-.cjs.map
1591
+ //# sourceMappingURL=KubbDriver-BXSnJ3qM.cjs.map