@kubb/studio 5.12.3 → 5.12.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -59,12 +59,23 @@ let node_zlib = require("node:zlib");
59
59
  *
60
60
  * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
61
61
  */
62
- function toCamelOrPascal(text, pascal) {
62
+ function convert(text, pascal) {
63
63
  return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
64
64
  if (word.length > 1 && word === word.toUpperCase()) return word;
65
65
  return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
66
66
  }).join("").replace(/[^a-zA-Z0-9]/g, "");
67
67
  }
68
+ const cache = /* @__PURE__ */ new Map();
69
+ const cacheLimit = 1e4;
70
+ function toCamelOrPascal(text, pascal) {
71
+ const key = `${pascal ? "P" : "c"}${text}`;
72
+ const cached = cache.get(key);
73
+ if (cached !== void 0) return cached;
74
+ const result = convert(text, pascal);
75
+ if (cache.size >= cacheLimit) cache.clear();
76
+ cache.set(key, result);
77
+ return result;
78
+ }
68
79
  /**
69
80
  * Converts `text` to camelCase.
70
81
  *
@@ -230,7 +241,7 @@ function getElapsedMs(hrStart) {
230
241
  }
231
242
  //#endregion
232
243
  //#region package.json
233
- var version = "5.12.3";
244
+ var version = "5.12.5";
234
245
  //#endregion
235
246
  //#region src/resolveConfig.ts
236
247
  /**
@@ -586,6 +597,44 @@ async function rebuildExtension({ disk, studioOptions, kind, extensions }) {
586
597
  });
587
598
  }))(mergeOptions(disk.options ?? {}, studioOptions));
588
599
  }
600
+ /** The root `output` members Studio may set, with the values each takes. The rest, `path` and `postGenerate` among them, stay with the config file. */
601
+ const OUTPUT_VALUES = {
602
+ barrel: (value) => value === false || isPlainObject(value) && Object.keys(value).length === 1 && (value.type === "named" || value.type === "all"),
603
+ format: (value) => value === false || [
604
+ "auto",
605
+ "prettier",
606
+ "biome",
607
+ "oxfmt"
608
+ ].includes(value),
609
+ lint: (value) => value === false || [
610
+ "auto",
611
+ "eslint",
612
+ "biome",
613
+ "oxlint"
614
+ ].includes(value),
615
+ clean: (value) => typeof value === "boolean",
616
+ defaultBanner: (value) => value === false || value === "simple" || value === "full"
617
+ };
618
+ /** Whether Studio may set the root `output` member `key`. */
619
+ function isOutputKey(key) {
620
+ return Object.hasOwn(OUTPUT_VALUES, key);
621
+ }
622
+ /** Whether Studio may set the root `output` member `key` to `value`. */
623
+ function isOutputValue({ key, value }) {
624
+ return isOutputKey(key) && OUTPUT_VALUES[key](value);
625
+ }
626
+ /** The members of `output` Studio may see and set. */
627
+ function pickOutput(output) {
628
+ return Object.fromEntries(Object.entries(output).filter(([key]) => isOutputKey(key)));
629
+ }
630
+ /** Merges Studio's valid `output` overrides, with `null` unsetting a member. */
631
+ function mergeOutput({ output, studio }) {
632
+ if (!isPlainObject(studio)) return output;
633
+ return mergeOptions(output, Object.fromEntries(Object.entries(studio).filter(([key, value]) => value === null ? isOutputKey(key) : isOutputValue({
634
+ key,
635
+ value
636
+ }))));
637
+ }
589
638
  /**
590
639
  * Merges Studio-provided adapter option overrides into the disk config's adapter, see {@link rebuildExtension}.
591
640
  */
