Skip to content

Commit 8f89275

Browse files
authored
feat: extract props info from defineProps (#21)
1 parent d3ded69 commit 8f89275

File tree

10 files changed

+282
-61
lines changed

10 files changed

+282
-61
lines changed

playground/components/TestComponent.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ const props = defineProps({
1111
hello: {
1212
type: String,
1313
default: 'Hello'
14+
},
15+
booleanProp: {
16+
type: Boolean,
17+
default: false
18+
},
19+
numberProp: {
20+
type: Number,
21+
default: 1.3
1422
}
1523
})
1624
const emit = defineEmits(['change', 'delete'])

playground/components/testTyped.vue

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<template>
2+
<div>
3+
<slot />
4+
<hr>
5+
<slot name="nuxt" />
6+
</div>
7+
</template>
8+
9+
<script setup lang="ts">
10+
const props = defineProps<{
11+
hello: string,
12+
booleanProp?: boolean,
13+
numberProp?: number
14+
}>()
15+
const emit = defineEmits(['change', 'delete'])
16+
17+
</script>

src/module.ts

Lines changed: 2 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { readFile } from 'fs/promises'
22
import { defineNuxtModule, resolveModule, createResolver, addServerHandler } from '@nuxt/kit'
3-
import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc'
4-
import type { SFCDescriptor } from '@vue/compiler-sfc'
3+
import { parseComponent } from './utils/parseComponent'
54

65
export interface ModuleOptions {}
76

@@ -21,23 +20,7 @@ export default defineNuxtModule<ModuleOptions>({
2120
const path = resolveModule((component as any).filePath, { paths: nuxt.options.rootDir })
2221
const source = await readFile(path, { encoding: 'utf-8' })
2322

24-
// Parse component source
25-
const { descriptor } = parse(source)
26-
27-
// Parse script
28-
const { props } = descriptor.scriptSetup
29-
? parseSetupScript(name, descriptor)
30-
: {
31-
props: []
32-
}
33-
34-
const { slots } = parseTemplate(name, descriptor)
35-
36-
return {
37-
name,
38-
props,
39-
slots
40-
}
23+
return parseComponent(name, source)
4124
})
4225
)
4326
})
@@ -62,45 +45,3 @@ export default defineNuxtModule<ModuleOptions>({
6245
})
6346
}
6447
})
65-
66-
function parseSetupScript (id: string, descriptor: SFCDescriptor) {
67-
const script = compileScript(descriptor, { id })
68-
const props = Object.entries(script.bindings).filter(([_name, type]) => type === 'props').map(([name]) => ({
69-
name,
70-
default: '?',
71-
type: '?',
72-
required: '?',
73-
values: '?',
74-
description: '?'
75-
}))
76-
return {
77-
props
78-
}
79-
}
80-
81-
function parseTemplate (id: string, descriptor: SFCDescriptor) {
82-
if (!descriptor.template) {
83-
return {
84-
slots: []
85-
}
86-
}
87-
88-
const template = compileTemplate({
89-
source: descriptor.template.content,
90-
id,
91-
filename: id
92-
})
93-
94-
const findSlots = (nodes: any[]) => {
95-
if (!nodes.length) { return [] }
96-
const slots = nodes.filter(n => n.tag === 'slot').map(s => JSON.parse(s.codegenNode.arguments[1]))
97-
return [
98-
...slots,
99-
...findSlots(nodes.flatMap(n => n.children || []))
100-
]
101-
}
102-
103-
return {
104-
slots: findSlots(template.ast?.children || [])
105-
}
106-
}

src/types.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface ComponentProp {
2+
name: string
3+
type?: string,
4+
default?: any
5+
required?: boolean,
6+
values?: any,
7+
description?: string
8+
}

