@kubb/plugin-ts 5.0.0-beta.3 → 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.js CHANGED
@@ -1,6 +1,6 @@
1
- import "./chunk--u3MIqq1.js";
2
- import { safePrint } from "@kubb/parser-ts";
3
- import { File, jsxRenderer } from "@kubb/renderer-jsx";
1
+ import { t as __name } from "./chunk--u3MIqq1.js";
2
+ import { parserTs } from "@kubb/parser-ts";
3
+ import { File, jsxRendererSync } from "@kubb/renderer-jsx";
4
4
  import { ast, defineGenerator, definePlugin, defineResolver } from "@kubb/core";
5
5
  import { isNumber } from "remeda";
6
6
  import ts from "typescript";
@@ -141,6 +141,129 @@ function stringify(value) {
141
141
  return JSON.stringify(trimQuotes(value.toString()));
142
142
  }
143
143
  //#endregion
144
+ //#region ../../internals/utils/src/reserved.ts
145
+ /**
146
+ * JavaScript and Java reserved words.
147
+ * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
148
+ */
149
+ const reservedWords = new Set([
150
+ "abstract",
151
+ "arguments",
152
+ "boolean",
153
+ "break",
154
+ "byte",
155
+ "case",
156
+ "catch",
157
+ "char",
158
+ "class",
159
+ "const",
160
+ "continue",
161
+ "debugger",
162
+ "default",
163
+ "delete",
164
+ "do",
165
+ "double",
166
+ "else",
167
+ "enum",
168
+ "eval",
169
+ "export",
170
+ "extends",
171
+ "false",
172
+ "final",
173
+ "finally",
174
+ "float",
175
+ "for",
176
+ "function",
177
+ "goto",
178
+ "if",
179
+ "implements",
180
+ "import",
181
+ "in",
182
+ "instanceof",
183
+ "int",
184
+ "interface",
185
+ "let",
186
+ "long",
187
+ "native",
188
+ "new",
189
+ "null",
190
+ "package",
191
+ "private",
192
+ "protected",
193
+ "public",
194
+ "return",
195
+ "short",
196
+ "static",
197
+ "super",
198
+ "switch",
199
+ "synchronized",
200
+ "this",
201
+ "throw",
202
+ "throws",
203
+ "transient",
204
+ "true",
205
+ "try",
206
+ "typeof",
207
+ "var",
208
+ "void",
209
+ "volatile",
210
+ "while",
211
+ "with",
212
+ "yield",
213
+ "Array",
214
+ "Date",
215
+ "hasOwnProperty",
216
+ "Infinity",
217
+ "isFinite",
218
+ "isNaN",
219
+ "isPrototypeOf",
220
+ "length",
221
+ "Math",
222
+ "name",
223
+ "NaN",
224
+ "Number",
225
+ "Object",
226
+ "prototype",
227
+ "String",
228
+ "toString",
229
+ "undefined",
230
+ "valueOf"
231
+ ]);
232
+ /**
233
+ * Returns `true` when `name` is a syntactically valid JavaScript variable name.
234
+ *
235
+ * @example
236
+ * ```ts
237
+ * isValidVarName('status') // true
238
+ * isValidVarName('class') // false (reserved word)
239
+ * isValidVarName('42foo') // false (starts with digit)
240
+ * ```
241
+ */
242
+ function isValidVarName(name) {
243
+ if (!name || reservedWords.has(name)) return false;
244
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
245
+ }
246
+ /**
247
+ * Returns `name` when it's a syntactically valid JavaScript variable name,
248
+ * otherwise prefixes it with `_` so the result is a valid identifier.
249
+ *
250
+ * Useful for sanitizing OpenAPI schema names or operation IDs that start with
251
+ * a digit (e.g. `409`, `504AccountCancel`) before using them as exported
252
+ * variable, type, or function names.
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * ensureValidVarName('409') // '_409'
257
+ * ensureValidVarName('504AccountCancel') // '_504AccountCancel'
258
+ * ensureValidVarName('Pet') // 'Pet'
259
+ * ensureValidVarName('class') // '_class'
260
+ * ```
261
+ */
262
+ function ensureValidVarName(name) {
263
+ if (!name || isValidVarName(name)) return name;
264
+ return `_${name}`;
265
+ }
266
+ //#endregion
144
267
  //#region src/constants.ts
145
268
  /**
146
269
  * `optionalType` values that cause a property's type to include `| undefined`.
@@ -203,6 +326,10 @@ const syntaxKind = {
203
326
  literalType: SyntaxKind.LiteralType,
204
327
  stringLiteral: SyntaxKind.StringLiteral
205
328
  };
329
+ function isNonNullable$1(value) {
330
+ return value !== null && value !== void 0;
331
+ }
332
+ __name(isNonNullable$1, "isNonNullable");
206
333
  function isValidIdentifier(str) {
207
334
  if (!str.length || str.trim() !== str) return false;
208
335
  const node = ts.parseIsolatedEntityName(str, ts.ScriptTarget.Latest);
@@ -272,7 +399,7 @@ function createUnionDeclaration({ nodes, withParentheses }) {
272
399
  * Supports optional markers, readonly modifiers, and type annotations.
273
400
  */
