@kubb/core 5.0.0-beta.107 → 5.0.0-beta.109

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-lve4tKsy.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
@@ -151,130 +153,11 @@ function randomCliColor(text) {
151
153
  return (0, node_util.styleText)(color, text);
152
154
  }
153
155
  //#endregion
154
- //#region package.json
155
- var version = "5.0.0-beta.107";
156
- //#endregion
157
- //#region src/constants.ts
158
- /**
159
- * Plugin `include` filter types that select operations directly. When one of these is set
160
- * without a `schemaName` include, the generate phase pre-scans operations to compute the set
161
- * of schemas they reach, so unreachable schemas can be pruned for that plugin.
162
- */
163
- const OPERATION_FILTER_TYPES = /* @__PURE__ */ new Set([
164
- "tag",
165
- "operationId",
166
- "path",
167
- "method",
168
- "contentType"
169
- ]);
170
- /**
171
- * Stable codes Kubb attaches to a `Diagnostic`. Each maps to a known failure mode
172
- * and stays stable so it can be referenced in tooling and (later) docs. Reference
173
- * these instead of inlining the string at a throw site.
174
- */
175
- const diagnosticCode = {
176
- /**
177
- * Fallback for an unstructured error with no specific code.
178
- */
179
- unknown: "KUBB_UNKNOWN",
180
- /**
181
- * The file or URL set as `input` could not be read.
182
- */
183
- inputNotFound: "KUBB_INPUT_NOT_FOUND",
184
- /**
185
- * An adapter was configured without an `input`.
186
- */
187
- inputRequired: "KUBB_INPUT_REQUIRED",
188
- /**
189
- * `input` uses the v4 `{ path }` / `{ data }` wrapper, which v5 reads as a parsed
190
- * document instead of a pointer to one.
191
- */
192
- legacyInput: "KUBB_LEGACY_INPUT",
193
- /**
194
- * The parsed `input` carries no `openapi` or `swagger` version, so it is not a
195
- * document the adapter can read.
196
- */
197
- invalidDocument: "KUBB_INVALID_DOCUMENT",
198
- /**
199
- * A `$ref` (or equivalent reference) could not be resolved in the source document.
200
- */
201
- refNotFound: "KUBB_REF_NOT_FOUND",
202
- /**
203
- * A server variable value is not allowed by its `enum`.
204
- */
205
- invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE",
206
- /**
207
- * A required plugin is missing from the config.
208
- */
209
- pluginNotFound: "KUBB_PLUGIN_NOT_FOUND",
210
- /**
211
- * A plugin threw while generating.
212
- */
213
- pluginFailed: "KUBB_PLUGIN_FAILED",
214
- /**
215
- * A plugin reported a non-fatal warning through `ctx.warn`.
216
- */
217
- pluginWarning: "KUBB_PLUGIN_WARNING",
218
- /**
219
- * A plugin reported an informational message through `ctx.info`.
220
- */
221
- pluginInfo: "KUBB_PLUGIN_INFO",
222
- /**
223
- * A schema uses a `format` Kubb does not map to a specific type. Reserved for
224
- * adapters to emit as a `warning`.
225
- */
226
- unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT",
227
- /**
228
- * A referenced schema or operation is marked `deprecated`. Reserved for adapters
229
- * to emit as an `info`.
230
- */
231
- deprecated: "KUBB_DEPRECATED",
232
- /**
233
- * An adapter is required but the config has none. The build cannot read the input
234
- * without one.
235
- */
236
- adapterRequired: "KUBB_ADAPTER_REQUIRED",
237
- /**
238
- * A resolved output path escapes the output directory, which can stem from a path
239
- * traversal in the spec or a misconfigured `group.name`.
240
- */
241
- pathTraversal: "KUBB_PATH_TRAVERSAL",
242
- /**
243
- * `output.clean` is enabled but `output.path` resolves to the project root or a parent of it,
244
- * so cleaning would delete kubb.config and every source file.
245
- */
246
- cleanRoot: "KUBB_CLEAN_ROOT",
247
- /**
248
- * A plugin's options are invalid, for example `output.mode: 'file'` paired with a `group` option.
249
- */
250
- invalidPluginOptions: "KUBB_INVALID_PLUGIN_OPTIONS",
251
- /**
252
- * A post-generate command (`output.postGenerate`) exited with a failure.
253
- */
254
- postGenerateFailed: "KUBB_POST_GENERATE_FAILED",
255
- /**
256
- * The formatter pass over the generated files failed.
257
- */
258
- formatFailed: "KUBB_FORMAT_FAILED",
259
- /**
260
- * The linter pass over the generated files failed.
261
- */
262
- lintFailed: "KUBB_LINT_FAILED",
263
- /**
264
- * Not a failure. Carries a plugin's elapsed time, summed into the run total.
265
- */
266
- performance: "KUBB_PERFORMANCE",
267
- /**
268
- * Not a failure. A newer Kubb version is available on npm.
269
- */
270
- updateAvailable: "KUBB_UPDATE_AVAILABLE"
271
- };
272
- //#endregion
273
156
  //#region src/Diagnostics.ts
