@kubb/studio 5.9.3 → 5.9.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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
2
- import { L as SnapshotUploadMetadata, a as AgentConnectInfo, c as ConfigEdit, j as PublishedSnapshot, l as ConfigEditOutcome, o as AgentPermissions, w as InstalledExtensions } from "./protocol-gXoAXvT3.js";
2
+ import { M as PublishedSnapshot, V as SnapshotUploadMetadata, a as AgentConnectInfo, c as ConfigEdit, l as ConfigEditOutcome, o as AgentPermissions, w as InstalledExtensions } from "./protocol-ixLkhvRJ.js";
3
3
  import { Config, Hookable, KubbHooks } from "@kubb/core";
4
4
  //#region src/connect.d.ts
5
5
  /**
@@ -46,7 +46,7 @@ declare global {
46
46
  * A finished command and what it did, such as `applied 2/3 edits to kubb.config.ts`.
47
47
  */
48
48
  'studio:command': [ctx: {
49
- command: 'generate' | 'saveConfig' | 'readFiles';
49
+ command: 'generate' | 'saveConfig' | 'readFiles' | 'readInput';
50
50
  info: string;
51
51
  }];
52
52
  /**
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./rolldown-runtime-CRm0XQPb.js";
2
- import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, GENERATION_GONE_MESSAGE, MAX_PAGE_LINES, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
2
+ import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, GENERATION_GONE_MESSAGE, MAX_PAGE_LINES, MAX_SNAPSHOT_BYTES, NO_INPUT_MESSAGE, READ_NOT_ALLOWED_MESSAGE, generationEventTypes } from "./protocol.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { createHash, hash, randomUUID } from "node:crypto";
5
5
  import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
@@ -12,9 +12,10 @@ import { existsSync } from "node:fs";
12
12
  import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, getInputKind, isPackageName, memoryStorage, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
13
13
  import { RpcTarget, newWebSocketRpcSession } from "capnweb";
14
14
  import { detectCodeFormat, parseExpression, parseModule } from "magicast";
15
- import { promisify, stripVTControlCharacters } from "node:util";
15
+ import { stripVTControlCharacters } from "node:util";
16
+ import { ast } from "@kubb/ast";
16
17
  import { ofetch } from "ofetch";
17
- import { gzip } from "node:zlib";
18
+ import { gzipSync } from "node:zlib";
18
19
  //#region ../../internals/utils/src/casing.ts
19
20
  /**
20
21
  * Shared implementation for camelCase and PascalCase conversion.
@@ -152,10 +153,18 @@ function getElapsedMs(hrStart) {
152
153
  }
153
154
  //#endregion
154
155
  //#region package.json
155
- var version = "5.9.3";
156
+ var version = "5.9.5";
156
157
  //#endregion
157
158
  //#region src/resolveConfig.ts
158
159
  /**
160
+ * Turns the JSON config Studio sends back into live Kubb objects.
161
+ *
162
+ * A plugin or adapter instance carries closures (`parse`, `getImports`, ...) that cannot survive
163
+ * JSON, so both sides pass options over the wire and the factory is re-invoked here with the merged
164
+ * result. Only packages {@link isAllowedPlugin} accepts are resolved this way, never an arbitrary
165
+ * module the payload names.
166
+ */
167
+ /**
159
168
  * Imports a package, falling back to how the user's project would resolve it.
160
169
  *
161
170
  * `import()` resolves from this file, so a linked or globally installed Studio (`pnpm link`,
@@ -234,8 +243,9 @@ function isAllowedPlugin({ packageName, extensions }) {
234
243
  return KUBB_PLUGIN_SPECIFIER.test(packageName) || extensions.get(packageName)?.manifest.kind === "plugin";
235
244
  }
236
245
  /**
237
- * Dynamically imports a plugin package and returns its factory. Only packages
238
- * {@link isAllowedPlugin} accepts reach `import()`.
246
+ * Dynamically imports an extension package and returns its factory. Callers check
247
+ * {@link isAllowedPlugin} or derive the package from a disk instance first, so an arbitrary module
248
+ * never reaches `import()`.
239
249
  *
240
250
  * Packages must be pre-installed in the Docker image at build time via the `KUBB_PACKAGES`
241
251
  * build ARG, no runtime installation is possible in the distroless container.
@@ -243,13 +253,9 @@ function isAllowedPlugin({ packageName, extensions }) {
243
253
  * Resolution order: the manifest's `export`, else the camelCase named export the package name
244
254
  * implies (e.g. `pluginTs`), then the default export.
245
255
  *
246
- * @throws if the package is not allowed, cannot be imported, or exports no callable factory.
256
+ * @throws if the package cannot be imported or exports no callable factory.
247
257
  */
