@kubb/cli 5.0.0-beta.84 → 5.0.0-beta.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/{Telemetry-ugO-B77y.js → Telemetry-Cuzoes9Y.js} +6 -2
  2. package/dist/Telemetry-Cuzoes9Y.js.map +1 -0
  3. package/dist/{Telemetry-jtbchpQE.cjs → Telemetry-CwQYBgsg.cjs} +11 -1
  4. package/dist/Telemetry-CwQYBgsg.cjs.map +1 -0
  5. package/dist/{generate-DDgZnlMs.js → generate-C1bIl1nH.js} +34 -24
  6. package/dist/generate-C1bIl1nH.js.map +1 -0
  7. package/dist/{generate-Diob_zqj.cjs → generate-Cgmty90l.cjs} +34 -23
  8. package/dist/generate-Cgmty90l.cjs.map +1 -0
  9. package/dist/index.cjs +28 -192
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.js +29 -193
  12. package/dist/index.js.map +1 -1
  13. package/dist/{init-uOQx7jSE.cjs → init-DqgB69HY.cjs} +9 -10
  14. package/dist/init-DqgB69HY.cjs.map +1 -0
  15. package/dist/{init-Cn3jZVkf.js → init-EnhDxlBB.js} +10 -10
  16. package/dist/init-EnhDxlBB.js.map +1 -0
  17. package/dist/{mcp-n-IOoFZL.js → mcp-5DDTiwIA.js} +6 -6
  18. package/dist/mcp-5DDTiwIA.js.map +1 -0
  19. package/dist/{mcp-BxhOXHGD.cjs → mcp-DYmjmXQb.cjs} +5 -6
  20. package/dist/mcp-DYmjmXQb.cjs.map +1 -0
  21. package/dist/{package-BBBOfxZz.cjs → package-B57sF1X0.cjs} +2 -2
  22. package/dist/package-B57sF1X0.cjs.map +1 -0
  23. package/dist/package-CQbxcw7I.js +6 -0
  24. package/dist/package-CQbxcw7I.js.map +1 -0
  25. package/dist/{run-DXEMoDFa.cjs → run-3qq_Q4Tg.cjs} +4 -4
  26. package/dist/run-3qq_Q4Tg.cjs.map +1 -0
  27. package/dist/{run-Bz6ZUrhp.cjs → run-B9FN0sHW.cjs} +2 -2
  28. package/dist/run-B9FN0sHW.cjs.map +1 -0
  29. package/dist/{run-CPBFfbI7.js → run-BFhNcn8b.js} +4 -10
  30. package/dist/run-BFhNcn8b.js.map +1 -0
  31. package/dist/{run-D8TRnpBB.js → run-CCsw-Y-v.js} +226 -268
  32. package/dist/run-CCsw-Y-v.js.map +1 -0
  33. package/dist/{run-BBLhhWZu.js → run-D0Gs_UzJ.js} +2 -2
  34. package/dist/run-D0Gs_UzJ.js.map +1 -0
  35. package/dist/{run-ChSqYuc1.cjs → run-DT9pRBLX.cjs} +228 -269
  36. package/dist/run-DT9pRBLX.cjs.map +1 -0
  37. package/dist/{run-DYkciQ4u.cjs → run-DYzUPE7Z.cjs} +4 -10
  38. package/dist/run-DYzUPE7Z.cjs.map +1 -0
  39. package/dist/{run-44Zxh8Ec.js → run-yMRylOST.js} +3 -3
  40. package/dist/run-yMRylOST.js.map +1 -0
  41. package/dist/{tools-D0UogMU3.js → tools-B23ltmWP.js} +3 -3
  42. package/dist/tools-B23ltmWP.js.map +1 -0
  43. package/dist/{tools-CZT9wSA6.cjs → tools-CeIABV0T.cjs} +3 -3
  44. package/dist/tools-CeIABV0T.cjs.map +1 -0
  45. package/dist/{validate-CYIieoc4.cjs → validate-BTVjxR4L.cjs} +8 -9
  46. package/dist/validate-BTVjxR4L.cjs.map +1 -0
  47. package/dist/{validate-BoG8ZZgE.js → validate-CZFmahWd.js} +9 -9
  48. package/dist/validate-CZFmahWd.js.map +1 -0
  49. package/package.json +8 -7
  50. package/dist/Telemetry-jtbchpQE.cjs.map +0 -1
  51. package/dist/Telemetry-ugO-B77y.js.map +0 -1
  52. package/dist/defineCommand-Bo3yZTWI.cjs +0 -36
  53. package/dist/defineCommand-Bo3yZTWI.cjs.map +0 -1
  54. package/dist/defineCommand-DMEeqliP.js +0 -31
  55. package/dist/defineCommand-DMEeqliP.js.map +0 -1
  56. package/dist/generate-DDgZnlMs.js.map +0 -1
  57. package/dist/generate-Diob_zqj.cjs.map +0 -1
  58. package/dist/init-Cn3jZVkf.js.map +0 -1
  59. package/dist/init-uOQx7jSE.cjs.map +0 -1
  60. package/dist/mcp-BxhOXHGD.cjs.map +0 -1
  61. package/dist/mcp-n-IOoFZL.js.map +0 -1
  62. package/dist/package-BBBOfxZz.cjs.map +0 -1
  63. package/dist/package-DKwwXnAz.js +0 -6
  64. package/dist/package-DKwwXnAz.js.map +0 -1
  65. package/dist/run-44Zxh8Ec.js.map +0 -1
  66. package/dist/run-BBLhhWZu.js.map +0 -1
  67. package/dist/run-Bz6ZUrhp.cjs.map +0 -1
  68. package/dist/run-CPBFfbI7.js.map +0 -1
  69. package/dist/run-ChSqYuc1.cjs.map +0 -1
  70. package/dist/run-D8TRnpBB.js.map +0 -1
  71. package/dist/run-DXEMoDFa.cjs.map +0 -1
  72. package/dist/run-DYkciQ4u.cjs.map +0 -1
  73. package/dist/tools-CZT9wSA6.cjs.map +0 -1
  74. package/dist/tools-D0UogMU3.js.map +0 -1
  75. package/dist/validate-BoG8ZZgE.js.map +0 -1
  76. package/dist/validate-CYIieoc4.cjs.map +0 -1
