]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(sfc): introduce `defineModel` macro and `useModel` helper (#8018)
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Sat, 8 Apr 2023 04:13:05 +0000 (12:13 +0800)
committerGitHub <noreply@github.com>
Sat, 8 Apr 2023 04:13:05 +0000 (12:13 +0800)
12 files changed:
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/__snapshots__/compileScriptPropsDestructure.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/dts-test/setupHelpers.test-d.ts
packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
packages/runtime-core/src/apiSetupHelpers.ts
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/index.ts
packages/runtime-core/types/scriptSetupHelpers.d.ts
packages/sfc-playground/src/App.vue

index 23a3741afb2810e5cc04e8f290fa3472c8721b6c..f6136eb1bac66f008d9c4dfb9a105ce219b4a77c 100644 (file)
@@ -653,6 +653,100 @@ return {  }
 }"
 `;
 
+exports[`SFC compile <script setup> > defineModel() > basic usage 1`] = `
+"import { useModel as _useModel } from 'vue'
+
+export default {
+  props: {
+    \\"modelValue\\": { required: true },
+    \\"count\\": {},
+  },
+  emits: [\\"update:modelValue\\", \\"update:count\\"],
+  setup(__props, { expose: __expose }) {
+  __expose();
+
+        const modelValue = _useModel(__props, \\"modelValue\\")
+        const c = _useModel(__props, \\"count\\")
+        
+return { modelValue, c }
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > defineModel() > w/ array props 1`] = `
+"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'
+
+export default {
+  props: _mergeModels(['foo', 'bar'], {
+    \\"count\\": {},
+  }),
+  emits: [\\"update:count\\"],
+  setup(__props, { expose: __expose }) {
+  __expose();
+
+        
+        const count = _useModel(__props, \\"count\\")
+        
+return { count }
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > defineModel() > w/ defineProps and defineEmits 1`] = `
+"import { useModel as _useModel, mergeModels as _mergeModels } from 'vue'
+
+export default {
+  props: _mergeModels({ foo: String }, {
+    \\"modelValue\\": { default: 0 },
+  }),
+  emits: _mergeModels(['change'], [\\"update:modelValue\\"]),
+  setup(__props, { expose: __expose }) {
+  __expose();
+
+        
+        
+        const count = _useModel(__props, \\"modelValue\\")
+        
+return { count }
+}
+
+}"
+`;
+
+exports[`SFC compile <script setup> > defineModel() > w/ local flag 1`] = `
+"import { useModel as _useModel } from 'vue'
+const local = true
+        
+export default {
+  props: {
+    \\"modelValue\\": { local: true, default: 1 },
+    \\"bar\\": { [key]: true },
+    \\"baz\\": { ...x },
+    \\"qux\\": x,
+    \\"foo2\\": { local: true, ...x },
+    \\"hoist\\": { local },
+  },
+  emits: [\\"update:modelValue\\", \\"update:bar\\", \\"update:baz\\", \\"update:qux\\", \\"update:foo2\\", \\"update:hoist\\"],
+  setup(__props, { expose: __expose }) {
+  __expose();
+
+        const foo = _useModel(__props, \\"modelValue\\", { local: true })
+        const bar = _useModel(__props, \\"bar\\", { [key]: true })
+        const baz = _useModel(__props, \\"baz\\", { ...x })
+        const qux = _useModel(__props, \\"qux\\", x)
+
+        const foo2 = _useModel(__props, \\"foo2\\", { local: true })
+
+        const hoist = _useModel(__props, \\"hoist\\", { local })
+        
+return { foo, bar, baz, qux, foo2, local, hoist }
+}
+
+}"
+`;
+
 exports[`SFC compile <script setup> > defineOptions() > basic usage 1`] = `
 "export default /*#__PURE__*/Object.assign({ name: 'FooApp' }, {
   setup(__props, { expose: __expose }) {
@@ -1596,6 +1690,58 @@ return { emit }
 })"
 `;
 
+exports[`SFC compile <script setup> > with TypeScript > defineModel() > basic usage 1`] = `
+"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+  props: {
+    \\"modelValue\\": { type: [Boolean, String] },
+    \\"count\\": { type: Number },
+    \\"disabled\\": { type: Number, ...{ required: false } },
+    \\"any\\": { type: Boolean, skipCheck: true },
+  },
+  emits: [\\"update:modelValue\\", \\"update:count\\", \\"update:disabled\\", \\"update:any\\"],
+  setup(__props, { expose: __expose }) {
+  __expose();
+
+          const modelValue = _useModel(__props, \\"modelValue\\")
+          const count = _useModel(__props, \\"count\\")
+          const disabled = _useModel(__props, \\"disabled\\")
+          const any = _useModel(__props, \\"any\\")
+          
+return { modelValue, count, disabled, any }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> > with TypeScript > defineModel() > w/ production mode 1`] = `
+"import { useModel as _useModel, defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+  props: {
+    \\"modelValue\\": { type: Boolean },
+    \\"fn\\": {},
+    \\"fnWithDefault\\": { type: Function, ...{ default: () => null } },
+    \\"str\\": {},
+    \\"optional\\": { required: false },
+  },
+  emits: [\\"update:modelValue\\", \\"update:fn\\", \\"update:fnWithDefault\\", \\"update:str\\", \\"update:optional\\"],
+  setup(__props, { expose: __expose }) {
+  __expose();
+
+          const modelValue = _useModel(__props, \\"modelValue\\")
+          const fn = _useModel(__props, \\"fn\\")
+          const fnWithDefault = _useModel(__props, \\"fnWithDefault\\")
+          const str = _useModel(__props, \\"str\\")
+          const optional = _useModel(__props, \\"optional\\")
+          
+return { modelValue, fn, fnWithDefault, str, optional }
+}
+
+})"
+`;
+
 exports[`SFC compile <script setup> > with TypeScript > defineProps w/ TS assertion 1`] = `
 "import { defineComponent as _defineComponent } from 'vue'
 
