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