@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/configFile-BTVeov27.cjs +585 -0
- package/dist/configFile-BTVeov27.cjs.map +1 -0
- package/dist/configFile-BymXUi9P.js +584 -0
- package/dist/configFile-BymXUi9P.js.map +1 -0
- package/dist/index.cjs +17 -1092
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -1059
- package/dist/index.js.map +1 -1
- package/dist/resolveConfig-D7I3Nj60.cjs +360 -0
- package/dist/resolveConfig-D7I3Nj60.cjs.map +1 -0
- package/dist/resolveConfig-DMIG_MH-.js +325 -0
- package/dist/resolveConfig-DMIG_MH-.js.map +1 -0
- package/dist/rolldown-runtime-qbf5tadS.cjs +38 -0
- package/dist/snapshotPackage-BwM3LFmZ.cjs +176 -0
- package/dist/snapshotPackage-BwM3LFmZ.cjs.map +1 -0
- package/dist/snapshotPackage-I8bt8KTR.js +175 -0
- package/dist/snapshotPackage-I8bt8KTR.js.map +1 -0
- package/package.json +4 -5
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 {
|
|
9
|
-
import path, { isAbsolute,
|
|
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 {
|
|
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.
|
|
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",
|