@kubb/core 5.0.0-alpha.3 → 5.0.0-alpha.30

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 (54) hide show
  1. package/dist/PluginDriver-D110FoJ-.d.ts +1632 -0
  2. package/dist/hooks.cjs +12 -27
  3. package/dist/hooks.cjs.map +1 -1
  4. package/dist/hooks.d.ts +11 -36
  5. package/dist/hooks.js +13 -27
  6. package/dist/hooks.js.map +1 -1
  7. package/dist/index.cjs +1410 -823
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +597 -95
  10. package/dist/index.js +1391 -818
  11. package/dist/index.js.map +1 -1
  12. package/package.json +7 -7
  13. package/src/Kubb.ts +40 -58
  14. package/src/{PluginManager.ts → PluginDriver.ts} +165 -177
  15. package/src/build.ts +167 -44
  16. package/src/config.ts +9 -8
  17. package/src/constants.ts +40 -7
  18. package/src/createAdapter.ts +25 -0
  19. package/src/createPlugin.ts +30 -0
  20. package/src/createStorage.ts +58 -0
  21. package/src/defineGenerator.ts +126 -0
  22. package/src/defineLogger.ts +13 -3
  23. package/src/definePresets.ts +16 -0
  24. package/src/defineResolver.ts +457 -0
  25. package/src/hooks/index.ts +1 -6
  26. package/src/hooks/useDriver.ts +11 -0
  27. package/src/hooks/useMode.ts +5 -5
  28. package/src/hooks/usePlugin.ts +3 -3
  29. package/src/index.ts +18 -7
  30. package/src/renderNode.tsx +25 -0
  31. package/src/storages/fsStorage.ts +2 -2
  32. package/src/storages/memoryStorage.ts +2 -2
  33. package/src/types.ts +589 -52
  34. package/src/utils/FunctionParams.ts +2 -2
  35. package/src/utils/TreeNode.ts +45 -7
  36. package/src/utils/diagnostics.ts +4 -1
  37. package/src/utils/executeStrategies.ts +29 -10
  38. package/src/utils/formatters.ts +10 -21
  39. package/src/utils/getBarrelFiles.ts +83 -10
  40. package/src/utils/getConfigs.ts +8 -22
  41. package/src/utils/getPreset.ts +78 -0
  42. package/src/utils/linters.ts +23 -3
  43. package/src/utils/packageJSON.ts +76 -0
  44. package/dist/types-CiPWLv-5.d.ts +0 -1001
  45. package/src/BarrelManager.ts +0 -74
  46. package/src/PackageManager.ts +0 -180
  47. package/src/PromiseManager.ts +0 -40
  48. package/src/defineAdapter.ts +0 -22
  49. package/src/definePlugin.ts +0 -12
  50. package/src/defineStorage.ts +0 -56
  51. package/src/errors.ts +0 -1
  52. package/src/hooks/useKubb.ts +0 -22
  53. package/src/hooks/usePluginManager.ts +0 -11
  54. package/src/utils/getPlugins.ts +0 -23
package/dist/index.js CHANGED
@@ -1,29 +1,29 @@
1
1
  import "./chunk--u3MIqq1.js";
2
- import mod from "node:module";
3
2
  import { EventEmitter } from "node:events";
4
- import { parseArgs, styleText } from "node:util";
5
3
  import { readFileSync } from "node:fs";
6
4
  import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
7
5
  import path, { basename, dirname, extname, join, posix, relative, resolve } from "node:path";
8
- import { definePrinter } from "@kubb/ast";
9
- import { createFabric } from "@kubb/react-fabric";
6
+ import { composeTransformers, composeTransformers as composeTransformers$1, definePrinter, isOperationNode, isSchemaNode, transform, walk } from "@kubb/ast";
7
+ import { Fabric, createFabric, createReactFabric } from "@kubb/react-fabric";
10
8
  import { typescriptParser } from "@kubb/react-fabric/parsers";
11
9
  import { fsPlugin } from "@kubb/react-fabric/plugins";
12
10
  import { performance } from "node:perf_hooks";
13
11
  import { deflateSync } from "fflate";
14
12
  import { x } from "tinyexec";
13
+ import { jsx } from "@kubb/react-fabric/jsx-runtime";
15
14
  import { version } from "node:process";
16
- import os from "node:os";
17
- import { pathToFileURL } from "node:url";
15
+ import { sortBy } from "remeda";
18
16
  import * as pkg from "empathic/package";
19
17
  import { coerce, satisfies } from "semver";
20
- import { sortBy } from "remeda";
21
- //#region ../../internals/utils/dist/index.js
22
- /** Thrown when a plugin's configuration or input fails validation. */
23
- var ValidationPluginError = class extends Error {};
18
+ //#region ../../internals/utils/src/errors.ts
24
19
  /**
25
20
  * Thrown when one or more errors occur during a Kubb build.
26
21
  * Carries the full list of underlying errors on `errors`.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * throw new BuildError('Build failed', { errors: [err1, err2] })
26
+ * ```
27
27
  */
28
28
  var BuildError = class extends Error {
29
29
  errors;
@@ -35,19 +35,34 @@ var BuildError = class extends Error {
35
35
  };
36
36
  /**
37
37
  * Coerces an unknown thrown value to an `Error` instance.
38
- * When the value is already an `Error` it is returned as-is;
39
- * otherwise a new `Error` is created whose message is `String(value)`.
38
+ * Returns the value as-is when it is already an `Error`; otherwise wraps it with `String(value)`.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * try { ... } catch(err) {
43
+ * throw new BuildError('Build failed', { cause: toError(err), errors: [] })
44
+ * }
45
+ * ```
40
46
  */
41
47
  function toError(value) {
42
48
  return value instanceof Error ? value : new Error(String(value));
43
49
  }
50
+ //#endregion
51
+ //#region ../../internals/utils/src/asyncEventEmitter.ts
44
52
  /**
45
- * A typed EventEmitter that awaits all async listeners before resolving.
53
+ * Typed `EventEmitter` that awaits all async listeners before resolving.
46
54
  * Wraps Node's `EventEmitter` with full TypeScript event-map inference.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const emitter = new AsyncEventEmitter<{ build: [name: string] }>()
59
+ * emitter.on('build', async (name) => { console.log(name) })
60
+ * await emitter.emit('build', 'petstore') // all listeners awaited
61
+ * ```
47
62
  */
48
63
  var AsyncEventEmitter = class {
49
64
  /**
50
- * `maxListener` controls the maximum number of listeners per event before Node emits a memory-leak warning.
65
+ * Maximum number of listeners per event before Node emits a memory-leak warning.
51
66
  * @default 10
52
67
  */
53
68
  constructor(maxListener = 10) {
@@ -55,31 +70,48 @@ var AsyncEventEmitter = class {
55
70
  }
56
71
  #emitter = new EventEmitter();
57
72
  /**
58
- * Emits an event and awaits all registered listeners in parallel.
73
+ * Emits `eventName` and awaits all registered listeners sequentially.
59
74
  * Throws if any listener rejects, wrapping the cause with the event name and serialized arguments.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * await emitter.emit('build', 'petstore')
79
+ * ```
60
80
  */
61
81
  async emit(eventName, ...eventArgs) {
62
82
  const listeners = this.#emitter.listeners(eventName);
63
83
  if (listeners.length === 0) return;
64
- await Promise.all(listeners.map(async (listener) => {
84
+ for (const listener of listeners) try {
85
+ await listener(...eventArgs);
86
+ } catch (err) {
87
+ let serializedArgs;
65
88
  try {
66
- return await listener(...eventArgs);
67
- } catch (err) {
68
- let serializedArgs;
69
- try {
70
- serializedArgs = JSON.stringify(eventArgs);
71
- } catch {
72
- serializedArgs = String(eventArgs);
73
- }
74
- throw new Error(`Error in async listener for "${eventName}" with eventArgs ${serializedArgs}`, { cause: toError(err) });
89
+ serializedArgs = JSON.stringify(eventArgs);
90
+ } catch {
91
+ serializedArgs = String(eventArgs);
75
92
  }
76
- }));
93
+ throw new Error(`Error in async listener for "${eventName}" with eventArgs ${serializedArgs}`, { cause: toError(err) });
94
+ }
77
95
  }
78
- /** Registers a persistent listener for the given event. */
96
+ /**
97
+ * Registers a persistent listener for `eventName`.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * emitter.on('build', async (name) => { console.log(name) })
102
+ * ```
103
+ */
79
104
  on(eventName, handler) {
80
105
  this.#emitter.on(eventName, handler);
81
106
  }
82
- /** Registers a one-shot listener that removes itself after the first invocation. */
107
+ /**
108
+ * Registers a one-shot listener that removes itself after the first invocation.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * emitter.onOnce('build', async (name) => { console.log(name) })
113
+ * ```
114
+ */
83
115
  onOnce(eventName, handler) {
84
116
  const wrapper = (...args) => {
85
117
  this.off(eventName, wrapper);
@@ -87,15 +119,31 @@ var AsyncEventEmitter = class {
87
119
  };
88
120
  this.on(eventName, wrapper);
89
121
  }
90
- /** Removes a previously registered listener. */
122
+ /**
123
+ * Removes a previously registered listener.
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * emitter.off('build', handler)
128
+ * ```
129
+ */
91
130
  off(eventName, handler) {
92
131
  this.#emitter.off(eventName, handler);
93
132
  }
94
- /** Removes all listeners from every event channel. */
133
+ /**
134
+ * Removes all listeners from every event channel.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * emitter.removeAll()
139
+ * ```
140
+ */
95
141
  removeAll() {
96
142
  this.#emitter.removeAllListeners();
97
143
  }
98
144
  };
145
+ //#endregion
146
+ //#region ../../internals/utils/src/casing.ts
99
147
  /**
100
148
  * Shared implementation for camelCase and PascalCase conversion.
101
149
  * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
@@ -114,9 +162,12 @@ function toCamelOrPascal(text, pascal) {
114
162
  * Splits `text` on `.` and applies `transformPart` to each segment.
115
163
  * The last segment receives `isLast = true`, all earlier segments receive `false`.
116
164
  * Segments are joined with `/` to form a file path.
165
+ *
166
+ * Only splits on dots followed by a letter so that version numbers
167
+ * embedded in operationIds (e.g. `v2025.0`) are kept intact.
117
168
  */
118
169
  function applyToFileParts(text, transformPart) {
119
- const parts = text.split(".");
170
+ const parts = text.split(/\.(?=[a-zA-Z])/);
120
171
  return parts.map((part, i) => transformPart(part, i === parts.length - 1)).join("/");
121
172
  }
122
173
  /**
@@ -134,190 +185,33 @@ function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
134
185
  } : {}));
135
186
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
136
187
  }
137
- /** Returns a `CLIAdapter` with type inference. Pass a different adapter to `createCLI` to swap the CLI engine. */
138
- function defineCLIAdapter(adapter) {
139
- return adapter;
140
- }
141
188
  /**
142
- * Serializes `CommandDefinition[]` to a plain, JSON-serializable structure.
143
- * Use to expose CLI capabilities to AI agents or MCP tools.
189
+ * Converts `text` to PascalCase.
190
+ * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
191
+ *
192
+ * @example
193
+ * pascalCase('hello-world') // 'HelloWorld'
194
+ * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
144
195
  */
145
- function getCommandSchema(defs) {
146
- return defs.map(serializeCommand);
147
- }
148
- function serializeCommand(def) {
149
- return {
150
- name: def.name,
151
- description: def.description,
152
- arguments: def.arguments,
153
- options: serializeOptions(def.options ?? {}),
154
- subCommands: def.subCommands ? def.subCommands.map(serializeCommand) : []
155
- };
156
- }
157
- function serializeOptions(options) {
158
- return Object.entries(options).map(([name, opt]) => {
159
- return {
160
- name,
161
- flags: `${opt.short ? `-${opt.short}, ` : ""}--${name}${opt.type === "string" ? ` <${opt.hint ?? name}>` : ""}`,
162
- type: opt.type,
163
- description: opt.description,
164
- ...opt.default !== void 0 ? { default: opt.default } : {},
165
- ...opt.hint ? { hint: opt.hint } : {},
166
- ...opt.enum ? { enum: opt.enum } : {},
167
- ...opt.required ? { required: opt.required } : {}
168
- };
169
- });
170
- }
171
- /** Prints formatted help output for a command using its `CommandDefinition`. */
172
- function renderHelp(def, parentName) {
173
- const schema = getCommandSchema([def])[0];
174
- const programName = parentName ? `${parentName} ${schema.name}` : schema.name;
175
- const argsPart = schema.arguments?.length ? ` ${schema.arguments.join(" ")}` : "";
176
- const subCmdPart = schema.subCommands.length ? " <command>" : "";
177
- console.log(`\n${styleText("bold", "Usage:")} ${programName}${argsPart}${subCmdPart} [options]\n`);
178
- if (schema.description) console.log(` ${schema.description}\n`);
179
- if (schema.subCommands.length) {
180
- console.log(styleText("bold", "Commands:"));
181
- for (const sub of schema.subCommands) console.log(` ${styleText("cyan", sub.name.padEnd(16))}${sub.description}`);
182
- console.log();
183
- }
184
- const options = [...schema.options, {
185
- name: "help",
186
- flags: "-h, --help",
187
- type: "boolean",
188
- description: "Show help"
189
- }];
190
- console.log(styleText("bold", "Options:"));
191
- for (const opt of options) {
192
- const flags = styleText("cyan", opt.flags.padEnd(30));
193
- const defaultPart = opt.default !== void 0 ? styleText("dim", ` (default: ${opt.default})`) : "";
194
- console.log(` ${flags}${opt.description}${defaultPart}`);
195
- }
196
- console.log();
197
- }
198
- function buildParseOptions(def) {
199
- const result = { help: {
200
- type: "boolean",
201
- short: "h"
202
- } };
203
- for (const [name, opt] of Object.entries(def.options ?? {})) result[name] = {
204
- type: opt.type,
205
- ...opt.short ? { short: opt.short } : {},
206
- ...opt.default !== void 0 ? { default: opt.default } : {}
207
- };
208
- return result;
196
+ function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
197
+ if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
198
+ prefix,
199
+ suffix
200
+ }) : camelCase(part));
201
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
209
202
  }
