vue3-core/packages/compiler-sfc/src/script/defineProps.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

392 lines
11 KiB
TypeScript
Raw Normal View History

import type {
Expression,
LVal,
Node,
ObjectExpression,
ObjectMethod,
ObjectProperty,
} from '@babel/types'
import { BindingTypes, isFunctionType, unwrapTSNode } from '@vue/compiler-dom'
import type { ScriptCompileContext } from './context'
import {
type TypeResolveContext,
inferRuntimeType,
resolveTypeElements,
} from './resolveType'
import {
UNKNOWN_TYPE,
concatStrings,
getEscapedPropName,
isCallOf,
isLiteralNode,
resolveObjectKey,
toRuntimeTypeString,
} from './utils'
import { genModelProps } from './defineModel'
import { getObjectOrArrayExpressionKeys } from './analyzeScriptBindings'
import { processPropsDestructure } from './definePropsDestructure'
export const DEFINE_PROPS = 'defineProps'
export const WITH_DEFAULTS = 'withDefaults'
export interface PropTypeData {
key: string
type: string[]
required: boolean
skipCheck: boolean
}
export type PropsDestructureBindings = Record<
string, // public prop key
{
local: string // local identifier, may be different
default?: Expression
}
>
export function processDefineProps(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal,
isWithDefaults = false,
): boolean {
if (!isCallOf(node, DEFINE_PROPS)) {
return processWithDefaults(ctx, node, declId)
}
if (ctx.hasDefinePropsCall) {
ctx.error(`duplicate ${DEFINE_PROPS}() call`, node)
}
ctx.hasDefinePropsCall = true
ctx.propsRuntimeDecl = node.arguments[0]
// register bindings
if (ctx.propsRuntimeDecl) {
for (const key of getObjectOrArrayExpressionKeys(ctx.propsRuntimeDecl)) {
if (!(key in ctx.bindingMetadata)) {
ctx.bindingMetadata[key] = BindingTypes.PROPS
}
}
}
// call has type parameters - infer runtime types from it
if (node.typeParameters) {
if (ctx.propsRuntimeDecl) {
ctx.error(
`${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`,
node,
)
}
ctx.propsTypeDecl = node.typeParameters.params[0]
}
// handle props destructure
if (!isWithDefaults && declId && declId.type === 'ObjectPattern') {
processPropsDestructure(ctx, declId)
}
ctx.propsCall = node
ctx.propsDecl = declId
return true
}
function processWithDefaults(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal,
): boolean {
if (!isCallOf(node, WITH_DEFAULTS)) {
return false
}
if (
!processDefineProps(
ctx,
node.arguments[0],
declId,
true /* isWithDefaults */,
)
) {
ctx.error(
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
node.arguments[0] || node,
)
}
if (ctx.propsRuntimeDecl) {
ctx.error(
`${WITH_DEFAULTS} can only be used with type-based ` +
`${DEFINE_PROPS} declaration.`,
node,
)
}
if (declId && declId.type === 'ObjectPattern') {
ctx.warn(
`${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
`Reactive destructure will be disabled when using withDefaults().\n` +
`Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...). `,
node.callee,
)
}
ctx.propsRuntimeDefaults = node.arguments[1]
if (!ctx.propsRuntimeDefaults) {
ctx.error(`The 2nd argument of ${WITH_DEFAULTS} is required.`, node)
}
ctx.propsCall = node
return true
}
export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
let propsDecls: undefined | string
if (ctx.propsRuntimeDecl) {
propsDecls = ctx.getString(ctx.propsRuntimeDecl).trim()
if (ctx.propsDestructureDecl) {
const defaults: string[] = []
for (const key in ctx.propsDestructuredBindings) {
const d = genDestructuredDefaultValue(ctx, key)
const finalKey = getEscapedPropName(key)
if (d)
defaults.push(
`${finalKey}: ${d.valueString}${
d.needSkipFactory ? `, __skip_${finalKey}: true` : ``
}`,
)
}
if (defaults.length) {
propsDecls = `/*@__PURE__*/${ctx.helper(
`mergeDefaults`,
)}(${propsDecls}, {\n ${defaults.join(',\n ')}\n})`
}
}
} else if (ctx.propsTypeDecl) {
propsDecls = extractRuntimeProps(ctx)
}
const modelsDecls = genModelProps(ctx)
if (propsDecls && modelsDecls) {
return `/*@__PURE__*/${ctx.helper(
'mergeModels',
)}(${propsDecls}, ${modelsDecls})`
} else {
return modelsDecls || propsDecls
}
}
export function extractRuntimeProps(
ctx: TypeResolveContext,
): string | undefined {
// this is only called if propsTypeDecl exists
const props = resolveRuntimePropsFromType(ctx, ctx.propsTypeDecl!)
if (!props.length) {
return
}
const propStrings: string[] = []
const hasStaticDefaults = hasStaticWithDefaults(ctx)
for (const prop of props) {
propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults))
// register bindings
if ('bindingMetadata' in ctx && !(prop.key in ctx.bindingMetadata)) {
ctx.bindingMetadata[prop.key] = BindingTypes.PROPS
}
}
let propsDecls = `{
${propStrings.join(',\n ')}\n }`
if (ctx.propsRuntimeDefaults && !hasStaticDefaults) {
propsDecls = `/*@__PURE__*/${ctx.helper(
'mergeDefaults',
)}(${propsDecls}, ${ctx.getString(ctx.propsRuntimeDefaults)})`
}
return propsDecls
}
function resolveRuntimePropsFromType(
ctx: TypeResolveContext,
node: Node,
): PropTypeData[] {
const props: PropTypeData[] = []
const elements = resolveTypeElements(ctx, node)
for (const key in elements.props) {
const e = elements.props[key]
let type = inferRuntimeType(ctx, e)
let skipCheck = false
// skip check for result containing unknown types
if (type.includes(UNKNOWN_TYPE)) {
if (type.includes('Boolean') || type.includes('Function')) {
type = type.filter(t => t !== UNKNOWN_TYPE)
skipCheck = true
} else {
type = ['null']
}
}
props.push({
key,
required: !e.optional,
type: type || [`null`],
skipCheck,
})
}
return props
}
function genRuntimePropFromType(
ctx: TypeResolveContext,
{ key, required, type, skipCheck }: PropTypeData,
hasStaticDefaults: boolean,
): string {
let defaultString: string | undefined
const destructured = genDestructuredDefaultValue(ctx, key, type)
if (destructured) {
defaultString = `default: ${destructured.valueString}${
destructured.needSkipFactory ? `, skipFactory: true` : ``
}`
} else if (hasStaticDefaults) {
const prop = (ctx.propsRuntimeDefaults as ObjectExpression).properties.find(
node => {
if (node.type === 'SpreadElement') return false
return resolveObjectKey(node.key, node.computed) === key
},
) as ObjectProperty | ObjectMethod
if (prop) {
if (prop.type === 'ObjectProperty') {
// prop has corresponding static default value
defaultString = `default: ${ctx.getString(prop.value)}`
} else {
defaultString = `${prop.async ? 'async ' : ''}${
prop.kind !== 'method' ? `${prop.kind} ` : ''
}default() ${ctx.getString(prop.body)}`
}
}
}
const finalKey = getEscapedPropName(key)
if (!ctx.options.isProd) {
return `${finalKey}: { ${concatStrings([
`type: ${toRuntimeTypeString(type)}`,
`required: ${required}`,
skipCheck && 'skipCheck: true',
defaultString,
])} }`
} else if (
type.some(
el =>
el === 'Boolean' ||
((!hasStaticDefaults || defaultString) && el === 'Function'),
)
) {
// #4783 for boolean, should keep the type
// #7111 for function, if default value exists or it's not static, should keep it
// in production
return `${finalKey}: { ${concatStrings([
`type: ${toRuntimeTypeString(type)}`,
defaultString,
])} }`
} else {
// #8989 for custom element, should keep the type
if (ctx.isCE) {
if (defaultString) {
return `${finalKey}: ${`{ ${defaultString}, type: ${toRuntimeTypeString(
type,
)} }`}`
} else {
return `${finalKey}: {type: ${toRuntimeTypeString(type)}}`
}
}
// production: checks are useless
return `${finalKey}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
}
}
/**
* check defaults. If the default object is an object literal with only
* static properties, we can directly generate more optimized default
* declarations. Otherwise we will have to fallback to runtime merging.
*/
function hasStaticWithDefaults(ctx: TypeResolveContext) {
return !!(
ctx.propsRuntimeDefaults &&
ctx.propsRuntimeDefaults.type === 'ObjectExpression' &&
ctx.propsRuntimeDefaults.properties.every(
node =>
node.type !== 'SpreadElement' &&
(!node.computed || node.key.type.endsWith('Literal')),
)
)
}
function genDestructuredDefaultValue(
ctx: TypeResolveContext,
key: string,
inferredType?: string[],
):
| {
valueString: string
needSkipFactory: boolean
}
| undefined {
const destructured = ctx.propsDestructuredBindings[key]
const defaultVal = destructured && destructured.default
if (defaultVal) {
const value = ctx.getString(defaultVal)
const unwrapped = unwrapTSNode(defaultVal)
if (inferredType && inferredType.length && !inferredType.includes('null')) {
const valueType = inferValueType(unwrapped)
if (valueType && !inferredType.includes(valueType)) {
ctx.error(
`Default value of prop "${key}" does not match declared type.`,
unwrapped,
)
}
}
// If the default value is a function or is an identifier referencing
// external value, skip factory wrap. This is needed when using
// destructure w/ runtime declaration since we cannot safely infer
// whether the expected runtime prop type is `Function`.
const needSkipFactory =
!inferredType &&
(isFunctionType(unwrapped) || unwrapped.type === 'Identifier')
const needFactoryWrap =
!needSkipFactory &&
!isLiteralNode(unwrapped) &&
!inferredType?.includes('Function')
return {
valueString: needFactoryWrap ? `() => (${value})` : value,
needSkipFactory,
}
}
}
// non-comprehensive, best-effort type infernece for a runtime value
// this is used to catch default value / type declaration mismatches
// when using props destructure.
function inferValueType(node: Node): string | undefined {
switch (node.type) {
case 'StringLiteral':
return 'String'
case 'NumericLiteral':
return 'Number'
case 'BooleanLiteral':
return 'Boolean'
case 'ObjectExpression':
return 'Object'
case 'ArrayExpression':
return 'Array'
case 'FunctionExpression':
case 'ArrowFunctionExpression':
return 'Function'
}
}