@kubb/core 5.0.0-beta.35 → 5.0.0-beta.37

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.
@@ -28,6 +28,7 @@ let node_events = require("node:events");
28
28
  let node_path = require("node:path");
29
29
  node_path = __toESM(node_path, 1);
30
30
  let _kubb_ast = require("@kubb/ast");
31
+ let node_async_hooks = require("node:async_hooks");
31
32
  let fflate = require("fflate");
32
33
  let tinyexec = require("tinyexec");
33
34
  //#region ../../internals/utils/src/errors.ts
@@ -62,6 +63,18 @@ var BuildError = class extends Error {
62
63
  function toError(value) {
63
64
  return value instanceof Error ? value : new Error(String(value));
64
65
  }
66
+ /**
67
+ * Extracts a human-readable message from any thrown value.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * getErrorMessage(new Error('oops')) // 'oops'
72
+ * getErrorMessage('plain string') // 'plain string'
73
+ * ```
74
+ */
75
+ function getErrorMessage(value) {
76
+ return value instanceof Error ? value.message : String(value);
77
+ }
65
78
  //#endregion
66
79
  //#region ../../internals/utils/src/asyncEventEmitter.ts
67
80
  /**
@@ -161,6 +174,24 @@ var AsyncEventEmitter = class {
161
174
  return this.#emitter.listenerCount(eventName);
162
175
  }
163
176
  /**
177
+ * Raises or lowers the per-event listener ceiling before Node warns about a memory leak.
178
+ * Set this above the expected listener count when many listeners attach by design.
179
+ *
180
+ * @example
181
+ * ```ts
182
+ * emitter.setMaxListeners(40)
183
+ * ```
184
+ */
185
+ setMaxListeners(max) {
186
+ this.#emitter.setMaxListeners(max);
187
+ }
188
+ /**
189
+ * Returns the current per-event listener ceiling.
190
+ */
191
+ getMaxListeners() {
192
+ return this.#emitter.getMaxListeners();
193
+ }
194
+ /**
164
195
  * Removes all listeners from every event channel.
165
196
  *
166
197
  * @example
@@ -254,21 +285,6 @@ function getElapsedMs(hrStart) {
254
285
  const ms = seconds * 1e3 + nanoseconds / 1e6;
255
286
  return Math.round(ms * 100) / 100;
256
287
  }
257
- /**
258
- * Converts a millisecond duration into a human-readable string (`ms`, `s`, or `m s`).
259
- *
260
- * @example
261
- * ```ts
262
- * formatMs(250) // '250ms'
263
- * formatMs(1500) // '1.50s'
264
- * formatMs(90000) // '1m 30.0s'
265
- * ```
266
- */
267
- function formatMs(ms) {
268
- if (ms >= 6e4) return `${Math.floor(ms / 6e4)}m ${(ms % 6e4 / 1e3).toFixed(1)}s`;
269
- if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
270
- return `${Math.round(ms)}ms`;
271
- }
272
288
  //#endregion
273
289
  //#region ../../internals/utils/src/promise.ts
274
290
  function* chunks(arr, size) {
@@ -636,13 +652,17 @@ var URLPath = class {
636
652
  */
637
653
  const DEFAULT_STUDIO_URL = "https://kubb.studio";
638
654
  /**
639
- * Default banner style written at the top of every generated file.
640
- */
641
- const DEFAULT_BANNER = "simple";
642
- /**
643
- * Default file-extension mapping used when no explicit mapping is configured.
655
+ * Plugin `include` filter types that select operations directly. When one of these is set
656
+ * without a `schemaName` include, the generate phase pre-scans operations to compute the set
657
+ * of schemas they reach, so unreachable schemas can be pruned for that plugin.
644
658
  */
645
- const DEFAULT_EXTENSION = { ".ts": ".ts" };
659
+ const OPERATION_FILTER_TYPES = new Set([
660
+ "tag",
661
+ "operationId",
662
+ "path",
663
+ "method",
664
+ "contentType"
665
+ ]);
646
666
  /**
647
667
  * Numeric log-level thresholds used internally to compare verbosity.
648
668
  *
@@ -653,8 +673,453 @@ const logLevel = {
653
673
  error: 0,
654
674
  warn: 1,
655
675
  info: 3,
656
- verbose: 4,
657
- debug: 5
676
+ verbose: 4
677
+ };
678
+ /**
679
+ * Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode
680
+ * and stays stable so it can be referenced in tooling and (later) docs. Reference
681
+ * these instead of inlining the string at a throw site.
682
+ */
683
+ const diagnosticCode = {
684
+ /**
685
+ * Fallback for an unstructured error with no specific code.
686
+ */
687
+ unknown: "KUBB_UNKNOWN",
688
+ /**
689
+ * The `input.path` file or URL could not be read.
690
+ */
691
+ inputNotFound: "KUBB_INPUT_NOT_FOUND",
692
+ /**
693
+ * An adapter was configured without an `input`.
694
+ */
695
+ inputRequired: "KUBB_INPUT_REQUIRED",
696
+ /**
697
+ * A `$ref` (or equivalent reference) could not be resolved in the source document.
698
+ */
699
+ refNotFound: "KUBB_REF_NOT_FOUND",
700
+ /**
701
+ * A server variable value is not allowed by its `enum`.
702
+ */
703
+ invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE",
704
+ /**
705
+ * A required plugin is missing from the config.
706
+ */
707
+ pluginNotFound: "KUBB_PLUGIN_NOT_FOUND",
708
+ /**
709
+ * A plugin threw while generating.
710
+ */
711
+ pluginFailed: "KUBB_PLUGIN_FAILED",
712
+ /**
713
+ * A plugin reported a non-fatal warning through `ctx.warn`.
714
+ */
715
+ pluginWarning: "KUBB_PLUGIN_WARNING",
716
+ /**
717
+ * A plugin reported an informational message through `ctx.info`.
718
+ */
719
+ pluginInfo: "KUBB_PLUGIN_INFO",
720
+ /**
721
+ * A schema uses a `format` Kubb does not map to a specific type. Reserved for
722
+ * adapters to emit as a `warning`.
723
+ */
724
+ unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT",
725
+ /**
726
+ * A referenced schema or operation is marked `deprecated`. Reserved for adapters
727
+ * to emit as an `info`.
728
+ */
729
+ deprecated: "KUBB_DEPRECATED",
730
+ /**
731
+ * An adapter is required but the config has none. The build cannot read the input
732
+ * without one.
733
+ */
734
+ adapterRequired: "KUBB_ADAPTER_REQUIRED",
735
+ /**
736
+ * The `devtools` config is set to something other than an object.
737
+ */
738
+ devtoolsInvalid: "KUBB_DEVTOOLS_INVALID",
739
+ /**
740
+ * A resolved output path escapes the output directory, which can stem from a path
741
+ * traversal in the spec or a misconfigured `group.name`.
742
+ */
743
+ pathTraversal: "KUBB_PATH_TRAVERSAL",
744
+ /**
745
+ * A post-generate shell hook (`hooks.done`) exited with a failure.
746
+ */
747
+ hookFailed: "KUBB_HOOK_FAILED",
748
+ /**
749
+ * The formatter pass over the generated files failed.
750
+ */
751
+ formatFailed: "KUBB_FORMAT_FAILED",
752
+ /**
753
+ * The linter pass over the generated files failed.
754
+ */
755
+ lintFailed: "KUBB_LINT_FAILED",
756
+ /**
757
+ * Not a failure. Carries a plugin's elapsed time, summed into the run total.
758
+ */
759
+ performance: "KUBB_PERFORMANCE",
760
+ /**
761
+ * Not a failure. A newer Kubb version is available on npm.
762
+ */
763
+ updateAvailable: "KUBB_UPDATE_AVAILABLE"
764
+ };
765
+ //#endregion
766
+ //#region src/diagnostics.ts
767
+ /**
768
+ * Docs major, derived from the package version so the link tracks the published major.
769
+ */
770
+ const docsMajor = "5.0.0-beta.37".split(".")[0] ?? "5";
771
+ /**
772
+ * Narrows a {@link Diagnostic} to the variant for `code`, or `null` when it does not match.
773
+ *
774
+ * @example
775
+ * ```ts
776
+ * const update = narrowDiagnostic(diagnostic, diagnosticCode.updateAvailable)
777
+ * if (update) {
778
+ * console.log(update.latestVersion)
779
+ * }
780
+ * ```
781
+ */
782
+ function narrowDiagnostic(diagnostic, code) {
783
+ return diagnostic.code === code ? diagnostic : null;
784
+ }
785
+ /**
786
+ * Builds a type guard that narrows a {@link Diagnostic} to the variant for `kind`. A diagnostic
787
+ * with no `kind` is treated as a `problem`.
788
+ */
789
+ function isKind(kind) {
790
+ return (diagnostic) => (diagnostic.kind ?? "problem") === kind;
791
+ }
792
+ /**
793
+ * Returns `true` when the diagnostic is a build {@link ProblemDiagnostic}.
794
+ *
795
+ * @example
796
+ * ```ts
797
+ * if (isProblemDiagnostic(diagnostic)) {
798
+ * console.log(diagnostic.location)
799
+ * }
800
+ * ```
801
+ */
802
+ const isProblemDiagnostic = isKind("problem");
803
+ /**
804
+ * Returns `true` when the diagnostic is a per-plugin {@link PerformanceDiagnostic}.
805
+ *
806
+ * @example
807
+ * ```ts
808
+ * const timings = diagnostics.filter(isPerformanceDiagnostic)
809
+ * ```
810
+ */
811
+ const isPerformanceDiagnostic = isKind("performance");
812
+ /**
813
+ * Returns `true` when the diagnostic is a version-update {@link UpdateDiagnostic}.
814
+ *
815
+ * @example
816
+ * ```ts
817
+ * if (isUpdateDiagnostic(diagnostic)) {
818
+ * console.log(diagnostic.latestVersion)
819
+ * }
820
+ * ```
821
+ */
822
+ const isUpdateDiagnostic = isKind("update");
823
+ /**
824
+ * An `Error` that carries a {@link Diagnostic}, so structured problems can flow
825
+ * through the existing throw/catch paths while keeping their code and location.
826
+ *
827
+ * @example
828
+ * ```ts
829
+ * throw new DiagnosticError({ code: diagnosticCode.refNotFound, severity: 'error', message: `Could not find ${ref}`, location: { kind: 'schema', pointer: ref, ref } })
830
+ * ```
831
+ */
832
+ var DiagnosticError = class extends Error {
833
+ diagnostic;
834
+ constructor(diagnostic) {
835
+ super(diagnostic.message, { cause: diagnostic.cause });
836
+ this.name = "DiagnosticError";
837
+ this.diagnostic = diagnostic;
838
+ }
839
+ };
840
+ /**
841
+ * Structural check for a {@link DiagnosticError}, including one thrown from a duplicated
842
+ * `@kubb/core` copy where `instanceof` fails. Matches on the `name` and a `diagnostic`
843
+ * that carries a `code`.
844
+ */
845
+ function isDiagnosticError(error) {
846
+ if (error instanceof DiagnosticError) return true;
847
+ return error instanceof Error && error.name === "DiagnosticError" && "diagnostic" in error && typeof error.diagnostic === "object" && error.diagnostic !== null && typeof error.diagnostic?.code === "string";
848
+ }
849
+ /**
850
+ * Explanation for every {@link diagnosticCode}. Use {@link Diagnostics.explain} to look one up
851
+ * and `Diagnostics.docsUrl` for the matching kubb.dev page.
852
+ */
853
+ const diagnosticCatalog = {
854
+ [diagnosticCode.unknown]: {
855
+ title: "Unknown error",
856
+ cause: "An error was thrown without a stable Kubb code, so it is reported as-is.",
857
+ fix: "Read the underlying message and stack. If it comes from a plugin or adapter, check its configuration; otherwise report it as a possible Kubb bug."
858
+ },
859
+ [diagnosticCode.inputNotFound]: {
860
+ title: "Input not found",
861
+ cause: "The file or URL set in `input.path` (or passed as `kubb generate PATH`) could not be read.",
862
+ fix: "Check that the path or URL exists and is readable, then set it in `input.path` or pass it on the CLI."
863
+ },
864
+ [diagnosticCode.inputRequired]: {
865
+ title: "Input required",
866
+ cause: "An adapter is configured but no `input` was provided.",
867
+ fix: "Set `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config."
868
+ },
869
+ [diagnosticCode.refNotFound]: {
870
+ title: "Reference not found",
871
+ cause: "A `$ref` could not be resolved in the source document.",
872
+ fix: "Add the missing definition (for example under `components.schemas`) or fix the `$ref`. Run `kubb validate` to check the spec."
873
+ },
874
+ [diagnosticCode.invalidServerVariable]: {
875
+ title: "Invalid server variable",
876
+ cause: "A server variable value is not allowed by its `enum`.",
877
+ fix: "Use one of the values listed in the server variable `enum`, or update the spec."
878
+ },
879
+ [diagnosticCode.pluginNotFound]: {
880
+ title: "Plugin not found",
881
+ cause: "A plugin that another plugin depends on is missing from the config.",
882
+ fix: "Add the required plugin to the `plugins` array in kubb.config.ts, or remove the dependency on it."
883
+ },
884
+ [diagnosticCode.pluginFailed]: {
885
+ title: "Plugin failed",
886
+ cause: "A plugin threw while generating, or reported an error through `ctx.error`.",
887
+ fix: "Read the underlying error and check the plugin options and the schema or operation it failed on."
888
+ },
889
+ [diagnosticCode.pluginWarning]: {
890
+ title: "Plugin warning",
891
+ cause: "A plugin reported a non-fatal warning through `ctx.warn`.",
892
+ fix: "Review the message. It does not fail the build; adjust the plugin options or input if the warning is unwanted."
893
+ },
894
+ [diagnosticCode.pluginInfo]: {
895
+ title: "Plugin info",
896
+ cause: "A plugin reported an informational message through `ctx.info`.",
897
+ fix: "Informational only. No action is required."
898
+ },
899
+ [diagnosticCode.unsupportedFormat]: {
900
+ title: "Unsupported format",
901
+ cause: "A schema uses a `format` Kubb does not map to a specific type, so it falls back to the base type.",
902
+ fix: "Use a format Kubb supports, or handle the custom format with a parser or plugin."
903
+ },
904
+ [diagnosticCode.deprecated]: {
905
+ title: "Deprecated",
906
+ cause: "A referenced schema or operation is marked `deprecated`.",
907
+ fix: "Migrate off the deprecated definition if the warning is unwanted."
908
+ },
909
+ [diagnosticCode.adapterRequired]: {
910
+ title: "Adapter required",
911
+ cause: "An action needs an adapter (for example opening Kubb Studio) but none is configured.",
912
+ fix: "Set `adapter` in kubb.config.ts, for example `adapterOas()`."
913
+ },
914
+ [diagnosticCode.devtoolsInvalid]: {
915
+ title: "Invalid devtools config",
916
+ cause: "The `devtools` config is set to something other than an object.",
917
+ fix: "Set `devtools` to an options object, or remove it to disable Kubb Studio."
918
+ },
919
+ [diagnosticCode.pathTraversal]: {
920
+ title: "Path traversal",
921
+ cause: "A resolved output path escaped the output directory, which can stem from a path traversal in the spec or a misconfigured `group.name`.",
922
+ fix: "Keep generated paths within the output directory. Review the `group.name` function and the names coming from the spec."
923
+ },
924
+ [diagnosticCode.hookFailed]: {
925
+ title: "Hook failed",
926
+ cause: "A post-generate shell hook (`hooks.done`) exited with a non-zero status.",
927
+ fix: "Check the command is installed and correct, and run it manually to see the error."
928
+ },
929
+ [diagnosticCode.formatFailed]: {
930
+ title: "Format failed",
931
+ cause: "The formatter pass over the generated files failed.",
932
+ fix: "Check the formatter (oxfmt, biome, or prettier) is installed and its config is valid, then run it manually on the output."
933
+ },
934
+ [diagnosticCode.lintFailed]: {
935
+ title: "Lint failed",
936
+ cause: "The linter pass over the generated files failed.",
937
+ fix: "Check the linter (oxlint, biome, or eslint) is installed and its config is valid, then run it manually on the output."
938
+ },
939
+ [diagnosticCode.performance]: {
940
+ title: "Performance",
941
+ cause: "Not a failure. Records a plugin’s elapsed time, summed into the run total.",
942
+ fix: "No action. This is an informational metric."
943
+ },
944
+ [diagnosticCode.updateAvailable]: {
945
+ title: "Update available",
946
+ cause: "A newer Kubb version is published on npm than the one running.",
947
+ fix: "Update the `@kubb/*` packages, for example `npm install -g @kubb/cli`, to get the latest fixes."
948
+ }
949
+ };
950
+ /**
951
+ * Static helpers for working with {@link Diagnostic}s, plus the run-scoped sink
952
+ * that lets deep code report a diagnostic without threading a callback.
953
+ *
954
+ * The sink lives in a single `AsyncLocalStorage` in the `@kubb/core` bundle.
955
+ * `Diagnostics.scope` activates it for a run, so anything inside that run (the
956
+ * adapter parse, a lazily consumed stream, a generator) reports through
957
+ * `Diagnostics.report` and lands in the same run.
958
+ */
959
+ var Diagnostics = class Diagnostics {
960
+ static #reporterStorage = new node_async_hooks.AsyncLocalStorage();
961
+ /**
962
+ * Runs `fn` with `sink` as the active diagnostic sink for the whole async
963
+ * subtree, so {@link Diagnostics.report} reaches it from anywhere inside.
964
+ */
965
+ static scope(sink, fn) {
966
+ return Diagnostics.#reporterStorage.run(sink, fn);
967
+ }
968
+ /**
969
+ * Collects a diagnostic into the active build via the run-scoped sink, without throwing.
970
+ * Returns `true` when a run consumed it, `false` when called outside a {@link Diagnostics.scope}
971
+ * (so callers can fall back to throwing). Use a `warning`/`info` severity for non-fatal issues.
972
+ * For rendering a diagnostic live on the hook bus, use {@link Diagnostics.emit} instead.
973
+ */
974
+ static report(diagnostic) {
975
+ const sink = Diagnostics.#reporterStorage.getStore();
976
+ if (!sink) return false;
977
+ sink(diagnostic);
978
+ return true;
979
+ }
980
+ /**
981
+ * Emits a diagnostic on the run's `kubb:diagnostic` event so the loggers render it live.
982
+ * Use it instead of calling `hooks.emit('kubb:diagnostic', ...)` directly. To collect a
983
+ * diagnostic into the build result from deep in a run, use {@link Diagnostics.report} instead.
984
+ */
985
+ static async emit(hooks, diagnostic) {
986
+ await hooks.emit("kubb:diagnostic", { diagnostic });
987
+ }
988
+ /**
989
+ * Coerces any thrown value into a {@link ProblemDiagnostic}. A {@link DiagnosticError}
990
+ * keeps its structured data, and anything else becomes a `KUBB_UNKNOWN` error.
991
+ */
992
+ static from(error) {
993
+ const seen = /* @__PURE__ */ new Set();
994
+ let current = error;
995
+ let root;
996
+ while (current instanceof Error && !seen.has(current)) {
997
+ if (isDiagnosticError(current)) return current.diagnostic;
998
+ seen.add(current);
999
+ root = current;
1000
+ current = current.cause;
1001
+ }
1002
+ return {
1003
+ code: diagnosticCode.unknown,
1004
+ severity: "error",
1005
+ message: root ? root.message : getErrorMessage(error),
1006
+ cause: root
1007
+ };
1008
+ }
1009
+ /**
1010
+ * Builds a per-plugin performance record. Reporters sum these into the run total.
1011
+ */
1012
+ static performance({ plugin, duration }) {
1013
+ return {
1014
+ kind: "performance",
1015
+ code: diagnosticCode.performance,
1016
+ severity: "info",
1017
+ message: `${plugin} generated in ${Math.round(duration)}ms`,
1018
+ plugin,
1019
+ duration
1020
+ };
1021
+ }
1022
+ /**
1023
+ * Builds the version-update notice shown when a newer Kubb is published on npm.
1024
+ */
1025
+ static update({ currentVersion, latestVersion }) {
1026
+ return {
1027
+ kind: "update",
1028
+ code: diagnosticCode.updateAvailable,
1029
+ severity: "info",
1030
+ message: `Update available: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`,
1031
+ currentVersion,
1032
+ latestVersion
1033
+ };
1034
+ }
1035
+ /**
1036
+ * True when any diagnostic is an error, the severity that fails a build. Non-error
1037
+ * diagnostics are ignored.
1038
+ */
1039
+ static hasError(diagnostics) {
1040
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error");
1041
+ }
1042
+ /**
1043
+ * Names of the plugins that failed, deduped, derived from the error diagnostics
1044
+ * that carry a `plugin`.
1045
+ */
1046
+ static failedPlugins(diagnostics) {
1047
+ const names = /* @__PURE__ */ new Set();
1048
+ for (const diagnostic of diagnostics) if (diagnostic.severity === "error" && diagnostic.plugin) names.add(diagnostic.plugin);
1049
+ return [...names];
1050
+ }
1051
+ /**
1052
+ * Counts `problem` diagnostics by severity for the run summary. `timing`
1053
+ * diagnostics are ignored.
1054
+ */
1055
+ static count(diagnostics) {
1056
+ let errors = 0;
1057
+ let warnings = 0;
1058
+ let infos = 0;
1059
+ for (const diagnostic of diagnostics) {
1060
+ if (!isProblemDiagnostic(diagnostic)) continue;
1061
+ if (diagnostic.severity === "error") errors += 1;
1062
+ else if (diagnostic.severity === "warning") warnings += 1;
1063
+ else infos += 1;
1064
+ }
1065
+ return {
1066
+ errors,
1067
+ warnings,
1068
+ infos
1069
+ };
1070
+ }
1071
+ /**
1072
+ * Drops duplicate `problem` diagnostics that share a code, location pointer, and
1073
+ * plugin, so the same issue reported across several passes is shown once. Non-problem
1074
+ * diagnostics are always kept.
1075
+ */
1076
+ static dedupe(diagnostics) {
1077
+ const seen = /* @__PURE__ */ new Set();
1078
+ const result = [];
1079
+ for (const diagnostic of diagnostics) {
1080
+ if (!isProblemDiagnostic(diagnostic)) {
1081
+ result.push(diagnostic);
1082
+ continue;
1083
+ }
1084
+ const pointer = diagnostic.location && "pointer" in diagnostic.location ? diagnostic.location.pointer : "";
1085
+ const key = `${diagnostic.code} ${pointer} ${diagnostic.plugin ?? ""}`;
1086
+ if (seen.has(key)) continue;
1087
+ seen.add(key);
1088
+ result.push(diagnostic);
1089
+ }
1090
+ return result;
1091
+ }
1092
+ /**
1093
+ * Builds the kubb.dev docs URL for a diagnostic code, e.g.
1094
+ * `KUBB_REF_NOT_FOUND` → `https://kubb.dev/docs/5.x/reference/diagnostics/kubb-ref-not-found`.
1095
+ */
1096
+ static docsUrl(code) {
1097
+ return `https://kubb.dev/docs/${docsMajor}.x/reference/diagnostics/${code.toLowerCase().replaceAll("_", "-")}`;
1098
+ }
1099
+ /**
1100
+ * The catalog entry for a code: its title, cause, and fix. Mirrors the kubb.dev
1101
+ * `/diagnostics/<slug>` page.
1102
+ */
1103
+ static explain(code) {
1104
+ return diagnosticCatalog[code];
1105
+ }
1106
+ /**
1107
+ * Reduces a diagnostic to its JSON-safe fields plus a `docsUrl`, for machine-readable
1108
+ * consumers. The `cause`, `kind`, and `duration` are dropped, and absent optional
1109
+ * fields are omitted rather than set to `undefined`.
1110
+ */
1111
+ static serialize(diagnostic) {
1112
+ const problem = isProblemDiagnostic(diagnostic) ? diagnostic : void 0;
1113
+ return {
1114
+ code: diagnostic.code,
1115
+ severity: diagnostic.severity,
1116
+ message: diagnostic.message,
1117
+ ...problem?.location ? { location: problem.location } : {},
1118
+ ...problem?.help ? { help: problem.help } : {},
1119
+ ...problem?.plugin ? { plugin: problem.plugin } : {},
1120
+ ...diagnostic.code === diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
1121
+ };
1122
+ }
658
1123
  };
659
1124
  //#endregion
660
1125
  //#region src/definePlugin.ts
@@ -764,7 +1229,7 @@ function defaultResolver(name, type) {
764
1229
  return camelCase(name);
765
1230
  }
766
1231
  /**
767
- * Default option resolver applies include/exclude filters and merges matching override options.
1232
+ * Default option resolver. Applies include/exclude filters and merges matching override options.
768
1233
  *
769
1234
  * Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule.
770
1235
  *
@@ -800,7 +1265,7 @@ function computeOptions(node, options, exclude, include, override) {
800
1265
  if ((0, _kubb_ast.isSchemaNode)(node)) {
801
1266
  if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null;
802
1267
  if (include) {
803
- const applicable = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)).filter((r) => r !== null);
1268
+ const applicable = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)).filter((result) => result !== null);
804
1269
  if (applicable.length > 0 && !applicable.includes(true)) return null;
805
1270
  }
806
1271
  const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options;
@@ -873,8 +1338,8 @@ function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root
873
1338
  const result = (() => {
874
1339
  if (group && (groupPath || tag)) {
875
1340
  const groupValue = group.type === "path" ? groupPath : tag;
876
- const defaultName = group.type === "tag" ? ({ group: g }) => `${camelCase(g)}Controller` : ({ group: g }) => {
877
- const segment = g.split("/").filter((s) => s !== "" && s !== "." && s !== "..")[0];
1341
+ const defaultName = group.type === "tag" ? ({ group: groupName }) => `${camelCase(groupName)}Controller` : ({ group: groupName }) => {
1342
+ const segment = groupName.split("/").filter((part) => part !== "" && part !== "." && part !== "..")[0];
878
1343
  return segment ? camelCase(segment) : "";
879
1344
  };
880
1345
  const resolveName = group.name ?? defaultName;
@@ -884,7 +1349,13 @@ function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root
884
1349
  })();
885
1350
  const outputDir = node_path.default.resolve(root, output.path);
886
1351
  const outputDirWithSep = outputDir.endsWith(node_path.default.sep) ? outputDir : `${outputDir}${node_path.default.sep}`;
887
- if (result !== outputDir && !result.startsWith(outputDirWithSep)) throw new Error(`[Kubb] Resolved path "${result}" is outside the output directory "${outputDir}". This may indicate a path traversal attempt in the OpenAPI specification or a misconfigured group.name function.`);
1352
+ if (result !== outputDir && !result.startsWith(outputDirWithSep)) throw new DiagnosticError({
1353
+ code: diagnosticCode.pathTraversal,
1354
+ severity: "error",
1355
+ message: `Resolved path "${result}" is outside the output directory "${outputDir}".`,
1356
+ help: "This can stem from a path traversal in the OpenAPI specification or a misconfigured `group.name` function. Keep generated paths within the output directory.",
1357
+ location: { kind: "config" }
1358
+ });
888
1359
  return result;
889
1360
  }
890
1361
  /**
@@ -892,7 +1363,7 @@ function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root
892
1363
  *
893
1364
  * Resolves a `FileNode` by combining name resolution (`resolver.default`) with
894
1365
  * path resolution (`resolver.resolvePath`). The resolved file always has empty
895
- * `sources`, `imports`, and `exports` arrays consumers populate those separately.
1366
+ * `sources`, `imports`, and `exports` arrays, which consumers populate separately.
896
1367
  *
897
1368
  * In `single` mode the name is omitted and the file sits directly in the output directory.
898
1369
  *
@@ -968,7 +1439,7 @@ function buildDefaultBanner({ title, description, version, config }) {
968
1439
  }
969
1440
  }
970
1441
  /**
971
- * Default banner resolver returns the banner string for a generated file.
1442
+ * Default banner resolver. Returns the banner string for a generated file.
972
1443
  *
973
1444
  * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
974
1445
  * When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
@@ -997,7 +1468,7 @@ function buildDefaultBanner({ title, description, version, config }) {
997
1468
  * // → ''
998
1469
  * ```
999
1470
  *
1000
- * @example No user banner Kubb notice with OAS metadata
1471
+ * @example No user banner, Kubb notice with OAS metadata
1001
1472
  * ```ts
1002
1473
  * defaultResolveBanner(meta, { config })
1003
1474
  * // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
@@ -1023,7 +1494,7 @@ function defaultResolveBanner(meta, { output, config, file }) {
1023
1494
  });
1024
1495
  }
1025
1496
  /**
1026
- * Default footer resolver returns the footer string for a generated file.
1497
+ * Default footer resolver. Returns the footer string for a generated file.
1027
1498
  *
1028
1499
  * - When `output.footer` is a function, calls it with the file's `BannerMeta` and returns the result.
1029
1500
  * - When `output.footer` is a string, returns it directly.
@@ -1055,11 +1526,11 @@ function defaultResolveFooter(meta, { output, file }) {
1055
1526
  * name casing, include/exclude/override filtering, output path computation,
1056
1527
  * and file construction. Supply your own to override any of them:
1057
1528
  *
1058
- * - `default` name casing strategy (camelCase / PascalCase).
1059
- * - `resolveOptions` include/exclude/override filtering.
1060
- * - `resolvePath` output path computation.
1061
- * - `resolveFile` full `FileNode` construction.
1062
- * - `resolveBanner` / `resolveFooter` top/bottom-of-file text.
1529
+ * - `default` sets the name casing strategy (camelCase or PascalCase).
1530
+ * - `resolveOptions` does include/exclude/override filtering.
1531
+ * - `resolvePath` computes the output path.
1532
+ * - `resolveFile` builds the full `FileNode`.
1533
+ * - `resolveBanner` and `resolveFooter` produce the top and bottom of file text.
1063
1534
  *
1064
1535
  * Methods in the returned object can call sibling resolver methods via `this`,
1065
1536
  * which keeps custom rules small (`this.default(name, 'type')` to delegate).
@@ -1108,7 +1579,7 @@ function defineResolver(build) {
1108
1579
  * Encodes an `InputNode` as a compressed, URL-safe string.
1109
1580
  *
1110
1581
  * The JSON representation is deflate-compressed with {@link deflateSync} before
1111
- * base64url encoding, which typically reduces payload size by 7080 % and
1582
+ * base64url encoding, which typically reduces payload size by 70, 80 % and
1112
1583
  * keeps URLs well within browser and server path-length limits.
1113
1584
  */
1114
1585
  function encodeAst(input) {
@@ -1125,8 +1596,7 @@ function getStudioUrl(input, studioUrl, options = {}) {
1125
1596
  return `${studioUrl.replace(/\/$/, "")}${options.ast ? "/ast" : ""}?root=${encodeAst(input)}`;
1126
1597
  }
1127
1598
  /**
1128
- * Opens the Kubb Studio URL for the given `InputNode` in the default browser
1129
- *
1599
+ * Opens the Kubb Studio URL for the given `InputNode` in the default browser, *
1130
1600
  * Falls back to printing the URL if the browser cannot be launched.
1131
1601
  */
1132
1602
  async function openInStudio(input, studioUrl, options = {}) {
@@ -1181,18 +1651,13 @@ function compareFiles(a, b) {
1181
1651
  * ```
1182
1652
  */
1183
1653
  var FileManager = class {
1184
- #cache = /* @__PURE__ */ new Map();
1185
- #sorted = null;
1186
- #onUpsert = null;
1187
1654
  /**
1188
- * Registers a callback invoked with the resolved {@link FileNode} on every
1189
- * `add` / `upsert`. Used by the build loop to track newly written files
1190
- * without keeping its own scan-based diff. Single subscriber by design —
1191
- * setting again replaces the previous callback. Pass `null` to detach.
1655
+ * Subscribe to file-store changes. Listeners on `upsert` see each resolved file as it lands
1656
+ * through `add` or `upsert`.
1192
1657
  */
1193
- setOnUpsert(callback) {
1194
- this.#onUpsert = callback;
1195
- }
1658
+ hooks = new AsyncEventEmitter();
1659
+ #cache = /* @__PURE__ */ new Map();
1660
+ #sorted = null;
1196
1661
  add(...files) {
1197
1662
  return this.#store(files, false);
1198
1663
  }
@@ -1207,7 +1672,7 @@ var FileManager = class {
1207
1672
  const merged = existing && mergeExisting ? (0, _kubb_ast.createFile)(mergeFile(existing, file)) : (0, _kubb_ast.createFile)(file);
1208
1673
  this.#cache.set(merged.path, merged);
1209
1674
  resolved.push(merged);
1210
- this.#onUpsert?.(merged);
1675
+ this.hooks.emit("upsert", merged);
1211
1676
  }
1212
1677
  if (resolved.length > 0) this.#sorted = null;
1213
1678
  return resolved;
@@ -1232,18 +1697,19 @@ var FileManager = class {
1232
1697
  this.#sorted = null;
1233
1698
  }
1234
1699
  /**
1235
- * Releases all stored files. Called by the core after `kubb:build:end`.
1700
+ * Releases all stored files and clears every `hooks` listener. Called by the core after
1701
+ * `kubb:build:end`.
1236
1702
  */
1237
1703
  dispose() {
1238
1704
  this.clear();
1239
- this.#onUpsert = null;
1705
+ this.hooks.removeAll();
1240
1706
  }
1241
1707
  [Symbol.dispose]() {
1242
1708
  this.dispose();
1243
1709
  }
1244
1710
  /**
1245
1711
  * All stored files in stable sort order (shortest path first, barrel files
1246
- * last within a length bucket). Returns a cached view do not mutate.
1712
+ * last within a length bucket). Returns a cached view, do not mutate.
1247
1713
  */
1248
1714
  get files() {
1249
1715
  return this.#sorted ??= [...this.#cache.values()].sort(compareFiles);
@@ -1256,32 +1722,57 @@ function joinSources(file) {
1256
1722
  if (sources.length === 0) return "";
1257
1723
  const parts = [];
1258
1724
  for (const source of sources) {
1259
- const s = (0, _kubb_ast.extractStringsFromNodes)(source.nodes);
1260
- if (s) parts.push(s);
1725
+ const text = (0, _kubb_ast.extractStringsFromNodes)(source.nodes);
1726
+ if (text) parts.push(text);
1261
1727
  }
1262
1728
  return parts.join("\n\n");
1263
1729
  }
1264
1730
  /**
1265
- * Converts a single file to a string using the registered parsers.
1266
- * Falls back to joining source values when no matching parser is found.
1731
+ * Turns `FileNode`s into source strings and writes them to storage.
1267
1732
  *
1268
- * @internal
1733
+ * Two modes share the same instance. Stateless mode (`parse`, `stream`, `run`) just runs the
1734
+ * conversion. Queue mode (`enqueue`, `flush`, `drain`) buffers files deduped by path and
1735
+ * writes each batch through storage with up to `STREAM_FLUSH_EVERY` requests in flight.
1736
+ *
1737
+ * `flush` does not wait for its batch to finish, so dispatch can overlap with IO. The next
1738
+ * `flush` or `drain` picks the in-flight batch up. `drain` blocks until everything has been
1739
+ * written and is meant for the end of a build.
1740
+ *
1741
+ * To surface build-level hook signals (`kubb:files:processing:*` and friends) subscribe to
1742
+ * `hooks` and re-emit on the kubb bus.
1269
1743
  */
1270
1744
  var FileProcessor = class {
1271
- events = new AsyncEventEmitter();
1272
- parse(file, { parsers, extension } = {}) {
1273
- const parseExtName = extension?.[file.extname] || void 0;
1745
+ hooks = new AsyncEventEmitter();
1746
+ #parsers;
1747
+ #storage;
1748
+ #extension;
1749
+ #pending = /* @__PURE__ */ new Map();
1750
+ #runningFlush = null;
1751
+ constructor(options) {
1752
+ this.#parsers = options.parsers ?? null;
1753
+ this.#storage = options.storage;
1754
+ this.#extension = options.extension ?? null;
1755
+ }
1756
+ /**
1757
+ * Files waiting in the queue.
1758
+ */
1759
+ get size() {
1760
+ return this.#pending.size;
1761
+ }
1762
+ parse(file) {
1763
+ const parsers = this.#parsers;
1764
+ const parseExtName = this.#extension?.[file.extname] || void 0;
1274
1765
  if (!parsers || !file.extname) return joinSources(file);
1275
1766
  const parser = parsers.get(file.extname);
1276
1767
  if (!parser) return joinSources(file);
1277
1768
  return parser.parse(file, { extname: parseExtName });
1278
1769
  }
1279
- *stream(files, options = {}) {
1770
+ *stream(files) {
1280
1771
  const total = files.length;
1281
1772
  if (total === 0) return;
1282
1773
  let processed = 0;
1283
1774
  for (const file of files) {
1284
- const source = this.parse(file, options);
1775
+ const source = this.parse(file);
1285
1776
  processed++;
1286
1777
  yield {
1287
1778
  file,
@@ -1292,30 +1783,158 @@ var FileProcessor = class {
1292
1783
  };
1293
1784
  }
1294
1785
  }
1295
- async run(files, options = {}) {
1296
- await this.events.emit("start", files);
1297
- for (const { file, source, processed, total, percentage } of this.stream(files, options)) await this.events.emit("update", {
1786
+ async run(files) {
1787
+ await this.hooks.emit("start", files);
1788
+ for (const { file, source, processed, total, percentage } of this.stream(files)) await this.hooks.emit("update", {
1298
1789
  file,
1299
1790
  source,
1300
1791
  processed,
1301
1792
  percentage,
1302
1793
  total
1303
1794
  });
1304
- await this.events.emit("end", files);
1795
+ await this.hooks.emit("end", files);
1305
1796
  return files;
1306
1797
  }
1307
1798
  /**
1308
- * Clears all registered event listeners.
1799
+ * Adds a file to the next flush. A later `enqueue` for the same path replaces the previous
1800
+ * entry, matching `FileManager.upsert`. Fires the `enqueue` event.
1801
+ */
1802
+ enqueue(file) {
1803
+ this.#pending.set(file.path, file);
1804
+ this.hooks.emit("enqueue", file);
1805
+ }
1806
+ /**
1807
+ * Starts processing the queued files. Waits for any previous flush to finish (so two
1808
+ * batches never run together) and then returns without waiting for the new one. The next
1809
+ * `flush` or `drain` picks up the in-flight task.
1810
+ */
1811
+ async flush() {
1812
+ if (this.#runningFlush) await this.#runningFlush;
1813
+ if (this.#pending.size === 0) return;
1814
+ const batch = [...this.#pending.values()];
1815
+ this.#pending.clear();
1816
+ this.#runningFlush = this.#processAndWrite(batch).finally(() => {
1817
+ this.#runningFlush = null;
1818
+ });
1819
+ }
1820
+ /**
1821
+ * Waits for the in-flight flush and writes any files still queued. Fires the `drain` event
1822
+ * when both are done.
1823
+ */
1824
+ async drain() {
1825
+ if (this.#runningFlush) await this.#runningFlush;
1826
+ if (this.#pending.size > 0) {
1827
+ const batch = [...this.#pending.values()];
1828
+ this.#pending.clear();
1829
+ await this.#processAndWrite(batch);
1830
+ }
1831
+ await this.hooks.emit("drain");
1832
+ }
1833
+ async #processAndWrite(files) {
1834
+ const storage = this.#storage;
1835
+ await this.hooks.emit("start", files);
1836
+ const items = [...this.stream(files)];
1837
+ for (const item of items) await this.hooks.emit("update", item);
1838
+ const queue = [];
1839
+ for (const { file, source } of items) if (source) {
1840
+ queue.push(storage.setItem(file.path, source));
1841
+ if (queue.length >= 50) await Promise.all(queue.splice(0));
1842
+ }
1843
+ await Promise.all(queue);
1844
+ await this.hooks.emit("end", files);
1845
+ }
1846
+ /**
1847
+ * Clears every listener and the pending queue.
1309
1848
  */
1310
1849
  dispose() {
1311
- this.events.removeAll();
1850
+ this.hooks.removeAll();
1851
+ this.#pending.clear();
1312
1852
  }
1313
1853
  [Symbol.dispose]() {
1314
1854
  this.dispose();
1315
1855
  }
1316
1856
  };
1317
1857
  //#endregion
1318
- //#region \0@oxc-project+runtime@0.132.0/helpers/usingCtx.js
1858
+ //#region src/HookRegistry.ts
1859
+ /**
1860
+ * Listener bookkeeping around an `AsyncEventEmitter`. Listeners attached through `register`
1861
+ * stay on the emitter but are tracked so `dispose()` removes only them, listeners attached
1862
+ * directly via `emitter.on(...)` survive.
1863
+ */
1864
+ var HookRegistry = class {
1865
+ #emitter;
1866
+ #entries = /* @__PURE__ */ new Set();
1867
+ constructor(options) {
1868
+ this.#emitter = options.emitter;
1869
+ }
1870
+ get emitter() {
1871
+ return this.#emitter;
1872
+ }
1873
+ get size() {
1874
+ return this.#entries.size;
1875
+ }
1876
+ register(options) {
1877
+ this.#emitter.on(options.event, options.handler);
1878
+ this.#entries.add(options);
1879
+ }
1880
+ dispose() {
1881
+ for (const entry of this.#entries) this.#emitter.off(entry.event, entry.handler);
1882
+ this.#entries.clear();
1883
+ }
1884
+ };
1885
+ //#endregion
1886
+ //#region src/Transform.ts
1887
+ /**
1888
+ * Holds one `Visitor` per plugin, keyed by plugin name. Each plugin's transformer runs in
1889
+ * isolation on the original adapter node. `applyTo` is a lookup, not a chain, so plugin A's
1890
+ * visitor never sees plugin B's output. When no transformer is registered, `applyTo` returns
1891
+ * the original node reference, and the `@kubb/ast` `transform` primitive does the same when
1892
+ * its visitor leaves the tree untouched. Callers can compare by identity to detect a no-op.
1893
+ *
1894
+ * Registration order matches the order setup hooks fire, which the driver has already sorted
1895
+ * by `enforce` and dependency edges. The registry does not re-order anything.
1896
+ */
1897
+ var Transform = class {
1898
+ #visitors = /* @__PURE__ */ new Map();
1899
+ /**
1900
+ * Number of plugins with a registered transformer.
1901
+ */
1902
+ get size() {
1903
+ return this.#visitors.size;
1904
+ }
1905
+ /**
1906
+ * Records `visitor` as the transformer for `pluginName`. A second call for the same plugin
1907
+ * replaces the first.
1908
+ */
1909
+ register(pluginName, visitor) {
1910
+ this.#visitors.set(pluginName, visitor);
1911
+ }
1912
+ /**
1913
+ * Looks up the transformer for `pluginName`. The generator context uses this so plugins can
1914
+ * read their own visitor through `ctx.transformer`.
1915
+ */
1916
+ get(pluginName) {
1917
+ return this.#visitors.get(pluginName);
1918
+ }
1919
+ /**
1920
+ * Runs the plugin's transformer on `node`. Returns the original node reference when the
1921
+ * plugin has no transformer, so callers can compare by identity to detect a no-op.
1922
+ */
1923
+ applyTo(pluginName, node) {
1924
+ const visitor = this.#visitors.get(pluginName);
1925
+ if (!visitor) return node;
1926
+ return (0, _kubb_ast.transform)(node, visitor);
1927
+ }
1928
+ /**
1929
+ * Clears every registration. Called from the driver's `dispose()` so visitors do not leak
1930
+ * across builds.
1931
+ */
1932
+ dispose() {
1933
+ this.#visitors.clear();
1934
+ }
1935
+ };
1936
+ //#endregion
1937
+ //#region \0@oxc-project+runtime@0.133.0/helpers/esm/usingCtx.js
1319
1938
  function _usingCtx() {
1320
1939
  var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
1321
1940
  var n = Error();
@@ -1379,13 +1998,6 @@ function _usingCtx() {
1379
1998
  function enforceOrder(enforce) {
1380
1999
  return enforce === "pre" ? -1 : enforce === "post" ? 1 : 0;
1381
2000
  }
1382
- const OPERATION_FILTER_TYPES = new Set([
1383
- "tag",
1384
- "operationId",
1385
- "path",
1386
- "method",
1387
- "contentType"
1388
- ]);
1389
2001
  var KubbDriver = class KubbDriver {
1390
2002
  config;
1391
2003
  options;
@@ -1403,7 +2015,7 @@ var KubbDriver = class KubbDriver {
1403
2015
  }
1404
2016
  /**
1405
2017
  * The streaming `InputStreamNode` produced by the adapter.
1406
- * Always set after adapter setup parse-only adapters are wrapped automatically.
2018
+ * Always set after adapter setup, parse-only adapters are wrapped automatically.
1407
2019
  */
1408
2020
  inputNode = null;
1409
2021
  adapter = null;
@@ -1411,7 +2023,7 @@ var KubbDriver = class KubbDriver {
1411
2023
  * Studio session state, kept together so `dispose()` can reset it atomically.
1412
2024
  *
1413
2025
  * - `source` holds the raw adapter source so `adapter.parse()` can be called lazily.
1414
- * Intentionally outlives the build; cleared by `dispose()`.
2026
+ * Intentionally outlives the build, cleared by `dispose()`.
1415
2027
  * - `isOpen` prevents opening the studio more than once per build.
1416
2028
  * - `inputNode` caches the parse promise so `adapter.parse()` is called at most once
1417
2029
  * per studio session, even when `openInStudio()` is called multiple times.
@@ -1421,14 +2033,12 @@ var KubbDriver = class KubbDriver {
1421
2033
  isOpen: false,
1422
2034
  inputNode: null
1423
2035
  };
1424
- #middlewareListeners = [];
1425
2036
  /**
1426
2037
  * Central file store for all generated files.
1427
2038
  * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
1428
- * add files; this property gives direct read/write access when needed.
2039
+ * add files. This property gives direct read/write access when needed.
1429
2040
  */
1430
2041
  fileManager = new FileManager();
1431
- #fileProcessor = new FileProcessor();
1432
2042
  plugins = /* @__PURE__ */ new Map();
1433
2043
  /**
1434
2044
  * Tracks which plugins have generators registered via `addGenerator()` (event-based path).
@@ -1437,11 +2047,22 @@ var KubbDriver = class KubbDriver {
1437
2047
  #eventGeneratorPlugins = /* @__PURE__ */ new Set();
1438
2048
  #resolvers = /* @__PURE__ */ new Map();
1439
2049
  #defaultResolvers = /* @__PURE__ */ new Map();
1440
- #hookListeners = /* @__PURE__ */ new Map();
2050
+ /**
2051
+ * Tracks every listener the driver added (plugin, middleware, generator) so `dispose()` can
2052
+ * remove them in one pass. Middleware registers after plugins, so it fires last via `Set`
2053
+ * insertion order. External `hooks.on(...)` listeners are not tracked.
2054
+ */
2055
+ #registry;
2056
+ /**
2057
+ * Transform registry. Plugins populate it during `kubb:plugin:setup` via `setTransformer`,
2058
+ * and `#runGenerators` reads it once per `(plugin, node)` pair through `applyTo`.
2059
+ */
2060
+ #transforms = new Transform();
1441
2061
  constructor(config, options) {
1442
2062
  this.config = config;
1443
2063
  this.options = options;
1444
2064
  this.adapter = config.adapter ?? null;
2065
+ this.#registry = new HookRegistry({ emitter: options.hooks });
1445
2066
  }
1446
2067
  async setup() {
1447
2068
  const normalized = this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin));
@@ -1456,7 +2077,7 @@ var KubbDriver = class KubbDriver {
1456
2077
  this.plugins.set(plugin.name, plugin);
1457
2078
  }
1458
2079
  if (this.config.middleware) for (const middleware of this.config.middleware) for (const event of Object.keys(middleware.hooks)) this.#registerMiddleware(event, middleware.hooks);
1459
- if (this.config.adapter) await this.#registerAdapter(this.config.adapter);
2080
+ if (this.config.adapter) this.#studio.source = inputToAdapterSource(this.config);
1460
2081
  }
1461
2082
  get hooks() {
1462
2083
  return this.options.hooks;
@@ -1480,46 +2101,39 @@ var KubbDriver = class KubbDriver {
1480
2101
  if ("apply" in plugin && typeof plugin.apply === "function") normalized.apply = plugin.apply;
1481
2102
  return normalized;
1482
2103
  }
1483
- async #registerAdapter(adapter) {
1484
- const source = inputToAdapterSource(this.config);
1485
- this.#studio.source = source;
2104
+ /**
2105
+ * Parses the adapter source into `this.inputNode`. Idempotent, so repeated calls from
2106
+ * `run` or the studio path do not re-parse. Adapters with `stream()` are used directly.
2107
+ * Adapters with only `parse()` are wrapped via `createStreamInput` so the dispatch loop
2108
+ * stays stream-only.
2109
+ */
2110
+ async #parseInput() {
2111
+ if (this.inputNode || !this.adapter || !this.#studio.source) return;
2112
+ const adapter = this.adapter;
2113
+ const source = this.#studio.source;
1486
2114
  if (adapter.stream) {
1487
2115
  this.inputNode = await adapter.stream(source);
1488
- await this.hooks.emit("kubb:debug", {
1489
- date: /* @__PURE__ */ new Date(),
1490
- logs: [`✓ Adapter '${adapter.name}' producing input stream`]
1491
- });
1492
- } else {
1493
- const inputNode = await adapter.parse(source);
1494
- this.inputNode = (0, _kubb_ast.createStreamInput)(arrayToAsyncIterable(inputNode.schemas), arrayToAsyncIterable(inputNode.operations), inputNode.meta);
1495
- await this.hooks.emit("kubb:debug", {
1496
- date: /* @__PURE__ */ new Date(),
1497
- logs: [
1498
- `✓ Adapter '${adapter.name}' resolved InputNode (wrapped as stream)`,
1499
- ` • Schemas: ${inputNode.schemas.length}`,
1500
- ` • Operations: ${inputNode.operations.length}`
1501
- ]
1502
- });
2116
+ return;
1503
2117
  }
2118
+ const parsed = await adapter.parse(source);
2119
+ this.inputNode = (0, _kubb_ast.createStreamInput)(arrayToAsyncIterable(parsed.schemas), arrayToAsyncIterable(parsed.operations), parsed.meta);
1504
2120
  }
1505
2121
  #registerMiddleware(event, middlewareHooks) {
1506
2122
  const handler = middlewareHooks[event];
1507
2123
  if (!handler) return;
1508
- this.hooks.on(event, handler);
1509
- this.#middlewareListeners.push([event, handler]);
2124
+ this.#registry.register({
2125
+ event,
2126
+ handler,
2127
+ source: "middleware"
2128
+ });
1510
2129
  }
1511
2130
  /**
1512
2131
  * Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.
1513
2132
  *
1514
- * For `kubb:plugin:setup`, the registered listener wraps the globally emitted context with a
1515
- * plugin-specific one so that `addGenerator`, `setResolver`, `setTransformer`, and
1516
- * `setRenderer` all target the correct `normalizedPlugin` entry in the plugins map.
1517
- *
1518
- * All other hooks are iterated and registered directly as pass-through listeners.
1519
- * Any event key present in the global `KubbHooks` interface can be subscribed to.
1520
- *
1521
- * External tooling can subscribe to any of these events via `hooks.on(...)` to observe
1522
- * the plugin lifecycle without modifying plugin behavior.
2133
+ * The `kubb:plugin:setup` listener wraps the global context in a plugin-specific one so
2134
+ * `addGenerator`, `setResolver`, and `setTransformer` target the right `normalizedPlugin`.
2135
+ * Every other `KubbHooks` event registers as a pass-through listener that external tooling
2136
+ * can observe via `hooks.on(...)`.
1523
2137
  *
1524
2138
  * @internal
1525
2139
  */
@@ -1538,10 +2152,7 @@ var KubbDriver = class KubbDriver {
1538
2152
  this.setPluginResolver(plugin.name, resolver);
1539
2153
  },
1540
2154
  setTransformer: (visitor) => {
1541
- plugin.transformer = visitor;
1542
- },
1543
- setRenderer: (renderer) => {
1544
- plugin.renderer = renderer;
2155
+ this.#transforms.register(plugin.name, visitor);
1545
2156
  },
1546
2157
  setOptions: (opts) => {
1547
2158
  plugin.options = {
@@ -1555,13 +2166,21 @@ var KubbDriver = class KubbDriver {
1555
2166
  };
1556
2167
  return hooks["kubb:plugin:setup"](pluginCtx);
1557
2168
  };
1558
- this.hooks.on("kubb:plugin:setup", setupHandler);
1559
- this.#trackHookListener("kubb:plugin:setup", setupHandler);
2169
+ this.#registry.register({
2170
+ event: "kubb:plugin:setup",
2171
+ handler: setupHandler,
2172
+ source: "plugin"
2173
+ });
1560
2174
  }
