@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.cjs CHANGED
@@ -678,7 +678,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
678
678
  }
679
679
  //#endregion
680
680
  //#region package.json
681
- var version = "5.3.4";
681
+ var version = "5.3.5";
682
682
  //#endregion
683
683
  //#region src/hooks.ts
684
684
  /**
@@ -1741,12 +1741,6 @@ async function createSnapshotPackage(files, packageInfo) {
1741
1741
  //#endregion
1742
1742
  //#region src/ws.ts
1743
1743
  /**
1744
- * How many generated files are read from storage at once when building the
1745
- * `kubb:generation:end` payload. A spec producing thousands of files would otherwise fire one
1746
- * `storage.readItem` per file simultaneously.
1747
- */
1748
- const FILE_READ_CONCURRENCY = 50;
1749
- /**
1750
1744
  * How long the initial handshake may take before the socket is closed and the reconnect loop
1751
1745
  * takes over.
1752
1746
  */
@@ -1762,6 +1756,12 @@ const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__
1762
1756
  function relativeStoragePath(root, filePath) {
1763
1757
  return ((0, node_path.isAbsolute)(filePath) ? (0, node_path.relative)((0, node_path.resolve)(root), filePath) : filePath).replaceAll("\\", "/");
1764
1758
  }
1759
+ /**
1760
+ * Inverse of {@link relativeStoragePath}: rebuilds the storage key a relative path came from.
1761
+ */
1762
+ function absoluteStoragePath(root, relativePath) {
1763
+ return (0, node_path.resolve)(root, relativePath);
1764
+ }
1765
1765
  async function resolvePeerDependencies(names) {
1766
1766
  const uniqueNames = [...new Set(names.map(toPackageName))];
1767
1767
  const peerDependencies = {};
@@ -1879,12 +1879,12 @@ function setupEventsStream(ws$5, hooks, jobId, options = {}) {
1879
1879
  }]
1880
1880
  });
1881
1881
  });
