defineProps.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. import type {
  2. Expression,
  3. LVal,
  4. Node,
  5. ObjectExpression,
  6. ObjectMethod,
  7. ObjectProperty,
  8. } from '@babel/types'
  9. import { BindingTypes, isFunctionType, unwrapTSNode } from '@vue/compiler-dom'
  10. import type { ScriptCompileContext } from './context'
  11. import {
  12. type TypeResolveContext,
  13. inferRuntimeType,
  14. resolveTypeElements,
  15. } from './resolveType'
  16. import {
  17. UNKNOWN_TYPE,
  18. concatStrings,
  19. getEscapedPropName,
  20. isCallOf,
  21. isLiteralNode,
  22. resolveObjectKey,
  23. toRuntimeTypeString,
  24. } from './utils'
  25. import { genModelProps } from './defineModel'
  26. import { getObjectOrArrayExpressionKeys } from './analyzeScriptBindings'
  27. import { processPropsDestructure } from './definePropsDestructure'
  28. export const DEFINE_PROPS = 'defineProps'
  29. export const WITH_DEFAULTS = 'withDefaults'
  30. export interface PropTypeData {
  31. key: string
  32. type: string[]
  33. required: boolean
  34. skipCheck: boolean
  35. }
  36. export type PropsDestructureBindings = Record<
  37. string, // public prop key
  38. {
  39. local: string // local identifier, may be different
  40. default?: Expression
  41. }
  42. >
  43. export function processDefineProps(
  44. ctx: ScriptCompileContext,
  45. node: Node,
  46. declId?: LVal,
  47. isWithDefaults = false,
  48. ): boolean {
  49. if (!isCallOf(node, DEFINE_PROPS)) {
  50. return processWithDefaults(ctx, node, declId)
  51. }
  52. if (ctx.hasDefinePropsCall) {
  53. ctx.error(`duplicate ${DEFINE_PROPS}() call`, node)
  54. }
  55. ctx.hasDefinePropsCall = true
  56. ctx.propsRuntimeDecl = node.arguments[0]
  57. // register bindings
  58. if (ctx.propsRuntimeDecl) {
  59. for (const key of getObjectOrArrayExpressionKeys(ctx.propsRuntimeDecl)) {
  60. if (!(key in ctx.bindingMetadata)) {
  61. ctx.bindingMetadata[key] = BindingTypes.PROPS
  62. }
  63. }
  64. }
  65. // call has type parameters - infer runtime types from it
  66. if (node.typeParameters) {
  67. if (ctx.propsRuntimeDecl) {
  68. ctx.error(
  69. `${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
  70. `at the same time. Use one or the other.`,
  71. node,
  72. )
  73. }
  74. ctx.propsTypeDecl = node.typeParameters.params[0]
  75. // register bindings
  76. const { props } = resolveTypeElements(ctx, ctx.propsTypeDecl)
  77. if (props) {
  78. for (const key in props) {
  79. if (!(key in ctx.bindingMetadata)) {
  80. ctx.bindingMetadata[key] = BindingTypes.PROPS
  81. }
  82. }
  83. }
  84. }
  85. // handle props destructure
  86. if (!isWithDefaults && declId && declId.type === 'ObjectPattern') {
  87. processPropsDestructure(ctx, declId)
  88. }
  89. ctx.propsCall = node
  90. ctx.propsDecl = declId
  91. return true
  92. }
  93. function processWithDefaults(
  94. ctx: ScriptCompileContext,
  95. node: Node,
  96. declId?: LVal,
  97. ): boolean {
  98. if (!isCallOf(node, WITH_DEFAULTS)) {
  99. return false
  100. }
  101. if (
  102. !processDefineProps(
  103. ctx,
  104. node.arguments[0],
  105. declId,
  106. true /* isWithDefaults */,
  107. )
  108. ) {
  109. ctx.error(
  110. `${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
  111. node.arguments[0] || node,
  112. )
  113. }
  114. if (ctx.propsRuntimeDecl) {
  115. ctx.error(
  116. `${WITH_DEFAULTS} can only be used with type-based ` +
  117. `${DEFINE_PROPS} declaration.`,
  118. node,
  119. )
  120. }
  121. if (declId && declId.type === 'ObjectPattern') {
  122. ctx.warn(
  123. `${WITH_DEFAULTS}() is unnecessary when using destructure with ${DEFINE_PROPS}().\n` +
  124. `Reactive destructure will be disabled when using withDefaults().\n` +
  125. `Prefer using destructure default values, e.g. const { foo = 1 } = defineProps(...). `,
  126. node.callee,
  127. )
  128. }
  129. ctx.propsRuntimeDefaults = node.arguments[1]
  130. if (!ctx.propsRuntimeDefaults) {
  131. ctx.error(`The 2nd argument of ${WITH_DEFAULTS} is required.`, node)
  132. }
  133. ctx.propsCall = node
  134. return true
  135. }
  136. export function genRuntimeProps(ctx: ScriptCompileContext): string | undefined {
  137. let propsDecls: undefined | string
  138. if (ctx.propsRuntimeDecl) {
  139. propsDecls = ctx.getString(ctx.propsRuntimeDecl).trim()
  140. if (ctx.propsDestructureDecl) {
  141. const defaults: string[] = []
  142. for (const key in ctx.propsDestructuredBindings) {
  143. const d = genDestructuredDefaultValue(ctx, key)
  144. const finalKey = getEscapedPropName(key)
  145. if (d)
  146. defaults.push(
  147. `${finalKey}: ${d.valueString}${
  148. d.needSkipFactory ? `, __skip_${finalKey}: true` : ``
  149. }`,
  150. )
  151. }
  152. if (defaults.length) {
  153. propsDecls = `/*@__PURE__*/${ctx.helper(
  154. `mergeDefaults`,
  155. )}(${propsDecls}, {\n ${defaults.join(',\n ')}\n})`
  156. }
  157. }
  158. } else if (ctx.propsTypeDecl) {
  159. propsDecls = extractRuntimeProps(ctx)
  160. }
  161. const modelsDecls = genModelProps(ctx)
  162. if (propsDecls && modelsDecls) {
  163. return `/*@__PURE__*/${ctx.helper(
  164. 'mergeModels',
  165. )}(${propsDecls}, ${modelsDecls})`
  166. } else {
  167. return modelsDecls || propsDecls
  168. }
  169. }
  170. export function extractRuntimeProps(
  171. ctx: TypeResolveContext,
  172. ): string | undefined {
  173. // this is only called if propsTypeDecl exists
  174. const props = resolveRuntimePropsFromType(ctx, ctx.propsTypeDecl!)
  175. if (!props.length) {
  176. return
  177. }
  178. const propStrings: string[] = []
  179. const hasStaticDefaults = hasStaticWithDefaults(ctx)
  180. for (const prop of props) {
  181. propStrings.push(genRuntimePropFromType(ctx, prop, hasStaticDefaults))
  182. }
  183. let propsDecls = `{
  184. ${propStrings.join(',\n ')}\n }`
  185. if (ctx.propsRuntimeDefaults && !hasStaticDefaults) {
  186. propsDecls = `/*@__PURE__*/${ctx.helper(
  187. 'mergeDefaults',
  188. )}(${propsDecls}, ${ctx.getString(ctx.propsRuntimeDefaults)})`
  189. }
  190. return propsDecls
  191. }
  192. function resolveRuntimePropsFromType(
  193. ctx: TypeResolveContext,
  194. node: Node,
  195. ): PropTypeData[] {
  196. const props: PropTypeData[] = []
  197. const elements = resolveTypeElements(ctx, node)
  198. for (const key in elements.props) {
  199. const e = elements.props[key]
  200. let type = inferRuntimeType(ctx, e)
  201. let skipCheck = false
  202. // skip check for result containing unknown types
  203. if (type.includes(UNKNOWN_TYPE)) {
  204. if (type.includes('Boolean') || type.includes('Function')) {
  205. type = type.filter(t => t !== UNKNOWN_TYPE)
  206. skipCheck = true
  207. } else {
  208. type = ['null']
  209. }
  210. }
  211. props.push({
  212. key,
  213. required: !e.optional,
  214. type: type || [`null`],
  215. skipCheck,
  216. })
  217. }
  218. return props
  219. }
  220. function genRuntimePropFromType(
  221. ctx: TypeResolveContext,
  222. { key, required, type, skipCheck }: PropTypeData,
  223. hasStaticDefaults: boolean,
  224. ): string {
  225. let defaultString: string | undefined
  226. const destructured = genDestructuredDefaultValue(ctx, key, type)
  227. if (destructured) {
  228. defaultString = `default: ${destructured.valueString}${
  229. destructured.needSkipFactory ? `, skipFactory: true` : ``
  230. }`
  231. } else if (hasStaticDefaults) {
  232. const prop = (ctx.propsRuntimeDefaults as ObjectExpression).properties.find(
  233. node => {
  234. if (node.type === 'SpreadElement') return false
  235. return resolveObjectKey(node.key, node.computed) === key
  236. },
  237. ) as ObjectProperty | ObjectMethod
  238. if (prop) {
  239. if (prop.type === 'ObjectProperty') {
  240. // prop has corresponding static default value
  241. defaultString = `default: ${ctx.getString(prop.value)}`
  242. } else {
  243. defaultString = `${prop.async ? 'async ' : ''}${
  244. prop.kind !== 'method' ? `${prop.kind} ` : ''
  245. }default() ${ctx.getString(prop.body)}`
  246. }
  247. }
  248. }
  249. const finalKey = getEscapedPropName(key)
  250. if (!ctx.options.isProd) {
  251. return `${finalKey}: { ${concatStrings([
  252. `type: ${toRuntimeTypeString(type)}`,
  253. `required: ${required}`,
  254. skipCheck && 'skipCheck: true',
  255. defaultString,
  256. ])} }`
  257. } else if (
  258. type.some(
  259. el =>
  260. el === 'Boolean' ||
  261. ((!hasStaticDefaults || defaultString) && el === 'Function'),
  262. )
  263. ) {
  264. // #4783 for boolean, should keep the type
  265. // #7111 for function, if default value exists or it's not static, should keep it
  266. // in production
  267. return `${finalKey}: { ${concatStrings([
  268. `type: ${toRuntimeTypeString(type)}`,
  269. defaultString,
  270. ])} }`
  271. } else {
  272. // #8989 for custom element, should keep the type
  273. if (ctx.isCE) {
  274. if (defaultString) {
  275. return `${finalKey}: ${`{ ${defaultString}, type: ${toRuntimeTypeString(
  276. type,
  277. )} }`}`
  278. } else {
  279. return `${finalKey}: {type: ${toRuntimeTypeString(type)}}`
  280. }
  281. }
  282. // production: checks are useless
  283. return `${finalKey}: ${defaultString ? `{ ${defaultString} }` : `{}`}`
  284. }
  285. }
  286. /**
  287. * check defaults. If the default object is an object literal with only
  288. * static properties, we can directly generate more optimized default
  289. * declarations. Otherwise we will have to fallback to runtime merging.
  290. */
  291. function hasStaticWithDefaults(ctx: TypeResolveContext) {
  292. return !!(
  293. ctx.propsRuntimeDefaults &&
  294. ctx.propsRuntimeDefaults.type === 'ObjectExpression' &&
  295. ctx.propsRuntimeDefaults.properties.every(
  296. node =>
  297. node.type !== 'SpreadElement' &&
  298. (!node.computed || node.key.type.endsWith('Literal')),
  299. )
  300. )
  301. }
  302. function genDestructuredDefaultValue(
  303. ctx: TypeResolveContext,
  304. key: string,
  305. inferredType?: string[],
  306. ):
  307. | {
  308. valueString: string
  309. needSkipFactory: boolean
  310. }
  311. | undefined {
  312. const destructured = ctx.propsDestructuredBindings[key]
  313. const defaultVal = destructured && destructured.default
  314. if (defaultVal) {
  315. const value = ctx.getString(defaultVal)
  316. const unwrapped = unwrapTSNode(defaultVal)
  317. if (inferredType && inferredType.length && !inferredType.includes('null')) {
  318. const valueType = inferValueType(unwrapped)
  319. if (valueType && !inferredType.includes(valueType)) {
  320. ctx.error(
  321. `Default value of prop "${key}" does not match declared type.`,
  322. unwrapped,
  323. )
  324. }
  325. }
  326. // If the default value is a function or is an identifier referencing
  327. // external value, skip factory wrap. This is needed when using
  328. // destructure w/ runtime declaration since we cannot safely infer
  329. // whether the expected runtime prop type is `Function`.
  330. const needSkipFactory =
  331. !inferredType &&
  332. (isFunctionType(unwrapped) || unwrapped.type === 'Identifier')
  333. const needFactoryWrap =
  334. !needSkipFactory &&
  335. !isLiteralNode(unwrapped) &&
  336. !inferredType?.includes('Function')
  337. return {
  338. valueString: needFactoryWrap ? `() => (${value})` : value,
  339. needSkipFactory,
  340. }
  341. }
  342. }
  343. // non-comprehensive, best-effort type infernece for a runtime value
  344. // this is used to catch default value / type declaration mismatches
  345. // when using props destructure.
  346. function inferValueType(node: Node): string | undefined {
  347. switch (node.type) {
  348. case 'StringLiteral':
  349. return 'String'
  350. case 'NumericLiteral':
  351. return 'Number'
  352. case 'BooleanLiteral':
  353. return 'Boolean'
  354. case 'ObjectExpression':
  355. return 'Object'
  356. case 'ArrayExpression':
  357. return 'Array'
  358. case 'FunctionExpression':
  359. case 'ArrowFunctionExpression':
  360. return 'Function'
  361. }
  362. }