diff --git a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts index b47c4f945..3edbb2e24 100644 --- a/packages/compiler-ssr/__tests__/ssrComponent.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrComponent.spec.ts @@ -45,4 +45,121 @@ describe('ssr: components', () => { }" `) }) + + describe('slots', () => { + test('implicit default slot', () => { + expect(compile(`hello
`).code).toMatchInlineSnapshot(` + "const { resolveComponent } = require(\\"vue\\") + const { _renderComponent } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + const _component_foo = resolveComponent(\\"foo\\") + + _renderComponent(_component_foo, null, { + default: (_, _push, _parent) => { + _push(\`hello
\`) + }, + _compiled: true + }, _parent) + }" + `) + }) + + test('explicit default slot', () => { + expect(compile(`{{ msg + outer }}`).code) + .toMatchInlineSnapshot(` + "const { resolveComponent } = require(\\"vue\\") + const { _renderComponent, _interpolate } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + const _component_foo = resolveComponent(\\"foo\\") + + _renderComponent(_component_foo, null, { + default: ({ msg }, _push, _parent) => { + _push(\`\${_interpolate(msg + _ctx.outer)}\`) + }, + _compiled: true + }, _parent) + }" + `) + }) + + test('named slots', () => { + expect( + compile(` + + + `).code + ).toMatchInlineSnapshot(` + "const { resolveComponent } = require(\\"vue\\") + const { _renderComponent } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + const _component_foo = resolveComponent(\\"foo\\") + + _renderComponent(_component_foo, null, { + default: (_, _push, _parent) => { + _push(\`foo\`) + }, + named: (_, _push, _parent) => { + _push(\`bar\`) + }, + _compiled: true + }, _parent) + }" + `) + }) + + test('v-if slot', () => { + expect( + compile(` + + `).code + ).toMatchInlineSnapshot(` + "const { resolveComponent, createSlots } = require(\\"vue\\") + const { _renderComponent } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + const _component_foo = resolveComponent(\\"foo\\") + + _renderComponent(_component_foo, null, createSlots({ _compiled: true }, [ + (_ctx.ok) + ? { + name: \\"named\\", + fn: (_, _push, _parent) => { + _push(\`foo\`) + } + } + : undefined + ]), _parent) + }" + `) + }) + + test('v-for slot', () => { + expect( + compile(` + + `).code + ).toMatchInlineSnapshot(` + "const { resolveComponent, renderList, createSlots } = require(\\"vue\\") + const { _renderComponent, _interpolate } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + const _component_foo = resolveComponent(\\"foo\\") + + _renderComponent(_component_foo, null, createSlots({ _compiled: true }, [ + renderList(_ctx.names, (key) => { + return { + name: key, + fn: ({ msg }, _push, _parent) => { + _push(\`\${_interpolate(msg + key + _ctx.bar)}\`) + } + } + }) + ]), _parent) + }" + `) + }) + }) }) diff --git a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts index 034f1efd4..6916515d8 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformComponent.ts @@ -7,12 +7,33 @@ import { buildProps, ComponentNode, PORTAL, - SUSPENSE + SUSPENSE, + SlotFnBuilder, + createFunctionExpression, + createBlockStatement, + buildSlots, + FunctionExpression, + TemplateChildNode } from '@vue/compiler-dom' import { SSR_RENDER_COMPONENT } from '../runtimeHelpers' -import { SSRTransformContext } from '../ssrCodegenTransform' +import { + SSRTransformContext, + createChildContext, + processChildren +} from '../ssrCodegenTransform' import { isSymbol } from '@vue/shared' +// We need to construct the slot functions in the 1st pass to ensure proper +// scope tracking, but the children of each slot cannot be processed until +// the 2nd pass, so we store the WIP slot functions in a weakmap during the 1st +// pass and complete them in the 2nd pass. +const wipMap = new WeakMap() + +interface WIPSlotEntry { + fn: FunctionExpression + children: TemplateChildNode[] +} + export const ssrTransformComponent: NodeTransform = (node, context) => { if ( node.type !== NodeTypes.ELEMENT || @@ -38,20 +59,37 @@ export const ssrTransformComponent: NodeTransform = (node, context) => { // note we are not passing ssr: true here because for components, v-on // handlers should still be passed - const { props } = buildProps(node, context) + const props = + node.props.length > 0 ? buildProps(node, context).props || `null` : `null` + + const wipEntries: WIPSlotEntry[] = [] + wipMap.set(node, wipEntries) + + const buildSSRSlotFn: SlotFnBuilder = (props, children, loc) => { + // An SSR slot function has the signature of + // (props, _push, _parent) => void + // See server-renderer/src/helpers/renderSlot.ts + const fn = createFunctionExpression( + [props || `_`, `_push`, `_parent`], + undefined, // no return, assign body later + true, // newline + false, // isSlot: pass false since we don't need client scopeId codegen + loc + ) + wipEntries.push({ fn, children }) + return fn + } + + const slots = node.children.length + ? buildSlots(node, context, buildSSRSlotFn).slots + : `null` - // TODO slots // TODO option for slots bail out // TODO scopeId node.ssrCodegenNode = createCallExpression( context.helper(SSR_RENDER_COMPONENT), - [ - component, - props || `null`, - `null`, // TODO slots - `_parent` - ] + [component, props, slots, `_parent`] ) } } @@ -60,5 +98,13 @@ export function ssrProcessComponent( node: ComponentNode, context: SSRTransformContext ) { + // finish up slot function expressions from the 1st pass. + 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) + } context.pushStatement(node.ssrCodegenNode!) } diff --git a/packages/compiler-ssr/src/transforms/ssrVModel.ts b/packages/compiler-ssr/src/transforms/ssrVModel.ts index 1af92879c..f23c62fed 100644 --- a/packages/compiler-ssr/src/transforms/ssrVModel.ts +++ b/packages/compiler-ssr/src/transforms/ssrVModel.ts @@ -41,7 +41,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { const res: DirectiveTransformResult = { props: [] } const defaultProps = [ // default value binding for text type inputs - createObjectProperty(createSimpleExpression(`value`, true), model) + createObjectProperty(`value`, model) ] if (node.tag === 'input') { const type = findProp(node, 'type') @@ -62,7 +62,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { case 'radio': res.props = [ createObjectProperty( - createSimpleExpression(`checked`, true), + `checked`, createCallExpression(context.helper(SSR_LOOSE_EQUAL), [ model, value @@ -73,7 +73,7 @@ export const ssrTransformModel: DirectiveTransform = (dir, node, context) => { case 'checkbox': res.props = [ createObjectProperty( - createSimpleExpression(`checked`, true), + `checked`, createConditionalExpression( createCallExpression(`Array.isArray`, [model]), createCallExpression(context.helper(SSR_LOOSE_CONTAIN), [ diff --git a/packages/compiler-ssr/src/transforms/ssrVShow.ts b/packages/compiler-ssr/src/transforms/ssrVShow.ts index 314d8a234..ecd1c0d60 100644 --- a/packages/compiler-ssr/src/transforms/ssrVShow.ts +++ b/packages/compiler-ssr/src/transforms/ssrVShow.ts @@ -17,13 +17,13 @@ export const ssrTransformShow: DirectiveTransform = (dir, node, context) => { return { props: [ createObjectProperty( - createSimpleExpression(`style`, true), + `style`, createConditionalExpression( dir.exp!, createSimpleExpression(`null`, false), createObjectExpression([ createObjectProperty( - createSimpleExpression(`display`, true), + `display`, createSimpleExpression(`none`, true) ) ]),