Skip to content

Commit

Permalink
fix(compiler-sfc): support using extends interface with defineProps() (
Browse files Browse the repository at this point in the history
  • Loading branch information
edison1105 committed Oct 26, 2022
1 parent 183e4e6 commit 83f7e6f
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 20 deletions.
Expand Up @@ -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 { }
}

Expand Down
25 changes: 25 additions & 0 deletions packages/compiler-sfc/__tests__/compileScript.spec.ts
Expand Up @@ -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', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
Expand Down
109 changes: 89 additions & 20 deletions packages/compiler-sfc/src/compileScript.ts
Expand Up @@ -36,6 +36,7 @@ import {
CallExpression,
RestElement,
TSInterfaceBody,
TSTypeElement,
AwaitExpression,
Program,
ObjectMethod,
Expand Down Expand Up @@ -558,6 +559,82 @@ export function compileScript(
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(
node: Node,
qualifier: (node: Node) => boolean
Expand All @@ -570,28 +647,20 @@ export function compileScript(
node.typeName.type === 'Identifier'
) {
const refName = node.typeName.name
const isQualifiedType = (node: Node): 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)
}
}
const body = scriptAst
? [...scriptSetupAst.body, ...scriptAst.body]
: scriptSetupAst.body
const body = getAstBody()
for (const node of body) {
const qualified = isQualifiedType(node)
let qualified = isQualifiedType(
node,
qualifier,
refName
) as TSInterfaceBody
if (qualified) {
const extendsTypes = resolveExtendsType(node, qualifier)
if (extendsTypes.length) {
const bodies: TSTypeElement[] = [...qualified.body]
filterExtendsType(extendsTypes, bodies)
qualified.body = bodies
}
return qualified
}
}
Expand Down

0 comments on commit 83f7e6f

Please sign in to comment.