@@ -1,126 +1,17 @@
1
1
  import "./rolldown-runtime-C0LytTxp.js";
2
+ import { a as WATCHER_IGNORED_PATHS, i as UPDATE_CHECK_TIMEOUT_MS, n as KUBB_NPM_PACKAGE_URL, o as canUseTTY, t as Telemetry } from "./Telemetry-Cuzoes9Y.js";
2
3
  import { n as toCause, r as toError } from "./errors-Dc_d7BfX.js";
3
- import { a as canUseTTY, i as WATCHER_IGNORED_PATHS, n as KUBB_NPM_PACKAGE_URL, t as Telemetry } from "./Telemetry-ugO-B77y.js";
4
- import { i as linters, n as detectTool, r as formatters } from "./tools-D0UogMU3.js";
5
- import { t as version } from "./package-DKwwXnAz.js";
4
+ import { t as version } from "./package-CQbxcw7I.js";
5
+ import { i as linters, n as detectTool, r as formatters } from "./tools-B23ltmWP.js";
6
6
  import { styleText } from "node:util";
7
- import { EventEmitter } from "node:events";
8
- import { hash } from "node:crypto";
7
+ import process$1 from "node:process";
9
8
  import { existsSync } from "node:fs";
10
9
  import path, { basename, dirname, relative, resolve } from "node:path";
11
- import process$1 from "node:process";
12
10
  import * as clack from "@clack/prompts";
13
- import { Diagnostics, cliReporter, createKubb, logLevel } from "@kubb/core";
11
+ import { AsyncEventEmitter, Diagnostics, cliReporter, createKubb, logLevel } from "@kubb/core";
14
12
  import { createJiti } from "jiti";
15
13
  import { NonZeroExitError, x } from "tinyexec";
16
14
  import { loadConfig } from "unconfig";
