@kubb/core 5.0.0-beta.108 → 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,140 +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.108";
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
- * A URL set as `input` (or referenced by a `$ref`) answered with a 4xx or 5xx status
186
- * instead of the document.
187
- */
188
- inputRequestFailed: "KUBB_INPUT_REQUEST_FAILED",
189
- /**
190
- * A URL set as `input` (or referenced by a `$ref`) never answered, so the request failed
191
- * before a status was returned.
192
- */
193
- inputUnreachable: "KUBB_INPUT_UNREACHABLE",
194
- /**
195
- * An adapter was configured without an `input`.
196
- */
197
- inputRequired: "KUBB_INPUT_REQUIRED",
198
- /**
199
- * `input` uses the v4 `{ path }` / `{ data }` wrapper, which v5 reads as a parsed
200
- * document instead of a pointer to one.
201
- */
202
- legacyInput: "KUBB_LEGACY_INPUT",
203
- /**
204
- * The parsed `input` carries no `openapi` or `swagger` version, so it is not a
205
- * document the adapter can read.
206
- */
207
- invalidDocument: "KUBB_INVALID_DOCUMENT",
208
- /**
209
- * A `$ref` (or equivalent reference) could not be resolved in the source document.
210
- */
211
- refNotFound: "KUBB_REF_NOT_FOUND",
212
- /**
213
- * A server variable value is not allowed by its `enum`.
214
- */
215
- invalidServerVariable: "KUBB_INVALID_SERVER_VARIABLE",
216
- /**
217
- * A required plugin is missing from the config.
218
- */
219
- pluginNotFound: "KUBB_PLUGIN_NOT_FOUND",
220
- /**
221
- * A plugin threw while generating.
222
- */
223
- pluginFailed: "KUBB_PLUGIN_FAILED",
224
- /**
225
- * A plugin reported a non-fatal warning through `ctx.warn`.
226
- */
227
- pluginWarning: "KUBB_PLUGIN_WARNING",
228
- /**
229
- * A plugin reported an informational message through `ctx.info`.
230
- */
231
- pluginInfo: "KUBB_PLUGIN_INFO",
232
- /**
233
- * A schema uses a `format` Kubb does not map to a specific type. Reserved for
234
- * adapters to emit as a `warning`.
235
- */
236
- unsupportedFormat: "KUBB_UNSUPPORTED_FORMAT",
237
- /**
238
- * A referenced schema or operation is marked `deprecated`. Reserved for adapters
239
- * to emit as an `info`.
240
- */
241
- deprecated: "KUBB_DEPRECATED",
242
- /**
243
- * An adapter is required but the config has none. The build cannot read the input
244
- * without one.
245
- */
246
- adapterRequired: "KUBB_ADAPTER_REQUIRED",
247
- /**
248
- * A resolved output path escapes the output directory, which can stem from a path
249
- * traversal in the spec or a misconfigured `group.name`.
250
- */
251
- pathTraversal: "KUBB_PATH_TRAVERSAL",
252
- /**
253
- * `output.clean` is enabled but `output.path` resolves to the project root or a parent of it,
254
- * so cleaning would delete kubb.config and every source file.
255
- */
256
- cleanRoot: "KUBB_CLEAN_ROOT",
257
- /**
258
- * A plugin's options are invalid, for example `output.mode: 'file'` paired with a `group` option.
259
- */
260
- invalidPluginOptions: "KUBB_INVALID_PLUGIN_OPTIONS",
261
- /**
262
- * A post-generate command (`output.postGenerate`) exited with a failure.
263
- */
264
- postGenerateFailed: "KUBB_POST_GENERATE_FAILED",
265
- /**
266
- * The formatter pass over the generated files failed.
267
- */
268
- formatFailed: "KUBB_FORMAT_FAILED",
269
- /**
270
- * The linter pass over the generated files failed.
271
- */
272
- lintFailed: "KUBB_LINT_FAILED",
273
- /**
274
- * Not a failure. Carries a plugin's elapsed time, summed into the run total.
275
- */
276
- performance: "KUBB_PERFORMANCE",
277
- /**
278
- * Not a failure. A newer Kubb version is available on npm.
279
- */
280
- updateAvailable: "KUBB_UPDATE_AVAILABLE"
281
- };
282
- //#endregion
283
156
  //#region src/Diagnostics.ts
284
157
  /**
285
158
  * Docs major version, derived from the package version so the link tracks the published major.
286
159
  */
