defineProps.ts 11 KB

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