248
- async function loadPluginFactory({ packageName, extensions }) {
249
- if (!isAllowedPlugin({
250
- packageName,
251
- extensions
252
- })) throw new Error(`Plugin "${packageName}" is not an installed Kubb plugin. Studio loads @kubb/plugin-* packages and installed packages that ship a kubb-extension.json with kind "plugin".`);
258
+ async function loadFactory({ packageName, extensions }) {
253
259
  let mod;
254
260
  try {
255
261
  mod = await importFromProject(packageName);
@@ -268,7 +274,7 @@ async function loadPluginFactory({ packageName, extensions }) {
268
274
  }
269
275
  /**
270
276
  * Resolves each plugin entry by dynamically importing its package and calling its factory with the
271
- * provided options. See {@link loadPluginFactory} for which packages are allowed.
277
+ * provided options. Only packages {@link isAllowedPlugin} accepts are loaded.
272
278
  *
273
279
  * @example
274
280
  * ```ts
@@ -282,7 +288,11 @@ async function loadPluginFactory({ packageName, extensions }) {
282
288
  */
283
289
  async function resolvePlugins({ plugins, extensions = /* @__PURE__ */ new Map() }) {
284
290
  return Promise.all(plugins.map(async ({ name, options }) => {
285
- return (await loadPluginFactory({
291
+ if (!isAllowedPlugin({
292
+ packageName: name,
293
+ extensions
294
+ })) throw new Error(`Plugin "${name}" is not an installed Kubb plugin. Studio loads @kubb/plugin-* packages and installed packages that ship a kubb-extension.json with kind "plugin".`);
295
+ return (await loadFactory({
286
296
  packageName: name,
287
297
  extensions
288
298
  }))(options ?? {});
@@ -430,13 +440,12 @@ async function mergePlugins({ diskPlugins, studioPlugins, extensions = /* @__PUR
430
440
  async function rebuildExtension({ disk, studioOptions, kind, extensions }) {
431
441
  if (!studioOptions || !Object.keys(studioOptions).length) return disk;
432
442
  const { name } = resolveMeta(disk);
433
- const installed = [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name);
434
- const packageName = installed?.package ?? `@kubb/${kind}-${name}`;
435
- const factory = await importFromProject(packageName).then((mod) => mod[installed?.manifest.export ?? toExportName(packageName)]).catch((cause) => {
443
+ return (await loadFactory({
444
+ packageName: [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name)?.package ?? `@kubb/${kind}-${name}`,
445
+ extensions
446
+ }).catch((cause) => {
436
447
  throw new Error(`The ${kind} "${name}" is not an installed Kubb extension, so Studio cannot change its options. Edit it in the config file.`, { cause });
437
- });
438
- if (typeof factory !== "function") throw new Error(`The ${kind} "${name}" is not an installed Kubb extension, so Studio cannot change its options. Edit it in the config file.`);
439
- return factory(mergeOptions(disk.options ?? {}, studioOptions));
448
+ }))(mergeOptions(disk.options ?? {}, studioOptions));
440
449
  }
441
450
  /**
442
451
  * Merges Studio-provided adapter option overrides into the disk config's adapter, see {@link rebuildExtension}.
@@ -1075,7 +1084,10 @@ function relativeStoragePath({ root, filePath }) {
1075
1084
  * The content fingerprint Studio compares snapshots by. Agent runs and CI uploads must agree on it.
1076
1085
  */
1077
1086
  const hashOf = (content) => createHash("sha1").update(content).digest("hex").slice(0, 16);
1078
- const totalBytes = (entries) => entries.reduce((sum, { output, disk }) => sum + output.bytes + (disk?.bytes ?? 0), 0);
1087
+ /**
1088
+ * The bytes `entries` keep on disk: output, written files, and kept input.
1089
+ */
1090
+ const totalBytes = (entries) => entries.reduce((sum, { output, disk, inputBytes }) => sum + output.bytes + (disk?.bytes ?? 0) + (inputBytes ?? 0), 0);
1079
1091
  function positiveNumber(value) {
1080
1092
  const parsed = Number(value);
1081
1093
  return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
@@ -1118,7 +1130,11 @@ async function generate({ config, hooks, signal, allowExec }) {
1118
1130
  hooks,
1119
1131
  signal
1120
1132
  });
1121
- const result = await kubb.generate();
1133
+ let input = null;
1134
+ const unhook = kubb.hooks.hook("kubb:build:end", (ctx) => {
1135
+ if (ctx.config === kubb.config) input = kubb.driver.inputNode;
1136
+ });
1137
+ const result = await kubb.generate().finally(unhook);
1122
1138
  if (!result.success) throw generationFailure(result.diagnostics);
1123
1139
  const { storage, root } = kubb.config;
1124
1140
  return {
@@ -1127,7 +1143,8 @@ async function generate({ config, hooks, signal, allowExec }) {
1127
1143
  paths: new Set(result.files.map((file) => relativeStoragePath({
1128
1144
  root,
1129
1145
  filePath: file.path
1130
- })))
1146
+ }))),
1147
+ input
1131
1148
  };
1132
1149
  }
1133
1150
  /**
@@ -1208,6 +1225,33 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
1208
1225
  return {
1209
1226
  keep,
1210
1227
  drop,
1228
+ /**
1229
+ * Keeps the `InputNode` of `jobId` and returns its size in bytes, or 0 when it keeps none. A ref schema loses
1230
+ * the copy of its target, which `input.schemas` holds: with it, a large spec outgrows what `JSON.stringify` can return.
1231
+ */
1232
+ async keepInput({ jobId, input, maxSetMb }) {
1233
+ if (!input) return 0;
1234
+ let json;
1235
+ try {
1236
+ json = JSON.stringify(ast.transform(input, { schema: (node) => node.type === "ref" && node.schema ? {
1237
+ ...node,
1238
+ schema: void 0
1239
+ } : void 0 }));
1240
+ } catch {
1241
+ return 0;
1242
+ }
1243
+ const bytes = Buffer.byteLength(json);
1244
+ if (bytes > maxSetMb * MB) return 0;
1245
+ await storage.writeItem(`${dirOf(jobId)}input.json`, json);
1246
+ return bytes;
1247
+ },
1248
+ /**
1249
+ * The input `keepInput` kept for `jobId`, or `undefined` when there is none.
1250
+ */
1251
+ async readInput(jobId) {
1252
+ const stored = await storage.readItem(`${dirOf(jobId)}input.json`);
1253
+ return stored === null ? void 0 : JSON.parse(stored);
1254
+ },
1211
1255
  get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId && isLive(generation)),
1212
1256
  async add(generation) {
1213
1257
  const current = await load();
@@ -1431,7 +1475,6 @@ function createGenerationStream(hooks, jobId) {
1431
1475
  return {
1432
1476
  stream,
1433
1477
  close,
1434
- dispose: () => fail(),
1435
1478
  fail
1436
1479
  };
1437
1480
  }
@@ -1564,29 +1607,29 @@ var Agent = class extends RpcTarget {
1564
1607
  });
1565
1608
  throw new Error(message);
1566
1609
  }
1567
- async #command({ command, run }) {
1610
+ #assertConnected() {
1568
1611
  if (!this.#session.info) throw new Error("Studio must call connect() before any other command");
1569
- const { value, info } = await run();
1570
- if (info !== void 0) await emit(this.#options.hooks, "studio:command", {
1612
+ }
1613
+ #logCommand(command, info) {
1614
+ return emit(this.#options.hooks, "studio:command", {
1571
1615
  command,
1572
1616
  info
1573
1617
  });
1574
- return value;
1575
1618
  }
1576
- async #readConfigFileView() {
1619
+ async #readConfigFileView(source) {
1577
1620
  if (!this.#permissions.allowConfigEdit) return void 0;
1578
1621
  try {
1579
- return readConfig(await read(this.#options.configFile));
1622
+ return readConfig(await source);
1580
1623
  } catch (error) {
1581
1624
  await this.#warn({ message: `Could not read ${this.#options.configFile}: ${getErrorMessage(error)}` });
1582
1625
  return;
1583
1626
  }
1584
1627
  }
1585
- async #installedExtensions() {
1586
- const source = await read(this.#options.configFile).catch(() => "");
1628
+ async #installedExtensions(source = read(this.#options.configFile)) {
1629
+ const imports = await source.then(readImports, () => []);
1587
1630
  const installed = await readInstalledPackages({
1588
1631
  root: this.#options.root,
1589
- packages: [...readImports(source), ...BUILT_IN_PACKAGES],
1632
+ packages: [...imports, ...BUILT_IN_PACKAGES],
1590
1633
  resolveFrom: ["kubb"]
1591
1634
  });
1592
1635
  return new Map(installed.flatMap(({ package: name, version, manifest }) => version !== void 0 && manifest ? [[name, {
@@ -1597,11 +1640,12 @@ var Agent = class extends RpcTarget {
1597
1640
  }
1598
1641
  async connect(info) {
1599
1642
  this.#session.info = info;
1600
- const { configPath, root, version: version$1, loadConfig } = this.#options;
1643
+ const { configPath, root, version: version$1, loadConfig, configFile } = this.#options;
1644
+ const source = read(configFile);
1601
1645
  const [config, file, extensions] = await Promise.all([
1602
1646
  loadConfig(),
1603
- this.#readConfigFileView(),
1604
- this.#installedExtensions()
1647
+ this.#readConfigFileView(source),
1648
+ this.#installedExtensions(source)
1605
1649
  ]);
1606
1650
  this.#session.connected.resolve(info);
1607
1651
  return {
@@ -1654,11 +1698,13 @@ var Agent = class extends RpcTarget {
1654
1698
  result,
1655
1699
  stop: () => {
1656
1700
  controller.abort(/* @__PURE__ */ new Error("Generation canceled"));
1657
- stream.dispose();
1701
+ stream.fail();
1658
1702
  }
1659
1703
  });
1660
1704
  }
1705
+ /** Runs one job and keeps its output, and its input with `allowRead`, for later reads. */
1661
1706
  async #runGeneration(input, controller) {
1707
+ this.#assertConnected();
1662
1708
  if (this.#session.closed) throw new Error("Connection is closed");
1663
1709
  if (this.#session.active) return this.#refuse({ message: "A generation is already in progress, please wait for it to finish" });
1664
1710
  const settled = Promise.withResolvers();
@@ -1670,13 +1716,10 @@ var Agent = class extends RpcTarget {
1670
1716
  let root = this.#options.root;
1671
1717
  try {
1672
1718
  if (this.#sandbox) root = await mkdtemp(path.join(tmpdir(), "kubb-job-"));
1673
- return await this.#command({
1674
- command: "generate",
1675
- run: () => this.#generate({
1676
- input,
1677
- root,
1678
- signal: controller.signal
1679
- })
1719
+ return await this.#generate({
1720
+ input,
1721
+ root,
1722
+ signal: controller.signal
1680
1723
  });
1681
1724
  } finally {
1682
1725
  if (root !== this.#options.root) await Promise.all([rm(root, {
@@ -1747,37 +1790,36 @@ var Agent = class extends RpcTarget {
1747
1790
  files,
1748
1791
  maxSetMb: this.#limits.maxMb
1749
1792
  });
1793
+ const inputBytes = await this.#generations.keepInput({
1794
+ jobId,
1795
+ input: this.#permissions.allowRead ? files.input : null,
1796
+ maxSetMb: this.#limits.maxMb
1797
+ });
1750
1798
  if (!output.paths.length && Object.keys(output.hashes).length) await this.#warn({ message: "Kept only the hashes of this generation: its output is too large to keep" });
1751
1799
  await this.#generations.add({
1752
1800
  jobId,
1753
1801
  output,
1754
- disk
1802
+ disk,
1803
+ inputBytes
1755
1804
  });
1805
+ await this.#logCommand("generate", `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "in memory"}${studioInput !== void 0 ? ", from a Studio spec" : ""}`);
1756
1806
  return {
1757
- value: {
1758
- status: "success",
1759
- files: [...files.paths],
1760
- fileCount: files.paths.size,
1761
- hashes: output.hashes,
1762
- disk: disk ? { hashes: disk.hashes } : void 0
1763
- },
1764
- info: `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "in memory"}${studioInput !== void 0 ? ", from a Studio spec" : ""}`
1807
+ status: "success",
1808
+ files: [...files.paths],
1809
+ fileCount: files.paths.size,
1810
+ hashes: output.hashes,
1811
+ disk: disk ? { hashes: disk.hashes } : void 0
1765
1812
  };
1766
1813
  }
1767
- saveConfig(input) {
1768
- return this.#command({
1769
- command: "saveConfig",
1770
- run: () => this.#saveConfig(input)
1771
- });
1772
- }
1773
- async #saveConfig(input) {
1814
+ async saveConfig(input) {
1815
+ this.#assertConnected();
1774
1816
  const { configPath, configFile } = this.#options;
1775
1817
  if (!Array.isArray(input.edits)) {
1776
1818
  await this.#warn({ message: "Ignored save: the message carried no edits" });
1777
- return { value: {
1819
+ return {
1778
1820
  outcomes: [],
1779
1821
  changed: false
1780
- } };
1822
+ };
1781
1823
  }
1782
1824
  const edits = input.edits;
1783
1825
  const refuse = (reason) => ({
@@ -1790,9 +1832,9 @@ var Agent = class extends RpcTarget {
1790
1832
  });
1791
1833
  if (!this.#permissions.allowConfigEdit) {
1792
1834
  await this.#warn({ message: "Ignored save: editing kubb.config.ts was not granted" });
1793
- return { value: refuse("the agent was not granted permission to edit kubb.config.ts") };
1835
+ return refuse("the agent was not granted permission to edit kubb.config.ts");
1794
1836
  }
1795
- if (this.#session.active) return { value: refuse("a generation is in progress") };
1837
+ if (this.#session.active) return refuse("a generation is in progress");
1796
1838
  try {
1797
1839
  const { outcomes, changed } = await writeConfigEdits({
1798
1840
  filePath: configFile,
@@ -1800,27 +1842,40 @@ var Agent = class extends RpcTarget {
1800
1842
  extensions: await this.#installedExtensions()
1801
1843
  });
1802
1844
  const applied = outcomes.filter((outcome) => outcome.applied).length;
1845
+ await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}`);
1803
1846
  return {
1804
- value: {
1805
- outcomes,
1806
- changed
1807
- },
1808
- info: `applied ${applied}/${outcomes.length} edits to ${configPath}`
1847
+ outcomes,
1848
+ changed
1809
1849
  };
1810
1850
  } catch (error) {
1811
1851
  await emit(this.#options.hooks, "studio:error", { error: toError(error) });
1812
- return { value: refuse(getErrorMessage(error)) };
1852
+ return refuse(getErrorMessage(error));
1813
1853
  }
1814
1854
  }
1815
- readFiles(input) {
1816
- return this.#command({
1817
- command: "readFiles",
1818
- run: () => this.#readFiles(input)
1855
+ /**
1856
+ * Returns the Kubb AST a kept job parsed its spec into. Refused without `allowRead`.
1857
+ */
1858
+ async readInput(input) {
1859
+ this.#assertConnected();
1860
+ if (!this.#permissions.allowRead) return this.#refuse({
1861
+ message: READ_NOT_ALLOWED_MESSAGE,
1862
+ permission: "allowRead"
1819
1863
  });
1864
+ if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job" });
1865
+ const generation = await this.#generations.get(input.jobId);
1866
+ if (!generation) return this.#refuse({ message: GENERATION_GONE_MESSAGE });
1867
+ const kept = generation.inputBytes ? await this.#generations.readInput(input.jobId) : void 0;
1868
+ if (!kept) return this.#refuse({ message: NO_INPUT_MESSAGE });
1869
+ await this.#logCommand("readInput", "read the API input");
1870
+ return { input: kept };
1820
1871
  }
1821
- async #readFiles(input) {
1872
+ /**
1873
+ * Returns the requested files of a kept job, one page of lines at a time. Refused without `allowRead`.
1874
+ */
1875
+ async readFiles(input) {
1876
+ this.#assertConnected();
1822
1877
  if (!this.#permissions.allowRead) return this.#refuse({
1823
- message: "The agent was not granted permission to read generated files",
1878
+ message: READ_NOT_ALLOWED_MESSAGE,
1824
1879
  permission: "allowRead"
1825
1880
  });
1826
1881
  if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job and carry paths" });
@@ -1838,10 +1893,8 @@ var Agent = class extends RpcTarget {
1838
1893
  cursor: input.cursor,
1839
1894
  limit: input.limit
1840
1895
  });
1841
- return {
1842
- value: result,
1843
- info: `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`
1844
- };
1896
+ await this.#logCommand("readFiles", `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`);
1897
+ return result;
1845
1898
  }
1846
1899
  };
1847
1900
  const openWebSocketRpc = ({ url, token, instanceId, agent }) => {
@@ -2132,7 +2185,6 @@ async function pairAgent({ onCode, onRetry, maxAttempts = 1, ...options }) {
2132
2185
  }
2133
2186
  //#endregion
2134
2187
  //#region src/snapshot.ts
2135
- const gzipAsync = promisify(gzip);
2136
2188
  const UPLOAD_TIMEOUT_MS = 12e4;
2137
2189
  const BUNDLED_DEPENDENCIES = ["@kubb/plugin-barrel"];
2138
2190
  /**
@@ -2252,7 +2304,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
2252
2304
  Buffer.alloc((512 - bytes.length % 512) % 512)
2253
2305
  ];
2254
2306
  });
2255
- return await gzipAsync(Buffer.concat([...chunks, Buffer.alloc(1024)]));
2307
+ return gzipSync(Buffer.concat([...chunks, Buffer.alloc(1024)]));
2256
2308
  } finally {
2257
2309
  await rm(root, {
2258
2310
  recursive: true,