@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
@@ -1,6 +1,6 @@
1
1
  import "./chunk--u3MIqq1.js";
2
2
  import path, { extname, resolve } from "node:path";
3
- import { createFile, isOperationNode, isSchemaNode } from "@kubb/ast";
3
+ import { createFile, createStreamInput, isOperationNode, isSchemaNode } from "@kubb/ast";
4
4
  import { deflateSync } from "fflate";
5
5
  import { x } from "tinyexec";
6
6
  //#region ../../internals/utils/src/casing.ts
@@ -67,11 +67,386 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
67
67
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
68
68
  }
69
69
  //#endregion
70
+ //#region ../../internals/utils/src/promise.ts
71
+ function* chunks(arr, size) {
72
+ for (let i = 0; i < arr.length; i += size) yield arr.slice(i, i + size);
73
+ }
74
+ /**
75
+ * Slices `source` into batches of `concurrency` items and awaits `process` for each batch.
76
+ * Accepts both plain arrays (sync) and `AsyncIterable` (streaming).
77
+ *
78
+ * `process` controls whether items inside a batch run in parallel; this helper only
79
+ * controls batch size and per-batch flushing.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * // parallel dispatch inside each batch
84
+ * await forBatches(schemas, (batch) => Promise.all(batch.map(process)), { concurrency: 8 })
85
+ *
86
+ * // async iterable with a flush after every batch
87
+ * await forBatches(stream.schemas, (batch) => dispatch(batch), { concurrency: 8, flush })
88
+ * ```
89
+ */
90
+ async function forBatches(source, process, options) {
91
+ const { concurrency, flush } = options;
92
+ if (Array.isArray(source)) {
93
+ for (const batch of chunks(source, concurrency)) {
94
+ await process(batch);
95
+ if (flush) await flush();
96
+ }
97
+ return;
98
+ }
99
+ const batch = [];
100
+ for await (const item of source) {
101
+ batch.push(item);
102
+ if (batch.length >= concurrency) {
103
+ await process(batch.splice(0));
104
+ if (flush) await flush();
105
+ }
106
+ }
107
+ if (batch.length > 0) {
108
+ await process(batch.splice(0));
109
+ if (flush) await flush();
110
+ }
111
+ }
112
+ /**
113
+ * Runs `work`, passing `flush` as its periodic-flush callback, then calls
114
+ * `flush` once more to drain any items that did not cross a flush boundary.
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * await withDrain(
119
+ * (flush) => processItems(items, { flush }),
120
+ * () => writeRemainingFiles(),
121
+ * )
122
+ * ```
123
+ */
124
+ async function withDrain(work, flush) {
125
+ await work(flush);
126
+ await flush();
127
+ }
128
+ /** Returns `true` when `result` is a thenable `Promise`.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * isPromise(Promise.resolve(1)) // true
133
+ * isPromise(42) // false
134
+ * ```
135
+ */
136
+ function isPromise(result) {
137
+ return result !== null && result !== void 0 && typeof result["then"] === "function";
138
+ }
139
+ /**
140
+ * Wraps `factory` with a keyed cache backed by the provided store.
141
+ *
142
+ * Pass a `WeakMap` for object keys (results are GC-eligible when the key is
143
+ * collected) or a `Map` for primitive keys. For multi-argument functions,
144
+ * nest two `memoize` calls — the outer keyed by the first argument, the
145
+ * inner (created once per outer miss) keyed by the second.
146
+ *
147
+ * Because the cache is owned by the caller, it can be shared, inspected, or
148
+ * cleared independently of the memoized function.
149
+ *
150
+ * @example Single WeakMap key
151
+ * ```ts
152
+ * const cache = new WeakMap<SchemaNode, Set<string>>()
153
+ * const getRefs = memoize(cache, (node) => collectRefs(node))
154
+ * ```
155
+ *
156
+ * @example Single Map key (primitive)
157
+ * ```ts
158
+ * const cache = new Map<string, Resolver>()
159
+ * const getResolver = memoize(cache, (name) => buildResolver(name))
160
+ * ```
161
+ *
162
+ * @example Two-level (object + primitive)
163
+ * ```ts
164
+ * const outer = new WeakMap<Params[], Map<string, Params[]>>()
165
+ * const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
166
+ * fn(params)('camelcase')
167
+ * ```
168
+ */
169
+ function memoize(store, factory) {
170
+ return (key) => {
171
+ if (store.has(key)) return store.get(key);
172
+ const value = factory(key);
173
+ store.set(key, value);
174
+ return value;
175
+ };
176
+ }
177
+ /**
178
+ * Wraps a plain array in a reusable `AsyncIterable`.
179
+ * Each `[Symbol.asyncIterator]()` call returns a fresh generator so the
180
+ * iterable can be consumed multiple times (e.g. once per plugin pre-scan).
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * const stream = arrayToAsyncIterable([1, 2, 3])
185
+ * for await (const n of stream) console.log(n) // 1, 2, 3
186
+ * ```
187
+ */
188
+ function arrayToAsyncIterable(arr) {
189
+ return { [Symbol.asyncIterator]() {
190
+ return (async function* () {
191
+ yield* arr;
192
+ })();
193
+ } };
194
+ }
195
+ //#endregion
196
+ //#region ../../internals/utils/src/reserved.ts
197
+ /**
198
+ * JavaScript and Java reserved words.
199
+ * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
200
+ */
201
+ const reservedWords = new Set([
202
+ "abstract",
203
+ "arguments",
204
+ "boolean",
205
+ "break",
206
+ "byte",
207
+ "case",
208
+ "catch",
209
+ "char",
210
+ "class",
211
+ "const",
212
+ "continue",
213
+ "debugger",
214
+ "default",
215
+ "delete",
216
+ "do",
217
+ "double",
218
+ "else",
219
+ "enum",
220
+ "eval",
221
+ "export",
222
+ "extends",
223
+ "false",
224
+ "final",
225
+ "finally",
226
+ "float",
227
+ "for",
228
+ "function",
229
+ "goto",
230
+ "if",
231
+ "implements",
232
+ "import",
233
+ "in",
234
+ "instanceof",
235
+ "int",
236
+ "interface",
237
+ "let",
238
+ "long",
239
+ "native",
240
+ "new",
241
+ "null",
242
+ "package",
243
+ "private",
244
+ "protected",
245
+ "public",
246
+ "return",
247
+ "short",
248
+ "static",
249
+ "super",
250
+ "switch",
251
+ "synchronized",
252
+ "this",
253
+ "throw",
254
+ "throws",
255
+ "transient",
256
+ "true",
257
+ "try",
258
+ "typeof",
259
+ "var",
260
+ "void",
261
+ "volatile",
262
+ "while",
263
+ "with",
264
+ "yield",
265
+ "Array",
266
+ "Date",
267
+ "hasOwnProperty",
268
+ "Infinity",
269
+ "isFinite",
270
+ "isNaN",
271
+ "isPrototypeOf",
272
+ "length",
273
+ "Math",
274
+ "name",
275
+ "NaN",
276
+ "Number",
277
+ "Object",
278
+ "prototype",
279
+ "String",
280
+ "toString",
281
+ "undefined",
282
+ "valueOf"
283
+ ]);
284
+ /**
285
+ * Returns `true` when `name` is a syntactically valid JavaScript variable name.
286
+ *
287
+ * @example
288
+ * ```ts
289
+ * isValidVarName('status') // true
290
+ * isValidVarName('class') // false (reserved word)
291
+ * isValidVarName('42foo') // false (starts with digit)
292
+ * ```
293
+ */
294
+ function isValidVarName(name) {
295
+ if (!name || reservedWords.has(name)) return false;
296
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
297
+ }
298
+ //#endregion
299
+ //#region ../../internals/utils/src/urlPath.ts
300
+ /**
301
+ * Parses and transforms an OpenAPI/Swagger path string into various URL formats.
302
+ *
303
+ * @example
304
+ * const p = new URLPath('/pet/{petId}')
305
+ * p.URL // '/pet/:petId'
306
+ * p.template // '`/pet/${petId}`'
307
+ */
308
+ var URLPath = class {
309
+ /**
310
+ * The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
311
+ */
312
+ path;
313
+ #options;
314
+ constructor(path, options = {}) {
315
+ this.path = path;
316
+ this.#options = options;
317
+ }
318
+ /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
319
+ *
320
+ * @example
321
+ * ```ts
322
+ * new URLPath('/pet/{petId}').URL // '/pet/:petId'
323
+ * ```
324
+ */
325
+ get URL() {
326
+ return this.toURLPath();
327
+ }
328
+ /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
329
+ *
330
+ * @example
331
+ * ```ts
332
+ * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
333
+ * new URLPath('/pet/{petId}').isURL // false
334
+ * ```
335
+ */
336
+ get isURL() {
337
+ try {
338
+ return !!new URL(this.path).href;
339
+ } catch {
340
+ return false;
341
+ }
342
+ }
343
+ /**
344
+ * Converts the OpenAPI path to a TypeScript template literal string.
345
+ *
346
+ * @example
347
+ * new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
348
+ * new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
349
+ */
350
+ get template() {
351
+ return this.toTemplateString();
352
+ }
353
+ /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
354
+ *
355
+ * @example
356
+ * ```ts
357
+ * new URLPath('/pet/{petId}').object
358
+ * // { url: '/pet/:petId', params: { petId: 'petId' } }
359
+ * ```
360
+ */
361
+ get object() {
362
+ return this.toObject();
363
+ }
364
+ /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
365
+ *
366
+ * @example
367
+ * ```ts
368
+ * new URLPath('/pet/{petId}').params // { petId: 'petId' }
369
+ * new URLPath('/pet').params // undefined
370
+ * ```
371
+ */
372
+ get params() {
373
+ return this.getParams();
374
+ }
375
+ #transformParam(raw) {
376
+ const param = isValidVarName(raw) ? raw : camelCase(raw);
377
+ return this.#options.casing === "camelcase" ? camelCase(param) : param;
378
+ }
379
+ /**
380
+ * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
381
+ */
382
+ #eachParam(fn) {
383
+ for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
384
+ const raw = match[1];
385
+ fn(raw, this.#transformParam(raw));
386
+ }
387
+ }
388
+ toObject({ type = "path", replacer, stringify } = {}) {
389
+ const object = {
390
+ url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
391
+ params: this.getParams()
392
+ };
393
+ if (stringify) {
394
+ if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
395
+ if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
396
+ return `{ url: '${object.url}' }`;
397
+ }
398
+ return object;
399
+ }
400
+ /**
401
+ * Converts the OpenAPI path to a TypeScript template literal string.
402
+ * An optional `replacer` can transform each extracted parameter name before interpolation.
403
+ *
404
+ * @example
405
+ * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
406
+ */
407
+ toTemplateString({ prefix = "", replacer } = {}) {
408
+ return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
409
+ if (i % 2 === 0) return part;
410
+ const param = this.#transformParam(part);
411
+ return `\${${replacer ? replacer(param) : param}}`;
412
+ }).join("")}\``;
413
+ }
414
+ /**
415
+ * Extracts all `{param}` segments from the path and returns them as a key-value map.
416
+ * An optional `replacer` transforms each parameter name in both key and value positions.
417
+ * Returns `undefined` when no path parameters are found.
418
+ *
419
+ * @example
420
+ * ```ts
421
+ * new URLPath('/pet/{petId}/tag/{tagId}').getParams()
422
+ * // { petId: 'petId', tagId: 'tagId' }
423
+ * ```
424
+ */
425
+ getParams(replacer) {
426
+ const params = {};
427
+ this.#eachParam((_raw, param) => {
428
+ const key = replacer ? replacer(param) : param;
429
+ params[key] = key;
430
+ });
431
+ return Object.keys(params).length > 0 ? params : void 0;
432
+ }
433
+ /** Converts the OpenAPI path to Express-style colon syntax.
434
+ *
435
+ * @example
436
+ * ```ts
437
+ * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
438
+ * ```
439
+ */
440
+ toURLPath() {
441
+ return this.path.replace(/\{([^}]+)\}/g, ":$1");
442
+ }
443
+ };
444
+ //#endregion
70
445
  //#region src/constants.ts
