@kubb/plugin-ts 5.0.0-beta.4 → 5.0.0-beta.42

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,8 +1,7 @@
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-C0LytTxp.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
- import { isNumber } from "remeda";
6
5
  import ts from "typescript";
7
6
  import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
8
7
  //#region ../../internals/utils/src/casing.ts
@@ -141,6 +140,129 @@ function stringify(value) {
141
140
  return JSON.stringify(trimQuotes(value.toString()));
142
141
  }
143
142
  //#endregion
143
+ //#region ../../internals/utils/src/reserved.ts
144
+ /**
145
+ * JavaScript and Java reserved words.
146
+ * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
147
+ */
148
+ const reservedWords = new Set([
149
+ "abstract",
150
+ "arguments",
151
+ "boolean",
152
+ "break",
153
+ "byte",
154
+ "case",
155
+ "catch",
156
+ "char",
157
+ "class",
158
+ "const",
159
+ "continue",
160
+ "debugger",
161
+ "default",
162
+ "delete",
163
+ "do",
164
+ "double",
165
+ "else",
166
+ "enum",
167
+ "eval",
168
+ "export",
169
+ "extends",
170
+ "false",
171
+ "final",
172
+ "finally",
173
+ "float",
174
+ "for",
175
+ "function",
176
+ "goto",
177
+ "if",
178
+ "implements",
179
+ "import",
180
+ "in",
181
+ "instanceof",
182
+ "int",
183
+ "interface",
184
+ "let",
185
+ "long",
186
+ "native",
187
+ "new",
188
+ "null",
189
+ "package",
190
+ "private",
191
+ "protected",
192
+ "public",
193
+ "return",
194
+ "short",
195
+ "static",
196
+ "super",
197
+ "switch",
198
+ "synchronized",
199
+ "this",
200
+ "throw",
201
+ "throws",
202
+ "transient",
203
+ "true",
204
+ "try",
205
+ "typeof",
206
+ "var",
207
+ "void",
208
+ "volatile",
209
+ "while",
210
+ "with",
211
+ "yield",
212
+ "Array",
213
+ "Date",
214
+ "hasOwnProperty",
215
+ "Infinity",
216
+ "isFinite",
217
+ "isNaN",
218
+ "isPrototypeOf",
219
+ "length",
220
+ "Math",
221
+ "name",
222
+ "NaN",
223
+ "Number",
224
+ "Object",
225
+ "prototype",
226
+ "String",
227
+ "toString",
228
+ "undefined",
229
+ "valueOf"
230
+ ]);
231
+ /**
232
+ * Returns `true` when `name` is a syntactically valid JavaScript variable name.
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * isValidVarName('status') // true
237
+ * isValidVarName('class') // false (reserved word)
238
+ * isValidVarName('42foo') // false (starts with digit)
239
+ * ```
240
+ */
241
+ function isValidVarName(name) {
242
+ if (!name || reservedWords.has(name)) return false;
243
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
244
+ }
245
+ /**
246
+ * Returns `name` when it's a syntactically valid JavaScript variable name,
247
+ * otherwise prefixes it with `_` so the result is a valid identifier.
248
+ *
249
+ * Useful for sanitizing OpenAPI schema names or operation IDs that start with
250
+ * a digit (e.g. `409`, `504AccountCancel`) before using them as exported
251
+ * variable, type, or function names.
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * ensureValidVarName('409') // '_409'
256
+ * ensureValidVarName('504AccountCancel') // '_504AccountCancel'
257
+ * ensureValidVarName('Pet') // 'Pet'
258
+ * ensureValidVarName('class') // '_class'
259
+ * ```
260
+ */
261
+ function ensureValidVarName(name) {
262
+ if (!name || isValidVarName(name)) return name;
263
+ return `_${name}`;
264
+ }
265
+ //#endregion
144
266
  //#region src/constants.ts
145
267
  /**
146
268
  * `optionalType` values that cause a property's type to include `| undefined`.
@@ -186,6 +308,9 @@ const PARAM_RANK = {
186
308
  //#endregion
187
309
  //#region src/factory.ts
188
310
  const { SyntaxKind, factory } = ts;
311
+ function isNumber(value) {
312
+ return typeof value === "number" && !Number.isNaN(value);
313
+ }
189
314
  /**
190
315
  * TypeScript AST modifiers for common keywords (async, export, const, static).
191
316
  */
@@ -203,10 +328,19 @@ const syntaxKind = {
203
328
  literalType: SyntaxKind.LiteralType,
204
329
  stringLiteral: SyntaxKind.StringLiteral
205
330
  };
