]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: defineOptions -> defineProps + defineEmit + useContext
authorEvan You <yyx990803@gmail.com>
Tue, 24 Nov 2020 20:12:59 +0000 (15:12 -0500)
committerEvan You <yyx990803@gmail.com>
Wed, 25 Nov 2020 00:04:21 +0000 (19:04 -0500)
13 files changed:
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/__snapshots__/cssVars.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/__tests__/cssVars.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/runtime-core/src/apiDefineOptions.ts [deleted file]
packages/runtime-core/src/apiSetupHelpers.ts [new file with mode: 0644]
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/index.ts
test-dts/defineOptions.test-d.ts [deleted file]
test-dts/setupHelpers.test-d.ts [new file with mode: 0644]

index 9d05d643a838a8cad125134a84f94068f2951a2a..11658523ecec24b9d665381ddc7144177608ce3f 100644 (file)
@@ -33,38 +33,55 @@ return { x }
       export const n = 1"
 `;
 
-exports[`SFC compile <script setup> defineOptions() 1`] = `
+exports[`SFC compile <script setup> defineEmit() 1`] = `
 "export default {
   expose: [],
-  props: {
-    foo: String
-  },
-  emit: ['a', 'b'],
-  setup(__props, { props, emit }) {
+  emits: ['foo', 'bar'],
+  setup(__props, { emit: myEmit }) {
+
 
 
+return { myEmit }
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> defineProps() 1`] = `
+"export default {
+  expose: [],
+  props: {
+  foo: String
+},
+  setup(__props) {
+
+const props = __props
 
 const bar = 1
 
-return { props, emit, bar }
+return { props, bar }
 }
 
 }"
 `;
 
-exports[`SFC compile <script setup> errors should allow defineOptions() referencing imported binding 1`] = `
+exports[`SFC compile <script setup> errors should allow defineProps/Emit() referencing imported binding 1`] = `
 "import { bar } from './bar'
-          
+        
 export default {
   expose: [],
   props: {
-              foo: {
-                default: () => bar
-              }
-            },
+          foo: {
+            default: () => bar
+          }
+        },
+  emits: {
+          foo: () => bar > 1
+        },
   setup(__props) {
 
-          
+        
+        
         
 return { bar }
 }
@@ -72,18 +89,22 @@ return { bar }
 }"
 `;
 
-exports[`SFC compile <script setup> errors should allow defineOptions() referencing scope var 1`] = `
+exports[`SFC compile <script setup> errors should allow defineProps/Emit() referencing scope var 1`] = `
 "export default {
   expose: [],
   props: {
-              foo: {
-                default: bar => bar + 1
-              }
-            },
+            foo: {
+              default: bar => bar + 1
+            }
+          },
+  emits: {
+            foo: bar => bar > 1
+          },
   setup(__props) {
 
           const bar = 1
           
+          
         
 return { bar }
 }
@@ -594,37 +615,18 @@ return { a, b, c, d, x }
 }"
 `;
 
-exports[`SFC compile <script setup> with TypeScript defineOptions w/ runtime options 1`] = `
+exports[`SFC compile <script setup> with TypeScript defineEmit w/ type (union) 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
 
-
-export default _defineComponent({
-  expose: [],
-  props: { foo: String },
-  emits: ['a', 'b'],
-  setup(__props, { props, emit }) {
-
-
-
-return { props, emit }
-}
-
-})"
-`;
-
-exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract emits (union) 1`] = `
-"import { Slots as _Slots, defineComponent as _defineComponent } from 'vue'
-
       
 export default _defineComponent({
   expose: [],
   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>
-}) {
+        emit: (((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)),
+        slots: any,
+        attrs: any
+      }) {
 
       
       
@@ -634,19 +636,18 @@ return { emit }
 })"
 `;
 
-exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract emits 1`] = `
-"import { Slots as _Slots, defineComponent as _defineComponent } from 'vue'
+exports[`SFC compile <script setup> with TypeScript defineEmit w/ type 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
 
       
 export default _defineComponent({
   expose: [],
   emits: [\\"foo\\", \\"bar\\"] as unknown as undefined,
   setup(__props, { emit }: {
-  props: {},
-  emit: (e: 'foo' | 'bar') => void,
-  slots: Slots,
-  attrs: Record<string, any>
-}) {
+        emit: ((e: 'foo' | 'bar') => void),
+        slots: any,
+        attrs: any
+      }) {
 
       
       
@@ -656,7 +657,7 @@ return { emit }
 })"
 `;
 
-exports[`SFC compile <script setup> with TypeScript defineOptions w/ type / extract props 1`] = `
+exports[`SFC compile <script setup> with TypeScript defineProps w/ type 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
 
       interface Test {}
@@ -689,7 +690,30 @@ export default _defineComponent({
     literalUnionMixed: { type: [String, Number, Boolean], required: true },
     intersection: { type: Object, required: true }
   } as unknown as undefined,
-  setup(__props) {
+  setup(__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 & {}
+      }) {
 
       
       
@@ -699,6 +723,26 @@ return {  }
 })"
 `;
 
+exports[`SFC compile <script setup> with TypeScript defineProps/Emit w/ runtime options 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+
+export default _defineComponent({
+  expose: [],
+  props: { foo: String },
+  emits: ['a', 'b'],
+  setup(__props, { emit }) {
+
+const props = __props
+
+
+
+return { props, emit }
+}
+
+})"
+`;
+
 exports[`SFC compile <script setup> with TypeScript hoist type declarations 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
 export interface Foo {}
index cc7c16bc7c634e6643460287e7f37370ead53df2..7cea1e8ddcdc75e01d58c10934ee9c6706010622 100644 (file)
@@ -86,8 +86,8 @@ import { ref } from 'vue'
 export default {
   expose: [],
   props: {
-            foo: String
-          },
+          foo: String
+        },
   setup(__props) {
 
 _useCssVars(_ctx => ({
index 9b4035185a7411f3e61a5abbc80735382261a5ac..ac607dcef0c8298014a0e62bfb6585dfbce49058 100644 (file)
@@ -16,17 +16,13 @@ describe('SFC compile <script setup>', () => {
     expect(content).toMatch('return { a, b, c, d, x }')
   })
 
-  test('defineOptions()', () => {
+  test('defineProps()', () => {
     const { content, bindings } = compile(`
 <script setup>
-import { defineOptions } from 'vue'
-const { props, emit } = defineOptions({
-  props: {
-    foo: String
-  },
-  emit: ['a', 'b']
+import { defineProps } from 'vue'
+const props = defineProps({
+  foo: String
 })
-
 const bar = 1
 </script>
   `)
@@ -36,21 +32,42 @@ const bar = 1
     expect(bindings).toStrictEqual({
       foo: BindingTypes.PROPS,
       bar: BindingTypes.SETUP_CONST,
-      props: BindingTypes.SETUP_CONST,
-      emit: BindingTypes.SETUP_CONST
+      props: BindingTypes.SETUP_CONST
     })
 
     // should remove defineOptions import and call
-    expect(content).not.toMatch('defineOptions')
+    expect(content).not.toMatch('defineProps')
     // should generate correct setup signature
-    expect(content).toMatch(`setup(__props, { props, emit }) {`)
+    expect(content).toMatch(`setup(__props) {`)
+    // should assign user identifier to it
+    expect(content).toMatch(`const props = __props`)
     // should include context options in default export
     expect(content).toMatch(`export default {
   expose: [],
   props: {
-    foo: String
-  },
-  emit: ['a', 'b'],`)
+  foo: String
+},`)
+  })
+
+  test('defineEmit()', () => {
+    const { content, bindings } = compile(`
+<script setup>
+import { defineEmit } from 'vue'
+const myEmit = defineEmit(['foo', 'bar'])
+</script>
+  `)
+    assertCode(content)
+    expect(bindings).toStrictEqual({
+      myEmit: BindingTypes.SETUP_CONST
+    })
+    // should remove defineOptions import and call
+    expect(content).not.toMatch('defineEmit')
+    // should generate correct setup signature
+    expect(content).toMatch(`setup(__props, { emit: myEmit }) {`)
+    // should include context options in default export
+    expect(content).toMatch(`export default {
+  expose: [],
+  emits: ['foo', 'bar'],`)
   })
 
   describe('<script> and <script setup> co-usage', () => {
@@ -174,7 +191,7 @@ const bar = 1
       // function, const, component import
       const { content } = compile(
         `<script setup>
-        import { ref, defineOptions } from 'vue'
+        import { ref } from 'vue'
         import Foo from './Foo.vue'
         import other from './util'
         const count = ref(0)
@@ -360,14 +377,12 @@ const bar = 1
       assertCode(content)
     })
 
-    test('defineOptions w/ runtime options', () => {
+    test('defineProps/Emit w/ runtime options', () => {
       const { content } = compile(`
 <script setup lang="ts">
-import { defineOptions } from 'vue'
-const { props, emit } = defineOptions({
-  props: { foo: String },
-  emits: ['a', 'b']
-})
+import { defineProps, defineEmit } from 'vue'
+const props = defineProps({ foo: String })
+const emit = defineEmit(['a', 'b'])
 </script>
       `)
       assertCode(content)
@@ -375,42 +390,40 @@ const { props, emit } = defineOptions({
   expose: [],
   props: { foo: String },
   emits: ['a', 'b'],
-  setup(__props, { props, emit }) {`)
+  setup(__props, { emit }) {`)
     })
 
-    test('defineOptions w/ type / extract props', () => {
+    test('defineProps w/ type', () => {
       const { content, bindings } = compile(`
       <script setup lang="ts">
-      import { defineOptions } from 'vue'
+      import { defineProps } from 'vue'
       interface Test {}
 
       type Alias = number[]
 
-      defineOptions<{
-        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 & {}
-        }
+      defineProps<{
+        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)
@@ -466,33 +479,28 @@ const { props, emit } = defineOptions({
       })
     })
 
-    test('defineOptions w/ type / extract emits', () => {
+    test('defineEmit w/ type', () => {
       const { content } = compile(`
       <script setup lang="ts">
-      import { defineOptions } from 'vue'
-      const { emit } = defineOptions<{
-        emit: (e: 'foo' | 'bar') => void
-      }>()
+      import { defineEmit } from 'vue'
+      const emit = defineEmit<(e: 'foo' | 'bar') => void>()
       </script>
       `)
       assertCode(content)
-      expect(content).toMatch(`props: {},\n  emit: (e: 'foo' | 'bar') => void,`)
+      expect(content).toMatch(`emit: ((e: 'foo' | 'bar') => void),`)
       expect(content).toMatch(`emits: ["foo", "bar"] as unknown as undefined`)
     })
 
-    test('defineOptions w/ type / extract emits (union)', () => {
+    test('defineEmit w/ type (union)', () => {
+      const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)`
       const { content } = compile(`
       <script setup lang="ts">
-      import { defineOptions } from 'vue'
-      const { emit } = defineOptions<{
-        emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)
-      }>()
+      import { defineEmit } from 'vue'
+      const emit = defineEmit<${type}>()
       </script>
       `)
       assertCode(content)
-      expect(content).toMatch(
-        `props: {},\n  emit: ((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void),`
-      )
+      expect(content).toMatch(`emit: (${type}),`)
       expect(content).toMatch(
         `emits: ["foo", "bar", "baz"] as unknown as undefined`
       )
@@ -774,71 +782,96 @@ const { props, emit } = defineOptions({
       ).toThrow(`ref: statements can only contain assignment expressions`)
     })
 
-    test('defineOptions() w/ both type and non-type args', () => {
+    test('defineProps/Emit() w/ both type and non-type args', () => {
+      expect(() => {
+        compile(`<script setup lang="ts">
+        import { defineProps } from 'vue'
+        defineProps<{}>({})
+        </script>`)
+      }).toThrow(`cannot accept both type and non-type arguments`)
+
       expect(() => {
         compile(`<script setup lang="ts">
-        import { defineOptions } from 'vue'
-        defineOptions<{}>({})
+        import { defineEmit } from 'vue'
+        defineEmit<{}>({})
         </script>`)
       }).toThrow(`cannot accept both type and non-type arguments`)
     })
 
-    test('defineOptions() referencing local var', () => {
+    test('defineProps/Emit() referencing local var', () => {
       expect(() =>
         compile(`<script setup>
-        import { defineOptions } from 'vue'
+        import { defineProps } from 'vue'
         const bar = 1
-        defineOptions({
-          props: {
-            foo: {
-              default: () => bar
-            }
+        defineProps({
+          foo: {
+            default: () => bar
           }
         })
         </script>`)
       ).toThrow(`cannot reference locally declared variables`)
+
+      expect(() =>
+        compile(`<script setup>
+        import { defineEmit } from 'vue'
+        const bar = 'hello'
+        defineEmit([bar])
+        </script>`)
+      ).toThrow(`cannot reference locally declared variables`)
     })
 
-    test('defineOptions() referencing ref declarations', () => {
+    test('defineProps/Emit() referencing ref declarations', () => {
+      expect(() =>
+        compile(`<script setup>
+        import { defineProps } from 'vue'
+        ref: bar = 1
+        defineProps({
+          bar
+        })
+      </script>`)
+      ).toThrow(`cannot reference locally declared variables`)
+
       expect(() =>
         compile(`<script setup>
-        import { defineOptions } from 'vue'
+        import { defineEmit } from 'vue'
         ref: bar = 1
-        defineOptions({
-          props: { bar }
+        defineEmit({
+          bar
         })
       </script>`)
       ).toThrow(`cannot reference locally declared variables`)
     })
 
-    test('should allow defineOptions() referencing scope var', () => {
+    test('should allow defineProps/Emit() referencing scope var', () => {
       assertCode(
         compile(`<script setup>
-          import { defineOptions } from 'vue'
+          import { defineProps, defineEmit } from 'vue'
           const bar = 1
-          defineOptions({
-            props: {
-              foo: {
-                default: bar => bar + 1
-              }
+          defineProps({
+            foo: {
+              default: bar => bar + 1
             }
           })
+          defineEmit({
+            foo: bar => bar > 1
+          })
         </script>`).content
       )
     })
 
-    test('should allow defineOptions() referencing imported binding', () => {
+    test('should allow defineProps/Emit() referencing imported binding', () => {
       assertCode(
         compile(`<script setup>
-          import { defineOptions } from 'vue'
-          import { bar } from './bar'
-          defineOptions({
-            props: {
-              foo: {
-                default: () => bar
-              }
-            }
-          })
+        import { defineProps, defineEmit } from 'vue'
+        import { bar } from './bar'
+        defineProps({
+          foo: {
+            default: () => bar
+          }
+        })
+        defineEmit({
+          foo: () => bar > 1
+        })
         </script>`).content
       )
     })
@@ -1063,11 +1096,9 @@ describe('SFC analyze <script> bindings', () => {
   it('works for script setup', () => {
     const { bindings } = compile(`
       <script setup>
-      import { defineOptions, ref as r } from 'vue'
-      defineOptions({
-        props: {
-          foo: String,
-        }
+      import { defineProps, ref as r } from 'vue'
+      defineProps({
+        foo: String
       })
 
       const a = r(1)
index 53e6408090b36c18e3d70f158cbb4a157d31afd7..d9d41ecce8ec94c59292373e3f14a5280e48d862 100644 (file)
@@ -20,13 +20,11 @@ describe('CSS vars injection', () => {
   test('w/ <script setup> binding analysis', () => {
     const { content } = compileSFCScript(
       `<script setup>
-        import { defineOptions, ref } from 'vue'
+        import { defineProps, ref } from 'vue'
         const color = 'red'
         const size = ref('10px')
-        defineOptions({
-          props: {
-            foo: String
-          }
+        defineProps({
+          foo: String
         })
         </script>\n` +
         `<style>
index 2e06b5fd6ccab08d9df0bf339bbede699381113c..c2080b84cb90edbfd6fd0e7da917e78edea23445 100644 (file)
@@ -29,7 +29,8 @@ import { CSS_VARS_HELPER, genCssVarsCode, injectCssVarsCalls } from './cssVars'
 import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
 import { warnExperimental, warnOnce } from './warn'
 
-const DEFINE_OPTIONS = 'defineOptions'
+const DEFINE_PROPS = 'defineProps'
+const DEFINE_EMIT = 'defineEmit'
 
 export interface SFCScriptCompileOptions {
   /**
@@ -162,17 +163,16 @@ export function compileScript(
   const refIdentifiers: Set<Identifier> = new Set()
   const enableRefSugar = options.refSugar !== false
   let defaultExport: Node | undefined
-  let hasOptionsCall = false
-  let optionsExp: string | undefined
-  let optionsArg: ObjectExpression | undefined
-  let optionsType: TSTypeLiteral | undefined
+  let hasDefinePropsCall = false
+  let hasDefineEmitCall = false
+  let propsRuntimeDecl: Node | undefined
+  let propsTypeDecl: TSTypeLiteral | undefined
+  let propsIdentifier: string | undefined
+  let emitRuntimeDecl: Node | undefined
+  let emitTypeDecl: TSFunctionType | TSUnionType | undefined
+  let emitIdentifier: string | undefined
   let hasAwait = false
   let hasInlinedSsrRenderFn = false
-  // context types to generate
-  let propsType = `{}`
-  let emitType = `(e: string, ...args: any[]) => void`
-  let slotsType = `Slots`
-  let attrsType = `Record<string, any>`
   // props/emits declared via types
   const typeDeclaredProps: Record<string, PropTypeData> = {}
   const typeDeclaredEmits: Set<string> = new Set()
@@ -236,38 +236,62 @@ export function compileScript(
     }
   }
 
-  function processDefineOptions(node: Node): boolean {
-    if (isCallOf(node, DEFINE_OPTIONS)) {
-      if (hasOptionsCall) {
-        error(`duplicate ${DEFINE_OPTIONS}() call`, node)
+  function processDefineProps(node: Node): boolean {
+    if (isCallOf(node, DEFINE_PROPS)) {
+      if (hasDefinePropsCall) {
+        error(`duplicate ${DEFINE_PROPS}() call`, node)
       }
-      hasOptionsCall = true
-      const optsArg = node.arguments[0]
-      if (optsArg) {
-        if (optsArg.type === 'ObjectExpression') {
-          optionsArg = optsArg
+      hasDefinePropsCall = true
+      propsRuntimeDecl = node.arguments[0]
+      // context call has type parameters - infer runtime types from it
+      if (node.typeParameters) {
+        if (propsRuntimeDecl) {
+          error(
+            `${DEFINE_PROPS}() 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') {
+          propsTypeDecl = typeArg
         } else {
           error(
-            `${DEFINE_OPTIONS}() argument must be an object literal.`,
-            optsArg
+            `type argument passed to ${DEFINE_PROPS}() must be a literal type.`,
+            typeArg
           )
         }
       }
-      // context call has type parameters - infer runtime types from it
+      return true
+    }
+    return false
+  }
+
+  function processDefineEmit(node: Node): boolean {
+    if (isCallOf(node, DEFINE_EMIT)) {
+      if (hasDefineEmitCall) {
+        error(`duplicate ${DEFINE_EMIT}() call`, node)
+      }
+      hasDefineEmitCall = true
+      emitRuntimeDecl = node.arguments[0]
       if (node.typeParameters) {
-        if (optionsArg) {
+        if (emitRuntimeDecl) {
           error(
-            `${DEFINE_OPTIONS}() cannot accept both type and non-type arguments ` +
+            `${DEFINE_EMIT}() 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') {
-          optionsType = typeArg
+        if (
+          typeArg.type === 'TSFunctionType' ||
+          typeArg.type === 'TSUnionType'
+        ) {
+          emitTypeDecl = typeArg
         } else {
           error(
-            `type argument passed to ${DEFINE_OPTIONS}() must be a literal type.`,
+            `type argument passed to ${DEFINE_EMIT}() must be a function type ` +
+              `or a union of function types.`,
             typeArg
           )
         }
@@ -277,6 +301,22 @@ export function compileScript(
     return false
   }
 
+  function checkInvalidScopeReference(node: Node | undefined, method: string) {
+    if (!node) return
+    walkIdentifiers(node, id => {
+      if (setupBindings[id.name]) {
+        error(
+          `\`${method}()\` in <script setup> cannot reference locally ` +
+            `declared variables because it will be hoisted outside of the ` +
+            `setup() function. If your component options requires initialization ` +
+            `in the module scope, use a separate normal <script> to export ` +
+            `the options instead.`,
+          id
+        )
+      }
+    })
+  }
+
   function processRefExpression(exp: Expression, statement: LabeledStatement) {
     if (exp.type === 'AssignmentExpression') {
       const { left, right } = exp
@@ -562,7 +602,10 @@ export function compileScript(
           specifier.imported.name
         const source = node.source.value
         const existing = userImports[local]
-        if (source === 'vue' && imported === DEFINE_OPTIONS) {
+        if (
+          source === 'vue' &&
+          (imported === DEFINE_PROPS || imported === DEFINE_EMIT)
+        ) {
           removeSpecifier(specifier)
         } else if (existing) {
           if (existing.source === source && existing.imported === imported) {
@@ -585,22 +628,37 @@ export function compileScript(
       }
     }
 
+    // process `defineProps` and `defineEmit` calls
     if (
       node.type === 'ExpressionStatement' &&
-      processDefineOptions(node.expression)
+      (processDefineProps(node.expression) ||
+        processDefineEmit(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 && processDefineOptions(decl.init)) {
-          optionsExp = scriptSetup.content.slice(decl.id.start!, decl.id.end!)
-          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) {
+          const isDefineProps = processDefineProps(decl.init)
+          if (isDefineProps) {
+            propsIdentifier = scriptSetup.content.slice(
+              decl.id.start!,
+              decl.id.end!
+            )
           }
+          const isDefineEmit = processDefineEmit(decl.init)
+          if (isDefineEmit) {
+            emitIdentifier = scriptSetup.content.slice(
+              decl.id.start!,
+              decl.id.end!
+            )
+          }
+          if (isDefineProps || isDefineEmit)
+            if (node.declarations.length === 1) {
+              s.remove(node.start! + startOffset, node.end! + startOffset)
+            } else {
+              s.remove(decl.start! + startOffset, decl.end! + startOffset)
+            }
         }
       }
     }
@@ -691,61 +749,17 @@ export function compileScript(
   }
 
   // 4. extract runtime props/emits code from setup context type
-  if (optionsType) {
-    for (const m of optionsType.members) {
-      if (m.type === 'TSPropertySignature' && m.key.type === 'Identifier') {
-        const typeNode = m.typeAnnotation!.typeAnnotation
-        const typeString = scriptSetup.content.slice(
-          typeNode.start!,
-          typeNode.end!
-        )
-        const key = m.key.name
-        if (key === 'props') {
-          propsType = typeString
-          if (typeNode.type === 'TSTypeLiteral') {
-            extractRuntimeProps(typeNode, typeDeclaredProps, declaredTypes)
-          } else {
-            // TODO be able to trace references
-            error(`props type must be an object literal type`, typeNode)
-          }
-        } else if (key === 'emit') {
-          emitType = typeString
-          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 (key === 'attrs') {
-          attrsType = typeString
-        } else if (key === 'slots') {
-          slotsType = typeString
-        } else {
-          error(`invalid setup context property: "${key}"`, m.key)
-        }
-      }
-    }
+  if (propsTypeDecl) {
+    extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
+  }
+  if (emitTypeDecl) {
+    extractRuntimeEmits(emitTypeDecl, typeDeclaredEmits)
   }
 
   // 5. check useOptions args to make sure it doesn't reference setup scope
   // variables
-  if (optionsArg) {
-    walkIdentifiers(optionsArg, id => {
-      if (setupBindings[id.name]) {
-        error(
-          `\`${DEFINE_OPTIONS}()\` in <script setup> cannot reference locally ` +
-            `declared variables because it will be hoisted outside of the ` +
-            `setup() function. If your component options requires initialization ` +
-            `in the module scope, use a separate normal <script> to export ` +
-            `the options instead.`,
-          id
-        )
-      }
-    })
-  }
+  checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
+  checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS)
 
   // 6. remove non-script content
   if (script) {
@@ -766,31 +780,17 @@ export function compileScript(
     s.remove(endOffset, source.length)
   }
 
-  // 7. finalize setup argument signature.
-  let args = optionsExp ? `__props, ${optionsExp}` : `__props`
-  if (optionsExp && optionsType) {
-    if (slotsType === 'Slots') {
-      helperImports.add('Slots')
-    }
-    args += `: {
-  props: ${propsType},
-  emit: ${emitType},
-  slots: ${slotsType},
-  attrs: ${attrsType}
-}`
-  }
-
-  // 8. analyze binding metadata
+  // 7. analyze binding metadata
   if (scriptAst) {
     Object.assign(bindingMetadata, analyzeScriptBindings(scriptAst))
   }
-  if (optionsType) {
-    for (const key in typeDeclaredProps) {
+  if (propsRuntimeDecl) {
+    for (const key of getObjectOrArrayExpressionKeys(propsRuntimeDecl)) {
       bindingMetadata[key] = BindingTypes.PROPS
     }
   }
-  if (optionsArg) {
-    Object.assign(bindingMetadata, analyzeBindingsFromOptions(optionsArg))
+  for (const key in typeDeclaredProps) {
+    bindingMetadata[key] = BindingTypes.PROPS
   }
   for (const [key, { isType, source }] of Object.entries(userImports)) {
     if (isType) continue
@@ -803,7 +803,7 @@ export function compileScript(
     bindingMetadata[key] = setupBindings[key]
   }
 
-  // 9. inject `useCssVars` calls
+  // 8. inject `useCssVars` calls
   if (cssVars.length) {
     helperImports.add(CSS_VARS_HELPER)
     helperImports.add('unref')
@@ -818,6 +818,35 @@ export function compileScript(
     )
   }
 
+  // 9. finalize setup() argument signature
+  let args = `__props`
+  if (propsTypeDecl) {
+    args += `: ${scriptSetup.content.slice(
+      propsTypeDecl.start!,
+      propsTypeDecl.end!
+    )}`
+  }
+  // inject user assignment of props
+  // we use a default __props so that template expressions referencing props
+  // can use it directly
+  if (propsIdentifier) {
+    s.prependRight(startOffset, `\nconst ${propsIdentifier} = __props`)
+  }
+  if (emitIdentifier) {
+    args +=
+      emitIdentifier === `emit` ? `, { emit }` : `, { emit: ${emitIdentifier} }`
+    if (emitTypeDecl) {
+      args += `: {
+        emit: (${scriptSetup.content.slice(
+          emitTypeDecl.start!,
+          emitTypeDecl.end!
+        )}),
+        slots: any,
+        attrs: any
+      }`
+    }
+  }
+
   // 10. generate return statement
   let returned
   if (options.inlineTemplate) {
@@ -896,13 +925,19 @@ export function compileScript(
   if (hasInlinedSsrRenderFn) {
     runtimeOptions += `\n  __ssrInlineRender: true,`
   }
-  if (optionsArg) {
-    runtimeOptions += `\n  ${scriptSetup.content
-      .slice(optionsArg.start! + 1, optionsArg.end! - 1)
+  if (propsRuntimeDecl) {
+    runtimeOptions += `\n  props: ${scriptSetup.content
+      .slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
+      .trim()},`
+  } else if (propsTypeDecl) {
+    runtimeOptions += genRuntimeProps(typeDeclaredProps)
+  }
+  if (emitRuntimeDecl) {
+    runtimeOptions += `\n  emits: ${scriptSetup.content
+      .slice(emitRuntimeDecl.start!, emitRuntimeDecl.end!)
       .trim()},`
-  } else if (optionsType) {
-    runtimeOptions +=
-      genRuntimeProps(typeDeclaredProps) + genRuntimeEmits(typeDeclaredEmits)
+  } else if (emitTypeDecl) {
+    runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
   }
   if (isTS) {
     // for TS, make sure the exported type is still valid type with
@@ -975,13 +1010,16 @@ function walkDeclaration(
     const isConst = node.kind === 'const'
     // export const foo = ...
     for (const { id, init } of node.declarations) {
-      const isUseOptionsCall = !!(isConst && isCallOf(init, DEFINE_OPTIONS))
+      const isDefineCall = !!(
+        isConst &&
+        (isCallOf(init, DEFINE_PROPS) || isCallOf(init, DEFINE_EMIT))
+      )
       if (id.type === 'Identifier') {
         let bindingType
         if (
           // if a declaration is a const literal, we can mark it so that
           // the generated render fn code doesn't need to unref() it
-          isUseOptionsCall ||
+          isDefineCall ||
           (isConst &&
             canNeverBeRef(init!, userImportAlias['reactive'] || 'reactive'))
         ) {
@@ -997,9 +1035,9 @@ function walkDeclaration(
         }
         bindings[id.name] = bindingType
       } else if (id.type === 'ObjectPattern') {
-        walkObjectPattern(id, bindings, isConst, isUseOptionsCall)
+        walkObjectPattern(id, bindings, isConst, isDefineCall)
       } else if (id.type === 'ArrayPattern') {
-        walkArrayPattern(id, bindings, isConst, isUseOptionsCall)
+        walkArrayPattern(id, bindings, isConst, isDefineCall)
       }
     }
   } else if (
@@ -1016,7 +1054,7 @@ function walkObjectPattern(
   node: ObjectPattern,
   bindings: Record<string, BindingTypes>,
   isConst: boolean,
-  isUseOptionsCall = false
+  isDefineCall = false
 ) {
   for (const p of node.properties) {
     if (p.type === 'ObjectProperty') {
@@ -1024,13 +1062,13 @@ function walkObjectPattern(
       if (p.key.type === 'Identifier') {
         if (p.key === p.value) {
           // const { x } = ...
-          bindings[p.key.name] = isUseOptionsCall
+          bindings[p.key.name] = isDefineCall
             ? BindingTypes.SETUP_CONST
             : isConst
               ? BindingTypes.SETUP_MAYBE_REF
               : BindingTypes.SETUP_LET
         } else {
-          walkPattern(p.value, bindings, isConst, isUseOptionsCall)
+          walkPattern(p.value, bindings, isConst, isDefineCall)
         }
       }
     } else {
@@ -1047,10 +1085,10 @@ function walkArrayPattern(
   node: ArrayPattern,
   bindings: Record<string, BindingTypes>,
   isConst: boolean,
-  isUseOptionsCall = false
+  isDefineCall = false
 ) {
   for (const e of node.elements) {
-    e && walkPattern(e, bindings, isConst, isUseOptionsCall)
+    e && walkPattern(e, bindings, isConst, isDefineCall)
   }
 }
 
@@ -1058,10 +1096,10 @@ function walkPattern(
   node: Node,
   bindings: Record<string, BindingTypes>,
   isConst: boolean,
-  isUseOptionsCall = false
+  isDefineCall = false
 ) {
   if (node.type === 'Identifier') {
-    bindings[node.name] = isUseOptionsCall
+    bindings[node.name] = isDefineCall
       ? BindingTypes.SETUP_CONST
       : isConst
         ? BindingTypes.SETUP_MAYBE_REF
@@ -1077,7 +1115,7 @@ function walkPattern(
     walkArrayPattern(node, bindings, isConst)
   } else if (node.type === 'AssignmentPattern') {
     if (node.left.type === 'Identifier') {
-      bindings[node.left.name] = isUseOptionsCall
+      bindings[node.left.name] = isDefineCall
         ? BindingTypes.SETUP_CONST
         : isConst
           ? BindingTypes.SETUP_MAYBE_REF
@@ -1418,43 +1456,6 @@ function isFunction(node: Node): node is FunctionNode {
   return /Function(?:Expression|Declaration)$|Method$/.test(node.type)
 }
 
-function getObjectExpressionKeys(node: ObjectExpression): string[] {
-  const keys = []
-  for (const prop of node.properties) {
-    if (
-      (prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
-      !prop.computed
-    ) {
-      if (prop.key.type === 'Identifier') {
-        keys.push(prop.key.name)
-      } else if (prop.key.type === 'StringLiteral') {
-        keys.push(prop.key.value)
-      }
-    }
-  }
-  return keys
-}
-
-function getArrayExpressionKeys(node: ArrayExpression): string[] {
-  const keys = []
-  for (const element of node.elements) {
-    if (element && element.type === 'StringLiteral') {
-      keys.push(element.value)
-    }
-  }
-  return keys
-}
-
-function getObjectOrArrayExpressionKeys(property: ObjectProperty): string[] {
-  if (property.value.type === 'ArrayExpression') {
-    return getArrayExpressionKeys(property.value)
-  }
-  if (property.value.type === 'ObjectExpression') {
-    return getObjectExpressionKeys(property.value)
-  }
-  return []
-}
-
 function isCallOf(node: Node | null, name: string): node is CallExpression {
   return !!(
     node &&
@@ -1542,7 +1543,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
       if (property.key.name === 'props') {
         // props: ['foo']
         // props: { foo: ... }
-        for (const key of getObjectOrArrayExpressionKeys(property)) {
+        for (const key of getObjectOrArrayExpressionKeys(property.value)) {
           bindings[key] = BindingTypes.PROPS
         }
       }
@@ -1551,7 +1552,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
       else if (property.key.name === 'inject') {
         // inject: ['foo']
         // inject: { foo: {} }
-        for (const key of getObjectOrArrayExpressionKeys(property)) {
+        for (const key of getObjectOrArrayExpressionKeys(property.value)) {
           bindings[key] = BindingTypes.OPTIONS
         }
       }
@@ -1599,3 +1600,40 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
 
   return bindings
 }
+
+function getObjectExpressionKeys(node: ObjectExpression): string[] {
+  const keys = []
+  for (const prop of node.properties) {
+    if (
+      (prop.type === 'ObjectProperty' || prop.type === 'ObjectMethod') &&
+      !prop.computed
+    ) {
+      if (prop.key.type === 'Identifier') {
+        keys.push(prop.key.name)
+      } else if (prop.key.type === 'StringLiteral') {
+        keys.push(prop.key.value)
+      }
+    }
+  }
+  return keys
+}
+
+function getArrayExpressionKeys(node: ArrayExpression): string[] {
+  const keys = []
+  for (const element of node.elements) {
+    if (element && element.type === 'StringLiteral') {
+      keys.push(element.value)
+    }
+  }
+  return keys
+}
+
+function getObjectOrArrayExpressionKeys(value: Node): string[] {
+  if (value.type === 'ArrayExpression') {
+    return getArrayExpressionKeys(value)
+  }
+  if (value.type === 'ObjectExpression') {
+    return getObjectExpressionKeys(value)
+  }
+  return []
+}
diff --git a/packages/runtime-core/src/apiDefineOptions.ts b/packages/runtime-core/src/apiDefineOptions.ts
deleted file mode 100644 (file)
index 624b116..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-import { EmitFn, EmitsOptions } from './componentEmits'
-import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
-import { Slots } from './componentSlots'
-import { Directive } from './directives'
-import { warn } from './warning'
-
-interface DefaultContext {
-  props: {}
-  attrs: Record<string, unknown>
-  emit: (...args: any[]) => void
-  slots: Slots
-}
-
-interface InferredContext<P, E> {
-  props: Readonly<P>
-  attrs: Record<string, unknown>
-  emit: EmitFn<E>
-  slots: Slots
-}
-
-type InferContext<T extends Partial<DefaultContext>, P, E> = {
-  [K in keyof DefaultContext]: T[K] extends {} ? T[K] : InferredContext<P, E>[K]
-}
-
-/**
- * This is a subset of full options that are still useful in the context of
- * <script setup>. Technically, other options can be used too, but are
- * discouraged - if using TypeScript, we nudge users away from doing so by
- * disallowing them in types.
- */
-interface Options<E extends EmitsOptions, EE extends string> {
-  emits?: E | EE[]
-  name?: string
-  inhertiAttrs?: boolean
-  directives?: Record<string, Directive>
-}
-
-/**
- * Compile-time-only helper used for declaring options and retrieving props
- * and the setup context inside `<script setup>`.
- * This is stripped away in the compiled code and should never be actually
- * called at runtime.
- */
-// overload 1: no props
-export function defineOptions<
-  T extends Partial<DefaultContext> = {},
-  E extends EmitsOptions = EmitsOptions,
-  EE extends string = string
->(
-  options?: Options<E, EE> & {
-    props?: undefined
-  }
-): InferContext<T, {}, E>
-
-// overload 2: object props
-export function defineOptions<
-  T extends Partial<DefaultContext> = {},
-  E extends EmitsOptions = EmitsOptions,
-  EE extends string = string,
-  PP extends string = string,
-  P = Readonly<{ [key in PP]?: any }>
->(
-  options?: Options<E, EE> & {
-    props?: PP[]
-  }
-): InferContext<T, P, E>
-
-// overload 3: object props
-export function defineOptions<
-  T extends Partial<DefaultContext> = {},
-  E extends EmitsOptions = EmitsOptions,
-  EE extends string = string,
-  PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
-  P = ExtractPropTypes<PP>
->(
-  options?: Options<E, EE> & {
-    props?: PP
-  }
-): InferContext<T, P, E>
-
-// implementation
-export function defineOptions() {
-  if (__DEV__) {
-    warn(
-      `defineContext() is a compiler-hint helper that is only usable inside ` +
-        `<script setup> of a single file component. It will be compiled away ` +
-        `and should not be used in final distributed code.`
-    )
-  }
-  return 0 as any
-}
diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts
new file mode 100644 (file)
index 0000000..31130aa
--- /dev/null
@@ -0,0 +1,60 @@
+import { shallowReadonly } from '@vue/reactivity'
+import { getCurrentInstance, SetupContext } from './component'
+import { EmitFn, EmitsOptions } from './componentEmits'
+import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
+import { warn } from './warning'
+
+/**
+ * Compile-time-only helper used for declaring props inside `<script setup>`.
+ * This is stripped away in the compiled code and should never be actually
+ * called at runtime.
+ */
+// overload 1: string props
+export function defineProps<
+  TypeProps = undefined,
+  PropNames extends string = string,
+  InferredProps = { [key in PropNames]?: any }
+>(
+  props?: PropNames[]
+): Readonly<TypeProps extends undefined ? InferredProps : TypeProps>
+// overload 2: object props
+export function defineProps<
+  TypeProps = undefined,
+  PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
+  InferredProps = ExtractPropTypes<PP>
+>(props?: PP): Readonly<TypeProps extends undefined ? InferredProps : TypeProps>
+// implementation
+export function defineProps(props?: any) {
+  if (__DEV__ && props) {
+    warn(
+      `defineProps() is a compiler-hint helper that is only usable inside ` +
+        `<script setup> of a single file component. Its arguments should be ` +
+        `compiled away and passing it at runtime has no effect.`
+    )
+  }
+  return __DEV__
+    ? shallowReadonly(getCurrentInstance()!.props)
+    : getCurrentInstance()!.props
+}
+
+export function defineEmit<
+  TypeEmit = undefined,
+  E extends EmitsOptions = EmitsOptions,
+  EE extends string = string,
+  InferredEmit = EmitFn<E>
+>(emitOptions?: E | EE[]): TypeEmit extends undefined ? InferredEmit : TypeEmit
+// implementation
+export function defineEmit(emitOptions?: any) {
+  if (__DEV__ && emitOptions) {
+    warn(
+      `defineEmit() is a compiler-hint helper that is only usable inside ` +
+        `<script setup> of a single file component. Its arguments should be ` +
+        `compiled away and passing it at runtime has no effect.`
+    )
+  }
+  return getCurrentInstance()!.emit
+}
+
+export function useContext(): SetupContext {
+  return getCurrentInstance()!.setupContext!
+}
index a29a0aeaf85da7130020e797b83096615cb912b6..2261dd81c90c671dc3dbe00b77b362ee328e9a4a 100644 (file)
@@ -105,7 +105,7 @@ export interface ComponentInternalOptions {
 export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
   extends ComponentInternalOptions {
   // use of any here is intentional so it can be a valid JSX Element constructor
-  (props: P, ctx: Omit<SetupContext<E, P>, 'expose'>): any
+  (props: P, ctx: Omit<SetupContext<E>, 'expose'>): any
   props?: ComponentPropsOptions<P>
   emits?: E | (keyof E)[]
   inheritAttrs?: boolean
@@ -167,8 +167,7 @@ export const enum LifecycleHooks {
   ERROR_CAPTURED = 'ec'
 }
 
-export interface SetupContext<E = EmitsOptions, P = Data> {
-  props: P
+export interface SetupContext<E = EmitsOptions> {
   attrs: Data
   slots: Slots
   emit: EmitFn<E>
@@ -775,7 +774,6 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
     })
   } else {
     return {
-      props: instance.props,
       attrs: instance.attrs,
       slots: instance.slots,
       emit: instance.emit,
index 52ec1004bd8ea9c79c9e773fa196673a47a1a30c..89bd8c060536cc0b922a7264c76ef33a71db6e45 100644 (file)
@@ -98,7 +98,7 @@ export interface ComponentOptionsBase<
   setup?: (
     this: void,
     props: Props,
-    ctx: SetupContext<E, Props>
+    ctx: SetupContext<E>
   ) => Promise<RawBindings> | RawBindings | RenderFunction | void
   name?: string
   template?: string | object // can be a direct DOM node
index bd8521415ff3bee4ffedbbf09047acbe94450099..9c5ecaf5b68dbf583f16388e3639c9ef66e0069a 100644 (file)
@@ -95,7 +95,6 @@ export function renderComponentRoot(
               props,
               __DEV__
                 ? {
-                    props,
                     get attrs() {
                       markAttrsAccessed()
                       return attrs
@@ -103,7 +102,7 @@ export function renderComponentRoot(
                     slots,
                     emit
                   }
-                : { props, attrs, slots, emit }
+                : { attrs, slots, emit }
             )
           : render(props, null as any /* we know it doesn't need it */)
       )
index 828850279ecf598063f1ba200fba486f3b076571..818caa597e6ce9de4328e14c9611d46180bbe8e0 100644 (file)
@@ -43,7 +43,7 @@ export { provide, inject } from './apiInject'
 export { nextTick } from './scheduler'
 export { defineComponent } from './apiDefineComponent'
 export { defineAsyncComponent } from './apiAsyncComponent'
-export { defineOptions } from './apiDefineOptions'
+export { defineProps, defineEmit, useContext } from './apiSetupHelpers'
 
 // Advanced API ----------------------------------------------------------------
 
diff --git a/test-dts/defineOptions.test-d.ts b/test-dts/defineOptions.test-d.ts
deleted file mode 100644 (file)
index d1125db..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-import { expectType, defineOptions, Slots, describe } from './index'
-
-describe('no args', () => {
-  const { props, attrs, emit, slots } = defineOptions()
-  expectType<{}>(props)
-  expectType<Record<string, unknown>>(attrs)
-  expectType<(...args: any[]) => void>(emit)
-  expectType<Slots>(slots)
-
-  // @ts-expect-error
-  props.foo
-  // should be able to emit anything
-  emit('foo')
-  emit('bar')
-})
-
-describe('with type arg', () => {
-  const { props, attrs, emit, slots } = defineOptions<{
-    props: {
-      foo: string
-    }
-    emit: (e: 'change') => void
-  }>()
-
-  // explicitly declared type should be refined
-  expectType<string>(props.foo)
-  // @ts-expect-error
-  props.bar
-
-  emit('change')
-  // @ts-expect-error
-  emit()
-  // @ts-expect-error
-  emit('bar')
-
-  // non explicitly declared type should fallback to default type
-  expectType<Record<string, unknown>>(attrs)
-  expectType<Slots>(slots)
-})
-
-// with runtime arg
-describe('with runtime arg (array syntax)', () => {
-  const { props, emit } = defineOptions({
-    props: ['foo', 'bar'],
-    emits: ['foo', 'bar']
-  })
-
-  expectType<{
-    foo?: any
-    bar?: any
-  }>(props)
-  // @ts-expect-error
-  props.baz
-
-  emit('foo')
-  emit('bar', 123)
-  // @ts-expect-error
-  emit('baz')
-})
-
-describe('with runtime arg (object syntax)', () => {
-  const { props, emit } = defineOptions({
-    props: {
-      foo: String,
-      bar: {
-        type: Number,
-        default: 1
-      },
-      baz: {
-        type: Array,
-        required: true
-      }
-    },
-    emits: {
-      foo: () => {},
-      bar: null
-    }
-  })
-
-  expectType<{
-    foo?: string
-    bar: number
-    baz: unknown[]
-  }>(props)
-
-  props.foo && props.foo + 'bar'
-  props.bar + 1
-  // @ts-expect-error should be readonly
-  props.bar++
-  props.baz.push(1)
-
-  emit('foo')
-  emit('bar')
-  // @ts-expect-error
-  emit('baz')
-})
diff --git a/test-dts/setupHelpers.test-d.ts b/test-dts/setupHelpers.test-d.ts
new file mode 100644 (file)
index 0000000..09ced2d
--- /dev/null
@@ -0,0 +1,89 @@
+import {
+  expectType,
+  defineProps,
+  defineEmit,
+  useContext,
+  Slots,
+  describe
+} from './index'
+
+describe('defineProps w/ type declaration', () => {
+  // type declaration
+  const props = defineProps<{
+    foo: string
+  }>()
+  // explicitly declared type should be refined
+  expectType<string>(props.foo)
+  // @ts-expect-error
+  props.bar
+})
+
+describe('defineProps w/ runtime declaration', () => {
+  // runtime declaration
+  const props = defineProps({
+    foo: String,
+    bar: {
+      type: Number,
+      default: 1
+    },
+    baz: {
+      type: Array,
+      required: true
+    }
+  })
+  expectType<{
+    foo?: string
+    bar: number
+    baz: unknown[]
+  }>(props)
+
+  props.foo && props.foo + 'bar'
+  props.bar + 1
+  // @ts-expect-error should be readonly
+  props.bar++
+  props.baz.push(1)
+
+  const props2 = defineProps(['foo', 'bar'])
+  props2.foo + props2.bar
+  // @ts-expect-error
+  props2.baz
+})
+
+describe('defineEmit w/ type declaration', () => {
+  const emit = defineEmit<(e: 'change') => void>()
+  emit('change')
+  // @ts-expect-error
+  emit()
+  // @ts-expect-error
+  emit('bar')
+})
+
+describe('defineEmit w/ runtime declaration', () => {
+  const emit = defineEmit({
+    foo: () => {},
+    bar: null
+  })
+  emit('foo')
+  emit('bar', 123)
+  // @ts-expect-error
+  emit('baz')
+
+  const emit2 = defineEmit(['foo', 'bar'])
+  emit2('foo')
+  emit2('bar', 123)
+  // @ts-expect-error
+  emit2('baz')
+})
+
+describe('useContext', () => {
+  const { attrs, emit, slots } = useContext()
+  expectType<Record<string, unknown>>(attrs)
+  expectType<(...args: any[]) => void>(emit)
+  expectType<Slots>(slots)
+
+  // @ts-expect-error
+  props.foo
+  // should be able to emit anything
+  emit('foo')
+  emit('bar')
+})