@kubb/studio 5.9.0 → 5.9.2

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 { M as PublishedSnapshot, V as SnapshotUploadMetadata, a as AgentConnectInfo, c as ConfigEdit, l as ConfigEditOutcome, m as ExtensionEntry, o as AgentPermissions } from "./protocol-BfjNY8-8.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";
3
3
  import { Config, Hookable, KubbHooks } from "@kubb/core";
4
4
  //#region src/connect.d.ts
5
5
  /**
@@ -242,13 +242,6 @@ export declare function createSnapshot({ studioUrl, apiKey, config, hooks, signa
242
242
  */
243
243
  export declare const defaultStudioUrl = "https://kubb.studio";
244
244
  //#endregion
245
- //#region src/extensions.d.ts
246
- /**
247
- * The extension packages installed in a project, keyed by package name. Only these, besides
248
- * `@kubb/plugin-*` packages, may be imported on behalf of Studio.
249
- */
250
- type InstalledExtensions = ReadonlyMap<string, ExtensionEntry>;
251
- //#endregion
252
245
  //#region src/configFile.d.ts
253
246
  /**
254
247
  * Applies `edits` to the config file at `filePath`, writing it only when something changed.
@@ -264,5 +257,5 @@ export declare function writeConfigEdits({ filePath, edits, extensions }: {
264
257
  changed: boolean;
265
258
  }>;
266
259
  //#endregion
267
- export type { ConnectAgentOptions, InstalledExtensions, StudioConnectedContext };
260
+ export type { ConnectAgentOptions, StudioConnectedContext };
268
261
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -71,74 +71,34 @@ function getErrorMessage(value) {
71
71
  }
72
72
  //#endregion
73
73
  //#region ../../internals/utils/src/runtime.ts
74
+ const isBun = typeof Bun !== "undefined";
75
+ const isDeno = "Deno" in globalThis;
74
76
  /**
75
- * Detects the JavaScript runtime executing the current process and exposes its name and version.
77
+ * The JavaScript runtime executing the current process: its name, version and whether it is Bun.
76
78
  *
77
- * Prefer the shared {@link runtime} instance over constructing your own.
79
+ * @example
80
+ * ```ts
81
+ * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
82
+ * ```
78
83
  */
