@kubb/plugin-ts 5.0.0-beta.42 → 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,1003 +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 a TypeScript namespace declaration (exported module).
366
- */
367
- export function createNamespaceDeclaration({ statements, name }: { name: string; statements: Array<ts.Statement> }) {
368
- return factory.createModuleDeclaration(
369
- [factory.createToken(ts.SyntaxKind.ExportKeyword)],
370
- factory.createIdentifier(name),
371
- factory.createModuleBlock(statements),
372
- ts.NodeFlags.Namespace,
373
- )
374
- }
375
-
376
- /**
377
- * Creates an import declaration with support for default imports, named imports, namespace imports, and type-only imports.
378
- * Optionally rename imported members with `propertyName` and `name` pairs.
379
- *
380
- * @example Default import
381
- * `import Pet from './Pet'`
382
- *
383
- * @example Named imports with rename
384
- * `import { Pet as Cat } from './Pet'`
385
- *
386
- * @example Namespace import
387
- * `import * as Pet from './Pet'`
388
- */
389
- export function createImportDeclaration({
390
- name,
391
- path,
392
- isTypeOnly = false,
393
- isNameSpace = false,
394
- }: {
395
- name: string | Array<string | { propertyName: string; name?: string }>
396
- path: string
397
- isTypeOnly?: boolean
398
- isNameSpace?: boolean
399
- }) {
400
- if (!Array.isArray(name)) {
401
- const importPropertyName = isNameSpace ? undefined : factory.createIdentifier(name)
402
- const importName = isNameSpace ? factory.createNamespaceImport(factory.createIdentifier(name)) : undefined
403
-
404
- return factory.createImportDeclaration(
405
- undefined,
406
- factory.createImportClause(isTypeOnly, importPropertyName, importName),
407
- factory.createStringLiteral(path),
408
- undefined,
409
- )
410
- }
411
-
412
- // Sort the imports alphabetically for consistent output across platforms
413
- const sortedName = name.toSorted((a, b) => compareStrings(typeof a === 'object' ? a.propertyName : a, typeof b === 'object' ? b.propertyName : b))
414
-
415
- return factory.createImportDeclaration(
416
- undefined,
417
- factory.createImportClause(
418
- isTypeOnly,
419
- undefined,
420
- factory.createNamedImports(
421
- sortedName.map((item) => {
422
- if (typeof item === 'object') {
423
- const obj = item as { propertyName: string; name?: string }
424
- if (obj.name) {
425
- return factory.createImportSpecifier(false, factory.createIdentifier(obj.propertyName), factory.createIdentifier(obj.name))
426
- }
427
-
428
- return factory.createImportSpecifier(false, undefined, factory.createIdentifier(obj.propertyName))
429
- }
430
-
431
- return factory.createImportSpecifier(false, undefined, factory.createIdentifier(item))
432
- }),
433
- ),
434
- ),
435
- factory.createStringLiteral(path),
436
- undefined,
437
- )
438
- }
439
-
440
- /**
441
- * Creates an export declaration with support for named exports, namespace exports, and type-only exports.
442
- * Sorts export names alphabetically for consistent output across platforms.
443
- */
444
- export function createExportDeclaration({
445
- path,
446
- asAlias,
447
- isTypeOnly = false,
448
- name,
449
- }: {
450
- path: string
451
- asAlias?: boolean
452
- isTypeOnly?: boolean
453
- name?: string | Array<ts.Identifier | string>
454
- }) {
455
- if (name && !Array.isArray(name) && !asAlias) {
456
- console.warn(`When using name as string, asAlias should be true ${name}`)
457
- }
458
-
459
- if (!Array.isArray(name)) {
460
- const parsedName = name?.match(/^\d/) ? `_${name?.slice(1)}` : name
461
-
462
- return factory.createExportDeclaration(
463
- undefined,
464
- isTypeOnly,
465
- asAlias && parsedName ? factory.createNamespaceExport(factory.createIdentifier(parsedName)) : undefined,
466
- factory.createStringLiteral(path),
467
- undefined,
468
- )
469
- }
470
-
471
- // Sort the exports alphabetically for consistent output across platforms
472
- const sortedName = name.toSorted((a, b) => compareStrings(typeof a === 'string' ? a : a.text, typeof b === 'string' ? b : b.text))
473
-
474
- return factory.createExportDeclaration(
475
- undefined,
476
- isTypeOnly,
477
- factory.createNamedExports(
478
- sortedName.map((propertyName) => {
479
- return factory.createExportSpecifier(false, undefined, typeof propertyName === 'string' ? factory.createIdentifier(propertyName) : propertyName)
480
- }),
481
- ),
482
- factory.createStringLiteral(path),
483
- undefined,
484
- )
485
- }
486
-
487
- /**
488
- * Apply casing transformation to enum keys
489
- */
490
- function applyEnumKeyCasing(key: string, casing: 'screamingSnakeCase' | 'snakeCase' | 'pascalCase' | 'camelCase' | 'none' = 'none'): string {
491
- if (casing === 'none') {
492
- return key
493
- }
494
- if (casing === 'screamingSnakeCase') {
495
- return screamingSnakeCase(key)
496
- }
497
- if (casing === 'snakeCase') {
498
- return snakeCase(key)
499
- }
500
- if (casing === 'pascalCase') {
501
- return pascalCase(key)
502
- }
503
- if (casing === 'camelCase') {
504
- return camelCase(key)
505
- }
506
- return key
507
- }
508
-
509
- /**
510
- * Creates a TypeScript enum declaration or equivalent construct in various formats.
511
- * Returns a tuple of [name node, type node] - name node may be undefined for certain types.
512
- *
513
- * @example
514
- * ```ts
515
- * const [name, type] = createEnumDeclaration({
516
- * type: 'enum',
517
- * name: 'petType',
518
- * typeName: 'PetType',
519
- * enums: [['cat', 'cat'], ['dog', 'dog']],
520
- * })
521
- * ```
522
- */
523
- export function createEnumDeclaration({
524
- type = 'enum',
525
- name,
526
- typeName,
527
- enums,
528
- enumKeyCasing = 'none',
529
- }: {
530
- /**
531
- * Choose to use `enum`, `asConst`, `asPascalConst`, `constEnum`, or `literal` for enums.
532
- * - `enum`: TypeScript enum
533
- * - `asConst`: const with camelCase name (e.g., `petType`)
534
- * - `asPascalConst`: const with PascalCase name (e.g., `PetType`)
535
- * - `constEnum`: const enum
536
- * - `literal`: literal union type
537
- * @default `'enum'`
538
- */
539
- type?: 'enum' | 'asConst' | 'asPascalConst' | 'constEnum' | 'literal' | 'inlineLiteral'
540
- /**
541
- * Enum name in camelCase.
542
- */
543
- name: string
544
- /**
545
- * Enum name in PascalCase.
546
- */
547
- typeName: string
548
- enums: Array<[key: string | number, value: string | number | boolean]>
549
- /**
550
- * Choose the casing for enum key names.
551
- * @default 'none'
552
- */
553
- enumKeyCasing?: 'screamingSnakeCase' | 'snakeCase' | 'pascalCase' | 'camelCase' | 'none'
554
- }): [name: ts.Node | undefined, type: ts.Node] {
555
- if (type === 'literal' || type === 'inlineLiteral') {
556
- return [
557
- undefined,
558
- factory.createTypeAliasDeclaration(
559
- [factory.createToken(ts.SyntaxKind.ExportKeyword)],
560
- factory.createIdentifier(typeName),
561
- undefined,
562
- factory.createUnionTypeNode(
563
- enums
564
- .map(([_key, value]) => {
565
- if (isNumber(value)) {
566
- if (value < 0) {
567
- return factory.createLiteralTypeNode(
568
- factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value))),
569
- )
570
- }
571
- return factory.createLiteralTypeNode(factory.createNumericLiteral(value?.toString()))
572
- }
573
-
574
- if (typeof value === 'boolean') {
575
- return factory.createLiteralTypeNode(value ? factory.createTrue() : factory.createFalse())
576
- }
577
- if (value) {
578
- return factory.createLiteralTypeNode(factory.createStringLiteral(value.toString()))
579
- }
580
-
581
- return undefined
582
- })
583
- .filter((node): node is ts.LiteralTypeNode => node !== undefined),
584
- ),
585
- ),
586
- ]
587
- }
588
-
589
- if (type === 'enum' || type === 'constEnum') {
590
- return [
591
- undefined,
592
- factory.createEnumDeclaration(
593
- [factory.createToken(ts.SyntaxKind.ExportKeyword), type === 'constEnum' ? factory.createToken(ts.SyntaxKind.ConstKeyword) : undefined].filter(
594
- (modifier): modifier is ts.ModifierToken<ts.SyntaxKind.ExportKeyword> | ts.ModifierToken<ts.SyntaxKind.ConstKeyword> => modifier !== undefined,
595
- ),
596
- factory.createIdentifier(typeName),
597
- enums
598
- .map(([key, value]) => {
599
- let initializer: ts.Expression = factory.createStringLiteral(value?.toString())
600
- const isExactNumber = Number.parseInt(value.toString(), 10) === value
601
-
602
- if (isExactNumber && isNumber(Number.parseInt(value.toString(), 10))) {
603
- if ((value as number) < 0) {
604
- initializer = factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value as number)))
605
- } else {
606
- initializer = factory.createNumericLiteral(value as number)
607
- }
608
- }
609
-
610
- if (typeof value === 'boolean') {
611
- initializer = value ? factory.createTrue() : factory.createFalse()
612
- }
613
-
614
- if (isNumber(Number.parseInt(key.toString(), 10))) {
615
- const casingKey = applyEnumKeyCasing(`${typeName}_${key}`, enumKeyCasing)
616
- return factory.createEnumMember(propertyName(casingKey), initializer)
617
- }
618
-
619
- if (key) {
620
- const casingKey = applyEnumKeyCasing(key.toString(), enumKeyCasing)
621
- return factory.createEnumMember(propertyName(casingKey), initializer)
622
- }
623
-
624
- return undefined
625
- })
626
- .filter((member): member is ts.EnumMember => member !== undefined),
627
- ),
628
- ]
629
- }
630
-
631
- // used when using `as const` instead of an TypeScript enum.
632
- // name is already PascalCase for asPascalConst and camelCase for asConst (set in Type.tsx)
633
- // typeName has the Key suffix for type alias, so we use name for the const identifier
634
- const identifierName = name
635
-
636
- // When there are no enum items (empty or all-null enum), don't generate a runtime const.
637
- // Return undefined for nameNode so the barrel won't try to export a non-existent symbol.
638
- // Use `never` as the type alias to keep references valid without creating a broken const.
639
- if (enums.length === 0) {
640
- return [
641
- undefined,
642
- factory.createTypeAliasDeclaration(
643
- [factory.createToken(ts.SyntaxKind.ExportKeyword)],
644
- factory.createIdentifier(typeName),
645
- undefined,
646
- factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword),
647
- ),
648
- ]
649
- }
650
-
651
- return [
652
- factory.createVariableStatement(
653
- [factory.createToken(ts.SyntaxKind.ExportKeyword)],
654
- factory.createVariableDeclarationList(
655
- [
656
- factory.createVariableDeclaration(
657
- factory.createIdentifier(identifierName),
658
- undefined,
659
- undefined,
660
- factory.createAsExpression(
661
- factory.createObjectLiteralExpression(
662
- enums
663
- .map(([key, value]) => {
664
- let initializer: ts.Expression = factory.createStringLiteral(value?.toString())
665
-
666
- if (isNumber(value)) {
667
- // Error: Negative numbers should be created in combination with createPrefixUnaryExpression factory.
668
- // The method createNumericLiteral only accepts positive numbers
669
- // or those combined with createPrefixUnaryExpression.
670
- // Therefore, we need to ensure that the number is not negative.
671
- if (value < 0) {
672
- initializer = factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value)))
673
- } else {
674
- initializer = factory.createNumericLiteral(value)
675
- }
676
- }
677
-
678
- if (typeof value === 'boolean') {
679
- initializer = value ? factory.createTrue() : factory.createFalse()
680
- }
681
-
682
- if (key) {
683
- const casingKey = applyEnumKeyCasing(key.toString(), enumKeyCasing)
684
- return factory.createPropertyAssignment(propertyName(casingKey), initializer)
685
- }
686
-
687
- return undefined
688
- })
689
- .filter((property): property is ts.PropertyAssignment => property !== undefined),
690
- true,
691
- ),
692
- factory.createTypeReferenceNode(factory.createIdentifier('const'), undefined),
693
- ),
694
- ),
695
- ],
696
- ts.NodeFlags.Const,
697
- ),
698
- ),
699
- factory.createTypeAliasDeclaration(
700
- [factory.createToken(ts.SyntaxKind.ExportKeyword)],
701
- factory.createIdentifier(typeName),
702
- undefined,
703
- factory.createIndexedAccessTypeNode(
704
- factory.createParenthesizedType(factory.createTypeQueryNode(factory.createIdentifier(identifierName), undefined)),
705
- factory.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, factory.createTypeQueryNode(factory.createIdentifier(identifierName), undefined)),
706
- ),
707
- ),
708
- ]
709
- }
710
-
711
- /**
712
- * Creates a TypeScript `Omit<T, Keys>` type reference node.
713
- * Optionally wraps the type in `NonNullable<T>` if `nonNullable` is true.
714
- */
715
- export function createOmitDeclaration({ keys, type, nonNullable }: { keys: Array<string> | string; type: ts.TypeNode; nonNullable?: boolean }) {
716
- const node = nonNullable ? factory.createTypeReferenceNode(factory.createIdentifier('NonNullable'), [type]) : type
717
-
718
- if (Array.isArray(keys)) {
719
- return factory.createTypeReferenceNode(factory.createIdentifier('Omit'), [
720
- node,
721
- factory.createUnionTypeNode(
722
- keys.map((key) => {
723
- return factory.createLiteralTypeNode(factory.createStringLiteral(key))
724
- }),
725
- ),
726
- ])
727
- }
728
-
729
- return factory.createTypeReferenceNode(factory.createIdentifier('Omit'), [node, factory.createLiteralTypeNode(factory.createStringLiteral(keys))])
730
- }
731
-
732
- /**
733
- * Pre-built TypeScript keyword type nodes for common primitive types.
734
- * Use these to avoid repeatedly creating the same type nodes.
735
- */
736
- export const keywordTypeNodes = {
737
- any: factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
738
- unknown: factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
739
- void: factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword),
740
- number: factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
741
- integer: factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
742
- bigint: factory.createKeywordTypeNode(ts.SyntaxKind.BigIntKeyword),
743
- object: factory.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword),
744
- string: factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
745
- boolean: factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword),
746
- undefined: factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
747
- null: factory.createLiteralTypeNode(factory.createToken(ts.SyntaxKind.NullKeyword)),
748
- never: factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword),
749
- } as const
750
-
751
- /**
752
- * Converts a path like '/pet/{petId}/uploadImage' to a template literal type
753
- * like `/pet/${string}/uploadImage`
754
- */
755
- /**
756
- * Converts an OAS-style path (e.g. `/pets/{petId}`) or an Express-style path
757
- * (e.g. `/pets/:petId`) to a TypeScript template literal type
758
- * like `` `/pets/${string}` ``.
759
- */
760
- export function createUrlTemplateType(path: string): ts.TypeNode {
761
- // normalized Express `:param` → OAS `{param}` so a single regex handles both.
762
- const normalized = path.replace(/:([^/]+)/g, '{$1}')
763
-
764
- if (!normalized.includes('{')) {
765
- return factory.createLiteralTypeNode(factory.createStringLiteral(normalized))
766
- }
767
-
768
- const segments = normalized.split(/(\{[^}]+\})/)
769
- const parts: Array<string> = []
770
- const parameterIndices: Array<number> = []
771
-
772
- segments.forEach((segment) => {
773
- if (segment.startsWith('{') && segment.endsWith('}')) {
774
- parameterIndices.push(parts.length)
775
- parts.push(segment)
776
- } else if (segment) {
777
- parts.push(segment)
778
- }
779
- })
780
-
781
- const head = ts.factory.createTemplateHead(parts[0] || '')
782
- const templateSpans: Array<ts.TemplateLiteralTypeSpan> = []
783
-
784
- parameterIndices.forEach((paramIndex, i) => {
785
- const isLast = i === parameterIndices.length - 1
786
- const nextPart = parts[paramIndex + 1] || ''
787
- const literal = isLast ? ts.factory.createTemplateTail(nextPart) : ts.factory.createTemplateMiddle(nextPart)
788
- templateSpans.push(ts.factory.createTemplateLiteralTypeSpan(keywordTypeNodes.string, literal))
789
- })
790
-
791
- return ts.factory.createTemplateLiteralType(head, templateSpans)
792
- }
793
-
794
- /**
795
- * Creates a TypeScript type literal node (anonymous object type).
796
- */
797
- export const createTypeLiteralNode = factory.createTypeLiteralNode
798
-
799
- /**
800
- * Creates a TypeScript type reference node (e.g., `Array<string>`, `Record<K, V>`).
801
- */
802
- export const createTypeReferenceNode = factory.createTypeReferenceNode
803
-
804
- /**
805
- * Creates a numeric literal type node.
806
- */
807
- export const createNumericLiteral = factory.createNumericLiteral
808
-
809
- /**
810
- * Creates a string literal type node.
811
- */
812
- export const createStringLiteral = factory.createStringLiteral
813
-
814
- /**
815
- * Creates an array type node (e.g., `T[]`).
816
- */
817
- export const createArrayTypeNode = factory.createArrayTypeNode
818
-
819
- /**
820
- * Creates a parenthesized type node to control operator precedence.
821
- */
822
- export const createParenthesizedType = factory.createParenthesizedType
823
-
824
- /**
825
- * Creates a literal type node (e.g., `'hello'`, `42`, `true`).
826
- */
827
- export const createLiteralTypeNode = factory.createLiteralTypeNode
828
-
829
- /**
830
- * Creates a null literal type node.
831
- */
832
- export const createNull = factory.createNull
833
-
834
- /**
835
- * Creates an identifier node.
836
- */
837
- export const createIdentifier = factory.createIdentifier
838
-
839
- /**
840
- * Creates an optional type node (e.g., `T | undefined`).
841
- */
842
- export const createOptionalTypeNode = factory.createOptionalTypeNode
843
-
844
- /**
845
- * Creates a tuple type node (e.g., `[string, number]`).
846
- */
847
- export const createTupleTypeNode = factory.createTupleTypeNode
848
-
849
- /**
850
- * Creates a rest type node for variadic tuple elements (e.g., `...T[]`).
851
- */
852
- export const createRestTypeNode = factory.createRestTypeNode
853
-
854
- /**
855
- * Creates a boolean true literal type node.
856
- */
857
- export const createTrue = factory.createTrue
858
-
859
- /**
860
- * Creates a boolean false literal type node.
861
- */
862
- export const createFalse = factory.createFalse
863
-
864
- /**
865
- * Creates an indexed access type node (e.g., `T[K]`).
866
- */
867
- export const createIndexedAccessTypeNode = factory.createIndexedAccessTypeNode
868
-
869
- /**
870
- * Creates a type operator node (e.g., `keyof T`, `readonly T[]`).
871
- */
872
- export const createTypeOperatorNode = factory.createTypeOperatorNode
873
-
874
- /**
875
- * Creates a prefix unary expression (e.g., negative numbers, logical not).
876
- */
877
- export const createPrefixUnaryExpression = factory.createPrefixUnaryExpression
878
-
879
- /**
880
- * Exports TypeScript SyntaxKind enum for AST node type checking.
881
- */
882
- export { SyntaxKind }
883
-
884
- // ─── Printer helpers ──────────────────────────────────────────────────────────
885
-
886
- /**
887
- * Converts a primitive const value to a TypeScript literal type node.
888
- * Handles negative numbers via a prefix unary expression.
889
- */
890
- export function constToTypeNode(value: string | number | boolean, format: 'string' | 'number' | 'boolean'): ts.TypeNode | undefined {
891
- if (format === 'boolean') {
892
- return createLiteralTypeNode(value === true ? createTrue() : createFalse())
893
- }
894
- if (format === 'number' && typeof value === 'number') {
895
- if (value < 0) {
896
- return createLiteralTypeNode(createPrefixUnaryExpression(SyntaxKind.MinusToken, createNumericLiteral(Math.abs(value))))
897
- }
898
- return createLiteralTypeNode(createNumericLiteral(value))
899
- }
900
- return createLiteralTypeNode(createStringLiteral(String(value)))
901
- }
902
-
903
- /**
904
- * Returns a `Date` reference type node when `representation` is `'date'`, otherwise falls back to `string`.
905
- */
906
- export function dateOrStringNode(node: { representation?: string }): ts.TypeNode {
907
- return node.representation === 'date' ? createTypeReferenceNode(createIdentifier('Date')) : keywordTypeNodes.string
908
- }
909
-
910
- /**
911
- * Maps an array of `SchemaNode`s through the printer, filtering out `null` and `undefined` results.
912
- */
913
- export function buildMemberNodes(
914
- members: Array<ast.SchemaNode> | undefined,
915
- print: (node: ast.SchemaNode) => ts.TypeNode | null | undefined,
916
- ): Array<ts.TypeNode> {
917
- return (members ?? []).map(print).filter(isNonNullable)
918
- }
919
-
920
- /**
921
- * Builds a TypeScript tuple type node from an array schema's `items`,
922
- * applying min/max slice and optional/rest element rules.
923
- */
924
- export function buildTupleNode(node: ast.ArraySchemaNode, print: (node: ast.SchemaNode) => ts.TypeNode | null | undefined): ts.TypeNode | undefined {
925
- let items = (node.items ?? []).map(print).filter(isNonNullable)
926
-
927
- const restNode = node.rest ? (print(node.rest) ?? undefined) : undefined
928
- const { min, max } = node
929
-
930
- if (max !== undefined) {
931
- items = items.slice(0, max)
932
- if (items.length < max && restNode) {
933
- items = [...items, ...Array(max - items.length).fill(restNode)]
934
- }
935
- }
936
-
937
- if (min !== undefined) {
938
- items = items.map((item, i) => (i >= min ? createOptionalTypeNode(item) : item))
939
- }
940
-
941
- if (max === undefined && restNode) {
942
- items.push(createRestTypeNode(createArrayTypeNode(restNode)))
943
- }
944
-
945
- return createTupleTypeNode(items)
946
- }
947
-
948
- /**
949
- * Applies `nullable` and optional/nullish `| undefined` union modifiers to a property's resolved base type.
950
- */
951
- export function buildPropertyType(
952
- schema: ast.SchemaNode,
953
- baseType: ts.TypeNode,
954
- optionalType: 'questionToken' | 'undefined' | 'questionTokenAndUndefined',
955
- ): ts.TypeNode {
956
- const addsUndefined = OPTIONAL_ADDS_UNDEFINED.has(optionalType)
957
- const meta = ast.syncSchemaRef(schema)
958
-
959
- let type = baseType
960
-
961
- if (meta.nullable) {
962
- type = createUnionDeclaration({ nodes: [type, keywordTypeNodes.null] })
963
- }
964
-
965
- if ((meta.nullish || meta.optional) && addsUndefined) {
966
- type = createUnionDeclaration({ nodes: [type, keywordTypeNodes.undefined] })
967
- }
968
-
969
- return type
970
- }
971
-
972
- /**
973
- * Creates TypeScript index signatures for `additionalProperties` and `patternProperties` on an object schema node.
974
- */
975
- export function buildIndexSignatures(
976
- node: { additionalProperties?: ast.SchemaNode | boolean; patternProperties?: Record<string, ast.SchemaNode> },
977
- propertyCount: number,
978
- print: (node: ast.SchemaNode) => ts.TypeNode | null | undefined,
979
- ): Array<ts.TypeElement> {
980
- const elements: Array<ts.TypeElement> = []
981
-
982
- if (node.additionalProperties && node.additionalProperties !== true) {
983
- const additionalType = print(node.additionalProperties) ?? keywordTypeNodes.unknown
984
-
985
- elements.push(createIndexSignature(propertyCount > 0 ? keywordTypeNodes.unknown : additionalType))
986
- } else if (node.additionalProperties === true) {
987
- elements.push(createIndexSignature(keywordTypeNodes.unknown))
988
- }
989
-
990
- if (node.patternProperties) {
991
- const first = Object.values(node.patternProperties)[0]
992
- if (first) {
993
- let patternType = print(first) ?? keywordTypeNodes.unknown
994
-
995
- if (first.nullable) {
996
- patternType = createUnionDeclaration({ nodes: [patternType, keywordTypeNodes.null] })
997
- }
998
- elements.push(createIndexSignature(patternType))
999
- }
1000
- }
1001
-
1002
- return elements
1003
- }