@kubb/studio 5.3.4 → 5.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -673,7 +673,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
673
673
  }
674
674
  //#endregion
675
675
  //#region package.json
676
- var version = "5.3.4";
676
+ var version = "5.3.5";
677
677
  //#endregion
678
678
  //#region src/hooks.ts
679
679
  /**
@@ -1736,12 +1736,6 @@ async function createSnapshotPackage(files, packageInfo) {
1736
1736
  //#endregion
1737
1737
  //#region src/ws.ts
1738
1738
  /**
1739
- * How many generated files are read from storage at once when building the
1740
- * `kubb:generation:end` payload. A spec producing thousands of files would otherwise fire one
1741
- * `storage.readItem` per file simultaneously.
1742
- */
1743
- const FILE_READ_CONCURRENCY = 50;
1744
- /**
1745
1739
  * How long the initial handshake may take before the socket is closed and the reconnect loop
1746
1740
  * takes over.
1747
1741
  */
@@ -1757,6 +1751,12 @@ const require = createRequire(import.meta.url);
1757
1751
  function relativeStoragePath(root, filePath) {
1758
1752
  return (isAbsolute(filePath) ? relative(resolve(root), filePath) : filePath).replaceAll("\\", "/");
1759
1753
  }
1754
+ /**
1755
+ * Inverse of {@link relativeStoragePath}: rebuilds the storage key a relative path came from.
1756
+ */
1757
+ function absoluteStoragePath(root, relativePath) {
1758
+ return resolve(root, relativePath);
1759
+ }
1760
1760
  async function resolvePeerDependencies(names) {
1761
1761
  const uniqueNames = [...new Set(names.map(toPackageName))];
1762
1762
  const peerDependencies = {};
@@ -1874,12 +1874,12 @@ function setupEventsStream(ws, hooks, jobId, options = {}) {
1874
1874
  }]
1875
1875
  });
1876
1876
  });
