@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 CHANGED
@@ -834,6 +834,54 @@ function Type({ name, node, printer, enumType, enumTypeSuffix, enumKeyCasing, re
834
834
  }
835
835
  //#endregion
836
836
  //#region ../../internals/shared/src/operation.ts
837
+ /**
838
+ * Maps a content type to the PascalCase suffix used to name per-content-type variants
839
+ * (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
840
+ */
841
+ function getContentTypeSuffix(contentType) {
842
+ const baseType = contentType.split(";")[0].trim();
843
+ if (baseType === "application/json") return "Json";
844
+ if (baseType === "multipart/form-data") return "FormData";
845
+ if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
846
+ const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
847
+ if (parts.length === 0) return "Unknown";
848
+ return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
849
+ }
850
+ /**
851
+ * Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
852
+ * (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
853
+ */
854
+ function getPerContentTypeName(baseName, suffix) {
855
+ if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
856
+ return baseName + suffix;
857
+ }
858
+ /**
859
+ * Resolves per-content-type variant names for a set of content entries, deduplicating suffix
860
+ * collisions with a numeric counter. Entries without a schema are skipped. The returned `suffix` is
861
+ * the final (possibly counter-augmented) value, so callers can derive parallel names in another
862
+ * namespace (e.g. plugin-faker deriving the matching plugin-ts type name).
863
+ */
864
+ function resolveContentTypeVariants(entries, baseName) {
865
+ const usedNames = /* @__PURE__ */ new Set();
866
+ return entries.filter((entry) => entry.schema).map((entry) => {
867
+ const baseSuffix = getContentTypeSuffix(entry.contentType);
868
+ let suffix = baseSuffix;
869
+ let name = getPerContentTypeName(baseName, suffix);
870
+ let counter = 2;
871
+ while (usedNames.has(name)) {
872
+ suffix = `${baseSuffix}${counter++}`;
873
+ name = getPerContentTypeName(baseName, suffix);
874
+ }
875
+ usedNames.add(name);
876
+ return {
877
+ name,
878
+ suffix,
879
+ schema: entry.schema,
880
+ keysToOmit: entry.keysToOmit,
881
+ contentType: entry.contentType
882
+ };
883
+ });
884
+ }
837
885
  function getOperationParameters(node, options = {}) {
838
886
  const params = _kubb_core.ast.caseParams(node.parameters, options.paramsCasing);
839
887
  return {
@@ -844,6 +892,39 @@ function getOperationParameters(node, options = {}) {
844
892
  };
845
893
  }
846
894
  //#endregion
895
+ //#region ../../internals/shared/src/group.ts
896
+ /**
897
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
898
+ * shared default naming so every plugin groups output consistently:
899
+ *
900
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
901
+ * - other groups use `${camelCase(group)}${suffix}` (e.g. `petController`).
902
+ *
903
+ * Returns `null` when grouping is disabled, matching the per-plugin convention.
904
+ *
905
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
906
+ * @param options.suffix - Appended to non-`path` group names, e.g. `'Controller'` or `'Requests'`.
907
+ * @param options.honorName - When `true`, a user-provided `group.name` overrides the default namer.
908
+ *
909
+ * @example
910
+ * ```ts
911
+ * createGroupConfig(group, { suffix: 'Controller' }) // plugin-ts, plugin-zod
912
+ * createGroupConfig(group, { suffix: 'Controller', honorName: true }) // plugin-faker, plugin-client, …
913
+ * createGroupConfig(group, { suffix: 'Requests', honorName: true }) // plugin-cypress, plugin-mcp
914
+ * ```
915
+ */
916
+ function createGroupConfig(group, options) {
917
+ if (!group) return null;
918
+ const defaultName = (ctx) => {
919
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
920
+ return `${camelCase(ctx.group)}${options.suffix}`;
921
+ };
922
+ return {
923
+ ...group,
924
+ name: options.honorName && group.name ? group.name : defaultName
925
+ };
926
+ }
927
+ //#endregion
847
928
  //#region src/utils.ts
848
929
  /**
849
930
  * Collects JSDoc annotation strings for a schema node.
@@ -965,7 +1046,7 @@ function buildResponses(node, { resolver }) {
965
1046
  });
966
1047
  }
967
1048
  function buildResponseUnion(node, { resolver }) {
968
- const responsesWithSchema = node.responses.filter((res) => res.content?.[0]?.schema);
1049
+ const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
969
1050
  if (responsesWithSchema.length === 0) return null;
970
1051
  return _kubb_core.ast.createSchema({
971
1052
  type: "union",
@@ -1138,19 +1219,6 @@ const printerTs = _kubb_core.ast.definePrinter((options) => {
1138
1219
  });
1139
1220
  //#endregion
1140
1221
  //#region src/generators/typeGenerator.tsx
1141
- function getContentTypeSuffix(contentType) {
1142
- const baseType = contentType.split(";")[0].trim();
1143
- if (baseType === "application/json") return "Json";
1144
- if (baseType === "multipart/form-data") return "FormData";
1145
- if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
1146
- const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
1147
- if (parts.length === 0) return "Unknown";
1148
- return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1149
- }
1150
- function getPerContentTypeName(dataName, suffix) {
1151
- if (dataName.endsWith("Data")) return suffix.endsWith("Data") ? dataName.slice(0, -4) + suffix : `${dataName.slice(0, -4)}${suffix}Data`;
1152
- return dataName + suffix;
1153
- }
1154
1222
  /**
1155
1223
  * Built-in generator for `@kubb/plugin-ts`. Emits one TypeScript file per
1156
1224
  * schema in the spec plus per-operation request, response, and parameter
@@ -1310,6 +1378,28 @@ const typeGenerator = (0, _kubb_core.defineGenerator)({
1310
1378
  printer: schemaPrinter
1311
1379
  })] });
1312
1380
  }
1381
+ /**
1382
+ * Emits an individual type per content type plus a union alias under `baseName`.
1383
+ * Shared by the request body and multi-content-type responses.
1384
+ */
1385
+ function buildContentTypeVariants(entries, baseName, decorate) {
1386
+ const variants = resolveContentTypeVariants(entries, baseName);
1387
+ const unionSchema = _kubb_core.ast.createSchema({
1388
+ type: "union",
1389
+ members: variants.map((variant) => _kubb_core.ast.createSchema({
1390
+ type: "ref",
1391
+ name: variant.name
1392
+ }))
1393
+ });
1394
+ return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [variants.map((variant) => renderSchemaType({
1395
+ schema: decorate ? decorate(variant.schema) : variant.schema,
1396
+ name: variant.name,
1397
+ keysToOmit: variant.keysToOmit
1398
+ })), renderSchemaType({
1399
+ schema: unionSchema,
1400
+ name: baseName
1401
+ })] });
1402
+ }
1313
1403
  const paramTypes = params.map((param) => renderSchemaType({
1314
1404
  schema: param.schema,
1315
1405
  name: resolver.resolveParamName(node, param)
@@ -1329,44 +1419,22 @@ const typeGenerator = (0, _kubb_core.defineGenerator)({
1329
1419
  keysToOmit: entry.keysToOmit
1330
1420
  });
1331
1421
  }
1332
- const dataName = resolver.resolveDataName(node);
1333
- const usedNames = /* @__PURE__ */ new Set();
1334
- const individualItems = requestBodyContent.filter((entry) => entry.schema).map((entry) => {
1335
- const baseSuffix = getContentTypeSuffix(entry.contentType);
1336
- let individualName = getPerContentTypeName(dataName, baseSuffix);
1337
- let counter = 2;
1338
- while (usedNames.has(individualName)) individualName = getPerContentTypeName(dataName, `${baseSuffix}${counter++}`);
1339
- usedNames.add(individualName);
1340
- return {
1341
- name: individualName,
1342
- rendered: renderSchemaType({
1343
- schema: {
1344
- ...entry.schema,
1345
- description: node.requestBody.description ?? entry.schema.description
1346
- },
1347
- name: individualName,
1348
- keysToOmit: entry.keysToOmit
1349
- })
1350
- };
1351
- });
1352
- const unionType = renderSchemaType({
1353
- schema: _kubb_core.ast.createSchema({
1354
- type: "union",
1355
- members: individualItems.map((item) => _kubb_core.ast.createSchema({
1356
- type: "ref",
1357
- name: item.name
1358
- }))
1359
- }),
1360
- name: dataName
1361
- });
1362
- return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [individualItems.map((item) => item.rendered), unionType] });
1422
+ return buildContentTypeVariants(requestBodyContent, resolver.resolveDataName(node), (schema) => ({
1423
+ ...schema,
1424
+ description: node.requestBody.description ?? schema.description
1425
+ }));
1363
1426
  }