1882
- on("kubb:build:end", ({ files, outputDir }) => {
1882
+ on("kubb:build:end", ({ files, config, outputDir }) => {
1883
1883
  sendDataMessage({
1884
1884
  type: "kubb:build:end",
1885
1885
  data: [{
1886
1886
  files: files.map((file) => ({
1887
- path: file.path,
1887
+ path: relativeStoragePath(config.root, file.path),
1888
1888
  name: file.name
1889
1889
  })),
1890
1890
  outputDir
@@ -1938,25 +1938,18 @@ function setupEventsStream(ws$5, hooks, jobId, options = {}) {
1938
1938
  });
1939
1939
  on("kubb:generation:end", async ({ config, storage, diagnostics = [], status, hrStart, filesCreated }) => {
1940
1940
  const { peerDependencies, missingDependencies } = await resolvePeerDependencies(config.plugins.map(({ name }) => name));
1941
- const paths = await storage.readKeys();
1942
- const files = {};
1943
- await inParallel({
1944
- items: paths,
1945
- limit: FILE_READ_CONCURRENCY,
1946
- run: async (path) => {
1947
- const content = await storage.readItem(path);
1948
- if (content !== null) files[relativeStoragePath(config.root, path)] = content;
1949
- }
1941
+ const keys = await storage.readKeys();
1942
+ const paths = new Set(keys.map((key) => relativeStoragePath(config.root, key)));
1943
+ options.onGenerationEnd?.({
1944
+ storage,
1945
+ root: config.root,
1946
+ paths,
1947
+ peerDependencies,
1948
+ missingDependencies
1950
1949
  });
1951
- options.onGenerationEnd?.(files);
1952
1950
  sendDataMessage({
1953
1951
  type: "kubb:generation:end",
1954
- data: [{
1955
- config,
1956
- storage: options.skipStorage ? {} : files,
1957
- peerDependencies,
1958
- missingDependencies
1959
- }]
1952
+ data: []
1960
1953
  });
1961
1954
  if (!hrStart) return;
1962
1955
  sendDataMessage({
@@ -2034,6 +2027,11 @@ function setupEventsStream(ws$5, hooks, jobId, options = {}) {
2034
2027
  //#endregion
2035
2028
  //#region src/StudioSession.ts
2036
2029
  /**
2030
+ * How many files are read from storage at once when serving `studio:files` or packing a
2031
+ * `studio:snapshot`.
2032
+ */
2033
+ const FILE_READ_CONCURRENCY = 50;
2034
+ /**
2037
2035
  * How long the `agent:connect` handshake may go unacknowledged before `studio:ready` is given up
2038
2036
  * on for this open. A Studio that predates the ack never sends one, so this only ever produces a
2039
2037
  * warning, not a reconnect.
@@ -2056,6 +2054,7 @@ function applyStudioDefaults(options) {
2056
2054
  allowConfigEdit: false,
2057
2055
  allowInput: false,
2058
2056
  allowExec: false,
2057
+ allowRead: false,
2059
2058
  ...options.permissions
2060
2059
  },
2061
2060
  retryInterval: options.retryInterval ?? agentDefaults.retryIntervalMs,
@@ -2136,6 +2135,12 @@ var StudioSession = class {
2136
2135
  get #canUseInput() {
2137
2136
  return this.#isSandbox || this.#options.permissions.allowInput;
2138
2137
  }
2138
+ /**
2139
+ * A sandbox agent always allows reading its output back; a local agent only when opted in.
2140
+ */
2141
+ get #canRead() {
2142
+ return this.#isSandbox || this.#options.permissions.allowRead;
2143
+ }
2139
2144
  async connect() {
2140
2145
  const { token, studioUrl, signal, heartbeatInterval, installLogger } = this.#options;
2141
2146
  await installLogger?.(this.#hooks);
@@ -2229,7 +2234,8 @@ var StudioSession = class {
2229
2234
  ...permissions,
2230
2235
  allowWrite: this.#canWrite,
2231
2236
  allowInput: this.#canUseInput,
2232
- allowConfigEdit: this.#canEditConfig
2237
+ allowConfigEdit: this.#canEditConfig,
2238
+ allowRead: this.#canRead
2233
2239
  }
2234
2240
  }
2235
2241
  });
@@ -2371,6 +2377,9 @@ var StudioSession = class {
2371
2377
  case "studio:snapshot":
2372
2378
  await this.#handleSnapshot(ws, data, command);
2373
2379
  return;
2380
+ case "studio:files":
2381
+ await this.#handleFiles(ws, data, command);
2382
+ return;
2374
2383
  }
2375
2384
  }
2376
2385
  async #handleGenerate(ws, data, command) {
@@ -2394,13 +2403,10 @@ var StudioSession = class {
2394
2403
  await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
2395
2404
  }
2396
2405
  const resolvedPlugins = plugins ?? config.plugins;
2397
- let generatedFiles;
2398
- const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, {
2399
- skipStorage: client?.kind === "ci",
2400
- onGenerationEnd: (files) => {
2401
- generatedFiles = files;
2402
- }
2403
- })];
2406
+ this.#lastGeneration = void 0;
2407
+ const detach = [setupHookListener(this.#hooks, root), setupEventsStream(ws, this.#hooks, data.jobId, { onGenerationEnd: (result) => {
2408
+ this.#lastGeneration = result;
2409
+ } })];
2404
2410
  try {
2405
2411
  await generate({
2406
2412
  config: {
@@ -2421,7 +2427,6 @@ var StudioSession = class {
2421
2427
  });
2422
2428
  } finally {
2423
2429
  for (const remove of detach) remove();
2424
- this.#lastGeneration = generatedFiles;
2425
2430
  }
2426
2431
  await this.#hooks.callHook("studio:command:end", {
2427
2432
  command,
@@ -2504,23 +2509,39 @@ var StudioSession = class {
2504
2509
  refuse("a sandbox agent has no project to build a package from");
2505
2510
  return;
2506
2511
  }
2507
- const { name, version, peerDependencies, uploadPath } = data.payload;
2512
+ const { name, version, bundledDependencies, uploadPath } = data.payload;
2508
2513
  if (!name || !version || !uploadPath) {
2509
2514
  await this.#warn("Ignored snapshot: the message was missing required fields");
2510
2515
  refuse("the message was missing required fields");
2511
2516
  return;
2512
2517
  }
2513
- const files = this.#lastGeneration;
2514
- if (!files) {
2518
+ const generation = this.#lastGeneration;
2519
+ if (!generation) {
2515
2520
  await this.#warn("Ignored snapshot: no prior generation to pack");
2516
2521
  refuse("no prior generation exists to pack, run a generation first");
2517
2522
  return;
2518
2523
  }
2524
+ const bundled = new Set(bundledDependencies ?? []);
2525
+ const missing = generation.missingDependencies.filter((dependency) => !bundled.has(dependency));
2526
+ if (missing.length) {
2527
+ await this.#warn(`Ignored snapshot: missing dependencies: ${missing.join(", ")}`);
2528
+ refuse(`missing dependencies: ${missing.join(", ")}`);
2529
+ return;
2530
+ }
2519
2531
  try {
2532
+ const files = {};
2533
+ await inParallel({
2534
+ items: [...generation.paths],
2535
+ limit: FILE_READ_CONCURRENCY,
2536
+ run: async (relativePath) => {
2537
+ const content = await generation.storage.readItem(absoluteStoragePath(generation.root, relativePath));
2538
+ if (content !== null) files[relativePath] = content;
2539
+ }
2540
+ });
2520
2541
  const { bytes, integrity } = await createSnapshotPackage(files, {
2521
2542
  name,
2522
2543
  version,
2523
- peerDependencies: peerDependencies ?? {}
2544
+ peerDependencies: generation.peerDependencies
2524
2545
  });
2525
2546
  const { token, studioUrl } = this.#options;
2526
2547
  const redirect = await fetch(new URL(uploadPath, studioUrl), {
@@ -2540,7 +2561,8 @@ var StudioSession = class {
2540
2561
  jobId: data.jobId,
2541
2562
  payload: {
2542
2563
  status: "ok",
2543
- integrity
2564
+ integrity,
2565
+ peerDependencies: generation.peerDependencies
2544
2566
  }
2545
2567
  });
2546
2568
  await this.#hooks.callHook("studio:command:end", {
@@ -2552,6 +2574,61 @@ var StudioSession = class {
2552
2574
  refuse(getErrorMessage(error));
2553
2575
  }
2554
2576
  }
2577
+ async #handleFiles(ws, data, command) {
2578
+ const { client } = this.#options;
2579
+ const refuse = (message) => sendAgentMessage(ws, {
2580
+ type: "agent:files",
2581
+ jobId: data.jobId,
2582
+ payload: {
2583
+ status: "error",
2584
+ message
2585
+ }
2586
+ });
2587
+ if (!this.#canRead) {
2588
+ await this.#warn("Ignored files: reading generated files was not granted");
2589
+ 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`);
2590
+ return;
2591
+ }
2592
+ if (!Array.isArray(data.payload?.paths)) {
2593
+ await this.#warn("Ignored files: the message carried no paths");
2594
+ refuse("the message carried no paths");
2595
+ return;
2596
+ }
2597
+ const { paths } = data.payload;
2598
+ if (paths.length > 50) {
2599
+ await this.#warn(`Ignored files: requested ${paths.length} paths, more than the 50 allowed per request`);
2600
+ refuse(`at most 50 paths may be requested at once`);
2601
+ return;
2602
+ }
2603
+ const generation = this.#lastGeneration;
2604
+ if (!generation) {
2605
+ await this.#warn("Ignored files: no prior generation to read from");
2606
+ refuse("no prior generation to read from, run a generation first");
2607
+ return;
2608
+ }
2609
+ const requested = paths.filter((path) => generation.paths.has(path));
2610
+ const files = {};
2611
+ await inParallel({
2612
+ items: requested,
2613
+ limit: FILE_READ_CONCURRENCY,
2614
+ run: async (path) => {
2615
+ const content = await generation.storage.readItem(absoluteStoragePath(generation.root, path));
2616
+ if (content !== null) files[path] = content;
2617
+ }
2618
+ });
2619
+ sendAgentMessage(ws, {
2620
+ type: "agent:files",
2621
+ jobId: data.jobId,
2622
+ payload: {
2623
+ status: "ok",
2624
+ files
2625
+ }
2626
+ });
2627
+ await this.#hooks.callHook("studio:command:end", {
2628
+ command,
2629
+ info: `read ${Object.keys(files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`
2630
+ });
2631
+ }
2555
2632
  };
2556
2633
  //#endregion
2557
2634
  //#region src/client.ts