fix(ssr): handle fallthrough attrs in ssr compile output

This commit is contained in:
Evan You
2020-06-26 14:23:50 -04:00
parent 30584bcc61
commit d5dbd27193
21 changed files with 776 additions and 488 deletions

View File

@@ -23,6 +23,7 @@ import { ssrTransformIf } from './transforms/ssrVIf'
import { ssrTransformFor } from './transforms/ssrVFor'
import { ssrTransformModel } from './transforms/ssrVModel'
import { ssrTransformShow } from './transforms/ssrVShow'
import { ssrInjectFallthroughAttrs } from './transforms/ssrInjectFallthroughAttrs'
export function compile(
template: string,
@@ -55,6 +56,7 @@ export function compile(
trackVForSlotScopes,
transformExpression,
ssrTransformSlotOutlet,
ssrInjectFallthroughAttrs,
ssrTransformElement,
ssrTransformComponent,
trackSlotScopes,

View File

@@ -0,0 +1,52 @@
import {
NodeTransform,
NodeTypes,
ElementTypes,
locStub,
createSimpleExpression,
RootNode,
TemplateChildNode,
ParentNode,
findDir
} from '@vue/compiler-dom'
const hasSingleChild = (node: ParentNode): boolean =>
node.children.filter(n => n.type !== NodeTypes.COMMENT).length === 1
export const ssrInjectFallthroughAttrs: NodeTransform = (node, context) => {
// _attrs is provided as a function argument.
// mark it as a known identifier so that it doesn't get prefixed by
// transformExpression.
if (node.type === NodeTypes.ROOT) {
context.identifiers._attrs = 1
}
const parent = context.parent
if (!parent || parent.type !== NodeTypes.ROOT) {
return
}
if (node.type === NodeTypes.IF_BRANCH && hasSingleChild(node)) {
injectFallthroughAttrs(node.children[0])
} else if (hasSingleChild(parent)) {
injectFallthroughAttrs(node)
}
}
function injectFallthroughAttrs(node: RootNode | TemplateChildNode) {
if (
node.type === NodeTypes.ELEMENT &&
(node.tagType === ElementTypes.ELEMENT ||
node.tagType === ElementTypes.COMPONENT) &&
!findDir(node, 'for')
) {
node.props.push({
type: NodeTypes.DIRECTIVE,
name: 'bind',
arg: undefined,
exp: createSimpleExpression(`_attrs`, false),
modifiers: [],
loc: locStub
})
}
}

View File

@@ -23,7 +23,8 @@ import {
hasDynamicKeyVBind,
MERGE_PROPS,
isBindKey,
createSequenceExpression
createSequenceExpression,
InterpolationNode
} from '@vue/compiler-dom'
import {
escapeHtml,
@@ -53,30 +54,40 @@ const rawChildrenMap = new WeakMap<
export const ssrTransformElement: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT
node.type !== NodeTypes.ELEMENT ||
node.tagType !== ElementTypes.ELEMENT
) {
return function ssrPostTransformElement() {
// element
// generate the template literal representing the open tag.
const openTag: TemplateLiteral['elements'] = [`<${node.tag}`]
// some tags need to be pasesd to runtime for special checks
const needTagForRuntime =
node.tag === 'textarea' || node.tag.indexOf('-') > 0
return
}
// 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]
)
return function ssrPostTransformElement() {
// element
// generate the template literal representing the open tag.
const openTag: TemplateLiteral['elements'] = [`<${node.tag}`]
// some tags need to be pasesd to runtime for special checks
const needTagForRuntime =
node.tag === 'textarea' || node.tag.indexOf('-') > 0
if (node.tag === 'textarea') {
// 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
@@ -88,7 +99,6 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
props
)
]
const existingText = node.children[0] as TextNode | undefined
rawChildrenMap.set(
node,
createCallExpression(context.helper(SSR_INTERPOLATE), [
@@ -103,189 +113,189 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
)
])
)
} 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
]
)
])
}
} 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)
}
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
// 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) {
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 if (prop.name === 'slot') {
}
} else {
// Directive transforms.
const directiveTransform = context.directiveTransforms[prop.name]
if (!directiveTransform) {
// no corresponding ssr directive transform found.
context.onError(
createCompilerError(ErrorCodes.X_V_SLOT_MISPLACED, prop.loc)
createSSRCompilerError(
SSRErrorCodes.X_SSR_CUSTOM_DIRECTIVE_NO_TRANSFORM,
prop.loc
)
)
} else if (isTextareaWithValue(node, prop) && prop.exp) {
if (!hasDynamicVBind) {
node.children = [createInterpolation(prop.exp, prop.loc)]
} else if (!hasDynamicVBind) {
const { props, ssrTagParts } = directiveTransform(
prop,
node,
context
)
if (ssrTagParts) {
openTag.push(...ssrTagParts)
}
} 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 (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
let attrName = key.content
// static key attr
if (attrName === 'class') {
for (let j = 0; j < props.length; j++) {
const { key, value } = props[j]
if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
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(
` class="`,
(dynamicClassBinding = createCallExpression(
context.helper(SSR_RENDER_CLASS),
` style="`,
(dynamicStyleBinding = createCallExpression(
context.helper(SSR_RENDER_STYLE),
[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
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)}"` : ``)
)
}
} 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)
}
// 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)
}
}