@kubb/core 5.0.0-beta.40 → 5.0.0-beta.42

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.
@@ -26,6 +26,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  //#endregion
27
27
  let node_events = require("node:events");
28
28
  let node_util = require("node:util");
29
+ let node_crypto = require("node:crypto");
30
+ let node_fs_promises = require("node:fs/promises");
29
31
  let node_path = require("node:path");
30
32
  node_path = __toESM(node_path, 1);
31
33
  let _kubb_ast = require("@kubb/ast");
@@ -661,7 +663,7 @@ var URLPath = class {
661
663
  };
662
664
  //#endregion
663
665
  //#region package.json
664
- var version = "5.0.0-beta.40";
666
+ var version = "5.0.0-beta.42";
665
667
  //#endregion
666
668
  //#region src/constants.ts
667
669
  /**
@@ -1230,6 +1232,89 @@ function createStorage(build) {
1230
1232
  return (options) => build(options ?? {});
1231
1233
  }
1232
1234
  //#endregion
1235
+ //#region src/Fingerprint.ts
1236
+ /**
1237
+ * Computes the cache key for an incremental build. All methods are static, so call them as
1238
+ * `Fingerprint.compute(...)` and `Fingerprint.stringify(...)`. The key holds no absolute
1239
+ * paths or modification times, so it never depends on where the project lives on disk.
1240
+ */
1241
+ var Fingerprint = class Fingerprint {
1242
+ /**
1243
+ * Bumped when the snapshot format or fingerprint inputs change in an incompatible way, so stale
1244
+ * cache entries from older Kubb builds are never reused.
1245
+ */
1246
+ static version = 1;
1247
+ /**
1248
+ * Deterministically serializes a value to JSON: object keys are sorted recursively and
1249
+ * `undefined` values and functions are dropped. Two structurally equal configs produce the same
1250
+ * string regardless of key order, which keeps the fingerprint stable across machines.
1251
+ */
1252
+ static stringify(value) {
1253
+ return JSON.stringify(Fingerprint.#normalize(value));
1254
+ }
1255
+ /**
1256
+ * Computes a cache key from everything that affects the generated output: the spec content, the
1257
+ * output-shaping config, each plugin's name and options, the middleware names, the running
1258
+ * `@kubb/core` version, and the cache format version. Returns `null` when the input can't be
1259
+ * fingerprinted (remote URL or no adapter source), which disables caching for that build.
1260
+ */
1261
+ static async compute({ config, adapterSource, version }) {
1262
+ if (!adapterSource) return null;
1263
+ const spec = await Fingerprint.#readSpec(adapterSource, config.root);
1264
+ if (spec === null) return null;
1265
+ const input = {
1266
+ cacheVersion: Fingerprint.version,
1267
+ version,
1268
+ spec,
1269
+ name: config.name,
1270
+ output: config.output,
1271
+ adapter: config.adapter?.name,
1272
+ parsers: config.parsers.map((parser) => parser.name),
1273
+ plugins: config.plugins.map((plugin) => ({
1274
+ name: plugin.name,
1275
+ options: plugin.options
1276
+ })),
1277
+ middleware: (config.middleware ?? []).map((middleware) => middleware.name)
1278
+ };
1279
+ return (0, node_crypto.createHash)("sha256").update(Fingerprint.stringify(input)).digest("hex");
1280
+ }
1281
+ static #normalize(value) {
1282
+ if (value === null || typeof value !== "object") return typeof value === "function" ? void 0 : value;
1283
+ if (Array.isArray(value)) return value.map((item) => Fingerprint.#normalize(item));
1284
+ const source = value;
1285
+ const result = {};
1286
+ for (const key of Object.keys(source).sort()) {
1287
+ const normalized = Fingerprint.#normalize(source[key]);
1288
+ if (normalized !== void 0) result[key] = normalized;
1289
+ }
1290
+ return result;
1291
+ }
1292
+ /**
1293
+ * Reads the spec content that feeds the fingerprint. Returns `null` for a remote URL source
1294
+ * (hashing remote content would mean fetching it on every run) or when a file can't be read, so a
1295
+ * missing or virtual spec disables caching instead of failing the build.
1296
+ */
1297
+ static async #readSpec(source, root) {
1298
+ if (source.type === "data") return {
1299
+ kind: "data",
1300
+ data: typeof source.data === "string" ? source.data : Fingerprint.stringify(source.data)
1301
+ };
1302
+ const paths = source.type === "paths" ? source.paths : [source.path];
1303
+ if (paths.some((path) => new URLPath(path).isURL)) return null;
1304
+ try {
1305
+ return {
1306
+ kind: "path",
1307
+ contents: await Promise.all(paths.map(async (path) => ({
1308
+ path: (0, node_path.relative)(root, path),
1309
+ content: await (0, node_fs_promises.readFile)(path, "utf8")
1310
+ })))
1311
+ };
1312
+ } catch {
1313
+ return null;
1314
+ }
1315
+ }
1316
+ };
1317
+ //#endregion
1233
1318
  //#region src/definePlugin.ts
