]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: tests for defineContext()
authorEvan You <yyx990803@gmail.com>
Thu, 12 Nov 2020 23:11:25 +0000 (18:11 -0500)
committerEvan You <yyx990803@gmail.com>
Thu, 12 Nov 2020 23:11:25 +0000 (18:11 -0500)
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/transformExpression.ts
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts

index f1e0fd2b16225b6e24a5e68cbd0c9a808c76311f..905eded29cdb8715c45c679c6bfc7bdd6ac38885 100644 (file)
@@ -272,8 +272,10 @@ export function resolveComponentType(
     }
     const tagFromConst = checkType(BindingTypes.CONST)
     if (tagFromConst) {
-      // constant setup bindings (e.g. imports) can be used as-is
-      return tagFromConst
+      return context.inline
+        ? // in inline mode, const setup bindings (e.g. imports) can be used as-is
+          tagFromConst
+        : `$setup[${JSON.stringify(tagFromConst)}]`
     }
   }
 
index 57039c05a5534289c1588cf32d0ecfb3cbd48139..276c8cb1bdfcc1d07f661886bf8976871c79c5b9 100644 (file)
@@ -102,19 +102,18 @@ export function processExpression(
   const { inline, bindingMetadata } = context
 
   // const bindings exposed from setup - we know they never change
-  if (inline && bindingMetadata[node.content] === BindingTypes.CONST) {
+  if (bindingMetadata[node.content] === BindingTypes.CONST) {
     node.isRuntimeConstant = true
     return node
   }
 
   const prefix = (raw: string) => {
     const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
-    if (type === BindingTypes.CONST) {
-      return raw
-    }
     if (inline) {
       // setup inline mode
-      if (type === BindingTypes.SETUP) {
+      if (type === BindingTypes.CONST) {
+        return raw
+      } else if (type === BindingTypes.SETUP) {
         return `${context.helperString(UNREF)}(${raw})`
       } else if (type === BindingTypes.PROPS) {
         // use __props which is generated by compileScript so in ts mode
@@ -122,8 +121,16 @@ export function processExpression(
         return `__props.${raw}`
       }
     }
-    // fallback to normal
-    return `${type ? `$${type}` : `_ctx`}.${raw}`
+
+    if (type === BindingTypes.CONST) {
+      // setup const binding in non-inline mode
+      return `$setup.${raw}`
+    } else if (type) {
+      return `$${type}.${raw}`
+    } else {
+      // fallback to ctx
+      return `_ctx.${raw}`
+    }
   }
 
   // fast path if expression is a simple identifier.
index a91e3e65462804ddbb5f994252c33276b6617a87..6b95ef42840a7c8bb2cc29a9858cf8db0e424cce 100644 (file)
@@ -1,106 +1,5 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`SFC compile <script setup> <script setup lang="ts"> extract emits 1`] = `
-"import { Slots, defineComponent } from 'vue'
-declare function __emit__(e: 'foo' | 'bar'): void
-      declare function __emit__(e: 'baz', id: number): void
-      
-export function setup(_: {}, { emit: myEmit }: {
-  emit: typeof __emit__,
-  slots: Slots,
-  attrs: Record<string, any>
-}) {
-
-      
-return {  }
-}
-
-export default defineComponent({
-  emits: [\\"foo\\", \\"bar\\", \\"baz\\"] as unknown as undefined,
-  setup
-})"
-`;
-
-exports[`SFC compile <script setup> <script setup lang="ts"> extract props 1`] = `
-"import { Slots, defineComponent } from 'vue'
-interface Test {}
-
-      type Alias = number[]
-
-      
-export function setup(myProps: {
-        string: string
-        number: number
-        boolean: boolean
-        object: object
-        objectLiteral: { a: number }
-        fn: (n: number) => void
-        functionRef: Function
-        objectRef: Object
-        array: string[]
-        arrayRef: Array<any>
-        tuple: [number, number]
-        set: Set<string>
-        literal: 'foo'
-        optional?: any
-        recordRef: Record<string, null>
-        interface: Test
-        alias: Alias
-
-        union: string | number
-        literalUnion: 'foo' | 'bar'
-        literalUnionMixed: 'foo' | 1 | boolean
-        intersection: Test & {}
-      }) {
-
-      
-return {  }
-}
-
-export default defineComponent({
-  props: {
-    string: { type: String, required: true },
-    number: { type: Number, required: true },
-    boolean: { type: Boolean, required: true },
-    object: { type: Object, required: true },
-    objectLiteral: { type: Object, required: true },
-    fn: { type: Function, required: true },
-    functionRef: { type: Function, required: true },
-    objectRef: { type: Object, required: true },
-    array: { type: Array, required: true },
-    arrayRef: { type: Array, required: true },
-    tuple: { type: Array, required: true },
-    set: { type: Set, required: true },
-    literal: { type: String, required: true },
-    optional: { type: null, required: false },
-    recordRef: { type: Object, required: true },
-    interface: { type: Object, required: true },
-    alias: { type: Array, required: true },
-    union: { type: [String, Number], required: true },
-    literalUnion: { type: [String, String], required: true },
-    literalUnionMixed: { type: [String, Number, Boolean], required: true },
-    intersection: { type: Object, required: true }
-  } as unknown as undefined,
-  setup
-})"
-`;
-
-exports[`SFC compile <script setup> <script setup lang="ts"> hoist type declarations 1`] = `
-"import { Slots, defineComponent } from 'vue'
-export interface Foo {}
-        type Bar = {}
-      
-export function setup() {
-
-        
-return {  }
-}
-
-export default defineComponent({
-  setup
-})"
-`;
-
 exports[`SFC compile <script setup> CSS vars injection <script> w/ default export 1`] = `
 "const __default__ = { setup() {} }
 import { useCssVars as __useCssVars__ } from 'vue'
@@ -147,116 +46,177 @@ export default __default__"
 exports[`SFC compile <script setup> CSS vars injection w/ <script setup> 1`] = `
 "import { useCssVars } from 'vue'
 
-export function setup() {
+export default {
+  setup() {
 const color = 'red'
 __useCssVars__(_ctx => ({ color }))
 return { color }
 }
 
-export default { setup }"
+}"
 `;
 
-exports[`SFC compile <script setup> errors should allow export default referencing imported binding 1`] = `
-"import { bar } from './bar'
-          
-export function setup() {
+exports[`SFC compile <script setup> defineContext() 1`] = `
+"export default {
+  props: {
+    foo: String
+  },
+  emit: ['a', 'b'],
+  setup(__props, { props, emit }) {
 
-          
-return { bar }
+
+
+const bar = 1
+
+return { props, emit, bar }
 }
 
-const __default__ = {
-            props: {
+}"
+`;
+
+exports[`SFC compile <script setup> errors should allow defineContext() referencing imported binding 1`] = `
+"import { bar } from './bar'
+          
+export default {
+  props: {
               foo: {
                 default: () => bar
               }
-            }
-          }
-        __default__.setup = setup
-export default __default__"
-`;
+            },
+  setup() {
 
-exports[`SFC compile <script setup> errors should allow export default referencing scope var 1`] = `
-"export function setup() {
-
-          const bar = 1
           
+        
 return { bar }
 }
 
-const __default__ = {
-            props: {
+}"
+`;
+
+exports[`SFC compile <script setup> errors should allow defineContext() referencing scope var 1`] = `
+"export default {
+  props: {
               foo: {
                 default: bar => bar + 1
               }
-            }
-          }
-        __default__.setup = setup
-export default __default__"
-`;
+            },
+  setup() {
 
-exports[`SFC compile <script setup> explicit setup signature 1`] = `
-"export function setup(props, { emit }) {
-emit('foo')
-return {  }
+          const bar = 1
+          
+        
+return { bar }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> imports dedupe between user & helper 1`] = `
 "import { ref } from 'vue'
-  
-export function setup() {
+      
+export default {
+  setup() {
 
-  const foo = ref(1)
-  
-return { ref, foo }
+      const foo = ref(1)
+      
+return { foo, ref }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> imports import dedupe between <script> and <script setup> 1`] = `
 "import { x } from './x'
         
-export function setup() {
+export default {
+  setup() {
 
         x()
         
 return { x }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> imports should extract comment for import or type declarations 1`] = `
 "import a from 'a' // comment
-  import b from 'b'
-  
-export function setup() {
+        import b from 'b'
+        
+export default {
+  setup() {
 
-  
+        
 return { a, b }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> imports should hoist and expose imports 1`] = `
 "import { ref } from 'vue'
-export function setup() {
+export default {
+  setup() {
 
 return { ref }
 }
 
-export default { setup }"
+}"
+`;
+
+exports[`SFC compile <script setup> inlineTemplate mode avoid unref() when necessary 1`] = `
+"import { createVNode as _createVNode, unref as _unref, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
+
+import { ref } from 'vue'
+        import Foo from './Foo.vue'
+        import other from './util'
+        
+export default {
+  setup() {
+
+        const count = ref(0)
+        const constant = {}
+        function fn() {}
+        
+return (_ctx, _cache, $props, $setup, $data, $options) => {
+  return (_openBlock(), _createBlock(_Fragment, null, [
+    _createVNode(Foo),
+    _createVNode(\\"div\\", { onClick: fn }, _toDisplayString(_unref(count)) + \\" \\" + _toDisplayString(constant) + \\" \\" + _toDisplayString(_unref(other)), 1 /* TEXT */)
+  ], 64 /* STABLE_FRAGMENT */))
+}
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> inlineTemplate mode should work 1`] = `
+"import { unref as _unref, toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from \\"vue\\"
+
+const _hoisted_1 = /*#__PURE__*/_createVNode(\\"div\\", null, \\"static\\", -1 /* HOISTED */)
+
+import { ref } from 'vue'
+        
+export default {
+  setup() {
+
+        const count = ref(0)
+        
+return (_ctx, _cache, $props, $setup, $data, $options) => {
+  return (_openBlock(), _createBlock(_Fragment, null, [
+    _createVNode(\\"div\\", null, _toDisplayString(_unref(count)), 1 /* TEXT */),
+    _hoisted_1
+  ], 64 /* STABLE_FRAGMENT */))
+}
+}
+
+}"
 `;
 
 exports[`SFC compile <script setup> ref: syntax sugar accessing ref binding 1`] = `
 "import { ref } from 'vue'
 
-export function setup() {
+export default {
+  setup() {
 
       const a = ref(1)
       console.log(a.value)
@@ -267,13 +227,14 @@ export function setup() {
 return { a, get }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> ref: syntax sugar array destructure 1`] = `
 "import { ref } from 'vue'
 
-export function setup() {
+export default {
+  setup() {
 
       const n = ref(1), [__a, __b = 1, ...__c] = useFoo()
 const a = ref(__a);
@@ -284,13 +245,14 @@ const c = ref(__c);
 return { n, a, b, c }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> ref: syntax sugar convert ref declarations 1`] = `
 "import { ref } from 'vue'
 
-export function setup() {
+export default {
+  setup() {
 
       const foo = ref()
       const a = ref(1)
@@ -303,13 +265,14 @@ export function setup() {
 return { foo, a, b, c, d }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> ref: syntax sugar multi ref declarations 1`] = `
 "import { ref } from 'vue'
 
-export function setup() {
+export default {
+  setup() {
 
       const a = ref(1), b = ref(2), c = ref({
         count: 0
@@ -318,13 +281,14 @@ export function setup() {
 return { a, b, c }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> ref: syntax sugar mutating ref binding 1`] = `
 "import { ref } from 'vue'
 
-export function setup() {
+export default {
+  setup() {
 
       const a = ref(1)
       const b = ref({ count: 0 })
@@ -338,13 +302,14 @@ export function setup() {
 return { a, b, inc }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> ref: syntax sugar nested destructure 1`] = `
 "import { ref } from 'vue'
 
-export function setup() {
+export default {
+  setup() {
 
       const [{ a: { b: __b }}] = useFoo()
 const b = ref(__b);
@@ -356,13 +321,14 @@ const e = ref(__e);
 return { b, d, e }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> ref: syntax sugar object destructure 1`] = `
 "import { ref } from 'vue'
 
-export function setup() {
+export default {
+  setup() {
 
       const n = ref(1), { a: __a, b: __c, d: __d = 1, e: __f = 2, ...__g } = useFoo()
 const a = ref(__a);
@@ -375,11 +341,12 @@ const g = ref(__g);
 return { n, a, c, d, f, g }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> ref: syntax sugar should not convert non ref labels 1`] = `
-"export function setup() {
+"export default {
+  setup() {
 
       foo: a = 1, b = 2, c = {
         count: 0
@@ -388,13 +355,14 @@ exports[`SFC compile <script setup> ref: syntax sugar should not convert non ref
 return {  }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> ref: syntax sugar using ref binding in property shorthand 1`] = `
 "import { ref } from 'vue'
 