210
- async function runCommand(def, argv, parentName) {
211
- const parseOptions = buildParseOptions(def);
212
- let parsed;
213
- try {
214
- const result = parseArgs({
215
- args: argv,
216
- options: parseOptions,
217
- allowPositionals: true,
218
- strict: false
219
- });
220
- parsed = {
221
- values: result.values,
222
- positionals: result.positionals
223
- };
224
- } catch {
225
- renderHelp(def, parentName);
226
- process.exit(1);
227
- }
228
- if (parsed.values["help"]) {
229
- renderHelp(def, parentName);
230
- process.exit(0);
231
- }
232
- for (const [name, opt] of Object.entries(def.options ?? {})) if (opt.required && parsed.values[name] === void 0) {
233
- console.error(styleText("red", `Error: --${name} is required`));
234
- renderHelp(def, parentName);
235
- process.exit(1);
236
- }
237
- if (!def.run) {
238
- renderHelp(def, parentName);
239
- process.exit(0);
240
- }
241
- try {
242
- await def.run(parsed);
243
- } catch (err) {
244
- console.error(styleText("red", `Error: ${err instanceof Error ? err.message : String(err)}`));
245
- renderHelp(def, parentName);
246
- process.exit(1);
247
- }
248
- }
249
- function printRootHelp(programName, version, defs) {
250
- console.log(`\n${styleText("bold", "Usage:")} ${programName} <command> [options]\n`);
251
- console.log(` Kubb generation — v${version}\n`);
252
- console.log(styleText("bold", "Commands:"));
253
- for (const def of defs) console.log(` ${styleText("cyan", def.name.padEnd(16))}${def.description}`);
254
- console.log();
255
- console.log(styleText("bold", "Options:"));
256
- console.log(` ${styleText("cyan", "-v, --version".padEnd(30))}Show version number`);
257
- console.log(` ${styleText("cyan", "-h, --help".padEnd(30))}Show help`);
258
- console.log();
259
- console.log(`Run ${styleText("cyan", `${programName} <command> --help`)} for command-specific help.\n`);
260
- }
261
- defineCLIAdapter({
262
- renderHelp(def, parentName) {
263
- renderHelp(def, parentName);
264
- },
265
- async run(defs, argv, opts) {
266
- const { programName, defaultCommandName, version } = opts;
267
- const args = argv.length >= 2 && argv[0]?.includes("node") ? argv.slice(2) : argv;
268
- if (args[0] === "--version" || args[0] === "-v") {
269
- console.log(version);
270
- process.exit(0);
271
- }
272
- if (args[0] === "--help" || args[0] === "-h") {
273
- printRootHelp(programName, version, defs);
274
- process.exit(0);
275
- }
276
- if (args.length === 0) {
277
- const defaultDef = defs.find((d) => d.name === defaultCommandName);
278
- if (defaultDef?.run) await runCommand(defaultDef, [], programName);
279
- else printRootHelp(programName, version, defs);
280
- return;
281
- }
282
- const [first, ...rest] = args;
283
- const isKnownSubcommand = defs.some((d) => d.name === first);
284
- let def;
285
- let commandArgv;
286
- let parentName;
287
- if (isKnownSubcommand) {
288
- def = defs.find((d) => d.name === first);
289
- commandArgv = rest;
290
- parentName = programName;
291
- } else {
292
- def = defs.find((d) => d.name === defaultCommandName);
293
- commandArgv = args;
294
- parentName = programName;
295
- }
296
- if (!def) {
297
- console.error(`Unknown command: ${first}`);
298
- printRootHelp(programName, version, defs);
299
- process.exit(1);
300
- }
301
- if (def.subCommands?.length) {
302
- const [subName, ...subRest] = commandArgv;
303
- const subDef = def.subCommands.find((s) => s.name === subName);
304
- if (subName === "--help" || subName === "-h") {
305
- renderHelp(def, parentName);
306
- process.exit(0);
307
- }
308
- if (!subDef) {
309
- renderHelp(def, parentName);
310
- process.exit(subName ? 1 : 0);
311
- }
312
- await runCommand(subDef, subRest, `${parentName} ${def.name}`);
313
- return;
314
- }
315
- await runCommand(def, commandArgv, parentName);
316
- }
317
- });
203
+ //#endregion
204
+ //#region ../../internals/utils/src/time.ts
318
205
  /**
319
- * Calculates elapsed time in milliseconds from a high-resolution start time.
320
- * Rounds to 2 decimal places to provide sub-millisecond precision without noise.
206
+ * Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.
207
+ * Rounds to 2 decimal places for sub-millisecond precision without noise.
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * const start = process.hrtime()
212
+ * doWork()
213
+ * getElapsedMs(start) // 42.35
214
+ * ```
321
215
  */
