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

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.
@@ -1,8 +1,152 @@
1
1
  import "./chunk--u3MIqq1.js";
2
+ import { EventEmitter } from "node:events";
2
3
  import path, { extname, resolve } from "node:path";
3
- import { createFile, createStreamInput, isOperationNode, isSchemaNode } from "@kubb/ast";
4
+ import { collectUsedSchemaNames, createFile, createStreamInput, extractStringsFromNodes, isOperationNode, isSchemaNode, transform } from "@kubb/ast";
4
5
  import { deflateSync } from "fflate";
5
6
  import { x } from "tinyexec";
7
+ //#region ../../internals/utils/src/errors.ts
8
+ /**
9
+ * Thrown when one or more errors occur during a Kubb build.
10
+ * Carries the full list of underlying errors on `errors`.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * throw new BuildError('Build failed', { errors: [err1, err2] })
15
+ * ```
16
+ */
17
+ var BuildError = class extends Error {
18
+ errors;
19
+ constructor(message, options) {
20
+ super(message, { cause: options.cause });
21
+ this.name = "BuildError";
22
+ this.errors = options.errors;
23
+ }
24
+ };
25
+ /**
26
+ * Coerces an unknown thrown value to an `Error` instance.
27
+ * Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * try { ... } catch(err) {
32
+ * throw new BuildError('Build failed', { cause: toError(err), errors: [] })
33
+ * }
34
+ * ```
35
+ */
36
+ function toError(value) {
37
+ return value instanceof Error ? value : new Error(String(value));
38
+ }
39
+ //#endregion
40
+ //#region ../../internals/utils/src/asyncEventEmitter.ts
41
+ /**
42
+ * Typed `EventEmitter` that awaits all async listeners before resolving.
43
+ * Wraps Node's `EventEmitter` with full TypeScript event-map inference.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const emitter = new AsyncEventEmitter<{ build: [name: string] }>()
48
+ * emitter.on('build', async (name) => { console.log(name) })
49
+ * await emitter.emit('build', 'petstore') // all listeners awaited
50
+ * ```
51
+ */
52
+ var AsyncEventEmitter = class {
53
+ /**
54
+ * Maximum number of listeners per event before Node emits a memory-leak warning.
55
+ * @default 10
56
+ */
57
+ constructor(maxListener = 10) {
58
+ this.#emitter.setMaxListeners(maxListener);
59
+ }
60
+ #emitter = new EventEmitter();
61
+ /**
62
+ * Emits `eventName` and awaits all registered listeners sequentially.
63
+ * Throws if any listener rejects, wrapping the cause with the event name and serialized arguments.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * await emitter.emit('build', 'petstore')
68
+ * ```
69
+ */
70
+ emit(eventName, ...eventArgs) {
71
+ const listeners = this.#emitter.listeners(eventName);
72
+ if (listeners.length === 0) return;
73
+ return this.#emitAll(eventName, listeners, eventArgs);
74
+ }
75
+ async #emitAll(eventName, listeners, eventArgs) {
76
+ for (const listener of listeners) try {
77
+ await listener(...eventArgs);
78
+ } catch (err) {
79
+ let serializedArgs;
80
+ try {
81
+ serializedArgs = JSON.stringify(eventArgs);
82
+ } catch {
83
+ serializedArgs = String(eventArgs);
84
+ }
85
+ throw new Error(`Error in async listener for "${eventName}" with eventArgs ${serializedArgs}`, { cause: toError(err) });
86
+ }
87
+ }
88
+ /**
89
+ * Registers a persistent listener for `eventName`.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * emitter.on('build', async (name) => { console.log(name) })
94
+ * ```
95
+ */
96
+ on(eventName, handler) {
97
+ this.#emitter.on(eventName, handler);
98
+ }
99
+ /**
100
+ * Registers a one-shot listener that removes itself after the first invocation.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * emitter.onOnce('build', async (name) => { console.log(name) })
105
+ * ```
106
+ */
107
+ onOnce(eventName, handler) {
108
+ const wrapper = (...args) => {
109
+ this.off(eventName, wrapper);
110
+ return handler(...args);
111
+ };
112
+ this.on(eventName, wrapper);
113
+ }
114
+ /**
115
+ * Removes a previously registered listener.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * emitter.off('build', handler)
120
+ * ```
121
+ */
122
+ off(eventName, handler) {
123
+ this.#emitter.off(eventName, handler);
124
+ }
125
+ /**
126
+ * Returns the number of listeners registered for `eventName`.
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * emitter.on('build', handler)
131
+ * emitter.listenerCount('build') // 1
132
+ * ```
133
+ */
134
+ listenerCount(eventName) {
135
+ return this.#emitter.listenerCount(eventName);
136
+ }
137
+ /**
138
+ * Removes all listeners from every event channel.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * emitter.removeAll()
143
+ * ```
144
+ */
145
+ removeAll() {
146
+ this.#emitter.removeAllListeners();
147
+ }
148
+ };
149
+ //#endregion
6
150
  //#region ../../internals/utils/src/casing.ts
