@kubb/core 5.0.0-beta.36 → 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.
@@ -2,6 +2,7 @@ import "./chunk-C0LytTxp.js";
2
2
  import { EventEmitter } from "node:events";
3
3
  import path, { extname, resolve } from "node:path";
4
4
  import { collectUsedSchemaNames, createFile, createStreamInput, extractStringsFromNodes, isOperationNode, isSchemaNode, transform } from "@kubb/ast";
5
+ import { AsyncLocalStorage } from "node:async_hooks";
5
6
  import { deflateSync } from "fflate";
6
7
  import { x } from "tinyexec";
7
8
  //#region ../../internals/utils/src/errors.ts
@@ -36,6 +37,18 @@ var BuildError = class extends Error {
36
37
  function toError(value) {
37
38
  return value instanceof Error ? value : new Error(String(value));
38
39
  }
40
+ /**
41
+ * Extracts a human-readable message from any thrown value.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * getErrorMessage(new Error('oops')) // 'oops'
46
+ * getErrorMessage('plain string') // 'plain string'
47
+ * ```
48
+ */
49
+ function getErrorMessage(value) {
50
+ return value instanceof Error ? value.message : String(value);
51
+ }
39
52
  //#endregion
40
53
  //#region ../../internals/utils/src/asyncEventEmitter.ts
41
54
  /**
@@ -135,6 +148,24 @@ var AsyncEventEmitter = class {
135
148
  return this.#emitter.listenerCount(eventName);
136
149
  }
137
150
  /**
151
+ * Raises or lowers the per-event listener ceiling before Node warns about a memory leak.
152
+ * Set this above the expected listener count when many listeners attach by design.
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * emitter.setMaxListeners(40)
157
+ * ```
158
+ */
159
+ setMaxListeners(max) {
160
+ this.#emitter.setMaxListeners(max);
161
+ }
162
+ /**
163
+ * Returns the current per-event listener ceiling.
164
+ */
165
+ getMaxListeners() {
166
+ return this.#emitter.getMaxListeners();
167
+ }
168
+ /**
138
169
  * Removes all listeners from every event channel.
139
170
  *
140
171
  * @example
@@ -228,21 +259,6 @@ function getElapsedMs(hrStart) {
228
259
  const ms = seconds * 1e3 + nanoseconds / 1e6;
229
260
  return Math.round(ms * 100) / 100;
230
261
  }
231
- /**
232
- * Converts a millisecond duration into a human-readable string (`ms`, `s`, or `m s`).
233
- *
234
- * @example
235
- * ```ts
236
- * formatMs(250) // '250ms'
237
- * formatMs(1500) // '1.50s'
238
- * formatMs(90000) // '1m 30.0s'
239
- * ```
240
- */
241
- function formatMs(ms) {
242
- if (ms >= 6e4) return `${Math.floor(ms / 6e4)}m ${(ms % 6e4 / 1e3).toFixed(1)}s`;
243
- if (ms >= 1e3) return `${(ms / 1e3).toFixed(2)}s`;
244
- return `${Math.round(ms)}ms`;
245
- }
246
262
  //#endregion
247
263
  //#region ../../internals/utils/src/promise.ts
248
264
  function* chunks(arr, size) {
@@ -610,13 +626,17 @@ var URLPath = class {
610
626
  */
611
627
  const DEFAULT_STUDIO_URL = "https://kubb.studio";
612
628
  /**
613
- * Default banner style written at the top of every generated file.
614
- */
615
- const DEFAULT_BANNER = "simple";
616
- /**
617
- * Default file-extension mapping used when no explicit mapping is configured.
629
+ * Plugin `include` filter types that select operations directly. When one of these is set
630
+ * without a `schemaName` include, the generate phase pre-scans operations to compute the set
631
+ * of schemas they reach, so unreachable schemas can be pruned for that plugin.
618
632
  */
619
- const DEFAULT_EXTENSION = { ".ts": ".ts" };
633
+ const OPERATION_FILTER_TYPES = new Set([
634
+ "tag",
635
+ "operationId",
636
+ "path",
637
+ "method",
638
+ "contentType"
639
+ ]);
620
640
  /**
621
641
  * Numeric log-level thresholds used internally to compare verbosity.
622
642
  *
@@ -627,8 +647,453 @@ const logLevel = {
627
647
  error: 0,
628
648
  warn: 1,
629
649
  info: 3,
630
- verbose: 4,
631
- debug: 5
650
+ verbose: 4
651
+ };
652
+ /**
653
+ * Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode
654
+ * and stays stable so it can be referenced in tooling and (later) docs. Reference
655
+ * these instead of inlining the string at a throw site.
656
+ */
657
+ const diagnosticCode = {
658
+ /**
659
+ * Fallback for an unstructured error with no specific code.
660
+ */
661
+ unknown: "KUBB_UNKNOWN",
662
+ /**
663
+ * The `input.path` file or URL could not be read.
664
+ */
665
+ inputNotFound: "KUBB_INPUT_NOT_FOUND",
666
+ /**
667
+ * An adapter was configured without an `input`.
668
+ */
669
+ inputRequired: "KUBB_INPUT_REQUIRED",
670
+ /**
671
+ * A `$ref` (or equivalent reference) could not be resolved in the source document.
672
+ */
673
+ refNotFound: "KUBB_REF_NOT_FOUND",
674
+ /**
675
+ * A server variable value is not allowed by its `enum`.
676
+ */
677
+ invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE",
678
+ /**
679
+ * A required plugin is missing from the config.
680
+ */
681
+ pluginNotFound: "KUBB_PLUGIN_NOT_FOUND",
682
+ /**
683
+ * A plugin threw while generating.
684
+ */
685
+ pluginFailed: "KUBB_PLUGIN_FAILED",
686
+ /**
687
+ * A plugin reported a non-fatal warning through `ctx.warn`.
688
+ */
689
+ pluginWarning: "KUBB_PLUGIN_WARNING",
690
+ /**
691
+ * A plugin reported an informational message through `ctx.info`.
692
+ */
693
+ pluginInfo: "KUBB_PLUGIN_INFO",
694
+ /**
695
+ * A schema uses a `format` Kubb does not map to a specific type. Reserved for
696
+ * adapters to emit as a `warning`.
697
+ */
698
+ unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT",
699
+ /**
700
+ * A referenced schema or operation is marked `deprecated`. Reserved for adapters
701
+ * to emit as an `info`.
702
+ */
703
+ deprecated: "KUBB_DEPRECATED",
704
+ /**
705
+ * An adapter is required but the config has none. The build cannot read the input
706
+ * without one.
707
+ */
708
+ adapterRequired: "KUBB_ADAPTER_REQUIRED",
709
+ /**
710
+ * The `devtools` config is set to something other than an object.
711
+ */
712
+ devtoolsInvalid: "KUBB_DEVTOOLS_INVALID",
713
+ /**
714
+ * A resolved output path escapes the output directory, which can stem from a path
715
+ * traversal in the spec or a misconfigured `group.name`.
716
+ */
717
+ pathTraversal: "KUBB_PATH_TRAVERSAL",
718
+ /**
719
+ * A post-generate shell hook (`hooks.done`) exited with a failure.
720
+ */
721
+ hookFailed: "KUBB_HOOK_FAILED",
722
+ /**
723
+ * The formatter pass over the generated files failed.
724
+ */
725
+ formatFailed: "KUBB_FORMAT_FAILED",
726
+ /**
727
+ * The linter pass over the generated files failed.
728
+ */
729
+ lintFailed: "KUBB_LINT_FAILED",
730
+ /**
731
+ * Not a failure. Carries a plugin's elapsed time, summed into the run total.
732
+ */
733
+ performance: "KUBB_PERFORMANCE",
734
+ /**
735
+ * Not a failure. A newer Kubb version is available on npm.
736
+ */
737
+ updateAvailable: "KUBB_UPDATE_AVAILABLE"
738
+ };
739
+ //#endregion
740
+ //#region src/diagnostics.ts
741
+ /**
742
+ * Docs major, derived from the package version so the link tracks the published major.
743
+ */
744
+ const docsMajor = "5.0.0-beta.37".split(".")[0] ?? "5";
745
+ /**
746
+ * Narrows a {@link Diagnostic} to the variant for `code`, or `null` when it does not match.
747
+ *
748
+ * @example
749
+ * ```ts
750
+ * const update = narrowDiagnostic(diagnostic, diagnosticCode.updateAvailable)
751
+ * if (update) {
752
+ * console.log(update.latestVersion)
753
+ * }
754
+ * ```
755
+ */
756
+ function narrowDiagnostic(diagnostic, code) {
757
+ return diagnostic.code === code ? diagnostic : null;
758
+ }
759
+ /**
760
+ * Builds a type guard that narrows a {@link Diagnostic} to the variant for `kind`. A diagnostic
761
+ * with no `kind` is treated as a `problem`.
762
+ */
763
+ function isKind(kind) {
764
+ return (diagnostic) => (diagnostic.kind ?? "problem") === kind;
765
+ }
766
+ /**
767
+ * Returns `true` when the diagnostic is a build {@link ProblemDiagnostic}.
768
+ *
769
+ * @example
770
+ * ```ts
771
+ * if (isProblemDiagnostic(diagnostic)) {
772
+ * console.log(diagnostic.location)
773
+ * }
774
+ * ```
775
+ */
776
+ const isProblemDiagnostic = isKind("problem");
777
+ /**
778
+ * Returns `true` when the diagnostic is a per-plugin {@link PerformanceDiagnostic}.
779
+ *
780
+ * @example
781
+ * ```ts
782
+ * const timings = diagnostics.filter(isPerformanceDiagnostic)
783
+ * ```
784
+ */
785
+ const isPerformanceDiagnostic = isKind("performance");
786
+ /**
787
+ * Returns `true` when the diagnostic is a version-update {@link UpdateDiagnostic}.
788
+ *
789
+ * @example
790
+ * ```ts
791
+ * if (isUpdateDiagnostic(diagnostic)) {
792
+ * console.log(diagnostic.latestVersion)
793
+ * }
794
+ * ```
795
+ */
796
+ const isUpdateDiagnostic = isKind("update");
797
+ /**
798
+ * An `Error` that carries a {@link Diagnostic}, so structured problems can flow
799
+ * through the existing throw/catch paths while keeping their code and location.
800
+ *
801
+ * @example
802
+ * ```ts
803
+ * throw new DiagnosticError({ code: diagnosticCode.refNotFound, severity: 'error', message: `Could not find ${ref}`, location: { kind: 'schema', pointer: ref, ref } })
804
+ * ```
805
+ */
806
+ var DiagnosticError = class extends Error {
807
+ diagnostic;
808
+ constructor(diagnostic) {
809
+ super(diagnostic.message, { cause: diagnostic.cause });
810
+ this.name = "DiagnosticError";
811
+ this.diagnostic = diagnostic;
812
+ }
813
+ };
814
+ /**
815
+ * Structural check for a {@link DiagnosticError}, including one thrown from a duplicated
816
+ * `@kubb/core` copy where `instanceof` fails. Matches on the `name` and a `diagnostic`
817
+ * that carries a `code`.
818
+ */
819
+ function isDiagnosticError(error) {
820
+ if (error instanceof DiagnosticError) return true;
821
+ return error instanceof Error && error.name === "DiagnosticError" && "diagnostic" in error && typeof error.diagnostic === "object" && error.diagnostic !== null && typeof error.diagnostic?.code === "string";
822
+ }
823
+ /**
824
+ * Explanation for every {@link diagnosticCode}. Use {@link Diagnostics.explain} to look one up
825
+ * and `Diagnostics.docsUrl` for the matching kubb.dev page.
826
+ */
827
+ const diagnosticCatalog = {
828
+ [diagnosticCode.unknown]: {
829
+ title: "Unknown error",
830
+ cause: "An error was thrown without a stable Kubb code, so it is reported as-is.",
831
+ 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."
832
+ },
833
+ [diagnosticCode.inputNotFound]: {
834
+ title: "Input not found",
835
+ cause: "The file or URL set in `input.path` (or passed as `kubb generate PATH`) could not be read.",
836
+ fix: "Check that the path or URL exists and is readable, then set it in `input.path` or pass it on the CLI."
837
+ },
838
+ [diagnosticCode.inputRequired]: {
839
+ title: "Input required",
840
+ cause: "An adapter is configured but no `input` was provided.",
841
+ fix: "Set `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config."
842
+ },
843
+ [diagnosticCode.refNotFound]: {
844
+ title: "Reference not found",
845
+ cause: "A `$ref` could not be resolved in the source document.",
846
+ fix: "Add the missing definition (for example under `components.schemas`) or fix the `$ref`. Run `kubb validate` to check the spec."
847
+ },
848
+ [diagnosticCode.invalidServerVariable]: {
849
+ title: "Invalid server variable",
850
+ cause: "A server variable value is not allowed by its `enum`.",
851
+ fix: "Use one of the values listed in the server variable `enum`, or update the spec."
852
+ },
853
+ [diagnosticCode.pluginNotFound]: {
854
+ title: "Plugin not found",
855
+ cause: "A plugin that another plugin depends on is missing from the config.",
856
+ fix: "Add the required plugin to the `plugins` array in kubb.config.ts, or remove the dependency on it."
857
+ },
858
+ [diagnosticCode.pluginFailed]: {
859
+ title: "Plugin failed",
860
+ cause: "A plugin threw while generating, or reported an error through `ctx.error`.",
861
+ fix: "Read the underlying error and check the plugin options and the schema or operation it failed on."
862
+ },
863
+ [diagnosticCode.pluginWarning]: {
864
+ title: "Plugin warning",
865
+ cause: "A plugin reported a non-fatal warning through `ctx.warn`.",
866
+ fix: "Review the message. It does not fail the build; adjust the plugin options or input if the warning is unwanted."
867
+ },
868
+ [diagnosticCode.pluginInfo]: {
869
+ title: "Plugin info",
870
+ cause: "A plugin reported an informational message through `ctx.info`.",
871
+ fix: "Informational only. No action is required."
872
+ },
873
+ [diagnosticCode.unsupportedFormat]: {
874
+ title: "Unsupported format",
875
+ cause: "A schema uses a `format` Kubb does not map to a specific type, so it falls back to the base type.",
876
+ fix: "Use a format Kubb supports, or handle the custom format with a parser or plugin."
877
+ },
878
+ [diagnosticCode.deprecated]: {
879
+ title: "Deprecated",
880
+ cause: "A referenced schema or operation is marked `deprecated`.",
881
+ fix: "Migrate off the deprecated definition if the warning is unwanted."
882
+ },
883
+ [diagnosticCode.adapterRequired]: {
884
+ title: "Adapter required",
885
+ cause: "An action needs an adapter (for example opening Kubb Studio) but none is configured.",
886
+ fix: "Set `adapter` in kubb.config.ts, for example `adapterOas()`."
887
+ },
888
+ [diagnosticCode.devtoolsInvalid]: {
889
+ title: "Invalid devtools config",
890
+ cause: "The `devtools` config is set to something other than an object.",
891
+ fix: "Set `devtools` to an options object, or remove it to disable Kubb Studio."
892
+ },
893
+ [diagnosticCode.pathTraversal]: {
894
+ title: "Path traversal",
895
+ cause: "A resolved output path escaped the output directory, which can stem from a path traversal in the spec or a misconfigured `group.name`.",
896
+ fix: "Keep generated paths within the output directory. Review the `group.name` function and the names coming from the spec."
897
+ },
898
+ [diagnosticCode.hookFailed]: {
899
+ title: "Hook failed",
900
+ cause: "A post-generate shell hook (`hooks.done`) exited with a non-zero status.",
901
+ fix: "Check the command is installed and correct, and run it manually to see the error."
902
+ },
903
+ [diagnosticCode.formatFailed]: {
904
+ title: "Format failed",
905
+ cause: "The formatter pass over the generated files failed.",
906
+ fix: "Check the formatter (oxfmt, biome, or prettier) is installed and its config is valid, then run it manually on the output."
907
+ },
908
+ [diagnosticCode.lintFailed]: {
909
+ title: "Lint failed",
910
+ cause: "The linter pass over the generated files failed.",
911
+ fix: "Check the linter (oxlint, biome, or eslint) is installed and its config is valid, then run it manually on the output."
912
+ },
913
+ [diagnosticCode.performance]: {
914
+ title: "Performance",
915
+ cause: "Not a failure. Records a plugin’s elapsed time, summed into the run total.",
916
+ fix: "No action. This is an informational metric."
917
+ },
918
+ [diagnosticCode.updateAvailable]: {
919
+ title: "Update available",
920
+ cause: "A newer Kubb version is published on npm than the one running.",
921
+ fix: "Update the `@kubb/*` packages, for example `npm install -g @kubb/cli`, to get the latest fixes."
922
+ }
923
+ };
924
+ /**
925
+ * Static helpers for working with {@link Diagnostic}s, plus the run-scoped sink
926
+ * that lets deep code report a diagnostic without threading a callback.
927
+ *
928
+ * The sink lives in a single `AsyncLocalStorage` in the `@kubb/core` bundle.
929
+ * `Diagnostics.scope` activates it for a run, so anything inside that run (the
930
+ * adapter parse, a lazily consumed stream, a generator) reports through
931
+ * `Diagnostics.report` and lands in the same run.
932
+ */
933
+ var Diagnostics = class Diagnostics {
934
+ static #reporterStorage = new AsyncLocalStorage();
935
+ /**
936
+ * Runs `fn` with `sink` as the active diagnostic sink for the whole async
937
+ * subtree, so {@link Diagnostics.report} reaches it from anywhere inside.
938
+ */
939
+ static scope(sink, fn) {
940
+ return Diagnostics.#reporterStorage.run(sink, fn);
941
+ }
942
+ /**
943
+ * Collects a diagnostic into the active build via the run-scoped sink, without throwing.
944
+ * Returns `true` when a run consumed it, `false` when called outside a {@link Diagnostics.scope}
945
+ * (so callers can fall back to throwing). Use a `warning`/`info` severity for non-fatal issues.
946
+ * For rendering a diagnostic live on the hook bus, use {@link Diagnostics.emit} instead.
947
+ */
948
+ static report(diagnostic) {
949
+ const sink = Diagnostics.#reporterStorage.getStore();
950
+ if (!sink) return false;
951
+ sink(diagnostic);
952
+ return true;
953
+ }
954
+ /**
955
+ * Emits a diagnostic on the run's `kubb:diagnostic` event so the loggers render it live.
956
+ * Use it instead of calling `hooks.emit('kubb:diagnostic', ...)` directly. To collect a
957
+ * diagnostic into the build result from deep in a run, use {@link Diagnostics.report} instead.
958
+ */
959
+ static async emit(hooks, diagnostic) {
960
+ await hooks.emit("kubb:diagnostic", { diagnostic });
961
+ }
962
+ /**
963
+ * Coerces any thrown value into a {@link ProblemDiagnostic}. A {@link DiagnosticError}
964
+ * keeps its structured data, and anything else becomes a `KUBB_UNKNOWN` error.
965
+ */
966
+ static from(error) {
967
+ const seen = /* @__PURE__ */ new Set();
968
+ let current = error;
969
+ let root;
970
+ while (current instanceof Error && !seen.has(current)) {
971
+ if (isDiagnosticError(current)) return current.diagnostic;
972
+ seen.add(current);
973
+ root = current;
974
+ current = current.cause;
975
+ }
976
+ return {
977
+ code: diagnosticCode.unknown,
978
+ severity: "error",
979
+ message: root ? root.message : getErrorMessage(error),
980
+ cause: root
981
+ };
982
+ }
983
+ /**
984
+ * Builds a per-plugin performance record. Reporters sum these into the run total.
985
+ */
986
+ static performance({ plugin, duration }) {
987
+ return {
988
+ kind: "performance",
989
+ code: diagnosticCode.performance,
990
+ severity: "info",
991
+ message: `${plugin} generated in ${Math.round(duration)}ms`,
992
+ plugin,
993
+ duration
994
+ };
995
+ }
996
+ /**
997
+ * Builds the version-update notice shown when a newer Kubb is published on npm.
998
+ */
999
+ static update({ currentVersion, latestVersion }) {
1000
+ return {
1001
+ kind: "update",
1002
+ code: diagnosticCode.updateAvailable,
1003
+ severity: "info",
1004
+ message: `Update available: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`,
1005
+ currentVersion,
1006
+ latestVersion
1007
+ };
1008
+ }
1009
+ /**
1010
+ * True when any diagnostic is an error, the severity that fails a build. Non-error
1011
+ * diagnostics are ignored.
1012
+ */
1013
+ static hasError(diagnostics) {
1014
+ return diagnostics.some((diagnostic) => diagnostic.severity === "error");
1015
+ }
1016
+ /**
1017
+ * Names of the plugins that failed, deduped, derived from the error diagnostics
1018
+ * that carry a `plugin`.
1019
+ */
1020
+ static failedPlugins(diagnostics) {
1021
+ const names = /* @__PURE__ */ new Set();
1022
+ for (const diagnostic of diagnostics) if (diagnostic.severity === "error" && diagnostic.plugin) names.add(diagnostic.plugin);
1023
+ return [...names];
1024
+ }
1025
+ /**
1026
+ * Counts `problem` diagnostics by severity for the run summary. `timing`
1027
+ * diagnostics are ignored.
1028
+ */
1029
+ static count(diagnostics) {
1030
+ let errors = 0;
1031
+ let warnings = 0;
1032
+ let infos = 0;
1033
+ for (const diagnostic of diagnostics) {
1034
+ if (!isProblemDiagnostic(diagnostic)) continue;
1035
+ if (diagnostic.severity === "error") errors += 1;
1036
+ else if (diagnostic.severity === "warning") warnings += 1;
1037
+ else infos += 1;
1038
+ }
1039
+ return {
1040
+ errors,
1041
+ warnings,
1042
+ infos
1043
+ };
1044
+ }
1045
+ /**
1046
+ * Drops duplicate `problem` diagnostics that share a code, location pointer, and
1047
+ * plugin, so the same issue reported across several passes is shown once. Non-problem
1048
+ * diagnostics are always kept.
1049
+ */
1050
+ static dedupe(diagnostics) {
1051
+ const seen = /* @__PURE__ */ new Set();
1052
+ const result = [];
1053
+ for (const diagnostic of diagnostics) {
1054
+ if (!isProblemDiagnostic(diagnostic)) {
1055
+ result.push(diagnostic);
1056
+ continue;
1057
+ }
1058
+ const pointer = diagnostic.location && "pointer" in diagnostic.location ? diagnostic.location.pointer : "";
1059
+ const key = `${diagnostic.code} ${pointer} ${diagnostic.plugin ?? ""}`;
1060
+ if (seen.has(key)) continue;
1061
+ seen.add(key);
1062
+ result.push(diagnostic);
1063
+ }
1064
+ return result;
1065
+ }
1066
+ /**
1067
+ * Builds the kubb.dev docs URL for a diagnostic code, e.g.
1068
+ * `KUBB_REF_NOT_FOUND` → `https://kubb.dev/docs/5.x/reference/diagnostics/kubb-ref-not-found`.
1069
+ */
1070
+ static docsUrl(code) {
1071
+ return `https://kubb.dev/docs/${docsMajor}.x/reference/diagnostics/${code.toLowerCase().replaceAll("_", "-")}`;
1072
+ }
1073
+ /**
1074
+ * The catalog entry for a code: its title, cause, and fix. Mirrors the kubb.dev
1075
+ * `/diagnostics/<slug>` page.
1076
+ */
1077
+ static explain(code) {
1078
+ return diagnosticCatalog[code];
1079
+ }
1080
+ /**
1081
+ * Reduces a diagnostic to its JSON-safe fields plus a `docsUrl`, for machine-readable
1082
+ * consumers. The `cause`, `kind`, and `duration` are dropped, and absent optional
1083
+ * fields are omitted rather than set to `undefined`.
1084
+ */
1085
+ static serialize(diagnostic) {
1086
+ const problem = isProblemDiagnostic(diagnostic) ? diagnostic : void 0;
1087
+ return {
1088
+ code: diagnostic.code,
1089
+ severity: diagnostic.severity,
1090
+ message: diagnostic.message,
1091
+ ...problem?.location ? { location: problem.location } : {},
1092
+ ...problem?.help ? { help: problem.help } : {},
1093
+ ...problem?.plugin ? { plugin: problem.plugin } : {},
1094
+ ...diagnostic.code === diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
1095
+ };
1096
+ }
632
1097
  };
633
1098
  //#endregion
634
1099
  //#region src/definePlugin.ts
@@ -738,7 +1203,7 @@ function defaultResolver(name, type) {
738
1203
  return camelCase(name);
739
1204
  }
740
1205
  /**
741
- * Default option resolver applies include/exclude filters and merges matching override options.
1206
+ * Default option resolver. Applies include/exclude filters and merges matching override options.
742
1207
  *
743
1208
  * Returns `null` when the node is filtered out by an `exclude` rule or not matched by any `include` rule.
744
1209
  *
@@ -774,7 +1239,7 @@ function computeOptions(node, options, exclude, include, override) {
774
1239
  if (isSchemaNode(node)) {
775
1240
  if (exclude.some(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)) return null;
776
1241
  if (include) {
777
- const applicable = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)).filter((r) => r !== null);
1242
+ const applicable = include.map(({ type, pattern }) => matchesSchemaPattern(node, type, pattern)).filter((result) => result !== null);
778
1243
  if (applicable.length > 0 && !applicable.includes(true)) return null;
779
1244
  }
780
1245
  const overrideOptions = override.find(({ type, pattern }) => matchesSchemaPattern(node, type, pattern) === true)?.options;
@@ -847,8 +1312,8 @@ function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root
847
1312
  const result = (() => {
848
1313
  if (group && (groupPath || tag)) {
849
1314
  const groupValue = group.type === "path" ? groupPath : tag;
850
- const defaultName = group.type === "tag" ? ({ group: g }) => `${camelCase(g)}Controller` : ({ group: g }) => {
851
- const segment = g.split("/").filter((s) => s !== "" && s !== "." && s !== "..")[0];
1315
+ const defaultName = group.type === "tag" ? ({ group: groupName }) => `${camelCase(groupName)}Controller` : ({ group: groupName }) => {
1316
+ const segment = groupName.split("/").filter((part) => part !== "" && part !== "." && part !== "..")[0];
852
1317
  return segment ? camelCase(segment) : "";
853
1318
  };
854
1319
  const resolveName = group.name ?? defaultName;
@@ -858,7 +1323,13 @@ function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root
858
1323
  })();
859
1324
  const outputDir = path.resolve(root, output.path);
860
1325
  const outputDirWithSep = outputDir.endsWith(path.sep) ? outputDir : `${outputDir}${path.sep}`;
861
- 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.`);
1326
+ if (result !== outputDir && !result.startsWith(outputDirWithSep)) throw new DiagnosticError({
1327
+ code: diagnosticCode.pathTraversal,
1328
+ severity: "error",
1329
+ message: `Resolved path "${result}" is outside the output directory "${outputDir}".`,
1330
+ 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.",
1331
+ location: { kind: "config" }
1332
+ });
862
1333
  return result;
863
1334
  }
864
1335
  /**
@@ -866,7 +1337,7 @@ function defaultResolvePath({ baseName, pathMode, tag, path: groupPath }, { root
866
1337
  *
867
1338
  * Resolves a `FileNode` by combining name resolution (`resolver.default`) with
868
1339
  * path resolution (`resolver.resolvePath`). The resolved file always has empty
869
- * `sources`, `imports`, and `exports` arrays consumers populate those separately.
1340
+ * `sources`, `imports`, and `exports` arrays, which consumers populate separately.
870
1341
  *
871
1342
  * In `single` mode the name is omitted and the file sits directly in the output directory.
872
1343
  *
@@ -942,7 +1413,7 @@ function buildDefaultBanner({ title, description, version, config }) {
942
1413
  }
943
1414
  }
944
1415
  /**
945
- * Default banner resolver returns the banner string for a generated file.
1416
+ * Default banner resolver. Returns the banner string for a generated file.
946
1417
  *
947
1418
  * A user-supplied `output.banner` overrides the default Kubb "Generated by Kubb" notice.
948
1419
  * When no `output.banner` is set, the Kubb notice is used (including `title` and `version`
@@ -971,7 +1442,7 @@ function buildDefaultBanner({ title, description, version, config }) {
971
1442
  * // → ''
972
1443
  * ```
973
1444
  *
974
- * @example No user banner Kubb notice with OAS metadata
1445
+ * @example No user banner, Kubb notice with OAS metadata
975
1446
  * ```ts
976
1447
  * defaultResolveBanner(meta, { config })
977
1448
  * // → '/** Generated by Kubb ... Title: Pet Store ... *\/'
@@ -997,7 +1468,7 @@ function defaultResolveBanner(meta, { output, config, file }) {
997
1468
  });
998
1469
  }
999
1470
  /**
1000
- * Default footer resolver returns the footer string for a generated file.
1471
+ * Default footer resolver. Returns the footer string for a generated file.
1001
1472
  *
1002
1473
  * - When `output.footer` is a function, calls it with the file's `BannerMeta` and returns the result.
1003
1474
  * - When `output.footer` is a string, returns it directly.
@@ -1029,11 +1500,11 @@ function defaultResolveFooter(meta, { output, file }) {
1029
1500
  * name casing, include/exclude/override filtering, output path computation,
1030
1501
  * and file construction. Supply your own to override any of them:
1031
1502
  *
1032
- * - `default` name casing strategy (camelCase / PascalCase).
1033
- * - `resolveOptions` include/exclude/override filtering.
1034
- * - `resolvePath` output path computation.
1035
- * - `resolveFile` full `FileNode` construction.
1036
- * - `resolveBanner` / `resolveFooter` top/bottom-of-file text.
1503
+ * - `default` sets the name casing strategy (camelCase or PascalCase).
1504
+ * - `resolveOptions` does include/exclude/override filtering.
1505
+ * - `resolvePath` computes the output path.
1506
+ * - `resolveFile` builds the full `FileNode`.
1507
+ * - `resolveBanner` and `resolveFooter` produce the top and bottom of file text.
1037
1508
  *
1038
1509
  * Methods in the returned object can call sibling resolver methods via `this`,
1039
1510
  * which keeps custom rules small (`this.default(name, 'type')` to delegate).
@@ -1082,7 +1553,7 @@ function defineResolver(build) {
1082
1553
  * Encodes an `InputNode` as a compressed, URL-safe string.
1083
1554
  *
1084
1555
  * The JSON representation is deflate-compressed with {@link deflateSync} before
1085
- * base64url encoding, which typically reduces payload size by 7080 % and
1556
+ * base64url encoding, which typically reduces payload size by 70, 80 % and
1086
1557
  * keeps URLs well within browser and server path-length limits.
1087
1558
  */
1088
1559
  function encodeAst(input) {
@@ -1099,8 +1570,7 @@ function getStudioUrl(input, studioUrl, options = {}) {
1099
1570
  return `${studioUrl.replace(/\/$/, "")}${options.ast ? "/ast" : ""}?root=${encodeAst(input)}`;
1100
1571
  }
1101
1572
  /**
1102
- * Opens the Kubb Studio URL for the given `InputNode` in the default browser
1103
- *
1573
+ * Opens the Kubb Studio URL for the given `InputNode` in the default browser, *
1104
1574
  * Falls back to printing the URL if the browser cannot be launched.
1105
1575
  */
1106
1576
  async function openInStudio(input, studioUrl, options = {}) {
@@ -1155,18 +1625,13 @@ function compareFiles(a, b) {
1155
1625
  * ```
1156
1626
  */
1157
1627
  var FileManager = class {
1158
- #cache = /* @__PURE__ */ new Map();
1159
- #sorted = null;
1160
- #onUpsert = null;
1161
1628
  /**
1162
- * Registers a callback invoked with the resolved {@link FileNode} on every
1163
- * `add` / `upsert`. Used by the build loop to track newly written files
1164
- * without keeping its own scan-based diff. Single subscriber by design —
1165
- * setting again replaces the previous callback. Pass `null` to detach.
1629
+ * Subscribe to file-store changes. Listeners on `upsert` see each resolved file as it lands
1630
+ * through `add` or `upsert`.
1166
1631
  */
1167
- setOnUpsert(callback) {
1168
- this.#onUpsert = callback;
1169
- }
1632
+ hooks = new AsyncEventEmitter();
1633
+ #cache = /* @__PURE__ */ new Map();
1634
+ #sorted = null;
1170
1635
  add(...files) {
1171
1636
  return this.#store(files, false);
1172
1637
  }
@@ -1181,7 +1646,7 @@ var FileManager = class {
1181
1646
  const merged = existing && mergeExisting ? createFile(mergeFile(existing, file)) : createFile(file);
1182
1647
  this.#cache.set(merged.path, merged);
1183
1648
  resolved.push(merged);
1184
- this.#onUpsert?.(merged);
1649
+ this.hooks.emit("upsert", merged);
1185
1650
  }
1186
1651
  if (resolved.length > 0) this.#sorted = null;
1187
1652
  return resolved;
@@ -1206,18 +1671,19 @@ var FileManager = class {
1206
1671
  this.#sorted = null;
1207
1672
  }
1208
1673
  /**
1209
- * Releases all stored files. Called by the core after `kubb:build:end`.
1674
+ * Releases all stored files and clears every `hooks` listener. Called by the core after
1675
+ * `kubb:build:end`.
1210
1676
  */
1211
1677
  dispose() {
1212
1678
  this.clear();
1213
- this.#onUpsert = null;
1679
+ this.hooks.removeAll();
1214
1680
  }
1215
1681
  [Symbol.dispose]() {
1216
1682
  this.dispose();
1217
1683
  }
1218
1684
  /**
1219
1685
  * All stored files in stable sort order (shortest path first, barrel files
1220
- * last within a length bucket). Returns a cached view do not mutate.
1686
+ * last within a length bucket). Returns a cached view, do not mutate.
1221
1687
  */
1222
1688
  get files() {
1223
1689
  return this.#sorted ??= [...this.#cache.values()].sort(compareFiles);
@@ -1230,32 +1696,57 @@ function joinSources(file) {
1230
1696
  if (sources.length === 0) return "";
1231
1697
  const parts = [];
1232
1698
  for (const source of sources) {
1233
- const s = extractStringsFromNodes(source.nodes);
1234
- if (s) parts.push(s);
1699
+ const text = extractStringsFromNodes(source.nodes);
1700
+ if (text) parts.push(text);
1235
1701
  }
1236
1702
  return parts.join("\n\n");
1237
1703
  }
1238
1704
  /**
1239
- * Converts a single file to a string using the registered parsers.
1240
- * Falls back to joining source values when no matching parser is found.
1705
+ * Turns `FileNode`s into source strings and writes them to storage.
1706
+ *
1707
+ * Two modes share the same instance. Stateless mode (`parse`, `stream`, `run`) just runs the
1708
+ * conversion. Queue mode (`enqueue`, `flush`, `drain`) buffers files deduped by path and
1709
+ * writes each batch through storage with up to `STREAM_FLUSH_EVERY` requests in flight.
1710
+ *
1711
+ * `flush` does not wait for its batch to finish, so dispatch can overlap with IO. The next
1712
+ * `flush` or `drain` picks the in-flight batch up. `drain` blocks until everything has been
1713
+ * written and is meant for the end of a build.
1241
1714
  *
1242
- * @internal
1715
+ * To surface build-level hook signals (`kubb:files:processing:*` and friends) subscribe to
1716
+ * `hooks` and re-emit on the kubb bus.
1243
1717
  */
1244
1718
  var FileProcessor = class {
1245
- events = new AsyncEventEmitter();
1246
- parse(file, { parsers, extension } = {}) {
1247
- const parseExtName = extension?.[file.extname] || void 0;
1719
+ hooks = new AsyncEventEmitter();
1720
+ #parsers;
1721
+ #storage;
1722
+ #extension;
1723
+ #pending = /* @__PURE__ */ new Map();
1724
+ #runningFlush = null;
1725
+ constructor(options) {
1726
+ this.#parsers = options.parsers ?? null;
1727
+ this.#storage = options.storage;
1728
+ this.#extension = options.extension ?? null;
1729
+ }
1730
+ /**
1731
+ * Files waiting in the queue.
1732
+ */
1733
+ get size() {
1734
+ return this.#pending.size;
1735
+ }
1736
+ parse(file) {
1737
+ const parsers = this.#parsers;
1738
+ const parseExtName = this.#extension?.[file.extname] || void 0;
1248
1739
  if (!parsers || !file.extname) return joinSources(file);
1249
1740
  const parser = parsers.get(file.extname);
1250
1741
  if (!parser) return joinSources(file);
1251
1742
  return parser.parse(file, { extname: parseExtName });
1252
1743
  }
1253
- *stream(files, options = {}) {
1744
+ *stream(files) {
1254
1745
  const total = files.length;
1255
1746
  if (total === 0) return;
1256
1747
  let processed = 0;
1257
1748
  for (const file of files) {
1258
- const source = this.parse(file, options);
1749
+ const source = this.parse(file);
1259
1750
  processed++;
1260
1751
  yield {
1261
1752
  file,
@@ -1266,29 +1757,157 @@ var FileProcessor = class {
1266
1757
  };
1267
1758
  }
1268
1759
  }
1269
- async run(files, options = {}) {
1270
- await this.events.emit("start", files);
1271
- for (const { file, source, processed, total, percentage } of this.stream(files, options)) await this.events.emit("update", {
1760
+ async run(files) {
1761
+ await this.hooks.emit("start", files);
1762
+ for (const { file, source, processed, total, percentage } of this.stream(files)) await this.hooks.emit("update", {
1272
1763
  file,
1273
1764
  source,
1274
1765
  processed,
1275
1766
  percentage,
1276
1767
  total
1277
1768
  });
1278
- await this.events.emit("end", files);
1769
+ await this.hooks.emit("end", files);
1279
1770
  return files;
1280
1771
  }
1281
1772
  /**
1282
- * Clears all registered event listeners.
1773
+ * Adds a file to the next flush. A later `enqueue` for the same path replaces the previous
1774
+ * entry, matching `FileManager.upsert`. Fires the `enqueue` event.
1775
+ */
1776
+ enqueue(file) {
1777
+ this.#pending.set(file.path, file);
1778
+ this.hooks.emit("enqueue", file);
1779
+ }
1780
+ /**
1781
+ * Starts processing the queued files. Waits for any previous flush to finish (so two
1782
+ * batches never run together) and then returns without waiting for the new one. The next
1783
+ * `flush` or `drain` picks up the in-flight task.
1784
+ */
1785
+ async flush() {
1786
+ if (this.#runningFlush) await this.#runningFlush;
1787
+ if (this.#pending.size === 0) return;
1788
+ const batch = [...this.#pending.values()];
1789
+ this.#pending.clear();
1790
+ this.#runningFlush = this.#processAndWrite(batch).finally(() => {
1791
+ this.#runningFlush = null;
1792
+ });
1793
+ }
1794
+ /**
1795
+ * Waits for the in-flight flush and writes any files still queued. Fires the `drain` event
1796
+ * when both are done.
1797
+ */
1798
+ async drain() {
1799
+ if (this.#runningFlush) await this.#runningFlush;
1800
+ if (this.#pending.size > 0) {
1801
+ const batch = [...this.#pending.values()];
1802
+ this.#pending.clear();
1803
+ await this.#processAndWrite(batch);
1804
+ }
1805
+ await this.hooks.emit("drain");
1806
+ }
1807
+ async #processAndWrite(files) {
1808
+ const storage = this.#storage;
1809
+ await this.hooks.emit("start", files);
1810
+ const items = [...this.stream(files)];
1811
+ for (const item of items) await this.hooks.emit("update", item);
1812
+ const queue = [];
1813
+ for (const { file, source } of items) if (source) {
1814
+ queue.push(storage.setItem(file.path, source));
1815
+ if (queue.length >= 50) await Promise.all(queue.splice(0));
1816
+ }
1817
+ await Promise.all(queue);
1818
+ await this.hooks.emit("end", files);
1819
+ }
1820
+ /**
1821
+ * Clears every listener and the pending queue.
1283
1822
  */
1284
1823
  dispose() {
1285
- this.events.removeAll();
1824
+ this.hooks.removeAll();
1825
+ this.#pending.clear();
1286
1826
  }
1287
1827
  [Symbol.dispose]() {
1288
1828
  this.dispose();
1289
1829
  }
1290
1830
  };
1291
1831
  //#endregion
1832
+ //#region src/HookRegistry.ts
1833
+ /**
1834
+ * Listener bookkeeping around an `AsyncEventEmitter`. Listeners attached through `register`
1835
+ * stay on the emitter but are tracked so `dispose()` removes only them, listeners attached
1836
+ * directly via `emitter.on(...)` survive.
1837
+ */
1838
+ var HookRegistry = class {
1839
+ #emitter;
1840
+ #entries = /* @__PURE__ */ new Set();
1841
+ constructor(options) {
1842
+ this.#emitter = options.emitter;
1843
+ }
1844
+ get emitter() {
1845
+ return this.#emitter;
1846
+ }
1847
+ get size() {
1848
+ return this.#entries.size;
1849
+ }
1850
+ register(options) {
1851
+ this.#emitter.on(options.event, options.handler);
1852
+ this.#entries.add(options);
1853
+ }
1854
+ dispose() {
1855
+ for (const entry of this.#entries) this.#emitter.off(entry.event, entry.handler);
1856
+ this.#entries.clear();
1857
+ }
1858
+ };
1859
+ //#endregion
1860
+ //#region src/Transform.ts
1861
+ /**
1862
+ * Holds one `Visitor` per plugin, keyed by plugin name. Each plugin's transformer runs in
1863
+ * isolation on the original adapter node. `applyTo` is a lookup, not a chain, so plugin A's
1864
+ * visitor never sees plugin B's output. When no transformer is registered, `applyTo` returns
1865
+ * the original node reference, and the `@kubb/ast` `transform` primitive does the same when
1866
+ * its visitor leaves the tree untouched. Callers can compare by identity to detect a no-op.
1867
+ *
1868
+ * Registration order matches the order setup hooks fire, which the driver has already sorted
1869
+ * by `enforce` and dependency edges. The registry does not re-order anything.
1870
+ */
1871
+ var Transform = class {
1872
+ #visitors = /* @__PURE__ */ new Map();
1873
+ /**
1874
+ * Number of plugins with a registered transformer.
1875
+ */
1876
+ get size() {
1877
+ return this.#visitors.size;
1878
+ }
1879
+ /**
1880
+ * Records `visitor` as the transformer for `pluginName`. A second call for the same plugin
1881
+ * replaces the first.
1882
+ */
1883
+ register(pluginName, visitor) {
1884
+ this.#visitors.set(pluginName, visitor);
1885
+ }
1886
+ /**
1887
+ * Looks up the transformer for `pluginName`. The generator context uses this so plugins can
1888
+ * read their own visitor through `ctx.transformer`.
1889
+ */
1890
+ get(pluginName) {
1891
+ return this.#visitors.get(pluginName);
1892
+ }
1893
+ /**
1894
+ * Runs the plugin's transformer on `node`. Returns the original node reference when the
1895
+ * plugin has no transformer, so callers can compare by identity to detect a no-op.
1896
+ */
1897
+ applyTo(pluginName, node) {
1898
+ const visitor = this.#visitors.get(pluginName);
1899
+ if (!visitor) return node;
1900
+ return transform(node, visitor);
1901
+ }
1902
+ /**
1903
+ * Clears every registration. Called from the driver's `dispose()` so visitors do not leak
1904
+ * across builds.
1905
+ */
1906
+ dispose() {
1907
+ this.#visitors.clear();
1908
+ }
1909
+ };
1910
+ //#endregion
1292
1911
  //#region \0@oxc-project+runtime@0.133.0/helpers/esm/usingCtx.js
1293
1912
  function _usingCtx() {
1294
1913
  var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
@@ -1353,13 +1972,6 @@ function _usingCtx() {
1353
1972
  function enforceOrder(enforce) {
1354
1973
  return enforce === "pre" ? -1 : enforce === "post" ? 1 : 0;
1355
1974
  }
1356
- const OPERATION_FILTER_TYPES = new Set([
1357
- "tag",
1358
- "operationId",
1359
- "path",
1360
- "method",
1361
- "contentType"
1362
- ]);
1363
1975
  var KubbDriver = class KubbDriver {
1364
1976
  config;
1365
1977
  options;
@@ -1377,7 +1989,7 @@ var KubbDriver = class KubbDriver {
1377
1989
  }
1378
1990
  /**
1379
1991
  * The streaming `InputStreamNode` produced by the adapter.
1380
- * Always set after adapter setup parse-only adapters are wrapped automatically.
1992
+ * Always set after adapter setup, parse-only adapters are wrapped automatically.
1381
1993
  */
1382
1994
  inputNode = null;
1383
1995
  adapter = null;
@@ -1385,7 +1997,7 @@ var KubbDriver = class KubbDriver {
1385
1997
  * Studio session state, kept together so `dispose()` can reset it atomically.
1386
1998
  *
1387
1999
  * - `source` holds the raw adapter source so `adapter.parse()` can be called lazily.
1388
- * Intentionally outlives the build; cleared by `dispose()`.
2000
+ * Intentionally outlives the build, cleared by `dispose()`.
1389
2001
  * - `isOpen` prevents opening the studio more than once per build.
1390
2002
  * - `inputNode` caches the parse promise so `adapter.parse()` is called at most once
1391
2003
  * per studio session, even when `openInStudio()` is called multiple times.
@@ -1395,14 +2007,12 @@ var KubbDriver = class KubbDriver {
1395
2007
  isOpen: false,
1396
2008
  inputNode: null
1397
2009
  };
1398
- #middlewareListeners = [];
1399
2010
  /**
1400
2011
  * Central file store for all generated files.
1401
2012
  * Plugins should use `this.addFile()` / `this.upsertFile()` (via their context) to
1402
- * add files; this property gives direct read/write access when needed.
2013
+ * add files. This property gives direct read/write access when needed.
1403
2014
  */
1404
2015
  fileManager = new FileManager();
1405
- #fileProcessor = new FileProcessor();
1406
2016
  plugins = /* @__PURE__ */ new Map();
1407
2017
  /**
1408
2018
  * Tracks which plugins have generators registered via `addGenerator()` (event-based path).
@@ -1411,11 +2021,22 @@ var KubbDriver = class KubbDriver {
1411
2021
  #eventGeneratorPlugins = /* @__PURE__ */ new Set();
1412
2022
  #resolvers = /* @__PURE__ */ new Map();
1413
2023
  #defaultResolvers = /* @__PURE__ */ new Map();
1414
- #hookListeners = /* @__PURE__ */ new Map();
2024
+ /**
2025
+ * Tracks every listener the driver added (plugin, middleware, generator) so `dispose()` can
2026
+ * remove them in one pass. Middleware registers after plugins, so it fires last via `Set`
2027
+ * insertion order. External `hooks.on(...)` listeners are not tracked.
2028
+ */
2029
+ #registry;
2030
+ /**
2031
+ * Transform registry. Plugins populate it during `kubb:plugin:setup` via `setTransformer`,
2032
+ * and `#runGenerators` reads it once per `(plugin, node)` pair through `applyTo`.
2033
+ */
2034
+ #transforms = new Transform();
1415
2035
  constructor(config, options) {
1416
2036
  this.config = config;
1417
2037
  this.options = options;
1418
2038
  this.adapter = config.adapter ?? null;
2039
+ this.#registry = new HookRegistry({ emitter: options.hooks });
1419
2040
  }
1420
2041
  async setup() {
1421
2042
  const normalized = this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin));
@@ -1430,7 +2051,7 @@ var KubbDriver = class KubbDriver {
1430
2051
  this.plugins.set(plugin.name, plugin);
1431
2052
  }
1432
2053
  if (this.config.middleware) for (const middleware of this.config.middleware) for (const event of Object.keys(middleware.hooks)) this.#registerMiddleware(event, middleware.hooks);
1433
- if (this.config.adapter) await this.#registerAdapter(this.config.adapter);
2054
+ if (this.config.adapter) this.#studio.source = inputToAdapterSource(this.config);
1434
2055
  }
1435
2056
  get hooks() {
1436
2057
  return this.options.hooks;
@@ -1454,46 +2075,39 @@ var KubbDriver = class KubbDriver {
1454
2075
  if ("apply" in plugin && typeof plugin.apply === "function") normalized.apply = plugin.apply;
1455
2076
  return normalized;
1456
2077
  }
1457
- async #registerAdapter(adapter) {
1458
- const source = inputToAdapterSource(this.config);
1459
- this.#studio.source = source;
2078
+ /**
2079
+ * Parses the adapter source into `this.inputNode`. Idempotent, so repeated calls from
2080
+ * `run` or the studio path do not re-parse. Adapters with `stream()` are used directly.
2081
+ * Adapters with only `parse()` are wrapped via `createStreamInput` so the dispatch loop
2082
+ * stays stream-only.
2083
+ */
2084
+ async #parseInput() {
2085
+ if (this.inputNode || !this.adapter || !this.#studio.source) return;
2086
+ const adapter = this.adapter;
2087
+ const source = this.#studio.source;
1460
2088
  if (adapter.stream) {
1461
2089
  this.inputNode = await adapter.stream(source);
1462
- await this.hooks.emit("kubb:debug", {
1463
- date: /* @__PURE__ */ new Date(),
1464
- logs: [`✓ Adapter '${adapter.name}' producing input stream`]
1465
- });
1466
- } else {
1467
- const inputNode = await adapter.parse(source);
1468
- this.inputNode = createStreamInput(arrayToAsyncIterable(inputNode.schemas), arrayToAsyncIterable(inputNode.operations), inputNode.meta);
1469
- await this.hooks.emit("kubb:debug", {
1470
- date: /* @__PURE__ */ new Date(),
1471
- logs: [
1472
- `✓ Adapter '${adapter.name}' resolved InputNode (wrapped as stream)`,
1473
- ` • Schemas: ${inputNode.schemas.length}`,
1474
- ` • Operations: ${inputNode.operations.length}`
1475
- ]
1476
- });
2090
+ return;
1477
2091
  }
2092
+ const parsed = await adapter.parse(source);
2093
+ this.inputNode = createStreamInput(arrayToAsyncIterable(parsed.schemas), arrayToAsyncIterable(parsed.operations), parsed.meta);
1478
2094
  }
1479
2095
  #registerMiddleware(event, middlewareHooks) {
1480
2096
  const handler = middlewareHooks[event];
1481
2097
  if (!handler) return;
1482
- this.hooks.on(event, handler);
1483
- this.#middlewareListeners.push([event, handler]);
2098
+ this.#registry.register({
2099
+ event,
2100
+ handler,
2101
+ source: "middleware"
2102
+ });
1484
2103
  }
1485
2104
  /**
1486
2105
  * Registers a hook-style plugin's lifecycle handlers on the shared `AsyncEventEmitter`.
1487
2106
  *
1488
- * For `kubb:plugin:setup`, the registered listener wraps the globally emitted context with a
1489
- * plugin-specific one so that `addGenerator`, `setResolver`, `setTransformer`, and
1490
- * `setRenderer` all target the correct `normalizedPlugin` entry in the plugins map.
1491
- *
1492
- * All other hooks are iterated and registered directly as pass-through listeners.
1493
- * Any event key present in the global `KubbHooks` interface can be subscribed to.
1494
- *
1495
- * External tooling can subscribe to any of these events via `hooks.on(...)` to observe
1496
- * the plugin lifecycle without modifying plugin behavior.
2107
+ * The `kubb:plugin:setup` listener wraps the global context in a plugin-specific one so
2108
+ * `addGenerator`, `setResolver`, and `setTransformer` target the right `normalizedPlugin`.
2109
+ * Every other `KubbHooks` event registers as a pass-through listener that external tooling
2110
+ * can observe via `hooks.on(...)`.
1497
2111
  *
1498
2112
  * @internal
1499
2113
  */
@@ -1512,10 +2126,7 @@ var KubbDriver = class KubbDriver {
1512
2126
  this.setPluginResolver(plugin.name, resolver);
1513
2127
  },
1514
2128
  setTransformer: (visitor) => {
1515
- plugin.transformer = visitor;
1516
- },
1517
- setRenderer: (renderer) => {
1518
- plugin.renderer = renderer;
2129
+ this.#transforms.register(plugin.name, visitor);
1519
2130
  },
1520
2131
  setOptions: (opts) => {
1521
2132
  plugin.options = {
@@ -1529,13 +2140,21 @@ var KubbDriver = class KubbDriver {
1529
2140
  };
1530
2141
  return hooks["kubb:plugin:setup"](pluginCtx);
1531
2142
  };
1532
- this.hooks.on("kubb:plugin:setup", setupHandler);
1533
- this.#trackHookListener("kubb:plugin:setup", setupHandler);
2143
+ this.#registry.register({
2144
+ event: "kubb:plugin:setup",
2145
+ handler: setupHandler,
2146
+ source: "plugin"
2147
+ });
1534
2148
  }