79
- var Runtime = class {
80
- /**
81
- * `true` when the current process is running under Bun.
82
- *
83
- * Detection keys off the global `Bun` object rather than `process.versions`,
84
- * because Bun polyfills `process.versions.node` for Node compatibility and would
85
- * otherwise look like Node.
86
- *
87
- * @example
88
- * ```ts
89
- * if (runtime.isBun) {
90
- * await Bun.write(path, data)
91
- * }
92
- * ```
93
- */
94
- get isBun() {
95
- return typeof Bun !== "undefined";
96
- }
97
- /**
98
- * `true` when the current process is running under Deno.
99
- */
100
- get isDeno() {
101
- return typeof globalThis.Deno !== "undefined";
102
- }
103
- /**
104
- * `true` when the current process is running under Node.
105
- *
106
- * Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
107
- */
108
- get isNode() {
109
- return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
110
- }
111
- /**
112
- * Name of the runtime executing the current process.
113
- *
114
- * @example
115
- * ```ts
116
- * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
117
- * ```
118
- */
119
- get name() {
120
- if (this.isBun) return "bun";
121
- if (this.isDeno) return "deno";
122
- return "node";
123
- }
124
- /**
125
- * Version of the active runtime, or an empty string when it cannot be read.
126
- *
127
- * @example
128
- * ```ts
129
- * runtime.version // '1.3.11' under Bun, '22.22.2' under Node
130
- * ```
131
- */
132
- get version() {
133
- if (this.isBun) return process.versions.bun ?? "";
134
- if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
135
- return process.versions?.node ?? "";
136
- }
84
+ const runtime = {
85
+ isBun,
86
+ ...detect()
137
87
  };
138
- /**
139
- * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
140
- */
141
- const runtime = new Runtime();
88
+ function detect() {
89
+ if (isBun) return {
90
+ name: "bun",
91
+ version: process.versions.bun ?? ""
92
+ };
93
+ if (isDeno) return {
94
+ name: "deno",
95
+ version: globalThis.Deno?.version?.deno ?? ""
96
+ };
97
+ return {
98
+ name: "node",
99
+ version: process.versions?.node ?? ""
100
+ };
101
+ }
142
102
  //#endregion
143
103
  //#region ../../internals/utils/src/fs.ts
144
104
  /**
@@ -192,7 +152,7 @@ function getElapsedMs(hrStart) {
192
152
  }
193
153
  //#endregion
194
154
  //#region package.json
195
- var version = "5.9.0";
155
+ var version = "5.9.2";
196
156
  //#endregion
197
157
  //#region src/resolveConfig.ts
198
158
  /**
@@ -237,47 +197,19 @@ function toPluginName(packageName) {
237
197
  function toPackageName(name) {
238
198
  return name.startsWith("plugin-") ? `@kubb/${name}` : name;
239
199
  }
240
- function resolvesFrom(base, packageName) {
241
- try {
242
- createRequire(base).resolve(`${packageName}/package.json`);
243
- return true;
244
- } catch {
245
- return false;
246
- }
247
- }
248
200
  /**
249
- * Whether `packageName` resolves from the project at `root`, from this package, or from the `kubb`
250
- * package the project installs, which brings the plugins Kubb adds on its own such as
251
- * `@kubb/plugin-barrel`. Only resolves the package, it never runs it.
252
- */
253
- function isInstalled({ packageName, root }) {
254
- const project = pathToFileURL(`${root}/`);
255
- if (resolvesFrom(project, packageName) || resolvesFrom(import.meta.url, packageName)) return true;
256
- try {
257
- return resolvesFrom(createRequire(project).resolve("kubb/package.json"), packageName);
258
- } catch {
259
- return false;
260
- }
261
- }
262
- /**
263
- * The name Studio knows a plugin by: its `@kubb/plugin-*` package when one is installed, otherwise
264
- * the plugin's own `name`, so a plugin defined in the user's project is never reported as a Kubb
265
- * package that does not exist. An installed extension whose manifest `name` matches wins.
201
+ * The name Studio knows an extension by: the package of the installed extension whose manifest
202
+ * `name` matches, otherwise its own `name`, so a plugin defined in the user's project is never
203
+ * reported as a Kubb package that does not exist.
266
204
  *
267
205
  * @example
268
206
  * ```ts
269
- * toStudioName({ name: 'plugin-ts', root }) // '@kubb/plugin-ts'
270
- * toStudioName({ name: 'plugin-zod-operations', root }) // 'plugin-zod-operations', a local plugin
207
+ * toStudioName({ name: 'plugin-ts', extensions }) // '@kubb/plugin-ts'
208
+ * toStudioName({ name: 'plugin-zod-operations', extensions }) // 'plugin-zod-operations', a local plugin
271
209
  * ```
272
210
  */
273
- function toStudioName({ name, root, extensions = /* @__PURE__ */ new Map() }) {
274
- const installed = [...extensions.values()].find(({ manifest }) => manifest.kind === "plugin" && manifest.name === name);
275
- if (installed) return installed.package;
276
- const packageName = toPackageName(name);
277
- return packageName !== name && isInstalled({
278
- packageName,
279
- root
280
- }) ? packageName : name;
211
+ function toStudioName({ name, extensions = /* @__PURE__ */ new Map(), kind = "plugin" }) {
212
+ return [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name)?.package ?? name;
281
213
  }
282
214
  /**
283
215
  * Derives the conventional named export for a `@kubb/*` plugin package from its package name.
@@ -356,33 +288,6 @@ async function resolvePlugins({ plugins, extensions = /* @__PURE__ */ new Map()
356
288
  }))(options ?? {});
357
289
  }));
358
290
  }
359
- /** The installed version of each plugin's package, plus the names that could not be resolved. */
360
- async function resolvePeerDependencies(names) {
361
- const require = createRequire(import.meta.url);
362
- const uniqueNames = [...new Set(names.map(toPackageName))];
363
- const peerDependencies = {};
364
- const missingDependencies = [];
365
- const versions = await Promise.all(uniqueNames.map(async (name) => {
366
- try {
367
- const path = require.resolve(`${name}/package.json`);
368
- return JSON.parse(await readFile(path, "utf8")).version;
369
- } catch {
370
- return;
371
- }
372
- }));
373
- for (const [index, name] of uniqueNames.entries()) {
374
- const version = versions[index];
375
- if (version) {
376
- peerDependencies[name] = version;
377
- continue;
378
- }
379
- missingDependencies.push(name);
380
- }
381
- return {
382
- peerDependencies,
383
- missingDependencies
384
- };
385
- }
386
291
  /**
387
292
  * Keys that reach an object's prototype when assigned. Never read from or written into options.
388
293
  */