-export function setup() {
+export default {
+  setup() {
 
       const a = ref(1)
       const b = { a: a.value }
@@ -405,21 +373,138 @@ export function setup() {
 return { a, b, test }
 }
 
-export default { setup }"
+}"
 `;
 
 exports[`SFC compile <script setup> should expose top level declarations 1`] = `
 "import { x } from './x'
       
-export function setup() {
+export default {
+  setup() {
 
       let a = 1
       const b = 2
       function c() {}
       class d {}
       
-return { x, a, b, c, d }
+return { a, b, c, d, x }
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> with TypeScript defineContext w/ runtime options 1`] = `
+"import { defineComponent } from 'vue'
+
+
+export default defineComponent({
+  props: { foo: String },
+  emits: ['a', 'b'],
+  setup(__props, { props, emit }) {
+
+
+
+return { props, emit }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> with TypeScript defineContext w/ type / extract emits (union) 1`] = `
+"import { Slots, defineComponent } from 'vue'
+
+      
+export default defineComponent({
+  emits: [\\"foo\\", \\"bar\\", \\"baz\\"] as unknown as undefined,
+  setup(__props, { emit }: {
+  props: {},
+  emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),
+  slots: Slots,
+  attrs: Record<string, any>
+}) {
+
+      
+      
+return { emit }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> with TypeScript defineContext w/ type / extract emits 1`] = `
+"import { Slots, defineComponent } from 'vue'
+
+      
+export default defineComponent({
+  emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
+  setup(__props, { emit }: {
+  props: {},
+  emit: (e: 'foo' | 'bar') => void,
+  slots: Slots,
+  attrs: Record<string, any>
+}) {
+
+      
+      
+return { emit }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> with TypeScript defineContext w/ type / extract props 1`] = `
+"import { defineComponent } from 'vue'
+
+      interface Test {}
+
+      type Alias = number[]
+
+      
+export default defineComponent({
+  props: {
+    string: { type: String, required: true },
+    number: { type: Number, required: true },
+    boolean: { type: Boolean, required: true },
+    object: { type: Object, required: true },
+    objectLiteral: { type: Object, required: true },
+    fn: { type: Function, required: true },
+    functionRef: { type: Function, required: true },
+    objectRef: { type: Object, required: true },
+    array: { type: Array, required: true },
+    arrayRef: { type: Array, required: true },
+    tuple: { type: Array, required: true },
+    set: { type: Set, required: true },
+    literal: { type: String, required: true },
+    optional: { type: null, required: false },
+    recordRef: { type: Object, required: true },
+    interface: { type: Object, required: true },
+    alias: { type: Array, required: true },
+    union: { type: [String, Number], required: true },
+    literalUnion: { type: [String, String], required: true },
+    literalUnionMixed: { type: [String, Number, Boolean], required: true },
+    intersection: { type: Object, required: true }
+  } as unknown as undefined,
+  setup() {
+
+      
+      
+return {  }
 }
 
-export default { setup }"
+})"
+`;
+
+exports[`SFC compile <script setup> with TypeScript hoist type declarations 1`] = `
+"import { defineComponent } from 'vue'
+export interface Foo {}
+        type Bar = {}
+      
+export default defineComponent({
+  setup() {
+
+        
+return {  }
+}
+
+})"
 `;
index d9fffd2f2a02e1f00febb74f0e9983dbb04ca4de..67882f7156e6f34a093fb31e2878180938711321 100644 (file)
@@ -22,12 +22,6 @@ function assertCode(code: string) {
 }
 
 describe('SFC compile <script setup>', () => {
-  test('explicit setup signature', () => {
-    assertCode(
-      compile(`<script setup="props, { emit }">emit('foo')</script>`).content
-    )
-  })
-
   test('should expose top level declarations', () => {
     const { content } = compile(`
       <script setup>
@@ -39,7 +33,43 @@ describe('SFC compile <script setup>', () => {
       </script>
       `)
     assertCode(content)
-    expect(content).toMatch('return { x, a, b, c, d }')
+    expect(content).toMatch('return { a, b, c, d, x }')
+  })
+
+  test('defineContext()', () => {
+    const { content, bindings } = compile(`
+<script setup>
+import { defineContext } from 'vue'
+const { props, emit } = defineContext({
+  props: {
+    foo: String
+  },
+  emit: ['a', 'b']
+})
+
+const bar = 1
+</script>
+  `)
+    // should generate working code
+    assertCode(content)
+    // should anayze bindings
+    expect(bindings).toStrictEqual({
+      foo: 'props',
+      bar: 'const',
+      props: 'const',
+      emit: 'const'
+    })
+
+    // should remove defineContext import and call
+    expect(content).not.toMatch('defineContext')
+    // should generate correct setup signature
+    expect(content).toMatch(`setup(__props, { props, emit }) {`)
+    // should include context options in default export
+    expect(content).toMatch(`export default {
+  props: {
+    foo: String
+  },
+  emit: ['a', 'b'],`)
   })
 
   describe('imports', () => {
@@ -51,18 +81,22 @@ describe('SFC compile <script setup>', () => {
 
     test('should extract comment for import or type declarations', () => {
       assertCode(
-        compile(`<script setup>
-  import a from 'a' // comment
-  import b from 'b'
-  </script>`).content
+        compile(`
+        <script setup>
+        import a from 'a' // comment
+        import b from 'b'
+        </script>
+        `).content
       )
     })
 
     test('dedupe between user & helper', () => {
-      const { content } = compile(`<script setup>
-  import { ref } from 'vue'
-  ref: foo = 1
-  </script>`)
+      const { content } = compile(`
+      <script setup>
+      import { ref } from 'vue'
+      ref: foo = 1
+      </script>
+      `)
       assertCode(content)
       expect(content).toMatch(`import { ref } from 'vue'`)
     })
@@ -84,7 +118,62 @@ describe('SFC compile <script setup>', () => {
     })
   })
 
-  describe('<script setup lang="ts">', () => {
+  describe('inlineTemplate mode', () => {
+    test('should work', () => {
+      const { content } = compile(
+        `
+        <script setup>
+        import { ref } from 'vue'
+        const count = ref(0)
+        </script>
+        <template>
+          <div>{{ count }}</div>
+          <div>static</div>
+        </template>
+        `,
+        { inlineTemplate: true }
+      )
+      // check snapshot and make sure helper imports and
+      // hoists are placed correctly.
+      assertCode(content)
+    })
+
+    test('avoid unref() when necessary', () => {
+      // function, const, component import
+      const { content } = compile(
+        `
+        <script setup>
+        import { ref, defineContext } from 'vue'
+        import Foo from './Foo.vue'
+        import other from './util'
+        const count = ref(0)
+        const constant = {}
+        function fn() {}
+        </script>
+        <template>
+          <Foo/>
+          <div @click="fn">{{ count }} {{ constant }} {{ other }}</div>
+        </template>
+        `,
+        { inlineTemplate: true }
+      )
+      assertCode(content)
+      // no need to unref vue component import
+      expect(content).toMatch(`createVNode(Foo)`)
+      // should unref other imports
+      expect(content).toMatch(`unref(other)`)
+      // no need to unref constant literals
+      expect(content).not.toMatch(`unref(constant)`)
+      // should unref const w/ call init (e.g. ref())
+      expect(content).toMatch(`unref(count)`)
+      // no need to unref function declarations
+      expect(content).toMatch(`{ onClick: fn }`)
+      // no need to mark constant fns in patch flag
+      expect(content).not.toMatch(`PROPS`)
+    })
+  })
+
+  describe('with TypeScript', () => {
     test('hoist type declarations', () => {
       const { content } = compile(`
       <script setup lang="ts">
@@ -94,37 +183,57 @@ describe('SFC compile <script setup>', () => {
       assertCode(content)
     })
 
-    test('extract props', () => {
+    test('defineContext w/ runtime options', () => {
       const { content } = compile(`
-      <script setup="myProps" lang="ts">
+<script setup lang="ts">
+import { defineContext } from 'vue'
+const { props, emit } = defineContext({
+  props: { foo: String },
+  emits: ['a', 'b']
+})
+</script>
+      `)
+      assertCode(content)
+      expect(content).toMatch(`export default defineComponent({
+  props: { foo: String },
+  emits: ['a', 'b'],
+  setup(__props, { props, emit }) {`)
+    })
+
+    test('defineContext w/ type / extract props', () => {
+      const { content, bindings } = compile(`
+      <script setup lang="ts">
+      import { defineContext } from 'vue'
       interface Test {}
 
       type Alias = number[]
 
-      declare const myProps: {
-        string: string
-        number: number
-        boolean: boolean
-        object: object
-        objectLiteral: { a: number }
-        fn: (n: number) => void
-        functionRef: Function
-        objectRef: Object
-        array: string[]
-        arrayRef: Array<any>
-        tuple: [number, number]
-        set: Set<string>
-        literal: 'foo'
-        optional?: any
-        recordRef: Record<string, null>
-        interface: Test
-        alias: Alias
-
-        union: string | number
-        literalUnion: 'foo' | 'bar'
-        literalUnionMixed: 'foo' | 1 | boolean
-        intersection: Test & {}
-      }
+      defineContext<{
+        props: {
+          string: string
+          number: number
+          boolean: boolean
+          object: object
+          objectLiteral: { a: number }
+          fn: (n: number) => void
+          functionRef: Function
+          objectRef: Object
+          array: string[]
+          arrayRef: Array<any>
+          tuple: [number, number]
+          set: Set<string>
+          literal: 'foo'
+          optional?: any
+          recordRef: Record<string, null>
+          interface: Test
+          alias: Alias
+
+          union: string | number
+          literalUnion: 'foo' | 'bar'
+          literalUnionMixed: 'foo' | 1 | boolean
+          intersection: Test & {}
+        }
+      }>()
       </script>`)
       assertCode(content)
       expect(content).toMatch(`string: { type: String, required: true }`)
@@ -154,21 +263,57 @@ describe('SFC compile <script setup>', () => {
         `literalUnionMixed: { type: [String, Number, Boolean], required: true }`
       )
       expect(content).toMatch(`intersection: { type: Object, required: true }`)
+      expect(bindings).toStrictEqual({
+        string: 'props',
+        number: 'props',
+        boolean: 'props',
+        object: 'props',
+        objectLiteral: 'props',
+        fn: 'props',
+        functionRef: 'props',
+        objectRef: 'props',
+        array: 'props',
+        arrayRef: 'props',
+        tuple: 'props',
+        set: 'props',
+        literal: 'props',
+        optional: 'props',
+        recordRef: 'props',
+        interface: 'props',
+        alias: 'props',
+        union: 'props',
+        literalUnion: 'props',
+        literalUnionMixed: 'props',
+        intersection: 'props'
+      })
     })
 
-    test('extract emits', () => {
+    test('defineContext w/ type / extract emits', () => {
       const { content } = compile(`
-      <script setup="_, { emit: myEmit }" lang="ts">
-      declare function myEmit(e: 'foo' | 'bar'): void
-      declare function myEmit(e: 'baz', id: number): void
+      <script setup lang="ts">
+      import { defineContext } from 'vue'
+      const { emit } = defineContext<{
+        emit: (e: 'foo' | 'bar') => void
+      }>()
+      </script>
+      `)
+      assertCode(content)
+      expect(content).toMatch(`props: {},\n  emit: (e: 'foo' | 'bar') => void,`)
+      expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
+    })
+
+    test('defineContext w/ type / extract emits (union)', () => {
+      const { content } = compile(`
+      <script setup lang="ts">
+      import { defineContext } from 'vue'
+      const { emit } = defineContext<{
+        emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)
+      }>()
       </script>
       `)
       assertCode(content)
       expect(content).toMatch(
-        `declare function __emit__(e: 'foo' | 'bar'): void`
-      )
-      expect(content).toMatch(
-        `declare function __emit__(e: 'baz', id: number): void`
+        `props: {},\n  emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),`
       )
       expect(content).toMatch(
         `emits: ["foo", "bar", "baz"] as unknown as undefined`
@@ -220,9 +365,7 @@ describe('SFC compile <script setup>', () => {
   describe('async/await detection', () => {
     function assertAwaitDetection(code: string, shouldAsync = true) {
       const { content } = compile(`<script setup>${code}</script>`)
-      expect(content).toMatch(
-        `export ${shouldAsync ? `async ` : ``}function setup`
-      )
+      expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup()`)
     }
 
     test('expression statement', () => {
@@ -459,25 +602,27 @@ describe('SFC compile <script setup>', () => {
       ).toThrow(`<script> and <script setup> must have the same language type`)
     })
 
+    const moduleErrorMsg = `cannot contain ES module exports`
+
     test('non-type named exports', () => {
       expect(() =>
         compile(`<script setup>
         export const a = 1
         </script>`)
-      ).toThrow(`cannot contain non-type named or * exports`)
+      ).toThrow(moduleErrorMsg)
 
       expect(() =>
         compile(`<script setup>
         export * from './foo'
         </script>`)
-      ).toThrow(`cannot contain non-type named or * exports`)
+      ).toThrow(moduleErrorMsg)
 
       expect(() =>
         compile(`<script setup>
           const bar = 1
           export { bar as default }
         </script>`)
-      ).toThrow(`cannot contain non-type named or * exports`)
+      ).toThrow(moduleErrorMsg)
     })
 
     test('ref: non-assignment expressions', () => {
@@ -488,97 +633,74 @@ describe('SFC compile <script setup>', () => {
       ).toThrow(`ref: statements can only contain assignment expressions`)
     })
 
-    test('export default referencing local var', () => {
+    test('defineContext() w/ both type and non-type args', () => {
+      expect(() => {
+        compile(`<script setup lang="ts">
+        import { defineContext } from 'vue'
+        defineContext<{}>({})
+        </script>`)
+      }).toThrow(`cannot accept both type and non-type arguments`)
+    })
+
+    test('defineContext() referencing local var', () => {
       expect(() =>
         compile(`<script setup>
-          const bar = 1
-          export default {
-            props: {
-              foo: {
-                default: () => bar
-              }
+        import { defineContext } from 'vue'
+        const bar = 1
+        defineContext({
+          props: {
+            foo: {
+              default: () => bar
             }
           }
+        })
         </script>`)
       ).toThrow(`cannot reference locally declared variables`)
     })
 
-    test('export default referencing ref declarations', () => {
+    test('defineContext() referencing ref declarations', () => {
       expect(() =>
         compile(`<script setup>
+        import { defineContext } from 'vue'
         ref: bar = 1
-        export default {
-          props: bar
-        }
+        defineContext({
+          props: { bar }
+        })
       </script>`)
       ).toThrow(`cannot reference locally declared variables`)
     })
 
-    test('should allow export default referencing scope var', () => {
+    test('should allow defineContext() referencing scope var', () => {
       assertCode(
         compile(`<script setup>
+          import { defineContext } from 'vue'
           const bar = 1
-          export default {
+          defineContext({
             props: {
               foo: {
                 default: bar => bar + 1
               }
             }
-          }
+          })
         </script>`).content
       )
     })
 
-    test('should allow export default referencing imported binding', () => {
+    test('should allow defineContext() referencing imported binding', () => {
       assertCode(
         compile(`<script setup>
+          import { defineContext } from 'vue'
           import { bar } from './bar'
-          export default {
+          defineContext({
             props: {
               foo: {
                 default: () => bar
               }
             }
-          }
+          })
         </script>`).content
       )
     })
-
-    test('error on duplicated default export', () => {
-      expect(() =>
-        compile(`
-      <script>
-      export default {}
-      </script>
-      <script setup>
-      export default {}
-      </script>
-      `)
-      ).toThrow(`Default export is already declared`)
-
-      expect(() =>
-        compile(`
-      <script>
-      export { x as default } from './y'
-      </script>
-      <script setup>
-      export default {}
-      </script>
-      `)
-      ).toThrow(`Default export is already declared`)
-
-      expect(() =>
-        compile(`
-      <script>
-      const x = {}
-      export { x as default }
-      </script>
-      <script setup>
-      export default {}
-      </script>
-      `)
-      ).toThrow(`Default export is already declared`)
-    })
   })
 })
 
@@ -779,11 +901,12 @@ describe('SFC analyze <script> bindings', () => {
   it('works for script setup', () => {
     const { bindings } = compile(`
       <script setup>
-        export default {
-          props: {
-            foo: String,
-          },
+      import { defineContext } from 'vue'
+      defineContext({
+        props: {
+          foo: String,
         }
+      })
       </script>
     `)
     expect(bindings).toStrictEqual({
index ea5bf95f0eae07e4daed55b4e7ad3cd2e4a552c4..b90b677f7c8f68a439b6faf6631f1b435647cb59 100644 (file)
@@ -15,12 +15,12 @@ import {
   TSType,
   TSTypeLiteral,
   TSFunctionType,
-  TSDeclareFunction,
   ObjectProperty,
   ArrayExpression,
   Statement,
   Expression,
-  LabeledStatement
+  LabeledStatement,
+  TSUnionType
 } from '@babel/types'
 import { walk } from 'estree-walker'
 import { RawSourceMap } from 'source-map'
@@ -143,6 +143,7 @@ export function compileScript(
   const refIdentifiers: Set<Identifier> = new Set()
   const enableRefSugar = options.refSugar !== false
   let defaultExport: Node | undefined
+  let hasContextCall = false
   let setupContextExp: string | undefined
   let setupContextArg: ObjectExpression | undefined
   let setupContextType: TSTypeLiteral | undefined
@@ -182,6 +183,48 @@ export function compileScript(
     )
   }
 
+  function processContextCall(node: Node): boolean {
+    if (
+      node.type === 'CallExpression' &&
+      node.callee.type === 'Identifier' &&
+      node.callee.name === CTX_FN_NAME
+    ) {
+      if (hasContextCall) {
+        error('duplicate defineContext() call', node)
+      }
+      hasContextCall = true
+      const optsArg = node.arguments[0]
+      if (optsArg) {
+        if (optsArg.type === 'ObjectExpression') {
+          setupContextArg = optsArg
+        } else {
+          error(`${CTX_FN_NAME}() argument must be an object literal.`, optsArg)
+        }
+      }
+      // context call has type parameters - infer runtime types from it
+      if (node.typeParameters) {
+        if (setupContextArg) {
+          error(
+            `${CTX_FN_NAME}() cannot accept both type and non-type arguments ` +
+              `at the same time. Use one or the other.`,
+            node
+          )
+        }
+        const typeArg = node.typeParameters.params[0]
+        if (typeArg.type === 'TSTypeLiteral') {
+          setupContextType = typeArg
+        } else {
+          error(
+            `type argument passed to ${CTX_FN_NAME}() must be a literal type.`,
+            typeArg
+          )
+        }
+      }
+      return true
+    }
+    return false
+  }
+
   function processRefExpression(exp: Expression, statement: LabeledStatement) {
     if (exp.type === 'AssignmentExpression') {
       helperImports.add('ref')
@@ -500,51 +543,24 @@ export function compileScript(
       }
     }
 
+    if (
+      node.type === 'ExpressionStatement' &&
+      processContextCall(node.expression)
+    ) {
+      s.remove(node.start! + startOffset, node.end! + startOffset)
+    }
+
     if (node.type === 'VariableDeclaration' && !node.declare) {
       for (const decl of node.declarations) {
-        if (
-          decl.init &&
-          decl.init.type === 'CallExpression' &&
-          decl.init.callee.type === 'Identifier' &&
-          decl.init.callee.name === CTX_FN_NAME
-        ) {
-          if (node.declarations.length === 1) {
-            s.remove(node.start! + startOffset, node.end! + startOffset)
-          } else {
-            s.remove(decl.start! + startOffset, decl.end! + startOffset)
-          }
+        if (decl.init && processContextCall(decl.init)) {
           setupContextExp = scriptSetup.content.slice(
             decl.id.start!,
             decl.id.end!
           )
-          const optsArg = decl.init.arguments[0]
-          if (optsArg.type === 'ObjectExpression') {
-            setupContextArg = optsArg
+          if (node.declarations.length === 1) {
+            s.remove(node.start! + startOffset, node.end! + startOffset)
           } else {
-            error(
-              `${CTX_FN_NAME}() argument must be an object literal.`,
-              optsArg
-            )
-          }
-
-          // useSetupContext() has type parameters - infer runtime types from it
-          if (decl.init.typeParameters) {
-            if (setupContextArg) {
-              error(
-                `${CTX_FN_NAME}() cannot accept both type and non-type arguments ` +
-                  `at the same time. Use one or the other.`,
-                decl.init
-              )
-            }
-            const typeArg = decl.init.typeParameters.params[0]
-            if (typeArg.type === 'TSTypeLiteral') {
-              setupContextType = typeArg
-            } else {
-              error(
-                `type argument passed to ${CTX_FN_NAME}() must be a literal type.`,
-                typeArg
-              )
-            }
+            s.remove(decl.start! + startOffset, decl.end! + startOffset)
           }
         }
       }
@@ -641,7 +657,8 @@ export function compileScript(
           typeNode.start!,
           typeNode.end!
         )
-        if (m.key.name === 'props') {
+        const key = m.key.name
+        if (key === 'props') {
           propsType = typeString
           if (typeNode.type === 'TSTypeLiteral') {
             extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
@@ -649,18 +666,23 @@ export function compileScript(
             // TODO be able to trace references
             error(`props type must be an object literal type`, typeNode)
           }
-        } else if (m.key.name === 'emit') {
+        } else if (key === 'emit') {
           emitType = typeString
-          if (typeNode.type === 'TSFunctionType') {
+          if (
+            typeNode.type === 'TSFunctionType' ||
+            typeNode.type === 'TSUnionType'
+          ) {
             extractRuntimeEmits(typeNode, typeDeclaredEmits)
           } else {
             // TODO be able to trace references
             error(`emit type must be a function type`, typeNode)
           }
-        } else if (m.key.name === 'attrs') {
+        } else if (key === 'attrs') {
           attrsType = typeString
-        } else if (m.key.name === 'slots') {
+        } else if (key === 'slots') {
           slotsType = typeString
+        } else {
+          error(`invalid setup context property: "${key}"`, m.key)
         }
       }
     }
@@ -747,19 +769,13 @@ export function compileScript(
   if (setupContextArg) {
     Object.assign(bindingMetadata, analyzeBindingsFromOptions(setupContextArg))
   }
-  if (options.inlineTemplate) {
-    for (const [key, { source }] of Object.entries(userImports)) {
-      bindingMetadata[key] = source.endsWith('.vue')
-        ? BindingTypes.CONST
-        : BindingTypes.SETUP
-    }
-    for (const key in setupBindings) {
-      bindingMetadata[key] = setupBindings[key]
-    }
-  } else {
-    for (const key in allBindings) {
-      bindingMetadata[key] = BindingTypes.SETUP
-    }
+  for (const [key, { source }] of Object.entries(userImports)) {
+    bindingMetadata[key] = source.endsWith('.vue')
+      ? BindingTypes.CONST
+      : BindingTypes.SETUP
+  }
+  for (const key in setupBindings) {
+    bindingMetadata[key] = setupBindings[key]
   }
 
   // 11. generate return statement
@@ -1135,11 +1151,20 @@ function toRuntimeTypeString(types: string[]) {
 }
 
 function extractRuntimeEmits(
-  node: TSFunctionType | TSDeclareFunction,
+  node: TSFunctionType | TSUnionType,
   emits: Set<string>
 ) {
-  const eventName =
-    node.type === 'TSDeclareFunction' ? node.params[0] : node.parameters[0]
+  if (node.type === 'TSUnionType') {
+    for (let t of node.types) {
+      if (t.type === 'TSParenthesizedType') t = t.typeAnnotation
+      if (t.type === 'TSFunctionType') {
+        extractRuntimeEmits(t, emits)
+      }
+    }
+    return
+  }
+
+  const eventName = node.parameters[0]
   if (
     eventName.type === 'Identifier' &&
     eventName.typeAnnotation &&