274
401
  function createPropertySignature({ readOnly, modifiers = [], name, questionToken, type }) {
275
- return factory.createPropertySignature([...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : void 0].filter(Boolean), propertyName(name), createQuestionToken(questionToken), type);
402
+ return factory.createPropertySignature([...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : void 0].filter((modifier) => modifier !== void 0), propertyName(name), createQuestionToken(questionToken), type);
276
403
  }
277
404
  /**
278
405
  * Creates a function parameter declaration with optional markers, rest parameters, and type annotations.
@@ -370,8 +497,8 @@ function createEnumDeclaration({ type = "enum", name, typeName, enums, enumKeyCa
370
497
  }
371
498
  if (typeof value === "boolean") return factory.createLiteralTypeNode(value ? factory.createTrue() : factory.createFalse());
372
499
  if (value) return factory.createLiteralTypeNode(factory.createStringLiteral(value.toString()));
373
- }).filter(Boolean)))];
374
- if (type === "enum" || type === "constEnum") return [void 0, factory.createEnumDeclaration([factory.createToken(ts.SyntaxKind.ExportKeyword), type === "constEnum" ? factory.createToken(ts.SyntaxKind.ConstKeyword) : void 0].filter(Boolean), factory.createIdentifier(typeName), enums.map(([key, value]) => {
500
+ }).filter((node) => node !== void 0)))];
501
+ if (type === "enum" || type === "constEnum") return [void 0, factory.createEnumDeclaration([factory.createToken(ts.SyntaxKind.ExportKeyword), type === "constEnum" ? factory.createToken(ts.SyntaxKind.ConstKeyword) : void 0].filter((modifier) => modifier !== void 0), factory.createIdentifier(typeName), enums.map(([key, value]) => {
375
502
  let initializer = factory.createStringLiteral(value?.toString());
376
503
  if (Number.parseInt(value.toString(), 10) === value && isNumber(Number.parseInt(value.toString(), 10))) if (value < 0) initializer = factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value)));
377
504
  else initializer = factory.createNumericLiteral(value);
@@ -384,7 +511,7 @@ function createEnumDeclaration({ type = "enum", name, typeName, enums, enumKeyCa
384
511
  const casingKey = applyEnumKeyCasing(key.toString(), enumKeyCasing);
385
512
  return factory.createEnumMember(propertyName(casingKey), initializer);
386
513
  }
387
- }).filter(Boolean))];
514
+ }).filter((member) => member !== void 0))];
388
515
  const identifierName = name;
389
516
  if (enums.length === 0) return [void 0, factory.createTypeAliasDeclaration([factory.createToken(ts.SyntaxKind.ExportKeyword)], factory.createIdentifier(typeName), void 0, factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword))];
390
517
  return [factory.createVariableStatement([factory.createToken(ts.SyntaxKind.ExportKeyword)], factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(identifierName), void 0, void 0, factory.createAsExpression(factory.createObjectLiteralExpression(enums.map(([key, value]) => {
@@ -396,7 +523,7 @@ function createEnumDeclaration({ type = "enum", name, typeName, enums, enumKeyCa
396
523
  const casingKey = applyEnumKeyCasing(key.toString(), enumKeyCasing);
397
524
  return factory.createPropertyAssignment(propertyName(casingKey), initializer);
398
525
  }
399
- }).filter(Boolean), true), factory.createTypeReferenceNode(factory.createIdentifier("const"), void 0)))], ts.NodeFlags.Const)), factory.createTypeAliasDeclaration([factory.createToken(ts.SyntaxKind.ExportKeyword)], factory.createIdentifier(typeName), void 0, factory.createIndexedAccessTypeNode(factory.createParenthesizedType(factory.createTypeQueryNode(factory.createIdentifier(identifierName), void 0)), factory.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, factory.createTypeQueryNode(factory.createIdentifier(identifierName), void 0))))];
526
+ }).filter((property) => property !== void 0), true), factory.createTypeReferenceNode(factory.createIdentifier("const"), void 0)))], ts.NodeFlags.Const)), factory.createTypeAliasDeclaration([factory.createToken(ts.SyntaxKind.ExportKeyword)], factory.createIdentifier(typeName), void 0, factory.createIndexedAccessTypeNode(factory.createParenthesizedType(factory.createTypeQueryNode(factory.createIdentifier(identifierName), void 0)), factory.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, factory.createTypeQueryNode(factory.createIdentifier(identifierName), void 0))))];
400
527
  }
401
528
  /**
402
529
  * Creates a TypeScript `Omit<T, Keys>` type reference node.
@@ -536,14 +663,14 @@ function dateOrStringNode(node) {
536
663
  * Maps an array of `SchemaNode`s through the printer, filtering out `null` and `undefined` results.
537
664
  */