7
151
  /**
8
152
  * Shared implementation for camelCase and PascalCase conversion.
@@ -67,6 +211,39 @@ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
67
211
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
68
212
  }
69
213
  //#endregion
214
+ //#region ../../internals/utils/src/time.ts
215
+ /**
216
+ * Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.
217
+ * Rounds to 2 decimal places for sub-millisecond precision without noise.
218
+ *
219
+ * @example
220
+ * ```ts
221
+ * const start = process.hrtime()
222
+ * doWork()
223
+ * getElapsedMs(start) // 42.35
224
+ * ```
225
+ */
226
+ function getElapsedMs(hrStart) {
227
+ const [seconds, nanoseconds] = process.hrtime(hrStart);
228
+ const ms = seconds * 1e3 + nanoseconds / 1e6;
229
+ return Math.round(ms * 100) / 100;
230
+ }
231
+ /**
232
+ * Converts a millisecond duration into a human-readable string (`ms`, `s`, or `m s`).
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * formatMs(250) // '250ms'
237
+ * formatMs(1500) // '1.50s'
238
+ * formatMs(90000) // '1m 30.0s'
239
+ * ```
240
+ */
241
+ function formatMs(ms) {
242
+ if (ms >= 6e4) return `${Math.floor(ms / 6e4)}m ${(ms % 6e4 / 1e3).toFixed(1)}s`;
243
+ if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
244
+ return `${Math.round(ms)}ms`;
245
+ }
246
+ //#endregion
70
247
  //#region ../../internals/utils/src/promise.ts
71
248
  function* chunks(arr, size) {
72
249
  for (let i = 0; i < arr.length; i += size) yield arr.slice(i, i + size);
@@ -109,22 +286,6 @@ async function forBatches(source, process, options) {
109
286
  if (flush) await flush();
110
287
  }
111
288
  }
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
289
  /** Returns `true` when `result` is a thenable `Promise`.
129
290
  *
130
291
  * @example
@@ -404,12 +565,13 @@ var URLPath = class {
404
565
  * @example
405
566
  * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
406
567
  */
407
- toTemplateString({ prefix = "", replacer } = {}) {
408
- return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
568
+ toTemplateString({ prefix, replacer } = {}) {
569
+ const result = this.path.split(/\{([^}]+)\}/).map((part, i) => {
409
570
  if (i % 2 === 0) return part;
410
571
  const param = this.#transformParam(part);
411
572
  return `\${${replacer ? replacer(param) : param}}`;
412
- }).join("")}\``;
573
+ }).join("");
574
+ return `\`${prefix ?? ""}${result}\``;
413
575
  }
414
576
  /**
415
577
  * Extracts all `{param}` segments from the path and returns them as a key-value map.
@@ -787,13 +949,13 @@ function buildDefaultBanner({ title, description, version, config }) {
787
949
  * @example Disabled default banner
788
950
  * ```ts
789
951
  * defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })
790
- * // → undefined
952
+ * // → null
791
953
  * ```
792
954
  */