1535
- for (const [event, handler] of Object.entries(hooks)) {
1536
- if (event === "kubb:plugin:setup" || !handler) continue;
1537
- this.hooks.on(event, handler);
1538
- this.#trackHookListener(event, handler);
2149
+ for (const event of Object.keys(hooks)) {
2150
+ if (event === "kubb:plugin:setup") continue;
2151
+ const handler = hooks[event];
2152
+ if (!handler) continue;
2153
+ this.#registry.register({
2154
+ event,
2155
+ handler,
2156
+ source: "plugin"
2157
+ });
1539
2158
  }
1540
2159
  }
1541
2160
  /**
@@ -1552,7 +2171,6 @@ var KubbDriver = class KubbDriver {
1552
2171
  addGenerator: noop,
1553
2172
  setResolver: noop,
1554
2173
  setTransformer: noop,
1555
- setRenderer: noop,
1556
2174
  setOptions: noop,
1557
2175
  injectFile: noop,
1558
2176
  updateConfig: noop
@@ -1566,52 +2184,56 @@ var KubbDriver = class KubbDriver {
1566
2184
  * respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check
1567
2185
  * so that generators from different plugins do not cross-fire.
1568
2186
  *
1569
- * The renderer resolution chain is: `generator.renderer plugin.renderer config.renderer`.
1570
- * Set `generator.renderer = null` to explicitly opt out of rendering even when the plugin
1571
- * declares a renderer.
2187
+ * The renderer comes from `generator.renderer`. Set `generator.renderer = null` (or leave it
2188
+ * unset) to opt out of rendering.
1572
2189
  *
1573
2190
  * Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
1574
2191
  */
1575
- registerGenerator(pluginName, gen) {
1576
- const resolveRenderer = () => {
1577
- const plugin = this.plugins.get(pluginName);
1578
- return gen.renderer === null ? void 0 : gen.renderer ?? plugin?.renderer ?? this.config.renderer;
1579
- };
1580
- if (gen.schema) {
2192
+ registerGenerator(pluginName, generator) {
2193
+ if (generator.schema) {
1581
2194
  const schemaHandler = async (node, ctx) => {
1582
2195
  if (ctx.plugin.name !== pluginName) return;
1583
- await applyHookResult({
1584
- result: await gen.schema(node, ctx),
1585
- driver: this,
1586
- rendererFactory: resolveRenderer()
2196
+ const result = await generator.schema(node, ctx);
2197
+ await this.dispatch({
2198
+ result,
2199
+ renderer: generator.renderer
1587
2200
  });
1588
2201
  };
1589
- this.hooks.on("kubb:generate:schema", schemaHandler);
1590
- this.#trackHookListener("kubb:generate:schema", schemaHandler);
2202
+ this.#registry.register({
2203
+ event: "kubb:generate:schema",
2204
+ handler: schemaHandler,
2205
+ source: "driver"
2206
+ });
1591
2207
  }
1592
- if (gen.operation) {
2208
+ if (generator.operation) {
1593
2209
  const operationHandler = async (node, ctx) => {
1594
2210
  if (ctx.plugin.name !== pluginName) return;
1595
- await applyHookResult({
1596
- result: await gen.operation(node, ctx),
1597
- driver: this,
1598
- rendererFactory: resolveRenderer()
2211
+ const result = await generator.operation(node, ctx);
2212
+ await this.dispatch({
2213
+ result,
2214
+ renderer: generator.renderer
1599
2215
  });
1600
2216
  };
1601
- this.hooks.on("kubb:generate:operation", operationHandler);
1602
- this.#trackHookListener("kubb:generate:operation", operationHandler);
2217
+ this.#registry.register({
2218
+ event: "kubb:generate:operation",
2219
+ handler: operationHandler,
2220
+ source: "driver"
2221
+ });
1603
2222
  }
1604
- if (gen.operations) {
2223
+ if (generator.operations) {
1605
2224
  const operationsHandler = async (nodes, ctx) => {
1606
2225
  if (ctx.plugin.name !== pluginName) return;
1607
- await applyHookResult({
1608
- result: await gen.operations(nodes, ctx),
1609
- driver: this,
1610
- rendererFactory: resolveRenderer()
2226
+ const result = await generator.operations(nodes, ctx);
2227
+ await this.dispatch({
2228
+ result,
2229
+ renderer: generator.renderer
1611
2230
  });
1612
2231
  };
1613
- this.hooks.on("kubb:generate:operations", operationsHandler);
1614
- this.#trackHookListener("kubb:generate:operations", operationsHandler);
2232
+ this.#registry.register({
2233
+ event: "kubb:generate:operations",
2234
+ handler: operationsHandler,
2235
+ source: "driver"
2236
+ });
1615
2237
  }
1616
2238
  this.#eventGeneratorPlugins.add(pluginName);
1617
2239
  }
@@ -1626,143 +2248,110 @@ var KubbDriver = class KubbDriver {
1626
2248
  return this.#eventGeneratorPlugins.has(pluginName);
1627
2249
  }
1628
2250
  /**
1629
- * Runs the full plugin pipeline. Returns timings/failures collected so far even
1630
- * when an outer hook throws the orchestrator preserves partial state by capturing
1631
- * the error into `error` instead of propagating.
2251
+ * Runs the full plugin pipeline. Returns the diagnostics collected so far even
2252
+ * when an outer hook throws, since the orchestrator preserves partial state by capturing
2253
+ * the failure as a {@link Diagnostic} instead of propagating. Each plugin also
2254
+ * contributes a `timing` diagnostic for the run summary.
1632
2255
  */
1633
2256
  async run({ storage }) {
1634
- const hooks = this.hooks;
1635
- const config = this.config;
1636
- const failedPlugins = /* @__PURE__ */ new Set();
1637
- const pluginTimings = /* @__PURE__ */ new Map();
2257
+ const { hooks, config } = this;
2258
+ const diagnostics = [];
1638
2259
  const parsersMap = /* @__PURE__ */ new Map();
1639
2260
  for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
1640
- const pendingFiles = /* @__PURE__ */ new Map();
1641
- this.fileManager.setOnUpsert((file) => {
1642
- pendingFiles.set(file.path, file);
2261
+ const processor = new FileProcessor({
2262
+ parsers: parsersMap,
2263
+ storage,
2264
+ extension: config.output.extension
1643
2265
  });
1644
- try {
1645
- const flushPending = async () => {
1646
- if (pendingFiles.size === 0) return;
1647
- const files = [...pendingFiles.values()];
1648
- pendingFiles.clear();
1649
- await hooks.emit("kubb:debug", {
1650
- date: /* @__PURE__ */ new Date(),
1651
- logs: [`Writing ${files.length} files...`]
1652
- });
1653
- await hooks.emit("kubb:files:processing:start", { files });
1654
- const items = [...this.#fileProcessor.stream(files, {
1655
- parsers: parsersMap,
1656
- extension: config.output.extension
1657
- })];
1658
- await hooks.emit("kubb:files:processing:update", { files: items.map(({ file, source, processed, total, percentage }) => ({
1659
- file,
1660
- source,
1661
- processed,
1662
- total,
1663
- percentage,
1664
- config
1665
- })) });
1666
- const queue = [];
1667
- for (const { file, source } of items) if (source) {
1668
- queue.push(storage.setItem(file.path, source));
1669
- if (queue.length >= 50) await Promise.all(queue.splice(0));
1670
- }
1671
- await Promise.all(queue);
1672
- await hooks.emit("kubb:files:processing:end", { files });
1673
- await hooks.emit("kubb:debug", {
1674
- date: /* @__PURE__ */ new Date(),
1675
- logs: [`✓ File write process completed for ${files.length} files`]
1676
- });
1677
- };
1678
- await this.emitSetupHooks();
1679
- if (this.adapter && this.inputNode) await hooks.emit("kubb:build:start", Object.assign({
1680
- config,
1681
- adapter: this.adapter,
1682
- meta: this.inputNode.meta,
1683
- getPlugin: this.getPlugin.bind(this)
1684
- }, this.#filesPayload()));
1685
- const generatorPlugins = [];
1686
- for (const plugin of this.plugins.values()) {
1687
- const context = this.getContext(plugin);
1688
- const hrStart = process.hrtime();
1689
- try {
1690
- await hooks.emit("kubb:plugin:start", { plugin });
1691
- await hooks.emit("kubb:debug", {
1692
- date: /* @__PURE__ */ new Date(),
1693
- logs: ["Starting plugin...", ` • Plugin Name: ${plugin.name}`]
1694
- });
1695
- } catch (caughtError) {
1696
- const error = caughtError;
2266
+ processor.hooks.on("start", async (files) => {
2267
+ await hooks.emit("kubb:files:processing:start", { files });
2268
+ });
2269
+ const updateBuffer = [];
2270
+ processor.hooks.on("update", (item) => {
2271
+ updateBuffer.push(item);
2272
+ });
2273
+ processor.hooks.on("end", async (files) => {
2274
+ await hooks.emit("kubb:files:processing:update", { files: updateBuffer.map((item) => ({
2275
+ ...item,
2276
+ config
2277
+ })) });
2278
+ updateBuffer.length = 0;
2279
+ await hooks.emit("kubb:files:processing:end", { files });
2280
+ });
2281
+ const onFileUpsert = (file) => {
2282
+ processor.enqueue(file);
2283
+ };
2284
+ this.fileManager.hooks.on("upsert", onFileUpsert);
2285
+ return Diagnostics.scope((diagnostic) => diagnostics.push(diagnostic), async () => {
2286
+ try {
2287
+ await this.#parseInput();
2288
+ await this.emitSetupHooks();
2289
+ if (this.adapter && this.inputNode) await hooks.emit("kubb:build:start", Object.assign({
2290
+ config,
2291
+ adapter: this.adapter,
2292
+ meta: this.inputNode.meta,
2293
+ getPlugin: this.getPlugin.bind(this)
2294
+ }, this.#filesPayload()));
2295
+ const generatorPlugins = [];
2296
+ for (const plugin of this.plugins.values()) {
2297
+ const context = this.getContext(plugin);
2298
+ const hrStart = process.hrtime();
2299
+ try {
2300
+ await hooks.emit("kubb:plugin:start", { plugin });
2301
+ } catch (caughtError) {
2302
+ const error = caughtError;
2303
+ const duration = getElapsedMs(hrStart);
2304
+ await this.#emitPluginEnd({
2305
+ plugin,
2306
+ duration,
2307
+ success: false,
2308
+ error
2309
+ });
2310
+ diagnostics.push({
2311
+ ...Diagnostics.from(error),
2312
+ plugin: plugin.name
2313
+ }, Diagnostics.performance({
2314
+ plugin: plugin.name,
2315
+ duration
2316
+ }));
2317
+ continue;
2318
+ }
2319
+ if (this.hasEventGenerators(plugin.name)) {
2320
+ generatorPlugins.push({
2321
+ plugin,
2322
+ context,
2323
+ hrStart
2324
+ });
2325
+ continue;
2326
+ }
1697
2327
  const duration = getElapsedMs(hrStart);
1698
- pluginTimings.set(plugin.name, duration);
2328
+ diagnostics.push(Diagnostics.performance({
2329
+ plugin: plugin.name,
2330
+ duration
2331
+ }));
1699
2332
  await this.#emitPluginEnd({
1700
2333
  plugin,
1701
2334
  duration,
1702
- success: false,
1703
- error
2335
+ success: true
1704
2336
  });
1705
- failedPlugins.add({
1706
- plugin,
1707
- error
1708
- });
1709
- continue;
1710
- }
1711
- if (plugin.generators?.length || this.hasEventGenerators(plugin.name)) {
1712
- generatorPlugins.push({
1713
- plugin,
1714
- context,
1715
- hrStart
1716
- });
1717
- continue;
1718
2337
  }
1719
- const duration = getElapsedMs(hrStart);
1720
- pluginTimings.set(plugin.name, duration);
1721
- await this.#emitPluginEnd({
1722
- plugin,
1723
- duration,
1724
- success: true
1725
- });
1726
- await hooks.emit("kubb:debug", {
1727
- date: /* @__PURE__ */ new Date(),
1728
- logs: [`✓ Plugin started successfully (${formatMs(duration)})`]
1729
- });
1730
- }
1731
- if (generatorPlugins.length > 0) if (this.inputNode) {
1732
- const { timings, failed } = await this.#runGenerators(generatorPlugins, flushPending);
1733
- await flushPending();
1734
- for (const [name, duration] of timings) pluginTimings.set(name, duration);
1735
- for (const entry of failed) failedPlugins.add(entry);
1736
- } else for (const { plugin, hrStart } of generatorPlugins) {
1737
- const duration = getElapsedMs(hrStart);
1738
- pluginTimings.set(plugin.name, duration);
1739
- await this.#emitPluginEnd({
1740
- plugin,
1741
- duration,
1742
- success: true
2338
+ diagnostics.push(...await this.#runGenerators(generatorPlugins, () => processor.flush()));
2339
+ await processor.drain();
2340
+ await hooks.emit("kubb:plugins:end", Object.assign({ config }, this.#filesPayload()));
2341
+ await processor.drain();
2342
+ await hooks.emit("kubb:build:end", {
2343
+ files: this.fileManager.files,
2344
+ config,
2345
+ outputDir: resolve(config.root, config.output.path)
1743
2346
  });
2347
+ return { diagnostics: Diagnostics.dedupe(diagnostics) };
2348
+ } catch (caughtError) {
2349
+ diagnostics.push(Diagnostics.from(caughtError));
2350
+ return { diagnostics: Diagnostics.dedupe(diagnostics) };
2351
+ } finally {
2352
+ this.fileManager.hooks.off("upsert", onFileUpsert);
1744
2353
  }
1745
- await hooks.emit("kubb:plugins:end", Object.assign({ config }, this.#filesPayload()));
1746
- await flushPending();
1747
- const files = this.fileManager.files;
1748
- await hooks.emit("kubb:build:end", {
1749
- files,
1750
- config,
1751
- outputDir: resolve(config.root, config.output.path)
1752
- });
1753
- return {
1754
- failedPlugins,
1755
- pluginTimings
1756
- };
1757
- } catch (caughtError) {
1758
- return {
1759
- failedPlugins,
1760
- pluginTimings,
1761
- error: caughtError
1762
- };
1763
- } finally {
1764
- this.fileManager.setOnUpsert(null);
1765
- }
2354
+ });
1766
2355
  }
1767
2356
  #filesPayload() {
1768
2357
  const driver = this;
@@ -1782,10 +2371,36 @@ var KubbDriver = class KubbDriver {
1782
2371
  config: this.config
1783
2372
  }, this.#filesPayload()));
1784
2373
  }
2374
+ /**
2375
+ * Streams schemas and operations through every plugin's generators. Each node is run
2376
+ * through the plugin's transformer (from `this.#transforms`) before the generator sees it,
2377
+ * so plugins stay isolated and the hot path stays per-node. Schemas run before operations
2378
+ * because the two passes share `flushPending` and the FileProcessor's event emitter.
2379
+ * A failing plugin contributes an error diagnostic so the rest of the build continues.
2380
+ * Every plugin also contributes a `timing` diagnostic.
2381
+ *
2382
+ * When `entries` is empty or `this.inputNode` is `null`, every entry still gets a
2383
+ * `kubb:plugin:end` so middleware listeners (the barrel writer and friends) complete.
2384
+ */
1785
2385
  async #runGenerators(entries, flushPending) {
1786
- const timings = /* @__PURE__ */ new Map();
1787
- const failed = /* @__PURE__ */ new Set();
1788
- const driver = this;
2386
+ const diagnostics = [];
2387
+ if (entries.length === 0) return diagnostics;
2388
+ if (!this.inputNode) {
2389
+ for (const { plugin, hrStart } of entries) {
2390
+ const duration = getElapsedMs(hrStart);
2391
+ diagnostics.push(Diagnostics.performance({
2392
+ plugin: plugin.name,
2393
+ duration
2394
+ }));
2395
+ await this.#emitPluginEnd({
2396
+ plugin,
2397
+ duration,
2398
+ success: true
2399
+ });
2400
+ }
2401
+ return diagnostics;
2402
+ }
2403
+ const transforms = this.#transforms;
1789
2404
  const { schemas, operations } = this.inputNode;
1790
2405
  const states = entries.map(({ plugin, context, hrStart }) => {
1791
2406
  const { exclude, include, override } = plugin.options;
@@ -1815,7 +2430,7 @@ var KubbDriver = class KubbDriver {
1815
2430
  if (pruningStates.length > 0) {
1816
2431
  const allSchemas = [];
1817
2432
  for await (const schema of schemas) allSchemas.push(schema);
1818
- const includedOpsByState = new Map(pruningStates.map((s) => [s, []]));
2433
+ const includedOpsByState = new Map(pruningStates.map((state) => [state, []]));
1819
2434
  for await (const operation of operations) for (const state of pruningStates) {
1820
2435
  const { exclude, include, override } = state.plugin.options;
1821
2436
  if (state.generatorContext.resolver.resolveOptions(operation, {
@@ -1830,33 +2445,45 @@ var KubbDriver = class KubbDriver {
1830
2445
  includedOpsByState.delete(state);
1831
2446
  }
1832
2447
  }
1833
- const resolveRendererFor = (gen, state) => gen.renderer === null ? void 0 : gen.renderer ?? state.plugin.renderer ?? state.generatorContext.config.renderer;
2448
+ const resolveForPlugin = (state, node) => {
2449
+ const { plugin, generatorContext } = state;
2450
+ const transformedNode = transforms.applyTo(plugin.name, node);
2451
+ if (state.optionsAreStatic) return {
2452
+ transformedNode,
2453
+ options: plugin.options
2454
+ };
2455
+ const { exclude, include, override } = plugin.options;
2456
+ const options = generatorContext.resolver.resolveOptions(transformedNode, {
2457
+ options: plugin.options,
2458
+ exclude,
2459
+ include,
2460
+ override
2461
+ });
2462
+ if (options === null) return null;
2463
+ return {
2464
+ transformedNode,
2465
+ options
2466
+ };
2467
+ };
1834
2468
  const dispatchNode = async (state, node, dispatch) => {
1835
2469
  if (state.failed) return;
1836
2470
  try {
1837
- const { plugin, generatorContext, generators } = state;
1838
- const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node;
2471
+ const resolved = resolveForPlugin(state, node);
2472
+ if (!resolved) return;
2473
+ const { transformedNode, options } = resolved;
1839
2474
  if (dispatch.checkAllowedNames && state.allowedSchemaNames !== null && "name" in transformedNode && transformedNode.name && !state.allowedSchemaNames.has(transformedNode.name)) return;
1840
- const { exclude, include, override } = plugin.options;
1841
- const options = state.optionsAreStatic ? plugin.options : generatorContext.resolver.resolveOptions(transformedNode, {
1842
- options: plugin.options,
1843
- exclude,
1844
- include,
1845
- override
1846
- });
1847
- if (options === null) return;
1848
2475
  const ctx = {
1849
- ...generatorContext,
2476
+ ...state.generatorContext,
1850
2477
  options
1851
2478
  };
1852
- for (const gen of generators) {
1853
- const generate = gen[dispatch.method];
1854
- if (!generate) continue;
1855
- const raw = generate(transformedNode, ctx);
1856
- const applied = applyHookResult({
1857
- result: isPromise(raw) ? await raw : raw,
1858
- driver,
1859
- rendererFactory: resolveRendererFor(gen, state)
2479
+ for (const gen of state.generators) {
2480
+ const run = gen[dispatch.method];
2481
+ if (!run) continue;
2482
+ const raw = run(transformedNode, ctx);
2483
+ const result = isPromise(raw) ? await raw : raw;
2484
+ const applied = this.dispatch({
2485
+ result,
2486
+ renderer: gen.renderer
1860
2487
  });
1861
2488
  if (isPromise(applied)) await applied;
1862
2489
  }
@@ -1876,15 +2503,15 @@ var KubbDriver = class KubbDriver {
1876
2503
  checkAllowedNames: false,
1877
2504
  emit: emitsOperationHook ? (node, ctx) => this.hooks.emit("kubb:generate:operation", node, ctx) : null
1878
2505
  };
1879
- const needsCollectedOperations = this.hooks.listenerCount("kubb:generate:operations") > 0 || states.some((s) => s.generators.some((g) => !!g.operations));
2506
+ const needsCollectedOperations = this.hooks.listenerCount("kubb:generate:operations") > 0 || states.some((state) => state.generators.some((gen) => !!gen.operations));
1880
2507
  const collectedOperations = needsCollectedOperations ? [] : void 0;
1881
- await forBatches(schemas, (nodes) => Promise.all(nodes.flatMap((n) => states.map((state) => dispatchNode(state, n, schemaDispatch)))), {
2508
+ await forBatches(schemas, (nodes) => Promise.all(nodes.flatMap((node) => states.map((state) => dispatchNode(state, node, schemaDispatch)))), {
1882
2509
  concurrency: 8,
1883
2510
  flush: flushPending
1884
2511
  });
1885
2512
  await forBatches(operations, (nodes) => {
1886
2513
  if (needsCollectedOperations) collectedOperations?.push(...nodes);
1887
- return Promise.all(nodes.flatMap((n) => states.map((state) => dispatchNode(state, n, operationDispatch))));
2514
+ return Promise.all(nodes.flatMap((node) => states.map((state) => dispatchNode(state, node, operationDispatch))));
1888
2515
  }, {
1889
2516
  concurrency: 8,
1890
2517
  flush: flushPending
@@ -1896,23 +2523,17 @@ var KubbDriver = class KubbDriver {
1896
2523
  ...generatorContext,
1897
2524
  options: plugin.options
1898
2525
  };
1899
- const ops = collectedOperations ?? [];
1900
- const pluginOperations = state.optionsAreStatic ? ops : ops.filter((node) => {
1901
- const transformed = plugin.transformer ? transform(node, plugin.transformer) : node;
1902
- const { exclude, include, override } = plugin.options;
1903
- return generatorContext.resolver.resolveOptions(transformed, {
1904
- options: plugin.options,
1905
- exclude,
1906
- include,
1907
- override
1908
- }) !== null;
1909
- });
2526
+ const pluginOperations = (collectedOperations ?? []).reduce((acc, node) => {
2527
+ const resolved = resolveForPlugin(state, node);
2528
+ if (resolved) acc.push(resolved.transformedNode);
2529
+ return acc;
2530
+ }, []);
1910
2531
  for (const gen of generators) {
1911
2532
  if (!gen.operations) continue;
1912
- await applyHookResult({
1913
- result: await gen.operations(pluginOperations, ctx),
1914
- driver,
1915
- rendererFactory: resolveRendererFor(gen, state)
2533
+ const result = await gen.operations(pluginOperations, ctx);
2534
+ await this.dispatch({
2535
+ result,
2536
+ renderer: gen.renderer
1916
2537
  });
1917
2538
  }
1918
2539
  await this.hooks.emit("kubb:generate:operations", pluginOperations, ctx);
@@ -1921,60 +2542,80 @@ var KubbDriver = class KubbDriver {
1921
2542
  state.error = caughtError;
1922
2543
  }
1923
2544
  const duration = getElapsedMs(state.hrStart);
1924
- timings.set(state.plugin.name, duration);
1925
2545
  await this.#emitPluginEnd({
1926
2546
  plugin: state.plugin,
1927
2547
  duration,
1928
2548
  success: !state.failed,
1929
2549
  error: state.failed && state.error ? state.error : void 0
1930
2550
  });
1931
- if (state.failed && state.error) failed.add({
1932
- plugin: state.plugin,
1933
- error: state.error
1934
- });
1935
- await this.hooks.emit("kubb:debug", {
1936
- date: /* @__PURE__ */ new Date(),
1937
- logs: [state.failed ? "✗ Plugin start failed" : `✓ Plugin started successfully (${formatMs(duration)})`]
2551
+ if (state.failed && state.error) diagnostics.push({
2552
+ ...Diagnostics.from(state.error),
2553
+ plugin: state.plugin.name
1938
2554
  });
2555
+ diagnostics.push(Diagnostics.performance({
2556
+ plugin: state.plugin.name,
2557
+ duration
2558
+ }));
1939
2559
  }
1940
- return {
1941
- timings,
1942
- failed
1943
- };
2560
+ return diagnostics;
1944
2561
  }
1945
2562
  /**
1946
- * Unregisters all plugin lifecycle listeners from the shared event emitter.
1947
- * Called at the end of a build to prevent listener leaks across repeated builds.
2563
+ * Stores whatever a generator method or `kubb:generate:*` hook returned.
2564
+ *
2565
+ * - An `Array<FileNode>` goes straight into `fileManager` via `upsert`.
2566
+ * - A renderer element runs through `renderer` (the renderer factory, e.g. JSX) and the
2567
+ * produced files go to `fileManager.upsert`.
2568
+ * - A falsy result is treated as a no-op. The generator wrote files itself via
2569
+ * `ctx.upsertFile`.
2570
+ *
2571
+ * Pass `renderer` when the result may be a renderer element. Generators that only return
2572
+ * `Array<FileNode>` do not need one.
2573
+ */
2574
+ async dispatch({ result, renderer }) {
2575
+ try {
2576
+ var _usingCtx$1 = _usingCtx();
2577
+ if (!result) return;
2578
+ if (Array.isArray(result)) {
2579
+ this.fileManager.upsert(...result);
2580
+ return;
2581
+ }
2582
+ if (!renderer) return;
2583
+ const instance = _usingCtx$1.u(renderer());
2584
+ if (instance.stream) {
2585
+ for (const file of instance.stream(result)) this.fileManager.upsert(file);
2586
+ return;
2587
+ }
2588
+ await instance.render(result);
2589
+ this.fileManager.upsert(...instance.files);
2590
+ } catch (_) {
2591
+ _usingCtx$1.e = _;
2592
+ } finally {
2593
+ _usingCtx$1.d();
2594
+ }
2595
+ }
2596
+ /**
2597
+ * Removes every listener the driver added. Listeners attached directly to `hooks` from outside
2598
+ * the driver survive. Called at the end of a build to prevent leaks across repeated builds.
1948
2599
  *
1949
2600
  * @internal
1950
2601
  */
1951
2602
  dispose() {
1952
- for (const [event, handlers] of this.#hookListeners) for (const handler of handlers) this.hooks.off(event, handler);
1953
- this.#hookListeners.clear();
2603
+ this.#registry.dispose();
1954
2604
  this.#eventGeneratorPlugins.clear();
2605
+ this.#transforms.dispose();
1955
2606
  this.#resolvers.clear();
1956
2607
  this.#defaultResolvers.clear();
1957
2608
  this.fileManager.dispose();
1958
- this.#fileProcessor.dispose();
1959
2609
  this.inputNode = null;
1960
2610
  this.#studio = {
1961
2611
  source: null,
1962
2612
  isOpen: false,
1963
2613
  inputNode: null
1964
2614
  };
1965
- for (const [event, handler] of this.#middlewareListeners) this.hooks.off(event, handler);
1966
2615
  }
1967
2616
  [Symbol.dispose]() {
1968
2617
  this.dispose();
1969
2618
  }
1970
- #trackHookListener(event, handler) {
1971
- let handlers = this.#hookListeners.get(event);
1972
- if (!handlers) {
1973
- handlers = /* @__PURE__ */ new Set();
1974
- this.#hookListeners.set(event, handlers);
1975
- }
1976
- handlers.add(handler);
1977
- }
1978
2619
  #getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => defineResolver(() => ({
1979
2620
  name: "default",
1980
2621
  pluginName
@@ -1998,6 +2639,21 @@ var KubbDriver = class KubbDriver {
1998
2639
  }
1999
2640
  getContext(plugin) {
2000
2641
  const driver = this;
2642
+ const report = (diagnostic) => {
2643
+ Diagnostics.report({
2644
+ ...diagnostic,
2645
+ plugin: plugin.name
2646
+ });
2647
+ if (diagnostic.severity === "error") {
2648
+ driver.hooks.emit("kubb:error", { error: diagnostic.cause ?? new Error(diagnostic.message) });
2649
+ return;
2650
+ }
2651
+ if (diagnostic.severity === "warning") {
2652
+ driver.hooks.emit("kubb:warn", { message: diagnostic.message });
2653
+ return;
2654
+ }
2655
+ driver.hooks.emit("kubb:info", { message: diagnostic.message });
2656
+ };
2001
2657
  return {
2002
2658
  config: driver.config,
2003
2659
  get root() {
@@ -2031,21 +2687,47 @@ var KubbDriver = class KubbDriver {
2031
2687
  return driver.getResolver(plugin.name);
2032
2688
  },
2033
2689
  get transformer() {
2034
- return plugin.transformer;
2690
+ return driver.#transforms.get(plugin.name);
2035
2691
  },
2036
2692
  warn(message) {
2037
- driver.hooks.emit("kubb:warn", { message });
2693
+ report({
2694
+ code: diagnosticCode.pluginWarning,
2695
+ severity: "warning",
2696
+ message
2697
+ });
2038
2698
  },
2039
2699
  error(error) {
2040
- driver.hooks.emit("kubb:error", { error: typeof error === "string" ? new Error(error) : error });
2700
+ const cause = typeof error === "string" ? void 0 : error;
2701
+ report({
2702
+ code: diagnosticCode.pluginFailed,
2703
+ severity: "error",
2704
+ message: typeof error === "string" ? error : error.message,
2705
+ cause
2706
+ });
2041
2707
  },
2042
2708
  info(message) {
2043
- driver.hooks.emit("kubb:info", { message });
2709
+ report({
2710
+ code: diagnosticCode.pluginInfo,
2711
+ severity: "info",
2712
+ message
2713
+ });
2044
2714
  },
2045
2715
  async openInStudio(options) {
2046
2716
  if (!driver.config.devtools || driver.#studio.isOpen) return;
2047
- if (typeof driver.config.devtools !== "object") throw new Error("Devtools must be an object");
2048
- if (!driver.adapter || !driver.#studio.source) throw new Error("adapter is not defined, make sure you have set the parser in kubb.config.ts");
2717
+ if (typeof driver.config.devtools !== "object") throw new DiagnosticError({
2718
+ code: diagnosticCode.devtoolsInvalid,
2719
+ severity: "error",
2720
+ message: "The `devtools` config must be an object.",
2721
+ help: "Set `devtools` to an options object, or remove it to disable Kubb Studio.",
2722
+ location: { kind: "config" }
2723
+ });
2724
+ if (!driver.adapter || !driver.#studio.source) throw new DiagnosticError({
2725
+ code: diagnosticCode.adapterRequired,
2726
+ severity: "error",
2727
+ message: "An adapter is required to open Kubb Studio, but none is configured.",
2728
+ help: "Set `adapter` in kubb.config.ts (for example `adapterOas()`).",
2729
+ location: { kind: "config" }
2730
+ });
2049
2731
  driver.#studio.isOpen = true;
2050
2732
  const studioUrl = driver.config.devtools?.studioUrl ?? "https://kubb.studio";
2051
2733
  driver.#studio.inputNode ??= Promise.resolve(driver.adapter.parse(driver.#studio.source));
@@ -2058,59 +2740,25 @@ var KubbDriver = class KubbDriver {
2058
2740
  }
2059
2741
  requirePlugin(pluginName) {
2060
2742
  const plugin = this.plugins.get(pluginName);
2061
- if (!plugin) throw new Error(`[kubb] Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`);
2743
+ if (!plugin) throw new DiagnosticError({
2744
+ code: diagnosticCode.pluginNotFound,
2745
+ severity: "error",
2746
+ message: `Plugin "${pluginName}" is required but not found. Make sure it is included in your Kubb config.`,
2747
+ help: `Add "${pluginName}" to the \`plugins\` array in kubb.config.ts, or remove the dependency on it.`,
2748
+ location: { kind: "config" }
2749
+ });
2062
2750
  return plugin;
2063
2751
  }
2064
2752
  };
2065
- /**
2066
- * Handles the return value of a plugin AST hook or generator method.
2067
- *
2068
- * - Renderer output → rendered via the provided `rendererFactory` (e.g. JSX), files stored in `driver.fileManager`
2069
- * - `Array<FileNode>` → added directly into `driver.fileManager`
2070
- * - `void` / `null` / `undefined` → no-op (plugin handled it via `this.upsertFile`)
2071
- *
2072
- * Pass a `rendererFactory` (e.g. `jsxRenderer` from `@kubb/renderer-jsx`) when the result
2073
- * may be a renderer element. Generators that only return `Array<FileNode>` do not need one.
2074
- */
2075
- function applyHookResult({ result, driver, rendererFactory }) {
2076
- if (!result) return;
2077
- if (Array.isArray(result)) {
2078
- driver.fileManager.upsert(...result);
2079
- return;
2080
- }
2081
- if (!rendererFactory) return;
2082
- const renderer = rendererFactory();
2083
- if (renderer.stream) try {
2084
- var _usingCtx$1 = _usingCtx();
2085
- const r = _usingCtx$1.u(renderer);
2086
- for (const file of r.stream(result)) driver.fileManager.upsert(file);
2087
- return;
2088
- } catch (_) {
2089
- _usingCtx$1.e = _;
2090
- } finally {
2091
- _usingCtx$1.d();
2092
- }
2093
- return applyAsyncRender({
2094
- renderer,
2095
- result,
2096
- driver
2097
- });
2098
- }
2099
- async function applyAsyncRender({ renderer, result, driver }) {
2100
- try {
2101
- var _usingCtx3 = _usingCtx();
2102
- const r = _usingCtx3.u(renderer);
2103
- await r.render(result);
2104
- driver.fileManager.upsert(...r.files);
2105
- } catch (_) {
2106
- _usingCtx3.e = _;
2107
- } finally {
2108
- _usingCtx3.d();
2109
- }
2110
- }
2111
2753
  function inputToAdapterSource(config) {
2112
2754
  const input = config.input;
2113
- if (!input) throw new Error("[kubb] input is required when using an adapter. Provide input.path or input.data in your config.");
2755
+ if (!input) throw new DiagnosticError({
2756
+ code: diagnosticCode.inputRequired,
2757
+ severity: "error",
2758
+ message: "An adapter is configured without an input.",
2759
+ help: "Provide `input.path` (a file or URL) or `input.data` (an inline spec) in your Kubb config.",
2760
+ location: { kind: "config" }
2761
+ });
2114
2762
  if ("data" in input) return {
2115
2763
  type: "data",
2116
2764
  data: input.data
@@ -2125,6 +2773,6 @@ function inputToAdapterSource(config) {
2125
2773
  };
2126
2774
  }
2127
2775
  //#endregion
2128
- export { FileManager as a, DEFAULT_BANNER as c, logLevel as d, URLPath as f, FileProcessor as i, DEFAULT_EXTENSION as l, BuildError as m, applyHookResult as n, defineResolver as o, AsyncEventEmitter as p, _usingCtx as r, definePlugin as s, KubbDriver as t, DEFAULT_STUDIO_URL as u };
2776
+ export { URLPath as _, defineResolver as a, Diagnostics as c, isProblemDiagnostic as d, isUpdateDiagnostic as f, logLevel as g, diagnosticCode as h, FileManager as i, diagnosticCatalog as l, DEFAULT_STUDIO_URL as m, _usingCtx as n, definePlugin as o, narrowDiagnostic as p, FileProcessor as r, DiagnosticError as s, KubbDriver as t, isPerformanceDiagnostic as u, AsyncEventEmitter as v, BuildError as y };
2129
2777
 
2130
- //# sourceMappingURL=KubbDriver-DrG5-FFe.js.map
2778
+ //# sourceMappingURL=KubbDriver-CckeYpMG.js.map