538
665
  function buildMemberNodes(members, print) {
539
- return (members ?? []).map(print).filter(Boolean);
666
+ return (members ?? []).map(print).filter(isNonNullable$1);
540
667
  }
541
668
  /**
542
669
  * Builds a TypeScript tuple type node from an array schema's `items`,
543
670
  * applying min/max slice and optional/rest element rules.
544
671
  */
545
672
  function buildTupleNode(node, print) {
546
- let items = (node.items ?? []).map(print).filter(Boolean);
673
+ let items = (node.items ?? []).map(print).filter(isNonNullable$1);
547
674
  const restNode = node.rest ? print(node.rest) ?? void 0 : void 0;
548
675
  const { min, max } = node;
549
676
  if (max !== void 0) {
@@ -630,13 +757,13 @@ function Enum({ node, enumType, enumTypeSuffix, enumKeyCasing, resolver }) {
630
757
  isExportable: true,
631
758
  isIndexable: true,
632
759
  isTypeOnly: false,
633
- children: safePrint(nameNode)
760
+ children: parserTs.print(nameNode)
634
761
  }), /* @__PURE__ */ jsx(File.Source, {
635
762
  name: typeName,
636
763
  isIndexable: true,
637
764
  isExportable: ENUM_TYPES_WITH_RUNTIME_VALUE.has(enumType),
638
765
  isTypeOnly: ENUM_TYPES_WITH_TYPE_ONLY.has(enumType),
639
- children: safePrint(typeNode)
766
+ children: parserTs.print(typeNode)
640
767
  })] });
641
768
  }
642
769
  //#endregion
@@ -676,6 +803,98 @@ function Type({ name, node, printer, enumType, enumTypeSuffix, enumKeyCasing, re
676
803
  })] });
677
804
  }
678
805
  //#endregion
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
+ }
855
+ function getOperationParameters(node, options = {}) {
856
+ const params = ast.caseParams(node.parameters, options.paramsCasing);
857
+ return {
858
+ path: params.filter((param) => param.in === "path"),
859
+ query: params.filter((param) => param.in === "query"),
860
+ header: params.filter((param) => param.in === "header"),
861
+ cookie: params.filter((param) => param.in === "cookie")
862
+ };
863
+ }
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
679
898
  //#region src/utils.ts
