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

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