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