src/utils/ast.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export function visit (node, test, visitNode) {
2+
if (Array.isArray(node)) {
3+
return node.forEach(n => visit(n, test, visitNode))
4+
}
5+
6+
if (!node?.type) { return }
7+
8+
if (test(node)) {
9+
visitNode(node)
10+
}
11+
12+
switch (node.type) {
13+
case 'VariableDeclaration':
14+
visit(node.declarations, test, visitNode)
15+
break
16+
case 'VariableDeclarator':
17+
visit(node.id, test, visitNode)
18+
visit(node.init, test, visitNode)
19+
break
20+
case 'CallExpression':
21+
visit(node.callee, test, visitNode)
22+
visit(node.arguments, test, visitNode)
23+
visit(node.typeParameters, test, visitNode)
24+
break
25+
case 'ObjectExpression':
26+
visit(node.properties, test, visitNode)
27+
break
28+
case 'ObjectProperty':
29+
visit(node.key, test, visitNode)
30+
visit(node.value, test, visitNode)
31+
break
32+
case 'TSTypeParameterInstantiation':
33+
visit(node.params, test, visitNode)
34+
break
35+
case 'TSTypeLiteral':
36+
visit(node.members, test, visitNode)
37+
break
38+
}
39+
}

src/utils/parseComponent.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { parse } from '@vue/compiler-sfc'
2+
import { parseSetupScript } from './parseSetupScript'
3+
import { parseTemplate } from './parseTemplate'
4+
5+
export function parseComponent (name: string, source: string) {
6+
// Parse component source
7+
const { descriptor } = parse(source)
8+
9+
// Parse script
10+
const { props } = descriptor.scriptSetup
11+
? parseSetupScript(name, descriptor)
12+
: { props: [] }
13+
14+
const { slots } = parseTemplate(name, descriptor)
15+
16+
return {
17+
name,
18+
props,
19+
slots
20+
}
21+
}

src/utils/parseSetupScript.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { SFCDescriptor } from '@vue/compiler-sfc'
2+
import { compileScript } from '@vue/compiler-sfc'
3+
import { ComponentProp } from '../types'
4+
import { visit } from './ast'
5+
6+
export function parseSetupScript (id: string, descriptor: SFCDescriptor) {
7+
const props: ComponentProp[] = []
8+
const script = compileScript(descriptor, { id })
9+
10+
function getValue (prop) {
11+
if (prop.type.endsWith('Literal')) {
12+
return prop.value
13+
}
14+
15+
if (prop.type === 'Identifier') {
16+
return prop.name
17+
}
18+
19+
if (prop.type === 'ObjectExpression') {
20+
return prop.properties.reduce((acc, prop) => {
21+
acc[prop.key.name] = getValue(prop.value)
22+
return acc
23+
}, {})
24+
}
25+
}
26+
function getType (tsProperty) {
27+
const { type } = tsProperty.typeAnnotation.typeAnnotation
28+
switch (type) {
29+
case 'TSStringKeyword':
30+
return 'String'
31+
case 'TSNumberKeyword':
32+
return 'Number'
33+
case 'TSBooleanKeyword':
34+
return 'Boolean'
35+
case 'TSObjectKeyword':
36+
return 'Object'
37+
}
38+
}
39+
40+
visit(script.scriptSetupAst, node => node.type === 'CallExpression' && node.callee?.name === 'defineProps', (node) => {
41+
const properties = node.arguments[0]?.properties || []
42+
properties.reduce((props, p) => {
43+
props.push({
44+
name: p.key.name,
45+
...getValue(p.value)
46+
})
47+
return props
48+
}, props)
49+
visit(node, n => n.type === 'TSPropertySignature', (property) => {
50+
const name = property.key.name
51+
props.push({
52+
name,
53+
required: !property.optional,
54+
type: getType(property)
55+
})
56+
})
57+
})
58+
59+
return {
60+
props
61+
}
62+
}

