diff --git a/playground/components/TestComponent.vue b/playground/components/TestComponent.vue
index 9d77895..75bdd76 100644
--- a/playground/components/TestComponent.vue
+++ b/playground/components/TestComponent.vue
@@ -11,6 +11,14 @@ const props = defineProps({
hello: {
type: String,
default: 'Hello'
+ },
+ booleanProp: {
+ type: Boolean,
+ default: false
+ },
+ numberProp: {
+ type: Number,
+ default: 1.3
}
})
const emit = defineEmits(['change', 'delete'])
diff --git a/playground/components/testTyped.vue b/playground/components/testTyped.vue
new file mode 100644
index 0000000..e6a0a97
--- /dev/null
+++ b/playground/components/testTyped.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/module.ts b/src/module.ts
index 5bd21e1..692d307 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -1,7 +1,6 @@
import { readFile } from 'fs/promises'
import { defineNuxtModule, resolveModule, createResolver, addServerHandler } from '@nuxt/kit'
-import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc'
-import type { SFCDescriptor } from '@vue/compiler-sfc'
+import { parseComponent } from './utils/parseComponent'
export interface ModuleOptions {}
@@ -21,23 +20,7 @@ export default defineNuxtModule({
const path = resolveModule((component as any).filePath, { paths: nuxt.options.rootDir })
const source = await readFile(path, { encoding: 'utf-8' })
- // Parse component source
- const { descriptor } = parse(source)
-
- // Parse script
- const { props } = descriptor.scriptSetup
- ? parseSetupScript(name, descriptor)
- : {
- props: []
- }
-
- const { slots } = parseTemplate(name, descriptor)
-
- return {
- name,
- props,
- slots
- }
+ return parseComponent(name, source)
})
)
})
@@ -62,45 +45,3 @@ export default defineNuxtModule({
})
}
})
-
-function parseSetupScript (id: string, descriptor: SFCDescriptor) {
- const script = compileScript(descriptor, { id })
- const props = Object.entries(script.bindings).filter(([_name, type]) => type === 'props').map(([name]) => ({
- name,
- default: '?',
- type: '?',
- required: '?',
- values: '?',
- description: '?'
- }))
- return {
- props
- }
-}
-
-function parseTemplate (id: string, descriptor: SFCDescriptor) {
- if (!descriptor.template) {
- return {
- slots: []
- }
- }
-
- const template = compileTemplate({
- source: descriptor.template.content,
- id,
- filename: id
- })
-
- const findSlots = (nodes: any[]) => {
- if (!nodes.length) { return [] }
- const slots = nodes.filter(n => n.tag === 'slot').map(s => JSON.parse(s.codegenNode.arguments[1]))
- return [
- ...slots,
- ...findSlots(nodes.flatMap(n => n.children || []))
- ]
- }
-
- return {
- slots: findSlots(template.ast?.children || [])
- }
-}
diff --git a/src/types.d.ts b/src/types.d.ts
new file mode 100644
index 0000000..2752504
--- /dev/null
+++ b/src/types.d.ts
@@ -0,0 +1,8 @@
+export interface ComponentProp {
+ name: string
+ type?: string,
+ default?: any
+ required?: boolean,
+ values?: any,
+ description?: string
+}
diff --git a/src/utils/ast.ts b/src/utils/ast.ts
new file mode 100644
index 0000000..633960b
--- /dev/null
+++ b/src/utils/ast.ts
@@ -0,0 +1,39 @@
+export function visit (node, test, visitNode) {
+ if (Array.isArray(node)) {
+ return node.forEach(n => visit(n, test, visitNode))
+ }
+
+ if (!node?.type) { return }
+
+ if (test(node)) {
+ visitNode(node)
+ }
+
+ switch (node.type) {
+ case 'VariableDeclaration':
+ visit(node.declarations, test, visitNode)
+ break
+ case 'VariableDeclarator':
+ visit(node.id, test, visitNode)
+ visit(node.init, test, visitNode)
+ break
+ case 'CallExpression':
+ visit(node.callee, test, visitNode)
+ visit(node.arguments, test, visitNode)
+ visit(node.typeParameters, test, visitNode)
+ break
+ case 'ObjectExpression':
+ visit(node.properties, test, visitNode)
+ break
+ case 'ObjectProperty':
+ visit(node.key, test, visitNode)
+ visit(node.value, test, visitNode)
+ break
+ case 'TSTypeParameterInstantiation':
+ visit(node.params, test, visitNode)
+ break
+ case 'TSTypeLiteral':
+ visit(node.members, test, visitNode)
+ break
+ }
+}
diff --git a/src/utils/parseComponent.ts b/src/utils/parseComponent.ts
new file mode 100644
index 0000000..bb363e2
--- /dev/null
+++ b/src/utils/parseComponent.ts
@@ -0,0 +1,21 @@
+import { parse } from '@vue/compiler-sfc'
+import { parseSetupScript } from './parseSetupScript'
+import { parseTemplate } from './parseTemplate'
+
+export function parseComponent (name: string, source: string) {
+ // Parse component source
+ const { descriptor } = parse(source)
+
+ // Parse script
+ const { props } = descriptor.scriptSetup
+ ? parseSetupScript(name, descriptor)
+ : { props: [] }
+
+ const { slots } = parseTemplate(name, descriptor)
+
+ return {
+ name,
+ props,
+ slots
+ }
+}
diff --git a/src/utils/parseSetupScript.ts b/src/utils/parseSetupScript.ts
new file mode 100644
index 0000000..0979d7a
--- /dev/null
+++ b/src/utils/parseSetupScript.ts
@@ -0,0 +1,62 @@
+import type { SFCDescriptor } from '@vue/compiler-sfc'
+import { compileScript } from '@vue/compiler-sfc'
+import { ComponentProp } from '../types'
+import { visit } from './ast'
+
+export function parseSetupScript (id: string, descriptor: SFCDescriptor) {
+ const props: ComponentProp[] = []
+ const script = compileScript(descriptor, { id })
+
+ function getValue (prop) {
+ if (prop.type.endsWith('Literal')) {
+ return prop.value
+ }
+
+ if (prop.type === 'Identifier') {
+ return prop.name
+ }
+
+ if (prop.type === 'ObjectExpression') {
+ return prop.properties.reduce((acc, prop) => {
+ acc[prop.key.name] = getValue(prop.value)
+ return acc
+ }, {})
+ }
+ }
+ function getType (tsProperty) {
+ const { type } = tsProperty.typeAnnotation.typeAnnotation
+ switch (type) {
+ case 'TSStringKeyword':
+ return 'String'
+ case 'TSNumberKeyword':
+ return 'Number'
+ case 'TSBooleanKeyword':
+ return 'Boolean'
+ case 'TSObjectKeyword':
+ return 'Object'
+ }
+ }
+
+ visit(script.scriptSetupAst, node => node.type === 'CallExpression' && node.callee?.name === 'defineProps', (node) => {
+ const properties = node.arguments[0]?.properties || []
+ properties.reduce((props, p) => {
+ props.push({
+ name: p.key.name,
+ ...getValue(p.value)
+ })
+ return props
+ }, props)
+ visit(node, n => n.type === 'TSPropertySignature', (property) => {
+ const name = property.key.name
+ props.push({
+ name,
+ required: !property.optional,
+ type: getType(property)
+ })
+ })
+ })
+
+ return {
+ props
+ }
+}
diff --git a/src/utils/parseTemplate.ts b/src/utils/parseTemplate.ts
new file mode 100644
index 0000000..2cfda45
--- /dev/null
+++ b/src/utils/parseTemplate.ts
@@ -0,0 +1,31 @@
+import type { SFCDescriptor } from '@vue/compiler-sfc'
+import { compileTemplate } from '@vue/compiler-sfc'
+
+export function parseTemplate (id: string, descriptor: SFCDescriptor) {
+ if (!descriptor.template) {
+ return {
+ slots: []
+ }
+ }
+
+ const template = compileTemplate({
+ source: descriptor.template.content,
+ id,
+ filename: id
+ })
+
+ const findSlots = (nodes: any[]) => {
+ if (!nodes.length) { return [] }
+ const slots = nodes.filter(n => n.tag === 'slot').map(s => ({
+ name: JSON.parse(s.codegenNode.arguments[1])
+ }))
+ return [
+ ...slots,
+ ...findSlots(nodes.flatMap(n => n.children || []))
+ ]
+ }
+
+ return {
+ slots: findSlots(template.ast?.children || [])
+ }
+}
diff --git a/test/basic-component.test.ts b/test/basic-component.test.ts
new file mode 100644
index 0000000..c032c04
--- /dev/null
+++ b/test/basic-component.test.ts
@@ -0,0 +1,61 @@
+import fsp from 'fs/promises'
+import { fileURLToPath } from 'url'
+import { test, describe, expect } from 'vitest'
+import { parseComponent } from '../src/utils/parseComponent'
+
+describe('Basic Component', async () => {
+ const path = fileURLToPath(new URL('./fixtures/basic/components/BasicComponent.vue', import.meta.url))
+ const source = await fsp.readFile(path, { encoding: 'utf-8' })
+ // Parse component source
+ const { props, slots } = parseComponent('BasicComponent', source)
+
+ test('Slots', () => {
+ expect(slots).toEqual([
+ { name: 'default' },
+ { name: 'nuxt' }
+ ])
+ })
+
+ test('Props', () => {
+ expect(props).toBeDefined()
+ expect(props.length > 0)
+ })
+
+ test('String', () => {
+ const stringProps = props.filter(p => p.type === 'String')
+
+ expect(stringProps.length).toBe(1)
+ expect(stringProps[0].name).toBe('stringProp')
+ expect(stringProps[0].default).toBe('Hello')
+ })
+
+ test('Boolean', () => {
+ const booleanProps = props.filter(p => p.type === 'Boolean')
+
+ expect(booleanProps.length).toBe(1)
+ expect(booleanProps[0].name).toBe('booleanProp')
+ expect(booleanProps[0].default).toBe(false)
+ })
+
+ test('Number', () => {
+ const numberProps = props.filter(p => p.type === 'Number')
+
+ expect(numberProps.length).toBe(1)
+ expect(numberProps[0].name).toBe('numberProp')
+ expect(numberProps[0].default).toBe(1.3)
+ })
+
+ test('Array', () => {
+ const arrayProps = props.filter(p => p.type === 'Array')
+
+ expect(arrayProps.length).toBe(1)
+ expect(arrayProps[0].name).toBe('arrayProp')
+ })
+
+ test('Object', () => {
+ const objectProps = props.filter(p => p.type === 'Object')
+
+ expect(objectProps.length).toBe(1)
+ expect(objectProps[0].name).toBe('objectProp')
+ })
+})
diff --git a/test/fixtures/basic/components/BasicComponent.vue b/test/fixtures/basic/components/BasicComponent.vue
new file mode 100644
index 0000000..ab53010
--- /dev/null
+++ b/test/fixtures/basic/components/BasicComponent.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+