@kubb/studio 5.9.0 → 5.9.1

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
@@ -95,74 +95,34 @@ function getErrorMessage(value) {
95
95
  }
96
96
  //#endregion
97
97
  //#region ../../internals/utils/src/runtime.ts
98
+ const isBun = typeof Bun !== "undefined";
99
+ const isDeno = "Deno" in globalThis;
98
100
  /**
99
- * Detects the JavaScript runtime executing the current process and exposes its name and version.
101
+ * The JavaScript runtime executing the current process: its name, version and whether it is Bun.
100
102
  *
101
- * Prefer the shared {@link runtime} instance over constructing your own.
103
+ * @example
104
+ * ```ts
105
+ * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
106
+ * ```
102
107
  */
103
- var Runtime = class {
104
- /**
105
- * `true` when the current process is running under Bun.
106
- *
107
- * Detection keys off the global `Bun` object rather than `process.versions`,
108
- * because Bun polyfills `process.versions.node` for Node compatibility and would
109
- * otherwise look like Node.
110
- *
111
- * @example
112
- * ```ts
113
- * if (runtime.isBun) {
114
- * await Bun.write(path, data)
115
- * }
116
- * ```
117
- */
118
- get isBun() {
119
- return typeof Bun !== "undefined";
120
- }
121
- /**
122
- * `true` when the current process is running under Deno.
123
- */
124
- get isDeno() {
125
- return typeof globalThis.Deno !== "undefined";
126
- }
127
- /**
128
- * `true` when the current process is running under Node.
129
- *
130
- * Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
131
- */
132
- get isNode() {
133
- return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
134
- }
135
- /**
136
- * Name of the runtime executing the current process.
137
- *
138
- * @example
139
- * ```ts
140
- * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
141
- * ```
142
- */
143
- get name() {
144
- if (this.isBun) return "bun";
145
- if (this.isDeno) return "deno";
146
- return "node";
147
- }
148
- /**
149
- * Version of the active runtime, or an empty string when it cannot be read.
150
- *
151
- * @example
152
- * ```ts
153
- * runtime.version // '1.3.11' under Bun, '22.22.2' under Node
154
- * ```
155
- */
156
- get version() {
157
- if (this.isBun) return process.versions.bun ?? "";
158
- if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
159
- return process.versions?.node ?? "";
160
- }
108
+ const runtime = {
109
+ isBun,
110
+ ...detect()
161
111
  };
162
- /**
163
- * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
164
- */
165
- const runtime = new Runtime();
112
+ function detect() {
113
+ if (isBun) return {
114
+ name: "bun",
115
+ version: process.versions.bun ?? ""
116
+ };
117
+ if (isDeno) return {
118
+ name: "deno",
119
+ version: globalThis.Deno?.version?.deno ?? ""
120
+ };
121
+ return {
122
+ name: "node",
123
+ version: process.versions?.node ?? ""
124
+ };
125
+ }
166
126
  //#endregion
167
127
  //#region ../../internals/utils/src/fs.ts
168
128
  /**
@@ -216,7 +176,7 @@ function getElapsedMs(hrStart) {
216
176
  }
217
177
  //#endregion
218
178
  //#region package.json
219
- var version = "5.9.0";
179
+ var version = "5.9.1";
220
180
  //#endregion
221
181
  //#region src/resolveConfig.ts
222
182
  /**
@@ -261,47 +221,19 @@ function toPluginName(packageName) {
261
221
  function toPackageName(name) {
262
222
  return name.startsWith("plugin-") ? `@kubb/${name}` : name;
263
223
  }
264
- function resolvesFrom(base, packageName) {
265
- try {
266
- (0, node_module.createRequire)(base).resolve(`${packageName}/package.json`);
267
- return true;
268
- } catch {
269
- return false;
270
- }
271
- }
272
224
  /**
273
- * Whether `packageName` resolves from the project at `root`, from this package, or from the `kubb`
274
- * package the project installs, which brings the plugins Kubb adds on its own such as
275
- * `@kubb/plugin-barrel`. Only resolves the package, it never runs it.
276
- */
277
- function isInstalled({ packageName, root }) {
278
- const project = (0, node_url.pathToFileURL)(`${root}/`);
279
- if (resolvesFrom(project, packageName) || resolvesFrom(require("url").pathToFileURL(__filename).href, packageName)) return true;
280
- try {
281
- return resolvesFrom((0, node_module.createRequire)(project).resolve("kubb/package.json"), packageName);
282
- } catch {
283
- return false;
284
- }
285
- }
286
- /**
287
- * The name Studio knows a plugin by: its `@kubb/plugin-*` package when one is installed, otherwise
288
- * the plugin's own `name`, so a plugin defined in the user's project is never reported as a Kubb
289
- * package that does not exist. An installed extension whose manifest `name` matches wins.
225
+ * The name Studio knows an extension by: the package of the installed extension whose manifest
226
+ * `name` matches, otherwise its own `name`, so a plugin defined in the user's project is never
227
+ * reported as a Kubb package that does not exist.
290
228
  *
291
229
  * @example
292
230
  * ```ts
293
- * toStudioName({ name: 'plugin-ts', root }) // '@kubb/plugin-ts'
294
- * toStudioName({ name: 'plugin-zod-operations', root }) // 'plugin-zod-operations', a local plugin
231
+ * toStudioName({ name: 'plugin-ts', extensions }) // '@kubb/plugin-ts'
232
+ * toStudioName({ name: 'plugin-zod-operations', extensions }) // 'plugin-zod-operations', a local plugin
295
233
  * ```
296
234
  */
297
- function toStudioName({ name, root, extensions = /* @__PURE__ */ new Map() }) {
298
- const installed = [...extensions.values()].find(({ manifest }) => manifest.kind === "plugin" && manifest.name === name);
299
- if (installed) return installed.package;
300
- const packageName = toPackageName(name);
301
- return packageName !== name && isInstalled({
302
- packageName,
303
- root
304
- }) ? packageName : name;
235
+ function toStudioName({ name, extensions = /* @__PURE__ */ new Map(), kind = "plugin" }) {
236
+ return [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name)?.package ?? name;
305
237
  }
306
238
  /**
307
239
  * Derives the conventional named export for a `@kubb/*` plugin package from its package name.
@@ -380,33 +312,6 @@ async function resolvePlugins({ plugins, extensions = /* @__PURE__ */ new Map()
380
312
  }))(options ?? {});