1561
- for (const [event, handler] of Object.entries(hooks)) {
1562
- if (event === "kubb:plugin:setup" || !handler) continue;
1563
- this.hooks.on(event, handler);
1564
- this.#trackHookListener(event, handler);
2175
+ for (const event of Object.keys(hooks)) {
2176
+ if (event === "kubb:plugin:setup") continue;
2177
+ const handler = hooks[event];
2178
+ if (!handler) continue;
2179
+ this.#registry.register({
2180
+ event,
2181
+ handler,
2182
+ source: "plugin"
2183
+ });
1565
2184
  }
1566
2185
  }
1567
2186
  /**
@@ -1578,7 +2197,6 @@ var KubbDriver = class KubbDriver {
1578
2197
  addGenerator: noop,
1579
2198
  setResolver: noop,
1580
2199
  setTransformer: noop,
1581
- setRenderer: noop,
1582
2200
  setOptions: noop,
1583
2201
  injectFile: noop,
1584
2202
  updateConfig: noop
@@ -1592,52 +2210,56 @@ var KubbDriver = class KubbDriver {
1592
2210
  * respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check
1593
2211
  * so that generators from different plugins do not cross-fire.
1594
2212
  *
1595
- * The renderer resolution chain is: `generator.renderer plugin.renderer config.renderer`.
1596
- * Set `generator.renderer = null` to explicitly opt out of rendering even when the plugin
1597
- * declares a renderer.
2213
+ * The renderer comes from `generator.renderer`. Set `generator.renderer = null` (or leave it
2214
+ * unset) to opt out of rendering.
1598
2215
  *
1599
2216
  * Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
1600
2217
  */
