fix(ssr): handle fallthrough attrs in ssr compile output
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user