@kubb/core 5.0.0-alpha.33 → 5.0.0-alpha.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,17 +1,15 @@
1
1
  import { t as __name } from "./chunk--u3MIqq1.js";
2
2
  import { EventEmitter } from "node:events";
3
- import { readFileSync } from "node:fs";
3
+ import { existsSync, readFileSync } from "node:fs";
4
4
  import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
5
  import path, { basename, dirname, extname, join, posix, resolve } from "node:path";
6
- import { composeTransformers, composeTransformers as composeTransformers$1, createExport, createFile, createSource, definePrinter, extractStringsFromNodes, isOperationNode, isSchemaNode, transform, walk } from "@kubb/ast";
6
+ import * as ast from "@kubb/ast";
7
+ import { composeTransformers, createExport, createFile, createSource, definePrinter, extractStringsFromNodes, isOperationNode, isSchemaNode, transform, walk } from "@kubb/ast";
7
8
  import { performance } from "node:perf_hooks";
8
9
  import { deflateSync } from "fflate";
9
10
  import { x } from "tinyexec";
10
- import { createRenderer } from "@kubb/renderer-jsx";
11
- import { Fragment, jsx } from "@kubb/renderer-jsx/jsx-runtime";
12
11
  import { version } from "node:process";
13
12
  import { sortBy } from "remeda";
14
- import * as pkg from "empathic/package";
15
13
  import { coerce, satisfies } from "semver";
16
14
  //#region ../../internals/utils/src/errors.ts
17
15
  /**
@@ -129,6 +127,18 @@ var AsyncEventEmitter = class {
129
127
  this.#emitter.off(eventName, handler);
130
128
  }
131
129
  /**
130
+ * Returns the number of listeners registered for `eventName`.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * emitter.on('build', handler)
135
+ * emitter.listenerCount('build') // 1
136
+ * ```
137
+ */
138
+ listenerCount(eventName) {
139
+ return this.#emitter.listenerCount(eventName);
140
+ }
141
+ /**
132
142
  * Removes all listeners from every event channel.
133
143
  *
134
144
  * @example
@@ -234,6 +244,26 @@ function formatMs(ms) {
234
244
  //#endregion
235
245
  //#region ../../internals/utils/src/fs.ts
236
246
  /**
247
+ * Walks up the directory tree from `cwd` (defaults to `process.cwd()`) and
248
+ * returns the absolute path of the nearest `package.json`, or `null` when none
249
+ * is found before reaching the filesystem root.
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * const pkgPath = findPackageJSON('/home/user/project/src') // '/home/user/project/package.json'
254
+ * ```
255
+ */
256
+ function findPackageJSON(cwd) {
257
+ let dir = cwd ? resolve(cwd) : process.cwd();
258
+ while (true) {
259
+ const pkgPath = join(dir, "package.json");
260
+ if (existsSync(pkgPath)) return pkgPath;
261
+ const parent = dirname(dir);
262
+ if (parent === dir) return null;
263
+ dir = parent;
264
+ }
265
+ }
266
+ /**
237
267
  * Converts all backslashes to forward slashes.
238
268
  * Extended-length Windows paths (`\\?\...`) are left unchanged.
239
269
  */
@@ -722,8 +752,31 @@ const formatters = {
722
752
  }
723
753
  };
724
754
  //#endregion
755
+ //#region src/createAdapter.ts
756
+ /**
757
+ * Creates an adapter factory. Call the returned function with optional options to get the adapter instance.
758
+ *
759
+ * @example
760
+ * export const myAdapter = createAdapter<MyAdapter>((options) => {
761
+ * return {
762
+ * name: 'my-adapter',
763
+ * options,
764
+ * async parse(source) { ... },
765
+ * }
766
+ * })
767
+ *
768
+ * // instantiate
769
+ * const adapter = myAdapter({ validate: true })
770
+ */
771
+ function createAdapter(build) {
772
+ return (options) => build(options ?? {});
773
+ }
774
+ //#endregion
725
775
  //#region ../../node_modules/.pnpm/yocto-queue@1.2.2/node_modules/yocto-queue/index.js
