@@ -71,28 +71,128 @@ export function processCommandFile(ctx: NuxtDiscordContext, file: string): Slash
7171 return
7272 }
7373
74+ const whichLiteral = ( node : ts . TypeNode ) : SlashCommandOptionTypeIdentifier | undefined => {
75+ if ( ts . isLiteralTypeNode ( node ) ) {
76+ if ( ts . isStringLiteral ( node . literal ) ) {
77+ return 'string'
78+ }
79+ if ( ts . isNumericLiteral ( node . literal ) ) {
80+ return 'number'
81+ }
82+ }
83+ }
84+
85+ const getType = ( type : ts . TypeNode | undefined ) : {
86+ type : SlashCommandOptionTypeIdentifier
87+ choices ?: ( string | number ) [ ]
88+ } => {
89+ if ( ! type ) {
90+ ctx . logger . warn ( `No type found for slash command option in ${ file } , defaulting to string` )
91+ return { type : 'string' as const }
92+ }
93+
94+ if ( ts . isUnionTypeNode ( type ) ) {
95+ const types = new Set ( type . types . map ( t => whichLiteral ( t ) ) )
96+ if ( types . size !== 1 ) {
97+ ctx . logger . warn ( `Union type with multiple conflicting types found in ${ file } , defaulting to string` )
98+ return { type : 'string' as const }
99+ }
100+ const typeName = types . values ( ) . next ( ) . value
101+ if ( ! ( typeName ! in typeIdentifierToEnum ) ) {
102+ ctx . logger . warn ( `Unrecognizable type ${ type . getText ( sourceFile ) } , defaulting to string` )
103+ return { type : 'string' as const }
104+ }
105+ return {
106+ type : typeName as SlashCommandOptionTypeIdentifier ,
107+ choices : typeName === 'string'
108+ ? type . types . map ( t => t . getText ( sourceFile ) . slice ( 1 , - 1 ) )
109+ : type . types . map ( t => Number ( t . getText ( sourceFile ) ) ) ,
110+ }
111+ }
112+
113+ if ( ts . isLiteralTypeNode ( type ) ) {
114+ const typeName = whichLiteral ( type )
115+ if ( ! typeName ) {
116+ ctx . logger . warn ( `Unrecognizable literal type ${ type . getText ( sourceFile ) } , defaulting to string` )
117+ return { type : 'string' as const }
118+ }
119+ if ( ! ( typeName in typeIdentifierToEnum ) ) {
120+ ctx . logger . warn ( `Unrecognizable literal type ${ type . getText ( sourceFile ) } , defaulting to string` )
121+ return { type : 'string' as const }
122+ }
123+ return {
124+ type : typeName ,
125+ choices : typeName === 'string'
126+ ? [ type . literal . getText ( sourceFile ) . slice ( 1 , - 1 ) ]
127+ : [ Number ( type . literal . getText ( sourceFile ) ) ] ,
128+ }
129+ }
130+
131+ const typeName = type . getText ( sourceFile )
132+ if ( typeName && typeName in typeIdentifierToEnum ) {
133+ return { type : typeName as SlashCommandOptionTypeIdentifier }
134+ }
135+
136+ ctx . logger . warn ( `Unknown slash command option type: ${ type . getText ( sourceFile ) } in ${ file } , defaulting to string` )
137+ return { type : 'string' as const }
138+ }
139+
74140 for ( const param of commandDefinition . parameters ) {
75141 const name = param . name . getText ( sourceFile )
76- let type = param . type ?. getText ( sourceFile )
142+ // let type = param.type?.getText(sourceFile)
143+ let { type, choices } = getType ( param . type )
144+
145+ const jsDocTagIdx = jsDocTags
146+ . findIndex ( tag => ts . isJSDocParameterTag ( tag ) && tag . name . getText ( ) === name )
147+
148+ const findModifiers = < K extends string > ( modifiers : K [ ] ) : Record < K , any > => {
149+ let idx = jsDocTagIdx
150+ const ret = { } as Record < K , any >
151+ while ( idx >= 0 ) {
152+ const tag = jsDocTags [ idx ]
153+ if ( modifiers . includes ( tag . tagName . escapedText as string as K ) && tag . comment ) {
154+ ret [ tag . tagName . escapedText as string as K ] = JSON . parse ( tag . comment . toString ( ) )
155+ }
156+ else {
157+ return ret
158+ }
159+ idx -= 1
160+ }
161+ return ret
162+ }
77163
78- // TODO: support literal union types
164+ const jsdocDescription = jsDocTagIdx !== - 1
165+ ? jsDocTags [ jsDocTagIdx ] . comment ?. toString ( ) ?? ''
166+ : ''
79167
80- if ( ! type || ! ( type in typeIdentifierToEnum ) ) {
81- ctx . logger . warn ( `Unknown slash command option type: ${ type } for parameter ${ name } in ${ file } , defaulting to string` )
82- type = 'string' as const
168+ const modifiersMap = {
169+ string : [ 'minLength' , 'maxLength' , 'choices' ] ,
170+ number : [ 'min' , 'max' , 'choices' ] ,
171+ integer : [ 'min' , 'max' , 'choices' ] ,
172+ boolean : [ ] ,
83173 }
84174
85- const jsdocDescription = jsDocTags
86- . find ( tag => ts . isJSDocParameterTag ( tag ) && tag . name . getText ( ) === name )
87- ?. comment
88- ?. toString ( ) ?? ''
175+ const modifiers = findModifiers ( modifiersMap [ type as SlashCommandOptionTypeIdentifier ] )
176+
177+ choices = choices ?? modifiers . choices
89178
90179 command . options ! . push ( {
91180 name,
92181 // TODO: remove this type assertion
93182 type : typeIdentifierToEnum [ type as SlashCommandOptionTypeIdentifier ] ,
94183 description : jsdocDescription ,
95184 required : ! param . questionToken ,
185+ ...modifiers ,
186+ ...choices
187+ ? {
188+ choices : choices . map ( ( choice ) => {
189+ if ( typeof choice === 'string' ) {
190+ return { name : choice , value : choice }
191+ }
192+ return { name : String ( choice ) , value : choice }
193+ } ) as any , // hmm... is it possible to not use `any` here?
194+ }
195+ : { } ,
96196 } )
97197 }
98198
0 commit comments