ssrTransformElement.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. import {
  2. NodeTransform,
  3. NodeTypes,
  4. ElementTypes,
  5. TemplateLiteral,
  6. createTemplateLiteral,
  7. createInterpolation,
  8. createCallExpression,
  9. createConditionalExpression,
  10. createSimpleExpression,
  11. buildProps,
  12. DirectiveNode,
  13. PlainElementNode,
  14. createCompilerError,
  15. ErrorCodes,
  16. CallExpression,
  17. createArrayExpression,
  18. ExpressionNode,
  19. JSChildNode,
  20. ArrayExpression,
  21. createAssignmentExpression,
  22. TextNode,
  23. hasDynamicKeyVBind,
  24. MERGE_PROPS,
  25. isStaticArgOf,
  26. createSequenceExpression,
  27. InterpolationNode,
  28. isStaticExp,
  29. AttributeNode,
  30. buildDirectiveArgs,
  31. TransformContext,
  32. PropsExpression
  33. } from '@vue/compiler-dom'
  34. import {
  35. escapeHtml,
  36. isBooleanAttr,
  37. isBuiltInDirective,
  38. isSSRSafeAttrName,
  39. NO,
  40. propsToAttrMap
  41. } from '@vue/shared'
  42. import { createSSRCompilerError, SSRErrorCodes } from '../errors'
  43. import {
  44. SSR_RENDER_ATTR,
  45. SSR_RENDER_CLASS,
  46. SSR_RENDER_STYLE,
  47. SSR_RENDER_DYNAMIC_ATTR,
  48. SSR_RENDER_ATTRS,
  49. SSR_INTERPOLATE,
  50. SSR_GET_DYNAMIC_MODEL_PROPS,
  51. SSR_INCLUDE_BOOLEAN_ATTR,
  52. SSR_GET_DIRECTIVE_PROPS
  53. } from '../runtimeHelpers'
  54. import { SSRTransformContext, processChildren } from '../ssrCodegenTransform'
  55. // for directives with children overwrite (e.g. v-html & v-text), we need to
  56. // store the raw children so that they can be added in the 2nd pass.
  57. const rawChildrenMap = new WeakMap<
  58. PlainElementNode,
  59. TemplateLiteral['elements'][0]
  60. >()
  61. export const ssrTransformElement: NodeTransform = (node, context) => {
  62. if (
  63. node.type !== NodeTypes.ELEMENT ||
  64. node.tagType !== ElementTypes.ELEMENT
  65. ) {
  66. return
  67. }
  68. return function ssrPostTransformElement() {
  69. // element
  70. // generate the template literal representing the open tag.
  71. const openTag: TemplateLiteral['elements'] = [`<${node.tag}`]
  72. // some tags need to be passed to runtime for special checks
  73. const needTagForRuntime =
  74. node.tag === 'textarea' || node.tag.indexOf('-') > 0
  75. // v-bind="obj", v-bind:[key] and custom directives can potentially
  76. // overwrite other static attrs and can affect final rendering result,
  77. // so when they are present we need to bail out to full `renderAttrs`
  78. const hasDynamicVBind = hasDynamicKeyVBind(node)
  79. const hasCustomDir = node.props.some(
  80. p => p.type === NodeTypes.DIRECTIVE && !isBuiltInDirective(p.name)
  81. )
  82. const needMergeProps = hasDynamicVBind || hasCustomDir
  83. if (needMergeProps) {
  84. const { props, directives } = buildProps(
  85. node,
  86. context,
  87. node.props,
  88. false /* isComponent */,
  89. false /* isDynamicComponent */,
  90. true /* ssr */
  91. )
  92. if (props || directives.length) {
  93. const mergedProps = buildSSRProps(props, directives, context)
  94. const propsExp = createCallExpression(
  95. context.helper(SSR_RENDER_ATTRS),
  96. [mergedProps]
  97. )
  98. if (node.tag === 'textarea') {
  99. const existingText = node.children[0] as
  100. | TextNode
  101. | InterpolationNode
  102. | undefined
  103. // If interpolation, this is dynamic <textarea> content, potentially
  104. // injected by v-model and takes higher priority than v-bind value
  105. if (!existingText || existingText.type !== NodeTypes.INTERPOLATION) {
  106. // <textarea> with dynamic v-bind. We don't know if the final props
  107. // will contain .value, so we will have to do something special:
  108. // assign the merged props to a temp variable, and check whether
  109. // it contains value (if yes, render is as children).
  110. const tempId = `_temp${context.temps++}`
  111. propsExp.arguments = [
  112. createAssignmentExpression(
  113. createSimpleExpression(tempId, false),
  114. mergedProps
  115. )
  116. ]
  117. rawChildrenMap.set(
  118. node,
  119. createCallExpression(context.helper(SSR_INTERPOLATE), [
  120. createConditionalExpression(
  121. createSimpleExpression(`"value" in ${tempId}`, false),
  122. createSimpleExpression(`${tempId}.value`, false),
  123. createSimpleExpression(
  124. existingText ? existingText.content : ``,
  125. true
  126. ),
  127. false
  128. )
  129. ])
  130. )
  131. }
  132. } else if (node.tag === 'input') {
  133. // <input v-bind="obj" v-model>
  134. // we need to determine the props to render for the dynamic v-model
  135. // and merge it with the v-bind expression.
  136. const vModel = findVModel(node)
  137. if (vModel) {
  138. // 1. save the props (san v-model) in a temp variable
  139. const tempId = `_temp${context.temps++}`
  140. const tempExp = createSimpleExpression(tempId, false)
  141. propsExp.arguments = [
  142. createSequenceExpression([
  143. createAssignmentExpression(tempExp, mergedProps),
  144. createCallExpression(context.helper(MERGE_PROPS), [
  145. tempExp,
  146. createCallExpression(
  147. context.helper(SSR_GET_DYNAMIC_MODEL_PROPS),
  148. [
  149. tempExp, // existing props
  150. vModel.exp! // model
  151. ]
  152. )
  153. ])
  154. ])
  155. ]
  156. }
  157. }
  158. if (needTagForRuntime) {
  159. propsExp.arguments.push(`"${node.tag}"`)
  160. }
  161. openTag.push(propsExp)
  162. }
  163. }
  164. // book keeping static/dynamic class merging.
  165. let dynamicClassBinding: CallExpression | undefined = undefined
  166. let staticClassBinding: string | undefined = undefined
  167. // all style bindings are converted to dynamic by transformStyle.
  168. // but we need to make sure to merge them.
  169. let dynamicStyleBinding: CallExpression | undefined = undefined
  170. for (let i = 0; i < node.props.length; i++) {
  171. const prop = node.props[i]
  172. // ignore true-value/false-value on input
  173. if (node.tag === 'input' && isTrueFalseValue(prop)) {
  174. continue
  175. }
  176. // special cases with children override
  177. if (prop.type === NodeTypes.DIRECTIVE) {
  178. if (prop.name === 'html' && prop.exp) {
  179. rawChildrenMap.set(node, prop.exp)
  180. } else if (prop.name === 'text' && prop.exp) {
  181. node.children = [createInterpolation(prop.exp, prop.loc)]
  182. } else if (prop.name === 'slot') {
  183. context.onError(
  184. createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, prop.loc)
  185. )
  186. } else if (isTextareaWithValue(node, prop) && prop.exp) {
  187. if (!needMergeProps) {
  188. node.children = [createInterpolation(prop.exp, prop.loc)]
  189. }
  190. } else if (!needMergeProps && prop.name !== 'on') {
  191. // Directive transforms.
  192. const directiveTransform = context.directiveTransforms[prop.name]
  193. if (directiveTransform) {
  194. const { props, ssrTagParts } = directiveTransform(
  195. prop,
  196. node,
  197. context
  198. )
  199. if (ssrTagParts) {
  200. openTag.push(...ssrTagParts)
  201. }
  202. for (let j = 0; j < props.length; j++) {
  203. const { key, value } = props[j]
  204. if (isStaticExp(key)) {
  205. let attrName = key.content
  206. // static key attr
  207. if (attrName === 'key' || attrName === 'ref') {
  208. continue
  209. }
  210. if (attrName === 'class') {
  211. openTag.push(
  212. ` class="`,
  213. (dynamicClassBinding = createCallExpression(
  214. context.helper(SSR_RENDER_CLASS),
  215. [value]
  216. )),
  217. `"`
  218. )
  219. } else if (attrName === 'style') {
  220. if (dynamicStyleBinding) {
  221. // already has style binding, merge into it.
  222. mergeCall(dynamicStyleBinding, value)
  223. } else {
  224. openTag.push(
  225. ` style="`,
  226. (dynamicStyleBinding = createCallExpression(
  227. context.helper(SSR_RENDER_STYLE),
  228. [value]
  229. )),
  230. `"`
  231. )
  232. }
  233. } else {
  234. attrName =
  235. node.tag.indexOf('-') > 0
  236. ? attrName // preserve raw name on custom elements
  237. : propsToAttrMap[attrName] || attrName.toLowerCase()
  238. if (isBooleanAttr(attrName)) {
  239. openTag.push(
  240. createConditionalExpression(
  241. createCallExpression(
  242. context.helper(SSR_INCLUDE_BOOLEAN_ATTR),
  243. [value]
  244. ),
  245. createSimpleExpression(' ' + attrName, true),
  246. createSimpleExpression('', true),
  247. false /* no newline */
  248. )
  249. )
  250. } else if (isSSRSafeAttrName(attrName)) {
  251. openTag.push(
  252. createCallExpression(context.helper(SSR_RENDER_ATTR), [
  253. key,
  254. value
  255. ])
  256. )
  257. } else {
  258. context.onError(
  259. createSSRCompilerError(
  260. SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME,
  261. key.loc
  262. )
  263. )
  264. }
  265. }
  266. } else {
  267. // dynamic key attr
  268. // this branch is only encountered for custom directive
  269. // transforms that returns properties with dynamic keys
  270. const args: CallExpression['arguments'] = [key, value]
  271. if (needTagForRuntime) {
  272. args.push(`"${node.tag}"`)
  273. }
  274. openTag.push(
  275. createCallExpression(
  276. context.helper(SSR_RENDER_DYNAMIC_ATTR),
  277. args
  278. )
  279. )
  280. }
  281. }
  282. }
  283. }
  284. } else {
  285. // special case: value on <textarea>
  286. if (node.tag === 'textarea' && prop.name === 'value' && prop.value) {
  287. rawChildrenMap.set(node, escapeHtml(prop.value.content))
  288. } else if (!needMergeProps) {
  289. if (prop.name === 'key' || prop.name === 'ref') {
  290. continue
  291. }
  292. // static prop
  293. if (prop.name === 'class' && prop.value) {
  294. staticClassBinding = JSON.stringify(prop.value.content)
  295. }
  296. openTag.push(
  297. ` ${prop.name}` +
  298. (prop.value ? `="${escapeHtml(prop.value.content)}"` : ``)
  299. )
  300. }
  301. }
  302. }
  303. // handle co-existence of dynamic + static class bindings
  304. if (dynamicClassBinding && staticClassBinding) {
  305. mergeCall(dynamicClassBinding, staticClassBinding)
  306. removeStaticBinding(openTag, 'class')
  307. }
  308. if (context.scopeId) {
  309. openTag.push(` ${context.scopeId}`)
  310. }
  311. node.ssrCodegenNode = createTemplateLiteral(openTag)
  312. }
  313. }
  314. export function buildSSRProps(
  315. props: PropsExpression | undefined,
  316. directives: DirectiveNode[],
  317. context: TransformContext
  318. ): JSChildNode {
  319. let mergePropsArgs: JSChildNode[] = []
  320. if (props) {
  321. if (props.type === NodeTypes.JS_CALL_EXPRESSION) {
  322. // already a mergeProps call
  323. mergePropsArgs = props.arguments as JSChildNode[]
  324. } else {
  325. mergePropsArgs.push(props)
  326. }
  327. }
  328. if (directives.length) {
  329. for (const dir of directives) {
  330. mergePropsArgs.push(
  331. createCallExpression(context.helper(SSR_GET_DIRECTIVE_PROPS), [
  332. `_ctx`,
  333. ...buildDirectiveArgs(dir, context).elements
  334. ] as JSChildNode[])
  335. )
  336. }
  337. }
  338. return mergePropsArgs.length > 1
  339. ? createCallExpression(context.helper(MERGE_PROPS), mergePropsArgs)
  340. : mergePropsArgs[0]
  341. }
  342. function isTrueFalseValue(prop: DirectiveNode | AttributeNode) {
  343. if (prop.type === NodeTypes.DIRECTIVE) {
  344. return (
  345. prop.name === 'bind' &&
  346. prop.arg &&
  347. isStaticExp(prop.arg) &&
  348. (prop.arg.content === 'true-value' || prop.arg.content === 'false-value')
  349. )
  350. } else {
  351. return prop.name === 'true-value' || prop.name === 'false-value'
  352. }
  353. }
  354. function isTextareaWithValue(
  355. node: PlainElementNode,
  356. prop: DirectiveNode
  357. ): boolean {
  358. return !!(
  359. node.tag === 'textarea' &&
  360. prop.name === 'bind' &&
  361. isStaticArgOf(prop.arg, 'value')
  362. )
  363. }
  364. function mergeCall(call: CallExpression, arg: string | JSChildNode) {
  365. const existing = call.arguments[0] as ExpressionNode | ArrayExpression
  366. if (existing.type === NodeTypes.JS_ARRAY_EXPRESSION) {
  367. existing.elements.push(arg)
  368. } else {
  369. call.arguments[0] = createArrayExpression([existing, arg])
  370. }
  371. }
  372. function removeStaticBinding(
  373. tag: TemplateLiteral['elements'],
  374. binding: string
  375. ) {
  376. const regExp = new RegExp(`^ ${binding}=".+"$`)
  377. const i = tag.findIndex(e => typeof e === 'string' && regExp.test(e))
  378. if (i > -1) {
  379. tag.splice(i, 1)
  380. }
  381. }
  382. function findVModel(node: PlainElementNode): DirectiveNode | undefined {
  383. return node.props.find(
  384. p => p.type === NodeTypes.DIRECTIVE && p.name === 'model' && p.exp
  385. ) as DirectiveNode | undefined
  386. }
  387. export function ssrProcessElement(
  388. node: PlainElementNode,
  389. context: SSRTransformContext
  390. ) {
  391. const isVoidTag = context.options.isVoidTag || NO
  392. const elementsToAdd = node.ssrCodegenNode!.elements
  393. for (let j = 0; j < elementsToAdd.length; j++) {
  394. context.pushStringPart(elementsToAdd[j])
  395. }
  396. // Handle slot scopeId
  397. if (context.withSlotScopeId) {
  398. context.pushStringPart(createSimpleExpression(`_scopeId`, false))
  399. }
  400. // close open tag
  401. context.pushStringPart(`>`)
  402. const rawChildren = rawChildrenMap.get(node)
  403. if (rawChildren) {
  404. context.pushStringPart(rawChildren)
  405. } else if (node.children.length) {
  406. processChildren(node, context)
  407. }
  408. if (!isVoidTag(node.tag)) {
  409. // push closing tag
  410. context.pushStringPart(`</${node.tag}>`)
  411. }
  412. }