@kubb/parser-ts 2.18.0

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 ADDED
@@ -0,0 +1,553 @@
1
+ import { isNumber } from 'remeda'
2
+ import ts from 'typescript'
3
+
4
+ const { factory } = ts
5
+
6
+ // https://ts-ast-viewer.com/
7
+
8
+ export const modifiers = {
9
+ async: factory.createModifier(ts.SyntaxKind.AsyncKeyword),
10
+ export: factory.createModifier(ts.SyntaxKind.ExportKeyword),
11
+ const: factory.createModifier(ts.SyntaxKind.ConstKeyword),
12
+ static: factory.createModifier(ts.SyntaxKind.StaticKeyword),
13
+ } as const
14
+
15
+ function isValidIdentifier(str: string): boolean {
16
+ if (!str.length || str.trim() !== str) {
17
+ return false
18
+ }
19
+ const node = ts.parseIsolatedEntityName(str, ts.ScriptTarget.Latest)
20
+
21
+ return !!node && node.kind === ts.SyntaxKind.Identifier && ts.identifierToKeywordKind(node.kind as unknown as ts.Identifier) === undefined
22
+ }
23
+
24
+ function propertyName(name: string | ts.PropertyName): ts.PropertyName {
25
+ if (typeof name === 'string') {
26
+ return isValidIdentifier(name) ? factory.createIdentifier(name) : factory.createStringLiteral(name)
27
+ }
28
+ return name
29
+ }
30
+
31
+ const questionToken = factory.createToken(ts.SyntaxKind.QuestionToken)
32
+
33
+ export function createQuestionToken(token?: boolean | ts.QuestionToken) {
34
+ if (!token) {
35
+ return undefined
36
+ }
37
+ if (token === true) {
38
+ return questionToken
39
+ }
40
+ return token
41
+ }
42
+
43
+ export function createIntersectionDeclaration({
44
+ nodes,
45
+ withParentheses,
46
+ }: {
47
+ nodes: Array<ts.TypeNode>
48
+ withParentheses?: boolean
49
+ }): ts.TypeNode | null {
50
+ if (!nodes.length) {
51
+ return null
52
+ }
53
+
54
+ if (nodes.length === 1) {
55
+ return nodes[0] || null
56
+ }
57
+
58
+ const node = factory.createIntersectionTypeNode(nodes)
59
+
60
+ if (withParentheses) {
61
+ return factory.createParenthesizedType(node)
62
+ }
63
+
64
+ return node
65
+ }
66
+
67
+ /**
68
+ * Minimum nodes length of 2
69
+ * @example `string & number`
70
+ */
71
+ export function createTupleDeclaration({
72
+ nodes,
73
+ withParentheses,
74
+ }: {
75
+ nodes: Array<ts.TypeNode>
76
+ withParentheses?: boolean
77
+ }): ts.TypeNode | null {
78
+ if (!nodes.length) {
79
+ return null
80
+ }
81
+
82
+ if (nodes.length === 1) {
83
+ return nodes[0] || null
84
+ }
85
+
86
+ const node = factory.createTupleTypeNode(nodes)
87
+
88
+ if (withParentheses) {
89
+ return factory.createParenthesizedType(node)
90
+ }
91
+
92
+ return node
93
+ }
94
+
95
+ export function createArrayDeclaration({
96
+ nodes,
97
+ }: {
98
+ nodes: Array<ts.TypeNode>
99
+ }): ts.TypeNode | null {
100
+ if (!nodes.length) {
101
+ return factory.createTupleTypeNode([])
102
+ }
103
+
104
+ if (nodes.length === 1) {
105
+ return factory.createArrayTypeNode(nodes.at(0)!)
106
+ }
107
+
108
+ return factory.createExpressionWithTypeArguments(factory.createIdentifier('Array'), [factory.createUnionTypeNode(nodes)])
109
+ }
110
+
111
+ /**
112
+ * Minimum nodes length of 2
113
+ * @example `string | number`
114
+ */
115
+ export function createUnionDeclaration({
116
+ nodes,
117
+ withParentheses,
118
+ }: {
119
+ nodes: Array<ts.TypeNode>
120
+ withParentheses?: boolean
121
+ }): ts.TypeNode | null {
122
+ if (!nodes.length) {
123
+ return null
124
+ }
125
+
126
+ if (nodes.length === 1) {
127
+ return nodes[0] || null
128
+ }
129
+
130
+ const node = factory.createUnionTypeNode(nodes)
131
+
132
+ if (withParentheses) {
133
+ return factory.createParenthesizedType(node)
134
+ }
135
+
136
+ return node
137
+ }
138
+
139
+ export function createPropertySignature({
140
+ readOnly,
141
+ modifiers = [],
142
+ name,
143
+ questionToken,
144
+ type,
145
+ }: {
146
+ readOnly?: boolean
147
+ modifiers?: Array<ts.Modifier>
148
+ name: ts.PropertyName | string
149
+ questionToken?: ts.QuestionToken | boolean
150
+ type?: ts.TypeNode
151
+ }) {
152
+ return factory.createPropertySignature(
153
+ [...modifiers, readOnly ? factory.createToken(ts.SyntaxKind.ReadonlyKeyword) : undefined].filter(Boolean),
154
+ propertyName(name),
155
+ createQuestionToken(questionToken),
156
+ type,
157
+ )
158
+ }
159
+
160
+ export function createParameterSignature(
161
+ name: string | ts.BindingName,
162
+ {
163
+ modifiers,
164
+ dotDotDotToken,
165
+ questionToken,
166
+ type,
167
+ initializer,
168
+ }: {
169
+ decorators?: Array<ts.Decorator>
170
+ modifiers?: Array<ts.Modifier>
171
+ dotDotDotToken?: ts.DotDotDotToken
172
+ questionToken?: ts.QuestionToken | boolean
173
+ type?: ts.TypeNode
174
+ initializer?: ts.Expression
175
+ },
176
+ ): ts.ParameterDeclaration {
177
+ return factory.createParameterDeclaration(modifiers, dotDotDotToken, name, createQuestionToken(questionToken), type, initializer)
178
+ }
179
+
180
+ export function createJSDoc({ comments }: { comments: string[] }) {
181
+ if (!comments.length) {
182
+ return null
183
+ }
184
+ return factory.createJSDocComment(
185
+ factory.createNodeArray(
186
+ comments.map((comment, i) => {
187
+ if (i === comments.length - 1) {
188
+ return factory.createJSDocText(comment)
189
+ }
190
+
191
+ return factory.createJSDocText(`${comment}\n`)
192
+ }),
193
+ ),
194
+ )
195
+ }
196
+
197
+ /**
198
+ * @link https://github.com/microsoft/TypeScript/issues/44151
199
+ */
200
+ export function appendJSDocToNode<TNode extends ts.Node>({
201
+ node,
202
+ comments,
203
+ }: {
204
+ node: TNode
205
+ comments: Array<string | undefined>
206
+ }) {
207
+ const filteredComments = comments.filter(Boolean)
208
+
209
+ if (!filteredComments.length) {
210
+ return node
211
+ }
212
+
213
+ const text = filteredComments.reduce((acc = '', comment = '') => {
214
+ return `${acc}\n * ${comment}`
215
+ }, '*')
216
+
217
+ // node: {...node}, with that ts.addSyntheticLeadingComment is appending
218
+ return ts.addSyntheticLeadingComment({ ...node }, ts.SyntaxKind.MultiLineCommentTrivia, `${text || '*'}\n`, true)
219
+ }
220
+
221
+ export function createIndexSignature(
222
+ type: ts.TypeNode,
223
+ {
224
+ modifiers,
225
+ indexName = 'key',
226
+ indexType = factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
227
+ }: {
228
+ indexName?: string
229
+ indexType?: ts.TypeNode
230
+ decorators?: Array<ts.Decorator>
231
+ modifiers?: Array<ts.Modifier>
232
+ } = {},
233
+ ) {
234
+ return factory.createIndexSignature(modifiers, [createParameterSignature(indexName, { type: indexType })], type)
235
+ }
236
+
237
+ export function createTypeAliasDeclaration({
238
+ modifiers,
239
+ name,
240
+ typeParameters,
241
+ type,
242
+ }: {
243
+ modifiers?: Array<ts.Modifier>
244
+ name: string | ts.Identifier
245
+ typeParameters?: Array<ts.TypeParameterDeclaration>
246
+ type: ts.TypeNode
247
+ }) {
248
+ return factory.createTypeAliasDeclaration(modifiers, name, typeParameters, type)
249
+ }
250
+
251
+ export function createNamespaceDeclaration({
252
+ statements,
253
+ name,
254
+ }: {
255
+ name: string
256
+ statements: ts.Statement[]
257
+ }) {
258
+ return factory.createModuleDeclaration(
259
+ [factory.createToken(ts.SyntaxKind.ExportKeyword)],
260
+ factory.createIdentifier(name),
261
+ factory.createModuleBlock(statements),
262
+ ts.NodeFlags.Namespace,
263
+ )
264
+ }
265
+
266
+ /**
267
+ * In { propertyName: string; name?: string } is `name` being used to make the type more unique when multiple same names are used.
268
+ * @example `import { Pet as Cat } from './Pet'`
269
+ */
270
+ export function createImportDeclaration({
271
+ name,
272
+ path,
273
+ isTypeOnly = false,
274
+ isNameSpace = false,
275
+ }: {
276
+ name: string | Array<string | { propertyName: string; name?: string }>
277
+ path: string
278
+ isTypeOnly?: boolean
279
+ isNameSpace?: boolean
280
+ }) {
281
+ if (!Array.isArray(name)) {
282
+ let importPropertyName: ts.Identifier | undefined = factory.createIdentifier(name)
283
+ let importName: ts.NamedImportBindings | undefined = undefined
284
+
285
+ if (isNameSpace) {
286
+ importPropertyName = undefined
287
+ importName = factory.createNamespaceImport(factory.createIdentifier(name))
288
+ }
289
+
290
+ return factory.createImportDeclaration(
291
+ undefined,
292
+ factory.createImportClause(isTypeOnly, importPropertyName, importName),
293
+ factory.createStringLiteral(path),
294
+ undefined,
295
+ )
296
+ }
297
+
298
+ return factory.createImportDeclaration(
299
+ undefined,
300
+ factory.createImportClause(
301
+ isTypeOnly,
302
+ undefined,
303
+ factory.createNamedImports(
304
+ name.map((item) => {
305
+ if (typeof item === 'object') {
306
+ const obj = item as { propertyName: string; name?: string }
307
+ if (obj.name) {
308
+ return factory.createImportSpecifier(false, factory.createIdentifier(obj.propertyName), factory.createIdentifier(obj.name))
309
+ }
310
+
311
+ return factory.createImportSpecifier(false, undefined, factory.createIdentifier(obj.propertyName))
312
+ }
313
+
314
+ return factory.createImportSpecifier(false, undefined, factory.createIdentifier(item))
315
+ }),
316
+ ),
317
+ ),
318
+ factory.createStringLiteral(path),
319
+ undefined,
320
+ )
321
+ }
322
+
323
+ export function createExportDeclaration({
324
+ path,
325
+ asAlias,
326
+ isTypeOnly = false,
327
+ name,
328
+ }: {
329
+ path: string
330
+ asAlias?: boolean
331
+ isTypeOnly?: boolean
332
+ name?: string | Array<ts.Identifier | string>
333
+ }) {
334
+ if (name && !Array.isArray(name) && !asAlias) {
335
+ throw new Error('When using `name` as string, `asAlias` should be true')
336
+ }
337
+
338
+ if (!Array.isArray(name)) {
339
+ const parsedName = name?.match(/^\d/) ? `_${name?.slice(1)}` : name
340
+
341
+ return factory.createExportDeclaration(
342
+ undefined,
343
+ isTypeOnly,
344
+ asAlias && parsedName ? factory.createNamespaceExport(factory.createIdentifier(parsedName)) : undefined,
345
+ factory.createStringLiteral(path),
346
+ undefined,
347
+ )
348
+ }
349
+
350
+ return factory.createExportDeclaration(
351
+ undefined,
352
+ isTypeOnly,
353
+ factory.createNamedExports(
354
+ name.map((propertyName) => {
355
+ return factory.createExportSpecifier(false, undefined, typeof propertyName === 'string' ? factory.createIdentifier(propertyName) : propertyName)
356
+ }),
357
+ ),
358
+ factory.createStringLiteral(path),
359
+ undefined,
360
+ )
361
+ }
362
+
363
+ export function createEnumDeclaration({
364
+ type = 'enum',
365
+ name,
366
+ typeName,
367
+ enums,
368
+ }: {
369
+ /**
370
+ * @default `'enum'`
371
+ */
372
+ type?: 'enum' | 'asConst' | 'asPascalConst' | 'constEnum' | 'literal'
373
+ /**
374
+ * Enum name in camelCase.
375
+ */
376
+ name: string
377
+ /**
378
+ * Enum name in PascalCase.
379
+ */
380
+ typeName: string
381
+ enums: [key: string | number, value: string | number | boolean][]
382
+ }) {
383
+ if (type === 'literal') {
384
+ return [
385
+ factory.createTypeAliasDeclaration(
386
+ [factory.createToken(ts.SyntaxKind.ExportKeyword)],
387
+ factory.createIdentifier(typeName),
388
+ undefined,
389
+ factory.createUnionTypeNode(
390
+ enums
391
+ .map(([_key, value]) => {
392
+ if (isNumber(value)) {
393
+ return factory.createLiteralTypeNode(factory.createNumericLiteral(value?.toString()))
394
+ }
395
+
396
+ if (typeof value === 'boolean') {
397
+ return factory.createLiteralTypeNode(value ? factory.createTrue() : factory.createFalse())
398
+ }
399
+ if (value) {
400
+ return factory.createLiteralTypeNode(factory.createStringLiteral(value.toString()))
401
+ }
402
+
403
+ return undefined
404
+ })
405
+ .filter(Boolean),
406
+ ),
407
+ ),
408
+ ]
409
+ }
410
+
411
+ if (type === 'enum' || type === 'constEnum') {
412
+ return [
413
+ factory.createEnumDeclaration(
414
+ [factory.createToken(ts.SyntaxKind.ExportKeyword), type === 'constEnum' ? factory.createToken(ts.SyntaxKind.ConstKeyword) : undefined].filter(Boolean),
415
+ factory.createIdentifier(typeName),
416
+ enums
417
+ .map(([key, value]) => {
418
+ let initializer: ts.Expression = factory.createStringLiteral(value?.toString())
419
+
420
+ if (isNumber(Number.parseInt(value.toString()))) {
421
+ initializer = factory.createNumericLiteral(value as number)
422
+ }
423
+ if (typeof value === 'boolean') {
424
+ initializer = value ? factory.createTrue() : factory.createFalse()
425
+ }
426
+
427
+ if (isNumber(Number.parseInt(key.toString()))) {
428
+ return factory.createEnumMember(factory.createStringLiteral(`${typeName}_${key}`), initializer)
429
+ }
430
+
431
+ if (key) {
432
+ return factory.createEnumMember(factory.createStringLiteral(`${key}`), initializer)
433
+ }
434
+
435
+ return undefined
436
+ })
437
+ .filter(Boolean),
438
+ ),
439
+ ]
440
+ }
441
+
442
+ // used when using `as const` instead of an TypeScript enum.
443
+ const identifierName = type === 'asPascalConst' ? typeName : name
444
+
445
+ return [
446
+ factory.createVariableStatement(
447
+ [factory.createToken(ts.SyntaxKind.ExportKeyword)],
448
+ factory.createVariableDeclarationList(
449
+ [
450
+ factory.createVariableDeclaration(
451
+ factory.createIdentifier(identifierName),
452
+ undefined,
453
+ undefined,
454
+ factory.createAsExpression(
455
+ factory.createObjectLiteralExpression(
456
+ enums
457
+ .map(([key, value]) => {
458
+ let initializer: ts.Expression = factory.createStringLiteral(`${value?.toString()}`)
459
+
460
+ if (isNumber(value)) {
461
+ // Error: Negative numbers should be created in combination with createPrefixUnaryExpression factory.
462
+ // The method createNumericLiteral only accepts positive numbers
463
+ // or those combined with createPrefixUnaryExpression.
464
+ // Therefore, we need to ensure that the number is not negative.
465
+ if (value < 0) {
466
+ initializer = factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, factory.createNumericLiteral(Math.abs(value)))
467
+ } else {
468
+ initializer = factory.createNumericLiteral(value)
469
+ }
470
+ }
471
+
472
+ if (typeof value === 'boolean') {
473
+ initializer = value ? factory.createTrue() : factory.createFalse()
474
+ }
475
+
476
+ if (key) {
477
+ return factory.createPropertyAssignment(factory.createStringLiteral(`${key}`), initializer)
478
+ }
479
+
480
+ return undefined
481
+ })
482
+ .filter(Boolean),
483
+ true,
484
+ ),
485
+ factory.createTypeReferenceNode(factory.createIdentifier('const'), undefined),
486
+ ),
487
+ ),
488
+ ],
489
+ ts.NodeFlags.Const,
490
+ ),
491
+ ),
492
+ factory.createTypeAliasDeclaration(
493
+ [factory.createToken(ts.SyntaxKind.ExportKeyword)],
494
+ factory.createIdentifier(typeName),
495
+ undefined,
496
+ factory.createIndexedAccessTypeNode(
497
+ factory.createParenthesizedType(factory.createTypeQueryNode(factory.createIdentifier(identifierName), undefined)),
498
+ factory.createTypeOperatorNode(ts.SyntaxKind.KeyOfKeyword, factory.createTypeQueryNode(factory.createIdentifier(identifierName), undefined)),
499
+ ),
500
+ ),
501
+ ]
502
+ }
503
+
504
+ export function createOmitDeclaration({
505
+ keys,
506
+ type,
507
+ nonNullable,
508
+ }: {
509
+ keys: Array<string> | string
510
+ type: ts.TypeNode
511
+ nonNullable?: boolean
512
+ }) {
513
+ const node = nonNullable ? factory.createTypeReferenceNode(factory.createIdentifier('NonNullable'), [type]) : type
514
+
515
+ if (Array.isArray(keys)) {
516
+ return factory.createTypeReferenceNode(factory.createIdentifier('Omit'), [
517
+ node,
518
+ factory.createUnionTypeNode(
519
+ keys.map((key) => {
520
+ return factory.createLiteralTypeNode(factory.createStringLiteral(key))
521
+ }),
522
+ ),
523
+ ])
524
+ }
525
+
526
+ return factory.createTypeReferenceNode(factory.createIdentifier('Omit'), [node, factory.createLiteralTypeNode(factory.createStringLiteral(keys))])
527
+ }
528
+
529
+ export const keywordTypeNodes = {
530
+ any: factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
531
+ unknown: factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
532
+ number: factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
533
+ integer: factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
534
+ object: factory.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword),
535
+ string: factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
536
+ boolean: factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword),
537
+ undefined: factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
538
+ null: factory.createLiteralTypeNode(factory.createToken(ts.SyntaxKind.NullKeyword)),
539
+ } as const
540
+
541
+ export const createTypeLiteralNode = factory.createTypeLiteralNode
542
+
543
+ export const createTypeReferenceNode = factory.createTypeReferenceNode
544
+ export const createNumericLiteral = factory.createNumericLiteral
545
+ export const createStringLiteral = factory.createStringLiteral
546
+
547
+ export const createArrayTypeNode = factory.createArrayTypeNode
548
+
549
+ export const createLiteralTypeNode = factory.createLiteralTypeNode
550
+ export const createNull = factory.createNull
551
+ export const createIdentifier = factory.createIdentifier
552
+
553
+ export const createTupleTypeNode = factory.createTupleTypeNode
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { getExports } from './api.ts'
2
+ export { parse } from './parse.ts'
3
+ export { print } from './print.ts'
4
+ export type { default as ts } from 'typescript'
5
+ export * as factory from './factory.ts'
package/src/parse.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { print } from './print.ts'
2
+
3
+ import type ts from 'typescript'
4
+
5
+ type ParseResult = {
6
+ ast: ts.Node
7
+ text: string
8
+ }
9
+
10
+ export function parse(ast: ts.Node): ParseResult {
11
+ return {
12
+ ast,
13
+ text: print(ast),
14
+ }
15
+ }
package/src/print.ts ADDED
@@ -0,0 +1,54 @@
1
+ import ts from 'typescript'
2
+
3
+ import type { PrinterOptions } from 'typescript'
4
+
5
+ const { factory } = ts
6
+
7
+ type Options = {
8
+ source?: string
9
+ baseName?: string
10
+ } & PrinterOptions
11
+
12
+ /**
13
+ * Escaped new lines in code with block comments so they can be restored by {@link restoreNewLines}
14
+ * @param {string} code The code to escape new lines in
15
+ * @returns The same code but with new lines escaped using block comments
16
+ */
17
+ const escapeNewLines = (code: string) => code.replace(/\n\n/g, '\n/* :newline: */')
18
+
19
+ /**
20
+ * Reverses {@link escapeNewLines} and restores new lines
21
+ * @param {string} code The code with escaped new lines
22
+ * @returns The same code with new lines restored
23
+ */
24
+ const restoreNewLines = (code: string) => code.replace(/\/\* :newline: \*\//g, '\n')
25
+
26
+ export function print(
27
+ elements: ts.Node | Array<ts.Node | undefined> | null,
28
+ { source = '', baseName = 'print.ts', removeComments, noEmitHelpers, newLine = ts.NewLineKind.LineFeed }: Options = {},
29
+ ): string {
30
+ const sourceFile = ts.createSourceFile(baseName, escapeNewLines(source), ts.ScriptTarget.ES2022, false, ts.ScriptKind.TS)
31
+ const printer = ts.createPrinter({
32
+ omitTrailingSemicolon: true,
33
+ newLine,
34
+ removeComments,
35
+ noEmitHelpers,
36
+ })
37
+
38
+ let nodes: Array<ts.Node> = []
39
+
40
+ if (!elements) {
41
+ return ''
42
+ }
43
+
44
+ if (Array.isArray(elements)) {
45
+ nodes = elements.filter(Boolean)
46
+ } else {
47
+ nodes = [elements].filter(Boolean)
48
+ }
49
+
50
+ const outputFile = printer.printList(ts.ListFormat.MultiLine, factory.createNodeArray(nodes), sourceFile)
51
+ const outputSource = printer.printFile(sourceFile)
52
+
53
+ return [outputFile, restoreNewLines(outputSource)].filter(Boolean).join('\n')
54
+ }