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