})"
`;
+
+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 }
+}
+
+})"
+`;
})
})
+ 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">
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`)
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 {
/**
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
}
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 {
}
}
+ 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) {
// 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)) {
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!,
// 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
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
}
}
-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`
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))
)
}
import {
defineEmits,
defineProps,
+ defineExpose,
+ withDefaults,
useAttrs,
- useSlots
+ useSlots,
+ mergeDefaults
} from '../src/apiSetupHelpers'
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)', () => {
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()
+ })
})
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
}
*/
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
}
/**
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) {
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
}
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
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'
DeepReadonly
} from '@vue/reactivity'
export {
- // types
WatchEffect,
WatchOptions,
WatchOptionsBase,
return [code, compiledScript.bindings]
} catch (e) {
- store.errors = [e]
+ store.errors = [
+ e.stack
+ .split('\n')
+ .slice(0, 12)
+ .join('\n')
+ ]
return
}
} else {
+import { withDefaults } from '../packages/runtime-core/src/apiSetupHelpers'
import {
expectType,
defineProps,
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', () => {