feat(sfc): withDefaults helper
This commit is contained in:
@@ -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)
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
if (!isCallOf(node, DEFINE_PROPS)) {
|
||||
return false
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
if (!isCallOf(node, c => c === DEFINE_EMIT || c === DEFINE_EMITS)) {
|
||||
return false
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user