@@ -1961,7 +2107,7 @@ export default /*#__PURE__*/_defineComponent({
     foo: { type: Function },
     bar: { type: Boolean },
     baz: { type: [Boolean, Function] },
-    qux: null
+    qux: {}
   }, { ...defaults }),
   setup(__props: any, { expose: __expose }) {
   __expose();
@@ -2063,7 +2209,7 @@ exports[`SFC compile <script setup> > with TypeScript > withDefaults (static) w/
 
 export default /*#__PURE__*/_defineComponent({
   props: {
-    foo: null,
+    foo: {},
     bar: { type: Boolean },
     baz: { type: [Boolean, Function], default: true },
     qux: { default: 'hi' }
index 525f4b6edb031d425052f630e5ce4dbd76576ba9..37c334e838d2c4052134a366cd983ae3142947fa 100644 (file)
@@ -123,7 +123,7 @@ export default /*#__PURE__*/_defineComponent({
   props: {
     foo: { default: 1 },
     bar: { default: () => ({}) },
-    baz: null,
+    baz: {},
     boola: { type: Boolean },
     boolb: { type: [Boolean, Number] },
     func: { type: Function, default: () => {} }
index 2da79cf8312b80670d2a009a7b64fe715db7a0ea..32c1367eb57db64fa0382604cdc0645d69498b34 100644 (file)
@@ -306,6 +306,48 @@ const myEmit = defineEmits(['foo', 'bar'])
         '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead.'
       )
     })
+
+    it('should emit an error with declaring props/emits/slots/expose', () => {
+      expect(() =>
+        compile(`
+          <script setup>
+          defineOptions({ props: ['foo'] })
+          </script>
+        `)
+      ).toThrowError(
+        '[@vue/compiler-sfc] defineOptions() cannot be used to declare props. Use defineProps() instead'
+      )
+
+      expect(() =>
+        compile(`
+          <script setup>
+          defineOptions({ emits: ['update'] })
+          </script>
+        `)
+      ).toThrowError(
+        '[@vue/compiler-sfc] defineOptions() cannot be used to declare emits. Use defineEmits() instead'
+      )
+
+      expect(() =>
+        compile(`
+          <script setup>
+          defineOptions({ expose: ['foo'] })
+          </script>
+        `)
+      ).toThrowError(
+        '[@vue/compiler-sfc] defineOptions() cannot be used to declare expose. Use defineExpose() instead'
+      )
+
+      expect(() =>
+        compile(`
+          <script setup lang="ts">
+          defineOptions({ slots: Object })
+          </script>
+        `)
+      ).toThrowError(
+        '[@vue/compiler-sfc] defineOptions() cannot be used to declare slots. Use defineSlots() instead'
+      )
+    })
   })
 
   test('defineExpose()', () => {
@@ -323,6 +365,109 @@ defineExpose({ foo: 123 })
     expect(content).toMatch(/\b__expose\(\{ foo: 123 \}\)/)
   })
 
+  describe('defineModel()', () => {
+    test('basic usage', () => {
+      const { content, bindings } = compile(
+        `
+        <script setup>
+        const modelValue = defineModel({ required: true })
+        const c = defineModel('count')
+        </script>
+        `,
+        { defineModel: true }
+      )
+      assertCode(content)
+      expect(content).toMatch('props: {')
+      expect(content).toMatch('"modelValue": { required: true },')
+      expect(content).toMatch('"count": {},')
+      expect(content).toMatch('emits: ["update:modelValue", "update:count"],')
+      expect(content).toMatch(
+        `const modelValue = _useModel(__props, "modelValue")`
+      )
+      expect(content).toMatch(`const c = _useModel(__props, "count")`)
+      expect(content).toMatch(`return { modelValue, c }`)
+      expect(content).not.toMatch('defineModel')
+
+      expect(bindings).toStrictEqual({
+        modelValue: BindingTypes.SETUP_REF,
+        count: BindingTypes.PROPS,
+        c: BindingTypes.SETUP_REF
+      })
+    })
+
+    test('w/ defineProps and defineEmits', () => {
+      const { content, bindings } = compile(
+        `
+        <script setup>
+        defineProps({ foo: String })
+        defineEmits(['change'])
+        const count = defineModel({ default: 0 })
+        </script>
+      `,
+        { defineModel: true }
+      )
+      assertCode(content)
+      expect(content).toMatch(`props: _mergeModels({ foo: String }`)
+      expect(content).toMatch(`"modelValue": { default: 0 }`)
+      expect(content).toMatch(`const count = _useModel(__props, "modelValue")`)
+      expect(content).not.toMatch('defineModel')
+      expect(bindings).toStrictEqual({
+        count: BindingTypes.SETUP_REF,
+        foo: BindingTypes.PROPS,
+        modelValue: BindingTypes.PROPS
+      })
+    })
+
+    test('w/ array props', () => {
+      const { content, bindings } = compile(
+        `
+        <script setup>
+        defineProps(['foo', 'bar'])
+        const count = defineModel('count')
+        </script>
+      `,
+        { defineModel: true }
+      )
+      assertCode(content)
+      expect(content).toMatch(`props: _mergeModels(['foo', 'bar'], {
+    "count": {},
+  })`)
+      expect(content).toMatch(`const count = _useModel(__props, "count")`)
+      expect(content).not.toMatch('defineModel')
+      expect(bindings).toStrictEqual({
+        foo: BindingTypes.PROPS,
+        bar: BindingTypes.PROPS,
+        count: BindingTypes.SETUP_REF
+      })
+    })
+
+    test('w/ local flag', () => {
+      const { content } = compile(
+        `<script setup>
+        const foo = defineModel({ local: true, default: 1 })
+        const bar = defineModel('bar', { [key]: true })
+        const baz = defineModel('baz', { ...x })
+        const qux = defineModel('qux', x)
+
+        const foo2 = defineModel('foo2', { local: true, ...x })
+
+        const local = true
+        const hoist = defineModel('hoist', { local })
+        </script>`,
+        { defineModel: true }
+      )
+      assertCode(content)
+      expect(content).toMatch(
+        `_useModel(__props, "modelValue", { local: true })`
+      )
+      expect(content).toMatch(`_useModel(__props, "bar", { [key]: true })`)
+      expect(content).toMatch(`_useModel(__props, "baz", { ...x })`)
+      expect(content).toMatch(`_useModel(__props, "qux", x)`)
+      expect(content).toMatch(`_useModel(__props, "foo2", { local: true })`)
+      expect(content).toMatch(`_useModel(__props, "hoist", { local })`)
+    })
+  })
+
   test('<script> after <script setup> the script content not end with `\\n`', () => {
     const { content } = compile(`
     <script setup>
@@ -1391,7 +1536,7 @@ const emit = defineEmits(['a', 'b'])
       expect(content).toMatch(`const props = __props`)
 
       // foo has no default value, the Function can be dropped
-      expect(content).toMatch(`foo: null`)
+      expect(content).toMatch(`foo: {}`)
       expect(content).toMatch(`bar: { type: Boolean }`)
       expect(content).toMatch(
         `baz: { type: [Boolean, Function], default: true }`
@@ -1469,7 +1614,7 @@ const emit = defineEmits(['a', 'b'])
     foo: { type: Function },
     bar: { type: Boolean },
     baz: { type: [Boolean, Function] },
-    qux: null
+    qux: {}
   }, { ...defaults })`.trim()
       )
     })
@@ -1679,6 +1824,86 @@ const emit = defineEmits(['a', 'b'])
       })
     })
 
+    describe('defineModel()', () => {
+      test('basic usage', () => {
+        const { content, bindings } = compile(
+          `
+          <script setup lang="ts">
+          const modelValue = defineModel<boolean | string>()
+          const count = defineModel<number>('count')
+          const disabled = defineModel<number>('disabled', { required: false })
+          const any = defineModel<any | boolean>('any')
+          </script>
+          `,
+          { defineModel: true }
+        )
+        assertCode(content)
+        expect(content).toMatch('"modelValue": { type: [Boolean, String] }')
+        expect(content).toMatch('"count": { type: Number }')
+        expect(content).toMatch(
+          '"disabled": { type: Number, ...{ required: false } }'
+        )
+        expect(content).toMatch('"any": { type: Boolean, skipCheck: true }')
+        expect(content).toMatch(
+          'emits: ["update:modelValue", "update:count", "update:disabled", "update:any"]'
+        )
+
+        expect(content).toMatch(
+          `const modelValue = _useModel(__props, "modelValue")`
+        )
+        expect(content).toMatch(`const count = _useModel(__props, "count")`)
+        expect(content).toMatch(
+          `const disabled = _useModel(__props, "disabled")`
+        )
+        expect(content).toMatch(`const any = _useModel(__props, "any")`)
+
+        expect(bindings).toStrictEqual({
+          modelValue: BindingTypes.SETUP_REF,
+          count: BindingTypes.SETUP_REF,
+          disabled: BindingTypes.SETUP_REF,
+          any: BindingTypes.SETUP_REF
+        })
+      })
+
+      test('w/ production mode', () => {
+        const { content, bindings } = compile(
+          `
+          <script setup lang="ts">
+          const modelValue = defineModel<boolean>()
+          const fn = defineModel<() => void>('fn')
+          const fnWithDefault = defineModel<() => void>('fnWithDefault', { default: () => null })
+          const str = defineModel<string>('str')
+          const optional = defineModel<string>('optional', { required: false })
+          </script>
+          `,
+          { defineModel: true, isProd: true }
+        )
+        assertCode(content)
+        expect(content).toMatch('"modelValue": { type: Boolean }')
+        expect(content).toMatch('"fn": {}')
+        expect(content).toMatch(
+          '"fnWithDefault": { type: Function, ...{ default: () => null } },'
+        )
+        expect(content).toMatch('"str": {}')
+        expect(content).toMatch('"optional": { required: false }')
+        expect(content).toMatch(
+          'emits: ["update:modelValue", "update:fn", "update:fnWithDefault", "update:str", "update:optional"]'
+        )
+        expect(content).toMatch(
+          `const modelValue = _useModel(__props, "modelValue")`
+        )
+        expect(content).toMatch(`const fn = _useModel(__props, "fn")`)
+        expect(content).toMatch(`const str = _useModel(__props, "str")`)
+        expect(bindings).toStrictEqual({
+          modelValue: BindingTypes.SETUP_REF,
+          fn: BindingTypes.SETUP_REF,
+          fnWithDefault: BindingTypes.SETUP_REF,
+          str: BindingTypes.SETUP_REF,
+          optional: BindingTypes.SETUP_REF
+        })
+      })
+    })
+
     test('runtime Enum', () => {
       const { content, bindings } = compile(
         `<script setup lang="ts">
index 193b3e5a1af589d54e8d6763ebc093de5e966fe8..e00d7d48b97ea5cdf14103903d4674262fe0cd67 100644 (file)
@@ -131,17 +131,17 @@ describe('sfc props transform', () => {
     `,
       { isProd: true }
     )
+    assertCode(content)
     // literals can be used as-is, non-literals are always returned from a
     // function
     expect(content).toMatch(`props: {
     foo: { default: 1 },
     bar: { default: () => ({}) },
-    baz: null,
+    baz: {},
     boola: { type: Boolean },
     boolb: { type: [Boolean, Number] },
     func: { type: Function, default: () => {} }
   }`)
-    assertCode(content)
   })
 
   test('aliasing', () => {
index b00c17799c0b7ea224298f266dd4c7e604f1a67e..35e690bb00ea5aaaac8a08d8148fdf5d9dbbb66b 100644 (file)
@@ -68,6 +68,7 @@ const DEFINE_EXPOSE = 'defineExpose'
 const WITH_DEFAULTS = 'withDefaults'
 const DEFINE_OPTIONS = 'defineOptions'
 const DEFINE_SLOTS = 'defineSlots'
+const DEFINE_MODEL = 'defineModel'
 
 const isBuiltInDir = makeMap(
   `once,memo,if,for,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
@@ -119,13 +120,16 @@ export interface SFCScriptCompileOptions {
    * options passed to `compiler-dom`.
    */
   templateOptions?: Partial<SFCTemplateCompileOptions>
-
   /**
    * Hoist <script setup> static constants.
    * - Only enables when one `<script setup>` exists.
    * @default true
    */
   hoistStatic?: boolean
+  /**
+   * (**Experimental**) Enable macro `defineModel`
+   */
+  defineModel?: boolean
 }
 
 export interface ImportBinding {
@@ -150,6 +154,11 @@ type PropsDeclType = FromNormalScript<TSTypeLiteral | TSInterfaceBody>
 type EmitsDeclType = FromNormalScript<
   TSFunctionType | TSTypeLiteral | TSInterfaceBody
 >
+interface ModelDecl {
+  type: TSType | undefined
+  options: string | undefined
+  identifier: string | undefined
+}
 
 /**
  * Compile `<script setup>`
@@ -164,6 +173,7 @@ export function compileScript(
   // feature flags
   // TODO remove in 3.4
   const enableReactivityTransform = !!options.reactivityTransform
+  const enableDefineModel = !!options.defineModel
   const isProd = !!options.isProd
   const genSourceMap = options.sourceMap !== false
   const hoistStatic = options.hoistStatic !== false && !script
@@ -314,6 +324,7 @@ export function compileScript(
   let hasDefaultExportRender = false
   let hasDefineOptionsCall = false
   let hasDefineSlotsCall = false
+  let hasDefineModelCall = false
   let propsRuntimeDecl: Node | undefined
   let propsRuntimeDefaults: Node | undefined
   let propsDestructureDecl: Node | undefined
@@ -325,6 +336,7 @@ export function compileScript(
   let emitsTypeDecl: EmitsDeclType | undefined
   let emitIdentifier: string | undefined
   let optionsRuntimeDecl: Node | undefined
+  let modelDecls: Record<string, ModelDecl> = {}
   let hasAwait = false
   let hasInlinedSsrRenderFn = false
   // props/emits declared via types
@@ -616,6 +628,79 @@ export function compileScript(
     return true
   }
 
+  function processDefineModel(node: Node, declId?: LVal): boolean {
+    if (!enableDefineModel || !isCallOf(node, DEFINE_MODEL)) {
+      return false
+    }
+    hasDefineModelCall = true
+
+    const type =
+      (node.typeParameters && node.typeParameters.params[0]) || undefined
+    let modelName: string
+    let options: Node | undefined
+    const arg0 = node.arguments[0] && unwrapTSNode(node.arguments[0])
+    if (arg0 && arg0.type === 'StringLiteral') {
+      modelName = arg0.value
+      options = node.arguments[1]
+    } else {
+      modelName = 'modelValue'
+      options = arg0
+    }
+
+    if (modelDecls[modelName]) {
+      error(`duplicate model name ${JSON.stringify(modelName)}`, node)
+    }
+
+    const optionsString = options
+      ? s.slice(startOffset + options.start!, startOffset + options.end!)
+      : undefined
+
+    modelDecls[modelName] = {
+      type,
+      options: optionsString,
+      identifier:
+        declId && declId.type === 'Identifier' ? declId.name : undefined
+    }
+
+    let runtimeOptions = ''
+    if (options) {
+      if (options.type === 'ObjectExpression') {
+        const local = options.properties.find(
+          p =>
+            p.type === 'ObjectProperty' &&
+            ((p.key.type === 'Identifier' && p.key.name === 'local') ||
+              (p.key.type === 'StringLiteral' && p.key.value === 'local'))
+        ) as ObjectProperty
+
+        if (local) {
+          runtimeOptions = `{ ${s.slice(
+            startOffset + local.start!,
+            startOffset + local.end!
+          )} }`
+        } else {
+          for (const p of options.properties) {
+            if (p.type === 'SpreadElement' || p.computed) {
+              runtimeOptions = optionsString!
+              break
+            }
+          }
+        }
+      } else {
+        runtimeOptions = optionsString!
+      }
+    }
+
+    s.overwrite(
+      startOffset + node.start!,
+      startOffset + node.end!,
+      `${helper('useModel')}(__props, ${JSON.stringify(modelName)}${
+        runtimeOptions ? `, ${runtimeOptions}` : ``
+      })`
+    )
+
+    return true
+  }
+
   function getAstBody(): Statement[] {
     return scriptAst
       ? [...scriptSetupAst.body, ...scriptAst.body]
@@ -883,18 +968,25 @@ export function compileScript(
     )
   }
 
-  function genRuntimeProps(props: Record<string, PropTypeData>) {
-    const keys = Object.keys(props)
-    if (!keys.length) {
-      return ``
-    }
-    const hasStaticDefaults = hasStaticWithDefaults()
-    const scriptSetupSource = scriptSetup!.content
-    let propsDecls = `{
+  function concatStrings(strs: Array<string | null | undefined | false>) {
+    return strs.filter((s): s is string => !!s).join(', ')
+  }
+
+  function genRuntimeProps() {
+    function genPropsFromTS() {
+      const keys = Object.keys(typeDeclaredProps)
+      if (!keys.length) return
+
+      const hasStaticDefaults = hasStaticWithDefaults()
+      const scriptSetupSource = scriptSetup!.content
+      let propsDecls = `{
     ${keys
       .map(key => {
         let defaultString: string | undefined
-        const destructured = genDestructuredDefaultValue(key, props[key].type)
+        const destructured = genDestructuredDefaultValue(
+          key,
+          typeDeclaredProps[key].type
+        )
         if (destructured) {
           defaultString = `default: ${destructured.valueString}${
             destructured.needSkipFactory ? `, skipFactory: true` : ``
@@ -924,13 +1016,14 @@ export function compileScript(
           }
         }
 
-        const { type, required, skipCheck } = props[key]
+        const { type, required, skipCheck } = typeDeclaredProps[key]
         if (!isProd) {
-          return `${key}: { type: ${toRuntimeTypeString(
-            type
-          )}, required: ${required}${skipCheck ? ', skipCheck: true' : ''}${
-            defaultString ? `, ${defaultString}` : ``
-          } }`
+          return `${key}: { ${concatStrings([
+            `type: ${toRuntimeTypeString(type)}`,
+            `required: ${required}`,
+            skipCheck && 'skipCheck: true',
+            defaultString
+          ])} }`
         } else if (
           type.some(
             el =>
@@ -941,24 +1034,104 @@ export function compileScript(
           // #4783 for boolean, should keep the type
           // #7111 for function, if default value exists or it's not static, should keep it
           // in production
-          return `${key}: { type: ${toRuntimeTypeString(type)}${
-            defaultString ? `, ${defaultString}` : ``
-          } }`
+          return `${key}: { ${concatStrings([
+            `type: ${toRuntimeTypeString(type)}`,
+            defaultString
+          ])} }`
         } else {
           // production: checks are useless
-          return `${key}: ${defaultString ? `{ ${defaultString} }` : 'null'}`
+          return `${key}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
         }
       })
       .join(',\n    ')}\n  }`
 
-    if (propsRuntimeDefaults && !hasStaticDefaults) {
-      propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice(
-        propsRuntimeDefaults.start! + startOffset,
-        propsRuntimeDefaults.end! + startOffset
-      )})`
+      if (propsRuntimeDefaults && !hasStaticDefaults) {
+        propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice(
+          propsRuntimeDefaults.start! + startOffset,
+          propsRuntimeDefaults.end! + startOffset
+        )})`
+      }
+
+      return propsDecls
+    }
+
+    function genModels() {
+      if (!hasDefineModelCall) return
+
+      let modelPropsDecl = ''
+      for (const [name, { type, options }] of Object.entries(modelDecls)) {
+        let skipCheck = false
+
+        let runtimeTypes = type && inferRuntimeType(type, declaredTypes)
+        if (runtimeTypes) {
+          const hasUnknownType = runtimeTypes.includes(UNKNOWN_TYPE)
+
+          runtimeTypes = runtimeTypes.filter(el => {
+            if (el === UNKNOWN_TYPE) return false
+            return isProd
+              ? el === 'Boolean' || (el === 'Function' && options)
+              : true
+          })
+          skipCheck = !isProd && hasUnknownType && runtimeTypes.length > 0
+        }
+
+        let runtimeType =
+          (runtimeTypes &&
+            runtimeTypes.length > 0 &&
+            toRuntimeTypeString(runtimeTypes)) ||
+          undefined
+
+        const codegenOptions = concatStrings([
+          runtimeType && `type: ${runtimeType}`,
+          skipCheck && 'skipCheck: true'
+        ])
+
+        let decl: string
+        if (runtimeType && options) {
+          decl = isTS
+            ? `{ ${codegenOptions}, ...${options} }`
+            : `Object.assign({ ${codegenOptions} }, ${options})`
+        } else {
+          decl = options || (runtimeType ? `{ ${codegenOptions} }` : '{}')
+        }
+        modelPropsDecl += `\n    ${JSON.stringify(name)}: ${decl},`
+      }
+      return `{${modelPropsDecl}\n  }`
+    }
+
+    let propsDecls: undefined | string
+    if (propsRuntimeDecl) {
+      propsDecls = scriptSetup!.content
+        .slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
+        .trim()
+      if (propsDestructureDecl) {
+        const defaults: string[] = []
+        for (const key in propsDestructuredBindings) {
+          const d = genDestructuredDefaultValue(key)
+          if (d)
+            defaults.push(
+              `${key}: ${d.valueString}${
+                d.needSkipFactory ? `, __skip_${key}: true` : ``
+              }`
+            )
+        }
+        if (defaults.length) {
+          propsDecls = `${helper(
+            `mergeDefaults`
+          )}(${propsDecls}, {\n  ${defaults.join(',\n  ')}\n})`
+        }
+      }
+    } else if (propsTypeDecl) {
+      propsDecls = genPropsFromTS()
     }
 
-    return `\n  props: ${propsDecls},`
+    const modelsDecls = genModels()
+
+    if (propsDecls && modelsDecls) {
+      return `${helper('mergeModels')}(${propsDecls}, ${modelsDecls})`
+    } else {
+      return modelsDecls || propsDecls
+    }
   }
 
   function genDestructuredDefaultValue(
@@ -1058,6 +1231,34 @@ export function compileScript(
     }
   }
 
+  function genRuntimeEmits() {
+    function genEmitsFromTS() {
+      return typeDeclaredEmits.size
+        ? `[${Array.from(typeDeclaredEmits)
+            .map(k => JSON.stringify(k))
+            .join(', ')}]`
+        : ``
+    }
+
+    let emitsDecl = ''
+    if (emitsRuntimeDecl) {
+      emitsDecl = scriptSetup!.content
+        .slice(emitsRuntimeDecl.start!, emitsRuntimeDecl.end!)
+        .trim()
+    } else if (emitsTypeDecl) {
+      emitsDecl = genEmitsFromTS()
+    }
+    if (hasDefineModelCall) {
+      let modelEmitsDecl = `[${Object.keys(modelDecls)
+        .map(n => JSON.stringify(`update:${n}`))
+        .join(', ')}]`
+      emitsDecl = emitsDecl
+        ? `${helper('mergeModels')}(${emitsDecl}, ${modelEmitsDecl})`
+        : modelEmitsDecl
+    }
+    return emitsDecl
+  }
+
   // 0. parse both <script> and <script setup> blocks
   const scriptAst =
     script &&
@@ -1345,6 +1546,8 @@ export function compileScript(
           callee.end! + startOffset,
           '__expose'
         )
+      } else {
+        processDefineModel(expr)
       }
     }
 
@@ -1370,7 +1573,9 @@ export function compileScript(
             processWithDefaults(init, decl.id)
           const isDefineEmits =
             !isDefineProps && processDefineEmits(init, decl.id)
-          !isDefineEmits && processDefineSlots(init, decl.id)
+          !isDefineEmits &&
+            (processDefineSlots(init, decl.id) ||
+              processDefineModel(init, decl.id))
 
           if (isDefineProps || isDefineEmits) {
             if (left === 1) {
@@ -1569,6 +1774,9 @@ export function compileScript(
   for (const key in typeDeclaredProps) {
     bindingMetadata[key] = BindingTypes.PROPS
   }
+  for (const key in modelDecls) {
+    bindingMetadata[key] = BindingTypes.PROPS
+  }
   // props aliases
   if (propsDestructureDecl) {
     if (propsDestructureRestId) {
@@ -1787,38 +1995,12 @@ export function compileScript(
   if (hasInlinedSsrRenderFn) {
     runtimeOptions += `\n  __ssrInlineRender: true,`
   }
-  if (propsRuntimeDecl) {
-    let declCode = scriptSetup.content
-      .slice(propsRuntimeDecl.start!, propsRuntimeDecl.end!)
-      .trim()
-    if (propsDestructureDecl) {
-      const defaults: string[] = []
-      for (const key in propsDestructuredBindings) {
-        const d = genDestructuredDefaultValue(key)
-        if (d)
-          defaults.push(
-            `${key}: ${d.valueString}${
-              d.needSkipFactory ? `, __skip_${key}: true` : ``
-            }`
-          )
-      }
-      if (defaults.length) {
-        declCode = `${helper(
-          `mergeDefaults`
-        )}(${declCode}, {\n  ${defaults.join(',\n  ')}\n})`
-      }
-    }
-    runtimeOptions += `\n  props: ${declCode},`
-  } else if (propsTypeDecl) {
-    runtimeOptions += genRuntimeProps(typeDeclaredProps)
-  }
-  if (emitsRuntimeDecl) {
-    runtimeOptions += `\n  emits: ${scriptSetup.content
-      .slice(emitsRuntimeDecl.start!, emitsRuntimeDecl.end!)
-      .trim()},`
-  } else if (emitsTypeDecl) {
-    runtimeOptions += genRuntimeEmits(typeDeclaredEmits)
-  }
+
+  const propsDecl = genRuntimeProps()
+  if (propsDecl) runtimeOptions += `\n  props: ${propsDecl},`
+
+  const emitsDecl = genRuntimeEmits()
+  if (emitsDecl) runtimeOptions += `\n  emits: ${emitsDecl},`
 
   let definedOptions = ''
   if (optionsRuntimeDecl) {
@@ -1958,7 +2140,10 @@ function walkDeclaration(
             ? BindingTypes.SETUP_REACTIVE_CONST
             : BindingTypes.SETUP_CONST
         } else if (isConst) {
-          if (isCallOf(init, userImportAliases['ref'])) {
+          if (
+            isCallOf(init, userImportAliases['ref']) ||
+            isCallOf(init, DEFINE_MODEL)
+          ) {
             bindingType = BindingTypes.SETUP_REF
           } else {
             bindingType = BindingTypes.SETUP_MAYBE_REF
@@ -2393,14 +2578,6 @@ function extractEventNames(
   }
 }
 
-function genRuntimeEmits(emits: Set<string>) {
-  return emits.size
-    ? `\n  emits: [${Array.from(emits)
-        .map(p => JSON.stringify(p))
-        .join(', ')}],`
-    : ``
-}
-
 function canNeverBeRef(node: Node, userReactiveImport?: string): boolean {
   if (isCallOf(node, userReactiveImport)) {
     return true
index 13d80947c83fb9f4631ba8eb7ef41160b9b8ccae..f738e58b9eb6e316421a3e84f10474cc58b2febc 100644 (file)
@@ -6,9 +6,13 @@ import {
   withDefaults,
   Slots,
   defineSlots,
-  VNode
+  VNode,
+  Ref,
+  defineModel
 } from 'vue'
 import { describe, expectType } from './utils'
+import { defineComponent } from 'vue'
+import { useModel } from 'vue'
 
 describe('defineProps w/ type declaration', () => {
   // type declaration
@@ -202,6 +206,72 @@ describe('defineSlots', () => {
   expectType<Slots>(slotsUntype)
 })
 
+describe('defineModel', () => {
+  // overload 1
+  const modelValueRequired = defineModel<boolean>({ required: true })
+  expectType<Ref<boolean>>(modelValueRequired)
+
+  // overload 2
+  const modelValue = defineModel<string>()
+  expectType<Ref<string | undefined>>(modelValue)
+  modelValue.value = 'new value'
+
+  const modelValueDefault = defineModel<boolean>({ default: true })
+  expectType<Ref<boolean>>(modelValueDefault)
+
+  // overload 3
+  const countRequired = defineModel<number>('count', { required: false })
+  expectType<Ref<number | undefined>>(countRequired)
+
+  // overload 4
+  const count = defineModel<number>('count')
+  expectType<Ref<number | undefined>>(count)
+
+  const countDefault = defineModel<number>('count', { default: 1 })
+  expectType<Ref<number>>(countDefault)
+
+  // infer type from default
+  const inferred = defineModel({ default: 123 })
+  expectType<Ref<number | undefined>>(inferred)
+  const inferredRequired = defineModel({ default: 123, required: true })
+  expectType<Ref<number>>(inferredRequired)
+
+  // @ts-expect-error type / default mismatch
+  defineModel<string>({ default: 123 })
+  // @ts-expect-error unknown props option
+  defineModel({ foo: 123 })
+
+  // accept defineModel-only options
+  defineModel({ local: true })
+  defineModel('foo', { local: true })
+})
+
+describe('useModel', () => {
+  defineComponent({
+    props: ['foo'],
+    setup(props) {
+      const r = useModel(props, 'foo')
+      expectType<Ref<any>>(r)
+
+      // @ts-expect-error
+      useModel(props, 'bar')
+    }
+  })
+
+  defineComponent({
+    props: {
+      foo: String,
+      bar: { type: Number, required: true },
+      baz: { type: Boolean }
+    },
+    setup(props) {
+      expectType<Ref<string | undefined>>(useModel(props, 'foo'))
+      expectType<Ref<number>>(useModel(props, 'bar'))
+      expectType<Ref<boolean>>(useModel(props, 'baz'))
+    }
+  })
+})
+
 describe('useAttrs', () => {
   const attrs = useAttrs()
   expectType<Record<string, unknown>>(attrs)
index 65fb325f044d89a3ce879c676cff1ba44b9de043..f15e03ff01fc773eb5993ae545335f598a01b297 100644 (file)
@@ -13,7 +13,9 @@ import {
   Suspense,
   computed,
   ComputedRef,
-  shallowReactive
+  shallowReactive,
+  nextTick,
+  ref
 } from '@vue/runtime-test'
 import {
   defineEmits,
@@ -24,7 +26,9 @@ import {
   useSlots,
   mergeDefaults,
   withAsyncContext,
-  createPropsRestProxy
+  createPropsRestProxy,
+  mergeModels,
+  useModel
 } from '../src/apiSetupHelpers'
 
 describe('SFC <script setup> helpers', () => {
@@ -133,6 +137,149 @@ describe('SFC <script setup> helpers', () => {
     })
   })
 
+  describe('mergeModels', () => {
+    test('array syntax', () => {
+      expect(mergeModels(['foo', 'bar'], ['baz'])).toMatchObject([
+        'foo',
+        'bar',
+        'baz'
+      ])
+    })
+
+    test('object syntax', () => {
+      expect(
+        mergeModels({ foo: null, bar: { required: true } }, ['baz'])
+      ).toMatchObject({
+        foo: null,
+        bar: { required: true },
+        baz: {}
+      })
+
+      expect(
+        mergeModels(['baz'], { foo: null, bar: { required: true } })
+      ).toMatchObject({
+        foo: null,
+        bar: { required: true },
+        baz: {}
+      })
+    })
+
+    test('overwrite', () => {
+      expect(
+        mergeModels(
+          { foo: null, bar: { required: true } },
+          { bar: {}, baz: {} }
+        )
+      ).toMatchObject({
+        foo: null,
+        bar: {},
+        baz: {}
+      })
+    })
+  })
+
+  describe('useModel', () => {
+    test('basic', async () => {
+      let foo: any
+      const update = () => {
+        foo.value = 'bar'
+      }
+
+      const Comp = defineComponent({
+        props: ['modelValue'],
+        emits: ['update:modelValue'],
+        setup(props) {
+          foo = useModel(props, 'modelValue')
+        },
+        render() {}
+      })
+
+      const msg = ref('')
+      const setValue = vi.fn(v => (msg.value = v))
+      const root = nodeOps.createElement('div')
+      createApp(() =>
+        h(Comp, {
+          modelValue: msg.value,
+          'onUpdate:modelValue': setValue
+        })
+      ).mount(root)
+
+      expect(foo.value).toBe('')
+      expect(msg.value).toBe('')
+      expect(setValue).not.toBeCalled()
+
+      // update from child
+      update()
+
+      await nextTick()
+      expect(msg.value).toBe('bar')
+      expect(foo.value).toBe('bar')
+      expect(setValue).toBeCalledTimes(1)
+
+      // update from parent
+      msg.value = 'qux'
+
+      await nextTick()
+      expect(msg.value).toBe('qux')
+      expect(foo.value).toBe('qux')
+      expect(setValue).toBeCalledTimes(1)
+    })
+
+    test('local', async () => {
+      let foo: any
+      const update = () => {
+        foo.value = 'bar'
+      }
+
+      const Comp = defineComponent({
+        props: ['foo'],
+        emits: ['update:foo'],
+        setup(props) {
+          foo = useModel(props, 'foo', { local: true })
+        },
+        render() {}
+      })
+
+      const root = nodeOps.createElement('div')
+      const updateFoo = vi.fn()
+      render(h(Comp, { 'onUpdate:foo': updateFoo }), root)
+
+      expect(foo.value).toBeUndefined()
+      update()
+
+      expect(foo.value).toBe('bar')
+
+      await nextTick()
+      expect(updateFoo).toBeCalledTimes(1)
+    })
+
+    test('default value', async () => {
+      let count: any
+      const inc = () => {
+        count.value++
+      }
+      const Comp = defineComponent({
+        props: { count: { default: 0 } },
+        emits: ['update:count'],
+        setup(props) {
+          count = useModel(props, 'count', { local: true })
+        },
+        render() {}
+      })
+
+      const root = nodeOps.createElement('div')
+      const updateCount = vi.fn()
+      render(h(Comp, { 'onUpdate:count': updateCount }), root)
+
+      expect(count.value).toBe(0)
+
+      inc()
+      expect(count.value).toBe(1)
+      await nextTick()
+      expect(updateCount).toBeCalledTimes(1)
+    })
+  })
+
   test('createPropsRestProxy', () => {
     const original = shallowReactive({
       foo: 1,
index 1c60416c6eae493e0a1ecfb5de8b88d0d4279daa..e0fe434210f04d365daa62fba7a7b4a4a13e2a06 100644 (file)
@@ -3,7 +3,8 @@ import {
   isPromise,
   isFunction,
   Prettify,
-  UnionToIntersection
+  UnionToIntersection,
+  extend
 } from '@vue/shared'
 import {
   getCurrentInstance,
@@ -12,7 +13,7 @@ import {
   createSetupContext,
   unsetCurrentInstance
 } from './component'
-import { EmitFn, EmitsOptions } from './componentEmits'
+import { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits'
 import {
   ComponentOptionsMixin,
   ComponentOptionsWithoutProps,
@@ -22,10 +23,14 @@ import {
 import {
   ComponentPropsOptions,
   ComponentObjectPropsOptions,
-  ExtractPropTypes
+  ExtractPropTypes,
+  NormalizedProps,
+  PropOptions
 } from './componentProps'
 import { warn } from './warning'
 import { SlotsType, TypedSlots } from './componentSlots'
+import { Ref, ref } from '@vue/reactivity'
+import { watch } from './apiWatch'
 
 // dev only
 const warnRuntimeUsage = (method: string) =>
@@ -200,11 +205,77 @@ export function defineOptions<
 
 export function defineSlots<
   S extends Record<string, any> = Record<string, any>
->(): // @ts-expect-error
-TypedSlots<SlotsType<S>> {
+>(): TypedSlots<SlotsType<S>> {
   if (__DEV__) {
     warnRuntimeUsage(`defineSlots`)
   }
+  return null as any
+}
+
+/**
+ * (**Experimental**) Vue `<script setup>` compiler macro for declaring a
+ * two-way binding prop that can be consumed via `v-model` from the parent
+ * component. This will declare a prop with the same name and a corresponding
+ * `update:propName` event.
+ *
+ * If the first argument is a string, it will be used as the prop name;
+ * Otherwise the prop name will default to "modelValue". In both cases, you
+ * can also pass an additional object which will be used as the prop's options.
+ *
+ * The options object can also specify an additional option, `local`. When set
+ * to `true`, the ref can be locally mutated even if the parent did not pass
+ * the matching `v-model`.
+ *
+ * @example
+ * ```ts
+ * // default model (consumed via `v-model`)
+ * const modelValue = defineModel<string>()
+ * modelValue.value = "hello"
+ *
+ * // default model with options
+ * const modelValue = defineModel<stirng>({ required: true })
+ *
+ * // with specified name (consumed via `v-model:count`)
+ * const count = defineModel<number>('count')
+ * count.value++
+ *
+ * // with specified name and default value
+ * const count = defineModel<number>('count', { default: 0 })
+ *
+ * // local mutable model, can be mutated locally
+ * // even if the parent did not pass the matching `v-model`.
+ * const count = defineModel<number>('count', { local: true, default: 0 })
+ * ```
+ */
+export function defineModel<T>(
+  options: { required: true } & PropOptions<T> & DefineModelOptions
+): Ref<T>
+export function defineModel<T>(
+  options: { default: any } & PropOptions<T> & DefineModelOptions
+): Ref<T>
+export function defineModel<T>(
+  options?: PropOptions<T> & DefineModelOptions
+): Ref<T | undefined>
+export function defineModel<T>(
+  name: string,
+  options: { required: true } & PropOptions<T> & DefineModelOptions
+): Ref<T>
+export function defineModel<T>(
+  name: string,
+  options: { default: any } & PropOptions<T> & DefineModelOptions
+): Ref<T>
+export function defineModel<T>(
+  name: string,
+  options?: PropOptions<T> & DefineModelOptions
+): Ref<T | undefined>
+export function defineModel(): any {
+  if (__DEV__) {
+    warnRuntimeUsage('defineModel')
+  }
+}
+
+interface DefineModelOptions {
+  local?: boolean
 }
 
 type NotUndefined<T> = T extends undefined ? never : T
@@ -268,6 +339,55 @@ export function useAttrs(): SetupContext['attrs'] {
   return getContext().attrs
 }
 
+export function useModel<T extends Record<string, any>, K extends keyof T>(
+  props: T,
+  name: K,
+  options?: { local?: boolean }
+): Ref<T[K]>
+export function useModel(
+  props: Record<string, any>,
+  name: string,
+  options?: { local?: boolean }
+): Ref {
+  const i = getCurrentInstance()!
+  if (__DEV__ && !i) {
+    warn(`useModel() called without active instance.`)
+    return ref() as any
+  }
+
+  if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[name]) {
+    warn(`useModel() called with prop "${name}" which is not declared.`)
+    return ref() as any
+  }
+
+  if (options && options.local) {
+    const proxy = ref<any>(props[name])
+
+    watch(
+      () => props[name],
+      v => (proxy.value = v)
+    )
+
+    watch(proxy, value => {
+      if (value !== props[name]) {
+        i.emit(`update:${name}`, value)
+      }
+    })
+
+    return proxy
+  } else {
+    return {
+      __v_isRef: true,
+      get value() {
+        return props[name]
+      },
+      set value(value) {
+        i.emit(`update:${name}`, value)
+      }
+    } as any
+  }
+}
+
 function getContext(): SetupContext {
   const i = getCurrentInstance()!
   if (__DEV__ && !i) {
@@ -276,6 +396,15 @@ function getContext(): SetupContext {
   return i.setupContext || (i.setupContext = createSetupContext(i))
 }
 
+function normalizePropsOrEmits(props: ComponentPropsOptions | EmitsOptions) {
+  return isArray(props)
+    ? props.reduce(
+        (normalized, p) => ((normalized[p] = {}), normalized),
+        {} as ComponentObjectPropsOptions | ObjectEmitsOptions
+      )
+    : props
+}
+
 /**
  * Runtime helper for merging default declarations. Imported by compiled code
  * only.
@@ -285,12 +414,7 @@ export function mergeDefaults(
   raw: ComponentPropsOptions,
   defaults: Record<string, any>
 ): ComponentObjectPropsOptions {
-  const props = isArray(raw)
-    ? raw.reduce(
-        (normalized, p) => ((normalized[p] = {}), normalized),
-        {} as ComponentObjectPropsOptions
-      )
-    : raw
+  const props = normalizePropsOrEmits(raw)
   for (const key in defaults) {
     if (key.startsWith('__skip')) continue
     let opt = props[key]
@@ -312,6 +436,20 @@ export function mergeDefaults(
   return props
 }
 
+/**
+ * Runtime helper for merging model declarations.
+ * Imported by compiled code only.
+ * @internal
+ */
+export function mergeModels(
+  a: ComponentPropsOptions | EmitsOptions,
+  b: ComponentPropsOptions | EmitsOptions
+) {
+  if (!a || !b) return a || b
+  if (isArray(a) && isArray(b)) return a.concat(b)
+  return extend({}, normalizePropsOrEmits(a), normalizePropsOrEmits(b))
+}
+
 /**
  * Used to create a proxy for the rest element when destructuring props with
  * defineProps().
index 186384d5b82fef1178dc01fdc293775caf13dcdc..887ce4f39c6a2fc9640b3e6bea14a143f184f3fc 100644 (file)
@@ -627,7 +627,7 @@ function validateProp(
     return
   }
   // missing but optional
-  if (value == null && !prop.required) {
+  if (value == null && !required) {
     return
   }
   // type check
index 428f3683d5aa8367136cdd374ab4d234802cc367..e427773a70e84f6f9c9df5a439753fb1b27617f4 100644 (file)
@@ -71,9 +71,12 @@ export {
   defineExpose,
   defineOptions,
   defineSlots,
+  defineModel,
   withDefaults,
+  useModel,
   // internal
   mergeDefaults,
+  mergeModels,
   createPropsRestProxy,
   withAsyncContext
 } from './apiSetupHelpers'
index 4ffc2f725aed0175b289c1d6d301610f09986c03..a9ba734a53db878c1ff275497f50fb4c55d635e8 100644 (file)
@@ -5,6 +5,7 @@ type _defineEmits = typeof defineEmits
 type _defineExpose = typeof defineExpose
 type _defineOptions = typeof defineOptions
 type _defineSlots = typeof defineSlots
+type _defineModel = typeof defineModel
 type _withDefaults = typeof withDefaults
 
 declare global {
@@ -13,5 +14,6 @@ declare global {
   const defineExpose: _defineExpose
   const defineOptions: _defineOptions
   const defineSlots: _defineSlots
+  const defineModel: _defineModel
   const withDefaults: _withDefaults
 }
index ef6481cb98b6d1dbe3a3ac5ce30103baf460788a..1bba8d6f2aeb40cc986d6db4cfa11dd1f1df90e4 100644 (file)
@@ -37,7 +37,8 @@ const sfcOptions = {
   script: {
     inlineTemplate: !useDevMode.value,
     isProd: !useDevMode.value,
-    reactivityTransform: true
+    reactivityTransform: true,
+    defineModel: true
   },
   style: {
     isProd: !useDevMode.value