mirror of https://github.com/vuejs/core.git
fix(compiler-sfc): support using extends interface with defineProps() (#4512)
fix #4498
This commit is contained in:
parent
183e4e6152
commit
83f7e6f8a6
|
|
@ -1467,6 +1467,33 @@ export default /*#__PURE__*/_defineComponent({
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return { }
|
||||||
|
}
|
||||||
|
|
||||||
|
})"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SFC compile <script setup> with TypeScript defineProps w/ extends interface 1`] = `
|
||||||
|
"import { defineComponent as _defineComponent } from 'vue'
|
||||||
|
interface Bar extends Foo { y?: number }
|
||||||
|
interface Props extends Bar {
|
||||||
|
z: number
|
||||||
|
y: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Foo { x?: number }
|
||||||
|
|
||||||
|
export default /*#__PURE__*/_defineComponent({
|
||||||
|
props: {
|
||||||
|
z: { type: Number, required: true },
|
||||||
|
y: { type: String, required: true },
|
||||||
|
x: { type: Number, required: false }
|
||||||
|
},
|
||||||
|
setup(__props: any, { expose }) {
|
||||||
|
expose()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return { }
|
return { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -885,6 +885,31 @@ const emit = defineEmits(['a', 'b'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('defineProps w/ extends interface', () => {
|
||||||
|
const { content, bindings } = compile(`
|
||||||
|
<script lang="ts">
|
||||||
|
interface Foo { x?: number }
|
||||||
|
</script>
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Bar extends Foo { y?: number }
|
||||||
|
interface Props extends Bar {
|
||||||
|
z: number
|
||||||
|
y: string
|
||||||
|
}
|
||||||
|
defineProps<Props>()
|
||||||
|
</script>
|
||||||
|
`)
|
||||||
|
assertCode(content)
|
||||||
|
expect(content).toMatch(`z: { type: Number, required: true }`)
|
||||||
|
expect(content).toMatch(`y: { type: String, required: true }`)
|
||||||
|
expect(content).toMatch(`x: { type: Number, required: false }`)
|
||||||
|
expect(bindings).toStrictEqual({
|
||||||
|
x: BindingTypes.PROPS,
|
||||||
|
y: BindingTypes.PROPS,
|
||||||
|
z: BindingTypes.PROPS
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('defineProps w/ exported interface', () => {
|
test('defineProps w/ exported interface', () => {
|
||||||
const { content, bindings } = compile(`
|
const { content, bindings } = compile(`
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import {
|
||||||
CallExpression,
|
CallExpression,
|
||||||
RestElement,
|
RestElement,
|
||||||
TSInterfaceBody,
|
TSInterfaceBody,
|
||||||
|
TSTypeElement,
|
||||||
AwaitExpression,
|
AwaitExpression,
|
||||||
Program,
|
Program,
|
||||||
ObjectMethod,
|
ObjectMethod,
|
||||||
|
|
@ -558,6 +559,82 @@ export function compileScript(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAstBody(): Statement[] {
|
||||||
|
return scriptAst
|
||||||
|
? [...scriptSetupAst.body, ...scriptAst.body]
|
||||||
|
: scriptSetupAst.body
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveExtendsType(
|
||||||
|
node: Node,
|
||||||
|
qualifier: (node: Node) => boolean,
|
||||||
|
cache: Array<Node> = []
|
||||||
|
): Array<Node> {
|
||||||
|
if (node.type === 'TSInterfaceDeclaration' && node.extends) {
|
||||||
|
node.extends.forEach(extend => {
|
||||||
|
if (
|
||||||
|
extend.type === 'TSExpressionWithTypeArguments' &&
|
||||||
|
extend.expression.type === 'Identifier'
|
||||||
|
) {
|
||||||
|
const body = getAstBody()
|
||||||
|
for (const node of body) {
|
||||||
|
const qualified = isQualifiedType(
|
||||||
|
node,
|
||||||
|
qualifier,
|
||||||
|
extend.expression.name
|
||||||
|
)
|
||||||
|
if (qualified) {
|
||||||
|
cache.push(qualified)
|
||||||
|
resolveExtendsType(node, qualifier, cache)
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
function isQualifiedType(
|
||||||
|
node: Node,
|
||||||
|
qualifier: (node: Node) => boolean,
|
||||||
|
refName: String
|
||||||
|
): Node | undefined {
|
||||||
|
if (node.type === 'TSInterfaceDeclaration' && node.id.name === refName) {
|
||||||
|
return node.body
|
||||||
|
} else if (
|
||||||
|
node.type === 'TSTypeAliasDeclaration' &&
|
||||||
|
node.id.name === refName &&
|
||||||
|
qualifier(node.typeAnnotation)
|
||||||
|
) {
|
||||||
|
return node.typeAnnotation
|
||||||
|
} else if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
||||||
|
return isQualifiedType(node.declaration, qualifier, refName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter all extends types to keep the override declaration
|
||||||
|
function filterExtendsType(extendsTypes: Node[], bodies: TSTypeElement[]) {
|
||||||
|
extendsTypes.forEach(extend => {
|
||||||
|
const body = (extend as TSInterfaceBody).body
|
||||||
|
body.forEach(newBody => {
|
||||||
|
if (
|
||||||
|
newBody.type === 'TSPropertySignature' &&
|
||||||
|
newBody.key.type === 'Identifier'
|
||||||
|
) {
|
||||||
|
const name = newBody.key.name
|
||||||
|
const hasOverride = bodies.some(
|
||||||
|
seenBody =>
|
||||||
|
seenBody.type === 'TSPropertySignature' &&
|
||||||
|
seenBody.key.type === 'Identifier' &&
|
||||||
|
seenBody.key.name === name
|
||||||
|
)
|
||||||
|
if (!hasOverride) bodies.push(newBody)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function resolveQualifiedType(
|
function resolveQualifiedType(
|
||||||
node: Node,
|
node: Node,
|
||||||
qualifier: (node: Node) => boolean
|
qualifier: (node: Node) => boolean
|
||||||
|
|
@ -570,28 +647,20 @@ export function compileScript(
|
||||||
node.typeName.type === 'Identifier'
|
node.typeName.type === 'Identifier'
|
||||||
) {
|
) {
|
||||||
const refName = node.typeName.name
|
const refName = node.typeName.name
|
||||||
const isQualifiedType = (node: Node): Node | undefined => {
|
const body = getAstBody()
|
||||||
if (
|
|
||||||
node.type === 'TSInterfaceDeclaration' &&
|
|
||||||
node.id.name === refName
|
|
||||||
) {
|
|
||||||
return node.body
|
|
||||||
} else if (
|
|
||||||
node.type === 'TSTypeAliasDeclaration' &&
|
|
||||||
node.id.name === refName &&
|
|
||||||
qualifier(node.typeAnnotation)
|
|
||||||
) {
|
|
||||||
return node.typeAnnotation
|
|
||||||
} else if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
||||||
return isQualifiedType(node.declaration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const body = scriptAst
|
|
||||||
? [...scriptSetupAst.body, ...scriptAst.body]
|
|
||||||
: scriptSetupAst.body
|
|
||||||
for (const node of body) {
|
for (const node of body) {
|
||||||
const qualified = isQualifiedType(node)
|
let qualified = isQualifiedType(
|
||||||
|
node,
|
||||||
|
qualifier,
|
||||||
|
refName
|
||||||
|
) as TSInterfaceBody
|
||||||
if (qualified) {
|
if (qualified) {
|
||||||
|
const extendsTypes = resolveExtendsType(node, qualifier)
|
||||||
|
if (extendsTypes.length) {
|
||||||
|
const bodies: TSTypeElement[] = [...qualified.body]
|
||||||
|
filterExtendsType(extendsTypes, bodies)
|
||||||
|
qualified.body = bodies
|
||||||
|
}
|
||||||
return qualified
|
return qualified
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue