| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397 |
- 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 {
- let paramsString = ''
- if (prop.params.length) {
- const start = prop.params[0].start
- const end = prop.params[prop.params.length - 1].end
- paramsString = ctx.getString({ start, end } as Node)
- }
- defaultString = `${prop.async ? 'async ' : ''}${
- prop.kind !== 'method' ? `${prop.kind} ` : ''
- }default(${paramsString}) ${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 inference 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'
- }
- }
|