17
- //#region ../../internals/utils/src/asyncEventEmitter.ts
18
- /**
19
- * Typed `EventEmitter` that awaits all async listeners before resolving.
20
- * Wraps Node's `EventEmitter` with full TypeScript event-map inference.
21
- *
22
- * @example
23
- * ```ts
24
- * const emitter = new AsyncEventEmitter<{ build: [name: string] }>()
25
- * emitter.on('build', async (name) => { console.log(name) })
26
- * await emitter.emit('build', 'petstore') // all listeners awaited
27
- * ```
28
- */
29
- var AsyncEventEmitter = class {
30
- /**
31
- * Maximum number of listeners per event before Node emits a memory-leak warning.
32
- * @default 10
33
- */
34
- constructor(maxListener = 10) {
35
- this.#emitter.setMaxListeners(maxListener);
36
- }
37
- #emitter = new EventEmitter();
38
- /**
39
- * Emits `eventName` and awaits all registered listeners sequentially.
40
- * Throws if any listener rejects, wrapping the cause with the event name and serialized arguments.
41
- *
42
- * @example
43
- * ```ts
44
- * await emitter.emit('build', 'petstore')
45
- * ```
46
- */
47
- emit(eventName, ...eventArgs) {
48
- const listeners = this.#emitter.listeners(eventName);
49
- if (listeners.length === 0) return;
50
- return this.#emitAll(eventName, listeners, eventArgs);
51
- }
52
- async #emitAll(eventName, listeners, eventArgs) {
53
- for (const listener of listeners) try {
54
- await listener(...eventArgs);
55
- } catch (err) {
56
- let serializedArgs;
57
- try {
58
- serializedArgs = JSON.stringify(eventArgs);
59
- } catch {
60
- serializedArgs = String(eventArgs);
61
- }
62
- throw new Error(`Error in async listener for "${eventName}" with eventArgs ${serializedArgs}`, { cause: toError(err) });
63
- }
64
- }
65
- /**
66
- * Registers a persistent listener for `eventName`.
67
- *
68
- * @example
69
- * ```ts
70
- * emitter.on('build', async (name) => { console.log(name) })
71
- * ```
72
- */
73
- on(eventName, handler) {
74
- this.#emitter.on(eventName, handler);
75
- }
76
- /**
77
- * Removes a previously registered listener.
78
- *
79
- * @example
80
- * ```ts
81
- * emitter.off('build', handler)
82
- * ```
83
- */
84
- off(eventName, handler) {
85
- this.#emitter.off(eventName, handler);
86
- }
87
- /**
88
- * Returns the number of listeners registered for `eventName`.
89
- *
90
- * @example
91
- * ```ts
92
- * emitter.on('build', handler)
93
- * emitter.listenerCount('build') // 1
94
- * ```
95
- */
96
- listenerCount(eventName) {
97
- return this.#emitter.listenerCount(eventName);
98
- }
99
- /**
100
- * Raises or lowers the per-event listener ceiling before Node warns about a memory leak.
101
- * Set this above the expected listener count when many listeners attach by design.
102
- *
103
- * @example
104
- * ```ts
105
- * emitter.setMaxListeners(40)
106
- * ```
107
- */
108
- setMaxListeners(max) {
109
- this.#emitter.setMaxListeners(max);
110
- }
111
- /**
112
- * Removes all listeners from every event channel.
113
- *
114
- * @example
115
- * ```ts
116
- * emitter.removeAll()
117
- * ```
118
- */
119
- removeAll() {
120
- this.#emitter.removeAllListeners();
121
- }
122
- };
123
- //#endregion
124
15
  //#region ../../internals/utils/src/time.ts
125
16
  /**
126
17
  * Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.
@@ -265,6 +156,43 @@ function formatMsWithColor(ms) {
265
156
  return styleText("red", formatted);
266
157
  }
267
158
  //#endregion
159
+ //#region ../../internals/utils/src/promise.ts
160
+ /**
161
+ * Wraps `run` so invocations never overlap: a trigger that lands while a run is in flight
162
+ * marks it dirty and runs once more after it finishes, no matter how many triggers arrived.
163
+ * Useful for event-driven reruns (a file watcher, a queue drain) where bursts should
164
+ * coalesce into a single trailing run.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const rebuild = createSerialRunner({
169
+ * run: () => build(),
170
+ * onError: (error) => log.error(error.message),
171
+ * })
172
+ * watcher.on('change', () => void rebuild())
173
+ * ```
174
+ */
175
+ function createSerialRunner({ run, onError }) {
176
+ let running = false;
177
+ let dirty = false;
178
+ return async () => {
179
+ if (running) {
180
+ dirty = true;
181
+ return;
182
+ }
183
+ running = true;
184
+ do {
185
+ dirty = false;
186
+ try {
187
+ await run();
188
+ } catch (error) {
189
+ onError(toError(error));
190
+ }
191
+ } while (dirty);
192
+ running = false;
193
+ };
194
+ }
195
+ //#endregion
268
196
  //#region src/loggers/defineLogger.ts