1601
- registerGenerator(pluginName, gen) {
1602
- const resolveRenderer = () => {
1603
- const plugin = this.plugins.get(pluginName);
1604
- return gen.renderer === null ? void 0 : gen.renderer ?? plugin?.renderer ?? this.config.renderer;
1605
- };
1606
- if (gen.schema) {
2218
+ registerGenerator(pluginName, generator) {
2219
+ if (generator.schema) {
1607
2220
  const schemaHandler = async (node, ctx) => {
1608
2221
  if (ctx.plugin.name !== pluginName) return;
1609
- await applyHookResult({
1610
- result: await gen.schema(node, ctx),
1611
- driver: this,
1612
- rendererFactory: resolveRenderer()
2222
+ const result = await generator.schema(node, ctx);
2223
+ await this.dispatch({
2224
+ result,
2225
+ renderer: generator.renderer
1613
2226
  });
1614
2227
  };
1615
- this.hooks.on("kubb:generate:schema", schemaHandler);
1616
- this.#trackHookListener("kubb:generate:schema", schemaHandler);
2228
+ this.#registry.register({
2229
+ event: "kubb:generate:schema",
2230
+ handler: schemaHandler,
2231
+ source: "driver"
2232
+ });
1617
2233
  }
1618
- if (gen.operation) {
2234
+ if (generator.operation) {
1619
2235
  const operationHandler = async (node, ctx) => {
1620
2236
  if (ctx.plugin.name !== pluginName) return;
1621
- await applyHookResult({
1622
- result: await gen.operation(node, ctx),
1623
- driver: this,
1624
- rendererFactory: resolveRenderer()
2237
+ const result = await generator.operation(node, ctx);
2238
+ await this.dispatch({
2239
+ result,
2240
+ renderer: generator.renderer
1625
2241
  });
1626
2242
  };
1627
- this.hooks.on("kubb:generate:operation", operationHandler);
1628
- this.#trackHookListener("kubb:generate:operation", operationHandler);
2243
+ this.#registry.register({
2244
+ event: "kubb:generate:operation",
2245
+ handler: operationHandler,
2246
+ source: "driver"
2247
+ });
1629
2248
  }
1630
- if (gen.operations) {
2249
+ if (generator.operations) {
1631
2250
  const operationsHandler = async (nodes, ctx) => {
1632
2251
  if (ctx.plugin.name !== pluginName) return;
1633
- await applyHookResult({
1634
- result: await gen.operations(nodes, ctx),
1635
- driver: this,
1636
- rendererFactory: resolveRenderer()
2252
+ const result = await generator.operations(nodes, ctx);
2253
+ await this.dispatch({
2254
+ result,
2255
+ renderer: generator.renderer
1637
2256
  });
1638
2257
  };
1639
- this.hooks.on("kubb:generate:operations", operationsHandler);
1640
- this.#trackHookListener("kubb:generate:operations", operationsHandler);
2258
+ this.#registry.register({
2259
+ event: "kubb:generate:operations",
2260
+ handler: operationsHandler,
2261
+ source: "driver"
2262
+ });
1641
2263
  }
1642
2264
  this.#eventGeneratorPlugins.add(pluginName);
1643
2265
  }
@@ -1652,143 +2274,110 @@ var KubbDriver = class KubbDriver {
1652
2274
  return this.#eventGeneratorPlugins.has(pluginName);
1653
2275
  }
1654
2276
  /**
1655
- * Runs the full plugin pipeline. Returns timings/failures collected so far even
1656
- * when an outer hook throws the orchestrator preserves partial state by capturing
1657
- * the error into `error` instead of propagating.
2277
+ * Runs the full plugin pipeline. Returns the diagnostics collected so far even
2278
+ * when an outer hook throws, since the orchestrator preserves partial state by capturing
2279
+ * the failure as a {@link Diagnostic} instead of propagating. Each plugin also
2280
+ * contributes a `timing` diagnostic for the run summary.
1658
2281
  */
1659
2282
  async run({ storage }) {
1660
- const hooks = this.hooks;
1661
- const config = this.config;
1662
- const failedPlugins = /* @__PURE__ */ new Set();
1663
- const pluginTimings = /* @__PURE__ */ new Map();
2283
+ const { hooks, config } = this;
2284
+ const diagnostics = [];
1664
2285
  const parsersMap = /* @__PURE__ */ new Map();
1665
2286
  for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
1666
- const pendingFiles = /* @__PURE__ */ new Map();
1667
- this.fileManager.setOnUpsert((file) => {
1668
- pendingFiles.set(file.path, file);
2287
+ const processor = new FileProcessor({
2288
+ parsers: parsersMap,
2289
+ storage,
2290
+ extension: config.output.extension
1669
2291
  });
1670
- try {
1671
- const flushPending = async () => {
1672
- if (pendingFiles.size === 0) return;
1673
- const files = [...pendingFiles.values()];
1674
- pendingFiles.clear();
1675
- await hooks.emit("kubb:debug", {
1676
- date: /* @__PURE__ */ new Date(),
1677
- logs: [`Writing ${files.length} files...`]
1678
- });
1679
- await hooks.emit("kubb:files:processing:start", { files });
1680
- const items = [...this.#fileProcessor.stream(files, {
1681
- parsers: parsersMap,
1682
- extension: config.output.extension
1683
- })];
1684
- await hooks.emit("kubb:files:processing:update", { files: items.map(({ file, source, processed, total, percentage }) => ({
1685
- file,
1686
- source,
1687
- processed,
1688
- total,
1689
- percentage,
1690
- config
1691
- })) });
1692
- const queue = [];
1693
- for (const { file, source } of items) if (source) {
1694
- queue.push(storage.setItem(file.path, source));
1695
- if (queue.length >= 50) await Promise.all(queue.splice(0));
1696
- }
1697
- await Promise.all(queue);
1698
- await hooks.emit("kubb:files:processing:end", { files });
1699
- await hooks.emit("kubb:debug", {
1700
- date: /* @__PURE__ */ new Date(),
1701
- logs: [`✓ File write process completed for ${files.length} files`]
1702
- });
1703
- };
1704
- await this.emitSetupHooks();
1705
- if (this.adapter && this.inputNode) await hooks.emit("kubb:build:start", Object.assign({
1706
- config,
1707
- adapter: this.adapter,
1708
- meta: this.inputNode.meta,
1709
- getPlugin: this.getPlugin.bind(this)
1710
- }, this.#filesPayload()));
1711
- const generatorPlugins = [];
1712
- for (const plugin of this.plugins.values()) {
1713
- const context = this.getContext(plugin);
1714
- const hrStart = process.hrtime();
1715
- try {
1716
- await hooks.emit("kubb:plugin:start", { plugin });
1717
- await hooks.emit("kubb:debug", {
1718
- date: /* @__PURE__ */ new Date(),
1719
- logs: ["Starting plugin...", ` • Plugin Name: ${plugin.name}`]
1720
- });
1721
- } catch (caughtError) {
1722
- const error = caughtError;
2292
+ processor.hooks.on("start", async (files) => {
2293
+ await hooks.emit("kubb:files:processing:start", { files });
2294
+ });
2295
+ const updateBuffer = [];
2296
+ processor.hooks.on("update", (item) => {
2297
+ updateBuffer.push(item);
2298
+ });
2299
+ processor.hooks.on("end", async (files) => {
2300
+ await hooks.emit("kubb:files:processing:update", { files: updateBuffer.map((item) => ({
2301
+ ...item,
2302
+ config
2303
+ })) });
2304
+ updateBuffer.length = 0;
2305
+ await hooks.emit("kubb:files:processing:end", { files });
2306
+ });
2307
+ const onFileUpsert = (file) => {
2308
+ processor.enqueue(file);
2309
+ };
2310
+ this.fileManager.hooks.on("upsert", onFileUpsert);
2311
+ return Diagnostics.scope((diagnostic) => diagnostics.push(diagnostic), async () => {
2312
+ try {
2313
+ await this.#parseInput();
2314
+ await this.emitSetupHooks();
2315
+ if (this.adapter && this.inputNode) await hooks.emit("kubb:build:start", Object.assign({
2316
+ config,
2317
+ adapter: this.adapter,
2318
+ meta: this.inputNode.meta,
2319
+ getPlugin: this.getPlugin.bind(this)
2320
+ }, this.#filesPayload()));
2321
+ const generatorPlugins = [];
2322
+ for (const plugin of this.plugins.values()) {
2323
+ const context = this.getContext(plugin);
2324
+ const hrStart = process.hrtime();
2325
+ try {
2326
+ await hooks.emit("kubb:plugin:start", { plugin });
2327
+ } catch (caughtError) {
2328
+ const error = caughtError;
2329
+ const duration = getElapsedMs(hrStart);
2330
+ await this.#emitPluginEnd({
2331
+ plugin,
2332
+ duration,
2333
+ success: false,
2334
+ error
2335
+ });
2336
+ diagnostics.push({
2337
+ ...Diagnostics.from(error),
2338
+ plugin: plugin.name
2339
+ }, Diagnostics.performance({
2340
+ plugin: plugin.name,
2341
+ duration
2342
+ }));
2343
+ continue;
2344
+ }
2345
+ if (this.hasEventGenerators(plugin.name)) {
2346
+ generatorPlugins.push({
2347
+ plugin,
2348
+ context,
2349
+ hrStart
2350
+ });
2351
+ continue;
2352
+ }
1723
2353
  const duration = getElapsedMs(hrStart);
1724
- pluginTimings.set(plugin.name, duration);
2354
+ diagnostics.push(Diagnostics.performance({
2355
+ plugin: plugin.name,
2356
+ duration
2357
+ }));
1725
2358
  await this.#emitPluginEnd({
1726
2359
  plugin,
1727
2360
  duration,
1728
- success: false,
1729
- error
2361
+ success: true
1730
2362
  });
1731
- failedPlugins.add({
1732
- plugin,
1733
- error
1734
- });
1735
- continue;
1736
2363
  }
1737
- if (plugin.generators?.length || this.hasEventGenerators(plugin.name)) {
1738
- generatorPlugins.push({
1739
- plugin,
1740
- context,
1741
- hrStart
1742
- });
1743
- continue;
1744
- }
1745
- const duration = getElapsedMs(hrStart);
1746
- pluginTimings.set(plugin.name, duration);
1747
- await this.#emitPluginEnd({
1748
- plugin,
1749
- duration,
1750
- success: true
1751
- });
1752
- await hooks.emit("kubb:debug", {
1753
- date: /* @__PURE__ */ new Date(),
1754
- logs: [`✓ Plugin started successfully (${formatMs(duration)})`]
1755
- });
1756
- }
1757
- if (generatorPlugins.length > 0) if (this.inputNode) {
1758
- const { timings, failed } = await this.#runGenerators(generatorPlugins, flushPending);
1759
- await flushPending();
1760
- for (const [name, duration] of timings) pluginTimings.set(name, duration);
1761
- for (const entry of failed) failedPlugins.add(entry);
1762
- } else for (const { plugin, hrStart } of generatorPlugins) {
1763
- const duration = getElapsedMs(hrStart);
1764
- pluginTimings.set(plugin.name, duration);
1765
- await this.#emitPluginEnd({
1766
- plugin,
1767
- duration,
1768
- success: true
2364
+ diagnostics.push(...await this.#runGenerators(generatorPlugins, () => processor.flush()));
2365
+ await processor.drain();
2366
+ await hooks.emit("kubb:plugins:end", Object.assign({ config }, this.#filesPayload()));
2367
+ await processor.drain();
2368
+ await hooks.emit("kubb:build:end", {
2369
+ files: this.fileManager.files,
2370
+ config,
2371
+ outputDir: (0, node_path.resolve)(config.root, config.output.path)
1769
2372
  });
2373
+ return { diagnostics: Diagnostics.dedupe(diagnostics) };
2374
+ } catch (caughtError) {
2375
+ diagnostics.push(Diagnostics.from(caughtError));
2376
+ return { diagnostics: Diagnostics.dedupe(diagnostics) };
2377
+ } finally {
2378
+ this.fileManager.hooks.off("upsert", onFileUpsert);
1770
2379
  }
1771
- await hooks.emit("kubb:plugins:end", Object.assign({ config }, this.#filesPayload()));
1772
- await flushPending();
1773
- const files = this.fileManager.files;
1774
- await hooks.emit("kubb:build:end", {
1775
- files,
1776
- config,
1777
- outputDir: (0, node_path.resolve)(config.root, config.output.path)
1778
- });
1779
- return {
1780
- failedPlugins,
1781
- pluginTimings
1782
- };
1783
- } catch (caughtError) {
1784
- return {
1785
- failedPlugins,
1786
- pluginTimings,
1787
- error: caughtError
1788
- };
1789
- } finally {
1790
- this.fileManager.setOnUpsert(null);
1791
- }
2380
+ });
1792
2381
  }
1793
2382
  #filesPayload() {
1794
2383
  const driver = this;
@@ -1808,10 +2397,36 @@ var KubbDriver = class KubbDriver {
1808
2397
  config: this.config
1809
2398
  }, this.#filesPayload()));
1810
2399
  }
2400
+ /**
2401
+ * Streams schemas and operations through every plugin's generators. Each node is run
2402
+ * through the plugin's transformer (from `this.#transforms`) before the generator sees it,
2403
+ * so plugins stay isolated and the hot path stays per-node. Schemas run before operations
2404
+ * because the two passes share `flushPending` and the FileProcessor's event emitter.
2405
+ * A failing plugin contributes an error diagnostic so the rest of the build continues.
2406
+ * Every plugin also contributes a `timing` diagnostic.
2407
+ *
2408
+ * When `entries` is empty or `this.inputNode` is `null`, every entry still gets a
2409
+ * `kubb:plugin:end` so middleware listeners (the barrel writer and friends) complete.
2410
+ */
1811
2411
  async #runGenerators(entries, flushPending) {
1812
- const timings = /* @__PURE__ */ new Map();
1813
- const failed = /* @__PURE__ */ new Set();
1814
- const driver = this;
2412
+ const diagnostics = [];
2413
+ if (entries.length === 0) return diagnostics;
2414
+ if (!this.inputNode) {
2415
+ for (const { plugin, hrStart } of entries) {
2416
+ const duration = getElapsedMs(hrStart);
2417
+ diagnostics.push(Diagnostics.performance({
2418
+ plugin: plugin.name,
2419
+ duration
2420
+ }));
2421
+ await this.#emitPluginEnd({
2422
+ plugin,
2423
+ duration,
2424
+ success: true
2425
+ });
2426
+ }
2427
+ return diagnostics;
2428
+ }
2429
+ const transforms = this.#transforms;
1815
2430
  const { schemas, operations } = this.inputNode;
1816
2431
  const states = entries.map(({ plugin, context, hrStart }) => {
1817
2432
  const { exclude, include, override } = plugin.options;
@@ -1841,7 +2456,7 @@ var KubbDriver = class KubbDriver {
1841
2456
  if (pruningStates.length > 0) {
1842
2457
  const allSchemas = [];
1843
2458
  for await (const schema of schemas) allSchemas.push(schema);
1844
- const includedOpsByState = new Map(pruningStates.map((s) => [s, []]));
2459
+ const includedOpsByState = new Map(pruningStates.map((state) => [state, []]));
1845
2460
  for await (const operation of operations) for (const state of pruningStates) {
1846
2461
  const { exclude, include, override } = state.plugin.options;
1847
2462
  if (state.generatorContext.resolver.resolveOptions(operation, {
@@ -1856,33 +2471,45 @@ var KubbDriver = class KubbDriver {
1856
2471
  includedOpsByState.delete(state);
1857
2472
  }
1858
2473
  }
1859
- const resolveRendererFor = (gen, state) => gen.renderer === null ? void 0 : gen.renderer ?? state.plugin.renderer ?? state.generatorContext.config.renderer;
2474
+ const resolveForPlugin = (state, node) => {
2475
+ const { plugin, generatorContext } = state;
2476
+ const transformedNode = transforms.applyTo(plugin.name, node);
2477
+ if (state.optionsAreStatic) return {
2478
+ transformedNode,
2479
+ options: plugin.options
2480
+ };
2481
+ const { exclude, include, override } = plugin.options;
2482
+ const options = generatorContext.resolver.resolveOptions(transformedNode, {
2483
+ options: plugin.options,
2484
+ exclude,
2485
+ include,
2486
+ override
2487
+ });
2488
+ if (options === null) return null;
2489
+ return {
2490
+ transformedNode,
2491
+ options
2492
+ };
2493
+ };
1860
2494
  const dispatchNode = async (state, node, dispatch) => {
1861
2495
  if (state.failed) return;
1862
2496
  try {
1863
- const { plugin, generatorContext, generators } = state;
1864
- const transformedNode = plugin.transformer ? (0, _kubb_ast.transform)(node, plugin.transformer) : node;
2497
+ const resolved = resolveForPlugin(state, node);
2498
+ if (!resolved) return;
2499
+ const { transformedNode, options } = resolved;
1865
2500
  if (dispatch.checkAllowedNames && state.allowedSchemaNames !== null && "name" in transformedNode && transformedNode.name && !state.allowedSchemaNames.has(transformedNode.name)) return;
1866
- const { exclude, include, override } = plugin.options;
1867
- const options = state.optionsAreStatic ? plugin.options : generatorContext.resolver.resolveOptions(transformedNode, {
1868
- options: plugin.options,
1869
- exclude,
1870
- include,
1871
- override
1872
- });
1873
- if (options === null) return;
1874
2501
  const ctx = {
1875
- ...generatorContext,
2502
+ ...state.generatorContext,
1876
2503
  options
1877
2504
  };
1878
- for (const gen of generators) {
1879
- const generate = gen[dispatch.method];
1880
- if (!generate) continue;
1881
- const raw = generate(transformedNode, ctx);
1882
- const applied = applyHookResult({
1883
- result: isPromise(raw) ? await raw : raw,
1884
- driver,
1885
- rendererFactory: resolveRendererFor(gen, state)
2505
+ for (const gen of state.generators) {
2506
+ const run = gen[dispatch.method];
2507
+ if (!run) continue;
2508
+ const raw = run(transformedNode, ctx);
2509
+ const result = isPromise(raw) ? await raw : raw;
2510
+ const applied = this.dispatch({
2511
+ result,
2512
+ renderer: gen.renderer
1886
2513
  });
1887
2514
  if (isPromise(applied)) await applied;
1888
2515
  }
@@ -1902,15 +2529,15 @@ var KubbDriver = class KubbDriver {
1902
2529
  checkAllowedNames: false,
1903
2530
  emit: emitsOperationHook ? (node, ctx) => this.hooks.emit("kubb:generate:operation", node, ctx) : null
1904
2531
  };
1905
- const needsCollectedOperations = this.hooks.listenerCount("kubb:generate:operations") > 0 || states.some((s) => s.generators.some((g) => !!g.operations));
2532
+ const needsCollectedOperations = this.hooks.listenerCount("kubb:generate:operations") > 0 || states.some((state) => state.generators.some((gen) => !!gen.operations));
1906
2533
  const collectedOperations = needsCollectedOperations ? [] : void 0;
1907
- await forBatches(schemas, (nodes) => Promise.all(nodes.flatMap((n) => states.map((state) => dispatchNode(state, n, schemaDispatch)))), {
2534
+ await forBatches(schemas, (nodes) => Promise.all(nodes.flatMap((node) => states.map((state) => dispatchNode(state, node, schemaDispatch)))), {
1908
2535
  concurrency: 8,
1909
2536
  flush: flushPending
1910
2537
  });
1911
2538
  await forBatches(operations, (nodes) => {
1912
2539
  if (needsCollectedOperations) collectedOperations?.push(...nodes);
1913
- return Promise.all(nodes.flatMap((n) => states.map((state) => dispatchNode(state, n, operationDispatch))));
2540
+ return Promise.all(nodes.flatMap((node) => states.map((state) => dispatchNode(state, node, operationDispatch))));
1914
2541
  }, {
1915
2542
  concurrency: 8,
1916
2543
  flush: flushPending
@@ -1922,23 +2549,17 @@ var KubbDriver = class KubbDriver {
1922
2549
  ...generatorContext,
1923
2550
  options: plugin.options
1924
2551
  };
1925
- const ops = collectedOperations ?? [];
1926
- const pluginOperations = state.optionsAreStatic ? ops : ops.filter((node) => {
1927
- const transformed = plugin.transformer ? (0, _kubb_ast.transform)(node, plugin.transformer) : node;
1928
- const { exclude, include, override } = plugin.options;
1929
- return generatorContext.resolver.resolveOptions(transformed, {
1930
- options: plugin.options,
1931
- exclude,
1932
- include,
1933
- override
1934
- }) !== null;
1935
- });
2552
+ const pluginOperations = (collectedOperations ?? []).reduce((acc, node) => {
2553
+ const resolved = resolveForPlugin(state, node);
2554
+ if (resolved) acc.push(resolved.transformedNode);
2555
+ return acc;
2556
+ }, []);
1936
2557
  for (const gen of generators) {
1937
2558
  if (!gen.operations) continue;
1938
- await applyHookResult({
1939
- result: await gen.operations(pluginOperations, ctx),
1940
- driver,
1941
- rendererFactory: resolveRendererFor(gen, state)
2559
+ const result = await gen.operations(pluginOperations, ctx);
2560
+ await this.dispatch({
2561
+ result,
2562
+ renderer: gen.renderer
1942
2563
  });
1943
2564
  }
1944
2565
  await this.hooks.emit("kubb:generate:operations", pluginOperations, ctx);
@@ -1947,60 +2568,80 @@ var KubbDriver = class KubbDriver {
1947
2568
  state.error = caughtError;
1948
2569
  }
1949
2570
  const duration = getElapsedMs(state.hrStart);
1950
- timings.set(state.plugin.name, duration);
1951
2571
  await this.#emitPluginEnd({
1952
2572
  plugin: state.plugin,
1953
2573
  duration,
1954
2574
  success: !state.failed,
1955
2575
  error: state.failed && state.error ? state.error : void 0
1956
2576
  });
1957
- if (state.failed && state.error) failed.add({
1958
- plugin: state.plugin,
1959
- error: state.error
1960
- });
1961
- await this.hooks.emit("kubb:debug", {
1962
- date: /* @__PURE__ */ new Date(),
1963
- logs: [state.failed ? "✗ Plugin start failed" : `✓ Plugin started successfully (${formatMs(duration)})`]
2577
+ if (state.failed && state.error) diagnostics.push({
2578
+ ...Diagnostics.from(state.error),
2579
+ plugin: state.plugin.name
1964
2580
  });
2581
+ diagnostics.push(Diagnostics.performance({
2582
+ plugin: state.plugin.name,
2583
+ duration
2584
+ }));
1965
2585
  }
1966
- return {
1967
- timings,
1968
- failed
1969
- };
2586
+ return diagnostics;
1970
2587
  }
1971
2588
  /**
1972
- * Unregisters all plugin lifecycle listeners from the shared event emitter.
1973
- * Called at the end of a build to prevent listener leaks across repeated builds.
2589
+ * Stores whatever a generator method or `kubb:generate:*` hook returned.
2590
+ *
2591
+ * - An `Array<FileNode>` goes straight into `fileManager` via `upsert`.
2592
+ * - A renderer element runs through `renderer` (the renderer factory, e.g. JSX) and the
2593
+ * produced files go to `fileManager.upsert`.
2594
+ * - A falsy result is treated as a no-op. The generator wrote files itself via
2595
+ * `ctx.upsertFile`.
2596
+ *
2597
+ * Pass `renderer` when the result may be a renderer element. Generators that only return
2598
+ * `Array<FileNode>` do not need one.
2599
+ */
2600
+ async dispatch({ result, renderer }) {
2601
+ try {
2602
+ var _usingCtx$1 = _usingCtx();
2603
+ if (!result) return;
2604
+ if (Array.isArray(result)) {
2605
+ this.fileManager.upsert(...result);
2606
+ return;
2607
+ }
2608
+ if (!renderer) return;
2609
+ const instance = _usingCtx$1.u(renderer());
2610
+ if (instance.stream) {
2611
+ for (const file of instance.stream(result)) this.fileManager.upsert(file);
2612
+ return;
2613
+ }
2614
+ await instance.render(result);
2615
+ this.fileManager.upsert(...instance.files);
2616
+ } catch (_) {
2617
+ _usingCtx$1.e = _;
2618
+ } finally {
2619
+ _usingCtx$1.d();
2620
+ }
2621
+ }
2622
+ /**
2623
+ * Removes every listener the driver added. Listeners attached directly to `hooks` from outside
2624
+ * the driver survive. Called at the end of a build to prevent leaks across repeated builds.
1974
2625
  *
1975
2626
  * @internal
1976
2627
  */
1977
2628
  dispose() {
1978
- for (const [event, handlers] of this.#hookListeners) for (const handler of handlers) this.hooks.off(event, handler);
1979
- this.#hookListeners.clear();
2629
+ this.#registry.dispose();
1980
2630
  this.#eventGeneratorPlugins.clear();
2631
+ this.#transforms.dispose();
1981
2632
  this.#resolvers.clear();
1982
2633
  this.#defaultResolvers.clear();
1983
2634
  this.fileManager.dispose();
1984
- this.#fileProcessor.dispose();
1985
2635
  this.inputNode = null;
1986
2636
  this.#studio = {
1987
2637
  source: null,
1988
2638
  isOpen: false,
1989
2639
  inputNode: null
1990
2640
  };
1991
- for (const [event, handler] of this.#middlewareListeners) this.hooks.off(event, handler);
1992
2641
  }
1993
2642
  [Symbol.dispose]() {
1994
2643
  this.dispose();
1995
2644
  }
1996
- #trackHookListener(event, handler) {
1997
- let handlers = this.#hookListeners.get(event);
1998
- if (!handlers) {
1999
- handlers = /* @__PURE__ */ new Set();
2000
- this.#hookListeners.set(event, handlers);
2001
- }
2002
- handlers.add(handler);
2003
- }
2004
2645
  #getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => defineResolver(() => ({
2005
2646
  name: "default",
2006
2647
  pluginName
@@ -2024,6 +2665,21 @@ var KubbDriver = class KubbDriver {
2024
2665
  }
2025
2666
  getContext(plugin) {
2026
2667
  const driver = this;
2668
+ const report = (diagnostic) => {
2669
+ Diagnostics.report({
2670
+ ...diagnostic,
2671
+ plugin: plugin.name
2672
+ });
2673
+ if (diagnostic.severity === "error") {
2674
+ driver.hooks.emit("kubb:error", { error: diagnostic.cause ?? new Error(diagnostic.message) });
2675
+ return;
2676
+ }
2677
+ if (diagnostic.severity === "warning") {
2678
+ driver.hooks.emit("kubb:warn", { message: diagnostic.message });
2679
+ return;
2680
+ }
2681
+ driver.hooks.emit("kubb:info", { message: diagnostic.message });
2682
+ };
2027
2683
  return {
2028
2684
  config: driver.config,
2029
2685
  get root() {
@@ -2057,21 +2713,47 @@ var KubbDriver = class KubbDriver {
2057
2713
  return driver.getResolver(plugin.name);
2058
2714
  },
2059
2715
  get transformer() {
2060
- return plugin.transformer;
2716
+ return driver.#transforms.get(plugin.name);
2061
2717
  },
2062
2718
  warn(message) {
2063
- driver.hooks.emit("kubb:warn", { message });
2719
+ report({
2720
+ code: diagnosticCode.pluginWarning,
2721
+ severity: "warning",
2722
+ message
2723
+ });
2064
2724
  },
2065
2725
  error(error) {
2066
- driver.hooks.emit("kubb:error", { error: typeof error === "string" ? new Error(error) : error });
2726
+ const cause = typeof error === "string" ? void 0 : error;
2727
+ report({
2728
+ code: diagnosticCode.pluginFailed,
2729
+ severity: "error",
2730
+ message: typeof error === "string" ? error : error.message,
2731
+ cause
2732
+ });
2067
2733
  },
2068
2734
  info(message) {
2069
- driver.hooks.emit("kubb:info", { message });
2735
+ report({
2736
+ code: diagnosticCode.pluginInfo,
2737
+ severity: "info",
2738
+ message
2739
+ });
2070
2740
  },
2071
2741
  async openInStudio(options) {
2072
2742
  if (!driver.config.devtools || driver.#studio.isOpen) return;
2073
- if (typeof driver.config.devtools !== "object") throw new Error("Devtools must be an object");
2074
- if (!driver.adapter || !driver.#studio.source) throw new Error("adapter is not defined, make sure you have set the parser in kubb.config.ts");
2743
+ if (typeof driver.config.devtools !== "object") throw new DiagnosticError({
2744
+ code: diagnosticCode.devtoolsInvalid,
2745
+ severity: "error",
2746
+ message: "The `devtools` config must be an object.",
2747
+ help: "Set `devtools` to an options object, or remove it to disable Kubb Studio.",
2748
+ location: { kind: "config" }
2749
+ });
2750
+ if (!driver.adapter || !driver.#studio.source) throw new DiagnosticError({
2751
+ code: diagnosticCode.adapterRequired,
2752
+ severity: "error",
2753
+ message: "An adapter is required to open Kubb Studio, but none is configured.",
2754
+ help: "Set `adapter` in kubb.config.ts (for example `adapterOas()`).",
2755
+ location: { kind: "config" }
2756
+ });
2075
2757
  driver.#studio.isOpen = true;
2076
2758
  const studioUrl = driver.config.devtools?.studioUrl ?? "https://kubb.studio";
2077
2759
  driver.#studio.inputNode ??= Promise.resolve(driver.adapter.parse(driver.#studio.source));
@@ -2084,59 +2766,25 @@ var KubbDriver = class KubbDriver {
2084
2766
  }
2085
2767
  requirePlugin(pluginName) {
2086
2768
  const plugin = this.plugins.get(pluginName);
2087
- if (!plugin) throw new Error(`[kubb] Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`);
2769
+ if (!plugin) throw new DiagnosticError({
2770
+ code: diagnosticCode.pluginNotFound,
2771
+ severity: "error",
2772
+ message: `Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`,
2773
+ help: `Add "${pluginName}" to the \`plugins\` array in kubb.config.ts, or remove the dependency on it.`,
2774
+ location: { kind: "config" }
2775
+ });
2088
2776
  return plugin;
2089
2777
  }
2090
2778
  };
2091
- /**
2092
- * Handles the return value of a plugin AST hook or generator method.
2093
- *
2094
- * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`
2095
- * - `Array<FileNode>` → added directly into `driver.fileManager`
2096
- * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
2097
- *
2098
- * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result
2099
- * may be a renderer element. Generators that only return `Array<FileNode>` do not need one.
2100
- */
2101
- function applyHookResult({ result, driver, rendererFactory }) {
2102
- if (!result) return;
2103
- if (Array.isArray(result)) {
2104
- driver.fileManager.upsert(...result);
2105
- return;
2106
- }
2107
- if (!rendererFactory) return;
2108
- const renderer = rendererFactory();
2109
- if (renderer.stream) try {
2110
- var _usingCtx$1 = _usingCtx();
2111
- const r = _usingCtx$1.u(renderer);
2112
- for (const file of r.stream(result)) driver.fileManager.upsert(file);
2113
- return;
2114
- } catch (_) {
2115
- _usingCtx$1.e = _;
2116
- } finally {
2117
- _usingCtx$1.d();
2118
- }
2119
- return applyAsyncRender({
2120
- renderer,
2121
- result,
2122
- driver
2123
- });
2124
- }
2125
- async function applyAsyncRender({ renderer, result, driver }) {
2126
- try {
2127
- var _usingCtx3 = _usingCtx();
2128
- const r = _usingCtx3.u(renderer);
2129
- await r.render(result);
2130
- driver.fileManager.upsert(...r.files);
2131
- } catch (_) {
2132
- _usingCtx3.e = _;
2133
- } finally {
2134
- _usingCtx3.d();
2135
- }
2136
- }
2137
2779
  function inputToAdapterSource(config) {
2138
2780
  const input = config.input;
2139
- if (!input) throw new Error("[kubb] input is required when using an adapter. Provide input.path or input.data in your config.");
2781
+ if (!input) throw new DiagnosticError({
2782
+ code: diagnosticCode.inputRequired,
2783
+ severity: "error",
2784
+ message: "An adapter is configured without an input.",
2785
+ help: "Provide `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config.",
2786
+ location: { kind: "config" }
2787
+ });
2140
2788
  if ("data" in input) return {
2141
2789
  type: "data",
2142
2790
  data: input.data
@@ -2163,22 +2811,22 @@ Object.defineProperty(exports, "BuildError", {
2163
2811
  return BuildError;
2164
2812
  }
2165
2813
  });
2166
- Object.defineProperty(exports, "DEFAULT_BANNER", {
2814
+ Object.defineProperty(exports, "DEFAULT_STUDIO_URL", {
2167
2815
  enumerable: true,
2168
2816
  get: function() {
2169
- return DEFAULT_BANNER;
2817
+ return DEFAULT_STUDIO_URL;
2170
2818
  }
2171
2819
  });
2172
- Object.defineProperty(exports, "DEFAULT_EXTENSION", {
2820
+ Object.defineProperty(exports, "DiagnosticError", {
2173
2821
  enumerable: true,
2174
2822
  get: function() {
2175
- return DEFAULT_EXTENSION;
2823
+ return DiagnosticError;
2176
2824
  }
2177
2825
  });
2178
- Object.defineProperty(exports, "DEFAULT_STUDIO_URL", {
2826
+ Object.defineProperty(exports, "Diagnostics", {
2179
2827
  enumerable: true,
2180
2828
  get: function() {
2181
- return DEFAULT_STUDIO_URL;
2829
+ return Diagnostics;
2182
2830
  }
2183
2831
  });
2184
2832
  Object.defineProperty(exports, "FileManager", {
@@ -2223,12 +2871,6 @@ Object.defineProperty(exports, "_usingCtx", {
2223
2871
  return _usingCtx;
2224
2872
  }
2225
2873
  });
2226
- Object.defineProperty(exports, "applyHookResult", {
2227
- enumerable: true,
2228
- get: function() {
2229
- return applyHookResult;
2230
- }
2231
- });
2232
2874
  Object.defineProperty(exports, "definePlugin", {
2233
2875
  enumerable: true,
2234
2876
  get: function() {
@@ -2241,11 +2883,47 @@ Object.defineProperty(exports, "defineResolver", {
2241
2883
  return defineResolver;
2242
2884
  }
2243
2885
  });
2886
+ Object.defineProperty(exports, "diagnosticCatalog", {
2887
+ enumerable: true,
2888
+ get: function() {
2889
+ return diagnosticCatalog;
2890
+ }
2891
+ });
2892
+ Object.defineProperty(exports, "diagnosticCode", {
2893
+ enumerable: true,
2894
+ get: function() {
2895
+ return diagnosticCode;
2896
+ }
2897
+ });
2898
+ Object.defineProperty(exports, "isPerformanceDiagnostic", {
2899
+ enumerable: true,
2900
+ get: function() {
2901
+ return isPerformanceDiagnostic;
2902
+ }
2903
+ });
2904
+ Object.defineProperty(exports, "isProblemDiagnostic", {
2905
+ enumerable: true,
2906
+ get: function() {
2907
+ return isProblemDiagnostic;
2908
+ }
2909
+ });
2910
+ Object.defineProperty(exports, "isUpdateDiagnostic", {
2911
+ enumerable: true,
2912
+ get: function() {
2913
+ return isUpdateDiagnostic;
2914
+ }
2915
+ });
2244
2916
  Object.defineProperty(exports, "logLevel", {
2245
2917
  enumerable: true,
2246
2918
  get: function() {
2247
2919
  return logLevel;
2248
2920
  }
2249
2921
  });
2922
+ Object.defineProperty(exports, "narrowDiagnostic", {
2923
+ enumerable: true,
2924
+ get: function() {
2925
+ return narrowDiagnostic;
2926
+ }
2927
+ });
2250
2928
 
2251
- //# sourceMappingURL=KubbDriver-T8W1wm4O.cjs.map
2929
+ //# sourceMappingURL=KubbDriver-CXoKVRxI.cjs.map