331
+ function isNonNullable$1(value) {
332
+ return value !== null && value !== void 0;
333
+ }
334
+ __name(isNonNullable$1, "isNonNullable");
206
335
  function isValidIdentifier(str) {
207
336
  if (!str.length || str.trim() !== str) return false;
208
- const node = ts.parseIsolatedEntityName(str, ts.ScriptTarget.Latest);
209
- return !!node && node.kind === ts.SyntaxKind.Identifier && ts.identifierToKeywordKind(node.kind) === void 0;
337
+ let ch = str.codePointAt(0);
338
+ if (!ts.isIdentifierStart(ch, ts.ScriptTarget.Latest)) return false;
339
+ for (let i = ch > 65535 ? 2 : 1; i < str.length; i += ch > 65535 ? 2 : 1) {
340
+ ch = str.codePointAt(i);
341
+ if (!ts.isIdentifierPart(ch, ts.ScriptTarget.Latest)) return false;
342
+ }
343
+ return true;
210
344
  }
211
345
  function propertyName(name) {
212
346
  if (typeof name === "string") return isValidIdentifier(name) ? factory.createIdentifier(name) : factory.createStringLiteral(name);
@@ -272,7 +406,7 @@ function createUnionDeclaration({ nodes, withParentheses }) {
272
406
  * Supports optional markers, readonly modifiers, and type annotations.
273
407
  */
274
408
  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);
409
+ return factory.createPropertySignature([...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : void 0].filter((modifier) => modifier !== void 0), propertyName(name), createQuestionToken(questionToken), type);
276
410
  }
277
411
  /**
278
412
  * Creates a function parameter declaration with optional markers, rest parameters, and type annotations.
@@ -370,8 +504,8 @@ function createEnumDeclaration({ type = "enum", name, typeName, enums, enumKeyCa
370
504
  }
371
505
  if (typeof value === "boolean") return factory.createLiteralTypeNode(value ? factory.createTrue() : factory.createFalse());
372
506
  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]) => {
507
+ }).filter((node) => node !== void 0)))];
508
+ 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
509
  let initializer = factory.createStringLiteral(value?.toString());
376
510
  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
511
  else initializer = factory.createNumericLiteral(value);
@@ -384,7 +518,7 @@ function createEnumDeclaration({ type = "enum", name, typeName, enums, enumKeyCa
384
518
  const casingKey = applyEnumKeyCasing(key.toString(), enumKeyCasing);
385
519
  return factory.createEnumMember(propertyName(casingKey), initializer);
386
520
  }
387
- }).filter(Boolean))];
521
+ }).filter((member) => member !== void 0))];
388
522
  const identifierName = name;
389
523
  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
524
  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 +530,7 @@ function createEnumDeclaration({ type = "enum", name, typeName, enums, enumKeyCa
396
530
  const casingKey = applyEnumKeyCasing(key.toString(), enumKeyCasing);
397
531
  return factory.createPropertyAssignment(propertyName(casingKey), initializer);
398
532
  }
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))))];
533
+ }).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
534
  }
401
535
  /**
402
536
  * Creates a TypeScript `Omit<T, Keys>` type reference node.
@@ -536,14 +670,14 @@ function dateOrStringNode(node) {
536
670
  * Maps an array of `SchemaNode`s through the printer, filtering out `null` and `undefined` results.
537
671
  */
538
672
  function buildMemberNodes(members, print) {
539
- return (members ?? []).map(print).filter(Boolean);
673
+ return (members ?? []).map(print).filter(isNonNullable$1);
540
674
  }
541
675
  /**
542
676
  * Builds a TypeScript tuple type node from an array schema's `items`,
543
677
  * applying min/max slice and optional/rest element rules.
544
678
  */
545
679
  function buildTupleNode(node, print) {
546
- let items = (node.items ?? []).map(print).filter(Boolean);
680
+ let items = (node.items ?? []).map(print).filter(isNonNullable$1);
547
681
  const restNode = node.rest ? print(node.rest) ?? void 0 : void 0;
548
682
  const { min, max } = node;
549
683
  if (max !== void 0) {
@@ -630,13 +764,13 @@ function Enum({ node, enumType, enumTypeSuffix, enumKeyCasing, resolver }) {
630
764
  isExportable: true,
631
765
  isIndexable: true,
632
766
  isTypeOnly: false,
633
- children: safePrint(nameNode)
767
+ children: parserTs.print(nameNode)
634
768
  }), /* @__PURE__ */ jsx(File.Source, {
635
769
  name: typeName,
636
770
  isIndexable: true,
637
771
  isExportable: ENUM_TYPES_WITH_RUNTIME_VALUE.has(enumType),
638
772
  isTypeOnly: ENUM_TYPES_WITH_TYPE_ONLY.has(enumType),
639
- children: safePrint(typeNode)
773
+ children: parserTs.print(typeNode)
640
774
  })] });
641
775
  }
642
776
  //#endregion
