@@ -253,34 +253,196 @@ function extractDeclarations(source: string, fileName: string): ExtractedDeclara
253253 return declarations
254254}
255255
256- // --- Component props extraction ---
256+ // --- Component props & events extraction ---
257257
258258const SCRIPT_SETUP_RE = / < s c r i p t \s [ ^ > ] * \b s e t u p \b [ ^ > ] * > ( [ \s \S ] * ?) < \/ s c r i p t > /
259259
260260function extractScriptSetup ( vueSource : string ) : string | null {
261- // Match <script setup> or <script lang="ts" setup> — handles attribute order variations
262261 const match = vueSource . match ( SCRIPT_SETUP_RE )
263262 return match ?. [ 1 ] ?? null
264263}
265264
266- function extractComponentProps ( scriptSource : string , fileName : string ) : ExtractedProps | null {
265+ interface ComponentMeta {
266+ code : string
267+ defaults : Record < string , string >
268+ fields : SchemaFieldMeta [ ]
269+ events : SchemaFieldMeta [ ]
270+ models : SchemaFieldMeta [ ]
271+ }
272+
273+ function resolveTSType ( node : any , source : string ) : string {
274+ if ( ! node )
275+ return 'unknown'
276+ // Handle common TS type nodes
277+ if ( node . type === 'TSStringKeyword' )
278+ return 'string'
279+ if ( node . type === 'TSNumberKeyword' )
280+ return 'number'
281+ if ( node . type === 'TSBooleanKeyword' )
282+ return 'boolean'
283+ if ( node . type === 'TSAnyKeyword' )
284+ return 'any'
285+ if ( node . type === 'TSVoidKeyword' )
286+ return 'void'
287+ if ( node . type === 'TSNullKeyword' )
288+ return 'null'
289+ if ( node . type === 'TSUndefinedKeyword' )
290+ return 'undefined'
291+ if ( node . type === 'TSNeverKeyword' )
292+ return 'never'
293+ if ( node . type === 'TSUnionType' ) {
294+ return node . types . map ( ( t : any ) => resolveTSType ( t , source ) ) . join ( ' | ' )
295+ }
296+ if ( node . type === 'TSIntersectionType' ) {
297+ return node . types . map ( ( t : any ) => resolveTSType ( t , source ) ) . join ( ' & ' )
298+ }
299+ if ( node . type === 'TSArrayType' ) {
300+ return `${ resolveTSType ( node . elementType , source ) } []`
301+ }
302+ if ( node . type === 'TSLiteralType' ) {
303+ if ( node . literal ?. type === 'StringLiteral' || ( node . literal ?. type === 'Literal' && typeof node . literal . value === 'string' ) )
304+ return `'${ node . literal . value } '`
305+ if ( node . literal ?. type === 'NumericLiteral' || ( node . literal ?. type === 'Literal' && typeof node . literal . value === 'number' ) )
306+ return String ( node . literal . value )
307+ if ( node . literal ?. type === 'BooleanLiteral' || ( node . literal ?. type === 'Literal' && typeof node . literal . value === 'boolean' ) )
308+ return String ( node . literal . value )
309+ }
310+ // For complex types (generics, qualified names), use the raw source
311+ return source . slice ( node . start , node . end )
312+ }
313+
314+ function extractPropsFields ( typeNode : any , source : string ) : SchemaFieldMeta [ ] {
315+ if ( ! typeNode || typeNode . type !== 'TSTypeLiteral' )
316+ return [ ]
317+
318+ const fields : SchemaFieldMeta [ ] = [ ]
319+ for ( const member of typeNode . members || [ ] ) {
320+ if ( member . type !== 'TSPropertySignature' )
321+ continue
322+ const name = member . key ?. name || member . key ?. value
323+ if ( ! name )
324+ continue
325+
326+ fields . push ( {
327+ name,
328+ type : resolveTSType ( member . typeAnnotation ?. typeAnnotation , source ) ,
329+ required : ! member . optional ,
330+ } )
331+ }
332+ return fields
333+ }
334+
335+ function extractComponentMeta ( scriptSource : string , fileName : string ) : ComponentMeta | null {
267336 const { program } = parseSync ( fileName , scriptSource )
268- let result : ExtractedProps | null = null
337+ let propsResult : { code : string , defaults : Record < string , string > , fields : SchemaFieldMeta [ ] } | null = null
338+ const events : SchemaFieldMeta [ ] = [ ]
339+ const models : SchemaFieldMeta [ ] = [ ]
340+ const constArrays : Record < string , string [ ] > = { }
341+
342+ // First pass: collect `as const` arrays for event name resolution
343+ walk ( program , {
344+ enter ( node ) {
345+ if ( node . type !== 'VariableDeclaration' )
346+ return
347+ for ( const decl of node . declarations || [ ] ) {
348+ if ( decl . id ?. type !== 'Identifier' )
349+ continue
350+ const init = decl . init
351+ // Match: const foo = [...] as const
352+ if ( init ?. type === 'TSAsExpression' && init . expression ?. type === 'ArrayExpression' ) {
353+ const names : string [ ] = [ ]
354+ for ( const el of init . expression . elements || [ ] ) {
355+ // oxc-parser uses 'Literal' (not 'StringLiteral')
356+ if ( ( el . type === 'StringLiteral' || el . type === 'Literal' ) && typeof el . value === 'string' )
357+ names . push ( el . value )
358+ }
359+ if ( names . length )
360+ constArrays [ decl . id . name ] = names
361+ }
362+ }
363+ } ,
364+ } )
269365
366+ // Second pass: extract defineProps, defineEmits, defineModel
270367 walk ( program , {
271368 enter ( node ) {
272369 if ( node . type !== 'CallExpression' )
273370 return
274371
372+ // defineModel<T>('name')
373+ if ( node . callee ?. name === 'defineModel' ) {
374+ const modelName = node . arguments ?. [ 0 ] ?. value || 'modelValue'
375+ const typeParam = node . typeArguments ?. params ?. [ 0 ]
376+ models . push ( {
377+ name : `v-model:${ modelName } ` ,
378+ type : typeParam ? resolveTSType ( typeParam , scriptSource ) : 'any' ,
379+ required : false ,
380+ } )
381+ return
382+ }
383+
384+ // defineEmits<{...}>()
385+ if ( node . callee ?. name === 'defineEmits' ) {
386+ const typeArg = node . typeArguments ?. params ?. [ 0 ]
387+ if ( ! typeArg )
388+ return
389+
390+ // Parse call signatures: (event: typeof arr[number], payload?: T): void
391+ if ( typeArg . type === 'TSTypeLiteral' ) {
392+ for ( const member of typeArg . members || [ ] ) {
393+ if ( member . type !== 'TSCallSignatureDeclaration' )
394+ continue
395+ const params = member . params || [ ]
396+ if ( params . length === 0 )
397+ continue
398+
399+ // First param is the event discriminator
400+ const eventParam = params [ 0 ]
401+ const eventType = eventParam ?. typeAnnotation ?. typeAnnotation
402+
403+ // typeof constArrayName[number] → resolve to array values
404+ if ( eventType ?. type === 'TSIndexedAccessType' ) {
405+ const objType = eventType . objectType
406+ if ( objType ?. type === 'TSTypeQuery' ) {
407+ const refName = objType . exprName ?. name
408+ if ( refName && constArrays [ refName ] ) {
409+ const payloadType = params . length > 1
410+ ? resolveTSType ( params [ 1 ] ?. typeAnnotation ?. typeAnnotation , scriptSource )
411+ : undefined
412+ for ( const eventName of constArrays [ refName ] ) {
413+ events . push ( {
414+ name : eventName ,
415+ type : payloadType || '-' ,
416+ required : false ,
417+ } )
418+ }
419+ }
420+ }
421+ }
422+ // Literal string event: (event: 'ready', ...): void
423+ else if ( eventType ?. type === 'TSLiteralType' && ( eventType . literal ?. type === 'StringLiteral' || eventType . literal ?. type === 'Literal' ) ) {
424+ const payloadType = params . length > 1
425+ ? resolveTSType ( params [ 1 ] ?. typeAnnotation ?. typeAnnotation , scriptSource )
426+ : undefined
427+ events . push ( {
428+ name : eventType . literal . value ,
429+ type : payloadType || '-' ,
430+ required : false ,
431+ } )
432+ }
433+ }
434+ }
435+ return
436+ }
437+
438+ // defineProps / withDefaults(defineProps)
275439 let definePropsCall : any = null
276440 let defaultsObj : any = null
277441
278- // withDefaults(defineProps<...>(), { ... })
279442 if ( node . callee ?. name === 'withDefaults' && node . arguments ?. [ 0 ] ?. callee ?. name === 'defineProps' ) {
280443 definePropsCall = node . arguments [ 0 ]
281444 defaultsObj = node . arguments [ 1 ]
282445 }
283- // defineProps<...>()
284446 else if ( node . callee ?. name === 'defineProps' ) {
285447 definePropsCall = node
286448 }
@@ -293,26 +455,39 @@ function extractComponentProps(scriptSource: string, fileName: string): Extracte
293455 return
294456
295457 const code = scriptSource . slice ( typeArg . start , typeArg . end )
458+ const fields = extractPropsFields ( typeArg , scriptSource )
296459
297- // Extract defaults
298460 const defaults : Record < string , string > = { }
299461 if ( defaultsObj ?. type === 'ObjectExpression' ) {
300462 for ( const prop of defaultsObj . properties || [ ] ) {
301463 if ( prop . type === 'ObjectProperty' || prop . type === 'Property' ) {
302464 const key = prop . key ?. name || prop . key ?. value
303- if ( key ) {
465+ if ( key )
304466 defaults [ key ] = scriptSource . slice ( prop . value . start , prop . value . end )
305- }
306467 }
307468 }
308469 }
309470
310- result = { code, defaults }
311- this . skip ( )
471+ // Merge defaults into fields
472+ for ( const field of fields ) {
473+ if ( defaults [ field . name ] )
474+ field . defaultValue = defaults [ field . name ]
475+ }
476+
477+ propsResult = { code, defaults, fields }
312478 } ,
313479 } )
314480
315- return result
481+ if ( ! propsResult )
482+ return null
483+
484+ return {
485+ code : propsResult . code ,
486+ defaults : propsResult . defaults ,
487+ fields : [ ...propsResult . fields , ...models ] ,
488+ events,
489+ models,
490+ }
316491}
317492
318493// --- Main ---
@@ -343,7 +518,7 @@ function findVueComponents(dir: string): string[] {
343518}
344519
345520const componentFiles = findVueComponents ( componentsDir )
346- const componentProps : Record < string , ExtractedProps > = { }
521+ const componentMetas : Record < string , ComponentMeta > = { }
347522
348523for ( const filePath of componentFiles ) {
349524 const vueSource = readFileSync ( filePath , 'utf-8' )
@@ -352,9 +527,9 @@ for (const filePath of componentFiles) {
352527 continue
353528
354529 const fileName = filePath . split ( '/' ) . pop ( ) !
355- const props = extractComponentProps ( scriptSetup , fileName . replace ( '.vue' , '.ts' ) )
356- if ( props ) {
357- componentProps [ fileName . replace ( '.vue' , '' ) ] = props
530+ const meta = extractComponentMeta ( scriptSetup , fileName . replace ( '.vue' , '.ts' ) )
531+ if ( meta ) {
532+ componentMetas [ fileName . replace ( '.vue' , '' ) ] = meta
358533 }
359534}
360535
@@ -387,31 +562,46 @@ const componentToSlug: Record<string, string> = {
387562 ScriptPayPalMessages : 'paypal' ,
388563}
389564
390- // Add component props as declarations to the types JSON
391- for ( const [ componentName , props ] of Object . entries ( componentProps ) ) {
392- // Skip non-public components (loading indicator, aria indicator, etc.)
565+ // Add component props/events as declarations and schema fields
566+ for ( const [ componentName , meta ] of Object . entries ( componentMetas ) ) {
393567 const slug = componentToSlug [ componentName ]
394568 if ( ! slug )
395569 continue
396570
397571 if ( ! types [ slug ] )
398572 types [ slug ] = [ ]
399573
400- const propsInterface = `interface ${ componentName } Props ${ props . code } `
574+ const propsInterface = `interface ${ componentName } Props ${ meta . code } `
401575 types [ slug ] . push ( {
402576 name : `${ componentName } Props` ,
403577 kind : 'interface' ,
404578 code : propsInterface ,
405579 } )
406580
407- if ( Object . keys ( props . defaults ) . length ) {
408- const defaultsCode = `const ${ componentName } Defaults = ${ JSON . stringify ( props . defaults , null , 2 ) } `
581+ if ( Object . keys ( meta . defaults ) . length ) {
582+ const defaultsCode = `const ${ componentName } Defaults = ${ JSON . stringify ( meta . defaults , null , 2 ) } `
409583 types [ slug ] . push ( {
410584 name : `${ componentName } Defaults` ,
411585 kind : 'const' ,
412586 code : defaultsCode ,
413587 } )
414588 }
589+
590+ // Store structured props fields as schema fields for table rendering
591+ if ( meta . fields . length ) {
592+ schemaFields [ `${ componentName } Props` ] = meta . fields
593+ }
594+
595+ // Store events as schema fields under a separate key
596+ if ( meta . events . length ) {
597+ schemaFields [ `${ componentName } Events` ] = meta . events
598+ // Also add an events declaration so ScriptTypes can display it
599+ types [ slug ] . push ( {
600+ name : `${ componentName } Events` ,
601+ kind : 'interface' ,
602+ code : `interface ${ componentName } Events {\n${ meta . events . map ( e => ` ${ e . name } : ${ e . type } ` ) . join ( '\n' ) } \n}` ,
603+ } )
604+ }
415605}
416606
417607const output = {
@@ -420,4 +610,4 @@ const output = {
420610}
421611
422612writeFileSync ( outputPath , `${ JSON . stringify ( output , null , 2 ) } \n` )
423- console . log ( `Generated registry types for ${ Object . keys ( types ) . length } scripts (${ Object . keys ( componentProps ) . length } with component props , ${ Object . keys ( schemaFields ) . length } schema fields)` )
613+ console . log ( `Generated registry types for ${ Object . keys ( types ) . length } scripts (${ Object . keys ( componentMetas ) . length } with component meta , ${ Object . keys ( schemaFields ) . length } schema fields)` )
0 commit comments