feat(sfc): support using declared interface or type alias with defineProps()

This commit is contained in:
Evan You 2021-06-28 11:39:24 -04:00
parent d069796b8f
commit 2f91db30cd
3 changed files with 175 additions and 8 deletions

View File

@ -780,6 +780,63 @@ return { emit }
})" })"
`; `;
exports[`SFC compile <script setup> with TypeScript defineProps w/ exported interface 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export interface Props { x?: number }
export default _defineComponent({
props: {
x: { type: Number, required: false }
} as unknown as undefined,
setup(__props: { x?: number }, { expose }) {
expose()
return { }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript defineProps w/ exported type alias 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export type Props = { x?: number }
export default _defineComponent({
props: {
x: { type: Number, required: false }
} as unknown as undefined,
setup(__props: { x?: number }, { expose }) {
expose()
return { }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript defineProps w/ interface 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
interface Props { x?: number }
export default _defineComponent({
props: {
x: { type: Number, required: false }
} as unknown as undefined,
setup(__props: { x?: number }, { expose }) {
expose()
return { }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript defineProps w/ type 1`] = ` exports[`SFC compile <script setup> with TypeScript defineProps w/ type 1`] = `
"import { defineComponent as _defineComponent } from 'vue' "import { defineComponent as _defineComponent } from 'vue'
@ -840,6 +897,25 @@ export default _defineComponent({
return { }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript defineProps w/ type alias 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
type Props = { x?: number }
export default _defineComponent({
props: {
x: { type: Number, required: false }
} as unknown as undefined,
setup(__props: { x?: number }, { expose }) {
expose()
return { } return { }
} }

View File

@ -592,6 +592,62 @@ const emit = defineEmits(['a', 'b'])
}) })
}) })
test('defineProps w/ interface', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
interface Props { x?: number }
defineProps<Props>()
</script>
`)
assertCode(content)
expect(content).toMatch(`x: { type: Number, required: false }`)
expect(bindings).toStrictEqual({
x: BindingTypes.PROPS
})
})
test('defineProps w/ exported interface', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
export interface Props { x?: number }
defineProps<Props>()
</script>
`)
assertCode(content)
expect(content).toMatch(`x: { type: Number, required: false }`)
expect(bindings).toStrictEqual({
x: BindingTypes.PROPS
})
})
test('defineProps w/ type alias', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
type Props = { x?: number }
defineProps<Props>()
</script>
`)
assertCode(content)
expect(content).toMatch(`x: { type: Number, required: false }`)
expect(bindings).toStrictEqual({
x: BindingTypes.PROPS
})
})
test('defineProps w/ exported type alias', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
export type Props = { x?: number }
defineProps<Props>()
</script>
`)
assertCode(content)
expect(content).toMatch(`x: { type: Number, required: false }`)
expect(bindings).toStrictEqual({
x: BindingTypes.PROPS
})
})
test('withDefaults (static)', () => { test('withDefaults (static)', () => {
const { content, bindings } = compile(` const { content, bindings } = compile(`
<script setup lang="ts"> <script setup lang="ts">

View File

@ -21,7 +21,8 @@ import {
Expression, Expression,
LabeledStatement, LabeledStatement,
CallExpression, CallExpression,
RestElement RestElement,
TSInterfaceBody
} from '@babel/types' } from '@babel/types'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map' import { RawSourceMap } from 'source-map'
@ -195,7 +196,7 @@ export function compileScript(
let hasDefineExposeCall = false let hasDefineExposeCall = false
let propsRuntimeDecl: Node | undefined let propsRuntimeDecl: Node | undefined
let propsRuntimeDefaults: Node | undefined let propsRuntimeDefaults: Node | undefined
let propsTypeDecl: TSTypeLiteral | undefined let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
let propsIdentifier: string | undefined let propsIdentifier: string | undefined
let emitRuntimeDecl: Node | undefined let emitRuntimeDecl: Node | undefined
let emitTypeDecl: TSFunctionType | TSTypeLiteral | undefined let emitTypeDecl: TSFunctionType | TSTypeLiteral | undefined
@ -287,12 +288,46 @@ export function compileScript(
) )
} }
const typeArg = node.typeParameters.params[0] let typeArg: Node = node.typeParameters.params[0]
if (typeArg.type === 'TSTypeLiteral') { if (typeArg.type === 'TSTypeLiteral') {
propsTypeDecl = typeArg propsTypeDecl = typeArg
} else { } else if (
typeArg.type === 'TSTypeReference' &&
typeArg.typeName.type === 'Identifier'
) {
const refName = typeArg.typeName.name
const isValidType = (node: Node): boolean => {
if (
node.type === 'TSInterfaceDeclaration' &&
node.id.name === refName
) {
propsTypeDecl = node.body
return true
} else if (
node.type === 'TSTypeAliasDeclaration' &&
node.id.name === refName &&
node.typeAnnotation.type === 'TSTypeLiteral'
) {
propsTypeDecl = node.typeAnnotation
return true
} else if (
node.type === 'ExportNamedDeclaration' &&
node.declaration
) {
return isValidType(node.declaration)
}
return false
}
for (const node of scriptSetupAst) {
if (isValidType(node)) break
}
}
if (!propsTypeDecl) {
error( error(
`type argument passed to ${DEFINE_PROPS}() must be a literal type.`, `type argument passed to ${DEFINE_PROPS}() must be a literal type, ` +
`or a reference to a interface or literal type.`,
typeArg typeArg
) )
} }
@ -661,7 +696,6 @@ export function compileScript(
for (const node of scriptSetupAst) { for (const node of scriptSetupAst) {
const start = node.start! + startOffset const start = node.start! + startOffset
let end = node.end! + startOffset let end = node.end! + startOffset
// import or type declarations: move to top
// locate comment // locate comment
if (node.trailingComments && node.trailingComments.length > 0) { if (node.trailingComments && node.trailingComments.length > 0) {
const lastCommentNode = const lastCommentNode =
@ -1315,11 +1349,12 @@ function recordType(node: Node, declaredTypes: Record<string, string[]>) {
} }
function extractRuntimeProps( function extractRuntimeProps(
node: TSTypeLiteral, node: TSTypeLiteral | TSInterfaceBody,
props: Record<string, PropTypeData>, props: Record<string, PropTypeData>,
declaredTypes: Record<string, string[]> declaredTypes: Record<string, string[]>
) { ) {
for (const m of node.members) { const members = node.type === 'TSTypeLiteral' ? node.members : node.body
for (const m of members) {
if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') { if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
props[m.key.name] = { props[m.key.name] = {
key: m.key.name, key: m.key.name,