@@ -676,6 +810,98 @@ function Type({ name, node, printer, enumType, enumTypeSuffix, enumKeyCasing, re
676
810
  })] });
677
811
  }
678
812
  //#endregion
813
+ //#region ../../internals/shared/src/operation.ts
814
+ /**
815
+ * Maps a content type to the PascalCase suffix used to name per-content-type variants
816
+ * (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
817
+ */
818
+ function getContentTypeSuffix(contentType) {
819
+ const baseType = contentType.split(";")[0].trim();
820
+ if (baseType === "application/json") return "Json";
821
+ if (baseType === "multipart/form-data") return "FormData";
822
+ if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
823
+ const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
824
+ if (parts.length === 0) return "Unknown";
825
+ return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
826
+ }
827
+ /**
828
+ * Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
829
+ * (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
830
+ */
831
+ function getPerContentTypeName(baseName, suffix) {
832
+ if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
833
+ return baseName + suffix;
834
+ }
835
+ /**
836
+ * Resolves per-content-type variant names for a set of content entries, deduplicating suffix
837
+ * collisions with a numeric counter. Entries without a schema are skipped. The returned `suffix` is
838
+ * the final (possibly counter-augmented) value, so callers can derive parallel names in another
839
+ * namespace (e.g. plugin-faker deriving the matching plugin-ts type name).
840
+ */
841
+ function resolveContentTypeVariants(entries, baseName) {
842
+ const usedNames = /* @__PURE__ */ new Set();
843
+ return entries.filter((entry) => entry.schema).map((entry) => {
844
+ const baseSuffix = getContentTypeSuffix(entry.contentType);
845
+ let suffix = baseSuffix;
846
+ let name = getPerContentTypeName(baseName, suffix);
847
+ let counter = 2;
848
+ while (usedNames.has(name)) {
849
+ suffix = `${baseSuffix}${counter++}`;
850
+ name = getPerContentTypeName(baseName, suffix);
851
+ }
852
+ usedNames.add(name);
853
+ return {
854
+ name,
855
+ suffix,
856
+ schema: entry.schema,
857
+ keysToOmit: entry.keysToOmit,
858
+ contentType: entry.contentType
859
+ };
860
+ });
861
+ }
862
+ function getOperationParameters(node, options = {}) {
863
+ const params = ast.caseParams(node.parameters, options.paramsCasing);
864
+ return {
865
+ path: params.filter((param) => param.in === "path"),
866
+ query: params.filter((param) => param.in === "query"),
867
+ header: params.filter((param) => param.in === "header"),
868
+ cookie: params.filter((param) => param.in === "cookie")
869
+ };
870
+ }
871
+ //#endregion
872
+ //#region ../../internals/shared/src/group.ts
873
+ /**
874
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
875
+ * shared default naming so every plugin groups output consistently:
876
+ *
877
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
878
+ * - other groups use `${camelCase(group)}${suffix}` (e.g. `petController`).
879
+ *
880
+ * A user-provided `group.name` always wins over the default namer, so callers stay in
881
+ * control of their output folders. Returns `null` when grouping is disabled, matching the
882
+ * per-plugin convention.
883
+ *
884
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
885
+ * @param options.suffix - Appended to non-`path` group names, e.g. `'Controller'` or `'Requests'`.
886
+ *
887
+ * @example
888
+ * ```ts
889
+ * createGroupConfig(group, { suffix: 'Controller' }) // plugin-ts, plugin-client, …
890
+ * createGroupConfig(group, { suffix: 'Requests' }) // plugin-cypress, plugin-mcp
891
+ * ```
892
+ */
893
+ function createGroupConfig(group, options) {
894
+ if (!group) return null;
895
+ const defaultName = (ctx) => {
896
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
897
+ return `${camelCase(ctx.group)}${options.suffix}`;
898
+ };
899
+ return {
900
+ ...group,
901
+ name: group.name ? group.name : defaultName
902
+ };
903
+ }
904
+ //#endregion
679
905
  //#region src/utils.ts