274
157
  /**
275
158
  * Docs major version, derived from the package version so the link tracks the published major.
276
159
  */
277
- const docsMajor = version.split(".")[0] ?? "5";
160
+ const docsMajor = "5.0.0-beta.109".split(".")[0] ?? "5";
278
161
  /**
279
162
  * Builds a type guard that narrows a {@link Diagnostic} to the variant for `kind`. A diagnostic
280
163
  * with no `kind` is treated as a `problem`.
@@ -327,112 +210,122 @@ const severityStyle = {
327
210
  * and `Diagnostics.docsUrl` for the matching kubb.dev page.
328
211
  */
329
212
  const diagnosticCatalog = {
330
- [diagnosticCode.unknown]: {
213
+ [require_usingCtx.diagnosticCode.unknown]: {
331
214
  title: "Unknown error",
332
215
  cause: "An error was thrown without a stable Kubb code, so it is reported as-is.",
333
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."
334
217
  },
335
- [diagnosticCode.inputNotFound]: {
218
+ [require_usingCtx.diagnosticCode.inputNotFound]: {
336
219
  title: "Input not found",
337
- cause: "The file or URL set as `input` (or passed as `kubb generate PATH`) could not be read.",
338
- 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."
222
+ },
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`."
339
232
  },
340
- [diagnosticCode.inputRequired]: {
233
+ [require_usingCtx.diagnosticCode.inputRequired]: {
341
234
  title: "Input required",
342
235
  cause: "An adapter is configured but no `input` was provided.",
343
236
  fix: "Set `input` to a file path, a URL, an inline spec (JSON/YAML string), or a parsed object in your Kubb config."
344
237
  },
345
- [diagnosticCode.legacyInput]: {
238
+ [require_usingCtx.diagnosticCode.legacyInput]: {
346
239
  title: "Legacy input shape",
347
240
  cause: "`input` is a `{ path }` or `{ data }` wrapper, which v4 used to point at a document and v5 reads as the document itself.",
348
241
  fix: "Unwrap it: `input: { path: \"./petStore.yaml\" }` becomes `input: \"./petStore.yaml\"`, and `input: { data: spec }` becomes `input: spec`."
349
242
  },
350
- [diagnosticCode.invalidDocument]: {
243
+ [require_usingCtx.diagnosticCode.invalidDocument]: {
351
244
  title: "Invalid document",
352
245
  cause: "The parsed `input` has no `openapi` or `swagger` version field, so it is not an OpenAPI or Swagger document.",
353
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."
354
247
  },
355
- [diagnosticCode.refNotFound]: {
248
+ [require_usingCtx.diagnosticCode.refNotFound]: {
356
249
  title: "Reference not found",
357
250
  cause: "A `$ref` could not be resolved in the source document.",
358
251
  fix: "Add the missing definition (for example under `components.schemas`) or fix the `$ref`. Run `kubb validate` to check the spec."
359
252
  },
360
- [diagnosticCode.invalidServerVariable]: {
253
+ [require_usingCtx.diagnosticCode.invalidServerVariable]: {
361
254
  title: "Invalid server variable",
362
255
  cause: "A server variable value is not allowed by its `enum`.",
363
256
  fix: "Use one of the values listed in the server variable `enum`, or update the spec."
364
257
  },
365
- [diagnosticCode.pluginNotFound]: {
258
+ [require_usingCtx.diagnosticCode.pluginNotFound]: {
366
259
  title: "Plugin not found",
367
260
  cause: "A plugin that another plugin depends on is missing from the config.",
368
261
  fix: "Add the required plugin to the `plugins` array in kubb.config.ts, or remove the dependency on it."
369
262
  },
370
- [diagnosticCode.pluginFailed]: {
263
+ [require_usingCtx.diagnosticCode.pluginFailed]: {
371
264
  title: "Plugin failed",
372
265
  cause: "A plugin threw while generating, or reported an error through `ctx.error`.",
373
266
  fix: "Read the underlying error and check the plugin options and the schema or operation it failed on."
374
267
  },
375
- [diagnosticCode.pluginWarning]: {
268
+ [require_usingCtx.diagnosticCode.pluginWarning]: {
376
269
  title: "Plugin warning",
377
270
  cause: "A plugin reported a non-fatal warning through `ctx.warn`.",
378
271
  fix: "Review the message. It does not fail the build; adjust the plugin options or input if the warning is unwanted."
379
272
  },
380
- [diagnosticCode.pluginInfo]: {
273
+ [require_usingCtx.diagnosticCode.pluginInfo]: {
381
274
  title: "Plugin info",
382
275
  cause: "A plugin reported an informational message through `ctx.info`.",
383
276
  fix: "Informational only. No action is required."
384
277
  },
385
- [diagnosticCode.unsupportedFormat]: {
278
+ [require_usingCtx.diagnosticCode.unsupportedFormat]: {
386
279
  title: "Unsupported format",
387
280
  cause: "A schema uses a `format` Kubb does not map to a specific type, so it falls back to the base type.",
388
281
  fix: "Use a format Kubb supports, or handle the custom format with a parser or plugin."
389
282
  },
390
- [diagnosticCode.deprecated]: {
283
+ [require_usingCtx.diagnosticCode.deprecated]: {
391
284
  title: "Deprecated",
392
285
  cause: "A referenced schema or operation is marked `deprecated`.",
393
286
  fix: "Migrate off the deprecated definition if the warning is unwanted."
394
287
  },
395
- [diagnosticCode.adapterRequired]: {
288
+ [require_usingCtx.diagnosticCode.adapterRequired]: {
396
289
  title: "Adapter required",
397
290
  cause: "An action needs an adapter but none is configured.",
398
291
  fix: "Set `adapter` in kubb.config.ts, for example `adapterOas()`."
399
292
  },
400
- [diagnosticCode.pathTraversal]: {
293
+ [require_usingCtx.diagnosticCode.pathTraversal]: {
401
294
  title: "Path traversal",
402
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`.",
403
296
  fix: "Keep generated paths within the output directory. Review the `group.name` function and the names coming from the spec."
404
297
  },
405
- [diagnosticCode.cleanRoot]: {
298
+ [require_usingCtx.diagnosticCode.cleanRoot]: {
406
299
  title: "Clean targets the project root",
407
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.",
408
301
  fix: "Point `output.path` at a subdirectory such as `./src/gen` so clean only removes generated code, or disable `output.clean`."
409
302
  },
410
- [diagnosticCode.invalidPluginOptions]: {
303
+ [require_usingCtx.diagnosticCode.invalidPluginOptions]: {
411
304
  title: "Invalid plugin options",
412
305
  cause: "A plugin was configured with options that cannot be honored, for example `output.mode: 'file'` paired with a `group` option.",
413
306
  fix: "Fix the plugin options. A single-file output has nothing to group, so remove the `group` option or use `output.mode: 'directory'`."
414
307
  },
415
- [diagnosticCode.postGenerateFailed]: {
308
+ [require_usingCtx.diagnosticCode.postGenerateFailed]: {
416
309
  title: "Post-generate command failed",
417
310
  cause: "A post-generate command (`output.postGenerate`) exited with a non-zero status.",
418
311
  fix: "Check the command is installed and correct, and run it manually to see the error."
419
312
  },
420
- [diagnosticCode.formatFailed]: {
313
+ [require_usingCtx.diagnosticCode.formatFailed]: {
421
314
  title: "Format failed",
422
315
  cause: "The formatter pass over the generated files failed.",
423
316
  fix: "Check the formatter (oxfmt, biome, or prettier) is installed and its config is valid, then run it manually on the output."
424
317
  },
425
- [diagnosticCode.lintFailed]: {
318
+ [require_usingCtx.diagnosticCode.lintFailed]: {
426
319
  title: "Lint failed",
427
320
  cause: "The linter pass over the generated files failed.",
428
321
  fix: "Check the linter (oxlint, biome, or eslint) is installed and its config is valid, then run it manually on the output."
429
322
  },
430
- [diagnosticCode.performance]: {
323
+ [require_usingCtx.diagnosticCode.performance]: {
431
324
  title: "Performance",
432
325
  cause: "Not a failure. Records a plugin’s elapsed time, summed into the run total.",
433
326
  fix: "No action. This is an informational metric."
434
327
  },
435
- [diagnosticCode.updateAvailable]: {
328
+ [require_usingCtx.diagnosticCode.updateAvailable]: {
436
329
  title: "Update available",
437
330
  cause: "A newer Kubb version is published on npm than the one running.",
438
331
  fix: "Update the `@kubb/*` packages, for example `npm install -g @kubb/cli`, to get the latest fixes."
@@ -452,7 +345,7 @@ var Diagnostics = class Diagnostics {
452
345
  /**
453
346
  * The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
454
347
  */
455
- static code = diagnosticCode;
348
+ static code = require_usingCtx.diagnosticCode;
456
349
  /**
457
350
  * Type guard for a build {@link ProblemDiagnostic}.
458
351
  */
@@ -533,7 +426,7 @@ var Diagnostics = class Diagnostics {
533
426
  current = current.cause;
534
427
  }
535
428
  return {
536
- code: diagnosticCode.unknown,
429
+ code: require_usingCtx.diagnosticCode.unknown,
537
430
  severity: "error",
538
431
  message: root ? root.message : require_usingCtx.getErrorMessage(error),
539
432
  cause: root
@@ -545,7 +438,7 @@ var Diagnostics = class Diagnostics {
545
438
  static performance({ plugin, duration }) {
546
439
  return {
547
440
  kind: "performance",
548
- code: diagnosticCode.performance,
441
+ code: require_usingCtx.diagnosticCode.performance,
549
442
  severity: "info",
550
443
  message: `${plugin} generated in ${Math.round(duration)}ms`,
551
444
  plugin,
@@ -558,7 +451,7 @@ var Diagnostics = class Diagnostics {
558
451
  static update({ currentVersion, latestVersion }) {
559
452
  return {
560
453
  kind: "update",
561
- code: diagnosticCode.updateAvailable,
454
+ code: require_usingCtx.diagnosticCode.updateAvailable,
562
455
  severity: "info",
563
456
  message: `Update available: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`,
564
457
  currentVersion,
@@ -651,7 +544,7 @@ var Diagnostics = class Diagnostics {
651
544
  ...problem?.location ? { location: problem.location } : {},
652
545
  ...problem?.help ? { help: problem.help } : {},
653
546
  ...problem?.plugin ? { plugin: problem.plugin } : {},
654
- ...diagnostic.code === diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
547
+ ...diagnostic.code === require_usingCtx.diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
655
548
  };
656
549
  }
657
550
  /**
@@ -671,7 +564,7 @@ var Diagnostics = class Diagnostics {
671
564
  const details = [];
672
565
  if (problem?.location && "pointer" in problem.location) details.push(` ${(0, node_util.styleText)("dim", "at:")} ${(0, node_util.styleText)("cyan", problem.location.pointer)}`);
673
566
  if (problem?.help) details.push(` ${(0, node_util.styleText)("cyan", "fix:")} ${problem.help}`);
674
- 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))}`);
675
568
  return {
676
569
  headline,
677
570
  details
@@ -696,7 +589,7 @@ var Diagnostics = class Diagnostics {
696
589
  function normalizeOutput({ output, group, pluginName }) {
697
590
  const mode = output.mode ?? "file";
698
591
  if (mode === "file" && group) throw new Diagnostics.Error({
699
- code: diagnosticCode.invalidPluginOptions,
592
+ code: require_usingCtx.diagnosticCode.invalidPluginOptions,
700
593
  severity: "error",
701
594
  message: `Plugin "${pluginName}" sets \`output.mode: 'file'\` but also configures a \`group\` option.`,
702
595
  help: "A single-file output has nothing to group. Remove the `group` option, or use `output.mode: 'directory'` to organize files into subdirectories.",
@@ -1513,22 +1406,25 @@ var KubbDriver = class {
1513
1406
  async run() {
1514
1407
  const { hooks, config, fileManager } = this;
1515
1408
  const diagnostics = [];
1516
- const updateBuffer = [];
1517
1409
  const parsersMap = /* @__PURE__ */ new Map();
1518
1410
  for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
1411
+ const updateBuffer = [];
1519
1412
  const unhookWrites = fileManager.hooks.addHooks({
1520
1413
  start: async (files) => {
1521
1414
  await hooks.callHook("kubb:files:processing:start", { files });
1522
1415
  },
1523
- update: (item) => {
1524
- updateBuffer.push(item);
1416
+ update: ({ file, processed, total, percentage }) => {
1417
+ updateBuffer.push({
1418
+ file,
1419
+ processed,
1420
+ total,
1421
+ percentage,
1422
+ config
1423
+ });
1525
1424
  },
1526
1425
  end: async (files) => {
1527
1426
  updateBuffer.sort((a, b) => a.processed - b.processed);
1528
- await hooks.callHook("kubb:files:processing:update", { files: updateBuffer.map((item) => ({
1529
- ...item,
1530
- config
1531
- })) });
1427
+ await hooks.callHook("kubb:files:processing:update", { files: updateBuffer });
1532
1428
  updateBuffer.length = 0;
1533
1429
  await hooks.callHook("kubb:files:processing:end", { files });
1534
1430
  }
@@ -1595,7 +1491,8 @@ var KubbDriver = class {
1595
1491
  await hooks.callHook("kubb:plugins:end", this.#withFiles({ config }));
1596
1492
  await fileManager.write(fileManager.files, {
1597
1493
  storage: config.storage,
1598
- parsers: parsersMap
1494
+ parsers: parsersMap,
1495
+ manifest: this.options.manifest
1599
1496
  });
1600
1497
  await hooks.callHook("kubb:build:end", {
1601
1498
  files: this.fileManager.files,
@@ -1674,7 +1571,7 @@ var KubbDriver = class {
1674
1571
  const allowedSchemaNamesByPlugin = /* @__PURE__ */ new Map();
1675
1572
  for (const { plugin } of entries) {
1676
1573
  const { exclude, include, override } = plugin.options;
1677
- if (!((include?.some(({ type }) => OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false))) continue;
1574
+ if (!((include?.some(({ type }) => require_usingCtx.OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false))) continue;
1678
1575
  const resolver = this.getResolver(plugin.name);
1679
1576
  const includedOps = operations.filter((operation) => resolver.default.options(operation, {
1680
1577
  options: plugin.options,
@@ -1956,6 +1853,72 @@ var KubbDriver = class {
1956
1853
  }
1957
1854
  };
1958
1855
  //#endregion
1856
+ //#region src/outputManifest.ts
1857
+ /**
1858
+ * Bumped when the stored shape changes, so an older cache is discarded instead of misread.
1859
+ */
1860
+ const VERSION = 1;
1861
+ function hash(value) {
1862
+ return (0, node_crypto.createHash)("sha256").update(value).digest("hex");
1863
+ }
1864
+ const MANIFEST_KEY = "output-manifest.json";
1865
+ async function loadEntries({ cache }) {
1866
+ try {
1867
+ const stored = await cache.readItem(MANIFEST_KEY);
1868
+ if (stored === null) return {};
1869
+ const data = JSON.parse(stored);
1870
+ if (data.version !== VERSION) return {};
1871
+ if (typeof data.entries !== "object" || data.entries === null || Array.isArray(data.entries)) return {};
1872
+ return data.entries;
1873
+ } catch {
1874
+ return {};
1875
+ }
1876
+ }
1877
+ /**
1878
+ * Loads the stored manifest, starting empty when it is missing, unreadable, or from an older
1879
+ * version. `storage` holds the generated files, `cache` holds the manifest itself.
1880
+ *
1881
+ * @example
1882
+ * ```ts
1883
+ * const manifest = await createOutputManifest({ storage: config.storage, cache: cacheStorage({ root: config.root }) })
1884
+ * ```
1885
+ */
1886
+ async function createOutputManifest({ storage, cache }) {
1887
+ const entries = await loadEntries({ cache });
1888
+ const tracked = /* @__PURE__ */ new Map();
1889
+ return {
1890
+ isUpToDate({ key, source, disk }) {
1891
+ const entry = entries[key];
1892
+ if (!entry) return false;
1893
+ return entry.source === hash(source) && entry.output === hash(disk);
1894
+ },
1895
+ track({ key, source }) {
1896
+ tracked.set(key, hash(source));
1897
+ },
1898
+ async commit() {
1899
+ try {
1900
+ const next = { ...entries };
1901
+ await require_usingCtx.inParallel({
1902
+ items: [...tracked],
1903
+ limit: 50,
1904
+ run: async ([key, source]) => {
1905
+ const stored = await storage.readItem(key);
1906
+ if (stored === null) return;
1907
+ next[key] = {
1908
+ source,
1909
+ output: hash(stored)
1910
+ };
1911
+ }
1912
+ });
1913
+ await cache.writeItem(MANIFEST_KEY, JSON.stringify({
1914
+ version: VERSION,
1915
+ entries: next
1916
+ }));
1917
+ } catch {}
1918
+ }
1919
+ };
1920
+ }
1921
+ //#endregion
1959
1922
  //#region src/createStorage.ts
1960
1923
  /**
1961
1924
  * Defines a custom storage backend. The builder receives user options and
@@ -2032,7 +1995,8 @@ function createLimiter(concurrency) {
2032
1995
  *
2033
1996
  * Writes are deduplicated and directory-safe:
2034
1997
  * - leading and trailing whitespace is trimmed before writing
2035
- * - the write is skipped when the file content is already identical
1998
+ * - the write is skipped when the file already holds that content, ignoring any trailing newline
1999
+ * a formatter left behind
2036
2000
  * - missing parent directories are created automatically
2037
2001
  * - Bun's native file API is used when running under Bun
2038
2002
  * - concurrent `writeItem` calls are capped at {@link WRITE_CONCURRENCY} in flight, so a caller
@@ -2093,6 +2057,60 @@ const fsStorage = createStorage(() => {
2093
2057
  };
2094
2058
  });
2095
2059
  //#endregion
2060
+ //#region src/storages/cacheStorage.ts
2061
+ /**
2062
+ * Directory Kubb keeps build caches in. A project with a `node_modules` gets
2063
+ * `node_modules/.cache/kubb`, the convention babel and eslint already use, so the cache stays out
2064
+ * of version control. Without one it falls back to the OS temp directory, keyed by root so two
2065
+ * projects sharing that directory keep their own cache.
2066
+ *
2067
+ * @example Inside a project
2068
+ * `resolveCacheDir('/project') // '/project/node_modules/.cache/kubb'`
2069
+ */
2070
+ function resolveCacheDir(root) {
2071
+ const nodeModules = (0, node_path.join)(root, "node_modules");
2072
+ if ((0, node_fs.existsSync)(nodeModules)) return (0, node_path.join)(nodeModules, ".cache", "kubb");
2073
+ return (0, node_path.join)((0, node_os.tmpdir)(), "kubb", (0, node_crypto.createHash)("sha256").update(root).digest("hex").slice(0, 16));
2074
+ }
2075
+ /**
2076
+ * Filesystem storage for build caches rather than generated code. Keys are plain names resolved
2077
+ * inside {@link resolveCacheDir}, so a caller stores `'x.json'` without knowing where the cache
2078
+ * lives. Kept apart from the configured output storage, which may not be a local disk at all.
2079
+ *
2080
+ * @example
2081
+ * ```ts
2082
+ * const cache = cacheStorage({ root: config.root })
2083
+ * await cache.writeItem('output-manifest.json', JSON.stringify(entries))
2084
+ * ```
2085
+ */
2086
+ const cacheStorage = createStorage(({ root = process.cwd() }) => {
2087
+ const dir = resolveCacheDir(root);
2088
+ const storage = fsStorage();
2089
+ const toPath = (key) => (0, node_path.join)(dir, key);
2090
+ return {
2091
+ name: "cache",
2092
+ async existsItem(key) {
2093
+ return storage.existsItem(toPath(key));
2094
+ },
2095
+ async readItem(key) {
2096
+ return storage.readItem(toPath(key));
2097
+ },
2098
+ async writeItem(key, value) {
2099
+ return storage.writeItem(toPath(key), value);
2100
+ },
2101
+ async removeItem(key) {
2102
+ return storage.removeItem(toPath(key));
2103
+ },
2104
+ async readKeys(base) {
2105
+ return storage.readKeys(base ? toPath(base) : dir);
2106
+ },
2107
+ async empty(base) {
2108
+ if (!base) return;
2109
+ return storage.empty(toPath(base));
2110
+ }
2111
+ };
2112
+ });
2113
+ //#endregion
2096
2114
  //#region src/createKubb.ts
2097
2115
  function resolveConfig(userConfig) {
2098
2116
  return {
@@ -2111,6 +2129,13 @@ function resolveConfig(userConfig) {
2111
2129
  };
2112
2130
  }
2113
2131
  /**
2132
+ * Whether anything runs over the output directory after the files are written. Only then can the
2133
+ * bytes on disk stop matching what Kubb wrote, which is what the manifest exists to track.
2134
+ */
2135
+ function hasOutputPasses(output) {
2136
+ return Boolean(output.format || output.lint || output.postGenerate?.length);
2137
+ }
2138
+ /**
2114
2139
  * Kubb code-generation instance bound to a single config entry. Resolves the user
2115
2140
  * config in the constructor, so `config` is available right away, and shares `hooks`,
2116
2141
  * `storage`, and `driver` across the `setup → build` lifecycle.
@@ -2132,6 +2157,7 @@ var Kubb = class {
2132
2157
  config;
2133
2158
  #driver = null;
2134
2159
  #storage = null;
2160
+ #manifest = null;
2135
2161
  constructor(userConfig, options = {}) {
2136
2162
  this.config = resolveConfig(userConfig);
2137
2163
  this.hooks = options.hooks ?? new require_usingCtx.Hookable();
@@ -2149,7 +2175,14 @@ var Kubb = class {
2149
2175
  */
2150
2176
  async setup() {
2151
2177
  const config = this.config;
2152
- const driver = new KubbDriver(config, { hooks: this.hooks });
2178
+ const manifest = hasOutputPasses(config.output) ? await createOutputManifest({
2179
+ storage: config.storage,
2180
+ cache: cacheStorage({ root: config.root })
2181
+ }) : void 0;
2182
+ const driver = new KubbDriver(config, {
2183
+ hooks: this.hooks,
2184
+ manifest
2185
+ });
2153
2186
  this.hooks.setMaxListeners(Math.max(10, config.plugins.length * 4));
2154
2187
  if (config.output.clean) {
2155
2188
  const cleanPath = (0, node_path.resolve)(config.root, config.output.path);
@@ -2165,6 +2198,7 @@ var Kubb = class {
2165
2198
  await driver.setup();
2166
2199
  this.#driver = driver;
2167
2200
  this.#storage = config.storage;
2201
+ this.#manifest = manifest ?? null;
2168
2202
  }
2169
2203
  /**
2170
2204
  * Runs the full pipeline and throws on any plugin error.
@@ -2252,6 +2286,7 @@ var Kubb = class {
2252
2286
  }) : [];
2253
2287
  const finalDiagnostics = [...diagnostics, ...outputDiagnostics];
2254
2288
  const failed = Diagnostics.hasError(outputDiagnostics);
2289
+ if (!failed) await this.#manifest?.commit();
2255
2290
  await hooks.callHook("kubb:generation:end", {
2256
2291
  config,
2257
2292
  storage,