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(``) - } + 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