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

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,8 @@
1
- import "./chunk--u3MIqq1.js";
2
- import { safePrint } from "@kubb/parser-ts";
1
+ import { t as __name } from "./chunk-C0LytTxp.js";
2
+ import { extractRefName, jsStringEscape, stringify, trimQuotes } from "@kubb/ast/utils";
3
+ import { parserTs } from "@kubb/parser-ts";
3
4
  import { File, jsxRenderer } from "@kubb/renderer-jsx";
4
5
  import { ast, defineGenerator, definePlugin, defineResolver } from "@kubb/core";
5
- import { isNumber } from "remeda";
6
6
  import ts from "typescript";
7
7
  import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
8
8
  //#region ../../internals/utils/src/casing.ts
@@ -16,58 +16,41 @@ import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
16
16
  function toCamelOrPascal(text, pascal) {
17
17
  return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
18
18
  if (word.length > 1 && word === word.toUpperCase()) return word;
19
- if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
20
- return word.charAt(0).toUpperCase() + word.slice(1);
19
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
21
20
  }).join("").replace(/[^a-zA-Z0-9]/g, "");
22
21
  }
23
22
  /**
24
- * Splits `text` on `.` and applies `transformPart` to each segment.
25
- * The last segment receives `isLast = true`, all earlier segments receive `false`.
26
- * Segments are joined with `/` to form a file path.
27
- *
28
- * Only splits on dots followed by a letter so that version numbers
29
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
30
- */
31
- function applyToFileParts(text, transformPart) {
32
- const parts = text.split(/\.(?=[a-zA-Z])/);
33
- return parts.map((part, i) => transformPart(part, i === parts.length - 1)).join("/");
34
- }
35
- /**
36
23
  * Converts `text` to camelCase.
37
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
38
24
  *
39
- * @example
40
- * camelCase('hello-world') // 'helloWorld'
41
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
25
+ * @example Word boundaries
26
+ * `camelCase('hello-world') // 'helloWorld'`
27
+ *
28
+ * @example With a prefix
29
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
42
30
  */
43
- function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
44
- if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
45
- prefix,
46
- suffix
47
- } : {}));
31
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
48
32
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
49
33
  }
50
34
  /**
51
35
  * Converts `text` to PascalCase.
52
- * When `isFile` is `true`, the last dot-separated segment is PascalCased and earlier segments are camelCased.
53
36
  *
54
- * @example
55
- * pascalCase('hello-world') // 'HelloWorld'
56
- * pascalCase('pet.petId', { isFile: true }) // 'pet/PetId'
37
+ * @example Word boundaries
38
+ * `pascalCase('hello-world') // 'HelloWorld'`
39
+ *
40
+ * @example With a suffix
41
+ * `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
57
42
  */
58
- function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
59
- if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
60
- prefix,
61
- suffix
62
- }) : camelCase(part));
43
+ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
63
44
  return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
64
45
  }
65
46
  /**
66
47
  * Converts `text` to snake_case.
67
48
  *
68
- * @example
69
- * snakeCase('helloWorld') // 'hello_world'
70
- * snakeCase('Hello-World') // 'hello_world'
49
+ * @example From camelCase
50
+ * `snakeCase('helloWorld') // 'hello_world'`
51
+ *
52
+ * @example From mixed separators
53
+ * `snakeCase('Hello-World') // 'hello_world'`
71
54
  */
72
55
  function snakeCase(text, { prefix = "", suffix = "" } = {}) {
73
56
  return `${prefix} ${text} ${suffix}`.trim().replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[\s\-.]+/g, "_").replace(/[^a-zA-Z0-9_]/g, "").toLowerCase().split("_").filter(Boolean).join("_");
