@kubb/core 5.11.4 → 5.11.5

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.
@@ -320,6 +320,16 @@ type DiagnosticFormatOptions = {
320
320
  * @default 0
321
321
  */
322
322
  width?: number;
323
+ /**
324
+ * Where the diagnostic's pointer sits in the input file, when the host resolved it. Replaces the
325
+ * pointer on the `at` row, and an error's `human` layout draws `frame` under it.
326
+ */
327
+ position?: {
328
+ file: string;
329
+ line: number;
330
+ column: number;
331
+ frame?: Array<string>;
332
+ };
323
333
  };
324
334
  /**
325
335
  * A {@link Diagnostic} reduced to its JSON-safe fields plus a `docsUrl`, for
@@ -371,7 +381,7 @@ declare function serialize(diagnostic: Diagnostic): SerializedDiagnostic;
371
381
  * with `at`, `fix`, the first cause, and `Learn more` for an error, and one row for a warning.
372
382
  * `verbose` adds the full stack.
373
383
  */
374
- declare function format(diagnostic: Diagnostic, { audience, verbose, width }?: DiagnosticFormatOptions): Array<string>;
384
+ declare function format(diagnostic: Diagnostic, { audience, verbose, width, position }?: DiagnosticFormatOptions): Array<string>;
375
385
  //#endregion
376
386
  //#region src/createReporter.d.ts
377
387
  /**
@@ -414,9 +424,28 @@ type GenerationResult = {
414
424
  */
415
425
  diagnostics: Array<Diagnostic>;
416
426
  /**
417
- * Number of files written for this config.
427
+ * Number of generated files for this config, including unchanged files.
418
428
  */
419
429
  filesCreated: number;
430
+ /**
431
+ * Number of generated files whose content matches the previous output.
432
+ */
433
+ filesUnchanged?: number;
434
+ /**
435
+ * Changes compared with the output before this run, when the host collected them.
436
+ * Line totals include added and deleted files.
437
+ */
438
+ fileChanges?: {
439
+ added: number;
440
+ modified: number;
441
+ deleted: number;
442
+ linesAdded: number;
443
+ linesRemoved: number;
444
+ };
445
+ /**
446
+ * The result previews generation without writing output.
447
+ */
448
+ dryRun?: boolean;
420
449
  status: 'success' | 'failed';
421
450
  /**
422
451
  * `process.hrtime()` snapshot taken when this config started generating.
@@ -453,10 +482,10 @@ type Reporter = {
453
482
  */
454
483
  report: (result: GenerationResult, context: ReporterContext) => void | Promise<void>;
455
484
  /**
456
- * Optional finalizer called once after the run's last config. The host wires it to
457
- * `kubb:lifecycle:end`. {@link createReporter} closes it over the values that `report` returned.
485
+ * Runs once after the run's last config, on `kubb:lifecycle:end`. Only a reporter that collects
486
+ * its configs' reports into one document, such as `json`, has one.
458
487
  */
459
- drain: (context: ReporterContext) => void | Promise<void>;
488
+ drain?: (context: ReporterContext) => void | Promise<void>;
460
489
  /**
461
490
  * Whether the host should collect files from `kubb:plugin:end` hooks.
462
491
  */
@@ -675,6 +704,19 @@ type OutputManifest = {
675
704
  */
676
705
  commit(): Promise<void>;
677
706
  };
707
+ /**
708
+ * Loads the stored manifest, starting empty when it is missing, unreadable, or from an older
709
+ * version. `storage` holds the generated files, `cache` holds the manifest itself.
710
+ *
711
+ * @example
712
+ * ```ts
713
+ * const manifest = await createOutputManifest({ storage: config.storage, cache: cacheStorage({ root: config.root }) })
714
+ * ```
715
+ */
716
+ declare function createOutputManifest({ storage, cache }: {
717
+ storage: Storage;
718
+ cache: Storage;
719
+ }): Promise<OutputManifest>;
678
720
  //#endregion
679
721
  //#region src/Hookable.d.ts
680
722
  /**
@@ -774,7 +816,7 @@ type WriteOptions = ParseOptions & {
774
816
  */
775
817
  manifest?: OutputManifest;
776
818
  /**
777
- * Receives `kubb:files:processing:start`, `:update` (one batch, in input order) and `:end` for the write. `config` fills the update rows.
819
+ * Receives `kubb:files:processing:start`, `:update` (in input order, as files finish) and `:end` for the write. `config` fills the update rows.
778
820
  */
779
821
  hooks?: Hookable<KubbHooks>;
780
822
  config?: Config;
@@ -819,11 +861,12 @@ declare class FileManager {
819
861
  * Parses and writes every file through a bounded pool of workers. A small spec runs all its files
820
862
  * at once; a spec with thousands of files keeps at most {@link FILE_CONCURRENCY} parsed sources in
821
863
  * memory rather than holding every source, while still overlapping each file's write with the
822
- * next file's parse. Files finish in whatever order they parse, so the update rows are sorted back
823
- * into input order before `kubb:files:processing:update` fires once with the whole batch.
864
+ * next file's parse. Files finish in whatever order they parse, so each finished file flushes the
865
+ * run of finished files ahead of it as one `kubb:files:processing:update`, keeping rows in input order.
824
866
  *
825
867
  * A file the storage already holds is skipped, so a rebuild that generates identical output
826
- * writes nothing and leaves every mtime where it was.
868
+ * writes nothing and leaves every mtime where it was. `kubb:files:processing:end` lists the
869
+ * skipped paths as `unchanged`.
827
870
  */
828
871
  write(files: Array<FileNode>, { storage, parsers, manifest, signal, hooks, config }: WriteOptions): Promise<void>;
829
872
  }