726
- var Node = class {
776
+ var Node$1 = class {
777
+ static {
778
+ __name(this, "Node");
779
+ }
727
780
  value;
728
781
  next;
729
782
  constructor(value) {
@@ -738,7 +791,7 @@ var Queue = class {
738
791
  this.clear();
739
792
  }
740
793
  enqueue(value) {
741
- const node = new Node(value);
794
+ const node = new Node$1(value);
742
795
  if (this.#head) {
743
796
  this.#tail.next = node;
744
797
  this.#tail = node;
@@ -892,249 +945,881 @@ var FileProcessor = class {
892
945
  }
893
946
  };
894
947
  //#endregion
895
- //#region src/devtools.ts
948
+ //#region src/definePlugin.ts
896
949
  /**
897
- * Encodes an `InputNode` as a compressed, URL-safe string.
898
- *
899
- * The JSON representation is deflate-compressed with {@link deflateSync} before
900
- * base64url encoding, which typically reduces payload size by 70–80 % and
901
- * keeps URLs well within browser and server path-length limits.
950
+ * Returns `true` when `plugin` is a hook-style plugin created with `definePlugin`.
902
951
  *
903
- * Use {@link decodeAst} to reverse.
904
- */
905
- function encodeAst(input) {
906
- const compressed = deflateSync(new TextEncoder().encode(JSON.stringify(input)));
907
- return Buffer.from(compressed).toString("base64url");
908
- }
909
- /**
910
- * Constructs the Kubb Studio URL for the given `InputNode`.
911
- * When `options.ast` is `true`, navigates to the AST inspector (`/ast`).
912
- * The `input` is encoded and attached as the `?root=` query parameter so Studio
913
- * can decode and render it without a round-trip to any server.
952
+ * Used by `PluginDriver` to distinguish hook-style plugins from legacy `createPlugin` plugins
953
+ * so it can normalize them and register their handlers on the `AsyncEventEmitter`.
914
954
  */
915
- function getStudioUrl(input, studioUrl, options = {}) {
916
- return `${studioUrl.replace(/\/$/, "")}${options.ast ? "/ast" : ""}?root=${encodeAst(input)}`;
955
+ function isHookStylePlugin(plugin) {
956
+ return typeof plugin === "object" && plugin !== null && "hooks" in plugin;
917
957
  }
918
958
  /**
919
- * Opens the Kubb Studio URL for the given `InputNode` in the default browser —
959
+ * Creates a plugin factory using the new hook-style (`hooks:`) API.
920
960
  *
921
- * Falls back to printing the URL if the browser cannot be launched.
922
- */
923
- async function openInStudio(input, studioUrl, options = {}) {
924
- const url = getStudioUrl(input, studioUrl, options);
925
- const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
926
- const args = process.platform === "win32" ? [
927
- "/c",
928
- "start",
929
- "",
930
- url
931
- ] : [url];
932
- try {
933
- await x(cmd, args);
934
- } catch {
935
- console.log(`\n ${url}\n`);
936
- }
937
- }
938
- //#endregion
939
- //#region src/FileManager.ts
940
- function mergeFile(a, b) {
941
- return {
942
- ...a,
943
- sources: [...a.sources || [], ...b.sources || []],
944
- imports: [...a.imports || [], ...b.imports || []],
945
- exports: [...a.exports || [], ...b.exports || []]
946
- };
947
- }
948
- /**
949
- * In-memory file store for generated files.
961
+ * The returned factory is called with optional options and produces a `HookStylePlugin`
962
+ * that coexists with plugins created via the legacy `createPlugin` API in the same
963
+ * `kubb.config.ts`.
950
964
  *
951
- * Files with the same `path` are merged sources, imports, and exports are concatenated.
952
- * The `files` getter returns all stored files sorted by path length (shortest first).
965
+ * Lifecycle handlers are registered on the `PluginDriver`'s `AsyncEventEmitter`, enabling
966
+ * both the plugin's own handlers and external tooling (CLI, devtools) to observe every event.
953
967
  *
954
968
  * @example
955
969
  * ```ts
956
- * import { FileManager } from '@kubb/core'
957
- *
958
- * const manager = new FileManager()
959
- * manager.upsert(myFile)
960
- * console.log(manager.files) // all stored files
970
+ * // With PluginFactoryOptions (recommended for real plugins)
971
+ * export const pluginTs = definePlugin<PluginTs>((options) => ({
972
+ * name: 'plugin-ts',
973
+ * hooks: {
974
+ * 'kubb:plugin:setup'(ctx) {
975
+ * ctx.setResolver(resolverTs) // typed as Partial<ResolverTs>
976
+ * },
977
+ * },
978
+ * }))
961
979
  * ```
962
980
  */
963
- var FileManager = class {
964
- #cache = /* @__PURE__ */ new Map();
965
- #filesCache = null;
966
- /**
967
- * Adds one or more files. Files with the same path are merged — sources, imports,
968
- * and exports from all calls with the same path are concatenated together.
969
- */
970
- add(...files) {
971
- const resolvedFiles = [];
972
- const mergedFiles = /* @__PURE__ */ new Map();
973
- files.forEach((file) => {
974
- const existing = mergedFiles.get(file.path);
975
- if (existing) mergedFiles.set(file.path, mergeFile(existing, file));
976
- else mergedFiles.set(file.path, file);
977
- });
978
- for (const file of mergedFiles.values()) {
979
- const resolvedFile = createFile(file);
980
- this.#cache.set(resolvedFile.path, resolvedFile);
981
- this.#filesCache = null;
982
- resolvedFiles.push(resolvedFile);
983
- }
984
- return resolvedFiles;
985
- }
986
- /**
987
- * Adds or merges one or more files.
988
- * If a file with the same path already exists, its sources/imports/exports are merged together.
989
- */
990
- upsert(...files) {
991
- const resolvedFiles = [];
992
- const mergedFiles = /* @__PURE__ */ new Map();
993
- files.forEach((file) => {
994
- const existing = mergedFiles.get(file.path);
995
- if (existing) mergedFiles.set(file.path, mergeFile(existing, file));
996
- else mergedFiles.set(file.path, file);
997
- });
998
- for (const file of mergedFiles.values()) {
999
- const existing = this.#cache.get(file.path);
1000
- const resolvedFile = createFile(existing ? mergeFile(existing, file) : file);
1001
- this.#cache.set(resolvedFile.path, resolvedFile);
1002
- this.#filesCache = null;
1003
- resolvedFiles.push(resolvedFile);
1004
- }
1005
- return resolvedFiles;
1006
- }
1007
- getByPath(path) {
1008
- return this.#cache.get(path) ?? null;
1009
- }
1010
- deleteByPath(path) {
1011
- this.#cache.delete(path);
1012
- this.#filesCache = null;
1013
- }
1014
- clear() {
1015
- this.#cache.clear();
1016
- this.#filesCache = null;
1017
- }
1018
- /**
1019
- * All stored files, sorted by path length (shorter paths first).
1020
- * Barrel/index files (e.g. index.ts) are sorted last within each length bucket.
1021
- */
1022
- get files() {
1023
- if (this.#filesCache) return this.#filesCache;
1024
- const keys = [...this.#cache.keys()].sort((a, b) => {
1025
- if (a.length !== b.length) return a.length - b.length;
1026
- const aIsIndex = trimExtName$1(a).endsWith("index");
1027
- if (aIsIndex !== trimExtName$1(b).endsWith("index")) return aIsIndex ? 1 : -1;
1028
- return 0;
1029
- });
1030
- const files = [];
1031
- for (const key of keys) {
1032
- const file = this.#cache.get(key);
1033
- if (file) files.push(file);
1034
- }
1035
- this.#filesCache = files;
1036
- return files;
1037
- }
1038
- };
981
+ function definePlugin(factory) {
982
+ return (options) => factory(options ?? {});
983
+ }
1039
984
  //#endregion
1040
- //#region src/utils/executeStrategies.ts
985
+ //#region src/defineResolver.ts
1041
986
  /**
1042
- * Runs promise functions in sequence, threading each result into the next call.
1043
- *
1044
- * - Each function receives the accumulated state from the previous call.
1045
- * - Skips functions that return a falsy value (acts as a no-op for that step).
1046
- * - Returns an array of all individual results.
1047
- * @deprecated
987
+ * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).
1048
988
  */
1049
- function hookSeq(promises) {
1050
- return promises.filter(Boolean).reduce((promise, func) => {
1051
- if (typeof func !== "function") throw new Error("HookSeq needs a function that returns a promise `() => Promise<unknown>`");
1052
- return promise.then((state) => {
1053
- const calledFunc = func(state);
1054
- if (calledFunc) return calledFunc.then(Array.prototype.concat.bind(state));
1055
- return state;
1056
- });
1057
- }, Promise.resolve([]));
989
+ function matchesOperationPattern(node, type, pattern) {
990
+ switch (type) {
991
+ case "tag": return node.tags.some((tag) => !!tag.match(pattern));
992
+ case "operationId": return !!node.operationId.match(pattern);
993
+ case "path": return !!node.path.match(pattern);
994
+ case "method": return !!node.method.toLowerCase().match(pattern);
995
+ case "contentType": return !!node.requestBody?.contentType?.match(pattern);
996
+ default: return false;
997
+ }
1058
998
  }
1059
999
  /**
1060
- * Runs promise functions in sequence and returns the first non-null result.
1000
+ * Checks if a schema matches a pattern for a given filter type (`schemaName`).
1061
1001
  *
1062
- * - Stops as soon as `nullCheck` passes for a result (default: `!== null`).
1063
- * - Subsequent functions are skipped once a match is found.
1064
- * @deprecated
1002
+ * Returns `null` when the filter type doesn't apply to schemas.
1065
1003
  */
1066
- function hookFirst(promises, nullCheck = (state) => state !== null) {
1067
- let promise = Promise.resolve(null);
1068
- for (const func of promises.filter(Boolean)) promise = promise.then((state) => {
1069
- if (nullCheck(state)) return state;
1070
- return func(state);
1071
- });
1072
- return promise;
1004
+ function matchesSchemaPattern(node, type, pattern) {
1005
+ switch (type) {
1006
+ case "schemaName": return node.name ? !!node.name.match(pattern) : false;
1007
+ default: return null;
1008
+ }
1073
1009
  }
1074
1010
  /**
1075
- * Runs promise functions concurrently and returns all settled results.
1011
+ * Default name resolver used by `defineResolver`.
1076
1012
  *
1077
- * - Limits simultaneous executions to `concurrency` (default: unlimited).
1078
- * - Uses `Promise.allSettled` so individual failures do not cancel other tasks.
1079
- * @deprecated
1013
+ * - `camelCase` for `function` and `file` types.
1014
+ * - `PascalCase` for `type`.
1015
+ * - `camelCase` for everything else.
1080
1016
  */
1081
- function hookParallel(promises, concurrency = Number.POSITIVE_INFINITY) {
1082
- const limit = pLimit(concurrency);
1083
- const tasks = promises.filter(Boolean).map((promise) => limit(() => promise()));
1084
- return Promise.allSettled(tasks);
1017
+ function defaultResolver(name, type) {
1018
+ let resolvedName = camelCase(name);
1019
+ if (type === "file" || type === "function") resolvedName = camelCase(name, { isFile: type === "file" });
1020
+ if (type === "type") resolvedName = pascalCase(name);
1021
+ return resolvedName;
1085
1022
  }
1086
- //#endregion
1087
- //#region src/PluginDriver.ts
1088
1023
  /**
1089
- * Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise.
1024
+ * Default option resolver applies include/exclude filters and merges matching override options.
1090
1025
  *
1091
- * @example
1026
+ * Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule.
1027
+ *
1028
+ * @example Include/exclude filtering
1092
1029
  * ```ts
1093
- * getMode('src/gen/types.ts') // 'single'
1094
- * getMode('src/gen/types') // 'split'
1030
+ * const options = defaultResolveOptions(operationNode, {
1031
+ * options: { output: 'types' },
1032
+ * exclude: [{ type: 'tag', pattern: 'internal' }],
1033
+ * })
1034
+ * // → null when node has tag 'internal'
1095
1035
  * ```
1096
- */
1097
- function getMode(fileOrFolder) {
1098
- if (!fileOrFolder) return "split";
1099
- return extname(fileOrFolder) ? "single" : "split";
1100
- }
1101
- const hookFirstNullCheck = (state) => !!state?.result;
1102
- var PluginDriver = class {
1036
+ *
1037
+ * @example Override merging
1038
+ * ```ts
1039
+ * const options = defaultResolveOptions(operationNode, {
1040
+ * options: { enumType: 'asConst' },
1041
+ * override: [{ type: 'operationId', pattern: 'listPets', options: { enumType: 'enum' } }],
1042
+ * })
1043
+ * // → { enumType: 'enum' } when operationId matches
1044
+ * ```
1045
+ */
1046
+ function defaultResolveOptions(node, { options, exclude = [], include, override = [] }) {
1047
+ if (isOperationNode(node)) {
1048
+ if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
1049
+ if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
1050
+ const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options;
1051
+ return {
1052
+ ...options,
1053
+ ...overrideOptions
1054
+ };
1055
+ }
1056
+ if (isSchemaNode(node)) {
1057
+ if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null;
1058
+ if (include) {
1059
+ const applicable = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)).filter((r) => r !== null);
1060
+ if (applicable.length > 0 && !applicable.includes(true)) return null;
1061
+ }
1062
+ const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options;
1063
+ return {
1064
+ ...options,
1065
+ ...overrideOptions
1066
+ };
1067
+ }
1068
+ return options;
1069
+ }
1070
+ /**
1071
+ * Default path resolver used by `defineResolver`.
1072
+ *
1073
+ * - Returns the output directory in `single` mode.
1074
+ * - Resolves into a tag- or path-based subdirectory when `group` and a `tag`/`path` value are provided.
1075
+ * - Falls back to a flat `output/baseName` path otherwise.
1076
+ *
1077
+ * A custom `group.name` function overrides the default subdirectory naming.
1078
+ * For `tag` groups the default is `${camelCase(tag)}Controller`.
1079
+ * For `path` groups the default is the first path segment after `/`.
1080
+ *
1081
+ * @example Flat output
1082
+ * ```ts
1083
+ * defaultResolvePath({ baseName: 'petTypes.ts' }, { root: '/src', output: { path: 'types' } })
1084
+ * // → '/src/types/petTypes.ts'
1085
+ * ```
1086
+ *
1087
+ * @example Tag-based grouping
1088
+ * ```ts
1089
+ * defaultResolvePath(
1090
+ * { baseName: 'petTypes.ts', tag: 'pets' },
1091
+ * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
1092
+ * )
1093
+ * // → '/src/types/petsController/petTypes.ts'
1094
+ * ```
1095
+ *
1096
+ * @example Path-based grouping
1097
+ * ```ts
1098
+ * defaultResolvePath(
1099
+ * { baseName: 'petTypes.ts', path: '/pets/list' },
1100
+ * { root: '/src', output: { path: 'types' }, group: { type: 'path' } },
1101
+ * )
1102
+ * // → '/src/types/pets/petTypes.ts'
1103
+ * ```
1104
+ *
1105
+ * @example Single-file mode
1106
+ * ```ts
1107
+ * defaultResolvePath(
1108
+ * { baseName: 'petTypes.ts', pathMode: 'single' },
1109
+ * { root: '/src', output: { path: 'types' } },
1110
+ * )
1111
+ * // → '/src/types'
1112
+ * ```
1113
+ */
1114
+ function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root, output, group }) {
1115
+ if ((pathMode ?? getMode(path.resolve(root, output.path))) === "single") return path.resolve(root, output.path);
1116
+ if (group && (groupPath || tag)) return path.resolve(root, output.path, group.name({ group: group.type === "path" ? groupPath : tag }), baseName);
1117
+ return path.resolve(root, output.path, baseName);
1118
+ }
1119
+ /**
1120
+ * Default file resolver used by `defineResolver`.
1121
+ *
1122
+ * Resolves a `FileNode` by combining name resolution (`resolver.default`) with
1123
+ * path resolution (`resolver.resolvePath`). The resolved file always has empty
1124
+ * `sources`, `imports`, and `exports` arrays — consumers populate those separately.
1125
+ *
1126
+ * In `single` mode the name is omitted and the file sits directly in the output directory.
1127
+ *
1128
+ * @example Resolve a schema file
1129
+ * ```ts
1130
+ * const file = defaultResolveFile.call(resolver,
1131
+ * { name: 'pet', extname: '.ts' },
1132
+ * { root: '/src', output: { path: 'types' } },
1133
+ * )
1134
+ * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
1135
+ * ```
1136
+ *
1137
+ * @example Resolve an operation file with tag grouping
1138
+ * ```ts
1139
+ * const file = defaultResolveFile.call(resolver,
1140
+ * { name: 'listPets', extname: '.ts', tag: 'pets' },
1141
+ * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
1142
+ * )
1143
+ * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
1144
+ * ```
1145
+ */
1146
+ function defaultResolveFile({ name, extname, tag, path: groupPath }, context) {
1147
+ const pathMode = getMode(path.resolve(context.root, context.output.path));
1148
+ const baseName = `${pathMode === "single" ? "" : this.default(name, "file")}${extname}`;
1149
+ const filePath = this.resolvePath({
1150
+ baseName,
1151
+ pathMode,
1152
+ tag,
1153
+ path: groupPath
1154
+ }, context);
1155
+ return createFile({
1156
+ path: filePath,
1157
+ baseName: path.basename(filePath),
1158
+ meta: { pluginName: this.pluginName },
1159
+ sources: [],
1160
+ imports: [],
1161
+ exports: []
1162
+ });
1163
+ }
1164
+ /**
1165
+ * Generates the default "Generated by Kubb" banner from config and optional node metadata.
1166
+ */
1167
+ function buildDefaultBanner({ title, description, version, config }) {
1168
+ try {
1169
+ let source = "";
1170
+ if (Array.isArray(config.input)) {
1171
+ const first = config.input[0];
1172
+ if (first && "path" in first) source = path.basename(first.path);
1173
+ } else if ("path" in config.input) source = path.basename(config.input.path);
1174
+ else if ("data" in config.input) source = "text content";
1175
+ let banner = "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n";
1176
+ if (config.output.defaultBanner === "simple") {
1177
+ banner += "*/\n";
1178
+ return banner;
1179
+ }
1180
+ if (source) banner += `* Source: ${source}\n`;
1181
+ if (title) banner += `* Title: ${title}\n`;
1182
+ if (description) {
1183
+ const formattedDescription = description.replace(/\n/gm, "\n* ");
1184
+ banner += `* Description: ${formattedDescription}\n`;
1185
+ }
1186
+ if (version) banner += `* OpenAPI spec version: ${version}\n`;
1187
+ banner += "*/\n";
1188
+ return banner;
1189
+ } catch (_error) {
1190
+ return "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n*/";
1191
+ }
1192
+ }
1193
+ /**
1194
+ * Default banner resolver — returns the banner string for a generated file.
1195
+ *
1196
+ * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
1197
+ * When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
1198
+ * from the OAS spec when a `node` is provided).
1199
+ *
1200
+ * - When `output.banner` is a function and `node` is provided, returns `output.banner(node)`.
1201
+ * - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice.
1202
+ * - When `output.banner` is a string, returns it directly.
1203
+ * - When `config.output.defaultBanner` is `false`, returns `undefined`.
1204
+ * - Otherwise returns the Kubb "Generated by Kubb" notice.
1205
+ *
1206
+ * @example String banner overrides default
1207
+ * ```ts
1208
+ * defaultResolveBanner(undefined, { output: { banner: '// my banner' }, config })
1209
+ * // → '// my banner'
1210
+ * ```
1211
+ *
1212
+ * @example Function banner with node
1213
+ * ```ts
1214
+ * defaultResolveBanner(inputNode, { output: { banner: (node) => `// v${node.version}` }, config })
1215
+ * // → '// v3.0.0'
1216
+ * ```
1217
+ *
1218
+ * @example No user banner — Kubb notice with OAS metadata
1219
+ * ```ts
1220
+ * defaultResolveBanner(inputNode, { config })
1221
+ * // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
1222
+ * ```
1223
+ *
1224
+ * @example Disabled default banner
1225
+ * ```ts
1226
+ * defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })
1227
+ * // → undefined
1228
+ * ```
1229
+ */
1230
+ function defaultResolveBanner(node, { output, config }) {
1231
+ if (typeof output?.banner === "function") return output.banner(node);
1232
+ if (typeof output?.banner === "string") return output.banner;
1233
+ if (config.output.defaultBanner === false) return;
1234
+ return buildDefaultBanner({
1235
+ title: node?.meta?.title,
1236
+ version: node?.meta?.version,
1237
+ config
1238
+ });
1239
+ }
1240
+ /**
1241
+ * Default footer resolver — returns the footer string for a generated file.
1242
+ *
1243
+ * - When `output.footer` is a function and `node` is provided, calls it with the node.
1244
+ * - When `output.footer` is a function and `node` is absent, returns `undefined`.
1245
+ * - When `output.footer` is a string, returns it directly.
1246
+ * - Otherwise returns `undefined`.
1247
+ *
1248
+ * @example String footer
1249
+ * ```ts
1250
+ * defaultResolveFooter(undefined, { output: { footer: '// end of file' }, config })
1251
+ * // → '// end of file'
1252
+ * ```
1253
+ *
1254
+ * @example Function footer with node
1255
+ * ```ts
1256
+ * defaultResolveFooter(inputNode, { output: { footer: (node) => `// ${node.title}` }, config })
1257
+ * // → '// Pet Store'
1258
+ * ```
1259
+ */
1260
+ function defaultResolveFooter(node, { output }) {
1261
+ if (typeof output?.footer === "function") return node ? output.footer(node) : void 0;
1262
+ if (typeof output?.footer === "string") return output.footer;
1263
+ }
1264
+ /**
1265
+ * Defines a resolver for a plugin, injecting built-in defaults for name casing,
1266
+ * include/exclude/override filtering, path resolution, and file construction.
1267
+ *
1268
+ * All four defaults can be overridden by providing them in the builder function:
1269
+ * - `default` — name casing strategy (camelCase / PascalCase)
1270
+ * - `resolveOptions` — include/exclude/override filtering
1271
+ * - `resolvePath` — output path computation
1272
+ * - `resolveFile` — full `FileNode` construction
1273
+ *
1274
+ * Methods in the builder have access to `this` (the full resolver object), so they
1275
+ * can call other resolver methods without circular imports.
1276
+ *
1277
+ * @example Basic resolver with naming helpers
1278
+ * ```ts
1279
+ * export const resolver = defineResolver<PluginTs>(() => ({
1280
+ * name: 'default',
1281
+ * resolveName(node) {
1282
+ * return this.default(node.name, 'function')
1283
+ * },
1284
+ * resolveTypedName(node) {
1285
+ * return this.default(node.name, 'type')
1286
+ * },
1287
+ * }))
1288
+ * ```
1289
+ *
1290
+ * @example Override resolvePath for a custom output structure
1291
+ * ```ts
1292
+ * export const resolver = defineResolver<PluginTs>(() => ({
1293
+ * name: 'custom',
1294
+ * resolvePath({ baseName }, { root, output }) {
1295
+ * return path.resolve(root, output.path, 'generated', baseName)
1296
+ * },
1297
+ * }))
1298
+ * ```
1299
+ *
1300
+ * @example Use this.default inside a helper
1301
+ * ```ts
1302
+ * export const resolver = defineResolver<PluginTs>(() => ({
1303
+ * name: 'default',
1304
+ * resolveParamName(node, param) {
1305
+ * return this.default(`${node.operationId} ${param.in} ${param.name}`, 'type')
1306
+ * },
1307
+ * }))
1308
+ * ```
1309
+ */
1310
+ function defineResolver(build) {
1311
+ return {
1312
+ default: defaultResolver,
1313
+ resolveOptions: defaultResolveOptions,
1314
+ resolvePath: defaultResolvePath,
1315
+ resolveFile: defaultResolveFile,
1316
+ resolveBanner: defaultResolveBanner,
1317
+ resolveFooter: defaultResolveFooter,
1318
+ ...build()
1319
+ };
1320
+ }
1321
+ //#endregion
1322
+ //#region src/devtools.ts
1323
+ /**
1324
+ * Encodes an `InputNode` as a compressed, URL-safe string.
1325
+ *
1326
+ * The JSON representation is deflate-compressed with {@link deflateSync} before
1327
+ * base64url encoding, which typically reduces payload size by 70–80 % and
1328
+ * keeps URLs well within browser and server path-length limits.
1329
+ *
1330
+ * Use {@link decodeAst} to reverse.
1331
+ */
1332
+ function encodeAst(input) {
1333
+ const compressed = deflateSync(new TextEncoder().encode(JSON.stringify(input)));
1334
+ return Buffer.from(compressed).toString("base64url");
1335
+ }
1336
+ /**
1337
+ * Constructs the Kubb Studio URL for the given `InputNode`.
1338
+ * When `options.ast` is `true`, navigates to the AST inspector (`/ast`).
1339
+ * The `input` is encoded and attached as the `?root=` query parameter so Studio
1340
+ * can decode and render it without a round-trip to any server.
1341
+ */
1342
+ function getStudioUrl(input, studioUrl, options = {}) {
1343
+ return `${studioUrl.replace(/\/$/, "")}${options.ast ? "/ast" : ""}?root=${encodeAst(input)}`;
1344
+ }
1345
+ /**
1346
+ * Opens the Kubb Studio URL for the given `InputNode` in the default browser —
1347
+ *
1348
+ * Falls back to printing the URL if the browser cannot be launched.
1349
+ */
1350
+ async function openInStudio(input, studioUrl, options = {}) {
1351
+ const url = getStudioUrl(input, studioUrl, options);
1352
+ const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
1353
+ const args = process.platform === "win32" ? [
1354
+ "/c",
1355
+ "start",
1356
+ "",
1357
+ url
1358
+ ] : [url];
1359
+ try {
1360
+ await x(cmd, args);
1361
+ } catch {
1362
+ console.log(`\n ${url}\n`);
1363
+ }
1364
+ }
1365
+ //#endregion
1366
+ //#region src/FileManager.ts
1367
+ function mergeFile(a, b) {
1368
+ return {
1369
+ ...a,
1370
+ sources: [...a.sources || [], ...b.sources || []],
1371
+ imports: [...a.imports || [], ...b.imports || []],
1372
+ exports: [...a.exports || [], ...b.exports || []]
1373
+ };
1374
+ }
1375
+ /**
1376
+ * In-memory file store for generated files.
1377
+ *
1378
+ * Files with the same `path` are merged — sources, imports, and exports are concatenated.
1379
+ * The `files` getter returns all stored files sorted by path length (shortest first).
1380
+ *
1381
+ * @example
1382
+ * ```ts
1383
+ * import { FileManager } from '@kubb/core'
1384
+ *
1385
+ * const manager = new FileManager()
1386
+ * manager.upsert(myFile)
1387
+ * console.log(manager.files) // all stored files
1388
+ * ```
1389
+ */
1390
+ var FileManager = class {
1391
+ #cache = /* @__PURE__ */ new Map();
1392
+ #filesCache = null;
1393
+ /**
1394
+ * Adds one or more files. Files with the same path are merged — sources, imports,
1395
+ * and exports from all calls with the same path are concatenated together.
1396
+ */
1397
+ add(...files) {
1398
+ const resolvedFiles = [];
1399
+ const mergedFiles = /* @__PURE__ */ new Map();
1400
+ files.forEach((file) => {
1401
+ const existing = mergedFiles.get(file.path);
1402
+ if (existing) mergedFiles.set(file.path, mergeFile(existing, file));
1403
+ else mergedFiles.set(file.path, file);
1404
+ });
1405
+ for (const file of mergedFiles.values()) {
1406
+ const resolvedFile = createFile(file);
1407
+ this.#cache.set(resolvedFile.path, resolvedFile);
1408
+ this.#filesCache = null;
1409
+ resolvedFiles.push(resolvedFile);
1410
+ }
1411
+ return resolvedFiles;
1412
+ }
1413
+ /**
1414
+ * Adds or merges one or more files.
1415
+ * If a file with the same path already exists, its sources/imports/exports are merged together.
1416
+ */
1417
+ upsert(...files) {
1418
+ const resolvedFiles = [];
1419
+ const mergedFiles = /* @__PURE__ */ new Map();
1420
+ files.forEach((file) => {
1421
+ const existing = mergedFiles.get(file.path);
1422
+ if (existing) mergedFiles.set(file.path, mergeFile(existing, file));
1423
+ else mergedFiles.set(file.path, file);
1424
+ });
1425
+ for (const file of mergedFiles.values()) {
1426
+ const existing = this.#cache.get(file.path);
1427
+ const resolvedFile = createFile(existing ? mergeFile(existing, file) : file);
1428
+ this.#cache.set(resolvedFile.path, resolvedFile);
1429
+ this.#filesCache = null;
1430
+ resolvedFiles.push(resolvedFile);
1431
+ }
1432
+ return resolvedFiles;
1433
+ }
1434
+ getByPath(path) {
1435
+ return this.#cache.get(path) ?? null;
1436
+ }
1437
+ deleteByPath(path) {
1438
+ this.#cache.delete(path);
1439
+ this.#filesCache = null;
1440
+ }
1441
+ clear() {
1442
+ this.#cache.clear();
1443
+ this.#filesCache = null;
1444
+ }
1445
+ /**
1446
+ * All stored files, sorted by path length (shorter paths first).
1447
+ * Barrel/index files (e.g. index.ts) are sorted last within each length bucket.
1448
+ */
1449
+ get files() {
1450
+ if (this.#filesCache) return this.#filesCache;
1451
+ const keys = [...this.#cache.keys()].sort((a, b) => {
1452
+ if (a.length !== b.length) return a.length - b.length;
1453
+ const aIsIndex = trimExtName$1(a).endsWith("index");
1454
+ if (aIsIndex !== trimExtName$1(b).endsWith("index")) return aIsIndex ? 1 : -1;
1455
+ return 0;
1456
+ });
1457
+ const files = [];
1458
+ for (const key of keys) {
1459
+ const file = this.#cache.get(key);
1460
+ if (file) files.push(file);
1461
+ }
1462
+ this.#filesCache = files;
1463
+ return files;
1464
+ }
1465
+ };
1466
+ //#endregion
1467
+ //#region src/renderNode.ts
1468
+ /**
1469
+ * Handles the return value of a plugin AST hook or generator method.
1470
+ *
1471
+ * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`
1472
+ * - `Array<FileNode>` → added directly into `driver.fileManager`
1473
+ * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
1474
+ *
1475
+ * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result
1476
+ * may be a renderer element. Generators that only return `Array<FileNode>` do not need one.
1477
+ */
1478
+ async function applyHookResult(result, driver, rendererFactory) {
1479
+ if (!result) return;
1480
+ if (Array.isArray(result)) {
1481
+ driver.fileManager.upsert(...result);
1482
+ return;
1483
+ }
1484
+ if (!rendererFactory) return;
1485
+ const renderer = rendererFactory();
1486
+ await renderer.render(result);
1487
+ driver.fileManager.upsert(...renderer.files);
1488
+ renderer.unmount();
1489
+ }
1490
+ //#endregion
1491
+ //#region src/utils/executeStrategies.ts
1492
+ /**
1493
+ * Runs promise functions in sequence, threading each result into the next call.
1494
+ *
1495
+ * - Each function receives the accumulated state from the previous call.
1496
+ * - Skips functions that return a falsy value (acts as a no-op for that step).
1497
+ * - Returns an array of all individual results.
1498
+ * @deprecated
1499
+ */
1500
+ function hookSeq(promises) {
1501
+ return promises.filter(Boolean).reduce((promise, func) => {
1502
+ if (typeof func !== "function") throw new Error("HookSeq needs a function that returns a promise `() => Promise<unknown>`");
1503
+ return promise.then((state) => {
1504
+ const calledFunc = func(state);
1505
+ if (calledFunc) return calledFunc.then(Array.prototype.concat.bind(state));
1506
+ return state;
1507
+ });
1508
+ }, Promise.resolve([]));
1509
+ }
1510
+ /**
1511
+ * Runs promise functions in sequence and returns the first non-null result.
1512
+ *
1513
+ * - Stops as soon as `nullCheck` passes for a result (default: `!== null`).
1514
+ * - Subsequent functions are skipped once a match is found.
1515
+ * @deprecated
1516
+ */
1517
+ function hookFirst(promises, nullCheck = (state) => state !== null) {
1518
+ let promise = Promise.resolve(null);
1519
+ for (const func of promises.filter(Boolean)) promise = promise.then((state) => {
1520
+ if (nullCheck(state)) return state;
1521
+ return func(state);
1522
+ });
1523
+ return promise;
1524
+ }
1525
+ /**
1526
+ * Runs promise functions concurrently and returns all settled results.
1527
+ *
1528
+ * - Limits simultaneous executions to `concurrency` (default: unlimited).
1529
+ * - Uses `Promise.allSettled` so individual failures do not cancel other tasks.
1530
+ * @deprecated
1531
+ */
1532
+ function hookParallel(promises, concurrency = Number.POSITIVE_INFINITY) {
1533
+ const limit = pLimit(concurrency);
1534
+ const tasks = promises.filter(Boolean).map((promise) => limit(() => promise()));
1535
+ return Promise.allSettled(tasks);
1536
+ }
1537
+ //#endregion
1538
+ //#region src/PluginDriver.ts
1539
+ /**
1540
+ * Returns `'single'` when `fileOrFolder` has a file extension, `'split'` otherwise.
1541
+ *
1542
+ * @example
1543
+ * ```ts
1544
+ * getMode('src/gen/types.ts') // 'single'
1545
+ * getMode('src/gen/types') // 'split'
1546
+ * ```
1547
+ */
1548
+ function getMode(fileOrFolder) {
1549
+ if (!fileOrFolder) return "split";
1550
+ return extname(fileOrFolder) ? "single" : "split";
1551
+ }
1552
+ const hookFirstNullCheck = (state) => !!state?.result;
1553
+ var PluginDriver = class {
1103
1554
  config;
1104
1555
  options;
1105
1556
  /**
1106
1557
  * The universal `@kubb/ast` `InputNode` produced by the adapter, set by
1107
1558
  * the build pipeline after the adapter's `parse()` resolves.
1108
1559
  */
1109
- inputNode = void 0;
1110
- adapter = void 0;
1111
- #studioIsOpen = false;
1560
+ inputNode = void 0;
1561
+ adapter = void 0;
1562
+ #studioIsOpen = false;
1563
+ /**
1564
+ * Central file store for all generated files.
1565
+ * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
1566
+ * add files; this property gives direct read/write access when needed.
1567
+ */
1568
+ fileManager = new FileManager();
1569
+ plugins = /* @__PURE__ */ new Map();
1570
+ /**
1571
+ * Tracks which plugins have generators registered via `addGenerator()` (event-based path).
1572
+ * Used by the build loop to decide whether to emit generator events for a given plugin.
1573
+ */
1574
+ #pluginsWithEventGenerators = /* @__PURE__ */ new Set();
1575
+ #resolvers = /* @__PURE__ */ new Map();
1576
+ #defaultResolvers = /* @__PURE__ */ new Map();
1577
+ #hookListeners = /* @__PURE__ */ new Map();
1578
+ constructor(config, options) {
1579
+ this.config = config;
1580
+ this.options = {
1581
+ ...options,
1582
+ hooks: options.hooks
1583
+ };
1584
+ config.plugins.map((rawPlugin) => {
1585
+ if (isHookStylePlugin(rawPlugin)) return this.#normalizeHookStylePlugin(rawPlugin);
1586
+ return {
1587
+ ...rawPlugin,
1588
+ buildStart: rawPlugin.buildStart ?? (() => {}),
1589
+ buildEnd: rawPlugin.buildEnd ?? (() => {})
1590
+ };
1591
+ }).filter((plugin) => {
1592
+ if (typeof plugin.apply === "function") return plugin.apply(config);
1593
+ return true;
1594
+ }).sort((a, b) => {
1595
+ if (b.dependencies?.includes(a.name)) return -1;
1596
+ if (a.dependencies?.includes(b.name)) return 1;
1597
+ return 0;
1598
+ }).forEach((plugin) => {
1599
+ this.plugins.set(plugin.name, plugin);
1600
+ });
1601
+ }
1602
+ get hooks() {
1603
+ if (!this.options.hooks) throw new Error("hooks are not defined");
1604
+ return this.options.hooks;
1605
+ }
1606
+ /**
1607
+ * Creates a `Plugin`-compatible object from a hook-style plugin and registers
1608
+ * its lifecycle handlers on the `AsyncEventEmitter`.
1609
+ *
1610
+ * The normalized plugin has an empty `buildStart` — generators registered via
1611
+ * `addGenerator()` in `kubb:plugin:setup` are stored on `normalizedPlugin.generators`
1612
+ * and used by `runPluginAstHooks` during the build.
1613
+ */
1614
+ #normalizeHookStylePlugin(hookPlugin) {
1615
+ const generators = [];
1616
+ const driver = this;
1617
+ const normalizedPlugin = {
1618
+ name: hookPlugin.name,
1619
+ dependencies: hookPlugin.dependencies,
1620
+ options: {
1621
+ output: { path: "." },
1622
+ exclude: [],
1623
+ override: []
1624
+ },
1625
+ generators,
1626
+ inject: () => void 0,
1627
+ resolveName(name, type) {
1628
+ return driver.getResolver(hookPlugin.name).default(name, type);
1629
+ },
1630
+ resolvePath(baseName, pathMode, resolveOptions) {
1631
+ const resolver = driver.getResolver(hookPlugin.name);
1632
+ const opts = normalizedPlugin.options;
1633
+ const group = resolveOptions?.group;
1634
+ return resolver.resolvePath({
1635
+ baseName,
1636
+ pathMode,
1637
+ tag: group?.tag,
1638
+ path: group?.path
1639
+ }, {
1640
+ root: resolve(driver.config.root, driver.config.output.path),
1641
+ output: opts.output,
1642
+ group: opts.group
1643
+ });
1644
+ },
1645
+ buildStart() {},
1646
+ buildEnd() {}
1647
+ };
1648
+ this.registerPluginHooks(hookPlugin, normalizedPlugin);
1649
+ return normalizedPlugin;
1650
+ }
1651
+ /**
1652
+ * Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.
1653
+ *
1654
+ * For `kubb:plugin:setup`, the registered listener wraps the globally emitted context with a
1655
+ * plugin-specific one so that `addGenerator`, `setResolver`, `setTransformer`, and
1656
+ * `setRenderer` all target the correct `normalizedPlugin` entry in the plugins map.
1657
+ *
1658
+ * All other hooks are iterated and registered directly as pass-through listeners.
1659
+ * Any event key present in the global `KubbHooks` interface can be subscribed to.
1660
+ *
1661
+ * External tooling can subscribe to any of these events via `hooks.on(...)` to observe
1662
+ * the plugin lifecycle without modifying plugin behavior.
1663
+ */
1664
+ registerPluginHooks(hookPlugin, normalizedPlugin) {
1665
+ const { hooks } = hookPlugin;
1666
+ if (hooks["kubb:plugin:setup"]) {
1667
+ const setupHandler = (globalCtx) => {
1668
+ const pluginCtx = {
1669
+ ...globalCtx,
1670
+ options: hookPlugin.options ?? {},
1671
+ addGenerator: (gen) => {
1672
+ this.registerGenerator(normalizedPlugin.name, gen);
1673
+ },
1674
+ setResolver: (resolver) => {
1675
+ this.setPluginResolver(normalizedPlugin.name, resolver);
1676
+ },
1677
+ setTransformer: (visitor) => {
1678
+ normalizedPlugin.transformer = visitor;
1679
+ },
1680
+ setRenderer: (renderer) => {
1681
+ normalizedPlugin.renderer = renderer;
1682
+ },
1683
+ setOptions: (opts) => {
1684
+ normalizedPlugin.options = {
1685
+ ...normalizedPlugin.options,
1686
+ ...opts
1687
+ };
1688
+ },
1689
+ injectFile: (file) => {
1690
+ const fileNode = createFile({
1691
+ baseName: file.baseName,
1692
+ path: file.path,
1693
+ sources: file.sources ?? [],
1694
+ imports: [],
1695
+ exports: []
1696
+ });
1697
+ this.fileManager.add(fileNode);
1698
+ }
1699
+ };
1700
+ return hooks["kubb:plugin:setup"](pluginCtx);
1701
+ };
1702
+ this.hooks.on("kubb:plugin:setup", setupHandler);
1703
+ this.#trackHookListener("kubb:plugin:setup", setupHandler);
1704
+ }
1705
+ for (const [event, handler] of Object.entries(hooks)) {
1706
+ if (event === "kubb:plugin:setup" || !handler) continue;
1707
+ this.hooks.on(event, handler);
1708
+ this.#trackHookListener(event, handler);
1709
+ }
1710
+ }
1112
1711
  /**
1113
- * Central file store for all generated files.
1114
- * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
1115
- * add files; this property gives direct read/write access when needed.
1712
+ * Emits the `kubb:plugin:setup` event so that all registered hook-style plugin listeners
1713
+ * can configure generators, resolvers, transformers and renderers before `buildStart` runs.
1714
+ *
1715
+ * Call this once from `safeBuild` before the plugin execution loop begins.
1116
1716
  */
1117
- fileManager = new FileManager();
1118
- plugins = /* @__PURE__ */ new Map();
1119
- constructor(config, options) {
1120
- this.config = config;
1121
- this.options = options;
1122
- config.plugins.map((plugin) => Object.assign({
1123
- buildStart() {},
1124
- buildEnd() {}
1125
- }, plugin)).filter((plugin) => {
1126
- if (typeof plugin.apply === "function") return plugin.apply(config);
1127
- return true;
1128
- }).sort((a, b) => {
1129
- if (b.pre?.includes(a.name)) return 1;
1130
- if (b.post?.includes(a.name)) return -1;
1131
- return 0;
1132
- }).forEach((plugin) => {
1133
- this.plugins.set(plugin.name, plugin);
1717
+ async emitSetupHooks() {
1718
+ await this.hooks.emit("kubb:plugin:setup", {
1719
+ config: this.config,
1720
+ addGenerator: () => {},
1721
+ setResolver: () => {},
1722
+ setTransformer: () => {},
1723
+ setRenderer: () => {},
1724
+ setOptions: () => {},
1725
+ injectFile: () => {},
1726
+ updateConfig: () => {},
1727
+ options: {}
1134
1728
  });
1135
1729
  }
1136
- get events() {
1137
- return this.options.events;
1730
+ /**
1731
+ * Registers a generator for the given plugin on the shared event emitter.
1732
+ *
1733
+ * The generator's `schema`, `operation`, and `operations` methods are registered as
1734
+ * listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations`
1735
+ * respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check
1736
+ * so that generators from different plugins do not cross-fire.
1737
+ *
1738
+ * The renderer resolution chain is: `generator.renderer → plugin.renderer → config.renderer`.
1739
+ * Set `generator.renderer = null` to explicitly opt out of rendering even when the plugin
1740
+ * declares a renderer.
1741
+ *
1742
+ * Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
1743
+ */
1744
+ registerGenerator(pluginName, gen) {
1745
+ const resolveRenderer = () => {
1746
+ const plugin = this.plugins.get(pluginName);
1747
+ return gen.renderer === null ? void 0 : gen.renderer ?? plugin?.renderer ?? this.config.renderer;
1748
+ };
1749
+ if (gen.schema) {
1750
+ const schemaHandler = async (node, ctx) => {
1751
+ if (ctx.plugin.name !== pluginName) return;
1752
+ await applyHookResult(await gen.schema(node, ctx), this, resolveRenderer());
1753
+ };
1754
+ this.hooks.on("kubb:generate:schema", schemaHandler);
1755
+ this.#trackHookListener("kubb:generate:schema", schemaHandler);
1756
+ }
1757
+ if (gen.operation) {
1758
+ const operationHandler = async (node, ctx) => {
1759
+ if (ctx.plugin.name !== pluginName) return;
1760
+ await applyHookResult(await gen.operation(node, ctx), this, resolveRenderer());
1761
+ };
1762
+ this.hooks.on("kubb:generate:operation", operationHandler);
1763
+ this.#trackHookListener("kubb:generate:operation", operationHandler);
1764
+ }
1765
+ if (gen.operations) {
1766
+ const operationsHandler = async (nodes, ctx) => {
1767
+ if (ctx.plugin.name !== pluginName) return;
1768
+ await applyHookResult(await gen.operations(nodes, ctx), this, resolveRenderer());
1769
+ };
1770
+ this.hooks.on("kubb:generate:operations", operationsHandler);
1771
+ this.#trackHookListener("kubb:generate:operations", operationsHandler);
1772
+ }
1773
+ this.#pluginsWithEventGenerators.add(pluginName);
1774
+ }
1775
+ /**
1776
+ * Returns `true` when at least one generator was registered for the given plugin
1777
+ * via `addGenerator()` in `kubb:plugin:setup` (event-based path).
1778
+ *
1779
+ * Used by the build loop to decide whether to walk the AST and emit generator events
1780
+ * for a plugin that has no static `plugin.generators`.
1781
+ */
1782
+ hasRegisteredGenerators(pluginName) {
1783
+ return this.#pluginsWithEventGenerators.has(pluginName);
1784
+ }
1785
+ dispose() {
1786
+ for (const [event, handlers] of this.#hookListeners) for (const handler of handlers) this.hooks.off(event, handler);
1787
+ this.#hookListeners.clear();
1788
+ this.#pluginsWithEventGenerators.clear();
1789
+ }
1790
+ #trackHookListener(event, handler) {
1791
+ let handlers = this.#hookListeners.get(event);
1792
+ if (!handlers) {
1793
+ handlers = /* @__PURE__ */ new Set();
1794
+ this.#hookListeners.set(event, handlers);
1795
+ }
1796
+ handlers.add(handler);
1797
+ }
1798
+ #createDefaultResolver(pluginName) {
1799
+ const existingResolver = this.#defaultResolvers.get(pluginName);
1800
+ if (existingResolver) return existingResolver;
1801
+ const resolver = defineResolver(() => ({
1802
+ name: "default",
1803
+ pluginName
1804
+ }));
1805
+ this.#defaultResolvers.set(pluginName, resolver);
1806
+ return resolver;
1807
+ }
1808
+ setPluginResolver(pluginName, partial) {
1809
+ const merged = {
1810
+ ...this.#createDefaultResolver(pluginName),
1811
+ ...partial
1812
+ };
1813
+ this.#resolvers.set(pluginName, merged);
1814
+ const plugin = this.plugins.get(pluginName);
1815
+ if (plugin) plugin.resolver = merged;
1816
+ }
1817
+ getResolver(pluginName) {
1818
+ const dynamicResolver = this.#resolvers.get(pluginName);
1819
+ if (dynamicResolver) return dynamicResolver;
1820
+ const pluginResolver = this.plugins.get(pluginName)?.resolver;
1821
+ if (pluginResolver) return pluginResolver;
1822
+ return this.#createDefaultResolver(pluginName);
1138
1823
  }
1139
1824
  getContext(plugin) {
1140
1825
  const driver = this;
@@ -1146,7 +1831,7 @@ var PluginDriver = class {
1146
1831
  getMode(output) {
1147
1832
  return getMode(resolve(driver.config.root, driver.config.output.path, output.path));
1148
1833
  },
1149
- events: driver.options.events,
1834
+ hooks: driver.hooks,
1150
1835
  plugin,
1151
1836
  getPlugin: driver.getPlugin.bind(driver),
1152
1837
  requirePlugin: driver.requirePlugin.bind(driver),
@@ -1164,19 +1849,19 @@ var PluginDriver = class {
1164
1849
  return driver.adapter;
1165
1850
  },
1166
1851
  get resolver() {
1167
- return plugin.resolver;
1852
+ return driver.getResolver(plugin.name);
1168
1853
  },
1169
1854
  get transformer() {
1170
1855
  return plugin.transformer;
1171
1856
  },
1172
1857
  warn(message) {
1173
- driver.events.emit("warn", message);
1858
+ driver.hooks.emit("kubb:warn", message);
1174
1859
  },
1175
1860
  error(error) {
1176
- driver.events.emit("error", typeof error === "string" ? new Error(error) : error);
1861
+ driver.hooks.emit("kubb:error", typeof error === "string" ? new Error(error) : error);
1177
1862
  },
1178
1863
  info(message) {
1179
- driver.events.emit("info", message);
1864
+ driver.hooks.emit("kubb:info", message);
1180
1865
  },
1181
1866
  openInStudio(options) {
1182
1867
  if (!driver.config.devtools || driver.#studioIsOpen) return;
@@ -1187,10 +1872,13 @@ var PluginDriver = class {
1187
1872
  return openInStudio(driver.inputNode, studioUrl, options);
1188
1873
  }
1189
1874
  };
1190
- const mergedExtras = {};
1875
+ let mergedExtras = {};
1191
1876
  for (const p of this.plugins.values()) if (typeof p.inject === "function") {
1192
1877
  const result = p.inject.call(baseContext);
1193
- if (result !== null && typeof result === "object") Object.assign(mergedExtras, result);
1878
+ if (result !== null && typeof result === "object") mergedExtras = {
1879
+ ...mergedExtras,
1880
+ ...result
1881
+ };
1194
1882
  }
1195
1883
  return {
1196
1884
  ...baseContext,
@@ -1266,7 +1954,7 @@ var PluginDriver = class {
1266
1954
  async hookForPlugin({ pluginName, hookName, parameters }) {
1267
1955
  const plugin = this.plugins.get(pluginName);
1268
1956
  if (!plugin) return [null];
1269
- this.events.emit("plugins:hook:progress:start", {
1957
+ this.hooks.emit("kubb:plugins:hook:progress:start", {
1270
1958
  hookName,
1271
1959
  plugins: [plugin]
1272
1960
  });
@@ -1276,7 +1964,7 @@ var PluginDriver = class {
1276
1964
  parameters,
1277
1965
  plugin
1278
1966
  });
1279
- this.events.emit("plugins:hook:progress:end", { hookName });
1967
+ this.hooks.emit("kubb:plugins:hook:progress:end", { hookName });
1280
1968
  return [result];
1281
1969
  }
1282
1970
  /**
@@ -1299,7 +1987,7 @@ var PluginDriver = class {
1299
1987
  async hookFirst({ hookName, parameters, skipped }) {
1300
1988
  const plugins = [];
1301
1989
  for (const plugin of this.plugins.values()) if (hookName in plugin && (skipped ? !skipped.has(plugin) : true)) plugins.push(plugin);
1302
- this.events.emit("plugins:hook:progress:start", {
1990
+ this.hooks.emit("kubb:plugins:hook:progress:start", {
1303
1991
  hookName,
1304
1992
  plugins
1305
1993
  });
@@ -1317,7 +2005,7 @@ var PluginDriver = class {
1317
2005
  });
1318
2006
  };
1319
2007
  }), hookFirstNullCheck);
1320
- this.events.emit("plugins:hook:progress:end", { hookName });
2008
+ this.hooks.emit("kubb:plugins:hook:progress:end", { hookName });
1321
2009
  return result;
1322
2010
  }
1323
2011
  /**
@@ -1342,12 +2030,12 @@ var PluginDriver = class {
1342
2030
  return parseResult;
1343
2031
  }
1344
2032
  /**
1345
- * Runs all plugins in parallel based on `this.plugin` order and `pre`/`post` settings.
2033
+ * Runs all plugins in parallel based on `this.plugin` order and `dependencies` settings.
1346
2034
  */
1347
2035
  async hookParallel({ hookName, parameters }) {
1348
2036
  const plugins = [];
1349
2037
  for (const plugin of this.plugins.values()) if (hookName in plugin) plugins.push(plugin);
1350
- this.events.emit("plugins:hook:progress:start", {
2038
+ this.hooks.emit("kubb:plugins:hook:progress:start", {
1351
2039
  hookName,
1352
2040
  plugins
1353
2041
  });
@@ -1368,7 +2056,7 @@ var PluginDriver = class {
1368
2056
  const plugin = plugins[index];
1369
2057
  if (plugin) {
1370
2058
  const startTime = pluginStartTimes.get(plugin) ?? performance.now();
1371
- this.events.emit("error", result.reason, {
2059
+ this.hooks.emit("kubb:error", result.reason, {
1372
2060
  plugin,
1373
2061
  hookName,
1374
2062
  strategy: "hookParallel",
@@ -1378,19 +2066,19 @@ var PluginDriver = class {
1378
2066
  }
1379
2067
  }
1380
2068
  });
1381
- this.events.emit("plugins:hook:progress:end", { hookName });
2069
+ this.hooks.emit("kubb:plugins:hook:progress:end", { hookName });
1382
2070
  return results.reduce((acc, result) => {
1383
2071
  if (result.status === "fulfilled") acc.push(result.value);
1384
2072
  return acc;
1385
2073
  }, []);
1386
2074
  }
1387
2075
  /**
1388
- * Chains plugins
2076
+ * Execute a lifecycle hook sequentially for all plugins that implement it.
1389
2077
  */
1390
2078
  async hookSeq({ hookName, parameters }) {
1391
2079
  const plugins = [];
1392
2080
  for (const plugin of this.plugins.values()) if (hookName in plugin) plugins.push(plugin);
1393
- this.events.emit("plugins:hook:progress:start", {
2081
+ this.hooks.emit("kubb:plugins:hook:progress:start", {
1394
2082
  hookName,
1395
2083
  plugins
1396
2084
  });
@@ -1402,7 +2090,7 @@ var PluginDriver = class {
1402
2090
  plugin
1403
2091
  });
1404
2092
  }));
1405
- this.events.emit("plugins:hook:progress:end", { hookName });
2093
+ this.hooks.emit("kubb:plugins:hook:progress:end", { hookName });
1406
2094
  }
1407
2095
  getPlugin(pluginName) {
1408
2096
  return this.plugins.get(pluginName);
@@ -1413,13 +2101,10 @@ var PluginDriver = class {
1413
2101
  return plugin;
1414
2102
  }
1415
2103
  /**
1416
- * Run an async plugin hook and return the result.
1417
- * @param hookName Name of the plugin hook. Must be either in `PluginHooks` or `OutputPluginValueHooks`.
1418
- * @param args Arguments passed to the plugin hook.
1419
- * @param plugin The actual pluginObject to run.
2104
+ * Emit hook-processing completion metadata after a plugin hook resolves.
1420
2105
  */
1421
2106
  #emitProcessingEnd({ startTime, output, strategy, hookName, plugin, parameters }) {
1422
- this.events.emit("plugins:hook:processing:end", {
2107
+ this.hooks.emit("kubb:plugins:hook:processing:end", {
1423
2108
  duration: Math.round(performance.now() - startTime),
1424
2109
  parameters,
1425
2110
  output,
@@ -1431,7 +2116,7 @@ var PluginDriver = class {
1431
2116
  #execute({ strategy, hookName, parameters, plugin }) {
1432
2117
  const hook = plugin[hookName];
1433
2118
  if (!hook) return null;
1434
- this.events.emit("plugins:hook:processing:start", {
2119
+ this.hooks.emit("kubb:plugins:hook:processing:start", {
1435
2120
  strategy,
1436
2121
  hookName,
1437
2122
  parameters,
@@ -1451,7 +2136,7 @@ var PluginDriver = class {
1451
2136
  });
1452
2137
  return output;
1453
2138
  } catch (error) {
1454
- this.events.emit("error", error, {
2139
+ this.hooks.emit("kubb:error", error, {
1455
2140
  plugin,
1456
2141
  hookName,
1457
2142
  strategy,
@@ -1462,15 +2147,12 @@ var PluginDriver = class {
1462
2147
  })();
1463
2148
  }
1464
2149
  /**
1465
- * Run a sync plugin hook and return the result.
1466
- * @param hookName Name of the plugin hook. Must be in `PluginHooks`.
1467
- * @param args Arguments passed to the plugin hook.
1468
- * @param plugin The actual plugin
2150
+ * Execute a plugin lifecycle hook synchronously and return its output.
1469
2151
  */
1470
2152
  #executeSync({ strategy, hookName, parameters, plugin }) {
1471
2153
  const hook = plugin[hookName];
1472
2154
  if (!hook) return null;
1473
- this.events.emit("plugins:hook:processing:start", {
2155
+ this.hooks.emit("kubb:plugins:hook:processing:start", {
1474
2156
  strategy,
1475
2157
  hookName,
1476
2158
  parameters,
@@ -1489,7 +2171,7 @@ var PluginDriver = class {
1489
2171
  });
1490
2172
  return output;
1491
2173
  } catch (error) {
1492
- this.events.emit("error", error, {
2174
+ this.hooks.emit("kubb:error", error, {
1493
2175
  plugin,
1494
2176
  hookName,
1495
2177
  strategy,
@@ -1500,26 +2182,6 @@ var PluginDriver = class {
1500
2182
  }
1501
2183
  };
1502
2184
  //#endregion
1503
- //#region src/renderNode.tsx
1504
- /**
1505
- * Handles the return value of a plugin AST hook or generator method.
1506
- *
1507
- * - React element → rendered via renderer-jsx, files stored in `driver.fileManager`
1508
- * - `Array<FileNode>` → upserted directly into `driver.fileManager`
1509
- * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
1510
- */
1511
- async function applyHookResult(result, driver) {
1512
- if (!result) return;
1513
- if (Array.isArray(result)) {
1514
- driver.fileManager.upsert(...result);
1515
- return;
1516
- }
1517
- const renderer = createRenderer();
1518
- await renderer.render(/* @__PURE__ */ jsx(Fragment, { children: result }));
1519
- driver.fileManager.upsert(...renderer.files);
1520
- renderer.unmount();
1521
- }
1522
- //#endregion
1523
2185
  //#region src/createStorage.ts
1524
2186
  /**
1525
2187
  * Creates a storage factory. Call the returned function with optional options to get the storage instance.
@@ -1617,7 +2279,7 @@ const fsStorage = createStorage(() => ({
1617
2279
  }));
1618
2280
  //#endregion
1619
2281
  //#region package.json
1620
- var version$1 = "5.0.0-alpha.33";
2282
+ var version$1 = "5.0.0-alpha.35";
1621
2283
  //#endregion
1622
2284
  //#region src/utils/diagnostics.ts
1623
2285
  /**
@@ -1878,24 +2540,14 @@ function isInputPath(config) {
1878
2540
  return typeof config?.input === "object" && config.input !== null && "path" in config.input;
1879
2541
  }
1880
2542
  //#endregion
1881
- //#region src/build.ts
1882
- /**
1883
- * Initializes all Kubb infrastructure for a build without executing any plugins.
1884
- *
1885
- * - Validates the input path (when applicable).
1886
- * - Applies config defaults (`root`, `output.*`, `devtools`).
1887
- * - Runs the adapter (if configured) to produce the universal `InputNode`.
1888
- * When no adapter is supplied and `@kubb/adapter-oas` is installed as an
1889
- *
1890
- * Pass the returned {@link SetupResult} directly to {@link safeBuild} or {@link build}
1891
- * via the `overrides` argument to reuse the same infrastructure across multiple runs.
1892
- */
2543
+ //#region src/createKubb.ts
1893
2544
  async function setup(options) {
1894
- const { config: userConfig, events = new AsyncEventEmitter() } = options;
2545
+ const { config: userConfig } = options;
2546
+ const hooks = options.hooks ?? new AsyncEventEmitter();
1895
2547
  const sources = /* @__PURE__ */ new Map();
1896
2548
  const diagnosticInfo = getDiagnosticInfo();
1897
- if (Array.isArray(userConfig.input)) await events.emit("warn", "This feature is still under development — use with caution");
1898
- await events.emit("debug", {
2549
+ if (Array.isArray(userConfig.input)) await hooks.emit("kubb:warn", "This feature is still under development — use with caution");
2550
+ await hooks.emit("kubb:debug", {
1899
2551
  date: /* @__PURE__ */ new Date(),
1900
2552
  logs: [
1901
2553
  "Configuration:",
@@ -1914,7 +2566,7 @@ async function setup(options) {
1914
2566
  try {
1915
2567
  if (isInputPath(userConfig) && !new URLPath(userConfig.input.path).isURL) {
1916
2568
  await exists(userConfig.input.path);
1917
- await events.emit("debug", {
2569
+ await hooks.emit("kubb:debug", {
1918
2570
  date: /* @__PURE__ */ new Date(),
1919
2571
  logs: [`✓ Input file validated: ${userConfig.input.path}`]
1920
2572
  });
@@ -1927,8 +2579,8 @@ async function setup(options) {
1927
2579
  }
1928
2580
  if (!userConfig.adapter) throw new Error("Adapter should be defined");
1929
2581
  const config = {
1930
- root: userConfig.root || process.cwd(),
1931
2582
  ...userConfig,
2583
+ root: userConfig.root || process.cwd(),
1932
2584
  parsers: userConfig.parsers ?? [],
1933
2585
  adapter: userConfig.adapter,
1934
2586
  output: {
@@ -1946,26 +2598,26 @@ async function setup(options) {
1946
2598
  };
1947
2599
  const storage = config.output.write === false ? null : config.output.storage ?? fsStorage();
1948
2600
  if (config.output.clean) {
1949
- await events.emit("debug", {
2601
+ await hooks.emit("kubb:debug", {
1950
2602
  date: /* @__PURE__ */ new Date(),
1951
2603
  logs: ["Cleaning output directories", ` • Output: ${config.output.path}`]
1952
2604
  });
1953
2605
  await storage?.clear(resolve(config.root, config.output.path));
1954
2606
  }
1955
2607
  const driver = new PluginDriver(config, {
1956
- events,
2608
+ hooks,
1957
2609
  concurrency: 15
1958
2610
  });
1959
2611
  const adapter = config.adapter;
1960
2612
  if (!adapter) throw new Error("No adapter configured. Please provide an adapter in your kubb.config.ts.");
1961
2613
  const source = inputToAdapterSource(config);
1962
- await events.emit("debug", {
2614
+ await hooks.emit("kubb:debug", {
1963
2615
  date: /* @__PURE__ */ new Date(),
1964
2616
  logs: [`Running adapter: ${adapter.name}`]
1965
2617
  });
1966
2618
  driver.adapter = adapter;
1967
2619
  driver.inputNode = await adapter.parse(source);
1968
- await events.emit("debug", {
2620
+ await hooks.emit("kubb:debug", {
1969
2621
  date: /* @__PURE__ */ new Date(),
1970
2622
  logs: [
1971
2623
  `✓ Adapter '${adapter.name}' resolved InputNode`,
@@ -1975,53 +2627,32 @@ async function setup(options) {
1975
2627
  });
1976
2628
  return {
1977
2629
  config,
1978
- events,
2630
+ hooks,
1979
2631
  driver,
1980
2632
  sources,
1981
2633
  storage
1982
2634
  };
1983
2635
  }
1984
2636
  /**
1985
- * Runs a full Kubb build and throws on any error or plugin failure.
1986
- *
1987
- * Internally delegates to {@link safeBuild} and rethrows collected errors.
1988
- * Pass an existing {@link SetupResult} via `overrides` to skip the setup phase.
1989
- */
1990
- async function build(options, overrides) {
1991
- const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(options, overrides);
1992
- if (error) throw error;
1993
- if (failedPlugins.size > 0) {
1994
- const errors = [...failedPlugins].map(({ error }) => error);
1995
- throw new BuildError(`Build Error with ${failedPlugins.size} failed plugins`, { errors });
1996
- }
1997
- return {
1998
- failedPlugins,
1999
- files,
2000
- driver,
2001
- pluginTimings,
2002
- error: void 0,
2003
- sources
2004
- };
2005
- }
2006
- /**
2007
2637
  * Walks the AST and dispatches nodes to a plugin's direct AST hooks
2008
2638
  * (`schema`, `operation`, `operations`).
2009
- *
2010
- * - Each hook accepts a single handler **or an array** — all entries are called in sequence.
2011
- * - Nodes that are excluded by `exclude`/`include` plugin options are skipped automatically.
2012
- * - Return values are handled via `applyHookResult`: React elements are rendered,
2013
- * `FileNode[]` are written via upsert, and `void` is a no-op (manual handling).
2014
- * - Barrel files are generated automatically when `output.barrelType` is set.
2015
2639
  */
2016
2640
  async function runPluginAstHooks(plugin, context) {
2017
2641
  const { adapter, inputNode, resolver, driver } = context;
2018
2642
  const { exclude, include, override } = plugin.options;
2019
2643
  if (!adapter || !inputNode) throw new Error(`[${plugin.name}] No adapter found. Add an OAS adapter (e.g. pluginOas()) before this plugin in your Kubb config.`);
2644
+ function resolveRenderer(gen) {
2645
+ return gen.renderer === null ? void 0 : gen.renderer ?? plugin.renderer ?? context.config.renderer;
2646
+ }
2647
+ const generators = plugin.generators ?? [];
2020
2648
  const collectedOperations = [];
2649
+ const generatorContext = {
2650
+ ...context,
2651
+ resolver: driver.getResolver(plugin.name)
2652
+ };
2021
2653
  await walk(inputNode, {
2022
2654
  depth: "shallow",
2023
2655
  async schema(node) {
2024
- if (!plugin.schema) return;
2025
2656
  const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node;
2026
2657
  const options = resolver.resolveOptions(transformedNode, {
2027
2658
  options: plugin.options,
@@ -2030,7 +2661,15 @@ async function runPluginAstHooks(plugin, context) {
2030
2661
  override
2031
2662
  });
2032
2663
  if (options === null) return;
2033
- await applyHookResult(await plugin.schema.call(context, transformedNode, options), driver);
2664
+ const ctx = {
2665
+ ...generatorContext,
2666
+ options
2667
+ };
2668
+ for (const gen of generators) {
2669
+ if (!gen.schema) continue;
2670
+ await applyHookResult(await gen.schema(transformedNode, ctx), driver, resolveRenderer(gen));
2671
+ }
2672
+ await driver.hooks.emit("kubb:generate:schema", transformedNode, ctx);
2034
2673
  },
2035
2674
  async operation(node) {
2036
2675
  const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node;
@@ -2042,28 +2681,43 @@ async function runPluginAstHooks(plugin, context) {
2042
2681
  });
2043
2682
  if (options !== null) {
2044
2683
  collectedOperations.push(transformedNode);
2045
- if (plugin.operation) await applyHookResult(await plugin.operation.call(context, transformedNode, options), driver);
2684
+ const ctx = {
2685
+ ...generatorContext,
2686
+ options
2687
+ };
2688
+ for (const gen of generators) {
2689
+ if (!gen.operation) continue;
2690
+ await applyHookResult(await gen.operation(transformedNode, ctx), driver, resolveRenderer(gen));
2691
+ }
2692
+ await driver.hooks.emit("kubb:generate:operation", transformedNode, ctx);
2046
2693
  }
2047
2694
  }
2048
2695
  });
2049
- if (plugin.operations && collectedOperations.length > 0) await applyHookResult(await plugin.operations.call(context, collectedOperations, plugin.options), driver);
2696
+ if (collectedOperations.length > 0) {
2697
+ const ctx = {
2698
+ ...generatorContext,
2699
+ options: plugin.options
2700
+ };
2701
+ for (const gen of generators) {
2702
+ if (!gen.operations) continue;
2703
+ await applyHookResult(await gen.operations(collectedOperations, ctx), driver, resolveRenderer(gen));
2704
+ }
2705
+ await driver.hooks.emit("kubb:generate:operations", collectedOperations, ctx);
2706
+ }
2050
2707
  }
2051
- /**
2052
- * Runs a full Kubb build and captures errors instead of throwing.
2053
- *
2054
- * - Installs each plugin in order, recording failures in `failedPlugins`.
2055
- * - Generates the root barrel file when `output.barrelType` is set.
2056
- * - Writes all files through the driver's FileManager and FileProcessor.
2057
- *
2058
- * Returns a {@link BuildOutput} even on failure — inspect `error` and
2059
- * `failedPlugins` to determine whether the build succeeded.
2060
- */
2061
- async function safeBuild(options, overrides) {
2062
- const { driver, events, sources, storage } = overrides ? overrides : await setup(options);
2708
+ async function safeBuild(setupResult) {
2709
+ const { driver, hooks, sources, storage } = setupResult;
2063
2710
  const failedPlugins = /* @__PURE__ */ new Set();
2064
2711
  const pluginTimings = /* @__PURE__ */ new Map();
2065
2712
  const config = driver.config;
2066
2713
  try {
2714
+ await driver.emitSetupHooks();
2715
+ if (driver.adapter && driver.inputNode) await hooks.emit("kubb:build:start", {
2716
+ config,
2717
+ adapter: driver.adapter,
2718
+ inputNode: driver.inputNode,
2719
+ getPlugin: (name) => driver.getPlugin(name)
2720
+ });
2067
2721
  for (const plugin of driver.plugins.values()) {
2068
2722
  const context = driver.getContext(plugin);
2069
2723
  const hrStart = process.hrtime();
@@ -2071,13 +2725,13 @@ async function safeBuild(options, overrides) {
2071
2725
  const root = resolve(config.root, config.output.path);
2072
2726
  try {
2073
2727
  const timestamp = /* @__PURE__ */ new Date();
2074
- await events.emit("plugin:start", plugin);
2075
- await events.emit("debug", {
2728
+ await hooks.emit("kubb:plugin:start", plugin);
2729
+ await hooks.emit("kubb:debug", {
2076
2730
  date: timestamp,
2077
2731
  logs: ["Starting plugin...", ` • Plugin Name: ${plugin.name}`]
2078
2732
  });
2079
2733
  await plugin.buildStart.call(context);
2080
- if (plugin.schema || plugin.operation || plugin.operations) await runPluginAstHooks(plugin, context);
2734
+ if (plugin.generators?.length || driver.hasRegisteredGenerators(plugin.name)) await runPluginAstHooks(plugin, context);
2081
2735
  if (output) {
2082
2736
  const barrelFiles = await getBarrelFiles(driver.fileManager.files, {
2083
2737
  type: output.barrelType ?? "named",
@@ -2089,11 +2743,11 @@ async function safeBuild(options, overrides) {
2089
2743
  }
2090
2744
  const duration = getElapsedMs(hrStart);
2091
2745
  pluginTimings.set(plugin.name, duration);
2092
- await events.emit("plugin:end", plugin, {
2746
+ await hooks.emit("kubb:plugin:end", plugin, {
2093
2747
  duration,
2094
2748
  success: true
2095
2749
  });
2096
- await events.emit("debug", {
2750
+ await hooks.emit("kubb:debug", {
2097
2751
  date: /* @__PURE__ */ new Date(),
2098
2752
  logs: [`✓ Plugin started successfully (${formatMs(duration)})`]
2099
2753
  });
@@ -2101,12 +2755,12 @@ async function safeBuild(options, overrides) {
2101
2755
  const error = caughtError;
2102
2756
  const errorTimestamp = /* @__PURE__ */ new Date();
2103
2757
  const duration = getElapsedMs(hrStart);
2104
- await events.emit("plugin:end", plugin, {
2758
+ await hooks.emit("kubb:plugin:end", plugin, {
2105
2759
  duration,
2106
2760
  success: false,
2107
2761
  error
2108
2762
  });
2109
- await events.emit("debug", {
2763
+ await hooks.emit("kubb:debug", {
2110
2764
  date: errorTimestamp,
2111
2765
  logs: [
2112
2766
  "✗ Plugin start failed",
@@ -2125,7 +2779,7 @@ async function safeBuild(options, overrides) {
2125
2779
  if (config.output.barrelType) {
2126
2780
  const rootPath = resolve(resolve(config.root), config.output.path, BARREL_FILENAME);
2127
2781
  const rootDir = dirname(rootPath);
2128
- await events.emit("debug", {
2782
+ await hooks.emit("kubb:debug", {
2129
2783
  date: /* @__PURE__ */ new Date(),
2130
2784
  logs: [
2131
2785
  "Generating barrel file",
@@ -2136,7 +2790,7 @@ async function safeBuild(options, overrides) {
2136
2790
  const barrelFiles = driver.fileManager.files.filter((file) => {
2137
2791
  return file.sources.some((source) => source.isIndexable);
2138
2792
  });
2139
- await events.emit("debug", {
2793
+ await hooks.emit("kubb:debug", {
2140
2794
  date: /* @__PURE__ */ new Date(),
2141
2795
  logs: [`Found ${barrelFiles.length} indexable files for barrel export`]
2142
2796
  });
@@ -2156,7 +2810,7 @@ async function safeBuild(options, overrides) {
2156
2810
  meta: {}
2157
2811
  });
2158
2812
  driver.fileManager.upsert(rootFile);
2159
- await events.emit("debug", {
2813
+ await hooks.emit("kubb:debug", {
2160
2814
  date: /* @__PURE__ */ new Date(),
2161
2815
  logs: [`✓ Generated barrel file (${rootFile.exports?.length || 0} exports)`]
2162
2816
  });
@@ -2165,7 +2819,7 @@ async function safeBuild(options, overrides) {
2165
2819
  const parsersMap = /* @__PURE__ */ new Map();
2166
2820
  for (const parser of config.parsers) if (parser.extNames) for (const extname of parser.extNames) parsersMap.set(extname, parser);
2167
2821
  const fileProcessor = new FileProcessor();
2168
- await events.emit("debug", {
2822
+ await hooks.emit("kubb:debug", {
2169
2823
  date: /* @__PURE__ */ new Date(),
2170
2824
  logs: [`Writing ${files.length} files...`]
2171
2825
  });
@@ -2173,591 +2827,272 @@ async function safeBuild(options, overrides) {
2173
2827
  parsers: parsersMap,
2174
2828
  extension: config.output.extension,
2175
2829
  onStart: async (processingFiles) => {
2176
- await events.emit("files:processing:start", processingFiles);
2177
- },
2178
- onUpdate: async ({ file, source, processed, total, percentage }) => {
2179
- await events.emit("file:processing:update", {
2180
- file,
2181
- source,
2182
- processed,
2183
- total,
2184
- percentage,
2185
- config
2186
- });
2187
- if (source) {
2188
- await storage?.setItem(file.path, source);
2189
- sources.set(file.path, source);
2190
- }
2830
+ await hooks.emit("kubb:files:processing:start", processingFiles);
2191
2831
  },
2192
- onEnd: async (processedFiles) => {
2193
- await events.emit("files:processing:end", processedFiles);
2194
- await events.emit("debug", {
2195
- date: /* @__PURE__ */ new Date(),
2196
- logs: [`✓ File write process completed for ${processedFiles.length} files`]
2197
- });
2198
- }
2199
- });
2200
- for (const plugin of driver.plugins.values()) if (plugin.buildEnd) {
2201
- const context = driver.getContext(plugin);
2202
- await plugin.buildEnd.call(context);
2203
- }
2204
- return {
2205
- failedPlugins,
2206
- files,
2207
- driver,
2208
- pluginTimings,
2209
- sources
2210
- };
2211
- } catch (error) {
2212
- return {
2213
- failedPlugins,
2214
- files: [],
2215
- driver,
2216
- pluginTimings,
2217
- error,
2218
- sources
2219
- };
2220
- }
2221
- }
2222
- function buildBarrelExports({ barrelFiles, rootDir, existingExports, config, driver }) {
2223
- const pluginNameMap = /* @__PURE__ */ new Map();
2224
- for (const plugin of driver.plugins.values()) pluginNameMap.set(plugin.name, plugin);
2225
- return barrelFiles.flatMap((file) => {
2226
- const containsOnlyTypes = file.sources?.every((source) => source.isTypeOnly);
2227
- return (file.sources ?? []).flatMap((source) => {
2228
- if (!file.path || !source.isIndexable) return [];
2229
- const meta = file.meta;
2230
- const pluginOptions = (meta?.pluginName ? pluginNameMap.get(meta.pluginName) : void 0)?.options;
2231
- if (!pluginOptions || pluginOptions.output?.barrelType === false) return [];
2232
- const exportName = config.output.barrelType === "all" ? void 0 : source.name ? [source.name] : void 0;
2233
- if (exportName?.some((n) => existingExports.has(n))) return [];
2234
- return [createExport({
2235
- name: exportName,
2236
- path: getRelativePath(rootDir, file.path),
2237
- isTypeOnly: config.output.barrelType === "all" ? containsOnlyTypes : source.isTypeOnly
2238
- })];
2239
- });
2240
- });
2241
- }
2242
- /**
2243
- * Maps the resolved `Config['input']` shape into an `AdapterSource` that
2244
- * the adapter's `parse()` can consume.
2245
- */
2246
- function inputToAdapterSource(config) {
2247
- if (Array.isArray(config.input)) return {
2248
- type: "paths",
2249
- paths: config.input.map((i) => new URLPath(i.path).isURL ? i.path : resolve(config.root, i.path))
2250
- };
2251
- if ("data" in config.input) return {
2252
- type: "data",
2253
- data: config.input.data
2254
- };
2255
- if (new URLPath(config.input.path).isURL) return {
2256
- type: "path",
2257
- path: config.input.path
2258
- };
2259
- return {
2260
- type: "path",
2261
- path: resolve(config.root, config.input.path)
2262
- };
2263
- }
2264
- //#endregion
2265
- //#region src/createAdapter.ts
2266
- /**
2267
- * Creates an adapter factory. Call the returned function with optional options to get the adapter instance.
2268
- *
2269
- * @example
2270
- * export const myAdapter = createAdapter<MyAdapter>((options) => {
2271
- * return {
2272
- * name: 'my-adapter',
2273
- * options,
2274
- * async parse(source) { ... },
2275
- * }
2276
- * })
2277
- *
2278
- * // instantiate
2279
- * const adapter = myAdapter({ validate: true })
2280
- */
2281
- function createAdapter(build) {
2282
- return (options) => build(options ?? {});
2283
- }
2284
- //#endregion
2285
- //#region src/createPlugin.ts
2286
- /**
2287
- * Creates a plugin factory. Call the returned function with optional options to get the plugin instance.
2288
- *
2289
- * @example
2290
- * ```ts
2291
- * export const myPlugin = createPlugin<MyPlugin>((options) => {
2292
- * return {
2293
- * name: 'my-plugin',
2294
- * get options() { return options },
2295
- * resolvePath(baseName) { ... },
2296
- * resolveName(name, type) { ... },
2297
- * }
2298
- * })
2299
- *
2300
- * // instantiate
2301
- * const plugin = myPlugin({ output: { path: 'src/gen' } })
2302
- * ```
2303
- */
2304
- function createPlugin(build) {
2305
- return (options) => build(options ?? {});
2306
- }
2307
- //#endregion
2308
- //#region src/defineConfig.ts
2309
- function defineConfig(config) {
2310
- return config;
2311
- }
2312
- //#endregion
2313
- //#region src/defineGenerator.ts
2314
- /**
2315
- * Defines a generator. Returns the object as-is with correct `this` typings.
2316
- * No type discrimination (`type: 'react' | 'core'`) needed — `applyHookResult`
2317
- * handles React elements and `File[]` uniformly.
2318
- */
2319
- function defineGenerator(generator) {
2320
- return generator;
2321
- }
2322
- /**
2323
- * Merges an array of generators into a single generator.
2324
- *
2325
- * The merged generator's `schema`, `operation`, and `operations` methods run
2326
- * the corresponding method from each input generator in sequence, applying each
2327
- * result via `applyHookResult`. This eliminates the need to write the loop
2328
- * manually in each plugin.
2329
- *
2330
- * @param generators - Array of generators to merge into a single generator.
2331
- *
2332
- * @example
2333
- * ```ts
2334
- * const merged = mergeGenerators(generators)
2335
- *
2336
- * return {
2337
- * name: pluginName,
2338
- * schema: merged.schema,
2339
- * operation: merged.operation,
2340
- * operations: merged.operations,
2341
- * }
2342
- * ```
2343
- */
2344
- function mergeGenerators(generators) {
2345
- return {
2346
- name: generators.length > 0 ? generators.map((g) => g.name).join("+") : "merged",
2347
- async schema(node, options) {
2348
- for (const gen of generators) {
2349
- if (!gen.schema) continue;
2350
- await applyHookResult(await gen.schema.call(this, node, options), this.driver);
2351
- }
2352
- },
2353
- async operation(node, options) {
2354
- for (const gen of generators) {
2355
- if (!gen.operation) continue;
2356
- await applyHookResult(await gen.operation.call(this, node, options), this.driver);
2357
- }
2358
- },
2359
- async operations(nodes, options) {
2360
- for (const gen of generators) {
2361
- if (!gen.operations) continue;
2362
- await applyHookResult(await gen.operations.call(this, nodes, options), this.driver);
2832
+ onUpdate: async ({ file, source, processed, total, percentage }) => {
2833
+ await hooks.emit("kubb:file:processing:update", {
2834
+ file,
2835
+ source,
2836
+ processed,
2837
+ total,
2838
+ percentage,
2839
+ config
2840
+ });
2841
+ if (source) {
2842
+ await storage?.setItem(file.path, source);
2843
+ sources.set(file.path, source);
2844
+ }
2845
+ },
2846
+ onEnd: async (processedFiles) => {
2847
+ await hooks.emit("kubb:files:processing:end", processedFiles);
2848
+ await hooks.emit("kubb:debug", {
2849
+ date: /* @__PURE__ */ new Date(),
2850
+ logs: [`✓ File write process completed for ${processedFiles.length} files`]
2851
+ });
2363
2852
  }
2853
+ });
2854
+ for (const plugin of driver.plugins.values()) if (plugin.buildEnd) {
2855
+ const context = driver.getContext(plugin);
2856
+ await plugin.buildEnd.call(context);
2364
2857
  }
2365
- };
2366
- }
2367
- //#endregion
2368
- //#region src/defineLogger.ts
2369
- /**
2370
- * Wraps a logger definition into a typed {@link Logger}.
2371
- *
2372
- * @example
2373
- * export const myLogger = defineLogger({
2374
- * name: 'my-logger',
2375
- * install(context, options) {
2376
- * context.on('info', (message) => console.log('ℹ', message))
2377
- * context.on('error', (error) => console.error('✗', error.message))
2378
- * },
2379
- * })
2380
- */
2381
- function defineLogger(logger) {
2382
- return logger;
2383
- }
2384
- //#endregion
2385
- //#region src/defineParser.ts
2386
- /**
2387
- * Defines a parser with type safety.
2388
- *
2389
- * Use this function to create parsers that transform generated files to strings
2390
- * based on their extension.
2391
- *
2392
- * @example
2393
- * ```ts
2394
- * import { defineParser } from '@kubb/core'
2395
- *
2396
- * export const jsonParser = defineParser({
2397
- * name: 'json',
2398
- * extNames: ['.json'],
2399
- * parse(file) {
2400
- * const { extractStringsFromNodes } = await import('@kubb/ast')
2401
- * return file.sources.map((s) => extractStringsFromNodes(s.nodes ?? [])).join('\n')
2402
- * },
2403
- * })
2404
- * ```
2405
- */
2406
- function defineParser(parser) {
2407
- return parser;
2408
- }
2409
- //#endregion
2410
- //#region src/definePresets.ts
2411
- /**
2412
- * Creates a typed presets registry object — a named collection of {@link Preset} entries.
2413
- *
2414
- * @example
2415
- * import { definePreset, definePresets } from '@kubb/core'
2416
- * import { resolverTsLegacy } from '@kubb/plugin-ts'
2417
- *
2418
- * export const myPresets = definePresets({
2419
- * kubbV4: definePreset('kubbV4', { resolvers: [resolverTsLegacy] }),
2420
- * })
2421
- */
2422
- function definePresets(presets) {
2423
- return presets;
2424
- }
2425
- //#endregion
2426
- //#region src/defineResolver.ts
2427
- /**
2428
- * Checks if an operation matches a pattern for a given filter type (`tag`, `operationId`, `path`, `method`).
2429
- */
2430
- function matchesOperationPattern(node, type, pattern) {
2431
- switch (type) {
2432
- case "tag": return node.tags.some((tag) => !!tag.match(pattern));
2433
- case "operationId": return !!node.operationId.match(pattern);
2434
- case "path": return !!node.path.match(pattern);
2435
- case "method": return !!node.method.toLowerCase().match(pattern);
2436
- case "contentType": return !!node.requestBody?.contentType?.match(pattern);
2437
- default: return false;
2438
- }
2439
- }
2440
- /**
2441
- * Checks if a schema matches a pattern for a given filter type (`schemaName`).
2442
- *
2443
- * Returns `null` when the filter type doesn't apply to schemas.
2444
- */
2445
- function matchesSchemaPattern(node, type, pattern) {
2446
- switch (type) {
2447
- case "schemaName": return node.name ? !!node.name.match(pattern) : false;
2448
- default: return null;
2449
- }
2450
- }
2451
- /**
2452
- * Default name resolver used by `defineResolver`.
2453
- *
2454
- * - `camelCase` for `function` and `file` types.
2455
- * - `PascalCase` for `type`.
2456
- * - `camelCase` for everything else.
2457
- */
2458
- function defaultResolver(name, type) {
2459
- let resolvedName = camelCase(name);
2460
- if (type === "file" || type === "function") resolvedName = camelCase(name, { isFile: type === "file" });
2461
- if (type === "type") resolvedName = pascalCase(name);
2462
- return resolvedName;
2463
- }
2464
- /**
2465
- * Default option resolver — applies include/exclude filters and merges matching override options.
2466
- *
2467
- * Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule.
2468
- *
2469
- * @example Include/exclude filtering
2470
- * ```ts
2471
- * const options = defaultResolveOptions(operationNode, {
2472
- * options: { output: 'types' },
2473
- * exclude: [{ type: 'tag', pattern: 'internal' }],
2474
- * })
2475
- * // → null when node has tag 'internal'
2476
- * ```
2477
- *
2478
- * @example Override merging
2479
- * ```ts
2480
- * const options = defaultResolveOptions(operationNode, {
2481
- * options: { enumType: 'asConst' },
2482
- * override: [{ type: 'operationId', pattern: 'listPets', options: { enumType: 'enum' } }],
2483
- * })
2484
- * // → { enumType: 'enum' } when operationId matches
2485
- * ```
2486
- */
2487
- function defaultResolveOptions(node, { options, exclude = [], include, override = [] }) {
2488
- if (isOperationNode(node)) {
2489
- if (exclude.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
2490
- if (include && !include.some(({ type, pattern }) => matchesOperationPattern(node, type, pattern))) return null;
2491
- const overrideOptions = override.find(({ type, pattern }) => matchesOperationPattern(node, type, pattern))?.options;
2858
+ await hooks.emit("kubb:build:end", {
2859
+ files,
2860
+ config,
2861
+ outputDir: resolve(config.root, config.output.path)
2862
+ });
2492
2863
  return {
2493
- ...options,
2494
- ...overrideOptions
2864
+ failedPlugins,
2865
+ files,
2866
+ driver,
2867
+ pluginTimings,
2868
+ sources
2495
2869
  };
2496
- }
2497
- if (isSchemaNode(node)) {
2498
- if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null;
2499
- if (include) {
2500
- const applicable = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)).filter((r) => r !== null);
2501
- if (applicable.length > 0 && !applicable.includes(true)) return null;
2502
- }
2503
- const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options;
2870
+ } catch (error) {
2504
2871
  return {
2505
- ...options,
2506
- ...overrideOptions
2872
+ failedPlugins,
2873
+ files: [],
2874
+ driver,
2875
+ pluginTimings,
2876
+ error,
2877
+ sources
2507
2878
  };
2879
+ } finally {
2880
+ driver.dispose();
2508
2881
  }
2509
- return options;
2510
2882
  }
2511
- /**
2512
- * Default path resolver used by `defineResolver`.
2513
- *
2514
- * - Returns the output directory in `single` mode.
2515
- * - Resolves into a tag- or path-based subdirectory when `group` and a `tag`/`path` value are provided.
2516
- * - Falls back to a flat `output/baseName` path otherwise.
2517
- *
2518
- * A custom `group.name` function overrides the default subdirectory naming.
2519
- * For `tag` groups the default is `${camelCase(tag)}Controller`.
2520
- * For `path` groups the default is the first path segment after `/`.
2521
- *
2522
- * @example Flat output
2523
- * ```ts
2524
- * defaultResolvePath({ baseName: 'petTypes.ts' }, { root: '/src', output: { path: 'types' } })
2525
- * // → '/src/types/petTypes.ts'
2526
- * ```
2527
- *
2528
- * @example Tag-based grouping
2529
- * ```ts
2530
- * defaultResolvePath(
2531
- * { baseName: 'petTypes.ts', tag: 'pets' },
2532
- * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
2533
- * )
2534
- * // '/src/types/petsController/petTypes.ts'
2535
- * ```
2536
- *
2537
- * @example Path-based grouping
2538
- * ```ts
2539
- * defaultResolvePath(
2540
- * { baseName: 'petTypes.ts', path: '/pets/list' },
2541
- * { root: '/src', output: { path: 'types' }, group: { type: 'path' } },
2542
- * )
2543
- * // → '/src/types/pets/petTypes.ts'
2544
- * ```
2545
- *
2546
- * @example Single-file mode
2547
- * ```ts
2548
- * defaultResolvePath(
2549
- * { baseName: 'petTypes.ts', pathMode: 'single' },
2550
- * { root: '/src', output: { path: 'types' } },
2551
- * )
2552
- * // '/src/types'
2553
- * ```
2554
- */
2555
- function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root, output, group }) {
2556
- if ((pathMode ?? getMode(path.resolve(root, output.path))) === "single") return path.resolve(root, output.path);
2557
- if (group && (groupPath || tag)) return path.resolve(root, output.path, group.name({ group: group.type === "path" ? groupPath : tag }), baseName);
2558
- return path.resolve(root, output.path, baseName);
2883
+ async function build(setupResult) {
2884
+ const { files, driver, failedPlugins, pluginTimings, error, sources } = await safeBuild(setupResult);
2885
+ if (error) throw error;
2886
+ if (failedPlugins.size > 0) {
2887
+ const errors = [...failedPlugins].map(({ error }) => error);
2888
+ throw new BuildError(`Build Error with ${failedPlugins.size} failed plugins`, { errors });
2889
+ }
2890
+ return {
2891
+ failedPlugins,
2892
+ files,
2893
+ driver,
2894
+ pluginTimings,
2895
+ error: void 0,
2896
+ sources
2897
+ };
2898
+ }
2899
+ function buildBarrelExports({ barrelFiles, rootDir, existingExports, config, driver }) {
2900
+ const pluginNameMap = /* @__PURE__ */ new Map();
2901
+ for (const plugin of driver.plugins.values()) pluginNameMap.set(plugin.name, plugin);
2902
+ return barrelFiles.flatMap((file) => {
2903
+ const containsOnlyTypes = file.sources?.every((source) => source.isTypeOnly);
2904
+ return (file.sources ?? []).flatMap((source) => {
2905
+ if (!file.path || !source.isIndexable) return [];
2906
+ const meta = file.meta;
2907
+ const pluginOptions = (meta?.pluginName ? pluginNameMap.get(meta.pluginName) : void 0)?.options;
2908
+ if (!pluginOptions || pluginOptions.output?.barrelType === false) return [];
2909
+ const exportName = config.output.barrelType === "all" ? void 0 : source.name ? [source.name] : void 0;
2910
+ if (exportName?.some((n) => existingExports.has(n))) return [];
2911
+ return [createExport({
2912
+ name: exportName,
2913
+ path: getRelativePath(rootDir, file.path),
2914
+ isTypeOnly: config.output.barrelType === "all" ? containsOnlyTypes : source.isTypeOnly
2915
+ })];
2916
+ });
2917
+ });
2918
+ }
2919
+ function inputToAdapterSource(config) {
2920
+ if (Array.isArray(config.input)) return {
2921
+ type: "paths",
2922
+ paths: config.input.map((i) => new URLPath(i.path).isURL ? i.path : resolve(config.root, i.path))
2923
+ };
2924
+ if ("data" in config.input) return {
2925
+ type: "data",
2926
+ data: config.input.data
2927
+ };
2928
+ if (new URLPath(config.input.path).isURL) return {
2929
+ type: "path",
2930
+ path: config.input.path
2931
+ };
2932
+ return {
2933
+ type: "path",
2934
+ path: resolve(config.root, config.input.path)
2935
+ };
2559
2936
  }
2560
2937
  /**
2561
- * Default file resolver used by `defineResolver`.
2562
- *
2563
- * Resolves a `FileNode` by combining name resolution (`resolver.default`) with
2564
- * path resolution (`resolver.resolvePath`). The resolved file always has empty
2565
- * `sources`, `imports`, and `exports` arrays — consumers populate those separately.
2938
+ * Creates a Kubb instance bound to a single config entry.
2566
2939
  *
2567
- * In `single` mode the name is omitted and the file sits directly in the output directory.
2940
+ * The instance holds shared state (`hooks`, `sources`, `driver`, `config`) across the
2941
+ * `setup → build` lifecycle. Attach event listeners to `kubb.hooks` before
2942
+ * calling `setup()` or `build()`.
2568
2943
  *
2569
- * @example Resolve a schema file
2944
+ * @example
2570
2945
  * ```ts
2571
- * const file = defaultResolveFile.call(resolver,
2572
- * { name: 'pet', extname: '.ts' },
2573
- * { root: '/src', output: { path: 'types' } },
2574
- * )
2575
- * // → { baseName: 'pet.ts', path: '/src/types/pet.ts', sources: [], ... }
2576
- * ```
2946
+ * const kubb = createKubb({ config })
2577
2947
  *
2578
- * @example Resolve an operation file with tag grouping
2579
- * ```ts
2580
- * const file = defaultResolveFile.call(resolver,
2581
- * { name: 'listPets', extname: '.ts', tag: 'pets' },
2582
- * { root: '/src', output: { path: 'types' }, group: { type: 'tag' } },
2583
- * )
2584
- * // → { baseName: 'listPets.ts', path: '/src/types/petsController/listPets.ts', ... }
2948
+ * kubb.hooks.on('kubb:plugin:end', (plugin, { duration }) => {
2949
+ * console.log(`${plugin.name} completed in ${duration}ms`)
2950
+ * })
2951
+ *
2952
+ * const { files, failedPlugins } = await kubb.safeBuild()
2585
2953
  * ```
2586
2954
  */
2587
- function defaultResolveFile({ name, extname, tag, path: groupPath }, context) {
2588
- const pathMode = getMode(path.resolve(context.root, context.output.path));
2589
- const baseName = `${pathMode === "single" ? "" : this.default(name, "file")}${extname}`;
2590
- const filePath = this.resolvePath({
2591
- baseName,
2592
- pathMode,
2593
- tag,
2594
- path: groupPath
2595
- }, context);
2596
- return createFile({
2597
- path: filePath,
2598
- baseName: path.basename(filePath),
2599
- meta: { pluginName: this.pluginName },
2600
- sources: [],
2601
- imports: [],
2602
- exports: []
2603
- });
2604
- }
2605
- /**
2606
- * Generates the default "Generated by Kubb" banner from config and optional node metadata.
2607
- */
2608
- function buildDefaultBanner({ title, description, version, config }) {
2609
- try {
2610
- let source = "";
2611
- if (Array.isArray(config.input)) {
2612
- const first = config.input[0];
2613
- if (first && "path" in first) source = path.basename(first.path);
2614
- } else if ("path" in config.input) source = path.basename(config.input.path);
2615
- else if ("data" in config.input) source = "text content";
2616
- let banner = "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n";
2617
- if (config.output.defaultBanner === "simple") {
2618
- banner += "*/\n";
2619
- return banner;
2620
- }
2621
- if (source) banner += `* Source: ${source}\n`;
2622
- if (title) banner += `* Title: ${title}\n`;
2623
- if (description) {
2624
- const formattedDescription = description.replace(/\n/gm, "\n* ");
2625
- banner += `* Description: ${formattedDescription}\n`;
2955
+ function createKubb(options) {
2956
+ const hooks = options.hooks ?? new AsyncEventEmitter();
2957
+ let setupResult;
2958
+ const instance = {
2959
+ get hooks() {
2960
+ return hooks;
2961
+ },
2962
+ get sources() {
2963
+ return setupResult?.sources ?? /* @__PURE__ */ new Map();
2964
+ },
2965
+ get driver() {
2966
+ return setupResult?.driver;
2967
+ },
2968
+ get config() {
2969
+ return setupResult?.config;
2970
+ },
2971
+ async setup() {
2972
+ setupResult = await setup({
2973
+ config: options.config,
2974
+ hooks
2975
+ });
2976
+ },
2977
+ async build() {
2978
+ if (!setupResult) await instance.setup();
2979
+ return build(setupResult);
2980
+ },
2981
+ async safeBuild() {
2982
+ if (!setupResult) await instance.setup();
2983
+ return safeBuild(setupResult);
2626
2984
  }
2627
- if (version) banner += `* OpenAPI spec version: ${version}\n`;
2628
- banner += "*/\n";
2629
- return banner;
2630
- } catch (_error) {
2631
- return "/**\n* Generated by Kubb (https://kubb.dev/).\n* Do not edit manually.\n*/";
2632
- }
2985
+ };
2986
+ return instance;
2633
2987
  }
2988
+ //#endregion
2989
+ //#region src/createPlugin.ts
2634
2990
  /**
2635
- * Default banner resolver returns the banner string for a generated file.
2636
- *
2637
- * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
2638
- * When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
2639
- * from the OAS spec when a `node` is provided).
2640
- *
2641
- * - When `output.banner` is a function and `node` is provided, returns `output.banner(node)`.
2642
- * - When `output.banner` is a function and `node` is absent, falls back to the Kubb notice.
2643
- * - When `output.banner` is a string, returns it directly.
2644
- * - When `config.output.defaultBanner` is `false`, returns `undefined`.
2645
- * - Otherwise returns the Kubb "Generated by Kubb" notice.
2646
- *
2647
- * @example String banner overrides default
2648
- * ```ts
2649
- * defaultResolveBanner(undefined, { output: { banner: '// my banner' }, config })
2650
- * // → '// my banner'
2651
- * ```
2652
- *
2653
- * @example Function banner with node
2654
- * ```ts
2655
- * defaultResolveBanner(inputNode, { output: { banner: (node) => `// v${node.version}` }, config })
2656
- * // → '// v3.0.0'
2657
- * ```
2991
+ * Creates a plugin factory. Call the returned function with optional options to get the plugin instance.
2658
2992
  *
2659
- * @example No user banner — Kubb notice with OAS metadata
2993
+ * @example
2660
2994
  * ```ts
2661
- * defaultResolveBanner(inputNode, { config })
2662
- * // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
2663
- * ```
2995
+ * export const myPlugin = createPlugin<MyPlugin>((options) => {
2996
+ * return {
2997
+ * name: 'my-plugin',
2998
+ * get options() { return options },
2999
+ * resolvePath(baseName) { ... },
3000
+ * resolveName(name, type) { ... },
3001
+ * }
3002
+ * })
2664
3003
  *
2665
- * @example Disabled default banner
2666
- * ```ts
2667
- * defaultResolveBanner(undefined, { config: { output: { defaultBanner: false }, ...config } })
2668
- * // → undefined
3004
+ * // instantiate
3005
+ * const plugin = myPlugin({ output: { path: 'src/gen' } })
2669
3006
  * ```
3007
+ * @deprecated use definePlugin instead
2670
3008
  */
2671
- function defaultResolveBanner(node, { output, config }) {
2672
- if (typeof output?.banner === "function") return output.banner(node);
2673
- if (typeof output?.banner === "string") return output.banner;
2674
- if (config.output.defaultBanner === false) return;
2675
- return buildDefaultBanner({
2676
- title: node?.meta?.title,
2677
- version: node?.meta?.version,
2678
- config
2679
- });
3009
+ function createPlugin(build) {
3010
+ return (options) => build(options ?? {});
2680
3011
  }
3012
+ //#endregion
3013
+ //#region src/createRenderer.ts
2681
3014
  /**
2682
- * Default footer resolver returns the footer string for a generated file.
3015
+ * Creates a renderer factory for use in generator definitions.
2683
3016
  *
2684
- * - When `output.footer` is a function and `node` is provided, calls it with the node.
2685
- * - When `output.footer` is a function and `node` is absent, returns `undefined`.
2686
- * - When `output.footer` is a string, returns it directly.
2687
- * - Otherwise returns `undefined`.
3017
+ * Wrap your renderer factory function with this helper to register it as the
3018
+ * renderer for a generator. Core will call this factory once per render cycle
3019
+ * to obtain a fresh renderer instance.
2688
3020
  *
2689
- * @example String footer
3021
+ * @example
2690
3022
  * ```ts
2691
- * defaultResolveFooter(undefined, { output: { footer: '// end of file' }, config })
2692
- * // '// end of file'
2693
- * ```
3023
+ * // packages/renderer-jsx/src/index.ts
3024
+ * export const jsxRenderer = createRenderer(() => {
3025
+ * const runtime = new Runtime()
3026
+ * return {
3027
+ * async render(element) { await runtime.render(element) },
3028
+ * get files() { return runtime.nodes },
3029
+ * unmount(error) { runtime.unmount(error) },
3030
+ * }
3031
+ * })
2694
3032
  *
2695
- * @example Function footer with node
2696
- * ```ts
2697
- * defaultResolveFooter(inputNode, { output: { footer: (node) => `// ${node.title}` }, config })
2698
- * // → '// Pet Store'
3033
+ * // packages/plugin-zod/src/generators/zodGenerator.tsx
3034
+ * import { jsxRenderer } from '@kubb/renderer-jsx'
3035
+ * export const zodGenerator = defineGenerator<PluginZod>({
3036
+ * name: 'zod',
3037
+ * renderer: jsxRenderer,
3038
+ * schema(node, options) { return <File ...>...</File> },
3039
+ * })
2699
3040
  * ```
2700
3041
  */
2701
- function defaultResolveFooter(node, { output }) {
2702
- if (typeof output?.footer === "function") return node ? output.footer(node) : void 0;
2703
- if (typeof output?.footer === "string") return output.footer;
3042
+ function createRenderer(factory) {
3043
+ return factory;
2704
3044
  }
3045
+ //#endregion
3046
+ //#region src/defineGenerator.ts
2705
3047
  /**
2706
- * Defines a resolver for a plugin, injecting built-in defaults for name casing,
2707
- * include/exclude/override filtering, path resolution, and file construction.
2708
- *
2709
- * All four defaults can be overridden by providing them in the builder function:
2710
- * - `default` — name casing strategy (camelCase / PascalCase)
2711
- * - `resolveOptions` — include/exclude/override filtering
2712
- * - `resolvePath` — output path computation
2713
- * - `resolveFile` — full `FileNode` construction
2714
- *
2715
- * Methods in the builder have access to `this` (the full resolver object), so they
2716
- * can call other resolver methods without circular imports.
3048
+ * Defines a generator. Returns the object as-is with correct `this` typings.
3049
+ * `applyHookResult` handles renderer elements and `File[]` uniformly using
3050
+ * the generator's declared `renderer` factory.
3051
+ */
3052
+ function defineGenerator(generator) {
3053
+ return generator;
3054
+ }
3055
+ //#endregion
3056
+ //#region src/defineLogger.ts
3057
+ /**
3058
+ * Wraps a logger definition into a typed {@link Logger}.
2717
3059
  *
2718
- * @example Basic resolver with naming helpers
2719
- * ```ts
2720
- * export const resolver = defineResolver<PluginTs>(() => ({
2721
- * name: 'default',
2722
- * resolveName(node) {
2723
- * return this.default(node.name, 'function')
2724
- * },
2725
- * resolveTypedName(node) {
2726
- * return this.default(node.name, 'type')
3060
+ * @example
3061
+ * export const myLogger = defineLogger({
3062
+ * name: 'my-logger',
3063
+ * install(context, options) {
3064
+ * context.on('kubb:info', (message) => console.log('ℹ', message))
3065
+ * context.on('kubb:error', (error) => console.error('', error.message))
2727
3066
  * },
2728
- * }))
2729
- * ```
3067
+ * })
3068
+ */
3069
+ function defineLogger(logger) {
3070
+ return logger;
3071
+ }
3072
+ //#endregion
3073
+ //#region src/defineParser.ts
3074
+ /**
3075
+ * Defines a parser with type safety.
2730
3076
  *
2731
- * @example Override resolvePath for a custom output structure
2732
- * ```ts
2733
- * export const resolver = defineResolver<PluginTs>(() => ({
2734
- * name: 'custom',
2735
- * resolvePath({ baseName }, { root, output }) {
2736
- * return path.resolve(root, output.path, 'generated', baseName)
2737
- * },
2738
- * }))
2739
- * ```
3077
+ * Use this function to create parsers that transform generated files to strings
3078
+ * based on their extension.
2740
3079
  *
2741
- * @example Use this.default inside a helper
3080
+ * @example
2742
3081
  * ```ts
2743
- * export const resolver = defineResolver<PluginTs>(() => ({
2744
- * name: 'default',
2745
- * resolveParamName(node, param) {
2746
- * return this.default(`${node.operationId} ${param.in} ${param.name}`, 'type')
3082
+ * import { defineParser } from '@kubb/core'
3083
+ *
3084
+ * export const jsonParser = defineParser({
3085
+ * name: 'json',
3086
+ * extNames: ['.json'],
3087
+ * parse(file) {
3088
+ * const { extractStringsFromNodes } = await import('@kubb/ast')
3089
+ * return file.sources.map((s) => extractStringsFromNodes(s.nodes ?? [])).join('\n')
2747
3090
  * },
2748
- * }))
3091
+ * })
2749
3092
  * ```
2750
3093
  */
2751
- function defineResolver(build) {
2752
- return {
2753
- default: defaultResolver,
2754
- resolveOptions: defaultResolveOptions,
2755
- resolvePath: defaultResolvePath,
2756
- resolveFile: defaultResolveFile,
2757
- resolveBanner: defaultResolveBanner,
2758
- resolveFooter: defaultResolveFooter,
2759
- ...build()
2760
- };
3094
+ function defineParser(parser) {
3095
+ return parser;
2761
3096
  }
2762
3097
  //#endregion
2763
3098
  //#region src/storages/memoryStorage.ts
@@ -2847,22 +3182,6 @@ async function detectFormatter() {
2847
3182
  return null;
2848
3183
  }
2849
3184
  //#endregion
2850
- //#region src/utils/getConfigs.ts
2851
- /**
2852
- * Resolves a {@link ConfigInput} into a normalized array of {@link Config} objects.
2853
- *
2854
- * - Awaits the config when it is a `Promise`.
2855
- * - Calls the factory function with `args` when the config is a function.
2856
- * - Wraps a single config object in an array for uniform downstream handling.
2857
- */
2858
- async function getConfigs(config, args) {
2859
- const resolved = await (typeof config === "function" ? config(args) : config);
2860
- return (Array.isArray(resolved) ? resolved : [resolved]).map((item) => ({
2861
- plugins: [],
2862
- ...item
2863
- }));
2864
- }
2865
- //#endregion
2866
3185
  //#region src/utils/getFunctionParams.ts
2867
3186
  function order(items) {
2868
3187
  return sortBy(items.filter(Boolean), ([_key, item]) => {
@@ -2981,47 +3300,6 @@ var FunctionParams = class FunctionParams {
2981
3300
  }
2982
3301
  };
2983
3302
  //#endregion
2984
- //#region src/utils/getPreset.ts
2985
- /**
2986
- * Returns a copy of `defaults` where each function in `userOverrides` is wrapped
2987
- * so a `null`/`undefined` return falls back to the original. Non-function values
2988
- * are assigned directly. All calls use the merged object as `this`.
2989
- */
2990
- function withFallback(defaults, userOverrides) {
2991
- const merged = { ...defaults };
2992
- for (const key of Object.keys(userOverrides)) {
2993
- const userVal = userOverrides[key];
2994
- const defaultVal = defaults[key];
2995
- if (typeof userVal === "function" && typeof defaultVal === "function") merged[key] = (...args) => userVal.apply(merged, args) ?? defaultVal.apply(merged, args);
2996
- else if (userVal !== void 0) merged[key] = userVal;
2997
- }
2998
- return merged;
2999
- }
3000
- /**
3001
- * Resolves a named preset into a resolver, transformer, and generators.
3002
- *
3003
- * - Selects the preset resolver; wraps it with user overrides using null/undefined fallback.
3004
- * - Composes the preset's transformers into a single visitor; wraps it with the user transformer using null/undefined fallback.
3005
- * - Combines preset generators with user-supplied generators; falls back to the `default` preset's generators when neither provides any.
3006
- */
3007
- function getPreset(params) {
3008
- const { preset: presetName, presets, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = params;
3009
- const preset = presets[presetName];
3010
- const presetResolver = preset?.resolver ?? presets["default"].resolver;
3011
- const resolver = userResolver ? withFallback(presetResolver, userResolver) : presetResolver;
3012
- const presetTransformers = preset?.transformers ?? [];
3013
- const presetTransformer = presetTransformers.length > 0 ? composeTransformers$1(...presetTransformers) : void 0;
3014
- const transformer = presetTransformer && userTransformer ? withFallback(presetTransformer, userTransformer) : userTransformer ?? presetTransformer;
3015
- const presetGenerators = preset?.generators ?? [];
3016
- const defaultGenerators = presets["default"]?.generators ?? [];
3017
- return {
3018
- resolver,
3019
- transformer,
3020
- generators: presetGenerators.length > 0 || userGenerators.length > 0 ? [...presetGenerators, ...userGenerators] : defaultGenerators,
3021
- preset
3022
- };
3023
- }
3024
- //#endregion
3025
3303
  //#region src/utils/linters.ts
3026
3304
  /**
3027
3305
  * Returns `true` when the given linter is installed and callable.
@@ -3063,7 +3341,7 @@ async function detectLinter() {
3063
3341
  //#endregion
3064
3342
  //#region src/utils/packageJSON.ts
3065
3343
  function getPackageJSONSync(cwd) {
3066
- const pkgPath = pkg.up({ cwd });
3344
+ const pkgPath = findPackageJSON(cwd);
3067
3345
  if (!pkgPath) return null;
3068
3346
  return JSON.parse(readSync(pkgPath));
3069
3347
  }
@@ -3104,6 +3382,6 @@ function satisfiesDependency(dependency, version, cwd) {
3104
3382
  return satisfies(semVer, version);
3105
3383
  }
3106
3384
  //#endregion
3107
- export { AsyncEventEmitter, FunctionParams, PluginDriver, URLPath, build, build as default, buildDefaultBanner, composeTransformers, createAdapter, createFunctionParams, createPlugin, createStorage, defaultResolveBanner, defaultResolveFile, defaultResolveFooter, defaultResolveOptions, defaultResolvePath, defineConfig, defineGenerator, defineLogger, defineParser, definePresets, definePrinter, defineResolver, detectFormatter, detectLinter, formatters, fsStorage, getBarrelFiles, getConfigs, getFunctionParams, getMode, getPreset, isInputPath, linters, logLevel, memoryStorage, mergeGenerators, safeBuild, satisfiesDependency, setup };
3385
+ export { AsyncEventEmitter, FunctionParams, PluginDriver, URLPath, ast, buildDefaultBanner, composeTransformers, createAdapter, createFunctionParams, createKubb, createPlugin, createRenderer, createStorage, defaultResolveBanner, defaultResolveFile, defaultResolveFooter, defaultResolveOptions, defaultResolvePath, defineGenerator, defineLogger, defineParser, definePlugin, definePrinter, defineResolver, detectFormatter, detectLinter, formatters, fsStorage, getBarrelFiles, getFunctionParams, getMode, isInputPath, linters, logLevel, memoryStorage, satisfiesDependency };
3108
3386
 
3109
3387
  //# sourceMappingURL=index.js.map