1364
1427
  const requestType = buildRequestType();
1365
- const responseTypes = node.responses.map((res) => renderSchemaType({
1366
- schema: res.content?.[0]?.schema ?? null,
1367
- name: resolver.resolveResponseStatusName(node, res.statusCode),
1368
- keysToOmit: res.content?.[0]?.keysToOmit
1369
- }));
1428
+ const responseTypes = node.responses.map((res) => {
1429
+ const variants = (res.content ?? []).filter((entry) => entry.schema);
1430
+ if (variants.length > 1) return buildContentTypeVariants(variants, resolver.resolveResponseStatusName(node, res.statusCode));
1431
+ const primary = variants[0] ?? res.content?.[0];
1432
+ return renderSchemaType({
1433
+ schema: primary?.schema ?? null,
1434
+ name: resolver.resolveResponseStatusName(node, res.statusCode),
1435
+ keysToOmit: primary?.keysToOmit
1436
+ });
1437
+ });
1370
1438
  const dataType = renderSchemaType({
1371
1439
  schema: buildData({
1372
1440
  ...node,
@@ -1379,13 +1447,14 @@ const typeGenerator = (0, _kubb_core.defineGenerator)({
1379
1447
  name: resolver.resolveResponsesName(node)
1380
1448
  });
1381
1449
  function buildResponseType() {
1382
- if (!node.responses.some((res) => res.content?.[0]?.schema)) return null;
1450
+ const hasSchema = (res) => (res.content ?? []).some((entry) => entry.schema);
1451
+ if (!node.responses.some(hasSchema)) return null;
1383
1452
  const responseName = resolver.resolveResponseName(node);
1384
- const responsesWithSchema = node.responses.filter((res) => res.content?.[0]?.schema);
1385
- if (new Set(responsesWithSchema.flatMap((res) => res.content?.[0]?.schema ? adapter.getImports(res.content[0].schema, (schemaName) => ({
1453
+ const responsesWithSchema = node.responses.filter(hasSchema);
1454
+ if (new Set(responsesWithSchema.flatMap((res) => (res.content ?? []).flatMap((entry) => entry.schema ? adapter.getImports(entry.schema, (schemaName) => ({
1386
1455
  name: resolveImportName(schemaName),
1387
1456
  path: ""
1388
- })).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : [])).has(responseName)) return null;
1457
+ })).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : []))).has(responseName)) return null;
1389
1458
  return renderSchemaType({
1390
1459
  schema: {
1391
1460
  ...buildResponseUnion(node, { resolver }),
@@ -1529,13 +1598,7 @@ const pluginTs = (0, _kubb_core.definePlugin)((options) => {
1529
1598
  path: "types",
1530
1599
  barrelType: "named"
1531
1600
  }, 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;
1532
- const groupConfig = group ? {
1533
- ...group,
1534
- name: (ctx) => {
1535
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
1536
- return `${camelCase(ctx.group)}Controller`;
1537
- }
1538
- } : null;
1601
+ const groupConfig = createGroupConfig(group, { suffix: "Controller" });
1539
1602
  return {
1540
1603
  name: pluginTsName,
1541
1604
  options,