1
- import type { ComponentMeta } from 'vue-component-meta'
1
+ import type { ComponentMeta , PropertyMetaSchema } from 'vue-component-meta'
2
2
import type { JsonSchema } from '../types/schema'
3
3
4
4
/**
@@ -47,105 +47,115 @@ export function propsToJsonSchema(props: ComponentMeta['props']): JsonSchema {
47
47
return schema
48
48
}
49
49
50
- function convertVueTypeToJsonSchema ( vueType : string , vueSchema : any ) : any {
51
- // Handle 'any' type
52
- if ( vueType === 'any' ) {
53
- return { } // JSON Schema allows any type when no type is specified
50
+ function convertVueTypeToJsonSchema ( vueType : string , vueSchema : PropertyMetaSchema ) : any {
51
+ // Unwrap enums for optionals/unions
52
+ const { type : unwrappedType , schema : unwrappedSchema , enumValues } = unwrapEnumSchema ( vueType , vueSchema )
53
+ if ( enumValues && unwrappedType === 'boolean' ) {
54
+ return { type : 'boolean' , enum : enumValues }
54
55
}
56
+ // Handle array with nested object schema FIRST to avoid union logic for array types
57
+ if ( unwrappedType . endsWith ( '[]' ) ) {
58
+ const itemType = unwrappedType . replace ( / \[ \] $ / , '' ) . trim ( )
59
+ // If the schema is an object with kind: 'array' and schema is an array, use the first element as the item schema
60
+ // Example: { kind: 'array', type: 'string[]', schema: [ 'string' ] }
61
+ if (
62
+ unwrappedSchema &&
63
+ typeof unwrappedSchema === 'object' &&
64
+ unwrappedSchema . kind === 'array' &&
65
+ Array . isArray ( unwrappedSchema . schema ) &&
66
+ unwrappedSchema . schema . length > 0
67
+ ) {
68
+ const itemSchema = unwrappedSchema . schema [ 0 ]
69
+ return {
70
+ type : 'array' ,
71
+ items : convertVueTypeToJsonSchema ( itemSchema . type || itemType , itemSchema )
72
+ }
73
+ }
55
74
56
- // Handle union types (e.g., "string | undefined" or "{ foo: string } | undefined")
57
- if ( vueType . includes ( ' | ' ) ) {
58
- const types = vueType . split ( ' | ' ) . map ( t => t . trim ( ) )
59
- // Remove undefined and null from the union
60
- const nonNullableTypes = types . filter ( t => t !== 'undefined' && t !== 'null' )
61
-
62
- if ( nonNullableTypes . length === 1 ) {
63
- // If only one non-nullable type, use it directly
64
- // Special handling: if schema is an enum with numeric keys, extract the schema for the non-undefined type
65
- if (
66
- vueSchema &&
67
- vueSchema . kind === 'enum' &&
68
- ( vueSchema as any ) . schema &&
69
- typeof ( vueSchema as any ) . schema === 'object' &&
70
- Object . keys ( ( vueSchema as any ) . schema ) . every ( k => ! isNaN ( Number ( k ) ) )
71
- ) {
72
- // Find the schema for the non-undefined type
73
- const matching = Object . values ( ( vueSchema as any ) . schema ) . find ( ( s : any ) => s . type === nonNullableTypes [ 0 ] )
74
- if ( matching ) {
75
- return convertVueTypeToJsonSchema ( nonNullableTypes [ 0 ] , matching . schema [ 0 ] || matching . schema )
75
+ // If the schema is an object with only key '0', treat its value as the item type/schema
76
+ // Example: { kind: 'array', type: 'string[]', schema: { '0': 'string' } }
77
+ if (
78
+ unwrappedSchema &&
79
+ typeof unwrappedSchema === 'object' &&
80
+ 'schema' in unwrappedSchema &&
81
+ ( unwrappedSchema as any ) [ 'schema' ] &&
82
+ typeof ( unwrappedSchema as any ) [ 'schema' ] === 'object' &&
83
+ ! Array . isArray ( ( unwrappedSchema as any ) [ 'schema' ] ) &&
84
+ Object . keys ( ( unwrappedSchema as any ) [ 'schema' ] ) . length === 1 &&
85
+ Object . keys ( ( unwrappedSchema as any ) [ 'schema' ] ) [ 0 ] === '0'
86
+ ) {
87
+ const itemSchema = ( unwrappedSchema as any ) [ 'schema' ] [ '0' ]
88
+ // If itemSchema is a string, treat as primitive
89
+ if ( typeof itemSchema === 'string' ) {
90
+ return {
91
+ type : 'array' ,
92
+ items : convertSimpleType ( itemSchema )
76
93
}
77
94
}
78
- return convertVueTypeToJsonSchema ( nonNullableTypes [ 0 ] , vueSchema )
79
- } else if ( nonNullableTypes . length > 1 ) {
80
- // If multiple non-nullable types, use anyOf
81
- return {
82
- anyOf : nonNullableTypes . map ( t => {
83
- if ( ( t . toLowerCase ( ) === 'object' || t . match ( / ^ { .* } $ / ) ) ) {
84
- if ( vueSchema && vueSchema . kind === 'enum' && ( vueSchema as any ) . schema && typeof ( vueSchema as any ) . schema === 'object' ) {
85
- const matching = Object . values ( ( vueSchema as any ) . schema ) . find ( ( s : any ) => s . type === t )
86
- if ( matching ) {
87
- return convertVueTypeToJsonSchema ( t , matching . schema as any )
88
- }
89
- }
95
+ // If itemSchema is an enum (for union types)
96
+ if ( itemSchema && typeof itemSchema === 'object' && itemSchema . kind === 'enum' && Array . isArray ( ( itemSchema as any ) [ 'schema' ] ) ) {
97
+ return {
98
+ type : 'array' ,
99
+ items : {
100
+ type : ( itemSchema as any ) [ 'schema' ] . map ( ( t : any ) => typeof t === 'string' ? t : t . type )
90
101
}
91
- return convertVueTypeToJsonSchema ( t , vueSchema as any )
92
- } )
102
+ }
103
+ }
104
+ // Otherwise, recursively convert
105
+ return {
106
+ type : 'array' ,
107
+ items : convertVueTypeToJsonSchema ( itemType , itemSchema )
93
108
}
94
109
}
110
+ // Fallback: treat as primitive
111
+ return {
112
+ type : 'array' ,
113
+ items : convertSimpleType ( itemType )
114
+ }
95
115
}
96
116
97
117
// Handle object with nested schema
98
- if ( ( vueType . toLowerCase ( ) === 'object' || vueType . match ( / ^ { .* } $ / ) ) ) {
118
+ if (
119
+ unwrappedType . toLowerCase ( ) === 'object' ||
120
+ unwrappedType . match ( / ^ { .* } $ / ) ||
121
+ ( unwrappedSchema && typeof unwrappedSchema === 'object' && unwrappedSchema . kind === 'object' )
122
+ ) {
99
123
// Try to extract nested schema from various possible shapes
100
124
let nested : Record < string , any > | undefined = undefined
101
- const vs : any = vueSchema
125
+ const vs : any = unwrappedSchema
102
126
if (
103
127
vs &&
104
128
typeof vs === 'object' &&
105
129
! Array . isArray ( vs ) &&
106
130
Object . prototype . hasOwnProperty . call ( vs , 'schema' ) &&
107
- // @ts -ignore
108
131
vs [ 'schema' ] &&
109
132
typeof vs [ 'schema' ] === 'object'
110
133
) {
111
- // @ts -ignore
112
134
nested = vs [ 'schema' ] as Record < string , any >
113
135
} else if ( vs && typeof vs === 'object' && ! Array . isArray ( vs ) ) {
114
136
nested = vs
115
137
}
116
138
if ( nested ) {
117
- return {
139
+ const properties = convertNestedSchemaToJsonSchemaProperties ( nested as Record < string , any > )
140
+ // Collect required fields
141
+ const required = Object . entries ( nested )
142
+ . filter ( ( [ _ , v ] ) => v && typeof v === 'object' && v . required )
143
+ . map ( ( [ k ] ) => k )
144
+ const schemaObj : any = {
118
145
type : 'object' ,
119
- properties : convertNestedSchemaToJsonSchemaProperties ( nested as Record < string , any > ) ,
146
+ properties,
120
147
additionalProperties : false
121
148
}
149
+ if ( required . length > 0 ) {
150
+ schemaObj . required = required
151
+ }
152
+ return schemaObj
122
153
}
123
154
// Fallback to generic object
124
155
return { type : 'object' }
125
156
}
126
-
127
- // Handle array with nested object schema
128
- if ( vueType . endsWith ( '[]' ) ) {
129
- if ( typeof vueSchema === 'string' ) {
130
- return {
131
- type : 'array' ,
132
- items : convertSimpleType ( vueSchema )
133
- }
134
- }
135
- const itemProperties = convertNestedSchemaToJsonSchemaProperties ( vueSchema . schema )
136
- return {
137
- type : 'array' ,
138
- items : {
139
- type : 'object' ,
140
- properties : itemProperties ,
141
- required : Object . keys ( itemProperties ) ,
142
- additionalProperties : false
143
- }
144
- }
145
- }
146
-
147
157
// Handle simple types
148
- return convertSimpleType ( vueType )
158
+ return convertSimpleType ( unwrappedType )
149
159
}
150
160
151
161
function convertNestedSchemaToJsonSchemaProperties ( nestedSchema : any ) : Record < string , any > {
@@ -232,3 +242,67 @@ function parseDefaultValue(defaultValue: string): any {
232
242
return defaultValue
233
243
}
234
244
}
245
+
246
+ /**
247
+ * Here are some examples of vueSchema:
248
+ *
249
+ * ```
250
+ * {
251
+ * kind: 'enum',
252
+ * type: 'string | undefined', // <-- vueType
253
+ * schema: { '0': 'undefined', '1': 'string' }
254
+ * }
255
+ * ```
256
+ * ```
257
+ * {
258
+ * kind: 'enum',
259
+ * type: '{ hello: string; } | undefined', // <-- vueType
260
+ * schema: {
261
+ * '0': 'undefined',
262
+ * '1': { kind: 'object', type: '{ hello: string; }', schema: [...] }
263
+ * }
264
+ * }
265
+ * ```
266
+ *
267
+ *
268
+ */
269
+ function unwrapEnumSchema ( vueType : string , vueSchema : PropertyMetaSchema ) : { type : string , schema : any , enumValues ?: any [ ] } {
270
+ // If schema is an enum with undefined, unwrap to the defined type
271
+ if (
272
+ typeof vueSchema === 'object' &&
273
+ vueSchema ?. kind === 'enum' &&
274
+ vueSchema ?. schema && typeof vueSchema ?. schema === 'object'
275
+ ) {
276
+ // Collect all non-undefined values
277
+ const values = Object . values ( vueSchema . schema ) . filter ( v => v !== 'undefined' )
278
+ // Special handling for boolean enums
279
+ if ( values . every ( v => v === 'true' || v === 'false' ) ) {
280
+ // If both true and false, it's a boolean
281
+ if ( values . length === 2 ) {
282
+ return { type : 'boolean' , schema : undefined }
283
+ } else if ( values . length === 1 ) {
284
+ // Only one value, still boolean but with enum
285
+ return { type : 'boolean' , schema : undefined , enumValues : [ values [ 0 ] === 'true' ] }
286
+ }
287
+ }
288
+ // If only one non-undefined value, unwrap it
289
+ if ( values . length === 1 ) {
290
+ const s = values [ 0 ]
291
+ let t = vueType
292
+ if ( typeof s === 'object' && s . type ) t = s . type
293
+ else if ( typeof s === 'string' ) t = s
294
+ return { type : t , schema : s }
295
+ }
296
+ // Otherwise, fallback to first non-undefined
297
+ for ( const s of values ) {
298
+ if ( s !== 'undefined' ) {
299
+ let t = vueType
300
+ if ( typeof s === 'object' && s . type ) t = s . type
301
+ else if ( typeof s === 'string' ) t = s
302
+ return { type : t , schema : s }
303
+ }
304
+ }
305
+ }
306
+
307
+ return { type : vueType , schema : vueSchema }
308
+ }
0 commit comments