Skip to content

Commit 8fca9e4

Browse files
authored
feat: parse normal scripts with defineComponent (#28)
1 parent dd85789 commit 8fca9e4

File tree

8 files changed

+239
-65
lines changed

8 files changed

+239
-65
lines changed

src/types.d.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
export interface ComponentPropType {
2-
type: string
2+
type: string | string[]
33
elementType?: string
4+
as?: string | ComponentPropType
45
}
56

67
export interface ComponentProp {
7-
name: string
8-
type?: string | ComponentPropType,
9-
default?: any
10-
required?: boolean,
11-
values?: any,
12-
description?: string
8+
name: string
9+
type?: string | ComponentPropType | string[],
10+
default?: any
11+
required?: boolean,
12+
values?: any,
13+
description?: string
1314
}

src/utils/ast.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,68 @@ export function visit (node, test, visitNode) {
3838
case 'TSTypeLiteral':
3939
visit(node.members, test, visitNode)
4040
break
41+
case 'ExportDefaultDeclaration':
42+
visit(node.declaration, test, visitNode)
43+
break
44+
}
45+
}
46+
47+
export function getValue (prop) {
48+
if (!prop?.type) {
49+
return undefined
50+
}
51+
if (prop.type.endsWith('Literal')) {
52+
return prop.value
53+
}
54+
55+
if (prop.type === 'Identifier') {
56+
return prop.name === 'undefined' ? undefined : prop.name
57+
}
58+
59+
if (prop.type === 'TSAsExpression') {
60+
return {
61+
type: getValue(prop.expression),
62+
as: getType(prop.typeAnnotation)
63+
}
64+
}
65+
if (prop.type === 'ArrayExpression') {
66+
return prop.elements.map(getValue)
67+
}
68+
69+
if (prop.type === 'ObjectExpression') {
70+
return prop.properties.reduce((acc, prop) => {
71+
acc[prop.key.name] = getValue(prop.value)
72+
return acc
73+
}, {})
74+
}
75+
}
76+
77+
export function getType (tsProperty) {
78+
const { type, typeName, elementType, typeParameters } = tsProperty.typeAnnotation?.typeAnnotation || tsProperty
79+
switch (type) {
80+
case 'TSStringKeyword':
81+
return 'String'
82+
case 'TSNumberKeyword':
83+
return 'Number'
84+
case 'TSBooleanKeyword':
85+
return 'Boolean'
86+
case 'TSObjectKeyword':
87+
return 'Object'
88+
case 'TSTypeReference':
89+
if (typeParameters?.params) {
90+
if (typeName.name === 'PropType') {
91+
return getType(typeParameters.params[0])
92+
}
93+
return {
94+
type: typeName.name,
95+
elementType: typeParameters.params.map(type => getType(type))
96+
}
97+
}
98+
return typeName.name
99+
case 'TSArrayType':
100+
return {
101+
type: 'Array',
102+
elementType: getType(elementType)
103+
}
41104
}
42105
}

src/utils/parseComponent.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import { parse } from '@vue/compiler-sfc'
2+
import type { ComponentProp } from '../types'
23
import { parseSetupScript } from './parseSetupScript'
4+
import { parseScript } from './parseScript'
35
import { parseTemplate } from './parseTemplate'
46

