diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
index 3fd0629f3..b957fd7b6 100644
--- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
+++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts
@@ -56,8 +56,12 @@ describe('ssr: components', () => {
const _component_foo = resolveComponent(\\"foo\\")
_ssrRenderComponent(_component_foo, null, {
- default: (_, _push, _parent) => {
- _push(\`hello
\`)
+ default: (_, _push, _parent, _scopeId) => {
+ if (_scopeId) {
+ _push(\`hello\`)
+ } else {
+ _push(\`hello\`)
+ }
},
_compiled: true
}, _parent)
@@ -75,7 +79,7 @@ describe('ssr: components', () => {
const _component_foo = resolveComponent(\\"foo\\")
_ssrRenderComponent(_component_foo, null, {
- default: ({ msg }, _push, _parent) => {
+ default: ({ msg }, _push, _parent, _scopeId) => {
_push(\`\${_ssrInterpolate(msg + _ctx.outer)}\`)
},
_compiled: true
@@ -98,10 +102,10 @@ describe('ssr: components', () => {
const _component_foo = resolveComponent(\\"foo\\")
_ssrRenderComponent(_component_foo, null, {
- default: (_, _push, _parent) => {
+ default: (_, _push, _parent, _scopeId) => {
_push(\`foo\`)
},
- named: (_, _push, _parent) => {
+ named: (_, _push, _parent, _scopeId) => {
_push(\`bar\`)
},
_compiled: true
@@ -126,7 +130,7 @@ describe('ssr: components', () => {
(_ctx.ok)
? {
name: \\"named\\",
- fn: (_, _push, _parent) => {
+ fn: (_, _push, _parent, _scopeId) => {
_push(\`foo\`)
}
}
@@ -152,7 +156,7 @@ describe('ssr: components', () => {
renderList(_ctx.names, (key) => {
return {
name: key,
- fn: ({ msg }, _push, _parent) => {
+ fn: ({ msg }, _push, _parent, _scopeId) => {
_push(\`\${_ssrInterpolate(msg + key + _ctx.bar)}\`)
}
}
diff --git a/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts
new file mode 100644
index 000000000..eb1aede49
--- /dev/null
+++ b/packages/compiler-ssr/__tests__/ssrScopeId.spec.ts
@@ -0,0 +1,114 @@
+import { compile } from '../src'
+
+const scopeId = 'data-v-xxxxxxx'
+
+describe('ssr: scopeId', () => {
+ test('basic', () => {
+ expect(
+ compile(`hello
`, {
+ scopeId
+ }).code
+ ).toMatchInlineSnapshot(`
+ "
+ return function ssrRender(_ctx, _push, _parent) {
+ _push(\`hello
\`)
+ }"
+ `)
+ })
+
+ test('inside slots (only text)', () => {
+ // should have no branching inside slot
+ expect(
+ compile(`foo`, {
+ scopeId
+ }).code
+ ).toMatchInlineSnapshot(`
+ "const { resolveComponent } = require(\\"vue\\")
+ const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent) {
+ const _component_foo = resolveComponent(\\"foo\\")
+
+ _ssrRenderComponent(_component_foo, null, {
+ default: (_, _push, _parent, _scopeId) => {
+ _push(\`foo\`)
+ },
+ _compiled: true
+ }, _parent)
+ }"
+ `)
+ })
+
+ test('inside slots (with elements)', () => {
+ expect(
+ compile(`hello`, {
+ scopeId
+ }).code
+ ).toMatchInlineSnapshot(`
+ "const { resolveComponent } = require(\\"vue\\")
+ const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent) {
+ const _component_foo = resolveComponent(\\"foo\\")
+
+ _ssrRenderComponent(_component_foo, null, {
+ default: (_, _push, _parent, _scopeId) => {
+ if (_scopeId) {
+ _push(\`hello\`)
+ } else {
+ _push(\`hello\`)
+ }
+ },
+ _compiled: true
+ }, _parent)
+ }"
+ `)
+ })
+
+ test('nested slots', () => {
+ expect(
+ compile(`hello`, {
+ scopeId
+ }).code
+ ).toMatchInlineSnapshot(`
+ "const { resolveComponent } = require(\\"vue\\")
+ const { _ssrRenderComponent } = require(\\"@vue/server-renderer\\")
+
+ return function ssrRender(_ctx, _push, _parent) {
+ const _component_bar = resolveComponent(\\"bar\\")
+ const _component_foo = resolveComponent(\\"foo\\")
+
+ _ssrRenderComponent(_component_foo, null, {
+ default: (_, _push, _parent, _scopeId) => {
+ if (_scopeId) {
+ _push(\`hello\`)
+ _ssrRenderComponent(_component_bar, null, {
+ default: (_, _push, _parent, _scopeId) => {
+ if (_scopeId) {
+ _push(\`\`)
+ } else {
+ _push(\`\`)
+ }
+ },
+ _compiled: true
+ }, _parent)
+ } else {
+ _push(\`hello\`)
+ _ssrRenderComponent(_component_bar, null, {
+ default: (_, _push, _parent, _scopeId) => {
+ if (_scopeId) {
+ _push(\`\`)
+ } else {
+ _push(\`\`)
+ }
+ },
+ _compiled: true
+ }, _parent)
+ }
+ },
+ _compiled: true
+ }, _parent)
+ }"
+ `)
+ })
+})
diff --git a/packages/compiler-ssr/src/ssrCodegenTransform.ts b/packages/compiler-ssr/src/ssrCodegenTransform.ts
index d9efb3b01..c5012e869 100644
--- a/packages/compiler-ssr/src/ssrCodegenTransform.ts
+++ b/packages/compiler-ssr/src/ssrCodegenTransform.ts
@@ -13,12 +13,13 @@ import {
IfStatement,
CallExpression
} from '@vue/compiler-dom'
-import { isString, escapeHtml, NO } from '@vue/shared'
+import { isString, escapeHtml } from '@vue/shared'
import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers'
import { ssrProcessIf } from './transforms/ssrVIf'
import { ssrProcessFor } from './transforms/ssrVFor'
import { ssrProcessSlotOutlet } from './transforms/ssrTransformSlotOutlet'
import { ssrProcessComponent } from './transforms/ssrTransformComponent'
+import { ssrProcessElement } from './transforms/ssrTransformElement'
// Because SSR codegen output is completely different from client-side output
// (e.g. multiple elements can be concatenated into a single template literal
@@ -29,7 +30,7 @@ import { ssrProcessComponent } from './transforms/ssrTransformComponent'
export function ssrCodegenTransform(ast: RootNode, options: CompilerOptions) {
const context = createSSRTransformContext(options)
const isFragment =
- ast.children.length > 1 && !ast.children.every(c => isText(c))
+ ast.children.length > 1 && ast.children.some(c => !isText(c))
processChildren(ast.children, context, isFragment)
ast.codegenNode = createBlockStatement(context.body)
@@ -46,7 +47,8 @@ export type SSRTransformContext = ReturnType
function createSSRTransformContext(
options: CompilerOptions,
- helpers: Set = new Set()
+ helpers: Set = new Set(),
+ withSlotScopeId = false
) {
const body: BlockStatement['body'] = []
let currentString: TemplateLiteral | null = null
@@ -55,6 +57,7 @@ function createSSRTransformContext(
options,
body,
helpers,
+ withSlotScopeId,
helper(name: T): T {
helpers.add(name)
return name
@@ -82,11 +85,16 @@ function createSSRTransformContext(
}
}
-export function createChildContext(
- parent: SSRTransformContext
+function createChildContext(
+ parent: SSRTransformContext,
+ withSlotScopeId = parent.withSlotScopeId
): SSRTransformContext {
// ensure child inherits parent helpers
- return createSSRTransformContext(parent.options, parent.helpers)
+ return createSSRTransformContext(
+ parent.options,
+ parent.helpers,
+ withSlotScopeId
+ )
}
export function processChildren(
@@ -97,23 +105,11 @@ export function processChildren(
if (asFragment) {
context.pushStringPart(``)
}
- const isVoidTag = context.options.isVoidTag || NO
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child.type === NodeTypes.ELEMENT) {
if (child.tagType === ElementTypes.ELEMENT) {
- const elementsToAdd = child.ssrCodegenNode!.elements
- for (let j = 0; j < elementsToAdd.length; j++) {
- context.pushStringPart(elementsToAdd[j])
- }
- if (child.children.length) {
- processChildren(child.children, context)
- }
-
- if (!isVoidTag(child.tag)) {
- // push closing tag
- context.pushStringPart(`${child.tag}>`)
- }
+ ssrProcessElement(child, context)
} else if (child.tagType === ElementTypes.COMPONENT) {
ssrProcessComponent(child, context)
} else if (child.tagType === ElementTypes.SLOT) {
@@ -135,3 +131,14 @@ export function processChildren(
context.pushStringPart(``)
}
}
+
+export function processChildrenAsStatement(
+ children: TemplateChildNode[],
+ parentContext: SSRTransformContext,
+ asFragment = false,
+ withSlotScopeId = parentContext.withSlotScopeId
+): BlockStatement {
+ const childContext = createChildContext(parentContext, withSlotScopeId)
+ processChildren(children, childContext, asFragment)
+ return createBlockStatement(childContext.body)
+}
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
index de055f3da..7cc0d49ba 100644
--- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
+++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts
@@ -14,13 +14,16 @@ import {
TemplateChildNode,
PORTAL,
SUSPENSE,
- TRANSITION_GROUP
+ TRANSITION_GROUP,
+ createIfStatement,
+ createSimpleExpression,
+ isText
} from '@vue/compiler-dom'
import { SSR_RENDER_COMPONENT } from '../runtimeHelpers'
import {
SSRTransformContext,
- createChildContext,
- processChildren
+ processChildren,
+ processChildrenAsStatement
} from '../ssrCodegenTransform'
import { isSymbol } from '@vue/shared'
@@ -62,10 +65,10 @@ export const ssrTransformComponent: NodeTransform = (node, context) => {
const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => {
// An SSR slot function has the signature of
- // (props, _push, _parent) => void
+ // (props, _push, _parent, _scopeId) => void
// See server-renderer/src/helpers/renderSlot.ts
const fn = createFunctionExpression(
- [props || `_`, `_push`, `_parent`],
+ [props || `_`, `_push`, `_parent`, `_scopeId`],
undefined, // no return, assign body later
true, // newline
false, // isSlot: pass false since we don't need client scopeId codegen
@@ -111,9 +114,24 @@ export function ssrProcessComponent(
const wipEntries = wipMap.get(node) || []
for (let i = 0; i < wipEntries.length; i++) {
const { fn, children } = wipEntries[i]
- const childContext = createChildContext(context)
- processChildren(children, childContext)
- fn.body = createBlockStatement(childContext.body)
+ const hasNonTextChild = children.some(c => !isText(c))
+ if (hasNonTextChild) {
+ // SSR slots need to handled potential presence of scopeId of the child
+ // component. To avoid the cost of concatenation when it's unnecessary,
+ // we split the code into two paths, one with slot scopeId and one without.
+ fn.body = createBlockStatement([
+ createIfStatement(
+ createSimpleExpression(`_scopeId`, false),
+ // branch with scopeId concatenation
+ processChildrenAsStatement(children, context, false, true),
+ // branch without scopeId concatenation
+ processChildrenAsStatement(children, context, false, false)
+ )
+ ])
+ } else {
+ // only text, no need for scopeId branching.
+ fn.body = processChildrenAsStatement(children, context)
+ }
}
context.pushStatement(node.ssrCodegenNode)
}
diff --git a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts
index cb35c3316..000266b09 100644
--- a/packages/compiler-ssr/src/transforms/ssrTransformElement.ts
+++ b/packages/compiler-ssr/src/transforms/ssrTransformElement.ts
@@ -24,7 +24,7 @@ import {
MERGE_PROPS,
isBindKey
} from '@vue/compiler-dom'
-import { escapeHtml, isBooleanAttr, isSSRSafeAttrName } from '@vue/shared'
+import { escapeHtml, isBooleanAttr, isSSRSafeAttrName, NO } from '@vue/shared'
import { createSSRCompilerError, SSRErrorCodes } from '../errors'
import {
SSR_RENDER_ATTR,
@@ -35,6 +35,14 @@ import {
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 (
@@ -45,7 +53,6 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
// element
// generate the template literal representing the open tag.
const openTag: TemplateLiteral['elements'] = [`<${node.tag}`]
- let rawChildren
// v-bind="obj" or v-bind:[key] can potentially overwrite other static
// attrs and can affect final rendering result, so when they are present
@@ -70,10 +77,9 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
props
)
const existingText = node.children[0] as TextNode | undefined
- node.children = []
- rawChildren = createCallExpression(
- context.helper(SSR_INTERPOLATE),
- [
+ rawChildrenMap.set(
+ node,
+ createCallExpression(context.helper(SSR_INTERPOLATE), [
createConditionalExpression(
createSimpleExpression(`"value" in ${tempId}`, false),
createSimpleExpression(`${tempId}.value`, false),
@@ -83,7 +89,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
),
false
)
- ]
+ ])
)
} else if (node.tag === 'input') {
//
@@ -126,8 +132,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
// special cases with children override
if (prop.type === NodeTypes.DIRECTIVE) {
if (prop.name === 'html' && prop.exp) {
- node.children = []
- rawChildren = 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') {
@@ -225,8 +230,7 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
} else {
// special case: value on