1234
1319
  /**
1235
1320
  * Wraps a plugin factory and returns a function that accepts user options and
@@ -2124,9 +2209,10 @@ var KubbDriver = class KubbDriver {
2124
2209
  }
2125
2210
  async setup() {
2126
2211
  const normalized = this.config.plugins.map((rawPlugin) => this.#normalizePlugin(rawPlugin));
2212
+ const dependenciesByName = new Map(normalized.map((plugin) => [plugin.name, new Set(plugin.dependencies ?? [])]));
2127
2213
  normalized.sort((a, b) => {
2128
- if (b.dependencies?.includes(a.name)) return -1;
2129
- if (a.dependencies?.includes(b.name)) return 1;
2214
+ if (dependenciesByName.get(b.name)?.has(a.name)) return -1;
2215
+ if (dependenciesByName.get(a.name)?.has(b.name)) return 1;
2130
2216
  return enforceOrder(a.enforce) - enforceOrder(b.enforce);
2131
2217
  });
2132
2218
  for (const plugin of normalized) {
@@ -2351,8 +2437,10 @@ var KubbDriver = class KubbDriver {
2351
2437
  await hooks.emit("kubb:files:processing:start", { files });
2352
2438
  });
2353
2439
  const updateBuffer = [];
2440
+ const snapshotSources = /* @__PURE__ */ new Map();
2354
2441
  processor.hooks.on("update", (item) => {
2355
2442
  updateBuffer.push(item);
2443
+ if (item.source !== void 0) snapshotSources.set(item.file.path, item.source);
2356
2444
  });
