]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(sfc): support referenced types for defineEmits
authorEvan You <yyx990803@gmail.com>
Mon, 28 Jun 2021 20:03:27 +0000 (16:03 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 28 Jun 2021 20:03:27 +0000 (16:03 -0400)
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts

index 2ff89f5ff66a3cfb70c547a877c2e7560f030c20..4164124a2b36af6929f23ba4ba1e2ed00a552bf5 100644 (file)
@@ -746,10 +746,111 @@ return { a, b, c, d, x }
 }"
 `;
 
-exports[`SFC compile <script setup> with TypeScript defineEmits w/ type (type literal w/ call signatures) 1`] = `
+exports[`SFC compile <script setup> with TypeScript defineEmits w/ type (exported interface) 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+export interface Emits { (e: 'foo' | 'bar'): void }
+      
+export default _defineComponent({
+  emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
+  setup(__props, { expose, emit }: { emit: ({ (e: 'foo' | 'bar'): void }), expose: any, slots: any, attrs: any }) {
+  expose()
+
+      
+      
+return { emit }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> with TypeScript defineEmits w/ type (exported type alias) 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+export type Emits = { (e: 'foo' | 'bar'): void }
+      
+export default _defineComponent({
+  emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
+  setup(__props, { expose, emit }: { emit: ({ (e: 'foo' | 'bar'): void }), expose: any, slots: any, attrs: any }) {
+  expose()
+
+      
+      
+return { emit }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> with TypeScript defineEmits w/ type (interface) 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+interface Emits { (e: 'foo' | 'bar'): void }
+      
+export default _defineComponent({
+  emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
+  setup(__props, { expose, emit }: { emit: ({ (e: 'foo' | 'bar'): void }), expose: any, slots: any, attrs: any }) {
+  expose()
+
+      
+      
+return { emit }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> with TypeScript defineEmits w/ type (referenced exported function type) 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+export type Emits = (e: 'foo' | 'bar') => void
+      
+export default _defineComponent({
+  emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
+  setup(__props, { expose, emit }: { emit: ((e: 'foo' | 'bar') => void), expose: any, slots: any, attrs: any }) {
+  expose()
+
+      
+      
+return { emit }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> with TypeScript defineEmits w/ type (referenced function type) 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
+type Emits = (e: 'foo' | 'bar') => void
+      
+export default _defineComponent({
+  emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
+  setup(__props, { expose, emit }: { emit: ((e: 'foo' | 'bar') => void), expose: any, slots: any, attrs: any }) {
+  expose()
 
       
+      
+return { emit }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> with TypeScript defineEmits w/ type (type alias) 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+type Emits = { (e: 'foo' | 'bar'): void }
+      
+export default _defineComponent({
+  emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
+  setup(__props, { expose, emit }: { emit: ({ (e: 'foo' | 'bar'): void }), expose: any, slots: any, attrs: any }) {
+  expose()
+
+      
+      
+return { emit }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> with TypeScript defineEmits w/ type (type literal w/ call signatures) 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
 export default _defineComponent({
   emits: [\\"foo\\", \\"bar\\", \\"baz\\"] as unknown as undefined,
   setup(__props, { expose, emit }: { emit: ({(e: 'foo' | 'bar'): void; (e: 'baz', id: number): void;}), expose: any, slots: any, attrs: any }) {
@@ -766,7 +867,6 @@ return { emit }
 exports[`SFC compile <script setup> with TypeScript defineEmits w/ type 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
 
-      
 export default _defineComponent({
   emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
   setup(__props, { expose, emit }: { emit: ((e: 'foo' | 'bar') => void), expose: any, slots: any, attrs: any }) {
@@ -839,8 +939,7 @@ return {  }
 
 exports[`SFC compile <script setup> with TypeScript defineProps w/ type 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
-
-      interface Test {}
+interface Test {}
 
       type Alias = number[]
 
@@ -927,7 +1026,6 @@ return {  }
 exports[`SFC compile <script setup> with TypeScript defineProps/Emit w/ runtime options 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
 
-
 export default _defineComponent({
   props: { foo: String },
   emits: ['a', 'b'],
index a33619dc3a3d3279b528d24b33a5ac1b4d2e07af..f59ee13fda34581cce4872c3882b6196fc782aa9 100644 (file)
@@ -19,7 +19,6 @@ describe('SFC compile <script setup>', () => {
   test('defineProps()', () => {
     const { content, bindings } = compile(`
 <script setup>
-import { defineProps } from 'vue'
 const props = defineProps({
   foo: String
 })
@@ -51,7 +50,6 @@ const bar = 1
   test('defineProps w/ external definition', () => {
     const { content } = compile(`
     <script setup>
-    import { defineProps } from 'vue'
     import { propsModel } from './props'
     const props = defineProps(propsModel)
     </script>
@@ -64,7 +62,6 @@ const bar = 1
   test('defineEmit() (deprecated)', () => {
     const { content, bindings } = compile(`
 <script setup>
-import { defineEmit } from 'vue'
 const myEmit = defineEmit(['foo', 'bar'])
 </script>
   `)
@@ -84,7 +81,6 @@ const myEmit = defineEmit(['foo', 'bar'])
   test('defineEmits()', () => {
     const { content, bindings } = compile(`
 <script setup>
-import { defineEmits } from 'vue'
 const myEmit = defineEmits(['foo', 'bar'])
 </script>
   `)
@@ -104,7 +100,6 @@ const myEmit = defineEmits(['foo', 'bar'])
   test('defineExpose()', () => {
     const { content } = compile(`
 <script setup>
-import { defineExpose } from 'vue'
 defineExpose({ foo: 123 })
 </script>
   `)
@@ -170,7 +165,7 @@ defineExpose({ foo: 123 })
     test('should allow defineProps/Emit at the start of imports', () => {
       assertCode(
         compile(`<script setup>
-      import { defineProps, defineEmits, ref } from 'vue'
+      import { ref } from 'vue'
       defineProps(['foo'])
       defineEmits(['bar'])
       const r = ref(0)
@@ -233,7 +228,6 @@ defineExpose({ foo: 123 })
       const { content } = compile(
         `
         <script setup>
-        import { defineExpose } from 'vue'
         const count = ref(0)
         defineExpose({ count })
         </script>
@@ -494,7 +488,6 @@ defineExpose({ foo: 123 })
     test('defineProps/Emit w/ runtime options', () => {
       const { content } = compile(`
 <script setup lang="ts">
-import { defineProps, defineEmits } from 'vue'
 const props = defineProps({ foo: String })
 const emit = defineEmits(['a', 'b'])
 </script>
@@ -509,7 +502,6 @@ const emit = defineEmits(['a', 'b'])
     test('defineProps w/ type', () => {
       const { content, bindings } = compile(`
       <script setup lang="ts">
-      import { defineProps } from 'vue'
       interface Test {}
 
       type Alias = number[]
@@ -699,7 +691,6 @@ const emit = defineEmits(['a', 'b'])
     test('defineEmits w/ type', () => {
       const { content } = compile(`
       <script setup lang="ts">
-      import { defineEmits } from 'vue'
       const emit = defineEmits<(e: 'foo' | 'bar') => void>()
       </script>
       `)
@@ -713,7 +704,6 @@ const emit = defineEmits(['a', 'b'])
       expect(() =>
         compile(`
       <script setup lang="ts">
-      import { defineEmits } from 'vue'
       const emit = defineEmits<${type}>()
       </script>
       `)
@@ -724,7 +714,6 @@ const emit = defineEmits(['a', 'b'])
       const type = `{(e: 'foo' | 'bar'): void; (e: 'baz', id: number): void;}`
       const { content } = compile(`
       <script setup lang="ts">
-      import { defineEmits } from 'vue'
       const emit = defineEmits<${type}>()
       </script>
       `)
@@ -734,6 +723,78 @@ const emit = defineEmits(['a', 'b'])
         `emits: ["foo", "bar", "baz"] as unknown as undefined`
       )
     })
+
+    test('defineEmits w/ type (interface)', () => {
+      const { content } = compile(`
+      <script setup lang="ts">
+      interface Emits { (e: 'foo' | 'bar'): void }
+      const emit = defineEmits<Emits>()
+      </script>
+      `)
+      assertCode(content)
+      expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`)
+      expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
+    })
+
+    test('defineEmits w/ type (exported interface)', () => {
+      const { content } = compile(`
+      <script setup lang="ts">
+      export interface Emits { (e: 'foo' | 'bar'): void }
+      const emit = defineEmits<Emits>()
+      </script>
+      `)
+      assertCode(content)
+      expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`)
+      expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
+    })
+
+    test('defineEmits w/ type (type alias)', () => {
+      const { content } = compile(`
+      <script setup lang="ts">
+      type Emits = { (e: 'foo' | 'bar'): void }
+      const emit = defineEmits<Emits>()
+      </script>
+      `)
+      assertCode(content)
+      expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`)
+      expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
+    })
+
+    test('defineEmits w/ type (exported type alias)', () => {
+      const { content } = compile(`
+      <script setup lang="ts">
+      export type Emits = { (e: 'foo' | 'bar'): void }
+      const emit = defineEmits<Emits>()
+      </script>
+      `)
+      assertCode(content)
+      expect(content).toMatch(`emit: ({ (e: 'foo' | 'bar'): void }),`)
+      expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
+    })
+
+    test('defineEmits w/ type (referenced function type)', () => {
+      const { content } = compile(`
+      <script setup lang="ts">
+      type Emits = (e: 'foo' | 'bar') => void
+      const emit = defineEmits<Emits>()
+      </script>
+      `)
+      assertCode(content)
+      expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`)
+      expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
+    })
+
+    test('defineEmits w/ type (referenced exported function type)', () => {
+      const { content } = compile(`
+      <script setup lang="ts">
+      export type Emits = (e: 'foo' | 'bar') => void
+      const emit = defineEmits<Emits>()
+      </script>
+      `)
+      assertCode(content)
+      expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`)
+      expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
+    })
   })
 
   describe('async/await detection', () => {
@@ -1052,7 +1113,6 @@ const emit = defineEmits(['a', 'b'])
 
       expect(() => {
         compile(`<script setup lang="ts">
-        import { defineEmits } from 'vue'
         defineEmits<{}>({})
         </script>`)
       }).toThrow(`cannot accept both type and non-type arguments`)
@@ -1061,7 +1121,6 @@ const emit = defineEmits(['a', 'b'])
     test('defineProps/Emit() referencing local var', () => {
       expect(() =>
         compile(`<script setup>
-        import { defineProps } from 'vue'
         const bar = 1
         defineProps({
           foo: {
@@ -1073,7 +1132,6 @@ const emit = defineEmits(['a', 'b'])
 
       expect(() =>
         compile(`<script setup>
-        import { defineEmits } from 'vue'
         const bar = 'hello'
         defineEmits([bar])
         </script>`)
@@ -1083,7 +1141,6 @@ const emit = defineEmits(['a', 'b'])
     test('defineProps/Emit() referencing ref declarations', () => {
       expect(() =>
         compile(`<script setup>
-        import { defineProps } from 'vue'
         ref: bar = 1
         defineProps({
           bar
@@ -1093,7 +1150,6 @@ const emit = defineEmits(['a', 'b'])
 
       expect(() =>
         compile(`<script setup>
-        import { defineEmits } from 'vue'
         ref: bar = 1
         defineEmits({
           bar
@@ -1105,7 +1161,6 @@ const emit = defineEmits(['a', 'b'])
     test('should allow defineProps/Emit() referencing scope var', () => {
       assertCode(
         compile(`<script setup>
-          import { defineProps, defineEmits } from 'vue'
           const bar = 1
           defineProps({
             foo: {
@@ -1122,7 +1177,6 @@ const emit = defineEmits(['a', 'b'])
     test('should allow defineProps/Emit() referencing imported binding', () => {
       assertCode(
         compile(`<script setup>
-        import { defineProps, defineEmits } from 'vue'
         import { bar } from './bar'
         defineProps({
           foo: {
@@ -1361,7 +1415,7 @@ describe('SFC analyze <script> bindings', () => {
   it('works for script setup', () => {
     const { bindings } = compile(`
       <script setup>
-      import { defineProps, ref as r } from 'vue'
+      import { ref as r } from 'vue'
       defineProps({
         foo: String
       })
index c7d1667f7a63a6efcf87e67fde7b45739e64de2f..08f0c2d4db1b3a7b76e314637613f9246952c61d 100644 (file)
@@ -199,7 +199,7 @@ export function compileScript(
   let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
   let propsIdentifier: string | undefined
   let emitRuntimeDecl: Node | undefined
-  let emitTypeDecl: TSFunctionType | TSTypeLiteral | undefined
+  let emitTypeDecl: TSFunctionType | TSTypeLiteral | TSInterfaceBody | undefined
   let emitIdentifier: string | undefined
   let hasAwait = false
   let hasInlinedSsrRenderFn = false
@@ -288,47 +288,16 @@ export function compileScript(
         )
       }
 
-      let typeArg: Node = node.typeParameters.params[0]
-      if (typeArg.type === 'TSTypeLiteral') {
-        propsTypeDecl = typeArg
-      } 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
-        }
-      }
+      propsTypeDecl = resolveQualifiedType(
+        node.typeParameters.params[0],
+        node => node.type === 'TSTypeLiteral'
+      ) as TSTypeLiteral | TSInterfaceBody | undefined
 
       if (!propsTypeDecl) {
         error(
           `type argument passed to ${DEFINE_PROPS}() must be a literal type, ` +
-            `or a reference to a interface or literal type.`,
-          typeArg
+            `or a reference to an interface or literal type.`,
+          node.typeParameters.params[0]
         )
       }
     }
@@ -375,23 +344,61 @@ export function compileScript(
           node
         )
       }
-      const typeArg = node.typeParameters.params[0]
-      if (
-        typeArg.type === 'TSFunctionType' ||
-        typeArg.type === 'TSTypeLiteral'
-      ) {
-        emitTypeDecl = typeArg
-      } else {
+
+      emitTypeDecl = resolveQualifiedType(
+        node.typeParameters.params[0],
+        node => node.type === 'TSFunctionType' || node.type === 'TSTypeLiteral'
+      ) as TSFunctionType | TSTypeLiteral | TSInterfaceBody | undefined
+
+      if (!emitTypeDecl) {
         error(
-          `type argument passed to ${DEFINE_EMITS}() must be a function type ` +
-            `or a literal type with call signatures.`,
-          typeArg
+          `type argument passed to ${DEFINE_EMITS}() must be a function type, ` +
+            `a literal type with call signatures, or a reference to the above types.`,
+          node.typeParameters.params[0]
         )
       }
     }
     return true
   }
 
+  function resolveQualifiedType(
+    node: Node,
+    qualifier: (node: Node) => boolean
+  ) {
+    if (qualifier(node)) {
+      return node
+    }
+    if (
+      node.type === 'TSTypeReference' &&
+      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)
+        }
+      }
+
+      for (const node of scriptSetupAst) {
+        const qualified = isQualifiedType(node)
+        if (qualified) {
+          return qualified
+        }
+      }
+    }
+  }
+
   function processDefineExpose(node: Node): boolean {
     if (isCallOf(node, DEFINE_EXPOSE)) {
       if (hasDefineExposeCall) {
@@ -1469,11 +1476,12 @@ function toRuntimeTypeString(types: string[]) {
 }
 
 function extractRuntimeEmits(
-  node: TSFunctionType | TSTypeLiteral,
+  node: TSFunctionType | TSTypeLiteral | TSInterfaceBody,
   emits: Set<string>
 ) {
-  if (node.type === 'TSTypeLiteral') {
-    for (let t of node.members) {
+  if (node.type === 'TSTypeLiteral' || node.type === 'TSInterfaceBody') {
+    const members = node.type === 'TSTypeLiteral' ? node.members : node.body
+    for (let t of members) {
       if (t.type === 'TSCallSignatureDeclaration') {
         extractEventNames(t.parameters[0], emits)
       }