287
- const docsMajor = version.split(".")[0] ?? "5";
160
+ const docsMajor = "5.0.0-beta.109".split(".")[0] ?? "5";
288
161
  /**
289
162
  * Builds a type guard that narrows a {@link Diagnostic} to the variant for `kind`. A diagnostic
290
163
  * with no `kind` is treated as a `problem`.
@@ -337,122 +210,122 @@ const severityStyle = {
337
210
  * and `Diagnostics.docsUrl` for the matching kubb.dev page.
338
211
  */
339
212
  const diagnosticCatalog = {
340
- [diagnosticCode.unknown]: {
213
+ [require_usingCtx.diagnosticCode.unknown]: {
341
214
  title: "Unknown error",
342
215
  cause: "An error was thrown without a stable Kubb code, so it is reported as-is.",
343
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."
344
217
  },
345
- [diagnosticCode.inputNotFound]: {
218
+ [require_usingCtx.diagnosticCode.inputNotFound]: {
346
219
  title: "Input not found",
347
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.",
348
221
  fix: "Check that the path exists and is readable, then set it as `input` or pass it on the CLI."
349
222
  },
350
- [diagnosticCode.inputRequestFailed]: {
223
+ [require_usingCtx.diagnosticCode.inputRequestFailed]: {
351
224
  title: "Input request failed",
352
225
  cause: "A URL set as `input` (or reached through a `$ref`) answered with a 4xx or 5xx status instead of the document.",
353
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."
354
227
  },
355
- [diagnosticCode.inputUnreachable]: {
228
+ [require_usingCtx.diagnosticCode.inputUnreachable]: {
356
229
  title: "Input unreachable",
357
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.",
358
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`."
359
232
  },
360
- [diagnosticCode.inputRequired]: {
233
+ [require_usingCtx.diagnosticCode.inputRequired]: {
361
234
  title: "Input required",
362
235
  cause: "An adapter is configured but no `input` was provided.",
363
236
  fix: "Set `input` to a file path, a URL, an inline spec (JSON/YAML string), or a parsed object in your Kubb config."
364
237
  },
365
- [diagnosticCode.legacyInput]: {
238
+ [require_usingCtx.diagnosticCode.legacyInput]: {
366
239
  title: "Legacy input shape",
367
240
  cause: "`input` is a `{ path }` or `{ data }` wrapper, which v4 used to point at a document and v5 reads as the document itself.",
368
241
  fix: "Unwrap it: `input: { path: \"./petStore.yaml\" }` becomes `input: \"./petStore.yaml\"`, and `input: { data: spec }` becomes `input: spec`."
369
242
  },
370
- [diagnosticCode.invalidDocument]: {
243
+ [require_usingCtx.diagnosticCode.invalidDocument]: {
371
244
  title: "Invalid document",
372
245
  cause: "The parsed `input` has no `openapi` or `swagger` version field, so it is not an OpenAPI or Swagger document.",
373
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."
374
247
  },
375
- [diagnosticCode.refNotFound]: {
248
+ [require_usingCtx.diagnosticCode.refNotFound]: {
376
249
  title: "Reference not found",
377
250
  cause: "A `$ref` could not be resolved in the source document.",
378
251
  fix: "Add the missing definition (for example under `components.schemas`) or fix the `$ref`. Run `kubb validate` to check the spec."
379
252
  },
380
- [diagnosticCode.invalidServerVariable]: {
253
+ [require_usingCtx.diagnosticCode.invalidServerVariable]: {
381
254
  title: "Invalid server variable",
382
255
  cause: "A server variable value is not allowed by its `enum`.",
383
256
  fix: "Use one of the values listed in the server variable `enum`, or update the spec."
384
257
  },
385
- [diagnosticCode.pluginNotFound]: {
258
+ [require_usingCtx.diagnosticCode.pluginNotFound]: {
386
259
  title: "Plugin not found",
387
260
  cause: "A plugin that another plugin depends on is missing from the config.",
388
261
  fix: "Add the required plugin to the `plugins` array in kubb.config.ts, or remove the dependency on it."
389
262
  },
390
- [diagnosticCode.pluginFailed]: {
263
+ [require_usingCtx.diagnosticCode.pluginFailed]: {
391
264
  title: "Plugin failed",
392
265
  cause: "A plugin threw while generating, or reported an error through `ctx.error`.",
393
266
  fix: "Read the underlying error and check the plugin options and the schema or operation it failed on."
394
267
  },
395
- [diagnosticCode.pluginWarning]: {
268
+ [require_usingCtx.diagnosticCode.pluginWarning]: {
396
269
  title: "Plugin warning",
397
270
  cause: "A plugin reported a non-fatal warning through `ctx.warn`.",
398
271
  fix: "Review the message. It does not fail the build; adjust the plugin options or input if the warning is unwanted."
399
272
  },
400
- [diagnosticCode.pluginInfo]: {
273
+ [require_usingCtx.diagnosticCode.pluginInfo]: {
401
274
  title: "Plugin info",
402
275
  cause: "A plugin reported an informational message through `ctx.info`.",
403
276
  fix: "Informational only. No action is required."
404
277
  },
405
- [diagnosticCode.unsupportedFormat]: {
278
+ [require_usingCtx.diagnosticCode.unsupportedFormat]: {
406
279
  title: "Unsupported format",
407
280
  cause: "A schema uses a `format` Kubb does not map to a specific type, so it falls back to the base type.",
408
281
  fix: "Use a format Kubb supports, or handle the custom format with a parser or plugin."
409
282
  },
410
- [diagnosticCode.deprecated]: {
283
+ [require_usingCtx.diagnosticCode.deprecated]: {
411
284
  title: "Deprecated",
412
285
  cause: "A referenced schema or operation is marked `deprecated`.",
413
286
  fix: "Migrate off the deprecated definition if the warning is unwanted."
414
287
  },
415
- [diagnosticCode.adapterRequired]: {
288
+ [require_usingCtx.diagnosticCode.adapterRequired]: {
416
289
  title: "Adapter required",
417
290
  cause: "An action needs an adapter but none is configured.",
418
291
  fix: "Set `adapter` in kubb.config.ts, for example `adapterOas()`."
419
292
  },
420
- [diagnosticCode.pathTraversal]: {
293
+ [require_usingCtx.diagnosticCode.pathTraversal]: {
421
294
  title: "Path traversal",
422
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`.",
423
296
  fix: "Keep generated paths within the output directory. Review the `group.name` function and the names coming from the spec."
424
297
  },
425
- [diagnosticCode.cleanRoot]: {
298
+ [require_usingCtx.diagnosticCode.cleanRoot]: {
426
299
  title: "Clean targets the project root",
427
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.",
428
301
  fix: "Point `output.path` at a subdirectory such as `./src/gen` so clean only removes generated code, or disable `output.clean`."
429
302
  },
430
- [diagnosticCode.invalidPluginOptions]: {
303
+ [require_usingCtx.diagnosticCode.invalidPluginOptions]: {
431
304
  title: "Invalid plugin options",
432
305
  cause: "A plugin was configured with options that cannot be honored, for example `output.mode: 'file'` paired with a `group` option.",
433
306
  fix: "Fix the plugin options. A single-file output has nothing to group, so remove the `group` option or use `output.mode: 'directory'`."
434
307
  },
435
- [diagnosticCode.postGenerateFailed]: {
308
+ [require_usingCtx.diagnosticCode.postGenerateFailed]: {
436
309
  title: "Post-generate command failed",
437
310
  cause: "A post-generate command (`output.postGenerate`) exited with a non-zero status.",
438
311
  fix: "Check the command is installed and correct, and run it manually to see the error."
439
312
  },
440
- [diagnosticCode.formatFailed]: {
313
+ [require_usingCtx.diagnosticCode.formatFailed]: {
441
314
  title: "Format failed",
442
315
  cause: "The formatter pass over the generated files failed.",
443
316
  fix: "Check the formatter (oxfmt, biome, or prettier) is installed and its config is valid, then run it manually on the output."
444
317
  },
445
- [diagnosticCode.lintFailed]: {
318
+ [require_usingCtx.diagnosticCode.lintFailed]: {
446
319
  title: "Lint failed",
447
320
  cause: "The linter pass over the generated files failed.",
448
321
  fix: "Check the linter (oxlint, biome, or eslint) is installed and its config is valid, then run it manually on the output."
449
322
  },
450
- [diagnosticCode.performance]: {
323
+ [require_usingCtx.diagnosticCode.performance]: {
451
324
  title: "Performance",
452
325
  cause: "Not a failure. Records a plugin’s elapsed time, summed into the run total.",
453
326
  fix: "No action. This is an informational metric."
454
327
  },
455
- [diagnosticCode.updateAvailable]: {
328
+ [require_usingCtx.diagnosticCode.updateAvailable]: {
456
329
  title: "Update available",
457
330
  cause: "A newer Kubb version is published on npm than the one running.",
458
331
  fix: "Update the `@kubb/*` packages, for example `npm install -g @kubb/cli`, to get the latest fixes."
@@ -472,7 +345,7 @@ var Diagnostics = class Diagnostics {
472
345
  /**
473
346
  * The diagnostic code catalog, exposed as `Diagnostics.code` (e.g. `Diagnostics.code.refNotFound`).
474
347
  */
475
- static code = diagnosticCode;
348
+ static code = require_usingCtx.diagnosticCode;
476
349
  /**
477
350
  * Type guard for a build {@link ProblemDiagnostic}.
478
351
  */
@@ -553,7 +426,7 @@ var Diagnostics = class Diagnostics {
553
426
  current = current.cause;
554
427
  }
555
428
  return {
556
- code: diagnosticCode.unknown,
429
+ code: require_usingCtx.diagnosticCode.unknown,
557
430
  severity: "error",
558
431
  message: root ? root.message : require_usingCtx.getErrorMessage(error),
559
432
  cause: root
@@ -565,7 +438,7 @@ var Diagnostics = class Diagnostics {
565
438
  static performance({ plugin, duration }) {
566
439
  return {
567
440
  kind: "performance",
568
- code: diagnosticCode.performance,
441
+ code: require_usingCtx.diagnosticCode.performance,
569
442
  severity: "info",
570
443
  message: `${plugin} generated in ${Math.round(duration)}ms`,
571
444
  plugin,
@@ -578,7 +451,7 @@ var Diagnostics = class Diagnostics {
578
451
  static update({ currentVersion, latestVersion }) {
579
452
  return {
580
453
  kind: "update",
581
- code: diagnosticCode.updateAvailable,
454
+ code: require_usingCtx.diagnosticCode.updateAvailable,
582
455
  severity: "info",
583
456
  message: `Update available: v${currentVersion} → v${latestVersion}. Run \`npm install -g @kubb/cli\` to update.`,
584
457
  currentVersion,
@@ -671,7 +544,7 @@ var Diagnostics = class Diagnostics {
671
544
  ...problem?.location ? { location: problem.location } : {},
672
545
  ...problem?.help ? { help: problem.help } : {},
673
546
  ...problem?.plugin ? { plugin: problem.plugin } : {},
674
- ...diagnostic.code === diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
547
+ ...diagnostic.code === require_usingCtx.diagnosticCode.unknown ? {} : { docsUrl: Diagnostics.docsUrl(diagnostic.code) }
675
548
  };
676
549
  }
677
550
  /**
@@ -691,7 +564,7 @@ var Diagnostics = class Diagnostics {
691
564
  const details = [];
692
565
  if (problem?.location && "pointer" in problem.location) details.push(` ${(0, node_util.styleText)("dim", "at:")} ${(0, node_util.styleText)("cyan", problem.location.pointer)}`);
693
566
  if (problem?.help) details.push(` ${(0, node_util.styleText)("cyan", "fix:")} ${problem.help}`);
694
- 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))}`);
695
568
  return {
696
569
  headline,
697
570
  details
@@ -716,7 +589,7 @@ var Diagnostics = class Diagnostics {
716
589
  function normalizeOutput({ output, group, pluginName }) {
717
590
  const mode = output.mode ?? "file";
718
591
  if (mode === "file" && group) throw new Diagnostics.Error({
719
- code: diagnosticCode.invalidPluginOptions,
592
+ code: require_usingCtx.diagnosticCode.invalidPluginOptions,
720
593
  severity: "error",
721
594
  message: `Plugin "${pluginName}" sets \`output.mode: 'file'\` but also configures a \`group\` option.`,
722
595
  help: "A single-file output has nothing to group. Remove the `group` option, or use `output.mode: 'directory'` to organize files into subdirectories.",
@@ -1533,22 +1406,25 @@ var KubbDriver = class {
1533
1406
  async run() {
1534
1407
  const { hooks, config, fileManager } = this;
1535
1408
  const diagnostics = [];
1536
- const updateBuffer = [];
1537
1409
  const parsersMap = /* @__PURE__ */ new Map();
1538
1410
  for (const parser of config.parsers) if (parser.extNames) for (const ext of parser.extNames) parsersMap.set(ext, parser);
1411
+ const updateBuffer = [];
1539
1412
  const unhookWrites = fileManager.hooks.addHooks({
1540
1413
  start: async (files) => {
1541
1414
  await hooks.callHook("kubb:files:processing:start", { files });
1542
1415
  },
1543
- update: (item) => {
1544
- updateBuffer.push(item);
1416
+ update: ({ file, processed, total, percentage }) => {
1417
+ updateBuffer.push({
1418
+ file,
1419
+ processed,
1420
+ total,
1421
+ percentage,
1422
+ config
1423
+ });
1545
1424
  },
1546
1425
  end: async (files) => {
1547
1426
  updateBuffer.sort((a, b) => a.processed - b.processed);
1548
- await hooks.callHook("kubb:files:processing:update", { files: updateBuffer.map((item) => ({
1549
- ...item,
1550
- config
1551
- })) });
1427
+ await hooks.callHook("kubb:files:processing:update", { files: updateBuffer });
1552
1428
  updateBuffer.length = 0;
1553
1429
  await hooks.callHook("kubb:files:processing:end", { files });
1554
1430
  }
@@ -1615,7 +1491,8 @@ var KubbDriver = class {
1615
1491
  await hooks.callHook("kubb:plugins:end", this.#withFiles({ config }));
1616
1492
  await fileManager.write(fileManager.files, {
1617
1493
  storage: config.storage,
1618
- parsers: parsersMap
1494
+ parsers: parsersMap,
1495
+ manifest: this.options.manifest
1619
1496
  });
1620
1497
  await hooks.callHook("kubb:build:end", {
1621
1498
  files: this.fileManager.files,
@@ -1694,7 +1571,7 @@ var KubbDriver = class {
1694
1571
  const allowedSchemaNamesByPlugin = /* @__PURE__ */ new Map();
1695
1572
  for (const { plugin } of entries) {
1696
1573
  const { exclude, include, override } = plugin.options;
1697
- 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;
1698
1575
  const resolver = this.getResolver(plugin.name);
1699
1576
  const includedOps = operations.filter((operation) => resolver.default.options(operation, {
1700
1577
  options: plugin.options,
@@ -1976,6 +1853,72 @@ var KubbDriver = class {
1976
1853
  }
1977
1854
  };
1978
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
1979
1922
  //#region src/createStorage.ts
1980
1923
  /**
1981
1924
  * Defines a custom storage backend. The builder receives user options and
@@ -2052,7 +1995,8 @@ function createLimiter(concurrency) {
2052
1995
  *
2053
1996
  * Writes are deduplicated and directory-safe:
2054
1997
  * - leading and trailing whitespace is trimmed before writing
2055
- * - 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
2056
2000
  * - missing parent directories are created automatically
2057
2001
  * - Bun's native file API is used when running under Bun
2058
2002
  * - concurrent `writeItem` calls are capped at {@link WRITE_CONCURRENCY} in flight, so a caller
@@ -2113,6 +2057,60 @@ const fsStorage = createStorage(() => {
2113
2057
  };
2114
2058
  });
2115
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
2116
2114
  //#region src/createKubb.ts
2117
2115
  function resolveConfig(userConfig) {
2118
2116
  return {
@@ -2131,6 +2129,13 @@ function resolveConfig(userConfig) {
2131
2129
  };
2132
2130
  }
2133
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
+ /**
2134
2139
  * Kubb code-generation instance bound to a single config entry. Resolves the user
2135
2140
  * config in the constructor, so `config` is available right away, and shares `hooks`,
2136
2141
  * `storage`, and `driver` across the `setup → build` lifecycle.
@@ -2152,6 +2157,7 @@ var Kubb = class {
2152
2157
  config;
2153
2158
  #driver = null;
2154
2159
  #storage = null;
2160
+ #manifest = null;
2155
2161
  constructor(userConfig, options = {}) {
2156
2162
  this.config = resolveConfig(userConfig);
2157
2163
  this.hooks = options.hooks ?? new require_usingCtx.Hookable();
@@ -2169,7 +2175,14 @@ var Kubb = class {
2169
2175
  */
2170
2176
  async setup() {
2171
2177
  const config = this.config;
2172
- 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
+ });
2173
2186
  this.hooks.setMaxListeners(Math.max(10, config.plugins.length * 4));
2174
2187
  if (config.output.clean) {
2175
2188
  const cleanPath = (0, node_path.resolve)(config.root, config.output.path);
@@ -2185,6 +2198,7 @@ var Kubb = class {
2185
2198
  await driver.setup();
2186
2199
  this.#driver = driver;
2187
2200
  this.#storage = config.storage;
2201
+ this.#manifest = manifest ?? null;
2188
2202
  }
2189
2203
  /**
2190
2204
  * Runs the full pipeline and throws on any plugin error.
@@ -2272,6 +2286,7 @@ var Kubb = class {
2272
2286
  }) : [];
2273
2287
  const finalDiagnostics = [...diagnostics, ...outputDiagnostics];
2274
2288
  const failed = Diagnostics.hasError(outputDiagnostics);
2289
+ if (!failed) await this.#manifest?.commit();
2275
2290
  await hooks.callHook("kubb:generation:end", {
2276
2291
  config,
2277
2292
  storage,