2357
2445
  processor.hooks.on("end", async (files) => {
2358
2446
  await hooks.emit("kubb:files:processing:update", { files: updateBuffer.map((item) => ({
@@ -2368,6 +2456,19 @@ var KubbDriver = class KubbDriver {
2368
2456
  this.fileManager.hooks.on("upsert", onFileUpsert);
2369
2457
  return Diagnostics.scope((diagnostic) => diagnostics.push(diagnostic), async () => {
2370
2458
  try {
2459
+ const cache = config.cache;
2460
+ const outputRoot = (0, node_path.resolve)(config.root, config.output.path);
2461
+ const cacheKey = cache ? await Fingerprint.compute({
2462
+ config,
2463
+ adapterSource: this.#adapterSource,
2464
+ version
2465
+ }) : null;
2466
+ if (cache && cacheKey && await this.#restoreSnapshot({
2467
+ cache,
2468
+ cacheKey,
2469
+ outputRoot,
2470
+ storage
2471
+ })) return { diagnostics: Diagnostics.dedupe(diagnostics) };
2371
2472
  await this.#parseInput();
2372
2473
  await this.emitSetupHooks();
2373
2474
  if (this.adapter && this.inputNode) await hooks.emit("kubb:build:start", Object.assign({
@@ -2426,7 +2527,13 @@ var KubbDriver = class KubbDriver {
2426
2527
  await hooks.emit("kubb:build:end", {
2427
2528
  files: this.fileManager.files,
2428
2529
  config,
2429
- outputDir: (0, node_path.resolve)(config.root, config.output.path)
2530
+ outputDir: outputRoot
2531
+ });
2532
+ if (cache && cacheKey && !Diagnostics.hasError(diagnostics)) await this.#persistSnapshot({
2533
+ cache,
2534
+ cacheKey,
2535
+ outputRoot,
2536
+ sources: snapshotSources
2430
2537
  });
2431
2538
  return { diagnostics: Diagnostics.dedupe(diagnostics) };
2432
2539
  } catch (caughtError) {
@@ -2437,6 +2544,40 @@ var KubbDriver = class KubbDriver {
2437
2544
  }
2438
2545
  });
2439
2546
  }
2547
+ /**
2548
+ * Writes a restored snapshot straight to storage and emits `kubb:build:end`. Returns `true` on a
2549
+ * hit (the build is done), `false` on a miss so the caller falls through to a full build.
2550
+ */
2551
+ async #restoreSnapshot({ cache, cacheKey, outputRoot, storage }) {
2552
+ const snapshot = await cache.restore({ key: cacheKey });
2553
+ if (!snapshot) return false;
2554
+ for (const [relativePath, source] of Object.entries(snapshot.files)) {
2555
+ const absolutePath = (0, node_path.join)(outputRoot, relativePath);
2556
+ this.fileManager.upsert((0, _kubb_ast.createFile)({
2557
+ path: absolutePath,
2558
+ baseName: (0, node_path.basename)(relativePath)
2559
+ }));
2560
+ await storage.setItem(absolutePath, source);
2561
+ }
2562
+ await this.hooks.emit("kubb:build:end", {
2563
+ files: this.fileManager.files,
2564
+ config: this.config,
2565
+ outputDir: outputRoot
2566
+ });
2567
+ return true;
2568
+ }
2569
+ /**
2570
+ * Stores this run's rendered output, keyed by the input fingerprint, so the next unchanged build
2571
+ * restores it instead of regenerating. `sources` is keyed by absolute path and relativized here.
2572
+ */
2573
+ async #persistSnapshot({ cache, cacheKey, outputRoot, sources }) {
2574
+ const files = {};
2575
+ for (const [absolutePath, source] of sources) files[(0, node_path.relative)(outputRoot, absolutePath)] = source;
2576
+ await cache.persist({
2577
+ key: cacheKey,
2578
+ snapshot: { files }
2579
+ });
2580
+ }
2440
2581
  #filesPayload() {
2441
2582
  const driver = this;
2442
2583
  return {
@@ -2463,6 +2604,11 @@ var KubbDriver = class KubbDriver {
2463
2604
  * A failing plugin contributes an error diagnostic so the rest of the build continues.
2464
2605
  * Every plugin also contributes a `timing` diagnostic.
2465
2606
  *
2607
+ * Plugins run sequentially so `kubb:plugin:end` fires as each plugin completes, instead
2608
+ * of all at once after every plugin has marched through the parallel batches together.
2609
+ * That ordering is what drives the CLI's `Plugins N/M` counter; without it the bar would
2610
+ * sit at the initial value until the very end of the run.
2611
+ *
2466
2612
  * When `entries` is empty or `this.inputNode` is `null`, every entry still gets a
2467
2613
  * `kubb:plugin:end` so middleware listeners (the barrel writer and friends) complete.
2468
2614
  */
@@ -2507,15 +2653,16 @@ var KubbDriver = class KubbDriver {
2507
2653
  });
2508
2654
  const emitsSchemaHook = this.hooks.listenerCount("kubb:generate:schema") > 0;
2509
2655
  const emitsOperationHook = this.hooks.listenerCount("kubb:generate:operation") > 0;
2656
+ const emitsOperationsHook = this.hooks.listenerCount("kubb:generate:operations") > 0;
2657
+ const schemasBuffer = await Array.fromAsync(schemas);
2658
+ const operationsBuffer = await Array.fromAsync(operations);
2510
2659
  const pruningStates = states.filter(({ plugin }) => {
2511
2660
  const { include } = plugin.options;
2512
2661
  return (include?.some(({ type }) => OPERATION_FILTER_TYPES.has(type)) ?? false) && !(include?.some(({ type }) => type === "schemaName") ?? false);
2513
2662
  });
2514
2663
  if (pruningStates.length > 0) {
2515
- const allSchemas = [];
2516
- for await (const schema of schemas) allSchemas.push(schema);
2517
2664
  const includedOpsByState = new Map(pruningStates.map((state) => [state, []]));
2518
- for await (const operation of operations) for (const state of pruningStates) {
2665
+ for (const operation of operationsBuffer) for (const state of pruningStates) {
2519
2666
  const { exclude, include, override } = state.plugin.options;
2520
2667
  if (state.generatorContext.resolver.resolveOptions(operation, {
2521
2668
  options: state.plugin.options,
@@ -2525,7 +2672,7 @@ var KubbDriver = class KubbDriver {
2525
2672
  }) !== null) includedOpsByState.get(state)?.push(operation);
2526
2673
  }
2527
2674
  for (const state of pruningStates) {
2528
- state.allowedSchemaNames = (0, _kubb_ast.collectUsedSchemaNames)(includedOpsByState.get(state) ?? [], allSchemas);
2675
+ state.allowedSchemaNames = (0, _kubb_ast.collectUsedSchemaNames)(includedOpsByState.get(state) ?? [], schemasBuffer);
2529
2676
  includedOpsByState.delete(state);
2530
2677
  }
2531
2678
  }
@@ -2587,20 +2734,20 @@ var KubbDriver = class KubbDriver {
2587
2734
  checkAllowedNames: false,
2588
2735
  emit: emitsOperationHook ? (node, ctx) => this.hooks.emit("kubb:generate:operation", node, ctx) : null
2589
2736
  };
2590
- const needsCollectedOperations = this.hooks.listenerCount("kubb:generate:operations") > 0 || states.some((state) => state.generators.some((gen) => !!gen.operations));
2591
- const collectedOperations = needsCollectedOperations ? [] : void 0;
2592
- await forBatches(schemas, (nodes) => Promise.all(nodes.flatMap((node) => states.map((state) => dispatchNode(state, node, schemaDispatch)))), {
2593
- concurrency: 8,
2594
- flush: flushPending
2595
- });
2596
- await forBatches(operations, (nodes) => {
2597
- if (needsCollectedOperations) collectedOperations?.push(...nodes);
2598
- return Promise.all(nodes.flatMap((node) => states.map((state) => dispatchNode(state, node, operationDispatch))));
2599
- }, {
2600
- concurrency: 8,
2601
- flush: flushPending
2602
- });
2603
2737
  for (const state of states) {
2738
+ const needsCollectedOperations = emitsOperationsHook || state.generators.some((gen) => !!gen.operations);
2739
+ const collectedOperations = needsCollectedOperations ? [] : void 0;
2740
+ await forBatches(schemasBuffer, (nodes) => Promise.all(nodes.map((node) => dispatchNode(state, node, schemaDispatch))), {
2741
+ concurrency: 8,
2742
+ flush: flushPending
2743
+ });
2744
+ await forBatches(operationsBuffer, (nodes) => {
2745
+ if (needsCollectedOperations) collectedOperations?.push(...nodes);
2746
+ return Promise.all(nodes.map((node) => dispatchNode(state, node, operationDispatch)));
2747
+ }, {
2748
+ concurrency: 8,
2749
+ flush: flushPending
2750
+ });
2604
2751
  if (!state.failed && needsCollectedOperations) try {
2605
2752
  const { plugin, generatorContext, generators } = state;
2606
2753
  const ctx = {
@@ -2982,4 +3129,4 @@ Object.defineProperty(exports, "memoryStorage", {
2982
3129
  }
2983
3130
  });
2984
3131
 
2985
- //# sourceMappingURL=memoryStorage-Dkxnid2K.cjs.map
3132
+ //# sourceMappingURL=memoryStorage-Dldu8sRT.cjs.map