680
906
  /**
681
907
  * Collects JSDoc annotation strings for a schema node.
@@ -687,15 +913,18 @@ function Type({ name, node, printer, enumType, enumTypeSuffix, enumKeyCasing, re
687
913
  function buildPropertyJSDocComments(schema) {
688
914
  const meta = ast.syncSchemaRef(schema);
689
915
  const isArray = meta?.primitive === "array";
916
+ const hasDescription = meta && "description" in meta && meta.description;
917
+ const formatComment = meta && "format" in meta && meta.format ? hasDescription ? [" ", `Format: \`${meta.format}\``] : ["@description", `Format: \`${meta.format}\``] : [];
690
918
  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
919
+ hasDescription ? `@description ${jsStringEscape(meta.description)}` : null,
920
+ ...formatComment,
921
+ meta && "deprecated" in meta && meta.deprecated ? "@deprecated" : null,
922
+ !isArray && meta && "min" in meta && meta.min !== void 0 ? `@minLength ${meta.min}` : null,
923
+ !isArray && meta && "max" in meta && meta.max !== void 0 ? `@maxLength ${meta.max}` : null,
924
+ meta && "pattern" in meta && meta.pattern ? `@pattern ${meta.pattern}` : null,
925
+ meta && "default" in meta && meta.default !== void 0 ? `@default ${"primitive" in meta && meta.primitive === "string" ? stringify(meta.default) : meta.default}` : null,
926
+ meta && "example" in meta && meta.example !== void 0 ? `@example ${meta.example}` : null,
927
+ meta && "primitive" in meta && meta.primitive ? [`@type ${meta.primitive}`, "optional" in schema && schema.optional ? " | undefined" : null].filter(Boolean).join("") : null
699
928
  ].filter(Boolean);
700
929
  }
701
930
  function buildParams(node, { params, resolver }) {
@@ -712,9 +941,7 @@ function buildParams(node, { params, resolver }) {
712
941
  });
713
942
  }
714
943
  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");
944
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
718
945
  return ast.createSchema({
719
946
  type: "object",
720
947
  deprecated: node.deprecated,
@@ -796,7 +1023,7 @@ function buildResponses(node, { resolver }) {
796
1023
  });
797
1024
  }
798
1025
  function buildResponseUnion(node, { resolver }) {
799
- const responsesWithSchema = node.responses.filter((res) => res.schema);
1026
+ const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
800
1027
  if (responsesWithSchema.length === 0) return null;
801
1028
  return ast.createSchema({
802
1029
  type: "union",
@@ -808,6 +1035,9 @@ function buildResponseUnion(node, { resolver }) {
808
1035
  }
809
1036
  //#endregion
810
1037
  //#region src/printers/printerTs.ts
1038
+ function isNonNullable(value) {
1039
+ return value !== null && value !== void 0;
1040
+ }
811
1041
  /**
812
1042
  * TypeScript type printer built with `definePrinter`.
813
1043
  *
@@ -860,7 +1090,7 @@ const printerTs = ast.definePrinter((options) => {
860
1090
  date: dateOrStringNode,
861
1091
  time: dateOrStringNode,
862
1092
  ref(node) {
863
- if (!node.name) return;
1093
+ if (!node.name) return null;
864
1094
  const refName = node.ref ? ast.extractRefName(node.ref) ?? node.name : node.name;
865
1095
  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
1096
  },
@@ -868,7 +1098,7 @@ const printerTs = ast.definePrinter((options) => {
868
1098
  const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? [];
869
1099
  if (this.options.enumType === "inlineLiteral" || !node.name) return createUnionDeclaration({
870
1100
  withParentheses: true,
871
- nodes: values.filter((v) => v !== null && v !== void 0).map((value) => constToTypeNode(value, typeof value)).filter(Boolean)
1101
+ nodes: values.filter((v) => v !== null && v !== void 0).map((value) => constToTypeNode(value, typeof value)).filter(isNonNullable)
872
1102
  }) ?? void 0;
873
1103
  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
1104
  },
@@ -886,7 +1116,7 @@ const printerTs = ast.definePrinter((options) => {
886
1116
  withParentheses: true
887
1117
  });
888
1118
  return this.transform(m);
889
- }).filter(Boolean)
1119
+ }).filter(isNonNullable)
890
1120
  }) ?? void 0;
891
1121
  return createUnionDeclaration({
892
1122
  withParentheses: true,
@@ -897,16 +1127,16 @@ const printerTs = ast.definePrinter((options) => {
897
1127
  return createIntersectionDeclaration({
898
1128
  withParentheses: true,
899
1129
  nodes: buildMemberNodes(node.members, this.transform)
900
- }) ?? void 0;
1130
+ }) ?? null;
901
1131
  },
902
1132
  array(node) {
903
1133
  return createArrayDeclaration({
904
- nodes: (node.items ?? []).map((item) => this.transform(item)).filter(Boolean),
1134
+ nodes: (node.items ?? []).map((item) => this.transform(item)).filter(isNonNullable),
905
1135
  arrayType: this.options.arrayType
906
- }) ?? void 0;
1136
+ }) ?? null;
907
1137
  },
908
1138
  tuple(node) {
909
- return buildTupleNode(node, this.transform);
1139
+ return buildTupleNode(node, this.transform) ?? null;
910
1140
  },
911
1141
  object(node) {
912
1142
  const { transform, options } = this;
@@ -933,46 +1163,54 @@ const printerTs = ast.definePrinter((options) => {
933
1163
  },
934
1164
  print(node) {
935
1165
  const { name, syntaxType = "type", description, keysToOmit } = this.options;
936
- let base = this.transform(node);
937
- if (!base) return null;
1166
+ const transformed = this.transform(node);
1167
+ if (!transformed) return null;
938
1168
  const meta = ast.syncSchemaRef(node);
939
1169
  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);
1170
+ const withNullable = meta.nullable ? createUnionDeclaration({ nodes: [transformed, keywordTypeNodes.null] }) : transformed;
1171
+ const result = (meta.nullish || meta.optional) && addsUndefined ? createUnionDeclaration({ nodes: [withNullable, keywordTypeNodes.undefined] }) : withNullable;
1172
+ return parserTs.print(result);
943
1173
  }
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({
1174
+ const inner = (() => {
1175
+ const omitted = keysToOmit?.length ? createOmitDeclaration({
1176
+ keys: keysToOmit,
1177
+ type: transformed,
1178
+ nonNullable: true
1179
+ }) : transformed;
1180
+ const withNullable = meta.nullable ? createUnionDeclaration({ nodes: [omitted, keywordTypeNodes.null] }) : omitted;
1181
+ return meta.nullish || meta.optional ? createUnionDeclaration({ nodes: [withNullable, keywordTypeNodes.undefined] }) : withNullable;
1182
+ })();
1183
+ const typeNode = createTypeDeclaration({
953
1184
  name,
954
1185
  isExportable: true,
955
1186
  type: inner,
956
- syntax: useTypeGeneration ? "type" : "interface",
1187
+ syntax: syntaxType === "type" || inner.kind === syntaxKind.union || !!keysToOmit?.length ? "type" : "interface",
957
1188
  comments: buildPropertyJSDocComments({
958
1189
  ...meta,
959
1190
  description
960
1191
  })
961
- }));
1192
+ });
1193
+ return parserTs.print(typeNode);
962
1194
  }
963
1195
  };
964
1196
  });
965
1197
  //#endregion
966
1198
  //#region src/generators/typeGenerator.tsx
1199
+ /**
1200
+ * Built-in generator for `@kubb/plugin-ts`. Emits one TypeScript file per
1201
+ * schema in the spec plus per-operation request, response, and parameter
1202
+ * types. Drop-replace with a custom `Generator<PluginTs>` to change how
1203
+ * TypeScript output is produced.
1204
+ */
967
1205
  const typeGenerator = defineGenerator({
968
1206
  name: "typescript",
969
- renderer: jsxRenderer,
1207
+ renderer: jsxRendererSync,
970
1208
  schema(node, ctx) {
971
1209
  const { enumType, enumTypeSuffix, enumKeyCasing, syntaxType, optionalType, arrayType, output, group, printer } = ctx.options;
972
1210
  const { adapter, config, resolver, root } = ctx;
973
1211
  if (!node.name) return;
974
1212
  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));
1213
+ const enumSchemaNames = new Set(ctx.meta.enumNames);
976
1214
  function resolveImportName(schemaName) {
977
1215
  if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) return resolver.resolveEnumKeyName({ name: schemaName }, enumTypeSuffix);
978
1216
  return resolver.resolveTypeName(schemaName);
@@ -985,7 +1223,7 @@ const typeGenerator = defineGenerator({
985
1223
  }, {
986
1224
  root,
987
1225
  output,
988
- group
1226
+ group: group ?? void 0
989
1227
  }).path
990
1228
  }));
