diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index cd605832e..dca1989a5 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -147,11 +147,13 @@ export interface ComponentNode extends BaseElementNode { | ComponentCodegenNode | CacheExpression // when cached by v-once | undefined + ssrCodegenNode?: CallExpression } export interface SlotOutletNode extends BaseElementNode { tagType: ElementTypes.SLOT codegenNode: SlotOutletCodegenNode | undefined | CacheExpression // when cached by v-once + ssrCodegenNode?: CallExpression } export interface TemplateNode extends BaseElementNode { diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index e807b0e50..65f3884f4 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -35,14 +35,15 @@ export { transformBind } from './transforms/vBind' // exported for compiler-ssr export { MERGE_PROPS } from './runtimeHelpers' -export { processIfBranches } from './transforms/vIf' -export { processForNode, createForLoopParams } from './transforms/vFor' +export { processIf } from './transforms/vIf' +export { processFor, createForLoopParams } from './transforms/vFor' export { transformExpression, processExpression } from './transforms/transformExpression' export { trackVForSlotScopes, trackSlotScopes } from './transforms/vSlot' export { buildProps } from './transforms/transformElement' +export { processSlotOutlet } from './transforms/transformSlotOutlet' // utility, but need to rewrite typing to avoid dts relying on @vue/shared import { generateCodeFrame as _genCodeFrame } from '@vue/shared' diff --git a/packages/compiler-core/src/transforms/transformSlotOutlet.ts b/packages/compiler-core/src/transforms/transformSlotOutlet.ts index f8862cbbf..da8ab8132 100644 --- a/packages/compiler-core/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-core/src/transforms/transformSlotOutlet.ts @@ -1,78 +1,32 @@ -import { NodeTransform } from '../transform' +import { NodeTransform, TransformContext } from '../transform' import { NodeTypes, CallExpression, createCallExpression, - ExpressionNode + ExpressionNode, + SlotOutletNode } from '../ast' -import { isSlotOutlet } from '../utils' -import { buildProps } from './transformElement' +import { isSlotOutlet, findProp } from '../utils' +import { buildProps, PropsExpression } from './transformElement' import { createCompilerError, ErrorCodes } from '../errors' import { RENDER_SLOT } from '../runtimeHelpers' export const transformSlotOutlet: NodeTransform = (node, context) => { if (isSlotOutlet(node)) { - const { props, children, loc } = node - const $slots = context.prefixIdentifiers ? `_ctx.$slots` : `$slots` - let slotName: string | ExpressionNode = `"default"` + const { children, loc } = node + const { slotName, slotProps } = processSlotOutlet(node, context) - // check for - let nameIndex: number = -1 - for (let i = 0; i < props.length; i++) { - const prop = props[i] - if (prop.type === NodeTypes.ATTRIBUTE) { - if (prop.name === `name` && prop.value) { - // static name="xxx" - slotName = JSON.stringify(prop.value.content) - nameIndex = i - break - } - } else if (prop.name === `bind`) { - const { arg, exp } = prop - if ( - arg && - exp && - arg.type === NodeTypes.SIMPLE_EXPRESSION && - arg.isStatic && - arg.content === `name` - ) { - // dynamic :name="xxx" - slotName = exp - nameIndex = i - break - } - } - } + const slotArgs: CallExpression['arguments'] = [ + context.prefixIdentifiers ? `_ctx.$slots` : `$slots`, + slotName + ] - const slotArgs: CallExpression['arguments'] = [$slots, slotName] - const propsWithoutName = - nameIndex > -1 - ? props.slice(0, nameIndex).concat(props.slice(nameIndex + 1)) - : props - let hasProps = propsWithoutName.length > 0 - if (hasProps) { - const { props: propsExpression, directives } = buildProps( - node, - context, - propsWithoutName - ) - if (directives.length) { - context.onError( - createCompilerError( - ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, - directives[0].loc - ) - ) - } - if (propsExpression) { - slotArgs.push(propsExpression) - } else { - hasProps = false - } + if (slotProps) { + slotArgs.push(slotProps) } if (children.length) { - if (!hasProps) { + if (!slotProps) { slotArgs.push(`{}`) } slotArgs.push(children) @@ -85,3 +39,49 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { ) } } + +interface SlotOutletProcessResult { + slotName: string | ExpressionNode + slotProps: PropsExpression | undefined +} + +export function processSlotOutlet( + node: SlotOutletNode, + context: TransformContext +): SlotOutletProcessResult { + let slotName: string | ExpressionNode = `"default"` + let slotProps: PropsExpression | undefined = undefined + + // check for + const name = findProp(node, 'name') + if (name) { + if (name.type === NodeTypes.ATTRIBUTE && name.value) { + // static name + slotName = JSON.stringify(name.value.content) + } else if (name.type === NodeTypes.DIRECTIVE && name.exp) { + // dynamic name + slotName = name.exp + } + } + + const propsWithoutName = name + ? node.props.filter(p => p !== name) + : node.props + if (propsWithoutName.length > 0) { + const { props, directives } = buildProps(node, context, propsWithoutName) + slotProps = props + if (directives.length) { + context.onError( + createCompilerError( + ErrorCodes.X_V_SLOT_UNEXPECTED_DIRECTIVE_ON_SLOT_OUTLET, + directives[0].loc + ) + ) + } + } + + return { + slotName, + slotProps + } +} diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index d8a8fe1b0..1032c0a56 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -46,7 +46,7 @@ export const transformFor = createStructuralDirectiveTransform( 'for', (node, dir, context) => { const { helper } = context - return processForNode(node, dir, context, forNode => { + return processFor(node, dir, context, forNode => { // create the loop render function expression now, and add the // iterator on exit after all children have been traversed const renderExp = createCallExpression(helper(RENDER_LIST), [ @@ -138,7 +138,7 @@ export const transformFor = createStructuralDirectiveTransform( ) // target-agnostic transform used for both Client and SSR -export function processForNode( +export function processFor( node: ElementNode, dir: DirectiveNode, context: TransformContext, diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts index 69cfb3f1b..5ddf8b176 100644 --- a/packages/compiler-core/src/transforms/vIf.ts +++ b/packages/compiler-core/src/transforms/vIf.ts @@ -41,7 +41,7 @@ import { injectProp } from '../utils' export const transformIf = createStructuralDirectiveTransform( /^(if|else|else-if)$/, (node, dir, context) => { - return processIfBranches(node, dir, context, (ifNode, branch, isRoot) => { + return processIf(node, dir, context, (ifNode, branch, isRoot) => { // Exit callback. Complete the codegenNode when all children have been // transformed. return () => { @@ -72,7 +72,7 @@ export const transformIf = createStructuralDirectiveTransform( ) // target-agnostic transform used for both Client and SSR -export function processIfBranches( +export function processIf( node: ElementNode, dir: DirectiveNode, context: TransformContext, diff --git a/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts b/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts new file mode 100644 index 000000000..1069fa36d --- /dev/null +++ b/packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts @@ -0,0 +1,60 @@ +import { compile } from '../src' + +describe('ssr: ', () => { + test('basic', () => { + expect(compile(``).code).toMatchInlineSnapshot(` + "const { _renderSlot } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + _renderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent) + }" + `) + }) + + test('with name', () => { + expect(compile(``).code).toMatchInlineSnapshot(` + "const { _renderSlot } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + _renderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent) + }" + `) + }) + + test('with dynamic name', () => { + expect(compile(``).code).toMatchInlineSnapshot(` + "const { _renderSlot } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + _renderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent) + }" + `) + }) + + test('with props', () => { + expect(compile(``).code) + .toMatchInlineSnapshot(` + "const { _renderSlot } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + _renderSlot(_ctx.$slots, \\"foo\\", { + p: 1, + bar: \\"2\\" + }, null, _push, _parent) + }" + `) + }) + + test('with fallback', () => { + expect(compile(`some {{ fallback }} content`).code) + .toMatchInlineSnapshot(` + "const { _renderSlot, _interpolate } = require(\\"@vue/server-renderer\\") + + return function ssrRender(_ctx, _push, _parent) { + _renderSlot(_ctx.$slots, \\"default\\", {}, () => { + _push(\`some \${_interpolate(_ctx.fallback)} content\`) + }, _push, _parent) + }" + `) + }) +}) diff --git a/packages/compiler-ssr/src/ssrCodegenTransform.ts b/packages/compiler-ssr/src/ssrCodegenTransform.ts index 8a0758aed..c0b960e62 100644 --- a/packages/compiler-ssr/src/ssrCodegenTransform.ts +++ b/packages/compiler-ssr/src/ssrCodegenTransform.ts @@ -15,8 +15,9 @@ import { } from '@vue/compiler-dom' import { isString, escapeHtml, NO } from '@vue/shared' import { SSR_INTERPOLATE, ssrHelpers } from './runtimeHelpers' -import { processIf } from './transforms/ssrVIf' -import { processFor } from './transforms/ssrVFor' +import { ssrProcessIf } from './transforms/ssrVIf' +import { ssrProcessFor } from './transforms/ssrVFor' +import { ssrProcessSlotOutlet } from './transforms/ssrTransformSlotOutlet' // Because SSR codegen output is completely different from client-side output // (e.g. multiple elements can be concatenated into a single template literal @@ -119,7 +120,7 @@ export function processChildren( } else if (child.tagType === ElementTypes.COMPONENT) { // TODO } else if (child.tagType === ElementTypes.SLOT) { - // TODO + ssrProcessSlotOutlet(child, context) } } else if (child.type === NodeTypes.TEXT) { context.pushStringPart(escapeHtml(child.content)) @@ -128,9 +129,9 @@ export function processChildren( createCallExpression(context.helper(SSR_INTERPOLATE), [child.content]) ) } else if (child.type === NodeTypes.IF) { - processIf(child, context) + ssrProcessIf(child, context) } else if (child.type === NodeTypes.FOR) { - processFor(child, context) + ssrProcessFor(child, context) } } } diff --git a/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts b/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts index 4e734dbc1..bef9c661f 100644 --- a/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts +++ b/packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts @@ -1,3 +1,49 @@ -import { NodeTransform } from '@vue/compiler-dom' +import { + NodeTransform, + isSlotOutlet, + processSlotOutlet, + createCallExpression, + SlotOutletNode, + createFunctionExpression, + createBlockStatement +} from '@vue/compiler-dom' +import { SSR_RENDER_SLOT } from '../runtimeHelpers' +import { + SSRTransformContext, + createChildContext, + processChildren +} from '../ssrCodegenTransform' -export const ssrTransformSlotOutlet: NodeTransform = () => {} +export const ssrTransformSlotOutlet: NodeTransform = (node, context) => { + if (isSlotOutlet(node)) { + const { slotName, slotProps } = processSlotOutlet(node, context) + node.ssrCodegenNode = createCallExpression( + context.helper(SSR_RENDER_SLOT), + [ + `_ctx.$slots`, + slotName, + slotProps || `{}`, + `null`, // fallback content placeholder. + `_push`, + `_parent` + ] + ) + } +} + +export function ssrProcessSlotOutlet( + node: SlotOutletNode, + context: SSRTransformContext +) { + const renderCall = node.ssrCodegenNode! + // has fallback content + if (node.children.length) { + const childContext = createChildContext(context) + processChildren(node.children, childContext) + const fallbackRenderFn = createFunctionExpression([]) + fallbackRenderFn.body = createBlockStatement(childContext.body) + // _renderSlot(slots, name, props, fallback, ...) + renderCall.arguments[3] = fallbackRenderFn + } + context.pushStatement(node.ssrCodegenNode!) +} diff --git a/packages/compiler-ssr/src/transforms/ssrVFor.ts b/packages/compiler-ssr/src/transforms/ssrVFor.ts index 06e687f31..effa8d880 100644 --- a/packages/compiler-ssr/src/transforms/ssrVFor.ts +++ b/packages/compiler-ssr/src/transforms/ssrVFor.ts @@ -1,7 +1,7 @@ import { createStructuralDirectiveTransform, ForNode, - processForNode, + processFor, createCallExpression, createFunctionExpression, createForLoopParams, @@ -18,12 +18,12 @@ import { SSR_RENDER_LIST } from '../runtimeHelpers' // Plugin for the first transform pass, which simply constructs the AST node export const ssrTransformFor = createStructuralDirectiveTransform( 'for', - processForNode + processFor ) // This is called during the 2nd transform pass to construct the SSR-sepcific // codegen nodes. -export function processFor(node: ForNode, context: SSRTransformContext) { +export function ssrProcessFor(node: ForNode, context: SSRTransformContext) { const childContext = createChildContext(context) const needFragmentWrapper = node.children.length !== 1 || node.children[0].type !== NodeTypes.ELEMENT diff --git a/packages/compiler-ssr/src/transforms/ssrVIf.ts b/packages/compiler-ssr/src/transforms/ssrVIf.ts index 905dd5391..23e06db2a 100644 --- a/packages/compiler-ssr/src/transforms/ssrVIf.ts +++ b/packages/compiler-ssr/src/transforms/ssrVIf.ts @@ -1,6 +1,6 @@ import { createStructuralDirectiveTransform, - processIfBranches, + processIf, IfNode, createIfStatement, createBlockStatement, @@ -18,12 +18,12 @@ import { // Plugin for the first transform pass, which simply constructs the AST node export const ssrTransformIf = createStructuralDirectiveTransform( /^(if|else|else-if)$/, - processIfBranches + processIf ) // This is called during the 2nd transform pass to construct the SSR-sepcific // codegen nodes. -export function processIf(node: IfNode, context: SSRTransformContext) { +export function ssrProcessIf(node: IfNode, context: SSRTransformContext) { const [rootBranch] = node.branches const ifStatement = createIfStatement( rootBranch.condition!, diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts index 36a7f2736..97d02db6f 100644 --- a/packages/server-renderer/__tests__/renderToString.spec.ts +++ b/packages/server-renderer/__tests__/renderToString.spec.ts @@ -7,11 +7,8 @@ import { ComponentOptions } from 'vue' import { escapeHtml } from '@vue/shared' -import { - renderToString, - renderComponent, - renderSlot -} from '../src/renderToString' +import { renderToString, renderComponent } from '../src/renderToString' +import { renderSlot } from '../src/helpers/renderSlot' describe('ssr: renderToString', () => { test('should apply app context', async () => { @@ -135,7 +132,16 @@ describe('ssr: renderToString', () => { props: ['msg'], ssrRender(ctx: any, push: any, parent: any) { push(`
`) - renderSlot(ctx.$slots.default, { msg: 'from slot' }, push, parent) + renderSlot( + ctx.$slots, + 'default', + { msg: 'from slot' }, + () => { + push(`fallback`) + }, + push, + parent + ) push(`
`) } } @@ -169,6 +175,19 @@ describe('ssr: renderToString', () => { `from slot` + `` ) + + // test fallback + expect( + await renderToString( + createApp({ + ssrRender(_ctx, push, parent) { + push(`
parent`) + push(renderComponent(Child, { msg: 'hello' }, null, parent)) + push(`
`) + } + }) + ) + ).toBe(`
parent
fallback
`) }) test('nested components with vnode slots', async () => { @@ -176,7 +195,14 @@ describe('ssr: renderToString', () => { props: ['msg'], ssrRender(ctx: any, push: any, parent: any) { push(`
`) - renderSlot(ctx.$slots.default, { msg: 'from slot' }, push, parent) + renderSlot( + ctx.$slots, + 'default', + { msg: 'from slot' }, + null, + push, + parent + ) push(`
`) } } diff --git a/packages/server-renderer/src/helpers/renderSlot.ts b/packages/server-renderer/src/helpers/renderSlot.ts new file mode 100644 index 000000000..e202cda5b --- /dev/null +++ b/packages/server-renderer/src/helpers/renderSlot.ts @@ -0,0 +1,35 @@ +import { Props, PushFn, renderVNodeChildren } from '../renderToString' +import { ComponentInternalInstance, Slot, Slots } from 'vue' + +export type SSRSlots = Record + +export type SSRSlot = ( + props: Props, + push: PushFn, + parentComponent: ComponentInternalInstance | null +) => void + +export function renderSlot( + slots: Slots | SSRSlots, + slotName: string, + slotProps: Props, + fallbackRenderFn: (() => void) | null, + push: PushFn, + parentComponent: ComponentInternalInstance | null = null +) { + const slotFn = slots[slotName] + // template-compiled slots are always rendered as fragments + push(``) + if (slotFn) { + if (slotFn.length > 1) { + // only ssr-optimized slot fns accept more than 1 arguments + slotFn(slotProps, push, parentComponent) + } else { + // normal slot + renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent) + } + } else if (fallbackRenderFn) { + fallbackRenderFn() + } + push(``) +} diff --git a/packages/server-renderer/src/index.ts b/packages/server-renderer/src/index.ts index 3f190b95d..6d7a7c41a 100644 --- a/packages/server-renderer/src/index.ts +++ b/packages/server-renderer/src/index.ts @@ -2,10 +2,8 @@ export { renderToString } from './renderToString' // internal runtime helpers -export { - renderComponent as _renderComponent, - renderSlot as _renderSlot -} from './renderToString' +export { renderComponent as _renderComponent } from './renderToString' +export { renderSlot as _renderSlot } from './helpers/renderSlot' export { renderClass as _renderClass, renderStyle as _renderStyle, diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index 3d0f01244..d7b6626bb 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -11,7 +11,6 @@ import { Portal, ShapeFlags, ssrUtils, - Slot, Slots } from 'vue' import { @@ -23,6 +22,7 @@ import { escapeHtml } from '@vue/shared' import { renderAttrs } from './helpers/renderAttrs' +import { SSRSlots } from './helpers/renderSlot' const { isVNode, @@ -41,8 +41,8 @@ const { type SSRBuffer = SSRBufferItem[] type SSRBufferItem = string | ResolvedSSRBuffer | Promise type ResolvedSSRBuffer = (string | ResolvedSSRBuffer)[] -type PushFn = (item: SSRBufferItem) => void -type Props = Record +export type PushFn = (item: SSRBufferItem) => void +export type Props = Record function createBuffer() { let appendable = false @@ -191,7 +191,7 @@ function renderVNode( } } -function renderVNodeChildren( +export function renderVNodeChildren( push: PushFn, children: VNodeArrayChildren, parentComponent: ComponentInternalInstance | null = null @@ -255,29 +255,3 @@ function renderElement( push(``) } } - -export type SSRSlots = Record - -export type SSRSlot = ( - props: Props, - push: PushFn, - parentComponent: ComponentInternalInstance | null -) => void - -export function renderSlot( - slotFn: Slot | SSRSlot, - slotProps: Props, - push: PushFn, - parentComponent: ComponentInternalInstance | null = null -) { - // template-compiled slots are always rendered as fragments - push(``) - if (slotFn.length > 1) { - // only ssr-optimized slot fns accept more than 1 arguments - slotFn(slotProps, push, parentComponent) - } else { - // normal slot - renderVNodeChildren(push, (slotFn as Slot)(slotProps), parentComponent) - } - push(``) -}