381
313
  }));
382
314
  }
383
- /** The installed version of each plugin's package, plus the names that could not be resolved. */
384
- async function resolvePeerDependencies(names) {
385
- const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
386
- const uniqueNames = [...new Set(names.map(toPackageName))];
387
- const peerDependencies = {};
388
- const missingDependencies = [];
389
- const versions = await Promise.all(uniqueNames.map(async (name) => {
390
- try {
391
- const path = require$1.resolve(`${name}/package.json`);
392
- return JSON.parse(await (0, node_fs_promises.readFile)(path, "utf8")).version;
393
- } catch {
394
- return;
395
- }
396
- }));
397
- for (const [index, name] of uniqueNames.entries()) {
398
- const version = versions[index];
399
- if (version) {
400
- peerDependencies[name] = version;
401
- continue;
402
- }
403
- missingDependencies.push(name);
404
- }
405
- return {
406
- peerDependencies,
407
- missingDependencies
408
- };
409
- }
410
315
  /**
411
316
  * Keys that reach an object's prototype when assigned. Never read from or written into options.
412
317
  */
@@ -469,6 +374,7 @@ function isOptionValue(value) {
469
374
  * ```ts
470
375
  * mergeOptions({ macros: [camelCaseMacro], syntaxType: 'type' }, { macros: [{ name: 'camelCase' }], syntaxType: 'interface' })
471
376
  * // { macros: [camelCaseMacro], syntaxType: 'interface' }
377
+ * mergeOptions({ emptySchemaType: 'any' }, { emptySchemaType: null }) // {}
472
378
  * ```
473
379
  */
474
380
  function mergeOptions(disk, studio) {
@@ -476,6 +382,10 @@ function mergeOptions(disk, studio) {
476
382
  for (const [key, value] of Object.entries(studio)) {
477
383
  if (!isSafeKey(key)) continue;
478
384
  const current = disk[key];
385
+ if (value === null) {
386
+ if (current === void 0 || isOptionValue(current)) delete merged[key];
387
+ continue;
388
+ }
479
389
  if (isPlainObject(current) && isPlainObject(value)) {
480
390
  merged[key] = mergeOptions(current, value);
481
391
  continue;
@@ -533,20 +443,54 @@ async function mergePlugins({ diskPlugins, studioPlugins, extensions = /* @__PUR
533
443
  })];
534
444
  }
535
445
  /**
536
- * Merges Studio-provided adapter option overrides into the disk config's adapter.
537
- *
538
- * Adapter instances carry live functions (`parse`, `getImports`, ...) that can't survive
539
- * JSON serialization over the WebSocket, so `studioOptions` is treated as an options patch
540
- * rather than a replacement adapter. Re-invokes the same `@kubb/adapter-<name>` factory the
541
- * disk config used, with the merged options, so the resulting instance has fresh closures
542
- * over the merged values instead of a plain object missing `parse`.
446
+ * Re-invokes the factory behind a disk adapter or parser with Studio's options merged over its own.
447
+ * An instance carries live functions (`parse`, `print`, ...) that cannot survive JSON, so Studio's
448
+ * options are a patch, not a replacement, and the factory is found from the disk instance itself: the
449
+ * installed extension whose manifest names it, or the `@kubb/<kind>-<name>` package for a built-in
450
+ * one. Throws when no factory resolves, as `mergePlugins` does, so an override Studio sent is never
451
+ * dropped without a word. An extension without a manifest, such as `parserTsx`, has no options in
452
+ * Studio, so Studio never sends one for it.
453
+ */
454
+ async function rebuildExtension({ disk, studioOptions, kind, extensions }) {
455
+ if (!studioOptions || !Object.keys(studioOptions).length) return disk;
456
+ const { name } = (0, _kubb_core.resolveMeta)(disk);
457
+ const installed = [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name);
458
+ const packageName = installed?.package ?? `@kubb/${kind}-${name}`;
459
+ const factory = await importFromProject(packageName).then((mod) => mod[installed?.manifest.export ?? toExportName(packageName)]).catch((cause) => {
460
+ 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 });
461
+ });
462
+ 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.`);
463
+ return factory(mergeOptions(disk.options ?? {}, studioOptions));
464
+ }
465
+ /**
466
+ * Merges Studio-provided adapter option overrides into the disk config's adapter, see {@link rebuildExtension}.
543
467
  */
544
- async function mergeAdapter(diskAdapter, studioOptions) {
545
- if (!studioOptions || !diskAdapter) return diskAdapter;
546
- const packageName = `@kubb/adapter-${diskAdapter.name}`;
547
- const factory = (await importFromProject(packageName))[toExportName(packageName)];
548
- if (typeof factory !== "function") return diskAdapter;
549
- return factory(mergeOptions(diskAdapter.options ?? {}, studioOptions));
468
+ async function mergeAdapter(diskAdapter, studioOptions, extensions = /* @__PURE__ */ new Map()) {
469
+ if (!diskAdapter) return diskAdapter;
470
+ return rebuildExtension({
471
+ disk: diskAdapter,
472
+ studioOptions,
473
+ kind: "adapter",
474
+ extensions
475
+ });
476
+ }
477
+ /**
478
+ * Merges Studio-provided parser option overrides into the disk config's parsers, matched by the name
479
+ * Studio knows each parser by, see {@link rebuildExtension}. Parsers Studio does not name are kept as is.
480
+ */
481
+ async function mergeParsers({ diskParsers, studioParsers, extensions = /* @__PURE__ */ new Map() }) {
482
+ if (!studioParsers?.length) return diskParsers;
483
+ const studioOptions = new Map(studioParsers.map((entry) => [entry.name, entry.options]));
484
+ return Promise.all(diskParsers.map((parser) => rebuildExtension({
485
+ disk: parser,
486
+ studioOptions: studioOptions.get(toStudioName({
487
+ name: (0, _kubb_core.resolveMeta)(parser).name,
488
+ extensions,
489
+ kind: "parser"
490
+ })),
491
+ kind: "parser",
492
+ extensions
493
+ })));
550
494
  }
551
495
  //#endregion
552
496
  //#region src/configFile.ts
@@ -567,14 +511,21 @@ const OPERATIONS = /* @__PURE__ */ new Set([
567
511
  "disable-plugin",
568
512
  "enable-plugin"
569
513
  ]);
514
+ const KINDS = /* @__PURE__ */ new Set([
515
+ "plugin",
516
+ "adapter",
517
+ "parser"
518
+ ]);
570
519
  /**
571
520
  * Why an edit from Studio cannot be applied as sent, checked before anything reads its fields.
572
521
  * The edit crossed the wire, so its declared type is a promise the sender may not have kept.
573
522
  */
574
523
  function invalidEdit(edit) {
575
- const { operation, plugin, config, path, importName } = typeof edit === "object" && edit !== null ? edit : {};
524
+ const { operation, plugin, config, path, kind } = typeof edit === "object" && edit !== null ? edit : {};
576
525
  if (typeof operation !== "string" || !OPERATIONS.has(operation)) return `unknown operation ${JSON.stringify(operation)}`;
577
526
  if (typeof plugin !== "string" || !plugin) return "the edit names no plugin";
527
+ if (kind !== void 0 && (typeof kind !== "string" || !KINDS.has(kind))) return `unknown kind ${JSON.stringify(kind)}`;
528
+ if (kind !== void 0 && operation !== "set" && operation !== "remove") return `${operation} edits the plugins array, so it takes no kind`;
578
529
  if (config !== void 0 && typeof config !== "string" && !Number.isSafeInteger(config)) return "config must name an entry or give its index";
579
530
  if (operation === "set" || operation === "remove") {
580
531
  if (!Array.isArray(path) || !path.every((key) => typeof key === "string" && key)) return "path must be a list of option names";
@@ -582,7 +533,6 @@ function invalidEdit(edit) {
582
533
  const unsafe = path.find((key) => !isSafeKey(key));
583
534
  if (unsafe !== void 0) return `${unsafe} cannot be used as an option name`;
584
535
  }
585
- if (operation === "add-plugin" && importName !== void 0 && typeof importName !== "string") return "importName must be a string";
586
536
  }
587
537
  /** Where a node sits in the source text. */
588
538
  function rangeOf(node) {
@@ -777,29 +727,42 @@ function importedFrom(mod) {
777
727
  const imports = /* @__PURE__ */ new Map();
778
728
  for (const statement of bodyOf(mod)) {
779
729
  if (statement.type === "ImportDeclaration") {
780
- for (const specifier of statement.specifiers) imports.set(specifier.local.name, statement.source.value);
730
+ for (const specifier of statement.specifiers) {
731
+ const imported = specifier.type === "ImportSpecifier" ? specifier.imported : specifier.local;
732
+ imports.set(specifier.local.name, {
733
+ from: statement.source.value,
734
+ exported: imported.type === "Identifier" ? imported.name : imported.value
735
+ });
736
+ }
781
737
  continue;
782
738
  }
783
739
  if (statement.type !== "VariableDeclaration") continue;
784
740
  for (const declarator of statement.declarations) {
785
741
  const from = requiredModule(declarator.init);
786
742
  if (from === void 0 || declarator.id.type !== "ObjectPattern") continue;
787
- for (const entry of declarator.id.properties) if (entry.type === "ObjectProperty" && entry.value.type === "Identifier") imports.set(entry.value.name, from);
743
+ for (const entry of declarator.id.properties) if (entry.type === "ObjectProperty" && entry.value.type === "Identifier") imports.set(entry.value.name, {
744
+ from,
745
+ exported: memberKey(entry) ?? entry.value.name
746
+ });
788
747
  }
789
748
  }
790
749
  return imports;
791
750
  }
792
- /** Every `pluginX(...)` element of a config's plugins array that resolves to an import. */
793
- function pluginCalls(mod, config) {
794
- const plugins = property(config, "plugins");
795
- if (plugins?.type !== "ArrayExpression") return [];
751
+ /**
752
+ * Every factory call of one kind that resolves to an import: the `pluginX(...)` elements of the
753
+ * `plugins` array, the `adapter` value, or the elements of the `parsers` array.
754
+ */
755
+ function factoryCalls(mod, config, kind = "plugin") {
756
+ const node = property(config, kind === "adapter" ? "adapter" : `${kind}s`);
757
+ const elements = kind === "adapter" ? [node] : node?.type === "ArrayExpression" ? node.elements : [];
796
758
  const imports = importedFrom(mod);
797
- return plugins.elements.flatMap((element) => {
759
+ return elements.flatMap((element) => {
798
760
  if (element?.type !== "CallExpression" || element.callee.type !== "Identifier") return [];
799
- const packageName = imports.get(element.callee.name);
800
- return packageName ? [{
761
+ const imported = imports.get(element.callee.name);
762
+ return imported ? [{
801
763
  importName: element.callee.name,
802
- packageName,
764
+ exportName: imported.exported,
765
+ packageName: imported.from,
803
766
  call: element
804
767
  }] : [];
805
768
  });
@@ -868,11 +831,11 @@ function readConfig(source) {
868
831
  reason: parsed.reason
869
832
  };
870
833
  const { mod, configs } = parsed;
871
- const importNames = new Map([...importedFrom(mod)].map(([local, from]) => [from, local]));
834
+ const importNames = new Map([...importedFrom(mod)].map(([local, { from }]) => [from, local]));
872
835
  return {
873
836
  managed: true,
874
837
  configs: configs.map((config) => {
875
- const active = pluginCalls(mod, config).map(({ importName, packageName, call }) => ({
838
+ const active = factoryCalls(mod, config).map(({ importName, packageName, call }) => ({
876
839
  line: call.loc?.start.line ?? 0,
877
840
  plugin: {
878
841
  importName,
@@ -890,9 +853,18 @@ function readConfig(source) {
890
853
  }
891
854
  }));
892
855
  const plugins = [...active, ...disabled].sort((a, b) => a.line - b.line).map(({ plugin }) => plugin);
856
+ const toView = ({ importName, packageName, call }) => ({
857
+ importName,
858
+ packageName,
859
+ options: readOptions(call)
860
+ });
861
+ const [adapter] = factoryCalls(mod, config, "adapter").map(toView);
862
+ const parsers = factoryCalls(mod, config, "parser").map(toView);
893
863
  return {
894
864
  name: configName(config),
895
- plugins
865
+ plugins,
866
+ ...adapter && { adapter },
867
+ ...parsers.length ? { parsers } : {}
896
868
  };
897
869
  })
898
870
  };
@@ -900,7 +872,7 @@ function readConfig(source) {
900
872
  /** The modules the config file imports, in source order. Empty when the file cannot be parsed. */
901
873
  function readImports(source) {
902
874
  try {
903
- return [...new Set(importedFrom((0, magicast.parseModule)(source)).values())];
875
+ return [...new Set([...importedFrom((0, magicast.parseModule)(source)).values()].map(({ from }) => from))];
904
876
  } catch {
905
877
  return [];
906
878
  }
@@ -937,10 +909,10 @@ function applySet(source, call, path, value, quote) {
937
909
  const options = call.arguments[0];
938
910
  if (!options) {
939
911
  const closing = rangeOf(call).end - 1;
940
- if (source[closing] !== ")") return { reason: "the plugin call could not be located in the file" };
912
+ if (source[closing] !== ")") return { reason: "the factory call could not be located in the file" };
941
913
  return { source: splice(source, closing, closing, printValue(nest(path, value), quote)) };
942
914
  }
943
- if (options.type !== "ObjectExpression") return { reason: "the plugin was not called with an object literal" };
915
+ if (options.type !== "ObjectExpression") return { reason: "the factory was not called with an object literal" };
944
916
  const target = walk(options, path);
945
917
  if ("reason" in target) return target;
946
918
  if ("missing" in target) {
@@ -952,7 +924,7 @@ function applySet(source, call, path, value, quote) {
952
924
  /** Deletes the option at `path`, falling the plugin back to its default for it. */
953
925
  function applyRemove(source, call, path) {
954
926
  const options = call.arguments[0];
955
- if (options?.type !== "ObjectExpression") return { reason: "the plugin has no options to remove" };
927
+ if (options?.type !== "ObjectExpression") return { reason: "the factory call has no options to remove" };
956
928
  const target = walk(options, path);
957
929
  if ("reason" in target) return target;
958
930
  if ("missing" in target) return { reason: `${path.join(".")} is not set` };
@@ -983,10 +955,10 @@ function applyAddPlugin({ source, parsed, config, edit, quote, extensions }) {
983
955
  packageName: edit.plugin,
984
956
  extensions
985
957
  })) return { reason: `"${edit.plugin}" is not a @kubb/plugin-* package or an installed Kubb plugin` };
986
- const importName = extensions.get(edit.plugin)?.manifest.export ?? edit.importName ?? toExportName(edit.plugin);
958
+ const importName = extensions.get(edit.plugin)?.manifest.export ?? toExportName(edit.plugin);
987
959
  if (!IDENTIFIER.test(importName)) return { reason: `"${importName}" is not a valid import name` };
988
- if (pluginCalls(parsed.mod, config).some((plugin) => plugin.packageName === edit.plugin)) return { source };
989
- const taken = importedFrom(parsed.mod).get(importName);
960
+ if (factoryCalls(parsed.mod, config).some((plugin) => plugin.packageName === edit.plugin)) return { source };
961
+ const taken = importedFrom(parsed.mod).get(importName)?.from;
990
962
  if (taken && taken !== edit.plugin) return { reason: `${importName} is already imported from ${taken}` };
991
963
  const options = edit.options ?? {};
992
964
  if (!isOptionValue(options)) return { reason: "the options are not literals that can be written to a config file" };
@@ -997,7 +969,7 @@ function applyAddPlugin({ source, parsed, config, edit, quote, extensions }) {
997
969
  }
998
970
  /** Comments out a plugin call in place, keeping its options on disk so `enable-plugin` can restore them exactly. */
999
971
  function disablePlugin(source, mod, config, plugin) {
1000
- const loc = pluginCalls(mod, config).find((entry) => entry.packageName === plugin)?.call.loc;
972
+ const loc = factoryCalls(mod, config).find((entry) => entry.packageName === plugin)?.call.loc;
1001
973
  if (!loc) return { reason: `${plugin} is not in the plugins array` };
1002
974
  const lines = source.split("\n");
1003
975
  const from = loc.start.line - 1;
@@ -1034,8 +1006,14 @@ function applyEdit({ source, edit, quote, extensions }) {
1034
1006
  quote,
1035
1007
  extensions
1036
1008
  });
1037
- const call = pluginCalls(parsed.mod, config).find((plugin) => plugin.packageName === edit.plugin)?.call;
1038
- 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` };
1009
+ const kind = edit.kind ?? "plugin";
1010
+ const calls = factoryCalls(parsed.mod, config, kind).filter((entry) => entry.packageName === edit.plugin);
1011
+ const exportName = extensions.get(edit.plugin)?.manifest.export ?? toExportName(edit.plugin);
1012
+ const call = (calls.find((entry) => entry.exportName === exportName) ?? calls[0])?.call;
1013
+ if (!call) {
1014
+ if (kind === "adapter") return { reason: `${edit.plugin} is not the config's adapter` };
1015
+ 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` };
1016
+ }
1039
1017
  return edit.operation === "set" ? applySet(source, call, edit.path, edit.value, quote) : applyRemove(source, call, edit.path);
1040
1018
  }
1041
1019
  /**
@@ -1107,32 +1085,6 @@ async function writeConfigEdits({ filePath, edits, extensions }) {
1107
1085
  return result;
1108
1086
  }
1109
1087
  //#endregion
1110
- //#region src/extensions.ts
1111
- /** Extensions `kubb` ships with, so they count as installed without being a direct dependency. */
1112
- const BUILT_IN_PACKAGES = [
1113
- "@kubb/adapter-oas",
1114
- "@kubb/parser-ts",
1115
- "@kubb/parser-md",
1116
- "@kubb/plugin-barrel"
1117
- ];
1118
- /**
1119
- * Finds the extensions installed in the project at `root`: its `dependencies` and `devDependencies`,
1120
- * the `packages` its config file imports, and the extensions `kubb` ships with. Keeps each package
1121
- * that resolves and ships a valid `kubb-extension.json`. Never throws.
1122
- */
1123
- async function discoverExtensions({ root, packages = [] }) {
1124
- const installed = await (0, _kubb_core.readInstalledPackages)({
1125
- root,
1126
- packages: [...packages, ...BUILT_IN_PACKAGES],
1127
- resolveFrom: ["kubb"]
1128
- });
1129
- return new Map(installed.flatMap(({ package: name, version, manifest }) => version !== void 0 && manifest ? [[name, {
1130
- package: name,
1131
- version,
1132
- manifest
1133
- }]] : []));
1134
- }
1135
- //#endregion
1136
1088
  //#region src/generations.ts
1137
1089
  const READ_CONCURRENCY = 50;
1138
1090
  const MB = 1048576;
@@ -1540,6 +1492,13 @@ const OPEN_TIMEOUT_MS = 5e3;
1540
1492
  const MAX_RETRY_DELAY_MS = 3e4;
1541
1493
  const DISK_SNAPSHOT_MAX_FILES = 1e4;
1542
1494
  const SANDBOX_GENERATION_TTL_MS = 9e5;
1495
+ /** Extensions `kubb` ships with, so they count as installed without being a direct dependency. */
1496
+ const BUILT_IN_PACKAGES = [
1497
+ "@kubb/adapter-oas",
1498
+ "@kubb/parser-ts",
1499
+ "@kubb/parser-md",
1500
+ "@kubb/plugin-barrel"
1501
+ ];
1543
1502
  /**
1544
1503
  * Studio closed with {@link AgentCloseCode.UNAUTHORIZED}. Hosts forget the token and pair again.
1545
1504
  */
@@ -1638,10 +1597,10 @@ var Agent = class extends capnweb.RpcTarget {
1638
1597
  });
1639
1598
  return value;
1640
1599
  }
1641
- async #readConfigFileView(source) {
1600
+ async #readConfigFileView() {
1642
1601
  if (!this.#permissions.allowConfigEdit) return void 0;
1643
1602
  try {
1644
- return readConfig(source ?? await read(this.#options.configFile));
1603
+ return readConfig(await read(this.#options.configFile));
1645
1604
  } catch (error) {
1646
1605
  await this.#warn({ message: `Could not read ${this.#options.configFile}: ${getErrorMessage(error)}` });
1647
1606
  return;
@@ -1649,10 +1608,16 @@ var Agent = class extends capnweb.RpcTarget {
1649
1608
  }
1650
1609
  async #installedExtensions() {
1651
1610
  const source = await read(this.#options.configFile).catch(() => "");
1652
- return discoverExtensions({
1611
+ const installed = await (0, _kubb_core.readInstalledPackages)({
1653
1612
  root: this.#options.root,
1654
- packages: readImports(source)
1613
+ packages: [...readImports(source), ...BUILT_IN_PACKAGES],
1614
+ resolveFrom: ["kubb"]
1655
1615
  });
1616
+ return new Map(installed.flatMap(({ package: name, version, manifest }) => version !== void 0 && manifest ? [[name, {
1617
+ package: name,
1618
+ version,
1619
+ manifest
1620
+ }]] : []));
1656
1621
  }
1657
1622
  async connect(info) {
1658
1623
  this.#session.info = info;
@@ -1675,11 +1640,26 @@ var Agent = class extends capnweb.RpcTarget {
1675
1640
  plugins: config.plugins.map((plugin) => ({
1676
1641
  name: toStudioName({
1677
1642
  name: (0, _kubb_core.resolveMeta)(plugin).name,
1678
- root,
1679
1643
  extensions
1680
1644
  }),
1681
1645
  options: toJSONOptions(plugin.options)
1682
1646
  })),
1647
+ adapter: config.adapter && {
1648
+ name: toStudioName({
1649
+ name: (0, _kubb_core.resolveMeta)(config.adapter).name,
1650
+ extensions,
1651
+ kind: "adapter"
1652
+ }),
1653
+ options: toJSONOptions(config.adapter.options)
1654
+ },
1655
+ parsers: config.parsers.map((parser) => ({
1656
+ name: toStudioName({
1657
+ name: (0, _kubb_core.resolveMeta)(parser).name,
1658
+ extensions,
1659
+ kind: "parser"
1660
+ }),
1661
+ options: toJSONOptions(parser.options)
1662
+ })),
1683
1663
  extensions: [...extensions.values()]
1684
1664
  },
1685
1665
  permissions: this.#permissions
@@ -1739,11 +1719,19 @@ var Agent = class extends capnweb.RpcTarget {
1739
1719
  const studioInput = this.#sandbox ? patch?.input : void 0;
1740
1720
  if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || (0, _kubb_core.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" });
1741
1721
  const [config, extensions] = await Promise.all([this.#options.loadConfig(), this.#installedExtensions()]);
1742
- const [plugins, adapter] = await Promise.all([mergePlugins({
1743
- diskPlugins: config.plugins,
1744
- studioPlugins: patch?.plugins,
1745
- extensions
1746
- }), mergeAdapter(config.adapter, patch?.adapter)]).catch(async (error) => {
1722
+ const [plugins, adapter, parsers] = await Promise.all([
1723
+ mergePlugins({
1724
+ diskPlugins: config.plugins,
1725
+ studioPlugins: patch?.plugins,
1726
+ extensions
1727
+ }),
1728
+ mergeAdapter(config.adapter, patch?.adapter, extensions),
1729
+ mergeParsers({
1730
+ diskParsers: config.parsers,
1731
+ studioParsers: patch?.parsers,
1732
+ extensions
1733
+ })
1734
+ ]).catch(async (error) => {
1747
1735
  await emit(this.#options.hooks, "kubb:error", { error: toError(error) });
1748
1736
  throw error;
1749
1737
  });
@@ -1767,7 +1755,8 @@ var Agent = class extends capnweb.RpcTarget {
1767
1755
  input: studioInput ?? config.input,
1768
1756
  storage: this.#permissions.allowWrite ? (0, _kubb_core.fsStorage)() : (0, _kubb_core.memoryStorage)(),
1769
1757
  plugins: activePlugins,
1770
- adapter
1758
+ adapter,
1759
+ parsers
1771
1760
  },
1772
1761
  hooks: this.#options.hooks,
1773
1762
  signal,
@@ -1829,7 +1818,7 @@ var Agent = class extends capnweb.RpcTarget {
1829
1818
  }
1830
1819
  if (this.#session.active) return { value: refuse("a generation is in progress") };
1831
1820
  try {
1832
- const { source, outcomes, changed } = await writeConfigEdits({
1821
+ const { outcomes, changed } = await writeConfigEdits({
1833
1822
  filePath: configFile,
1834
1823
  edits,
1835
1824
  extensions: await this.#installedExtensions()
@@ -1838,8 +1827,7 @@ var Agent = class extends capnweb.RpcTarget {
1838
1827
  return {
1839
1828
  value: {
1840
1829
  outcomes,
1841
- changed,
1842
- file: changed ? await this.#readConfigFileView(source) : void 0
1830
+ changed
1843
1831
  },
1844
1832
  info: `applied ${applied}/${outcomes.length} edits to ${configPath}`
1845
1833
  };
@@ -2320,8 +2308,16 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2320
2308
  if (content === null) throw new Error(`The generation listed ${path} but did not keep its content`);
2321
2309
  return [path, content];
2322
2310
  })));
2323
- const { peerDependencies, missingDependencies } = await resolvePeerDependencies(config.plugins.map((plugin) => (0, _kubb_core.resolveMeta)(plugin).name));
2324
- const missing = missingDependencies.filter((dependency) => !BUNDLED_DEPENDENCIES.includes(dependency));
2311
+ const pluginNames = config.plugins.map((plugin) => (0, _kubb_core.resolveMeta)(plugin).name);
2312
+ const installed = await (0, _kubb_core.readInstalledPackages)({
2313
+ root: config.root,
2314
+ packages: pluginNames.map(toPackageName),
2315
+ resolveFrom: ["kubb"]
2316
+ });
2317
+ const packageOf = (name) => installed.find(({ manifest }) => manifest?.kind === "plugin" && manifest.name === name)?.package ?? toPackageName(name);
2318
+ const names = [...new Set(pluginNames.map(packageOf))];
2319
+ const peerDependencies = Object.fromEntries(installed.flatMap(({ package: name, version }) => names.includes(name) && version ? [[name, version]] : []));
2320
+ const missing = names.filter((name) => !(name in peerDependencies) && !BUNDLED_DEPENDENCIES.includes(name));
2325
2321
  if (missing.length) throw new Error(`Missing dependencies: ${missing.join(", ")}`);
2326
2322
  const bytes = await createSnapshotPackage({
2327
2323
  files,