src/utils/parseTemplate.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { SFCDescriptor } from '@vue/compiler-sfc'
2+
import { compileTemplate } from '@vue/compiler-sfc'
3+
4+
export function parseTemplate (id: string, descriptor: SFCDescriptor) {
5+
if (!descriptor.template) {
6+
return {
7+
slots: []
8+
}
9+
}
10+
11+
const template = compileTemplate({
12+
source: descriptor.template.content,
13+
id,
14+
filename: id
15+
})
16+
17+
const findSlots = (nodes: any[]) => {
18+
if (!nodes.length) { return [] }
19+
const slots = nodes.filter(n => n.tag === 'slot').map(s => ({
20+
name: JSON.parse(s.codegenNode.arguments[1])
21+
}))
22+
return [
23+
...slots,
24+
...findSlots(nodes.flatMap(n => n.children || []))
25+
]
26+
}
27+
28+
return {
29+
slots: findSlots(template.ast?.children || [])
30+
}
31+
}

test/basic-component.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import fsp from 'fs/promises'
2+
import { fileURLToPath } from 'url'
3+
import { test, describe, expect } from 'vitest'
4+
import { parseComponent } from '../src/utils/parseComponent'
5+
6+
describe('Basic Component', async () => {
7+
const path = fileURLToPath(new URL('./fixtures/basic/components/BasicComponent.vue', import.meta.url))
8+
const source = await fsp.readFile(path, { encoding: 'utf-8' })
9+
// Parse component source
10+
const { props, slots } = parseComponent('BasicComponent', source)
11+
12+
test('Slots', () => {
13+
expect(slots).toEqual([
14+
{ name: 'default' },
15+
{ name: 'nuxt' }
16+
])
17+
})
18+
19+
test('Props', () => {
20+
expect(props).toBeDefined()
21+
expect(props.length > 0)
22+
})
23+
24+
test('String', () => {
25+
const stringProps = props.filter(p => p.type === 'String')
26+
27+
expect(stringProps.length).toBe(1)
28+
expect(stringProps[0].name).toBe('stringProp')
29+
expect(stringProps[0].default).toBe('Hello')
30+
})
31+
32+
test('Boolean', () => {
33+
const booleanProps = props.filter(p => p.type === 'Boolean')
34+
35+
expect(booleanProps.length).toBe(1)
36+
expect(booleanProps[0].name).toBe('booleanProp')
37+
expect(booleanProps[0].default).toBe(false)
38+
})
39+
40+
test('Number', () => {
41+
const numberProps = props.filter(p => p.type === 'Number')
42+
43+
expect(numberProps.length).toBe(1)
44+
expect(numberProps[0].name).toBe('numberProp')
45+
expect(numberProps[0].default).toBe(1.3)
46+
})
47+
48+
test('Array', () => {
49+
const arrayProps = props.filter(p => p.type === 'Array')
50+
51+
expect(arrayProps.length).toBe(1)
52+
expect(arrayProps[0].name).toBe('arrayProp')
53+
})
54+
55+
test('Object', () => {
56+
const objectProps = props.filter(p => p.type === 'Object')
57+
58+
expect(objectProps.length).toBe(1)
59+
expect(objectProps[0].name).toBe('objectProp')
60+
})
61+
})
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<template>
2+
<div>
3+
<slot />
4+
<hr>
5+
<slot name="nuxt" />
6+
</div>
7+
</template>
8+
9+
<script setup>
10+
const props = defineProps({
11+
stringProp: {
12+
type: String,
13+
default: 'Hello'
14+
},
15+
booleanProp: {
16+
type: Boolean,
17+
default: false
18+
},
19+
numberProp: {
20+
type: Number,
21+
default: 1.3
22+
},
23+
arrayProp: {
24+
type: Array,
25+
default: () => []
26+
},
27+
objectProp: {
28+
type: Object,
29+
default: () => ({})
30+
}
31+
})
32+
const emit = defineEmits(['change', 'delete'])
33+
</script>

0 commit comments

Comments
 (0)