]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(sfc): withDefaults helper
authorEvan You <yyx990803@gmail.com>
Sun, 27 Jun 2021 01:11:57 +0000 (21:11 -0400)
committerEvan You <yyx990803@gmail.com>
Sun, 27 Jun 2021 01:11:57 +0000 (21:11 -0400)
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.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
packages/runtime-core/src/index.ts
packages/sfc-playground/src/sfcCompiler.ts
test-dts/setupHelpers.test-d.ts

index 5e80e9790869ec074bfe54a6c8ccb9931bf22357..b1d54478b3beb50b9320c70453cbd858a081bdc6 100644 (file)
@@ -881,3 +881,50 @@ return {  }
 
 })"
 `;
+
+exports[`SFC compile <script setup> with TypeScript withDefaults (dynamic) 1`] = `
+"import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue'
+import { defaults } from './foo'
+      
+export default _defineComponent({
+  props: _mergeDefaults({
+    foo: { type: String, required: false },
+    bar: { type: Number, required: false }
+  }, { ...defaults }) as unknown as undefined,
+  setup(__props: {
+        foo?: string
+        bar?: number
+      }, { expose }) {
+  expose()
+
+const props = __props
+      
+      
+return { props, defaults }
+}
+
+})"
+`;
+
+exports[`SFC compile <script setup> with TypeScript withDefaults (static) 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default _defineComponent({
+  props: {
+    foo: { type: String, required: false, default: 'hi' },
+    bar: { type: Number, required: false }
+  } as unknown as undefined,
+  setup(__props: {
+        foo?: string
+        bar?: number
+      }, { expose }) {
+  expose()
+
+const props = __props
+      
+      
+return { props }
+}
+
+})"
+`;
index e0c2c5584b563f6f737b99601aeebc7f15af30dc..52272546ff4bd41a369637256df77e9e36477ee5 100644 (file)
@@ -592,6 +592,51 @@ const emit = defineEmits(['a', 'b'])
       })
     })
 
+    test('withDefaults (static)', () => {
+      const { content, bindings } = compile(`
+      <script setup lang="ts">
+      const props = withDefaults(defineProps<{
+        foo?: string
+        bar?: number
+      }>(), {
+        foo: 'hi'
+      })
+      </script>
+      `)
+      assertCode(content)
+      expect(content).toMatch(
+        `foo: { type: String, required: false, default: 'hi' }`
+      )
+      expect(content).toMatch(`bar: { type: Number, required: false }`)
+      expect(content).toMatch(`const props = __props`)
+      expect(bindings).toStrictEqual({
+        foo: BindingTypes.PROPS,
+        bar: BindingTypes.PROPS,
+        props: BindingTypes.SETUP_CONST
+      })
+    })
+
+    test('withDefaults (dynamic)', () => {
+      const { content } = compile(`
+      <script setup lang="ts">
+      import { defaults } from './foo'
+      const props = withDefaults(defineProps<{
+        foo?: string
+        bar?: number
+      }>(), { ...defaults })
+      </script>
+      `)
+      assertCode(content)
+      expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`)
+      expect(content).toMatch(
+        `
+  _mergeDefaults({
+    foo: { type: String, required: false },
+    bar: { type: Number, required: false }
+  }, { ...defaults })`.trim()
+      )
+    })
+
     test('defineEmits w/ type', () => {
       const { content } = compile(`
       <script setup lang="ts">
@@ -942,7 +987,6 @@ const emit = defineEmits(['a', 'b'])
     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`)
index 0eacf1f6ceb35c6da179eafabdd415a3d4c6c8ae..014a0afbfb491fb35ed0adf7b2270545c30cdd9d 100644 (file)
@@ -36,8 +36,11 @@ import { rewriteDefault } from './rewriteDefault'
 
 const DEFINE_PROPS = 'defineProps'
 const DEFINE_EMIT = 'defineEmit'
-const DEFINE_EMITS = 'defineEmits'
 const DEFINE_EXPOSE = 'defineExpose'
+const WITH_DEFAULTS = 'withDefaults'
+
+// deprecated
+const DEFINE_EMITS = 'defineEmits'
 
 export interface SFCScriptCompileOptions {
   /**
@@ -191,6 +194,7 @@ export function compileScript(
   let hasDefineEmitCall = false
   let hasDefineExposeCall = false
   let propsRuntimeDecl: Node | undefined
+  let propsRuntimeDefaults: Node | undefined
   let propsTypeDecl: TSTypeLiteral | undefined
   let propsIdentifier: string | undefined
   let emitRuntimeDecl: Node | undefined
@@ -262,68 +266,95 @@ export function compileScript(
   }
 
   function processDefineProps(node: Node): boolean {
-    if (isCallOf(node, DEFINE_PROPS)) {
-      if (hasDefinePropsCall) {
-        error(`duplicate ${DEFINE_PROPS}() call`, node)
+    if (!isCallOf(node, DEFINE_PROPS)) {
+      return false
+    }
+
+    if (hasDefinePropsCall) {
+      error(`duplicate ${DEFINE_PROPS}() call`, node)
+    }
+    hasDefinePropsCall = true
+
+    propsRuntimeDecl = node.arguments[0]
+
+    // 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
+        )
       }
-      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(
-            `type argument passed to ${DEFINE_PROPS}() must be a literal type.`,
-            typeArg
-          )
-        }
+
+      const typeArg = node.typeParameters.params[0]
+      if (typeArg.type === 'TSTypeLiteral') {
+        propsTypeDecl = typeArg
+      } else {
+        error(
+          `type argument passed to ${DEFINE_PROPS}() must be a literal type.`,
+          typeArg
+        )
       }
-      return true
     }
-    return false
+
+    return true
+  }
+
+  function processWithDefaults(node: Node): boolean {
+    if (!isCallOf(node, WITH_DEFAULTS)) {
+      return false
+    }
+    if (processDefineProps(node.arguments[0])) {
+      if (propsRuntimeDecl) {
+        error(
+          `${WITH_DEFAULTS} can only be used with type-based ` +
+            `${DEFINE_PROPS} declaration.`,
+          node
+        )
+      }
+      propsRuntimeDefaults = node.arguments[1]
+    } else {
+      error(
+        `${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
+        node.arguments[0] || node
+      )
+    }
+    return true
   }
 
   function processDefineEmits(node: Node): boolean {
-    if (isCallOf(node, DEFINE_EMIT) || isCallOf(node, DEFINE_EMITS)) {
-      if (hasDefineEmitCall) {
-        error(`duplicate ${DEFINE_EMITS}() call`, node)
+    if (!isCallOf(node, c => c === DEFINE_EMIT || c === DEFINE_EMITS)) {
+      return false
+    }
+    if (hasDefineEmitCall) {
+      error(`duplicate ${DEFINE_EMITS}() call`, node)
+    }
+    hasDefineEmitCall = true
+    emitRuntimeDecl = node.arguments[0]
+    if (node.typeParameters) {
+      if (emitRuntimeDecl) {
+        error(
+          `${DEFINE_EMIT}() cannot accept both type and non-type arguments ` +
+            `at the same time. Use one or the other.`,
+          node
+        )
       }
-      hasDefineEmitCall = true
-      emitRuntimeDecl = node.arguments[0]
-      if (node.typeParameters) {
-        if (emitRuntimeDecl) {
-          error(
-            `${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 === 'TSFunctionType' ||
-          typeArg.type === 'TSTypeLiteral'
-        ) {
-          emitTypeDecl = typeArg
-        } else {
-          error(
-            `type argument passed to ${DEFINE_EMITS}() must be a function type ` +
-              `or a literal type with call signatures.`,
-            typeArg
-          )
-        }
+      const typeArg = node.typeParameters.params[0]
+      if (
+        typeArg.type === 'TSFunctionType' ||
+        typeArg.type === 'TSTypeLiteral'
+      ) {
+        emitTypeDecl = typeArg
+      } else {
+        error(
+          `type argument passed to ${DEFINE_EMITS}() must be a function type ` +
+            `or a literal type with call signatures.`,
+          typeArg
+        )
       }
-      return true
     }
-    return false
+    return true
   }
 
   function processDefineExpose(node: Node): boolean {
@@ -480,6 +511,63 @@ 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 =
+      propsRuntimeDefaults &&
+      propsRuntimeDefaults.type === 'ObjectExpression' &&
+      propsRuntimeDefaults.properties.every(
+        node => node.type === 'ObjectProperty' && !node.computed
+      )
+
+    let propsDecls = `{
+    ${keys
+      .map(key => {
+        let defaultString: string | undefined
+        if (hasStaticDefaults) {
+          const prop = (propsRuntimeDefaults as ObjectExpression).properties.find(
+            (node: any) => node.key.name === key
+          ) as ObjectProperty
+          if (prop) {
+            // prop has corresponding static default value
+            defaultString = `default: ${source.slice(
+              prop.value.start! + startOffset,
+              prop.value.end! + startOffset
+            )}`
+          }
+        }
+
+        if (__DEV__) {
+          const { type, required } = props[key]
+          return `${key}: { type: ${toRuntimeTypeString(
+            type
+          )}, required: ${required}${
+            defaultString ? `, ${defaultString}` : ``
+          } }`
+        } else {
+          // production: checks are useless
+          return `${key}: ${defaultString ? `{ ${defaultString} }` : 'null'}`
+        }
+      })
+      .join(',\n    ')}\n  }`
+
+    if (propsRuntimeDefaults && !hasStaticDefaults) {
+      propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice(
+        propsRuntimeDefaults.start! + startOffset,
+        propsRuntimeDefaults.end! + startOffset
+      )})`
+    }
+
+    return `\n  props: ${propsDecls} as unknown as undefined,`
+  }
+
   // 1. process normal <script> first if it exists
   let scriptAst
   if (script) {
@@ -675,7 +763,8 @@ export function compileScript(
       // process `defineProps` and `defineEmit(s)` calls
       if (
         processDefineProps(node.expression) ||
-        processDefineEmits(node.expression)
+        processDefineEmits(node.expression) ||
+        processWithDefaults(node.expression)
       ) {
         s.remove(node.start! + startOffset, node.end! + startOffset)
       } else if (processDefineExpose(node.expression)) {
@@ -692,7 +781,8 @@ export function compileScript(
     if (node.type === 'VariableDeclaration' && !node.declare) {
       for (const decl of node.declarations) {
         if (decl.init) {
-          const isDefineProps = processDefineProps(decl.init)
+          const isDefineProps =
+            processDefineProps(decl.init) || processWithDefaults(decl.init)
           if (isDefineProps) {
             propsIdentifier = scriptSetup.content.slice(
               decl.id.start!,
@@ -812,6 +902,7 @@ export function compileScript(
   // 5. check useOptions args to make sure it doesn't reference setup scope
   // variables
   checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
+  checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
   checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS)
 
   // 6. remove non-script content
@@ -1080,9 +1171,14 @@ function walkDeclaration(
     for (const { id, init } of node.declarations) {
       const isDefineCall = !!(
         isConst &&
-        (isCallOf(init, DEFINE_PROPS) ||
-          isCallOf(init, DEFINE_EMIT) ||
-          isCallOf(init, DEFINE_EMITS))
+        isCallOf(
+          init,
+          c =>
+            c === DEFINE_PROPS ||
+            c === DEFINE_EMIT ||
+            c === DEFINE_EMITS ||
+            c === WITH_DEFAULTS
+        )
       )
       if (id.type === 'Identifier') {
         let bindingType
@@ -1318,29 +1414,6 @@ function inferRuntimeType(
   }
 }
 
-function genRuntimeProps(props: Record<string, PropTypeData>) {
-  const keys = Object.keys(props)
-  if (!keys.length) {
-    return ``
-  }
-
-  if (!__DEV__) {
-    // production: generate array version only
-    return `\n  props: [\n    ${keys
-      .map(k => JSON.stringify(k))
-      .join(',\n    ')}\n  ] as unknown as undefined,`
-  }
-
-  return `\n  props: {\n    ${keys
-    .map(key => {
-      const { type, required } = props[key]
-      return `${key}: { type: ${toRuntimeTypeString(
-        type
-      )}, required: ${required} }`
-    })
-    .join(',\n    ')}\n  } as unknown as undefined,`
-}
-
 function toRuntimeTypeString(types: string[]) {
   return types.some(t => t === 'null')
     ? `null`
@@ -1567,13 +1640,15 @@ function isFunction(node: Node): node is FunctionNode {
 
 function isCallOf(
   node: Node | null | undefined,
-  name: string
+  test: string | ((id: string) => boolean)
 ): node is CallExpression {
   return !!(
     node &&
     node.type === 'CallExpression' &&
     node.callee.type === 'Identifier' &&
-    node.callee.name === name
+    (typeof test === 'string'
+      ? node.callee.name === test
+      : test(node.callee.name))
   )
 }
 
index fbc884d0555d29b6a7b2c23156623e819d496cfb..c1e72ee4e6b412eaeadccb7daaeaf4714a028340 100644 (file)
@@ -8,8 +8,11 @@ import {
 import {
   defineEmits,
   defineProps,
+  defineExpose,
+  withDefaults,
   useAttrs,
-  useSlots
+  useSlots,
+  mergeDefaults
 } from '../src/apiSetupHelpers'
 
 describe('SFC <script setup> helpers', () => {
@@ -19,6 +22,12 @@ describe('SFC <script setup> helpers', () => {
 
     defineEmits()
     expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned()
+
+    defineExpose()
+    expect(`defineExpose() is a compiler-hint`).toHaveBeenWarned()
+
+    withDefaults({}, {})
+    expect(`withDefaults() is a compiler-hint`).toHaveBeenWarned()
   })
 
   test('useSlots / useAttrs (no args)', () => {
@@ -58,4 +67,26 @@ describe('SFC <script setup> helpers', () => {
     expect(slots).toBe(ctx!.slots)
     expect(attrs).toBe(ctx!.attrs)
   })
+
+  test('mergeDefaults', () => {
+    const merged = mergeDefaults(
+      {
+        foo: null,
+        bar: { type: String, required: false }
+      },
+      {
+        foo: 1,
+        bar: 'baz'
+      }
+    )
+    expect(merged).toMatchObject({
+      foo: { default: 1 },
+      bar: { type: String, required: false, default: 'baz' }
+    })
+
+    mergeDefaults({}, { foo: 1 })
+    expect(
+      `props default key "foo" has no corresponding declaration`
+    ).toHaveBeenWarned()
+  })
 })
index ff770e3ffc4239a5b55861715f06ee0c33d088d9..493cb71395991c478a6390d7753d425a50de1d44 100644 (file)
@@ -4,63 +4,104 @@ import {
   createSetupContext
 } from './component'
 import { EmitFn, EmitsOptions } from './componentEmits'
-import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
+import {
+  ComponentObjectPropsOptions,
+  PropOptions,
+  ExtractPropTypes
+} from './componentProps'
 import { warn } from './warning'
 
-type InferDefaults<T> = {
-  [K in keyof T]?: NonNullable<T[K]> extends object
-    ? () => NonNullable<T[K]>
-    : NonNullable<T[K]>
-}
+// dev only
+const warnRuntimeUsage = (method: string) =>
+  warn(
+    `${method}() 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.`
+  )
 
 /**
- * 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.
+ * Vue `<script setup>` compiler macro for declaring component props. The
+ * expected argument is the same as the component `props` option.
+ *
+ * Example runtime declaration:
+ * ```js
+ * // using Array syntax
+ * const props = defineProps(['foo', 'bar'])
+ * // using Object syntax
+ * const props = defineProps({
+ *   foo: String,
+ *   bar: {
+ *     type: Number,
+ *     required: true
+ *   }
+ * })
+ * ```
+ *
+ * Equivalent type-based decalration:
+ * ```ts
+ * // will be compiled into equivalent runtime declarations
+ * const props = defineProps<{
+ *   foo?: string
+ *   bar: number
+ * }>()
+ * ```
+ *
+ * This is only usable inside `<script setup>`, is compiled away in the
+ * output and should **not** be actually called at runtime.
  */
-// overload 1: string props
+// overload 1: runtime props w/ array
+export function defineProps<PropNames extends string = string>(
+  props: PropNames[]
+): Readonly<{ [key in PropNames]?: any }>
+// overload 2: runtime props w/ object
 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,
-  defaults?: InferDefaults<TypeProps>
-): Readonly<TypeProps extends undefined ? InferredProps : TypeProps>
+  PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions
+>(props: PP): Readonly<ExtractPropTypes<PP>>
+// overload 3: typed-based declaration
+export function defineProps<TypeProps>(): Readonly<TypeProps>
 // implementation
 export function defineProps() {
   if (__DEV__) {
-    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.`
-    )
+    warnRuntimeUsage(`defineProps`)
   }
   return null as any
 }
 
-export function defineEmits<
-  TypeEmit = undefined,
-  E extends EmitsOptions = EmitsOptions,
-  EE extends string = string,
-  InferredEmit = EmitFn<E>
->(emitOptions?: E | EE[]): TypeEmit extends undefined ? InferredEmit : TypeEmit
+/**
+ * Vue `<script setup>` compiler macro for declaring a component's emitted
+ * events. The expected argument is the same as the component `emits` option.
+ *
+ * Example runtime declaration:
+ * ```js
+ * const emit = defineEmits(['change', 'update'])
+ * ```
+ *
+ * Example type-based decalration:
+ * ```ts
+ * const emit = defineEmits<{
+ *   (event: 'change'): void
+ *   (event: 'update', id: number): void
+ * }>()
+ *
+ * emit('change')
+ * emit('update', 1)
+ * ```
+ *
+ * This is only usable inside `<script setup>`, is compiled away in the
+ * output and should **not** be actually called at runtime.
+ */
+// overload 1: runtime emits w/ array
+export function defineEmits<EE extends string = string>(
+  emitOptions: EE[]
+): EmitFn<EE[]>
+export function defineEmits<E extends EmitsOptions = EmitsOptions>(
+  emitOptions: E
+): EmitFn<E>
+export function defineEmits<TypeEmit>(): TypeEmit
 // implementation
 export function defineEmits() {
   if (__DEV__) {
-    warn(
-      `defineEmits() 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.`
-    )
+    warnRuntimeUsage(`defineEmits`)
   }
   return null as any
 }
@@ -70,14 +111,68 @@ export function defineEmits() {
  */
 export const defineEmit = defineEmits
 
+/**
+ * Vue `<script setup>` compiler macro for declaring a component's exposed
+ * instance properties when it is accessed by a parent component via template
+ * refs.
+ *
+ * `<script setup>` components are closed by default - i.e. varaibles inside
+ * the `<script setup>` scope is not exposed to parent unless explicitly exposed
+ * via `defineExpose`.
+ *
+ * This is only usable inside `<script setup>`, is compiled away in the
+ * output and should **not** be actually called at runtime.
+ */
 export function defineExpose(exposed?: Record<string, any>) {
   if (__DEV__) {
-    warn(
-      `defineExpose() is a compiler-hint helper that is only usable inside ` +
-        `<script setup> of a single file component. Its usage should be ` +
-        `compiled away and calling it at runtime has no effect.`
-    )
+    warnRuntimeUsage(`defineExpose`)
+  }
+}
+
+type NotUndefined<T> = T extends undefined ? never : T
+
+type InferDefaults<T> = {
+  [K in keyof T]?: NotUndefined<T[K]> extends (
+    | number
+    | string
+    | boolean
+    | symbol
+    | Function)
+    ? NotUndefined<T[K]>
+    : (props: T) => NotUndefined<T[K]>
+}
+
+type PropsWithDefaults<Base, Defaults> = Base &
+  {
+    [K in keyof Defaults]: K extends keyof Base ? NotUndefined<Base[K]> : never
+  }
+
+/**
+ * Vue `<script setup>` compiler macro for providing props default values when
+ * using type-based `defineProps` decalration.
+ *
+ * Example usage:
+ * ```ts
+ * withDefaults(defineProps<{
+ *   size?: number
+ *   labels?: string[]
+ * }>(), {
+ *   size: 3,
+ *   labels: () => ['default label']
+ * })
+ * ```
+ *
+ * This is only usable inside `<script setup>`, is compiled away in the output
+ * and should **not** be actually called at runtime.
+ */
+export function withDefaults<Props, Defaults extends InferDefaults<Props>>(
+  props: Props,
+  defaults: Defaults
+): PropsWithDefaults<Props, Defaults> {
+  if (__DEV__) {
+    warnRuntimeUsage(`withDefaults`)
   }
+  return null as any
 }
 
 /**
@@ -93,6 +188,14 @@ export function useContext(): SetupContext {
   return getContext()
 }
 
+export function useSlots(): SetupContext['slots'] {
+  return getContext().slots
+}
+
+export function useAttrs(): SetupContext['attrs'] {
+  return getContext().attrs
+}
+
 function getContext(): SetupContext {
   const i = getCurrentInstance()!
   if (__DEV__ && !i) {
@@ -101,10 +204,25 @@ function getContext(): SetupContext {
   return i.setupContext || (i.setupContext = createSetupContext(i))
 }
 
-export function useSlots(): SetupContext['slots'] {
-  return getContext().slots
-}
-
-export function useAttrs(): SetupContext['attrs'] {
-  return getContext().attrs
+/**
+ * Runtime helper for merging default declarations. Imported by compiled code
+ * only.
+ * @internal
+ */
+export function mergeDefaults(
+  // the base props is compiler-generated and guaranteed to be in this shape.
+  props: Record<string, PropOptions | null>,
+  defaults: Record<string, any>
+) {
+  for (const key in defaults) {
+    const val = props[key]
+    if (val) {
+      val.default = defaults[key]
+    } else if (val === null) {
+      props[key] = { default: defaults[key] }
+    } else if (__DEV__) {
+      warn(`props default key "${key}" has no corresponding declaration.`)
+    }
+  }
+  return props
 }
index b83f0abbe24c85f3c9e48b1fe932b4e70cb60b4f..5f73fbd84169c39d5711d91f5b4ef936dda8d6ef 100644 (file)
@@ -51,7 +51,7 @@ export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
 
 type DefaultFactory<T> = (props: Data) => T | null | undefined
 
-interface PropOptions<T = any, D = T> {
+export interface PropOptions<T = any, D = T> {
   type?: PropType<T> | true | null
   required?: boolean
   default?: D | DefaultFactory<D> | null | undefined | object
index e7140f3efbc6e02114ae4dc4e571a4d5100cf2b3..e7ef359d6e385942383afb701d7848fc5bb7ab84 100644 (file)
@@ -44,9 +44,17 @@ export { provide, inject } from './apiInject'
 export { nextTick } from './scheduler'
 export { defineComponent } from './apiDefineComponent'
 export { defineAsyncComponent } from './apiAsyncComponent'
+
+// <script setup> API ----------------------------------------------------------
+
 export {
   defineProps,
   defineEmits,
+  defineExpose,
+  withDefaults,
+  // internal
+  mergeDefaults,
+  // deprecated
   defineEmit,
   useContext
 } from './apiSetupHelpers'
@@ -140,7 +148,6 @@ export {
   DeepReadonly
 } from '@vue/reactivity'
 export {
-  // types
   WatchEffect,
   WatchOptions,
   WatchOptionsBase,
index 8fe74f9ecc1ec02cdee5d822d408889692a9dc4c..09e748053911d676329276a9ec37a07f622a94c5 100644 (file)
@@ -214,7 +214,12 @@ async function doCompileScript(
 
       return [code, compiledScript.bindings]
     } catch (e) {
-      store.errors = [e]
+      store.errors = [
+        e.stack
+          .split('\n')
+          .slice(0, 12)
+          .join('\n')
+      ]
       return
     }
   } else {
index 66c8fe5a3c28de784f36e97dc93fe0b780f3470f..2b5ab693e560725e8e7b2383057158fc3ef75bb3 100644 (file)
@@ -1,3 +1,4 @@
+import { withDefaults } from '../packages/runtime-core/src/apiSetupHelpers'
 import {
   expectType,
   defineProps,
@@ -19,30 +20,29 @@ describe('defineProps w/ type declaration', () => {
   props.bar
 })
 
-describe('defineProps w/ type declaration + defaults', () => {
-  defineProps<{
-    number?: number
-    arr?: string[]
-    arr2?: string[]
-    obj?: { x: number }
-    obj2?: { x: number }
-    obj3?: { x: number }
-  }>(
-    {},
+describe('defineProps w/ type declaration + withDefaults', () => {
+  const res = withDefaults(
+    defineProps<{
+      number?: number
+      arr?: string[]
+      obj?: { x: number }
+      fn?: (e: string) => void
+      x?: string
+    }>(),
     {
-      number: 1,
-
-      arr: () => [''],
-      // @ts-expect-error not using factory
-      arr2: [''],
-
+      number: 123,
+      arr: () => [],
       obj: () => ({ x: 123 }),
-      // @ts-expect-error not using factory
-      obj2: { x: 123 },
-      // @ts-expect-error factory return type does not match
-      obj3: () => ({ x: 'foo' })
+      fn: () => {}
     }
   )
+
+  res.number + 1
+  res.arr.push('hi')
+  res.obj.x
+  res.fn('hi')
+  // @ts-expect-error
+  res.x.slice()
 })
 
 describe('defineProps w/ runtime declaration', () => {