@@ -23,12 +23,25 @@ export function propsToJsonSchema(props: ComponentMeta['props']): JsonSchema {
2323
2424 // Convert Vue prop type to JSON Schema type
2525 const propType = convertVueTypeToJsonSchema ( prop . type , prop . schema as any )
26+ // Ignore if the prop type is undefined
27+ if ( ! propType ) {
28+ continue
29+ }
30+
2631 Object . assign ( propSchema , propType )
2732
2833 // Add default value if available and not already present, only for primitive types or for object with '{}'
2934 if ( prop . default !== undefined && propSchema . default === undefined ) {
3035 propSchema . default = parseDefaultValue ( prop . default )
3136 }
37+
38+ // Also check for default values in tags
39+ if ( propSchema . default === undefined && prop . tags ) {
40+ const defaultValueTag = prop . tags . find ( tag => tag . name === 'defaultValue' )
41+ if ( defaultValueTag ) {
42+ propSchema . default = parseDefaultValue ( ( defaultValueTag as unknown as { text : string } ) . text )
43+ }
44+ }
3245
3346 // Add the property to the schema
3447 schema . properties ! [ prop . name ] = propSchema
@@ -48,6 +61,24 @@ export function propsToJsonSchema(props: ComponentMeta['props']): JsonSchema {
4861}
4962
5063function convertVueTypeToJsonSchema ( vueType : string , vueSchema : PropertyMetaSchema ) : any {
64+ // Skip function/event props as they're not useful in JSON Schema
65+ if ( isFunctionProp ( vueType , vueSchema ) ) {
66+ return undefined
67+ }
68+
69+ // Check for intersection types first (but only for simple cases, not union types)
70+ if ( ! vueType . includes ( '|' ) && vueType . includes ( ' & ' ) ) {
71+ const intersectionSchema = convertIntersectionType ( vueType )
72+ if ( intersectionSchema ) {
73+ return intersectionSchema
74+ }
75+ }
76+
77+ // Check if this is an enum type
78+ if ( isEnumType ( vueType , vueSchema ) ) {
79+ return convertEnumToJsonSchema ( vueType , vueSchema )
80+ }
81+
5182 // Unwrap enums for optionals/unions
5283 const { type : unwrappedType , schema : unwrappedSchema , enumValues } = unwrapEnumSchema ( vueType , vueSchema )
5384 if ( enumValues && unwrappedType === 'boolean' ) {
@@ -172,6 +203,12 @@ function convertNestedSchemaToJsonSchemaProperties(nestedSchema: any): Record<st
172203 } else if ( typeof prop === 'string' ) {
173204 type = prop
174205 }
206+ const converted = convertVueTypeToJsonSchema ( type , schema )
207+ // Ignore if the converted type is undefined
208+ if ( ! converted ) {
209+ continue
210+ }
211+
175212 properties [ key ] = convertVueTypeToJsonSchema ( type , schema )
176213 // Only add description if non-empty
177214 if ( description ) {
@@ -218,8 +255,9 @@ function convertSimpleType(type: string): any {
218255
219256function parseDefaultValue ( defaultValue : string ) : any {
220257 try {
221- // Remove quotes if it's a string literal
222- if ( defaultValue . startsWith ( '"' ) && defaultValue . endsWith ( '"' ) ) {
258+ // Remove quotes if it's a string literal (both single and double quotes)
259+ if ( ( defaultValue . startsWith ( '"' ) && defaultValue . endsWith ( '"' ) ) ||
260+ ( defaultValue . startsWith ( "'" ) && defaultValue . endsWith ( "'" ) ) ) {
223261 return defaultValue . slice ( 1 , - 1 )
224262 }
225263
@@ -305,4 +343,236 @@ function unwrapEnumSchema(vueType: string, vueSchema: PropertyMetaSchema): { typ
305343 }
306344
307345 return { type : vueType , schema : vueSchema }
346+ }
347+
348+ /**
349+ * Check if a type is an enum (union of string literals or boolean values)
350+ */
351+ function isEnumType ( vueType : string , vueSchema : PropertyMetaSchema ) : boolean {
352+ // Check if it's a union type with string literals or boolean values
353+ if ( typeof vueSchema === 'object' && vueSchema ?. kind === 'enum' ) {
354+ const schema = vueSchema . schema
355+ if ( schema && typeof schema === 'object' ) {
356+ const values = Object . values ( schema )
357+ // Check if all non-undefined values are string literals
358+ const stringLiterals = values . filter ( v =>
359+ v !== 'undefined' &&
360+ typeof v === 'string' &&
361+ v . startsWith ( '"' ) &&
362+ v . endsWith ( '"' )
363+ )
364+ // Check if all non-undefined values are boolean literals
365+ const booleanLiterals = values . filter ( v =>
366+ v !== 'undefined' &&
367+ ( v === 'true' || v === 'false' )
368+ )
369+ return stringLiterals . length > 0 || booleanLiterals . length > 0
370+ }
371+ }
372+
373+ // Check if the type string contains string literals
374+ if ( vueType . includes ( '"' ) && vueType . includes ( '|' ) ) {
375+ return true
376+ }
377+
378+ return false
379+ }
380+
381+ /**
382+ * Convert enum type to JSON Schema
383+ */
384+ function convertEnumToJsonSchema ( vueType : string , vueSchema : PropertyMetaSchema ) : any {
385+ if ( typeof vueSchema === 'object' && vueSchema ?. kind === 'enum' ) {
386+ const schema = vueSchema . schema
387+ if ( schema && typeof schema === 'object' ) {
388+ const enumValues : any [ ] = [ ]
389+ const types = new Set < string > ( )
390+
391+ // Extract enum values and types
392+ Object . values ( schema ) . forEach ( value => {
393+ if ( value === 'undefined' ) {
394+ // Handle optional types
395+ return
396+ }
397+
398+ if ( typeof value === 'string' ) {
399+ if ( value === 'true' || value === 'false' ) {
400+ enumValues . push ( value === 'true' )
401+ types . add ( 'boolean' )
402+ } else if ( value . startsWith ( '"' ) && value . endsWith ( '"' ) ) {
403+ enumValues . push ( value . slice ( 1 , - 1 ) ) // Remove quotes
404+ types . add ( 'string' )
405+ } else if ( value === 'string' ) {
406+ types . add ( 'string' )
407+ } else if ( value === 'number' ) {
408+ types . add ( 'number' )
409+ } else if ( value === 'boolean' ) {
410+ types . add ( 'boolean' )
411+ }
412+ } else if ( typeof value === 'object' && value !== null ) {
413+ // Complex type like (string & {}) - convert to allOf schema
414+ if ( value . type ) {
415+ const convertedType = convertIntersectionType ( value . type )
416+ if ( convertedType ) {
417+ // For intersection types in enums, we need to handle them differently
418+ // We'll add a special marker to indicate this is an intersection type
419+ types . add ( '__intersection__' )
420+ } else {
421+ types . add ( value . type )
422+ }
423+ }
424+ }
425+ } )
426+
427+ // If we have enum values, create an enum schema
428+ if ( enumValues . length > 0 ) {
429+ const result : any = { enum : enumValues }
430+
431+ // Check if we have intersection types
432+ if ( types . has ( '__intersection__' ) ) {
433+ // For enums with intersection types, we need to create a more complex schema
434+ // Find the intersection type in the original schema
435+ const intersectionType = Object . values ( schema ) . find ( v =>
436+ typeof v === 'object' && v ?. type && v . type . includes ( ' & ' )
437+ )
438+
439+ if ( intersectionType ) {
440+ const convertedIntersection = convertIntersectionType ( ( intersectionType as unknown as { type : string } ) . type )
441+ if ( convertedIntersection ) {
442+ // Create an anyOf schema that combines the enum with the intersection type
443+ return {
444+ anyOf : [
445+ { enum : enumValues } ,
446+ convertedIntersection
447+ ]
448+ }
449+ }
450+ }
451+ }
452+
453+ // Add type if it's consistent
454+ if ( types . size === 1 ) {
455+ result . type = Array . from ( types ) [ 0 ]
456+ } else if ( types . size > 1 ) {
457+ result . type = Array . from ( types )
458+ }
459+
460+ // Special case: if it's a boolean enum with just true/false, treat as regular boolean
461+ if ( types . size === 1 && types . has ( 'boolean' ) && enumValues . length === 2 &&
462+ enumValues . includes ( true ) && enumValues . includes ( false ) ) {
463+ return { type : 'boolean' }
464+ }
465+
466+ return result
467+ }
468+
469+ // If no enum values but we have types, create a union type
470+ if ( types . size > 1 ) {
471+ return { type : Array . from ( types ) }
472+ } else if ( types . size === 1 ) {
473+ return { type : Array . from ( types ) [ 0 ] }
474+ }
475+ }
476+ }
477+
478+ // Fallback: try to extract from type string
479+ if ( vueType . includes ( '"' ) && vueType . includes ( '|' ) ) {
480+ const enumValues : string [ ] = [ ]
481+ const parts = vueType . split ( '|' ) . map ( p => p . trim ( ) )
482+
483+ parts . forEach ( part => {
484+ if ( part . startsWith ( '"' ) && part . endsWith ( '"' ) ) {
485+ enumValues . push ( part . slice ( 1 , - 1 ) )
486+ } else if ( part === 'undefined' ) {
487+ // Skip undefined
488+ }
489+ } )
490+
491+ if ( enumValues . length > 0 ) {
492+ return { type : 'string' , enum : enumValues }
493+ }
494+ }
495+
496+ // Final fallback
497+ return { type : 'string' }
498+ }
499+
500+ /**
501+ * Check if a prop is a function/event prop that should be excluded from JSON Schema
502+ */
503+ function isFunctionProp ( type : string , schema : any ) : boolean {
504+ // Check if the type contains function signatures
505+ if ( type && typeof type === 'string' ) {
506+ // Look for function patterns like (event: MouseEvent) => void
507+ if ( type . includes ( '=>' ) || type . includes ( '(event:' ) || type . includes ( 'void' ) ) {
508+ return true
509+ }
510+ }
511+
512+ // Check if the schema contains event handlers
513+ if ( schema && typeof schema === 'object' ) {
514+ // Check for event kind in enum schemas
515+ if ( schema . kind === 'enum' && schema . schema ) {
516+ const values = Object . values ( schema . schema ) as Record < string , unknown > [ ]
517+ for ( const value of values ) {
518+ if ( typeof value === 'object' && value ?. kind === 'event' ) {
519+ return true
520+ }
521+ // Check nested arrays for event handlers
522+ if ( typeof value === 'object' && value ?. kind === 'array' && value . schema ) {
523+ for ( const item of value . schema as Record < string , unknown > [ ] ) {
524+ if ( typeof item === 'object' && item ?. kind === 'event' ) {
525+ return true
526+ }
527+ }
528+ }
529+ }
530+ }
531+ }
532+
533+ return false
534+ }
535+
536+ /**
537+ * Convert TypeScript intersection types to JSON Schema allOf
538+ */
539+ function convertIntersectionType ( typeString : string ) : any | null {
540+ // Handle string & {} pattern
541+ if ( typeString === 'string & {}' ) {
542+ return {
543+ allOf : [
544+ { type : 'string' } ,
545+ { type : 'object' , additionalProperties : false }
546+ ]
547+ }
548+ }
549+
550+ // Handle other intersection patterns
551+ if ( typeString . includes ( ' & ' ) ) {
552+ const parts = typeString . split ( ' & ' ) . map ( p => p . trim ( ) )
553+ const allOfSchemas = parts . map ( part => {
554+ if ( part === 'string' ) {
555+ return { type : 'string' }
556+ } else if ( part === 'number' ) {
557+ return { type : 'number' }
558+ } else if ( part === 'boolean' ) {
559+ return { type : 'boolean' }
560+ } else if ( part === 'object' ) {
561+ return { type : 'object' }
562+ } else if ( part === '{}' ) {
563+ return { type : 'object' , additionalProperties : false }
564+ } else if ( part === 'null' ) {
565+ return { type : 'null' }
566+ } else {
567+ // For other types, return as-is
568+ return { type : part }
569+ }
570+ } )
571+
572+ return {
573+ allOf : allOfSchemas
574+ }
575+ }
576+
577+ return null
308578}
0 commit comments