793
955
  function defaultResolveBanner(meta, { output, config }) {
794
956
  if (typeof output?.banner === "function") return output.banner(meta);
795
957
  if (typeof output?.banner === "string") return output.banner;
796
- if (config.output.defaultBanner === false) return;
958
+ if (config.output.defaultBanner === false) return null;
797
959
  return buildDefaultBanner({
798
960
  title: meta?.title,
799
961
  version: meta?.version,
@@ -822,6 +984,7 @@ function defaultResolveBanner(meta, { output, config }) {
822
984
  function defaultResolveFooter(meta, { output }) {
823
985
  if (typeof output?.footer === "function") return output.footer(meta);
824
986
  if (typeof output?.footer === "string") return output.footer;
987
+ return null;
825
988
  }
826
989
  /**
827
990
  * Defines a resolver for a plugin, injecting built-in defaults for name casing,
@@ -932,111 +1095,241 @@ function mergeFile(a, b) {
932
1095
  ...a,
933
1096
  banner: b.banner,
934
1097
  footer: b.footer,
935
- sources: [...a.sources || [], ...b.sources || []],
936
- imports: [...a.imports || [], ...b.imports || []],
937
- exports: [...a.exports || [], ...b.exports || []]
1098
+ sources: a.sources.length ? b.sources.length ? [...a.sources, ...b.sources] : a.sources : b.sources,
1099
+ imports: a.imports.length ? b.imports.length ? [...a.imports, ...b.imports] : a.imports : b.imports,
1100
+ exports: a.exports.length ? b.exports.length ? [...a.exports, ...b.exports] : a.exports : b.exports
938
1101
  };
939
1102
  }
940
- /**
941
- * Collapses a list of files so that duplicates sharing the same `path` are merged
942
- * in arrival order. Keeps the original order of first occurrence.
943
- */
944
- function mergeFilesByPath(files) {
945
- const merged = /* @__PURE__ */ new Map();
946
- for (const file of files) {
947
- const existing = merged.get(file.path);
948
- merged.set(file.path, existing ? mergeFile(existing, file) : file);
949
- }
950
- return merged;
1103
+ function isIndexPath(path) {
1104
+ return path.endsWith("/index.ts") || path === "index.ts";
1105
+ }
1106
+ function compareFiles(a, b) {
1107
+ const lenDiff = a.path.length - b.path.length;
1108
+ if (lenDiff !== 0) return lenDiff;
1109
+ const aIsIndex = isIndexPath(a.path);
1110
+ const bIsIndex = isIndexPath(b.path);
1111
+ if (aIsIndex && !bIsIndex) return 1;
1112
+ if (!aIsIndex && bIsIndex) return -1;
1113
+ return 0;
951
1114
  }
952
1115
  /**
953
- * In-memory file store for generated files.
954
- *
955
- * Files with the same `path` are merged sources, imports, and exports are concatenated.
956
- * The `files` getter returns all stored files sorted by path length (shortest first).
1116
+ * In-memory file store for generated files. Files sharing a `path` are merged
1117
+ * (sources/imports/exports concatenated). The `files` getter is sorted by
1118
+ * path length (barrel `index.ts` last within a bucket).
957
1119
  *
958
1120
  * @example
959
1121
  * ```ts
960
- * import { FileManager } from '@kubb/core'
961
- *
962
1122
  * const manager = new FileManager()
963
1123
  * manager.upsert(myFile)
964
- * console.log(manager.files) // all stored files
1124
+ * manager.files // sorted view
965
1125
  * ```
966
1126
  */
967
1127
  var FileManager = class {
968
1128
  #cache = /* @__PURE__ */ new Map();
969
- #filesCache = null;
1129
+ #sorted = null;
1130
+ #onUpsert = null;
970
1131
  /**
971
- * Adds one or more files. Incoming files with the same path are merged
972
- * (sources/imports/exports concatenated), but existing cache entries are
973
- * replaced use {@link upsert} when you want to merge into the cache too.
1132
+ * Registers a callback invoked with the resolved {@link FileNode} on every
1133
+ * `add` / `upsert`. Used by the build loop to track newly written files
1134
+ * without keeping its own scan-based diff. Single subscriber by design
1135
+ * setting again replaces the previous callback. Pass `null` to detach.
974
1136
  */
1137
+ setOnUpsert(callback) {
1138
+ this.#onUpsert = callback;
1139
+ }
975
1140
  add(...files) {
976
1141
  return this.#store(files, false);
977
1142
  }
978
- /**
979
- * Adds or merges one or more files.
980
- * If a file with the same path already exists in the cache, its
981
- * sources/imports/exports are merged into the incoming file.
982
- */
983
1143
  upsert(...files) {
984
1144
  return this.#store(files, true);
985
1145
  }
986
1146
  #store(files, mergeExisting) {
987
- const resolvedFiles = [];
988
- for (const file of mergeFilesByPath(files).values()) {
989
- const existing = mergeExisting ? this.#cache.get(file.path) : void 0;
990
- const resolvedFile = createFile(existing ? mergeFile(existing, file) : file);
991
- this.#cache.set(resolvedFile.path, resolvedFile);
992
- resolvedFiles.push(resolvedFile);
1147
+ const batch = files.length > 1 ? this.#dedupe(files) : files;
1148
+ const resolved = [];
1149
+ for (const file of batch) {
1150
+ const existing = this.#cache.get(file.path);
1151
+ const merged = existing && mergeExisting ? createFile(mergeFile(existing, file)) : createFile(file);
1152
+ this.#cache.set(merged.path, merged);
1153
+ resolved.push(merged);
1154
+ this.#onUpsert?.(merged);
1155
+ }
1156
+ if (resolved.length > 0) this.#sorted = null;
1157
+ return resolved;
1158
+ }
1159
+ #dedupe(files) {
1160
+ const seen = /* @__PURE__ */ new Map();
1161
+ for (const file of files) {
1162
+ const prev = seen.get(file.path);
1163
+ seen.set(file.path, prev ? mergeFile(prev, file) : file);
993
1164
  }
994
- this.#filesCache = null;
995
- return resolvedFiles;
1165
+ return [...seen.values()];
996
1166
  }
997
1167
  getByPath(path) {
998
1168
  return this.#cache.get(path) ?? null;
999
1169
  }
1000
1170
  deleteByPath(path) {
1001
- this.#cache.delete(path);
1002
- this.#filesCache = null;
1171
+ if (!this.#cache.delete(path)) return;
1172
+ this.#sorted = null;
1003
1173
  }
1004
1174
  clear() {
1005
1175
  this.#cache.clear();
1006
- this.#filesCache = null;
1176
+ this.#sorted = null;
1007
1177
  }
1008
1178
  /**
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.
1179
+ * Releases all stored files. Called by the core after `kubb:build:end`.
1011
1180
  */
1012
1181
  dispose() {
1013
1182
  this.clear();
1183
+ this.#onUpsert = null;
1014
1184
  }
1015
1185
  [Symbol.dispose]() {
1016
1186
  this.dispose();
1017
1187
  }
1018
1188
  /**
1019
- * All stored files, sorted by path length (shorter paths first).
1189
+ * All stored files in stable sort order (shortest path first, barrel files
1190
+ * last within a length bucket). Returns a cached view — do not mutate.
1020
1191
  */
1021
1192
  get files() {
1022
- if (this.#filesCache) return this.#filesCache;
1023
- this.#filesCache = [...this.#cache.values()].sort((a, b) => {
1024
- const lenDiff = a.path.length - b.path.length;
1025
- if (lenDiff !== 0) return lenDiff;
1026
- const aIsIndex = a.path.endsWith("/index.ts") || a.path === "index.ts";
1027
- const bIsIndex = b.path.endsWith("/index.ts") || b.path === "index.ts";
1028
- if (aIsIndex && !bIsIndex) return 1;
1029
- if (!aIsIndex && bIsIndex) return -1;
1030
- return 0;
1193
+ return this.#sorted ??= [...this.#cache.values()].sort(compareFiles);
1194
+ }
1195
+ };
1196
+ //#endregion
1197
+ //#region src/FileProcessor.ts
1198
+ function joinSources(file) {
1199
+ const sources = file.sources;
1200
+ if (sources.length === 0) return "";
1201
+ const parts = [];
1202
+ for (const source of sources) {
1203
+ const s = extractStringsFromNodes(source.nodes);
1204
+ if (s) parts.push(s);
1205
+ }
1206
+ return parts.join("\n\n");
1207
+ }
1208
+ /**
1209
+ * Converts a single file to a string using the registered parsers.
1210
+ * Falls back to joining source values when no matching parser is found.
1211
+ *
1212
+ * @internal
1213
+ */
1214
+ var FileProcessor = class {
1215
+ events = new AsyncEventEmitter();
1216
+ parse(file, { parsers, extension } = {}) {
1217
+ const parseExtName = extension?.[file.extname] || void 0;
1218
+ if (!parsers || !file.extname) return joinSources(file);
1219
+ const parser = parsers.get(file.extname);
1220
+ if (!parser) return joinSources(file);
1221
+ return parser.parse(file, { extname: parseExtName });
1222
+ }
1223
+ *stream(files, options = {}) {
1224
+ const total = files.length;
1225
+ if (total === 0) return;
1226
+ let processed = 0;
1227
+ for (const file of files) {
1228
+ const source = this.parse(file, options);
1229
+ processed++;
1230
+ yield {
1231
+ file,
1232
+ source,
1233
+ processed,
1234
+ total,
1235
+ percentage: processed / total * 100
1236
+ };
1237
+ }
1238
+ }
1239
+ async run(files, options = {}) {
1240
+ await this.events.emit("start", files);
1241
+ for (const { file, source, processed, total, percentage } of this.stream(files, options)) await this.events.emit("update", {
1242
+ file,
1243
+ source,
1244
+ processed,
1245
+ percentage,
1246
+ total
1031
1247
  });
1032
- return this.#filesCache;
1248
+ await this.events.emit("end", files);
1249
+ return files;
1250
+ }
1251
+ /**
1252
+ * Clears all registered event listeners.
1253
+ */
1254
+ dispose() {
1255
+ this.events.removeAll();
1256
+ }
1257
+ [Symbol.dispose]() {
1258
+ this.dispose();
1033
1259
  }
1034
1260
  };
1035
1261
  //#endregion
1262
+ //#region \0@oxc-project+runtime@0.129.0/helpers/usingCtx.js
1263
+ function _usingCtx() {
1264
+ var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
1265
+ var n = Error();
1266
+ return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
1267
+ };
1268
+ var e = {};
1269
+ var n = [];
1270
+ function using(r, e) {
1271
+ if (null != e) {
1272
+ if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
1273
+ if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
1274
+ if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
1275
+ if ("function" != typeof o) throw new TypeError("Object is not disposable.");
1276
+ t && (o = function o() {
1277
+ try {
1278
+ t.call(e);
1279
+ } catch (r) {
1280
+ return Promise.reject(r);
1281
+ }
1282
+ }), n.push({
1283
+ v: e,
1284
+ d: o,
1285
+ a: r
1286
+ });
1287
+ } else r && n.push({
1288
+ d: e,
1289
+ a: r
1290
+ });
1291
+ return e;
1292
+ }
1293
+ return {
1294
+ e,
1295
+ u: using.bind(null, !1),
1296
+ a: using.bind(null, !0),
1297
+ d: function d() {
1298
+ var o;
1299
+ var t = this.e;
1300
+ var s = 0;
1301
+ function next() {
1302
+ for (; o = n.pop();) try {
1303
+ if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
1304
+ if (o.d) {
1305
+ var r = o.d.call(o.v);
1306
+ if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
1307
+ } else s |= 1;
1308
+ } catch (r) {
1309
+ return err(r);
1310
+ }
1311
+ if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
1312
+ if (t !== e) throw t;
1313
+ }
1314
+ function err(n) {
1315
+ return t = t !== e ? new r(n, t) : n, next();
1316
+ }
1317
+ return next();
1318
+ }
1319
+ };
1320
+ }
1321
+ //#endregion
1036
1322
  //#region src/KubbDriver.ts
1037
1323
  function enforceOrder(enforce) {
1038
1324
  return enforce === "pre" ? -1 : enforce === "post" ? 1 : 0;
1039
1325
  }
1326
+ const OPERATION_FILTER_TYPES = new Set([
1327
+ "tag",
1328
+ "operationId",
1329
+ "path",
1330
+ "method",
1331
+ "contentType"
1332
+ ]);
1040
1333
  var KubbDriver = class KubbDriver {
1041
1334
  config;
1042
1335
  options;
@@ -1056,8 +1349,8 @@ var KubbDriver = class KubbDriver {
1056
1349
  * The streaming `InputStreamNode` produced by the adapter.
1057
1350
  * Always set after adapter setup — parse-only adapters are wrapped automatically.
1058
1351
  */
1059
- inputNode = void 0;
1060
- adapter = void 0;
1352
+ inputNode = null;
1353
+ adapter = null;
1061
1354
  /**
1062
1355
  * Studio session state, kept together so `dispose()` can reset it atomically.
1063
1356
  *
@@ -1068,9 +1361,9 @@ var KubbDriver = class KubbDriver {
1068
1361
  * per studio session, even when `openInStudio()` is called multiple times.
1069
1362
  */
1070
1363
  #studio = {
1071
- source: void 0,
1364
+ source: null,
1072
1365
  isOpen: false,
1073
- inputNode: void 0
1366
+ inputNode: null
1074
1367
  };
1075
1368
  #middlewareListeners = [];
1076
1369
  /**
@@ -1079,6 +1372,7 @@ var KubbDriver = class KubbDriver {
1079
1372
  * add files; this property gives direct read/write access when needed.
1080
1373
  */
1081
1374
  fileManager = new FileManager();
1375
+ #fileProcessor = new FileProcessor();
1082
1376
  plugins = /* @__PURE__ */ new Map();
1083
1377
  /**
1084
1378
  * Tracks which plugins have generators registered via `addGenerator()` (event-based path).
@@ -1091,7 +1385,7 @@ var KubbDriver = class KubbDriver {
1091
1385
  constructor(config, options) {
1092
1386
  this.config = config;
1093
1387
  this.options = options;
1094
- this.adapter = config.adapter;
1388
+ this.adapter = config.adapter ?? null;
1095
1389
  }
1096
1390
  async setup() {
1097
1391
  const normalized = this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin));
@@ -1302,6 +1596,344 @@ var KubbDriver = class KubbDriver {
1302
1596
  return this.#eventGeneratorPlugins.has(pluginName);
1303
1597
  }
1304
1598
  /**
1599
+ * Runs the full plugin pipeline. Returns timings/failures collected so far even
1600
+ * when an outer hook throws — the orchestrator preserves partial state by capturing
1601
+ * the error into `error` instead of propagating.
1602
+ */
1603
+ async run({ storage }) {
1604
+ const hooks = this.hooks;
1605
+ const config = this.config;
1606
+ const failedPlugins = /* @__PURE__ */ new Set();
1607
+ const pluginTimings = /* @__PURE__ */ new Map();
1608
+ const parsersMap = /* @__PURE__ */ new Map();
1609
+ for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
1610
+ const pendingFiles = /* @__PURE__ */ new Map();
1611
+ this.fileManager.setOnUpsert((file) => {
1612
+ pendingFiles.set(file.path, file);
1613
+ });
1614
+ try {
1615
+ const flushPending = async () => {
1616
+ if (pendingFiles.size === 0) return;
1617
+ const files = [...pendingFiles.values()];
1618
+ pendingFiles.clear();
1619
+ await hooks.emit("kubb:debug", {
1620
+ date: /* @__PURE__ */ new Date(),
1621
+ logs: [`Writing ${files.length} files...`]
1622
+ });
1623
+ await hooks.emit("kubb:files:processing:start", { files });
1624
+ const items = [...this.#fileProcessor.stream(files, {
1625
+ parsers: parsersMap,
1626
+ extension: config.output.extension
1627
+ })];
1628
+ await hooks.emit("kubb:files:processing:update", { files: items.map(({ file, source, processed, total, percentage }) => ({
1629
+ file,
1630
+ source,
1631
+ processed,
1632
+ total,
1633
+ percentage,
1634
+ config
1635
+ })) });
1636
+ const queue = [];
1637
+ for (const { file, source } of items) if (source) {
1638
+ queue.push(storage.setItem(file.path, source));
1639
+ if (queue.length >= 50) await Promise.all(queue.splice(0));
1640
+ }
1641
+ await Promise.all(queue);
1642
+ await hooks.emit("kubb:files:processing:end", { files });
1643
+ await hooks.emit("kubb:debug", {
1644
+ date: /* @__PURE__ */ new Date(),
1645
+ logs: [`✓ File write process completed for ${files.length} files`]
1646
+ });
1647
+ };
1648
+ await this.emitSetupHooks();
1649
+ if (this.adapter && this.inputNode) await hooks.emit("kubb:build:start", Object.assign({
1650
+ config,
1651
+ adapter: this.adapter,
1652
+ meta: this.inputNode.meta,
1653
+ getPlugin: this.getPlugin.bind(this)
1654
+ }, this.#filesPayload()));
1655
+ const generatorPlugins = [];
1656
+ for (const plugin of this.plugins.values()) {
1657
+ const context = this.getContext(plugin);
1658
+ const hrStart = process.hrtime();
1659
+ try {
1660
+ await hooks.emit("kubb:plugin:start", { plugin });
1661
+ await hooks.emit("kubb:debug", {
1662
+ date: /* @__PURE__ */ new Date(),
1663
+ logs: ["Starting plugin...", ` • Plugin Name: ${plugin.name}`]
1664
+ });
1665
+ } catch (caughtError) {
1666
+ const error = caughtError;
1667
+ const duration = getElapsedMs(hrStart);
1668
+ pluginTimings.set(plugin.name, duration);
1669
+ await this.#emitPluginEnd({
1670
+ plugin,
1671
+ duration,
1672
+ success: false,
1673
+ error
1674
+ });
1675
+ failedPlugins.add({
1676
+ plugin,
1677
+ error
1678
+ });
1679
+ continue;
1680
+ }
1681
+ if (plugin.generators?.length || this.hasEventGenerators(plugin.name)) {
1682
+ generatorPlugins.push({
1683
+ plugin,
1684
+ context,
1685
+ hrStart
1686
+ });
1687
+ continue;
1688
+ }
1689
+ const duration = getElapsedMs(hrStart);
1690
+ pluginTimings.set(plugin.name, duration);
1691
+ await this.#emitPluginEnd({
1692
+ plugin,
1693
+ duration,
1694
+ success: true
1695
+ });
1696
+ await hooks.emit("kubb:debug", {
1697
+ date: /* @__PURE__ */ new Date(),
1698
+ logs: [`✓ Plugin started successfully (${formatMs(duration)})`]
1699
+ });
1700
+ }
1701
+ if (generatorPlugins.length > 0) if (this.inputNode) {
1702
+ const { timings, failed } = await this.#runGenerators(generatorPlugins, flushPending);
1703
+ await flushPending();
1704
+ for (const [name, duration] of timings) pluginTimings.set(name, duration);
1705
+ for (const entry of failed) failedPlugins.add(entry);
1706
+ } else for (const { plugin, hrStart } of generatorPlugins) {
1707
+ const duration = getElapsedMs(hrStart);
1708
+ pluginTimings.set(plugin.name, duration);
1709
+ await this.#emitPluginEnd({
1710
+ plugin,
1711
+ duration,
1712
+ success: true
1713
+ });
1714
+ }
1715
+ await hooks.emit("kubb:plugins:end", Object.assign({ config }, this.#filesPayload()));
1716
+ await flushPending();
1717
+ const files = this.fileManager.files;
1718
+ await hooks.emit("kubb:build:end", {
1719
+ files,
1720
+ config,
1721
+ outputDir: resolve(config.root, config.output.path)
1722
+ });
1723
+ return {
1724
+ failedPlugins,
1725
+ pluginTimings
1726
+ };
1727
+ } catch (caughtError) {
1728
+ return {
1729
+ failedPlugins,
1730
+ pluginTimings,
1731
+ error: caughtError
1732
+ };
1733
+ } finally {
1734
+ this.fileManager.setOnUpsert(null);
1735
+ }
1736
+ }
1737
+ #filesPayload() {
1738
+ const driver = this;
1739
+ return {
1740
+ get files() {
1741
+ return driver.fileManager.files;
1742
+ },
1743
+ upsertFile: (...files) => driver.fileManager.upsert(...files)
1744
+ };
1745
+ }
1746
+ #emitPluginEnd({ plugin, duration, success, error }) {
1747
+ return this.hooks.emit("kubb:plugin:end", Object.assign({
1748
+ plugin,
1749
+ duration,
1750
+ success,
1751
+ ...error ? { error } : {},
1752
+ config: this.config
1753
+ }, this.#filesPayload()));
1754
+ }
1755
+ async #runGenerators(entries, flushPending) {
1756
+ const timings = /* @__PURE__ */ new Map();
1757
+ const failed = /* @__PURE__ */ new Set();
1758
+ const driver = this;
1759
+ const { schemas, operations } = this.inputNode;
1760
+ const states = entries.map(({ plugin, context, hrStart }) => {
1761
+ const { exclude, include, override } = plugin.options;
1762
+ const hasExclude = Array.isArray(exclude) && exclude.length > 0;
1763
+ const hasInclude = Array.isArray(include) && include.length > 0;
1764
+ const hasOverride = Array.isArray(override) && override.length > 0;
1765
+ return {
1766
+ plugin,
1767
+ generatorContext: {
1768
+ ...context,
1769
+ resolver: this.getResolver(plugin.name)
1770
+ },
1771
+ generators: plugin.generators ?? [],
1772
+ hrStart,
1773
+ failed: false,
1774
+ error: null,
1775
+ optionsAreStatic: !hasExclude && !hasInclude && !hasOverride,
1776
+ allowedSchemaNames: null
1777
+ };
1778
+ });
1779
+ const emitsSchemaHook = this.hooks.listenerCount("kubb:generate:schema") > 0;
1780
+ const emitsOperationHook = this.hooks.listenerCount("kubb:generate:operation") > 0;
1781
+ const pruningStates = states.filter(({ plugin }) => {
1782
+ const { include } = plugin.options;
1783
+ return (include?.some(({ type }) => OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false);
1784
+ });
1785
+ if (pruningStates.length > 0) {
1786
+ const allSchemas = [];
1787
+ for await (const schema of schemas) allSchemas.push(schema);
1788
+ const includedOpsByState = new Map(pruningStates.map((s) => [s, []]));
1789
+ for await (const operation of operations) for (const state of pruningStates) {
1790
+ const { exclude, include, override } = state.plugin.options;
1791
+ if (state.generatorContext.resolver.resolveOptions(operation, {
1792
+ options: state.plugin.options,
1793
+ exclude,
1794
+ include,
1795
+ override
1796
+ }) !== null) includedOpsByState.get(state)?.push(operation);
1797
+ }
1798
+ for (const state of pruningStates) {
1799
+ state.allowedSchemaNames = collectUsedSchemaNames(includedOpsByState.get(state) ?? [], allSchemas);
1800
+ includedOpsByState.delete(state);
1801
+ }
1802
+ }
1803
+ const resolveRendererFor = (gen, state) => gen.renderer === null ? void 0 : gen.renderer ?? state.plugin.renderer ?? state.generatorContext.config.renderer;
1804
+ const dispatchSchema = async (state, node) => {
1805
+ if (state.failed) return;
1806
+ try {
1807
+ const { plugin, generatorContext, generators } = state;
1808
+ const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node;
1809
+ if (state.allowedSchemaNames !== null && transformedNode.name && !state.allowedSchemaNames.has(transformedNode.name)) return;
1810
+ const { exclude, include, override } = plugin.options;
1811
+ const options = state.optionsAreStatic ? plugin.options : generatorContext.resolver.resolveOptions(transformedNode, {
1812
+ options: plugin.options,
1813
+ exclude,
1814
+ include,
1815
+ override
1816
+ });
1817
+ if (options === null) return;
1818
+ const ctx = {
1819
+ ...generatorContext,
1820
+ options
1821
+ };
1822
+ for (const gen of generators) {
1823
+ if (!gen.schema) continue;
1824
+ const raw = gen.schema(transformedNode, ctx);
1825
+ const applied = applyHookResult({
1826
+ result: isPromise(raw) ? await raw : raw,
1827
+ driver,
1828
+ rendererFactory: resolveRendererFor(gen, state)
1829
+ });
1830
+ if (isPromise(applied)) await applied;
1831
+ }
1832
+ if (emitsSchemaHook) await this.hooks.emit("kubb:generate:schema", transformedNode, ctx);
1833
+ } catch (caughtError) {
1834
+ state.failed = true;
1835
+ state.error = caughtError;
1836
+ }
1837
+ };
1838
+ const dispatchOperation = async (state, node) => {
1839
+ if (state.failed) return;
1840
+ try {
1841
+ const { plugin, generatorContext, generators } = state;
1842
+ const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node;
1843
+ const { exclude, include, override } = plugin.options;
1844
+ const options = state.optionsAreStatic ? plugin.options : generatorContext.resolver.resolveOptions(transformedNode, {
1845
+ options: plugin.options,
1846
+ exclude,
1847
+ include,
1848
+ override
1849
+ });
1850
+ if (options === null) return;
1851
+ const ctx = {
1852
+ ...generatorContext,
1853
+ options
1854
+ };
1855
+ for (const gen of generators) {
1856
+ if (!gen.operation) continue;
1857
+ const raw = gen.operation(transformedNode, ctx);
1858
+ const applied = applyHookResult({
1859
+ result: isPromise(raw) ? await raw : raw,
1860
+ driver,
1861
+ rendererFactory: resolveRendererFor(gen, state)
1862
+ });
1863
+ if (isPromise(applied)) await applied;
1864
+ }
1865
+ if (emitsOperationHook) await this.hooks.emit("kubb:generate:operation", transformedNode, ctx);
1866
+ } catch (caughtError) {
1867
+ state.failed = true;
1868
+ state.error = caughtError;
1869
+ }
1870
+ };
1871
+ const needsCollectedOperations = this.hooks.listenerCount("kubb:generate:operations") > 0 || states.some((s) => s.generators.some((g) => !!g.operations));
1872
+ const collectedOperations = needsCollectedOperations ? [] : void 0;
1873
+ await forBatches(schemas, (nodes) => Promise.all(nodes.flatMap((n) => states.map((state) => dispatchSchema(state, n)))), {
1874
+ concurrency: 8,
1875
+ flush: flushPending
1876
+ });
1877
+ await forBatches(operations, (nodes) => {
1878
+ if (needsCollectedOperations) collectedOperations.push(...nodes);
1879
+ return Promise.all(nodes.flatMap((n) => states.map((state) => dispatchOperation(state, n))));
1880
+ }, {
1881
+ concurrency: 8,
1882
+ flush: flushPending
1883
+ });
1884
+ for (const state of states) {
1885
+ if (!state.failed && needsCollectedOperations) try {
1886
+ const { plugin, generatorContext, generators } = state;
1887
+ const ctx = {
1888
+ ...generatorContext,
1889
+ options: plugin.options
1890
+ };
1891
+ const pluginOperations = state.optionsAreStatic ? collectedOperations : collectedOperations.filter((node) => {
1892
+ const transformed = plugin.transformer ? transform(node, plugin.transformer) : node;
1893
+ const { exclude, include, override } = plugin.options;
1894
+ return generatorContext.resolver.resolveOptions(transformed, {
1895
+ options: plugin.options,
1896
+ exclude,
1897
+ include,
1898
+ override
1899
+ }) !== null;
1900
+ });
1901
+ for (const gen of generators) {
1902
+ if (!gen.operations) continue;
1903
+ await applyHookResult({
1904
+ result: await gen.operations(pluginOperations, ctx),
1905
+ driver,
1906
+ rendererFactory: resolveRendererFor(gen, state)
1907
+ });
1908
+ }
1909
+ await this.hooks.emit("kubb:generate:operations", pluginOperations, ctx);
1910
+ } catch (caughtError) {
1911
+ state.failed = true;
1912
+ state.error = caughtError;
1913
+ }
1914
+ const duration = getElapsedMs(state.hrStart);
1915
+ timings.set(state.plugin.name, duration);
1916
+ await this.#emitPluginEnd({
1917
+ plugin: state.plugin,
1918
+ duration,
1919
+ success: !state.failed,
1920
+ error: state.failed && state.error ? state.error : void 0
1921
+ });
1922
+ if (state.failed && state.error) failed.add({
1923
+ plugin: state.plugin,
1924
+ error: state.error
1925
+ });
1926
+ await this.hooks.emit("kubb:debug", {
1927
+ date: /* @__PURE__ */ new Date(),
1928
+ logs: [state.failed ? "✗ Plugin start failed" : `✓ Plugin started successfully (${formatMs(duration)})`]
1929
+ });
1930
+ }
1931
+ return {
1932
+ timings,
1933
+ failed
1934
+ };
1935
+ }
1936
+ /**
1305
1937
  * Unregisters all plugin lifecycle listeners from the shared event emitter.
1306
1938
  * Called at the end of a build to prevent listener leaks across repeated builds.
1307
1939
  *
@@ -1314,11 +1946,12 @@ var KubbDriver = class KubbDriver {
1314
1946
  this.#resolvers.clear();
1315
1947
  this.#defaultResolvers.clear();
1316
1948
  this.fileManager.dispose();
1317
- this.inputNode = void 0;
1949
+ this.#fileProcessor.dispose();
1950
+ this.inputNode = null;
1318
1951
  this.#studio = {
1319
- source: void 0,
1952
+ source: null,
1320
1953
  isOpen: false,
1321
- inputNode: void 0
1954
+ inputNode: null
1322
1955
  };
1323
1956
  for (const [event, handler] of this.#middlewareListeners) this.hooks.off(event, handler);
1324
1957
  }
@@ -1438,10 +2071,15 @@ function applyHookResult({ result, driver, rendererFactory }) {
1438
2071
  }
1439
2072
  if (!rendererFactory) return;
1440
2073
  const renderer = rendererFactory();
1441
- if (renderer.stream) {
1442
- for (const file of renderer.stream(result)) driver.fileManager.upsert(file);
1443
- renderer.unmount();
2074
+ if (renderer.stream) try {
2075
+ var _usingCtx$1 = _usingCtx();
2076
+ const r = _usingCtx$1.u(renderer);
2077
+ for (const file of r.stream(result)) driver.fileManager.upsert(file);
1444
2078
  return;
2079
+ } catch (_) {
2080
+ _usingCtx$1.e = _;
2081
+ } finally {
2082
+ _usingCtx$1.d();
1445
2083
  }
1446
2084
  return applyAsyncRender({
1447
2085
  renderer,
@@ -1450,9 +2088,16 @@ function applyHookResult({ result, driver, rendererFactory }) {
1450
2088
  });
1451
2089
  }
1452
2090
  async function applyAsyncRender({ renderer, result, driver }) {
1453
- await renderer.render(result);
1454
- driver.fileManager.upsert(...renderer.files);
1455
- renderer.unmount();
2091
+ try {
2092
+ var _usingCtx3 = _usingCtx();
2093
+ const r = _usingCtx3.u(renderer);
2094
+ await r.render(result);
2095
+ driver.fileManager.upsert(...r.files);
2096
+ } catch (_) {
2097
+ _usingCtx3.e = _;
2098
+ } finally {
2099
+ _usingCtx3.d();
2100
+ }
1456
2101
  }
1457
2102
  function inputToAdapterSource(config) {
1458
2103
  const input = config.input;
@@ -1471,6 +2116,6 @@ function inputToAdapterSource(config) {
1471
2116
  };
1472
2117
  }
1473
2118
  //#endregion
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 };
2119
+ export { FileManager as a, DEFAULT_BANNER as c, logLevel as d, URLPath as f, FileProcessor as i, DEFAULT_EXTENSION as l, BuildError as m, applyHookResult as n, defineResolver as o, AsyncEventEmitter as p, _usingCtx as r, definePlugin as s, KubbDriver as t, DEFAULT_STUDIO_URL as u };
1475
2120
 
1476
- //# sourceMappingURL=KubbDriver-Cxii_rBp.js.map
2121
+ //# sourceMappingURL=KubbDriver-l31wllgN.js.map