991
1229
  const isEnumSchema = !!ast.narrowSchema(node, ast.schemaTypes.enum);
@@ -997,7 +1235,7 @@ const typeGenerator = defineGenerator({
997
1235
  }, {
998
1236
  root,
999
1237
  output,
1000
- group
1238
+ group: group ?? void 0
1001
1239
  })
1002
1240
  };
1003
1241
  const schemaPrinter = printerTs({
@@ -1016,13 +1254,21 @@ const typeGenerator = defineGenerator({
1016
1254
  baseName: meta.file.baseName,
1017
1255
  path: meta.file.path,
1018
1256
  meta: meta.file.meta,
1019
- banner: resolver.resolveBanner(adapter.inputNode, {
1257
+ banner: resolver.resolveBanner(ctx.meta, {
1020
1258
  output,
1021
- config
1259
+ config,
1260
+ file: {
1261
+ path: meta.file.path,
1262
+ baseName: meta.file.baseName
1263
+ }
1022
1264
  }),
1023
- footer: resolver.resolveFooter(adapter.inputNode, {
1265
+ footer: resolver.resolveFooter(ctx.meta, {
1024
1266
  output,
1025
- config
1267
+ config,
1268
+ file: {
1269
+ path: meta.file.path,
1270
+ baseName: meta.file.baseName
1271
+ }
1026
1272
  }),
1027
1273
  children: [mode === "split" && imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
1028
1274
  root: meta.file.path,
@@ -1057,9 +1303,9 @@ const typeGenerator = defineGenerator({
1057
1303
  }, {
1058
1304
  root,
1059
1305
  output,
1060
- group
1306
+ group: group ?? void 0
1061
1307
  }) };
1062
- const enumSchemaNames = new Set((adapter.inputNode?.schemas ?? []).filter((s) => ast.narrowSchema(s, ast.schemaTypes.enum) && s.name).map((s) => s.name));
1308
+ const enumSchemaNames = new Set(ctx.meta.enumNames);
1063
1309
  function resolveImportName(schemaName) {
1064
1310
  if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) return resolver.resolveEnumKeyName({ name: schemaName }, enumTypeSuffix);
1065
1311
  return resolver.resolveTypeName(schemaName);
@@ -1074,7 +1320,7 @@ const typeGenerator = defineGenerator({
1074
1320
  }, {
1075
1321
  root,
1076
1322
  output,
1077
- group
1323
+ group: group ?? void 0
1078
1324
  }).path
1079
1325
  }));
1080
1326
  const schemaPrinter = printerTs({
@@ -1109,23 +1355,63 @@ const typeGenerator = defineGenerator({
1109
1355
  printer: schemaPrinter
1110
1356
  })] });
1111
1357
  }
1358
+ /**
1359
+ * Emits an individual type per content type plus a union alias under `baseName`.
1360
+ * Shared by the request body and multi-content-type responses.
1361
+ */
1362
+ function buildContentTypeVariants(entries, baseName, decorate) {
1363
+ const variants = resolveContentTypeVariants(entries, baseName);
1364
+ const unionSchema = ast.createSchema({
1365
+ type: "union",
1366
+ members: variants.map((variant) => ast.createSchema({
1367
+ type: "ref",
1368
+ name: variant.name
1369
+ }))
1370
+ });
1371
+ return /* @__PURE__ */ jsxs(Fragment, { children: [variants.map((variant) => renderSchemaType({
1372
+ schema: decorate ? decorate(variant.schema) : variant.schema,
1373
+ name: variant.name,
1374
+ keysToOmit: variant.keysToOmit
1375
+ })), renderSchemaType({
1376
+ schema: unionSchema,
1377
+ name: baseName
1378
+ })] });
1379
+ }
1112
1380
  const paramTypes = params.map((param) => renderSchemaType({
1113
1381
  schema: param.schema,
1114
1382
  name: resolver.resolveParamName(node, param)
1115
1383
  }));
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
- }));
1384
+ const requestBodyContent = node.requestBody?.content ?? [];
1385
+ function buildRequestType() {
1386
+ if (requestBodyContent.length === 0) return null;
1387
+ if (requestBodyContent.length === 1) {
1388
+ const entry = requestBodyContent[0];
1389
+ if (!entry.schema) return null;
1390
+ return renderSchemaType({
1391
+ schema: {
1392
+ ...entry.schema,
1393
+ description: node.requestBody.description ?? entry.schema.description
1394
+ },
1395
+ name: resolver.resolveDataName(node),
1396
+ keysToOmit: entry.keysToOmit
1397
+ });
1398
+ }
1399
+ return buildContentTypeVariants(requestBodyContent, resolver.resolveDataName(node), (schema) => ({
1400
+ ...schema,
1401
+ description: node.requestBody.description ?? schema.description
1402
+ }));
1403
+ }
1404
+ const requestType = buildRequestType();
1405
+ const responseTypes = node.responses.map((res) => {
1406
+ const variants = (res.content ?? []).filter((entry) => entry.schema);
1407
+ if (variants.length > 1) return buildContentTypeVariants(variants, resolver.resolveResponseStatusName(node, res.statusCode));
1408
+ const primary = variants[0] ?? res.content?.[0];
1409
+ return renderSchemaType({
1410
+ schema: primary?.schema ?? null,
1411
+ name: resolver.resolveResponseStatusName(node, res.statusCode),
1412
+ keysToOmit: primary?.keysToOmit
1413
+ });
1414
+ });
1129
1415
  const dataType = renderSchemaType({
1130
1416
  schema: buildData({
1131
1417
  ...node,
@@ -1137,14 +1423,15 @@ const typeGenerator = defineGenerator({
1137
1423
  schema: buildResponses(node, { resolver }),
1138
1424
  name: resolver.resolveResponsesName(node)
1139
1425
  });
1140
- const responseType = (() => {
1141
- if (!node.responses.some((res) => res.schema)) return null;
1426
+ function buildResponseType() {
1427
+ const hasSchema = (res) => (res.content ?? []).some((entry) => entry.schema);
1428
+ if (!node.responses.some(hasSchema)) return null;
1142
1429
  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) => ({
1430
+ const responsesWithSchema = node.responses.filter(hasSchema);
1431
+ if (new Set(responsesWithSchema.flatMap((res) => (res.content ?? []).flatMap((entry) => entry.schema ? adapter.getImports(entry.schema, (schemaName) => ({
1145
1432
  name: resolveImportName(schemaName),
1146
1433
  path: ""
1147
- })).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : [])).has(responseName)) return null;
1434
+ })).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : []))).has(responseName)) return null;
1148
1435
  return renderSchemaType({
1149
1436
  schema: {
1150
1437
  ...buildResponseUnion(node, { resolver }),
@@ -1152,18 +1439,27 @@ const typeGenerator = defineGenerator({
1152
1439
  },
1153
1440
  name: responseName
1154
1441
  });
1155
- })();
1442
+ }
1443
+ const responseType = buildResponseType();
1156
1444
  return /* @__PURE__ */ jsxs(File, {
1157
1445
  baseName: meta.file.baseName,
1158
1446
  path: meta.file.path,
1159
1447
  meta: meta.file.meta,
1160
- banner: resolver.resolveBanner(adapter.inputNode, {
1448
+ banner: resolver.resolveBanner(ctx.meta, {
1161
1449
  output,
1162
- config
1450
+ config,
1451
+ file: {
1452
+ path: meta.file.path,
1453
+ baseName: meta.file.baseName
1454
+ }
1163
1455
  }),
1164
- footer: resolver.resolveFooter(adapter.inputNode, {
1456
+ footer: resolver.resolveFooter(ctx.meta, {
1165
1457
  output,
1166
- config
1458
+ config,
1459
+ file: {
1460
+ path: meta.file.path,
1461
+ baseName: meta.file.baseName
1462
+ }
1167
1463
  }),
1168
1464
  children: [
1169
1465
  paramTypes,
@@ -1179,86 +1475,98 @@ const typeGenerator = defineGenerator({
1179
1475
  //#endregion
1180
1476
  //#region src/resolvers/resolverTs.ts
1181
1477
  /**
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.
1478
+ * Default resolver used by `@kubb/plugin-ts`. Decides the names and file paths
1479
+ * for every generated TypeScript type. Import this in other plugins that need
1480
+ * to reference the exact names `plugin-ts` produces without duplicating the
1481
+ * casing/file-layout rules.
1185
1482
  *
1186
- * The `default` method is automatically injected by `defineResolver` it uses `camelCase`
1187
- * for identifiers/files and `pascalCase` for type names.
1483
+ * The `default` method is supplied by `defineResolver`. It uses PascalCase for
1484
+ * type names and PascalCase-with-isFile for files.
1188
1485
  *
1189
- * @example
1486
+ * @example Resolve a type and file name
1190
1487
  * ```ts
1191
- * import { resolver } from '@kubb/plugin-ts'
1488
+ * import { resolverTs } from '@kubb/plugin-ts'
1192
1489
  *
1193
- * resolver.default('list pets', 'type') // 'ListPets'
1194
- * resolver.resolveName('list pets status 200') // 'ListPetsStatus200'
1195
- * resolver.resolvePathName('list pets', 'file') // 'listPets'
1490
+ * resolverTs.default('list pets', 'type') // 'ListPets'
1491
+ * resolverTs.resolvePathName('list pets', 'file') // 'ListPets'
1492
+ * resolverTs.resolveResponseStatusName(node, 200) // 'ListPetsStatus200'
1196
1493
  * ```
1197
1494
  */
1198
- const resolverTs = defineResolver((ctx) => {
1495
+ const resolverTs = defineResolver(() => {
1199
1496
  return {
1200
1497
  name: "default",
1201
1498
  pluginName: "plugin-ts",
1202
1499
  default(name, type) {
1203
- return pascalCase(name, { isFile: type === "file" });
1500
+ const resolved = pascalCase(name, { isFile: type === "file" });
1501
+ return type === "file" ? resolved : ensureValidVarName(resolved);
1204
1502
  },
1205
1503
  resolveTypeName(name) {
1206
- return pascalCase(name);
1504
+ return ensureValidVarName(pascalCase(name));
1207
1505
  },
1208
1506
  resolvePathName(name, type) {
1209
- return pascalCase(name, { isFile: type === "file" });
1507
+ const resolved = pascalCase(name, { isFile: type === "file" });
1508
+ return type === "file" ? resolved : ensureValidVarName(resolved);
1210
1509
  },
1211
1510
  resolveParamName(node, param) {
1212
- return ctx.resolveTypeName(`${node.operationId} ${param.in} ${param.name}`);
1511
+ return this.resolveTypeName(`${node.operationId} ${param.in} ${param.name}`);
1213
1512
  },
1214
1513
  resolveResponseStatusName(node, statusCode) {
1215
- return ctx.resolveTypeName(`${node.operationId} Status ${statusCode}`);
1514
+ return this.resolveTypeName(`${node.operationId} Status ${statusCode}`);
1216
1515
  },
1217
1516
  resolveDataName(node) {
1218
- return ctx.resolveTypeName(`${node.operationId} Data`);
1517
+ return this.resolveTypeName(`${node.operationId} Data`);
1219
1518
  },
1220
1519
  resolveRequestConfigName(node) {
1221
- return ctx.resolveTypeName(`${node.operationId} RequestConfig`);
1520
+ return this.resolveTypeName(`${node.operationId} RequestConfig`);
1222
1521
  },
1223
1522
  resolveResponsesName(node) {
1224
- return ctx.resolveTypeName(`${node.operationId} Responses`);
1523
+ return this.resolveTypeName(`${node.operationId} Responses`);
1225
1524
  },
1226
1525
  resolveResponseName(node) {
1227
- return ctx.resolveTypeName(`${node.operationId} Response`);
1526
+ return this.resolveTypeName(`${node.operationId} Response`);
1228
1527
  },
1229
1528
  resolveEnumKeyName(node, enumTypeSuffix = "key") {
1230
- return `${ctx.resolveTypeName(node.name ?? "")}${enumTypeSuffix}`;
1529
+ return `${this.resolveTypeName(node.name ?? "")}${enumTypeSuffix}`;
1231
1530
  },
1232
1531
  resolvePathParamsName(node, param) {
1233
- return ctx.resolveParamName(node, param);
1532
+ return this.resolveParamName(node, param);
1234
1533
  },
1235
1534
  resolveQueryParamsName(node, param) {
1236
- return ctx.resolveParamName(node, param);
1535
+ return this.resolveParamName(node, param);
1237
1536
  },
1238
1537
  resolveHeaderParamsName(node, param) {
1239
- return ctx.resolveParamName(node, param);
1538
+ return this.resolveParamName(node, param);
1240
1539
  }
1241
1540
  };
1242
1541
  });
1243
1542
  //#endregion
1244
1543
  //#region src/plugin.ts
1245
1544
  /**
1246
- * Canonical plugin name for `@kubb/plugin-ts`, used to identify the plugin in driver lookups and warnings.
1545
+ * Canonical plugin name for `@kubb/plugin-ts`. Used for driver lookups and
1546
+ * cross-plugin dependency references.
1247
1547
  */
1248
1548
  const pluginTsName = "plugin-ts";
1249
1549
  /**
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`.
1550
+ * Generates TypeScript `type` aliases and `interface` declarations from an
1551
+ * OpenAPI spec. The foundation that every other Kubb plugin builds on:
1552
+ * clients, query hooks, mocks, and validators all reference the names this
1553
+ * plugin produces.
1255
1554
  *
1256
1555
  * @example
1257
1556
  * ```ts
1258
- * import pluginTs from '@kubb/plugin-ts'
1557
+ * import { defineConfig } from 'kubb'
1558
+ * import { pluginTs } from '@kubb/plugin-ts'
1259
1559
  *
1260
1560
  * export default defineConfig({
1261
- * plugins: [pluginTs({ output: { path: 'types' }, enumType: 'asConst' })],
1561
+ * input: { path: './petStore.yaml' },
1562
+ * output: { path: './src/gen' },
1563
+ * plugins: [
1564
+ * pluginTs({
1565
+ * output: { path: './types' },
1566
+ * enumType: 'asConst',
1567
+ * optionalType: 'questionTokenAndUndefined',
1568
+ * }),
1569
+ * ],
1262
1570
  * })
1263
1571
  * ```
1264
1572
  */
@@ -1267,13 +1575,7 @@ const pluginTs = definePlugin((options) => {
1267
1575
  path: "types",
1268
1576
  barrelType: "named"
1269
1577
  }, 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;
1578
+ const groupConfig = createGroupConfig(group, { suffix: "Controller" });
1277
1579
  return {
1278
1580
  name: pluginTsName,
1279
1581
  options,
@@ -1328,10 +1630,10 @@ function rank(param) {
1328
1630
  return param.optional ? PARAM_RANK.optional : PARAM_RANK.required;
1329
1631
  }
1330
1632
  function sortParams(params) {
1331
- return [...params].sort((a, b) => rank(a) - rank(b));
1633
+ return params.toSorted((a, b) => rank(a) - rank(b));
1332
1634
  }
1333
1635
  function sortChildParams(params) {
1334
- return [...params].sort((a, b) => rank(a) - rank(b));
1636
+ return params.toSorted((a, b) => rank(a) - rank(b));
1335
1637
  }
1336
1638
  /**
1337
1639
  * Default function-signature printer.