@kubb/plugin-ts 5.0.0-beta.10 → 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/src/factory.ts DELETED
@@ -1,982 +0,0 @@
1
- import { camelCase, pascalCase, screamingSnakeCase, snakeCase } from '@internals/utils'
2
- import { ast } from '@kubb/core'
3
- import { isNumber, sortBy } from 'remeda'
4
- import ts from 'typescript'
5
- import { OPTIONAL_ADDS_UNDEFINED } from './constants.ts'
6
-
7
- const { SyntaxKind, factory } = ts
8
-
9
- // https://ts-ast-viewer.com/
10
-
11
- /**
12
- * TypeScript AST modifiers for common keywords (async, export, const, static).
13
- */
14
- export const modifiers = {
15
- async: factory.createModifier(ts.SyntaxKind.AsyncKeyword),
16
- export: factory.createModifier(ts.SyntaxKind.ExportKeyword),
17
- const: factory.createModifier(ts.SyntaxKind.ConstKeyword),
18
- static: factory.createModifier(ts.SyntaxKind.StaticKeyword),
19
- } as const
20
-
21
- /**
22
- * TypeScript syntax kind constants for union, literal, and string types.
23
- */
24
- export const syntaxKind = {
25
- union: SyntaxKind.UnionType as 192,
26
- literalType: SyntaxKind.LiteralType,
27
- stringLiteral: SyntaxKind.StringLiteral,
28
- } as const
29
-
30
- function isNonNullable<T>(value: T | null | undefined): value is T {
31
- return value !== null && value !== undefined
32
- }
33
-
34
- function isValidIdentifier(str: string): boolean {
35
- if (!str.length || str.trim() !== str) {
36
- return false
37
- }
38
- const node = ts.parseIsolatedEntityName(str, ts.ScriptTarget.Latest)
39
-
40
- return !!node && node.kind === ts.SyntaxKind.Identifier && ts.identifierToKeywordKind(node.kind as unknown as ts.Identifier) === undefined
41
- }
42
-
43
- function propertyName(name: string | ts.PropertyName): ts.PropertyName {
44
- if (typeof name === 'string') {
45
- const isValid = isValidIdentifier(name)
46
- return isValid ? factory.createIdentifier(name) : factory.createStringLiteral(name)
47
- }
48
- return name
49
- }
50
-
51
- const questionToken = factory.createToken(ts.SyntaxKind.QuestionToken)
52
-
53
- /**
54
- * Creates a question token for optional type annotations.
55
- * Pass `true` to use the cached token, or provide a pre-created token.
56
- */
57
- export function createQuestionToken(token?: boolean | ts.QuestionToken) {
58
- if (!token) {
59
- return undefined
60
- }
61
- if (token === true) {
62
- return questionToken
63
- }
64
- return token
65
- }
66
-
67
- /**
68
- * Creates a TypeScript intersection type node from multiple type nodes.
69
- * Returns the single node if only one is provided, or wraps in parentheses if requested.
70
- */
71
- export function createIntersectionDeclaration({ nodes, withParentheses }: { nodes: Array<ts.TypeNode>; withParentheses?: boolean }): ts.TypeNode | null {
72
- if (!nodes.length) {
73
- return null
74
- }
75
-
76
- if (nodes.length === 1) {
77
- return nodes[0] || null
78
- }
79
-
80
- const node = factory.createIntersectionTypeNode(nodes)
81
-
82
- if (withParentheses) {
83
- return factory.createParenthesizedType(node)
84
- }
85
-
86
- return node
87
- }
88
-
89
- /**
90
- * Creates a TypeScript array type node.
91
- * Use `arrayType: 'array'` for bracket syntax (`T[]`), or `'generic'` for `Array<T>`.
92
- *
93
- * @example Array bracket syntax
94
- * `createArrayDeclaration({ nodes: [stringType], arrayType: 'array' }) // → string[]`
95
- *
96
- * @example Generic Array syntax
97
- * `createArrayDeclaration({ nodes: [stringType], arrayType: 'generic' }) // → Array<string>`
98
- */
99
- export function createArrayDeclaration({ nodes, arrayType = 'array' }: { nodes: Array<ts.TypeNode>; arrayType?: 'array' | 'generic' }): ts.TypeNode | null {
100
- if (!nodes.length) {
101
- return factory.createTupleTypeNode([])
102
- }
103
-
104
- if (nodes.length === 1) {
105
- const node = nodes[0]
106
- if (!node) {
107
- return null
108
- }
109
- if (arrayType === 'generic') {
110
- return factory.createTypeReferenceNode(factory.createIdentifier('Array'), [node])
111
- }
112
- return factory.createArrayTypeNode(node)
113
- }
114
-
115
- // For union types (multiple nodes), respect arrayType preference
116
- const unionType = factory.createUnionTypeNode(nodes)
117
- if (arrayType === 'generic') {
118
- return factory.createTypeReferenceNode(factory.createIdentifier('Array'), [unionType])
119
- }
120
- // For array syntax with unions, we need parentheses: (string | number)[]
121
- return factory.createArrayTypeNode(factory.createParenthesizedType(unionType))
122
- }
123
-
124
- /**
125
- * Minimum nodes length of 2
126
- * @example Union type example
127
- * `string | number`
128
- */
129
- export function createUnionDeclaration({ nodes, withParentheses }: { nodes: Array<ts.TypeNode>; withParentheses?: boolean }): ts.TypeNode {
130
- if (!nodes.length) {
131
- return keywordTypeNodes.any
132
- }
133
-
134
- if (nodes.length === 1) {
135
- return nodes[0] as ts.TypeNode
136
- }
137
-
138
- const node = factory.createUnionTypeNode(nodes)
139
-
140
- if (withParentheses) {
141
- return factory.createParenthesizedType(node)
142
- }
143
-
144
- return node
145
- }
146
-
147
- /**
148
- * Creates a TypeScript property signature for object/interface members.
149
- * Supports optional markers, readonly modifiers, and type annotations.
150
- */
151
- export function createPropertySignature({
152
- readOnly,
153
- modifiers = [],
154
- name,
155
- questionToken,
156
- type,
157
- }: {
158
- readOnly?: boolean
159
- modifiers?: Array<ts.Modifier>
160
- name: ts.PropertyName | string
161
- questionToken?: ts.QuestionToken | boolean
162
- type?: ts.TypeNode
163
- }) {
164
- return factory.createPropertySignature(
165
- [...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : undefined].filter(
166
- (modifier): modifier is ts.Modifier => modifier !== undefined,
167
- ),
168
- propertyName(name),
169
- createQuestionToken(questionToken),
170
- type,
171
- )
172
- }
173
-
174
- /**
175
- * Creates a function parameter declaration with optional markers, rest parameters, and type annotations.
176
- */
177
- export function createParameterSignature(
178
- name: string | ts.BindingName,
179
- {
180
- modifiers,
181
- dotDotDotToken,
182
- questionToken,
183
- type,
184
- initializer,
185
- }: {
186
- decorators?: Array<ts.Decorator>
187
- modifiers?: Array<ts.Modifier>
188
- dotDotDotToken?: ts.DotDotDotToken
189
- questionToken?: ts.QuestionToken | boolean
190
- type?: ts.TypeNode
191
- initializer?: ts.Expression
192
- },
193
- ): ts.ParameterDeclaration {
194
- return factory.createParameterDeclaration(modifiers, dotDotDotToken, name, createQuestionToken(questionToken), type, initializer)
195
- }
196
-
197
- /**
198
- * Creates a JSDoc comment node from an array of comment strings.
199
- * Returns null if no comments are provided.
200
- */
201
- export function createJSDoc({ comments }: { comments: string[] }) {
202
- if (!comments.length) {
203
- return null
204
- }
205
- return factory.createJSDocComment(
206
- factory.createNodeArray(
207
- comments.map((comment, i) => {
208
- if (i === comments.length - 1) {
209
- return factory.createJSDocText(comment)
210
- }
211
-
212
- return factory.createJSDocText(`${comment}\n`)
213
- }),
214
- ),
215
- )
216
- }
217
-
218
- /**
219
- * Attaches JSDoc comments to an AST node as synthetic leading comments.
220
- * Filters out undefined comments before attaching.
221
- *
222
- * @see https://github.com/microsoft/TypeScript/issues/44151
223
- */
224
- export function appendJSDocToNode<TNode extends ts.Node>({ node, comments }: { node: TNode; comments: Array<string | undefined> }) {
225
- const filteredComments = comments.filter(Boolean)
226
-
227
- if (!filteredComments.length) {
228
- return node
229
- }
230
-
231
- const text = filteredComments.reduce((acc = '', comment = '') => {
232
- return `${acc}\n * ${comment.replaceAll('*/', '*\\/')}`
233
- }, '*')
234
-
235
- // Use the node directly instead of spreading to avoid creating Unknown nodes
236
- // TypeScript's addSyntheticLeadingComment accepts the node as-is
237
- return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, `${text || '*'}\n`, true)
238
- }
239
-
240
- /**
241
- * Creates a TypeScript index signature for dynamic property access.
242
- * Defines the key type (default: `string`) and value type on an object.
243
- */
244
- export function createIndexSignature(
245
- type: ts.TypeNode,
246
- {
247
- modifiers,
248
- indexName = 'key',
249
- indexType = factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
250
- }: {
251
- indexName?: string
252
- indexType?: ts.TypeNode
253
- decorators?: Array<ts.Decorator>
254
- modifiers?: Array<ts.Modifier>
255
- } = {},
256
- ) {
257
- return factory.createIndexSignature(modifiers, [createParameterSignature(indexName, { type: indexType })], type)
258
- }
259
-
260
- /**
261
- * Creates a TypeScript type alias declaration with optional modifiers and type parameters.
262
- */
263
- export function createTypeAliasDeclaration({
264
- modifiers,
265
- name,
266
- typeParameters,
267
- type,
268
- }: {
269
- modifiers?: Array<ts.Modifier>
270
- name: string | ts.Identifier
271
- typeParameters?: Array<ts.TypeParameterDeclaration>
272
- type: ts.TypeNode
273
- }) {
274
- return factory.createTypeAliasDeclaration(modifiers, name, typeParameters, type)
275
- }
276
-
277
- /**
278
- * Creates a TypeScript interface declaration with optional modifiers, type parameters, and members.
279
- */
280
- export function createInterfaceDeclaration({
281
- modifiers,
282
- name,
283
- typeParameters,
284
- members,
285
- }: {
286
- modifiers?: Array<ts.Modifier>
287
- name: string | ts.Identifier
288
- typeParameters?: Array<ts.TypeParameterDeclaration>
289
- members: Array<ts.TypeElement>
290
- }) {
291
- return factory.createInterfaceDeclaration(modifiers, name, typeParameters, undefined, members)
292
- }
293
-
294
- /**
295
- * Creates a TypeScript type declaration as either a type alias or interface.
296
- * Intelligently selects the syntax based on the type structure and attaches JSDoc comments.
297
- */
298
- export function createTypeDeclaration({
299
- syntax,
300
- isExportable,
301
- comments,
302
- name,
303
- type,
304
- }: {
305
- syntax: 'type' | 'interface'
306
- comments: Array<string | undefined>
307
- isExportable?: boolean
308
- name: string | ts.Identifier
309
- type: ts.TypeNode
310
- }) {
311
- if (syntax === 'interface' && 'members' in type) {
312
- const node = createInterfaceDeclaration({
313
- members: [...(type as ts.TypeLiteralNode).members],
314
- modifiers: isExportable ? [modifiers.export] : [],
315
- name,
316
- typeParameters: undefined,
317
- })
318
-
319
- return appendJSDocToNode({
320
- node,
321
- comments,
322
- })
323
- }
324
-
325
- const node = createTypeAliasDeclaration({
326
- type,
327
- modifiers: isExportable ? [modifiers.export] : [],
328
- name,
329
- typeParameters: undefined,
330
- })
331
-
332
- return appendJSDocToNode({
333
- node,
334
- comments,
335
- })
336
- }
337
-
338
- /**
339
- * Creates a TypeScript namespace declaration (exported module).
340
- */
341
- export function createNamespaceDeclaration({ statements, name }: { name: string; statements: ts.Statement[] }) {
342
- return factory.createModuleDeclaration(
343
- [factory.createToken(ts.SyntaxKind.ExportKeyword)],
344
- factory.createIdentifier(name),
345
- factory.createModuleBlock(statements),
346
- ts.NodeFlags.Namespace,
347
- )
348
- }
349
-
350
- /**
351
- * Creates an import declaration with support for default imports, named imports, namespace imports, and type-only imports.
352
- * Optionally rename imported members with `propertyName` and `name` pairs.
353
- *
354
- * @example Default import
355
- * `import Pet from './Pet'`
356
- *
357
- * @example Named imports with rename
358
- * `import { Pet as Cat } from './Pet'`
359
- *
360
- * @example Namespace import
361
- * `import * as Pet from './Pet'`
362
- */
363
- export function createImportDeclaration({
364
- name,
365
- path,
366
- isTypeOnly = false,
367
- isNameSpace = false,
368
- }: {
369
- name: string | Array<string | { propertyName: string; name?: string }>
370
- path: string
371
- isTypeOnly?: boolean
372
- isNameSpace?: boolean
373
- }) {
374
- if (!Array.isArray(name)) {
375
- let importPropertyName: ts.Identifier | undefined = factory.createIdentifier(name)
376
- let importName: ts.NamedImportBindings | undefined
377
-
378
- if (isNameSpace) {
379
- importPropertyName = undefined
380
- importName = factory.createNamespaceImport(factory.createIdentifier(name))
381
- }
382
-
383
- return factory.createImportDeclaration(
384
- undefined,
385
- factory.createImportClause(isTypeOnly, importPropertyName, importName),
386
- factory.createStringLiteral(path),
387
- undefined,
388
- )
389
- }
390
-
391
- // Sort the imports alphabetically for consistent output across platforms
392
- const sortedName = sortBy(name, [(item) => (typeof item === 'object' ? item.propertyName : item), 'asc'])
393
-
394
- return factory.createImportDeclaration(
395
- undefined,
396
- factory.createImportClause(
397
- isTypeOnly,
398
- undefined,
399
- factory.createNamedImports(
400
- sortedName.map((item) => {
401
- if (typeof item === 'object') {
402
- const obj = item as { propertyName: string; name?: string }
403
- if (obj.name) {
404
- return factory.createImportSpecifier(false, factory.createIdentifier(obj.propertyName), factory.createIdentifier(obj.name))
405
- }
406
-
407
- return factory.createImportSpecifier(false, undefined, factory.createIdentifier(obj.propertyName))
408
- }
409
-
410
- return factory.createImportSpecifier(false, undefined, factory.createIdentifier(item))
411
- }),
412
- ),
413
- ),
414
- factory.createStringLiteral(path),
415
- undefined,
416
- )
417
- }
418
-
419
- /**
420
- * Creates an export declaration with support for named exports, namespace exports, and type-only exports.
421
- * Sorts export names alphabetically for consistent output across platforms.
422
- */
423
- export function createExportDeclaration({
424
- path,
425
- asAlias,
426
- isTypeOnly = false,
427
- name,
428
- }: {
429
- path: string
430
- asAlias?: boolean
431
- isTypeOnly?: boolean
432
- name?: string | Array<ts.Identifier | string>
433
- }) {
434
- if (name && !Array.isArray(name) && !asAlias) {
435
- console.warn(`When using name as string, asAlias should be true ${name}`)
436
- }
437
-
438
- if (!Array.isArray(name)) {
439
- const parsedName = name?.match(/^\d/) ? `_${name?.slice(1)}` : name
440
-
441
- return factory.createExportDeclaration(
442
- undefined,
443
- isTypeOnly,
444
- asAlias && parsedName ? factory.createNamespaceExport(factory.createIdentifier(parsedName)) : undefined,
445
- factory.createStringLiteral(path),
446
- undefined,
447
- )
448
- }
449
-
450
- // Sort the exports alphabetically for consistent output across platforms
451
- const sortedName = sortBy(name, [(propertyName) => (typeof propertyName === 'string' ? propertyName : propertyName.text), 'asc'])
452
-
453
- return factory.createExportDeclaration(
454
- undefined,
455
- isTypeOnly,
456
- factory.createNamedExports(
457
- sortedName.map((propertyName) => {
458
- return factory.createExportSpecifier(false, undefined, typeof propertyName === 'string' ? factory.createIdentifier(propertyName) : propertyName)
459
- }),
460
- ),
461
- factory.createStringLiteral(path),
462
- undefined,
463
- )
464
- }
465
-
466
- /**
467
- * Apply casing transformation to enum keys
468
- */
469
- function applyEnumKeyCasing(key: string, casing: 'screamingSnakeCase' | 'snakeCase' | 'pascalCase' | 'camelCase' | 'none' = 'none'): string {
470
- if (casing === 'none') {
471
- return key
472
- }
473
- if (casing === 'screamingSnakeCase') {
474
- return screamingSnakeCase(key)
475
- }
476
- if (casing === 'snakeCase') {
477
- return snakeCase(key)
478
- }
479
- if (casing === 'pascalCase') {
480
- return pascalCase(key)
481
- }
482
- if (casing === 'camelCase') {
483
- return camelCase(key)
484
- }
485
- return key
486
- }
487
-
488
- /**
489
- * Creates a TypeScript enum declaration or equivalent construct in various formats.
490
- * Returns a tuple of [name node, type node] - name node may be undefined for certain types.
491
- *
492
- * @example
493
- * ```ts
494
- * const [name, type] = createEnumDeclaration({
495
- * type: 'enum',
496
- * name: 'petType',
497
- * typeName: 'PetType',
498
- * enums: [['cat', 'cat'], ['dog', 'dog']],
499
- * })
500
- * ```
501
- */
502
- export function createEnumDeclaration({
503
- type = 'enum',
504
- name,
505
- typeName,
506
- enums,
507
- enumKeyCasing = 'none',
508
- }: {
509
- /**
510
- * Choose to use `enum`, `asConst`, `asPascalConst`, `constEnum`, or `literal` for enums.
511
- * - `enum`: TypeScript enum
512
- * - `asConst`: const with camelCase name (e.g., `petType`)
513
- * - `asPascalConst`: const with PascalCase name (e.g., `PetType`)
514
- * - `constEnum`: const enum
515
- * - `literal`: literal union type
516
- * @default `'enum'`
517
- */
518
- type?: 'enum' | 'asConst' | 'asPascalConst' | 'constEnum' | 'literal' | 'inlineLiteral'
519
- /**
520
- * Enum name in camelCase.
521
- */
522
- name: string
523
- /**
524
- * Enum name in PascalCase.
525
- */
526
- typeName: string
527
- enums: [key: string | number, value: string | number | boolean][]
528
- /**
529
- * Choose the casing for enum key names.
530
- * @default 'none'
531
- */
532
- enumKeyCasing?: 'screamingSnakeCase' | 'snakeCase' | 'pascalCase' | 'camelCase' | 'none'
533
- }): [name: ts.Node | undefined, type: ts.Node] {
534
- if (type === 'literal' || type === 'inlineLiteral') {
535
- return [
536
- undefined,
537
- factory.createTypeAliasDeclaration(
538
- [factory.createToken(ts.SyntaxKind.ExportKeyword)],
539
- factory.createIdentifier(typeName),
540
- undefined,
541
- factory.createUnionTypeNode(
542
- enums
543
- .map(([_key, value]) => {
544
- if (isNumber(value)) {
545
- if (value < 0) {
546
- return factory.createLiteralTypeNode(
547
- factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value))),
548
- )
549
- }
550
- return factory.createLiteralTypeNode(factory.createNumericLiteral(value?.toString()))
551
- }
552
-
553
- if (typeof value === 'boolean') {
554
- return factory.createLiteralTypeNode(value ? factory.createTrue() : factory.createFalse())
555
- }
556
- if (value) {
557
- return factory.createLiteralTypeNode(factory.createStringLiteral(value.toString()))
558
- }
559
-
560
- return undefined
561
- })
562
- .filter((node): node is ts.LiteralTypeNode => node !== undefined),
563
- ),
564
- ),
565
- ]
566
- }
567
-
568
- if (type === 'enum' || type === 'constEnum') {
569
- return [
570
- undefined,
571
- factory.createEnumDeclaration(
572
- [factory.createToken(ts.SyntaxKind.ExportKeyword), type === 'constEnum' ? factory.createToken(ts.SyntaxKind.ConstKeyword) : undefined].filter(
573
- (modifier): modifier is ts.ModifierToken<ts.SyntaxKind.ExportKeyword> | ts.ModifierToken<ts.SyntaxKind.ConstKeyword> => modifier !== undefined,
574
- ),
575
- factory.createIdentifier(typeName),
576
- enums
577
- .map(([key, value]) => {
578
- let initializer: ts.Expression = factory.createStringLiteral(value?.toString())
579
- const isExactNumber = Number.parseInt(value.toString(), 10) === value
580
-
581
- if (isExactNumber && isNumber(Number.parseInt(value.toString(), 10))) {
582
- if ((value as number) < 0) {
583
- initializer = factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value as number)))
584
- } else {
585
- initializer = factory.createNumericLiteral(value as number)
586
- }
587
- }
588
-
589
- if (typeof value === 'boolean') {
590
- initializer = value ? factory.createTrue() : factory.createFalse()
591
- }
592
-
593
- if (isNumber(Number.parseInt(key.toString(), 10))) {
594
- const casingKey = applyEnumKeyCasing(`${typeName}_${key}`, enumKeyCasing)
595
- return factory.createEnumMember(propertyName(casingKey), initializer)
596
- }
597
-
598
- if (key) {
599
- const casingKey = applyEnumKeyCasing(key.toString(), enumKeyCasing)
600
- return factory.createEnumMember(propertyName(casingKey), initializer)
601
- }
602
-
603
- return undefined
604
- })
605
- .filter((member): member is ts.EnumMember => member !== undefined),
606
- ),
607
- ]
608
- }
609
-
610
- // used when using `as const` instead of an TypeScript enum.
611
- // name is already PascalCase for asPascalConst and camelCase for asConst (set in Type.tsx)
612
- // typeName has the Key suffix for type alias, so we use name for the const identifier
613
- const identifierName = name
614
-
615
- // When there are no enum items (empty or all-null enum), don't generate a runtime const.
616
- // Return undefined for nameNode so the barrel won't try to export a non-existent symbol.
617
- // Use `never` as the type alias to keep references valid without creating a broken const.
618
- if (enums.length === 0) {
619
- return [
620
- undefined,
621
- factory.createTypeAliasDeclaration(
622
- [factory.createToken(ts.SyntaxKind.ExportKeyword)],
623
- factory.createIdentifier(typeName),
624
- undefined,
625
- factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword),
626
- ),
627
- ]
628
- }
629
-
630
- return [
631
- factory.createVariableStatement(
632
- [factory.createToken(ts.SyntaxKind.ExportKeyword)],
633
- factory.createVariableDeclarationList(
634
- [
635
- factory.createVariableDeclaration(
636
- factory.createIdentifier(identifierName),
637
- undefined,
638
- undefined,
639
- factory.createAsExpression(
640
- factory.createObjectLiteralExpression(
641
- enums
642
- .map(([key, value]) => {
643
- let initializer: ts.Expression = factory.createStringLiteral(value?.toString())
644
-
645
- if (isNumber(value)) {
646
- // Error: Negative numbers should be created in combination with createPrefixUnaryExpression factory.
647
- // The method createNumericLiteral only accepts positive numbers
648
- // or those combined with createPrefixUnaryExpression.
649
- // Therefore, we need to ensure that the number is not negative.
650
- if (value < 0) {
651
- initializer = factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value)))
652
- } else {
653
- initializer = factory.createNumericLiteral(value)
654
- }
655
- }
656
-
657
- if (typeof value === 'boolean') {
658
- initializer = value ? factory.createTrue() : factory.createFalse()
659
- }
660
-
661
- if (key) {
662
- const casingKey = applyEnumKeyCasing(key.toString(), enumKeyCasing)
663
- return factory.createPropertyAssignment(propertyName(casingKey), initializer)
664
- }
665
-
666
- return undefined
667
- })
668
- .filter((property): property is ts.PropertyAssignment => property !== undefined),
669
- true,
670
- ),
671
- factory.createTypeReferenceNode(factory.createIdentifier('const'), undefined),
672
- ),
673
- ),
674
- ],
675
- ts.NodeFlags.Const,
676
- ),
677
- ),
678
- factory.createTypeAliasDeclaration(
679
- [factory.createToken(ts.SyntaxKind.ExportKeyword)],
680
- factory.createIdentifier(typeName),
681
- undefined,
682
- factory.createIndexedAccessTypeNode(
683
- factory.createParenthesizedType(factory.createTypeQueryNode(factory.createIdentifier(identifierName), undefined)),
684
- factory.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, factory.createTypeQueryNode(factory.createIdentifier(identifierName), undefined)),
685
- ),
686
- ),
687
- ]
688
- }
689
-
690
- /**
691
- * Creates a TypeScript `Omit<T, Keys>` type reference node.
692
- * Optionally wraps the type in `NonNullable<T>` if `nonNullable` is true.
693
- */
694
- export function createOmitDeclaration({ keys, type, nonNullable }: { keys: Array<string> | string; type: ts.TypeNode; nonNullable?: boolean }) {
695
- const node = nonNullable ? factory.createTypeReferenceNode(factory.createIdentifier('NonNullable'), [type]) : type
696
-
697
- if (Array.isArray(keys)) {
698
- return factory.createTypeReferenceNode(factory.createIdentifier('Omit'), [
699
- node,
700
- factory.createUnionTypeNode(
701
- keys.map((key) => {
702
- return factory.createLiteralTypeNode(factory.createStringLiteral(key))
703
- }),
704
- ),
705
- ])
706
- }
707
-
708
- return factory.createTypeReferenceNode(factory.createIdentifier('Omit'), [node, factory.createLiteralTypeNode(factory.createStringLiteral(keys))])
709
- }
710
-
711
- /**
712
- * Pre-built TypeScript keyword type nodes for common primitive types.
713
- * Use these to avoid repeatedly creating the same type nodes.
714
- */
715
- export const keywordTypeNodes = {
716
- any: factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
717
- unknown: factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
718
- void: factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword),
719
- number: factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
720
- integer: factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
721
- bigint: factory.createKeywordTypeNode(ts.SyntaxKind.BigIntKeyword),
722
- object: factory.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword),
723
- string: factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
724
- boolean: factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword),
725
- undefined: factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
726
- null: factory.createLiteralTypeNode(factory.createToken(ts.SyntaxKind.NullKeyword)),
727
- never: factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword),
728
- } as const
729
-
730
- /**
731
- * Converts a path like '/pet/{petId}/uploadImage' to a template literal type
732
- * like `/pet/${string}/uploadImage`
733
- */
734
- /**
735
- * Converts an OAS-style path (e.g. `/pets/{petId}`) or an Express-style path
736
- * (e.g. `/pets/:petId`) to a TypeScript template literal type
737
- * like `` `/pets/${string}` ``.
738
- */
739
- export function createUrlTemplateType(path: string): ts.TypeNode {
740
- // normalized Express `:param` → OAS `{param}` so a single regex handles both.
741
- const normalized = path.replace(/:([^/]+)/g, '{$1}')
742
-
743
- if (!normalized.includes('{')) {
744
- return factory.createLiteralTypeNode(factory.createStringLiteral(normalized))
745
- }
746
-
747
- const segments = normalized.split(/(\{[^}]+\})/)
748
- const parts: string[] = []
749
- const parameterIndices: number[] = []
750
-
751
- segments.forEach((segment) => {
752
- if (segment.startsWith('{') && segment.endsWith('}')) {
753
- parameterIndices.push(parts.length)
754
- parts.push(segment)
755
- } else if (segment) {
756
- parts.push(segment)
757
- }
758
- })
759
-
760
- const head = ts.factory.createTemplateHead(parts[0] || '')
761
- const templateSpans: ts.TemplateLiteralTypeSpan[] = []
762
-
763
- parameterIndices.forEach((paramIndex, i) => {
764
- const isLast = i === parameterIndices.length - 1
765
- const nextPart = parts[paramIndex + 1] || ''
766
- const literal = isLast ? ts.factory.createTemplateTail(nextPart) : ts.factory.createTemplateMiddle(nextPart)
767
- templateSpans.push(ts.factory.createTemplateLiteralTypeSpan(keywordTypeNodes.string, literal))
768
- })
769
-
770
- return ts.factory.createTemplateLiteralType(head, templateSpans)
771
- }
772
-
773
- /**
774
- * Creates a TypeScript type literal node (anonymous object type).
775
- */
776
- export const createTypeLiteralNode = factory.createTypeLiteralNode
777
-
778
- /**
779
- * Creates a TypeScript type reference node (e.g., `Array<string>`, `Record<K, V>`).
780
- */
781
- export const createTypeReferenceNode = factory.createTypeReferenceNode
782
-
783
- /**
784
- * Creates a numeric literal type node.
785
- */
786
- export const createNumericLiteral = factory.createNumericLiteral
787
-
788
- /**
789
- * Creates a string literal type node.
790
- */
791
- export const createStringLiteral = factory.createStringLiteral
792
-
793
- /**
794
- * Creates an array type node (e.g., `T[]`).
795
- */
796
- export const createArrayTypeNode = factory.createArrayTypeNode
797
-
798
- /**
799
- * Creates a parenthesized type node to control operator precedence.
800
- */
801
- export const createParenthesizedType = factory.createParenthesizedType
802
-
803
- /**
804
- * Creates a literal type node (e.g., `'hello'`, `42`, `true`).
805
- */
806
- export const createLiteralTypeNode = factory.createLiteralTypeNode
807
-
808
- /**
809
- * Creates a null literal type node.
810
- */
811
- export const createNull = factory.createNull
812
-
813
- /**
814
- * Creates an identifier node.
815
- */
816
- export const createIdentifier = factory.createIdentifier
817
-
818
- /**
819
- * Creates an optional type node (e.g., `T | undefined`).
820
- */
821
- export const createOptionalTypeNode = factory.createOptionalTypeNode
822
-
823
- /**
824
- * Creates a tuple type node (e.g., `[string, number]`).
825
- */
826
- export const createTupleTypeNode = factory.createTupleTypeNode
827
-
828
- /**
829
- * Creates a rest type node for variadic tuple elements (e.g., `...T[]`).
830
- */
831
- export const createRestTypeNode = factory.createRestTypeNode
832
-
833
- /**
834
- * Creates a boolean true literal type node.
835
- */
836
- export const createTrue = factory.createTrue
837
-
838
- /**
839
- * Creates a boolean false literal type node.
840
- */
841
- export const createFalse = factory.createFalse
842
-
843
- /**
844
- * Creates an indexed access type node (e.g., `T[K]`).
845
- */
846
- export const createIndexedAccessTypeNode = factory.createIndexedAccessTypeNode
847
-
848
- /**
849
- * Creates a type operator node (e.g., `keyof T`, `readonly T[]`).
850
- */
851
- export const createTypeOperatorNode = factory.createTypeOperatorNode
852
-
853
- /**
854
- * Creates a prefix unary expression (e.g., negative numbers, logical not).
855
- */
856
- export const createPrefixUnaryExpression = factory.createPrefixUnaryExpression
857
-
858
- /**
859
- * Exports TypeScript SyntaxKind enum for AST node type checking.
860
- */
861
- export { SyntaxKind }
862
-
863
- // ─── Printer helpers ──────────────────────────────────────────────────────────
864
-
865
- /**
866
- * Converts a primitive const value to a TypeScript literal type node.
867
- * Handles negative numbers via a prefix unary expression.
868
- */
869
- export function constToTypeNode(value: string | number | boolean, format: 'string' | 'number' | 'boolean'): ts.TypeNode | undefined {
870
- if (format === 'boolean') {
871
- return createLiteralTypeNode(value === true ? createTrue() : createFalse())
872
- }
873
- if (format === 'number' && typeof value === 'number') {
874
- if (value < 0) {
875
- return createLiteralTypeNode(createPrefixUnaryExpression(SyntaxKind.MinusToken, createNumericLiteral(Math.abs(value))))
876
- }
877
- return createLiteralTypeNode(createNumericLiteral(value))
878
- }
879
- return createLiteralTypeNode(createStringLiteral(String(value)))
880
- }
881
-
882
- /**
883
- * Returns a `Date` reference type node when `representation` is `'date'`, otherwise falls back to `string`.
884
- */
885
- export function dateOrStringNode(node: { representation?: string }): ts.TypeNode {
886
- return node.representation === 'date' ? createTypeReferenceNode(createIdentifier('Date')) : keywordTypeNodes.string
887
- }
888
-
889
- /**
890
- * Maps an array of `SchemaNode`s through the printer, filtering out `null` and `undefined` results.
891
- */
892
- export function buildMemberNodes(
893
- members: Array<ast.SchemaNode> | undefined,
894
- print: (node: ast.SchemaNode) => ts.TypeNode | null | undefined,
895
- ): Array<ts.TypeNode> {
896
- return (members ?? []).map(print).filter(isNonNullable)
897
- }
898
-
899
- /**
900
- * Builds a TypeScript tuple type node from an array schema's `items`,
901
- * applying min/max slice and optional/rest element rules.
902
- */
903
- export function buildTupleNode(node: ast.ArraySchemaNode, print: (node: ast.SchemaNode) => ts.TypeNode | null | undefined): ts.TypeNode | undefined {
904
- let items = (node.items ?? []).map(print).filter(isNonNullable)
905
-
906
- const restNode = node.rest ? (print(node.rest) ?? undefined) : undefined
907
- const { min, max } = node
908
-
909
- if (max !== undefined) {
910
- items = items.slice(0, max)
911
- if (items.length < max && restNode) {
912
- items = [...items, ...Array(max - items.length).fill(restNode)]
913
- }
914
- }
915
-
916
- if (min !== undefined) {
917
- items = items.map((item, i) => (i >= min ? createOptionalTypeNode(item) : item))
918
- }
919
-
920
- if (max === undefined && restNode) {
921
- items.push(createRestTypeNode(createArrayTypeNode(restNode)))
922
- }
923
-
924
- return createTupleTypeNode(items)
925
- }
926
-
927
- /**
928
- * Applies `nullable` and optional/nullish `| undefined` union modifiers to a property's resolved base type.
929
- */
930
- export function buildPropertyType(
931
- schema: ast.SchemaNode,
932
- baseType: ts.TypeNode,
933
- optionalType: 'questionToken' | 'undefined' | 'questionTokenAndUndefined',
934
- ): ts.TypeNode {
935
- const addsUndefined = OPTIONAL_ADDS_UNDEFINED.has(optionalType)
936
- const meta = ast.syncSchemaRef(schema)
937
-
938
- let type = baseType
939
-
940
- if (meta.nullable) {
941
- type = createUnionDeclaration({ nodes: [type, keywordTypeNodes.null] })
942
- }
943
-
944
- if ((meta.nullish || meta.optional) && addsUndefined) {
945
- type = createUnionDeclaration({ nodes: [type, keywordTypeNodes.undefined] })
946
- }
947
-
948
- return type
949
- }
950
-
951
- /**
952
- * Creates TypeScript index signatures for `additionalProperties` and `patternProperties` on an object schema node.
953
- */
954
- export function buildIndexSignatures(
955
- node: { additionalProperties?: ast.SchemaNode | boolean; patternProperties?: Record<string, ast.SchemaNode> },
956
- propertyCount: number,
957
- print: (node: ast.SchemaNode) => ts.TypeNode | null | undefined,
958
- ): Array<ts.TypeElement> {
959
- const elements: Array<ts.TypeElement> = []
960
-
961
- if (node.additionalProperties && node.additionalProperties !== true) {
962
- const additionalType = print(node.additionalProperties) ?? keywordTypeNodes.unknown
963
-
964
- elements.push(createIndexSignature(propertyCount > 0 ? keywordTypeNodes.unknown : additionalType))
965
- } else if (node.additionalProperties === true) {
966
- elements.push(createIndexSignature(keywordTypeNodes.unknown))
967
- }
968
-
969
- if (node.patternProperties) {
970
- const first = Object.values(node.patternProperties)[0]
971
- if (first) {
972
- let patternType = print(first) ?? keywordTypeNodes.unknown
973
-
974
- if (first.nullable) {
975
- patternType = createUnionDeclaration({ nodes: [patternType, keywordTypeNodes.null] })
976
- }
977
- elements.push(createIndexSignature(patternType))
978
- }
979
- }
980
-
981
- return elements
982
- }