@kubb/studio 5.9.2 → 5.9.4

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
@@ -12,9 +12,9 @@ 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
16
  import { ofetch } from "ofetch";
17
- import { gzip } from "node:zlib";
17
+ import { gzipSync } from "node:zlib";
18
18
  //#region ../../internals/utils/src/casing.ts
19
19
  /**
20
20
  * Shared implementation for camelCase and PascalCase conversion.
@@ -152,10 +152,18 @@ function getElapsedMs(hrStart) {
152
152
  }
153
153
  //#endregion
154
154
  //#region package.json
155
- var version = "5.9.2";
155
+ var version = "5.9.4";
156
156
  //#endregion
157
157
  //#region src/resolveConfig.ts
158
158
  /**
159
+ * Turns the JSON config Studio sends back into live Kubb objects.
160
+ *
161
+ * A plugin or adapter instance carries closures (`parse`, `getImports`, ...) that cannot survive
162
+ * JSON, so both sides pass options over the wire and the factory is re-invoked here with the merged
163
+ * result. Only packages {@link isAllowedPlugin} accepts are resolved this way, never an arbitrary
164
+ * module the payload names.
165
+ */
166
+ /**
159
167
  * Imports a package, falling back to how the user's project would resolve it.
160
168
  *
161
169
  * `import()` resolves from this file, so a linked or globally installed Studio (`pnpm link`,
@@ -234,8 +242,9 @@ function isAllowedPlugin({ packageName, extensions }) {
234
242
  return KUBB_PLUGIN_SPECIFIER.test(packageName) || extensions.get(packageName)?.manifest.kind === "plugin";
235
243
  }
236
244
  /**
237
- * Dynamically imports a plugin package and returns its factory. Only packages
238
- * {@link isAllowedPlugin} accepts reach `import()`.
245
+ * Dynamically imports an extension package and returns its factory. Callers check
246
+ * {@link isAllowedPlugin} or derive the package from a disk instance first, so an arbitrary module
247
+ * never reaches `import()`.
239
248
  *
240
249
  * Packages must be pre-installed in the Docker image at build time via the `KUBB_PACKAGES`
241
250
  * build ARG, no runtime installation is possible in the distroless container.
@@ -243,13 +252,9 @@ function isAllowedPlugin({ packageName, extensions }) {
243
252
  * Resolution order: the manifest's `export`, else the camelCase named export the package name
244
253
  * implies (e.g. `pluginTs`), then the default export.
245
254
  *
246
- * @throws if the package is not allowed, cannot be imported, or exports no callable factory.
255
+ * @throws if the package cannot be imported or exports no callable factory.
247
256
  */
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".`);
257
+ async function loadFactory({ packageName, extensions }) {
253
258
  let mod;
254
259
  try {
255
260
  mod = await importFromProject(packageName);
@@ -268,7 +273,7 @@ async function loadPluginFactory({ packageName, extensions }) {
268
273
  }
269
274
  /**
270
275
  * 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.
276
+ * provided options. Only packages {@link isAllowedPlugin} accepts are loaded.
272
277
  *
273
278
  * @example
274
279
  * ```ts
@@ -282,7 +287,11 @@ async function loadPluginFactory({ packageName, extensions }) {
282
287
  */
283
288
  async function resolvePlugins({ plugins, extensions = /* @__PURE__ */ new Map() }) {
284
289
  return Promise.all(plugins.map(async ({ name, options }) => {
285
- return (await loadPluginFactory({
290
+ if (!isAllowedPlugin({
291
+ packageName: name,
292
+ extensions
293
+ })) 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".`);
294
+ return (await loadFactory({
286
295
  packageName: name,
287
296
  extensions
288
297
  }))(options ?? {});
@@ -430,13 +439,12 @@ async function mergePlugins({ diskPlugins, studioPlugins, extensions = /* @__PUR
430
439
  async function rebuildExtension({ disk, studioOptions, kind, extensions }) {
431
440
  if (!studioOptions || !Object.keys(studioOptions).length) return disk;
432
441
  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) => {
442
+ return (await loadFactory({
443
+ packageName: [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name)?.package ?? `@kubb/${kind}-${name}`,
444
+ extensions
445
+ }).catch((cause) => {
436
446
  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));
447
+ }))(mergeOptions(disk.options ?? {}, studioOptions));
440
448
  }
441
449
  /**
442
450
  * Merges Studio-provided adapter option overrides into the disk config's adapter, see {@link rebuildExtension}.
@@ -1431,7 +1439,6 @@ function createGenerationStream(hooks, jobId) {
1431
1439
  return {
1432
1440
  stream,
1433
1441
  close,
1434
- dispose: () => fail(),
1435
1442
  fail
1436
1443
  };
1437
1444
  }
@@ -1564,29 +1571,29 @@ var Agent = class extends RpcTarget {
1564
1571
  });
1565
1572
  throw new Error(message);
1566
1573
  }
1567
- async #command({ command, run }) {
1574
+ #assertConnected() {
1568
1575
  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", {
1576
+ }
1577
+ #logCommand(command, info) {
1578
+ return emit(this.#options.hooks, "studio:command", {
1571
1579
  command,
1572
1580
  info
1573
1581
  });
1574
- return value;
1575
1582
  }
1576
- async #readConfigFileView() {
1583
+ async #readConfigFileView(source) {
1577
1584
  if (!this.#permissions.allowConfigEdit) return void 0;
1578
1585
  try {
1579
- return readConfig(await read(this.#options.configFile));
1586
+ return readConfig(await source);
1580
1587
  } catch (error) {
1581
1588
  await this.#warn({ message: `Could not read ${this.#options.configFile}: ${getErrorMessage(error)}` });
1582
1589
  return;
1583
1590
  }
1584
1591
  }
1585
- async #installedExtensions() {
1586
- const source = await read(this.#options.configFile).catch(() => "");
1592
+ async #installedExtensions(source = read(this.#options.configFile)) {
1593
+ const imports = await source.then(readImports, () => []);
1587
1594
  const installed = await readInstalledPackages({
1588
1595
  root: this.#options.root,
1589
- packages: [...readImports(source), ...BUILT_IN_PACKAGES],
1596
+ packages: [...imports, ...BUILT_IN_PACKAGES],
1590
1597
  resolveFrom: ["kubb"]
1591
1598
  });
1592
1599
  return new Map(installed.flatMap(({ package: name, version, manifest }) => version !== void 0 && manifest ? [[name, {
@@ -1597,11 +1604,12 @@ var Agent = class extends RpcTarget {
1597
1604
  }
1598
1605
  async connect(info) {
1599
1606
  this.#session.info = info;
1600
- const { configPath, root, version: version$1, loadConfig } = this.#options;
1607
+ const { configPath, root, version: version$1, loadConfig, configFile } = this.#options;
1608
+ const source = read(configFile);
1601
1609
  const [config, file, extensions] = await Promise.all([
1602
1610
  loadConfig(),
1603
- this.#readConfigFileView(),
1604
- this.#installedExtensions()
1611
+ this.#readConfigFileView(source),
1612
+ this.#installedExtensions(source)
1605
1613
  ]);
1606
1614
  this.#session.connected.resolve(info);
1607
1615
  return {
@@ -1654,11 +1662,12 @@ var Agent = class extends RpcTarget {
1654
1662
  result,
1655
1663
  stop: () => {
1656
1664
  controller.abort(/* @__PURE__ */ new Error("Generation canceled"));
1657
- stream.dispose();
1665
+ stream.fail();
1658
1666
  }
1659
1667
  });
1660
1668
  }
1661
1669
  async #runGeneration(input, controller) {
1670
+ this.#assertConnected();
1662
1671
  if (this.#session.closed) throw new Error("Connection is closed");
1663
1672
  if (this.#session.active) return this.#refuse({ message: "A generation is already in progress, please wait for it to finish" });
1664
1673
  const settled = Promise.withResolvers();
@@ -1670,13 +1679,10 @@ var Agent = class extends RpcTarget {
1670
1679
  let root = this.#options.root;
1671
1680
  try {
1672
1681
  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
- })
1682
+ return await this.#generate({
1683
+ input,
1684
+ root,
1685
+ signal: controller.signal
1680
1686
  });
1681
1687
  } finally {
1682
1688
  if (root !== this.#options.root) await Promise.all([rm(root, {
@@ -1753,31 +1759,24 @@ var Agent = class extends RpcTarget {
1753
1759
  output,
1754
1760
  disk
1755
1761
  });
1762
+ 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
1763
  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" : ""}`
1764
+ status: "success",
1765
+ files: [...files.paths],
1766
+ fileCount: files.paths.size,
1767
+ hashes: output.hashes,
1768
+ disk: disk ? { hashes: disk.hashes } : void 0
1765
1769
  };
1766
1770
  }
1767
- saveConfig(input) {
1768
- return this.#command({
1769
- command: "saveConfig",
1770
- run: () => this.#saveConfig(input)
1771
- });
1772
- }
1773
- async #saveConfig(input) {
1771
+ async saveConfig(input) {
1772
+ this.#assertConnected();
1774
1773
  const { configPath, configFile } = this.#options;
1775
1774
  if (!Array.isArray(input.edits)) {
1776
1775
  await this.#warn({ message: "Ignored save: the message carried no edits" });
1777
- return { value: {
1776
+ return {
1778
1777
  outcomes: [],
1779
1778
  changed: false
1780
- } };
1779
+ };
1781
1780
  }
1782
1781
  const edits = input.edits;
1783
1782
  const refuse = (reason) => ({
@@ -1790,9 +1789,9 @@ var Agent = class extends RpcTarget {
1790
1789
  });
1791
1790
  if (!this.#permissions.allowConfigEdit) {
1792
1791
  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") };
1792
+ return refuse("the agent was not granted permission to edit kubb.config.ts");
1794
1793
  }
1795
- if (this.#session.active) return { value: refuse("a generation is in progress") };
1794
+ if (this.#session.active) return refuse("a generation is in progress");
1796
1795
  try {
1797
1796
  const { outcomes, changed } = await writeConfigEdits({
1798
1797
  filePath: configFile,
@@ -1800,25 +1799,18 @@ var Agent = class extends RpcTarget {
1800
1799
  extensions: await this.#installedExtensions()
1801
1800
  });
1802
1801
  const applied = outcomes.filter((outcome) => outcome.applied).length;
1802
+ await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}`);
1803
1803
  return {
1804
- value: {
1805
- outcomes,
1806
- changed
1807
- },
1808
- info: `applied ${applied}/${outcomes.length} edits to ${configPath}`
1804
+ outcomes,
1805
+ changed
1809
1806
  };
1810
1807
  } catch (error) {
1811
1808
  await emit(this.#options.hooks, "studio:error", { error: toError(error) });
1812
- return { value: refuse(getErrorMessage(error)) };
1809
+ return refuse(getErrorMessage(error));
1813
1810
  }
1814
1811
  }
1815
- readFiles(input) {
1816
- return this.#command({
1817
- command: "readFiles",
1818
- run: () => this.#readFiles(input)
1819
- });
1820
- }
1821
- async #readFiles(input) {
1812
+ async readFiles(input) {
1813
+ this.#assertConnected();
1822
1814
  if (!this.#permissions.allowRead) return this.#refuse({
1823
1815
  message: "The agent was not granted permission to read generated files",
1824
1816
  permission: "allowRead"
@@ -1838,10 +1830,8 @@ var Agent = class extends RpcTarget {
1838
1830
  cursor: input.cursor,
1839
1831
  limit: input.limit
1840
1832
  });
1841
- return {
1842
- value: result,
1843
- info: `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`
1844
- };
1833
+ await this.#logCommand("readFiles", `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`);
1834
+ return result;
1845
1835
  }
1846
1836
  };
1847
1837
  const openWebSocketRpc = ({ url, token, instanceId, agent }) => {
@@ -2132,7 +2122,6 @@ async function pairAgent({ onCode, onRetry, maxAttempts = 1, ...options }) {
2132
2122
  }
2133
2123
  //#endregion
2134
2124
  //#region src/snapshot.ts
2135
- const gzipAsync = promisify(gzip);
2136
2125
  const UPLOAD_TIMEOUT_MS = 12e4;
2137
2126
  const BUNDLED_DEPENDENCIES = ["@kubb/plugin-barrel"];
2138
2127
  /**
@@ -2252,7 +2241,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
2252
2241
  Buffer.alloc((512 - bytes.length % 512) % 512)
2253
2242
  ];
2254
2243
  });
2255
- return await gzipAsync(Buffer.concat([...chunks, Buffer.alloc(1024)]));
2244
+ return gzipSync(Buffer.concat([...chunks, Buffer.alloc(1024)]));
2256
2245
  } finally {
2257
2246
  await rm(root, {
2258
2247
  recursive: true,