]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-sfc): generate matching prop types when withDefaults is used (#4466)
authorwebfansplz <308241863@qq.com>
Thu, 2 Sep 2021 15:26:52 +0000 (23:26 +0800)
committerGitHub <noreply@github.com>
Thu, 2 Sep 2021 15:26:52 +0000 (11:26 -0400)
fix #4455

packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts

index 8e2a50971b9061cf35f155a2d13a4b93f69b3ddc..9e63ee1a42c439fee444a3d023ddf85c4f563dbf 100644 (file)
@@ -1037,11 +1037,13 @@ import { defaults } from './foo'
 export default /*#__PURE__*/_defineComponent({
   props: _mergeDefaults({
     foo: { type: String, required: false },
-    bar: { type: Number, required: false }
+    bar: { type: Number, required: false },
+    baz: { type: Boolean, required: true }
   }, { ...defaults }) as unknown as undefined,
   setup(__props: {
         foo?: string
         bar?: number
+        baz: boolean
       }, { expose }) {
   expose()
 
@@ -1060,12 +1062,11 @@ exports[`SFC compile <script setup> with TypeScript withDefaults (static) 1`] =
 export default /*#__PURE__*/_defineComponent({
   props: {
     foo: { type: String, required: false, default: 'hi' },
-    bar: { type: Number, required: false }
+    bar: { type: Number, required: false },
+    baz: { type: Boolean, required: true },
+    qux: { type: Function, required: false, default() { return 1 } }
   } as unknown as undefined,
-  setup(__props: {
-        foo?: string
-        bar?: number
-      }, { expose }) {
+  setup(__props: { foo: string, bar?: number, baz: boolean, qux(): number }, { expose }) {
   expose()
 
 const props = __props
index 0db19188b615640cbb4c709538f327882fad119d..8a8b591ce0977c2be39decc2f2f1a73ae8493abf 100644 (file)
@@ -800,8 +800,11 @@ const emit = defineEmits(['a', 'b'])
       const props = withDefaults(defineProps<{
         foo?: string
         bar?: number
+        baz: boolean
+        qux?(): number
       }>(), {
-        foo: 'hi'
+        foo: 'hi',
+        qux() { return 1 }
       })
       </script>
       `)
@@ -810,10 +813,19 @@ const emit = defineEmits(['a', 'b'])
         `foo: { type: String, required: false, default: 'hi' }`
       )
       expect(content).toMatch(`bar: { type: Number, required: false }`)
+      expect(content).toMatch(`baz: { type: Boolean, required: true }`)
+      expect(content).toMatch(
+        `qux: { type: Function, required: false, default() { return 1 } }`
+      )
+      expect(content).toMatch(
+        `{ foo: string, bar?: number, baz: boolean, qux(): number }`
+      )
       expect(content).toMatch(`const props = __props`)
       expect(bindings).toStrictEqual({
         foo: BindingTypes.PROPS,
         bar: BindingTypes.PROPS,
+        baz: BindingTypes.PROPS,
+        qux: BindingTypes.PROPS,
         props: BindingTypes.SETUP_CONST
       })
     })
@@ -825,6 +837,7 @@ const emit = defineEmits(['a', 'b'])
       const props = withDefaults(defineProps<{
         foo?: string
         bar?: number
+        baz: boolean
       }>(), { ...defaults })
       </script>
       `)
@@ -834,7 +847,8 @@ const emit = defineEmits(['a', 'b'])
         `
   _mergeDefaults({
     foo: { type: String, required: false },
-    bar: { type: Number, required: false }
+    bar: { type: Number, required: false },
+    baz: { type: Boolean, required: true }
   }, { ...defaults })`.trim()
       )
     })
index 6a8d17968447ac30cad089412de071349494820b..ac38540f60ef476151168cde093a1895232b5c26 100644 (file)
@@ -38,7 +38,8 @@ import {
   RestElement,
   TSInterfaceBody,
   AwaitExpression,
-  Program
+  Program,
+  ObjectMethod
 } from '@babel/types'
 import { walk } from 'estree-walker'
 import { RawSourceMap } from 'source-map'
@@ -242,7 +243,7 @@ export function compileScript(
   let hasDefineEmitCall = false
   let hasDefineExposeCall = false
   let propsRuntimeDecl: Node | undefined
-  let propsRuntimeDefaults: Node | undefined
+  let propsRuntimeDefaults: ObjectExpression | undefined
   let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
   let propsTypeDeclRaw: Node | undefined
   let propsIdentifier: string | undefined
@@ -384,7 +385,16 @@ export function compileScript(
           node
         )
       }
-      propsRuntimeDefaults = node.arguments[1]
+      propsRuntimeDefaults = node.arguments[1] as ObjectExpression
+      if (
+        !propsRuntimeDefaults ||
+        propsRuntimeDefaults.type !== 'ObjectExpression'
+      ) {
+        error(
+          `The 2nd argument of ${WITH_DEFAULTS} must be an object literal.`,
+          propsRuntimeDefaults || node
+        )
+      }
     } else {
       error(
         `${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
@@ -513,38 +523,51 @@ export function compileScript(
     )
   }
 
-  function genRuntimeProps(props: Record<string, PropTypeData>) {
-    const keys = Object.keys(props)
-    if (!keys.length) {
-      return ``
-    }
-
-    // check defaults. If the default object is an object literal with only
-    // static properties, we can directly generate more optimzied default
-    // decalrations. Otherwise we will have to fallback to runtime merging.
-    const hasStaticDefaults =
+  /**
+   * check defaults. If the default object is an object literal with only
+   * static properties, we can directly generate more optimzied default
+   * decalrations. Otherwise we will have to fallback to runtime merging.
+   */
+  function checkStaticDefaults() {
+    return (
       propsRuntimeDefaults &&
       propsRuntimeDefaults.type === 'ObjectExpression' &&
       propsRuntimeDefaults.properties.every(
-        node => node.type === 'ObjectProperty' && !node.computed
+        node =>
+          (node.type === 'ObjectProperty' && !node.computed) ||
+          node.type === 'ObjectMethod'
       )
+    )
+  }
 
+  function genRuntimeProps(props: Record<string, PropTypeData>) {
+    const keys = Object.keys(props)
+    if (!keys.length) {
+      return ``
+    }
+    const hasStaticDefaults = checkStaticDefaults()
+    const scriptSetupSource = scriptSetup!.content
     let propsDecls = `{
     ${keys
       .map(key => {
         let defaultString: string | undefined
         if (hasStaticDefaults) {
-          const prop = (
-            propsRuntimeDefaults as ObjectExpression
-          ).properties.find(
+          const prop = propsRuntimeDefaults!.properties.find(
             (node: any) => node.key.name === key
-          ) as ObjectProperty
+          ) as ObjectProperty | ObjectMethod
           if (prop) {
-            // prop has corresponding static default value
-            defaultString = `default: ${source.slice(
-              prop.value.start! + startOffset,
-              prop.value.end! + startOffset
-            )}`
+            if (prop.type === 'ObjectProperty') {
+              // prop has corresponding static default value
+              defaultString = `default: ${scriptSetupSource.slice(
+                prop.value.start!,
+                prop.value.end!
+              )}`
+            } else {
+              defaultString = `default() ${scriptSetupSource.slice(
+                prop.body.start!,
+                prop.body.end!
+              )}`
+            }
           }
         }
 
@@ -572,6 +595,44 @@ export function compileScript(
     return `\n  props: ${propsDecls} as unknown as undefined,`
   }
 
+  function genSetupPropsType(node: TSTypeLiteral | TSInterfaceBody) {
+    const scriptSetupSource = scriptSetup!.content
+    if (checkStaticDefaults()) {
+      // if withDefaults() is used, we need to remove the optional flags
+      // on props that have default values
+      let res = `: { `
+      const members = node.type === 'TSTypeLiteral' ? node.members : node.body
+      for (const m of members) {
+        if (
+          (m.type === 'TSPropertySignature' ||
+            m.type === 'TSMethodSignature') &&
+          m.typeAnnotation &&
+          m.key.type === 'Identifier'
+        ) {
+          if (
+            propsRuntimeDefaults!.properties.some(
+              (p: any) => p.key.name === (m.key as Identifier).name
+            )
+          ) {
+            res +=
+              m.key.name +
+              (m.type === 'TSMethodSignature' ? '()' : '') +
+              scriptSetupSource.slice(
+                m.typeAnnotation.start!,
+                m.typeAnnotation.end!
+              ) +
+              ', '
+          } else {
+            res += scriptSetupSource.slice(m.start!, m.end!) + `, `
+          }
+        }
+      }
+      return (res.length ? res.slice(0, -2) : res) + ` }`
+    } else {
+      return `: ${scriptSetupSource.slice(node.start!, node.end!)}`
+    }
+  }
+
   // 1. process normal <script> first if it exists
   let scriptAst
   if (script) {
@@ -990,10 +1051,7 @@ export function compileScript(
   // 9. finalize setup() argument signature
   let args = `__props`
   if (propsTypeDecl) {
-    args += `: ${scriptSetup.content.slice(
-      propsTypeDecl.start!,
-      propsTypeDecl.end!
-    )}`
+    args += genSetupPropsType(propsTypeDecl)
   }
   // inject user assignment of props
   // we use a default __props so that template expressions referencing props