@player-tools/xlr-converters 0.0.2-next.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/types.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Specific error that can be caught to indicate an error in conversion
3
+ */
4
+ export class ConversionError extends Error {
5
+ constructor(msg: string) {
6
+ super(msg);
7
+
8
+ // Set the prototype explicitly.
9
+ Object.setPrototypeOf(this, ConversionError.prototype);
10
+ }
11
+
12
+ toString() {
13
+ return `Conversion Error: ${this.message}`;
14
+ }
15
+ }
@@ -0,0 +1,511 @@
1
+ import type {
2
+ ConditionalType,
3
+ Annotations,
4
+ FunctionType,
5
+ NamedType,
6
+ NodeType,
7
+ NodeTypeWithGenerics,
8
+ ObjectType,
9
+ PrimitiveTypes,
10
+ RecordType,
11
+ RefType,
12
+ TemplateLiteralType,
13
+ TupleType,
14
+ } from '@player-tools/xlr';
15
+ import type { TopLevelDeclaration } from '@player-tools/xlr-utils';
16
+ import {
17
+ isGenericNamedType,
18
+ isPrimitiveTypeNode,
19
+ } from '@player-tools/xlr-utils';
20
+ import ts from 'typescript';
21
+ import { ConversionError } from './types';
22
+
23
+ const templateSplit = /(?=true\|false|\.\*|\[0-9]\*)/gm;
24
+
25
+ export interface ConvertedType {
26
+ /** Converted input type represented as in TS Nodes */
27
+ type: TopLevelDeclaration;
28
+
29
+ /** Types that may require import statements to be added */
30
+ referencedTypes?: Set<string>;
31
+
32
+ /** Any additionally referenced types that were deserialized that should be separate declarations */
33
+ additionalTypes?: Map<string, TopLevelDeclaration>;
34
+ }
35
+
36
+ interface TSWriterContext {
37
+ /** */
38
+ factory: ts.NodeFactory;
39
+
40
+ /** */
41
+ throwError: (message: string) => never;
42
+ }
43
+
44
+ /** */
45
+ export class TSWriter {
46
+ private context: TSWriterContext;
47
+ private importSet: Set<string>;
48
+ private additionalTypes: Map<string, TopLevelDeclaration>;
49
+
50
+ constructor(factory?: ts.NodeFactory) {
51
+ this.context = {
52
+ factory: factory ?? ts.factory,
53
+ throwError: (message: string) => {
54
+ throw new ConversionError(message);
55
+ },
56
+ };
57
+ this.importSet = new Set();
58
+ this.additionalTypes = new Map();
59
+ }
60
+
61
+ public convertNamedType(type: NamedType): ConvertedType {
62
+ this.importSet.clear();
63
+ this.additionalTypes.clear();
64
+
65
+ const finalNode = this.convertNamedTypeNode(type);
66
+
67
+ const referencedTypes =
68
+ this.importSet.size > 0 ? this.importSet : undefined;
69
+ const additionalTypes =
70
+ this.additionalTypes.size > 0 ? this.additionalTypes : undefined;
71
+
72
+ return {
73
+ type: this.makeAnnotations(finalNode, type),
74
+ referencedTypes,
75
+ additionalTypes,
76
+ };
77
+ }
78
+
79
+ private convertNamedTypeNode(type: NamedType): TopLevelDeclaration {
80
+ const typeName = type.name;
81
+ const tsNode = this.convertTypeNode(type);
82
+
83
+ let generics;
84
+ if (isGenericNamedType(type)) {
85
+ generics = this.createTypeParameters(type);
86
+ }
87
+
88
+ let finalNode;
89
+ if (ts.isTypeLiteralNode(tsNode)) {
90
+ finalNode = this.makeInterfaceDeclaration(
91
+ typeName,
92
+ tsNode.members,
93
+ generics
94
+ );
95
+ } else {
96
+ finalNode = this.makeTypeDeclaration(typeName, tsNode, generics);
97
+ }
98
+
99
+ return finalNode;
100
+ }
101
+
102
+ private convertTypeNode(type: NodeType): ts.TypeNode {
103
+ if (type.type === 'object') {
104
+ return this.createObjectNode(type);
105
+ }
106
+
107
+ if (type.type === 'and') {
108
+ return this.context.factory.createIntersectionTypeNode(
109
+ type.and.map((element) => {
110
+ return this.convertTypeNode(element);
111
+ })
112
+ );
113
+ }
114
+
115
+ if (type.type === 'or') {
116
+ return this.context.factory.createUnionTypeNode(
117
+ type.or.map((element) => {
118
+ return this.convertTypeNode(element);
119
+ })
120
+ );
121
+ }
122
+
123
+ if (type.type === 'array') {
124
+ return this.context.factory.createTypeReferenceNode(
125
+ this.context.factory.createIdentifier('Array'),
126
+ [this.convertTypeNode(type.elementType)]
127
+ );
128
+ }
129
+
130
+ if (isPrimitiveTypeNode(type)) {
131
+ return this.createPrimitiveNode(type);
132
+ }
133
+
134
+ if (type.type === 'conditional') {
135
+ return this.createConditionalTypeNode(type);
136
+ }
137
+
138
+ if (type.type === 'function') {
139
+ return this.createFunctionDeclarationNode(type);
140
+ }
141
+
142
+ if (type.type === 'record') {
143
+ return this.createRecordNode(type);
144
+ }
145
+
146
+ if (type.type === 'ref') {
147
+ return this.createRefNode(type);
148
+ }
149
+
150
+ if (type.type === 'template') {
151
+ return this.createTemplateLiteral(type);
152
+ }
153
+
154
+ if (type.type === 'tuple') {
155
+ return this.createTupleNode(type);
156
+ }
157
+
158
+ this.context.throwError(
159
+ `Unable to convert node type: ${(type as any).type}`
160
+ );
161
+ }
162
+
163
+ private createRefNode(xlrNode: RefType): ts.TypeReferenceNode {
164
+ if (xlrNode.genericArguments) {
165
+ xlrNode.genericArguments.forEach((genericArg) => {
166
+ if (genericArg.name) {
167
+ const additionalType = this.convertNamedTypeNode(
168
+ genericArg as NamedType
169
+ );
170
+ this.additionalTypes.set(genericArg.name, additionalType);
171
+ } else if (genericArg.type === 'and') {
172
+ genericArg.and.forEach((type) => {
173
+ if (type.name) {
174
+ const additionalType = this.convertNamedTypeNode(
175
+ type as NamedType
176
+ );
177
+ this.additionalTypes.set(type.name, additionalType);
178
+ }
179
+ });
180
+ } else if (genericArg.type === 'or') {
181
+ genericArg.or.forEach((type) => {
182
+ if (type.name) {
183
+ const additionalType = this.convertNamedTypeNode(
184
+ type as NamedType
185
+ );
186
+ this.additionalTypes.set(type.name, additionalType);
187
+ }
188
+ });
189
+ }
190
+ });
191
+ }
192
+
193
+ const importName = xlrNode.ref.split('<')[0];
194
+ this.importSet.add(importName);
195
+ return this.context.factory.createTypeReferenceNode(xlrNode.ref);
196
+ }
197
+
198
+ private createPrimitiveNode(xlrNode: PrimitiveTypes): ts.TypeNode {
199
+ if (
200
+ ((xlrNode.type === 'string' ||
201
+ xlrNode.type === 'boolean' ||
202
+ xlrNode.type === 'number') &&
203
+ xlrNode.const) ||
204
+ xlrNode.type === 'null'
205
+ ) {
206
+ return this.context.factory.createLiteralTypeNode(
207
+ this.createLiteralTypeNode(xlrNode)
208
+ );
209
+ }
210
+
211
+ switch (xlrNode.type) {
212
+ case 'string':
213
+ return this.context.factory.createKeywordTypeNode(
214
+ ts.SyntaxKind.StringKeyword
215
+ );
216
+ case 'number':
217
+ return this.context.factory.createKeywordTypeNode(
218
+ ts.SyntaxKind.NumberKeyword
219
+ );
220
+ case 'boolean':
221
+ return this.context.factory.createKeywordTypeNode(
222
+ ts.SyntaxKind.BooleanKeyword
223
+ );
224
+ case 'any':
225
+ return this.context.factory.createKeywordTypeNode(
226
+ ts.SyntaxKind.AnyKeyword
227
+ );
228
+ case 'unknown':
229
+ return this.context.factory.createKeywordTypeNode(
230
+ ts.SyntaxKind.UnknownKeyword
231
+ );
232
+ case 'never':
233
+ return this.context.factory.createKeywordTypeNode(
234
+ ts.SyntaxKind.NeverKeyword
235
+ );
236
+ case 'undefined':
237
+ return this.context.factory.createKeywordTypeNode(
238
+ ts.SyntaxKind.UndefinedKeyword
239
+ );
240
+ default:
241
+ this.context.throwError(
242
+ `Unknown primitive type ${(xlrNode as any).type}`
243
+ );
244
+ }
245
+ }
246
+
247
+ private createLiteralTypeNode(
248
+ xlrNode: NodeType
249
+ ): ts.NullLiteral | ts.BooleanLiteral | ts.LiteralExpression {
250
+ if (xlrNode.type === 'boolean') {
251
+ return xlrNode.const
252
+ ? this.context.factory.createTrue()
253
+ : this.context.factory.createFalse();
254
+ }
255
+
256
+ if (xlrNode.type === 'number') {
257
+ return xlrNode.const
258
+ ? this.context.factory.createNumericLiteral(xlrNode.const)
259
+ : this.context.throwError(
260
+ "Can't make literal type out of non constant number"
261
+ );
262
+ }
263
+
264
+ if (xlrNode.type === 'string') {
265
+ return xlrNode.const
266
+ ? this.context.factory.createStringLiteral(xlrNode.const)
267
+ : this.context.throwError(
268
+ "Can't make literal type out of non constant string"
269
+ );
270
+ }
271
+
272
+ if (xlrNode.type === 'null') {
273
+ return this.context.factory.createNull();
274
+ }
275
+
276
+ this.context.throwError(`Can't make literal out of type ${xlrNode.type}`);
277
+ }
278
+
279
+ private createTupleNode(xlrNode: TupleType): ts.TypeNode {
280
+ return this.context.factory.createTupleTypeNode(
281
+ xlrNode.elementTypes.map((e) => this.convertTypeNode(e))
282
+ );
283
+ }
284
+
285
+ private createFunctionDeclarationNode(xlrNode: FunctionType): ts.TypeNode {
286
+ return this.context.factory.createFunctionTypeNode(
287
+ undefined,
288
+ xlrNode.parameters.map((e) => {
289
+ return this.context.factory.createParameterDeclaration(
290
+ undefined,
291
+ undefined,
292
+ undefined,
293
+ e.name,
294
+ e.optional
295
+ ? this.context.factory.createToken(ts.SyntaxKind.QuestionToken)
296
+ : undefined,
297
+ this.convertTypeNode(e.type),
298
+ e.default ? this.createLiteralTypeNode(e.default) : undefined
299
+ );
300
+ }),
301
+ xlrNode.returnType
302
+ ? this.convertTypeNode(xlrNode.returnType)
303
+ : this.context.factory.createToken(ts.SyntaxKind.VoidKeyword)
304
+ );
305
+ }
306
+
307
+ private createRecordNode(xlrNode: RecordType): ts.TypeNode {
308
+ const keyType = this.convertTypeNode(xlrNode.keyType);
309
+ const valueType = this.convertTypeNode(xlrNode.valueType);
310
+ return this.context.factory.createTypeReferenceNode(
311
+ this.context.factory.createIdentifier('Record'),
312
+ [keyType, valueType]
313
+ );
314
+ }
315
+
316
+ private createConditionalTypeNode(xlrNode: ConditionalType): ts.TypeNode {
317
+ const leftCheck = this.convertTypeNode(xlrNode.check.left);
318
+ const rightCheck = this.convertTypeNode(xlrNode.check.right);
319
+ const trueValue = this.convertTypeNode(xlrNode.value.true);
320
+ const falseValue = this.convertTypeNode(xlrNode.value.false);
321
+
322
+ return this.context.factory.createConditionalTypeNode(
323
+ leftCheck,
324
+ rightCheck,
325
+ trueValue,
326
+ falseValue
327
+ );
328
+ }
329
+
330
+ private createObjectNode(xlrNode: ObjectType): ts.TypeLiteralNode {
331
+ const { properties, additionalProperties = false } = xlrNode;
332
+
333
+ const propertyNodes: Array<ts.TypeElement> = [
334
+ ...Object.keys(properties)
335
+ .map((name) => ({ name, ...properties[name] }))
336
+ .map(({ name, node, required }) =>
337
+ this.makeAnnotations(
338
+ this.context.factory.createPropertySignature(
339
+ undefined, // modifiers
340
+ name,
341
+ required
342
+ ? undefined
343
+ : this.context.factory.createToken(ts.SyntaxKind.QuestionToken),
344
+ this.convertTypeNode(node)
345
+ ),
346
+ node
347
+ )
348
+ ),
349
+ ];
350
+
351
+ if (additionalProperties) {
352
+ propertyNodes.push(
353
+ this.context.factory.createIndexSignature(
354
+ undefined, // decorators
355
+ undefined, // modifiers
356
+ [
357
+ this.context.factory.createParameterDeclaration(
358
+ undefined, // decorators
359
+ undefined, // modifiers
360
+ undefined, // dotdotdot token
361
+ 'key',
362
+ undefined, // question token
363
+ this.context.factory.createKeywordTypeNode(
364
+ ts.SyntaxKind.StringKeyword
365
+ )
366
+ ),
367
+ ],
368
+ this.convertTypeNode(additionalProperties)
369
+ )
370
+ );
371
+ }
372
+
373
+ return this.context.factory.createTypeLiteralNode(propertyNodes);
374
+ }
375
+
376
+ private createTemplateLiteral(xlrNode: TemplateLiteralType) {
377
+ const templateSegments = xlrNode.format.split(templateSplit);
378
+ let templateHead;
379
+
380
+ if (templateSegments.length % 2) {
381
+ templateHead = this.context.factory.createTemplateHead(
382
+ templateSegments[0]
383
+ );
384
+ templateSegments.splice(0, 1);
385
+ } else {
386
+ templateHead = this.context.factory.createTemplateHead('');
387
+ }
388
+
389
+ return this.context.factory.createTemplateLiteralType(
390
+ templateHead,
391
+ templateSegments.map((segments, i) => {
392
+ const [regexSegment, stringSegment] = segments.split(' ', 1);
393
+
394
+ let regexTemplateType: ts.KeywordSyntaxKind;
395
+ if (regexSegment === '.*') {
396
+ regexTemplateType = ts.SyntaxKind.StringKeyword;
397
+ } else if (regexSegment === '[0-9]*') {
398
+ regexTemplateType = ts.SyntaxKind.NumberKeyword;
399
+ } else if (regexSegment === 'true|false') {
400
+ regexTemplateType = ts.SyntaxKind.BooleanKeyword;
401
+ } else {
402
+ this.context.throwError(
403
+ `Can't make template literal type from regex ${regexSegment}`
404
+ );
405
+ }
406
+
407
+ let stringTemplateType;
408
+
409
+ if (i === templateSegments.length - 1) {
410
+ stringTemplateType =
411
+ this.context.factory.createTemplateTail(stringSegment);
412
+ } else {
413
+ stringTemplateType =
414
+ this.context.factory.createTemplateMiddle(stringSegment);
415
+ }
416
+
417
+ return this.context.factory.createTemplateLiteralTypeSpan(
418
+ this.context.factory.createKeywordTypeNode(regexTemplateType),
419
+ stringTemplateType
420
+ );
421
+ })
422
+ );
423
+ }
424
+
425
+ private createGenericArgumentNode(node?: NodeType): ts.TypeNode | undefined {
426
+ if (node) {
427
+ if (node.type === 'object' && node.name) {
428
+ const additionalType = this.convertNamedTypeNode(
429
+ node as NamedType<ObjectType>
430
+ );
431
+ this.additionalTypes.set(node.name, additionalType);
432
+ return this.context.factory.createTypeReferenceNode(node.name);
433
+ }
434
+
435
+ return this.convertTypeNode(node);
436
+ }
437
+
438
+ return undefined;
439
+ }
440
+
441
+ private makeAnnotations<T extends ts.Node>(
442
+ tsNode: T,
443
+ xlrAnnotations: Annotations
444
+ ) {
445
+ let comment = xlrAnnotations.description;
446
+ if (!comment) {
447
+ return tsNode;
448
+ }
449
+
450
+ if (comment.includes('\n')) {
451
+ comment = `*\n${comment
452
+ .split('\n')
453
+ .map((s) => ` * ${s}`)
454
+ .join('\n')}\n`;
455
+ } else {
456
+ comment = `* ${comment} `;
457
+ }
458
+
459
+ return ts.addSyntheticLeadingComment(
460
+ tsNode,
461
+ ts.SyntaxKind.MultiLineCommentTrivia,
462
+ comment,
463
+ true
464
+ );
465
+ }
466
+
467
+ private createTypeParameters(
468
+ genericxlrNode: NodeTypeWithGenerics
469
+ ): Array<ts.TypeParameterDeclaration> {
470
+ return genericxlrNode.genericTokens.map((generic) => {
471
+ return this.context.factory.createTypeParameterDeclaration(
472
+ generic.symbol,
473
+ this.createGenericArgumentNode(generic.constraints),
474
+ this.createGenericArgumentNode(generic.default)
475
+ );
476
+ });
477
+ }
478
+
479
+ private makeInterfaceDeclaration(
480
+ name: string,
481
+ node: ts.NodeArray<ts.TypeElement>,
482
+ generics: Array<ts.TypeParameterDeclaration> | undefined
483
+ ) {
484
+ return this.context.factory.createInterfaceDeclaration(
485
+ undefined,
486
+ this.context.factory.createModifiersFromModifierFlags(
487
+ ts.ModifierFlags.Export
488
+ ),
489
+ this.context.factory.createIdentifier(name),
490
+ generics, // type parameters
491
+ undefined, // heritage
492
+ node
493
+ );
494
+ }
495
+
496
+ private makeTypeDeclaration(
497
+ name: string,
498
+ node: ts.TypeNode,
499
+ generics: Array<ts.TypeParameterDeclaration> | undefined
500
+ ) {
501
+ return this.context.factory.createTypeAliasDeclaration(
502
+ undefined, // decorators
503
+ this.context.factory.createModifiersFromModifierFlags(
504
+ ts.ModifierFlags.Export
505
+ ),
506
+ this.context.factory.createIdentifier(name),
507
+ generics, // type parameters
508
+ node
509
+ );
510
+ }
511
+ }