@@ -209,12 +209,14 @@ export function tsDedupe(types: ts.TypeNode[]): ts.TypeNode[] {
209
209
}
210
210
211
211
export const enumCache = new Map < string , ts . EnumDeclaration > ( ) ;
212
+ export const constEnumCache = new Map < string , ts . VariableStatement > ( ) ;
213
+ export type EnumMemberMetadata = { readonly name ?: string ; readonly description ?: string } ;
212
214
213
215
/** Create a TS enum (with sanitized name and members) */
214
216
export function tsEnum (
215
217
name : string ,
216
218
members : ( string | number ) [ ] ,
217
- metadata ?: { name ?: string ; description ?: string } [ ] ,
219
+ metadata ?: EnumMemberMetadata [ ] ,
218
220
options ?: { export ?: boolean ; shouldCache ?: boolean } ,
219
221
) {
220
222
let enumName = sanitizeMemberName ( name ) ;
@@ -224,69 +226,87 @@ export function tsEnum(
224
226
key = `${ members
225
227
. slice ( 0 )
226
228
. sort ( )
227
- . map ( ( v , i ) => {
228
- return `${ metadata ?. [ i ] ?. name ?? String ( v ) } :${ metadata ?. [ i ] ?. description || "" } ` ;
229
+ . map ( ( v , index ) => {
230
+ return `${ metadata ?. [ index ] ?. name ?? String ( v ) } :${ metadata ?. [ index ] ?. description || "" } ` ;
229
231
} )
230
232
. join ( "," ) } `;
231
- if ( enumCache . has ( key ) ) {
232
- return enumCache . get ( key ) as ts . EnumDeclaration ;
233
+
234
+ const cached = enumCache . get ( key ) ;
235
+ if ( cached ) {
236
+ return cached ;
233
237
}
234
238
}
235
239
const enumDeclaration = ts . factory . createEnumDeclaration (
236
240
/* modifiers */ options ? tsModifiers ( { export : options . export ?? false } ) : undefined ,
237
241
/* name */ enumName ,
238
- /* members */ members . map ( ( value , i ) => tsEnumMember ( value , metadata ?. [ i ] ) ) ,
242
+ /* members */ members . map ( ( value , index ) => tsEnumMember ( value , metadata ?. [ index ] ) ) ,
239
243
) ;
240
244
options ?. shouldCache && enumCache . set ( key , enumDeclaration ) ;
241
245
return enumDeclaration ;
242
246
}
243
247
244
- /** Create an exported TS array literal expression */
248
+ /** Create a TS array literal expression */
245
249
export function tsArrayLiteralExpression (
246
250
name : string ,
247
- elementType : ts . TypeNode ,
248
- values : ( string | number ) [ ] ,
249
- options ?: { export ?: boolean ; readonly ?: boolean ; injectFooter ?: ts . Node [ ] } ,
251
+ elementType : ts . TypeNode | undefined ,
252
+ members : ( string | number ) [ ] ,
253
+ metadata ?: readonly EnumMemberMetadata [ ] ,
254
+ options ?: { export ?: boolean ; readonly ?: boolean ; inject ?: ts . Node [ ] ; shouldCache ?: boolean } ,
250
255
) {
251
- let variableName = sanitizeMemberName ( name ) ;
252
- variableName = `${ variableName [ 0 ] . toLowerCase ( ) } ${ variableName . substring ( 1 ) } ` ;
256
+ let key = "" ;
257
+ if ( options ?. shouldCache ) {
258
+ key = `${ members
259
+ . slice ( 0 )
260
+ . sort ( )
261
+ . map ( ( v , i ) => {
262
+ return `${ metadata ?. [ i ] ?. name ?? String ( v ) } :${ metadata ?. [ i ] ?. description || "" } ` ;
263
+ } )
264
+ . join ( "," ) } `;
265
+ const cached = constEnumCache . get ( key ) ;
266
+ if ( cached ) {
267
+ return cached ;
268
+ }
269
+ }
270
+
271
+ const variableName = sanitizeMemberName ( name ) ;
272
+
273
+ const arrayType =
274
+ ( elementType && options ?. readonly ? tsReadonlyArray ( elementType , options . inject ) : undefined ) ??
275
+ ( elementType && ! options ?. readonly ? ts . factory . createArrayTypeNode ( elementType ) : undefined ) ;
276
+
277
+ const initializer = ts . factory . createArrayLiteralExpression (
278
+ members . flatMap ( ( value , index ) => {
279
+ return tsLiteralValue ( value , metadata ?. [ index ] ) ?? [ ] ;
280
+ } ) ,
281
+ ) ;
253
282
254
- const arrayType = options ?. readonly
255
- ? tsReadonlyArray ( elementType , options . injectFooter )
256
- : ts . factory . createArrayTypeNode ( elementType ) ;
283
+ const asConstInitializer = arrayType
284
+ ? initializer
285
+ : ts . factory . createAsExpression ( initializer , ts . factory . createTypeReferenceNode ( "const" ) ) ;
257
286
258
- return ts . factory . createVariableStatement (
287
+ const variableStatement = ts . factory . createVariableStatement (
259
288
options ? tsModifiers ( { export : options . export ?? false } ) : undefined ,
260
289
ts . factory . createVariableDeclarationList (
261
290
[
262
291
ts . factory . createVariableDeclaration (
263
- variableName ,
264
- undefined ,
265
- arrayType ,
266
- ts . factory . createArrayLiteralExpression (
267
- values . map ( ( value ) => {
268
- if ( typeof value === "number" ) {
269
- if ( value < 0 ) {
270
- return ts . factory . createPrefixUnaryExpression (
271
- ts . SyntaxKind . MinusToken ,
272
- ts . factory . createNumericLiteral ( Math . abs ( value ) ) ,
273
- ) ;
274
- } else {
275
- return ts . factory . createNumericLiteral ( value ) ;
276
- }
277
- } else {
278
- return ts . factory . createStringLiteral ( value ) ;
279
- }
280
- } ) ,
281
- ) ,
292
+ /* name */ variableName ,
293
+ /* exclamationToken */ undefined ,
294
+ /* type */ arrayType ,
295
+ /* initializer */ asConstInitializer ,
282
296
) ,
283
297
] ,
284
298
ts . NodeFlags . Const ,
285
299
) ,
286
300
) ;
301
+
302
+ if ( options ?. shouldCache ) {
303
+ constEnumCache . set ( key , variableStatement ) ;
304
+ }
305
+
306
+ return variableStatement ;
287
307
}
288
308
289
- function sanitizeMemberName ( name : string ) {
309
+ export function sanitizeMemberName ( name : string ) {
290
310
let sanitizedName = name . replace ( JS_ENUM_INVALID_CHARS_RE , ( c ) => {
291
311
const last = c [ c . length - 1 ] ;
292
312
return JS_PROPERTY_INDEX_INVALID_CHARS_RE . test ( last ) ? "" : last . toUpperCase ( ) ;
@@ -298,7 +318,7 @@ function sanitizeMemberName(name: string) {
298
318
}
299
319
300
320
/** Sanitize TS enum member expression */
301
- export function tsEnumMember ( value : string | number , metadata : { name ?: string ; description ?: string } = { } ) {
321
+ export function tsEnumMember ( value : string | number , metadata : EnumMemberMetadata = { } ) {
302
322
let name = metadata . name ?? String ( value ) ;
303
323
if ( ! JS_PROPERTY_INDEX_RE . test ( name ) ) {
304
324
if ( Number ( name [ 0 ] ) >= 0 ) {
@@ -418,19 +438,40 @@ export function tsLiteral(value: unknown): ts.TypeNode {
418
438
return UNKNOWN ;
419
439
}
420
440
441
+ /**
442
+ * Create a literal value (different from a literal type), such as a string or number
443
+ */
444
+ export function tsLiteralValue ( value : string | number , metadata ?: EnumMemberMetadata ) {
445
+ const literalExpression =
446
+ ( typeof value === "number" && value < 0
447
+ ? ts . factory . createPrefixUnaryExpression (
448
+ ts . SyntaxKind . MinusToken ,
449
+ ts . factory . createNumericLiteral ( Math . abs ( value ) ) ,
450
+ )
451
+ : undefined ) ??
452
+ ( typeof value === "number" ? ts . factory . createNumericLiteral ( value ) : undefined ) ??
453
+ ( typeof value === "string" ? ts . factory . createStringLiteral ( value ) : undefined ) ;
454
+
455
+ if ( literalExpression && metadata ?. description ) {
456
+ return ts . addSyntheticLeadingComment (
457
+ literalExpression ,
458
+ ts . SyntaxKind . SingleLineCommentTrivia ,
459
+ " " . concat ( metadata . description . trim ( ) ) ,
460
+ ) ;
461
+ }
462
+
463
+ return literalExpression ;
464
+ }
465
+
421
466
/** Modifiers (readonly) */
422
467
export function tsModifiers ( modifiers : {
423
468
readonly ?: boolean ;
424
469
export ?: boolean ;
425
470
} ) : ts . Modifier [ ] {
426
- const typeMods : ts . Modifier [ ] = [ ] ;
427
- if ( modifiers . export ) {
428
- typeMods . push ( ts . factory . createModifier ( ts . SyntaxKind . ExportKeyword ) ) ;
429
- }
430
- if ( modifiers . readonly ) {
431
- typeMods . push ( ts . factory . createModifier ( ts . SyntaxKind . ReadonlyKeyword ) ) ;
432
- }
433
- return typeMods ;
471
+ return [
472
+ modifiers . export ? ts . factory . createModifier ( ts . SyntaxKind . ExportKeyword ) : undefined ,
473
+ modifiers . readonly ? ts . factory . createModifier ( ts . SyntaxKind . ReadonlyKeyword ) : undefined ,
474
+ ] . filter ( ( modifier ) => modifier !== undefined ) ;
434
475
}
435
476
436
477
/** Create a T | null union */
@@ -475,20 +516,23 @@ export function tsUnion(types: ts.TypeNode[]): ts.TypeNode {
475
516
return ts . factory . createUnionTypeNode ( tsDedupe ( types ) ) ;
476
517
}
477
518
519
+ const withRequiredHelper : ts . Node = stringToAST (
520
+ "type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };" ,
521
+ ) [ 0 ] as ts . Node ;
522
+
478
523
/** Create a WithRequired<X, Y> type */
479
524
export function tsWithRequired (
480
525
type : ts . TypeNode ,
481
526
keys : string [ ] ,
482
- injectFooter : ts . Node [ ] , // needed to inject type helper if used
527
+ inject : ts . Node [ ] , // needed to inject type helper if used
483
528
) : ts . TypeNode {
484
529
if ( keys . length === 0 ) {
485
530
return type ;
486
531
}
487
532
488
533
// inject helper, if needed
489
- if ( ! injectFooter . some ( ( node ) => ts . isTypeAliasDeclaration ( node ) && node ?. name ?. escapedText === "WithRequired" ) ) {
490
- const helper = stringToAST ( "type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };" ) [ 0 ] as any ;
491
- injectFooter . push ( helper ) ;
534
+ if ( ! inject . includes ( withRequiredHelper ) ) {
535
+ inject . push ( withRequiredHelper ) ;
492
536
}
493
537
494
538
return ts . factory . createTypeReferenceNode ( ts . factory . createIdentifier ( "WithRequired" ) , [
@@ -497,20 +541,19 @@ export function tsWithRequired(
497
541
] ) ;
498
542
}
499
543
544
+ const readonlyArrayHelper : ts . Node = stringToAST (
545
+ "type ReadonlyArray<T> = [Exclude<T, undefined>] extends [any[]] ? Readonly<Exclude<T, undefined>> : Readonly<Exclude<T, undefined>[]>;" ,
546
+ ) [ 0 ] as ts . Node ;
547
+
500
548
/**
501
549
* Enhanced ReadonlyArray.
502
550
* eg: type Foo = ReadonlyArray<T>; type Bar = ReadonlyArray<T[]>
503
551
* Foo and Bar are both of type `readonly T[]`
504
552
*/
505
- export function tsReadonlyArray ( type : ts . TypeNode , injectFooter ?: ts . Node [ ] ) : ts . TypeNode {
506
- if (
507
- injectFooter &&
508
- ! injectFooter . some ( ( node ) => ts . isTypeAliasDeclaration ( node ) && node ?. name ?. escapedText === "ReadonlyArray" )
509
- ) {
510
- const helper = stringToAST (
511
- "type ReadonlyArray<T> = [Exclude<T, undefined>] extends [any[]] ? Readonly<Exclude<T, undefined>> : Readonly<Exclude<T, undefined>[]>;" ,
512
- ) [ 0 ] as any ;
513
- injectFooter . push ( helper ) ;
553
+ export function tsReadonlyArray ( type : ts . TypeNode , inject ?: ts . Node [ ] ) : ts . TypeNode {
554
+ if ( inject && ! inject . includes ( readonlyArrayHelper ) ) {
555
+ inject . push ( readonlyArrayHelper ) ;
514
556
}
557
+
515
558
return ts . factory . createTypeReferenceNode ( ts . factory . createIdentifier ( "ReadonlyArray" ) , [ type ] ) ;
516
559
}
0 commit comments