@kubb/plugin-ts 5.0.0-alpha.9 → 5.0.0-beta.100
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/LICENSE +17 -10
- package/README.md +38 -23
- package/dist/index.cjs +2376 -5
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +941 -40
- package/dist/index.js +2321 -2
- package/dist/index.js.map +1 -0
- package/dist/rolldown-runtime-CNktS9qV.js +17 -0
- package/package.json +38 -74
- package/dist/chunk--u3MIqq1.js +0 -8
- package/dist/components-CRu8IKY3.js +0 -729
- package/dist/components-CRu8IKY3.js.map +0 -1
- package/dist/components-DeNDKlzf.cjs +0 -982
- package/dist/components-DeNDKlzf.cjs.map +0 -1
- package/dist/components.cjs +0 -3
- package/dist/components.d.ts +0 -36
- package/dist/components.js +0 -2
- package/dist/generators.cjs +0 -4
- package/dist/generators.d.ts +0 -509
- package/dist/generators.js +0 -2
- package/dist/plugin-BZkBwnEA.js +0 -1269
- package/dist/plugin-BZkBwnEA.js.map +0 -1
- package/dist/plugin-Bunz1oGa.cjs +0 -1322
- package/dist/plugin-Bunz1oGa.cjs.map +0 -1
- package/dist/types-mSXmB8WU.d.ts +0 -298
- package/src/components/Type.tsx +0 -190
- package/src/components/index.ts +0 -1
- package/src/components/v2/Enum.tsx +0 -67
- package/src/components/v2/Type.tsx +0 -59
- package/src/constants.ts +0 -29
- package/src/factory.ts +0 -726
- package/src/generators/index.ts +0 -2
- package/src/generators/typeGenerator.tsx +0 -468
- package/src/generators/v2/typeGenerator.tsx +0 -167
- package/src/generators/v2/utils.ts +0 -140
- package/src/index.ts +0 -3
- package/src/parser.ts +0 -389
- package/src/plugin.ts +0 -232
- package/src/printer.ts +0 -368
- package/src/resolverTs.ts +0 -77
- package/src/types.ts +0 -296
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2321 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { n as __name, t as __exportAll } from "./rolldown-runtime-CNktS9qV.js";
|
|
2
|
+
import { parserTs } from "@kubb/parser-ts";
|
|
3
|
+
import { File, jsxRenderer } from "kubb/jsx";
|
|
4
|
+
import { Resolver, ast, createResolver, defineGenerator, definePlugin, isStringType, syncSchemaRef } from "kubb/kit";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "kubb/jsx/jsx-runtime";
|
|
7
|
+
//#region ../../internals/utils/src/casing.ts
|
|
8
|
+
/**
|
|
9
|
+
* Shared implementation for camelCase and PascalCase conversion.
|
|
10
|
+
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
11
|
+
* and capitalizes each word according to `pascal`.
|
|
12
|
+
*
|
|
13
|
+
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
14
|
+
*/
|
|
15
|
+
function toCamelOrPascal(text, pascal) {
|
|
16
|
+
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) => {
|
|
17
|
+
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
18
|
+
return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
|
|
19
|
+
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Converts `text` to camelCase.
|
|
23
|
+
*
|
|
24
|
+
* @example Word boundaries
|
|
25
|
+
* `camelCase('hello-world') // 'helloWorld'`
|
|
26
|
+
*
|
|
27
|
+
* @example With a prefix
|
|
28
|
+
* `camelCase('tag', { prefix: 'create' }) // 'createTag'`
|
|
29
|
+
*/
|
|
30
|
+
function camelCase(text, { prefix = "", suffix = "" } = {}) {
|
|
31
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Converts `text` to PascalCase.
|
|
35
|
+
*
|
|
36
|
+
* @example Word boundaries
|
|
37
|
+
* `pascalCase('hello-world') // 'HelloWorld'`
|
|
38
|
+
*
|
|
39
|
+
* @example With a suffix
|
|
40
|
+
* `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
|
|
41
|
+
*/
|
|
42
|
+
function pascalCase(text, { prefix = "", suffix = "" } = {}) {
|
|
43
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Converts `text` to snake_case.
|
|
47
|
+
*
|
|
48
|
+
* @example From camelCase
|
|
49
|
+
* `snakeCase('helloWorld') // 'hello_world'`
|
|
50
|
+
*
|
|
51
|
+
* @example From mixed separators
|
|
52
|
+
* `snakeCase('Hello-World') // 'hello_world'`
|
|
53
|
+
*/
|
|
54
|
+
function snakeCase(text, { prefix = "", suffix = "" } = {}) {
|
|
55
|
+
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("_");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Converts `text` to SCREAMING_SNAKE_CASE.
|
|
59
|
+
*
|
|
60
|
+
* @example From camelCase
|
|
61
|
+
* `screamingSnakeCase('helloWorld') // 'HELLO_WORLD'`
|
|
62
|
+
*/
|
|
63
|
+
function screamingSnakeCase(text, { prefix = "", suffix = "" } = {}) {
|
|
64
|
+
return snakeCase(text, {
|
|
65
|
+
prefix,
|
|
66
|
+
suffix
|
|
67
|
+
}).toUpperCase();
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region ../../internals/utils/src/reserved.ts
|
|
71
|
+
/**
|
|
72
|
+
* JavaScript and Java reserved words.
|
|
73
|
+
* @link https://github.com/jonschlinkert/reserved/blob/master/index.js
|
|
74
|
+
*/
|
|
75
|
+
const reservedWords = /* @__PURE__ */ new Set([
|
|
76
|
+
"abstract",
|
|
77
|
+
"arguments",
|
|
78
|
+
"boolean",
|
|
79
|
+
"break",
|
|
80
|
+
"byte",
|
|
81
|
+
"case",
|
|
82
|
+
"catch",
|
|
83
|
+
"char",
|
|
84
|
+
"class",
|
|
85
|
+
"const",
|
|
86
|
+
"continue",
|
|
87
|
+
"debugger",
|
|
88
|
+
"default",
|
|
89
|
+
"delete",
|
|
90
|
+
"do",
|
|
91
|
+
"double",
|
|
92
|
+
"else",
|
|
93
|
+
"enum",
|
|
94
|
+
"eval",
|
|
95
|
+
"export",
|
|
96
|
+
"extends",
|
|
97
|
+
"false",
|
|
98
|
+
"final",
|
|
99
|
+
"finally",
|
|
100
|
+
"float",
|
|
101
|
+
"for",
|
|
102
|
+
"function",
|
|
103
|
+
"goto",
|
|
104
|
+
"if",
|
|
105
|
+
"implements",
|
|
106
|
+
"import",
|
|
107
|
+
"in",
|
|
108
|
+
"instanceof",
|
|
109
|
+
"int",
|
|
110
|
+
"interface",
|
|
111
|
+
"let",
|
|
112
|
+
"long",
|
|
113
|
+
"native",
|
|
114
|
+
"new",
|
|
115
|
+
"null",
|
|
116
|
+
"package",
|
|
117
|
+
"private",
|
|
118
|
+
"protected",
|
|
119
|
+
"public",
|
|
120
|
+
"return",
|
|
121
|
+
"short",
|
|
122
|
+
"static",
|
|
123
|
+
"super",
|
|
124
|
+
"switch",
|
|
125
|
+
"synchronized",
|
|
126
|
+
"this",
|
|
127
|
+
"throw",
|
|
128
|
+
"throws",
|
|
129
|
+
"transient",
|
|
130
|
+
"true",
|
|
131
|
+
"try",
|
|
132
|
+
"typeof",
|
|
133
|
+
"var",
|
|
134
|
+
"void",
|
|
135
|
+
"volatile",
|
|
136
|
+
"while",
|
|
137
|
+
"with",
|
|
138
|
+
"yield",
|
|
139
|
+
"Array",
|
|
140
|
+
"Date",
|
|
141
|
+
"hasOwnProperty",
|
|
142
|
+
"Infinity",
|
|
143
|
+
"isFinite",
|
|
144
|
+
"isNaN",
|
|
145
|
+
"isPrototypeOf",
|
|
146
|
+
"length",
|
|
147
|
+
"Math",
|
|
148
|
+
"name",
|
|
149
|
+
"NaN",
|
|
150
|
+
"Number",
|
|
151
|
+
"Object",
|
|
152
|
+
"prototype",
|
|
153
|
+
"String",
|
|
154
|
+
"toString",
|
|
155
|
+
"undefined",
|
|
156
|
+
"valueOf"
|
|
157
|
+
]);
|
|
158
|
+
/**
|
|
159
|
+
* Returns `true` when `name` is a syntactically valid JavaScript variable name.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* isValidVarName('status') // true
|
|
164
|
+
* isValidVarName('class') // false (reserved word)
|
|
165
|
+
* isValidVarName('42foo') // false (starts with digit)
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
function isValidVarName(name) {
|
|
169
|
+
if (!name || reservedWords.has(name)) return false;
|
|
170
|
+
return isIdentifier(name);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Returns `name` when it's a syntactically valid JavaScript variable name,
|
|
174
|
+
* otherwise prefixes it with `_` so the result is a valid identifier.
|
|
175
|
+
*
|
|
176
|
+
* Useful for sanitizing OpenAPI schema names or operation IDs that start with
|
|
177
|
+
* a digit (e.g. `409`, `504AccountCancel`) before using them as exported
|
|
178
|
+
* variable, type, or function names.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* ensureValidVarName('409') // '_409'
|
|
183
|
+
* ensureValidVarName('504AccountCancel') // '_504AccountCancel'
|
|
184
|
+
* ensureValidVarName('Pet') // 'Pet'
|
|
185
|
+
* ensureValidVarName('class') // '_class'
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
function ensureValidVarName(name) {
|
|
189
|
+
if (!name || isValidVarName(name)) return name;
|
|
190
|
+
return `_${name}`;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Returns `true` when `name` is syntactically a valid identifier, ignoring reserved words.
|
|
194
|
+
*
|
|
195
|
+
* Reserved words and globals (`class`, `name`, `Date`, …) are valid as bare object-literal keys
|
|
196
|
+
* even though they are not valid variable names, so use this (not {@link isValidVarName}) when
|
|
197
|
+
* deciding whether an object key needs quoting.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* isIdentifier('name') // true
|
|
202
|
+
* isIdentifier('x-total')// false
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
function isIdentifier(name) {
|
|
206
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region ../../internals/utils/src/strings.ts
|
|
210
|
+
/**
|
|
211
|
+
* Strips a single matching pair of `"..."`, `'...'`, or `` `...` `` from both ends of `text`.
|
|
212
|
+
* Returns the string unchanged when no balanced quote pair is found.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```ts
|
|
216
|
+
* trimQuotes('"hello"') // 'hello'
|
|
217
|
+
* trimQuotes('hello') // 'hello'
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
function trimQuotes(text) {
|
|
221
|
+
if (text.length >= 2) {
|
|
222
|
+
const first = text[0];
|
|
223
|
+
const last = text[text.length - 1];
|
|
224
|
+
if (first === "\"" && last === "\"" || first === "'" && last === "'" || first === "`" && last === "`") return text.slice(1, -1);
|
|
225
|
+
}
|
|
226
|
+
return text;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Serializes a primitive to a single-quoted string literal, stripping any surrounding quotes first.
|
|
230
|
+
*
|
|
231
|
+
* Escaping runs through `JSON.stringify`, then the result switches to single quotes so the generated
|
|
232
|
+
* code matches the repo style without a formatter.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```ts
|
|
236
|
+
* stringify('hello') // "'hello'"
|
|
237
|
+
* stringify('"hello"') // "'hello'"
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
function stringify(value) {
|
|
241
|
+
if (value === void 0 || value === null) return "''";
|
|
242
|
+
return `'${JSON.stringify(trimQuotes(value.toString())).slice(1, -1).replace(/\\"/g, "\"").replace(/'/g, "\\'")}'`;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Escapes characters that are not allowed inside JS string literals, covering quotes, backslashes,
|
|
246
|
+
* and the Unicode line terminators U+2028 and U+2029.
|
|
247
|
+
*
|
|
248
|
+
* @see http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* jsStringEscape('say "hi"\nbye') // 'say \\"hi\\"\\nbye'
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
function jsStringEscape(input) {
|
|
256
|
+
return `${input}`.replace(/["'\\\n\r\u2028\u2029]/g, (character) => {
|
|
257
|
+
switch (character) {
|
|
258
|
+
case "\"":
|
|
259
|
+
case "'":
|
|
260
|
+
case "\\": return `\\${character}`;
|
|
261
|
+
case "\n": return "\\n";
|
|
262
|
+
case "\r": return "\\r";
|
|
263
|
+
case "\u2028": return "\\u2028";
|
|
264
|
+
case "\u2029": return "\\u2029";
|
|
265
|
+
default: return "";
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region ../../internals/utils/src/fs.ts
|
|
271
|
+
/**
|
|
272
|
+
* Builds a nested file path from a dotted name. Splits on dots that precede a letter
|
|
273
|
+
* (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
|
|
274
|
+
* every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
|
|
275
|
+
*
|
|
276
|
+
* Empty segments are dropped before joining. They arise when the name starts with a dot
|
|
277
|
+
* followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
|
|
278
|
+
* an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
|
|
279
|
+
* absolute path, letting generated files escape the configured output directory.
|
|
280
|
+
*
|
|
281
|
+
* @example Nested path from a dotted name
|
|
282
|
+
* `toFilePath('pet.petId') // 'pet/petId'`
|
|
283
|
+
*
|
|
284
|
+
* @example PascalCase the final segment
|
|
285
|
+
* `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
|
|
286
|
+
*
|
|
287
|
+
* @example Suffix applied to the final segment only
|
|
288
|
+
* `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
|
|
289
|
+
*/
|
|
290
|
+
function toFilePath(name, caseLast = camelCase) {
|
|
291
|
+
const parts = name.split(/\.(?=[a-zA-Z])/);
|
|
292
|
+
return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
|
|
293
|
+
}
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/constants.ts
|
|
296
|
+
/**
|
|
297
|
+
* `optionalType` values that cause a property's type to include `| undefined`.
|
|
298
|
+
*/
|
|
299
|
+
const OPTIONAL_ADDS_UNDEFINED = /* @__PURE__ */ new Set(["undefined", "questionTokenAndUndefined"]);
|
|
300
|
+
/**
|
|
301
|
+
* `optionalType` values that render the property key with a `?` token.
|
|
302
|
+
*/
|
|
303
|
+
const OPTIONAL_ADDS_QUESTION_TOKEN = /* @__PURE__ */ new Set(["questionToken", "questionTokenAndUndefined"]);
|
|
304
|
+
/**
|
|
305
|
+
* `enum.type` values that append a `typeSuffix` to the generated enum type alias.
|
|
306
|
+
*/
|
|
307
|
+
const ENUM_TYPES_WITH_KEY_SUFFIX = /* @__PURE__ */ new Set(["asConst"]);
|
|
308
|
+
/**
|
|
309
|
+
* `enum.type` values that require a runtime value declaration (object, enum, or literal).
|
|
310
|
+
*/
|
|
311
|
+
const ENUM_TYPES_WITH_RUNTIME_VALUE = /* @__PURE__ */ new Set([
|
|
312
|
+
"enum",
|
|
313
|
+
"asConst",
|
|
314
|
+
"constEnum",
|
|
315
|
+
"literal",
|
|
316
|
+
void 0
|
|
317
|
+
]);
|
|
318
|
+
/**
|
|
319
|
+
* `enum.type` values whose type declaration is type-only (no runtime value emitted for the type alias).
|
|
320
|
+
*/
|
|
321
|
+
const ENUM_TYPES_WITH_TYPE_ONLY = /* @__PURE__ */ new Set([
|
|
322
|
+
"asConst",
|
|
323
|
+
"literal",
|
|
324
|
+
void 0
|
|
325
|
+
]);
|
|
326
|
+
/**
|
|
327
|
+
* Ordering priority for function parameters: lower = sorted earlier.
|
|
328
|
+
*/
|
|
329
|
+
const PARAM_RANK = {
|
|
330
|
+
required: 0,
|
|
331
|
+
optional: 1,
|
|
332
|
+
withDefault: 2,
|
|
333
|
+
rest: 3
|
|
334
|
+
};
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/factory.ts
|
|
337
|
+
var factory_exports = /* @__PURE__ */ __exportAll({
|
|
338
|
+
appendJSDocToNode: () => appendJSDocToNode,
|
|
339
|
+
buildIndexSignatures: () => buildIndexSignatures,
|
|
340
|
+
buildMemberNodes: () => buildMemberNodes,
|
|
341
|
+
buildPropertyType: () => buildPropertyType,
|
|
342
|
+
buildTupleNode: () => buildTupleNode,
|
|
343
|
+
constToTypeNode: () => constToTypeNode,
|
|
344
|
+
createArrayDeclaration: () => createArrayDeclaration,
|
|
345
|
+
createEnumDeclaration: () => createEnumDeclaration,
|
|
346
|
+
createExportDeclaration: () => createExportDeclaration,
|
|
347
|
+
createImportDeclaration: () => createImportDeclaration,
|
|
348
|
+
createIntersectionDeclaration: () => createIntersectionDeclaration,
|
|
349
|
+
createJSDoc: () => createJSDoc,
|
|
350
|
+
createOmitDeclaration: () => createOmitDeclaration,
|
|
351
|
+
createParameterSignature: () => createParameterSignature,
|
|
352
|
+
createPropertySignature: () => createPropertySignature,
|
|
353
|
+
createQuestionToken: () => createQuestionToken,
|
|
354
|
+
createTypeDeclaration: () => createTypeDeclaration,
|
|
355
|
+
createTypeLiteralNode: () => createTypeLiteralNode,
|
|
356
|
+
createTypeReferenceNode: () => createTypeReferenceNode,
|
|
357
|
+
createUnionDeclaration: () => createUnionDeclaration,
|
|
358
|
+
createUrlTemplateType: () => createUrlTemplateType,
|
|
359
|
+
dateOrStringNode: () => dateOrStringNode,
|
|
360
|
+
isNonNullable: () => isNonNullable$1,
|
|
361
|
+
keywordTypeNodes: () => keywordTypeNodes,
|
|
362
|
+
modifiers: () => modifiers,
|
|
363
|
+
syntaxKind: () => syntaxKind
|
|
364
|
+
});
|
|
365
|
+
const { SyntaxKind, factory } = ts;
|
|
366
|
+
/**
|
|
367
|
+
* Compares two strings by UTF-16 code unit, keeping sorted output identical across platforms
|
|
368
|
+
* regardless of locale.
|
|
369
|
+
*/
|
|
370
|
+
function compareStrings(a, b) {
|
|
371
|
+
if (a < b) return -1;
|
|
372
|
+
if (a > b) return 1;
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
function isNumber(value) {
|
|
376
|
+
return typeof value === "number" && !Number.isNaN(value);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* TypeScript AST modifiers for common keywords (async, export, const, static).
|
|
380
|
+
*/
|
|
381
|
+
const modifiers = {
|
|
382
|
+
async: factory.createModifier(ts.SyntaxKind.AsyncKeyword),
|
|
383
|
+
export: factory.createModifier(ts.SyntaxKind.ExportKeyword),
|
|
384
|
+
const: factory.createModifier(ts.SyntaxKind.ConstKeyword),
|
|
385
|
+
static: factory.createModifier(ts.SyntaxKind.StaticKeyword)
|
|
386
|
+
};
|
|
387
|
+
/**
|
|
388
|
+
* TypeScript syntax kind constants for union, literal, and string types.
|
|
389
|
+
*/
|
|
390
|
+
const syntaxKind = {
|
|
391
|
+
union: SyntaxKind.UnionType,
|
|
392
|
+
literalType: SyntaxKind.LiteralType,
|
|
393
|
+
stringLiteral: SyntaxKind.StringLiteral
|
|
394
|
+
};
|
|
395
|
+
function isNonNullable$1(value) {
|
|
396
|
+
return value !== null && value !== void 0;
|
|
397
|
+
}
|
|
398
|
+
__name(isNonNullable$1, "isNonNullable");
|
|
399
|
+
function isValidIdentifier(str) {
|
|
400
|
+
if (!str.length || str.trim() !== str) return false;
|
|
401
|
+
let ch = str.codePointAt(0);
|
|
402
|
+
if (!ts.isIdentifierStart(ch, ts.ScriptTarget.Latest)) return false;
|
|
403
|
+
for (let i = ch > 65535 ? 2 : 1; i < str.length; i += ch > 65535 ? 2 : 1) {
|
|
404
|
+
ch = str.codePointAt(i);
|
|
405
|
+
if (!ts.isIdentifierPart(ch, ts.ScriptTarget.Latest)) return false;
|
|
406
|
+
}
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
function propertyName(name) {
|
|
410
|
+
if (typeof name === "string") return isValidIdentifier(name) ? factory.createIdentifier(name) : factory.createStringLiteral(name);
|
|
411
|
+
return name;
|
|
412
|
+
}
|
|
413
|
+
const questionToken = factory.createToken(ts.SyntaxKind.QuestionToken);
|
|
414
|
+
/**
|
|
415
|
+
* Creates a question token for optional type annotations.
|
|
416
|
+
* Pass `true` to use the cached token, or provide a pre-created token.
|
|
417
|
+
*/
|
|
418
|
+
function createQuestionToken(token) {
|
|
419
|
+
if (!token) return;
|
|
420
|
+
if (token === true) return questionToken;
|
|
421
|
+
return token;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Creates a TypeScript intersection type node from multiple type nodes.
|
|
425
|
+
* Returns the single node if only one is provided, or wraps in parentheses if requested.
|
|
426
|
+
*/
|
|
427
|
+
function createIntersectionDeclaration({ nodes, withParentheses }) {
|
|
428
|
+
if (!nodes.length) return null;
|
|
429
|
+
if (nodes.length === 1) return nodes[0] || null;
|
|
430
|
+
const node = factory.createIntersectionTypeNode(nodes);
|
|
431
|
+
if (withParentheses) return factory.createParenthesizedType(node);
|
|
432
|
+
return node;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Creates a TypeScript array type node.
|
|
436
|
+
* Use `arrayType: 'array'` for bracket syntax (`T[]`), or `'generic'` for `Array<T>`.
|
|
437
|
+
*
|
|
438
|
+
* @example Array bracket syntax
|
|
439
|
+
* `createArrayDeclaration({ nodes: [stringType], arrayType: 'array' }) // → string[]`
|
|
440
|
+
*
|
|
441
|
+
* @example Generic Array syntax
|
|
442
|
+
* `createArrayDeclaration({ nodes: [stringType], arrayType: 'generic' }) // → Array<string>`
|
|
443
|
+
*/
|
|
444
|
+
function createArrayDeclaration({ nodes, arrayType = "array" }) {
|
|
445
|
+
if (!nodes.length) return factory.createTupleTypeNode([]);
|
|
446
|
+
if (nodes.length === 1) {
|
|
447
|
+
const node = nodes[0];
|
|
448
|
+
if (!node) return null;
|
|
449
|
+
if (arrayType === "generic") return factory.createTypeReferenceNode(factory.createIdentifier("Array"), [node]);
|
|
450
|
+
return factory.createArrayTypeNode(node);
|
|
451
|
+
}
|
|
452
|
+
const unionType = factory.createUnionTypeNode(nodes);
|
|
453
|
+
if (arrayType === "generic") return factory.createTypeReferenceNode(factory.createIdentifier("Array"), [unionType]);
|
|
454
|
+
return factory.createArrayTypeNode(factory.createParenthesizedType(unionType));
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Minimum nodes length of 2
|
|
458
|
+
* @example Union type example
|
|
459
|
+
* `string | number`
|
|
460
|
+
*/
|
|
461
|
+
function createUnionDeclaration({ nodes, withParentheses }) {
|
|
462
|
+
if (!nodes.length) return keywordTypeNodes.any;
|
|
463
|
+
if (nodes.length === 1) return nodes[0];
|
|
464
|
+
const node = factory.createUnionTypeNode(nodes);
|
|
465
|
+
if (withParentheses) return factory.createParenthesizedType(node);
|
|
466
|
+
return node;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Creates a TypeScript property signature for object/interface members.
|
|
470
|
+
* Supports optional markers, readonly modifiers, and type annotations.
|
|
471
|
+
*/
|
|
472
|
+
function createPropertySignature({ readOnly, modifiers = [], name, questionToken, type }) {
|
|
473
|
+
return factory.createPropertySignature([...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : void 0].filter((modifier) => modifier !== void 0), propertyName(name), createQuestionToken(questionToken), type);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Creates a function parameter declaration with optional markers, rest parameters, and type annotations.
|
|
477
|
+
*/
|
|
478
|
+
function createParameterSignature(name, { modifiers, dotDotDotToken, questionToken, type, initializer }) {
|
|
479
|
+
return factory.createParameterDeclaration(modifiers, dotDotDotToken, name, createQuestionToken(questionToken), type, initializer);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Creates a JSDoc comment node from an array of comment strings.
|
|
483
|
+
* Returns null if no comments are provided.
|
|
484
|
+
*/
|
|
485
|
+
function createJSDoc({ comments }) {
|
|
486
|
+
if (!comments.length) return null;
|
|
487
|
+
return factory.createJSDocComment(factory.createNodeArray(comments.map((comment, i) => {
|
|
488
|
+
if (i === comments.length - 1) return factory.createJSDocText(comment);
|
|
489
|
+
return factory.createJSDocText(`${comment}\n`);
|
|
490
|
+
})));
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Attaches JSDoc comments to an AST node as synthetic leading comments.
|
|
494
|
+
* Filters out undefined comments before attaching.
|
|
495
|
+
*
|
|
496
|
+
* @see https://github.com/microsoft/TypeScript/issues/44151
|
|
497
|
+
*/
|
|
498
|
+
function appendJSDocToNode({ node, comments }) {
|
|
499
|
+
const filteredComments = comments.filter(Boolean);
|
|
500
|
+
if (!filteredComments.length) return node;
|
|
501
|
+
const text = filteredComments.reduce((acc = "", comment = "") => {
|
|
502
|
+
return `${acc}\n * ${comment.replaceAll("*/", "*\\/")}`;
|
|
503
|
+
}, "*");
|
|
504
|
+
return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, `${text || "*"}\n`, true);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Creates a TypeScript index signature for dynamic property access.
|
|
508
|
+
* Defines the key type (default: `string`) and value type on an object.
|
|
509
|
+
*/
|
|
510
|
+
function createIndexSignature(type, { modifiers, indexName = "key", indexType = factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) } = {}) {
|
|
511
|
+
return factory.createIndexSignature(modifiers, [createParameterSignature(indexName, { type: indexType })], type);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Creates a TypeScript type alias declaration with optional modifiers and type parameters.
|
|
515
|
+
*/
|
|
516
|
+
function createTypeAliasDeclaration({ modifiers, name, typeParameters, type }) {
|
|
517
|
+
return factory.createTypeAliasDeclaration(modifiers, name, typeParameters, type);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Creates a TypeScript interface declaration with optional modifiers, type parameters, and members.
|
|
521
|
+
*/
|
|
522
|
+
function createInterfaceDeclaration({ modifiers, name, typeParameters, members }) {
|
|
523
|
+
return factory.createInterfaceDeclaration(modifiers, name, typeParameters, void 0, members);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Creates a TypeScript type declaration as either a type alias or interface.
|
|
527
|
+
* Intelligently selects the syntax based on the type structure and attaches JSDoc comments.
|
|
528
|
+
*/
|
|
529
|
+
function createTypeDeclaration({ syntax, isExportable, comments, name, type }) {
|
|
530
|
+
if (syntax === "interface" && ts.isTypeLiteralNode(type)) return appendJSDocToNode({
|
|
531
|
+
node: createInterfaceDeclaration({
|
|
532
|
+
members: [...type.members],
|
|
533
|
+
modifiers: isExportable ? [modifiers.export] : [],
|
|
534
|
+
name,
|
|
535
|
+
typeParameters: void 0
|
|
536
|
+
}),
|
|
537
|
+
comments
|
|
538
|
+
});
|
|
539
|
+
return appendJSDocToNode({
|
|
540
|
+
node: createTypeAliasDeclaration({
|
|
541
|
+
type,
|
|
542
|
+
modifiers: isExportable ? [modifiers.export] : [],
|
|
543
|
+
name,
|
|
544
|
+
typeParameters: void 0
|
|
545
|
+
}),
|
|
546
|
+
comments
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Creates an import declaration with support for default imports, named imports, namespace imports, and type-only imports.
|
|
551
|
+
* Optionally rename imported members with `propertyName` and `name` pairs.
|
|
552
|
+
*
|
|
553
|
+
* @example Default import
|
|
554
|
+
* `import Pet from './Pet'`
|
|
555
|
+
*
|
|
556
|
+
* @example Named imports with rename
|
|
557
|
+
* `import { Pet as Cat } from './Pet'`
|
|
558
|
+
*
|
|
559
|
+
* @example Namespace import
|
|
560
|
+
* `import * as Pet from './Pet'`
|
|
561
|
+
*/
|
|
562
|
+
function createImportDeclaration({ name, path, isTypeOnly = false, isNameSpace = false }) {
|
|
563
|
+
if (!Array.isArray(name)) {
|
|
564
|
+
const importPropertyName = isNameSpace ? void 0 : factory.createIdentifier(name);
|
|
565
|
+
const importName = isNameSpace ? factory.createNamespaceImport(factory.createIdentifier(name)) : void 0;
|
|
566
|
+
return factory.createImportDeclaration(void 0, factory.createImportClause(isTypeOnly, importPropertyName, importName), factory.createStringLiteral(path), void 0);
|
|
567
|
+
}
|
|
568
|
+
const sortedName = name.toSorted((a, b) => compareStrings(typeof a === "object" ? a.propertyName : a, typeof b === "object" ? b.propertyName : b));
|
|
569
|
+
return factory.createImportDeclaration(void 0, factory.createImportClause(isTypeOnly, void 0, factory.createNamedImports(sortedName.map((item) => {
|
|
570
|
+
if (typeof item === "object") {
|
|
571
|
+
const obj = item;
|
|
572
|
+
if (obj.name) return factory.createImportSpecifier(false, factory.createIdentifier(obj.propertyName), factory.createIdentifier(obj.name));
|
|
573
|
+
return factory.createImportSpecifier(false, void 0, factory.createIdentifier(obj.propertyName));
|
|
574
|
+
}
|
|
575
|
+
return factory.createImportSpecifier(false, void 0, factory.createIdentifier(item));
|
|
576
|
+
}))), factory.createStringLiteral(path), void 0);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Creates an export declaration with support for named exports, namespace exports, and type-only exports.
|
|
580
|
+
* Sorts export names alphabetically for consistent output across platforms.
|
|
581
|
+
*/
|
|
582
|
+
function createExportDeclaration({ path, asAlias, isTypeOnly = false, name }) {
|
|
583
|
+
if (name && !Array.isArray(name) && !asAlias) console.warn(`When using name as string, asAlias should be true ${name}`);
|
|
584
|
+
if (!Array.isArray(name)) {
|
|
585
|
+
const parsedName = name?.match(/^\d/) ? `_${name?.slice(1)}` : name;
|
|
586
|
+
return factory.createExportDeclaration(void 0, isTypeOnly, asAlias && parsedName ? factory.createNamespaceExport(factory.createIdentifier(parsedName)) : void 0, factory.createStringLiteral(path), void 0);
|
|
587
|
+
}
|
|
588
|
+
const sortedName = name.toSorted((a, b) => compareStrings(typeof a === "string" ? a : a.text, typeof b === "string" ? b : b.text));
|
|
589
|
+
return factory.createExportDeclaration(void 0, isTypeOnly, factory.createNamedExports(sortedName.map((propertyName) => {
|
|
590
|
+
return factory.createExportSpecifier(false, void 0, typeof propertyName === "string" ? factory.createIdentifier(propertyName) : propertyName);
|
|
591
|
+
})), factory.createStringLiteral(path), void 0);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Apply casing transformation to enum keys
|
|
595
|
+
*/
|
|
596
|
+
function applyEnumKeyCasing(key, casing = "none") {
|
|
597
|
+
if (casing === "none") return key;
|
|
598
|
+
if (casing === "screamingSnakeCase") return screamingSnakeCase(key);
|
|
599
|
+
if (casing === "snakeCase") return snakeCase(key);
|
|
600
|
+
if (casing === "pascalCase") return pascalCase(key);
|
|
601
|
+
if (casing === "camelCase") return camelCase(key);
|
|
602
|
+
return key;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Creates a TypeScript enum declaration or equivalent construct in various formats.
|
|
606
|
+
* Returns a tuple of [name node, type node] - name node may be undefined for certain types.
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* ```ts
|
|
610
|
+
* const [name, type] = createEnumDeclaration({
|
|
611
|
+
* type: 'enum',
|
|
612
|
+
* name: 'petType',
|
|
613
|
+
* typeName: 'PetType',
|
|
614
|
+
* enums: [['cat', 'cat', 'A cat'], ['dog', 'dog']],
|
|
615
|
+
* })
|
|
616
|
+
* ```
|
|
617
|
+
*/
|
|
618
|
+
function createEnumDeclaration({ type = "enum", name, typeName, enums, enumKeyCasing = "none" }) {
|
|
619
|
+
if (type === "literal" || type === "inlineLiteral") return [void 0, factory.createTypeAliasDeclaration([factory.createToken(ts.SyntaxKind.ExportKeyword)], factory.createIdentifier(typeName), void 0, factory.createUnionTypeNode(enums.map(([_key, value]) => {
|
|
620
|
+
if (isNumber(value)) {
|
|
621
|
+
if (value < 0) return factory.createLiteralTypeNode(factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value))));
|
|
622
|
+
return factory.createLiteralTypeNode(factory.createNumericLiteral(value?.toString()));
|
|
623
|
+
}
|
|
624
|
+
if (typeof value === "boolean") return factory.createLiteralTypeNode(value ? factory.createTrue() : factory.createFalse());
|
|
625
|
+
if (value !== null && value !== void 0) return factory.createLiteralTypeNode(factory.createStringLiteral(value.toString()));
|
|
626
|
+
}).filter((node) => node !== void 0)))];
|
|
627
|
+
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, description]) => {
|
|
628
|
+
let initializer = factory.createStringLiteral(value?.toString());
|
|
629
|
+
if (Number.parseInt(value.toString(), 10) === value && isNumber(value)) if (value < 0) initializer = factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value)));
|
|
630
|
+
else initializer = factory.createNumericLiteral(value);
|
|
631
|
+
if (typeof value === "boolean") initializer = value ? factory.createTrue() : factory.createFalse();
|
|
632
|
+
if (isNumber(Number.parseInt(key.toString(), 10))) {
|
|
633
|
+
const casingKey = applyEnumKeyCasing(`${typeName}_${key}`, enumKeyCasing);
|
|
634
|
+
return appendJSDocToNode({
|
|
635
|
+
node: factory.createEnumMember(propertyName(casingKey), initializer),
|
|
636
|
+
comments: [description]
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
if (key !== null && key !== void 0) {
|
|
640
|
+
const casingKey = applyEnumKeyCasing(key.toString(), enumKeyCasing);
|
|
641
|
+
return appendJSDocToNode({
|
|
642
|
+
node: factory.createEnumMember(propertyName(casingKey), initializer),
|
|
643
|
+
comments: [description]
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}).filter((member) => member !== void 0))];
|
|
647
|
+
const identifierName = name;
|
|
648
|
+
if (enums.length === 0) return [void 0, factory.createTypeAliasDeclaration([factory.createToken(ts.SyntaxKind.ExportKeyword)], factory.createIdentifier(typeName), void 0, factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword))];
|
|
649
|
+
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, description]) => {
|
|
650
|
+
let initializer = factory.createStringLiteral(value?.toString());
|
|
651
|
+
if (isNumber(value)) if (value < 0) initializer = factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value)));
|
|
652
|
+
else initializer = factory.createNumericLiteral(value);
|
|
653
|
+
if (typeof value === "boolean") initializer = value ? factory.createTrue() : factory.createFalse();
|
|
654
|
+
if (key !== null && key !== void 0) {
|
|
655
|
+
const casingKey = applyEnumKeyCasing(key.toString(), enumKeyCasing);
|
|
656
|
+
return appendJSDocToNode({
|
|
657
|
+
node: factory.createPropertyAssignment(propertyName(casingKey), initializer),
|
|
658
|
+
comments: [description]
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}).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))))];
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Creates a TypeScript `Omit<T, Keys>` type reference node.
|
|
665
|
+
* Optionally wraps the type in `NonNullable<T>` if `nonNullable` is true.
|
|
666
|
+
*/
|
|
667
|
+
function createOmitDeclaration({ keys, type, nonNullable }) {
|
|
668
|
+
const node = nonNullable ? factory.createTypeReferenceNode(factory.createIdentifier("NonNullable"), [type]) : type;
|
|
669
|
+
if (Array.isArray(keys)) return factory.createTypeReferenceNode(factory.createIdentifier("Omit"), [node, factory.createUnionTypeNode(keys.map((key) => {
|
|
670
|
+
return factory.createLiteralTypeNode(factory.createStringLiteral(key));
|
|
671
|
+
}))]);
|
|
672
|
+
return factory.createTypeReferenceNode(factory.createIdentifier("Omit"), [node, factory.createLiteralTypeNode(factory.createStringLiteral(keys))]);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Pre-built TypeScript keyword type nodes for common primitive types.
|
|
676
|
+
* Use these to avoid repeatedly creating the same type nodes.
|
|
677
|
+
*/
|
|
678
|
+
const keywordTypeNodes = {
|
|
679
|
+
any: factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
|
|
680
|
+
unknown: factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
|
|
681
|
+
void: factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword),
|
|
682
|
+
number: factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
|
|
683
|
+
integer: factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
|
|
684
|
+
bigint: factory.createKeywordTypeNode(ts.SyntaxKind.BigIntKeyword),
|
|
685
|
+
object: factory.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword),
|
|
686
|
+
string: factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
|
687
|
+
boolean: factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword),
|
|
688
|
+
undefined: factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
|
|
689
|
+
null: factory.createLiteralTypeNode(factory.createToken(ts.SyntaxKind.NullKeyword)),
|
|
690
|
+
never: factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword)
|
|
691
|
+
};
|
|
692
|
+
/**
|
|
693
|
+
* Converts a path like '/pet/{petId}/uploadImage' to a template literal type
|
|
694
|
+
* like `/pet/${string}/uploadImage`
|
|
695
|
+
*/
|
|
696
|
+
/**
|
|
697
|
+
* Converts an OAS-style path (e.g. `/pets/{petId}`) or an Express-style path
|
|
698
|
+
* (e.g. `/pets/:petId`) to a TypeScript template literal type
|
|
699
|
+
* like `` `/pets/${string}` ``.
|
|
700
|
+
*/
|
|
701
|
+
function createUrlTemplateType(path) {
|
|
702
|
+
const normalized = path.replace(/:([^/]+)/g, "{$1}");
|
|
703
|
+
if (!normalized.includes("{")) return factory.createLiteralTypeNode(factory.createStringLiteral(normalized));
|
|
704
|
+
const segments = normalized.split(/(\{[^}]+\})/);
|
|
705
|
+
const parts = [];
|
|
706
|
+
const parameterIndices = [];
|
|
707
|
+
segments.forEach((segment) => {
|
|
708
|
+
if (segment.startsWith("{") && segment.endsWith("}")) {
|
|
709
|
+
parameterIndices.push(parts.length);
|
|
710
|
+
parts.push(segment);
|
|
711
|
+
} else if (segment) parts.push(segment);
|
|
712
|
+
});
|
|
713
|
+
const head = ts.factory.createTemplateHead(parts[0] || "");
|
|
714
|
+
const templateSpans = [];
|
|
715
|
+
parameterIndices.forEach((paramIndex, i) => {
|
|
716
|
+
const isLast = i === parameterIndices.length - 1;
|
|
717
|
+
const nextPart = parts[paramIndex + 1] || "";
|
|
718
|
+
const literal = isLast ? ts.factory.createTemplateTail(nextPart) : ts.factory.createTemplateMiddle(nextPart);
|
|
719
|
+
templateSpans.push(ts.factory.createTemplateLiteralTypeSpan(keywordTypeNodes.string, literal));
|
|
720
|
+
});
|
|
721
|
+
return ts.factory.createTemplateLiteralType(head, templateSpans);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Creates a TypeScript type literal node (anonymous object type).
|
|
725
|
+
*/
|
|
726
|
+
const createTypeLiteralNode = factory.createTypeLiteralNode;
|
|
727
|
+
/**
|
|
728
|
+
* Creates a TypeScript type reference node (e.g., `Array<string>`, `Record<K, V>`).
|
|
729
|
+
*/
|
|
730
|
+
const createTypeReferenceNode = factory.createTypeReferenceNode;
|
|
731
|
+
/**
|
|
732
|
+
* Creates a numeric literal type node.
|
|
733
|
+
*/
|
|
734
|
+
const createNumericLiteral = factory.createNumericLiteral;
|
|
735
|
+
/**
|
|
736
|
+
* Creates a string literal type node.
|
|
737
|
+
*/
|
|
738
|
+
const createStringLiteral = factory.createStringLiteral;
|
|
739
|
+
/**
|
|
740
|
+
* Creates an array type node (e.g., `T[]`).
|
|
741
|
+
*/
|
|
742
|
+
const createArrayTypeNode = factory.createArrayTypeNode;
|
|
743
|
+
/**
|
|
744
|
+
* Creates a literal type node (e.g., `'hello'`, `42`, `true`).
|
|
745
|
+
*/
|
|
746
|
+
const createLiteralTypeNode = factory.createLiteralTypeNode;
|
|
747
|
+
/**
|
|
748
|
+
* Creates an identifier node.
|
|
749
|
+
*/
|
|
750
|
+
const createIdentifier = factory.createIdentifier;
|
|
751
|
+
/**
|
|
752
|
+
* Creates an optional type node (e.g., `T | undefined`).
|
|
753
|
+
*/
|
|
754
|
+
const createOptionalTypeNode = factory.createOptionalTypeNode;
|
|
755
|
+
/**
|
|
756
|
+
* Creates a tuple type node (e.g., `[string, number]`).
|
|
757
|
+
*/
|
|
758
|
+
const createTupleTypeNode = factory.createTupleTypeNode;
|
|
759
|
+
/**
|
|
760
|
+
* Creates a rest type node for variadic tuple elements (e.g., `...T[]`).
|
|
761
|
+
*/
|
|
762
|
+
const createRestTypeNode = factory.createRestTypeNode;
|
|
763
|
+
/**
|
|
764
|
+
* Creates a boolean true literal type node.
|
|
765
|
+
*/
|
|
766
|
+
const createTrue = factory.createTrue;
|
|
767
|
+
/**
|
|
768
|
+
* Creates a boolean false literal type node.
|
|
769
|
+
*/
|
|
770
|
+
const createFalse = factory.createFalse;
|
|
771
|
+
/**
|
|
772
|
+
* Creates a prefix unary expression (e.g., negative numbers, logical not).
|
|
773
|
+
*/
|
|
774
|
+
const createPrefixUnaryExpression = factory.createPrefixUnaryExpression;
|
|
775
|
+
/**
|
|
776
|
+
* Converts a primitive const value to a TypeScript literal type node.
|
|
777
|
+
* Handles negative numbers via a prefix unary expression.
|
|
778
|
+
*/
|
|
779
|
+
function constToTypeNode(value, format) {
|
|
780
|
+
if (format === "boolean") return createLiteralTypeNode(value === true ? createTrue() : createFalse());
|
|
781
|
+
if (format === "number" && typeof value === "number") {
|
|
782
|
+
if (value < 0) return createLiteralTypeNode(createPrefixUnaryExpression(SyntaxKind.MinusToken, createNumericLiteral(Math.abs(value))));
|
|
783
|
+
return createLiteralTypeNode(createNumericLiteral(value));
|
|
784
|
+
}
|
|
785
|
+
return createLiteralTypeNode(createStringLiteral(String(value)));
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Returns a `Date` reference type node when `representation` is `'date'`, otherwise falls back to `string`.
|
|
789
|
+
*/
|
|
790
|
+
function dateOrStringNode(node) {
|
|
791
|
+
return node.representation === "date" ? createTypeReferenceNode(createIdentifier("Date")) : keywordTypeNodes.string;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Maps an array of `SchemaNode`s through the printer, filtering out `null` and `undefined` results.
|
|
795
|
+
*/
|
|
796
|
+
function buildMemberNodes(members, print) {
|
|
797
|
+
return (members ?? []).map(print).filter(isNonNullable$1);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Builds a TypeScript tuple type node from an array schema's `items`,
|
|
801
|
+
* applying min/max slice and optional/rest element rules.
|
|
802
|
+
*/
|
|
803
|
+
function buildTupleNode(node, print) {
|
|
804
|
+
let items = (node.items ?? []).map(print).filter(isNonNullable$1);
|
|
805
|
+
const restNode = node.rest ? print(node.rest) ?? void 0 : void 0;
|
|
806
|
+
const { min, max } = node;
|
|
807
|
+
if (max !== void 0) {
|
|
808
|
+
items = items.slice(0, max);
|
|
809
|
+
if (items.length < max && restNode) items = [...items, ...Array(max - items.length).fill(restNode)];
|
|
810
|
+
}
|
|
811
|
+
if (min !== void 0) items = items.map((item, i) => i >= min ? createOptionalTypeNode(item) : item);
|
|
812
|
+
if (max === void 0 && restNode) items.push(createRestTypeNode(createArrayTypeNode(restNode)));
|
|
813
|
+
return createTupleTypeNode(items);
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Applies `nullable` and optional/nullish `| undefined` union modifiers to a property's resolved base type.
|
|
817
|
+
*/
|
|
818
|
+
function buildPropertyType(schema, baseType, optionalType, optional) {
|
|
819
|
+
const addsUndefined = OPTIONAL_ADDS_UNDEFINED.has(optionalType);
|
|
820
|
+
const meta = syncSchemaRef(schema);
|
|
821
|
+
let type = baseType;
|
|
822
|
+
if (meta.nullable) type = createUnionDeclaration({ nodes: [type, keywordTypeNodes.null] });
|
|
823
|
+
if ((optional || meta.nullish || meta.optional) && addsUndefined) type = createUnionDeclaration({ nodes: [type, keywordTypeNodes.undefined] });
|
|
824
|
+
return type;
|
|
825
|
+
}
|
|
826
|
+
const indexSignaturePrinter = ts.createPrinter();
|
|
827
|
+
const indexSignatureSource = ts.createSourceFile("", "", ts.ScriptTarget.Latest);
|
|
828
|
+
/**
|
|
829
|
+
* Creates a TypeScript index signature for `additionalProperties` and `patternProperties` on an
|
|
830
|
+
* object schema node. TypeScript allows only one string index signature and its value must be
|
|
831
|
+
* assignable from every named property, so both keywords collapse into a single signature: the
|
|
832
|
+
* union of all value types (deduplicated), or `unknown` when the object also has fixed properties.
|
|
833
|
+
* The key regex of `patternProperties` cannot be expressed by an index signature and is dropped.
|
|
834
|
+
*/
|
|
835
|
+
function buildIndexSignatures(node, propertyCount, print) {
|
|
836
|
+
const valueTypes = [];
|
|
837
|
+
if (node.additionalProperties && node.additionalProperties !== true) valueTypes.push(print(node.additionalProperties) ?? keywordTypeNodes.unknown);
|
|
838
|
+
else if (node.additionalProperties === true) valueTypes.push(keywordTypeNodes.unknown);
|
|
839
|
+
for (const schema of Object.values(node.patternProperties ?? {})) {
|
|
840
|
+
const patternType = print(schema) ?? keywordTypeNodes.unknown;
|
|
841
|
+
valueTypes.push(schema.nullable ? createUnionDeclaration({ nodes: [patternType, keywordTypeNodes.null] }) : patternType);
|
|
842
|
+
}
|
|
843
|
+
if (valueTypes.length === 0) return [];
|
|
844
|
+
const seen = /* @__PURE__ */ new Set();
|
|
845
|
+
const distinct = valueTypes.filter((type) => {
|
|
846
|
+
const key = indexSignaturePrinter.printNode(ts.EmitHint.Unspecified, type, indexSignatureSource);
|
|
847
|
+
return seen.has(key) ? false : (seen.add(key), true);
|
|
848
|
+
});
|
|
849
|
+
return [createIndexSignature(propertyCount > 0 ? keywordTypeNodes.unknown : createUnionDeclaration({ nodes: distinct }))];
|
|
850
|
+
}
|
|
851
|
+
//#endregion
|
|
852
|
+
//#region src/components/Enum.tsx
|
|
853
|
+
/**
|
|
854
|
+
* Resolves the runtime identifier name and the TypeScript type name for an enum schema node.
|
|
855
|
+
*
|
|
856
|
+
* The raw `node.name` may be a YAML key such as `"enumNames.Type"` which is not a
|
|
857
|
+
* valid TypeScript identifier. The resolver normalizes it. For inline enum properties the adapter
|
|
858
|
+
* already emits a PascalCase+suffix name, so resolution is typically a no-op.
|
|
859
|
+
*
|
|
860
|
+
* When `constCasing` is `'pascalCase'` and `typeSuffix` is empty, the const and the type
|
|
861
|
+
* resolve to the same name, which TypeScript merges into a single value+type declaration.
|
|
862
|
+
*/
|
|
863
|
+
function getEnumNames({ node, enum: enumOptions, resolver }) {
|
|
864
|
+
const resolved = resolver.name(node.name);
|
|
865
|
+
return {
|
|
866
|
+
enumName: enumOptions.constCasing === "pascalCase" ? resolved : camelCase(node.name),
|
|
867
|
+
typeName: ENUM_TYPES_WITH_KEY_SUFFIX.has(enumOptions.type) ? resolver.enum.keyName(node, enumOptions.typeSuffix) : resolved
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Renders the enum declaration(s) for a single named `EnumSchemaNode`.
|
|
872
|
+
*
|
|
873
|
+
* Depending on `enum.type` this may emit:
|
|
874
|
+
* - A runtime object (`asConst`) plus a `typeof` type alias
|
|
875
|
+
* - A `const enum` or plain `enum` declaration (`constEnum` / `enum`)
|
|
876
|
+
* - A union literal type alias (`literal`)
|
|
877
|
+
*
|
|
878
|
+
* The emitted `File.Source` nodes carry the resolved names so that the barrel
|
|
879
|
+
* index picks up the correct export identifiers.
|
|
880
|
+
*/
|
|
881
|
+
function Enum({ node, enum: enumOptions, resolver }) {
|
|
882
|
+
const { enumName, typeName } = getEnumNames({
|
|
883
|
+
node,
|
|
884
|
+
enum: enumOptions,
|
|
885
|
+
resolver
|
|
886
|
+
});
|
|
887
|
+
const [nameNode, typeNode] = createEnumDeclaration({
|
|
888
|
+
name: enumName,
|
|
889
|
+
typeName,
|
|
890
|
+
enums: node.namedEnumValues?.map((v) => [
|
|
891
|
+
trimQuotes(v.name.toString()),
|
|
892
|
+
v.value,
|
|
893
|
+
v.description
|
|
894
|
+
]) ?? node.enumValues?.filter((v) => v !== null && v !== void 0).map((v) => [trimQuotes(v.toString()), v]) ?? [],
|
|
895
|
+
type: enumOptions.type,
|
|
896
|
+
enumKeyCasing: enumOptions.keyCasing
|
|
897
|
+
});
|
|
898
|
+
const namesMerge = !!nameNode && enumName === typeName;
|
|
899
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [nameNode && /* @__PURE__ */ jsx(File.Source, {
|
|
900
|
+
name: enumName,
|
|
901
|
+
isExportable: true,
|
|
902
|
+
isIndexable: true,
|
|
903
|
+
isTypeOnly: false,
|
|
904
|
+
children: parserTs().print(nameNode)
|
|
905
|
+
}), /* @__PURE__ */ jsx(File.Source, {
|
|
906
|
+
name: typeName,
|
|
907
|
+
isIndexable: !namesMerge,
|
|
908
|
+
isExportable: !namesMerge && ENUM_TYPES_WITH_RUNTIME_VALUE.has(enumOptions.type),
|
|
909
|
+
isTypeOnly: ENUM_TYPES_WITH_TYPE_ONLY.has(enumOptions.type),
|
|
910
|
+
children: parserTs().print(typeNode)
|
|
911
|
+
})] });
|
|
912
|
+
}
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region ../../internals/shared/src/params.ts
|
|
915
|
+
/**
|
|
916
|
+
* Drops parameters that share the same name, keeping the first.
|
|
917
|
+
*
|
|
918
|
+
* A malformed spec can declare the same parameter name twice within one `in` location. Both would
|
|
919
|
+
* resolve to the same output property, so emitting both would yield an object type with a duplicate
|
|
920
|
+
* member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
|
|
921
|
+
* parameter names flow through unchanged, so no two distinct names ever collide here anymore.
|
|
922
|
+
*/
|
|
923
|
+
function dedupeParams(params) {
|
|
924
|
+
const seen = /* @__PURE__ */ new Set();
|
|
925
|
+
return params.filter((param) => {
|
|
926
|
+
if (seen.has(param.name)) return false;
|
|
927
|
+
seen.add(param.name);
|
|
928
|
+
return true;
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
//#endregion
|
|
932
|
+
//#region ../../internals/shared/src/operation.ts
|
|
933
|
+
/**
|
|
934
|
+
* Maps a content type to the PascalCase suffix used to name per-content-type variants
|
|
935
|
+
* (e.g. `application/json` → `Json`, `application/xml` → `Xml`, `multipart/form-data` → `FormData`).
|
|
936
|
+
*/
|
|
937
|
+
function getContentTypeSuffix(contentType) {
|
|
938
|
+
const baseType = contentType.split(";")[0].trim();
|
|
939
|
+
if (baseType === "application/json") return "Json";
|
|
940
|
+
if (baseType === "multipart/form-data") return "FormData";
|
|
941
|
+
if (baseType === "application/x-www-form-urlencoded") return "FormUrlEncoded";
|
|
942
|
+
const parts = (baseType.split("/").pop() ?? baseType).split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
943
|
+
if (parts.length === 0) return "Unknown";
|
|
944
|
+
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Appends a content-type suffix to a base name, keeping a trailing `Data` segment last
|
|
948
|
+
* (e.g. `AddPetData` + `Json` → `AddPetJsonData`, `AddPetStatus200` + `Xml` → `AddPetStatus200Xml`).
|
|
949
|
+
*/
|
|
950
|
+
function getPerContentTypeName(baseName, suffix) {
|
|
951
|
+
if (baseName.endsWith("Data")) return suffix.endsWith("Data") ? baseName.slice(0, -4) + suffix : `${baseName.slice(0, -4)}${suffix}Data`;
|
|
952
|
+
return baseName + suffix;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Resolves per-content-type variant names for a set of content entries, deduplicating suffix
|
|
956
|
+
* collisions with a numeric counter. Entries without a schema are skipped. The returned `suffix` is
|
|
957
|
+
* the final (possibly counter-augmented) value, so callers can derive parallel names in another
|
|
958
|
+
* namespace (e.g. plugin-faker deriving the matching plugin-ts type name).
|
|
959
|
+
*/
|
|
960
|
+
function resolveContentTypeVariants(entries, baseName) {
|
|
961
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
962
|
+
return entries.filter((entry) => entry.schema).map((entry) => {
|
|
963
|
+
const baseSuffix = getContentTypeSuffix(entry.contentType);
|
|
964
|
+
let suffix = baseSuffix;
|
|
965
|
+
let name = getPerContentTypeName(baseName, suffix);
|
|
966
|
+
let counter = 2;
|
|
967
|
+
while (usedNames.has(name)) {
|
|
968
|
+
suffix = `${baseSuffix}${counter++}`;
|
|
969
|
+
name = getPerContentTypeName(baseName, suffix);
|
|
970
|
+
}
|
|
971
|
+
usedNames.add(name);
|
|
972
|
+
return {
|
|
973
|
+
name,
|
|
974
|
+
suffix,
|
|
975
|
+
schema: entry.schema,
|
|
976
|
+
keysToOmit: entry.keysToOmit,
|
|
977
|
+
contentType: entry.contentType
|
|
978
|
+
};
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
function getOperationParameters(node) {
|
|
982
|
+
return {
|
|
983
|
+
path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
|
|
984
|
+
query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
|
|
985
|
+
header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
|
|
986
|
+
cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Builds the combined `{ body, path, query, headers }` options object schema for an operation,
|
|
991
|
+
* referencing the already-resolved body and grouped param names. Shared by `@kubb/plugin-ts`'s
|
|
992
|
+
* `Options` type and `@kubb/plugin-zod`'s inferred options schema, so both printers emit the same
|
|
993
|
+
* shape from the same inputs. `primitive: 'object'` is a no-op for the TS printer and tells the Zod
|
|
994
|
+
* printer to emit `z.object(…)` rather than a record.
|
|
995
|
+
*/
|
|
996
|
+
function buildOptionsSchema(node, resolver) {
|
|
997
|
+
const { path, query, header } = getOperationParameters(node);
|
|
998
|
+
const hasBody = Boolean(node.requestBody?.content?.[0]?.schema);
|
|
999
|
+
const createNever = () => ast.factory.createSchema({
|
|
1000
|
+
type: "never",
|
|
1001
|
+
primitive: void 0,
|
|
1002
|
+
optional: true
|
|
1003
|
+
});
|
|
1004
|
+
const groups = [
|
|
1005
|
+
{
|
|
1006
|
+
name: "path",
|
|
1007
|
+
params: path,
|
|
1008
|
+
resolve: resolver.param.path
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
name: "query",
|
|
1012
|
+
params: query,
|
|
1013
|
+
resolve: resolver.param.query
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
name: "headers",
|
|
1017
|
+
params: header,
|
|
1018
|
+
resolve: resolver.param.headers
|
|
1019
|
+
}
|
|
1020
|
+
];
|
|
1021
|
+
return ast.factory.createSchema({
|
|
1022
|
+
type: "object",
|
|
1023
|
+
primitive: "object",
|
|
1024
|
+
deprecated: node.deprecated,
|
|
1025
|
+
properties: [ast.factory.createProperty({
|
|
1026
|
+
name: "body",
|
|
1027
|
+
required: hasBody,
|
|
1028
|
+
schema: hasBody ? ast.factory.createSchema({
|
|
1029
|
+
type: "ref",
|
|
1030
|
+
name: resolver.response.body(node)
|
|
1031
|
+
}) : createNever()
|
|
1032
|
+
}), ...groups.map(({ name, params, resolve }) => {
|
|
1033
|
+
const required = params.some((param) => param.required);
|
|
1034
|
+
return ast.factory.createProperty({
|
|
1035
|
+
name,
|
|
1036
|
+
required,
|
|
1037
|
+
schema: params.length > 0 ? ast.factory.createSchema({
|
|
1038
|
+
type: "ref",
|
|
1039
|
+
name: resolve.call(resolver.param, node, params[0]),
|
|
1040
|
+
optional: !required
|
|
1041
|
+
}) : createNever()
|
|
1042
|
+
});
|
|
1043
|
+
})]
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
//#endregion
|
|
1047
|
+
//#region ../../internals/shared/src/resolver.ts
|
|
1048
|
+
/**
|
|
1049
|
+
* Resolves a single operation parameter name with the
|
|
1050
|
+
* `<operationId> <in> <name>` template.
|
|
1051
|
+
*
|
|
1052
|
+
* @example
|
|
1053
|
+
* `operationParamName.call(resolver, node, param) // → 'DeletePetPathPetId'`
|
|
1054
|
+
*/
|
|
1055
|
+
function operationParamName(node, param) {
|
|
1056
|
+
return this.name(`${node.operationId} ${param.in} ${param.name}`);
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Builds the shared `param` namespace. Spread the result into `createResolver`
|
|
1060
|
+
* and override individual methods next to it when a plugin deviates.
|
|
1061
|
+
*
|
|
1062
|
+
* @example
|
|
1063
|
+
* ```ts
|
|
1064
|
+
* createResolver<PluginTs>({ param: createOperationParamResolver(), ... })
|
|
1065
|
+
* ```
|
|
1066
|
+
*/
|
|
1067
|
+
function createOperationParamResolver() {
|
|
1068
|
+
return {
|
|
1069
|
+
name: operationParamName,
|
|
1070
|
+
path(node) {
|
|
1071
|
+
return this.name(`${node.operationId} Path`);
|
|
1072
|
+
},
|
|
1073
|
+
query(node) {
|
|
1074
|
+
return this.name(`${node.operationId} Query`);
|
|
1075
|
+
},
|
|
1076
|
+
headers(node) {
|
|
1077
|
+
return this.name(`${node.operationId} Headers`);
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Builds the shared `response` namespace. Spread the result into
|
|
1083
|
+
* `createResolver` and add plugin-specific methods (`options`, `error`) next
|
|
1084
|
+
* to it.
|
|
1085
|
+
*
|
|
1086
|
+
* @example
|
|
1087
|
+
* ```ts
|
|
1088
|
+
* createResolver<PluginTs>({ response: { ...createOperationResponseResolver(), options(node) {...} }, ... })
|
|
1089
|
+
* ```
|
|
1090
|
+
*/
|
|
1091
|
+
function createOperationResponseResolver() {
|
|
1092
|
+
return {
|
|
1093
|
+
status(node, statusCode) {
|
|
1094
|
+
return this.name(`${node.operationId} Status ${statusCode}`);
|
|
1095
|
+
},
|
|
1096
|
+
body(node) {
|
|
1097
|
+
return this.name(`${node.operationId} Body`);
|
|
1098
|
+
},
|
|
1099
|
+
responses(node) {
|
|
1100
|
+
return this.name(`${node.operationId} Responses`);
|
|
1101
|
+
},
|
|
1102
|
+
response(node) {
|
|
1103
|
+
return this.name(`${node.operationId} Response`);
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Builds a resolver `file` override whose base name runs every path segment
|
|
1109
|
+
* through `toFilePath`, casing the final segment with `caseLast`.
|
|
1110
|
+
*
|
|
1111
|
+
* @example
|
|
1112
|
+
* ```ts
|
|
1113
|
+
* createResolver<PluginTs>({ file: createCasedFile(pascalCase), ... })
|
|
1114
|
+
* ```
|
|
1115
|
+
*/
|
|
1116
|
+
function createCasedFile(caseLast) {
|
|
1117
|
+
return { baseName({ name, extname }) {
|
|
1118
|
+
return `${toFilePath(name, caseLast)}${extname}`;
|
|
1119
|
+
} };
|
|
1120
|
+
}
|
|
1121
|
+
//#endregion
|
|
1122
|
+
//#region ../../internals/shared/src/refs.ts
|
|
1123
|
+
/**
|
|
1124
|
+
* Collects the resolved target name of every pointer-carrying ref in a schema tree, in
|
|
1125
|
+
* first-occurrence order. Use this for name-only checks (e.g. redeclaration detection) where
|
|
1126
|
+
* `resolver.imports` would resolve file paths that are then discarded.
|
|
1127
|
+
*/
|
|
1128
|
+
function collectRefNames(schema) {
|
|
1129
|
+
return ast.collectSync(schema, { schema: (node) => {
|
|
1130
|
+
const refNode = ast.narrowSchema(node, "ref");
|
|
1131
|
+
if (!refNode?.ref) return null;
|
|
1132
|
+
return ast.resolveRefName(refNode);
|
|
1133
|
+
} });
|
|
1134
|
+
}
|
|
1135
|
+
//#endregion
|
|
1136
|
+
//#region ../../internals/shared/src/group.ts
|
|
1137
|
+
/**
|
|
1138
|
+
* Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
|
|
1139
|
+
* shared default naming so every plugin groups output consistently:
|
|
1140
|
+
*
|
|
1141
|
+
* - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
|
|
1142
|
+
* - other groups use the camelCased group (`pet store` → `petStore`).
|
|
1143
|
+
*
|
|
1144
|
+
* A user-provided `group.name` always wins over the default namer, so callers stay in
|
|
1145
|
+
* control of their output folders. Returns `null` when grouping is disabled, matching the
|
|
1146
|
+
* per-plugin convention.
|
|
1147
|
+
*
|
|
1148
|
+
* @param group - The user-supplied group option, or `undefined` to disable grouping.
|
|
1149
|
+
*
|
|
1150
|
+
* @example
|
|
1151
|
+
* ```ts
|
|
1152
|
+
* createGroupConfig(group) // shared across every plugin
|
|
1153
|
+
* ```
|
|
1154
|
+
*/
|
|
1155
|
+
function createGroupConfig(group) {
|
|
1156
|
+
if (!group) return null;
|
|
1157
|
+
const defaultName = (ctx) => {
|
|
1158
|
+
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
|
|
1159
|
+
return camelCase(ctx.group);
|
|
1160
|
+
};
|
|
1161
|
+
return {
|
|
1162
|
+
...group,
|
|
1163
|
+
name: group.name ? group.name : defaultName
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
//#endregion
|
|
1167
|
+
//#region ../../internals/shared/src/schemaTraversal.ts
|
|
1168
|
+
/**
|
|
1169
|
+
* Maps each property of an object schema to its transformed output. Pairs every result with the
|
|
1170
|
+
* original property so the printer keeps full control over modifiers, getters, and key syntax.
|
|
1171
|
+
*
|
|
1172
|
+
* @example
|
|
1173
|
+
* ```ts
|
|
1174
|
+
* const entries = mapSchemaProperties(node, (schema) => this.transform(schema))
|
|
1175
|
+
* // entries: [{ name: 'id', property, output: 'z.number()' }, ...]
|
|
1176
|
+
* ```
|
|
1177
|
+
*/
|
|
1178
|
+
function mapSchemaProperties(node, transform) {
|
|
1179
|
+
return node.properties.map((property) => ({
|
|
1180
|
+
name: property.name,
|
|
1181
|
+
property,
|
|
1182
|
+
output: transform(property.schema)
|
|
1183
|
+
}));
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Maps each item of an array or tuple schema to its transformed output, pairing every result with
|
|
1187
|
+
* the original item.
|
|
1188
|
+
*/
|
|
1189
|
+
function mapSchemaItems(node, transform) {
|
|
1190
|
+
return (node.items ?? []).map((schema) => ({
|
|
1191
|
+
schema,
|
|
1192
|
+
output: transform(schema)
|
|
1193
|
+
}));
|
|
1194
|
+
}
|
|
1195
|
+
//#endregion
|
|
1196
|
+
//#region src/utils.ts
|
|
1197
|
+
/**
|
|
1198
|
+
* Tells whether a `const` (single-value enum) should render as a bare literal type (`'active'`)
|
|
1199
|
+
* rather than a named enum reference or a runtime enum declaration.
|
|
1200
|
+
*
|
|
1201
|
+
* The parser folds `const` into a single-value enum node. The adapter decides which schemas are
|
|
1202
|
+
* named enums and lists them in `enumSchemaNames`, and references to those names get suffixed (for
|
|
1203
|
+
* example `StatusKey`). A const renders as a literal only when the adapter has not registered it as
|
|
1204
|
+
* a named enum, which keeps the declaration and its references in sync across adapter versions.
|
|
1205
|
+
*/
|
|
1206
|
+
function isInlineConstEnum(node, enumSchemaNames) {
|
|
1207
|
+
return (node.namedEnumValues ?? node.enumValues ?? []).length === 1 && !(node.name && enumSchemaNames?.has(node.name));
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Collects JSDoc annotation strings for a schema node.
|
|
1211
|
+
*
|
|
1212
|
+
* Only uses official JSDoc tags from https://jsdoc.app/: `@description`, `@deprecated`, `@default`, `@example`, `@type`.
|
|
1213
|
+
* Constraint metadata (min/max length, pattern, multipleOf, min/maxProperties) is emitted as plain-text lines.
|
|
1214
|
+
|
|
1215
|
+
*/
|
|
1216
|
+
function isSchemaOptional(schema) {
|
|
1217
|
+
return Boolean("optional" in schema && schema.optional || "nullish" in schema && schema.nullish);
|
|
1218
|
+
}
|
|
1219
|
+
function formatExample(value) {
|
|
1220
|
+
if (value === null || typeof value !== "object") return String(value);
|
|
1221
|
+
return (JSON.stringify(value) ?? String(value)).replaceAll("*/", "*\\/");
|
|
1222
|
+
}
|
|
1223
|
+
function buildPropertyJSDocComments(schema, optional) {
|
|
1224
|
+
const meta = syncSchemaRef(schema);
|
|
1225
|
+
const isArray = meta?.primitive === "array";
|
|
1226
|
+
const hasDescription = meta && "description" in meta && meta.description;
|
|
1227
|
+
const formatComment = meta && "format" in meta && meta.format ? hasDescription ? [" ", `Format: \`${meta.format}\``] : ["@description", `Format: \`${meta.format}\``] : [];
|
|
1228
|
+
const exampleValues = meta?.examples ?? [];
|
|
1229
|
+
const comments = [
|
|
1230
|
+
hasDescription ? `@description ${jsStringEscape(meta.description)}` : null,
|
|
1231
|
+
...formatComment,
|
|
1232
|
+
meta && "deprecated" in meta && meta.deprecated ? "@deprecated" : null,
|
|
1233
|
+
!isArray && meta && "min" in meta && meta.min !== void 0 ? `@minLength ${meta.min}` : null,
|
|
1234
|
+
!isArray && meta && "max" in meta && meta.max !== void 0 ? `@maxLength ${meta.max}` : null,
|
|
1235
|
+
meta && "pattern" in meta && meta.pattern ? `@pattern ${meta.pattern}` : null,
|
|
1236
|
+
meta && "default" in meta && meta.default !== void 0 ? `@default ${"primitive" in meta && meta.primitive === "string" && typeof meta.default === "string" ? stringify(meta.default) : meta.default}` : null,
|
|
1237
|
+
...exampleValues.map((example) => `@example ${formatExample(example)}`)
|
|
1238
|
+
].filter(Boolean);
|
|
1239
|
+
const typeTag = comments.length && meta && "primitive" in meta && meta.primitive ? [`@type ${meta.primitive}`, optional ?? isSchemaOptional(schema) ? " | undefined" : null].filter(Boolean).join("") : null;
|
|
1240
|
+
return [...comments, typeTag].filter(Boolean);
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Builds the object schema for a group of parameters sharing one `in` location (path, query, or
|
|
1244
|
+
* header), embedding each param's own schema (and JSDoc) directly rather than referencing a
|
|
1245
|
+
* separate per-param type — the group itself is the only type these params get exported as.
|
|
1246
|
+
*/
|
|
1247
|
+
function buildParams({ params }) {
|
|
1248
|
+
return ast.factory.createSchema({
|
|
1249
|
+
type: "object",
|
|
1250
|
+
properties: params.map((param) => ast.factory.createProperty({
|
|
1251
|
+
name: param.name,
|
|
1252
|
+
required: param.required,
|
|
1253
|
+
schema: ast.factory.createSchema({
|
|
1254
|
+
...param.schema,
|
|
1255
|
+
optional: !param.required
|
|
1256
|
+
})
|
|
1257
|
+
}))
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* The schema a status occupies in the `<Name>Responses` record. A status that documents several
|
|
1262
|
+
* content types becomes a `{ contentType; data }` union so the runtime can surface the negotiated type
|
|
1263
|
+
* on `result.parsed`, while the standalone `<Name>StatusNNN` alias stays the plain body union that the
|
|
1264
|
+
* query hooks and `result.data` use.
|
|
1265
|
+
*/
|
|
1266
|
+
function buildResponseRecordEntry(node, res, resolver) {
|
|
1267
|
+
const statusName = resolver.response.status(node, res.statusCode);
|
|
1268
|
+
const variants = (res.content ?? []).filter((entry) => entry.schema);
|
|
1269
|
+
if (variants.length <= 1) return ast.factory.createSchema({
|
|
1270
|
+
type: "ref",
|
|
1271
|
+
name: statusName
|
|
1272
|
+
});
|
|
1273
|
+
return ast.factory.createSchema({
|
|
1274
|
+
type: "union",
|
|
1275
|
+
members: resolveContentTypeVariants(variants, statusName).map((variant) => ast.factory.createSchema({
|
|
1276
|
+
type: "object",
|
|
1277
|
+
properties: [ast.factory.createProperty({
|
|
1278
|
+
name: "contentType",
|
|
1279
|
+
required: true,
|
|
1280
|
+
schema: ast.factory.createSchema({
|
|
1281
|
+
type: "enum",
|
|
1282
|
+
enumValues: [variant.contentType]
|
|
1283
|
+
})
|
|
1284
|
+
}), ast.factory.createProperty({
|
|
1285
|
+
name: "data",
|
|
1286
|
+
required: true,
|
|
1287
|
+
schema: ast.factory.createSchema({
|
|
1288
|
+
type: "ref",
|
|
1289
|
+
name: variant.name
|
|
1290
|
+
})
|
|
1291
|
+
})]
|
|
1292
|
+
}))
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
function buildResponses(node, { resolver }) {
|
|
1296
|
+
return ast.factory.createSchema({
|
|
1297
|
+
type: "object",
|
|
1298
|
+
properties: node.responses.map((res) => ast.factory.createProperty({
|
|
1299
|
+
name: String(res.statusCode),
|
|
1300
|
+
required: true,
|
|
1301
|
+
schema: buildResponseRecordEntry(node, res, resolver)
|
|
1302
|
+
}))
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
function buildResponseUnion(node, { resolver }) {
|
|
1306
|
+
const responsesWithSchema = node.responses.filter((res) => res.content?.some((entry) => entry.schema));
|
|
1307
|
+
if (responsesWithSchema.length === 0) return null;
|
|
1308
|
+
return ast.factory.createSchema({
|
|
1309
|
+
type: "union",
|
|
1310
|
+
members: responsesWithSchema.map((res) => ast.factory.createSchema({
|
|
1311
|
+
type: "ref",
|
|
1312
|
+
name: resolver.response.status(node, res.statusCode)
|
|
1313
|
+
}))
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
//#endregion
|
|
1317
|
+
//#region src/components/Type.tsx
|
|
1318
|
+
function Type({ name, node, printer, enum: enumOptions, resolver }) {
|
|
1319
|
+
const enumSchemaNodes = ast.collectSync(node, { schema(n) {
|
|
1320
|
+
const enumNode = ast.narrowSchema(n, ast.schemaTypes.enum);
|
|
1321
|
+
if (enumNode?.name && !isInlineConstEnum(enumNode, printer.options.enumSchemaNames)) return enumNode;
|
|
1322
|
+
} });
|
|
1323
|
+
const output = printer.print(node);
|
|
1324
|
+
if (!output) return;
|
|
1325
|
+
const enums = [...new Map(enumSchemaNodes.map((n) => [n.name, n])).values()].map((node) => {
|
|
1326
|
+
return {
|
|
1327
|
+
node,
|
|
1328
|
+
...getEnumNames({
|
|
1329
|
+
node,
|
|
1330
|
+
enum: enumOptions,
|
|
1331
|
+
resolver
|
|
1332
|
+
})
|
|
1333
|
+
};
|
|
1334
|
+
});
|
|
1335
|
+
const shouldExportEnums = enumOptions.type !== "inlineLiteral";
|
|
1336
|
+
const shouldExportType = enumOptions.type === "inlineLiteral" || enums.every((item) => item.typeName !== name);
|
|
1337
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [shouldExportEnums && enums.map(({ node }) => /* @__PURE__ */ jsx(Enum, {
|
|
1338
|
+
node,
|
|
1339
|
+
enum: enumOptions,
|
|
1340
|
+
resolver
|
|
1341
|
+
}, node.name)), shouldExportType && /* @__PURE__ */ jsx(File.Source, {
|
|
1342
|
+
name,
|
|
1343
|
+
isTypeOnly: true,
|
|
1344
|
+
isExportable: true,
|
|
1345
|
+
isIndexable: true,
|
|
1346
|
+
children: output
|
|
1347
|
+
})] });
|
|
1348
|
+
}
|
|
1349
|
+
//#endregion
|
|
1350
|
+
//#region src/printers/printerTs.ts
|
|
1351
|
+
const { isNonNullable } = factory_exports;
|
|
1352
|
+
/**
|
|
1353
|
+
* TypeScript type printer built with `definePrinter`.
|
|
1354
|
+
*
|
|
1355
|
+
* Converts a `SchemaNode` AST node into a TypeScript AST node:
|
|
1356
|
+
* - **`printer.print(node)`** — when `options.typeName` is set, returns a full
|
|
1357
|
+
* `type Name = …` or `interface Name { … }` declaration (`ts.Node`).
|
|
1358
|
+
* Without `typeName`, returns the raw `ts.TypeNode` for the schema.
|
|
1359
|
+
*
|
|
1360
|
+
* Dispatches on `node.type` to the appropriate handler in `nodes`. Options are closed
|
|
1361
|
+
* over per printer instance, so each call to `printerTs(options)` produces an independent printer.
|
|
1362
|
+
*
|
|
1363
|
+
* @example Raw type node (no `typeName`)
|
|
1364
|
+
* ```ts
|
|
1365
|
+
* const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enum: { type: 'inlineLiteral' } })
|
|
1366
|
+
* const typeNode = printer.print(schemaNode) // ts.TypeNode
|
|
1367
|
+
* ```
|
|
1368
|
+
*
|
|
1369
|
+
* @example Full declaration (with `typeName`)
|
|
1370
|
+
* ```ts
|
|
1371
|
+
* const printer = printerTs({ optionalType: 'questionToken', arrayType: 'array', enum: { type: 'inlineLiteral' }, typeName: 'MyType' })
|
|
1372
|
+
* const declaration = printer.print(schemaNode) // ts.TypeAliasDeclaration | ts.InterfaceDeclaration
|
|
1373
|
+
* ```
|
|
1374
|
+
*/
|
|
1375
|
+
const printerTs = ast.createPrinter((options) => {
|
|
1376
|
+
const addsUndefined = OPTIONAL_ADDS_UNDEFINED.has(options.optionalType);
|
|
1377
|
+
return {
|
|
1378
|
+
name: "typescript",
|
|
1379
|
+
options,
|
|
1380
|
+
nodes: {
|
|
1381
|
+
any: () => keywordTypeNodes.any,
|
|
1382
|
+
unknown: () => keywordTypeNodes.unknown,
|
|
1383
|
+
void: () => keywordTypeNodes.void,
|
|
1384
|
+
never: () => keywordTypeNodes.never,
|
|
1385
|
+
boolean: () => keywordTypeNodes.boolean,
|
|
1386
|
+
null: () => keywordTypeNodes.null,
|
|
1387
|
+
blob: () => createTypeReferenceNode("Blob", []),
|
|
1388
|
+
string: () => keywordTypeNodes.string,
|
|
1389
|
+
uuid: () => keywordTypeNodes.string,
|
|
1390
|
+
email: () => keywordTypeNodes.string,
|
|
1391
|
+
url: (node) => {
|
|
1392
|
+
if (node.path) return createUrlTemplateType(node.path);
|
|
1393
|
+
return keywordTypeNodes.string;
|
|
1394
|
+
},
|
|
1395
|
+
ipv4: () => keywordTypeNodes.string,
|
|
1396
|
+
ipv6: () => keywordTypeNodes.string,
|
|
1397
|
+
datetime: () => keywordTypeNodes.string,
|
|
1398
|
+
number: () => keywordTypeNodes.number,
|
|
1399
|
+
integer: () => keywordTypeNodes.number,
|
|
1400
|
+
bigint: () => keywordTypeNodes.bigint,
|
|
1401
|
+
date: dateOrStringNode,
|
|
1402
|
+
time: dateOrStringNode,
|
|
1403
|
+
ref(node) {
|
|
1404
|
+
if (!node.name) return null;
|
|
1405
|
+
const refName = ast.resolveRefName(node);
|
|
1406
|
+
if (!refName) return null;
|
|
1407
|
+
const name = node.ref && ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enum.type) && this.options.enum.typeSuffix && this.options.enumSchemaNames?.has(refName) ? this.options.resolver.enum.keyName({ name: refName }, this.options.enum.typeSuffix) : node.ref ? this.options.resolver.name(refName) : refName;
|
|
1408
|
+
return createTypeReferenceNode(name, void 0);
|
|
1409
|
+
},
|
|
1410
|
+
enum(node) {
|
|
1411
|
+
const values = node.namedEnumValues?.map((v) => v.value) ?? node.enumValues ?? [];
|
|
1412
|
+
if (this.options.enum.type === "inlineLiteral" || !node.name || isInlineConstEnum(node, this.options.enumSchemaNames)) return createUnionDeclaration({
|
|
1413
|
+
withParentheses: true,
|
|
1414
|
+
nodes: values.filter((v) => v !== null && v !== void 0).map((value) => constToTypeNode(value, typeof value)).filter(isNonNullable)
|
|
1415
|
+
}) ?? void 0;
|
|
1416
|
+
const resolvedName = ENUM_TYPES_WITH_KEY_SUFFIX.has(this.options.enum.type) && this.options.enum.typeSuffix ? this.options.resolver.enum.keyName(node, this.options.enum.typeSuffix) : this.options.resolver.name(node.name);
|
|
1417
|
+
return createTypeReferenceNode(resolvedName, void 0);
|
|
1418
|
+
},
|
|
1419
|
+
union(node) {
|
|
1420
|
+
const members = node.members ?? [];
|
|
1421
|
+
const hasStringLiteral = members.some((m) => {
|
|
1422
|
+
return ast.narrowSchema(m, ast.schemaTypes.enum)?.primitive === "string";
|
|
1423
|
+
});
|
|
1424
|
+
const hasPlainString = members.some((m) => isStringType(m));
|
|
1425
|
+
if (hasStringLiteral && hasPlainString) return createUnionDeclaration({
|
|
1426
|
+
withParentheses: true,
|
|
1427
|
+
nodes: members.map((m) => {
|
|
1428
|
+
if (isStringType(m)) return createIntersectionDeclaration({
|
|
1429
|
+
nodes: [keywordTypeNodes.string, createTypeLiteralNode([])],
|
|
1430
|
+
withParentheses: true
|
|
1431
|
+
});
|
|
1432
|
+
return this.transform(m);
|
|
1433
|
+
}).filter(isNonNullable)
|
|
1434
|
+
}) ?? void 0;
|
|
1435
|
+
return createUnionDeclaration({
|
|
1436
|
+
withParentheses: true,
|
|
1437
|
+
nodes: buildMemberNodes(members, this.transform)
|
|
1438
|
+
}) ?? void 0;
|
|
1439
|
+
},
|
|
1440
|
+
intersection(node) {
|
|
1441
|
+
return createIntersectionDeclaration({
|
|
1442
|
+
withParentheses: true,
|
|
1443
|
+
nodes: buildMemberNodes(node.members, this.transform)
|
|
1444
|
+
}) ?? null;
|
|
1445
|
+
},
|
|
1446
|
+
array(node) {
|
|
1447
|
+
return createArrayDeclaration({
|
|
1448
|
+
nodes: mapSchemaItems(node, (item) => this.transform(item)).map(({ output }) => output).filter(isNonNullable),
|
|
1449
|
+
arrayType: this.options.arrayType
|
|
1450
|
+
}) ?? null;
|
|
1451
|
+
},
|
|
1452
|
+
tuple(node) {
|
|
1453
|
+
return buildTupleNode(node, this.transform) ?? null;
|
|
1454
|
+
},
|
|
1455
|
+
object(node) {
|
|
1456
|
+
const { transform, options } = this;
|
|
1457
|
+
const addsQuestionToken = OPTIONAL_ADDS_QUESTION_TOKEN.has(options.optionalType);
|
|
1458
|
+
const propertyNodes = mapSchemaProperties(node, (schema) => transform(schema)).map(({ name, property, output }) => {
|
|
1459
|
+
const baseType = output ?? keywordTypeNodes.unknown;
|
|
1460
|
+
const optional = !property.required || !!property.schema.optional || !!property.schema.nullish;
|
|
1461
|
+
const type = buildPropertyType(property.schema, baseType, options.optionalType, optional);
|
|
1462
|
+
const propMeta = syncSchemaRef(property.schema);
|
|
1463
|
+
return appendJSDocToNode({
|
|
1464
|
+
node: createPropertySignature({
|
|
1465
|
+
questionToken: optional ? addsQuestionToken : false,
|
|
1466
|
+
name,
|
|
1467
|
+
type,
|
|
1468
|
+
readOnly: propMeta?.readOnly
|
|
1469
|
+
}),
|
|
1470
|
+
comments: buildPropertyJSDocComments(property.schema, optional)
|
|
1471
|
+
});
|
|
1472
|
+
});
|
|
1473
|
+
const allElements = [...propertyNodes, ...buildIndexSignatures(node, propertyNodes.length, transform)];
|
|
1474
|
+
if (!allElements.length) return keywordTypeNodes.object;
|
|
1475
|
+
return createTypeLiteralNode(allElements);
|
|
1476
|
+
},
|
|
1477
|
+
...options.nodes
|
|
1478
|
+
},
|
|
1479
|
+
print(node) {
|
|
1480
|
+
const { name, syntaxType = "type", description, keysToOmit } = this.options;
|
|
1481
|
+
const transformed = this.transform(node);
|
|
1482
|
+
if (!transformed) return null;
|
|
1483
|
+
const meta = syncSchemaRef(node);
|
|
1484
|
+
if (!name) {
|
|
1485
|
+
const withNullable = meta.nullable ? createUnionDeclaration({ nodes: [transformed, keywordTypeNodes.null] }) : transformed;
|
|
1486
|
+
const result = (meta.nullish || meta.optional) && addsUndefined ? createUnionDeclaration({ nodes: [withNullable, keywordTypeNodes.undefined] }) : withNullable;
|
|
1487
|
+
return parserTs().print(result);
|
|
1488
|
+
}
|
|
1489
|
+
const inner = (() => {
|
|
1490
|
+
const omitted = keysToOmit?.length ? createOmitDeclaration({
|
|
1491
|
+
keys: keysToOmit,
|
|
1492
|
+
type: transformed,
|
|
1493
|
+
nonNullable: true
|
|
1494
|
+
}) : transformed;
|
|
1495
|
+
const withNullable = meta.nullable ? createUnionDeclaration({ nodes: [omitted, keywordTypeNodes.null] }) : omitted;
|
|
1496
|
+
return meta.nullish || meta.optional ? createUnionDeclaration({ nodes: [withNullable, keywordTypeNodes.undefined] }) : withNullable;
|
|
1497
|
+
})();
|
|
1498
|
+
const typeNode = createTypeDeclaration({
|
|
1499
|
+
name,
|
|
1500
|
+
isExportable: true,
|
|
1501
|
+
type: inner,
|
|
1502
|
+
syntax: syntaxType === "type" || inner.kind === syntaxKind.union || !!keysToOmit?.length ? "type" : "interface",
|
|
1503
|
+
comments: buildPropertyJSDocComments({
|
|
1504
|
+
...meta,
|
|
1505
|
+
description
|
|
1506
|
+
})
|
|
1507
|
+
});
|
|
1508
|
+
return parserTs().print(typeNode);
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
});
|
|
1512
|
+
//#endregion
|
|
1513
|
+
//#region src/generators/typeGenerator.tsx
|
|
1514
|
+
/**
|
|
1515
|
+
* Resolves the imported type name for a referenced schema. An enum schema emitted
|
|
1516
|
+
* as a `const` object imports its suffixed key alias (e.g. `StatusKey`); every other
|
|
1517
|
+
* schema imports its plain resolved name.
|
|
1518
|
+
*/
|
|
1519
|
+
function resolveImportName({ schemaName, enumOptions, enumSchemaNames, resolver }) {
|
|
1520
|
+
if (ENUM_TYPES_WITH_KEY_SUFFIX.has(enumOptions.type) && enumOptions.typeSuffix && enumSchemaNames.has(schemaName)) return resolver.enum.keyName({ name: schemaName }, enumOptions.typeSuffix);
|
|
1521
|
+
return resolver.name(schemaName);
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Built-in generator for `@kubb/plugin-ts`. Emits one TypeScript file per
|
|
1525
|
+
* schema in the spec plus per-operation request, response, and parameter
|
|
1526
|
+
* types. Drop-replace with a custom `Generator<PluginTs>` to change how
|
|
1527
|
+
* TypeScript output is produced.
|
|
1528
|
+
*/
|
|
1529
|
+
const typeGenerator = defineGenerator({
|
|
1530
|
+
name: "typescript",
|
|
1531
|
+
renderer: jsxRenderer,
|
|
1532
|
+
schema(node, ctx) {
|
|
1533
|
+
const { enum: enumOptions, syntaxType, optionalType, arrayType, output, group, printer } = ctx.options;
|
|
1534
|
+
const { config, resolver, root } = ctx;
|
|
1535
|
+
if (!node.name) return;
|
|
1536
|
+
const enumSchemaNames = new Set(ctx.meta.enumNames);
|
|
1537
|
+
const importName = (schemaName) => resolveImportName({
|
|
1538
|
+
schemaName,
|
|
1539
|
+
enumOptions,
|
|
1540
|
+
enumSchemaNames,
|
|
1541
|
+
resolver
|
|
1542
|
+
});
|
|
1543
|
+
const imports = resolver.imports({
|
|
1544
|
+
node,
|
|
1545
|
+
root,
|
|
1546
|
+
output,
|
|
1547
|
+
group: group ?? void 0,
|
|
1548
|
+
name: importName
|
|
1549
|
+
});
|
|
1550
|
+
const enumNode = ast.narrowSchema(node, ast.schemaTypes.enum);
|
|
1551
|
+
const isEnumSchema = !!enumNode && !isInlineConstEnum(enumNode, enumSchemaNames);
|
|
1552
|
+
const meta = {
|
|
1553
|
+
name: ENUM_TYPES_WITH_KEY_SUFFIX.has(enumOptions.type) && isEnumSchema ? resolver.enum.keyName(node, enumOptions.typeSuffix) : resolver.name(node.name),
|
|
1554
|
+
file: resolver.file({
|
|
1555
|
+
name: node.name,
|
|
1556
|
+
extname: ".ts",
|
|
1557
|
+
root,
|
|
1558
|
+
output,
|
|
1559
|
+
group: group ?? void 0
|
|
1560
|
+
})
|
|
1561
|
+
};
|
|
1562
|
+
const schemaPrinter = printerTs({
|
|
1563
|
+
optionalType,
|
|
1564
|
+
arrayType,
|
|
1565
|
+
enum: enumOptions,
|
|
1566
|
+
name: meta.name,
|
|
1567
|
+
syntaxType,
|
|
1568
|
+
description: node.description,
|
|
1569
|
+
resolver,
|
|
1570
|
+
enumSchemaNames,
|
|
1571
|
+
nodes: printer?.nodes
|
|
1572
|
+
});
|
|
1573
|
+
return /* @__PURE__ */ jsxs(File, {
|
|
1574
|
+
baseName: meta.file.baseName,
|
|
1575
|
+
path: meta.file.path,
|
|
1576
|
+
meta: meta.file.meta,
|
|
1577
|
+
banner: resolver.default.banner(ctx.meta, {
|
|
1578
|
+
output,
|
|
1579
|
+
config,
|
|
1580
|
+
file: {
|
|
1581
|
+
path: meta.file.path,
|
|
1582
|
+
baseName: meta.file.baseName
|
|
1583
|
+
}
|
|
1584
|
+
}),
|
|
1585
|
+
footer: resolver.default.footer(ctx.meta, {
|
|
1586
|
+
output,
|
|
1587
|
+
config,
|
|
1588
|
+
file: {
|
|
1589
|
+
path: meta.file.path,
|
|
1590
|
+
baseName: meta.file.baseName
|
|
1591
|
+
}
|
|
1592
|
+
}),
|
|
1593
|
+
children: [imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
|
|
1594
|
+
root: meta.file.path,
|
|
1595
|
+
path: imp.path,
|
|
1596
|
+
name: imp.name,
|
|
1597
|
+
isTypeOnly: true
|
|
1598
|
+
}, [
|
|
1599
|
+
node.name,
|
|
1600
|
+
imp.path,
|
|
1601
|
+
imp.isTypeOnly
|
|
1602
|
+
].join("-"))), /* @__PURE__ */ jsx(Type, {
|
|
1603
|
+
name: meta.name,
|
|
1604
|
+
node,
|
|
1605
|
+
enum: enumOptions,
|
|
1606
|
+
resolver,
|
|
1607
|
+
printer: schemaPrinter
|
|
1608
|
+
})]
|
|
1609
|
+
});
|
|
1610
|
+
},
|
|
1611
|
+
operation(node, ctx) {
|
|
1612
|
+
const { enum: enumOptions, optionalType, arrayType, syntaxType, group, output, printer } = ctx.options;
|
|
1613
|
+
const { config, resolver, root } = ctx;
|
|
1614
|
+
const meta = { file: resolver.file({
|
|
1615
|
+
name: node.operationId,
|
|
1616
|
+
extname: ".ts",
|
|
1617
|
+
tag: node.tags[0] ?? "default",
|
|
1618
|
+
path: node.path,
|
|
1619
|
+
root,
|
|
1620
|
+
output,
|
|
1621
|
+
group: group ?? void 0
|
|
1622
|
+
}) };
|
|
1623
|
+
const enumSchemaNames = new Set(ctx.meta.enumNames);
|
|
1624
|
+
const importName = (schemaName) => resolveImportName({
|
|
1625
|
+
schemaName,
|
|
1626
|
+
enumOptions,
|
|
1627
|
+
enumSchemaNames,
|
|
1628
|
+
resolver
|
|
1629
|
+
});
|
|
1630
|
+
function renderSchemaType({ schema, name, keysToOmit }) {
|
|
1631
|
+
if (!schema) return null;
|
|
1632
|
+
const imports = resolver.imports({
|
|
1633
|
+
node: schema,
|
|
1634
|
+
root,
|
|
1635
|
+
output,
|
|
1636
|
+
group: group ?? void 0,
|
|
1637
|
+
name: importName
|
|
1638
|
+
});
|
|
1639
|
+
const schemaPrinter = printerTs({
|
|
1640
|
+
optionalType,
|
|
1641
|
+
arrayType,
|
|
1642
|
+
enum: enumOptions,
|
|
1643
|
+
name,
|
|
1644
|
+
syntaxType,
|
|
1645
|
+
description: schema.description,
|
|
1646
|
+
keysToOmit,
|
|
1647
|
+
resolver,
|
|
1648
|
+
enumSchemaNames,
|
|
1649
|
+
nodes: printer?.nodes
|
|
1650
|
+
});
|
|
1651
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [imports.map((imp) => /* @__PURE__ */ jsx(File.Import, {
|
|
1652
|
+
root: meta.file.path,
|
|
1653
|
+
path: imp.path,
|
|
1654
|
+
name: imp.name,
|
|
1655
|
+
isTypeOnly: true
|
|
1656
|
+
}, [
|
|
1657
|
+
name,
|
|
1658
|
+
imp.path,
|
|
1659
|
+
imp.isTypeOnly
|
|
1660
|
+
].join("-"))), /* @__PURE__ */ jsx(Type, {
|
|
1661
|
+
name,
|
|
1662
|
+
node: schema,
|
|
1663
|
+
enum: enumOptions,
|
|
1664
|
+
resolver,
|
|
1665
|
+
printer: schemaPrinter
|
|
1666
|
+
})] });
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Emits an individual type per content type plus a plain union alias under `baseName`. Shared by
|
|
1670
|
+
* the request body and multi-content-type responses. The response record discriminates these
|
|
1671
|
+
* variants by content type in {@link buildResponses}; the standalone alias stays a plain union so
|
|
1672
|
+
* query hooks and `result.data` keep the bare body.
|
|
1673
|
+
*/
|
|
1674
|
+
function buildContentTypeVariants(entries, baseName, decorate) {
|
|
1675
|
+
const variants = resolveContentTypeVariants(entries, baseName);
|
|
1676
|
+
const unionSchema = ast.factory.createSchema({
|
|
1677
|
+
type: "union",
|
|
1678
|
+
members: variants.map((variant) => ast.factory.createSchema({
|
|
1679
|
+
type: "ref",
|
|
1680
|
+
name: variant.name
|
|
1681
|
+
}))
|
|
1682
|
+
});
|
|
1683
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [variants.map((variant) => renderSchemaType({
|
|
1684
|
+
schema: decorate ? decorate(variant.schema) : variant.schema,
|
|
1685
|
+
name: variant.name,
|
|
1686
|
+
keysToOmit: variant.keysToOmit
|
|
1687
|
+
})), renderSchemaType({
|
|
1688
|
+
schema: unionSchema,
|
|
1689
|
+
name: baseName
|
|
1690
|
+
})] });
|
|
1691
|
+
}
|
|
1692
|
+
const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
|
|
1693
|
+
const paramGroupTypes = [
|
|
1694
|
+
pathParams.length > 0 && renderSchemaType({
|
|
1695
|
+
schema: buildParams({ params: pathParams }),
|
|
1696
|
+
name: resolver.param.path(node, pathParams[0])
|
|
1697
|
+
}),
|
|
1698
|
+
queryParams.length > 0 && renderSchemaType({
|
|
1699
|
+
schema: buildParams({ params: queryParams }),
|
|
1700
|
+
name: resolver.param.query(node, queryParams[0])
|
|
1701
|
+
}),
|
|
1702
|
+
headerParams.length > 0 && renderSchemaType({
|
|
1703
|
+
schema: buildParams({ params: headerParams }),
|
|
1704
|
+
name: resolver.param.headers(node, headerParams[0])
|
|
1705
|
+
})
|
|
1706
|
+
];
|
|
1707
|
+
const requestBodyContent = node.requestBody?.content ?? [];
|
|
1708
|
+
function buildRequestType() {
|
|
1709
|
+
if (requestBodyContent.length === 0) return null;
|
|
1710
|
+
if (requestBodyContent.length === 1) {
|
|
1711
|
+
const entry = requestBodyContent[0];
|
|
1712
|
+
if (!entry.schema) return null;
|
|
1713
|
+
return renderSchemaType({
|
|
1714
|
+
schema: {
|
|
1715
|
+
...entry.schema,
|
|
1716
|
+
description: node.requestBody.description ?? entry.schema.description
|
|
1717
|
+
},
|
|
1718
|
+
name: resolver.response.body(node),
|
|
1719
|
+
keysToOmit: entry.keysToOmit
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
return buildContentTypeVariants(requestBodyContent, resolver.response.body(node), (schema) => ({
|
|
1723
|
+
...schema,
|
|
1724
|
+
description: node.requestBody.description ?? schema.description
|
|
1725
|
+
}));
|
|
1726
|
+
}
|
|
1727
|
+
const requestType = buildRequestType();
|
|
1728
|
+
const responseTypes = node.responses.map((res) => {
|
|
1729
|
+
const variants = (res.content ?? []).filter((entry) => entry.schema);
|
|
1730
|
+
if (variants.length > 1) return buildContentTypeVariants(variants, resolver.response.status(node, res.statusCode));
|
|
1731
|
+
const primary = variants[0] ?? res.content?.[0];
|
|
1732
|
+
return renderSchemaType({
|
|
1733
|
+
schema: primary?.schema ?? null,
|
|
1734
|
+
name: resolver.response.status(node, res.statusCode),
|
|
1735
|
+
keysToOmit: primary?.keysToOmit
|
|
1736
|
+
});
|
|
1737
|
+
});
|
|
1738
|
+
const optionsType = renderSchemaType({
|
|
1739
|
+
schema: buildOptionsSchema(node, resolver),
|
|
1740
|
+
name: resolver.response.options(node)
|
|
1741
|
+
});
|
|
1742
|
+
const responsesType = renderSchemaType({
|
|
1743
|
+
schema: buildResponses(node, { resolver }),
|
|
1744
|
+
name: resolver.response.responses(node)
|
|
1745
|
+
});
|
|
1746
|
+
function buildResponseType() {
|
|
1747
|
+
const hasSchema = (res) => (res.content ?? []).some((entry) => entry.schema);
|
|
1748
|
+
if (!node.responses.some(hasSchema)) return null;
|
|
1749
|
+
const responseName = resolver.response.response(node);
|
|
1750
|
+
const responsesWithSchema = node.responses.filter(hasSchema);
|
|
1751
|
+
if (new Set(responsesWithSchema.flatMap((res) => (res.content ?? []).flatMap((entry) => entry.schema ? collectRefNames(entry.schema).map(importName) : []))).has(responseName)) return null;
|
|
1752
|
+
return renderSchemaType({
|
|
1753
|
+
schema: {
|
|
1754
|
+
...buildResponseUnion(node, { resolver }),
|
|
1755
|
+
description: "Union of all possible responses"
|
|
1756
|
+
},
|
|
1757
|
+
name: responseName
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
const responseType = buildResponseType();
|
|
1761
|
+
return /* @__PURE__ */ jsxs(File, {
|
|
1762
|
+
baseName: meta.file.baseName,
|
|
1763
|
+
path: meta.file.path,
|
|
1764
|
+
meta: meta.file.meta,
|
|
1765
|
+
banner: resolver.default.banner(ctx.meta, {
|
|
1766
|
+
output,
|
|
1767
|
+
config,
|
|
1768
|
+
file: {
|
|
1769
|
+
path: meta.file.path,
|
|
1770
|
+
baseName: meta.file.baseName
|
|
1771
|
+
}
|
|
1772
|
+
}),
|
|
1773
|
+
footer: resolver.default.footer(ctx.meta, {
|
|
1774
|
+
output,
|
|
1775
|
+
config,
|
|
1776
|
+
file: {
|
|
1777
|
+
path: meta.file.path,
|
|
1778
|
+
baseName: meta.file.baseName
|
|
1779
|
+
}
|
|
1780
|
+
}),
|
|
1781
|
+
children: [
|
|
1782
|
+
paramGroupTypes,
|
|
1783
|
+
responseTypes,
|
|
1784
|
+
requestType,
|
|
1785
|
+
optionsType,
|
|
1786
|
+
responsesType,
|
|
1787
|
+
responseType
|
|
1788
|
+
]
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
//#endregion
|
|
1793
|
+
//#region src/resolvers/resolverTs.ts
|
|
1794
|
+
/**
|
|
1795
|
+
* Default resolver used by `@kubb/plugin-ts`. Decides the names and file paths
|
|
1796
|
+
* for every generated TypeScript type. Import this in other plugins that need
|
|
1797
|
+
* to reference the exact names `plugin-ts` produces without duplicating the
|
|
1798
|
+
* casing/file-layout rules.
|
|
1799
|
+
*
|
|
1800
|
+
* The `default` helpers are supplied by `createResolver`. This plugin overrides the top-level `name`
|
|
1801
|
+
* to use PascalCase for value and type names and `file` to write PascalCase file paths (dotted names
|
|
1802
|
+
* become `/`-joined), and groups the operation-specific naming under the `param`, `response`, and
|
|
1803
|
+
* `enum` namespaces.
|
|
1804
|
+
*
|
|
1805
|
+
* @example Resolve a type and file name
|
|
1806
|
+
* ```ts
|
|
1807
|
+
* import { resolverTs } from '@kubb/plugin-ts'
|
|
1808
|
+
*
|
|
1809
|
+
* resolverTs.name('list pets') // 'ListPets'
|
|
1810
|
+
* resolverTs.response.status(node, 200) // 'ListPetsStatus200'
|
|
1811
|
+
* ```
|
|
1812
|
+
*/
|
|
1813
|
+
const resolverTs = createResolver({
|
|
1814
|
+
pluginName: "plugin-ts",
|
|
1815
|
+
name(name) {
|
|
1816
|
+
return ensureValidVarName(pascalCase(name));
|
|
1817
|
+
},
|
|
1818
|
+
file: createCasedFile(pascalCase),
|
|
1819
|
+
param: createOperationParamResolver(),
|
|
1820
|
+
response: {
|
|
1821
|
+
...createOperationResponseResolver(),
|
|
1822
|
+
options(node) {
|
|
1823
|
+
return this.name(`${node.operationId} Options`);
|
|
1824
|
+
}
|
|
1825
|
+
},
|
|
1826
|
+
enum: { keyName(node, enumTypeSuffix = "key") {
|
|
1827
|
+
return `${this.name(node.name ?? "")}${enumTypeSuffix}`;
|
|
1828
|
+
} }
|
|
1829
|
+
});
|
|
1830
|
+
//#endregion
|
|
1831
|
+
//#region src/plugin.ts
|
|
1832
|
+
/**
|
|
1833
|
+
* Canonical plugin name for `@kubb/plugin-ts`. Used for driver lookups and
|
|
1834
|
+
* cross-plugin dependency references.
|
|
1835
|
+
*/
|
|
1836
|
+
const pluginTsName = "plugin-ts";
|
|
1837
|
+
/**
|
|
1838
|
+
* Generates TypeScript `type` aliases and `interface` declarations from an
|
|
1839
|
+
* OpenAPI spec. The foundation that every other Kubb plugin builds on:
|
|
1840
|
+
* clients, query hooks, mocks, and validators all reference the names this
|
|
1841
|
+
* plugin produces.
|
|
1842
|
+
*
|
|
1843
|
+
* @example
|
|
1844
|
+
* ```ts
|
|
1845
|
+
* import { defineConfig } from 'kubb/config'
|
|
1846
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
1847
|
+
*
|
|
1848
|
+
* export default defineConfig({
|
|
1849
|
+
* input: './petStore.yaml',
|
|
1850
|
+
* output: { path: './src/gen' },
|
|
1851
|
+
* plugins: [
|
|
1852
|
+
* pluginTs({
|
|
1853
|
+
* output: { path: './types' },
|
|
1854
|
+
* enum: { type: 'asConst' },
|
|
1855
|
+
* optionalType: 'questionTokenAndUndefined',
|
|
1856
|
+
* }),
|
|
1857
|
+
* ],
|
|
1858
|
+
* })
|
|
1859
|
+
* ```
|
|
1860
|
+
*/
|
|
1861
|
+
const pluginTs = definePlugin((options) => {
|
|
1862
|
+
const { output = {
|
|
1863
|
+
path: "types",
|
|
1864
|
+
barrel: { type: "named" }
|
|
1865
|
+
}, group, exclude = [], include, override = [], enum: enumOptions = {}, optionalType = "questionToken", arrayType = "array", syntaxType = "type", printer, resolver: userResolver, macros: userMacros } = options;
|
|
1866
|
+
const groupConfig = createGroupConfig(group);
|
|
1867
|
+
const resolvedEnum = {
|
|
1868
|
+
type: enumOptions.type ?? "asConst",
|
|
1869
|
+
constCasing: enumOptions.constCasing ?? "camelCase",
|
|
1870
|
+
typeSuffix: enumOptions.typeSuffix ?? "Key",
|
|
1871
|
+
keyCasing: enumOptions.keyCasing ?? "none"
|
|
1872
|
+
};
|
|
1873
|
+
return {
|
|
1874
|
+
name: pluginTsName,
|
|
1875
|
+
options,
|
|
1876
|
+
hooks: { "kubb:plugin:setup"(ctx) {
|
|
1877
|
+
ctx.setOptions({
|
|
1878
|
+
output,
|
|
1879
|
+
exclude,
|
|
1880
|
+
include,
|
|
1881
|
+
override,
|
|
1882
|
+
optionalType,
|
|
1883
|
+
group: groupConfig,
|
|
1884
|
+
arrayType,
|
|
1885
|
+
enum: resolvedEnum,
|
|
1886
|
+
syntaxType,
|
|
1887
|
+
printer
|
|
1888
|
+
});
|
|
1889
|
+
ctx.setResolver(userResolver ? Resolver.merge(resolverTs, userResolver) : resolverTs);
|
|
1890
|
+
if (userMacros?.length) ctx.setMacros(userMacros);
|
|
1891
|
+
ctx.addGenerator(typeGenerator);
|
|
1892
|
+
} }
|
|
1893
|
+
};
|
|
1894
|
+
});
|
|
1895
|
+
//#endregion
|
|
1896
|
+
//#region src/printers/functionParams.ts
|
|
1897
|
+
/**
|
|
1898
|
+
* Narrows a {@link TypeExpression} to a {@link TypeLiteralNode}.
|
|
1899
|
+
*/
|
|
1900
|
+
function isTypeLiteral(type) {
|
|
1901
|
+
return typeof type !== "string" && type.kind === "TypeLiteral";
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Narrows a {@link TypeExpression} to an {@link IndexedAccessTypeNode}.
|
|
1905
|
+
*/
|
|
1906
|
+
function isIndexedAccessType(type) {
|
|
1907
|
+
return typeof type !== "string" && type.kind === "IndexedAccessType";
|
|
1908
|
+
}
|
|
1909
|
+
/**
|
|
1910
|
+
* Creates a {@link TypeLiteralNode} representing an inline anonymous object type.
|
|
1911
|
+
*
|
|
1912
|
+
* @example
|
|
1913
|
+
* ```ts
|
|
1914
|
+
* createTypeLiteral({ members: [{ name: 'petId', type: 'string', optional: false }] })
|
|
1915
|
+
* // { petId: string }
|
|
1916
|
+
* ```
|
|
1917
|
+
*/
|
|
1918
|
+
function createTypeLiteral(props) {
|
|
1919
|
+
return {
|
|
1920
|
+
kind: "TypeLiteral",
|
|
1921
|
+
...props
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Creates an {@link IndexedAccessTypeNode} representing a single field accessed from a named type.
|
|
1926
|
+
*
|
|
1927
|
+
* @example
|
|
1928
|
+
* ```ts
|
|
1929
|
+
* createIndexedAccessType({ target: 'DeletePetPathParams', key: 'petId' })
|
|
1930
|
+
* // DeletePetPathParams['petId']
|
|
1931
|
+
* ```
|
|
1932
|
+
*/
|
|
1933
|
+
function createIndexedAccessType(props) {
|
|
1934
|
+
return {
|
|
1935
|
+
kind: "IndexedAccessType",
|
|
1936
|
+
...props
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Creates an {@link ObjectBindingPatternNode} for a destructured parameter binding.
|
|
1941
|
+
*
|
|
1942
|
+
* @example
|
|
1943
|
+
* ```ts
|
|
1944
|
+
* createObjectBindingPattern({ elements: [{ name: 'id' }, { name: 'name' }] })
|
|
1945
|
+
* // { id, name }
|
|
1946
|
+
* ```
|
|
1947
|
+
*/
|
|
1948
|
+
function createObjectBindingPattern(props) {
|
|
1949
|
+
return {
|
|
1950
|
+
kind: "ObjectBindingPattern",
|
|
1951
|
+
...props
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Creates a {@link FunctionParameterNode}. `optional` defaults to `false`.
|
|
1956
|
+
* Passing `properties` builds a destructured group: an {@link ObjectBindingPatternNode} name
|
|
1957
|
+
* paired with a {@link TypeLiteralNode} type.
|
|
1958
|
+
*
|
|
1959
|
+
* @example Optional param
|
|
1960
|
+
* ```ts
|
|
1961
|
+
* createFunctionParameter({ name: 'params', type: 'QueryParams', optional: true })
|
|
1962
|
+
* // → params?: QueryParams
|
|
1963
|
+
* ```
|
|
1964
|
+
*
|
|
1965
|
+
* @example Destructured group
|
|
1966
|
+
* ```ts
|
|
1967
|
+
* createFunctionParameter({ properties: [{ name: 'id', type: 'string' }, { name: 'name', type: 'string', optional: true }], default: '{}' })
|
|
1968
|
+
* // → { id, name }: { id: string; name?: string } = {}
|
|
1969
|
+
* ```
|
|
1970
|
+
*/
|
|
1971
|
+
function createFunctionParameter(input) {
|
|
1972
|
+
if ("properties" in input) return {
|
|
1973
|
+
kind: "FunctionParameter",
|
|
1974
|
+
name: createObjectBindingPattern({ elements: input.properties.map((p) => ({ name: p.name })) }),
|
|
1975
|
+
type: createTypeLiteral({ members: input.properties.map((p) => ({
|
|
1976
|
+
name: p.name,
|
|
1977
|
+
type: p.type,
|
|
1978
|
+
optional: p.optional ?? false
|
|
1979
|
+
})) }),
|
|
1980
|
+
optional: input.optional ?? false,
|
|
1981
|
+
...input.default !== void 0 ? { default: input.default } : {}
|
|
1982
|
+
};
|
|
1983
|
+
return {
|
|
1984
|
+
kind: "FunctionParameter",
|
|
1985
|
+
optional: false,
|
|
1986
|
+
...input
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Creates a {@link FunctionParametersNode} from an ordered list of parameters.
|
|
1991
|
+
*
|
|
1992
|
+
* @example
|
|
1993
|
+
* ```ts
|
|
1994
|
+
* const empty = createFunctionParameters()
|
|
1995
|
+
* // { params: [] }
|
|
1996
|
+
* ```
|
|
1997
|
+
*/
|
|
1998
|
+
function createFunctionParameters(props = {}) {
|
|
1999
|
+
return {
|
|
2000
|
+
kind: "FunctionParameters",
|
|
2001
|
+
params: [],
|
|
2002
|
+
...props
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
//#endregion
|
|
2006
|
+
//#region src/printers/functionPrinter.ts
|
|
2007
|
+
/**
|
|
2008
|
+
* Renders a {@link TypeExpression} to its TypeScript source.
|
|
2009
|
+
*
|
|
2010
|
+
* - a `string` is a type reference, returned as-is
|
|
2011
|
+
* - an `IndexedAccessType` becomes `objectType['indexType']`
|
|
2012
|
+
* - a `TypeLiteral` becomes `{ key: Type; key?: Type }`
|
|
2013
|
+
*
|
|
2014
|
+
* `transformType` is applied once to the fully rendered type, matching how the
|
|
2015
|
+
* printer wrapped reference types before the `ts.factory` model.
|
|
2016
|
+
*/
|
|
2017
|
+
function renderType(type, transformType) {
|
|
2018
|
+
const rendered = renderTypeExpression(type);
|
|
2019
|
+
return transformType ? transformType(rendered) : rendered;
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Renders an object-type key, quoting it as a string literal when it is not a valid
|
|
2023
|
+
* identifier so `{ 'content-type': string }` stays valid TypeScript.
|
|
2024
|
+
*/
|
|
2025
|
+
function renderKey(name) {
|
|
2026
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
2027
|
+
}
|
|
2028
|
+
function renderTypeExpression(type) {
|
|
2029
|
+
if (typeof type === "string") return type;
|
|
2030
|
+
if (isIndexedAccessType(type)) return `${type.target}['${type.key}']`;
|
|
2031
|
+
return `{ ${type.members.map((member) => {
|
|
2032
|
+
const value = renderTypeExpression(member.type);
|
|
2033
|
+
return member.optional ? `${renderKey(member.name)}?: ${value}` : `${renderKey(member.name)}: ${value}`;
|
|
2034
|
+
}).join("; ")} }`;
|
|
2035
|
+
}
|
|
2036
|
+
function groupMembers(param) {
|
|
2037
|
+
if (typeof param.name === "string") return null;
|
|
2038
|
+
return param.type && isTypeLiteral(param.type) ? param.type.members : [];
|
|
2039
|
+
}
|
|
2040
|
+
function rank(param) {
|
|
2041
|
+
if (param.rest) return PARAM_RANK.rest;
|
|
2042
|
+
if (param.default) return PARAM_RANK.withDefault;
|
|
2043
|
+
const members = groupMembers(param);
|
|
2044
|
+
if (members) return members.every((m) => m.optional) ? PARAM_RANK.optional : PARAM_RANK.required;
|
|
2045
|
+
return param.optional ? PARAM_RANK.optional : PARAM_RANK.required;
|
|
2046
|
+
}
|
|
2047
|
+
function sortParams(params) {
|
|
2048
|
+
return params.toSorted((a, b) => rank(a) - rank(b));
|
|
2049
|
+
}
|
|
2050
|
+
function sortedGroupMembers(name, type) {
|
|
2051
|
+
const members = type && isTypeLiteral(type) ? type.members : [];
|
|
2052
|
+
const memberRank = (optional) => optional ? PARAM_RANK.optional : PARAM_RANK.required;
|
|
2053
|
+
return name.elements.map((element, index) => ({
|
|
2054
|
+
name: element.name,
|
|
2055
|
+
propertyName: element.propertyName,
|
|
2056
|
+
type: members[index]?.type,
|
|
2057
|
+
optional: members[index]?.optional
|
|
2058
|
+
})).toSorted((a, b) => memberRank(a.optional) - memberRank(b.optional));
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Renders one binding element: the local name, prefixed with its source key when the
|
|
2062
|
+
* binding renames the property, as in `{ petId: id }`.
|
|
2063
|
+
*/
|
|
2064
|
+
function renderBindingMember(member, transformName) {
|
|
2065
|
+
const local = transformName ? transformName(member.name) : member.name;
|
|
2066
|
+
return member.propertyName ? `${member.propertyName}: ${local}` : local;
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Renders the type annotation of a destructured group. An inline object type is
|
|
2070
|
+
* rendered from the already-sorted members so its key order matches the binding;
|
|
2071
|
+
* a reference type is rendered as-is.
|
|
2072
|
+
*/
|
|
2073
|
+
function renderGroupType(type, sorted, transformType) {
|
|
2074
|
+
if (!type) return void 0;
|
|
2075
|
+
if (!isTypeLiteral(type)) return renderType(type);
|
|
2076
|
+
const typed = sorted.filter((member) => member.type !== void 0);
|
|
2077
|
+
if (!typed.length) return void 0;
|
|
2078
|
+
return `{ ${typed.map((member) => {
|
|
2079
|
+
const key = renderKey(member.propertyName ?? member.name);
|
|
2080
|
+
const value = renderType(member.type, transformType);
|
|
2081
|
+
return member.optional ? `${key}?: ${value}` : `${key}: ${value}`;
|
|
2082
|
+
}).join("; ")} }`;
|
|
2083
|
+
}
|
|
2084
|
+
function printParameter(node, options) {
|
|
2085
|
+
const { mode, transformName, transformType } = options;
|
|
2086
|
+
const bindingName = node.name;
|
|
2087
|
+
if (mode === "call") {
|
|
2088
|
+
if (typeof bindingName !== "string") return `{ ${sortedGroupMembers(bindingName, node.type).map((member) => renderBindingMember(member)).join(", ")} }`;
|
|
2089
|
+
const name = transformName ? transformName(bindingName) : bindingName;
|
|
2090
|
+
return node.rest ? `...${name}` : name;
|
|
2091
|
+
}
|
|
2092
|
+
if (typeof bindingName !== "string") {
|
|
2093
|
+
const sorted = sortedGroupMembers(bindingName, node.type);
|
|
2094
|
+
const binding = `{ ${sorted.map((member) => renderBindingMember(member, transformName)).join(", ")} }`;
|
|
2095
|
+
const allOptional = sorted.every((member) => member.optional);
|
|
2096
|
+
const type = renderGroupType(node.type, sorted, transformType);
|
|
2097
|
+
if (type) {
|
|
2098
|
+
if (allOptional) return `${binding}: ${type} = ${node.default ?? "{}"}`;
|
|
2099
|
+
return node.default ? `${binding}: ${type} = ${node.default}` : `${binding}: ${type}`;
|
|
2100
|
+
}
|
|
2101
|
+
return node.default ? `${binding} = ${node.default}` : binding;
|
|
2102
|
+
}
|
|
2103
|
+
const name = transformName ? transformName(bindingName) : bindingName;
|
|
2104
|
+
const type = node.type ? renderType(node.type, transformType) : void 0;
|
|
2105
|
+
if (node.rest) return type ? `...${name}: ${type}` : `...${name}`;
|
|
2106
|
+
if (type) {
|
|
2107
|
+
if (node.optional) return `${name}?: ${type}`;
|
|
2108
|
+
return node.default ? `${name}: ${type} = ${node.default}` : `${name}: ${type}`;
|
|
2109
|
+
}
|
|
2110
|
+
return node.default ? `${name} = ${node.default}` : name;
|
|
2111
|
+
}
|
|
2112
|
+
/**
|
|
2113
|
+
* Default function-signature printer. Renders a parameter list in one of two modes:
|
|
2114
|
+
* `declaration` for the function signature and `call` for the call arguments.
|
|
2115
|
+
*
|
|
2116
|
+
* @example
|
|
2117
|
+
* ```ts
|
|
2118
|
+
* const printer = functionPrinter({ mode: 'declaration' })
|
|
2119
|
+
*
|
|
2120
|
+
* const sig = createFunctionParameters({
|
|
2121
|
+
* params: [
|
|
2122
|
+
* createFunctionParameter({ name: 'petId', type: 'string', optional: false }),
|
|
2123
|
+
* createFunctionParameter({ name: 'config', type: 'Config', optional: false, default: '{}' }),
|
|
2124
|
+
* ],
|
|
2125
|
+
* })
|
|
2126
|
+
*
|
|
2127
|
+
* printer.print(sig) // → "petId: string, config: Config = {}"
|
|
2128
|
+
* ```
|
|
2129
|
+
*/
|
|
2130
|
+
function functionPrinter(options) {
|
|
2131
|
+
return {
|
|
2132
|
+
name: "functionParameters",
|
|
2133
|
+
options,
|
|
2134
|
+
print(node) {
|
|
2135
|
+
return sortParams(node.params).map((p) => printParameter(p, options)).filter(Boolean).join(", ");
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
//#endregion
|
|
2140
|
+
//#region src/printers/operationParams.ts
|
|
2141
|
+
/**
|
|
2142
|
+
* Resolves the {@link TypeExpression} for an individual parameter.
|
|
2143
|
+
*
|
|
2144
|
+
* Without a resolver, it falls back to the schema primitive (a plain type-name string). When the
|
|
2145
|
+
* parameter belongs to a named group, it emits an `IndexedAccessType` like `GroupParams['petId']`,
|
|
2146
|
+
* otherwise the resolved individual name.
|
|
2147
|
+
*/
|
|
2148
|
+
function resolveParamType({ node, param, resolver }) {
|
|
2149
|
+
if (!resolver) return param.schema.primitive ?? "unknown";
|
|
2150
|
+
const individualName = resolver.param.name(node, param);
|
|
2151
|
+
const groupLocation = param.in === "path" || param.in === "query" || param.in === "header" ? param.in : void 0;
|
|
2152
|
+
const groupResolvers = {
|
|
2153
|
+
path: resolver.param.path,
|
|
2154
|
+
query: resolver.param.query,
|
|
2155
|
+
header: resolver.param.headers
|
|
2156
|
+
};
|
|
2157
|
+
const groupName = groupLocation ? groupResolvers[groupLocation].call(resolver, node, param) : void 0;
|
|
2158
|
+
if (groupName && groupName !== individualName) return createIndexedAccessType({
|
|
2159
|
+
target: groupName,
|
|
2160
|
+
key: param.name
|
|
2161
|
+
});
|
|
2162
|
+
return individualName;
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Derives a {@link ParamGroupType} for a query or header group from the resolver.
|
|
2166
|
+
*
|
|
2167
|
+
* Returns `null` when there is no resolver, no params, or the group name equals the
|
|
2168
|
+
* individual param name (so there is no real group to emit).
|
|
2169
|
+
*/
|
|
2170
|
+
function resolveGroupType({ node, params, group, resolver }) {
|
|
2171
|
+
if (!resolver || !params.length) return null;
|
|
2172
|
+
const firstParam = params[0];
|
|
2173
|
+
const groupName = (group === "query" ? resolver.param.query : resolver.param.headers).call(resolver, node, firstParam);
|
|
2174
|
+
if (groupName === resolver.param.name(node, firstParam)) return null;
|
|
2175
|
+
return {
|
|
2176
|
+
type: groupName,
|
|
2177
|
+
optional: params.every((p) => !p.required)
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Converts an `OperationNode` into function parameters for code generation.
|
|
2182
|
+
*
|
|
2183
|
+
* Centralizes parameter grouping logic for all plugins. `paramsType` chooses between one
|
|
2184
|
+
* destructured object parameter (`object`) and separate top-level parameters (`inline`), while
|
|
2185
|
+
* `pathParamsType` controls how path params render in inline mode. Provide a `resolver` for type
|
|
2186
|
+
* name resolution and `extraParams` for plugin-specific trailing parameters such as an `options` object.
|
|
2187
|
+
*/
|
|
2188
|
+
function createOperationParams(node, options) {
|
|
2189
|
+
const { paramsType, pathParamsType, resolver, pathParamsDefault, extraParams = [], paramNames, typeWrapper } = options;
|
|
2190
|
+
const dataName = paramNames?.data ?? "data";
|
|
2191
|
+
const paramsName = paramNames?.params ?? "params";
|
|
2192
|
+
const headersName = paramNames?.headers ?? "headers";
|
|
2193
|
+
const pathName = paramNames?.path ?? "pathParams";
|
|
2194
|
+
const wrapType = (type) => typeWrapper ? typeWrapper(type) : type;
|
|
2195
|
+
const wrapTypeExpression = (type) => typeof type === "string" ? wrapType(type) : type;
|
|
2196
|
+
const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
|
|
2197
|
+
const toProperty = (param) => ({
|
|
2198
|
+
name: param.name,
|
|
2199
|
+
type: wrapTypeExpression(resolveParamType({
|
|
2200
|
+
node,
|
|
2201
|
+
param,
|
|
2202
|
+
resolver
|
|
2203
|
+
})),
|
|
2204
|
+
optional: !param.required
|
|
2205
|
+
});
|
|
2206
|
+
const emptyObjectDefault = (props) => props.every((p) => p.optional) ? "{}" : void 0;
|
|
2207
|
+
const bodyType = node.requestBody?.content?.[0]?.schema ? wrapType(resolver?.response.body(node) ?? "unknown") : void 0;
|
|
2208
|
+
const bodyProperty = bodyType ? [{
|
|
2209
|
+
name: dataName,
|
|
2210
|
+
type: bodyType,
|
|
2211
|
+
optional: !(node.requestBody?.required ?? false)
|
|
2212
|
+
}] : [];
|
|
2213
|
+
const trailingGroups = [{
|
|
2214
|
+
name: paramsName,
|
|
2215
|
+
node,
|
|
2216
|
+
params: queryParams,
|
|
2217
|
+
groupType: resolveGroupType({
|
|
2218
|
+
node,
|
|
2219
|
+
params: queryParams,
|
|
2220
|
+
group: "query",
|
|
2221
|
+
resolver
|
|
2222
|
+
}),
|
|
2223
|
+
resolver,
|
|
2224
|
+
wrapType
|
|
2225
|
+
}, {
|
|
2226
|
+
name: headersName,
|
|
2227
|
+
node,
|
|
2228
|
+
params: headerParams,
|
|
2229
|
+
groupType: resolveGroupType({
|
|
2230
|
+
node,
|
|
2231
|
+
params: headerParams,
|
|
2232
|
+
group: "header",
|
|
2233
|
+
resolver
|
|
2234
|
+
}),
|
|
2235
|
+
resolver,
|
|
2236
|
+
wrapType
|
|
2237
|
+
}];
|
|
2238
|
+
const params = [];
|
|
2239
|
+
if (paramsType === "object") {
|
|
2240
|
+
const children = [
|
|
2241
|
+
...pathParams.map(toProperty),
|
|
2242
|
+
...bodyProperty,
|
|
2243
|
+
...trailingGroups.flatMap(buildGroupProperty)
|
|
2244
|
+
];
|
|
2245
|
+
if (children.length) params.push(createFunctionParameter({
|
|
2246
|
+
properties: children,
|
|
2247
|
+
default: emptyObjectDefault(children)
|
|
2248
|
+
}));
|
|
2249
|
+
} else {
|
|
2250
|
+
if (pathParamsType === "inlineSpread" && pathParams.length) {
|
|
2251
|
+
const spreadType = resolver?.param.path(node, pathParams[0]);
|
|
2252
|
+
params.push(createFunctionParameter({
|
|
2253
|
+
name: pathName,
|
|
2254
|
+
type: spreadType ? wrapType(spreadType) : void 0,
|
|
2255
|
+
rest: true
|
|
2256
|
+
}));
|
|
2257
|
+
} else if (pathParamsType === "inline") params.push(...pathParams.map((p) => createFunctionParameter(toProperty(p))));
|
|
2258
|
+
else if (pathParams.length) {
|
|
2259
|
+
const pathChildren = pathParams.map(toProperty);
|
|
2260
|
+
params.push(createFunctionParameter({
|
|
2261
|
+
properties: pathChildren,
|
|
2262
|
+
default: pathParamsDefault ?? emptyObjectDefault(pathChildren)
|
|
2263
|
+
}));
|
|
2264
|
+
}
|
|
2265
|
+
params.push(...bodyProperty.map((p) => createFunctionParameter(p)));
|
|
2266
|
+
params.push(...trailingGroups.flatMap(buildGroupParam));
|
|
2267
|
+
}
|
|
2268
|
+
params.push(...extraParams);
|
|
2269
|
+
return createFunctionParameters({ params });
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2272
|
+
* Builds the property descriptor for a query or header group.
|
|
2273
|
+
* Returns an empty array when there are no params to emit.
|
|
2274
|
+
*
|
|
2275
|
+
* A pre-resolved `groupType` emits `name: GroupType`. Otherwise it builds an inline
|
|
2276
|
+
* {@link TypeLiteralNode} from the individual params.
|
|
2277
|
+
*/
|
|
2278
|
+
function buildGroupProperty({ name, node, params, groupType, resolver, wrapType }) {
|
|
2279
|
+
if (groupType) return [{
|
|
2280
|
+
name,
|
|
2281
|
+
type: typeof groupType.type === "string" ? wrapType(groupType.type) : groupType.type,
|
|
2282
|
+
optional: groupType.optional
|
|
2283
|
+
}];
|
|
2284
|
+
if (params.length) return [{
|
|
2285
|
+
name,
|
|
2286
|
+
type: buildTypeLiteral({
|
|
2287
|
+
node,
|
|
2288
|
+
params,
|
|
2289
|
+
resolver
|
|
2290
|
+
}),
|
|
2291
|
+
optional: params.every((p) => !p.required)
|
|
2292
|
+
}];
|
|
2293
|
+
return [];
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* Builds a single {@link FunctionParameterNode} for a query or header group.
|
|
2297
|
+
* Returns an empty array when there are no params to emit.
|
|
2298
|
+
*/
|
|
2299
|
+
function buildGroupParam(args) {
|
|
2300
|
+
return buildGroupProperty(args).map((p) => createFunctionParameter(p));
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Builds a {@link TypeLiteralNode} for an inline anonymous type grouping named fields.
|
|
2304
|
+
*
|
|
2305
|
+
* Used when query or header parameters have no dedicated group type name.
|
|
2306
|
+
*/
|
|
2307
|
+
function buildTypeLiteral({ node, params, resolver }) {
|
|
2308
|
+
return createTypeLiteral({ members: params.map((p) => ({
|
|
2309
|
+
name: p.name,
|
|
2310
|
+
type: resolveParamType({
|
|
2311
|
+
node,
|
|
2312
|
+
param: p,
|
|
2313
|
+
resolver
|
|
2314
|
+
}),
|
|
2315
|
+
optional: !p.required
|
|
2316
|
+
})) });
|
|
2317
|
+
}
|
|
2318
|
+
//#endregion
|
|
2319
|
+
export { Enum, Type, buildParams, createFunctionParameter, createFunctionParameters, createIndexedAccessType, createObjectBindingPattern, createOperationParams, createTypeLiteral, pluginTs as default, pluginTs, functionPrinter, pluginTsName, printerTs, renderType, resolverTs, typeGenerator };
|
|
2320
|
+
|
|
2321
|
+
//# sourceMappingURL=index.js.map
|