680
899
  /**
681
900
  * Collects JSDoc annotation strings for a schema node.
@@ -687,15 +906,18 @@ function Type({ name, node, printer, enumType, enumTypeSuffix, enumKeyCasing, re
687
906
  function buildPropertyJSDocComments(schema) {
688
907
  const meta = ast.syncSchemaRef(schema);
689
908
  const isArray = meta?.primitive === "array";
909
+ const hasDescription = meta && "description" in meta && meta.description;
910
+ const formatComment = meta && "format" in meta && meta.format ? hasDescription ? [" ", `Format: \`${meta.format}\``] : ["@description", `Format: \`${meta.format}\``] : [];
690
911
  return [
691
- meta && "description" in meta && meta.description ? `@description ${jsStringEscape(meta.description)}` : void 0,
692
- meta && "deprecated" in meta && meta.deprecated ? "@deprecated" : void 0,
693
- !isArray && meta && "min" in meta && meta.min !== void 0 ? `@minLength ${meta.min}` : void 0,
694
- !isArray && meta && "max" in meta && meta.max !== void 0 ? `@maxLength ${meta.max}` : void 0,
695
- meta && "pattern" in meta && meta.pattern ? `@pattern ${meta.pattern}` : void 0,
696
- meta && "default" in meta && meta.default !== void 0 ? `@default ${"primitive" in meta && meta.primitive === "string" ? stringify(meta.default) : meta.default}` : void 0,
697
- meta && "example" in meta && meta.example !== void 0 ? `@example ${meta.example}` : void 0,
698
- meta && "primitive" in meta && meta.primitive ? [`@type ${meta.primitive}`, "optional" in schema && schema.optional ? " | undefined" : void 0].filter(Boolean).join("") : void 0
912
+ hasDescription ? `@description ${jsStringEscape(meta.description)}` : null,
913
+ ...formatComment,
914
+ meta && "deprecated" in meta && meta.deprecated ? "@deprecated" : null,
915
+ !isArray && meta && "min" in meta && meta.min !== void 0 ? `@minLength ${meta.min}` : null,
916
+ !isArray && meta && "max" in meta && meta.max !== void 0 ? `@maxLength ${meta.max}` : null,
917
+ meta && "pattern" in meta && meta.pattern ? `@pattern ${meta.pattern}` : null,
918
+ meta && "default" in meta && meta.default !== void 0 ? `@default ${"primitive" in meta && meta.primitive === "string" ? stringify(meta.default) : meta.default}` : null,
919
+ meta && "example" in meta && meta.example !== void 0 ? `@example ${meta.example}` : null,
920
+ meta && "primitive" in meta && meta.primitive ? [`@type ${meta.primitive}`, "optional" in schema && schema.optional ? " | undefined" : null].filter(Boolean).join("") : null
699
921
  ].filter(Boolean);
700
922
  }
701
923
  function buildParams(node, { params, resolver }) {
@@ -712,9 +934,7 @@ function buildParams(node, { params, resolver }) {
712
934
  });
713
935
  }
714
936
  function buildData(node, { resolver }) {
715
- const pathParams = node.parameters.filter((p) => p.in === "path");
716
- const queryParams = node.parameters.filter((p) => p.in === "query");
717
- const headerParams = node.parameters.filter((p) => p.in === "header");
937
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
718
938
  return ast.createSchema({
719
939
  type: "object",
720
940
  deprecated: node.deprecated,
@@ -796,7 +1016,7 @@ function buildResponses(node, { resolver }) {
796
1016
  });
797
1017
  }
798
1018
  function buildResponseUnion(node, { resolver }) {
799
- const responsesWithSchema = node.responses.filter((res) => res.schema);
1019
+ const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
800
1020
  if (responsesWithSchema.length === 0) return null;
801
1021
  return ast.createSchema({
802
1022
  type: "union",
@@ -808,6 +1028,9 @@ function buildResponseUnion(node, { resolver }) {
808
1028
  }
809
1029
  //#endregion
810
1030
  //#region src/printers/printerTs.ts
1031
+ function isNonNullable(value) {
1032
+ return value !== null && value !== void 0;
1033
+ }
811
1034
  /**
812
1035
  * TypeScript type printer built with `definePrinter`.
813
1036
  *
@@ -860,7 +1083,7 @@ const printerTs = ast.definePrinter((options) => {
860
1083
  date: dateOrStringNode,
861
1084
  time: dateOrStringNode,
862
1085
  ref(node) {
863
- if (!node.name) return;
1086
+ if (!node.name) return null;
864
1087
  const refName = node.ref ? ast.extractRefName(node.ref) ?? node.name : node.name;
865
1088
  return createTypeReferenceNode(node.ref && ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enumType) && this.options.enumTypeSuffix && this.options.enumSchemaNames?.has(refName) ? this.options.resolver.resolveEnumKeyName({ name: refName }, this.options.enumTypeSuffix) : node.ref ? this.options.resolver.default(refName, "type") : refName, void 0);
866
1089
  },
@@ -868,7 +1091,7 @@ const printerTs = ast.definePrinter((options) => {
868
1091
  const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? [];
869
1092
  if (this.options.enumType === "inlineLiteral" || !node.name) return createUnionDeclaration({
870
1093
  withParentheses: true,
871
- nodes: values.filter((v) => v !== null && v !== void 0).map((value) => constToTypeNode(value, typeof value)).filter(Boolean)
1094
+ nodes: values.filter((v) => v !== null && v !== void 0).map((value) => constToTypeNode(value, typeof value)).filter(isNonNullable)
872
1095
  }) ?? void 0;
873
1096
  return createTypeReferenceNode(ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enumType) && this.options.enumTypeSuffix ? this.options.resolver.resolveEnumKeyName(node, this.options.enumTypeSuffix) : this.options.resolver.default(node.name, "type"), void 0);
874
1097
  },
@@ -886,7 +1109,7 @@ const printerTs = ast.definePrinter((options) => {
886
1109
  withParentheses: true
887
1110
  });
888
1111
  return this.transform(m);
889
- }).filter(Boolean)
1112
+ }).filter(isNonNullable)
890
1113
  }) ?? void 0;
891
1114
  return createUnionDeclaration({
892
1115
  withParentheses: true,
@@ -897,16 +1120,16 @@ const printerTs = ast.definePrinter((options) => {
897
1120
  return createIntersectionDeclaration({
898
1121
  withParentheses: true,
899
1122
  nodes: buildMemberNodes(node.members, this.transform)
900
- }) ?? void 0;
1123
+ }) ?? null;
901
1124
  },
902
1125
  array(node) {
903
1126
  return createArrayDeclaration({
904
- nodes: (node.items ?? []).map((item) => this.transform(item)).filter(Boolean),
1127
+ nodes: (node.items ?? []).map((item) => this.transform(item)).filter(isNonNullable),
905
1128
  arrayType: this.options.arrayType
906
- }) ?? void 0;
1129
+ }) ?? null;
907
1130
  },
908
1131
  tuple(node) {
909
- return buildTupleNode(node, this.transform);
1132
+ return buildTupleNode(node, this.transform) ?? null;
910
1133
  },
911
1134
  object(node) {
912
1135
  const { transform, options } = this;
@@ -933,46 +1156,54 @@ const printerTs = ast.definePrinter((options) => {
933
1156
  },
934
1157
  print(node) {
935
1158
  const { name, syntaxType = "type", description, keysToOmit } = this.options;
936
- let base = this.transform(node);
937
- if (!base) return null;
1159
+ const transformed = this.transform(node);
1160
+ if (!transformed) return null;
938
1161
  const meta = ast.syncSchemaRef(node);
939
1162
  if (!name) {
940
- if (meta.nullable) base = createUnionDeclaration({ nodes: [base, keywordTypeNodes.null] });
941
- if ((meta.nullish || meta.optional) && addsUndefined) base = createUnionDeclaration({ nodes: [base, keywordTypeNodes.undefined] });
942
- return safePrint(base);
1163
+ const withNullable = meta.nullable ? createUnionDeclaration({ nodes: [transformed, keywordTypeNodes.null] }) : transformed;
1164
+ const result = (meta.nullish || meta.optional) && addsUndefined ? createUnionDeclaration({ nodes: [withNullable, keywordTypeNodes.undefined] }) : withNullable;
1165
+ return parserTs.print(result);
943
1166
  }
944
- let inner = keysToOmit?.length ? createOmitDeclaration({
945
- keys: keysToOmit,
946
- type: base,
947
- nonNullable: true
948
- }) : base;
949
- if (meta.nullable) inner = createUnionDeclaration({ nodes: [inner, keywordTypeNodes.null] });
950
- if (meta.nullish || meta.optional) inner = createUnionDeclaration({ nodes: [inner, keywordTypeNodes.undefined] });
951
- const useTypeGeneration = syntaxType === "type" || inner.kind === syntaxKind.union || !!keysToOmit?.length;
952
- return safePrint(createTypeDeclaration({
1167
+ const inner = (() => {
1168
+ const omitted = keysToOmit?.length ? createOmitDeclaration({
1169
+ keys: keysToOmit,
1170
+ type: transformed,
1171
+ nonNullable: true
1172
+ }) : transformed;
1173
+ const withNullable = meta.nullable ? createUnionDeclaration({ nodes: [omitted, keywordTypeNodes.null] }) : omitted;
1174
+ return meta.nullish || meta.optional ? createUnionDeclaration({ nodes: [withNullable, keywordTypeNodes.undefined] }) : withNullable;
1175
+ })();
1176
+ const typeNode = createTypeDeclaration({
953
1177
  name,
954
1178
  isExportable: true,
955
1179
  type: inner,
956
- syntax: useTypeGeneration ? "type" : "interface",
1180
+ syntax: syntaxType === "type" || inner.kind === syntaxKind.union || !!keysToOmit?.length ? "type" : "interface",
957
1181
  comments: buildPropertyJSDocComments({
958
1182
  ...meta,
959
1183
  description
960
1184
  })
961
- }));
1185
+ });
1186
+ return parserTs.print(typeNode);
962
1187
  }
963
1188
  };
964
1189
  });
965
1190
  //#endregion
966
1191
  //#region src/generators/typeGenerator.tsx
1192
+ /**
1193
+ * Built-in generator for `@kubb/plugin-ts`. Emits one TypeScript file per
1194
+ * schema in the spec plus per-operation request, response, and parameter
1195
+ * types. Drop-replace with a custom `Generator<PluginTs>` to change how
1196
+ * TypeScript output is produced.
1197
+ */
967
1198
  const typeGenerator = defineGenerator({
968
1199
  name: "typescript",
969
- renderer: jsxRenderer,
1200
+ renderer: jsxRendererSync,
970
1201
  schema(node, ctx) {
971
1202
  const { enumType, enumTypeSuffix, enumKeyCasing, syntaxType, optionalType, arrayType, output, group, printer } = ctx.options;
972
1203
  const { adapter, config, resolver, root } = ctx;
973
1204
  if (!node.name) return;
974
1205
  const mode = ctx.getMode(output);
975
- const enumSchemaNames = new Set((adapter.inputNode?.schemas ?? []).filter((s) => ast.narrowSchema(s, ast.schemaTypes.enum) && s.name).map((s) => s.name));
1206
+ const enumSchemaNames = new Set(ctx.meta.enumNames);
976
1207
  function resolveImportName(schemaName) {
977
1208
  if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) return resolver.resolveEnumKeyName({ name: schemaName }, enumTypeSuffix);
978
1209
  return resolver.resolveTypeName(schemaName);
@@ -985,7 +1216,7 @@ const typeGenerator = defineGenerator({
985
1216
  }, {
986
1217
  root,
987
1218
  output,
988
- group
1219
+ group: group ?? void 0
989
1220
  }).path
990
1221
  }));
