@kubb/studio 5.6.2 → 5.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +456 -398
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +458 -400
- package/dist/index.js.map +1 -1
- package/dist/{protocol-P0znFp9w.d.ts → protocol-i7JV0P4z.d.ts} +7 -5
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -9,9 +9,9 @@ import process$1 from "node:process";
|
|
|
9
9
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
10
10
|
import { pathToFileURL } from "node:url";
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
|
-
import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, memoryStorage, resolveCacheDir } from "@kubb/core";
|
|
12
|
+
import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, getInputKind, memoryStorage, resolveCacheDir } from "@kubb/core";
|
|
13
13
|
import { RpcTarget, newWebSocketRpcSession } from "capnweb";
|
|
14
|
-
import {
|
|
14
|
+
import { detectCodeFormat, parseExpression, parseModule } from "magicast";
|
|
15
15
|
import { promisify, stripVTControlCharacters } from "node:util";
|
|
16
16
|
import { ofetch } from "ofetch";
|
|
17
17
|
import { gzip } from "node:zlib";
|
|
@@ -192,7 +192,7 @@ function getElapsedMs(hrStart) {
|
|
|
192
192
|
}
|
|
193
193
|
//#endregion
|
|
194
194
|
//#region package.json
|
|
195
|
-
var version = "5.6.
|
|
195
|
+
var version = "5.6.3";
|
|
196
196
|
//#endregion
|
|
197
197
|
//#region src/resolveConfig.ts
|
|
198
198
|
/**
|
|
@@ -237,6 +237,46 @@ function toPluginName(packageName) {
|
|
|
237
237
|
function toPackageName(name) {
|
|
238
238
|
return name.startsWith("plugin-") ? `@kubb/${name}` : name;
|
|
239
239
|
}
|
|
240
|
+
function resolvesFrom(base, packageName) {
|
|
241
|
+
try {
|
|
242
|
+
createRequire(base).resolve(`${packageName}/package.json`);
|
|
243
|
+
return true;
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Whether `packageName` resolves from the project at `root`, from this package, or from the `kubb`
|
|
250
|
+
* package the project installs, which brings the plugins Kubb adds on its own such as
|
|
251
|
+
* `@kubb/plugin-barrel`. Only resolves the package, it never runs it.
|
|
252
|
+
*/
|
|
253
|
+
function isInstalled({ packageName, root }) {
|
|
254
|
+
const project = pathToFileURL(`${root}/`);
|
|
255
|
+
if (resolvesFrom(project, packageName) || resolvesFrom(import.meta.url, packageName)) return true;
|
|
256
|
+
try {
|
|
257
|
+
return resolvesFrom(createRequire(project).resolve("kubb/package.json"), packageName);
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The name Studio knows a plugin by: its `@kubb/plugin-*` package when one is installed, otherwise
|
|
264
|
+
* the plugin's own `name`, so a plugin defined in the user's project is never reported as a Kubb
|
|
265
|
+
* package that does not exist.
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```ts
|
|
269
|
+
* toStudioName({ name: 'plugin-ts', root }) // '@kubb/plugin-ts'
|
|
270
|
+
* toStudioName({ name: 'plugin-zod-operations', root }) // 'plugin-zod-operations', a local plugin
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
function toStudioName({ name, root }) {
|
|
274
|
+
const packageName = toPackageName(name);
|
|
275
|
+
return packageName !== name && isInstalled({
|
|
276
|
+
packageName,
|
|
277
|
+
root
|
|
278
|
+
}) ? packageName : name;
|
|
279
|
+
}
|
|
240
280
|
/**
|
|
241
281
|
* Derives the conventional named export for a `@kubb/*` plugin package from its package name.
|
|
242
282
|
*
|
|
@@ -332,6 +372,38 @@ async function resolvePeerDependencies(names) {
|
|
|
332
372
|
};
|
|
333
373
|
}
|
|
334
374
|
/**
|
|
375
|
+
* Keys that reach an object's prototype when assigned. Never read from or written into options.
|
|
376
|
+
*/
|
|
377
|
+
const UNSAFE_KEYS = /* @__PURE__ */ new Set([
|
|
378
|
+
"__proto__",
|
|
379
|
+
"constructor",
|
|
380
|
+
"prototype"
|
|
381
|
+
]);
|
|
382
|
+
/**
|
|
383
|
+
* Whether `key` can be used as an option key without touching an object's prototype.
|
|
384
|
+
*/
|
|
385
|
+
function isSafeKey(key) {
|
|
386
|
+
return !UNSAFE_KEYS.has(key);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* A JSON copy of plugin or adapter options: functions and other values JSON cannot hold are left
|
|
390
|
+
* out, so nothing callable crosses the socket. Options that cannot be copied at all, such as a
|
|
391
|
+
* circular structure, come back empty.
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* ```ts
|
|
395
|
+
* toJSONOptions({ output: { path: 'types' }, group: { name: () => 'x' } }) // { output: { path: 'types' }, group: {} }
|
|
396
|
+
* ```
|
|
397
|
+
*/
|
|
398
|
+
function toJSONOptions(options) {
|
|
399
|
+
try {
|
|
400
|
+
const copy = JSON.parse(JSON.stringify(options ?? {}, (key, value) => isSafeKey(key) && typeof value !== "bigint" ? value : void 0));
|
|
401
|
+
return isPlainObject(copy) ? copy : {};
|
|
402
|
+
} catch {
|
|
403
|
+
return {};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
335
407
|
* Whether `value` is an object literal or a null-prototype object, not an array or class instance.
|
|
336
408
|
*/
|
|
337
409
|
function isPlainObject(value) {
|
|
@@ -348,7 +420,7 @@ function isOptionValue(value) {
|
|
|
348
420
|
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
349
421
|
if (typeof value === "number") return Number.isFinite(value);
|
|
350
422
|
if (Array.isArray(value)) return value.every(isOptionValue);
|
|
351
|
-
if (isPlainObject(value)) return Object.
|
|
423
|
+
if (isPlainObject(value)) return Object.entries(value).every(([key, entry]) => isSafeKey(key) && isOptionValue(entry));
|
|
352
424
|
return false;
|
|
353
425
|
}
|
|
354
426
|
/**
|
|
@@ -366,6 +438,7 @@ function isOptionValue(value) {
|
|
|
366
438
|
function mergeOptions(disk, studio) {
|
|
367
439
|
const merged = { ...disk };
|
|
368
440
|
for (const [key, value] of Object.entries(studio)) {
|
|
441
|
+
if (!isSafeKey(key)) continue;
|
|
369
442
|
const current = disk[key];
|
|
370
443
|
if (isPlainObject(current) && isPlainObject(value)) {
|
|
371
444
|
merged[key] = mergeOptions(current, value);
|
|
@@ -398,11 +471,16 @@ async function mergePlugins(diskPlugins, studioPlugins) {
|
|
|
398
471
|
const merged = await Promise.all(activeDiskPlugins.map(async (diskPlugin) => {
|
|
399
472
|
const studioEntry = studioEntryByName.get(diskPlugin.name);
|
|
400
473
|
if (!studioEntry) return diskPlugin;
|
|
401
|
-
const
|
|
474
|
+
const diskOptions = diskPlugin.options ?? {};
|
|
475
|
+
const options = mergeOptions(diskOptions, studioEntry.options ?? {});
|
|
476
|
+
const unchanged = JSON.stringify(toJSONOptions(options)) === JSON.stringify(toJSONOptions(diskOptions));
|
|
402
477
|
const [resolved] = await resolvePlugins([{
|
|
403
478
|
name: studioEntry.name,
|
|
404
479
|
options
|
|
405
|
-
}])
|
|
480
|
+
}]).catch((cause) => {
|
|
481
|
+
if (unchanged) return [diskPlugin];
|
|
482
|
+
throw new Error(`Plugin "${diskPlugin.name}" is not an installed @kubb/plugin-* package, so Studio cannot change its options. Edit it in the config file, or only enable or disable it from Studio.`, { cause });
|
|
483
|
+
});
|
|
406
484
|
return resolved ?? diskPlugin;
|
|
407
485
|
}));
|
|
408
486
|
const studioOnly = activeStudioPlugins.filter((entry) => !diskNames.has(toPluginName(entry.name)));
|
|
@@ -432,142 +510,130 @@ async function mergeAdapter(diskAdapter, studioOptions) {
|
|
|
432
510
|
*/
|
|
433
511
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
434
512
|
/**
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
*/
|
|
440
|
-
|
|
441
|
-
|
|
513
|
+
* The marker line a `disable-plugin` writes above the block it comments out. It carries the block's
|
|
514
|
+
* line count, so `enable-plugin` restores exactly those lines and not whatever comments follow.
|
|
515
|
+
*/
|
|
516
|
+
const DISABLED_MARKER = /^\/\/ kubb:disabled (.+)\s+(\d+)$/;
|
|
517
|
+
const OPERATIONS = /* @__PURE__ */ new Set([
|
|
518
|
+
"set",
|
|
519
|
+
"remove",
|
|
520
|
+
"add-plugin",
|
|
521
|
+
"disable-plugin",
|
|
522
|
+
"enable-plugin"
|
|
523
|
+
]);
|
|
524
|
+
/**
|
|
525
|
+
* Why an edit from Studio cannot be applied as sent, checked before anything reads its fields.
|
|
526
|
+
* The edit crossed the wire, so its declared type is a promise the sender may not have kept.
|
|
527
|
+
*/
|
|
528
|
+
function invalidEdit(edit) {
|
|
529
|
+
const { operation, plugin, config, path, importName } = typeof edit === "object" && edit !== null ? edit : {};
|
|
530
|
+
if (typeof operation !== "string" || !OPERATIONS.has(operation)) return `unknown operation ${JSON.stringify(operation)}`;
|
|
531
|
+
if (typeof plugin !== "string" || !plugin) return "the edit names no plugin";
|
|
532
|
+
if (config !== void 0 && typeof config !== "string" && !Number.isSafeInteger(config)) return "config must name an entry or give its index";
|
|
533
|
+
if (operation === "set" || operation === "remove") {
|
|
534
|
+
if (!Array.isArray(path) || !path.every((key) => typeof key === "string" && key)) return "path must be a list of option names";
|
|
535
|
+
if (!path.length) return "no option path given";
|
|
536
|
+
const unsafe = path.find((key) => !isSafeKey(key));
|
|
537
|
+
if (unsafe !== void 0) return `${unsafe} cannot be used as an option name`;
|
|
538
|
+
}
|
|
539
|
+
if (operation === "add-plugin" && importName !== void 0 && typeof importName !== "string") return "importName must be a string";
|
|
540
|
+
}
|
|
541
|
+
/** Where a node sits in the source text. */
|
|
542
|
+
function rangeOf(node) {
|
|
543
|
+
if (typeof node.start !== "number" || typeof node.end !== "number") throw new Error("the config file could not be mapped back to its source text");
|
|
442
544
|
return {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
value: built.value,
|
|
446
|
-
computed: false,
|
|
447
|
-
shorthand: false
|
|
545
|
+
start: node.start,
|
|
546
|
+
end: node.end
|
|
448
547
|
};
|
|
449
548
|
}
|
|
450
|
-
/**
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* The plugin and comment-block length a marker line names, when `line` is one.
|
|
458
|
-
*/
|
|
459
|
-
function parseMarker(line) {
|
|
460
|
-
const trimmed = line.trim();
|
|
461
|
-
if (!trimmed.startsWith(`// ${DISABLED_MARKER} `)) return;
|
|
462
|
-
const match = trimmed.slice(`// ${DISABLED_MARKER} `.length).match(/^(.+)\s+(\d+)$/);
|
|
463
|
-
return match ? {
|
|
464
|
-
plugin: match[1],
|
|
465
|
-
lineCount: Number(match[2])
|
|
466
|
-
} : void 0;
|
|
549
|
+
/** `source` with the text between `start` and `end` replaced by `text`. */
|
|
550
|
+
function splice(source, start, end, text = "") {
|
|
551
|
+
return `${source.slice(0, start)}${text}${source.slice(end)}`;
|
|
552
|
+
}
|
|
553
|
+
function bodyOf(mod) {
|
|
554
|
+
return mod.$ast.type === "Program" ? mod.$ast.body : [];
|
|
467
555
|
}
|
|
468
556
|
/**
|
|
469
557
|
* Steps through a config's wrappers to the object literal underneath: a `satisfies`/`as`
|
|
470
558
|
* assertion, a `() => ...` factory, or a factory whose block body returns the config.
|
|
471
559
|
*/
|
|
472
560
|
function unwrap(node) {
|
|
473
|
-
if (!node) return;
|
|
561
|
+
if (!node) return void 0;
|
|
474
562
|
if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression") return unwrap(node.expression);
|
|
475
563
|
if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") return node;
|
|
476
564
|
if (node.body.type !== "BlockStatement") return unwrap(node.body);
|
|
477
565
|
return unwrap(node.body.body.find((statement) => statement.type === "ReturnStatement")?.argument);
|
|
478
566
|
}
|
|
567
|
+
function isModuleExports(node) {
|
|
568
|
+
return node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier" && node.object.name === "module" && node.property.type === "Identifier" && node.property.name === "exports";
|
|
569
|
+
}
|
|
479
570
|
/**
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
571
|
+
* Parses `source` and finds every config object in `export default defineConfig(...)` or
|
|
572
|
+
* `module.exports = defineConfig(...)`, or why the file is unmanaged. An array export gets one entry
|
|
573
|
+
* per element, matching a numeric `config` index.
|
|
483
574
|
*
|
|
484
575
|
* Walks the parsed AST rather than magicast's proxies, which throw on node types they cannot
|
|
485
576
|
* cast, most of what an unmanaged config file is made of.
|
|
486
577
|
*/
|
|
487
|
-
function
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
578
|
+
function parseConfigs(source) {
|
|
579
|
+
let mod;
|
|
580
|
+
try {
|
|
581
|
+
mod = parseModule(source);
|
|
582
|
+
} catch {
|
|
583
|
+
return { reason: "the config file could not be parsed" };
|
|
584
|
+
}
|
|
585
|
+
let exported;
|
|
586
|
+
let kind = "esm";
|
|
587
|
+
for (const node of bodyOf(mod)) {
|
|
588
|
+
if (node.type === "ExportDefaultDeclaration") {
|
|
589
|
+
exported = node.declaration;
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
if (node.type === "ExpressionStatement" && node.expression.type === "AssignmentExpression" && isModuleExports(node.expression.left)) {
|
|
593
|
+
exported = node.expression.right;
|
|
594
|
+
kind = "cjs";
|
|
595
|
+
break;
|
|
499
596
|
}
|
|
500
|
-
return { configs };
|
|
501
597
|
}
|
|
502
|
-
|
|
503
|
-
return {
|
|
598
|
+
const value = unwrap(exported);
|
|
599
|
+
if (!value) return { reason: "no `export default` or `module.exports` found" };
|
|
600
|
+
if (value.type !== "CallExpression" || value.callee.type !== "Identifier" || value.callee.name !== "defineConfig") return { reason: "the exported config is not a defineConfig(...) call" };
|
|
601
|
+
const argument = unwrap(value.arguments[0]);
|
|
602
|
+
if (!argument) return { reason: "defineConfig(...) was called without a config" };
|
|
603
|
+
const entries = argument.type === "ArrayExpression" ? argument.elements.map((element) => unwrap(element)) : [argument];
|
|
604
|
+
const configs = entries.filter((entry) => entry?.type === "ObjectExpression");
|
|
605
|
+
if (configs.length !== entries.length) return { reason: "config is not an object literal" };
|
|
606
|
+
return {
|
|
607
|
+
mod,
|
|
608
|
+
configs,
|
|
609
|
+
kind
|
|
610
|
+
};
|
|
504
611
|
}
|
|
505
|
-
/**
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
612
|
+
/** The name of a `key` or `'key'` member, `undefined` for a spread or a computed key. */
|
|
613
|
+
function memberKey(entry) {
|
|
614
|
+
if (entry.type === "SpreadElement" || entry.computed) return void 0;
|
|
615
|
+
return entry.key.type === "Identifier" ? entry.key.name : entry.key.type === "StringLiteral" ? entry.key.value : void 0;
|
|
616
|
+
}
|
|
617
|
+
/** The member named `key`. The last one wins when the key repeats, as it does when the object is evaluated. */
|
|
618
|
+
function findMember(node, key) {
|
|
619
|
+
const index = node.properties.findLastIndex((entry) => memberKey(entry) === key);
|
|
620
|
+
return index === -1 ? void 0 : {
|
|
621
|
+
member: node.properties[index],
|
|
622
|
+
index
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
/** The value node of a member. A method or accessor is its own value: it is code, never a literal. */
|
|
626
|
+
function property(node, key) {
|
|
627
|
+
const member = findMember(node, key)?.member;
|
|
628
|
+
return member?.type === "ObjectProperty" ? member.value : member;
|
|
512
629
|
}
|
|
513
630
|
function configName(config) {
|
|
514
631
|
const name = property(config, "name");
|
|
515
632
|
return name?.type === "StringLiteral" ? name.value : void 0;
|
|
516
633
|
}
|
|
517
|
-
/**
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
*/
|
|
521
|
-
function propertyKey(entry) {
|
|
522
|
-
if (entry.key.type === "Identifier") return entry.key.name;
|
|
523
|
-
if (entry.key.type === "StringLiteral") return entry.key.value;
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* The index of an object literal's own property named `key`, `-1` when it has none.
|
|
527
|
-
*/
|
|
528
|
-
function entryIndex({ node, key }) {
|
|
529
|
-
return node.properties.findIndex((entry) => entry.type === "ObjectProperty" && propertyKey(entry) === key);
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* The value node of an object literal's own property.
|
|
533
|
-
*/
|
|
534
|
-
function property(node, key) {
|
|
535
|
-
const index = entryIndex({
|
|
536
|
-
node,
|
|
537
|
-
key
|
|
538
|
-
});
|
|
539
|
-
return index === -1 ? void 0 : node.properties[index].value;
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Writes `key: value` on an object literal, replacing the value when the property is already there.
|
|
543
|
-
*
|
|
544
|
-
* An existing property has its value swapped in place rather than being replaced whole, so recast
|
|
545
|
-
* reprints only that value and leaves the object's own layout alone.
|
|
546
|
-
*/
|
|
547
|
-
function setProperty({ node, key, value }) {
|
|
548
|
-
const entry = literalProperty({
|
|
549
|
-
key,
|
|
550
|
-
value
|
|
551
|
-
});
|
|
552
|
-
const index = entryIndex({
|
|
553
|
-
node,
|
|
554
|
-
key
|
|
555
|
-
});
|
|
556
|
-
if (index === -1) {
|
|
557
|
-
node.properties.push(entry);
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
node.properties[index].value = entry.value;
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* Drops `key` from an object literal.
|
|
564
|
-
*/
|
|
565
|
-
function removeProperty({ node, key }) {
|
|
566
|
-
const index = entryIndex({
|
|
567
|
-
node,
|
|
568
|
-
key
|
|
569
|
-
});
|
|
570
|
-
if (index !== -1) node.properties.splice(index, 1);
|
|
634
|
+
/** Whether a `...spread` after the member at `index` could override it. */
|
|
635
|
+
function isShadowed(node, index) {
|
|
636
|
+
return node.properties.slice(index + 1).some((entry) => entry.type === "SpreadElement");
|
|
571
637
|
}
|
|
572
638
|
/**
|
|
573
639
|
* Reads a literal node's value: a primitive, or an object/array built only from primitives.
|
|
@@ -575,42 +641,109 @@ function removeProperty({ node, key }) {
|
|
|
575
641
|
* whether a node is a literal at all.
|
|
576
642
|
*/
|
|
577
643
|
function readLiteral(node) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
644
|
+
switch (node?.type) {
|
|
645
|
+
case "StringLiteral":
|
|
646
|
+
case "NumericLiteral":
|
|
647
|
+
case "BooleanLiteral": return node.value;
|
|
648
|
+
case "NullLiteral": return null;
|
|
649
|
+
case "TemplateLiteral": return node.expressions.length ? void 0 : node.quasis[0]?.value.cooked ?? "";
|
|
650
|
+
case "UnaryExpression": {
|
|
651
|
+
const value = readLiteral(node.argument);
|
|
652
|
+
if (typeof value !== "number") return void 0;
|
|
653
|
+
return node.operator === "-" ? -value : node.operator === "+" ? value : void 0;
|
|
654
|
+
}
|
|
655
|
+
case "ArrayExpression": {
|
|
656
|
+
const values = node.elements.map((element) => readLiteral(element));
|
|
657
|
+
return values.every((value) => value !== void 0) ? values : void 0;
|
|
658
|
+
}
|
|
659
|
+
case "ObjectExpression": {
|
|
660
|
+
const entries = {};
|
|
661
|
+
for (const entry of node.properties) {
|
|
662
|
+
if (entry.type !== "ObjectProperty") return void 0;
|
|
663
|
+
const key = memberKey(entry);
|
|
664
|
+
const value = readLiteral(entry.value);
|
|
665
|
+
if (key === void 0 || !isSafeKey(key) || value === void 0) return void 0;
|
|
666
|
+
entries[key] = value;
|
|
667
|
+
}
|
|
668
|
+
return entries;
|
|
601
669
|
}
|
|
602
|
-
return entries;
|
|
603
670
|
}
|
|
604
671
|
}
|
|
605
|
-
/**
|
|
606
|
-
|
|
607
|
-
|
|
672
|
+
/** Writes `value` as a single-line literal with the file's quote style. */
|
|
673
|
+
function printValue(value, quote) {
|
|
674
|
+
if (typeof value === "string") {
|
|
675
|
+
const escaped = JSON.stringify(value).slice(1, -1);
|
|
676
|
+
return quote === `"` ? `"${escaped}"` : `'${escaped.replaceAll(`\\"`, `"`).replaceAll(`'`, `\\'`)}'`;
|
|
677
|
+
}
|
|
678
|
+
if (value === null || typeof value !== "object") return String(value);
|
|
679
|
+
if (Array.isArray(value)) return `[${value.map((item) => printValue(item, quote)).join(", ")}]`;
|
|
680
|
+
const entries = Object.entries(value);
|
|
681
|
+
return entries.length ? `{ ${entries.map(([key, item]) => `${printKey(key, quote)}: ${printValue(item, quote)}`).join(", ")} }` : "{}";
|
|
682
|
+
}
|
|
683
|
+
/** An object key as source text: bare when it is an identifier, quoted otherwise. */
|
|
684
|
+
function printKey(key, quote) {
|
|
685
|
+
return IDENTIFIER.test(key) ? key : printValue(key, quote);
|
|
686
|
+
}
|
|
687
|
+
/** `value` wrapped in one object per key of `path`: `nest(['enum', 'type'], 'enum')` is `{ enum: { type: 'enum' } }`. */
|
|
688
|
+
function nest(path, value) {
|
|
689
|
+
return path.reduceRight((inner, key) => ({ [key]: inner }), value);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Adds `text` as the last item of an object or array literal, following its layout: on a line of
|
|
693
|
+
* its own with the last item's indentation when the literal spans lines, inline otherwise.
|
|
694
|
+
*/
|
|
695
|
+
function insertItem(source, container, text) {
|
|
696
|
+
const { start, end } = rangeOf(container);
|
|
697
|
+
const last = (container.type === "ObjectExpression" ? container.properties : container.elements).filter((item) => item !== null).at(-1);
|
|
698
|
+
if (!last) {
|
|
699
|
+
const inner = source.slice(start + 1, end - 1);
|
|
700
|
+
if (inner.trim()) return splice(source, start + 1, start + 1, /^\s/.test(inner) ? `${text},` : `${text}, `);
|
|
701
|
+
return splice(source, start, end, container.type === "ObjectExpression" ? `{ ${text} }` : `[${text}]`);
|
|
702
|
+
}
|
|
703
|
+
const lastRange = rangeOf(last);
|
|
704
|
+
const between = source.slice(lastRange.end, end - 1);
|
|
705
|
+
if (!between.includes("\n")) return splice(source, lastRange.end, lastRange.end, `, ${text}`);
|
|
706
|
+
const lineStart = source.lastIndexOf("\n", lastRange.start - 1) + 1;
|
|
707
|
+
const indent = source.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
|
|
708
|
+
const lineEnd = source.indexOf("\n", lastRange.end);
|
|
709
|
+
const comma = /^\s*,/.test(between);
|
|
710
|
+
return `${source.slice(0, lastRange.end)}${comma ? "" : ","}${source.slice(lastRange.end, lineEnd)}\n${indent}${text}${comma ? "," : ""}${source.slice(lineEnd)}`;
|
|
711
|
+
}
|
|
712
|
+
/** Drops the member at `index` from an object literal, with its own lines when it sits on them alone. */
|
|
713
|
+
function removeMember(source, node, index) {
|
|
714
|
+
if (node.properties.length === 1) return splice(source, rangeOf(node).start, rangeOf(node).end, "{}");
|
|
715
|
+
const { start, end } = rangeOf(node.properties[index]);
|
|
716
|
+
const lineStart = source.lastIndexOf("\n", start - 1) + 1;
|
|
717
|
+
const nextNewline = source.indexOf("\n", end);
|
|
718
|
+
const lineEnd = nextNewline === -1 ? source.length : nextNewline;
|
|
719
|
+
if (!source.slice(lineStart, start).trim() && /^\s*,?\s*(\/\/.*)?$/.test(source.slice(end, lineEnd))) return splice(source, lineStart, Math.min(lineEnd + 1, source.length));
|
|
720
|
+
const next = node.properties[index + 1];
|
|
721
|
+
return next ? splice(source, start, rangeOf(next).start) : splice(source, rangeOf(node.properties[index - 1]).end, end);
|
|
722
|
+
}
|
|
723
|
+
/** The module a `require('...')` call loads, when `node` is one. */
|
|
724
|
+
function requiredModule(node) {
|
|
725
|
+
if (node?.type !== "CallExpression" || node.callee.type !== "Identifier" || node.callee.name !== "require") return void 0;
|
|
726
|
+
const [argument] = node.arguments;
|
|
727
|
+
return argument?.type === "StringLiteral" ? argument.value : void 0;
|
|
728
|
+
}
|
|
729
|
+
/** Maps each identifier the file imports, through `import { x } from` or `const { x } = require(...)`, to its module. */
|
|
608
730
|
function importedFrom(mod) {
|
|
609
|
-
|
|
731
|
+
const imports = /* @__PURE__ */ new Map();
|
|
732
|
+
for (const statement of bodyOf(mod)) {
|
|
733
|
+
if (statement.type === "ImportDeclaration") {
|
|
734
|
+
for (const specifier of statement.specifiers) imports.set(specifier.local.name, statement.source.value);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (statement.type !== "VariableDeclaration") continue;
|
|
738
|
+
for (const declarator of statement.declarations) {
|
|
739
|
+
const from = requiredModule(declarator.init);
|
|
740
|
+
if (from === void 0 || declarator.id.type !== "ObjectPattern") continue;
|
|
741
|
+
for (const entry of declarator.id.properties) if (entry.type === "ObjectProperty" && entry.value.type === "Identifier") imports.set(entry.value.name, from);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return imports;
|
|
610
745
|
}
|
|
611
|
-
/**
|
|
612
|
-
* Every `pluginX(...)` element of a config's plugins array that resolves to an import.
|
|
613
|
-
*/
|
|
746
|
+
/** Every `pluginX(...)` element of a config's plugins array that resolves to an import. */
|
|
614
747
|
function pluginCalls(mod, config) {
|
|
615
748
|
const plugins = property(config, "plugins");
|
|
616
749
|
if (plugins?.type !== "ArrayExpression") return [];
|
|
@@ -626,19 +759,51 @@ function pluginCalls(mod, config) {
|
|
|
626
759
|
});
|
|
627
760
|
}
|
|
628
761
|
/**
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
* Read from the marker lines rather than the AST, since a commented-out call is no longer a node.
|
|
762
|
+
* The blocks a previous `disable-plugin` commented out of this config, read from the marker lines
|
|
763
|
+
* since a commented-out call is no longer a node. `code` holds the block's lines as they were.
|
|
632
764
|
*/
|
|
633
|
-
function
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
765
|
+
function disabledBlocks(source, config) {
|
|
766
|
+
const lines = source.split("\n");
|
|
767
|
+
const first = config.loc?.start.line ?? 0;
|
|
768
|
+
const last = config.loc?.end.line ?? Number.POSITIVE_INFINITY;
|
|
769
|
+
return lines.flatMap((text, index) => {
|
|
770
|
+
const match = text.trim().match(DISABLED_MARKER);
|
|
771
|
+
if (!match || index + 1 < first || index + 1 > last) return [];
|
|
772
|
+
const end = index + 1 + Number(match[2]);
|
|
773
|
+
const code = lines.slice(index + 1, end).map((line) => line.replace(/^(\s*)\/\/ ?/, "$1").replace(/^\s+$/, ""));
|
|
774
|
+
return [{
|
|
775
|
+
packageName: match[1],
|
|
776
|
+
line: index + 1,
|
|
777
|
+
end,
|
|
778
|
+
code
|
|
779
|
+
}];
|
|
640
780
|
});
|
|
641
781
|
}
|
|
782
|
+
/** The top-level options of a plugin call, each flagged with whether Studio may write it. */
|
|
783
|
+
function readOptions(call) {
|
|
784
|
+
const entries = {};
|
|
785
|
+
const options = call.arguments[0];
|
|
786
|
+
if (options?.type !== "ObjectExpression") return entries;
|
|
787
|
+
for (const [index, entry] of options.properties.entries()) {
|
|
788
|
+
const key = memberKey(entry);
|
|
789
|
+
if (key === void 0 || !isSafeKey(key)) continue;
|
|
790
|
+
const value = entry.type === "ObjectProperty" && !isShadowed(options, index) ? readLiteral(entry.value) : void 0;
|
|
791
|
+
entries[key] = value === void 0 ? { literal: false } : {
|
|
792
|
+
literal: true,
|
|
793
|
+
value
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
return entries;
|
|
797
|
+
}
|
|
798
|
+
/** The options a commented-out plugin call holds, so Studio can show them while it is disabled. */
|
|
799
|
+
function readDisabledOptions(code) {
|
|
800
|
+
try {
|
|
801
|
+
const call = parseExpression(code.join("\n").trim().replace(/,$/, "")).$ast;
|
|
802
|
+
return call.type === "CallExpression" ? readOptions(call) : {};
|
|
803
|
+
} catch {
|
|
804
|
+
return {};
|
|
805
|
+
}
|
|
806
|
+
}
|
|
642
807
|
/**
|
|
643
808
|
* Reads which plugins the file declares and which of their options Studio may write.
|
|
644
809
|
*
|
|
@@ -658,37 +823,27 @@ function readConfig(source) {
|
|
|
658
823
|
};
|
|
659
824
|
const { mod, configs } = parsed;
|
|
660
825
|
const importNames = new Map([...importedFrom(mod)].map(([local, from]) => [from, local]));
|
|
661
|
-
const disabled = disabledMarkers(source);
|
|
662
826
|
return {
|
|
663
827
|
managed: true,
|
|
664
828
|
configs: configs.map((config) => {
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if (options?.type === "ObjectExpression") for (const entry of options.properties) {
|
|
669
|
-
if (entry.type !== "ObjectProperty") continue;
|
|
670
|
-
const key = propertyKey(entry);
|
|
671
|
-
if (key === void 0) continue;
|
|
672
|
-
const value = readLiteral(entry.value);
|
|
673
|
-
entries[key] = value === void 0 ? { literal: false } : {
|
|
674
|
-
literal: true,
|
|
675
|
-
value
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
return {
|
|
829
|
+
const active = pluginCalls(mod, config).map(({ importName, packageName, call }) => ({
|
|
830
|
+
line: call.loc?.start.line ?? 0,
|
|
831
|
+
plugin: {
|
|
679
832
|
importName,
|
|
680
833
|
packageName,
|
|
681
|
-
options:
|
|
682
|
-
}
|
|
683
|
-
});
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
834
|
+
options: readOptions(call)
|
|
835
|
+
}
|
|
836
|
+
}));
|
|
837
|
+
const disabled = disabledBlocks(source, config).map(({ packageName, line, code }) => ({
|
|
838
|
+
line,
|
|
839
|
+
plugin: {
|
|
840
|
+
importName: importNames.get(packageName) ?? toExportName(packageName),
|
|
841
|
+
packageName,
|
|
842
|
+
options: readDisabledOptions(code),
|
|
843
|
+
disabled: true
|
|
844
|
+
}
|
|
845
|
+
}));
|
|
846
|
+
const plugins = [...active, ...disabled].sort((a, b) => a.line - b.line).map(({ plugin }) => plugin);
|
|
692
847
|
return {
|
|
693
848
|
name: configName(config),
|
|
694
849
|
plugins
|
|
@@ -697,173 +852,131 @@ function readConfig(source) {
|
|
|
697
852
|
};
|
|
698
853
|
}
|
|
699
854
|
/**
|
|
700
|
-
*
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
return options?.type === "ObjectExpression" ? options : void 0;
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* The options object of a plugin call, creating an empty one when the plugin was called bare.
|
|
708
|
-
*/
|
|
709
|
-
function ensureOptions(call) {
|
|
710
|
-
if (call.arguments.length === 0) call.arguments.push({
|
|
711
|
-
type: "ObjectExpression",
|
|
712
|
-
properties: []
|
|
713
|
-
});
|
|
714
|
-
return getOptions(call);
|
|
715
|
-
}
|
|
716
|
-
/**
|
|
717
|
-
* Walks `path` down to the object holding its last key, descending only through object literals.
|
|
855
|
+
* Walks `path` through a plugin's options to the literal at its last key. Returns `missing` with
|
|
856
|
+
* the index of the first key that is not there yet, so `set` can write the rest of the path as one
|
|
857
|
+
* nested literal and `remove` can report it as unset. Refuses a value customized in code, so it is
|
|
858
|
+
* never overwritten.
|
|
718
859
|
*/
|
|
719
|
-
function
|
|
860
|
+
function walk(options, path) {
|
|
720
861
|
let object = options;
|
|
721
|
-
for (const [
|
|
722
|
-
|
|
862
|
+
for (const [depth, key] of path.entries()) {
|
|
863
|
+
const found = findMember(object, key);
|
|
864
|
+
if (!found) return {
|
|
723
865
|
object,
|
|
724
|
-
|
|
866
|
+
missing: depth
|
|
725
867
|
};
|
|
726
|
-
if (
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
object
|
|
868
|
+
if (isShadowed(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` };
|
|
869
|
+
const { member, index } = found;
|
|
870
|
+
if (depth === path.length - 1) return member.type === "ObjectProperty" && readLiteral(member.value) !== void 0 ? {
|
|
871
|
+
object,
|
|
872
|
+
index,
|
|
873
|
+
value: member.value
|
|
874
|
+
} : { reason: `${path.join(".")} is customized in code` };
|
|
875
|
+
if (member.type !== "ObjectProperty" || member.value.type !== "ObjectExpression") return { reason: `${key} is not an object, so ${path.join(".")} cannot be reached` };
|
|
876
|
+
object = member.value;
|
|
734
877
|
}
|
|
735
878
|
return { reason: "no option path given" };
|
|
736
879
|
}
|
|
737
|
-
/**
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
880
|
+
/** Writes `value` at `path` inside a plugin call's options, adding the options object and any intermediate object it needs. */
|
|
881
|
+
function applySet(source, call, path, value, quote) {
|
|
882
|
+
if (!isOptionValue(value)) return { reason: "the value is not a literal that can be written to a config file" };
|
|
883
|
+
const options = call.arguments[0];
|
|
884
|
+
if (!options) {
|
|
885
|
+
const closing = rangeOf(call).end - 1;
|
|
886
|
+
if (source[closing] !== ")") return { reason: "the plugin call could not be located in the file" };
|
|
887
|
+
return { source: splice(source, closing, closing, printValue(nest(path, value), quote)) };
|
|
888
|
+
}
|
|
889
|
+
if (options.type !== "ObjectExpression") return { reason: "the plugin was not called with an object literal" };
|
|
890
|
+
const target = walk(options, path);
|
|
891
|
+
if ("reason" in target) return target;
|
|
892
|
+
if ("missing" in target) {
|
|
893
|
+
const [key, ...rest] = path.slice(target.missing);
|
|
894
|
+
return { source: insertItem(source, target.object, `${printKey(key, quote)}: ${printValue(nest(rest, value), quote)}`) };
|
|
895
|
+
}
|
|
896
|
+
return { source: splice(source, rangeOf(target.value).start, rangeOf(target.value).end, printValue(value, quote)) };
|
|
897
|
+
}
|
|
898
|
+
/** Deletes the option at `path`, falling the plugin back to its default for it. */
|
|
899
|
+
function applyRemove(source, call, path) {
|
|
900
|
+
const options = call.arguments[0];
|
|
901
|
+
if (options?.type !== "ObjectExpression") return { reason: "the plugin has no options to remove" };
|
|
902
|
+
const target = walk(options, path);
|
|
903
|
+
if ("reason" in target) return target;
|
|
904
|
+
if ("missing" in target) return { reason: `${path.join(".")} is not set` };
|
|
905
|
+
return { source: removeMember(source, target.object, target.index) };
|
|
755
906
|
}
|
|
756
907
|
/**
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
* `
|
|
908
|
+
* Writes an import after the last one already in the file, matching its quote style and whether it
|
|
909
|
+
* ends in a semicolon: `import { x } from '...'`, or `const { x } = require('...')` in a CommonJS file.
|
|
910
|
+
* The last import is read off the AST, so a multi-line `import {\n x,\n} from '...'` counts to its closing line.
|
|
760
911
|
*/
|
|
761
|
-
function
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
key: target.key
|
|
772
|
-
});
|
|
912
|
+
function insertImport(source, { mod, kind }, importName, moduleSpecifier) {
|
|
913
|
+
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;
|
|
914
|
+
const lines = source.split("\n");
|
|
915
|
+
const lastImportLine = afterLine > 0 ? lines[afterLine - 1] : void 0;
|
|
916
|
+
const quote = lastImportLine?.includes(`"`) ? `"` : `'`;
|
|
917
|
+
const semicolon = lastImportLine?.trimEnd().endsWith(";") ? ";" : "";
|
|
918
|
+
const specifier = `${quote}${moduleSpecifier}${quote}`;
|
|
919
|
+
lines.splice(afterLine, 0, kind === "esm" ? `import { ${importName} } from ${specifier}${semicolon}` : `const { ${importName} } = require(${specifier})${semicolon}`);
|
|
920
|
+
if (afterLine === 0) lines.splice(1, 0, "");
|
|
921
|
+
return lines.join("\n");
|
|
773
922
|
}
|
|
774
923
|
/**
|
|
775
|
-
* Adds a `pluginX(...)` call to a config's plugins array. Replaying the same add is
|
|
776
|
-
* an import name collision with an unrelated package remains an error.
|
|
924
|
+
* Adds a `pluginX(...)` call to a config's plugins array, and its import. Replaying the same add is
|
|
925
|
+
* a no-op, while an import name collision with an unrelated package remains an error.
|
|
777
926
|
*/
|
|
778
|
-
function applyAddPlugin(
|
|
927
|
+
function applyAddPlugin(source, parsed, config, edit, quote) {
|
|
779
928
|
if (!isKubbPluginSpecifier(edit.plugin)) return { reason: `"${edit.plugin}" is not a @kubb/plugin-* package` };
|
|
780
929
|
const importName = edit.importName ?? toExportName(edit.plugin);
|
|
781
930
|
if (!IDENTIFIER.test(importName)) return { reason: `"${importName}" is not a valid import name` };
|
|
782
|
-
if (pluginCalls(mod, config).some((plugin) => plugin.packageName === edit.plugin)) return {
|
|
783
|
-
const taken = importedFrom(mod).get(importName);
|
|
931
|
+
if (pluginCalls(parsed.mod, config).some((plugin) => plugin.packageName === edit.plugin)) return { source };
|
|
932
|
+
const taken = importedFrom(parsed.mod).get(importName);
|
|
784
933
|
if (taken && taken !== edit.plugin) return { reason: `${importName} is already imported from ${taken}` };
|
|
785
934
|
const options = edit.options ?? {};
|
|
786
935
|
if (!isOptionValue(options)) return { reason: "the options are not literals that can be written to a config file" };
|
|
787
936
|
const plugins = property(config, "plugins");
|
|
788
937
|
if (plugins?.type !== "ArrayExpression") return { reason: "plugins is not an array literal" };
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
return taken ? {} : { addImport: {
|
|
792
|
-
importName,
|
|
793
|
-
moduleSpecifier: edit.plugin
|
|
794
|
-
} };
|
|
938
|
+
const next = insertItem(source, plugins, `${importName}(${Object.keys(options).length ? printValue(options, quote) : ""})`);
|
|
939
|
+
return { source: taken ? next : insertImport(next, parsed, importName, edit.plugin) };
|
|
795
940
|
}
|
|
796
|
-
/**
|
|
797
|
-
* Comments out a plugin call in place, keeping its options on disk so `enable-plugin` can restore
|
|
798
|
-
* them exactly. Operates on `source` text rather than the AST: a commented-out call is no longer a
|
|
799
|
-
* node magicast can address, and the surrounding array must not reflow when its element count
|
|
800
|
-
* never actually changes.
|
|
801
|
-
*/
|
|
941
|
+
/** Comments out a plugin call in place, keeping its options on disk so `enable-plugin` can restore them exactly. */
|
|
802
942
|
function disablePlugin(source, mod, config, plugin) {
|
|
803
|
-
const
|
|
804
|
-
if (!
|
|
805
|
-
const loc = target.call.loc;
|
|
806
|
-
if (!loc?.start || !loc.end) return { reason: `${plugin} has no source location to comment out` };
|
|
943
|
+
const loc = pluginCalls(mod, config).find((entry) => entry.packageName === plugin)?.call.loc;
|
|
944
|
+
if (!loc) return { reason: `${plugin} is not in the plugins array` };
|
|
807
945
|
const lines = source.split("\n");
|
|
808
946
|
const from = loc.start.line - 1;
|
|
809
947
|
const to = loc.end.line - 1;
|
|
810
948
|
const firstLine = lines[from] ?? "";
|
|
811
|
-
|
|
812
|
-
if (firstLine.slice(0, loc.start.column).trim() !== "" || !/^,?\s*$/.test(lastLine.slice(loc.end.column))) return { reason: `${plugin} shares a line with other code, so it cannot be commented out safely` };
|
|
949
|
+
if (firstLine.slice(0, loc.start.column).trim() || !/^,?\s*$/.test((lines[to] ?? "").slice(loc.end.column))) return { reason: `${plugin} shares a line with other code, so it cannot be commented out safely` };
|
|
813
950
|
const indent = firstLine.match(/^\s*/)?.[0] ?? "";
|
|
814
|
-
const commented = lines.slice(from, to + 1).map((line) => line.trim() ? `${indent}// ${line.slice(indent.length)}` :
|
|
815
|
-
lines.splice(from,
|
|
951
|
+
const commented = lines.slice(from, to + 1).map((line) => line.trim() ? `${indent}// ${line.slice(indent.length)}` : `${indent}//`);
|
|
952
|
+
lines.splice(from, commented.length, `${indent}// kubb:disabled ${plugin} ${commented.length}`, ...commented);
|
|
816
953
|
return { source: lines.join("\n") };
|
|
817
954
|
}
|
|
818
|
-
/**
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
955
|
+
/** Uncomments the block a previous `disable-plugin` left behind for `plugin` in this config. */
|
|
956
|
+
function enablePlugin(source, config, plugin) {
|
|
957
|
+
const block = disabledBlocks(source, config).find((entry) => entry.packageName === plugin);
|
|
958
|
+
if (!block) return { reason: `${plugin} is not disabled` };
|
|
822
959
|
const lines = source.split("\n");
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
if (marker?.plugin !== plugin) continue;
|
|
826
|
-
const end = index + 1 + marker.lineCount;
|
|
827
|
-
const restored = lines.slice(index + 1, end).map((commented) => commented.replace(/^(\s*)\/\/ ?/, "$1"));
|
|
828
|
-
lines.splice(index, end - index, ...restored);
|
|
829
|
-
return { source: lines.join("\n") };
|
|
830
|
-
}
|
|
831
|
-
return { reason: `${plugin} is not disabled` };
|
|
832
|
-
}
|
|
833
|
-
/**
|
|
834
|
-
* Parses `source` and finds the config entries it exports.
|
|
835
|
-
*/
|
|
836
|
-
function parseConfigs(source) {
|
|
837
|
-
let mod;
|
|
838
|
-
try {
|
|
839
|
-
mod = parseModule(source);
|
|
840
|
-
} catch {
|
|
841
|
-
return { reason: "the config file could not be parsed" };
|
|
842
|
-
}
|
|
843
|
-
const found = findConfigs(mod);
|
|
844
|
-
return "reason" in found ? found : {
|
|
845
|
-
mod,
|
|
846
|
-
configs: found.configs
|
|
847
|
-
};
|
|
960
|
+
lines.splice(block.line - 1, block.end - block.line + 1, ...block.code);
|
|
961
|
+
return { source: lines.join("\n") };
|
|
848
962
|
}
|
|
849
|
-
/**
|
|
850
|
-
|
|
851
|
-
* than sharing one module across the batch, since the disable/enable edits rewrite `source` as
|
|
852
|
-
* text and would otherwise leave the others working from a stale tree.
|
|
853
|
-
*/
|
|
854
|
-
function parseTarget(source, ref) {
|
|
963
|
+
/** Applies one edit to `source`. Every edit re-parses the file, since the previous one rewrote it. */
|
|
964
|
+
function applyEdit(source, edit, quote) {
|
|
855
965
|
const parsed = parseConfigs(source);
|
|
856
966
|
if ("reason" in parsed) return parsed;
|
|
857
|
-
const
|
|
967
|
+
const ref = edit.config;
|
|
968
|
+
const config = typeof ref === "string" ? parsed.configs.find((entry) => configName(entry) === ref) : parsed.configs[ref ?? 0];
|
|
858
969
|
if (!config) return { reason: `no config entry found for ${JSON.stringify(ref)}` };
|
|
859
|
-
return
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
970
|
+
if (edit.operation === "disable-plugin") return disablePlugin(source, parsed.mod, config, edit.plugin);
|
|
971
|
+
if (edit.operation === "enable-plugin") return enablePlugin(source, config, edit.plugin);
|
|
972
|
+
if (edit.operation === "add-plugin") return applyAddPlugin(source, parsed, config, edit, quote);
|
|
973
|
+
const call = pluginCalls(parsed.mod, config).find((plugin) => plugin.packageName === edit.plugin)?.call;
|
|
974
|
+
if (!call) return { reason: disabledBlocks(source, config).some((block) => block.packageName === edit.plugin) ? `${edit.plugin} is disabled, enable it first` : `${edit.plugin} is not in the plugins array` };
|
|
975
|
+
return edit.operation === "set" ? applySet(source, call, edit.path, edit.value, quote) : applyRemove(source, call, edit.path);
|
|
863
976
|
}
|
|
864
977
|
/**
|
|
865
|
-
* Applies edits to a `kubb.config.ts` in place.
|
|
866
|
-
*
|
|
978
|
+
* Applies edits to a `kubb.config.ts` in place. Each edit rewrites only the text it targets, so
|
|
979
|
+
* comments, formatting, and hand-written code around the config survive.
|
|
867
980
|
*
|
|
868
981
|
* Edits are independent: one that cannot be applied is reported in `outcomes` and the rest still run.
|
|
869
982
|
*
|
|
@@ -873,25 +986,19 @@ function parseTarget(source, ref) {
|
|
|
873
986
|
* { operation: 'set', plugin: '@kubb/plugin-ts', path: ['enum', 'type'], value: 'enum' },
|
|
874
987
|
* ])
|
|
875
988
|
* ```
|
|
876
|
-
*
|
|
877
|
-
* @note recast always reprints a semicolon on a reprinted statement, so editing a block-body
|
|
878
|
-
* `defineConfig` in a semicolon-free file adds one to the `return` line. This is a known gap.
|
|
879
|
-
* Strip it when `detectCodeFormat` reports `useSemi: false`, if it turns out to matter in practice.
|
|
880
989
|
*/
|
|
881
990
|
function applyConfigEdits(source, edits) {
|
|
882
991
|
let current = source;
|
|
883
|
-
const
|
|
884
|
-
const endsWithNewline = source.endsWith("\n");
|
|
992
|
+
const quote = detectCodeFormat(source).quote === "double" ? `"` : `'`;
|
|
885
993
|
const outcomes = edits.map((edit) => {
|
|
886
|
-
const
|
|
887
|
-
if (
|
|
994
|
+
const invalid = invalidEdit(edit);
|
|
995
|
+
if (invalid) return {
|
|
888
996
|
edit,
|
|
889
997
|
applied: false,
|
|
890
|
-
reason:
|
|
998
|
+
reason: invalid
|
|
891
999
|
};
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
const result = edit.operation === "disable-plugin" ? disablePlugin(current, mod, config, edit.plugin) : enablePlugin(current, edit.plugin);
|
|
1000
|
+
try {
|
|
1001
|
+
const result = applyEdit(current, edit, quote);
|
|
895
1002
|
if ("reason" in result) return {
|
|
896
1003
|
edit,
|
|
897
1004
|
applied: false,
|
|
@@ -902,44 +1009,13 @@ function applyConfigEdits(source, edits) {
|
|
|
902
1009
|
edit,
|
|
903
1010
|
applied: true
|
|
904
1011
|
};
|
|
905
|
-
}
|
|
906
|
-
if (edit.operation === "add-plugin") {
|
|
907
|
-
const result = applyAddPlugin(mod, config, edit);
|
|
908
|
-
if ("reason" in result) return {
|
|
909
|
-
edit,
|
|
910
|
-
applied: false,
|
|
911
|
-
reason: result.reason
|
|
912
|
-
};
|
|
913
|
-
if ("noop" in result) return {
|
|
914
|
-
edit,
|
|
915
|
-
applied: true
|
|
916
|
-
};
|
|
917
|
-
const afterLine = lastImportEndLine(mod);
|
|
918
|
-
let next = generateCode(mod, { format }).code;
|
|
919
|
-
if (result.addImport) next = insertImportLine({
|
|
920
|
-
source: next,
|
|
921
|
-
afterLine,
|
|
922
|
-
...result.addImport
|
|
923
|
-
});
|
|
924
|
-
current = withTrailingNewline(next, endsWithNewline);
|
|
1012
|
+
} catch (error) {
|
|
925
1013
|
return {
|
|
926
1014
|
edit,
|
|
927
|
-
applied:
|
|
1015
|
+
applied: false,
|
|
1016
|
+
reason: getErrorMessage(error)
|
|
928
1017
|
};
|
|
929
1018
|
}
|
|
930
|
-
const pluginCall = pluginCalls(mod, config).find((plugin) => plugin.packageName === edit.plugin);
|
|
931
|
-
if (!pluginCall) return {
|
|
932
|
-
edit,
|
|
933
|
-
applied: false,
|
|
934
|
-
reason: `${edit.plugin} is not in the plugins array`
|
|
935
|
-
};
|
|
936
|
-
const reason = edit.operation === "set" ? applySet(pluginCall.call, edit.path, edit.value) : applyRemove(pluginCall.call, edit.path);
|
|
937
|
-
if (!reason) current = withTrailingNewline(generateCode(mod, { format }).code, endsWithNewline);
|
|
938
|
-
return {
|
|
939
|
-
edit,
|
|
940
|
-
applied: !reason,
|
|
941
|
-
reason
|
|
942
|
-
};
|
|
943
1019
|
});
|
|
944
1020
|
return {
|
|
945
1021
|
source: current,
|
|
@@ -947,42 +1023,12 @@ function applyConfigEdits(source, edits) {
|
|
|
947
1023
|
changed: current !== source
|
|
948
1024
|
};
|
|
949
1025
|
}
|
|
1026
|
+
/** Applies `edits` to the config file at `filePath`, writing it only when something changed. */
|
|
950
1027
|
async function writeConfigEdits({ filePath, edits }) {
|
|
951
1028
|
const result = applyConfigEdits(await read(filePath), edits);
|
|
952
1029
|
if (result.changed) await writeFile(filePath, result.source, "utf-8");
|
|
953
1030
|
return result;
|
|
954
1031
|
}
|
|
955
|
-
/**
|
|
956
|
-
* The 1-based line where the file's last import declaration ends, or `0` when it has none. Read
|
|
957
|
-
* off the parsed module, so a multi-line `import {\n x,\n} from '...'` reports its closing line
|
|
958
|
-
* rather than the `import` keyword.
|
|
959
|
-
*/
|
|
960
|
-
function lastImportEndLine(mod) {
|
|
961
|
-
return (mod.$ast.type === "Program" ? mod.$ast.body : []).filter((node) => node.type === "ImportDeclaration").at(-1)?.loc?.end.line ?? 0;
|
|
962
|
-
}
|
|
963
|
-
/**
|
|
964
|
-
* Writes an import after the last one already in the file, matching its quote style and whether it
|
|
965
|
-
* ends in a semicolon. `afterLine` is where that last import ends, `0` for a file with none.
|
|
966
|
-
*
|
|
967
|
-
* Written as plain text rather than through magicast's import builder, which prints a brand-new
|
|
968
|
-
* import declaration with its own default spacing and a semicolon regardless of `format`, since
|
|
969
|
-
* that formatting only governs nodes recast can diff against the original source.
|
|
970
|
-
*/
|
|
971
|
-
function insertImportLine({ source, importName, moduleSpecifier, afterLine }) {
|
|
972
|
-
const lines = source.split("\n");
|
|
973
|
-
const lastImportLine = afterLine > 0 ? lines[afterLine - 1] : void 0;
|
|
974
|
-
const quote = lastImportLine?.includes(`"`) ? `"` : `'`;
|
|
975
|
-
const line = `import { ${importName} } from ${quote}${moduleSpecifier}${quote}${lastImportLine?.trimEnd().endsWith(";") ? ";" : ""}`;
|
|
976
|
-
lines.splice(afterLine, 0, ...afterLine > 0 ? [line] : [line, ""]);
|
|
977
|
-
return lines.join("\n");
|
|
978
|
-
}
|
|
979
|
-
/**
|
|
980
|
-
* `generateCode` always drops the file's trailing newline. Restore it when the input had one.
|
|
981
|
-
*/
|
|
982
|
-
function withTrailingNewline(code, hadTrailingNewline) {
|
|
983
|
-
if (!hadTrailingNewline || code.endsWith("\n")) return code;
|
|
984
|
-
return `${code}\n`;
|
|
985
|
-
}
|
|
986
1032
|
//#endregion
|
|
987
1033
|
//#region src/generations.ts
|
|
988
1034
|
const READ_CONCURRENCY = 50;
|
|
@@ -1105,7 +1151,8 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1105
1151
|
if (path.split("/").includes("..")) return;
|
|
1106
1152
|
const content = await files.storage.readItem(resolve(files.root, path));
|
|
1107
1153
|
if (content === null) return;
|
|
1108
|
-
|
|
1154
|
+
if (source === "output" && !content.trim()) return;
|
|
1155
|
+
hashes[path] = hashOf(source === "output" ? `${content.trim()}\n` : content);
|
|
1109
1156
|
bytes += Buffer.byteLength(content);
|
|
1110
1157
|
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}${source}/${path}`, content);
|
|
1111
1158
|
}
|
|
@@ -1512,8 +1559,11 @@ var Agent = class extends RpcTarget {
|
|
|
1512
1559
|
path: configPath,
|
|
1513
1560
|
file,
|
|
1514
1561
|
plugins: config.plugins.map((plugin) => ({
|
|
1515
|
-
name:
|
|
1516
|
-
|
|
1562
|
+
name: toStudioName({
|
|
1563
|
+
name: plugin.name,
|
|
1564
|
+
root
|
|
1565
|
+
}),
|
|
1566
|
+
options: toJSONOptions(plugin.options)
|
|
1517
1567
|
}))
|
|
1518
1568
|
},
|
|
1519
1569
|
permissions: this.#permissions
|
|
@@ -1570,11 +1620,15 @@ var Agent = class extends RpcTarget {
|
|
|
1570
1620
|
}
|
|
1571
1621
|
async #generate({ input, root, signal }) {
|
|
1572
1622
|
const { jobId, config: patch } = input;
|
|
1623
|
+
const studioInput = this.#sandbox ? patch?.input : void 0;
|
|
1624
|
+
if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || getInputKind(studioInput) !== "inline")) return this.#refuse({ message: "A sandbox agent generates only from the OpenAPI document content Studio sends, not from a file path or URL" });
|
|
1573
1625
|
const config = await this.#options.loadConfig();
|
|
1574
|
-
const plugins = await mergePlugins(config.plugins, patch?.plugins)
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1626
|
+
const [plugins, adapter] = await Promise.all([mergePlugins(config.plugins, patch?.plugins), mergeAdapter(config.adapter, patch?.adapter)]).catch(async (error) => {
|
|
1627
|
+
await emit(this.#options.hooks, "kubb:error", { error: toError(error) });
|
|
1628
|
+
throw error;
|
|
1629
|
+
});
|
|
1630
|
+
const activePlugins = plugins ?? config.plugins;
|
|
1631
|
+
if (patch?.input && !this.#sandbox) await emit(this.#options.hooks, "kubb:warn", { message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox agent" });
|
|
1578
1632
|
const diskFiles = !this.#sandbox && this.#permissions.allowRead ? await listDisk({
|
|
1579
1633
|
root,
|
|
1580
1634
|
outputPath: config.output.path,
|
|
@@ -1592,7 +1646,7 @@ var Agent = class extends RpcTarget {
|
|
|
1592
1646
|
root,
|
|
1593
1647
|
input: studioInput ?? config.input,
|
|
1594
1648
|
storage: this.#permissions.allowWrite ? fsStorage() : memoryStorage(),
|
|
1595
|
-
plugins,
|
|
1649
|
+
plugins: activePlugins,
|
|
1596
1650
|
adapter
|
|
1597
1651
|
},
|
|
1598
1652
|
hooks: this.#options.hooks,
|
|
@@ -1622,7 +1676,7 @@ var Agent = class extends RpcTarget {
|
|
|
1622
1676
|
hashes: output.hashes,
|
|
1623
1677
|
disk: disk ? { hashes: disk.hashes } : void 0
|
|
1624
1678
|
},
|
|
1625
|
-
info: `${
|
|
1679
|
+
info: `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "in memory"}${studioInput !== void 0 ? ", from a Studio spec" : ""}`
|
|
1626
1680
|
};
|
|
1627
1681
|
}
|
|
1628
1682
|
saveConfig(input) {
|
|
@@ -1777,8 +1831,12 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
1777
1831
|
clearTimeout(timer);
|
|
1778
1832
|
signal?.removeEventListener("abort", rpc.close);
|
|
1779
1833
|
session.connected.reject(/* @__PURE__ */ new Error("The socket closed before Studio called connect()"));
|
|
1780
|
-
session.active
|
|
1781
|
-
|
|
1834
|
+
const active = session.active;
|
|
1835
|
+
if (active) {
|
|
1836
|
+
active.abort();
|
|
1837
|
+
await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
|
|
1838
|
+
}
|
|
1839
|
+
await active?.settled;
|
|
1782
1840
|
return {
|
|
1783
1841
|
...close,
|
|
1784
1842
|
connected: session.info !== void 0
|