]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-sfc): fix function default value handling w/ props destructure
authorEvan You <yyx990803@gmail.com>
Wed, 29 Mar 2023 14:21:27 +0000 (22:21 +0800)
committerEvan You <yyx990803@gmail.com>
Wed, 29 Mar 2023 14:21:27 +0000 (22:21 +0800)
packages/compiler-sfc/__tests__/__snapshots__/compileScriptPropsTransform.spec.ts.snap
packages/compiler-sfc/__tests__/compileScriptPropsTransform.spec.ts
packages/compiler-sfc/src/compileScript.ts
packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
packages/runtime-core/src/apiSetupHelpers.ts
packages/runtime-core/src/componentProps.ts

index c6123b0767bf57d3c1ad8709aa31532488c2ac3e..328f242010c04b0350bd5f7df8de8b5b884e3fe5 100644 (file)
@@ -78,13 +78,34 @@ return (_ctx, _cache) => {
 }"
 `;
 
-exports[`sfc props transform > default values w/ runtime declaration 1`] = `
+exports[`sfc props transform > default values w/ array runtime declaration 1`] = `
 "import { mergeDefaults as _mergeDefaults } from 'vue'
 
 export default {
-  props: _mergeDefaults(['foo', 'bar'], {
+  props: _mergeDefaults(['foo', 'bar', 'baz'], {
   foo: 1,
-  bar: () => ({})
+  bar: () => ({}),
+  func: () => {}, __skip_func: true
+}),
+  setup(__props) {
+
+      
+      
+return () => {}
+}
+
+}"
+`;
+
+exports[`sfc props transform > default values w/ object runtime declaration 1`] = `
+"import { mergeDefaults as _mergeDefaults } from 'vue'
+
+export default {
+  props: _mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, {
+  foo: 1,
+  bar: () => ({}),
+  func: () => {}, __skip_func: true,
+  ext: x, __skip_ext: true
 }),
   setup(__props) {
 
@@ -102,7 +123,8 @@ exports[`sfc props transform > default values w/ type declaration 1`] = `
 export default /*#__PURE__*/_defineComponent({
   props: {
     foo: { type: Number, required: false, default: 1 },
-    bar: { type: Object, required: false, default: () => ({}) }
+    bar: { type: Object, required: false, default: () => ({}) },
+    func: { type: Function, required: false, default: () => {} }
   },
   setup(__props: any) {
 
@@ -124,7 +146,7 @@ export default /*#__PURE__*/_defineComponent({
     baz: null,
     boola: { type: Boolean },
     boolb: { type: [Boolean, Number] },
-    func: { type: Function, default: () => (() => {}) }
+    func: { type: Function, default: () => {} }
   },
   setup(__props: any) {
 
index 5401a9e030512dfd1cf9813c7116cf22648c2879..9fe711fbc3da90396952ec7a02dc2d64de757cf6 100644 (file)
@@ -69,17 +69,40 @@ describe('sfc props transform', () => {
     })
   })
 
-  test('default values w/ runtime declaration', () => {
+  test('default values w/ array runtime declaration', () => {
     const { content } = compile(`
       <script setup>
-      const { foo = 1, bar = {} } = defineProps(['foo', 'bar'])
+      const { foo = 1, bar = {}, func = () => {} } = defineProps(['foo', 'bar', 'baz'])
       </script>
     `)
     // literals can be used as-is, non-literals are always returned from a
     // function
-    expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar'], {
+    // functions need to be marked with a skip marker
+    expect(content).toMatch(`props: _mergeDefaults(['foo', 'bar', 'baz'], {
   foo: 1,
-  bar: () => ({})
+  bar: () => ({}),
+  func: () => {}, __skip_func: true
+})`)
+    assertCode(content)
+  })
+
+  test('default values w/ object runtime declaration', () => {
+    const { content } = compile(`
+      <script setup>
+      const { foo = 1, bar = {}, func = () => {}, ext = x } = defineProps({ foo: Number, bar: Object, func: Function, ext: null })
+      </script>
+    `)
+    // literals can be used as-is, non-literals are always returned from a
+    // function
+    // functions need to be marked with a skip marker since we cannot always
+    // safely infer whether runtime type is Function (e.g. if the runtime decl
+    // is imported, or spreads another object)
+    expect(content)
+      .toMatch(`props: _mergeDefaults({ foo: Number, bar: Object, func: Function, ext: null }, {
+  foo: 1,
+  bar: () => ({}),
+  func: () => {}, __skip_func: true,
+  ext: x, __skip_ext: true
 })`)
     assertCode(content)
   })
@@ -87,14 +110,15 @@ describe('sfc props transform', () => {
   test('default values w/ type declaration', () => {
     const { content } = compile(`
       <script setup lang="ts">
-      const { foo = 1, bar = {} } = defineProps<{ foo?: number, bar?: object }>()
+      const { foo = 1, bar = {}, func = () => {} } = defineProps<{ foo?: number, bar?: object, func?: () => any }>()
       </script>
     `)
     // literals can be used as-is, non-literals are always returned from a
     // function
     expect(content).toMatch(`props: {
     foo: { type: Number, required: false, default: 1 },
-    bar: { type: Object, required: false, default: () => ({}) }
+    bar: { type: Object, required: false, default: () => ({}) },
+    func: { type: Function, required: false, default: () => {} }
   }`)
     assertCode(content)
   })
@@ -116,7 +140,7 @@ describe('sfc props transform', () => {
     baz: null,
     boola: { type: Boolean },
     boolb: { type: [Boolean, Number] },
-    func: { type: Function, default: () => (() => {}) }
+    func: { type: Function, default: () => {} }
   }`)
     assertCode(content)
   })
index af54641f4deda6cad4e592dc01a9a166b7fa349c..d9c59ab665ef632d7f506524eea630940ec506fe 100644 (file)
@@ -862,9 +862,11 @@ export function compileScript(
     ${keys
       .map(key => {
         let defaultString: string | undefined
-        const destructured = genDestructuredDefaultValue(key)
+        const destructured = genDestructuredDefaultValue(key, props[key].type)
         if (destructured) {
-          defaultString = `default: ${destructured}`
+          defaultString = `default: ${destructured.valueString}${
+            destructured.needSkipFactory ? `, skipFactory: true` : ``
+          }`
         } else if (hasStaticDefaults) {
           const prop = propsRuntimeDefaults!.properties.find(node => {
             if (node.type === 'SpreadElement') return false
@@ -925,15 +927,38 @@ export function compileScript(
     return `\n  props: ${propsDecls},`
   }
 
-  function genDestructuredDefaultValue(key: string): string | undefined {
+  function genDestructuredDefaultValue(
+    key: string,
+    inferredType?: string[]
+  ):
+    | {
+        valueString: string
+        needSkipFactory: boolean
+      }
+    | undefined {
     const destructured = propsDestructuredBindings[key]
-    if (destructured && destructured.default) {
+    const defaultVal = destructured && destructured.default
+    if (defaultVal) {
       const value = scriptSetup!.content.slice(
-        destructured.default.start!,
-        destructured.default.end!
+        defaultVal.start!,
+        defaultVal.end!
       )
-      const isLiteral = isLiteralNode(destructured.default)
-      return isLiteral ? value : `() => (${value})`
+      const unwrapped = unwrapTSNode(defaultVal)
+      // If the default value is a function or is an identifier referencing
+      // external value, skip factory wrap. This is needed when using
+      // destructure w/ runtime declaration since we cannot safely infer
+      // whether tje expected runtime prop type is `Function`.
+      const needSkipFactory =
+        !inferredType &&
+        (isFunctionType(unwrapped) || unwrapped.type === 'Identifier')
+      const needFactoryWrap =
+        !needSkipFactory &&
+        !isLiteralNode(unwrapped) &&
+        !inferredType?.includes('Function')
+      return {
+        valueString: needFactoryWrap ? `() => (${value})` : value,
+        needSkipFactory
+      }
     }
   }
 
@@ -1693,7 +1718,12 @@ export function compileScript(
       const defaults: string[] = []
       for (const key in propsDestructuredBindings) {
         const d = genDestructuredDefaultValue(key)
-        if (d) defaults.push(`${key}: ${d}`)
+        if (d)
+          defaults.push(
+            `${key}: ${d.valueString}${
+              d.needSkipFactory ? `, __skip_${key}: true` : ``
+            }`
+          )
       }
       if (defaults.length) {
         declCode = `${helper(
index aa4e04e80f4f3979753df69a1a52d10acb174d02..65fb325f044d89a3ce879c676cff1ba44b9de043 100644 (file)
@@ -114,6 +114,17 @@ describe('SFC <script setup> helpers', () => {
       })
     })
 
+    test('merging with skipFactory', () => {
+      const fn = () => {}
+      const merged = mergeDefaults(['foo', 'bar', 'baz'], {
+        foo: fn,
+        __skip_foo: true
+      })
+      expect(merged).toMatchObject({
+        foo: { default: fn, skipFactory: true }
+      })
+    })
+
     test('should warn missing', () => {
       mergeDefaults({}, { foo: 1 })
       expect(
index bdda22e9398c4a0d5c54955a4dbc44d34a4a63dd..072bc98e4a3bd3f84d56fb6600ae9ef7a4c9f06e 100644 (file)
@@ -259,18 +259,22 @@ export function mergeDefaults(
       )
     : raw
   for (const key in defaults) {
-    const opt = props[key]
+    if (key.startsWith('__skip')) continue
+    let opt = props[key]
     if (opt) {
       if (isArray(opt) || isFunction(opt)) {
-        props[key] = { type: opt, default: defaults[key] }
+        opt = props[key] = { type: opt, default: defaults[key] }
       } else {
         opt.default = defaults[key]
       }
     } else if (opt === null) {
-      props[key] = { default: defaults[key] }
+      opt = props[key] = { default: defaults[key] }
     } else if (__DEV__) {
       warn(`props default key "${key}" has no corresponding declaration.`)
     }
+    if (opt && defaults[`__skip_${key}`]) {
+      opt.skipFactory = true
+    }
   }
   return props
 }
index 03c83990c577f870350744bf09068120c40e9861..186384d5b82fef1178dc01fdc293775caf13dcdc 100644 (file)
@@ -58,7 +58,14 @@ export interface PropOptions<T = any, D = T> {
   required?: boolean
   default?: D | DefaultFactory<D> | null | undefined | object
   validator?(value: unknown): boolean
+  /**
+   * @internal
+   */
   skipCheck?: boolean
+  /**
+   * @internal
+   */
+  skipFactory?: boolean
 }
 
 export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
@@ -425,7 +432,11 @@ function resolvePropValue(
     // default values
     if (hasDefault && value === undefined) {
       const defaultValue = opt.default
-      if (opt.type !== Function && isFunction(defaultValue)) {
+      if (
+        opt.type !== Function &&
+        !opt.skipFactory &&
+        isFunction(defaultValue)
+      ) {
         const { propsDefaults } = instance
         if (key in propsDefaults) {
           value = propsDefaults[key]