@kubb/core 5.11.3 → 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.
package/dist/index.cjs CHANGED
@@ -29,10 +29,10 @@ let tinyexec = require("tinyexec");
29
29
  let node_fs_promises = require("node:fs/promises");
30
30
  let node_path = require("node:path");
31
31
  node_path = __toESM(node_path, 1);
32
+ let node_fs = require("node:fs");
32
33
  let node_util = require("node:util");
33
34
  let _kubb_ast = require("@kubb/ast");
34
35
  let node_crypto = require("node:crypto");
35
- let node_fs = require("node:fs");
36
36
  let node_os = require("node:os");
37
37
  let node_readline = require("node:readline");
38
38
  let unstorage = require("unstorage");
@@ -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.3";
44
+ var version = "5.11.5";
45
45
  //#endregion
46
46
  //#region ../../internals/utils/src/casing.ts
47
47
  /**
@@ -247,9 +247,26 @@ async function write(path, data, options = {}) {
247
247
  stored: stored ?? "",
248
248
  source: trimmed
249
249
  })) return null;
250
+ await writeRaw(resolved, content);
251
+ if (options.sanity) {
252
+ const savedData = await read(resolved);
253
+ if (savedData !== content) throw new Error(`Sanity check failed for ${path}\n\nData[${data.length}]:\n${data}\n\nSaved[${savedData.length}]:\n${savedData}\n`);
254
+ return savedData;
255
+ }
256
+ return content;
257
+ }
258
+ /**
259
+ * Writes UTF-8 content verbatim, including empty files and surrounding whitespace.
260
+ * Uses Bun.write under Bun and creates missing parent directories under either runtime.
261
+ *
262
+ * @example
263
+ * await writeRaw('./cache/output.ts', ' // keep these bytes\n\n')
264
+ */
265
+ async function writeRaw(path, content) {
266
+ const resolved = (0, node_path.resolve)(path);
250
267
  if (runtime.isBun) {
251
268
  await Bun.write(resolved, content);
252
- return content;
269
+ return;
253
270
  }
254
271
  try {
255
272
  await (0, node_fs_promises.writeFile)(resolved, content, { encoding: "utf-8" });
@@ -258,12 +275,29 @@ async function write(path, data, options = {}) {
258
275
  await (0, node_fs_promises.mkdir)((0, node_path.dirname)(resolved), { recursive: true });
259
276
  await (0, node_fs_promises.writeFile)(resolved, content, { encoding: "utf-8" });
260
277
  }
261
- if (options.sanity) {
262
- const savedData = await (0, node_fs_promises.readFile)(resolved, { encoding: "utf-8" });
263
- if (savedData !== content) throw new Error(`Sanity check failed for ${path}\n\nData[${data.length}]:\n${data}\n\nSaved[${savedData.length}]:\n${savedData}\n`);
264
- return savedData;
278
+ }
279
+ /**
280
+ * Lists non-hidden regular files below `directory` as relative POSIX paths, excluding directories and symlinks.
281
+ * Uses Bun.Glob under Bun because its Node glob compatibility API lacks directory-entry support.
282
+ *
283
+ * @example
284
+ * for await (const path of listFiles('./src')) console.log(path)
285
+ */
286
+ async function* listFiles(directory) {
287
+ const cwd = (0, node_path.resolve)(directory);
288
+ if (runtime.isBun) {
289
+ yield* new Bun.Glob("**/*").scan({
290
+ cwd,
291
+ dot: false,
292
+ onlyFiles: true,
293
+ followSymlinks: false
294
+ });
295
+ return;
265
296
  }
266
- return content;
297
+ for await (const entry of (0, node_fs_promises.glob)("**/*", {
298
+ cwd,
299
+ withFileTypes: true
300
+ })) if (entry.isFile()) yield toPosixPath((0, node_path.relative)(cwd, (0, node_path.join)(entry.parentPath, entry.name)));
267
301
  }
268
302
  /**
269
303
  * Converts a filesystem path to use POSIX (`/`) separators.
@@ -745,13 +779,15 @@ function serialize(diagnostic) {
745
779
  * with `at`, `fix`, the first cause, and `Learn more` for an error, and one row for a warning.
746
780
  * `verbose` adds the full stack.
747
781
  */
748
- function format(diagnostic, { audience = "agent", verbose = false, width = 0 } = {}) {
782
+ function format(diagnostic, { audience = "agent", verbose = false, width = 0, position } = {}) {
749
783
  const { code, severity, message } = diagnostic;
750
784
  const problem = Diagnostics.isProblem(diagnostic) ? diagnostic : void 0;
751
785
  const human = audience === "human";
752
786
  const paint = human ? node_util.styleText : (_format, text) => text;
753
787
  const [firstLine = "", ...rest] = message.split("\n");
754
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;
755
791
  const help = problem ? getHelp(problem) : void 0;
756
792
  const causes = problem ? getCauses(problem) : [];
757
793
  const crash = problem ? crashCodes.has(problem.code) : false;
@@ -761,7 +797,7 @@ function format(diagnostic, { audience = "agent", verbose = false, width = 0 } =
761
797
  const row = (label, value) => {
762
798
  if (value) details.push(` ${label}: ${value}`);
763
799
  };
764
- row("at", pointer);
800
+ row("at", at && pointer ? `${at} (${pointer})` : pointer);
765
801
  if (problem && severity !== "info") row("why", Diagnostics.explain(problem.code).cause);
766
802
  row("fix", help);
767
803
  for (const cause of causes) row("caused by", cause);
@@ -771,24 +807,25 @@ function format(diagnostic, { audience = "agent", verbose = false, width = 0 } =
771
807
  return [`${severity} ${code}${problem?.plugin ? ` [${problem.plugin}]` : ""}: ${firstLine}`, ...details];
772
808
  }
773
809
  const left = `${paint(severityStyle[severity], severityIcon[severity])} ${paint("bold", firstLine)}`;
774
- const gap = Math.max(2, width - (0, node_util.stripVTControlCharacters)(left).length - code.length);
775
- 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)}`];
776
812
  const frame = rest.filter((line) => line.trim()).map((line) => ` ${paint("dim", line)}`);
777
813
  const label = (name) => paint("dim", name.length < 4 ? name.padEnd(4) : `${name} `);
778
- const rows = [["at", pointer && paint("cyan", pointer)], ["fix", help]];
814
+ const rows = [["at", where && paint("cyan", where)], ["fix", help]];
779
815
  if (severity !== "error") {
780
816
  const first = rows.find(([, value]) => value);
781
817
  return [
782
- headline,
818
+ ...headline,
783
819
  ...frame,
784
820
  ...first ? [` ${label(first[0])} ${first[1]}`] : []
785
821
  ];
786
822
  }
787
823
  rows.push(["caused by", causes[0]], ["report", problem?.issues && paint("cyan", problem.issues)]);
788
- 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 : []] : [])];
789
826
  if (frames?.length) details.push(` ${paint("dim", "stack")}`, ...frames);
790
827
  if (code !== diagnosticCode.unknown) details.push(` ${paint("dim", "↳ Learn more")} ${paint("cyan", Diagnostics.docsUrl(code).replace(/^https:\/\//, ""))}`);
791
- return [headline, ...details];
828
+ return [...headline, ...details];
792
829
  }
793
830
  //#endregion
794
831
  //#region src/Diagnostics.ts
@@ -840,6 +877,7 @@ const failedCode = {
840
877
  parser: diagnosticCode.parserFailed
841
878
  };
842
879
  const docsByCode = new Map(Object.values(diagnosticCatalog).map((doc) => [doc.code, doc]));
880
+ const reporterKey = Symbol.for("kubb.diagnostics.reporter");
843
881
  const countKey = {
844
882
  error: "errors",
845
883
  warning: "warnings",
@@ -849,13 +887,13 @@ const countKey = {
849
887
  * Static helpers for working with {@link Diagnostic}s, plus the run-scoped sink
850
888
  * that lets deep code report a diagnostic without threading a callback.
851
889
  *
852
- * 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`.
853
891
  * `Diagnostics.scope` activates it for a run, so anything inside that run (the
854
892
  * adapter parse, a generator) reports through `Diagnostics.report` and lands
855
893
  * in the same run.
856
894
  */
857
895
  var Diagnostics = class Diagnostics {
858
- static #reporterStorage = new node_async_hooks.AsyncLocalStorage();
896
+ static #reporterStorage = globalThis[reporterKey] ??= new node_async_hooks.AsyncLocalStorage();
859
897
  /**
860
898
  * The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
861
899
  */
@@ -1198,7 +1236,7 @@ function defineExtension(definition) {
1198
1236
  return Object.assign(build(options), {
1199
1237
  name: meta.name,
1200
1238
  meta,
1201
- options
1239
+ options: raw
1202
1240
  });
1203
1241
  }
1204
1242
  });
@@ -1711,7 +1749,8 @@ var Resolver = class Resolver {
1711
1749
  #resolvePath({ baseName, tag, path: groupPath, root, output, group }) {
1712
1750
  if (output.mode !== "directory") return node_path.default.resolve(root, output.path);
1713
1751
  const outputDir = node_path.default.resolve(root, output.path);
1714
- 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);
1715
1754
  assertInside({
1716
1755
  target: result,
1717
1756
  parent: outputDir,
@@ -1952,10 +1991,7 @@ function runParser(parser, print) {
1952
1991
  * Reads a `copy` file and prints it through the parser's `copy` hook, or verbatim with `banner`/`footer` when there is none.
1953
1992
  */
1954
1993
  async function parseCopy(file, parser) {
1955
- let content;
1956
- try {
1957
- content = await (0, node_fs_promises.readFile)(file.copy, "utf8");
1958
- } catch (err) {
1994
+ const content = await (0, node_fs_promises.readFile)(file.copy, "utf8").catch((err) => {
1959
1995
  throw new Diagnostics.Error({
1960
1996
  code: Diagnostics.code.pluginFailed,
1961
1997
  severity: "error",
@@ -1963,7 +1999,7 @@ async function parseCopy(file, parser) {
1963
1999
  help: "Check that the file a plugin copies into the output exists and is readable.",
1964
2000
  cause: toError(err)
1965
2001
  });
1966
- }
2002
+ });
1967
2003
  if (parser?.copy) return runParser(parser, () => parser.parse(_kubb_ast.ast.factory.createFile(parser.copy(file, content))));
1968
2004
  return [
1969
2005
  file.banner,
@@ -1977,9 +2013,9 @@ function mergeFile(a, b) {
1977
2013
  banner: b.banner,
1978
2014
  footer: b.footer,
1979
2015
  copy: b.copy ?? a.copy,
1980
- sources: a.sources.length ? b.sources.length ? [...a.sources, ...b.sources] : a.sources : b.sources,
1981
- imports: a.imports.length ? b.imports.length ? [...a.imports, ...b.imports] : a.imports : b.imports,
1982
- 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]
1983
2019
  };
1984
2020
  }
1985
2021
  function isIndexPath(path) {
@@ -1988,17 +2024,49 @@ function isIndexPath(path) {
1988
2024
  function compareFiles(a, b) {
1989
2025
  return a.path.length - b.path.length || Number(isIndexPath(a.path)) - Number(isIndexPath(b.path));
1990
2026
  }
1991
- function isUnchanged({ stored, source, key, manifest }) {
1992
- if (stored === null) return false;
1993
- 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({
1994
2036
  stored,
1995
2037
  source
1996
- })) return true;
1997
- return manifest?.isUpToDate({
1998
- key,
2038
+ }) || manifest?.isUpToDate({
2039
+ key: file.path,
1999
2040
  source,
2000
2041
  disk: stored
2001
- }) ?? 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
+ };
2002
2070
  }
2003
2071
  /**
2004
2072
  * In-memory file store for generated files, and the writer that turns them into source
@@ -2043,25 +2111,17 @@ var FileManager = class {
2043
2111
  }
2044
2112
  }
2045
2113
  #store(files, mergeExisting) {
2046
- const batch = files.length > 1 ? this.#dedupe(files) : files;
2047
- const resolved = [];
2048
- for (const file of batch) {
2114
+ const batch = /* @__PURE__ */ new Set();
2115
+ const resolved = files.map((file) => {
2049
2116
  const existing = this.#cache.get(file.path);
2050
- 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);
2051
2118
  this.#cache.set(merged.path, merged);
2052
- resolved.push(merged);
2053
- }
2119
+ batch.add(merged.path);
2120
+ return merged;
2121
+ });
2054
2122
  if (resolved.length > 0) this.#sorted = null;
2055
2123
  return resolved;
2056
2124
  }
2057
- #dedupe(files) {
2058
- const seen = /* @__PURE__ */ new Map();
2059
- for (const file of files) {
2060
- const prev = seen.get(file.path);
2061
- seen.set(file.path, prev ? mergeFile(prev, file) : file);
2062
- }
2063
- return [...seen.values()];
2064
- }
2065
2125
  /**
2066
2126
  * Releases all stored files. Called by the core after `kubb:build:end`.
2067
2127
  */
@@ -2089,50 +2149,38 @@ var FileManager = class {
2089
2149
  * Parses and writes every file through a bounded pool of workers. A small spec runs all its files
2090
2150
  * at once; a spec with thousands of files keeps at most {@link FILE_CONCURRENCY} parsed sources in
2091
2151
  * memory rather than holding every source, while still overlapping each file's write with the
2092
- * next file's parse. Files finish in whatever order they parse, so the update rows are sorted back
2093
- * 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.
2094
2154
  *
2095
2155
  * A file the storage already holds is skipped, so a rebuild that generates identical output
2096
- * 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`.
2097
2158
  */
2098
2159
  async write(files, { storage, parsers, manifest, signal, hooks, config }) {
2099
2160
  if (files.length === 0) return;
2100
2161
  await hooks?.callHook("kubb:files:processing:start", { files });
2101
- const total = files.length;
2102
- const updates = [];
2162
+ const unchanged = [];
2163
+ const progress = hooks && config ? createProgress(files, config, hooks) : void 0;
2103
2164
  await inParallel({
2104
2165
  items: files,
2105
2166
  limit: 50,
2106
2167
  run: async (file, index) => {
2107
2168
  signal?.throwIfAborted();
2108
2169
  const source = await this.parse(file, { parsers });
2109
- if (config) updates.push({
2170
+ if (source && !await writeChanged({
2171
+ storage,
2172
+ manifest,
2110
2173
  file,
2111
- processed: index + 1,
2112
- total,
2113
- percentage: (index + 1) / total * 100,
2114
- config
2115
- });
2116
- if (!source) return;
2117
- signal?.throwIfAborted();
2118
- const stored = await storage.readItem(file.path);
2119
- if (isUnchanged({
2120
- stored,
2121
2174
  source,
2122
- key: file.path,
2123
- manifest
2124
- })) return;
2125
- signal?.throwIfAborted();
2126
- await storage.writeItem(file.path, source, { stored });
2127
- manifest?.track({
2128
- key: file.path,
2129
- source
2130
- });
2175
+ signal
2176
+ })) unchanged.push(file.path);
2177
+ await progress?.(index);
2131
2178
  }
2132
2179
  });
2133
- updates.sort((a, b) => a.processed - b.processed);
2134
- await hooks?.callHook("kubb:files:processing:update", { files: updates });
2135
- await hooks?.callHook("kubb:files:processing:end", { files });
2180
+ await hooks?.callHook("kubb:files:processing:end", {
2181
+ files,
2182
+ unchanged
2183
+ });
2136
2184
  }
2137
2185
  };
2138
2186
  //#endregion
@@ -2460,10 +2508,15 @@ var KubbDriver = class {
2460
2508
  plugin.options = normalizePluginOptions(plugin.options, plugin.name);
2461
2509
  }
2462
2510
  if (this.adapter && this.inputNode) {
2511
+ const { meta, schemas, operations } = this.inputNode;
2463
2512
  const buildStartContext = this.#withFiles({
2464
2513
  config,
2465
2514
  adapter: this.adapter,
2466
- meta: this.inputNode.meta,
2515
+ meta,
2516
+ nodeCounts: {
2517
+ schemas: schemas.length,
2518
+ operations: operations.length
2519
+ },
2467
2520
  getPlugin: this.getPlugin.bind(this)
2468
2521
  });
2469
2522
  await hooks.callHook("kubb:build:start", buildStartContext);
@@ -2748,7 +2801,10 @@ var KubbDriver = class {
2748
2801
  }
2749
2802
  return diagnostics;
2750
2803
  }
2751
- /** 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
+ */
2752
2808
  async #dispatchNode({ generators, node, ctx, generate }) {
2753
2809
  for (const generator of generators) {
2754
2810
  if (!(generator.match ? await generator.match(node, ctx) : true)) continue;
@@ -3008,7 +3064,7 @@ const fsStorage = createStorage(() => ({
3008
3064
  name: "fs",
3009
3065
  async readItem(key) {
3010
3066
  try {
3011
- return await (0, node_fs_promises.readFile)((0, node_path.resolve)(key), "utf8");
3067
+ return await read((0, node_path.resolve)(key));
3012
3068
  } catch (_error) {
3013
3069
  return null;
3014
3070
  }
@@ -3026,10 +3082,7 @@ const fsStorage = createStorage(() => ({
3026
3082
  const resolvedBase = (0, node_path.resolve)(base ?? process.cwd());
3027
3083
  const keys = [];
3028
3084
  try {
3029
- for await (const entry of (0, node_fs_promises.glob)("**/*", {
3030
- cwd: resolvedBase,
3031
- withFileTypes: true
3032
- })) if (entry.isFile()) keys.push(toPosixPath((0, node_path.relative)(resolvedBase, (0, node_path.join)(entry.parentPath, entry.name))));
3085
+ for await (const path of listFiles(resolvedBase)) keys.push(path);
3033
3086
  } catch (_error) {}
3034
3087
  return keys;
3035
3088
  },
@@ -3075,7 +3128,9 @@ const cacheStorage = createStorage(({ root = process.cwd() }) => {
3075
3128
  return {
3076
3129
  name: "cache",
3077
3130
  readItem: (key) => fs.readItem(toPath(key)),
3078
- writeItem: (key, value) => fs.writeItem(toPath(key), value),
3131
+ async writeItem(key, value) {
3132
+ await writeRaw(toPath(key), value);
3133
+ },
3079
3134
  removeItem: (key) => fs.removeItem(toPath(key)),
3080
3135
  readKeys: (base) => fs.readKeys(base ? toPath(base) : dir),
3081
3136
  empty: async (base) => base ? fs.empty(toPath(base)) : void 0
@@ -3659,17 +3714,23 @@ const logLevel = {
3659
3714
  * ```
3660
3715
  */
3661
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
+ };
3662
3725
  const reports = [];
3663
3726
  return {
3664
- name: reporter.name,
3665
- needsPluginFiles: reporter.needsPluginFiles,
3727
+ name,
3728
+ needsPluginFiles,
3666
3729
  async report(result, context) {
3667
- const report = await reporter.report(result, context);
3668
- if (reporter.drain) reports.push(report);
3730
+ reports.push(await reporter.report(result, context));
3669
3731
  },
3670
3732
  async drain(context) {
3671
- await reporter.drain?.(context, [...reports]);
3672
- reports.length = 0;
3733
+ await drain(context, reports.splice(0));
3673
3734
  }
3674
3735
  };
3675
3736
  }
@@ -3681,7 +3742,7 @@ function createReporter(reporter) {
3681
3742
  * counts, so every reporter renders the same data.
3682
3743
  */
3683
3744
  function buildReport(result) {
3684
- const { config, diagnostics, filesCreated, status, hrStart } = result;
3745
+ const { config, diagnostics, filesCreated, filesUnchanged = 0, status, hrStart } = result;
3685
3746
  const failed = Diagnostics.failedPlugins(diagnostics);
3686
3747
  const total = config.plugins?.length ?? 0;
3687
3748
  const counts = Diagnostics.count(diagnostics);
@@ -3700,6 +3761,9 @@ function buildReport(result) {
3700
3761
  },
3701
3762
  counts,
3702
3763
  filesCreated,
3764
+ filesUnchanged,
3765
+ fileChanges: result.fileChanges,
3766
+ dryRun: result.dryRun,
3703
3767
  durationMs: getElapsedMs(hrStart),
3704
3768
  output: (0, node_path.resolve)(config.root, config.output.path),
3705
3769
  timings,
@@ -3709,39 +3773,48 @@ function buildReport(result) {
3709
3773
  //#endregion
3710
3774
  //#region src/reporters/summary.ts
3711
3775
  /**
3712
- * Formats the vitest/jest-style summary for one {@link Report}: right-aligned dim labels with
3713
- * `N passed (total)` counts, and a per-plugin `Timings` section when `showTimings`. Pure, so a host
3714
- * 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.
3715
3778
  */
3716
3779
  function formatSummary(report, { showTimings }) {
3717
- const { status, plugins, counts, filesCreated, durationMs, output, timings } = report;
3718
- 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
+ ];
3719
3799
  const notRun = plugins.total - plugins.passed - plugins.failed.length;
3720
- rows.push(["Plugins", status === "success" ? `${(0, node_util.styleText)("green", `${plugins.passed} passed`)} (${plugins.total})` : [
3721
- (0, node_util.styleText)("green", `${plugins.passed} passed`),
3722
- (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,
3723
3803
  notRun > 0 ? (0, node_util.styleText)("dim", `${notRun} not run`) : void 0
3724
- ].filter(Boolean).join(" | ") + ` (${plugins.total})`]);
3725
- if (status === "failed" && plugins.failed.length > 0) rows.push(["Failed", plugins.failed.map((name) => (0, node_util.styleText)("red", name)).join(", ")]);
3726
- if (counts.errors > 0 || counts.warnings > 0) {
3727
- 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(" | ");
3728
- rows.push(["Issues", issues]);
3729
- }
3730
- rows.push(["Files", `${(0, node_util.styleText)("green", String(filesCreated))} generated`]);
3731
- rows.push(["Duration", (0, node_util.styleText)("green", formatMs(durationMs))]);
3732
- rows.push(["Output", output]);
3733
- const labelWidth = Math.max(...rows.map(([label]) => label.length), timings.length > 0 ? 7 : 0);
3734
- 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}`)}`);
3735
3814
  if (showTimings && timings.length > 0) {
3736
- const nameWidth = Math.max(0, ...timings.map((timing) => timing.plugin.length));
3737
- const indent = " ".repeat(labelWidth + 2);
3738
- lines.push((0, node_util.styleText)("dim", "Timings".padStart(labelWidth)));
3739
- for (const timing of timings) {
3740
- const timeStr = formatMs(timing.durationMs);
3741
- const barLength = Math.min(Math.ceil(timing.durationMs / 100), 10);
3742
- const bar = (0, node_util.styleText)("dim", "█".repeat(barLength));
3743
- lines.push(`${indent}${(0, node_util.styleText)("dim", "•")} ${timing.plugin.padEnd(nameWidth)} ${bar} ${timeStr}`);
3744
- }
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))}`);
3745
3818
  }
3746
3819
  return lines;
3747
3820
  }
@@ -4193,6 +4266,7 @@ exports.cliReporter = cliReporter;
4193
4266
  exports.createAdapter = createAdapter;
4194
4267
  exports.createCliReporter = createCliReporter;
4195
4268
  exports.createKubb = createKubb;
4269
+ exports.createOutputManifest = createOutputManifest;
4196
4270
  exports.createRenderer = createRenderer;
4197
4271
  exports.createReporter = createReporter;
4198
4272
  exports.createResolver = createResolver;