@@ -209,12 +209,14 @@ export function tsDedupe(types: ts.TypeNode[]): ts.TypeNode[] {
209209}
210210
211211export 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 } ;
212214
213215/** Create a TS enum (with sanitized name and members) */
214216export function tsEnum (
215217 name : string ,
216218 members : ( string | number ) [ ] ,
217- metadata ?: { name ?: string ; description ?: string } [ ] ,
219+ metadata ?: EnumMemberMetadata [ ] ,
218220 options ?: { export ?: boolean ; shouldCache ?: boolean } ,
219221) {
220222 let enumName = sanitizeMemberName ( name ) ;
@@ -224,69 +226,87 @@ export function tsEnum(
224226 key = `${ members
225227 . slice ( 0 )
226228 . 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 || "" } ` ;
229231 } )
230232 . 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 ;
233237 }
234238 }
235239 const enumDeclaration = ts . factory . createEnumDeclaration (
236240 /* modifiers */ options ? tsModifiers ( { export : options . export ?? false } ) : undefined ,
237241 /* name */ enumName ,
238- /* members */ members . map ( ( value , i ) => tsEnumMember ( value , metadata ?. [ i ] ) ) ,
242+ /* members */ members . map ( ( value , index ) => tsEnumMember ( value , metadata ?. [ index ] ) ) ,
239243 ) ;
240244 options ?. shouldCache && enumCache . set ( key , enumDeclaration ) ;
241245 return enumDeclaration ;
242246}
243247
244- /** Create an exported TS array literal expression */
248+ /** Create a TS array literal expression */
245249export function tsArrayLiteralExpression (
246250 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 } ,
250255) {
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+ ) ;
253282
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" ) ) ;
257286
258- return ts . factory . createVariableStatement (
287+ const variableStatement = ts . factory . createVariableStatement (
259288 options ? tsModifiers ( { export : options . export ?? false } ) : undefined ,
260289 ts . factory . createVariableDeclarationList (
261290 [
262291 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 ,
282296 ) ,
283297 ] ,
284298 ts . NodeFlags . Const ,
285299 ) ,
286300 ) ;
301+
302+ if ( options ?. shouldCache ) {
303+ constEnumCache . set ( key , variableStatement ) ;
304+ }
305+
306+ return variableStatement ;
287307}
288308
289- function sanitizeMemberName ( name : string ) {
309+ export function sanitizeMemberName ( name : string ) {
290310 let sanitizedName = name . replace ( JS_ENUM_INVALID_CHARS_RE , ( c ) => {
291311 const last = c [ c . length - 1 ] ;
292312 return JS_PROPERTY_INDEX_INVALID_CHARS_RE . test ( last ) ? "" : last . toUpperCase ( ) ;
@@ -298,7 +318,7 @@ function sanitizeMemberName(name: string) {
298318}
299319
300320/** 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 = { } ) {
302322 let name = metadata . name ?? String ( value ) ;
303323 if ( ! JS_PROPERTY_INDEX_RE . test ( name ) ) {
304324 if ( Number ( name [ 0 ] ) >= 0 ) {
@@ -418,19 +438,40 @@ export function tsLiteral(value: unknown): ts.TypeNode {
418438 return UNKNOWN ;
419439}
420440
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+
421466/** Modifiers (readonly) */
422467export function tsModifiers ( modifiers : {
423468 readonly ?: boolean ;
424469 export ?: boolean ;
425470} ) : 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 ) ;
434475}
435476
436477/** Create a T | null union */
@@ -475,20 +516,23 @@ export function tsUnion(types: ts.TypeNode[]): ts.TypeNode {
475516 return ts . factory . createUnionTypeNode ( tsDedupe ( types ) ) ;
476517}
477518
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+
478523/** Create a WithRequired<X, Y> type */
479524export function tsWithRequired (
480525 type : ts . TypeNode ,
481526 keys : string [ ] ,
482- injectFooter : ts . Node [ ] , // needed to inject type helper if used
527+ inject : ts . Node [ ] , // needed to inject type helper if used
483528) : ts . TypeNode {
484529 if ( keys . length === 0 ) {
485530 return type ;
486531 }
487532
488533 // 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 ) ;
492536 }
493537
494538 return ts . factory . createTypeReferenceNode ( ts . factory . createIdentifier ( "WithRequired" ) , [
@@ -497,20 +541,19 @@ export function tsWithRequired(
497541 ] ) ;
498542}
499543
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+
500548/**
501549 * Enhanced ReadonlyArray.
502550 * eg: type Foo = ReadonlyArray<T>; type Bar = ReadonlyArray<T[]>
503551 * Foo and Bar are both of type `readonly T[]`
504552 */
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 ) ;
514556 }
557+
515558 return ts . factory . createTypeReferenceNode ( ts . factory . createIdentifier ( "ReadonlyArray" ) , [ type ] ) ;
516559}
0 commit comments