@kubb/studio 5.5.3 → 5.5.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
@@ -1,54 +1,22 @@
1
1
  import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
2
+ import { a as toPackageName, n as mergeAdapter, o as read, r as mergePlugins } from "./resolveConfig-DMIG_MH-.js";
2
3
  import { AGENT_INSTANCE_HEADER, AgentCloseCode, GENERATION_GONE_MESSAGE, MAX_PAGE_LINES, generationEventTypes } from "./protocol.js";
3
4
  import { createRequire } from "node:module";
4
5
  import { createHash, hash, randomBytes, randomUUID } from "node:crypto";
5
6
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
6
7
  import process$1 from "node:process";
7
8
  import { spawn } from "node:child_process";
8
- import { glob, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
9
- import path, { isAbsolute, join, relative, resolve, sep } from "node:path";
9
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
10
+ import path, { isAbsolute, relative, resolve, sep } from "node:path";
10
11
  import { FetchError, ofetch } from "ofetch";
11
- import { promisify, styleText } from "node:util";
12
+ import { styleText } from "node:util";
12
13
  import { createStorage } from "unstorage";
13
14
  import fsDriver from "unstorage/drivers/fs";
14
15
  import { tmpdir } from "node:os";
15
16
  import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, memoryStorage, resolveCacheDir } from "@kubb/core";
16
- import { builders, detectCodeFormat, generateCode, parseModule } from "magicast";
17
- import { existsSync } from "node:fs";
18
- import { pathToFileURL } from "node:url";
19
- import { isPlainObject } from "remeda";
20
- import { gzip } from "node:zlib";
21
- import { build } from "tsdown";
22
17
  import { RpcTarget, newWebSocketRpcSession } from "capnweb";
23
18
  import WebSocket from "ws";
24
19
  import { x } from "tinyexec";
25
- //#region ../../internals/utils/src/casing.ts
26
- /**
27
- * Shared implementation for camelCase and PascalCase conversion.
28
- * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
29
- * and capitalizes each word according to `pascal`.
30
- *
31
- * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
32
- */
33
- function toCamelOrPascal(text, pascal) {
34
- 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) => {
35
- if (word.length > 1 && word === word.toUpperCase()) return word;
36
- return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
37
- }).join("").replace(/[^a-zA-Z0-9]/g, "");
38
- }
39
- /**
40
- * Converts `text` to camelCase.
41
- *
42
- * @example Word boundaries
43
- * `camelCase('hello-world') // 'helloWorld'`
44
- *
45
- * @example With a prefix
46
- * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
47
- */
48
- function camelCase(text, { prefix = "", suffix = "" } = {}) {
49
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
50
- }
51
- //#endregion
52
20
  //#region ../../internals/utils/src/errors.ts
53
21
  /**
54
22
  * Coerces an unknown thrown value to an `Error` instance.
@@ -77,91 +45,6 @@ function getErrorMessage(value) {
77
45
  return value instanceof Error ? value.message : String(value);
78
46
  }
79
47
  //#endregion
80
- //#region ../../internals/utils/src/runtime.ts
81
- /**
82
- * Detects the JavaScript runtime executing the current process and exposes its name and version.
83
- *
84
- * Prefer the shared {@link runtime} instance over constructing your own.
85
- */
86
- var Runtime = class {
87
- /**
88
- * `true` when the current process is running under Bun.
89
- *
90
- * Detection keys off the global `Bun` object rather than `process.versions`,
91
- * because Bun polyfills `process.versions.node` for Node compatibility and would
92
- * otherwise look like Node.
93
- *
94
- * @example
95
- * ```ts
96
- * if (runtime.isBun) {
97
- * await Bun.write(path, data)
98
- * }
99
- * ```
100
- */
101
- get isBun() {
102
- return typeof Bun !== "undefined";
103
- }
104
- /**
105
- * `true` when the current process is running under Deno.
106
- */
107
- get isDeno() {
108
- return typeof globalThis.Deno !== "undefined";
109
- }
110
- /**
111
- * `true` when the current process is running under Node.
112
- *
113
- * Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
114
- */
115
- get isNode() {
116
- return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
117
- }
118
- /**
119
- * Name of the runtime executing the current process.
120
- *
121
- * @example
122
- * ```ts
123
- * runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
124
- * ```
125
- */
126
- get name() {
127
- if (this.isBun) return "bun";
128
- if (this.isDeno) return "deno";
129
- return "node";
130
- }
131
- /**
132
- * Version of the active runtime, or an empty string when it cannot be read.
133
- *
134
- * @example
135
- * ```ts
136
- * runtime.version // '1.3.11' under Bun, '22.22.2' under Node
137
- * ```
138
- */
139
- get version() {
140
- if (this.isBun) return process.versions.bun ?? "";
141
- if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
142
- return process.versions?.node ?? "";
143
- }
144
- };
145
- /**
146
- * Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
147
- */
148
- const runtime = new Runtime();
149
- //#endregion
150
- //#region ../../internals/utils/src/fs.ts
151
- /**
152
- * Reads the file at `path` as a UTF-8 string.
153
- * Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.
154
- *
155
- * @example
156
- * ```ts
157
- * const source = await read('./src/Pet.ts')
158
- * ```
159
- */
160
- async function read(path) {
161
- if (runtime.isBun) return Bun.file(path).text();
162
- return readFile(path, { encoding: "utf8" });
163
- }
164
- //#endregion
165
48
  //#region ../../internals/utils/src/promise.ts
