@kubb/studio 5.6.1 → 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 +539 -495
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -14
- package/dist/index.js +542 -498
- 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
|
@@ -4,14 +4,14 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import { createHash, hash, randomUUID } from "node:crypto";
|
|
5
5
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
|
-
import path, { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
8
|
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
|
-
*
|
|
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.
|
|
701
859
|
*/
|
|
702
|
-
function
|
|
703
|
-
const options = call.arguments[0];
|
|
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.
|
|
718
|
-
*/
|
|
719
|
-
function optionParent(options, path) {
|
|
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;
|
|
@@ -1034,19 +1080,21 @@ async function generate({ config, hooks, signal, allowExec }) {
|
|
|
1034
1080
|
lint: false,
|
|
1035
1081
|
postGenerate: []
|
|
1036
1082
|
};
|
|
1037
|
-
const
|
|
1083
|
+
const kubb = createKubb({
|
|
1038
1084
|
...config,
|
|
1039
1085
|
output
|
|
1040
1086
|
}, {
|
|
1041
1087
|
hooks,
|
|
1042
1088
|
signal
|
|
1043
|
-
})
|
|
1089
|
+
});
|
|
1090
|
+
const result = await kubb.generate();
|
|
1044
1091
|
if (!result.success) throw generationFailure(result.diagnostics);
|
|
1092
|
+
const { storage, root } = kubb.config;
|
|
1045
1093
|
return {
|
|
1046
|
-
storage
|
|
1047
|
-
root
|
|
1094
|
+
storage,
|
|
1095
|
+
root,
|
|
1048
1096
|
paths: new Set(result.files.map((file) => relativeStoragePath({
|
|
1049
|
-
root
|
|
1097
|
+
root,
|
|
1050
1098
|
filePath: file.path
|
|
1051
1099
|
})))
|
|
1052
1100
|
};
|
|
@@ -1103,7 +1151,8 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1103
1151
|
if (path.split("/").includes("..")) return;
|
|
1104
1152
|
const content = await files.storage.readItem(resolve(files.root, path));
|
|
1105
1153
|
if (content === null) return;
|
|
1106
|
-
|
|
1154
|
+
if (source === "output" && !content.trim()) return;
|
|
1155
|
+
hashes[path] = hashOf(source === "output" ? `${content.trim()}\n` : content);
|
|
1107
1156
|
bytes += Buffer.byteLength(content);
|
|
1108
1157
|
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}${source}/${path}`, content);
|
|
1109
1158
|
}
|
|
@@ -1510,8 +1559,11 @@ var Agent = class extends RpcTarget {
|
|
|
1510
1559
|
path: configPath,
|
|
1511
1560
|
file,
|
|
1512
1561
|
plugins: config.plugins.map((plugin) => ({
|
|
1513
|
-
name:
|
|
1514
|
-
|
|
1562
|
+
name: toStudioName({
|
|
1563
|
+
name: plugin.name,
|
|
1564
|
+
root
|
|
1565
|
+
}),
|
|
1566
|
+
options: toJSONOptions(plugin.options)
|
|
1515
1567
|
}))
|
|
1516
1568
|
},
|
|
1517
1569
|
permissions: this.#permissions
|
|
@@ -1568,11 +1620,15 @@ var Agent = class extends RpcTarget {
|
|
|
1568
1620
|
}
|
|
1569
1621
|
async #generate({ input, root, signal }) {
|
|
1570
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" });
|
|
1571
1625
|
const config = await this.#options.loadConfig();
|
|
1572
|
-
const plugins = await mergePlugins(config.plugins, patch?.plugins)
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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" });
|
|
1576
1632
|
const diskFiles = !this.#sandbox && this.#permissions.allowRead ? await listDisk({
|
|
1577
1633
|
root,
|
|
1578
1634
|
outputPath: config.output.path,
|
|
@@ -1590,7 +1646,7 @@ var Agent = class extends RpcTarget {
|
|
|
1590
1646
|
root,
|
|
1591
1647
|
input: studioInput ?? config.input,
|
|
1592
1648
|
storage: this.#permissions.allowWrite ? fsStorage() : memoryStorage(),
|
|
1593
|
-
plugins,
|
|
1649
|
+
plugins: activePlugins,
|
|
1594
1650
|
adapter
|
|
1595
1651
|
},
|
|
1596
1652
|
hooks: this.#options.hooks,
|
|
@@ -1620,7 +1676,7 @@ var Agent = class extends RpcTarget {
|
|
|
1620
1676
|
hashes: output.hashes,
|
|
1621
1677
|
disk: disk ? { hashes: disk.hashes } : void 0
|
|
1622
1678
|
},
|
|
1623
|
-
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" : ""}`
|
|
1624
1680
|
};
|
|
1625
1681
|
}
|
|
1626
1682
|
saveConfig(input) {
|
|
@@ -1775,8 +1831,12 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
1775
1831
|
clearTimeout(timer);
|
|
1776
1832
|
signal?.removeEventListener("abort", rpc.close);
|
|
1777
1833
|
session.connected.reject(/* @__PURE__ */ new Error("The socket closed before Studio called connect()"));
|
|
1778
|
-
session.active
|
|
1779
|
-
|
|
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;
|
|
1780
1840
|
return {
|
|
1781
1841
|
...close,
|
|
1782
1842
|
connected: session.info !== void 0
|
|
@@ -1991,29 +2051,24 @@ const gzipAsync = promisify(gzip);
|
|
|
1991
2051
|
const UPLOAD_TIMEOUT_MS = 12e4;
|
|
1992
2052
|
const BUNDLED_DEPENDENCIES = ["@kubb/plugin-barrel"];
|
|
1993
2053
|
/**
|
|
1994
|
-
* Maps a generated file
|
|
1995
|
-
* `src`/`dist` segment and any `..`/empty path segment so a crafted file name cannot escape the
|
|
1996
|
-
* `package/` root.
|
|
2054
|
+
* Maps a generated file to its path in the package, from its `src`/`dist` segment on, without `..` parts that could escape it.
|
|
1997
2055
|
*/
|
|
1998
2056
|
function packagePath(filePath) {
|
|
1999
2057
|
const normalized = filePath.replaceAll("\\", "/");
|
|
2000
|
-
return
|
|
2058
|
+
return (normalized.match(/\/(?:src|dist)\/.*$/)?.[0] ?? normalized).split("/").filter((part) => part && part !== "." && part !== "..").join("/");
|
|
2001
2059
|
}
|
|
2002
2060
|
/**
|
|
2003
|
-
* Splits a
|
|
2004
|
-
* fit, the 155-byte USTAR `prefix` field that extends it. Throws rather than silently truncating
|
|
2005
|
-
* a path the format cannot address (max 256 bytes: 100 name + 1 separator + 155 prefix).
|
|
2061
|
+
* Splits a tar path into the 100-byte USTAR `name` and the 155-byte `prefix`, and throws for a path that fits neither.
|
|
2006
2062
|
*/
|
|
2007
2063
|
function splitEntryPath(path) {
|
|
2008
|
-
if (Buffer.byteLength(path
|
|
2064
|
+
if (Buffer.byteLength(path) <= 100) return {
|
|
2009
2065
|
name: path,
|
|
2010
2066
|
prefix: ""
|
|
2011
2067
|
};
|
|
2012
|
-
for (let i = path.
|
|
2013
|
-
if (path[i] !== "/") continue;
|
|
2068
|
+
for (let i = path.lastIndexOf("/"); i > 0; i = path.lastIndexOf("/", i - 1)) {
|
|
2014
2069
|
const prefix = path.slice(0, i);
|
|
2015
2070
|
const name = path.slice(i + 1);
|
|
2016
|
-
if (Buffer.byteLength(prefix
|
|
2071
|
+
if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(name) <= 100) return {
|
|
2017
2072
|
name,
|
|
2018
2073
|
prefix
|
|
2019
2074
|
};
|
|
@@ -2023,21 +2078,23 @@ function splitEntryPath(path) {
|
|
|
2023
2078
|
function header(path, size) {
|
|
2024
2079
|
const { name, prefix } = splitEntryPath(path);
|
|
2025
2080
|
const value = Buffer.alloc(512);
|
|
2026
|
-
value.write(name
|
|
2027
|
-
value.write("0000644\0", 100
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2081
|
+
value.write(name);
|
|
2082
|
+
value.write("0000644\0", 100);
|
|
2083
|
+
for (const offset of [
|
|
2084
|
+
108,
|
|
2085
|
+
116,
|
|
2086
|
+
265,
|
|
2087
|
+
297
|
|
2088
|
+
]) value.write("0000000\0", offset);
|
|
2089
|
+
value.write(`${size.toString(8).padStart(11, "0")}\0`, 124);
|
|
2090
|
+
value.write("03560116604\0", 136);
|
|
2032
2091
|
value.fill(32, 148, 156);
|
|
2033
|
-
value.write("0", 156
|
|
2034
|
-
value.write("ustar\0", 257
|
|
2035
|
-
value.write("00", 263
|
|
2036
|
-
value.write(
|
|
2037
|
-
value.
|
|
2038
|
-
value.write(
|
|
2039
|
-
const checksum = [...value].reduce((sum, byte) => sum + byte, 0);
|
|
2040
|
-
value.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, "ascii");
|
|
2092
|
+
value.write("0", 156);
|
|
2093
|
+
value.write("ustar\0", 257);
|
|
2094
|
+
value.write("00", 263);
|
|
2095
|
+
value.write(prefix, 345);
|
|
2096
|
+
const checksum = value.reduce((sum, byte) => sum + byte, 0);
|
|
2097
|
+
value.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148);
|
|
2041
2098
|
return value;
|
|
2042
2099
|
}
|
|
2043
2100
|
/**
|
|
@@ -2045,31 +2102,29 @@ function header(path, size) {
|
|
|
2045
2102
|
* generated sources, a `tsdown`-built `dist/` (esm + cjs), and a `package.json` manifest.
|
|
2046
2103
|
*/
|
|
2047
2104
|
async function createSnapshotPackage({ files, packageInfo }) {
|
|
2105
|
+
const sources = /* @__PURE__ */ new Map();
|
|
2106
|
+
for (const [name, content] of Object.entries(files)) {
|
|
2107
|
+
const path = packagePath(name);
|
|
2108
|
+
const owner = sources.get(path)?.name;
|
|
2109
|
+
if (owner) throw new Error(`Snapshot has two generated files that sanitize to the same path "package/${path}": "${owner}" and "${name}"`);
|
|
2110
|
+
sources.set(path, {
|
|
2111
|
+
name,
|
|
2112
|
+
content
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2048
2115
|
const root = await mkdtemp(join(tmpdir(), "kubb-snapshot-"));
|
|
2049
2116
|
const dist = join(root, "dist");
|
|
2050
|
-
const resolvedPaths = Object.entries(files).map(([name, content]) => ({
|
|
2051
|
-
name,
|
|
2052
|
-
content,
|
|
2053
|
-
target: packagePath(name)
|
|
2054
|
-
}));
|
|
2055
|
-
const targetOwners = /* @__PURE__ */ new Map();
|
|
2056
|
-
for (const { name, target } of resolvedPaths) {
|
|
2057
|
-
const owner = targetOwners.get(target);
|
|
2058
|
-
if (owner) throw new Error(`Snapshot has two generated files that sanitize to the same path "${target}": "${owner}" and "${name}"`);
|
|
2059
|
-
targetOwners.set(target, name);
|
|
2060
|
-
}
|
|
2061
2117
|
try {
|
|
2062
2118
|
await mkdir(dist);
|
|
2063
|
-
await Promise.all(
|
|
2064
|
-
|
|
2065
|
-
await
|
|
2066
|
-
await writeFile(path, content);
|
|
2119
|
+
await Promise.all([...sources].map(async ([path, { content }]) => {
|
|
2120
|
+
await mkdir(dirname(join(root, path)), { recursive: true });
|
|
2121
|
+
await writeFile(join(root, path), content);
|
|
2067
2122
|
}));
|
|
2068
|
-
const
|
|
2069
|
-
if (
|
|
2123
|
+
const entry = [...sources.keys()].filter((path) => /\.(?:[cm]?[jt]sx?)$/.test(path)).map((path) => join(root, path));
|
|
2124
|
+
if (entry.length) {
|
|
2070
2125
|
const { build } = await import("tsdown");
|
|
2071
2126
|
await build({
|
|
2072
|
-
entry
|
|
2127
|
+
entry,
|
|
2073
2128
|
outDir: dist,
|
|
2074
2129
|
format: ["esm", "cjs"],
|
|
2075
2130
|
dts: false,
|
|
@@ -2080,38 +2135,38 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2080
2135
|
fixedExtension: true
|
|
2081
2136
|
});
|
|
2082
2137
|
}
|
|
2083
|
-
const
|
|
2084
|
-
const
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
...barrelExport,
|
|
2101
|
-
"./*": {
|
|
2102
|
-
import: "./dist/*.mjs",
|
|
2103
|
-
require: "./dist/*.cjs"
|
|
2104
|
-
}
|
|
2138
|
+
const built = await fsStorage().readKeys(dist);
|
|
2139
|
+
const hasBarrel = built.includes("index.mjs") && built.includes("index.cjs");
|
|
2140
|
+
const manifest = {
|
|
2141
|
+
...packageInfo,
|
|
2142
|
+
type: "module",
|
|
2143
|
+
...hasBarrel ? {
|
|
2144
|
+
main: "./dist/index.cjs",
|
|
2145
|
+
module: "./dist/index.mjs"
|
|
2146
|
+
} : {},
|
|
2147
|
+
exports: {
|
|
2148
|
+
...hasBarrel ? { ".": {
|
|
2149
|
+
import: "./dist/index.mjs",
|
|
2150
|
+
require: "./dist/index.cjs"
|
|
2151
|
+
} } : {},
|
|
2152
|
+
"./*": {
|
|
2153
|
+
import: "./dist/*.mjs",
|
|
2154
|
+
require: "./dist/*.cjs"
|
|
2105
2155
|
}
|
|
2106
|
-
}
|
|
2107
|
-
...Object.fromEntries(resolvedPaths.map(({ content, target }) => [target, content])),
|
|
2108
|
-
...Object.fromEntries(builtEntries)
|
|
2156
|
+
}
|
|
2109
2157
|
};
|
|
2110
|
-
const chunks = [
|
|
2111
|
-
|
|
2158
|
+
const chunks = [...new Map([
|
|
2159
|
+
["package.json", JSON.stringify(manifest, null, 2)],
|
|
2160
|
+
...[...sources].map(([path, { content }]) => [path, content]),
|
|
2161
|
+
...await Promise.all(built.map(async (key) => [`dist/${key}`, await readFile(join(dist, key), "utf8")]))
|
|
2162
|
+
])].flatMap(([path, content]) => {
|
|
2112
2163
|
const bytes = Buffer.from(content);
|
|
2113
|
-
|
|
2114
|
-
|
|
2164
|
+
return [
|
|
2165
|
+
header(`package/${path}`, bytes.length),
|
|
2166
|
+
bytes,
|
|
2167
|
+
Buffer.alloc((512 - bytes.length % 512) % 512)
|
|
2168
|
+
];
|
|
2169
|
+
});
|
|
2115
2170
|
return await gzipAsync(Buffer.concat([...chunks, Buffer.alloc(1024)]));
|
|
2116
2171
|
} finally {
|
|
2117
2172
|
await rm(root, {
|
|
@@ -2120,25 +2175,12 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2120
2175
|
});
|
|
2121
2176
|
}
|
|
2122
2177
|
}
|
|
2123
|
-
function failureMessage({ status, _data }) {
|
|
2124
|
-
const upgrade = _data?.data?.upgradeUrl ? ` Upgrade at ${_data.data.upgradeUrl}.` : "";
|
|
2125
|
-
return `Kubb Studio refused the snapshot (status ${status})${_data?.message ? `: ${_data.message}` : ""}.${upgrade}`;
|
|
2126
|
-
}
|
|
2127
2178
|
/**
|
|
2128
|
-
* Generates the project, packs it as an npm tarball, and publishes it to Studio in one
|
|
2129
|
-
* `POST /api/snapshots` request.
|
|
2179
|
+
* Generates the project, packs it as an npm tarball, and publishes it to Studio in one `POST /api/snapshots` request.
|
|
2130
2180
|
*
|
|
2131
2181
|
* @example
|
|
2132
2182
|
* ```ts
|
|
2133
|
-
* const
|
|
2134
|
-
* apiKey: process.env.KUBB_TOKEN!,
|
|
2135
|
-
* config,
|
|
2136
|
-
* id: 'gh:123:pr-4',
|
|
2137
|
-
* name: 'acme/api#4',
|
|
2138
|
-
* packageName: '@acme/api',
|
|
2139
|
-
* version: '1.0.0',
|
|
2140
|
-
* })
|
|
2141
|
-
* console.log(snapshot.url, snapshot.changes)
|
|
2183
|
+
* const { url } = await createSnapshot({ apiKey: process.env.KUBB_TOKEN!, config, id: 'gh:123:pr-4', name: 'acme/api#4', packageName: '@acme/api', version: '1.0.0' })
|
|
2142
2184
|
* ```
|
|
2143
2185
|
*/
|
|
2144
2186
|
async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, hooks, signal, ...metadata }) {
|
|
@@ -2178,7 +2220,7 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2178
2220
|
}));
|
|
2179
2221
|
body.set("tarball", new Blob([new Uint8Array(bytes)], { type: "application/gzip" }), "snapshot.tgz");
|
|
2180
2222
|
const timeout = AbortSignal.timeout(UPLOAD_TIMEOUT_MS);
|
|
2181
|
-
const
|
|
2223
|
+
const { ok, status, _data } = await ofetch.raw(url.href, {
|
|
2182
2224
|
method: "POST",
|
|
2183
2225
|
headers: { "x-api-key": apiKey },
|
|
2184
2226
|
body,
|
|
@@ -2186,10 +2228,12 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2186
2228
|
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
2187
2229
|
ignoreResponseError: true
|
|
2188
2230
|
});
|
|
2189
|
-
if (!
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2231
|
+
if (!ok) {
|
|
2232
|
+
const upgrade = _data?.data?.upgradeUrl ? ` Upgrade at ${_data.data.upgradeUrl}.` : "";
|
|
2233
|
+
throw new Error(`Kubb Studio refused the snapshot (status ${status})${_data?.message ? `: ${_data.message}` : ""}.${upgrade}`);
|
|
2234
|
+
}
|
|
2235
|
+
if (!_data?.snapshot) throw new Error(`Kubb Studio returned no snapshot (status ${status})`);
|
|
2236
|
+
return _data.snapshot;
|
|
2193
2237
|
}
|
|
2194
2238
|
//#endregion
|
|
2195
2239
|
export { IncompatibleAgentError, InvalidAgentTokenError, PairingCanceledError, PairingDeniedError, PairingExpiredError, connectAgent, createSnapshot, defaultStudioUrl, pairAgent };
|