@@ -445,6 +350,7 @@ function isOptionValue(value) {
445
350
  * ```ts
446
351
  * mergeOptions({ macros: [camelCaseMacro], syntaxType: 'type' }, { macros: [{ name: 'camelCase' }], syntaxType: 'interface' })
447
352
  * // { macros: [camelCaseMacro], syntaxType: 'interface' }
353
+ * mergeOptions({ emptySchemaType: 'any' }, { emptySchemaType: null }) // {}
448
354
  * ```
449
355
  */
450
356
  function mergeOptions(disk, studio) {
@@ -452,6 +358,10 @@ function mergeOptions(disk, studio) {
452
358
  for (const [key, value] of Object.entries(studio)) {
453
359
  if (!isSafeKey(key)) continue;
454
360
  const current = disk[key];
361
+ if (value === null) {
362
+ if (current === void 0 || isOptionValue(current)) delete merged[key];
363
+ continue;
364
+ }
455
365
  if (isPlainObject(current) && isPlainObject(value)) {
456
366
  merged[key] = mergeOptions(current, value);
457
367
  continue;
@@ -509,20 +419,54 @@ async function mergePlugins({ diskPlugins, studioPlugins, extensions = /* @__PUR
509
419
  })];
510
420
  }
511
421
  /**
512
- * Merges Studio-provided adapter option overrides into the disk config's adapter.
513
- *
514
- * Adapter instances carry live functions (`parse`, `getImports`, ...) that can't survive
515
- * JSON serialization over the WebSocket, so `studioOptions` is treated as an options patch
516
- * rather than a replacement adapter. Re-invokes the same `@kubb/adapter-<name>` factory the
517
- * disk config used, with the merged options, so the resulting instance has fresh closures
518
- * over the merged values instead of a plain object missing `parse`.
422
+ * Re-invokes the factory behind a disk adapter or parser with Studio's options merged over its own.
423
+ * An instance carries live functions (`parse`, `print`, ...) that cannot survive JSON, so Studio's
424
+ * options are a patch, not a replacement, and the factory is found from the disk instance itself: the
425
+ * installed extension whose manifest names it, or the `@kubb/<kind>-<name>` package for a built-in
426
+ * one. Throws when no factory resolves, as `mergePlugins` does, so an override Studio sent is never
427
+ * dropped without a word. An extension without a manifest, such as `parserTsx`, has no options in
428
+ * Studio, so Studio never sends one for it.
429
+ */
430
+ async function rebuildExtension({ disk, studioOptions, kind, extensions }) {
431
+ if (!studioOptions || !Object.keys(studioOptions).length) return disk;
432
+ 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) => {
436
+ 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));
440
+ }
441
+ /**
442
+ * Merges Studio-provided adapter option overrides into the disk config's adapter, see {@link rebuildExtension}.
519
443
  */
520
- async function mergeAdapter(diskAdapter, studioOptions) {
521
- if (!studioOptions || !diskAdapter) return diskAdapter;
522
- const packageName = `@kubb/adapter-${diskAdapter.name}`;
523
- const factory = (await importFromProject(packageName))[toExportName(packageName)];
524
- if (typeof factory !== "function") return diskAdapter;
525
- return factory(mergeOptions(diskAdapter.options ?? {}, studioOptions));
444
+ async function mergeAdapter(diskAdapter, studioOptions, extensions = /* @__PURE__ */ new Map()) {
445
+ if (!diskAdapter) return diskAdapter;
446
+ return rebuildExtension({
447
+ disk: diskAdapter,
448
+ studioOptions,
449
+ kind: "adapter",
450
+ extensions
451
+ });
452
+ }
453
+ /**
454
+ * Merges Studio-provided parser option overrides into the disk config's parsers, matched by the name
455
+ * Studio knows each parser by, see {@link rebuildExtension}. Parsers Studio does not name are kept as is.
456
+ */
457
+ async function mergeParsers({ diskParsers, studioParsers, extensions = /* @__PURE__ */ new Map() }) {
458
+ if (!studioParsers?.length) return diskParsers;
459
+ const studioOptions = new Map(studioParsers.map((entry) => [entry.name, entry.options]));
460
+ return Promise.all(diskParsers.map((parser) => rebuildExtension({
461
+ disk: parser,
462
+ studioOptions: studioOptions.get(toStudioName({
463
+ name: resolveMeta(parser).name,
464
+ extensions,
465
+ kind: "parser"
466
+ })),
467
+ kind: "parser",
468
+ extensions
469
+ })));
526
470
  }
527
471
  //#endregion
528
472
  //#region src/configFile.ts
@@ -543,14 +487,21 @@ const OPERATIONS = /* @__PURE__ */ new Set([
543
487
  "disable-plugin",
544
488
  "enable-plugin"
545
489
  ]);
490
+ const KINDS = /* @__PURE__ */ new Set([
491
+ "plugin",
492
+ "adapter",
493
+ "parser"
494
+ ]);
546
495
  /**
547
496
  * Why an edit from Studio cannot be applied as sent, checked before anything reads its fields.
548
497
  * The edit crossed the wire, so its declared type is a promise the sender may not have kept.
549
498
  */
550
499
  function invalidEdit(edit) {
551
- const { operation, plugin, config, path, importName } = typeof edit === "object" && edit !== null ? edit : {};
500
+ const { operation, plugin, config, path, kind } = typeof edit === "object" && edit !== null ? edit : {};
552
501
  if (typeof operation !== "string" || !OPERATIONS.has(operation)) return `unknown operation ${JSON.stringify(operation)}`;
553
502
  if (typeof plugin !== "string" || !plugin) return "the edit names no plugin";
503
+ if (kind !== void 0 && (typeof kind !== "string" || !KINDS.has(kind))) return `unknown kind ${JSON.stringify(kind)}`;
504
+ if (kind !== void 0 && operation !== "set" && operation !== "remove") return `${operation} edits the plugins array, so it takes no kind`;
554
505
  if (config !== void 0 && typeof config !== "string" && !Number.isSafeInteger(config)) return "config must name an entry or give its index";
555
506
  if (operation === "set" || operation === "remove") {
556
507
  if (!Array.isArray(path) || !path.every((key) => typeof key === "string" && key)) return "path must be a list of option names";
@@ -558,7 +509,6 @@ function invalidEdit(edit) {
558
509
  const unsafe = path.find((key) => !isSafeKey(key));
559
510
  if (unsafe !== void 0) return `${unsafe} cannot be used as an option name`;
560
511
  }
561
- if (operation === "add-plugin" && importName !== void 0 && typeof importName !== "string") return "importName must be a string";
562
512
  }
563
513
  /** Where a node sits in the source text. */
564
514
  function rangeOf(node) {
@@ -753,29 +703,42 @@ function importedFrom(mod) {
753
703
  const imports = /* @__PURE__ */ new Map();
754
704
  for (const statement of bodyOf(mod)) {
755
705
  if (statement.type === "ImportDeclaration") {
756
- for (const specifier of statement.specifiers) imports.set(specifier.local.name, statement.source.value);
706
+ for (const specifier of statement.specifiers) {
707
+ const imported = specifier.type === "ImportSpecifier" ? specifier.imported : specifier.local;
708
+ imports.set(specifier.local.name, {
709
+ from: statement.source.value,
710
+ exported: imported.type === "Identifier" ? imported.name : imported.value
711
+ });
712
+ }
757
713
  continue;
758
714
  }
759
715
  if (statement.type !== "VariableDeclaration") continue;
760
716
  for (const declarator of statement.declarations) {
761
717
  const from = requiredModule(declarator.init);
762
718
  if (from === void 0 || declarator.id.type !== "ObjectPattern") continue;
763
- for (const entry of declarator.id.properties) if (entry.type === "ObjectProperty" && entry.value.type === "Identifier") imports.set(entry.value.name, from);
719
+ for (const entry of declarator.id.properties) if (entry.type === "ObjectProperty" && entry.value.type === "Identifier") imports.set(entry.value.name, {
720
+ from,
721
+ exported: memberKey(entry) ?? entry.value.name
722
+ });
764
723
  }
765
724
  }
766
725
  return imports;
767
726
  }
768
- /** Every `pluginX(...)` element of a config's plugins array that resolves to an import. */
769
- function pluginCalls(mod, config) {
770
- const plugins = property(config, "plugins");
771
- if (plugins?.type !== "ArrayExpression") return [];
727
+ /**
728
+ * Every factory call of one kind that resolves to an import: the `pluginX(...)` elements of the
729
+ * `plugins` array, the `adapter` value, or the elements of the `parsers` array.
730
+ */
731
+ function factoryCalls(mod, config, kind = "plugin") {
732
+ const node = property(config, kind === "adapter" ? "adapter" : `${kind}s`);
733
+ const elements = kind === "adapter" ? [node] : node?.type === "ArrayExpression" ? node.elements : [];
772
734
  const imports = importedFrom(mod);
773
- return plugins.elements.flatMap((element) => {
735
+ return elements.flatMap((element) => {
774
736
  if (element?.type !== "CallExpression" || element.callee.type !== "Identifier") return [];
775
- const packageName = imports.get(element.callee.name);
776
- return packageName ? [{
737
+ const imported = imports.get(element.callee.name);
738
+ return imported ? [{
777
739
  importName: element.callee.name,
778
- packageName,
740
+ exportName: imported.exported,
741
+ packageName: imported.from,
779
742
  call: element
780
743
  }] : [];
781
744
  });
@@ -844,11 +807,11 @@ function readConfig(source) {
844
807
  reason: parsed.reason
845
808
  };
846
809
  const { mod, configs } = parsed;
847
- const importNames = new Map([...importedFrom(mod)].map(([local, from]) => [from, local]));
810
+ const importNames = new Map([...importedFrom(mod)].map(([local, { from }]) => [from, local]));
848
811
  return {
849
812
  managed: true,
850
813
  configs: configs.map((config) => {
851
- const active = pluginCalls(mod, config).map(({ importName, packageName, call }) => ({
814
+ const active = factoryCalls(mod, config).map(({ importName, packageName, call }) => ({
852
815
  line: call.loc?.start.line ?? 0,
853
816
  plugin: {
854
817
  importName,
@@ -866,9 +829,18 @@ function readConfig(source) {
866
829
  }
867
830
  }));
868
831
  const plugins = [...active, ...disabled].sort((a, b) => a.line - b.line).map(({ plugin }) => plugin);
832
+ const toView = ({ importName, packageName, call }) => ({
833
+ importName,
834
+ packageName,
835
+ options: readOptions(call)
836
+ });
837
+ const [adapter] = factoryCalls(mod, config, "adapter").map(toView);
838
+ const parsers = factoryCalls(mod, config, "parser").map(toView);
869
839
  return {
870
840
  name: configName(config),
871
- plugins
841
+ plugins,
842
+ ...adapter && { adapter },
843
+ ...parsers.length ? { parsers } : {}
872
844
  };
873
845
  })
874
846
  };
@@ -876,7 +848,7 @@ function readConfig(source) {
876
848
  /** The modules the config file imports, in source order. Empty when the file cannot be parsed. */
877
849
  function readImports(source) {
878
850
  try {
879
- return [...new Set(importedFrom(parseModule(source)).values())];
851
+ return [...new Set([...importedFrom(parseModule(source)).values()].map(({ from }) => from))];
880
852
  } catch {
881
853
  return [];
882
854
  }
@@ -913,10 +885,10 @@ function applySet(source, call, path, value, quote) {
913
885
  const options = call.arguments[0];
914
886
  if (!options) {
915
887
  const closing = rangeOf(call).end - 1;
916
- if (source[closing] !== ")") return { reason: "the plugin call could not be located in the file" };
888
+ if (source[closing] !== ")") return { reason: "the factory call could not be located in the file" };
917
889
  return { source: splice(source, closing, closing, printValue(nest(path, value), quote)) };
918
890
  }
919
- if (options.type !== "ObjectExpression") return { reason: "the plugin was not called with an object literal" };
891
+ if (options.type !== "ObjectExpression") return { reason: "the factory was not called with an object literal" };
920
892
  const target = walk(options, path);
921
893
  if ("reason" in target) return target;
922
894
  if ("missing" in target) {
@@ -928,7 +900,7 @@ function applySet(source, call, path, value, quote) {
928
900
  /** Deletes the option at `path`, falling the plugin back to its default for it. */
929
901
  function applyRemove(source, call, path) {
930
902
  const options = call.arguments[0];
931
- if (options?.type !== "ObjectExpression") return { reason: "the plugin has no options to remove" };
903
+ if (options?.type !== "ObjectExpression") return { reason: "the factory call has no options to remove" };
932
904
  const target = walk(options, path);
933
905
  if ("reason" in target) return target;
934
906
  if ("missing" in target) return { reason: `${path.join(".")} is not set` };
@@ -959,10 +931,10 @@ function applyAddPlugin({ source, parsed, config, edit, quote, extensions }) {
959
931
  packageName: edit.plugin,
960
932
  extensions
961
933
  })) return { reason: `"${edit.plugin}" is not a @kubb/plugin-* package or an installed Kubb plugin` };
962
- const importName = extensions.get(edit.plugin)?.manifest.export ?? edit.importName ?? toExportName(edit.plugin);
934
+ const importName = extensions.get(edit.plugin)?.manifest.export ?? toExportName(edit.plugin);
963
935
  if (!IDENTIFIER.test(importName)) return { reason: `"${importName}" is not a valid import name` };
964
- if (pluginCalls(parsed.mod, config).some((plugin) => plugin.packageName === edit.plugin)) return { source };
965
- const taken = importedFrom(parsed.mod).get(importName);
936
+ if (factoryCalls(parsed.mod, config).some((plugin) => plugin.packageName === edit.plugin)) return { source };
937
+ const taken = importedFrom(parsed.mod).get(importName)?.from;
966
938
  if (taken && taken !== edit.plugin) return { reason: `${importName} is already imported from ${taken}` };
967
939
  const options = edit.options ?? {};
968
940
  if (!isOptionValue(options)) return { reason: "the options are not literals that can be written to a config file" };
@@ -973,7 +945,7 @@ function applyAddPlugin({ source, parsed, config, edit, quote, extensions }) {
973
945
  }
974
946
  /** Comments out a plugin call in place, keeping its options on disk so `enable-plugin` can restore them exactly. */
975
947
  function disablePlugin(source, mod, config, plugin) {
976
- const loc = pluginCalls(mod, config).find((entry) => entry.packageName === plugin)?.call.loc;
948
+ const loc = factoryCalls(mod, config).find((entry) => entry.packageName === plugin)?.call.loc;
977
949
  if (!loc) return { reason: `${plugin} is not in the plugins array` };
978
950
  const lines = source.split("\n");
979
951
  const from = loc.start.line - 1;
@@ -1010,8 +982,14 @@ function applyEdit({ source, edit, quote, extensions }) {
1010
982
  quote,
1011
983
  extensions
1012
984
  });
1013
- const call = pluginCalls(parsed.mod, config).find((plugin) => plugin.packageName === edit.plugin)?.call;
1014
- if (!call) return { reason: disabledBlocks(source, config).some((block) => block.packageName === edit.plugin) ? `${edit.plugin} is disabled, enable it first` : `${edit.plugin} is not in the plugins array` };
985
+ const kind = edit.kind ?? "plugin";
986
+ const calls = factoryCalls(parsed.mod, config, kind).filter((entry) => entry.packageName === edit.plugin);
987
+ const exportName = extensions.get(edit.plugin)?.manifest.export ?? toExportName(edit.plugin);
988
+ const call = (calls.find((entry) => entry.exportName === exportName) ?? calls[0])?.call;
989
+ if (!call) {
990
+ if (kind === "adapter") return { reason: `${edit.plugin} is not the config's adapter` };
991
+ return { reason: kind === "plugin" && disabledBlocks(source, config).some((block) => block.packageName === edit.plugin) ? `${edit.plugin} is disabled, enable it first` : `${edit.plugin} is not in the ${kind}s array` };
992
+ }
1015
993
  return edit.operation === "set" ? applySet(source, call, edit.path, edit.value, quote) : applyRemove(source, call, edit.path);
1016
994
  }
1017
995
  /**
@@ -1083,32 +1061,6 @@ async function writeConfigEdits({ filePath, edits, extensions }) {
1083
1061
  return result;
1084
1062
  }
1085
1063
  //#endregion
1086
- //#region src/extensions.ts
1087
- /** Extensions `kubb` ships with, so they count as installed without being a direct dependency. */
1088
- const BUILT_IN_PACKAGES = [
1089
- "@kubb/adapter-oas",
1090
- "@kubb/parser-ts",
1091
- "@kubb/parser-md",
1092
- "@kubb/plugin-barrel"
1093
- ];
1094
- /**
1095
- * Finds the extensions installed in the project at `root`: its `dependencies` and `devDependencies`,
1096
- * the `packages` its config file imports, and the extensions `kubb` ships with. Keeps each package
1097
- * that resolves and ships a valid `kubb-extension.json`. Never throws.
1098
- */
1099
- async function discoverExtensions({ root, packages = [] }) {
1100
- const installed = await readInstalledPackages({
1101
- root,
1102
- packages: [...packages, ...BUILT_IN_PACKAGES],
1103
- resolveFrom: ["kubb"]
1104
- });
1105
- return new Map(installed.flatMap(({ package: name, version, manifest }) => version !== void 0 && manifest ? [[name, {
1106
- package: name,
1107
- version,
1108
- manifest
1109
- }]] : []));
1110
- }
1111
- //#endregion
1112
1064
  //#region src/generations.ts
1113
1065
  const READ_CONCURRENCY = 50;
1114
1066
  const MB = 1048576;
@@ -1516,6 +1468,13 @@ const OPEN_TIMEOUT_MS = 5e3;
1516
1468
  const MAX_RETRY_DELAY_MS = 3e4;
1517
1469
  const DISK_SNAPSHOT_MAX_FILES = 1e4;
1518
1470
  const SANDBOX_GENERATION_TTL_MS = 9e5;
1471
+ /** Extensions `kubb` ships with, so they count as installed without being a direct dependency. */
1472
+ const BUILT_IN_PACKAGES = [
1473
+ "@kubb/adapter-oas",
1474
+ "@kubb/parser-ts",
1475
+ "@kubb/parser-md",
1476
+ "@kubb/plugin-barrel"
1477
+ ];
1519
1478
  /**
1520
1479
  * Studio closed with {@link AgentCloseCode.UNAUTHORIZED}. Hosts forget the token and pair again.
1521
1480
  */
@@ -1614,10 +1573,10 @@ var Agent = class extends RpcTarget {
1614
1573
  });
1615
1574
  return value;
1616
1575
  }
1617
- async #readConfigFileView(source) {
1576
+ async #readConfigFileView() {
1618
1577
  if (!this.#permissions.allowConfigEdit) return void 0;
1619
1578
  try {
1620
- return readConfig(source ?? await read(this.#options.configFile));
1579
+ return readConfig(await read(this.#options.configFile));
1621
1580
  } catch (error) {
1622
1581
  await this.#warn({ message: `Could not read ${this.#options.configFile}: ${getErrorMessage(error)}` });
1623
1582
  return;
@@ -1625,10 +1584,16 @@ var Agent = class extends RpcTarget {
1625
1584
  }
1626
1585
  async #installedExtensions() {
1627
1586
  const source = await read(this.#options.configFile).catch(() => "");
1628
- return discoverExtensions({
1587
+ const installed = await readInstalledPackages({
1629
1588
  root: this.#options.root,
1630
- packages: readImports(source)
1589
+ packages: [...readImports(source), ...BUILT_IN_PACKAGES],
1590
+ resolveFrom: ["kubb"]
1631
1591
  });
1592
+ return new Map(installed.flatMap(({ package: name, version, manifest }) => version !== void 0 && manifest ? [[name, {
1593
+ package: name,
1594
+ version,
1595
+ manifest
1596
+ }]] : []));
1632
1597
  }
1633
1598
  async connect(info) {
1634
1599
  this.#session.info = info;
@@ -1651,11 +1616,26 @@ var Agent = class extends RpcTarget {
1651
1616
  plugins: config.plugins.map((plugin) => ({
1652
1617
  name: toStudioName({
1653
1618
  name: resolveMeta(plugin).name,
1654
- root,
1655
1619
  extensions
1656
1620
  }),
1657
1621
  options: toJSONOptions(plugin.options)
1658
1622
  })),
1623
+ adapter: config.adapter && {
1624
+ name: toStudioName({
1625
+ name: resolveMeta(config.adapter).name,
1626
+ extensions,
1627
+ kind: "adapter"
1628
+ }),
1629
+ options: toJSONOptions(config.adapter.options)
1630
+ },
1631
+ parsers: config.parsers.map((parser) => ({
1632
+ name: toStudioName({
1633
+ name: resolveMeta(parser).name,
1634
+ extensions,
1635
+ kind: "parser"
1636
+ }),
1637
+ options: toJSONOptions(parser.options)
1638
+ })),
1659
1639
  extensions: [...extensions.values()]
1660
1640
  },
1661
1641
  permissions: this.#permissions
@@ -1715,11 +1695,19 @@ var Agent = class extends RpcTarget {
1715
1695
  const studioInput = this.#sandbox ? patch?.input : void 0;
1716
1696
  if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || getInputKind(studioInput) !== "inline")) return this.#refuse({ message: "A sandbox agent generates only from the OpenAPI document content Studio sends, not from a file path or URL" });
1717
1697
  const [config, extensions] = await Promise.all([this.#options.loadConfig(), this.#installedExtensions()]);
1718
- const [plugins, adapter] = await Promise.all([mergePlugins({
1719
- diskPlugins: config.plugins,
1720
- studioPlugins: patch?.plugins,
1721
- extensions
1722
- }), mergeAdapter(config.adapter, patch?.adapter)]).catch(async (error) => {
1698
+ const [plugins, adapter, parsers] = await Promise.all([
1699
+ mergePlugins({
1700
+ diskPlugins: config.plugins,
1701
+ studioPlugins: patch?.plugins,
1702
+ extensions
1703
+ }),
1704
+ mergeAdapter(config.adapter, patch?.adapter, extensions),
1705
+ mergeParsers({
1706
+ diskParsers: config.parsers,
1707
+ studioParsers: patch?.parsers,
1708
+ extensions
1709
+ })
1710
+ ]).catch(async (error) => {
1723
1711
  await emit(this.#options.hooks, "kubb:error", { error: toError(error) });
1724
1712
  throw error;
1725
1713
  });
@@ -1743,7 +1731,8 @@ var Agent = class extends RpcTarget {
1743
1731
  input: studioInput ?? config.input,
1744
1732
  storage: this.#permissions.allowWrite ? fsStorage() : memoryStorage(),
1745
1733
  plugins: activePlugins,
1746
- adapter
1734
+ adapter,
1735
+ parsers
1747
1736
  },
1748
1737
  hooks: this.#options.hooks,
1749
1738
  signal,
@@ -1805,7 +1794,7 @@ var Agent = class extends RpcTarget {
1805
1794
  }
1806
1795
  if (this.#session.active) return { value: refuse("a generation is in progress") };
1807
1796
  try {
1808
- const { source, outcomes, changed } = await writeConfigEdits({
1797
+ const { outcomes, changed } = await writeConfigEdits({
1809
1798
  filePath: configFile,
1810
1799
  edits,
1811
1800
  extensions: await this.#installedExtensions()
@@ -1814,8 +1803,7 @@ var Agent = class extends RpcTarget {
1814
1803
  return {
1815
1804
  value: {
1816
1805
  outcomes,
1817
- changed,
1818
- file: changed ? await this.#readConfigFileView(source) : void 0
1806
+ changed
1819
1807
  },
1820
1808
  info: `applied ${applied}/${outcomes.length} edits to ${configPath}`
1821
1809
  };
@@ -2296,8 +2284,16 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2296
2284
  if (content === null) throw new Error(`The generation listed ${path} but did not keep its content`);
2297
2285
  return [path, content];
2298
2286
  })));
2299
- const { peerDependencies, missingDependencies } = await resolvePeerDependencies(config.plugins.map((plugin) => resolveMeta(plugin).name));
2300
- const missing = missingDependencies.filter((dependency) => !BUNDLED_DEPENDENCIES.includes(dependency));
2287
+ const pluginNames = config.plugins.map((plugin) => resolveMeta(plugin).name);
2288
+ const installed = await readInstalledPackages({
2289
+ root: config.root,
2290
+ packages: pluginNames.map(toPackageName),
2291
+ resolveFrom: ["kubb"]
2292
+ });
2293
+ const packageOf = (name) => installed.find(({ manifest }) => manifest?.kind === "plugin" && manifest.name === name)?.package ?? toPackageName(name);
2294
+ const names = [...new Set(pluginNames.map(packageOf))];
2295
+ const peerDependencies = Object.fromEntries(installed.flatMap(({ package: name, version }) => names.includes(name) && version ? [[name, version]] : []));
2296
+ const missing = names.filter((name) => !(name in peerDependencies) && !BUNDLED_DEPENDENCIES.includes(name));
2301
2297
  if (missing.length) throw new Error(`Missing dependencies: ${missing.join(", ")}`);
2302
2298
  const bytes = await createSnapshotPackage({
2303
2299
  files,