@kubb/plugin-ts 5.0.0-beta.30 → 5.0.0-beta.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +124 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +124 -61
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/generators/typeGenerator.tsx +58 -69
- package/src/plugin.ts +3 -13
- package/src/utils.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -804,6 +804,54 @@ function Type({ name, node, printer, enumType, enumTypeSuffix, enumKeyCasing, re
|
|
|
804
804
|
}
|
|
805
805
|
//#endregion
|
|
806
806
|
//#region ../../internals/shared/src/operation.ts
|
|
807
|
+
/**
|
|
808
|
+
* Maps a content type to the PascalCase suffix used to name per-content-type variants
|
|
809
|
+
* (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
|
|
810
|
+
*/
|
|
811
|
+
function getContentTypeSuffix(contentType) {
|
|
812
|
+
const baseType = contentType.split(";")[0].trim();
|
|
813
|
+
if (baseType === "application/json") return "Json";
|
|
814
|
+
if (baseType === "multipart/form-data") return "FormData";
|
|
815
|
+
if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
|
|
816
|
+
const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
817
|
+
if (parts.length === 0) return "Unknown";
|
|
818
|
+
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
|
|
822
|
+
* (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
|
|
823
|
+
*/
|
|
824
|
+
function getPerContentTypeName(baseName, suffix) {
|
|
825
|
+
if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
|
|
826
|
+
return baseName + suffix;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Resolves per-content-type variant names for a set of content entries, deduplicating suffix
|
|
830
|
+
* collisions with a numeric counter. Entries without a schema are skipped. The returned `suffix` is
|
|
831
|
+
* the final (possibly counter-augmented) value, so callers can derive parallel names in another
|
|
832
|
+
* namespace (e.g. plugin-faker deriving the matching plugin-ts type name).
|
|
833
|
+
*/
|
|
834
|
+
function resolveContentTypeVariants(entries, baseName) {
|
|
835
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
836
|
+
return entries.filter((entry) => entry.schema).map((entry) => {
|
|
837
|
+
const baseSuffix = getContentTypeSuffix(entry.contentType);
|
|
838
|
+
let suffix = baseSuffix;
|
|
839
|
+
let name = getPerContentTypeName(baseName, suffix);
|
|
840
|
+
let counter = 2;
|
|
841
|
+
while (usedNames.has(name)) {
|
|
842
|
+
suffix = `${baseSuffix}${counter++}`;
|
|
843
|
+
name = getPerContentTypeName(baseName, suffix);
|
|
844
|
+
}
|
|
845
|
+
usedNames.add(name);
|
|
846
|
+
return {
|
|
847
|
+
name,
|
|
848
|
+
suffix,
|
|
849
|
+
schema: entry.schema,
|
|
850
|
+
keysToOmit: entry.keysToOmit,
|
|
851
|
+
contentType: entry.contentType
|
|
852
|
+
};
|
|
853
|
+
});
|
|
854
|
+
}
|
|
807
855
|
function getOperationParameters(node, options = {}) {
|
|
808
856
|
const params = ast.caseParams(node.parameters, options.paramsCasing);
|
|
809
857
|
return {
|
|
@@ -814,6 +862,39 @@ function getOperationParameters(node, options = {}) {
|
|
|
814
862
|
};
|
|
815
863
|
}
|
|
816
864
|
//#endregion
|
|
865
|
+
//#region ../../internals/shared/src/group.ts
|
|
866
|
+
/**
|
|
867
|
+
* Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
|
|
868
|
+
* shared default naming so every plugin groups output consistently:
|
|
869
|
+
*
|
|
870
|
+
* - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
|
|
871
|
+
* - other groups use `${camelCase(group)}${suffix}` (e.g. `petController`).
|
|
872
|
+
*
|
|
873
|
+
* Returns `null` when grouping is disabled, matching the per-plugin convention.
|
|
874
|
+
*
|
|
875
|
+
* @param group - The user-supplied group option, or `undefined` to disable grouping.
|
|
876
|
+
* @param options.suffix - Appended to non-`path` group names, e.g. `'Controller'` or `'Requests'`.
|
|
877
|
+
* @param options.honorName - When `true`, a user-provided `group.name` overrides the default namer.
|
|
878
|
+
*
|
|
879
|
+
* @example
|
|
880
|
+
* ```ts
|
|
881
|
+
* createGroupConfig(group, { suffix: 'Controller' }) // plugin-ts, plugin-zod
|
|
882
|
+
* createGroupConfig(group, { suffix: 'Controller', honorName: true }) // plugin-faker, plugin-client, …
|
|
883
|
+
* createGroupConfig(group, { suffix: 'Requests', honorName: true }) // plugin-cypress, plugin-mcp
|
|
884
|
+
* ```
|
|
885
|
+
*/
|
|
886
|
+
function createGroupConfig(group, options) {
|
|
887
|
+
if (!group) return null;
|
|
888
|
+
const defaultName = (ctx) => {
|
|
889
|
+
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
|
|
890
|
+
return `${camelCase(ctx.group)}${options.suffix}`;
|
|
891
|
+
};
|
|
892
|
+
return {
|
|
893
|
+
...group,
|
|
894
|
+
name: options.honorName && group.name ? group.name : defaultName
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
//#endregion
|
|
817
898
|
//#region src/utils.ts
|
|
818
899
|
/**
|
|
819
900
|
* Collects JSDoc annotation strings for a schema node.
|
|
@@ -935,7 +1016,7 @@ function buildResponses(node, { resolver }) {
|
|
|
935
1016
|
});
|
|
936
1017
|
}
|
|
937
1018
|
function buildResponseUnion(node, { resolver }) {
|
|
938
|
-
const responsesWithSchema = node.responses.filter((res) => res.content?.
|
|
1019
|
+
const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
|
|
939
1020
|
if (responsesWithSchema.length === 0) return null;
|
|
940
1021
|
return ast.createSchema({
|
|
941
1022
|
type: "union",
|
|
@@ -1108,19 +1189,6 @@ const printerTs = ast.definePrinter((options) => {
|
|
|
1108
1189
|
});
|
|
1109
1190
|
//#endregion
|
|
1110
1191
|
//#region src/generators/typeGenerator.tsx
|
|
1111
|
-
function getContentTypeSuffix(contentType) {
|
|
1112
|
-
const baseType = contentType.split(";")[0].trim();
|
|
1113
|
-
if (baseType === "application/json") return "Json";
|
|
1114
|
-
if (baseType === "multipart/form-data") return "FormData";
|
|
1115
|
-
if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
|
|
1116
|
-
const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
1117
|
-
if (parts.length === 0) return "Unknown";
|
|
1118
|
-
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1119
|
-
}
|
|
1120
|
-
function getPerContentTypeName(dataName, suffix) {
|
|
1121
|
-
if (dataName.endsWith("Data")) return suffix.endsWith("Data") ? dataName.slice(0, -4) + suffix : `${dataName.slice(0, -4)}${suffix}Data`;
|
|
1122
|
-
return dataName + suffix;
|
|
1123
|
-
}
|
|
1124
1192
|
/**
|
|
1125
1193
|
* Built-in generator for `@kubb/plugin-ts`. Emits one TypeScript file per
|
|
1126
1194
|
* schema in the spec plus per-operation request, response, and parameter
|
|
@@ -1280,6 +1348,28 @@ const typeGenerator = defineGenerator({
|
|
|
1280
1348
|
printer: schemaPrinter
|
|
1281
1349
|
})] });
|
|
1282
1350
|
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Emits an individual type per content type plus a union alias under `baseName`.
|
|
1353
|
+
* Shared by the request body and multi-content-type responses.
|
|
1354
|
+
*/
|
|
1355
|
+
function buildContentTypeVariants(entries, baseName, decorate) {
|
|
1356
|
+
const variants = resolveContentTypeVariants(entries, baseName);
|
|
1357
|
+
const unionSchema = ast.createSchema({
|
|
1358
|
+
type: "union",
|
|
1359
|
+
members: variants.map((variant) => ast.createSchema({
|
|
1360
|
+
type: "ref",
|
|
1361
|
+
name: variant.name
|
|
1362
|
+
}))
|
|
1363
|
+
});
|
|
1364
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [variants.map((variant) => renderSchemaType({
|
|
1365
|
+
schema: decorate ? decorate(variant.schema) : variant.schema,
|
|
1366
|
+
name: variant.name,
|
|
1367
|
+
keysToOmit: variant.keysToOmit
|
|
1368
|
+
})), renderSchemaType({
|
|
1369
|
+
schema: unionSchema,
|
|
1370
|
+
name: baseName
|
|
1371
|
+
})] });
|
|
1372
|
+
}
|
|
1283
1373
|
const paramTypes = params.map((param) => renderSchemaType({
|
|
1284
1374
|
schema: param.schema,
|
|
1285
1375
|
name: resolver.resolveParamName(node, param)
|
|
@@ -1299,44 +1389,22 @@ const typeGenerator = defineGenerator({
|
|
|
1299
1389
|
keysToOmit: entry.keysToOmit
|
|
1300
1390
|
});
|
|
1301
1391
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
let individualName = getPerContentTypeName(dataName, baseSuffix);
|
|
1307
|
-
let counter = 2;
|
|
1308
|
-
while (usedNames.has(individualName)) individualName = getPerContentTypeName(dataName, `${baseSuffix}${counter++}`);
|
|
1309
|
-
usedNames.add(individualName);
|
|
1310
|
-
return {
|
|
1311
|
-
name: individualName,
|
|
1312
|
-
rendered: renderSchemaType({
|
|
1313
|
-
schema: {
|
|
1314
|
-
...entry.schema,
|
|
1315
|
-
description: node.requestBody.description ?? entry.schema.description
|
|
1316
|
-
},
|
|
1317
|
-
name: individualName,
|
|
1318
|
-
keysToOmit: entry.keysToOmit
|
|
1319
|
-
})
|
|
1320
|
-
};
|
|
1321
|
-
});
|
|
1322
|
-
const unionType = renderSchemaType({
|
|
1323
|
-
schema: ast.createSchema({
|
|
1324
|
-
type: "union",
|
|
1325
|
-
members: individualItems.map((item) => ast.createSchema({
|
|
1326
|
-
type: "ref",
|
|
1327
|
-
name: item.name
|
|
1328
|
-
}))
|
|
1329
|
-
}),
|
|
1330
|
-
name: dataName
|
|
1331
|
-
});
|
|
1332
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [individualItems.map((item) => item.rendered), unionType] });
|
|
1392
|
+
return buildContentTypeVariants(requestBodyContent, resolver.resolveDataName(node), (schema) => ({
|
|
1393
|
+
...schema,
|
|
1394
|
+
description: node.requestBody.description ?? schema.description
|
|
1395
|
+
}));
|
|
1333
1396
|
}
|
|
1334
1397
|
const requestType = buildRequestType();
|
|
1335
|
-
const responseTypes = node.responses.map((res) =>
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1398
|
+
const responseTypes = node.responses.map((res) => {
|
|
1399
|
+
const variants = (res.content ?? []).filter((entry) => entry.schema);
|
|
1400
|
+
if (variants.length > 1) return buildContentTypeVariants(variants, resolver.resolveResponseStatusName(node, res.statusCode));
|
|
1401
|
+
const primary = variants[0] ?? res.content?.[0];
|
|
1402
|
+
return renderSchemaType({
|
|
1403
|
+
schema: primary?.schema ?? null,
|
|
1404
|
+
name: resolver.resolveResponseStatusName(node, res.statusCode),
|
|
1405
|
+
keysToOmit: primary?.keysToOmit
|
|
1406
|
+
});
|
|
1407
|
+
});
|
|
1340
1408
|
const dataType = renderSchemaType({
|
|
1341
1409
|
schema: buildData({
|
|
1342
1410
|
...node,
|
|
@@ -1349,13 +1417,14 @@ const typeGenerator = defineGenerator({
|
|
|
1349
1417
|
name: resolver.resolveResponsesName(node)
|
|
1350
1418
|
});
|
|
1351
1419
|
function buildResponseType() {
|
|
1352
|
-
|
|
1420
|
+
const hasSchema = (res) => (res.content ?? []).some((entry) => entry.schema);
|
|
1421
|
+
if (!node.responses.some(hasSchema)) return null;
|
|
1353
1422
|
const responseName = resolver.resolveResponseName(node);
|
|
1354
|
-
const responsesWithSchema = node.responses.filter(
|
|
1355
|
-
if (new Set(responsesWithSchema.flatMap((res) => res.content
|
|
1423
|
+
const responsesWithSchema = node.responses.filter(hasSchema);
|
|
1424
|
+
if (new Set(responsesWithSchema.flatMap((res) => (res.content ?? []).flatMap((entry) => entry.schema ? adapter.getImports(entry.schema, (schemaName) => ({
|
|
1356
1425
|
name: resolveImportName(schemaName),
|
|
1357
1426
|
path: ""
|
|
1358
|
-
})).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : [])).has(responseName)) return null;
|
|
1427
|
+
})).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : []))).has(responseName)) return null;
|
|
1359
1428
|
return renderSchemaType({
|
|
1360
1429
|
schema: {
|
|
1361
1430
|
...buildResponseUnion(node, { resolver }),
|
|
@@ -1499,13 +1568,7 @@ const pluginTs = definePlugin((options) => {
|
|
|
1499
1568
|
path: "types",
|
|
1500
1569
|
barrelType: "named"
|
|
1501
1570
|
}, group, exclude = [], include, override = [], enumType = "asConst", enumTypeSuffix = "Key", enumKeyCasing = "none", optionalType = "questionToken", arrayType = "array", syntaxType = "type", paramsCasing, printer, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
|
|
1502
|
-
const groupConfig = group
|
|
1503
|
-
...group,
|
|
1504
|
-
name: (ctx) => {
|
|
1505
|
-
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
|
|
1506
|
-
return `${camelCase(ctx.group)}Controller`;
|
|
1507
|
-
}
|
|
1508
|
-
} : null;
|
|
1571
|
+
const groupConfig = createGroupConfig(group, { suffix: "Controller" });
|
|
1509
1572
|
return {
|
|
1510
1573
|
name: pluginTsName,
|
|
1511
1574
|
options,
|