71
446
  /**
72
447
  * Base URL for the Kubb Studio web app.
73
448
  */
74
- const DEFAULT_STUDIO_URL = "https://studio.kubb.dev";
449
+ const DEFAULT_STUDIO_URL = "https://kubb.studio";
75
450
  /**
76
451
  * Default banner style written at the top of every generated file.
77
452
  */
@@ -94,6 +469,42 @@ const logLevel = {
94
469
  debug: 5
95
470
  };
96
471
  //#endregion
472
+ //#region src/definePlugin.ts
473
+ /**
474
+ * Wraps a factory function and returns a typed `Plugin` with lifecycle handlers grouped under `hooks`.
475
+ *
476
+ * Handlers live in a single `hooks` object (inspired by Astro integrations).
477
+ * All lifecycle events from `KubbHooks` are available for subscription.
478
+ *
479
+ * @note For real plugins, use a `PluginFactoryOptions` type parameter to get type-safe context in `kubb:plugin:setup`.
480
+ * Plugin names should follow the convention `plugin-<feature>` (e.g., `plugin-react-query`, `plugin-zod`).
481
+ *
482
+ * @example
483
+ * ```ts
484
+ * import { definePlugin } from '@kubb/core'
485
+ *
486
+ * export const pluginTs = definePlugin((options: { prefix?: string } = {}) => ({
487
+ * name: 'plugin-ts',
488
+ * hooks: {
489
+ * 'kubb:plugin:setup'(ctx) {
490
+ * ctx.setResolver(resolverTs)
491
+ * },
492
+ * },
493
+ * }))
494
+ * ```
495
+ */
496
+ function definePlugin(factory) {
497
+ return (options) => factory(options ?? {});
498
+ }
499
+ /**
500
+ * Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise.
501
+ * Used to determine whether an output path targets a single file or a directory.
502
+ */
503
+ function getMode(fileOrFolder) {
504
+ if (!fileOrFolder) return "split";
505
+ return extname(fileOrFolder) ? "single" : "split";
506
+ }
507
+ //#endregion
97
508
  //#region src/defineResolver.ts