269
197
  /**
270
198
  * Type-safe helper for authoring a {@link Logger}.
@@ -373,20 +301,18 @@ const clackLogger = defineLogger({
373
301
  clack.log.warn(text);
374
302
  });
375
303
  context.on("kubb:error", ({ error }) => {
376
- const caused = toCause(error);
377
304
  const text = [styleText("red", "✗"), error.message].join(" ");
378
305
  if (state.isSpinning) {
379
306
  stopSpinner(getMessage(text));
380
307
  return;
381
308
  }
382
309
  clack.log.error(getMessage(text));
383
- if (logLevel$5 >= logLevel.verbose && error.stack) {
384
- const frames = error.stack.split("\n").slice(1, 4);
385
- for (const frame of frames) clack.log.message(getMessage(styleText("dim", frame.trim())));
386
- if (caused?.stack) {
387
- clack.log.message(styleText("dim", `└─ caused by ${caused.message}`));
388
- const frames = caused.stack.split("\n").slice(1, 4);
389
- for (const frame of frames) clack.log.message(getMessage(` ${styleText("dim", frame.trim())}`));
310
+ const frames = logLevel$5 >= logLevel.verbose ? formatErrorFrames(error) : null;
311
+ if (frames) {
312
+ for (const frame of frames.frames) clack.log.message(getMessage(styleText("dim", frame)));
313
+ if (frames.cause) {
314
+ clack.log.message(styleText("dim", frames.cause.header));
315
+ for (const frame of frames.cause.frames) clack.log.message(getMessage(` ${styleText("dim", frame)}`));
390
316
  }
391
317
  }
392
318
  });
@@ -586,16 +512,14 @@ const plainLogger = defineLogger({
586
512
  console.log(text);
587
513
  });
588
514
  context.on("kubb:error", ({ error }) => {
589
- const caused = toCause(error);
590
515
  const text = getMessage(["✗", error.message].join(" "));
591
516
  console.log(text);
592
- if (logLevel$4 >= logLevel.verbose && error.stack) {
593
- const frames = error.stack.split("\n").slice(1, 4);
594
- for (const frame of frames) console.log(getMessage(frame.trim()));
595
- if (caused?.stack) {
596
- console.log(`└─ caused by ${caused.message}`);
597
- const frames = caused.stack.split("\n").slice(1, 4);
598
- for (const frame of frames) console.log(getMessage(` ${frame.trim()}`));
517
+ const frames = logLevel$4 >= logLevel.verbose ? formatErrorFrames(error) : null;
518
+ if (frames) {
519
+ for (const frame of frames.frames) console.log(getMessage(frame));
520
+ if (frames.cause) {
521
+ console.log(frames.cause.header);
522
+ for (const frame of frames.cause.frames) console.log(getMessage(` ${frame}`));
599
523
  }
600
524
  }
601
525
  });
@@ -682,6 +606,22 @@ function formatMessage(message, logLevel$3) {
682
606
  return message;
683
607
  }
684
608
  /**
609
+ * First stack frames for verbose error output, including an optional `cause` chain.
610
+ */
611
+ function formatErrorFrames(error) {
612
+ if (!error.stack) return null;
613
+ const frames = error.stack.split("\n").slice(1, 4).map((frame) => frame.trim());
614
+ const caused = toCause(error);
615
+ if (!caused?.stack) return { frames };
616
+ return {
617
+ frames,
618
+ cause: {
619
+ header: `└─ caused by ${caused.message}`,
620
+ frames: caused.stack.split("\n").slice(1, 4).map((frame) => frame.trim())
621
+ }
622
+ };
623
+ }
624
+ /**
685
625
  * Build the progress summary line shown by the clack logger.
686
626
  * Returns null when there is nothing to display.
687
627
  */
@@ -742,9 +682,6 @@ function createHookTimer() {
742
682
  if (!hrStart) return;
743
683
  starts.delete(id);
744
684
  return getElapsedMs(hrStart);
745
- },
746
- clear() {
747
- starts.clear();
748
685
  }
749
686
  };
750
687
  }