166
49
  /**
167
50
  * Wraps `factory` with a keyed cache backed by the provided store.
@@ -695,944 +578,7 @@ function resolveAgentCapacity(env = process.env) {
695
578
  }
696
579
  //#endregion
697
580
  //#region package.json
698
- var version = "5.5.3";
699
- //#endregion
700
- //#region src/operations/resolveConfig.ts
701
- /**
702
- * Imports a package, falling back to how the user's project would resolve it.
703
- *
704
- * `import()` resolves from this file, so a linked or globally installed Studio (`pnpm link`,
705
- * `npm i -g`) only sees its own `node_modules` and misses the plugins installed next to the user's
706
- * config. The retry resolves from `process.cwd()` instead.
707
- */
708
- async function importFromProject(packageName) {
709
- try {
710
- return await import(packageName);
711
- } catch {
712
- const resolved = createRequire(pathToFileURL(`${process.cwd()}/`)).resolve(packageName);
713
- const esm = resolved.replace(/\.cjs$/, ".js");
714
- return await import(pathToFileURL(esm !== resolved && existsSync(esm) ? esm : resolved).href);
715
- }
716
- }
717
- /**
718
- * Strips the `@kubb/` scope from a plugin package name, matching the `name` convention Kubb
719
- * plugin factories use internally.
720
- *
721
- * @example
722
- * ```ts
723
- * toPluginName('@kubb/plugin-ts') // 'plugin-ts'
724
- * ```
725
- */
726
- function toPluginName(packageName) {
727
- return packageName.split("/").pop() ?? packageName;
728
- }
729
- /**
730
- * Adds the `@kubb/` scope a plugin's package carries but its `name` does not, the inverse of
731
- * {@link toPluginName}. A name outside the `plugin-` convention is left alone, so a third-party
732
- * plugin is not reported as one of Kubb's.
733
- *
734
- * @example
735
- * ```ts
736
- * toPackageName('plugin-ts') // '@kubb/plugin-ts'
737
- * toPackageName('@acme/my-plugin') // '@acme/my-plugin'
738
- * ```
739
- */
740
- function toPackageName(name) {
741
- return name.startsWith("plugin-") ? `@kubb/${name}` : name;
742
- }
743
- /**
744
- * Derives the conventional named export for a `@kubb/*` plugin package from its package name.
745
- *
746
- * @example
747
- * ```ts
748
- * toExportName('@kubb/plugin-react-query') // 'pluginReactQuery'
749
- * toExportName('@kubb/plugin-ts') // 'pluginTs'
750
- * ```
751
- */
752
- function toExportName(packageName) {
753
- return camelCase(toPluginName(packageName));
754
- }
755
- /**
756
- * A `@kubb/plugin-*` package specifier. Nothing else may reach `import()`: only Kubb's own
757
- * plugins are supported, so a payload naming anything else, a third-party package or a path, is
758
- * refused before it can execute.
759
- */
760
- const KUBB_PLUGIN_SPECIFIER = /^@kubb\/plugin-[\w.-]+$/;
761
- /**
762
- * Whether `name` is a `@kubb/plugin-*` specifier. Exported so `configFile.ts` can refuse the same
763
- * shape before printing a Studio-supplied plugin name into the config file's source text.
764
- */
765
- function isKubbPluginSpecifier(name) {
766
- return KUBB_PLUGIN_SPECIFIER.test(name);
767
- }
768
- /**
769
- * Dynamically imports a `@kubb/plugin-*` package and returns its factory function.
770
- *
771
- * Packages must be pre-installed in the Docker image at build time via the `KUBB_PACKAGES`
772
- * build ARG, no runtime installation is possible in the distroless container.
773
- *
774
- * Resolution order: the camelCase named export the package name implies (e.g. `pluginTs`), then
775
- * the default export.
776
- *
777
- * @throws if the package cannot be imported or exports no callable factory.
778
- */
779
- async function loadPluginFactory(packageName) {
780
- if (!isKubbPluginSpecifier(packageName)) throw new Error(`Plugin "${packageName}" is not a @kubb/plugin-* package. Kubb Studio only supports Kubb's own plugins.`);
781
- let mod;
782
- try {
783
- mod = await importFromProject(packageName);
784
- } catch (cause) {
785
- throw new Error(`Plugin "${packageName}" could not be loaded. Make sure it is installed: \`npm install ${packageName}\``, { cause });
786
- }
787
- const exportName = toExportName(packageName);
788
- if (typeof mod[exportName] === "function") return mod[exportName];
789
- if (typeof mod["default"] === "function") return mod["default"];
790
- throw new Error(`Plugin "${packageName}" does not export a callable factory. Tried the named export "${exportName}" and "default".`);
791
- }
792
- /**
793
- * Resolves each plugin entry by dynamically importing the `@kubb/plugin-*` package and
794
- * calling its factory with the provided options.
795
- *
796
- * Packages must be pre-installed in the Docker image at build time, use the `KUBB_PACKAGES`
797
- * build ARG to control which ones are available at runtime.
798
- *
799
- * @example
800
- * ```ts
801
- * { name: '@kubb/plugin-react-query', options: { output: { path: './hooks' } } }
802
- * { name: '@kubb/plugin-ts', options: { output: { path: './types' } } }
803
- * ```
804
- */
805
- async function resolvePlugins(plugins) {
806
- return Promise.all(plugins.map(async ({ name, options }) => {
807
- return (await loadPluginFactory(name))(options ?? {});
808
- }));
809
- }
810
- /**
811
- * Whether `value` comes back unchanged from a JSON round trip. A function, a `RegExp` or another
812
- * class instance does not: the agent reports options to Studio as JSON, so what Studio sends back
813
- * for such a value is a lossy copy (`macros: [{ name }]` without its hooks, `pattern: {}`).
814
- */
815
- function survivesJson(value) {
816
- if (value === null || typeof value === "string" || typeof value === "boolean") return true;
817
- if (typeof value === "number") return Number.isFinite(value);
818
- if (Array.isArray(value)) return value.every(survivesJson);
819
- if (isPlainObject(value)) return Object.values(value).every(survivesJson);
820
- return false;
821
- }
822
- /**
823
- * Deep-merges Studio's options over the disk options, with Studio taking priority, except where the
824
- * disk value cannot survive JSON. Studio only ever saw a lossy copy of such a value, so the disk
825
- * value is kept rather than replaced by what Studio echoes back. Plain objects merge key by key, so
826
- * Studio can still change a literal that sits next to a function (e.g. `group.type` beside `group.name`).
827
- *
828
- * @example
829
- * ```ts
830
- * mergeOptions({ macros: [camelCaseMacro], syntaxType: 'type' }, { macros: [{ name: 'camelCase' }], syntaxType: 'interface' })
831
- * // { macros: [camelCaseMacro], syntaxType: 'interface' }
832
- * ```
833
- */
834
- function mergeOptions(disk, studio) {
835
- const merged = { ...disk };
836
- for (const [key, value] of Object.entries(studio)) {
837
- const current = disk[key];
838
- if (isPlainObject(current) && isPlainObject(value)) {
839
- merged[key] = mergeOptions(current, value);
840
- continue;
841
- }
842
- if (current !== void 0 && !survivesJson(current)) continue;
843
- merged[key] = value;
844
- }
845
- return merged;
846
- }
847
- /**
848
- * Merges studio plugin options with disk config plugins.
849
- * Studio takes priority: options from studio win over disk (except values only the disk config
850
- * can express, see {@link mergeOptions}), and a plugin Studio explicitly
851
- * disabled is dropped even when the disk config still lists it. Disk plugins without a studio
852
- * counterpart are kept as-is. Studio plugins not present on disk are appended.
853
- *
854
- * For plugins present in both configs, the plugin is re-instantiated with merged options
855
- * so that all internal closures correctly reference the merged values.
856
- */
857
- async function mergePlugins(diskPlugins, studioPlugins) {
858
- const disabledNames = new Set((studioPlugins ?? []).filter((entry) => entry.disabled).map((entry) => toPluginName(entry.name)));
859
- const activeDiskPlugins = disabledNames.size ? diskPlugins?.filter((plugin) => !disabledNames.has(plugin.name)) : diskPlugins;
860
- const activeStudioPlugins = studioPlugins?.filter((entry) => !entry.disabled);
861
- if (!activeDiskPlugins && !activeStudioPlugins?.length) return void 0;
862
- if (!activeStudioPlugins?.length) return activeDiskPlugins;
863
- if (!activeDiskPlugins) return resolvePlugins(activeStudioPlugins);
864
- const studioEntryByName = new Map(activeStudioPlugins.map((entry) => [toPluginName(entry.name), entry]));
865
- const diskNames = new Set(activeDiskPlugins.map((plugin) => plugin.name));
866
- const merged = await Promise.all(activeDiskPlugins.map(async (diskPlugin) => {
867
- const studioEntry = studioEntryByName.get(diskPlugin.name);
868
- if (!studioEntry) return diskPlugin;
869
- const options = mergeOptions(diskPlugin.options ?? {}, studioEntry.options ?? {});
870
- const [resolved] = await resolvePlugins([{
871
- name: studioEntry.name,
872
- options
873
- }]);
874
- return resolved ?? diskPlugin;
875
- }));
876
- const studioOnly = activeStudioPlugins.filter((entry) => !diskNames.has(toPluginName(entry.name)));
877
- return [...merged, ...await resolvePlugins(studioOnly)];
878
- }
879
- /**
880
- * Merges Studio-provided adapter option overrides into the disk config's adapter.
881
- *
882
- * Adapter instances carry live functions (`parse`, `getImports`, ...) that can't survive
883
- * JSON serialization over the WebSocket, so `studioOptions` is treated as an options patch
884
- * rather than a replacement adapter. Re-invokes the same `@kubb/adapter-<name>` factory the
885
- * disk config used, with the merged options, so the resulting instance has fresh closures
886
- * over the merged values instead of a plain object missing `parse`.
887
- */
888
- async function mergeAdapter(diskAdapter, studioOptions) {
889
- if (!studioOptions || !diskAdapter) return diskAdapter;
890
- const packageName = `@kubb/adapter-${diskAdapter.name}`;
891
- const factory = (await importFromProject(packageName))[toExportName(packageName)];
892
- if (typeof factory !== "function") return diskAdapter;
893
- return factory(mergeOptions(diskAdapter.options ?? {}, studioOptions));
894
- }
895
- //#endregion
896
- //#region src/operations/configFile.ts
897
- /**
898
- * A valid JavaScript identifier, so an import name can only ever print as `import { name } from`,
899
- * never as source that breaks out of the import statement.
900
- */
901
- const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
902
- /**
903
- * `key: value` as an object literal property, in the file's quote and key style.
904
- *
905
- * Uses magicast's literal builder for the key/value nodes, then wraps them as a Babel
906
- * `ObjectProperty`, the type the rest of this file reads.
907
- */
908
- function literalProperty({ key, value }) {
909
- const built = builders.literal({ [key]: value }).properties[0];
910
- return {
911
- type: "ObjectProperty",
912
- key: built.key,
913
- value: built.value,
914
- computed: false,
915
- shorthand: false
916
- };
917
- }
918
- /**
919
- * Marks the comment block a `disable-plugin` leaves behind, so `enable-plugin` can find its way
920
- * back to the exact lines it commented out. Carries the block's line count, so `enable-plugin`
921
- * restores exactly those lines instead of scanning forward through whatever comments follow.
922
- */
923
- const DISABLED_MARKER = "kubb:disabled";
924
- /**
925
- * The one line `disable-plugin` writes above the comment block it produces for `plugin`.
926
- */
927
- function formatMarker(plugin, lineCount, indent = "") {
928
- return `${indent}// ${DISABLED_MARKER} ${plugin} ${lineCount}`;
929
- }
930
- /**
931
- * The plugin and comment-block length a marker line names, when `line` is one.
932
- */
933
- function parseMarker(line) {
934
- const trimmed = line.trim();
935
- if (!trimmed.startsWith(`// ${DISABLED_MARKER} `)) return;
936
- const match = trimmed.slice(`// ${DISABLED_MARKER} `.length).match(/^(.+)\s+(\d+)$/);
937
- return match ? {
938
- plugin: match[1],
939
- lineCount: Number(match[2])
940
- } : void 0;
941
- }
942
- /**
943
- * Steps through a config's wrappers to the object literal underneath: a `satisfies`/`as`
944
- * assertion, a `() => ...` factory, or a factory whose block body returns the config.
945
- */
946
- function unwrap(node) {
947
- if (!node) return;
948
- if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression") return unwrap(node.expression);
949
- if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") return node;
950
- if (node.body.type !== "BlockStatement") return unwrap(node.body);
951
- return unwrap(node.body.body.find((statement) => statement.type === "ReturnStatement")?.argument);
952
- }
953
- /**
954
- * Every config object in `export default defineConfig(...)`, or why the file is unmanaged.
955
- *
956
- * An array export gets one entry per element, matching {@link ConfigRef}'s numeric index.
957
- *
958
- * Walks the parsed AST rather than magicast's proxies, which throw on node types they cannot
959
- * cast, most of what an unmanaged config file is made of.
960
- */
961
- function findConfigs(mod) {
962
- const exported = unwrap((mod.$ast.type === "Program" ? mod.$ast.body : []).find((node) => node.type === "ExportDefaultDeclaration")?.declaration);
963
- if (!exported) return { reason: "no default export found" };
964
- if (exported.type !== "CallExpression" || exported.callee.type !== "Identifier" || exported.callee.name !== "defineConfig") return { reason: "default export is not a defineConfig(...) call" };
965
- const argument = unwrap(exported.arguments[0]);
966
- if (!argument) return { reason: "defineConfig(...) was called without a config" };
967
- if (argument.type === "ArrayExpression") {
968
- const configs = [];
969
- for (const element of argument.elements) {
970
- const entry = unwrap(element);
971
- if (entry?.type !== "ObjectExpression") return { reason: "config is not an object literal" };
972
- configs.push(entry);
973
- }
974
- return { configs };
975
- }
976
- if (argument.type !== "ObjectExpression") return { reason: "config is not an object literal" };
977
- return { configs: [argument] };
978
- }
979
- /**
980
- * The config entry an edit names, defaulting to the first when it names none.
981
- */
982
- function selectConfig(configs, ref) {
983
- if (ref === void 0) return configs[0];
984
- if (typeof ref === "number") return configs[ref];
985
- return configs.find((config) => configName(config) === ref);
986
- }
987
- function configName(config) {
988
- const name = property(config, "name");
989
- return name?.type === "StringLiteral" ? name.value : void 0;
990
- }
991
- /**
992
- * The name of an object literal property, for the two key shapes a config uses: `key: value` and
993
- * `'key': value`. `undefined` for a computed key, which the patcher never touches.
994
- */
995
- function propertyKey(entry) {
996
- if (entry.key.type === "Identifier") return entry.key.name;
997
- if (entry.key.type === "StringLiteral") return entry.key.value;
998
- }
999
- /**
1000
- * The index of an object literal's own property named `key`, `-1` when it has none.
1001
- */
1002
- function entryIndex({ node, key }) {
1003
- return node.properties.findIndex((entry) => entry.type === "ObjectProperty" && propertyKey(entry) === key);
1004
- }
1005
- /**
1006
- * The value node of an object literal's own property.
1007
- */
1008
- function property(node, key) {
1009
- const index = entryIndex({
1010
- node,
1011
- key
1012
- });
1013
- return index === -1 ? void 0 : node.properties[index].value;
1014
- }
1015
- /**
1016
- * Writes `key: value` on an object literal, replacing the value when the property is already there.
1017
- *
1018
- * An existing property has its value swapped in place rather than being replaced whole, so recast
1019
- * reprints only that value and leaves the object's own layout alone.
1020
- */
1021
- function setProperty({ node, key, value }) {
1022
- const entry = literalProperty({
1023
- key,
1024
- value
1025
- });
1026
- const index = entryIndex({
1027
- node,
1028
- key
1029
- });
1030
- if (index === -1) {
1031
- node.properties.push(entry);
1032
- return;
1033
- }
1034
- node.properties[index].value = entry.value;
1035
- }
1036
- /**
1037
- * Drops `key` from an object literal.
1038
- */
1039
- function removeProperty({ node, key }) {
1040
- const index = entryIndex({
1041
- node,
1042
- key
1043
- });
1044
- if (index !== -1) node.properties.splice(index, 1);
1045
- }
1046
- /**
1047
- * Reads a literal node's value: a primitive, or an object/array built only from primitives.
1048
- * `undefined` for anything else, so a caller can use this both to read a value and to check
1049
- * whether a node is a literal at all.
1050
- */
1051
- function readLiteral(node) {
1052
- if (!node) return;
1053
- if (node.type === "StringLiteral" || node.type === "NumericLiteral" || node.type === "BooleanLiteral") return node.value;
1054
- if (node.type === "NullLiteral") return null;
1055
- if (node.type === "TemplateLiteral") return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? "" : void 0;
1056
- if (node.type === "UnaryExpression") {
1057
- const value = readLiteral(node.argument);
1058
- if (typeof value !== "number") return;
1059
- if (node.operator === "-") return -value;
1060
- if (node.operator === "+") return value;
1061
- return;
1062
- }
1063
- if (node.type === "ArrayExpression") {
1064
- const values = node.elements.map((element) => element ? readLiteral(element) : void 0);
1065
- return values.every((value) => value !== void 0) ? values : void 0;
1066
- }
1067
- if (node.type === "ObjectExpression") {
1068
- const entries = {};
1069
- for (const entry of node.properties) {
1070
- if (entry.type !== "ObjectProperty") return;
1071
- const key = propertyKey(entry);
1072
- const value = readLiteral(entry.value);
1073
- if (key === void 0 || value === void 0) return;
1074
- entries[key] = value;
1075
- }
1076
- return entries;
1077
- }
1078
- }
1079
- /**
1080
- * Maps a factory identifier in the file back to the module it was imported from.
1081
- */
1082
- function importedFrom(mod) {
1083
- return new Map(mod.imports.$items.map((item) => [item.local, item.from]));
1084
- }
1085
- /**
1086
- * Every `pluginX(...)` element of a config's plugins array that resolves to an import.
1087
- */
1088
- function pluginCalls(mod, config) {
1089
- const plugins = property(config, "plugins");
1090
- if (plugins?.type !== "ArrayExpression") return [];
1091
- const imports = importedFrom(mod);
1092
- return plugins.elements.flatMap((element) => {
1093
- if (element?.type !== "CallExpression" || element.callee.type !== "Identifier") return [];
1094
- const packageName = imports.get(element.callee.name);
1095
- return packageName ? [{
1096
- importName: element.callee.name,
1097
- packageName,
1098
- call: element
1099
- }] : [];
1100
- });
1101
- }
1102
- /**
1103
- * Plugins a previous `disable-plugin` commented out of this config, keyed by package name.
1104
- *
1105
- * Read from the marker lines rather than the AST, since a commented-out call is no longer a node.
1106
- */
1107
- function disabledMarkers(source) {
1108
- return source.split("\n").flatMap((line, index) => {
1109
- const marker = parseMarker(line);
1110
- return marker ? [{
1111
- packageName: marker.plugin,
1112
- line: index + 1
1113
- }] : [];
1114
- });
1115
- }
1116
- /**
1117
- * Reads which plugins the file declares and which of their options Studio may write.
1118
- *
1119
- * @example
1120
- * ```ts
1121
- * const view = readConfig(await readFile('kubb.config.ts', 'utf8'))
1122
- * if (view.managed) {
1123
- * view.configs.forEach((config) => console.log(config.name, config.plugins.length))
1124
- * }
1125
- * ```
1126
- */
1127
- function readConfig(source) {
1128
- let mod;
1129
- try {
1130
- mod = parseModule(source);
1131
- } catch {
1132
- return {
1133
- managed: false,
1134
- reason: "the config file could not be parsed"
1135
- };
1136
- }
1137
- const found = findConfigs(mod);
1138
- if ("reason" in found) return {
1139
- managed: false,
1140
- reason: found.reason
1141
- };
1142
- const importNames = new Map([...importedFrom(mod)].map(([local, from]) => [from, local]));
1143
- const disabled = disabledMarkers(source);
1144
- return {
1145
- managed: true,
1146
- configs: found.configs.map((config) => {
1147
- const plugins = pluginCalls(mod, config).map(({ importName, packageName, call }) => {
1148
- const entries = {};
1149
- const options = call.arguments[0];
1150
- if (options?.type === "ObjectExpression") for (const entry of options.properties) {
1151
- if (entry.type !== "ObjectProperty") continue;
1152
- const key = propertyKey(entry);
1153
- if (key === void 0) continue;
1154
- const value = readLiteral(entry.value);
1155
- entries[key] = value === void 0 ? { literal: false } : {
1156
- literal: true,
1157
- value
1158
- };
1159
- }
1160
- return {
1161
- importName,
1162
- packageName,
1163
- options: entries
1164
- };
1165
- });
1166
- const start = config.loc?.start.line ?? 0;
1167
- const end = config.loc?.end.line ?? Number.POSITIVE_INFINITY;
1168
- for (const { packageName } of disabled.filter((entry) => entry.line >= start && entry.line <= end)) plugins.push({
1169
- importName: importNames.get(packageName) ?? toExportName(packageName),
1170
- packageName,
1171
- options: {},
1172
- disabled: true
1173
- });
1174
- return {
1175
- name: configName(config),
1176
- plugins
1177
- };
1178
- })
1179
- };
1180
- }
1181
- /**
1182
- * Whether a value can be written into a config file as a literal.
1183
- *
1184
- * This is the trust boundary for edits that arrive over the agent WebSocket: a function, `undefined`,
1185
- * or a non-finite number is refused rather than printed into the user's source.
1186
- */
1187
- function isOptionValue(value) {
1188
- if (value === null) return true;
1189
- if (typeof value === "string" || typeof value === "boolean") return true;
1190
- if (typeof value === "number") return Number.isFinite(value);
1191
- if (Array.isArray(value)) return value.every(isOptionValue);
1192
- if (typeof value === "object") return Object.values(value).every(isOptionValue);
1193
- return false;
1194
- }
1195
- /**
1196
- * The options object of a plugin call, when it was called with one.
1197
- */
1198
- function getOptions(call) {
1199
- const options = call.arguments[0];
1200
- return options?.type === "ObjectExpression" ? options : void 0;
1201
- }
1202
- /**
1203
- * The options object of a plugin call, creating an empty one when the plugin was called bare.
1204
- */
1205
- function ensureOptions(call) {
1206
- if (call.arguments.length === 0) call.arguments.push({
1207
- type: "ObjectExpression",
1208
- properties: []
1209
- });
1210
- return getOptions(call);
1211
- }
1212
- /**
1213
- * Walks `path` down to the object holding its last key, descending only through object literals.
1214
- */
1215
- function optionParent(options, path) {
1216
- let object = options;
1217
- for (const [index, key] of path.entries()) {
1218
- if (index === path.length - 1) return {
1219
- object,
1220
- key
1221
- };
1222
- if (property(object, key) === void 0) setProperty({
1223
- node: object,
1224
- key,
1225
- value: {}
1226
- });
1227
- const next = property(object, key);
1228
- if (next?.type !== "ObjectExpression") return { reason: `${key} is not an object, so ${path.join(".")} cannot be reached` };
1229
- object = next;
1230
- }
1231
- return { reason: "no option path given" };
1232
- }
1233
- /**
1234
- * Writes `value` at `path` inside a plugin call's options, creating the options object and any
1235
- * intermediate object along the path as needed. Refuses when the current value at `path` is
1236
- * something other than a literal, so an option customized in code is never overwritten.
1237
- */
1238
- function applySet(call, path, value) {
1239
- if (!isOptionValue(value)) return "the value is not a literal that can be written to a config file";
1240
- const options = ensureOptions(call);
1241
- if (!options) return "the plugin was not called with an object literal";
1242
- const target = optionParent(options, path);
1243
- if ("reason" in target) return target.reason;
1244
- const current = property(target.object, target.key);
1245
- if (current !== void 0 && readLiteral(current) === void 0) return `${path.join(".")} is customized in code`;
1246
- setProperty({
1247
- node: target.object,
1248
- key: target.key,
1249
- value
1250
- });
1251
- }
1252
- /**
1253
- * Deletes the property at `path` inside a plugin call's options, falling the plugin back to its
1254
- * default for that option. Refuses when the value at `path` is not a literal, for the same reason
1255
- * `applySet` does.
1256
- */
1257
- function applyRemove(call, path) {
1258
- const options = getOptions(call);
1259
- if (!options) return "the plugin has no options to remove";
1260
- const target = optionParent(options, path);
1261
- if ("reason" in target) return target.reason;
1262
- const current = property(target.object, target.key);
1263
- if (current === void 0) return `${path.join(".")} is not set`;
1264
- if (readLiteral(current) === void 0) return `${path.join(".")} is customized in code`;
1265
- removeProperty({
1266
- node: target.object,
1267
- key: target.key
1268
- });
1269
- }
1270
- /**
1271
- * Adds a `pluginX(...)` call to a config's plugins array. Replaying the same add is a no-op, while
1272
- * an import name collision with an unrelated package remains an error.
1273
- */
1274
- function applyAddPlugin(mod, config, edit) {
1275
- if (!isKubbPluginSpecifier(edit.plugin)) return { reason: `"${edit.plugin}" is not a @kubb/plugin-* package` };
1276
- const importName = edit.importName ?? toExportName(edit.plugin);
1277
- if (!IDENTIFIER.test(importName)) return { reason: `"${importName}" is not a valid import name` };
1278
- if (pluginCalls(mod, config).some((plugin) => plugin.packageName === edit.plugin)) return { noop: true };
1279
- const taken = importedFrom(mod).get(importName);
1280
- if (taken && taken !== edit.plugin) return { reason: `${importName} is already imported from ${taken}` };
1281
- const options = edit.options ?? {};
1282
- if (!isOptionValue(options)) return { reason: "the options are not literals that can be written to a config file" };
1283
- const plugins = property(config, "plugins");
1284
- if (plugins?.type !== "ArrayExpression") return { reason: "plugins is not an array literal" };
1285
- const call = Object.keys(options).length ? builders.functionCall(importName, options) : builders.functionCall(importName);
1286
- plugins.elements.push(call.$ast);
1287
- return taken ? {} : { addImport: {
1288
- importName,
1289
- moduleSpecifier: edit.plugin
1290
- } };
1291
- }
1292
- /**
1293
- * Comments out a plugin call in place, keeping its options on disk so `enable-plugin` can restore
1294
- * them exactly. Operates on `source` text rather than the AST: a commented-out call is no longer a
1295
- * node magicast can address, and the surrounding array must not reflow when its element count
1296
- * never actually changes.
1297
- */
1298
- function disablePlugin(source, mod, config, plugin) {
1299
- const target = pluginCalls(mod, config).find((entry) => entry.packageName === plugin);
1300
- if (!target) return { reason: `${plugin} is not in the plugins array` };
1301
- const loc = target.call.loc;
1302
- if (!loc?.start || !loc.end) return { reason: `${plugin} has no source location to comment out` };
1303
- const lines = source.split("\n");
1304
- const from = loc.start.line - 1;
1305
- const to = loc.end.line - 1;
1306
- const firstLine = lines[from] ?? "";
1307
- const lastLine = lines[to] ?? "";
1308
- if (firstLine.slice(0, loc.start.column).trim() !== "" || !/^,?\s*$/.test(lastLine.slice(loc.end.column))) return { reason: `${plugin} shares a line with other code, so it cannot be commented out safely` };
1309
- const indent = firstLine.match(/^\s*/)?.[0] ?? "";
1310
- const commented = lines.slice(from, to + 1).map((line) => line.trim() ? `${indent}// ${line.slice(indent.length)}` : indent ? `${indent}//` : "//");
1311
- lines.splice(from, to - from + 1, formatMarker(plugin, commented.length, indent), ...commented);
1312
- return { source: lines.join("\n") };
1313
- }
1314
- /**
1315
- * Uncomments the block a previous `disable-plugin` left behind for `plugin`.
1316
- */
1317
- function enablePlugin(source, plugin) {
1318
- const lines = source.split("\n");
1319
- for (const [index, line] of lines.entries()) {
1320
- const marker = parseMarker(line);
1321
- if (marker?.plugin !== plugin) continue;
1322
- const end = index + 1 + marker.lineCount;
1323
- const restored = lines.slice(index + 1, end).map((commented) => commented.replace(/^(\s*)\/\/ ?/, "$1"));
1324
- lines.splice(index, end - index, ...restored);
1325
- return { source: lines.join("\n") };
1326
- }
1327
- return { reason: `${plugin} is not disabled` };
1328
- }
1329
- /**
1330
- * Re-parses `source` and resolves the config entry an edit targets. Every edit re-parses rather
1331
- * than sharing one module across the batch, since the disable/enable edits rewrite `source` as
1332
- * text and would otherwise leave the others working from a stale tree.
1333
- */
1334
- function parseTarget(source, ref) {
1335
- let mod;
1336
- try {
1337
- mod = parseModule(source);
1338
- } catch {
1339
- return { reason: "the config file could not be parsed" };
1340
- }
1341
- const found = findConfigs(mod);
1342
- if ("reason" in found) return { reason: found.reason };
1343
- const config = selectConfig(found.configs, ref);
1344
- if (!config) return { reason: `no config entry found for ${JSON.stringify(ref)}` };
1345
- return {
1346
- mod,
1347
- config
1348
- };
1349
- }
1350
- /**
1351
- * Applies edits to a `kubb.config.ts` in place. Every node the edits do not touch keeps its
1352
- * original text, so comments, formatting, and hand-written code around the config survive.
1353
- *
1354
- * Edits are independent: one that cannot be applied is reported in `outcomes` and the rest still run.
1355
- *
1356
- * @example
1357
- * ```ts
1358
- * const { source, outcomes } = applyConfigEdits(current, [
1359
- * { operation: 'set', plugin: '@kubb/plugin-ts', path: ['enum', 'type'], value: 'enum' },
1360
- * ])
1361
- * ```
1362
- *
1363
- * @note recast always reprints a semicolon on a reprinted statement, so editing a block-body
1364
- * `defineConfig` in a semicolon-free file adds one to the `return` line. This is a known gap.
1365
- * Strip it when `detectCodeFormat` reports `useSemi: false`, if it turns out to matter in practice.
1366
- */
1367
- function applyConfigEdits(source, edits) {
1368
- let current = source;
1369
- const format = detectCodeFormat(source);
1370
- const endsWithNewline = source.endsWith("\n");
1371
- const outcomes = edits.map((edit) => {
1372
- const target = parseTarget(current, edit.config);
1373
- if ("reason" in target) return {
1374
- edit,
1375
- applied: false,
1376
- reason: target.reason
1377
- };
1378
- const { mod, config } = target;
1379
- if (edit.operation === "disable-plugin" || edit.operation === "enable-plugin") {
1380
- const result = edit.operation === "disable-plugin" ? disablePlugin(current, mod, config, edit.plugin) : enablePlugin(current, edit.plugin);
1381
- if ("reason" in result) return {
1382
- edit,
1383
- applied: false,
1384
- reason: result.reason
1385
- };
1386
- current = result.source;
1387
- return {
1388
- edit,
1389
- applied: true
1390
- };
1391
- }
1392
- if (edit.operation === "add-plugin") {
1393
- const result = applyAddPlugin(mod, config, edit);
1394
- if ("reason" in result) return {
1395
- edit,
1396
- applied: false,
1397
- reason: result.reason
1398
- };
1399
- if ("noop" in result) return {
1400
- edit,
1401
- applied: true
1402
- };
1403
- const afterLine = lastImportEndLine(mod);
1404
- let next = generateCode(mod, { format }).code;
1405
- if (result.addImport) next = insertImportLine({
1406
- source: next,
1407
- afterLine,
1408
- ...result.addImport
1409
- });
1410
- current = withTrailingNewline(next, endsWithNewline);
1411
- return {
1412
- edit,
1413
- applied: true
1414
- };
1415
- }
1416
- const pluginCall = pluginCalls(mod, config).find((plugin) => plugin.packageName === edit.plugin);
1417
- if (!pluginCall) return {
1418
- edit,
1419
- applied: false,
1420
- reason: `${edit.plugin} is not in the plugins array`
1421
- };
1422
- const reason = edit.operation === "set" ? applySet(pluginCall.call, edit.path, edit.value) : applyRemove(pluginCall.call, edit.path);
1423
- if (!reason) current = withTrailingNewline(generateCode(mod, { format }).code, endsWithNewline);
1424
- return {
1425
- edit,
1426
- applied: !reason,
1427
- reason
1428
- };
1429
- });
1430
- return {
1431
- source: current,
1432
- outcomes,
1433
- changed: current !== source
1434
- };
1435
- }
1436
- async function writeConfigEdits({ filePath, edits }) {
1437
- const result = applyConfigEdits(await read(filePath), edits);
1438
- if (result.changed) await writeFile(filePath, result.source, "utf-8");
1439
- return result;
1440
- }
1441
- /**
1442
- * The 1-based line where the file's last import declaration ends, or `0` when it has none. Read
1443
- * off the parsed module, so a multi-line `import {\n x,\n} from '...'` reports its closing line
1444
- * rather than the `import` keyword.
1445
- */
1446
- function lastImportEndLine(mod) {
1447
- return (mod.$ast.type === "Program" ? mod.$ast.body : []).filter((node) => node.type === "ImportDeclaration").at(-1)?.loc?.end.line ?? 0;
1448
- }
1449
- /**
1450
- * Writes an import after the last one already in the file, matching its quote style and whether it
1451
- * ends in a semicolon. `afterLine` is where that last import ends, `0` for a file with none.
1452
- *
1453
- * Written as plain text rather than through magicast's import builder, which prints a brand-new
1454
- * import declaration with its own default spacing and a semicolon regardless of `format`, since
1455
- * that formatting only governs nodes recast can diff against the original source.
1456
- */
1457
- function insertImportLine({ source, importName, moduleSpecifier, afterLine }) {
1458
- const lines = source.split("\n");
1459
- const lastImportLine = afterLine > 0 ? lines[afterLine - 1] : void 0;
1460
- const quote = lastImportLine?.includes(`"`) ? `"` : `'`;
1461
- const line = `import { ${importName} } from ${quote}${moduleSpecifier}${quote}${lastImportLine?.trimEnd().endsWith(";") ? ";" : ""}`;
1462
- lines.splice(afterLine, 0, ...afterLine > 0 ? [line] : [line, ""]);
1463
- return lines.join("\n");
1464
- }
1465
- /**
1466
- * `generateCode` always drops the file's trailing newline. Restore it when the input had one.
1467
- */
1468
- function withTrailingNewline(code, hadTrailingNewline) {
1469
- if (!hadTrailingNewline || code.endsWith("\n")) return code;
1470
- return `${code}\n`;
1471
- }
1472
- //#endregion
1473
- //#region src/operations/snapshotPackage.ts
1474
- const gzipAsync = promisify(gzip);
1475
- const UPLOAD_TIMEOUT_MS = 12e4;
1476
- /**
1477
- * Maps a generated file's path to its place inside the tarball, stripping everything before a
1478
- * `src`/`dist` segment and any `..`/empty path segment so a crafted file name cannot escape the
1479
- * `package/` root.
1480
- */
1481
- function packagePath(filePath) {
1482
- const normalized = filePath.replaceAll("\\", "/");
1483
- return `package/${(normalized.match(/\/(?:src|dist)\/.*$/)?.[0].slice(1) ?? normalized.replace(/^\/+/, "")).split("/").filter((part) => part && part !== "." && part !== "..").join("/")}`;
1484
- }
1485
- /**
1486
- * Splits a tarball entry path into the legacy 100-byte `name` field and, when the path does not
1487
- * fit, the 155-byte USTAR `prefix` field that extends it. Throws rather than silently truncating
1488
- * a path the format cannot address (max 256 bytes: 100 name + 1 separator + 155 prefix).
1489
- */
1490
- function splitEntryPath(path) {
1491
- if (Buffer.byteLength(path, "utf8") <= 100) return {
1492
- name: path,
1493
- prefix: ""
1494
- };
1495
- for (let i = path.length - 1; i >= 0; i--) {
1496
- if (path[i] !== "/") continue;
1497
- const prefix = path.slice(0, i);
1498
- const name = path.slice(i + 1);
1499
- if (Buffer.byteLength(prefix, "utf8") <= 155 && Buffer.byteLength(name, "utf8") <= 100) return {
1500
- name,
1501
- prefix
1502
- };
1503
- }
1504
- throw new Error(`Snapshot path is too long for a tar entry: ${path}`);
1505
- }
1506
- function header(path, size) {
1507
- const { name, prefix } = splitEntryPath(path);
1508
- const value = Buffer.alloc(512);
1509
- value.write(name, 0, "utf8");
1510
- value.write("0000644\0", 100, "ascii");
1511
- value.write("0000000\0", 108, "ascii");
1512
- value.write("0000000\0", 116, "ascii");
1513
- value.write(`${size.toString(8).padStart(11, "0")}\0`, 124, "ascii");
1514
- value.write(`${Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0")}\0`, 136, "ascii");
1515
- value.fill(32, 148, 156);
1516
- value.write("0", 156, "ascii");
1517
- value.write("ustar\0", 257, "ascii");
1518
- value.write("00", 263, "ascii");
1519
- value.write("0000000\0", 265, "ascii");
1520
- value.write("0000000\0", 297, "ascii");
1521
- value.write(prefix, 345, "utf8");
1522
- const checksum = [...value].reduce((sum, byte) => sum + byte, 0);
1523
- value.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, "ascii");
1524
- return value;
1525
- }
1526
- /**
1527
- * Packs a generation's files into a gzipped, npm-installable tarball: a `package/` root with the
1528
- * generated sources, a `tsdown`-built `dist/` (esm + cjs), and a `package.json` manifest.
1529
- */
1530
- async function createSnapshotPackage(files, packageInfo) {
1531
- const root = await mkdtemp(join(tmpdir(), "kubb-snapshot-"));
1532
- const dist = join(root, "dist");
1533
- const resolvedPaths = Object.entries(files).map(([name, content]) => ({
1534
- name,
1535
- content,
1536
- target: packagePath(name)
1537
- }));
1538
- const targetOwners = /* @__PURE__ */ new Map();
1539
- for (const { name, target } of resolvedPaths) {
1540
- const owner = targetOwners.get(target);
1541
- if (owner) throw new Error(`Snapshot has two generated files that sanitize to the same path "${target}": "${owner}" and "${name}"`);
1542
- targetOwners.set(target, name);
1543
- }
1544
- try {
1545
- await mkdir(dist);
1546
- await Promise.all(resolvedPaths.map(async ({ content, target }) => {
1547
- const path = join(root, target.slice(8));
1548
- await mkdir(join(path, ".."), { recursive: true });
1549
- await writeFile(path, content);
1550
- }));
1551
- const sourceEntries = resolvedPaths.filter(({ name }) => /\.(?:[cm]?[jt]sx?)$/.test(name)).map(({ target }) => join(root, target.slice(8)));
1552
- if (sourceEntries.length) await build({
1553
- entry: sourceEntries,
1554
- outDir: dist,
1555
- format: ["esm", "cjs"],
1556
- dts: false,
1557
- sourcemap: false,
1558
- unbundle: true,
1559
- report: false,
1560
- logLevel: "silent",
1561
- fixedExtension: true
1562
- });
1563
- const builtEntries = await Promise.all((await Array.fromAsync(glob("**/*", {
1564
- cwd: dist,
1565
- withFileTypes: true
1566
- }))).filter((entry) => entry.isFile()).map(async (entry) => {
1567
- const filePath = join(entry.parentPath, entry.name);
1568
- return [`package/dist/${relative(dist, filePath).split(sep).join("/")}`, await readFile(filePath, "utf8")];
1569
- }));
1570
- const builtPaths = new Set(builtEntries.map(([path]) => path));
1571
- const hasBarrel = builtPaths.has("package/dist/index.mjs") && builtPaths.has("package/dist/index.cjs");
1572
- const barrelFields = hasBarrel ? {
1573
- main: "./dist/index.cjs",
1574
- module: "./dist/index.mjs"
1575
- } : {};
1576
- const barrelExport = hasBarrel ? { ".": {
1577
- import: "./dist/index.mjs",
1578
- require: "./dist/index.cjs"
1579
- } } : {};
1580
- const entries = {
1581
- "package/package.json": JSON.stringify({
1582
- ...packageInfo,
1583
- type: "module",
1584
- ...barrelFields,
1585
- exports: {
1586
- ...barrelExport,
1587
- "./*": {
1588
- import: "./dist/*.mjs",
1589
- require: "./dist/*.cjs"
1590
- }
1591
- }
1592
- }, null, 2),
1593
- ...Object.fromEntries(resolvedPaths.map(({ content, target }) => [target, content])),
1594
- ...Object.fromEntries(builtEntries)
1595
- };
1596
- const chunks = [];
1597
- for (const [name, content] of Object.entries(entries)) {
1598
- const bytes = Buffer.from(content);
1599
- chunks.push(header(name, bytes.length), bytes, Buffer.alloc((512 - bytes.length % 512) % 512));
1600
- }
1601
- const bytes = await gzipAsync(Buffer.concat([...chunks, Buffer.alloc(1024)]));
1602
- return {
1603
- bytes,
1604
- integrity: `sha512-${createHash("sha512").update(bytes).digest("base64")}`
1605
- };
1606
- } finally {
1607
- await rm(root, {
1608
- recursive: true,
1609
- force: true
1610
- });
1611
- }
1612
- }
1613
- async function uploadSnapshot({ bytes, uploadPath, studioUrl, token, shutdown }) {
1614
- const uploadUrl = new URL(uploadPath, studioUrl);
1615
- if (uploadUrl.origin !== new URL(studioUrl).origin) throw new Error("Snapshot upload path must stay on the Studio origin");
1616
- const timeout = AbortSignal.timeout(UPLOAD_TIMEOUT_MS);
1617
- const signal = shutdown ? AbortSignal.any([shutdown, timeout]) : timeout;
1618
- const redirect = await fetch(uploadUrl, {
1619
- method: "PUT",
1620
- headers: { Authorization: `Bearer ${token}` },
1621
- redirect: "manual",
1622
- signal
1623
- });
1624
- const storageUrl = redirect.headers.get("location");
1625
- if (redirect.status !== 307 || !storageUrl) throw new Error(`Studio did not provide a storage URL (status ${redirect.status})`);
1626
- const storage = new URL(storageUrl);
1627
- if (storage.protocol !== "https:" && storage.hostname !== "localhost" && storage.hostname !== "127.0.0.1") throw new Error(`Refusing snapshot upload to ${storage.origin}`);
1628
- const response = await fetch(storage, {
1629
- method: "PUT",
1630
- body: new Uint8Array(bytes),
1631
- redirect: "error",
1632
- signal
1633
- });
1634
- if (!response.ok) throw new Error(`Snapshot upload failed with status ${response.status}`);
1635
- }
581
+ var version = "5.5.4";
1636
582
  //#endregion