322
216
  function getElapsedMs(hrStart) {
323
217
  const [seconds, nanoseconds] = process.hrtime(hrStart);
@@ -325,39 +219,22 @@ function getElapsedMs(hrStart) {
325
219
  return Math.round(ms * 100) / 100;
326
220
  }
327
221
  /**
328
- * Converts a millisecond duration into a human-readable string.
329
- * Adjusts units (ms, s, m s) based on the magnitude of the duration.
222
+ * Converts a millisecond duration into a human-readable string (`ms`, `s`, or `m s`).
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * formatMs(250) // '250ms'
227
+ * formatMs(1500) // '1.50s'
228
+ * formatMs(90000) // '1m 30.0s'
229
+ * ```
330
230
  */
331
231
  function formatMs(ms) {
332
232
  if (ms >= 6e4) return `${Math.floor(ms / 6e4)}m ${(ms % 6e4 / 1e3).toFixed(1)}s`;
333
233
  if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
334
234
  return `${Math.round(ms)}ms`;
335
235
  }
336
- /**
337
- * Parses a CSS hex color string (`#RGB`) into its RGB channels.
338
- * Falls back to `255` for any channel that cannot be parsed.
339
- */
340
- function parseHex(color) {
341
- const int = Number.parseInt(color.replace("#", ""), 16);
342
- return Number.isNaN(int) ? {
343
- r: 255,
344
- g: 255,
345
- b: 255
346
- } : {
347
- r: int >> 16 & 255,
348
- g: int >> 8 & 255,
349
- b: int & 255
350
- };
351
- }
352
- /**
353
- * Returns a function that wraps a string in a 24-bit ANSI true-color escape sequence
354
- * for the given hex color.
355
- */
356
- function hex(color) {
357
- const { r, g, b } = parseHex(color);
358
- return (text) => `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
359
- }
360
- hex("#F55A17"), hex("#F5A217"), hex("#F58517"), hex("#B45309"), hex("#FFFFFF"), hex("#adadc6"), hex("#FDA4AF");
236
+ //#endregion
237
+ //#region ../../internals/utils/src/fs.ts
361
238
  /**
362
239
  * Converts all backslashes to forward slashes.
363
240
  * Extended-length Windows paths (`\\?\...`) are left unchanged.
@@ -367,8 +244,14 @@ function toSlash(p) {
367
244
  return p.replaceAll("\\", "/");
368
245
  }
369
246
  /**
370
- * Returns the relative path from `rootDir` to `filePath`, always using
371
- * forward slashes and prefixed with `./` when not already traversing upward.
247
+ * Returns the relative path from `rootDir` to `filePath`, always using forward slashes
248
+ * and prefixed with `./` when not already traversing upward.
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * getRelativePath('/src/components', '/src/components/Button.tsx') // './Button.tsx'
253
+ * getRelativePath('/src/components', '/src/utils/helpers.ts') // '../utils/helpers.ts'
254
+ * ```
372
255
  */
373
256
  function getRelativePath(rootDir, filePath) {
374
257
  if (!rootDir || !filePath) throw new Error(`Root and file should be filled in when retrieving the relativePath, ${rootDir || ""} ${filePath || ""}`);
@@ -378,43 +261,54 @@ function getRelativePath(rootDir, filePath) {
378
261
  /**
379
262
  * Resolves to `true` when the file or directory at `path` exists.
380
263
  * Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
264
+ *
265
+ * @example
266
+ * ```ts
267
+ * if (await exists('./kubb.config.ts')) {
268
+ * const content = await read('./kubb.config.ts')
269
+ * }
270
+ * ```
381
271
  */
382
272
  async function exists(path) {
383
273
  if (typeof Bun !== "undefined") return Bun.file(path).exists();
384
274
  return access(path).then(() => true, () => false);
385
275
  }
386
276
  /**
387
- * Reads the file at `path` as a UTF-8 string.
388
- * Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.
277
+ * Synchronous counterpart of `read`.
278
+ *
279
+ * @example
280
+ * ```ts
281
+ * const source = readSync('./src/Pet.ts')
282
+ * ```
389
283
  */
390
- async function read(path) {
391
- if (typeof Bun !== "undefined") return Bun.file(path).text();
392
- return readFile(path, { encoding: "utf8" });
393
- }
394
- /** Synchronous counterpart of `read`. */
395
284
  function readSync(path) {
396
285
  return readFileSync(path, { encoding: "utf8" });
397
286
  }
398
287
  /**
399
288
  * Writes `data` to `path`, trimming leading/trailing whitespace before saving.
400
- * Skips the write and returns `undefined` when the trimmed content is empty or
401
- * identical to what is already on disk.
289
+ * Skips the write when the trimmed content is empty or identical to what is already on disk.
402
290
  * Creates any missing parent directories automatically.
403
- * When `sanity` is `true`, re-reads the file after writing and throws if the
404
- * content does not match.
291
+ * When `sanity` is `true`, re-reads the file after writing and throws if the content does not match.
292
+ *
293
+ * @example
294
+ * ```ts
295
+ * await write('./src/Pet.ts', source) // writes and returns trimmed content
296
+ * await write('./src/Pet.ts', source) // null — file unchanged
297
+ * await write('./src/Pet.ts', ' ') // null — empty content skipped
298
+ * ```
405
299
  */
406
300
  async function write(path, data, options = {}) {
407
301
  const trimmed = data.trim();
408
- if (trimmed === "") return void 0;
302
+ if (trimmed === "") return null;
409
303
  const resolved = resolve(path);
410
304
  if (typeof Bun !== "undefined") {
411
305
  const file = Bun.file(resolved);
412
- if ((await file.exists() ? await file.text() : null) === trimmed) return void 0;
306
+ if ((await file.exists() ? await file.text() : null) === trimmed) return null;
413
307
  await Bun.write(resolved, trimmed);
414
308
  return trimmed;
415
309
  }
416
310
  try {
417
- if (await readFile(resolved, { encoding: "utf-8" }) === trimmed) return void 0;
311
+ if (await readFile(resolved, { encoding: "utf-8" }) === trimmed) return null;
418
312
  } catch {}
419
313
  await mkdir(dirname(resolved), { recursive: true });
420
314
  await writeFile(resolved, trimmed, { encoding: "utf-8" });
@@ -425,31 +319,40 @@ async function write(path, data, options = {}) {
425
319
  }
426
320
  return trimmed;
427
321
  }
428
- /** Recursively removes `path`. Silently succeeds when `path` does not exist. */
322
+ /**
323
+ * Recursively removes `path`. Silently succeeds when `path` does not exist.
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * await clean('./dist')
328
+ * ```
329
+ */
429
330
  async function clean(path) {
430
331
  return rm(path, {
431
332
  recursive: true,
432
333
  force: true
433
334
  });
434
335
  }
435
- /**
436
- * Registers `originalName` in `data` without altering the returned name.
437
- * Use this when you need to track usage frequency but always emit the original identifier.
336
+ //#endregion
337
+ //#region ../../internals/utils/src/promise.ts
338
+ /** Returns `true` when `result` is a rejected `Promise.allSettled` result with a typed `reason`.
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * const results = await Promise.allSettled([p1, p2])
343
+ * results.filter(isPromiseRejectedResult<Error>).map((r) => r.reason.message)
344
+ * ```
438
345
  */
439
- function setUniqueName(originalName, data) {
440
- let used = data[originalName] || 0;
441
- if (used) {
442
- data[originalName] = ++used;
443
- return originalName;
444
- }
445
- data[originalName] = 1;
446
- return originalName;
346
+ function isPromiseRejectedResult(result) {
347
+ return result.status === "rejected";
447
348
  }
349
+ //#endregion
350
+ //#region ../../internals/utils/src/reserved.ts
448
351
  /**
449
352
  * JavaScript and Java reserved words.
450
353
  * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
451
354
  */
452
- const reservedWords = [
355
+ const reservedWords = new Set([
453
356
  "abstract",
454
357
  "arguments",
455
358
  "boolean",
@@ -531,18 +434,31 @@ const reservedWords = [
531
434
  "toString",
532
435
  "undefined",
533
436
  "valueOf"
534
- ];
437
+ ]);
535
438
  /**
536
- * Prefixes a word with `_` when it is a reserved JavaScript/Java identifier
537
- * or starts with a digit.
439
+ * Prefixes `word` with `_` when it is a reserved JavaScript/Java identifier or starts with a digit.
440
+ *
441
+ * @example
442
+ * ```ts
443
+ * transformReservedWord('class') // '_class'
444
+ * transformReservedWord('42foo') // '_42foo'
445
+ * transformReservedWord('status') // 'status'
446
+ * ```
538
447
  */
539
448
  function transformReservedWord(word) {
540
449
  const firstChar = word.charCodeAt(0);
541
- if (word && (reservedWords.includes(word) || firstChar >= 48 && firstChar <= 57)) return `_${word}`;
450
+ if (word && (reservedWords.has(word) || firstChar >= 48 && firstChar <= 57)) return `_${word}`;
542
451
  return word;
543
452
  }
544
453
  /**
545
454
  * Returns `true` when `name` is a syntactically valid JavaScript variable name.
455
+ *
456
+ * @example
457
+ * ```ts
458
+ * isValidVarName('status') // true
459
+ * isValidVarName('class') // false (reserved word)
460
+ * isValidVarName('42foo') // false (starts with digit)
461
+ * ```
546
462
  */
547
463
  function isValidVarName(name) {
548
464
  try {
@@ -552,6 +468,8 @@ function isValidVarName(name) {
552
468
  }
553
469
  return true;
554
470
  }
471
+ //#endregion
472
+ //#region ../../internals/utils/src/urlPath.ts
555
473
  /**
556
474
  * Parses and transforms an OpenAPI/Swagger path string into various URL formats.
557
475
  *
@@ -561,18 +479,33 @@ function isValidVarName(name) {
561
479
  * p.template // '`/pet/${petId}`'
562
480
  */
563
481
  var URLPath = class {
564
- /** The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`. */
482
+ /**
483
+ * The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
484
+ */
565
485
  path;
566
486
  #options;
567
487
  constructor(path, options = {}) {
568
488
  this.path = path;
569
489
  this.#options = options;
570
490
  }
571
- /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`. */
491
+ /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * new URLPath('/pet/{petId}').URL // '/pet/:petId'
496
+ * ```
497
+ */
572
498
  get URL() {
573
499
  return this.toURLPath();
574
500
  }
575
- /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`). */
501
+ /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
502
+ *
503
+ * @example
504
+ * ```ts
505
+ * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
506
+ * new URLPath('/pet/{petId}').isURL // false
507
+ * ```
508
+ */
576
509
  get isURL() {
577
510
  try {
578
511
  return !!new URL(this.path).href;
@@ -590,11 +523,25 @@ var URLPath = class {
590
523
  get template() {
591
524
  return this.toTemplateString();
592
525
  }
593
- /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set. */
526
+ /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
527
+ *
528
+ * @example
529
+ * ```ts
530
+ * new URLPath('/pet/{petId}').object
531
+ * // { url: '/pet/:petId', params: { petId: 'petId' } }
532
+ * ```
533
+ */
594
534
  get object() {
595
535
  return this.toObject();
596
536
  }
597
- /** Returns a map of path parameter names, or `undefined` when the path has no parameters. */
537
+ /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
538
+ *
539
+ * @example
540
+ * ```ts
541
+ * new URLPath('/pet/{petId}').params // { petId: 'petId' }
542
+ * new URLPath('/pet').params // undefined
543
+ * ```
544
+ */
598
545
  get params() {
599
546
  return this.getParams();
600
547
  }
@@ -602,7 +549,9 @@ var URLPath = class {
602
549
  const param = isValidVarName(raw) ? raw : camelCase(raw);
603
550
  return this.#options.casing === "camelcase" ? camelCase(param) : param;
604
551
  }
605
- /** Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name. */
552
+ /**
553
+ * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
554
+ */
606
555
  #eachParam(fn) {
607
556
  for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
608
557
  const raw = match[1];
@@ -639,6 +588,12 @@ var URLPath = class {
639
588
  * Extracts all `{param}` segments from the path and returns them as a key-value map.
640
589
  * An optional `replacer` transforms each parameter name in both key and value positions.
641
590
  * Returns `undefined` when no path parameters are found.
591
+ *
592
+ * @example
593
+ * ```ts
594
+ * new URLPath('/pet/{petId}/tag/{tagId}').getParams()
595
+ * // { petId: 'petId', tagId: 'tagId' }
596
+ * ```
642
597
  */
643
598
  getParams(replacer) {
644
599
  const params = {};
@@ -648,7 +603,13 @@ var URLPath = class {
648
603
  });
649
604
  return Object.keys(params).length > 0 ? params : void 0;
650
605
  }
651
- /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`. */
606
+ /** Converts the OpenAPI path to Express-style colon syntax.
607
+ *
608
+ * @example
609
+ * ```ts
610
+ * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
611
+ * ```
612
+ */
652
613
  toURLPath() {
653
614
  return this.path.replace(/\{([^}]+)\}/g, ":$1");
654
615
  }
@@ -666,11 +627,27 @@ function isInputPath(config) {
666
627
  }
667
628
  //#endregion
668
629
  //#region src/constants.ts
630
+ /**
631
+ * Base URL for the Kubb Studio web app.
632
+ */
669
633
  const DEFAULT_STUDIO_URL = "https://studio.kubb.dev";
634
+ /**
635
+ * File name used for generated barrel (index) files.
636
+ */
670
637
  const BARREL_FILENAME = "index.ts";
638
+ /**
639
+ * Default banner style written at the top of every generated file.
640
+ */
671
641
  const DEFAULT_BANNER = "simple";
642
+ /**
643
+ * Default file-extension mapping used when no explicit mapping is configured.
644
+ */
672
645
  const DEFAULT_EXTENSION = { ".ts": ".ts" };
673
- const PATH_SEPARATORS = ["/", "\\"];
646
+ /**
647
+ * Numeric log-level thresholds used internally to compare verbosity.
648
+ *
649
+ * Higher numbers are more verbose.
650
+ */
674
651
  const logLevel = {
675
652
  silent: Number.NEGATIVE_INFINITY,
676
653
  error: 0,
@@ -679,6 +656,13 @@ const logLevel = {
679
656
  verbose: 4,
680
657
  debug: 5
681
658
  };
659
+ /**
660
+ * CLI command descriptors for each supported linter.
661
+ *
662
+ * Each entry contains the executable `command`, an `args` factory that maps an
663
+ * output path to the correct argument list, and an `errorMessage` shown when
664
+ * the linter is not found.
665
+ */
682
666
  const linters = {
683
667
  eslint: {
684
668
  command: "eslint",
@@ -700,6 +684,13 @@ const linters = {
700
684
  errorMessage: "Oxlint not found"
701
685
  }
702
686
  };
687
+ /**
688
+ * CLI command descriptors for each supported code formatter.
689
+ *
690
+ * Each entry contains the executable `command`, an `args` factory that maps an
691
+ * output path to the correct argument list, and an `errorMessage` shown when
692
+ * the formatter is not found.
693
+ */
703
694
  const formatters = {
704
695
  prettier: {
705
696
  command: "prettier",
@@ -899,7 +890,12 @@ function validateConcurrency(concurrency) {
899
890
  //#endregion
900
891
  //#region src/utils/executeStrategies.ts
901
892
  /**
902
- * Chains promises
893
+ * Runs promise functions in sequence, threading each result into the next call.
894
+ *
895
+ * - Each function receives the accumulated state from the previous call.
896
+ * - Skips functions that return a falsy value (acts as a no-op for that step).
897
+ * - Returns an array of all individual results.
898
+ * @deprecated
903
899
  */
904
900
  function hookSeq(promises) {
905
901
  return promises.filter(Boolean).reduce((promise, func) => {
@@ -912,7 +908,11 @@ function hookSeq(promises) {
912
908
  }, Promise.resolve([]));
913
909
  }
914
910
  /**
915
- * Chains promises, first non-null result stops and returns
911
+ * Runs promise functions in sequence and returns the first non-null result.
912
+ *
913
+ * - Stops as soon as `nullCheck` passes for a result (default: `!== null`).
914
+ * - Subsequent functions are skipped once a match is found.
915
+ * @deprecated
916
916
  */
917
917
  function hookFirst(promises, nullCheck = (state) => state !== null) {
918
918
  let promise = Promise.resolve(null);
@@ -923,7 +923,11 @@ function hookFirst(promises, nullCheck = (state) => state !== null) {
923
923
  return promise;
924
924
  }
925
925
  /**
926
- * Runs an array of promise functions with optional concurrency limit.
926
+ * Runs promise functions concurrently and returns all settled results.
927
+ *
928
+ * - Limits simultaneous executions to `concurrency` (default: unlimited).
929
+ * - Uses `Promise.allSettled` so individual failures do not cancel other tasks.
930
+ * @deprecated
927
931
  */
928
932
  function hookParallel(promises, concurrency = Number.POSITIVE_INFINITY) {
929
933
  const limit = pLimit(concurrency);
@@ -931,29 +935,22 @@ function hookParallel(promises, concurrency = Number.POSITIVE_INFINITY) {
931
935
  return Promise.allSettled(tasks);
932
936
  }
933
937
  //#endregion
934
- //#region src/PromiseManager.ts
935
- var PromiseManager = class {
936
- #options = {};
937
- constructor(options = {}) {
938
- this.#options = options;
939
- }
940
- run(strategy, promises, { concurrency = Number.POSITIVE_INFINITY } = {}) {
941
- if (strategy === "seq") return hookSeq(promises);
942
- if (strategy === "first") return hookFirst(promises, this.#options.nullCheck);
943
- if (strategy === "parallel") return hookParallel(promises, concurrency);
944
- throw new Error(`${strategy} not implemented`);
945
- }
946
- };
947
- function isPromiseRejectedResult(result) {
948
- return result.status === "rejected";
949
- }
950
- //#endregion
951
- //#region src/PluginManager.ts
938
+ //#region src/PluginDriver.ts
939
+ /**
940
+ * Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise.
941
+ *
942
+ * @example
943
+ * ```ts
944
+ * getMode('src/gen/types.ts') // 'single'
945
+ * getMode('src/gen/types') // 'split'
946
+ * ```
947
+ */
952
948
  function getMode(fileOrFolder) {
953
949
  if (!fileOrFolder) return "split";
954
950
  return extname(fileOrFolder) ? "single" : "split";
955
951
  }
956
- var PluginManager = class {
952
+ const hookFirstNullCheck = (state) => !!state?.result;
953
+ var PluginDriver = class {
957
954
  config;
958
955
  options;
959
956
  /**
@@ -963,31 +960,43 @@ var PluginManager = class {
963
960
  rootNode = void 0;
964
961
  adapter = void 0;
965
962
  #studioIsOpen = false;
966
- #plugins = /* @__PURE__ */ new Set();
967
- #usedPluginNames = {};
968
- #promiseManager;
963
+ plugins = /* @__PURE__ */ new Map();
969
964
  constructor(config, options) {
970
965
  this.config = config;
971
966
  this.options = options;
972
- this.#promiseManager = new PromiseManager({ nullCheck: (state) => !!state?.result });
973
- [...config.plugins || []].forEach((plugin) => {
974
- const parsedPlugin = this.#parse(plugin);
975
- this.#plugins.add(parsedPlugin);
967
+ config.plugins.map((plugin) => Object.assign({
968
+ buildStart() {},
969
+ buildEnd() {}
970
+ }, plugin)).filter((plugin) => {
971
+ if (typeof plugin.apply === "function") return plugin.apply(config);
972
+ return true;
973
+ }).sort((a, b) => {
974
+ if (b.pre?.includes(a.name)) return 1;
975
+ if (b.post?.includes(a.name)) return -1;
976
+ return 0;
977
+ }).forEach((plugin) => {
978
+ this.plugins.set(plugin.name, plugin);
976
979
  });
977
980
  }
978
981
  get events() {
979
982
  return this.options.events;
980
983
  }
981
984
  getContext(plugin) {
982
- const plugins = [...this.#plugins];
983
- const pluginManager = this;
985
+ const driver = this;
984
986
  const baseContext = {
985
- fabric: this.options.fabric,
986
- config: this.config,
987
+ fabric: driver.options.fabric,
988
+ config: driver.config,
989
+ get root() {
990
+ return resolve(driver.config.root, driver.config.output.path);
991
+ },
992
+ getMode(output) {
993
+ return getMode(resolve(driver.config.root, driver.config.output.path, output.path));
994
+ },
995
+ events: driver.options.events,
987
996
  plugin,
988
- events: this.options.events,
989
- pluginManager: this,
990
- mode: getMode(resolve(this.config.root, this.config.output.path)),
997
+ getPlugin: driver.getPlugin.bind(driver),
998
+ requirePlugin: driver.requirePlugin.bind(driver),
999
+ driver,
991
1000
  addFile: async (...files) => {
992
1001
  await this.options.fabric.addFile(...files);
993
1002
  },
@@ -995,23 +1004,38 @@ var PluginManager = class {
995
1004
  await this.options.fabric.upsertFile(...files);
996
1005
  },
997
1006
  get rootNode() {
998
- return pluginManager.rootNode;
1007
+ return driver.rootNode;
999
1008
  },
1000
1009
  get adapter() {
1001
- return pluginManager.adapter;
1010
+ return driver.adapter;
1011
+ },
1012
+ get resolver() {
1013
+ return plugin.resolver;
1014
+ },
1015
+ get transformer() {
1016
+ return plugin.transformer;
1017
+ },
1018
+ warn(message) {
1019
+ driver.events.emit("warn", message);
1020
+ },
1021
+ error(error) {
1022
+ driver.events.emit("error", typeof error === "string" ? new Error(error) : error);
1023
+ },
1024
+ info(message) {
1025
+ driver.events.emit("info", message);
1002
1026
  },
1003
1027
  openInStudio(options) {
1004
- if (!pluginManager.config.devtools || pluginManager.#studioIsOpen) return;
1005
- if (typeof pluginManager.config.devtools !== "object") throw new Error("Devtools must be an object");
1006
- if (!pluginManager.rootNode || !pluginManager.adapter) throw new Error("adapter is not defined, make sure you have set the parser in kubb.config.ts");
1007
- pluginManager.#studioIsOpen = true;
1008
- const studioUrl = pluginManager.config.devtools?.studioUrl ?? "https://studio.kubb.dev";
1009
- return openInStudio(pluginManager.rootNode, studioUrl, options);
1028
+ if (!driver.config.devtools || driver.#studioIsOpen) return;
1029
+ if (typeof driver.config.devtools !== "object") throw new Error("Devtools must be an object");
1030
+ if (!driver.rootNode || !driver.adapter) throw new Error("adapter is not defined, make sure you have set the parser in kubb.config.ts");
1031
+ driver.#studioIsOpen = true;
1032
+ const studioUrl = driver.config.devtools?.studioUrl ?? "https://studio.kubb.dev";
1033
+ return openInStudio(driver.rootNode, studioUrl, options);
1010
1034
  }
1011
1035
  };
1012
1036
  const mergedExtras = {};
1013
- for (const p of plugins) if (typeof p.inject === "function") {
1014
- const result = p.inject.call(baseContext, baseContext);
1037
+ for (const p of this.plugins.values()) if (typeof p.inject === "function") {
1038
+ const result = p.inject.call(baseContext);
1015
1039
  if (result !== null && typeof result === "object") Object.assign(mergedExtras, result);
1016
1040
  }
1017
1041
  return {
@@ -1019,9 +1043,9 @@ var PluginManager = class {
1019
1043
  ...mergedExtras
1020
1044
  };
1021
1045
  }
1022
- get plugins() {
1023
- return this.#getSortedPlugins();
1024
- }
1046
+ /**
1047
+ * @deprecated use resolvers context instead
1048
+ */
1025
1049
  getFile({ name, mode, extname, pluginName, options }) {
1026
1050
  const resolvedName = mode ? mode === "single" ? "" : this.resolveName({
1027
1051
  name,
@@ -1044,6 +1068,9 @@ var PluginManager = class {
1044
1068
  exports: []
1045
1069
  };
1046
1070
  }
1071
+ /**
1072
+ * @deprecated use resolvers context instead
1073
+ */
1047
1074
  resolvePath = (params) => {
1048
1075
  const defaultPath = resolve(resolve(this.config.root, this.config.output.path), params.baseName);
1049
1076
  if (params.pluginName) return this.hookForPluginSync({
@@ -1064,15 +1091,15 @@ var PluginManager = class {
1064
1091
  ]
1065
1092
  })?.result || defaultPath;
1066
1093
  };
1094
+ /**
1095
+ * @deprecated use resolvers context instead
1096
+ */
1067
1097
  resolveName = (params) => {
1068
- if (params.pluginName) {
1069
- const names = this.hookForPluginSync({
1070
- pluginName: params.pluginName,
1071
- hookName: "resolveName",
1072
- parameters: [params.name.trim(), params.type]
1073
- });
1074
- return transformReservedWord([...new Set(names)].at(0) || params.name);
1075
- }
1098
+ if (params.pluginName) return transformReservedWord(this.hookForPluginSync({
1099
+ pluginName: params.pluginName,
1100
+ hookName: "resolveName",
1101
+ parameters: [params.name.trim(), params.type]
1102
+ })?.at(0) ?? params.name);
1076
1103
  const name = this.hookFirstSync({
1077
1104
  hookName: "resolveName",
1078
1105
  parameters: [params.name.trim(), params.type]
@@ -1083,49 +1110,46 @@ var PluginManager = class {
1083
1110
  * Run a specific hookName for plugin x.
1084
1111
  */
1085
1112
  async hookForPlugin({ pluginName, hookName, parameters }) {
1086
- const plugins = this.getPluginsByName(hookName, pluginName);
1113
+ const plugin = this.plugins.get(pluginName);
1114
+ if (!plugin) return [null];
1087
1115
  this.events.emit("plugins:hook:progress:start", {
1088
1116
  hookName,
1089
- plugins
1117
+ plugins: [plugin]
1118
+ });
1119
+ const result = await this.#execute({
1120
+ strategy: "hookFirst",
1121
+ hookName,
1122
+ parameters,
1123
+ plugin
1090
1124
  });
1091
- const items = [];
1092
- for (const plugin of plugins) {
1093
- const result = await this.#execute({
1094
- strategy: "hookFirst",
1095
- hookName,
1096
- parameters,
1097
- plugin
1098
- });
1099
- if (result !== void 0 && result !== null) items.push(result);
1100
- }
1101
1125
  this.events.emit("plugins:hook:progress:end", { hookName });
1102
- return items;
1126
+ return [result];
1103
1127
  }
1104
1128
  /**
1105
1129
  * Run a specific hookName for plugin x.
1106
1130
  */
1107
1131
  hookForPluginSync({ pluginName, hookName, parameters }) {
1108
- return this.getPluginsByName(hookName, pluginName).map((plugin) => {
1109
- return this.#executeSync({
1110
- strategy: "hookFirst",
1111
- hookName,
1112
- parameters,
1113
- plugin
1114
- });
1115
- }).filter((x) => x !== null);
1132
+ const plugin = this.plugins.get(pluginName);
1133
+ if (!plugin) return null;
1134
+ const result = this.#executeSync({
1135
+ strategy: "hookFirst",
1136
+ hookName,
1137
+ parameters,
1138
+ plugin
1139
+ });
1140
+ return result !== null ? [result] : [];
1116
1141
  }
1117
1142
  /**
1118
1143
  * Returns the first non-null result.
1119
1144
  */
1120
1145
  async hookFirst({ hookName, parameters, skipped }) {
1121
- const plugins = this.#getSortedPlugins(hookName).filter((plugin) => {
1122
- return skipped ? !skipped.has(plugin) : true;
1123
- });
1146
+ const plugins = [];
1147
+ for (const plugin of this.plugins.values()) if (hookName in plugin && (skipped ? !skipped.has(plugin) : true)) plugins.push(plugin);
1124
1148
  this.events.emit("plugins:hook:progress:start", {
1125
1149
  hookName,
1126
1150
  plugins
1127
1151
  });
1128
- const promises = plugins.map((plugin) => {
1152
+ const result = await hookFirst(plugins.map((plugin) => {
1129
1153
  return async () => {
1130
1154
  const value = await this.#execute({
1131
1155
  strategy: "hookFirst",
@@ -1138,8 +1162,7 @@ var PluginManager = class {
1138
1162
  result: value
1139
1163
  });
1140
1164
  };
1141
- });
1142
- const result = await this.#promiseManager.run("first", promises);
1165
+ }), hookFirstNullCheck);
1143
1166
  this.events.emit("plugins:hook:progress:end", { hookName });
1144
1167
  return result;
1145
1168
  }
@@ -1148,10 +1171,9 @@ var PluginManager = class {
1148
1171
  */
1149
1172
  hookFirstSync({ hookName, parameters, skipped }) {
1150
1173
  let parseResult = null;
1151
- const plugins = this.#getSortedPlugins(hookName).filter((plugin) => {
1152
- return skipped ? !skipped.has(plugin) : true;
1153
- });
1154
- for (const plugin of plugins) {
1174
+ for (const plugin of this.plugins.values()) {
1175
+ if (!(hookName in plugin)) continue;
1176
+ if (skipped?.has(plugin)) continue;
1155
1177
  parseResult = {
1156
1178
  result: this.#executeSync({
1157
1179
  strategy: "hookFirst",
@@ -1161,7 +1183,7 @@ var PluginManager = class {
1161
1183
  }),
1162
1184
  plugin
1163
1185
  };
1164
- if (parseResult?.result != null) break;
1186
+ if (parseResult.result != null) break;
1165
1187
  }
1166
1188
  return parseResult;
1167
1189
  }
@@ -1169,13 +1191,14 @@ var PluginManager = class {
1169
1191
  * Runs all plugins in parallel based on `this.plugin` order and `pre`/`post` settings.
1170
1192
  */
1171
1193
  async hookParallel({ hookName, parameters }) {
1172
- const plugins = this.#getSortedPlugins(hookName);
1194
+ const plugins = [];
1195
+ for (const plugin of this.plugins.values()) if (hookName in plugin) plugins.push(plugin);
1173
1196
  this.events.emit("plugins:hook:progress:start", {
1174
1197
  hookName,
1175
1198
  plugins
1176
1199
  });
1177
1200
  const pluginStartTimes = /* @__PURE__ */ new Map();
1178
- const promises = plugins.map((plugin) => {
1201
+ const results = await hookParallel(plugins.map((plugin) => {
1179
1202
  return () => {
1180
1203
  pluginStartTimes.set(plugin, performance.now());
1181
1204
  return this.#execute({
@@ -1185,11 +1208,10 @@ var PluginManager = class {
1185
1208
  plugin
1186
1209
  });
1187
1210
  };
1188
- });
1189
- const results = await this.#promiseManager.run("parallel", promises, { concurrency: this.options.concurrency });
1211
+ }), this.options.concurrency);
1190
1212
  results.forEach((result, index) => {
1191
1213
  if (isPromiseRejectedResult(result)) {
1192
- const plugin = this.#getSortedPlugins(hookName)[index];
1214
+ const plugin = plugins[index];
1193
1215
  if (plugin) {
1194
1216
  const startTime = pluginStartTimes.get(plugin) ?? performance.now();
1195
1217
  this.events.emit("error", result.reason, {
@@ -1212,49 +1234,29 @@ var PluginManager = class {
1212
1234
  * Chains plugins
1213
1235
  */
1214
1236
  async hookSeq({ hookName, parameters }) {
1215
- const plugins = this.#getSortedPlugins(hookName);
1237
+ const plugins = [];
1238
+ for (const plugin of this.plugins.values()) if (hookName in plugin) plugins.push(plugin);
1216
1239
  this.events.emit("plugins:hook:progress:start", {
1217
1240
  hookName,
1218
1241
  plugins
1219
1242
  });
1220
- const promises = plugins.map((plugin) => {
1243
+ await hookSeq(plugins.map((plugin) => {
1221
1244
  return () => this.#execute({
1222
1245
  strategy: "hookSeq",
1223
1246
  hookName,
1224
1247
  parameters,
1225
1248
  plugin
1226
1249
  });
1227
- });
1228
- await this.#promiseManager.run("seq", promises);
1250
+ }));
1229
1251
  this.events.emit("plugins:hook:progress:end", { hookName });
1230
1252
  }
1231
- #getSortedPlugins(hookName) {
1232
- const plugins = [...this.#plugins];
1233
- if (hookName) return plugins.filter((plugin) => hookName in plugin);
1234
- return plugins.map((plugin) => {
1235
- if (plugin.pre) {
1236
- let missingPlugins = plugin.pre.filter((pluginName) => !plugins.find((pluginToFind) => pluginToFind.name === pluginName));
1237
- if (missingPlugins.includes("plugin-oas") && this.adapter) missingPlugins = missingPlugins.filter((pluginName) => pluginName !== "plugin-oas");
1238
- if (missingPlugins.length > 0) throw new ValidationPluginError(`The plugin '${plugin.name}' has a pre set that references missing plugins for '${missingPlugins.join(", ")}'`);
1239
- }
1240
- return plugin;
1241
- }).sort((a, b) => {
1242
- if (b.pre?.includes(a.name)) return 1;
1243
- if (b.post?.includes(a.name)) return -1;
1244
- return 0;
1245
- });
1246
- }
1247
- getPluginByName(pluginName) {
1248
- return [...this.#plugins].find((item) => item.name === pluginName);
1253
+ getPlugin(pluginName) {
1254
+ return this.plugins.get(pluginName);
1249
1255
  }
1250
- getPluginsByName(hookName, pluginName) {
1251
- const plugins = [...this.plugins];
1252
- const pluginByPluginName = plugins.filter((plugin) => hookName in plugin).filter((item) => item.name === pluginName);
1253
- if (!pluginByPluginName?.length) {
1254
- const corePlugin = plugins.find((plugin) => plugin.name === "core" && hookName in plugin);
1255
- return corePlugin ? [corePlugin] : [];
1256
- }
1257
- return pluginByPluginName;
1256
+ requirePlugin(pluginName) {
1257
+ const plugin = this.plugins.get(pluginName);
1258
+ if (!plugin) throw new Error(`[kubb] Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`);
1259
+ return plugin;
1258
1260
  }
1259
1261
  /**
1260
1262
  * Run an async plugin hook and return the result.
@@ -1342,31 +1344,34 @@ var PluginManager = class {
1342
1344
  return null;
1343
1345
  }
1344
1346
  }
1345
- #parse(plugin) {
1346
- const usedPluginNames = this.#usedPluginNames;
1347
- setUniqueName(plugin.name, usedPluginNames);
1348
- const usageCount = usedPluginNames[plugin.name];
1349
- if (usageCount && usageCount > 1) throw new ValidationPluginError(`Duplicate plugin "${plugin.name}" detected. Each plugin can only be used once. Use a different configuration instead of adding multiple instances of the same plugin.`);
1350
- return {
1351
- install() {},
1352
- ...plugin
1353
- };
1354
- }
1355
1347
  };
1356
1348
  //#endregion
1357
- //#region src/defineStorage.ts
1349
+ //#region src/renderNode.tsx
1358
1350
  /**
1359
- * Wraps a storage builder so the `options` argument is optional, following the
1360
- * same factory pattern as `definePlugin`, `defineLogger`, and `defineAdapter`.
1351
+ * Handles the return value of a plugin AST hook or generator method.
1361
1352
  *
1362
- * The builder receives the resolved options object and must return a
1363
- * `DefineStorage`-compatible object that includes a `name` string.
1353
+ * - React element rendered via an isolated react-fabric context, files merged into `fabric`
1354
+ * - `Array<FabricFile.File>` upserted directly into `fabric`
1355
+ * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
1356
+ */
1357
+ async function applyHookResult(result, fabric) {
1358
+ if (!result) return;
1359
+ if (Array.isArray(result)) {
1360
+ await fabric.upsertFile(...result);
1361
+ return;
1362
+ }
1363
+ const fabricChild = createReactFabric();
1364
+ await fabricChild.render(/* @__PURE__ */ jsx(Fabric, { children: result }));
1365
+ fabric.context.fileManager.upsert(...fabricChild.files);
1366
+ fabricChild.unmount();
1367
+ }
1368
+ //#endregion
1369
+ //#region src/createStorage.ts
1370
+ /**
1371
+ * Creates a storage factory. Call the returned function with optional options to get the storage instance.
1364
1372
  *
1365
1373
  * @example
1366
- * ```ts
1367
- * import { defineStorage } from '@kubb/core'
1368
- *
1369
- * export const memoryStorage = defineStorage((_options) => {
1374
+ * export const memoryStorage = createStorage(() => {
1370
1375
  * const store = new Map<string, string>()
1371
1376
  * return {
1372
1377
  * name: 'memory',
@@ -1374,13 +1379,15 @@ var PluginManager = class {
1374
1379
  * async getItem(key) { return store.get(key) ?? null },
1375
1380
  * async setItem(key, value) { store.set(key, value) },
1376
1381
  * async removeItem(key) { store.delete(key) },
1377
- * async getKeys() { return [...store.keys()] },
1378
- * async clear() { store.clear() },
1382
+ * async getKeys(base) {
1383
+ * const keys = [...store.keys()]
1384
+ * return base ? keys.filter((k) => k.startsWith(base)) : keys
1385
+ * },
1386
+ * async clear(base) { if (!base) store.clear() },
1379
1387
  * }
1380
1388
  * })
1381
- * ```
1382
1389
  */
1383
- function defineStorage(build) {
1390
+ function createStorage(build) {
1384
1391
  return (options) => build(options ?? {});
1385
1392
  }
1386
1393
  //#endregion
@@ -1408,7 +1415,7 @@ function defineStorage(build) {
1408
1415
  * })
1409
1416
  * ```
1410
1417
  */
1411
- const fsStorage = defineStorage(() => ({
1418
+ const fsStorage = createStorage(() => ({
1412
1419
  name: "fs",
1413
1420
  async hasItem(key) {
1414
1421
  try {
@@ -1456,11 +1463,14 @@ const fsStorage = defineStorage(() => ({
1456
1463
  }));
1457
1464
  //#endregion
1458
1465
  //#region package.json
1459
- var version$1 = "5.0.0-alpha.3";
1466
+ var version$1 = "5.0.0-alpha.30";
1460
1467
  //#endregion
1461
1468
  //#region src/utils/diagnostics.ts
1462
1469
  /**
1463
- * Get diagnostic information for debugging
1470
+ * Returns a snapshot of the current runtime environment.
1471
+ *
1472
+ * Useful for attaching context to debug logs and error reports so that
1473
+ * issues can be reproduced without manual information gathering.
1464
1474
  */
1465
1475
  function getDiagnosticInfo() {
1466
1476
  return {
@@ -1472,7 +1482,253 @@ function getDiagnosticInfo() {
1472
1482
  };
1473
1483
  }
1474
1484
  //#endregion
1485
+ //#region src/utils/TreeNode.ts
1486
+ /**
1487
+ * Tree structure used to build per-directory barrel (`index.ts`) files from a
1488
+ * flat list of generated {@link FabricFile.File} entries.
1489
+ *
1490
+ * Each node represents either a directory or a file within the output tree.
1491
+ * Use {@link TreeNode.build} to construct a root node from a file list, then
1492
+ * traverse with {@link TreeNode.forEach}, {@link TreeNode.leaves}, or the
1493
+ * `*Deep` helpers.
1494
+ */
1495
+ var TreeNode = class TreeNode {
1496
+ data;
1497
+ parent;
1498
+ children = [];
1499
+ #cachedLeaves = void 0;
1500
+ constructor(data, parent) {
1501
+ this.data = data;
1502
+ this.parent = parent;
1503
+ }
1504
+ addChild(data) {
1505
+ const child = new TreeNode(data, this);
1506
+ if (!this.children) this.children = [];
1507
+ this.children.push(child);
1508
+ return child;
1509
+ }
1510
+ /**
1511
+ * Returns the root ancestor of this node, walking up via `parent` links.
1512
+ */
1513
+ get root() {
1514
+ if (!this.parent) return this;
1515
+ return this.parent.root;
1516
+ }
1517
+ /**
1518
+ * Returns all leaf descendants (nodes with no children) of this node.
1519
+ *
1520
+ * Results are cached after the first traversal.
1521
+ */
1522
+ get leaves() {
1523
+ if (!this.children || this.children.length === 0) return [this];
1524
+ if (this.#cachedLeaves) return this.#cachedLeaves;
1525
+ const leaves = [];
1526
+ for (const child of this.children) leaves.push(...child.leaves);
1527
+ this.#cachedLeaves = leaves;
1528
+ return leaves;
1529
+ }
1530
+ /**
1531
+ * Visits this node and every descendant in depth-first order.
1532
+ */
1533
+ forEach(callback) {
1534
+ if (typeof callback !== "function") throw new TypeError("forEach() callback must be a function");
1535
+ callback(this);
1536
+ for (const child of this.children) child.forEach(callback);
1537
+ return this;
1538
+ }
1539
+ /**
1540
+ * Finds the first leaf that satisfies `predicate`, or `undefined` when none match.
1541
+ */
1542
+ findDeep(predicate) {
1543
+ if (typeof predicate !== "function") throw new TypeError("find() predicate must be a function");
1544
+ return this.leaves.find(predicate);
1545
+ }
1546
+ /**
1547
+ * Calls `callback` for every leaf of this node.
1548
+ */
1549
+ forEachDeep(callback) {
1550
+ if (typeof callback !== "function") throw new TypeError("forEach() callback must be a function");
1551
+ this.leaves.forEach(callback);
1552
+ }
1553
+ /**
1554
+ * Returns all leaves that satisfy `callback`.
1555
+ */
1556
+ filterDeep(callback) {
1557
+ if (typeof callback !== "function") throw new TypeError("filter() callback must be a function");
1558
+ return this.leaves.filter(callback);
1559
+ }
1560
+ /**
1561
+ * Maps every leaf through `callback` and returns the resulting array.
1562
+ */
1563
+ mapDeep(callback) {
1564
+ if (typeof callback !== "function") throw new TypeError("map() callback must be a function");
1565
+ return this.leaves.map(callback);
1566
+ }
1567
+ /**
1568
+ * Builds a {@link TreeNode} tree from a flat list of files.
1569
+ *
1570
+ * - Filters to files under `root` (when provided) and skips `.json` files.
1571
+ * - Returns `null` when no files match.
1572
+ */
1573
+ static build(files, root) {
1574
+ try {
1575
+ const filteredTree = buildDirectoryTree(files, root);
1576
+ if (!filteredTree) return null;
1577
+ const treeNode = new TreeNode({
1578
+ name: filteredTree.name,
1579
+ path: filteredTree.path,
1580
+ file: filteredTree.file,
1581
+ type: getMode(filteredTree.path)
1582
+ });
1583
+ const recurse = (node, item) => {
1584
+ const subNode = node.addChild({
1585
+ name: item.name,
1586
+ path: item.path,
1587
+ file: item.file,
1588
+ type: getMode(item.path)
1589
+ });
1590
+ if (item.children?.length) item.children?.forEach((child) => {
1591
+ recurse(subNode, child);
1592
+ });
1593
+ };
1594
+ filteredTree.children?.forEach((child) => {
1595
+ recurse(treeNode, child);
1596
+ });
1597
+ return treeNode;
1598
+ } catch (error) {
1599
+ throw new Error("Something went wrong with creating barrel files with the TreeNode class", { cause: error });
1600
+ }
1601
+ }
1602
+ };
1603
+ const normalizePath = (p) => p.replaceAll("\\", "/");
1604
+ function buildDirectoryTree(files, rootFolder = "") {
1605
+ const normalizedRootFolder = normalizePath(rootFolder);
1606
+ const rootPrefix = normalizedRootFolder.endsWith("/") ? normalizedRootFolder : `${normalizedRootFolder}/`;
1607
+ const filteredFiles = files.filter((file) => {
1608
+ const normalizedFilePath = normalizePath(file.path);
1609
+ return rootFolder ? normalizedFilePath.startsWith(rootPrefix) && !normalizedFilePath.endsWith(".json") : !normalizedFilePath.endsWith(".json");
1610
+ });
1611
+ if (filteredFiles.length === 0) return null;
1612
+ const root = {
1613
+ name: rootFolder || "",
1614
+ path: rootFolder || "",
1615
+ children: []
1616
+ };
1617
+ filteredFiles.forEach((file) => {
1618
+ const parts = file.path.slice(rootFolder.length).split("/").filter(Boolean);
1619
+ let currentLevel = root.children;
1620
+ let currentPath = normalizePath(rootFolder);
1621
+ parts.forEach((part, index) => {
1622
+ currentPath = path.posix.join(currentPath, part);
1623
+ let existingNode = currentLevel.find((node) => node.name === part);
1624
+ if (!existingNode) {
1625
+ if (index === parts.length - 1) existingNode = {
1626
+ name: part,
1627
+ file,
1628
+ path: currentPath
1629
+ };
1630
+ else existingNode = {
1631
+ name: part,
1632
+ path: currentPath,
1633
+ children: []
1634
+ };
1635
+ currentLevel.push(existingNode);
1636
+ }
1637
+ if (!existingNode.file) currentLevel = existingNode.children;
1638
+ });
1639
+ });
1640
+ return root;
1641
+ }
1642
+ //#endregion
1643
+ //#region src/utils/getBarrelFiles.ts
1644
+ /** biome-ignore-all lint/suspicious/useIterableCallbackReturn: not needed */
1645
+ function getBarrelFilesByRoot(root, files) {
1646
+ const cachedFiles = /* @__PURE__ */ new Map();
1647
+ TreeNode.build(files, root)?.forEach((treeNode) => {
1648
+ if (!treeNode?.children || !treeNode.parent?.data.path) return;
1649
+ const barrelFile = {
1650
+ path: join(treeNode.parent?.data.path, "index.ts"),
1651
+ baseName: "index.ts",
1652
+ exports: [],
1653
+ imports: [],
1654
+ sources: []
1655
+ };
1656
+ const previousBarrelFile = cachedFiles.get(barrelFile.path);
1657
+ treeNode.leaves.forEach((item) => {
1658
+ if (!item.data.name) return;
1659
+ (item.data.file?.sources || []).forEach((source) => {
1660
+ if (!item.data.file?.path || !source.isIndexable || !source.name) return;
1661
+ if (previousBarrelFile?.sources.some((item) => item.name === source.name && item.isTypeOnly === source.isTypeOnly)) return;
1662
+ barrelFile.exports.push({
1663
+ name: [source.name],
1664
+ path: getRelativePath(treeNode.parent?.data.path, item.data.path),
1665
+ isTypeOnly: source.isTypeOnly
1666
+ });
1667
+ barrelFile.sources.push({
1668
+ name: source.name,
1669
+ isTypeOnly: source.isTypeOnly,
1670
+ value: "",
1671
+ isExportable: false,
1672
+ isIndexable: false
1673
+ });
1674
+ });
1675
+ });
1676
+ if (previousBarrelFile) {
1677
+ previousBarrelFile.sources.push(...barrelFile.sources);
1678
+ previousBarrelFile.exports?.push(...barrelFile.exports || []);
1679
+ } else cachedFiles.set(barrelFile.path, barrelFile);
1680
+ });
1681
+ return [...cachedFiles.values()];
1682
+ }
1683
+ function trimExtName(text) {
1684
+ const dotIndex = text.lastIndexOf(".");
1685
+ if (dotIndex > 0 && !text.includes("/", dotIndex)) return text.slice(0, dotIndex);
1686
+ return text;
1687
+ }
1688
+ /**
1689
+ * Generates `index.ts` barrel files for all directories under `root/output.path`.
1690
+ *
1691
+ * - Returns an empty array when `type` is falsy or `'propagate'`.
1692
+ * - Skips generation when the output path itself ends with `index` (already a barrel).
1693
+ * - When `type` is `'all'`, strips named exports so every re-export becomes a wildcard (`export * from`).
1694
+ * - Attaches `meta` to each barrel file for downstream plugin identification.
1695
+ */
1696
+ async function getBarrelFiles(files, { type, meta = {}, root, output }) {
1697
+ if (!type || type === "propagate") return [];
1698
+ const pathToBuildFrom = join(root, output.path);
1699
+ if (trimExtName(pathToBuildFrom).endsWith("index")) return [];
1700
+ const barrelFiles = getBarrelFilesByRoot(pathToBuildFrom, files);
1701
+ if (type === "all") return barrelFiles.map((file) => {
1702
+ return {
1703
+ ...file,
1704
+ exports: file.exports?.map((exportItem) => {
1705
+ return {
1706
+ ...exportItem,
1707
+ name: void 0
1708
+ };
1709
+ })
1710
+ };
1711
+ });
1712
+ return barrelFiles.map((indexFile) => {
1713
+ return {
1714
+ ...indexFile,
1715
+ meta
1716
+ };
1717
+ });
1718
+ }
1719
+ //#endregion
1475
1720
  //#region src/build.ts
1721
+ /**
1722
+ * Initializes all Kubb infrastructure for a build without executing any plugins.
1723
+ *
1724
+ * - Validates the input path (when applicable).
1725
+ * - Applies config defaults (`root`, `output.*`, `devtools`).
1726
+ * - Creates the Fabric instance and wires storage, format, and lint hooks.
1727
+ * - Runs the adapter (if configured) to produce the universal `RootNode`.
1728
+ *
1729
+ * Pass the returned {@link SetupResult} directly to {@link safeBuild} or {@link build}
1730
+ * via the `overrides` argument to reuse the same infrastructure across multiple runs.
1731
+ */
1476
1732
  async function setup(options) {
1477
1733
  const { config: userConfig, events = new AsyncEventEmitter() } = options;
1478
1734
  const sources = /* @__PURE__ */ new Map();
@@ -1570,7 +1826,7 @@ async function setup(options) {
1570
1826
  ` • Barrel type: ${definedConfig.output.barrelType || "none"}`
1571
1827
  ]
1572
1828
  });
1573
- const pluginManager = new PluginManager(definedConfig, {
1829
+ const pluginDriver = new PluginDriver(definedConfig, {
1574
1830
  fabric,
1575
1831
  events,
1576
1832
  concurrency: 15
@@ -1581,26 +1837,32 @@ async function setup(options) {
1581
1837
  date: /* @__PURE__ */ new Date(),
1582
1838
  logs: [`Running adapter: ${definedConfig.adapter.name}`]
1583
1839
  });
1584
- pluginManager.adapter = definedConfig.adapter;
1585
- pluginManager.rootNode = await definedConfig.adapter.parse(source);
1840
+ pluginDriver.adapter = definedConfig.adapter;
1841
+ pluginDriver.rootNode = await definedConfig.adapter.parse(source);
1586
1842
  await events.emit("debug", {
1587
1843
  date: /* @__PURE__ */ new Date(),
1588
1844
  logs: [
1589
1845
  `✓ Adapter '${definedConfig.adapter.name}' resolved RootNode`,
1590
- ` • Schemas: ${pluginManager.rootNode.schemas.length}`,
1591
- ` • Operations: ${pluginManager.rootNode.operations.length}`
1846
+ ` • Schemas: ${pluginDriver.rootNode.schemas.length}`,
1847
+ ` • Operations: ${pluginDriver.rootNode.operations.length}`
1592
1848
  ]
1593
1849
  });
1594
1850
  }
1595
1851
  return {
1596
1852
  events,
1597
1853
  fabric,
1598
- pluginManager,
1854
+ driver: pluginDriver,
1599
1855
  sources
1600
1856
  };
1601
1857
  }
1858
+ /**
1859
+ * Runs a full Kubb build and throws on any error or plugin failure.
1860
+ *
1861
+ * Internally delegates to {@link safeBuild} and rethrows collected errors.
1862
+ * Pass an existing {@link SetupResult} via `overrides` to skip the setup phase.
1863
+ */
1602
1864
  async function build(options, overrides) {
1603
- const { fabric, files, pluginManager, failedPlugins, pluginTimings, error, sources } = await safeBuild(options, overrides);
1865
+ const { fabric, files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(options, overrides);
1604
1866
  if (error) throw error;
1605
1867
  if (failedPlugins.size > 0) {
1606
1868
  const errors = [...failedPlugins].map(({ error }) => error);
@@ -1610,30 +1872,96 @@ async function build(options, overrides) {
1610
1872
  failedPlugins,
1611
1873
  fabric,
1612
1874
  files,
1613
- pluginManager,
1875
+ driver,
1614
1876
  pluginTimings,
1615
1877
  error: void 0,
1616
1878
  sources
1617
1879
  };
1618
1880
  }
1881
+ /**
1882
+ * Walks the AST and dispatches nodes to a plugin's direct AST hooks
1883
+ * (`schema`, `operation`, `operations`).
1884
+ *
1885
+ * - Each hook accepts a single handler **or an array** — all entries are called in sequence.
1886
+ * - Nodes that are excluded by `exclude`/`include` plugin options are skipped automatically.
1887
+ * - Return values are handled via `applyHookResult`: React elements are rendered,
1888
+ * `FabricFile.File[]` are written via upsert, and `void` is a no-op (manual handling).
1889
+ * - Barrel files are generated automatically when `output.barrelType` is set.
1890
+ */
1891
+ async function runPluginAstHooks(plugin, context) {
1892
+ const { adapter, rootNode, resolver, fabric } = context;
1893
+ const { exclude, include, override } = plugin.options;
1894
+ if (!adapter || !rootNode) throw new Error(`[${plugin.name}] No adapter found. Add an OAS adapter (e.g. pluginOas()) before this plugin in your Kubb config.`);
1895
+ const collectedOperations = [];
1896
+ await walk(rootNode, {
1897
+ depth: "shallow",
1898
+ async schema(node) {
1899
+ if (!plugin.schema) return;
1900
+ const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node;
1901
+ const options = resolver.resolveOptions(transformedNode, {
1902
+ options: plugin.options,
1903
+ exclude,
1904
+ include,
1905
+ override
1906
+ });
1907
+ if (options === null) return;
1908
+ await applyHookResult(await plugin.schema.call(context, transformedNode, options), fabric);
1909
+ },
1910
+ async operation(node) {
1911
+ const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node;
1912
+ const options = resolver.resolveOptions(transformedNode, {
1913
+ options: plugin.options,
1914
+ exclude,
1915
+ include,
1916
+ override
1917
+ });
1918
+ if (options !== null) {
1919
+ collectedOperations.push(transformedNode);
1920
+ if (plugin.operation) await applyHookResult(await plugin.operation.call(context, transformedNode, options), fabric);
1921
+ }
1922
+ }
1923
+ });
1924
+ if (plugin.operations && collectedOperations.length > 0) await applyHookResult(await plugin.operations.call(context, collectedOperations, plugin.options), fabric);
1925
+ }
1926
+ /**
1927
+ * Runs a full Kubb build and captures errors instead of throwing.
1928
+ *
1929
+ * - Installs each plugin in order, recording failures in `failedPlugins`.
1930
+ * - Generates the root barrel file when `output.barrelType` is set.
1931
+ * - Writes all files through Fabric.
1932
+ *
1933
+ * Returns a {@link BuildOutput} even on failure — inspect `error` and
1934
+ * `failedPlugins` to determine whether the build succeeded.
1935
+ */
1619
1936
  async function safeBuild(options, overrides) {
1620
- const { fabric, pluginManager, events, sources } = overrides ? overrides : await setup(options);
1937
+ const { fabric, driver, events, sources } = overrides ? overrides : await setup(options);
1621
1938
  const failedPlugins = /* @__PURE__ */ new Set();
1622
1939
  const pluginTimings = /* @__PURE__ */ new Map();
1623
- const config = pluginManager.config;
1940
+ const config = driver.config;
1624
1941
  try {
1625
- for (const plugin of pluginManager.plugins) {
1626
- const context = pluginManager.getContext(plugin);
1942
+ for (const plugin of driver.plugins.values()) {
1943
+ const context = driver.getContext(plugin);
1627
1944
  const hrStart = process.hrtime();
1628
- const installer = plugin.install.bind(context);
1945
+ const { output } = plugin.options ?? {};
1946
+ const root = resolve(config.root, config.output.path);
1629
1947
  try {
1630
1948
  const timestamp = /* @__PURE__ */ new Date();
1631
1949
  await events.emit("plugin:start", plugin);
1632
1950
  await events.emit("debug", {
1633
1951
  date: timestamp,
1634
- logs: ["Installing plugin...", ` • Plugin Name: ${plugin.name}`]
1952
+ logs: ["Starting plugin...", ` • Plugin Name: ${plugin.name}`]
1635
1953
  });
1636
- await installer(context);
1954
+ await plugin.buildStart.call(context);
1955
+ if (plugin.schema || plugin.operation || plugin.operations) await runPluginAstHooks(plugin, context);
1956
+ if (output) {
1957
+ const barrelFiles = await getBarrelFiles(fabric.files, {
1958
+ type: output.barrelType ?? "named",
1959
+ root,
1960
+ output,
1961
+ meta: { pluginName: plugin.name }
1962
+ });
1963
+ await context.upsertFile(...barrelFiles);
1964
+ }
1637
1965
  const duration = getElapsedMs(hrStart);
1638
1966
  pluginTimings.set(plugin.name, duration);
1639
1967
  await events.emit("plugin:end", plugin, {
@@ -1642,7 +1970,7 @@ async function safeBuild(options, overrides) {
1642
1970
  });
1643
1971
  await events.emit("debug", {
1644
1972
  date: /* @__PURE__ */ new Date(),
1645
- logs: [`✓ Plugin installed successfully (${formatMs(duration)})`]
1973
+ logs: [`✓ Plugin started successfully (${formatMs(duration)})`]
1646
1974
  });
1647
1975
  } catch (caughtError) {
1648
1976
  const error = caughtError;
@@ -1656,7 +1984,7 @@ async function safeBuild(options, overrides) {
1656
1984
  await events.emit("debug", {
1657
1985
  date: errorTimestamp,
1658
1986
  logs: [
1659
- "✗ Plugin installation failed",
1987
+ "✗ Plugin start failed",
1660
1988
  ` • Plugin Name: ${plugin.name}`,
1661
1989
  ` • Error: ${error.constructor.name} - ${error.message}`,
1662
1990
  " • Stack Trace:",
@@ -1696,7 +2024,7 @@ async function safeBuild(options, overrides) {
1696
2024
  rootDir,
1697
2025
  existingExports: new Set(existingBarrel?.exports?.flatMap((e) => Array.isArray(e.name) ? e.name : [e.name]).filter((n) => Boolean(n)) ?? []),
1698
2026
  config,
1699
- pluginManager
2027
+ driver
1700
2028
  }),
1701
2029
  sources: [],
1702
2030
  imports: [],
@@ -1710,11 +2038,15 @@ async function safeBuild(options, overrides) {
1710
2038
  }
1711
2039
  const files = [...fabric.files];
1712
2040
  await fabric.write({ extension: config.output.extension });
2041
+ for (const plugin of driver.plugins.values()) if (plugin.buildEnd) {
2042
+ const context = driver.getContext(plugin);
2043
+ await plugin.buildEnd.call(context);
2044
+ }
1713
2045
  return {
1714
2046
  failedPlugins,
1715
2047
  fabric,
1716
2048
  files,
1717
- pluginManager,
2049
+ driver,
1718
2050
  pluginTimings,
1719
2051
  sources
1720
2052
  };
@@ -1723,16 +2055,16 @@ async function safeBuild(options, overrides) {
1723
2055
  failedPlugins,
1724
2056
  fabric,
1725
2057
  files: [],
1726
- pluginManager,
2058
+ driver,
1727
2059
  pluginTimings,
1728
2060
  error,
1729
2061
  sources
1730
2062
  };
1731
2063
  }
1732
2064
  }
1733
- function buildBarrelExports({ barrelFiles, rootDir, existingExports, config, pluginManager }) {
2065
+ function buildBarrelExports({ barrelFiles, rootDir, existingExports, config, driver }) {
1734
2066
  const pluginNameMap = /* @__PURE__ */ new Map();
1735
- for (const plugin of pluginManager.plugins) pluginNameMap.set(plugin.name, plugin);
2067
+ for (const plugin of driver.plugins.values()) pluginNameMap.set(plugin.name, plugin);
1736
2068
  return barrelFiles.flatMap((file) => {
1737
2069
  const containsOnlyTypes = file.sources?.every((source) => source.isTypeOnly);
1738
2070
  return (file.sources ?? []).flatMap((source) => {
@@ -1757,164 +2089,520 @@ function buildBarrelExports({ barrelFiles, rootDir, existingExports, config, plu
1757
2089
  function inputToAdapterSource(config) {
1758
2090
  if (Array.isArray(config.input)) return {
1759
2091
  type: "paths",
1760
- paths: config.input.map((i) => resolve(config.root, i.path))
2092
+ paths: config.input.map((i) => new URLPath(i.path).isURL ? i.path : resolve(config.root, i.path))
1761
2093
  };
1762
2094
  if ("data" in config.input) return {
1763
2095
  type: "data",
1764
2096
  data: config.input.data
1765
2097
  };
2098
+ if (new URLPath(config.input.path).isURL) return {
2099
+ type: "path",
2100
+ path: config.input.path
2101
+ };
1766
2102
  return {
1767
2103
  type: "path",
1768
2104
  path: resolve(config.root, config.input.path)
1769
2105
  };
1770
2106
  }
1771
2107
  //#endregion
1772
- //#region src/defineAdapter.ts
2108
+ //#region src/createAdapter.ts
1773
2109
  /**
1774
- * Wraps an adapter builder to make the options parameter optional.
2110
+ * Creates an adapter factory. Call the returned function with optional options to get the adapter instance.
1775
2111
  *
1776
2112
  * @example
1777
- * ```ts
1778
- * export const adapterOas = defineAdapter<OasAdapter>((options) => {
1779
- * const { validate = true, dateType = 'string' } = options
2113
+ * export const myAdapter = createAdapter<MyAdapter>((options) => {
1780
2114
  * return {
1781
- * name: adapterOasName,
1782
- * options: { validate, dateType, ... },
1783
- * parse(source) { ... },
2115
+ * name: 'my-adapter',
2116
+ * options,
2117
+ * async parse(source) { ... },
1784
2118
  * }
1785
2119
  * })
1786
- * ```
2120
+ *
2121
+ * // instantiate
2122
+ * const adapter = myAdapter({ validate: true })
1787
2123
  */
1788
- function defineAdapter(build) {
2124
+ function createAdapter(build) {
1789
2125
  return (options) => build(options ?? {});
1790
2126
  }
1791
2127
  //#endregion
1792
- //#region src/defineLogger.ts
1793
- function defineLogger(logger) {
1794
- return { ...logger };
1795
- }
1796
- //#endregion
1797
- //#region src/definePlugin.ts
2128
+ //#region src/createPlugin.ts
1798
2129
  /**
1799
- * Wraps a plugin builder to make the options parameter optional.
2130
+ * Creates a plugin factory. Call the returned function with optional options to get the plugin instance.
2131
+ *
2132
+ * @example
2133
+ * ```ts
2134
+ * export const myPlugin = createPlugin<MyPlugin>((options) => {
2135
+ * return {
2136
+ * name: 'my-plugin',
2137
+ * get options() { return options },
2138
+ * resolvePath(baseName) { ... },
2139
+ * resolveName(name, type) { ... },
2140
+ * }
2141
+ * })
2142
+ *
2143
+ * // instantiate
2144
+ * const plugin = myPlugin({ output: { path: 'src/gen' } })
2145
+ * ```
1800
2146
  */
1801
- function definePlugin(build) {
2147
+ function createPlugin(build) {
1802
2148
  return (options) => build(options ?? {});
1803
2149
  }
1804
2150
  //#endregion
1805
- //#region src/PackageManager.ts
1806
- var PackageManager = class PackageManager {
1807
- static #cache = {};
1808
- #cwd;
1809
- constructor(workspace) {
1810
- if (workspace) this.#cwd = workspace;
1811
- }
1812
- set workspace(workspace) {
1813
- this.#cwd = workspace;
1814
- }
1815
- get workspace() {
1816
- return this.#cwd;
1817
- }
1818
- normalizeDirectory(directory) {
1819
- const lastChar = directory[directory.length - 1];
1820
- if (lastChar && !PATH_SEPARATORS.includes(lastChar)) return `${directory}/`;
1821
- return directory;
1822
- }
1823
- getLocation(path) {
1824
- let location = path;
1825
- if (this.#cwd) location = mod.createRequire(this.normalizeDirectory(this.#cwd)).resolve(path);
1826
- return location;
1827
- }
1828
- async import(path) {
1829
- let location = this.getLocation(path);
1830
- if (os.platform() === "win32") location = pathToFileURL(location).href;
1831
- const module = await import(location);
1832
- return module?.default ?? module;
1833
- }
1834
- async getPackageJSON() {
1835
- const pkgPath = pkg.up({ cwd: this.#cwd });
1836
- if (!pkgPath) return;
1837
- const json = await read(pkgPath);
1838
- return JSON.parse(json);
1839
- }
1840
- getPackageJSONSync() {
1841
- const pkgPath = pkg.up({ cwd: this.#cwd });
1842
- if (!pkgPath) return;
1843
- const json = readSync(pkgPath);
1844
- return JSON.parse(json);
1845
- }
1846
- static setVersion(dependency, version) {
1847
- PackageManager.#cache[dependency] = version;
1848
- }
1849
- #match(packageJSON, dependency) {
1850
- const dependencies = {
1851
- ...packageJSON.dependencies || {},
1852
- ...packageJSON.devDependencies || {}
1853
- };
1854
- if (typeof dependency === "string" && dependencies[dependency]) return dependencies[dependency];
1855
- const matchedDependency = Object.keys(dependencies).find((dep) => dep.match(dependency));
1856
- return matchedDependency ? dependencies[matchedDependency] : void 0;
1857
- }
1858
- async getVersion(dependency) {
1859
- if (typeof dependency === "string" && PackageManager.#cache[dependency]) return PackageManager.#cache[dependency];
1860
- const packageJSON = await this.getPackageJSON();
1861
- if (!packageJSON) return;
1862
- return this.#match(packageJSON, dependency);
1863
- }
1864
- getVersionSync(dependency) {
1865
- if (typeof dependency === "string" && PackageManager.#cache[dependency]) return PackageManager.#cache[dependency];
1866
- const packageJSON = this.getPackageJSONSync();
1867
- if (!packageJSON) return;
1868
- return this.#match(packageJSON, dependency);
1869
- }
1870
- async isValid(dependency, version) {
1871
- const packageVersion = await this.getVersion(dependency);
1872
- if (!packageVersion) return false;
1873
- if (packageVersion === version) return true;
1874
- const semVer = coerce(packageVersion);
1875
- if (!semVer) return false;
1876
- return satisfies(semVer, version);
1877
- }
1878
- isValidSync(dependency, version) {
1879
- const packageVersion = this.getVersionSync(dependency);
1880
- if (!packageVersion) return false;
1881
- if (packageVersion === version) return true;
1882
- const semVer = coerce(packageVersion);
1883
- if (!semVer) return false;
1884
- return satisfies(semVer, version);
1885
- }
1886
- };
1887
- //#endregion
1888
- //#region src/storages/memoryStorage.ts
2151
+ //#region src/defineGenerator.ts
1889
2152
  /**
1890
- * In-memory storage driver. Useful for testing and dry-run scenarios where
1891
- * generated output should be captured without touching the filesystem.
2153
+ * Defines a generator. Returns the object as-is with correct `this` typings.
2154
+ * No type discrimination (`type: 'react' | 'core'`) needed — `applyHookResult`
2155
+ * handles React elements and `File[]` uniformly.
2156
+ */
2157
+ function defineGenerator(generator) {
2158
+ return generator;
2159
+ }
2160
+ /**
2161
+ * Merges an array of generators into a single generator.
1892
2162
  *
1893
- * All data lives in a `Map` scoped to the storage instance and is discarded
1894
- * when the instance is garbage-collected.
2163
+ * The merged generator's `schema`, `operation`, and `operations` methods run
2164
+ * the corresponding method from each input generator in sequence, applying each
2165
+ * result via `applyHookResult`. This eliminates the need to write the loop
2166
+ * manually in each plugin.
2167
+ *
2168
+ * @param generators - Array of generators to merge into a single generator.
1895
2169
  *
1896
2170
  * @example
1897
2171
  * ```ts
1898
- * import { defineConfig, memoryStorage } from '@kubb/core'
2172
+ * const merged = mergeGenerators(generators)
1899
2173
  *
1900
- * export default defineConfig({
1901
- * input: { path: './petStore.yaml' },
1902
- * output: { path: './src/gen', storage: memoryStorage() },
1903
- * })
2174
+ * return {
2175
+ * name: pluginName,
2176
+ * schema: merged.schema,
2177
+ * operation: merged.operation,
2178
+ * operations: merged.operations,
2179
+ * }
1904
2180
  * ```
1905
2181
  */
1906
- const memoryStorage = defineStorage(() => {
1907
- const store = /* @__PURE__ */ new Map();
2182
+ function mergeGenerators(generators) {
1908
2183
  return {
1909
- name: "memory",
1910
- async hasItem(key) {
1911
- return store.has(key);
1912
- },
1913
- async getItem(key) {
1914
- return store.get(key) ?? null;
2184
+ name: generators.length > 0 ? generators.map((g) => g.name).join("+") : "merged",
2185
+ async schema(node, options) {
2186
+ for (const gen of generators) {
2187
+ if (!gen.schema) continue;
2188
+ await applyHookResult(await gen.schema.call(this, node, options), this.fabric);
2189
+ }
1915
2190
  },
1916
- async setItem(key, value) {
1917
- store.set(key, value);
2191
+ async operation(node, options) {
2192
+ for (const gen of generators) {
2193
+ if (!gen.operation) continue;
2194
+ await applyHookResult(await gen.operation.call(this, node, options), this.fabric);
2195
+ }
2196
+ },
2197
+ async operations(nodes, options) {
2198
+ for (const gen of generators) {
2199
+ if (!gen.operations) continue;
2200
+ await applyHookResult(await gen.operations.call(this, nodes, options), this.fabric);
2201
+ }
2202
+ }
2203
+ };
2204
+ }
2205
+ //#endregion
2206
+ //#region src/defineLogger.ts
2207
+ /**
2208
+ * Wraps a logger definition into a typed {@link Logger}.
2209
+ *
2210
+ * @example
2211
+ * export const myLogger = defineLogger({
2212
+ * name: 'my-logger',
2213
+ * install(context, options) {
2214
+ * context.on('info', (message) => console.log('ℹ', message))
2215
+ * context.on('error', (error) => console.error('✗', error.message))
2216
+ * },
2217
+ * })
2218
+ */
2219
+ function defineLogger(logger) {
2220
+ return logger;
2221
+ }
2222
+ //#endregion
2223
+ //#region src/definePresets.ts
2224
+ /**
2225
+ * Creates a typed presets registry object — a named collection of {@link Preset} entries.
2226
+ *
2227
+ * @example
2228
+ * import { definePreset, definePresets } from '@kubb/core'
2229
+ * import { resolverTsLegacy } from '@kubb/plugin-ts'
2230
+ *
2231
+ * export const myPresets = definePresets({
2232
+ * kubbV4: definePreset('kubbV4', { resolvers: [resolverTsLegacy] }),
2233
+ * })
2234
+ */
2235
+ function definePresets(presets) {
2236
+ return presets;
2237
+ }
2238
+ //#endregion
2239
+ //#region src/defineResolver.ts
2240
+ /**
2241
+ * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).
2242
+ */
2243
+ function matchesOperationPattern(node, type, pattern) {
2244
+ switch (type) {
2245
+ case "tag": return node.tags.some((tag) => !!tag.match(pattern));
2246
+ case "operationId": return !!node.operationId.match(pattern);
2247
+ case "path": return !!node.path.match(pattern);
2248
+ case "method": return !!node.method.toLowerCase().match(pattern);
2249
+ case "contentType": return !!node.requestBody?.contentType?.match(pattern);
2250
+ default: return false;
2251
+ }
2252
+ }
2253
+ /**
2254
+ * Checks if a schema matches a pattern for a given filter type (`schemaName`).
2255
+ *
2256
+ * Returns `null` when the filter type doesn't apply to schemas.
2257
+ */
2258
+ function matchesSchemaPattern(node, type, pattern) {
2259
+ switch (type) {
2260
+ case "schemaName": return node.name ? !!node.name.match(pattern) : false;
2261
+ default: return null;
2262
+ }
2263
+ }
2264
+ /**
2265
+ * Default name resolver used by `defineResolver`.
2266
+ *
2267
+ * - `camelCase` for `function` and `file` types.
2268
+ * - `PascalCase` for `type`.
2269
+ * - `camelCase` for everything else.
2270
+ */
2271
+ function defaultResolver(name, type) {
2272
+ let resolvedName = camelCase(name);
2273
+ if (type === "file" || type === "function") resolvedName = camelCase(name, { isFile: type === "file" });
2274
+ if (type === "type") resolvedName = pascalCase(name);
2275
+ return resolvedName;
2276
+ }
2277
+ /**
2278
+ * Default option resolver — applies include/exclude filters and merges matching override options.
2279
+ *
2280
+ * Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule.
2281
+ *
2282
+ * @example Include/exclude filtering
2283
+ * ```ts
2284
+ * const options = defaultResolveOptions(operationNode, {
2285
+ * options: { output: 'types' },
2286
+ * exclude: [{ type: 'tag', pattern: 'internal' }],
2287
+ * })
2288
+ * // → null when node has tag 'internal'
2289
+ * ```
2290
+ *
2291
+ * @example Override merging
2292
+ * ```ts
2293
+ * const options = defaultResolveOptions(operationNode, {
2294
+ * options: { enumType: 'asConst' },
2295
+ * override: [{ type: 'operationId', pattern: 'listPets', options: { enumType: 'enum' } }],
2296
+ * })
2297
+ * // → { enumType: 'enum' } when operationId matches
2298
+ * ```
2299
+ */
2300
+ function defaultResolveOptions(node, { options, exclude = [], include, override = [] }) {
2301
+ if (isOperationNode(node)) {
2302
+ if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
2303
+ if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
2304
+ const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options;
2305
+ return {
2306
+ ...options,
2307
+ ...overrideOptions
2308
+ };
2309
+ }
2310
+ if (isSchemaNode(node)) {
2311
+ if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null;
2312
+ if (include) {
2313
+ const applicable = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)).filter((r) => r !== null);
2314
+ if (applicable.length > 0 && !applicable.includes(true)) return null;
2315
+ }
2316
+ const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options;
2317
+ return {
2318
+ ...options,
2319
+ ...overrideOptions
2320
+ };
2321
+ }
2322
+ return options;
2323
+ }
2324
+ /**
2325
+ * Default path resolver used by `defineResolver`.
2326
+ *
2327
+ * - Returns the output directory in `single` mode.
2328
+ * - Resolves into a tag- or path-based subdirectory when `group` and a `tag`/`path` value are provided.
2329
+ * - Falls back to a flat `output/baseName` path otherwise.
2330
+ *
2331
+ * A custom `group.name` function overrides the default subdirectory naming.
2332
+ * For `tag` groups the default is `${camelCase(tag)}Controller`.
2333
+ * For `path` groups the default is the first path segment after `/`.
2334
+ *
2335
+ * @example Flat output
2336
+ * ```ts
2337
+ * defaultResolvePath({ baseName: 'petTypes.ts' }, { root: '/src', output: { path: 'types' } })
2338
+ * // → '/src/types/petTypes.ts'
2339
+ * ```
2340
+ *
2341
+ * @example Tag-based grouping
2342
+ * ```ts
2343
+ * defaultResolvePath(
2344
+ * { baseName: 'petTypes.ts', tag: 'pets' },
2345
+ * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
2346
+ * )
2347
+ * // → '/src/types/petsController/petTypes.ts'
2348
+ * ```
2349
+ *
2350
+ * @example Path-based grouping
2351
+ * ```ts
2352
+ * defaultResolvePath(
2353
+ * { baseName: 'petTypes.ts', path: '/pets/list' },
2354
+ * { root: '/src', output: { path: 'types' }, group: { type: 'path' } },
2355
+ * )
2356
+ * // → '/src/types/pets/petTypes.ts'
2357
+ * ```
2358
+ *
2359
+ * @example Single-file mode
2360
+ * ```ts
2361
+ * defaultResolvePath(
2362
+ * { baseName: 'petTypes.ts', pathMode: 'single' },
2363
+ * { root: '/src', output: { path: 'types' } },
2364
+ * )
2365
+ * // → '/src/types'
2366
+ * ```
2367
+ */
2368
+ function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root, output, group }) {
2369
+ if ((pathMode ?? getMode(path.resolve(root, output.path))) === "single") return path.resolve(root, output.path);
2370
+ if (group && (groupPath || tag)) return path.resolve(root, output.path, group.name({ group: group.type === "path" ? groupPath : tag }), baseName);
2371
+ return path.resolve(root, output.path, baseName);
2372
+ }
2373
+ /**
2374
+ * Default file resolver used by `defineResolver`.
2375
+ *
2376
+ * Resolves a `FabricFile.File` by combining name resolution (`resolver.default`) with
2377
+ * path resolution (`resolver.resolvePath`). The resolved file always has empty
2378
+ * `sources`, `imports`, and `exports` arrays — consumers populate those separately.
2379
+ *
2380
+ * In `single` mode the name is omitted and the file sits directly in the output directory.
2381
+ *
2382
+ * @example Resolve a schema file
2383
+ * ```ts
2384
+ * const file = defaultResolveFile.call(resolver,
2385
+ * { name: 'pet', extname: '.ts' },
2386
+ * { root: '/src', output: { path: 'types' } },
2387
+ * )
2388
+ * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
2389
+ * ```
2390
+ *
2391
+ * @example Resolve an operation file with tag grouping
2392
+ * ```ts
2393
+ * const file = defaultResolveFile.call(resolver,
2394
+ * { name: 'listPets', extname: '.ts', tag: 'pets' },
2395
+ * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
2396
+ * )
2397
+ * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
2398
+ * ```
2399
+ */
2400
+ function defaultResolveFile({ name, extname, tag, path: groupPath }, context) {
2401
+ const pathMode = getMode(path.resolve(context.root, context.output.path));
2402
+ const baseName = `${pathMode === "single" ? "" : this.default(name, "file")}${extname}`;
2403
+ const filePath = this.resolvePath({
2404
+ baseName,
2405
+ pathMode,
2406
+ tag,
2407
+ path: groupPath
2408
+ }, context);
2409
+ return {
2410
+ path: filePath,
2411
+ baseName: path.basename(filePath),
2412
+ meta: { pluginName: this.pluginName },
2413
+ sources: [],
2414
+ imports: [],
2415
+ exports: []
2416
+ };
2417
+ }
2418
+ /**
2419
+ * Generates the default "Generated by Kubb" banner from config and optional node metadata.
2420
+ */
2421
+ function buildDefaultBanner({ title, description, version, config }) {
2422
+ try {
2423
+ let source = "";
2424
+ if (Array.isArray(config.input)) {
2425
+ const first = config.input[0];
2426
+ if (first && "path" in first) source = path.basename(first.path);
2427
+ } else if ("path" in config.input) source = path.basename(config.input.path);
2428
+ else if ("data" in config.input) source = "text content";
2429
+ let banner = "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n";
2430
+ if (config.output.defaultBanner === "simple") {
2431
+ banner += "*/\n";
2432
+ return banner;
2433
+ }
2434
+ if (source) banner += `* Source: ${source}\n`;
2435
+ if (title) banner += `* Title: ${title}\n`;
2436
+ if (description) {
2437
+ const formattedDescription = description.replace(/\n/gm, "\n* ");
2438
+ banner += `* Description: ${formattedDescription}\n`;
2439
+ }
2440
+ if (version) banner += `* OpenAPI spec version: ${version}\n`;
2441
+ banner += "*/\n";
2442
+ return banner;
2443
+ } catch (_error) {
2444
+ return "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n*/";
2445
+ }
2446
+ }
2447
+ /**
2448
+ * Default banner resolver — returns the banner string for a generated file.
2449
+ *
2450
+ * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
2451
+ * When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
2452
+ * from the OAS spec when a `node` is provided).
2453
+ *
2454
+ * - When `output.banner` is a function and `node` is provided, returns `output.banner(node)`.
2455
+ * - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice.
2456
+ * - When `output.banner` is a string, returns it directly.
2457
+ * - When `config.output.defaultBanner` is `false`, returns `undefined`.
2458
+ * - Otherwise returns the Kubb "Generated by Kubb" notice.
2459
+ *
2460
+ * @example String banner overrides default
2461
+ * ```ts
2462
+ * defaultResolveBanner(undefined, { output: { banner: '// my banner' }, config })
2463
+ * // → '// my banner'
2464
+ * ```
2465
+ *
2466
+ * @example Function banner with node
2467
+ * ```ts
2468
+ * defaultResolveBanner(rootNode, { output: { banner: (node) => `// v${node.version}` }, config })
2469
+ * // → '// v3.0.0'
2470
+ * ```
2471
+ *
2472
+ * @example No user banner — Kubb notice with OAS metadata
2473
+ * ```ts
2474
+ * defaultResolveBanner(rootNode, { config })
2475
+ * // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
2476
+ * ```
2477
+ *
2478
+ * @example Disabled default banner
2479
+ * ```ts
2480
+ * defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })
2481
+ * // → undefined
2482
+ * ```
2483
+ */
2484
+ function defaultResolveBanner(node, { output, config }) {
2485
+ if (typeof output?.banner === "function") return output.banner(node);
2486
+ if (typeof output?.banner === "string") return output.banner;
2487
+ if (config.output.defaultBanner === false) return;
2488
+ return buildDefaultBanner({
2489
+ title: node?.meta?.title,
2490
+ version: node?.meta?.version,
2491
+ config
2492
+ });
2493
+ }
2494
+ /**
2495
+ * Default footer resolver — returns the footer string for a generated file.
2496
+ *
2497
+ * - When `output.footer` is a function and `node` is provided, calls it with the node.
2498
+ * - When `output.footer` is a function and `node` is absent, returns `undefined`.
2499
+ * - When `output.footer` is a string, returns it directly.
2500
+ * - Otherwise returns `undefined`.
2501
+ *
2502
+ * @example String footer
2503
+ * ```ts
2504
+ * defaultResolveFooter(undefined, { output: { footer: '// end of file' }, config })
2505
+ * // → '// end of file'
2506
+ * ```
2507
+ *
2508
+ * @example Function footer with node
2509
+ * ```ts
2510
+ * defaultResolveFooter(rootNode, { output: { footer: (node) => `// ${node.title}` }, config })
2511
+ * // → '// Pet Store'
2512
+ * ```
2513
+ */
2514
+ function defaultResolveFooter(node, { output }) {
2515
+ if (typeof output?.footer === "function") return node ? output.footer(node) : void 0;
2516
+ if (typeof output?.footer === "string") return output.footer;
2517
+ }
2518
+ /**
2519
+ * Defines a resolver for a plugin, injecting built-in defaults for name casing,
2520
+ * include/exclude/override filtering, path resolution, and file construction.
2521
+ *
2522
+ * All four defaults can be overridden by providing them in the builder function:
2523
+ * - `default` — name casing strategy (camelCase / PascalCase)
2524
+ * - `resolveOptions` — include/exclude/override filtering
2525
+ * - `resolvePath` — output path computation
2526
+ * - `resolveFile` — full `FabricFile.File` construction
2527
+ *
2528
+ * Methods in the builder have access to `this` (the full resolver object), so they
2529
+ * can call other resolver methods without circular imports.
2530
+ *
2531
+ * @example Basic resolver with naming helpers
2532
+ * ```ts
2533
+ * export const resolver = defineResolver<PluginTs>(() => ({
2534
+ * name: 'default',
2535
+ * resolveName(node) {
2536
+ * return this.default(node.name, 'function')
2537
+ * },
2538
+ * resolveTypedName(node) {
2539
+ * return this.default(node.name, 'type')
2540
+ * },
2541
+ * }))
2542
+ * ```
2543
+ *
2544
+ * @example Override resolvePath for a custom output structure
2545
+ * ```ts
2546
+ * export const resolver = defineResolver<PluginTs>(() => ({
2547
+ * name: 'custom',
2548
+ * resolvePath({ baseName }, { root, output }) {
2549
+ * return path.resolve(root, output.path, 'generated', baseName)
2550
+ * },
2551
+ * }))
2552
+ * ```
2553
+ *
2554
+ * @example Use this.default inside a helper
2555
+ * ```ts
2556
+ * export const resolver = defineResolver<PluginTs>(() => ({
2557
+ * name: 'default',
2558
+ * resolveParamName(node, param) {
2559
+ * return this.default(`${node.operationId} ${param.in} ${param.name}`, 'type')
2560
+ * },
2561
+ * }))
2562
+ * ```
2563
+ */
2564
+ function defineResolver(build) {
2565
+ return {
2566
+ default: defaultResolver,
2567
+ resolveOptions: defaultResolveOptions,
2568
+ resolvePath: defaultResolvePath,
2569
+ resolveFile: defaultResolveFile,
2570
+ resolveBanner: defaultResolveBanner,
2571
+ resolveFooter: defaultResolveFooter,
2572
+ ...build()
2573
+ };
2574
+ }
2575
+ //#endregion
2576
+ //#region src/storages/memoryStorage.ts
2577
+ /**
2578
+ * In-memory storage driver. Useful for testing and dry-run scenarios where
2579
+ * generated output should be captured without touching the filesystem.
2580
+ *
2581
+ * All data lives in a `Map` scoped to the storage instance and is discarded
2582
+ * when the instance is garbage-collected.
2583
+ *
2584
+ * @example
2585
+ * ```ts
2586
+ * import { defineConfig, memoryStorage } from '@kubb/core'
2587
+ *
2588
+ * export default defineConfig({
2589
+ * input: { path: './petStore.yaml' },
2590
+ * output: { path: './src/gen', storage: memoryStorage() },
2591
+ * })
2592
+ * ```
2593
+ */
2594
+ const memoryStorage = createStorage(() => {
2595
+ const store = /* @__PURE__ */ new Map();
2596
+ return {
2597
+ name: "memory",
2598
+ async hasItem(key) {
2599
+ return store.has(key);
2600
+ },
2601
+ async getItem(key) {
2602
+ return store.get(key) ?? null;
2603
+ },
2604
+ async setItem(key, value) {
2605
+ store.set(key, value);
1918
2606
  },
1919
2607
  async removeItem(key) {
1920
2608
  store.delete(key);
@@ -1935,7 +2623,7 @@ const memoryStorage = defineStorage(() => {
1935
2623
  //#endregion
1936
2624
  //#region src/utils/FunctionParams.ts
1937
2625
  /**
1938
- * @deprecated
2626
+ * @deprecated use ast package instead
1939
2627
  */
1940
2628
  var FunctionParams = class FunctionParams {
1941
2629
  #items = [];
@@ -2011,14 +2699,10 @@ var FunctionParams = class FunctionParams {
2011
2699
  //#endregion
2012
2700
  //#region src/utils/formatters.ts
2013
2701
  /**
2014
- * Check if a formatter command is available in the system.
2702
+ * Returns `true` when the given formatter is installed and callable.
2015
2703
  *
2016
- * @param formatter - The formatter to check ('biome', 'prettier', or 'oxfmt')
2017
- * @returns Promise that resolves to true if the formatter is available, false otherwise
2018
- *
2019
- * @remarks
2020
- * This function checks availability by running `<formatter> --version` command.
2021
- * All supported formatters (biome, prettier, oxfmt) implement the --version flag.
2704
+ * Availability is detected by running `<formatter> --version` and checking
2705
+ * that the process exits without error.
2022
2706
  */
2023
2707
  async function isFormatterAvailable(formatter) {
2024
2708
  try {
@@ -2029,263 +2713,93 @@ async function isFormatterAvailable(formatter) {
2029
2713
  }
2030
2714
  }
2031
2715
  /**
2032
- * Detect which formatter is available in the system.
2033
- *
2034
- * @returns Promise that resolves to the first available formatter or undefined if none are found
2716
+ * Detects the first available code formatter on the current system.
2035
2717
  *
2036
- * @remarks
2037
- * Checks in order of preference: biome, oxfmt, prettier.
2038
- * Uses the `--version` flag to detect if each formatter command is available.
2039
- * This is a reliable method as all supported formatters implement this flag.
2718
+ * - Checks in preference order: `biome`, `oxfmt`, `prettier`.
2719
+ * - Returns `null` when none are found.
2040
2720
  *
2041
2721
  * @example
2042
- * ```typescript
2722
+ * ```ts
2043
2723
  * const formatter = await detectFormatter()
2044
2724
  * if (formatter) {
2045
2725
  * console.log(`Using ${formatter} for formatting`)
2046
- * } else {
2047
- * console.log('No formatter found')
2048
2726
  * }
2049
2727
  * ```
2050
2728
  */
2051
2729
  async function detectFormatter() {
2052
- for (const formatter of [
2730
+ const formatterNames = new Set([
2053
2731
  "biome",
2054
2732
  "oxfmt",
2055
2733
  "prettier"
2056
- ]) if (await isFormatterAvailable(formatter)) return formatter;
2734
+ ]);
2735
+ for (const formatter of formatterNames) if (await isFormatterAvailable(formatter)) return formatter;
2736
+ return null;
2057
2737
  }
2058
2738
  //#endregion
2059
- //#region src/utils/TreeNode.ts
2060
- var TreeNode = class TreeNode {
2061
- data;
2062
- parent;
2063
- children = [];
2064
- #cachedLeaves = void 0;
2065
- constructor(data, parent) {
2066
- this.data = data;
2067
- this.parent = parent;
2068
- }
2069
- addChild(data) {
2070
- const child = new TreeNode(data, this);
2071
- if (!this.children) this.children = [];
2072
- this.children.push(child);
2073
- return child;
2074
- }
2075
- get root() {
2076
- if (!this.parent) return this;
2077
- return this.parent.root;
2078
- }
2079
- get leaves() {
2080
- if (!this.children || this.children.length === 0) return [this];
2081
- if (this.#cachedLeaves) return this.#cachedLeaves;
2082
- const leaves = [];
2083
- for (const child of this.children) leaves.push(...child.leaves);
2084
- this.#cachedLeaves = leaves;
2085
- return leaves;
2086
- }
2087
- forEach(callback) {
2088
- if (typeof callback !== "function") throw new TypeError("forEach() callback must be a function");
2089
- callback(this);
2090
- for (const child of this.children) child.forEach(callback);
2091
- return this;
2092
- }
2093
- findDeep(predicate) {
2094
- if (typeof predicate !== "function") throw new TypeError("find() predicate must be a function");
2095
- return this.leaves.find(predicate);
2096
- }
2097
- forEachDeep(callback) {
2098
- if (typeof callback !== "function") throw new TypeError("forEach() callback must be a function");
2099
- this.leaves.forEach(callback);
2100
- }
2101
- filterDeep(callback) {
2102
- if (typeof callback !== "function") throw new TypeError("filter() callback must be a function");
2103
- return this.leaves.filter(callback);
2104
- }
2105
- mapDeep(callback) {
2106
- if (typeof callback !== "function") throw new TypeError("map() callback must be a function");
2107
- return this.leaves.map(callback);
2108
- }
2109
- static build(files, root) {
2110
- try {
2111
- const filteredTree = buildDirectoryTree(files, root);
2112
- if (!filteredTree) return null;
2113
- const treeNode = new TreeNode({
2114
- name: filteredTree.name,
2115
- path: filteredTree.path,
2116
- file: filteredTree.file,
2117
- type: getMode(filteredTree.path)
2118
- });
2119
- const recurse = (node, item) => {
2120
- const subNode = node.addChild({
2121
- name: item.name,
2122
- path: item.path,
2123
- file: item.file,
2124
- type: getMode(item.path)
2125
- });
2126
- if (item.children?.length) item.children?.forEach((child) => {
2127
- recurse(subNode, child);
2128
- });
2129
- };
2130
- filteredTree.children?.forEach((child) => {
2131
- recurse(treeNode, child);
2132
- });
2133
- return treeNode;
2134
- } catch (error) {
2135
- throw new Error("Something went wrong with creating barrel files with the TreeNode class", { cause: error });
2136
- }
2137
- }
2138
- };
2139
- const normalizePath = (p) => p.replaceAll("\\", "/");
2140
- function buildDirectoryTree(files, rootFolder = "") {
2141
- const normalizedRootFolder = normalizePath(rootFolder);
2142
- const rootPrefix = normalizedRootFolder.endsWith("/") ? normalizedRootFolder : `${normalizedRootFolder}/`;
2143
- const filteredFiles = files.filter((file) => {
2144
- const normalizedFilePath = normalizePath(file.path);
2145
- return rootFolder ? normalizedFilePath.startsWith(rootPrefix) && !normalizedFilePath.endsWith(".json") : !normalizedFilePath.endsWith(".json");
2146
- });
2147
- if (filteredFiles.length === 0) return null;
2148
- const root = {
2149
- name: rootFolder || "",
2150
- path: rootFolder || "",
2151
- children: []
2152
- };
2153
- filteredFiles.forEach((file) => {
2154
- const parts = file.path.slice(rootFolder.length).split("/").filter(Boolean);
2155
- let currentLevel = root.children;
2156
- let currentPath = normalizePath(rootFolder);
2157
- parts.forEach((part, index) => {
2158
- currentPath = path.posix.join(currentPath, part);
2159
- let existingNode = currentLevel.find((node) => node.name === part);
2160
- if (!existingNode) {
2161
- if (index === parts.length - 1) existingNode = {
2162
- name: part,
2163
- file,
2164
- path: currentPath
2165
- };
2166
- else existingNode = {
2167
- name: part,
2168
- path: currentPath,
2169
- children: []
2170
- };
2171
- currentLevel.push(existingNode);
2172
- }
2173
- if (!existingNode.file) currentLevel = existingNode.children;
2174
- });
2175
- });
2176
- return root;
2177
- }
2178
- //#endregion
2179
- //#region src/BarrelManager.ts
2180
- /** biome-ignore-all lint/suspicious/useIterableCallbackReturn: not needed */
2181
- var BarrelManager = class {
2182
- getFiles({ files: generatedFiles, root }) {
2183
- const cachedFiles = /* @__PURE__ */ new Map();
2184
- TreeNode.build(generatedFiles, root)?.forEach((treeNode) => {
2185
- if (!treeNode || !treeNode.children || !treeNode.parent?.data.path) return;
2186
- const barrelFile = {
2187
- path: join(treeNode.parent?.data.path, "index.ts"),
2188
- baseName: "index.ts",
2189
- exports: [],
2190
- imports: [],
2191
- sources: []
2192
- };
2193
- const previousBarrelFile = cachedFiles.get(barrelFile.path);
2194
- treeNode.leaves.forEach((item) => {
2195
- if (!item.data.name) return;
2196
- (item.data.file?.sources || []).forEach((source) => {
2197
- if (!item.data.file?.path || !source.isIndexable || !source.name) return;
2198
- if (previousBarrelFile?.sources.some((item) => item.name === source.name && item.isTypeOnly === source.isTypeOnly)) return;
2199
- barrelFile.exports.push({
2200
- name: [source.name],
2201
- path: getRelativePath(treeNode.parent?.data.path, item.data.path),
2202
- isTypeOnly: source.isTypeOnly
2203
- });
2204
- barrelFile.sources.push({
2205
- name: source.name,
2206
- isTypeOnly: source.isTypeOnly,
2207
- value: "",
2208
- isExportable: false,
2209
- isIndexable: false
2210
- });
2211
- });
2212
- });
2213
- if (previousBarrelFile) {
2214
- previousBarrelFile.sources.push(...barrelFile.sources);
2215
- previousBarrelFile.exports?.push(...barrelFile.exports || []);
2216
- } else cachedFiles.set(barrelFile.path, barrelFile);
2217
- });
2218
- return [...cachedFiles.values()];
2219
- }
2220
- };
2221
- //#endregion
2222
- //#region src/utils/getBarrelFiles.ts
2223
- function trimExtName(text) {
2224
- const dotIndex = text.lastIndexOf(".");
2225
- if (dotIndex > 0 && !text.includes("/", dotIndex)) return text.slice(0, dotIndex);
2226
- return text;
2227
- }
2228
- async function getBarrelFiles(files, { type, meta = {}, root, output }) {
2229
- if (!type || type === "propagate") return [];
2230
- const barrelManager = new BarrelManager();
2231
- const pathToBuildFrom = join(root, output.path);
2232
- if (trimExtName(pathToBuildFrom).endsWith("index")) return [];
2233
- const barrelFiles = barrelManager.getFiles({
2234
- files,
2235
- root: pathToBuildFrom,
2236
- meta
2237
- });
2238
- if (type === "all") return barrelFiles.map((file) => {
2239
- return {
2240
- ...file,
2241
- exports: file.exports?.map((exportItem) => {
2242
- return {
2243
- ...exportItem,
2244
- name: void 0
2245
- };
2246
- })
2247
- };
2248
- });
2249
- return barrelFiles.map((indexFile) => {
2250
- return {
2251
- ...indexFile,
2252
- meta
2253
- };
2254
- });
2739
+ //#region src/utils/getConfigs.ts
2740
+ /**
2741
+ * Resolves a {@link ConfigInput} into a normalized array of {@link Config} objects.
2742
+ *
2743
+ * - Awaits the config when it is a `Promise`.
2744
+ * - Calls the factory function with `args` when the config is a function.
2745
+ * - Wraps a single config object in an array for uniform downstream handling.
2746
+ */
2747
+ async function getConfigs(config, args) {
2748
+ const resolved = await (typeof config === "function" ? config(args) : config);
2749
+ return (Array.isArray(resolved) ? resolved : [resolved]).map((item) => ({
2750
+ plugins: [],
2751
+ ...item
2752
+ }));
2255
2753
  }
2256
2754
  //#endregion
2257
- //#region src/utils/getPlugins.ts
2258
- function isJSONPlugins(plugins) {
2259
- return Array.isArray(plugins) && plugins.some((plugin) => Array.isArray(plugin) && typeof plugin[0] === "string");
2260
- }
2261
- function isObjectPlugins(plugins) {
2262
- return plugins instanceof Object && !Array.isArray(plugins);
2263
- }
2264
- function getPlugins(plugins) {
2265
- if (isObjectPlugins(plugins)) throw new Error("Object plugins are not supported anymore, best to use http://kubb.dev/getting-started/configure#json");
2266
- if (isJSONPlugins(plugins)) throw new Error("JSON plugins are not supported anymore, best to use http://kubb.dev/getting-started/configure#json");
2267
- return Promise.resolve(plugins);
2755
+ //#region src/utils/getPreset.ts
2756
+ /**
2757
+ * Returns a copy of `defaults` where each function in `userOverrides` is wrapped
2758
+ * so a `null`/`undefined` return falls back to the original. Non-function values
2759
+ * are assigned directly. All calls use the merged object as `this`.
2760
+ */
2761
+ function withFallback(defaults, userOverrides) {
2762
+ const merged = { ...defaults };
2763
+ for (const key of Object.keys(userOverrides)) {
2764
+ const userVal = userOverrides[key];
2765
+ const defaultVal = defaults[key];
2766
+ if (typeof userVal === "function" && typeof defaultVal === "function") merged[key] = (...args) => userVal.apply(merged, args) ?? defaultVal.apply(merged, args);
2767
+ else if (userVal !== void 0) merged[key] = userVal;
2768
+ }
2769
+ return merged;
2268
2770
  }
2269
- //#endregion
2270
- //#region src/utils/getConfigs.ts
2271
2771
  /**
2272
- * Converting UserConfig to Config Array without a change in the object beside the JSON convert.
2772
+ * Resolves a named preset into a resolver, transformer, and generators.
2773
+ *
2774
+ * - Selects the preset resolver; wraps it with user overrides using null/undefined fallback.
2775
+ * - Composes the preset's transformers into a single visitor; wraps it with the user transformer using null/undefined fallback.
2776
+ * - Combines preset generators with user-supplied generators; falls back to the `default` preset's generators when neither provides any.
2273
2777
  */
2274
- async function getConfigs(config, args) {
2275
- let userConfigs = await (typeof config === "function" ? Promise.resolve(config(args)) : Promise.resolve(config));
2276
- if (!Array.isArray(userConfigs)) userConfigs = [userConfigs];
2277
- const results = [];
2278
- for (const item of userConfigs) {
2279
- const plugins = item.plugins ? await getPlugins(item.plugins) : void 0;
2280
- results.push({
2281
- ...item,
2282
- plugins
2283
- });
2284
- }
2285
- return results;
2778
+ function getPreset(params) {
2779
+ const { preset: presetName, presets, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = params;
2780
+ const preset = presets[presetName];
2781
+ const presetResolver = preset?.resolver ?? presets["default"].resolver;
2782
+ const resolver = userResolver ? withFallback(presetResolver, userResolver) : presetResolver;
2783
+ const presetTransformers = preset?.transformers ?? [];
2784
+ const presetTransformer = presetTransformers.length > 0 ? composeTransformers$1(...presetTransformers) : void 0;
2785
+ const transformer = presetTransformer && userTransformer ? withFallback(presetTransformer, userTransformer) : userTransformer ?? presetTransformer;
2786
+ const presetGenerators = preset?.generators ?? [];
2787
+ const defaultGenerators = presets["default"]?.generators ?? [];
2788
+ return {
2789
+ resolver,
2790
+ transformer,
2791
+ generators: presetGenerators.length > 0 || userGenerators.length > 0 ? [...presetGenerators, ...userGenerators] : defaultGenerators,
2792
+ preset
2793
+ };
2286
2794
  }
2287
2795
  //#endregion
2288
2796
  //#region src/utils/linters.ts
2797
+ /**
2798
+ * Returns `true` when the given linter is installed and callable.
2799
+ *
2800
+ * Availability is detected by running `<linter> --version` and checking
2801
+ * that the process exits without error.
2802
+ */
2289
2803
  async function isLinterAvailable(linter) {
2290
2804
  try {
2291
2805
  await x(linter, ["--version"], { nodeOptions: { stdio: "ignore" } });
@@ -2294,14 +2808,73 @@ async function isLinterAvailable(linter) {
2294
2808
  return false;
2295
2809
  }
2296
2810
  }
2811
+ /**
2812
+ * Detects the first available linter on the current system.
2813
+ *
2814
+ * - Checks in preference order: `biome`, `oxlint`, `eslint`.
2815
+ * - Returns `null` when none are found.
2816
+ *
2817
+ * @example
2818
+ * ```ts
2819
+ * const linter = await detectLinter()
2820
+ * if (linter) {
2821
+ * console.log(`Using ${linter} for linting`)
2822
+ * }
2823
+ * ```
2824
+ */
2297
2825
  async function detectLinter() {
2298
- for (const linter of [
2826
+ const linterNames = new Set([
2299
2827
  "biome",
2300
2828
  "oxlint",
2301
2829
  "eslint"
2302
- ]) if (await isLinterAvailable(linter)) return linter;
2830
+ ]);
2831
+ for (const linter of linterNames) if (await isLinterAvailable(linter)) return linter;
2832
+ return null;
2833
+ }
2834
+ //#endregion
2835
+ //#region src/utils/packageJSON.ts
2836
+ function getPackageJSONSync(cwd) {
2837
+ const pkgPath = pkg.up({ cwd });
2838
+ if (!pkgPath) return null;
2839
+ return JSON.parse(readSync(pkgPath));
2840
+ }
2841
+ function match(packageJSON, dependency) {
2842
+ const dependencies = {
2843
+ ...packageJSON.dependencies || {},
2844
+ ...packageJSON.devDependencies || {}
2845
+ };
2846
+ if (typeof dependency === "string" && dependencies[dependency]) return dependencies[dependency];
2847
+ const matched = Object.keys(dependencies).find((dep) => dep.match(dependency));
2848
+ return matched ? dependencies[matched] ?? null : null;
2849
+ }
2850
+ function getVersionSync(dependency, cwd) {
2851
+ const packageJSON = getPackageJSONSync(cwd);
2852
+ return packageJSON ? match(packageJSON, dependency) : null;
2853
+ }
2854
+ /**
2855
+ * Returns `true` when the nearest `package.json` declares a dependency that
2856
+ * satisfies the given semver range.
2857
+ *
2858
+ * - Searches both `dependencies` and `devDependencies`.
2859
+ * - Accepts a string package name or a `RegExp` to match scoped/pattern packages.
2860
+ * - Uses `semver.satisfies` for range comparison; returns `false` when the
2861
+ * version string cannot be coerced into a valid semver.
2862
+ *
2863
+ * @example
2864
+ * ```ts
2865
+ * satisfiesDependency('react', '>=18') // true when react@18.x is installed
2866
+ * satisfiesDependency(/^@tanstack\//, '>=5') // true when any @tanstack/* >=5 is found
2867
+ * ```
2868
+ */
2869
+ function satisfiesDependency(dependency, version, cwd) {
2870
+ const packageVersion = getVersionSync(dependency, cwd);
2871
+ if (!packageVersion) return false;
2872
+ if (packageVersion === version) return true;
2873
+ const semVer = coerce(packageVersion);
2874
+ if (!semVer) return false;
2875
+ return satisfies(semVer, version);
2303
2876
  }
2304
2877
  //#endregion
2305
- export { AsyncEventEmitter, FunctionParams, PackageManager, PluginManager, PromiseManager, URLPath, build, build as default, defineAdapter, defineConfig, defineLogger, definePlugin, definePrinter, defineStorage, detectFormatter, detectLinter, formatters, fsStorage, getBarrelFiles, getConfigs, getMode, isInputPath, linters, logLevel, memoryStorage, safeBuild, setup };
2878
+ export { AsyncEventEmitter, FunctionParams, PluginDriver, URLPath, build, build as default, buildDefaultBanner, composeTransformers, createAdapter, createPlugin, createStorage, defaultResolveBanner, defaultResolveFile, defaultResolveFooter, defaultResolveOptions, defaultResolvePath, defineConfig, defineGenerator, defineLogger, definePresets, definePrinter, defineResolver, detectFormatter, detectLinter, formatters, fsStorage, getBarrelFiles, getConfigs, getMode, getPreset, isInputPath, linters, logLevel, memoryStorage, mergeGenerators, safeBuild, satisfiesDependency, setup };
2306
2879
 
2307
2880
  //# sourceMappingURL=index.js.map