@@ -75,8 +58,8 @@ function snakeCase(text, { prefix = "", suffix = "" } = {}) {
75
58
  /**
76
59
  * Converts `text` to SCREAMING_SNAKE_CASE.
77
60
  *
78
- * @example
79
- * screamingSnakeCase('helloWorld') // 'HELLO_WORLD'
61
+ * @example From camelCase
62
+ * `screamingSnakeCase('helloWorld') // 'HELLO_WORLD'`
80
63
  */
81
64
  function screamingSnakeCase(text, { prefix = "", suffix = "" } = {}) {
82
65
  return snakeCase(text, {
@@ -85,60 +68,152 @@ function screamingSnakeCase(text, { prefix = "", suffix = "" } = {}) {
85
68
  }).toUpperCase();
86
69
  }
87
70
  //#endregion
88
- //#region ../../internals/utils/src/string.ts
71
+ //#region ../../internals/utils/src/fs.ts
89
72
  /**
90
- * Strips a single matching pair of `"..."`, `'...'`, or `` `...` `` from both ends of `text`.
91
- * Returns the string unchanged when no balanced quote pair is found.
73
+ * Builds a nested file path from a dotted name. Splits on dots that precede a letter
74
+ * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
75
+ * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
92
76
  *
93
- * @example
94
- * trimQuotes('"hello"') // 'hello'
95
- * trimQuotes('hello') // 'hello'
96
- */
97
- function trimQuotes(text) {
98
- if (text.length >= 2) {
99
- const first = text[0];
100
- const last = text[text.length - 1];
101
- if (first === "\"" && last === "\"" || first === "'" && last === "'" || first === "`" && last === "`") return text.slice(1, -1);
102
- }
103
- return text;
77
+ * Empty segments are dropped before joining. They arise when the name starts with a dot
78
+ * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
79
+ * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
80
+ * absolute path, letting generated files escape the configured output directory.
81
+ *
82
+ * @example Nested path from a dotted name
83
+ * `toFilePath('pet.petId') // 'pet/petId'`
84
+ *
85
+ * @example PascalCase the final segment
86
+ * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
87
+ *
88
+ * @example Suffix applied to the final segment only
89
+ * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
90
+ */
91
+ function toFilePath(name, caseLast = camelCase) {
92
+ const parts = name.split(/\.(?=[a-zA-Z])/);
93
+ return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
104
94
  }
95
+ //#endregion
96
+ //#region ../../internals/utils/src/reserved.ts
105
97
  /**
106
- * Escapes characters that are not allowed inside JS string literals.
107
- * Handles quotes, backslashes, and Unicode line terminators (U+2028 / U+2029).
108
- *
109
- * @see http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
98
+ * JavaScript and Java reserved words.
99
+ * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
100
+ */
101
+ const reservedWords = new Set([
102
+ "abstract",
103
+ "arguments",
104
+ "boolean",
105
+ "break",
106
+ "byte",
107
+ "case",
108
+ "catch",
109
+ "char",
110
+ "class",
111
+ "const",
112
+ "continue",
113
+ "debugger",
114
+ "default",
115
+ "delete",
116
+ "do",
117
+ "double",
118
+ "else",
119
+ "enum",
120
+ "eval",
121
+ "export",
122
+ "extends",
123
+ "false",
124
+ "final",
125
+ "finally",
126
+ "float",
127
+ "for",
128
+ "function",
129
+ "goto",
130
+ "if",
131
+ "implements",
132
+ "import",
133
+ "in",
134
+ "instanceof",
135
+ "int",
136
+ "interface",
137
+ "let",
138
+ "long",
139
+ "native",
140
+ "new",
141
+ "null",
142
+ "package",
143
+ "private",
144
+ "protected",
145
+ "public",
146
+ "return",
147
+ "short",
148
+ "static",
149
+ "super",
150
+ "switch",
151
+ "synchronized",
152
+ "this",
153
+ "throw",
154
+ "throws",
155
+ "transient",
156
+ "true",
157
+ "try",
158
+ "typeof",
159
+ "var",
160
+ "void",
161
+ "volatile",
162
+ "while",
163
+ "with",
164
+ "yield",
165
+ "Array",
166
+ "Date",
167
+ "hasOwnProperty",
168
+ "Infinity",
169
+ "isFinite",
170
+ "isNaN",
171
+ "isPrototypeOf",
172
+ "length",
173
+ "Math",
174
+ "name",
175
+ "NaN",
176
+ "Number",
177
+ "Object",
178
+ "prototype",
179
+ "String",
180
+ "toString",
181
+ "undefined",
182
+ "valueOf"
183
+ ]);
184
+ /**
185
+ * Returns `true` when `name` is a syntactically valid JavaScript variable name.
110
186
  *
111
187
  * @example
112
188
  * ```ts
113
- * jsStringEscape('say "hi"\nbye') // 'say \\"hi\\"\\nbye'
189
+ * isValidVarName('status') // true
190
+ * isValidVarName('class') // false (reserved word)
191
+ * isValidVarName('42foo') // false (starts with digit)
114
192
  * ```
115
193
  */
116
- function jsStringEscape(input) {
117
- return `${input}`.replace(/["'\\\n\r\u2028\u2029]/g, (character) => {
118
- switch (character) {
119
- case "\"":
120
- case "'":
121
- case "\\": return `\\${character}`;
122
- case "\n": return "\\n";
123
- case "\r": return "\\r";
124
- case "\u2028": return "\\u2028";
125
- case "\u2029": return "\\u2029";
126
- default: return "";
127
- }
128
- });
194
+ function isValidVarName(name) {
195
+ if (!name || reservedWords.has(name)) return false;
196
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
129
197
  }
130
- //#endregion
131
- //#region ../../internals/utils/src/object.ts
132
198
  /**
133
- * Serializes a primitive value to a JSON string literal, stripping any surrounding quote characters first.
199
+ * Returns `name` when it's a syntactically valid JavaScript variable name,
200
+ * otherwise prefixes it with `_` so the result is a valid identifier.
201
+ *
202
+ * Useful for sanitizing OpenAPI schema names or operation IDs that start with
203
+ * a digit (e.g. `409`, `504AccountCancel`) before using them as exported
204
+ * variable, type, or function names.
134
205
  *
135
206
  * @example
136
- * stringify('hello') // '"hello"'
137
- * stringify('"hello"') // '"hello"'
207
+ * ```ts
208
+ * ensureValidVarName('409') // '_409'
209
+ * ensureValidVarName('504AccountCancel') // '_504AccountCancel'
210
+ * ensureValidVarName('Pet') // 'Pet'
211
+ * ensureValidVarName('class') // '_class'
212
+ * ```
138
213
  */
139
- function stringify(value) {
140
- if (value === void 0 || value === null) return "\"\"";
141
- return JSON.stringify(trimQuotes(value.toString()));
214
+ function ensureValidVarName(name) {
215
+ if (!name || isValidVarName(name)) return name;
216
+ return `_${name}`;
142
217
  }
143
218
  //#endregion
144
219
  //#region src/constants.ts
@@ -151,26 +226,24 @@ const OPTIONAL_ADDS_UNDEFINED = new Set(["undefined", "questionTokenAndUndefined
151
226
  */
152
227
  const OPTIONAL_ADDS_QUESTION_TOKEN = new Set(["questionToken", "questionTokenAndUndefined"]);
153
228
  /**
154
- * `enumType` values that append a `Key` suffix to the generated enum type alias.
229
+ * `enum.type` values that append a `typeSuffix` to the generated enum type alias.
155
230
  */
156
- const ENUM_TYPES_WITH_KEY_SUFFIX = new Set(["asConst", "asPascalConst"]);
231
+ const ENUM_TYPES_WITH_KEY_SUFFIX = new Set(["asConst"]);
157
232
  /**
158
- * `enumType` values that require a runtime value declaration (object, enum, or literal).
233
+ * `enum.type` values that require a runtime value declaration (object, enum, or literal).
159
234
  */
160
235
  const ENUM_TYPES_WITH_RUNTIME_VALUE = new Set([
161
236
  "enum",
162
237
  "asConst",
163
- "asPascalConst",
164
238
  "constEnum",
165
239
  "literal",
166
240
  void 0
167
241
  ]);
168
242
  /**
169
- * `enumType` values whose type declaration is type-only (no runtime value emitted for the type alias).
243
+ * `enum.type` values whose type declaration is type-only (no runtime value emitted for the type alias).
170
244
  */
171
245
  const ENUM_TYPES_WITH_TYPE_ONLY = new Set([
172
246
  "asConst",
173
- "asPascalConst",
174
247
  "literal",
175
248
  void 0
176
249
  ]);
@@ -186,6 +259,9 @@ const PARAM_RANK = {
186
259
  //#endregion
187
260
  //#region src/factory.ts
188
261
  const { SyntaxKind, factory } = ts;
262
+ function isNumber(value) {
263
+ return typeof value === "number" && !Number.isNaN(value);
264
+ }
189
265
  /**
190
266
  * TypeScript AST modifiers for common keywords (async, export, const, static).
191
267
  */
@@ -203,10 +279,19 @@ const syntaxKind = {
203
279
  literalType: SyntaxKind.LiteralType,
204
280
  stringLiteral: SyntaxKind.StringLiteral
205
281
  };
282
+ function isNonNullable$1(value) {
283
+ return value !== null && value !== void 0;
284
+ }
285
+ __name(isNonNullable$1, "isNonNullable");
206
286
  function isValidIdentifier(str) {
207
287
  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;
288
+ let ch = str.codePointAt(0);
289
+ if (!ts.isIdentifierStart(ch, ts.ScriptTarget.Latest)) return false;
290
+ for (let i = ch > 65535 ? 2 : 1; i < str.length; i += ch > 65535 ? 2 : 1) {
291
+ ch = str.codePointAt(i);
292
+ if (!ts.isIdentifierPart(ch, ts.ScriptTarget.Latest)) return false;
293
+ }
294
+ return true;
210
295
  }
211
296
  function propertyName(name) {
212
297
  if (typeof name === "string") return isValidIdentifier(name) ? factory.createIdentifier(name) : factory.createStringLiteral(name);
@@ -272,7 +357,7 @@ function createUnionDeclaration({ nodes, withParentheses }) {
272
357
  * Supports optional markers, readonly modifiers, and type annotations.
273
358
  */
274
359
  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);
360
+ return factory.createPropertySignature([...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : void 0].filter((modifier) => modifier !== void 0), propertyName(name), createQuestionToken(questionToken), type);
276
361
  }
277
362
  /**
278
363
  * Creates a function parameter declaration with optional markers, rest parameters, and type annotations.
@@ -370,8 +455,8 @@ function createEnumDeclaration({ type = "enum", name, typeName, enums, enumKeyCa
370
455
  }
371
456
  if (typeof value === "boolean") return factory.createLiteralTypeNode(value ? factory.createTrue() : factory.createFalse());
372
457
  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]) => {
458
+ }).filter((node) => node !== void 0)))];
459
+ 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
460
  let initializer = factory.createStringLiteral(value?.toString());
376
461
  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
462
  else initializer = factory.createNumericLiteral(value);
@@ -384,7 +469,7 @@ function createEnumDeclaration({ type = "enum", name, typeName, enums, enumKeyCa
384
469
  const casingKey = applyEnumKeyCasing(key.toString(), enumKeyCasing);
385
470
  return factory.createEnumMember(propertyName(casingKey), initializer);
386
471
  }
387
- }).filter(Boolean))];
472
+ }).filter((member) => member !== void 0))];
388
473
  const identifierName = name;
389
474
  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
475
  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 +481,7 @@ function createEnumDeclaration({ type = "enum", name, typeName, enums, enumKeyCa
396
481
  const casingKey = applyEnumKeyCasing(key.toString(), enumKeyCasing);
397
482
  return factory.createPropertyAssignment(propertyName(casingKey), initializer);
398
483
  }
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))))];
484
+ }).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
485
  }
401
486
  /**
402
487
  * Creates a TypeScript `Omit<T, Keys>` type reference node.
@@ -478,12 +563,10 @@ const createStringLiteral = factory.createStringLiteral;
478
563
  * Creates an array type node (e.g., `T[]`).
479
564
  */
480
565
  const createArrayTypeNode = factory.createArrayTypeNode;
481
- factory.createParenthesizedType;
482
566
  /**
483
567
  * Creates a literal type node (e.g., `'hello'`, `42`, `true`).
484
568
  */
485
569
  const createLiteralTypeNode = factory.createLiteralTypeNode;
486
- factory.createNull;
487
570
  /**
488
571
  * Creates an identifier node.
489
572
  */
@@ -508,8 +591,6 @@ const createTrue = factory.createTrue;
508
591
  * Creates a boolean false literal type node.
509
592
  */
510
593
  const createFalse = factory.createFalse;
511
- factory.createIndexedAccessTypeNode;
512
- factory.createTypeOperatorNode;
513
594
  /**
514
595
  * Creates a prefix unary expression (e.g., negative numbers, logical not).
515
596
  */
@@ -536,14 +617,14 @@ function dateOrStringNode(node) {
536
617
  * Maps an array of `SchemaNode`s through the printer, filtering out `null` and `undefined` results.
537
618
  */
538
619
  function buildMemberNodes(members, print) {
539
- return (members ?? []).map(print).filter(Boolean);
620
+ return (members ?? []).map(print).filter(isNonNullable$1);
540
621
  }
541
622
  /**
542
623
  * Builds a TypeScript tuple type node from an array schema's `items`,
543
624
  * applying min/max slice and optional/rest element rules.
544
625
  */
545
626
  function buildTupleNode(node, print) {
546
- let items = (node.items ?? []).map(print).filter(Boolean);
627
+ let items = (node.items ?? []).map(print).filter(isNonNullable$1);
547
628
  const restNode = node.rest ? print(node.rest) ?? void 0 : void 0;
548
629
  const { min, max } = node;
549
630
  if (max !== void 0) {
@@ -590,58 +671,61 @@ function buildIndexSignatures(node, propertyCount, print) {
590
671
  * Resolves the runtime identifier name and the TypeScript type name for an enum schema node.
591
672
  *
592
673
  * The raw `node.name` may be a YAML key such as `"enumNames.Type"` which is not a
593
- * valid TypeScript identifier. The resolver normalizes it; for inline enum
594
- * properties the adapter already emits a PascalCase+suffix name so resolution is typically a no-op.
674
+ * valid TypeScript identifier. The resolver normalizes it. For inline enum properties the adapter
675
+ * already emits a PascalCase+suffix name, so resolution is typically a no-op.
676
+ *
677
+ * When `constCasing` is `'pascalCase'` and `typeSuffix` is empty, the const and the type
678
+ * resolve to the same name, which TypeScript merges into a single value+type declaration.
595
679
  */
596
- function getEnumNames({ node, enumType, enumTypeSuffix, resolver }) {
680
+ function getEnumNames({ node, enum: enumOptions, resolver }) {
597
681
  const resolved = resolver.default(node.name, "type");
598
682
  return {
599
- enumName: enumType === "asPascalConst" ? resolved : camelCase(node.name),
600
- typeName: ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) ? resolver.resolveEnumKeyName(node, enumTypeSuffix) : resolved
683
+ enumName: enumOptions.constCasing === "pascalCase" ? resolved : camelCase(node.name),
684
+ typeName: ENUM_TYPES_WITH_KEY_SUFFIX.has(enumOptions.type) ? resolver.resolveEnumKeyName(node, enumOptions.typeSuffix) : resolved
601
685
  };
602
686
  }
603
687
  /**
604
688
  * Renders the enum declaration(s) for a single named `EnumSchemaNode`.
605
689
  *
606
- * Depending on `enumType` this may emit:
607
- * - A runtime object (`asConst` / `asPascalConst`) plus a `typeof` type alias
690
+ * Depending on `enum.type` this may emit:
691
+ * - A runtime object (`asConst`) plus a `typeof` type alias
608
692
  * - A `const enum` or plain `enum` declaration (`constEnum` / `enum`)
609
693
  * - A union literal type alias (`literal`)
610
694
  *
611
695
  * The emitted `File.Source` nodes carry the resolved names so that the barrel
612
696
  * index picks up the correct export identifiers.
613
697
  */
614
- function Enum({ node, enumType, enumTypeSuffix, enumKeyCasing, resolver }) {
698
+ function Enum({ node, enum: enumOptions, resolver }) {
615
699
  const { enumName, typeName } = getEnumNames({
616
700
  node,
617
- enumType,
618
- enumTypeSuffix,
701
+ enum: enumOptions,
619
702
  resolver
620
703
  });
621
704
  const [nameNode, typeNode] = createEnumDeclaration({
622
705
  name: enumName,
623
706
  typeName,
624
707
  enums: node.namedEnumValues?.map((v) => [trimQuotes(v.name.toString()), v.value]) ?? node.enumValues?.filter((v) => v !== null && v !== void 0).map((v) => [trimQuotes(v.toString()), v]) ?? [],
625
- type: enumType,
626
- enumKeyCasing
708
+ type: enumOptions.type,
709
+ enumKeyCasing: enumOptions.keyCasing
627
710
  });
711
+ const namesMerge = !!nameNode && enumName === typeName;
628
712
  return /* @__PURE__ */ jsxs(Fragment, { children: [nameNode && /* @__PURE__ */ jsx(File.Source, {
629
713
  name: enumName,
630
714
  isExportable: true,
631
715
  isIndexable: true,
632
716
  isTypeOnly: false,
633
- children: safePrint(nameNode)
717
+ children: parserTs.print(nameNode)
634
718
  }), /* @__PURE__ */ jsx(File.Source, {
635
719
  name: typeName,
636
- isIndexable: true,
637
- isExportable: ENUM_TYPES_WITH_RUNTIME_VALUE.has(enumType),
638
- isTypeOnly: ENUM_TYPES_WITH_TYPE_ONLY.has(enumType),
639
- children: safePrint(typeNode)
720
+ isIndexable: !namesMerge,
721
+ isExportable: !namesMerge && ENUM_TYPES_WITH_RUNTIME_VALUE.has(enumOptions.type),
722
+ isTypeOnly: ENUM_TYPES_WITH_TYPE_ONLY.has(enumOptions.type),
723
+ children: parserTs.print(typeNode)
640
724
  })] });
641
725
  }
642
726
  //#endregion
643
727
  //#region src/components/Type.tsx
644
- function Type({ name, node, printer, enumType, enumTypeSuffix, enumKeyCasing, resolver }) {
728
+ function Type({ name, node, printer, enum: enumOptions, resolver }) {
645
729
  const enumSchemaNodes = ast.collect(node, { schema(n) {
646
730
  const enumNode = ast.narrowSchema(n, ast.schemaTypes.enum);
647
731
  if (enumNode?.name) return enumNode;
@@ -653,19 +737,16 @@ function Type({ name, node, printer, enumType, enumTypeSuffix, enumKeyCasing, re
653
737
  node,
654
738
  ...getEnumNames({
655
739
  node,
656
- enumType,
657
- enumTypeSuffix,
740
+ enum: enumOptions,
658
741
  resolver
659
742
  })
660
743
  };
661
744
  });
662
- const shouldExportEnums = enumType !== "inlineLiteral";
663
- const shouldExportType = enumType === "inlineLiteral" || enums.every((item) => item.typeName !== name);
745
+ const shouldExportEnums = enumOptions.type !== "inlineLiteral";
746
+ const shouldExportType = enumOptions.type === "inlineLiteral" || enums.every((item) => item.typeName !== name);
664
747
  return /* @__PURE__ */ jsxs(Fragment, { children: [shouldExportEnums && enums.map(({ node }) => /* @__PURE__ */ jsx(Enum, {
665
748
  node,
666
- enumType,
667
- enumTypeSuffix,
668
- enumKeyCasing,
749
+ enum: enumOptions,
669
750
  resolver
670
751
  }, node.name)), shouldExportType && /* @__PURE__ */ jsx(File.Source, {
671
752
  name,
@@ -676,6 +757,96 @@ function Type({ name, node, printer, enumType, enumTypeSuffix, enumKeyCasing, re
676
757
  })] });
677
758
  }
678
759
  //#endregion
760
+ //#region ../../internals/shared/src/operation.ts
761
+ /**
762
+ * Maps a content type to the PascalCase suffix used to name per-content-type variants
763
+ * (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
764
+ */
765
+ function getContentTypeSuffix(contentType) {
766
+ const baseType = contentType.split(";")[0].trim();
767
+ if (baseType === "application/json") return "Json";
768
+ if (baseType === "multipart/form-data") return "FormData";
769
+ if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
770
+ const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
771
+ if (parts.length === 0) return "Unknown";
772
+ return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
773
+ }
774
+ /**
775
+ * Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
776
+ * (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
777
+ */
778
+ function getPerContentTypeName(baseName, suffix) {
779
+ if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
780
+ return baseName + suffix;
781
+ }
782
+ /**
783
+ * Resolves per-content-type variant names for a set of content entries, deduplicating suffix
784
+ * collisions with a numeric counter. Entries without a schema are skipped. The returned `suffix` is
785
+ * the final (possibly counter-augmented) value, so callers can derive parallel names in another
786
+ * namespace (e.g. plugin-faker deriving the matching plugin-ts type name).
787
+ */
788
+ function resolveContentTypeVariants(entries, baseName) {
789
+ const usedNames = /* @__PURE__ */ new Set();
790
+ return entries.filter((entry) => entry.schema).map((entry) => {
791
+ const baseSuffix = getContentTypeSuffix(entry.contentType);
792
+ let suffix = baseSuffix;
793
+ let name = getPerContentTypeName(baseName, suffix);
794
+ let counter = 2;
795
+ while (usedNames.has(name)) {
796
+ suffix = `${baseSuffix}${counter++}`;
797
+ name = getPerContentTypeName(baseName, suffix);
798
+ }
799
+ usedNames.add(name);
800
+ return {
801
+ name,
802
+ suffix,
803
+ schema: entry.schema,
804
+ keysToOmit: entry.keysToOmit,
805
+ contentType: entry.contentType
806
+ };
807
+ });
808
+ }
809
+ function getOperationParameters(node, options = {}) {
810
+ const params = ast.caseParams(node.parameters, options.paramsCasing);
811
+ return {
812
+ path: params.filter((param) => param.in === "path"),
813
+ query: params.filter((param) => param.in === "query"),
814
+ header: params.filter((param) => param.in === "header"),
815
+ cookie: params.filter((param) => param.in === "cookie")
816
+ };
817
+ }
818
+ //#endregion
819
+ //#region ../../internals/shared/src/group.ts
820
+ /**
821
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
822
+ * shared default naming so every plugin groups output consistently:
823
+ *
824
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
825
+ * - other groups use the camelCased group (`pet store` → `petStore`).
826
+ *
827
+ * A user-provided `group.name` always wins over the default namer, so callers stay in
828
+ * control of their output folders. Returns `null` when grouping is disabled, matching the
829
+ * per-plugin convention.
830
+ *
831
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
832
+ *
833
+ * @example
834
+ * ```ts
835
+ * createGroupConfig(group) // shared across every plugin
836
+ * ```
837
+ */
838
+ function createGroupConfig(group) {
839
+ if (!group) return null;
840
+ const defaultName = (ctx) => {
841
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
842
+ return camelCase(ctx.group);
843
+ };
844
+ return {
845
+ ...group,
846
+ name: group.name ? group.name : defaultName
847
+ };
848
+ }
849
+ //#endregion
679
850
  //#region src/utils.ts
680
851
  /**
681
852
  * Collects JSDoc annotation strings for a schema node.
@@ -687,15 +858,18 @@ function Type({ name, node, printer, enumType, enumTypeSuffix, enumKeyCasing, re
687
858
  function buildPropertyJSDocComments(schema) {
688
859
  const meta = ast.syncSchemaRef(schema);
689
860
  const isArray = meta?.primitive === "array";
861
+ const hasDescription = meta && "description" in meta && meta.description;
862
+ const formatComment = meta && "format" in meta && meta.format ? hasDescription ? [" ", `Format: \`${meta.format}\``] : ["@description", `Format: \`${meta.format}\``] : [];
690
863
  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
864
+ hasDescription ? `@description ${jsStringEscape(meta.description)}` : null,
865
+ ...formatComment,
866
+ meta && "deprecated" in meta && meta.deprecated ? "@deprecated" : null,
867
+ !isArray && meta && "min" in meta && meta.min !== void 0 ? `@minLength ${meta.min}` : null,
868
+ !isArray && meta && "max" in meta && meta.max !== void 0 ? `@maxLength ${meta.max}` : null,
869
+ meta && "pattern" in meta && meta.pattern ? `@pattern ${meta.pattern}` : null,
870
+ meta && "default" in meta && meta.default !== void 0 ? `@default ${"primitive" in meta && meta.primitive === "string" ? stringify(meta.default) : meta.default}` : null,
871
+ meta && "example" in meta && meta.example !== void 0 ? `@example ${meta.example}` : null,
872
+ meta && "primitive" in meta && meta.primitive ? [`@type ${meta.primitive}`, "optional" in schema && schema.optional ? " | undefined" : null].filter(Boolean).join("") : null
699
873
  ].filter(Boolean);
700
874
  }
701
875
  function buildParams(node, { params, resolver }) {
@@ -712,9 +886,7 @@ function buildParams(node, { params, resolver }) {
712
886
  });
713
887
  }
714
888
  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");
889
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
718
890
  return ast.createSchema({
719
891
  type: "object",
720
892
  deprecated: node.deprecated,
@@ -796,7 +968,7 @@ function buildResponses(node, { resolver }) {
796
968
  });
797
969
  }
798
970
  function buildResponseUnion(node, { resolver }) {
799
- const responsesWithSchema = node.responses.filter((res) => res.schema);
971
+ const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
800
972
  if (responsesWithSchema.length === 0) return null;
801
973
  return ast.createSchema({
802
974
  type: "union",
@@ -808,6 +980,9 @@ function buildResponseUnion(node, { resolver }) {
808
980
  }
809
981
  //#endregion
810
982
  //#region src/printers/printerTs.ts
983
+ function isNonNullable(value) {
984
+ return value !== null && value !== void 0;
985
+ }
811
986
  /**
812
987
  * TypeScript type printer built with `definePrinter`.
813
988
  *
@@ -821,13 +996,13 @@ function buildResponseUnion(node, { resolver }) {
821
996
  *
822
997
  * @example Raw type node (no `typeName`)
823
998
  * ```ts
824
- * const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enumType: 'inlineLiteral' })
999
+ * const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enum: { type: 'inlineLiteral' } })
825
1000
  * const typeNode = printer.print(schemaNode) // ts.TypeNode
826
1001
  * ```
827
1002
  *
828
1003
  * @example Full declaration (with `typeName`)
829
1004
  * ```ts
830
- * const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enumType: 'inlineLiteral', typeName: 'MyType' })
1005
+ * const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enum: { type: 'inlineLiteral' }, typeName: 'MyType' })
831
1006
  * const declaration = printer.print(schemaNode) // ts.TypeAliasDeclaration | ts.InterfaceDeclaration
832
1007
  * ```
833
1008
  */
@@ -860,17 +1035,17 @@ const printerTs = ast.definePrinter((options) => {
860
1035
  date: dateOrStringNode,
861
1036
  time: dateOrStringNode,
862
1037
  ref(node) {
863
- if (!node.name) return;
864
- const refName = node.ref ? ast.extractRefName(node.ref) ?? node.name : node.name;
865
- 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);
1038
+ if (!node.name) return null;
1039
+ const refName = node.ref ? extractRefName(node.ref) ?? node.name : node.name;
1040
+ return createTypeReferenceNode(node.ref && ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enum.type) && this.options.enum.typeSuffix && this.options.enumSchemaNames?.has(refName) ? this.options.resolver.resolveEnumKeyName({ name: refName }, this.options.enum.typeSuffix) : node.ref ? this.options.resolver.default(refName, "type") : refName, void 0);
866
1041
  },
867
1042
  enum(node) {
868
1043
  const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? [];
869
- if (this.options.enumType === "inlineLiteral" || !node.name) return createUnionDeclaration({
1044
+ if (this.options.enum.type === "inlineLiteral" || !node.name) return createUnionDeclaration({
870
1045
  withParentheses: true,
871
- nodes: values.filter((v) => v !== null && v !== void 0).map((value) => constToTypeNode(value, typeof value)).filter(Boolean)
1046
+ nodes: values.filter((v) => v !== null && v !== void 0).map((value) => constToTypeNode(value, typeof value)).filter(isNonNullable)
872
1047
  }) ?? void 0;
873
- 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);
1048
+ return createTypeReferenceNode(ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enum.type) && this.options.enum.typeSuffix ? this.options.resolver.resolveEnumKeyName(node, this.options.enum.typeSuffix) : this.options.resolver.default(node.name, "type"), void 0);
874
1049
  },
875
1050
  union(node) {
876
1051
  const members = node.members ?? [];
@@ -886,7 +1061,7 @@ const printerTs = ast.definePrinter((options) => {
886
1061
  withParentheses: true
887
1062
  });
888
1063
  return this.transform(m);
889
- }).filter(Boolean)
1064
+ }).filter(isNonNullable)
890
1065
  }) ?? void 0;
891
1066
  return createUnionDeclaration({
892
1067
  withParentheses: true,
@@ -897,16 +1072,16 @@ const printerTs = ast.definePrinter((options) => {
897
1072
  return createIntersectionDeclaration({
898
1073
  withParentheses: true,
899
1074
  nodes: buildMemberNodes(node.members, this.transform)
900
- }) ?? void 0;
1075
+ }) ?? null;
901
1076
  },
902
1077
  array(node) {
903
1078
  return createArrayDeclaration({
904
- nodes: (node.items ?? []).map((item) => this.transform(item)).filter(Boolean),
1079
+ nodes: (node.items ?? []).map((item) => this.transform(item)).filter(isNonNullable),
905
1080
  arrayType: this.options.arrayType
906
- }) ?? void 0;
1081
+ }) ?? null;
907
1082
  },
908
1083
  tuple(node) {
909
- return buildTupleNode(node, this.transform);
1084
+ return buildTupleNode(node, this.transform) ?? null;
910
1085
  },
911
1086
  object(node) {
912
1087
  const { transform, options } = this;
@@ -933,48 +1108,55 @@ const printerTs = ast.definePrinter((options) => {
933
1108
  },
934
1109
  print(node) {
935
1110
  const { name, syntaxType = "type", description, keysToOmit } = this.options;
936
- let base = this.transform(node);
937
- if (!base) return null;
1111
+ const transformed = this.transform(node);
1112
+ if (!transformed) return null;
938
1113
  const meta = ast.syncSchemaRef(node);
939
1114
  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);
1115
+ const withNullable = meta.nullable ? createUnionDeclaration({ nodes: [transformed, keywordTypeNodes.null] }) : transformed;
1116
+ const result = (meta.nullish || meta.optional) && addsUndefined ? createUnionDeclaration({ nodes: [withNullable, keywordTypeNodes.undefined] }) : withNullable;
1117
+ return parserTs.print(result);
943
1118
  }
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({
1119
+ const inner = (() => {
1120
+ const omitted = keysToOmit?.length ? createOmitDeclaration({
1121
+ keys: keysToOmit,
1122
+ type: transformed,
1123
+ nonNullable: true
1124
+ }) : transformed;
1125
+ const withNullable = meta.nullable ? createUnionDeclaration({ nodes: [omitted, keywordTypeNodes.null] }) : omitted;
1126
+ return meta.nullish || meta.optional ? createUnionDeclaration({ nodes: [withNullable, keywordTypeNodes.undefined] }) : withNullable;
1127
+ })();
1128
+ const typeNode = createTypeDeclaration({
953
1129
  name,
954
1130
  isExportable: true,
955
1131
  type: inner,
956
- syntax: useTypeGeneration ? "type" : "interface",
1132
+ syntax: syntaxType === "type" || inner.kind === syntaxKind.union || !!keysToOmit?.length ? "type" : "interface",
957
1133
  comments: buildPropertyJSDocComments({
958
1134
  ...meta,
959
1135
  description
960
1136
  })
961
- }));
1137
+ });
1138
+ return parserTs.print(typeNode);
962
1139
  }
963
1140
  };
964
1141
  });
965
1142
  //#endregion
966
1143
  //#region src/generators/typeGenerator.tsx
1144
+ /**
1145
+ * Built-in generator for `@kubb/plugin-ts`. Emits one TypeScript file per
1146
+ * schema in the spec plus per-operation request, response, and parameter
1147
+ * types. Drop-replace with a custom `Generator<PluginTs>` to change how
1148
+ * TypeScript output is produced.
1149
+ */
967
1150
  const typeGenerator = defineGenerator({
968
1151
  name: "typescript",
969
1152
  renderer: jsxRenderer,
970
1153
  schema(node, ctx) {
971
- const { enumType, enumTypeSuffix, enumKeyCasing, syntaxType, optionalType, arrayType, output, group, printer } = ctx.options;
1154
+ const { enum: enumOptions, syntaxType, optionalType, arrayType, output, group, printer } = ctx.options;
972
1155
  const { adapter, config, resolver, root } = ctx;
973
1156
  if (!node.name) return;
974
- 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));
1157
+ const enumSchemaNames = new Set(ctx.meta.enumNames);
976
1158
  function resolveImportName(schemaName) {
977
- if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) return resolver.resolveEnumKeyName({ name: schemaName }, enumTypeSuffix);
1159
+ if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumOptions.type) && enumOptions.typeSuffix && enumSchemaNames.has(schemaName)) return resolver.resolveEnumKeyName({ name: schemaName }, enumOptions.typeSuffix);
978
1160
  return resolver.resolveTypeName(schemaName);
979
1161
  }
980
1162
  const imports = adapter.getImports(node, (schemaName) => ({
@@ -985,26 +1167,25 @@ const typeGenerator = defineGenerator({
985
1167
  }, {
986
1168
  root,
987
1169
  output,
988
- group
1170
+ group: group ?? void 0
989
1171
  }).path
990
1172
  }));
991
1173
  const isEnumSchema = !!ast.narrowSchema(node, ast.schemaTypes.enum);
992
1174
  const meta = {
993
- name: ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && isEnumSchema ? resolver.resolveEnumKeyName(node, enumTypeSuffix) : resolver.resolveTypeName(node.name),
1175
+ name: ENUM_TYPES_WITH_KEY_SUFFIX.has(enumOptions.type) && isEnumSchema ? resolver.resolveEnumKeyName(node, enumOptions.typeSuffix) : resolver.resolveTypeName(node.name),
994
1176
  file: resolver.resolveFile({
995
1177
  name: node.name,
996
1178
  extname: ".ts"
997
1179
  }, {
998
1180
  root,
999
1181
  output,
1000
- group
1182
+ group: group ?? void 0
1001
1183
  })
1002
1184
  };
1003
1185
  const schemaPrinter = printerTs({
1004
1186
  optionalType,
1005
1187
  arrayType,
1006
- enumType,
1007
- enumTypeSuffix,
1188
+ enum: enumOptions,
1008
1189
  name: meta.name,
1009
1190
  syntaxType,
1010
1191
  description: node.description,
@@ -1016,15 +1197,23 @@ const typeGenerator = defineGenerator({
1016
1197
  baseName: meta.file.baseName,
1017
1198
  path: meta.file.path,
1018
1199
  meta: meta.file.meta,
1019
- banner: resolver.resolveBanner(adapter.inputNode, {
1200
+ banner: resolver.resolveBanner(ctx.meta, {
1020
1201
  output,
1021
- config
1202
+ config,
1203
+ file: {
1204
+ path: meta.file.path,
1205
+ baseName: meta.file.baseName
1206
+ }
1022
1207
  }),
1023
- footer: resolver.resolveFooter(adapter.inputNode, {
1208
+ footer: resolver.resolveFooter(ctx.meta, {
1024
1209
  output,
1025
- config
1210
+ config,
1211
+ file: {
1212
+ path: meta.file.path,
1213
+ baseName: meta.file.baseName
1214
+ }
1026
1215
  }),
1027
- children: [mode === "split" && imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
1216
+ children: [imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
1028
1217
  root: meta.file.path,
1029
1218
  path: imp.path,
1030
1219
  name: imp.name,
@@ -1036,18 +1225,15 @@ const typeGenerator = defineGenerator({
1036
1225
  ].join("-"))), /* @__PURE__ */ jsx(Type, {
1037
1226
  name: meta.name,
1038
1227
  node,
1039
- enumType,
1040
- enumTypeSuffix,
1041
- enumKeyCasing,
1228
+ enum: enumOptions,
1042
1229
  resolver,
1043
1230
  printer: schemaPrinter
1044
1231
  })]
1045
1232
  });
1046
1233
  },
1047
1234
  operation(node, ctx) {
1048
- const { enumType, enumTypeSuffix, enumKeyCasing, optionalType, arrayType, syntaxType, paramsCasing, group, output, printer } = ctx.options;
1235
+ const { enum: enumOptions, optionalType, arrayType, syntaxType, paramsCasing, group, output, printer } = ctx.options;
1049
1236
  const { adapter, config, resolver, root } = ctx;
1050
- const mode = ctx.getMode(output);
1051
1237
  const params = ast.caseParams(node.parameters, paramsCasing);
1052
1238
  const meta = { file: resolver.resolveFile({
1053
1239
  name: node.operationId,
@@ -1057,11 +1243,11 @@ const typeGenerator = defineGenerator({
1057
1243
  }, {
1058
1244
  root,
1059
1245
  output,
1060
- group
1246
+ group: group ?? void 0
1061
1247
  }) };
1062
- const enumSchemaNames = new Set((adapter.inputNode?.schemas ?? []).filter((s) => ast.narrowSchema(s, ast.schemaTypes.enum) && s.name).map((s) => s.name));
1248
+ const enumSchemaNames = new Set(ctx.meta.enumNames);
1063
1249
  function resolveImportName(schemaName) {
1064
- if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumType) && enumTypeSuffix && enumSchemaNames.has(schemaName)) return resolver.resolveEnumKeyName({ name: schemaName }, enumTypeSuffix);
1250
+ if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumOptions.type) && enumOptions.typeSuffix && enumSchemaNames.has(schemaName)) return resolver.resolveEnumKeyName({ name: schemaName }, enumOptions.typeSuffix);
1065
1251
  return resolver.resolveTypeName(schemaName);
1066
1252
  }
1067
1253
  function renderSchemaType({ schema, name, keysToOmit }) {
@@ -1074,14 +1260,13 @@ const typeGenerator = defineGenerator({
1074
1260
  }, {
1075
1261
  root,
1076
1262
  output,
1077
- group
1263
+ group: group ?? void 0
1078
1264
  }).path
1079
1265
  }));
1080
1266
  const schemaPrinter = printerTs({
1081
1267
  optionalType,
1082
1268
  arrayType,
1083
- enumType,
1084
- enumTypeSuffix,
1269
+ enum: enumOptions,
1085
1270
  name,
1086
1271
  syntaxType,
1087
1272
  description: schema.description,
@@ -1090,7 +1275,7 @@ const typeGenerator = defineGenerator({
1090
1275
  enumSchemaNames,
1091
1276
  nodes: printer?.nodes
1092
1277
  });
1093
- return /* @__PURE__ */ jsxs(Fragment, { children: [mode === "split" && imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
1278
+ return /* @__PURE__ */ jsxs(Fragment, { children: [imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
1094
1279
  root: meta.file.path,
1095
1280
  path: imp.path,
1096
1281
  name: imp.name,
@@ -1102,30 +1287,68 @@ const typeGenerator = defineGenerator({
1102
1287
  ].join("-"))), /* @__PURE__ */ jsx(Type, {
1103
1288
  name,
1104
1289
  node: schema,
1105
- enumType,
1106
- enumTypeSuffix,
1107
- enumKeyCasing,
1290
+ enum: enumOptions,
1108
1291
  resolver,
1109
1292
  printer: schemaPrinter
1110
1293
  })] });
1111
1294
  }
1295
+ /**
1296
+ * Emits an individual type per content type plus a union alias under `baseName`.
1297
+ * Shared by the request body and multi-content-type responses.
1298
+ */
1299
+ function buildContentTypeVariants(entries, baseName, decorate) {
1300
+ const variants = resolveContentTypeVariants(entries, baseName);
1301
+ const unionSchema = ast.createSchema({
1302
+ type: "union",
1303
+ members: variants.map((variant) => ast.createSchema({
1304
+ type: "ref",
1305
+ name: variant.name
1306
+ }))
1307
+ });
1308
+ return /* @__PURE__ */ jsxs(Fragment, { children: [variants.map((variant) => renderSchemaType({
1309
+ schema: decorate ? decorate(variant.schema) : variant.schema,
1310
+ name: variant.name,
1311
+ keysToOmit: variant.keysToOmit
1312
+ })), renderSchemaType({
1313
+ schema: unionSchema,
1314
+ name: baseName
1315
+ })] });
1316
+ }
1112
1317
  const paramTypes = params.map((param) => renderSchemaType({
1113
1318
  schema: param.schema,
1114
1319
  name: resolver.resolveParamName(node, param)
1115
1320
  }));
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
- }));
1321
+ const requestBodyContent = node.requestBody?.content ?? [];
1322
+ function buildRequestType() {
1323
+ if (requestBodyContent.length === 0) return null;
1324
+ if (requestBodyContent.length === 1) {
1325
+ const entry = requestBodyContent[0];
1326
+ if (!entry.schema) return null;
1327
+ return renderSchemaType({
1328
+ schema: {
1329
+ ...entry.schema,
1330
+ description: node.requestBody.description ?? entry.schema.description
1331
+ },
1332
+ name: resolver.resolveDataName(node),
1333
+ keysToOmit: entry.keysToOmit
1334
+ });
1335
+ }
1336
+ return buildContentTypeVariants(requestBodyContent, resolver.resolveDataName(node), (schema) => ({
1337
+ ...schema,
1338
+ description: node.requestBody.description ?? schema.description
1339
+ }));
1340
+ }
1341
+ const requestType = buildRequestType();
1342
+ const responseTypes = node.responses.map((res) => {
1343
+ const variants = (res.content ?? []).filter((entry) => entry.schema);
1344
+ if (variants.length > 1) return buildContentTypeVariants(variants, resolver.resolveResponseStatusName(node, res.statusCode));
1345
+ const primary = variants[0] ?? res.content?.[0];
1346
+ return renderSchemaType({
1347
+ schema: primary?.schema ?? null,
1348
+ name: resolver.resolveResponseStatusName(node, res.statusCode),
1349
+ keysToOmit: primary?.keysToOmit
1350
+ });
1351
+ });
1129
1352
  const dataType = renderSchemaType({
1130
1353
  schema: buildData({
1131
1354
  ...node,
@@ -1137,14 +1360,15 @@ const typeGenerator = defineGenerator({
1137
1360
  schema: buildResponses(node, { resolver }),
1138
1361
  name: resolver.resolveResponsesName(node)
1139
1362
  });
1140
- const responseType = (() => {
1141
- if (!node.responses.some((res) => res.schema)) return null;
1363
+ function buildResponseType() {
1364
+ const hasSchema = (res) => (res.content ?? []).some((entry) => entry.schema);
1365
+ if (!node.responses.some(hasSchema)) return null;
1142
1366
  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) => ({
1367
+ const responsesWithSchema = node.responses.filter(hasSchema);
1368
+ if (new Set(responsesWithSchema.flatMap((res) => (res.content ?? []).flatMap((entry) => entry.schema ? adapter.getImports(entry.schema, (schemaName) => ({
1145
1369
  name: resolveImportName(schemaName),
1146
1370
  path: ""
1147
- })).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : [])).has(responseName)) return null;
1371
+ })).flatMap((imp) => Array.isArray(imp.name) ? imp.name : [imp.name]) : []))).has(responseName)) return null;
1148
1372
  return renderSchemaType({
1149
1373
  schema: {
1150
1374
  ...buildResponseUnion(node, { resolver }),
@@ -1152,18 +1376,27 @@ const typeGenerator = defineGenerator({
1152
1376
  },
1153
1377
  name: responseName
1154
1378
  });
1155
- })();
1379
+ }
1380
+ const responseType = buildResponseType();
1156
1381
  return /* @__PURE__ */ jsxs(File, {
1157
1382
  baseName: meta.file.baseName,
1158
1383
  path: meta.file.path,
1159
1384
  meta: meta.file.meta,
1160
- banner: resolver.resolveBanner(adapter.inputNode, {
1385
+ banner: resolver.resolveBanner(ctx.meta, {
1161
1386
  output,
1162
- config
1387
+ config,
1388
+ file: {
1389
+ path: meta.file.path,
1390
+ baseName: meta.file.baseName
1391
+ }
1163
1392
  }),
1164
- footer: resolver.resolveFooter(adapter.inputNode, {
1393
+ footer: resolver.resolveFooter(ctx.meta, {
1165
1394
  output,
1166
- config
1395
+ config,
1396
+ file: {
1397
+ path: meta.file.path,
1398
+ baseName: meta.file.baseName
1399
+ }
1167
1400
  }),
1168
1401
  children: [
1169
1402
  paramTypes,
@@ -1179,101 +1412,113 @@ const typeGenerator = defineGenerator({
1179
1412
  //#endregion
1180
1413
  //#region src/resolvers/resolverTs.ts
1181
1414
  /**
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.
1415
+ * Default resolver used by `@kubb/plugin-ts`. Decides the names and file paths
1416
+ * for every generated TypeScript type. Import this in other plugins that need
1417
+ * to reference the exact names `plugin-ts` produces without duplicating the
1418
+ * casing/file-layout rules.
1185
1419
  *
1186
- * The `default` method is automatically injected by `defineResolver` it uses `camelCase`
1187
- * for identifiers/files and `pascalCase` for type names.
1420
+ * The `default` method is supplied by `defineResolver`. It uses PascalCase for
1421
+ * type names and PascalCase file paths (dotted names become `/`-joined) for files.
1188
1422
  *
1189
- * @example
1423
+ * @example Resolve a type and file name
1190
1424
  * ```ts
1191
- * import { resolver } from '@kubb/plugin-ts'
1425
+ * import { resolverTs } from '@kubb/plugin-ts'
1192
1426
  *
1193
- * resolver.default('list pets', 'type') // 'ListPets'
1194
- * resolver.resolveName('list pets status 200') // 'ListPetsStatus200'
1195
- * resolver.resolvePathName('list pets', 'file') // 'listPets'
1427
+ * resolverTs.default('list pets', 'type') // 'ListPets'
1428
+ * resolverTs.resolvePathName('list pets', 'file') // 'ListPets'
1429
+ * resolverTs.resolveResponseStatusName(node, 200) // 'ListPetsStatus200'
1196
1430
  * ```
1197
1431
  */
1198
- const resolverTs = defineResolver((ctx) => {
1432
+ const resolverTs = defineResolver(() => {
1199
1433
  return {
1200
1434
  name: "default",
1201
1435
  pluginName: "plugin-ts",
1202
1436
  default(name, type) {
1203
- return pascalCase(name, { isFile: type === "file" });
1437
+ if (type === "file") return toFilePath(name, pascalCase);
1438
+ return ensureValidVarName(pascalCase(name));
1204
1439
  },
1205
1440
  resolveTypeName(name) {
1206
- return pascalCase(name);
1441
+ return ensureValidVarName(pascalCase(name));
1207
1442
  },
1208
1443
  resolvePathName(name, type) {
1209
- return pascalCase(name, { isFile: type === "file" });
1444
+ if (type === "file") return toFilePath(name, pascalCase);
1445
+ return ensureValidVarName(pascalCase(name));
1210
1446
  },
1211
1447
  resolveParamName(node, param) {
1212
- return ctx.resolveTypeName(`${node.operationId} ${param.in} ${param.name}`);
1448
+ return this.resolveTypeName(`${node.operationId} ${param.in} ${param.name}`);
1213
1449
  },
1214
1450
  resolveResponseStatusName(node, statusCode) {
1215
- return ctx.resolveTypeName(`${node.operationId} Status ${statusCode}`);
1451
+ return this.resolveTypeName(`${node.operationId} Status ${statusCode}`);
1216
1452
  },
1217
1453
  resolveDataName(node) {
1218
- return ctx.resolveTypeName(`${node.operationId} Data`);
1454
+ return this.resolveTypeName(`${node.operationId} Data`);
1219
1455
  },
1220
1456
  resolveRequestConfigName(node) {
1221
- return ctx.resolveTypeName(`${node.operationId} RequestConfig`);
1457
+ return this.resolveTypeName(`${node.operationId} RequestConfig`);
1222
1458
  },
1223
1459
  resolveResponsesName(node) {
1224
- return ctx.resolveTypeName(`${node.operationId} Responses`);
1460
+ return this.resolveTypeName(`${node.operationId} Responses`);
1225
1461
  },
1226
1462
  resolveResponseName(node) {
1227
- return ctx.resolveTypeName(`${node.operationId} Response`);
1463
+ return this.resolveTypeName(`${node.operationId} Response`);
1228
1464
  },
1229
1465
  resolveEnumKeyName(node, enumTypeSuffix = "key") {
1230
- return `${ctx.resolveTypeName(node.name ?? "")}${enumTypeSuffix}`;
1466
+ return `${this.resolveTypeName(node.name ?? "")}${enumTypeSuffix}`;
1231
1467
  },
1232
1468
  resolvePathParamsName(node, param) {
1233
- return ctx.resolveParamName(node, param);
1469
+ return this.resolveParamName(node, param);
1234
1470
  },
1235
1471
  resolveQueryParamsName(node, param) {
1236
- return ctx.resolveParamName(node, param);
1472
+ return this.resolveParamName(node, param);
1237
1473
  },
1238
1474
  resolveHeaderParamsName(node, param) {
1239
- return ctx.resolveParamName(node, param);
1475
+ return this.resolveParamName(node, param);
1240
1476
  }
1241
1477
  };
1242
1478
  });
1243
1479
  //#endregion
1244
1480
  //#region src/plugin.ts
1245
1481
  /**
1246
- * Canonical plugin name for `@kubb/plugin-ts`, used to identify the plugin in driver lookups and warnings.
1482
+ * Canonical plugin name for `@kubb/plugin-ts`. Used for driver lookups and
1483
+ * cross-plugin dependency references.
1247
1484
  */
1248
1485
  const pluginTsName = "plugin-ts";
1249
1486
  /**
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`.
1487
+ * Generates TypeScript `type` aliases and `interface` declarations from an
1488
+ * OpenAPI spec. The foundation that every other Kubb plugin builds on:
1489
+ * clients, query hooks, mocks, and validators all reference the names this
1490
+ * plugin produces.
1255
1491
  *
1256
1492
  * @example
1257
1493
  * ```ts
1258
- * import pluginTs from '@kubb/plugin-ts'
1494
+ * import { defineConfig } from 'kubb'
1495
+ * import { pluginTs } from '@kubb/plugin-ts'
1259
1496
  *
1260
1497
  * export default defineConfig({
1261
- * plugins: [pluginTs({ output: { path: 'types' }, enumType: 'asConst' })],
1498
+ * input: { path: './petStore.yaml' },
1499
+ * output: { path: './src/gen' },
1500
+ * plugins: [
1501
+ * pluginTs({
1502
+ * output: { path: './types' },
1503
+ * enum: { type: 'asConst' },
1504
+ * optionalType: 'questionTokenAndUndefined',
1505
+ * }),
1506
+ * ],
1262
1507
  * })
1263
1508
  * ```
1264
1509
  */
1265
1510
  const pluginTs = definePlugin((options) => {
1266
1511
  const { output = {
1267
1512
  path: "types",
1268
- barrelType: "named"
1269
- }, 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;
1513
+ barrel: { type: "named" }
1514
+ }, group, exclude = [], include, override = [], enum: enumOptions = {}, optionalType = "questionToken", arrayType = "array", syntaxType = "type", paramsCasing, printer, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
1515
+ const groupConfig = createGroupConfig(group);
1516
+ const resolvedEnum = {
1517
+ type: enumOptions.type ?? "asConst",
1518
+ constCasing: enumOptions.constCasing ?? "camelCase",
1519
+ typeSuffix: enumOptions.typeSuffix ?? "Key",
1520
+ keyCasing: enumOptions.keyCasing ?? "none"
1521
+ };
1277
1522
  return {
1278
1523
  name: pluginTsName,
1279
1524
  options,
@@ -1286,9 +1531,7 @@ const pluginTs = definePlugin((options) => {
1286
1531
  optionalType,
1287
1532
  group: groupConfig,
1288
1533
  arrayType,
1289
- enumType,
1290
- enumTypeSuffix,
1291
- enumKeyCasing,
1534
+ enum: resolvedEnum,
1292
1535
  syntaxType,
1293
1536
  paramsCasing,
1294
1537
  printer
@@ -1328,10 +1571,10 @@ function rank(param) {
1328
1571
  return param.optional ? PARAM_RANK.optional : PARAM_RANK.required;
1329
1572
  }
1330
1573
  function sortParams(params) {
1331
- return [...params].sort((a, b) => rank(a) - rank(b));
1574
+ return params.toSorted((a, b) => rank(a) - rank(b));
1332
1575
  }
1333
1576
  function sortChildParams(params) {
1334
- return [...params].sort((a, b) => rank(a) - rank(b));
1577
+ return params.toSorted((a, b) => rank(a) - rank(b));
1335
1578
  }
1336
1579
  /**
1337
1580
  * Default function-signature printer.