@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.
@@ -1,8 +1,9 @@
1
1
  import "node:module";
2
2
  import { satisfies } from "verkit";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
- import { mkdir, readFile, writeFile } from "node:fs/promises";
5
- import path, { dirname, isAbsolute, relative, resolve } from "node:path";
4
+ import { glob, mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
+ import path, { dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { existsSync as existsSync$1 } from "node:fs";
6
7
  import { stripVTControlCharacters, styleText } from "node:util";
7
8
  import { ast, collectImportedRefNames, collectUsedSchemaNames, extractStringsFromNodes } from "@kubb/ast";
8
9
  //#region \0rolldown/runtime.js
@@ -13,7 +14,7 @@ var __name = (target, value) => __defProp(target, "name", {
13
14
  });
14
15
  //#endregion
15
16
  //#region package.json
16
- var version = "5.11.3";
17
+ var version = "5.11.5";
17
18
  //#endregion
18
19
  //#region ../../internals/utils/src/casing.ts
19
20
  /**
@@ -190,9 +191,26 @@ async function write(path, data, options = {}) {
190
191
  stored: stored ?? "",
191
192
  source: trimmed
192
193
  })) return null;
194
+ await writeRaw(resolved, content);
195
+ if (options.sanity) {
196
+ const savedData = await read(resolved);
197
+ if (savedData !== content) throw new Error(`Sanity check failed for ${path}\n\nData[${data.length}]:\n${data}\n\nSaved[${savedData.length}]:\n${savedData}\n`);
198
+ return savedData;
199
+ }
200
+ return content;
201
+ }
202
+ /**
203
+ * Writes UTF-8 content verbatim, including empty files and surrounding whitespace.
204
+ * Uses Bun.write under Bun and creates missing parent directories under either runtime.
205
+ *
206
+ * @example
207
+ * await writeRaw('./cache/output.ts', ' // keep these bytes\n\n')
208
+ */
209
+ async function writeRaw(path, content) {
210
+ const resolved = resolve(path);
193
211
  if (runtime.isBun) {
194
212
  await Bun.write(resolved, content);
195
- return content;
213
+ return;
196
214
  }
197
215
  try {
198
216
  await writeFile(resolved, content, { encoding: "utf-8" });
@@ -201,12 +219,29 @@ async function write(path, data, options = {}) {
201
219
  await mkdir(dirname(resolved), { recursive: true });
202
220
  await writeFile(resolved, content, { encoding: "utf-8" });
203
221
  }
204
- if (options.sanity) {
205
- const savedData = await readFile(resolved, { encoding: "utf-8" });
206
- if (savedData !== content) throw new Error(`Sanity check failed for ${path}\n\nData[${data.length}]:\n${data}\n\nSaved[${savedData.length}]:\n${savedData}\n`);
207
- return savedData;
222
+ }
223
+ /**
224
+ * Lists non-hidden regular files below `directory` as relative POSIX paths, excluding directories and symlinks.
225
+ * Uses Bun.Glob under Bun because its Node glob compatibility API lacks directory-entry support.
226
+ *
227
+ * @example
228
+ * for await (const path of listFiles('./src')) console.log(path)
229
+ */
230
+ async function* listFiles(directory) {
231
+ const cwd = resolve(directory);
232
+ if (runtime.isBun) {
233
+ yield* new Bun.Glob("**/*").scan({
234
+ cwd,
235
+ dot: false,
236
+ onlyFiles: true,
237
+ followSymlinks: false
238
+ });
239
+ return;
208
240
  }
209
- return content;
241
+ for await (const entry of glob("**/*", {
242
+ cwd,
243
+ withFileTypes: true
244
+ })) if (entry.isFile()) yield toPosixPath(relative(cwd, join(entry.parentPath, entry.name)));
210
245
  }
211
246
  /**
212
247
  * Converts a filesystem path to use POSIX (`/`) separators.
@@ -579,13 +614,15 @@ function serialize(diagnostic) {
579
614
  * with `at`, `fix`, the first cause, and `Learn more` for an error, and one row for a warning.
580
615
  * `verbose` adds the full stack.
581
616
  */
582
- function format(diagnostic, { audience = "agent", verbose = false, width = 0 } = {}) {
617
+ function format(diagnostic, { audience = "agent", verbose = false, width = 0, position } = {}) {
583
618
  const { code, severity, message } = diagnostic;
584
619
  const problem = Diagnostics.isProblem(diagnostic) ? diagnostic : void 0;
585
620
  const human = audience === "human";
586
621
  const paint = human ? styleText : (_format, text) => text;
587
622
  const [firstLine = "", ...rest] = message.split("\n");
588
623
  const pointer = problem?.location && "pointer" in problem.location ? problem.location.pointer : void 0;
624
+ const at = position ? `${position.file}:${position.line}:${position.column}` : void 0;
625
+ const where = at ?? pointer;
589
626
  const help = problem ? getHelp(problem) : void 0;
590
627
  const causes = problem ? getCauses(problem) : [];
591
628
  const crash = problem ? crashCodes.has(problem.code) : false;
@@ -595,7 +632,7 @@ function format(diagnostic, { audience = "agent", verbose = false, width = 0 } =
595
632
  const row = (label, value) => {
596
633
  if (value) details.push(` ${label}: ${value}`);
597
634
  };
598
- row("at", pointer);
635
+ row("at", at && pointer ? `${at} (${pointer})` : pointer);
599
636
  if (problem && severity !== "info") row("why", Diagnostics.explain(problem.code).cause);
600
637
  row("fix", help);
601
638
  for (const cause of causes) row("caused by", cause);
@@ -605,24 +642,25 @@ function format(diagnostic, { audience = "agent", verbose = false, width = 0 } =
605
642
  return [`${severity} ${code}${problem?.plugin ? ` [${problem.plugin}]` : ""}: ${firstLine}`, ...details];
606
643
  }
607
644
  const left = `${paint(severityStyle[severity], severityIcon[severity])} ${paint("bold", firstLine)}`;
608
- const gap = Math.max(2, width - stripVTControlCharacters(left).length - code.length);
609
- const headline = `${left}${" ".repeat(gap)}${paint("dim", code)}`;
645
+ const gap = width - stripVTControlCharacters(left).length - code.length;
646
+ 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)}`];
610
647
  const frame = rest.filter((line) => line.trim()).map((line) => ` ${paint("dim", line)}`);
611
648
  const label = (name) => paint("dim", name.length < 4 ? name.padEnd(4) : `${name} `);
612
- const rows = [["at", pointer && paint("cyan", pointer)], ["fix", help]];
649
+ const rows = [["at", where && paint("cyan", where)], ["fix", help]];
613
650
  if (severity !== "error") {
614
651
  const first = rows.find(([, value]) => value);
615
652
  return [
616
- headline,
653
+ ...headline,
617
654
  ...frame,
618
655
  ...first ? [` ${label(first[0])} ${first[1]}`] : []
619
656
  ];
620
657
  }
621
658
  rows.push(["caused by", causes[0]], ["report", problem?.issues && paint("cyan", problem.issues)]);
622
- const details = [...frame, ...rows.flatMap(([name, value]) => value ? [` ${label(name)} ${value}`] : [])];
659
+ const codeFrame = (position?.frame ?? []).map((line) => ` ${line}`);
660
+ const details = [...frame, ...rows.flatMap(([name, value]) => value ? [` ${label(name)} ${value}`, ...name === "at" ? codeFrame : []] : [])];
623
661
  if (frames?.length) details.push(` ${paint("dim", "stack")}`, ...frames);
624
662
  if (code !== diagnosticCode.unknown) details.push(` ${paint("dim", "↳ Learn more")} ${paint("cyan", Diagnostics.docsUrl(code).replace(/^https:\/\//, ""))}`);
625
- return [headline, ...details];
663
+ return [...headline, ...details];
626
664
  }
627
665
  //#endregion
628
666
  //#region src/Diagnostics.ts
@@ -674,6 +712,7 @@ const failedCode = {
674
712
  parser: diagnosticCode.parserFailed
675
713
  };
676
714
  const docsByCode = new Map(Object.values(diagnosticCatalog).map((doc) => [doc.code, doc]));
715
+ const reporterKey = Symbol.for("kubb.diagnostics.reporter");
677
716
  const countKey = {
678
717
  error: "errors",
679
718
  warning: "warnings",
@@ -683,13 +722,13 @@ const countKey = {
683
722
  * Static helpers for working with {@link Diagnostic}s, plus the run-scoped sink
684
723
  * that lets deep code report a diagnostic without threading a callback.
685
724
  *
686
- * The sink lives in a single `AsyncLocalStorage` in the `@kubb/core` bundle.
725
+ * The sink lives in a single `AsyncLocalStorage` on `globalThis`, shared by every copy of `@kubb/core`.
687
726
  * `Diagnostics.scope` activates it for a run, so anything inside that run (the
688
727
  * adapter parse, a generator) reports through `Diagnostics.report` and lands
689
728
  * in the same run.
690
729
  */
691
730
  var Diagnostics = class Diagnostics {
692
- static #reporterStorage = new AsyncLocalStorage();
731
+ static #reporterStorage = globalThis[reporterKey] ??= new AsyncLocalStorage();
693
732
  /**
694
733
  * The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
695
734
  */
@@ -1032,7 +1071,7 @@ function defineExtension(definition) {
1032
1071
  return Object.assign(build(options), {
1033
1072
  name: meta.name,
1034
1073
  meta,
1035
- options
1074
+ options: raw
1036
1075
  });
1037
1076
  }
1038
1077
  });
@@ -1527,7 +1566,8 @@ var Resolver = class Resolver {
1527
1566
  #resolvePath({ baseName, tag, path: groupPath, root, output, group }) {
1528
1567
  if (output.mode !== "directory") return path.resolve(root, output.path);
1529
1568
  const outputDir = path.resolve(root, output.path);
1530
- const result = group && (groupPath || tag) ? path.resolve(outputDir, Resolver.#resolveGroupDir(group, group.type === "path" ? groupPath : tag), baseName) : path.resolve(outputDir, baseName);
1569
+ const groupValue = group?.type === "path" ? groupPath : tag;
1570
+ const result = group && groupValue ? path.resolve(outputDir, Resolver.#resolveGroupDir(group, groupValue), baseName) : path.resolve(outputDir, baseName);
1531
1571
  assertInside({
1532
1572
  target: result,
1533
1573
  parent: outputDir,
@@ -1768,10 +1808,7 @@ function runParser(parser, print) {
1768
1808
  * Reads a `copy` file and prints it through the parser's `copy` hook, or verbatim with `banner`/`footer` when there is none.
1769
1809
  */
1770
1810
  async function parseCopy(file, parser) {
1771
- let content;
1772
- try {
1773
- content = await readFile(file.copy, "utf8");
1774
- } catch (err) {
1811
+ const content = await readFile(file.copy, "utf8").catch((err) => {
1775
1812
  throw new Diagnostics.Error({
1776
1813
  code: Diagnostics.code.pluginFailed,
1777
1814
  severity: "error",
@@ -1779,7 +1816,7 @@ async function parseCopy(file, parser) {
1779
1816
  help: "Check that the file a plugin copies into the output exists and is readable.",
1780
1817
  cause: toError(err)
1781
1818
  });
1782
- }
1819
+ });
1783
1820
  if (parser?.copy) return runParser(parser, () => parser.parse(ast.factory.createFile(parser.copy(file, content))));
1784
1821
  return [
1785
1822
  file.banner,
@@ -1793,9 +1830,9 @@ function mergeFile(a, b) {
1793
1830
  banner: b.banner,
1794
1831
  footer: b.footer,
1795
1832
  copy: b.copy ?? a.copy,
1796
- sources: a.sources.length ? b.sources.length ? [...a.sources, ...b.sources] : a.sources : b.sources,
1797
- imports: a.imports.length ? b.imports.length ? [...a.imports, ...b.imports] : a.imports : b.imports,
1798
- exports: a.exports.length ? b.exports.length ? [...a.exports, ...b.exports] : a.exports : b.exports
1833
+ sources: [...a.sources, ...b.sources],
1834
+ imports: [...a.imports, ...b.imports],
1835
+ exports: [...a.exports, ...b.exports]
1799
1836
  };
1800
1837
  }
1801
1838
  function isIndexPath(path) {
@@ -1804,17 +1841,49 @@ function isIndexPath(path) {
1804
1841
  function compareFiles(a, b) {
1805
1842
  return a.path.length - b.path.length || Number(isIndexPath(a.path)) - Number(isIndexPath(b.path));
1806
1843
  }
1807
- function isUnchanged({ stored, source, key, manifest }) {
1808
- if (stored === null) return false;
1809
- if (matchesStored({
1844
+ /**
1845
+ * Writes `source` to `file.path` unless the storage already holds it, either verbatim or, per the
1846
+ * manifest, as what the output passes make of it. Checking here rather than in a driver gives a
1847
+ * storage writing to S3 or a database the same guarantee `fsStorage` has. `false` when skipped.
1848
+ */
1849
+ async function writeChanged({ storage, manifest, file, source, signal }) {
1850
+ signal?.throwIfAborted();
1851
+ const stored = await storage.readItem(file.path);
1852
+ if (stored !== null && (matchesStored({
1810
1853
  stored,
1811
1854
  source
1812
- })) return true;
1813
- return manifest?.isUpToDate({
1814
- key,
1855
+ }) || manifest?.isUpToDate({
1856
+ key: file.path,
1815
1857
  source,
1816
1858
  disk: stored
1817
- }) ?? false;
1859
+ }))) return false;
1860
+ signal?.throwIfAborted();
1861
+ await storage.writeItem(file.path, source, { stored });
1862
+ manifest?.track({
1863
+ key: file.path,
1864
+ source
1865
+ });
1866
+ return true;
1867
+ }
1868
+ /** Reports finished files in input order: each finished file flushes the run of finished files ahead of it as one update. */
1869
+ function createProgress(files, config, hooks) {
1870
+ const finished = [];
1871
+ let flushed = 0;
1872
+ return async (index) => {
1873
+ finished[index] = true;
1874
+ const updates = [];
1875
+ while (finished[flushed]) {
1876
+ flushed++;
1877
+ updates.push({
1878
+ file: files[flushed - 1],
1879
+ processed: flushed,
1880
+ total: files.length,
1881
+ percentage: flushed / files.length * 100,
1882
+ config
1883
+ });
1884
+ }
1885
+ if (updates.length) await hooks.callHook("kubb:files:processing:update", { files: updates });
1886
+ };
1818
1887
  }
1819
1888
  /**
1820
1889
  * In-memory file store for generated files, and the writer that turns them into source
@@ -1859,25 +1928,17 @@ var FileManager = class {
1859
1928
  }
1860
1929
  }
1861
1930
  #store(files, mergeExisting) {
1862
- const batch = files.length > 1 ? this.#dedupe(files) : files;
1863
- const resolved = [];
1864
- for (const file of batch) {
1931
+ const batch = /* @__PURE__ */ new Set();
1932
+ const resolved = files.map((file) => {
1865
1933
  const existing = this.#cache.get(file.path);
1866
- const merged = existing && mergeExisting ? ast.factory.createFile(mergeFile(existing, file)) : ast.factory.createFile(file);
1934
+ const merged = ast.factory.createFile(existing && (mergeExisting || batch.has(file.path)) ? mergeFile(existing, file) : file);
1867
1935
  this.#cache.set(merged.path, merged);
1868
- resolved.push(merged);
1869
- }
1936
+ batch.add(merged.path);
1937
+ return merged;
1938
+ });
1870
1939
  if (resolved.length > 0) this.#sorted = null;
1871
1940
  return resolved;
1872
1941
  }
1873
- #dedupe(files) {
1874
- const seen = /* @__PURE__ */ new Map();
1875
- for (const file of files) {
1876
- const prev = seen.get(file.path);
1877
- seen.set(file.path, prev ? mergeFile(prev, file) : file);
1878
- }
1879
- return [...seen.values()];
1880
- }
1881
1942
  /**
1882
1943
  * Releases all stored files. Called by the core after `kubb:build:end`.
1883
1944
  */
@@ -1905,50 +1966,38 @@ var FileManager = class {
1905
1966
  * Parses and writes every file through a bounded pool of workers. A small spec runs all its files
1906
1967
  * at once; a spec with thousands of files keeps at most {@link FILE_CONCURRENCY} parsed sources in
1907
1968
  * memory rather than holding every source, while still overlapping each file's write with the
1908
- * next file's parse. Files finish in whatever order they parse, so the update rows are sorted back
1909
- * into input order before `kubb:files:processing:update` fires once with the whole batch.
1969
+ * next file's parse. Files finish in whatever order they parse, so each finished file flushes the
1970
+ * run of finished files ahead of it as one `kubb:files:processing:update`, keeping rows in input order.
1910
1971
  *
1911
1972
  * A file the storage already holds is skipped, so a rebuild that generates identical output
1912
- * writes nothing and leaves every mtime where it was.
1973
+ * writes nothing and leaves every mtime where it was. `kubb:files:processing:end` lists the
1974
+ * skipped paths as `unchanged`.
1913
1975
  */
1914
1976
  async write(files, { storage, parsers, manifest, signal, hooks, config }) {
1915
1977
  if (files.length === 0) return;
1916
1978
  await hooks?.callHook("kubb:files:processing:start", { files });
1917
- const total = files.length;
1918
- const updates = [];
1979
+ const unchanged = [];
1980
+ const progress = hooks && config ? createProgress(files, config, hooks) : void 0;
1919
1981
  await inParallel({
1920
1982
  items: files,
1921
1983
  limit: 50,
1922
1984
  run: async (file, index) => {
1923
1985
  signal?.throwIfAborted();
1924
1986
  const source = await this.parse(file, { parsers });
1925
- if (config) updates.push({
1987
+ if (source && !await writeChanged({
1988
+ storage,
1989
+ manifest,
1926
1990
  file,
1927
- processed: index + 1,
1928
- total,
1929
- percentage: (index + 1) / total * 100,
1930
- config
1931
- });
1932
- if (!source) return;
1933
- signal?.throwIfAborted();
1934
- const stored = await storage.readItem(file.path);
1935
- if (isUnchanged({
1936
- stored,
1937
1991
  source,
1938
- key: file.path,
1939
- manifest
1940
- })) return;
1941
- signal?.throwIfAborted();
1942
- await storage.writeItem(file.path, source, { stored });
1943
- manifest?.track({
1944
- key: file.path,
1945
- source
1946
- });
1992
+ signal
1993
+ })) unchanged.push(file.path);
1994
+ await progress?.(index);
1947
1995
  }
1948
1996
  });
1949
- updates.sort((a, b) => a.processed - b.processed);
1950
- await hooks?.callHook("kubb:files:processing:update", { files: updates });
1951
- await hooks?.callHook("kubb:files:processing:end", { files });
1997
+ await hooks?.callHook("kubb:files:processing:end", {
1998
+ files,
1999
+ unchanged
2000
+ });
1952
2001
  }
1953
2002
  };
1954
2003
  //#endregion
@@ -2276,10 +2325,15 @@ var KubbDriver = class {
2276
2325
  plugin.options = normalizePluginOptions(plugin.options, plugin.name);
2277
2326
  }
2278
2327
  if (this.adapter && this.inputNode) {
2328
+ const { meta, schemas, operations } = this.inputNode;
2279
2329
  const buildStartContext = this.#withFiles({
2280
2330
  config,
2281
2331
  adapter: this.adapter,
2282
- meta: this.inputNode.meta,
2332
+ meta,
2333
+ nodeCounts: {
2334
+ schemas: schemas.length,
2335
+ operations: operations.length
2336
+ },
2283
2337
  getPlugin: this.getPlugin.bind(this)
2284
2338
  });
2285
2339
  await hooks.callHook("kubb:build:start", buildStartContext);
@@ -2564,7 +2618,10 @@ var KubbDriver = class {
2564
2618
  }
2565
2619
  return diagnostics;
2566
2620
  }
2567
- /** Runs `node` through the generators whose `match` accepts it; `generate` picks the method for the node kind. */
2621
+ /**
2622
+ * Runs `node` through the generators whose `match` accepts it; `generate` picks the method for the
2623
+ * node kind.
2624
+ */
2568
2625
  async #dispatchNode({ generators, node, ctx, generate }) {
2569
2626
  for (const generator of generators) {
2570
2627
  if (!(generator.match ? await generator.match(node, ctx) : true)) continue;
@@ -2799,6 +2856,6 @@ var Hookable = class {
2799
2856
  }
2800
2857
  };
2801
2858
  //#endregion
2802
- export { inParallel as A, useOptions as C, Diagnostics as D, resolveMeta as E, toError as F, camelCase as I, __name as L, read as M, toPosixPath as N, formatMs as O, write as P, useKubb as S, defineExtension as T, setMacros as _, FileManager as a, tryUseKubb as b, Resolver as c, definePlugin as d, addGenerator as f, requirePlugin as g, kubbContext as h, createNodeCache as i, isPathInside as j, getElapsedMs as k, getInputKind as l, injectFile as m, KubbDriver as n, _usingCtx as o, addMacro as p, pluginNotFoundError as r, createResolver as s, Hookable as t, assertPluginOption as u, setOptions as v, checkCompatibility as w, useConfig as x, setResolver as y };
2859
+ export { inParallel as A, __name as B, useOptions as C, Diagnostics as D, resolveMeta as E, rm as F, write as I, writeRaw as L, isPathInside as M, listFiles as N, formatMs as O, read as P, toError as R, useKubb as S, defineExtension as T, setMacros as _, FileManager as a, tryUseKubb as b, Resolver as c, definePlugin as d, addGenerator as f, requirePlugin as g, kubbContext as h, createNodeCache as i, existsSync$1 as j, getElapsedMs as k, getInputKind as l, injectFile as m, KubbDriver as n, _usingCtx as o, addMacro as p, pluginNotFoundError as r, createResolver as s, Hookable as t, assertPluginOption as u, setOptions as v, checkCompatibility as w, useConfig as x, setResolver as y, camelCase as z };
2803
2860
 
2804
- //# sourceMappingURL=Hookable-Ce-Xu-KB.js.map
2861
+ //# sourceMappingURL=Hookable-7Kv_oj72.js.map