991
1222
  const isEnumSchema = !!ast.narrowSchema(node, ast.schemaTypes.enum);
@@ -997,7 +1228,7 @@ const typeGenerator = defineGenerator({
997
1228
  }, {
998
1229
  root,
999
1230
  output,
1000
- group
1231
+ group: group ?? void 0
1001
1232
  })
1002
1233
  };
1003
1234
  const schemaPrinter = printerTs({
@@ -1016,13 +1247,21 @@ const typeGenerator = defineGenerator({
1016
1247
  baseName: meta.file.baseName,
1017
1248
  path: meta.file.path,
1018
1249
  meta: meta.file.meta,
1019
- banner: resolver.resolveBanner(adapter.inputNode, {
1250
+ banner: resolver.resolveBanner(ctx.meta, {
1020
1251
  output,
1021
- config
1252
+ config,
1253
+ file: {
1254
+ path: meta.file.path,
1255
+ baseName: meta.file.baseName
1256
+ }
1022
1257
  }),
1023
- footer: resolver.resolveFooter(adapter.inputNode, {
1258
+ footer: resolver.resolveFooter(ctx.meta, {
1024
1259
  output,
1025
- config
1260
+ config,
1261
+ file: {
1262
+ path: meta.file.path,
1263
+ baseName: meta.file.baseName
1264
+ }
1026
1265
  }),
1027
1266
  children: [mode === "split" && imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
1028
1267
  root: meta.file.path,
@@ -1057,9 +1296,9 @@ const typeGenerator = defineGenerator({
1057
1296
  }, {
1058
1297
  root,
1059
1298
  output,
1060
- group
1299
+ group: group ?? void 0
1061
1300
  }) };
1062
- const enumSchemaNames = new Set((adapter.inputNode?.schemas ?? []).filter((s) => ast.narrowSchema(s, ast.schemaTypes.enum) && s.name).map((s) => s.name));
1301
+ const enumSchemaNames = new Set(ctx.meta.enumNames);
1063
1302
  function resolveImportName(schemaName) {
1064
1303
  if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) return resolver.resolveEnumKeyName({ name: schemaName }, enumTypeSuffix);
1065
1304
  return resolver.resolveTypeName(schemaName);
@@ -1074,7 +1313,7 @@ const typeGenerator = defineGenerator({
1074
1313
  }, {
1075
1314
  root,
1076
1315
  output,
1077
- group
1316
+ group: group ?? void 0
1078
1317
  }).path
1079
1318
  }));
1080
1319
  const schemaPrinter = printerTs({
@@ -1109,23 +1348,63 @@ const typeGenerator = defineGenerator({
1109
1348
  printer: schemaPrinter
1110
1349
  })] });
1111
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
+ }
1112
1373
  const paramTypes = params.map((param) => renderSchemaType({
1113
1374
  schema: param.schema,
1114
1375
  name: resolver.resolveParamName(node, param)
1115
1376
  }));
1116
- const requestType = node.requestBody?.content?.[0]?.schema ? renderSchemaType({
1117
- schema: {
1118
- ...node.requestBody.content[0].schema,
1119
- description: node.requestBody.description ?? node.requestBody.content[0].schema.description
1120
- },
1121
- name: resolver.resolveDataName(node),
1122
- keysToOmit: node.requestBody.content[0].keysToOmit
1123
- }) : null;
1124
- const responseTypes = node.responses.map((res) => renderSchemaType({
1125
- schema: res.schema,
1126
- name: resolver.resolveResponseStatusName(node, res.statusCode),
1127
- keysToOmit: res.keysToOmit
1128
- }));
1377
+ const requestBodyContent = node.requestBody?.content ?? [];
1378
+ function buildRequestType() {
1379
+ if (requestBodyContent.length === 0) return null;
1380
+ if (requestBodyContent.length === 1) {
1381
+ const entry = requestBodyContent[0];
1382
+ if (!entry.schema) return null;
1383
+ return renderSchemaType({
1384
+ schema: {
1385
+ ...entry.schema,
1386
+ description: node.requestBody.description ?? entry.schema.description
1387
+ },
1388
+ name: resolver.resolveDataName(node),
1389
+ keysToOmit: entry.keysToOmit
1390
+ });
1391
+ }
1392
+ return buildContentTypeVariants(requestBodyContent, resolver.resolveDataName(node), (schema) => ({
1393
+ ...schema,
1394
+ description: node.requestBody.description ?? schema.description
1395
+ }));
1396
+ }
1397
+ const requestType = buildRequestType();
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
+ });
1129
1408
  const dataType = renderSchemaType({
1130
1409
  schema: buildData({
1131
1410
  ...node,
@@ -1137,14 +1416,15 @@ const typeGenerator = defineGenerator({
1137
1416
  schema: buildResponses(node, { resolver }),
1138
1417
  name: resolver.resolveResponsesName(node)
1139
1418
  });
1140
- const responseType = (() => {
1141
- if (!node.responses.some((res) => res.schema)) return null;
1419
+ function buildResponseType() {
1420
+ const hasSchema = (res) => (res.content ?? []).some((entry) => entry.schema);
1421
+ if (!node.responses.some(hasSchema)) return null;
1142
1422
  const responseName = resolver.resolveResponseName(node);
1143
- const responsesWithSchema = node.responses.filter((res) => res.schema);
1144
- if (new Set(responsesWithSchema.flatMap((res) => res.schema ? adapter.getImports(res.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) => ({
1145
1425
  name: resolveImportName(schemaName),
1146
1426
  path: ""
1147
- })).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;
1148
1428
  return renderSchemaType({
1149
1429
  schema: {
1150
1430
  ...buildResponseUnion(node, { resolver }),
@@ -1152,18 +1432,27 @@ const typeGenerator = defineGenerator({
1152
1432
  },
1153
1433
  name: responseName
1154
1434
  });
1155
- })();
1435
+ }
1436
+ const responseType = buildResponseType();
1156
1437
  return /* @__PURE__ */ jsxs(File, {
1157
1438
  baseName: meta.file.baseName,
1158
1439
  path: meta.file.path,
1159
1440
  meta: meta.file.meta,
1160
- banner: resolver.resolveBanner(adapter.inputNode, {
1441
+ banner: resolver.resolveBanner(ctx.meta, {
1161
1442
  output,
1162
- config
1443
+ config,
1444
+ file: {
1445
+ path: meta.file.path,
1446
+ baseName: meta.file.baseName
1447
+ }
1163
1448
  }),
1164
- footer: resolver.resolveFooter(adapter.inputNode, {
1449
+ footer: resolver.resolveFooter(ctx.meta, {
1165
1450
  output,
1166
- config
1451
+ config,
1452
+ file: {
1453
+ path: meta.file.path,
1454
+ baseName: meta.file.baseName
1455
+ }
1167
1456
  }),
1168
1457
  children: [
1169
1458
  paramTypes,
@@ -1179,86 +1468,98 @@ const typeGenerator = defineGenerator({
1179
1468
  //#endregion
1180
1469
  //#region src/resolvers/resolverTs.ts
1181
1470
  /**
1182
- * Resolver for `@kubb/plugin-ts` that provides the default naming and path-resolution
1183
- * helpers used by the plugin. Import this in other plugins to resolve the exact names and
1184
- * paths that `plugin-ts` generates without hardcoding the conventions.
1471
+ * Default resolver used by `@kubb/plugin-ts`. Decides the names and file paths
1472
+ * for every generated TypeScript type. Import this in other plugins that need
1473
+ * to reference the exact names `plugin-ts` produces without duplicating the
1474
+ * casing/file-layout rules.
1185
1475
  *
1186
- * The `default` method is automatically injected by `defineResolver` it uses `camelCase`
1187
- * for identifiers/files and `pascalCase` for type names.
1476
+ * The `default` method is supplied by `defineResolver`. It uses PascalCase for
1477
+ * type names and PascalCase-with-isFile for files.
1188
1478
  *
1189
- * @example
1479
+ * @example Resolve a type and file name
1190
1480
  * ```ts
1191
- * import { resolver } from '@kubb/plugin-ts'
1481
+ * import { resolverTs } from '@kubb/plugin-ts'
1192
1482
  *
1193
- * resolver.default('list pets', 'type') // 'ListPets'
1194
- * resolver.resolveName('list pets status 200') // 'ListPetsStatus200'
1195
- * resolver.resolvePathName('list pets', 'file') // 'listPets'
1483
+ * resolverTs.default('list pets', 'type') // 'ListPets'
1484
+ * resolverTs.resolvePathName('list pets', 'file') // 'ListPets'
1485
+ * resolverTs.resolveResponseStatusName(node, 200) // 'ListPetsStatus200'
1196
1486
  * ```
1197
1487
  */
1198
- const resolverTs = defineResolver((ctx) => {
1488
+ const resolverTs = defineResolver(() => {
1199
1489
  return {
1200
1490
  name: "default",
1201
1491
  pluginName: "plugin-ts",
1202
1492
  default(name, type) {
1203
- return pascalCase(name, { isFile: type === "file" });
1493
+ const resolved = pascalCase(name, { isFile: type === "file" });
1494
+ return type === "file" ? resolved : ensureValidVarName(resolved);
1204
1495
  },
1205
1496
  resolveTypeName(name) {
1206
- return pascalCase(name);
1497
+ return ensureValidVarName(pascalCase(name));
1207
1498
  },
1208
1499
  resolvePathName(name, type) {
1209
- return pascalCase(name, { isFile: type === "file" });
1500
+ const resolved = pascalCase(name, { isFile: type === "file" });
1501
+ return type === "file" ? resolved : ensureValidVarName(resolved);
1210
1502
  },
1211
1503
  resolveParamName(node, param) {
1212
- return ctx.resolveTypeName(`${node.operationId} ${param.in} ${param.name}`);
1504
+ return this.resolveTypeName(`${node.operationId} ${param.in} ${param.name}`);
1213
1505
  },
1214
1506
  resolveResponseStatusName(node, statusCode) {
1215
- return ctx.resolveTypeName(`${node.operationId} Status ${statusCode}`);
1507
+ return this.resolveTypeName(`${node.operationId} Status ${statusCode}`);
1216
1508
  },
1217
1509
  resolveDataName(node) {
1218
- return ctx.resolveTypeName(`${node.operationId} Data`);
1510
+ return this.resolveTypeName(`${node.operationId} Data`);
1219
1511
  },
1220
1512
  resolveRequestConfigName(node) {
1221
- return ctx.resolveTypeName(`${node.operationId} RequestConfig`);
1513
+ return this.resolveTypeName(`${node.operationId} RequestConfig`);
1222
1514
  },
1223
1515
  resolveResponsesName(node) {
1224
- return ctx.resolveTypeName(`${node.operationId} Responses`);
1516
+ return this.resolveTypeName(`${node.operationId} Responses`);
1225
1517
  },
1226
1518
  resolveResponseName(node) {
1227
- return ctx.resolveTypeName(`${node.operationId} Response`);
1519
+ return this.resolveTypeName(`${node.operationId} Response`);
1228
1520
  },
1229
1521
  resolveEnumKeyName(node, enumTypeSuffix = "key") {
1230
- return `${ctx.resolveTypeName(node.name ?? "")}${enumTypeSuffix}`;
1522
+ return `${this.resolveTypeName(node.name ?? "")}${enumTypeSuffix}`;
1231
1523
  },
1232
1524
  resolvePathParamsName(node, param) {
1233
- return ctx.resolveParamName(node, param);
1525
+ return this.resolveParamName(node, param);
1234
1526
  },
1235
1527
  resolveQueryParamsName(node, param) {
1236
- return ctx.resolveParamName(node, param);
1528
+ return this.resolveParamName(node, param);
1237
1529
  },
1238
1530
  resolveHeaderParamsName(node, param) {
1239
- return ctx.resolveParamName(node, param);
1531
+ return this.resolveParamName(node, param);
1240
1532
  }
1241
1533
  };
1242
1534
  });
1243
1535
  //#endregion
1244
1536
  //#region src/plugin.ts
1245
1537
  /**
1246
- * Canonical plugin name for `@kubb/plugin-ts`, used to identify the plugin in driver lookups and warnings.
1538
+ * Canonical plugin name for `@kubb/plugin-ts`. Used for driver lookups and
1539
+ * cross-plugin dependency references.
1247
1540
  */
1248
1541
  const pluginTsName = "plugin-ts";
1249
1542
  /**
1250
- * The `@kubb/plugin-ts` plugin factory.
1251
- *
1252
- * Generates TypeScript type declarations from an OpenAPI/AST `RootNode`.
1253
- * Walks schemas and operations, delegates rendering to the active generators,
1254
- * and writes barrel files based on `output.barrelType`.
1543
+ * Generates TypeScript `type` aliases and `interface` declarations from an
1544
+ * OpenAPI spec. The foundation that every other Kubb plugin builds on:
1545
+ * clients, query hooks, mocks, and validators all reference the names this
1546
+ * plugin produces.
1255
1547
  *
1256
1548
  * @example
1257
1549
  * ```ts
1258
- * import pluginTs from '@kubb/plugin-ts'
1550
+ * import { defineConfig } from 'kubb'
1551
+ * import { pluginTs } from '@kubb/plugin-ts'
1259
1552
  *
1260
1553
  * export default defineConfig({
1261
- * plugins: [pluginTs({ output: { path: 'types' }, enumType: 'asConst' })],
1554
+ * input: { path: './petStore.yaml' },
1555
+ * output: { path: './src/gen' },
1556
+ * plugins: [
1557
+ * pluginTs({
1558
+ * output: { path: './types' },
1559
+ * enumType: 'asConst',
1560
+ * optionalType: 'questionTokenAndUndefined',
1561
+ * }),
1562
+ * ],
1262
1563
  * })
1263
1564
  * ```
1264
1565
  */
@@ -1267,13 +1568,7 @@ const pluginTs = definePlugin((options) => {
1267
1568
  path: "types",
1268
1569
  barrelType: "named"
1269
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;
1270
- const groupConfig = group ? {
1271
- ...group,
1272
- name: (ctx) => {
1273
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
1274
- return `${camelCase(ctx.group)}Controller`;
1275
- }
1276
- } : void 0;
1571
+ const groupConfig = createGroupConfig(group, { suffix: "Controller" });
1277
1572
  return {
1278
1573
  name: pluginTsName,
1279
1574
  options,