@@ -23,12 +23,25 @@ export function propsToJsonSchema(props: ComponentMeta['props']): JsonSchema {
23
23
24
24
// Convert Vue prop type to JSON Schema type
25
25
const propType = convertVueTypeToJsonSchema ( prop . type , prop . schema as any )
26
+ // Ignore if the prop type is undefined
27
+ if ( ! propType ) {
28
+ continue
29
+ }
30
+
26
31
Object . assign ( propSchema , propType )
27
32
28
33
// Add default value if available and not already present, only for primitive types or for object with '{}'
29
34
if ( prop . default !== undefined && propSchema . default === undefined ) {
30
35
propSchema . default = parseDefaultValue ( prop . default )
31
36
}
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
+ }
32
45
33
46
// Add the property to the schema
34
47
schema . properties ! [ prop . name ] = propSchema
@@ -48,6 +61,24 @@ export function propsToJsonSchema(props: ComponentMeta['props']): JsonSchema {
48
61
}
49
62
50
63
function 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
+
51
82
// Unwrap enums for optionals/unions
52
83
const { type : unwrappedType , schema : unwrappedSchema , enumValues } = unwrapEnumSchema ( vueType , vueSchema )
53
84
if ( enumValues && unwrappedType === 'boolean' ) {
@@ -172,6 +203,12 @@ function convertNestedSchemaToJsonSchemaProperties(nestedSchema: any): Record<st
172
203
} else if ( typeof prop === 'string' ) {
173
204
type = prop
174
205
}
206
+ const converted = convertVueTypeToJsonSchema ( type , schema )
207
+ // Ignore if the converted type is undefined
208
+ if ( ! converted ) {
209
+ continue
210
+ }
211
+
175
212
properties [ key ] = convertVueTypeToJsonSchema ( type , schema )
176
213
// Only add description if non-empty
177
214
if ( description ) {
@@ -218,8 +255,9 @@ function convertSimpleType(type: string): any {
218
255
219
256
function parseDefaultValue ( defaultValue : string ) : any {
220
257
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 ( "'" ) ) ) {
223
261
return defaultValue . slice ( 1 , - 1 )
224
262
}
225
263
@@ -305,4 +343,236 @@ function unwrapEnumSchema(vueType: string, vueSchema: PropertyMetaSchema): { typ
305
343
}
306
344
307
345
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
308
578
}
0 commit comments