diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
index 5e80e979086..b1d54478b3b 100644
--- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
+++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
@@ -881,3 +881,50 @@ return { }
})"
`;
+
+exports[`SFC compile
+ `)
+ 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(`
+
+ `)
+ 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(`
`)
}).toThrow(`cannot accept both type and non-type arguments`)
diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts
index 0eacf1f6ceb..014a0afbfb4 100644
--- a/packages/compiler-sfc/src/compileScript.ts
+++ b/packages/compiler-sfc/src/compileScript.ts
@@ -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) {
+ 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