@@ -643,14 +692,16 @@ function invalidEdit(edit) {
643
692
  if (typeof operation !== "string" || !OPERATIONS.has(operation)) return `unknown operation ${JSON.stringify(operation)}`;
644
693
  if (config !== void 0 && typeof config !== "string" && !Number.isSafeInteger(config)) return "config must name an entry or give its index";
645
694
  if (operation === require_protocol.ConfigEditOperation.SET_INPUT) return typeof input === "string" && input.trim() ? void 0 : "the edit names no input";
646
- if (typeof plugin !== "string" || !plugin) return "the edit names no plugin";
695
+ const isOutput = operation === require_protocol.ConfigEditOperation.SET_OUTPUT || operation === require_protocol.ConfigEditOperation.REMOVE_OUTPUT;
696
+ if (!isOutput && (typeof plugin !== "string" || !plugin)) return "the edit names no plugin";
647
697
  if (kind !== void 0 && (typeof kind !== "string" || !KINDS.has(kind))) return `unknown kind ${JSON.stringify(kind)}`;
648
698
  if (kind !== void 0 && operation !== require_protocol.ConfigEditOperation.SET && operation !== require_protocol.ConfigEditOperation.REMOVE) return `${operation} edits the plugins array, so it takes no kind`;
649
- if (operation === require_protocol.ConfigEditOperation.SET || operation === require_protocol.ConfigEditOperation.REMOVE) {
699
+ if (isOutput || operation === require_protocol.ConfigEditOperation.SET || operation === require_protocol.ConfigEditOperation.REMOVE) {
650
700
  if (!Array.isArray(path) || !path.every((key) => typeof key === "string" && key)) return "path must be a list of option names";
651
701
  if (!path.length) return "no option path given";
652
702
  const unsafe = path.find((key) => !isSafeKey(key));
653
703
  if (unsafe !== void 0) return `${unsafe} cannot be used as an option name`;
704
+ if (isOutput && !isOutputKey(path[0])) return `output.${path[0]} can only be changed in the config file`;
654
705
  }
655
706
  }
656
707
  /** Where a node sits in the source text. */
@@ -951,9 +1002,12 @@ function readPartial(node, prefix = []) {
951
1002
  }
952
1003
  /** The top-level options of a plugin call, each flagged with whether Studio may write it. */
953
1004
  function readOptions(call) {
954
- const entries = {};
955
1005
  const options = call.arguments[0];
956
- if (options?.type !== "ObjectExpression") return entries;
1006
+ return options?.type === "ObjectExpression" ? readObject(options) : {};
1007
+ }
1008
+ /** The members of an object literal, each flagged with whether Studio may write it. */
1009
+ function readObject(options) {
1010
+ const entries = {};
957
1011
  for (const [index, entry] of options.properties.entries()) {
958
1012
  const key = memberKey(entry);
959
1013
  if (key === void 0 || !isSafeKey(key)) continue;
@@ -1026,11 +1080,13 @@ function readConfig(source) {
1026
1080
  });
1027
1081
  const [adapter] = factoryCalls(mod, config, "adapter").map(toView);
1028
1082
  const parsers = factoryCalls(mod, config, "parser").map(toView);
1083
+ const output = outputObject(config);
1029
1084
  return {
1030
1085
  name: configName(config),
1031
1086
  plugins,
1032
1087
  ...adapter && { adapter },
1033
- ...parsers.length ? { parsers } : {}
1088
+ ...parsers.length ? { parsers } : {},
1089
+ ...!("reason" in output) && { output: pickOutput(readObject(output.object)) }
1034
1090
  };
1035
1091
  })
1036
1092
  };
@@ -1079,6 +1135,16 @@ function applySet(source, call, path, value, quote) {
1079
1135
  return { source: splice(source, closing, closing, printValue(nest(path, value), quote)) };
1080
1136
  }
1081
1137
  if (options.type !== "ObjectExpression") return { reason: "the factory was not called with an object literal" };