1637
583
  //#region src/operations/generations.ts
1638
584
  const READ_CONCURRENCY = 50;
@@ -2615,6 +1561,7 @@ var StudioSession = class {
2615
1561
  async #readConfigFileView(source) {
2616
1562
  if (!this.#canEditConfig) return;
2617
1563
  try {
1564
+ const { readConfig } = await import("./configFile-BymXUi9P.js");
2618
1565
  return readConfig(source ?? await read(this.#options.configFile));
2619
1566
  } catch (error) {
2620
1567
  await this.#warn(`Could not read ${this.#options.configFile}: ${getErrorMessage(error)}`);
@@ -2787,6 +1734,7 @@ var StudioSession = class {
2787
1734
  disk: disk ? { hashes: disk.hashes } : void 0
2788
1735
  };
2789
1736
  } finally {
1737
+ this.#lastGeneration = void 0;
2790
1738
  if (root !== this.#options.root) await removeJobRoot(root);
2791
1739
  this.#isGenerating = false;
2792
1740
  }
@@ -2817,6 +1765,7 @@ var StudioSession = class {
2817
1765
  }
2818
1766
  if (this.#isGenerating) return refuse("a generation is in progress");
2819
1767
  try {
1768
+ const { writeConfigEdits } = await import("./configFile-BymXUi9P.js");
2820
1769
  const { source: patched, outcomes, changed } = await writeConfigEdits({
2821
1770
  filePath: configFile,
2822
1771
  edits
@@ -2848,6 +1797,7 @@ var StudioSession = class {
2848
1797
  const missing = generation.missingDependencies.filter((dependency) => !bundled.has(dependency));
2849
1798
  if (missing.length) return this.#refuse(`Ignored snapshot: missing dependencies: ${missing.join(", ")}`, `Missing dependencies: ${missing.join(", ")}`);
2850
1799
  try {
1800
+ const { createSnapshotPackage, uploadSnapshot } = await import("./snapshotPackage-I8bt8KTR.js");
2851
1801
  const { files } = await this.#generations.read({
2852
1802
  generation,
2853
1803
  source: "output",