Skip to content

Commit ff0edad

Browse files
committed
fix: props with type
1 parent a26a6af commit ff0edad

File tree

4 files changed

+188
-71
lines changed

4 files changed

+188
-71
lines changed

playground/components/TestD.vue

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script lang="ts" setup>
2+
defineProps<{
3+
msg: string
4+
count: number
5+
disabled: boolean
6+
/**
7+
* FOOOOOO
8+
*/
9+
foo: Array<string>
10+
bar: Array<string | number>
11+
}>()
12+
13+
</script>
14+
15+
<template>
16+
<span class="text-green-500">
17+
Test C <slot />
18+
</span>
19+
</template>

src/parser/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createCheckerByJson } from "vue-component-meta"
22
import type { ComponentMeta } from 'vue-component-meta'
33
import { refineMeta } from "./utils"
4-
import { join } from "pathe"
4+
import { isAbsolute, join } from "pathe"
55
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
66
import { withBase } from "ufo"
77

@@ -19,7 +19,7 @@ export function getComponentMeta(component: string, options?: Options): Componen
1919
cacheDir: join(rootDir, ".data/cache"),
2020
...options
2121
}
22-
const fullPath = withBase(component, opts.rootDir)
22+
const fullPath = isAbsolute(component) ? component : withBase(component, opts.rootDir)
2323
const cachePath = join(opts.cacheDir, `${component}.json`)
2424

2525
if (opts.cache && existsSync(cachePath)) {

src/utils/schema.ts

Lines changed: 141 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ComponentMeta } from 'vue-component-meta'
1+
import type { ComponentMeta, PropertyMetaSchema } from 'vue-component-meta'
22
import type { JsonSchema } from '../types/schema'
33

44
/**
@@ -47,105 +47,115 @@ export function propsToJsonSchema(props: ComponentMeta['props']): JsonSchema {
4747
return schema
4848
}
4949

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 }
5455
}
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+
}
5574

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)
7693
}
7794
}
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)
90101
}
91-
return convertVueTypeToJsonSchema(t, vueSchema as any)
92-
})
102+
}
103+
}
104+
// Otherwise, recursively convert
105+
return {
106+
type: 'array',
107+
items: convertVueTypeToJsonSchema(itemType, itemSchema)
93108
}
94109
}
110+
// Fallback: treat as primitive
111+
return {
112+
type: 'array',
113+
items: convertSimpleType(itemType)
114+
}
95115
}
96116

97117
// 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+
) {
99123
// Try to extract nested schema from various possible shapes
100124
let nested: Record<string, any> | undefined = undefined
101-
const vs: any = vueSchema
125+
const vs: any = unwrappedSchema
102126
if (
103127
vs &&
104128
typeof vs === 'object' &&
105129
!Array.isArray(vs) &&
106130
Object.prototype.hasOwnProperty.call(vs, 'schema') &&
107-
// @ts-ignore
108131
vs['schema'] &&
109132
typeof vs['schema'] === 'object'
110133
) {
111-
// @ts-ignore
112134
nested = vs['schema'] as Record<string, any>
113135
} else if (vs && typeof vs === 'object' && !Array.isArray(vs)) {
114136
nested = vs
115137
}
116138
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 = {
118145
type: 'object',
119-
properties: convertNestedSchemaToJsonSchemaProperties(nested as Record<string, any>),
146+
properties,
120147
additionalProperties: false
121148
}
149+
if (required.length > 0) {
150+
schemaObj.required = required
151+
}
152+
return schemaObj
122153
}
123154
// Fallback to generic object
124155
return { type: 'object' }
125156
}
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-
147157
// Handle simple types
148-
return convertSimpleType(vueType)
158+
return convertSimpleType(unwrappedType)
149159
}
150160

151161
function convertNestedSchemaToJsonSchemaProperties(nestedSchema: any): Record<string, any> {
@@ -232,3 +242,67 @@ function parseDefaultValue(defaultValue: string): any {
232242
return defaultValue
233243
}
234244
}
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+
}

test/parser.test.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, test, expect } from 'vitest'
22
import { getComponentMeta } from '../src/parser'
3-
import { propsToJsonSchema } from '../src/utils'
3+
import { propsToJsonSchema } from '../src/utils/schema'
44
import { jsonSchemaToZod } from 'json-schema-to-zod'
55

66
describe('ComponentMetaParser', () => {
@@ -93,7 +93,8 @@ describe('ComponentMetaParser', () => {
9393
}
9494
},
9595
"additionalProperties": false,
96-
"default": {}
96+
"default": {},
97+
"required": ["gello"]
9798
})
9899

99100
expect(jsonSchema.properties?.array).toEqual({
@@ -136,4 +137,27 @@ describe('ComponentMetaParser', () => {
136137
// Since no props are required, the required array should not exist
137138
expect(jsonSchema.required).toEqual(['name'])
138139
})
140+
141+
test('manual', () => {
142+
const meta = getComponentMeta('playground/components/TestD.vue')
143+
const result = propsToJsonSchema(meta.props)
144+
145+
expect(result.properties?.foo).toEqual({
146+
description: "FOOOOOO",
147+
"type": "array",
148+
"items": {
149+
"type": "string"
150+
}
151+
})
152+
153+
expect(result.properties?.bar).toEqual({
154+
"type": "array",
155+
"items": {
156+
"type": [
157+
"string",
158+
"number"
159+
]
160+
}
161+
})
162+
})
139163
})

0 commit comments

Comments
 (0)