372 lines
12 KiB
TypeScript
372 lines
12 KiB
TypeScript
import {
|
|
NodeTransform,
|
|
NodeTypes,
|
|
ElementTypes,
|
|
TemplateLiteral,
|
|
createTemplateLiteral,
|
|
createInterpolation,
|
|
createCallExpression,
|
|
createConditionalExpression,
|
|
createSimpleExpression,
|
|
buildProps,
|
|
DirectiveNode,
|
|
PlainElementNode,
|
|
createCompilerError,
|
|
ErrorCodes,
|
|
CallExpression,
|
|
createArrayExpression,
|
|
ExpressionNode,
|
|
JSChildNode,
|
|
ArrayExpression,
|
|
createAssignmentExpression,
|
|
TextNode,
|
|
hasDynamicKeyVBind,
|
|
MERGE_PROPS,
|
|
isBindKey,
|
|
createSequenceExpression,
|
|
InterpolationNode,
|
|
isStaticExp
|
|
} from '@vue/compiler-dom'
|
|
import {
|
|
escapeHtml,
|
|
isBooleanAttr,
|
|
isSSRSafeAttrName,
|
|
NO,
|
|
propsToAttrMap
|
|
} from '@vue/shared'
|
|
import { createSSRCompilerError, SSRErrorCodes } from '../errors'
|
|
import {
|
|
SSR_RENDER_ATTR,
|
|
SSR_RENDER_CLASS,
|
|
SSR_RENDER_STYLE,
|
|
SSR_RENDER_DYNAMIC_ATTR,
|
|
SSR_RENDER_ATTRS,
|
|
SSR_INTERPOLATE,
|
|
SSR_GET_DYNAMIC_MODEL_PROPS
|
|
} from '../runtimeHelpers'
|
|
import { SSRTransformContext, processChildren } from '../ssrCodegenTransform'
|
|
|
|
// for directives with children overwrite (e.g. v-html & v-text), we need to
|
|
// store the raw children so that they can be added in the 2nd pass.
|
|
const rawChildrenMap = new WeakMap<
|
|
PlainElementNode,
|
|
TemplateLiteral['elements'][0]
|
|
>()
|
|
|
|
export const ssrTransformElement: NodeTransform = (node, context) => {
|
|
if (
|
|
node.type !== NodeTypes.ELEMENT ||
|
|
node.tagType !== ElementTypes.ELEMENT
|
|
) {
|
|
return
|
|
}
|
|
|
|
return function ssrPostTransformElement() {
|
|
// element
|
|
// generate the template literal representing the open tag.
|
|
const openTag: TemplateLiteral['elements'] = [`<${node.tag}`]
|
|
// some tags need to be passed to runtime for special checks
|
|
const needTagForRuntime =
|
|
node.tag === 'textarea' || node.tag.indexOf('-') > 0
|
|
|
|
// v-bind="obj" or v-bind:[key] can potentially overwrite other static
|
|
// attrs and can affect final rendering result, so when they are present
|
|
// we need to bail out to full `renderAttrs`
|
|
const hasDynamicVBind = hasDynamicKeyVBind(node)
|
|
if (hasDynamicVBind) {
|
|
const { props } = buildProps(node, context, node.props, true /* ssr */)
|
|
if (props) {
|
|
const propsExp = createCallExpression(
|
|
context.helper(SSR_RENDER_ATTRS),
|
|
[props]
|
|
)
|
|
|
|
if (node.tag === 'textarea') {
|
|
const existingText = node.children[0] as
|
|
| TextNode
|
|
| InterpolationNode
|
|
| undefined
|
|
// If interpolation, this is dynamic <textarea> content, potentially
|
|
// injected by v-model and takes higher priority than v-bind value
|
|
if (!existingText || existingText.type !== NodeTypes.INTERPOLATION) {
|
|
// <textarea> with dynamic v-bind. We don't know if the final props
|
|
// will contain .value, so we will have to do something special:
|
|
// assign the merged props to a temp variable, and check whether
|
|
// it contains value (if yes, render is as children).
|
|
const tempId = `_temp${context.temps++}`
|
|
propsExp.arguments = [
|
|
createAssignmentExpression(
|
|
createSimpleExpression(tempId, false),
|
|
props
|
|
)
|
|
]
|
|
rawChildrenMap.set(
|
|
node,
|
|
createCallExpression(context.helper(SSR_INTERPOLATE), [
|
|
createConditionalExpression(
|
|
createSimpleExpression(`"value" in ${tempId}`, false),
|
|
createSimpleExpression(`${tempId}.value`, false),
|
|
createSimpleExpression(
|
|
existingText ? existingText.content : ``,
|
|
true
|
|
),
|
|
false
|
|
)
|
|
])
|
|
)
|
|
}
|
|
} else if (node.tag === 'input') {
|
|
// <input v-bind="obj" v-model>
|
|
// we need to determine the props to render for the dynamic v-model
|
|
// and merge it with the v-bind expression.
|
|
const vModel = findVModel(node)
|
|
if (vModel) {
|
|
// 1. save the props (san v-model) in a temp variable
|
|
const tempId = `_temp${context.temps++}`
|
|
const tempExp = createSimpleExpression(tempId, false)
|
|
propsExp.arguments = [
|
|
createSequenceExpression([
|
|
createAssignmentExpression(tempExp, props),
|
|
createCallExpression(context.helper(MERGE_PROPS), [
|
|
tempExp,
|
|
createCallExpression(
|
|
context.helper(SSR_GET_DYNAMIC_MODEL_PROPS),
|
|
[
|
|
tempExp, // existing props
|
|
vModel.exp! // model
|
|
]
|
|
)
|
|
])
|
|
])
|
|
]
|
|
}
|
|
}
|
|
|
|
if (needTagForRuntime) {
|
|
propsExp.arguments.push(`"${node.tag}"`)
|
|
}
|
|
|
|
openTag.push(propsExp)
|
|
}
|
|
}
|
|
|
|
// book keeping static/dynamic class merging.
|
|
let dynamicClassBinding: CallExpression | undefined = undefined
|
|
let staticClassBinding: string | undefined = undefined
|
|
// all style bindings are converted to dynamic by transformStyle.
|
|
// but we need to make sure to merge them.
|
|
let dynamicStyleBinding: CallExpression | undefined = undefined
|
|
|
|
for (let i = 0; i < node.props.length; i++) {
|
|
const prop = node.props[i]
|
|
// special cases with children override
|
|
if (prop.type === NodeTypes.DIRECTIVE) {
|
|
if (prop.name === 'html' && prop.exp) {
|
|
rawChildrenMap.set(node, prop.exp)
|
|
} else if (prop.name === 'text' && prop.exp) {
|
|
node.children = [createInterpolation(prop.exp, prop.loc)]
|
|
} else if (prop.name === 'slot') {
|
|
context.onError(
|
|
createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, prop.loc)
|
|
)
|
|
} else if (isTextareaWithValue(node, prop) && prop.exp) {
|
|
if (!hasDynamicVBind) {
|
|
node.children = [createInterpolation(prop.exp, prop.loc)]
|
|
}
|
|
} else {
|
|
// Directive transforms.
|
|
const directiveTransform = context.directiveTransforms[prop.name]
|
|
if (!directiveTransform) {
|
|
// no corresponding ssr directive transform found.
|
|
context.onError(
|
|
createSSRCompilerError(
|
|
SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM,
|
|
prop.loc
|
|
)
|
|
)
|
|
} else if (!hasDynamicVBind) {
|
|
const { props, ssrTagParts } = directiveTransform(
|
|
prop,
|
|
node,
|
|
context
|
|
)
|
|
if (ssrTagParts) {
|
|
openTag.push(...ssrTagParts)
|
|
}
|
|
for (let j = 0; j < props.length; j++) {
|
|
const { key, value } = props[j]
|
|
if (isStaticExp(key)) {
|
|
let attrName = key.content
|
|
// static key attr
|
|
if (attrName === 'class') {
|
|
openTag.push(
|
|
` class="`,
|
|
(dynamicClassBinding = createCallExpression(
|
|
context.helper(SSR_RENDER_CLASS),
|
|
[value]
|
|
)),
|
|
`"`
|
|
)
|
|
} else if (attrName === 'style') {
|
|
if (dynamicStyleBinding) {
|
|
// already has style binding, merge into it.
|
|
mergeCall(dynamicStyleBinding, value)
|
|
} else {
|
|
openTag.push(
|
|
` style="`,
|
|
(dynamicStyleBinding = createCallExpression(
|
|
context.helper(SSR_RENDER_STYLE),
|
|
[value]
|
|
)),
|
|
`"`
|
|
)
|
|
}
|
|
} else {
|
|
attrName =
|
|
node.tag.indexOf('-') > 0
|
|
? attrName // preserve raw name on custom elements
|
|
: propsToAttrMap[attrName] || attrName.toLowerCase()
|
|
if (isBooleanAttr(attrName)) {
|
|
openTag.push(
|
|
createConditionalExpression(
|
|
value,
|
|
createSimpleExpression(' ' + attrName, true),
|
|
createSimpleExpression('', true),
|
|
false /* no newline */
|
|
)
|
|
)
|
|
} else if (isSSRSafeAttrName(attrName)) {
|
|
openTag.push(
|
|
createCallExpression(context.helper(SSR_RENDER_ATTR), [
|
|
key,
|
|
value
|
|
])
|
|
)
|
|
} else {
|
|
context.onError(
|
|
createSSRCompilerError(
|
|
SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME,
|
|
key.loc
|
|
)
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
// dynamic key attr
|
|
// this branch is only encountered for custom directive
|
|
// transforms that returns properties with dynamic keys
|
|
const args: CallExpression['arguments'] = [key, value]
|
|
if (needTagForRuntime) {
|
|
args.push(`"${node.tag}"`)
|
|
}
|
|
openTag.push(
|
|
createCallExpression(
|
|
context.helper(SSR_RENDER_DYNAMIC_ATTR),
|
|
args
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// special case: value on <textarea>
|
|
if (node.tag === 'textarea' && prop.name === 'value' && prop.value) {
|
|
rawChildrenMap.set(node, escapeHtml(prop.value.content))
|
|
} else if (!hasDynamicVBind) {
|
|
// static prop
|
|
if (prop.name === 'class' && prop.value) {
|
|
staticClassBinding = JSON.stringify(prop.value.content)
|
|
}
|
|
openTag.push(
|
|
` ${prop.name}` +
|
|
(prop.value ? `="${escapeHtml(prop.value.content)}"` : ``)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handle co-existence of dynamic + static class bindings
|
|
if (dynamicClassBinding && staticClassBinding) {
|
|
mergeCall(dynamicClassBinding, staticClassBinding)
|
|
removeStaticBinding(openTag, 'class')
|
|
}
|
|
|
|
if (context.scopeId) {
|
|
openTag.push(` ${context.scopeId}`)
|
|
}
|
|
|
|
node.ssrCodegenNode = createTemplateLiteral(openTag)
|
|
}
|
|
}
|
|
|
|
function isTextareaWithValue(
|
|
node: PlainElementNode,
|
|
prop: DirectiveNode
|
|
): boolean {
|
|
return !!(
|
|
node.tag === 'textarea' &&
|
|
prop.name === 'bind' &&
|
|
isBindKey(prop.arg, 'value')
|
|
)
|
|
}
|
|
|
|
function mergeCall(call: CallExpression, arg: string | JSChildNode) {
|
|
const existing = call.arguments[0] as ExpressionNode | ArrayExpression
|
|
if (existing.type === NodeTypes.JS_ARRAY_EXPRESSION) {
|
|
existing.elements.push(arg)
|
|
} else {
|
|
call.arguments[0] = createArrayExpression([existing, arg])
|
|
}
|
|
}
|
|
|
|
function removeStaticBinding(
|
|
tag: TemplateLiteral['elements'],
|
|
binding: string
|
|
) {
|
|
const regExp = new RegExp(`^ ${binding}=".+"$`)
|
|
|
|
const i = tag.findIndex(e => typeof e === 'string' && regExp.test(e))
|
|
|
|
if (i > -1) {
|
|
tag.splice(i, 1)
|
|
}
|
|
}
|
|
|
|
function findVModel(node: PlainElementNode): DirectiveNode | undefined {
|
|
return node.props.find(
|
|
p => p.type === NodeTypes.DIRECTIVE && p.name === 'model' && p.exp
|
|
) as DirectiveNode | undefined
|
|
}
|
|
|
|
export function ssrProcessElement(
|
|
node: PlainElementNode,
|
|
context: SSRTransformContext
|
|
) {
|
|
const isVoidTag = context.options.isVoidTag || NO
|
|
const elementsToAdd = node.ssrCodegenNode!.elements
|
|
for (let j = 0; j < elementsToAdd.length; j++) {
|
|
context.pushStringPart(elementsToAdd[j])
|
|
}
|
|
|
|
// Handle slot scopeId
|
|
if (context.withSlotScopeId) {
|
|
context.pushStringPart(createSimpleExpression(`_scopeId`, false))
|
|
}
|
|
|
|
// close open tag
|
|
context.pushStringPart(`>`)
|
|
|
|
const rawChildren = rawChildrenMap.get(node)
|
|
if (rawChildren) {
|
|
context.pushStringPart(rawChildren)
|
|
} else if (node.children.length) {
|
|
processChildren(node.children, context)
|
|
}
|
|
|
|
if (!isVoidTag(node.tag)) {
|
|
// push closing tag
|
|
context.pushStringPart(`</${node.tag}>`)
|
|
}
|
|
}
|