1138
+ return setIn({
1139
+ source,
1140
+ options,
1141
+ path,
1142
+ value,
1143
+ quote
1144
+ });
1145
+ }
1146
+ /** Writes the literal `value` at `path` inside `options`, adding any intermediate object it needs. */
1147
+ function setIn({ source, options, path, value, quote }) {
1082
1148
  const target = walk(options, path);
1083
1149
  if ("reason" in target) return target;
1084
1150
  if ("missing" in target) {
@@ -1096,6 +1162,34 @@ function applySetInput(source, config, input, quote) {
1096
1162
  if (isShadowed(config, found.index)) return { reason: "a spread after input could override it" };
1097
1163
  return { source: splice(source, rangeOf(found.member.value).start, rangeOf(found.member.value).end, value) };
1098
1164
  }
1165
+ /** The config's root `output` object literal, or why Studio cannot edit inside it. */
1166
+ function outputObject(config) {
1167
+ const found = findMember(config, "output");
1168
+ if (found?.member.type !== "ObjectProperty" || found.member.value.type !== "ObjectExpression") return { reason: "output is not an object literal" };
1169
+ if (isShadowed(config, found.index)) return { reason: "a spread after output could override it" };
1170
+ return { object: found.member.value };
1171
+ }
1172
+ /** Writes or drops a member of the config's root `output` object. */
1173
+ function applyOutputEdit({ source, config, edit, quote }) {
1174
+ const output = outputObject(config);
1175
+ if ("reason" in output) return output;
1176
+ if (edit.operation === require_protocol.ConfigEditOperation.REMOVE_OUTPUT) return removeIn({
1177
+ source,
1178
+ options: output.object,
1179
+ path: edit.path
1180
+ });
1181
+ if (!isOptionValue(edit.value) || !isOutputValue({
1182
+ key: edit.path[0],
1183
+ value: nest(edit.path.slice(1), edit.value)
1184
+ })) return { reason: `the value is not one output.${edit.path[0]} takes` };
1185
+ return setIn({
1186
+ source,
1187
+ options: output.object,
1188
+ path: edit.path,
1189
+ value: edit.value,
1190
+ quote
1191
+ });
1192
+ }
1099
1193
  /**
1100
1194
  * Deletes the option at `path`, falling the plugin back to its default for it. An option the file
1101
1195
  * does not set already has its default, so removing it changes nothing.
@@ -1104,6 +1198,14 @@ function applyRemove(source, call, path) {
1104
1198
  const options = call.arguments[0];
1105
1199
  if (!options) return { source };
1106
1200
  if (options.type !== "ObjectExpression") return { reason: "the factory was not called with an object literal" };
1201
+ return removeIn({
1202
+ source,
1203
+ options,
1204
+ path
1205
+ });
1206
+ }
1207
+ /** Deletes the member at `path` inside `options`. A path that is not there already holds its default. */
1208
+ function removeIn({ source, options, path }) {
1107
1209
  const target = walk(options, path);
1108
1210
  if ("reason" in target) return target;
1109
1211
  if ("missing" in target) return { source };
@@ -1193,6 +1295,12 @@ function applyEdit({ source, edit, quote, extensions }) {
1193
1295
  const config = findConfig(parsed);
1194
1296
  if (!config) return { reason: `no config entry found for ${JSON.stringify(ref)}` };
1195
1297
  if (edit.operation === require_protocol.ConfigEditOperation.SET_INPUT) return applySetInput(source, config, edit.input, quote);
1298
+ if (edit.operation === require_protocol.ConfigEditOperation.SET_OUTPUT || edit.operation === require_protocol.ConfigEditOperation.REMOVE_OUTPUT) return applyOutputEdit({
1299
+ source,
1300
+ config,
1301
+ edit,
1302
+ quote
1303
+ });
1196
1304
  if (edit.operation === require_protocol.ConfigEditOperation.DISABLE_PLUGIN) {
1197
1305
  const result = disablePlugin(source, parsed.mod, config, edit.plugin);
1198
1306
  if (!("reason" in result) || !result.reason.includes("shares a line")) return result;
@@ -2392,6 +2500,10 @@ var Agent = class extends capnweb.RpcTarget {
2392
2500
  ...config,
2393
2501
  root,
2394
2502
  input: checkout?.path ?? studioInput ?? config.input,
2503
+ output: mergeOutput({
2504
+ output: config.output,
2505
+ studio: patch?.output
2506
+ }),
2395
2507
  storage: outputRoot ? stagingStorage({
2396
2508
  root,
2397
2509
  directory: outputRoot
@@ -2851,6 +2963,7 @@ function toEntryView(config, extensions) {
2851
2963
  ...config.name !== void 0 && { name: config.name },
2852
2964
  ...input !== void 0 && { input },
2853
2965
  output: config.output.path,
2966
+ outputOptions: toJSONOptions(pickOutput(config.output)),
2854
2967
  plugins: config.plugins.map((plugin) => ({
2855
2968
  name: toStudioName({
2856
2969
  name: (0, _kubb_core.resolveMeta)(plugin).name,