1877
- on("kubb:build:end", ({ files, outputDir }) => {
1877
+ on("kubb:build:end", ({ files, config, outputDir }) => {
1878
1878
  sendDataMessage({
1879
1879
  type: "kubb:build:end",
1880
1880
  data: [{
1881
1881
  files: files.map((file) => ({
1882
- path: file.path,
1882
+ path: relativeStoragePath(config.root, file.path),
1883
1883
  name: file.name
1884
1884
  })),
1885
1885
  outputDir
@@ -1933,25 +1933,18 @@ function setupEventsStream(ws, hooks, jobId, options = {}) {
1933
1933
  });
1934
1934
  on("kubb:generation:end", async ({ config, storage, diagnostics = [], status, hrStart, filesCreated }) => {
1935
1935
  const { peerDependencies, missingDependencies } = await resolvePeerDependencies(config.plugins.map(({ name }) => name));
1936
- const paths = await storage.readKeys();
1937
- const files = {};
1938
- await inParallel({
1939
- items: paths,
1940
- limit: FILE_READ_CONCURRENCY,
1941
- run: async (path) => {
1942
- const content = await storage.readItem(path);
1943
- if (content !== null) files[relativeStoragePath(config.root, path)] = content;
1944
- }
1936
+ const keys = await storage.readKeys();
1937
+ const paths = new Set(keys.map((key) => relativeStoragePath(config.root, key)));
1938
+ options.onGenerationEnd?.({
1939
+ storage,
1940
+ root: config.root,
1941
+ paths,
1942
+ peerDependencies,
1943
+ missingDependencies
1945
1944
  });
1946
- options.onGenerationEnd?.(files);
1947
1945
  sendDataMessage({
1948
1946
  type: "kubb:generation:end",
1949
- data: [{
1950
- config,
1951
- storage: options.skipStorage ? {} : files,
1952
- peerDependencies,
1953
- missingDependencies
1954
- }]
1947
+ data: []
1955
1948
  });
1956
1949
  if (!hrStart) return;
1957
1950
  sendDataMessage({
@@ -2029,6 +2022,11 @@ function setupEventsStream(ws, hooks, jobId, options = {}) {
2029
2022
  //#endregion
2030
2023
  //#region src/StudioSession.ts
2031
2024
  /**
2025
+ * How many files are read from storage at once when serving `studio:files` or packing a
2026
+ * `studio:snapshot`.
2027
+ */
2028
+ const FILE_READ_CONCURRENCY = 50;
2029
+ /**
2032
2030
  * How long the `agent:connect` handshake may go unacknowledged before `studio:ready` is given up
2033
2031
  * on for this open. A Studio that predates the ack never sends one, so this only ever produces a
2034
2032
  * warning, not a reconnect.
@@ -2051,6 +2049,7 @@ function applyStudioDefaults(options) {
2051
2049
  allowConfigEdit: false,
2052
2050
  allowInput: false,
2053
2051
  allowExec: false,
2052
+ allowRead: false,
2054
2053
  ...options.permissions
2055
2054
  },
2056
2055
  retryInterval: options.retryInterval ?? agentDefaults.retryIntervalMs,
@@ -2131,6 +2130,12 @@ var StudioSession = class {
2131
2130
  get #canUseInput() {
2132
2131
  return this.#isSandbox || this.#options.permissions.allowInput;
2133
2132
  }
2133
+ /**
2134
+ * A sandbox agent always allows reading its output back; a local agent only when opted in.
2135
+ */
2136
+ get #canRead() {
2137
+ return this.#isSandbox || this.#options.permissions.allowRead;
2138
+ }
2134
2139
  async connect() {
2135
2140
  const { token, studioUrl, signal, heartbeatInterval, installLogger } = this.#options;
2136
2141
  await installLogger?.(this.#hooks);
@@ -2224,7 +2229,8 @@ var StudioSession = class {
2224
2229
  ...permissions,
2225
2230
  allowWrite: this.#canWrite,
2226
2231
  allowInput: this.#canUseInput,
2227
- allowConfigEdit: this.#canEditConfig
2232
+ allowConfigEdit: this.#canEditConfig,
2233
+ allowRead: this.#canRead
2228
2234
  }
2229
2235
  }
2230
2236
  });
@@ -2366,6 +2372,9 @@ var StudioSession = class {
2366
2372
  case "studio:snapshot":
2367
2373
  await this.#handleSnapshot(ws, data, command);
2368
2374
  return;
2375
+ case "studio:files":
2376
+ await this.#handleFiles(ws, data, command);
2377
+ return;
2369
2378
  }
2370
2379
  }
2371
2380
  async #handleGenerate(ws, data, command) {
@@ -2389,13 +2398,10 @@ var StudioSession = class {
2389
2398
  await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
2390
2399
  }
2391
2400
  const resolvedPlugins = plugins ?? config.plugins;
2392
- let generatedFiles;
2393
- const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, {
2394
- skipStorage: client?.kind === "ci",
2395
- onGenerationEnd: (files) => {
2396
- generatedFiles = files;
2397
- }
2398
- })];
2401
+ this.#lastGeneration = void 0;
2402
+ const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, { onGenerationEnd: (result) => {
2403
+ this.#lastGeneration = result;
2404
+ } })];
2399
2405
  try {
2400
2406
  await generate({
2401
2407
  config: {
@@ -2416,7 +2422,6 @@ var StudioSession = class {
2416
2422
  });
2417
2423
  } finally {
2418
2424
  for (const remove of detach) remove();
2419
- this.#lastGeneration = generatedFiles;
2420
2425
  }
2421
2426
  await this.#hooks.callHook("studio:command:end", {
2422
2427
  command,
@@ -2499,23 +2504,39 @@ var StudioSession = class {
2499
2504
  refuse("a sandbox agent has no project to build a package from");
2500
2505
  return;
2501
2506
  }
2502
- const { name, version, peerDependencies, uploadPath } = data.payload;
2507
+ const { name, version, bundledDependencies, uploadPath } = data.payload;
2503
2508
  if (!name || !version || !uploadPath) {
2504
2509
  await this.#warn("Ignored snapshot: the message was missing required fields");
2505
2510
  refuse("the message was missing required fields");
2506
2511
  return;
2507
2512
  }
2508
- const files = this.#lastGeneration;
2509
- if (!files) {
2513
+ const generation = this.#lastGeneration;
2514
+ if (!generation) {
2510
2515
  await this.#warn("Ignored snapshot: no prior generation to pack");
2511
2516
  refuse("no prior generation exists to pack, run a generation first");
2512
2517
  return;
2513
2518
  }
2519
+ const bundled = new Set(bundledDependencies ?? []);
2520
+ const missing = generation.missingDependencies.filter((dependency) => !bundled.has(dependency));
2521
+ if (missing.length) {
2522
+ await this.#warn(`Ignored snapshot: missing dependencies: ${missing.join(", ")}`);
2523
+ refuse(`missing dependencies: ${missing.join(", ")}`);
2524
+ return;
2525
+ }
2514
2526
  try {
2527
+ const files = {};
2528
+ await inParallel({
2529
+ items: [...generation.paths],
2530
+ limit: FILE_READ_CONCURRENCY,
2531
+ run: async (relativePath) => {
2532
+ const content = await generation.storage.readItem(absoluteStoragePath(generation.root, relativePath));
2533
+ if (content !== null) files[relativePath] = content;
2534
+ }
2535
+ });
2515
2536
  const { bytes, integrity } = await createSnapshotPackage(files, {
2516
2537
  name,
2517
2538
  version,
2518
- peerDependencies: peerDependencies ?? {}
2539
+ peerDependencies: generation.peerDependencies
2519
2540
  });
2520
2541
  const { token, studioUrl } = this.#options;
2521
2542
  const redirect = await fetch(new URL(uploadPath, studioUrl), {
@@ -2535,7 +2556,8 @@ var StudioSession = class {
2535
2556
  jobId: data.jobId,
2536
2557
  payload: {
2537
2558
  status: "ok",
2538
- integrity
2559
+ integrity,
2560
+ peerDependencies: generation.peerDependencies
2539
2561
  }
2540
2562
  });
2541
2563
  await this.#hooks.callHook("studio:command:end", {
@@ -2547,6 +2569,61 @@ var StudioSession = class {
2547
2569
  refuse(getErrorMessage(error));
2548
2570
  }
2549
2571
  }
2572
+ async #handleFiles(ws, data, command) {
2573
+ const { client } = this.#options;
2574
+ const refuse = (message) => sendAgentMessage(ws, {
2575
+ type: "agent:files",
2576
+ jobId: data.jobId,
2577
+ payload: {
2578
+ status: "error",
2579
+ message
2580
+ }
2581
+ });
2582
+ if (!this.#canRead) {
2583
+ await this.#warn("Ignored files: reading generated files was not granted");
2584
+ refuse(`the agent was not granted permission to read generated files; set ${client?.kind === "cli" ? "--allow-read, or answer yes when kubb studio asks," : "KUBB_AGENT_ALLOW_READ=true"} to allow it`);
2585
+ return;
2586
+ }
2587
+ if (!Array.isArray(data.payload?.paths)) {
2588
+ await this.#warn("Ignored files: the message carried no paths");
2589
+ refuse("the message carried no paths");
2590
+ return;
2591
+ }
2592
+ const { paths } = data.payload;
2593
+ if (paths.length > 50) {
2594
+ await this.#warn(`Ignored files: requested ${paths.length} paths, more than the 50 allowed per request`);
2595
+ refuse(`at most 50 paths may be requested at once`);
2596
+ return;
2597
+ }
2598
+ const generation = this.#lastGeneration;
2599
+ if (!generation) {
2600
+ await this.#warn("Ignored files: no prior generation to read from");
2601
+ refuse("no prior generation to read from, run a generation first");
2602
+ return;
2603
+ }
2604
+ const requested = paths.filter((path) => generation.paths.has(path));
2605
+ const files = {};
2606
+ await inParallel({
2607
+ items: requested,
2608
+ limit: FILE_READ_CONCURRENCY,
2609
+ run: async (path) => {
2610
+ const content = await generation.storage.readItem(absoluteStoragePath(generation.root, path));
2611
+ if (content !== null) files[path] = content;
2612
+ }
2613
+ });
2614
+ sendAgentMessage(ws, {
2615
+ type: "agent:files",
2616
+ jobId: data.jobId,
2617
+ payload: {
2618
+ status: "ok",
2619
+ files
2620
+ }
2621
+ });
2622
+ await this.#hooks.callHook("studio:command:end", {
2623
+ command,
2624
+ info: `read ${Object.keys(files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`
2625
+ });
2626
+ }
2550
2627
  };
2551
2628
  //#endregion
2552
2629
  //#region src/client.ts