@kubb/plugin-ts 5.0.0-beta.30 → 5.0.0-beta.33

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.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?.[0]?.schema);
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
- const dataName = resolver.resolveDataName(node);
1303
- const usedNames = /* @__PURE__ */ new Set();
1304
- const individualItems = requestBodyContent.filter((entry) => entry.schema).map((entry) => {
1305
- const baseSuffix = getContentTypeSuffix(entry.contentType);
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) => renderSchemaType({
1336
- schema: res.content?.[0]?.schema ?? null,
1337
- name: resolver.resolveResponseStatusName(node, res.statusCode),
1338
- keysToOmit: res.content?.[0]?.keysToOmit
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
- if (!node.responses.some((res) => res.content?.[0]?.schema)) return null;
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((res) => res.content?.[0]?.schema);
1355
- if (new Set(responsesWithSchema.flatMap((res) => res.content?.[0]?.schema ? adapter.getImports(res.content[0].schema, (schemaName) => ({
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,