@@ -1368,6 +1411,13 @@ type KubbBuildStartContext = FilesContext & {
1368
1411
  * To observe individual schemas and operations use the `kubb:generate:schema` / `kubb:generate:operation` hooks.
1369
1412
  */
1370
1413
  meta: InputMeta | undefined;
1414
+ /**
1415
+ * How many schemas and operations the parsed document holds, the totals for a progress display.
1416
+ */
1417
+ nodeCounts: {
1418
+ schemas: number;
1419
+ operations: number;
1420
+ };
1371
1421
  /**
1372
1422
  * Looks up a registered plugin by name, typed by the plugin registry.
1373
1423
  */
@@ -1519,6 +1569,10 @@ type KubbFilesProcessingEndContext = {
1519
1569
  * All files that were serialized in this batch.
1520
1570
  */
1521
1571
  files: Array<FileNode>;
1572
+ /**
1573
+ * Paths of the files the storage already held with the same content, so they were not written.
1574
+ */
1575
+ unchanged: Array<string>;
1522
1576
  };
1523
1577
  type KubbHookStartContext = {
1524
1578
  /**
@@ -1756,7 +1810,7 @@ type Diagnostic = ProblemDiagnostic | PerformanceDiagnostic | UpdateDiagnostic;
1756
1810
  * Static helpers for working with {@link Diagnostic}s, plus the run-scoped sink
1757
1811
  * that lets deep code report a diagnostic without threading a callback.
1758
1812
  *
1759
- * The sink lives in a single `AsyncLocalStorage` in the `@kubb/core` bundle.
1813
+ * The sink lives in a single `AsyncLocalStorage` on `globalThis`, shared by every copy of `@kubb/core`.
1760
1814
  * `Diagnostics.scope` activates it for a run, so anything inside that run (the
1761
1815
  * adapter parse, a generator) reports through `Diagnostics.report` and lands
1762
1816
  * in the same run.
@@ -3096,9 +3150,11 @@ type Adapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions> = {
3096
3150
  name: TOptions['name'];
3097
3151
  meta?: ExtensionMeta<TOptions['name']>;
3098
3152
  /**
3099
- * Resolved adapter options after defaults have been applied.
3153
+ * The options the adapter was created with, before `setup` applied defaults, as a plugin keeps its
3154
+ * own. Kubb Studio merges its overrides onto these, so a default derived from another option, such
3155
+ * as `emptySchemaType` from `unknownType`, follows a change to it.
3100
3156
  */
3101
- options: TOptions['resolvedOptions'];
3157
+ options: TOptions['options'];
3102
3158
  /**
3103
3159
  * Parsed source document after the first `parse()` call. `null` before parsing.
3104
3160
  */
@@ -3120,6 +3176,13 @@ type Adapter<TOptions extends AdapterFactoryOptions = AdapterFactoryOptions> = {
3120
3176
  validate: (input: string, options?: {
3121
3177
  throwOnError?: boolean;
3122
3178
  }) => Promise<void>;
3179
+ /**
3180
+ * Zero-based UTF-16 offsets of a diagnostic in `source` for a code frame, `end` exclusive; `undefined` when it cannot be found.
3181
+ */
3182
+ locate?: (source: string, location: DiagnosticLocation) => {
3183
+ start: number;
3184
+ end: number;
3185
+ } | undefined;
3123
3186
  };
3124
3187
  /**
3125
3188
  * The object passed to `defineExtension` with `kind: 'adapter'`.
@@ -3152,5 +3215,5 @@ type AdapterFactory<T extends AdapterFactoryOptions = AdapterFactoryOptions> = E
3152
3215
  */
3153
3216
  declare function createAdapter<T extends AdapterFactoryOptions = AdapterFactoryOptions>(definition: AdapterDefinition<T>): AdapterFactory<T>;
3154
3217
  //#endregion
3155
- export { NodeCache as $, ReporterPluginFiles as $t, PluginFactory as A, KubbInfoContext as At, ResolveImportsOptions as B, RunHookOptions as Bt, NormalizedPlugin as C, KubbGenerationEndContext as Ct, Override as D, KubbHookLineContext as Dt, OutputOptions as E, KubbHookEndContext as Et, definePlugin as F, KubbWarnContext as Ft, ResolverFile as G, RendererFactory as Gt, ResolvePathOptions as H, KubbDriver as Ht, BannerMeta as I, PossibleConfig as It, ResolverPatch as J, createStorage as Jt, ResolverFileParams as K, createRenderer as Kt, ResolveBannerContext as L, PostGenerateCommand as Lt, PluginType as M, KubbMessageContext as Mt, ResolvePluginOptions as N, KubbPluginsEndContext as Nt, Plugin as O, KubbHookStartContext as Ot, assertPluginOption as P, KubbSuccessContext as Pt, defineGenerator as Q, ReporterName as Qt, ResolveBannerFile as R, UserConfig as Rt, KubbPluginStartContext as S, KubbFilesProcessingUpdateContext as St, OutputMode as T, KubbGenerationSummaryContext as Tt, Resolver as U, Hookable as Ut, ResolveOptionsContext as V, runHook as Vt, ResolverDefault as W, Renderer as Wt, Generator as X, Reporter as Xt, ResolverPathParams as Y, GenerationResult as Yt, GeneratorContext as Z, ReporterContext as Zt, Filter as _, KubbDiagnosticContext as _t, AdapterSource as a, Parser as an, Diagnostics as at, KubbPluginEndContext as b, KubbFilesProcessingEndContext as bt, ExtensionDependency as c, defineParser as cn, ProblemDiagnostic as ct, ExtensionKind as d, CLIOptions as dt, UserReporter as en, Diagnostic as et, ExtensionMeta as f, Config as ft, Exclude$1 as g, KubbBuildStartContext as gt, resolveMeta as h, KubbBuildEndContext as ht, AdapterFactoryOptions as i, SerializedDiagnostic as in, DiagnosticSeverity as it, PluginName as j, KubbLifecycleStartContext as jt, PluginDefinition as k, KubbHooks as kt, ExtensionDependencyRecord as l, UpdateDiagnostic as lt, defineExtension as m, Input as mt, AdapterDefinition as n, logLevel as nn, DiagnosticKind as nt, createAdapter as o, ParserDefinition as on, PerformanceDiagnostic as ot, checkCompatibility as p, FilesContext as pt, ResolverFilePathParams as q, Storage as qt, AdapterFactory as r, DiagnosticFormatOptions as rn, DiagnosticLocation as rt, ExtensionDependencies as s, ParserFactory as sn, ProblemCode as st, Adapter as t, createReporter as tn, DiagnosticDoc as tt, ExtensionFactory as u, BuildOutput as ut, Group as v, KubbErrorContext as vt, Output as w, KubbGenerationStartContext as wt, KubbPluginSetupContext as x, KubbFilesProcessingStartContext as xt, Include as y, KubbFileProcessingUpdate as yt, ResolveFileOptions as z, HookResult as zt };
3156
- //# sourceMappingURL=createAdapter-D6osJ_0O.d.ts.map
3218
+ export { NodeCache as $, ReporterContext as $t, PluginFactory as A, KubbInfoContext as At, ResolveImportsOptions as B, RunHookOptions as Bt, NormalizedPlugin as C, KubbGenerationEndContext as Ct, Override as D, KubbHookLineContext as Dt, OutputOptions as E, KubbHookEndContext as Et, definePlugin as F, KubbWarnContext as Ft, ResolverFile as G, createOutputManifest as Gt, ResolvePathOptions as H, KubbDriver as Ht, BannerMeta as I, PossibleConfig as It, ResolverPatch as J, createRenderer as Jt, ResolverFileParams as K, Renderer as Kt, ResolveBannerContext as L, PostGenerateCommand as Lt, PluginType as M, KubbMessageContext as Mt, ResolvePluginOptions as N, KubbPluginsEndContext as Nt, Plugin as O, KubbHookStartContext as Ot, assertPluginOption as P, KubbSuccessContext as Pt, defineGenerator as Q, Reporter as Qt, ResolveBannerFile as R, UserConfig as Rt, KubbPluginStartContext as S, KubbFilesProcessingUpdateContext as St, OutputMode as T, KubbGenerationSummaryContext as Tt, Resolver as U, Hookable as Ut, ResolveOptionsContext as V, runHook as Vt, ResolverDefault as W, OutputManifest as Wt, Generator as X, createStorage as Xt, ResolverPathParams as Y, Storage as Yt, GeneratorContext as Z, GenerationResult as Zt, Filter as _, KubbDiagnosticContext as _t, AdapterSource as a, DiagnosticFormatOptions as an, Diagnostics as at, KubbPluginEndContext as b, KubbFilesProcessingEndContext as bt, ExtensionDependency as c, ParserDefinition as cn, ProblemDiagnostic as ct, ExtensionKind as d, CLIOptions as dt, ReporterName as en, Diagnostic as et, ExtensionMeta as f, Config as ft, Exclude$1 as g, KubbBuildStartContext as gt, resolveMeta as h, KubbBuildEndContext as ht, AdapterFactoryOptions as i, logLevel as in, DiagnosticSeverity as it, PluginName as j, KubbLifecycleStartContext as jt, PluginDefinition as k, KubbHooks as kt, ExtensionDependencyRecord as l, ParserFactory as ln, UpdateDiagnostic as lt, defineExtension as m, Input as mt, AdapterDefinition as n, UserReporter as nn, DiagnosticKind as nt, createAdapter as o, SerializedDiagnostic as on, PerformanceDiagnostic as ot, checkCompatibility as p, FilesContext as pt, ResolverFilePathParams as q, RendererFactory as qt, AdapterFactory as r, createReporter as rn, DiagnosticLocation as rt, ExtensionDependencies as s, Parser as sn, ProblemCode as st, Adapter as t, ReporterPluginFiles as tn, DiagnosticDoc as tt, ExtensionFactory as u, defineParser as un, BuildOutput as ut, Group as v, KubbErrorContext as vt, Output as w, KubbGenerationStartContext as wt, KubbPluginSetupContext as x, KubbFilesProcessingStartContext as xt, Include as y, KubbFileProcessingUpdate as yt, ResolveFileOptions as z, HookResult as zt };
3219
+ //# sourceMappingURL=createAdapter-DtX_MKL-.d.ts.map
package/dist/index.cjs CHANGED
@@ -41,7 +41,7 @@ unstorage_drivers_fs = __toESM(unstorage_drivers_fs, 1);
41
41
  let node_url = require("node:url");
42
42
  let node_module = require("node:module");
43
43
  //#region package.json
44
- var version = "5.11.4";
44
+ var version = "5.11.5";
45
45
  //#endregion
46
46
  //#region ../../internals/utils/src/casing.ts
47
47
  /**
@@ -779,13 +779,15 @@ function serialize(diagnostic) {
779
779
  * with `at`, `fix`, the first cause, and `Learn more` for an error, and one row for a warning.
780
780
  * `verbose` adds the full stack.
781
781
  */
782
- function format(diagnostic, { audience = "agent", verbose = false, width = 0 } = {}) {
782
+ function format(diagnostic, { audience = "agent", verbose = false, width = 0, position } = {}) {
783
783
  const { code, severity, message } = diagnostic;
784
784
  const problem = Diagnostics.isProblem(diagnostic) ? diagnostic : void 0;
785
785
  const human = audience === "human";
786
786
  const paint = human ? node_util.styleText : (_format, text) => text;
787
787
  const [firstLine = "", ...rest] = message.split("\n");
788
788
  const pointer = problem?.location && "pointer" in problem.location ? problem.location.pointer : void 0;
789
+ const at = position ? `${position.file}:${position.line}:${position.column}` : void 0;
790
+ const where = at ?? pointer;
789
791
  const help = problem ? getHelp(problem) : void 0;
790
792
  const causes = problem ? getCauses(problem) : [];
791
793
  const crash = problem ? crashCodes.has(problem.code) : false;
@@ -795,7 +797,7 @@ function format(diagnostic, { audience = "agent", verbose = false, width = 0 } =
795
797
  const row = (label, value) => {
796
798
  if (value) details.push(` ${label}: ${value}`);
797
799
  };
798
- row("at", pointer);
800
+ row("at", at && pointer ? `${at} (${pointer})` : pointer);
799
801
  if (problem && severity !== "info") row("why", Diagnostics.explain(problem.code).cause);
800
802
  row("fix", help);
801
803
  for (const cause of causes) row("caused by", cause);
@@ -805,24 +807,25 @@ function format(diagnostic, { audience = "agent", verbose = false, width = 0 } =
805
807
  return [`${severity} ${code}${problem?.plugin ? ` [${problem.plugin}]` : ""}: ${firstLine}`, ...details];
806
808
  }
807
809
  const left = `${paint(severityStyle[severity], severityIcon[severity])} ${paint("bold", firstLine)}`;
808
- const gap = Math.max(2, width - (0, node_util.stripVTControlCharacters)(left).length - code.length);
809
- const headline = `${left}${" ".repeat(gap)}${paint("dim", code)}`;
810
+ const gap = width - (0, node_util.stripVTControlCharacters)(left).length - code.length;
811
+ const headline = width && gap < 2 ? [left, `${" ".repeat(Math.max(0, width - code.length))}${paint("dim", code)}`] : [`${left}${" ".repeat(Math.max(2, gap))}${paint("dim", code)}`];
810
812
  const frame = rest.filter((line) => line.trim()).map((line) => ` ${paint("dim", line)}`);
811
813
  const label = (name) => paint("dim", name.length < 4 ? name.padEnd(4) : `${name} `);
812
- const rows = [["at", pointer && paint("cyan", pointer)], ["fix", help]];
814
+ const rows = [["at", where && paint("cyan", where)], ["fix", help]];
813
815
  if (severity !== "error") {
814
816
  const first = rows.find(([, value]) => value);
815
817
  return [
816
- headline,
818
+ ...headline,
817
819
  ...frame,
818
820
  ...first ? [` ${label(first[0])} ${first[1]}`] : []
819
821
  ];
820
822
  }
821
823
  rows.push(["caused by", causes[0]], ["report", problem?.issues && paint("cyan", problem.issues)]);
822
- const details = [...frame, ...rows.flatMap(([name, value]) => value ? [` ${label(name)} ${value}`] : [])];
824
+ const codeFrame = (position?.frame ?? []).map((line) => ` ${line}`);
825
+ const details = [...frame, ...rows.flatMap(([name, value]) => value ? [` ${label(name)} ${value}`, ...name === "at" ? codeFrame : []] : [])];
823
826
  if (frames?.length) details.push(` ${paint("dim", "stack")}`, ...frames);
824
827
  if (code !== diagnosticCode.unknown) details.push(` ${paint("dim", "↳ Learn more")} ${paint("cyan", Diagnostics.docsUrl(code).replace(/^https:\/\//, ""))}`);
825
- return [headline, ...details];
828
+ return [...headline, ...details];
826
829
  }
827
830
  //#endregion
828
831
  //#region src/Diagnostics.ts
@@ -874,6 +877,7 @@ const failedCode = {
874
877
  parser: diagnosticCode.parserFailed
875
878
  };
876
879
  const docsByCode = new Map(Object.values(diagnosticCatalog).map((doc) => [doc.code, doc]));
880
+ const reporterKey = Symbol.for("kubb.diagnostics.reporter");
877
881
  const countKey = {
878
882
  error: "errors",
879
883
  warning: "warnings",
@@ -883,13 +887,13 @@ const countKey = {
883
887
  * Static helpers for working with {@link Diagnostic}s, plus the run-scoped sink
884
888
  * that lets deep code report a diagnostic without threading a callback.
885
889
  *
886
- * The sink lives in a single `AsyncLocalStorage` in the `@kubb/core` bundle.
890
+ * The sink lives in a single `AsyncLocalStorage` on `globalThis`, shared by every copy of `@kubb/core`.
887
891
  * `Diagnostics.scope` activates it for a run, so anything inside that run (the
888
892
  * adapter parse, a generator) reports through `Diagnostics.report` and lands
889
893
  * in the same run.
890
894
  */
891
895
  var Diagnostics = class Diagnostics {
892
- static #reporterStorage = new node_async_hooks.AsyncLocalStorage();
896
+ static #reporterStorage = globalThis[reporterKey] ??= new node_async_hooks.AsyncLocalStorage();
893
897
  /**
894
898
  * The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
895
899
  */
@@ -1232,7 +1236,7 @@ function defineExtension(definition) {
1232
1236
  return Object.assign(build(options), {
1233
1237
  name: meta.name,
1234
1238
  meta,
1235
- options
1239
+ options: raw
1236
1240
  });
1237
1241
  }
1238
1242
  });
@@ -1745,7 +1749,8 @@ var Resolver = class Resolver {
1745
1749
  #resolvePath({ baseName, tag, path: groupPath, root, output, group }) {
1746
1750
  if (output.mode !== "directory") return node_path.default.resolve(root, output.path);
1747
1751
  const outputDir = node_path.default.resolve(root, output.path);
1748
- const result = group && (groupPath || tag) ? node_path.default.resolve(outputDir, Resolver.#resolveGroupDir(group, group.type === "path" ? groupPath : tag), baseName) : node_path.default.resolve(outputDir, baseName);
1752
+ const groupValue = group?.type === "path" ? groupPath : tag;
1753
+ const result = group && groupValue ? node_path.default.resolve(outputDir, Resolver.#resolveGroupDir(group, groupValue), baseName) : node_path.default.resolve(outputDir, baseName);
1749
1754
  assertInside({
1750
1755
  target: result,
1751
1756
  parent: outputDir,
@@ -1986,10 +1991,7 @@ function runParser(parser, print) {
1986
1991
  * Reads a `copy` file and prints it through the parser's `copy` hook, or verbatim with `banner`/`footer` when there is none.
1987
1992
  */
1988
1993
  async function parseCopy(file, parser) {
1989
- let content;
1990
- try {
1991
- content = await (0, node_fs_promises.readFile)(file.copy, "utf8");
1992
- } catch (err) {
1994
+ const content = await (0, node_fs_promises.readFile)(file.copy, "utf8").catch((err) => {
1993
1995
  throw new Diagnostics.Error({
1994
1996
  code: Diagnostics.code.pluginFailed,
1995
1997
  severity: "error",
@@ -1997,7 +1999,7 @@ async function parseCopy(file, parser) {
1997
1999
  help: "Check that the file a plugin copies into the output exists and is readable.",
1998
2000
  cause: toError(err)
1999
2001
  });
2000
- }
2002
+ });
2001
2003
  if (parser?.copy) return runParser(parser, () => parser.parse(_kubb_ast.ast.factory.createFile(parser.copy(file, content))));
2002
2004
  return [
2003
2005
  file.banner,
@@ -2011,9 +2013,9 @@ function mergeFile(a, b) {
2011
2013
  banner: b.banner,
2012
2014
  footer: b.footer,
2013
2015
  copy: b.copy ?? a.copy,
2014
- sources: a.sources.length ? b.sources.length ? [...a.sources, ...b.sources] : a.sources : b.sources,
2015
- imports: a.imports.length ? b.imports.length ? [...a.imports, ...b.imports] : a.imports : b.imports,
2016
- exports: a.exports.length ? b.exports.length ? [...a.exports, ...b.exports] : a.exports : b.exports
2016
+ sources: [...a.sources, ...b.sources],
2017
+ imports: [...a.imports, ...b.imports],
2018
+ exports: [...a.exports, ...b.exports]
2017
2019
  };
2018
2020
  }
2019
2021
  function isIndexPath(path) {
@@ -2022,17 +2024,49 @@ function isIndexPath(path) {
2022
2024
  function compareFiles(a, b) {
2023
2025
  return a.path.length - b.path.length || Number(isIndexPath(a.path)) - Number(isIndexPath(b.path));
2024
2026
  }
2025
- function isUnchanged({ stored, source, key, manifest }) {
2026
- if (stored === null) return false;
2027
- if (matchesStored({
2027
+ /**
2028
+ * Writes `source` to `file.path` unless the storage already holds it, either verbatim or, per the
2029
+ * manifest, as what the output passes make of it. Checking here rather than in a driver gives a
2030
+ * storage writing to S3 or a database the same guarantee `fsStorage` has. `false` when skipped.
2031
+ */
2032
+ async function writeChanged({ storage, manifest, file, source, signal }) {
2033
+ signal?.throwIfAborted();
2034
+ const stored = await storage.readItem(file.path);
2035
+ if (stored !== null && (matchesStored({
2028
2036
  stored,
2029
2037
  source
2030
- })) return true;
2031
- return manifest?.isUpToDate({
2032
- key,
2038
+ }) || manifest?.isUpToDate({
2039
+ key: file.path,
2033
2040
  source,
2034
2041
  disk: stored
2035
- }) ?? false;
2042
+ }))) return false;
2043
+ signal?.throwIfAborted();
2044
+ await storage.writeItem(file.path, source, { stored });
2045
+ manifest?.track({
2046
+ key: file.path,
2047
+ source
2048
+ });
2049
+ return true;
2050
+ }
2051
+ /** Reports finished files in input order: each finished file flushes the run of finished files ahead of it as one update. */
2052
+ function createProgress(files, config, hooks) {
2053
+ const finished = [];
2054
+ let flushed = 0;
2055
+ return async (index) => {
2056
+ finished[index] = true;
2057
+ const updates = [];
2058
+ while (finished[flushed]) {
2059
+ flushed++;
2060
+ updates.push({
2061
+ file: files[flushed - 1],
2062
+ processed: flushed,
2063
+ total: files.length,
2064
+ percentage: flushed / files.length * 100,
2065
+ config
2066
+ });
2067
+ }
2068
+ if (updates.length) await hooks.callHook("kubb:files:processing:update", { files: updates });
2069
+ };
2036
2070
  }
2037
2071
  /**
2038
2072
  * In-memory file store for generated files, and the writer that turns them into source
@@ -2077,25 +2111,17 @@ var FileManager = class {
2077
2111
  }
2078
2112
  }
2079
2113
  #store(files, mergeExisting) {
2080
- const batch = files.length > 1 ? this.#dedupe(files) : files;
2081
- const resolved = [];
2082
- for (const file of batch) {
2114
+ const batch = /* @__PURE__ */ new Set();
2115
+ const resolved = files.map((file) => {
2083
2116
  const existing = this.#cache.get(file.path);
2084
- const merged = existing && mergeExisting ? _kubb_ast.ast.factory.createFile(mergeFile(existing, file)) : _kubb_ast.ast.factory.createFile(file);
2117
+ const merged = _kubb_ast.ast.factory.createFile(existing && (mergeExisting || batch.has(file.path)) ? mergeFile(existing, file) : file);
2085
2118
  this.#cache.set(merged.path, merged);
2086
- resolved.push(merged);
2087
- }
2119
+ batch.add(merged.path);
2120
+ return merged;
2121
+ });
2088
2122
  if (resolved.length > 0) this.#sorted = null;
2089
2123
  return resolved;
2090
2124
  }
2091
- #dedupe(files) {
2092
- const seen = /* @__PURE__ */ new Map();
2093
- for (const file of files) {
2094
- const prev = seen.get(file.path);
2095
- seen.set(file.path, prev ? mergeFile(prev, file) : file);
2096
- }
2097
- return [...seen.values()];
2098
- }
2099
2125
  /**
2100
2126
  * Releases all stored files. Called by the core after `kubb:build:end`.
2101
2127
  */
@@ -2123,50 +2149,38 @@ var FileManager = class {
2123
2149
  * Parses and writes every file through a bounded pool of workers. A small spec runs all its files
2124
2150
  * at once; a spec with thousands of files keeps at most {@link FILE_CONCURRENCY} parsed sources in
2125
2151
  * memory rather than holding every source, while still overlapping each file's write with the
2126
- * next file's parse. Files finish in whatever order they parse, so the update rows are sorted back
2127
- * into input order before `kubb:files:processing:update` fires once with the whole batch.
2152
+ * next file's parse. Files finish in whatever order they parse, so each finished file flushes the
2153
+ * run of finished files ahead of it as one `kubb:files:processing:update`, keeping rows in input order.
2128
2154
  *
2129
2155
  * A file the storage already holds is skipped, so a rebuild that generates identical output
2130
- * writes nothing and leaves every mtime where it was.
2156
+ * writes nothing and leaves every mtime where it was. `kubb:files:processing:end` lists the
2157
+ * skipped paths as `unchanged`.
2131
2158
  */
2132
2159
  async write(files, { storage, parsers, manifest, signal, hooks, config }) {
2133
2160
  if (files.length === 0) return;
2134
2161
  await hooks?.callHook("kubb:files:processing:start", { files });
2135
- const total = files.length;
2136
- const updates = [];
2162
+ const unchanged = [];
2163
+ const progress = hooks && config ? createProgress(files, config, hooks) : void 0;
2137
2164
  await inParallel({
2138
2165
  items: files,
2139
2166
  limit: 50,
2140
2167
  run: async (file, index) => {
2141
2168
  signal?.throwIfAborted();
2142
2169
  const source = await this.parse(file, { parsers });
2143
- if (config) updates.push({
2170
+ if (source && !await writeChanged({
2171
+ storage,
2172
+ manifest,
2144
2173
  file,
2145
- processed: index + 1,
2146
- total,
2147
- percentage: (index + 1) / total * 100,
2148
- config
2149
- });
2150
- if (!source) return;
2151
- signal?.throwIfAborted();
2152
- const stored = await storage.readItem(file.path);
2153
- if (isUnchanged({
2154
- stored,
2155
2174
  source,
2156
- key: file.path,
2157
- manifest
2158
- })) return;
2159
- signal?.throwIfAborted();
2160
- await storage.writeItem(file.path, source, { stored });
2161
- manifest?.track({
2162
- key: file.path,
2163
- source
2164
- });
2175
+ signal
2176
+ })) unchanged.push(file.path);
2177
+ await progress?.(index);
2165
2178
  }
2166
2179
  });
2167
- updates.sort((a, b) => a.processed - b.processed);
2168
- await hooks?.callHook("kubb:files:processing:update", { files: updates });
2169
- await hooks?.callHook("kubb:files:processing:end", { files });
2180
+ await hooks?.callHook("kubb:files:processing:end", {
2181
+ files,
2182
+ unchanged
2183
+ });
2170
2184
  }
2171
2185
  };
2172
2186
  //#endregion
@@ -2494,10 +2508,15 @@ var KubbDriver = class {
2494
2508
  plugin.options = normalizePluginOptions(plugin.options, plugin.name);
2495
2509
  }
2496
2510
  if (this.adapter && this.inputNode) {
2511
+ const { meta, schemas, operations } = this.inputNode;
2497
2512
  const buildStartContext = this.#withFiles({
2498
2513
  config,
2499
2514
  adapter: this.adapter,
2500
- meta: this.inputNode.meta,
2515
+ meta,
2516
+ nodeCounts: {
2517
+ schemas: schemas.length,
2518
+ operations: operations.length
2519
+ },
2501
2520
  getPlugin: this.getPlugin.bind(this)
2502
2521
  });
2503
2522
  await hooks.callHook("kubb:build:start", buildStartContext);
@@ -2782,7 +2801,10 @@ var KubbDriver = class {
2782
2801
  }
2783
2802
  return diagnostics;
2784
2803
  }
2785
- /** Runs `node` through the generators whose `match` accepts it; `generate` picks the method for the node kind. */
2804
+ /**
2805
+ * Runs `node` through the generators whose `match` accepts it; `generate` picks the method for the
2806
+ * node kind.
2807
+ */
2786
2808
  async #dispatchNode({ generators, node, ctx, generate }) {
2787
2809
  for (const generator of generators) {
2788
2810
  if (!(generator.match ? await generator.match(node, ctx) : true)) continue;
@@ -3692,17 +3714,23 @@ const logLevel = {
3692
3714
  * ```
3693
3715
  */
3694
3716
  function createReporter(reporter) {
3717
+ const { name, needsPluginFiles, drain } = reporter;
3718
+ if (!drain) return {
3719
+ name,
3720
+ needsPluginFiles,
3721
+ async report(result, context) {
3722
+ await reporter.report(result, context);
3723
+ }
3724
+ };
3695
3725
  const reports = [];
3696
3726
  return {
3697
- name: reporter.name,
3698
- needsPluginFiles: reporter.needsPluginFiles,
3727
+ name,
3728
+ needsPluginFiles,
3699
3729
  async report(result, context) {
3700
- const report = await reporter.report(result, context);
3701
- if (reporter.drain) reports.push(report);
3730
+ reports.push(await reporter.report(result, context));
3702
3731
  },
3703
3732
  async drain(context) {
3704
- await reporter.drain?.(context, [...reports]);
3705
- reports.length = 0;
3733
+ await drain(context, reports.splice(0));
3706
3734
  }
3707
3735
  };
3708
3736
  }
@@ -3714,7 +3742,7 @@ function createReporter(reporter) {
3714
3742
  * counts, so every reporter renders the same data.
3715
3743
  */
3716
3744
  function buildReport(result) {
3717
- const { config, diagnostics, filesCreated, status, hrStart } = result;
3745
+ const { config, diagnostics, filesCreated, filesUnchanged = 0, status, hrStart } = result;
3718
3746
  const failed = Diagnostics.failedPlugins(diagnostics);
3719
3747
  const total = config.plugins?.length ?? 0;
3720
3748
  const counts = Diagnostics.count(diagnostics);
@@ -3733,6 +3761,9 @@ function buildReport(result) {
3733
3761
  },
3734
3762
  counts,
3735
3763
  filesCreated,
3764
+ filesUnchanged,
3765
+ fileChanges: result.fileChanges,
3766
+ dryRun: result.dryRun,
3736
3767
  durationMs: getElapsedMs(hrStart),
3737
3768
  output: (0, node_path.resolve)(config.root, config.output.path),
3738
3769
  timings,
@@ -3742,39 +3773,48 @@ function buildReport(result) {
3742
3773
  //#endregion
3743
3774
  //#region src/reporters/summary.ts
3744
3775
  /**
3745
- * Formats the vitest/jest-style summary for one {@link Report}: right-aligned dim labels with
3746
- * `N passed (total)` counts, and a per-plugin `Timings` section when `showTimings`. Pure, so a host
3747
- * can render the same lines through whatever output it already owns.
3776
+ * Puts the outcome first, followed by file counts, labeled plugin results, and the output path.
3777
+ * Hosts that collect file changes get a compact diff section. Verbose output adds plugin timings.
3748
3778
  */
3749
3779
  function formatSummary(report, { showTimings }) {
3750
- const { status, plugins, counts, filesCreated, durationMs, output, timings } = report;
3751
- const rows = [];
3780
+ const { status, plugins, counts, filesCreated, filesUnchanged, fileChanges, dryRun, durationMs, output, timings } = report;
3781
+ const failed = status === "failed";
3782
+ const successTone = counts.warnings ? "yellow" : "green";
3783
+ const tone = failed ? "red" : successTone;
3784
+ const title = (() => {
3785
+ if (failed) return "✗ Generation failed";
3786
+ if (dryRun) return "◇ Dry run complete";
3787
+ if (counts.warnings) return "✓ Generation complete with warnings";
3788
+ if (filesCreated > 0 && filesCreated === filesUnchanged) return "✓ Already up to date";
3789
+ return "✓ Generation complete";
3790
+ })();
3791
+ const separator = (0, node_util.styleText)("dim", " · ");
3792
+ const generated = Math.max(0, filesCreated - filesUnchanged);
3793
+ const fileLabel = `${generated} ${generated === 1 ? "file" : "files"} ${dryRun ? "planned" : "generated"}`;
3794
+ const lines = [
3795
+ `${(0, node_util.styleText)(tone, title)} ${(0, node_util.styleText)("dim", formatMs(durationMs))}`,
3796
+ "",
3797
+ ` ${(0, node_util.styleText)(generated ? "bold" : "dim", fileLabel)}${filesUnchanged ? separator + (0, node_util.styleText)("dim", `${filesUnchanged} unchanged`) : ""}`
3798
+ ];
3752
3799
  const notRun = plugins.total - plugins.passed - plugins.failed.length;
3753
- rows.push(["Plugins", status === "success" ? `${(0, node_util.styleText)("green", `${plugins.passed} passed`)} (${plugins.total})` : [
3754
- (0, node_util.styleText)("green", `${plugins.passed} passed`),
3755
- (0, node_util.styleText)("red", `${plugins.failed.length} failed`),
3800
+ const pluginCounts = [
3801
+ plugins.passed > 0 ? (0, node_util.styleText)("green", `${plugins.passed} passed`) : void 0,
3802
+ plugins.failed.length > 0 ? (0, node_util.styleText)("red", `${plugins.failed.length} failed`) : void 0,
3756
3803
  notRun > 0 ? (0, node_util.styleText)("dim", `${notRun} not run`) : void 0
3757
- ].filter(Boolean).join(" | ") + ` (${plugins.total})`]);
3758
- if (status === "failed" && plugins.failed.length > 0) rows.push(["Failed", plugins.failed.map((name) => (0, node_util.styleText)("red", name)).join(", ")]);
3759
- if (counts.errors > 0 || counts.warnings > 0) {
3760
- const issues = [counts.errors > 0 ? (0, node_util.styleText)("red", `${counts.errors} ${counts.errors === 1 ? "error" : "errors"}`) : void 0, counts.warnings > 0 ? (0, node_util.styleText)("yellow", `${counts.warnings} ${counts.warnings === 1 ? "warning" : "warnings"}`) : void 0].filter(Boolean).join(" | ");
3761
- rows.push(["Issues", issues]);
3762
- }
3763
- rows.push(["Files", `${(0, node_util.styleText)("green", String(filesCreated))} generated`]);
3764
- rows.push(["Duration", (0, node_util.styleText)("green", formatMs(durationMs))]);
3765
- rows.push(["Output", output]);
3766
- const labelWidth = Math.max(...rows.map(([label]) => label.length), timings.length > 0 ? 7 : 0);
3767
- const lines = rows.map(([label, value]) => `${(0, node_util.styleText)("dim", label.padStart(labelWidth))} ${value}`);
3804
+ ].filter(Boolean);
3805
+ lines.push(` ${(0, node_util.styleText)("dim", "Plugins")} ${pluginCounts.join(separator) || (0, node_util.styleText)("dim", "none configured")}`);
3806
+ if (plugins.failed.length) lines.push(` ${(0, node_util.styleText)("dim", "Failed plugins")} ${(0, node_util.styleText)("red", plugins.failed.join(", "))}`);
3807
+ const issues = [counts.errors > 0 ? (0, node_util.styleText)("red", `${counts.errors} ${counts.errors === 1 ? "error" : "errors"}`) : void 0, counts.warnings > 0 ? (0, node_util.styleText)("yellow", `${counts.warnings} ${counts.warnings === 1 ? "warning" : "warnings"}`) : void 0].filter(Boolean);
3808
+ if (issues.length) lines.push(` ${(0, node_util.styleText)("dim", "Issues")} ${issues.join(separator)}`);
3809
+ const destination = (0, node_path.relative)(process.cwd(), output);
3810
+ const displayPath = destination.startsWith("..") || (0, node_path.isAbsolute)(destination) ? output : `./${destination}`;
3811
+ lines.push(` ${(0, node_util.styleText)("dim", failed || dryRun ? "Target" : "Output")} ${(0, node_util.styleText)(failed ? "dim" : "cyan", displayPath)}`);
3812
+ if (dryRun) lines.push(` ${(0, node_util.styleText)("dim", "Nothing was written.")}`);
3813
+ if (fileChanges) lines.push("", (0, node_util.styleText)("bold", dryRun ? "Planned file changes" : "File changes"), ` ${(0, node_util.styleText)(fileChanges.added ? "green" : "dim", `${fileChanges.added} added`)}${separator}${(0, node_util.styleText)(fileChanges.modified ? "yellow" : "dim", `${fileChanges.modified} modified`)}${separator}${(0, node_util.styleText)(fileChanges.deleted ? "red" : "dim", `${fileChanges.deleted} deleted`)}`, ` ${(0, node_util.styleText)("dim", "Lines")} ${(0, node_util.styleText)(fileChanges.linesAdded ? "green" : "dim", `+${fileChanges.linesAdded}`)} ${(0, node_util.styleText)(fileChanges.linesRemoved ? "red" : "dim", `−${fileChanges.linesRemoved}`)}`);
3768
3814
  if (showTimings && timings.length > 0) {
3769
- const nameWidth = Math.max(0, ...timings.map((timing) => timing.plugin.length));
3770
- const indent = " ".repeat(labelWidth + 2);
3771
- lines.push((0, node_util.styleText)("dim", "Timings".padStart(labelWidth)));
3772
- for (const timing of timings) {
3773
- const timeStr = formatMs(timing.durationMs);
3774
- const barLength = Math.min(Math.ceil(timing.durationMs / 100), 10);
3775
- const bar = (0, node_util.styleText)("dim", "█".repeat(barLength));
3776
- lines.push(`${indent}${(0, node_util.styleText)("dim", "•")} ${timing.plugin.padEnd(nameWidth)} ${bar} ${timeStr}`);
3777
- }
3815
+ const nameWidth = Math.max(...timings.map((timing) => timing.plugin.length));
3816
+ lines.push("", (0, node_util.styleText)("dim", "Plugin timings"));
3817
+ for (const timing of timings) lines.push(` ${timing.plugin.padEnd(nameWidth)} ${(0, node_util.styleText)("dim", formatMs(timing.durationMs))}`);
3778
3818
  }
3779
3819
  return lines;
3780
3820
  }
@@ -4226,6 +4266,7 @@ exports.cliReporter = cliReporter;
4226
4266
  exports.createAdapter = createAdapter;
4227
4267
  exports.createCliReporter = createCliReporter;
4228
4268
  exports.createKubb = createKubb;
4269
+ exports.createOutputManifest = createOutputManifest;
4229
4270
  exports.createRenderer = createRenderer;
4230
4271
  exports.createReporter = createReporter;
4231
4272
  exports.createResolver = createResolver;