@kubb/studio 5.13.0 → 5.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/index.cjs +1183 -1030
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -41
- package/dist/index.d.ts +3 -41
- package/dist/index.js +1182 -1029
- package/dist/index.js.map +1 -1
- package/dist/{protocol-C4loKcxw.d.ts → protocol-DQTwqmNO.d.cts} +127 -175
- package/dist/{protocol-C4loKcxw.d.cts → protocol-DQTwqmNO.d.ts} +127 -175
- package/dist/protocol-DQnAN8NE.js +118 -0
- package/dist/protocol-DQnAN8NE.js.map +1 -0
- package/dist/protocol-n7fVO3_7.cjs +183 -0
- package/dist/protocol-n7fVO3_7.cjs.map +1 -0
- package/dist/protocol.cjs +11 -121
- package/dist/protocol.d.cts +2 -2
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +1 -112
- package/package.json +7 -6
- package/dist/protocol.cjs.map +0 -1
- package/dist/protocol.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as AgentPermission, c as MAX_SNAPSHOT_BYTES, d as extensionKinds, i as AgentMessage, l as generationEventTypes, n as AGENT_SOCKET_PATH, r as AgentCloseCode, t as AGENT_INSTANCE_HEADER, u as ConfigEditOperation } from "./protocol-DQnAN8NE.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { createHash, hash, randomUUID } from "node:crypto";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
@@ -10,10 +10,10 @@ import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
|
10
10
|
import { x } from "tinyexec";
|
|
11
11
|
import { Diagnostics, Hookable, cacheStorage, checkoutGitInput, createKubb, fsStorage, getInputKind, isPackageName, isPublicAddress, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
|
|
12
12
|
import { RpcTarget, newWebSocketRpcSession } from "capnweb";
|
|
13
|
-
import { detectCodeFormat, parseExpression, parseModule } from "magicast";
|
|
14
13
|
import { pathToFileURL } from "node:url";
|
|
15
|
-
import { structuredPatch } from "diff";
|
|
16
14
|
import { stripVTControlCharacters } from "node:util";
|
|
15
|
+
import { parse } from "yuku-parser";
|
|
16
|
+
import { structuredPatch } from "diff";
|
|
17
17
|
import { ast } from "@kubb/ast";
|
|
18
18
|
import dns from "node:dns";
|
|
19
19
|
import http from "node:http";
|
|
@@ -213,220 +213,112 @@ function getElapsedMs(hrStart) {
|
|
|
213
213
|
}
|
|
214
214
|
//#endregion
|
|
215
215
|
//#region package.json
|
|
216
|
-
var version = "5.13.
|
|
216
|
+
var version = "5.13.1";
|
|
217
217
|
//#endregion
|
|
218
|
-
//#region src/
|
|
218
|
+
//#region ../../internals/shared/src/constants.ts
|
|
219
219
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* A plugin or adapter instance carries closures (`parse`, `getImports`, ...) that cannot survive
|
|
223
|
-
* JSON, so both sides pass options over the wire and the factory is re-invoked here with the merged
|
|
224
|
-
* result. Only packages {@link isAllowedPlugin} accepts are resolved this way, never an arbitrary
|
|
225
|
-
* module the payload names.
|
|
220
|
+
* Extensions `kubb` ships at its own version, so they count as installed without being a direct
|
|
221
|
+
* dependency. `constants.test.ts` checks it against the `kubb` package's dependencies.
|
|
226
222
|
*/
|
|
223
|
+
const BUNDLED_PACKAGES = [
|
|
224
|
+
"@kubb/adapter-oas",
|
|
225
|
+
"@kubb/parser-ts",
|
|
226
|
+
"@kubb/parser-md",
|
|
227
|
+
"@kubb/plugin-barrel"
|
|
228
|
+
];
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region ../../internals/shared/src/configFile/ast.ts
|
|
231
|
+
/** Node types the config editor reads from the yuku AST. */
|
|
232
|
+
const nodeTypes = {
|
|
233
|
+
arrayExpression: "ArrayExpression",
|
|
234
|
+
arrowFunctionExpression: "ArrowFunctionExpression",
|
|
235
|
+
assignmentExpression: "AssignmentExpression",
|
|
236
|
+
blockStatement: "BlockStatement",
|
|
237
|
+
callExpression: "CallExpression",
|
|
238
|
+
exportDefaultDeclaration: "ExportDefaultDeclaration",
|
|
239
|
+
expressionStatement: "ExpressionStatement",
|
|
240
|
+
functionExpression: "FunctionExpression",
|
|
241
|
+
identifier: "Identifier",
|
|
242
|
+
importDeclaration: "ImportDeclaration",
|
|
243
|
+
importSpecifier: "ImportSpecifier",
|
|
244
|
+
literal: "Literal",
|
|
245
|
+
memberExpression: "MemberExpression",
|
|
246
|
+
objectExpression: "ObjectExpression",
|
|
247
|
+
objectPattern: "ObjectPattern",
|
|
248
|
+
property: "Property",
|
|
249
|
+
returnStatement: "ReturnStatement",
|
|
250
|
+
spreadElement: "SpreadElement",
|
|
251
|
+
tSAsExpression: "TSAsExpression",
|
|
252
|
+
tSSatisfiesExpression: "TSSatisfiesExpression",
|
|
253
|
+
templateLiteral: "TemplateLiteral",
|
|
254
|
+
unaryExpression: "UnaryExpression",
|
|
255
|
+
variableDeclaration: "VariableDeclaration"
|
|
256
|
+
};
|
|
257
|
+
const commentTypes = { line: "Line" };
|
|
258
|
+
const propertyKinds = { init: "init" };
|
|
259
|
+
const unaryOperators = {
|
|
260
|
+
minus: "-",
|
|
261
|
+
plus: "+"
|
|
262
|
+
};
|
|
263
|
+
const diagnosticSeverities = { error: "error" };
|
|
227
264
|
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* `import()` resolves from this file, so a linked or globally installed Studio (`pnpm link`,
|
|
231
|
-
* `npm i -g`) only sees its own `node_modules` and misses the plugins installed next to the user's
|
|
232
|
-
* config. The retry resolves from `process.cwd()` instead.
|
|
265
|
+
* Parses `source` as a TypeScript module. Never throws: a syntax error, or a native parser that
|
|
266
|
+
* cannot load on this platform, comes back as a `reason`.
|
|
233
267
|
*/
|
|
234
|
-
|
|
268
|
+
function parseSource(source) {
|
|
235
269
|
try {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
270
|
+
const { program, comments, diagnostics } = parse(source, {
|
|
271
|
+
lang: "ts",
|
|
272
|
+
sourceType: "module",
|
|
273
|
+
preserveParens: false
|
|
274
|
+
});
|
|
275
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === diagnosticSeverities.error)) return { reason: "the config file could not be parsed" };
|
|
276
|
+
return {
|
|
277
|
+
program,
|
|
278
|
+
comments
|
|
279
|
+
};
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return { reason: `the config parser could not load on this platform: ${getErrorMessage(error)}` };
|
|
241
282
|
}
|
|
242
283
|
}
|
|
243
|
-
/**
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
284
|
+
/** Parses `source` as one expression, such as a commented-out `pluginTs({ ... })` call. */
|
|
285
|
+
function parseExpression(source) {
|
|
286
|
+
const parsed = parseSource(source);
|
|
287
|
+
if ("reason" in parsed) return void 0;
|
|
288
|
+
const [statement] = parsed.program.body;
|
|
289
|
+
return parsed.program.body.length === 1 && statement?.type === nodeTypes.expressionStatement ? statement.expression : void 0;
|
|
290
|
+
}
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region ../../internals/shared/src/configFile/rules.ts
|
|
293
|
+
/** `@kubb/plugin-ts` becomes `plugin-ts`, the `name` Kubb plugin factories use. */
|
|
252
294
|
function toPluginName(packageName) {
|
|
253
295
|
return packageName.split("/").pop() ?? packageName;
|
|
254
296
|
}
|
|
255
|
-
/**
|
|
256
|
-
* Adds the `@kubb/` scope a plugin's package carries but its `name` does not, the inverse of
|
|
257
|
-
* {@link toPluginName}. A name outside the `plugin-` convention is left alone, so a third-party
|
|
258
|
-
* plugin is not reported as one of Kubb's.
|
|
259
|
-
*
|
|
260
|
-
* @example
|
|
261
|
-
* ```ts
|
|
262
|
-
* toPackageName('plugin-ts') // '@kubb/plugin-ts'
|
|
263
|
-
* toPackageName('@acme/my-plugin') // '@acme/my-plugin'
|
|
264
|
-
* ```
|
|
265
|
-
*/
|
|
266
|
-
function toPackageName(name) {
|
|
267
|
-
return name.startsWith("plugin-") ? `@kubb/${name}` : name;
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* The name Studio knows an extension by: the package of the installed extension whose manifest
|
|
271
|
-
* `name` matches, otherwise its own `name`, so a plugin defined in the user's project is never
|
|
272
|
-
* reported as a Kubb package that does not exist.
|
|
273
|
-
*
|
|
274
|
-
* @example
|
|
275
|
-
* ```ts
|
|
276
|
-
* toStudioName({ name: 'plugin-ts', extensions }) // '@kubb/plugin-ts'
|
|
277
|
-
* toStudioName({ name: 'plugin-zod-operations', extensions }) // 'plugin-zod-operations', a local plugin
|
|
278
|
-
* ```
|
|
279
|
-
*/
|
|
280
|
-
function toStudioName({ name, extensions = /* @__PURE__ */ new Map(), kind = "plugin" }) {
|
|
281
|
-
return [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name)?.package ?? name;
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Derives the conventional named export for a `@kubb/*` plugin package from its package name.
|
|
285
|
-
*
|
|
286
|
-
* @example
|
|
287
|
-
* ```ts
|
|
288
|
-
* toExportName('@kubb/plugin-react-query') // 'pluginReactQuery'
|
|
289
|
-
* toExportName('@kubb/plugin-ts') // 'pluginTs'
|
|
290
|
-
* ```
|
|
291
|
-
*/
|
|
297
|
+
/** `@kubb/plugin-react-query` becomes `pluginReactQuery`, the export a plugin package uses by convention. */
|
|
292
298
|
function toExportName(packageName) {
|
|
293
299
|
return camelCase(toPluginName(packageName));
|
|
294
300
|
}
|
|
295
301
|
const KUBB_PLUGIN_SPECIFIER = /^@kubb\/plugin-[\w.-]+$/;
|
|
296
|
-
/**
|
|
297
|
-
|
|
298
|
-
* package whose `kubb-extension.json` has `kind: "plugin"`. A path, URL or arbitrary package is
|
|
299
|
-
* refused before it can execute. `configFile.ts` applies the same rule before printing a plugin name.
|
|
300
|
-
*/
|
|
301
|
-
function isAllowedPlugin({ packageName, extensions }) {
|
|
302
|
+
/** Only a `@kubb/plugin-*` package or an installed extension of `kind` may be imported, never a path or URL. */
|
|
303
|
+
function isAllowedPlugin({ packageName, extensions, kind = extensionKinds.plugin }) {
|
|
302
304
|
if (!isPackageName(packageName)) return false;
|
|
303
|
-
return KUBB_PLUGIN_SPECIFIER.test(packageName) || extensions.get(packageName)?.manifest.kind ===
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* Dynamically imports an extension package and returns its factory. Callers check
|
|
307
|
-
* {@link isAllowedPlugin} or derive the package from a disk instance first, so an arbitrary module
|
|
308
|
-
* never reaches `import()`.
|
|
309
|
-
*
|
|
310
|
-
* Packages must be pre-installed in the Docker image at build time via the `KUBB_PACKAGES`
|
|
311
|
-
* build ARG, no runtime installation is possible in the distroless container.
|
|
312
|
-
*
|
|
313
|
-
* Resolution order: the manifest's `export`, else the camelCase named export the package name
|
|
314
|
-
* implies (e.g. `pluginTs`), then the default export.
|
|
315
|
-
*
|
|
316
|
-
* @throws if the package cannot be imported or exports no callable factory.
|
|
317
|
-
*/
|
|
318
|
-
async function loadFactory({ packageName, extensions, kind = "plugin" }) {
|
|
319
|
-
const label = {
|
|
320
|
-
plugin: "Plugin",
|
|
321
|
-
adapter: "Adapter",
|
|
322
|
-
parser: "Parser"
|
|
323
|
-
}[kind];
|
|
324
|
-
let mod;
|
|
325
|
-
try {
|
|
326
|
-
mod = await importFromProject(packageName);
|
|
327
|
-
} catch (cause) {
|
|
328
|
-
throw new Diagnostics.Error({
|
|
329
|
-
code: kind === "plugin" ? Diagnostics.code.pluginNotFound : Diagnostics.code.unknown,
|
|
330
|
-
severity: "error",
|
|
331
|
-
message: `${label} "${packageName}" could not be loaded. Make sure it is installed: \`npm install ${packageName}\``,
|
|
332
|
-
cause: toError(cause)
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
const manifestExport = extensions.get(packageName)?.manifest.export;
|
|
336
|
-
if (manifestExport) {
|
|
337
|
-
if (typeof mod[manifestExport] === "function") return mod[manifestExport];
|
|
338
|
-
throw new Diagnostics.Error({
|
|
339
|
-
code: Diagnostics.code.incompatibleExtension,
|
|
340
|
-
severity: "error",
|
|
341
|
-
message: `${label} "${packageName}" does not export "${manifestExport}", the factory its kubb-extension.json names.`,
|
|
342
|
-
help: "Install a version of the plugin whose exports match its kubb-extension.json."
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
const exportName = toExportName(packageName);
|
|
346
|
-
if (typeof mod[exportName] === "function") return mod[exportName];
|
|
347
|
-
if (typeof mod["default"] === "function") return mod["default"];
|
|
348
|
-
throw new Diagnostics.Error({
|
|
349
|
-
code: Diagnostics.code.incompatibleExtension,
|
|
350
|
-
severity: "error",
|
|
351
|
-
message: `${label} "${packageName}" does not export a callable factory. Tried the named export "${exportName}" and "default".`,
|
|
352
|
-
help: "Install a Kubb plugin package that exports its factory."
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Resolves each plugin entry by dynamically importing its package and calling its factory with the
|
|
357
|
-
* provided options. Only packages {@link isAllowedPlugin} accepts are loaded.
|
|
358
|
-
*
|
|
359
|
-
* @example
|
|
360
|
-
* ```ts
|
|
361
|
-
* const plugins = await resolvePlugins({
|
|
362
|
-
* plugins: [
|
|
363
|
-
* { name: '@kubb/plugin-react-query', options: { output: { path: './hooks' } } },
|
|
364
|
-
* { name: '@kubb/plugin-ts', options: { output: { path: './types' } } },
|
|
365
|
-
* ],
|
|
366
|
-
* })
|
|
367
|
-
* ```
|
|
368
|
-
*/
|
|
369
|
-
async function resolvePlugins({ plugins, extensions = /* @__PURE__ */ new Map() }) {
|
|
370
|
-
return Promise.all(plugins.map(async ({ name, options }) => {
|
|
371
|
-
if (!isAllowedPlugin({
|
|
372
|
-
packageName: name,
|
|
373
|
-
extensions
|
|
374
|
-
})) throw new Diagnostics.Error({
|
|
375
|
-
code: Diagnostics.code.pluginNotFound,
|
|
376
|
-
severity: "error",
|
|
377
|
-
message: `Plugin "${name}" is not an installed Kubb plugin. Studio loads @kubb/plugin-* packages and installed packages that ship a kubb-extension.json with kind "plugin".`
|
|
378
|
-
});
|
|
379
|
-
return (await loadFactory({
|
|
380
|
-
packageName: name,
|
|
381
|
-
extensions
|
|
382
|
-
}))(options ?? {});
|
|
383
|
-
}));
|
|
305
|
+
return kind === extensionKinds.plugin && KUBB_PLUGIN_SPECIFIER.test(packageName) || extensions.get(packageName)?.manifest.kind === kind;
|
|
384
306
|
}
|
|
385
|
-
/**
|
|
386
|
-
* Keys that reach an object's prototype when assigned. Never read from or written into options.
|
|
387
|
-
*/
|
|
388
307
|
const UNSAFE_KEYS = /* @__PURE__ */ new Set([
|
|
389
308
|
"__proto__",
|
|
390
309
|
"constructor",
|
|
391
310
|
"prototype"
|
|
392
311
|
]);
|
|
393
|
-
/**
|
|
394
|
-
* Whether `key` can be used as an option key without touching an object's prototype.
|
|
395
|
-
*/
|
|
312
|
+
/** Whether `key` can be an option key without reaching the object's prototype. */
|
|
396
313
|
function isSafeKey(key) {
|
|
397
314
|
return !UNSAFE_KEYS.has(key);
|
|
398
315
|
}
|
|
399
|
-
/**
|
|
400
|
-
* A JSON copy of plugin or adapter options: functions and other values JSON cannot hold are left
|
|
401
|
-
* out, so nothing callable crosses the socket. Options that cannot be copied at all, such as a
|
|
402
|
-
* circular structure, come back empty.
|
|
403
|
-
*
|
|
404
|
-
* @example
|
|
405
|
-
* ```ts
|
|
406
|
-
* toJSONOptions({ output: { path: 'types' }, group: { name: () => 'x' } }) // { output: { path: 'types' }, group: {} }
|
|
407
|
-
* ```
|
|
408
|
-
*/
|
|
409
|
-
function toJSONOptions(options) {
|
|
410
|
-
try {
|
|
411
|
-
const copy = JSON.parse(JSON.stringify(options ?? {}, (key, value) => isSafeKey(key) && typeof value !== "bigint" ? value : void 0));
|
|
412
|
-
return isPlainObject(copy) ? copy : {};
|
|
413
|
-
} catch {
|
|
414
|
-
return {};
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Whether `value` is an object literal or a null-prototype object, not an array or class instance.
|
|
419
|
-
*/
|
|
420
316
|
function isPlainObject(value) {
|
|
421
317
|
if (typeof value !== "object" || value === null) return false;
|
|
422
318
|
const prototype = Object.getPrototypeOf(value);
|
|
423
319
|
return prototype === null || prototype === Object.prototype;
|
|
424
320
|
}
|
|
425
|
-
/**
|
|
426
|
-
* Whether `value` survives a JSON round trip, so it can be printed into a config file as a literal.
|
|
427
|
-
* A function, `undefined`, a non-finite number or a class instance comes back from Studio as a
|
|
428
|
-
* lossy copy, so an edit carrying one is refused rather than written into the user's source.
|
|
429
|
-
*/
|
|
321
|
+
/** Whether `value` survives a JSON round trip, so an edit never writes a lossy copy into the file. */
|
|
430
322
|
function isOptionValue(value) {
|
|
431
323
|
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
432
324
|
if (typeof value === "number") return Number.isFinite(value);
|
|
@@ -434,375 +326,250 @@ function isOptionValue(value) {
|
|
|
434
326
|
if (isPlainObject(value)) return Object.entries(value).every(([key, entry]) => isSafeKey(key) && isOptionValue(entry));
|
|
435
327
|
return false;
|
|
436
328
|
}
|
|
329
|
+
/** The root `output` members Studio may set, with the values each takes. */
|
|
330
|
+
const OUTPUT_VALUES = {
|
|
331
|
+
barrel: (value) => value === false || isPlainObject(value) && Object.keys(value).length === 1 && (value.type === "named" || value.type === "all"),
|
|
332
|
+
format: (value) => value === false || [
|
|
333
|
+
"auto",
|
|
334
|
+
"prettier",
|
|
335
|
+
"biome",
|
|
336
|
+
"oxfmt"
|
|
337
|
+
].includes(value),
|
|
338
|
+
lint: (value) => value === false || [
|
|
339
|
+
"auto",
|
|
340
|
+
"eslint",
|
|
341
|
+
"biome",
|
|
342
|
+
"oxlint"
|
|
343
|
+
].includes(value),
|
|
344
|
+
clean: (value) => typeof value === "boolean",
|
|
345
|
+
defaultBanner: (value) => value === false || value === "simple" || value === "full"
|
|
346
|
+
};
|
|
347
|
+
function isOutputKey(key) {
|
|
348
|
+
return Object.hasOwn(OUTPUT_VALUES, key);
|
|
349
|
+
}
|
|
350
|
+
function isOutputValue({ key, value }) {
|
|
351
|
+
return isOutputKey(key) && OUTPUT_VALUES[key](value);
|
|
352
|
+
}
|
|
353
|
+
function pickOutput(output) {
|
|
354
|
+
return Object.fromEntries(Object.entries(output).filter(([key]) => isOutputKey(key)));
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region ../../internals/shared/src/configFile/text.ts
|
|
437
358
|
/**
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
* value is kept rather than replaced by what Studio echoes back. Plain objects merge key by key, so
|
|
441
|
-
* Studio can still change a literal that sits next to a function (e.g. `group.type` beside `group.name`).
|
|
442
|
-
*
|
|
443
|
-
* @example
|
|
444
|
-
* ```ts
|
|
445
|
-
* mergeOptions({ macros: [camelCaseMacro], syntaxType: 'type' }, { macros: [{ name: 'camelCase' }], syntaxType: 'interface' })
|
|
446
|
-
* // { macros: [camelCaseMacro], syntaxType: 'interface' }
|
|
447
|
-
* mergeOptions({ emptySchemaType: 'any' }, { emptySchemaType: null }) // {}
|
|
448
|
-
* ```
|
|
359
|
+
* A valid JavaScript identifier, so an import name can only ever print as `import { name } from`,
|
|
360
|
+
* never as source that breaks out of the import statement.
|
|
449
361
|
*/
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
362
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/;
|
|
363
|
+
const quotes = {
|
|
364
|
+
single: `'`,
|
|
365
|
+
double: `"`
|
|
366
|
+
};
|
|
367
|
+
/** `source` with the text between `start` and `end` replaced by `text`. */
|
|
368
|
+
function replaceText(source, start, end, text = "") {
|
|
369
|
+
return `${source.slice(0, start)}${text}${source.slice(end)}`;
|
|
370
|
+
}
|
|
371
|
+
/** The 1-based line and 0-based column of `offset` in `source`. */
|
|
372
|
+
function getPosition(source, offset) {
|
|
373
|
+
return {
|
|
374
|
+
line: source.slice(0, offset).split("\n").length,
|
|
375
|
+
column: offset - source.lastIndexOf("\n", offset - 1) - 1
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
/** Writes `value` as a single-line literal with the file's quote style. */
|
|
379
|
+
function printValue(value, quote) {
|
|
380
|
+
if (typeof value === "string") {
|
|
381
|
+
const escaped = JSON.stringify(value).slice(1, -1);
|
|
382
|
+
return quote === quotes.double ? `"${escaped}"` : `'${escaped.replaceAll(`\\"`, `"`).replaceAll(`'`, `\\'`)}'`;
|
|
465
383
|
}
|
|
466
|
-
return
|
|
384
|
+
if (value === null || typeof value !== "object") return String(value);
|
|
385
|
+
if (Array.isArray(value)) return `[${value.map((item) => printValue(item, quote)).join(", ")}]`;
|
|
386
|
+
const entries = Object.entries(value);
|
|
387
|
+
return entries.length ? `{ ${entries.map(([key, item]) => `${printKey(key, quote)}: ${printValue(item, quote)}`).join(", ")} }` : "{}";
|
|
467
388
|
}
|
|
468
|
-
/**
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
* can express, see {@link mergeOptions}), and a plugin Studio explicitly
|
|
472
|
-
* disabled is dropped even when the disk config still lists it. Disk plugins without a studio
|
|
473
|
-
* counterpart are kept as-is. Studio plugins not present on disk are appended.
|
|
474
|
-
*
|
|
475
|
-
* For plugins present in both configs, the plugin is re-instantiated with merged options
|
|
476
|
-
* so that all internal closures correctly reference the merged values.
|
|
477
|
-
*/
|
|
478
|
-
async function mergePlugins({ diskPlugins, studioPlugins, extensions = /* @__PURE__ */ new Map() }) {
|
|
479
|
-
const pluginNameOf = (packageName) => extensions.get(packageName)?.manifest.name ?? toPluginName(packageName);
|
|
480
|
-
const disabledNames = new Set((studioPlugins ?? []).filter((entry) => entry.disabled).map((entry) => pluginNameOf(entry.name)));
|
|
481
|
-
const activeDiskPlugins = disabledNames.size ? diskPlugins?.filter((plugin) => !disabledNames.has(resolveMeta(plugin).name)) : diskPlugins;
|
|
482
|
-
const activeStudioPlugins = studioPlugins?.filter((entry) => !entry.disabled);
|
|
483
|
-
if (!activeDiskPlugins && !activeStudioPlugins?.length) return void 0;
|
|
484
|
-
if (!activeStudioPlugins?.length) return activeDiskPlugins;
|
|
485
|
-
if (!activeDiskPlugins) return resolvePlugins({
|
|
486
|
-
plugins: activeStudioPlugins,
|
|
487
|
-
extensions
|
|
488
|
-
});
|
|
489
|
-
const studioEntryByName = new Map(activeStudioPlugins.map((entry) => [pluginNameOf(entry.name), entry]));
|
|
490
|
-
const diskNames = new Set(activeDiskPlugins.map((plugin) => plugin.name));
|
|
491
|
-
const merged = await Promise.all(activeDiskPlugins.map(async (diskPlugin) => {
|
|
492
|
-
const studioEntry = studioEntryByName.get(resolveMeta(diskPlugin).name);
|
|
493
|
-
if (!studioEntry) return diskPlugin;
|
|
494
|
-
const diskOptions = diskPlugin.options ?? {};
|
|
495
|
-
const options = mergeOptions(diskOptions, studioEntry.options ?? {});
|
|
496
|
-
const unchanged = JSON.stringify(toJSONOptions(options)) === JSON.stringify(toJSONOptions(diskOptions));
|
|
497
|
-
const [resolved] = await resolvePlugins({
|
|
498
|
-
plugins: [{
|
|
499
|
-
name: studioEntry.name,
|
|
500
|
-
options
|
|
501
|
-
}],
|
|
502
|
-
extensions
|
|
503
|
-
}).catch((cause) => {
|
|
504
|
-
if (unchanged) return [diskPlugin];
|
|
505
|
-
throw new Diagnostics.Error({
|
|
506
|
-
code: Diagnostics.code.pluginNotFound,
|
|
507
|
-
severity: "error",
|
|
508
|
-
message: `Plugin "${diskPlugin.name}" is not an installed @kubb/plugin-* package or Kubb extension, so Studio cannot change its options. Edit it in the config file, or only enable or disable it from Studio.`,
|
|
509
|
-
cause: toError(cause)
|
|
510
|
-
});
|
|
511
|
-
});
|
|
512
|
-
return resolved ?? diskPlugin;
|
|
513
|
-
}));
|
|
514
|
-
const studioOnly = activeStudioPlugins.filter((entry) => !diskNames.has(pluginNameOf(entry.name)));
|
|
515
|
-
return [...merged, ...await resolvePlugins({
|
|
516
|
-
plugins: studioOnly,
|
|
517
|
-
extensions
|
|
518
|
-
})];
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* Throws `KUBB_PATH_TRAVERSAL` when a plugin would write outside the config's output folder to a path
|
|
522
|
-
* the disk config does not set. Studio can change `output.path`, also inside `override`, and add
|
|
523
|
-
* plugins, so without this check any member of the Studio organization could write any file the agent
|
|
524
|
-
* can reach, including `node_modules` code the next run imports. A path from the config file stays
|
|
525
|
-
* allowed, since a monorepo may write outside its root.
|
|
526
|
-
*/
|
|
527
|
-
function assertOutputPaths({ plugins, diskPlugins, root, outputPath }) {
|
|
528
|
-
const base = path.resolve(root, outputPath);
|
|
529
|
-
const targetsOf = (plugin) => {
|
|
530
|
-
const { output, override } = plugin.options ?? {};
|
|
531
|
-
return [output, ...Array.isArray(override) ? override.map((entry) => entry?.options?.output) : []].flatMap((entry) => typeof entry?.path === "string" ? [path.resolve(base, entry.path)] : []);
|
|
532
|
-
};
|
|
533
|
-
const diskTargets = new Set(diskPlugins.flatMap(targetsOf));
|
|
534
|
-
for (const plugin of plugins) {
|
|
535
|
-
const target = targetsOf(plugin).find((resolved) => !diskTargets.has(resolved) && !isPathInside(resolved, base));
|
|
536
|
-
if (!target) continue;
|
|
537
|
-
throw new Diagnostics.Error({
|
|
538
|
-
code: Diagnostics.code.pathTraversal,
|
|
539
|
-
severity: "error",
|
|
540
|
-
message: `Plugin "${plugin.name}" would write to "${target}", outside the output folder "${base}". Studio can only set an output path inside it.`,
|
|
541
|
-
help: "Choose an output path inside the output folder, or set this path in the config file.",
|
|
542
|
-
location: { kind: "config" },
|
|
543
|
-
plugin: plugin.name
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
/**
|
|
548
|
-
* Re-invokes the factory behind a disk adapter or parser with Studio's options merged over its own.
|
|
549
|
-
* An instance carries live functions (`parse`, `print`, ...) that cannot survive JSON, so Studio's
|
|
550
|
-
* options are a patch, not a replacement, and the factory is found from the disk instance itself: the
|
|
551
|
-
* installed extension whose manifest names it, or the `@kubb/<kind>-<name>` package for a built-in
|
|
552
|
-
* one. Throws when no factory resolves, as `mergePlugins` does, so an override Studio sent is never
|
|
553
|
-
* dropped without a word. An extension without a manifest, such as `parserTsx`, has no options in
|
|
554
|
-
* Studio, so Studio never sends one for it.
|
|
555
|
-
*/
|
|
556
|
-
async function rebuildExtension({ disk, studioOptions, kind, extensions }) {
|
|
557
|
-
if (!studioOptions || !Object.keys(studioOptions).length) return disk;
|
|
558
|
-
const { name } = resolveMeta(disk);
|
|
559
|
-
return (await loadFactory({
|
|
560
|
-
packageName: [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name)?.package ?? `@kubb/${kind}-${name}`,
|
|
561
|
-
extensions,
|
|
562
|
-
kind
|
|
563
|
-
}).catch((cause) => {
|
|
564
|
-
throw new Diagnostics.Error({
|
|
565
|
-
code: kind === "plugin" ? Diagnostics.code.pluginNotFound : Diagnostics.code.unknown,
|
|
566
|
-
severity: "error",
|
|
567
|
-
message: `The ${kind} "${name}" is not an installed Kubb extension, so Studio cannot change its options. Edit it in the config file.`,
|
|
568
|
-
cause: toError(cause)
|
|
569
|
-
});
|
|
570
|
-
}))(mergeOptions(disk.options ?? {}, studioOptions));
|
|
571
|
-
}
|
|
572
|
-
/** The root `output` members Studio may set, with the values each takes. The rest, `path` and `postGenerate` among them, stay with the config file. */
|
|
573
|
-
const OUTPUT_VALUES = {
|
|
574
|
-
barrel: (value) => value === false || isPlainObject(value) && Object.keys(value).length === 1 && (value.type === "named" || value.type === "all"),
|
|
575
|
-
format: (value) => value === false || [
|
|
576
|
-
"auto",
|
|
577
|
-
"prettier",
|
|
578
|
-
"biome",
|
|
579
|
-
"oxfmt"
|
|
580
|
-
].includes(value),
|
|
581
|
-
lint: (value) => value === false || [
|
|
582
|
-
"auto",
|
|
583
|
-
"eslint",
|
|
584
|
-
"biome",
|
|
585
|
-
"oxlint"
|
|
586
|
-
].includes(value),
|
|
587
|
-
clean: (value) => typeof value === "boolean",
|
|
588
|
-
defaultBanner: (value) => value === false || value === "simple" || value === "full"
|
|
589
|
-
};
|
|
590
|
-
/** Whether Studio may set the root `output` member `key`. */
|
|
591
|
-
function isOutputKey(key) {
|
|
592
|
-
return Object.hasOwn(OUTPUT_VALUES, key);
|
|
593
|
-
}
|
|
594
|
-
/** Whether Studio may set the root `output` member `key` to `value`. */
|
|
595
|
-
function isOutputValue({ key, value }) {
|
|
596
|
-
return isOutputKey(key) && OUTPUT_VALUES[key](value);
|
|
597
|
-
}
|
|
598
|
-
/** The members of `output` Studio may see and set. */
|
|
599
|
-
function pickOutput(output) {
|
|
600
|
-
return Object.fromEntries(Object.entries(output).filter(([key]) => isOutputKey(key)));
|
|
389
|
+
/** An object key as source text: bare when it is an identifier, quoted otherwise. */
|
|
390
|
+
function printKey(key, quote) {
|
|
391
|
+
return IDENTIFIER_PATTERN.test(key) ? key : printValue(key, quote);
|
|
601
392
|
}
|
|
602
|
-
/**
|
|
603
|
-
function
|
|
604
|
-
|
|
605
|
-
return mergeOptions(output, Object.fromEntries(Object.entries(studio).filter(([key, value]) => value === null ? isOutputKey(key) : isOutputValue({
|
|
606
|
-
key,
|
|
607
|
-
value
|
|
608
|
-
}))));
|
|
393
|
+
/** `value` wrapped in one object per key of `path`: `wrapPath(['enum', 'type'], 'enum')` is `{ enum: { type: 'enum' } }`. */
|
|
394
|
+
function wrapPath(path, value) {
|
|
395
|
+
return path.reduceRight((inner, key) => ({ [key]: inner }), value);
|
|
609
396
|
}
|
|
610
397
|
/**
|
|
611
|
-
*
|
|
398
|
+
* Adds `text` as the last item of an object or array literal, following its layout: on a line of
|
|
399
|
+
* its own with the last item's indentation when the literal spans lines, inline otherwise.
|
|
612
400
|
*/
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
}
|
|
401
|
+
function appendItem(source, container, text) {
|
|
402
|
+
const { start, end } = container;
|
|
403
|
+
const last = (container.type === nodeTypes.objectExpression ? container.properties : container.elements).filter((item) => item !== null).at(-1);
|
|
404
|
+
if (!last) {
|
|
405
|
+
const inner = source.slice(start + 1, end - 1);
|
|
406
|
+
if (inner.trim()) return replaceText(source, start + 1, start + 1, /^\s/.test(inner) ? `${text},` : `${text}, `);
|
|
407
|
+
return replaceText(source, start, end, container.type === nodeTypes.objectExpression ? `{ ${text} }` : `[${text}]`);
|
|
408
|
+
}
|
|
409
|
+
const between = source.slice(last.end, end - 1);
|
|
410
|
+
if (!between.includes("\n")) return replaceText(source, last.end, last.end, `, ${text}`);
|
|
411
|
+
const lineStart = source.lastIndexOf("\n", last.start - 1) + 1;
|
|
412
|
+
const indent = source.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
|
|
413
|
+
const lineEnd = source.indexOf("\n", last.end);
|
|
414
|
+
const comma = /^\s*,/.test(between);
|
|
415
|
+
return `${source.slice(0, last.end)}${comma ? "" : ","}${source.slice(last.end, lineEnd)}\n${indent}${text}${comma ? "," : ""}${source.slice(lineEnd)}`;
|
|
621
416
|
}
|
|
622
|
-
/**
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
extensions,
|
|
634
|
-
kind: "parser"
|
|
635
|
-
})),
|
|
636
|
-
kind: "parser",
|
|
637
|
-
extensions
|
|
638
|
-
})));
|
|
417
|
+
/** Drops the item at `index` from an object or array literal, with its own lines when it sits on them alone. */
|
|
418
|
+
function removeItem(source, node, index) {
|
|
419
|
+
const items = node.type === nodeTypes.objectExpression ? node.properties : node.elements.filter((element) => element !== null);
|
|
420
|
+
if (items.length === 1) return replaceText(source, node.start, node.end, node.type === nodeTypes.objectExpression ? "{}" : "[]");
|
|
421
|
+
const { start, end } = items[index];
|
|
422
|
+
const lineStart = source.lastIndexOf("\n", start - 1) + 1;
|
|
423
|
+
const nextNewline = source.indexOf("\n", end);
|
|
424
|
+
const lineEnd = nextNewline === -1 ? source.length : nextNewline;
|
|
425
|
+
if (!source.slice(lineStart, start).trim() && /^\s*,?\s*(\/\/.*)?$/.test(source.slice(end, lineEnd))) return replaceText(source, lineStart, Math.min(lineEnd + 1, source.length));
|
|
426
|
+
const next = items[index + 1];
|
|
427
|
+
return next ? replaceText(source, start, next.start) : replaceText(source, items[index - 1].end, end);
|
|
639
428
|
}
|
|
640
429
|
//#endregion
|
|
641
|
-
//#region src/configFile.ts
|
|
642
|
-
/**
|
|
643
|
-
|
|
644
|
-
* never as source that breaks out of the import statement.
|
|
645
|
-
*/
|
|
646
|
-
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
647
|
-
/**
|
|
648
|
-
* The marker line a `disable-plugin` writes above the block it comments out. It carries the block's
|
|
649
|
-
* line count, so `enable-plugin` restores exactly those lines and not whatever comments follow.
|
|
650
|
-
*/
|
|
651
|
-
const DISABLED_MARKER = /^\/\/ kubb:disabled (.+)\s+(\d+)$/;
|
|
430
|
+
//#region ../../internals/shared/src/configFile/configFile.ts
|
|
431
|
+
/** The marker `disable-plugin` writes, with the block's line count so `enable-plugin` restores exactly those lines. */
|
|
432
|
+
const DISABLED_MARKER_PATTERN = /^kubb:disabled (.+)\s+(\d+)$/;
|
|
652
433
|
const OPERATIONS = new Set(Object.values(ConfigEditOperation));
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
434
|
+
const OPTION_OPERATIONS = /* @__PURE__ */ new Set([ConfigEditOperation.SET_OPTION, ConfigEditOperation.REMOVE_OPTION]);
|
|
435
|
+
const OUTPUT_OPERATIONS = /* @__PURE__ */ new Set([ConfigEditOperation.SET_OUTPUT, ConfigEditOperation.REMOVE_OUTPUT]);
|
|
436
|
+
const KINDS = new Set(Object.values(extensionKinds));
|
|
437
|
+
/** Identifiers the editor matches by name. */
|
|
438
|
+
const identifiers = {
|
|
439
|
+
defineConfig: "defineConfig",
|
|
440
|
+
require: "require",
|
|
441
|
+
module: "module",
|
|
442
|
+
exports: "exports"
|
|
443
|
+
};
|
|
444
|
+
const moduleKinds = {
|
|
445
|
+
esm: "esm",
|
|
446
|
+
cjs: "cjs"
|
|
447
|
+
};
|
|
448
|
+
function isNonEmptyString(value) {
|
|
449
|
+
return typeof value === "string" && value !== "";
|
|
450
|
+
}
|
|
451
|
+
/** Edits cross the wire, so their shape is checked before anything reads them. */
|
|
452
|
+
function validateEdit(edit) {
|
|
663
453
|
const { operation, plugin, config, path, kind, input } = typeof edit === "object" && edit !== null ? edit : {};
|
|
664
454
|
if (typeof operation !== "string" || !OPERATIONS.has(operation)) return `unknown operation ${JSON.stringify(operation)}`;
|
|
665
455
|
if (config !== void 0 && typeof config !== "string" && !Number.isSafeInteger(config)) return "config must name an entry or give its index";
|
|
666
|
-
if (operation === ConfigEditOperation.SET_INPUT) return typeof input === "string" && input.trim() ? void 0 : "the edit names no input";
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
if (
|
|
670
|
-
if (kind !== void 0
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
});
|
|
686
|
-
return {
|
|
687
|
-
start: node.start,
|
|
688
|
-
end: node.end
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
/** `source` with the text between `start` and `end` replaced by `text`. */
|
|
692
|
-
function splice(source, start, end, text = "") {
|
|
693
|
-
return `${source.slice(0, start)}${text}${source.slice(end)}`;
|
|
694
|
-
}
|
|
695
|
-
function bodyOf(mod) {
|
|
696
|
-
return mod.$ast.type === "Program" ? mod.$ast.body : [];
|
|
697
|
-
}
|
|
698
|
-
/**
|
|
699
|
-
* Steps through a config's wrappers to the object literal underneath: a `satisfies`/`as`
|
|
700
|
-
* assertion, a `() => ...` factory, or a factory whose block body returns the config.
|
|
701
|
-
*/
|
|
702
|
-
function unwrap(node) {
|
|
456
|
+
if (operation === ConfigEditOperation.SET_INPUT) return typeof input === "string" && input.trim() !== "" ? void 0 : "the edit names no input";
|
|
457
|
+
const isOption = OPTION_OPERATIONS.has(operation);
|
|
458
|
+
const isOutput = OUTPUT_OPERATIONS.has(operation);
|
|
459
|
+
if (!isOutput && !isNonEmptyString(plugin)) return "the edit names no plugin";
|
|
460
|
+
if (kind !== void 0) {
|
|
461
|
+
if (typeof kind !== "string" || !KINDS.has(kind)) return `unknown kind ${JSON.stringify(kind)}`;
|
|
462
|
+
const takesKind = operation === ConfigEditOperation.ADD_PLUGIN || operation === ConfigEditOperation.REMOVE_EXTENSION;
|
|
463
|
+
if (takesKind && kind === extensionKinds.adapter) return `${operation} takes a plugin or a parser, since a config has one adapter`;
|
|
464
|
+
if (!isOption && !takesKind) return `${operation} edits the plugins array, so it takes no kind`;
|
|
465
|
+
}
|
|
466
|
+
if (!isOption && !isOutput) return void 0;
|
|
467
|
+
if (!Array.isArray(path) || !path.every(isNonEmptyString)) return "path must be a list of option names";
|
|
468
|
+
if (path.length === 0) return "no option path given";
|
|
469
|
+
const unsafe = path.find((key) => !isSafeKey(key));
|
|
470
|
+
if (unsafe !== void 0) return `${unsafe} cannot be used as an option name`;
|
|
471
|
+
if (isOutput && !isOutputKey(path[0])) return `output.${path[0]} can only be changed in the config file`;
|
|
472
|
+
}
|
|
473
|
+
/** Steps through `as`/`satisfies` and `() => ...` wrappers to the config object. */
|
|
474
|
+
function unwrapValue(node) {
|
|
703
475
|
if (!node) return void 0;
|
|
704
|
-
if (node.type ===
|
|
705
|
-
if (node.type !==
|
|
706
|
-
if (node.body
|
|
707
|
-
|
|
476
|
+
if (node.type === nodeTypes.tSAsExpression || node.type === nodeTypes.tSSatisfiesExpression) return unwrapValue(node.expression);
|
|
477
|
+
if (node.type !== nodeTypes.arrowFunctionExpression && node.type !== nodeTypes.functionExpression) return node;
|
|
478
|
+
if (node.body?.type !== nodeTypes.blockStatement) return unwrapValue(node.body);
|
|
479
|
+
const returned = node.body.body.find((statement) => statement.type === nodeTypes.returnStatement);
|
|
480
|
+
return unwrapValue(returned?.type === nodeTypes.returnStatement ? returned.argument : void 0);
|
|
708
481
|
}
|
|
709
482
|
function isModuleExports(node) {
|
|
710
|
-
return node.type ===
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
mod = parseModule(source);
|
|
724
|
-
} catch {
|
|
725
|
-
return { reason: "the config file could not be parsed" };
|
|
726
|
-
}
|
|
727
|
-
let exported;
|
|
728
|
-
let kind = "esm";
|
|
729
|
-
for (const node of bodyOf(mod)) {
|
|
730
|
-
if (node.type === "ExportDefaultDeclaration") {
|
|
731
|
-
exported = node.declaration;
|
|
732
|
-
break;
|
|
733
|
-
}
|
|
734
|
-
if (node.type === "ExpressionStatement" && node.expression.type === "AssignmentExpression" && isModuleExports(node.expression.left)) {
|
|
735
|
-
exported = node.expression.right;
|
|
736
|
-
kind = "cjs";
|
|
737
|
-
break;
|
|
738
|
-
}
|
|
483
|
+
return node.type === nodeTypes.memberExpression && !node.computed && node.object.type === nodeTypes.identifier && node.object.name === identifiers.module && node.property.type === nodeTypes.identifier && node.property.name === identifiers.exports;
|
|
484
|
+
}
|
|
485
|
+
/** The exported config value and how the file exports it. */
|
|
486
|
+
function findExport(program) {
|
|
487
|
+
for (const node of program.body) {
|
|
488
|
+
if (node.type === nodeTypes.exportDefaultDeclaration) return {
|
|
489
|
+
exported: node.declaration,
|
|
490
|
+
kind: moduleKinds.esm
|
|
491
|
+
};
|
|
492
|
+
if (node.type === nodeTypes.expressionStatement && node.expression.type === nodeTypes.assignmentExpression && isModuleExports(node.expression.left)) return {
|
|
493
|
+
exported: node.expression.right,
|
|
494
|
+
kind: moduleKinds.cjs
|
|
495
|
+
};
|
|
739
496
|
}
|
|
740
|
-
|
|
497
|
+
return {
|
|
498
|
+
exported: void 0,
|
|
499
|
+
kind: moduleKinds.esm
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/** Every config object in `export default` or `module.exports = defineConfig(...)`, or why the file is unmanaged. */
|
|
503
|
+
function findConfigs(source) {
|
|
504
|
+
const parsed = parseSource(source);
|
|
505
|
+
if ("reason" in parsed) return parsed;
|
|
506
|
+
const { exported, kind } = findExport(parsed.program);
|
|
507
|
+
const value = unwrapValue(exported);
|
|
741
508
|
if (!value) return { reason: "no `export default` or `module.exports` found" };
|
|
742
|
-
if (value.type !==
|
|
743
|
-
const argument =
|
|
509
|
+
if (value.type !== nodeTypes.callExpression || value.callee.type !== nodeTypes.identifier || value.callee.name !== identifiers.defineConfig) return { reason: "the exported config is not a defineConfig(...) call" };
|
|
510
|
+
const argument = unwrapValue(value.arguments[0]);
|
|
744
511
|
if (!argument) return { reason: "defineConfig(...) was called without a config" };
|
|
745
|
-
const entries = argument.type ===
|
|
746
|
-
const configs = entries.filter((entry) => entry?.type ===
|
|
512
|
+
const entries = argument.type === nodeTypes.arrayExpression ? argument.elements.map((element) => unwrapValue(element)) : [argument];
|
|
513
|
+
const configs = entries.filter((entry) => entry?.type === nodeTypes.objectExpression);
|
|
747
514
|
if (configs.length !== entries.length) return { reason: "config is not an object literal" };
|
|
748
515
|
return {
|
|
749
|
-
|
|
516
|
+
...parsed,
|
|
750
517
|
configs,
|
|
751
518
|
kind
|
|
752
519
|
};
|
|
753
520
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if (entry.type === "SpreadElement" || entry.computed) return void 0;
|
|
757
|
-
return entry.key.type === "Identifier" ? entry.key.name : entry.key.type === "StringLiteral" ? entry.key.value : void 0;
|
|
521
|
+
function findConfigEntry(configs, ref) {
|
|
522
|
+
return typeof ref === "string" ? configs.find((entry) => getConfigName(entry) === ref) : configs[ref ?? 0];
|
|
758
523
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
524
|
+
function getKey(entry) {
|
|
525
|
+
if (entry.type !== nodeTypes.property || entry.computed) return void 0;
|
|
526
|
+
if (entry.key.type === nodeTypes.identifier) return entry.key.name;
|
|
527
|
+
return entry.key.type === nodeTypes.literal && typeof entry.key.value === "string" ? entry.key.value : void 0;
|
|
528
|
+
}
|
|
529
|
+
/** The last one wins when the key repeats, as at runtime. */
|
|
530
|
+
function findProperty(node, key) {
|
|
531
|
+
const index = node.properties.findLastIndex((entry) => getKey(entry) === key);
|
|
762
532
|
return index === -1 ? void 0 : {
|
|
763
533
|
member: node.properties[index],
|
|
764
534
|
index
|
|
765
535
|
};
|
|
766
536
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
return member?.type === "ObjectProperty" ? member.value : member;
|
|
537
|
+
function getValue(node, key) {
|
|
538
|
+
const member = findProperty(node, key)?.member;
|
|
539
|
+
return member?.type === nodeTypes.property ? member.value : member;
|
|
771
540
|
}
|
|
772
|
-
function
|
|
773
|
-
const name =
|
|
774
|
-
return name?.type === "
|
|
541
|
+
function getConfigName(config) {
|
|
542
|
+
const name = getValue(config, "name");
|
|
543
|
+
return name?.type === nodeTypes.literal && typeof name.value === "string" ? name.value : void 0;
|
|
775
544
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
return node.properties.slice(index + 1).some((entry) => entry.type === "SpreadElement");
|
|
545
|
+
function isOverridden(node, index) {
|
|
546
|
+
return node.properties.slice(index + 1).some((entry) => entry.type === nodeTypes.spreadElement);
|
|
779
547
|
}
|
|
780
|
-
/**
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
*/
|
|
548
|
+
/** `key: value`, not a method, getter, setter, or `{ key }` shorthand. */
|
|
549
|
+
function isPlainProperty(member) {
|
|
550
|
+
return !member.method && !member.shorthand && member.kind === propertyKinds.init;
|
|
551
|
+
}
|
|
552
|
+
/** A literal's value, `undefined` for code. */
|
|
785
553
|
function readLiteral(node) {
|
|
786
554
|
switch (node?.type) {
|
|
787
|
-
case "
|
|
788
|
-
case "
|
|
789
|
-
case
|
|
790
|
-
case "NullLiteral": return null;
|
|
791
|
-
case "TemplateLiteral": return node.expressions.length ? void 0 : node.quasis[0]?.value.cooked ?? "";
|
|
792
|
-
case "UnaryExpression": {
|
|
555
|
+
case nodeTypes.literal: return "regex" in node || "bigint" in node ? void 0 : node.value;
|
|
556
|
+
case nodeTypes.templateLiteral: return node.expressions.length ? void 0 : node.quasis[0]?.value.cooked ?? "";
|
|
557
|
+
case nodeTypes.unaryExpression: {
|
|
793
558
|
const value = readLiteral(node.argument);
|
|
794
559
|
if (typeof value !== "number") return void 0;
|
|
795
|
-
|
|
560
|
+
if (node.operator === unaryOperators.minus) return -value;
|
|
561
|
+
if (node.operator === unaryOperators.plus) return value;
|
|
562
|
+
return;
|
|
796
563
|
}
|
|
797
|
-
case
|
|
564
|
+
case nodeTypes.arrayExpression: {
|
|
798
565
|
const values = node.elements.map((element) => readLiteral(element));
|
|
799
566
|
return values.every((value) => value !== void 0) ? values : void 0;
|
|
800
567
|
}
|
|
801
|
-
case
|
|
568
|
+
case nodeTypes.objectExpression: {
|
|
802
569
|
const entries = {};
|
|
803
570
|
for (const entry of node.properties) {
|
|
804
|
-
if (entry.type !==
|
|
805
|
-
const key =
|
|
571
|
+
if (entry.type !== nodeTypes.property) return void 0;
|
|
572
|
+
const key = getKey(entry);
|
|
806
573
|
const value = readLiteral(entry.value);
|
|
807
574
|
if (key === void 0 || !isSafeKey(key) || value === void 0) return void 0;
|
|
808
575
|
entries[key] = value;
|
|
@@ -811,158 +578,123 @@ function readLiteral(node) {
|
|
|
811
578
|
}
|
|
812
579
|
}
|
|
813
580
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
581
|
+
function getRequirePath(node) {
|
|
582
|
+
if (node?.type !== nodeTypes.callExpression || node.callee.type !== nodeTypes.identifier || node.callee.name !== identifiers.require) return void 0;
|
|
583
|
+
const [argument] = node.arguments;
|
|
584
|
+
return argument?.type === nodeTypes.literal && typeof argument.value === "string" ? argument.value : void 0;
|
|
585
|
+
}
|
|
586
|
+
/** `import { exported as local } from '...'`. A default or namespace import has no exported name, so its local name stands in. */
|
|
587
|
+
function importBindings(statement) {
|
|
588
|
+
return statement.specifiers.map((specifier) => {
|
|
589
|
+
const imported = specifier.type === nodeTypes.importSpecifier ? specifier.imported : specifier.local;
|
|
590
|
+
const exported = imported.type === nodeTypes.identifier ? imported.name : imported.value;
|
|
591
|
+
return [specifier.local.name, {
|
|
592
|
+
from: statement.source.value,
|
|
593
|
+
exported
|
|
594
|
+
}];
|
|
595
|
+
});
|
|
824
596
|
}
|
|
825
|
-
/**
|
|
826
|
-
function
|
|
827
|
-
return
|
|
597
|
+
/** `const { exported: local } = require('...')`. */
|
|
598
|
+
function requireBindings(statement) {
|
|
599
|
+
return statement.declarations.flatMap((declarator) => {
|
|
600
|
+
const from = getRequirePath(declarator.init);
|
|
601
|
+
if (from === void 0 || declarator.id.type !== nodeTypes.objectPattern) return [];
|
|
602
|
+
return declarator.id.properties.flatMap((entry) => {
|
|
603
|
+
if (entry.type !== nodeTypes.property || entry.value.type !== nodeTypes.identifier) return [];
|
|
604
|
+
return [[entry.value.name, {
|
|
605
|
+
from,
|
|
606
|
+
exported: getKey(entry) ?? entry.value.name
|
|
607
|
+
}]];
|
|
608
|
+
});
|
|
609
|
+
});
|
|
828
610
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
611
|
+
function getImports(program) {
|
|
612
|
+
return new Map(program.body.flatMap((statement) => {
|
|
613
|
+
if (statement.type === nodeTypes.importDeclaration) return importBindings(statement);
|
|
614
|
+
if (statement.type === nodeTypes.variableDeclaration) return requireBindings(statement);
|
|
615
|
+
return [];
|
|
616
|
+
}));
|
|
832
617
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
618
|
+
function isImportStatement(node, kind) {
|
|
619
|
+
if (kind === moduleKinds.esm) return node.type === nodeTypes.importDeclaration;
|
|
620
|
+
return node.type === nodeTypes.variableDeclaration && node.declarations.some((declarator) => getRequirePath(declarator.init) !== void 0);
|
|
621
|
+
}
|
|
622
|
+
/** The quote of the first string literal in the file, so new code matches it. */
|
|
623
|
+
function detectQuote(node) {
|
|
624
|
+
if (!node || typeof node !== "object") return void 0;
|
|
625
|
+
const { type, raw } = node;
|
|
626
|
+
if (type === nodeTypes.literal && typeof raw === "string" && (raw[0] === quotes.single || raw[0] === quotes.double)) return raw[0];
|
|
627
|
+
for (const child of Object.values(node)) {
|
|
628
|
+
const quote = detectQuote(child);
|
|
629
|
+
if (quote) return quote;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/** The `adapter` value, or the elements of the `plugins` or `parsers` array. */
|
|
633
|
+
function factoryNodes(config, kind) {
|
|
634
|
+
if (kind === extensionKinds.adapter) return [getValue(config, "adapter")];
|
|
635
|
+
const array = getValue(config, `${kind}s`);
|
|
636
|
+
return array?.type === nodeTypes.arrayExpression ? array.elements : [];
|
|
637
|
+
}
|
|
638
|
+
/** The factory calls of one kind whose callee the file imports. */
|
|
639
|
+
function findCalls(program, config, kind = extensionKinds.plugin) {
|
|
640
|
+
const imports = getImports(program);
|
|
641
|
+
return factoryNodes(config, kind).flatMap((node) => {
|
|
642
|
+
if (node?.type !== nodeTypes.callExpression || node.callee.type !== nodeTypes.identifier) return [];
|
|
643
|
+
const imported = imports.get(node.callee.name);
|
|
644
|
+
return imported ? [{
|
|
645
|
+
importName: node.callee.name,
|
|
646
|
+
exportName: imported.exported,
|
|
647
|
+
packageName: imported.from,
|
|
648
|
+
call: node
|
|
649
|
+
}] : [];
|
|
650
|
+
});
|
|
853
651
|
}
|
|
854
|
-
/**
|
|
855
|
-
function
|
|
856
|
-
|
|
857
|
-
const { start, end } = rangeOf(node.properties[index]);
|
|
858
|
-
const lineStart = source.lastIndexOf("\n", start - 1) + 1;
|
|
859
|
-
const nextNewline = source.indexOf("\n", end);
|
|
860
|
-
const lineEnd = nextNewline === -1 ? source.length : nextNewline;
|
|
861
|
-
if (!source.slice(lineStart, start).trim() && /^\s*,?\s*(\/\/.*)?$/.test(source.slice(end, lineEnd))) return splice(source, lineStart, Math.min(lineEnd + 1, source.length));
|
|
862
|
-
const next = node.properties[index + 1];
|
|
863
|
-
return next ? splice(source, start, rangeOf(next).start) : splice(source, rangeOf(node.properties[index - 1]).end, end);
|
|
652
|
+
/** Whether nothing but whitespace precedes `offset` on its line. */
|
|
653
|
+
function startsLine(source, offset) {
|
|
654
|
+
return source.slice(source.lastIndexOf("\n", offset - 1) + 1, offset).trim() === "";
|
|
864
655
|
}
|
|
865
|
-
/** The
|
|
866
|
-
function
|
|
867
|
-
if (node?.type !== "CallExpression" || node.callee.type !== "Identifier" || node.callee.name !== "require") return void 0;
|
|
868
|
-
const [argument] = node.arguments;
|
|
869
|
-
return argument?.type === "StringLiteral" ? argument.value : void 0;
|
|
870
|
-
}
|
|
871
|
-
/** Maps each identifier the file imports, through `import { x } from` or `const { x } = require(...)`, to its module. */
|
|
872
|
-
function importedFrom(mod) {
|
|
873
|
-
const imports = /* @__PURE__ */ new Map();
|
|
874
|
-
for (const statement of bodyOf(mod)) {
|
|
875
|
-
if (statement.type === "ImportDeclaration") {
|
|
876
|
-
for (const specifier of statement.specifiers) {
|
|
877
|
-
const imported = specifier.type === "ImportSpecifier" ? specifier.imported : specifier.local;
|
|
878
|
-
imports.set(specifier.local.name, {
|
|
879
|
-
from: statement.source.value,
|
|
880
|
-
exported: imported.type === "Identifier" ? imported.name : imported.value
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
continue;
|
|
884
|
-
}
|
|
885
|
-
if (statement.type !== "VariableDeclaration") continue;
|
|
886
|
-
for (const declarator of statement.declarations) {
|
|
887
|
-
const from = requiredModule(declarator.init);
|
|
888
|
-
if (from === void 0 || declarator.id.type !== "ObjectPattern") continue;
|
|
889
|
-
for (const entry of declarator.id.properties) if (entry.type === "ObjectProperty" && entry.value.type === "Identifier") imports.set(entry.value.name, {
|
|
890
|
-
from,
|
|
891
|
-
exported: memberKey(entry) ?? entry.value.name
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
return imports;
|
|
896
|
-
}
|
|
897
|
-
/**
|
|
898
|
-
* Every factory call of one kind that resolves to an import: the `pluginX(...)` elements of the
|
|
899
|
-
* `plugins` array, the `adapter` value, or the elements of the `parsers` array.
|
|
900
|
-
*/
|
|
901
|
-
function factoryCalls(mod, config, kind = "plugin") {
|
|
902
|
-
const node = property(config, kind === "adapter" ? "adapter" : `${kind}s`);
|
|
903
|
-
const elements = kind === "adapter" ? [node] : node?.type === "ArrayExpression" ? node.elements : [];
|
|
904
|
-
const imports = importedFrom(mod);
|
|
905
|
-
return elements.flatMap((element) => {
|
|
906
|
-
if (element?.type !== "CallExpression" || element.callee.type !== "Identifier") return [];
|
|
907
|
-
const imported = imports.get(element.callee.name);
|
|
908
|
-
return imported ? [{
|
|
909
|
-
importName: element.callee.name,
|
|
910
|
-
exportName: imported.exported,
|
|
911
|
-
packageName: imported.from,
|
|
912
|
-
call: element
|
|
913
|
-
}] : [];
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
/**
|
|
917
|
-
* The blocks a previous `disable-plugin` commented out of this config, read from the marker lines
|
|
918
|
-
* since a commented-out call is no longer a node. `code` holds the block's lines as they were.
|
|
919
|
-
*/
|
|
920
|
-
function disabledBlocks(source, config) {
|
|
656
|
+
/** The `disable-plugin` blocks in this config, read from marker comments since the call is no longer a node. */
|
|
657
|
+
function findDisabled(source, comments, config) {
|
|
921
658
|
const lines = source.split("\n");
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
const
|
|
928
|
-
const code = lines.slice(index + 1, end).map((line) => line.replace(/^(\s*)\/\/ ?/, "$1").replace(/^\s+$/, ""));
|
|
659
|
+
return comments.flatMap((comment) => {
|
|
660
|
+
const match = comment.type === commentTypes.line ? comment.value.trim().match(DISABLED_MARKER_PATTERN) : null;
|
|
661
|
+
if (!match || comment.start < config.start || comment.end > config.end || !startsLine(source, comment.start)) return [];
|
|
662
|
+
const line = getPosition(source, comment.start).line;
|
|
663
|
+
const end = line + Number(match[2]);
|
|
664
|
+
const code = lines.slice(line, end).map((text) => text.replace(/^(\s*)\/\/ ?/, "$1").replace(/^\s+$/, ""));
|
|
929
665
|
return [{
|
|
930
666
|
packageName: match[1],
|
|
931
|
-
line
|
|
667
|
+
line,
|
|
932
668
|
end,
|
|
933
669
|
code
|
|
934
670
|
}];
|
|
935
671
|
});
|
|
936
672
|
}
|
|
937
|
-
/**
|
|
938
|
-
|
|
939
|
-
* the members that are code as `locked`. `set` can write any path outside `locked`, so Studio
|
|
940
|
-
* edits those fields. `undefined` for a computed key, since it could set any path.
|
|
941
|
-
*/
|
|
942
|
-
function readPartial(node, prefix = []) {
|
|
673
|
+
/** Literal members as `value` and code paths as `locked`, `undefined` for a computed key. */
|
|
674
|
+
function readEditable(node, prefix = []) {
|
|
943
675
|
const value = {};
|
|
944
676
|
const locked = [];
|
|
945
677
|
for (const [index, entry] of node.properties.entries()) {
|
|
946
|
-
if (entry.type ===
|
|
947
|
-
const key =
|
|
678
|
+
if (entry.type === nodeTypes.spreadElement) continue;
|
|
679
|
+
const key = getKey(entry);
|
|
948
680
|
if (key === void 0) return void 0;
|
|
949
|
-
if (
|
|
681
|
+
if (findProperty(node, key)?.index !== index) continue;
|
|
950
682
|
const path = [...prefix, key];
|
|
951
683
|
if (!isSafeKey(key)) {
|
|
952
684
|
locked.push(path);
|
|
953
685
|
continue;
|
|
954
686
|
}
|
|
955
|
-
const member =
|
|
687
|
+
const member = isOverridden(node, index) ? void 0 : entry.value;
|
|
956
688
|
const literal = readLiteral(member);
|
|
957
689
|
if (literal !== void 0) {
|
|
958
690
|
value[key] = literal;
|
|
959
691
|
continue;
|
|
960
692
|
}
|
|
961
|
-
if (member?.type !==
|
|
693
|
+
if (member?.type !== nodeTypes.objectExpression) {
|
|
962
694
|
locked.push(path);
|
|
963
695
|
continue;
|
|
964
696
|
}
|
|
965
|
-
const nested =
|
|
697
|
+
const nested = readEditable(member, path);
|
|
966
698
|
if (!nested) return void 0;
|
|
967
699
|
value[key] = nested.value;
|
|
968
700
|
locked.push(...nested.locked);
|
|
@@ -972,142 +704,144 @@ function readPartial(node, prefix = []) {
|
|
|
972
704
|
locked
|
|
973
705
|
};
|
|
974
706
|
}
|
|
975
|
-
|
|
976
|
-
function readOptions(call) {
|
|
977
|
-
const options = call.arguments[0];
|
|
978
|
-
return options?.type === "ObjectExpression" ? readObject(options) : {};
|
|
979
|
-
}
|
|
980
|
-
/** The members of an object literal, each flagged with whether Studio may write it. */
|
|
981
|
-
function readObject(options) {
|
|
707
|
+
function readObjectOptions(options) {
|
|
982
708
|
const entries = {};
|
|
983
709
|
for (const [index, entry] of options.properties.entries()) {
|
|
984
|
-
const key =
|
|
710
|
+
const key = getKey(entry);
|
|
985
711
|
if (key === void 0 || !isSafeKey(key)) continue;
|
|
986
|
-
const member = entry.type ===
|
|
712
|
+
const member = entry.type === nodeTypes.property && !isOverridden(options, index) ? entry.value : void 0;
|
|
987
713
|
const value = readLiteral(member);
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
714
|
+
if (value !== void 0) {
|
|
715
|
+
entries[key] = {
|
|
716
|
+
literal: true,
|
|
717
|
+
value
|
|
718
|
+
};
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
entries[key] = {
|
|
993
722
|
literal: false,
|
|
994
|
-
...
|
|
723
|
+
...member?.type === nodeTypes.objectExpression ? readEditable(member) : void 0
|
|
995
724
|
};
|
|
996
725
|
}
|
|
997
726
|
return entries;
|
|
998
727
|
}
|
|
999
|
-
|
|
728
|
+
function readCallOptions(call) {
|
|
729
|
+
const options = call.arguments[0];
|
|
730
|
+
return options?.type === nodeTypes.objectExpression ? readObjectOptions(options) : {};
|
|
731
|
+
}
|
|
1000
732
|
function readDisabledOptions(code) {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
return call.type === "CallExpression" ? readOptions(call) : {};
|
|
1004
|
-
} catch {
|
|
1005
|
-
return {};
|
|
1006
|
-
}
|
|
733
|
+
const call = parseExpression(code.join("\n").trim().replace(/,$/, ""));
|
|
734
|
+
return call?.type === nodeTypes.callExpression ? readCallOptions(call) : {};
|
|
1007
735
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
736
|
+
function toPluginView({ importName, packageName, call }) {
|
|
737
|
+
return {
|
|
738
|
+
importName,
|
|
739
|
+
packageName,
|
|
740
|
+
options: readCallOptions(call)
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
/** Active and disabled plugins, in source order so a disabled plugin keeps its place. */
|
|
744
|
+
function readPlugins({ source, parsed, config }) {
|
|
745
|
+
const importNames = new Map([...getImports(parsed.program)].map(([local, { from }]) => [from, local]));
|
|
746
|
+
const active = findCalls(parsed.program, config).map((call) => ({
|
|
747
|
+
line: getPosition(source, call.call.start).line,
|
|
748
|
+
plugin: toPluginView(call)
|
|
749
|
+
}));
|
|
750
|
+
const disabled = findDisabled(source, parsed.comments, config).map(({ packageName, line, code }) => ({
|
|
751
|
+
line,
|
|
752
|
+
plugin: {
|
|
753
|
+
importName: importNames.get(packageName) ?? toExportName(packageName),
|
|
754
|
+
packageName,
|
|
755
|
+
options: readDisabledOptions(code),
|
|
756
|
+
disabled: true
|
|
757
|
+
}
|
|
758
|
+
}));
|
|
759
|
+
return [...active, ...disabled].sort((a, b) => a.line - b.line).map(({ plugin }) => plugin);
|
|
760
|
+
}
|
|
761
|
+
function readConfigView({ source, parsed, config }) {
|
|
762
|
+
const [adapter] = findCalls(parsed.program, config, extensionKinds.adapter).map(toPluginView);
|
|
763
|
+
const parsers = findCalls(parsed.program, config, extensionKinds.parser).map(toPluginView);
|
|
764
|
+
const output = getOutput(config);
|
|
765
|
+
return {
|
|
766
|
+
name: getConfigName(config),
|
|
767
|
+
plugins: readPlugins({
|
|
768
|
+
source,
|
|
769
|
+
parsed,
|
|
770
|
+
config
|
|
771
|
+
}),
|
|
772
|
+
...adapter && { adapter },
|
|
773
|
+
...parsers.length ? { parsers } : {},
|
|
774
|
+
...!("reason" in output) && { output: pickOutput(readObjectOptions(output.object)) }
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
/** Which plugins the file declares and which options Studio may write. */
|
|
1019
778
|
function readConfig(source) {
|
|
1020
|
-
const parsed =
|
|
779
|
+
const parsed = findConfigs(source);
|
|
1021
780
|
if ("reason" in parsed) return {
|
|
1022
781
|
managed: false,
|
|
1023
782
|
reason: parsed.reason
|
|
1024
783
|
};
|
|
1025
|
-
const { mod, configs } = parsed;
|
|
1026
|
-
const importNames = new Map([...importedFrom(mod)].map(([local, { from }]) => [from, local]));
|
|
1027
784
|
return {
|
|
1028
785
|
managed: true,
|
|
1029
|
-
configs: configs.map((config) => {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
packageName,
|
|
1035
|
-
options: readOptions(call)
|
|
1036
|
-
}
|
|
1037
|
-
}));
|
|
1038
|
-
const disabled = disabledBlocks(source, config).map(({ packageName, line, code }) => ({
|
|
1039
|
-
line,
|
|
1040
|
-
plugin: {
|
|
1041
|
-
importName: importNames.get(packageName) ?? toExportName(packageName),
|
|
1042
|
-
packageName,
|
|
1043
|
-
options: readDisabledOptions(code),
|
|
1044
|
-
disabled: true
|
|
1045
|
-
}
|
|
1046
|
-
}));
|
|
1047
|
-
const plugins = [...active, ...disabled].sort((a, b) => a.line - b.line).map(({ plugin }) => plugin);
|
|
1048
|
-
const toView = ({ importName, packageName, call }) => ({
|
|
1049
|
-
importName,
|
|
1050
|
-
packageName,
|
|
1051
|
-
options: readOptions(call)
|
|
1052
|
-
});
|
|
1053
|
-
const [adapter] = factoryCalls(mod, config, "adapter").map(toView);
|
|
1054
|
-
const parsers = factoryCalls(mod, config, "parser").map(toView);
|
|
1055
|
-
const output = outputObject(config);
|
|
1056
|
-
return {
|
|
1057
|
-
name: configName(config),
|
|
1058
|
-
plugins,
|
|
1059
|
-
...adapter && { adapter },
|
|
1060
|
-
...parsers.length ? { parsers } : {},
|
|
1061
|
-
...!("reason" in output) && { output: pickOutput(readObject(output.object)) }
|
|
1062
|
-
};
|
|
1063
|
-
})
|
|
786
|
+
configs: parsed.configs.map((config) => readConfigView({
|
|
787
|
+
source,
|
|
788
|
+
parsed,
|
|
789
|
+
config
|
|
790
|
+
}))
|
|
1064
791
|
};
|
|
1065
792
|
}
|
|
1066
|
-
/** The modules the
|
|
793
|
+
/** The modules the file imports, empty when it cannot be parsed. */
|
|
1067
794
|
function readImports(source) {
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
}
|
|
1071
|
-
return [];
|
|
1072
|
-
}
|
|
795
|
+
const parsed = parseSource(source);
|
|
796
|
+
if ("reason" in parsed) return [];
|
|
797
|
+
return [...new Set([...getImports(parsed.program).values()].map(({ from }) => from))];
|
|
1073
798
|
}
|
|
1074
|
-
/**
|
|
1075
|
-
|
|
1076
|
-
* the index of the first key that is not there yet, so `set` can write the rest of the path as one
|
|
1077
|
-
* nested literal and `remove` can report it as unset. Refuses a value customized in code, so it is
|
|
1078
|
-
* never overwritten.
|
|
1079
|
-
*/
|
|
1080
|
-
function walk(options, path) {
|
|
799
|
+
/** Follows `path` to its literal, or to the first missing key. Refuses a value set in code. */
|
|
800
|
+
function findPath(options, path) {
|
|
1081
801
|
let object = options;
|
|
1082
802
|
for (const [depth, key] of path.entries()) {
|
|
1083
|
-
const found =
|
|
803
|
+
const found = findProperty(object, key);
|
|
1084
804
|
if (!found) return {
|
|
1085
805
|
object,
|
|
1086
806
|
missing: depth
|
|
1087
807
|
};
|
|
1088
|
-
if (
|
|
808
|
+
if (isOverridden(object, found.index)) return { reason: `${path.slice(0, depth + 1).join(".")} can be overridden by a spread after it, so it is customized in code` };
|
|
1089
809
|
const { member, index } = found;
|
|
1090
|
-
if (depth === path.length - 1) return member.type ===
|
|
810
|
+
if (depth === path.length - 1) return member.type === nodeTypes.property && readLiteral(member.value) !== void 0 ? {
|
|
1091
811
|
object,
|
|
1092
812
|
index,
|
|
1093
813
|
value: member.value
|
|
1094
814
|
} : { reason: `${path.join(".")} is customized in code` };
|
|
1095
|
-
if (member.type !==
|
|
815
|
+
if (member.type !== nodeTypes.property || member.value.type !== nodeTypes.objectExpression) return { reason: `${key} is not an object, so ${path.join(".")} cannot be reached` };
|
|
1096
816
|
object = member.value;
|
|
1097
817
|
}
|
|
1098
818
|
return { reason: "no option path given" };
|
|
1099
819
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
820
|
+
function setAtPath({ source, options, path, value, quote }) {
|
|
821
|
+
const target = findPath(options, path);
|
|
822
|
+
if ("reason" in target) return target;
|
|
823
|
+
if ("missing" in target) {
|
|
824
|
+
const [key, ...rest] = path.slice(target.missing);
|
|
825
|
+
return { source: appendItem(source, target.object, `${printKey(key, quote)}: ${printValue(wrapPath(rest, value), quote)}`) };
|
|
826
|
+
}
|
|
827
|
+
return { source: replaceText(source, target.value.start, target.value.end, printValue(value, quote)) };
|
|
828
|
+
}
|
|
829
|
+
function removeAtPath({ source, options, path }) {
|
|
830
|
+
const target = findPath(options, path);
|
|
831
|
+
if ("reason" in target) return target;
|
|
832
|
+
if ("missing" in target) return { source };
|
|
833
|
+
return { source: removeItem(source, target.object, target.index) };
|
|
834
|
+
}
|
|
835
|
+
function setOption(source, call, path, value, quote) {
|
|
1102
836
|
if (!isOptionValue(value)) return { reason: "the value is not a literal that can be written to a config file" };
|
|
1103
837
|
const options = call.arguments[0];
|
|
1104
838
|
if (!options) {
|
|
1105
|
-
const closing =
|
|
839
|
+
const closing = call.end - 1;
|
|
1106
840
|
if (source[closing] !== ")") return { reason: "the factory call could not be located in the file" };
|
|
1107
|
-
return { source:
|
|
841
|
+
return { source: replaceText(source, closing, closing, printValue(wrapPath(path, value), quote)) };
|
|
1108
842
|
}
|
|
1109
|
-
if (options.type !==
|
|
1110
|
-
return
|
|
843
|
+
if (options.type !== nodeTypes.objectExpression) return { reason: "the factory was not called with an object literal" };
|
|
844
|
+
return setAtPath({
|
|
1111
845
|
source,
|
|
1112
846
|
options,
|
|
1113
847
|
path,
|
|
@@ -1115,46 +849,56 @@ function applySet(source, call, path, value, quote) {
|
|
|
1115
849
|
quote
|
|
1116
850
|
});
|
|
1117
851
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
if (
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
852
|
+
function removeOption(source, call, path) {
|
|
853
|
+
const options = call.arguments[0];
|
|
854
|
+
if (!options) return { source };
|
|
855
|
+
if (options.type !== nodeTypes.objectExpression) return { reason: "the factory was not called with an object literal" };
|
|
856
|
+
return removeAtPath({
|
|
857
|
+
source,
|
|
858
|
+
options,
|
|
859
|
+
path
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
function editOption({ source, parsed, config, edit, quote, extensions }) {
|
|
863
|
+
const kind = edit.kind ?? extensionKinds.plugin;
|
|
864
|
+
const calls = findCalls(parsed.program, config, kind).filter((entry) => entry.packageName === edit.plugin);
|
|
865
|
+
const exportName = extensions.get(edit.plugin)?.manifest.export ?? toExportName(edit.plugin);
|
|
866
|
+
const call = (calls.find((entry) => entry.exportName === exportName) ?? calls[0])?.call;
|
|
867
|
+
if (!call) {
|
|
868
|
+
if (kind === extensionKinds.adapter) return { reason: `${edit.plugin} is not the config's adapter` };
|
|
869
|
+
return { reason: kind === extensionKinds.plugin && findDisabled(source, parsed.comments, config).some((block) => block.packageName === edit.plugin) ? `${edit.plugin} is disabled, enable it first` : `${edit.plugin} is not in the ${kind}s array` };
|
|
1125
870
|
}
|
|
1126
|
-
|
|
871
|
+
if (edit.operation === ConfigEditOperation.SET_OPTION) return setOption(source, call, edit.path, edit.value, quote);
|
|
872
|
+
return removeOption(source, call, edit.path);
|
|
1127
873
|
}
|
|
1128
|
-
|
|
1129
|
-
function applySetInput(source, config, input, quote) {
|
|
874
|
+
function setInput(source, config, input, quote) {
|
|
1130
875
|
const value = printValue(input, quote);
|
|
1131
|
-
const found =
|
|
1132
|
-
if (!found) return { source:
|
|
1133
|
-
if (found.member.type !==
|
|
1134
|
-
if (
|
|
1135
|
-
return { source:
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
if (
|
|
1141
|
-
if (isShadowed(config, found.index)) return { reason: "a spread after output could override it" };
|
|
876
|
+
const found = findProperty(config, "input");
|
|
877
|
+
if (!found) return { source: appendItem(source, config, `input: ${value}`) };
|
|
878
|
+
if (found.member.type !== nodeTypes.property || !isPlainProperty(found.member)) return { reason: "input is not a plain property" };
|
|
879
|
+
if (isOverridden(config, found.index)) return { reason: "a spread after input could override it" };
|
|
880
|
+
return { source: replaceText(source, found.member.value.start, found.member.value.end, value) };
|
|
881
|
+
}
|
|
882
|
+
function getOutput(config) {
|
|
883
|
+
const found = findProperty(config, "output");
|
|
884
|
+
if (found?.member.type !== nodeTypes.property || found.member.value.type !== nodeTypes.objectExpression) return { reason: "output is not an object literal" };
|
|
885
|
+
if (isOverridden(config, found.index)) return { reason: "a spread after output could override it" };
|
|
1142
886
|
return { object: found.member.value };
|
|
1143
887
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
const output = outputObject(config);
|
|
888
|
+
function editOutput({ source, config, edit, quote }) {
|
|
889
|
+
const output = getOutput(config);
|
|
1147
890
|
if ("reason" in output) return output;
|
|
1148
|
-
if (edit.operation === ConfigEditOperation.REMOVE_OUTPUT) return
|
|
891
|
+
if (edit.operation === ConfigEditOperation.REMOVE_OUTPUT) return removeAtPath({
|
|
1149
892
|
source,
|
|
1150
893
|
options: output.object,
|
|
1151
894
|
path: edit.path
|
|
1152
895
|
});
|
|
896
|
+
const [key, ...rest] = edit.path;
|
|
1153
897
|
if (!isOptionValue(edit.value) || !isOutputValue({
|
|
1154
|
-
key
|
|
1155
|
-
value:
|
|
1156
|
-
})) return { reason: `the value is not one output.${
|
|
1157
|
-
return
|
|
898
|
+
key,
|
|
899
|
+
value: wrapPath(rest, edit.value)
|
|
900
|
+
})) return { reason: `the value is not one output.${key} takes` };
|
|
901
|
+
return setAtPath({
|
|
1158
902
|
source,
|
|
1159
903
|
options: output.object,
|
|
1160
904
|
path: edit.path,
|
|
@@ -1162,163 +906,216 @@ function applyOutputEdit({ source, config, edit, quote }) {
|
|
|
1162
906
|
quote
|
|
1163
907
|
});
|
|
1164
908
|
}
|
|
1165
|
-
/**
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
function applyRemove(source, call, path) {
|
|
1170
|
-
const options = call.arguments[0];
|
|
1171
|
-
if (!options) return { source };
|
|
1172
|
-
if (options.type !== "ObjectExpression") return { reason: "the factory was not called with an object literal" };
|
|
1173
|
-
return removeIn({
|
|
1174
|
-
source,
|
|
1175
|
-
options,
|
|
1176
|
-
path
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
/** Deletes the member at `path` inside `options`. A path that is not there already holds its default. */
|
|
1180
|
-
function removeIn({ source, options, path }) {
|
|
1181
|
-
const target = walk(options, path);
|
|
1182
|
-
if ("reason" in target) return target;
|
|
1183
|
-
if ("missing" in target) return { source };
|
|
1184
|
-
return { source: removeMember(source, target.object, target.index) };
|
|
1185
|
-
}
|
|
1186
|
-
/**
|
|
1187
|
-
* Writes an import after the last one already in the file, matching its quote style and whether it
|
|
1188
|
-
* ends in a semicolon: `import { x } from '...'`, or `const { x } = require('...')` in a CommonJS file.
|
|
1189
|
-
* The last import is read off the AST, so a multi-line `import {\n x,\n} from '...'` counts to its closing line.
|
|
1190
|
-
*/
|
|
1191
|
-
function insertImport(source, { mod, kind }, importName, moduleSpecifier) {
|
|
1192
|
-
const afterLine = bodyOf(mod).findLast((node) => kind === "esm" ? node.type === "ImportDeclaration" : node.type === "VariableDeclaration" && node.declarations.some((declarator) => requiredModule(declarator.init) !== void 0))?.loc?.end.line ?? 0;
|
|
909
|
+
/** Adds the import after the last one, matching its semicolon style. */
|
|
910
|
+
function addImport({ source, parsed, importNames, moduleSpecifier, quote }) {
|
|
911
|
+
const lastImport = parsed.program.body.findLast((node) => isImportStatement(node, parsed.kind));
|
|
912
|
+
const afterLine = lastImport ? getPosition(source, lastImport.end).line : 0;
|
|
1193
913
|
const lines = source.split("\n");
|
|
1194
|
-
const
|
|
1195
|
-
const quote = lastImportLine?.includes(`"`) ? `"` : `'`;
|
|
1196
|
-
const semicolon = lastImportLine?.trimEnd().endsWith(";") ? ";" : "";
|
|
914
|
+
const semicolon = afterLine > 0 && lines[afterLine - 1]?.trimEnd().endsWith(";") ? ";" : "";
|
|
1197
915
|
const specifier = `${quote}${moduleSpecifier}${quote}`;
|
|
1198
|
-
|
|
916
|
+
const statement = parsed.kind === moduleKinds.esm ? `import { ${importNames.join(", ")} } from ${specifier}` : `const { ${importNames.join(", ")} } = require(${specifier})`;
|
|
917
|
+
lines.splice(afterLine, 0, `${statement}${semicolon}`);
|
|
1199
918
|
if (afterLine === 0) lines.splice(1, 0, "");
|
|
1200
919
|
return lines.join("\n");
|
|
1201
920
|
}
|
|
1202
921
|
/**
|
|
1203
|
-
*
|
|
1204
|
-
*
|
|
922
|
+
* The parsers `defineConfig` applies when a config sets no `parsers`, by import name. Adding a parser
|
|
923
|
+
* to such a config writes these too, or the new array would replace them.
|
|
1205
924
|
*/
|
|
1206
|
-
|
|
925
|
+
const DEFAULT_PARSERS = [
|
|
926
|
+
{
|
|
927
|
+
importName: "parserTs",
|
|
928
|
+
packageName: "@kubb/parser-ts"
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
importName: "parserTsx",
|
|
932
|
+
packageName: "@kubb/parser-ts"
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
importName: "parserMd",
|
|
936
|
+
packageName: "@kubb/parser-md"
|
|
937
|
+
}
|
|
938
|
+
];
|
|
939
|
+
/**
|
|
940
|
+
* Adds a plugin to `plugins`, or with `kind: 'parser'` a parser to `parsers`. Replaying the same add
|
|
941
|
+
* is a no-op, while an import name taken by another package is an error.
|
|
942
|
+
*/
|
|
943
|
+
function addPlugin({ source, parsed, config, edit, quote, extensions }) {
|
|
944
|
+
const kind = edit.kind ?? extensionKinds.plugin;
|
|
945
|
+
const isParser = kind === extensionKinds.parser;
|
|
1207
946
|
if (!isAllowedPlugin({
|
|
1208
947
|
packageName: edit.plugin,
|
|
1209
|
-
extensions
|
|
1210
|
-
|
|
948
|
+
extensions,
|
|
949
|
+
kind
|
|
950
|
+
})) return { reason: isParser ? `"${edit.plugin}" is not an installed Kubb parser` : `"${edit.plugin}" is not a @kubb/plugin-* package or an installed Kubb plugin` };
|
|
1211
951
|
const importName = extensions.get(edit.plugin)?.manifest.export ?? toExportName(edit.plugin);
|
|
1212
|
-
if (!
|
|
1213
|
-
if (
|
|
1214
|
-
const taken = importedFrom(parsed.mod).get(importName)?.from;
|
|
1215
|
-
if (taken && taken !== edit.plugin) return { reason: `${importName} is already imported from ${taken}` };
|
|
952
|
+
if (!IDENTIFIER_PATTERN.test(importName)) return { reason: `"${importName}" is not a valid import name` };
|
|
953
|
+
if (findCalls(parsed.program, config, kind).some((entry) => entry.packageName === edit.plugin && (!isParser || entry.exportName === importName))) return { source };
|
|
1216
954
|
const options = edit.options ?? {};
|
|
1217
955
|
if (!isOptionValue(options)) return { reason: "the options are not literals that can be written to a config file" };
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
956
|
+
const key = `${kind}s`;
|
|
957
|
+
const array = getValue(config, key);
|
|
958
|
+
if (array ? array.type !== nodeTypes.arrayExpression : !isParser) return { reason: `${key} is not an array literal` };
|
|
959
|
+
const defaults = isParser && !array ? DEFAULT_PARSERS.filter((parser) => parser.importName !== importName) : [];
|
|
960
|
+
const imports = getImports(parsed.program);
|
|
961
|
+
const missing = /* @__PURE__ */ new Map();
|
|
962
|
+
for (const item of [...defaults, {
|
|
963
|
+
importName,
|
|
964
|
+
packageName: edit.plugin
|
|
965
|
+
}]) {
|
|
966
|
+
const from = imports.get(item.importName)?.from;
|
|
967
|
+
if (from && from !== item.packageName) return { reason: `${item.importName} is already imported from ${from}` };
|
|
968
|
+
if (!from) missing.set(item.packageName, [...missing.get(item.packageName) ?? [], item.importName]);
|
|
969
|
+
}
|
|
970
|
+
const call = `${importName}(${Object.keys(options).length ? printValue(options, quote) : ""})`;
|
|
971
|
+
const next = array?.type === nodeTypes.arrayExpression ? appendItem(source, array, call) : appendItem(source, config, `parsers: [${[...defaults.map((parser) => `${parser.importName}()`), call].join(", ")}]`);
|
|
972
|
+
return { source: [...missing].reduceRight((current, [moduleSpecifier, importNames]) => addImport({
|
|
973
|
+
source: current,
|
|
974
|
+
parsed,
|
|
975
|
+
importNames,
|
|
976
|
+
moduleSpecifier,
|
|
977
|
+
quote
|
|
978
|
+
}), next) };
|
|
1222
979
|
}
|
|
1223
|
-
/**
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
function expandPluginsArray(source, config) {
|
|
1228
|
-
const array = property(config, "plugins");
|
|
1229
|
-
if (array?.type !== "ArrayExpression") return void 0;
|
|
980
|
+
/** One item per line, so a call can be commented out alone. Skipped when comments sit between items. */
|
|
981
|
+
function splitPlugins(source, config) {
|
|
982
|
+
const array = getValue(config, "plugins");
|
|
983
|
+
if (array?.type !== nodeTypes.arrayExpression) return void 0;
|
|
1230
984
|
const items = array.elements.flatMap((element) => element ? [element] : []);
|
|
1231
|
-
const { start, end } =
|
|
1232
|
-
const
|
|
1233
|
-
const gaps = [source.slice(start + 1, ranges[0]?.start ?? end - 1), ...ranges.map((range, index) => source.slice(range.end, ranges[index + 1]?.start ?? end - 1))];
|
|
985
|
+
const { start, end } = array;
|
|
986
|
+
const gaps = [source.slice(start + 1, items[0]?.start ?? end - 1), ...items.map((item, index) => source.slice(item.end, items[index + 1]?.start ?? end - 1))];
|
|
1234
987
|
if (!items.length || gaps.some((gap) => !/^[\s,]*$/.test(gap))) return void 0;
|
|
1235
988
|
const lineStart = source.lastIndexOf("\n", start - 1) + 1;
|
|
1236
989
|
const indent = source.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
|
|
1237
|
-
return
|
|
990
|
+
return replaceText(source, start, end, `[\n${items.map((item) => `${indent} ${source.slice(item.start, item.end)},`).join("\n")}\n${indent}]`);
|
|
1238
991
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
992
|
+
function disablePlugin(source, program, config, plugin) {
|
|
993
|
+
const call = findCalls(program, config).find((entry) => entry.packageName === plugin)?.call;
|
|
994
|
+
if (!call) return { reason: `${plugin} is not in the plugins array` };
|
|
995
|
+
const start = getPosition(source, call.start);
|
|
996
|
+
const end = getPosition(source, call.end);
|
|
1243
997
|
const lines = source.split("\n");
|
|
1244
|
-
const from =
|
|
1245
|
-
const to =
|
|
998
|
+
const from = start.line - 1;
|
|
999
|
+
const to = end.line - 1;
|
|
1246
1000
|
const firstLine = lines[from] ?? "";
|
|
1247
|
-
if (firstLine.slice(0,
|
|
1001
|
+
if (firstLine.slice(0, start.column).trim() || !/^,?\s*$/.test((lines[to] ?? "").slice(end.column))) return { reason: `${plugin} shares a line with other code, so it cannot be commented out safely` };
|
|
1248
1002
|
const indent = firstLine.match(/^\s*/)?.[0] ?? "";
|
|
1249
1003
|
const commented = lines.slice(from, to + 1).map((line) => line.trim() ? `${indent}// ${line.slice(indent.length)}` : `${indent}//`);
|
|
1250
1004
|
lines.splice(from, commented.length, `${indent}// kubb:disabled ${plugin} ${commented.length}`, ...commented);
|
|
1251
1005
|
return { source: lines.join("\n") };
|
|
1252
1006
|
}
|
|
1253
|
-
/**
|
|
1254
|
-
function
|
|
1255
|
-
const
|
|
1007
|
+
/** Disables the plugin, first putting a one-line `plugins: [a(), b()]` (what `add-plugin` writes) on separate lines. */
|
|
1008
|
+
function disablePluginInConfig({ source, parsed, config, edit }) {
|
|
1009
|
+
const result = disablePlugin(source, parsed.program, config, edit.plugin);
|
|
1010
|
+
if (!("reason" in result) || !result.reason.includes("shares a line")) return result;
|
|
1011
|
+
const split = splitPlugins(source, config);
|
|
1012
|
+
if (split === void 0) return result;
|
|
1013
|
+
const reparsed = findConfigs(split);
|
|
1014
|
+
const reparsedConfig = "reason" in reparsed ? void 0 : findConfigEntry(reparsed.configs, edit.config);
|
|
1015
|
+
if ("reason" in reparsed || !reparsedConfig) return result;
|
|
1016
|
+
return disablePlugin(split, reparsed.program, reparsedConfig, edit.plugin);
|
|
1017
|
+
}
|
|
1018
|
+
function enablePlugin(source, comments, config, plugin) {
|
|
1019
|
+
const block = findDisabled(source, comments, config).find((entry) => entry.packageName === plugin);
|
|
1256
1020
|
if (!block) return { reason: `${plugin} is not disabled` };
|
|
1257
1021
|
const lines = source.split("\n");
|
|
1258
1022
|
lines.splice(block.line - 1, block.end - block.line + 1, ...block.code);
|
|
1259
1023
|
return { source: lines.join("\n") };
|
|
1260
1024
|
}
|
|
1261
|
-
/**
|
|
1262
|
-
function
|
|
1263
|
-
const parsed =
|
|
1025
|
+
/** Drops the import statements of `packageName` once nothing else in the file uses their names. */
|
|
1026
|
+
function removeUnusedImports(source, packageName) {
|
|
1027
|
+
const parsed = parseSource(source);
|
|
1028
|
+
if ("reason" in parsed) return source;
|
|
1029
|
+
return parsed.program.body.reduceRight((current, statement) => {
|
|
1030
|
+
const bindings = statement.type === nodeTypes.importDeclaration ? importBindings(statement) : statement.type === nodeTypes.variableDeclaration ? requireBindings(statement) : [];
|
|
1031
|
+
if (!bindings.length || bindings.some(([, { from }]) => from !== packageName)) return current;
|
|
1032
|
+
const rest = replaceText(current, statement.start, statement.end);
|
|
1033
|
+
if (bindings.some(([local]) => new RegExp(`(?<![\\w$])${local.replaceAll("$", "\\$")}(?![\\w$])`).test(rest))) return current;
|
|
1034
|
+
const lineStart = current.lastIndexOf("\n", statement.start - 1) + 1;
|
|
1035
|
+
const lineEnd = current.indexOf("\n", statement.end);
|
|
1036
|
+
const after = lineEnd === -1 ? current.length : lineEnd + 1;
|
|
1037
|
+
return replaceText(current, lineStart, lineStart === 0 && current[after] === "\n" ? after + 1 : after);
|
|
1038
|
+
}, source);
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Removes every call of a plugin or parser package, and the block `disable-plugin` commented out,
|
|
1042
|
+
* then its unused imports. Removing what the config does not hold is a no-op. Refuses to empty
|
|
1043
|
+
* `parsers`, since a config with no parser cannot print files.
|
|
1044
|
+
*/
|
|
1045
|
+
function removeExtension(source, edit) {
|
|
1046
|
+
const kind = edit.kind ?? extensionKinds.plugin;
|
|
1047
|
+
let current = source;
|
|
1048
|
+
for (;;) {
|
|
1049
|
+
const parsed = findConfigs(current);
|
|
1050
|
+
if ("reason" in parsed) return parsed;
|
|
1051
|
+
const config = findConfigEntry(parsed.configs, edit.config);
|
|
1052
|
+
if (!config) return { reason: `no config entry found for ${JSON.stringify(edit.config)}` };
|
|
1053
|
+
const array = getValue(config, `${kind}s`);
|
|
1054
|
+
const calls = findCalls(parsed.program, config, kind).filter((entry) => entry.packageName === edit.plugin);
|
|
1055
|
+
const block = findDisabled(current, parsed.comments, config).find((entry) => entry.packageName === edit.plugin);
|
|
1056
|
+
if (calls[0] && array?.type === nodeTypes.arrayExpression) {
|
|
1057
|
+
const elements = array.elements.filter((element) => element !== null);
|
|
1058
|
+
if (kind === extensionKinds.parser && elements.length === calls.length) return { reason: `${edit.plugin} is the last parser in the parsers array` };
|
|
1059
|
+
current = removeItem(current, array, elements.indexOf(calls[0].call));
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
if (block && kind === extensionKinds.plugin) {
|
|
1063
|
+
const lines = current.split("\n");
|
|
1064
|
+
lines.splice(block.line - 1, block.end - block.line + 1);
|
|
1065
|
+
current = lines.join("\n");
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
return { source: current === source ? source : removeUnusedImports(current, edit.plugin) };
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
/** Every edit re-parses the file, since the previous one rewrote it. */
|
|
1072
|
+
function applyEdit({ source, edit, extensions }) {
|
|
1073
|
+
const parsed = findConfigs(source);
|
|
1264
1074
|
if ("reason" in parsed) return parsed;
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1075
|
+
const config = findConfigEntry(parsed.configs, edit.config);
|
|
1076
|
+
if (!config) return { reason: `no config entry found for ${JSON.stringify(edit.config)}` };
|
|
1077
|
+
const quote = detectQuote(parsed.program) ?? quotes.single;
|
|
1078
|
+
switch (edit.operation) {
|
|
1079
|
+
case ConfigEditOperation.SET_INPUT: return setInput(source, config, edit.input, quote);
|
|
1080
|
+
case ConfigEditOperation.SET_OUTPUT:
|
|
1081
|
+
case ConfigEditOperation.REMOVE_OUTPUT: return editOutput({
|
|
1082
|
+
source,
|
|
1083
|
+
config,
|
|
1084
|
+
edit,
|
|
1085
|
+
quote
|
|
1086
|
+
});
|
|
1087
|
+
case ConfigEditOperation.DISABLE_PLUGIN: return disablePluginInConfig({
|
|
1088
|
+
source,
|
|
1089
|
+
parsed,
|
|
1090
|
+
config,
|
|
1091
|
+
edit
|
|
1092
|
+
});
|
|
1093
|
+
case ConfigEditOperation.ENABLE_PLUGIN: return enablePlugin(source, parsed.comments, config, edit.plugin);
|
|
1094
|
+
case ConfigEditOperation.ADD_PLUGIN: return addPlugin({
|
|
1095
|
+
source,
|
|
1096
|
+
parsed,
|
|
1097
|
+
config,
|
|
1098
|
+
edit,
|
|
1099
|
+
quote,
|
|
1100
|
+
extensions
|
|
1101
|
+
});
|
|
1102
|
+
case ConfigEditOperation.REMOVE_EXTENSION: return removeExtension(source, edit);
|
|
1103
|
+
case ConfigEditOperation.SET_OPTION:
|
|
1104
|
+
case ConfigEditOperation.REMOVE_OPTION: return editOption({
|
|
1105
|
+
source,
|
|
1106
|
+
parsed,
|
|
1107
|
+
config,
|
|
1108
|
+
edit,
|
|
1109
|
+
quote,
|
|
1110
|
+
extensions
|
|
1111
|
+
});
|
|
1300
1112
|
}
|
|
1301
|
-
return edit.operation === ConfigEditOperation.SET ? applySet(source, call, edit.path, edit.value, quote) : applyRemove(source, call, edit.path);
|
|
1302
1113
|
}
|
|
1303
|
-
/**
|
|
1304
|
-
|
|
1305
|
-
* comments, formatting, and hand-written code around the config survive.
|
|
1306
|
-
*
|
|
1307
|
-
* Edits are independent: one that cannot be applied is reported in `outcomes` and the rest still run.
|
|
1308
|
-
*
|
|
1309
|
-
* @example
|
|
1310
|
-
* ```ts
|
|
1311
|
-
* const { source, outcomes } = applyConfigEdits({
|
|
1312
|
-
* source: current,
|
|
1313
|
-
* edits: [{ operation: 'set', plugin: '@kubb/plugin-ts', path: ['enum', 'type'], value: 'enum' }],
|
|
1314
|
-
* })
|
|
1315
|
-
* ```
|
|
1316
|
-
*/
|
|
1317
|
-
function applyConfigEdits({ source, edits, extensions = /* @__PURE__ */ new Map() }) {
|
|
1114
|
+
/** Applies edits in place, touching only the text each targets. A refused edit is reported and the rest still run. */
|
|
1115
|
+
function editConfig({ source, edits, extensions = /* @__PURE__ */ new Map() }) {
|
|
1318
1116
|
let current = source;
|
|
1319
|
-
const quote = detectCodeFormat(source).quote === "double" ? `"` : `'`;
|
|
1320
1117
|
const outcomes = edits.map((edit) => {
|
|
1321
|
-
const invalid =
|
|
1118
|
+
const invalid = validateEdit(edit);
|
|
1322
1119
|
if (invalid) return {
|
|
1323
1120
|
edit,
|
|
1324
1121
|
applied: false,
|
|
@@ -1328,7 +1125,6 @@ function applyConfigEdits({ source, edits, extensions = /* @__PURE__ */ new Map(
|
|
|
1328
1125
|
const result = applyEdit({
|
|
1329
1126
|
source: current,
|
|
1330
1127
|
edit,
|
|
1331
|
-
quote,
|
|
1332
1128
|
extensions
|
|
1333
1129
|
});
|
|
1334
1130
|
if ("reason" in result) return {
|
|
@@ -1355,17 +1151,14 @@ function applyConfigEdits({ source, edits, extensions = /* @__PURE__ */ new Map(
|
|
|
1355
1151
|
changed: current !== source
|
|
1356
1152
|
};
|
|
1357
1153
|
}
|
|
1358
|
-
/**
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
*/
|
|
1362
|
-
async function writeConfigEdits({ filePath, edits, extensions }) {
|
|
1363
|
-
const result = applyConfigEdits({
|
|
1154
|
+
/** Applies `edits` to `filePath`, writing only when something changed. */
|
|
1155
|
+
async function editConfigFile({ filePath, edits, extensions }) {
|
|
1156
|
+
const result = editConfig({
|
|
1364
1157
|
source: await read(filePath),
|
|
1365
1158
|
edits,
|
|
1366
1159
|
extensions
|
|
1367
1160
|
});
|
|
1368
|
-
if (result.changed) await
|
|
1161
|
+
if (result.changed) await writeRaw(filePath, result.source);
|
|
1369
1162
|
return result;
|
|
1370
1163
|
}
|
|
1371
1164
|
//#endregion
|
|
@@ -1916,149 +1709,474 @@ function createGenerationStream(hooks, jobId) {
|
|
|
1916
1709
|
} : void 0
|
|
1917
1710
|
}]
|
|
1918
1711
|
};
|
|
1919
|
-
function forward(type) {
|
|
1920
|
-
on(type, (...args) => {
|
|
1921
|
-
emitEvent(type, projections[type](...args));
|
|
1922
|
-
if (!isDiscardable(type)) return new Promise((resolve) => setImmediate(resolve));
|
|
1712
|
+
function forward(type) {
|
|
1713
|
+
on(type, (...args) => {
|
|
1714
|
+
emitEvent(type, projections[type](...args));
|
|
1715
|
+
if (!isDiscardable(type)) return new Promise((resolve) => setImmediate(resolve));
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
on("kubb:build:start", ({ config }) => {
|
|
1719
|
+
root = config.root;
|
|
1720
|
+
});
|
|
1721
|
+
for (const type of generationEventTypes) forward(type);
|
|
1722
|
+
on("kubb:generation:end", ({ diagnostics = [], status, hrStart, filesCreated }) => {
|
|
1723
|
+
if (!hrStart) return;
|
|
1724
|
+
emitEvent("kubb:generation:summary", [{
|
|
1725
|
+
duration: Math.round(getElapsedMs(hrStart)),
|
|
1726
|
+
fileCount: filesCreated ?? 0,
|
|
1727
|
+
failedPlugins: Diagnostics.failedPlugins(diagnostics).length,
|
|
1728
|
+
status: status ?? "success"
|
|
1729
|
+
}]);
|
|
1730
|
+
});
|
|
1731
|
+
/**
|
|
1732
|
+
* Takes this generation's listeners off the session emitter. Safe to call twice.
|
|
1733
|
+
*/
|
|
1734
|
+
function detach() {
|
|
1735
|
+
for (const unhook of unhooks) unhook();
|
|
1736
|
+
unhooks.length = 0;
|
|
1737
|
+
}
|
|
1738
|
+
async function close() {
|
|
1739
|
+
if (closed) return;
|
|
1740
|
+
closed = true;
|
|
1741
|
+
detach();
|
|
1742
|
+
controller.close();
|
|
1743
|
+
}
|
|
1744
|
+
function fail(error) {
|
|
1745
|
+
detach();
|
|
1746
|
+
if (closed) return;
|
|
1747
|
+
closed = true;
|
|
1748
|
+
controller.error(error);
|
|
1749
|
+
}
|
|
1750
|
+
return {
|
|
1751
|
+
stream,
|
|
1752
|
+
close,
|
|
1753
|
+
fail
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
//#endregion
|
|
1757
|
+
//#region src/publicFetch.ts
|
|
1758
|
+
const MAX_REDIRECTS = 5;
|
|
1759
|
+
const MAX_BYTES = 52428800;
|
|
1760
|
+
const REDIRECTS = /* @__PURE__ */ new Set([
|
|
1761
|
+
301,
|
|
1762
|
+
302,
|
|
1763
|
+
303,
|
|
1764
|
+
307,
|
|
1765
|
+
308
|
|
1766
|
+
]);
|
|
1767
|
+
const NO_BODY = /* @__PURE__ */ new Set([
|
|
1768
|
+
204,
|
|
1769
|
+
205,
|
|
1770
|
+
304
|
|
1771
|
+
]);
|
|
1772
|
+
function notPublic(host) {
|
|
1773
|
+
return /* @__PURE__ */ new Error(`${host} is not a public address, so a managed agent does not fetch from it`);
|
|
1774
|
+
}
|
|
1775
|
+
function createLookup(blocked) {
|
|
1776
|
+
return (hostname, options, callback) => {
|
|
1777
|
+
dns.lookup(hostname, {
|
|
1778
|
+
...options,
|
|
1779
|
+
all: true
|
|
1780
|
+
}, (error, addresses) => {
|
|
1781
|
+
if (error) return callback(error, "", 0);
|
|
1782
|
+
const found = addresses;
|
|
1783
|
+
if (!found.length || found.some(({ address }) => !isPublicAddress(address, blocked))) return callback(notPublic(hostname), "", 0);
|
|
1784
|
+
return options.all ? callback(null, found) : callback(null, found[0].address, found[0].family);
|
|
1785
|
+
});
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function request({ url, blocked, signal }) {
|
|
1789
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return Promise.reject(/* @__PURE__ */ new Error(`Only http and https URLs are fetched, not ${url.protocol}`));
|
|
1790
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
1791
|
+
if (net.isIP(host) && !isPublicAddress(host, blocked)) return Promise.reject(notPublic(host));
|
|
1792
|
+
return new Promise((resolve, reject) => {
|
|
1793
|
+
const req = (url.protocol === "https:" ? https.request : http.request)(url, {
|
|
1794
|
+
method: "GET",
|
|
1795
|
+
lookup: createLookup(blocked),
|
|
1796
|
+
signal: signal ?? void 0,
|
|
1797
|
+
headers: { "accept-encoding": "identity" }
|
|
1798
|
+
}, (res) => {
|
|
1799
|
+
const headers = new Headers();
|
|
1800
|
+
for (const [key, value] of Object.entries(res.headers)) for (const item of [value].flat()) if (item !== void 0) headers.append(key, item);
|
|
1801
|
+
let total = 0;
|
|
1802
|
+
const limit = new Transform({ transform(chunk, _encoding, done) {
|
|
1803
|
+
total += chunk.length;
|
|
1804
|
+
done(total > MAX_BYTES ? /* @__PURE__ */ new Error("The response is larger than 50 MB") : null, chunk);
|
|
1805
|
+
} });
|
|
1806
|
+
res.on("error", (error) => limit.destroy(error));
|
|
1807
|
+
res.pipe(limit);
|
|
1808
|
+
const body = NO_BODY.has(res.statusCode ?? 0) ? null : Readable.toWeb(limit);
|
|
1809
|
+
resolve(new Response(body, {
|
|
1810
|
+
status: res.statusCode,
|
|
1811
|
+
statusText: res.statusMessage,
|
|
1812
|
+
headers
|
|
1813
|
+
}));
|
|
1814
|
+
});
|
|
1815
|
+
req.on("error", reject);
|
|
1816
|
+
req.end();
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Builds a `fetch` that only reaches addresses `blocked` does not list. It follows redirects itself
|
|
1821
|
+
* and checks every hop, and takes GET requests only, since that is all a spec load needs.
|
|
1822
|
+
*/
|
|
1823
|
+
function createPublicFetch(blocked) {
|
|
1824
|
+
const publicFetch = async (input, init) => {
|
|
1825
|
+
if (typeof input !== "string" && !(input instanceof URL)) throw new TypeError("A managed agent fetches by URL only");
|
|
1826
|
+
if (init?.method && init.method.toUpperCase() !== "GET") throw new TypeError("A managed agent only sends GET requests");
|
|
1827
|
+
let url = new URL(input);
|
|
1828
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
1829
|
+
const response = await request({
|
|
1830
|
+
url,
|
|
1831
|
+
blocked,
|
|
1832
|
+
signal: init?.signal
|
|
1833
|
+
});
|
|
1834
|
+
const location = response.headers.get("location");
|
|
1835
|
+
if (!REDIRECTS.has(response.status) || !location) return response;
|
|
1836
|
+
await response.body?.cancel();
|
|
1837
|
+
url = new URL(location, url);
|
|
1838
|
+
}
|
|
1839
|
+
throw new Error(`Stopped after ${MAX_REDIRECTS} redirects from ${String(input)}`);
|
|
1840
|
+
};
|
|
1841
|
+
return publicFetch;
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Runs `run` with the global `fetch` limited to public addresses, then puts the original back.
|
|
1845
|
+
* A managed agent runs one job at a time, so nothing else shares the window.
|
|
1846
|
+
*/
|
|
1847
|
+
async function withPublicFetch(run) {
|
|
1848
|
+
const original = globalThis.fetch;
|
|
1849
|
+
globalThis.fetch = createPublicFetch();
|
|
1850
|
+
try {
|
|
1851
|
+
return await run();
|
|
1852
|
+
} finally {
|
|
1853
|
+
globalThis.fetch = original;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
//#endregion
|
|
1857
|
+
//#region src/resolveConfig.ts
|
|
1858
|
+
/**
|
|
1859
|
+
* Turns the JSON config Studio sends back into live Kubb objects.
|
|
1860
|
+
*
|
|
1861
|
+
* A plugin or adapter instance carries closures (`parse`, `getImports`, ...) that cannot survive
|
|
1862
|
+
* JSON, so both sides pass options over the wire and the factory is re-invoked here with the merged
|
|
1863
|
+
* result. Only packages {@link isAllowedPlugin} accepts are resolved this way, never an arbitrary
|
|
1864
|
+
* module the payload names.
|
|
1865
|
+
*/
|
|
1866
|
+
/**
|
|
1867
|
+
* Imports a package, falling back to how the user's project would resolve it.
|
|
1868
|
+
*
|
|
1869
|
+
* `import()` resolves from this file, so a linked or globally installed Studio (`pnpm link`,
|
|
1870
|
+
* `npm i -g`) only sees its own `node_modules` and misses the plugins installed next to the user's
|
|
1871
|
+
* config. The retry resolves from `process.cwd()` instead.
|
|
1872
|
+
*/
|
|
1873
|
+
async function importFromProject(packageName) {
|
|
1874
|
+
try {
|
|
1875
|
+
return await import(packageName);
|
|
1876
|
+
} catch {
|
|
1877
|
+
const resolved = createRequire(pathToFileURL(`${process.cwd()}/`)).resolve(packageName);
|
|
1878
|
+
const esm = resolved.replace(/\.cjs$/, ".js");
|
|
1879
|
+
return await import(pathToFileURL(esm !== resolved && existsSync(esm) ? esm : resolved).href);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Adds the `@kubb/` scope a plugin's package carries but its `name` does not, the inverse of
|
|
1884
|
+
* {@link toPluginName}. A name outside the `plugin-` convention is left alone, so a third-party
|
|
1885
|
+
* plugin is not reported as one of Kubb's.
|
|
1886
|
+
*
|
|
1887
|
+
* @example
|
|
1888
|
+
* ```ts
|
|
1889
|
+
* toPackageName('plugin-ts') // '@kubb/plugin-ts'
|
|
1890
|
+
* toPackageName('@acme/my-plugin') // '@acme/my-plugin'
|
|
1891
|
+
* ```
|
|
1892
|
+
*/
|
|
1893
|
+
function toPackageName(name) {
|
|
1894
|
+
return name.startsWith("plugin-") ? `@kubb/${name}` : name;
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* The name Studio knows an extension by: the package of the installed extension whose manifest
|
|
1898
|
+
* `name` matches, otherwise its own `name`, so a plugin defined in the user's project is never
|
|
1899
|
+
* reported as a Kubb package that does not exist.
|
|
1900
|
+
*
|
|
1901
|
+
* @example
|
|
1902
|
+
* ```ts
|
|
1903
|
+
* toStudioName({ name: 'plugin-ts', extensions }) // '@kubb/plugin-ts'
|
|
1904
|
+
* toStudioName({ name: 'plugin-zod-operations', extensions }) // 'plugin-zod-operations', a local plugin
|
|
1905
|
+
* ```
|
|
1906
|
+
*/
|
|
1907
|
+
function toStudioName({ name, extensions = /* @__PURE__ */ new Map(), kind = "plugin" }) {
|
|
1908
|
+
return [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name)?.package ?? name;
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Dynamically imports an extension package and returns its factory. Callers check
|
|
1912
|
+
* {@link isAllowedPlugin} or derive the package from a disk instance first, so an arbitrary module
|
|
1913
|
+
* never reaches `import()`.
|
|
1914
|
+
*
|
|
1915
|
+
* Packages must be pre-installed in the Docker image at build time via the `KUBB_PACKAGES`
|
|
1916
|
+
* build ARG, no runtime installation is possible in the distroless container.
|
|
1917
|
+
*
|
|
1918
|
+
* Resolution order: the manifest's `export`, else the camelCase named export the package name
|
|
1919
|
+
* implies (e.g. `pluginTs`), then the default export.
|
|
1920
|
+
*
|
|
1921
|
+
* @throws if the package cannot be imported or exports no callable factory.
|
|
1922
|
+
*/
|
|
1923
|
+
async function loadFactory({ packageName, extensions, kind = "plugin" }) {
|
|
1924
|
+
const label = {
|
|
1925
|
+
plugin: "Plugin",
|
|
1926
|
+
adapter: "Adapter",
|
|
1927
|
+
parser: "Parser"
|
|
1928
|
+
}[kind];
|
|
1929
|
+
let mod;
|
|
1930
|
+
try {
|
|
1931
|
+
mod = await importFromProject(packageName);
|
|
1932
|
+
} catch (cause) {
|
|
1933
|
+
throw new Diagnostics.Error({
|
|
1934
|
+
code: kind === "plugin" ? Diagnostics.code.pluginNotFound : Diagnostics.code.unknown,
|
|
1935
|
+
severity: "error",
|
|
1936
|
+
message: `${label} "${packageName}" could not be loaded. Make sure it is installed: \`npm install ${packageName}\``,
|
|
1937
|
+
cause: toError(cause)
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
const manifestExport = extensions.get(packageName)?.manifest.export;
|
|
1941
|
+
if (manifestExport) {
|
|
1942
|
+
if (typeof mod[manifestExport] === "function") return mod[manifestExport];
|
|
1943
|
+
throw new Diagnostics.Error({
|
|
1944
|
+
code: Diagnostics.code.incompatibleExtension,
|
|
1945
|
+
severity: "error",
|
|
1946
|
+
message: `${label} "${packageName}" does not export "${manifestExport}", the factory its kubb-extension.json names.`,
|
|
1947
|
+
help: "Install a version of the plugin whose exports match its kubb-extension.json."
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
const exportName = toExportName(packageName);
|
|
1951
|
+
if (typeof mod[exportName] === "function") return mod[exportName];
|
|
1952
|
+
if (typeof mod["default"] === "function") return mod["default"];
|
|
1953
|
+
throw new Diagnostics.Error({
|
|
1954
|
+
code: Diagnostics.code.incompatibleExtension,
|
|
1955
|
+
severity: "error",
|
|
1956
|
+
message: `${label} "${packageName}" does not export a callable factory. Tried the named export "${exportName}" and "default".`,
|
|
1957
|
+
help: "Install a Kubb plugin package that exports its factory."
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Resolves each plugin entry by dynamically importing its package and calling its factory with the
|
|
1962
|
+
* provided options. Only packages {@link isAllowedPlugin} accepts are loaded.
|
|
1963
|
+
*
|
|
1964
|
+
* @example
|
|
1965
|
+
* ```ts
|
|
1966
|
+
* const plugins = await resolvePlugins({
|
|
1967
|
+
* plugins: [
|
|
1968
|
+
* { name: '@kubb/plugin-react-query', options: { output: { path: './hooks' } } },
|
|
1969
|
+
* { name: '@kubb/plugin-ts', options: { output: { path: './types' } } },
|
|
1970
|
+
* ],
|
|
1971
|
+
* })
|
|
1972
|
+
* ```
|
|
1973
|
+
*/
|
|
1974
|
+
async function resolvePlugins({ plugins, extensions = /* @__PURE__ */ new Map() }) {
|
|
1975
|
+
return Promise.all(plugins.map(async ({ name, options }) => {
|
|
1976
|
+
if (!isAllowedPlugin({
|
|
1977
|
+
packageName: name,
|
|
1978
|
+
extensions
|
|
1979
|
+
})) throw new Diagnostics.Error({
|
|
1980
|
+
code: Diagnostics.code.pluginNotFound,
|
|
1981
|
+
severity: "error",
|
|
1982
|
+
message: `Plugin "${name}" is not an installed Kubb plugin. Studio loads @kubb/plugin-* packages and installed packages that ship a kubb-extension.json with kind "plugin".`
|
|
1983
|
+
});
|
|
1984
|
+
return (await loadFactory({
|
|
1985
|
+
packageName: name,
|
|
1986
|
+
extensions
|
|
1987
|
+
}))(options ?? {});
|
|
1988
|
+
}));
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* A JSON copy of plugin or adapter options: functions and other values JSON cannot hold are left
|
|
1992
|
+
* out, so nothing callable crosses the socket. Options that cannot be copied at all, such as a
|
|
1993
|
+
* circular structure, come back empty.
|
|
1994
|
+
*
|
|
1995
|
+
* @example
|
|
1996
|
+
* ```ts
|
|
1997
|
+
* toJSONOptions({ output: { path: 'types' }, group: { name: () => 'x' } }) // { output: { path: 'types' }, group: {} }
|
|
1998
|
+
* ```
|
|
1999
|
+
*/
|
|
2000
|
+
function toJSONOptions(options) {
|
|
2001
|
+
try {
|
|
2002
|
+
const copy = JSON.parse(JSON.stringify(options ?? {}, (key, value) => isSafeKey(key) && typeof value !== "bigint" ? value : void 0));
|
|
2003
|
+
return isPlainObject(copy) ? copy : {};
|
|
2004
|
+
} catch {
|
|
2005
|
+
return {};
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* Deep-merges Studio's options over the disk options, with Studio taking priority, except where the
|
|
2010
|
+
* disk value cannot survive JSON. Studio only ever saw a lossy copy of such a value, so the disk
|
|
2011
|
+
* value is kept rather than replaced by what Studio echoes back. Plain objects merge key by key, so
|
|
2012
|
+
* Studio can still change a literal that sits next to a function (e.g. `group.type` beside `group.name`).
|
|
2013
|
+
*
|
|
2014
|
+
* @example
|
|
2015
|
+
* ```ts
|
|
2016
|
+
* mergeOptions({ macros: [camelCaseMacro], syntaxType: 'type' }, { macros: [{ name: 'camelCase' }], syntaxType: 'interface' })
|
|
2017
|
+
* // { macros: [camelCaseMacro], syntaxType: 'interface' }
|
|
2018
|
+
* mergeOptions({ emptySchemaType: 'any' }, { emptySchemaType: null }) // {}
|
|
2019
|
+
* ```
|
|
2020
|
+
*/
|
|
2021
|
+
function mergeOptions(disk, studio) {
|
|
2022
|
+
const merged = { ...disk };
|
|
2023
|
+
for (const [key, value] of Object.entries(studio)) {
|
|
2024
|
+
if (!isSafeKey(key)) continue;
|
|
2025
|
+
const current = disk[key];
|
|
2026
|
+
if (value === null) {
|
|
2027
|
+
if (current === void 0 || isOptionValue(current)) delete merged[key];
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
if (isPlainObject(current) && isPlainObject(value)) {
|
|
2031
|
+
merged[key] = mergeOptions(current, value);
|
|
2032
|
+
continue;
|
|
2033
|
+
}
|
|
2034
|
+
if (current !== void 0 && !isOptionValue(current)) continue;
|
|
2035
|
+
merged[key] = value;
|
|
2036
|
+
}
|
|
2037
|
+
return merged;
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Merges studio plugin options with disk config plugins.
|
|
2041
|
+
* Studio takes priority: options from studio win over disk (except values only the disk config
|
|
2042
|
+
* can express, see {@link mergeOptions}), and a plugin Studio explicitly
|
|
2043
|
+
* disabled is dropped even when the disk config still lists it. Disk plugins without a studio
|
|
2044
|
+
* counterpart are kept as-is. Studio plugins not present on disk are appended.
|
|
2045
|
+
*
|
|
2046
|
+
* For plugins present in both configs, the plugin is re-instantiated with merged options
|
|
2047
|
+
* so that all internal closures correctly reference the merged values.
|
|
2048
|
+
*/
|
|
2049
|
+
async function mergePlugins({ diskPlugins, studioPlugins, extensions = /* @__PURE__ */ new Map() }) {
|
|
2050
|
+
const pluginNameOf = (packageName) => extensions.get(packageName)?.manifest.name ?? toPluginName(packageName);
|
|
2051
|
+
const disabledNames = new Set((studioPlugins ?? []).filter((entry) => entry.disabled).map((entry) => pluginNameOf(entry.name)));
|
|
2052
|
+
const activeDiskPlugins = disabledNames.size ? diskPlugins?.filter((plugin) => !disabledNames.has(resolveMeta(plugin).name)) : diskPlugins;
|
|
2053
|
+
const activeStudioPlugins = studioPlugins?.filter((entry) => !entry.disabled);
|
|
2054
|
+
if (!activeDiskPlugins && !activeStudioPlugins?.length) return void 0;
|
|
2055
|
+
if (!activeStudioPlugins?.length) return activeDiskPlugins;
|
|
2056
|
+
if (!activeDiskPlugins) return resolvePlugins({
|
|
2057
|
+
plugins: activeStudioPlugins,
|
|
2058
|
+
extensions
|
|
2059
|
+
});
|
|
2060
|
+
const studioEntryByName = new Map(activeStudioPlugins.map((entry) => [pluginNameOf(entry.name), entry]));
|
|
2061
|
+
const diskNames = new Set(activeDiskPlugins.map((plugin) => plugin.name));
|
|
2062
|
+
const merged = await Promise.all(activeDiskPlugins.map(async (diskPlugin) => {
|
|
2063
|
+
const studioEntry = studioEntryByName.get(resolveMeta(diskPlugin).name);
|
|
2064
|
+
if (!studioEntry) return diskPlugin;
|
|
2065
|
+
const diskOptions = diskPlugin.options ?? {};
|
|
2066
|
+
const options = mergeOptions(diskOptions, studioEntry.options ?? {});
|
|
2067
|
+
const unchanged = JSON.stringify(toJSONOptions(options)) === JSON.stringify(toJSONOptions(diskOptions));
|
|
2068
|
+
const [resolved] = await resolvePlugins({
|
|
2069
|
+
plugins: [{
|
|
2070
|
+
name: studioEntry.name,
|
|
2071
|
+
options
|
|
2072
|
+
}],
|
|
2073
|
+
extensions
|
|
2074
|
+
}).catch((cause) => {
|
|
2075
|
+
if (unchanged) return [diskPlugin];
|
|
2076
|
+
throw new Diagnostics.Error({
|
|
2077
|
+
code: Diagnostics.code.pluginNotFound,
|
|
2078
|
+
severity: "error",
|
|
2079
|
+
message: `Plugin "${diskPlugin.name}" is not an installed @kubb/plugin-* package or Kubb extension, so Studio cannot change its options. Edit it in the config file, or only enable or disable it from Studio.`,
|
|
2080
|
+
cause: toError(cause)
|
|
2081
|
+
});
|
|
2082
|
+
});
|
|
2083
|
+
return resolved ?? diskPlugin;
|
|
2084
|
+
}));
|
|
2085
|
+
const studioOnly = activeStudioPlugins.filter((entry) => !diskNames.has(pluginNameOf(entry.name)));
|
|
2086
|
+
return [...merged, ...await resolvePlugins({
|
|
2087
|
+
plugins: studioOnly,
|
|
2088
|
+
extensions
|
|
2089
|
+
})];
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* Throws `KUBB_PATH_TRAVERSAL` when a plugin would write outside the config's output folder to a path
|
|
2093
|
+
* the disk config does not set. Studio can change `output.path`, also inside `override`, and add
|
|
2094
|
+
* plugins, so without this check any member of the Studio organization could write any file the agent
|
|
2095
|
+
* can reach, including `node_modules` code the next run imports. A path from the config file stays
|
|
2096
|
+
* allowed, since a monorepo may write outside its root.
|
|
2097
|
+
*/
|
|
2098
|
+
function assertOutputPaths({ plugins, diskPlugins, root, outputPath }) {
|
|
2099
|
+
const base = path.resolve(root, outputPath);
|
|
2100
|
+
const targetsOf = (plugin) => {
|
|
2101
|
+
const { output, override } = plugin.options ?? {};
|
|
2102
|
+
return [output, ...Array.isArray(override) ? override.map((entry) => entry?.options?.output) : []].flatMap((entry) => typeof entry?.path === "string" ? [path.resolve(base, entry.path)] : []);
|
|
2103
|
+
};
|
|
2104
|
+
const diskTargets = new Set(diskPlugins.flatMap(targetsOf));
|
|
2105
|
+
for (const plugin of plugins) {
|
|
2106
|
+
const target = targetsOf(plugin).find((resolved) => !diskTargets.has(resolved) && !isPathInside(resolved, base));
|
|
2107
|
+
if (!target) continue;
|
|
2108
|
+
throw new Diagnostics.Error({
|
|
2109
|
+
code: Diagnostics.code.pathTraversal,
|
|
2110
|
+
severity: "error",
|
|
2111
|
+
message: `Plugin "${plugin.name}" would write to "${target}", outside the output folder "${base}". Studio can only set an output path inside it.`,
|
|
2112
|
+
help: "Choose an output path inside the output folder, or set this path in the config file.",
|
|
2113
|
+
location: { kind: "config" },
|
|
2114
|
+
plugin: plugin.name
|
|
1923
2115
|
});
|
|
1924
2116
|
}
|
|
1925
|
-
on("kubb:build:start", ({ config }) => {
|
|
1926
|
-
root = config.root;
|
|
1927
|
-
});
|
|
1928
|
-
for (const type of generationEventTypes) forward(type);
|
|
1929
|
-
on("kubb:generation:end", ({ diagnostics = [], status, hrStart, filesCreated }) => {
|
|
1930
|
-
if (!hrStart) return;
|
|
1931
|
-
emitEvent("kubb:generation:summary", [{
|
|
1932
|
-
duration: Math.round(getElapsedMs(hrStart)),
|
|
1933
|
-
fileCount: filesCreated ?? 0,
|
|
1934
|
-
failedPlugins: Diagnostics.failedPlugins(diagnostics).length,
|
|
1935
|
-
status: status ?? "success"
|
|
1936
|
-
}]);
|
|
1937
|
-
});
|
|
1938
|
-
/**
|
|
1939
|
-
* Takes this generation's listeners off the session emitter. Safe to call twice.
|
|
1940
|
-
*/
|
|
1941
|
-
function detach() {
|
|
1942
|
-
for (const unhook of unhooks) unhook();
|
|
1943
|
-
unhooks.length = 0;
|
|
1944
|
-
}
|
|
1945
|
-
async function close() {
|
|
1946
|
-
if (closed) return;
|
|
1947
|
-
closed = true;
|
|
1948
|
-
detach();
|
|
1949
|
-
controller.close();
|
|
1950
|
-
}
|
|
1951
|
-
function fail(error) {
|
|
1952
|
-
detach();
|
|
1953
|
-
if (closed) return;
|
|
1954
|
-
closed = true;
|
|
1955
|
-
controller.error(error);
|
|
1956
|
-
}
|
|
1957
|
-
return {
|
|
1958
|
-
stream,
|
|
1959
|
-
close,
|
|
1960
|
-
fail
|
|
1961
|
-
};
|
|
1962
|
-
}
|
|
1963
|
-
//#endregion
|
|
1964
|
-
//#region src/publicFetch.ts
|
|
1965
|
-
const MAX_REDIRECTS = 5;
|
|
1966
|
-
const MAX_BYTES = 52428800;
|
|
1967
|
-
const REDIRECTS = /* @__PURE__ */ new Set([
|
|
1968
|
-
301,
|
|
1969
|
-
302,
|
|
1970
|
-
303,
|
|
1971
|
-
307,
|
|
1972
|
-
308
|
|
1973
|
-
]);
|
|
1974
|
-
const NO_BODY = /* @__PURE__ */ new Set([
|
|
1975
|
-
204,
|
|
1976
|
-
205,
|
|
1977
|
-
304
|
|
1978
|
-
]);
|
|
1979
|
-
function notPublic(host) {
|
|
1980
|
-
return /* @__PURE__ */ new Error(`${host} is not a public address, so a managed agent does not fetch from it`);
|
|
1981
2117
|
}
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2118
|
+
/**
|
|
2119
|
+
* Re-invokes the factory behind a disk adapter or parser with Studio's options merged over its own.
|
|
2120
|
+
* An instance carries live functions (`parse`, `print`, ...) that cannot survive JSON, so Studio's
|
|
2121
|
+
* options are a patch, not a replacement, and the factory is found from the disk instance itself: the
|
|
2122
|
+
* installed extension whose manifest names it, or the `@kubb/<kind>-<name>` package for a built-in
|
|
2123
|
+
* one. Throws when no factory resolves, as `mergePlugins` does, so an override Studio sent is never
|
|
2124
|
+
* dropped without a word. An extension without a manifest, such as `parserTsx`, has no options in
|
|
2125
|
+
* Studio, so Studio never sends one for it.
|
|
2126
|
+
*/
|
|
2127
|
+
async function rebuildExtension({ disk, studioOptions, kind, extensions }) {
|
|
2128
|
+
if (!studioOptions || !Object.keys(studioOptions).length) return disk;
|
|
2129
|
+
const { name } = resolveMeta(disk);
|
|
2130
|
+
return (await loadFactory({
|
|
2131
|
+
packageName: [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name)?.package ?? `@kubb/${kind}-${name}`,
|
|
2132
|
+
extensions,
|
|
2133
|
+
kind
|
|
2134
|
+
}).catch((cause) => {
|
|
2135
|
+
throw new Diagnostics.Error({
|
|
2136
|
+
code: kind === "plugin" ? Diagnostics.code.pluginNotFound : Diagnostics.code.unknown,
|
|
2137
|
+
severity: "error",
|
|
2138
|
+
message: `The ${kind} "${name}" is not an installed Kubb extension, so Studio cannot change its options. Edit it in the config file.`,
|
|
2139
|
+
cause: toError(cause)
|
|
1992
2140
|
});
|
|
1993
|
-
};
|
|
2141
|
+
}))(mergeOptions(disk.options ?? {}, studioOptions));
|
|
1994
2142
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
lookup: createLookup(blocked),
|
|
2003
|
-
signal: signal ?? void 0,
|
|
2004
|
-
headers: { "accept-encoding": "identity" }
|
|
2005
|
-
}, (res) => {
|
|
2006
|
-
const headers = new Headers();
|
|
2007
|
-
for (const [key, value] of Object.entries(res.headers)) for (const item of [value].flat()) if (item !== void 0) headers.append(key, item);
|
|
2008
|
-
let total = 0;
|
|
2009
|
-
const limit = new Transform({ transform(chunk, _encoding, done) {
|
|
2010
|
-
total += chunk.length;
|
|
2011
|
-
done(total > MAX_BYTES ? /* @__PURE__ */ new Error("The response is larger than 50 MB") : null, chunk);
|
|
2012
|
-
} });
|
|
2013
|
-
res.on("error", (error) => limit.destroy(error));
|
|
2014
|
-
res.pipe(limit);
|
|
2015
|
-
const body = NO_BODY.has(res.statusCode ?? 0) ? null : Readable.toWeb(limit);
|
|
2016
|
-
resolve(new Response(body, {
|
|
2017
|
-
status: res.statusCode,
|
|
2018
|
-
statusText: res.statusMessage,
|
|
2019
|
-
headers
|
|
2020
|
-
}));
|
|
2021
|
-
});
|
|
2022
|
-
req.on("error", reject);
|
|
2023
|
-
req.end();
|
|
2024
|
-
});
|
|
2143
|
+
/** Merges Studio's valid `output` overrides, with `null` unsetting a member. */
|
|
2144
|
+
function mergeOutput({ output, studio }) {
|
|
2145
|
+
if (!isPlainObject(studio)) return output;
|
|
2146
|
+
return mergeOptions(output, Object.fromEntries(Object.entries(studio).filter(([key, value]) => value === null ? isOutputKey(key) : isOutputValue({
|
|
2147
|
+
key,
|
|
2148
|
+
value
|
|
2149
|
+
}))));
|
|
2025
2150
|
}
|
|
2026
2151
|
/**
|
|
2027
|
-
*
|
|
2028
|
-
* and checks every hop, and takes GET requests only, since that is all a spec load needs.
|
|
2152
|
+
* Merges Studio-provided adapter option overrides into the disk config's adapter, see {@link rebuildExtension}.
|
|
2029
2153
|
*/
|
|
2030
|
-
function
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
blocked,
|
|
2039
|
-
signal: init?.signal
|
|
2040
|
-
});
|
|
2041
|
-
const location = response.headers.get("location");
|
|
2042
|
-
if (!REDIRECTS.has(response.status) || !location) return response;
|
|
2043
|
-
await response.body?.cancel();
|
|
2044
|
-
url = new URL(location, url);
|
|
2045
|
-
}
|
|
2046
|
-
throw new Error(`Stopped after ${MAX_REDIRECTS} redirects from ${String(input)}`);
|
|
2047
|
-
};
|
|
2048
|
-
return publicFetch;
|
|
2154
|
+
async function mergeAdapter(diskAdapter, studioOptions, extensions = /* @__PURE__ */ new Map()) {
|
|
2155
|
+
if (!diskAdapter) return diskAdapter;
|
|
2156
|
+
return rebuildExtension({
|
|
2157
|
+
disk: diskAdapter,
|
|
2158
|
+
studioOptions,
|
|
2159
|
+
kind: "adapter",
|
|
2160
|
+
extensions
|
|
2161
|
+
});
|
|
2049
2162
|
}
|
|
2050
2163
|
/**
|
|
2051
|
-
*
|
|
2052
|
-
*
|
|
2164
|
+
* Merges Studio-provided parser option overrides into the disk config's parsers, matched by the name
|
|
2165
|
+
* Studio knows each parser by, see {@link rebuildExtension}. Parsers Studio does not name are kept as is.
|
|
2053
2166
|
*/
|
|
2054
|
-
async function
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2167
|
+
async function mergeParsers({ diskParsers, studioParsers, extensions = /* @__PURE__ */ new Map() }) {
|
|
2168
|
+
if (!studioParsers?.length) return diskParsers;
|
|
2169
|
+
const studioOptions = new Map(studioParsers.map((entry) => [entry.name, entry.options]));
|
|
2170
|
+
return Promise.all(diskParsers.map((parser) => rebuildExtension({
|
|
2171
|
+
disk: parser,
|
|
2172
|
+
studioOptions: studioOptions.get(toStudioName({
|
|
2173
|
+
name: resolveMeta(parser).name,
|
|
2174
|
+
extensions,
|
|
2175
|
+
kind: "parser"
|
|
2176
|
+
})),
|
|
2177
|
+
kind: "parser",
|
|
2178
|
+
extensions
|
|
2179
|
+
})));
|
|
2062
2180
|
}
|
|
2063
2181
|
//#endregion
|
|
2064
2182
|
//#region src/url.ts
|
|
@@ -2102,13 +2220,6 @@ const MANAGED_INPUT_KINDS = /* @__PURE__ */ new Set([
|
|
|
2102
2220
|
"url",
|
|
2103
2221
|
"git"
|
|
2104
2222
|
]);
|
|
2105
|
-
/** Extensions `kubb` ships with, so they count as installed without being a direct dependency. */
|
|
2106
|
-
const BUILT_IN_PACKAGES = [
|
|
2107
|
-
"@kubb/adapter-oas",
|
|
2108
|
-
"@kubb/parser-ts",
|
|
2109
|
-
"@kubb/parser-md",
|
|
2110
|
-
"@kubb/plugin-barrel"
|
|
2111
|
-
];
|
|
2112
2223
|
/**
|
|
2113
2224
|
* Studio closed with {@link AgentCloseCode.UNAUTHORIZED}. Hosts forget the token and pair again.
|
|
2114
2225
|
*/
|
|
@@ -2236,6 +2347,7 @@ var Agent = class extends RpcTarget {
|
|
|
2236
2347
|
allowConfigEdit: false,
|
|
2237
2348
|
allowInstall: false,
|
|
2238
2349
|
allowUpdate: false,
|
|
2350
|
+
allowRemove: false,
|
|
2239
2351
|
allowSaveInput: false,
|
|
2240
2352
|
allowRead: true,
|
|
2241
2353
|
allowReadAst: true
|
|
@@ -2301,13 +2413,14 @@ var Agent = class extends RpcTarget {
|
|
|
2301
2413
|
const imports = await source.then(readImports, () => []);
|
|
2302
2414
|
const installed = await readInstalledPackages({
|
|
2303
2415
|
root: this.#options.root,
|
|
2304
|
-
packages: [...imports, ...
|
|
2416
|
+
packages: [...imports, ...BUNDLED_PACKAGES],
|
|
2305
2417
|
resolveFrom: ["kubb"]
|
|
2306
2418
|
});
|
|
2307
2419
|
return new Map(installed.flatMap(({ package: name, version, manifest }) => version !== void 0 && manifest ? [[name, {
|
|
2308
2420
|
package: name,
|
|
2309
2421
|
version,
|
|
2310
|
-
manifest
|
|
2422
|
+
manifest,
|
|
2423
|
+
...BUNDLED_PACKAGES.includes(name) ? { bundled: true } : {}
|
|
2311
2424
|
}]] : []));
|
|
2312
2425
|
}
|
|
2313
2426
|
async connect(info) {
|
|
@@ -2546,7 +2659,7 @@ var Agent = class extends RpcTarget {
|
|
|
2546
2659
|
try {
|
|
2547
2660
|
const kept = [];
|
|
2548
2661
|
for (const edit of edits) kept.push(await this.#keepInput(edit));
|
|
2549
|
-
const written = await
|
|
2662
|
+
const written = await editConfigFile({
|
|
2550
2663
|
filePath: configFile,
|
|
2551
2664
|
edits: kept.flatMap((edit) => "reason" in edit ? [] : [edit]),
|
|
2552
2665
|
extensions: await this.#installedExtensions()
|
|
@@ -2683,10 +2796,49 @@ var Agent = class extends RpcTarget {
|
|
|
2683
2796
|
reason: output
|
|
2684
2797
|
} : { updated: true };
|
|
2685
2798
|
}
|
|
2799
|
+
/**
|
|
2800
|
+
* Removes an extension with `kubb remove <id> --yes`, which deletes it from `kubb.config.ts` and
|
|
2801
|
+
* uninstalls its package. Studio calls `connect()` again afterwards.
|
|
2802
|
+
*/
|
|
2803
|
+
async removeExtension(input) {
|
|
2804
|
+
this.#assertConnected();
|
|
2805
|
+
const { root, configFile, kubbBin = findKubbBin(root) } = this.#options;
|
|
2806
|
+
const refuse = async (reason, permission) => {
|
|
2807
|
+
await this.#warn({
|
|
2808
|
+
message: `Ignored remove: ${reason}`,
|
|
2809
|
+
permission
|
|
2810
|
+
});
|
|
2811
|
+
return {
|
|
2812
|
+
removed: false,
|
|
2813
|
+
reason
|
|
2814
|
+
};
|
|
2815
|
+
};
|
|
2816
|
+
if (typeof input?.id !== "string" || !REGISTRY_ID.test(input.id)) return refuse("the message carried no registry id");
|
|
2817
|
+
if (!this.#permissions.allowRemove) return refuse("removing extensions was not granted", AgentPermission.REMOVE);
|
|
2818
|
+
if (!this.#permissions.allowConfigEdit) return refuse("editing kubb.config.ts was not granted", AgentPermission.CONFIG_EDIT);
|
|
2819
|
+
const busy = this.#busy();
|
|
2820
|
+
if (busy) return refuse(busy);
|
|
2821
|
+
if (!kubbBin) return refuse("kubb is not installed in the project");
|
|
2822
|
+
const { failed, output } = await this.#runKubb({
|
|
2823
|
+
bin: kubbBin,
|
|
2824
|
+
args: [
|
|
2825
|
+
"remove",
|
|
2826
|
+
input.id,
|
|
2827
|
+
"--yes",
|
|
2828
|
+
"--config",
|
|
2829
|
+
configFile
|
|
2830
|
+
]
|
|
2831
|
+
});
|
|
2832
|
+
await this.#logCommand("removeExtension", `${failed ? "failed to remove" : "removed"} ${input.id}`, input.requestedBy, failed);
|
|
2833
|
+
return failed ? {
|
|
2834
|
+
removed: false,
|
|
2835
|
+
reason: output
|
|
2836
|
+
} : { removed: true };
|
|
2837
|
+
}
|
|
2686
2838
|
/** Why a command that touches the project cannot start right now. */
|
|
2687
2839
|
#busy() {
|
|
2688
2840
|
if (this.#session.active) return "a generation is in progress";
|
|
2689
|
-
if (this.#session.install) return "an install or
|
|
2841
|
+
if (this.#session.install) return "an install, update, or remove is in progress";
|
|
2690
2842
|
if (this.#session.saving) return "a save is in progress";
|
|
2691
2843
|
}
|
|
2692
2844
|
/** Runs `kubb` in the project while holding off saves and generations, and ends it when the socket closes. */
|
|
@@ -2973,6 +3125,7 @@ async function runAgent({ options, openRpc }) {
|
|
|
2973
3125
|
allowConfigEdit: false,
|
|
2974
3126
|
allowInstall: false,
|
|
2975
3127
|
allowUpdate: false,
|
|
3128
|
+
allowRemove: false,
|
|
2976
3129
|
allowSaveInput: false,
|
|
2977
3130
|
allowExec: false,
|
|
2978
3131
|
allowRead: false,
|
|
@@ -3434,6 +3587,6 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
3434
3587
|
return _data.snapshot;
|
|
3435
3588
|
}
|
|
3436
3589
|
//#endregion
|
|
3437
|
-
export { IncompatibleAgentError, InvalidAgentTokenError, PairingCanceledError, PairingDeniedError, PairingExpiredError,
|
|
3590
|
+
export { IncompatibleAgentError, InvalidAgentTokenError, PairingCanceledError, PairingDeniedError, PairingExpiredError, connectAgent, createSnapshot, defaultStudioUrl, editConfig, editConfigFile, pairAgent };
|
|
3438
3591
|
|
|
3439
3592
|
//# sourceMappingURL=index.js.map
|