@kubb/core 5.0.0-beta.99 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_usingCtx = require("./usingCtx-CZyLSqds.cjs");
2
+ const require_usingCtx = require("./usingCtx-BdYw7ICK.cjs");
3
3
  let node_async_hooks = require("node:async_hooks");
4
4
  let node_util = require("node:util");
5
5
  let node_crypto = require("node:crypto");
@@ -7,6 +7,8 @@ let node_fs_promises = require("node:fs/promises");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_usingCtx.__toESM(node_path, 1);
9
9
  let _kubb_ast = require("@kubb/ast");
10
+ let node_fs = require("node:fs");
11
+ let node_os = require("node:os");
10
12
  let node_process = require("node:process");
11
13
  node_process = require_usingCtx.__toESM(node_process, 1);
12
14
  //#region src/createAdapter.ts
@@ -147,163 +149,15 @@ const randomColors = [
147
149
  function randomCliColor(text) {
148
150
  if (!text) return "";
149
151
  const index = (0, node_crypto.hash)("sha256", text, "buffer").readUInt32BE(0) % randomColors.length;
150
- return (0, node_util.styleText)(randomColors[index] ?? "white", text);
152
+ const color = randomColors[index] ?? "white";
153
+ return (0, node_util.styleText)(color, text);
151
154
  }
152
155
  //#endregion
153
- //#region ../../internals/utils/src/promise.ts
154
- /**
155
- * Wraps `factory` with a keyed cache backed by the provided store.
156
- *
157
- * Pass a `WeakMap` for object keys (results are GC-eligible when the key is
158
- * collected) or a `Map` for primitive keys. For multi-argument functions,
159
- * nest two `memoize` calls — the outer keyed by the first argument, the
160
- * inner (created once per outer miss) keyed by the second.
161
- *
162
- * Because the cache is owned by the caller, it can be shared, inspected, or
163
- * cleared independently of the memoized function.
164
- *
165
- * @example Single WeakMap key
166
- * ```ts
167
- * const cache = new WeakMap<SchemaNode, Set<string>>()
168
- * const getRefs = memoize(cache, (node) => collectRefs(node))
169
- * ```
170
- *
171
- * @example Single Map key (primitive)
172
- * ```ts
173
- * const cache = new Map<string, Resolver>()
174
- * const getResolver = memoize(cache, (name) => buildResolver(name))
175
- * ```
176
- *
177
- * @example Two-level (object + primitive)
178
- * ```ts
179
- * const outer = new WeakMap<Params[], Map<string, Params[]>>()
180
- * const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
181
- * fn(params)('camelcase')
182
- * ```
183
- */
184
- function memoize(store, factory) {
185
- return (key) => {
186
- if (store.has(key)) return store.get(key);
187
- const value = factory(key);
188
- store.set(key, value);
189
- return value;
190
- };
191
- }
192
- //#endregion
193
- //#region package.json
194
- var version = "5.0.0-beta.99";
195
- //#endregion
196
- //#region src/constants.ts
197
- /**
198
- * Plugin `include` filter types that select operations directly. When one of these is set
199
- * without a `schemaName` include, the generate phase pre-scans operations to compute the set
200
- * of schemas they reach, so unreachable schemas can be pruned for that plugin.
201
- */
202
- const OPERATION_FILTER_TYPES = /* @__PURE__ */ new Set([
203
- "tag",
204
- "operationId",
205
- "path",
206
- "method",
207
- "contentType"
208
- ]);
209
- /**
210
- * Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode
211
- * and stays stable so it can be referenced in tooling and (later) docs. Reference
212
- * these instead of inlining the string at a throw site.
213
- */
214
- const diagnosticCode = {
215
- /**
216
- * Fallback for an unstructured error with no specific code.
217
- */
218
- unknown: "KUBB_UNKNOWN",
219
- /**
220
- * The file or URL set as `input` could not be read.
221
- */
222
- inputNotFound: "KUBB_INPUT_NOT_FOUND",
223
- /**
224
- * An adapter was configured without an `input`.
225
- */
226
- inputRequired: "KUBB_INPUT_REQUIRED",
227
- /**
228
- * A `$ref` (or equivalent reference) could not be resolved in the source document.
229
- */
230
- refNotFound: "KUBB_REF_NOT_FOUND",
231
- /**
232
- * A server variable value is not allowed by its `enum`.
233
- */
234
- invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE",
235
- /**
236
- * A required plugin is missing from the config.
237
- */
238
- pluginNotFound: "KUBB_PLUGIN_NOT_FOUND",
239
- /**
240
- * A plugin threw while generating.
241
- */
242
- pluginFailed: "KUBB_PLUGIN_FAILED",
243
- /**
244
- * A plugin reported a non-fatal warning through `ctx.warn`.
245
- */
246
- pluginWarning: "KUBB_PLUGIN_WARNING",
247
- /**
248
- * A plugin reported an informational message through `ctx.info`.
249
- */
250
- pluginInfo: "KUBB_PLUGIN_INFO",
251
- /**
252
- * A schema uses a `format` Kubb does not map to a specific type. Reserved for
253
- * adapters to emit as a `warning`.
254
- */
255
- unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT",
256
- /**
257
- * A referenced schema or operation is marked `deprecated`. Reserved for adapters
258
- * to emit as an `info`.
259
- */
260
- deprecated: "KUBB_DEPRECATED",
261
- /**
262
- * An adapter is required but the config has none. The build cannot read the input
263
- * without one.
264
- */
265
- adapterRequired: "KUBB_ADAPTER_REQUIRED",
266
- /**
267
- * A resolved output path escapes the output directory, which can stem from a path
268
- * traversal in the spec or a misconfigured `group.name`.
269
- */
270
- pathTraversal: "KUBB_PATH_TRAVERSAL",
271
- /**
272
- * `output.clean` is enabled but `output.path` resolves to the project root or a parent of it,
273
- * so cleaning would delete kubb.config and every source file.
274
- */
275
- cleanRoot: "KUBB_CLEAN_ROOT",
276
- /**
277
- * A plugin's options are invalid, for example `output.mode: 'file'` paired with a `group` option.
278
- */
279
- invalidPluginOptions: "KUBB_INVALID_PLUGIN_OPTIONS",
280
- /**
281
- * A post-generate command (`output.postGenerate`) exited with a failure.
282
- */
283
- postGenerateFailed: "KUBB_POST_GENERATE_FAILED",
284
- /**
285
- * The formatter pass over the generated files failed.
286
- */
287
- formatFailed: "KUBB_FORMAT_FAILED",
288
- /**
289
- * The linter pass over the generated files failed.
290
- */
291
- lintFailed: "KUBB_LINT_FAILED",
292
- /**
293
- * Not a failure. Carries a plugin's elapsed time, summed into the run total.
294
- */
295
- performance: "KUBB_PERFORMANCE",
296
- /**
297
- * Not a failure. A newer Kubb version is available on npm.
298
- */
299
- updateAvailable: "KUBB_UPDATE_AVAILABLE"
300
- };
301
- //#endregion
302
156
  //#region src/Diagnostics.ts
303
157
  /**
304
158
  * Docs major version, derived from the package version so the link tracks the published major.
305
159
  */
306
- const docsMajor = version.split(".")[0] ?? "5";
160
+ const docsMajor = "5.0.0".split(".")[0] ?? "5";
307
161
  /**
308
162
  * Builds a type guard that narrows a {@link Diagnostic} to the variant for `kind`. A diagnostic
309
163
  * with no `kind` is treated as a `problem`.
@@ -356,102 +210,122 @@ const severityStyle = {
356
210
  * and `Diagnostics.docsUrl` for the matching kubb.dev page.
357
211
  */
358
212
  const diagnosticCatalog = {
359
- [diagnosticCode.unknown]: {
213
+ [require_usingCtx.diagnosticCode.unknown]: {
360
214
  title: "Unknown error",
361
215
  cause: "An error was thrown without a stable Kubb code, so it is reported as-is.",
362
216
  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."
363
217
  },
364
- [diagnosticCode.inputNotFound]: {
218
+ [require_usingCtx.diagnosticCode.inputNotFound]: {
365
219
  title: "Input not found",
366
- cause: "The file or URL set as `input` (or passed as `kubb generate PATH`) could not be read.",
367
- fix: "Check that the path or URL exists and is readable, then set it as `input` or pass it on the CLI."
220
+ cause: "The file set as `input` (or passed as `kubb generate PATH`) could not be read. A URL reports `KUBB_INPUT_REQUEST_FAILED` or `KUBB_INPUT_UNREACHABLE` instead.",
221
+ fix: "Check that the path exists and is readable, then set it as `input` or pass it on the CLI."
368
222
  },
369
- [diagnosticCode.inputRequired]: {
223
+ [require_usingCtx.diagnosticCode.inputRequestFailed]: {
224
+ title: "Input request failed",
225
+ cause: "A URL set as `input` (or reached through a `$ref`) answered with a 4xx or 5xx status instead of the document.",
226
+ fix: "Open the URL to see what the server returns. A 401 or 403 needs credentials Kubb does not send, so download the document and point `input` at the local file. A 404 means the path is wrong, and a 5xx means the server itself failed."
227
+ },
228
+ [require_usingCtx.diagnosticCode.inputUnreachable]: {
229
+ title: "Input unreachable",
230
+ cause: "A URL set as `input` (or reached through a `$ref`) never answered, so the request failed before a status came back. A refused connection, an unknown host, an expired certificate, and a timeout all land here.",
231
+ fix: "Check that the host is running and reachable from this machine. For a local server, start it and confirm the port matches the one in `input`."
232
+ },
233
+ [require_usingCtx.diagnosticCode.inputRequired]: {
370
234
  title: "Input required",
371
235
  cause: "An adapter is configured but no `input` was provided.",
372
236
  fix: "Set `input` to a file path, a URL, an inline spec (JSON/YAML string), or a parsed object in your Kubb config."
373
237
  },
374
- [diagnosticCode.refNotFound]: {
238
+ [require_usingCtx.diagnosticCode.legacyInput]: {
239
+ title: "Legacy input shape",
240
+ cause: "`input` is a `{ path }` or `{ data }` wrapper, which v4 used to point at a document and v5 reads as the document itself.",
241
+ fix: "Unwrap it: `input: { path: \"./petStore.yaml\" }` becomes `input: \"./petStore.yaml\"`, and `input: { data: spec }` becomes `input: spec`."
242
+ },
243
+ [require_usingCtx.diagnosticCode.invalidDocument]: {
244
+ title: "Invalid document",
245
+ cause: "The parsed `input` has no `openapi` or `swagger` version field, so it is not an OpenAPI or Swagger document.",
246
+ fix: "Point `input` at a document that declares `openapi` or `swagger`, and check that a passed object is the spec itself rather than a wrapper around it."
247
+ },
248
+ [require_usingCtx.diagnosticCode.refNotFound]: {
375
249
  title: "Reference not found",
376
250
  cause: "A `$ref` could not be resolved in the source document.",
377
251
  fix: "Add the missing definition (for example under `components.schemas`) or fix the `$ref`. Run `kubb validate` to check the spec."
378
252
  },
379
- [diagnosticCode.invalidServerVariable]: {
253
+ [require_usingCtx.diagnosticCode.invalidServerVariable]: {
380
254
  title: "Invalid server variable",
381
255
  cause: "A server variable value is not allowed by its `enum`.",
382
256
  fix: "Use one of the values listed in the server variable `enum`, or update the spec."
383
257
  },
384
- [diagnosticCode.pluginNotFound]: {
258
+ [require_usingCtx.diagnosticCode.pluginNotFound]: {
385
259
  title: "Plugin not found",
386
260
  cause: "A plugin that another plugin depends on is missing from the config.",
387
261
  fix: "Add the required plugin to the `plugins` array in kubb.config.ts, or remove the dependency on it."
388
262
  },
389
- [diagnosticCode.pluginFailed]: {
263
+ [require_usingCtx.diagnosticCode.pluginFailed]: {
390
264
  title: "Plugin failed",
391
265
  cause: "A plugin threw while generating, or reported an error through `ctx.error`.",
392
266
  fix: "Read the underlying error and check the plugin options and the schema or operation it failed on."
393
267
  },
394
- [diagnosticCode.pluginWarning]: {
268
+ [require_usingCtx.diagnosticCode.pluginWarning]: {
395
269
  title: "Plugin warning",
396
270
  cause: "A plugin reported a non-fatal warning through `ctx.warn`.",
397
271
  fix: "Review the message. It does not fail the build; adjust the plugin options or input if the warning is unwanted."
398
272
  },
399
- [diagnosticCode.pluginInfo]: {
273
+ [require_usingCtx.diagnosticCode.pluginInfo]: {
400
274
  title: "Plugin info",
401
275
  cause: "A plugin reported an informational message through `ctx.info`.",
402
276
  fix: "Informational only. No action is required."
403
277
  },
404
- [diagnosticCode.unsupportedFormat]: {
278
+ [require_usingCtx.diagnosticCode.unsupportedFormat]: {
405
279
  title: "Unsupported format",
406
280
  cause: "A schema uses a `format` Kubb does not map to a specific type, so it falls back to the base type.",
407
281
  fix: "Use a format Kubb supports, or handle the custom format with a parser or plugin."
408
282
  },
409
- [diagnosticCode.deprecated]: {
283
+ [require_usingCtx.diagnosticCode.deprecated]: {
410
284
  title: "Deprecated",
411
285
  cause: "A referenced schema or operation is marked `deprecated`.",
412
286
  fix: "Migrate off the deprecated definition if the warning is unwanted."
413
287
  },
414
- [diagnosticCode.adapterRequired]: {
288
+ [require_usingCtx.diagnosticCode.adapterRequired]: {
415
289
  title: "Adapter required",
416
290
  cause: "An action needs an adapter but none is configured.",
417
291
  fix: "Set `adapter` in kubb.config.ts, for example `adapterOas()`."
418
292
  },
419
- [diagnosticCode.pathTraversal]: {
293
+ [require_usingCtx.diagnosticCode.pathTraversal]: {
420
294
  title: "Path traversal",
421
295
  cause: "A resolved output path escaped the output directory, which can stem from a path traversal in the spec or a misconfigured `group.name`.",
422
296
  fix: "Keep generated paths within the output directory. Review the `group.name` function and the names coming from the spec."
423
297
  },
424
- [diagnosticCode.cleanRoot]: {
298
+ [require_usingCtx.diagnosticCode.cleanRoot]: {
425
299
  title: "Clean targets the project root",
426
300
  cause: "`output.clean` is enabled and `output.path` resolves to the project root or a parent of it, so cleaning would delete `kubb.config` and every source file.",
427
301
  fix: "Point `output.path` at a subdirectory such as `./src/gen` so clean only removes generated code, or disable `output.clean`."
428
302
  },
429
- [diagnosticCode.invalidPluginOptions]: {
303
+ [require_usingCtx.diagnosticCode.invalidPluginOptions]: {
430
304
  title: "Invalid plugin options",
431
305
  cause: "A plugin was configured with options that cannot be honored, for example `output.mode: 'file'` paired with a `group` option.",
432
306
  fix: "Fix the plugin options. A single-file output has nothing to group, so remove the `group` option or use `output.mode: 'directory'`."
433
307
  },
434
- [diagnosticCode.postGenerateFailed]: {
308
+ [require_usingCtx.diagnosticCode.postGenerateFailed]: {
435
309
  title: "Post-generate command failed",
436
310
  cause: "A post-generate command (`output.postGenerate`) exited with a non-zero status.",
437
311
  fix: "Check the command is installed and correct, and run it manually to see the error."
438
312
  },
439
- [diagnosticCode.formatFailed]: {
313
+ [require_usingCtx.diagnosticCode.formatFailed]: {
440
314
  title: "Format failed",
441
315
  cause: "The formatter pass over the generated files failed.",
442
316
  fix: "Check the formatter (oxfmt, biome, or prettier) is installed and its config is valid, then run it manually on the output."
443
317
  },
444
- [diagnosticCode.lintFailed]: {
318
+ [require_usingCtx.diagnosticCode.lintFailed]: {
445
319
  title: "Lint failed",
446
320
  cause: "The linter pass over the generated files failed.",
447
321
  fix: "Check the linter (oxlint, biome, or eslint) is installed and its config is valid, then run it manually on the output."
448
322
  },
449
- [diagnosticCode.performance]: {
323
+ [require_usingCtx.diagnosticCode.performance]: {
450
324
  title: "Performance",
451
325
  cause: "Not a failure. Records a plugin’s elapsed time, summed into the run total.",
452
326
  fix: "No action. This is an informational metric."
453
327
  },
454
- [diagnosticCode.updateAvailable]: {
328
+ [require_usingCtx.diagnosticCode.updateAvailable]: {
455
329
  title: "Update available",
456
330
  cause: "A newer Kubb version is published on npm than the one running.",
457
331
  fix: "Update the `@kubb/*` packages, for example `npm install -g @kubb/cli`, to get the latest fixes."
@@ -471,7 +345,7 @@ var Diagnostics = class Diagnostics {
471
345
  /**
472
346
  * The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
473
347
  */
474
- static code = diagnosticCode;
348
+ static code = require_usingCtx.diagnosticCode;
475
349
  /**
476
350
  * Type guard for a build {@link ProblemDiagnostic}.
477
351
  */
@@ -552,7 +426,7 @@ var Diagnostics = class Diagnostics {
552
426
  current = current.cause;
553
427
  }
554
428
  return {
555
- code: diagnosticCode.unknown,
429
+ code: require_usingCtx.diagnosticCode.unknown,
556
430
  severity: "error",
557
431
  message: root ? root.message : require_usingCtx.getErrorMessage(error),
558
432
  cause: root
@@ -564,7 +438,7 @@ var Diagnostics = class Diagnostics {
564
438
  static performance({ plugin, duration }) {
565
439
  return {
566
440
  kind: "performance",
567
- code: diagnosticCode.performance,
441
+ code: require_usingCtx.diagnosticCode.performance,
568
442
  severity: "info",
569
443
  message: `${plugin} generated in ${Math.round(duration)}ms`,
570
444
  plugin,
@@ -577,7 +451,7 @@ var Diagnostics = class Diagnostics {
577
451
  static update({ currentVersion, latestVersion }) {
578
452
  return {
579
453
  kind: "update",
580
- code: diagnosticCode.updateAvailable,
454
+ code: require_usingCtx.diagnosticCode.updateAvailable,
581
455
  severity: "info",
582
456
  message: `Update available: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`,
583
457
  currentVersion,
@@ -670,7 +544,7 @@ var Diagnostics = class Diagnostics {
670
544
  ...problem?.location ? { location: problem.location } : {},
671
545
  ...problem?.help ? { help: problem.help } : {},
672
546
  ...problem?.plugin ? { plugin: problem.plugin } : {},
673
- ...diagnostic.code === diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
547
+ ...diagnostic.code === require_usingCtx.diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
674
548
  };
675
549
  }
676
550
  /**
@@ -690,7 +564,7 @@ var Diagnostics = class Diagnostics {
690
564
  const details = [];
691
565
  if (problem?.location && "pointer" in problem.location) details.push(` ${(0, node_util.styleText)("dim", "at:")} ${(0, node_util.styleText)("cyan", problem.location.pointer)}`);
692
566
  if (problem?.help) details.push(` ${(0, node_util.styleText)("cyan", "fix:")} ${problem.help}`);
693
- if (code !== diagnosticCode.unknown) details.push(` ${(0, node_util.styleText)("dim", "see:")} ${(0, node_util.styleText)("cyan", Diagnostics.docsUrl(code))}`);
567
+ if (code !== require_usingCtx.diagnosticCode.unknown) details.push(` ${(0, node_util.styleText)("dim", "see:")} ${(0, node_util.styleText)("cyan", Diagnostics.docsUrl(code))}`);
694
568
  return {
695
569
  headline,
696
570
  details
@@ -711,14 +585,18 @@ var Diagnostics = class Diagnostics {
711
585
  * Merges the `output.mode` default into the output config and validates the combination.
712
586
  * Throws `KUBB_INVALID_PLUGIN_OPTIONS` when `mode: 'file'` is paired with a `group` option,
713
587
  * since a single-file output has nothing to group.
588
+ *
589
+ * An omitted `mode` follows the shape of `path`: an extension means a single file, anything
590
+ * else is a directory. Plugin defaults such as `path: 'types'` name a directory, so they
591
+ * generate without the caller spelling out `mode`.
714
592
  */
715
593
  function normalizeOutput({ output, group, pluginName }) {
716
- const mode = output.mode ?? "file";
594
+ const mode = output.mode ?? (node_path.default.extname(output.path) ? "file" : "directory");
717
595
  if (mode === "file" && group) throw new Diagnostics.Error({
718
- code: diagnosticCode.invalidPluginOptions,
596
+ code: require_usingCtx.diagnosticCode.invalidPluginOptions,
719
597
  severity: "error",
720
- message: `Plugin "${pluginName}" sets \`output.mode: 'file'\` but also configures a \`group\` option.`,
721
- help: "A single-file output has nothing to group. Remove the `group` option, or use `output.mode: 'directory'` to organize files into subdirectories.",
598
+ message: `Plugin "${pluginName}" resolves \`output.mode\` to 'file' but also configures a \`group\` option.`,
599
+ help: "A single-file output has nothing to group. Remove the `group` option, give `output.path` an extensionless directory name, or set `output.mode: 'directory'` explicitly.",
722
600
  location: { kind: "config" },
723
601
  plugin: pluginName
724
602
  });
@@ -769,6 +647,23 @@ function getInputKind(input) {
769
647
  return "file";
770
648
  }
771
649
  /**
650
+ * The v4 `input` wrapper keys. v4 typed `input` as `{ path }` or `{ data }`; v5 takes the value
651
+ * directly, so the wrapper now matches the "already-parsed document" branch and silently yields
652
+ * an empty build.
653
+ */
654
+ const legacyInputKeys = ["path", "data"];
655
+ /**
656
+ * Detects the v4 `{ path }` / `{ data }` wrapper so it fails loudly instead of being read as a
657
+ * document. A real spec always carries more than these keys, so an object whose keys are drawn
658
+ * only from them is the old shape rather than a document that happens to have a `path` property.
659
+ */
660
+ function isLegacyInput(input) {
661
+ if (Array.isArray(input)) return input.some(isLegacyInput);
662
+ if (typeof input !== "object" || input === null) return false;
663
+ const keys = Object.keys(input);
664
+ return keys.length > 0 && keys.every((key) => legacyInputKeys.includes(key));
665
+ }
666
+ /**
772
667
  * Normalizes `config.input` into an `AdapterSource` the adapter can parse.
773
668
  *
774
669
  * A parsed object and inline content become `{ type: 'data' }`; a URL is kept verbatim and a
@@ -776,6 +671,13 @@ function getInputKind(input) {
776
671
  */
777
672
  function inputToAdapterSource(config) {
778
673
  const input = config.input;
674
+ if (input && isLegacyInput(input)) throw new Diagnostics.Error({
675
+ code: Diagnostics.code.legacyInput,
676
+ severity: "error",
677
+ message: "The `input` option uses the v4 `{ path }` / `{ data }` wrapper.",
678
+ help: "Unwrap it: `input: { path: \"./petStore.yaml\" }` becomes `input: \"./petStore.yaml\"`, and `input: { data: spec }` becomes `input: spec`.",
679
+ location: { kind: "config" }
680
+ });
779
681
  if (!input) throw new Diagnostics.Error({
780
682
  code: Diagnostics.code.inputRequired,
781
683
  severity: "error",
@@ -887,28 +789,24 @@ var Resolver = class Resolver {
887
789
  * schemas (`targetName`) import the emitted name. Names and paths go through the top-level
888
790
  * `name` and `file`, so import entries follow the plugin's conventions, and a per-call
889
791
  * `name` override wins over both.
792
+ *
793
+ * The subtree scan runs through `collectImportedRefNames`, which memoizes by node identity, so a
794
+ * schema shared across the ts, zod, and faker plugins is walked once and every plugin's resolver
795
+ * reads the same ref set instead of re-scanning it per plugin.
890
796
  */
891
797
  imports(options) {
892
798
  const { node, root, output, group, extname = ".ts", name } = options;
893
799
  const resolveName = name ?? ((schemaName) => this.name(schemaName));
894
- const seen = /* @__PURE__ */ new Set();
895
- return (0, _kubb_ast.collect)(node, { schema: (schemaNode) => {
896
- const schemaRef = (0, _kubb_ast.narrowSchema)(schemaNode, "ref");
897
- if (!schemaRef?.ref) return null;
898
- const schemaName = (0, _kubb_ast.resolveRefName)(schemaRef);
899
- if (!schemaName || seen.has(schemaName)) return null;
900
- seen.add(schemaName);
901
- return _kubb_ast.ast.factory.createImport({
902
- name: [resolveName(schemaName)],
903
- path: this.file({
904
- name: schemaName,
905
- extname,
906
- root,
907
- output,
908
- group
909
- }).path
910
- });
911
- } });
800
+ return (0, _kubb_ast.collectImportedRefNames)(node).map((schemaName) => _kubb_ast.ast.factory.createImport({
801
+ name: [resolveName(schemaName)],
802
+ path: this.file({
803
+ name: schemaName,
804
+ extname,
805
+ root,
806
+ output,
807
+ group
808
+ }).path
809
+ }));
912
810
  }
913
811
  /**
914
812
  * Folds each `override` over `base`, left to right, and returns a new resolver with helpers
@@ -1288,6 +1186,38 @@ const ENFORCE_ORDER = {
1288
1186
  post: 1
1289
1187
  };
1290
1188
  const enforceWeight = (plugin) => plugin.enforce ? ENFORCE_ORDER[plugin.enforce] : 0;
1189
+ /**
1190
+ * The options bag a `NormalizedPlugin` starts with before a plugin refines it: a directory output
1191
+ * at the plugin root and empty filter lists.
1192
+ */
1193
+ function defaultPluginOptions() {
1194
+ return {
1195
+ output: {
1196
+ path: ".",
1197
+ mode: "directory"
1198
+ },
1199
+ exclude: [],
1200
+ override: []
1201
+ };
1202
+ }
1203
+ /**
1204
+ * Fills in the `output`, `exclude`, and `override` a `NormalizedPlugin` needs from a plugin's raw
1205
+ * options, running `output` through `normalizeOutput`. Idempotent, so the driver can apply it after
1206
+ * `setOptions` has already run without disturbing an already-normalized bag.
1207
+ */
1208
+ function normalizePluginOptions(rawOptions, pluginName) {
1209
+ const options = {
1210
+ ...defaultPluginOptions(),
1211
+ ...rawOptions ?? {}
1212
+ };
1213
+ const group = "group" in options ? options.group : void 0;
1214
+ options.output = normalizeOutput({
1215
+ output: options.output,
1216
+ group,
1217
+ pluginName
1218
+ });
1219
+ return options;
1220
+ }
1291
1221
  var KubbDriver = class {
1292
1222
  config;
1293
1223
  options;
@@ -1309,13 +1239,6 @@ var KubbDriver = class {
1309
1239
  fileManager = new require_usingCtx.FileManager();
1310
1240
  plugins = /* @__PURE__ */ new Map();
1311
1241
  /**
1312
- * Tracks which plugins have generators registered via `addGenerator()` (hook-based path).
1313
- * Used by the build loop to decide whether to emit generator hooks for a given plugin.
1314
- */
1315
- #hookGeneratorPlugins = /* @__PURE__ */ new Set();
1316
- #resolvers = /* @__PURE__ */ new Map();
1317
- #defaultResolvers = /* @__PURE__ */ new Map();
1318
- /**
1319
1242
  * Removers for every listener the driver added (plugin, generator) so `dispose()` can detach
1320
1243
  * them in one pass. External `hooks.hook(...)` listeners are not tracked.
1321
1244
  */
@@ -1343,15 +1266,8 @@ var KubbDriver = class {
1343
1266
  dependencies: rawPlugin.dependencies,
1344
1267
  enforce: rawPlugin.enforce,
1345
1268
  hooks: rawPlugin.hooks,
1346
- options: rawPlugin.options ?? {
1347
- output: {
1348
- path: ".",
1349
- mode: "directory"
1350
- },
1351
- exclude: [],
1352
- override: []
1353
- },
1354
- resolver: this.#getDefaultResolver(rawPlugin.name)
1269
+ options: rawPlugin.options ?? defaultPluginOptions(),
1270
+ resolver: createResolver({ pluginName: rawPlugin.name })
1355
1271
  };
1356
1272
  }));
1357
1273
  for (const plugin of normalized) {
@@ -1460,46 +1376,30 @@ var KubbDriver = class {
1460
1376
  }
1461
1377
  }
1462
1378
  /**
1463
- * Registers a generator for the given plugin on the shared hook emitter.
1464
- *
1465
- * The generator's `schema`, `operation`, and `operations` methods are registered as
1466
- * listeners on `kubb:generate:schema`, `kubb:generate:operation`, and `kubb:generate:operations`
1467
- * respectively. Each listener is scoped to the owning plugin via a `ctx.plugin.name` check
1468
- * so that generators from different plugins do not cross-fire.
1379
+ * Appends a generator to its owning plugin so the generate loop can call it directly.
1469
1380
  *
1470
- * The renderer comes from `generator.renderer`. Set `generator.renderer = null` (or leave it
1471
- * unset) to opt out of rendering.
1381
+ * The generator's `schema`, `operation`, and `operations` methods run per node during the AST
1382
+ * walk in `#runGenerators`, and their result is routed through `dispatch`. Because a generator is
1383
+ * bound to a plugin, generators from different plugins never cross-fire without a name check. The
1384
+ * renderer comes from `generator.renderer`; set it to `null` (or leave it unset) to opt out of
1385
+ * rendering.
1472
1386
  *
1473
1387
  * Call this method inside `addGenerator()` (in `kubb:plugin:setup`) to wire up a generator.
1474
1388
  */
1475
1389
  registerGenerator(pluginName, generator) {
1476
- const wrap = (method) => {
1477
- if (!method) return void 0;
1478
- return async (node, ctx) => {
1479
- if (ctx.plugin.name !== pluginName) return;
1480
- const result = await method(node, ctx);
1481
- await this.dispatch({
1482
- result,
1483
- renderer: generator.renderer
1484
- });
1485
- };
1486
- };
1487
- this.#unhooks.push(this.hooks.addHooks({
1488
- "kubb:generate:schema": wrap(generator.schema),
1489
- "kubb:generate:operation": wrap(generator.operation),
1490
- "kubb:generate:operations": wrap(generator.operations)
1491
- }));
1492
- this.#hookGeneratorPlugins.add(pluginName);
1390
+ const plugin = this.plugins.get(pluginName);
1391
+ if (!plugin) return;
1392
+ plugin.generators = plugin.generators ? [...plugin.generators, generator] : [generator];
1493
1393
  }
1494
1394
  /**
1495
1395
  * Returns `true` when at least one generator was registered for the given plugin
1496
1396
  * via `addGenerator()` in `kubb:plugin:setup`.
1497
1397
  *
1498
- * Used by the build loop to decide whether to walk the AST and emit generator hooks
1398
+ * Used by the build loop to decide whether to walk the AST and run the generators
1499
1399
  * for a plugin.
1500
1400
  */
1501
1401
  hasHookGenerators(pluginName) {
1502
- return this.#hookGeneratorPlugins.has(pluginName);
1402
+ return (this.plugins.get(pluginName)?.generators?.length ?? 0) > 0;
1503
1403
  }
1504
1404
  /**
1505
1405
  * Runs the full plugin pipeline. Returns the diagnostics collected so far even
@@ -1510,21 +1410,25 @@ var KubbDriver = class {
1510
1410
  async run() {
1511
1411
  const { hooks, config, fileManager } = this;
1512
1412
  const diagnostics = [];
1513
- const updateBuffer = [];
1514
1413
  const parsersMap = /* @__PURE__ */ new Map();
1515
1414
  for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
1415
+ const updateBuffer = [];
1516
1416
  const unhookWrites = fileManager.hooks.addHooks({
1517
1417
  start: async (files) => {
1518
1418
  await hooks.callHook("kubb:files:processing:start", { files });
1519
1419
  },
1520
- update: (item) => {
1521
- updateBuffer.push(item);
1420
+ update: ({ file, processed, total, percentage }) => {
1421
+ updateBuffer.push({
1422
+ file,
1423
+ processed,
1424
+ total,
1425
+ percentage,
1426
+ config
1427
+ });
1522
1428
  },
1523
1429
  end: async (files) => {
1524
- await hooks.callHook("kubb:files:processing:update", { files: updateBuffer.map((item) => ({
1525
- ...item,
1526
- config
1527
- })) });
1430
+ updateBuffer.sort((a, b) => a.processed - b.processed);
1431
+ await hooks.callHook("kubb:files:processing:update", { files: updateBuffer });
1528
1432
  updateBuffer.length = 0;
1529
1433
  await hooks.callHook("kubb:files:processing:end", { files });
1530
1434
  }
@@ -1534,12 +1438,16 @@ var KubbDriver = class {
1534
1438
  const outputRoot = (0, node_path.resolve)(config.root, config.output.path);
1535
1439
  await this.#parseInput();
1536
1440
  await this.setupHooks();
1537
- if (this.adapter && this.inputNode) await hooks.callHook("kubb:build:start", Object.assign({
1538
- config,
1539
- adapter: this.adapter,
1540
- meta: this.inputNode.meta,
1541
- getPlugin: this.getPlugin.bind(this)
1542
- }, this.#filesPayload()));
1441
+ for (const plugin of this.plugins.values()) plugin.options = normalizePluginOptions(plugin.options, plugin.name);
1442
+ if (this.adapter && this.inputNode) {
1443
+ const buildStartContext = this.#withFiles({
1444
+ config,
1445
+ adapter: this.adapter,
1446
+ meta: this.inputNode.meta,
1447
+ getPlugin: this.getPlugin.bind(this)
1448
+ });
1449
+ await hooks.callHook("kubb:build:start", buildStartContext);
1450
+ }
1543
1451
  const generatorPlugins = [];
1544
1452
  for (const plugin of this.plugins.values()) {
1545
1453
  const context = this.getContext(plugin);
@@ -1584,10 +1492,11 @@ var KubbDriver = class {
1584
1492
  });
1585
1493
  }
1586
1494
  diagnostics.push(...await this.#runGenerators(generatorPlugins));
1587
- await hooks.callHook("kubb:plugins:end", Object.assign({ config }, this.#filesPayload()));
1495
+ await hooks.callHook("kubb:plugins:end", this.#withFiles({ config }));
1588
1496
  await fileManager.write(fileManager.files, {
1589
1497
  storage: config.storage,
1590
- parsers: parsersMap
1498
+ parsers: parsersMap,
1499
+ manifest: this.options.manifest
1591
1500
  });
1592
1501
  await hooks.callHook("kubb:build:end", {
1593
1502
  files: this.fileManager.files,
@@ -1603,35 +1512,42 @@ var KubbDriver = class {
1603
1512
  }
1604
1513
  });
1605
1514
  }
1606
- #filesPayload() {
1607
- const driver = this;
1515
+ /**
1516
+ * Widens `extra` with the files present at emit time and a bound `upsertFile`, the shape every
1517
+ * file-carrying hook context shares. Building it here in one place keeps the `files` and
1518
+ * `upsertFile` keys from being dropped by a stray spread at the call site.
1519
+ */
1520
+ #withFiles(extra) {
1608
1521
  return {
1609
- get files() {
1610
- return driver.fileManager.files;
1611
- },
1612
- upsertFile: (...files) => driver.fileManager.upsert(...files)
1522
+ ...extra,
1523
+ files: this.fileManager.files,
1524
+ upsertFile: (...files) => this.fileManager.upsert(...files)
1613
1525
  };
1614
1526
  }
1615
1527
  #emitPluginEnd({ plugin, duration, success, error }) {
1616
- return this.hooks.callHook("kubb:plugin:end", Object.assign({
1528
+ return this.hooks.callHook("kubb:plugin:end", this.#withFiles({
1617
1529
  plugin,
1618
1530
  duration,
1619
1531
  success,
1620
1532
  ...error ? { error } : {},
1621
1533
  config: this.config
1622
- }, this.#filesPayload()));
1534
+ }));
1623
1535
  }
1624
1536
  /**
1625
- * Runs schemas and operations through every plugin's generators. Each node is run
1626
- * through the plugin's macros (from `this.#transforms`) before the generator sees it,
1627
- * so plugins stay isolated and the hot path stays per-node. Schemas run before operations
1628
- * so file output stays deterministic across runs.
1629
- * A failing plugin contributes an error diagnostic so the rest of the build continues.
1630
- * Every plugin also contributes a `timing` diagnostic.
1537
+ * Runs schemas and operations through every plugin's generators. The walk is node-outer: each
1538
+ * schema is visited once and each operation once, and the node fans out to the matching
1539
+ * generators of every plugin in dependency order. A per-node cache (`createNodeCache`) is created
1540
+ * once per node and shared by all of that node's plugins, so node-derived work is computed once
1541
+ * and reused instead of recomputed per plugin. Each node still runs through the plugin's macros
1542
+ * (from `this.#transforms`) and its exclude/include/override filters before that plugin's
1543
+ * generator sees it, so plugins stay isolated. Schemas run before operations so file output
1544
+ * stays deterministic across runs. A generator with a `match` predicate that resolves `false`
1545
+ * for a node is skipped for that node, without calling `schema`/`operation`.
1631
1546
  *
1632
- * Plugins are processed one at a time, in full, so `kubb:plugin:end` fires as each one
1633
- * completes rather than all at once at the end. That ordering drives the CLI's
1634
- * `Plugins N/M` counter.
1547
+ * A failing plugin is dropped from the remaining walk, contributes an error diagnostic, and no
1548
+ * longer aborts the other plugins. `kubb:plugin:end` and each plugin's `timing` diagnostic fire
1549
+ * in dependency order once the walk finishes, driving the CLI's `Plugins N/M` counter. The
1550
+ * `operations` batch fires once per plugin after the single operation walk.
1635
1551
  *
1636
1552
  * When `this.inputNode` is `null`, every entry still gets a `kubb:plugin:end` so
1637
1553
  * post-plugin listeners (the barrel writer and friends) complete.
@@ -1656,13 +1572,10 @@ var KubbDriver = class {
1656
1572
  }
1657
1573
  const transforms = this.#transforms;
1658
1574
  const { schemas, operations } = this.inputNode;
1659
- const emitsSchemaHook = this.hooks.listenerCount("kubb:generate:schema") > 0;
1660
- const emitsOperationHook = this.hooks.listenerCount("kubb:generate:operation") > 0;
1661
- const emitsOperationsHook = this.hooks.listenerCount("kubb:generate:operations") > 0;
1662
1575
  const allowedSchemaNamesByPlugin = /* @__PURE__ */ new Map();
1663
1576
  for (const { plugin } of entries) {
1664
1577
  const { exclude, include, override } = plugin.options;
1665
- if (!((include?.some(({ type }) => OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false))) continue;
1578
+ if (!((include?.some(({ type }) => require_usingCtx.OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false))) continue;
1666
1579
  const resolver = this.getResolver(plugin.name);
1667
1580
  const includedOps = operations.filter((operation) => resolver.default.options(operation, {
1668
1581
  options: plugin.options,
@@ -1672,88 +1585,133 @@ var KubbDriver = class {
1672
1585
  }) !== null);
1673
1586
  allowedSchemaNamesByPlugin.set(plugin.name, (0, _kubb_ast.collectUsedSchemaNames)(includedOps, schemas));
1674
1587
  }
1675
- for (const { plugin, context, hrStart } of entries) {
1588
+ const states = entries.map(({ plugin, context, hrStart }) => {
1676
1589
  const generatorContext = {
1677
1590
  ...context,
1678
1591
  resolver: this.getResolver(plugin.name)
1679
1592
  };
1680
1593
  const { exclude, include, override } = plugin.options;
1681
- const optionsAreStatic = !exclude?.length && !include?.length && !override?.length;
1682
- const allowedSchemaNames = allowedSchemaNamesByPlugin.get(plugin.name) ?? null;
1683
- let error = null;
1684
- const resolveForPlugin = (node) => {
1685
- const transformedNode = transforms.applyTo(plugin.name, node);
1686
- if (optionsAreStatic) return {
1687
- transformedNode,
1688
- options: plugin.options
1689
- };
1690
- const options = generatorContext.resolver.default.options(transformedNode, {
1691
- options: plugin.options,
1692
- exclude,
1693
- include,
1694
- override
1695
- });
1696
- if (options === null) return null;
1697
- return {
1698
- transformedNode,
1699
- options
1700
- };
1594
+ const generators = plugin.generators ?? [];
1595
+ return {
1596
+ plugin,
1597
+ hrStart,
1598
+ generatorContext,
1599
+ exclude,
1600
+ include,
1601
+ override,
1602
+ optionsAreStatic: !exclude?.length && !include?.length && !override?.length,
1603
+ allowedSchemaNames: allowedSchemaNamesByPlugin.get(plugin.name) ?? null,
1604
+ schemaGenerators: generators.filter((generator) => generator.schema),
1605
+ operationGenerators: generators.filter((generator) => generator.operation),
1606
+ operationsGenerators: generators.filter((generator) => generator.operations),
1607
+ pluginOperations: [],
1608
+ error: null
1701
1609
  };
1702
- if (emitsSchemaHook) for (const node of schemas) {
1703
- if (error) break;
1610
+ });
1611
+ const resolveForPlugin = (state, node) => {
1612
+ const transformedNode = transforms.applyTo(state.plugin.name, node);
1613
+ if (state.optionsAreStatic) return {
1614
+ transformedNode,
1615
+ options: state.plugin.options
1616
+ };
1617
+ const options = state.generatorContext.resolver.default.options(transformedNode, {
1618
+ options: state.plugin.options,
1619
+ exclude: state.exclude,
1620
+ include: state.include,
1621
+ override: state.override
1622
+ });
1623
+ if (options === null) return null;
1624
+ return {
1625
+ transformedNode,
1626
+ options
1627
+ };
1628
+ };
1629
+ for (const node of schemas) {
1630
+ const cache = require_usingCtx.createNodeCache();
1631
+ for (const state of states) {
1632
+ if (state.error || !state.schemaGenerators.length) continue;
1704
1633
  try {
1705
- const resolved = resolveForPlugin(node);
1634
+ const resolved = resolveForPlugin(state, node);
1706
1635
  if (!resolved) continue;
1707
1636
  const { transformedNode, options } = resolved;
1708
- if (allowedSchemaNames !== null && transformedNode.name && !allowedSchemaNames.has(transformedNode.name)) continue;
1709
- await this.hooks.callHook("kubb:generate:schema", transformedNode, {
1710
- ...generatorContext,
1711
- options
1712
- });
1637
+ if (state.allowedSchemaNames !== null && transformedNode.name && !state.allowedSchemaNames.has(transformedNode.name)) continue;
1638
+ const ctx = {
1639
+ ...state.generatorContext,
1640
+ options,
1641
+ cache
1642
+ };
1643
+ for (const generator of state.schemaGenerators) {
1644
+ if (!(generator.match ? await generator.match(transformedNode, ctx) : true)) continue;
1645
+ await this.dispatch({
1646
+ result: await generator.schema(transformedNode, ctx),
1647
+ renderer: generator.renderer
1648
+ });
1649
+ }
1650
+ await this.hooks.callHook("kubb:generate:schema", transformedNode, ctx);
1713
1651
  } catch (caughtError) {
1714
- error = require_usingCtx.toError(caughtError);
1652
+ state.error = require_usingCtx.toError(caughtError);
1715
1653
  }
1716
1654
  }
1717
- if (emitsOperationHook) for (const node of operations) {
1718
- if (error) break;
1655
+ }
1656
+ for (const node of operations) {
1657
+ const cache = require_usingCtx.createNodeCache();
1658
+ for (const state of states) {
1659
+ if (state.error || !state.operationGenerators.length && !state.operationsGenerators.length) continue;
1719
1660
  try {
1720
- const resolved = resolveForPlugin(node);
1661
+ const resolved = resolveForPlugin(state, node);
1721
1662
  if (!resolved) continue;
1722
- await this.hooks.callHook("kubb:generate:operation", resolved.transformedNode, {
1723
- ...generatorContext,
1724
- options: resolved.options
1725
- });
1663
+ state.pluginOperations.push(resolved.transformedNode);
1664
+ if (state.operationGenerators.length) {
1665
+ const ctx = {
1666
+ ...state.generatorContext,
1667
+ options: resolved.options,
1668
+ cache
1669
+ };
1670
+ for (const generator of state.operationGenerators) {
1671
+ if (!(generator.match ? await generator.match(resolved.transformedNode, ctx) : true)) continue;
1672
+ await this.dispatch({
1673
+ result: await generator.operation(resolved.transformedNode, ctx),
1674
+ renderer: generator.renderer
1675
+ });
1676
+ }
1677
+ await this.hooks.callHook("kubb:generate:operation", resolved.transformedNode, ctx);
1678
+ }
1726
1679
  } catch (caughtError) {
1727
- error = require_usingCtx.toError(caughtError);
1680
+ state.error = require_usingCtx.toError(caughtError);
1728
1681
  }
1729
1682
  }
1730
- if (!error && emitsOperationsHook) try {
1683
+ }
1684
+ for (const state of states) {
1685
+ if (state.error || !state.operationsGenerators.length) continue;
1686
+ try {
1731
1687
  const ctx = {
1732
- ...generatorContext,
1733
- options: plugin.options
1688
+ ...state.generatorContext,
1689
+ options: state.plugin.options,
1690
+ cache: require_usingCtx.createNodeCache()
1734
1691
  };
1735
- const pluginOperations = operations.reduce((acc, node) => {
1736
- const resolved = resolveForPlugin(node);
1737
- if (resolved) acc.push(resolved.transformedNode);
1738
- return acc;
1739
- }, []);
1740
- await this.hooks.callHook("kubb:generate:operations", pluginOperations, ctx);
1692
+ for (const generator of state.operationsGenerators) await this.dispatch({
1693
+ result: await generator.operations(state.pluginOperations, ctx),
1694
+ renderer: generator.renderer
1695
+ });
1696
+ await this.hooks.callHook("kubb:generate:operations", state.pluginOperations, ctx);
1741
1697
  } catch (caughtError) {
1742
- error = require_usingCtx.toError(caughtError);
1698
+ state.error = require_usingCtx.toError(caughtError);
1743
1699
  }
1744
- const duration = getElapsedMs(hrStart);
1700
+ }
1701
+ for (const state of states) {
1702
+ const duration = getElapsedMs(state.hrStart);
1745
1703
  await this.#emitPluginEnd({
1746
- plugin,
1704
+ plugin: state.plugin,
1747
1705
  duration,
1748
- success: !error,
1749
- error: error ?? void 0
1706
+ success: !state.error,
1707
+ error: state.error ?? void 0
1750
1708
  });
1751
- if (error) diagnostics.push({
1752
- ...Diagnostics.from(error),
1753
- plugin: plugin.name
1709
+ if (state.error) diagnostics.push({
1710
+ ...Diagnostics.from(state.error),
1711
+ plugin: state.plugin.name
1754
1712
  });
1755
1713
  diagnostics.push(Diagnostics.performance({
1756
- plugin: plugin.name,
1714
+ plugin: state.plugin.name,
1757
1715
  duration
1758
1716
  }));
1759
1717
  }
@@ -1798,10 +1756,7 @@ var KubbDriver = class {
1798
1756
  dispose() {
1799
1757
  for (const unhook of this.#unhooks) unhook();
1800
1758
  this.#unhooks.length = 0;
1801
- this.#hookGeneratorPlugins.clear();
1802
1759
  this.#transforms.dispose();
1803
- this.#resolvers.clear();
1804
- this.#defaultResolvers.clear();
1805
1760
  this.fileManager.dispose();
1806
1761
  this.inputNode = null;
1807
1762
  this.#adapterSource = null;
@@ -1809,21 +1764,17 @@ var KubbDriver = class {
1809
1764
  [Symbol.dispose]() {
1810
1765
  this.dispose();
1811
1766
  }
1812
- #getDefaultResolver = memoize(this.#defaultResolvers, (pluginName) => createResolver({ pluginName }));
1813
1767
  /**
1814
- * Merges `partial` with the plugin's default resolver and stores the result.
1815
- * Also mirrors it onto `plugin.resolver` so callers using `getPlugin(name).resolver`
1816
- * get the up-to-date resolver without going through `getResolver()`.
1768
+ * Merges `partial` onto a fresh default resolver and stores the result on `plugin.resolver`,
1769
+ * which is the single source `getResolver` and `getPlugin(name).resolver` both read.
1817
1770
  */
1818
1771
  setPluginResolver(pluginName, partial) {
1819
- const defaultResolver = this.#getDefaultResolver(pluginName);
1820
- const merged = Resolver.merge(defaultResolver, partial);
1821
- this.#resolvers.set(pluginName, merged);
1822
1772
  const plugin = this.plugins.get(pluginName);
1823
- if (plugin) plugin.resolver = merged;
1773
+ if (!plugin) return;
1774
+ plugin.resolver = Resolver.merge(createResolver({ pluginName }), partial);
1824
1775
  }
1825
1776
  getResolver(pluginName) {
1826
- return this.#resolvers.get(pluginName) ?? this.#getDefaultResolver(pluginName);
1777
+ return this.plugins.get(pluginName)?.resolver ?? createResolver({ pluginName });
1827
1778
  }
1828
1779
  getContext(plugin) {
1829
1780
  const driver = this;
@@ -1906,6 +1857,72 @@ var KubbDriver = class {
1906
1857
  }
1907
1858
  };
1908
1859
  //#endregion
1860
+ //#region src/outputManifest.ts
1861
+ /**
1862
+ * Bumped when the stored shape changes, so an older cache is discarded instead of misread.
1863
+ */
1864
+ const VERSION = 1;
1865
+ function hash(value) {
1866
+ return (0, node_crypto.createHash)("sha256").update(value).digest("hex");
1867
+ }
1868
+ const MANIFEST_KEY = "output-manifest.json";
1869
+ async function loadEntries({ cache }) {
1870
+ try {
1871
+ const stored = await cache.readItem(MANIFEST_KEY);
1872
+ if (stored === null) return {};
1873
+ const data = JSON.parse(stored);
1874
+ if (data.version !== VERSION) return {};
1875
+ if (typeof data.entries !== "object" || data.entries === null || Array.isArray(data.entries)) return {};
1876
+ return data.entries;
1877
+ } catch {
1878
+ return {};
1879
+ }
1880
+ }
1881
+ /**
1882
+ * Loads the stored manifest, starting empty when it is missing, unreadable, or from an older
1883
+ * version. `storage` holds the generated files, `cache` holds the manifest itself.
1884
+ *
1885
+ * @example
1886
+ * ```ts
1887
+ * const manifest = await createOutputManifest({ storage: config.storage, cache: cacheStorage({ root: config.root }) })
1888
+ * ```
1889
+ */
1890
+ async function createOutputManifest({ storage, cache }) {
1891
+ const entries = await loadEntries({ cache });
1892
+ const tracked = /* @__PURE__ */ new Map();
1893
+ return {
1894
+ isUpToDate({ key, source, disk }) {
1895
+ const entry = entries[key];
1896
+ if (!entry) return false;
1897
+ return entry.source === hash(source) && entry.output === hash(disk);
1898
+ },
1899
+ track({ key, source }) {
1900
+ tracked.set(key, hash(source));
1901
+ },
1902
+ async commit() {
1903
+ try {
1904
+ const next = { ...entries };
1905
+ await require_usingCtx.inParallel({
1906
+ items: [...tracked],
1907
+ limit: 50,
1908
+ run: async ([key, source]) => {
1909
+ const stored = await storage.readItem(key);
1910
+ if (stored === null) return;
1911
+ next[key] = {
1912
+ source,
1913
+ output: hash(stored)
1914
+ };
1915
+ }
1916
+ });
1917
+ await cache.writeItem(MANIFEST_KEY, JSON.stringify({
1918
+ version: VERSION,
1919
+ entries: next
1920
+ }));
1921
+ } catch {}
1922
+ }
1923
+ };
1924
+ }
1925
+ //#endregion
1909
1926
  //#region src/createStorage.ts
1910
1927
  /**
1911
1928
  * Defines a custom storage backend. The builder receives user options and
@@ -1922,23 +1939,23 @@ var KubbDriver = class {
1922
1939
  *
1923
1940
  * return {
1924
1941
  * name: 'memory',
1925
- * async hasItem(key) {
1942
+ * async existsItem(key) {
1926
1943
  * return store.has(key)
1927
1944
  * },
1928
- * async getItem(key) {
1945
+ * async readItem(key) {
1929
1946
  * return store.get(key) ?? null
1930
1947
  * },
1931
- * async setItem(key, value) {
1948
+ * async writeItem(key, value) {
1932
1949
  * store.set(key, value)
1933
1950
  * },
1934
1951
  * async removeItem(key) {
1935
1952
  * store.delete(key)
1936
1953
  * },
1937
- * async getKeys(base) {
1954
+ * async readKeys(base) {
1938
1955
  * const keys = [...store.keys()]
1939
1956
  * return base ? keys.filter((k) => k.startsWith(base)) : keys
1940
1957
  * },
1941
- * async clear(base) {
1958
+ * async empty(base) {
1942
1959
  * if (!base) store.clear()
1943
1960
  * },
1944
1961
  * }
@@ -1982,10 +1999,11 @@ function createLimiter(concurrency) {
1982
1999
  *
1983
2000
  * Writes are deduplicated and directory-safe:
1984
2001
  * - leading and trailing whitespace is trimmed before writing
1985
- * - the write is skipped when the file content is already identical
2002
+ * - the write is skipped when the file already holds that content, ignoring any trailing newline
2003
+ * a formatter left behind
1986
2004
  * - missing parent directories are created automatically
1987
2005
  * - Bun's native file API is used when running under Bun
1988
- * - concurrent `setItem` calls are capped at {@link WRITE_CONCURRENCY} in flight, so a caller
2006
+ * - concurrent `writeItem` calls are capped at {@link WRITE_CONCURRENCY} in flight, so a caller
1989
2007
  * can fire every file's write without pacing itself
1990
2008
  *
1991
2009
  * @example
@@ -2004,7 +2022,7 @@ const fsStorage = createStorage(() => {
2004
2022
  const limit = createLimiter(WRITE_CONCURRENCY);
2005
2023
  return {
2006
2024
  name: "fs",
2007
- async hasItem(key) {
2025
+ async existsItem(key) {
2008
2026
  try {
2009
2027
  await (0, node_fs_promises.access)((0, node_path.resolve)(key));
2010
2028
  return true;
@@ -2012,20 +2030,20 @@ const fsStorage = createStorage(() => {
2012
2030
  return false;
2013
2031
  }
2014
2032
  },
2015
- async getItem(key) {
2033
+ async readItem(key) {
2016
2034
  try {
2017
2035
  return await (0, node_fs_promises.readFile)((0, node_path.resolve)(key), "utf8");
2018
2036
  } catch (_error) {
2019
2037
  return null;
2020
2038
  }
2021
2039
  },
2022
- async setItem(key, value) {
2040
+ async writeItem(key, value) {
2023
2041
  await limit(() => require_usingCtx.write((0, node_path.resolve)(key), value, { sanity: false }));
2024
2042
  },
2025
2043
  async removeItem(key) {
2026
2044
  await (0, node_fs_promises.rm)((0, node_path.resolve)(key), { force: true });
2027
2045
  },
2028
- async getKeys(base) {
2046
+ async readKeys(base) {
2029
2047
  const resolvedBase = (0, node_path.resolve)(base ?? process.cwd());
2030
2048
  const keys = [];
2031
2049
  try {
@@ -2036,13 +2054,67 @@ const fsStorage = createStorage(() => {
2036
2054
  } catch (_error) {}
2037
2055
  return keys;
2038
2056
  },
2039
- async clear(base) {
2057
+ async empty(base) {
2040
2058
  if (!base) return;
2041
2059
  await require_usingCtx.clean((0, node_path.resolve)(base));
2042
2060
  }
2043
2061
  };
2044
2062
  });
2045
2063
  //#endregion
2064
+ //#region src/storages/cacheStorage.ts
2065
+ /**
2066
+ * Directory Kubb keeps build caches in. A project with a `node_modules` gets
2067
+ * `node_modules/.cache/kubb`, the convention babel and eslint already use, so the cache stays out
2068
+ * of version control. Without one it falls back to the OS temp directory, keyed by root so two
2069
+ * projects sharing that directory keep their own cache.
2070
+ *
2071
+ * @example Inside a project
2072
+ * `resolveCacheDir('/project') // '/project/node_modules/.cache/kubb'`
2073
+ */
2074
+ function resolveCacheDir(root) {
2075
+ const nodeModules = (0, node_path.join)(root, "node_modules");
2076
+ if ((0, node_fs.existsSync)(nodeModules)) return (0, node_path.join)(nodeModules, ".cache", "kubb");
2077
+ return (0, node_path.join)((0, node_os.tmpdir)(), "kubb", (0, node_crypto.createHash)("sha256").update(root).digest("hex").slice(0, 16));
2078
+ }
2079
+ /**
2080
+ * Filesystem storage for build caches rather than generated code. Keys are plain names resolved
2081
+ * inside {@link resolveCacheDir}, so a caller stores `'x.json'` without knowing where the cache
2082
+ * lives. Kept apart from the configured output storage, which may not be a local disk at all.
2083
+ *
2084
+ * @example
2085
+ * ```ts
2086
+ * const cache = cacheStorage({ root: config.root })
2087
+ * await cache.writeItem('output-manifest.json', JSON.stringify(entries))
2088
+ * ```
2089
+ */
2090
+ const cacheStorage = createStorage(({ root = process.cwd() }) => {
2091
+ const dir = resolveCacheDir(root);
2092
+ const storage = fsStorage();
2093
+ const toPath = (key) => (0, node_path.join)(dir, key);
2094
+ return {
2095
+ name: "cache",
2096
+ async existsItem(key) {
2097
+ return storage.existsItem(toPath(key));
2098
+ },
2099
+ async readItem(key) {
2100
+ return storage.readItem(toPath(key));
2101
+ },
2102
+ async writeItem(key, value) {
2103
+ return storage.writeItem(toPath(key), value);
2104
+ },
2105
+ async removeItem(key) {
2106
+ return storage.removeItem(toPath(key));
2107
+ },
2108
+ async readKeys(base) {
2109
+ return storage.readKeys(base ? toPath(base) : dir);
2110
+ },
2111
+ async empty(base) {
2112
+ if (!base) return;
2113
+ return storage.empty(toPath(base));
2114
+ }
2115
+ };
2116
+ });
2117
+ //#endregion
2046
2118
  //#region src/createKubb.ts
2047
2119
  function resolveConfig(userConfig) {
2048
2120
  return {
@@ -2061,6 +2133,13 @@ function resolveConfig(userConfig) {
2061
2133
  };
2062
2134
  }
2063
2135
  /**
2136
+ * Whether anything runs over the output directory after the files are written. Only then can the
2137
+ * bytes on disk stop matching what Kubb wrote, which is what the manifest exists to track.
2138
+ */
2139
+ function hasOutputPasses(output) {
2140
+ return Boolean(output.format || output.lint || output.postGenerate?.length);
2141
+ }
2142
+ /**
2064
2143
  * Kubb code-generation instance bound to a single config entry. Resolves the user
2065
2144
  * config in the constructor, so `config` is available right away, and shares `hooks`,
2066
2145
  * `storage`, and `driver` across the `setup → build` lifecycle.
@@ -2082,6 +2161,7 @@ var Kubb = class {
2082
2161
  config;
2083
2162
  #driver = null;
2084
2163
  #storage = null;
2164
+ #manifest = null;
2085
2165
  constructor(userConfig, options = {}) {
2086
2166
  this.config = resolveConfig(userConfig);
2087
2167
  this.hooks = options.hooks ?? new require_usingCtx.Hookable();
@@ -2099,7 +2179,14 @@ var Kubb = class {
2099
2179
  */
2100
2180
  async setup() {
2101
2181
  const config = this.config;
2102
- const driver = new KubbDriver(config, { hooks: this.hooks });
2182
+ const manifest = hasOutputPasses(config.output) ? await createOutputManifest({
2183
+ storage: config.storage,
2184
+ cache: cacheStorage({ root: config.root })
2185
+ }) : void 0;
2186
+ const driver = new KubbDriver(config, {
2187
+ hooks: this.hooks,
2188
+ manifest
2189
+ });
2103
2190
  this.hooks.setMaxListeners(Math.max(10, config.plugins.length * 4));
2104
2191
  if (config.output.clean) {
2105
2192
  const cleanPath = (0, node_path.resolve)(config.root, config.output.path);
@@ -2110,11 +2197,12 @@ var Kubb = class {
2110
2197
  help: "Point `output.path` at a subdirectory such as `./src/gen` so clean only removes generated code.",
2111
2198
  location: { kind: "config" }
2112
2199
  });
2113
- await config.storage.clear(cleanPath);
2200
+ await config.storage.empty(cleanPath);
2114
2201
  }
2115
2202
  await driver.setup();
2116
2203
  this.#driver = driver;
2117
2204
  this.#storage = config.storage;
2205
+ this.#manifest = manifest ?? null;
2118
2206
  }
2119
2207
  /**
2120
2208
  * Runs the full pipeline and throws on any plugin error.
@@ -2153,6 +2241,70 @@ var Kubb = class {
2153
2241
  _usingCtx$1.d();
2154
2242
  }
2155
2243
  }
2244
+ /**
2245
+ * Run one build and its output passes end to end, emitting the surrounding `kubb:generation:*`
2246
+ * hooks. Never throws on a build error: the outcome comes back in {@link GenerateResult} so the
2247
+ * host decides how failures surface. Telemetry and progress narration stay with the host, which
2248
+ * reads the result and subscribes to the `kubb:*` hooks.
2249
+ *
2250
+ * @example
2251
+ * ```ts
2252
+ * const result = await createKubb(config, { hooks }).generate()
2253
+ * if (!result.success) process.exitCode = 1
2254
+ * ```
2255
+ */
2256
+ async generate(options = {}) {
2257
+ const { hooks, config } = this;
2258
+ const hrStart = process.hrtime();
2259
+ await hooks.callHook("kubb:generation:start", { config });
2260
+ await hooks.callHook("kubb:setup:start");
2261
+ await this.setup();
2262
+ await hooks.callHook("kubb:setup:end");
2263
+ const { files, diagnostics, storage } = await this.safeBuild();
2264
+ for (const diagnostic of diagnostics) {
2265
+ if (!Diagnostics.isProblem(diagnostic)) continue;
2266
+ if (diagnostic.code === Diagnostics.code.unknown) {
2267
+ await hooks.callHook("kubb:error", { error: diagnostic.cause ?? new Error(diagnostic.message) });
2268
+ continue;
2269
+ }
2270
+ await Diagnostics.emit(hooks, diagnostic);
2271
+ }
2272
+ if (Diagnostics.hasError(diagnostics)) {
2273
+ await hooks.callHook("kubb:generation:end", {
2274
+ config,
2275
+ storage,
2276
+ diagnostics,
2277
+ filesCreated: files.length,
2278
+ status: "failed",
2279
+ hrStart
2280
+ });
2281
+ return {
2282
+ success: false,
2283
+ files,
2284
+ diagnostics
2285
+ };
2286
+ }
2287
+ const outputDiagnostics = options.processOutput ? await options.processOutput({
2288
+ config,
2289
+ outputPath: (0, node_path.resolve)(config.root, config.output.path)
2290
+ }) : [];
2291
+ const finalDiagnostics = [...diagnostics, ...outputDiagnostics];
2292
+ const failed = Diagnostics.hasError(outputDiagnostics);
2293
+ if (!failed) await this.#manifest?.commit();
2294
+ await hooks.callHook("kubb:generation:end", {
2295
+ config,
2296
+ storage,
2297
+ diagnostics: finalDiagnostics,
2298
+ filesCreated: files.length,
2299
+ status: failed ? "failed" : "success",
2300
+ hrStart
2301
+ });
2302
+ return {
2303
+ success: !failed,
2304
+ files,
2305
+ diagnostics: finalDiagnostics
2306
+ };
2307
+ }
2156
2308
  dispose() {
2157
2309
  this.#driver?.dispose();
2158
2310
  }
@@ -2393,11 +2545,13 @@ const fileReporter = createReporter({
2393
2545
  const { diagnostics, config } = result;
2394
2546
  if (diagnostics.length === 0) return;
2395
2547
  const report = buildReport(result);
2396
- const content = (0, node_util.stripVTControlCharacters)([config.name ? `# ${config.name} — ${(/* @__PURE__ */ new Date()).toISOString()}` : `# ${(/* @__PURE__ */ new Date()).toISOString()}`, ...[
2548
+ const header = config.name ? `# ${config.name} — ${(/* @__PURE__ */ new Date()).toISOString()}` : `# ${(/* @__PURE__ */ new Date()).toISOString()}`;
2549
+ const sections = [
2397
2550
  buildSummarySection(report),
2398
2551
  buildProblemSection(diagnostics),
2399
2552
  buildTimingSection(report)
2400
- ].filter((section) => section.length > 0).map((section) => section.join("\n"))].join("\n\n"));
2553
+ ].filter((section) => section.length > 0);
2554
+ const content = (0, node_util.stripVTControlCharacters)([header, ...sections.map((section) => section.join("\n"))].join("\n\n"));
2401
2555
  const baseName = `${[
2402
2556
  "kubb",
2403
2557
  config.name,
@@ -2550,23 +2704,23 @@ const memoryStorage = createStorage(() => {
2550
2704
  const store = /* @__PURE__ */ new Map();
2551
2705
  return {
2552
2706
  name: "memory",
2553
- async hasItem(key) {
2707
+ async existsItem(key) {
2554
2708
  return store.has(key);
2555
2709
  },
2556
- async getItem(key) {
2710
+ async readItem(key) {
2557
2711
  return store.get(key) ?? null;
2558
2712
  },
2559
- async setItem(key, value) {
2713
+ async writeItem(key, value) {
2560
2714
  store.set(key, value);
2561
2715
  },
2562
2716
  async removeItem(key) {
2563
2717
  store.delete(key);
2564
2718
  },
2565
- async getKeys(base) {
2719
+ async readKeys(base) {
2566
2720
  const keys = [...store.keys()];
2567
2721
  return base ? keys.filter((k) => k.startsWith(base)) : keys;
2568
2722
  },
2569
- async clear(base) {
2723
+ async empty(base) {
2570
2724
  if (!base) {
2571
2725
  store.clear();
2572
2726
  return;