98
509
  const stringPatternCache = /* @__PURE__ */ new Map();
99
510
  function testPattern(value, pattern) {
@@ -111,14 +522,12 @@ function testPattern(value, pattern) {
111
522
  * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).
112
523
  */
113
524
  function matchesOperationPattern(node, type, pattern) {
114
- switch (type) {
115
- case "tag": return node.tags.some((tag) => testPattern(tag, pattern));
116
- case "operationId": return testPattern(node.operationId, pattern);
117
- case "path": return testPattern(node.path, pattern);
118
- case "method": return testPattern(node.method.toLowerCase(), pattern);
119
- case "contentType": return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false;
120
- default: return false;
121
- }
525
+ if (type === "tag") return node.tags.some((tag) => testPattern(tag, pattern));
526
+ if (type === "operationId") return testPattern(node.operationId, pattern);
527
+ if (type === "path") return testPattern(node.path, pattern);
528
+ if (type === "method") return testPattern(node.method.toLowerCase(), pattern);
529
+ if (type === "contentType") return node.requestBody?.content?.some((c) => testPattern(c.contentType, pattern)) ?? false;
530
+ return false;
122
531
  }
123
532
  /**
124
533
  * Checks if a schema matches a pattern for a given filter type (`schemaName`).
@@ -126,10 +535,8 @@ function matchesOperationPattern(node, type, pattern) {
126
535
  * Returns `null` when the filter type doesn't apply to schemas.
127
536
  */
128
537
  function matchesSchemaPattern(node, type, pattern) {
129
- switch (type) {
130
- case "schemaName": return node.name ? testPattern(node.name, pattern) : false;
131
- default: return null;
132
- }
538
+ if (type === "schemaName") return node.name ? testPattern(node.name, pattern) : false;
539
+ return null;
133
540
  }
134
541
  /**
135
542
  * Default name resolver used by `defineResolver`.
@@ -139,10 +546,9 @@ function matchesSchemaPattern(node, type, pattern) {
139
546
  * - `camelCase` for everything else.
140
547
  */
141
548
  function defaultResolver(name, type) {
142
- let resolvedName = camelCase(name);
143
- if (type === "file" || type === "function") resolvedName = camelCase(name, { isFile: type === "file" });
144
- if (type === "type") resolvedName = pascalCase(name);
145
- return resolvedName;
549
+ if (type === "file" || type === "function") return camelCase(name, { isFile: type === "file" });
550
+ if (type === "type") return pascalCase(name);
551
+ return camelCase(name);
146
552
  }
147
553
  /**
148
554
  * Default option resolver — applies include/exclude filters and merges matching override options.
@@ -167,7 +573,8 @@ function defaultResolver(name, type) {
167
573
  * // → { enumType: 'enum' } when operationId matches
168
574
  * ```
169
575
  */
170
- function defaultResolveOptions(node, { options, exclude = [], include, override = [] }) {
576
+ const resolveOptionsCache = /* @__PURE__ */ new WeakMap();
577
+ function computeOptions(node, options, exclude, include, override) {
171
578
  if (isOperationNode(node)) {
172
579
  if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
173
580
  if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
@@ -191,6 +598,19 @@ function defaultResolveOptions(node, { options, exclude = [], include, override
191
598
  }
192
599
  return options;
193
600
  }
601
+ function defaultResolveOptions(node, { options, exclude = [], include, override = [] }) {
602
+ const optionsKey = options;
603
+ let byOptions = resolveOptionsCache.get(optionsKey);
604
+ if (!byOptions) {
605
+ byOptions = /* @__PURE__ */ new WeakMap();
606
+ resolveOptionsCache.set(optionsKey, byOptions);
607
+ }
608
+ const cached = byOptions.get(node);
609
+ if (cached !== void 0) return cached.value;
610
+ const result = computeOptions(node, options, exclude, include, override);
611
+ byOptions.set(node, { value: result });
612
+ return result;
613
+ }
194
614
  /**
195
615
  * Default path resolver used by `defineResolver`.
196
616
  *
@@ -236,17 +656,19 @@ function defaultResolveOptions(node, { options, exclude = [], include, override
236
656
  * ```
237
657
  */
238
658
  function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root, output, group }) {
239
- if ((pathMode ?? PluginDriver.getMode(path.resolve(root, output.path))) === "single") return path.resolve(root, output.path);
240
- let result;
241
- if (group && (groupPath || tag)) {
242
- const groupValue = group.type === "path" ? groupPath : tag;
243
- const defaultName = group.type === "tag" ? ({ group: g }) => `${camelCase(g)}Controller` : ({ group: g }) => {
244
- const segment = g.split("/").filter((s) => s !== "" && s !== "." && s !== "..")[0];
245
- return segment ? camelCase(segment) : "";
246
- };
247
- const resolveName = group.name ?? defaultName;
248
- result = path.resolve(root, output.path, resolveName({ group: groupValue }), baseName);
249
- } else result = path.resolve(root, output.path, baseName);
659
+ if ((pathMode ?? getMode(path.resolve(root, output.path))) === "single") return path.resolve(root, output.path);
660
+ const result = (() => {
661
+ if (group && (groupPath || tag)) {
662
+ const groupValue = group.type === "path" ? groupPath : tag;
663
+ const defaultName = group.type === "tag" ? ({ group: g }) => `${camelCase(g)}Controller` : ({ group: g }) => {
664
+ const segment = g.split("/").filter((s) => s !== "" && s !== "." && s !== "..")[0];
665
+ return segment ? camelCase(segment) : "";
666
+ };
667
+ const resolveName = group.name ?? defaultName;
668
+ return path.resolve(root, output.path, resolveName({ group: groupValue }), baseName);
669
+ }
670
+ return path.resolve(root, output.path, baseName);
671
+ })();
250
672
  const outputDir = path.resolve(root, output.path);
251
673
  const outputDirWithSep = outputDir.endsWith(path.sep) ? outputDir : `${outputDir}${path.sep}`;
252
674
  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.`);
@@ -263,28 +685,28 @@ function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root
263
685
  *
264
686
  * @example Resolve a schema file
265
687
  * ```ts
266
- * const file = defaultResolveFile(
688
+ * const file = defaultResolveFile.call(
689
+ * resolver,
267
690
  * { name: 'pet', extname: '.ts' },
268
691
  * { root: '/src', output: { path: 'types' } },
269
- * resolver,
270
692
  * )
271
693
  * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
272
694
  * ```
273
695
  *
274
696
  * @example Resolve an operation file with tag grouping
275
697
  * ```ts
276
- * const file = defaultResolveFile(
698
+ * const file = defaultResolveFile.call(
699
+ * resolver,
277
700
  * { name: 'listPets', extname: '.ts', tag: 'pets' },
278
701
  * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
279
- * resolver,
280
702
  * )
281
703
  * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
282
704
  * ```
283
705
  */
284
- function defaultResolveFile({ name, extname, tag, path: groupPath }, context, ctx) {
285
- const pathMode = PluginDriver.getMode(path.resolve(context.root, context.output.path));
286
- const baseName = `${pathMode === "single" ? "" : ctx.default(name, "file")}${extname}`;
287
- const filePath = ctx.resolvePath({
706
+ function defaultResolveFile({ name, extname, tag, path: groupPath }, context) {
707
+ const pathMode = getMode(path.resolve(context.root, context.output.path));
708
+ const baseName = `${pathMode === "single" ? "" : this.default(name, "file")}${extname}`;
709
+ const filePath = this.resolvePath({
288
710
  baseName,
289
711
  pathMode,
290
712
  tag,
@@ -293,7 +715,7 @@ function defaultResolveFile({ name, extname, tag, path: groupPath }, context, ct
293
715
  return createFile({
294
716
  path: filePath,
295
717
  baseName: path.basename(filePath),
296
- meta: { pluginName: ctx.pluginName },
718
+ meta: { pluginName: this.pluginName },
297
719
  sources: [],
298
720
  imports: [],
299
721
  exports: []
@@ -304,12 +726,16 @@ function defaultResolveFile({ name, extname, tag, path: groupPath }, context, ct
304
726
  */
305
727
  function buildDefaultBanner({ title, description, version, config }) {
306
728
  try {
307
- let source = "";
308
- if (Array.isArray(config.input)) {
309
- const first = config.input[0];
310
- if (first && "path" in first) source = path.basename(first.path);
311
- } else if ("path" in config.input) source = path.basename(config.input.path);
312
- else if ("data" in config.input) source = "text content";
729
+ const source = (() => {
730
+ if (Array.isArray(config.input)) {
731
+ const first = config.input[0];
732
+ if (first && "path" in first) return path.basename(first.path);
733
+ return "";
734
+ }
735
+ if (config.input && "path" in config.input) return path.basename(config.input.path);
736
+ if (config.input && "data" in config.input) return "text content";
737
+ return "";
738
+ })();
313
739
  let banner = "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n";
314
740
  if (config.output.defaultBanner === "simple") {
315
741
  banner += "*/\n";
@@ -333,10 +759,9 @@ function buildDefaultBanner({ title, description, version, config }) {
333
759
  *
334
760
  * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
335
761
  * When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
336
- * from the OAS spec when a `node` is provided).
762
+ * from the document metadata when `meta` is provided).
337
763
  *
338
- * - When `output.banner` is a function and `node` is provided, returns `output.banner(node)`.
339
- * - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice.
764
+ * - When `output.banner` is a function, calls it with `meta` and returns the result.
340
765
  * - When `output.banner` is a string, returns it directly.
341
766
  * - When `config.output.defaultBanner` is `false`, returns `undefined`.
342
767
  * - Otherwise returns the Kubb "Generated by Kubb" notice.
@@ -347,15 +772,15 @@ function buildDefaultBanner({ title, description, version, config }) {
347
772
  * // → '// my banner'
348
773
  * ```
349
774
  *
350
- * @example Function banner with node
775
+ * @example Function banner with metadata
351
776
  * ```ts
352
- * defaultResolveBanner(inputNode, { output: { banner: (node) => `// v${node.version}` }, config })
777
+ * defaultResolveBanner(meta, { output: { banner: (m) => `// v${m?.version}` }, config })
353
778
  * // → '// v3.0.0'
354
779
  * ```
355
780
  *
356
781
  * @example No user banner — Kubb notice with OAS metadata
357
782
  * ```ts
358
- * defaultResolveBanner(inputNode, { config })
783
+ * defaultResolveBanner(meta, { config })
359
784
  * // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
360
785
  * ```
361
786
  *
@@ -365,21 +790,20 @@ function buildDefaultBanner({ title, description, version, config }) {
365
790
  * // → undefined
366
791
  * ```
367
792
  */
368
- function defaultResolveBanner(node, { output, config }) {
369
- if (typeof output?.banner === "function") return output.banner(node);
793
+ function defaultResolveBanner(meta, { output, config }) {
794
+ if (typeof output?.banner === "function") return output.banner(meta);
370
795
  if (typeof output?.banner === "string") return output.banner;
371
796
  if (config.output.defaultBanner === false) return;
372
797
  return buildDefaultBanner({
373
- title: node?.meta?.title,
374
- version: node?.meta?.version,
798
+ title: meta?.title,
799
+ version: meta?.version,
375
800
  config
376
801
  });
377
802
  }
378
803
  /**
379
804
  * Default footer resolver — returns the footer string for a generated file.
380
805
  *
381
- * - When `output.footer` is a function and `node` is provided, calls it with the node.
382
- * - When `output.footer` is a function and `node` is absent, returns `undefined`.
806
+ * - When `output.footer` is a function, calls it with `meta` and returns the result.
383
807
  * - When `output.footer` is a string, returns it directly.
384
808
  * - Otherwise returns `undefined`.
385
809
  *
@@ -389,14 +813,14 @@ function defaultResolveBanner(node, { output, config }) {
389
813
  * // → '// end of file'
390
814
  * ```
391
815
  *
392
- * @example Function footer with node
816
+ * @example Function footer with metadata
393
817
  * ```ts
394
- * defaultResolveFooter(inputNode, { output: { footer: (node) => `// ${node.title}` }, config })
818
+ * defaultResolveFooter(meta, { output: { footer: (m) => `// ${m?.title}` }, config })
395
819
  * // → '// Pet Store'
396
820
  * ```
397
821
  */
398
- function defaultResolveFooter(node, { output }) {
399
- if (typeof output?.footer === "function") return node ? output.footer(node) : void 0;
822
+ function defaultResolveFooter(meta, { output }) {
823
+ if (typeof output?.footer === "function") return output.footer(meta);
400
824
  if (typeof output?.footer === "string") return output.footer;
401
825
  }
402
826
  /**
@@ -409,25 +833,24 @@ function defaultResolveFooter(node, { output }) {
409
833
  * - `resolvePath` — output path computation
410
834
  * - `resolveFile` — full `FileNode` construction
411
835
  *
412
- * The builder receives `ctx` a reference to the assembled resolver so methods can
413
- * call sibling resolver methods using `ctx` instead of `this`.
836
+ * Methods in the returned object can call sibling resolver methods via `this`.
414
837
  *
415
838
  * @example Basic resolver with naming helpers
416
839
  * ```ts
417
- * export const resolver = defineResolver<PluginTs>((ctx) => ({
840
+ * export const resolver = defineResolver<PluginTs>(() => ({
418
841
  * name: 'default',
419
842
  * resolveName(node) {
420
- * return ctx.default(node.name, 'function')
843
+ * return this.default(node.name, 'function')
421
844
  * },
422
845
  * resolveTypedName(node) {
423
- * return ctx.default(node.name, 'type')
846
+ * return this.default(node.name, 'type')
424
847
  * },
425
848
  * }))
426
849
  * ```
427
850
  *
428
851
  * @example Override resolvePath for a custom output structure
429
852
  * ```ts
430
- * export const resolver = defineResolver<PluginTs>((_ctx) => ({
853
+ * export const resolver = defineResolver<PluginTs>(() => ({
431
854
  * name: 'custom',
432
855
  * resolvePath({ baseName }, { root, output }) {
433
856
  * return path.resolve(root, output.path, 'generated', baseName)
@@ -435,27 +858,27 @@ function defaultResolveFooter(node, { output }) {
435
858
  * }))
436
859
  * ```
437
860
  *
438
- * @example Use ctx.default inside a helper
861
+ * @example Use this.default inside a helper
439
862
  * ```ts
440
- * export const resolver = defineResolver<PluginTs>((ctx) => ({
863
+ * export const resolver = defineResolver<PluginTs>(() => ({
441
864
  * name: 'default',
442
865
  * resolveParamName(node, param) {
443
- * return ctx.default(`${node.operationId} ${param.in} ${param.name}`, 'type')
866
+ * return this.default(`${node.operationId} ${param.in} ${param.name}`, 'type')
444
867
  * },
445
868
  * }))
446
869
  * ```
447
870
  */
448
871
  function defineResolver(build) {
449
- const resolver = {};
450
- Object.assign(resolver, {
872
+ let resolver;
873
+ resolver = {
451
874
  default: defaultResolver,
452
875
  resolveOptions: defaultResolveOptions,
453
876
  resolvePath: defaultResolvePath,
454
- resolveFile: (params, context) => defaultResolveFile(params, context, resolver),
877
+ resolveFile: (params, context) => defaultResolveFile.call(resolver, params, context),
455
878
  resolveBanner: defaultResolveBanner,
456
879
  resolveFooter: defaultResolveFooter,
457
- ...build(resolver)
458
- });
880
+ ...build()
881
+ };
459
882
  return resolver;
460
883
  }
461
884
  //#endregion
@@ -583,6 +1006,16 @@ var FileManager = class {
583
1006
  this.#filesCache = null;
584
1007
  }
585
1008
  /**
1009
+ * Releases all stored files. Called by the core after `kubb:build:end` to
1010
+ * free the per-plugin FileNode caches for the rest of the process lifetime.
1011
+ */
1012
+ dispose() {
1013
+ this.clear();
1014
+ }
1015
+ [Symbol.dispose]() {
1016
+ this.dispose();
1017
+ }
1018
+ /**
586
1019
  * All stored files, sorted by path length (shorter paths first).
587
1020
  */
588
1021
  get files() {
@@ -600,35 +1033,11 @@ var FileManager = class {
600
1033
  }
601
1034
  };
602
1035
  //#endregion
603
- //#region src/renderNode.ts
604
- /**
605
- * Handles the return value of a plugin AST hook or generator method.
606
- *
607
- * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`
608
- * - `Array<FileNode>` → added directly into `driver.fileManager`
609
- * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
610
- *
611
- * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result
612
- * may be a renderer element. Generators that only return `Array<FileNode>` do not need one.
613
- */
614
- async function applyHookResult(result, driver, rendererFactory) {
615
- if (!result) return;
616
- if (Array.isArray(result)) {
617
- driver.fileManager.upsert(...result);
618
- return;
619
- }
620
- if (!rendererFactory) return;
621
- const renderer = rendererFactory();
622
- await renderer.render(result);
623
- driver.fileManager.upsert(...renderer.files);
624
- renderer.unmount();
625
- }
626
- //#endregion
627
- //#region src/PluginDriver.ts
1036
+ //#region src/KubbDriver.ts
628
1037
  function enforceOrder(enforce) {
629
1038
  return enforce === "pre" ? -1 : enforce === "post" ? 1 : 0;
630
1039
  }
631
- var PluginDriver = class PluginDriver {
1040
+ var KubbDriver = class KubbDriver {
632
1041
  config;
633
1042
  options;
634
1043
  /**
@@ -636,21 +1045,34 @@ var PluginDriver = class PluginDriver {
636
1045
  *
637
1046
  * @example
638
1047
  * ```ts
639
- * PluginDriver.getMode('src/gen/types.ts') // 'single'
640
- * PluginDriver.getMode('src/gen/types') // 'split'
1048
+ * KubbDriver.getMode('src/gen/types.ts') // 'single'
1049
+ * KubbDriver.getMode('src/gen/types') // 'split'
641
1050
  * ```
642
1051
  */
643
1052
  static getMode(fileOrFolder) {
644
- if (!fileOrFolder) return "split";
645
- return extname(fileOrFolder) ? "single" : "split";
1053
+ return getMode(fileOrFolder);
646
1054
  }
647
1055
  /**
648
- * The universal `@kubb/ast` `InputNode` produced by the adapter, set by
649
- * the build pipeline after the adapter's `parse()` resolves.
1056
+ * The streaming `InputStreamNode` produced by the adapter.
1057
+ * Always set after adapter setup — parse-only adapters are wrapped automatically.
650
1058
  */
651
1059
  inputNode = void 0;
652
1060
  adapter = void 0;
653
- #studioIsOpen = false;
1061
+ /**
1062
+ * Studio session state, kept together so `dispose()` can reset it atomically.
1063
+ *
1064
+ * - `source` holds the raw adapter source so `adapter.parse()` can be called lazily.
1065
+ * Intentionally outlives the build; cleared by `dispose()`.
1066
+ * - `isOpen` prevents opening the studio more than once per build.
1067
+ * - `inputNode` caches the parse promise so `adapter.parse()` is called at most once
1068
+ * per studio session, even when `openInStudio()` is called multiple times.
1069
+ */
1070
+ #studio = {
1071
+ source: void 0,
1072
+ isOpen: false,
1073
+ inputNode: void 0
1074
+ };
1075
+ #middlewareListeners = [];
654
1076
  /**
655
1077
  * Central file store for all generated files.
656
1078
  * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
@@ -662,23 +1084,29 @@ var PluginDriver = class PluginDriver {
662
1084
  * Tracks which plugins have generators registered via `addGenerator()` (event-based path).
663
1085
  * Used by the build loop to decide whether to emit generator events for a given plugin.
664
1086
  */
665
- #pluginsWithEventGenerators = /* @__PURE__ */ new Set();
1087
+ #eventGeneratorPlugins = /* @__PURE__ */ new Set();
666
1088
  #resolvers = /* @__PURE__ */ new Map();
667
1089
  #defaultResolvers = /* @__PURE__ */ new Map();
668
1090
  #hookListeners = /* @__PURE__ */ new Map();
669
1091
  constructor(config, options) {
670
1092
  this.config = config;
671
1093
  this.options = options;
672
- config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin)).filter((plugin) => {
673
- if (typeof plugin.apply === "function") return plugin.apply(config);
674
- return true;
675
- }).sort((a, b) => {
1094
+ this.adapter = config.adapter;
1095
+ }
1096
+ async setup() {
1097
+ const normalized = this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin));
1098
+ normalized.sort((a, b) => {
676
1099
  if (b.dependencies?.includes(a.name)) return -1;
677
1100
  if (a.dependencies?.includes(b.name)) return 1;
678
1101
  return enforceOrder(a.enforce) - enforceOrder(b.enforce);
679
- }).forEach((plugin) => {
680
- this.plugins.set(plugin.name, plugin);
681
1102
  });
1103
+ for (const plugin of normalized) {
1104
+ if (plugin.apply) plugin.apply(this.config);
1105
+ this.#registerPlugin(plugin);
1106
+ this.plugins.set(plugin.name, plugin);
1107
+ }
1108
+ if (this.config.middleware) for (const middleware of this.config.middleware) for (const event of Object.keys(middleware.hooks)) this.#registerMiddleware(event, middleware.hooks);
1109
+ if (this.config.adapter) await this.#registerAdapter(this.config.adapter);
682
1110
  }
683
1111
  get hooks() {
684
1112
  return this.options.hooks;
@@ -687,19 +1115,48 @@ var PluginDriver = class PluginDriver {
687
1115
  * Creates an `NormalizedPlugin` from a hook-style plugin and registers
688
1116
  * its lifecycle handlers on the `AsyncEventEmitter`.
689
1117
  */
690
- #normalizePlugin(hookPlugin) {
691
- const normalizedPlugin = {
692
- name: hookPlugin.name,
693
- dependencies: hookPlugin.dependencies,
694
- enforce: hookPlugin.enforce,
695
- options: {
1118
+ #normalizePlugin(plugin) {
1119
+ const normalized = {
1120
+ name: plugin.name,
1121
+ dependencies: plugin.dependencies,
1122
+ enforce: plugin.enforce,
1123
+ hooks: plugin.hooks,
1124
+ options: plugin.options ?? {
696
1125
  output: { path: "." },
697
1126
  exclude: [],
698
1127
  override: []
699
1128
  }
700
1129
  };
701
- this.registerPluginHooks(hookPlugin, normalizedPlugin);
702
- return normalizedPlugin;
1130
+ if ("apply" in plugin && typeof plugin.apply === "function") normalized.apply = plugin.apply;
1131
+ return normalized;
1132
+ }
1133
+ async #registerAdapter(adapter) {
1134
+ const source = inputToAdapterSource(this.config);
1135
+ this.#studio.source = source;
1136
+ if (adapter.stream) {
1137
+ this.inputNode = await adapter.stream(source);
1138
+ await this.hooks.emit("kubb:debug", {
1139
+ date: /* @__PURE__ */ new Date(),
1140
+ logs: [`✓ Adapter '${adapter.name}' producing input stream`]
1141
+ });
1142
+ } else {
1143
+ const inputNode = await adapter.parse(source);
1144
+ this.inputNode = createStreamInput(arrayToAsyncIterable(inputNode.schemas), arrayToAsyncIterable(inputNode.operations), inputNode.meta);
1145
+ await this.hooks.emit("kubb:debug", {
1146
+ date: /* @__PURE__ */ new Date(),
1147
+ logs: [
1148
+ `✓ Adapter '${adapter.name}' resolved InputNode (wrapped as stream)`,
1149
+ ` • Schemas: ${inputNode.schemas.length}`,
1150
+ ` • Operations: ${inputNode.operations.length}`
1151
+ ]
1152
+ });
1153
+ }
1154
+ }
1155
+ #registerMiddleware(event, middlewareHooks) {
1156
+ const handler = middlewareHooks[event];
1157
+ if (!handler) return;
1158
+ this.hooks.on(event, handler);
1159
+ this.#middlewareListeners.push([event, handler]);
703
1160
  }
704
1161
  /**
705
1162
  * Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.
@@ -716,28 +1173,29 @@ var PluginDriver = class PluginDriver {
716
1173
  *
717
1174
  * @internal
718
1175
  */
719
- registerPluginHooks(hookPlugin, normalizedPlugin) {
720
- const { hooks } = hookPlugin;
1176
+ #registerPlugin(plugin) {
1177
+ const { hooks } = plugin;
1178
+ if (!hooks) return;
721
1179
  if (hooks["kubb:plugin:setup"]) {
722
1180
  const setupHandler = (globalCtx) => {
723
1181
  const pluginCtx = {
724
1182
  ...globalCtx,
725
- options: hookPlugin.options ?? {},
1183
+ options: plugin.options ?? {},
726
1184
  addGenerator: (gen) => {
727
- this.registerGenerator(normalizedPlugin.name, gen);
1185
+ this.registerGenerator(plugin.name, gen);
728
1186
  },
729
1187
  setResolver: (resolver) => {
730
- this.setPluginResolver(normalizedPlugin.name, resolver);
1188
+ this.setPluginResolver(plugin.name, resolver);
731
1189
  },
732
1190
  setTransformer: (visitor) => {
733
- normalizedPlugin.transformer = visitor;
1191
+ plugin.transformer = visitor;
734
1192
  },
735
1193
  setRenderer: (renderer) => {
736
- normalizedPlugin.renderer = renderer;
1194
+ plugin.renderer = renderer;
737
1195
  },
738
1196
  setOptions: (opts) => {
739
- normalizedPlugin.options = {
740
- ...normalizedPlugin.options,
1197
+ plugin.options = {
1198
+ ...plugin.options,
741
1199
  ...opts
742
1200
  };
743
1201
  },
@@ -798,7 +1256,11 @@ var PluginDriver = class PluginDriver {
798
1256
  if (gen.schema) {
799
1257
  const schemaHandler = async (node, ctx) => {
800
1258
  if (ctx.plugin.name !== pluginName) return;
801
- await applyHookResult(await gen.schema(node, ctx), this, resolveRenderer());
1259
+ await applyHookResult({
1260
+ result: await gen.schema(node, ctx),
1261
+ driver: this,
1262
+ rendererFactory: resolveRenderer()
1263
+ });
802
1264
  };
803
1265
  this.hooks.on("kubb:generate:schema", schemaHandler);
804
1266
  this.#trackHookListener("kubb:generate:schema", schemaHandler);
@@ -806,7 +1268,11 @@ var PluginDriver = class PluginDriver {
806
1268
  if (gen.operation) {
807
1269
  const operationHandler = async (node, ctx) => {
808
1270
  if (ctx.plugin.name !== pluginName) return;
809
- await applyHookResult(await gen.operation(node, ctx), this, resolveRenderer());
1271
+ await applyHookResult({
1272
+ result: await gen.operation(node, ctx),
1273
+ driver: this,
1274
+ rendererFactory: resolveRenderer()
1275
+ });
810
1276
  };
811
1277
  this.hooks.on("kubb:generate:operation", operationHandler);
812
1278
  this.#trackHookListener("kubb:generate:operation", operationHandler);
@@ -814,12 +1280,16 @@ var PluginDriver = class PluginDriver {
814
1280
  if (gen.operations) {
815
1281
  const operationsHandler = async (nodes, ctx) => {
816
1282
  if (ctx.plugin.name !== pluginName) return;
817
- await applyHookResult(await gen.operations(nodes, ctx), this, resolveRenderer());
1283
+ await applyHookResult({
1284
+ result: await gen.operations(nodes, ctx),
1285
+ driver: this,
1286
+ rendererFactory: resolveRenderer()
1287
+ });
818
1288
  };
819
1289
  this.hooks.on("kubb:generate:operations", operationsHandler);
820
1290
  this.#trackHookListener("kubb:generate:operations", operationsHandler);
821
1291
  }
822
- this.#pluginsWithEventGenerators.add(pluginName);
1292
+ this.#eventGeneratorPlugins.add(pluginName);
823
1293
  }
824
1294
  /**
825
1295
  * Returns `true` when at least one generator was registered for the given plugin
@@ -828,8 +1298,8 @@ var PluginDriver = class PluginDriver {
828
1298
  * Used by the build loop to decide whether to walk the AST and emit generator events
829
1299
  * for a plugin that has no static `plugin.generators`.
830
1300
  */
831
- hasRegisteredGenerators(pluginName) {
832
- return this.#pluginsWithEventGenerators.has(pluginName);
1301
+ hasEventGenerators(pluginName) {
1302
+ return this.#eventGeneratorPlugins.has(pluginName);
833
1303
  }
834
1304
  /**
835
1305
  * Unregisters all plugin lifecycle listeners from the shared event emitter.
@@ -840,7 +1310,20 @@ var PluginDriver = class PluginDriver {
840
1310
  dispose() {
841
1311
  for (const [event, handlers] of this.#hookListeners) for (const handler of handlers) this.hooks.off(event, handler);
842
1312
  this.#hookListeners.clear();
843
- this.#pluginsWithEventGenerators.clear();
1313
+ this.#eventGeneratorPlugins.clear();
1314
+ this.#resolvers.clear();
1315
+ this.#defaultResolvers.clear();
1316
+ this.fileManager.dispose();
1317
+ this.inputNode = void 0;
1318
+ this.#studio = {
1319
+ source: void 0,
1320
+ isOpen: false,
1321
+ inputNode: void 0
1322
+ };
1323
+ for (const [event, handler] of this.#middlewareListeners) this.hooks.off(event, handler);
1324
+ }
1325
+ [Symbol.dispose]() {
1326
+ this.dispose();
844
1327
  }
845
1328
  #trackHookListener(event, handler) {
846
1329
  let handlers = this.#hookListeners.get(event);
@@ -850,16 +1333,10 @@ var PluginDriver = class PluginDriver {
850
1333
  }
851
1334
  handlers.add(handler);
852
1335
  }
853
- #createDefaultResolver(pluginName) {
854
- const existingResolver = this.#defaultResolvers.get(pluginName);
855
- if (existingResolver) return existingResolver;
856
- const resolver = defineResolver((_ctx) => ({
857
- name: "default",
858
- pluginName
859
- }));
860
- this.#defaultResolvers.set(pluginName, resolver);
861
- return resolver;
862
- }
1336
+ #getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => defineResolver(() => ({
1337
+ name: "default",
1338
+ pluginName
1339
+ })));
863
1340
  /**
864
1341
  * Merges `partial` with the plugin's default resolver and stores the result.
865
1342
  * Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver`
@@ -867,7 +1344,7 @@ var PluginDriver = class PluginDriver {
867
1344
  */
868
1345
  setPluginResolver(pluginName, partial) {
869
1346
  const merged = {
870
- ...this.#createDefaultResolver(pluginName),
1347
+ ...this.#getDefaultResolver(pluginName),
871
1348
  ...partial
872
1349
  };
873
1350
  this.#resolvers.set(pluginName, merged);
@@ -875,7 +1352,7 @@ var PluginDriver = class PluginDriver {
875
1352
  if (plugin) plugin.resolver = merged;
876
1353
  }
877
1354
  getResolver(pluginName) {
878
- return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#createDefaultResolver(pluginName);
1355
+ return this.#resolvers.get(pluginName) ?? this.plugins.get(pluginName)?.resolver ?? this.#getDefaultResolver(pluginName);
879
1356
  }
880
1357
  getContext(plugin) {
881
1358
  const driver = this;
@@ -885,7 +1362,7 @@ var PluginDriver = class PluginDriver {
885
1362
  return resolve(driver.config.root, driver.config.output.path);
886
1363
  },
887
1364
  getMode(output) {
888
- return PluginDriver.getMode(resolve(driver.config.root, driver.config.output.path, output.path));
1365
+ return KubbDriver.getMode(resolve(driver.config.root, driver.config.output.path, output.path));
889
1366
  },
890
1367
  hooks: driver.hooks,
891
1368
  plugin,
@@ -899,8 +1376,11 @@ var PluginDriver = class PluginDriver {
899
1376
  upsertFile: async (...files) => {
900
1377
  driver.fileManager.upsert(...files);
901
1378
  },
902
- get inputNode() {
903
- return driver.inputNode;
1379
+ get meta() {
1380
+ return driver.inputNode?.meta ?? {
1381
+ circularNames: [],
1382
+ enumNames: []
1383
+ };
904
1384
  },
905
1385
  get adapter() {
906
1386
  return driver.adapter;
@@ -920,13 +1400,14 @@ var PluginDriver = class PluginDriver {
920
1400
  info(message) {
921
1401
  driver.hooks.emit("kubb:info", { message });
922
1402
  },
923
- openInStudio(options) {
924
- if (!driver.config.devtools || driver.#studioIsOpen) return;
1403
+ async openInStudio(options) {
1404
+ if (!driver.config.devtools || driver.#studio.isOpen) return;
925
1405
  if (typeof driver.config.devtools !== "object") throw new Error("Devtools must be an object");
926
- if (!driver.inputNode || !driver.adapter) throw new Error("adapter is not defined, make sure you have set the parser in kubb.config.ts");
927
- driver.#studioIsOpen = true;
928
- const studioUrl = driver.config.devtools?.studioUrl ?? "https://studio.kubb.dev";
929
- return openInStudio(driver.inputNode, studioUrl, options);
1406
+ if (!driver.adapter || !driver.#studio.source) throw new Error("adapter is not defined, make sure you have set the parser in kubb.config.ts");
1407
+ driver.#studio.isOpen = true;
1408
+ const studioUrl = driver.config.devtools?.studioUrl ?? "https://kubb.studio";
1409
+ driver.#studio.inputNode ??= Promise.resolve(driver.adapter.parse(driver.#studio.source));
1410
+ return openInStudio(await driver.#studio.inputNode, studioUrl, options);
930
1411
  }
931
1412
  };
932
1413
  }
@@ -939,7 +1420,57 @@ var PluginDriver = class PluginDriver {
939
1420
  return plugin;
940
1421
  }
941
1422
  };
1423
+ /**
1424
+ * Handles the return value of a plugin AST hook or generator method.
1425
+ *
1426
+ * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`
1427
+ * - `Array<FileNode>` → added directly into `driver.fileManager`
1428
+ * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
1429
+ *
1430
+ * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result
1431
+ * may be a renderer element. Generators that only return `Array<FileNode>` do not need one.
1432
+ */
1433
+ function applyHookResult({ result, driver, rendererFactory }) {
1434
+ if (!result) return;
1435
+ if (Array.isArray(result)) {
1436
+ driver.fileManager.upsert(...result);
1437
+ return;
1438
+ }
1439
+ if (!rendererFactory) return;
1440
+ const renderer = rendererFactory();
1441
+ if (renderer.stream) {
1442
+ for (const file of renderer.stream(result)) driver.fileManager.upsert(file);
1443
+ renderer.unmount();
1444
+ return;
1445
+ }
1446
+ return applyAsyncRender({
1447
+ renderer,
1448
+ result,
1449
+ driver
1450
+ });
1451
+ }
1452
+ async function applyAsyncRender({ renderer, result, driver }) {
1453
+ await renderer.render(result);
1454
+ driver.fileManager.upsert(...renderer.files);
1455
+ renderer.unmount();
1456
+ }
1457
+ function inputToAdapterSource(config) {
1458
+ const input = config.input;
1459
+ if (!input) throw new Error("[kubb] input is required when using an adapter. Provide input.path or input.data in your config.");
1460
+ if ("data" in input) return {
1461
+ type: "data",
1462
+ data: input.data
1463
+ };
1464
+ if (new URLPath(input.path).isURL) return {
1465
+ type: "path",
1466
+ path: input.path
1467
+ };
1468
+ return {
1469
+ type: "path",
1470
+ path: resolve(config.root, input.path)
1471
+ };
1472
+ }
942
1473
  //#endregion
943
- export { DEFAULT_BANNER as a, logLevel as c, defineResolver as i, camelCase as l, applyHookResult as n, DEFAULT_EXTENSION as o, FileManager as r, DEFAULT_STUDIO_URL as s, PluginDriver as t };
1474
+ export { definePlugin as a, DEFAULT_STUDIO_URL as c, forBatches as d, isPromise as f, defineResolver as i, logLevel as l, applyHookResult as n, DEFAULT_BANNER as o, withDrain as p, FileManager as r, DEFAULT_EXTENSION as s, KubbDriver as t, URLPath as u };
944
1475
 
945
- //# sourceMappingURL=PluginDriver-DV3p2Hky.js.map
1476
+ //# sourceMappingURL=KubbDriver-Cxii_rBp.js.map