]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(sfc): support using declared interface or type alias with defineProps()
authorEvan You <yyx990803@gmail.com>
Mon, 28 Jun 2021 15:39:24 +0000 (11:39 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 28 Jun 2021 19:30:28 +0000 (15:30 -0400)
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts

index b1d54478b3beb50b9320c70453cbd858a081bdc6..9553ab1250e8f5609a4261726b3a3b756d86af23 100644 (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`] = `
 "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 {  }
 }
 
index 52272546ff4bd41a369637256df77e9e36477ee5..6f91fa872385bea5df6cdc9fc733befa7d0a1346 100644 (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)', () => {
       const { content, bindings } = compile(`
       <script setup lang="ts">
index 014a0afbfb491fb35ed0adf7b2270545c30cdd9d..b7bb09de36484ace63876c7b7984f9948c3bac94 100644 (file)
@@ -21,7 +21,8 @@ import {
   Expression,
   LabeledStatement,
   CallExpression,
-  RestElement
+  RestElement,
+  TSInterfaceBody
 } from '@babel/types'
 import { walk } from 'estree-walker'
 import { RawSourceMap } from 'source-map'
@@ -195,7 +196,7 @@ export function compileScript(
   let hasDefineExposeCall = false
   let propsRuntimeDecl: Node | undefined
   let propsRuntimeDefaults: Node | undefined
-  let propsTypeDecl: TSTypeLiteral | undefined
+  let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
   let propsIdentifier: string | undefined
   let emitRuntimeDecl: Node | 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') {
         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(
-          `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
         )
       }
@@ -661,7 +696,6 @@ export function compileScript(
   for (const node of scriptSetupAst) {
     const start = node.start! + startOffset
     let end = node.end! + startOffset
-    // import or type declarations: move to top
     // locate comment
     if (node.trailingComments && node.trailingComments.length > 0) {
       const lastCommentNode =
@@ -1315,11 +1349,12 @@ function recordType(node: Node, declaredTypes: Record<string, string[]>) {
 }
 
 function extractRuntimeProps(
-  node: TSTypeLiteral,
+  node: TSTypeLiteral | TSInterfaceBody,
   props: Record<string, PropTypeData>,
   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') {
       props[m.key.name] = {
         key: m.key.name,