57
export function parseComponent (name: string, source: string) {
68
// Parse component source
79
const { descriptor } = parse(source)
10+
let props: ComponentProp[] = []
811

912
// Parse script
10-
const { props } = descriptor.scriptSetup
11-
? parseSetupScript(name, descriptor)
12-
: { props: [] }
13+
if (descriptor.scriptSetup) {
14+
const setupScrip = parseSetupScript(name, descriptor)
15+
props = setupScrip.props
16+
} else if (descriptor.script) {
17+
const script = parseScript(name, descriptor)
18+
props = script.props
19+
}
1320

1421
const { slots } = parseTemplate(name, descriptor)
1522

src/utils/parseScript.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { SFCDescriptor } from '@vue/compiler-sfc'
2+
import { compileScript } from '@vue/compiler-sfc'
3+
import { ComponentProp } from '../types'
4+
import { getType, getValue, visit } from './ast'
5+
6+
export function parseScript (id: string, descriptor: SFCDescriptor) {
7+
const props: ComponentProp[] = []
8+
const script = compileScript(descriptor, { id })
9+
10+
visit(script.scriptAst, node => node.type === 'CallExpression' && node.callee?.name === 'defineComponent', (node) => {
11+
const nodeProps = node.arguments?.[0]?.properties?.find(prop => prop.key.name === 'props')
12+
if (!nodeProps) {
13+
return
14+
}
15+
const properties = nodeProps.value.properties || []
16+
properties.reduce((props, p) => {
17+
if (p.type === 'ObjectProperty') {
18+
props.push({
19+
name: p.key.name,
20+
...getValue(p.value)
21+
})
22+
}
23+
return props
24+
}, props)
25+
visit(node, n => n.type === 'TSPropertySignature', (property) => {
26+
const name = property.key.name
27+
props.push({
28+
name,
29+
required: !property.optional,
30+
type: getType(property)
31+
})
32+
})
33+
})
34+
35+
return {
36+
props
37+
}
38+
}

src/utils/parseSetupScript.ts

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,12 @@
11
import type { SFCDescriptor } from '@vue/compiler-sfc'
22
import { compileScript } from '@vue/compiler-sfc'
33
import { ComponentProp } from '../types'
4-
import { visit } from './ast'
4+
import { getType, getValue, visit } from './ast'
55

66
export function parseSetupScript (id: string, descriptor: SFCDescriptor) {
77
const props: ComponentProp[] = []
88
const script = compileScript(descriptor, { id })
99

10-
function getValue (prop) {
11-
if (!prop?.type) {
12-
return undefined
13-
}
14-
if (prop.type.endsWith('Literal')) {
15-
return prop.value
16-
}
17-
18-
if (prop.type === 'Identifier') {
19-
return prop.name
20-
}
21-
22-
if (prop.type === 'TSAsExpression') {
23-
return getType(prop.typeAnnotation)
24-
}
25-
26-
if (prop.type === 'ObjectExpression') {
27-
return prop.properties.reduce((acc, prop) => {
28-
acc[prop.key.name] = getValue(prop.value)
29-
return acc
30-
}, {})
31-
}
32-
}
33-
function getType (tsProperty) {
34-
const { type, typeName, elementType, typeParameters } = tsProperty.typeAnnotation?.typeAnnotation || tsProperty
35-
switch (type) {
36-
case 'TSStringKeyword':
37-
return 'String'
38-
case 'TSNumberKeyword':
39-
return 'Number'
40-
case 'TSBooleanKeyword':
41-
return 'Boolean'
42-
case 'TSObjectKeyword':
43-
return 'Object'
44-
case 'TSTypeReference':
45-
if (typeParameters?.params) {
46-
if (typeName.name === 'PropType') {
47-
return getType(typeParameters.params[0])
48-
}
49-
return {
50-
type: typeName.name,
51-
elementType: typeParameters.params.map(type => getType(type))
52-
}
53-
}
54-
return typeName.name
55-
case 'TSArrayType':
56-
return {
57-
type: 'Array',
58-
elementType: getType(elementType)
59-
}
60-
}
61-
}
62-
6310
visit(script.scriptSetupAst, node => node.type === 'CallExpression' && node.callee?.name === 'defineProps', (node) => {
6411
const properties = node.arguments[0]?.properties || []
6512
properties.reduce((props, p) => {

test/basic-component.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ describe('Basic Component', async () => {
5858

5959
expect(typedArrayProps.length).toBe(1)
6060
expect(typedArrayProps[0].name).toBe('typedArrayProps')
61-
expect((typedArrayProps[0].type as ComponentPropType).elementType).toBe('String')
61+
expect(((typedArrayProps[0].type as ComponentPropType)?.as as ComponentPropType)?.type).toBe('Array')
62+
expect(((typedArrayProps[0].type as ComponentPropType)?.as as ComponentPropType)?.elementType).toBe('String')
6263
})
6364

6465
test('Object', () => {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<script lang="ts">
2+
import type { PropType } from 'vue'
3+
// @ts-ignore
4+
import { computed, defineComponent, h } from '#imports'
5+
6+
type NuxtImg = string & {
7+
light: string
8+
dark: string
9+
}
10+
11+
export default defineComponent({
12+
props: {
13+
src: {
14+
type: [String, Object] as PropType<NuxtImg>,
15+
default: null
16+
},
17+
alt: {
18+
type: String,
19+
default: ''
20+
},
21+
width: {
22+
type: [String, Number],
23+
default: undefined
24+
},
25+
height: {
26+
type: [String, Number],
27+
default: undefined
28+
}
29+
},
30+
setup (props) {
31+
const imgSrc = computed(() => {
32+
let src = props.src
33+
34+
try {
35+
src = JSON.parse(src as any)
36+
} catch (e) {
37+
src = props.src
38+
}
39+
40+
if (typeof src === 'string') { return props.src }
41+
42+
return src
43+
})
44+
45+
return {
46+
imgSrc
47+
}
48+
},
49+
render ({ imgSrc }) {
50+
// String as `src`; return a single image
51+
if (typeof imgSrc === 'string') {
52+
return h('img', { src: imgSrc })
53+
}
54+
55+
// Object as `src`; return a light and dark image if present
56+
const nodes: any[] = []
57+
if (imgSrc.light) {
58+
nodes.push(h('img', { src: imgSrc.light, class: ['dark-img'] }))
59+
}
60+
if (imgSrc.dark) {
61+
nodes.push(h('img', { src: imgSrc.dark, class: ['light-img'] }))
62+
}
63+
64+
return nodes
65+
}
66+
})
67+
</script>

test/normarl-script.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import fsp from 'fs/promises'
2+
import { fileURLToPath } from 'url'
3+
import { test, describe, expect } from 'vitest'
4+
import { ComponentProp, ComponentPropType } from '../src/types'
5+
import { parseComponent } from '../src/utils/parseComponent'
6+
7+
describe('Basic Component', async () => {
8+
const path = fileURLToPath(new URL('./fixtures/basic/components/NormalScript.vue', import.meta.url))
9+
const source = await fsp.readFile(path, { encoding: 'utf-8' })
10+
// Parse component source
11+
const { props, slots } = parseComponent('NormalScript', source)
12+
13+
test('slots', () => {
14+
expect(slots).toEqual([])
15+
})
16+
17+
test('props', () => {
18+
expect(props).toBeDefined()
19+
expect(props.length > 0)
20+
})
21+
22+
test('props:src', () => {
23+
const prop = props.find(prop => prop.name === 'src') as ComponentProp
24+
expect(prop).toBeDefined()
25+
expect((prop.type as ComponentPropType)?.type).toEqual(['String', 'Object'])
26+
expect((prop.type as ComponentPropType)?.as).toEqual('NuxtImg')
27+
expect(prop.default).toEqual(undefined)
28+
})
29+
30+
test('props:alt', () => {
31+
const prop = props.find(prop => prop.name === 'alt') as ComponentProp
32+
expect(prop).toBeDefined()
33+
expect(prop.type).toEqual('String')
34+
expect(prop.default).toEqual('')
35+
})
36+
37+
test('props:width', () => {
38+
const prop = props.find(prop => prop.name === 'width') as ComponentProp
39+
expect(prop).toBeDefined()
40+
expect(prop.type).toEqual(['String', 'Number'])
41+
expect(prop.default).toEqual(undefined)
42+
})
43+
44+
test('props:height', () => {
45+
const prop = props.find(prop => prop.name === 'height') as ComponentProp
46+
expect(prop).toBeDefined()
47+
expect(prop.type).toEqual(['String', 'Number'])
48+
expect(prop.default).toEqual(undefined)
49+
})
50+
})

0 commit comments

Comments
 (0)