@@ -757,14 +694,6 @@ function createHookTimer() {
757
694
  function formatCommandWithArgs(command, args) {
758
695
  return args?.length ? `${command} ${args.join(" ")}` : command;
759
696
  }
760
- function detectLogger() {
761
- if (canUseTTY()) return "clack";
762
- return "plain";
763
- }
764
- const logMapper = {
765
- clack: clackLogger,
766
- plain: plainLogger
767
- };
768
697
  /**
769
698
  * Bridges a {@link Reporter} onto the run's event emitter: calls `report` with each config's
770
699
  * {@link GenerationResult} on `kubb:generation:end`. The reporter never touches the emitter.
@@ -798,10 +727,7 @@ async function setupReporters(context, { logLevel, reporters }) {
798
727
  for (const reporter of reporters) {
799
728
  if (reporter.name === "cli") {
800
729
  if (hasJson) continue;
801
- const type = detectLogger();
802
- const logger = logMapper[type];
803
- if (!logger) throw new Error(`Unknown adapter type: ${type}`);
804
- await logger.install(context, { logLevel });
730
+ await (canUseTTY() ? clackLogger : plainLogger).install(context, { logLevel });
805
731
  }
806
732
  installReporter(context, reporter, ctx);
807
733
  }
@@ -912,6 +838,34 @@ async function getConfigs({ configPath, input, watch, logLevel }) {
912
838
  }))
913
839
  };
914
840
  }
841
+ let hookSequence = 0;
842
+ /**
843
+ * Returns a process-unique id that correlates a hook's `kubb:hook:start` and `kubb:hook:end`
844
+ * events, which loggers use to key their per-hook state.
845
+ */
846
+ function createHookId() {
847
+ hookSequence += 1;
848
+ return `hook-${hookSequence}`;
849
+ }
850
+ /**
851
+ * Returns `true` when `latest` is a newer semver version than `current`. Compares each numeric
852
+ * part, so `5.10.0` beats `5.9.0` where a plain string comparison would not. Prerelease
853
+ * suffixes are ignored, and a malformed version never reports an update.
854
+ *
855
+ * @example Double-digit minor
856
+ * `isNewerVersion('5.9.0', '5.10.0') // true`
857
+ *
858
+ * @example String comparison would get this wrong the other way
859
+ * `isNewerVersion('5.10.0', '5.9.0') // false`
860
+ */
861
+ function isNewerVersion(current, latest) {
862
+ const release = (value) => value.split("-")[0] ?? "";
863
+ const isNumeric = (value) => /^\d+(\.\d+)*$/.test(value);
864
+ const currentRelease = release(current);
865
+ const latestRelease = release(latest);
866
+ if (!isNumeric(currentRelease) || !isNumeric(latestRelease)) return false;
867
+ return currentRelease.localeCompare(latestRelease, void 0, { numeric: true }) < 0;
868
+ }
915
869
  /**
916
870
  * Tokenizes a shell command string, respecting single and double quotes.
917
871
  *
@@ -925,44 +879,48 @@ function tokenize(command) {
925
879
  return (command.match(/[^\s"']+|"([^"]*)"|'([^']*)'/g) ?? []).map((token) => token.replace(/^["']|["']$/g, ""));
926
880
  }
927
881
  /**
928
- * Runs the `done` hooks defined in a Kubb config in sequence.
882
+ * Runs the `done` hooks defined in a Kubb config in sequence and returns each hook's outcome,
883
+ * so the caller can turn failures into diagnostics.
929
884
  */
930
885
  async function executeHooks({ configHooks, hooks }) {
931
886
  const commands = Array.isArray(configHooks.done) ? configHooks.done : [configHooks.done].filter(Boolean);
887
+ const results = [];
932
888
  for (const command of commands) {
933
889
  const [cmd, ...args] = tokenize(command);
934
890
  if (!cmd) continue;
935
- const hookId = hash("sha256", command, "hex");
891
+ const hookId = createHookId();
936
892
  const commandWithArgs = [cmd, ...args].join(" ");
937
893
  await hooks.emit("kubb:hook:start", {
938
894
  id: hookId,
939
895
  command: cmd,
940
896
  args
941
897
  });
942
- await runHook({
898
+ results.push(await runHook({
943
899
  id: hookId,
944
900
  command: cmd,
945
901
  args,
946
902
  commandWithArgs,
947
903
  hooks
948
- });
904
+ }));
949
905
  }
906
+ return results;
950
907
  }
951
908
  /**
952
- * Spawns a hook command and reports its outcome through `kubb:hook:end`.
953
- * A non-zero exit signals failure via `kubb:hook:end` rather than throwing, so the caller can turn
909
+ * Spawns a hook command and returns its outcome, mirroring it through `kubb:hook:end` for the
910
+ * loggers. A non-zero exit returns `success: false` rather than throwing, so the caller can turn
954
911
  * it into a diagnostic. Other spawn errors do the same. Output is streamed through `kubb:hook:line`
955
912
  * only while a listener is attached.
956
913
  */
957
914
  async function runHook({ id, command, args, commandWithArgs, hooks }) {
958
- const emitEnd = (success, error, output) => hooks.emit("kubb:hook:end", {
959
- command,
960
- args,
961
- id,
962
- success,
963
- error,
964
- ...output
965
- });
915
+ const emitEnd = async (result) => {
916
+ await hooks.emit("kubb:hook:end", {
917
+ command,
918
+ args,
919
+ id,
920
+ ...result
921
+ });
922
+ return result;
923
+ };
966
924
  const stream = hooks.listenerCount("kubb:hook:line") > 0;
967
925
  try {
968
926
  const proc = x(command, [...args ?? []], {
@@ -975,15 +933,20 @@ async function runHook({ id, command, args, commandWithArgs, hooks }) {
975
933
  });
976
934
  await proc;
977
935
  await hooks.emit("kubb:success", { message: `${styleText("dim", commandWithArgs)} successfully executed` });
978
- await emitEnd(true, null);
936
+ return emitEnd({
937
+ success: true,
938
+ error: null
939
+ });
979
940
  } catch (err) {
980
- if (!(err instanceof NonZeroExitError)) {
981
- await emitEnd(false, toError(err));
982
- return;
983
- }
941
+ if (!(err instanceof NonZeroExitError)) return emitEnd({
942
+ success: false,
943
+ error: toError(err)
944
+ });
984
945
  const stderr = err.output?.stderr ?? "";
985
946
  const stdout = err.output?.stdout ?? "";
986
- await emitEnd(false, /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`), {
947
+ return emitEnd({
948
+ success: false,
949
+ error: /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`),
987
950
  stdout,
988
951
  stderr
989
952
  });
@@ -991,7 +954,9 @@ async function runHook({ id, command, args, commandWithArgs, hooks }) {
991
954
  }
992
955
  /**
993
956
  * Starts a file watcher on the given paths and calls `cb` on any change.
994
- * Ignores `.git` and `node_modules` directories.
957
+ * Ignores `.git` and `node_modules` directories. Event bursts (an editor save emits several)
958
+ * are debounced into one build, and builds never overlap: changes during a build queue exactly
959
+ * one rebuild.
995
960
  */
996
961
  async function startWatcher(path, cb, log = {
997
962
  info: console.log,
@@ -1000,7 +965,8 @@ async function startWatcher(path, cb, log = {
1000
965
  const { watch } = await import("chokidar");
1001
966
  const watcher = watch(path, {
1002
967
  ignorePermissionErrors: true,
1003
- ignored: WATCHER_IGNORED_PATHS
968
+ ignored: WATCHER_IGNORED_PATHS,
969
+ ignoreInitial: true
1004
970
  });
1005
971
  process$1.once("SIGINT", () => {
1006
972
  watcher.close();
@@ -1008,36 +974,28 @@ async function startWatcher(path, cb, log = {
1008
974
  process$1.once("SIGTERM", () => {
1009
975
  watcher.close();
1010
976
  });
1011
- watcher.on("all", async (type, file) => {
977
+ const runBuild = createSerialRunner({
978
+ run: () => cb(path),
979
+ onError: () => log.error(styleText("red", "Watcher failed"))
980
+ });
981
+ let debounceTimer = null;
982
+ watcher.on("all", (type, file) => {
1012
983
  log.info(styleText("yellow", styleText("bold", `Change detected: ${type} ${file}`)));
1013
- try {
1014
- await cb(path);
1015
- } catch (_e) {
1016
- log.error(styleText("red", "Watcher failed"));
1017
- }
984
+ if (debounceTimer) clearTimeout(debounceTimer);
985
+ debounceTimer = setTimeout(() => {
986
+ debounceTimer = null;
987
+ runBuild();
988
+ }, 100);
1018
989
  });
1019
990
  }
1020
991
  //#endregion
1021
992
  //#region src/runners/generate/run.ts
1022
993
  /**
1023
- * Registers a one-shot `kubb:hook:end` listener for `hookId` before the caller emits `kubb:hook:start`,
1024
- * so a synchronous emitter cannot fire end before the listener is attached.
994
+ * Runs one formatter or linter pass over the output directory. Returns the failure instead of
995
+ * throwing, so the caller can turn it into a coded diagnostic. Failures never render here:
996
+ * the caller emits them through `Diagnostics.emit`, like every other diagnostic.
1025
997
  */
1026
- function waitForHookEnd(hooks, hookId, onSuccess, fallbackErrorMessage) {
1027
- return new Promise((resolve, reject) => {
1028
- const handler = (ctx) => {
1029
- if (ctx.id !== hookId) return;
1030
- hooks.off("kubb:hook:end", handler);
1031
- if (!ctx.success) {
1032
- reject(ctx.error ?? new Error(fallbackErrorMessage));
1033
- return;
1034
- }
1035
- Promise.resolve(onSuccess()).then(resolve).catch(reject);
1036
- };
1037
- hooks.on("kubb:hook:end", handler);
1038
- });
1039
- }
1040
- async function runToolPass({ toolValue, detect, toolMap, toolLabel, successPrefix, noToolMessage, configName, outputPath, logLevel: logLevel$1, hooks, onStart, onEnd }) {
998
+ async function runToolPass({ toolValue, detect, toolMap, toolLabel, successPrefix, noToolMessage, outputPath, logLevel: logLevel$1, hooks, onStart, onEnd }) {
1041
999
  await onStart();
1042
1000
  let resolvedTool = toolValue;
1043
1001
  if (resolvedTool === "auto") {
@@ -1048,38 +1006,38 @@ async function runToolPass({ toolValue, detect, toolMap, toolLabel, successPrefi
1048
1006
  await hooks.emit("kubb:info", { message: `Auto-detected ${toolLabel}: ${styleText("dim", resolvedTool)}` });
1049
1007
  }
1050
1008
  }
1051
- let toolError;
1009
+ let toolError = null;
1052
1010
  if (resolvedTool && resolvedTool !== "auto" && resolvedTool in toolMap && existsSync(outputPath)) {
1053
1011
  const toolConfig = toolMap[resolvedTool];
1054
- const hookId = hash("sha256", [configName, resolvedTool].filter(Boolean).join("-"), "hex");
1055
1012
  const successMessage = [
1056
1013
  `${successPrefix} with ${styleText("dim", resolvedTool)}`,
1057
1014
  logLevel$1 >= logLevel.info ? `on ${styleText("dim", outputPath)}` : void 0,
1058
1015
  "successfully"
1059
1016
  ].filter(Boolean).join(" ");
1060
1017
  try {
1018
+ const hookId = createHookId();
1061
1019
  const hookArgs = toolConfig.args(outputPath);
1062
1020
  const commandWithArgs = [toolConfig.command, ...hookArgs].join(" ");
1063
- const hookEndPromise = waitForHookEnd(hooks, hookId, () => hooks.emit("kubb:success", { message: successMessage }), toolConfig.errorMessage);
1064
1021
  await hooks.emit("kubb:hook:start", {
1065
1022
  id: hookId,
1066
1023
  command: toolConfig.command,
1067
1024
  args: hookArgs
1068
1025
  });
1069
- runHook({
1026
+ const result = await runHook({
1070
1027
  id: hookId,
1071
1028
  command: toolConfig.command,
1072
1029
  args: hookArgs,
1073
1030
  commandWithArgs,
1074
1031
  hooks
1075
- }).catch(() => {});
1076
- await hookEndPromise;
1032
+ });
1033
+ if (result.success) await hooks.emit("kubb:success", { message: successMessage });
1034
+ else toolError = result.error ?? new Error(toolConfig.errorMessage);
1077
1035
  } catch (caughtError) {
1078
1036
  toolError = toError(caughtError);
1079
1037
  }
1080
1038
  }
1081
1039
  await onEnd();
1082
- if (toolError) throw toolError;
1040
+ return toolError;
1083
1041
  }
1084
1042
  async function generate(options) {
1085
1043
  const { input, hooks, logLevel } = options;
@@ -1090,8 +1048,7 @@ async function generate(options) {
1090
1048
  input: inputPath ? {
1091
1049
  ...options.config.input,
1092
1050
  path: inputPath
1093
- } : options.config.input,
1094
- ...options.config.output
1051
+ } : options.config.input
1095
1052
  };
1096
1053
  const kubb = createKubb(config, { hooks });
1097
1054
  await hooks.emit("kubb:generation:start", { config });
@@ -1138,67 +1095,60 @@ async function generate(options) {
1138
1095
  }
1139
1096
  const outputPath = path.resolve(config.root, config.output.path);
1140
1097
  const outputDiagnostics = [];
1141
- const toolPasses = [config.output.format && {
1142
- code: Diagnostics.code.formatFailed,
1143
- toolValue: config.output.format,
1144
- detect: () => detectTool([
1145
- "oxfmt",
1146
- "biome",
1147
- "prettier"
1148
- ]),
1149
- toolMap: formatters,
1150
- toolLabel: "formatter",
1151
- successPrefix: "Formatting",
1152
- noToolMessage: "No formatter found (oxfmt, biome, or prettier). Skipping formatting.",
1153
- onStart: () => hooks.emit("kubb:format:start"),
1154
- onEnd: () => hooks.emit("kubb:format:end")
1155
- }, config.output.lint && {
1156
- code: Diagnostics.code.lintFailed,
1157
- toolValue: config.output.lint,
1158
- detect: () => detectTool([
1159
- "oxlint",
1160
- "biome",
1161
- "eslint"
1162
- ]),
1163
- toolMap: linters,
1164
- toolLabel: "linter",
1165
- successPrefix: "Linting",
1166
- noToolMessage: "No linter found (oxlint, biome, or eslint). Skipping linting.",
1167
- onStart: () => hooks.emit("kubb:lint:start"),
1168
- onEnd: () => hooks.emit("kubb:lint:end")
1169
- }].filter(Boolean);
1170
- for (const { code, ...pass } of toolPasses) try {
1171
- await runToolPass({
1172
- ...pass,
1173
- configName: config.name,
1098
+ const reportOutputFailure = async (code, label, error) => {
1099
+ const diagnostic = outputDiagnostic(code, label, error);
1100
+ outputDiagnostics.push(diagnostic);
1101
+ await Diagnostics.emit(hooks, diagnostic);
1102
+ };
1103
+ if (config.output.format) {
1104
+ const error = await runToolPass({
1105
+ toolValue: config.output.format,
1106
+ detect: () => detectTool([
1107
+ "oxfmt",
1108
+ "biome",
1109
+ "prettier"
1110
+ ]),
1111
+ toolMap: formatters,
1112
+ toolLabel: "formatter",
1113
+ successPrefix: "Formatting",
1114
+ noToolMessage: "No formatter found (oxfmt, biome, or prettier). Skipping formatting.",
1115
+ onStart: () => hooks.emit("kubb:format:start"),
1116
+ onEnd: () => hooks.emit("kubb:format:end"),
1174
1117
  outputPath,
1175
1118
  logLevel,
1176
1119
  hooks
1177
1120
  });
1178
- } catch (caughtError) {
1179
- const diagnostic = outputDiagnostic(code, pass.toolLabel, caughtError);
1180
- outputDiagnostics.push(diagnostic);
1181
- await Diagnostics.emit(hooks, diagnostic);
1121
+ if (error) await reportOutputFailure(Diagnostics.code.formatFailed, "formatter", error);
1122
+ }
1123
+ if (config.output.lint) {
1124
+ const error = await runToolPass({
1125
+ toolValue: config.output.lint,
1126
+ detect: () => detectTool([
1127
+ "oxlint",
1128
+ "biome",
1129
+ "eslint"
1130
+ ]),
1131
+ toolMap: linters,
1132
+ toolLabel: "linter",
1133
+ successPrefix: "Linting",
1134
+ noToolMessage: "No linter found (oxlint, biome, or eslint). Skipping linting.",
1135
+ onStart: () => hooks.emit("kubb:lint:start"),
1136
+ onEnd: () => hooks.emit("kubb:lint:end"),
1137
+ outputPath,
1138
+ logLevel,
1139
+ hooks
1140
+ });
1141
+ if (error) await reportOutputFailure(Diagnostics.code.lintFailed, "linter", error);
1182
1142
  }
1183
1143
  if (config.hooks) {
1184
1144
  await hooks.emit("kubb:hooks:start");
1185
- const hookFailures = [];
1186
- const onHookEnd = (ctx) => {
1187
- if (!ctx.success) hookFailures.push(ctx.error ?? /* @__PURE__ */ new Error("Post-generate hook failed"));
1188
- };
1189
- hooks.on("kubb:hook:end", onHookEnd);
1190
- try {
1191
- await executeHooks({
1192
- configHooks: config.hooks,
1193
- hooks
1194
- });
1195
- } finally {
1196
- hooks.off("kubb:hook:end", onHookEnd);
1197
- }
1198
- for (const error of hookFailures) {
1199
- const diagnostic = outputDiagnostic(Diagnostics.code.hookFailed, "Post-generate hook", error);
1200
- outputDiagnostics.push(diagnostic);
1201
- await Diagnostics.emit(hooks, diagnostic);
1145
+ const hookResults = await executeHooks({
1146
+ configHooks: config.hooks,
1147
+ hooks
1148
+ });
1149
+ for (const result of hookResults) {
1150
+ if (result.success) continue;
1151
+ await reportOutputFailure(Diagnostics.code.hookFailed, "Post-generate hook", result.error ?? /* @__PURE__ */ new Error("Post-generate hook failed"));
1202
1152
  }
1203
1153
  await hooks.emit("kubb:hooks:end");
1204
1154
  }
@@ -1235,8 +1185,8 @@ function outputDiagnostic(code, label, caughtError) {
1235
1185
  }
1236
1186
  async function checkForUpdate(hooks) {
1237
1187
  try {
1238
- const data = await (await fetch(KUBB_NPM_PACKAGE_URL)).json();
1239
- if (data.version && version < data.version) await Diagnostics.emit(hooks, Diagnostics.update({
1188
+ const data = await (await fetch(KUBB_NPM_PACKAGE_URL, { signal: AbortSignal.timeout(UPDATE_CHECK_TIMEOUT_MS) })).json();
1189
+ if (data.version && isNewerVersion(version, data.version)) await Diagnostics.emit(hooks, Diagnostics.update({
1240
1190
  currentVersion: version,
1241
1191
  latestVersion: data.version
1242
1192
  }));
@@ -1287,19 +1237,27 @@ async function run({ input, configPath, logLevel: logLevelKey, watch, reporters:
1287
1237
  info: relativeConfigPath
1288
1238
  });
1289
1239
  let anyFailed = false;
1290
- for (const config of configs) if (config.input && "path" in config.input && watch) await startWatcher([input || config.input.path], async (paths) => {
1291
- await generate({
1292
- input,
1293
- config,
1294
- logLevel: logLevel$2,
1295
- hooks
1240
+ for (const config of configs) if (config.input && "path" in config.input && watch) {
1241
+ const watchedPaths = [input || config.input.path];
1242
+ const build = async (paths) => {
1243
+ await generate({
1244
+ input,
1245
+ config,
1246
+ logLevel: logLevel$2,
1247
+ hooks
1248
+ });
1249
+ clack.log.step(styleText("yellow", `Watching for changes in ${paths.join(" and ")}`));
1250
+ };
1251
+ try {
1252
+ await build(watchedPaths);
1253
+ } catch (buildError) {
1254
+ await hooks.emit("kubb:error", { error: toError(buildError) });
1255
+ }
1256
+ await startWatcher(watchedPaths, build, {
1257
+ info: (msg) => clack.log.info(msg),
1258
+ error: (msg) => clack.log.error(msg)
1296
1259
  });
1297
- clack.log.step(styleText("yellow", `Watching for changes in ${paths.join(" and ")}`));
1298
- }, {
1299
- info: (msg) => clack.log.info(msg),
1300
- error: (msg) => clack.log.error(msg)
1301
- });
1302
- else try {
1260
+ } else try {
1303
1261
  if (!await generate({
1304
1262
  input,
1305
1263
  config,
@@ -1320,4 +1278,4 @@ async function run({ input, configPath, logLevel: logLevelKey, watch, reporters:
1320
1278
  //#endregion
1321
1279
  export { run };
1322
1280
 
1323
- //# sourceMappingURL=run-D8TRnpBB.js.map
1281
+ //# sourceMappingURL=run-CCsw-Y-v.js.map